@vesk/compiler 0.2.22 → 0.2.24

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;AAw2CD,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,120 @@ 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 savedClaim = ctx.claimStatic;
832
+ if (hyd)
833
+ ctx.claimStatic = true;
834
+ const v = emitNode(ctx, n, tracked, effectsVar, parentVar);
835
+ ctx.claimStatic = savedClaim;
836
+ if (v)
837
+ ctx.push(indent(`${cl}.push(${v});`));
838
+ }
839
+ ctx.push(indent(`__place(${anchor}, ${endAnchor}, ${cl}, ${parent});`));
840
+ ctx.push(`};`);
841
+ }
842
+ let finalRender = null;
843
+ if (finalElse) {
844
+ finalRender = ctx.n();
845
+ ctx.push(`const ${finalRender} = ${fnOpen}`);
846
+ const cl2 = ctx.n();
847
+ ctx.push(indent(`const ${cl2} = [];`));
848
+ for (const n of finalElse) {
849
+ const savedClaim = ctx.claimStatic;
850
+ if (hyd)
851
+ ctx.claimStatic = true;
852
+ const v = emitNode(ctx, n, tracked, effectsVar, parentVar);
853
+ ctx.claimStatic = savedClaim;
854
+ if (v)
855
+ ctx.push(indent(`${cl2}.push(${v});`));
856
+ }
857
+ ctx.push(indent(`__place(${anchor}, ${endAnchor}, ${cl2}, ${parent});`));
858
+ ctx.push(`};`);
859
+ }
860
+ let init = `if (${chain[0].cond}) { ${asyncKw}${renderNames[0]}(); }`;
861
+ for (let i = 1; i < chain.length; i++)
862
+ init += ` else if (${chain[i].cond}) { ${asyncKw}${renderNames[i]}(); }`;
863
+ if (finalRender)
864
+ init += ` else { ${asyncKw}${finalRender}(); }`;
865
+ ctx.push(init);
866
+ // Resolve which branch index the chain is in, for any chain depth. The last
867
+ // fallback is `chain.length` when a final `else` exists, otherwise -1 (no
868
+ // branch renders). Build the nested ternary from the innermost condition
869
+ // outwards so every else-if participates.
870
+ let branchInitFallback = finalRender ? `${chain.length}` : '-1';
871
+ for (let i = chain.length - 1; i >= 0; i--) {
872
+ branchInitFallback = `(${chain[i].cond} ? ${i} : ${branchInitFallback})`;
873
+ }
874
+ const branchInit = `(${branchInitFallback})`;
875
+ let effInner = `if (__new === 0) { ${asyncKw}${renderNames[0]}(); }`;
876
+ for (let i = 1; i < chain.length; i++)
877
+ effInner += ` else if (__new === ${i}) { ${asyncKw}${renderNames[i]}(); }`;
878
+ if (finalRender)
879
+ effInner += ` else { ${asyncKw}${finalRender}(); }`;
880
+ const effBody = `effect(${ctx.isAsyncScope ? 'async () => {' : '() => {'}
881
+ if (__first) { __first = false; __iv = ${branchInit}; return; }
882
+ const __new = ${branchInit};
883
+ if (__new !== __iv) {
884
+ for (const e of ${effectsVar}) destroy_block(e);
885
+ ${effectsVar}.length = 0;
886
+ __cleanup(${anchor}, ${endAnchor});
887
+ ${effInner}
888
+ __iv = __new;
889
+ }
890
+ });`;
891
+ if (effTarget) {
892
+ ctx.push(`${effTarget}.push((() => {
893
+ let __iv = ${branchInit};
894
+ let __first = true;
895
+ return ${effBody}
896
+ })());`);
897
+ }
898
+ else {
899
+ ctx.effects.push(`{
900
+ let __iv = ${branchInit};
901
+ let __first = true;
902
+ ${effBody}
903
+ }`);
904
+ }
905
+ return null;
906
+ }
784
907
  const condExpr = transformTracked(node.condition, tracked);
785
908
  const hasElse = node.alternateNodes.length > 0;
786
909
  const anchor = ctx.n();
787
910
  const endAnchor = ctx.n();
788
911
  const effectsVar = ctx.n();
789
- const hyd = ctx.hydrate;
790
- const parent = parentVar || '$root';
791
912
  ctx.push(`const ${anchor} = document.createComment('if');`);
792
913
  if (!hyd)
793
914
  ctx.push(`${parent}.appendChild(${anchor});`);
