@vesk/adapter 0.2.50-canary.1 → 0.2.50-canary.3

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;AAQjG,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,CAoqBvG"}
@@ -1,10 +1,11 @@
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';
8
9
  import { buildErrorPayload, createHmrServer } from './hmr';
9
10
  import * as hmrApi from './hmr';
10
11
  import { createDevApiRouter } from './dev-api';
@@ -572,7 +573,9 @@ export async function startDevServer(appDir, options) {
572
573
  }
573
574
  if (existsSync(staticPath) && statSync(staticPath).isFile()) {
574
575
  const ext = extname(staticPath);
575
- res.writeHead(200, { 'Content-Type': MIME[ext] || 'application/octet-stream' });
576
+ // Dev names are stable so HMR can rewrite them, which means they must
577
+ // never be cached: the same URL has to serve the newest bytes.
578
+ res.writeHead(200, { 'Content-Type': MIME[ext] || 'application/octet-stream', 'Cache-Control': cacheControlFor(basename(staticPath), true) });
576
579
  res.end(readFileSync(staticPath));
577
580
  return;
578
581
  }
@@ -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":"AAcA,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,CAiWpG;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
@@ -6,6 +6,7 @@ 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';
9
10
  import { compileMiddleware, compileMiddlewareCode } from '@vesk/adapter/src/middleware';
10
11
  import { compileEvents } from '@vesk/adapter/src/events';
11
12
  import { generateClientBundle } from '@vesk/adapter/src/client-bundle';
