Skip to content

Commit 0a7c406

Browse files
authored
Merge pull request #65 from jviide/jsx-to-htm
Add packages/babel-plugin-transform-jsx-to-htm
2 parents 0bade18 + be39e20 commit 0a7c406

File tree

7 files changed

+510
-39
lines changed

7 files changed

+510
-39
lines changed

babel.config.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
module.exports = {
2+
presets: [
3+
[
4+
'@babel/preset-env',
5+
{
6+
targets: {
7+
node: 'current'
8+
}
9+
}
10+
]
11+
]
12+
};

package.json

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@
66
"umd:main": "dist/htm.umd.js",
77
"module": "dist/htm.mjs",
88
"scripts": {
9-
"build": "npm run -s build:main && npm run -s build:preact && npm run -s build:babel",
9+
"build": "npm run -s build:main && npm run -s build:preact && npm run -s build:babel && npm run -s build:babel-transform-jsx",
1010
"build:main": "microbundle src/index.mjs -f es,umd --no-sourcemap --target web && microbundle src/cjs.mjs -f iife --no-sourcemap --target web",
1111
"build:preact": "cd src/integrations/preact && npm run build",
1212
"build:babel": "cd packages/babel-plugin-htm && npm run build",
13+
"build:babel-transform-jsx": "cd packages/babel-plugin-transform-jsx-to-htm && npm run build",
1314
"test": "eslint src/**/*.mjs test/**/*.mjs && npm run build && jest test",
1415
"release": "npm t && git commit -am \"$npm_package_version\" && git tag $npm_package_version && git push && git push --tags && npm publish"
1516
},
@@ -39,16 +40,12 @@
3940
"js"
4041
],
4142
"moduleNameMapper": {
43+
"^babel-plugin-transform-jsx-to-htm$": "<rootDir>/packages/babel-plugin-transform-jsx-to-htm/index.mjs",
4244
"^babel-plugin-htm$": "<rootDir>/packages/babel-plugin-htm/index.mjs",
4345
"^htm$": "<rootDir>/src/index.mjs",
4446
"^htm/preact$": "<rootDir>/src/integrations/preact/index.mjs"
4547
}
4648
},
47-
"babel": {
48-
"presets": [
49-
"env"
50-
]
51-
},
5249
"repository": "developit/htm",
5350
"keywords": [
5451
"Hyperscript Tagged Markup",
@@ -66,11 +63,12 @@
6663
"devDependencies": {
6764
"@babel/core": "^7.2.2",
6865
"@babel/preset-env": "^7.1.6",
69-
"babel-jest": "^23.6.0",
66+
"babel-jest": "^24.1.0",
67+
"babel-plugin-jsx-pragmatic": "^1.0.2",
7068
"babel-preset-env": "^1.7.0",
7169
"eslint": "^5.2.0",
7270
"eslint-config-developit": "^1.1.1",
73-
"jest": "^23.4.2",
71+
"jest": "^24.1.0",
7472
"microbundle": "^0.8.3",
7573
"preact": "^8.4.2"
7674
},
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# babel-plugin-transform-jsx-to-htm
2+
3+
This plugin converts JSX into Tagged Templates that work with things like [htm] and [lit-html].
4+
5+
```js
6+
// INPUT:
7+
const Foo = () => <h1>Hello</h1>
8+
9+
// OUTPUT:
10+
const Foo = () => html`<h1>Hello</h1>`
11+
```
12+
13+
## Installation
14+
15+
Grab it from npm:
16+
17+
```sh
18+
npm i -D babel-plugin-transform-jsx-to-htm
19+
```
20+
21+
... then add it to your Babel config (eg: `.babelrc`):
22+
23+
```js
24+
"plugins": [
25+
"babel-plugin-transform-jsx-to-htm"
26+
]
27+
```
28+
29+
## Options
30+
31+
The following options are available:
32+
33+
| Option | Type | Default | Description
34+
|--------|---------|----------|------------
35+
| `tag` | String | `"html"` | The "tag" function to prefix [Tagged Templates] with.<br> _Useful when [Auto-importing a tag function](#auto-importing-the-tag)._
36+
| `html` | Boolean | `false` | `true` outputs HTML-like templates for use with [lit-html].<br> _The is default XML-like, with self-closing tags._
37+
38+
Options are passed to a Babel plugin using a nested Array:
39+
40+
```js
41+
"plugins": [
42+
["babel-plugin-transform-jsx-to-htm", {
43+
"tag": "$$html",
44+
"html": true
45+
}]
46+
]
47+
```
48+
49+
## Auto-importing the tag
50+
51+
Want to automatically import `html` into any file that uses JSX? It works the same as with JSX!
52+
Just use [babel-plugin-jsx-pragmatic]:
53+
54+
```js
55+
"plugins": [
56+
["babel-plugin-jsx-pragmatic", {
57+
// the module to import:
58+
"module": "lit-html",
59+
// a named export to use from that module:
60+
"export": "html",
61+
// what to call it locally: (should match your "tag" option)
62+
"import": "$$html"
63+
}],
64+
["babel-plugin-transform-jsx-to-htm", {
65+
"tag": "$$html"
66+
}]
67+
]
68+
```
69+
70+
The above will produce files that look like:
71+
72+
```js
73+
import { html as $$html } from 'lit-html';
74+
75+
export default $$html`<h1>hello</h1>`
76+
```
77+
78+
### License
79+
80+
Apache 2
81+
82+
[htm]: https://github.com/developit/htm
83+
[lit-html]: https://github.com/polymer/lit-html
84+
[babel-plugin-jsx-pragmatic]: https://github.com/jmm/babel-plugin-jsx-pragmatic
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import jsx from '@babel/plugin-syntax-jsx';
2+
3+
/**
4+
* @param {Babel} babel
5+
* @param {object} [options]
6+
* @param {string} [options.tag='html'] The tagged template "tag" function name to produce.
7+
* @param {string} [options.html=false] If `true`, output HTML-like instead of XML-like (no self-closing tags, etc).
8+
*/
9+
export default function jsxToHtmBabelPlugin({ types: t }, options = {}) {
10+
const tag = dottedIdentifier(options.tag || 'html');
11+
const htmlOutput = !!options.html;
12+
13+
function dottedIdentifier(keypath) {
14+
const path = keypath.split('.');
15+
let out;
16+
for (let i = 0; i < path.length; i++) {
17+
const ident = t.identifier(path[i]);
18+
out = i === 0 ? ident : t.memberExpression(out, ident);
19+
}
20+
return out;
21+
}
22+
23+
let quasis = [];
24+
let expressions = [];
25+
let buffer = '';
26+
27+
function expr(value) {
28+
commit(true);
29+
expressions.push(value);
30+
}
31+
32+
function raw(str) {
33+
buffer += str;
34+
}
35+
36+
function escapeText(text) {
37+
if (text.indexOf('<') < 0) {
38+
return raw(text);
39+
}
40+
return expr(t.stringLiteral(text));
41+
}
42+
43+
function escapePropValue(node) {
44+
const value = node.value;
45+
46+
if (value.match(/^.*$/u)) {
47+
if (value.indexOf('"') < 0) {
48+
return raw(`"${value}"`);
49+
}
50+
else if (value.indexOf("'") < 0) {
51+
return raw(`'${value}'`);
52+
}
53+
}
54+
55+
return expr(t.stringLiteral(node.value));
56+
}
57+
58+
function commit(force) {
59+
if (!buffer && !force) return;
60+
quasis.push(t.templateElement({
61+
raw: buffer,
62+
cooked: buffer
63+
}));
64+
buffer = '';
65+
}
66+
67+
function processNode(node, path, isRoot) {
68+
const open = node.openingElement;
69+
const { name } = open.name;
70+
71+
if (name.match(/^[A-Z]/)) {
72+
raw('<');
73+
expr(t.identifier(name));
74+
}
75+
else {
76+
raw('<');
77+
raw(name);
78+
}
79+
80+
if (open.attributes) {
81+
for (let i = 0; i < open.attributes.length; i++) {
82+
const attr = open.attributes[i];
83+
raw(' ');
84+
if (t.isJSXSpreadAttribute(attr)) {
85+
raw('...');
86+
expr(attr.argument);
87+
continue;
88+
}
89+
const { name, value } = attr;
90+
raw(name.name);
91+
if (value) {
92+
raw('=');
93+
if (value.expression) {
94+
expr(value.expression);
95+
}
96+
else if (t.isStringLiteral(value)) {
97+
escapePropValue(value);
98+
}
99+
else {
100+
expr(value);
101+
}
102+
}
103+
}
104+
}
105+
106+
const children = t.react.buildChildren(node);
107+
if (htmlOutput || children && children.length !== 0) {
108+
raw('>');
109+
for (let i = 0; i < children.length; i++) {
110+
let child = children[i];
111+
if (t.isStringLiteral(child)) {
112+
// @todo - expose `whitespace: true` option?
113+
escapeText(child.value);
114+
}
115+
else if (t.isJSXElement(child)) {
116+
processNode(child);
117+
}
118+
else {
119+
expr(child);
120+
}
121+
}
122+
123+
if (name.match(/^[A-Z]/)) {
124+
raw('</');
125+
expr(t.identifier(name));
126+
raw('>');
127+
}
128+
else {
129+
raw('</');
130+
raw(name);
131+
raw('>');
132+
}
133+
}
134+
else {
135+
raw('/>');
136+
}
137+
138+
if (isRoot) {
139+
commit();
140+
const template = t.templateLiteral(quasis, expressions);
141+
const replacement = t.taggedTemplateExpression(tag, template);
142+
path.replaceWith(replacement);
143+
}
144+
}
145+
146+
return {
147+
name: 'transform-jsx-to-htm',
148+
inherits: jsx,
149+
visitor: {
150+
JSXElement(path) {
151+
let quasisBefore = quasis.slice();
152+
let expressionsBefore = expressions.slice();
153+
let bufferBefore = buffer;
154+
155+
buffer = '';
156+
quasis.length = 0;
157+
expressions.length = 0;
158+
159+
processNode(path.node, path, true);
160+
161+
quasis = quasisBefore;
162+
expressions = expressionsBefore;
163+
buffer = bufferBefore;
164+
}
165+
}
166+
};
167+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"name": "babel-plugin-transform-jsx-to-htm",
3+
"version": "0.1.0",
4+
"description": "Babel plugin to compile JSX to Tagged Templates.",
5+
"main": "dist/babel-plugin-transform-jsx-to-htm.js",
6+
"module": "dist/babel-plugin-transform-jsx-to-htm.mjs",
7+
"scripts": {
8+
"build": "microbundle index.mjs -f es,cjs --target node --no-compress --no-sourcemap",
9+
"prepare": "npm run build"
10+
},
11+
"files": [
12+
"dist"
13+
],
14+
"repository": "developit/htm",
15+
"keywords": [
16+
"tagged template",
17+
"template literals",
18+
"html",
19+
"htm",
20+
"jsx",
21+
"virtual dom",
22+
"hyperscript",
23+
"babel",
24+
"babel plugin",
25+
"babel-plugin"
26+
],
27+
"author": "Jason Miller <[email protected]>",
28+
"license": "Apache-2.0",
29+
"homepage": "https://github.com/developit/htm/tree/master/packages/babel-plugin-transform-jsx-to-htm",
30+
"devDependencies": {
31+
"microbundle": "^0.8.3"
32+
}
33+
}

0 commit comments

Comments
 (0)