Skip to content

Commit

Permalink
Merge pull request #131 from zaygraveyard/issue-118
Browse files Browse the repository at this point in the history
[babel-plugin-htm] Add auto-import pragma option
  • Loading branch information
jviide authored Dec 26, 2019
2 parents 75fbb2e + 7a1137f commit c7eb893
Show file tree
Hide file tree
Showing 3 changed files with 237 additions and 12 deletions.
72 changes: 72 additions & 0 deletions packages/babel-plugin-htm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,78 @@ By default, `babel-plugin-htm` will process all Tagged Templates with a tag func
]}
```

### `import=false` _(experimental)_

Auto-import the pragma function, off by default.

#### `false` (default)

Don't auto-import anything.

#### `String`

Import the `pragma` like `import {<pragma>} from '<import>'`.

With Babel config:
```js
"plugins": [
["babel-plugin-htm", {
"tag": "$$html",
"import": "preact"
}]
]
```

```js
import { html as $$html } from 'htm/preact';

export default $$html`<div id="foo">hello ${you}</div>`
```

The above will produce files that look like:

```js
import { h } from 'preact';
import { html as $$html } from 'htm/preact';

export default h("div", { id: "foo" }, "hello ", you)
```

#### `{module: String, export: String}`

Import the `pragma` like `import {<import.export> as <pragma>} from '<import.module>'`.

With Babel config:
```js
"plugins": [
["babel-plugin-htm", {
"pragma": "React.createElement",
"tag": "$$html",
"import": {
// the module to import:
"module": "react",
// a named export to use from that module:
"export": "default"
}
}]
]
```

```js
import { html as $$html } from 'htm/react';

export default $$html`<div id="foo">hello ${you}</div>`
```

The above will produce files that look like:

```js
import React from 'react';
import { html as $$html } from 'htm/react';

