@sigil-dev/compiler 0.6.10 → 0.6.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/babel/jsx/element.ts +526 -487
- package/test/jsx.test.ts +97 -78
package/package.json
CHANGED
package/src/babel/jsx/element.ts
CHANGED
|
@@ -6,515 +6,554 @@ import { processFragment } from "./fragment";
|
|
|
6
6
|
import { buildKeyedList, findKeyedMapExpr } from "./keyed-list";
|
|
7
7
|
import { buildTextNode } from "./text-node";
|
|
8
8
|
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
ATTR_MAP,
|
|
10
|
+
buildHydrationScope,
|
|
11
|
+
containsSignal,
|
|
12
|
+
getCreateElement,
|
|
13
|
+
isPrimitive,
|
|
14
14
|
} from "./utils";
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Add classList.add(hash) statement for scoped CSS.
|
|
18
18
|
*/
|
|
19
19
|
function addScopedClass(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
varName: string,
|
|
21
|
+
hash: string,
|
|
22
|
+
statements: t.Statement[],
|
|
23
23
|
): void {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
24
|
+
statements.push(
|
|
25
|
+
t.expressionStatement(
|
|
26
|
+
t.callExpression(
|
|
27
|
+
t.memberExpression(
|
|
28
|
+
t.memberExpression(t.identifier(varName), t.identifier("classList")),
|
|
29
|
+
t.identifier("add"),
|
|
30
|
+
),
|
|
31
|
+
[t.stringLiteral(hash)],
|
|
32
|
+
),
|
|
33
|
+
),
|
|
34
|
+
);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
export function processElement(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
38
|
+
node: t.JSXElement,
|
|
39
|
+
statements: t.Statement[],
|
|
40
|
+
genId: () => string,
|
|
41
|
+
signals: Set<string>,
|
|
42
|
+
hash?: string,
|
|
43
|
+
hydrate?: boolean,
|
|
44
|
+
nodesVar?: string,
|
|
45
|
+
parentVar?: string,
|
|
46
46
|
): string {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
const varName = genId();
|
|
48
|
+
const tag = (node.openingElement.name as t.JSXIdentifier).name;
|
|
49
|
+
const isComponent = /^[A-Z]/.test(tag);
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
if (isComponent) {
|
|
52
|
+
// Component: call the function with a props object
|
|
53
|
+
const propsObj = t.objectExpression([]);
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
55
|
+
// Add attributes to props (including spread)
|
|
56
|
+
for (const attr of node.openingElement.attributes) {
|
|
57
|
+
if (t.isJSXSpreadAttribute(attr)) {
|
|
58
|
+
// {…obj} — spread into props
|
|
59
|
+
propsObj.properties.push(t.spreadElement(attr.argument));
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (!t.isJSXAttribute(attr)) continue;
|
|
63
|
+
const attrName = (attr.name as t.JSXIdentifier).name;
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
65
|
+
if (t.isStringLiteral(attr.value)) {
|
|
66
|
+
propsObj.properties.push(
|
|
67
|
+
t.objectProperty(t.identifier(attrName), attr.value),
|
|
68
|
+
);
|
|
69
|
+
} else if (t.isJSXExpressionContainer(attr.value)) {
|
|
70
|
+
const expr = attr.value.expression as t.Expression;
|
|
71
|
+
propsObj.properties.push(
|
|
72
|
+
t.objectProperty(t.identifier(attrName), expr),
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
77
|
+
// Add children to props
|
|
78
|
+
const childrenExpr = collectChildren(
|
|
79
|
+
node,
|
|
80
|
+
statements,
|
|
81
|
+
genId,
|
|
82
|
+
signals,
|
|
83
|
+
hash,
|
|
84
|
+
hydrate,
|
|
85
|
+
nodesVar,
|
|
86
|
+
);
|
|
87
|
+
if (childrenExpr) {
|
|
88
|
+
propsObj.properties.push(
|
|
89
|
+
t.objectProperty(t.identifier("children"), childrenExpr),
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
92
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
93
|
+
// const _el0 = TagName({ ...props })
|
|
94
|
+
statements.push(
|
|
95
|
+
t.variableDeclaration("const", [
|
|
96
|
+
t.variableDeclarator(
|
|
97
|
+
t.identifier(varName),
|
|
98
|
+
t.callExpression(t.identifier(tag), [propsObj]),
|
|
99
|
+
),
|
|
100
|
+
]),
|
|
101
|
+
);
|
|
102
102
|
|
|
103
|
-
|
|
104
|
-
|
|
103
|
+
return varName;
|
|
104
|
+
}
|
|
105
105
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
106
|
+
// Native element: claim (hydrate) or create (dom)
|
|
107
|
+
const currentNodesVar = nodesVar ?? "__nodes";
|
|
108
|
+
statements.push(
|
|
109
|
+
t.variableDeclaration("const", [
|
|
110
|
+
t.variableDeclarator(
|
|
111
|
+
t.identifier(varName),
|
|
112
|
+
getCreateElement(
|
|
113
|
+
tag,
|
|
114
|
+
!!hydrate,
|
|
115
|
+
currentNodesVar,
|
|
116
|
+
hydrate ? parentVar : undefined,
|
|
117
|
+
),
|
|
118
|
+
),
|
|
119
|
+
]),
|
|
120
|
+
);
|
|
121
121
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
122
|
+
// In hydrate mode, scope to this element's children for descendants
|
|
123
|
+
let childNodesVar: string | undefined;
|
|
124
|
+
if (hydrate) {
|
|
125
|
+
childNodesVar = genId();
|
|
126
|
+
buildHydrationScope(varName, statements, childNodesVar);
|
|
127
|
+
}
|
|
128
128
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
129
|
+
// attributes
|
|
130
|
+
const seenAttrs = new Set<string>();
|
|
131
|
+
for (const attr of node.openingElement.attributes) {
|
|
132
|
+
if (t.isJSXSpreadAttribute(attr)) {
|
|
133
|
+
// {…obj} — spread onto native element
|
|
134
|
+
statements.push(
|
|
135
|
+
t.expressionStatement(
|
|
136
|
+
t.callExpression(t.identifier("Object.assign"), [
|
|
137
|
+
t.identifier(varName),
|
|
138
|
+
attr.argument as t.Expression,
|
|
139
|
+
]),
|
|
140
|
+
),
|
|
141
|
+
);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (!t.isJSXAttribute(attr)) continue;
|
|
145
|
+
const attrName = (attr.name as t.JSXIdentifier).name;
|
|
146
|
+
const realAttr = ATTR_MAP[attrName] ?? attrName;
|
|
147
147
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
148
|
+
if (seenAttrs.has(realAttr)) {
|
|
149
|
+
throw new TypeError(
|
|
150
|
+
`Duplicate attribute "${realAttr}" (via "${attrName}"). Use class or className, not both.`,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
seenAttrs.add(realAttr);
|
|
154
|
+
if (attrName === "use") {
|
|
155
|
+
// use={directive} or use={[directive, params]}
|
|
156
|
+
const expr = (attr.value as t.JSXExpressionContainer)
|
|
157
|
+
.expression as t.Expression;
|
|
158
|
+
statements.push(
|
|
159
|
+
t.expressionStatement(
|
|
160
|
+
t.callExpression(t.identifier("applyDirective"), [
|
|
161
|
+
t.identifier(varName),
|
|
162
|
+
expr,
|
|
163
|
+
]),
|
|
164
|
+
),
|
|
165
|
+
);
|
|
166
|
+
} else if (/^on[A-Z]/.test(attrName)) {
|
|
167
|
+
// Standard JSX: onClick, onSubmit, onMouseenter, etc.
|
|
168
|
+
// Convert camelCase to lowercase: onClick -> click, onMouseenter -> mouseenter
|
|
169
|
+
const event = attrName.slice(2).toLowerCase();
|
|
170
|
+
const handler = (attr.value as t.JSXExpressionContainer)
|
|
171
|
+
.expression as t.Expression;
|
|
172
|
+
statements.push(
|
|
173
|
+
t.expressionStatement(
|
|
174
|
+
t.callExpression(
|
|
175
|
+
t.memberExpression(
|
|
176
|
+
t.identifier(varName),
|
|
177
|
+
t.identifier("addEventListener"),
|
|
178
|
+
),
|
|
179
|
+
[t.stringLiteral(event), handler],
|
|
180
|
+
),
|
|
181
|
+
),
|
|
182
|
+
);
|
|
183
|
+
} else if (attrName.startsWith("bind")) {
|
|
184
|
+
buildBind(varName, attrName, attr, statements);
|
|
185
|
+
} else if (t.isStringLiteral(attr.value)) {
|
|
186
|
+
statements.push(
|
|
187
|
+
t.expressionStatement(
|
|
188
|
+
t.assignmentExpression(
|
|
189
|
+
"=",
|
|
190
|
+
t.memberExpression(t.identifier(varName), t.identifier(realAttr)),
|
|
191
|
+
attr.value,
|
|
192
|
+
),
|
|
193
|
+
),
|
|
194
|
+
);
|
|
195
|
+
} else if (t.isJSXExpressionContainer(attr.value)) {
|
|
196
|
+
const expr = attr.value.expression as t.Expression;
|
|
197
|
+
// style={{ color: "red", fontSize: val }} — per-property assignment
|
|
198
|
+
if (attrName === "style" && t.isObjectExpression(expr)) {
|
|
199
|
+
for (const prop of expr.properties) {
|
|
200
|
+
if (!t.isObjectProperty(prop)) continue;
|
|
201
|
+
const key = t.isIdentifier(prop.key)
|
|
202
|
+
? prop.key.name
|
|
203
|
+
: t.isStringLiteral(prop.key)
|
|
204
|
+
? prop.key.value
|
|
205
|
+
: null;
|
|
206
|
+
if (!key) continue;
|
|
207
|
+
const val = prop.value as t.Expression;
|
|
208
|
+
const styleProp = t.memberExpression(
|
|
209
|
+
t.memberExpression(t.identifier(varName), t.identifier("style")),
|
|
210
|
+
t.identifier(key),
|
|
211
|
+
);
|
|
212
|
+
if (containsSignal(val, signals)) {
|
|
213
|
+
statements.push(
|
|
214
|
+
t.expressionStatement(
|
|
215
|
+
t.callExpression(t.identifier("createEffect"), [
|
|
216
|
+
t.arrowFunctionExpression(
|
|
217
|
+
[],
|
|
218
|
+
t.blockStatement([
|
|
219
|
+
t.expressionStatement(
|
|
220
|
+
t.assignmentExpression("=", styleProp, val),
|
|
221
|
+
),
|
|
222
|
+
]),
|
|
223
|
+
),
|
|
224
|
+
]),
|
|
225
|
+
),
|
|
226
|
+
);
|
|
227
|
+
} else {
|
|
228
|
+
statements.push(
|
|
229
|
+
t.expressionStatement(
|
|
230
|
+
t.assignmentExpression("=", styleProp, val),
|
|
231
|
+
),
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// done with this attribute
|
|
236
|
+
} else if (containsSignal(expr, signals)) {
|
|
237
|
+
// Dynamic class with scoped CSS: use classList.value to preserve hash
|
|
238
|
+
if (attrName === "class" && hash) {
|
|
239
|
+
statements.push(
|
|
240
|
+
t.expressionStatement(
|
|
241
|
+
t.callExpression(t.identifier("createEffect"), [
|
|
242
|
+
t.arrowFunctionExpression(
|
|
243
|
+
[],
|
|
244
|
+
t.blockStatement([
|
|
245
|
+
t.expressionStatement(
|
|
246
|
+
t.assignmentExpression(
|
|
247
|
+
"=",
|
|
248
|
+
t.memberExpression(
|
|
249
|
+
t.memberExpression(
|
|
250
|
+
t.identifier(varName),
|
|
251
|
+
t.identifier("classList"),
|
|
252
|
+
),
|
|
253
|
+
t.identifier("value"),
|
|
254
|
+
),
|
|
255
|
+
expr,
|
|
256
|
+
),
|
|
257
|
+
),
|
|
258
|
+
t.expressionStatement(
|
|
259
|
+
t.callExpression(
|
|
260
|
+
t.memberExpression(
|
|
261
|
+
t.memberExpression(
|
|
262
|
+
t.identifier(varName),
|
|
263
|
+
t.identifier("classList"),
|
|
264
|
+
),
|
|
265
|
+
t.identifier("add"),
|
|
266
|
+
),
|
|
267
|
+
[t.stringLiteral(hash)],
|
|
268
|
+
),
|
|
269
|
+
),
|
|
270
|
+
]),
|
|
271
|
+
),
|
|
272
|
+
]),
|
|
273
|
+
),
|
|
274
|
+
);
|
|
275
|
+
} else {
|
|
276
|
+
// createEffect(() => varName.realAttr = expr)
|
|
277
|
+
statements.push(
|
|
278
|
+
t.expressionStatement(
|
|
279
|
+
t.callExpression(t.identifier("createEffect"), [
|
|
280
|
+
t.arrowFunctionExpression(
|
|
281
|
+
[],
|
|
282
|
+
t.blockStatement([
|
|
283
|
+
t.expressionStatement(
|
|
284
|
+
t.assignmentExpression(
|
|
285
|
+
"=",
|
|
286
|
+
t.memberExpression(
|
|
287
|
+
t.identifier(varName),
|
|
288
|
+
t.identifier(realAttr),
|
|
289
|
+
),
|
|
290
|
+
expr,
|
|
291
|
+
),
|
|
292
|
+
),
|
|
293
|
+
]),
|
|
294
|
+
),
|
|
295
|
+
]),
|
|
296
|
+
),
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
} else {
|
|
300
|
+
// varName.realAttr = expr
|
|
301
|
+
statements.push(
|
|
302
|
+
t.expressionStatement(
|
|
303
|
+
t.assignmentExpression(
|
|
304
|
+
"=",
|
|
305
|
+
t.memberExpression(t.identifier(varName), t.identifier(realAttr)),
|
|
306
|
+
expr,
|
|
307
|
+
),
|
|
308
|
+
),
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
274
313
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
314
|
+
// Add scoped CSS hash to classList (if hash is present)
|
|
315
|
+
if (hash) {
|
|
316
|
+
addScopedClass(varName, hash, statements);
|
|
317
|
+
}
|
|
279
318
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
319
|
+
// children
|
|
320
|
+
for (const child of node.children) {
|
|
321
|
+
if (t.isJSXElement(child)) {
|
|
322
|
+
const childVar = processElement(
|
|
323
|
+
child,
|
|
324
|
+
statements,
|
|
325
|
+
genId,
|
|
326
|
+
signals,
|
|
327
|
+
hash,
|
|
328
|
+
hydrate,
|
|
329
|
+
childNodesVar,
|
|
330
|
+
hydrate ? varName : undefined,
|
|
331
|
+
);
|
|
332
|
+
if (!hydrate) {
|
|
333
|
+
statements.push(
|
|
334
|
+
t.expressionStatement(
|
|
335
|
+
t.callExpression(
|
|
336
|
+
t.memberExpression(t.identifier(varName), t.identifier("append")),
|
|
337
|
+
[t.identifier(childVar)],
|
|
338
|
+
),
|
|
339
|
+
),
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
} else if (t.isJSXFragment(child)) {
|
|
343
|
+
const childVar = processFragment(
|
|
344
|
+
child,
|
|
345
|
+
statements,
|
|
346
|
+
genId,
|
|
347
|
+
signals,
|
|
348
|
+
hash,
|
|
349
|
+
hydrate,
|
|
350
|
+
childNodesVar,
|
|
351
|
+
hydrate ? varName : undefined,
|
|
352
|
+
);
|
|
353
|
+
if (!hydrate) {
|
|
354
|
+
statements.push(
|
|
355
|
+
t.expressionStatement(
|
|
356
|
+
t.callExpression(
|
|
357
|
+
t.memberExpression(t.identifier(varName), t.identifier("append")),
|
|
358
|
+
[t.identifier(childVar)],
|
|
359
|
+
),
|
|
360
|
+
),
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
} else if (t.isJSXText(child)) {
|
|
364
|
+
const text = child.value;
|
|
365
|
+
if (!text.trim()) continue;
|
|
366
|
+
if (hydrate) {
|
|
367
|
+
// SSR hydration: text already in DOM from SSR, do nothing.
|
|
368
|
+
// SPA navigation: childNodesVar pool is empty, create text node.
|
|
369
|
+
const poolLen = t.memberExpression(
|
|
370
|
+
t.identifier(childNodesVar!),
|
|
371
|
+
t.identifier("length"),
|
|
372
|
+
);
|
|
373
|
+
statements.push(
|
|
374
|
+
t.ifStatement(
|
|
375
|
+
t.binaryExpression("===", poolLen, t.numericLiteral(0)),
|
|
376
|
+
t.expressionStatement(
|
|
377
|
+
t.callExpression(
|
|
378
|
+
t.memberExpression(
|
|
379
|
+
t.identifier(varName),
|
|
380
|
+
t.identifier("append"),
|
|
381
|
+
),
|
|
382
|
+
[
|
|
383
|
+
t.callExpression(
|
|
384
|
+
t.memberExpression(
|
|
385
|
+
t.identifier("document"),
|
|
386
|
+
t.identifier("createTextNode"),
|
|
387
|
+
),
|
|
388
|
+
[t.stringLiteral(text.replace(/\s*\n\s*/g, " "))],
|
|
389
|
+
),
|
|
390
|
+
],
|
|
391
|
+
),
|
|
392
|
+
),
|
|
393
|
+
),
|
|
394
|
+
);
|
|
395
|
+
} else {
|
|
396
|
+
statements.push(
|
|
397
|
+
t.expressionStatement(
|
|
398
|
+
t.callExpression(
|
|
399
|
+
t.memberExpression(t.identifier(varName), t.identifier("append")),
|
|
400
|
+
[
|
|
401
|
+
t.callExpression(
|
|
402
|
+
t.memberExpression(
|
|
403
|
+
t.identifier("document"),
|
|
404
|
+
t.identifier("createTextNode"),
|
|
405
|
+
),
|
|
406
|
+
[t.stringLiteral(text)],
|
|
407
|
+
),
|
|
408
|
+
],
|
|
409
|
+
),
|
|
410
|
+
),
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
} else if (t.isJSXExpressionContainer(child)) {
|
|
414
|
+
if (t.isJSXEmptyExpression(child.expression)) continue;
|
|
415
|
+
const expr = child.expression as t.Expression;
|
|
416
|
+
if (t.isJSXElement(expr)) {
|
|
417
|
+
const childVar = processElement(
|
|
418
|
+
expr,
|
|
419
|
+
statements,
|
|
420
|
+
genId,
|
|
421
|
+
signals,
|
|
422
|
+
hash,
|
|
423
|
+
hydrate,
|
|
424
|
+
childNodesVar,
|
|
425
|
+
hydrate ? varName : undefined,
|
|
426
|
+
);
|
|
427
|
+
if (!hydrate) {
|
|
428
|
+
statements.push(
|
|
429
|
+
t.expressionStatement(
|
|
430
|
+
t.callExpression(
|
|
431
|
+
t.memberExpression(
|
|
432
|
+
t.identifier(varName),
|
|
433
|
+
t.identifier("append"),
|
|
434
|
+
),
|
|
435
|
+
[t.identifier(childVar)],
|
|
436
|
+
),
|
|
437
|
+
),
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
} else if (t.isJSXFragment(expr)) {
|
|
441
|
+
const childVar = processFragment(
|
|
442
|
+
expr,
|
|
443
|
+
statements,
|
|
444
|
+
genId,
|
|
445
|
+
signals,
|
|
446
|
+
hash,
|
|
447
|
+
hydrate,
|
|
448
|
+
childNodesVar,
|
|
449
|
+
hydrate ? varName : undefined,
|
|
450
|
+
);
|
|
451
|
+
if (!hydrate) {
|
|
452
|
+
statements.push(
|
|
453
|
+
t.expressionStatement(
|
|
454
|
+
t.callExpression(
|
|
455
|
+
t.memberExpression(
|
|
456
|
+
t.identifier(varName),
|
|
457
|
+
t.identifier("append"),
|
|
458
|
+
),
|
|
459
|
+
[t.identifier(childVar)],
|
|
460
|
+
),
|
|
461
|
+
),
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
} else if (containsSignal(expr, signals)) {
|
|
465
|
+
if (isPrimitive(expr)) {
|
|
466
|
+
// fast path: text node + textContent
|
|
467
|
+
buildTextNode(
|
|
468
|
+
varName,
|
|
469
|
+
expr,
|
|
470
|
+
statements,
|
|
471
|
+
genId,
|
|
472
|
+
hydrate,
|
|
473
|
+
childNodesVar,
|
|
474
|
+
hydrate ? varName : undefined,
|
|
475
|
+
);
|
|
476
|
+
} else {
|
|
477
|
+
// Check for keyed list pattern
|
|
478
|
+
const keyed = findKeyedMapExpr(expr, signals);
|
|
479
|
+
if (keyed) {
|
|
480
|
+
buildKeyedList(
|
|
481
|
+
varName,
|
|
482
|
+
keyed,
|
|
483
|
+
statements,
|
|
484
|
+
genId,
|
|
485
|
+
signals,
|
|
486
|
+
processElement,
|
|
487
|
+
hash,
|
|
488
|
+
hydrate,
|
|
489
|
+
childNodesVar,
|
|
490
|
+
hydrate ? varName : undefined,
|
|
491
|
+
);
|
|
492
|
+
} else {
|
|
493
|
+
buildAnchorMount(
|
|
494
|
+
varName,
|
|
495
|
+
expr,
|
|
496
|
+
statements,
|
|
497
|
+
genId,
|
|
498
|
+
hydrate,
|
|
499
|
+
childNodesVar,
|
|
500
|
+
hydrate ? varName : undefined,
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
} else {
|
|
505
|
+
// non-reactive: set once
|
|
506
|
+
if (hydrate) {
|
|
507
|
+
const claimCommentArgs: t.Expression[] = [
|
|
508
|
+
t.identifier(childNodesVar!),
|
|
509
|
+
t.stringLiteral("g"),
|
|
510
|
+
];
|
|
511
|
+
if (varName) claimCommentArgs.push(t.identifier(varName));
|
|
512
|
+
const poolLen = t.memberExpression(
|
|
513
|
+
t.identifier(childNodesVar!),
|
|
514
|
+
t.identifier("length"),
|
|
515
|
+
);
|
|
516
|
+
// Pool has elements (initial load): SSR content already in DOM, just consume delimiters
|
|
517
|
+
const claimG = t.expressionStatement(
|
|
518
|
+
t.callExpression(t.identifier("claimComment"), claimCommentArgs),
|
|
519
|
+
);
|
|
520
|
+
const claimSlashG = (() => {
|
|
521
|
+
const args: t.Expression[] = [
|
|
522
|
+
t.identifier(childNodesVar!),
|
|
523
|
+
t.stringLiteral("/g"),
|
|
524
|
+
];
|
|
525
|
+
if (varName) args.push(t.identifier(varName));
|
|
526
|
+
return t.expressionStatement(
|
|
527
|
+
t.callExpression(t.identifier("claimComment"), args),
|
|
528
|
+
);
|
|
529
|
+
})();
|
|
530
|
+
// Pool empty (SPA nav): create and insert fresh elements
|
|
531
|
+
const insertCall = t.expressionStatement(
|
|
532
|
+
t.callExpression(t.identifier("insert"), [
|
|
533
|
+
t.identifier(varName),
|
|
534
|
+
expr,
|
|
535
|
+
]),
|
|
536
|
+
);
|
|
537
|
+
statements.push(
|
|
538
|
+
t.ifStatement(
|
|
539
|
+
t.binaryExpression(">", poolLen, t.numericLiteral(0)),
|
|
540
|
+
t.blockStatement([claimG, claimSlashG]),
|
|
541
|
+
t.blockStatement([insertCall]),
|
|
542
|
+
),
|
|
543
|
+
);
|
|
544
|
+
} else {
|
|
545
|
+
statements.push(
|
|
546
|
+
t.expressionStatement(
|
|
547
|
+
t.callExpression(t.identifier("insert"), [
|
|
548
|
+
t.identifier(varName),
|
|
549
|
+
expr,
|
|
550
|
+
]),
|
|
551
|
+
),
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
518
557
|
|
|
519
|
-
|
|
558
|
+
return varName;
|
|
520
559
|
}
|
package/test/jsx.test.ts
CHANGED
|
@@ -2,80 +2,99 @@ import { describe, expect, test } from "bun:test";
|
|
|
2
2
|
import { transform } from "./helpers/transform.ts";
|
|
3
3
|
|
|
4
4
|
describe("JSX", () => {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
test("single static element", () => {
|
|
6
|
+
const result = transform(`const el = <div />;`);
|
|
7
|
+
expect(result).toContain('document.createElement("div")');
|
|
8
|
+
});
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
test("nested static elements", () => {
|
|
11
|
+
const result = transform(`const el = <div><span /></div>;`);
|
|
12
|
+
expect(result).toContain('document.createElement("div")');
|
|
13
|
+
expect(result).toContain('document.createElement("span")');
|
|
14
|
+
expect(result).toContain(".append(");
|
|
15
|
+
});
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
test("static string attribute", () => {
|
|
18
|
+
const result = transform(`const el = <div class="app" />;`);
|
|
19
|
+
expect(result).toContain('_el0.className = "app"');
|
|
20
|
+
});
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
test("attribute without mapping", () => {
|
|
23
|
+
const result = transform(`const el = <input type="text" />;`);
|
|
24
|
+
expect(result).toContain('_el0.type = "text"');
|
|
25
|
+
});
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
test("expression attribute", () => {
|
|
28
|
+
const result = transform(`const el = <div id={myId} />;`);
|
|
29
|
+
expect(result).toContain("_el0.id = myId");
|
|
30
|
+
});
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
describe("Reactivity", () => {
|
|
33
|
+
test("dynamic attribute wraps in createEffect", () => {
|
|
34
|
+
const result = transform(`
|
|
35
35
|
let count = $state(0);
|
|
36
36
|
const el = <div id={count} />;
|
|
37
37
|
`);
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
expect(result).toContain("createEffect(");
|
|
39
|
+
expect(result).toContain("_el0.id = count()");
|
|
40
|
+
});
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
test("static expression attribute does not wrap in createEffect", () => {
|
|
43
|
+
const result = transform(`
|
|
44
44
|
const myId = "app";
|
|
45
45
|
const el = <div id={myId} />;
|
|
46
46
|
`);
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
});
|
|
47
|
+
expect(result).not.toContain("createEffect(");
|
|
48
|
+
expect(result).toContain("_el0.id = myId");
|
|
49
|
+
});
|
|
51
50
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
51
|
+
test("style object compiles to per-property assignment", () => {
|
|
52
|
+
const code = transform(`
|
|
53
|
+
const el = <div style={{ color: "red", fontSize: "16px" }} />;
|
|
54
|
+
`);
|
|
55
|
+
expect(code).toContain('.style.color = "red"');
|
|
56
|
+
expect(code).toContain('.style.fontSize = "16px"');
|
|
57
|
+
expect(code).not.toContain("[object Object]");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("reactive style property wraps in createEffect", () => {
|
|
61
|
+
const code = transform(`
|
|
62
|
+
let size = $state(16);
|
|
63
|
+
const el = <div style={{ fontSize: size + "px", color: "red" }} />;
|
|
64
|
+
`);
|
|
65
|
+
expect(code).toContain("createEffect");
|
|
66
|
+
expect(code).toContain(".style.fontSize");
|
|
67
|
+
expect(code).toContain('.style.color = "red"');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
58
70
|
|
|
59
|
-
|
|
60
|
-
|
|
71
|
+
describe("Children", () => {
|
|
72
|
+
// text children
|
|
73
|
+
test("static text child", () => {
|
|
74
|
+
const result = transform(`const el = <div>hello</div>;`);
|
|
75
|
+
expect(result).toContain('createTextNode("hello")');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("dynamic text child", () => {
|
|
79
|
+
const result = transform(`
|
|
61
80
|
let count = $state(0);
|
|
62
81
|
const el = <div>{count}</div>;
|
|
63
82
|
`);
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
83
|
+
expect(result).toContain("createEffect(");
|
|
84
|
+
expect(result).toContain("count()");
|
|
85
|
+
});
|
|
67
86
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
87
|
+
// event handlers
|
|
88
|
+
test("onClick handler", () => {
|
|
89
|
+
const result = transform(
|
|
90
|
+
`const el = <button onClick={() => {}}>click</button>;`,
|
|
91
|
+
);
|
|
92
|
+
expect(result).toContain('addEventListener("click"');
|
|
93
|
+
});
|
|
75
94
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
95
|
+
// combined
|
|
96
|
+
test("counter shape", () => {
|
|
97
|
+
const result = transform(`
|
|
79
98
|
let count = $state(0);
|
|
80
99
|
const el = (
|
|
81
100
|
<div>
|
|
@@ -84,25 +103,25 @@ describe("JSX", () => {
|
|
|
84
103
|
</div>
|
|
85
104
|
);
|
|
86
105
|
`);
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
106
|
+
expect(result).toContain("createSignal(0)");
|
|
107
|
+
expect(result).toContain('addEventListener("click"');
|
|
108
|
+
expect(result).toContain("createEffect(");
|
|
109
|
+
expect(result).toContain("createTextNode");
|
|
110
|
+
});
|
|
111
|
+
});
|
|
93
112
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
113
|
+
describe("Events", () => {
|
|
114
|
+
test("onClick handler", () => {
|
|
115
|
+
const result = transform(
|
|
116
|
+
`const el = <button onClick={() => {}}>click</button>;`,
|
|
117
|
+
);
|
|
118
|
+
expect(result).toContain('addEventListener("click"');
|
|
119
|
+
});
|
|
120
|
+
});
|
|
102
121
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
122
|
+
describe("Integration", () => {
|
|
123
|
+
test("counter shape", () => {
|
|
124
|
+
const result = transform(`
|
|
106
125
|
let count = $state(0);
|
|
107
126
|
const el = (
|
|
108
127
|
<div>
|
|
@@ -111,10 +130,10 @@ describe("JSX", () => {
|
|
|
111
130
|
</div>
|
|
112
131
|
);
|
|
113
132
|
`);
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
133
|
+
expect(result).toContain("createSignal(0)");
|
|
134
|
+
expect(result).toContain('addEventListener("click"');
|
|
135
|
+
expect(result).toContain("createEffect(");
|
|
136
|
+
expect(result).toContain("createTextNode");
|
|
137
|
+
});
|
|
138
|
+
});
|
|
120
139
|
});
|