├── .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 |
14 |
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 |
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 |
--------------------------------------------------------------------------------