From 66735151ed97d44f0f4533e5df13ece7afa64e84 Mon Sep 17 00:00:00 2001 From: Luna Ruan Date: Fri, 2 Aug 2019 10:44:24 -0700 Subject: [PATCH 01/20] initial babel --- package.json | 1 + .../babel/__tests__/fixtures/basic/input.js | 1 + .../babel/__tests__/fixtures/basic/output.js | 1 + .../__tests__/transform-react-jsx-test.js | 50 ++++ scripts/babel/transform-react-to-jsx.js | 273 ++++++++++++++++++ yarn.lock | 12 + 6 files changed, 338 insertions(+) create mode 100644 scripts/babel/__tests__/fixtures/basic/input.js create mode 100644 scripts/babel/__tests__/fixtures/basic/output.js create mode 100644 scripts/babel/__tests__/transform-react-jsx-test.js create mode 100644 scripts/babel/transform-react-to-jsx.js diff --git a/package.json b/package.json index 13b23b070a0bb..1c0176dcb4b59 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "packages/*" ], "devDependencies": { + "@babel/plugin-syntax-jsx": "^7.2.0", "@mattiasbuelens/web-streams-polyfill": "0.1.0", "art": "^0.10.1", "babel-cli": "^6.6.5", diff --git a/scripts/babel/__tests__/fixtures/basic/input.js b/scripts/babel/__tests__/fixtures/basic/input.js new file mode 100644 index 0000000000000..5818db6bc16b8 --- /dev/null +++ b/scripts/babel/__tests__/fixtures/basic/input.js @@ -0,0 +1 @@ +const a =
; diff --git a/scripts/babel/__tests__/fixtures/basic/output.js b/scripts/babel/__tests__/fixtures/basic/output.js new file mode 100644 index 0000000000000..81719c3db0fc0 --- /dev/null +++ b/scripts/babel/__tests__/fixtures/basic/output.js @@ -0,0 +1 @@ +const a = React.createElement('div', {className: 'foo'}); diff --git a/scripts/babel/__tests__/transform-react-jsx-test.js b/scripts/babel/__tests__/transform-react-jsx-test.js new file mode 100644 index 0000000000000..472bf93d3b818 --- /dev/null +++ b/scripts/babel/__tests__/transform-react-jsx-test.js @@ -0,0 +1,50 @@ +/** + * 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 transformReactJSX = require('../transform-react-jsx'); +const fs = require('fs'); +const path = require('path'); + +function transform(input, isDev) { + return babel.transform(input, { + plugins: [ + [ + './scripts/babel/transform-react-to-jsx', + { + module: 'bluebird', + method: 'coroutine', + }, + ], + ], + }).code; +} + +function compare(input, output) { + const compiled = transform(input); + expect(compiled).toEqual(output); +} + +const TEST_DIR = './scripts/babel/__tests__/fixtures'; +function makeTests() { + fs.readdirSync(TEST_DIR).forEach(filename => { + const testLoc = path.join(TEST_DIR, filename); + const inputLoc = path.join(testLoc, 'input.js'); + const outputLoc = path.join(testLoc, 'output.js'); + const input = fs.readFileSync(inputLoc, 'utf8'); + const output = fs.readFileSync(outputLoc, 'utf8'); + it(filename, () => { + compare(input, output); + }); + }); +} + +describe('transform react to jsx', () => { + makeTests(); +}); diff --git a/scripts/babel/transform-react-to-jsx.js b/scripts/babel/transform-react-to-jsx.js new file mode 100644 index 0000000000000..2993eaa55f7b0 --- /dev/null +++ b/scripts/babel/transform-react-to-jsx.js @@ -0,0 +1,273 @@ +'use strict'; + +const t = require('@babel/types'); +const esutils = require('esutils'); +const jsx = require('@babel/plugin-syntax-jsx'); + +function helper(opts) { + console.log(jsx); + const visitor = {}; + + visitor.JSXSpreadChild = function(path) { + throw path.buildCodeFrameError( + 'Spread children are not supported in React.' + ); + }; + + visitor.JSXElement = { + exit(path, file) { + const callExpr = buildElementCall(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.' + ); + } + const callExpr = buildFragmentCall(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) + ); + } + + 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); + } + + // //
should use React.createElement + // function isPropsSpreadFollowedByKey(node) { + + // } + + function buildElementCall(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 = buildOpeningElementAttributes(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.callee, 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 buildOpeningElementAttributes(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 { + _props.push(convertAttribute(prop)); + } + } + + 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 buildFragmentCall(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); + } + + file.set('usedFragment', true); + return state.call || t.callExpression(state.callee, args); + } +} + +module.exports = function(babel, options) { + // const NEW_PRAGMA = options.development ? 'React.jsx' : 'React.jsxDEV'; + const NEW_PRAGMA = 'React.jsx'; + const OLD_PRAGMA = 'React.createElement'; + + const createIdentifierParser = id => () => { + return id + .split('.') + .map(name => t.identifier(name)) + .reduce((object, property) => t.memberExpression(object, property)); + }; + + const visitor = helper({ + 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')(); + }, + }); + + visitor.JSXAttribute = function(path) { + if (t.isJSXElement(path.node.value)) { + path.node.value = t.v(path.node.value); + } + }; + + return { + name: 'transform-react-jsx', + inherits: jsx, + visitor, + }; +}; diff --git a/yarn.lock b/yarn.lock index 6605e61fa9cdb..1a457be41e621 100644 --- a/yarn.lock +++ b/yarn.lock @@ -38,6 +38,11 @@ dependencies: "@babel/types" "^7.0.0" +"@babel/helper-plugin-utils@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0.tgz#bbb3fbee98661c569034237cc03967ba99b4f250" + integrity sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA== + "@babel/helper-split-export-declaration@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0.tgz#3aae285c0311c2ab095d997b8c9a94cad547d813" @@ -64,6 +69,13 @@ version "7.1.0" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.1.0.tgz#a7cd42cb3c12aec52e24375189a47b39759b783e" +"@babel/plugin-syntax-jsx@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.2.0.tgz#0b85a3b4bc7cdf4cc4b8bf236335b907ca22e7c7" + integrity sha512-VyN4QANJkRW6lDBmENzRszvZf3/4AXaj9YR7GwrWeeN9tEBPuXbmDYVU9bYBN0D70zCWVwUy0HWq2553VCb6Hw== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/template@^7.1.0": version "7.1.0" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.1.0.tgz#58cc9572e1bfe24fe1537fdf99d839d53e517e22" From bef39280a4d636ba000322da2b5d8fed9615ac1c Mon Sep 17 00:00:00 2001 From: Luna Ruan Date: Fri, 9 Aug 2019 13:05:06 -0700 Subject: [PATCH 02/20] added more stuff --- .../react-jsx-babel-plugin}/__tests__/transform-react-jsx-test.js | 0 .../react-jsx-babel-plugin}/transform-react-to-jsx.js | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename {scripts/babel => packages/react-jsx-babel-plugin}/__tests__/transform-react-jsx-test.js (100%) rename {scripts/babel => packages/react-jsx-babel-plugin}/transform-react-to-jsx.js (100%) diff --git a/scripts/babel/__tests__/transform-react-jsx-test.js b/packages/react-jsx-babel-plugin/__tests__/transform-react-jsx-test.js similarity index 100% rename from scripts/babel/__tests__/transform-react-jsx-test.js rename to packages/react-jsx-babel-plugin/__tests__/transform-react-jsx-test.js diff --git a/scripts/babel/transform-react-to-jsx.js b/packages/react-jsx-babel-plugin/transform-react-to-jsx.js similarity index 100% rename from scripts/babel/transform-react-to-jsx.js rename to packages/react-jsx-babel-plugin/transform-react-to-jsx.js From d773b2b229403c8fac22b944ec30f9c0023fb8b5 Mon Sep 17 00:00:00 2001 From: Luna Ruan Date: Fri, 9 Aug 2019 16:59:57 -0700 Subject: [PATCH 03/20] wrote the tests/snapshots --- babel.config.js | 2 +- .../transform-react-jsx-test.js.snap | 232 ++++++++++ .../__tests__/transform-react-jsx-test.js | 417 +++++++++++++++++- .../transform-react-to-jsx.js | 71 ++- .../babel/__tests__/fixtures/basic/input.js | 1 - .../babel/__tests__/fixtures/basic/output.js | 1 - 6 files changed, 682 insertions(+), 42 deletions(-) create mode 100644 packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-react-jsx-test.js.snap delete mode 100644 scripts/babel/__tests__/fixtures/basic/input.js delete mode 100644 scripts/babel/__tests__/fixtures/basic/output.js diff --git a/babel.config.js b/babel.config.js index d4d1e3213c574..30114aab44568 100644 --- a/babel.config.js +++ b/babel.config.js @@ -3,7 +3,7 @@ module.exports = { plugins: [ '@babel/plugin-syntax-jsx', - '@babel/plugin-transform-react-jsx', + // '@babel/plugin-transform-react-jsx', '@babel/plugin-transform-flow-strip-types', ['@babel/plugin-proposal-class-properties', {loose: true}], 'syntax-trailing-function-commas', diff --git a/packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-react-jsx-test.js.snap b/packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-react-jsx-test.js.snap new file mode 100644 index 0000000000000..cdb9eebd249d8 --- /dev/null +++ b/packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-react-jsx-test.js.snap @@ -0,0 +1,232 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +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 display name assignment expression 1`] = ` +"var Component; +Component = React.createClass({ + render: function render() { + return null; + } +});" +`; + +exports[`transform react to jsx display name export default 1`] = ` +"export default React.createClass({ + render: function render() { + return null; + } +});" +`; + +exports[`transform react to jsx display name if missing 1`] = ` +"var Whateva = React.createClass({ + displayName: \\"Whatever\\", + render: function render() { + return null; + } +}); +var Bar = React.createClass({ + \\"displayName\\": \\"Ba\\", + render: function render() { + return null; + } +});" +`; + +exports[`transform react to jsx display name object declaration 1`] = ` +"exports = { + Component: React.createClass({ + render: function render() { + return null; + } + }) +};" +`; + +exports[`transform react to jsx display name property assignment 1`] = ` +"exports.Component = React.createClass({ + render: function render() { + return null; + } +});" +`; + +exports[`transform react to jsx display name variable declaration 1`] = ` +"var Component = React.createClass({ + render: function render() { + return null; + } +});" +`; + +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 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/react-jsx-babel-plugin/__tests__/transform-react-jsx-test.js b/packages/react-jsx-babel-plugin/__tests__/transform-react-jsx-test.js index 472bf93d3b818..afbc65a32c087 100644 --- a/packages/react-jsx-babel-plugin/__tests__/transform-react-jsx-test.js +++ b/packages/react-jsx-babel-plugin/__tests__/transform-react-jsx-test.js @@ -7,44 +7,415 @@ /* eslint-disable quotes */ 'use strict'; -const babel = require('babel-core'); +const babel = require('@babel/core'); // const transformReactJSX = require('../transform-react-jsx'); -const fs = require('fs'); -const path = require('path'); +const codeFrame = require('@babel/code-frame'); -function transform(input, isDev) { +function transform(input, options) { return babel.transform(input, { plugins: [ [ - './scripts/babel/transform-react-to-jsx', + './packages/react-jsx-babel-plugin/transform-react-to-jsx', + // '@babel/plugin-transform-react-jsx', { module: 'bluebird', method: 'coroutine', + development: __DEV__, + useBuiltIns: true, + ...options, }, ], ], }).code; } -function compare(input, output) { - const compiled = transform(input); - expect(compiled).toEqual(output); -} +// function compare(input, output) { +// const compiled = transform(input); +// expect(compiled).toEqual(output); +// } -const TEST_DIR = './scripts/babel/__tests__/fixtures'; -function makeTests() { - fs.readdirSync(TEST_DIR).forEach(filename => { - const testLoc = path.join(TEST_DIR, filename); - const inputLoc = path.join(testLoc, 'input.js'); - const outputLoc = path.join(testLoc, 'output.js'); - const input = fs.readFileSync(inputLoc, 'utf8'); - const output = fs.readFileSync(outputLoc, 'utf8'); - it(filename, () => { - compare(input, output); - }); - }); -} +// const TEST_DIR = './packages/react-jsx-babel-plugin/__tests__/fixtures'; +// function makeTests() { +// fs.readdirSync(TEST_DIR).forEach(filename => { +// const testLoc = path.join(TEST_DIR, filename); +// const inputLoc = path.join(testLoc, 'input.js'); +// const outputLoc = path.join(testLoc, 'output.js'); +// const input = fs.readFileSync(inputLoc, 'utf8'); +// const output = fs.readFileSync(outputLoc, 'utf8'); +// it(filename, () => { +// compare(input, output); +// }); +// }); +// } describe('transform react to jsx', () => { - makeTests(); + 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('display name assignment expression', () => { + expect( + transform(` + var Component; + Component = React.createClass({ + render: function render() { + return null; + } + }); + `) + ).toMatchSnapshot(); + }); + it('display name export default', () => { + expect( + transform(` + export default React.createClass({ + render: function render() { + return null; + } + }); + `) + ).toMatchSnapshot(); + }); + it('display name if missing', () => { + expect( + transform(` + var Whateva = React.createClass({ + displayName: "Whatever", + render: function render() { + return null; + } + }); + + var Bar = React.createClass({ + "displayName": "Ba", + render: function render() { + return null; + } + }); + `) + ).toMatchSnapshot(); + }); + it('display name object declaration', () => { + expect( + transform(` + exports = { + Component: React.createClass({ + render: function render() { + return null; + } + }) + }; + `) + ).toMatchSnapshot(); + }); + it('display name property assignment', () => { + expect( + transform(` + exports.Component = React.createClass({ + render: function render() { + return null; + } + }); + `) + ).toMatchSnapshot(); + }); + it('display name variable declaration', () => { + expect( + transform(` + var Component = React.createClass({ + render: function render() { + return null; + } + }); + `) + ).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', () => { + 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(); + }); + 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(); + }); }); diff --git a/packages/react-jsx-babel-plugin/transform-react-to-jsx.js b/packages/react-jsx-babel-plugin/transform-react-to-jsx.js index 2993eaa55f7b0..aac9013e39407 100644 --- a/packages/react-jsx-babel-plugin/transform-react-to-jsx.js +++ b/packages/react-jsx-babel-plugin/transform-react-to-jsx.js @@ -2,19 +2,34 @@ const t = require('@babel/types'); const esutils = require('esutils'); -const jsx = require('@babel/plugin-syntax-jsx'); +// const jsx = '@babel/plugin-syntax-jsx'; function helper(opts) { - console.log(jsx); 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.' + 'Spread children are not supported in React.', ); }; visitor.JSXElement = { + enter(path, state) { + // console.log(path, state); + }, exit(path, file) { const callExpr = buildElementCall(path, file); if (callExpr) { @@ -27,7 +42,7 @@ function helper(opts) { exit(path, file) { if (opts.compat) { throw path.buildCodeFrameError( - 'Fragment tags are only supported in React 16 and up.' + 'Fragment tags are only supported in React 16 and up.', ); } const callExpr = buildFragmentCall(path, file); @@ -36,7 +51,6 @@ function helper(opts) { } }, }; - return visitor; function convertJSXIdentifier(node, parent) { @@ -51,8 +65,14 @@ function helper(opts) { } else if (t.isJSXMemberExpression(node)) { return t.memberExpression( convertJSXIdentifier(node.object, node), - convertJSXIdentifier(node.property, 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; @@ -80,7 +100,7 @@ function helper(opts) { if (t.isJSXNamespacedName(node.name)) { node.name = t.stringLiteral( - node.name.namespace.name + ':' + node.name.name.name + node.name.namespace.name + ':' + node.name.name.name, ); } else if (esutils.keyword.isIdentifierNameES6(node.name.name)) { node.name.type = 'Identifier'; @@ -104,7 +124,7 @@ function helper(opts) { const tagExpr = convertJSXIdentifier( openingPath.node.name, - openingPath.node + openingPath.node, ); const args = []; @@ -163,7 +183,7 @@ function helper(opts) { if (typeof useBuiltIns !== 'boolean') { throw new Error( 'transform-react-jsx currently only accepts a boolean option for ' + - 'useBuiltIns (defaults to false)' + 'useBuiltIns (defaults to false)', ); } @@ -231,11 +251,7 @@ function helper(opts) { } } -module.exports = function(babel, options) { - // const NEW_PRAGMA = options.development ? 'React.jsx' : 'React.jsxDEV'; - const NEW_PRAGMA = 'React.jsx'; - const OLD_PRAGMA = 'React.createElement'; - +module.exports = function(babel) { const createIdentifierParser = id => () => { return id .split('.') @@ -259,15 +275,38 @@ module.exports = function(babel, options) { }, }); + visitor.Program = { + enter(path, state) { + const pragma = state.opts.development ? 'React.jsxDEV' : 'React.jsx'; + const pragmaFrag = 'React.Fragment'; + state.set('jsxIdentifier', createIdentifierParser(pragma)); + state.set('jsxFragIdentifier', createIdentifierParser(pragmaFrag)); + state.set('usedFragment', false); + state.set('pragmaSet', true); + state.set('pragmaFragSet', true); + }, + exit(path, state) { + if ( + state.get('pragmaSet') && + state.get('usedFragment') && + !state.get('pragmaFragSet') + ) { + throw new Error( + 'transform-react-jsx: pragma has been set but ' + + 'pragmafrag has not been set', + ); + } + }, + }; + visitor.JSXAttribute = function(path) { if (t.isJSXElement(path.node.value)) { - path.node.value = t.v(path.node.value); + path.node.value = t.jsxExpressionContainer(path.node.value); } }; return { name: 'transform-react-jsx', - inherits: jsx, visitor, }; }; diff --git a/scripts/babel/__tests__/fixtures/basic/input.js b/scripts/babel/__tests__/fixtures/basic/input.js deleted file mode 100644 index 5818db6bc16b8..0000000000000 --- a/scripts/babel/__tests__/fixtures/basic/input.js +++ /dev/null @@ -1 +0,0 @@ -const a =
; diff --git a/scripts/babel/__tests__/fixtures/basic/output.js b/scripts/babel/__tests__/fixtures/basic/output.js deleted file mode 100644 index 81719c3db0fc0..0000000000000 --- a/scripts/babel/__tests__/fixtures/basic/output.js +++ /dev/null @@ -1 +0,0 @@ -const a = React.createElement('div', {className: 'foo'}); From d9450ab16162da038a3dd50c7163c43bb6c2bb30 Mon Sep 17 00:00:00 2001 From: Luna Ruan Date: Mon, 12 Aug 2019 18:16:28 -0700 Subject: [PATCH 04/20] wrote plugin and added tests --- ...form-react-jsx-createElement-test.js.snap} | 0 .../transform-react-jsx-jsx-test.js.snap | 344 ++++++++++++++ ...transform-react-jsx-createElement-test.js} | 21 +- .../__tests__/transform-react-jsx-jsx-test.js | 432 ++++++++++++++++++ .../transform-react-to-jsx.js | 171 +++++-- 5 files changed, 920 insertions(+), 48 deletions(-) rename packages/react-jsx-babel-plugin/__tests__/__snapshots__/{transform-react-jsx-test.js.snap => transform-react-jsx-createElement-test.js.snap} (100%) create mode 100644 packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-react-jsx-jsx-test.js.snap rename packages/react-jsx-babel-plugin/__tests__/{transform-react-jsx-test.js => transform-react-jsx-createElement-test.js} (93%) create mode 100644 packages/react-jsx-babel-plugin/__tests__/transform-react-jsx-jsx-test.js diff --git a/packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-react-jsx-test.js.snap b/packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-react-jsx-createElement-test.js.snap similarity index 100% rename from packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-react-jsx-test.js.snap rename to packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-react-jsx-createElement-test.js.snap diff --git a/packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-react-jsx-jsx-test.js.snap b/packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-react-jsx-jsx-test.js.snap new file mode 100644 index 0000000000000..5e15051de5b2a --- /dev/null +++ b/packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-react-jsx-jsx-test.js.snap @@ -0,0 +1,344 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +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, null); + }; +}; + +var bar = function () { + var _this2 = this; + + return function () { + return React.jsx(_this2.foo, null); + }; +};" +`; + +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.jsx(\\"div\\", { + children: [\\"foo\\", \\"bar\\", \\"baz\\", React.jsx(\\"div\\", { + children: \\"buz bang\\" + }), \\"qux\\", null, \\"quack\\"] +});" +`; + +exports[`transform react to jsx display name assignment expression 1`] = ` +"var Component; +Component = React.createClass({ + render: function render() { + return null; + } +});" +`; + +exports[`transform react to jsx display name export default 1`] = ` +"export default React.createClass({ + render: function render() { + return null; + } +});" +`; + +exports[`transform react to jsx display name if missing 1`] = ` +"var Whateva = React.createClass({ + displayName: \\"Whatever\\", + render: function render() { + return null; + } +}); +var Bar = React.createClass({ + \\"displayName\\": \\"Ba\\", + render: function render() { + return null; + } +});" +`; + +exports[`transform react to jsx display name object declaration 1`] = ` +"exports = { + Component: React.createClass({ + render: function render() { + return null; + } + }) +};" +`; + +exports[`transform react to jsx display name property assignment 1`] = ` +"exports.Component = React.createClass({ + render: function render() { + return null; + } +});" +`; + +exports[`transform react to jsx display name variable declaration 1`] = ` +"var Component = React.createClass({ + render: function render() { + return null; + } +});" +`; + +exports[`transform react to jsx properly handles keys 1`] = ` +"var x = React.jsx(\\"div\\", { + children: [React.jsx(\\"div\\", Object.assign({}), \\"1\\"), React.jsx(\\"div\\", { + meow: \\"wolf\\" + }, \\"2\\"), React.jsx(\\"div\\", Object.assign({}), \\"3\\")] +});" +`; + +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, null);"`; + +exports[`transform react to jsx should allow elements as attributes 1`] = ` +"React.jsx(\\"div\\", { + attr: React.jsx(\\"div\\", null) +});" +`; + +exports[`transform react to jsx should allow js namespacing 1`] = `"React.jsx(Namespace.Component, null);"`; + +exports[`transform react to jsx should allow nested fragments 1`] = ` +"React.jsx(\\"div\\", { + children: React.jsx(React.Fragment, { + children: [React.jsx(React.Fragment, { + children: [React.jsx(\\"span\\", { + children: \\"Hello\\" + }), React.jsx(\\"span\\", { + children: \\"world\\" + })] + }), React.jsx(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, null) +}); +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, null) +});" +`; + +exports[`transform react to jsx should convert simple tags 1`] = `"var x = React.jsx(\\"div\\", null);"`; + +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.jsx(\\"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.jsx(\\"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.jsx(\\"div\\", { + children: [React.jsx(\\"div\\", { + children: React.jsx(\\"br\\", null) + }), React.jsx(Component, { + children: [foo, React.jsx(\\"br\\", null), bar] + }), React.jsx(\\"br\\", null)] +});" +`; + +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.jsx(\\"div\\", { + children: [React.jsx(\\"span\\", null), React.jsx(\\"br\\", null)] +});" +`; + +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 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\\", null);"`; + +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 x = React.jsxDEV(\\"span\\", { + propOne: \\"one\\", + children: \\"Hi\\" +});" +`; + +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/react-jsx-babel-plugin/__tests__/transform-react-jsx-test.js b/packages/react-jsx-babel-plugin/__tests__/transform-react-jsx-createElement-test.js similarity index 93% rename from packages/react-jsx-babel-plugin/__tests__/transform-react-jsx-test.js rename to packages/react-jsx-babel-plugin/__tests__/transform-react-jsx-createElement-test.js index afbc65a32c087..14919db1c399c 100644 --- a/packages/react-jsx-babel-plugin/__tests__/transform-react-jsx-test.js +++ b/packages/react-jsx-babel-plugin/__tests__/transform-react-jsx-createElement-test.js @@ -8,7 +8,6 @@ 'use strict'; const babel = require('@babel/core'); -// const transformReactJSX = require('../transform-react-jsx'); const codeFrame = require('@babel/code-frame'); function transform(input, options) { @@ -22,6 +21,7 @@ function transform(input, options) { method: 'coroutine', development: __DEV__, useBuiltIns: true, + useCreateElement: true, ...options, }, ], @@ -29,25 +29,6 @@ function transform(input, options) { }).code; } -// function compare(input, output) { -// const compiled = transform(input); -// expect(compiled).toEqual(output); -// } - -// const TEST_DIR = './packages/react-jsx-babel-plugin/__tests__/fixtures'; -// function makeTests() { -// fs.readdirSync(TEST_DIR).forEach(filename => { -// const testLoc = path.join(TEST_DIR, filename); -// const inputLoc = path.join(testLoc, 'input.js'); -// const outputLoc = path.join(testLoc, 'output.js'); -// const input = fs.readFileSync(inputLoc, 'utf8'); -// const output = fs.readFileSync(outputLoc, 'utf8'); -// it(filename, () => { -// compare(input, output); -// }); -// }); -// } - describe('transform react to jsx', () => { it('should properly handle comments adjacent to children', () => { expect( diff --git a/packages/react-jsx-babel-plugin/__tests__/transform-react-jsx-jsx-test.js b/packages/react-jsx-babel-plugin/__tests__/transform-react-jsx-jsx-test.js new file mode 100644 index 0000000000000..43a4beb5f9f34 --- /dev/null +++ b/packages/react-jsx-babel-plugin/__tests__/transform-react-jsx-jsx-test.js @@ -0,0 +1,432 @@ +/** + * 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'); + +function transform(input, options) { + return babel.transform(input, { + plugins: [ + [ + './packages/react-jsx-babel-plugin/transform-react-to-jsx', + // '@babel/plugin-transform-react-jsx', + { + module: 'bluebird', + method: 'coroutine', + development: false, + useBuiltIns: true, + useCreateElement: false, + ...options, + }, + ], + ], + }).code; +} + +describe('transform react to jsx', () => { + it('uses jsxDEV instead of jsx in dev mode', () => { + expect( + transform(`var x = Hi`, {development: true}) + ).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('display name assignment expression', () => { + expect( + transform(` + var Component; + Component = React.createClass({ + render: function render() { + return null; + } + }); + `) + ).toMatchSnapshot(); + }); + it('display name export default', () => { + expect( + transform(` + export default React.createClass({ + render: function render() { + return null; + } + }); + `) + ).toMatchSnapshot(); + }); + it('display name if missing', () => { + expect( + transform(` + var Whateva = React.createClass({ + displayName: "Whatever", + render: function render() { + return null; + } + }); + + var Bar = React.createClass({ + "displayName": "Ba", + render: function render() { + return null; + } + }); + `) + ).toMatchSnapshot(); + }); + it('display name object declaration', () => { + expect( + transform(` + exports = { + Component: React.createClass({ + render: function render() { + return null; + } + }) + }; + `) + ).toMatchSnapshot(); + }); + it('display name property assignment', () => { + expect( + transform(` + exports.Component = React.createClass({ + render: function render() { + return null; + } + }); + `) + ).toMatchSnapshot(); + }); + it('display name variable declaration', () => { + expect( + transform(` + var Component = React.createClass({ + render: function render() { + return null; + } + }); + `) + ).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', () => { + 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(); + }); + 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(); + }); +}); diff --git a/packages/react-jsx-babel-plugin/transform-react-to-jsx.js b/packages/react-jsx-babel-plugin/transform-react-to-jsx.js index aac9013e39407..fd2dfb260b287 100644 --- a/packages/react-jsx-babel-plugin/transform-react-to-jsx.js +++ b/packages/react-jsx-babel-plugin/transform-react-to-jsx.js @@ -27,11 +27,15 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, }; visitor.JSXElement = { - enter(path, state) { - // console.log(path, state); - }, + enter(path, state) {}, exit(path, file) { - const callExpr = buildElementCall(path, file); + let callExpr; + if (file.opts.useCreateElement || useCreateElement(path)) { + callExpr = buildElementCall(path, file); + } else { + callExpr = buildJSXElementCall(path, file); + } + if (callExpr) { path.replaceWith(t.inherits(callExpr, path.node)); } @@ -45,6 +49,7 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, 'Fragment tags are only supported in React 16 and up.', ); } + const callExpr = buildFragmentCall(path, file); if (callExpr) { path.replaceWith(t.inherits(callExpr, path.node)); @@ -111,10 +116,70 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, return t.inherits(t.objectProperty(node.name, value), node); } - // //
should use React.createElement - // function isPropsSpreadFollowedByKey(node) { + 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 = openingPath.node.attributes; + let keyValue; + for (let i = 0, attrLength = attribs.length; i < attrLength; i++) { + const attr = attribs[i]; + if (t.isJSXAttribute(attr)) { + if (t.isJSXIdentifier(attr.name) && attr.name.name === 'key') { + keyValue = attr.value; + } + } + } + + if (attribs.length || path.node.children.length) { + attribs = buildOpeningElementAttributes( + attribs, + file, + true, + path.node.children, + ); + } else { + attribs = t.nullLiteral(); + } + + args.push(attribs); + + if (keyValue !== undefined) { + args.push(keyValue); + } + + if (opts.post) { + opts.post(state, file); + } - // } + return state.call || t.callExpression(state.callee, args); + } function buildElementCall(path, file) { if (opts.filter && !opts.filter(path.node, file)) return; @@ -158,7 +223,7 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, opts.post(state, file); } - return state.call || t.callExpression(state.callee, args); + return state.call || t.callExpression(state.oldCallee, args); } function pushProps(_props, objs) { @@ -175,7 +240,7 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, * all prior attributes to an array for later processing. */ - function buildOpeningElementAttributes(attribs, file) { + function buildOpeningElementAttributes(attribs, file, isReactJSX, children) { let _props = []; const objs = []; @@ -193,7 +258,28 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, _props = pushProps(_props, objs); objs.push(prop.argument); } else { - _props.push(convertAttribute(prop)); + const attr = convertAttribute(prop); + // if we are using React.JSX, we don't want to pass 'key' as a prop + // so don't add it to the list + if (!isReactJSX || attr.key.name !== 'key') { + _props.push(attr); + } + } + } + + // if we are using React.JSX, children is now a prop, so add it to the list + + console.log(children); + if (isReactJSX && 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), + ), + ); } } @@ -240,14 +326,53 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, } // no attributes are allowed with <> syntax - args.push(t.nullLiteral(), ...path.node.children); + // React.createElement uses different syntax than React.jsx + // createElement passes in children as a separate argument, + // whereas jsx passes children in as a prop + if (file.opts.useCreateElement) { + args.push(t.nullLiteral(), ...path.node.children); + } else { + args.push( + t.objectExpression([ + t.objectProperty( + t.identifier('children'), + t.arrayExpression(path.node.children), + ), + ]), + ); + } if (opts.post) { opts.post(state, file); } - file.set('usedFragment', true); - return state.call || t.callExpression(state.callee, args); + return ( + state.call || + t.callExpression( + file.opts.useCreateElement ? state.oldCallee : state.callee, + args, + ) + ); + } + + function useCreateElement(path) { + const openingPath = path.get('openingElement'); + const attributes = openingPath.node.attributes; + + let seenPropsSpread = false; + for (let i = 0, length = attributes.length; i < 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; } } @@ -272,6 +397,7 @@ module.exports = function(babel) { post(state, pass) { state.callee = pass.get('jsxIdentifier')(); + state.oldCallee = pass.get('oldJSXIdentifier')(); }, }); @@ -279,23 +405,12 @@ module.exports = function(babel) { enter(path, state) { const pragma = state.opts.development ? 'React.jsxDEV' : 'React.jsx'; const pragmaFrag = 'React.Fragment'; + state.set( + 'oldJSXIdentifier', + createIdentifierParser('React.createElement'), + ); state.set('jsxIdentifier', createIdentifierParser(pragma)); state.set('jsxFragIdentifier', createIdentifierParser(pragmaFrag)); - state.set('usedFragment', false); - state.set('pragmaSet', true); - state.set('pragmaFragSet', true); - }, - exit(path, state) { - if ( - state.get('pragmaSet') && - state.get('usedFragment') && - !state.get('pragmaFragSet') - ) { - throw new Error( - 'transform-react-jsx: pragma has been set but ' + - 'pragmafrag has not been set', - ); - } }, }; From 79ac078bc368e4902a1cd1b1d949cb9d0690666e Mon Sep 17 00:00:00 2001 From: Luna Ruan Date: Tue, 13 Aug 2019 10:57:38 -0700 Subject: [PATCH 05/20] some more stuff --- .../transform-react-jsx-jsx-test.js.snap | 21 +++++- .../__tests__/transform-react-jsx-jsx-test.js | 19 ++++++ .../transform-react-to-jsx.js | 67 ++++++++++++------- 3 files changed, 80 insertions(+), 27 deletions(-) diff --git a/packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-react-jsx-jsx-test.js.snap b/packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-react-jsx-jsx-test.js.snap index 5e15051de5b2a..084875a736072 100644 --- a/packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-react-jsx-jsx-test.js.snap +++ b/packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-react-jsx-jsx-test.js.snap @@ -98,12 +98,27 @@ exports[`transform react to jsx display name variable declaration 1`] = ` exports[`transform react to jsx properly handles keys 1`] = ` "var x = React.jsx(\\"div\\", { - children: [React.jsx(\\"div\\", Object.assign({}), \\"1\\"), React.jsx(\\"div\\", { + children: [React.jsx(\\"div\\", null, \\"1\\"), React.jsx(\\"div\\", { meow: \\"wolf\\" - }, \\"2\\"), React.jsx(\\"div\\", Object.assign({}), \\"3\\")] + }, \\"2\\"), React.jsx(\\"div\\", null, \\"3\\")] });" `; +exports[`transform react to jsx properly passes in source and self 1`] = ` +"var x = React.jsxDEV(\\"div\\", null, undefined, { + fileName: 'this/file.js', + lineNumber: 10 +}, this); +var y = React.jsxDEV(\\"div\\", null, undefined, undefined, this);" +`; + +exports[`transform react to jsx properly passes in source and self 2`] = ` +"var x = React.jsxDEV(\\"div\\", null, undefined, { + fileName: 'this/file.js', + lineNumber: 10 +}, this);" +`; + exports[`transform react to jsx should allow constructor as prop 1`] = ` "React.jsx(Component, { constructor: \\"foo\\" @@ -318,7 +333,7 @@ exports[`transform react to jsx uses jsxDEV instead of jsx in dev mode 1`] = ` "var x = React.jsxDEV(\\"span\\", { propOne: \\"one\\", children: \\"Hi\\" -});" +}, undefined, undefined, undefined);" `; exports[`transform react to jsx wraps props in react spread for first spread attributes 1`] = ` diff --git a/packages/react-jsx-babel-plugin/__tests__/transform-react-jsx-jsx-test.js b/packages/react-jsx-babel-plugin/__tests__/transform-react-jsx-jsx-test.js index 43a4beb5f9f34..7a89abeb6a73a 100644 --- a/packages/react-jsx-babel-plugin/__tests__/transform-react-jsx-jsx-test.js +++ b/packages/react-jsx-babel-plugin/__tests__/transform-react-jsx-jsx-test.js @@ -35,6 +35,25 @@ describe('transform react to jsx', () => { transform(`var x = Hi`, {development: true}) ).toMatchSnapshot(); }); + it('properly passes in source and self', () => { + expect( + transform( + `var x = ( +
+ ); + var y = ( +
+ );`, + {development: true} + ) + ).toMatchSnapshot(); + }); + it('properly handles keys', () => { expect( transform(`var x = ( diff --git a/packages/react-jsx-babel-plugin/transform-react-to-jsx.js b/packages/react-jsx-babel-plugin/transform-react-to-jsx.js index fd2dfb260b287..aae77b1f8282c 100644 --- a/packages/react-jsx-babel-plugin/transform-react-to-jsx.js +++ b/packages/react-jsx-babel-plugin/transform-react-to-jsx.js @@ -126,7 +126,6 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, openingPath.node.name, openingPath.node, ); - const args = []; let tagName; @@ -146,14 +145,29 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, opts.pre(state, file); } - let attribs = openingPath.node.attributes; - let keyValue; - for (let i = 0, attrLength = attribs.length; i < attrLength; i++) { - const attr = attribs[i]; - if (t.isJSXAttribute(attr)) { - if (t.isJSXIdentifier(attr.name) && attr.name.name === 'key') { - keyValue = attr.value; + 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, len = openingPath.node.attributes.length; i < len; 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); } } @@ -170,14 +184,22 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, args.push(attribs); - if (keyValue !== undefined) { - args.push(keyValue); + // __source and __self are only used in development + if (!file.opts.development) { + if (key !== undefined) { + args.push(key); + } + } else { + args.push( + key === undefined ? t.identifier('undefined') : key, + source === undefined ? t.identifier('undefined') : source, + self === undefined ? t.identifier('undefined') : self, + ); } if (opts.post) { opts.post(state, file); } - return state.call || t.callExpression(state.callee, args); } @@ -259,17 +281,12 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, objs.push(prop.argument); } else { const attr = convertAttribute(prop); - // if we are using React.JSX, we don't want to pass 'key' as a prop - // so don't add it to the list - if (!isReactJSX || attr.key.name !== 'key') { - _props.push(attr); - } + _props.push(attr); } } - // if we are using React.JSX, children is now a prop, so add it to the list - - console.log(children); + // In React.JSX, children is no longer a separate argument, but passed in + // through the argument object if (isReactJSX && children && children.length > 0) { if (children.length === 1) { _props.push(t.objectProperty(t.identifier('children'), children[0])); @@ -282,7 +299,6 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, ); } } - pushProps(_props, objs); if (objs.length === 1) { @@ -403,14 +419,17 @@ module.exports = function(babel) { visitor.Program = { enter(path, state) { - const pragma = state.opts.development ? 'React.jsxDEV' : 'React.jsx'; - const pragmaFrag = 'React.Fragment'; state.set( 'oldJSXIdentifier', createIdentifierParser('React.createElement'), ); - state.set('jsxIdentifier', createIdentifierParser(pragma)); - state.set('jsxFragIdentifier', createIdentifierParser(pragmaFrag)); + state.set( + 'jsxIdentifier', + createIdentifierParser( + state.opts.development ? 'React.jsxDEV' : 'React.jsx', + ), + ); + state.set('jsxFragIdentifier', createIdentifierParser('React.Fragment')); }, }; From 8c27b24c2681b66bf4ff98dbb32c18aa9c756225 Mon Sep 17 00:00:00 2001 From: Luna Ruan Date: Thu, 15 Aug 2019 17:27:36 -0700 Subject: [PATCH 06/20] fixed all the tests --- ...m-jsx-to-react-createElement-test.js.snap} | 0 ...> transform-jsx-to-react-jsx-test.js.snap} | 106 +++++++++---- ...nsform-jsx-to-react-createElement-test.js} | 6 +- ....js => transform-jsx-to-react-jsx-test.js} | 72 +++++++-- ...o-jsx.js => transform-jsx-to-react-jsx.js} | 140 ++++++++++++++---- packages/react/src/ReactElement.js | 47 +++--- packages/react/src/ReactElementValidator.js | 15 +- packages/shared/ReactFeatureFlags.js | 2 +- .../forks/ReactFeatureFlags.native-oss.js | 2 +- scripts/jest/preprocessor.js | 11 ++ 10 files changed, 301 insertions(+), 100 deletions(-) rename packages/react-jsx-babel-plugin/__tests__/__snapshots__/{transform-react-jsx-createElement-test.js.snap => transform-jsx-to-react-createElement-test.js.snap} (100%) rename packages/react-jsx-babel-plugin/__tests__/__snapshots__/{transform-react-jsx-jsx-test.js.snap => transform-jsx-to-react-jsx-test.js.snap} (76%) rename packages/react-jsx-babel-plugin/__tests__/{transform-react-jsx-createElement-test.js => transform-jsx-to-react-createElement-test.js} (98%) rename packages/react-jsx-babel-plugin/__tests__/{transform-react-jsx-jsx-test.js => transform-jsx-to-react-jsx-test.js} (89%) rename packages/react-jsx-babel-plugin/{transform-react-to-jsx.js => transform-jsx-to-react-jsx.js} (78%) diff --git a/packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-react-jsx-createElement-test.js.snap b/packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-jsx-to-react-createElement-test.js.snap similarity index 100% rename from packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-react-jsx-createElement-test.js.snap rename to packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-jsx-to-react-createElement-test.js.snap diff --git a/packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-react-jsx-jsx-test.js.snap b/packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-jsx-to-react-jsx-test.js.snap similarity index 76% rename from packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-react-jsx-jsx-test.js.snap rename to packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-jsx-to-react-jsx-test.js.snap index 084875a736072..24412cd51abd9 100644 --- a/packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-react-jsx-jsx-test.js.snap +++ b/packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-jsx-to-react-jsx-test.js.snap @@ -1,5 +1,11 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`transform react to jsx fragments 1`] = ` +"var x = React.jsx(React.Fragment, { + children: React.jsx(\\"div\\", {}) +});" +`; + exports[`transform react to jsx adds appropriate new lines when using spread attribute 1`] = ` "React.jsx(Component, Object.assign({}, props, { sound: \\"moo\\" @@ -11,7 +17,7 @@ exports[`transform react to jsx arrow functions 1`] = ` var _this = this; return function () { - return React.jsx(_this, null); + return React.jsx(_this, {}); }; }; @@ -19,7 +25,7 @@ var bar = function () { var _this2 = this; return function () { - return React.jsx(_this2.foo, null); + return React.jsx(_this2.foo, {}); }; };" `; @@ -96,26 +102,47 @@ exports[`transform react to jsx display name variable declaration 1`] = ` });" `; +exports[`transform react to jsx fragments in dev mode 1`] = ` +"var _jsxFileName = \\"\\"; +var x = React.jsxDEV(React.Fragment, { + children: React.jsxDEV(\\"div\\", {}, undefined, true, { + fileName: _jsxFileName, + lineNumber: 1 + }, this) +}, undefined, true);" +`; + +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', true, { + fileName: _jsxFileName, + lineNumber: 3 + }, this), React.jsxDEV(\\"span\\", {}, '1', true, { + fileName: _jsxFileName, + lineNumber: 3 + }, this)] +}, undefined, false, { + fileName: _jsxFileName, + lineNumber: 2 +}, this);" +`; + exports[`transform react to jsx properly handles keys 1`] = ` "var x = React.jsx(\\"div\\", { - children: [React.jsx(\\"div\\", null, \\"1\\"), React.jsx(\\"div\\", { + children: [React.jsx(\\"div\\", {}, \\"1\\"), React.jsx(\\"div\\", { meow: \\"wolf\\" - }, \\"2\\"), React.jsx(\\"div\\", null, \\"3\\")] + }, \\"2\\"), React.jsx(\\"div\\", {}, \\"3\\")] });" `; exports[`transform react to jsx properly passes in source and self 1`] = ` -"var x = React.jsxDEV(\\"div\\", null, undefined, { - fileName: 'this/file.js', - lineNumber: 10 -}, this); -var y = React.jsxDEV(\\"div\\", null, undefined, undefined, this);" -`; - -exports[`transform react to jsx properly passes in source and self 2`] = ` -"var x = React.jsxDEV(\\"div\\", null, undefined, { - fileName: 'this/file.js', - lineNumber: 10 +"var _jsxFileName = \\"\\"; +var x = React.jsxDEV(\\"div\\", {}, undefined, true, { + fileName: _jsxFileName, + lineNumber: 1 }, this);" `; @@ -125,15 +152,15 @@ exports[`transform react to jsx should allow constructor as prop 1`] = ` });" `; -exports[`transform react to jsx should allow deeper js namespacing 1`] = `"React.jsx(Namespace.DeepNamespace.Component, null);"`; +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\\", null) + attr: React.jsx(\\"div\\", {}) });" `; -exports[`transform react to jsx should allow js namespacing 1`] = `"React.jsx(Namespace.Component, null);"`; +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\\", { @@ -157,7 +184,7 @@ exports[`transform react to jsx should allow nested fragments 1`] = ` exports[`transform react to jsx should avoid wrapping in extra parens if not needed 1`] = ` "var x = React.jsx(\\"div\\", { - children: React.jsx(Component, null) + children: React.jsx(Component, {}) }); var x = React.jsx(\\"div\\", { children: props.children @@ -166,11 +193,11 @@ var x = React.jsx(Composite, { children: props.children }); var x = React.jsx(Composite, { - children: React.jsx(Composite2, null) + children: React.jsx(Composite2, {}) });" `; -exports[`transform react to jsx should convert simple tags 1`] = `"var x = React.jsx(\\"div\\", null);"`; +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\\", { @@ -244,10 +271,10 @@ exports[`transform react to jsx should handle has own property correctly 1`] = ` exports[`transform react to jsx should have correct comma in nested children 1`] = ` "var x = React.jsx(\\"div\\", { children: [React.jsx(\\"div\\", { - children: React.jsx(\\"br\\", null) + children: React.jsx(\\"br\\", {}) }), React.jsx(Component, { - children: [foo, React.jsx(\\"br\\", null), bar] - }), React.jsx(\\"br\\", null)] + children: [foo, React.jsx(\\"br\\", {}), bar] + }), React.jsx(\\"br\\", {})] });" `; @@ -285,7 +312,7 @@ exports[`transform react to jsx should not strip tags with a single child of nbs exports[`transform react to jsx should properly handle comments adjacent to children 1`] = ` "var x = React.jsx(\\"div\\", { - children: [React.jsx(\\"span\\", null), React.jsx(\\"br\\", null)] + children: [React.jsx(\\"span\\", {}), React.jsx(\\"br\\", {})] });" `; @@ -314,7 +341,26 @@ exports[`transform react to jsx should support xml namespaces if flag 1`] = ` });" `; -exports[`transform react to jsx should transform known hyphenated tags 1`] = `"React.jsx(\\"font-face\\", null);"`; +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, true, { + fileName: _jsxFileName, + lineNumber: 3 + }, this), [React.jsxDEV(\\"span\\", {}, '0', true, { + fileName: _jsxFileName, + lineNumber: 4 + }, this), React.jsxDEV(\\"span\\", {}, '1', true, { + fileName: _jsxFileName, + lineNumber: 4 + }, this)]] +}, undefined, true, { + fileName: _jsxFileName, + lineNumber: 2 +}, this);" +`; exports[`transform react to jsx uses createElement when the key comes after a spread 1`] = ` "var x = React.createElement(\\"div\\", Object.assign({}, props, { @@ -330,10 +376,14 @@ exports[`transform react to jsx uses jsx when the key comes before a spread 1`] `; exports[`transform react to jsx uses jsxDEV instead of jsx in dev mode 1`] = ` -"var x = React.jsxDEV(\\"span\\", { +"var _jsxFileName = \\"\\"; +var x = React.jsxDEV(\\"span\\", { propOne: \\"one\\", children: \\"Hi\\" -}, undefined, undefined, undefined);" +}, undefined, true, { + fileName: _jsxFileName, + lineNumber: 1 +}, this);" `; exports[`transform react to jsx wraps props in react spread for first spread attributes 1`] = ` diff --git a/packages/react-jsx-babel-plugin/__tests__/transform-react-jsx-createElement-test.js b/packages/react-jsx-babel-plugin/__tests__/transform-jsx-to-react-createElement-test.js similarity index 98% rename from packages/react-jsx-babel-plugin/__tests__/transform-react-jsx-createElement-test.js rename to packages/react-jsx-babel-plugin/__tests__/transform-jsx-to-react-createElement-test.js index 14919db1c399c..df5f917191458 100644 --- a/packages/react-jsx-babel-plugin/__tests__/transform-react-jsx-createElement-test.js +++ b/packages/react-jsx-babel-plugin/__tests__/transform-jsx-to-react-createElement-test.js @@ -12,10 +12,12 @@ const codeFrame = require('@babel/code-frame'); function transform(input, options) { return babel.transform(input, { + configFile: false, plugins: [ + '@babel/plugin-syntax-jsx', + '@babel/plugin-transform-arrow-functions', [ - './packages/react-jsx-babel-plugin/transform-react-to-jsx', - // '@babel/plugin-transform-react-jsx', + './packages/react-jsx-babel-plugin/transform-jsx-to-react-jsx', { module: 'bluebird', method: 'coroutine', diff --git a/packages/react-jsx-babel-plugin/__tests__/transform-react-jsx-jsx-test.js b/packages/react-jsx-babel-plugin/__tests__/transform-jsx-to-react-jsx-test.js similarity index 89% rename from packages/react-jsx-babel-plugin/__tests__/transform-react-jsx-jsx-test.js rename to packages/react-jsx-babel-plugin/__tests__/transform-jsx-to-react-jsx-test.js index 7a89abeb6a73a..1197e1f79080c 100644 --- a/packages/react-jsx-babel-plugin/__tests__/transform-react-jsx-jsx-test.js +++ b/packages/react-jsx-babel-plugin/__tests__/transform-jsx-to-react-jsx-test.js @@ -12,14 +12,21 @@ const codeFrame = require('@babel/code-frame'); function transform(input, options) { return 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/react-jsx-babel-plugin/transform-react-to-jsx', - // '@babel/plugin-transform-react-jsx', + './packages/react-jsx-babel-plugin/transform-jsx-to-react-jsx', { module: 'bluebird', method: 'coroutine', - development: false, useBuiltIns: true, useCreateElement: false, ...options, @@ -30,29 +37,62 @@ function transform(input, options) { } describe('transform react to jsx', () => { - it('uses jsxDEV instead of jsx in dev mode', () => { + it(' fragments', () => { + expect(transform(`var x = <>
`)).toMatchSnapshot(); + }); + it('fragments to set keys', () => { expect( - transform(`var x = Hi`, {development: true}) + transform(`var x = `) ).toMatchSnapshot(); }); - it('properly passes in source and self', () => { + it('fragments in dev mode', () => { + expect( + transform(`var x = <>
`, { + development: true, + }) + ).toMatchSnapshot(); + }); + it('nonStatic children', () => { expect( transform( `var x = ( -
+
+ {[, ]} +
); - var y = ( -
- );`, - {development: true} + `, + { + 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('properly handles keys', () => { expect( diff --git a/packages/react-jsx-babel-plugin/transform-react-to-jsx.js b/packages/react-jsx-babel-plugin/transform-jsx-to-react-jsx.js similarity index 78% rename from packages/react-jsx-babel-plugin/transform-react-to-jsx.js rename to packages/react-jsx-babel-plugin/transform-jsx-to-react-jsx.js index aae77b1f8282c..c0cab605c0a55 100644 --- a/packages/react-jsx-babel-plugin/transform-react-to-jsx.js +++ b/packages/react-jsx-babel-plugin/transform-jsx-to-react-jsx.js @@ -2,7 +2,6 @@ const t = require('@babel/types'); const esutils = require('esutils'); -// const jsx = '@babel/plugin-syntax-jsx'; function helper(opts) { const visitor = {}; @@ -31,7 +30,7 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, exit(path, file) { let callExpr; if (file.opts.useCreateElement || useCreateElement(path)) { - callExpr = buildElementCall(path, file); + callExpr = buildCreateElementCall(path, file); } else { callExpr = buildJSXElementCall(path, file); } @@ -56,6 +55,7 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, } }, }; + return visitor; function convertJSXIdentifier(node, parent) { @@ -91,6 +91,15 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, } } + function isStaticChildren(children) { + return !( + children.length === 1 && + (t.isArrayExpression(children[0]) || + t.isIdentifier(children[0]) || + t.isMemberExpression(children[0])) + ); + } + function convertAttribute(node) { const value = convertAttributeValue(node.value || t.booleanLiteral(true)); @@ -172,14 +181,13 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, } if (attribs.length || path.node.children.length) { - attribs = buildOpeningElementAttributes( + attribs = buildJSXOpeningElementAttributes( attribs, file, - true, path.node.children, ); } else { - attribs = t.nullLiteral(); + attribs = t.objectExpression([]); } args.push(attribs); @@ -192,6 +200,7 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, } else { args.push( key === undefined ? t.identifier('undefined') : key, + t.booleanLiteral(isStaticChildren(path.node.children)), source === undefined ? t.identifier('undefined') : source, self === undefined ? t.identifier('undefined') : self, ); @@ -203,7 +212,85 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, return state.call || t.callExpression(state.callee, args); } - function buildElementCall(path, file) { + /** + * 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 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 { + const attr = convertAttribute(prop); + _props.push(attr); + } + } + + // 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])) { + // this could be null, and jsx expects props to be non-null + const expressionHelper = useBuiltIns + ? t.memberExpression(t.identifier('Object'), t.identifier('assign')) + : file.addHelper('extends'); + + // spread it + attribs = t.callExpression(expressionHelper, [ + t.objectExpression([]), + ...objs, + ]); + } 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; + } + + function buildCreateElementCall(path, file) { if (opts.filter && !opts.filter(path.node, file)) return; const openingPath = path.get('openingElement'); @@ -261,8 +348,7 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, * breaking on spreads, we then push a new object containing * all prior attributes to an array for later processing. */ - - function buildOpeningElementAttributes(attribs, file, isReactJSX, children) { + function buildOpeningElementAttributes(attribs, file) { let _props = []; const objs = []; @@ -285,20 +371,6 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, } } - // In React.JSX, children is no longer a separate argument, but passed in - // through the argument object - if (isReactJSX && 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) { @@ -341,23 +413,29 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, opts.pre(state, file); } - // no attributes are allowed with <> syntax - // React.createElement uses different syntax than React.jsx - // createElement passes in children as a separate argument, - // whereas jsx passes children in as a prop if (file.opts.useCreateElement) { args.push(t.nullLiteral(), ...path.node.children); } else { + let childrenNode; + if (path.node.children.length === 0) { + childrenNode = t.nullLiteral(); + } else if (path.node.children.length === 1) { + childrenNode = path.node.children[0]; + } else { + childrenNode = t.arrayExpression(path.node.children); + } args.push( t.objectExpression([ - t.objectProperty( - t.identifier('children'), - t.arrayExpression(path.node.children), - ), + t.objectProperty(t.identifier('children'), childrenNode), ]), ); + if (file.opts.development) { + args.push( + t.identifier('undefined'), + t.booleanLiteral(isStaticChildren(path.node.children)), + ); + } } - if (opts.post) { opts.post(state, file); } diff --git a/packages/react/src/ReactElement.js b/packages/react/src/ReactElement.js index 9416a313b9648..858844c8e28e1 100644 --- a/packages/react/src/ReactElement.js +++ b/packages/react/src/ReactElement.js @@ -12,6 +12,7 @@ import {REACT_ELEMENT_TYPE} from 'shared/ReactSymbols'; import ReactCurrentOwner from './ReactCurrentOwner'; const hasOwnProperty = Object.prototype.hasOwnProperty; +const freeze = Object.freeze; const RESERVED_PROPS = { key: true, @@ -155,9 +156,9 @@ const ReactElement = function(type, key, ref, self, source, owner, props) { writable: false, value: source, }); - if (Object.freeze) { - Object.freeze(element.props); - Object.freeze(element); + if (freeze) { + freeze(element.props); + freeze(element); } } @@ -179,6 +180,16 @@ export function jsx(type, config, maybeKey) { let key = null; let ref = null; + // 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 (hasValidRef(config)) { ref = config.ref; } @@ -197,12 +208,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,6 +244,16 @@ export function jsxDEV(type, config, maybeKey, source, self) { let key = null; let ref = null; + // 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 (hasValidRef(config)) { ref = config.ref; } @@ -257,12 +272,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; @@ -273,6 +282,10 @@ export function jsxDEV(type, config, maybeKey, source, self) { } } + if (freeze && Array.isArray(props.children)) { + freeze(props.children); + } + if (key || ref) { const displayName = typeof type === 'function' @@ -344,8 +357,8 @@ export function createElement(type, config, children) { childArray[i] = arguments[i + 2]; } if (__DEV__) { - if (Object.freeze) { - Object.freeze(childArray); + if (freeze) { + freeze(childArray); } } props.children = childArray; diff --git a/packages/react/src/ReactElementValidator.js b/packages/react/src/ReactElementValidator.js index 6a1626a2e27e8..5f523c2611e0d 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); @@ -313,7 +315,7 @@ export function jsxWithValidation( warning( false, - 'React.jsx: type is invalid -- expected a string (for ' + + 'React.createElement: type is invalid -- expected a string (for ' + 'built-in components) or a class/function (for composite ' + 'components) but got: %s.%s', typeString, @@ -334,12 +336,17 @@ 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); + } + } else { + validateChildKeys(children, type); } } else { validateChildKeys(children, type); @@ -347,7 +354,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/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index f3ebeaa96c48e..e66457bf9f033 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -66,7 +66,7 @@ export const enableFlareAPI = false; export const enableFundamentalAPI = false; // New API for JSX transforms to target - https://github.com/reactjs/rfcs/pull/107 -export const enableJSXTransformAPI = false; +export const enableJSXTransformAPI = true; // We will enforce mocking scheduler with scheduler/unstable_mock at some point. (v17?) // Till then, we warn about the missing mock, but still fallback to a sync mode compatible version diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index 7db7b5728a246..5b33457e5f3b1 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -28,7 +28,7 @@ export const enableSchedulerDebugging = false; export const warnAboutDeprecatedSetNativeProps = false; export const enableFlareAPI = false; export const enableFundamentalAPI = false; -export const enableJSXTransformAPI = false; +export const enableJSXTransformAPI = true; export const warnAboutUnmockedScheduler = false; export const revertPassiveEffectsChange = false; export const flushSuspenseFallbacksInTests = true; diff --git a/scripts/jest/preprocessor.js b/scripts/jest/preprocessor.js index d97b61e5e6234..20d621b5550a0 100644 --- a/scripts/jest/preprocessor.js +++ b/scripts/jest/preprocessor.js @@ -22,6 +22,9 @@ const pathToBabelPluginWrapWarning = require.resolve( const pathToBabelPluginAsyncToGenerator = require.resolve( '@babel/plugin-transform-async-to-generator' ); +const pathToBabelReactJSXTransform = require.resolve( + '../../packages/react-jsx-babel-plugin/transform-jsx-to-react-jsx' +); const pathToBabelrc = path.join(__dirname, '..', '..', 'babel.config.js'); const pathToErrorCodes = require.resolve('../error-codes/codes.json'); @@ -39,6 +42,14 @@ const babelOptions = { // TODO: I have not verified that this actually works. require.resolve('@babel/plugin-transform-react-jsx-source'), + [ + pathToBabelReactJSXTransform, + { + development: process.env.NODE_ENV === 'development', + useCreateElement: false, + }, + ], + require.resolve('../babel/transform-prevent-infinite-loops'), ], retainLines: true, From 574c6611000976dbf768f0d23f41eb503f7b56bc Mon Sep 17 00:00:00 2001 From: Luna Ruan Date: Thu, 15 Aug 2019 17:34:25 -0700 Subject: [PATCH 07/20] cleaned up code some --- babel.config.js | 2 +- packages/react/src/ReactElementValidator.js | 2 +- packages/shared/ReactFeatureFlags.js | 2 +- packages/shared/forks/ReactFeatureFlags.native-oss.js | 4 ++-- scripts/jest/preprocessor.js | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/babel.config.js b/babel.config.js index 30114aab44568..d4d1e3213c574 100644 --- a/babel.config.js +++ b/babel.config.js @@ -3,7 +3,7 @@ module.exports = { plugins: [ '@babel/plugin-syntax-jsx', - // '@babel/plugin-transform-react-jsx', + '@babel/plugin-transform-react-jsx', '@babel/plugin-transform-flow-strip-types', ['@babel/plugin-proposal-class-properties', {loose: true}], 'syntax-trailing-function-commas', diff --git a/packages/react/src/ReactElementValidator.js b/packages/react/src/ReactElementValidator.js index 5f523c2611e0d..15957988c7cb6 100644 --- a/packages/react/src/ReactElementValidator.js +++ b/packages/react/src/ReactElementValidator.js @@ -315,7 +315,7 @@ export function jsxWithValidation( warning( false, - 'React.createElement: type is invalid -- expected a string (for ' + + 'React.jsx: type is invalid -- expected a string (for ' + 'built-in components) or a class/function (for composite ' + 'components) but got: %s.%s', typeString, diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index e66457bf9f033..f3ebeaa96c48e 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -66,7 +66,7 @@ export const enableFlareAPI = false; export const enableFundamentalAPI = false; // New API for JSX transforms to target - https://github.com/reactjs/rfcs/pull/107 -export const enableJSXTransformAPI = true; +export const enableJSXTransformAPI = false; // We will enforce mocking scheduler with scheduler/unstable_mock at some point. (v17?) // Till then, we warn about the missing mock, but still fallback to a sync mode compatible version diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index 5b33457e5f3b1..f8907cd1c9f85 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -28,10 +28,10 @@ export const enableSchedulerDebugging = false; export const warnAboutDeprecatedSetNativeProps = false; export const enableFlareAPI = false; export const enableFundamentalAPI = false; -export const enableJSXTransformAPI = true; +export const enableJSXTransformAPI = false; export const warnAboutUnmockedScheduler = false; export const revertPassiveEffectsChange = false; -export const flushSuspenseFallbacksInTests = true; +export const flushSuspenseFallbacksInTests = false; export const enableUserBlockingEvents = false; export const enableSuspenseCallback = false; export const warnAboutDefaultPropsOnFunctionComponents = false; diff --git a/scripts/jest/preprocessor.js b/scripts/jest/preprocessor.js index 20d621b5550a0..f049b089bad34 100644 --- a/scripts/jest/preprocessor.js +++ b/scripts/jest/preprocessor.js @@ -46,7 +46,7 @@ const babelOptions = { pathToBabelReactJSXTransform, { development: process.env.NODE_ENV === 'development', - useCreateElement: false, + useCreateElement: true, }, ], From 68640913261bf353e7f46f7a61adc4fba74c5a16 Mon Sep 17 00:00:00 2001 From: Luna Ruan Date: Fri, 16 Aug 2019 11:29:36 -0700 Subject: [PATCH 08/20] refactored code --- ...rm-jsx-to-react-createElement-test.js.snap | 31 ++++ .../transform-jsx-to-react-jsx-test.js.snap | 26 ++- ...ansform-jsx-to-react-createElement-test.js | 25 ++- .../transform-jsx-to-react-jsx-test.js | 18 +- .../transform-jsx-to-react-jsx.js | 173 +++++++++++------- scripts/jest/preprocessor.js | 8 - 6 files changed, 191 insertions(+), 90 deletions(-) diff --git a/packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-jsx-to-react-createElement-test.js.snap b/packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-jsx-to-react-createElement-test.js.snap index cdb9eebd249d8..6acbaacb4d0da 100644 --- a/packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-jsx-to-react-createElement-test.js.snap +++ b/packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-jsx-to-react-createElement-test.js.snap @@ -1,5 +1,23 @@ // 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\\" @@ -90,6 +108,19 @@ exports[`transform react to jsx display name variable declaration 1`] = ` });" `; +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\\" diff --git a/packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-jsx-to-react-jsx-test.js.snap b/packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-jsx-to-react-jsx-test.js.snap index 24412cd51abd9..833e4b199dd96 100644 --- a/packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-jsx-to-react-jsx-test.js.snap +++ b/packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-jsx-to-react-jsx-test.js.snap @@ -1,9 +1,11 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`transform react to jsx fragments 1`] = ` -"var x = React.jsx(React.Fragment, { - children: React.jsx(\\"div\\", {}) -});" +exports[`transform react to jsx React.fragment to set keys and source 1`] = ` +"var _jsxFileName = \\"\\"; +var x = React.jsxDEV(React.Fragment, {}, \\"foo\\", true, { + fileName: _jsxFileName, + lineNumber: 1 +}, this);" `; exports[`transform react to jsx adds appropriate new lines when using spread attribute 1`] = ` @@ -102,17 +104,25 @@ exports[`transform react to jsx display name variable declaration 1`] = ` });" `; -exports[`transform react to jsx fragments in dev mode 1`] = ` +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, true, { fileName: _jsxFileName, lineNumber: 1 }, this) -}, undefined, true);" +}, undefined, false);" `; -exports[`transform react to jsx fragments to set keys 1`] = `"var x = React.jsx(React.Fragment, {}, 'foo');"`; +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 = \\"\\"; @@ -380,7 +390,7 @@ exports[`transform react to jsx uses jsxDEV instead of jsx in dev mode 1`] = ` var x = React.jsxDEV(\\"span\\", { propOne: \\"one\\", children: \\"Hi\\" -}, undefined, true, { +}, undefined, false, { fileName: _jsxFileName, lineNumber: 1 }, this);" diff --git a/packages/react-jsx-babel-plugin/__tests__/transform-jsx-to-react-createElement-test.js b/packages/react-jsx-babel-plugin/__tests__/transform-jsx-to-react-createElement-test.js index df5f917191458..11c120d7bc7e3 100644 --- a/packages/react-jsx-babel-plugin/__tests__/transform-jsx-to-react-createElement-test.js +++ b/packages/react-jsx-babel-plugin/__tests__/transform-jsx-to-react-createElement-test.js @@ -16,11 +16,15 @@ function transform(input, options) { 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/react-jsx-babel-plugin/transform-jsx-to-react-jsx', { - module: 'bluebird', - method: 'coroutine', development: __DEV__, useBuiltIns: true, useCreateElement: true, @@ -32,6 +36,23 @@ function transform(input, options) { } 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(` diff --git a/packages/react-jsx-babel-plugin/__tests__/transform-jsx-to-react-jsx-test.js b/packages/react-jsx-babel-plugin/__tests__/transform-jsx-to-react-jsx-test.js index 1197e1f79080c..8e6e269b036df 100644 --- a/packages/react-jsx-babel-plugin/__tests__/transform-jsx-to-react-jsx-test.js +++ b/packages/react-jsx-babel-plugin/__tests__/transform-jsx-to-react-jsx-test.js @@ -25,8 +25,6 @@ function transform(input, options) { [ './packages/react-jsx-babel-plugin/transform-jsx-to-react-jsx', { - module: 'bluebird', - method: 'coroutine', useBuiltIns: true, useCreateElement: false, ...options, @@ -37,15 +35,25 @@ function transform(input, options) { } describe('transform react to jsx', () => { - it(' fragments', () => { + 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 = `) + transform(`var x = `) + ).toMatchSnapshot(); + }); + it('React.fragment to set keys and source', () => { + expect( + transform(`var x = `, { + development: true, + }) ).toMatchSnapshot(); }); - it('fragments in dev mode', () => { + it('fragments in dev mode (no key and source)', () => { expect( transform(`var x = <>
`, { development: true, diff --git a/packages/react-jsx-babel-plugin/transform-jsx-to-react-jsx.js b/packages/react-jsx-babel-plugin/transform-jsx-to-react-jsx.js index c0cab605c0a55..a0650f13cbf21 100644 --- a/packages/react-jsx-babel-plugin/transform-jsx-to-react-jsx.js +++ b/packages/react-jsx-babel-plugin/transform-jsx-to-react-jsx.js @@ -26,7 +26,6 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, }; visitor.JSXElement = { - enter(path, state) {}, exit(path, file) { let callExpr; if (file.opts.useCreateElement || useCreateElement(path)) { @@ -48,8 +47,13 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, 'Fragment tags are only supported in React 16 and up.', ); } + let callExpr; + if (file.opts.useCreateElement) { + callExpr = buildCreateElementFragmentCall(path, file); + } else { + callExpr = buildJSXFragmentCall(path, file); + } - const callExpr = buildFragmentCall(path, file); if (callExpr) { path.replaceWith(t.inherits(callExpr, path.node)); } @@ -91,15 +95,6 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, } } - function isStaticChildren(children) { - return !( - children.length === 1 && - (t.isArrayExpression(children[0]) || - t.isIdentifier(children[0]) || - t.isMemberExpression(children[0])) - ); - } - function convertAttribute(node) { const value = convertAttributeValue(node.value || t.booleanLiteral(true)); @@ -125,6 +120,34 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, return t.inherits(t.objectProperty(node.name, value), node); } + // We want to use React.createElement, even in the case of + // jsx, for
to distinguish it + // from
. This is an intermediary + // step while we deprecate key spread from props. Afterwards, + // we will remove createElement entirely + function useCreateElement(path) { + const openingPath = path.get('openingElement'); + const attributes = openingPath.node.attributes; + + let seenPropsSpread = false; + for (let i = 0, length = attributes.length; i < 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; @@ -187,20 +210,21 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, path.node.children, ); } else { + // attributes should never be null attribs = t.objectExpression([]); } args.push(attribs); - // __source and __self are only used in development 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(isStaticChildren(path.node.children)), + t.booleanLiteral(path.node.children.length !== 1), source === undefined ? t.identifier('undefined') : source, self === undefined ? t.identifier('undefined') : self, ); @@ -212,12 +236,6 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, return state.call || t.callExpression(state.callee, args); } - /** - * 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 buildJSXOpeningElementAttributes(attribs, file, children) { let _props = []; const objs = []; @@ -236,8 +254,7 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, _props = pushProps(_props, objs); objs.push(prop.argument); } else { - const attr = convertAttribute(prop); - _props.push(attr); + _props.push(convertAttribute(prop)); } } @@ -255,6 +272,7 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, ); } } + pushProps(_props, objs); if (objs.length === 1) { @@ -265,10 +283,9 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, ? t.memberExpression(t.identifier('Object'), t.identifier('assign')) : file.addHelper('extends'); - // spread it attribs = t.callExpression(expressionHelper, [ t.objectExpression([]), - ...objs, + objs[0], ]); } else { attribs = objs[0]; @@ -290,6 +307,69 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, 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( + file.opts.useCreateElement ? state.oldCallee : 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; @@ -393,7 +473,7 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, return attribs; } - function buildFragmentCall(path, file) { + function buildCreateElementFragmentCall(path, file) { if (opts.filter && !opts.filter(path.node, file)) return; const openingPath = path.get('openingElement'); @@ -413,29 +493,8 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, opts.pre(state, file); } - if (file.opts.useCreateElement) { - args.push(t.nullLiteral(), ...path.node.children); - } else { - let childrenNode; - if (path.node.children.length === 0) { - childrenNode = t.nullLiteral(); - } else if (path.node.children.length === 1) { - childrenNode = path.node.children[0]; - } else { - childrenNode = t.arrayExpression(path.node.children); - } - args.push( - t.objectExpression([ - t.objectProperty(t.identifier('children'), childrenNode), - ]), - ); - if (file.opts.development) { - args.push( - t.identifier('undefined'), - t.booleanLiteral(isStaticChildren(path.node.children)), - ); - } - } + args.push(t.nullLiteral(), ...path.node.children); + if (opts.post) { opts.post(state, file); } @@ -448,26 +507,6 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, ) ); } - - function useCreateElement(path) { - const openingPath = path.get('openingElement'); - const attributes = openingPath.node.attributes; - - let seenPropsSpread = false; - for (let i = 0, length = attributes.length; i < 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; - } } module.exports = function(babel) { diff --git a/scripts/jest/preprocessor.js b/scripts/jest/preprocessor.js index f049b089bad34..0bd11adf68b49 100644 --- a/scripts/jest/preprocessor.js +++ b/scripts/jest/preprocessor.js @@ -42,14 +42,6 @@ const babelOptions = { // TODO: I have not verified that this actually works. require.resolve('@babel/plugin-transform-react-jsx-source'), - [ - pathToBabelReactJSXTransform, - { - development: process.env.NODE_ENV === 'development', - useCreateElement: true, - }, - ], - require.resolve('../babel/transform-prevent-infinite-loops'), ], retainLines: true, From 0645bbb8a7d9144d372871b14520ae6cae4564d2 Mon Sep 17 00:00:00 2001 From: Luna Ruan Date: Fri, 16 Aug 2019 11:38:22 -0700 Subject: [PATCH 09/20] added another test --- .../transform-jsx-to-react-jsx-test.js.snap | 5 +++++ .../transform-jsx-to-react-jsx-test.js | 9 ++++++++- .../transform-jsx-to-react-jsx.js | 19 +++++-------------- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-jsx-to-react-jsx-test.js.snap b/packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-jsx-to-react-jsx-test.js.snap index 833e4b199dd96..e94281c6acc70 100644 --- a/packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-jsx-to-react-jsx-test.js.snap +++ b/packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-jsx-to-react-jsx-test.js.snap @@ -338,6 +338,11 @@ exports[`transform react to jsx should properly handle comments between props 1` });" `; +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\\", diff --git a/packages/react-jsx-babel-plugin/__tests__/transform-jsx-to-react-jsx-test.js b/packages/react-jsx-babel-plugin/__tests__/transform-jsx-to-react-jsx-test.js index 8e6e269b036df..69c47aafb1438 100644 --- a/packages/react-jsx-babel-plugin/__tests__/transform-jsx-to-react-jsx-test.js +++ b/packages/react-jsx-babel-plugin/__tests__/transform-jsx-to-react-jsx-test.js @@ -101,7 +101,14 @@ describe('transform react to jsx', () => { 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 = ( diff --git a/packages/react-jsx-babel-plugin/transform-jsx-to-react-jsx.js b/packages/react-jsx-babel-plugin/transform-jsx-to-react-jsx.js index a0650f13cbf21..5ea4aab1bcd32 100644 --- a/packages/react-jsx-babel-plugin/transform-jsx-to-react-jsx.js +++ b/packages/react-jsx-babel-plugin/transform-jsx-to-react-jsx.js @@ -236,6 +236,8 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, return state.call || t.callExpression(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 = []; @@ -358,13 +360,7 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, opts.post(state, file); } - return ( - state.call || - t.callExpression( - file.opts.useCreateElement ? state.oldCallee : state.callee, - args, - ) - ); + return state.call || t.callExpression(state.callee, args); } // Builds JSX into: @@ -493,19 +489,14 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, 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( - file.opts.useCreateElement ? state.oldCallee : state.callee, - args, - ) - ); + return state.call || t.callExpression(state.oldCallee, args); } } From 518c1512dc9166b86f3d008e55fd76806359f277 Mon Sep 17 00:00:00 2001 From: Luna Ruan Date: Fri, 16 Aug 2019 13:16:06 -0700 Subject: [PATCH 10/20] cleaned up more stuff --- packages/react-jsx-babel-plugin/transform-jsx-to-react-jsx.js | 4 ++-- packages/react/src/ReactElementValidator.js | 1 - scripts/jest/preprocessor.js | 3 --- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/react-jsx-babel-plugin/transform-jsx-to-react-jsx.js b/packages/react-jsx-babel-plugin/transform-jsx-to-react-jsx.js index 5ea4aab1bcd32..5f7d0dd4be35e 100644 --- a/packages/react-jsx-babel-plugin/transform-jsx-to-react-jsx.js +++ b/packages/react-jsx-babel-plugin/transform-jsx-to-react-jsx.js @@ -397,7 +397,7 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, let attribs = openingPath.node.attributes; if (attribs.length) { - attribs = buildOpeningElementAttributes(attribs, file); + attribs = buildCreateElementOpeningElementAttributes(attribs, file); } else { attribs = t.nullLiteral(); } @@ -424,7 +424,7 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, * breaking on spreads, we then push a new object containing * all prior attributes to an array for later processing. */ - function buildOpeningElementAttributes(attribs, file) { + function buildCreateElementOpeningElementAttributes(attribs, file) { let _props = []; const objs = []; diff --git a/packages/react/src/ReactElementValidator.js b/packages/react/src/ReactElementValidator.js index 15957988c7cb6..324c2449bddd0 100644 --- a/packages/react/src/ReactElementValidator.js +++ b/packages/react/src/ReactElementValidator.js @@ -336,7 +336,6 @@ 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) { diff --git a/scripts/jest/preprocessor.js b/scripts/jest/preprocessor.js index 7a8b63942b315..44642ec91888d 100644 --- a/scripts/jest/preprocessor.js +++ b/scripts/jest/preprocessor.js @@ -22,9 +22,6 @@ const pathToBabelPluginWrapWarning = require.resolve( const pathToBabelPluginAsyncToGenerator = require.resolve( '@babel/plugin-transform-async-to-generator' ); -const pathToBabelReactJSXTransform = require.resolve( - '../../packages/react-jsx-babel-plugin/transform-jsx-to-react-jsx' -); const pathToBabelrc = path.join(__dirname, '..', '..', 'babel.config.js'); const pathToErrorCodes = require.resolve('../error-codes/codes.json'); From 159c7f1834bc73c1b67c4515c44271c81d0525a7 Mon Sep 17 00:00:00 2001 From: Luna Ruan Date: Fri, 16 Aug 2019 14:00:45 -0700 Subject: [PATCH 11/20] added package.json --- packages/react-jsx-babel-plugin/package.json | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 packages/react-jsx-babel-plugin/package.json diff --git a/packages/react-jsx-babel-plugin/package.json b/packages/react-jsx-babel-plugin/package.json new file mode 100644 index 0000000000000..0d7185b0fc572 --- /dev/null +++ b/packages/react-jsx-babel-plugin/package.json @@ -0,0 +1,8 @@ +{ + "name": "react-jsx-babel-plugin", + "version": "1.0.0", + "description": "@babel/plugin-transform-react-jsx", + "main": "transform-jsx-to-react-jsx.js", + "repository": "https://github.com/facebook/react.git", + "license": "MIT" +} From 938d1638343ae69d9a6b5a5e140e82edf8e064a3 Mon Sep 17 00:00:00 2001 From: Luna Ruan Date: Fri, 16 Aug 2019 14:17:31 -0700 Subject: [PATCH 12/20] fixed linter --- ...ansform-jsx-to-react-createElement-test.js | 2 ++ .../transform-jsx-to-react-jsx-test.js | 2 ++ .../transform-jsx-to-react-jsx.js | 20 ++++++++++++++----- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/packages/react-jsx-babel-plugin/__tests__/transform-jsx-to-react-createElement-test.js b/packages/react-jsx-babel-plugin/__tests__/transform-jsx-to-react-createElement-test.js index 11c120d7bc7e3..8f4cc3abbd616 100644 --- a/packages/react-jsx-babel-plugin/__tests__/transform-jsx-to-react-createElement-test.js +++ b/packages/react-jsx-babel-plugin/__tests__/transform-jsx-to-react-createElement-test.js @@ -279,6 +279,7 @@ describe('transform react to jsx', () => { ).toMatchSnapshot(); }); it('should escape xhtml jsxtext', () => { + /* eslint-disable no-irregular-whitespace */ expect( transform(`
wow
; @@ -295,6 +296,7 @@ describe('transform react to jsx', () => {
w < w
; `) ).toMatchSnapshot(); + /*eslint-enable */ }); it('should handle attributed elements', () => { expect( diff --git a/packages/react-jsx-babel-plugin/__tests__/transform-jsx-to-react-jsx-test.js b/packages/react-jsx-babel-plugin/__tests__/transform-jsx-to-react-jsx-test.js index 69c47aafb1438..08a48617c6809 100644 --- a/packages/react-jsx-babel-plugin/__tests__/transform-jsx-to-react-jsx-test.js +++ b/packages/react-jsx-babel-plugin/__tests__/transform-jsx-to-react-jsx-test.js @@ -360,6 +360,7 @@ describe('transform react to jsx', () => { ).toMatchSnapshot(); }); it('should escape xhtml jsxtext', () => { + /* eslint-disable no-irregular-whitespace */ expect( transform(`
wow
; @@ -376,6 +377,7 @@ describe('transform react to jsx', () => {
w < w
; `) ).toMatchSnapshot(); + /*eslint-enable */ }); it('should handle attributed elements', () => { expect( diff --git a/packages/react-jsx-babel-plugin/transform-jsx-to-react-jsx.js b/packages/react-jsx-babel-plugin/transform-jsx-to-react-jsx.js index 5f7d0dd4be35e..d0a1029d3e0a1 100644 --- a/packages/react-jsx-babel-plugin/transform-jsx-to-react-jsx.js +++ b/packages/react-jsx-babel-plugin/transform-jsx-to-react-jsx.js @@ -149,7 +149,9 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, // 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; + if (opts.filter && !opts.filter(path.node, file)) { + return; + } const openingPath = path.get('openingElement'); openingPath.parent.children = t.react.buildChildren(openingPath.parent); @@ -313,7 +315,9 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, // Production: React.jsx(type, arguments) // Development: React.jsxDEV(type, { children}) function buildJSXFragmentCall(path, file) { - if (opts.filter && !opts.filter(path.node, file)) return; + if (opts.filter && !opts.filter(path.node, file)) { + return; + } const openingPath = path.get('openingElement'); openingPath.parent.children = t.react.buildChildren(openingPath.parent); @@ -367,7 +371,9 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, // 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; + if (opts.filter && !opts.filter(path.node, file)) { + return; + } const openingPath = path.get('openingElement'); openingPath.parent.children = t.react.buildChildren(openingPath.parent); @@ -412,7 +418,9 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, } function pushProps(_props, objs) { - if (!_props.length) return _props; + if (!_props.length) { + return _props; + } objs.push(t.objectExpression(_props)); return []; @@ -470,7 +478,9 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, } function buildCreateElementFragmentCall(path, file) { - if (opts.filter && !opts.filter(path.node, file)) return; + if (opts.filter && !opts.filter(path.node, file)) { + return; + } const openingPath = path.get('openingElement'); openingPath.parent.children = t.react.buildChildren(openingPath.parent); From ad5b0eda559bc6d0596a39aa57b2ee25fe2b4a35 Mon Sep 17 00:00:00 2001 From: Luna Ruan Date: Mon, 19 Aug 2019 15:55:45 -0700 Subject: [PATCH 13/20] updated tests and addressed some comments --- ...rm-jsx-to-react-createElement-test.js.snap | 66 ++------------- .../transform-jsx-to-react-jsx-test.js.snap | 66 ++------------- ...ansform-jsx-to-react-createElement-test.js | 82 ++----------------- .../transform-jsx-to-react-jsx-test.js | 82 ++----------------- .../transform-jsx-to-react-jsx.js | 2 +- packages/react/src/ReactElement.js | 31 ++++--- 6 files changed, 42 insertions(+), 287 deletions(-) diff --git a/packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-jsx-to-react-createElement-test.js.snap b/packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-jsx-to-react-createElement-test.js.snap index 6acbaacb4d0da..ac3405810ff48 100644 --- a/packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-jsx-to-react-createElement-test.js.snap +++ b/packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-jsx-to-react-createElement-test.js.snap @@ -50,64 +50,6 @@ exports[`transform react to jsx assignment 1`] = ` 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 display name assignment expression 1`] = ` -"var Component; -Component = React.createClass({ - render: function render() { - return null; - } -});" -`; - -exports[`transform react to jsx display name export default 1`] = ` -"export default React.createClass({ - render: function render() { - return null; - } -});" -`; - -exports[`transform react to jsx display name if missing 1`] = ` -"var Whateva = React.createClass({ - displayName: \\"Whatever\\", - render: function render() { - return null; - } -}); -var Bar = React.createClass({ - \\"displayName\\": \\"Ba\\", - render: function render() { - return null; - } -});" -`; - -exports[`transform react to jsx display name object declaration 1`] = ` -"exports = { - Component: React.createClass({ - render: function render() { - return null; - } - }) -};" -`; - -exports[`transform react to jsx display name property assignment 1`] = ` -"exports.Component = React.createClass({ - render: function render() { - return null; - } -});" -`; - -exports[`transform react to jsx display name variable declaration 1`] = ` -"var Component = React.createClass({ - render: function render() { - return null; - } -});" -`; - 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`] = ` @@ -240,6 +182,14 @@ exports[`transform react to jsx should support xml namespaces if flag 1`] = ` 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, diff --git a/packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-jsx-to-react-jsx-test.js.snap b/packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-jsx-to-react-jsx-test.js.snap index e94281c6acc70..8530a3c375528 100644 --- a/packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-jsx-to-react-jsx-test.js.snap +++ b/packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-jsx-to-react-jsx-test.js.snap @@ -46,64 +46,6 @@ exports[`transform react to jsx concatenates adjacent string literals 1`] = ` });" `; -exports[`transform react to jsx display name assignment expression 1`] = ` -"var Component; -Component = React.createClass({ - render: function render() { - return null; - } -});" -`; - -exports[`transform react to jsx display name export default 1`] = ` -"export default React.createClass({ - render: function render() { - return null; - } -});" -`; - -exports[`transform react to jsx display name if missing 1`] = ` -"var Whateva = React.createClass({ - displayName: \\"Whatever\\", - render: function render() { - return null; - } -}); -var Bar = React.createClass({ - \\"displayName\\": \\"Ba\\", - render: function render() { - return null; - } -});" -`; - -exports[`transform react to jsx display name object declaration 1`] = ` -"exports = { - Component: React.createClass({ - render: function render() { - return null; - } - }) -};" -`; - -exports[`transform react to jsx display name property assignment 1`] = ` -"exports.Component = React.createClass({ - render: function render() { - return null; - } -});" -`; - -exports[`transform react to jsx display name variable declaration 1`] = ` -"var Component = React.createClass({ - render: function render() { - return null; - } -});" -`; - exports[`transform react to jsx fragment with no children 1`] = `"var x = React.jsx(React.Fragment, {});"`; exports[`transform react to jsx fragments 1`] = ` @@ -377,6 +319,14 @@ var x = React.jsxDEV(\\"div\\", { }, 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\\", diff --git a/packages/react-jsx-babel-plugin/__tests__/transform-jsx-to-react-createElement-test.js b/packages/react-jsx-babel-plugin/__tests__/transform-jsx-to-react-createElement-test.js index 8f4cc3abbd616..49d3c7fa07cd5 100644 --- a/packages/react-jsx-babel-plugin/__tests__/transform-jsx-to-react-createElement-test.js +++ b/packages/react-jsx-babel-plugin/__tests__/transform-jsx-to-react-createElement-test.js @@ -113,83 +113,6 @@ describe('transform react to jsx', () => { `) ).toMatchSnapshot(); }); - it('display name assignment expression', () => { - expect( - transform(` - var Component; - Component = React.createClass({ - render: function render() { - return null; - } - }); - `) - ).toMatchSnapshot(); - }); - it('display name export default', () => { - expect( - transform(` - export default React.createClass({ - render: function render() { - return null; - } - }); - `) - ).toMatchSnapshot(); - }); - it('display name if missing', () => { - expect( - transform(` - var Whateva = React.createClass({ - displayName: "Whatever", - render: function render() { - return null; - } - }); - - var Bar = React.createClass({ - "displayName": "Ba", - render: function render() { - return null; - } - }); - `) - ).toMatchSnapshot(); - }); - it('display name object declaration', () => { - expect( - transform(` - exports = { - Component: React.createClass({ - render: function render() { - return null; - } - }) - }; - `) - ).toMatchSnapshot(); - }); - it('display name property assignment', () => { - expect( - transform(` - exports.Component = React.createClass({ - render: function render() { - return null; - } - }); - `) - ).toMatchSnapshot(); - }); - it('display name variable declaration', () => { - expect( - transform(` - var Component = React.createClass({ - render: function render() { - return null; - } - }); - `) - ).toMatchSnapshot(); - }); it('should allow constructor as prop', () => { expect(transform(`;`)).toMatchSnapshot(); }); @@ -424,4 +347,9 @@ describe('transform react to jsx', () => { 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/react-jsx-babel-plugin/__tests__/transform-jsx-to-react-jsx-test.js b/packages/react-jsx-babel-plugin/__tests__/transform-jsx-to-react-jsx-test.js index 08a48617c6809..8cfcf23f64098 100644 --- a/packages/react-jsx-babel-plugin/__tests__/transform-jsx-to-react-jsx-test.js +++ b/packages/react-jsx-babel-plugin/__tests__/transform-jsx-to-react-jsx-test.js @@ -194,83 +194,6 @@ describe('transform react to jsx', () => { `) ).toMatchSnapshot(); }); - it('display name assignment expression', () => { - expect( - transform(` - var Component; - Component = React.createClass({ - render: function render() { - return null; - } - }); - `) - ).toMatchSnapshot(); - }); - it('display name export default', () => { - expect( - transform(` - export default React.createClass({ - render: function render() { - return null; - } - }); - `) - ).toMatchSnapshot(); - }); - it('display name if missing', () => { - expect( - transform(` - var Whateva = React.createClass({ - displayName: "Whatever", - render: function render() { - return null; - } - }); - - var Bar = React.createClass({ - "displayName": "Ba", - render: function render() { - return null; - } - }); - `) - ).toMatchSnapshot(); - }); - it('display name object declaration', () => { - expect( - transform(` - exports = { - Component: React.createClass({ - render: function render() { - return null; - } - }) - }; - `) - ).toMatchSnapshot(); - }); - it('display name property assignment', () => { - expect( - transform(` - exports.Component = React.createClass({ - render: function render() { - return null; - } - }); - `) - ).toMatchSnapshot(); - }); - it('display name variable declaration', () => { - expect( - transform(` - var Component = React.createClass({ - render: function render() { - return null; - } - }); - `) - ).toMatchSnapshot(); - }); it('should allow constructor as prop', () => { expect(transform(`;`)).toMatchSnapshot(); }); @@ -505,4 +428,9 @@ describe('transform react to jsx', () => { 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/react-jsx-babel-plugin/transform-jsx-to-react-jsx.js b/packages/react-jsx-babel-plugin/transform-jsx-to-react-jsx.js index d0a1029d3e0a1..dfc043cced4ea 100644 --- a/packages/react-jsx-babel-plugin/transform-jsx-to-react-jsx.js +++ b/packages/react-jsx-babel-plugin/transform-jsx-to-react-jsx.js @@ -185,7 +185,7 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, 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 + // 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, len = openingPath.node.attributes.length; i < len; i++) { diff --git a/packages/react/src/ReactElement.js b/packages/react/src/ReactElement.js index 858844c8e28e1..84e9d888328ff 100644 --- a/packages/react/src/ReactElement.js +++ b/packages/react/src/ReactElement.js @@ -12,7 +12,6 @@ import {REACT_ELEMENT_TYPE} from 'shared/ReactSymbols'; import ReactCurrentOwner from './ReactCurrentOwner'; const hasOwnProperty = Object.prototype.hasOwnProperty; -const freeze = Object.freeze; const RESERVED_PROPS = { key: true, @@ -156,9 +155,9 @@ const ReactElement = function(type, key, ref, self, source, owner, props) { writable: false, value: source, }); - if (freeze) { - freeze(element.props); - freeze(element); + if (Object.freeze) { + Object.freeze(element.props); + Object.freeze(element); } } @@ -190,14 +189,14 @@ export function jsx(type, config, maybeKey) { key = '' + maybeKey; } - if (hasValidRef(config)) { - ref = config.ref; - } - 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 ( @@ -254,14 +253,14 @@ export function jsxDEV(type, config, maybeKey, source, self) { key = '' + maybeKey; } - if (hasValidRef(config)) { - ref = config.ref; - } - 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 ( @@ -282,8 +281,8 @@ export function jsxDEV(type, config, maybeKey, source, self) { } } - if (freeze && Array.isArray(props.children)) { - freeze(props.children); + if (Object.freeze && Array.isArray(props.children)) { + Object.freeze(props.children); } if (key || ref) { @@ -357,8 +356,8 @@ export function createElement(type, config, children) { childArray[i] = arguments[i + 2]; } if (__DEV__) { - if (freeze) { - freeze(childArray); + if (Object.freeze) { + Object.freeze(childArray); } } props.children = childArray; From 1b07249c3bd6e628f28755135396a39e5e261f88 Mon Sep 17 00:00:00 2001 From: Luna Ruan Date: Tue, 20 Aug 2019 10:59:09 -0700 Subject: [PATCH 14/20] components with static children use jsxs. made babel plugin part of the build --- packages/react-jsx-babel-plugin/README.md | 5 ++ .../transform-jsx-to-react-jsx-test.js.snap | 54 +++++++++---------- ...ansform-jsx-to-react-createElement-test.js | 2 +- .../transform-jsx-to-react-jsx-test.js | 2 +- packages/react-jsx-babel-plugin/index.js | 3 ++ packages/react-jsx-babel-plugin/npm/index.js | 7 +++ packages/react-jsx-babel-plugin/package.json | 20 +++++-- .../TransformJSXToReactBabelPlugin.js} | 32 +++++++++-- scripts/rollup/bundles.js | 8 +++ 9 files changed, 96 insertions(+), 37 deletions(-) create mode 100644 packages/react-jsx-babel-plugin/README.md create mode 100644 packages/react-jsx-babel-plugin/index.js create mode 100644 packages/react-jsx-babel-plugin/npm/index.js rename packages/react-jsx-babel-plugin/{transform-jsx-to-react-jsx.js => src/TransformJSXToReactBabelPlugin.js} (95%) diff --git a/packages/react-jsx-babel-plugin/README.md b/packages/react-jsx-babel-plugin/README.md new file mode 100644 index 0000000000000..3e2858d310c65 --- /dev/null +++ b/packages/react-jsx-babel-plugin/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/react-jsx-babel-plugin/__tests__/__snapshots__/transform-jsx-to-react-jsx-test.js.snap b/packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-jsx-to-react-jsx-test.js.snap index 8530a3c375528..510de731536f4 100644 --- a/packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-jsx-to-react-jsx-test.js.snap +++ b/packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-jsx-to-react-jsx-test.js.snap @@ -40,7 +40,7 @@ exports[`transform react to jsx assignment 1`] = ` exports[`transform react to jsx concatenates adjacent string literals 1`] = ` "var x = React.jsx(\\"div\\", { - children: [\\"foo\\", \\"bar\\", \\"baz\\", React.jsx(\\"div\\", { + children: [\\"foo\\", \\"bar\\", \\"baz\\", React.jsxs(\\"div\\", { children: \\"buz bang\\" }), \\"qux\\", null, \\"quack\\"] });" @@ -49,7 +49,7 @@ exports[`transform react to jsx concatenates adjacent string literals 1`] = ` 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, { +"var x = React.jsxs(React.Fragment, { children: React.jsx(\\"div\\", {}) });" `; @@ -115,18 +115,18 @@ exports[`transform react to jsx should allow elements as attributes 1`] = ` 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\\", { +"React.jsxs(\\"div\\", { children: React.jsx(React.Fragment, { children: [React.jsx(React.Fragment, { - children: [React.jsx(\\"span\\", { + children: [React.jsxs(\\"span\\", { children: \\"Hello\\" - }), React.jsx(\\"span\\", { + }), React.jsxs(\\"span\\", { children: \\"world\\" })] }), React.jsx(React.Fragment, { - children: [React.jsx(\\"span\\", { + children: [React.jsxs(\\"span\\", { children: \\"Goodbye\\" - }), React.jsx(\\"span\\", { + }), React.jsxs(\\"span\\", { children: \\"world\\" })] })] @@ -135,16 +135,16 @@ exports[`transform react to jsx should allow nested fragments 1`] = ` `; exports[`transform react to jsx should avoid wrapping in extra parens if not needed 1`] = ` -"var x = React.jsx(\\"div\\", { +"var x = React.jsxs(\\"div\\", { children: React.jsx(Component, {}) }); -var x = React.jsx(\\"div\\", { +var x = React.jsxs(\\"div\\", { children: props.children }); -var x = React.jsx(Composite, { +var x = React.jsxs(Composite, { children: props.children }); -var x = React.jsx(Composite, { +var x = React.jsxs(Composite, { children: React.jsx(Composite2, {}) });" `; @@ -152,7 +152,7 @@ var x = React.jsx(Composite, { 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\\", { +"var x = React.jsxs(\\"div\\", { children: \\"text\\" });" `; @@ -170,31 +170,31 @@ React.jsx(\\"div\\", { `; exports[`transform react to jsx should escape xhtml jsxtext 1`] = ` -"React.jsx(\\"div\\", { +"React.jsxs(\\"div\\", { children: \\"wow\\" }); -React.jsx(\\"div\\", { +React.jsxs(\\"div\\", { children: \\"w\\\\xF4w\\" }); -React.jsx(\\"div\\", { +React.jsxs(\\"div\\", { children: \\"w & w\\" }); -React.jsx(\\"div\\", { +React.jsxs(\\"div\\", { children: \\"w & w\\" }); -React.jsx(\\"div\\", { +React.jsxs(\\"div\\", { children: \\"w \\\\xA0 w\\" }); -React.jsx(\\"div\\", { +React.jsxs(\\"div\\", { children: \\"this should not parse as unicode: \\\\xA0\\" }); -React.jsx(\\"div\\", { +React.jsxs(\\"div\\", { children: \\"this should parse as nbsp: \\\\xA0 \\" }); React.jsx(\\"div\\", { children: [\\"this should parse as unicode: \\", '  '] }); -React.jsx(\\"div\\", { +React.jsxs(\\"div\\", { children: \\"w < w\\" });" `; @@ -208,21 +208,21 @@ exports[`transform react to jsx should handle attributed elements 1`] = ` } }); React.render(React.jsx(HelloMessage, { - name: React.jsx(\\"span\\", { + name: React.jsxs(\\"span\\", { children: \\"Sebastian\\" }) }), mountNode);" `; exports[`transform react to jsx should handle has own property correctly 1`] = ` -"React.jsx(\\"hasOwnProperty\\", { +"React.jsxs(\\"hasOwnProperty\\", { children: \\"testing\\" });" `; exports[`transform react to jsx should have correct comma in nested children 1`] = ` "var x = React.jsx(\\"div\\", { - children: [React.jsx(\\"div\\", { + children: [React.jsxs(\\"div\\", { children: React.jsx(\\"br\\", {}) }), React.jsx(Component, { children: [foo, React.jsx(\\"br\\", {}), bar] @@ -251,13 +251,13 @@ exports[`transform react to jsx should not add quotes to identifier names 1`] = `; exports[`transform react to jsx should not strip nbsp even couple with other whitespace 1`] = ` -"React.jsx(\\"div\\", { +"React.jsxs(\\"div\\", { children: \\"\\\\xA0 \\" });" `; exports[`transform react to jsx should not strip tags with a single child of nbsp 1`] = ` -"React.jsx(\\"div\\", { +"React.jsxs(\\"div\\", { children: \\"\\\\xA0\\" });" `; @@ -269,7 +269,7 @@ exports[`transform react to jsx should properly handle comments adjacent to chil `; exports[`transform react to jsx should properly handle comments between props 1`] = ` -"var x = React.jsx(\\"div\\", { +"var x = React.jsxs(\\"div\\", { /* a multi-line comment */ attr1: \\"foo\\", @@ -286,7 +286,7 @@ var x = React.jsx(\\"div\\", Object.assign({}, foo));" `; exports[`transform react to jsx should quote jsx attributes 1`] = ` -"React.jsx(\\"button\\", { +"React.jsxs(\\"button\\", { \\"data-value\\": \\"a value\\", children: \\"Button\\" });" diff --git a/packages/react-jsx-babel-plugin/__tests__/transform-jsx-to-react-createElement-test.js b/packages/react-jsx-babel-plugin/__tests__/transform-jsx-to-react-createElement-test.js index 49d3c7fa07cd5..1c77f630d0af2 100644 --- a/packages/react-jsx-babel-plugin/__tests__/transform-jsx-to-react-createElement-test.js +++ b/packages/react-jsx-babel-plugin/__tests__/transform-jsx-to-react-createElement-test.js @@ -23,7 +23,7 @@ function transform(input, options) { ] : []), [ - './packages/react-jsx-babel-plugin/transform-jsx-to-react-jsx', + './packages/react-jsx-babel-plugin/src/TransformJSXToReactBabelPlugin', { development: __DEV__, useBuiltIns: true, diff --git a/packages/react-jsx-babel-plugin/__tests__/transform-jsx-to-react-jsx-test.js b/packages/react-jsx-babel-plugin/__tests__/transform-jsx-to-react-jsx-test.js index 8cfcf23f64098..fc92beabc4cd2 100644 --- a/packages/react-jsx-babel-plugin/__tests__/transform-jsx-to-react-jsx-test.js +++ b/packages/react-jsx-babel-plugin/__tests__/transform-jsx-to-react-jsx-test.js @@ -23,7 +23,7 @@ function transform(input, options) { ] : []), [ - './packages/react-jsx-babel-plugin/transform-jsx-to-react-jsx', + './packages/react-jsx-babel-plugin/src/TransformJSXToReactBabelPlugin', { useBuiltIns: true, useCreateElement: false, diff --git a/packages/react-jsx-babel-plugin/index.js b/packages/react-jsx-babel-plugin/index.js new file mode 100644 index 0000000000000..7401bda105727 --- /dev/null +++ b/packages/react-jsx-babel-plugin/index.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = require('./src/TransformJSXToReactBabelPlugin'); diff --git a/packages/react-jsx-babel-plugin/npm/index.js b/packages/react-jsx-babel-plugin/npm/index.js new file mode 100644 index 0000000000000..af4ea6a44bb9a --- /dev/null +++ b/packages/react-jsx-babel-plugin/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/react-jsx-babel-plugin/package.json b/packages/react-jsx-babel-plugin/package.json index 0d7185b0fc572..22b4780fd1f0f 100644 --- a/packages/react-jsx-babel-plugin/package.json +++ b/packages/react-jsx-babel-plugin/package.json @@ -2,7 +2,21 @@ "name": "react-jsx-babel-plugin", "version": "1.0.0", "description": "@babel/plugin-transform-react-jsx", - "main": "transform-jsx-to-react-jsx.js", - "repository": "https://github.com/facebook/react.git", - "license": "MIT" + "main": "index.js", + "repository": { + "type": "git", + "url": "https://github.com/facebook/react.git", + "directory": "packages/react-jsx-babel-plugin" + }, + "license": "MIT", + "dependencies": { + "esutils": "^2.0.0" + }, + "files": [ + "README.md", + "index.js", + "build-info.json", + "cjs/", + "umd/" + ] } diff --git a/packages/react-jsx-babel-plugin/transform-jsx-to-react-jsx.js b/packages/react-jsx-babel-plugin/src/TransformJSXToReactBabelPlugin.js similarity index 95% rename from packages/react-jsx-babel-plugin/transform-jsx-to-react-jsx.js rename to packages/react-jsx-babel-plugin/src/TransformJSXToReactBabelPlugin.js index dfc043cced4ea..0b39356ca8ed6 100644 --- a/packages/react-jsx-babel-plugin/transform-jsx-to-react-jsx.js +++ b/packages/react-jsx-babel-plugin/src/TransformJSXToReactBabelPlugin.js @@ -1,9 +1,10 @@ 'use strict'; -const t = require('@babel/types'); const esutils = require('esutils'); -function helper(opts) { +function helper(babel, opts) { + const {types: t} = babel; + const visitor = {}; visitor.JSXNamespacedName = function(path, state) { @@ -235,7 +236,13 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, if (opts.post) { opts.post(state, file); } - return state.call || t.callExpression(state.callee, args); + 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 @@ -364,7 +371,13 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, opts.post(state, file); } - return state.call || t.callExpression(state.callee, args); + return ( + state.call || + t.callExpression( + path.node.children.length === 1 ? state.staticCallee : state.callee, + args, + ) + ); } // Builds JSX into: @@ -511,6 +524,8 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, } module.exports = function(babel) { + const {types: t} = babel; + const createIdentifierParser = id => () => { return id .split('.') @@ -518,7 +533,7 @@ module.exports = function(babel) { .reduce((object, property) => t.memberExpression(object, property)); }; - const visitor = helper({ + const visitor = helper(babel, { pre(state) { const tagName = state.tagName; const args = state.args; @@ -531,6 +546,7 @@ module.exports = function(babel) { post(state, pass) { state.callee = pass.get('jsxIdentifier')(); + state.staticCallee = pass.get('jsxStaticIdentifier')(); state.oldCallee = pass.get('oldJSXIdentifier')(); }, }); @@ -547,6 +563,12 @@ module.exports = function(babel) { state.opts.development ? 'React.jsxDEV' : 'React.jsx', ), ); + state.set( + 'jsxStaticIdentifier', + createIdentifierParser( + state.opts.development ? 'React.jsxDEV' : 'React.jsxs', + ), + ); state.set('jsxFragIdentifier', createIdentifierParser('React.Fragment')); }, }; diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js index fb36d65dde452..9da0f484c2dfe 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -455,6 +455,14 @@ const bundles = [ global: 'ESLintPluginReactHooks', externals: [], }, + /******** JSX to React.JSX Babel Plugin ********/ + { + bundleTypes: [NODE_DEV, NODE_PROD], + moduleType: ISOMORPHIC, + entry: 'react-jsx-babel-plugin', + global: 'TransformJSXToReactBabelPlugin', + externals: [], + }, /******* React Fresh *******/ { From 87e2dc5b60bf1c6b8bc16c3e071a4d5ece9e0e8d Mon Sep 17 00:00:00 2001 From: Luna Ruan Date: Tue, 20 Aug 2019 14:06:20 -0700 Subject: [PATCH 15/20] fixed isStaticChildren --- .../transform-jsx-to-react-jsx-test.js.snap | 90 +++++++++---------- .../src/TransformJSXToReactBabelPlugin.js | 8 +- packages/react/src/ReactElementValidator.js | 8 +- .../ReactElementJSX-test.internal.js | 12 +++ 4 files changed, 68 insertions(+), 50 deletions(-) diff --git a/packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-jsx-to-react-jsx-test.js.snap b/packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-jsx-to-react-jsx-test.js.snap index 510de731536f4..8f48cd25af71c 100644 --- a/packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-jsx-to-react-jsx-test.js.snap +++ b/packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-jsx-to-react-jsx-test.js.snap @@ -2,7 +2,7 @@ exports[`transform react to jsx React.fragment to set keys and source 1`] = ` "var _jsxFileName = \\"\\"; -var x = React.jsxDEV(React.Fragment, {}, \\"foo\\", true, { +var x = React.jsxDEV(React.Fragment, {}, \\"foo\\", false, { fileName: _jsxFileName, lineNumber: 1 }, this);" @@ -39,8 +39,8 @@ exports[`transform react to jsx assignment 1`] = ` `; exports[`transform react to jsx concatenates adjacent string literals 1`] = ` -"var x = React.jsx(\\"div\\", { - children: [\\"foo\\", \\"bar\\", \\"baz\\", React.jsxs(\\"div\\", { +"var x = React.jsxs(\\"div\\", { + children: [\\"foo\\", \\"bar\\", \\"baz\\", React.jsx(\\"div\\", { children: \\"buz bang\\" }), \\"qux\\", null, \\"quack\\"] });" @@ -49,7 +49,7 @@ exports[`transform react to jsx concatenates adjacent string literals 1`] = ` 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.jsxs(React.Fragment, { +"var x = React.jsx(React.Fragment, { children: React.jsx(\\"div\\", {}) });" `; @@ -57,7 +57,7 @@ exports[`transform react to jsx fragments 1`] = ` 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, true, { + children: React.jsxDEV(\\"div\\", {}, undefined, false, { fileName: _jsxFileName, lineNumber: 1 }, this) @@ -69,10 +69,10 @@ exports[`transform react to jsx fragments to set keys 1`] = `"var x = React.jsx( exports[`transform react to jsx nonStatic children 1`] = ` "var _jsxFileName = \\"\\"; var x = React.jsxDEV(\\"div\\", { - children: [React.jsxDEV(\\"span\\", {}, '0', true, { + children: [React.jsxDEV(\\"span\\", {}, '0', false, { fileName: _jsxFileName, lineNumber: 3 - }, this), React.jsxDEV(\\"span\\", {}, '1', true, { + }, this), React.jsxDEV(\\"span\\", {}, '1', false, { fileName: _jsxFileName, lineNumber: 3 }, this)] @@ -83,7 +83,7 @@ var x = React.jsxDEV(\\"div\\", { `; exports[`transform react to jsx properly handles keys 1`] = ` -"var x = React.jsx(\\"div\\", { +"var x = React.jsxs(\\"div\\", { children: [React.jsx(\\"div\\", {}, \\"1\\"), React.jsx(\\"div\\", { meow: \\"wolf\\" }, \\"2\\"), React.jsx(\\"div\\", {}, \\"3\\")] @@ -92,7 +92,7 @@ exports[`transform react to jsx properly handles keys 1`] = ` exports[`transform react to jsx properly passes in source and self 1`] = ` "var _jsxFileName = \\"\\"; -var x = React.jsxDEV(\\"div\\", {}, undefined, true, { +var x = React.jsxDEV(\\"div\\", {}, undefined, false, { fileName: _jsxFileName, lineNumber: 1 }, this);" @@ -115,18 +115,18 @@ exports[`transform react to jsx should allow elements as attributes 1`] = ` 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.jsxs(\\"div\\", { - children: React.jsx(React.Fragment, { - children: [React.jsx(React.Fragment, { - children: [React.jsxs(\\"span\\", { +"React.jsx(\\"div\\", { + children: React.jsxs(React.Fragment, { + children: [React.jsxs(React.Fragment, { + children: [React.jsx(\\"span\\", { children: \\"Hello\\" - }), React.jsxs(\\"span\\", { + }), React.jsx(\\"span\\", { children: \\"world\\" })] - }), React.jsx(React.Fragment, { - children: [React.jsxs(\\"span\\", { + }), React.jsxs(React.Fragment, { + children: [React.jsx(\\"span\\", { children: \\"Goodbye\\" - }), React.jsxs(\\"span\\", { + }), React.jsx(\\"span\\", { children: \\"world\\" })] })] @@ -135,16 +135,16 @@ exports[`transform react to jsx should allow nested fragments 1`] = ` `; exports[`transform react to jsx should avoid wrapping in extra parens if not needed 1`] = ` -"var x = React.jsxs(\\"div\\", { +"var x = React.jsx(\\"div\\", { children: React.jsx(Component, {}) }); -var x = React.jsxs(\\"div\\", { +var x = React.jsx(\\"div\\", { children: props.children }); -var x = React.jsxs(Composite, { +var x = React.jsx(Composite, { children: props.children }); -var x = React.jsxs(Composite, { +var x = React.jsx(Composite, { children: React.jsx(Composite2, {}) });" `; @@ -152,7 +152,7 @@ var x = React.jsxs(Composite, { 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.jsxs(\\"div\\", { +"var x = React.jsx(\\"div\\", { children: \\"text\\" });" `; @@ -170,31 +170,31 @@ React.jsx(\\"div\\", { `; exports[`transform react to jsx should escape xhtml jsxtext 1`] = ` -"React.jsxs(\\"div\\", { +"React.jsx(\\"div\\", { children: \\"wow\\" }); -React.jsxs(\\"div\\", { +React.jsx(\\"div\\", { children: \\"w\\\\xF4w\\" }); -React.jsxs(\\"div\\", { +React.jsx(\\"div\\", { children: \\"w & w\\" }); -React.jsxs(\\"div\\", { +React.jsx(\\"div\\", { children: \\"w & w\\" }); -React.jsxs(\\"div\\", { +React.jsx(\\"div\\", { children: \\"w \\\\xA0 w\\" }); -React.jsxs(\\"div\\", { +React.jsx(\\"div\\", { children: \\"this should not parse as unicode: \\\\xA0\\" }); -React.jsxs(\\"div\\", { +React.jsx(\\"div\\", { children: \\"this should parse as nbsp: \\\\xA0 \\" }); -React.jsx(\\"div\\", { +React.jsxs(\\"div\\", { children: [\\"this should parse as unicode: \\", '  '] }); -React.jsxs(\\"div\\", { +React.jsx(\\"div\\", { children: \\"w < w\\" });" `; @@ -202,29 +202,29 @@ React.jsxs(\\"div\\", { exports[`transform react to jsx should handle attributed elements 1`] = ` "var HelloMessage = React.createClass({ render: function () { - return React.jsx(\\"div\\", { + return React.jsxs(\\"div\\", { children: [\\"Hello \\", this.props.name] }); } }); React.render(React.jsx(HelloMessage, { - name: React.jsxs(\\"span\\", { + name: React.jsx(\\"span\\", { children: \\"Sebastian\\" }) }), mountNode);" `; exports[`transform react to jsx should handle has own property correctly 1`] = ` -"React.jsxs(\\"hasOwnProperty\\", { +"React.jsx(\\"hasOwnProperty\\", { children: \\"testing\\" });" `; exports[`transform react to jsx should have correct comma in nested children 1`] = ` -"var x = React.jsx(\\"div\\", { - children: [React.jsxs(\\"div\\", { +"var x = React.jsxs(\\"div\\", { + children: [React.jsx(\\"div\\", { children: React.jsx(\\"br\\", {}) - }), React.jsx(Component, { + }), React.jsxs(Component, { children: [foo, React.jsx(\\"br\\", {}), bar] }), React.jsx(\\"br\\", {})] });" @@ -251,25 +251,25 @@ exports[`transform react to jsx should not add quotes to identifier names 1`] = `; exports[`transform react to jsx should not strip nbsp even couple with other whitespace 1`] = ` -"React.jsxs(\\"div\\", { +"React.jsx(\\"div\\", { children: \\"\\\\xA0 \\" });" `; exports[`transform react to jsx should not strip tags with a single child of nbsp 1`] = ` -"React.jsxs(\\"div\\", { +"React.jsx(\\"div\\", { children: \\"\\\\xA0\\" });" `; exports[`transform react to jsx should properly handle comments adjacent to children 1`] = ` -"var x = React.jsx(\\"div\\", { +"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.jsxs(\\"div\\", { +"var x = React.jsx(\\"div\\", { /* a multi-line comment */ attr1: \\"foo\\", @@ -286,7 +286,7 @@ var x = React.jsx(\\"div\\", Object.assign({}, foo));" `; exports[`transform react to jsx should quote jsx attributes 1`] = ` -"React.jsxs(\\"button\\", { +"React.jsx(\\"button\\", { \\"data-value\\": \\"a value\\", children: \\"Button\\" });" @@ -303,13 +303,13 @@ exports[`transform react to jsx should transform known hyphenated tags 1`] = `"R exports[`transform react to jsx static children 1`] = ` "var _jsxFileName = \\"\\"; var x = React.jsxDEV(\\"div\\", { - children: [React.jsxDEV(\\"span\\", {}, undefined, true, { + children: [React.jsxDEV(\\"span\\", {}, undefined, false, { fileName: _jsxFileName, lineNumber: 3 - }, this), [React.jsxDEV(\\"span\\", {}, '0', true, { + }, this), [React.jsxDEV(\\"span\\", {}, '0', false, { fileName: _jsxFileName, lineNumber: 4 - }, this), React.jsxDEV(\\"span\\", {}, '1', true, { + }, this), React.jsxDEV(\\"span\\", {}, '1', false, { fileName: _jsxFileName, lineNumber: 4 }, this)]] diff --git a/packages/react-jsx-babel-plugin/src/TransformJSXToReactBabelPlugin.js b/packages/react-jsx-babel-plugin/src/TransformJSXToReactBabelPlugin.js index 0b39356ca8ed6..186a8288cf428 100644 --- a/packages/react-jsx-babel-plugin/src/TransformJSXToReactBabelPlugin.js +++ b/packages/react-jsx-babel-plugin/src/TransformJSXToReactBabelPlugin.js @@ -227,7 +227,7 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, // isStaticChildren, __source, and __self are only used in development args.push( key === undefined ? t.identifier('undefined') : key, - t.booleanLiteral(path.node.children.length !== 1), + t.booleanLiteral(path.node.children.length > 1), source === undefined ? t.identifier('undefined') : source, self === undefined ? t.identifier('undefined') : self, ); @@ -239,7 +239,7 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, return ( state.call || t.callExpression( - path.node.children.length === 1 ? state.staticCallee : state.callee, + path.node.children.length > 1 ? state.staticCallee : state.callee, args, ) ); @@ -363,7 +363,7 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, if (file.opts.development) { args.push( t.identifier('undefined'), - t.booleanLiteral(path.node.children.length !== 1), + t.booleanLiteral(path.node.children.length > 1), ); } @@ -374,7 +374,7 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, return ( state.call || t.callExpression( - path.node.children.length === 1 ? state.staticCallee : state.callee, + path.node.children.length > 1 ? state.staticCallee : state.callee, args, ) ); diff --git a/packages/react/src/ReactElementValidator.js b/packages/react/src/ReactElementValidator.js index 324c2449bddd0..44ac18239d408 100644 --- a/packages/react/src/ReactElementValidator.js +++ b/packages/react/src/ReactElementValidator.js @@ -336,6 +336,7 @@ 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) { @@ -345,7 +346,12 @@ export function jsxWithValidation( validateChildKeys(children[i], type); } } else { - validateChildKeys(children, type); + 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); diff --git a/packages/react/src/__tests__/ReactElementJSX-test.internal.js b/packages/react/src/__tests__/ReactElementJSX-test.internal.js index db62a2c51ec15..602cc3024a3f6 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( From 737ebb547a86bce4e621c2e2b44a1541aa0a7d2d Mon Sep 17 00:00:00 2001 From: Luna Ruan Date: Tue, 20 Aug 2019 14:16:43 -0700 Subject: [PATCH 16/20] modified tests to not import source and removed bundle code for separate PR --- .../transform-jsx-to-react-createElement-test.js | 2 +- .../__tests__/transform-jsx-to-react-jsx-test.js | 2 +- scripts/rollup/bundles.js | 8 -------- 3 files changed, 2 insertions(+), 10 deletions(-) diff --git a/packages/react-jsx-babel-plugin/__tests__/transform-jsx-to-react-createElement-test.js b/packages/react-jsx-babel-plugin/__tests__/transform-jsx-to-react-createElement-test.js index 1c77f630d0af2..46fcbbe307e85 100644 --- a/packages/react-jsx-babel-plugin/__tests__/transform-jsx-to-react-createElement-test.js +++ b/packages/react-jsx-babel-plugin/__tests__/transform-jsx-to-react-createElement-test.js @@ -23,7 +23,7 @@ function transform(input, options) { ] : []), [ - './packages/react-jsx-babel-plugin/src/TransformJSXToReactBabelPlugin', + './packages/react-jsx-babel-plugin', { development: __DEV__, useBuiltIns: true, diff --git a/packages/react-jsx-babel-plugin/__tests__/transform-jsx-to-react-jsx-test.js b/packages/react-jsx-babel-plugin/__tests__/transform-jsx-to-react-jsx-test.js index fc92beabc4cd2..409f8b59294ad 100644 --- a/packages/react-jsx-babel-plugin/__tests__/transform-jsx-to-react-jsx-test.js +++ b/packages/react-jsx-babel-plugin/__tests__/transform-jsx-to-react-jsx-test.js @@ -23,7 +23,7 @@ function transform(input, options) { ] : []), [ - './packages/react-jsx-babel-plugin/src/TransformJSXToReactBabelPlugin', + './packages/react-jsx-babel-plugin', { useBuiltIns: true, useCreateElement: false, diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js index 9da0f484c2dfe..fb36d65dde452 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -455,14 +455,6 @@ const bundles = [ global: 'ESLintPluginReactHooks', externals: [], }, - /******** JSX to React.JSX Babel Plugin ********/ - { - bundleTypes: [NODE_DEV, NODE_PROD], - moduleType: ISOMORPHIC, - entry: 'react-jsx-babel-plugin', - global: 'TransformJSXToReactBabelPlugin', - externals: [], - }, /******* React Fresh *******/ { From aa83e963adb1b2ce662ebdc8cbc29a6585632112 Mon Sep 17 00:00:00 2001 From: Luna Ruan Date: Thu, 22 Aug 2019 16:34:12 -0700 Subject: [PATCH 17/20] edits from comments --- .../README.md | 0 .../TransformJSXToReactCreateElement-test.js} | 79 +++-- .../__tests__/TransformJSXToReactJSX-test.js} | 87 +++-- ...nsformJSXToReactCreateElement-test.js.snap | 213 ++++++++++++ .../TransformJSXToReactJSX-test.js.snap} | 324 +++++++++--------- .../index.js | 0 .../npm/index.js | 0 .../package.json | 12 +- .../src/TransformJSXToReactBabelPlugin.js | 38 +- ...rm-jsx-to-react-createElement-test.js.snap | 213 ------------ packages/react/src/ReactElementValidator.js | 2 +- .../ReactElementJSX-test.internal.js | 2 +- 12 files changed, 540 insertions(+), 430 deletions(-) rename packages/{react-jsx-babel-plugin => babel-plugin-react-jsx}/README.md (100%) rename packages/{react-jsx-babel-plugin/__tests__/transform-jsx-to-react-createElement-test.js => babel-plugin-react-jsx/__tests__/TransformJSXToReactCreateElement-test.js} (92%) rename packages/{react-jsx-babel-plugin/__tests__/transform-jsx-to-react-jsx-test.js => babel-plugin-react-jsx/__tests__/TransformJSXToReactJSX-test.js} (94%) create mode 100644 packages/babel-plugin-react-jsx/__tests__/__snapshots__/TransformJSXToReactCreateElement-test.js.snap rename packages/{react-jsx-babel-plugin/__tests__/__snapshots__/transform-jsx-to-react-jsx-test.js.snap => babel-plugin-react-jsx/__tests__/__snapshots__/TransformJSXToReactJSX-test.js.snap} (50%) rename packages/{react-jsx-babel-plugin => babel-plugin-react-jsx}/index.js (100%) rename packages/{react-jsx-babel-plugin => babel-plugin-react-jsx}/npm/index.js (100%) rename packages/{react-jsx-babel-plugin => babel-plugin-react-jsx}/package.json (50%) rename packages/{react-jsx-babel-plugin => babel-plugin-react-jsx}/src/TransformJSXToReactBabelPlugin.js (90%) delete mode 100644 packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-jsx-to-react-createElement-test.js.snap diff --git a/packages/react-jsx-babel-plugin/README.md b/packages/babel-plugin-react-jsx/README.md similarity index 100% rename from packages/react-jsx-babel-plugin/README.md rename to packages/babel-plugin-react-jsx/README.md diff --git a/packages/react-jsx-babel-plugin/__tests__/transform-jsx-to-react-createElement-test.js b/packages/babel-plugin-react-jsx/__tests__/TransformJSXToReactCreateElement-test.js similarity index 92% rename from packages/react-jsx-babel-plugin/__tests__/transform-jsx-to-react-createElement-test.js rename to packages/babel-plugin-react-jsx/__tests__/TransformJSXToReactCreateElement-test.js index 46fcbbe307e85..6e44eaeaec95b 100644 --- a/packages/react-jsx-babel-plugin/__tests__/transform-jsx-to-react-createElement-test.js +++ b/packages/babel-plugin-react-jsx/__tests__/TransformJSXToReactCreateElement-test.js @@ -9,36 +9,40 @@ const babel = require('@babel/core'); const codeFrame = require('@babel/code-frame'); +const {wrap} = require('jest-snapshot-serializer-raw'); function transform(input, options) { - return 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/react-jsx-babel-plugin', - { - development: __DEV__, - useBuiltIns: true, - useCreateElement: true, - ...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; + }).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 =
`, { @@ -46,6 +50,7 @@ describe('transform react to jsx', () => { }) ).toMatchSnapshot(); }); + it('normal fragments not to set key and source', () => { expect( transform(`var x = <>
`, { @@ -53,6 +58,7 @@ describe('transform react to jsx', () => { }) ).toMatchSnapshot(); }); + it('should properly handle comments adjacent to children', () => { expect( transform(` @@ -72,9 +78,11 @@ describe('transform react to jsx', () => { `) ).toMatchSnapshot(); }); + it('adds appropriate new lines when using spread attribute', () => { expect(transform(``)).toMatchSnapshot(); }); + it('arrow functions', () => { expect( transform(` @@ -89,11 +97,13 @@ describe('transform react to jsx', () => { `) ).toMatchSnapshot(); }); + it('assignment', () => { expect( transform(`var div = `) ).toMatchSnapshot(); }); + it('concatenates adjacent string literals', () => { expect( transform(` @@ -113,20 +123,25 @@ describe('transform react to jsx', () => { `) ).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(` @@ -145,6 +160,7 @@ describe('transform react to jsx', () => { `) ).toMatchSnapshot(); }); + it('should avoid wrapping in extra parens if not needed', () => { expect( transform(` @@ -166,12 +182,15 @@ describe('transform react to jsx', () => { `) ).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}
;`; @@ -192,6 +211,7 @@ describe('transform react to jsx', () => { ) ); }); + it('should escape xhtml jsxattribute', () => { expect( transform(` @@ -201,6 +221,7 @@ describe('transform react to jsx', () => { `) ).toMatchSnapshot(); }); + it('should escape xhtml jsxtext', () => { /* eslint-disable no-irregular-whitespace */ expect( @@ -221,6 +242,7 @@ describe('transform react to jsx', () => { ).toMatchSnapshot(); /*eslint-enable */ }); + it('should handle attributed elements', () => { expect( transform(` @@ -238,11 +260,13 @@ describe('transform react to jsx', () => { `) ).toMatchSnapshot(); }); + it('should handle has own property correctly', () => { expect( transform(`testing;`) ).toMatchSnapshot(); }); + it('should have correct comma in nested children', () => { expect( transform(` @@ -254,6 +278,7 @@ describe('transform react to jsx', () => { `) ).toMatchSnapshot(); }); + it('should insert commas after expressions before whitespace', () => { expect( transform(` @@ -276,17 +301,21 @@ describe('transform react to jsx', () => { `) ).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(` @@ -303,16 +332,19 @@ describe('transform react to jsx', () => { `) ).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 = ``; @@ -335,18 +367,23 @@ describe('transform react to jsx', () => { ) ); }); + 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}) diff --git a/packages/react-jsx-babel-plugin/__tests__/transform-jsx-to-react-jsx-test.js b/packages/babel-plugin-react-jsx/__tests__/TransformJSXToReactJSX-test.js similarity index 94% rename from packages/react-jsx-babel-plugin/__tests__/transform-jsx-to-react-jsx-test.js rename to packages/babel-plugin-react-jsx/__tests__/TransformJSXToReactJSX-test.js index 409f8b59294ad..86ee84f1fa260 100644 --- a/packages/react-jsx-babel-plugin/__tests__/transform-jsx-to-react-jsx-test.js +++ b/packages/babel-plugin-react-jsx/__tests__/TransformJSXToReactJSX-test.js @@ -9,43 +9,49 @@ const babel = require('@babel/core'); const codeFrame = require('@babel/code-frame'); +const {wrap} = require('jest-snapshot-serializer-raw'); function transform(input, options) { - return 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/react-jsx-babel-plugin', - { - useBuiltIns: true, - useCreateElement: false, - ...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; + }).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 = `, { @@ -53,6 +59,7 @@ describe('transform react to jsx', () => { }) ).toMatchSnapshot(); }); + it('fragments in dev mode (no key and source)', () => { expect( transform(`var x = <>
`, { @@ -60,6 +67,7 @@ describe('transform react to jsx', () => { }) ).toMatchSnapshot(); }); + it('nonStatic children', () => { expect( transform( @@ -75,6 +83,7 @@ describe('transform react to jsx', () => { ) ).toMatchSnapshot(); }); + it('static children', () => { expect( transform( @@ -91,16 +100,19 @@ describe('transform react to jsx', () => { ) ).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(` @@ -109,6 +121,7 @@ describe('transform react to jsx', () => { `) ).toMatchSnapshot(); }); + it('properly handles keys', () => { expect( transform(`var x = ( @@ -120,6 +133,7 @@ describe('transform react to jsx', () => { );`) ).toMatchSnapshot(); }); + it('uses createElement when the key comes after a spread', () => { expect( transform(`var x = ( @@ -127,6 +141,7 @@ describe('transform react to jsx', () => { );`) ).toMatchSnapshot(); }); + it('uses jsx when the key comes before a spread', () => { expect( transform(`var x = ( @@ -134,6 +149,7 @@ describe('transform react to jsx', () => { );`) ).toMatchSnapshot(); }); + it('should properly handle comments adjacent to children', () => { expect( transform(` @@ -153,9 +169,11 @@ describe('transform react to jsx', () => { `) ).toMatchSnapshot(); }); + it('adds appropriate new lines when using spread attribute', () => { expect(transform(``)).toMatchSnapshot(); }); + it('arrow functions', () => { expect( transform(` @@ -170,11 +188,13 @@ describe('transform react to jsx', () => { `) ).toMatchSnapshot(); }); + it('assignment', () => { expect( transform(`var div = `) ).toMatchSnapshot(); }); + it('concatenates adjacent string literals', () => { expect( transform(` @@ -194,20 +214,25 @@ describe('transform react to jsx', () => { `) ).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(` @@ -226,6 +251,7 @@ describe('transform react to jsx', () => { `) ).toMatchSnapshot(); }); + it('should avoid wrapping in extra parens if not needed', () => { expect( transform(` @@ -247,12 +273,15 @@ describe('transform react to jsx', () => { `) ).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}
;`; @@ -273,6 +302,7 @@ describe('transform react to jsx', () => { ) ); }); + it('should escape xhtml jsxattribute', () => { expect( transform(` @@ -282,6 +312,7 @@ describe('transform react to jsx', () => { `) ).toMatchSnapshot(); }); + it('should escape xhtml jsxtext', () => { /* eslint-disable no-irregular-whitespace */ expect( @@ -302,6 +333,7 @@ describe('transform react to jsx', () => { ).toMatchSnapshot(); /*eslint-enable */ }); + it('should handle attributed elements', () => { expect( transform(` @@ -319,11 +351,13 @@ describe('transform react to jsx', () => { `) ).toMatchSnapshot(); }); + it('should handle has own property correctly', () => { expect( transform(`testing;`) ).toMatchSnapshot(); }); + it('should have correct comma in nested children', () => { expect( transform(` @@ -335,6 +369,7 @@ describe('transform react to jsx', () => { `) ).toMatchSnapshot(); }); + it('should insert commas after expressions before whitespace', () => { expect( transform(` @@ -357,17 +392,21 @@ describe('transform react to jsx', () => { `) ).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(` @@ -384,16 +423,19 @@ describe('transform react to jsx', () => { `) ).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 = ``; @@ -416,18 +458,23 @@ describe('transform react to jsx', () => { ) ); }); + 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}) 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/react-jsx-babel-plugin/__tests__/__snapshots__/transform-jsx-to-react-jsx-test.js.snap b/packages/babel-plugin-react-jsx/__tests__/__snapshots__/TransformJSXToReactJSX-test.js.snap similarity index 50% rename from packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-jsx-to-react-jsx-test.js.snap rename to packages/babel-plugin-react-jsx/__tests__/__snapshots__/TransformJSXToReactJSX-test.js.snap index 8f48cd25af71c..e5b844b12fc0d 100644 --- a/packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-jsx-to-react-jsx-test.js.snap +++ b/packages/babel-plugin-react-jsx/__tests__/__snapshots__/TransformJSXToReactJSX-test.js.snap @@ -1,21 +1,21 @@ // 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, { +var _jsxFileName = ""; +var x = React.jsxDEV(React.Fragment, {}, "foo", false, { fileName: _jsxFileName, lineNumber: 1 -}, this);" +}, this); `; exports[`transform react to jsx adds appropriate new lines when using spread attribute 1`] = ` -"React.jsx(Component, Object.assign({}, props, { - sound: \\"moo\\" -}));" +React.jsx(Component, Object.assign({}, props, { + sound: "moo" +})); `; exports[`transform react to jsx arrow functions 1`] = ` -"var foo = function () { +var foo = function () { var _this = this; return function () { @@ -29,116 +29,116 @@ var bar = function () { return function () { return React.jsx(_this2.foo, {}); }; -};" +}; `; exports[`transform react to jsx assignment 1`] = ` -"var div = React.jsx(Component, Object.assign({}, props, { - foo: \\"bar\\" -}));" +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\\"] -});" +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 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\\", {}) -});" +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 _jsxFileName = ""; var x = React.jsxDEV(React.Fragment, { - children: React.jsxDEV(\\"div\\", {}, undefined, false, { + children: React.jsxDEV("div", {}, undefined, false, { fileName: _jsxFileName, lineNumber: 1 }, this) -}, undefined, false);" +}, undefined, false); `; -exports[`transform react to jsx fragments to set keys 1`] = `"var x = React.jsx(React.Fragment, {}, \\"foo\\");"`; +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, { +var _jsxFileName = ""; +var x = React.jsxDEV("div", { + children: [React.jsxDEV("span", {}, '0', false, { fileName: _jsxFileName, lineNumber: 3 - }, this), React.jsxDEV(\\"span\\", {}, '1', false, { + }, this), React.jsxDEV("span", {}, '1', false, { fileName: _jsxFileName, lineNumber: 3 }, this)] }, undefined, false, { fileName: _jsxFileName, lineNumber: 2 -}, this);" +}, 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\\")] -});" +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, { +var _jsxFileName = ""; +var x = React.jsxDEV("div", {}, undefined, false, { fileName: _jsxFileName, lineNumber: 1 -}, this);" +}, this); `; exports[`transform react to jsx should allow constructor as prop 1`] = ` -"React.jsx(Component, { - constructor: \\"foo\\" -});" +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 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\\", {}) -});" +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 js namespacing 1`] = `React.jsx(Namespace.Component, {});`; exports[`transform react to jsx should allow nested fragments 1`] = ` -"React.jsx(\\"div\\", { +React.jsx("div", { children: React.jsxs(React.Fragment, { children: [React.jsxs(React.Fragment, { - children: [React.jsx(\\"span\\", { - children: \\"Hello\\" - }), React.jsx(\\"span\\", { - children: \\"world\\" + 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\\" + 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\\", { +var x = React.jsx("div", { children: React.jsx(Component, {}) }); -var x = React.jsx(\\"div\\", { +var x = React.jsx("div", { children: props.children }); var x = React.jsx(Composite, { @@ -146,229 +146,229 @@ var x = React.jsx(Composite, { }); 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 tags 1`] = `var x = React.jsx("div", {});`; exports[`transform react to jsx should convert simple text 1`] = ` -"var x = React.jsx(\\"div\\", { - children: \\"text\\" -});" +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\\xF4w" }); -React.jsx(\\"div\\", { - id: \\"w\\" +React.jsx("div", { + id: "w" +}); +React.jsx("div", { + id: "w < 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: "wow" +}); +React.jsx("div", { + children: "w\\xF4w" }); -React.jsx(\\"div\\", { - children: \\"w\\\\xF4w\\" +React.jsx("div", { + children: "w & w" }); -React.jsx(\\"div\\", { - children: \\"w & w\\" +React.jsx("div", { + children: "w & w" }); -React.jsx(\\"div\\", { - children: \\"w & w\\" +React.jsx("div", { + children: "w \\xA0 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 not parse as unicode: \\\\xA0\\" +React.jsx("div", { + children: "this should parse as nbsp: \\xA0 " }); -React.jsx(\\"div\\", { - children: \\"this should parse as nbsp: \\\\xA0 \\" +React.jsxs("div", { + children: ["this should parse as unicode: ", '  '] }); -React.jsxs(\\"div\\", { - children: [\\"this should parse as unicode: \\", '  '] +React.jsx("div", { + children: "w < w" }); -React.jsx(\\"div\\", { - children: \\"w < w\\" -});" `; exports[`transform react to jsx should handle attributed elements 1`] = ` -"var HelloMessage = React.createClass({ +var HelloMessage = React.createClass({ render: function () { - return React.jsxs(\\"div\\", { - children: [\\"Hello \\", this.props.name] + return React.jsxs("div", { + children: ["Hello ", this.props.name] }); } }); React.render(React.jsx(HelloMessage, { - name: React.jsx(\\"span\\", { - children: \\"Sebastian\\" + name: React.jsx("span", { + children: "Sebastian" }) -}), mountNode);" +}), mountNode); `; exports[`transform react to jsx should handle has own property correctly 1`] = ` -"React.jsx(\\"hasOwnProperty\\", { - children: \\"testing\\" -});" +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\\", {}) +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\\", {})] -});" + 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\\" -});" +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, { +var e = React.jsx(F, { aaa: true, new: true, const: true, var: true, default: true, - \\"foo-bar\\": true -});" + "foo-bar": true +}); `; exports[`transform react to jsx should not strip nbsp even couple with other whitespace 1`] = ` -"React.jsx(\\"div\\", { - children: \\"\\\\xA0 \\" -});" +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\\" -});" +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\\", {})] -});" +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\\", { +var x = React.jsx("div", { /* a multi-line comment */ - attr1: \\"foo\\", - children: React.jsx(\\"span\\", { + attr1: "foo", + children: React.jsx("span", { // a double-slash comment - attr2: \\"bar\\" + 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));" +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\\" -});" +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 -});" +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 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, { +var _jsxFileName = ""; +var x = React.jsxDEV("div", { + children: [React.jsxDEV("span", {}, undefined, false, { fileName: _jsxFileName, lineNumber: 3 - }, this), [React.jsxDEV(\\"span\\", {}, '0', false, { + }, this), [React.jsxDEV("span", {}, '0', false, { fileName: _jsxFileName, lineNumber: 4 - }, this), React.jsxDEV(\\"span\\", {}, '1', false, { + }, this), React.jsxDEV("span", {}, '1', false, { fileName: _jsxFileName, lineNumber: 4 }, this)]] }, undefined, true, { fileName: _jsxFileName, lineNumber: 2 -}, this);" +}, 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); } +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));" +}, 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\\" -}));" +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\\");" +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\\" +var _jsxFileName = ""; +var x = React.jsxDEV("span", { + propOne: "one", + children: "Hi" }, undefined, false, { fileName: _jsxFileName, lineNumber: 1 -}, this);" +}, this); `; exports[`transform react to jsx wraps props in react spread for first spread attributes 1`] = ` -"React.jsx(Component, Object.assign({}, x, { +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({ +React.jsx(Component, Object.assign({ y: 2, z: true -}, x));" +}, x)); `; exports[`transform react to jsx wraps props in react spread for middle spread attributes 1`] = ` -"React.jsx(Component, Object.assign({ +React.jsx(Component, Object.assign({ y: 2 }, x, { z: true -}));" +})); `; diff --git a/packages/react-jsx-babel-plugin/index.js b/packages/babel-plugin-react-jsx/index.js similarity index 100% rename from packages/react-jsx-babel-plugin/index.js rename to packages/babel-plugin-react-jsx/index.js diff --git a/packages/react-jsx-babel-plugin/npm/index.js b/packages/babel-plugin-react-jsx/npm/index.js similarity index 100% rename from packages/react-jsx-babel-plugin/npm/index.js rename to packages/babel-plugin-react-jsx/npm/index.js diff --git a/packages/react-jsx-babel-plugin/package.json b/packages/babel-plugin-react-jsx/package.json similarity index 50% rename from packages/react-jsx-babel-plugin/package.json rename to packages/babel-plugin-react-jsx/package.json index 22b4780fd1f0f..41243452d4f17 100644 --- a/packages/react-jsx-babel-plugin/package.json +++ b/packages/babel-plugin-react-jsx/package.json @@ -1,16 +1,12 @@ { - "name": "react-jsx-babel-plugin", - "version": "1.0.0", + "name": "babel-plugin-react-jsx", + "version": "0.1.0", + "private": true, "description": "@babel/plugin-transform-react-jsx", "main": "index.js", - "repository": { - "type": "git", - "url": "https://github.com/facebook/react.git", - "directory": "packages/react-jsx-babel-plugin" - }, - "license": "MIT", "dependencies": { "esutils": "^2.0.0" + }, "files": [ "README.md", diff --git a/packages/react-jsx-babel-plugin/src/TransformJSXToReactBabelPlugin.js b/packages/babel-plugin-react-jsx/src/TransformJSXToReactBabelPlugin.js similarity index 90% rename from packages/react-jsx-babel-plugin/src/TransformJSXToReactBabelPlugin.js rename to packages/babel-plugin-react-jsx/src/TransformJSXToReactBabelPlugin.js index 186a8288cf428..669fbe8de3bc3 100644 --- a/packages/react-jsx-babel-plugin/src/TransformJSXToReactBabelPlugin.js +++ b/packages/babel-plugin-react-jsx/src/TransformJSXToReactBabelPlugin.js @@ -1,3 +1,33 @@ +// 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. + +/** + * 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. + */ 'use strict'; const esutils = require('esutils'); @@ -29,7 +59,7 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, visitor.JSXElement = { exit(path, file) { let callExpr; - if (file.opts.useCreateElement || useCreateElement(path)) { + if (file.opts.useCreateElement || shouldUseCreateElement(path)) { callExpr = buildCreateElementCall(path, file); } else { callExpr = buildJSXElementCall(path, file); @@ -126,12 +156,12 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, // from
. This is an intermediary // step while we deprecate key spread from props. Afterwards, // we will remove createElement entirely - function useCreateElement(path) { + function shouldUseCreateElement(path) { const openingPath = path.get('openingElement'); const attributes = openingPath.node.attributes; let seenPropsSpread = false; - for (let i = 0, length = attributes.length; i < length; i++) { + for (let i = 0; i < attributes.length; i++) { const attr = attributes[i]; if ( seenPropsSpread && @@ -189,7 +219,7 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, // 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, len = openingPath.node.attributes.length; i < len; i++) { + 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') { diff --git a/packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-jsx-to-react-createElement-test.js.snap b/packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-jsx-to-react-createElement-test.js.snap deleted file mode 100644 index ac3405810ff48..0000000000000 --- a/packages/react-jsx-babel-plugin/__tests__/__snapshots__/transform-jsx-to-react-createElement-test.js.snap +++ /dev/null @@ -1,213 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`transform react to jsx React.Fragment to set keys and source 1`] = ` -"var _jsxFileName = \\"\\"; -var x = React.createElement(React.Fragment, { - key: \\"foo\\", - __source: { - fileName: _jsxFileName, - lineNumber: 1 - }, - __self: this -}, React.createElement(\\"div\\", { - __source: { - fileName: _jsxFileName, - lineNumber: 1 - }, - __self: this -}));" -`; - -exports[`transform react to jsx adds appropriate new lines when using spread attribute 1`] = ` -"React.createElement(Component, Object.assign({}, props, { - sound: \\"moo\\" -}));" -`; - -exports[`transform react to jsx arrow functions 1`] = ` -"var foo = function () { - var _this = this; - - return function () { - return React.createElement(_this, null); - }; -}; - -var bar = function () { - var _this2 = this; - - return function () { - return React.createElement(_this2.foo, null); - }; -};" -`; - -exports[`transform react to jsx assignment 1`] = ` -"var div = React.createElement(Component, Object.assign({}, props, { - foo: \\"bar\\" -}));" -`; - -exports[`transform react to jsx concatenates adjacent string literals 1`] = `"var x = React.createElement(\\"div\\", null, \\"foo\\", \\"bar\\", \\"baz\\", React.createElement(\\"div\\", null, \\"buz bang\\"), \\"qux\\", null, \\"quack\\");"`; - -exports[`transform react to jsx fragment with no children 1`] = `"var x = React.createElement(React.Fragment, null);"`; - -exports[`transform react to jsx normal fragments not to set key and source 1`] = ` -"var _jsxFileName = \\"\\"; -var x = React.createElement(React.Fragment, null, React.createElement(\\"div\\", { - __source: { - fileName: _jsxFileName, - lineNumber: 1 - }, - __self: this -}));" -`; - -exports[`transform react to jsx should allow constructor as prop 1`] = ` -"React.createElement(Component, { - constructor: \\"foo\\" -});" -`; - -exports[`transform react to jsx should allow deeper js namespacing 1`] = `"React.createElement(Namespace.DeepNamespace.Component, null);"`; - -exports[`transform react to jsx should allow elements as attributes 1`] = ` -"React.createElement(\\"div\\", { - attr: React.createElement(\\"div\\", null) -});" -`; - -exports[`transform react to jsx should allow js namespacing 1`] = `"React.createElement(Namespace.Component, null);"`; - -exports[`transform react to jsx should allow nested fragments 1`] = `"React.createElement(\\"div\\", null, React.createElement(React.Fragment, null, React.createElement(React.Fragment, null, React.createElement(\\"span\\", null, \\"Hello\\"), React.createElement(\\"span\\", null, \\"world\\")), React.createElement(React.Fragment, null, React.createElement(\\"span\\", null, \\"Goodbye\\"), React.createElement(\\"span\\", null, \\"world\\"))));"`; - -exports[`transform react to jsx should avoid wrapping in extra parens if not needed 1`] = ` -"var x = React.createElement(\\"div\\", null, React.createElement(Component, null)); -var x = React.createElement(\\"div\\", null, props.children); -var x = React.createElement(Composite, null, props.children); -var x = React.createElement(Composite, null, React.createElement(Composite2, null));" -`; - -exports[`transform react to jsx should convert simple tags 1`] = `"var x = React.createElement(\\"div\\", null);"`; - -exports[`transform react to jsx should convert simple text 1`] = `"var x = React.createElement(\\"div\\", null, \\"text\\");"`; - -exports[`transform react to jsx should escape xhtml jsxattribute 1`] = ` -"React.createElement(\\"div\\", { - id: \\"w\\\\xF4w\\" -}); -React.createElement(\\"div\\", { - id: \\"w\\" -}); -React.createElement(\\"div\\", { - id: \\"w < w\\" -});" -`; - -exports[`transform react to jsx should escape xhtml jsxtext 1`] = ` -"React.createElement(\\"div\\", null, \\"wow\\"); -React.createElement(\\"div\\", null, \\"w\\\\xF4w\\"); -React.createElement(\\"div\\", null, \\"w & w\\"); -React.createElement(\\"div\\", null, \\"w & w\\"); -React.createElement(\\"div\\", null, \\"w \\\\xA0 w\\"); -React.createElement(\\"div\\", null, \\"this should not parse as unicode: \\\\xA0\\"); -React.createElement(\\"div\\", null, \\"this should parse as nbsp: \\\\xA0 \\"); -React.createElement(\\"div\\", null, \\"this should parse as unicode: \\", '  '); -React.createElement(\\"div\\", null, \\"w < w\\");" -`; - -exports[`transform react to jsx should handle attributed elements 1`] = ` -"var HelloMessage = React.createClass({ - render: function () { - return React.createElement(\\"div\\", null, \\"Hello \\", this.props.name); - } -}); -React.render(React.createElement(HelloMessage, { - name: React.createElement(\\"span\\", null, \\"Sebastian\\") -}), mountNode);" -`; - -exports[`transform react to jsx should handle has own property correctly 1`] = `"React.createElement(\\"hasOwnProperty\\", null, \\"testing\\");"`; - -exports[`transform react to jsx should have correct comma in nested children 1`] = `"var x = React.createElement(\\"div\\", null, React.createElement(\\"div\\", null, React.createElement(\\"br\\", null)), React.createElement(Component, null, foo, React.createElement(\\"br\\", null), bar), React.createElement(\\"br\\", null));"`; - -exports[`transform react to jsx should insert commas after expressions before whitespace 1`] = ` -"var x = React.createElement(\\"div\\", { - attr1: \\"foo\\" + \\"bar\\", - attr2: \\"foo\\" + \\"bar\\" + \\"baz\\" + \\"bug\\", - attr3: \\"foo\\" + \\"bar\\" + \\"baz\\" + \\"bug\\", - attr4: \\"baz\\" -});" -`; - -exports[`transform react to jsx should not add quotes to identifier names 1`] = ` -"var e = React.createElement(F, { - aaa: true, - new: true, - const: true, - var: true, - default: true, - \\"foo-bar\\": true -});" -`; - -exports[`transform react to jsx should not strip nbsp even couple with other whitespace 1`] = `"React.createElement(\\"div\\", null, \\"\\\\xA0 \\");"`; - -exports[`transform react to jsx should not strip tags with a single child of nbsp 1`] = `"React.createElement(\\"div\\", null, \\"\\\\xA0\\");"`; - -exports[`transform react to jsx should properly handle comments adjacent to children 1`] = `"var x = React.createElement(\\"div\\", null, React.createElement(\\"span\\", null), React.createElement(\\"br\\", null));"`; - -exports[`transform react to jsx should properly handle comments between props 1`] = ` -"var x = React.createElement(\\"div\\", { - /* a multi-line - comment */ - attr1: \\"foo\\" -}, React.createElement(\\"span\\", { - // a double-slash comment - attr2: \\"bar\\" -}));" -`; - -exports[`transform react to jsx should quote jsx attributes 1`] = ` -"React.createElement(\\"button\\", { - \\"data-value\\": \\"a value\\" -}, \\"Button\\");" -`; - -exports[`transform react to jsx should support xml namespaces if flag 1`] = ` -"React.createElement(\\"f:image\\", { - \\"n:attr\\": true -});" -`; - -exports[`transform react to jsx should transform known hyphenated tags 1`] = `"React.createElement(\\"font-face\\", null);"`; - -exports[`transform react to jsx useBuiltIns false uses extend instead of Object.assign 1`] = ` -"function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } - -React.createElement(Component, _extends({ - y: 2 -}, x));" -`; - -exports[`transform react to jsx wraps props in react spread for first spread attributes 1`] = ` -"React.createElement(Component, Object.assign({}, x, { - y: 2, - z: true -}));" -`; - -exports[`transform react to jsx wraps props in react spread for last spread attributes 1`] = ` -"React.createElement(Component, Object.assign({ - y: 2, - z: true -}, x));" -`; - -exports[`transform react to jsx wraps props in react spread for middle spread attributes 1`] = ` -"React.createElement(Component, Object.assign({ - y: 2 -}, x, { - z: true -}));" -`; diff --git a/packages/react/src/ReactElementValidator.js b/packages/react/src/ReactElementValidator.js index 44ac18239d408..e3b1351b53ab9 100644 --- a/packages/react/src/ReactElementValidator.js +++ b/packages/react/src/ReactElementValidator.js @@ -350,7 +350,7 @@ export function jsxWithValidation( 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.', + 'Use the Babel transform instead.', ); } } else { diff --git a/packages/react/src/__tests__/ReactElementJSX-test.internal.js b/packages/react/src/__tests__/ReactElementJSX-test.internal.js index 602cc3024a3f6..23135d15de8e3 100644 --- a/packages/react/src/__tests__/ReactElementJSX-test.internal.js +++ b/packages/react/src/__tests__/ReactElementJSX-test.internal.js @@ -222,7 +222,7 @@ describe('ReactElement.jsx', () => { ).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.', + 'Use the Babel transform instead.', {withoutStack: true}, ); }); From 208b29109119f6593118eceab85fc6f6911d90c4 Mon Sep 17 00:00:00 2001 From: Luna Ruan Date: Fri, 23 Aug 2019 14:28:44 -0700 Subject: [PATCH 18/20] modified comments --- .../src/TransformJSXToReactBabelPlugin.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/babel-plugin-react-jsx/src/TransformJSXToReactBabelPlugin.js b/packages/babel-plugin-react-jsx/src/TransformJSXToReactBabelPlugin.js index 669fbe8de3bc3..e26b65fcf7c88 100644 --- a/packages/babel-plugin-react-jsx/src/TransformJSXToReactBabelPlugin.js +++ b/packages/babel-plugin-react-jsx/src/TransformJSXToReactBabelPlugin.js @@ -319,7 +319,9 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, if (objs.length === 1) { // only one object if (!t.isObjectExpression(objs[0])) { - // this could be null, and jsx expects props to be non-null + // 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'); From 21550fa0ca0c81d9e120219fbe47d8866d9269fe Mon Sep 17 00:00:00 2001 From: Luna Ruan Date: Mon, 26 Aug 2019 10:00:41 -0700 Subject: [PATCH 19/20] use babel license --- .../src/TransformJSXToReactBabelPlugin.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/babel-plugin-react-jsx/src/TransformJSXToReactBabelPlugin.js b/packages/babel-plugin-react-jsx/src/TransformJSXToReactBabelPlugin.js index e26b65fcf7c88..960f75a3a8ecb 100644 --- a/packages/babel-plugin-react-jsx/src/TransformJSXToReactBabelPlugin.js +++ b/packages/babel-plugin-react-jsx/src/TransformJSXToReactBabelPlugin.js @@ -21,13 +21,6 @@ // 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. - -/** - * 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. - */ 'use strict'; const esutils = require('esutils'); From 765ea44db2dc58c6a9eb7ab527ef70b290620939 Mon Sep 17 00:00:00 2001 From: Luna Ruan Date: Tue, 27 Aug 2019 10:51:36 -0700 Subject: [PATCH 20/20] moved object freezing into the validator --- packages/react/src/ReactElement.js | 4 ---- packages/react/src/ReactElementValidator.js | 4 ++++ 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/react/src/ReactElement.js b/packages/react/src/ReactElement.js index 84e9d888328ff..163ae077f0aca 100644 --- a/packages/react/src/ReactElement.js +++ b/packages/react/src/ReactElement.js @@ -281,10 +281,6 @@ export function jsxDEV(type, config, maybeKey, source, self) { } } - if (Object.freeze && Array.isArray(props.children)) { - Object.freeze(props.children); - } - if (key || ref) { const displayName = typeof type === 'function' diff --git a/packages/react/src/ReactElementValidator.js b/packages/react/src/ReactElementValidator.js index e3b1351b53ab9..b81fe9c942036 100644 --- a/packages/react/src/ReactElementValidator.js +++ b/packages/react/src/ReactElementValidator.js @@ -345,6 +345,10 @@ export function jsxWithValidation( for (let i = 0; i < children.length; i++) { validateChildKeys(children[i], type); } + + if (Object.freeze) { + Object.freeze(children); + } } else { warning( false,