@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.
@@ -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 || ${ctx.walker}.nextElement(${JSON.stringify(node.tag)})`
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
- ctx.push(`if (${v} && ${v}.parentNode == null && ${v}.nodeType !== 11) { const __sr = ${walkerArg}.claimOnly(); if (__sr && __sr.parentNode) __sr.parentNode.replaceChild(${v}, __sr); }`);
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 (__hydrate && __hydrate.insertBeforeNextClaim) {`);
986
- ctx.push(indent(`__hydrate.insertBeforeNextClaim(${anchor});`));
987
- ctx.push(indent(`__hydrate.insertBeforeNextClaim(${endAnchor});`));
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
- function emitTryCatch(ctx, node, tracked, effectsVar, parentVar) {
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
- const stub = `(props, __registry, __hydrate) => { return __hydrate.claimOnly(); }`;
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
  }