@vesk/adapter 0.2.22 → 0.2.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"client-bundle.d.ts","sourceRoot":"","sources":["../src/client-bundle.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,SAAS,EAAE,mBAAmB,EAAE,kBAAkB,EAAqC,MAAM,yBAAyB,CAAC;AAoCrI;;;;;;;;;;;;;GAaG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CA8BhE;AAmUD;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAkBxD;AAmCD,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,CAuX7B;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;AAYD;;;;;;;;GAQG;AACH,wBAAsB,sBAAsB,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAuCrG"}
1
+ {"version":3,"file":"client-bundle.d.ts","sourceRoot":"","sources":["../src/client-bundle.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,SAAS,EAAE,mBAAmB,EAAE,kBAAkB,EAAmF,MAAM,yBAAyB,CAAC;AAoCnL;;;;;;;;;;;;;GAaG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CA8BhE;AAoWD;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAkBxD;AAmCD,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,CA4Z7B;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;AAYD;;;;;;;;GAQG;AACH,wBAAsB,sBAAsB,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAuCrG"}
@@ -197,14 +197,15 @@ function demoteExports(code) {
197
197
  code = code.slice(0, e.start) + e.text + code.slice(e.end);
198
198
  return code;
199
199
  }
200
- function hasChunkImports(code) {
201
- try {
202
- const ast = parse(code);
203
- return (ast.body ?? []).some((n) => n.type === 'ImportDeclaration');
204
- }
205
- catch {
206
- return false;
207
- }
200
+ /**
201
+ * Parse-free stand-in for the per-chunk `import` scan: a chunk carries live
202
+ * `import` statements iff its deduped head rendered non-empty, or its import
203
+ * merge aborted (in which case some file bodies retain their raw imports for
204
+ * esbuild to bundle). Equivalent to parsing the assembled chunk code, without
205
+ * the ~50-100KB acorn parse per chunk on every dev rebuild.
206
+ */
207
+ function chunkHasImports(headLen, mergeAborted) {
208
+ return headLen > 0 || mergeAborted;
208
209
  }
209
210
  /**
210
211
  * Rewrites a compiled file's source specifier so esbuild can resolve it from
@@ -228,8 +229,8 @@ function resolveImportSource(src, filePath) {
228
229
  * each file's component AND hydrator both keep their imports verbatim. So the
229
230
  * same named binding frequently appears multiple times in one chunk — once per
230
231
  * (file, comp|hyd) pair, plus once per extra file. acorn's `parse()` rejects
231
- * that with `Identifier has already been declared`, so `hasChunkImports` used
232
- * to silently return false and esbuild — which rejects the same duplicates —
232
+ * that with `Identifier has already been declared`, so a naive `import` scan
233
+ * used to silently pass and esbuild — which rejects the same duplicates —
233
234
  * never ran; the chunk shipped with raw `import` statements inside its
234
235
  * classic-script IIFE and hydration died with `SyntaxError: Cannot use import
235
236
  * statement outside a module`.
@@ -251,7 +252,7 @@ class ChunkImports {
251
252
  return code;
252
253
  }
253
254
  /** Folds one file's imports into the merged set and returns the code with its imports stripped. */
254
- add(code, filePath) {
255
+ add(code, filePath, specSink) {
255
256
  if (!code || this.aborted)
256
257
  return code;
257
258
  let ast;
@@ -273,34 +274,28 @@ class ChunkImports {
273
274
  if (typeof src !== 'string')
274
275
  continue;
275
276
  const resolved = resolveImportSource(src, filePath);
276
- let group = this.bySource.get(resolved);
277
- if (!group) {
278
- group = { named: new Map() };
279
- this.bySource.set(resolved, group);
280
- }
281
277
  for (const spec of node.specifiers ?? []) {
282
278
  const s = spec;
283
279
  const local = s.local?.name;
284
280
  if (typeof local !== 'string')
285
281
  return this.fail(`nameless specifier from '${resolved}' in ${filePath}`, code);
286
282
  if (s.type === 'ImportDefaultSpecifier') {
287
- if (group.default && group.default.local !== local)
288
- return this.fail(`conflicting default imports from '${resolved}' in ${filePath}`, code);
289
- group.default = { kind: 'default', local };
283
+ if (!this.foldSpec(resolved, 'default', local, undefined, filePath, code))
284
+ return code;
285
+ specSink?.push({ source: resolved, kind: 'default', local });
290
286
  }
291
287
  else if (s.type === 'ImportNamespaceSpecifier') {
292
- if (group.namespace && group.namespace.local !== local)
293
- return this.fail(`conflicting namespace imports from '${resolved}' in ${filePath}`, code);
294
- group.namespace = { kind: 'namespace', local };
288
+ if (!this.foldSpec(resolved, 'namespace', local, undefined, filePath, code))
289
+ return code;
290
+ specSink?.push({ source: resolved, kind: 'namespace', local });
295
291
  }
296
292
  else if (s.type === 'ImportSpecifier') {
297
293
  const imported = s.imported?.name;
298
294
  if (typeof imported !== 'string')
299
295
  return this.fail(`nameless named-import from '${resolved}' in ${filePath}`, code);
300
- const existing = group.named.get(imported);
301
- if (existing && existing.local !== local)
302
- return this.fail(`'${imported}' from '${resolved}' bound to both '${existing.local}' and '${local}' in ${filePath}`, code);
303
- group.named.set(imported, { kind: 'named', local, imported });
296
+ if (!this.foldSpec(resolved, 'named', local, imported, filePath, code))
297
+ return code;
298
+ specSink?.push({ source: resolved, kind: 'named', local, imported });
304
299
  }
305
300
  }
306
301
  let cut = node.end;
@@ -312,6 +307,45 @@ class ChunkImports {
312
307
  return code;
313
308
  return removeRanges(code, ranges);
314
309
  }
310
+ /**
311
+ * Shared single-spec fold used by both the fresh compile path and the
312
+ * parse-free warm replay. Returns false (and aborts the merge) on a
313
+ * conflicting binding — same shape as the pre-refactor inline logic.
314
+ */
315
+ foldSpec(source, kind, local, imported, filePath, code) {
316
+ let group = this.bySource.get(source);
317
+ if (!group) {
318
+ group = { named: new Map() };
319
+ this.bySource.set(source, group);
320
+ }
321
+ if (kind === 'default') {
322
+ if (group.default && group.default.local !== local) {
323
+ this.fail(`conflicting default imports from '${source}' in ${filePath}`, code);
324
+ return false;
325
+ }
326
+ group.default = { kind: 'default', local };
327
+ }
328
+ else if (kind === 'namespace') {
329
+ if (group.namespace && group.namespace.local !== local) {
330
+ this.fail(`conflicting namespace imports from '${source}' in ${filePath}`, code);
331
+ return false;
332
+ }
333
+ group.namespace = { kind: 'namespace', local };
334
+ }
335
+ else {
336
+ if (typeof imported !== 'string') {
337
+ this.fail(`nameless named-import from '${source}' in ${filePath}`, code);
338
+ return false;
339
+ }
340
+ const existing = group.named.get(imported);
341
+ if (existing && existing.local !== local) {
342
+ this.fail(`'${imported}' from '${source}' bound to both '${existing.local}' and '${local}' in ${filePath}`, code);
343
+ return false;
344
+ }
345
+ group.named.set(imported, { kind: 'named', local, imported });
346
+ }
347
+ return true;
348
+ }
315
349
  /** True when every file parsed and merged cleanly (no conflict, no parse abort). */
316
350
  get ok() {
317
351
  return !this.aborted;
@@ -352,6 +386,16 @@ function removeRanges(code, ranges) {
352
386
  }
353
387
  return out.trim();
354
388
  }
389
+ /**
390
+ * Historical cache replay: re-parse a cached entry's import-carrying codes
391
+ * and re-fold them into the chunk accumulator. Used only on rare fold aborts
392
+ * and old-style cache entries — the parse-free fast path is the norm.
393
+ */
394
+ function fallbackReplay(cached, filePath, imports) {
395
+ const comp = cached.compCode ? imports.add(cached.compCode, filePath).replace(/^\n+/, '').replace(/\n+$/, '') : '';
396
+ const hyd = cached.hydCode ? imports.add(cached.hydCode, filePath).replace(/__components/g, '__hydrators').replace(/^\n+/, '').replace(/\n+$/, '') : '';
397
+ return [comp, hyd].filter(Boolean).join('\n');
398
+ }
355
399
  /**
356
400
  * Scopes one file's contribution to the shared chunk: module-level
357
401
  * import/export statements pass through untouched, all other top-level
@@ -454,7 +498,7 @@ function removeCompiledNodes(code, isTarget) {
454
498
  }
455
499
  export async function generateClientBundle(routeTree, appDir, componentMap, options) {
456
500
  const runtimeDir = findRuntimeSrc(appDir);
457
- const seen = new Set();
501
+ let seen = new Set();
458
502
  const chunks = [];
459
503
  const runtimeImportNames = new Set();
460
504
  const cache = options?.cache;
@@ -540,9 +584,33 @@ export async function generateClientBundle(routeTree, appDir, componentMap, opti
540
584
  // file carry the same import twice (joint parse would abort the merge).
541
585
  // Mirror the fresh path exactly: strip, rename the hydrator registry,
542
586
  // trim — otherwise warm output drifts from cold output.
543
- const cachedComp = cached.compCode ? imports.add(cached.compCode, filePath).replace(/^\n+/, '').replace(/\n+$/, '') : '';
544
- const cachedHyd = cached.hydCode ? imports.add(cached.hydCode, filePath).replace(/__components/g, '__hydrators').replace(/^\n+/, '').replace(/\n+$/, '') : '';
545
- const cachedFileCode = [cachedComp, cachedHyd].filter(Boolean).join('\n');
587
+ let cachedFileCode = '';
588
+ if (cached.compBody !== undefined) {
589
+ // Parse-free warm replay: the body/scoped strings and the folded
590
+ // import specifiers were computed once at fresh-compile time, so this
591
+ // hit costs zero parses (the hot path paid ~5 acorn parses per cached
592
+ // file per chunk before — ~4k parses per edit on the test app).
593
+ let aborted = false;
594
+ for (const s of cached.importSpecs ?? []) {
595
+ if (!imports.foldSpec(s.source, s.kind, s.local, s.imported, filePath, cached.compCode)) {
596
+ aborted = true;
597
+ break;
598
+ }
599
+ }
600
+ if (aborted) {
601
+ // Rare fold conflict — fall back to the historical parse path so
602
+ // the merge-abort semantics (and `ok` reporting) stay identical.
603
+ cachedFileCode = fallbackReplay(cached, filePath, imports);
604
+ }
605
+ else if (cached.scoped) {
606
+ output.push(cached.scoped);
607
+ }
608
+ }
609
+ else {
610
+ // Old-style entry (e.g. populated before the parse-free fields were
611
+ // introduced): historical replay.
612
+ cachedFileCode = fallbackReplay(cached, filePath, imports);
613
+ }
546
614
  if (cachedFileCode.trim())
547
615
  output.push(scopeFileContribution(cachedFileCode));
548
616
  for (const n of cached.runtimeNames)
@@ -566,10 +634,14 @@ export async function generateClientBundle(routeTree, appDir, componentMap, opti
566
634
  const { comp: rawComp, hyd: rawHyd, name: actualName } = compileClientBoth(src, null, filePath);
567
635
  // Cache keeps the import-carrying codes so a warm build can re-fold them
568
636
  // into its own fresh accumulator; the emitted codes are import-stripped.
637
+ // The bodies/specs/scoped artifacts are cached too so warm replay never
638
+ // re-parses (see the `compBody !== undefined` fast path above).
569
639
  const compWithImports = rawComp ? stripExports(stripVskImports(stripRuntimeImport(rawComp))) : '';
570
640
  const hydWithImports = rawHyd ? stripExports(stripVskImports(stripRuntimeImport(rawHyd))) : '';
571
- const compCode = compWithImports ? imports.add(compWithImports, filePath).replace(/^\n+/, '').replace(/\n+$/, '') : '';
572
- const hydCode = hydWithImports ? imports.add(hydWithImports, filePath).replace(/__components/g, '__hydrators').replace(/^\n+/, '').replace(/\n+$/, '') : '';
641
+ const compSpecs = [];
642
+ const hydSpecs = [];
643
+ const compCode = compWithImports ? imports.add(compWithImports, filePath, compSpecs).replace(/^\n+/, '').replace(/\n+$/, '') : '';
644
+ const hydCode = hydWithImports ? imports.add(hydWithImports, filePath, hydSpecs).replace(/__components/g, '__hydrators').replace(/^\n+/, '').replace(/\n+$/, '') : '';
573
645
  if (rawComp)
574
646
  collectRuntimeImports(rawComp);
575
647
  if (rawHyd)
@@ -585,8 +657,9 @@ export async function generateClientBundle(routeTree, appDir, componentMap, opti
585
657
  // Comp and hyd contributions of one file share its top-level bindings,
586
658
  // so they are scoped together in a single block (see replay path above).
587
659
  const fileCode = [compCode, hydCode].filter(Boolean).join('\n');
588
- if (fileCode.trim())
589
- output.push(scopeFileContribution(fileCode));
660
+ const scopedContribution = fileCode.trim() ? scopeFileContribution(fileCode) : '';
661
+ if (scopedContribution)
662
+ output.push(scopedContribution);
590
663
  if (actualName && resolvedName !== null && actualName !== resolvedName) {
591
664
  output.push(`Object.defineProperty(__components, ${JSON.stringify(resolvedName)}, { get: () => __components[${JSON.stringify(actualName)}], configurable: true });`);
592
665
  output.push(`Object.defineProperty(__hydrators, ${JSON.stringify(resolvedName)}, { get: () => __hydrators[${JSON.stringify(actualName)}], configurable: true });`);
@@ -601,6 +674,10 @@ export async function generateClientBundle(routeTree, appDir, componentMap, opti
601
674
  actualName,
602
675
  runtimeNames: [...runtimeImportNames].filter((n) => !namesBefore.has(n)),
603
676
  imports: importedPaths,
677
+ compBody: compCode,
678
+ hydBody: hydCode,
679
+ scoped: scopedContribution,
680
+ importSpecs: [...compSpecs, ...hydSpecs],
604
681
  });
605
682
  }
606
683
  }
@@ -630,6 +707,14 @@ export async function generateClientBundle(routeTree, appDir, componentMap, opti
630
707
  const chunkEntries = [];
631
708
  function walkSplit(nodes, _chain) {
632
709
  for (const node of nodes) {
710
+ // Each chunk must be self-contained: reset the compile dedupe so a
711
+ // shared .vsk component (e.g. a site-wide Footer imported by the root
712
+ // layout) is emitted into every chunk that references it. With a
713
+ // single global `seen`, the first chunk to import a component owns it
714
+ // and later chunks reference `__components["X"]` without registering
715
+ // it — which breaks standalone layouts, whose match chain drops the
716
+ // index chunk at runtime.
717
+ seen = new Set();
633
718
  const chunkCode = [];
634
719
  const chunkImports = new ChunkImports();
635
720
  const pagePath = resolve(appDir, node.sourceDir, 'page.vsk');
@@ -670,7 +755,7 @@ export async function generateClientBundle(routeTree, appDir, componentMap, opti
670
755
  console.error('[vesk] chunk import merge aborted for', chunkName + ':', chunkImports.reason);
671
756
  const head = chunkImports.render();
672
757
  const code = (head ? head + '\n\n' : '') + chunkCode.join('\n\n');
673
- chunkEntries.push({ name: chunkName, code, node });
758
+ chunkEntries.push({ name: chunkName, code, node, imports: chunkHasImports(head.length, !chunkImports.ok) });
674
759
  }
675
760
  walkSplit(node.children || [], [..._chain, node]);
676
761
  }
@@ -679,6 +764,7 @@ export async function generateClientBundle(routeTree, appDir, componentMap, opti
679
764
  const sharedCode = [];
680
765
  const sharedImports = new ChunkImports();
681
766
  const compMap = componentMap || new Map();
767
+ seen = new Set();
682
768
  for (const [compName, compPath] of compMap) {
683
769
  compileFile(compPath, compName, sharedCode, sharedImports);
684
770
  }
@@ -687,14 +773,14 @@ export async function generateClientBundle(routeTree, appDir, componentMap, opti
687
773
  console.error('[vesk] chunk import merge aborted for shared.js:', sharedImports.reason);
688
774
  const head = sharedImports.render();
689
775
  const code = (head ? head + '\n\n' : '') + sharedCode.join('\n\n');
690
- chunkEntries.push({ name: 'shared.js', code, node: null });
776
+ chunkEntries.push({ name: 'shared.js', code, node: null, imports: chunkHasImports(head.length, !sharedImports.ok) });
691
777
  }
692
778
  const chunkIIFE = (body) => `(()=>{\nconst __components = globalThis.__components || (globalThis.__components = {});\nconst __hydrators = globalThis.__hydrators || (globalThis.__hydrators = {});\n${body}\n})();\n`;
693
779
  for (const entry of chunkEntries) {
694
780
  if (!entry.code.trim())
695
781
  continue;
696
782
  let finalCode;
697
- if (hasChunkImports(entry.code)) {
783
+ if (entry.imports) {
698
784
  // Bundle the chunk through esbuild so every remaining import (npm
699
785
  // packages like lucide-vesk and rewritten relative `.ts` value
700
786
  // modules) is inlined into the IIFE — a classic script cannot carry
@@ -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;AAOjG,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,OAAO,CAAC;AAI7C,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,CAikBvG"}
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;AAOjG,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,OAAO,CAAC;AAI7C,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,CAwnBvG"}
@@ -3,12 +3,12 @@ import { resolve, extname, dirname } from 'node:path';
3
3
  import { createServer } from 'node:http';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { stripCodeTypes } from '@vesk/compiler/src/strip-ts';
6
- import { DEFAULT_MAX_BODY_BYTES } from '@vesk/compiler/src/server-codegen';
6
+ import { DEFAULT_MAX_BODY_BYTES, safeJsonForScript } from '@vesk/compiler/src/server-codegen';
7
7
  import { build } from '@vesk/adapter/src/index';
8
- import { createHmrServer } from './hmr';
8
+ import { buildErrorPayload, createHmrServer } from './hmr';
9
9
  import * as hmrApi from './hmr';
10
10
  import { createDevApiRouter } from './dev-api';
11
- import { buildRuntimeCode } from '@vesk/adapter/src/client-bundle';
11
+ import { buildTreeShakenRuntime, runtimeExportNames } from '@vesk/adapter/src/client-bundle';
12
12
  import { resolveWithin, installMdReadHook } from '@vesk/adapter/src/paths';
13
13
  import { getPluginRecords } from './plugins';
14
14
  // @vesk/agentic — optional AI plugin; gated by active state if installed
@@ -358,6 +358,7 @@ export async function startDevServer(appDir, options) {
358
358
  let ssrVersion = Date.now();
359
359
  let routeTree = [];
360
360
  let runtimeBundle = '';
361
+ let pendingInitialError = null;
361
362
  let eventsMod = null;
362
363
  async function reloadEventsMod() {
363
364
  const eventsPath = resolve(devDir, 'server', 'events.js');
@@ -375,7 +376,7 @@ export async function startDevServer(appDir, options) {
375
376
  async function doBuild() {
376
377
  const start = Date.now();
377
378
  try {
378
- const result = await build(appDir, { outDir: devDir, publicDir, hmr: true });
379
+ const result = await build(appDir, { outDir: devDir, publicDir, hmr: true, plugins: options?.plugins });
379
380
  const configPath = resolve(devDir, 'config.json');
380
381
  if (existsSync(configPath)) {
381
382
  config = JSON.parse(readFileSync(configPath, 'utf-8'));
@@ -384,17 +385,39 @@ export async function startDevServer(appDir, options) {
384
385
  routeTree = result.routeTree;
385
386
  const monorepoRoot = resolve(__dirname, '..', '..', '..');
386
387
  const runtimeDir = resolve(monorepoRoot, 'packages', 'runtime', 'dist');
387
- runtimeBundle = buildRuntimeCode(runtimeDir);
388
+ // The full runtime surface for /_vesk/runtime.js. The legacy concat
389
+ // (buildRuntimeCode) collides on module-scope names shared by separate
390
+ // runtime modules (e.g. hydrateInitial in hydrate.ts and router.ts) and
391
+ // is a SyntaxError as a module — every import() of it fails. The
392
+ // tree-shaken full set is one closed IIFE + explicit re-exports, so all
393
+ // runtime names resolve. Matches prod, which serves client.js there.
394
+ runtimeBundle = await buildTreeShakenRuntime(runtimeDir, [...runtimeExportNames(runtimeDir)]);
388
395
  await reloadEventsMod();
389
396
  lastBuild = Date.now();
397
+ // successful build clears any pending error
398
+ pendingInitialError = null;
390
399
  console.error(`vesk dev: rebuilt in ${Date.now() - start}ms`);
391
400
  }
392
401
  catch (e) {
393
402
  const message = e instanceof Error ? e.message : String(e);
394
403
  console.error(`vesk dev: build error: ${message}`);
404
+ // surface the error to the HMR overlay + devtool Errors panel
405
+ try {
406
+ const filename = e?.filename
407
+ || e?.file
408
+ || 'app';
409
+ const payload = buildErrorPayload(e, String(filename), { appDir });
410
+ pendingInitialError = payload;
411
+ if (hmrSession) {
412
+ hmrSession.broadcast('error', payload);
413
+ }
414
+ }
415
+ catch {
416
+ /* best-effort */
417
+ }
395
418
  }
396
419
  }
397
- await doBuild();
420
+ await doBuild().catch(() => { });
398
421
  const server = createServer(async (req, res) => {
399
422
  const url = new URL(req.url || '/', `http://localhost:${port}`);
400
423
  // Dev panel endpoints (/__vesk/...): HMR state, plugin list, activate/
@@ -491,6 +514,28 @@ export async function startDevServer(appDir, options) {
491
514
  res.end(runtimeBundle);
492
515
  return;
493
516
  }
517
+ // External data scripts (/ssr-data.js?t=...) are emitted by
518
+ // storeDataScriptGlobal once a render serializes SSR data. Without this
519
+ // route the dev SPA fallback served the HTML document as the script's
520
+ // payload, which the client then failed to parse as JavaScript
521
+ // ("Unexpected token '<'") — dev pages never serialized data before so
522
+ // the tag was never emitted and this was unreachable. Mirrors prod-server.
523
+ if (url.pathname === '/ssr-data.js') {
524
+ const token = url.searchParams.get('t') || '';
525
+ const store = globalThis.__vsk_ssr_data_store;
526
+ const payload = store?.[token];
527
+ if (payload)
528
+ delete store[token];
529
+ console.error(`[ssr-data-trace] ${url.pathname} token=${token.slice(0, 6)} referer=${req.headers['referer'] || req.headers['referrer'] || '-'} payload=${payload ? JSON.stringify({ props: Object.keys(payload.props || {}), data: Object.keys(payload.ssrData || {}) }) : 'NONE'}`);
530
+ const lines = [];
531
+ if (payload?.props)
532
+ lines.push(`globalThis.__vesk_props = ${safeJsonForScript(JSON.stringify(payload.props))};`);
533
+ if (payload?.ssrData)
534
+ lines.push(`globalThis.__vsk_ssr_data = ${safeJsonForScript(JSON.stringify(payload.ssrData))};`);
535
+ res.writeHead(200, { 'Content-Type': 'application/javascript', 'Cache-Control': 'no-store' });
536
+ res.end(lines.join('\n') || '// no ssr data');
537
+ return;
538
+ }
494
539
  if (url.pathname.startsWith('/_vesk/static/')) {
495
540
  const relPath = url.pathname.slice('/_vesk/static/'.length);
496
541
  const staticPath = resolveWithin(resolve(devDir, 'static'), relPath);
@@ -558,6 +603,9 @@ export async function startDevServer(appDir, options) {
558
603
  let finalBody = body;
559
604
  if (contentType.includes('text/html')) {
560
605
  finalBody = injectDevScripts(body);
606
+ if (finalBody.includes('/ssr-data.js')) {
607
+ console.error(`[body-trace] ${url.pathname} HTML body carries ssr-data ref (was-in-body=${body.includes('/ssr-data.js')})`);
608
+ }
561
609
  }
562
610
  res.writeHead(response.status, headers);
563
611
  res.end(finalBody);
@@ -602,10 +650,15 @@ export async function startDevServer(appDir, options) {
602
650
  const handlerPath = resolve(devDir, ssrRoute.function);
603
651
  if (existsSync(handlerPath)) {
604
652
  try {
653
+ const t0 = process.hrtime.bigint();
654
+ const tImport0 = process.hrtime.bigint();
605
655
  const mod = await import(`${handlerPath}?t=${ssrVersion}`);
656
+ const tImport1 = process.hrtime.bigint();
606
657
  const webRequest = makeWebRequest(req, url.href, maxBodyBytes);
607
658
  const response = await mod.handle(webRequest);
659
+ const tHandle1 = process.hrtime.bigint();
608
660
  const body = await response.text();
661
+ const tText1 = process.hrtime.bigint();
609
662
  const headers = Object.fromEntries(response.headers);
610
663
  const contentType = headers['content-type'] || '';
611
664
  let finalBody = body;
@@ -614,6 +667,7 @@ export async function startDevServer(appDir, options) {
614
667
  }
615
668
  res.writeHead(response.status, headers);
616
669
  res.end(finalBody);
670
+ console.error(`[vdtime] ${url.pathname} ssrVersion=${ssrVersion} total=${Number(tHandle1 - t0) / 1e6 | 0}ms import=${Number(tImport1 - tImport0) / 1e6 | 0}ms handle=${Number(tHandle1 - tImport1) / 1e6 | 0}ms text=${Number(tText1 - tHandle1) / 1e6 | 0}ms`);
617
671
  }
618
672
  catch (e) {
619
673
  res.writeHead(500, { 'Content-Type': 'text/html' });
@@ -655,6 +709,10 @@ export async function startDevServer(appDir, options) {
655
709
  });
656
710
  const hmr = createHmrServer(server, appDir, devDir, componentMap);
657
711
  hmrSession = hmr;
712
+ // replay any build error that happened before the HMR server existed
713
+ if (pendingInitialError) {
714
+ hmr.broadcast('error', pendingInitialError);
715
+ }
658
716
  // ── Console streaming to devtool ────────────────────────────────────────
659
717
  // Read `logs` from vesk.config and wire up server-side console interception
660
718
  // so console.log/warn/error/info/debug are forwarded to the devtool Log tab.
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;CAChB;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,CAsEjB;AAoRD,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,CAyJA"}
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;CAChB;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,CA6FjB;AAwRD,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
@@ -57,15 +57,39 @@ export function buildErrorPayload(error, filename, opts = {}) {
57
57
  ? error.stack
58
58
  : undefined;
59
59
  let codeframe;
60
+ let resolvedAbsPath;
60
61
  if (line !== null && line >= 1 && typeof opts.appDir === 'string') {
61
62
  let src;
62
- try {
63
- const absPath = resolve(opts.appDir, file);
64
- if (existsSync(absPath))
65
- src = readFileSync(absPath, 'utf-8');
63
+ const candidates = [];
64
+ // file may be relative to appDir (e.g. "comparison/page.vsk"),
65
+ // prefixed with "app/" (e.g. "app/comparison/page.vsk"),
66
+ // absolute, or relative to project root.
67
+ if (file.startsWith('/')) {
68
+ candidates.push(file);
66
69
  }
67
- catch {
68
- src = undefined;
70
+ else {
71
+ candidates.push(resolve(opts.appDir, file));
72
+ if (file.startsWith('app/')) {
73
+ candidates.push(resolve(opts.appDir, file.slice(4)));
74
+ candidates.push(resolve(opts.appDir, '..', file));
75
+ }
76
+ else {
77
+ candidates.push(resolve(opts.appDir, '..', file));
78
+ }
79
+ // also try file as-is relative to cwd
80
+ candidates.push(resolve(file));
81
+ }
82
+ for (const absPath of candidates) {
83
+ try {
84
+ if (existsSync(absPath)) {
85
+ src = readFileSync(absPath, 'utf-8');
86
+ resolvedAbsPath = absPath;
87
+ break;
88
+ }
89
+ }
90
+ catch {
91
+ /* try next candidate */
92
+ }
69
93
  }
70
94
  if (typeof src === 'string' && src.length > 0) {
71
95
  const cf = buildCodeframe(src, line, column ?? 1);
@@ -76,9 +100,9 @@ export function buildErrorPayload(error, filename, opts = {}) {
76
100
  }
77
101
  }
78
102
  const tipsData = suggestFor(message);
79
- const filePath = typeof opts.appDir === 'string'
103
+ const filePath = resolvedAbsPath ?? (typeof opts.appDir === 'string'
80
104
  ? resolve(opts.appDir, file)
81
- : undefined;
105
+ : undefined);
82
106
  // The file header already shows the location; strip the redundant
83
107
  // " in <file>" suffix compilers append to messages (e.g. acorn's
84
108
  // "Unexpected token in /abs/app/page.vsk").
@@ -129,9 +153,13 @@ function collectAncestorLayouts(routeTree, sourceDir, chain = []) {
129
153
  if (node.sourceDir === sourceDir)
130
154
  return chain;
131
155
  if (node.children) {
156
+ // Standalone layouts drop the ancestor chain — they replace the
157
+ // root layout nesting for all descendants.
158
+ const isStandalone = node.standalone === true;
159
+ const base = isStandalone ? [] : chain;
132
160
  const nextChain = node.layout
133
- ? [...chain, { sourceDir: node.sourceDir, layoutCompName: node.layout }]
134
- : chain;
161
+ ? [...base, { sourceDir: node.sourceDir, layoutCompName: node.layout }]
162
+ : base;
135
163
  const found = collectAncestorLayouts(node.children, sourceDir, nextChain);
136
164
  if (found)
137
165
  return found;
@@ -478,11 +506,35 @@ export function createHmrServer(httpServer, appDir, devDir, componentMap) {
478
506
  }
479
507
  }
480
508
  if (sourceDir !== null) {
481
- const routeNode = findRouteForSource(routeTree, sourceDir);
509
+ // sourceDir from extractSourceDir is relative to appDir (e.g. "community/discussions"),
510
+ // while routeTree nodes store absolute sourceDir — normalize to absolute for lookup
511
+ let absSourceDir;
512
+ if (sourceDir === '') {
513
+ absSourceDir = appDir;
514
+ }
515
+ else if (sourceDir.startsWith('app/')) {
516
+ absSourceDir = resolve(appDir, '..', sourceDir);
517
+ }
518
+ else {
519
+ absSourceDir = resolve(appDir, sourceDir);
520
+ }
521
+ const routeNode = findRouteForSource(routeTree, absSourceDir);
482
522
  if (routeNode) {
483
- const ancestorLayouts = collectAncestorLayouts(routeTree, sourceDir);
523
+ const ancestorLayouts = collectAncestorLayouts(routeTree, absSourceDir);
484
524
  regenerateSsrFunction(routeNode, appDir, devDir, componentMap, { ancestorLayouts: ancestorLayouts || [] });
485
525
  }
526
+ else if (filename.endsWith('page.vsk') || filename.endsWith('layout.vsk')) {
527
+ // new route (e.g. freshly created app/community/discussions/page.vsk)
528
+ // — routeTree is stale, need a full rebuild to pick up the new node
529
+ // and generate its SSR function with the correct layout chain
530
+ try {
531
+ await doFullBuild();
532
+ broadcast('reload', { reason: `new route: ${filename}`, time: Date.now() - start });
533
+ }
534
+ catch (e) {
535
+ broadcast('error', buildErrorPayload(e, filename, { appDir }));
536
+ }
537
+ }
486
538
  }
487
539
  console.error(`vesk hmr: ${assignments.map(a => a.name).join(', ')} (${Date.now() - start}ms)`);
488
540
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAaA,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;AA+BD,wBAAsB,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CAuTpG;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":"AAaA,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;AA+BD,wBAAsB,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CA8TpG;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
@@ -197,9 +197,13 @@ export async function build(appDir, options) {
197
197
  const actionMap = {};
198
198
  function walk(nodes, ancestorLayouts = []) {
199
199
  for (const node of nodes) {
200
- const childAncestorLayouts = node.layout
201
- ? [...ancestorLayouts, { sourceDir: node.sourceDir, layoutCompName: node.layout }]
202
- : ancestorLayouts;
200
+ // Standalone layouts drop the entire ancestor chain — they replace
201
+ // the root layout nesting, so only their own layout (if any) remains.
202
+ const isStandalone = node.standalone === true;
203
+ let childAncestorLayouts = isStandalone ? [] : ancestorLayouts;
204
+ if (node.layout) {
205
+ childAncestorLayouts = [...childAncestorLayouts, { sourceDir: node.sourceDir, layoutCompName: node.layout }];
206
+ }
203
207
  if (node.page) {
204
208
  const mwChain = collectMiddlewareChain(routeTree, node.fullPath, appDir);
205
209
  let mwCode = null;
@@ -207,7 +211,10 @@ export async function build(appDir, options) {
207
211
  const mwSources = mwChain.map((m) => readFileSync(m.sourcePath, 'utf-8'));
208
212
  mwCode = compileMiddlewareCode(mwSources);
209
213
  }
210
- const { funcPath, funcCode, name } = generateSsrFunction(node, appDir, outDir, componentMap, { ancestorLayouts, middlewareCode: mwCode, headExtra: pluginHeadExtra });
214
+ // For standalone routes the layout chain is cleared — only the
215
+ // standalone node's own layout (if any) applies, no root ancestors.
216
+ const ssrAncestorLayouts = isStandalone ? [] : ancestorLayouts;
217
+ const { funcPath, funcCode, name } = generateSsrFunction(node, appDir, outDir, componentMap, { ancestorLayouts: ssrAncestorLayouts, middlewareCode: mwCode, headExtra: pluginHeadExtra });
211
218
  writeFileSync(funcPath, funcCode, 'utf-8');
212
219
  const pagePath = resolve(appDir, node.sourceDir, 'page.vsk');
213
220
  if (existsSync(pagePath)) {
@@ -217,7 +224,7 @@ export async function build(appDir, options) {
217
224
  const layoutSrc = readFileSync(resolve(appDir, node.sourceDir, 'layout.vsk'), 'utf-8');
218
225
  actionIds.push(...collectActionIds(layoutSrc));
219
226
  }
220
- for (const a of ancestorLayouts) {
227
+ for (const a of ssrAncestorLayouts) {
221
228
  const ancestorSrc = readFileSync(resolve(appDir, a.sourceDir, 'layout.vsk'), 'utf-8');
222
229
  actionIds.push(...collectActionIds(ancestorSrc));
223
230
  }
@@ -0,0 +1,3 @@
1
+ import type { VeskConfig } from '@vesk/compiler/src/types';
2
+ export declare function loadVeskConfig(projectDir: string): Promise<VeskConfig | undefined>;
3
+ //# sourceMappingURL=load-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"load-config.d.ts","sourceRoot":"","sources":["../src/load-config.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAE3D,wBAAsB,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC,CA+DxF"}
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Shared `vesk.config.{ts,js}` loader for the CLI and the production server.
3
+ *
4
+ * TS configs are transpiled to ESM and evaluated from a temp module rooted
5
+ * inside the project (`.vesk/`) so bare package imports (`@vesk/plugin-*`)
6
+ * resolve against the project's own `node_modules`. `@vesk/compiler` imports
7
+ * are stripped and the helpers injected via `globalThis.__vesk_inject` so the
8
+ * running CLI/adapter's own compiler build is used regardless of which version
9
+ * the project has installed.
10
+ */
11
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
12
+ import { join, dirname } from 'node:path';
13
+ import { pathToFileURL } from 'node:url';
14
+ import { createRequire } from 'node:module';
15
+ import { defineConfig, definePlugin, preset, validateConfig } from '@vesk/compiler/src/config';
16
+ export async function loadVeskConfig(projectDir) {
17
+ const jsPath = join(projectDir, 'vesk.config.js');
18
+ const tsPath = join(projectDir, 'vesk.config.ts');
19
+ let configPath = null;
20
+ if (existsSync(jsPath))
21
+ configPath = jsPath;
22
+ else if (existsSync(tsPath))
23
+ configPath = tsPath;
24
+ if (!configPath)
25
+ return undefined;
26
+ let raw;
27
+ if (configPath.endsWith('.ts')) {
28
+ const { transpile } = await import('typescript');
29
+ const src = readFileSync(configPath, 'utf-8');
30
+ let js = transpile(src, { module: 99, target: 99 });
31
+ const inject = { defineConfig, definePlugin, preset };
32
+ const projectRequire = createRequire(join(projectDir, 'package.json'));
33
+ const importRe = /import\s+(?:(?:\*\s*as\s+([\w$]+))|([\w$]+(?:\s*,\s*\{[^}]*\})?)|(\{[^}]*\}))\s+from\s+['"]([^'"]+)['"]\s*;?/g;
34
+ let m;
35
+ while ((m = importRe.exec(js)) !== null) {
36
+ const nsName = m[1];
37
+ const defPart = m[2];
38
+ const namedPart = m[3];
39
+ const specifier = m[4];
40
+ if (specifier === '@vesk/compiler')
41
+ continue;
42
+ const mod = await import(pathToFileURL(projectRequire.resolve(specifier)).href);
43
+ if (nsName) {
44
+ inject[nsName] = mod;
45
+ }
46
+ else if (defPart) {
47
+ const defaultName = defPart.split(',')[0].trim();
48
+ inject[defaultName] = mod.default ?? mod;
49
+ const open = defPart.indexOf('{');
50
+ if (open !== -1) {
51
+ for (const n of defPart.slice(open).matchAll(/(\w+)/g))
52
+ inject[n[1]] = mod[n[1]];
53
+ }
54
+ }
55
+ else if (namedPart) {
56
+ for (const n of namedPart.matchAll(/(\w+)/g))
57
+ inject[n[1]] = mod[n[1]];
58
+ }
59
+ }
60
+ js = js.replace(importRe, '');
61
+ const injectKeys = Object.keys(inject);
62
+ js = `const {${injectKeys.join(',')}} = globalThis.__vesk_inject;\n` + js;
63
+ const tmpFile = join(projectDir, '.vesk', 'config.loader.tmp.mjs');
64
+ mkdirSync(dirname(tmpFile), { recursive: true });
65
+ writeFileSync(tmpFile, js, 'utf-8');
66
+ globalThis.__vesk_inject = inject;
67
+ try {
68
+ const mod = await import(`${pathToFileURL(tmpFile).href}?t=${Date.now()}`);
69
+ raw = mod.default;
70
+ }
71
+ finally {
72
+ delete globalThis.__vesk_inject;
73
+ }
74
+ }
75
+ else {
76
+ const mod = await import(`${pathToFileURL(configPath).href}?t=${Date.now()}`);
77
+ raw = mod.default;
78
+ }
79
+ if (typeof raw === 'function')
80
+ raw = raw();
81
+ const config = (typeof defineConfig === 'function' ? defineConfig(raw) : raw);
82
+ if (typeof validateConfig === 'function')
83
+ validateConfig(config);
84
+ return config;
85
+ }
@@ -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;AA+CjG,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,KAAK,GAAG;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CAIzE;AA+FD,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,CAkcxI"}
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;AA8CjG,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,KAAK,GAAG;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CAIzE;AA+FD,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,CAwbxI"}
@@ -1,13 +1,12 @@
1
1
  import { readFileSync, existsSync, statSync } from 'node:fs';
2
2
  import { resolve, extname, dirname } from 'node:path';
3
3
  import { createServer } from 'node:http';
4
- import { createRequire } from 'node:module';
5
4
  import { fileURLToPath } from 'node:url';
6
5
  import { createRateLimiter, resolveComponentName, safeJsonForScript, getClientProtocol, DEFAULT_MAX_BODY_BYTES } from '@vesk/compiler/src/server-codegen';
7
6
  import { securityHeaders } from '@vesk/compiler/src/server-utils';
8
7
  import { resolveWithin, installMdReadHook } from '@vesk/adapter/src/paths';
9
8
  import { resolveCssUrls, hasBuiltGlobalCss } from '@vesk/adapter/src/css';
10
- const _require = createRequire(import.meta.url);
9
+ import { loadVeskConfig } from '@vesk/adapter/src/load-config';
11
10
  const __dirname = dirname(fileURLToPath(import.meta.url));
12
11
  async function readBody(req, maxBytes = DEFAULT_MAX_BODY_BYTES) {
13
12
  const chunks = [];
@@ -140,23 +139,11 @@ export async function startProdServer(outDir, options) {
140
139
  let securityConfig = {};
141
140
  let mdConfig;
142
141
  try {
143
- const veskConfigPath = resolve(projectDir, 'vesk.config.js');
144
- const veskConfigTsPath = resolve(projectDir, 'vesk.config.ts');
145
- let rawConfig = {};
146
- if (existsSync(veskConfigPath)) {
147
- rawConfig = _require(veskConfigPath);
142
+ const config = await loadVeskConfig(projectDir);
143
+ if (config) {
144
+ securityConfig = { security: config.security };
145
+ mdConfig = config.md;
148
146
  }
149
- else if (existsSync(veskConfigTsPath)) {
150
- const { transpile } = _require('typescript');
151
- const src = readFileSync(veskConfigTsPath, 'utf-8');
152
- const result = transpile(src, { module: 99, target: 99 });
153
- rawConfig = eval(`(${result})`);
154
- }
155
- if (typeof rawConfig === 'function')
156
- rawConfig = rawConfig();
157
- const configObj = rawConfig;
158
- securityConfig = { security: configObj.security };
159
- mdConfig = configObj.md;
160
147
  }
161
148
  catch (e) {
162
149
  // Fail closed: keep the secure defaults above and warn loudly — a broken
@@ -39,7 +39,7 @@ export async function bundleRuntime(appDir, outDir) {
39
39
  const runtimeRoot = findRuntimeSrc(appDir);
40
40
  const entryFile = resolve(outDir, 'server', `.runtime-entry-${buildId++}.mjs`);
41
41
  const entryContent = [
42
- `import { renderPage, renderFullPage, renderPageStream, compileFile, setRuntimeModule, setVskHydrate, assertSameOrigin } from ${JSON.stringify(resolve(compilerRoot, 'server-codegen.js'))};`,
42
+ `import { renderPage, renderFullPage, renderPageStream, compileFile, setRuntimeModule, setVskHydrate, hydratePrecompile, assertSameOrigin } 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
45
  '',
@@ -77,7 +77,7 @@ export async function bundleRuntime(appDir, outDir) {
77
77
  ' return req.locals || {};',
78
78
  '}',
79
79
  '',
80
- 'export { renderPage, renderFullPage, renderPageStream, compileFile, setVskHydrate, parseCookies, assertSameOrigin };',
80
+ 'export { renderPage, renderFullPage, renderPageStream, compileFile, setVskHydrate, hydratePrecompile, parseCookies, assertSameOrigin };',
81
81
  'export { withSsrStore } from "@vesk/compiler/src/ssr-store";',
82
82
  '',
83
83
  '// Deliver hydration data as an origin-served script so strict CSP (no unsafe-inline)',
@@ -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;AAW7F,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAQjF;AAiDD,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,CAoYtD"}
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,CAoYtD"}
@@ -1,13 +1,7 @@
1
1
  import { readFileSync, existsSync } from 'node:fs';
2
2
  import { resolve, relative, join } from 'node:path';
3
- import { resolveComponentName } from '@vesk/compiler/src/server-codegen';
3
+ import { resolveComponentName, precompileFile } from '@vesk/compiler/src/server-codegen';
4
4
  import { resolveCssUrls, hasBuiltGlobalCss } from '@vesk/adapter/src/css';
5
- function escapeSource(src) {
6
- return src
7
- .replace(/\\/g, '\\\\')
8
- .replace(/`/g, '\\`')
9
- .replace(/\$/g, '\\$');
10
- }
11
5
  // Finds the nearest error.vsk walking up from the route's own directory to the
12
6
  // app root, mirroring the router's findErrorComponent chain semantics.
13
7
  export function resolveErrorFile(sourceDir, appDir) {
@@ -31,6 +25,19 @@ function routeName(segments) {
31
25
  function extractCompName(src) {
32
26
  return resolveComponentName(src);
33
27
  }
28
+ /**
29
+ * Precompiles a `.vsk` file at BUILD time into a `PrecompileFilePlan` and
30
+ * returns it as a JSON object literal. True AOT: no source is embedded, no
31
+ * compile happens in the deployed function — `hydratePrecompile` rebuilds the
32
+ * render result at module load from this JSON alone.
33
+ */
34
+ function precompilePlan(entryPath) {
35
+ const src = readFileSync(entryPath, 'utf-8');
36
+ return JSON.stringify(precompileFile(src, entryPath));
37
+ }
38
+ function hydrateInvoke(planExpr) {
39
+ return `(() => { try { return hydratePrecompile(${planExpr}); } catch { return null; } })()`;
40
+ }
34
41
  function buildParamExtraction(node, urlParts) {
35
42
  const parts = [];
36
43
  let partIdx = Math.max(0, urlParts.length - 1);
@@ -96,38 +103,32 @@ export function generateSsrFunction(routeNode, appDir, outDir, componentMap, opt
96
103
  const entryPath = resolve(appDir, entry.sourceDir, 'layout.vsk');
97
104
  const entrySrc = readFileSync(entryPath, 'utf-8');
98
105
  const entryComp = extractCompName(entrySrc) || 'Layout';
99
- const escaped = escapeSource(entrySrc);
106
+ const plan = precompilePlan(entryPath);
100
107
  layoutDecls.push({
101
- src: `\`${escaped}\``,
102
108
  comp: JSON.stringify(entryComp),
103
- path: JSON.stringify(entryPath),
104
- compiled: `(() => { try { setVskHydrate(true); return compileFile(\`${escaped}\`, { sourcePath: ${JSON.stringify(entryPath)} }); } catch { return null; } finally { setVskHydrate(false); } })()`,
109
+ plan,
110
+ compiled: hydrateInvoke(plan),
105
111
  });
106
112
  }
107
- const pageSrc = readFileSync(pagePath, 'utf-8');
108
- const pageComp = extractCompName(pageSrc) || 'Page';
113
+ const pagePlan = precompilePlan(pagePath);
114
+ const pageComp = extractCompName(readFileSync(pagePath, 'utf-8')) || 'Page';
109
115
  const errorPath = resolveErrorFile(routeNode.sourceDir, appDir);
110
116
  const errorSrc = errorPath ? readFileSync(errorPath, 'utf-8') : null;
111
117
  const errorComp = errorPath ? (extractCompName(errorSrc) || 'Error') : null;
112
118
  const errorVars = errorPath
113
- ? `const _errorSrc = \`${escapeSource(errorSrc)}\`;\nconst _errorComp = ${JSON.stringify(errorComp)};\nconst _errorPath = ${JSON.stringify(errorPath)};\nconst _errorCompiled = (() => { try { setVskHydrate(true); return compileFile(_errorSrc, { sourcePath: _errorPath }); } catch { return null; } finally { setVskHydrate(false); } })();\n`
114
- : 'const _errorSrc = null;\nconst _errorComp = null;\nconst _errorPath = null;\nconst _errorCompiled = null;\n';
119
+ ? `const _errorComp = ${JSON.stringify(errorComp)};\nconst _errorCompiled = ${hydrateInvoke(precompilePlan(errorPath))};\n`
120
+ : 'const _errorComp = null;\nconst _errorCompiled = null;\n';
115
121
  let src = '';
116
122
  if (layoutDecls.length > 0) {
117
- src = `const _pageSrc = \`${escapeSource(pageSrc)}\`;\n`;
118
- src += `const _pageComp = ${JSON.stringify(pageComp)};\n`;
119
- src += `const _pagePath = ${JSON.stringify(pagePath)};\n`;
120
- src += `const _pageCompiled = (() => { try { setVskHydrate(true); return compileFile(_pageSrc, { sourcePath: _pagePath }); } catch { return null; } finally { setVskHydrate(false); } })();\n`;
121
- src += `const _layoutSrcList = [\n${layoutDecls.map((d) => ' ' + d.src + ',').join('\n')}\n];\n`;
123
+ src = `const _pageComp = ${JSON.stringify(pageComp)};\n`;
124
+ src += `const _pageCompiled = (() => { try { return hydratePrecompile(${pagePlan}); } catch { return null; } })();\n`;
122
125
  src += `const _layoutCompList = [${layoutDecls.map((d) => d.comp).join(', ')}];\n`;
123
- src += `const _layoutPathList = [${layoutDecls.map((d) => d.path).join(', ')}];\n`;
124
126
  src += `const _layoutCompiledList = [\n${layoutDecls.map((d) => ' ' + d.compiled + ',').join('\n')}\n];\n`;
125
127
  src += errorVars;
126
128
  }
127
129
  else {
128
- src = `const _src = \`${escapeSource(pageSrc)}\`;\nconst _comp = ${JSON.stringify(pageComp)};\n`;
129
- src += `const _srcPath = ${JSON.stringify(pagePath)};\n`;
130
- src += `const _srcCompiled = (() => { try { setVskHydrate(true); return compileFile(_src, { sourcePath: _srcPath }); } catch { return null; } finally { setVskHydrate(false); } })();\n`;
130
+ src = `const _comp = ${JSON.stringify(pageComp)};\n`;
131
+ src += `const _compiled = (() => { try { return hydratePrecompile(${pagePlan}); } catch { return null; } })();\n`;
131
132
  src += errorVars;
132
133
  }
133
134
  const urlParts = routeNode.fullPath.split('/').filter(Boolean);
@@ -139,12 +140,11 @@ export function generateSsrFunction(routeNode, appDir, outDir, componentMap, opt
139
140
  const compRegEntries = [];
140
141
  const compMap = componentMap || new Map();
141
142
  for (const [compName, compPath] of compMap) {
142
- const compSrc = readFileSync(compPath, 'utf-8');
143
- const escapedSrc = escapeSource(compSrc);
144
- compRegEntries.push(` registry.set(${JSON.stringify(compName)}, async (props, __registry, __vesk) => {\n const _src = \`${escapedSrc}\`;\n const _comp = ${JSON.stringify(compName)};\n const _compiled = (() => { try { setVskHydrate(true); return compileFile(_src, { sourcePath: ${JSON.stringify(compPath)} }); } catch { return null; } finally { setVskHydrate(false); } })();\n const result = await renderPage(_src, _comp, props, __registry, { hydrate: true, cached: _compiled, sourcePath: ${JSON.stringify(compPath)} });\n return result.body;\n })`);
143
+ const plan = precompilePlan(compPath);
144
+ compRegEntries.push(` (() => {\n const _compiled = (() => { try { return hydratePrecompile(${plan}); } catch { return null; } })();\n registry.set(${JSON.stringify(compName)}, async (props, __registry, __vesk) => {\n const result = await renderPage('', ${JSON.stringify(compName)}, props, __registry, { hydrate: true, cached: _compiled });\n return result.body;\n });\n })();`);
145
145
  }
146
146
  if (compRegEntries.length > 0) {
147
- registryCode = `const __componentRegistry = new Map();\n{\n${compRegEntries.join('\n')}\n}\n`;
147
+ registryCode = 'const __componentRegistry = new Map();\n{\n' + compRegEntries.join('\n') + '\n}\n';
148
148
  }
149
149
  else {
150
150
  registryCode = 'const __componentRegistry = new Map();\n';
@@ -153,9 +153,9 @@ export function generateSsrFunction(routeNode, appDir, outDir, componentMap, opt
153
153
  if (layoutDecls.length > 0) {
154
154
  htmlFnCode = [
155
155
  'async function __renderErrorBody(props) {',
156
- ' if (!_errorSrc) throw props.error || new Error("Internal Server Error");',
156
+ ' if (!_errorComp) throw props.error || new Error("Internal Server Error");',
157
157
  ' try {',
158
- ' const result = await renderPage(_errorSrc, _errorComp, props, __componentRegistry, { hydrate: true, cached: _errorCompiled, sourcePath: _errorPath });',
158
+ ' const result = await renderPage(\'\', _errorComp, props, __componentRegistry, { hydrate: true, cached: _errorCompiled });',
159
159
  ' return result.body;',
160
160
  ' } catch {',
161
161
  ' return \'<h1>500 \\u2014 Internal Server Error</h1>\';',
@@ -168,7 +168,7 @@ export function generateSsrFunction(routeNode, appDir, outDir, componentMap, opt
168
168
  ' let caughtError = null;',
169
169
  " const __expose = process.env.NODE_ENV !== 'production';",
170
170
  ' try {',
171
- ' page = await renderPage(_pageSrc, _pageComp, { params }, __componentRegistry, { hydrate: true, cached: _pageCompiled, sourcePath: _pagePath });',
171
+ ' page = await renderPage(\'\', _pageComp, { params }, __componentRegistry, { hydrate: true, cached: _pageCompiled });',
172
172
  ' } catch (err) {',
173
173
  ' if (err && (err.name === \'NotFoundError\' || err.name === \'Redirect\')) throw err;',
174
174
  ' caughtError = err;',
@@ -178,12 +178,12 @@ export function generateSsrFunction(routeNode, appDir, outDir, componentMap, opt
178
178
  ' }',
179
179
  ' let _body = (caughtError ? \'<!--vesk-ssr-error:\' + (caughtError && typeof caughtError === \'object\' && \'message\' in caughtError ? encodeURIComponent(__expose ? String(caughtError.message) : \'Internal Server Error\') : \'\') + \'-->\' : \'\') + page.body;',
180
180
  ' let _head = page.head || \'\';',
181
- ' for (let _i = _layoutSrcList.length - 1; _i > 0; _i--) {',
182
- ' const _inner = await renderPage(_layoutSrcList[_i], _layoutCompList[_i], { params, children: _body }, __componentRegistry, { hydrate: true, cached: _layoutCompiledList[_i], sourcePath: _layoutPathList[_i] });',
181
+ ' for (let _i = _layoutCompList.length - 1; _i > 0; _i--) {',
182
+ " const _inner = await renderPage('', _layoutCompList[_i], { params, children: _body }, __componentRegistry, { hydrate: true, cached: _layoutCompiledList[_i] });",
183
183
  ' _body = _inner.body;',
184
184
  ' if (_inner.head) _head = _inner.head + _head;',
185
185
  ' }',
186
- ' 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] });',
186
+ " const html = await renderFullPage('', _layoutCompList[0], { params, children: _body }, __componentRegistry, { hydrate: true, cached: _layoutCompiledList[0]" + bakedOptions + clientScriptOption + dataScriptOption + ", pageHead: _head });",
187
187
  " return new Response(html, { headers: { 'Content-Type': 'text/html' }, status: caughtError ? 500 : 200 });",
188
188
  ' });',
189
189
  '}',
@@ -193,19 +193,19 @@ export function generateSsrFunction(routeNode, appDir, outDir, componentMap, opt
193
193
  else {
194
194
  htmlFnCode = [
195
195
  'async function __renderErrorFullPage(params, requestUrl, err) {',
196
- ' if (!_errorSrc) throw err;',
196
+ ' if (!_errorComp) throw err;',
197
197
  " const __expose = process.env.NODE_ENV !== 'production';",
198
198
  " const message = err && typeof err === 'object' && 'message' in err ? String(err.message) : String(err);",
199
199
  " const stack = err && typeof err === 'object' && 'stack' in err ? String(err.stack) : '';",
200
200
  " const props = { params, statusCode: 500, error: __expose ? message : 'Internal Server Error', stack: __expose ? stack : '', url: requestUrl || '' };",
201
- ' return renderFullPage(_errorSrc, _errorComp, props, __componentRegistry, { hydrate: true, cached: _errorCompiled' + bakedOptions + clientScriptOption + dataScriptOption + ', sourcePath: _errorPath });',
201
+ " return renderFullPage('', _errorComp, props, __componentRegistry, { hydrate: true, cached: _errorCompiled" + bakedOptions + clientScriptOption + dataScriptOption + " });",
202
202
  '}',
203
203
  '',
204
204
  'async function __renderHtml(params, requestUrl) {',
205
205
  ' return withSsrStore(async () => {',
206
206
  ' let stream;',
207
207
  ' try {',
208
- ' stream = renderPageStream(_src, _comp, { params }, __componentRegistry, { hydrate: true, cached: _srcCompiled' + bakedOptions + clientScriptOption + dataScriptOption + ', sourcePath: _srcPath });',
208
+ " stream = renderPageStream('', _comp, { params }, __componentRegistry, { hydrate: true, cached: _compiled" + bakedOptions + clientScriptOption + dataScriptOption + ' });',
209
209
  ' } catch (err) {',
210
210
  ' if (err && (err.name === \'NotFoundError\' || err.name === \'Redirect\')) throw err;',
211
211
  ' const html = await __renderErrorFullPage(params, requestUrl, err);',
@@ -235,25 +235,41 @@ export function generateSsrFunction(routeNode, appDir, outDir, componentMap, opt
235
235
  }
236
236
  let dataCode;
237
237
  const exposeErr = "process.env.NODE_ENV !== 'production'";
238
+ // SPA data-nav renders page + layouts through renderPage and never runs
239
+ // renderFullPage/renderPageStream, whose finally blocks own the per-request
240
+ // token/slot cleanup in the full-page path. Clear the handoff globals here so
241
+ // the next full-page render can't reuse the stale token and serialize the
242
+ // previous nav's SSR data into its page (ssr-data leak).
243
+ const dataNavCleanup = [
244
+ ' } finally {',
245
+ ' const __dnToken = globalThis.__vsk_ssr_token;',
246
+ ' delete globalThis.__vsk_ssr;',
247
+ ' if (__dnToken) delete globalThis[`__vsk_ssr_data_${__dnToken}`];',
248
+ ' delete globalThis.__vsk_ssr_token;',
249
+ ' delete globalThis.__vsk_ssr_data;',
250
+ ' }',
251
+ ];
238
252
  if (layoutDecls.length > 0) {
239
253
  dataCode = [
240
254
  " if (request.headers.get('x-vesk-data') === '1') {",
241
- ' let dataPage;',
242
255
  ' try {',
243
- ' dataPage = await renderPage(_pageSrc, _pageComp, { params }, __componentRegistry, { hydrate: true, cached: _pageCompiled, sourcePath: _pagePath });',
244
- ' } catch (err) {',
245
- ' if (err && (err.name === \'NotFoundError\' || err.name === \'Redirect\')) throw err;',
246
- ' const message = err && typeof err === \'object\' && \'message\' in err ? String(err.message) : String(err);',
247
- ` return new Response(JSON.stringify({ error: ${exposeErr} ? message : 'Internal Server Error' }), { status: 500, headers: { 'Content-Type': 'application/json', 'Cache-Control': 'no-store', Vary: 'x-vesk-data' } });`,
248
- ' }',
249
- ' let _dataHead = dataPage.head || \'\';',
250
- ' for (let _i = _layoutSrcList.length - 1; _i >= 0; _i--) {',
251
- ' const _dl = await renderPage(_layoutSrcList[_i], _layoutCompList[_i], { params, children: \'\' }, __componentRegistry, { hydrate: true, cached: _layoutCompiledList[_i], sourcePath: _layoutPathList[_i] });',
252
- ' _dataHead = (_dl.head || \'\') + _dataHead;',
253
- ' }',
254
- " return new Response(JSON.stringify({ path: url.pathname, params, props: dataPage.props || { params }, head: _dataHead }), {",
255
- " headers: { 'Content-Type': 'application/json', 'Cache-Control': 'no-store', Vary: 'x-vesk-data' },",
256
- ' });',
256
+ ' let dataPage;',
257
+ ' try {',
258
+ " dataPage = await renderPage('', _pageComp, { params }, __componentRegistry, { hydrate: true, cached: _pageCompiled });",
259
+ ' } catch (err) {',
260
+ " if (err && (err.name === 'NotFoundError' || err.name === 'Redirect')) throw err;",
261
+ ' const message = err && typeof err === \'object\' && \'message\' in err ? String(err.message) : String(err);',
262
+ ` return new Response(JSON.stringify({ error: ${exposeErr} ? message : 'Internal Server Error' }), { status: 500, headers: { 'Content-Type': 'application/json', 'Cache-Control': 'no-store', Vary: 'x-vesk-data' } });`,
263
+ ' }',
264
+ ' let _dataHead = dataPage.head || \'\';',
265
+ ' for (let _i = _layoutCompList.length - 1; _i >= 0; _i--) {',
266
+ " const _dl = await renderPage('', _layoutCompList[_i], { params, children: '' }, __componentRegistry, { hydrate: true, cached: _layoutCompiledList[_i] });",
267
+ ' _dataHead = (_dl.head || \'\') + _dataHead;',
268
+ ' }',
269
+ " return new Response(JSON.stringify({ path: url.pathname, params, props: dataPage.props || { params }, head: _dataHead }), {",
270
+ " headers: { 'Content-Type': 'application/json', 'Cache-Control': 'no-store', Vary: 'x-vesk-data' },",
271
+ ' });',
272
+ ...dataNavCleanup,
257
273
  ' }',
258
274
  ' return __renderHtml(params, url.href);',
259
275
  ].join('\n');
@@ -261,17 +277,19 @@ export function generateSsrFunction(routeNode, appDir, outDir, componentMap, opt
261
277
  else {
262
278
  dataCode = [
263
279
  " if (request.headers.get('x-vesk-data') === '1') {",
264
- ' let dataPage;',
265
280
  ' try {',
266
- ' dataPage = await renderPage(_src, _comp, { params }, __componentRegistry, { hydrate: true, cached: _srcCompiled, sourcePath: _srcPath });',
267
- ' } catch (err) {',
268
- ' if (err && (err.name === \'NotFoundError\' || err.name === \'Redirect\')) throw err;',
269
- ' const message = err && typeof err === \'object\' && \'message\' in err ? String(err.message) : String(err);',
270
- ` return new Response(JSON.stringify({ error: ${exposeErr} ? message : 'Internal Server Error' }), { status: 500, headers: { 'Content-Type': 'application/json', 'Cache-Control': 'no-store', Vary: 'x-vesk-data' } });`,
271
- ' }',
272
- " return new Response(JSON.stringify({ path: url.pathname, params, props: dataPage.props || { params }, head: dataPage.head || '' }), {",
273
- " headers: { 'Content-Type': 'application/json', 'Cache-Control': 'no-store', Vary: 'x-vesk-data' },",
274
- ' });',
281
+ ' let dataPage;',
282
+ ' try {',
283
+ " dataPage = await renderPage('', _comp, { params }, __componentRegistry, { hydrate: true, cached: _compiled });",
284
+ ' } catch (err) {',
285
+ " if (err && (err.name === 'NotFoundError' || err.name === 'Redirect')) throw err;",
286
+ ' const message = err && typeof err === \'object\' && \'message\' in err ? String(err.message) : String(err);',
287
+ ` return new Response(JSON.stringify({ error: ${exposeErr} ? message : 'Internal Server Error' }), { status: 500, headers: { 'Content-Type': 'application/json', 'Cache-Control': 'no-store', Vary: 'x-vesk-data' } });`,
288
+ ' }',
289
+ " return new Response(JSON.stringify({ path: url.pathname, params, props: dataPage.props || { params }, head: dataPage.head || '' }), {",
290
+ " headers: { 'Content-Type': 'application/json', 'Cache-Control': 'no-store', Vary: 'x-vesk-data' },",
291
+ ' });',
292
+ ...dataNavCleanup,
275
293
  ' }',
276
294
  ' return __renderHtml(params, url.href);',
277
295
  ].join('\n');
@@ -318,28 +336,16 @@ export function generateSsrFunction(routeNode, appDir, outDir, componentMap, opt
318
336
  ' }',
319
337
  ].join('\n');
320
338
  }
321
- let registerActionsCode;
322
- if (layoutDecls.length > 0) {
323
- registerActionsCode = [
324
- 'async function __registerActions() {',
325
- ' if (__actionsRegistered) return;',
326
- ' __actionsRegistered = true;',
327
- ' for (let _i = 0; _i < _layoutSrcList.length; _i++) compileFile(_layoutSrcList[_i], { sourcePath: _layoutPathList[_i] });',
328
- ' compileFile(_pageSrc, { sourcePath: _pagePath });',
329
- '}',
330
- '',
331
- ].join('\n');
332
- }
333
- else {
334
- registerActionsCode = [
335
- 'async function __registerActions() {',
336
- ' if (__actionsRegistered) return;',
337
- ' __actionsRegistered = true;',
338
- ' compileFile(_src, { sourcePath: _srcPath });',
339
- '}',
340
- '',
341
- ].join('\n');
342
- }
339
+ // Actions are registered while each plan hydrates (its actions-transformed
340
+ // top-level code runs `defineAction` during `hydratePrecompile`), so the
341
+ // request-time registration pass is a no-op.
342
+ const registerActionsCode = [
343
+ 'async function __registerActions() {',
344
+ ' if (__actionsRegistered) return;',
345
+ ' __actionsRegistered = true;',
346
+ '}',
347
+ '',
348
+ ].join('\n');
343
349
  const actionCode = [
344
350
  'export async function handleAction(request, id) {',
345
351
  ' // CSRF defense: cross-site browser POSTs always carry an Origin header.',
@@ -421,7 +427,7 @@ export function generateSsrFunction(routeNode, appDir, outDir, componentMap, opt
421
427
  '',
422
428
  ].join('\n');
423
429
  const funcCode = [
424
- "import { renderFullPage, renderPageStream, renderPage, compileFile, setVskHydrate, parseCookies, getAction, validateActionInput, issuesToFieldMap, storeDataScriptGlobal, withSsrStore, assertSameOrigin, VeskRequest } from '../runtime.js';",
430
+ "import { renderFullPage, renderPageStream, renderPage, hydratePrecompile, parseCookies, getAction, validateActionInput, issuesToFieldMap, storeDataScriptGlobal, withSsrStore, assertSameOrigin, VeskRequest } from '../runtime.js';",
425
431
  '',
426
432
  middlewareCode || '',
427
433
  registryCode,
package/dist/types.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export type { RouteNode, ApiRouteNode, VeskPlugin, ServerEventContext, VeskEventHandlers, VeskSecurity, VeskCors, VeskRateLimit, VeskSecurityPreset, SecurityConfig, MiddlewareChainItem, MiddlewareExtractResult, AncestorLayout, BuildOptions, DevServerOptions, ProdServerOptions, SsrFunctionOptions, ApiFunctionOptions, ClientBundleOptions, ClientBundleFileEntry, ClientBundleCache, ChunkEntry, ClientBundleResult, MonolithicBundleParts, Manifest, ManifestRouteEntry, ManifestActionEntry, ManifestPrerenderedEntry, SsgRouteResult, BuildResult, ImageRef, ImageResult, SeoCheckIssue, SeoAuditResult, CombinedPageInfo, Tracked, Resource, UseFetchOptions, UseFetchStreamOptions, VeskRequest, VeskResponse, SetCookieOptions, CookieStore, ServerRequest, SsrDataSink, MdProps, MdConfig, MdHtmlMode, MarkdownOptions, MdHtmlWarning, } from '@vesk/types';
1
+ export type { RouteNode, ApiRouteNode, VeskPlugin, ServerEventContext, VeskEventHandlers, VeskSecurity, VeskCors, VeskRateLimit, VeskSecurityPreset, SecurityConfig, MiddlewareChainItem, MiddlewareExtractResult, AncestorLayout, BuildOptions, DevServerOptions, ProdServerOptions, SsrFunctionOptions, ApiFunctionOptions, ClientBundleOptions, ClientBundleFileEntry, ClientBundleChunkSpec, ClientBundleCache, ChunkEntry, ClientBundleResult, MonolithicBundleParts, Manifest, ManifestRouteEntry, ManifestActionEntry, ManifestPrerenderedEntry, SsgRouteResult, BuildResult, ImageRef, ImageResult, SeoCheckIssue, SeoAuditResult, CombinedPageInfo, Tracked, Resource, UseFetchOptions, UseFetchStreamOptions, VeskRequest, VeskResponse, SetCookieOptions, CookieStore, ServerRequest, SsrDataSink, MdProps, MdConfig, MdHtmlMode, MarkdownOptions, MdHtmlWarning, } from '@vesk/types';
2
2
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAGA,YAAY,EACV,SAAS,EACT,YAAY,EACZ,UAAU,EACV,kBAAkB,EAClB,iBAAiB,EACjB,YAAY,EACZ,QAAQ,EACR,aAAa,EACb,kBAAkB,EAClB,cAAc,EACd,mBAAmB,EACnB,uBAAuB,EACvB,cAAc,EACd,YAAY,EACZ,gBAAgB,EAChB,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,mBAAmB,EACnB,qBAAqB,EACrB,iBAAiB,EACjB,UAAU,EACV,kBAAkB,EAClB,qBAAqB,EACrB,QAAQ,EACR,kBAAkB,EAClB,mBAAmB,EACnB,wBAAwB,EACxB,cAAc,EACd,WAAW,EACX,QAAQ,EACR,WAAW,EACX,aAAa,EACb,cAAc,EACd,gBAAgB,EAChB,OAAO,EACP,QAAQ,EACR,eAAe,EACf,qBAAqB,EACrB,WAAW,EACX,YAAY,EACZ,gBAAgB,EAChB,WAAW,EACX,aAAa,EACb,WAAW,EACX,OAAO,EACP,QAAQ,EACR,UAAU,EACV,eAAe,EACf,aAAa,GACd,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAGA,YAAY,EACV,SAAS,EACT,YAAY,EACZ,UAAU,EACV,kBAAkB,EAClB,iBAAiB,EACjB,YAAY,EACZ,QAAQ,EACR,aAAa,EACb,kBAAkB,EAClB,cAAc,EACd,mBAAmB,EACnB,uBAAuB,EACvB,cAAc,EACd,YAAY,EACZ,gBAAgB,EAChB,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,mBAAmB,EACnB,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EACjB,UAAU,EACV,kBAAkB,EAClB,qBAAqB,EACrB,QAAQ,EACR,kBAAkB,EAClB,mBAAmB,EACnB,wBAAwB,EACxB,cAAc,EACd,WAAW,EACX,QAAQ,EACR,WAAW,EACX,aAAa,EACb,cAAc,EACd,gBAAgB,EAChB,OAAO,EACP,QAAQ,EACR,eAAe,EACf,qBAAqB,EACrB,WAAW,EACX,YAAY,EACZ,gBAAgB,EAChB,WAAW,EACX,aAAa,EACb,WAAW,EACX,OAAO,EACP,QAAQ,EACR,UAAU,EACV,eAAe,EACf,aAAa,GACd,MAAM,aAAa,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vesk/adapter",
3
- "version": "0.2.22",
3
+ "version": "0.2.23",
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.22",
27
- "@vesk/runtime": "^0.2.22",
28
- "@vesk/types": "^0.2.22"
26
+ "@vesk/compiler": "^0.2.23",
27
+ "@vesk/runtime": "^0.2.23",
28
+ "@vesk/types": "^0.2.23"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@types/ws": "^8.18.1",