From f5125377543749a4cee6ac887a006b6567767862 Mon Sep 17 00:00:00 2001 From: Luna Ruan Date: Tue, 27 Aug 2019 16:00:20 -0700 Subject: [PATCH] Babel Transform JSX to React.jsx/React.jsxDEV Plugin (#16432) 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 change the JSX transform from targeting React.createElement(type, props, children) to React.jsx(type, props, key). This modifies the existing @babel/plugin-transform-react-jsx (and helper) babel plugin to support React.jsx and React.jsxDEV. The main differences between React.jsx/React.jsxDEV and React.createElement are: 1.) key is now passed as an explicit argument rather than through props 3.) children are now passed through props rather than as an explicit argument 4.) props must always be an object 5.) __source and and __self are now passed as separate arguments into React.jsxDEV rather than through props Part of the rationale for this change is that we want to deprecate key spread through props because this is an expensive dynamic comparison operation. We want users instead always explicitly pass key as a prop. However, in the interim, we need a way to distinguish between
and
. Therefore, until we completely deprecate key spreading, we will use React.createElement to transform
and React.jsx to transform everything else. --- package.json | 1 + packages/babel-plugin-react-jsx/README.md | 5 + .../TransformJSXToReactCreateElement-test.js | 392 +++++++++++ .../__tests__/TransformJSXToReactJSX-test.js | 483 ++++++++++++++ ...nsformJSXToReactCreateElement-test.js.snap | 213 ++++++ .../TransformJSXToReactJSX-test.js.snap | 374 +++++++++++ packages/babel-plugin-react-jsx/index.js | 3 + packages/babel-plugin-react-jsx/npm/index.js | 7 + packages/babel-plugin-react-jsx/package.json | 18 + .../src/TransformJSXToReactBabelPlugin.js | 611 ++++++++++++++++++ packages/react/src/ReactElement.js | 40 +- packages/react/src/ReactElementValidator.js | 22 +- .../ReactElementJSX-test.internal.js | 12 + 13 files changed, 2162 insertions(+), 19 deletions(-) create mode 100644 packages/babel-plugin-react-jsx/README.md create mode 100644 packages/babel-plugin-react-jsx/__tests__/TransformJSXToReactCreateElement-test.js create mode 100644 packages/babel-plugin-react-jsx/__tests__/TransformJSXToReactJSX-test.js create mode 100644 packages/babel-plugin-react-jsx/__tests__/__snapshots__/TransformJSXToReactCreateElement-test.js.snap create mode 100644 packages/babel-plugin-react-jsx/__tests__/__snapshots__/TransformJSXToReactJSX-test.js.snap create mode 100644 packages/babel-plugin-react-jsx/index.js create mode 100644 packages/babel-plugin-react-jsx/npm/index.js create mode 100644 packages/babel-plugin-react-jsx/package.json create mode 100644 packages/babel-plugin-react-jsx/src/TransformJSXToReactBabelPlugin.js diff --git a/package.json b/package.json index bdbe2f971076f..28cc06d5dda9b 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "packages/*" ], "devDependencies": { + "@babel/plugin-syntax-jsx": "^7.2.0", "@babel/cli": "^7.0.0", "@babel/code-frame": "^7.0.0", "@babel/core": "^7.0.0", diff --git a/packages/babel-plugin-react-jsx/README.md b/packages/babel-plugin-react-jsx/README.md new file mode 100644 index 0000000000000..3e2858d310c65 --- /dev/null +++ b/packages/babel-plugin-react-jsx/README.md @@ -0,0 +1,5 @@ +This package is intended to eventually replace the current `@babel/plugin-transform-react-jsx`, changing the JSX transform from targeting `React.createElement(type, props, children)` to `React.jsx(types, props, key)`. + +https://github.com/reactjs/rfcs/blob/createlement-rfc/text/0000-create-element-changes.md + +**This is experimental and not intended to be used directly.** diff --git a/packages/babel-plugin-react-jsx/__tests__/TransformJSXToReactCreateElement-test.js b/packages/babel-plugin-react-jsx/__tests__/TransformJSXToReactCreateElement-test.js new file mode 100644 index 0000000000000..6e44eaeaec95b --- /dev/null +++ b/packages/babel-plugin-react-jsx/__tests__/TransformJSXToReactCreateElement-test.js @@ -0,0 +1,392 @@ +/** + * 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( + 'undefined: Spread children are not supported in React.' + + '\n' + + codeFrame.codeFrameColumns( + code, + {start: {line: 1, column: 6}}, + {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( + "undefined: 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}}, + {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 new file mode 100644 index 0000000000000..86ee84f1fa260 --- /dev/null +++ b/packages/babel-plugin-react-jsx/__tests__/TransformJSXToReactJSX-test.js @@ -0,0 +1,483 @@ +/** + * 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', + { + useBuiltIns: true, + useCreateElement: false, + ...options, + }, + ], + ], + }).code + ); +} + +describe('transform react to jsx', () => { + it('fragment with no children', () => { + expect(transform(`var x = <>`)).toMatchSnapshot(); + }); + + it('fragments', () => { + expect(transform(`var x = <>
`)).toMatchSnapshot(); + }); + + it('fragments to set keys', () => { + expect( + transform(`var x = `) + ).toMatchSnapshot(); + }); + + it('React.fragment to set keys and source', () => { + expect( + transform(`var x = `, { + development: true, + }) + ).toMatchSnapshot(); + }); + + it('fragments in dev mode (no key and source)', () => { + expect( + transform(`var x = <>
`, { + development: true, + }) + ).toMatchSnapshot(); + }); + + it('nonStatic children', () => { + expect( + transform( + `var x = ( +
+ {[, ]} +
+ ); + `, + { + development: true, + } + ) + ).toMatchSnapshot(); + }); + + it('static children', () => { + expect( + transform( + `var x = ( +
+ + {[, ]} +
+ ); + `, + { + development: true, + } + ) + ).toMatchSnapshot(); + }); + + it('uses jsxDEV instead of jsx in dev mode', () => { + expect( + transform(`var x = Hi`, {development: true}) + ).toMatchSnapshot(); + }); + + it('properly passes in source and self', () => { + expect( + transform(`var x =
;`, {development: true}) + ).toMatchSnapshot(); + }); + + it('should properly handle potentially null variables', () => { + expect( + transform(` + var foo = null; + var x =
; + `) + ).toMatchSnapshot(); + }); + + it('properly handles keys', () => { + expect( + transform(`var x = ( +
+
+
+
+
+ );`) + ).toMatchSnapshot(); + }); + + it('uses createElement when the key comes after a spread', () => { + expect( + transform(`var x = ( +
+ );`) + ).toMatchSnapshot(); + }); + + it('uses jsx when the key comes before a spread', () => { + expect( + transform(`var x = ( +
+ );`) + ).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( + 'undefined: Spread children are not supported in React.' + + '\n' + + codeFrame.codeFrameColumns( + code, + {start: {line: 1, column: 6}}, + {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( + "undefined: 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}}, + {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__/__snapshots__/TransformJSXToReactCreateElement-test.js.snap b/packages/babel-plugin-react-jsx/__tests__/__snapshots__/TransformJSXToReactCreateElement-test.js.snap new file mode 100644 index 0000000000000..a74c7e1d15e82 --- /dev/null +++ b/packages/babel-plugin-react-jsx/__tests__/__snapshots__/TransformJSXToReactCreateElement-test.js.snap @@ -0,0 +1,213 @@ +// 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 new file mode 100644 index 0000000000000..e5b844b12fc0d --- /dev/null +++ b/packages/babel-plugin-react-jsx/__tests__/__snapshots__/TransformJSXToReactJSX-test.js.snap @@ -0,0 +1,374 @@ +// 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.jsxDEV(React.Fragment, {}, "foo", false, { + fileName: _jsxFileName, + lineNumber: 1 +}, this); +`; + +exports[`transform react to jsx adds appropriate new lines when using spread attribute 1`] = ` +React.jsx(Component, Object.assign({}, props, { + sound: "moo" +})); +`; + +exports[`transform react to jsx arrow functions 1`] = ` +var foo = function () { + var _this = this; + + return function () { + return React.jsx(_this, {}); + }; +}; + +var bar = function () { + var _this2 = this; + + return function () { + return React.jsx(_this2.foo, {}); + }; +}; +`; + +exports[`transform react to jsx assignment 1`] = ` +var div = React.jsx(Component, Object.assign({}, props, { + foo: "bar" +})); +`; + +exports[`transform react to jsx concatenates adjacent string literals 1`] = ` +var x = React.jsxs("div", { + children: ["foo", "bar", "baz", React.jsx("div", { + children: "buz bang" + }), "qux", null, "quack"] +}); +`; + +exports[`transform react to jsx fragment with no children 1`] = `var x = React.jsx(React.Fragment, {});`; + +exports[`transform react to jsx fragments 1`] = ` +var x = React.jsx(React.Fragment, { + children: React.jsx("div", {}) +}); +`; + +exports[`transform react to jsx fragments in dev mode (no key and source) 1`] = ` +var _jsxFileName = ""; +var x = React.jsxDEV(React.Fragment, { + children: React.jsxDEV("div", {}, undefined, false, { + fileName: _jsxFileName, + lineNumber: 1 + }, this) +}, undefined, false); +`; + +exports[`transform react to jsx fragments to set keys 1`] = `var x = React.jsx(React.Fragment, {}, "foo");`; + +exports[`transform react to jsx nonStatic children 1`] = ` +var _jsxFileName = ""; +var x = React.jsxDEV("div", { + children: [React.jsxDEV("span", {}, '0', false, { + fileName: _jsxFileName, + lineNumber: 3 + }, this), React.jsxDEV("span", {}, '1', false, { + fileName: _jsxFileName, + lineNumber: 3 + }, this)] +}, undefined, false, { + fileName: _jsxFileName, + lineNumber: 2 +}, this); +`; + +exports[`transform react to jsx properly handles keys 1`] = ` +var x = React.jsxs("div", { + children: [React.jsx("div", {}, "1"), React.jsx("div", { + meow: "wolf" + }, "2"), React.jsx("div", {}, "3")] +}); +`; + +exports[`transform react to jsx properly passes in source and self 1`] = ` +var _jsxFileName = ""; +var x = React.jsxDEV("div", {}, undefined, false, { + fileName: _jsxFileName, + lineNumber: 1 +}, this); +`; + +exports[`transform react to jsx should allow constructor as prop 1`] = ` +React.jsx(Component, { + constructor: "foo" +}); +`; + +exports[`transform react to jsx should allow deeper js namespacing 1`] = `React.jsx(Namespace.DeepNamespace.Component, {});`; + +exports[`transform react to jsx should allow elements as attributes 1`] = ` +React.jsx("div", { + attr: React.jsx("div", {}) +}); +`; + +exports[`transform react to jsx should allow js namespacing 1`] = `React.jsx(Namespace.Component, {});`; + +exports[`transform react to jsx should allow nested fragments 1`] = ` +React.jsx("div", { + children: React.jsxs(React.Fragment, { + children: [React.jsxs(React.Fragment, { + children: [React.jsx("span", { + children: "Hello" + }), React.jsx("span", { + children: "world" + })] + }), React.jsxs(React.Fragment, { + children: [React.jsx("span", { + children: "Goodbye" + }), React.jsx("span", { + children: "world" + })] + })] + }) +}); +`; + +exports[`transform react to jsx should avoid wrapping in extra parens if not needed 1`] = ` +var x = React.jsx("div", { + children: React.jsx(Component, {}) +}); +var x = React.jsx("div", { + children: props.children +}); +var x = React.jsx(Composite, { + children: props.children +}); +var x = React.jsx(Composite, { + children: React.jsx(Composite2, {}) +}); +`; + +exports[`transform react to jsx should convert simple tags 1`] = `var x = React.jsx("div", {});`; + +exports[`transform react to jsx should convert simple text 1`] = ` +var x = React.jsx("div", { + children: "text" +}); +`; + +exports[`transform react to jsx should escape xhtml jsxattribute 1`] = ` +React.jsx("div", { + id: "w\\xF4w" +}); +React.jsx("div", { + id: "w" +}); +React.jsx("div", { + id: "w < w" +}); +`; + +exports[`transform react to jsx should escape xhtml jsxtext 1`] = ` +React.jsx("div", { + children: "wow" +}); +React.jsx("div", { + children: "w\\xF4w" +}); +React.jsx("div", { + children: "w & w" +}); +React.jsx("div", { + children: "w & w" +}); +React.jsx("div", { + children: "w \\xA0 w" +}); +React.jsx("div", { + children: "this should not parse as unicode: \\xA0" +}); +React.jsx("div", { + children: "this should parse as nbsp: \\xA0 " +}); +React.jsxs("div", { + children: ["this should parse as unicode: ", '  '] +}); +React.jsx("div", { + children: "w < w" +}); +`; + +exports[`transform react to jsx should handle attributed elements 1`] = ` +var HelloMessage = React.createClass({ + render: function () { + return React.jsxs("div", { + children: ["Hello ", this.props.name] + }); + } +}); +React.render(React.jsx(HelloMessage, { + name: React.jsx("span", { + children: "Sebastian" + }) +}), mountNode); +`; + +exports[`transform react to jsx should handle has own property correctly 1`] = ` +React.jsx("hasOwnProperty", { + children: "testing" +}); +`; + +exports[`transform react to jsx should have correct comma in nested children 1`] = ` +var x = React.jsxs("div", { + children: [React.jsx("div", { + children: React.jsx("br", {}) + }), React.jsxs(Component, { + children: [foo, React.jsx("br", {}), bar] + }), React.jsx("br", {})] +}); +`; + +exports[`transform react to jsx should insert commas after expressions before whitespace 1`] = ` +var x = React.jsx("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.jsx(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.jsx("div", { + children: "\\xA0 " +}); +`; + +exports[`transform react to jsx should not strip tags with a single child of nbsp 1`] = ` +React.jsx("div", { + children: "\\xA0" +}); +`; + +exports[`transform react to jsx should properly handle comments adjacent to children 1`] = ` +var x = React.jsxs("div", { + children: [React.jsx("span", {}), React.jsx("br", {})] +}); +`; + +exports[`transform react to jsx should properly handle comments between props 1`] = ` +var x = React.jsx("div", { + /* a multi-line + comment */ + attr1: "foo", + children: React.jsx("span", { + // a double-slash comment + attr2: "bar" + }) +}); +`; + +exports[`transform react to jsx should properly handle potentially null variables 1`] = ` +var foo = null; +var x = React.jsx("div", Object.assign({}, foo)); +`; + +exports[`transform react to jsx should quote jsx attributes 1`] = ` +React.jsx("button", { + "data-value": "a value", + children: "Button" +}); +`; + +exports[`transform react to jsx should support xml namespaces if flag 1`] = ` +React.jsx("f:image", { + "n:attr": true +}); +`; + +exports[`transform react to jsx should transform known hyphenated tags 1`] = `React.jsx("font-face", {});`; + +exports[`transform react to jsx static children 1`] = ` +var _jsxFileName = ""; +var x = React.jsxDEV("div", { + children: [React.jsxDEV("span", {}, undefined, false, { + fileName: _jsxFileName, + lineNumber: 3 + }, this), [React.jsxDEV("span", {}, '0', false, { + fileName: _jsxFileName, + lineNumber: 4 + }, this), React.jsxDEV("span", {}, '1', false, { + fileName: _jsxFileName, + lineNumber: 4 + }, this)]] +}, undefined, true, { + fileName: _jsxFileName, + lineNumber: 2 +}, this); +`; + +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.jsx(Component, _extends({ + y: 2 +}, x)); +`; + +exports[`transform react to jsx uses createElement when the key comes after a spread 1`] = ` +var x = React.createElement("div", Object.assign({}, props, { + key: "1", + foo: "bar" +})); +`; + +exports[`transform react to jsx uses jsx when the key comes before a spread 1`] = ` +var x = React.jsx("div", Object.assign({}, props, { + foo: "bar" +}), "1"); +`; + +exports[`transform react to jsx uses jsxDEV instead of jsx in dev mode 1`] = ` +var _jsxFileName = ""; +var x = React.jsxDEV("span", { + propOne: "one", + children: "Hi" +}, undefined, false, { + fileName: _jsxFileName, + lineNumber: 1 +}, this); +`; + +exports[`transform react to jsx wraps props in react spread for first spread attributes 1`] = ` +React.jsx(Component, Object.assign({}, x, { + y: 2, + z: true +})); +`; + +exports[`transform react to jsx wraps props in react spread for last spread attributes 1`] = ` +React.jsx(Component, Object.assign({ + y: 2, + z: true +}, x)); +`; + +exports[`transform react to jsx wraps props in react spread for middle spread attributes 1`] = ` +React.jsx(Component, Object.assign({ + y: 2 +}, x, { + z: true +})); +`; diff --git a/packages/babel-plugin-react-jsx/index.js b/packages/babel-plugin-react-jsx/index.js new file mode 100644 index 0000000000000..7401bda105727 --- /dev/null +++ b/packages/babel-plugin-react-jsx/index.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = require('./src/TransformJSXToReactBabelPlugin'); diff --git a/packages/babel-plugin-react-jsx/npm/index.js b/packages/babel-plugin-react-jsx/npm/index.js new file mode 100644 index 0000000000000..af4ea6a44bb9a --- /dev/null +++ b/packages/babel-plugin-react-jsx/npm/index.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-jsx-babel.production.min.js'); +} else { + module.exports = require('./cjs/react-jsx-babel.development.js'); +} diff --git a/packages/babel-plugin-react-jsx/package.json b/packages/babel-plugin-react-jsx/package.json new file mode 100644 index 0000000000000..41243452d4f17 --- /dev/null +++ b/packages/babel-plugin-react-jsx/package.json @@ -0,0 +1,18 @@ +{ + "name": "babel-plugin-react-jsx", + "version": "0.1.0", + "private": true, + "description": "@babel/plugin-transform-react-jsx", + "main": "index.js", + "dependencies": { + "esutils": "^2.0.0" + + }, + "files": [ + "README.md", + "index.js", + "build-info.json", + "cjs/", + "umd/" + ] +} diff --git a/packages/babel-plugin-react-jsx/src/TransformJSXToReactBabelPlugin.js b/packages/babel-plugin-react-jsx/src/TransformJSXToReactBabelPlugin.js new file mode 100644 index 0000000000000..960f75a3a8ecb --- /dev/null +++ b/packages/babel-plugin-react-jsx/src/TransformJSXToReactBabelPlugin.js @@ -0,0 +1,611 @@ +// MIT License + +// Copyright (c) 2014-present Sebastian McKenzie and other contributors + +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: + +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// © 2019 GitHub, Inc. +'use strict'; + +const esutils = require('esutils'); + +function helper(babel, opts) { + const {types: t} = babel; + + const visitor = {}; + + visitor.JSXNamespacedName = function(path, state) { + const throwIfNamespace = + state.opts.throwIfNamespace === undefined + ? true + : !!state.opts.throwIfNamespace; + if (throwIfNamespace) { + throw path.buildCodeFrameError( + `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.`, + ); + } + }; + + visitor.JSXSpreadChild = function(path) { + throw path.buildCodeFrameError( + 'Spread children are not supported in React.', + ); + }; + + visitor.JSXElement = { + exit(path, file) { + let callExpr; + if (file.opts.useCreateElement || shouldUseCreateElement(path)) { + callExpr = buildCreateElementCall(path, file); + } else { + callExpr = buildJSXElementCall(path, file); + } + + if (callExpr) { + path.replaceWith(t.inherits(callExpr, path.node)); + } + }, + }; + + visitor.JSXFragment = { + exit(path, file) { + if (opts.compat) { + throw path.buildCodeFrameError( + '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); + } + + if (callExpr) { + path.replaceWith(t.inherits(callExpr, path.node)); + } + }, + }; + + return visitor; + + function convertJSXIdentifier(node, parent) { + if (t.isJSXIdentifier(node)) { + if (node.name === 'this' && t.isReferenced(node, parent)) { + return t.thisExpression(); + } else if (esutils.keyword.isIdentifierNameES6(node.name)) { + node.type = 'Identifier'; + } else { + return t.stringLiteral(node.name); + } + } else if (t.isJSXMemberExpression(node)) { + return t.memberExpression( + convertJSXIdentifier(node.object, node), + convertJSXIdentifier(node.property, node), + ); + } else if (t.isJSXNamespacedName(node)) { + /** + * If there is flag "throwIfNamespace" + * print XMLNamespace like string literal + */ + return t.stringLiteral(`${node.namespace.name}:${node.name.name}`); + } + + return node; + } + + function convertAttributeValue(node) { + if (t.isJSXExpressionContainer(node)) { + return node.expression; + } else { + return node; + } + } + + function convertAttribute(node) { + const value = convertAttributeValue(node.value || t.booleanLiteral(true)); + + if (t.isStringLiteral(value) && !t.isJSXExpressionContainer(node.value)) { + value.value = value.value.replace(/\n\s+/g, ' '); + + // "raw" JSXText should not be used from a StringLiteral because it needs to be escaped. + if (value.extra && value.extra.raw) { + delete value.extra.raw; + } + } + + if (t.isJSXNamespacedName(node.name)) { + node.name = t.stringLiteral( + node.name.namespace.name + ':' + node.name.name.name, + ); + } else if (esutils.keyword.isIdentifierNameES6(node.name.name)) { + node.name.type = 'Identifier'; + } else { + node.name = t.stringLiteral(node.name.name); + } + + 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) + function buildJSXElementCall(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 tagExpr = convertJSXIdentifier( + openingPath.node.name, + openingPath.node, + ); + const args = []; + + let tagName; + if (t.isIdentifier(tagExpr)) { + tagName = tagExpr.name; + } else if (t.isLiteral(tagExpr)) { + tagName = tagExpr.value; + } + + const state = { + tagExpr: tagExpr, + tagName: tagName, + args: args, + }; + + if (opts.pre) { + opts.pre(state, file); + } + + let attribs = []; + let key; + let source; + let self; + + // for React.jsx, key, __source (dev), and __self (dev) is passed in as + // a separate argument rather than in the args object. We go through the + // props and filter out these three keywords so we can pass them in + // as separate arguments later + for (let i = 0; i < openingPath.node.attributes.length; i++) { + const attr = openingPath.node.attributes[i]; + if (t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name)) { + if (attr.name.name === 'key') { + key = convertAttribute(attr).value; + } else if (attr.name.name === '__source') { + source = convertAttribute(attr).value; + } else if (attr.name.name === '__self') { + self = convertAttribute(attr).value; + } else { + attribs.push(attr); + } + } else { + attribs.push(attr); + } + } + + if (attribs.length || path.node.children.length) { + attribs = buildJSXOpeningElementAttributes( + attribs, + file, + path.node.children, + ); + } else { + // attributes should never be null + attribs = t.objectExpression([]); + } + + args.push(attribs); + + if (!file.opts.development) { + if (key !== undefined) { + args.push(key); + } + } else { + // isStaticChildren, __source, and __self are only used in development + args.push( + key === undefined ? t.identifier('undefined') : key, + t.booleanLiteral(path.node.children.length > 1), + source === undefined ? t.identifier('undefined') : source, + self === undefined ? t.identifier('undefined') : self, + ); + } + + if (opts.post) { + opts.post(state, file); + } + return ( + state.call || + t.callExpression( + path.node.children.length > 1 ? state.staticCallee : state.callee, + args, + ) + ); + } + + // Builds props for React.jsx. This function adds children into the props + // and ensures that props is always an object + function buildJSXOpeningElementAttributes(attribs, file, children) { + let _props = []; + const objs = []; + + const useBuiltIns = file.opts.useBuiltIns || false; + if (typeof useBuiltIns !== 'boolean') { + throw new Error( + 'transform-react-jsx currently only accepts a boolean option for ' + + 'useBuiltIns (defaults to false)', + ); + } + + while (attribs.length) { + const prop = attribs.shift(); + if (t.isJSXSpreadAttribute(prop)) { + _props = pushProps(_props, objs); + objs.push(prop.argument); + } else { + _props.push(convertAttribute(prop)); + } + } + + // In React.JSX, children is no longer a separate argument, but passed in + // through the argument object + if (children && children.length > 0) { + if (children.length === 1) { + _props.push(t.objectProperty(t.identifier('children'), children[0])); + } else { + _props.push( + t.objectProperty( + t.identifier('children'), + t.arrayExpression(children), + ), + ); + } + } + + pushProps(_props, objs); + + if (objs.length === 1) { + // only one object + if (!t.isObjectExpression(objs[0])) { + // if the prop object isn't an object, use Object.assign or _extends + // to ensure that the prop will always be an object (as opposed to a variable + // that could be null at some point) + const expressionHelper = useBuiltIns + ? t.memberExpression(t.identifier('Object'), t.identifier('assign')) + : file.addHelper('extends'); + + attribs = t.callExpression(expressionHelper, [ + t.objectExpression([]), + objs[0], + ]); + } else { + attribs = objs[0]; + } + } else { + // looks like we have multiple objects + if (!t.isObjectExpression(objs[0])) { + objs.unshift(t.objectExpression([])); + } + + const expressionHelper = useBuiltIns + ? t.memberExpression(t.identifier('Object'), t.identifier('assign')) + : file.addHelper('extends'); + + // spread it + attribs = t.callExpression(expressionHelper, objs); + } + + return attribs; + } + + // Builds JSX Fragment <> into + // Production: React.jsx(type, arguments) + // Development: React.jsxDEV(type, { children}) + function buildJSXFragmentCall(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); + } + + let childrenNode; + if (path.node.children.length > 0) { + if (path.node.children.length === 1) { + childrenNode = path.node.children[0]; + } else { + childrenNode = t.arrayExpression(path.node.children); + } + } + + args.push( + t.objectExpression( + childrenNode !== undefined + ? [t.objectProperty(t.identifier('children'), childrenNode)] + : [], + ), + ); + + if (file.opts.development) { + args.push( + t.identifier('undefined'), + t.booleanLiteral(path.node.children.length > 1), + ); + } + + if (opts.post) { + opts.post(state, file); + } + + return ( + state.call || + t.callExpression( + path.node.children.length > 1 ? state.staticCallee : state.callee, + args, + ) + ); + } + + // Builds JSX into: + // Production: React.createElement(type, arguments, children) + // Development: React.createElement(type, arguments, children, source, self) + function buildCreateElementCall(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 tagExpr = convertJSXIdentifier( + openingPath.node.name, + openingPath.node, + ); + const args = []; + + let tagName; + if (t.isIdentifier(tagExpr)) { + tagName = tagExpr.name; + } else if (t.isLiteral(tagExpr)) { + tagName = tagExpr.value; + } + + const state = { + tagExpr: tagExpr, + tagName: tagName, + args: args, + }; + + if (opts.pre) { + opts.pre(state, file); + } + + let attribs = openingPath.node.attributes; + if (attribs.length) { + attribs = buildCreateElementOpeningElementAttributes(attribs, file); + } else { + attribs = t.nullLiteral(); + } + + args.push(attribs, ...path.node.children); + + if (opts.post) { + opts.post(state, file); + } + + return state.call || t.callExpression(state.oldCallee, args); + } + + function pushProps(_props, objs) { + if (!_props.length) { + return _props; + } + + objs.push(t.objectExpression(_props)); + return []; + } + + /** + * The logic for this is quite terse. It's because we need to + * support spread elements. We loop over all attributes, + * breaking on spreads, we then push a new object containing + * all prior attributes to an array for later processing. + */ + function buildCreateElementOpeningElementAttributes(attribs, file) { + let _props = []; + const objs = []; + + const useBuiltIns = file.opts.useBuiltIns || false; + if (typeof useBuiltIns !== 'boolean') { + throw new Error( + 'transform-react-jsx currently only accepts a boolean option for ' + + 'useBuiltIns (defaults to false)', + ); + } + + while (attribs.length) { + const prop = attribs.shift(); + if (t.isJSXSpreadAttribute(prop)) { + _props = pushProps(_props, objs); + objs.push(prop.argument); + } else { + const attr = convertAttribute(prop); + _props.push(attr); + } + } + + pushProps(_props, objs); + + if (objs.length === 1) { + // only one object + attribs = objs[0]; + } else { + // looks like we have multiple objects + if (!t.isObjectExpression(objs[0])) { + objs.unshift(t.objectExpression([])); + } + + const expressionHelper = useBuiltIns + ? t.memberExpression(t.identifier('Object'), t.identifier('assign')) + : file.addHelper('extends'); + + // spread it + attribs = t.callExpression(expressionHelper, objs); + } + + 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) { + const {types: t} = babel; + + const createIdentifierParser = id => () => { + return id + .split('.') + .map(name => t.identifier(name)) + .reduce((object, property) => t.memberExpression(object, property)); + }; + + const visitor = helper(babel, { + pre(state) { + const tagName = state.tagName; + const args = state.args; + if (t.react.isCompatTag(tagName)) { + args.push(t.stringLiteral(tagName)); + } else { + args.push(state.tagExpr); + } + }, + + post(state, pass) { + state.callee = pass.get('jsxIdentifier')(); + state.staticCallee = pass.get('jsxStaticIdentifier')(); + state.oldCallee = pass.get('oldJSXIdentifier')(); + }, + }); + + 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')); + }, + }; + + visitor.JSXAttribute = function(path) { + if (t.isJSXElement(path.node.value)) { + path.node.value = t.jsxExpressionContainer(path.node.value); + } + }; + + return { + name: 'transform-react-jsx', + visitor, + }; +}; diff --git a/packages/react/src/ReactElement.js b/packages/react/src/ReactElement.js index 9416a313b9648..163ae077f0aca 100644 --- a/packages/react/src/ReactElement.js +++ b/packages/react/src/ReactElement.js @@ -179,14 +179,24 @@ export function jsx(type, config, maybeKey) { let key = null; let ref = null; - if (hasValidRef(config)) { - ref = config.ref; + // Currently, key can be spread in as a prop. This causes a potential + // issue if key is also explicitly declared (ie.
+ // or
). We want to deprecate key spread, + // but as an intermediary step, we will use jsxDEV for everything except + //
, because we aren't currently able to tell if + // key is explicitly declared to be undefined or not. + if (maybeKey !== undefined) { + key = '' + maybeKey; } if (hasValidKey(config)) { key = '' + config.key; } + if (hasValidRef(config)) { + ref = config.ref; + } + // Remaining properties are added to a new props object for (propName in config) { if ( @@ -197,12 +207,6 @@ export function jsx(type, config, maybeKey) { } } - // intentionally not checking if key was set above - // this key is higher priority as it's static - if (maybeKey !== undefined) { - key = '' + maybeKey; - } - // Resolve default props if (type && type.defaultProps) { const defaultProps = type.defaultProps; @@ -239,14 +243,24 @@ export function jsxDEV(type, config, maybeKey, source, self) { let key = null; let ref = null; - if (hasValidRef(config)) { - ref = config.ref; + // Currently, key can be spread in as a prop. This causes a potential + // issue if key is also explicitly declared (ie.
+ // or
). We want to deprecate key spread, + // but as an intermediary step, we will use jsxDEV for everything except + //
, because we aren't currently able to tell if + // key is explicitly declared to be undefined or not. + if (maybeKey !== undefined) { + key = '' + maybeKey; } if (hasValidKey(config)) { key = '' + config.key; } + if (hasValidRef(config)) { + ref = config.ref; + } + // Remaining properties are added to a new props object for (propName in config) { if ( @@ -257,12 +271,6 @@ export function jsxDEV(type, config, maybeKey, source, self) { } } - // intentionally not checking if key was set above - // this key is higher priority as it's static - if (maybeKey !== undefined) { - key = '' + maybeKey; - } - // Resolve default props if (type && type.defaultProps) { const defaultProps = type.defaultProps; diff --git a/packages/react/src/ReactElementValidator.js b/packages/react/src/ReactElementValidator.js index 6a1626a2e27e8..b81fe9c942036 100644 --- a/packages/react/src/ReactElementValidator.js +++ b/packages/react/src/ReactElementValidator.js @@ -43,6 +43,8 @@ if (__DEV__) { propTypesMisspellWarningShown = false; } +const hasOwnProperty = Object.prototype.hasOwnProperty; + function getDeclarationErrorAddendum() { if (ReactCurrentOwner.current) { const name = getComponentName(ReactCurrentOwner.current.type); @@ -334,12 +336,26 @@ export function jsxWithValidation( // We don't want exception behavior to differ between dev and prod. // (Rendering will throw with a helpful message and as soon as the type is // fixed, the key warnings will appear.) + if (validType) { const children = props.children; if (children !== undefined) { if (isStaticChildren) { - for (let i = 0; i < children.length; i++) { - validateChildKeys(children[i], type); + if (Array.isArray(children)) { + for (let i = 0; i < children.length; i++) { + validateChildKeys(children[i], type); + } + + if (Object.freeze) { + Object.freeze(children); + } + } else { + warning( + false, + 'React.jsx: Static children should always be an array. ' + + 'You are likely explicitly calling React.jsxs or React.jsxDEV. ' + + 'Use the Babel transform instead.', + ); } } else { validateChildKeys(children, type); @@ -347,7 +363,7 @@ export function jsxWithValidation( } } - if (props.key !== undefined) { + if (hasOwnProperty.call(props, 'key')) { warning( false, 'React.jsx: Spreading a key to JSX is a deprecated pattern. ' + diff --git a/packages/react/src/__tests__/ReactElementJSX-test.internal.js b/packages/react/src/__tests__/ReactElementJSX-test.internal.js index db62a2c51ec15..23135d15de8e3 100644 --- a/packages/react/src/__tests__/ReactElementJSX-test.internal.js +++ b/packages/react/src/__tests__/ReactElementJSX-test.internal.js @@ -215,6 +215,18 @@ describe('ReactElement.jsx', () => { ); }); + it('warns when a jsxs is passed something that is not an array', () => { + const container = document.createElement('div'); + expect(() => + ReactDOM.render(React.jsxs('div', {children: 'foo'}, null), container), + ).toWarnDev( + 'React.jsx: Static children should always be an array. ' + + 'You are likely explicitly calling React.jsxs or React.jsxDEV. ' + + 'Use the Babel transform instead.', + {withoutStack: true}, + ); + }); + it('should warn when `key` is being accessed on a host element', () => { const element = React.jsxs('div', {}, '3'); expect(() => void element.props.key).toWarnDev(