diff --git a/packages/babel-plugin-react-jsx/__tests__/TransformJSXToReactCreateElement-test.js b/packages/babel-plugin-react-jsx/__tests__/TransformJSXToReactCreateElement-test.js deleted file mode 100644 index d8340607faa91..0000000000000 --- a/packages/babel-plugin-react-jsx/__tests__/TransformJSXToReactCreateElement-test.js +++ /dev/null @@ -1,392 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ -/* eslint-disable quotes */ -'use strict'; - -const babel = require('@babel/core'); -const codeFrame = require('@babel/code-frame'); -const {wrap} = require('jest-snapshot-serializer-raw'); - -function transform(input, options) { - return wrap( - babel.transform(input, { - configFile: false, - plugins: [ - '@babel/plugin-syntax-jsx', - '@babel/plugin-transform-arrow-functions', - ...(options && options.development - ? [ - '@babel/plugin-transform-react-jsx-source', - '@babel/plugin-transform-react-jsx-self', - ] - : []), - [ - './packages/babel-plugin-react-jsx', - { - development: __DEV__, - useBuiltIns: true, - useCreateElement: true, - ...options, - }, - ], - ], - }).code - ); -} - -describe('transform react to jsx', () => { - it('fragment with no children', () => { - expect(transform(`var x = <>`)).toMatchSnapshot(); - }); - - it('React.Fragment to set keys and source', () => { - expect( - transform(`var x =
`, { - development: true, - }) - ).toMatchSnapshot(); - }); - - it('normal fragments not to set key and source', () => { - expect( - transform(`var x = <>
`, { - development: true, - }) - ).toMatchSnapshot(); - }); - - it('should properly handle comments adjacent to children', () => { - expect( - transform(` - var x = ( -
- {/* A comment at the beginning */} - {/* A second comment at the beginning */} - - {/* A nested comment */} - - {/* A sandwiched comment */} -
- {/* A comment at the end */} - {/* A second comment at the end */} -
- ); - `) - ).toMatchSnapshot(); - }); - - it('adds appropriate new lines when using spread attribute', () => { - expect(transform(``)).toMatchSnapshot(); - }); - - it('arrow functions', () => { - expect( - transform(` - var foo = function () { - return () => ; - }; - - var bar = function () { - return () => ; - }; - - `) - ).toMatchSnapshot(); - }); - - it('assignment', () => { - expect( - transform(`var div = `) - ).toMatchSnapshot(); - }); - - it('concatenates adjacent string literals', () => { - expect( - transform(` - var x = -
- foo - {"bar"} - baz -
- buz - bang -
- qux - {null} - quack -
- `) - ).toMatchSnapshot(); - }); - - it('should allow constructor as prop', () => { - expect(transform(`;`)).toMatchSnapshot(); - }); - - it('should allow deeper js namespacing', () => { - expect( - transform(`;`) - ).toMatchSnapshot(); - }); - - it('should allow elements as attributes', () => { - expect(transform(`
/>`)).toMatchSnapshot(); - }); - - it('should allow js namespacing', () => { - expect(transform(`;`)).toMatchSnapshot(); - }); - - it('should allow nested fragments', () => { - expect( - transform(` -
- < > - <> - Hello - world - - <> - Goodbye - world - - -
- `) - ).toMatchSnapshot(); - }); - - it('should avoid wrapping in extra parens if not needed', () => { - expect( - transform(` - var x =
- -
; - - var x =
- {props.children} -
; - - var x = - {props.children} - ; - - var x = - - ; - `) - ).toMatchSnapshot(); - }); - - it('should convert simple tags', () => { - expect(transform(`var x =
;`)).toMatchSnapshot(); - }); - - it('should convert simple text', () => { - expect(transform(`var x =
text
;`)).toMatchSnapshot(); - }); - - it('should disallow spread children', () => { - let _error; - const code = `
{...children}
;`; - try { - transform(code); - } catch (error) { - _error = error; - } - expect(_error).toEqual( - new SyntaxError( - 'unknown: Spread children are not supported in React.' + - '\n' + - codeFrame.codeFrameColumns( - code, - {start: {line: 1, column: 6}, end: {line: 1, column: 19}}, - {highlightCode: true} - ) - ) - ); - }); - - it('should escape xhtml jsxattribute', () => { - expect( - transform(` -
; -
; -
; - `) - ).toMatchSnapshot(); - }); - - it('should escape xhtml jsxtext', () => { - /* eslint-disable no-irregular-whitespace */ - expect( - transform(` -
wow
; -
wôw
; - -
w & w
; -
w & w
; - -
w   w
; -
this should not parse as unicode: \u00a0
; -
this should parse as nbsp:  
; -
this should parse as unicode: {'\u00a0 '}
; - -
w < w
; - `) - ).toMatchSnapshot(); - /*eslint-enable */ - }); - - it('should handle attributed elements', () => { - expect( - transform(` - var HelloMessage = React.createClass({ - render: function() { - return
Hello {this.props.name}
; - } - }); - - React.render( - Sebastian - - } />, mountNode); - `) - ).toMatchSnapshot(); - }); - - it('should handle has own property correctly', () => { - expect( - transform(`testing;`) - ).toMatchSnapshot(); - }); - - it('should have correct comma in nested children', () => { - expect( - transform(` - var x =
-

- {foo}
{bar}
-
-
; - `) - ).toMatchSnapshot(); - }); - - it('should insert commas after expressions before whitespace', () => { - expect( - transform(` - var x = -
-
- `) - ).toMatchSnapshot(); - }); - - it('should not add quotes to identifier names', () => { - expect( - transform(`var e = ;`) - ).toMatchSnapshot(); - }); - - it('should not strip nbsp even couple with other whitespace', () => { - expect(transform(`
 
;`)).toMatchSnapshot(); - }); - - it('should not strip tags with a single child of nbsp', () => { - expect(transform(`
 
;`)).toMatchSnapshot(); - }); - - it('should properly handle comments between props', () => { - expect( - transform(` - var x = ( -
- -
- ); - `) - ).toMatchSnapshot(); - }); - - it('should quote jsx attributes', () => { - expect( - transform(``) - ).toMatchSnapshot(); - }); - - it('should support xml namespaces if flag', () => { - expect( - transform('', {throwIfNamespace: false}) - ).toMatchSnapshot(); - }); - - it('should throw error namespaces if not flag', () => { - let _error; - const code = ``; - try { - transform(code); - } catch (error) { - _error = error; - } - expect(_error).toEqual( - new SyntaxError( - "unknown: Namespace tags are not supported by default. React's " + - "JSX doesn't support namespace tags. You can turn on the " + - "'throwIfNamespace' flag to bypass this warning." + - '\n' + - codeFrame.codeFrameColumns( - code, - {start: {line: 1, column: 2}, end: {line: 1, column: 9}}, - {highlightCode: true} - ) - ) - ); - }); - - it('should transform known hyphenated tags', () => { - expect(transform(``)).toMatchSnapshot(); - }); - - it('wraps props in react spread for first spread attributes', () => { - expect(transform(``)).toMatchSnapshot(); - }); - - it('wraps props in react spread for last spread attributes', () => { - expect(transform(``)).toMatchSnapshot(); - }); - - it('wraps props in react spread for middle spread attributes', () => { - expect(transform(``)).toMatchSnapshot(); - }); - - it('useBuiltIns false uses extend instead of Object.assign', () => { - expect( - transform(``, {useBuiltIns: false}) - ).toMatchSnapshot(); - }); -}); diff --git a/packages/babel-plugin-react-jsx/__tests__/TransformJSXToReactJSX-test.js b/packages/babel-plugin-react-jsx/__tests__/TransformJSXToReactJSX-test.js index ce7179e2b0ac1..e01259c851a39 100644 --- a/packages/babel-plugin-react-jsx/__tests__/TransformJSXToReactJSX-test.js +++ b/packages/babel-plugin-react-jsx/__tests__/TransformJSXToReactJSX-test.js @@ -11,14 +11,15 @@ const babel = require('@babel/core'); const codeFrame = require('@babel/code-frame'); const {wrap} = require('jest-snapshot-serializer-raw'); -function transform(input, options) { +function transform(input, pluginOpts, babelOpts) { return wrap( babel.transform(input, { configFile: false, + sourceType: 'module', plugins: [ '@babel/plugin-syntax-jsx', '@babel/plugin-transform-arrow-functions', - ...(options && options.development + ...(pluginOpts && pluginOpts.development ? [ '@babel/plugin-transform-react-jsx-source', '@babel/plugin-transform-react-jsx-self', @@ -29,15 +30,380 @@ function transform(input, options) { { useBuiltIns: true, useCreateElement: false, - ...options, + ...pluginOpts, }, ], ], + ...babelOpts, }).code ); } describe('transform react to jsx', () => { + it('auto import pragma overrides regular pragma', () => { + expect( + transform( + `/** @jsxAutoImport defaultExport */ + var x =
+ `, + { + autoImport: 'namespace', + importSource: 'foobar', + } + ) + ).toMatchSnapshot(); + }); + + it('import source pragma overrides regular pragma', () => { + expect( + transform( + `/** @jsxImportSource baz */ + var x =
+ `, + { + autoImport: 'namespace', + importSource: 'foobar', + } + ) + ).toMatchSnapshot(); + }); + + it('multiple pragmas work', () => { + expect( + transform( + `/** Some comment here + * @jsxImportSource baz + * @jsxAutoImport defaultExport + */ + var x =
+ `, + { + autoImport: 'namespace', + importSource: 'foobar', + } + ) + ).toMatchSnapshot(); + }); + + it('throws error when sourceType is module and autoImport is require', () => { + const code = `var x =
`; + expect(() => { + transform(code, { + autoImport: 'require', + }); + }).toThrow( + 'Babel `sourceType` must be set to `script` for autoImport ' + + 'to use `require` syntax. See Babel `sourceType` for details.\n' + + codeFrame.codeFrameColumns( + code, + {start: {line: 1, column: 1}, end: {line: 1, column: 28}}, + {highlightCode: true} + ) + ); + }); + + it('throws error when sourceType is script and autoImport is not require', () => { + const code = `var x =
`; + expect(() => { + transform( + code, + { + autoImport: 'namespace', + }, + {sourceType: 'script'} + ); + }).toThrow( + 'Babel `sourceType` must be set to `module` for autoImport ' + + 'to use `namespace` syntax. See Babel `sourceType` for details.\n' + + codeFrame.codeFrameColumns( + code, + {start: {line: 1, column: 1}, end: {line: 1, column: 28}}, + {highlightCode: true} + ) + ); + }); + + it("auto import that doesn't exist should throw error", () => { + const code = `var x =
`; + expect(() => { + transform(code, { + autoImport: 'foo', + }); + }).toThrow( + 'autoImport must be one of the following: none, require, namespace, defaultExport, namedExports\n' + + codeFrame.codeFrameColumns( + code, + {start: {line: 1, column: 1}, end: {line: 1, column: 28}}, + {highlightCode: true} + ) + ); + }); + + it('auto import can specify source', () => { + expect( + transform(`var x =
`, { + autoImport: 'namespace', + importSource: 'foobar', + }) + ).toMatchSnapshot(); + }); + + it('auto import require', () => { + expect( + transform( + `var x = ( + <> +
+
+
+
+
+
+ + );`, + { + autoImport: 'require', + }, + { + sourceType: 'script', + } + ) + ).toMatchSnapshot(); + }); + + it('auto import namespace', () => { + expect( + transform( + `var x = ( + <> +
+
+
+
+
+
+ + );`, + { + autoImport: 'namespace', + } + ) + ).toMatchSnapshot(); + }); + + it('auto import default', () => { + expect( + transform( + `var x = ( + <> +
+
+
+
+
+
+ + );`, + { + autoImport: 'defaultExport', + } + ) + ).toMatchSnapshot(); + }); + + it('auto import named exports', () => { + expect( + transform( + `var x = ( + <> +
+
+
+
+
+
+ + );`, + { + autoImport: 'namedExports', + } + ) + ).toMatchSnapshot(); + }); + + it('auto import with no JSX', () => { + expect( + transform( + `var foo = "
"`, + { + autoImport: 'require', + }, + { + sourceType: 'script', + } + ) + ).toMatchSnapshot(); + }); + + it('complicated scope require', () => { + expect( + transform( + ` + const Bar = () => { + const Foo = () => { + const Component = ({thing, ..._react}) => { + if (!thing) { + var _react2 = "something useless"; + var b = _react3(); + var c = _react5(); + var jsx = 1; + var _jsx = 2; + return
; + }; + return ; + }; + } + } + `, + { + autoImport: 'require', + }, + { + sourceType: 'script', + } + ) + ).toMatchSnapshot(); + }); + + it('complicated scope named exports', () => { + expect( + transform( + ` + const Bar = () => { + const Foo = () => { + const Component = ({thing, ..._react}) => { + if (!thing) { + var _react2 = "something useless"; + var b = _react3(); + var jsx = 1; + var _jsx = 2; + return
; + }; + return ; + }; + } + } + `, + { + autoImport: 'namedExports', + } + ) + ).toMatchSnapshot(); + }); + + it('auto import in dev', () => { + expect( + transform( + `var x = ( + <> +
+
+
+
+
+
+ + );`, + { + autoImport: 'namedExports', + development: true, + } + ) + ).toMatchSnapshot(); + }); + + it('auto import none', () => { + expect( + transform( + `var x = ( + <> +
+
+
+
+
+
+ + );`, + { + autoImport: 'none', + } + ) + ).toMatchSnapshot(); + }); + + it('auto import undefined', () => { + expect( + transform( + `var x = ( + <> +
+
+
+
+
+
+ + );` + ) + ).toMatchSnapshot(); + }); + + it('auto import with namespaces already defined', () => { + expect( + transform( + ` + import * as _react from "foo"; + const react = _react(1); + const _react1 = react; + const _react2 = react; + var x = ( +
+
+
+
+
+
+ );`, + { + autoImport: 'namespace', + } + ) + ).toMatchSnapshot(); + }); + + it('auto import with react already defined', () => { + expect( + transform( + ` + import * as react from "react"; + var y = react.createElement("div", {foo: 1}); + var x = ( +
+
+
+
+
+
+ );`, + + { + autoImport: 'namespace', + } + ) + ).toMatchSnapshot(); + }); + it('fragment with no children', () => { expect(transform(`var x = <>`)).toMatchSnapshot(); }); diff --git a/packages/babel-plugin-react-jsx/__tests__/__snapshots__/TransformJSXToReactCreateElement-test.js.snap b/packages/babel-plugin-react-jsx/__tests__/__snapshots__/TransformJSXToReactCreateElement-test.js.snap deleted file mode 100644 index a74c7e1d15e82..0000000000000 --- a/packages/babel-plugin-react-jsx/__tests__/__snapshots__/TransformJSXToReactCreateElement-test.js.snap +++ /dev/null @@ -1,213 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`transform react to jsx React.Fragment to set keys and source 1`] = ` -var _jsxFileName = ""; -var x = React.createElement(React.Fragment, { - key: "foo", - __source: { - fileName: _jsxFileName, - lineNumber: 1 - }, - __self: this -}, React.createElement("div", { - __source: { - fileName: _jsxFileName, - lineNumber: 1 - }, - __self: this -})); -`; - -exports[`transform react to jsx adds appropriate new lines when using spread attribute 1`] = ` -React.createElement(Component, Object.assign({}, props, { - sound: "moo" -})); -`; - -exports[`transform react to jsx arrow functions 1`] = ` -var foo = function () { - var _this = this; - - return function () { - return React.createElement(_this, null); - }; -}; - -var bar = function () { - var _this2 = this; - - return function () { - return React.createElement(_this2.foo, null); - }; -}; -`; - -exports[`transform react to jsx assignment 1`] = ` -var div = React.createElement(Component, Object.assign({}, props, { - foo: "bar" -})); -`; - -exports[`transform react to jsx concatenates adjacent string literals 1`] = `var x = React.createElement("div", null, "foo", "bar", "baz", React.createElement("div", null, "buz bang"), "qux", null, "quack");`; - -exports[`transform react to jsx fragment with no children 1`] = `var x = React.createElement(React.Fragment, null);`; - -exports[`transform react to jsx normal fragments not to set key and source 1`] = ` -var _jsxFileName = ""; -var x = React.createElement(React.Fragment, null, React.createElement("div", { - __source: { - fileName: _jsxFileName, - lineNumber: 1 - }, - __self: this -})); -`; - -exports[`transform react to jsx should allow constructor as prop 1`] = ` -React.createElement(Component, { - constructor: "foo" -}); -`; - -exports[`transform react to jsx should allow deeper js namespacing 1`] = `React.createElement(Namespace.DeepNamespace.Component, null);`; - -exports[`transform react to jsx should allow elements as attributes 1`] = ` -React.createElement("div", { - attr: React.createElement("div", null) -}); -`; - -exports[`transform react to jsx should allow js namespacing 1`] = `React.createElement(Namespace.Component, null);`; - -exports[`transform react to jsx should allow nested fragments 1`] = `React.createElement("div", null, React.createElement(React.Fragment, null, React.createElement(React.Fragment, null, React.createElement("span", null, "Hello"), React.createElement("span", null, "world")), React.createElement(React.Fragment, null, React.createElement("span", null, "Goodbye"), React.createElement("span", null, "world"))));`; - -exports[`transform react to jsx should avoid wrapping in extra parens if not needed 1`] = ` -var x = React.createElement("div", null, React.createElement(Component, null)); -var x = React.createElement("div", null, props.children); -var x = React.createElement(Composite, null, props.children); -var x = React.createElement(Composite, null, React.createElement(Composite2, null)); -`; - -exports[`transform react to jsx should convert simple tags 1`] = `var x = React.createElement("div", null);`; - -exports[`transform react to jsx should convert simple text 1`] = `var x = React.createElement("div", null, "text");`; - -exports[`transform react to jsx should escape xhtml jsxattribute 1`] = ` -React.createElement("div", { - id: "w\\xF4w" -}); -React.createElement("div", { - id: "w" -}); -React.createElement("div", { - id: "w < w" -}); -`; - -exports[`transform react to jsx should escape xhtml jsxtext 1`] = ` -React.createElement("div", null, "wow"); -React.createElement("div", null, "w\\xF4w"); -React.createElement("div", null, "w & w"); -React.createElement("div", null, "w & w"); -React.createElement("div", null, "w \\xA0 w"); -React.createElement("div", null, "this should not parse as unicode: \\xA0"); -React.createElement("div", null, "this should parse as nbsp: \\xA0 "); -React.createElement("div", null, "this should parse as unicode: ", '  '); -React.createElement("div", null, "w < w"); -`; - -exports[`transform react to jsx should handle attributed elements 1`] = ` -var HelloMessage = React.createClass({ - render: function () { - return React.createElement("div", null, "Hello ", this.props.name); - } -}); -React.render(React.createElement(HelloMessage, { - name: React.createElement("span", null, "Sebastian") -}), mountNode); -`; - -exports[`transform react to jsx should handle has own property correctly 1`] = `React.createElement("hasOwnProperty", null, "testing");`; - -exports[`transform react to jsx should have correct comma in nested children 1`] = `var x = React.createElement("div", null, React.createElement("div", null, React.createElement("br", null)), React.createElement(Component, null, foo, React.createElement("br", null), bar), React.createElement("br", null));`; - -exports[`transform react to jsx should insert commas after expressions before whitespace 1`] = ` -var x = React.createElement("div", { - attr1: "foo" + "bar", - attr2: "foo" + "bar" + "baz" + "bug", - attr3: "foo" + "bar" + "baz" + "bug", - attr4: "baz" -}); -`; - -exports[`transform react to jsx should not add quotes to identifier names 1`] = ` -var e = React.createElement(F, { - aaa: true, - new: true, - const: true, - var: true, - default: true, - "foo-bar": true -}); -`; - -exports[`transform react to jsx should not strip nbsp even couple with other whitespace 1`] = `React.createElement("div", null, "\\xA0 ");`; - -exports[`transform react to jsx should not strip tags with a single child of nbsp 1`] = `React.createElement("div", null, "\\xA0");`; - -exports[`transform react to jsx should properly handle comments adjacent to children 1`] = `var x = React.createElement("div", null, React.createElement("span", null), React.createElement("br", null));`; - -exports[`transform react to jsx should properly handle comments between props 1`] = ` -var x = React.createElement("div", { - /* a multi-line - comment */ - attr1: "foo" -}, React.createElement("span", { - // a double-slash comment - attr2: "bar" -})); -`; - -exports[`transform react to jsx should quote jsx attributes 1`] = ` -React.createElement("button", { - "data-value": "a value" -}, "Button"); -`; - -exports[`transform react to jsx should support xml namespaces if flag 1`] = ` -React.createElement("f:image", { - "n:attr": true -}); -`; - -exports[`transform react to jsx should transform known hyphenated tags 1`] = `React.createElement("font-face", null);`; - -exports[`transform react to jsx useBuiltIns false uses extend instead of Object.assign 1`] = ` -function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } - -React.createElement(Component, _extends({ - y: 2 -}, x)); -`; - -exports[`transform react to jsx wraps props in react spread for first spread attributes 1`] = ` -React.createElement(Component, Object.assign({}, x, { - y: 2, - z: true -})); -`; - -exports[`transform react to jsx wraps props in react spread for last spread attributes 1`] = ` -React.createElement(Component, Object.assign({ - y: 2, - z: true -}, x)); -`; - -exports[`transform react to jsx wraps props in react spread for middle spread attributes 1`] = ` -React.createElement(Component, Object.assign({ - y: 2 -}, x, { - z: true -})); -`; diff --git a/packages/babel-plugin-react-jsx/__tests__/__snapshots__/TransformJSXToReactJSX-test.js.snap b/packages/babel-plugin-react-jsx/__tests__/__snapshots__/TransformJSXToReactJSX-test.js.snap index 22b83d9ec1513..cb8a89cc046de 100644 --- a/packages/babel-plugin-react-jsx/__tests__/__snapshots__/TransformJSXToReactJSX-test.js.snap +++ b/packages/babel-plugin-react-jsx/__tests__/__snapshots__/TransformJSXToReactJSX-test.js.snap @@ -38,6 +38,230 @@ var div = React.jsx(Component, Object.assign({}, props, { })); `; +exports[`transform react to jsx auto import can specify source 1`] = ` +import * as _foobar from "foobar"; + +var x = _foobar.jsx("div", { + children: _foobar.jsx("span", {}) +}); +`; + +exports[`transform react to jsx auto import default 1`] = ` +import _default from "react"; + +var x = _default.jsx(_default.Fragment, { + children: _default.jsxs("div", { + children: [_default.jsx("div", {}, "1"), _default.jsx("div", { + meow: "wolf" + }, "2"), _default.jsx("div", {}, "3"), _default.createElement("div", Object.assign({}, props, { + key: "4" + }))] + }) +}); +`; + +exports[`transform react to jsx auto import in dev 1`] = ` +import { createElement as _createElement } from "react"; +import { jsxDEV as _jsxDEV } from "react"; +import { Fragment as _Fragment } from "react"; +var _jsxFileName = ""; + +var x = _jsxDEV(_Fragment, { + children: _jsxDEV("div", { + children: [_jsxDEV("div", {}, "1", false, { + fileName: _jsxFileName, + lineNumber: 4 + }, this), _jsxDEV("div", { + meow: "wolf" + }, "2", false, { + fileName: _jsxFileName, + lineNumber: 5 + }, this), _jsxDEV("div", {}, "3", false, { + fileName: _jsxFileName, + lineNumber: 6 + }, this), _createElement("div", Object.assign({}, props, { + key: "4", + __source: { + fileName: _jsxFileName, + lineNumber: 7 + }, + __self: this + }))] + }, undefined, true, { + fileName: _jsxFileName, + lineNumber: 3 + }, this) +}, undefined, false); +`; + +exports[`transform react to jsx auto import named exports 1`] = ` +import { createElement as _createElement } from "react"; +import { jsx as _jsx } from "react"; +import { jsxs as _jsxs } from "react"; +import { Fragment as _Fragment } from "react"; + +var x = _jsx(_Fragment, { + children: _jsxs("div", { + children: [_jsx("div", {}, "1"), _jsx("div", { + meow: "wolf" + }, "2"), _jsx("div", {}, "3"), _createElement("div", Object.assign({}, props, { + key: "4" + }))] + }) +}); +`; + +exports[`transform react to jsx auto import namespace 1`] = ` +import * as _react from "react"; + +var x = _react.jsx(_react.Fragment, { + children: _react.jsxs("div", { + children: [_react.jsx("div", {}, "1"), _react.jsx("div", { + meow: "wolf" + }, "2"), _react.jsx("div", {}, "3"), _react.createElement("div", Object.assign({}, props, { + key: "4" + }))] + }) +}); +`; + +exports[`transform react to jsx auto import none 1`] = ` +var x = React.jsx(React.Fragment, { + children: React.jsxs("div", { + children: [React.jsx("div", {}, "1"), React.jsx("div", { + meow: "wolf" + }, "2"), React.jsx("div", {}, "3"), React.createElement("div", Object.assign({}, props, { + key: "4" + }))] + }) +}); +`; + +exports[`transform react to jsx auto import pragma overrides regular pragma 1`] = ` +import _default from "foobar"; + +/** @jsxAutoImport defaultExport */ +var x = _default.jsx("div", { + children: _default.jsx("span", {}) +}); +`; + +exports[`transform react to jsx auto import require 1`] = ` +var _react = require("react"); + +var x = _react.jsx(_react.Fragment, { + children: _react.jsxs("div", { + children: [_react.jsx("div", {}, "1"), _react.jsx("div", { + meow: "wolf" + }, "2"), _react.jsx("div", {}, "3"), _react.createElement("div", Object.assign({}, props, { + key: "4" + }))] + }) +}); +`; + +exports[`transform react to jsx auto import undefined 1`] = ` +var x = React.jsx(React.Fragment, { + children: React.jsxs("div", { + children: [React.jsx("div", {}, "1"), React.jsx("div", { + meow: "wolf" + }, "2"), React.jsx("div", {}, "3"), React.createElement("div", Object.assign({}, props, { + key: "4" + }))] + }) +}); +`; + +exports[`transform react to jsx auto import with namespaces already defined 1`] = ` +import * as _react3 from "react"; +import * as _react from "foo"; + +const react = _react(1); + +const _react1 = react; +const _react2 = react; + +var x = _react3.jsxs("div", { + children: [_react3.jsx("div", {}, "1"), _react3.jsx("div", { + meow: "wolf" + }, "2"), _react3.jsx("div", {}, "3"), _react3.createElement("div", Object.assign({}, props, { + key: "4" + }))] +}); +`; + +exports[`transform react to jsx auto import with no JSX 1`] = `var foo = "
";`; + +exports[`transform react to jsx auto import with react already defined 1`] = ` +import * as _react from "react"; +import * as react from "react"; +var y = react.createElement("div", { + foo: 1 +}); + +var x = _react.jsxs("div", { + children: [_react.jsx("div", {}, "1"), _react.jsx("div", { + meow: "wolf" + }, "2"), _react.jsx("div", {}, "3"), _react.createElement("div", Object.assign({}, props, { + key: "4" + }))] +}); +`; + +exports[`transform react to jsx complicated scope named exports 1`] = ` +import { jsx as _jsx2 } from "react"; + +const Bar = function () { + const Foo = function () { + const Component = function ({ + thing, + ..._react + }) { + if (!thing) { + var _react2 = "something useless"; + + var b = _react3(); + + var jsx = 1; + var _jsx = 2; + return _jsx2("div", {}); + } + + ; + return _jsx2("span", {}); + }; + }; +}; +`; + +exports[`transform react to jsx complicated scope require 1`] = ` +var _react4 = require("react"); + +const Bar = function () { + const Foo = function () { + const Component = function ({ + thing, + ..._react + }) { + if (!thing) { + var _react2 = "something useless"; + + var b = _react3(); + + var c = _react5(); + + var jsx = 1; + var _jsx = 2; + return _react4.jsx("div", {}); + } + + ; + return _react4.jsx("span", {}); + }; + }; +}; +`; + exports[`transform react to jsx concatenates adjacent string literals 1`] = ` var x = React.jsxs("div", { children: ["foo", "bar", "baz", React.jsx("div", { @@ -92,6 +316,27 @@ var x = React.jsxDEV(React.Fragment, { exports[`transform react to jsx fragments to set keys 1`] = `var x = React.jsx(React.Fragment, {}, "foo");`; +exports[`transform react to jsx import source pragma overrides regular pragma 1`] = ` +import * as _baz from "baz"; + +/** @jsxImportSource baz */ +var x = _baz.jsx("div", { + children: _baz.jsx("span", {}) +}); +`; + +exports[`transform react to jsx multiple pragmas work 1`] = ` +import _default from "baz"; + +/** Some comment here + * @jsxImportSource baz + * @jsxAutoImport defaultExport + */ +var x = _default.jsx("div", { + children: _default.jsx("span", {}) +}); +`; + exports[`transform react to jsx nonStatic children 1`] = ` var _jsxFileName = ""; var x = React.jsxDEV("div", { diff --git a/packages/babel-plugin-react-jsx/package.json b/packages/babel-plugin-react-jsx/package.json index 41243452d4f17..c33a20e06965b 100644 --- a/packages/babel-plugin-react-jsx/package.json +++ b/packages/babel-plugin-react-jsx/package.json @@ -5,8 +5,8 @@ "description": "@babel/plugin-transform-react-jsx", "main": "index.js", "dependencies": { + "@babel/helper-module-imports": "^7.0.0", "esutils": "^2.0.0" - }, "files": [ "README.md", diff --git a/packages/babel-plugin-react-jsx/src/TransformJSXToReactBabelPlugin.js b/packages/babel-plugin-react-jsx/src/TransformJSXToReactBabelPlugin.js index 308fc7116f8c0..2d8ebc8c6ec99 100644 --- a/packages/babel-plugin-react-jsx/src/TransformJSXToReactBabelPlugin.js +++ b/packages/babel-plugin-react-jsx/src/TransformJSXToReactBabelPlugin.js @@ -24,6 +24,50 @@ 'use strict'; const esutils = require('esutils'); +const { + isModule, + addNamespace, + addNamed, + addDefault, +} = require('@babel/helper-module-imports'); + +// These are all the valid auto import types (under the config autoImport) +// that a user can specific +const IMPORT_TYPES = { + none: 'none', // default option. Will not import anything + require: 'require', // var _react = require("react"); + namespace: 'namespace', // import * as _react from "react"; + defaultExport: 'defaultExport', // import _default from "react"; + namedExports: 'namedExports', // import { jsx } from "react"; +}; + +const JSX_AUTO_IMPORT_ANNOTATION_REGEX = /\*?\s*@jsxAutoImport\s+([^\s]+)/; +const JSX_IMPORT_SOURCE_ANNOTATION_REGEX = /\*?\s*@jsxImportSource\s+([^\s]+)/; + +// We want to use React.createElement, even in the case of +// jsx, for
to distinguish it +// from
. This is an intermediary +// step while we deprecate key spread from props. Afterwards, +// we will remove createElement entirely +function shouldUseCreateElement(path, types) { + const openingPath = path.get('openingElement'); + const attributes = openingPath.node.attributes; + + let seenPropsSpread = false; + for (let i = 0; i < attributes.length; i++) { + const attr = attributes[i]; + if ( + seenPropsSpread && + types.isJSXAttribute(attr) && + attr.name.name === 'key' + ) { + return true; + } else if (types.isJSXSpreadAttribute(attr)) { + seenPropsSpread = true; + } + } + return false; +} function helper(babel, opts) { const {types: t} = babel; @@ -52,7 +96,7 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, visitor.JSXElement = { exit(path, file) { let callExpr; - if (file.opts.useCreateElement || shouldUseCreateElement(path)) { + if (shouldUseCreateElement(path, t)) { callExpr = buildCreateElementCall(path, file); } else { callExpr = buildJSXElementCall(path, file); @@ -71,12 +115,7 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, 'Fragment tags are only supported in React 16 and up.', ); } - let callExpr; - if (file.opts.useCreateElement) { - callExpr = buildCreateElementFragmentCall(path, file); - } else { - callExpr = buildJSXFragmentCall(path, file); - } + let callExpr = buildJSXFragmentCall(path, file); if (callExpr) { path.replaceWith(t.inherits(callExpr, path.node)); @@ -147,31 +186,6 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, return t.inherits(t.objectProperty(node.name, value), node); } - // We want to use React.createElement, even in the case of - // jsx, for
to distinguish it - // from
. This is an intermediary - // step while we deprecate key spread from props. Afterwards, - // we will remove createElement entirely - function shouldUseCreateElement(path) { - const openingPath = path.get('openingElement'); - const attributes = openingPath.node.attributes; - - let seenPropsSpread = false; - for (let i = 0; i < attributes.length; i++) { - const attr = attributes[i]; - if ( - seenPropsSpread && - t.isJSXAttribute(attr) && - attr.name.name === 'key' - ) { - return true; - } else if (t.isJSXSpreadAttribute(attr)) { - seenPropsSpread = true; - } - } - return false; - } - // Builds JSX into: // Production: React.jsx(type, arguments, key) // Development: React.jsxDEV(type, arguments, key, isStaticChildren, source, self) @@ -262,6 +276,7 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, if (opts.post) { opts.post(state, file); } + return ( state.call || t.callExpression( @@ -561,38 +576,6 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, return attribs; } - - function buildCreateElementFragmentCall(path, file) { - if (opts.filter && !opts.filter(path.node, file)) { - return; - } - - const openingPath = path.get('openingElement'); - openingPath.parent.children = t.react.buildChildren(openingPath.parent); - - const args = []; - const tagName = null; - const tagExpr = file.get('jsxFragIdentifier')(); - - const state = { - tagExpr: tagExpr, - tagName: tagName, - args: args, - }; - - if (opts.pre) { - opts.pre(state, file); - } - - // no attributes are allowed with <> syntax - args.push(t.nullLiteral(), ...path.node.children); - - if (opts.post) { - opts.post(state, file); - } - - return state.call || t.callExpression(state.oldCallee, args); - } } module.exports = function(babel) { @@ -623,25 +606,183 @@ module.exports = function(babel) { }, }); - visitor.Program = { - enter(path, state) { - state.set( - 'oldJSXIdentifier', - createIdentifierParser('React.createElement'), + const createIdentifierName = (path, autoImport, name, importName) => { + if (autoImport === IMPORT_TYPES.none) { + return `React.${name}`; + } else if (autoImport === IMPORT_TYPES.namedExports) { + if (importName) { + const identifierName = `${importName[name]}`; + return identifierName; + } + } else { + return `${importName}.${name}`; + } + }; + + function getImportNames(parentPath, state) { + const imports = {}; + parentPath.traverse({ + JSXElement(path) { + if (shouldUseCreateElement(path, t)) { + imports.createElement = true; + } else if (path.node.children.length > 1) { + const importName = state.development ? 'jsxDEV' : 'jsxs'; + imports[importName] = true; + } else { + const importName = state.development ? 'jsxDEV' : 'jsx'; + imports[importName] = true; + } + }, + + JSXFragment(path) { + imports.Fragment = true; + }, + }); + return imports; + } + + function hasJSX(parentPath) { + let fileHasJSX = false; + parentPath.traverse({ + JSXElement(path) { + fileHasJSX = true; + path.stop(); + }, + + JSXFragment(path) { + fileHasJSX = true; + path.stop(); + }, + }); + + return fileHasJSX; + } + + function addAutoImports(path, state) { + if (state.autoImport === IMPORT_TYPES.none) { + return; + } + + if (IMPORT_TYPES[state.autoImport] === undefined) { + throw path.buildCodeFrameError( + 'autoImport must be one of the following: ' + + Object.keys(IMPORT_TYPES).join(', '), ); - state.set( - 'jsxIdentifier', - createIdentifierParser( - state.opts.development ? 'React.jsxDEV' : 'React.jsx', - ), + } + if (state.autoImport === IMPORT_TYPES.require && isModule(path)) { + throw path.buildCodeFrameError( + 'Babel `sourceType` must be set to `script` for autoImport ' + + 'to use `require` syntax. See Babel `sourceType` for details.', ); - state.set( - 'jsxStaticIdentifier', - createIdentifierParser( - state.opts.development ? 'React.jsxDEV' : 'React.jsxs', - ), + } + if (state.autoImport !== IMPORT_TYPES.require && !isModule(path)) { + throw path.buildCodeFrameError( + 'Babel `sourceType` must be set to `module` for autoImport to use `' + + state.autoImport + + '` syntax. See Babel `sourceType` for details.', ); - state.set('jsxFragIdentifier', createIdentifierParser('React.Fragment')); + } + + // import {jsx} from "react"; + // import {createElement} from "react"; + if (state.autoImport === IMPORT_TYPES.namedExports) { + const imports = getImportNames(path, state); + const importMap = {}; + + Object.keys(imports).forEach(importName => { + importMap[importName] = addNamed(path, importName, state.source).name; + }); + + return importMap; + } + + // add import to file and get the import name + let name; + if (state.autoImport === IMPORT_TYPES.require) { + // var _react = require("react"); + name = addNamespace(path, state.source, { + importedInterop: 'uncompiled', + }).name; + } else if (state.autoImport === IMPORT_TYPES.namespace) { + // import * as _react from "react"; + name = addNamespace(path, state.source).name; + } else if (state.autoImport === IMPORT_TYPES.defaultExport) { + // import _default from "react"; + name = addDefault(path, state.source).name; + } + + return name; + } + + visitor.Program = { + enter(path, state) { + if (hasJSX(path)) { + let autoImport = state.opts.autoImport || IMPORT_TYPES.none; + let source = state.opts.importSource || 'react'; + const {file} = state; + + if (file.ast.comments) { + for (let i = 0; i < file.ast.comments.length; i++) { + const comment = file.ast.comments[i]; + const jsxAutoImportMatches = JSX_AUTO_IMPORT_ANNOTATION_REGEX.exec( + comment.value, + ); + if (jsxAutoImportMatches) { + autoImport = jsxAutoImportMatches[1]; + } + const jsxImportSourceMatches = JSX_IMPORT_SOURCE_ANNOTATION_REGEX.exec( + comment.value, + ); + if (jsxImportSourceMatches) { + source = jsxImportSourceMatches[1]; + } + } + } + + const importName = addAutoImports(path, { + ...state.opts, + autoImport, + source, + }); + + state.set( + 'oldJSXIdentifier', + createIdentifierParser( + createIdentifierName(path, autoImport, 'createElement', importName), + ), + ); + + state.set( + 'jsxIdentifier', + createIdentifierParser( + createIdentifierName( + path, + autoImport, + state.opts.development ? 'jsxDEV' : 'jsx', + importName, + ), + ), + ); + + state.set( + 'jsxStaticIdentifier', + createIdentifierParser( + createIdentifierName( + path, + autoImport, + state.opts.development ? 'jsxDEV' : 'jsxs', + importName, + ), + ), + ); + + state.set( + 'jsxFragIdentifier', + createIdentifierParser( + createIdentifierName(path, autoImport, 'Fragment', importName), + ), + ); + } }, };