@vesk/adapter 0.2.50-canary.2 → 0.2.50-canary.4

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.
@@ -0,0 +1,26 @@
1
+ export interface AssetDigest {
2
+ /** `client.a1b2c3d4e5.js` */
3
+ fileName: string;
4
+ /** 10 hex chars of the content hash, safe in a URL and a filename. */
5
+ hash: string;
6
+ bytes: number;
7
+ /** `sha384-<base64>`, ready for an `integrity` attribute. */
8
+ integrity: string;
9
+ }
10
+ /** `client.js` + bytes -> `client.a1b2c3d4e5.js` (the extension is preserved). */
11
+ export declare function hashedName(fileName: string, content: string | Buffer): string;
12
+ export declare function hashOf(content: string | Buffer): string;
13
+ export declare function integrityOf(content: string | Buffer): string;
14
+ export declare function describeAsset(fileName: string, content: string | Buffer): AssetDigest;
15
+ /** Does this name carry a content hash? Used to pick the cache policy. */
16
+ export declare function isHashedName(fileName: string): boolean;
17
+ /**
18
+ * Cache-Control for a served asset.
19
+ *
20
+ * A hashed name means the bytes behind that URL can never change, so it is
21
+ * immutable for a year. Anything else (dev bundles, `public/` files copied
22
+ * verbatim, the unhashed name a prerendered page may still reference) must be
23
+ * revalidated, or a deploy would serve stale code forever.
24
+ */
25
+ export declare function cacheControlFor(fileName: string, isDev: boolean): string;
26
+ //# sourceMappingURL=asset-hash.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"asset-hash.d.ts","sourceRoot":"","sources":["../src/asset-hash.ts"],"names":[],"mappings":"AAcA,MAAM,WAAW,WAAW;IAC1B,6BAA6B;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,sEAAsE;IACtE,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,6DAA6D;IAC7D,SAAS,EAAE,MAAM,CAAC;CACnB;AAID,kFAAkF;AAClF,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAK7E;AAED,wBAAgB,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAEvD;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAE5D;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,WAAW,CAOrF;AAED,0EAA0E;AAC1E,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAQtD;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,CAKxE"}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Content-addressed asset names and Subresource Integrity digests.
3
+ *
4
+ * Production assets are emitted under a content hash — `client.a1b2c3d4.js` —
5
+ * so they can be served `immutable` and never go stale in a CDN, and so a
6
+ * deploy that changes the runtime cannot be served the previous one. Dev keeps
7
+ * the plain name: the dev server rewrites it on every edit and HMR needs a
8
+ * stable URL.
9
+ *
10
+ * The digest is also the SRI value. Both come from the same bytes, so a tag
11
+ * carrying `integrity` can only execute the file that was actually built.
12
+ */
13
+ import { createHash } from 'node:crypto';
14
+ const EXT_RE = /\.([a-z0-9]+)$/i;
15
+ /** `client.js` + bytes -> `client.a1b2c3d4e5.js` (the extension is preserved). */
16
+ export function hashedName(fileName, content) {
17
+ const m = EXT_RE.exec(fileName);
18
+ if (!m)
19
+ return fileName;
20
+ const base = fileName.slice(0, m.index);
21
+ return `${base}.${hashOf(content)}${m[0]}`;
22
+ }
23
+ export function hashOf(content) {
24
+ return createHash('sha256').update(content).digest('hex').slice(0, 10);
25
+ }
26
+ export function integrityOf(content) {
27
+ return `sha384-${createHash('sha384').update(content).digest('base64')}`;
28
+ }
29
+ export function describeAsset(fileName, content) {
30
+ return {
31
+ fileName: hashedName(fileName, content),
32
+ hash: hashOf(content),
33
+ bytes: typeof content === 'string' ? Buffer.byteLength(content) : content.length,
34
+ integrity: integrityOf(content),
35
+ };
36
+ }
37
+ /** Does this name carry a content hash? Used to pick the cache policy. */
38
+ export function isHashedName(fileName) {
39
+ const m = EXT_RE.exec(fileName);
40
+ if (!m)
41
+ return false;
42
+ const base = fileName.slice(0, m.index);
43
+ const dot = base.lastIndexOf('.');
44
+ if (dot === -1)
45
+ return false;
46
+ const suffix = base.slice(dot + 1);
47
+ return suffix.length === 10 && /^[0-9a-f]{10}$/.test(suffix);
48
+ }
49
+ /**
50
+ * Cache-Control for a served asset.
51
+ *
52
+ * A hashed name means the bytes behind that URL can never change, so it is
53
+ * immutable for a year. Anything else (dev bundles, `public/` files copied
54
+ * verbatim, the unhashed name a prerendered page may still reference) must be
55
+ * revalidated, or a deploy would serve stale code forever.
56
+ */
57
+ export function cacheControlFor(fileName, isDev) {
58
+ if (isDev)
59
+ return 'no-cache';
60
+ return isHashedName(fileName)
61
+ ? 'public, max-age=31536000, immutable'
62
+ : 'public, max-age=0, must-revalidate';
63
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"client-bundle.d.ts","sourceRoot":"","sources":["../src/client-bundle.ts"],"names":[],"mappings":"AAaA,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,CAoc7B;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;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,CA6c7B;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"}
@@ -3,6 +3,7 @@ import { resolve, join, dirname, relative, sep } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { tmpdir } from 'node:os';
5
5
  import { build } from './esbuild-fallback.js';
