Skip to content

Commit b7f8347

Browse files
committed
Merge branch 'master' into concatenate
2 parents f7c5dd8 + 0ed6079 commit b7f8347

File tree

4 files changed

+225
-47
lines changed

4 files changed

+225
-47
lines changed

packages/babel-plugin-htm/index.mjs

Lines changed: 37 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ import { build, treeify } from '../../src/build.mjs';
77
* @param {string} [options.tag=html] The tagged template "tag" function name to process.
88
* @param {boolean} [options.monomorphic=false] Output monomorphic inline objects instead of using String literals.
99
* @param {boolean} [options.useBuiltIns=false] Use the native Object.assign instead of trying to polyfill it.
10+
* @param {boolean} [options.useNativeSpread=false] Use the native { ...a, ...b } syntax for prop spreads.
1011
* @param {boolean} [options.variableArity=true] If `false`, always passes exactly 3 arguments to the pragma function.
1112
*/
1213
export default function htmBabelPlugin({ types: t }, options = {}) {
1314
const pragma = options.pragma===false ? false : dottedIdentifier(options.pragma || 'h');
1415
const useBuiltIns = options.useBuiltIns;
16+
const useNativeSpread = options.useNativeSpread;
1517
const inlineVNodes = options.monomorphic || pragma===false;
1618

1719
function dottedIdentifier(keypath) {
@@ -29,14 +31,32 @@ export default function htmBabelPlugin({ types: t }, options = {}) {
2931
const end = parts.pop() || '';
3032
return new RegExp(parts.join('/'), end);
3133
}
32-
34+
3335
function propertyName(key) {
3436
if (t.isValidIdentifier(key)) {
3537
return t.identifier(key);
3638
}
3739
return t.stringLiteral(key);
3840
}
39-
41+
42+
function objectProperties(obj) {
43+
return Object.keys(obj).map(key => {
44+
const values = obj[key].map(valueOrNode =>
45+
t.isNode(valueOrNode) ? valueOrNode : t.valueToNode(valueOrNode)
46+
);
47+
48+
let node = values[0];
49+
if (values.length > 1 && !t.isStringLiteral(node) && !t.isStringLiteral(values[1])) {
50+
node = t.binaryExpression('+', t.stringLiteral(''), node);
51+
}
52+
values.slice(1).forEach(value => {
53+
node = t.binaryExpression('+', node, value);
54+
});
55+
56+
return t.objectProperty(propertyName(key), node);
57+
});
58+
}
59+
4060
function stringValue(str) {
4161
if (options.monomorphic) {
4262
return t.objectExpression([
@@ -91,36 +111,26 @@ export default function htmBabelPlugin({ types: t }, options = {}) {
91111
if (args.length === 2 && !t.isNode(args[0]) && Object.keys(args[0]).length === 0) {
92112
return propsNode(args[1]);
93113
}
114+
115+
if (useNativeSpread) {
116+
const properties = [];
117+
args.forEach(arg => {
118+
if (t.isNode(arg)) {
119+
properties.push(t.spreadElement(arg));
120+
}
121+
else {
122+
properties.push(...objectProperties(arg));
123+
}
124+
});
125+
return t.objectExpression(properties);
126+
}
127+
94128
const helper = useBuiltIns ? dottedIdentifier('Object.assign') : state.addHelper('extends');
95129
return t.callExpression(helper, args.map(propsNode));
96130
}
97131

98-
function propValueNode(value) {
99-
if (typeof value==='string') {
100-
value = t.stringLiteral(value);
101-
}
102-
else if (typeof value==='boolean') {
103-
value = t.booleanLiteral(value);
104-
}
105-
return value;
106-
}
107-
108132
function propsNode(props) {
109-
return t.isNode(props) ? props : t.objectExpression(
110-
Object.keys(props).map(key => {
111-
const values = props[key].map(propValueNode);
112-
113-
let node = values[0];
114-
if (values.length > 1 && !t.isStringLiteral(node) && !t.isStringLiteral(values[1])) {
115-
node = t.binaryExpression('+', t.stringLiteral(''), node);
116-
}
117-
values.slice(1).forEach(value => {
118-
node = t.binaryExpression('+', node, value);
119-
});
120-
121-
return t.objectProperty(propertyName(key), node);
122-
})
123-
);
133+
return t.isNode(props) ? props : t.objectExpression(objectProperties(props));
124134
}
125135

126136
function transform(node, state) {

packages/babel-plugin-transform-jsx-to-htm/index.mjs

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -87,23 +87,33 @@ export default function jsxToHtmBabelPlugin({ types: t }, options = {}) {
8787
}));
8888
buffer = '';
8989
}
90+
91+
const FRAGMENT_EXPR = dottedIdentifier('React.Fragment');
92+
93+
function isFragmentName(node) {
94+
return t.isNodesEquivalent(FRAGMENT_EXPR, node);
95+
}
9096

91-
function getName(node) {
92-
switch (node.type) {
93-
case 'JSXMemberExpression':
94-
return `${node.object.name}.${node.property.name}`
95-
96-
default:
97-
return node.name;
97+
function isComponentName(node) {
98+
return !t.isIdentifier(node) || node.name.match(/^[$_A-Z]/);
99+
}
100+
101+
function getNameExpr(node) {
102+
if (!t.isJSXMemberExpression(node)) {
103+
return t.identifier(node.name);
98104
}
105+
return t.memberExpression(
106+
getNameExpr(node.object),
107+
t.identifier(node.property.name)
108+
);
99109
}
100110

101111
function processChildren(node, name, isFragment) {
102112
const children = t.react.buildChildren(node);
103113
if (children && children.length !== 0) {
104114
if (!isFragment) {
105115
raw('>');
106-
}
116+
}
107117
for (let i = 0; i < children.length; i++) {
108118
let child = children[i];
109119
if (t.isStringLiteral(child)) {
@@ -119,17 +129,17 @@ export default function jsxToHtmBabelPlugin({ types: t }, options = {}) {
119129
}
120130

121131
if (!isFragment) {
122-
if (name.match(/(^[$_A-Z]|\.)/)) {
132+
if (isComponentName(name)) {
123133
raw('</');
124-
expr(t.identifier(name));
134+
expr(name);
125135
raw('>');
126136
}
127137
else {
128138
raw('</');
129-
raw(name);
139+
raw(name.name);
130140
raw('>');
131141
}
132-
}
142+
}
133143
}
134144
else if (!isFragment) {
135145
raw('/>');
@@ -138,17 +148,17 @@ export default function jsxToHtmBabelPlugin({ types: t }, options = {}) {
138148

139149
function processNode(node, path, isRoot) {
140150
const open = node.openingElement;
141-
const name = getName(open.name);
142-
const isFragment = name === 'React.Fragment';
143-
151+
const name = getNameExpr(open.name);
152+
const isFragment = isFragmentName(name);
153+
144154
if (!isFragment) {
145-
if (name.match(/(^[$_A-Z]|\.)/)) {
155+
if (isComponentName(name)) {
146156
raw('<');
147-
expr(t.identifier(name));
157+
expr(name);
148158
}
149159
else {
150160
raw('<');
151-
raw(name);
161+
raw(name.name);
152162
}
153163

154164
if (open.attributes) {
@@ -198,12 +208,13 @@ export default function jsxToHtmBabelPlugin({ types: t }, options = {}) {
198208
expressions.length = 0;
199209

200210
if (isFragment) {
201-
processChildren(path.node, '', true);
211+
processChildren(path.node, null, true);
202212
commit();
203213
const template = t.templateLiteral(quasis, expressions);
204214
const replacement = t.taggedTemplateExpression(tag, template);
205215
path.replaceWith(replacement);
206-
} else {
216+
}
217+
else {
207218
processNode(path.node, path, true);
208219
}
209220

test/babel-transform-jsx.test.mjs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,22 @@ describe('babel-plugin-transform-jsx-to-htm', () => {
6767
expect(
6868
compile('(<div>a</div>);')
6969
).toBe('html`<div>a</div>`;');
70+
71+
expect(
72+
compile('(<div$ />);')
73+
).toBe('html`<div$/>`;');
74+
75+
expect(
76+
compile('(<div$>a</div$>);')
77+
).toBe('html`<div$>a</div$>`;');
78+
79+
expect(
80+
compile('(<div_ />);')
81+
).toBe('html`<div_/>`;');
82+
83+
expect(
84+
compile('(<div_>a</div_>);')
85+
).toBe('html`<div_>a</div_>`;');
7086
});
7187

7288
test('single component element', () => {
@@ -77,6 +93,48 @@ describe('babel-plugin-transform-jsx-to-htm', () => {
7793
expect(
7894
compile('(<Foo>a</Foo>);')
7995
).toBe('html`<${Foo}>a</${Foo}>`;');
96+
97+
expect(
98+
compile('(<$ />);')
99+
).toBe('html`<${$}/>`;');
100+
101+
expect(
102+
compile('(<$>a</$>);')
103+
).toBe('html`<${$}>a</${$}>`;');
104+
105+
expect(
106+
compile('(<_ />);')
107+
).toBe('html`<${_}/>`;');
108+
109+
expect(
110+
compile('(<_>a</_>);')
111+
).toBe('html`<${_}>a</${_}>`;');
112+
113+
expect(
114+
compile('(<_foo />);')
115+
).toBe('html`<${_foo}/>`;');
116+
117+
expect(
118+
compile('(<_foo>a</_foo>);')
119+
).toBe('html`<${_foo}>a</${_foo}>`;');
120+
121+
expect(
122+
compile('(<$foo />);')
123+
).toBe('html`<${$foo}/>`;');
124+
125+
expect(
126+
compile('(<$foo>a</$foo>);')
127+
).toBe('html`<${$foo}>a</${$foo}>`;');
128+
});
129+
130+
test('dotted component element', () => {
131+
expect(
132+
compile('(<a.b.c />);')
133+
).toBe('html`<${a.b.c}/>`;');
134+
135+
expect(
136+
compile('(<a.b.c>a</a.b.c>);')
137+
).toBe('html`<${a.b.c}>a</${a.b.c}>`;');
80138
});
81139

82140
test('static text', () => {

0 commit comments

Comments
 (0)