@vesk/adapter 0.2.58 → 0.2.60

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-bundle.d.ts","sourceRoot":"","sources":["../src/client-bundle.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,SAAS,EAAE,mBAAmB,EAAE,kBAAkB,EAAmF,MAAM,yBAAyB,CAAC;AAoCnL;;;;;;;;;;;;;GAaG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CA4BhE;AAED;;;;;GAKG;AACH,UAAU,YAAY;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IAC7B,EAAE,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACvB,YAAY,CAAC,EAAE,KAAK,CAAC;QAAE,EAAE,CAAC,EAAE;YAAE,IAAI,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;QAAC,IAAI,CAAC,EAAE;YAAE,IAAI,CAAC,EAAE,MAAM,CAAC;YAAC,MAAM,CAAC,EAAE;gBAAE,IAAI,CAAC,EAAE,MAAM,CAAC;gBAAC,IAAI,CAAC,EAAE,MAAM,CAAA;aAAE,CAAA;SAAE,CAAA;KAAE,CAAC,CAAC;IACtH,WAAW,CAAC,EAAE;QACZ,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,MAAM,CAAC,EAAE;YAAE,IAAI,CAAC,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;QAC1C,YAAY,CAAC,EAAE,KAAK,CAAC;YAAE,IAAI,CAAC,EAAE;gBAAE,IAAI,CAAC,EAAE,MAAM,CAAC;gBAAC,MAAM,CAAC,EAAE;oBAAE,IAAI,CAAC,EAAE,MAAM,CAAC;oBAAC,IAAI,CAAC,EAAE,MAAM,CAAA;iBAAE,CAAA;aAAE,CAAA;SAAE,CAAC,CAAC;KAC/F,CAAC;IACF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAmDD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAKzD;AAkCD;;;;;;;;;;;GAWG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CA+B7D;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAyBlD;AA4MD;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CA+B5D;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CASxD;AA4CD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,OAAO,GAAG,MAAM,CAoBnG;AAED,wBAAsB,oBAAoB,CACxC,SAAS,EAAE,SAAS,EAAE,EACtB,MAAM,EAAE,MAAM,EACd,YAAY,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAClC,OAAO,CAAC,EAAE,mBAAmB,GAC5B,OAAO,CAAC,kBAAkB,CAAC,CAie7B;AAMD,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CA+B3D;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAUlE;AAoCD;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAI/F"}
1
+ {"version":3,"file":"client-bundle.d.ts","sourceRoot":"","sources":["../src/client-bundle.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,SAAS,EAAE,mBAAmB,EAAE,kBAAkB,EAAmF,MAAM,yBAAyB,CAAC;AAoCnL;;;;;;;;;;;;;GAaG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CA4BhE;AAED;;;;;GAKG;AACH,UAAU,YAAY;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IAC7B,EAAE,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACvB,YAAY,CAAC,EAAE,KAAK,CAAC;QAAE,EAAE,CAAC,EAAE;YAAE,IAAI,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;QAAC,IAAI,CAAC,EAAE;YAAE,IAAI,CAAC,EAAE,MAAM,CAAC;YAAC,MAAM,CAAC,EAAE;gBAAE,IAAI,CAAC,EAAE,MAAM,CAAC;gBAAC,IAAI,CAAC,EAAE,MAAM,CAAA;aAAE,CAAA;SAAE,CAAA;KAAE,CAAC,CAAC;IACtH,WAAW,CAAC,EAAE;QACZ,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,MAAM,CAAC,EAAE;YAAE,IAAI,CAAC,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;QAC1C,YAAY,CAAC,EAAE,KAAK,CAAC;YAAE,IAAI,CAAC,EAAE;gBAAE,IAAI,CAAC,EAAE,MAAM,CAAC;gBAAC,MAAM,CAAC,EAAE;oBAAE,IAAI,CAAC,EAAE,MAAM,CAAC;oBAAC,IAAI,CAAC,EAAE,MAAM,CAAA;iBAAE,CAAA;aAAE,CAAA;SAAE,CAAC,CAAC;KAC/F,CAAC;IACF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAmDD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAKzD;AAkCD;;;;;;;;;;;GAWG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CA+B7D;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAyBlD;AAuQD;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CA+B5D;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CASxD;AA4CD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,OAAO,GAAG,MAAM,CAoBnG;AAED,wBAAsB,oBAAoB,CACxC,SAAS,EAAE,SAAS,EAAE,EACtB,MAAM,EAAE,MAAM,EACd,YAAY,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAClC,OAAO,CAAC,EAAE,mBAAmB,GAC5B,OAAO,CAAC,kBAAkB,CAAC,CA0nB7B;AAMD,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CA+B3D;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAUlE;AAoCD;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAI/F"}
@@ -268,6 +268,48 @@ export function demoteExports(code) {
268
268
  function chunkHasImports(headLen, mergeAborted) {
269
269
  return headLen > 0 || mergeAborted;
270
270
  }
271
+ /**
272
+ * Scans a module's own source for the names it imports from `@vesk/runtime*`.
273
+ * The shared-module pass needs them so the entry bundle publishes exactly the
274
+ * bindings the shared code will read back off `globalThis`.
275
+ */
276
+ function runtimeImportsOf(filePath) {
277
+ let text;
278
+ try {
279
+ text = readFileSync(filePath, 'utf8');
280
+ }
281
+ catch {
282
+ return [];
283
+ }
284
+ const names = [];
285
+ const pattern = /import\s+(type\s+)?\{([^}]*)\}\s*from\s*['"]@vesk\/runtime[^'"]*['"]/g;
286
+ for (const match of text.matchAll(pattern)) {
287
+ if (match[1])
288
+ continue; // type-only imports are erased
289
+ for (const part of match[2].split(',')) {
290
+ const cleaned = part.trim().replace(/^type\s+/, '');
291
+ if (!cleaned)
292
+ continue;
293
+ const name = cleaned.split(/\s+as\s+/)[0].trim();
294
+ if (name)
295
+ names.push(name);
296
+ }
297
+ }
298
+ return names;
299
+ }
300
+ /**
301
+ * esbuild resolver that redirects `@vesk/runtime*` imports in the shared-module
302
+ * pass to a generated shim reading the ENTRY bundle's runtime off `globalThis`.
303
+ * `shimPath` is generated by the caller, which knows which names are needed.
304
+ */
305
+ function sharedRuntimeResolver(shimPath) {
306
+ return {
307
+ name: 'vesk-shared-runtime',
308
+ setup(build) {
309
+ build.onResolve({ filter: /^@vesk\/runtime(\/.*)?$/ }, () => ({ path: shimPath }));
310
+ },
311
+ };
312
+ }
271
313
  /**
272
314
  * Accumulates every import a code-split chunk needs and rebuilds it as a
273
315
  * single deduped import block.
@@ -401,6 +443,21 @@ class ChunkImports {
401
443
  get reason() {
402
444
  return this.abortReason;
403
445
  }
446
+ /** Every import source this chunk's merged block pulls in (absolute paths). */
447
+ sources() {
448
+ return this.aborted ? [] : [...this.bySource.keys()];
449
+ }
450
+ /**
451
+ * The named bindings this chunk imports from `source`, so a shared-module
452
+ * shim can re-export exactly what consumers use (re-exporting `*` from the
453
+ * real module would drag it back into the chunk being built).
454
+ */
455
+ bindingsFor(source) {
456
+ const group = this.bySource.get(source);
457
+ if (!group)
458
+ return { named: [], hasDefault: false };
459
+ return { named: [...group.named.keys()].sort(), hasDefault: !!group.default };
460
+ }
404
461
  /** Renders the deduped import block (empty string when there are no imports). */
405
462
  render() {
406
463
  if (this.aborted || this.bySource.size === 0)
@@ -631,6 +688,9 @@ export async function generateClientBundle(routeTree, appDir, componentMap, opti
631
688
  let cachedFileHits = 0;
632
689
  let compiledFiles = 0;
633
690
  let mainFromCache = false;
691
+ // Shared-module bundle inlined at the top of the entry chunk (code-split
692
+ // path only). Non-empty when app modules are shared between route chunks.
693
+ let sharedModuleBundle = '';
634
694
  function mustReuseWithoutStat(filePath) {
635
695
  return !!only && !only.has(filePath);
636
696
  }
@@ -899,7 +959,7 @@ export async function generateClientBundle(routeTree, appDir, componentMap, opti
899
959
  console.error('[vesk] chunk import merge aborted for', chunkName + ':', chunkImports.reason);
900
960
  const head = chunkImports.render();
901
961
  const code = (head ? head + '\n\n' : '') + chunkCode.join('\n\n');
902
- chunkEntries.push({ name: chunkName, code, node, imports: chunkHasImports(head.length, !chunkImports.ok) });
962
+ chunkEntries.push({ name: chunkName, code, node, importsTable: chunkImports, imports: chunkHasImports(head.length, !chunkImports.ok) });
903
963
  }
904
964
  walkSplit(node.children || [], [..._chain, node]);
905
965
  }
@@ -917,9 +977,144 @@ export async function generateClientBundle(routeTree, appDir, componentMap, opti
917
977
  console.error('[vesk] chunk import merge aborted for shared.js:', sharedImports.reason);
918
978
  const head = sharedImports.render();
919
979
  const code = (head ? head + '\n\n' : '') + sharedCode.join('\n\n');
920
- chunkEntries.push({ name: 'shared.js', code, node: null, imports: chunkHasImports(head.length, !sharedImports.ok) });
980
+ chunkEntries.push({ name: 'shared.js', code, node: null, importsTable: sharedImports, imports: chunkHasImports(head.length, !sharedImports.ok) });
921
981
  }
922
982
  const chunkIIFE = (body) => `(()=>{\nconst __components = globalThis.__components || (globalThis.__components = {});\nconst __hydrators = globalThis.__hydrators || (globalThis.__hydrators = {});\n${body}\n})();\n`;
983
+ /**
984
+ * Modules imported by MORE THAN ONE chunk are bundled ONCE and shipped
985
+ * inside the main bundle, instead of being inlined into every chunk.
986
+ *
987
+ * Per-chunk bundling duplicates them, and a duplicated module means a
988
+ * duplicated module SCOPE: a module-scope `track()` cell in `auth/store.ts`
989
+ * exists once per chunk, so signing in through the login chunk left the
990
+ * portal chunk's copy of the session empty and the router rendered the
991
+ * signed-out view — silently, with a green build and no thrown error.
992
+ *
993
+ * The one copy lives in the entry bundle (loaded by every page before any
994
+ * route chunk exists) and publishes itself on `globalThis.__veskShared`;
995
+ * each route chunk imports a generated shim that reads its bindings from
996
+ * there. Keeping it in the MAIN bundle rather than a separate chunk is what
997
+ * makes it race-free: dynamically inserted classic scripts are async by
998
+ * default and do not execute in insertion order, so a shared chunk fetched
999
+ * alongside route chunks can — and did — lose that race.
1000
+ *
1001
+ * Only app-local modules qualify. A node_modules package used by several
1002
+ * chunks is pure third-party code with no module state worth sharing, and
1003
+ * hoisting one (a `lucide-vesk` barrel) would pull it into the entry
1004
+ * bundle for every page.
1005
+ */
1006
+ const sharedUsage = new Map();
1007
+ for (const entry of chunkEntries) {
1008
+ for (const source of new Set(entry.importsTable.sources())) {
1009
+ sharedUsage.set(source, (sharedUsage.get(source) ?? 0) + 1);
1010
+ }
1011
+ }
1012
+ const isAppLocal = (source) => !source.includes('node_modules') && source.startsWith(resolve(appDir, '..'));
1013
+ const sharedSources = [...sharedUsage.entries()]
1014
+ .filter(([source, uses]) => uses > 1 && isAppLocal(source))
1015
+ .map(([source]) => source);
1016
+ if (process.env.VESK_DEBUG_SHARED) {
1017
+ console.error('[shared] candidates:', [...sharedUsage.entries()].map(([s, n]) => `${n}x ${s}`).join('\n'));
1018
+ console.error('[shared] selected:', sharedSources.join(', '));
1019
+ }
1020
+ const chunkAlias = {};
1021
+ let sharedBundleCode = '';
1022
+ // Runtime bindings the shared modules need. The entry bundle publishes each
1023
+ // of these on `globalThis`, and the shared bundle reads them back, so the
1024
+ // app and its stores share ONE runtime instance.
1025
+ const sharedRuntimeNames = new Set();
1026
+ if (sharedSources.length > 0) {
1027
+ const tmpBase = mkdtempSync(join(resolve(appDir, '..'), 'tmp-vesk-shared-'));
1028
+ try {
1029
+ // One entry that imports every shared module and publishes the whole
1030
+ // map on a global, so esbuild emits a single deduped body.
1031
+ const sharedEntry = join(tmpBase, 'shared-entry.js');
1032
+ const lines = [];
1033
+ const members = [];
1034
+ sharedSources.forEach((source, i) => {
1035
+ lines.push(`import * as m${i} from ${JSON.stringify(source)};`);
1036
+ members.push(` ${JSON.stringify(source)}: m${i},`);
1037
+ });
1038
+ lines.push(`globalThis.__veskShared = { ${members.join('\n')} };`);
1039
+ writeFileSync(sharedEntry, lines.join('\n'));
1040
+ const appTsconfig = findTsconfigPath(appDir);
1041
+ // Which runtime bindings do the shared modules need? The entry bundle
1042
+ // publishes exactly these on `globalThis`; the shim reads them back.
1043
+ const runtimeShim = join(tmpBase, 'runtime-shim.js');
1044
+ const runtimeLines = [
1045
+ '// ONE runtime instance: the entry bundle publishes these on globalThis,',
1046
+ '// and route chunks import the same functions. A second bundled copy',
1047
+ '// would mean a second Cell class and a second currentEffect.',
1048
+ 'const __r = (n) => (...args) => globalThis[n](...args);',
1049
+ ];
1050
+ for (const source of sharedSources) {
1051
+ for (const name of runtimeImportsOf(source))
1052
+ sharedRuntimeNames.add(name);
1053
+ }
1054
+ for (const name of [...sharedRuntimeNames].sort()) {
1055
+ runtimeLines.push(`export const ${name} = __r(${JSON.stringify(name)});`);
1056
+ }
1057
+ writeFileSync(runtimeShim, runtimeLines.join('\n') + '\n');
1058
+ const result = await build({
1059
+ entryPoints: [sharedEntry],
1060
+ bundle: true,
1061
+ format: 'iife',
1062
+ platform: 'browser',
1063
+ write: false,
1064
+ logLevel: 'silent',
1065
+ loader: { '.js': 'tsx' },
1066
+ legalComments: 'none',
1067
+ // `@vesk/runtime` MUST stay the entry bundle's instance. Bundling a
1068
+ // second copy in here gives the app a second `Cell` class and a
1069
+ // second `currentEffect` variable: a store cell written from a click
1070
+ // notifies subscribers registered against the OTHER copy, so nothing
1071
+ // the store drives ever updates — writes land, no error, no repaint.
1072
+ plugins: [sharedRuntimeResolver(runtimeShim)],
1073
+ ...(appTsconfig ? { tsconfig: appTsconfig } : {}),
1074
+ });
1075
+ sharedBundleCode = result.outputFiles[0].text;
1076
+ // Per-chunk shims: `export const login = __veskShared['…'].login`.
1077
+ // esbuild inlines these tiny stubs instead of the real module.
1078
+ sharedSources.forEach((source, i) => {
1079
+ const named = new Set();
1080
+ let hasDefault = false;
1081
+ for (const entry of chunkEntries) {
1082
+ const bindings = entry.importsTable.bindingsFor(source);
1083
+ bindings.named.forEach((n) => named.add(n));
1084
+ hasDefault = hasDefault || bindings.hasDefault;
1085
+ }
1086
+ const shim = join(tmpBase, `shim-${i}.js`);
1087
+ const shimLines = [`const m = globalThis.__veskShared[${JSON.stringify(source)}];`];
1088
+ if (hasDefault)
1089
+ shimLines.push(`export default m;`);
1090
+ for (const name of [...named].sort()) {
1091
+ shimLines.push(`export const ${name} = m[${JSON.stringify(name)}];`);
1092
+ }
1093
+ writeFileSync(shim, shimLines.join('\n') + '\n');
1094
+ chunkAlias[source] = shim;
1095
+ });
1096
+ }
1097
+ catch (e) {
1098
+ // A failed shared pass must never ship a half-shared graph: fall back
1099
+ // to per-chunk inlining (the historical behaviour, duplicated modules
1100
+ // and all) rather than to dangling bindings.
1101
+ console.error('[vesk] shared-module pass failed; falling back to per-chunk inlining:', e.message);
1102
+ sharedBundleCode = '';
1103
+ for (const key of Object.keys(chunkAlias))
1104
+ delete chunkAlias[key];
1105
+ }
1106
+ }
1107
+ const sharedRedirectPlugin = Object.keys(chunkAlias).length > 0
1108
+ ? {
1109
+ name: 'vesk-shared-modules',
1110
+ setup(build) {
1111
+ build.onResolve({ filter: /.*/ }, (args) => {
1112
+ const mapped = chunkAlias[args.path];
1113
+ return mapped ? { path: mapped } : null;
1114
+ });
1115
+ },
1116
+ }
1117
+ : null;
923
1118
  for (const entry of chunkEntries) {
924
1119
  if (!entry.code.trim())
925
1120
  continue;
@@ -955,6 +1150,13 @@ export async function generateClientBundle(routeTree, appDir, componentMap, opti
955
1150
  // for a dependency stays in its own package; shipping every
956
1151
  // module's header inside our own chunk adds nothing.
957
1152
  legalComments: 'none',
1153
+ // Shared app modules are NOT inlined here: the resolver below
1154
+ // redirects them to a generated shim reading
1155
+ // `globalThis.__veskShared`, so the single copy in
1156
+ // shared-modules.js serves every route chunk. (esbuild's `alias`
1157
+ // option only accepts bare package names, so this is a resolver
1158
+ // rather than a map.)
1159
+ ...(sharedRedirectPlugin ? { plugins: [sharedRedirectPlugin] } : {}),
958
1160
  ...(appTsconfig ? { tsconfig: appTsconfig } : {}),
959
1161
  });
960
1162
  finalCode = result.outputFiles[0].text;
@@ -1018,14 +1220,22 @@ export async function generateClientBundle(routeTree, appDir, componentMap, opti
1018
1220
  }
1019
1221
  }
1020
1222
  annotate(routeTree);
1021
- const mainKey = JSON.stringify(routeTree) + '|' + [...runtimeImportNames].sort().join(',') + `|${!!options?.hmr}|${!!options?.importRuntime}|${options?.routeDataCache ?? ''}`;
1223
+ // Publish shared modules from the ENTRY bundle so they exist before any
1224
+ // route chunk is even requested (see the shared-module pass above).
1225
+ sharedModuleBundle = sharedBundleCode;
1226
+ // The shared modules read these runtime bindings back off `globalThis`, so
1227
+ // the entry bundle must publish them even when no `.vsk` file imports them
1228
+ // (a store-only app would otherwise get `undefined` for `track`).
1229
+ for (const name of sharedRuntimeNames)
1230
+ runtimeImportNames.add(name);
1231
+ const mainKey = JSON.stringify(routeTree) + '|' + [...runtimeImportNames].sort().join(',') + `|${!!options?.hmr}|${!!options?.importRuntime}|${options?.routeDataCache ?? ''}|${sharedModuleBundle.length}`;
1022
1232
  let main;
1023
1233
  if (cache?.mainBundle && cache.mainBundle.key === mainKey) {
1024
1234
  mainFromCache = true;
1025
1235
  main = cache.mainBundle.code;
1026
1236
  }
1027
1237
  else {
1028
- main = await buildMainBundle(routeTree, runtimeDir, true, {}, !!options?.hmr, !!options?.importRuntime, runtimeImportNames, options?.routeDataCache);
1238
+ main = await buildMainBundle(routeTree, runtimeDir, true, {}, !!options?.hmr, !!options?.importRuntime, runtimeImportNames, options?.routeDataCache, sharedModuleBundle);
1029
1239
  if (cache)
1030
1240
  cache.mainBundle = { key: mainKey, code: main };
1031
1241
  }
@@ -1275,7 +1485,7 @@ function appendHmrGlobals(code) {
1275
1485
  " return eval(code);\n" +
1276
1486
  "};\n";
1277
1487
  }
1278
- async function buildMainBundle(routeTree, runtimeDir, codeSplit, mono, hmr, importRuntime, runtimeImportNames, routeDataCache) {
1488
+ async function buildMainBundle(routeTree, runtimeDir, codeSplit, mono, hmr, importRuntime, runtimeImportNames, routeDataCache, sharedModuleBundle = '') {
1279
1489
  const baseRuntimeImports = ['createFileRouter', 'get', 'set', 'effect', 'track', 'destroy_block', 'getActiveComponent', 'setActiveComponent', 'NavLink', 'Link', 'reactiveProps', 'matchRoute', 'ensureChunk', 'toDomNode', 'rerenderNode'];
1280
1490
  const allRuntimeImports = runtimeImportNames && runtimeImportNames.size > 0
1281
1491
  ? [...new Set([...baseRuntimeImports, ...runtimeImportNames])]
@@ -1392,6 +1602,11 @@ async function buildMainBundle(routeTree, runtimeDir, codeSplit, mono, hmr, impo
1392
1602
  'const __hydrators = globalThis.__hydrators || (globalThis.__hydrators = {});\n' +
1393
1603
  'const __runtime_comps = __components;\n\n' +
1394
1604
  runtimeGlobals + extraGlobals +
1605
+ // Shared app modules run HERE: after the runtime bindings are published,
1606
+ // before anything can request a route chunk. Earlier (at the top of the
1607
+ // bundle) `globalThis.track` was still undefined, so every module-scope
1608
+ // cell in a store was created against a missing runtime.
1609
+ (sharedModuleBundle ? sharedModuleBundle + '\n' : '') +
1395
1610
  cleanupFn +
1396
1611
  placeFn +
1397
1612
  'globalThis.__components = __components;\n' +
@@ -1 +1 @@
1
- {"version":3,"file":"dev-server.d.ts","sourceRoot":"","sources":["../src/dev-server.ts"],"names":[],"mappings":"AAEA,OAAO,EAAgB,KAAK,MAAM,EAA6C,MAAM,WAAW,CAAC;AASjG,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,OAAO,CAAC;AAK7C,OAAO,KAAK,EAAa,gBAAgB,EAAwB,MAAM,yBAAyB,CAAC;AAgBjG;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,IAAI,GAAG,WAAW,GAAG,OAAO,GAAG,MAAM,CAAC;IAC9C,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,KAAK,EAAE,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACxD,QAAQ,EAAE,OAAO,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB;+EAC2E;IAC3E,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,mEAAmE;AACnE,wBAAgB,kBAAkB,IAAI,WAAW,CAEhD;AAuCD,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,KAAK,GAAG;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CAIzE;AAkBD,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,mHAAmH;IACnH,QAAQ,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAC;IAC7B;;8BAE0B;IAC1B,MAAM,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;CAChC;AAED,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,UAAU,GAAG,YAAY,GAAG,SAAS,GAAG,WAAW,GAAG,QAAQ,CAAC;IACrE,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,WAAW,CAAC,EAAE,MAAM,WAAW,CAAC;IAChC,8EAA8E;IAC9E,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,uBAAuB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3E;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,wBAAwB,GAAG;IACvE,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;CAChH,CAUA;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAa3C;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAIrD;AA8CD,wBAAsB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAwqBvG"}
1
+ {"version":3,"file":"dev-server.d.ts","sourceRoot":"","sources":["../src/dev-server.ts"],"names":[],"mappings":"AAEA,OAAO,EAAgB,KAAK,MAAM,EAA6C,MAAM,WAAW,CAAC;AASjG,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,OAAO,CAAC;AAK7C,OAAO,KAAK,EAAa,gBAAgB,EAAwB,MAAM,yBAAyB,CAAC;AAgBjG;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,IAAI,GAAG,WAAW,GAAG,OAAO,GAAG,MAAM,CAAC;IAC9C,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,KAAK,EAAE,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACxD,QAAQ,EAAE,OAAO,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB;+EAC2E;IAC3E,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,mEAAmE;AACnE,wBAAgB,kBAAkB,IAAI,WAAW,CAEhD;AAuCD,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,KAAK,GAAG;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CAIzE;AAkBD,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,mHAAmH;IACnH,QAAQ,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAC;IAC7B;;8BAE0B;IAC1B,MAAM,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;CAChC;AAED,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,UAAU,GAAG,YAAY,GAAG,SAAS,GAAG,WAAW,GAAG,QAAQ,CAAC;IACrE,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,WAAW,CAAC,EAAE,MAAM,WAAW,CAAC;IAChC,8EAA8E;IAC9E,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,uBAAuB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3E;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,wBAAwB,GAAG;IACvE,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;CAChH,CAUA;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAa3C;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAIrD;AA8CD,wBAAsB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAorBvG"}
@@ -567,6 +567,18 @@ export async function startDevServer(appDir, options) {
567
567
  res.end(lines.join('\n') || '// no ssr data');
568
568
  return;
569
569
  }
570
+ // Persisted-store pre-paint payload — same token-stash/consume contract as
571
+ // /ssr-data.js above so the dev SPA fallback never serves HTML as JS.
572
+ if (url.pathname === '/store-init.js') {
573
+ const token = url.searchParams.get('t') || '';
574
+ const store = globalThis.__vsk_store_init_payload_store;
575
+ const payload = store?.[token];
576
+ if (payload)
577
+ delete store[token];
578
+ res.writeHead(200, { 'Content-Type': 'application/javascript', 'Cache-Control': 'no-store' });
579
+ res.end(payload ?? '// no store init');
580
+ return;
581
+ }
570
582
  if (url.pathname.startsWith('/_vesk/static/')) {
571
583
  const relPath = url.pathname.slice('/_vesk/static/'.length);
572
584
  const staticPath = resolveWithin(resolve(devDir, 'static'), relPath);
package/dist/hmr.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"hmr.d.ts","sourceRoot":"","sources":["../src/hmr.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAUxC,OAAO,EAAsC,KAAK,SAAS,EAAE,MAAM,mCAAmC,CAAC;AAEvG,OAAO,KAAK,EAAE,SAAS,EAAkB,MAAM,yBAAyB,CAAC;AAIzE;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gFAAgF;IAChF,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAaD,wBAAgB,WAAW,IAAI;IAC7B,MAAM,EAAE,IAAI,GAAG,QAAQ,CAAC;IACxB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,KAAK,EAAE,eAAe,GAAG,IAAI,CAAC;IAC9B,QAAQ,EAAE,OAAO,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAQA;AAED,MAAM,WAAW,wBAAwB;IACvC,8EAA8E;IAC9E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wEAAwE;IACxE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,OAAO,EACd,QAAQ,EAAE,MAAM,EAChB,IAAI,GAAE,wBAA6B,GAClC,eAAe,CAsGjB;AAoSD,wBAAgB,eAAe,CAC7B,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,YAAY,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GACjC;IACD,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IAClE,gBAAgB,EAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,EAAE,WAAW,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,SAAS,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACvH,WAAW,EAAE,MAAM,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;CACnD,CA6KA"}
1
+ {"version":3,"file":"hmr.d.ts","sourceRoot":"","sources":["../src/hmr.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAUxC,OAAO,EAAsC,KAAK,SAAS,EAAE,MAAM,mCAAmC,CAAC;AAEvG,OAAO,KAAK,EAAE,SAAS,EAAkB,MAAM,yBAAyB,CAAC;AAIzE;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gFAAgF;IAChF,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAaD,wBAAgB,WAAW,IAAI;IAC7B,MAAM,EAAE,IAAI,GAAG,QAAQ,CAAC;IACxB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,KAAK,EAAE,eAAe,GAAG,IAAI,CAAC;IAC9B,QAAQ,EAAE,OAAO,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAQA;AAED,MAAM,WAAW,wBAAwB;IACvC,8EAA8E;IAC9E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wEAAwE;IACxE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,OAAO,EACd,QAAQ,EAAE,MAAM,EAChB,IAAI,GAAE,wBAA6B,GAClC,eAAe,CAsGjB;AAqTD,wBAAgB,eAAe,CAC7B,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,YAAY,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GACjC;IACD,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IAClE,gBAAgB,EAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,EAAE,WAAW,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,SAAS,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACvH,WAAW,EAAE,MAAM,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;CACnD,CA6KA"}
package/dist/hmr.js CHANGED
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'node:url';
5
5
  import { compileClient } from '@vesk/compiler/src/client-codegen';
6
6
  import { buildHmrEvalSnippet } from './client-bundle';
7
7
  import { resolveComponentName, randomToken } from '@vesk/compiler/src/server-codegen';
8
- import { resolveErrorFile } from '@vesk/adapter/src/ssr-function';
8
+ import { resolveErrorFile, persistedStoreSsrCode } from '@vesk/adapter/src/ssr-function';
9
9
  import { resolveCssUrls, hasBuiltGlobalCss } from '@vesk/adapter/src/css';
10
10
  import { isAllowedWsUpgrade } from '@vesk/adapter/src/paths';
11
11
  import { parseCompilerError, buildCodeframe } from '@vesk/adapter/src/error-codeframe';
@@ -363,6 +363,7 @@ function regenerateSsrFunction(routeNode, appDir, outDir, componentMap, options)
363
363
  // serialized by `renderFullPage` under another and the page would ship
364
364
  // with no ssr-data script.
365
365
  ' return withSsrStore(async () => {',
366
+ ' resetPersistedStoreKeys();',
366
367
  ' let page;',
367
368
  ' let caughtError = null;',
368
369
  ' try {',
@@ -382,7 +383,7 @@ function regenerateSsrFunction(routeNode, appDir, outDir, componentMap, options)
382
383
  " if (_inner.head) _head = _inner.head + _head;",
383
384
  ' }',
384
385
  " const html = await renderFullPage(_layoutSrcList[0], _layoutCompList[0], { params, children: _body }, __componentRegistry, { hydrate: true, cached: _layoutCompiledList[0]" + bakedOptions + clientScriptOption + dataScriptOption + ', pageHead: _head, sourcePath: _layoutPathList[0] });',
385
- " return new Response(html, { headers: { 'Content-Type': 'text/html' }, status: caughtError ? 500 : 200 });",
386
+ " return new Response(__injectStoreInit(html), { headers: { 'Content-Type': 'text/html' }, status: caughtError ? 500 : 200 });",
386
387
  ' });',
387
388
  ].join('\n');
388
389
  }
@@ -390,6 +391,7 @@ function regenerateSsrFunction(routeNode, appDir, outDir, componentMap, options)
390
391
  renderCode = [
391
392
  ' return withSsrStore(async () => {',
392
393
  ' let stream;',
394
+ " const __streamToken = globalThis.__vsk_ssr_token || '';",
393
395
  ' try {',
394
396
  ' stream = renderPageStream(_src, _comp, { params }, __componentRegistry, { hydrate: true, cached: _srcCompiled' + bakedOptions + clientScriptOption + dataScriptOption + ", sourcePath: _srcPath });",
395
397
  ' } catch (err) {',
@@ -398,13 +400,26 @@ function regenerateSsrFunction(routeNode, appDir, outDir, componentMap, options)
398
400
  " const message = err && typeof err === 'object' && 'message' in err ? String(err.message) : String(err);",
399
401
  " const stack = err && typeof err === 'object' && 'stack' in err ? String(err.stack) : '';",
400
402
  " const html = await renderFullPage(_errorSrc, _errorComp, { params, statusCode: 500, error: message, stack, url: url.href }, __componentRegistry, { hydrate: true, cached: _errorCompiled" + bakedOptions + clientScriptOption + dataScriptOption + ', sourcePath: _errorPath });',
401
- " return new Response(html, { headers: { 'Content-Type': 'text/html' }, status: 500 });",
403
+ " return new Response(__injectStoreInit(html), { headers: { 'Content-Type': 'text/html' }, status: 500 });",
402
404
  ' }',
403
405
  ' return new Response(new ReadableStream({',
404
406
  ' async start(controller) {',
405
407
  ' const enc = new TextEncoder();',
408
+ " let __tail = null;",
409
+ ' resetPersistedStoreKeys(__streamToken);',
406
410
  ' for await (const chunk of stream) {',
407
- ' controller.enqueue(enc.encode(chunk));',
411
+ ' if (__tail !== null) controller.enqueue(enc.encode(__tail));',
412
+ ' __tail = chunk;',
413
+ ' }',
414
+ ' if (__tail !== null) {',
415
+ ' const __tag = __storeInitTag(__streamToken);',
416
+ " if (__tag !== '') {",
417
+ " const __bi = __tail.lastIndexOf('</body>');",
418
+ " __tail = __bi === -1 ? __tail + __tag : __tail.slice(0, __bi) + __tag + __tail.slice(__bi);",
419
+ ' }',
420
+ ' controller.enqueue(enc.encode(__tail));',
421
+ ' } else {',
422
+ ' resetPersistedStoreKeys(__streamToken);',
408
423
  ' }',
409
424
  ' controller.close();',
410
425
  ' },',
@@ -424,10 +439,12 @@ function regenerateSsrFunction(routeNode, appDir, outDir, componentMap, options)
424
439
  '}',
425
440
  ].join('\n');
426
441
  const funcCode = [
427
- "import { renderFullPage, renderPageStream, renderPage, compileFile, setVskHydrate, storeDataScriptGlobal, withSsrStore } from '../runtime.js';",
442
+ "import { renderFullPage, renderPageStream, renderPage, compileFile, setVskHydrate, storeDataScriptGlobal, withSsrStore, installPersistedStoreServerHooks, persistedStoreCookieName, getPersistedStoreKeys, resetPersistedStoreKeys, buildStorePrePaintPayload, storeInitScriptGlobal, storeInitScriptTag } from '../runtime.js';",
428
443
  '', registryCode, src, '',
429
444
  errorBodyFnCode,
430
445
  '',
446
+ persistedStoreSsrCode(),
447
+ '',
431
448
  'export async function handle(request) {',
432
449
  ' const url = new URL(request.url);',
433
450
  " const urlParts = url.pathname.split('/').filter(Boolean);",
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EACe,YAAY,EAAE,WAAW,EAC7B,UAAU,EAChC,MAAM,yBAAyB,CAAC;AAiBjC;;;;;;GAMG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,OAAO,CAAC;CACjB;AACD,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,iBAAiB,EAAE,CAAC;CAC9B;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,UAAU,EAAE,EACrB,OAAO,EAAE,iBAAiB,EAAE,GAAG,IAAI,GAAG,SAAS,GAC9C,UAAU,EAAE,CAQd;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,UAAU,EAAE,EACrB,KAAK,EAAE,eAAe,GAAG,IAAI,GAAG,SAAS,GACxC,UAAU,EAAE,CAGd;AAkDD,wBAAsB,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CAsWpG;AAED,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAIhE,OAAO,EACL,kBAAkB,EAClB,oBAAoB,EACpB,yBAAyB,EACzB,eAAe,GAChB,MAAM,2BAA2B,CAAC;AACnC,YAAY,EACV,mBAAmB,EACnB,YAAY,EACZ,kBAAkB,EAClB,cAAc,EACd,iBAAiB,EACjB,gBAAgB,EAChB,aAAa,EACb,aAAa,GACd,MAAM,2BAA2B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EACe,YAAY,EAAE,WAAW,EAC7B,UAAU,EAChC,MAAM,yBAAyB,CAAC;AAoDjC;;;;;;GAMG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,OAAO,CAAC;CACjB;AACD,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,iBAAiB,EAAE,CAAC;CAC9B;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,UAAU,EAAE,EACrB,OAAO,EAAE,iBAAiB,EAAE,GAAG,IAAI,GAAG,SAAS,GAC9C,UAAU,EAAE,CAQd;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,UAAU,EAAE,EACrB,KAAK,EAAE,eAAe,GAAG,IAAI,GAAG,SAAS,GACxC,UAAU,EAAE,CAGd;AAkDD,wBAAsB,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CAiXpG;AAED,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAIhE,OAAO,EACL,kBAAkB,EAClB,oBAAoB,EACpB,yBAAyB,EACzB,eAAe,GAChB,MAAM,2BAA2B,CAAC;AACnC,YAAY,EACV,mBAAmB,EACnB,YAAY,EACZ,kBAAkB,EAClB,cAAc,EACd,iBAAiB,EACjB,gBAAgB,EAChB,aAAa,EACb,aAAa,GACd,MAAM,2BAA2B,CAAC"}
package/dist/index.js CHANGED
@@ -28,6 +28,38 @@ async function resolveCompilerApi(name) {
28
28
  }
29
29
  return import(`@vesk/compiler/src/${name.replace(/\.js$/, '')}`);
30
30
  }
31
+ /**
32
+ * Build gate against two page files claiming one URL.
33
+ *
34
+ * SSR routes are keyed by URL pattern (fullPath) — last write wins in the
35
+ * path->file map — while the client router walks the manifest tree first-match
36
+ * wins, so a group page and a root page both claiming `/` ship a document for
37
+ * one of them and hydrate the other. Both respond 200; the only symptom is a
38
+ * page whose claims land on the layout's elements. Refuse the build instead.
39
+ */
40
+ function findRouteConflicts(routeTree, appDir) {
41
+ const projectRoot = resolve(appDir, '..');
42
+ const byPath = new Map();
43
+ const walk = (nodes) => {
44
+ for (const node of nodes) {
45
+ if (node.page) {
46
+ const pageFile = join(node.sourceDir, 'page.vsk');
47
+ const files = byPath.get(node.fullPath) ?? [];
48
+ files.push(relative(projectRoot, pageFile));
49
+ byPath.set(node.fullPath, files);
50
+ }
51
+ if (node.children && node.children.length > 0)
52
+ walk(node.children);
53
+ }
54
+ };
55
+ walk(routeTree);
56
+ const conflicts = [];
57
+ for (const [fullPath, files] of byPath) {
58
+ if (files.length > 1)
59
+ conflicts.push({ fullPath, files });
60
+ }
61
+ return conflicts;
62
+ }
31
63
  /**
32
64
  * Defensive local mirror of the plugin-manager's `filterActivePlugins`
33
65
  * (`@vesk/adapter/src/plugins`): keep when there is no matching record, or the
@@ -214,6 +246,14 @@ export async function build(appDir, options) {
214
246
  console.error('vesk build: no routes found in', appDir);
215
247
  return;
216
248
  }
249
+ const routeConflicts = findRouteConflicts(routeTree, appDir);
250
+ if (routeConflicts.length > 0) {
251
+ for (const conflict of routeConflicts) {
252
+ console.error(`vesk build: conflicting routes for ${conflict.fullPath}: ${conflict.files.join(', ')}`);
253
+ }
254
+ const first = routeConflicts[0];
255
+ throw new Error(`conflicting routes: ${first.fullPath} is claimed by both ${first.files.join(' and ')}`);
256
+ }
217
257
  const projectRoot = resolve(appDir, '..');
218
258
  const componentsDir = resolve(projectRoot, 'components');
219
259
  const componentMap = scanComponents(componentsDir);
@@ -1 +1 @@
1
- {"version":3,"file":"platform-handler.d.ts","sourceRoot":"","sources":["../src/platform-handler.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAIvE,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,SAAS,EAAE,CAAC;IACvB,SAAS,EAAE,YAAY,EAAE,CAAC;IAC1B,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,aAAa,EAAE,OAAO,CAAC;IACvB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,iFAAiF;IACjF,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAM,CAMpD;AAED,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAGrD;AAED,wBAAgB,IAAI,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAEtC;AAgBD;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,oBAAoB,GAAG,MAAM,CAwLjF;AAED,MAAM,WAAW,qBAAqB;IACpC,kEAAkE;IAClE,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,wBAAsB,qBAAqB,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAsFzF;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,SAAS,EAAE,CAAC;IACvB,SAAS,EAAE,YAAY,EAAE,CAAC;IAC1B,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,aAAa,EAAE,OAAO,CAAC;IACvB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB"}
1
+ {"version":3,"file":"platform-handler.d.ts","sourceRoot":"","sources":["../src/platform-handler.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAIvE,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,SAAS,EAAE,CAAC;IACvB,SAAS,EAAE,YAAY,EAAE,CAAC;IAC1B,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,aAAa,EAAE,OAAO,CAAC;IACvB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,iFAAiF;IACjF,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAM,CAMpD;AAED,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAGrD;AAED,wBAAgB,IAAI,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAEtC;AAgBD;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,oBAAoB,GAAG,MAAM,CAsMjF;AAED,MAAM,WAAW,qBAAqB;IACpC,kEAAkE;IAClE,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,wBAAsB,qBAAqB,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAsFzF;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,SAAS,EAAE,CAAC;IACvB,SAAS,EAAE,YAAY,EAAE,CAAC;IAC1B,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,aAAa,EAAE,OAAO,CAAC;IACvB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB"}
@@ -154,6 +154,20 @@ ${serverStoreLine}
154
154
  });
155
155
  }
156
156
 
157
+ // Persisted-store pre-paint payload (buildStorePrePaintPayload) — same
158
+ // token-stash/consume contract as /ssr-data.js, own store so neither handoff
159
+ // can eat the other.
160
+ if (pathname === '/store-init.js') {
161
+ const initToken = url.searchParams.get('t') || '';
162
+ const initStore = (globalThis.__vsk_store_init_payload_store || {});
163
+ const initPayload = initStore[initToken];
164
+ if (initPayload !== undefined) delete initStore[initToken];
165
+ return new Response(initPayload ?? '// no store init', {
166
+ status: 200,
167
+ headers: { 'Content-Type': 'application/javascript', 'Cache-Control': 'no-store' },
168
+ });
169
+ }
170
+
157
171
  ${eventsStartLine}
158
172
  let mwCtx = { params: {}, url, locals: ${localsSeed}, cookies: {}, request, resolveUrl(u) { return new URL(u, request.url).href; }, set() {}, get() { return undefined; } };
159
173
  if (${hasMwLiteral}) {
@@ -1 +1 @@
1
- {"version":3,"file":"prod-server.d.ts","sourceRoot":"","sources":["../src/prod-server.ts"],"names":[],"mappings":"AAEA,OAAO,EAA2D,KAAK,MAAM,EAAE,MAAM,WAAW,CAAC;AAiDjG,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,KAAK,GAAG;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CAIzE;AAkGD,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAwdxI"}
1
+ {"version":3,"file":"prod-server.d.ts","sourceRoot":"","sources":["../src/prod-server.ts"],"names":[],"mappings":"AAEA,OAAO,EAA2D,KAAK,MAAM,EAAE,MAAM,WAAW,CAAC;AAiDjG,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,KAAK,GAAG;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CAIzE;AAkGD,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAqexI"}
@@ -290,6 +290,19 @@ export async function startProdServer(outDir, options) {
290
290
  res.end(lines.join('\n') || '// no ssr data');
291
291
  return;
292
292
  }
293
+ // Persisted-store pre-paint payload (buildStorePrePaintPayload), stashed by
294
+ // storeInitScriptGlobal under its own token store so consuming one never
295
+ // eats the page's /ssr-data.js handoff. Served once, consumed-and-deleted.
296
+ if (url.pathname === '/store-init.js') {
297
+ const token = url.searchParams.get('t') || '';
298
+ const store = globalThis.__vsk_store_init_payload_store;
299
+ const payload = store?.[token];
300
+ if (payload)
301
+ delete store[token];
302
+ res.writeHead(200, { 'Content-Type': 'application/javascript', 'Cache-Control': 'no-store' });
303
+ res.end(payload ?? '// no store init');
304
+ return;
305
+ }
293
306
  // Stable alias for the client runtime: the file on disk is content-hashed in
294
307
  // production, so the name is resolved from the manifest rather than assumed.
295
308
  if (url.pathname === '/_vesk/runtime.js') {
@@ -1 +1 @@
1
- {"version":3,"file":"runtime-bundle.d.ts","sourceRoot":"","sources":["../src/runtime-bundle.ts"],"names":[],"mappings":"AAuCA,wBAAsB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA4JnF"}
1
+ {"version":3,"file":"runtime-bundle.d.ts","sourceRoot":"","sources":["../src/runtime-bundle.ts"],"names":[],"mappings":"AAuCA,wBAAsB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA+LnF"}
@@ -42,6 +42,7 @@ export async function bundleRuntime(appDir, outDir) {
42
42
  `import { renderPage, renderFullPage, renderPageStream, compileFile, setRuntimeModule, setVskHydrate, hydratePrecompile, assertSameOrigin, withSsrStore } from ${JSON.stringify(resolve(compilerRoot, 'server-codegen.js'))};`,
43
43
  `import { parseCookies } from ${JSON.stringify(resolve(compilerRoot, 'server-cookies.js'))};`,
44
44
  `import * as __veskRuntime from ${JSON.stringify(resolve(runtimeRoot, 'index-server.js'))};`,
45
+ `import * as __persistence from ${JSON.stringify(resolve(runtimeRoot, 'store-persistence.js'))};`,
45
46
  '',
46
47
  '// Inject runtime module so server-codegen can find components like NavLink, Link, etc.',
47
48
  'setRuntimeModule(__veskRuntime);',
@@ -107,6 +108,40 @@ export async function bundleRuntime(appDir, outDir) {
107
108
  " return '/ssr-data.js?t=' + token;",
108
109
  '}',
109
110
  '',
111
+ '// Persisted-store SSR continuity. `installPersistedStoreServerHooks` installs the',
112
+ '// adapter-side read hook; `get/getReset` handle the per-request key registry',
113
+ '// the server drains into the pre-paint payload. The init payload is delivered',
114
+ '// as an origin-served script (/store-init.js?t=...) exactly like /ssr-data.js',
115
+ '// so strict CSP (no unsafe-inline) cannot block it; it is never inlined.',
116
+ 'export const installPersistedStoreServerHooks = __persistence.installPersistedStoreServerHooks;',
117
+ 'export const persistedStoreCookieName = __persistence.persistedStoreCookieName;',
118
+ 'export const getPersistedStoreKeys = __persistence.getPersistedStoreKeys;',
119
+ 'export const resetPersistedStoreKeys = __persistence.resetPersistedStoreKeys;',
120
+ 'export const buildStorePrePaintPayload = __persistence.buildStorePrePaintPayload;',
121
+ 'export const storeInitScriptTag = __persistence.storeInitScriptTag;',
122
+ '',
123
+ '// Stash the pre-paint payload under a CSPRNG token and return the origin URL',
124
+ '// the blocking <head> script points at. Own token store (NOT __vsk_ssr_payload_store)',
125
+ '// so consuming the page hydration handoff never eats the store-init payload',
126
+ '// and vice versa. Same 100-entry bound as /ssr-data.js.',
127
+ 'export function storeInitScriptGlobal(payload) {',
128
+ " if (!payload) return null;",
129
+ ' const bytes = new Uint8Array(12);',
130
+ ' crypto.getRandomValues(bytes);',
131
+ " let tk = '';",
132
+ " for (const b of bytes) tk += b.toString(16).padStart(2, '0');",
133
+ ' const token = tk;',
134
+ " const store = (globalThis.__vsk_store_init_payload_store ||= {});",
135
+ " store[token] = payload;",
136
+ " const keys = Object.keys(store);",
137
+ ' if (keys.length > 100) {',
138
+ ' for (let i = 0; i < keys.length - 100; i++) {',
139
+ ' delete store[keys[i]];',
140
+ ' }',
141
+ ' }',
142
+ " return '/store-init.js?t=' + token;",
143
+ '}',
144
+ '',
110
145
  '// Re-export runtime classes used by API routes',
111
146
  'export const VeskRequest = __veskRuntime.VeskRequest;',
112
147
  'export const PayloadTooLargeError = __veskRuntime.PayloadTooLargeError;',
@@ -1,5 +1,10 @@
1
1
  import type { RouteNode, SsrFunctionOptions } from '@vesk/adapter/src/types';
2
2
  export declare function resolveErrorFile(sourceDir: string, appDir: string): string | null;
3
+ /** Shared persisted-store SSR helper block emitted into every generated SSR
4
+ * function (prod via ssr-function.ts, dev via hmr.ts — keep the two callers
5
+ * in sync by calling this). Emit it AFTER the function declarations so the
6
+ * module-level `installPersistedStoreServerHooks` call runs on import. */
7
+ export declare function persistedStoreSsrCode(): string;
3
8
  export declare function generateSsrFunction(routeNode: RouteNode, appDir: string, outDir: string, componentMap?: Map<string, string>, options?: SsrFunctionOptions): {
4
9
  funcPath: string;
5
10
  funcCode: string;
@@ -1 +1 @@
1
- {"version":3,"file":"ssr-function.d.ts","sourceRoot":"","sources":["../src/ssr-function.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAkB,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAI7F,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAQjF;AAgED,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,SAAS,EACpB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,YAAY,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAClC,OAAO,CAAC,EAAE,kBAAkB,GAC3B;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAwZtD"}
1
+ {"version":3,"file":"ssr-function.d.ts","sourceRoot":"","sources":["../src/ssr-function.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAkB,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAI7F,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAQjF;AAgED;;;2EAG2E;AAC3E,wBAAgB,qBAAqB,IAAI,MAAM,CA4C9C;AAED,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,SAAS,EACpB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,YAAY,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAClC,OAAO,CAAC,EAAE,kBAAkB,GAC3B;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CA4btD"}
@@ -77,6 +77,55 @@ function buildParamExtraction(node, urlParts) {
77
77
  walk(node);
78
78
  return parts;
79
79
  }
80
+ /** Shared persisted-store SSR helper block emitted into every generated SSR
81
+ * function (prod via ssr-function.ts, dev via hmr.ts — keep the two callers
82
+ * in sync by calling this). Emit it AFTER the function declarations so the
83
+ * module-level `installPersistedStoreServerHooks` call runs on import. */
84
+ export function persistedStoreSsrCode() {
85
+ return [
86
+ '// ── Persisted-store SSR continuity (createStore / createPathStore persist) ──',
87
+ '// Reads a persisted store\'s cookie value through the adapter request context.',
88
+ '// Client writes mirror URL-encoded; decode on the way back (best effort).',
89
+ 'function __persistRead(key) {',
90
+ ' const cookieName = persistedStoreCookieName(key);',
91
+ ' const req = globalThis.__vesk_request;',
92
+ ' if (!req) return null;',
93
+ ' const c = req.cookies || {};',
94
+ ' const v = typeof c.get === \'function\' ? c.get(cookieName) : c[cookieName];',
95
+ ' if (v == null) return null;',
96
+ ' try { return decodeURIComponent(String(v)); } catch { return String(v); }',
97
+ '}',
98
+ '',
99
+ '// Drain this request\'s registered persisted keys into a served pre-paint',
100
+ '// script (/store-init.js?t=...) then forget them. `token` selects the',
101
+ '// render\'s bucket for callers outside the ALS scope (the no-layout stream',
102
+ '// path captures its token at the renderPageStream call site); the layout',
103
+ '// path runs this inside the scope and relies on the ambient token.',
104
+ 'function __storeInitTag(token) {',
105
+ ' const keys = getPersistedStoreKeys(token);',
106
+ ' if (keys.length === 0) { resetPersistedStoreKeys(token); return \'\'; }',
107
+ ' const values = {};',
108
+ ' for (let __i = 0; __i < keys.length; __i++) values[keys[__i]] = __persistRead(keys[__i]);',
109
+ ' resetPersistedStoreKeys(token);',
110
+ ' const payload = buildStorePrePaintPayload(values);',
111
+ ' const url = storeInitScriptGlobal(payload);',
112
+ ' return url ? storeInitScriptTag(url) : \'\';',
113
+ '}',
114
+ '',
115
+ '// Inject the blocking store-init <script src> into a full HTML string',
116
+ '// (before </head>; append if the document has no head).',
117
+ 'function __injectStoreInit(html) {',
118
+ ' const tag = __storeInitTag();',
119
+ ' if (tag === \'\') return html;',
120
+ ' const idx = html.lastIndexOf(\'</head>\');',
121
+ ' if (idx === -1) return html + tag;',
122
+ ' return html.slice(0, idx) + tag + html.slice(idx);',
123
+ '}',
124
+ '',
125
+ 'installPersistedStoreServerHooks({ read: __persistRead });',
126
+ '',
127
+ ].join('\n');
128
+ }
80
129
  export function generateSsrFunction(routeNode, appDir, outDir, componentMap, options) {
81
130
  const ancestorLayouts = options?.ancestorLayouts || [];
82
131
  const middlewareCode = options?.middlewareCode || null;
@@ -167,6 +216,7 @@ export function generateSsrFunction(routeNode, appDir, outDir, componentMap, opt
167
216
  '',
168
217
  'async function __renderHtml(params, requestUrl) {',
169
218
  ' return withSsrStore(async () => {',
219
+ ' resetPersistedStoreKeys();',
170
220
  ' let page;',
171
221
  ' let caughtError = null;',
172
222
  " const __expose = process.env.NODE_ENV !== 'production';",
@@ -187,7 +237,7 @@ export function generateSsrFunction(routeNode, appDir, outDir, componentMap, opt
187
237
  ' if (_inner.head) _head = _inner.head + _head;',
188
238
  ' }',
189
239
  " const html = await renderFullPage('', _layoutCompList[0], { params, children: _body }, __componentRegistry, { hydrate: true, cached: _layoutCompiledList[0]" + bakedOptions + clientScriptOption + dataScriptOption + ", pageHead: _head });",
190
- " return new Response(html, { headers: { 'Content-Type': 'text/html' }, status: caughtError ? 500 : 200 });",
240
+ " return new Response(__injectStoreInit(html), { headers: { 'Content-Type': 'text/html' }, status: caughtError ? 500 : 200 });",
191
241
  ' });',
192
242
  '}',
193
243
  '',
@@ -207,25 +257,48 @@ export function generateSsrFunction(routeNode, appDir, outDir, componentMap, opt
207
257
  'async function __renderHtml(params, requestUrl) {',
208
258
  ' return withSsrStore(async () => {',
209
259
  ' let stream;',
260
+ // The streamed no-layout page emits its <head> before the component body
261
+ // renders, so its persisted stores don't exist yet when the head is
262
+ // flushed — the pre-paint tag therefore goes into the final chunk, right
263
+ // before </body> (still a blocking, pre-paint script). Capture the render
264
+ // token now (renderPageStream adopts this exact ambient store) so the
265
+ // registry drain in the consumer's start() can hit the right bucket even
266
+ // though it runs outside the ALS scope.
267
+ " const __streamToken = globalThis.__vsk_ssr_token || '';",
210
268
  ' try {',
211
269
  " stream = renderPageStream('', _comp, { params }, __componentRegistry, { hydrate: true, cached: _compiled" + bakedOptions + clientScriptOption + dataScriptOption + ' });',
212
270
  ' } catch (err) {',
213
271
  ' if (err && (err.name === \'NotFoundError\' || err.name === \'Redirect\')) throw err;',
214
272
  ' const html = await __renderErrorFullPage(params, requestUrl, err);',
215
- " return new Response(html, { headers: { 'Content-Type': 'text/html' }, status: 500 });",
273
+ " return new Response(__injectStoreInit(html), { headers: { 'Content-Type': 'text/html' }, status: 500 });",
216
274
  ' }',
217
275
  ' return new Response(new ReadableStream({',
218
276
  ' async start(controller) {',
219
277
  ' const enc = new TextEncoder();',
278
+ " let __tail = null;",
279
+ ' resetPersistedStoreKeys(__streamToken);',
220
280
  ' try {',
221
281
  ' for await (const chunk of stream) {',
222
- ' controller.enqueue(enc.encode(chunk));',
282
+ ' if (__tail !== null) controller.enqueue(enc.encode(__tail));',
283
+ ' __tail = chunk;',
284
+ ' }',
285
+ ' if (__tail !== null) {',
286
+ ' const __tag = __storeInitTag(__streamToken);',
287
+ ' if (__tag !== \'\') {',
288
+ ' const __bi = __tail.lastIndexOf(\'</body>\');',
289
+ ' __tail = __bi === -1 ? __tail + __tag : __tail.slice(0, __bi) + __tag + __tail.slice(__bi);',
290
+ ' }',
291
+ ' controller.enqueue(enc.encode(__tail));',
292
+ ' } else {',
293
+ ' // Empty stream (no chunks) — nothing to attach the tag to; just',
294
+ ' // clear the registry so a later render can\'t inherit these keys.',
295
+ ' resetPersistedStoreKeys(__streamToken);',
223
296
  ' }',
224
297
  ' } catch (err) {',
225
298
  ' if (err && (err.name === \'NotFoundError\' || err.name === \'Redirect\')) throw err;',
226
299
  ' try {',
227
300
  ' const html = await __renderErrorFullPage(params, requestUrl, err);',
228
- ' controller.enqueue(enc.encode(html));',
301
+ ' controller.enqueue(enc.encode(__injectStoreInit(html)));',
229
302
  ' } catch {}',
230
303
  ' }',
231
304
  ' controller.close();',
@@ -236,6 +309,12 @@ export function generateSsrFunction(routeNode, appDir, outDir, componentMap, opt
236
309
  '',
237
310
  ].join('\n');
238
311
  }
312
+ // Persisted-store SSR continuity (createStore / createPathStore persist).
313
+ // The adapter-installed read hook pulls each store's cookie value through the
314
+ // request context; the tag the generated pages emit points at an origin-served
315
+ // /store-init.js?t=<token> file (CSP-safe, never inlined — see
316
+ // buildStorePrePaintPayload) so the client can pre-paint the user's value.
317
+ const persistCode = persistedStoreSsrCode();
239
318
  let dataCode;
240
319
  const exposeErr = "process.env.NODE_ENV !== 'production'";
241
320
  // SPA data-nav renders page + layouts through renderPage and never runs
@@ -254,6 +333,9 @@ export function generateSsrFunction(routeNode, appDir, outDir, componentMap, opt
254
333
  // process-global token — the cross-request data-slot race, for good. The
255
334
  // token dies with the withSsrStore scope wrapped around this block instead.
256
335
  ' delete globalThis.__vsk_ssr_data;',
336
+ // SPA data-nav only runs renderPage, so the persisted-store registration
337
+ // bucket would otherwise keep this request's keys forever.
338
+ ' resetPersistedStoreKeys();',
257
339
  ' }',
258
340
  ];
259
341
  // The data-nav branch never runs renderFullPage/renderPageStream, so it needs
@@ -446,7 +528,7 @@ export function generateSsrFunction(routeNode, appDir, outDir, componentMap, opt
446
528
  '',
447
529
  ].join('\n');
448
530
  const funcCode = [
449
- "import { renderFullPage, renderPageStream, renderPage, hydratePrecompile, parseCookies, getAction, validateActionInput, issuesToFieldMap, storeDataScriptGlobal, withSsrStore, assertSameOrigin, VeskRequest } from '../runtime.js';",
531
+ "import { renderFullPage, renderPageStream, renderPage, hydratePrecompile, parseCookies, getAction, validateActionInput, issuesToFieldMap, storeDataScriptGlobal, withSsrStore, assertSameOrigin, VeskRequest, installPersistedStoreServerHooks, persistedStoreCookieName, getPersistedStoreKeys, resetPersistedStoreKeys, buildStorePrePaintPayload, storeInitScriptGlobal, storeInitScriptTag } from '../runtime.js';",
450
532
  '',
451
533
  middlewareCode || '',
452
534
  registryCode,
@@ -455,6 +537,8 @@ export function generateSsrFunction(routeNode, appDir, outDir, componentMap, opt
455
537
  paramsCode,
456
538
  htmlFnCode,
457
539
  '',
540
+ persistCode,
541
+ '',
458
542
  'export async function handle(request) {',
459
543
  ' const url = new URL(request.url);',
460
544
  ' const params = __paramsFor(url.pathname);',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vesk/adapter",
3
- "version": "0.2.58",
3
+ "version": "0.2.60",
4
4
  "type": "module",
5
5
  "description": "Vesk adapter — builds deployable .vesk/ output for Deno and Node serverless",
6
6
  "main": "./dist/index.js",
@@ -23,9 +23,9 @@
23
23
  "sharp": "^0.35.3"
24
24
  },
25
25
  "dependencies": {
26
- "@vesk/compiler": "^0.2.58",
27
- "@vesk/runtime": "^0.2.58",
28
- "@vesk/types": "^0.2.58"
26
+ "@vesk/compiler": "^0.2.60",
27
+ "@vesk/runtime": "^0.2.60",
28
+ "@vesk/types": "^0.2.60"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@types/ws": "^8.18.1",