Skip to content

Commit c7eb893

Browse files
authored
Merge pull request #131 from zaygraveyard/issue-118
[babel-plugin-htm] Add auto-import pragma option
2 parents 75fbb2e + 7a1137f commit c7eb893

File tree

3 files changed

+237
-12
lines changed

3 files changed

+237
-12
lines changed

packages/babel-plugin-htm/README.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,78 @@ By default, `babel-plugin-htm` will process all Tagged Templates with a tag func
4343
]}
4444
```
4545

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+
46118
### `useBuiltIns=false`
47119

48120
`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
}

test/babel.test.mjs

Lines changed: 125 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ describe('htm/babel', () => {
4646
]
4747
}).code
4848
).toBe(`h("a",Object.assign({b:2},{c:3}),"d: ",4);`);
49-
49+
5050
expect(
5151
transform('html`<a b=${2} ...${{ c: 3 }}>d: ${4}</a>`;', {
5252
...options,
@@ -68,7 +68,7 @@ describe('htm/babel', () => {
6868
]
6969
}).code
7070
).toBe(`h("a",foo);`);
71-
71+
7272
expect(
7373
transform('html`<a ...${foo}></a>`;', {
7474
...options,
@@ -92,7 +92,7 @@ describe('htm/babel', () => {
9292
]
9393
}).code
9494
).toBe(`h("a",Object.assign({},foo,bar));`);
95-
95+
9696
expect(
9797
transform('html`<a ...${foo} ...${bar}></a>`;', {
9898
...options,
@@ -116,7 +116,7 @@ describe('htm/babel', () => {
116116
]
117117
}).code
118118
).toBe(`h("a",Object.assign({b:"1"},foo));`);
119-
119+
120120
expect(
121121
transform('html`<a b="1" ...${foo}></a>`;', {
122122
...options,
@@ -140,7 +140,7 @@ describe('htm/babel', () => {
140140
]
141141
}).code
142142
).toBe(`h("a",Object.assign({},foo,{b:"1"}));`);
143-
143+
144144
expect(
145145
transform('html`<a ...${foo} b="1"></a>`;', {
146146
...options,
@@ -164,7 +164,7 @@ describe('htm/babel', () => {
164164
]
165165
}).code
166166
).toBe(`h("a",Object.assign({b:"1"},foo,{c:2},{d:3}));`);
167-
167+
168168
expect(
169169
transform('html`<a b="1" ...${foo} c=${2} ...${{d:3}}></a>`;', {
170170
...options,
@@ -307,6 +307,125 @@ describe('htm/babel', () => {
307307
});
308308
});
309309

310+
describe('{import:"preact"}', () => {
311+
test('should do nothing when pragma=false', () => {
312+
expect(
313+
transform('var name="world",vnode=html`<div id=hello>hello, ${name}</div>`;', {
314+
...options,
315+
plugins: [
316+
[htmBabelPlugin, {
317+
pragma: false,
318+
import: 'preact'
319+
}]
320+
]
321+
}).code
322+
).toBe(`var name="world",vnode={tag:"div",props:{id:"hello"},children:["hello, ",name]};`);
323+
});
324+
test('should do nothing when tag is not used', () => {
325+
expect(
326+
transform('console.log("hi");', {
327+
...options,
328+
plugins: [
329+
[htmBabelPlugin, {
330+
import: 'preact'
331+
}]
332+
]
333+
}).code
334+
).toBe(`console.log("hi");`);
335+
});
336+
test('should add import', () => {
337+
expect(
338+
transform('html`<div id=hello>hello</div>`;', {
339+
...options,
340+
plugins: [
341+
[htmBabelPlugin, {
342+
import: 'preact'
343+
}]
344+
]
345+
}).code
346+
).toBe(`import{h}from"preact";h("div",{id:"hello"},"hello");`);
347+
});
348+
test('should add import for pragma', () => {
349+
expect(
350+
transform('html`<div id=hello>hello</div>`;', {
351+
...options,
352+
plugins: [
353+
[htmBabelPlugin, {
354+
pragma: 'createElement',
355+
import: 'react'
356+
}]
357+
]
358+
}).code
359+
).toBe(`import{createElement}from"react";createElement("div",{id:"hello"},"hello");`);
360+
});
361+
});
362+
363+
describe('{import:Object}', () => {
364+
test('should add import', () => {
365+
expect(
366+
transform('html`<div id=hello>hello</div>`;', {
367+
...options,
368+
plugins: [
369+
[htmBabelPlugin, {
370+
import: {
371+
module: 'preact',
372+
export: 'h'
373+
}
374+
}]
375+
]
376+
}).code
377+
).toBe(`import{h}from"preact";h("div",{id:"hello"},"hello");`);
378+
});
379+
test('should add import as pragma', () => {
380+
expect(
381+
transform('html`<div id=hello>hello</div>`;', {
382+
...options,
383+
plugins: [
384+
[htmBabelPlugin, {
385+
pragma: 'hh',
386+
import: {
387+
module: 'preact',
388+
export: 'h'
389+
}
390+
}]
391+
]
392+
}).code
393+
).toBe(`import{h as hh}from"preact";hh("div",{id:"hello"},"hello");`);
394+
});
395+
test('should add import default', () => {
396+
expect(
397+
transform('html`<div id=hello>hello</div>`;', {
398+
...options,
399+
plugins: [
400+
[htmBabelPlugin, {
401+
pragma: 'React.createElement',
402+
import: {
403+
module: 'react',
404+
export: 'default'
405+
}
406+
}]
407+
]
408+
}).code
409+
).toBe(`import React from"react";React.createElement("div",{id:"hello"},"hello");`);
410+
});
411+
test('should add import *', () => {
412+
expect(
413+
transform('html`<div id=hello>hello</div>`;', {
414+
...options,
415+
plugins: [
416+
[htmBabelPlugin, {
417+
pragma: 'Preact.h',
418+
import: {
419+
module: 'preact',
420+
export: '*'
421+
}
422+
}]
423+
]
424+
}).code
425+
).toBe(`import*as Preact from"preact";Preact.h("div",{id:"hello"},"hello");`);
426+
});
427+
});
428+
310429
describe('main test suite', () => {
311430
// Run all of the main tests against the Babel plugin:
312431
const mod = require('fs').readFileSync(require('path').resolve(__dirname, 'index.test.mjs'), 'utf8').replace(/\\0/g, '\0');

0 commit comments

Comments
 (0)