diff --git a/package.json b/package.json index 6ef4909eddf7b..2f2bda553eaa7 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-events/src/dom/testing-library/domEnvironment.js b/packages/react-events/src/dom/testing-library/domEnvironment.js index f0a65576a94ce..160d6183d0563 100644 --- a/packages/react-events/src/dom/testing-library/domEnvironment.js +++ b/packages/react-events/src/dom/testing-library/domEnvironment.js @@ -13,12 +13,17 @@ * Change environment support for PointerEvent. */ +const emptyFunction = function() {}; + export function hasPointerEvent() { return global != null && global.PointerEvent != null; } export function setPointerEvent(bool) { - global.PointerEvent = bool ? function() {} : undefined; + const mock = bool ? emptyFunction : undefined; + global.PointerEvent = mock; + global.HTMLElement.prototype.setPointerCapture = mock; + global.HTMLElement.prototype.releasePointerCapture = mock; } /** diff --git a/packages/react-events/src/dom/testing-library/domEventSequences.js b/packages/react-events/src/dom/testing-library/domEventSequences.js index ebda2ec05779d..6272cd0da1a87 100644 --- a/packages/react-events/src/dom/testing-library/domEventSequences.js +++ b/packages/react-events/src/dom/testing-library/domEventSequences.js @@ -138,12 +138,18 @@ export function pointermove(target, payload) { const dispatch = arg => target.dispatchEvent(arg); const pointerType = getPointerType(payload); if (hasPointerEvent()) { - dispatch(domEvents.pointermove(payload)); - } - if (pointerType === 'mouse') { - dispatch(domEvents.mousemove(payload)); + dispatch( + domEvents.pointermove({ + pressure: pointerType === 'touch' ? 1 : 0.5, + ...payload, + }), + ); } else { - dispatch(domEvents.touchmove(payload)); + if (pointerType === 'mouse') { + dispatch(domEvents.mousemove(payload)); + } else { + dispatch(domEvents.touchmove(payload)); + } } } diff --git a/packages/react-events/src/dom/testing-library/domEvents.js b/packages/react-events/src/dom/testing-library/domEvents.js index 876ba4bd66d4b..e7b09c92c5ac9 100644 --- a/packages/react-events/src/dom/testing-library/domEvents.js +++ b/packages/react-events/src/dom/testing-library/domEvents.js @@ -64,6 +64,7 @@ function createPointerEvent( altKey = false, buttons = buttonsType.none, ctrlKey = false, + detail = 1, height, metaKey = false, movementX = 0, @@ -76,6 +77,8 @@ function createPointerEvent( pressure = 0, preventDefault = emptyFunction, pointerType = 'mouse', + screenX, + screenY, shiftKey = false, tangentialPressure = 0, tiltX = 0, @@ -87,6 +90,7 @@ function createPointerEvent( } = {}, ) { const modifierState = {altKey, ctrlKey, metaKey, shiftKey}; + const isMouse = pointerType === 'mouse'; return createEvent(type, { altKey, @@ -94,15 +98,11 @@ function createPointerEvent( clientX: x, clientY: y, ctrlKey, + detail, getModifierState(keyArg) { createGetModifierState(keyArg, modifierState); }, - height: - pointerType === 'mouse' - ? 1 - : height != null - ? height - : defaultPointerSize, + height: isMouse ? 1 : height != null ? height : defaultPointerSize, metaKey, movementX, movementY, @@ -114,15 +114,16 @@ function createPointerEvent( pointerType, pressure, preventDefault, - screenX: x, - screenY: y + defaultBrowserChromeSize, + releasePointerCapture: emptyFunction, + screenX: screenX === 0 ? screenX : x, + screenY: screenY === 0 ? screenY : y + defaultBrowserChromeSize, + setPointerCapture: emptyFunction, shiftKey, tangentialPressure, tiltX, tiltY, twist, - width: - pointerType === 'mouse' ? 1 : width != null ? width : defaultPointerSize, + width: isMouse ? 1 : width != null ? width : defaultPointerSize, }); } @@ -158,6 +159,7 @@ function createMouseEvent( altKey = false, buttons = buttonsType.none, ctrlKey = false, + detail = 1, metaKey = false, movementX = 0, movementY = 0, @@ -166,81 +168,107 @@ function createMouseEvent( pageX, pageY, preventDefault = emptyFunction, + screenX, + screenY, shiftKey = false, x = 0, y = 0, } = {}, - virtual = false, ) { const modifierState = {altKey, ctrlKey, metaKey, shiftKey}; return createEvent(type, { altKey, buttons, - clientX: virtual ? 0 : x, - clientY: virtual ? 0 : y, + clientX: x, + clientY: y, ctrlKey, - detail: virtual ? 0 : 1, + detail, getModifierState(keyArg) { createGetModifierState(keyArg, modifierState); }, metaKey, - movementX: virtual ? 0 : movementX, - movementY: virtual ? 0 : movementY, - offsetX: virtual ? 0 : offsetX, - offsetY: virtual ? 0 : offsetY, - pageX: virtual ? 0 : pageX || x, - pageY: virtual ? 0 : pageY || y, + movementX, + movementY, + offsetX, + offsetY, + pageX: pageX || x, + pageY: pageY || y, preventDefault, - screenX: virtual ? 0 : x, - screenY: virtual ? 0 : y + defaultBrowserChromeSize, + screenX: screenX === 0 ? screenX : x, + screenY: screenY === 0 ? screenY : y + defaultBrowserChromeSize, shiftKey, }); } -function createTouchEvent( - type, - { - altKey = false, - ctrlKey = false, - height = defaultPointerSize, - metaKey = false, - pageX, - pageY, - pointerId = 1, - preventDefault = emptyFunction, - shiftKey = false, - twist = 0, - width = defaultPointerSize, - x = 0, - y = 0, - } = {}, -) { - const touch = { - clientX: x, - clientY: y, - force: 1, - identifier: pointerId, - pageX: pageX || x, - pageY: pageY || y, - radiusX: width / 2, - radiusY: height / 2, - rotationAngle: twist, - screenX: x, - screenY: y + defaultBrowserChromeSize, - }; +function createTouchEvent(type, payload) { + const touchesPayload = Array.isArray(payload) ? payload : [payload]; + const firstTouch = touchesPayload[0]; + let altKey = false; + let ctrlKey = false; + let metaKey = false; + let preventDefault = emptyFunction; + let shiftKey = false; + if (firstTouch != null) { + if (firstTouch.altKey != null) { + altKey = firstTouch.altKey; + } + if (firstTouch.ctrlKey != null) { + ctrlKey = firstTouch.ctrlKey; + } + if (firstTouch.metaKey != null) { + metaKey = firstTouch.metaKey; + } + if (firstTouch.preventDefault != null) { + preventDefault = firstTouch.preventDefault; + } + if (firstTouch.shiftKey != null) { + shiftKey = firstTouch.shiftKey; + } + } + + const touches = touchesPayload.map( + ({ + height = defaultPointerSize, + pageX, + pageY, + pointerId = 1, + twist = 0, + width = defaultPointerSize, + x = 0, + y = 0, + } = {}) => { + return { + clientX: x, + clientY: y, + force: 1, + identifier: pointerId, + pageX: pageX || x, + pageY: pageY || y, + radiusX: width / 2, + radiusY: height / 2, + rotationAngle: twist, + screenX: x, + screenY: y + defaultBrowserChromeSize, + }; + }, + ); - const activeTouch = type !== 'touchend' ? [touch] : null; + const activeTouches = type !== 'touchend' ? touches : null; return createEvent(type, { altKey, - changedTouches: [touch], + changedTouches: touches, ctrlKey, + detail: 0, metaKey, preventDefault, shiftKey, - targetTouches: activeTouch, - touches: activeTouch, + sourceCapabilities: { + firesTouchEvents: true, + }, + targetTouches: activeTouches, + touches: activeTouches, }); } @@ -253,11 +281,24 @@ export function blur({relatedTarget} = {}) { } export function click(payload) { - return createMouseEvent('click', payload, false); + return createMouseEvent('click', payload); } export function virtualclick(payload) { - return createMouseEvent('click', payload, true); + return createMouseEvent('click', { + ...payload, + buttons: 0, + detail: 0, + height: 1, + pageX: 0, + pageY: 0, + pressure: 0, + screenX: 0, + screenY: 0, + width: 1, + x: 0, + y: 0, + }); } export function contextmenu(payload) { @@ -301,11 +342,24 @@ export function lostpointercapture(payload) { } export function pointercancel(payload) { - return createPointerEvent('pointercancel', payload); + return createPointerEvent('pointercancel', { + ...payload, + buttons: 0, + detail: 0, + height: 1, + pageX: 0, + pageY: 0, + pressure: 0, + screenX: 0, + screenY: 0, + width: 1, + x: 0, + y: 0, + }); } export function pointerdown(payload) { - const isTouch = payload != null && payload.pointerType === 'mouse'; + const isTouch = payload != null && payload.pointerType === 'touch'; return createPointerEvent('pointerdown', { buttons: buttonsType.primary, pressure: isTouch ? 1 : 0.5, @@ -337,6 +391,7 @@ export function pointerup(payload) { return createPointerEvent('pointerup', { ...payload, buttons: buttonsType.none, + pressure: 0, }); } diff --git a/packages/react-events/src/dom/testing-library/index.js b/packages/react-events/src/dom/testing-library/index.js index 6c8f6f198894b..a8cebf0f77e1e 100644 --- a/packages/react-events/src/dom/testing-library/index.js +++ b/packages/react-events/src/dom/testing-library/index.js @@ -109,10 +109,35 @@ const createEventTarget = node => ({ }, }); +function describeWithPointerEvent(message, describeFn) { + const pointerEvent = 'PointerEvent'; + const fallback = 'MouseEvent/TouchEvent'; + describe.each` + value | name + ${true} | ${pointerEvent} + ${false} | ${fallback} + `(`${message}: $name`, entry => { + const hasPointerEvents = entry.value; + setPointerEvent(hasPointerEvents); + describeFn(hasPointerEvents); + }); +} + +function testWithPointerType(message, testFn) { + const table = hasPointerEvent() + ? ['mouse', 'touch', 'pen'] + : ['mouse', 'touch']; + test.each(table)(`${message}: %s`, pointerType => { + testFn(pointerType); + }); +} + export { buttonsType, createEventTarget, + describeWithPointerEvent, platform, hasPointerEvent, setPointerEvent, + testWithPointerType, }; 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(