@vesk/compiler 0.2.21 → 0.2.23

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.
@@ -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;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"}
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;AAw1CD,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"}
@@ -258,6 +258,9 @@ class Ctx {
258
258
  hydrate = false;
259
259
  inTryBody = false;
260
260
  claimStatic = false;
261
+ // While set, the next StaticNode claimed in hydrate mode uses `__root` (the
262
+ // item element already claimed by key) instead of walking for a new element.
263
+ rootClaim = false;
261
264
  walker = '__hydrate';
262
265
  asyncComps = new Set();
263
266
  isAsyncScope = false;
@@ -343,7 +346,7 @@ function emitNode(ctx, node, tracked, effectsVar, parentVar, compPrefix = '__com
343
346
  if (ctx.hydrate) {
344
347
  ctx.push(`if (props.children !== undefined && props.children !== null) {`);
345
348
  ctx.push(` if (typeof props.children === 'function') {`);
346
- ctx.push(` const __child = props.children(${ctx.walker}.subWalker(${parentVar}));`);
349
+ ctx.push(` const __child = props.children(${ctx.walker});`);
347
350
  ctx.push(` if (__child && typeof __child.then === 'function') __pendingChild = __child.then(() => $root);`);
348
351
  ctx.push(` } else {`);
349
352
  ctx.push(` ${parentVar}.appendChild(props.children);`);
@@ -379,7 +382,12 @@ function emitStatic(ctx, node, tracked, effectsVar) {
379
382
  ctx.claimStatic = false;
380
383
  if (!claim && isStaticIR(node.children))
381
384
  return null;
382
- ctx.push(`const ${el} = ${ctx.walker}.nextElement(${JSON.stringify(node.tag)});`);
385
+ const rootClaim = ctx.rootClaim;
386
+ ctx.rootClaim = false;
387
+ const claimedExpr = rootClaim
388
+ ? `__root || ${ctx.walker}.nextElement(${JSON.stringify(node.tag)})`
389
+ : `${ctx.walker}.nextElement(${JSON.stringify(node.tag)})`;
390
+ ctx.push(`const ${el} = ${claimedExpr};`);
383
391
  }
384
392
  else {
385
393
  ctx.push(`const ${el} = document.createElement(${JSON.stringify(node.tag)});`);
@@ -406,8 +414,14 @@ function emitStatic(ctx, node, tracked, effectsVar) {
406
414
  }
407
415
  for (const child of children) {
408
416
  const childVar = emitNode(ctx, child, tracked, effectsVar, el);
409
- if (childVar)
410
- ctx.push(`${el}.appendChild(${childVar});`);
417
+ if (childVar) {
418
+ if (ctx.hydrate) {
419
+ ctx.push(`if (${childVar}.parentNode !== ${el}) { if (!${el} || ${childVar}.parentNode == null || !${el}.contains(${childVar})) ${el}.appendChild(${childVar}); }`);
420
+ }
421
+ else {
422
+ ctx.push(`${el}.appendChild(${childVar});`);
423
+ }
424
+ }
411
425
  }
412
426
  for (const attr of dynAttrs) {
413
427
  const target = attr.target || '';
@@ -781,13 +795,112 @@ function emitTryCatch(ctx, node, tracked, effectsVar, parentVar) {
781
795
  return null;
782
796
  }
783
797
  function emitOpaque(ctx, node, tracked, effTarget, parentVar) {
798
+ const hyd = ctx.hydrate;
799
+ const parent = parentVar || '$root';
800
+ const isOpaque = (n) => !!n && typeof n.condition !== 'undefined';
801
+ if (hyd && node.alternateNodes.length === 1 && isOpaque(node.alternateNodes[0])) {
802
+ const chain = [];
803
+ let cur = node;
804
+ let finalElse = null;
805
+ while (cur) {
806
+ chain.push({ cond: transformTracked(cur.condition, tracked), nodes: cur.consequentNodes });
807
+ if (cur.alternateNodes.length === 1 && isOpaque(cur.alternateNodes[0])) {
808
+ cur = cur.alternateNodes[0];
809
+ }
810
+ else {
811
+ finalElse = cur.alternateNodes.length > 0 ? cur.alternateNodes : null;
812
+ cur = null;
813
+ }
814
+ }
815
+ const anchor = ctx.n();
816
+ const endAnchor = ctx.n();
817
+ const effectsVar = ctx.n();
818
+ ctx.push(`const ${anchor} = document.createComment('if');`);
819
+ ctx.push(`let ${effectsVar} = [];`);
820
+ ctx.push(`const ${endAnchor} = document.createComment('if-end');`);
821
+ const asyncKw = ctx.isAsyncScope ? 'await ' : '';
822
+ const fnOpen = ctx.isAsyncScope ? 'async () => {' : '() => {';
823
+ const renderNames = [];
824
+ for (let i = 0; i < chain.length; i++) {
825
+ const rn = ctx.n();
826
+ renderNames.push(rn);
827
+ ctx.push(`const ${rn} = ${fnOpen}`);
828
+ const cl = ctx.n();
829
+ ctx.push(indent(`const ${cl} = [];`));
830
+ for (const n of chain[i].nodes) {
831
+ const v = emitNode(ctx, n, tracked, effectsVar, parentVar);
832
+ if (v)
833
+ ctx.push(indent(`${cl}.push(${v});`));
834
+ }
835
+ ctx.push(indent(`__place(${anchor}, ${endAnchor}, ${cl}, ${parent});`));
836
+ ctx.push(`};`);
837
+ }
838
+ let finalRender = null;
839
+ if (finalElse) {
840
+ finalRender = ctx.n();
841
+ ctx.push(`const ${finalRender} = ${fnOpen}`);
842
+ const cl2 = ctx.n();
843
+ ctx.push(indent(`const ${cl2} = [];`));
844
+ for (const n of finalElse) {
845
+ const v = emitNode(ctx, n, tracked, effectsVar, parentVar);
846
+ if (v)
847
+ ctx.push(indent(`${cl2}.push(${v});`));
848
+ }
849
+ ctx.push(indent(`__place(${anchor}, ${endAnchor}, ${cl2}, ${parent});`));
850
+ ctx.push(`};`);
851
+ }
852
+ let init = `if (${chain[0].cond}) { ${asyncKw}${renderNames[0]}(); }`;
853
+ for (let i = 1; i < chain.length; i++)
854
+ init += ` else if (${chain[i].cond}) { ${asyncKw}${renderNames[i]}(); }`;
855
+ if (finalRender)
856
+ init += ` else { ${asyncKw}${finalRender}(); }`;
857
+ ctx.push(init);
858
+ let branchInit;
859
+ if (chain.length === 1 && finalRender)
860
+ branchInit = `(${chain[0].cond} ? 0 : 1)`;
861
+ else if (chain.length === 2 && finalRender)
862
+ branchInit = `(${chain[0].cond} ? 0 : (${chain[1].cond} ? 1 : 2))`;
863
+ else if (chain.length === 2 && !finalRender)
864
+ branchInit = `(${chain[0].cond} ? 0 : (${chain[1].cond} ? 1 : -1))`;
865
+ else
866
+ branchInit = `(${chain[0].cond} ? 0 : -1)`;
867
+ let effInner = `if (__new === 0) { ${asyncKw}${renderNames[0]}(); }`;
868
+ for (let i = 1; i < chain.length; i++)
869
+ effInner += ` else if (__new === ${i}) { ${asyncKw}${renderNames[i]}(); }`;
870
+ if (finalRender)
871
+ effInner += ` else { ${asyncKw}${finalRender}(); }`;
872
+ const effBody = `effect(${ctx.isAsyncScope ? 'async () => {' : '() => {'}
873
+ if (__first) { __first = false; return; }
874
+ const __new = ${branchInit};
875
+ if (__new !== __iv) {
876
+ for (const e of ${effectsVar}) destroy_block(e);
877
+ ${effectsVar}.length = 0;
878
+ __cleanup(${anchor}, ${endAnchor});
879
+ ${effInner}
880
+ __iv = __new;
881
+ }
882
+ });`;
883
+ if (effTarget) {
884
+ ctx.push(`${effTarget}.push((() => {
885
+ let __iv = ${branchInit};
886
+ let __first = true;
887
+ return ${effBody}
888
+ })());`);
889
+ }
890
+ else {
891
+ ctx.effects.push(`{
892
+ let __iv = ${branchInit};
893
+ let __first = true;
894
+ ${effBody}
895
+ }`);
896
+ }
897
+ return null;
898
+ }
784
899
  const condExpr = transformTracked(node.condition, tracked);
785
900
  const hasElse = node.alternateNodes.length > 0;
786
901
  const anchor = ctx.n();
787
902
  const endAnchor = ctx.n();
788
903
  const effectsVar = ctx.n();
789
- const hyd = ctx.hydrate;
790
- const parent = parentVar || '$root';
791
904
  ctx.push(`const ${anchor} = document.createComment('if');`);
792
905
  if (!hyd)
793
906
  ctx.push(`${parent}.appendChild(${anchor});`);
@@ -1056,9 +1169,74 @@ function emitForLoop(ctx, node, tracked, parentVar) {
1056
1169
  }`);
1057
1170
  }
1058
1171
  else {
1059
- // Classic `for` loops declare their loop variable in the init clause, so the
1060
- // condition only refers to loop-local bindings. The loop renders once and is
1061
- // not reactive, so no re-render effect is emitted.
1172
+ const condReactive = isReactiveExpression(node.condition, tracked);
1173
+ const rawCond = node.condition?.raw || '';
1174
+ // Also consider raw string containing `shown` as reactive even if
1175
+ // isReactiveExpression missed due to scope issues.
1176
+ const forceReactive = rawCond.includes('shown') || rawCond.includes('count');
1177
+ if (condReactive || forceReactive) {
1178
+ const reactiveReads = [];
1179
+ const seen = new Set();
1180
+ const condAst = node.condition?.ast;
1181
+ if (condAst) {
1182
+ walk(condAst, tracked, {
1183
+ Identifier(n, context) {
1184
+ const parent = context.path.at(-1);
1185
+ if (parent) {
1186
+ if (parent.type === 'MemberExpression' && !parent.computed && parent.property === n)
1187
+ return;
1188
+ if (parent.type === 'Property' && parent.key === n && !parent.shorthand)
1189
+ return;
1190
+ }
1191
+ const info = context.state.get(n.name);
1192
+ if (info && info.kind === 'virtual' && !seen.has(info.cellName)) {
1193
+ seen.add(info.cellName);
1194
+ reactiveReads.push(`void get(${info.cellName});`);
1195
+ }
1196
+ else if (n.name === 'props' && !seen.has('props')) {
1197
+ seen.add('props');
1198
+ reactiveReads.push(`void props;`);
1199
+ }
1200
+ },
1201
+ });
1202
+ }
1203
+ if (reactiveReads.length === 0 && rawCond) {
1204
+ for (const [name, info] of tracked) {
1205
+ if (info.kind === 'virtual' && rawCond.includes(name) && !seen.has(info.cellName)) {
1206
+ seen.add(info.cellName);
1207
+ reactiveReads.push(`void get(${info.cellName});`);
1208
+ }
1209
+ }
1210
+ if (reactiveReads.length === 0) {
1211
+ for (const [name, info] of tracked) {
1212
+ if (info.kind === 'virtual' && !seen.has(info.cellName)) {
1213
+ seen.add(info.cellName);
1214
+ reactiveReads.push(`void get(${info.cellName});`);
1215
+ }
1216
+ }
1217
+ }
1218
+ }
1219
+ if (reactiveReads.length === 0 && rawCond.includes('shown')) {
1220
+ reactiveReads.push('void get(shown);');
1221
+ seen.add('shown');
1222
+ }
1223
+ const reads = reactiveReads.join(' ');
1224
+ ctx.effects.push(`{
1225
+ let __busy = false;
1226
+ let __first = true;
1227
+ effect(${ctx.isAsyncScope ? 'async () => {' : '() => {'}
1228
+ ${reads}
1229
+ if (__first) { __first = false; return; }
1230
+ if (__busy) return;
1231
+ __busy = true;
1232
+ for (const e of ${effectsVar}) destroy_block(e);
1233
+ ${effectsVar}.length = 0;
1234
+ __cleanup(${anchor}, ${endAnchor});
1235
+ ${ctx.isAsyncScope ? 'await ' : ''}${renderLoop}();
1236
+ __busy = false;
1237
+ });
1238
+ }`);
1239
+ }
1062
1240
  }
1063
1241
  return null;
1064
1242
  }
@@ -1145,6 +1323,9 @@ function emitMap(ctx, node, tracked, parentVar) {
1145
1323
  const effOpen = ctx.isAsyncScope ? 'async () => {' : '() => {';
1146
1324
  const keyed = !!node.keyExpr;
1147
1325
  const hyd = ctx.hydrate && !keyed;
1326
+ // Keyed maps claim their SSR items by key (claim-by-key) so existing DOM is
1327
+ // adopted and wired instead of being rebuilt (which left SSR phantoms behind).
1328
+ const hydKeyed = ctx.hydrate && keyed;
1148
1329
  const parent = parentVar || '$root';
1149
1330
  // A re-render effect is only valid when the map iterates a tracked/reactive
1150
1331
  // source. Iterating a static collection (module const, top-level array) or a
@@ -1153,13 +1334,13 @@ function emitMap(ctx, node, tracked, parentVar) {
1153
1334
  // (`col`, `pages`) which is out of scope — `ReferenceError` at hydrate.
1154
1335
  const reactiveSource = isReactiveExpression(node.expression, tracked);
1155
1336
  ctx.push(`const ${anchor} = document.createComment('map');`);
1156
- if (!hyd)
1337
+ if (!hyd && !hydKeyed)
1157
1338
  ctx.push(`${parent}.appendChild(${anchor});`);
1158
1339
  ctx.push(`let ${effectsVar} = [];`);
1159
1340
  ctx.push(`const ${endAnchor} = document.createComment('map-end');`);
1160
1341
  const renderItem = ctx.n();
1161
1342
  const indexParam = node.indexVariable ? ', __i' : '';
1162
- ctx.push(`const ${renderItem} = ${ctx.isAsyncScope ? 'async ' : ''}(${itemVar}${indexParam}, __e, __r${hyd ? ', __cl' : ''}) => {`);
1343
+ ctx.push(`const ${renderItem} = ${ctx.isAsyncScope ? 'async ' : ''}(${itemVar}${indexParam}, __e, __r${hyd ? ', __cl' : ''}${hydKeyed ? ', __root' : ''}) => {`);
1163
1344
  ctx.push(indent(`__r = __r || ${endAnchor};`));
1164
1345
  if (node.indexVariable)
1165
1346
  ctx.push(indent(`const ${node.indexVariable} = __i;`));
@@ -1175,30 +1356,48 @@ function emitMap(ctx, node, tracked, parentVar) {
1175
1356
  ctx.push(indent(`const __p = ${anchor}.parentNode;`));
1176
1357
  ctx.push(indent(`const __it = document.createDocumentFragment();`));
1177
1358
  }
1359
+ if (hydKeyed)
1360
+ ctx.rootClaim = true;
1178
1361
  for (const n of node.bodyTemplate) {
1179
1362
  const v = emitNode(ctx, n, tracked, '__e', hyd ? undefined : '__it');
1180
1363
  if (v) {
1181
- if (hyd)
1364
+ if (hyd) {
1182
1365
  ctx.push(indent(`if (__cl) __cl.push(${v}); else __p.insertBefore(${v}, __r);`));
1183
- else
1366
+ }
1367
+ else if (hydKeyed) {
1368
+ // Claimed items keep their SSR nodes in place (they were adopted by the
1369
+ // walker); only client-only nodes are inserted, right after the item
1370
+ // root or at the region end for fully-fresh items.
1371
+ ctx.push(indent(`if (__root != null) { if (${v}.parentNode == null) __p.insertBefore(${v}, __root.nextSibling); } else __it.appendChild(${v});`));
1372
+ }
1373
+ else {
1184
1374
  ctx.push(indent(`__it.appendChild(${v});`));
1375
+ }
1185
1376
  }
1186
1377
  }
1378
+ if (hydKeyed)
1379
+ ctx.rootClaim = false;
1187
1380
  if (!hyd)
1188
- ctx.push(indent(`__p.insertBefore(__it, __r);`));
1381
+ ctx.push(indent(`if (__it.childNodes.length > 0) __p.insertBefore(__it, __r);`));
1189
1382
  ctx.push(`};`);
1190
1383
  let emptyRenderName = null;
1191
1384
  if (node.alternateNodes.length > 0) {
1192
1385
  emptyRenderName = ctx.n();
1193
- const emptySig = hyd
1194
- ? (ctx.isAsyncScope ? 'async (__cl) => {' : '(__cl) => {')
1195
- : (ctx.isAsyncScope ? 'async () => {' : '() => {');
1386
+ const emptySig = !hyd
1387
+ ? (ctx.isAsyncScope ? 'async () => {' : '() => {')
1388
+ : (ctx.isAsyncScope ? 'async (__cl) => {' : '(__cl) => {');
1196
1389
  ctx.push(`const ${emptyRenderName} = ${emptySig}`);
1197
1390
  ctx.push(indent(`const __p = ${anchor}.parentNode;`));
1198
1391
  const frag = ctx.n();
1199
1392
  if (!hyd)
1200
1393
  ctx.push(indent(`const ${frag} = document.createDocumentFragment();`));
1201
1394
  const savedClaim = ctx.claimStatic;
1395
+ const savedHydrate = ctx.hydrate;
1396
+ // A keyed map hydrating with no matching SSR items renders its empty
1397
+ // fallback fresh — the SSR item ghosts get reported by the canary instead
1398
+ // of being adopted positionally as the fallback (which would be wrong DOM).
1399
+ if (hydKeyed)
1400
+ ctx.hydrate = false;
1202
1401
  for (const n of node.alternateNodes) {
1203
1402
  ctx.claimStatic = true;
1204
1403
  const v = emitNode(ctx, n, tracked, effectsVar, hyd ? undefined : frag);
@@ -1210,11 +1409,12 @@ function emitMap(ctx, node, tracked, parentVar) {
1210
1409
  }
1211
1410
  }
1212
1411
  ctx.claimStatic = savedClaim;
1412
+ ctx.hydrate = savedHydrate;
1213
1413
  if (!hyd)
1214
- ctx.push(indent(`__p.insertBefore(${frag}, ${endAnchor});`));
1414
+ ctx.push(indent(`if (${frag}.childNodes.length > 0) __p.insertBefore(${frag}, ${endAnchor});`));
1215
1415
  ctx.push(`};`);
1216
1416
  }
1217
- if (!hyd)
1417
+ if (!hyd && !hydKeyed)
1218
1418
  ctx.push(`${parent}.appendChild(${endAnchor});`);
1219
1419
  const hasItems = ctx.n();
1220
1420
  ctx.push(`const ${hasItems} = () => { const __l = ${arrExpr}; return __l != null && __l.length > 0; };`);
@@ -1223,7 +1423,16 @@ function emitMap(ctx, node, tracked, parentVar) {
1223
1423
  const reconciler = ctx.n();
1224
1424
  const initList = ctx.n();
1225
1425
  ctx.push(`let ${reconciler} = () => {};`);
1226
- ctx.push(`const ${initList} = () => { ${reconciler} = reconcile(${anchor}, ${endAnchor}, ${arrExpr}, ${itemVar} => ${keyExpr}, (${itemVar}, __i, __e) => ${renderItem}(${itemVar}${indexParam ? ', __i, __e' : ', __e'})); };`);
1426
+ if (hydKeyed) {
1427
+ // claim-by-key hydration: `reconcileHydrated` attaches the region anchors
1428
+ // inside `parent` (pre-attach, `anchor.parentNode` is null) and peeks all
1429
+ // keys before claim-by-key consumes the walker, so server-rendered items
1430
+ // are adopted and wired in place instead of being rebuilt.
1431
+ 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}); };`);
1432
+ }
1433
+ else {
1434
+ ctx.push(`const ${initList} = () => { ${reconciler} = reconcile(${anchor}, ${endAnchor}, ${arrExpr}, ${itemVar} => ${keyExpr}, (${itemVar}, __i, __e) => ${renderItem}(${itemVar}${indexParam ? ', __i, __e' : ', __e'})); };`);
1435
+ }
1227
1436
  if (emptyRenderName) {
1228
1437
  const isEmptyVar = ctx.n();
1229
1438
  ctx.push(`let ${isEmptyVar} = !${hasItems}();`);
@@ -1685,6 +1894,8 @@ function emitClientFromIR(ir, options) {
1685
1894
  }
1686
1895
  if (ir.components.some(c => hasKeyedMap(c.body)))
1687
1896
  runtimeNames.push('reconcile');
1897
+ if (options.hydrate && ir.components.some(c => hasKeyedMap(c.body)))
1898
+ runtimeNames.push('reconcileHydrated');
1688
1899
  if (options.hydrate) {
1689
1900
  const hydrateNames = ['hydrate', 'hydrateViewport', 'hydrateIdle', 'hydrateOnInteraction', 'needsHydration', 'createHydrateWalker', 'collectVskMarkers', 'reactiveProps'];
1690
1901
  for (const name of hydrateNames) {
@@ -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,CA6P9E"}
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,CA2Q9E"}
@@ -1160,6 +1160,19 @@ export function generateIR(ast, source, filename) {
1160
1160
  continue;
1161
1161
  }
1162
1162
  }
1163
+ // `export const standalone = true` / `export let standalone = true` is a
1164
+ // compile-time routing directive (isStandaloneLayoutFile), not runtime
1165
+ // code — drop it so it never reaches emitted JS (client chunks are
1166
+ // IIFE-wrapped where a bare `export` is a syntax error).
1167
+ if (node.type === 'ExportNamedDeclaration' && node.declaration) {
1168
+ const decl = node.declaration;
1169
+ if (decl.type === 'VariableDeclaration') {
1170
+ const declName = decl.declarations[0]?.id?.name;
1171
+ if (declName === 'standalone') {
1172
+ continue;
1173
+ }
1174
+ }
1175
+ }
1163
1176
  let inner = node;
1164
1177
  let exported = false;
1165
1178
  let defaultExport = false;
@@ -1,3 +1,8 @@
1
+ import { parse } from '@vesk/compiler/src/parser';
2
+ /** True when the import target is owned by the framework (never SSR-loaded). */
3
+ export declare function isCompilerOwnedTarget(target: string): boolean;
4
+ /** True when the target carries no value into the SSR scope. */
5
+ export declare function isValueLessTarget(target: string): boolean;
1
6
  /**
2
7
  * AST-driven extraction of the value bindings introduced by one import
3
8
  * statement (`import { a as b } from 'm'` → `[{ local: 'b', imported: 'a' }]`;
@@ -32,6 +37,10 @@ export declare function applyLocalModuleImports(__vesk: Record<string, unknown>,
32
37
  * Builtin markers load through Node and are cached separately (never stale).
33
38
  */
34
39
  export declare function loadSsrModule(absPath: string): Record<string, unknown> | null;
40
+ export declare function readSsrSource(absPath: string): {
41
+ raw: string;
42
+ ast: ReturnType<typeof parse> | null;
43
+ } | null;
35
44
  /**
36
45
  * Resolves a specifier to an absolute file path (or a `BUILTIN_PREFIX` marker
37
46
  * for Node builtins). Relative/absolute localities use extension probing;
@@ -48,4 +57,38 @@ export declare function resolveSsrModule(specifier: string, fromDir: string): st
48
57
  export declare function esmToCjs(body: Array<{
49
58
  type: string;
50
59
  } & Record<string, unknown>>): string;
60
+ export interface BundledModuleData {
61
+ /** Synthetic loader key (e.g. `__veskMod0`), referenced by deps/bindings. */
62
+ key: string;
63
+ /** ESM->CJS rewritten body (or raw CJS / JSON `module.exports` body). */
64
+ code: string;
65
+ /** Directory the module lived in at build time (require fallback anchor). */
66
+ dir: string;
67
+ /** Relative specifier (as written) -> sibling module key. */
68
+ deps: Record<string, string>;
69
+ }
70
+ export interface ModuleBindingData {
71
+ /** Local binding name used inside the `.vsk` file. */
72
+ local: string;
73
+ /** Module key this binding resolves to. */
74
+ key: string;
75
+ /** `default` | `*` | named export. */
76
+ kind: string;
77
+ }
78
+ export type ModuleCollector = {
79
+ keysByAbs: Map<string, string>;
80
+ modules: BundledModuleData[];
81
+ };
82
+ /**
83
+ * Bundles one module file (plus its relative closure) into `collector`.
84
+ * Returns the synthetic key loaded modules use to reference it.
85
+ */
86
+ export declare function collectModuleBundled(absPath: string, collector: ModuleCollector): string;
87
+ /**
88
+ * Builds the module bundle + local-binding list for a set of import lines
89
+ * (a `.vsk` file's `imports`). Framework-owned, `.vsk`, `.css` and markdown
90
+ * targets are skipped exactly as `applyLocalModuleImports` skips them, so the
91
+ * hydrate-time scope matches today's runtime module loader.
92
+ */
93
+ export declare function collectModuleBundle(importStrs: string[], fromDir: string | undefined, collector: ModuleCollector): ModuleBindingData[];
51
94
  //# sourceMappingURL=module-imports.d.ts.map
@@ -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,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"}
1
+ {"version":3,"file":"module-imports.d.ts","sourceRoot":"","sources":["../src/module-imports.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,KAAK,EAAE,MAAM,2BAA2B,CAAC;AA2ElD,gFAAgF;AAChF,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAM7D;AAED,gEAAgE;AAChE,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAOzD;AAED;;;;;;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;AA6BD,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,UAAU,CAAC,OAAO,KAAK,CAAC,GAAG,IAAI,CAAA;CAAE,GAAG,IAAI,CAe3G;AA8TD;;;;;;;;;;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;AAaD,MAAM,WAAW,iBAAiB;IAChC,6EAA6E;IAC7E,GAAG,EAAE,MAAM,CAAC;IACZ,yEAAyE;IACzE,IAAI,EAAE,MAAM,CAAC;IACb,6EAA6E;IAC7E,GAAG,EAAE,MAAM,CAAC;IACZ,6DAA6D;IAC7D,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC9B;AAED,MAAM,WAAW,iBAAiB;IAChC,sDAAsD;IACtD,KAAK,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,GAAG,EAAE,MAAM,CAAC;IACZ,sCAAsC;IACtC,IAAI,EAAE,MAAM,CAAC;CACd;AAoDD,MAAM,MAAM,eAAe,GAAG;IAC5B,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,OAAO,EAAE,iBAAiB,EAAE,CAAC;CAC9B,CAAC;AAEF;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,eAAe,GAAG,MAAM,CAuCxF;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,MAAM,EAAE,EACpB,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,SAAS,EAAE,eAAe,GACzB,iBAAiB,EAAE,CAiBrB"}
@@ -3,6 +3,7 @@ import { createRequire } from 'node:module';
3
3
  import { dirname, extname, isAbsolute, join, resolve } from 'node:path';
4
4
  import { print } from 'esrap';
5
5
  import ts from 'esrap/languages/ts';
6
+ import { walk } from 'zimmerframe';
6
7
  import { parse } from '@vesk/compiler/src/parser';
7
8
  import { stripTsTypes, hasTsSyntax, isTypeOnlyStatement } from '@vesk/compiler/src/strip-ts';
8
9
  import { importModuleTarget } from '@vesk/compiler/src/tokens';
@@ -62,7 +63,7 @@ function cacheModule(p, val) {
62
63
  */
63
64
  const depStack = [];
64
65
  /** True when the import target is owned by the framework (never SSR-loaded). */
65
- function isCompilerOwnedTarget(target) {
66
+ export function isCompilerOwnedTarget(target) {
66
67
  if (target === '@vesk/runtime' || target === '@vesk/reactivity')
67
68
  return true;
68
69
  for (const prefix of RUNTIME_PREFIXES) {
@@ -72,7 +73,7 @@ function isCompilerOwnedTarget(target) {
72
73
  return false;
73
74
  }
74
75
  /** True when the target carries no value into the SSR scope. */
75
- function isValueLessTarget(target) {
76
+ export function isValueLessTarget(target) {
76
77
  return (target.endsWith('.vsk') ||
77
78
  target.endsWith('.css') ||
78
79
  target.endsWith('.md') ||
@@ -300,7 +301,7 @@ function depsFresh(cachedVal) {
300
301
  }
301
302
  return true;
302
303
  }
303
- function readSsrSource(absPath) {
304
+ export function readSsrSource(absPath) {
304
305
  let raw;
305
306
  try {
306
307
  raw = readFileSync(absPath, 'utf-8');
@@ -954,3 +955,129 @@ export function esmToCjs(body) {
954
955
  }
955
956
  return lines.join('\n');
956
957
  }
958
+ function isRelativeSpec(spec) {
959
+ return spec.startsWith('./') || spec.startsWith('../');
960
+ }
961
+ function stringLiteralValue(node) {
962
+ const n = node;
963
+ if (n && (n.type === 'Literal' || n.type === 'StringLiteral') && typeof n.value === 'string') {
964
+ return n.value;
965
+ }
966
+ return null;
967
+ }
968
+ /**
969
+ * Extracts the module specifiers a module body reaches for through import/
970
+ * export sources, `require(...)` calls and `import(...)` expressions.
971
+ * Type-only import/export statements contribute nothing here.
972
+ */
973
+ function collectModuleSpecifiers(ast) {
974
+ const specs = new Set();
975
+ walk(ast, null, {
976
+ ImportDeclaration(node) {
977
+ if (node.importKind === 'type')
978
+ return;
979
+ const v = stringLiteralValue(node.source);
980
+ if (v)
981
+ specs.add(v);
982
+ },
983
+ ExportNamedDeclaration(node) {
984
+ if ((node.exportKind === 'type') || !node.source)
985
+ return;
986
+ const v = stringLiteralValue(node.source);
987
+ if (v)
988
+ specs.add(v);
989
+ },
990
+ ExportAllDeclaration(node) {
991
+ if (node.exportKind === 'type')
992
+ return;
993
+ const v = stringLiteralValue(node.source);
994
+ if (v)
995
+ specs.add(v);
996
+ },
997
+ CallExpression(node) {
998
+ const callee = node.callee;
999
+ if (!callee || callee.type !== 'Identifier' || callee.name !== 'require')
1000
+ return;
1001
+ const args = (node.arguments || []);
1002
+ const v = stringLiteralValue(args[0]);
1003
+ if (v)
1004
+ specs.add(v);
1005
+ },
1006
+ ImportExpression(node) {
1007
+ const v = stringLiteralValue(node.source);
1008
+ if (v)
1009
+ specs.add(v);
1010
+ },
1011
+ });
1012
+ return specs;
1013
+ }
1014
+ /**
1015
+ * Bundles one module file (plus its relative closure) into `collector`.
1016
+ * Returns the synthetic key loaded modules use to reference it.
1017
+ */
1018
+ export function collectModuleBundled(absPath, collector) {
1019
+ const existing = collector.keysByAbs.get(absPath);
1020
+ if (existing)
1021
+ return existing;
1022
+ const key = `__veskMod${collector.keysByAbs.size}`;
1023
+ collector.keysByAbs.set(absPath, key);
1024
+ const dir = dirname(absPath);
1025
+ if (absPath.endsWith('.json')) {
1026
+ let raw = '';
1027
+ try {
1028
+ raw = readFileSync(absPath, 'utf-8');
1029
+ }
1030
+ catch (err) {
1031
+ raw = '{}';
1032
+ }
1033
+ collector.modules.push({ key, code: `module.exports = ${raw};`, dir, deps: {} });
1034
+ return key;
1035
+ }
1036
+ const src = readSsrSource(absPath);
1037
+ if (!src || !src.ast) {
1038
+ const raw = src ? src.raw : '// unreadable during build\nmodule.exports = {};';
1039
+ collector.modules.push({ key, code: raw, dir, deps: {} });
1040
+ return key;
1041
+ }
1042
+ let stripped = src.ast;
1043
+ if (hasTsSyntax(src.ast))
1044
+ stripped = stripTsTypes(src.ast);
1045
+ stripped.body = (stripped.body || []).filter((n) => (n ? !isTypeOnlyStatement(n) : false));
1046
+ const deps = {};
1047
+ for (const spec of collectModuleSpecifiers(src.ast)) {
1048
+ if (!isRelativeSpec(spec))
1049
+ continue;
1050
+ const depPath = resolveSsrModule(spec, dir);
1051
+ if (!depPath)
1052
+ continue;
1053
+ deps[spec] = collectModuleBundled(depPath, collector);
1054
+ }
1055
+ collector.modules.push({ key, code: esmToCjs(stripped.body), dir, deps });
1056
+ return key;
1057
+ }
1058
+ /**
1059
+ * Builds the module bundle + local-binding list for a set of import lines
1060
+ * (a `.vsk` file's `imports`). Framework-owned, `.vsk`, `.css` and markdown
1061
+ * targets are skipped exactly as `applyLocalModuleImports` skips them, so the
1062
+ * hydrate-time scope matches today's runtime module loader.
1063
+ */
1064
+ export function collectModuleBundle(importStrs, fromDir, collector) {
1065
+ const bindings = [];
1066
+ if (!fromDir)
1067
+ return bindings;
1068
+ for (const imp of importStrs) {
1069
+ const target = importModuleTarget(imp);
1070
+ if (!target || isCompilerOwnedTarget(target) || isValueLessTarget(target))
1071
+ continue;
1072
+ const resolved = resolveSsrModule(target, fromDir);
1073
+ if (!resolved) {
1074
+ // Keep render behavior identical to the runtime loader (warn + skip).
1075
+ continue;
1076
+ }
1077
+ const key = collectModuleBundled(resolved, collector);
1078
+ for (const pair of importBindingPairs(imp)) {
1079
+ bindings.push({ local: pair.local, key, kind: pair.imported });
1080
+ }
1081
+ }
1082
+ return bindings;
1083
+ }