diff --git a/packages/plugin-react/src/jsx-runtime/restore-jsx.spec.ts b/packages/plugin-react/src/jsx-runtime/restore-jsx.spec.ts new file mode 100644 index 00000000000000..adbd7a60599c25 --- /dev/null +++ b/packages/plugin-react/src/jsx-runtime/restore-jsx.spec.ts @@ -0,0 +1,55 @@ +import { restoreJSX } from './restore-jsx' +import * as babel from '@babel/core' + +async function jsx(sourceCode: string) { + const [ast] = await restoreJSX(babel, sourceCode, 'test.js') + if (ast === null) { + return ast + } + const { code } = await babel.transformFromAstAsync(ast, null, { + configFile: false + }) + return code +} +// jsx(`import React__default, { PureComponent, Component, forwardRef, memo, createElement } from 'react'; +// React__default.createElement(Foo)`) +// Tests adapted from: https://github.com/flying-sheep/babel-plugin-transform-react-createelement-to-jsx/blob/63137b6/test/index.js +describe('restore-jsx', () => { + it('should trans to ', async () => { + expect( + await jsx(`import React__default, { PureComponent, Component, forwardRef, memo, createElement } from 'react'; + React__default.createElement(foo)`) + ).toBeNull() + expect( + await jsx(`import React__default, { PureComponent, Component, forwardRef, memo, createElement } from 'react'; + React__default.createElement("h1")`) + ).toMatch(`

;`) + expect( + await jsx(`import React__default, { PureComponent, Component, forwardRef, memo, createElement } from 'react'; + React__default.createElement(Foo)`) + ).toMatch(`;`) + expect( + await jsx(`import React__default, { PureComponent, Component, forwardRef, memo, createElement } from 'react'; + React__default.createElement(Foo.Bar)`) + ).toMatch(`;`) + expect( + await jsx(`import React__default, { PureComponent, Component, forwardRef, memo, createElement } from 'react'; + React__default.createElement(Foo.Bar.Baz)`) + ).toMatch(`;`) + }) + + it('should handle props', async () => { + expect( + await jsx(`import React__default, { PureComponent, Component, forwardRef, memo, createElement } from 'react'; + React__default.createElement(foo, {hi: there})`) + ).toBeNull() + expect( + await jsx(`import React__default, { PureComponent, Component, forwardRef, memo, createElement } from 'react'; + React__default.createElement("h1", {hi: there})`) + ).toMatch(`

;`) + expect( + await jsx(`import React__default, { PureComponent, Component, forwardRef, memo, createElement } from 'react'; + React__default.createElement(Foo, {hi: there})`) + ).toMatch(`;`) + }) +}) diff --git a/packages/plugin-react/src/jsx-runtime/restore-jsx.ts b/packages/plugin-react/src/jsx-runtime/restore-jsx.ts index 268153f4ba5d45..dbc2218d2343ff 100644 --- a/packages/plugin-react/src/jsx-runtime/restore-jsx.ts +++ b/packages/plugin-react/src/jsx-runtime/restore-jsx.ts @@ -20,32 +20,42 @@ export async function restoreJSX( } const [reactAlias, isCommonJS] = parseReactAlias(code) + if (!reactAlias) { return jsxNotFound } - const reactJsxRE = new RegExp( - '\\b' + reactAlias + '\\.(createElement|Fragment)\\b', - 'g' - ) - let hasCompiledJsx = false - code = code.replace(reactJsxRE, (_, prop) => { - hasCompiledJsx = true - // Replace with "React" so JSX can be reverse compiled. - return 'React.' + prop - }) + + 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) + ) + }) if (!hasCompiledJsx) { return jsxNotFound } - // Support modules that use `import {Fragment} from 'react'` - code = code.replace( - /createElement\(Fragment,/g, - 'createElement(React.Fragment,' - ) - babelRestoreJSX ||= import('./babel-restore-jsx') const result = await babel.transformAsync(code, {