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.
This commit is contained in:
Luna Ruan
2020-02-13 12:36:19 -08:00
committed by GitHub
parent 8777b44e98
commit d4f2b0379c
6 changed files with 837 additions and 690 deletions
@@ -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 = <React.Fragment key='foo'><div /></React.Fragment>`, {
development: true,
})
).toMatchSnapshot();
});
it('normal fragments not to set key and source', () => {
expect(
transform(`var x = <><div /></>`, {
development: true,
})
).toMatchSnapshot();
});
it('should properly handle comments adjacent to children', () => {
expect(
transform(`
var x = (
<div>
{/* A comment at the beginning */}
{/* A second comment at the beginning */}
<span>
{/* A nested comment */}
</span>
{/* A sandwiched comment */}
<br />
{/* A comment at the end */}
{/* A second comment at the end */}
</div>
);
`)
).toMatchSnapshot();
});
it('adds appropriate new lines when using spread attribute', () => {
expect(transform(`<Component {...props} sound="moo" />`)).toMatchSnapshot();
});
it('arrow functions', () => {
expect(
transform(`
var foo = function () {
return () => <this />;
};
var bar = function () {
return () => <this.foo />;
};
`)
).toMatchSnapshot();
});
it('assignment', () => {
expect(
transform(`var div = <Component {...props} foo="bar" />`)
).toMatchSnapshot();
});
it('concatenates adjacent string literals', () => {
expect(
transform(`
var x =
<div>
foo
{"bar"}
baz
<div>
buz
bang
</div>
qux
{null}
quack
</div>
`)
).toMatchSnapshot();
});
it('should allow constructor as prop', () => {
expect(transform(`<Component constructor="foo" />;`)).toMatchSnapshot();
});
it('should allow deeper js namespacing', () => {
expect(
transform(`<Namespace.DeepNamespace.Component />;`)
).toMatchSnapshot();
});
it('should allow elements as attributes', () => {
expect(transform(`<div attr=<div /> />`)).toMatchSnapshot();
});
it('should allow js namespacing', () => {
expect(transform(`<Namespace.Component />;`)).toMatchSnapshot();
});
it('should allow nested fragments', () => {
expect(
transform(`
<div>
< >
<>
<span>Hello</span>
<span>world</span>
</>
<>
<span>Goodbye</span>
<span>world</span>
</>
</>
</div>
`)
).toMatchSnapshot();
});
it('should avoid wrapping in extra parens if not needed', () => {
expect(
transform(`
var x = <div>
<Component />
</div>;
var x = <div>
{props.children}
</div>;
var x = <Composite>
{props.children}
</Composite>;
var x = <Composite>
<Composite2 />
</Composite>;
`)
).toMatchSnapshot();
});
it('should convert simple tags', () => {
expect(transform(`var x = <div></div>;`)).toMatchSnapshot();
});
it('should convert simple text', () => {
expect(transform(`var x = <div>text</div>;`)).toMatchSnapshot();
});
it('should disallow spread children', () => {
let _error;
const code = `<div>{...children}</div>;`;
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(`
<div id="wôw" />;
<div id="\w" />;
<div id="w &lt; w" />;
`)
).toMatchSnapshot();
});
it('should escape xhtml jsxtext', () => {
/* eslint-disable no-irregular-whitespace */
expect(
transform(`
<div>wow</div>;
<div>wôw</div>;
<div>w & w</div>;
<div>w &amp; w</div>;
<div>w &nbsp; w</div>;
<div>this should not parse as unicode: \u00a0</div>;
<div>this should parse as nbsp:   </div>;
<div>this should parse as unicode: {'\u00a0 '}</div>;
<div>w &lt; w</div>;
`)
).toMatchSnapshot();
/*eslint-enable */
});
it('should handle attributed elements', () => {
expect(
transform(`
var HelloMessage = React.createClass({
render: function() {
return <div>Hello {this.props.name}</div>;
}
});
React.render(<HelloMessage name={
<span>
Sebastian
</span>
} />, mountNode);
`)
).toMatchSnapshot();
});
it('should handle has own property correctly', () => {
expect(
transform(`<hasOwnProperty>testing</hasOwnProperty>;`)
).toMatchSnapshot();
});
it('should have correct comma in nested children', () => {
expect(
transform(`
var x = <div>
<div><br /></div>
<Component>{foo}<br />{bar}</Component>
<br />
</div>;
`)
).toMatchSnapshot();
});
it('should insert commas after expressions before whitespace', () => {
expect(
transform(`
var x =
<div
attr1={
"foo" + "bar"
}
attr2={
"foo" + "bar" +
"baz" + "bug"
}
attr3={
"foo" + "bar" +
"baz" + "bug"
}
attr4="baz">
</div>
`)
).toMatchSnapshot();
});
it('should not add quotes to identifier names', () => {
expect(
transform(`var e = <F aaa new const var default foo-bar/>;`)
).toMatchSnapshot();
});
it('should not strip nbsp even couple with other whitespace', () => {
expect(transform(`<div>&nbsp; </div>;`)).toMatchSnapshot();
});
it('should not strip tags with a single child of nbsp', () => {
expect(transform(`<div>&nbsp;</div>;`)).toMatchSnapshot();
});
it('should properly handle comments between props', () => {
expect(
transform(`
var x = (
<div
/* a multi-line
comment */
attr1="foo">
<span // a double-slash comment
attr2="bar"
/>
</div>
);
`)
).toMatchSnapshot();
});
it('should quote jsx attributes', () => {
expect(
transform(`<button data-value='a value'>Button</button>`)
).toMatchSnapshot();
});
it('should support xml namespaces if flag', () => {
expect(
transform('<f:image n:attr />', {throwIfNamespace: false})
).toMatchSnapshot();
});
it('should throw error namespaces if not flag', () => {
let _error;
const code = `<f:image />`;
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(`<font-face />`)).toMatchSnapshot();
});
it('wraps props in react spread for first spread attributes', () => {
expect(transform(`<Component {...x} y={2} z />`)).toMatchSnapshot();
});
it('wraps props in react spread for last spread attributes', () => {
expect(transform(`<Component y={2} z { ... x } />`)).toMatchSnapshot();
});
it('wraps props in react spread for middle spread attributes', () => {
expect(transform(`<Component y={2} { ... x } z />`)).toMatchSnapshot();
});
it('useBuiltIns false uses extend instead of Object.assign', () => {
expect(
transform(`<Component y={2} {...x} />`, {useBuiltIns: false})
).toMatchSnapshot();
});
});
@@ -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 = <div><span /></div>
`,
{
autoImport: 'namespace',
importSource: 'foobar',
}
)
).toMatchSnapshot();
});
it('import source pragma overrides regular pragma', () => {
expect(
transform(
`/** @jsxImportSource baz */
var x = <div><span /></div>
`,
{
autoImport: 'namespace',
importSource: 'foobar',
}
)
).toMatchSnapshot();
});
it('multiple pragmas work', () => {
expect(
transform(
`/** Some comment here
* @jsxImportSource baz
* @jsxAutoImport defaultExport
*/
var x = <div><span /></div>
`,
{
autoImport: 'namespace',
importSource: 'foobar',
}
)
).toMatchSnapshot();
});
it('throws error when sourceType is module and autoImport is require', () => {
const code = `var x = <div><span /></div>`;
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 = <div><span /></div>`;
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 = <div><span /></div>`;
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 = <div><span /></div>`, {
autoImport: 'namespace',
importSource: 'foobar',
})
).toMatchSnapshot();
});
it('auto import require', () => {
expect(
transform(
`var x = (
<>
<div>
<div key="1" />
<div key="2" meow="wolf" />
<div key="3" />
<div {...props} key="4" />
</div>
</>
);`,
{
autoImport: 'require',
},
{
sourceType: 'script',
}
)
).toMatchSnapshot();
});
it('auto import namespace', () => {
expect(
transform(
`var x = (
<>
<div>
<div key="1" />
<div key="2" meow="wolf" />
<div key="3" />
<div {...props} key="4" />
</div>
</>
);`,
{
autoImport: 'namespace',
}
)
).toMatchSnapshot();
});
it('auto import default', () => {
expect(
transform(
`var x = (
<>
<div>
<div key="1" />
<div key="2" meow="wolf" />
<div key="3" />
<div {...props} key="4" />
</div>
</>
);`,
{
autoImport: 'defaultExport',
}
)
).toMatchSnapshot();
});
it('auto import named exports', () => {
expect(
transform(
`var x = (
<>
<div>
<div key="1" />
<div key="2" meow="wolf" />
<div key="3" />
<div {...props} key="4" />
</div>
</>
);`,
{
autoImport: 'namedExports',
}
)
).toMatchSnapshot();
});
it('auto import with no JSX', () => {
expect(
transform(
`var foo = "<div></div>"`,
{
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 <div />;
};
return <span />;
};
}
}
`,
{
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 <div />;
};
return <span />;
};
}
}
`,
{
autoImport: 'namedExports',
}
)
).toMatchSnapshot();
});
it('auto import in dev', () => {
expect(
transform(
`var x = (
<>
<div>
<div key="1" />
<div key="2" meow="wolf" />
<div key="3" />
<div {...props} key="4" />
</div>
</>
);`,
{
autoImport: 'namedExports',
development: true,
}
)
).toMatchSnapshot();
});
it('auto import none', () => {
expect(
transform(
`var x = (
<>
<div>
<div key="1" />
<div key="2" meow="wolf" />
<div key="3" />
<div {...props} key="4" />
</div>
</>
);`,
{
autoImport: 'none',
}
)
).toMatchSnapshot();
});
it('auto import undefined', () => {
expect(
transform(
`var x = (
<>
<div>
<div key="1" />
<div key="2" meow="wolf" />
<div key="3" />
<div {...props} key="4" />
</div>
</>
);`
)
).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 = (
<div>
<div key="1" />
<div key="2" meow="wolf" />
<div key="3" />
<div {...props} key="4" />
</div>
);`,
{
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 = (
<div>
<div key="1" />
<div key="2" meow="wolf" />
<div key="3" />
<div {...props} key="4" />
</div>
);`,
{
autoImport: 'namespace',
}
)
).toMatchSnapshot();
});
it('fragment with no children', () => {
expect(transform(`var x = <></>`)).toMatchSnapshot();
});
@@ -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
}));
`;
@@ -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 = "<div></div>";`;
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", {
+1 -1
View File
@@ -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",
@@ -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 <div {...props} key={key} /> to distinguish it
// from <div key={key} {...props} />. 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 <div {...props} key={key} /> to distinguish it
// from <div key={key} {...props} />. 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) {
},
});
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(', '),
);
}
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.',
);
}
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.',
);
}
// 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) {
state.set(
'oldJSXIdentifier',
createIdentifierParser('React.createElement'),
);
state.set(
'jsxIdentifier',
createIdentifierParser(
state.opts.development ? 'React.jsxDEV' : 'React.jsx',
),
);
state.set(
'jsxStaticIdentifier',
createIdentifierParser(
state.opts.development ? 'React.jsxDEV' : 'React.jsxs',
),
);
state.set('jsxFragIdentifier', createIdentifierParser('React.Fragment'));
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),
),
);
}
},
};