@vesk/compiler 0.2.26 → 0.2.28

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.
@@ -16,6 +16,20 @@ export declare function collectTrackedNames(body: IRNode[]): Map<string, Tracked
16
16
  * normal vesk parser so TS generics and `.vsk` syntax survive.
17
17
  */
18
18
  export declare function transformTrackedInit(init: string, tracked: Map<string, TrackedInfo>): string;
19
+ /** Shared counter for generated `$n` identifiers. */
20
+ export type NameAlloc = {
21
+ c: number;
22
+ };
23
+ /**
24
+ * Allocates a disjoint `$n` name block per source file. esbuild's bundler
25
+ * renames colliding top-level symbols when it links a chunk, and on 0.25.x
26
+ * that rename can leave references inside nested closures pointing at the
27
+ * pre-rename name — surfacing as `$nNN is not defined` at hydration. Seeding
28
+ * every file from a hash of its path keeps the names globally unique so the
29
+ * renamer has nothing to rename. Callers without a `sourcePath` keep the
30
+ * historical `$n0…` numbering.
31
+ */
32
+ export declare function nameAllocFor(sourcePath?: string, start?: number): NameAlloc;
19
33
  declare function isStaticIR(body: IRNode[]): boolean;
20
34
  export declare function escapeHtml(str: string): string;
