Skip to content

Commit

Permalink
fix(plugin-react): wrong substitution causes React is not defined (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
Dunqing committed Aug 10, 2022
1 parent 4e6a77f commit 8a5b575
Show file tree
Hide file tree
Showing 3 changed files with 48 additions and 33 deletions.
13 changes: 10 additions & 3 deletions packages/plugin-react/src/jsx-runtime/babel-restore-jsx.ts
Expand Up @@ -4,6 +4,10 @@
*/
import type * as babel from '@babel/core'

interface PluginOptions {
reactAlias: string
}

/**
* Visitor factory for babel, converting React.createElement(...) to <jsx ...>...</jsx>
*
Expand All @@ -17,7 +21,10 @@ import type * as babel from '@babel/core'
*
* Any of those arguments might also be missing (undefined) and/or invalid.
*/
export default function ({ types: t }: typeof babel): babel.PluginObj {
export default function (
{ types: t }: typeof babel,
{ reactAlias = 'React' }: PluginOptions
): babel.PluginObj {
/**
* Get a `JSXElement` from a `CallExpression`.
* Returns `null` if this impossible.
Expand Down Expand Up @@ -48,7 +55,7 @@ export default function ({ types: t }: typeof babel): babel.PluginObj {
if (
t.isJSXMemberExpression(name) &&
t.isJSXIdentifier(name.object) &&
name.object.name === 'React' &&
name.object.name === reactAlias &&
name.property.name === 'Fragment'
) {
return t.jsxFragment(
Expand Down Expand Up @@ -182,7 +189,7 @@ export default function ({ types: t }: typeof babel): babel.PluginObj {
const isReactCreateElement = (node: any) =>
t.isCallExpression(node) &&
t.isMemberExpression(node.callee) &&
t.isIdentifier(node.callee.object, { name: 'React' }) &&
t.isIdentifier(node.callee.object, { name: reactAlias }) &&
t.isIdentifier(node.callee.property, { name: 'createElement' }) &&
!node.callee.computed

Expand Down
34 changes: 32 additions & 2 deletions packages/plugin-react/src/jsx-runtime/restore-jsx.spec.ts
Expand Up @@ -81,7 +81,10 @@ describe('restore-jsx', () => {
expect(
await jsx(`import React__default, { PureComponent, Component, forwardRef, memo, createElement } from 'react';
React__default.createElement(foo)`)
).toBeNull()
).toMatchInlineSnapshot(`
"import React__default, { PureComponent, Component, forwardRef, memo, createElement } from 'react';
React__default.createElement(foo);"
`)
expect(
await jsx(`import React__default, { PureComponent, Component, forwardRef, memo, createElement } from 'react';
React__default.createElement("h1")`)
Expand All @@ -104,7 +107,12 @@ describe('restore-jsx', () => {
expect(
await jsx(`import React__default, { PureComponent, Component, forwardRef, memo, createElement } from 'react';
React__default.createElement(foo, {hi: there})`)
).toBeNull()
).toMatchInlineSnapshot(`
"import React__default, { PureComponent, Component, forwardRef, memo, createElement } from 'react';
React__default.createElement(foo, {
hi: there
});"
`)
expect(
await jsx(`import React__default, { PureComponent, Component, forwardRef, memo, createElement } from 'react';
React__default.createElement("h1", {hi: there})`)
Expand All @@ -114,4 +122,26 @@ describe('restore-jsx', () => {
React__default.createElement(Foo, {hi: there})`)
).toMatch(`<Foo hi={there} />;`)
})

it('should handle Fragment', async () => {
expect(
await jsx(`import R, { Fragment } from 'react';
R.createElement(Fragment)
`)
).toMatchInlineSnapshot(`
"import R, { Fragment } from 'react';
<Fragment />;"
`)
})

it('should handle Fragment alias', async () => {
expect(
await jsx(`import RA, { Fragment as F } from 'react';
RA.createElement(F, null, RA.createElement(RA.Fragment))
`)
).toMatchInlineSnapshot(`
"import RA, { Fragment as F } from 'react';
<F><></></F>;"
`)
})
})
34 changes: 6 additions & 28 deletions packages/plugin-react/src/jsx-runtime/restore-jsx.ts
Expand Up @@ -33,34 +33,12 @@ export async function restoreJSX(
return jsxNotFound
}

let hasCompiledJsx = false

const fragmentPattern = `\\b${reactAlias}\\.Fragment\\b`
const createElementPattern = `\\b${reactAlias}\\.createElement\\(\\s*([A-Z"'][\\w$.]*["']?)`

// Replace the alias with "React" so JSX can be reverse compiled.
code = code
.replace(new RegExp(fragmentPattern, 'g'), () => {
hasCompiledJsx = true
return 'React.Fragment'
})
.replace(new RegExp(createElementPattern, 'g'), (original, component) => {
if (/^[a-z][\w$]*$/.test(component)) {
// Take care not to replace the alias for `createElement` calls whose
// component is a lowercased variable, since the `restoreJSX` Babel
// plugin leaves them untouched.
return original
}
hasCompiledJsx = true
return (
'React.createElement(' +
// Assume `Fragment` is equivalent to `React.Fragment` so modules
// that use `import {Fragment} from 'react'` are reverse compiled.
(component === 'Fragment' ? 'React.Fragment' : component)
)
})
const reactJsxRE = new RegExp(
`\\b${reactAlias}\\.(createElement|Fragment)\\b`,
'g'
)

if (!hasCompiledJsx) {
if (!reactJsxRE.test(code)) {
return jsxNotFound
}

Expand All @@ -73,7 +51,7 @@ export async function restoreJSX(
parserOpts: {
plugins: ['jsx']
},
plugins: [await getBabelRestoreJSX()]
plugins: [[await getBabelRestoreJSX(), { reactAlias }]]
})

return [result?.ast, isCommonJS]
Expand Down

0 comments on commit 8a5b575

Please sign in to comment.