@vesk/compiler 0.2.16 → 0.2.18

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.
@@ -8,6 +8,14 @@ export interface TrackedInfo {
8
8
  export declare function semicolonizeStatement(code: string): string;
9
9
  export declare function transformTracked(irNode: Expression | RuntimeStatement | DynamicBinding, tracked: Map<string, TrackedInfo>): string;
10
10
  export declare function collectTrackedNames(body: IRNode[]): Map<string, TrackedInfo>;
11
+ /**
12
+ * Tracked reads inside a track/derived declaration init are re-emitted as
13
+ * `get(…)` subscriptions so a `derived(() => … cell …)` (or
14
+ * `track(() => … cell …)`) recomputes when the cell changes, instead of
15
+ * reading the raw cell object as a key/property. The init is parsed with the
16
+ * normal vesk parser so TS generics and `.vsk` syntax survive.
17
+ */
18
+ export declare function transformTrackedInit(init: string, tracked: Map<string, TrackedInfo>): string;
11
19
  declare function isStaticIR(body: IRNode[]): boolean;
12
20
  export declare function escapeHtml(str: string): string;
13
21
  export declare function compileClient(source: string, _componentName: string | null, options?: {
@@ -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;AAwCxD,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;AA8kCD,iBAAS,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAe3C;AA2HD,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAO9C;AAoED,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,EAOjB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAwCxD,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;AA0pCD,iBAAS,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAe3C;AA2HD,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAO9C;AAoED,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"}
@@ -208,6 +208,29 @@ export function collectTrackedNames(body) {
208
208
  }
209
209
  return names;
210
210
  }
211
+ /**
212
+ * Tracked reads inside a track/derived declaration init are re-emitted as
213
+ * `get(…)` subscriptions so a `derived(() => … cell …)` (or
214
+ * `track(() => … cell …)`) recomputes when the cell changes, instead of
215
+ * reading the raw cell object as a key/property. The init is parsed with the
216
+ * normal vesk parser so TS generics and `.vsk` syntax survive.
217
+ */
218
+ export function transformTrackedInit(init, tracked) {
219
+ if (!init)
220
+ return init;
221
+ let expr = null;
222
+ try {
223
+ const prog = parse(`(${init})`);
224
+ expr = prog.body?.[0]?.type === 'ExpressionStatement' ? (prog.body[0].expression ?? null) : null;
225
+ }
226
+ catch {
227
+ // Unparseable init (defensive) — leave raw; the generated code is checked
228
+ // for syntax at build time and would be reported there.
229
+ }
230
+ if (!expr)
231
+ return init;
232
+ return transformTracked({ raw: init, ast: expr }, tracked);
233
+ }
211
234
  const NON_BUBBLING_EVENTS = new Set([
212
235
  'focus', 'blur',
213
236
  'mouseenter', 'mouseleave',
@@ -229,6 +252,7 @@ class Ctx {
229
252
  effects = [];
230
253
  c = 0;
231
254
  importedNames = new Set();
255
+ linkNames = new Set();
232
256
  delegatedEvents = new Set();
233
257
  directEvents = new Set();
234
258
  hydrate = false;
@@ -281,7 +305,7 @@ function emitNode(ctx, node, tracked, effectsVar, parentVar, compPrefix = '__com
281
305
  return emitDynamicBinding(ctx, node, tracked, effectsVar);
282
306
  if (node instanceof TrackDecl) {
283
307
  const cellName = node.rawName || node.name;
284
- const init = stripTrackGeneric(node.init);
308
+ const init = transformTrackedInit(stripTrackGeneric(node.init), tracked);
285
309
  ctx.push(`const ${cellName} = ${init};`);
286
310
  return null;
287
311
  }
@@ -621,6 +645,13 @@ function emitComponentCall(ctx, node, tracked, effectsVar, parentVar, compPrefix
621
645
  ctx.push(`const ${frag} = (() => { const $f = document.createDocumentFragment();`);
622
646
  const savedWalker = ctx.walker;
623
647
  const savedEffects = ctx.effects;
648
+ // Link/NavLink hydrate branches WIPE their SSR anchor (a.replaceChildren())
649
+ // and remount the children fragment. Suppressed fully-static children would
650
+ // therefore be lost, so rebuild the whole subtree fresh instead of claiming.
651
+ const wipeMount = ctx.linkNames.has(node.componentName);
652
+ const savedHydrate = ctx.hydrate;
653
+ if (wipeMount)
654
+ ctx.hydrate = false;
624
655
  ctx.walker = walkerVar;
625
656
  ctx.effects = [];
626
657
  for (const child of node.children) {
@@ -632,6 +663,7 @@ function emitComponentCall(ctx, node, tracked, effectsVar, parentVar, compPrefix
632
663
  ctx.push(effectsVar ? `${effectsVar}.push(${effectBlockToHandlerExpr(eff)});` : eff);
633
664
  ctx.effects = savedEffects;
634
665
  ctx.walker = savedWalker;
666
+ ctx.hydrate = savedHydrate;
635
667
  ctx.push(`return $f; })();`);
636
668
  propsEntries.push(`children: ${frag}`);
637
669
  ctx.push(`const ${v} = ${awaitKw}${access}({ ${propsEntries.join(', ')} }, __registry, ${walkerVar});`);
@@ -697,7 +729,7 @@ function emitTryCatch(ctx, node, tracked, effectsVar, parentVar) {
697
729
  ctx.push(indent(`const __cl = [];`));
698
730
  }
699
731
  for (const child of body) {
700
- const childVar = emitNode(ctx, child, tracked, isCatch ? null : effArr, undefined, compPrefix);
732
+ const childVar = emitNode(ctx, child, tracked, isCatch ? null : effArr, hydMode ? undefined : '__p', compPrefix);
701
733
  if (childVar) {
702
734
  if (hydMode)
703
735
  ctx.push(indent(`__cl.push(${childVar});`));
@@ -817,7 +849,7 @@ function emitOpaque(ctx, node, tracked, effTarget, parentVar) {
817
849
  ctx.push(`if (${condExpr}) { ${asyncKw}${conRenderName}(); }` + (hasElse ? ` else { ${asyncKw}${altRenderName}(); }` : ''));
818
850
  const renderBranch = `if (__nv) { ${asyncKw}${conRenderName}(); }` + (hasElse ? ` else { ${asyncKw}${altRenderName}(); }` : '');
819
851
  const effectBody = `effect(${ctx.isAsyncScope ? 'async () => {' : '() => {'}
820
- if (__first) { __first = false; return; }
852
+ if (__first) { __first = false; __iv = ${condExpr}; return; }
821
853
  const __nv = ${condExpr};
822
854
  if (__nv !== __iv) {
823
855
  for (const e of ${effectsVar}) destroy_block(e);
@@ -827,6 +859,11 @@ function emitOpaque(ctx, node, tracked, effTarget, parentVar) {
827
859
  __iv = __nv;
828
860
  }
829
861
  });`;
862
+ // A static condition renders once; an effect referencing it would only ever
863
+ // reference statement/loop-local bindings that are out of scope at the flush
864
+ // site (e.g. `const pages = …; if (pages.length > 0)` inside a for-of body).
865
+ if (!isReactiveExpression(node.condition, tracked))
866
+ return null;
830
867
  if (effTarget) {
831
868
  ctx.push(`${effTarget}.push((() => {
832
869
  let __iv = ${condExpr};
@@ -859,17 +896,19 @@ function emitWhileLoop(ctx, node, tracked, parentVar) {
859
896
  ctx.push(`const ${renderLoop} = ${ctx.isAsyncScope ? 'async () => {' : '() => {'}`);
860
897
  if (!hyd)
861
898
  ctx.push(indent(`const __p = ${anchor}.parentNode;`));
899
+ if (!hyd)
900
+ ctx.push(indent(`const __b = document.createDocumentFragment();`));
862
901
  if (hyd)
863
902
  ctx.push(indent(`const __cl = [];`));
864
903
  if (node.isDoWhile) {
865
904
  ctx.push(indent(`do {`));
866
905
  for (const n of node.bodyTemplate) {
867
- const v = emitNode(ctx, n, tracked, effectsVar, hyd ? parentVar : undefined);
906
+ const v = emitNode(ctx, n, tracked, effectsVar, hyd ? parentVar : '__b');
868
907
  if (v) {
869
908
  if (hyd)
870
909
  ctx.push(indent(`__cl.push(${v});`));
871
910
  else
872
- ctx.push(indent(`__p.insertBefore(${v}, ${endAnchor});`));
911
+ ctx.push(indent(`__b.appendChild(${v});`));
873
912
  }
874
913
  }
875
914
  ctx.push(indent(`} while (${condExpr});`));
@@ -877,16 +916,18 @@ function emitWhileLoop(ctx, node, tracked, parentVar) {
877
916
  else {
878
917
  ctx.push(indent(`while (${condExpr}) {`));
879
918
  for (const n of node.bodyTemplate) {
880
- const v = emitNode(ctx, n, tracked, effectsVar, hyd ? parentVar : undefined);
919
+ const v = emitNode(ctx, n, tracked, effectsVar, hyd ? parentVar : '__b');
881
920
  if (v) {
882
921
  if (hyd)
883
922
  ctx.push(indent(`__cl.push(${v});`));
884
923
  else
885
- ctx.push(indent(`__p.insertBefore(${v}, ${endAnchor});`));
924
+ ctx.push(indent(`__b.appendChild(${v});`));
886
925
  }
887
926
  }
888
927
  ctx.push(indent(`}`));
889
928
  }
929
+ if (!hyd)
930
+ ctx.push(indent(`__p.insertBefore(__b, ${endAnchor});`));
890
931
  if (hyd)
891
932
  ctx.push(indent(`__place(${anchor}, ${endAnchor}, __cl, ${parent});`));
892
933
  ctx.push(`};`);
@@ -898,7 +939,7 @@ function emitWhileLoop(ctx, node, tracked, parentVar) {
898
939
  let __iv = (${condExpr});
899
940
  let __first = true;
900
941
  effect(${ctx.isAsyncScope ? 'async () => {' : '() => {'}
901
- if (__first) { __first = false; return; }
942
+ if (__first) { __first = false; __iv = (${condExpr}); return; }
902
943
  if (__busy) return;
903
944
  const __nv = (${condExpr});
904
945
  if (__nv !== __iv) {
@@ -929,40 +970,55 @@ function emitForLoop(ctx, node, tracked, parentVar) {
929
970
  ctx.push(`const ${renderLoop} = ${ctx.isAsyncScope ? 'async () => {' : '() => {'}`);
930
971
  if (!hyd)
931
972
  ctx.push(indent(`const __p = ${anchor}.parentNode;`));
973
+ if (!hyd)
974
+ ctx.push(indent(`const __b = document.createDocumentFragment();`));
932
975
  if (hyd)
933
976
  ctx.push(indent(`const __cl = [];`));
934
977
  if (node.kind === 'for-in') {
935
978
  const srcExpr = transformTracked(node.condition, tracked);
936
979
  ctx.push(indent(`for (${node.init} of (Array.isArray(${srcExpr}) ? ${srcExpr} : (${srcExpr} == null ? [] : Object.keys(${srcExpr})))) {`));
937
980
  for (const n of node.bodyTemplate) {
938
- const v = emitNode(ctx, n, tracked, effectsVar, hyd ? parentVar : undefined);
981
+ const v = emitNode(ctx, n, tracked, effectsVar, hyd ? parentVar : '__b');
939
982
  if (v) {
940
983
  if (hyd)
941
984
  ctx.push(indent(`__cl.push(${v});`));
942
985
  else
943
- ctx.push(indent(`__p.insertBefore(${v}, ${endAnchor});`));
986
+ ctx.push(indent(`__b.appendChild(${v});`));
944
987
  }
945
988
  }
946
989
  ctx.push(indent(`}`));
947
990
  }
948
991
  else {
949
992
  const condExpr = transformTracked(node.condition, tracked);
950
- if (node.init)
951
- ctx.push(indent(`${node.init}`));
952
- ctx.push(indent(`while (${condExpr}) {`));
993
+ // Emit a real `for` statement. A `let`/`const` declared in the init clause
994
+ // gets a fresh binding per iteration (per the spec), so closures created in
995
+ // the body — event handlers, effects, `createComment('if')` render fns —
996
+ // capture that iteration's value instead of the loop variable's final value.
997
+ // The old hoisted `let i = 0; while (…) { …; i++; }` shape made every
998
+ // closure alias-exit to the final counter (e.g. `onClick={() => index = i}`
999
+ // inside `for (let i = 0; i < n; i++)` always wrote n-last, and
1000
+ // `aria-pressed={i === index}` compared against n-last for every button).
1001
+ let initPart = (node.init || '').trim();
1002
+ let updatePart = (node.update || '').trim();
1003
+ if (initPart.endsWith(';'))
1004
+ initPart = initPart.slice(0, -1).trim();
1005
+ if (updatePart.endsWith(';'))
1006
+ updatePart = updatePart.slice(0, -1).trim();
1007
+ const head = initPart ? `${initPart}; ${condExpr}` : `; ${condExpr}`;
1008
+ ctx.push(indent(`for (${head}; ${updatePart}) {`));
953
1009
  for (const n of node.bodyTemplate) {
954
- const v = emitNode(ctx, n, tracked, effectsVar, hyd ? parentVar : undefined);
1010
+ const v = emitNode(ctx, n, tracked, effectsVar, hyd ? parentVar : '__b');
955
1011
  if (v) {
956
1012
  if (hyd)
957
1013
  ctx.push(indent(`__cl.push(${v});`));
958
1014
  else
959
- ctx.push(indent(`__p.insertBefore(${v}, ${endAnchor});`));
1015
+ ctx.push(indent(`__b.appendChild(${v});`));
960
1016
  }
961
1017
  }
962
- if (node.update)
963
- ctx.push(indent(`${node.update}`));
964
1018
  ctx.push(indent(`}`));
965
1019
  }
1020
+ if (!hyd)
1021
+ ctx.push(indent(`__p.insertBefore(__b, ${endAnchor});`));
966
1022
  if (hyd)
967
1023
  ctx.push(indent(`__place(${anchor}, ${endAnchor}, __cl, ${parent});`));
968
1024
  ctx.push(`};`);
@@ -971,12 +1027,20 @@ function emitForLoop(ctx, node, tracked, parentVar) {
971
1027
  ctx.push(`${ctx.isAsyncScope ? 'await ' : ''}${renderLoop}();`);
972
1028
  if (node.kind === 'for-in') {
973
1029
  const srcExpr = transformTracked(node.condition, tracked);
1030
+ // A `for ... of x` loop is only reactive when its source references a
1031
+ // tracked cell/prop. Emitting the re-render effect unconditionally would
1032
+ // reference the loop-local item variable (e.g. `col`, `group`, `p`) at
1033
+ // the top level, where it is out of scope — `ReferenceError` at hydrate
1034
+ // for loops over static/member collections or statement-mode locals.
1035
+ if (!isReactiveExpression(node.condition, tracked)) {
1036
+ return null;
1037
+ }
974
1038
  ctx.effects.push(`{
975
1039
  let __busy = false;
976
1040
  let __iv = undefined;
977
1041
  let __first = true;
978
1042
  effect(${ctx.isAsyncScope ? 'async () => {' : '() => {'}
979
- if (__first) { __first = false; return; }
1043
+ if (__first) { __first = false; __iv = (${srcExpr}); return; }
980
1044
  if (__busy) return;
981
1045
  const __nv = (${srcExpr});
982
1046
  if (__nv !== __iv) {
@@ -1012,10 +1076,16 @@ function emitSwitchBlock(ctx, node, tracked, parentVar) {
1012
1076
  ctx.push(`const ${endAnchor} = document.createComment('switch-end');`);
1013
1077
  const renderSwitch = ctx.n();
1014
1078
  ctx.push(`const ${renderSwitch} = ${ctx.isAsyncScope ? 'async () => {' : '() => {'}`);
1015
- if (!hyd)
1016
- ctx.push(indent(`const __p = ${anchor}.parentNode;`));
1017
- if (hyd)
1079
+ if (hyd) {
1018
1080
  ctx.push(indent(`const __cl = [];`));
1081
+ }
1082
+ else {
1083
+ // Case bodies can contain block-level nodes (`if`/`map`/…). Route them into
1084
+ // a whole-case fragment so their anchors stay inside the switch region
1085
+ // instead of leaking to `$root` on fresh client renders.
1086
+ ctx.push(indent(`const __p = ${anchor}.parentNode;`));
1087
+ ctx.push(indent(`const __c = document.createDocumentFragment();`));
1088
+ }
1019
1089
  ctx.push(indent(`switch (${discExpr}) {`));
1020
1090
  for (const c of node.cases) {
1021
1091
  if (c.test) {
@@ -1025,17 +1095,19 @@ function emitSwitchBlock(ctx, node, tracked, parentVar) {
1025
1095
  ctx.push(indent(`default:`));
1026
1096
  }
1027
1097
  for (const n of c.body) {
1028
- const v = emitNode(ctx, n, tracked, effectsVar, hyd ? parentVar : undefined);
1098
+ const v = emitNode(ctx, n, tracked, effectsVar, hyd ? parentVar : '__c');
1029
1099
  if (v) {
1030
1100
  if (hyd)
1031
1101
  ctx.push(indent(`__cl.push(${v});`));
1032
1102
  else
1033
- ctx.push(indent(`__p.insertBefore(${v}, ${endAnchor});`));
1103
+ ctx.push(indent(`__c.appendChild(${v});`));
1034
1104
  }
1035
1105
  }
1036
1106
  ctx.push(indent(`break;`));
1037
1107
  }
1038
1108
  ctx.push(indent(`}`));
1109
+ if (!hyd)
1110
+ ctx.push(indent(`__p.insertBefore(__c, ${endAnchor});`));
1039
1111
  if (hyd)
1040
1112
  ctx.push(indent(`__place(${anchor}, ${endAnchor}, __cl, ${parent});`));
1041
1113
  ctx.push(`};`);
@@ -1047,7 +1119,7 @@ function emitSwitchBlock(ctx, node, tracked, parentVar) {
1047
1119
  let __iv = (${discExpr});
1048
1120
  let __first = true;
1049
1121
  effect(${ctx.isAsyncScope ? 'async () => {' : '() => {'}
1050
- if (__first) { __first = false; return; }
1122
+ if (__first) { __first = false; __iv = (${discExpr}); return; }
1051
1123
  if (__busy) return;
1052
1124
  const __nv = (${discExpr});
1053
1125
  if (__nv !== __iv) {
@@ -1074,6 +1146,12 @@ function emitMap(ctx, node, tracked, parentVar) {
1074
1146
  const keyed = !!node.keyExpr;
1075
1147
  const hyd = ctx.hydrate && !keyed;
1076
1148
  const parent = parentVar || '$root';
1149
+ // A re-render effect is only valid when the map iterates a tracked/reactive
1150
+ // source. Iterating a static collection (module const, top-level array) or a
1151
+ // loop/statement-local derived value (e.g. `col.links`, `pages`) renders once;
1152
+ // emitting a top-level effect there would reference the loop/statement local
1153
+ // (`col`, `pages`) which is out of scope — `ReferenceError` at hydrate.
1154
+ const reactiveSource = isReactiveExpression(node.expression, tracked);
1077
1155
  ctx.push(`const ${anchor} = document.createComment('map');`);
1078
1156
  if (!hyd)
1079
1157
  ctx.push(`${parent}.appendChild(${anchor});`);
@@ -1085,16 +1163,29 @@ function emitMap(ctx, node, tracked, parentVar) {
1085
1163
  ctx.push(indent(`__r = __r || ${endAnchor};`));
1086
1164
  if (node.indexVariable)
1087
1165
  ctx.push(indent(`const ${node.indexVariable} = __i;`));
1088
- ctx.push(indent(`const __p = ${anchor}.parentNode;`));
1166
+ if (hyd) {
1167
+ ctx.push(indent(`const __p = ${anchor}.parentNode;`));
1168
+ }
1169
+ else {
1170
+ // Block-level item nodes (`if`/`map`/`try`/…) self-append their anchors to
1171
+ // whatever container they are told to. A fresh fragment per item keeps them
1172
+ // inside the map region (before `__r`/`endAnchor`) instead of leaking to
1173
+ // `$root`, which would render map content above the enclosing layout slot
1174
+ // during every fresh client render (SPA navigation / renderMatch).
1175
+ ctx.push(indent(`const __p = ${anchor}.parentNode;`));
1176
+ ctx.push(indent(`const __it = document.createDocumentFragment();`));
1177
+ }
1089
1178
  for (const n of node.bodyTemplate) {
1090
- const v = emitNode(ctx, n, tracked, '__e');
1179
+ const v = emitNode(ctx, n, tracked, '__e', hyd ? undefined : '__it');
1091
1180
  if (v) {
1092
1181
  if (hyd)
1093
1182
  ctx.push(indent(`if (__cl) __cl.push(${v}); else __p.insertBefore(${v}, __r);`));
1094
1183
  else
1095
- ctx.push(indent(`__p.insertBefore(${v}, __r);`));
1184
+ ctx.push(indent(`__it.appendChild(${v});`));
1096
1185
  }
1097
1186
  }
1187
+ if (!hyd)
1188
+ ctx.push(indent(`__p.insertBefore(__it, __r);`));
1098
1189
  ctx.push(`};`);
1099
1190
  let emptyRenderName = null;
1100
1191
  if (node.alternateNodes.length > 0) {
@@ -1137,11 +1228,13 @@ function emitMap(ctx, node, tracked, parentVar) {
1137
1228
  const isEmptyVar = ctx.n();
1138
1229
  ctx.push(`let ${isEmptyVar} = !${hasItems}();`);
1139
1230
  ctx.push(`if (!${isEmptyVar}) { ${initList}(); } else { ${aw}${emptyRenderName}(); }`);
1231
+ if (!reactiveSource)
1232
+ return null;
1140
1233
  ctx.effects.push(`{
1141
1234
  let __first = true;
1142
1235
  effect(${effOpen}
1143
1236
  const __new = ${hasItems}();
1144
- if (__first) { __first = false; return; }
1237
+ if (__first) { __first = false; __new; return; }
1145
1238
  if (__new !== ${isEmptyVar}) {
1146
1239
  if (__new) ${reconciler}(${arrExpr});
1147
1240
  return;
@@ -1156,11 +1249,13 @@ function emitMap(ctx, node, tracked, parentVar) {
1156
1249
  }
1157
1250
  else {
1158
1251
  ctx.push(`${initList}();`);
1252
+ if (!reactiveSource)
1253
+ return null;
1159
1254
  ctx.effects.push(`{
1160
1255
  let __first = true;
1161
1256
  effect(() => {
1162
1257
  const __nv = ${arrExpr};
1163
- if (__first) { __first = false; return; }
1258
+ if (__first) { __first = false; __nv; ${reconciler}(${arrExpr}); return; }
1164
1259
  ${reconciler}(${arrExpr});
1165
1260
  });
1166
1261
  }`);
@@ -1194,11 +1289,13 @@ function emitMap(ctx, node, tracked, parentVar) {
1194
1289
  ctx.push(indent(`} else { ${aw}${emptyRenderName}(${collectVar ? collectVar : ''}); }`));
1195
1290
  if (collectVar)
1196
1291
  ctx.push(`__place(${anchor}, ${endAnchor}, ${collectVar}, ${parent});`);
1292
+ if (!reactiveSource)
1293
+ return null;
1197
1294
  ctx.effects.push(`{
1198
1295
  let __first = true;
1199
1296
  effect(${effOpen}
1200
1297
  const __new = ${hasItems}();
1201
- if (__first) { __first = false; return; }
1298
+ if (__first) { __first = false; __new; return; }
1202
1299
  if (__new !== ${isEmptyVar}) {
1203
1300
  if (__new) {
1204
1301
  for (const e of ${effectsVar}) destroy_block(e);
@@ -1225,11 +1322,13 @@ ${renderAllItems(' ', null)}
1225
1322
  ctx.push(renderAllItems('', collectVar));
1226
1323
  if (collectVar)
1227
1324
  ctx.push(`__place(${anchor}, ${endAnchor}, ${collectVar}, ${parent});`);
1325
+ if (!reactiveSource)
1326
+ return null;
1228
1327
  ctx.effects.push(`{
1229
1328
  let __first = true;
1230
1329
  effect(${effOpen}
1231
1330
  const __nv = ${arrExpr};
1232
- if (__first) { __first = false; return; }
1331
+ if (__first) { __first = false; __nv; return; }
1233
1332
  for (const e of ${effectsVar}) destroy_block(e);
1234
1333
  ${effectsVar}.length = 0;
1235
1334
  __cleanup(${anchor}, ${endAnchor});
@@ -1243,10 +1342,11 @@ ${renderAllItems(' ', null)}
1243
1342
  function computeAsyncComponents(comps) {
1244
1343
  return new Set(comps.filter((c) => c.isAsync).map((c) => c.name));
1245
1344
  }
1246
- function generateComponent(comp, importedNames = new Set(), hydrate = false, asyncComps = new Set()) {
1345
+ function generateComponent(comp, importedNames = new Set(), hydrate = false, asyncComps = new Set(), linkNames = new Set()) {
1247
1346
  const tracked = collectTrackedNames(comp.body);
1248
1347
  const ctx = new Ctx();
1249
1348
  ctx.importedNames = importedNames;
1349
+ ctx.linkNames = linkNames;
1250
1350
  ctx.hydrate = hydrate;
1251
1351
  ctx.asyncComps = asyncComps;
1252
1352
  ctx.isAsyncScope = comp.isAsync || asyncComps.has(comp.name);
@@ -1275,7 +1375,7 @@ function generateComponent(comp, importedNames = new Set(), hydrate = false, asy
1275
1375
  const v = emitNode(ctx, node, tracked, null);
1276
1376
  if (v) {
1277
1377
  if (ctx.hydrate) {
1278
- ctx.push(indent(`if (${v}.parentNode !== $root) $root.appendChild(${v});`));
1378
+ ctx.push(indent(`if (${v}.parentNode !== $root) { if (!$root || ${v}.parentNode == null || !$root.contains(${v})) $root.appendChild(${v}); }`));
1279
1379
  }
1280
1380
  else {
1281
1381
  ctx.push(indent(`$root.appendChild(${v});`));
@@ -1306,6 +1406,16 @@ function buildComponentMap(irRoot, hydrate = false) {
1306
1406
  const mapLines = [];
1307
1407
  mapLines.push(`const __components = {};`);
1308
1408
  const asyncComps = computeAsyncComponents(irRoot.components);
1409
+ // Wipe-style router components (Link/NavLink) replace their SSR children on
1410
+ // hydration, so their children fragments must be rebuilt in full. Track the
1411
+ // file-local binding names regardless of aliasing (`const Nav = NavLink`).
1412
+ const linkNames = new Set();
1413
+ for (const imp of irRoot.imports) {
1414
+ for (const pair of importBindingPairs(imp)) {
1415
+ if (pair.imported === 'Link' || pair.imported === 'NavLink')
1416
+ linkNames.add(pair.local);
1417
+ }
1418
+ }
1309
1419
  // Top-level value bindings (`const MyIcon = Cpu`) live in module scope
1310
1420
  // alongside the component map, so a same-named JSX tag must invoke that
1311
1421
  // in-scope value directly — not a registry lookup. File-defined components
@@ -1319,7 +1429,7 @@ function buildComponentMap(irRoot, hydrate = false) {
1319
1429
  mapLines.push(`__components[${JSON.stringify(comp.name)}] = ${stub};`);
1320
1430
  continue;
1321
1431
  }
1322
- const code = generateComponent(comp, directNames, hydrate, asyncComps);
1432
+ const code = generateComponent(comp, directNames, hydrate, asyncComps, linkNames);
1323
1433
  mapLines.push(`__components[${JSON.stringify(comp.name)}] = ${code};`);
1324
1434
  }
1325
1435
  mapLines.push(`function __cleanup(start, end) {`);
@@ -1 +1 @@
1
- {"version":3,"file":"ir-generator.d.ts","sourceRoot":"","sources":["../src/ir-generator.ts"],"names":[],"mappings":"AACA,OAAO,EACL,MAAM,EAoBP,MAAM,uBAAuB,CAAC;AA0L/B,wBAAgB,YAAY,CAAC,MAAM,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,GAAG,CAAC;IAAC,cAAc,CAAC,EAAE,GAAG,CAAC;IAAC,UAAU,CAAC,EAAE,GAAG,EAAE,CAAA;CAAE,CAAC,GAAG,SAAS,GAAG,IAAI,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAkCnL;AAg0BD,wBAAgB,UAAU,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAoO9E"}
1
+ {"version":3,"file":"ir-generator.d.ts","sourceRoot":"","sources":["../src/ir-generator.ts"],"names":[],"mappings":"AACA,OAAO,EACL,MAAM,EAoBP,MAAM,uBAAuB,CAAC;AA0L/B,wBAAgB,YAAY,CAAC,MAAM,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,GAAG,CAAC;IAAC,cAAc,CAAC,EAAE,GAAG,CAAC;IAAC,UAAU,CAAC,EAAE,GAAG,EAAE,CAAA;CAAE,CAAC,GAAG,SAAS,GAAG,IAAI,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAkCnL;AAg0BD,wBAAgB,UAAU,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CA6P9E"}
@@ -1277,6 +1277,7 @@ export function generateIR(ast, source, filename) {
1277
1277
  'createContext',
1278
1278
  'redirect', 'permanentRedirect', 'notFound', 'NotFoundError',
1279
1279
  'createResource', 'getAction', 'validateActionInput', 'issuesToFieldMap', 'isFormAction',
1280
+ 'Show', 'For', 'Switch', 'Match',
1280
1281
  ];
1281
1282
  const usedFunctions = new Set();
1282
1283
  const addUsedFrom = (code) => {
@@ -1297,6 +1298,36 @@ export function generateIR(ast, source, filename) {
1297
1298
  if (node instanceof RuntimeStatement && node.raw) {
1298
1299
  addUsedFrom(node.raw);
1299
1300
  }
1301
+ if (node instanceof TrackDecl && node.init) {
1302
+ addUsedFrom(node.init);
1303
+ }
1304
+ if (node instanceof ForLoop) {
1305
+ if (node.init)
1306
+ addUsedFrom(node.init);
1307
+ if (node.update)
1308
+ addUsedFrom(node.update);
1309
+ if (node.condition && node.condition.raw)
1310
+ addUsedFrom(node.condition.raw);
1311
+ scanForAutoImport(node.bodyTemplate);
1312
+ }
1313
+ if (node instanceof WhileLoop) {
1314
+ if (node.condition && node.condition.raw)
1315
+ addUsedFrom(node.condition.raw);
1316
+ scanForAutoImport(node.bodyTemplate);
1317
+ }
1318
+ if (node instanceof SwitchBlock) {
1319
+ if (node.discriminant && node.discriminant.raw)
1320
+ addUsedFrom(node.discriminant.raw);
1321
+ for (const c of node.cases) {
1322
+ if (c.test && c.test.raw)
1323
+ addUsedFrom(c.test.raw);
1324
+ scanForAutoImport(c.body);
1325
+ }
1326
+ }
1327
+ if (node instanceof TryCatch) {
1328
+ scanForAutoImport(node.bodyTemplate);
1329
+ scanForAutoImport(node.catchBody);
1330
+ }
1300
1331
  if (node instanceof ComponentCall) {
1301
1332
  if (autoImportable.includes(node.componentName)) {
1302
1333
  usedFunctions.add(node.componentName);
@@ -1 +1 @@
1
- {"version":3,"file":"module-imports.d.ts","sourceRoot":"","sources":["../src/module-imports.ts"],"names":[],"mappings":"AAmGA;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,KAAK,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAgC1F;AAED,yEAAyE;AACzE,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAQpE;AAED,0FAA0F;AAC1F,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAIvD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,UAAU,EAAE,MAAM,EAAE,EACpB,UAAU,EAAE,MAAM,GAAG,SAAS,GAC7B,IAAI,CAuBN;AAiBD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAqD7E;AAqJD;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAyBlF;AAoJD,kFAAkF;AAClF,wBAAgB,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,MAAM,CAiGxF"}
1
+ {"version":3,"file":"module-imports.d.ts","sourceRoot":"","sources":["../src/module-imports.ts"],"names":[],"mappings":"AAmGA;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,KAAK,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAgC1F;AAED,yEAAyE;AACzE,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAQpE;AAED,0FAA0F;AAC1F,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAIvD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,UAAU,EAAE,MAAM,EAAE,EACpB,UAAU,EAAE,MAAM,GAAG,SAAS,GAC7B,IAAI,CAuBN;AAiBD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAiE7E;AA0WD;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CA8BlF;AAoJD,kFAAkF;AAClF,wBAAgB,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,MAAM,CAiGxF"}
@@ -238,13 +238,26 @@ export function loadSsrModule(absPath) {
238
238
  }
239
239
  }
240
240
  else {
241
- exportsObj = {};
242
- EVALUATING.set(absPath, exportsObj);
243
- const ran = evaluateModuleFile(absPath, exportsObj);
244
- // Runtime evaluation failure (already warned) — give Node's own loader
245
- // a chance before giving up.
246
- if (!ran)
247
- exportsObj = null;
241
+ // Pure re-export barrels (`export { x as y } from`, `export * from`,
242
+ // `export * as ns from`) return a lazy Proxy: only the submodule backing
243
+ // a name the consumer actually touches gets evaluated. Without this, a
244
+ // several-thousand-export barrel (e.g. lucide's icon index) pays its full
245
+ // transitive closure on every cold load (tens of seconds dev, too slow to
246
+ // ship in prod).
247
+ const src = readSsrSource(absPath);
248
+ const body = (src?.ast?.body || []);
249
+ if (src && isPureReexportBarrel(body)) {
250
+ exportsObj = createLazyBarrelExports(buildBarrelSpec(body), dirname(absPath));
251
+ }
252
+ else {
253
+ exportsObj = {};
254
+ EVALUATING.set(absPath, exportsObj);
255
+ const ran = evaluateModuleFile(absPath, exportsObj, src);
256
+ // Runtime evaluation failure (already warned) — give Node's own loader
257
+ // a chance before giving up.
258
+ if (!ran)
259
+ exportsObj = null;
260
+ }
248
261
  }
249
262
  if (exportsObj === null) {
250
263
  exportsObj = nativeRequireFallback(absPath);
@@ -287,19 +300,14 @@ function depsFresh(cachedVal) {
287
300
  }
288
301
  return true;
289
302
  }
290
- /**
291
- * Runs one module file. Returns `true` when the body executed, `false` when a
292
- * runtime failure was already warned about. Unsupported ESM constructs throw —
293
- * the only honest outcome is a loud, specific error, never silently undefined.
294
- */
295
- function evaluateModuleFile(absPath, exportsObj) {
303
+ function readSsrSource(absPath) {
296
304
  let raw;
297
305
  try {
298
306
  raw = readFileSync(absPath, 'utf-8');
299
307
  }
300
308
  catch (err) {
301
309
  console.warn(`[vesk] SSR: failed to read ${absPath}: ${err?.message ?? String(err)}`);
302
- return false;
310
+ return null;
303
311
  }
304
312
  let ast = null;
305
313
  try {
@@ -308,6 +316,233 @@ function evaluateModuleFile(absPath, exportsObj) {
308
316
  catch {
309
317
  ast = null;
310
318
  }
319
+ return { raw, ast };
320
+ }
321
+ /** True when every statement in a module body is a re-export (or a type-only /
322
+ * directive statement). Such "pure barrels" carry no computation of their own,
323
+ * so they can be served lazily instead of paying their full transitive closure. */
324
+ function isPureReexportBarrel(body) {
325
+ for (const stmt of body) {
326
+ if (stmt.type === 'EmptyStatement')
327
+ continue;
328
+ if (stmt.type === 'ExpressionStatement') {
329
+ // A leading directive such as 'use strict' — no runtime effect here.
330
+ const expr = stmt.expression;
331
+ if (expr && expr.type === 'Literal' && typeof expr.value === 'string' && expr.value.length > 0)
332
+ continue;
333
+ return false;
334
+ }
335
+ if (stmt.type === 'ExportAllDeclaration') {
336
+ if (stmt.exportKind === 'type')
337
+ continue;
338
+ continue;
339
+ }
340
+ if (stmt.type === 'ExportNamedDeclaration') {
341
+ if (stmt.exportKind === 'type')
342
+ continue;
343
+ if (stmt.source)
344
+ continue; // `export { x } from 'm'` re-export
345
+ return false; // local declaration or local re-export — real code
346
+ }
347
+ if (stmt.type === 'ImportDeclaration' && stmt.importKind === 'type')
348
+ continue;
349
+ return false;
350
+ }
351
+ return true;
352
+ }
353
+ function buildBarrelSpec(body) {
354
+ const spec = { direct: new Map(), namespaces: new Map(), stars: [] };
355
+ for (const stmt of body) {
356
+ if (stmt.type === 'ExportNamedDeclaration') {
357
+ if (stmt.exportKind === 'type' || !stmt.source)
358
+ continue;
359
+ // The module source is a string literal — read its value directly instead
360
+ // of reprinting the node (esrap print per statement is the dominant cost
361
+ // in multi-thousand-export barrels).
362
+ const source = quotedSourceValue(stmt.source);
363
+ const specifiers = stmt.specifiers || [];
364
+ for (const ex of specifiers) {
365
+ if (ex.exportKind === 'type')
366
+ continue;
367
+ const imported = exportKeyName(ex.local);
368
+ const exported = exportKeyName(ex.exported);
369
+ if (!exported || !imported)
370
+ continue;
371
+ if (source !== null)
372
+ spec.direct.set(exported, { source, imported });
373
+ }
374
+ }
375
+ else if (stmt.type === 'ExportAllDeclaration') {
376
+ if (stmt.exportKind === 'type')
377
+ continue;
378
+ const source = quotedSourceValue(stmt.source);
379
+ if (stmt.exported) {
380
+ const nsName = exportKeyName(stmt.exported);
381
+ if (source !== null && nsName)
382
+ spec.namespaces.set(nsName, source);
383
+ }
384
+ else if (source !== null) {
385
+ spec.stars.push(source);
386
+ }
387
+ }
388
+ }
389
+ return spec;
390
+ }
391
+ /**
392
+ * Raw string value of an `... from <literal>` source node. Returns the unquoted
393
+ * specifier, or `null` when the node isn't a plain string literal (callers fall
394
+ * back — and caching the string is safer than reprinting per statement).
395
+ */
396
+ /**
397
+ * Raw string value of an `... from <literal>` source node. String literals are
398
+ * read directly (esrap printing per statement is the dominant cost in
399
+ * multi-thousand-export barrels); any other node falls back to a reprinted,
400
+ * quote-stripped value so no export is ever dropped.
401
+ */
402
+ function quotedSourceValue(node) {
403
+ const n = node;
404
+ if (n && (n.type === 'Literal' || n.type === 'StringLiteral') && typeof n.value === 'string') {
405
+ return n.value;
406
+ }
407
+ const printed = printNode(node);
408
+ return printed ? stripOuterQuotes(printed) : null;
409
+ }
410
+ /**
411
+ * A lazy exports object for a pure re-export barrel. Names resolve on access;
412
+ * the submodule backing them is loaded (and cached) only then. Barrels load
413
+ * with an empty dependency closure, so editing one re-exported module
414
+ * invalidates that module alone — never the whole barrel (a several-thousand
415
+ * file stat walk otherwise).
416
+ */
417
+ function createLazyBarrelExports(spec, dir) {
418
+ // Resolved submodule TARGETS are cached, but their values are pulled through
419
+ // `loadSsrModule` on every access — that call is mtime-keyed per module, so
420
+ // editing a re-exported file invalidates exactly that module (and is cheap:
421
+ // one stat), while the barrel itself stays cached with no dependency walk.
422
+ const targets = new Map();
423
+ const starKeys = new Set();
424
+ let starsEnumerated = false;
425
+ const loadSource = (source) => {
426
+ let target = targets.get(source);
427
+ if (!target) {
428
+ target = resolveSsrModule(source, dir) ?? '';
429
+ if (!target)
430
+ return null;
431
+ targets.set(source, target);
432
+ }
433
+ const mod = loadSsrModule(target);
434
+ return mod && typeof mod === 'object' ? mod : null;
435
+ };
436
+ const hasPresent = (prop) => {
437
+ if (spec.direct.has(prop) || spec.namespaces.has(prop))
438
+ return true;
439
+ if (starsEnumerated && starKeys.has(prop))
440
+ return true;
441
+ if (prop === 'default')
442
+ return false; // `export *` never re-exports default
443
+ for (const source of spec.stars) {
444
+ const mod = loadSource(source);
445
+ if (mod && Object.prototype.hasOwnProperty.call(mod, prop))
446
+ return true;
447
+ }
448
+ return false;
449
+ };
450
+ const getNamed = (prop) => {
451
+ const direct = spec.direct.get(prop);
452
+ if (direct) {
453
+ const mod = loadSource(direct.source);
454
+ if (!mod)
455
+ return undefined;
456
+ return mod[direct.imported];
457
+ }
458
+ const nsSource = spec.namespaces.get(prop);
459
+ if (nsSource) {
460
+ return loadSource(nsSource) ?? undefined;
461
+ }
462
+ if (prop === 'default')
463
+ return undefined;
464
+ for (const source of spec.stars) {
465
+ const mod = loadSource(source);
466
+ if (mod && Object.prototype.hasOwnProperty.call(mod, prop)) {
467
+ return mod[prop];
468
+ }
469
+ }
470
+ return undefined;
471
+ };
472
+ return new Proxy(Object.create(null), {
473
+ get(_target, prop) {
474
+ if (typeof prop === 'symbol')
475
+ return undefined;
476
+ return getNamed(prop);
477
+ },
478
+ has(_target, prop) {
479
+ if (typeof prop === 'symbol')
480
+ return false;
481
+ return hasPresent(prop);
482
+ },
483
+ ownKeys() {
484
+ if (!starsEnumerated) {
485
+ for (const source of spec.stars) {
486
+ const mod = loadSource(source);
487
+ if (!mod)
488
+ continue;
489
+ for (const k of Reflect.ownKeys(mod)) {
490
+ if (typeof k === 'string' && k !== 'default')
491
+ starKeys.add(k);
492
+ }
493
+ }
494
+ starsEnumerated = true;
495
+ }
496
+ return [...new Set([...spec.direct.keys(), ...spec.namespaces.keys(), ...starKeys])];
497
+ },
498
+ getOwnPropertyDescriptor(_target, prop) {
499
+ if (typeof prop === 'symbol')
500
+ return undefined;
501
+ if (!hasPresent(prop))
502
+ return undefined;
503
+ // Accessor descriptor so iteration doesn't force-load every submodule;
504
+ // values are pulled via the getter only when a consumer reads them.
505
+ return {
506
+ enumerable: true,
507
+ configurable: true,
508
+ get: () => getNamed(prop),
509
+ };
510
+ },
511
+ });
512
+ }
513
+ /**
514
+ * Runs one module file. Returns `true` when the body executed, `false` when a
515
+ * runtime failure was already warned about. Unsupported ESM constructs throw —
516
+ * the only honest outcome is a loud, specific error, never silently undefined.
517
+ * `src` carries a read+parse already done by the caller (either a barrel probe
518
+ * or a plain load) so big files aren't read/parsed twice.
519
+ */
520
+ function evaluateModuleFile(absPath, exportsObj, src) {
521
+ let raw;
522
+ if (src) {
523
+ raw = src.raw;
524
+ }
525
+ else {
526
+ try {
527
+ raw = readFileSync(absPath, 'utf-8');
528
+ }
529
+ catch (err) {
530
+ console.warn(`[vesk] SSR: failed to read ${absPath}: ${err?.message ?? String(err)}`);
531
+ return false;
532
+ }
533
+ }
534
+ let ast;
535
+ if (src) {
536
+ ast = src.ast;
537
+ }
538
+ else {
539
+ try {
540
+ ast = parse(raw, { filename: absPath });
541
+ }
542
+ catch {
543
+ ast = null;
544
+ }
545
+ }
311
546
  if (!ast) {
312
547
  // Not parseable as ESM — run verbatim as CJS.
313
548
  try {
@@ -421,7 +656,13 @@ function createModuleRequire(fromDir) {
421
656
  */
422
657
  export function resolveSsrModule(specifier, fromDir) {
423
658
  let resolved = null;
424
- if (specifier === '.' || specifier === '..' || isAbsolute(specifier)) {
659
+ if (specifier === '.' || specifier === '..') {
660
+ // Literal `.`/`..` are relative — anchor them to the importing file's
661
+ // directory like every other relative specifier (a bare resolve()
662
+ // against process.cwd() silently resolved the wrong directory).
663
+ resolved = probeFile(resolve(fromDir, specifier));
664
+ }
665
+ else if (isAbsolute(specifier)) {
425
666
  resolved = probeFile(resolve(specifier));
426
667
  }
427
668
  else if (specifier.startsWith('./') || specifier.startsWith('../')) {
@@ -1 +1 @@
1
- {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAMtC,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,YAAY,CAAC;IACnB,8CAA8C;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,gEAAgE;IAChE,WAAW,EAAE,MAAM,CAAC;IACpB,8CAA8C;IAC9C,SAAS,EAAE,MAAM,CAAC;IAClB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B,sDAAsD;IACtD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAYD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,cAAc,EAAE,CAAA;CAAE,CA0KpG;AAED,wBAAgB,gBAAgB,IAAI,OAAO,KAAK,CAAC,MAAM,CAEtD;AAED,wBAAgB,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB,GAAG,OAAO,CA8CzE"}
1
+ {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAMtC,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,YAAY,CAAC;IACnB,8CAA8C;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,gEAAgE;IAChE,WAAW,EAAE,MAAM,CAAC;IACpB,8CAA8C;IAC9C,SAAS,EAAE,MAAM,CAAC;IAClB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B,sDAAsD;IACtD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA0BD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,cAAc,EAAE,CAAA;CAAE,CA4LpG;AAED,wBAAgB,gBAAgB,IAAI,OAAO,KAAK,CAAC,MAAM,CAEtD;AAED,wBAAgB,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB,GAAG,OAAO,CA8CzE"}
package/dist/parser.js CHANGED
@@ -9,6 +9,20 @@ function isIdentChar(code) {
9
9
  function isWhitespaceChar(ch) {
10
10
  return ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r' || ch === '\f' || ch === '\v' || ch === '\u00a0' || ch === '\ufeff';
11
11
  }
12
+ function offsetToLineCol(source, offset) {
13
+ let line = 1;
14
+ let column = 1;
15
+ for (let i = 0; i < offset && i < source.length; i++) {
16
+ if (source.charCodeAt(i) === 10) {
17
+ line++;
18
+ column = 1;
19
+ }
20
+ else {
21
+ column++;
22
+ }
23
+ }
24
+ return { line, column };
25
+ }
12
26
  /**
13
27
  * Scans the source and blanks `; key <expr>` / `; index <ident>` clauses
14
28
  * found in `for (...)` headers, returning the rewritten code (same length
@@ -181,6 +195,21 @@ export function preprocessForClauses(source) {
181
195
  k++;
182
196
  const expr = source.slice(k, end).trim();
183
197
  if (expr) {
198
+ if (expr[0] === ':') {
199
+ // `; key : <expr>` / `; key: <expr>` (colon form) is not valid
200
+ // Vesk: the clause is `; key <expr>`, and passing the colon
201
+ // through used to emit a broken reconciliation key.
202
+ const { line, column } = offsetToLineCol(source, start);
203
+ throw new VeskError(`Invalid \`; key\` clause — write the key expression after \`key\` without a colon: \`key ${expr.slice(1).trim()}\`.`, {
204
+ line,
205
+ column,
206
+ code: codeFrame(source, line, column),
207
+ nextSteps: [
208
+ 'Use `; key <expr>` (no colon), e.g. `for (const item of items; key item.id)`.',
209
+ 'For the iteration index use `; index <name>`, e.g. `for (const item of items; key item.id; index i)`.',
210
+ ],
211
+ });
212
+ }
184
213
  keyRange = [k, start + clause.length];
185
214
  clauseCode = ' '.repeat(clause.length);
186
215
  }
@@ -1 +1 @@
1
- {"version":3,"file":"server-jsgen.d.ts","sourceRoot":"","sources":["../src/server-jsgen.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAOzE,OAAO,EAA4E,KAAK,WAAW,EAAE,MAAM,mCAAmC,CAAC;AAW/I,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,EAAE,OAAO,GAAE,OAAe,EAAE,OAAO,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,GAAG,MAAM,CA8CjJ;AAyTD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,WAAW,EAAE,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,MAAM,CAmD1F;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,OAAO,GAAG,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,CAkChG"}
1
+ {"version":3,"file":"server-jsgen.d.ts","sourceRoot":"","sources":["../src/server-jsgen.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAOzE,OAAO,EAAkG,KAAK,WAAW,EAAE,MAAM,mCAAmC,CAAC;AAWrK,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,EAAE,OAAO,GAAE,OAAe,EAAE,OAAO,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,GAAG,MAAM,CAmDjJ;AAiUD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,WAAW,EAAE,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,MAAM,CAmD1F;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,OAAO,GAAG,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,CAkChG"}
@@ -1,5 +1,5 @@
1
1
  import { StaticNode, TextNode, DynamicBinding, OpaqueDynamicRegion, MapRegion, WhileLoop, SwitchBlock, TryCatch, ForLoop, TrackDecl, RuntimeStatement, ComponentRef, ComponentCall, ServerBlock, ClientBlock, HeadBlock, SlotNode, } from '@vesk/compiler/src/ir';
2
- import { isStaticIR, collectTrackedNames, transformTracked, semicolonizeStatement } from '@vesk/compiler/src/client-codegen';
2
+ import { isStaticIR, collectTrackedNames, transformTracked, transformTrackedInit, semicolonizeStatement } from '@vesk/compiler/src/client-codegen';
3
3
  import { unwrapTrackCall, stripTrackGeneric, hasTopLevelComma, skipWhitespace, findBalancedEnd, startsWithIdentifier } from '@vesk/compiler/src/scan';
4
4
  import { escapeHtml, indent, exprJS, extractTopLevelNames, extractRuntimeNames, buildParamInit, __vskHydrate, __vskImportedNames, setVskImportedNames, setVskForceClaim, takeVskForceClaim, nextVskId, } from '@vesk/compiler/src/server-utils';
5
5
  import { localValueImportNames } from '@vesk/compiler/src/module-imports';
@@ -45,8 +45,13 @@ export function irNodeToJS(node, importedNames, isAsync = false, tracked) {
45
45
  return forLoopToJS(node, isAsync, tracked);
46
46
  if (node instanceof TrackDecl) {
47
47
  const cellName = node.rawName || node.name;
48
- const unwrapped = unwrapTrackCall(node.init);
49
- const inner = hasTopLevelComma(unwrapped) ? stripTrackGeneric(node.init) : unwrapped;
48
+ // Tracked reads inside a `track`/`derived` init are re-emitted as `get(…)`
49
+ // (matching client codegen) so a function init like
50
+ // `track(() => x * 2)` / `derived(() => props.tabs[x])` computes against the
51
+ // cell VALUES server-side instead of the raw cell objects.
52
+ const init = transformTrackedInit(node.init, tracked || new Map());
53
+ const unwrapped = unwrapTrackCall(init);
54
+ const inner = hasTopLevelComma(unwrapped) ? stripTrackGeneric(init) : unwrapped;
50
55
  const key = JSON.stringify(`${compKey(node)}:${node.name}`);
51
56
  return [
52
57
  `const ${cellName} = (() => {`,
@@ -316,16 +321,24 @@ function forLoopToJS(node, isAsync = false, tracked) {
316
321
  lines.push(`}`);
317
322
  }
318
323
  else {
319
- if (node.init)
320
- lines.push(`${node.init}`);
321
- lines.push(`while (${exprJSX(node.condition, tracked)}) {`);
324
+ // Emit a real `for` statement (matching the client codegen): a `let`/`const`
325
+ // declared in the init clause gets a fresh binding per iteration, so closures
326
+ // created in the body capture that iteration's value rather than the loop
327
+ // variable's final value after the last pass.
328
+ let initPart = (node.init || '').trim();
329
+ let updatePart = (node.update || '').trim();
330
+ if (initPart.endsWith(';'))
331
+ initPart = initPart.slice(0, -1).trim();
332
+ if (updatePart.endsWith(';'))
333
+ updatePart = updatePart.slice(0, -1).trim();
334
+ const cond = exprJSX(node.condition, tracked);
335
+ const head = initPart ? `${initPart}; ${cond}` : `; ${cond}`;
336
+ lines.push(`for (${head}; ${updatePart}) {`);
322
337
  for (const n of node.bodyTemplate) {
323
338
  const code = irNodeToJS(n, null, isAsync, tracked);
324
339
  if (code)
325
340
  lines.push(indent(code));
326
341
  }
327
- if (node.update)
328
- lines.push(indent(`${node.update}`));
329
342
  lines.push(`}`);
330
343
  }
331
344
  return lines.join('\n');
@@ -1 +1 @@
1
- {"version":3,"file":"vesk-plugin.d.ts","sourceRoot":"","sources":["../src/vesk-plugin.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"vesk-plugin.d.ts","sourceRoot":"","sources":["../src/vesk-plugin.ts"],"names":[],"mappings":"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"}
@@ -463,16 +463,32 @@ export function VeskParserPlugin(config = {}) {
463
463
  }
464
464
  if (this.#componentDepth > 0 && code === 60 && this.#isBlockContext()) {
465
465
  const next = this.input.charCodeAt(this.pos + 1);
466
- if (next === 47 || (next >= 65 && next <= 90) || (next >= 97 && next <= 122)) {
467
- const startsNewStatement = this.hasPrecedingLineBreak();
468
- const inType = this.inType;
469
- const forceJsx = () => {
470
- const savedExprAllowed = this.exprAllowed;
471
- this.exprAllowed = true;
472
- const result = super.readToken(code);
473
- this.exprAllowed = savedExprAllowed;
474
- return result;
475
- };
466
+ const startsNewStatement = this.hasPrecedingLineBreak();
467
+ const inType = this.inType;
468
+ const forceJsx = () => {
469
+ const savedExprAllowed = this.exprAllowed;
470
+ this.exprAllowed = true;
471
+ const result = super.readToken(code);
472
+ this.exprAllowed = savedExprAllowed;
473
+ return result;
474
+ };
475
+ const prev = this.type;
476
+ const canEndExpr = prev === tt.name || prev === tt.num || prev === tt.string || prev === tt.regexp ||
477
+ prev === tt.bracketR || prev === tt.backQuote || prev === tt.template ||
478
+ prev === tt._this || prev === tt._super || prev === tt._true || prev === tt._false ||
479
+ prev === tt._null || prev === tt.jsxTagEnd;
480
+ // A bare `<>` fragment opening a statement (rather than a
481
+ // relational/type token) is JSX. It is only safe to force when it
482
+ // follows a line break — e.g. a semicolon-less declaration such as
483
+ // `const &[x] = track(...)` in statement mode, where plain
484
+ // tokenisation emits a `<`/`>` pair. No expression can end in `<>`,
485
+ // so a line break alone is sufficient to disambiguate.
486
+ const fragmentStart = !inType && next === 62 && startsNewStatement;
487
+ if (next === 47 || fragmentStart || (next >= 65 && next <= 90) || (next >= 97 && next <= 122)) {
488
+ if (fragmentStart) {
489
+ this.#jsxStartsStatement = true;
490
+ return forceJsx();
491
+ }
476
492
  if (inType) {
477
493
  if (startsNewStatement) {
478
494
  if (looksLikeGenericArrowAt(this.input, this.pos) || looksLikeTypeAssertionAt(this.input, this.pos)) {
@@ -485,11 +501,6 @@ export function VeskParserPlugin(config = {}) {
485
501
  }
486
502
  }
487
503
  else {
488
- const prev = this.type;
489
- const canEndExpr = prev === tt.name || prev === tt.num || prev === tt.string || prev === tt.regexp ||
490
- prev === tt.bracketR || prev === tt.backQuote || prev === tt.template ||
491
- prev === tt._this || prev === tt._super || prev === tt._true || prev === tt._false ||
492
- prev === tt._null || prev === tt.jsxTagEnd;
493
504
  if (!canEndExpr || startsNewStatement) {
494
505
  if (looksLikeGenericArrowAt(this.input, this.pos) || looksLikeTypeAssertionAt(this.input, this.pos)) {
495
506
  this.#jsxStartsStatement = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vesk/compiler",
3
- "version": "0.2.16",
3
+ "version": "0.2.18",
4
4
  "type": "module",
5
5
  "description": "Vesk compiler — lexer, parser, semantic analysis, IR, codegen",
6
6
  "main": "./dist/index.js",
@@ -20,7 +20,7 @@
20
20
  },
21
21
  "dependencies": {
22
22
  "@sveltejs/acorn-typescript": "^1.0.0",
23
- "@vesk/types": "^0.2.16",
23
+ "@vesk/types": "^0.2.18",
24
24
  "acorn": "^8.15.0",
25
25
  "esrap": "^2.3.0",
26
26
  "is-reference": "^3.0.0",