Skip to content

Commit 93124af

Browse files
authored
Merge branch 'master' into master
2 parents 8d76204 + c7eb893 commit 93124af

File tree

6 files changed

+340
-64
lines changed

6 files changed

+340
-64
lines changed

packages/babel-plugin-htm/README.md

Lines changed: 80 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,16 @@ A Babel plugin that compiles [htm] syntax to hyperscript, React.createElement, o
44

55
## Usage
66

7-
Basic usage:
7+
In your Babel configuration (`.babelrc`, `babel.config.js`, `"babel"` field in package.json, etc), add the plugin:
88

99
```js
10-
[
11-
["htm", {
12-
"pragma": "React.createElement"
13-
}]
14-
]
10+
{
11+
"plugins": [
12+
["htm", {
13+
"pragma": "React.createElement"
14+
}]
15+
]
16+
}
1517
```
1618

1719
```js
@@ -41,6 +43,78 @@ By default, `babel-plugin-htm` will process all Tagged Templates with a tag func
4143
]}
4244
```
4345

46+
### `import=false` _(experimental)_
47+
48+
Auto-import the pragma function, off by default.
49+
50+
#### `false` (default)
51+
52+
Don't auto-import anything.
53+
54+
#### `String`
55+
56+
Import the `pragma` like `import {<pragma>} from '<import>'`.
57+
58+
With Babel config:
59+
```js
60+
"plugins": [
61+
["babel-plugin-htm", {
62+
"tag": "$$html",
63+
"import": "preact"
64+
}]
65+
]
66+
```
67+
68+
```js
69+
import { html as $$html } from 'htm/preact';
70+
71+
export default $$html`<div id="foo">hello ${you}</div>`
72+
```
73+
74+
The above will produce files that look like:
75+
76+
```js
77+
import { h } from 'preact';
78+
import { html as $$html } from 'htm/preact';
79+
80+
export default h("div", { id: "foo" }, "hello ", you)
81+
```
82+
83+
#### `{module: String, export: String}`
84+
85+
Import the `pragma` like `import {<import.export> as <pragma>} from '<import.module>'`.
86+
87+
With Babel config:
88+
```js
89+
"plugins": [
90+
["babel-plugin-htm", {
91+
"pragma": "React.createElement",
92+
"tag": "$$html",
93+
"import": {
94+
// the module to import:
95+
"module": "react",
96+
// a named export to use from that module:
97+
"export": "default"
98+
}
99+
}]
100+
]
101+
```
102+
103+
```js
104+
import { html as $$html } from 'htm/react';
105+
106+
export default $$html`<div id="foo">hello ${you}</div>`
107+
```
108+
109+
The above will produce files that look like:
110+
111+
```js
112+
import React from 'react';
113+
import { html as $$html } from 'htm/react';
114+
115+
export default React.createElement("div", { id: "foo" }, "hello ", you)
116+
```
117+
44118
### `useBuiltIns=false`
45119

46120
`babel-plugin-htm` transforms prop spreads (`<a ...${b}>`) into `Object.assign()` calls. For browser support reasons, Babel's standard `_extends` helper is used by default. To use native `Object.assign` directly, pass `{useBuiltIns:true}`.

