@vesk/adapter 0.2.50-canary.2 → 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.
- package/dist/asset-hash.d.ts +26 -0
- package/dist/asset-hash.d.ts.map +1 -0
- package/dist/asset-hash.js +63 -0
- package/dist/client-bundle.d.ts.map +1 -1
- package/dist/client-bundle.js +12 -3
- package/dist/dev-server.d.ts.map +1 -1
- package/dist/dev-server.js +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +42 -23
- package/dist/prod-server.d.ts.map +1 -1
- package/dist/prod-server.js +18 -5
- package/dist/ssr-function.d.ts.map +1 -1
- package/dist/ssr-function.js +4 -1
- package/package.json +4 -4
|
@@ -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":"
|
|
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"}
|
package/dist/client-bundle.js
CHANGED
|
@@ -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
|
-
|
|
986
|
-
|
|
987
|
-
|
|
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
|
}
|
package/dist/dev-server.d.ts.map
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/dev-server.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
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;
|
|
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"}
|
package/dist/prod-server.js
CHANGED
|
@@ -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
|
|
294
|
+
const clientFile = buildConfig?.assets?.client?.file || 'client.js';
|
|
295
|
+
const clientPath = resolve(staticDir, clientFile);
|
|
292
296
|
if (existsSync(clientPath)) {
|
|
293
|
-
res.writeHead(200, {
|
|
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, {
|
|
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,
|
|
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"}
|
package/dist/ssr-function.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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.
|
|
27
|
-
"@vesk/runtime": "^0.2.50-canary.
|
|
28
|
-
"@vesk/types": "^0.2.50-canary.
|
|
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",
|