├── .npmrc ├── cjs ├── package.json └── index.js ├── test ├── package.json ├── input.jsx └── output.js ├── .gitignore ├── babel.config.json ├── .npmignore ├── package.json ├── README.md └── esm └── index.js /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /cjs/package.json: -------------------------------------------------------------------------------- 1 | {"type":"commonjs"} -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | {"type":"commonjs"} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .nyc_output 3 | coverage/ 4 | node_modules/ 5 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "./esm/index.js" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .nyc_output 3 | .eslintrc.json 4 | .travis.yml 5 | coverage/ 6 | node_modules/ 7 | rollup/ 8 | test/ 9 | -------------------------------------------------------------------------------- /test/input.jsx: -------------------------------------------------------------------------------- 1 | /** @jsx a.b.c.d.createElement */ 2 | /** @jsxFrag a.b.c.d.Fragment */ 3 | /** @jsxInterpolation a.b.c.d.interpolation */ 4 | 5 | function Component({ className, props, others }) { 6 | return ( 7 | <> 8 |
9 | <> 10 | 11 | OK 12 | 13 |
15 | 16 | {[

]} 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /test/output.js: -------------------------------------------------------------------------------- 1 | var _token = {}, 2 | _token2 = {}; 3 | 4 | /** @jsx a.b.c.d.createElement */ 5 | 6 | /** @jsxFrag a.b.c.d.Fragment */ 7 | 8 | /** @jsxInterpolation a.b.c.d.interpolation */ 9 | function Component({ 10 | className, 11 | props, 12 | others 13 | }) { 14 | return a.b.c.d.createElement(a.b.c.d.Fragment, { 15 | __token: _token 16 | }, a.b.c.d.createElement("div", { 17 | id: "my-div", 18 | className: a.b.c.d.interpolation(className) 19 | }, a.b.c.d.createElement(a.b.c.d.Fragment, null, a.b.c.d.createElement("span", null), "OK"), a.b.c.d.createElement("p", { 20 | color: a.b.c.d.interpolation(color), 21 | label: "f\"o", 22 | hidden: a.b.c.d.interpolation(Math.random() < .5) 23 | })), a.b.c.d.createElement(Component, a.b.c.d.interpolation({ 24 | id: "my-component", 25 | className: className, 26 | ...props, 27 | ...others 28 | }), a.b.c.d.interpolation([a.b.c.d.createElement("p", { 29 | __token: _token2, 30 | a: "a", 31 | b: a.b.c.d.interpolation(Math.random() < .5) 32 | })]))); 33 | } 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ungap/babel-plugin-transform-hinted-jsx", 3 | "version": "0.1.0", 4 | "main": "./cjs/index.js", 5 | "scripts": { 6 | "build": "npm run cjs && npm run test", 7 | "cjs": "ascjs --no-default esm cjs", 8 | "test": "babel test/input.jsx -o test/output.js" 9 | }, 10 | "keywords": [ 11 | "JSX", 12 | "performance", 13 | "hints", 14 | "interpolations" 15 | ], 16 | "author": "Andrea Giammarchi", 17 | "license": "ISC", 18 | "module": "./esm/index.js", 19 | "type": "module", 20 | "exports": { 21 | ".": { 22 | "import": "./esm/index.js", 23 | "default": "./cjs/index.js" 24 | }, 25 | "./package.json": "./package.json" 26 | }, 27 | "dependencies": { 28 | "@babel/plugin-transform-react-jsx": "^7.19.0" 29 | }, 30 | "peerDependencies": { 31 | "@babel/core": "^7.0.0" 32 | }, 33 | "devDependencies": { 34 | "@babel/cli": "^7.19.3", 35 | "@babel/core": "^7.19.3", 36 | "ascjs": "^5.0.1" 37 | }, 38 | "description": "A JSX transformer with extra hints around interpolations and outer templates", 39 | "repository": { 40 | "type": "git", 41 | "url": "git+https://github.com/ungap/babel-plugin-transform-hinted-jsx.git" 42 | }, 43 | "bugs": { 44 | "url": "https://github.com/ungap/babel-plugin-transform-hinted-jsx/issues" 45 | }, 46 | "homepage": "https://github.com/ungap/babel-plugin-transform-hinted-jsx#readme" 47 | } 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @ungap/babel-plugin-transform-hinted-jsx 2 | 3 | This plugin is [a follow up of this post](https://webreflection.medium.com/jsx-is-inefficient-by-default-but-d1122c992399) and it can be used in place of [@babel/plugin-transform-react-jsx](https://www.npmjs.com/package/@babel/plugin-transform-react-jsx). 4 | 5 | A huge thanks to [Nicolò Ribaudo](https://twitter.com/NicoloRibaudo) for helping out. 6 | 7 | ### babel.config.json 8 | 9 | ```json 10 | { 11 | "plugins": [ 12 | ["@ungap/babel-plugin-transform-hinted-jsx"] 13 | ] 14 | } 15 | ``` 16 | 17 | ### npm install 18 | 19 | ```sh 20 | npm i --save-dev @babel/cli 21 | npm i --save-dev @babel/core 22 | npm i --save-dev @ungap/plugin-transform-hinted-jsx 23 | ``` 24 | 25 | ### What is it / How to use it 26 | 27 | This produces a slightly different *JSX* transform. 28 | 29 | ```js 30 | const div = ( 31 |

32 |

33 | {

} 34 |

35 | ); 36 | 37 | // becomes 38 | var _token = {}, 39 | _token2 = {}; 40 | 41 | const div = React.createElement( 42 | "div", 43 | {__token: _token}, 44 | React.createElement( 45 | "p", 46 | { 47 | className: "static", 48 | runtime: React.interpolation('prop') 49 | } 50 | ), 51 | React.interpolation( 52 | React.createElement( 53 | "p", 54 | {__token: _token2} 55 | ) 56 | ) 57 | ); 58 | ``` 59 | 60 | ### How to hint interpolations 61 | 62 | ```js 63 | /** @jsx your.createElement */ 64 | /** @jsxFrag your.Fragment */ 65 | /** @jsxInterpolation your.interpolation */ 66 | ``` 67 | -------------------------------------------------------------------------------- /esm/index.js: -------------------------------------------------------------------------------- 1 | import _pluginJSX from "@babel/plugin-transform-react-jsx"; 2 | 3 | // _pluginJSX.default when using native ESM; 4 | // _pluginJSX when using the version compiled by ascjs. 5 | const pluginJSX = _pluginJSX.default || _pluginJSX; 6 | 7 | const JSX_ANNOTATION_REGEX = /\*?\s*@jsx\s+([^\s]+)/; 8 | const JSX_FRAG_ANNOTATION_REGEX = /\*?\s*@jsxFrag\s+([^\s]+)/; 9 | const JSX_INTERPOLATION_ANNOTATION_REGEX = /\*?\s*@jsxInterpolation\s+([^\s]+)/; 10 | 11 | export default ({types: t}, options) => { 12 | let pragma = '', pragmaFrag = '', pragmaPrefix = '', pragmaInterplt = ''; 13 | 14 | const injectedContainers = new WeakSet; 15 | 16 | const getCalleeName = ({object, property, name}) => { 17 | if (name) return name; 18 | const whole = [property.name]; 19 | while (object.object) { 20 | whole.push(object.property.name); 21 | object = object.object; 22 | } 23 | whole.push(object.name); 24 | return whole.reverse().join('.'); 25 | }; 26 | 27 | const interpolation = () => ( 28 | pragmaInterplt || 29 | ((pragmaPrefix || 'React') + '.interpolation')) 30 | ; 31 | 32 | const interpolation2ME = () => toMemberExpression( 33 | interpolation(), 34 | 'identifier', 35 | 'memberExpression' 36 | ); 37 | 38 | const fragment2ME = () => toMemberExpression( 39 | pragmaFrag || 'React.Fragment', 40 | 'jsxIdentifier', 41 | 'jsxMemberExpression' 42 | ); 43 | 44 | const toMemberExpression = (id, identifier, memberExpression) => ( 45 | id.split('.') 46 | .map(name => t[identifier](name)) 47 | .reduce( 48 | (object, property) => t[memberExpression](object, property) 49 | ) 50 | ); 51 | 52 | // Force the JSX plugin to use object spread instead of _extends. 53 | options.useSpread = true; 54 | 55 | return { 56 | inherits: pluginJSX, 57 | visitor: { 58 | // intercepts comments directive to name pragma and utils 59 | Program: { 60 | enter(_, state) { 61 | const {file: {ast: {comments}}} = state; 62 | if (comments) { 63 | for (const comment of comments) { 64 | if (JSX_ANNOTATION_REGEX.test(comment.value)) { 65 | pragma = RegExp.$1; 66 | [pragmaPrefix] = pragma.split('.'); 67 | } 68 | else if (JSX_FRAG_ANNOTATION_REGEX.test(comment.value)) 69 | pragmaFrag = RegExp.$1; 70 | else if (JSX_INTERPOLATION_ANNOTATION_REGEX.test(comment.value)) 71 | pragmaInterplt = RegExp.$1; 72 | } 73 | } 74 | } 75 | }, 76 | // add a unique token to outer most JSX templates 77 | JSXElement(path) { 78 | if (path.parentPath.isJSXElement()) return; 79 | 80 | const tokenId = path.scope.generateUidIdentifier("token"); 81 | path.scope.getProgramParent().push({ 82 | id: tokenId, 83 | init: t.objectExpression([]) 84 | }); 85 | 86 | const expr = t.jsxExpressionContainer(t.cloneNode(tokenId)); 87 | injectedContainers.add(expr); 88 | 89 | path.node.openingElement.attributes.unshift( 90 | t.jsxAttribute( 91 | t.jsxIdentifier("__token"), 92 | expr 93 | ) 94 | ); 95 | }, 96 | // augment interpolations with an explicit call 97 | // to its React.interpolation equivalent 98 | JSXExpressionContainer({node, parentPath}) { 99 | if ( 100 | injectedContainers.has(node) || 101 | ( 102 | parentPath.isJSXAttribute() && 103 | parentPath.parent.attributes.some( 104 | attr => t.isJSXSpreadAttribute(attr) 105 | ) 106 | ) 107 | ) return; 108 | 109 | injectedContainers.add(node); 110 | node.expression = t.callExpression( 111 | interpolation2ME(), 112 | [node.expression] 113 | ); 114 | }, 115 | // transform a fragment into a JSXExpressionContainer 116 | // where checks around its top most definition are performed 117 | JSXFragment(path) { 118 | path.replaceWith( 119 | t.jsxElement( 120 | t.jsxOpeningElement( 121 | fragment2ME(), 122 | [] 123 | ), 124 | t.jsxClosingElement( 125 | fragment2ME(), 126 | [] 127 | ), 128 | path.node.children 129 | ) 130 | ) 131 | }, 132 | // makes spread operations around attributes pollute the whole 133 | // attributes handling as dynamic interpolation 134 | SpreadElement(path) { 135 | const {parentPath} = path.parentPath; 136 | if (parentPath && parentPath.isCallExpression()) { 137 | const name = getCalleeName(parentPath.node.callee); 138 | if ( 139 | name === pragma || 140 | name === 'React.createElement' 141 | ) { 142 | const {callee} = path.parentPath.node; 143 | if (callee && getCalleeName(callee) === interpolation()) 144 | return; 145 | path.parentPath.replaceWith( 146 | t.inherits( 147 | t.callExpression( 148 | interpolation2ME(), 149 | [path.parentPath.node] 150 | ), 151 | path.parentPath 152 | ) 153 | ); 154 | } 155 | } 156 | } 157 | } 158 | }; 159 | }; 160 | -------------------------------------------------------------------------------- /cjs/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const _pluginJSX = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require("@babel/plugin-transform-react-jsx")); 3 | 4 | // _pluginJSX.default when using native ESM; 5 | // _pluginJSX when using the version compiled by ascjs. 6 | const pluginJSX = _pluginJSX.default || _pluginJSX; 7 | 8 | const JSX_ANNOTATION_REGEX = /\*?\s*@jsx\s+([^\s]+)/; 9 | const JSX_FRAG_ANNOTATION_REGEX = /\*?\s*@jsxFrag\s+([^\s]+)/; 10 | const JSX_INTERPOLATION_ANNOTATION_REGEX = /\*?\s*@jsxInterpolation\s+([^\s]+)/; 11 | 12 | module.exports = ({types: t}, options) => { 13 | let pragma = '', pragmaFrag = '', pragmaPrefix = '', pragmaInterplt = ''; 14 | 15 | const injectedContainers = new WeakSet; 16 | 17 | const getCalleeName = ({object, property, name}) => { 18 | if (name) return name; 19 | const whole = [property.name]; 20 | while (object.object) { 21 | whole.push(object.property.name); 22 | object = object.object; 23 | } 24 | whole.push(object.name); 25 | return whole.reverse().join('.'); 26 | }; 27 | 28 | const interpolation = () => ( 29 | pragmaInterplt || 30 | ((pragmaPrefix || 'React') + '.interpolation')) 31 | ; 32 | 33 | const interpolation2ME = () => toMemberExpression( 34 | interpolation(), 35 | 'identifier', 36 | 'memberExpression' 37 | ); 38 | 39 | const fragment2ME = () => toMemberExpression( 40 | pragmaFrag || 'React.Fragment', 41 | 'jsxIdentifier', 42 | 'jsxMemberExpression' 43 | ); 44 | 45 | const toMemberExpression = (id, identifier, memberExpression) => ( 46 | id.split('.') 47 | .map(name => t[identifier](name)) 48 | .reduce( 49 | (object, property) => t[memberExpression](object, property) 50 | ) 51 | ); 52 | 53 | // Force the JSX plugin to use object spread instead of _extends. 54 | options.useSpread = true; 55 | 56 | return { 57 | inherits: pluginJSX, 58 | visitor: { 59 | // intercepts comments directive to name pragma and utils 60 | Program: { 61 | enter(_, state) { 62 | const {file: {ast: {comments}}} = state; 63 | if (comments) { 64 | for (const comment of comments) { 65 | if (JSX_ANNOTATION_REGEX.test(comment.value)) { 66 | pragma = RegExp.$1; 67 | [pragmaPrefix] = pragma.split('.'); 68 | } 69 | else if (JSX_FRAG_ANNOTATION_REGEX.test(comment.value)) 70 | pragmaFrag = RegExp.$1; 71 | else if (JSX_INTERPOLATION_ANNOTATION_REGEX.test(comment.value)) 72 | pragmaInterplt = RegExp.$1; 73 | } 74 | } 75 | } 76 | }, 77 | // add a unique token to outer most JSX templates 78 | JSXElement(path) { 79 | if (path.parentPath.isJSXElement()) return; 80 | 81 | const tokenId = path.scope.generateUidIdentifier("token"); 82 | path.scope.getProgramParent().push({ 83 | id: tokenId, 84 | init: t.objectExpression([]) 85 | }); 86 | 87 | const expr = t.jsxExpressionContainer(t.cloneNode(tokenId)); 88 | injectedContainers.add(expr); 89 | 90 | path.node.openingElement.attributes.unshift( 91 | t.jsxAttribute( 92 | t.jsxIdentifier("__token"), 93 | expr 94 | ) 95 | ); 96 | }, 97 | // augment interpolations with an explicit call 98 | // to its React.interpolation equivalent 99 | JSXExpressionContainer({node, parentPath}) { 100 | if ( 101 | injectedContainers.has(node) || 102 | ( 103 | parentPath.isJSXAttribute() && 104 | parentPath.parent.attributes.some( 105 | attr => t.isJSXSpreadAttribute(attr) 106 | ) 107 | ) 108 | ) return; 109 | 110 | injectedContainers.add(node); 111 | node.expression = t.callExpression( 112 | interpolation2ME(), 113 | [node.expression] 114 | ); 115 | }, 116 | // transform a fragment into a JSXExpressionContainer 117 | // where checks around its top most definition are performed 118 | JSXFragment(path) { 119 | path.replaceWith( 120 | t.jsxElement( 121 | t.jsxOpeningElement( 122 | fragment2ME(), 123 | [] 124 | ), 125 | t.jsxClosingElement( 126 | fragment2ME(), 127 | [] 128 | ), 129 | path.node.children 130 | ) 131 | ) 132 | }, 133 | // makes spread operations around attributes pollute the whole 134 | // attributes handling as dynamic interpolation 135 | SpreadElement(path) { 136 | const {parentPath} = path.parentPath; 137 | if (parentPath && parentPath.isCallExpression()) { 138 | const name = getCalleeName(parentPath.node.callee); 139 | if ( 140 | name === pragma || 141 | name === 'React.createElement' 142 | ) { 143 | const {callee} = path.parentPath.node; 144 | if (callee && getCalleeName(callee) === interpolation()) 145 | return; 146 | path.parentPath.replaceWith( 147 | t.inherits( 148 | t.callExpression( 149 | interpolation2ME(), 150 | [path.parentPath.node] 151 | ), 152 | path.parentPath 153 | ) 154 | ); 155 | } 156 | } 157 | } 158 | } 159 | }; 160 | }; 161 | --------------------------------------------------------------------------------