@@ -219,6 +220,39 @@ export async function build(appDir, options) {
219
220
  const apiTree = existsSync(apiDir) ? scanApiRoutes(apiDir) : [];
220
221
  console.error(`vesk build: ${routeTree.length} root routes, ${apiTree.length} API routes`);
221
222
  markPhase('scan routes');
223
+ console.error('vesk build: bundling client runtime...');
224
+ const bundleOpts = {};
225
+ if (options?.codeSplit)
226
+ bundleOpts.codeSplit = true;
227
+ if (options?.hmr)
228
+ bundleOpts.hmr = true;
229
+ if (options?.routeDataCache !== undefined)
230
+ bundleOpts.routeDataCache = options.routeDataCache;
231
+ if (pluginsPipelines.length > 0)
232
+ bundleOpts.plugins = pluginsPipelines;
233
+ const { main, chunks } = await generateClientBundle(routeTree, appDir, componentMap, bundleOpts);
234
+ markPhase('client runtime + chunks');
235
+ // Content-hash every emitted browser asset. A hashed name can be served
236
+ // immutable and can never be stale in a CDN; the digest rides along as the
237
+ // SRI attribute, so a tag can only execute the bytes this build produced.
238
+ // Dev keeps the plain name — the dev server rewrites it on every edit and HMR
239
+ // needs a stable URL.
240
+ const staticDir = resolve(outDir, 'static');
241
+ const hashed = !options?.hmr;
242
+ const clientAsset = describeAsset('client.js', main);
243
+ const clientFileName = hashed ? clientAsset.fileName : 'client.js';
244
+ writeFileSync(resolve(staticDir, clientFileName), main, 'utf-8');
245
+ const clientUrl = `/_vesk/static/${clientFileName}`;
246
+ const mode = chunks.length > 0 ? 'code-split' : 'monolithic';
247
+ console.error(`vesk build: client → static/${clientFileName} (${main.length} bytes, ${mode}${hashed ? `, ${clientAsset.hash}` : ''})`);
248
+ const emittedAssets = [clientAsset];
249
+ for (const chunk of chunks) {
250
+ const asset = describeAsset(chunk.name, chunk.code);
251
+ emittedAssets.push(asset);
252
+ writeFileSync(resolve(staticDir, hashed ? asset.fileName : chunk.name), chunk.code, 'utf-8');
253
+ console.error(`vesk build: chunk → static/${hashed ? asset.fileName : chunk.name} (${chunk.code.length} bytes${hashed ? `, ${asset.hash}` : ''})`);
254
+ }
255
+ const clientIntegrity = hashed ? clientAsset.integrity : undefined;
222
256
  console.error('vesk build: bundling server runtime...');
223
257
  await bundleRuntime(appDir, outDir);
224
258
  const ssrRoutes = [];
@@ -242,7 +276,7 @@ export async function build(appDir, options) {
242
276
  // For standalone routes the layout chain is cleared — only the
243
277
  // standalone node's own layout (if any) applies, no root ancestors.
244
278
  const ssrAncestorLayouts = isStandalone ? [] : ancestorLayouts;
245
- const { funcPath, funcCode, name } = generateSsrFunction(node, appDir, outDir, componentMap, { ancestorLayouts: ssrAncestorLayouts, middlewareCode: mwCode, headExtra: pluginHeadExtra });
279
+ const { funcPath, funcCode, name } = generateSsrFunction(node, appDir, outDir, componentMap, { ancestorLayouts: ssrAncestorLayouts, middlewareCode: mwCode, headExtra: pluginHeadExtra, clientScriptUrl: clientUrl, clientScriptIntegrity: clientIntegrity });
246
280
  writeFileSync(funcPath, funcCode, 'utf-8');
247
281
  const pagePath = resolve(appDir, node.sourceDir, 'page.vsk');
248
282
  if (existsSync(pagePath)) {
@@ -302,28 +336,6 @@ export async function build(appDir, options) {
302
336
  const hasEvents = await compileEvents(appDir, outDir);
303
337
  if (hasEvents)
304
338
  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
339
  copyStaticAssets(publicDir, outDir);
328
340
  markPhase('client runtime + chunks');
329
341
  console.error('vesk build: static → static/public/');
@@ -366,6 +378,13 @@ export async function build(appDir, options) {
366
378
  }
367
379
  markPhase('ssg+images+seo');
368
380
  const manifest = generateManifest(routeTree, ssrRoutes, apiRoutes, prerenderedRoutes, middlewareEnabled, actionMap, pluginHeadExtra, hasEvents);
381
+ // The asset map is what makes a deploy auditable: sizes, content hashes and
382
+ // the SRI digests a page can carry.
383
+ manifest.assets = {
384
+ hashed,
385
+ client: { file: clientFileName, url: clientUrl, hash: clientAsset.hash, bytes: clientAsset.bytes, integrity: clientAsset.integrity },
386
+ chunks: emittedAssets.slice(1).map((a) => ({ file: a.fileName, hash: a.hash, bytes: a.bytes, integrity: a.integrity })),
387
+ };
369
388
  writeFileSync(resolve(outDir, 'config.json'), JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
370
389
  console.error('vesk build: config → config.json');
371
390
  {
@@ -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;AA+CjG,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,KAAK,GAAG;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CAIzE;AAiGD,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,CA0cxI"}
@@ -1,9 +1,10 @@
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';
7
8
  import { resolveWithin, installMdReadHook } from '@vesk/adapter/src/paths';
8
9
  import { resolveCssUrls, hasBuiltGlobalCss } from '@vesk/adapter/src/css';
9
10
  import { loadVeskConfig } from '@vesk/adapter/src/load-config';
@@ -287,10 +288,16 @@ export async function startProdServer(outDir, options) {
287
288
  res.end(lines.join('\n') || '// no ssr data');
288
289
  return;
289
290
  }
291
+ // Stable alias for the client runtime: the file on disk is content-hashed in
292
+ // production, so the name is resolved from the manifest rather than assumed.
290
293
  if (url.pathname === '/_vesk/runtime.js') {
291
- const clientPath = resolve(staticDir, 'client.js');
294
+ const clientFile = buildConfig?.assets?.client?.file || 'client.js';
295
+ const clientPath = resolve(staticDir, clientFile);
292
296
  if (existsSync(clientPath)) {
293
- res.writeHead(200, { 'Content-Type': 'application/javascript' });
297
+ res.writeHead(200, {
298
+ 'Content-Type': 'application/javascript',
299
+ 'Cache-Control': cacheControlFor(clientFile, false),
300
+ });
294
301
  res.end(readFileSync(clientPath));
295
302
  return;
296
303
  }
@@ -305,7 +312,13 @@ export async function startProdServer(outDir, options) {
305
312
  }
306
313
  if (existsSync(staticPath) && statSync(staticPath).isFile()) {
307
314
  const ext = extname(staticPath);
308
- res.writeHead(200, { 'Content-Type': MIME[ext] || 'application/octet-stream' });
315
+ res.writeHead(200, {
316
+ 'Content-Type': MIME[ext] || 'application/octet-stream',
317
+ // A content-hashed name can never change meaning, so it is immutable
318
+ // for a year; anything unhashed must be revalidated or a deploy would
319
+ // serve stale JS forever.
320
+ 'Cache-Control': cacheControlFor(basename(staticPath), false),
321
+ });
309
322
  res.end(readFileSync(staticPath));
310
323
  return;
311
324
  }
@@ -508,7 +521,7 @@ export async function startProdServer(outDir, options) {
508
521
  const compName = resolveComponentName(src) || 'Error';
509
522
  // never leak stack traces / internal messages to prod error pages
510
523
  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 });
524
+ 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
525
  }
513
526
  catch { }
514
527
  }
@@ -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.1",
3
+ "version": "0.2.50-canary.3",
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.1",
27
- "@vesk/runtime": "^0.2.50-canary.1",
28
- "@vesk/types": "^0.2.50-canary.1"
26
+ "@vesk/compiler": "^0.2.50-canary.3",
27
+ "@vesk/runtime": "^0.2.50-canary.3",
28
+ "@vesk/types": "^0.2.50-canary.3"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@types/ws": "^8.18.1",