@sigil-dev/compiler 0.4.0 → 0.6.0

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.
@@ -1,516 +1,520 @@
1
- import { types as t } from "@babel/core";
2
- import { buildBind } from "../util/bind";
3
- import { buildAnchorMount } from "./anchor-mount";
4
- import { collectChildren } from "./children";
5
- import { processFragment } from "./fragment";
6
- import { buildKeyedList, findKeyedMapExpr } from "./keyed-list";
7
- import { buildTextNode } from "./text-node";
8
- import {
9
- ATTR_MAP,
10
- buildHydrationScope,
11
- containsSignal,
12
- getCreateElement,
13
- isPrimitive,
14
- } from "./utils";
15
-
16
- /**
17
- * Add classList.add(hash) statement for scoped CSS.
18
- */
19
- function addScopedClass(
20
- varName: string,
21
- hash: string,
22
- statements: t.Statement[],
23
- ): void {
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
- }
36
-
37
- export function processElement(
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
- ): string {
47
- const varName = genId();
48
- const tag = (node.openingElement.name as t.JSXIdentifier).name;
49
- const isComponent = /^[A-Z]/.test(tag);
50
-
51
- if (isComponent) {
52
- // Component: call the function with a props object
53
- const propsObj = t.objectExpression([]);
54
-
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
-
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
-
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
-
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
-
103
- return varName;
104
- }
105
-
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
-
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
-
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
-
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).expression as t.Expression;
157
- statements.push(
158
- t.expressionStatement(
159
- t.callExpression(t.identifier("applyDirective"), [
160
- t.identifier(varName),
161
- expr,
162
- ]),
163
- ),
164
- );
165
- } else if (/^on[A-Z]/.test(attrName)) {
166
- // Standard JSX: onClick, onSubmit, onMouseenter, etc.
167
- // Convert camelCase to lowercase: onClick -> click, onMouseenter -> mouseenter
168
- const event = attrName.slice(2).toLowerCase();
169
- const handler = (attr.value as t.JSXExpressionContainer)
170
- .expression as t.Expression;
171
- statements.push(
172
- t.expressionStatement(
173
- t.callExpression(
174
- t.memberExpression(
175
- t.identifier(varName),
176
- t.identifier("addEventListener"),
177
- ),
178
- [t.stringLiteral(event), handler],
179
- ),
180
- ),
181
- );
182
- } else if (attrName.startsWith("bind")) {
183
- buildBind(varName, attrName, attr, statements);
184
- } else if (t.isStringLiteral(attr.value)) {
185
- statements.push(
186
- t.expressionStatement(
187
- t.assignmentExpression(
188
- "=",
189
- t.memberExpression(t.identifier(varName), t.identifier(realAttr)),
190
- attr.value,
191
- ),
192
- ),
193
- );
194
- } else if (t.isJSXExpressionContainer(attr.value)) {
195
- const expr = attr.value.expression as t.Expression;
196
- if (containsSignal(expr, signals)) {
197
- // Dynamic class with scoped CSS: use classList.value to preserve hash
198
- if (attrName === "class" && hash) {
199
- statements.push(
200
- t.expressionStatement(
201
- t.callExpression(t.identifier("createEffect"), [
202
- t.arrowFunctionExpression(
203
- [],
204
- t.blockStatement([
205
- t.expressionStatement(
206
- t.assignmentExpression(
207
- "=",
208
- t.memberExpression(
209
- t.memberExpression(
210
- t.identifier(varName),
211
- t.identifier("classList"),
212
- ),
213
- t.identifier("value"),
214
- ),
215
- expr,
216
- ),
217
- ),
218
- t.expressionStatement(
219
- t.callExpression(
220
- t.memberExpression(
221
- t.memberExpression(
222
- t.identifier(varName),
223
- t.identifier("classList"),
224
- ),
225
- t.identifier("add"),
226
- ),
227
- [t.stringLiteral(hash)],
228
- ),
229
- ),
230
- ]),
231
- ),
232
- ]),
233
- ),
234
- );
235
- } else {
236
- // createEffect(() => varName.realAttr = expr)
237
- statements.push(
238
- t.expressionStatement(
239
- t.callExpression(t.identifier("createEffect"), [
240
- t.arrowFunctionExpression(
241
- [],
242
- t.blockStatement([
243
- t.expressionStatement(
244
- t.assignmentExpression(
245
- "=",
246
- t.memberExpression(
247
- t.identifier(varName),
248
- t.identifier(realAttr),
249
- ),
250
- expr,
251
- ),
252
- ),
253
- ]),
254
- ),
255
- ]),
256
- ),
257
- );
258
- }
259
- } else {
260
- // varName.realAttr = expr
261
- statements.push(
262
- t.expressionStatement(
263
- t.assignmentExpression(
264
- "=",
265
- t.memberExpression(t.identifier(varName), t.identifier(realAttr)),
266
- expr,
267
- ),
268
- ),
269
- );
270
- }
271
- }
272
- }
273
-
274
- // Add scoped CSS hash to classList (if hash is present)
275
- if (hash) {
276
- addScopedClass(varName, hash, statements);
277
- }
278
-
279
- // children
280
- for (const child of node.children) {
281
- if (t.isJSXElement(child)) {
282
- const childVar = processElement(
283
- child,
284
- statements,
285
- genId,
286
- signals,
287
- hash,
288
- hydrate,
289
- childNodesVar,
290
- hydrate ? varName : undefined,
291
- );
292
- if (!hydrate) {
293
- statements.push(
294
- t.expressionStatement(
295
- t.callExpression(
296
- t.memberExpression(t.identifier(varName), t.identifier("append")),
297
- [t.identifier(childVar)],
298
- ),
299
- ),
300
- );
301
- }
302
- } else if (t.isJSXFragment(child)) {
303
- const childVar = processFragment(
304
- child,
305
- statements,
306
- genId,
307
- signals,
308
- hash,
309
- hydrate,
310
- childNodesVar,
311
- hydrate ? varName : undefined,
312
- );
313
- if (!hydrate) {
314
- statements.push(
315
- t.expressionStatement(
316
- t.callExpression(
317
- t.memberExpression(t.identifier(varName), t.identifier("append")),
318
- [t.identifier(childVar)],
319
- ),
320
- ),
321
- );
322
- }
323
- } else if (t.isJSXText(child)) {
324
- const text = child.value;
325
- if (!text.trim()) continue;
326
- if (hydrate) {
327
- // SSR hydration: text already in DOM from SSR, do nothing.
328
- // SPA navigation: childNodesVar pool is empty, create text node.
329
- const poolLen = t.memberExpression(
330
- t.identifier(childNodesVar!),
331
- t.identifier("length"),
332
- );
333
- statements.push(
334
- t.ifStatement(
335
- t.binaryExpression("===", poolLen, t.numericLiteral(0)),
336
- t.expressionStatement(
337
- t.callExpression(
338
- t.memberExpression(t.identifier(varName), t.identifier("append")),
339
- [
340
- t.callExpression(
341
- t.memberExpression(
342
- t.identifier("document"),
343
- t.identifier("createTextNode"),
344
- ),
345
- [t.stringLiteral(text.replace(/\s*\n\s*/g, " "))],
346
- ),
347
- ],
348
- ),
349
- ),
350
- ),
351
- );
352
- } else {
353
- statements.push(
354
- t.expressionStatement(
355
- t.callExpression(
356
- t.memberExpression(t.identifier(varName), t.identifier("append")),
357
- [
358
- t.callExpression(
359
- t.memberExpression(
360
- t.identifier("document"),
361
- t.identifier("createTextNode"),
362
- ),
363
- [t.stringLiteral(text)],
364
- ),
365
- ],
366
- ),
367
- ),
368
- );
369
- }
370
- } else if (t.isJSXExpressionContainer(child)) {
371
- if (t.isJSXEmptyExpression(child.expression)) continue;
372
- const expr = child.expression as t.Expression;
373
- if (t.isJSXElement(expr)) {
374
- const childVar = processElement(
375
- expr,
376
- statements,
377
- genId,
378
- signals,
379
- hash,
380
- hydrate,
381
- childNodesVar,
382
- hydrate ? varName : undefined,
383
- );
384
- if (!hydrate) {
385
- statements.push(
386
- t.expressionStatement(
387
- t.callExpression(
388
- t.memberExpression(
389
- t.identifier(varName),
390
- t.identifier("append"),
391
- ),
392
- [t.identifier(childVar)],
393
- ),
394
- ),
395
- );
396
- }
397
- } else if (t.isJSXFragment(expr)) {
398
- const childVar = processFragment(
399
- expr,
400
- statements,
401
- genId,
402
- signals,
403
- hash,
404
- hydrate,
405
- childNodesVar,
406
- hydrate ? varName : undefined,
407
- );
408
- if (!hydrate) {
409
- statements.push(
410
- t.expressionStatement(
411
- t.callExpression(
412
- t.memberExpression(
413
- t.identifier(varName),
414
- t.identifier("append"),
415
- ),
416
- [t.identifier(childVar)],
417
- ),
418
- ),
419
- );
420
- }
421
- } else if (containsSignal(expr, signals)) {
422
- if (isPrimitive(expr)) {
423
- // fast path: text node + textContent
424
- buildTextNode(
425
- varName,
426
- expr,
427
- statements,
428
- genId,
429
- hydrate,
430
- childNodesVar,
431
- hydrate ? varName : undefined,
432
- );
433
- } else {
434
- // Check for keyed list pattern
435
- const keyed = findKeyedMapExpr(expr, signals);
436
- if (keyed) {
437
- buildKeyedList(
438
- varName,
439
- keyed,
440
- statements,
441
- genId,
442
- signals,
443
- processElement,
444
- hash,
445
- hydrate,
446
- childNodesVar,
447
- hydrate ? varName : undefined,
448
- );
449
- } else {
450
- buildAnchorMount(
451
- varName,
452
- expr,
453
- statements,
454
- genId,
455
- hydrate,
456
- childNodesVar,
457
- hydrate ? varName : undefined,
458
- );
459
- }
460
- }
461
- } else {
462
- // non-reactive: set once
463
- if (hydrate) {
464
- const claimCommentArgs: t.Expression[] = [
465
- t.identifier(childNodesVar!),
466
- t.stringLiteral("g"),
467
- ];
468
- if (varName) claimCommentArgs.push(t.identifier(varName));
469
- const poolLen = t.memberExpression(
470
- t.identifier(childNodesVar!),
471
- t.identifier("length"),
472
- );
473
- // Pool has elements (initial load): SSR content already in DOM, just consume delimiters
474
- const claimG = t.expressionStatement(
475
- t.callExpression(t.identifier("claimComment"), claimCommentArgs),
476
- );
477
- const claimSlashG = (() => {
478
- const args: t.Expression[] = [
479
- t.identifier(childNodesVar!),
480
- t.stringLiteral("/g"),
481
- ];
482
- if (varName) args.push(t.identifier(varName));
483
- return t.expressionStatement(
484
- t.callExpression(t.identifier("claimComment"), args),
485
- );
486
- })();
487
- // Pool empty (SPA nav): create and insert fresh elements
488
- const insertCall = t.expressionStatement(
489
- t.callExpression(t.identifier("insert"), [
490
- t.identifier(varName),
491
- expr,
492
- ]),
493
- );
494
- statements.push(
495
- t.ifStatement(
496
- t.binaryExpression(">", poolLen, t.numericLiteral(0)),
497
- t.blockStatement([claimG, claimSlashG]),
498
- t.blockStatement([insertCall]),
499
- ),
500
- );
501
- } else {
502
- statements.push(
503
- t.expressionStatement(
504
- t.callExpression(t.identifier("insert"), [
505
- t.identifier(varName),
506
- expr,
507
- ]),
508
- ),
509
- );
510
- }
511
- }
512
- }
513
- }
514
-
515
- return varName;
516
- }
1
+ import { types as t } from "@babel/core";
2
+ import { buildBind } from "../util/bind";
3
+ import { buildAnchorMount } from "./anchor-mount";
4
+ import { collectChildren } from "./children";
5
+ import { processFragment } from "./fragment";
6
+ import { buildKeyedList, findKeyedMapExpr } from "./keyed-list";
7
+ import { buildTextNode } from "./text-node";
8
+ import {
9
+ ATTR_MAP,
10
+ buildHydrationScope,
11
+ containsSignal,
12
+ getCreateElement,
13
+ isPrimitive,
14
+ } from "./utils";
15
+
16
+ /**
17
+ * Add classList.add(hash) statement for scoped CSS.
18
+ */
19
+ function addScopedClass(
20
+ varName: string,
21
+ hash: string,
22
+ statements: t.Statement[],
23
+ ): void {
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
+ }
36
+
37
+ export function processElement(
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
+ ): string {
47
+ const varName = genId();
48
+ const tag = (node.openingElement.name as t.JSXIdentifier).name;
49
+ const isComponent = /^[A-Z]/.test(tag);
50
+
51
+ if (isComponent) {
52
+ // Component: call the function with a props object
53
+ const propsObj = t.objectExpression([]);
54
+
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
+
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
+
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
+
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
+
103
+ return varName;
104
+ }
105
+
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
+
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
+
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
+
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
+ if (containsSignal(expr, signals)) {
198
+ // Dynamic class with scoped CSS: use classList.value to preserve hash
199
+ if (attrName === "class" && hash) {
200
+ statements.push(
201
+ t.expressionStatement(
202
+ t.callExpression(t.identifier("createEffect"), [
203
+ t.arrowFunctionExpression(
204
+ [],
205
+ t.blockStatement([
206
+ t.expressionStatement(
207
+ t.assignmentExpression(
208
+ "=",
209
+ t.memberExpression(
210
+ t.memberExpression(
211
+ t.identifier(varName),
212
+ t.identifier("classList"),
213
+ ),
214
+ t.identifier("value"),
215
+ ),
216
+ expr,
217
+ ),
218
+ ),
219
+ t.expressionStatement(
220
+ t.callExpression(
221
+ t.memberExpression(
222
+ t.memberExpression(
223
+ t.identifier(varName),
224
+ t.identifier("classList"),
225
+ ),
226
+ t.identifier("add"),
227
+ ),
228
+ [t.stringLiteral(hash)],
229
+ ),
230
+ ),
231
+ ]),
232
+ ),
233
+ ]),
234
+ ),
235
+ );
236
+ } else {
237
+ // createEffect(() => varName.realAttr = expr)
238
+ statements.push(
239
+ t.expressionStatement(
240
+ t.callExpression(t.identifier("createEffect"), [
241
+ t.arrowFunctionExpression(
242
+ [],
243
+ t.blockStatement([
244
+ t.expressionStatement(
245
+ t.assignmentExpression(
246
+ "=",
247
+ t.memberExpression(
248
+ t.identifier(varName),
249
+ t.identifier(realAttr),
250
+ ),
251
+ expr,
252
+ ),
253
+ ),
254
+ ]),
255
+ ),
256
+ ]),
257
+ ),
258
+ );
259
+ }
260
+ } else {
261
+ // varName.realAttr = expr
262
+ statements.push(
263
+ t.expressionStatement(
264
+ t.assignmentExpression(
265
+ "=",
266
+ t.memberExpression(t.identifier(varName), t.identifier(realAttr)),
267
+ expr,
268
+ ),
269
+ ),
270
+ );
271
+ }
272
+ }
273
+ }
274
+
275
+ // Add scoped CSS hash to classList (if hash is present)
276
+ if (hash) {
277
+ addScopedClass(varName, hash, statements);
278
+ }
279
+
280
+ // children
281
+ for (const child of node.children) {
282
+ if (t.isJSXElement(child)) {
283
+ const childVar = processElement(
284
+ child,
285
+ statements,
286
+ genId,
287
+ signals,
288
+ hash,
289
+ hydrate,
290
+ childNodesVar,
291
+ hydrate ? varName : undefined,
292
+ );
293
+ if (!hydrate) {
294
+ statements.push(
295
+ t.expressionStatement(
296
+ t.callExpression(
297
+ t.memberExpression(t.identifier(varName), t.identifier("append")),
298
+ [t.identifier(childVar)],
299
+ ),
300
+ ),
301
+ );
302
+ }
303
+ } else if (t.isJSXFragment(child)) {
304
+ const childVar = processFragment(
305
+ child,
306
+ statements,
307
+ genId,
308
+ signals,
309
+ hash,
310
+ hydrate,
311
+ childNodesVar,
312
+ hydrate ? varName : undefined,
313
+ );
314
+ if (!hydrate) {
315
+ statements.push(
316
+ t.expressionStatement(
317
+ t.callExpression(
318
+ t.memberExpression(t.identifier(varName), t.identifier("append")),
319
+ [t.identifier(childVar)],
320
+ ),
321
+ ),
322
+ );
323
+ }
324
+ } else if (t.isJSXText(child)) {
325
+ const text = child.value;
326
+ if (!text.trim()) continue;
327
+ if (hydrate) {
328
+ // SSR hydration: text already in DOM from SSR, do nothing.
329
+ // SPA navigation: childNodesVar pool is empty, create text node.
330
+ const poolLen = t.memberExpression(
331
+ t.identifier(childNodesVar!),
332
+ t.identifier("length"),
333
+ );
334
+ statements.push(
335
+ t.ifStatement(
336
+ t.binaryExpression("===", poolLen, t.numericLiteral(0)),
337
+ t.expressionStatement(
338
+ t.callExpression(
339
+ t.memberExpression(
340
+ t.identifier(varName),
341
+ t.identifier("append"),
342
+ ),
343
+ [
344
+ t.callExpression(
345
+ t.memberExpression(
346
+ t.identifier("document"),
347
+ t.identifier("createTextNode"),
348
+ ),
349
+ [t.stringLiteral(text.replace(/\s*\n\s*/g, " "))],
350
+ ),
351
+ ],
352
+ ),
353
+ ),
354
+ ),
355
+ );
356
+ } else {
357
+ statements.push(
358
+ t.expressionStatement(
359
+ t.callExpression(
360
+ t.memberExpression(t.identifier(varName), t.identifier("append")),
361
+ [
362
+ t.callExpression(
363
+ t.memberExpression(
364
+ t.identifier("document"),
365
+ t.identifier("createTextNode"),
366
+ ),
367
+ [t.stringLiteral(text)],
368
+ ),
369
+ ],
370
+ ),
371
+ ),
372
+ );
373
+ }
374
+ } else if (t.isJSXExpressionContainer(child)) {
375
+ if (t.isJSXEmptyExpression(child.expression)) continue;
376
+ const expr = child.expression as t.Expression;
377
+ if (t.isJSXElement(expr)) {
378
+ const childVar = processElement(
379
+ expr,
380
+ statements,
381
+ genId,
382
+ signals,
383
+ hash,
384
+ hydrate,
385
+ childNodesVar,
386
+ hydrate ? varName : undefined,
387
+ );
388
+ if (!hydrate) {
389
+ statements.push(
390
+ t.expressionStatement(
391
+ t.callExpression(
392
+ t.memberExpression(
393
+ t.identifier(varName),
394
+ t.identifier("append"),
395
+ ),
396
+ [t.identifier(childVar)],
397
+ ),
398
+ ),
399
+ );
400
+ }
401
+ } else if (t.isJSXFragment(expr)) {
402
+ const childVar = processFragment(
403
+ expr,
404
+ statements,
405
+ genId,
406
+ signals,
407
+ hash,
408
+ hydrate,
409
+ childNodesVar,
410
+ hydrate ? varName : undefined,
411
+ );
412
+ if (!hydrate) {
413
+ statements.push(
414
+ t.expressionStatement(
415
+ t.callExpression(
416
+ t.memberExpression(
417
+ t.identifier(varName),
418
+ t.identifier("append"),
419
+ ),
420
+ [t.identifier(childVar)],
421
+ ),
422
+ ),
423
+ );
424
+ }
425
+ } else if (containsSignal(expr, signals)) {
426
+ if (isPrimitive(expr)) {
427
+ // fast path: text node + textContent
428
+ buildTextNode(
429
+ varName,
430
+ expr,
431
+ statements,
432
+ genId,
433
+ hydrate,
434
+ childNodesVar,
435
+ hydrate ? varName : undefined,
436
+ );
437
+ } else {
438
+ // Check for keyed list pattern
439
+ const keyed = findKeyedMapExpr(expr, signals);
440
+ if (keyed) {
441
+ buildKeyedList(
442
+ varName,
443
+ keyed,
444
+ statements,
445
+ genId,
446
+ signals,
447
+ processElement,
448
+ hash,
449
+ hydrate,
450
+ childNodesVar,
451
+ hydrate ? varName : undefined,
452
+ );
453
+ } else {
454
+ buildAnchorMount(
455
+ varName,
456
+ expr,
457
+ statements,
458
+ genId,
459
+ hydrate,
460
+ childNodesVar,
461
+ hydrate ? varName : undefined,
462
+ );
463
+ }
464
+ }
465
+ } else {
466
+ // non-reactive: set once
467
+ if (hydrate) {
468
+ const claimCommentArgs: t.Expression[] = [
469
+ t.identifier(childNodesVar!),
470
+ t.stringLiteral("g"),
471
+ ];
472
+ if (varName) claimCommentArgs.push(t.identifier(varName));
473
+ const poolLen = t.memberExpression(
474
+ t.identifier(childNodesVar!),
475
+ t.identifier("length"),
476
+ );
477
+ // Pool has elements (initial load): SSR content already in DOM, just consume delimiters
478
+ const claimG = t.expressionStatement(
479
+ t.callExpression(t.identifier("claimComment"), claimCommentArgs),
480
+ );
481
+ const claimSlashG = (() => {
482
+ const args: t.Expression[] = [
483
+ t.identifier(childNodesVar!),
484
+ t.stringLiteral("/g"),
485
+ ];
486
+ if (varName) args.push(t.identifier(varName));
487
+ return t.expressionStatement(
488
+ t.callExpression(t.identifier("claimComment"), args),
489
+ );
490
+ })();
491
+ // Pool empty (SPA nav): create and insert fresh elements
492
+ const insertCall = t.expressionStatement(
493
+ t.callExpression(t.identifier("insert"), [
494
+ t.identifier(varName),
495
+ expr,
496
+ ]),
497
+ );
498
+ statements.push(
499
+ t.ifStatement(
500
+ t.binaryExpression(">", poolLen, t.numericLiteral(0)),
501
+ t.blockStatement([claimG, claimSlashG]),
502
+ t.blockStatement([insertCall]),
503
+ ),
504
+ );
505
+ } else {
506
+ statements.push(
507
+ t.expressionStatement(
508
+ t.callExpression(t.identifier("insert"), [
509
+ t.identifier(varName),
510
+ expr,
511
+ ]),
512
+ ),
513
+ );
514
+ }
515
+ }
516
+ }
517
+ }
518
+
519
+ return varName;
520
+ }