21
35
  export declare function compileClient(source: string, _componentName: string | null, options?: {
@@ -24,6 +38,7 @@ export declare function compileClient(source: string, _componentName: string | n
24
38
  includeTopLevel?: boolean;
25
39
  sourcePath?: string;
26
40
  mdRoots?: string[];
41
+ nameAllocator?: NameAlloc;
27
42
  }): string;
28
43
  /**
29
44
  * Compiles a component source in BOTH client modes (plain + hydrate)
@@ -1 +1 @@
1
- {"version":3,"file":"client-codegen.d.ts","sourceRoot":"","sources":["../src/client-codegen.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,MAAM,EAAuB,MAAM,uBAAuB,CAAC;AACzE,OAAO,EAGL,cAAc,EAQd,gBAAgB,EAOjB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAyCxD,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,SAAS,GAAG,MAAM,CAAC;CAC1B;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAI1D;AAiDD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,UAAU,GAAG,gBAAgB,GAAG,cAAc,EAAE,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,GAAG,MAAM,CAwGlI;AAID,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAU5E;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,GAAG,MAAM,CAY5F;AAk8CD,iBAAS,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAe3C;AA2HD,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAO9C;AAqED,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,IAAI,EAAE,OAAO,GAAE;IAAE,WAAW,CAAC,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,eAAe,CAAC,EAAE,OAAO,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;CAAO,GAAG,MAAM,CAOnN;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,IAAI,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAqBxJ;AAED,OAAO,EAAE,aAAa,IAAI,OAAO,EAAE,UAAU,EAAE,CAAC"}
1
+ {"version":3,"file":"client-codegen.d.ts","sourceRoot":"","sources":["../src/client-codegen.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,MAAM,EAAuB,MAAM,uBAAuB,CAAC;AACzE,OAAO,EAGL,cAAc,EAQd,gBAAgB,EASjB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAyCxD,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,SAAS,GAAG,MAAM,CAAC;CAC1B;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAI1D;AAiDD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,UAAU,GAAG,gBAAgB,GAAG,cAAc,EAAE,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,GAAG,MAAM,CAwGlI;AAID,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAU5E;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,GAAG,MAAM,CAY5F;AAoBD,qDAAqD;AACrD,MAAM,MAAM,SAAS,GAAG;IAAE,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtC;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,UAAU,CAAC,EAAE,MAAM,EAAE,KAAK,SAAI,GAAG,SAAS,CAQtE;AAmjDD,iBAAS,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAe3C;AAgID,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAO9C;AAqED,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,IAAI,EAAE,OAAO,GAAE;IAAE,WAAW,CAAC,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,eAAe,CAAC,EAAE,OAAO,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,aAAa,CAAC,EAAE,SAAS,CAAA;CAAO,GAAG,MAAM,CAO9O;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,IAAI,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAwBxJ;AAED,OAAO,EAAE,aAAa,IAAI,OAAO,EAAE,UAAU,EAAE,CAAC"}
@@ -2,7 +2,7 @@ import { walk } from 'zimmerframe';
2
2
  import { print } from 'esrap';
3
3
  import ts from 'esrap/languages/ts';
4
4
  import tsx from 'esrap/languages/tsx';
5
- import { StaticNode, TextNode, DynamicBinding, OpaqueDynamicRegion, MapRegion, WhileLoop, SwitchBlock, TryCatch, ForLoop, TrackDecl, RuntimeStatement, ComponentRef, ComponentCall, ServerBlock, ClientBlock, HeadBlock, SlotNode, } from '@vesk/compiler/src/ir';
5
+ import { StaticNode, TextNode, DynamicBinding, OpaqueDynamicRegion, MapRegion, WhileLoop, SwitchBlock, TryCatch, ForLoop, TrackDecl, RuntimeStatement, ComponentRef, ComponentCall, ServerBlock, ClientBlock, HeadBlock, SlotNode, PropSlot, PropSlotRender, } from '@vesk/compiler/src/ir';
6
6
  import { parse } from '@vesk/compiler/src/parser';
7
7
  import { generateIR } from '@vesk/compiler/src/ir-generator';
8
8
  import { transformTopLevelForActions } from '@vesk/compiler/src/actions';
@@ -248,10 +248,29 @@ function indent(code, level = 1) {
248
248
  const pad = '\t'.repeat(level);
249
249
  return code.split('\n').map((l) => (l ? pad + l : '')).join('\n');
250
250
  }
251
+ /**
252
+ * Allocates a disjoint `$n` name block per source file. esbuild's bundler
253
+ * renames colliding top-level symbols when it links a chunk, and on 0.25.x
254
+ * that rename can leave references inside nested closures pointing at the
255
+ * pre-rename name — surfacing as `$nNN is not defined` at hydration. Seeding
256
+ * every file from a hash of its path keeps the names globally unique so the
257
+ * renamer has nothing to rename. Callers without a `sourcePath` keep the
258
+ * historical `$n0…` numbering.
259
+ */
260
+ export function nameAllocFor(sourcePath, start = 0) {
261
+ if (!sourcePath)
262
+ return { c: start };
263
+ let h = 0x811c9dc5;
264
+ for (let i = 0; i < sourcePath.length; i++) {
265
+ h ^= sourcePath.charCodeAt(i);
266
+ h = Math.imul(h, 0x01000193) >>> 0;
267
+ }
268
+ return { c: h * 1_000_000 + start };
269
+ }
251
270
  class Ctx {
252
271
  lines = [];
253
272
  effects = [];
254
- c = 0;
273
+ alloc;
255
274
  importedNames = new Set();
256
275
  linkNames = new Set();
257
276
  // Runtime component imports that claim their own SSR root via `nextElement`
@@ -270,12 +289,15 @@ class Ctx {
270
289
  walker = '__hydrate';
271
290
  asyncComps = new Set();
272
291
  isAsyncScope = false;
292
+ constructor(alloc) {
293
+ this.alloc = alloc ?? { c: 0 };
294
+ }
273
295
  push(...args) {
274
296
  for (const a of args)
275
297
  if (a)
276
298
  this.lines.push(a);
277
299
  }
278
- n() { return `$n${this.c++}`; }
300
+ n() { return `$n${this.alloc.c++}`; }
279
301
  getCode() { return this.lines.join('\n'); }
280
302
  flushEffects() {
281
303
  if (this.effects.length === 0)
@@ -292,8 +314,17 @@ class Ctx {
292
314
  lines.push(`if (!document.${guard}) {`);
293
315
  lines.push(`\tdocument.${guard} = true;`);
294
316
  lines.push(`\tdocument.addEventListener(${JSON.stringify(type)}, (e) => {`);
295
- lines.push(`\t\tvar el = e.target.closest('[data-vsk-ev]');`);
296
- lines.push(`\t\tif (el && el.${prop}) el.${prop}(e);`);
317
+ // Walk up from the target to the nearest element that actually has a
318
+ // handler for this event. Using `closest('[data-vsk-ev]')` alone stops at
319
+ // the nearest event-capable element even when its handler is undefined
320
+ // (`<Button/>` forwards an optional onClick that was never passed), which
321
+ // shadows a real handler on an ancestor (e.g. a Popover trigger wrapper).
322
+ lines.push(`\t\tvar el = e.target;`);
323
+ lines.push(`\t\tif (el && el.nodeType !== 1) el = el.parentElement;`);
324
+ lines.push(`\t\twhile (el && el.nodeType === 1) {`);
325
+ lines.push(`\t\t\tif (el.${prop}) { el.${prop}(e); break; }`);
326
+ lines.push(`\t\t\tel = el.parentElement;`);
327
+ lines.push(`\t\t}`);
297
328
  lines.push(`\t});`);
298
329
  lines.push(`}`);
299
330
  }
@@ -325,7 +356,7 @@ function emitNode(ctx, node, tracked, effectsVar, parentVar, compPrefix = '__com
325
356
  if (node instanceof OpaqueDynamicRegion)
326
357
  return emitOpaque(ctx, node, tracked, effectsVar, parentVar);
327
358
  if (node instanceof MapRegion)
328
- return emitMap(ctx, node, tracked, parentVar);
359
+ return emitMap(ctx, node, tracked, effectsVar, parentVar);
329
360
  if (node instanceof ServerBlock)
330
361
  return null;
331
362
  if (node instanceof ClientBlock) {
@@ -353,7 +384,7 @@ function emitNode(ctx, node, tracked, effectsVar, parentVar, compPrefix = '__com
353
384
  ctx.push(`if (props.children !== undefined && props.children !== null) {`);
354
385
  ctx.push(` if (typeof props.children === 'function') {`);
355
386
  ctx.push(` const __child = props.children(${ctx.walker});`);
356
- ctx.push(` if (__child && typeof __child.then === 'function') __pendingChild = __child.then(() => $root);`);
387
+ ctx.push(` if (__child && typeof __child.then === 'function') __pendingChild = __child.then(() => $mount || $root);`);
357
388
  ctx.push(` } else {`);
358
389
  ctx.push(` ${parentVar}.appendChild(props.children);`);
359
390
  ctx.push(` }`);
@@ -372,6 +403,32 @@ function emitNode(ctx, node, tracked, effectsVar, parentVar, compPrefix = '__com
372
403
  return emitForLoop(ctx, node, tracked, parentVar);
373
404
  if (node instanceof SwitchBlock)
374
405
  return emitSwitchBlock(ctx, node, tracked, parentVar);
406
+ if (node instanceof PropSlot)
407
+ return null;
408
+ if (node instanceof PropSlotRender) {
409
+ const slotProp = `props.${node.propName}`;
410
+ if (!parentVar)
411
+ return null;
412
+ if (ctx.hydrate) {
413
+ ctx.push(`if (${slotProp} !== undefined && ${slotProp} !== null) {`);
414
+ ctx.push(` if (typeof ${slotProp} === 'function') {`);
415
+ ctx.push(` const __child = ${slotProp}(${ctx.walker});`);
416
+ ctx.push(` if (__child && typeof __child.then === 'function') __pendingChild = __child.then(() => ${parentVar});`);
417
+ ctx.push(` } else if (${slotProp}.nodeType !== undefined) {`);
418
+ ctx.push(` ${parentVar}.appendChild(${slotProp});`);
419
+ ctx.push(` } else {`);
420
+ ctx.push(` ${parentVar}.appendChild(document.createTextNode(String(${slotProp})));`);
421
+ ctx.push(` }`);
422
+ ctx.push(`}`);
423
+ }
424
+ else {
425
+ ctx.push(`if (${slotProp} !== undefined && ${slotProp} !== null) {`);
426
+ ctx.push(` if (${slotProp}.nodeType !== undefined) ${parentVar}.appendChild(${slotProp});`);
427
+ ctx.push(` else ${parentVar}.appendChild(document.createTextNode(String(${slotProp})));`);
428
+ ctx.push(`}`);
429
+ }
430
+ return null;
431
+ }
375
432
  return null;
376
433
  }
377
434
  const PROPERTY_ATTRS = {
@@ -408,9 +465,15 @@ function emitStatic(ctx, node, tracked, effectsVar) {
408
465
  children.push(child);
409
466
  }
410
467
  }
468
+ const dynAttrNames = new Set();
469
+ for (const d of dynAttrs)
470
+ if (d.target)
471
+ dynAttrNames.add(d.target);
411
472
  for (const attr of node.attributes) {
412
473
  if (attr.name.startsWith('on') && attr.name.length > 2)
413
474
  continue;
475
+ if (dynAttrNames.has(attr.name))
476
+ continue;
414
477
  if (attr.value === '') {
415
478
  ctx.push(`${el}.setAttribute(${JSON.stringify(attr.name)}, '');`);
416
479
  }
@@ -418,9 +481,24 @@ function emitStatic(ctx, node, tracked, effectsVar) {
418
481
  ctx.push(`${el}.setAttribute(${JSON.stringify(attr.name)}, ${JSON.stringify(escapeHtml(attr.value))});`);
419
482
  }
420
483
  }
484
+ // Hydrate-mode A5 snapshot: exactly one text-ish child that is a reactive
485
+ // dynamic binding gets initialized from the claimed parent's SSR text.
486
+ // Computed only in hydrate mode — normal mode pays zero for this.
487
+ let singleReactiveText = false;
488
+ if (ctx.hydrate) {
489
+ const textKids = children.filter((c) => c instanceof TextNode || (c instanceof DynamicBinding && c.kind === 'text'));
490
+ singleReactiveText = textKids.length === 1 && textKids[0] instanceof DynamicBinding
491
+ && isReactiveExpression(textKids[0].expression, tracked);
492
+ }
421
493
  let residueBefore = 0;
422
494
  for (const child of children) {
423
- const childVar = emitNode(ctx, child, tracked, effectsVar, el);
495
+ let childVar;
496
+ if (singleReactiveText && child instanceof DynamicBinding && child.kind === 'text') {
497
+ childVar = emitDynamicBinding(ctx, child, tracked, effectsVar, `${el}.__vsk_ssrText`);
498
+ }
499
+ else {
500
+ childVar = emitNode(ctx, child, tracked, effectsVar, el);
501
+ }
424
502
  if (childVar) {
425
503
  if (ctx.hydrate) {
426
504
  // Fresh text nodes (static or dynamic) cannot be appended at the end of
@@ -467,12 +545,22 @@ function emitStatic(ctx, node, tracked, effectsVar) {
467
545
  }
468
546
  ctx.push(`${el}.setAttribute('data-vsk-ev', '');`);
469
547
  }
548
+ else if (target === 'style') {
549
+ const expr = transformTracked(attr.expression, tracked);
550
+ const eff = `effect(() => { applyStyle(${el}, ${expr}); })`;
551
+ if (effectsVar) {
552
+ ctx.push(`${effectsVar}.push(${eff});`);
553
+ }
554
+ else {
555
+ ctx.effects.push(`${eff};`);
556
+ }
557
+ }
470
558
  else {
471
559
  const expr = transformTracked(attr.expression, tracked);
472
560
  const useProp = PROPERTY_ATTRS[node.tag]?.has(target);
473
561
  const eff = useProp
474
562
  ? `effect(() => { ${el}.${target} = ${expr}; })`
475
- : `effect(() => { ${el}.setAttribute(${JSON.stringify(target)}, String(${expr})); })`;
563
+ : `effect(() => { const __v = ${expr}; if (__v != null && __v !== false) ${el}.setAttribute(${JSON.stringify(target)}, __v === true ? 'true' : String(__v)); })`;
476
564
  if (effectsVar) {
477
565
  ctx.push(`${effectsVar}.push(${eff});`);
478
566
  }
@@ -627,7 +715,7 @@ function isReactiveExpression(node, tracked) {
627
715
  });
628
716
  return reactive;
629
717
  }
630
- function emitDynamicBinding(ctx, node, tracked, effectsVar) {
718
+ function emitDynamicBinding(ctx, node, tracked, effectsVar, ssrTextExpr = null) {
631
719
  if (node.kind === 'attribute')
632
720
  return null;
633
721
  const expr = transformTracked(node.expression, tracked);
@@ -636,7 +724,13 @@ function emitDynamicBinding(ctx, node, tracked, effectsVar) {
636
724
  ctx.push(`const ${v} = document.createTextNode(String(${expr}));`);
637
725
  return v;
638
726
  }
639
- ctx.push(`const ${v} = document.createTextNode('');`);
727
+ // Hydrate-mode sole-dynamic-text snapshot (Hydrate-Todo A5): initialize from
728
+ // the claimed parent's stashed SSR text so a missed effect degrades to stale
729
+ // text instead of an empty element. The effect below still overwrites on its
730
+ // first run, so live values are unaffected. Fresh (unclaimed) parents stash
731
+ // nothing and the `?? ''` keeps the old empty initial.
732
+ const init = ssrTextExpr ? `String(${ssrTextExpr} ?? '')` : `''`;
733
+ ctx.push(`const ${v} = document.createTextNode(${init});`);
640
734
  const eff = `effect(() => { ${v}.data = String(${expr}); })`;
641
735
  if (effectsVar) {
642
736
  ctx.push(`${effectsVar}.push(${eff});`);
@@ -646,6 +740,29 @@ function emitDynamicBinding(ctx, node, tracked, effectsVar) {
646
740
  }
647
741
  return v;
648
742
  }
743
+ /**
744
+ * Emits a fresh DocumentFragment built from `nodes` (used for both the
745
+ * `children` of a component call and every named content slot). Children
746
+ * effects are collected into `effectsVar` (or `ctx.effects`) exactly as the
747
+ * component-call path does today, so slot bodies inherit identical effects,
748
+ * claim, and async semantics as regular children.
749
+ */
750
+ function emitFragment(ctx, nodes, tracked, effectsVar) {
751
+ const frag = ctx.n();
752
+ const savedEffects = ctx.effects;
753
+ ctx.effects = [];
754
+ ctx.push(`const ${frag} = (() => { const $f = document.createDocumentFragment();`);
755
+ for (const child of nodes) {
756
+ const childVar = emitNode(ctx, child, tracked, effectsVar, '$f');
757
+ if (childVar)
758
+ ctx.push(`$f.appendChild(${childVar});`);
759
+ }
760
+ for (const eff of ctx.effects)
761
+ ctx.push(effectsVar ? `${effectsVar}.push(${effectBlockToHandlerExpr(eff)});` : eff);
762
+ ctx.effects = savedEffects;
763
+ ctx.push(`return $f; })();`);
764
+ return frag;
765
+ }
649
766
  function emitComponentCall(ctx, node, tracked, effectsVar, parentVar, compPrefix = '__components') {
650
767
  const propsEntries = node.props.map((p) => {
651
768
  if (typeof p.value === 'string')
@@ -661,7 +778,6 @@ function emitComponentCall(ctx, node, tracked, effectsVar, parentVar, compPrefix
661
778
  const expr = transformTracked(sp, tracked);
662
779
  propsEntries.push(`...${expr}`);
663
780
  }
664
- const propsObj = `{ ${propsEntries.join(', ')} }`;
665
781
  const v = ctx.n();
666
782
  // Await children whenever the calling scope is async. Async-ness is
667
783
  // parent-driven (mirroring the server side) so that imported async
@@ -671,6 +787,11 @@ function emitComponentCall(ctx, node, tracked, effectsVar, parentVar, compPrefix
671
787
  // Member-expression tags (`<it.icon>`) carry the raw component-valued
672
788
  // expression — invoke it directly; it is never a registry name.
673
789
  const calleeExpr = node.calleeExpr ? `(${node.calleeExpr})` : null;
790
+ // Named content slots (`trigger={<Button/>}`) thread through the same channel
791
+ // as `children`: a fresh DocumentFragment per slot hoisted from the IR.
792
+ const slotChildren = node.children.filter((c) => c instanceof PropSlot);
793
+ const regChildren = node.children.filter((c) => !(c instanceof PropSlot));
794
+ const callArgs = () => `{ ${propsEntries.join(', ')} }`;
674
795
  if (ctx.hydrate) {
675
796
  const access = calleeExpr
676
797
  ?? (ctx.importedNames.has(node.componentName)
@@ -697,19 +818,20 @@ function emitComponentCall(ctx, node, tracked, effectsVar, parentVar, compPrefix
697
818
  ctx.push(`${walkerArg}.retireDetached();`);
698
819
  }
699
820
  };
700
- if (node.children.length > 0) {
821
+ for (const slot of slotChildren) {
822
+ const frag = emitFragment(ctx, slot.body, tracked, effectsVar);
823
+ propsEntries.push(`${JSON.stringify(slot.propName)}: ${frag}`);
824
+ }
825
+ if (regChildren.length > 0) {
701
826
  const frag = ctx.n();
702
- ctx.push(`const ${frag} = (() => { const $f = document.createDocumentFragment();`);
703
- const savedEffects = ctx.effects;
704
- // Link/NavLink hydrate branches WIPE their SSR anchor (a.replaceChildren())
705
- // and remount the children fragment. Suppressed fully-static children would
706
- // therefore be lost, so rebuild the whole subtree fresh instead of claiming.
707
827
  const wipeMount = ctx.linkNames.has(node.componentName);
708
828
  const savedHydrate = ctx.hydrate;
709
829
  if (wipeMount)
710
830
  ctx.hydrate = false;
831
+ ctx.push(`const ${frag} = (() => { const $f = document.createDocumentFragment();`);
832
+ const savedEffects = ctx.effects;
711
833
  ctx.effects = [];
712
- for (const child of node.children) {
834
+ for (const child of regChildren) {
713
835
  const childVar = emitNode(ctx, child, tracked, effectsVar, '$f');
714
836
  if (childVar)
715
837
  ctx.push(`$f.appendChild(${childVar});`);
@@ -720,23 +842,23 @@ function emitComponentCall(ctx, node, tracked, effectsVar, parentVar, compPrefix
720
842
  ctx.hydrate = savedHydrate;
721
843
  ctx.push(`return $f; })();`);
722
844
  propsEntries.push(`children: ${frag}`);
723
- ctx.push(`const ${v} = ${awaitKw}${access}({ ${propsEntries.join(', ')} }, __registry, ${walkerArg});`);
724
- maybeReplace(v);
725
- maybeRetire();
726
- return v;
727
845
  }
728
- ctx.push(`const ${v} = ${awaitKw}${access}(${propsObj}, __registry, ${walkerArg});`);
846
+ ctx.push(`const ${v} = ${awaitKw}${access}(${callArgs()}, __registry, ${walkerArg});`);
729
847
  maybeReplace(v);
730
848
  maybeRetire();
731
849
  return v;
732
850
  }
733
851
  else {
734
- if (node.children.length > 0) {
852
+ for (const slot of slotChildren) {
853
+ const frag = emitFragment(ctx, slot.body, tracked, effectsVar);
854
+ propsEntries.push(`${JSON.stringify(slot.propName)}: ${frag}`);
855
+ }
856
+ if (regChildren.length > 0) {
735
857
  const frag = ctx.n();
736
- ctx.push(`const ${frag} = (() => { const $f = document.createDocumentFragment();`);
737
858
  const savedEffects = ctx.effects;
738
859
  ctx.effects = [];
739
- for (const child of node.children) {
860
+ ctx.push(`const ${frag} = (() => { const $f = document.createDocumentFragment();`);
861
+ for (const child of regChildren) {
740
862
  const childVar = emitNode(ctx, child, tracked, effectsVar, '$f');
741
863
  if (childVar)
742
864
  ctx.push(`$f.appendChild(${childVar});`);
@@ -748,13 +870,13 @@ function emitComponentCall(ctx, node, tracked, effectsVar, parentVar, compPrefix
748
870
  propsEntries.push(`children: ${frag}`);
749
871
  }
750
872
  if (calleeExpr) {
751
- ctx.push(`const ${v} = ${calleeExpr}({ ${propsEntries.join(', ')} });`);
873
+ ctx.push(`const ${v} = ${calleeExpr}(${callArgs()});`);
752
874
  }
753
875
  else if (ctx.importedNames.has(node.componentName)) {
754
- ctx.push(`const ${v} = ${awaitKw}${node.componentName}({ ${propsEntries.join(', ')} });`);
876
+ ctx.push(`const ${v} = ${awaitKw}${node.componentName}(${callArgs()});`);
755
877
  }
756
878
  else {
757
- ctx.push(`const ${v} = ${awaitKw}${compPrefix}[${JSON.stringify(node.componentName)}]({ ${propsEntries.join(', ')} });`);
879
+ ctx.push(`const ${v} = ${awaitKw}${compPrefix}[${JSON.stringify(node.componentName)}](${callArgs()});`);
758
880
  }
759
881
  }
760
882
  return v;
@@ -1373,7 +1495,7 @@ function emitSwitchBlock(ctx, node, tracked, parentVar) {
1373
1495
  }`);
1374
1496
  return null;
1375
1497
  }
1376
- function emitMap(ctx, node, tracked, parentVar) {
1498
+ function emitMap(ctx, node, tracked, effTarget, parentVar) {
1377
1499
  const arrExpr = transformTracked(node.expression, tracked);
1378
1500
  const itemVar = node.itemVariable;
1379
1501
  const anchor = ctx.n();
@@ -1393,6 +1515,26 @@ function emitMap(ctx, node, tracked, parentVar) {
1393
1515
  // emitting a top-level effect there would reference the loop/statement local
1394
1516
  // (`col`, `pages`) which is out of scope — `ReferenceError` at hydrate.
1395
1517
  const reactiveSource = isReactiveExpression(node.expression, tracked);
1518
+ // The re-render effect below closes over the anchors/reconciler declared
1519
+ // inline above. When this map is nested inside a branch/item builder those
1520
+ // bindings live in that nested closure — so the effect must be created in
1521
+ // the same scope (registered on the enclosing effects array), not flushed
1522
+ // to the component top level where the names are out of scope
1523
+ // (`$nNN is not defined` at hydration, plus an esbuild shadowing rename).
1524
+ const pushEffect = (lets, effCall) => {
1525
+ if (effTarget) {
1526
+ ctx.push(`${effTarget}.push((() => {
1527
+ ${lets}
1528
+ return ${effCall};
1529
+ })());`);
1530
+ }
1531
+ else {
1532
+ ctx.effects.push(`{
1533
+ ${lets}
1534
+ ${effCall};
1535
+ }`);
1536
+ }
1537
+ };
1396
1538
  ctx.push(`const ${anchor} = document.createComment('map');`);
1397
1539
  if (!hyd && !hydKeyed)
1398
1540
  ctx.push(`${parent}.appendChild(${anchor});`);
@@ -1480,6 +1622,13 @@ function emitMap(ctx, node, tracked, parentVar) {
1480
1622
  ctx.push(`const ${hasItems} = () => { const __l = ${arrExpr}; return __l != null && __l.length > 0; };`);
1481
1623
  if (node.keyExpr) {
1482
1624
  const keyExpr = transformTracked(node.keyExpr, tracked);
1625
+ // A `key` may reference the loop's `index` variable (`for (… ; key i; index i)`).
1626
+ // The key function is evaluated outside the item renderer (where the index is
1627
+ // in scope), so it must receive the index as its own parameter — otherwise the
1628
+ // emitted `item => i` key closes over an undefined `i` (ReferenceError).
1629
+ // Parenthesize the parameters: without them `item, i => …` parses as a comma
1630
+ // sequence expression rather than a two-parameter arrow in argument position.
1631
+ const keyFnSrc = `(${itemVar}${node.indexVariable ? `, ${node.indexVariable}` : ''}) => ${keyExpr}`;
1483
1632
  const reconciler = ctx.n();
1484
1633
  const initList = ctx.n();
1485
1634
  ctx.push(`let ${reconciler} = () => {};`);
@@ -1488,10 +1637,10 @@ function emitMap(ctx, node, tracked, parentVar) {
1488
1637
  // inside `parent` (pre-attach, `anchor.parentNode` is null) and peeks all
1489
1638
  // keys before claim-by-key consumes the walker, so server-rendered items
1490
1639
  // are adopted and wired in place instead of being rebuilt.
1491
- ctx.push(`const ${initList} = () => { ${reconciler} = reconcileHydrated(${anchor}, ${endAnchor}, ${arrExpr}, ${itemVar} => ${keyExpr}, (${itemVar}, __i, __e, __root) => ${renderItem}(${itemVar}${indexParam ? ', __i, __e' : ', __e'}, null, __root), __hydrate, ${parent}); };`);
1640
+ ctx.push(`const ${initList} = () => { ${reconciler} = reconcileHydrated(${anchor}, ${endAnchor}, ${arrExpr}, ${keyFnSrc}, (${itemVar}, __i, __e, __root) => ${renderItem}(${itemVar}${indexParam ? ', __i, __e' : ', __e'}, null, __root), __hydrate, ${parent}); };`);
1492
1641
  }
1493
1642
  else {
1494
- ctx.push(`const ${initList} = () => { ${reconciler} = reconcile(${anchor}, ${endAnchor}, ${arrExpr}, ${itemVar} => ${keyExpr}, (${itemVar}, __i, __e) => ${renderItem}(${itemVar}${indexParam ? ', __i, __e' : ', __e'})); };`);
1643
+ ctx.push(`const ${initList} = () => { ${reconciler} = reconcile(${anchor}, ${endAnchor}, ${arrExpr}, ${keyFnSrc}, (${itemVar}, __i, __e) => ${renderItem}(${itemVar}${indexParam ? ', __i, __e' : ', __e'})); };`);
1495
1644
  }
1496
1645
  if (emptyRenderName) {
1497
1646
  const isEmptyVar = ctx.n();
@@ -1499,9 +1648,7 @@ function emitMap(ctx, node, tracked, parentVar) {
1499
1648
  ctx.push(`if (!${isEmptyVar}) { ${initList}(); } else { ${aw}${emptyRenderName}(); }`);
1500
1649
  if (!reactiveSource)
1501
1650
  return null;
1502
- ctx.effects.push(`{
1503
- let __first = true;
1504
- effect(${effOpen}
1651
+ pushEffect(`let __first = true;`, `effect(${effOpen}
1505
1652
  const __new = ${hasItems}();
1506
1653
  if (__first) { __first = false; __new; return; }
1507
1654
  if (__new !== ${isEmptyVar}) {
@@ -1513,21 +1660,17 @@ function emitMap(ctx, node, tracked, parentVar) {
1513
1660
  ${effectsVar}.length = 0;
1514
1661
  __cleanup(${anchor}, ${endAnchor});
1515
1662
  if (__new) { ${initList}(); } else { ${aw}${emptyRenderName}(); }
1516
- });
1517
- }`);
1663
+ })`);
1518
1664
  }
1519
1665
  else {
1520
1666
  ctx.push(`${initList}();`);
1521
1667
  if (!reactiveSource)
1522
1668
  return null;
1523
- ctx.effects.push(`{
1524
- let __first = true;
1525
- effect(() => {
1669
+ pushEffect(`let __first = true;`, `effect(() => {
1526
1670
  const __nv = ${arrExpr};
1527
1671
  if (__first) { __first = false; __nv; ${reconciler}(${arrExpr}); return; }
1528
1672
  ${reconciler}(${arrExpr});
1529
- });
1530
- }`);
1673
+ })`);
1531
1674
  }
1532
1675
  }
1533
1676
  else {
@@ -1560,9 +1703,7 @@ function emitMap(ctx, node, tracked, parentVar) {
1560
1703
  ctx.push(`__place(${anchor}, ${endAnchor}, ${collectVar}, ${parent});`);
1561
1704
  if (!reactiveSource)
1562
1705
  return null;
1563
- ctx.effects.push(`{
1564
- let __first = true;
1565
- effect(${effOpen}
1706
+ pushEffect(`let __first = true;`, `effect(${effOpen}
1566
1707
  const __new = ${hasItems}();
1567
1708
  if (__first) { __first = false; __new; return; }
1568
1709
  if (__new !== ${isEmptyVar}) {
@@ -1581,8 +1722,7 @@ ${renderAllItems(' ', null)}
1581
1722
  if (__new) {
1582
1723
  ${renderAllItems(' ', null)}
1583
1724
  } else { ${aw}${emptyRenderName}(${collectVar ? collectVar : ''}); }
1584
- });
1585
- }`);
1725
+ })`);
1586
1726
  }
1587
1727
  else {
1588
1728
  const collectVar = hyd ? ctx.n() : null;
@@ -1593,17 +1733,14 @@ ${renderAllItems(' ', null)}
1593
1733
  ctx.push(`__place(${anchor}, ${endAnchor}, ${collectVar}, ${parent});`);
1594
1734
  if (!reactiveSource)
1595
1735
  return null;
1596
- ctx.effects.push(`{
1597
- let __first = true;
1598
- effect(${effOpen}
1736
+ pushEffect(`let __first = true;`, `effect(${effOpen}
1599
1737
  const __nv = ${arrExpr};
1600
1738
  if (__first) { __first = false; __nv; return; }
1601
1739
  for (const e of ${effectsVar}) destroy_block(e);
1602
1740
  ${effectsVar}.length = 0;
1603
1741
  __cleanup(${anchor}, ${endAnchor});
1604
1742
  ${renderAllItems(' ', null)}
1605
- });
1606
- }`);
1743
+ })`);
1607
1744
  }
1608
1745
  }
1609
1746
  return null;
@@ -1611,9 +1748,9 @@ ${renderAllItems(' ', null)}
1611
1748
  function computeAsyncComponents(comps) {
1612
1749
  return new Set(comps.filter((c) => c.isAsync).map((c) => c.name));
1613
1750
  }
1614
- function generateComponent(comp, importedNames = new Set(), hydrate = false, asyncComps = new Set(), linkNames = new Set(), selfClaimNames = new Set()) {
1751
+ function generateComponent(comp, importedNames = new Set(), hydrate = false, asyncComps = new Set(), linkNames = new Set(), selfClaimNames = new Set(), alloc) {
1615
1752
  const tracked = collectTrackedNames(comp.body);
1616
- const ctx = new Ctx();
1753
+ const ctx = new Ctx(alloc);
1617
1754
  ctx.importedNames = importedNames;
1618
1755
  ctx.linkNames = linkNames;
1619
1756
  ctx.selfClaimNames = selfClaimNames;
@@ -1632,7 +1769,16 @@ function generateComponent(comp, importedNames = new Set(), hydrate = false, asy
1632
1769
  ctx.push(indent(`}`));
1633
1770
  }
1634
1771
  if (ctx.hydrate) {
1772
+ // Two distinct roles must not be conflated:
1773
+ // - `$root` is the shared page container. It is only ever a last-resort
1774
+ // mount target for detached dynamic regions (`__place`) that could not
1775
+ // be claimed in place; it must never be handed back to a caller.
1776
+ // - `$mount` is THIS component's own first top-level node. It is what the
1777
+ // hydrator returns so the parent can position/guard the component. A
1778
+ // component's own node is never an ancestor of the caller, so the
1779
+ // parent's `contains()` guards and `__place` end-anchor bounds are valid.
1635
1780
  ctx.push(indent(`const $root = __hydrate.root;`));
1781
+ ctx.push(indent(`let $mount = null;`));
1636
1782
  }
1637
1783
  else {
1638
1784
  ctx.push(indent(`const $root = document.createDocumentFragment();`));
@@ -1653,7 +1799,11 @@ function generateComponent(comp, importedNames = new Set(), hydrate = false, asy
1653
1799
  const v = emitNode(ctx, node, tracked, null);
1654
1800
  if (v) {
1655
1801
  if (ctx.hydrate) {
1656
- ctx.push(indent(`if (${v}.parentNode !== $root) { if (!$root || ${v}.parentNode == null || !${v}.parentNode.contains(${v})) $root.appendChild(${v}); }`));
1802
+ // The first top-level node becomes `$mount`. Late top-level nodes are
1803
+ // already in the DOM when claimed; only a divergent fresh node is
1804
+ // nested onto the mount (matching the pre-marker wrapper behaviour).
1805
+ ctx.push(indent(`if ($mount === null) $mount = ${v};`));
1806
+ ctx.push(indent(`if (${v} !== $mount && ${v}.parentNode == null) $mount.appendChild(${v});`));
1657
1807
  }
1658
1808
  else {
1659
1809
  ctx.push(indent(`$root.appendChild(${v});`));
@@ -1666,7 +1816,15 @@ function generateComponent(comp, importedNames = new Set(), hydrate = false, asy
1666
1816
  const delCode = ctx.emitDelegates();
1667
1817
  if (delCode)
1668
1818
  ctx.push(indent(delCode.trim()));
1669
- ctx.push(indent(`return __pendingChild || $root;`));
1819
+ if (ctx.hydrate) {
1820
+ // A hydrator with no claimable nodes must still hand its caller a value the
1821
+ // parent guards can safely inspect (an empty fragment places nothing).
1822
+ ctx.push(indent(`if ($mount === null) $mount = document.createDocumentFragment();`));
1823
+ ctx.push(indent(`return __pendingChild || $mount;`));
1824
+ }
1825
+ else {
1826
+ ctx.push(indent(`return __pendingChild || $root;`));
1827
+ }
1670
1828
  ctx.push(indent(`} finally {`));
1671
1829
  ctx.push(indent(`\tsetActiveComponent(__prev);`));
1672
1830
  ctx.push(indent(`}`));
@@ -1680,7 +1838,7 @@ function buildParamInit(paramNames) {
1680
1838
  return '';
1681
1839
  return `const { ${paramNames.join(', ')} } = props;`;
1682
1840
  }
1683
- function buildComponentMap(irRoot, hydrate = false) {
1841
+ function buildComponentMap(irRoot, hydrate = false, alloc) {
1684
1842
  const mapLines = [];
1685
1843
  mapLines.push(`const __components = {};`);
1686
1844
  const asyncComps = computeAsyncComponents(irRoot.components);
@@ -1721,7 +1879,7 @@ function buildComponentMap(irRoot, hydrate = false) {
1721
1879
  mapLines.push(`__components[${JSON.stringify(comp.name)}] = ${stub};`);
1722
1880
  continue;
1723
1881
  }
1724
- const code = generateComponent(comp, directNames, hydrate, asyncComps, linkNames, selfClaimNames);
1882
+ const code = generateComponent(comp, directNames, hydrate, asyncComps, linkNames, selfClaimNames, alloc);
1725
1883
  mapLines.push(`__components[${JSON.stringify(comp.name)}] = ${code};`);
1726
1884
  }
1727
1885
  mapLines.push(`function __cleanup(start, end) {`);
@@ -1879,6 +2037,10 @@ function findBindingInIR(nodes, names) {
1879
2037
  if (findBindingInIR(node.children, names))
1880
2038
  return true;
1881
2039
  }
2040
+ else if (node instanceof PropSlot) {
2041
+ if (findBindingInIR(node.body, names))
2042
+ return true;
2043
+ }
1882
2044
  else if (node instanceof RuntimeStatement) {
1883
2045
  if (astHasBinding(node.ast, names))
1884
2046
  return true;
@@ -1907,6 +2069,10 @@ function findFormNameInIR(nodes, name) {
1907
2069
  if (findFormNameInIR(node.children, name))
1908
2070
  return true;
1909
2071
  }
2072
+ if (node instanceof PropSlot) {
2073
+ if (findFormNameInIR(node.body, name))
2074
+ return true;
2075
+ }
1910
2076
  if (node instanceof DynamicBinding && node.expression && node.expression.raw) {
1911
2077
  if (node.expression.raw.includes(fnPattern))
1912
2078
  return true;
@@ -1985,7 +2151,7 @@ function emitClientFromIR(ir, options) {
1985
2151
  if (!options.forceClient && !needsClient) {
1986
2152
  return '';
1987
2153
  }
1988
- const componentMapCode = buildComponentMap(ir, options.hydrate);
2154
+ const componentMapCode = buildComponentMap(ir, options.hydrate, options.nameAllocator);
1989
2155
  const importLines = ir.imports.length > 0 ? ir.imports.join('\n') + '\n' : '';
1990
2156
  const topCode = (options.includeTopLevel === false ? [] : transformTopLevelForActions(ir.topLevelCode, 'client')).join('\n') + '\n';
1991
2157
  const exportLines = [];
@@ -2000,7 +2166,7 @@ function emitClientFromIR(ir, options) {
2000
2166
  }
2001
2167
  }
2002
2168
  const exportCode = exportLines.join('\n');
2003
- const runtimeNames = ['track', 'get', 'set', 'destroy_block', 'getActiveComponent', 'setActiveComponent', 'reactiveProps'];
2169
+ const runtimeNames = ['track', 'get', 'set', 'destroy_block', 'getActiveComponent', 'setActiveComponent', 'reactiveProps', 'applyStyle'];
2004
2170
  if (ir.components.some(c => !isStaticIR(c.body)))
2005
2171
  runtimeNames.push('effect');
2006
2172
  for (const name of usedRuntimeBindings(ir))
@@ -2051,7 +2217,7 @@ export function compileClient(source, _componentName, options = {}) {
2051
2217
  }
2052
2218
  const ast = parse(source, options.sourcePath ? { filename: options.sourcePath } : {});
2053
2219
  const ir = generateIR(ast, source, options.sourcePath);
2054
- return emitClientFromIR(ir, options);
2220
+ return emitClientFromIR(ir, { ...options, nameAllocator: options.nameAllocator ?? nameAllocFor(options.sourcePath) });
2055
2221
  }
2056
2222
  /**
2057
2223
  * Compiles a component source in BOTH client modes (plain + hydrate)
@@ -2080,9 +2246,12 @@ export function compileClientBoth(source, _componentName, sourcePath) {
2080
2246
  if (exportedComp)
2081
2247
  name = exportedComp.name;
2082
2248
  }
2249
+ // Comp and hyd land in the same scoped file block, so they share one
2250
+ // allocator: hyd continues after the names comp already consumed.
2251
+ const alloc = nameAllocFor(sourcePath);
2083
2252
  return {
2084
- comp: emitClientFromIR(ir, { forceClient: true }),
2085
- hyd: emitClientFromIR(irHyd, { forceClient: true, hydrate: true, includeTopLevel: false }),
2253
+ comp: emitClientFromIR(ir, { forceClient: true, nameAllocator: alloc }),
2254
+ hyd: emitClientFromIR(irHyd, { forceClient: true, hydrate: true, includeTopLevel: false, nameAllocator: alloc }),
2086
2255
  name,
2087
2256
  };
2088
2257
  }
package/dist/errors.d.ts CHANGED
@@ -28,6 +28,9 @@ export declare class VeskError extends Error {
28
28
  static componentNotFound(name: string, available?: string[], context?: VeskErrorOptions): VeskError;
29
29
  static configError(msg: string, validOptions?: string[], context?: VeskErrorOptions): VeskError;
30
30
  static asyncChildInSyncParent(parentName: string, childName: string, context?: VeskErrorOptions): VeskError;
31
+ static attrJsxElement(context?: VeskErrorOptions & {
32
+ attr?: string;
33
+ }): VeskError;
31
34
  toString(): string;
32
35
  toJSON(): Record<string, unknown>;
33
36
  }