@sigil-dev/compiler 0.9.7 → 0.9.11
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 +3 -1
- package/src/babel/handlers/dom/state.ts +4 -5
- package/src/babel/index.ts +77 -24
- package/src/babel/jsx/children.ts +10 -2
- package/src/babel/jsx/element.ts +209 -112
- package/src/babel/jsx/fragment.ts +15 -3
- package/src/babel/jsx/keyed-list.ts +10 -2
- package/src/babel/jsx/ssr.ts +138 -238
- package/src/babel/jsx/utils.ts +47 -1
- package/src/babel/util/bind.ts +4 -0
- package/src/babel/util/css.ts +170 -113
- package/src/babel/util/props.ts +256 -0
- package/src/bun-plugin.ts +28 -17
- package/src/vite/index.ts +4 -1
- package/test/bun-plugin.test.ts +45 -0
- package/test/hydration.test.ts +3 -2
- package/test/jsx.test.ts +26 -4
- package/test/render-parity.test.ts +639 -0
- package/test/scoped-css.test.ts +39 -2
- package/test/ssr-raw-text.test.ts +68 -0
- package/test/ssr.test.ts +1 -1
- package/test/style-object.test.ts +29 -6
package/src/babel/jsx/element.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
isGlobalComponent,
|
|
7
7
|
} from "../handlers/dom/globals";
|
|
8
8
|
import { buildBind, buildProxyBind } from "../util/bind";
|
|
9
|
+
import { isPropRead } from "../util/props.ts";
|
|
9
10
|
import { buildAnchorMount } from "./anchor-mount";
|
|
10
11
|
import { collectChildren } from "./children";
|
|
11
12
|
import { processFragment } from "./fragment";
|
|
@@ -16,7 +17,9 @@ import {
|
|
|
16
17
|
buildHydrationScope,
|
|
17
18
|
containsSignal,
|
|
18
19
|
getCreateElement,
|
|
20
|
+
hasJSX,
|
|
19
21
|
isPrimitive,
|
|
22
|
+
jsxText,
|
|
20
23
|
} from "./utils";
|
|
21
24
|
|
|
22
25
|
/**
|
|
@@ -59,6 +62,61 @@ function setPropCall(
|
|
|
59
62
|
]);
|
|
60
63
|
}
|
|
61
64
|
|
|
65
|
+
function isClaimedCall(varName: string): t.CallExpression {
|
|
66
|
+
return t.callExpression(t.identifier("isClaimed"), [t.identifier(varName)]);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function effect(body: t.Statement[]): t.ExpressionStatement {
|
|
70
|
+
return t.expressionStatement(
|
|
71
|
+
t.callExpression(t.identifier("createEffect"), [
|
|
72
|
+
t.arrowFunctionExpression([], t.blockStatement(body)),
|
|
73
|
+
]),
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Emit fresh-element-only writes; hydration guards them with isClaimed(). */
|
|
78
|
+
function pushInit(
|
|
79
|
+
varName: string,
|
|
80
|
+
init: t.Statement[],
|
|
81
|
+
statements: t.Statement[],
|
|
82
|
+
hydrate?: boolean,
|
|
83
|
+
): void {
|
|
84
|
+
if (!init.length) return;
|
|
85
|
+
if (hydrate) {
|
|
86
|
+
statements.push(
|
|
87
|
+
t.ifStatement(
|
|
88
|
+
t.unaryExpression("!", isClaimedCall(varName)),
|
|
89
|
+
t.blockStatement(init),
|
|
90
|
+
),
|
|
91
|
+
);
|
|
92
|
+
} else {
|
|
93
|
+
statements.push(...init);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** A <script>'s children as one string expression, or null if it has none. */
|
|
98
|
+
function scriptText(node: t.JSXElement): t.Expression | null {
|
|
99
|
+
const parts: t.Expression[] = [];
|
|
100
|
+
for (const child of node.children) {
|
|
101
|
+
if (t.isJSXText(child)) {
|
|
102
|
+
const text = jsxText(child);
|
|
103
|
+
if (text !== null) parts.push(t.stringLiteral(text));
|
|
104
|
+
} else if (
|
|
105
|
+
t.isJSXExpressionContainer(child) &&
|
|
106
|
+
!t.isJSXEmptyExpression(child.expression)
|
|
107
|
+
) {
|
|
108
|
+
parts.push(
|
|
109
|
+
t.logicalExpression("??", child.expression, t.stringLiteral("")),
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (!parts.length) return null;
|
|
114
|
+
return parts.reduce(
|
|
115
|
+
(a, b) => t.binaryExpression("+", a, b),
|
|
116
|
+
t.stringLiteral(""),
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
62
120
|
/**
|
|
63
121
|
* Build an object property key (identifier for valid names, string literal for dashed).
|
|
64
122
|
*/
|
|
@@ -123,13 +181,44 @@ export function processElement(
|
|
|
123
181
|
if (!t.isJSXAttribute(attr)) continue;
|
|
124
182
|
const attrName = (attr.name as t.JSXIdentifier).name;
|
|
125
183
|
|
|
126
|
-
if (
|
|
184
|
+
if (attr.value === null) {
|
|
185
|
+
// <Comp disabled /> → { disabled: true }
|
|
186
|
+
propsObj.properties.push(
|
|
187
|
+
t.objectProperty(attrKey(attrName), t.booleanLiteral(true)),
|
|
188
|
+
);
|
|
189
|
+
} else if (t.isStringLiteral(attr.value)) {
|
|
127
190
|
propsObj.properties.push(
|
|
128
191
|
t.objectProperty(attrKey(attrName), attr.value),
|
|
129
192
|
);
|
|
130
193
|
} else if (t.isJSXExpressionContainer(attr.value)) {
|
|
131
194
|
const expr = attr.value.expression as t.Expression;
|
|
132
|
-
|
|
195
|
+
// JSX in a prop (icon={<Icon />}, actions={<>…</>}) belongs
|
|
196
|
+
// wherever the component puts it, inside its own root. Built
|
|
197
|
+
// now, during hydration, it would claim from the caller's pool
|
|
198
|
+
// and could take the component's root element. Defer it like
|
|
199
|
+
// children: the component's {prop} slot runs the thunk against
|
|
200
|
+
// the right nodes.
|
|
201
|
+
if (hydrate && hasJSX(expr)) {
|
|
202
|
+
propsObj.properties.push(
|
|
203
|
+
t.objectProperty(
|
|
204
|
+
attrKey(attrName),
|
|
205
|
+
t.arrowFunctionExpression([], expr),
|
|
206
|
+
),
|
|
207
|
+
);
|
|
208
|
+
} else if (containsSignal(expr, signals)) {
|
|
209
|
+
// a prop that reads state is passed as a getter, so the
|
|
210
|
+
// component reads the current value wherever it uses it
|
|
211
|
+
propsObj.properties.push(
|
|
212
|
+
t.objectMethod(
|
|
213
|
+
"get",
|
|
214
|
+
attrKey(attrName),
|
|
215
|
+
[],
|
|
216
|
+
t.blockStatement([t.returnStatement(expr)]),
|
|
217
|
+
),
|
|
218
|
+
);
|
|
219
|
+
} else {
|
|
220
|
+
propsObj.properties.push(t.objectProperty(attrKey(attrName), expr));
|
|
221
|
+
}
|
|
133
222
|
}
|
|
134
223
|
}
|
|
135
224
|
|
|
@@ -230,18 +319,24 @@ export function processElement(
|
|
|
230
319
|
buildHydrationScope(varName, statements, childNodesVar);
|
|
231
320
|
}
|
|
232
321
|
|
|
322
|
+
// Attribute writes that only matter on a fresh element. When hydration
|
|
323
|
+
// claims server HTML they already happened there, and redoing them would
|
|
324
|
+
// wipe whatever a page script changed in the meantime.
|
|
325
|
+
const init: t.Statement[] = [];
|
|
326
|
+
// <select value> needs its options to exist first.
|
|
327
|
+
const initAfter: t.Statement[] = [];
|
|
328
|
+
const liveAfter: t.Statement[] = [];
|
|
329
|
+
|
|
233
330
|
// attributes
|
|
234
331
|
const seenAttrs = new Set<string>();
|
|
235
332
|
for (const attr of node.openingElement.attributes) {
|
|
236
333
|
if (t.isJSXSpreadAttribute(attr)) {
|
|
237
|
-
// {…obj}
|
|
334
|
+
// {…obj}: listeners always, attributes unless hydration claimed
|
|
335
|
+
// the element (then the server already rendered them)
|
|
336
|
+
const args: t.Expression[] = [t.identifier(varName), attr.argument];
|
|
337
|
+
if (hydrate) args.push(t.unaryExpression("!", isClaimedCall(varName)));
|
|
238
338
|
statements.push(
|
|
239
|
-
t.expressionStatement(
|
|
240
|
-
t.callExpression(t.identifier("Object.assign"), [
|
|
241
|
-
t.identifier(varName),
|
|
242
|
-
attr.argument as t.Expression,
|
|
243
|
-
]),
|
|
244
|
-
),
|
|
339
|
+
t.expressionStatement(t.callExpression(t.identifier("spread"), args)),
|
|
245
340
|
);
|
|
246
341
|
continue;
|
|
247
342
|
}
|
|
@@ -256,7 +351,8 @@ export function processElement(
|
|
|
256
351
|
// bindValue -> bind:value, bindChecked -> bind:checked
|
|
257
352
|
if (/^bind[A-Z]/.test(attrName)) {
|
|
258
353
|
attrNamespace = "bind";
|
|
259
|
-
|
|
354
|
+
// bindScrollTop -> bind:scrollTop: only the first letter lowers
|
|
355
|
+
attrName = attrName.charAt(4).toLowerCase() + attrName.slice(5);
|
|
260
356
|
}
|
|
261
357
|
}
|
|
262
358
|
const realAttr = ATTR_MAP[attrName] ?? attrName;
|
|
@@ -491,9 +587,19 @@ export function processElement(
|
|
|
491
587
|
).name,
|
|
492
588
|
);
|
|
493
589
|
|
|
590
|
+
// bind:checked={item.done} in a list row, or {book.read} where `book`
|
|
591
|
+
// is a prop: the chain is rooted at a row item or a props object,
|
|
592
|
+
// and what it ends in is a property of a $state proxy
|
|
593
|
+
let root: t.Node = signal;
|
|
594
|
+
while (t.isMemberExpression(root)) root = root.object;
|
|
595
|
+
const isMemberOfItem =
|
|
596
|
+
t.isMemberExpression(signal) &&
|
|
597
|
+
t.isIdentifier(root) &&
|
|
598
|
+
signals.has(root.name);
|
|
599
|
+
|
|
494
600
|
if (isTopLevelSignal) {
|
|
495
601
|
buildBind(varName, bindProp, attr, statements);
|
|
496
|
-
} else if (isMemberOfSignal) {
|
|
602
|
+
} else if (isMemberOfSignal || isMemberOfItem) {
|
|
497
603
|
buildProxyBind(varName, bindProp, attr, statements, signal);
|
|
498
604
|
} else {
|
|
499
605
|
console.warn(
|
|
@@ -504,30 +610,22 @@ export function processElement(
|
|
|
504
610
|
}
|
|
505
611
|
} else if (attr.value === null) {
|
|
506
612
|
// bare attribute (<input disabled>, <div data-beacon>)
|
|
507
|
-
|
|
508
|
-
statements.push(
|
|
509
|
-
t.expressionStatement(
|
|
510
|
-
setAttrCall(varName, realAttr, t.stringLiteral("")),
|
|
511
|
-
),
|
|
512
|
-
);
|
|
513
|
-
} else {
|
|
514
|
-
statements.push(
|
|
515
|
-
t.expressionStatement(
|
|
516
|
-
setPropCall(varName, realAttr, t.booleanLiteral(true)),
|
|
517
|
-
),
|
|
518
|
-
);
|
|
519
|
-
}
|
|
520
|
-
} else if (t.isStringLiteral(attr.value)) {
|
|
521
|
-
statements.push(
|
|
613
|
+
init.push(
|
|
522
614
|
t.expressionStatement(
|
|
523
615
|
isDashedAttr(realAttr)
|
|
524
|
-
? setAttrCall(varName, realAttr, t.stringLiteral(
|
|
525
|
-
: setPropCall(varName, realAttr,
|
|
616
|
+
? setAttrCall(varName, realAttr, t.stringLiteral(""))
|
|
617
|
+
: setPropCall(varName, realAttr, t.booleanLiteral(true)),
|
|
526
618
|
),
|
|
527
619
|
);
|
|
620
|
+
} else if (t.isStringLiteral(attr.value)) {
|
|
621
|
+
const stmt = t.expressionStatement(
|
|
622
|
+
setPropCall(varName, realAttr, t.stringLiteral(attr.value.value)),
|
|
623
|
+
);
|
|
624
|
+
(tag === "select" && realAttr === "value" ? initAfter : init).push(stmt);
|
|
528
625
|
} else if (t.isJSXExpressionContainer(attr.value)) {
|
|
529
626
|
const expr = attr.value.expression as t.Expression;
|
|
530
|
-
|
|
627
|
+
const afterChildren = tag === "select" && realAttr === "value";
|
|
628
|
+
// style={{ color: "red", fontSize: val }} — per-property writes
|
|
531
629
|
if (attrName === "style" && t.isObjectExpression(expr)) {
|
|
532
630
|
for (const prop of expr.properties) {
|
|
533
631
|
if (!t.isObjectProperty(prop)) continue;
|
|
@@ -538,112 +636,96 @@ export function processElement(
|
|
|
538
636
|
: null;
|
|
539
637
|
if (!key) continue;
|
|
540
638
|
const val = prop.value as t.Expression;
|
|
541
|
-
const
|
|
542
|
-
t.
|
|
543
|
-
|
|
639
|
+
const write = t.expressionStatement(
|
|
640
|
+
t.callExpression(t.identifier("setStyleProp"), [
|
|
641
|
+
t.identifier(varName),
|
|
642
|
+
t.stringLiteral(key),
|
|
643
|
+
val,
|
|
644
|
+
]),
|
|
544
645
|
);
|
|
545
646
|
if (containsSignal(val, signals)) {
|
|
546
|
-
statements.push(
|
|
547
|
-
t.expressionStatement(
|
|
548
|
-
t.callExpression(t.identifier("createEffect"), [
|
|
549
|
-
t.arrowFunctionExpression(
|
|
550
|
-
[],
|
|
551
|
-
t.blockStatement([
|
|
552
|
-
t.expressionStatement(
|
|
553
|
-
t.assignmentExpression("=", styleProp, val),
|
|
554
|
-
),
|
|
555
|
-
]),
|
|
556
|
-
),
|
|
557
|
-
]),
|
|
558
|
-
),
|
|
559
|
-
);
|
|
647
|
+
statements.push(effect([write]));
|
|
560
648
|
} else {
|
|
561
|
-
|
|
562
|
-
t.expressionStatement(
|
|
563
|
-
t.assignmentExpression("=", styleProp, val),
|
|
564
|
-
),
|
|
565
|
-
);
|
|
649
|
+
init.push(write);
|
|
566
650
|
}
|
|
567
651
|
}
|
|
568
|
-
// done with this attribute
|
|
569
652
|
} else if (containsSignal(expr, signals)) {
|
|
570
653
|
// Dynamic class with scoped CSS: use classList.value to preserve hash
|
|
571
654
|
if (attrName === "class" && hash) {
|
|
572
655
|
statements.push(
|
|
573
|
-
|
|
574
|
-
t.
|
|
575
|
-
t.
|
|
576
|
-
|
|
577
|
-
t.
|
|
578
|
-
t.
|
|
579
|
-
t.
|
|
580
|
-
|
|
581
|
-
t.memberExpression(
|
|
582
|
-
t.memberExpression(
|
|
583
|
-
t.identifier(varName),
|
|
584
|
-
t.identifier("classList"),
|
|
585
|
-
),
|
|
586
|
-
t.identifier("value"),
|
|
587
|
-
),
|
|
588
|
-
expr,
|
|
589
|
-
),
|
|
590
|
-
),
|
|
591
|
-
t.expressionStatement(
|
|
592
|
-
t.callExpression(
|
|
593
|
-
t.memberExpression(
|
|
594
|
-
t.memberExpression(
|
|
595
|
-
t.identifier(varName),
|
|
596
|
-
t.identifier("classList"),
|
|
597
|
-
),
|
|
598
|
-
t.identifier("add"),
|
|
599
|
-
),
|
|
600
|
-
[t.stringLiteral(hash)],
|
|
601
|
-
),
|
|
656
|
+
effect([
|
|
657
|
+
t.expressionStatement(
|
|
658
|
+
t.assignmentExpression(
|
|
659
|
+
"=",
|
|
660
|
+
t.memberExpression(
|
|
661
|
+
t.memberExpression(
|
|
662
|
+
t.identifier(varName),
|
|
663
|
+
t.identifier("classList"),
|
|
602
664
|
),
|
|
603
|
-
|
|
665
|
+
t.identifier("value"),
|
|
666
|
+
),
|
|
667
|
+
expr,
|
|
604
668
|
),
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
t.callExpression(t.identifier("createEffect"), [
|
|
613
|
-
t.arrowFunctionExpression(
|
|
614
|
-
[],
|
|
615
|
-
t.blockStatement([
|
|
616
|
-
t.expressionStatement(
|
|
617
|
-
isDashedAttr(realAttr)
|
|
618
|
-
? setAttrCall(varName, realAttr, expr)
|
|
619
|
-
: setPropCall(varName, realAttr, expr),
|
|
669
|
+
),
|
|
670
|
+
t.expressionStatement(
|
|
671
|
+
t.callExpression(
|
|
672
|
+
t.memberExpression(
|
|
673
|
+
t.memberExpression(
|
|
674
|
+
t.identifier(varName),
|
|
675
|
+
t.identifier("classList"),
|
|
620
676
|
),
|
|
621
|
-
|
|
677
|
+
t.identifier("add"),
|
|
678
|
+
),
|
|
679
|
+
[t.stringLiteral(hash)],
|
|
622
680
|
),
|
|
623
|
-
|
|
624
|
-
),
|
|
681
|
+
),
|
|
682
|
+
]),
|
|
625
683
|
);
|
|
684
|
+
} else {
|
|
685
|
+
const stmt = effect([
|
|
686
|
+
t.expressionStatement(setPropCall(varName, realAttr, expr)),
|
|
687
|
+
]);
|
|
688
|
+
(afterChildren ? liveAfter : statements).push(stmt);
|
|
626
689
|
}
|
|
627
690
|
} else {
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
t.expressionStatement(
|
|
631
|
-
isDashedAttr(realAttr)
|
|
632
|
-
? setAttrCall(varName, realAttr, expr)
|
|
633
|
-
: setPropCall(varName, realAttr, expr),
|
|
634
|
-
),
|
|
691
|
+
const stmt = t.expressionStatement(
|
|
692
|
+
setPropCall(varName, realAttr, expr),
|
|
635
693
|
);
|
|
694
|
+
(afterChildren ? initAfter : init).push(stmt);
|
|
636
695
|
}
|
|
637
696
|
}
|
|
638
697
|
}
|
|
639
698
|
|
|
699
|
+
pushInit(varName, init, statements, hydrate);
|
|
700
|
+
|
|
640
701
|
// Add scoped CSS hash to classList (if hash is present)
|
|
641
702
|
if (hash) {
|
|
642
703
|
addScopedClass(varName, hash, statements);
|
|
643
704
|
}
|
|
644
705
|
|
|
645
|
-
// children
|
|
646
|
-
|
|
706
|
+
// children. <script> text is already in the server HTML, raw and unmarked
|
|
707
|
+
// (see ssr.ts), so hydration has nothing to claim there: the marker search
|
|
708
|
+
// would miss and append a second copy of the code.
|
|
709
|
+
if (hydrate && tag === "script") {
|
|
710
|
+
// ...but SPA navigation creates the element fresh and needs the text.
|
|
711
|
+
const text = scriptText(node);
|
|
712
|
+
if (text) {
|
|
713
|
+
const write = [
|
|
714
|
+
t.expressionStatement(
|
|
715
|
+
t.assignmentExpression(
|
|
716
|
+
"=",
|
|
717
|
+
t.memberExpression(
|
|
718
|
+
t.identifier(varName),
|
|
719
|
+
t.identifier("textContent"),
|
|
720
|
+
),
|
|
721
|
+
text,
|
|
722
|
+
),
|
|
723
|
+
),
|
|
724
|
+
];
|
|
725
|
+
pushInit(varName, write, statements, hydrate);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
for (const child of hydrate && tag === "script" ? [] : node.children) {
|
|
647
729
|
if (t.isJSXElement(child)) {
|
|
648
730
|
const childVar = processElement(
|
|
649
731
|
child,
|
|
@@ -687,8 +769,8 @@ export function processElement(
|
|
|
687
769
|
);
|
|
688
770
|
}
|
|
689
771
|
} else if (t.isJSXText(child)) {
|
|
690
|
-
const text = child
|
|
691
|
-
if (
|
|
772
|
+
const text = jsxText(child);
|
|
773
|
+
if (text === null) continue;
|
|
692
774
|
if (hydrate) {
|
|
693
775
|
// SSR hydration: text already in DOM from SSR, do nothing.
|
|
694
776
|
// SPA navigation: childNodesVar pool is empty, create text node.
|
|
@@ -711,7 +793,7 @@ export function processElement(
|
|
|
711
793
|
t.identifier("document"),
|
|
712
794
|
t.identifier("createTextNode"),
|
|
713
795
|
),
|
|
714
|
-
[t.stringLiteral(text
|
|
796
|
+
[t.stringLiteral(text)],
|
|
715
797
|
),
|
|
716
798
|
],
|
|
717
799
|
),
|
|
@@ -787,6 +869,18 @@ export function processElement(
|
|
|
787
869
|
),
|
|
788
870
|
);
|
|
789
871
|
}
|
|
872
|
+
} else if (isPropRead(expr)) {
|
|
873
|
+
// a prop may hold text, nodes or a deferred child, and may be a
|
|
874
|
+
// getter over state: a live slot handles all of them
|
|
875
|
+
statements.push(
|
|
876
|
+
t.expressionStatement(
|
|
877
|
+
t.callExpression(t.identifier("liveSlot"), [
|
|
878
|
+
hydrate ? t.identifier(childNodesVar!) : t.arrayExpression([]),
|
|
879
|
+
t.identifier(varName),
|
|
880
|
+
t.arrowFunctionExpression([], expr),
|
|
881
|
+
]),
|
|
882
|
+
),
|
|
883
|
+
);
|
|
790
884
|
} else if (containsSignal(expr, signals)) {
|
|
791
885
|
if (isPrimitive(expr)) {
|
|
792
886
|
// fast path: text node + textContent
|
|
@@ -859,5 +953,8 @@ export function processElement(
|
|
|
859
953
|
}
|
|
860
954
|
}
|
|
861
955
|
|
|
956
|
+
pushInit(varName, initAfter, statements, hydrate);
|
|
957
|
+
statements.push(...liveAfter);
|
|
958
|
+
|
|
862
959
|
return varName;
|
|
863
960
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { types as t } from "@babel/core";
|
|
2
|
+
import { isPropRead } from "../util/props.ts";
|
|
2
3
|
import { buildAnchorMount } from "./anchor-mount";
|
|
3
4
|
import { processElement } from "./element";
|
|
4
5
|
import { buildKeyedList, findKeyedMapExpr } from "./keyed-list";
|
|
5
6
|
import { buildTextNode } from "./text-node";
|
|
6
|
-
import { containsSignal, isPrimitive } from "./utils";
|
|
7
|
+
import { containsSignal, isPrimitive, jsxText } from "./utils";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Process a JSX fragment (<>...</>).
|
|
@@ -76,8 +77,8 @@ export function processFragment(
|
|
|
76
77
|
);
|
|
77
78
|
}
|
|
78
79
|
} else if (t.isJSXText(child)) {
|
|
79
|
-
const text = child
|
|
80
|
-
if (
|
|
80
|
+
const text = jsxText(child);
|
|
81
|
+
if (text === null) continue;
|
|
81
82
|
if (hydrate) {
|
|
82
83
|
// SSR hydration: text already in DOM from SSR, do nothing.
|
|
83
84
|
// SPA navigation: pool is empty, create text node and append to effectiveParent.
|
|
@@ -176,6 +177,17 @@ export function processFragment(
|
|
|
176
177
|
),
|
|
177
178
|
);
|
|
178
179
|
}
|
|
180
|
+
} else if (isPropRead(expr)) {
|
|
181
|
+
// see element.ts: a prop may be text, nodes, a thunk, or a getter
|
|
182
|
+
statements.push(
|
|
183
|
+
t.expressionStatement(
|
|
184
|
+
t.callExpression(t.identifier("liveSlot"), [
|
|
185
|
+
hydrate ? t.identifier(currentNodesVar) : t.arrayExpression([]),
|
|
186
|
+
t.identifier(hydrate ? effectiveParent : varName),
|
|
187
|
+
t.arrowFunctionExpression([], expr),
|
|
188
|
+
]),
|
|
189
|
+
),
|
|
190
|
+
);
|
|
179
191
|
} else if (containsSignal(expr, signals)) {
|
|
180
192
|
if (isPrimitive(expr)) {
|
|
181
193
|
buildTextNode(
|
|
@@ -107,6 +107,14 @@ export function buildKeyedList(
|
|
|
107
107
|
keyed.keyExpr ?? t.identifier("_i"),
|
|
108
108
|
);
|
|
109
109
|
|
|
110
|
+
// Inside a row, the item is an element of a $state array, which makes it a
|
|
111
|
+
// reactive proxy: `item.done` must update the row when it changes, and
|
|
112
|
+
// bind:checked={item.done} must write back to it. Treating the item param
|
|
113
|
+
// as a signal gives its reads effects; a primitive item costs one effect
|
|
114
|
+
// that never re-runs.
|
|
115
|
+
const rowSignals = new Set(signals);
|
|
116
|
+
rowSignals.add(keyed.itemParam.name);
|
|
117
|
+
|
|
110
118
|
// Strip key prop from JSX before processing.
|
|
111
119
|
// In SSR mode, the Babel plugin already converted key → data-key.
|
|
112
120
|
// In DOM/hydrate mode, we don't need either.
|
|
@@ -139,7 +147,7 @@ export function buildKeyedList(
|
|
|
139
147
|
keyed.jsxBody,
|
|
140
148
|
createNodeStmts,
|
|
141
149
|
genId,
|
|
142
|
-
|
|
150
|
+
rowSignals,
|
|
143
151
|
hash,
|
|
144
152
|
hydrate,
|
|
145
153
|
nodesVar,
|
|
@@ -274,7 +282,7 @@ export function buildKeyedList(
|
|
|
274
282
|
keyed.jsxBody,
|
|
275
283
|
createNodeStmts,
|
|
276
284
|
genId,
|
|
277
|
-
|
|
285
|
+
rowSignals,
|
|
278
286
|
hash,
|
|
279
287
|
);
|
|
280
288
|
|