6
+ import { hashedName } from '@vesk/adapter/src/asset-hash';
6
7
  import { stripCodeTypes } from '@vesk/compiler/src/strip-ts';
7
8
  import { parse, parseGeneratedJs } from '@vesk/compiler/src/parser';
8
9
  import { compileClient, compileClientBoth, nameAllocFor, vskComponentNames } from '@vesk/compiler/src/client-codegen';
@@ -982,9 +983,17 @@ export async function generateClientBundle(routeTree, appDir, componentMap, opti
982
983
  function annotate(nodes) {
983
984
  for (const node of nodes) {
984
985
  const chunkName = `page-${buildChunkName(node)}.js`;
985
- const hasEntry = chunkEntries.some(e => e.name === chunkName && e.code.trim());
986
- if (hasEntry)
987
- node.chunk = `/_vesk/static/${chunkName}`;
986
+ // Production chunk names are content-hashed, and the ROUTER asks for
987
+ // chunks by the URL baked here — so the hash has to be applied to this
988
+ // reference, not just to the file on disk. Getting it wrong is silent
989
+ // and total: every chunk 404s, the server answers those requests with
990
+ // the SPA fallback (text/html), the browser refuses the script, and the
991
+ // page never hydrates at all.
992
+ const emitted = chunks.find((c) => c.name === chunkName && c.code.trim());
993
+ if (emitted) {
994
+ const fileName = options?.hmr ? chunkName : hashedName(chunkName, emitted.code);
995
+ node.chunk = `/_vesk/static/${fileName}`;
996
+ }
988
997
  annotate(node.children || []);
989
998
  }
990
999
  }
@@ -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;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,CAkqBvG"}
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,10 +1,12 @@
1
1
  import { readFileSync, existsSync, watch, statSync } from 'node:fs';
2
- import { resolve, extname, dirname } from 'node:path';
2
+ import { resolve, extname, dirname, basename } 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
6
  import { DEFAULT_MAX_BODY_BYTES, safeJsonForScript } from '@vesk/compiler/src/server-codegen';
7
7
  import { build } from '@vesk/adapter/src/index';
8
+ import { cacheControlFor } from '@vesk/adapter/src/asset-hash';
9
+ import { reportServerError, registerErrorHooks } from '@vesk/adapter/src/error-report';
8
10
  import { buildErrorPayload, createHmrServer } from './hmr';
9
11
  import * as hmrApi from './hmr';
10
12
  import { createDevApiRouter } from './dev-api';
@@ -205,6 +207,9 @@ export async function startDevServer(appDir, options) {
205
207
  const devDir = resolve(appDir, '..', '.vesk', 'dev');
206
208
  const publicDir = options?.publicDir || resolve(appDir, '..', 'public');
207
209
  installMdReadHook([publicDir, resolve(devDir, 'static', 'public')]);
210
+ // Plugin `onError` hooks only exist here (dev): in production there are no
211
+ // plugin objects, and `_events.ts` is the seam that ships.
212
+ registerErrorHooks(options?.plugins);
208
213
  // Dev-panel plugin list: plugin / module names declared in the dev server's own config.
209
214
  const configPluginNames = (options?.plugins || []).map((p) => {
210
215
  if (typeof p === 'string')
@@ -572,7 +577,9 @@ export async function startDevServer(appDir, options) {
572
577
  }
573
578
  if (existsSync(staticPath) && statSync(staticPath).isFile()) {
574
579
  const ext = extname(staticPath);
575
- res.writeHead(200, { 'Content-Type': MIME[ext] || 'application/octet-stream' });
580
+ // Dev names are stable so HMR can rewrite them, which means they must
581
+ // never be cached: the same URL has to serve the newest bytes.
582
+ res.writeHead(200, { 'Content-Type': MIME[ext] || 'application/octet-stream', 'Cache-Control': cacheControlFor(basename(staticPath), true) });
576
583
  res.end(readFileSync(staticPath));
577
584
  return;
578
585
  }
@@ -697,6 +704,7 @@ export async function startDevServer(appDir, options) {
697
704
  }
698
705
  catch (e) {
699
706
  console.error('[vesk dev] SSR render failed:', e instanceof Error ? (e.stack || e.message) : String(e));
707
+ await reportServerError(e, { url: url.href, target: 'dev', status: 500 });
700
708
  res.writeHead(500, { 'Content-Type': 'text/html' });
701
709
  res.end(renderSsrErrorPage(e, url.pathname));
702
710
  }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The production error-reporting seam.
3
+ *
4
+ * Production never leaks a stack trace to the client (that is the right
5
+ * default), which used to mean an adopter had two bad options: show the stack,
6
+ * or replace the adapter's error handling. This is the supported middle: a
7
+ * plugin's `onError` hook receives the error, the route and the build id.
8
+ *
9
+ * Design rules:
10
+ * - a hook that throws is caught and logged, never turned into a second
11
+ * failure (a reporting outage must not take down the page that was already
12
+ * failing);
13
+ * - one failing hook does not stop the others;
14
+ * - nothing is reported twice for one request.
15
+ */
16
+ import type { ServerErrorContext, VeskPlugin } from '@vesk/types';
17
+ export interface BuildConfigLike {
18
+ version?: number;
19
+ buildId?: string;
20
+ veskVersion?: string;
21
+ [key: string]: unknown;
22
+ }
23
+ export declare function registerErrorReporter(fn: (err: unknown, ctx: ServerErrorContext) => void | Promise<void>): void;
24
+ export declare function clearErrorReporters(): void;
25
+ export declare function errorReporterCount(): number;
26
+ /** Register every plugin that declares `onError`. */
27
+ export declare function registerErrorHooks(plugins: VeskPlugin[] | null | undefined): void;
28
+ export interface ReportOptions {
29
+ url?: string;
30
+ routePath?: string;
31
+ target: ServerErrorContext['target'];
32
+ status?: number;
33
+ durationMs?: number;
34
+ buildConfig?: BuildConfigLike | null;
35
+ /** Suppress a duplicate report for the same error object. */
36
+ dedupe?: boolean;
37
+ }
38
+ /**
39
+ * Report a production failure. Never throws: a failure to report is logged and
40
+ * swallowed, because the caller is already handling a worse problem.
41
+ */
42
+ export declare function reportServerError(err: unknown, options: ReportOptions): Promise<void>;
43
+ //# sourceMappingURL=error-report.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error-report.d.ts","sourceRoot":"","sources":["../src/error-report.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EAAE,kBAAkB,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAElE,MAAM,WAAW,eAAe;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAMD,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,kBAAkB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAE/G;AAED,wBAAgB,mBAAmB,IAAI,IAAI,CAE1C;AAED,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED,qDAAqD;AACrD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,IAAI,GAAG,SAAS,GAAG,IAAI,CAIjF;AAED,MAAM,WAAW,aAAa;IAC5B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,kBAAkB,CAAC,QAAQ,CAAC,CAAC;IACrC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,eAAe,GAAG,IAAI,CAAC;IACrC,6DAA6D;IAC7D,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;;GAGG;AACH,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAyB3F"}
@@ -0,0 +1,52 @@
1
+ /** Process-wide hook list; the config's plugins are registered at boot. */
2
+ const hooks = [];
3
+ const reported = new WeakSet();
4
+ export function registerErrorReporter(fn) {
5
+ if (typeof fn === 'function')
6
+ hooks.push(fn);
7
+ }
8
+ export function clearErrorReporters() {
9
+ hooks.length = 0;
10
+ }
11
+ export function errorReporterCount() {
12
+ return hooks.length;
13
+ }
14
+ /** Register every plugin that declares `onError`. */
15
+ export function registerErrorHooks(plugins) {
16
+ for (const plugin of plugins || []) {
17
+ if (plugin && typeof plugin.onError === 'function')
18
+ registerErrorReporter(plugin.onError);
19
+ }
20
+ }
21
+ /**
22
+ * Report a production failure. Never throws: a failure to report is logged and
23
+ * swallowed, because the caller is already handling a worse problem.
24
+ */
25
+ export async function reportServerError(err, options) {
26
+ if (options.dedupe !== false && err && typeof err === 'object') {
27
+ if (reported.has(err))
28
+ return;
29
+ reported.add(err);
30
+ }
31
+ const cfg = options.buildConfig || null;
32
+ const ctx = {
33
+ url: options.url,
34
+ routePath: options.routePath,
35
+ target: options.target,
36
+ status: options.status ?? 500,
37
+ durationMs: options.durationMs,
38
+ version: typeof cfg?.veskVersion === 'string' ? cfg.veskVersion : undefined,
39
+ buildId: typeof cfg?.buildId === 'string' ? cfg.buildId : undefined,
40
+ errorName: err instanceof Error ? err.name : typeof err,
41
+ message: err instanceof Error ? err.message : String(err),
42
+ stack: err instanceof Error ? err.stack : undefined,
43
+ };
44
+ for (const hook of hooks) {
45
+ try {
46
+ await hook(err, ctx);
47
+ }
48
+ catch (e) {
49
+ console.error('vesk: an onError hook threw and was ignored:', e?.message ?? e);
50
+ }
51
+ }
52
+ }
package/dist/events.d.ts CHANGED
@@ -1,3 +1,20 @@
1
+ /**
2
+ * Wrapper module bundled into `server/events.js`. It lifts the user's
3
+ * `_events.ts` exports into `executeStart` / `executeRequest` / `executeStop`
4
+ * entry points with a context that routes `set`/`get` into the process-wide
5
+ * server context (`globalThis.__vesk_server_ctx`).
6
+ *
7
+ * The `__vesk_events_started` guard is global (not module-local) so lazy edge
8
+ * isolates and the Node prod server share one lifetime; `onStop` clears it so
9
+ * dev HMR reloads can boot a fresh module.
10
+ */
11
+ export declare function buildEventsEntry(sourcePath: string): string;
12
+ /**
13
+ * Report a production failure to `_events.ts`'s `onError`, if it exports one.
14
+ * Never throws: a reporting outage must not add a second failure to a request
15
+ * that already failed.
16
+ */
17
+ export declare function executeError(err: unknown, base: Record<string, unknown>, mod: unknown): Promise<void>;
1
18
  /**
2
19
  * Compile the app's `_events.ts` (if present) into a self-contained
3
20
  * `server/events.js` next to the server runtime. Returns true when an events
@@ -1 +1 @@
1
- {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAqEA;;;;;;GAMG;AACH,wBAAsB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAiCpF"}
1
+ {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAOA;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAuD3D;AAED;;;;GAIG;AACH,wBAAsB,YAAY,CAChC,GAAG,EAAE,OAAO,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,GAAG,EAAE,OAAO,GACX,OAAO,CAAC,IAAI,CAAC,CAQf;AAED;;;;;;GAMG;AACH,wBAAsB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAiCpF"}
package/dist/events.js CHANGED
@@ -13,14 +13,17 @@ let buildId = 0;
13
13
  * isolates and the Node prod server share one lifetime; `onStop` clears it so
14
14
  * dev HMR reloads can boot a fresh module.
15
15
  */
16
- function buildEntry(sourcePath) {
16
+ export function buildEventsEntry(sourcePath) {
17
17
  return [
18
18
  `import * as __events from ${JSON.stringify(sourcePath)};`,
19
19
  '',
20
20
  'const __store = () => (globalThis.__vesk_server_ctx ||= {});',
21
21
  '',
22
22
  'const __handlers = {};',
23
- "for (const __name of ['onStart', 'onRequest', 'onStop']) {",
23
+ // `onError` joins the server events: production has no plugin objects, so
24
+ // this is the seam an error reporter actually uses there (plugins carry
25
+ // their own `onError` in dev, where plugin objects do exist).
26
+ "for (const __name of ['onStart', 'onRequest', 'onStop', 'onError']) {",
24
27
  ' const __fn = __events[__name];',
25
28
  " if (typeof __fn === 'function') __handlers[__name] = __fn;",
26
29
  '}',
@@ -61,9 +64,26 @@ function buildEntry(sourcePath) {
61
64
  'export const onStart = __handlers.onStart;',
62
65
  'export const onRequest = __handlers.onRequest;',
63
66
  'export const onStop = __handlers.onStop;',
67
+ 'export const onError = __handlers.onError;',
64
68
  '',
65
69
  ].join('\n');
66
70
  }
71
+ /**
72
+ * Report a production failure to `_events.ts`'s `onError`, if it exports one.
73
+ * Never throws: a reporting outage must not add a second failure to a request
74
+ * that already failed.
75
+ */
76
+ export async function executeError(err, base, mod) {
77
+ const fn = mod?.onError;
78
+ if (typeof fn !== 'function')
79
+ return;
80
+ try {
81
+ await fn(err, base);
82
+ }
83
+ catch (e) {
84
+ console.error('vesk: _events onError threw and was ignored:', e instanceof Error ? e.message : e);
85
+ }
86
+ }
67
87
  /**
68
88
  * Compile the app's `_events.ts` (if present) into a self-contained
69
89
  * `server/events.js` next to the server runtime. Returns true when an events
@@ -78,7 +98,7 @@ export async function compileEvents(appDir, outDir) {
78
98
  const serverDir = resolve(outDir, 'server');
79
99
  mkdirSync(serverDir, { recursive: true });
80
100
  const entryFile = resolve(serverDir, `.events-entry-${buildId++}.mjs`);
81
- writeFileSync(entryFile, buildEntry(sourcePath), 'utf-8');
101
+ writeFileSync(entryFile, buildEventsEntry(sourcePath), 'utf-8');
82
102
  try {
83
103
  const result = await esbuild.build({
84
104
  entryPoints: [entryFile],
@@ -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;AAkDD,wBAAsB,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CA6UpG;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;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,CAqWpG;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
@@ -1,11 +1,16 @@
1
1
  import { mkdirSync, writeFileSync, existsSync, readFileSync } from 'node:fs';
2
- import { resolve, dirname, relative, basename } from 'node:path';
2
+ import { resolve, dirname, relative, basename, join } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { bundleRuntime } from '@vesk/adapter/src/runtime-bundle';
5
5
  import { generateSsrFunction } from '@vesk/adapter/src/ssr-function';
6
6
  import { resolveUserCssPath, isTailwindPlugin, resolveCssUrls, hasBuiltGlobalCss } from '@vesk/adapter/src/css';
7
7
  import { collectActionIds } from '@vesk/compiler/src/actions';
8
8
  import { generateApiFunction } from '@vesk/adapter/src/api-function';
9
+ import { describeAsset } from '@vesk/adapter/src/asset-hash';
10
+ // The adapter's own package.json, for the version stamped into the manifest.
11
+ function PACKAGES_DIR_SELF() {
12
+ return resolve(dirname(fileURLToPath(import.meta.url)), '..');
13
+ }
9
14
  import { compileMiddleware, compileMiddlewareCode } from '@vesk/adapter/src/middleware';
10
15
  import { compileEvents } from '@vesk/adapter/src/events';
11
16
  import { generateClientBundle } from '@vesk/adapter/src/client-bundle';
@@ -219,6 +224,39 @@ export async function build(appDir, options) {
219
224
  const apiTree = existsSync(apiDir) ? scanApiRoutes(apiDir) : [];
220
225
  console.error(`vesk build: ${routeTree.length} root routes, ${apiTree.length} API routes`);
221
226
  markPhase('scan routes');
227
+ console.error('vesk build: bundling client runtime...');
228
+ const bundleOpts = {};
229
+ if (options?.codeSplit)
230
+ bundleOpts.codeSplit = true;
231
+ if (options?.hmr)
232
+ bundleOpts.hmr = true;
233
+ if (options?.routeDataCache !== undefined)
234
+ bundleOpts.routeDataCache = options.routeDataCache;
235
+ if (pluginsPipelines.length > 0)
236
+ bundleOpts.plugins = pluginsPipelines;
237
+ const { main, chunks } = await generateClientBundle(routeTree, appDir, componentMap, bundleOpts);
238
+ markPhase('client runtime + chunks');
239
+ // Content-hash every emitted browser asset. A hashed name can be served
240
+ // immutable and can never be stale in a CDN; the digest rides along as the
241
+ // SRI attribute, so a tag can only execute the bytes this build produced.
242
+ // Dev keeps the plain name — the dev server rewrites it on every edit and HMR
243
+ // needs a stable URL.
244
+ const staticDir = resolve(outDir, 'static');
245
+ const hashed = !options?.hmr;
246
+ const clientAsset = describeAsset('client.js', main);
247
+ const clientFileName = hashed ? clientAsset.fileName : 'client.js';
248
+ writeFileSync(resolve(staticDir, clientFileName), main, 'utf-8');
249
+ const clientUrl = `/_vesk/static/${clientFileName}`;
250
+ const mode = chunks.length > 0 ? 'code-split' : 'monolithic';
251
+ console.error(`vesk build: client → static/${clientFileName} (${main.length} bytes, ${mode}${hashed ? `, ${clientAsset.hash}` : ''})`);
252
+ const emittedAssets = [clientAsset];
253
+ for (const chunk of chunks) {
254
+ const asset = describeAsset(chunk.name, chunk.code);
255
+ emittedAssets.push(asset);
256
+ writeFileSync(resolve(staticDir, hashed ? asset.fileName : chunk.name), chunk.code, 'utf-8');
257
+ console.error(`vesk build: chunk → static/${hashed ? asset.fileName : chunk.name} (${chunk.code.length} bytes${hashed ? `, ${asset.hash}` : ''})`);
258
+ }
259
+ const clientIntegrity = hashed ? clientAsset.integrity : undefined;
222
260
  console.error('vesk build: bundling server runtime...');
223
261
  await bundleRuntime(appDir, outDir);
224
262
  const ssrRoutes = [];
@@ -242,7 +280,7 @@ export async function build(appDir, options) {
242
280
  // For standalone routes the layout chain is cleared — only the
243
281
  // standalone node's own layout (if any) applies, no root ancestors.
244
282
  const ssrAncestorLayouts = isStandalone ? [] : ancestorLayouts;
245
- const { funcPath, funcCode, name } = generateSsrFunction(node, appDir, outDir, componentMap, { ancestorLayouts: ssrAncestorLayouts, middlewareCode: mwCode, headExtra: pluginHeadExtra });
283
+ const { funcPath, funcCode, name } = generateSsrFunction(node, appDir, outDir, componentMap, { ancestorLayouts: ssrAncestorLayouts, middlewareCode: mwCode, headExtra: pluginHeadExtra, clientScriptUrl: clientUrl, clientScriptIntegrity: clientIntegrity });
246
284
  writeFileSync(funcPath, funcCode, 'utf-8');
247
285
  const pagePath = resolve(appDir, node.sourceDir, 'page.vsk');
248
286
  if (existsSync(pagePath)) {
@@ -302,28 +340,6 @@ export async function build(appDir, options) {
302
340
  const hasEvents = await compileEvents(appDir, outDir);
303
341
  if (hasEvents)
304
342
  console.error('vesk build: evt → server/events.js (onStart/onRequest/onStop)');
305
- markPhase('api functions');
306
- console.error('vesk build: bundling client runtime...');
307
- const bundleOpts = {};
308
- if (options?.codeSplit)
309
- bundleOpts.codeSplit = true;
310
- if (options?.hmr)
311
- bundleOpts.hmr = true;
312
- if (options?.routeDataCache !== undefined)
313
- bundleOpts.routeDataCache = options.routeDataCache;
314
- if (pluginsPipelines.length > 0)
315
- bundleOpts.plugins = pluginsPipelines;
316
- const { main, chunks } = await generateClientBundle(routeTree, appDir, componentMap, bundleOpts);
317
- writeFileSync(resolve(outDir, 'static', 'client.js'), main, 'utf-8');
318
- const mode = chunks.length > 0 ? 'code-split' : 'monolithic';
319
- console.error(`vesk build: client → static/client.js (${main.length} bytes, ${mode})`);
320
- if (chunks.length > 0) {
321
- const staticDir = resolve(outDir, 'static');
322
- for (const chunk of chunks) {
323
- writeFileSync(resolve(staticDir, chunk.name), chunk.code, 'utf-8');
324
- console.error(`vesk build: chunk → static/${chunk.name} (${chunk.code.length} bytes)`);
325
- }
326
- }
327
343
  copyStaticAssets(publicDir, outDir);
328
344
  markPhase('client runtime + chunks');
329
345
  console.error('vesk build: static → static/public/');
@@ -366,6 +382,17 @@ export async function build(appDir, options) {
366
382
  }
367
383
  markPhase('ssg+images+seo');
368
384
  const manifest = generateManifest(routeTree, ssrRoutes, apiRoutes, prerenderedRoutes, middlewareEnabled, actionMap, pluginHeadExtra, hasEvents);
385
+ // The asset map is what makes a deploy auditable: sizes, content hashes and
386
+ // the SRI digests a page can carry.
387
+ // The build id is the asset map's own fingerprint: same assets, same id.
388
+ const buildId = emittedAssets.map((a) => a.hash).join('-');
389
+ manifest.buildId = buildId.slice(0, 32);
390
+ manifest.veskVersion = JSON.parse(readFileSync(join(PACKAGES_DIR_SELF(), 'package.json'), 'utf-8')).version;
391
+ manifest.assets = {
392
+ hashed,
393
+ client: { file: clientFileName, url: clientUrl, hash: clientAsset.hash, bytes: clientAsset.bytes, integrity: clientAsset.integrity },
394
+ chunks: emittedAssets.slice(1).map((a) => ({ file: a.fileName, hash: a.hash, bytes: a.bytes, integrity: a.integrity })),
395
+ };
369
396
  writeFileSync(resolve(outDir, 'config.json'), JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
370
397
  console.error('vesk build: config → config.json');
371
398
  {
@@ -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;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,CA8bxI"}
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,9 +1,12 @@
1
1
  import { readFileSync, existsSync, statSync } from 'node:fs';
2
- import { resolve, extname, dirname } from 'node:path';
2
+ import { resolve, extname, dirname, basename } from 'node:path';
3
3
  import { createServer } from 'node:http';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { createRateLimiter, resolveComponentName, safeJsonForScript, getClientProtocol, DEFAULT_MAX_BODY_BYTES } from '@vesk/compiler/src/server-codegen';
6
6
  import { securityHeaders } from '@vesk/compiler/src/server-utils';
7
+ import { cacheControlFor } from '@vesk/adapter/src/asset-hash';
8
+ import { reportServerError } from '@vesk/adapter/src/error-report';
9
+ import { executeError } from '@vesk/adapter/src/events';
7
10
  import { resolveWithin, installMdReadHook } from '@vesk/adapter/src/paths';
8
11
  import { resolveCssUrls, hasBuiltGlobalCss } from '@vesk/adapter/src/css';
9
12
  import { loadVeskConfig } from '@vesk/adapter/src/load-config';
@@ -287,10 +290,16 @@ export async function startProdServer(outDir, options) {
287
290
  res.end(lines.join('\n') || '// no ssr data');
288
291
  return;
289
292
  }
293
+ // Stable alias for the client runtime: the file on disk is content-hashed in
294
+ // production, so the name is resolved from the manifest rather than assumed.
290
295
  if (url.pathname === '/_vesk/runtime.js') {
291
- const clientPath = resolve(staticDir, 'client.js');
296
+ const clientFile = buildConfig?.assets?.client?.file || 'client.js';
297
+ const clientPath = resolve(staticDir, clientFile);
292
298
  if (existsSync(clientPath)) {
293
- res.writeHead(200, { 'Content-Type': 'application/javascript' });
299
+ res.writeHead(200, {
300
+ 'Content-Type': 'application/javascript',
301
+ 'Cache-Control': cacheControlFor(clientFile, false),
302
+ });
294
303
  res.end(readFileSync(clientPath));
295
304
  return;
296
305
  }
@@ -305,7 +314,13 @@ export async function startProdServer(outDir, options) {
305
314
  }
306
315
  if (existsSync(staticPath) && statSync(staticPath).isFile()) {
307
316
  const ext = extname(staticPath);
308
- res.writeHead(200, { 'Content-Type': MIME[ext] || 'application/octet-stream' });
317
+ res.writeHead(200, {
318
+ 'Content-Type': MIME[ext] || 'application/octet-stream',
319
+ // A content-hashed name can never change meaning, so it is immutable
320
+ // for a year; anything unhashed must be revalidated or a deploy would
321
+ // serve stale JS forever.
322
+ 'Cache-Control': cacheControlFor(basename(staticPath), false),
323
+ });
309
324
  res.end(readFileSync(staticPath));
310
325
  return;
311
326
  }
@@ -498,6 +513,19 @@ export async function startProdServer(outDir, options) {
498
513
  return;
499
514
  }
500
515
  console.error('vesk ssr error:', err.message);
516
+ // Two seams, both best-effort: a plugin's `onError` (dev-style
517
+ // plugin objects don't exist here) and `_events.ts`'s, which IS
518
+ // baked into the production build.
519
+ await reportServerError(err, {
520
+ url: url.pathname + url.search,
521
+ routePath: route.path,
522
+ target: 'node',
523
+ status: 500,
524
+ buildConfig,
525
+ });
526
+ if (eventsMod) {
527
+ await executeError(err, { url: url.href, routePath: route.path, target: 'node', status: 500 }, eventsMod);
528
+ }
501
529
  const errPath = resolve(appDir, 'error.vsk');
502
530
  let errorHtml = null;
503
531
  if (existsSync(errPath)) {
@@ -508,7 +536,7 @@ export async function startProdServer(outDir, options) {
508
536
  const compName = resolveComponentName(src) || 'Error';
509
537
  // never leak stack traces / internal messages to prod error pages
510
538
  const expose = process.env.NODE_ENV !== 'production';
511
- errorHtml = await renderFullPage(src, compName, { error: expose ? err.message : 'Internal Server Error', stack: expose ? err.stack : '', statusCode: 500, url: url.pathname }, new Map(), { hydrate: true, cssUrls: pageCssUrls, clientScriptUrl: '/_vesk/static/client.js', security: securityConfig?.security || {}, externalDataScript: storeDataScriptGlobal, sourcePath: errPath, headExtra: bakedHeadExtra });
539
+ errorHtml = await renderFullPage(src, compName, { error: expose ? err.message : 'Internal Server Error', stack: expose ? err.stack : '', statusCode: 500, url: url.pathname }, new Map(), { hydrate: true, cssUrls: pageCssUrls, clientScriptUrl: buildConfig?.assets?.client?.url || '/_vesk/static/client.js', clientScriptIntegrity: buildConfig?.assets?.client?.integrity, security: securityConfig?.security || {}, externalDataScript: storeDataScriptGlobal, sourcePath: errPath, headExtra: bakedHeadExtra });
512
540
  }
513
541
  catch { }
514
542
  }
@@ -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,CAoZtD"}
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"}
@@ -134,7 +134,10 @@ export function generateSsrFunction(routeNode, appDir, outDir, componentMap, opt
134
134
  const urlParts = routeNode.fullPath.split('/').filter(Boolean);
135
135
  const paramExprs = buildParamExtraction(routeNode, urlParts);
136
136
  const paramsCode = `function __paramsFor(pathname) {\n const urlParts = pathname.split('/').filter(Boolean);\n return { ${paramExprs.join(', ')} };\n}\n`;
137
- const clientScriptOption = ', clientScriptUrl: "/_vesk/static/client.js"';
137
+ // Content-hashed in production (the build computes it before generating the
138
+ // functions), the stable name in dev.
139
+ const clientScriptOption = `, clientScriptUrl: ${JSON.stringify(options?.clientScriptUrl || '/_vesk/static/client.js')}` +
140
+ (options?.clientScriptIntegrity ? `, clientScriptIntegrity: ${JSON.stringify(options.clientScriptIntegrity)}` : '');
138
141
  const dataScriptOption = ', externalDataScript: storeDataScriptGlobal';
139
142
  let registryCode = '';
140
143
  const compRegEntries = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vesk/adapter",
3
- "version": "0.2.50-canary.2",
3
+ "version": "0.2.50-canary.4",
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.50-canary.2",
27
- "@vesk/runtime": "^0.2.50-canary.2",
28
- "@vesk/types": "^0.2.50-canary.2"
26
+ "@vesk/compiler": "^0.2.50-canary.4",
27
+ "@vesk/runtime": "^0.2.50-canary.4",
28
+ "@vesk/types": "^0.2.50-canary.4"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@types/ws": "^8.18.1",