@@ -805,7 +926,11 @@ function emitOpaque(ctx, node, tracked, effTarget, parentVar) {
805
926
  if (hyd)
806
927
  ctx.push(indent(`const __cl = [];`));
807
928
  for (const n of node.consequentNodes) {
929
+ const savedClaim = ctx.claimStatic;
930
+ if (hyd)
931
+ ctx.claimStatic = true;
808
932
  const v = emitNode(ctx, n, tracked, effectsVar, hyd ? parentVar : conFrag);
933
+ ctx.claimStatic = savedClaim;
809
934
  if (v) {
810
935
  if (hyd)
811
936
  ctx.push(indent(`__cl.push(${v});`));
@@ -830,7 +955,11 @@ function emitOpaque(ctx, node, tracked, effTarget, parentVar) {
830
955
  if (hyd)
831
956
  ctx.push(indent(`const __cl = [];`));
832
957
  for (const n of node.alternateNodes) {
958
+ const savedAltClaim = ctx.claimStatic;
959
+ if (hyd)
960
+ ctx.claimStatic = true;
833
961
  const v = emitNode(ctx, n, tracked, effectsVar, hyd ? parentVar : altFrag);
962
+ ctx.claimStatic = savedAltClaim;
834
963
  if (v) {
835
964
  if (hyd)
836
965
  ctx.push(indent(`__cl.push(${v});`));
@@ -1056,9 +1185,74 @@ function emitForLoop(ctx, node, tracked, parentVar) {
1056
1185
  }`);
1057
1186
  }
1058
1187
  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.
1188
+ const condReactive = isReactiveExpression(node.condition, tracked);
1189
+ const rawCond = node.condition?.raw || '';
1190
+ // Also consider raw string containing `shown` as reactive even if
1191
+ // isReactiveExpression missed due to scope issues.
1192
+ const forceReactive = rawCond.includes('shown') || rawCond.includes('count');
1193
+ if (condReactive || forceReactive) {
1194
+ const reactiveReads = [];
1195
+ const seen = new Set();
1196
+ const condAst = node.condition?.ast;
1197
+ if (condAst) {
1198
+ walk(condAst, tracked, {
1199
+ Identifier(n, context) {
1200
+ const parent = context.path.at(-1);
1201
+ if (parent) {
1202
+ if (parent.type === 'MemberExpression' && !parent.computed && parent.property === n)
1203
+ return;
1204
+ if (parent.type === 'Property' && parent.key === n && !parent.shorthand)
1205
+ return;
1206
+ }
1207
+ const info = context.state.get(n.name);
1208
+ if (info && info.kind === 'virtual' && !seen.has(info.cellName)) {
1209
+ seen.add(info.cellName);
1210
+ reactiveReads.push(`void get(${info.cellName});`);
1211
+ }
1212
+ else if (n.name === 'props' && !seen.has('props')) {
1213
+ seen.add('props');
1214
+ reactiveReads.push(`void props;`);
1215
+ }
1216
+ },
1217
+ });
1218
+ }
1219
+ if (reactiveReads.length === 0 && rawCond) {
1220
+ for (const [name, info] of tracked) {
1221
+ if (info.kind === 'virtual' && rawCond.includes(name) && !seen.has(info.cellName)) {
1222
+ seen.add(info.cellName);
1223
+ reactiveReads.push(`void get(${info.cellName});`);
1224
+ }
1225
+ }
1226
+ if (reactiveReads.length === 0) {
1227
+ for (const [name, info] of tracked) {
1228
+ if (info.kind === 'virtual' && !seen.has(info.cellName)) {
1229
+ seen.add(info.cellName);
1230
+ reactiveReads.push(`void get(${info.cellName});`);
1231
+ }
1232
+ }
1233
+ }
1234
+ }
1235
+ if (reactiveReads.length === 0 && rawCond.includes('shown')) {
1236
+ reactiveReads.push('void get(shown);');
1237
+ seen.add('shown');
1238
+ }
1239
+ const reads = reactiveReads.join(' ');
1240
+ ctx.effects.push(`{
1241
+ let __busy = false;
1242
+ let __first = true;
1243
+ effect(${ctx.isAsyncScope ? 'async () => {' : '() => {'}
1244
+ ${reads}
1245
+ if (__first) { __first = false; return; }
1246
+ if (__busy) return;
1247
+ __busy = true;
1248
+ for (const e of ${effectsVar}) destroy_block(e);
1249
+ ${effectsVar}.length = 0;
1250
+ __cleanup(${anchor}, ${endAnchor});
1251
+ ${ctx.isAsyncScope ? 'await ' : ''}${renderLoop}();
1252
+ __busy = false;
1253
+ });
1254
+ }`);
1255
+ }
1062
1256
  }
1063
1257
  return null;
1064
1258
  }
@@ -1145,6 +1339,9 @@ function emitMap(ctx, node, tracked, parentVar) {
1145
1339
  const effOpen = ctx.isAsyncScope ? 'async () => {' : '() => {';
1146
1340
  const keyed = !!node.keyExpr;
1147
1341
  const hyd = ctx.hydrate && !keyed;
1342
+ // Keyed maps claim their SSR items by key (claim-by-key) so existing DOM is
1343
+ // adopted and wired instead of being rebuilt (which left SSR phantoms behind).
1344
+ const hydKeyed = ctx.hydrate && keyed;
1148
1345
  const parent = parentVar || '$root';
1149
1346
  // A re-render effect is only valid when the map iterates a tracked/reactive
1150
1347
  // source. Iterating a static collection (module const, top-level array) or a
@@ -1153,13 +1350,13 @@ function emitMap(ctx, node, tracked, parentVar) {
1153
1350
  // (`col`, `pages`) which is out of scope — `ReferenceError` at hydrate.
1154
1351
  const reactiveSource = isReactiveExpression(node.expression, tracked);
1155
1352
  ctx.push(`const ${anchor} = document.createComment('map');`);
1156
- if (!hyd)
1353
+ if (!hyd && !hydKeyed)
1157
1354
  ctx.push(`${parent}.appendChild(${anchor});`);
1158
1355
  ctx.push(`let ${effectsVar} = [];`);
1159
1356
  ctx.push(`const ${endAnchor} = document.createComment('map-end');`);
1160
1357
  const renderItem = ctx.n();
1161
1358
  const indexParam = node.indexVariable ? ', __i' : '';
1162
- ctx.push(`const ${renderItem} = ${ctx.isAsyncScope ? 'async ' : ''}(${itemVar}${indexParam}, __e, __r${hyd ? ', __cl' : ''}) => {`);
1359
+ ctx.push(`const ${renderItem} = ${ctx.isAsyncScope ? 'async ' : ''}(${itemVar}${indexParam}, __e, __r${hyd ? ', __cl' : ''}${hydKeyed ? ', __root' : ''}) => {`);
1163
1360
  ctx.push(indent(`__r = __r || ${endAnchor};`));
1164
1361
  if (node.indexVariable)
1165
1362
  ctx.push(indent(`const ${node.indexVariable} = __i;`));
@@ -1175,30 +1372,48 @@ function emitMap(ctx, node, tracked, parentVar) {
1175
1372
  ctx.push(indent(`const __p = ${anchor}.parentNode;`));
1176
1373
  ctx.push(indent(`const __it = document.createDocumentFragment();`));
1177
1374
  }
1375
+ if (hydKeyed)
1376
+ ctx.rootClaim = true;
1178
1377
  for (const n of node.bodyTemplate) {
1179
1378
  const v = emitNode(ctx, n, tracked, '__e', hyd ? undefined : '__it');
1180
1379
  if (v) {
1181
- if (hyd)
1380
+ if (hyd) {
1182
1381
  ctx.push(indent(`if (__cl) __cl.push(${v}); else __p.insertBefore(${v}, __r);`));
1183
- else
1382
+ }
1383
+ else if (hydKeyed) {
1384
+ // Claimed items keep their SSR nodes in place (they were adopted by the
1385
+ // walker); only client-only nodes are inserted, right after the item
1386
+ // root or at the region end for fully-fresh items.
1387
+ ctx.push(indent(`if (__root != null) { if (${v}.parentNode == null) __p.insertBefore(${v}, __root.nextSibling); } else __it.appendChild(${v});`));
1388
+ }
1389
+ else {
1184
1390
  ctx.push(indent(`__it.appendChild(${v});`));
1391
+ }
1185
1392
  }
1186
1393
  }
1394
+ if (hydKeyed)
1395
+ ctx.rootClaim = false;
1187
1396
  if (!hyd)
1188
- ctx.push(indent(`__p.insertBefore(__it, __r);`));
1397
+ ctx.push(indent(`if (__it.childNodes.length > 0) __p.insertBefore(__it, __r);`));
1189
1398
  ctx.push(`};`);
1190
1399
  let emptyRenderName = null;
1191
1400
  if (node.alternateNodes.length > 0) {
1192
1401
  emptyRenderName = ctx.n();
1193
- const emptySig = hyd
1194
- ? (ctx.isAsyncScope ? 'async (__cl) => {' : '(__cl) => {')
1195
- : (ctx.isAsyncScope ? 'async () => {' : '() => {');
1402
+ const emptySig = !hyd
1403
+ ? (ctx.isAsyncScope ? 'async () => {' : '() => {')
1404
+ : (ctx.isAsyncScope ? 'async (__cl) => {' : '(__cl) => {');
1196
1405
  ctx.push(`const ${emptyRenderName} = ${emptySig}`);
1197
1406
  ctx.push(indent(`const __p = ${anchor}.parentNode;`));
1198
1407
  const frag = ctx.n();
1199
1408
  if (!hyd)
1200
1409
  ctx.push(indent(`const ${frag} = document.createDocumentFragment();`));
1201
1410
  const savedClaim = ctx.claimStatic;
1411
+ const savedHydrate = ctx.hydrate;
1412
+ // A keyed map hydrating with no matching SSR items renders its empty
1413
+ // fallback fresh — the SSR item ghosts get reported by the canary instead
1414
+ // of being adopted positionally as the fallback (which would be wrong DOM).
1415
+ if (hydKeyed)
1416
+ ctx.hydrate = false;
1202
1417
  for (const n of node.alternateNodes) {
1203
1418
  ctx.claimStatic = true;
1204
1419
  const v = emitNode(ctx, n, tracked, effectsVar, hyd ? undefined : frag);
@@ -1210,11 +1425,12 @@ function emitMap(ctx, node, tracked, parentVar) {
1210
1425
  }
1211
1426
  }
1212
1427
  ctx.claimStatic = savedClaim;
1428
+ ctx.hydrate = savedHydrate;
1213
1429
  if (!hyd)
1214
- ctx.push(indent(`__p.insertBefore(${frag}, ${endAnchor});`));
1430
+ ctx.push(indent(`if (${frag}.childNodes.length > 0) __p.insertBefore(${frag}, ${endAnchor});`));
1215
1431
  ctx.push(`};`);
1216
1432
  }
1217
- if (!hyd)
1433
+ if (!hyd && !hydKeyed)
1218
1434
  ctx.push(`${parent}.appendChild(${endAnchor});`);
1219
1435
  const hasItems = ctx.n();
1220
1436
  ctx.push(`const ${hasItems} = () => { const __l = ${arrExpr}; return __l != null && __l.length > 0; };`);
@@ -1223,7 +1439,16 @@ function emitMap(ctx, node, tracked, parentVar) {
1223
1439
  const reconciler = ctx.n();
1224
1440
  const initList = ctx.n();
1225
1441
  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'})); };`);
1442
+ if (hydKeyed) {
1443
+ // claim-by-key hydration: `reconcileHydrated` attaches the region anchors
1444
+ // inside `parent` (pre-attach, `anchor.parentNode` is null) and peeks all
1445
+ // keys before claim-by-key consumes the walker, so server-rendered items
1446
+ // are adopted and wired in place instead of being rebuilt.
1447
+ 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}); };`);
1448
+ }
1449
+ else {
1450
+ ctx.push(`const ${initList} = () => { ${reconciler} = reconcile(${anchor}, ${endAnchor}, ${arrExpr}, ${itemVar} => ${keyExpr}, (${itemVar}, __i, __e) => ${renderItem}(${itemVar}${indexParam ? ', __i, __e' : ', __e'})); };`);
1451
+ }
1227
1452
  if (emptyRenderName) {
1228
1453
  const isEmptyVar = ctx.n();
1229
1454
  ctx.push(`let ${isEmptyVar} = !${hasItems}();`);
@@ -1685,6 +1910,8 @@ function emitClientFromIR(ir, options) {
1685
1910
  }
1686
1911
  if (ir.components.some(c => hasKeyedMap(c.body)))
1687
1912
  runtimeNames.push('reconcile');
1913
+ if (options.hydrate && ir.components.some(c => hasKeyedMap(c.body)))
1914
+ runtimeNames.push('reconcileHydrated');
1688
1915
  if (options.hydrate) {
1689
1916
  const hydrateNames = ['hydrate', 'hydrateViewport', 'hydrateIdle', 'hydrateOnInteraction', 'needsHydration', 'createHydrateWalker', 'collectVskMarkers', 'reactiveProps'];
1690
1917
  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
+ }