@vesk/compiler 0.2.36 → 0.2.38
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/dist/client-codegen.d.ts +2 -0
- package/dist/client-codegen.d.ts.map +1 -1
- package/dist/client-codegen.js +318 -53
- package/dist/ir-generator.d.ts.map +1 -1
- package/dist/ir-generator.js +10 -0
- package/dist/parser.js +2 -2
- package/dist/scan.d.ts +23 -0
- package/dist/scan.d.ts.map +1 -1
- package/dist/scan.js +80 -0
- package/dist/server-codegen.d.ts +1 -1
- package/dist/server-codegen.d.ts.map +1 -1
- package/dist/server-codegen.js +1 -1
- package/dist/server-head.d.ts.map +1 -1
- package/dist/server-head.js +15 -5
- package/dist/server-jsgen.d.ts.map +1 -1
- package/dist/server-jsgen.js +20 -4
- package/dist/server-render.js +5 -5
- package/dist/server-utils.d.ts +3 -1
- package/dist/server-utils.d.ts.map +1 -1
- package/dist/server-utils.js +18 -1
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
package/dist/client-codegen.js
CHANGED
|
@@ -171,6 +171,13 @@ export function transformTracked(irNode, tracked) {
|
|
|
171
171
|
if (parent.type === 'UpdateExpression' &&
|
|
172
172
|
parent.argument === node)
|
|
173
173
|
return context.next();
|
|
174
|
+
if (parent.type === 'MemberExpression' &&
|
|
175
|
+
!parent.computed &&
|
|
176
|
+
parent.property === node)
|
|
177
|
+
// Non-computed member PROPERTY names are never identifier reads —
|
|
178
|
+
// `props.items` must stay `props.items` even when a tracked binding
|
|
179
|
+
// is also named `items`; only the `.object` side rewires.
|
|
180
|
+
return context.next();
|
|
174
181
|
if (parent.type === 'Property' &&
|
|
175
182
|
parent.value === node &&
|
|
176
183
|
parent.key &&
|
|
@@ -281,6 +288,12 @@ class Ctx {
|
|
|
281
288
|
delegatedEvents = new Set();
|
|
282
289
|
directEvents = new Set();
|
|
283
290
|
hydrate = false;
|
|
291
|
+
// Markerless structural hydration: claims run `nextElement(tag, skipK)` with
|
|
292
|
+
// positional residue offsets and interior claims rebind the walker to the
|
|
293
|
+
// claimed element (subWalker). Only meaningful when `hydrate` is also true.
|
|
294
|
+
// Markerless is the DEFAULT; pass `markerless: false` to restore legacy
|
|
295
|
+
// marker-mode claim emission.
|
|
296
|
+
markerless = true;
|
|
284
297
|
inTryBody = false;
|
|
285
298
|
claimStatic = false;
|
|
286
299
|
// While set, the next StaticNode claimed in hydrate mode uses `__root` (the
|
|
@@ -331,9 +344,9 @@ class Ctx {
|
|
|
331
344
|
return '\n' + lines.join('\n');
|
|
332
345
|
}
|
|
333
346
|
}
|
|
334
|
-
function emitNode(ctx, node, tracked, effectsVar, parentVar, compPrefix = '__components') {
|
|
347
|
+
function emitNode(ctx, node, tracked, effectsVar, parentVar, compPrefix = '__components', skipK = 0, regionBudget) {
|
|
335
348
|
if (node instanceof StaticNode)
|
|
336
|
-
return emitStatic(ctx, node, tracked, effectsVar);
|
|
349
|
+
return emitStatic(ctx, node, tracked, effectsVar, skipK, regionBudget);
|
|
337
350
|
if (node instanceof TextNode) {
|
|
338
351
|
if (!node.value)
|
|
339
352
|
return null;
|
|
@@ -352,11 +365,11 @@ function emitNode(ctx, node, tracked, effectsVar, parentVar, compPrefix = '__com
|
|
|
352
365
|
if (node instanceof ComponentRef)
|
|
353
366
|
return null;
|
|
354
367
|
if (node instanceof ComponentCall)
|
|
355
|
-
return emitComponentCall(ctx, node, tracked, effectsVar, parentVar, compPrefix);
|
|
368
|
+
return emitComponentCall(ctx, node, tracked, effectsVar, parentVar, compPrefix, skipK, regionBudget);
|
|
356
369
|
if (node instanceof OpaqueDynamicRegion)
|
|
357
|
-
return emitOpaque(ctx, node, tracked, effectsVar, parentVar);
|
|
370
|
+
return emitOpaque(ctx, node, tracked, effectsVar, parentVar, skipK, regionBudget);
|
|
358
371
|
if (node instanceof MapRegion)
|
|
359
|
-
return emitMap(ctx, node, tracked, effectsVar, parentVar);
|
|
372
|
+
return emitMap(ctx, node, tracked, effectsVar, parentVar, skipK, regionBudget);
|
|
360
373
|
if (node instanceof ServerBlock)
|
|
361
374
|
return null;
|
|
362
375
|
if (node instanceof ClientBlock) {
|
|
@@ -397,13 +410,13 @@ function emitNode(ctx, node, tracked, effectsVar, parentVar, compPrefix = '__com
|
|
|
397
410
|
return null;
|
|
398
411
|
}
|
|
399
412
|
if (node instanceof TryCatch)
|
|
400
|
-
return emitTryCatch(ctx, node, tracked, effectsVar, parentVar);
|
|
413
|
+
return emitTryCatch(ctx, node, tracked, effectsVar, parentVar, skipK, regionBudget);
|
|
401
414
|
if (node instanceof WhileLoop)
|
|
402
|
-
return emitWhileLoop(ctx, node, tracked, parentVar);
|
|
415
|
+
return emitWhileLoop(ctx, node, tracked, parentVar, skipK, regionBudget);
|
|
403
416
|
if (node instanceof ForLoop)
|
|
404
|
-
return emitForLoop(ctx, node, tracked, parentVar);
|
|
417
|
+
return emitForLoop(ctx, node, tracked, parentVar, skipK, regionBudget);
|
|
405
418
|
if (node instanceof SwitchBlock)
|
|
406
|
-
return emitSwitchBlock(ctx, node, tracked, parentVar);
|
|
419
|
+
return emitSwitchBlock(ctx, node, tracked, parentVar, skipK, regionBudget);
|
|
407
420
|
if (node instanceof PropSlot)
|
|
408
421
|
return null;
|
|
409
422
|
if (node instanceof PropSlotRender) {
|
|
@@ -439,7 +452,7 @@ const PROPERTY_ATTRS = {
|
|
|
439
452
|
option: new Set(['selected']),
|
|
440
453
|
progress: new Set(['value']),
|
|
441
454
|
};
|
|
442
|
-
function emitStatic(ctx, node, tracked, effectsVar) {
|
|
455
|
+
function emitStatic(ctx, node, tracked, effectsVar, skipK = 0, regionBudget) {
|
|
443
456
|
const el = ctx.n();
|
|
444
457
|
if (ctx.hydrate) {
|
|
445
458
|
const claim = ctx.claimStatic;
|
|
@@ -448,10 +461,30 @@ function emitStatic(ctx, node, tracked, effectsVar) {
|
|
|
448
461
|
return null;
|
|
449
462
|
const rootClaim = ctx.rootClaim;
|
|
450
463
|
ctx.rootClaim = false;
|
|
464
|
+
// Markerless mode positions the claim explicitly: `skipK` is the number of
|
|
465
|
+
// source siblings already consumed before this node (static residues the
|
|
466
|
+
// caller retrieved from `__vsk_ssrEls`, preceding claims, etc.). Marker
|
|
467
|
+
// mode keeps the raw nextElement — the marker stream positions it.
|
|
468
|
+
//
|
|
469
|
+
// `regionBudget` is a markerless first-claim budget `let __pk = <N>`
|
|
470
|
+
// declared by an enclosing region: the first claim that EXECUTES consumes
|
|
471
|
+
// it (the region's SSR elements sit `N` source slots after the parent
|
|
472
|
+
// cursor — leftover pure-static residues the walker never claimed), then
|
|
473
|
+
// it drops to 0 so every later claim resolves positionally on the walker's
|
|
474
|
+
// advancing cursor. It only applies to THIS claim — the element's children
|
|
475
|
+
// are claimed on their own sub-walker and carry their own residue math.
|
|
476
|
+
const skipExpr = regionBudget && ctx.markerless ? markerlessSkipExpr(regionBudget, skipK) : skipK;
|
|
477
|
+
const skipArg = ctx.markerless ? `, ${skipExpr}` : '';
|
|
478
|
+
// `rootClaim` marks the keyed renderItem root: claimed items supply the
|
|
479
|
+
// SSR element as `__root`, fresh items must CREATE one. The walker is never
|
|
480
|
+
// valid here as a fallback — its cursor doesn't advance past claimByKey, so
|
|
481
|
+
// `nextElement` would steal a neighboring SSR node to render the new item.
|
|
451
482
|
const claimedExpr = rootClaim
|
|
452
|
-
? `__root ||
|
|
453
|
-
: `${ctx.walker}.nextElement(${JSON.stringify(node.tag)})`;
|
|
483
|
+
? `__root || document.createElement(${JSON.stringify(node.tag)})`
|
|
484
|
+
: `${ctx.walker}.nextElement(${JSON.stringify(node.tag)}${skipArg})`;
|
|
454
485
|
ctx.push(`const ${el} = ${claimedExpr};`);
|
|
486
|
+
if (regionBudget && ctx.markerless)
|
|
487
|
+
ctx.push(`${regionBudget} = 0;`);
|
|
455
488
|
}
|
|
456
489
|
else {
|
|
457
490
|
ctx.push(`const ${el} = document.createElement(${JSON.stringify(node.tag)});`);
|
|
@@ -491,7 +524,55 @@ function emitStatic(ctx, node, tracked, effectsVar) {
|
|
|
491
524
|
singleReactiveText = textKids.length === 1 && textKids[0] instanceof DynamicBinding
|
|
492
525
|
&& isReactiveExpression(textKids[0].expression, tracked);
|
|
493
526
|
}
|
|
527
|
+
// Markerless scoping: any descendant claim (nested static with dynamic
|
|
528
|
+
// content, component call, region, loop) must resolve against THIS claimed
|
|
529
|
+
// element's own children, not the page walker — rebind the walker to a
|
|
530
|
+
// sub-walker over `el` for the children pass. Fully-static children and the
|
|
531
|
+
// single text-snapshot are covered within the loop and never claim, so they
|
|
532
|
+
// don't force a sub-walker. Rebound, never leaked: marker mode is untouched.
|
|
533
|
+
const savedWalker = ctx.walker;
|
|
534
|
+
let needsSubWalker = false;
|
|
535
|
+
if (ctx.hydrate && ctx.markerless) {
|
|
536
|
+
for (const child of children) {
|
|
537
|
+
if (child instanceof TextNode)
|
|
538
|
+
continue;
|
|
539
|
+
if (child instanceof DynamicBinding && child.kind === 'text')
|
|
540
|
+
continue;
|
|
541
|
+
if (child instanceof StaticNode && isStaticIR(child.children))
|
|
542
|
+
continue;
|
|
543
|
+
if (child instanceof TrackDecl)
|
|
544
|
+
continue;
|
|
545
|
+
if (child instanceof ComponentRef)
|
|
546
|
+
continue;
|
|
547
|
+
if (child instanceof ServerBlock)
|
|
548
|
+
continue;
|
|
549
|
+
if (child instanceof ClientBlock)
|
|
550
|
+
continue;
|
|
551
|
+
if (child instanceof HeadBlock)
|
|
552
|
+
continue;
|
|
553
|
+
if (child instanceof RuntimeStatement)
|
|
554
|
+
continue;
|
|
555
|
+
if (child instanceof PropSlot)
|
|
556
|
+
continue;
|
|
557
|
+
needsSubWalker = true;
|
|
558
|
+
break;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
if (needsSubWalker) {
|
|
562
|
+
const sub = ctx.n();
|
|
563
|
+
ctx.push(`const ${sub} = ${ctx.walker}.subWalker(${el});`);
|
|
564
|
+
ctx.walker = sub;
|
|
565
|
+
}
|
|
494
566
|
let residueBefore = 0;
|
|
567
|
+
// Markerless skipK bookkeeping: `residueBefore` is the absolute element-slot
|
|
568
|
+
// index (used for `__vsk_ssrEls` retrieval and text-slot anchoring) and
|
|
569
|
+
// counts CLAIMED children too. But the structural walker advances its own
|
|
570
|
+
// cursor for every claimed child, so skipK must only stand in for slots the
|
|
571
|
+
// walker will NOT consume: pure-static children (retrieved from
|
|
572
|
+
// `__vsk_ssrEls`, never via nextElement) and text/other zero-residue nodes.
|
|
573
|
+
// Counting claimed residue into skipK double-jumps the cursor past the list
|
|
574
|
+
// and exhausts the walker early — fresh nodes where SSR already bound one.
|
|
575
|
+
let pendingSkip = 0;
|
|
495
576
|
for (const child of children) {
|
|
496
577
|
let childVar;
|
|
497
578
|
// Hydrate mode: pure static subtree children of a claimed element are already
|
|
@@ -563,12 +644,21 @@ function emitStatic(ctx, node, tracked, effectsVar) {
|
|
|
563
644
|
// duplicates after the SSR text (the SSR <h1> "compiler is the
|
|
564
645
|
// product." regression). Keep the element as-is and don't recurse.
|
|
565
646
|
childVar = ssrEl;
|
|
647
|
+
// The walker cursor never advances past this retrieved slot — it was
|
|
648
|
+
// never claimed via nextElement — so the next claim must skip it.
|
|
649
|
+
pendingSkip += ssrResidueEstimate(child);
|
|
566
650
|
}
|
|
567
651
|
else if (singleReactiveText && child instanceof DynamicBinding && child.kind === 'text') {
|
|
568
652
|
childVar = emitDynamicBinding(ctx, child, tracked, effectsVar, `${el}.__vsk_ssrText`);
|
|
569
653
|
}
|
|
570
654
|
else {
|
|
571
|
-
childVar = emitNode(ctx, child, tracked, effectsVar, el);
|
|
655
|
+
childVar = emitNode(ctx, child, tracked, effectsVar, el, '__components', pendingSkip);
|
|
656
|
+
// This node claims at least one walker slot (typically a subWalker over
|
|
657
|
+
// this element). The walker cursor advances past the claim AND past every
|
|
658
|
+
// unclaimed pure-static sibling it just skipped over, so relative skip
|
|
659
|
+
// accounting restarts from zero for the next sibling.
|
|
660
|
+
if (claimsWalkerNode(ctx, child))
|
|
661
|
+
pendingSkip = 0;
|
|
572
662
|
}
|
|
573
663
|
if (childVar) {
|
|
574
664
|
if (ctx.hydrate) {
|
|
@@ -640,6 +730,8 @@ function emitStatic(ctx, node, tracked, effectsVar) {
|
|
|
640
730
|
}
|
|
641
731
|
}
|
|
642
732
|
}
|
|
733
|
+
if (ctx.walker !== savedWalker)
|
|
734
|
+
ctx.walker = savedWalker;
|
|
643
735
|
return el;
|
|
644
736
|
}
|
|
645
737
|
function emitClientHead(ctx, node, tracked) {
|
|
@@ -823,10 +915,18 @@ function emitFragment(ctx, nodes, tracked, effectsVar) {
|
|
|
823
915
|
const savedEffects = ctx.effects;
|
|
824
916
|
ctx.effects = [];
|
|
825
917
|
ctx.push(`const ${frag} = (() => { const $f = document.createDocumentFragment();`);
|
|
918
|
+
let fragPendingSkip = 0;
|
|
826
919
|
for (const child of nodes) {
|
|
827
|
-
const childVar = emitNode(ctx, child, tracked, effectsVar, '$f');
|
|
920
|
+
const childVar = emitNode(ctx, child, tracked, effectsVar, '$f', '__components', fragPendingSkip);
|
|
828
921
|
if (childVar)
|
|
829
922
|
ctx.push(`$f.appendChild(${childVar});`);
|
|
923
|
+
// Markerless: only unclaimed residue must be skipped by the next claim.
|
|
924
|
+
// Claiming children advance the walker cursor themselves; pure-static
|
|
925
|
+
// children retrieved from the SSR element list never do.
|
|
926
|
+
if (claimsWalkerNode(ctx, child))
|
|
927
|
+
fragPendingSkip = 0;
|
|
928
|
+
else
|
|
929
|
+
fragPendingSkip += ssrResidueEstimate(child);
|
|
830
930
|
}
|
|
831
931
|
for (const eff of ctx.effects)
|
|
832
932
|
ctx.push(effectsVar ? `${effectsVar}.push(${effectBlockToHandlerExpr(eff)});` : eff);
|
|
@@ -834,7 +934,7 @@ function emitFragment(ctx, nodes, tracked, effectsVar) {
|
|
|
834
934
|
ctx.push(`return $f; })();`);
|
|
835
935
|
return frag;
|
|
836
936
|
}
|
|
837
|
-
function emitComponentCall(ctx, node, tracked, effectsVar, parentVar, compPrefix = '__components') {
|
|
937
|
+
function emitComponentCall(ctx, node, tracked, effectsVar, parentVar, compPrefix = '__components', skipK = 0, regionBudget) {
|
|
838
938
|
const propsEntries = node.props.map((p) => {
|
|
839
939
|
if (typeof p.value === 'string')
|
|
840
940
|
return `${JSON.stringify(p.name)}: ${JSON.stringify(p.value)}`;
|
|
@@ -868,6 +968,14 @@ function emitComponentCall(ctx, node, tracked, effectsVar, parentVar, compPrefix
|
|
|
868
968
|
?? (ctx.importedNames.has(node.componentName)
|
|
869
969
|
? node.componentName
|
|
870
970
|
: `__components[${JSON.stringify(node.componentName)}]`);
|
|
971
|
+
// Registry lookups can resolve to `undefined` when a JSX tag names a
|
|
972
|
+
// component that was never declared, imported, or registered (e.g.
|
|
973
|
+
// `<Slot/>` in a framework where `{props.children}` is the channel).
|
|
974
|
+
// Guard BEFORE the call so the failure is an actionable message instead
|
|
975
|
+
// of `undefined is not a function` mid-render.
|
|
976
|
+
if (!calleeExpr && !ctx.importedNames.has(node.componentName)) {
|
|
977
|
+
ctx.push(`if (${access} == null) throw new Error(${JSON.stringify(`Component "${node.componentName}" was not found while rendering. Declare it with the \`component\` keyword, import it, or register it in the component registry.`)});`);
|
|
978
|
+
}
|
|
871
979
|
// SSR for component calls is marker-ONLY (`<!--vsk-->` + the component's
|
|
872
980
|
// own root; no display:contents wrapper). Compiled `.vsk` components claim
|
|
873
981
|
// their own root on the SHARED walker. Call targets that are not known
|
|
@@ -876,13 +984,34 @@ function emitComponentCall(ctx, node, tracked, effectsVar, parentVar, compPrefix
|
|
|
876
984
|
// with the fresh node they returned — never a duplicate, never a box.
|
|
877
985
|
const plainTarget = !!(calleeExpr || ctx.importedNames.has(node.componentName)) && !ctx.selfClaimNames.has(node.componentName);
|
|
878
986
|
const walkerArg = ctx.walker;
|
|
987
|
+
// Markerless value-thread offset (Phase 3.c): the residue that precedes THIS
|
|
988
|
+
// call site — a static sibling the walker never claimed (`skipK`), or a
|
|
989
|
+
// region/branch budget (`regionBudget`), or both, which ADD. It must reach
|
|
990
|
+
// the child's first top-level claim even though that code lives in a
|
|
991
|
+
// separately compiled function with its own `topPendingSkip = 0`:
|
|
992
|
+
// - compiled (self-claiming) children receive it as a transient deposit on
|
|
993
|
+
// the shared walker they are invoked with (`injectSkipK` before the call,
|
|
994
|
+
// consumed by their first claim). A region budget is spent in the process
|
|
995
|
+
// so later claims in the branch resolve positionally.
|
|
996
|
+
// - plain targets (imported/member expressions) cannot claim, so the offset
|
|
997
|
+
// instead flows into the post-call `claimOnly` adoption.
|
|
998
|
+
const promptExpr = ctx.markerless && (regionBudget || skipK > 0)
|
|
999
|
+
? markerlessSkipExpr(regionBudget, skipK)
|
|
1000
|
+
: null;
|
|
879
1001
|
const maybeReplace = (v) => {
|
|
880
1002
|
if (!plainTarget)
|
|
881
1003
|
return;
|
|
882
1004
|
// nodeType 11 = DocumentFragment: self-claiming imports (Link/Md/Form)
|
|
883
1005
|
// return a fragment when they adopted their SSR root, so there is nothing
|
|
884
1006
|
// to replace — claiming here would steal the NEXT sibling's root.
|
|
885
|
-
|
|
1007
|
+
const claim = promptExpr
|
|
1008
|
+
? `${walkerArg}.claimOnly(undefined, ${promptExpr})`
|
|
1009
|
+
: `${walkerArg}.claimOnly()`;
|
|
1010
|
+
ctx.push(`if (${v} && ${v}.parentNode == null && ${v}.nodeType !== 11) { const __sr = ${claim}; if (__sr && __sr.parentNode) __sr.parentNode.replaceChild(${v}, __sr); }`);
|
|
1011
|
+
// A plain target consumes the region budget at adoption; subsequent
|
|
1012
|
+
// claims in the enclosing branch must resolve positionally from here.
|
|
1013
|
+
if (regionBudget && ctx.markerless)
|
|
1014
|
+
ctx.push(`${regionBudget} = 0;`);
|
|
886
1015
|
};
|
|
887
1016
|
const maybeRetire = () => {
|
|
888
1017
|
if (ctx.selfClaimNames.has(node.componentName)) {
|
|
@@ -914,9 +1043,23 @@ function emitComponentCall(ctx, node, tracked, effectsVar, parentVar, compPrefix
|
|
|
914
1043
|
ctx.push(`return $f; })();`);
|
|
915
1044
|
propsEntries.push(`children: ${frag}`);
|
|
916
1045
|
}
|
|
1046
|
+
if (ctx.markerless && promptExpr && !plainTarget) {
|
|
1047
|
+
// Deposit the residue for the child's first top-level claim (it runs on
|
|
1048
|
+
// this same walker with its own zero `topPendingSkip`). A region budget is
|
|
1049
|
+
// spent by THIS call, so sibling claims in the branch stay positional.
|
|
1050
|
+
ctx.push(`${walkerArg}.injectSkipK(${promptExpr});`);
|
|
1051
|
+
}
|
|
917
1052
|
ctx.push(`let ${v} = undefined;`);
|
|
918
1053
|
ctx.push(`if (__hydrate) __hydrate.childFrame = (__hydrate.childFrame || 0) + 1;`);
|
|
919
1054
|
ctx.push(`try { ${v} = ${awaitKw}${access}(${callArgs()}, __registry, ${walkerArg}); } finally { if (__hydrate) __hydrate.childFrame--; }`);
|
|
1055
|
+
// Spend the region budget only once the call RETURNS: a self-claiming child
|
|
1056
|
+
// that throws before claiming (the try/catch fallback in / with fail:true)
|
|
1057
|
+
// consumed the takeSkipK deposit but never advanced the walker cursor, so the
|
|
1058
|
+
// enclosing catch branch must still see the original budget. Zeroing it before
|
|
1059
|
+
// the call made the catch claim with skipK 0 and adopt the WRONG SSR slot
|
|
1060
|
+
// (the Appxx duplicate-claims / orphaned SSR Count + Insufficient p nodes).
|
|
1061
|
+
if (ctx.markerless && promptExpr && !plainTarget && regionBudget)
|
|
1062
|
+
ctx.push(`${regionBudget} = 0;`);
|
|
920
1063
|
maybeReplace(v);
|
|
921
1064
|
maybeRetire();
|
|
922
1065
|
return v;
|
|
@@ -949,6 +1092,7 @@ function emitComponentCall(ctx, node, tracked, effectsVar, parentVar, compPrefix
|
|
|
949
1092
|
ctx.push(`const ${v} = ${awaitKw}${node.componentName}(${callArgs()});`);
|
|
950
1093
|
}
|
|
951
1094
|
else {
|
|
1095
|
+
ctx.push(`if (${compPrefix}[${JSON.stringify(node.componentName)}] == null) throw new Error(${JSON.stringify(`Component "${node.componentName}" was not found while rendering. Declare it with the \`component\` keyword, import it, or register it in the component registry.`)});`);
|
|
952
1096
|
ctx.push(`const ${v} = ${awaitKw}${compPrefix}[${JSON.stringify(node.componentName)}](${callArgs()});`);
|
|
953
1097
|
}
|
|
954
1098
|
}
|
|
@@ -970,7 +1114,7 @@ function emitComponentCall(ctx, node, tracked, effectsVar, parentVar, compPrefix
|
|
|
970
1114
|
// walker slot moves their content to the element's following sibling slot, which
|
|
971
1115
|
// makes the icon vanish from the button on re-render. `rootLevel` should be
|
|
972
1116
|
// `parent === '$root'`.
|
|
973
|
-
function emitHydrateFenceAnchoring(ctx, anchor, endAnchor, rootLevel) {
|
|
1117
|
+
function emitHydrateFenceAnchoring(ctx, anchor, endAnchor, rootLevel, offsetExpr) {
|
|
974
1118
|
if (!ctx.hydrate || !rootLevel)
|
|
975
1119
|
return;
|
|
976
1120
|
// `rootLevel` (fallback parent is `$root`) already implies the region is a
|
|
@@ -981,24 +1125,77 @@ function emitHydrateFenceAnchoring(ctx, anchor, endAnchor, rootLevel) {
|
|
|
981
1125
|
// at the region's SSR slot (e.g. a polish-mobile menu panel that follows a
|
|
982
1126
|
// `<header>` in a `<>` fragment), so a no-content nested region must anchor
|
|
983
1127
|
// there instead of drifting to the page root's end via `__place`'s fallback.
|
|
1128
|
+
// Anchor against the CURRENT walker (`ctx.walker`), not always `__hydrate`:
|
|
1129
|
+
// for a region nested inside a claimed element the row fences must land inside
|
|
1130
|
+
// that element (where its `__place` fallback lives), otherwise a non-keyed
|
|
1131
|
+
// list's row claims get relocated to the top-level root and its SSR content
|
|
1132
|
+
// leaks out of the container. Root-level regions keep `ctx.walker ===
|
|
1133
|
+
// '__hydrate'`, so their emission is unchanged.
|
|
984
1134
|
ctx.push(`// Anchor region fences to the SSR slot so no-content regions do not fall back to $root's end.`);
|
|
985
|
-
ctx.push(`if (
|
|
986
|
-
ctx.push(indent(
|
|
987
|
-
ctx.push(indent(
|
|
1135
|
+
ctx.push(`if (${ctx.walker} && ${ctx.walker}.insertBeforeNextClaim) {`);
|
|
1136
|
+
ctx.push(indent(`${ctx.walker}.insertBeforeNextClaim(${anchor}${offsetExpr ? `, ${offsetExpr}` : ''});`));
|
|
1137
|
+
ctx.push(indent(`${ctx.walker}.insertBeforeNextClaim(${endAnchor}${offsetExpr ? `, ${offsetExpr}` : ''});`));
|
|
988
1138
|
ctx.push(`}`);
|
|
989
1139
|
}
|
|
990
|
-
|
|
1140
|
+
// Markerless first-claim budget for a dynamic region's SSR offset.
|
|
1141
|
+
//
|
|
1142
|
+
// A region is emitted into a scope whose walker cursor has NOT advanced over the
|
|
1143
|
+
// pure-static residues that precede it (they were retrieved from `__vsk_ssrEls`,
|
|
1144
|
+
// never claimed), so the region's SSR elements sit `skipK` source slots past the
|
|
1145
|
+
// cursor. Every turn a branch renders, its claims are emitted against that same
|
|
1146
|
+
// cursor — the FIRST claim of whichever branch actually executes must translate
|
|
1147
|
+
// the offset into a positional skip. Each region therefore declares a budget
|
|
1148
|
+
// `let __pk = skipK` and references it from EVERY claim in its branch bodies:
|
|
1149
|
+
// the first claim to execute consumes it (`nextElement(tag, __pk)`, then
|
|
1150
|
+
// `__pk = 0`), and every later claim — same branch, other branches, later loop
|
|
1151
|
+
// iterations, map items — resolves positionally on the advancing cursor. Text
|
|
1152
|
+
// children never claim a walker slot, so they leave the budget intact for the
|
|
1153
|
+
// next element claim (matching the walker, which ignores text nodes).
|
|
1154
|
+
//
|
|
1155
|
+
// A nested region that is itself a branch's first claim has its own skipK of 0
|
|
1156
|
+
// and inherits the enclosing branch's budget instead of declaring a fresh one —
|
|
1157
|
+
// exactly one claim (its first) still consumes the offset. A nested region with
|
|
1158
|
+
// its own positive skipK (residue inside ITS parent, e.g. a claimed element)
|
|
1159
|
+
// declares a fresh budget; the enclosing budget was already consumed by that
|
|
1160
|
+
// element's own claim.
|
|
1161
|
+
function regionBudgetVar(ctx, skipK, regionBudget, inheritedInit) {
|
|
1162
|
+
if (!ctx.markerless || !ctx.hydrate)
|
|
1163
|
+
return null;
|
|
1164
|
+
if (skipK > 0) {
|
|
1165
|
+
const pk = ctx.n();
|
|
1166
|
+
ctx.push(`let ${pk} = ${skipK}; // first-claim budget: region SSR sits ${skipK} slot(s) past the walker cursor`);
|
|
1167
|
+
return pk;
|
|
1168
|
+
}
|
|
1169
|
+
if (inheritedInit) {
|
|
1170
|
+
const pk = ctx.n();
|
|
1171
|
+
ctx.push(`let ${pk} = ${inheritedInit}; // first-claim budget: inherited from the caller's walker residue`);
|
|
1172
|
+
return pk;
|
|
1173
|
+
}
|
|
1174
|
+
return regionBudget || null;
|
|
1175
|
+
}
|
|
1176
|
+
// Combine a compile-time residue offset (`skipK`) with a runtime first-claim
|
|
1177
|
+
// budget (`regionBudget` — a `let __pk = …` var declared by a region or the
|
|
1178
|
+
// inherited walker deposit) into a single skip expression. When both are
|
|
1179
|
+
// present they ADD: the budget is the CALLER's residue past the cursor, while
|
|
1180
|
+
// skipK is residue WITHIN the current scope; the claim must skip both.
|
|
1181
|
+
function markerlessSkipExpr(regionBudget, skipK) {
|
|
1182
|
+
if (regionBudget)
|
|
1183
|
+
return skipK > 0 ? `${regionBudget} + ${skipK}` : regionBudget;
|
|
1184
|
+
return String(skipK);
|
|
1185
|
+
}
|
|
1186
|
+
function emitTryCatch(ctx, node, tracked, effectsVar, parentVar, skipK = 0, regionBudget) {
|
|
991
1187
|
const anchor = ctx.n();
|
|
992
1188
|
const endAnchor = ctx.n();
|
|
993
1189
|
const effArr = ctx.n();
|
|
994
1190
|
const catchParam = node.catchParamName || '__e';
|
|
995
1191
|
const parent = parentVar || '$root';
|
|
1192
|
+
const budget = ctx.hydrate ? regionBudgetVar(ctx, skipK, regionBudget) : null;
|
|
996
1193
|
ctx.push(`const ${anchor} = document.createComment('try');`);
|
|
997
1194
|
if (!ctx.hydrate)
|
|
998
1195
|
ctx.push(`${parent}.appendChild(${anchor});`);
|
|
999
1196
|
ctx.push(`let ${effArr} = [];`);
|
|
1000
1197
|
ctx.push(`const ${endAnchor} = document.createComment('try-end');`);
|
|
1001
|
-
emitHydrateFenceAnchoring(ctx, anchor, endAnchor, parent === '$root');
|
|
1198
|
+
emitHydrateFenceAnchoring(ctx, anchor, endAnchor, parent === '$root', budget || undefined);
|
|
1002
1199
|
const asyncKw = ctx.isAsyncScope ? 'await ' : '';
|
|
1003
1200
|
function emitRenderFunc(name, isCatch, hydMode, compPrefix = '__components') {
|
|
1004
1201
|
const savedHydrate = ctx.hydrate;
|
|
@@ -1016,7 +1213,7 @@ function emitTryCatch(ctx, node, tracked, effectsVar, parentVar) {
|
|
|
1016
1213
|
ctx.push(indent(`const __cl = [];`));
|
|
1017
1214
|
}
|
|
1018
1215
|
for (const child of body) {
|
|
1019
|
-
const childVar = emitNode(ctx, child, tracked, isCatch ? null : effArr, hydMode ? undefined : '__p', compPrefix);
|
|
1216
|
+
const childVar = emitNode(ctx, child, tracked, isCatch ? null : effArr, hydMode ? undefined : '__p', compPrefix, 0, budget || undefined);
|
|
1020
1217
|
if (childVar) {
|
|
1021
1218
|
if (hydMode)
|
|
1022
1219
|
ctx.push(indent(`__cl.push(${childVar});`));
|
|
@@ -1067,9 +1264,10 @@ function emitTryCatch(ctx, node, tracked, effectsVar, parentVar) {
|
|
|
1067
1264
|
}`);
|
|
1068
1265
|
return null;
|
|
1069
1266
|
}
|
|
1070
|
-
function emitOpaque(ctx, node, tracked, effTarget, parentVar) {
|
|
1267
|
+
function emitOpaque(ctx, node, tracked, effTarget, parentVar, skipK = 0, regionBudget) {
|
|
1071
1268
|
const hyd = ctx.hydrate;
|
|
1072
1269
|
const parent = parentVar || '$root';
|
|
1270
|
+
const budget = hyd ? regionBudgetVar(ctx, skipK, regionBudget) : null;
|
|
1073
1271
|
const isOpaque = (n) => !!n && typeof n.condition !== 'undefined';
|
|
1074
1272
|
if (hyd && node.alternateNodes.length === 1 && isOpaque(node.alternateNodes[0])) {
|
|
1075
1273
|
const chain = [];
|
|
@@ -1091,7 +1289,7 @@ function emitOpaque(ctx, node, tracked, effTarget, parentVar) {
|
|
|
1091
1289
|
ctx.push(`const ${anchor} = document.createComment('if');`);
|
|
1092
1290
|
ctx.push(`let ${effectsVar} = [];`);
|
|
1093
1291
|
ctx.push(`const ${endAnchor} = document.createComment('if-end');`);
|
|
1094
|
-
emitHydrateFenceAnchoring(ctx, anchor, endAnchor, parent === '$root');
|
|
1292
|
+
emitHydrateFenceAnchoring(ctx, anchor, endAnchor, parent === '$root', budget || undefined);
|
|
1095
1293
|
const asyncKw = ctx.isAsyncScope ? 'await ' : '';
|
|
1096
1294
|
const fnOpen = ctx.isAsyncScope ? 'async () => {' : '() => {';
|
|
1097
1295
|
const renderNames = [];
|
|
@@ -1105,7 +1303,7 @@ function emitOpaque(ctx, node, tracked, effTarget, parentVar) {
|
|
|
1105
1303
|
const savedClaim = ctx.claimStatic;
|
|
1106
1304
|
if (hyd)
|
|
1107
1305
|
ctx.claimStatic = true;
|
|
1108
|
-
const v = emitNode(ctx, n, tracked, effectsVar, parentVar);
|
|
1306
|
+
const v = emitNode(ctx, n, tracked, effectsVar, parentVar, '__components', 0, budget || undefined);
|
|
1109
1307
|
ctx.claimStatic = savedClaim;
|
|
1110
1308
|
if (v)
|
|
1111
1309
|
ctx.push(indent(`${cl}.push(${v});`));
|
|
@@ -1123,7 +1321,7 @@ function emitOpaque(ctx, node, tracked, effTarget, parentVar) {
|
|
|
1123
1321
|
const savedClaim = ctx.claimStatic;
|
|
1124
1322
|
if (hyd)
|
|
1125
1323
|
ctx.claimStatic = true;
|
|
1126
|
-
const v = emitNode(ctx, n, tracked, effectsVar, parentVar);
|
|
1324
|
+
const v = emitNode(ctx, n, tracked, effectsVar, parentVar, '__components', 0, budget || undefined);
|
|
1127
1325
|
ctx.claimStatic = savedClaim;
|
|
1128
1326
|
if (v)
|
|
1129
1327
|
ctx.push(indent(`${cl2}.push(${v});`));
|
|
@@ -1188,7 +1386,7 @@ function emitOpaque(ctx, node, tracked, effTarget, parentVar) {
|
|
|
1188
1386
|
ctx.push(`${parent}.appendChild(${anchor});`);
|
|
1189
1387
|
ctx.push(`let ${effectsVar} = [];`);
|
|
1190
1388
|
ctx.push(`const ${endAnchor} = document.createComment('if-end');`);
|
|
1191
|
-
emitHydrateFenceAnchoring(ctx, anchor, endAnchor, parent === '$root');
|
|
1389
|
+
emitHydrateFenceAnchoring(ctx, anchor, endAnchor, parent === '$root', budget || undefined);
|
|
1192
1390
|
const asyncKw = ctx.isAsyncScope ? 'await ' : '';
|
|
1193
1391
|
const fnOpen = ctx.isAsyncScope ? 'async () => {' : '() => {';
|
|
1194
1392
|
const conRenderName = ctx.n();
|
|
@@ -1204,7 +1402,7 @@ function emitOpaque(ctx, node, tracked, effTarget, parentVar) {
|
|
|
1204
1402
|
const savedClaim = ctx.claimStatic;
|
|
1205
1403
|
if (hyd)
|
|
1206
1404
|
ctx.claimStatic = true;
|
|
1207
|
-
const v = emitNode(ctx, n, tracked, effectsVar, hyd ? parentVar : conFrag);
|
|
1405
|
+
const v = emitNode(ctx, n, tracked, effectsVar, hyd ? parentVar : conFrag, '__components', 0, budget || undefined);
|
|
1208
1406
|
ctx.claimStatic = savedClaim;
|
|
1209
1407
|
if (v) {
|
|
1210
1408
|
if (hyd)
|
|
@@ -1233,7 +1431,7 @@ function emitOpaque(ctx, node, tracked, effTarget, parentVar) {
|
|
|
1233
1431
|
const savedAltClaim = ctx.claimStatic;
|
|
1234
1432
|
if (hyd)
|
|
1235
1433
|
ctx.claimStatic = true;
|
|
1236
|
-
const v = emitNode(ctx, n, tracked, effectsVar, hyd ? parentVar : altFrag);
|
|
1434
|
+
const v = emitNode(ctx, n, tracked, effectsVar, hyd ? parentVar : altFrag, '__components', 0, budget || undefined);
|
|
1237
1435
|
ctx.claimStatic = savedAltClaim;
|
|
1238
1436
|
if (v) {
|
|
1239
1437
|
if (hyd)
|
|
@@ -1284,19 +1482,20 @@ function emitOpaque(ctx, node, tracked, effTarget, parentVar) {
|
|
|
1284
1482
|
}
|
|
1285
1483
|
return null;
|
|
1286
1484
|
}
|
|
1287
|
-
function emitWhileLoop(ctx, node, tracked, parentVar) {
|
|
1485
|
+
function emitWhileLoop(ctx, node, tracked, parentVar, skipK = 0, regionBudget) {
|
|
1288
1486
|
const condExpr = transformTracked(node.condition, tracked);
|
|
1289
1487
|
const anchor = ctx.n();
|
|
1290
1488
|
const endAnchor = ctx.n();
|
|
1291
1489
|
const effectsVar = ctx.n();
|
|
1292
1490
|
const hyd = ctx.hydrate;
|
|
1293
1491
|
const parent = parentVar || '$root';
|
|
1492
|
+
const budget = hyd ? regionBudgetVar(ctx, skipK, regionBudget) : null;
|
|
1294
1493
|
ctx.push(`const ${anchor} = document.createComment('while');`);
|
|
1295
1494
|
if (!hyd)
|
|
1296
1495
|
ctx.push(`${parent}.appendChild(${anchor});`);
|
|
1297
1496
|
ctx.push(`let ${effectsVar} = [];`);
|
|
1298
1497
|
ctx.push(`const ${endAnchor} = document.createComment('while-end');`);
|
|
1299
|
-
emitHydrateFenceAnchoring(ctx, anchor, endAnchor, parent === '$root');
|
|
1498
|
+
emitHydrateFenceAnchoring(ctx, anchor, endAnchor, parent === '$root', budget || undefined);
|
|
1300
1499
|
const renderLoop = ctx.n();
|
|
1301
1500
|
ctx.push(`const ${renderLoop} = ${ctx.isAsyncScope ? 'async () => {' : '() => {'}`);
|
|
1302
1501
|
if (!hyd)
|
|
@@ -1308,7 +1507,7 @@ function emitWhileLoop(ctx, node, tracked, parentVar) {
|
|
|
1308
1507
|
if (node.isDoWhile) {
|
|
1309
1508
|
ctx.push(indent(`do {`));
|
|
1310
1509
|
for (const n of node.bodyTemplate) {
|
|
1311
|
-
const v = emitNode(ctx, n, tracked, effectsVar, hyd ? parentVar : '__b');
|
|
1510
|
+
const v = emitNode(ctx, n, tracked, effectsVar, hyd ? parentVar : '__b', '__components', 0, budget || undefined);
|
|
1312
1511
|
if (v) {
|
|
1313
1512
|
if (hyd)
|
|
1314
1513
|
ctx.push(indent(`__cl.push(${v});`));
|
|
@@ -1321,7 +1520,7 @@ function emitWhileLoop(ctx, node, tracked, parentVar) {
|
|
|
1321
1520
|
else {
|
|
1322
1521
|
ctx.push(indent(`while (${condExpr}) {`));
|
|
1323
1522
|
for (const n of node.bodyTemplate) {
|
|
1324
|
-
const v = emitNode(ctx, n, tracked, effectsVar, hyd ? parentVar : '__b');
|
|
1523
|
+
const v = emitNode(ctx, n, tracked, effectsVar, hyd ? parentVar : '__b', '__components', 0, budget || undefined);
|
|
1325
1524
|
if (v) {
|
|
1326
1525
|
if (hyd)
|
|
1327
1526
|
ctx.push(indent(`__cl.push(${v});`));
|
|
@@ -1360,18 +1559,19 @@ function emitWhileLoop(ctx, node, tracked, parentVar) {
|
|
|
1360
1559
|
}`);
|
|
1361
1560
|
return null;
|
|
1362
1561
|
}
|
|
1363
|
-
function emitForLoop(ctx, node, tracked, parentVar) {
|
|
1562
|
+
function emitForLoop(ctx, node, tracked, parentVar, skipK = 0, regionBudget) {
|
|
1364
1563
|
const anchor = ctx.n();
|
|
1365
1564
|
const endAnchor = ctx.n();
|
|
1366
1565
|
const effectsVar = ctx.n();
|
|
1367
1566
|
const hyd = ctx.hydrate;
|
|
1368
1567
|
const parent = parentVar || '$root';
|
|
1568
|
+
const budget = hyd ? regionBudgetVar(ctx, skipK, regionBudget) : null;
|
|
1369
1569
|
ctx.push(`const ${anchor} = document.createComment('for');`);
|
|
1370
1570
|
if (!hyd)
|
|
1371
1571
|
ctx.push(`${parent}.appendChild(${anchor});`);
|
|
1372
1572
|
ctx.push(`let ${effectsVar} = [];`);
|
|
1373
1573
|
ctx.push(`const ${endAnchor} = document.createComment('for-end');`);
|
|
1374
|
-
emitHydrateFenceAnchoring(ctx, anchor, endAnchor, parent === '$root');
|
|
1574
|
+
emitHydrateFenceAnchoring(ctx, anchor, endAnchor, parent === '$root', budget || undefined);
|
|
1375
1575
|
const renderLoop = ctx.n();
|
|
1376
1576
|
ctx.push(`const ${renderLoop} = ${ctx.isAsyncScope ? 'async () => {' : '() => {'}`);
|
|
1377
1577
|
if (!hyd)
|
|
@@ -1384,7 +1584,7 @@ function emitForLoop(ctx, node, tracked, parentVar) {
|
|
|
1384
1584
|
const srcExpr = transformTracked(node.condition, tracked);
|
|
1385
1585
|
ctx.push(indent(`for (${node.init} of (Array.isArray(${srcExpr}) ? ${srcExpr} : (${srcExpr} == null ? [] : Object.keys(${srcExpr})))) {`));
|
|
1386
1586
|
for (const n of node.bodyTemplate) {
|
|
1387
|
-
const v = emitNode(ctx, n, tracked, effectsVar, hyd ? parentVar : '__b');
|
|
1587
|
+
const v = emitNode(ctx, n, tracked, effectsVar, hyd ? parentVar : '__b', '__components', 0, budget || undefined);
|
|
1388
1588
|
if (v) {
|
|
1389
1589
|
if (hyd)
|
|
1390
1590
|
ctx.push(indent(`__cl.push(${v});`));
|
|
@@ -1413,7 +1613,7 @@ function emitForLoop(ctx, node, tracked, parentVar) {
|
|
|
1413
1613
|
const head = initPart ? `${initPart}; ${condExpr}` : `; ${condExpr}`;
|
|
1414
1614
|
ctx.push(indent(`for (${head}; ${updatePart}) {`));
|
|
1415
1615
|
for (const n of node.bodyTemplate) {
|
|
1416
|
-
const v = emitNode(ctx, n, tracked, effectsVar, hyd ? parentVar : '__b');
|
|
1616
|
+
const v = emitNode(ctx, n, tracked, effectsVar, hyd ? parentVar : '__b', '__components', 0, budget || undefined);
|
|
1417
1617
|
if (v) {
|
|
1418
1618
|
if (hyd)
|
|
1419
1619
|
ctx.push(indent(`__cl.push(${v});`));
|
|
@@ -1533,19 +1733,20 @@ function emitForLoop(ctx, node, tracked, parentVar) {
|
|
|
1533
1733
|
}
|
|
1534
1734
|
return null;
|
|
1535
1735
|
}
|
|
1536
|
-
function emitSwitchBlock(ctx, node, tracked, parentVar) {
|
|
1736
|
+
function emitSwitchBlock(ctx, node, tracked, parentVar, skipK = 0, regionBudget) {
|
|
1537
1737
|
const discExpr = transformTracked(node.discriminant, tracked);
|
|
1538
1738
|
const anchor = ctx.n();
|
|
1539
1739
|
const endAnchor = ctx.n();
|
|
1540
1740
|
const effectsVar = ctx.n();
|
|
1541
1741
|
const hyd = ctx.hydrate;
|
|
1542
1742
|
const parent = parentVar || '$root';
|
|
1743
|
+
const budget = hyd ? regionBudgetVar(ctx, skipK, regionBudget) : null;
|
|
1543
1744
|
ctx.push(`const ${anchor} = document.createComment('switch');`);
|
|
1544
1745
|
if (!hyd)
|
|
1545
1746
|
ctx.push(`${parent}.appendChild(${anchor});`);
|
|
1546
1747
|
ctx.push(`let ${effectsVar} = [];`);
|
|
1547
1748
|
ctx.push(`const ${endAnchor} = document.createComment('switch-end');`);
|
|
1548
|
-
emitHydrateFenceAnchoring(ctx, anchor, endAnchor, parent === '$root');
|
|
1749
|
+
emitHydrateFenceAnchoring(ctx, anchor, endAnchor, parent === '$root', budget || undefined);
|
|
1549
1750
|
const renderSwitch = ctx.n();
|
|
1550
1751
|
ctx.push(`const ${renderSwitch} = ${ctx.isAsyncScope ? 'async () => {' : '() => {'}`);
|
|
1551
1752
|
if (hyd) {
|
|
@@ -1567,7 +1768,7 @@ function emitSwitchBlock(ctx, node, tracked, parentVar) {
|
|
|
1567
1768
|
ctx.push(indent(`default:`));
|
|
1568
1769
|
}
|
|
1569
1770
|
for (const n of c.body) {
|
|
1570
|
-
const v = emitNode(ctx, n, tracked, effectsVar, hyd ? parentVar : '__c');
|
|
1771
|
+
const v = emitNode(ctx, n, tracked, effectsVar, hyd ? parentVar : '__c', '__components', 0, budget || undefined);
|
|
1571
1772
|
if (v) {
|
|
1572
1773
|
if (hyd)
|
|
1573
1774
|
ctx.push(indent(`__cl.push(${v});`));
|
|
@@ -1607,7 +1808,7 @@ function emitSwitchBlock(ctx, node, tracked, parentVar) {
|
|
|
1607
1808
|
}`);
|
|
1608
1809
|
return null;
|
|
1609
1810
|
}
|
|
1610
|
-
function emitMap(ctx, node, tracked, effTarget, parentVar) {
|
|
1811
|
+
function emitMap(ctx, node, tracked, effTarget, parentVar, skipK = 0, regionBudget) {
|
|
1611
1812
|
const arrExpr = transformTracked(node.expression, tracked);
|
|
1612
1813
|
const itemVar = node.itemVariable;
|
|
1613
1814
|
const anchor = ctx.n();
|
|
@@ -1621,6 +1822,9 @@ function emitMap(ctx, node, tracked, effTarget, parentVar) {
|
|
|
1621
1822
|
// adopted and wired instead of being rebuilt (which left SSR phantoms behind).
|
|
1622
1823
|
const hydKeyed = ctx.hydrate && keyed;
|
|
1623
1824
|
const parent = parentVar || '$root';
|
|
1825
|
+
// Keyed maps resolve each item through claim-by-key, never positionally, so
|
|
1826
|
+
// the first-claim budget only applies to the non-keyed hydrate path.
|
|
1827
|
+
const budget = hyd ? regionBudgetVar(ctx, skipK, regionBudget) : null;
|
|
1624
1828
|
// A re-render effect is only valid when the map iterates a tracked/reactive
|
|
1625
1829
|
// source. Iterating a static collection (module const, top-level array) or a
|
|
1626
1830
|
// loop/statement-local derived value (e.g. `col.links`, `pages`) renders once;
|
|
@@ -1673,7 +1877,7 @@ function emitMap(ctx, node, tracked, effTarget, parentVar) {
|
|
|
1673
1877
|
if (hydKeyed)
|
|
1674
1878
|
ctx.rootClaim = true;
|
|
1675
1879
|
for (const n of node.bodyTemplate) {
|
|
1676
|
-
const v = emitNode(ctx, n, tracked, '__e', hyd ? undefined : '__it');
|
|
1880
|
+
const v = emitNode(ctx, n, tracked, '__e', hyd ? undefined : '__it', '__components', 0, budget || undefined);
|
|
1677
1881
|
if (v) {
|
|
1678
1882
|
if (hyd) {
|
|
1679
1883
|
ctx.push(indent(`if (__cl) __cl.push(${v}); else __p.insertBefore(${v}, __r);`));
|
|
@@ -1714,7 +1918,7 @@ function emitMap(ctx, node, tracked, effTarget, parentVar) {
|
|
|
1714
1918
|
ctx.hydrate = false;
|
|
1715
1919
|
for (const n of node.alternateNodes) {
|
|
1716
1920
|
ctx.claimStatic = true;
|
|
1717
|
-
const v = emitNode(ctx, n, tracked, effectsVar, hyd ? undefined : frag);
|
|
1921
|
+
const v = emitNode(ctx, n, tracked, effectsVar, hyd ? undefined : frag, '__components', 0, budget || undefined);
|
|
1718
1922
|
if (v) {
|
|
1719
1923
|
if (hyd)
|
|
1720
1924
|
ctx.push(indent(`if (__cl) __cl.push(${v}); else __p.insertBefore(${v}, ${endAnchor});`));
|
|
@@ -1749,7 +1953,7 @@ function emitMap(ctx, node, tracked, effTarget, parentVar) {
|
|
|
1749
1953
|
// inside `parent` (pre-attach, `anchor.parentNode` is null) and peeks all
|
|
1750
1954
|
// keys before claim-by-key consumes the walker, so server-rendered items
|
|
1751
1955
|
// are adopted and wired in place instead of being rebuilt.
|
|
1752
|
-
ctx.push(`const ${initList} = () => { ${reconciler} = reconcileHydrated(${anchor}, ${endAnchor}, ${arrExpr}, ${keyFnSrc}, (${itemVar}, __i, __e, __root) => ${renderItem}(${itemVar}${indexParam ? ', __i, __e' : ', __e'}, null, __root), __hydrate, ${parent}); };`);
|
|
1956
|
+
ctx.push(`const ${initList} = () => { ${reconciler} = reconcileHydrated(${anchor}, ${endAnchor}, ${arrExpr}, ${keyFnSrc}, (${itemVar}, __i, __e, __root) => ${renderItem}(${itemVar}${indexParam ? ', __i, __e' : ', __e'}, null, __root), __hydrate, ${parent}, ${skipK}); };`);
|
|
1753
1957
|
}
|
|
1754
1958
|
else {
|
|
1755
1959
|
ctx.push(`const ${initList} = () => { ${reconciler} = reconcile(${anchor}, ${endAnchor}, ${arrExpr}, ${keyFnSrc}, (${itemVar}, __i, __e) => ${renderItem}(${itemVar}${indexParam ? ', __i, __e' : ', __e'})); };`);
|
|
@@ -1860,13 +2064,14 @@ ${renderAllItems(' ', null)}
|
|
|
1860
2064
|
function computeAsyncComponents(comps) {
|
|
1861
2065
|
return new Set(comps.filter((c) => c.isAsync).map((c) => c.name));
|
|
1862
2066
|
}
|
|
1863
|
-
function generateComponent(comp, importedNames = new Set(), hydrate = false, asyncComps = new Set(), linkNames = new Set(), selfClaimNames = new Set(), alloc) {
|
|
2067
|
+
function generateComponent(comp, importedNames = new Set(), hydrate = false, asyncComps = new Set(), linkNames = new Set(), selfClaimNames = new Set(), alloc, markerless = true) {
|
|
1864
2068
|
const tracked = collectTrackedNames(comp.body);
|
|
1865
2069
|
const ctx = new Ctx(alloc);
|
|
1866
2070
|
ctx.importedNames = importedNames;
|
|
1867
2071
|
ctx.linkNames = linkNames;
|
|
1868
2072
|
ctx.selfClaimNames = selfClaimNames;
|
|
1869
2073
|
ctx.hydrate = hydrate;
|
|
2074
|
+
ctx.markerless = markerless;
|
|
1870
2075
|
ctx.asyncComps = asyncComps;
|
|
1871
2076
|
ctx.isAsyncScope = comp.isAsync || asyncComps.has(comp.name);
|
|
1872
2077
|
ctx.push(hydrate ? (ctx.isAsyncScope ? 'async (props, __registry, __hydrate) => {' : '(props, __registry, __hydrate) => {') : (ctx.isAsyncScope ? 'async (props) => {' : '(props) => {'));
|
|
@@ -1905,10 +2110,22 @@ function generateComponent(comp, importedNames = new Set(), hydrate = false, asy
|
|
|
1905
2110
|
emitNode(ctx, node, tracked, null);
|
|
1906
2111
|
}
|
|
1907
2112
|
}
|
|
2113
|
+
let topPendingSkip = 0;
|
|
2114
|
+
let topBudget = null;
|
|
2115
|
+
if (ctx.markerless && ctx.hydrate) {
|
|
2116
|
+
// Inherited walker deposit (Phase 3.c): a caller that placed this component
|
|
2117
|
+
// right after pure-static residue deposits it on the shared walker
|
|
2118
|
+
// (`injectSkipK`, consumed once by `takeSkipK`). The FIRST top-level claim
|
|
2119
|
+
// translates it into a positional skip exactly like a region budget, so the
|
|
2120
|
+
// component's root lands on its own SSR slot wherever the call appears.
|
|
2121
|
+
// Pure-static top-level nodes return null without claiming and leave the
|
|
2122
|
+
// budget intact for the next claim.
|
|
2123
|
+
topBudget = regionBudgetVar(ctx, 0, undefined, `(typeof __hydrate.takeSkipK === 'function' ? __hydrate.takeSkipK() : 0)`);
|
|
2124
|
+
}
|
|
1908
2125
|
for (const node of comp.body) {
|
|
1909
2126
|
if (node instanceof TrackDecl)
|
|
1910
2127
|
continue;
|
|
1911
|
-
const v = emitNode(ctx, node, tracked, null);
|
|
2128
|
+
const v = emitNode(ctx, node, tracked, null, undefined, '__components', topPendingSkip, topBudget || undefined);
|
|
1912
2129
|
if (v) {
|
|
1913
2130
|
if (ctx.hydrate) {
|
|
1914
2131
|
// The first top-level node becomes `$mount`. Late top-level nodes are
|
|
@@ -1921,6 +2138,16 @@ function generateComponent(comp, importedNames = new Set(), hydrate = false, asy
|
|
|
1921
2138
|
ctx.push(indent(`$root.appendChild(${v});`));
|
|
1922
2139
|
}
|
|
1923
2140
|
}
|
|
2141
|
+
// Markerless: only unclaimed residue must be skipped by the next claim.
|
|
2142
|
+
// Claiming children advance the walker cursor themselves; pure-static
|
|
2143
|
+
// top-level nodes return null (left in the SSR DOM unclaimed) and are
|
|
2144
|
+
// never walked over, so the next claim must skip past them.
|
|
2145
|
+
if (claimsWalkerNode(ctx, node)) {
|
|
2146
|
+
topBudget = null;
|
|
2147
|
+
topPendingSkip = 0;
|
|
2148
|
+
}
|
|
2149
|
+
else
|
|
2150
|
+
topPendingSkip += ssrResidueEstimate(node);
|
|
1924
2151
|
}
|
|
1925
2152
|
const effCode = ctx.flushEffects();
|
|
1926
2153
|
if (effCode)
|
|
@@ -1950,7 +2177,7 @@ function buildParamInit(paramNames) {
|
|
|
1950
2177
|
return '';
|
|
1951
2178
|
return `const { ${paramNames.join(', ')} } = props;`;
|
|
1952
2179
|
}
|
|
1953
|
-
function buildComponentMap(irRoot, hydrate = false, alloc) {
|
|
2180
|
+
function buildComponentMap(irRoot, hydrate = false, alloc, markerless = true) {
|
|
1954
2181
|
const mapLines = [];
|
|
1955
2182
|
mapLines.push(`const __components = {};`);
|
|
1956
2183
|
const asyncComps = computeAsyncComponents(irRoot.components);
|
|
@@ -1986,12 +2213,18 @@ function buildComponentMap(irRoot, hydrate = false, alloc) {
|
|
|
1986
2213
|
for (const comp of irRoot.components) {
|
|
1987
2214
|
if (hydrate && isStaticComponent(comp)) {
|
|
1988
2215
|
// Static components keep their SSR content untouched: claim the root
|
|
1989
|
-
// WITHOUT stripping its direct text (nothing will re-create it).
|
|
1990
|
-
|
|
2216
|
+
// WITHOUT stripping its direct text (nothing will re-create it). In
|
|
2217
|
+
// markerless mode the claim also consumes the caller's inherited
|
|
2218
|
+
// residue deposit (`takeSkipK`), matching a generated component's
|
|
2219
|
+
// first-claim budget.
|
|
2220
|
+
const claimExpr = markerless
|
|
2221
|
+
? `__hydrate.claimOnly(undefined, (typeof __hydrate.takeSkipK === 'function' ? __hydrate.takeSkipK() : 0))`
|
|
2222
|
+
: `__hydrate.claimOnly()`;
|
|
2223
|
+
const stub = `(props, __registry, __hydrate) => { return ${claimExpr}; }`;
|
|
1991
2224
|
mapLines.push(`__components[${JSON.stringify(comp.name)}] = ${stub};`);
|
|
1992
2225
|
continue;
|
|
1993
2226
|
}
|
|
1994
|
-
const code = generateComponent(comp, directNames, hydrate, asyncComps, linkNames, selfClaimNames, alloc);
|
|
2227
|
+
const code = generateComponent(comp, directNames, hydrate, asyncComps, linkNames, selfClaimNames, alloc, markerless);
|
|
1995
2228
|
mapLines.push(`__components[${JSON.stringify(comp.name)}] = ${code};`);
|
|
1996
2229
|
}
|
|
1997
2230
|
mapLines.push(`function __cleanup(start, end) {`);
|
|
@@ -2012,6 +2245,12 @@ function buildComponentMap(irRoot, hydrate = false, alloc) {
|
|
|
2012
2245
|
mapLines.push(`\t\tconst p = nodes[0].parentNode;`);
|
|
2013
2246
|
mapLines.push(`\t\tp.insertBefore(start, nodes[0]);`);
|
|
2014
2247
|
mapLines.push(`\t\tp.insertBefore(end, nodes[nodes.length - 1].nextSibling);`);
|
|
2248
|
+
mapLines.push(`\t\t// Client-surplus nodes (SSR rendered fewer items than the client data)`);
|
|
2249
|
+
mapLines.push(`\t\t// are freshly claimed and still detached — move them into the region in`);
|
|
2250
|
+
mapLines.push(`\t\t// client order before \`end\`, instead of stranding them off-parent.`);
|
|
2251
|
+
mapLines.push(`\t\tfor (let i = 0; i < nodes.length; i++) {`);
|
|
2252
|
+
mapLines.push(`\t\t\tif (nodes[i].parentNode === null) p.insertBefore(nodes[i], end);`);
|
|
2253
|
+
mapLines.push(`\t\t}`);
|
|
2015
2254
|
mapLines.push(`\t\treturn;`);
|
|
2016
2255
|
mapLines.push(`\t}`);
|
|
2017
2256
|
mapLines.push(`\tfallback.appendChild(start);`);
|
|
@@ -2073,6 +2312,32 @@ function isStaticIR(body) {
|
|
|
2073
2312
|
}
|
|
2074
2313
|
return true;
|
|
2075
2314
|
}
|
|
2315
|
+
// Whether emitting `node` advances the structural walker's cursor (claims a
|
|
2316
|
+
// walker slot via nextElement). Markerless skipK accounting must only stand in
|
|
2317
|
+
// for siblings the walker will NOT consume; a claiming sibling advances the
|
|
2318
|
+
// cursor itself, so its residue must not be counted again. A pure-static
|
|
2319
|
+
// StaticNode only claims when `claimStatic` forces it (the emitStatic early
|
|
2320
|
+
// return for static-only subtrees under `!claim`); otherwise its SSR element is
|
|
2321
|
+
// left in place untouched and the next claim must skip it.
|
|
2322
|
+
function claimsWalkerNode(ctx, node) {
|
|
2323
|
+
if (node instanceof StaticNode)
|
|
2324
|
+
return ctx.claimStatic || !isStaticIR(node.children);
|
|
2325
|
+
if (node instanceof ComponentCall)
|
|
2326
|
+
return true;
|
|
2327
|
+
if (node instanceof OpaqueDynamicRegion)
|
|
2328
|
+
return true;
|
|
2329
|
+
if (node instanceof MapRegion)
|
|
2330
|
+
return true;
|
|
2331
|
+
if (node instanceof ForLoop)
|
|
2332
|
+
return true;
|
|
2333
|
+
if (node instanceof WhileLoop)
|
|
2334
|
+
return true;
|
|
2335
|
+
if (node instanceof SwitchBlock)
|
|
2336
|
+
return true;
|
|
2337
|
+
if (node instanceof TryCatch)
|
|
2338
|
+
return true;
|
|
2339
|
+
return false;
|
|
2340
|
+
}
|
|
2076
2341
|
function isStaticComponent(comp) {
|
|
2077
2342
|
if (comp.style)
|
|
2078
2343
|
return false;
|
|
@@ -2263,7 +2528,7 @@ function emitClientFromIR(ir, options) {
|
|
|
2263
2528
|
if (!options.forceClient && !needsClient) {
|
|
2264
2529
|
return '';
|
|
2265
2530
|
}
|
|
2266
|
-
const componentMapCode = buildComponentMap(ir, options.hydrate, options.nameAllocator);
|
|
2531
|
+
const componentMapCode = buildComponentMap(ir, options.hydrate, options.nameAllocator, options.markerless !== false);
|
|
2267
2532
|
const importLines = ir.imports.length > 0 ? ir.imports.join('\n') + '\n' : '';
|
|
2268
2533
|
const topCode = (options.includeTopLevel === false ? [] : transformTopLevelForActions(ir.topLevelCode, 'client')).join('\n') + '\n';
|
|
2269
2534
|
const exportLines = [];
|
|
@@ -2366,7 +2631,7 @@ export function compileClientBoth(source, _componentName, sourcePath, opts) {
|
|
|
2366
2631
|
return { comp, hyd: '', name };
|
|
2367
2632
|
return {
|
|
2368
2633
|
comp,
|
|
2369
|
-
hyd: emitClientFromIR(irHyd, { forceClient: true, hydrate: true, includeTopLevel: false, nameAllocator: alloc }),
|
|
2634
|
+
hyd: emitClientFromIR(irHyd, { forceClient: true, hydrate: true, includeTopLevel: false, nameAllocator: alloc, markerless: opts?.markerless !== false }),
|
|
2370
2635
|
name,
|
|
2371
2636
|
};
|
|
2372
2637
|
}
|