export default React.createElement("div", { id: "foo" }, "hello ", you)
```

### `useBuiltIns=false`

`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}`.
Expand Down
46 changes: 40 additions & 6 deletions packages/babel-plugin-htm/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,42 @@ import { build, treeify } from '../../src/build.mjs';
* @param {object} options
* @param {string} [options.pragma=h] JSX/hyperscript pragma.
* @param {string} [options.tag=html] The tagged template "tag" function name to process.
* @param {string | boolean | object} [options.import=false] Import the tag automatically
* @param {boolean} [options.monomorphic=false] Output monomorphic inline objects instead of using String literals.
* @param {boolean} [options.useBuiltIns=false] Use the native Object.assign instead of trying to polyfill it.
* @param {boolean} [options.useNativeSpread=false] Use the native { ...a, ...b } syntax for prop spreads.
* @param {boolean} [options.variableArity=true] If `false`, always passes exactly 3 arguments to the pragma function.
*/
export default function htmBabelPlugin({ types: t }, options = {}) {
const pragma = options.pragma===false ? false : dottedIdentifier(options.pragma || 'h');
const pragmaString = options.pragma===false ? false : options.pragma || 'h';
const pragma = pragmaString===false ? false : dottedIdentifier(pragmaString);
const useBuiltIns = options.useBuiltIns;
const useNativeSpread = options.useNativeSpread;
const inlineVNodes = options.monomorphic || pragma===false;
const importDeclaration = pragmaImport(options.import || false);

function pragmaImport(imp) {
if (pragmaString === false || imp === false) {
return null;
}
const pragmaRoot = t.identifier(pragmaString.split('.')[0]);
const { module, export: export_ } = typeof imp !== 'string' ? imp : {
module: imp,
export: null
};

let specifier;
if (export_ === '*') {
specifier = t.importNamespaceSpecifier(pragmaRoot);
}
else if (export_ === 'default') {
specifier = t.importDefaultSpecifier(pragmaRoot);
}
else {
specifier = t.importSpecifier(pragmaRoot, export_ ? t.identifier(export_) : pragmaRoot);
}
return t.importDeclaration([specifier], t.stringLiteral(module));
}

function dottedIdentifier(keypath) {
const path = keypath.split('.');
Expand Down Expand Up @@ -69,7 +95,7 @@ export default function htmBabelPlugin({ types: t }, options = {}) {
}
return t.stringLiteral(str);
}

function createVNode(tag, props, children) {
// Never pass children=[[]].
if (children.elements.length === 1 && t.isArrayExpression(children.elements[0]) && children.elements[0].elements.length === 0) {
Expand All @@ -94,15 +120,15 @@ export default function htmBabelPlugin({ types: t }, options = {}) {

return t.callExpression(pragma, [tag, props].concat(children));
}

function spreadNode(args, state) {
if (args.length === 0) {
return t.nullLiteral();
}
if (args.length > 0 && t.isNode(args[0])) {
args.unshift({});
}

// 'Object.assign(x)', can be collapsed to 'x'.
if (args.length === 1) {
return propsNode(args[0]);
Expand All @@ -124,11 +150,11 @@ export default function htmBabelPlugin({ types: t }, options = {}) {
});
return t.objectExpression(properties);
}

const helper = useBuiltIns ? dottedIdentifier('Object.assign') : state.addHelper('extends');
return t.callExpression(helper, args.map(propsNode));
}

function propsNode(props) {
return t.isNode(props) ? props : t.objectExpression(objectProperties(props));
}
Expand All @@ -152,6 +178,13 @@ export default function htmBabelPlugin({ types: t }, options = {}) {
return {
name: 'htm',
visitor: {
Program: {
exit(path, state) {
if (state.get('hasHtm') && importDeclaration) {
path.unshiftContainer('body', importDeclaration);
}
},
},
TaggedTemplateExpression(path, state) {
const tag = path.node.tag.name;
if (htmlName[0]==='/' ? patternStringToRegExp(htmlName).test(tag) : tag === htmlName) {
Expand All @@ -163,6 +196,7 @@ export default function htmBabelPlugin({ types: t }, options = {}) {
? transform(tree, state)
: t.arrayExpression(tree.map(root => transform(root, state)));
path.replaceWith(node);
state.set('hasHtm', true);
}
}
}
Expand Down
131 changes: 125 additions & 6 deletions test/babel.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ describe('htm/babel', () => {
]
}).code
).toBe(`h("a",Object.assign({b:2},{c:3}),"d: ",4);`);

expect(
transform('html`<a b=${2} ...${{ c: 3 }}>d: ${4}</a>`;', {
...options,
Expand All @@ -68,7 +68,7 @@ describe('htm/babel', () => {
]
}).code
).toBe(`h("a",foo);`);

expect(
transform('html`<a ...${foo}></a>`;', {
...options,
Expand All @@ -92,7 +92,7 @@ describe('htm/babel', () => {
]
}).code
).toBe(`h("a",Object.assign({},foo,bar));`);

expect(
transform('html`<a ...${foo} ...${bar}></a>`;', {
...options,
Expand All @@ -116,7 +116,7 @@ describe('htm/babel', () => {
]
}).code
).toBe(`h("a",Object.assign({b:"1"},foo));`);

expect(
transform('html`<a b="1" ...${foo}></a>`;', {
...options,
Expand All @@ -140,7 +140,7 @@ describe('htm/babel', () => {
]
}).code
).toBe(`h("a",Object.assign({},foo,{b:"1"}));`);

expect(
transform('html`<a ...${foo} b="1"></a>`;', {
...options,
Expand All @@ -164,7 +164,7 @@ describe('htm/babel', () => {
]
}).code
).toBe(`h("a",Object.assign({b:"1"},foo,{c:2},{d:3}));`);

expect(
transform('html`<a b="1" ...${foo} c=${2} ...${{d:3}}></a>`;', {
...options,
Expand Down Expand Up @@ -307,6 +307,125 @@ describe('htm/babel', () => {
});
});

describe('{import:"preact"}', () => {
test('should do nothing when pragma=false', () => {
expect(
transform('var name="world",vnode=html`<div id=hello>hello, ${name}</div>`;', {
...options,
plugins: [
[htmBabelPlugin, {
pragma: false,
import: 'preact'
}]
]
}).code
).toBe(`var name="world",vnode={tag:"div",props:{id:"hello"},children:["hello, ",name]};`);
});
test('should do nothing when tag is not used', () => {
expect(
transform('console.log("hi");', {
...options,
plugins: [
[htmBabelPlugin, {
import: 'preact'
}]
]
}).code
).toBe(`console.log("hi");`);
});
test('should add import', () => {
expect(
transform('html`<div id=hello>hello</div>`;', {
...options,
plugins: [
[htmBabelPlugin, {
import: 'preact'
}]
]
}).code
).toBe(`import{h}from"preact";h("div",{id:"hello"},"hello");`);
});
test('should add import for pragma', () => {
expect(
transform('html`<div id=hello>hello</div>`;', {
...options,
plugins: [
[htmBabelPlugin, {
pragma: 'createElement',
import: 'react'
}]
]
}).code
).toBe(`import{createElement}from"react";createElement("div",{id:"hello"},"hello");`);
});
});

describe('{import:Object}', () => {
test('should add import', () => {
expect(
transform('html`<div id=hello>hello</div>`;', {
...options,
plugins: [
[htmBabelPlugin, {
import: {
module: 'preact',
export: 'h'
}
}]
]
}).code
).toBe(`import{h}from"preact";h("div",{id:"hello"},"hello");`);
});
test('should add import as pragma', () => {
expect(
transform('html`<div id=hello>hello</div>`;', {
...options,
plugins: [
[htmBabelPlugin, {
pragma: 'hh',
import: {
module: 'preact',
export: 'h'
}
}]
]
}).code
).toBe(`import{h as hh}from"preact";hh("div",{id:"hello"},"hello");`);
});
test('should add import default', () => {
expect(
transform('html`<div id=hello>hello</div>`;', {
...options,
plugins: [
[htmBabelPlugin, {
pragma: 'React.createElement',
import: {
module: 'react',
export: 'default'
}
}]
]
}).code
).toBe(`import React from"react";React.createElement("div",{id:"hello"},"hello");`);
});
test('should add import *', () => {
expect(
transform('html`<div id=hello>hello</div>`;', {
...options,
plugins: [
[htmBabelPlugin, {
pragma: 'Preact.h',
import: {
module: 'preact',
export: '*'
}
}]
]
}).code
).toBe(`import*as Preact from"preact";Preact.h("div",{id:"hello"},"hello");`);
});
});

describe('main test suite', () => {
// Run all of the main tests against the Babel plugin:
const mod = require('fs').readFileSync(require('path').resolve(__dirname, 'index.test.mjs'), 'utf8').replace(/\\0/g, '\0');
Expand Down

0 comments on commit c7eb893

Please sign in to comment.