├── .github └── workflows │ └── nodejs.yml ├── .gitignore ├── LICENSE ├── README.md ├── index.js ├── package-lock.json ├── package.json └── tests ├── __snapshots__ └── index.test.js.snap └── index.test.js /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | test: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [10.x, 12.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v1 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - name: npm install test 21 | run: | 22 | npm install 23 | npm install @babel/core 24 | npm run test 25 | env: 26 | CI: true 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Daniil Poroshin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | *Note:* since there is the new [JSX transform](https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html), it might make sense to prefer the official plugin. 2 | 3 | This plugin transforms ES6-style imports from React that cannot be mangled (because with standard options minifiers don't know if property access is constant or not) to local variables. 4 | 5 | Example: 6 | 7 | ```javascript 8 | // Original code: 9 | function render() { 10 | const [v, s] = a.b.useState(0) 11 | return a.b.createElement('p', null, 'Text', v) 12 | } 13 | 14 | // Modified code: 15 | const h = a.b.createElement 16 | const { useState: f } = a.b 17 | function render() { 18 | const [v, s] = f(0) 19 | return h('p', null, 'Text', v) 20 | } 21 | ``` -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const { types: t } = require('@babel/core') 2 | 3 | module.exports = function() { 4 | // Create variable declaration node with destructuring pattern and 5 | // insert it after first default or namespace import from react 6 | function createDestructuringNode(path, props) { 7 | // First specifier of import statement is default or namespace one 8 | const target = path.node.specifiers[0].local.name 9 | const declarator = t.variableDeclarator( 10 | t.objectPattern( 11 | props.map(specifier => 12 | t.objectProperty( 13 | t.identifier(specifier.imported), 14 | t.identifier(specifier.local), 15 | false, 16 | specifier.imported === specifier.local 17 | ) 18 | ) 19 | ), 20 | t.identifier(target) 21 | ) 22 | path.insertAfter(t.variableDeclaration('const', [declarator])) 23 | path.scope.registerDeclaration(path.getNextSibling()) 24 | } 25 | 26 | function visitImportDeclaration(path, state) { 27 | if (path.node.source.value !== 'react') { 28 | return 29 | } 30 | 31 | // Collect named specifiers to list and remove them from the import 32 | // statement. Default and namespace specifiers are saved 33 | const imports = [] 34 | const specifiers = path.get('specifiers') 35 | for (const specifier of specifiers) { 36 | if (t.isImportSpecifier(specifier)) { 37 | imports.push({ 38 | imported: specifier.node.imported.name, 39 | local: specifier.node.local.name 40 | }) 41 | specifier.remove() 42 | } else if (!state.generalImport) { 43 | // This is default or namespace specifier 44 | state.generalImport = path 45 | } 46 | } 47 | 48 | if (!state.generalImport) { 49 | // We need default or namespace import from React for variable declaration, 50 | // so if there is no default or namespace import just add it 51 | const id = path.scope.generateUidIdentifier('React') 52 | path.unshiftContainer('specifiers', [t.importDefaultSpecifier(id)]) 53 | path.scope.registerDeclaration(path) 54 | state.generalImport = path 55 | } else { 56 | // In case we have multiple imports we can just remove the last one 57 | const updatedSpecifiers = path.get('specifiers') 58 | if (updatedSpecifiers.length === 0) { 59 | path.remove() 60 | } 61 | } 62 | 63 | if (imports.length > 0) { 64 | createDestructuringNode(state.generalImport, imports) 65 | } 66 | } 67 | 68 | function isReactImport(path) { 69 | const identifierName = path.node.name 70 | const binding = path.scope.getBinding(identifierName) 71 | if ( 72 | binding !== undefined && 73 | (t.isImportDefaultSpecifier(binding.path) || 74 | t.isImportNamespaceSpecifier(binding.path)) 75 | ) { 76 | const parentPath = binding.path.parentPath 77 | return parentPath.node.source.value === 'react' 78 | } 79 | return false 80 | } 81 | 82 | // Insert after import statement reference to `React.createElement` 83 | // and return its name for use across all AST 84 | function createCreateElementRef(path) { 85 | const reference = path.scope.generateUidIdentifier('createElement') 86 | const declaration = t.variableDeclaration('const', [ 87 | t.variableDeclarator( 88 | reference, 89 | t.memberExpression(t.identifier('React'), t.identifier('createElement')) 90 | ) 91 | ]) 92 | path.insertAfter(declaration) 93 | path.scope.registerDeclaration(path.getNextSibling()) 94 | return reference 95 | } 96 | 97 | // Transform each calling of `React.createElement` to calling of its 98 | // alias declared in a global scope (import statement scope) 99 | function visitCallExpression(path, state) { 100 | const callee = path.get('callee') 101 | if (t.isMemberExpression(callee)) { 102 | const object = callee.get('object') 103 | if (!isReactImport(object)) { 104 | return 105 | } 106 | const property = callee.get('property') 107 | if (t.isIdentifier(property) && property.node.name === 'createElement') { 108 | // We keep it in state to be sure we avoid name collision in each file 109 | if (!state.createElementRef) { 110 | state.createElementRef = createCreateElementRef(state.generalImport) 111 | } 112 | callee.replaceWith(state.createElementRef) 113 | } 114 | } 115 | } 116 | 117 | return { 118 | name: 'babel-plugin-react-local', 119 | visitor: { 120 | ImportDeclaration: visitImportDeclaration, 121 | CallExpression: visitCallExpression 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "babel-plugin-react-local", 3 | "version": "0.7.1", 4 | "repository": "https://github.com/danya/react-local", 5 | "author": "Daniil Poroshin ", 6 | "license": "MIT", 7 | "main": "index.js", 8 | "scripts": { 9 | "test": "jest ./tests --coverage" 10 | }, 11 | "peerDependencies": { 12 | "@babel/core": "^7.1.6" 13 | }, 14 | "devDependencies": { 15 | "jest": "^27.4.3" 16 | }, 17 | "dependencies": { 18 | "@babel/core": "^7.1.6" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/__snapshots__/index.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`creates local variables for named imports 1`] = ` 4 | "import React from 'react'; 5 | const { 6 | useState, 7 | useEffect 8 | } = React;" 9 | `; 10 | 11 | exports[`supports import aliasing 1`] = ` 12 | "import React from 'react'; 13 | const { 14 | useState: foo 15 | } = React;" 16 | `; 17 | 18 | exports[`supports import with no default import 1`] = ` 19 | "import _React from 'react'; 20 | const { 21 | useState 22 | } = _React;" 23 | `; 24 | 25 | exports[`supports mixed imports 1`] = ` 26 | "import React from 'react'; 27 | const { 28 | useState 29 | } = React; 30 | const { 31 | useEffect 32 | } = React;" 33 | `; 34 | 35 | exports[`transforms React.createElement calls 1`] = ` 36 | "import React from 'react'; 37 | const _createElement = React.createElement; 38 | 39 | const element = _createElement('div');" 40 | `; 41 | -------------------------------------------------------------------------------- /tests/index.test.js: -------------------------------------------------------------------------------- 1 | const babel = require('@babel/core') 2 | const plugin = require('../index.js') 3 | 4 | function transform(input) { 5 | return babel.transformSync(input, { 6 | plugins: [plugin()] 7 | }).code 8 | } 9 | 10 | it('creates local variables for named imports', () => { 11 | const input = ` 12 | import React, {useState, useEffect} from 'react'; 13 | ` 14 | const output = transform(input) 15 | expect(output).toMatchSnapshot() 16 | }) 17 | 18 | it('supports import aliasing', () => { 19 | const input = ` 20 | import React, {useState as foo} from 'react'; 21 | ` 22 | const output = transform(input) 23 | expect(output).toMatchSnapshot() 24 | }) 25 | 26 | it('supports import with no default import', () => { 27 | const input = ` 28 | import {useState} from 'react'; 29 | ` 30 | const output = transform(input) 31 | expect(output).toMatchSnapshot() 32 | }) 33 | 34 | it('supports mixed imports', () => { 35 | const input = ` 36 | import React, {useEffect} from 'react' 37 | import {useState} from 'react'; 38 | ` 39 | const output = transform(input) 40 | expect(output).toMatchSnapshot() 41 | }) 42 | 43 | it('transforms React.createElement calls', () => { 44 | const input = ` 45 | import React from 'react'; 46 | const element = React.createElement('div'); 47 | ` 48 | const output = transform(input) 49 | expect(output).toMatchSnapshot() 50 | }) 51 | --------------------------------------------------------------------------------