Skip to content

Commit cc5f93f

Browse files
committed
Add packages/babel-plugin-transform-jsx-to-tagged-templates
1 parent 0bade18 commit cc5f93f

File tree

3 files changed

+259
-0
lines changed

3 files changed

+259
-0
lines changed
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
{
2+
"name": "babel-plugin-transform-jsx-to-tagged-templates",
3+
"version": "0.1.0",
4+
"description": "Babel plugin to compile JSX to Tagged Templates.",
5+
"main": "dist/babel-plugin-transform-jsx-to-tagged-templates.js",
6+
"module": "dist/babel-plugin-transform-jsx-to-tagged-templates.mjs",
7+
"scripts": {
8+
"build": "microbundle src/index.mjs -f es,cjs --target node --no-compress --no-sourcemap",
9+
"test": "jest",
10+
"prepare": "npm run build"
11+
},
12+
"files": [
13+
"dist",
14+
"src"
15+
],
16+
"eslintConfig": {
17+
"extends": "developit"
18+
},
19+
"babel": {
20+
"presets": [
21+
"env"
22+
]
23+
},
24+
"jest": {
25+
"moduleFileExtensions": [
26+
"mjs",
27+
"js",
28+
"json"
29+
],
30+
"transform": {
31+
"^.+\\.m?js$": "babel-jest"
32+
}
33+
},
34+
"repository": "developit/htm",
35+
"keywords": [
36+
"tagged template",
37+
"template literals",
38+
"html",
39+
"htm",
40+
"jsx",
41+
"virtual dom",
42+
"hyperscript",
43+
"babel",
44+
"babel plugin",
45+
"babel-plugin"
46+
],
47+
"author": "Jason Miller <[email protected]>",
48+
"license": "Apache-2.0",
49+
"homepage": "https://github.com/developit/htm/tree/master/packages/babel-plugin-transform-jsx-to-tagged-templates",
50+
"devDependencies": {
51+
"babel-core": "^6.26.3",
52+
"babel-jest": "^23.6.0",
53+
"babel-preset-env": "^1.7.0",
54+
"eslint": "^5.10.0",
55+
"eslint-config-developit": "^1.1.1",
56+
"jest": "^23.6.0",
57+
"microbundle": "^0.8.3"
58+
},
59+
"dependencies": {
60+
"@babel/plugin-syntax-jsx": "^7.2.0",
61+
"babel-plugin-syntax-jsx": "^6.18.0"
62+
}
63+
}
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import jsx from 'babel-plugin-syntax-jsx';
2+
// import jsx from '@babel/plugin-syntax-jsx';
3+
4+
function escapeValue(value) {
5+
if (value.match(/^[a-z0-9_' &]+$/gi)) {
6+
return `"${value}"`;
7+
}
8+
return JSON.stringify(value);
9+
}
10+
11+
/**
12+
* @param {Babel} babel
13+
* @param {object} [options]
14+
* @param {string} [options.tag='html'] The tagged template "tag" function name to produce.
15+
*/
16+
export default function jsxToTaggedTemplatesBabelPlugin({ types: t }, options = {}) {
17+
const tag = dottedIdentifier(options.tag || 'html');
18+
19+
function dottedIdentifier(keypath) {
20+
const path = keypath.split('.');
21+
let out;
22+
for (let i = 0; i < path.length; i++) {
23+
const ident = t.identifier(path[i]);
24+
out = i === 0 ? ident : t.memberExpression(out, ident);
25+
}
26+
return out;
27+
}
28+
29+
let quasis = [];
30+
let expressions = [];
31+
let buffer = '';
32+
33+
function expr(value) {
34+
commit(true);
35+
expressions.push(value);
36+
}
37+
38+
function raw(str) {
39+
buffer += str;
40+
}
41+
42+
function commit(force) {
43+
if (!buffer && !force) return;
44+
quasis.push(t.templateElement({
45+
raw: buffer,
46+
cooked: buffer
47+
}));
48+
buffer = '';
49+
}
50+
51+
function processNode(node, path, isRoot) {
52+
const open = node.openingElement;
53+
const { name } = open.name;
54+
55+
const toProcess = [];
56+
57+
if (name.match(/^[A-Z]/)) {
58+
raw('<');
59+
expr(t.identifier(name));
60+
}
61+
else {
62+
raw('<');
63+
raw(name);
64+
}
65+
66+
if (open.attributes) {
67+
for (let i = 0; i < open.attributes.length; i++) {
68+
const { name, value } = open.attributes[i];
69+
raw(' ');
70+
raw(name.name);
71+
if (value) {
72+
raw('=');
73+
if (value.expression) {
74+
expr(value.expression);
75+
}
76+
else if (t.isStringLiteral(value)) {
77+
raw(escapeValue(value.value));
78+
}
79+
else {
80+
expr(value);
81+
}
82+
}
83+
}
84+
}
85+
raw('>');
86+
87+
if (node.children) {
88+
for (let i = 0; i < node.children.length; i++) {
89+
let child = node.children[i];
90+
if (t.isJSXText(child)) {
91+
raw(child.value);
92+
}
93+
else {
94+
if (t.isJSXExpressionContainer(child)) {
95+
child = child.expression;
96+
}
97+
if (t.isJSXElement(child)) {
98+
processNode(child);
99+
}
100+
else {
101+
expr(child);
102+
toProcess.push(child);
103+
}
104+
}
105+
}
106+
}
107+
108+
if (name.match(/^[A-Z]/)) {
109+
raw('</');
110+
expr(t.identifier(name));
111+
raw('>');
112+
}
113+
else {
114+
raw('</');
115+
raw(name);
116+
raw('>');
117+
}
118+
119+
if (isRoot) {
120+
commit();
121+
const template = t.templateLiteral(quasis, expressions);
122+
const replacement = t.taggedTemplateExpression(tag, template);
123+
path.replaceWith(replacement);
124+
}
125+
}
126+
127+
return {
128+
name: 'transform-jsx-to-tagged-templates',
129+
inherits: jsx,
130+
visitor: {
131+
JSXElement(path) {
132+
let quasisBefore = quasis.slice();
133+
let expressionsBefore = expressions.slice();
134+
let bufferBefore = buffer;
135+
136+
buffer = '';
137+
quasis.length = 0;
138+
expressions.length = 0;
139+
140+
processNode(path.node, path, true);
141+
142+
quasis = quasisBefore;
143+
expressions = expressionsBefore;
144+
buffer = bufferBefore;
145+
}
146+
}
147+
};
148+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { transform } from '@babel/core';
2+
import transformJsxToTaggedTemplatesPlugin from '../src/index.mjs';
3+
4+
describe('babel-plugin-transform-jsx-to-tagged-templates', () => {
5+
test('basic transformation', () => {
6+
expect(
7+
transform('(<div id="hello">hello</div>);', {
8+
babelrc: false,
9+
plugins: [
10+
transformJsxToTaggedTemplatesPlugin
11+
]
12+
}).code
13+
).toBe('html`<div id="hello">hello</div>`;');
14+
});
15+
16+
test('nested children transformation', () => {
17+
expect(
18+
transform('const Foo = () => <div class="foo" draggable>\n <h1>Hello</h1>\n <p>world.</p>\n</div>;', {
19+
babelrc: false,
20+
plugins: [
21+
transformJsxToTaggedTemplatesPlugin
22+
]
23+
}).code
24+
).toBe('const Foo = () => html`<div class="foo" draggable>\n <h1>Hello</h1>\n <p>world.</p>\n</div>`;');
25+
});
26+
27+
test('whitespace', () => {
28+
expect(
29+
transform('const Foo = props => <div a b> a <em> b </em> c <strong> d </strong> e </div>;', {
30+
babelrc: false,
31+
plugins: [
32+
transformJsxToTaggedTemplatesPlugin
33+
]
34+
}).code
35+
).toBe('const Foo = props => html`<div a b> a <em> b </em> c <strong> d </strong> e </div>`;');
36+
});
37+
38+
test('nested templates', () => {
39+
expect(
40+
transform('const Foo = props => <ul>{props.items.map(item =>\n <li>\n {item}\n </li>\n)}</ul>;', {
41+
babelrc: false,
42+
plugins: [
43+
transformJsxToTaggedTemplatesPlugin
44+
]
45+
}).code
46+
).toBe('const Foo = props => html`<ul>${props.items.map(item => html`<li>\n ${item}\n </li>`)}</ul>`;');
47+
});
48+
});

0 commit comments

Comments
 (0)