diff --git a/packages/babel-plugin-transform-react-pure-annotations/.npmignore b/packages/babel-plugin-transform-react-pure-annotations/.npmignore new file mode 100644 index 0000000000..f980694583 --- /dev/null +++ b/packages/babel-plugin-transform-react-pure-annotations/.npmignore @@ -0,0 +1,3 @@ +src +test +*.log diff --git a/packages/babel-plugin-transform-react-pure-annotations/package.json b/packages/babel-plugin-transform-react-pure-annotations/package.json new file mode 100644 index 0000000000..d6a04c9163 --- /dev/null +++ b/packages/babel-plugin-transform-react-pure-annotations/package.json @@ -0,0 +1,25 @@ +{ + "name": "@babel/plugin-transform-react-pure-annotations", + "version": "7.9.4", + "description": "Mark top-level React method calls as pure for tree shaking", + "repository": "https://github.com/babel/babel/tree/master/packages/babel-plugin-transform-react-pure", + "license": "MIT", + "publishConfig": { + "access": "public" + }, + "main": "lib/index.js", + "keywords": [ + "babel-plugin" + ], + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.8.3", + "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + }, + "devDependencies": { + "@babel/core": "^7.9.0", + "@babel/helper-plugin-test-runner": "^7.8.3" + } +} diff --git a/packages/babel-plugin-transform-react-pure-annotations/src/index.js b/packages/babel-plugin-transform-react-pure-annotations/src/index.js new file mode 100644 index 0000000000..14ed44c127 --- /dev/null +++ b/packages/babel-plugin-transform-react-pure-annotations/src/index.js @@ -0,0 +1,79 @@ +import { declare } from "@babel/helper-plugin-utils"; +import annotateAsPure from "@babel/helper-annotate-as-pure"; +import { types as t } from "@babel/core"; + +// Mapping of React top-level methods that are pure. +// This plugin adds a /*#__PURE__#/ annotation to calls to these methods, +// so that terser and other minifiers can safely remove them during dead +// code elimination. +// See https://reactjs.org/docs/react-api.html +const PURE_CALLS = new Map([ + [ + "react", + [ + "cloneElement", + "createElement", + "createFactory", + "createRef", + "forwardRef", + "isValidElement", + "memo", + "lazy", + ], + ], + ["react-dom", ["createPortal"]], +]); + +export default declare(api => { + api.assertVersion(7); + + return { + name: "transform-react-pure-annotations", + visitor: { + CallExpression(path) { + if (isReactCall(path)) { + annotateAsPure(path); + } + }, + }, + }; +}); + +function isReactCall(path) { + // If the callee is not a member expression, then check if it matches + // a named import, e.g. `import {forwardRef} from 'react'`. + if (!t.isMemberExpression(path.node.callee)) { + const callee = path.get("callee"); + for (const [module, methods] of PURE_CALLS) { + for (const method of methods) { + if (callee.referencesImport(module, method)) { + return true; + } + } + } + + return false; + } + + // Otherwise, check if the member expression's object matches + // a default import (`import React from 'react'`) or namespace + // import (`import * as React from 'react'), and check if the + // property matches one of the pure methods. + for (const [module, methods] of PURE_CALLS) { + const object = path.get("callee.object"); + if ( + object.referencesImport(module, "default") || + object.referencesImport(module, "*") + ) { + for (const method of methods) { + if (t.isIdentifier(path.node.callee.property, { name: method })) { + return true; + } + } + + return false; + } + } + + return false; +} diff --git a/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react-dom/createPortal/input.js b/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react-dom/createPortal/input.js new file mode 100644 index 0000000000..fa90a38427 --- /dev/null +++ b/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react-dom/createPortal/input.js @@ -0,0 +1,4 @@ +import * as React from 'react'; +import ReactDOM from 'react-dom'; + +const Portal = ReactDOM.createPortal(React.createElement('div'), document.getElementById('test')); diff --git a/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react-dom/createPortal/options.json b/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react-dom/createPortal/options.json new file mode 100644 index 0000000000..d5601dabff --- /dev/null +++ b/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react-dom/createPortal/options.json @@ -0,0 +1,4 @@ +{ + "sourceType": "module", + "plugins": ["transform-react-pure-annotations"] +} diff --git a/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react-dom/createPortal/output.mjs b/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react-dom/createPortal/output.mjs new file mode 100644 index 0000000000..5ad8477477 --- /dev/null +++ b/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react-dom/createPortal/output.mjs @@ -0,0 +1,3 @@ +import * as React from 'react'; +import ReactDOM from 'react-dom'; +const Portal = /*#__PURE__*/ReactDOM.createPortal( /*#__PURE__*/React.createElement('div'), document.getElementById('test')); diff --git a/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/cloneElement/input.js b/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/cloneElement/input.js new file mode 100644 index 0000000000..2e023247d5 --- /dev/null +++ b/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/cloneElement/input.js @@ -0,0 +1,3 @@ +import React from 'react'; + +React.cloneElement(React.createElement('div')); diff --git a/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/cloneElement/options.json b/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/cloneElement/options.json new file mode 100644 index 0000000000..d5601dabff --- /dev/null +++ b/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/cloneElement/options.json @@ -0,0 +1,4 @@ +{ + "sourceType": "module", + "plugins": ["transform-react-pure-annotations"] +} diff --git a/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/cloneElement/output.mjs b/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/cloneElement/output.mjs new file mode 100644 index 0000000000..c05fd3e356 --- /dev/null +++ b/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/cloneElement/output.mjs @@ -0,0 +1,4 @@ +import React from 'react'; + +/*#__PURE__*/ +React.cloneElement( /*#__PURE__*/React.createElement('div')); diff --git a/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/createElement/input.js b/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/createElement/input.js new file mode 100644 index 0000000000..70a1f8f2df --- /dev/null +++ b/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/createElement/input.js @@ -0,0 +1,3 @@ +import React from 'react'; + +React.createElement('div'); diff --git a/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/createElement/options.json b/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/createElement/options.json new file mode 100644 index 0000000000..d5601dabff --- /dev/null +++ b/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/createElement/options.json @@ -0,0 +1,4 @@ +{ + "sourceType": "module", + "plugins": ["transform-react-pure-annotations"] +} diff --git a/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/createElement/output.mjs b/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/createElement/output.mjs new file mode 100644 index 0000000000..f65cf5f296 --- /dev/null +++ b/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/createElement/output.mjs @@ -0,0 +1,4 @@ +import React from 'react'; + +/*#__PURE__*/ +React.createElement('div'); diff --git a/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/createFactory/input.js b/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/createFactory/input.js new file mode 100644 index 0000000000..38bb1d1b99 --- /dev/null +++ b/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/createFactory/input.js @@ -0,0 +1,3 @@ +import {createFactory} from 'react'; + +const div = createFactory('div'); diff --git a/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/createFactory/options.json b/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/createFactory/options.json new file mode 100644 index 0000000000..d5601dabff --- /dev/null +++ b/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/createFactory/options.json @@ -0,0 +1,4 @@ +{ + "sourceType": "module", + "plugins": ["transform-react-pure-annotations"] +} diff --git a/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/createFactory/output.mjs b/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/createFactory/output.mjs new file mode 100644 index 0000000000..e6762916a0 --- /dev/null +++ b/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/createFactory/output.mjs @@ -0,0 +1,2 @@ +import { createFactory } from 'react'; +const div = /*#__PURE__*/createFactory('div'); diff --git a/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/createRef/input.js b/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/createRef/input.js new file mode 100644 index 0000000000..4ef3d45f6d --- /dev/null +++ b/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/createRef/input.js @@ -0,0 +1,3 @@ +import React from 'react'; + +React.createRef(); diff --git a/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/createRef/options.json b/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/createRef/options.json new file mode 100644 index 0000000000..d5601dabff --- /dev/null +++ b/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/createRef/options.json @@ -0,0 +1,4 @@ +{ + "sourceType": "module", + "plugins": ["transform-react-pure-annotations"] +} diff --git a/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/createRef/output.mjs b/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/createRef/output.mjs new file mode 100644 index 0000000000..3fb443880a --- /dev/null +++ b/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/createRef/output.mjs @@ -0,0 +1,4 @@ +import React from 'react'; + +/*#__PURE__*/ +React.createRef(); diff --git a/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/forwardRef/input.js b/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/forwardRef/input.js new file mode 100644 index 0000000000..2002155083 --- /dev/null +++ b/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/forwardRef/input.js @@ -0,0 +1,3 @@ +import {forwardRef} from 'react'; + +const Comp = forwardRef((props, ref) => null); diff --git a/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/forwardRef/options.json b/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/forwardRef/options.json new file mode 100644 index 0000000000..d5601dabff --- /dev/null +++ b/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/forwardRef/options.json @@ -0,0 +1,4 @@ +{ + "sourceType": "module", + "plugins": ["transform-react-pure-annotations"] +} diff --git a/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/forwardRef/output.mjs b/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/forwardRef/output.mjs new file mode 100644 index 0000000000..14378e97d6 --- /dev/null +++ b/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/forwardRef/output.mjs @@ -0,0 +1,2 @@ +import { forwardRef } from 'react'; +const Comp = /*#__PURE__*/forwardRef((props, ref) => null); diff --git a/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/isValidElement/input.js b/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/isValidElement/input.js new file mode 100644 index 0000000000..61175d7878 --- /dev/null +++ b/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/isValidElement/input.js @@ -0,0 +1,3 @@ +import React from 'react'; + +const isElement = React.isValidElement(React.createElement('div')); diff --git a/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/isValidElement/options.json b/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/isValidElement/options.json new file mode 100644 index 0000000000..d5601dabff --- /dev/null +++ b/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/isValidElement/options.json @@ -0,0 +1,4 @@ +{ + "sourceType": "module", + "plugins": ["transform-react-pure-annotations"] +} diff --git a/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/isValidElement/output.mjs b/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/isValidElement/output.mjs new file mode 100644 index 0000000000..b8a4817191 --- /dev/null +++ b/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/isValidElement/output.mjs @@ -0,0 +1,2 @@ +import React from 'react'; +const isElement = /*#__PURE__*/React.isValidElement( /*#__PURE__*/React.createElement('div')); \ No newline at end of file diff --git a/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/lazy/input.js b/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/lazy/input.js new file mode 100644 index 0000000000..1f2379150c --- /dev/null +++ b/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/lazy/input.js @@ -0,0 +1,3 @@ +import React from 'react'; + +const SomeComponent = React.lazy(() => import('./SomeComponent')); diff --git a/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/lazy/options.json b/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/lazy/options.json new file mode 100644 index 0000000000..d5601dabff --- /dev/null +++ b/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/lazy/options.json @@ -0,0 +1,4 @@ +{ + "sourceType": "module", + "plugins": ["transform-react-pure-annotations"] +} diff --git a/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/lazy/output.mjs b/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/lazy/output.mjs new file mode 100644 index 0000000000..5c6cf13271 --- /dev/null +++ b/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/lazy/output.mjs @@ -0,0 +1,2 @@ +import React from 'react'; +const SomeComponent = /*#__PURE__*/React.lazy(() => import('./SomeComponent')); diff --git a/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/memo/input.js b/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/memo/input.js new file mode 100644 index 0000000000..c53d22c31a --- /dev/null +++ b/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/memo/input.js @@ -0,0 +1,3 @@ +import React from 'react'; + +const Comp = React.memo((props) => null); diff --git a/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/memo/options.json b/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/memo/options.json new file mode 100644 index 0000000000..d5601dabff --- /dev/null +++ b/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/memo/options.json @@ -0,0 +1,4 @@ +{ + "sourceType": "module", + "plugins": ["transform-react-pure-annotations"] +} diff --git a/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/memo/output.mjs b/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/memo/output.mjs new file mode 100644 index 0000000000..9364c2d612 --- /dev/null +++ b/packages/babel-plugin-transform-react-pure-annotations/test/fixtures/react/memo/output.mjs @@ -0,0 +1,2 @@ +import React from 'react'; +const Comp = /*#__PURE__*/React.memo(props => null); diff --git a/packages/babel-plugin-transform-react-pure-annotations/test/index.js b/packages/babel-plugin-transform-react-pure-annotations/test/index.js new file mode 100644 index 0000000000..1b534b8fc6 --- /dev/null +++ b/packages/babel-plugin-transform-react-pure-annotations/test/index.js @@ -0,0 +1,3 @@ +import runner from "@babel/helper-plugin-test-runner"; + +runner(__dirname); diff --git a/packages/babel-preset-react/package.json b/packages/babel-preset-react/package.json index 83ceffe03d..5639a6a1af 100644 --- a/packages/babel-preset-react/package.json +++ b/packages/babel-preset-react/package.json @@ -16,7 +16,8 @@ "@babel/plugin-transform-react-jsx": "^7.9.4", "@babel/plugin-transform-react-jsx-development": "^7.9.0", "@babel/plugin-transform-react-jsx-self": "^7.9.0", - "@babel/plugin-transform-react-jsx-source": "^7.9.0" + "@babel/plugin-transform-react-jsx-source": "^7.9.0", + "@babel/plugin-transform-react-pure-annotations": "^7.9.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" diff --git a/packages/babel-preset-react/src/index.js b/packages/babel-preset-react/src/index.js index 40f3e0e635..c57cc52ff5 100644 --- a/packages/babel-preset-react/src/index.js +++ b/packages/babel-preset-react/src/index.js @@ -4,6 +4,7 @@ import transformReactJSXDevelopment from "@babel/plugin-transform-react-jsx-deve import transformReactDisplayName from "@babel/plugin-transform-react-display-name"; import transformReactJSXSource from "@babel/plugin-transform-react-jsx-source"; import transformReactJSXSelf from "@babel/plugin-transform-react-jsx-self"; +import transformReactPure from "@babel/plugin-transform-react-pure-annotations"; export default declare((api, opts) => { api.assertVersion(7); @@ -55,6 +56,7 @@ export default declare((api, opts) => { }, ], transformReactDisplayName, + pure !== false && transformReactPure, development && runtime === "classic" && transformReactJSXSource, development && runtime === "classic" && transformReactJSXSelf, diff --git a/packages/babel-preset-react/test/fixtures/preset-options/pure-false/input.js b/packages/babel-preset-react/test/fixtures/preset-options/pure-false/input.js new file mode 100644 index 0000000000..2002155083 --- /dev/null +++ b/packages/babel-preset-react/test/fixtures/preset-options/pure-false/input.js @@ -0,0 +1,3 @@ +import {forwardRef} from 'react'; + +const Comp = forwardRef((props, ref) => null); diff --git a/packages/babel-preset-react/test/fixtures/preset-options/pure-false/options.json b/packages/babel-preset-react/test/fixtures/preset-options/pure-false/options.json new file mode 100644 index 0000000000..91c500e74d --- /dev/null +++ b/packages/babel-preset-react/test/fixtures/preset-options/pure-false/options.json @@ -0,0 +1,4 @@ +{ + "sourceType": "module", + "presets": [["react", {"pure": false}]] +} diff --git a/packages/babel-preset-react/test/fixtures/preset-options/pure-false/output.mjs b/packages/babel-preset-react/test/fixtures/preset-options/pure-false/output.mjs new file mode 100644 index 0000000000..10ae240e0d --- /dev/null +++ b/packages/babel-preset-react/test/fixtures/preset-options/pure-false/output.mjs @@ -0,0 +1,2 @@ +import { forwardRef } from 'react'; +const Comp = forwardRef((props, ref) => null); diff --git a/packages/babel-preset-react/test/fixtures/preset-options/pure/input.js b/packages/babel-preset-react/test/fixtures/preset-options/pure/input.js new file mode 100644 index 0000000000..2002155083 --- /dev/null +++ b/packages/babel-preset-react/test/fixtures/preset-options/pure/input.js @@ -0,0 +1,3 @@ +import {forwardRef} from 'react'; + +const Comp = forwardRef((props, ref) => null); diff --git a/packages/babel-preset-react/test/fixtures/preset-options/pure/options.json b/packages/babel-preset-react/test/fixtures/preset-options/pure/options.json new file mode 100644 index 0000000000..f3f20277d7 --- /dev/null +++ b/packages/babel-preset-react/test/fixtures/preset-options/pure/options.json @@ -0,0 +1,4 @@ +{ + "sourceType": "module", + "presets": ["react"] +} diff --git a/packages/babel-preset-react/test/fixtures/preset-options/pure/output.mjs b/packages/babel-preset-react/test/fixtures/preset-options/pure/output.mjs new file mode 100644 index 0000000000..14378e97d6 --- /dev/null +++ b/packages/babel-preset-react/test/fixtures/preset-options/pure/output.mjs @@ -0,0 +1,2 @@ +import { forwardRef } from 'react'; +const Comp = /*#__PURE__*/forwardRef((props, ref) => null);