From bead342b516bc714276a13c62e2f388f450c8424 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Thu, 17 Jan 2019 14:54:15 +0000 Subject: [PATCH 1/4] Adds Babel plugin babel-plugin-optimize-react --- packages/babel-plugin-optimize-react/LICENSE | 21 ++ .../babel-plugin-optimize-react/README.md | 58 ++++ .../__snapshots__/createElement-test.js.snap | 30 ++ .../__snapshots__/hooks-test.js.snap | 102 +++++++ .../__tests__/createElement-test.js | 50 ++++ .../__tests__/hooks-test.js | 116 ++++++++ packages/babel-plugin-optimize-react/index.js | 277 ++++++++++++++++++ .../babel-plugin-optimize-react/package.json | 24 ++ 8 files changed, 678 insertions(+) create mode 100644 packages/babel-plugin-optimize-react/LICENSE create mode 100644 packages/babel-plugin-optimize-react/README.md create mode 100644 packages/babel-plugin-optimize-react/__tests__/__snapshots__/createElement-test.js.snap create mode 100644 packages/babel-plugin-optimize-react/__tests__/__snapshots__/hooks-test.js.snap create mode 100644 packages/babel-plugin-optimize-react/__tests__/createElement-test.js create mode 100644 packages/babel-plugin-optimize-react/__tests__/hooks-test.js create mode 100644 packages/babel-plugin-optimize-react/index.js create mode 100644 packages/babel-plugin-optimize-react/package.json diff --git a/packages/babel-plugin-optimize-react/LICENSE b/packages/babel-plugin-optimize-react/LICENSE new file mode 100644 index 00000000000..188fb2b0bd8 --- /dev/null +++ b/packages/babel-plugin-optimize-react/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2013-present, Facebook, Inc. + +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. diff --git a/packages/babel-plugin-optimize-react/README.md b/packages/babel-plugin-optimize-react/README.md new file mode 100644 index 00000000000..dd08f9fdc63 --- /dev/null +++ b/packages/babel-plugin-optimize-react/README.md @@ -0,0 +1,58 @@ +# babel-plugin-optimize-react-hooks + +This Babel 7 plugin optimizes React hooks by transforming common patterns into more effecient output when using with tools such as [Create React App](https://github.com/facebook/create-react-app). For example, with this plugin the following output is optimized as shown: + +```js +// Original +var _useState = Object(react__WEBPACK_IMPORTED_MODULE_1_["useState"])(Math.random()), + _State2 = Object(_Users_gaearon_p_create_rreact_app_node_modules_babel_runtime_helpers_esm_sliceToArray_WEBPACK_IMPORTED_MODULE_0__["default"])(_useState, 1), + value = _useState2[0]; + +// With this plugin +var useState = react__WEBPACK_IMPORTED_MODULE_1_.useState; +var __ref__0 = useState(Math.random()); +var value = __ref__0[0]; +``` + +## Named imports to hooks get transformed + +```js +// Original +import React, {useState} from 'react'; + +// With this plugin +import React from 'react'; +const {useState} = React; +``` + +## Array destructuring transform for React's built-in hooks + +```js +// Original +const [counter, setCounter] = useState(0); + +// With this plugin +const __ref__0 = useState(0); +const counter = __ref__0[0]; +const setCounter = __ref__0[1]; +``` + +## React.createElement becomes a hoisted constant + +```js +// Original +import React from 'react'; + +function MyComponent() { + return React.createElement('div', null, 'Hello world'); +} + +// With this plugin +import React from 'react'; +const __reactCreateElement__ = React.createElement; + +function MyComponent() { + return __reactCreateElement__('div', null, 'Hello world'); +} +``` + diff --git a/packages/babel-plugin-optimize-react/__tests__/__snapshots__/createElement-test.js.snap b/packages/babel-plugin-optimize-react/__tests__/__snapshots__/createElement-test.js.snap new file mode 100644 index 00000000000..60d3cd3c6b2 --- /dev/null +++ b/packages/babel-plugin-optimize-react/__tests__/__snapshots__/createElement-test.js.snap @@ -0,0 +1,30 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`React createElement transforms should transform React.createElement calls #2 1`] = ` +"const React = require(\\"react\\"); + +const __reactCreateElement__ = React.createElement; +export function MyComponent() { + return __reactCreateElement__(\\"div\\", null, __reactCreateElement__(\\"span\\", null, \\"Hello world!\\")); +}" +`; + +exports[`React createElement transforms should transform React.createElement calls #3 1`] = ` +"const React = require(\\"react\\"); + +const __reactCreateElement__ = React.createElement; + +const node = __reactCreateElement__(\\"div\\", null, __reactCreateElement__(\\"span\\", null, \\"Hello world!\\")); + +export function MyComponent() { + return node; +}" +`; + +exports[`React createElement transforms should transform React.createElement calls 1`] = ` +"import React from \\"react\\"; +const __reactCreateElement__ = React.createElement; +export function MyComponent() { + return __reactCreateElement__(\\"div\\"); +}" +`; diff --git a/packages/babel-plugin-optimize-react/__tests__/__snapshots__/hooks-test.js.snap b/packages/babel-plugin-optimize-react/__tests__/__snapshots__/hooks-test.js.snap new file mode 100644 index 00000000000..9378ef695b1 --- /dev/null +++ b/packages/babel-plugin-optimize-react/__tests__/__snapshots__/hooks-test.js.snap @@ -0,0 +1,102 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Babel plugin optimize React hooks should support destructuring hooks from imports #2 1`] = ` +"import React from \\"react\\"; +const __reactCreateElement__ = React.createElement; +const { + useState +} = React; +export function MyComponent() { + const _ref_0 = useState(0); + + const setCounter = _ref_0[1]; + const counter = _ref_0[0]; + return __reactCreateElement__(\\"div\\", null, counter); +}" +`; + +exports[`Babel plugin optimize React hooks should support destructuring hooks from imports #3 1`] = ` +"import React from \\"react\\"; +const __reactCreateElement__ = React.createElement; +const useState = React.useState; +export function MyComponent() { + const _ref_0 = useState(0); + + const setCounter = _ref_0[1]; + const counter = _ref_0[0]; + return __reactCreateElement__(\\"div\\", null, counter); +}" +`; + +exports[`Babel plugin optimize React hooks should support destructuring hooks from imports #4 1`] = ` +"import React from \\"react\\"; +const __reactCreateElement__ = React.createElement; +const foo = React.useState; +export function MyComponent() { + const _ref_0 = foo(0); + + const setCounter = _ref_0[1]; + const counter = _ref_0[0]; + return __reactCreateElement__(\\"div\\", null, counter); +}" +`; + +exports[`Babel plugin optimize React hooks should support destructuring hooks from imports #5 1`] = ` +"import React from \\"react\\"; +const __reactCreateElement__ = React.createElement; +const { + useState: foo +} = React; +export function MyComponent() { + const _ref_0 = foo(0); + + const setCounter = _ref_0[1]; + const counter = _ref_0[0]; + return __reactCreateElement__(\\"div\\", null, counter); +}" +`; + +exports[`Babel plugin optimize React hooks should support destructuring hooks from imports 1`] = ` +"import React from \\"react\\"; +const __reactCreateElement__ = React.createElement; +const { + useState +} = React; +export function MyComponent() { + const _ref_0 = useState(0); + + const setCounter = _ref_0[1]; + const counter = _ref_0[0]; + return __reactCreateElement__(\\"div\\", null, counter); +}" +`; + +exports[`Babel plugin optimize React hooks should support destructuring hooks from require calls 1`] = ` +"const React = require(\\"react\\"); + +const __reactCreateElement__ = React.createElement; +const { + useState +} = React; +export function MyComponent() { + const _ref_0 = useState(0); + + const setCounter = _ref_0[1]; + const counter = _ref_0[0]; + return __reactCreateElement__(\\"div\\", null, counter); +}" +`; + +exports[`Babel plugin optimize React hooks should support transform hook imports 1`] = ` +"import React from \\"react\\"; +const { + useState +} = React;" +`; + +exports[`Babel plugin optimize React hooks should support transform hook imports with aliasing 1`] = ` +"import React from \\"react\\"; +const { + useState: foo +} = React;" +`; diff --git a/packages/babel-plugin-optimize-react/__tests__/createElement-test.js b/packages/babel-plugin-optimize-react/__tests__/createElement-test.js new file mode 100644 index 00000000000..b2c388834a3 --- /dev/null +++ b/packages/babel-plugin-optimize-react/__tests__/createElement-test.js @@ -0,0 +1,50 @@ +'use strict'; + +const plugin = require('../index.js'); +const babel = require('@babel/core'); + +function transform(code) { + return babel.transform(code, { + plugins: [plugin], + }).code; +} + +describe('React createElement transforms', () => { + it('should transform React.createElement calls', () => { + const test = ` + import React from "react"; + + export function MyComponent() { + return React.createElement("div"); + } + `; + const output = transform(test); + expect(output).toMatchSnapshot(); + }); + + it('should transform React.createElement calls #2', () => { + const test = ` + const React = require("react"); + + export function MyComponent() { + return React.createElement("div", null, React.createElement("span", null, "Hello world!")); + } + `; + const output = transform(test); + expect(output).toMatchSnapshot(); + }); + + it('should transform React.createElement calls #3', () => { + const test = ` + const React = require("react"); + + const node = React.createElement("div", null, React.createElement("span", null, "Hello world!")); + + export function MyComponent() { + return node; + } + `; + const output = transform(test); + expect(output).toMatchSnapshot(); + }); +}); diff --git a/packages/babel-plugin-optimize-react/__tests__/hooks-test.js b/packages/babel-plugin-optimize-react/__tests__/hooks-test.js new file mode 100644 index 00000000000..96f8d639bbd --- /dev/null +++ b/packages/babel-plugin-optimize-react/__tests__/hooks-test.js @@ -0,0 +1,116 @@ +'use strict'; + +const plugin = require('../index.js'); +const babel = require('@babel/core'); + +function transform(code) { + return babel.transform(code, { + plugins: [plugin], + }).code; +} + +describe('Babel plugin optimize React hooks', () => { + it('should support transform hook imports', () => { + const test = ` + import React, {useState} from "react"; + `; + const output = transform(test); + expect(output).toMatchSnapshot(); + }); + + it('should support transform hook imports with aliasing', () => { + const test = ` + import React, {useState as foo} from "react"; + `; + const output = transform(test); + expect(output).toMatchSnapshot(); + }); + + it('should support destructuring hooks from imports', () => { + const test = ` + import React, {useState} from "react"; + + export function MyComponent() { + const [counter, setCounter] = useState(0); + + return React.createElement("div", null, counter); + } + `; + const output = transform(test); + expect(output).toMatchSnapshot(); + }); + + it('should support destructuring hooks from imports #2', () => { + const test = ` + import React from "react"; + const {useState} = React; + + export function MyComponent() { + const [counter, setCounter] = useState(0); + + return React.createElement("div", null, counter); + } + `; + const output = transform(test); + expect(output).toMatchSnapshot(); + }); + + it('should support destructuring hooks from imports #3', () => { + const test = ` + import React from "react"; + const useState = React.useState; + + export function MyComponent() { + const [counter, setCounter] = useState(0); + + return React.createElement("div", null, counter); + } + `; + const output = transform(test); + expect(output).toMatchSnapshot(); + }); + + it('should support destructuring hooks from imports #4', () => { + const test = ` + import React from "react"; + const foo = React.useState; + + export function MyComponent() { + const [counter, setCounter] = foo(0); + + return React.createElement("div", null, counter); + } + `; + const output = transform(test); + expect(output).toMatchSnapshot(); + }); + + it('should support destructuring hooks from imports #5', () => { + const test = ` + import React, {useState as foo} from "react"; + + export function MyComponent() { + const [counter, setCounter] = foo(0); + + return React.createElement("div", null, counter); + } + `; + const output = transform(test); + expect(output).toMatchSnapshot(); + }); + + it('should support destructuring hooks from require calls', () => { + const test = ` + const React = require("react"); + const {useState} = React; + + export function MyComponent() { + const [counter, setCounter] = useState(0); + + return React.createElement("div", null, counter); + } + `; + const output = transform(test); + expect(output).toMatchSnapshot(); + }); +}); diff --git a/packages/babel-plugin-optimize-react/index.js b/packages/babel-plugin-optimize-react/index.js new file mode 100644 index 00000000000..20e7d2235b1 --- /dev/null +++ b/packages/babel-plugin-optimize-react/index.js @@ -0,0 +1,277 @@ +'use strict'; + +const reactHooks = new Set([ + 'useCallback', + 'useContext', + 'useDebugValue', + 'useEffect', + 'useImperativeHandle', + 'useLayoutEffect', + 'useMemo', + 'useReducer', + 'useRef', + 'useState', +]); + +module.exports = function(babel) { + const { types: t } = babel; + + // Collects named imports of React hooks from the "react" package + function collectReactHooksAndRemoveTheirNamedImports(path) { + const node = path.node; + const hooks = []; + if (t.isStringLiteral(node.source) && node.source.value === 'react') { + const specifiers = path.get('specifiers'); + + for (let specifier of specifiers) { + if (t.isImportSpecifier(specifier)) { + const importedNode = specifier.node.imported; + const localNode = specifier.node.local; + + if (t.isIdentifier(importedNode) && t.isIdentifier(localNode)) { + if (reactHooks.has(importedNode.name)) { + hooks.push({ + imported: importedNode.name, + local: localNode.name, + }); + specifier.remove(); + } + } + } + } + } + return hooks; + } + + function isReactImport(path) { + if (t.isIdentifier(path)) { + const identifierName = path.node.name; + const binding = path.scope.getBinding(identifierName); + + if (binding !== undefined) { + const bindingPath = binding.path; + + if (t.isImportDefaultSpecifier(bindingPath)) { + const parentPath = bindingPath.parentPath; + + if ( + t.isImportDeclaration(parentPath) && + t.isStringLiteral(parentPath.node.source) && + parentPath.node.source.value === 'react' + ) { + return true; + } + } else if (t.isVariableDeclarator(bindingPath)) { + const init = bindingPath.get('init'); + + if ( + t.isCallExpression(init) && + t.isIdentifier(init.node.callee) && + init.node.callee.name === 'require' && + init.node.arguments.length === 1 && + t.isStringLiteral(init.node.arguments[0]) && + init.node.arguments[0].value === 'react' + ) { + return true; + } + } + } + } + return false; + } + + function isReferencingReactHook(path) { + if (t.isIdentifier(path)) { + const identifierName = path.node.name; + const binding = path.scope.getBinding(identifierName); + + if (binding !== undefined) { + const bindingPath = binding.path; + + if (t.isVariableDeclarator(bindingPath)) { + const init = bindingPath.get('init'); + const bindingId = binding.identifier; + + if (t.isIdentifier(init) && isReactImport(init)) { + if (reactHooks.has(bindingId.name)) { + return true; + } + const id = bindingPath.get('id'); + + if (t.isObjectPattern(id)) { + const properties = id.get('properties'); + + for (let property of properties) { + if ( + t.isObjectProperty(property) && + property.node.value === bindingId && + t.isIdentifier(property.node.key) && + reactHooks.has(property.node.key.name) + ) { + return true; + } + } + } + } else if (t.isMemberExpression(init)) { + const object = init.get('object'); + const property = init.get('property'); + + if ( + isReactImport(object) && + t.isIdentifier(property) && + reactHooks.has(property.node.name) + ) { + return true; + } + } + } + } + } + return false; + } + + function isUsingDestructuredArray(path) { + const parentPath = path.parentPath; + + if (t.isVariableDeclarator(parentPath)) { + const id = parentPath.get('id'); + return t.isArrayPattern(id); + } + return false; + } + + function isCreateReactElementCall(path) { + if (t.isCallExpression(path)) { + const callee = path.get('callee'); + + if (t.isMemberExpression(callee)) { + const object = callee.get('object'); + const property = callee.get('property'); + + if ( + isReactImport(object) && + t.isIdentifier(property) && + property.node.name === 'createElement' + ) { + return true; + } + } + } + } + + function createConstantCreateElementReference(reactReferencePath) { + const identifierName = reactReferencePath.node.name; + const binding = reactReferencePath.scope.getBinding(identifierName); + const createElementReference = t.identifier('__reactCreateElement__'); + const createElementDeclaration = t.variableDeclaration('const', [ + t.variableDeclarator( + createElementReference, + t.memberExpression(t.identifier('React'), t.identifier('createElement')) + ), + ]); + const bindingPath = binding.path; + + if (t.isImportDefaultSpecifier(bindingPath) || t.isVariableDeclarator(bindingPath)) { + bindingPath.parentPath.insertAfter(createElementDeclaration); + // Make sure we declare our new now so scope tracking continues to work + const reactElementDeclarationPath = bindingPath.parentPath.getNextSibling(); + reactReferencePath.scope.registerDeclaration(reactElementDeclarationPath); + } + return createElementReference; + } + + return { + name: 'babel-plugin-optimize-react', + visitor: { + ImportDeclaration(path) { + // Collect all hooks that are named imports from the React package. i.e.: + // import React, {useState} from "react"; + // As we collection them, we also remove the imports from the declaration. + + const importedHooks = collectReactHooksAndRemoveTheirNamedImports(path); + if (importedHooks.length > 0) { + // Create a destructured variable declaration. i.e.: + // const {useEffect, useState} = React; + // Then insert it below the import declaration node. + + const declarations = t.variableDeclarator( + t.objectPattern( + importedHooks.map(({ imported, local }) => + t.objectProperty( + t.identifier(imported), + t.identifier(local), + false, + imported === local + ) + ) + ), + t.identifier('React') + ); + const hookDeclarationNode = t.variableDeclaration('const', [ + declarations, + ]); + path.insertAfter(hookDeclarationNode); + // Make sure we declare our new now so scope tracking continues to work + const hookDeclarationPath = path.getNextSibling(); + path.scope.registerDeclaration(hookDeclarationPath); + } + }, + CallExpression(path, state) { + if (state.destructuredCounter === undefined) { + state.destructuredCounter = 0; + } + const calleePath = path.get('callee'); + + // Ensure we found a primitive React hook that is using a destructuring array pattern + if ( + isUsingDestructuredArray(path) && + isReferencingReactHook(calleePath) + ) { + const parentPath = path.parentPath; + + if (t.isVariableDeclarator(parentPath)) { + const id = parentPath.get('id'); + const elements = id.get('elements'); + const kind = parentPath.parentPath.node.kind; + // Replace the array destructure pattern with a reference node. + + const referenceNode = t.identifier( + '_ref_' + state.destructuredCounter++ + ); + id.replaceWith(referenceNode); + // Now insert references to the reference node, i.e.: + // const counter = __ref__[0]; + + let arrayIndex = 0; + for (let element of elements) { + const arrayAccessNode = t.variableDeclaration(kind, [ + t.variableDeclarator( + element.node, + t.memberExpression( + referenceNode, + t.numericLiteral(arrayIndex++), + true + ) + ), + ]); + parentPath.parentPath.insertAfter(arrayAccessNode); + // Make sure we declare our new now so scope tracking continues to work + const arrayAccessPath = path.getNextSibling(); + path.scope.registerDeclaration(arrayAccessPath); + } + } + } else if (isCreateReactElementCall(path)) { + const callee = path.get('callee'); + const reactReferencePath = callee.get('object'); + + if (state.createElementReference === undefined) { + state.createElementReference = createConstantCreateElementReference( + reactReferencePath + ); + } + callee.replaceWith(state.createElementReference); + } + }, + }, + }; +}; diff --git a/packages/babel-plugin-optimize-react/package.json b/packages/babel-plugin-optimize-react/package.json new file mode 100644 index 00000000000..39360d6fb1d --- /dev/null +++ b/packages/babel-plugin-optimize-react/package.json @@ -0,0 +1,24 @@ +{ + "name": "babel-plugin-optimize-react", + "version": "0.0.1", + "description": "Babel plugin for optimizing common React patterns", + "repository": "facebookincubator/create-react-app", + "license": "MIT", + "bugs": { + "url": "https://github.com/facebookincubator/create-react-app/issues" + }, + "main": "index.js", + "files": [ + "index.js" + ], + "peerDependencies": { + "@babel/core": "^7.1.0" + }, + "devDependencies": { + "jest": "^23.6.0", + "prettier": "^1.15.3" + }, + "scripts": { + "test": "jest" + } +} From 591ce2509e26d158f280dce43c3548f4d1f3ddae Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Thu, 17 Jan 2019 15:16:10 +0000 Subject: [PATCH 2/4] Typo --- packages/babel-plugin-optimize-react/__tests__/hooks-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/babel-plugin-optimize-react/__tests__/hooks-test.js b/packages/babel-plugin-optimize-react/__tests__/hooks-test.js index 96f8d639bbd..beb15e436cb 100644 --- a/packages/babel-plugin-optimize-react/__tests__/hooks-test.js +++ b/packages/babel-plugin-optimize-react/__tests__/hooks-test.js @@ -9,7 +9,7 @@ function transform(code) { }).code; } -describe('Babel plugin optimize React hooks', () => { +describe('React hook transforms', () => { it('should support transform hook imports', () => { const test = ` import React, {useState} from "react"; From 835fcbd34fb988bb15b493dd963e05a4ff8ecd52 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Thu, 17 Jan 2019 19:00:39 +0000 Subject: [PATCH 3/4] Increase version and added fix for no default import --- .../babel-plugin-optimize-react/README.md | 2 +- .../__snapshots__/hooks-test.js.snap | 108 ++++++++++++++++++ .../__tests__/hooks-test.js | 8 ++ packages/babel-plugin-optimize-react/index.js | 9 ++ .../babel-plugin-optimize-react/package.json | 2 +- 5 files changed, 127 insertions(+), 2 deletions(-) diff --git a/packages/babel-plugin-optimize-react/README.md b/packages/babel-plugin-optimize-react/README.md index dd08f9fdc63..40ed39e0c04 100644 --- a/packages/babel-plugin-optimize-react/README.md +++ b/packages/babel-plugin-optimize-react/README.md @@ -1,4 +1,4 @@ -# babel-plugin-optimize-react-hooks +# babel-plugin-optimize-react This Babel 7 plugin optimizes React hooks by transforming common patterns into more effecient output when using with tools such as [Create React App](https://github.com/facebook/create-react-app). For example, with this plugin the following output is optimized as shown: diff --git a/packages/babel-plugin-optimize-react/__tests__/__snapshots__/hooks-test.js.snap b/packages/babel-plugin-optimize-react/__tests__/__snapshots__/hooks-test.js.snap index 9378ef695b1..2c1b42ff359 100644 --- a/packages/babel-plugin-optimize-react/__tests__/__snapshots__/hooks-test.js.snap +++ b/packages/babel-plugin-optimize-react/__tests__/__snapshots__/hooks-test.js.snap @@ -100,3 +100,111 @@ const { useState: foo } = React;" `; + +exports[`React hook transforms should support destructuring hooks from imports #2 1`] = ` +"import React from \\"react\\"; +const __reactCreateElement__ = React.createElement; +const { + useState +} = React; +export function MyComponent() { + const _ref_0 = useState(0); + + const setCounter = _ref_0[1]; + const counter = _ref_0[0]; + return __reactCreateElement__(\\"div\\", null, counter); +}" +`; + +exports[`React hook transforms should support destructuring hooks from imports #3 1`] = ` +"import React from \\"react\\"; +const __reactCreateElement__ = React.createElement; +const useState = React.useState; +export function MyComponent() { + const _ref_0 = useState(0); + + const setCounter = _ref_0[1]; + const counter = _ref_0[0]; + return __reactCreateElement__(\\"div\\", null, counter); +}" +`; + +exports[`React hook transforms should support destructuring hooks from imports #4 1`] = ` +"import React from \\"react\\"; +const __reactCreateElement__ = React.createElement; +const foo = React.useState; +export function MyComponent() { + const _ref_0 = foo(0); + + const setCounter = _ref_0[1]; + const counter = _ref_0[0]; + return __reactCreateElement__(\\"div\\", null, counter); +}" +`; + +exports[`React hook transforms should support destructuring hooks from imports #5 1`] = ` +"import React from \\"react\\"; +const __reactCreateElement__ = React.createElement; +const { + useState: foo +} = React; +export function MyComponent() { + const _ref_0 = foo(0); + + const setCounter = _ref_0[1]; + const counter = _ref_0[0]; + return __reactCreateElement__(\\"div\\", null, counter); +}" +`; + +exports[`React hook transforms should support destructuring hooks from imports 1`] = ` +"import React from \\"react\\"; +const __reactCreateElement__ = React.createElement; +const { + useState +} = React; +export function MyComponent() { + const _ref_0 = useState(0); + + const setCounter = _ref_0[1]; + const counter = _ref_0[0]; + return __reactCreateElement__(\\"div\\", null, counter); +}" +`; + +exports[`React hook transforms should support destructuring hooks from require calls 1`] = ` +"const React = require(\\"react\\"); + +const __reactCreateElement__ = React.createElement; +const { + useState +} = React; +export function MyComponent() { + const _ref_0 = useState(0); + + const setCounter = _ref_0[1]; + const counter = _ref_0[0]; + return __reactCreateElement__(\\"div\\", null, counter); +}" +`; + +exports[`React hook transforms should support transform hook imports 1`] = ` +"import React from \\"react\\"; +const { + useState +} = React;" +`; + +exports[`React hook transforms should support transform hook imports with aliasing 1`] = ` +"import React from \\"react\\"; +const { + useState: foo +} = React;" +`; + +exports[`React hook transforms should support transform hook imports with no default 1`] = ` +"import React from \\"react\\"; +const { + useState +} = React;" +`; diff --git a/packages/babel-plugin-optimize-react/__tests__/hooks-test.js b/packages/babel-plugin-optimize-react/__tests__/hooks-test.js index beb15e436cb..0cc16e441d4 100644 --- a/packages/babel-plugin-optimize-react/__tests__/hooks-test.js +++ b/packages/babel-plugin-optimize-react/__tests__/hooks-test.js @@ -113,4 +113,12 @@ describe('React hook transforms', () => { const output = transform(test); expect(output).toMatchSnapshot(); }); + + it('should support transform hook imports with no default', () => { + const test = ` + import {useState} from "react"; + `; + const output = transform(test); + expect(output).toMatchSnapshot(); + }); }); diff --git a/packages/babel-plugin-optimize-react/index.js b/packages/babel-plugin-optimize-react/index.js index 20e7d2235b1..7331795f15c 100644 --- a/packages/babel-plugin-optimize-react/index.js +++ b/packages/babel-plugin-optimize-react/index.js @@ -22,6 +22,7 @@ module.exports = function(babel) { const hooks = []; if (t.isStringLiteral(node.source) && node.source.value === 'react') { const specifiers = path.get('specifiers'); + let hasDefaultSpecifier = false; for (let specifier of specifiers) { if (t.isImportSpecifier(specifier)) { @@ -37,8 +38,16 @@ module.exports = function(babel) { specifier.remove(); } } + } else if (t.isImportDefaultSpecifier(specifier)) { + hasDefaultSpecifier = true; } } + // If there is no default specifier for React, add one + if (!hasDefaultSpecifier && specifiers.length > 0) { + const defaultSpecifierNode = t.importDefaultSpecifier(t.identifier("React")); + + path.pushContainer('specifiers', defaultSpecifierNode); + } } return hooks; } diff --git a/packages/babel-plugin-optimize-react/package.json b/packages/babel-plugin-optimize-react/package.json index 39360d6fb1d..eabc1594a66 100644 --- a/packages/babel-plugin-optimize-react/package.json +++ b/packages/babel-plugin-optimize-react/package.json @@ -1,6 +1,6 @@ { "name": "babel-plugin-optimize-react", - "version": "0.0.1", + "version": "0.0.2", "description": "Babel plugin for optimizing common React patterns", "repository": "facebookincubator/create-react-app", "license": "MIT", From f472a2af8bed7505af347a99a50e12a133bba752 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Thu, 17 Jan 2019 20:29:04 +0000 Subject: [PATCH 4/4] Fixed more bugs + added more tests + bumped version --- .../__snapshots__/hooks-test.js.snap | 115 +++--------------- .../__tests__/hooks-test.js | 30 ++++- packages/babel-plugin-optimize-react/index.js | 29 +++-- .../babel-plugin-optimize-react/package.json | 2 +- 4 files changed, 67 insertions(+), 109 deletions(-) diff --git a/packages/babel-plugin-optimize-react/__tests__/__snapshots__/hooks-test.js.snap b/packages/babel-plugin-optimize-react/__tests__/__snapshots__/hooks-test.js.snap index 2c1b42ff359..e2aef83464f 100644 --- a/packages/babel-plugin-optimize-react/__tests__/__snapshots__/hooks-test.js.snap +++ b/packages/babel-plugin-optimize-react/__tests__/__snapshots__/hooks-test.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Babel plugin optimize React hooks should support destructuring hooks from imports #2 1`] = ` +exports[`React hook transforms should support destructuring hooks from imports #2 1`] = ` "import React from \\"react\\"; const __reactCreateElement__ = React.createElement; const { @@ -15,7 +15,7 @@ export function MyComponent() { }" `; -exports[`Babel plugin optimize React hooks should support destructuring hooks from imports #3 1`] = ` +exports[`React hook transforms should support destructuring hooks from imports #3 1`] = ` "import React from \\"react\\"; const __reactCreateElement__ = React.createElement; const useState = React.useState; @@ -28,7 +28,7 @@ export function MyComponent() { }" `; -exports[`Babel plugin optimize React hooks should support destructuring hooks from imports #4 1`] = ` +exports[`React hook transforms should support destructuring hooks from imports #4 1`] = ` "import React from \\"react\\"; const __reactCreateElement__ = React.createElement; const foo = React.useState; @@ -41,7 +41,7 @@ export function MyComponent() { }" `; -exports[`Babel plugin optimize React hooks should support destructuring hooks from imports #5 1`] = ` +exports[`React hook transforms should support destructuring hooks from imports #5 1`] = ` "import React from \\"react\\"; const __reactCreateElement__ = React.createElement; const { @@ -56,7 +56,7 @@ export function MyComponent() { }" `; -exports[`Babel plugin optimize React hooks should support destructuring hooks from imports 1`] = ` +exports[`React hook transforms should support destructuring hooks from imports 1`] = ` "import React from \\"react\\"; const __reactCreateElement__ = React.createElement; const { @@ -71,7 +71,7 @@ export function MyComponent() { }" `; -exports[`Babel plugin optimize React hooks should support destructuring hooks from require calls 1`] = ` +exports[`React hook transforms should support destructuring hooks from require calls 1`] = ` "const React = require(\\"react\\"); const __reactCreateElement__ = React.createElement; @@ -87,105 +87,40 @@ export function MyComponent() { }" `; -exports[`Babel plugin optimize React hooks should support transform hook imports 1`] = ` -"import React from \\"react\\"; -const { +exports[`React hook transforms should support hook CJS require with no default 1`] = ` +"const { useState -} = React;" +} = require(\\"react\\");" `; -exports[`Babel plugin optimize React hooks should support transform hook imports with aliasing 1`] = ` +exports[`React hook transforms should support hook imports with aliasing 1`] = ` "import React from \\"react\\"; const { useState: foo } = React;" `; -exports[`React hook transforms should support destructuring hooks from imports #2 1`] = ` +exports[`React hook transforms should support hook imports with no default 1`] = ` "import React from \\"react\\"; -const __reactCreateElement__ = React.createElement; const { useState -} = React; -export function MyComponent() { - const _ref_0 = useState(0); - - const setCounter = _ref_0[1]; - const counter = _ref_0[0]; - return __reactCreateElement__(\\"div\\", null, counter); -}" -`; - -exports[`React hook transforms should support destructuring hooks from imports #3 1`] = ` -"import React from \\"react\\"; -const __reactCreateElement__ = React.createElement; -const useState = React.useState; -export function MyComponent() { - const _ref_0 = useState(0); - - const setCounter = _ref_0[1]; - const counter = _ref_0[0]; - return __reactCreateElement__(\\"div\\", null, counter); -}" -`; - -exports[`React hook transforms should support destructuring hooks from imports #4 1`] = ` -"import React from \\"react\\"; -const __reactCreateElement__ = React.createElement; -const foo = React.useState; -export function MyComponent() { - const _ref_0 = foo(0); - - const setCounter = _ref_0[1]; - const counter = _ref_0[0]; - return __reactCreateElement__(\\"div\\", null, counter); -}" -`; - -exports[`React hook transforms should support destructuring hooks from imports #5 1`] = ` -"import React from \\"react\\"; -const __reactCreateElement__ = React.createElement; -const { - useState: foo -} = React; -export function MyComponent() { - const _ref_0 = foo(0); - - const setCounter = _ref_0[1]; - const counter = _ref_0[0]; - return __reactCreateElement__(\\"div\\", null, counter); -}" +} = React;" `; -exports[`React hook transforms should support destructuring hooks from imports 1`] = ` +exports[`React hook transforms should support mixed hook imports 1`] = ` "import React from \\"react\\"; -const __reactCreateElement__ = React.createElement; +import { memo } from \\"react\\"; const { useState -} = React; -export function MyComponent() { - const _ref_0 = useState(0); - - const setCounter = _ref_0[1]; - const counter = _ref_0[0]; - return __reactCreateElement__(\\"div\\", null, counter); -}" +} = React;" `; -exports[`React hook transforms should support destructuring hooks from require calls 1`] = ` -"const React = require(\\"react\\"); - -const __reactCreateElement__ = React.createElement; +exports[`React hook transforms should support mixed hook imports with no default 1`] = ` +"import React from \\"react\\"; const { useState } = React; -export function MyComponent() { - const _ref_0 = useState(0); - - const setCounter = _ref_0[1]; - const counter = _ref_0[0]; - return __reactCreateElement__(\\"div\\", null, counter); -}" +import { memo } from \\"react\\";" `; exports[`React hook transforms should support transform hook imports 1`] = ` @@ -194,17 +129,3 @@ const { useState } = React;" `; - -exports[`React hook transforms should support transform hook imports with aliasing 1`] = ` -"import React from \\"react\\"; -const { - useState: foo -} = React;" -`; - -exports[`React hook transforms should support transform hook imports with no default 1`] = ` -"import React from \\"react\\"; -const { - useState -} = React;" -`; diff --git a/packages/babel-plugin-optimize-react/__tests__/hooks-test.js b/packages/babel-plugin-optimize-react/__tests__/hooks-test.js index 0cc16e441d4..c67046e756c 100644 --- a/packages/babel-plugin-optimize-react/__tests__/hooks-test.js +++ b/packages/babel-plugin-optimize-react/__tests__/hooks-test.js @@ -18,7 +18,7 @@ describe('React hook transforms', () => { expect(output).toMatchSnapshot(); }); - it('should support transform hook imports with aliasing', () => { + it('should support hook imports with aliasing', () => { const test = ` import React, {useState as foo} from "react"; `; @@ -114,11 +114,37 @@ describe('React hook transforms', () => { expect(output).toMatchSnapshot(); }); - it('should support transform hook imports with no default', () => { + it('should support hook imports with no default', () => { const test = ` import {useState} from "react"; `; const output = transform(test); expect(output).toMatchSnapshot(); }); + + it('should support hook CJS require with no default', () => { + const test = ` + const {useState} = require("react"); + `; + const output = transform(test); + expect(output).toMatchSnapshot(); + }); + + it('should support mixed hook imports', () => { + const test = ` + import React from "react"; + import {memo, useState} from "react"; + `; + const output = transform(test); + expect(output).toMatchSnapshot(); + }); + + it('should support mixed hook imports with no default', () => { + const test = ` + import {useState} from "react"; + import {memo} from "react"; + `; + const output = transform(test); + expect(output).toMatchSnapshot(); + }); }); diff --git a/packages/babel-plugin-optimize-react/index.js b/packages/babel-plugin-optimize-react/index.js index 7331795f15c..e42efb54eec 100644 --- a/packages/babel-plugin-optimize-react/index.js +++ b/packages/babel-plugin-optimize-react/index.js @@ -17,12 +17,14 @@ module.exports = function(babel) { const { types: t } = babel; // Collects named imports of React hooks from the "react" package - function collectReactHooksAndRemoveTheirNamedImports(path) { + function collectReactHooksAndRemoveTheirNamedImports(path, state) { const node = path.node; const hooks = []; if (t.isStringLiteral(node.source) && node.source.value === 'react') { const specifiers = path.get('specifiers'); - let hasDefaultSpecifier = false; + if (state.hasDefaultSpecifier === undefined) { + state.hasDefaultSpecifier = false; + } for (let specifier of specifiers) { if (t.isImportSpecifier(specifier)) { @@ -39,14 +41,17 @@ module.exports = function(babel) { } } } else if (t.isImportDefaultSpecifier(specifier)) { - hasDefaultSpecifier = true; + state.hasDefaultSpecifier = true; } } // If there is no default specifier for React, add one - if (!hasDefaultSpecifier && specifiers.length > 0) { - const defaultSpecifierNode = t.importDefaultSpecifier(t.identifier("React")); - + if (state.hasDefaultSpecifier === false && specifiers.length > 0) { + const defaultSpecifierNode = t.importDefaultSpecifier( + t.identifier('React') + ); + path.pushContainer('specifiers', defaultSpecifierNode); + state.hasDefaultSpecifier = true; } } return hooks; @@ -180,7 +185,10 @@ module.exports = function(babel) { ]); const bindingPath = binding.path; - if (t.isImportDefaultSpecifier(bindingPath) || t.isVariableDeclarator(bindingPath)) { + if ( + t.isImportDefaultSpecifier(bindingPath) || + t.isVariableDeclarator(bindingPath) + ) { bindingPath.parentPath.insertAfter(createElementDeclaration); // Make sure we declare our new now so scope tracking continues to work const reactElementDeclarationPath = bindingPath.parentPath.getNextSibling(); @@ -192,12 +200,15 @@ module.exports = function(babel) { return { name: 'babel-plugin-optimize-react', visitor: { - ImportDeclaration(path) { + ImportDeclaration(path, state) { // Collect all hooks that are named imports from the React package. i.e.: // import React, {useState} from "react"; // As we collection them, we also remove the imports from the declaration. - const importedHooks = collectReactHooksAndRemoveTheirNamedImports(path); + const importedHooks = collectReactHooksAndRemoveTheirNamedImports( + path, + state + ); if (importedHooks.length > 0) { // Create a destructured variable declaration. i.e.: // const {useEffect, useState} = React; diff --git a/packages/babel-plugin-optimize-react/package.json b/packages/babel-plugin-optimize-react/package.json index eabc1594a66..b51d78d4179 100644 --- a/packages/babel-plugin-optimize-react/package.json +++ b/packages/babel-plugin-optimize-react/package.json @@ -1,6 +1,6 @@ { "name": "babel-plugin-optimize-react", - "version": "0.0.2", + "version": "0.0.3", "description": "Babel plugin for optimizing common React patterns", "repository": "facebookincubator/create-react-app", "license": "MIT",