packages/babel-plugin-htm/index.mjs

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,42 @@ import { build, treeify } from '../../src/build.mjs';
55
* @param {object} options
66
* @param {string} [options.pragma=h] JSX/hyperscript pragma.
77
* @param {string} [options.tag=html] The tagged template "tag" function name to process.
8+
* @param {string | boolean | object} [options.import=false] Import the tag automatically
89
* @param {boolean} [options.monomorphic=false] Output monomorphic inline objects instead of using String literals.
910
* @param {boolean} [options.useBuiltIns=false] Use the native Object.assign instead of trying to polyfill it.
1011
* @param {boolean} [options.useNativeSpread=false] Use the native { ...a, ...b } syntax for prop spreads.
1112
* @param {boolean} [options.variableArity=true] If `false`, always passes exactly 3 arguments to the pragma function.
1213
*/
1314
export default function htmBabelPlugin({ types: t }, options = {}) {
14-
const pragma = options.pragma===false ? false : dottedIdentifier(options.pragma || 'h');
15+
const pragmaString = options.pragma===false ? false : options.pragma || 'h';
16+
const pragma = pragmaString===false ? false : dottedIdentifier(pragmaString);
1517
const useBuiltIns = options.useBuiltIns;
1618
const useNativeSpread = options.useNativeSpread;
1719
const inlineVNodes = options.monomorphic || pragma===false;
20+
const importDeclaration = pragmaImport(options.import || false);
21+
22+
function pragmaImport(imp) {
23+
if (pragmaString === false || imp === false) {
24+
return null;
25+
}
26+
const pragmaRoot = t.identifier(pragmaString.split('.')[0]);
27+
const { module, export: export_ } = typeof imp !== 'string' ? imp : {
28+
module: imp,
29+
export: null
30+
};
31+
32+
let specifier;
33+
if (export_ === '*') {
34+
specifier = t.importNamespaceSpecifier(pragmaRoot);
35+
}
36+
else if (export_ === 'default') {
37+
specifier = t.importDefaultSpecifier(pragmaRoot);
38+
}
39+
else {
40+
specifier = t.importSpecifier(pragmaRoot, export_ ? t.identifier(export_) : pragmaRoot);
41+
}
42+
return t.importDeclaration([specifier], t.stringLiteral(module));
43+
}
1844

1945
function dottedIdentifier(keypath) {
2046
const path = keypath.split('.');
@@ -69,7 +95,7 @@ export default function htmBabelPlugin({ types: t }, options = {}) {
6995
}
7096
return t.stringLiteral(str);
7197
}
72-
98+
7399
function createVNode(tag, props, children) {
74100
// Never pass children=[[]].
75101
if (children.elements.length === 1 && t.isArrayExpression(children.elements[0]) && children.elements[0].elements.length === 0) {
@@ -94,15 +120,15 @@ export default function htmBabelPlugin({ types: t }, options = {}) {
94120

95121
return t.callExpression(pragma, [tag, props].concat(children));
96122
}
97-
123+
98124
function spreadNode(args, state) {
99125
if (args.length === 0) {
100126
return t.nullLiteral();
101127
}
102128
if (args.length > 0 && t.isNode(args[0])) {
103129
args.unshift({});
104130
}
105-
131+
106132
// 'Object.assign(x)', can be collapsed to 'x'.
107133
if (args.length === 1) {
108134
return propsNode(args[0]);
@@ -124,11 +150,11 @@ export default function htmBabelPlugin({ types: t }, options = {}) {
124150
});
125151
return t.objectExpression(properties);
126152
}
127-
153+
128154
const helper = useBuiltIns ? dottedIdentifier('Object.assign') : state.addHelper('extends');
129155
return t.callExpression(helper, args.map(propsNode));
130156
}
131-
157+
132158
function propsNode(props) {
133159
return t.isNode(props) ? props : t.objectExpression(objectProperties(props));
134160
}
@@ -152,6 +178,13 @@ export default function htmBabelPlugin({ types: t }, options = {}) {
152178
return {
153179
name: 'htm',
154180
visitor: {
181+
Program: {
182+
exit(path, state) {
183+
if (state.get('hasHtm') && importDeclaration) {
184+
path.unshiftContainer('body', importDeclaration);
185+
}
186+
},
187+
},
155188
TaggedTemplateExpression(path, state) {
156189
const tag = path.node.tag.name;
157190
if (htmlName[0]==='/' ? patternStringToRegExp(htmlName).test(tag) : tag === htmlName) {
@@ -163,6 +196,7 @@ export default function htmBabelPlugin({ types: t }, options = {}) {
163196
? transform(tree, state)
164197
: t.arrayExpression(tree.map(root => transform(root, state)));
165198
path.replaceWith(node);
199+
state.set('hasHtm', true);
166200
}
167201
}
168202
}

src/build.mjs

Lines changed: 47 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ const MODE_COMMENT = 4;
88
const MODE_PROP_SET = 5;
99
const MODE_PROP_APPEND = 6;
1010

11-
const TAG_SET = 1;
1211
const CHILD_APPEND = 0;
1312
const CHILD_RECURSE = 2;
14-
const PROPS_ASSIGN = 3;
13+
const TAG_SET = 3;
14+
const PROPS_ASSIGN = 4;
1515
const PROP_SET = MODE_PROP_SET;
1616
const PROP_APPEND = MODE_PROP_APPEND;
1717

@@ -36,30 +36,30 @@ export const treeify = (built, fields) => {
3636
const children = [];
3737

3838
for (let i = 1; i < built.length; i++) {
39-
const field = built[i++];
40-
const value = typeof field === 'number' ? fields[field - 1] : field;
39+
const type = built[i++];
40+
const value = built[i] ? fields[built[i++]-1] : built[++i];
4141

42-
if (built[i] === TAG_SET) {
42+
if (type === TAG_SET) {
4343
tag = value;
4444
}
45-
else if (built[i] === PROPS_ASSIGN) {
45+
else if (type === PROPS_ASSIGN) {
4646
props.push(value);
4747
currentProps = null;
4848
}
49-
else if (built[i] === PROP_SET) {
49+
else if (type === PROP_SET) {
5050
if (!currentProps) {
5151
currentProps = Object.create(null);
5252
props.push(currentProps);
5353
}
5454
currentProps[built[++i]] = [value];
5555
}
56-
else if (built[i] === PROP_APPEND) {
56+
else if (type === PROP_APPEND) {
5757
currentProps[built[++i]].push(value);
5858
}
59-
else if (built[i] === CHILD_RECURSE) {
59+
else if (type === CHILD_RECURSE) {
6060
children.push(_treeify(value));
6161
}
62-
else if (built[i] === CHILD_APPEND) {
62+
else if (type === CHILD_APPEND) {
6363
children.push(value);
6464
}
6565
}
@@ -70,12 +70,20 @@ export const treeify = (built, fields) => {
7070
return children.length > 1 ? children : children[0];
7171
};
7272

73-
7473
export const evaluate = (h, built, fields, args) => {
74+
let tmp;
75+
76+
// `build()` used the first element of the operation list as
77+
// temporary workspace. Now that `build()` is done we can use
78+
// that space to track whether the current element is "dynamic"
79+
// (i.e. it or any of its descendants depend on dynamic values).
80+
built[0] = 0;
81+
7582
for (let i = 1; i < built.length; i++) {
76-
const field = built[i];
77-
const value = typeof field === 'number' ? fields[field] : field;
78-
const type = built[++i];
83+
const type = built[i++];
84+
85+
// Set `built[0]` to truthy if this element depends on a dynamic value.
86+
const value = built[i] ? fields[built[0] = built[i++]] : built[++i];
7987

8088
if (type === TAG_SET) {
8189
args[0] = value;
@@ -90,11 +98,26 @@ export const evaluate = (h, built, fields, args) => {
9098
args[1][built[++i]] += (value + '');
9199
}
92100
else if (type) {
93-
// code === CHILD_RECURSE
94-
args.push(h.apply(null, evaluate(h, value, fields, ['', null])));
101+
// type === CHILD_RECURSE
102+
tmp = h.apply(0, evaluate(h, value, fields, ['', null]));
103+
args.push(tmp);
104+
105+
if (value[0]) {
106+
// If the child element is dynamic, then so is the current element.
107+
built[0] = 1;
108+
}
109+
else {
110+
// Rewrite the operation list in-place if the child element is static.
111+
// The currently evaluated piece `CHILD_RECURSE, 0, [...]` becomes
112+
// `CHILD_APPEND, 0, tmp`.
113+
// Essentially the operation list gets optimized for potential future
114+
// re-evaluations.
115+
built[i-2] = CHILD_APPEND;
116+
built[i] = tmp;
117+
}
95118
}
96119
else {
97-
// code === CHILD_APPEND
120+
// type === CHILD_APPEND
98121
args.push(value);
99122
}
100123
}
@@ -118,15 +141,15 @@ export const build = function(statics) {
118141
current.push(field ? fields[field] : buffer);
119142
}
120143
else {
121-
current.push(field || buffer, CHILD_APPEND);
144+
current.push(CHILD_APPEND, field, buffer);
122145
}
123146
}
124147
else if (mode === MODE_TAGNAME && (field || buffer)) {
125148
if (MINI) {
126149
current[1] = field ? fields[field] : buffer;
127150
}
128151
else {
129-
current.push(field || buffer, TAG_SET);
152+
current.push(TAG_SET, field, buffer);
130153
}
131154
mode = MODE_WHITESPACE;
132155
}
@@ -135,15 +158,15 @@ export const build = function(statics) {
135158
current[2] = Object.assign(current[2] || {}, fields[field]);
136159
}
137160
else {
138-
current.push(field, PROPS_ASSIGN);
161+
current.push(PROPS_ASSIGN, field, 0);
139162
}
140163
}
141164
else if (mode === MODE_WHITESPACE && buffer && !field) {
142165
if (MINI) {
143166
(current[2] = current[2] || {})[buffer] = true;
144167
}
145168
else {
146-
current.push(true, PROP_SET, buffer);
169+
current.push(PROP_SET, 0, true, buffer);
147170
}
148171
}
149172
else if (mode >= MODE_PROP_SET) {
@@ -158,11 +181,11 @@ export const build = function(statics) {
158181
}
159182
else {
160183
if (buffer || (!field && mode === MODE_PROP_SET)) {
161-
current.push(buffer, mode, propName);
184+
current.push(mode, 0, buffer, propName);
162185
mode = MODE_PROP_APPEND;
163186
}
164187
if (field) {
165-
current.push(field, mode, propName);
188+
current.push(mode, field, 0, propName);
166189
mode = MODE_PROP_APPEND;
167190
}
168191
}
@@ -241,7 +264,7 @@ export const build = function(statics) {
241264
(current = current[0]).push(h.apply(null, mode.slice(1)));
242265
}
243266
else {
244-
(current = current[0]).push(mode, CHILD_RECURSE);
267+
(current = current[0]).push(CHILD_RECURSE, 0, mode);
245268
}
246269
mode = MODE_SLASH;
247270
}

0 commit comments

Comments
 (0)