@sigil-dev/compiler 0.9.2 → 0.9.3
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/README.md +162 -162
- package/index.ts +4 -4
- package/jsr.json +8 -8
- package/package.json +1 -1
- package/src/babel/handlers/dom/derived.ts +62 -62
- package/src/babel/handlers/dom/effect.ts +15 -15
- package/src/babel/handlers/dom/globals.ts +105 -105
- package/src/babel/handlers/dom/state.ts +107 -104
- package/src/babel/handlers/ssr/derived.ts +27 -27
- package/src/babel/handlers/ssr/state.ts +11 -11
- package/src/babel/index.ts +594 -589
- package/src/babel/jsx/anchor-mount.ts +389 -389
- package/src/babel/jsx/children.ts +124 -124
- package/src/babel/jsx/element.ts +831 -831
- package/src/babel/jsx/fragment.ts +282 -282
- package/src/babel/jsx/index.ts +2 -2
- package/src/babel/jsx/keyed-list.ts +278 -278
- package/src/babel/jsx/ssr.ts +319 -319
- package/src/babel/jsx/text-node.ts +192 -192
- package/src/babel/jsx/utils.ts +179 -179
- package/src/babel/util/bind.ts +139 -139
- package/src/babel/util/css.ts +151 -151
- package/src/babel/util/dead-code.ts +28 -28
- package/src/babel/util/helpers.ts +46 -46
- package/src/babel/util/magic.ts +11 -11
- package/src/bun-plugin.ts +92 -90
- package/src/vite/index.ts +58 -58
- package/test/async-components.test.ts +24 -24
- package/test/components.test.ts +129 -129
- package/test/containsSignal.test.ts +288 -288
- package/test/fragments.test.ts +90 -90
- package/test/helpers/transform.ts +45 -48
- package/test/hydration.test.ts +147 -147
- package/test/jsx.test.ts +197 -197
- package/test/keyed-lists.test.ts +88 -88
- package/test/reactivity.test.ts +225 -225
- package/test/scoped-css.test.ts +129 -129
- package/test/ssr.test.ts +130 -130
- package/test/tree-shaking.test.ts +78 -78
- package/tsconfig.json +30 -30
package/src/babel/jsx/element.ts
CHANGED
|
@@ -1,831 +1,831 @@
|
|
|
1
|
-
import { types as t } from "@babel/core";
|
|
2
|
-
//@ts-expect-error Not our problem babel forgot tp export types.
|
|
3
|
-
import { generate } from "@babel/generator";
|
|
4
|
-
import {
|
|
5
|
-
handleGlobalComponent,
|
|
6
|
-
isGlobalComponent,
|
|
7
|
-
} from "../handlers/dom/globals";
|
|
8
|
-
import { buildBind, buildProxyBind } from "../util/bind";
|
|
9
|
-
import { buildAnchorMount } from "./anchor-mount";
|
|
10
|
-
import { collectChildren } from "./children";
|
|
11
|
-
import { processFragment } from "./fragment";
|
|
12
|
-
import { buildKeyedList, findKeyedMapExpr } from "./keyed-list";
|
|
13
|
-
import { buildTextNode } from "./text-node";
|
|
14
|
-
import {
|
|
15
|
-
ATTR_MAP,
|
|
16
|
-
buildHydrationScope,
|
|
17
|
-
containsSignal,
|
|
18
|
-
getCreateElement,
|
|
19
|
-
isPrimitive,
|
|
20
|
-
} from "./utils";
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Build a property-access expression for a JSX attribute name.
|
|
24
|
-
* Dashed names (data-*, aria-*) require computed access (e.g. el["data-id"])
|
|
25
|
-
* because t.identifier() rejects names with dashes.
|
|
26
|
-
*/
|
|
27
|
-
function attrAccess(varName: string, attrName: string): t.MemberExpression {
|
|
28
|
-
if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(attrName)) {
|
|
29
|
-
return t.memberExpression(t.identifier(varName), t.identifier(attrName));
|
|
30
|
-
}
|
|
31
|
-
return t.memberExpression(
|
|
32
|
-
t.identifier(varName),
|
|
33
|
-
t.stringLiteral(attrName),
|
|
34
|
-
true,
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Build an object property key (identifier for valid names, string literal for dashed).
|
|
40
|
-
*/
|
|
41
|
-
function attrKey(attrName: string): t.Identifier | t.StringLiteral {
|
|
42
|
-
if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(attrName)) {
|
|
43
|
-
return t.identifier(attrName);
|
|
44
|
-
}
|
|
45
|
-
return t.stringLiteral(attrName);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Add classList.add(hash) statement for scoped CSS.
|
|
50
|
-
*/
|
|
51
|
-
function addScopedClass(
|
|
52
|
-
varName: string,
|
|
53
|
-
hash: string,
|
|
54
|
-
statements: t.Statement[],
|
|
55
|
-
): void {
|
|
56
|
-
statements.push(
|
|
57
|
-
t.expressionStatement(
|
|
58
|
-
t.callExpression(
|
|
59
|
-
t.memberExpression(
|
|
60
|
-
t.memberExpression(t.identifier(varName), t.identifier("classList")),
|
|
61
|
-
t.identifier("add"),
|
|
62
|
-
),
|
|
63
|
-
[t.stringLiteral(hash)],
|
|
64
|
-
),
|
|
65
|
-
),
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function processElement(
|
|
70
|
-
node: t.JSXElement,
|
|
71
|
-
statements: t.Statement[],
|
|
72
|
-
genId: () => string,
|
|
73
|
-
signals: Set<string>,
|
|
74
|
-
hash?: string,
|
|
75
|
-
hydrate?: boolean,
|
|
76
|
-
nodesVar?: string,
|
|
77
|
-
parentVar?: string,
|
|
78
|
-
): string {
|
|
79
|
-
const varName = genId();
|
|
80
|
-
const tag = (node.openingElement.name as t.JSXIdentifier).name;
|
|
81
|
-
const isComponent = /^[A-Z]/.test(tag);
|
|
82
|
-
|
|
83
|
-
if (isComponent) {
|
|
84
|
-
// Global components: Window, Document, Body
|
|
85
|
-
if (isGlobalComponent(tag)) {
|
|
86
|
-
return handleGlobalComponent(tag, node, statements, genId);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Component: call the function with a props object
|
|
90
|
-
const propsObj = t.objectExpression([]);
|
|
91
|
-
|
|
92
|
-
// Add attributes to props (including spread)
|
|
93
|
-
for (const attr of node.openingElement.attributes) {
|
|
94
|
-
if (t.isJSXSpreadAttribute(attr)) {
|
|
95
|
-
// {…obj} — spread into props
|
|
96
|
-
propsObj.properties.push(t.spreadElement(attr.argument));
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
if (!t.isJSXAttribute(attr)) continue;
|
|
100
|
-
const attrName = (attr.name as t.JSXIdentifier).name;
|
|
101
|
-
|
|
102
|
-
if (t.isStringLiteral(attr.value)) {
|
|
103
|
-
propsObj.properties.push(
|
|
104
|
-
t.objectProperty(attrKey(attrName), attr.value),
|
|
105
|
-
);
|
|
106
|
-
} else if (t.isJSXExpressionContainer(attr.value)) {
|
|
107
|
-
const expr = attr.value.expression as t.Expression;
|
|
108
|
-
propsObj.properties.push(t.objectProperty(attrKey(attrName), expr));
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Add children to props
|
|
113
|
-
const childrenExpr = collectChildren(
|
|
114
|
-
node,
|
|
115
|
-
statements,
|
|
116
|
-
genId,
|
|
117
|
-
signals,
|
|
118
|
-
hash,
|
|
119
|
-
hydrate,
|
|
120
|
-
nodesVar,
|
|
121
|
-
);
|
|
122
|
-
if (childrenExpr) {
|
|
123
|
-
propsObj.properties.push(
|
|
124
|
-
t.objectProperty(t.identifier("children"), childrenExpr),
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const componentCall = t.callExpression(t.identifier(tag), [propsObj]);
|
|
129
|
-
|
|
130
|
-
if (hydrate && nodesVar) {
|
|
131
|
-
// Push the parent's child pool before calling the component so that
|
|
132
|
-
// getHydrationNodes() inside the component returns the right scope.
|
|
133
|
-
// In SSR hydration the component claims its root node from that pool
|
|
134
|
-
// (it's already in the DOM). In empty-pool / SPA mode the component
|
|
135
|
-
// creates a fresh node, so we explicitly insert it when the pool is empty.
|
|
136
|
-
statements.push(
|
|
137
|
-
t.expressionStatement(
|
|
138
|
-
t.callExpression(t.identifier("pushHydrationNodes"), [
|
|
139
|
-
t.identifier(nodesVar),
|
|
140
|
-
]),
|
|
141
|
-
),
|
|
142
|
-
);
|
|
143
|
-
statements.push(
|
|
144
|
-
t.variableDeclaration("const", [
|
|
145
|
-
t.variableDeclarator(t.identifier(varName), componentCall),
|
|
146
|
-
]),
|
|
147
|
-
);
|
|
148
|
-
statements.push(
|
|
149
|
-
t.expressionStatement(
|
|
150
|
-
t.callExpression(t.identifier("popHydrationNodes"), []),
|
|
151
|
-
),
|
|
152
|
-
);
|
|
153
|
-
if (parentVar) {
|
|
154
|
-
// Insert only when the pool was empty (fresh DOM); if the pool had
|
|
155
|
-
// nodes the component already claimed its SSR element (already in DOM).
|
|
156
|
-
statements.push(
|
|
157
|
-
t.ifStatement(
|
|
158
|
-
t.binaryExpression(
|
|
159
|
-
"===",
|
|
160
|
-
t.memberExpression(
|
|
161
|
-
t.identifier(nodesVar),
|
|
162
|
-
t.identifier("length"),
|
|
163
|
-
),
|
|
164
|
-
t.numericLiteral(0),
|
|
165
|
-
),
|
|
166
|
-
t.expressionStatement(
|
|
167
|
-
t.callExpression(t.identifier("insert"), [
|
|
168
|
-
t.identifier(parentVar),
|
|
169
|
-
t.identifier(varName),
|
|
170
|
-
]),
|
|
171
|
-
),
|
|
172
|
-
),
|
|
173
|
-
);
|
|
174
|
-
}
|
|
175
|
-
} else {
|
|
176
|
-
statements.push(
|
|
177
|
-
t.variableDeclaration("const", [
|
|
178
|
-
t.variableDeclarator(t.identifier(varName), componentCall),
|
|
179
|
-
]),
|
|
180
|
-
);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
return varName;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// Native element: claim (hydrate) or create (dom)
|
|
187
|
-
const currentNodesVar = nodesVar ?? "__nodes";
|
|
188
|
-
statements.push(
|
|
189
|
-
t.variableDeclaration("const", [
|
|
190
|
-
t.variableDeclarator(
|
|
191
|
-
t.identifier(varName),
|
|
192
|
-
getCreateElement(
|
|
193
|
-
tag,
|
|
194
|
-
!!hydrate,
|
|
195
|
-
currentNodesVar,
|
|
196
|
-
hydrate ? parentVar : undefined,
|
|
197
|
-
),
|
|
198
|
-
),
|
|
199
|
-
]),
|
|
200
|
-
);
|
|
201
|
-
|
|
202
|
-
// In hydrate mode, scope to this element's children for descendants
|
|
203
|
-
let childNodesVar: string | undefined;
|
|
204
|
-
if (hydrate) {
|
|
205
|
-
childNodesVar = genId();
|
|
206
|
-
buildHydrationScope(varName, statements, childNodesVar);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// attributes
|
|
210
|
-
const seenAttrs = new Set<string>();
|
|
211
|
-
for (const attr of node.openingElement.attributes) {
|
|
212
|
-
if (t.isJSXSpreadAttribute(attr)) {
|
|
213
|
-
// {…obj} — spread onto native element
|
|
214
|
-
statements.push(
|
|
215
|
-
t.expressionStatement(
|
|
216
|
-
t.callExpression(t.identifier("Object.assign"), [
|
|
217
|
-
t.identifier(varName),
|
|
218
|
-
attr.argument as t.Expression,
|
|
219
|
-
]),
|
|
220
|
-
),
|
|
221
|
-
);
|
|
222
|
-
continue;
|
|
223
|
-
}
|
|
224
|
-
if (!t.isJSXAttribute(attr)) continue;
|
|
225
|
-
let attrName: string;
|
|
226
|
-
let attrNamespace: string | undefined;
|
|
227
|
-
if (t.isJSXNamespacedName(attr.name)) {
|
|
228
|
-
attrNamespace = attr.name.namespace.name;
|
|
229
|
-
attrName = attr.name.name.name;
|
|
230
|
-
} else {
|
|
231
|
-
attrName = (attr.name as t.JSXIdentifier).name;
|
|
232
|
-
// bindValue -> bind:value, bindChecked -> bind:checked
|
|
233
|
-
if (/^bind[A-Z]/.test(attrName)) {
|
|
234
|
-
attrNamespace = "bind";
|
|
235
|
-
attrName = attrName.slice(4).toLowerCase();
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
const realAttr = ATTR_MAP[attrName] ?? attrName;
|
|
239
|
-
|
|
240
|
-
if (seenAttrs.has(realAttr)) {
|
|
241
|
-
throw new TypeError(
|
|
242
|
-
`Duplicate attribute "${realAttr}" (via "${attrName}"). Use class or className, not both.`,
|
|
243
|
-
);
|
|
244
|
-
}
|
|
245
|
-
seenAttrs.add(realAttr);
|
|
246
|
-
if (attrName === "use") {
|
|
247
|
-
// use={directive} or use={[directive, params]}
|
|
248
|
-
const expr = (attr.value as t.JSXExpressionContainer)
|
|
249
|
-
.expression as t.Expression;
|
|
250
|
-
statements.push(
|
|
251
|
-
t.expressionStatement(
|
|
252
|
-
t.callExpression(t.identifier("applyDirective"), [
|
|
253
|
-
t.identifier(varName),
|
|
254
|
-
expr,
|
|
255
|
-
]),
|
|
256
|
-
),
|
|
257
|
-
);
|
|
258
|
-
} else if (/^on[A-Z]/.test(attrName)) {
|
|
259
|
-
// Standard JSX: onClick, onSubmit, onMouseenter, etc.
|
|
260
|
-
// Convert camelCase to lowercase: onClick -> click, onMouseenter -> mouseenter
|
|
261
|
-
const event = attrName.slice(2).toLowerCase();
|
|
262
|
-
const handler = (attr.value as t.JSXExpressionContainer)
|
|
263
|
-
.expression as t.Expression;
|
|
264
|
-
statements.push(
|
|
265
|
-
t.expressionStatement(
|
|
266
|
-
t.callExpression(
|
|
267
|
-
t.memberExpression(
|
|
268
|
-
t.identifier(varName),
|
|
269
|
-
t.identifier("addEventListener"),
|
|
270
|
-
),
|
|
271
|
-
[t.stringLiteral(event), handler],
|
|
272
|
-
),
|
|
273
|
-
),
|
|
274
|
-
);
|
|
275
|
-
} else if (attrNamespace === "bind") {
|
|
276
|
-
const bindProp = attrName;
|
|
277
|
-
const signal = (attr.value as t.JSXExpressionContainer)
|
|
278
|
-
.expression as t.Expression;
|
|
279
|
-
|
|
280
|
-
if (bindProp === "this") {
|
|
281
|
-
statements.push(
|
|
282
|
-
t.expressionStatement(
|
|
283
|
-
t.callExpression(
|
|
284
|
-
t.memberExpression(
|
|
285
|
-
(signal as t.CallExpression).callee as t.Expression,
|
|
286
|
-
t.identifier("set"),
|
|
287
|
-
),
|
|
288
|
-
[t.identifier(varName)],
|
|
289
|
-
),
|
|
290
|
-
),
|
|
291
|
-
);
|
|
292
|
-
continue;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
if (bindProp === "group") {
|
|
296
|
-
// Scan element attributes for type and value
|
|
297
|
-
let inputType = "";
|
|
298
|
-
let _inputValue: t.StringLiteral | undefined;
|
|
299
|
-
for (const a of node.openingElement.attributes) {
|
|
300
|
-
if (t.isJSXAttribute(a) && t.isJSXIdentifier(a.name)) {
|
|
301
|
-
if (a.name.name === "type" && t.isStringLiteral(a.value)) {
|
|
302
|
-
inputType = a.value.value;
|
|
303
|
-
}
|
|
304
|
-
if (a.name.name === "value" && t.isStringLiteral(a.value)) {
|
|
305
|
-
_inputValue = a.value;
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
if (inputType === "radio" || inputType === "") {
|
|
311
|
-
const isTopLevel =
|
|
312
|
-
t.isCallExpression(signal) &&
|
|
313
|
-
t.isIdentifier((signal as t.CallExpression).callee) &&
|
|
314
|
-
signals.has(
|
|
315
|
-
((signal as t.CallExpression).callee as t.Identifier).name,
|
|
316
|
-
);
|
|
317
|
-
|
|
318
|
-
if (isTopLevel) {
|
|
319
|
-
const signalId = (signal as t.CallExpression)
|
|
320
|
-
.callee as t.Identifier;
|
|
321
|
-
statements.push(
|
|
322
|
-
t.expressionStatement(
|
|
323
|
-
t.callExpression(
|
|
324
|
-
t.memberExpression(
|
|
325
|
-
t.identifier(varName),
|
|
326
|
-
t.identifier("addEventListener"),
|
|
327
|
-
),
|
|
328
|
-
[
|
|
329
|
-
t.stringLiteral("change"),
|
|
330
|
-
t.arrowFunctionExpression(
|
|
331
|
-
[],
|
|
332
|
-
t.callExpression(
|
|
333
|
-
t.memberExpression(signalId, t.identifier("set")),
|
|
334
|
-
[
|
|
335
|
-
t.memberExpression(
|
|
336
|
-
t.identifier(varName),
|
|
337
|
-
t.identifier("value"),
|
|
338
|
-
),
|
|
339
|
-
],
|
|
340
|
-
),
|
|
341
|
-
),
|
|
342
|
-
],
|
|
343
|
-
),
|
|
344
|
-
),
|
|
345
|
-
);
|
|
346
|
-
}
|
|
347
|
-
} else if (inputType === "checkbox") {
|
|
348
|
-
const isTopLevel =
|
|
349
|
-
t.isCallExpression(signal) &&
|
|
350
|
-
t.isIdentifier((signal as t.CallExpression).callee) &&
|
|
351
|
-
signals.has(
|
|
352
|
-
((signal as t.CallExpression).callee as t.Identifier).name,
|
|
353
|
-
);
|
|
354
|
-
|
|
355
|
-
if (isTopLevel) {
|
|
356
|
-
const callExpr = signal as t.CallExpression;
|
|
357
|
-
statements.push(
|
|
358
|
-
t.expressionStatement(
|
|
359
|
-
t.callExpression(
|
|
360
|
-
t.memberExpression(
|
|
361
|
-
t.identifier(varName),
|
|
362
|
-
t.identifier("addEventListener"),
|
|
363
|
-
),
|
|
364
|
-
[
|
|
365
|
-
t.stringLiteral("change"),
|
|
366
|
-
t.arrowFunctionExpression(
|
|
367
|
-
[],
|
|
368
|
-
t.blockStatement([
|
|
369
|
-
t.ifStatement(
|
|
370
|
-
t.memberExpression(
|
|
371
|
-
t.identifier(varName),
|
|
372
|
-
t.identifier("checked"),
|
|
373
|
-
),
|
|
374
|
-
// checked: push value into array
|
|
375
|
-
t.blockStatement([
|
|
376
|
-
t.expressionStatement(
|
|
377
|
-
t.callExpression(
|
|
378
|
-
t.memberExpression(
|
|
379
|
-
t.callExpression(callExpr, []),
|
|
380
|
-
t.identifier("push"),
|
|
381
|
-
),
|
|
382
|
-
[
|
|
383
|
-
t.memberExpression(
|
|
384
|
-
t.identifier(varName),
|
|
385
|
-
t.identifier("value"),
|
|
386
|
-
),
|
|
387
|
-
],
|
|
388
|
-
),
|
|
389
|
-
),
|
|
390
|
-
]),
|
|
391
|
-
// unchecked: splice value out of array
|
|
392
|
-
t.blockStatement([
|
|
393
|
-
t.expressionStatement(
|
|
394
|
-
t.callExpression(
|
|
395
|
-
t.memberExpression(
|
|
396
|
-
t.callExpression(callExpr, []),
|
|
397
|
-
t.identifier("splice"),
|
|
398
|
-
),
|
|
399
|
-
[
|
|
400
|
-
t.callExpression(
|
|
401
|
-
t.memberExpression(
|
|
402
|
-
t.callExpression(callExpr, []),
|
|
403
|
-
t.identifier("indexOf"),
|
|
404
|
-
),
|
|
405
|
-
[
|
|
406
|
-
t.memberExpression(
|
|
407
|
-
t.identifier(varName),
|
|
408
|
-
t.identifier("value"),
|
|
409
|
-
),
|
|
410
|
-
],
|
|
411
|
-
),
|
|
412
|
-
t.numericLiteral(1),
|
|
413
|
-
],
|
|
414
|
-
),
|
|
415
|
-
),
|
|
416
|
-
]),
|
|
417
|
-
),
|
|
418
|
-
]),
|
|
419
|
-
),
|
|
420
|
-
],
|
|
421
|
-
),
|
|
422
|
-
),
|
|
423
|
-
);
|
|
424
|
-
}
|
|
425
|
-
} else {
|
|
426
|
-
console.warn(
|
|
427
|
-
`[sigil] bind:group requires type="radio" or type="checkbox" on the element. ` +
|
|
428
|
-
`Got: type="${inputType}".`,
|
|
429
|
-
);
|
|
430
|
-
}
|
|
431
|
-
continue;
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
// Generic bind:innerHTML, bind:textContent, bind:value, bind:checked, etc.
|
|
435
|
-
const isTopLevelSignal =
|
|
436
|
-
t.isCallExpression(signal) &&
|
|
437
|
-
t.isIdentifier((signal as t.CallExpression).callee) &&
|
|
438
|
-
signals.has(((signal as t.CallExpression).callee as t.Identifier).name);
|
|
439
|
-
|
|
440
|
-
const isMemberOfSignal =
|
|
441
|
-
t.isMemberExpression(signal) &&
|
|
442
|
-
t.isCallExpression((signal as t.MemberExpression).object) &&
|
|
443
|
-
t.isIdentifier(
|
|
444
|
-
((signal as t.MemberExpression).object as t.CallExpression).callee,
|
|
445
|
-
) &&
|
|
446
|
-
signals.has(
|
|
447
|
-
(
|
|
448
|
-
((signal as t.MemberExpression).object as t.CallExpression)
|
|
449
|
-
.callee as t.Identifier
|
|
450
|
-
).name,
|
|
451
|
-
);
|
|
452
|
-
|
|
453
|
-
if (isTopLevelSignal) {
|
|
454
|
-
buildBind(varName, bindProp, attr, statements);
|
|
455
|
-
} else if (isMemberOfSignal) {
|
|
456
|
-
buildProxyBind(varName, bindProp, attr, statements, signal);
|
|
457
|
-
} else {
|
|
458
|
-
console.warn(
|
|
459
|
-
`[sigil] bind:${bindProp}={...} requires a $state signal or $state object property. ` +
|
|
460
|
-
`Got: ${generate(signal).code}. ` +
|
|
461
|
-
`Hint: change to a $state() call.`,
|
|
462
|
-
);
|
|
463
|
-
}
|
|
464
|
-
} else if (t.isStringLiteral(attr.value)) {
|
|
465
|
-
statements.push(
|
|
466
|
-
t.expressionStatement(
|
|
467
|
-
t.assignmentExpression(
|
|
468
|
-
"=",
|
|
469
|
-
attrAccess(varName, realAttr),
|
|
470
|
-
attr.value,
|
|
471
|
-
),
|
|
472
|
-
),
|
|
473
|
-
);
|
|
474
|
-
} else if (t.isJSXExpressionContainer(attr.value)) {
|
|
475
|
-
const expr = attr.value.expression as t.Expression;
|
|
476
|
-
// style={{ color: "red", fontSize: val }} — per-property assignment
|
|
477
|
-
if (attrName === "style" && t.isObjectExpression(expr)) {
|
|
478
|
-
for (const prop of expr.properties) {
|
|
479
|
-
if (!t.isObjectProperty(prop)) continue;
|
|
480
|
-
const key = t.isIdentifier(prop.key)
|
|
481
|
-
? prop.key.name
|
|
482
|
-
: t.isStringLiteral(prop.key)
|
|
483
|
-
? prop.key.value
|
|
484
|
-
: null;
|
|
485
|
-
if (!key) continue;
|
|
486
|
-
const val = prop.value as t.Expression;
|
|
487
|
-
const styleProp = t.memberExpression(
|
|
488
|
-
t.memberExpression(t.identifier(varName), t.identifier("style")),
|
|
489
|
-
t.identifier(key),
|
|
490
|
-
);
|
|
491
|
-
if (containsSignal(val, signals)) {
|
|
492
|
-
statements.push(
|
|
493
|
-
t.expressionStatement(
|
|
494
|
-
t.callExpression(t.identifier("createEffect"), [
|
|
495
|
-
t.arrowFunctionExpression(
|
|
496
|
-
[],
|
|
497
|
-
t.blockStatement([
|
|
498
|
-
t.expressionStatement(
|
|
499
|
-
t.assignmentExpression("=", styleProp, val),
|
|
500
|
-
),
|
|
501
|
-
]),
|
|
502
|
-
),
|
|
503
|
-
]),
|
|
504
|
-
),
|
|
505
|
-
);
|
|
506
|
-
} else {
|
|
507
|
-
statements.push(
|
|
508
|
-
t.expressionStatement(
|
|
509
|
-
t.assignmentExpression("=", styleProp, val),
|
|
510
|
-
),
|
|
511
|
-
);
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
// done with this attribute
|
|
515
|
-
} else if (containsSignal(expr, signals)) {
|
|
516
|
-
// Dynamic class with scoped CSS: use classList.value to preserve hash
|
|
517
|
-
if (attrName === "class" && hash) {
|
|
518
|
-
statements.push(
|
|
519
|
-
t.expressionStatement(
|
|
520
|
-
t.callExpression(t.identifier("createEffect"), [
|
|
521
|
-
t.arrowFunctionExpression(
|
|
522
|
-
[],
|
|
523
|
-
t.blockStatement([
|
|
524
|
-
t.expressionStatement(
|
|
525
|
-
t.assignmentExpression(
|
|
526
|
-
"=",
|
|
527
|
-
t.memberExpression(
|
|
528
|
-
t.memberExpression(
|
|
529
|
-
t.identifier(varName),
|
|
530
|
-
t.identifier("classList"),
|
|
531
|
-
),
|
|
532
|
-
t.identifier("value"),
|
|
533
|
-
),
|
|
534
|
-
expr,
|
|
535
|
-
),
|
|
536
|
-
),
|
|
537
|
-
t.expressionStatement(
|
|
538
|
-
t.callExpression(
|
|
539
|
-
t.memberExpression(
|
|
540
|
-
t.memberExpression(
|
|
541
|
-
t.identifier(varName),
|
|
542
|
-
t.identifier("classList"),
|
|
543
|
-
),
|
|
544
|
-
t.identifier("add"),
|
|
545
|
-
),
|
|
546
|
-
[t.stringLiteral(hash)],
|
|
547
|
-
),
|
|
548
|
-
),
|
|
549
|
-
]),
|
|
550
|
-
),
|
|
551
|
-
]),
|
|
552
|
-
),
|
|
553
|
-
);
|
|
554
|
-
} else {
|
|
555
|
-
// createEffect(() => varName.realAttr = expr)
|
|
556
|
-
statements.push(
|
|
557
|
-
t.expressionStatement(
|
|
558
|
-
t.callExpression(t.identifier("createEffect"), [
|
|
559
|
-
t.arrowFunctionExpression(
|
|
560
|
-
[],
|
|
561
|
-
t.blockStatement([
|
|
562
|
-
t.expressionStatement(
|
|
563
|
-
t.assignmentExpression(
|
|
564
|
-
"=",
|
|
565
|
-
attrAccess(varName, realAttr),
|
|
566
|
-
expr,
|
|
567
|
-
),
|
|
568
|
-
),
|
|
569
|
-
]),
|
|
570
|
-
),
|
|
571
|
-
]),
|
|
572
|
-
),
|
|
573
|
-
);
|
|
574
|
-
}
|
|
575
|
-
} else {
|
|
576
|
-
// varName.realAttr = expr
|
|
577
|
-
statements.push(
|
|
578
|
-
t.expressionStatement(
|
|
579
|
-
t.assignmentExpression("=", attrAccess(varName, realAttr), expr),
|
|
580
|
-
),
|
|
581
|
-
);
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
// Add scoped CSS hash to classList (if hash is present)
|
|
587
|
-
if (hash) {
|
|
588
|
-
addScopedClass(varName, hash, statements);
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
// children
|
|
592
|
-
for (const child of node.children) {
|
|
593
|
-
if (t.isJSXElement(child)) {
|
|
594
|
-
const childVar = processElement(
|
|
595
|
-
child,
|
|
596
|
-
statements,
|
|
597
|
-
genId,
|
|
598
|
-
signals,
|
|
599
|
-
hash,
|
|
600
|
-
hydrate,
|
|
601
|
-
childNodesVar,
|
|
602
|
-
hydrate ? varName : undefined,
|
|
603
|
-
);
|
|
604
|
-
if (!hydrate) {
|
|
605
|
-
statements.push(
|
|
606
|
-
t.expressionStatement(
|
|
607
|
-
t.callExpression(
|
|
608
|
-
t.memberExpression(t.identifier(varName), t.identifier("append")),
|
|
609
|
-
[t.identifier(childVar)],
|
|
610
|
-
),
|
|
611
|
-
),
|
|
612
|
-
);
|
|
613
|
-
}
|
|
614
|
-
} else if (t.isJSXFragment(child)) {
|
|
615
|
-
const childVar = processFragment(
|
|
616
|
-
child,
|
|
617
|
-
statements,
|
|
618
|
-
genId,
|
|
619
|
-
signals,
|
|
620
|
-
hash,
|
|
621
|
-
hydrate,
|
|
622
|
-
childNodesVar,
|
|
623
|
-
hydrate ? varName : undefined,
|
|
624
|
-
);
|
|
625
|
-
if (!hydrate) {
|
|
626
|
-
statements.push(
|
|
627
|
-
t.expressionStatement(
|
|
628
|
-
t.callExpression(
|
|
629
|
-
t.memberExpression(t.identifier(varName), t.identifier("append")),
|
|
630
|
-
[t.identifier(childVar)],
|
|
631
|
-
),
|
|
632
|
-
),
|
|
633
|
-
);
|
|
634
|
-
}
|
|
635
|
-
} else if (t.isJSXText(child)) {
|
|
636
|
-
const text = child.value;
|
|
637
|
-
if (!text.trim()) continue;
|
|
638
|
-
if (hydrate) {
|
|
639
|
-
// SSR hydration: text already in DOM from SSR, do nothing.
|
|
640
|
-
// SPA navigation: childNodesVar pool is empty, create text node.
|
|
641
|
-
const poolLen = t.memberExpression(
|
|
642
|
-
t.identifier(childNodesVar!),
|
|
643
|
-
t.identifier("length"),
|
|
644
|
-
);
|
|
645
|
-
statements.push(
|
|
646
|
-
t.ifStatement(
|
|
647
|
-
t.binaryExpression("===", poolLen, t.numericLiteral(0)),
|
|
648
|
-
t.expressionStatement(
|
|
649
|
-
t.callExpression(
|
|
650
|
-
t.memberExpression(
|
|
651
|
-
t.identifier(varName),
|
|
652
|
-
t.identifier("append"),
|
|
653
|
-
),
|
|
654
|
-
[
|
|
655
|
-
t.callExpression(
|
|
656
|
-
t.memberExpression(
|
|
657
|
-
t.identifier("document"),
|
|
658
|
-
t.identifier("createTextNode"),
|
|
659
|
-
),
|
|
660
|
-
[t.stringLiteral(text.replace(/\s*\n\s*/g, " "))],
|
|
661
|
-
),
|
|
662
|
-
],
|
|
663
|
-
),
|
|
664
|
-
),
|
|
665
|
-
),
|
|
666
|
-
);
|
|
667
|
-
} else {
|
|
668
|
-
statements.push(
|
|
669
|
-
t.expressionStatement(
|
|
670
|
-
t.callExpression(
|
|
671
|
-
t.memberExpression(t.identifier(varName), t.identifier("append")),
|
|
672
|
-
[
|
|
673
|
-
t.callExpression(
|
|
674
|
-
t.memberExpression(
|
|
675
|
-
t.identifier("document"),
|
|
676
|
-
t.identifier("createTextNode"),
|
|
677
|
-
),
|
|
678
|
-
[t.stringLiteral(text)],
|
|
679
|
-
),
|
|
680
|
-
],
|
|
681
|
-
),
|
|
682
|
-
),
|
|
683
|
-
);
|
|
684
|
-
}
|
|
685
|
-
} else if (t.isJSXExpressionContainer(child)) {
|
|
686
|
-
if (t.isJSXEmptyExpression(child.expression)) continue;
|
|
687
|
-
const expr = child.expression as t.Expression;
|
|
688
|
-
if (t.isJSXElement(expr)) {
|
|
689
|
-
const childVar = processElement(
|
|
690
|
-
expr,
|
|
691
|
-
statements,
|
|
692
|
-
genId,
|
|
693
|
-
signals,
|
|
694
|
-
hash,
|
|
695
|
-
hydrate,
|
|
696
|
-
childNodesVar,
|
|
697
|
-
hydrate ? varName : undefined,
|
|
698
|
-
);
|
|
699
|
-
if (!hydrate) {
|
|
700
|
-
statements.push(
|
|
701
|
-
t.expressionStatement(
|
|
702
|
-
t.callExpression(
|
|
703
|
-
t.memberExpression(
|
|
704
|
-
t.identifier(varName),
|
|
705
|
-
t.identifier("append"),
|
|
706
|
-
),
|
|
707
|
-
[t.identifier(childVar)],
|
|
708
|
-
),
|
|
709
|
-
),
|
|
710
|
-
);
|
|
711
|
-
}
|
|
712
|
-
} else if (t.isJSXFragment(expr)) {
|
|
713
|
-
const childVar = processFragment(
|
|
714
|
-
expr,
|
|
715
|
-
statements,
|
|
716
|
-
genId,
|
|
717
|
-
signals,
|
|
718
|
-
hash,
|
|
719
|
-
hydrate,
|
|
720
|
-
childNodesVar,
|
|
721
|
-
hydrate ? varName : undefined,
|
|
722
|
-
);
|
|
723
|
-
if (!hydrate) {
|
|
724
|
-
statements.push(
|
|
725
|
-
t.expressionStatement(
|
|
726
|
-
t.callExpression(
|
|
727
|
-
t.memberExpression(
|
|
728
|
-
t.identifier(varName),
|
|
729
|
-
t.identifier("append"),
|
|
730
|
-
),
|
|
731
|
-
[t.identifier(childVar)],
|
|
732
|
-
),
|
|
733
|
-
),
|
|
734
|
-
);
|
|
735
|
-
}
|
|
736
|
-
} else if (containsSignal(expr, signals)) {
|
|
737
|
-
if (isPrimitive(expr)) {
|
|
738
|
-
// fast path: text node + textContent
|
|
739
|
-
buildTextNode(
|
|
740
|
-
varName,
|
|
741
|
-
expr,
|
|
742
|
-
statements,
|
|
743
|
-
genId,
|
|
744
|
-
hydrate,
|
|
745
|
-
childNodesVar,
|
|
746
|
-
hydrate ? varName : undefined,
|
|
747
|
-
);
|
|
748
|
-
} else {
|
|
749
|
-
// Check for keyed list pattern
|
|
750
|
-
const keyed = findKeyedMapExpr(expr, signals);
|
|
751
|
-
if (keyed) {
|
|
752
|
-
buildKeyedList(
|
|
753
|
-
varName,
|
|
754
|
-
keyed,
|
|
755
|
-
statements,
|
|
756
|
-
genId,
|
|
757
|
-
signals,
|
|
758
|
-
processElement,
|
|
759
|
-
hash,
|
|
760
|
-
hydrate,
|
|
761
|
-
childNodesVar,
|
|
762
|
-
hydrate ? varName : undefined,
|
|
763
|
-
);
|
|
764
|
-
} else {
|
|
765
|
-
buildAnchorMount(
|
|
766
|
-
varName,
|
|
767
|
-
expr,
|
|
768
|
-
statements,
|
|
769
|
-
genId,
|
|
770
|
-
hydrate,
|
|
771
|
-
childNodesVar,
|
|
772
|
-
hydrate ? varName : undefined,
|
|
773
|
-
);
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
} else {
|
|
777
|
-
// non-reactive: set once
|
|
778
|
-
if (hydrate) {
|
|
779
|
-
const claimCommentArgs: t.Expression[] = [
|
|
780
|
-
t.identifier(childNodesVar!),
|
|
781
|
-
t.stringLiteral("g"),
|
|
782
|
-
];
|
|
783
|
-
if (varName) claimCommentArgs.push(t.identifier(varName));
|
|
784
|
-
const poolLen = t.memberExpression(
|
|
785
|
-
t.identifier(childNodesVar!),
|
|
786
|
-
t.identifier("length"),
|
|
787
|
-
);
|
|
788
|
-
// Pool has elements (initial load): SSR content already in DOM, just consume delimiters
|
|
789
|
-
const claimG = t.expressionStatement(
|
|
790
|
-
t.callExpression(t.identifier("claimComment"), claimCommentArgs),
|
|
791
|
-
);
|
|
792
|
-
const claimSlashG = (() => {
|
|
793
|
-
const args: t.Expression[] = [
|
|
794
|
-
t.identifier(childNodesVar!),
|
|
795
|
-
t.stringLiteral("/g"),
|
|
796
|
-
];
|
|
797
|
-
if (varName) args.push(t.identifier(varName));
|
|
798
|
-
return t.expressionStatement(
|
|
799
|
-
t.callExpression(t.identifier("claimComment"), args),
|
|
800
|
-
);
|
|
801
|
-
})();
|
|
802
|
-
// Pool empty (SPA nav): create and insert fresh elements
|
|
803
|
-
const insertCall = t.expressionStatement(
|
|
804
|
-
t.callExpression(t.identifier("insert"), [
|
|
805
|
-
t.identifier(varName),
|
|
806
|
-
expr,
|
|
807
|
-
]),
|
|
808
|
-
);
|
|
809
|
-
statements.push(
|
|
810
|
-
t.ifStatement(
|
|
811
|
-
t.binaryExpression(">", poolLen, t.numericLiteral(0)),
|
|
812
|
-
t.blockStatement([claimG, claimSlashG]),
|
|
813
|
-
t.blockStatement([insertCall]),
|
|
814
|
-
),
|
|
815
|
-
);
|
|
816
|
-
} else {
|
|
817
|
-
statements.push(
|
|
818
|
-
t.expressionStatement(
|
|
819
|
-
t.callExpression(t.identifier("insert"), [
|
|
820
|
-
t.identifier(varName),
|
|
821
|
-
expr,
|
|
822
|
-
]),
|
|
823
|
-
),
|
|
824
|
-
);
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
return varName;
|
|
831
|
-
}
|
|
1
|
+
import { types as t } from "@babel/core";
|
|
2
|
+
//@ts-expect-error Not our problem babel forgot tp export types.
|
|
3
|
+
import { generate } from "@babel/generator";
|
|
4
|
+
import {
|
|
5
|
+
handleGlobalComponent,
|
|
6
|
+
isGlobalComponent,
|
|
7
|
+
} from "../handlers/dom/globals";
|
|
8
|
+
import { buildBind, buildProxyBind } from "../util/bind";
|
|
9
|
+
import { buildAnchorMount } from "./anchor-mount";
|
|
10
|
+
import { collectChildren } from "./children";
|
|
11
|
+
import { processFragment } from "./fragment";
|
|
12
|
+
import { buildKeyedList, findKeyedMapExpr } from "./keyed-list";
|
|
13
|
+
import { buildTextNode } from "./text-node";
|
|
14
|
+
import {
|
|
15
|
+
ATTR_MAP,
|
|
16
|
+
buildHydrationScope,
|
|
17
|
+
containsSignal,
|
|
18
|
+
getCreateElement,
|
|
19
|
+
isPrimitive,
|
|
20
|
+
} from "./utils";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Build a property-access expression for a JSX attribute name.
|
|
24
|
+
* Dashed names (data-*, aria-*) require computed access (e.g. el["data-id"])
|
|
25
|
+
* because t.identifier() rejects names with dashes.
|
|
26
|
+
*/
|
|
27
|
+
function attrAccess(varName: string, attrName: string): t.MemberExpression {
|
|
28
|
+
if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(attrName)) {
|
|
29
|
+
return t.memberExpression(t.identifier(varName), t.identifier(attrName));
|
|
30
|
+
}
|
|
31
|
+
return t.memberExpression(
|
|
32
|
+
t.identifier(varName),
|
|
33
|
+
t.stringLiteral(attrName),
|
|
34
|
+
true,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Build an object property key (identifier for valid names, string literal for dashed).
|
|
40
|
+
*/
|
|
41
|
+
function attrKey(attrName: string): t.Identifier | t.StringLiteral {
|
|
42
|
+
if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(attrName)) {
|
|
43
|
+
return t.identifier(attrName);
|
|
44
|
+
}
|
|
45
|
+
return t.stringLiteral(attrName);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Add classList.add(hash) statement for scoped CSS.
|
|
50
|
+
*/
|
|
51
|
+
function addScopedClass(
|
|
52
|
+
varName: string,
|
|
53
|
+
hash: string,
|
|
54
|
+
statements: t.Statement[],
|
|
55
|
+
): void {
|
|
56
|
+
statements.push(
|
|
57
|
+
t.expressionStatement(
|
|
58
|
+
t.callExpression(
|
|
59
|
+
t.memberExpression(
|
|
60
|
+
t.memberExpression(t.identifier(varName), t.identifier("classList")),
|
|
61
|
+
t.identifier("add"),
|
|
62
|
+
),
|
|
63
|
+
[t.stringLiteral(hash)],
|
|
64
|
+
),
|
|
65
|
+
),
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function processElement(
|
|
70
|
+
node: t.JSXElement,
|
|
71
|
+
statements: t.Statement[],
|
|
72
|
+
genId: () => string,
|
|
73
|
+
signals: Set<string>,
|
|
74
|
+
hash?: string,
|
|
75
|
+
hydrate?: boolean,
|
|
76
|
+
nodesVar?: string,
|
|
77
|
+
parentVar?: string,
|
|
78
|
+
): string {
|
|
79
|
+
const varName = genId();
|
|
80
|
+
const tag = (node.openingElement.name as t.JSXIdentifier).name;
|
|
81
|
+
const isComponent = /^[A-Z]/.test(tag);
|
|
82
|
+
|
|
83
|
+
if (isComponent) {
|
|
84
|
+
// Global components: Window, Document, Body
|
|
85
|
+
if (isGlobalComponent(tag)) {
|
|
86
|
+
return handleGlobalComponent(tag, node, statements, genId);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Component: call the function with a props object
|
|
90
|
+
const propsObj = t.objectExpression([]);
|
|
91
|
+
|
|
92
|
+
// Add attributes to props (including spread)
|
|
93
|
+
for (const attr of node.openingElement.attributes) {
|
|
94
|
+
if (t.isJSXSpreadAttribute(attr)) {
|
|
95
|
+
// {…obj} — spread into props
|
|
96
|
+
propsObj.properties.push(t.spreadElement(attr.argument));
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (!t.isJSXAttribute(attr)) continue;
|
|
100
|
+
const attrName = (attr.name as t.JSXIdentifier).name;
|
|
101
|
+
|
|
102
|
+
if (t.isStringLiteral(attr.value)) {
|
|
103
|
+
propsObj.properties.push(
|
|
104
|
+
t.objectProperty(attrKey(attrName), attr.value),
|
|
105
|
+
);
|
|
106
|
+
} else if (t.isJSXExpressionContainer(attr.value)) {
|
|
107
|
+
const expr = attr.value.expression as t.Expression;
|
|
108
|
+
propsObj.properties.push(t.objectProperty(attrKey(attrName), expr));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Add children to props
|
|
113
|
+
const childrenExpr = collectChildren(
|
|
114
|
+
node,
|
|
115
|
+
statements,
|
|
116
|
+
genId,
|
|
117
|
+
signals,
|
|
118
|
+
hash,
|
|
119
|
+
hydrate,
|
|
120
|
+
nodesVar,
|
|
121
|
+
);
|
|
122
|
+
if (childrenExpr) {
|
|
123
|
+
propsObj.properties.push(
|
|
124
|
+
t.objectProperty(t.identifier("children"), childrenExpr),
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const componentCall = t.callExpression(t.identifier(tag), [propsObj]);
|
|
129
|
+
|
|
130
|
+
if (hydrate && nodesVar) {
|
|
131
|
+
// Push the parent's child pool before calling the component so that
|
|
132
|
+
// getHydrationNodes() inside the component returns the right scope.
|
|
133
|
+
// In SSR hydration the component claims its root node from that pool
|
|
134
|
+
// (it's already in the DOM). In empty-pool / SPA mode the component
|
|
135
|
+
// creates a fresh node, so we explicitly insert it when the pool is empty.
|
|
136
|
+
statements.push(
|
|
137
|
+
t.expressionStatement(
|
|
138
|
+
t.callExpression(t.identifier("pushHydrationNodes"), [
|
|
139
|
+
t.identifier(nodesVar),
|
|
140
|
+
]),
|
|
141
|
+
),
|
|
142
|
+
);
|
|
143
|
+
statements.push(
|
|
144
|
+
t.variableDeclaration("const", [
|
|
145
|
+
t.variableDeclarator(t.identifier(varName), componentCall),
|
|
146
|
+
]),
|
|
147
|
+
);
|
|
148
|
+
statements.push(
|
|
149
|
+
t.expressionStatement(
|
|
150
|
+
t.callExpression(t.identifier("popHydrationNodes"), []),
|
|
151
|
+
),
|
|
152
|
+
);
|
|
153
|
+
if (parentVar) {
|
|
154
|
+
// Insert only when the pool was empty (fresh DOM); if the pool had
|
|
155
|
+
// nodes the component already claimed its SSR element (already in DOM).
|
|
156
|
+
statements.push(
|
|
157
|
+
t.ifStatement(
|
|
158
|
+
t.binaryExpression(
|
|
159
|
+
"===",
|
|
160
|
+
t.memberExpression(
|
|
161
|
+
t.identifier(nodesVar),
|
|
162
|
+
t.identifier("length"),
|
|
163
|
+
),
|
|
164
|
+
t.numericLiteral(0),
|
|
165
|
+
),
|
|
166
|
+
t.expressionStatement(
|
|
167
|
+
t.callExpression(t.identifier("insert"), [
|
|
168
|
+
t.identifier(parentVar),
|
|
169
|
+
t.identifier(varName),
|
|
170
|
+
]),
|
|
171
|
+
),
|
|
172
|
+
),
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
} else {
|
|
176
|
+
statements.push(
|
|
177
|
+
t.variableDeclaration("const", [
|
|
178
|
+
t.variableDeclarator(t.identifier(varName), componentCall),
|
|
179
|
+
]),
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return varName;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Native element: claim (hydrate) or create (dom)
|
|
187
|
+
const currentNodesVar = nodesVar ?? "__nodes";
|
|
188
|
+
statements.push(
|
|
189
|
+
t.variableDeclaration("const", [
|
|
190
|
+
t.variableDeclarator(
|
|
191
|
+
t.identifier(varName),
|
|
192
|
+
getCreateElement(
|
|
193
|
+
tag,
|
|
194
|
+
!!hydrate,
|
|
195
|
+
currentNodesVar,
|
|
196
|
+
hydrate ? parentVar : undefined,
|
|
197
|
+
),
|
|
198
|
+
),
|
|
199
|
+
]),
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
// In hydrate mode, scope to this element's children for descendants
|
|
203
|
+
let childNodesVar: string | undefined;
|
|
204
|
+
if (hydrate) {
|
|
205
|
+
childNodesVar = genId();
|
|
206
|
+
buildHydrationScope(varName, statements, childNodesVar);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// attributes
|
|
210
|
+
const seenAttrs = new Set<string>();
|
|
211
|
+
for (const attr of node.openingElement.attributes) {
|
|
212
|
+
if (t.isJSXSpreadAttribute(attr)) {
|
|
213
|
+
// {…obj} — spread onto native element
|
|
214
|
+
statements.push(
|
|
215
|
+
t.expressionStatement(
|
|
216
|
+
t.callExpression(t.identifier("Object.assign"), [
|
|
217
|
+
t.identifier(varName),
|
|
218
|
+
attr.argument as t.Expression,
|
|
219
|
+
]),
|
|
220
|
+
),
|
|
221
|
+
);
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (!t.isJSXAttribute(attr)) continue;
|
|
225
|
+
let attrName: string;
|
|
226
|
+
let attrNamespace: string | undefined;
|
|
227
|
+
if (t.isJSXNamespacedName(attr.name)) {
|
|
228
|
+
attrNamespace = attr.name.namespace.name;
|
|
229
|
+
attrName = attr.name.name.name;
|
|
230
|
+
} else {
|
|
231
|
+
attrName = (attr.name as t.JSXIdentifier).name;
|
|
232
|
+
// bindValue -> bind:value, bindChecked -> bind:checked
|
|
233
|
+
if (/^bind[A-Z]/.test(attrName)) {
|
|
234
|
+
attrNamespace = "bind";
|
|
235
|
+
attrName = attrName.slice(4).toLowerCase();
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const realAttr = ATTR_MAP[attrName] ?? attrName;
|
|
239
|
+
|
|
240
|
+
if (seenAttrs.has(realAttr)) {
|
|
241
|
+
throw new TypeError(
|
|
242
|
+
`Duplicate attribute "${realAttr}" (via "${attrName}"). Use class or className, not both.`,
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
seenAttrs.add(realAttr);
|
|
246
|
+
if (attrName === "use") {
|
|
247
|
+
// use={directive} or use={[directive, params]}
|
|
248
|
+
const expr = (attr.value as t.JSXExpressionContainer)
|
|
249
|
+
.expression as t.Expression;
|
|
250
|
+
statements.push(
|
|
251
|
+
t.expressionStatement(
|
|
252
|
+
t.callExpression(t.identifier("applyDirective"), [
|
|
253
|
+
t.identifier(varName),
|
|
254
|
+
expr,
|
|
255
|
+
]),
|
|
256
|
+
),
|
|
257
|
+
);
|
|
258
|
+
} else if (/^on[A-Z]/.test(attrName)) {
|
|
259
|
+
// Standard JSX: onClick, onSubmit, onMouseenter, etc.
|
|
260
|
+
// Convert camelCase to lowercase: onClick -> click, onMouseenter -> mouseenter
|
|
261
|
+
const event = attrName.slice(2).toLowerCase();
|
|
262
|
+
const handler = (attr.value as t.JSXExpressionContainer)
|
|
263
|
+
.expression as t.Expression;
|
|
264
|
+
statements.push(
|
|
265
|
+
t.expressionStatement(
|
|
266
|
+
t.callExpression(
|
|
267
|
+
t.memberExpression(
|
|
268
|
+
t.identifier(varName),
|
|
269
|
+
t.identifier("addEventListener"),
|
|
270
|
+
),
|
|
271
|
+
[t.stringLiteral(event), handler],
|
|
272
|
+
),
|
|
273
|
+
),
|
|
274
|
+
);
|
|
275
|
+
} else if (attrNamespace === "bind") {
|
|
276
|
+
const bindProp = attrName;
|
|
277
|
+
const signal = (attr.value as t.JSXExpressionContainer)
|
|
278
|
+
.expression as t.Expression;
|
|
279
|
+
|
|
280
|
+
if (bindProp === "this") {
|
|
281
|
+
statements.push(
|
|
282
|
+
t.expressionStatement(
|
|
283
|
+
t.callExpression(
|
|
284
|
+
t.memberExpression(
|
|
285
|
+
(signal as t.CallExpression).callee as t.Expression,
|
|
286
|
+
t.identifier("set"),
|
|
287
|
+
),
|
|
288
|
+
[t.identifier(varName)],
|
|
289
|
+
),
|
|
290
|
+
),
|
|
291
|
+
);
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (bindProp === "group") {
|
|
296
|
+
// Scan element attributes for type and value
|
|
297
|
+
let inputType = "";
|
|
298
|
+
let _inputValue: t.StringLiteral | undefined;
|
|
299
|
+
for (const a of node.openingElement.attributes) {
|
|
300
|
+
if (t.isJSXAttribute(a) && t.isJSXIdentifier(a.name)) {
|
|
301
|
+
if (a.name.name === "type" && t.isStringLiteral(a.value)) {
|
|
302
|
+
inputType = a.value.value;
|
|
303
|
+
}
|
|
304
|
+
if (a.name.name === "value" && t.isStringLiteral(a.value)) {
|
|
305
|
+
_inputValue = a.value;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (inputType === "radio" || inputType === "") {
|
|
311
|
+
const isTopLevel =
|
|
312
|
+
t.isCallExpression(signal) &&
|
|
313
|
+
t.isIdentifier((signal as t.CallExpression).callee) &&
|
|
314
|
+
signals.has(
|
|
315
|
+
((signal as t.CallExpression).callee as t.Identifier).name,
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
if (isTopLevel) {
|
|
319
|
+
const signalId = (signal as t.CallExpression)
|
|
320
|
+
.callee as t.Identifier;
|
|
321
|
+
statements.push(
|
|
322
|
+
t.expressionStatement(
|
|
323
|
+
t.callExpression(
|
|
324
|
+
t.memberExpression(
|
|
325
|
+
t.identifier(varName),
|
|
326
|
+
t.identifier("addEventListener"),
|
|
327
|
+
),
|
|
328
|
+
[
|
|
329
|
+
t.stringLiteral("change"),
|
|
330
|
+
t.arrowFunctionExpression(
|
|
331
|
+
[],
|
|
332
|
+
t.callExpression(
|
|
333
|
+
t.memberExpression(signalId, t.identifier("set")),
|
|
334
|
+
[
|
|
335
|
+
t.memberExpression(
|
|
336
|
+
t.identifier(varName),
|
|
337
|
+
t.identifier("value"),
|
|
338
|
+
),
|
|
339
|
+
],
|
|
340
|
+
),
|
|
341
|
+
),
|
|
342
|
+
],
|
|
343
|
+
),
|
|
344
|
+
),
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
} else if (inputType === "checkbox") {
|
|
348
|
+
const isTopLevel =
|
|
349
|
+
t.isCallExpression(signal) &&
|
|
350
|
+
t.isIdentifier((signal as t.CallExpression).callee) &&
|
|
351
|
+
signals.has(
|
|
352
|
+
((signal as t.CallExpression).callee as t.Identifier).name,
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
if (isTopLevel) {
|
|
356
|
+
const callExpr = signal as t.CallExpression;
|
|
357
|
+
statements.push(
|
|
358
|
+
t.expressionStatement(
|
|
359
|
+
t.callExpression(
|
|
360
|
+
t.memberExpression(
|
|
361
|
+
t.identifier(varName),
|
|
362
|
+
t.identifier("addEventListener"),
|
|
363
|
+
),
|
|
364
|
+
[
|
|
365
|
+
t.stringLiteral("change"),
|
|
366
|
+
t.arrowFunctionExpression(
|
|
367
|
+
[],
|
|
368
|
+
t.blockStatement([
|
|
369
|
+
t.ifStatement(
|
|
370
|
+
t.memberExpression(
|
|
371
|
+
t.identifier(varName),
|
|
372
|
+
t.identifier("checked"),
|
|
373
|
+
),
|
|
374
|
+
// checked: push value into array
|
|
375
|
+
t.blockStatement([
|
|
376
|
+
t.expressionStatement(
|
|
377
|
+
t.callExpression(
|
|
378
|
+
t.memberExpression(
|
|
379
|
+
t.callExpression(callExpr, []),
|
|
380
|
+
t.identifier("push"),
|
|
381
|
+
),
|
|
382
|
+
[
|
|
383
|
+
t.memberExpression(
|
|
384
|
+
t.identifier(varName),
|
|
385
|
+
t.identifier("value"),
|
|
386
|
+
),
|
|
387
|
+
],
|
|
388
|
+
),
|
|
389
|
+
),
|
|
390
|
+
]),
|
|
391
|
+
// unchecked: splice value out of array
|
|
392
|
+
t.blockStatement([
|
|
393
|
+
t.expressionStatement(
|
|
394
|
+
t.callExpression(
|
|
395
|
+
t.memberExpression(
|
|
396
|
+
t.callExpression(callExpr, []),
|
|
397
|
+
t.identifier("splice"),
|
|
398
|
+
),
|
|
399
|
+
[
|
|
400
|
+
t.callExpression(
|
|
401
|
+
t.memberExpression(
|
|
402
|
+
t.callExpression(callExpr, []),
|
|
403
|
+
t.identifier("indexOf"),
|
|
404
|
+
),
|
|
405
|
+
[
|
|
406
|
+
t.memberExpression(
|
|
407
|
+
t.identifier(varName),
|
|
408
|
+
t.identifier("value"),
|
|
409
|
+
),
|
|
410
|
+
],
|
|
411
|
+
),
|
|
412
|
+
t.numericLiteral(1),
|
|
413
|
+
],
|
|
414
|
+
),
|
|
415
|
+
),
|
|
416
|
+
]),
|
|
417
|
+
),
|
|
418
|
+
]),
|
|
419
|
+
),
|
|
420
|
+
],
|
|
421
|
+
),
|
|
422
|
+
),
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
} else {
|
|
426
|
+
console.warn(
|
|
427
|
+
`[sigil] bind:group requires type="radio" or type="checkbox" on the element. ` +
|
|
428
|
+
`Got: type="${inputType}".`,
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Generic bind:innerHTML, bind:textContent, bind:value, bind:checked, etc.
|
|
435
|
+
const isTopLevelSignal =
|
|
436
|
+
t.isCallExpression(signal) &&
|
|
437
|
+
t.isIdentifier((signal as t.CallExpression).callee) &&
|
|
438
|
+
signals.has(((signal as t.CallExpression).callee as t.Identifier).name);
|
|
439
|
+
|
|
440
|
+
const isMemberOfSignal =
|
|
441
|
+
t.isMemberExpression(signal) &&
|
|
442
|
+
t.isCallExpression((signal as t.MemberExpression).object) &&
|
|
443
|
+
t.isIdentifier(
|
|
444
|
+
((signal as t.MemberExpression).object as t.CallExpression).callee,
|
|
445
|
+
) &&
|
|
446
|
+
signals.has(
|
|
447
|
+
(
|
|
448
|
+
((signal as t.MemberExpression).object as t.CallExpression)
|
|
449
|
+
.callee as t.Identifier
|
|
450
|
+
).name,
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
if (isTopLevelSignal) {
|
|
454
|
+
buildBind(varName, bindProp, attr, statements);
|
|
455
|
+
} else if (isMemberOfSignal) {
|
|
456
|
+
buildProxyBind(varName, bindProp, attr, statements, signal);
|
|
457
|
+
} else {
|
|
458
|
+
console.warn(
|
|
459
|
+
`[sigil] bind:${bindProp}={...} requires a $state signal or $state object property. ` +
|
|
460
|
+
`Got: ${generate(signal).code}. ` +
|
|
461
|
+
`Hint: change to a $state() call.`,
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
} else if (t.isStringLiteral(attr.value)) {
|
|
465
|
+
statements.push(
|
|
466
|
+
t.expressionStatement(
|
|
467
|
+
t.assignmentExpression(
|
|
468
|
+
"=",
|
|
469
|
+
attrAccess(varName, realAttr),
|
|
470
|
+
attr.value,
|
|
471
|
+
),
|
|
472
|
+
),
|
|
473
|
+
);
|
|
474
|
+
} else if (t.isJSXExpressionContainer(attr.value)) {
|
|
475
|
+
const expr = attr.value.expression as t.Expression;
|
|
476
|
+
// style={{ color: "red", fontSize: val }} — per-property assignment
|
|
477
|
+
if (attrName === "style" && t.isObjectExpression(expr)) {
|
|
478
|
+
for (const prop of expr.properties) {
|
|
479
|
+
if (!t.isObjectProperty(prop)) continue;
|
|
480
|
+
const key = t.isIdentifier(prop.key)
|
|
481
|
+
? prop.key.name
|
|
482
|
+
: t.isStringLiteral(prop.key)
|
|
483
|
+
? prop.key.value
|
|
484
|
+
: null;
|
|
485
|
+
if (!key) continue;
|
|
486
|
+
const val = prop.value as t.Expression;
|
|
487
|
+
const styleProp = t.memberExpression(
|
|
488
|
+
t.memberExpression(t.identifier(varName), t.identifier("style")),
|
|
489
|
+
t.identifier(key),
|
|
490
|
+
);
|
|
491
|
+
if (containsSignal(val, signals)) {
|
|
492
|
+
statements.push(
|
|
493
|
+
t.expressionStatement(
|
|
494
|
+
t.callExpression(t.identifier("createEffect"), [
|
|
495
|
+
t.arrowFunctionExpression(
|
|
496
|
+
[],
|
|
497
|
+
t.blockStatement([
|
|
498
|
+
t.expressionStatement(
|
|
499
|
+
t.assignmentExpression("=", styleProp, val),
|
|
500
|
+
),
|
|
501
|
+
]),
|
|
502
|
+
),
|
|
503
|
+
]),
|
|
504
|
+
),
|
|
505
|
+
);
|
|
506
|
+
} else {
|
|
507
|
+
statements.push(
|
|
508
|
+
t.expressionStatement(
|
|
509
|
+
t.assignmentExpression("=", styleProp, val),
|
|
510
|
+
),
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
// done with this attribute
|
|
515
|
+
} else if (containsSignal(expr, signals)) {
|
|
516
|
+
// Dynamic class with scoped CSS: use classList.value to preserve hash
|
|
517
|
+
if (attrName === "class" && hash) {
|
|
518
|
+
statements.push(
|
|
519
|
+
t.expressionStatement(
|
|
520
|
+
t.callExpression(t.identifier("createEffect"), [
|
|
521
|
+
t.arrowFunctionExpression(
|
|
522
|
+
[],
|
|
523
|
+
t.blockStatement([
|
|
524
|
+
t.expressionStatement(
|
|
525
|
+
t.assignmentExpression(
|
|
526
|
+
"=",
|
|
527
|
+
t.memberExpression(
|
|
528
|
+
t.memberExpression(
|
|
529
|
+
t.identifier(varName),
|
|
530
|
+
t.identifier("classList"),
|
|
531
|
+
),
|
|
532
|
+
t.identifier("value"),
|
|
533
|
+
),
|
|
534
|
+
expr,
|
|
535
|
+
),
|
|
536
|
+
),
|
|
537
|
+
t.expressionStatement(
|
|
538
|
+
t.callExpression(
|
|
539
|
+
t.memberExpression(
|
|
540
|
+
t.memberExpression(
|
|
541
|
+
t.identifier(varName),
|
|
542
|
+
t.identifier("classList"),
|
|
543
|
+
),
|
|
544
|
+
t.identifier("add"),
|
|
545
|
+
),
|
|
546
|
+
[t.stringLiteral(hash)],
|
|
547
|
+
),
|
|
548
|
+
),
|
|
549
|
+
]),
|
|
550
|
+
),
|
|
551
|
+
]),
|
|
552
|
+
),
|
|
553
|
+
);
|
|
554
|
+
} else {
|
|
555
|
+
// createEffect(() => varName.realAttr = expr)
|
|
556
|
+
statements.push(
|
|
557
|
+
t.expressionStatement(
|
|
558
|
+
t.callExpression(t.identifier("createEffect"), [
|
|
559
|
+
t.arrowFunctionExpression(
|
|
560
|
+
[],
|
|
561
|
+
t.blockStatement([
|
|
562
|
+
t.expressionStatement(
|
|
563
|
+
t.assignmentExpression(
|
|
564
|
+
"=",
|
|
565
|
+
attrAccess(varName, realAttr),
|
|
566
|
+
expr,
|
|
567
|
+
),
|
|
568
|
+
),
|
|
569
|
+
]),
|
|
570
|
+
),
|
|
571
|
+
]),
|
|
572
|
+
),
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
} else {
|
|
576
|
+
// varName.realAttr = expr
|
|
577
|
+
statements.push(
|
|
578
|
+
t.expressionStatement(
|
|
579
|
+
t.assignmentExpression("=", attrAccess(varName, realAttr), expr),
|
|
580
|
+
),
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Add scoped CSS hash to classList (if hash is present)
|
|
587
|
+
if (hash) {
|
|
588
|
+
addScopedClass(varName, hash, statements);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// children
|
|
592
|
+
for (const child of node.children) {
|
|
593
|
+
if (t.isJSXElement(child)) {
|
|
594
|
+
const childVar = processElement(
|
|
595
|
+
child,
|
|
596
|
+
statements,
|
|
597
|
+
genId,
|
|
598
|
+
signals,
|
|
599
|
+
hash,
|
|
600
|
+
hydrate,
|
|
601
|
+
childNodesVar,
|
|
602
|
+
hydrate ? varName : undefined,
|
|
603
|
+
);
|
|
604
|
+
if (!hydrate) {
|
|
605
|
+
statements.push(
|
|
606
|
+
t.expressionStatement(
|
|
607
|
+
t.callExpression(
|
|
608
|
+
t.memberExpression(t.identifier(varName), t.identifier("append")),
|
|
609
|
+
[t.identifier(childVar)],
|
|
610
|
+
),
|
|
611
|
+
),
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
} else if (t.isJSXFragment(child)) {
|
|
615
|
+
const childVar = processFragment(
|
|
616
|
+
child,
|
|
617
|
+
statements,
|
|
618
|
+
genId,
|
|
619
|
+
signals,
|
|
620
|
+
hash,
|
|
621
|
+
hydrate,
|
|
622
|
+
childNodesVar,
|
|
623
|
+
hydrate ? varName : undefined,
|
|
624
|
+
);
|
|
625
|
+
if (!hydrate) {
|
|
626
|
+
statements.push(
|
|
627
|
+
t.expressionStatement(
|
|
628
|
+
t.callExpression(
|
|
629
|
+
t.memberExpression(t.identifier(varName), t.identifier("append")),
|
|
630
|
+
[t.identifier(childVar)],
|
|
631
|
+
),
|
|
632
|
+
),
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
} else if (t.isJSXText(child)) {
|
|
636
|
+
const text = child.value;
|
|
637
|
+
if (!text.trim()) continue;
|
|
638
|
+
if (hydrate) {
|
|
639
|
+
// SSR hydration: text already in DOM from SSR, do nothing.
|
|
640
|
+
// SPA navigation: childNodesVar pool is empty, create text node.
|
|
641
|
+
const poolLen = t.memberExpression(
|
|
642
|
+
t.identifier(childNodesVar!),
|
|
643
|
+
t.identifier("length"),
|
|
644
|
+
);
|
|
645
|
+
statements.push(
|
|
646
|
+
t.ifStatement(
|
|
647
|
+
t.binaryExpression("===", poolLen, t.numericLiteral(0)),
|
|
648
|
+
t.expressionStatement(
|
|
649
|
+
t.callExpression(
|
|
650
|
+
t.memberExpression(
|
|
651
|
+
t.identifier(varName),
|
|
652
|
+
t.identifier("append"),
|
|
653
|
+
),
|
|
654
|
+
[
|
|
655
|
+
t.callExpression(
|
|
656
|
+
t.memberExpression(
|
|
657
|
+
t.identifier("document"),
|
|
658
|
+
t.identifier("createTextNode"),
|
|
659
|
+
),
|
|
660
|
+
[t.stringLiteral(text.replace(/\s*\n\s*/g, " "))],
|
|
661
|
+
),
|
|
662
|
+
],
|
|
663
|
+
),
|
|
664
|
+
),
|
|
665
|
+
),
|
|
666
|
+
);
|
|
667
|
+
} else {
|
|
668
|
+
statements.push(
|
|
669
|
+
t.expressionStatement(
|
|
670
|
+
t.callExpression(
|
|
671
|
+
t.memberExpression(t.identifier(varName), t.identifier("append")),
|
|
672
|
+
[
|
|
673
|
+
t.callExpression(
|
|
674
|
+
t.memberExpression(
|
|
675
|
+
t.identifier("document"),
|
|
676
|
+
t.identifier("createTextNode"),
|
|
677
|
+
),
|
|
678
|
+
[t.stringLiteral(text)],
|
|
679
|
+
),
|
|
680
|
+
],
|
|
681
|
+
),
|
|
682
|
+
),
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
} else if (t.isJSXExpressionContainer(child)) {
|
|
686
|
+
if (t.isJSXEmptyExpression(child.expression)) continue;
|
|
687
|
+
const expr = child.expression as t.Expression;
|
|
688
|
+
if (t.isJSXElement(expr)) {
|
|
689
|
+
const childVar = processElement(
|
|
690
|
+
expr,
|
|
691
|
+
statements,
|
|
692
|
+
genId,
|
|
693
|
+
signals,
|
|
694
|
+
hash,
|
|
695
|
+
hydrate,
|
|
696
|
+
childNodesVar,
|
|
697
|
+
hydrate ? varName : undefined,
|
|
698
|
+
);
|
|
699
|
+
if (!hydrate) {
|
|
700
|
+
statements.push(
|
|
701
|
+
t.expressionStatement(
|
|
702
|
+
t.callExpression(
|
|
703
|
+
t.memberExpression(
|
|
704
|
+
t.identifier(varName),
|
|
705
|
+
t.identifier("append"),
|
|
706
|
+
),
|
|
707
|
+
[t.identifier(childVar)],
|
|
708
|
+
),
|
|
709
|
+
),
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
} else if (t.isJSXFragment(expr)) {
|
|
713
|
+
const childVar = processFragment(
|
|
714
|
+
expr,
|
|
715
|
+
statements,
|
|
716
|
+
genId,
|
|
717
|
+
signals,
|
|
718
|
+
hash,
|
|
719
|
+
hydrate,
|
|
720
|
+
childNodesVar,
|
|
721
|
+
hydrate ? varName : undefined,
|
|
722
|
+
);
|
|
723
|
+
if (!hydrate) {
|
|
724
|
+
statements.push(
|
|
725
|
+
t.expressionStatement(
|
|
726
|
+
t.callExpression(
|
|
727
|
+
t.memberExpression(
|
|
728
|
+
t.identifier(varName),
|
|
729
|
+
t.identifier("append"),
|
|
730
|
+
),
|
|
731
|
+
[t.identifier(childVar)],
|
|
732
|
+
),
|
|
733
|
+
),
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
} else if (containsSignal(expr, signals)) {
|
|
737
|
+
if (isPrimitive(expr)) {
|
|
738
|
+
// fast path: text node + textContent
|
|
739
|
+
buildTextNode(
|
|
740
|
+
varName,
|
|
741
|
+
expr,
|
|
742
|
+
statements,
|
|
743
|
+
genId,
|
|
744
|
+
hydrate,
|
|
745
|
+
childNodesVar,
|
|
746
|
+
hydrate ? varName : undefined,
|
|
747
|
+
);
|
|
748
|
+
} else {
|
|
749
|
+
// Check for keyed list pattern
|
|
750
|
+
const keyed = findKeyedMapExpr(expr, signals);
|
|
751
|
+
if (keyed) {
|
|
752
|
+
buildKeyedList(
|
|
753
|
+
varName,
|
|
754
|
+
keyed,
|
|
755
|
+
statements,
|
|
756
|
+
genId,
|
|
757
|
+
signals,
|
|
758
|
+
processElement,
|
|
759
|
+
hash,
|
|
760
|
+
hydrate,
|
|
761
|
+
childNodesVar,
|
|
762
|
+
hydrate ? varName : undefined,
|
|
763
|
+
);
|
|
764
|
+
} else {
|
|
765
|
+
buildAnchorMount(
|
|
766
|
+
varName,
|
|
767
|
+
expr,
|
|
768
|
+
statements,
|
|
769
|
+
genId,
|
|
770
|
+
hydrate,
|
|
771
|
+
childNodesVar,
|
|
772
|
+
hydrate ? varName : undefined,
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
} else {
|
|
777
|
+
// non-reactive: set once
|
|
778
|
+
if (hydrate) {
|
|
779
|
+
const claimCommentArgs: t.Expression[] = [
|
|
780
|
+
t.identifier(childNodesVar!),
|
|
781
|
+
t.stringLiteral("g"),
|
|
782
|
+
];
|
|
783
|
+
if (varName) claimCommentArgs.push(t.identifier(varName));
|
|
784
|
+
const poolLen = t.memberExpression(
|
|
785
|
+
t.identifier(childNodesVar!),
|
|
786
|
+
t.identifier("length"),
|
|
787
|
+
);
|
|
788
|
+
// Pool has elements (initial load): SSR content already in DOM, just consume delimiters
|
|
789
|
+
const claimG = t.expressionStatement(
|
|
790
|
+
t.callExpression(t.identifier("claimComment"), claimCommentArgs),
|
|
791
|
+
);
|
|
792
|
+
const claimSlashG = (() => {
|
|
793
|
+
const args: t.Expression[] = [
|
|
794
|
+
t.identifier(childNodesVar!),
|
|
795
|
+
t.stringLiteral("/g"),
|
|
796
|
+
];
|
|
797
|
+
if (varName) args.push(t.identifier(varName));
|
|
798
|
+
return t.expressionStatement(
|
|
799
|
+
t.callExpression(t.identifier("claimComment"), args),
|
|
800
|
+
);
|
|
801
|
+
})();
|
|
802
|
+
// Pool empty (SPA nav): create and insert fresh elements
|
|
803
|
+
const insertCall = t.expressionStatement(
|
|
804
|
+
t.callExpression(t.identifier("insert"), [
|
|
805
|
+
t.identifier(varName),
|
|
806
|
+
expr,
|
|
807
|
+
]),
|
|
808
|
+
);
|
|
809
|
+
statements.push(
|
|
810
|
+
t.ifStatement(
|
|
811
|
+
t.binaryExpression(">", poolLen, t.numericLiteral(0)),
|
|
812
|
+
t.blockStatement([claimG, claimSlashG]),
|
|
813
|
+
t.blockStatement([insertCall]),
|
|
814
|
+
),
|
|
815
|
+
);
|
|
816
|
+
} else {
|
|
817
|
+
statements.push(
|
|
818
|
+
t.expressionStatement(
|
|
819
|
+
t.callExpression(t.identifier("insert"), [
|
|
820
|
+
t.identifier(varName),
|
|
821
|
+
expr,
|
|
822
|
+
]),
|
|
823
|
+
),
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
return varName;
|
|
831
|
+
}
|