From d4f2b0379c8ecf8266497f863ee5a2aa612ff4ba Mon Sep 17 00:00:00 2001 From: Luna Ruan Date: Thu, 13 Feb 2020 12:36:19 -0800 Subject: [PATCH] Add Auto Import to Babel Plugin (#16626) This babel transform is a fork of the @babel/plugin-transform-react-jsx transform and is for experimentation purposes only. We don't plan to own this code in the future, and we will upstream this to Babel at some point once we've proven out the concept. As per the RFC to simplify element creation, we want to add the ability to auto import "react' directly from the babel plugin. This commit updates the babel plugin with two options: 1.) importSource: The React module to import from. Defaults to react. 2.) autoImport: The type of import. Defaults to none. - none: Does not import React. JSX compiles to React.jsx etc. - namespace: import * as _react from "react";. JSX compiles to _react.jsx etc. - default: import _default from "react"; JSX compiles to _default.jsx etc. - namedExports: import {jsx as _jsx} from "react"; JSX compiles to _jsx etc. - require: var _react = _interopRequireWildcard(require("react"));. jSX compiles to _react.jsx etc. namespace, default, and namedExports can only be used when sourceType: module and require can only be used when sourceType: script. It also adds two pragmas (jsxAutoImport and jsxImportSource) that allow users to specify autoImport and importSource in the docblock. --- .../TransformJSXToReactCreateElement-test.js | 392 ------------------ .../__tests__/TransformJSXToReactJSX-test.js | 372 ++++++++++++++++- ...nsformJSXToReactCreateElement-test.js.snap | 213 ---------- .../TransformJSXToReactJSX-test.js.snap | 245 +++++++++++ packages/babel-plugin-react-jsx/package.json | 2 +- .../src/TransformJSXToReactBabelPlugin.js | 301 ++++++++++---- 6 files changed, 836 insertions(+), 689 deletions(-) delete mode 100644 packages/babel-plugin-react-jsx/__tests__/TransformJSXToReactCreateElement-test.js delete mode 100644 packages/babel-plugin-react-jsx/__tests__/__snapshots__/TransformJSXToReactCreateElement-test.js.snap 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), + ), + ); + } }, };