@rshono/core 1.0.0-rc.6 → 1.0.0-rc.8
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/README.md +195 -168
- package/dist/builder/page-files.d.ts.map +1 -1
- package/dist/builder/page-files.js +7 -3
- package/dist/builder/page-files.js.map +1 -1
- package/dist/builder/public-env.d.ts +6 -0
- package/dist/builder/public-env.d.ts.map +1 -1
- package/dist/builder/public-env.js +6 -0
- package/dist/builder/public-env.js.map +1 -1
- package/dist/builder/rspack-config.d.ts.map +1 -1
- package/dist/builder/rspack-config.js +16 -19
- package/dist/builder/rspack-config.js.map +1 -1
- package/dist/cli/dev.d.ts.map +1 -1
- package/dist/cli/dev.js +63 -35
- package/dist/cli/dev.js.map +1 -1
- package/dist/cli/index.js +3 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/config.d.ts +56 -55
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +17 -1
- package/dist/config.js.map +1 -1
- package/dist/deploy/contract.d.ts +15 -10
- package/dist/deploy/contract.d.ts.map +1 -1
- package/dist/deploy/contract.js.map +1 -1
- package/dist/deploy/filesystem.d.ts.map +1 -1
- package/dist/deploy/filesystem.js +6 -7
- package/dist/deploy/filesystem.js.map +1 -1
- package/dist/deploy/node/runtime.d.ts +5 -5
- package/dist/deploy/node/runtime.d.ts.map +1 -1
- package/dist/deploy/node/runtime.js +7 -11
- package/dist/deploy/node/runtime.js.map +1 -1
- package/dist/deploy/presets.d.ts +2 -6
- package/dist/deploy/presets.d.ts.map +1 -1
- package/dist/deploy/presets.js +2 -6
- package/dist/deploy/presets.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/router.d.ts +74 -36
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +2 -3
- package/dist/router.js.map +1 -1
- package/dist/runtime/boundaries.d.ts +10 -0
- package/dist/runtime/boundaries.d.ts.map +1 -1
- package/dist/runtime/boundaries.js +6 -0
- package/dist/runtime/boundaries.js.map +1 -1
- package/dist/runtime/client.d.ts +5 -3
- package/dist/runtime/client.d.ts.map +1 -1
- package/dist/runtime/client.js +5 -3
- package/dist/runtime/client.js.map +1 -1
- package/dist/runtime/context.d.ts +195 -45
- package/dist/runtime/context.d.ts.map +1 -1
- package/dist/runtime/context.js +277 -59
- package/dist/runtime/context.js.map +1 -1
- package/dist/runtime/control.d.ts.map +1 -1
- package/dist/runtime/control.js +7 -0
- package/dist/runtime/control.js.map +1 -1
- package/dist/runtime/entry.client.js +29 -47
- package/dist/runtime/entry.client.js.map +1 -1
- package/dist/runtime/entry.rsc.d.ts.map +1 -1
- package/dist/runtime/entry.rsc.js +109 -140
- package/dist/runtime/entry.rsc.js.map +1 -1
- package/dist/runtime/flight-inject.d.ts +4 -4
- package/dist/runtime/flight-inject.js +2 -2
- package/dist/runtime/flight-inject.js.map +1 -1
- package/dist/runtime/navigation.d.ts +11 -0
- package/dist/runtime/navigation.d.ts.map +1 -1
- package/dist/runtime/navigation.js +3 -0
- package/dist/runtime/navigation.js.map +1 -1
- package/dist/runtime/request.d.ts +3 -3
- package/dist/runtime/request.js +3 -3
- package/dist/runtime/request.js.map +1 -1
- package/dist/runtime/server.d.ts +7 -12
- package/dist/runtime/server.d.ts.map +1 -1
- package/dist/runtime/server.js +15 -12
- package/dist/runtime/server.js.map +1 -1
- package/dist/server/prerendered.d.ts +20 -35
- package/dist/server/prerendered.d.ts.map +1 -1
- package/dist/server/prerendered.js +8 -18
- package/dist/server/prerendered.js.map +1 -1
- package/dist/server/server-config.d.ts +10 -29
- package/dist/server/server-config.d.ts.map +1 -1
- package/dist/server/server-config.js +4 -70
- package/dist/server/server-config.js.map +1 -1
- package/dist/server/ssg.d.ts.map +1 -1
- package/dist/server/ssg.js +10 -11
- package/dist/server/ssg.js.map +1 -1
- package/package.json +1 -1
|
@@ -7,11 +7,9 @@
|
|
|
7
7
|
* it from the fs module would drag `node:fs` and `node:crypto` into a bundle that has neither.
|
|
8
8
|
*/
|
|
9
9
|
/**
|
|
10
|
-
* The two representations of a page, prerendered side by side
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* Writing only the HTML meant every in-app click re-rendered a page that was already built, so the
|
|
14
|
-
* prerender only ever paid off for cold loads and crawlers.
|
|
10
|
+
* The two representations of a page, prerendered side by side: a hard load wants the HTML document, a
|
|
11
|
+
* soft navigation asks the same URL for a flight payload. Without both, every in-app click would
|
|
12
|
+
* re-render a page that was already built.
|
|
15
13
|
*/
|
|
16
14
|
export type PrerenderVariant = 'html' | 'flight';
|
|
17
15
|
export declare const VARIANTS: {
|
|
@@ -45,37 +43,27 @@ export declare function prerenderedRelPath(requestPath: string, variant: Prerend
|
|
|
45
43
|
/**
|
|
46
44
|
* A bounded, insertion-ordered cache of prerendered pages.
|
|
47
45
|
*
|
|
48
|
-
* Bounded so a site with thousands of prerendered pages keeps a working set rather than the whole
|
|
49
|
-
* in memory. Only *hits* are ever stored: caching misses would let anyone mint entries by
|
|
50
|
-
* paths that don't exist. The files are written at build time and never change while the
|
|
51
|
-
* so an entry never needs invalidating.
|
|
52
|
-
*
|
|
53
|
-
* Shared because both targets need exactly this and had a hand-rolled copy of it, down to the same
|
|
54
|
-
* eviction line.
|
|
46
|
+
* Bounded so a site with thousands of prerendered pages keeps a working set rather than the whole
|
|
47
|
+
* build in memory. Only *hits* are ever stored: caching misses would let anyone mint entries by
|
|
48
|
+
* requesting paths that don't exist. The files are written at build time and never change while the
|
|
49
|
+
* server is up, so an entry never needs invalidating.
|
|
55
50
|
*/
|
|
56
51
|
export declare function createPageCache(max?: number): {
|
|
57
52
|
get(key: string): PrerenderedPage | undefined;
|
|
58
53
|
set(key: string, page: PrerenderedPage): void;
|
|
59
54
|
};
|
|
60
55
|
/**
|
|
61
|
-
* A weak `ETag` for a page body.
|
|
56
|
+
* A weak `ETag` for a page body — see {@link PrerenderedPage.etag} for why weak.
|
|
62
57
|
*
|
|
63
58
|
* Web Crypto rather than `node:crypto`, so the one implementation serves both a Node server and
|
|
64
|
-
* `workerd
|
|
65
|
-
* conversion, purely because the other one reached for a Node builtin.
|
|
66
|
-
*
|
|
67
|
-
* Deliberately **weak**: the bytes on the wire depend on whether the client took gzip, and a strong
|
|
68
|
-
* validator would have to differ between those two — so the 200 and the 304 that revalidates it would
|
|
69
|
-
* disagree, and a cache would treat them as different pages. A weak tag says "the same
|
|
70
|
-
* representation", which is exactly what is true across content codings.
|
|
59
|
+
* `workerd`.
|
|
71
60
|
*/
|
|
72
61
|
export declare function weakEtag(body: Uint8Array<ArrayBuffer>): Promise<string>;
|
|
73
62
|
/**
|
|
74
63
|
* Assembles a {@link PrerenderedPage} from a body just read out of the build.
|
|
75
64
|
*
|
|
76
|
-
* `storeEtag` is the validator the store supplied, where it has one — it already describes these
|
|
77
|
-
* bytes, so it is preferred over hashing them again, and only weakened.
|
|
78
|
-
* their own copy of this before, with two different digest implementations.
|
|
65
|
+
* `storeEtag` is the validator the store supplied, where it has one — it already describes these
|
|
66
|
+
* exact bytes, so it is preferred over hashing them again, and only weakened.
|
|
79
67
|
*/
|
|
80
68
|
export declare function toPrerenderedPage(body: Uint8Array<ArrayBuffer>, storeEtag?: string | null): Promise<PrerenderedPage>;
|
|
81
69
|
/** A prerendered page, ready to serve: its body and a validator derived from those exact bytes. */
|
|
@@ -84,26 +72,23 @@ export interface PrerenderedPage {
|
|
|
84
72
|
* The document or the flight payload, depending on which {@link PrerenderVariant} was read.
|
|
85
73
|
*
|
|
86
74
|
* Bytes rather than a string, because this is a *cache* entry served verbatim to every request that
|
|
87
|
-
* hits it
|
|
88
|
-
* way out
|
|
89
|
-
* re-encoding bytes it had already encoded once to measure them. Read once, encoded never.
|
|
75
|
+
* hits it — held as a string, each of those would pay a fresh UTF-8 encode of the whole page on the
|
|
76
|
+
* way out. Read once, encoded never.
|
|
90
77
|
*/
|
|
91
78
|
body: Uint8Array<ArrayBuffer>;
|
|
92
79
|
/**
|
|
93
|
-
* `Content-Length` for {@link body}, in bytes rather than characters.
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
* front is entitled to make decisions with it — whether gzipping this response is worth the framing,
|
|
97
|
-
* most obviously.
|
|
80
|
+
* `Content-Length` for {@link body}, in bytes rather than characters. Served explicitly because
|
|
81
|
+
* Hono sets no length for an in-memory body, and a proxy or CDN in front is entitled to make
|
|
82
|
+
* decisions with it — whether gzipping this response is worth the framing, most obviously.
|
|
98
83
|
*/
|
|
99
84
|
contentLength: string;
|
|
100
85
|
/**
|
|
101
86
|
* `ETag` for the page, so a revalidating client can be answered with a 304 instead of the body.
|
|
102
87
|
*
|
|
103
|
-
* Deliberately **weak**. The bytes on the wire depend on whether something in front re-encoded
|
|
104
|
-
* and a strong validator would have to differ per coding — so the 200 and the 304 that
|
|
105
|
-
* could disagree, and a cache would treat them as different pages. A weak tag says
|
|
106
|
-
* representation", which is
|
|
88
|
+
* Deliberately **weak**. The bytes on the wire depend on whether something in front re-encoded
|
|
89
|
+
* them, and a strong validator would have to differ per coding — so the 200 and the 304 that
|
|
90
|
+
* revalidates it could disagree, and a cache would treat them as different pages. A weak tag says
|
|
91
|
+
* "the same representation", which is what stays true across content codings.
|
|
107
92
|
*/
|
|
108
93
|
etag: string;
|
|
109
94
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prerendered.d.ts","sourceRoot":"","sources":["../../src/server/prerendered.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH
|
|
1
|
+
{"version":3,"file":"prerendered.d.ts","sourceRoot":"","sources":["../../src/server/prerendered.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEjD,eAAO,MAAM,QAAQ;;iBACX,IAAI,EAAE,YAAY;iBAAE,MAAM,EAAE,WAAW;iBAAE,WAAW,EAAE,WAAW;;;iBAC/D,IAAI,EAAE,WAAW;iBAAE,MAAM,EAAE,kBAAkB;iBAAE,WAAW,EAAE,kBAAkB;;CACU,CAAC;AAErG;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAyB,GAAG,MAAM,GAAG,IAAI,CAKhG;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,gBAAgB,GAAG,MAAM,GAAG,IAAI,CAGhG;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,GAAG,SAAM,GAAG;IAAE,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAAC;IAAC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,GAAG,IAAI,CAAA;CAAE,CAU3I;AAED;;;;;GAKG;AACH,wBAAsB,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC,WAAW,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,CAO7E;AAED;;;;;GAKG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,UAAU,CAAC,WAAW,CAAC,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,eAAe,CAAC,CAM1H;AAED,mGAAmG;AACnG,MAAM,WAAW,eAAe;IAC9B;;;;;;OAMG;IACH,IAAI,EAAE,UAAU,CAAC,WAAW,CAAC,CAAC;IAC9B;;;;OAIG;IACH,aAAa,EAAE,MAAM,CAAC;IACtB;;;;;;;OAOG;IACH,IAAI,EAAE,MAAM,CAAC;CACd"}
|
|
@@ -39,13 +39,10 @@ export function prerenderedRelPath(requestPath, variant) {
|
|
|
39
39
|
/**
|
|
40
40
|
* A bounded, insertion-ordered cache of prerendered pages.
|
|
41
41
|
*
|
|
42
|
-
* Bounded so a site with thousands of prerendered pages keeps a working set rather than the whole
|
|
43
|
-
* in memory. Only *hits* are ever stored: caching misses would let anyone mint entries by
|
|
44
|
-
* paths that don't exist. The files are written at build time and never change while the
|
|
45
|
-
* so an entry never needs invalidating.
|
|
46
|
-
*
|
|
47
|
-
* Shared because both targets need exactly this and had a hand-rolled copy of it, down to the same
|
|
48
|
-
* eviction line.
|
|
42
|
+
* Bounded so a site with thousands of prerendered pages keeps a working set rather than the whole
|
|
43
|
+
* build in memory. Only *hits* are ever stored: caching misses would let anyone mint entries by
|
|
44
|
+
* requesting paths that don't exist. The files are written at build time and never change while the
|
|
45
|
+
* server is up, so an entry never needs invalidating.
|
|
49
46
|
*/
|
|
50
47
|
export function createPageCache(max = 128) {
|
|
51
48
|
const pages = new Map();
|
|
@@ -60,16 +57,10 @@ export function createPageCache(max = 128) {
|
|
|
60
57
|
};
|
|
61
58
|
}
|
|
62
59
|
/**
|
|
63
|
-
* A weak `ETag` for a page body.
|
|
60
|
+
* A weak `ETag` for a page body — see {@link PrerenderedPage.etag} for why weak.
|
|
64
61
|
*
|
|
65
62
|
* Web Crypto rather than `node:crypto`, so the one implementation serves both a Node server and
|
|
66
|
-
* `workerd
|
|
67
|
-
* conversion, purely because the other one reached for a Node builtin.
|
|
68
|
-
*
|
|
69
|
-
* Deliberately **weak**: the bytes on the wire depend on whether the client took gzip, and a strong
|
|
70
|
-
* validator would have to differ between those two — so the 200 and the 304 that revalidates it would
|
|
71
|
-
* disagree, and a cache would treat them as different pages. A weak tag says "the same
|
|
72
|
-
* representation", which is exactly what is true across content codings.
|
|
63
|
+
* `workerd`.
|
|
73
64
|
*/
|
|
74
65
|
export async function weakEtag(body) {
|
|
75
66
|
const digest = await crypto.subtle.digest('SHA-256', body);
|
|
@@ -82,9 +73,8 @@ export async function weakEtag(body) {
|
|
|
82
73
|
/**
|
|
83
74
|
* Assembles a {@link PrerenderedPage} from a body just read out of the build.
|
|
84
75
|
*
|
|
85
|
-
* `storeEtag` is the validator the store supplied, where it has one — it already describes these
|
|
86
|
-
* bytes, so it is preferred over hashing them again, and only weakened.
|
|
87
|
-
* their own copy of this before, with two different digest implementations.
|
|
76
|
+
* `storeEtag` is the validator the store supplied, where it has one — it already describes these
|
|
77
|
+
* exact bytes, so it is preferred over hashing them again, and only weakened.
|
|
88
78
|
*/
|
|
89
79
|
export async function toPrerenderedPage(body, storeEtag) {
|
|
90
80
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prerendered.js","sourceRoot":"","sources":["../../src/server/prerendered.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;
|
|
1
|
+
{"version":3,"file":"prerendered.js","sourceRoot":"","sources":["../../src/server/prerendered.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AASH,MAAM,CAAC,MAAM,QAAQ,GAAG;IACtB,IAAI,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW,EAAE;IAC3E,MAAM,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,kBAAkB,EAAE,WAAW,EAAE,kBAAkB,EAAE;CACQ,CAAC;AAErG;;;;;;;GAOG;AACH,MAAM,UAAU,WAAW,CAAC,SAAiB,EAAE,OAAO,GAAqB,MAAM;IAC/E,IAAI,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC;QAAE,OAAO,IAAI,CAAC;IACxC,MAAM,OAAO,GAAG,SAAS,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC;IACpD,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC;IACpC,OAAO,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,OAAO,IAAI,IAAI,EAAE,CAAC;AACtD,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,WAAmB,EAAE,OAAyB;IAC/E,IAAI,mBAAmB,CAAC,IAAI,CAAC,WAAW,CAAC;QAAE,OAAO,IAAI,CAAC;IACvD,OAAO,WAAW,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;AAC3C,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAAC,GAAG,GAAG,GAAG;IACvC,MAAM,KAAK,GAAG,IAAI,GAAG,EAA2B,CAAC;IACjD,OAAO;QACL,GAAG,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC;QAC5B,GAAG,CAAC,GAAG,EAAE,IAAI;YACX,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;YACrB,yFAAyF;YACzF,IAAI,KAAK,CAAC,IAAI,GAAG,GAAG;gBAAE,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,KAAM,CAAC,CAAC;QACjE,CAAC;KACF,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,IAA6B;IAC1D,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;IAC3D,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,GAAG,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC;SACnE,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC;SACnB,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC;SACnB,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACtB,OAAO,MAAM,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC;AACzC,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,IAA6B,EAAE,SAAyB;IAC9F,OAAO;QACL,IAAI;QACJ,aAAa,EAAE,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC;QACtC,IAAI,EAAE,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,QAAQ,CAAC,IAAI,CAAC;KAC7E,CAAC;AACJ,CAAC","sourcesContent":["/**\n * Everything about a prerendered page that doesn't need a filesystem: where the build puts it, and\n * what it looks like once read back.\n *\n * Split out of `ssg.ts` — which owns the writing and the reading, both through `node:fs` — because a\n * deploy target without a filesystem reads the very same layout out of an asset store, and importing\n * it from the fs module would drag `node:fs` and `node:crypto` into a bundle that has neither.\n */\n\n/**\n * The two representations of a page, prerendered side by side: a hard load wants the HTML document, a\n * soft navigation asks the same URL for a flight payload. Without both, every in-app click would\n * re-render a page that was already built.\n */\nexport type PrerenderVariant = 'html' | 'flight';\n\nexport const VARIANTS = {\n html: { file: 'index.html', accept: 'text/html', contentType: 'text/html' },\n flight: { file: 'index.rsc', accept: 'text/x-component', contentType: 'text/x-component' },\n} as const satisfies Record<PrerenderVariant, { file: string; accept: string; contentType: string }>;\n\n/**\n * Where a route's prerendered output lives, relative to the output root — or `null` for a path that\n * cannot be prerendered at all (one with a param or a wildcard left in it).\n *\n * Always `/`-separated, never the host's separator: the same string addresses a file on a filesystem\n * (`resolve()` and `join()` both accept forward slashes on Windows) and a key in an asset store,\n * where a backslash would simply be the wrong character.\n */\nexport function ssgFilePath(routePath: string, variant: PrerenderVariant = 'html'): string | null {\n if (/[:*]/.test(routePath)) return null;\n const trimmed = routePath.replace(/^\\/+|\\/+$/g, '');\n const file = VARIANTS[variant].file;\n return trimmed === '' ? file : `${trimmed}/${file}`;\n}\n\n/**\n * {@link ssgFilePath} for a path that came off a request, so traversal is a miss rather than a lookup.\n *\n * The first line of defence for every deploy target: a store addressed by key has no `resolve()` to\n * fall back on, so `..` has to be refused here or not at all.\n */\nexport function prerenderedRelPath(requestPath: string, variant: PrerenderVariant): string | null {\n if (/(^|\\/)\\.\\.?(\\/|$)/.test(requestPath)) return null;\n return ssgFilePath(requestPath, variant);\n}\n\n/**\n * A bounded, insertion-ordered cache of prerendered pages.\n *\n * Bounded so a site with thousands of prerendered pages keeps a working set rather than the whole\n * build in memory. Only *hits* are ever stored: caching misses would let anyone mint entries by\n * requesting paths that don't exist. The files are written at build time and never change while the\n * server is up, so an entry never needs invalidating.\n */\nexport function createPageCache(max = 128): { get(key: string): PrerenderedPage | undefined; set(key: string, page: PrerenderedPage): void } {\n const pages = new Map<string, PrerenderedPage>();\n return {\n get: (key) => pages.get(key),\n set(key, page) {\n pages.set(key, page);\n // Insertion-ordered, so the first key is the oldest. One entry in means at most one out.\n if (pages.size > max) pages.delete(pages.keys().next().value!);\n },\n };\n}\n\n/**\n * A weak `ETag` for a page body — see {@link PrerenderedPage.etag} for why weak.\n *\n * Web Crypto rather than `node:crypto`, so the one implementation serves both a Node server and\n * `workerd`.\n */\nexport async function weakEtag(body: Uint8Array<ArrayBuffer>): Promise<string> {\n const digest = await crypto.subtle.digest('SHA-256', body);\n const base64url = btoa(String.fromCharCode(...new Uint8Array(digest)))\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n .replace(/=+$/, '');\n return `W/\"${base64url.slice(0, 22)}\"`;\n}\n\n/**\n * Assembles a {@link PrerenderedPage} from a body just read out of the build.\n *\n * `storeEtag` is the validator the store supplied, where it has one — it already describes these\n * exact bytes, so it is preferred over hashing them again, and only weakened.\n */\nexport async function toPrerenderedPage(body: Uint8Array<ArrayBuffer>, storeEtag?: string | null): Promise<PrerenderedPage> {\n return {\n body,\n contentLength: String(body.byteLength),\n etag: storeEtag ? storeEtag.replace(/^(?!W\\/)/, 'W/') : await weakEtag(body),\n };\n}\n\n/** A prerendered page, ready to serve: its body and a validator derived from those exact bytes. */\nexport interface PrerenderedPage {\n /**\n * The document or the flight payload, depending on which {@link PrerenderVariant} was read.\n *\n * Bytes rather than a string, because this is a *cache* entry served verbatim to every request that\n * hits it — held as a string, each of those would pay a fresh UTF-8 encode of the whole page on the\n * way out. Read once, encoded never.\n */\n body: Uint8Array<ArrayBuffer>;\n /**\n * `Content-Length` for {@link body}, in bytes rather than characters. Served explicitly because\n * Hono sets no length for an in-memory body, and a proxy or CDN in front is entitled to make\n * decisions with it — whether gzipping this response is worth the framing, most obviously.\n */\n contentLength: string;\n /**\n * `ETag` for the page, so a revalidating client can be answered with a 304 instead of the body.\n *\n * Deliberately **weak**. The bytes on the wire depend on whether something in front re-encoded\n * them, and a strong validator would have to differ per coding — so the 200 and the 304 that\n * revalidates it could disagree, and a cache would treat them as different pages. A weak tag says\n * \"the same representation\", which is what stays true across content codings.\n */\n etag: string;\n}\n"]}
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import type { RshonoConfig } from '../config.js';
|
|
2
2
|
/**
|
|
3
|
-
* The framework settings the server bundle needs at request time, fully resolved
|
|
4
|
-
* (defaults applied, `bodySizeLimit` parsed to bytes, origins normalized to hosts).
|
|
3
|
+
* The framework settings the server bundle needs at request time, fully resolved.
|
|
5
4
|
*
|
|
6
5
|
* The value is produced once by {@link resolveServerConfig} from `rshono.config.ts`
|
|
7
6
|
* and compiled into the server bundle as the `__RSHONO_CONFIG__` literal (see
|
|
8
7
|
* `builder/rspack-config.ts`) — there is no runtime env-var interface for these.
|
|
8
|
+
*
|
|
9
|
+
* Only two fields, because only two things here are decided by the *build*. The per-request
|
|
10
|
+
* security controls that used to live alongside them — the CSRF check, the CSP, the body cap — are
|
|
11
|
+
* Hono middleware an app registers in `src/server.ts`, where they can be configured per route and
|
|
12
|
+
* per environment instead of once per bundle.
|
|
9
13
|
*/
|
|
10
14
|
export interface ServerConfig {
|
|
11
15
|
/**
|
|
@@ -17,41 +21,18 @@ export interface ServerConfig {
|
|
|
17
21
|
isDev: boolean;
|
|
18
22
|
/** Honour `X-Forwarded-Host` / `-Proto` when resolving the browser-facing URL. Forced on in dev. */
|
|
19
23
|
trustProxy: boolean;
|
|
20
|
-
/** Send a strict per-request-nonce Content-Security-Policy with every HTML document. */
|
|
21
|
-
cspEnabled: boolean;
|
|
22
|
-
/** The resolved CSP directives (built-in defaults with the user's merged over them), minus the nonce. */
|
|
23
|
-
cspDirectives: Record<string, string>;
|
|
24
|
-
/** CSRF origin check on server-action POSTs. */
|
|
25
|
-
checkOrigin: boolean;
|
|
26
|
-
/** Extra origins allowed to post server actions, normalized to lowercase `URL.host` values. */
|
|
27
|
-
allowedOrigins: string[];
|
|
28
|
-
/** Max request body in bytes before a 413; `0` disables the cap. */
|
|
29
|
-
maxBodyBytes: number;
|
|
30
24
|
}
|
|
31
25
|
/**
|
|
32
|
-
* The
|
|
26
|
+
* The framework's built-in defaults.
|
|
33
27
|
*
|
|
34
|
-
* `
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* default both fall back to. A config field for them would have been a fourth precedence level for
|
|
38
|
-
* one number, overridden by the environment on every host that matters.
|
|
28
|
+
* `port` and `host` are not config fields at all — they come from `PORT` / `HOST` (or `--port`)
|
|
29
|
+
* wherever the address is resolved, `deploy/node/runtime.ts` for a server bundle and `cli/dev.ts`
|
|
30
|
+
* for the dev server, and this is what both fall back to.
|
|
39
31
|
*/
|
|
40
32
|
export declare const SERVER_DEFAULTS: {
|
|
41
|
-
readonly maxBodyBytes: number;
|
|
42
33
|
readonly port: 3000;
|
|
43
34
|
readonly host: '0.0.0.0';
|
|
44
35
|
};
|
|
45
|
-
/**
|
|
46
|
-
* The built-in {@link RshonoConfig.csp} policy, keyed by directive so
|
|
47
|
-
* {@link RshonoConfig.cspDirectives} can override entries individually.
|
|
48
|
-
*
|
|
49
|
-
* `script-src` carries the per-request nonce, appended at request time (see `entry.rsc.tsx`).
|
|
50
|
-
* `style-src` needs `'unsafe-inline'` because React writes inline styles.
|
|
51
|
-
*/
|
|
52
|
-
export declare const CSP_DEFAULTS: Record<string, string>;
|
|
53
|
-
/** Parse a {@link RshonoConfig.bodySizeLimit} value into a byte count (`false`/`0` → `0`, disabling the cap). */
|
|
54
|
-
export declare function parseByteSize(value: string | number | false | undefined): number | undefined;
|
|
55
36
|
/**
|
|
56
37
|
* Resolve the user's {@link RshonoConfig} into the {@link ServerConfig} baked into the bundle.
|
|
57
38
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server-config.d.ts","sourceRoot":"","sources":["../../src/server/server-config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAEjD
|
|
1
|
+
{"version":3,"file":"server-config.d.ts","sourceRoot":"","sources":["../../src/server/server-config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAEjD;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,YAAY;IAC3B;;;;;OAKG;IACH,KAAK,EAAE,OAAO,CAAC;IACf,oGAAoG;IACpG,UAAU,EAAE,OAAO,CAAC;CACrB;AAED;;;;;;GAMG;AACH,eAAO,MAAM,eAAe;aAC1B,IAAI,EAAE,IAAI;aACV,IAAI,EAAE,SAAS;CACP,CAAC;AAEX;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,YAAY,EAAE,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,EAAE,OAAO,CAAA;CAAE,GAAG,YAAY,CAKrG"}
|
|
@@ -1,72 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
2
|
+
* The framework's built-in defaults.
|
|
3
3
|
*
|
|
4
|
-
* `
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* default both fall back to. A config field for them would have been a fourth precedence level for
|
|
8
|
-
* one number, overridden by the environment on every host that matters.
|
|
4
|
+
* `port` and `host` are not config fields at all — they come from `PORT` / `HOST` (or `--port`)
|
|
5
|
+
* wherever the address is resolved, `deploy/node/runtime.ts` for a server bundle and `cli/dev.ts`
|
|
6
|
+
* for the dev server, and this is what both fall back to.
|
|
9
7
|
*/
|
|
10
8
|
export const SERVER_DEFAULTS = {
|
|
11
|
-
maxBodyBytes: 1024 * 1024, // 1 MiB, matching Next.js's server-action body-size limit.
|
|
12
9
|
port: 3000,
|
|
13
10
|
host: '0.0.0.0',
|
|
14
11
|
};
|
|
15
|
-
/**
|
|
16
|
-
* The built-in {@link RshonoConfig.csp} policy, keyed by directive so
|
|
17
|
-
* {@link RshonoConfig.cspDirectives} can override entries individually.
|
|
18
|
-
*
|
|
19
|
-
* `script-src` carries the per-request nonce, appended at request time (see `entry.rsc.tsx`).
|
|
20
|
-
* `style-src` needs `'unsafe-inline'` because React writes inline styles.
|
|
21
|
-
*/
|
|
22
|
-
export const CSP_DEFAULTS = {
|
|
23
|
-
'default-src': "'self'",
|
|
24
|
-
'script-src': "'self'",
|
|
25
|
-
'style-src': "'self' 'unsafe-inline'",
|
|
26
|
-
'img-src': "'self' data:",
|
|
27
|
-
'connect-src': "'self'",
|
|
28
|
-
// Not covered by default-src, and each closes an injection route of its own: a stray <base>
|
|
29
|
-
// retargeting every relative URL, plugin content, framing (clickjacking), off-site form posts.
|
|
30
|
-
'base-uri': "'self'",
|
|
31
|
-
'object-src': "'none'",
|
|
32
|
-
'frame-ancestors': "'none'",
|
|
33
|
-
'form-action': "'self'",
|
|
34
|
-
};
|
|
35
|
-
const UNITS = { b: 1, kb: 1024, mb: 1024 ** 2, gb: 1024 ** 3 };
|
|
36
|
-
/** Parse a {@link RshonoConfig.bodySizeLimit} value into a byte count (`false`/`0` → `0`, disabling the cap). */
|
|
37
|
-
export function parseByteSize(value) {
|
|
38
|
-
if (value === undefined)
|
|
39
|
-
return undefined;
|
|
40
|
-
if (value === false)
|
|
41
|
-
return 0;
|
|
42
|
-
if (typeof value === 'number')
|
|
43
|
-
return value;
|
|
44
|
-
const match = /^\s*(\d+(?:\.\d+)?)\s*(b|kb|mb|gb)?\s*$/i.exec(value);
|
|
45
|
-
if (!match) {
|
|
46
|
-
throw new Error(`[rshono] invalid bodySizeLimit ${JSON.stringify(value)} — use e.g. '1mb', 1048576, or false.`);
|
|
47
|
-
}
|
|
48
|
-
return Math.floor(Number(match[1]) * UNITS[(match[2] ?? 'b').toLowerCase()]);
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Normalize a config `allowedOrigins` entry (full origin or bare host) to a lowercase `URL.host`,
|
|
52
|
-
* ready for a direct comparison against a parsed `Origin` header's host.
|
|
53
|
-
*
|
|
54
|
-
* A bare `host:port` has to be retried against a base, because on its own `'localhost:4000'`
|
|
55
|
-
* parses as the *scheme* `localhost:` with path `4000` — leaving an empty host that would
|
|
56
|
-
* silently never match anything. Throws rather than passing a junk entry through, so a typo
|
|
57
|
-
* fails the build instead of quietly disabling the allowlist entry it was meant to add.
|
|
58
|
-
*/
|
|
59
|
-
function normalizeOrigin(entry) {
|
|
60
|
-
const host = URL.parse(entry)?.host || URL.parse(`http://${entry}`)?.host;
|
|
61
|
-
if (!host) {
|
|
62
|
-
throw new Error(`[rshono] invalid allowedOrigins entry ${JSON.stringify(entry)} — use a full origin ('https://admin.example.com') or a bare host ('localhost:4000').`);
|
|
63
|
-
}
|
|
64
|
-
return host.toLowerCase();
|
|
65
|
-
}
|
|
66
|
-
/** Drop directives the user blanked out, so `cspDirectives: { 'frame-ancestors': '' }` removes one. */
|
|
67
|
-
function resolveCspDirectives(overrides) {
|
|
68
|
-
return Object.fromEntries(Object.entries({ ...CSP_DEFAULTS, ...overrides }).filter(([, value]) => value.trim() !== ''));
|
|
69
|
-
}
|
|
70
12
|
/**
|
|
71
13
|
* Resolve the user's {@link RshonoConfig} into the {@link ServerConfig} baked into the bundle.
|
|
72
14
|
*
|
|
@@ -78,14 +20,6 @@ export function resolveServerConfig(config, { isDev }) {
|
|
|
78
20
|
return {
|
|
79
21
|
isDev,
|
|
80
22
|
trustProxy: isDev || (config.trustProxy ?? false),
|
|
81
|
-
cspEnabled: config.csp ?? false,
|
|
82
|
-
cspDirectives: resolveCspDirectives(config.cspDirectives),
|
|
83
|
-
checkOrigin: config.checkOrigin ?? true,
|
|
84
|
-
allowedOrigins: (config.allowedOrigins ?? [])
|
|
85
|
-
.map((entry) => entry.trim())
|
|
86
|
-
.filter(Boolean)
|
|
87
|
-
.map(normalizeOrigin),
|
|
88
|
-
maxBodyBytes: parseByteSize(config.bodySizeLimit) ?? SERVER_DEFAULTS.maxBodyBytes,
|
|
89
23
|
};
|
|
90
24
|
}
|
|
91
25
|
//# sourceMappingURL=server-config.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server-config.js","sourceRoot":"","sources":["../../src/server/server-config.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"server-config.js","sourceRoot":"","sources":["../../src/server/server-config.ts"],"names":[],"mappings":"AA0BA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B,IAAI,EAAE,IAAI;IACV,IAAI,EAAE,SAAS;CACP,CAAC;AAEX;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CAAC,MAAoB,EAAE,EAAE,KAAK,EAAsB;IACrF,OAAO;QACL,KAAK;QACL,UAAU,EAAE,KAAK,IAAI,CAAC,MAAM,CAAC,UAAU,IAAI,KAAK,CAAC;KAClD,CAAC;AACJ,CAAC","sourcesContent":["import type { RshonoConfig } from '../config.js';\n\n/**\n * The framework settings the server bundle needs at request time, fully resolved.\n *\n * The value is produced once by {@link resolveServerConfig} from `rshono.config.ts`\n * and compiled into the server bundle as the `__RSHONO_CONFIG__` literal (see\n * `builder/rspack-config.ts`) — there is no runtime env-var interface for these.\n *\n * Only two fields, because only two things here are decided by the *build*. The per-request\n * security controls that used to live alongside them — the CSRF check, the CSP, the body cap — are\n * Hono middleware an app registers in `src/server.ts`, where they can be configured per route and\n * per environment instead of once per bundle.\n */\nexport interface ServerConfig {\n /**\n * `true` when the bundle came from `rshono dev`.\n *\n * Baked in rather than read from `process.env.NODE_ENV` at runtime: it is decided by which command\n * produced the bundle, and a deploy target need not have a `process` to read it from.\n */\n isDev: boolean;\n /** Honour `X-Forwarded-Host` / `-Proto` when resolving the browser-facing URL. Forced on in dev. */\n trustProxy: boolean;\n}\n\n/**\n * The framework's built-in defaults.\n *\n * `port` and `host` are not config fields at all — they come from `PORT` / `HOST` (or `--port`)\n * wherever the address is resolved, `deploy/node/runtime.ts` for a server bundle and `cli/dev.ts`\n * for the dev server, and this is what both fall back to.\n */\nexport const SERVER_DEFAULTS = {\n port: 3000,\n host: '0.0.0.0',\n} as const;\n\n/**\n * Resolve the user's {@link RshonoConfig} into the {@link ServerConfig} baked into the bundle.\n *\n * `isDev` is a build-time input rather than a config field because it decides one thing the user\n * shouldn't have to: `trustProxy` is forced on under `rshono dev`, where the framework's own proxy\n * is the only way in (it sets the forwarded headers itself and binds to localhost).\n */\nexport function resolveServerConfig(config: RshonoConfig, { isDev }: { isDev: boolean }): ServerConfig {\n return {\n isDev,\n trustProxy: isDev || (config.trustProxy ?? false),\n };\n}\n"]}
|
package/dist/server/ssg.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ssg.d.ts","sourceRoot":"","sources":["../../src/server/ssg.ts"],"names":[],"mappings":"AAGA,OAAO,EAA+B,KAAK,KAAK,EAAE,MAAM,cAAc,CAAC;AACvE,OAAO,EAML,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACtB,MAAM,kBAAkB,CAAC;AAS1B;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAUrE;AA4BD,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAyB,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CA8B9I;AAED,UAAU,gBAAgB;IACxB,MAAM,EAAE,SAAS,KAAK,EAAE,CAAC;IACzB,KAAK,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC1D,MAAM,EAAE,MAAM,CAAC;IACf,+FAA+F;IAC/F,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;
|
|
1
|
+
{"version":3,"file":"ssg.d.ts","sourceRoot":"","sources":["../../src/server/ssg.ts"],"names":[],"mappings":"AAGA,OAAO,EAA+B,KAAK,KAAK,EAAE,MAAM,cAAc,CAAC;AACvE,OAAO,EAML,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACtB,MAAM,kBAAkB,CAAC;AAS1B;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAUrE;AA4BD,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAyB,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CA8B9I;AAED,UAAU,gBAAgB;IACxB,MAAM,EAAE,SAAS,KAAK,EAAE,CAAC;IACzB,KAAK,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC1D,MAAM,EAAE,MAAM,CAAC;IACf,+FAA+F;IAC/F,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAoBD,wBAAsB,qBAAqB,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,CA0D/F"}
|
package/dist/server/ssg.js
CHANGED
|
@@ -84,14 +84,14 @@ export async function readPrerendered(ssgDir, requestPath, variant = 'html') {
|
|
|
84
84
|
pageCache.set(key, page);
|
|
85
85
|
return page;
|
|
86
86
|
}
|
|
87
|
-
/** Renders one representation of a path, or `null` if the app didn't answer with it. */
|
|
88
87
|
async function renderVariant(fetch, url, variant) {
|
|
89
88
|
const response = await fetch(new Request(url, { headers: { Accept: VARIANTS[variant].accept } }));
|
|
90
89
|
if (response.status !== 200)
|
|
91
|
-
return {
|
|
92
|
-
if (!(response.headers.get('Content-Type') ?? '').includes(VARIANTS[variant].contentType))
|
|
93
|
-
return
|
|
94
|
-
|
|
90
|
+
return { ok: false, reason: `${response.status}` };
|
|
91
|
+
if (!(response.headers.get('Content-Type') ?? '').includes(VARIANTS[variant].contentType)) {
|
|
92
|
+
return { ok: false, reason: `a non-${VARIANTS[variant].contentType} response` };
|
|
93
|
+
}
|
|
94
|
+
return { ok: true, body: await response.text() };
|
|
95
95
|
}
|
|
96
96
|
export async function prerenderStaticRoutes(options) {
|
|
97
97
|
const { routes, fetch, ssgDir } = options;
|
|
@@ -118,9 +118,8 @@ export async function prerenderStaticRoutes(options) {
|
|
|
118
118
|
}
|
|
119
119
|
for (const path of paths) {
|
|
120
120
|
const document = await renderVariant(fetch, origin + path, 'html');
|
|
121
|
-
if (
|
|
122
|
-
|
|
123
|
-
console.warn(` ⚠ "${path}" rendered ${rendered} at build time — skipping, will SSR per request.`);
|
|
121
|
+
if (!document.ok) {
|
|
122
|
+
console.warn(` ⚠ "${path}" rendered ${document.reason} at build time — skipping, will SSR per request.`);
|
|
124
123
|
skipped.push(path);
|
|
125
124
|
continue;
|
|
126
125
|
}
|
|
@@ -131,10 +130,10 @@ export async function prerenderStaticRoutes(options) {
|
|
|
131
130
|
};
|
|
132
131
|
write('html', document.body);
|
|
133
132
|
// The soft-navigation representation of the same page. Best-effort: if it doesn't come back
|
|
134
|
-
// cleanly the document is still valid on its own, and serving falls back to rendering
|
|
135
|
-
// per request
|
|
133
|
+
// cleanly the document is still valid on its own, and serving falls back to rendering the
|
|
134
|
+
// flight payload per request.
|
|
136
135
|
const flight = await renderVariant(fetch, origin + path, 'flight');
|
|
137
|
-
if (flight
|
|
136
|
+
if (flight.ok) {
|
|
138
137
|
write('flight', flight.body);
|
|
139
138
|
}
|
|
140
139
|
else {
|
package/dist/server/ssg.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ssg.js","sourceRoot":"","sources":["../../src/server/ssg.ts"],"names":[],"mappings":"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","sourcesContent":["import { mkdirSync, writeFileSync } from 'node:fs';\nimport { readFile } from 'node:fs/promises';\nimport { dirname, join, resolve, sep } from 'node:path';\nimport { isPageRoute, type PageRoute, type Route } from '../router.js';\nimport {\n createPageCache,\n prerenderedRelPath,\n ssgFilePath,\n toPrerenderedPage,\n VARIANTS,\n type PrerenderedPage,\n type PrerenderVariant,\n} from './prerendered.js';\n\n/**\n * Stand-in origin for a build that didn't declare {@link RshonoConfig.siteUrl}. Deliberately\n * obviously-wrong rather than a guess: a page that bakes this into a canonical tag should be easy\n * to spot, and the build warns when static routes are prerendered without a real origin.\n */\nconst DEFAULT_SSG_ORIGIN = 'http://localhost';\n\n/**\n * Resolve {@link RshonoConfig.siteUrl} to the origin prerendering should render against.\n *\n * A path is rejected rather than dropped: `'https://example.com/docs'` almost certainly means the\n * author expects a base path, and silently serving from the root would be a confusing way to find\n * out that isn't supported.\n */\nexport function resolveSiteOrigin(siteUrl: string | undefined): string {\n if (!siteUrl) return DEFAULT_SSG_ORIGIN;\n const parsed = URL.parse(siteUrl);\n if (!parsed || (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')) {\n throw new Error(`[rshono] invalid siteUrl ${JSON.stringify(siteUrl)} — use a full origin, e.g. 'https://example.com'.`);\n }\n if (parsed.pathname !== '/' || parsed.search || parsed.hash) {\n throw new Error(`[rshono] siteUrl ${JSON.stringify(siteUrl)} must be a bare origin — a base path is not supported.`);\n }\n return parsed.origin;\n}\n\nfunction interpolatePath(pattern: string, params: Record<string, string>): string {\n return pattern\n .split('/')\n .map((segment) => {\n if (!segment.startsWith(':')) {\n if (segment.includes('*')) {\n throw new Error(`Cannot prerender \"${pattern}\": wildcard segments are not supported by staticPaths.`);\n }\n return segment;\n }\n const name = segment.slice(1);\n if (!/^\\w+$/.test(name)) {\n throw new Error(`Cannot prerender \"${pattern}\": optional/regex params are not supported by staticPaths.`);\n }\n const value = params[name];\n if (value === undefined) {\n throw new Error(`staticPaths for \"${pattern}\" returned a param set without \"${name}\".`);\n }\n return encodeURIComponent(value);\n })\n .join('/');\n}\n\n/** Prerendered pages, keyed by the request that produced them (see {@link readPrerendered}). */\nconst pageCache = createPageCache();\n\nexport async function readPrerendered(ssgDir: string, requestPath: string, variant: PrerenderVariant = 'html'): Promise<PrerenderedPage | null> {\n // Keyed by what the request carried, not by the resolved filename, so a hit costs one Map lookup\n // instead of re-deriving the path every time. Safe because only *hits* are cached: an entry\n // exists only if this exact key already passed the checks below.\n const key = `${ssgDir}\\0${variant}\\0${requestPath}`;\n const cached = pageCache.get(key);\n if (cached) return cached;\n\n const relPath = prerenderedRelPath(requestPath, variant);\n if (relPath === null) return null;\n // Belt and braces: the shared guard already refused a traversal, and this proves the resolved file\n // is under the root whatever else the path contained.\n const root = resolve(ssgDir);\n const file = resolve(root, relPath);\n if (!file.startsWith(root + sep)) return null;\n\n // No encoding argument: the bytes are what gets served, and decoding them to a string here would\n // only mean re-encoding them on every request that hits the cache. Copied out of the Buffer rather\n // than kept as one, because `readFile` can hand back a view into Node's shared allocation pool and\n // this is retained for the life of the process.\n let body: Uint8Array<ArrayBuffer>;\n try {\n body = new Uint8Array(await readFile(file));\n } catch {\n return null;\n }\n\n const page = await toPrerenderedPage(body);\n pageCache.set(key, page);\n return page;\n}\n\ninterface PrerenderOptions {\n routes: readonly Route[];\n fetch: (request: Request) => Response | Promise<Response>;\n ssgDir: string;\n /** {@link RshonoConfig.siteUrl} — the origin absolute URLs in the output are built against. */\n siteUrl?: string;\n}\n\nexport interface PrerenderResult {\n written: string[];\n skipped: string[];\n}\n\n/** Renders one representation of a path, or `null` if the app didn't answer with it. */\nasync function renderVariant(\n fetch: PrerenderOptions['fetch'],\n url: string,\n variant: PrerenderVariant,\n): Promise<{ body: string } | { status: number } | null> {\n const response = await fetch(new Request(url, { headers: { Accept: VARIANTS[variant].accept } }));\n if (response.status !== 200) return { status: response.status };\n if (!(response.headers.get('Content-Type') ?? '').includes(VARIANTS[variant].contentType)) return null;\n return { body: await response.text() };\n}\n\nexport async function prerenderStaticRoutes(options: PrerenderOptions): Promise<PrerenderResult> {\n const { routes, fetch, ssgDir } = options;\n const staticRoutes = routes.filter((r): r is PageRoute => isPageRoute(r) && r.render === 'static');\n const origin = resolveSiteOrigin(options.siteUrl);\n\n if (staticRoutes.length > 0 && !options.siteUrl) {\n console.warn(\n ` ⚠ No siteUrl in rshono.config — prerendered pages are built against ${DEFAULT_SSG_ORIGIN}, so any absolute URL\\n` +\n ` they derive from a page's \\`url\\` prop (canonical tags, og:url, absolute links) will point there.`,\n );\n }\n\n const written: string[] = [];\n const skipped: string[] = [];\n\n for (const route of staticRoutes) {\n let paths: string[];\n if (!/[:*]/.test(route.path)) {\n paths = [route.path];\n } else {\n if (!route.staticPaths) {\n console.warn(` ⚠ Static route \"${route.path}\" has params but no staticPaths — will SSR per request.`);\n skipped.push(route.path);\n continue;\n }\n paths = (await route.staticPaths()).map((params) => interpolatePath(route.path, params));\n }\n\n for (const path of paths) {\n const document = await renderVariant(fetch, origin + path, 'html');\n if (document === null || !('body' in document)) {\n const rendered = document === null ? 'a non-HTML response' : `${document.status}`;\n console.warn(` ⚠ \"${path}\" rendered ${rendered} at build time — skipping, will SSR per request.`);\n skipped.push(path);\n continue;\n }\n\n const write = (variant: PrerenderVariant, body: string) => {\n const file = join(ssgDir, ssgFilePath(path, variant)!);\n mkdirSync(dirname(file), { recursive: true });\n writeFileSync(file, body);\n };\n write('html', document.body);\n\n // The soft-navigation representation of the same page. Best-effort: if it doesn't come back\n // cleanly the document is still valid on its own, and serving falls back to rendering flight\n // per request — the behaviour before this was written at all.\n const flight = await renderVariant(fetch, origin + path, 'flight');\n if (flight !== null && 'body' in flight) {\n write('flight', flight.body);\n } else {\n console.warn(` ⚠ \"${path}\" produced no flight payload — soft navigations to it will render per request.`);\n }\n\n written.push(path);\n }\n }\n\n return { written, skipped };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"ssg.js","sourceRoot":"","sources":["../../src/server/ssg.ts"],"names":[],"mappings":"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","sourcesContent":["import { mkdirSync, writeFileSync } from 'node:fs';\nimport { readFile } from 'node:fs/promises';\nimport { dirname, join, resolve, sep } from 'node:path';\nimport { isPageRoute, type PageRoute, type Route } from '../router.js';\nimport {\n createPageCache,\n prerenderedRelPath,\n ssgFilePath,\n toPrerenderedPage,\n VARIANTS,\n type PrerenderedPage,\n type PrerenderVariant,\n} from './prerendered.js';\n\n/**\n * Stand-in origin for a build that didn't declare {@link RshonoConfig.siteUrl}. Deliberately\n * obviously-wrong rather than a guess: a page that bakes this into a canonical tag should be easy\n * to spot, and the build warns when static routes are prerendered without a real origin.\n */\nconst DEFAULT_SSG_ORIGIN = 'http://localhost';\n\n/**\n * Resolve {@link RshonoConfig.siteUrl} to the origin prerendering should render against.\n *\n * A path is rejected rather than dropped: `'https://example.com/docs'` almost certainly means the\n * author expects a base path, and silently serving from the root would be a confusing way to find\n * out that isn't supported.\n */\nexport function resolveSiteOrigin(siteUrl: string | undefined): string {\n if (!siteUrl) return DEFAULT_SSG_ORIGIN;\n const parsed = URL.parse(siteUrl);\n if (!parsed || (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')) {\n throw new Error(`[rshono] invalid siteUrl ${JSON.stringify(siteUrl)} — use a full origin, e.g. 'https://example.com'.`);\n }\n if (parsed.pathname !== '/' || parsed.search || parsed.hash) {\n throw new Error(`[rshono] siteUrl ${JSON.stringify(siteUrl)} must be a bare origin — a base path is not supported.`);\n }\n return parsed.origin;\n}\n\nfunction interpolatePath(pattern: string, params: Record<string, string>): string {\n return pattern\n .split('/')\n .map((segment) => {\n if (!segment.startsWith(':')) {\n if (segment.includes('*')) {\n throw new Error(`Cannot prerender \"${pattern}\": wildcard segments are not supported by staticPaths.`);\n }\n return segment;\n }\n const name = segment.slice(1);\n if (!/^\\w+$/.test(name)) {\n throw new Error(`Cannot prerender \"${pattern}\": optional/regex params are not supported by staticPaths.`);\n }\n const value = params[name];\n if (value === undefined) {\n throw new Error(`staticPaths for \"${pattern}\" returned a param set without \"${name}\".`);\n }\n return encodeURIComponent(value);\n })\n .join('/');\n}\n\n/** Prerendered pages, keyed by the request that produced them (see {@link readPrerendered}). */\nconst pageCache = createPageCache();\n\nexport async function readPrerendered(ssgDir: string, requestPath: string, variant: PrerenderVariant = 'html'): Promise<PrerenderedPage | null> {\n // Keyed by what the request carried, not by the resolved filename, so a hit costs one Map lookup\n // instead of re-deriving the path every time. Safe because only *hits* are cached: an entry\n // exists only if this exact key already passed the checks below.\n const key = `${ssgDir}\\0${variant}\\0${requestPath}`;\n const cached = pageCache.get(key);\n if (cached) return cached;\n\n const relPath = prerenderedRelPath(requestPath, variant);\n if (relPath === null) return null;\n // Belt and braces: the shared guard already refused a traversal, and this proves the resolved file\n // is under the root whatever else the path contained.\n const root = resolve(ssgDir);\n const file = resolve(root, relPath);\n if (!file.startsWith(root + sep)) return null;\n\n // No encoding argument: the bytes are what gets served, and decoding them to a string here would\n // only mean re-encoding them on every request that hits the cache. Copied out of the Buffer rather\n // than kept as one, because `readFile` can hand back a view into Node's shared allocation pool and\n // this is retained for the life of the process.\n let body: Uint8Array<ArrayBuffer>;\n try {\n body = new Uint8Array(await readFile(file));\n } catch {\n return null;\n }\n\n const page = await toPrerenderedPage(body);\n pageCache.set(key, page);\n return page;\n}\n\ninterface PrerenderOptions {\n routes: readonly Route[];\n fetch: (request: Request) => Response | Promise<Response>;\n ssgDir: string;\n /** {@link RshonoConfig.siteUrl} — the origin absolute URLs in the output are built against. */\n siteUrl?: string;\n}\n\nexport interface PrerenderResult {\n written: string[];\n skipped: string[];\n}\n\n/**\n * One representation of a path, as the app answered for it at build time.\n *\n * Discriminated on `ok` rather than returned as a body-or-status-or-null union: both callers have to\n * tell \"the app rendered this\" from \"it did not\", and only one of them cares *why* — which as three\n * shapes made every read a `'body' in result` check against a value that could also be null.\n */\ntype RenderedVariant = { ok: true; body: string } | { ok: false; reason: string };\n\nasync function renderVariant(fetch: PrerenderOptions['fetch'], url: string, variant: PrerenderVariant): Promise<RenderedVariant> {\n const response = await fetch(new Request(url, { headers: { Accept: VARIANTS[variant].accept } }));\n if (response.status !== 200) return { ok: false, reason: `${response.status}` };\n if (!(response.headers.get('Content-Type') ?? '').includes(VARIANTS[variant].contentType)) {\n return { ok: false, reason: `a non-${VARIANTS[variant].contentType} response` };\n }\n return { ok: true, body: await response.text() };\n}\n\nexport async function prerenderStaticRoutes(options: PrerenderOptions): Promise<PrerenderResult> {\n const { routes, fetch, ssgDir } = options;\n const staticRoutes = routes.filter((r): r is PageRoute => isPageRoute(r) && r.render === 'static');\n const origin = resolveSiteOrigin(options.siteUrl);\n\n if (staticRoutes.length > 0 && !options.siteUrl) {\n console.warn(\n ` ⚠ No siteUrl in rshono.config — prerendered pages are built against ${DEFAULT_SSG_ORIGIN}, so any absolute URL\\n` +\n ` they derive from a page's \\`url\\` prop (canonical tags, og:url, absolute links) will point there.`,\n );\n }\n\n const written: string[] = [];\n const skipped: string[] = [];\n\n for (const route of staticRoutes) {\n let paths: string[];\n if (!/[:*]/.test(route.path)) {\n paths = [route.path];\n } else {\n if (!route.staticPaths) {\n console.warn(` ⚠ Static route \"${route.path}\" has params but no staticPaths — will SSR per request.`);\n skipped.push(route.path);\n continue;\n }\n paths = (await route.staticPaths()).map((params) => interpolatePath(route.path, params));\n }\n\n for (const path of paths) {\n const document = await renderVariant(fetch, origin + path, 'html');\n if (!document.ok) {\n console.warn(` ⚠ \"${path}\" rendered ${document.reason} at build time — skipping, will SSR per request.`);\n skipped.push(path);\n continue;\n }\n\n const write = (variant: PrerenderVariant, body: string) => {\n const file = join(ssgDir, ssgFilePath(path, variant)!);\n mkdirSync(dirname(file), { recursive: true });\n writeFileSync(file, body);\n };\n write('html', document.body);\n\n // The soft-navigation representation of the same page. Best-effort: if it doesn't come back\n // cleanly the document is still valid on its own, and serving falls back to rendering the\n // flight payload per request.\n const flight = await renderVariant(fetch, origin + path, 'flight');\n if (flight.ok) {\n write('flight', flight.body);\n } else {\n console.warn(` ⚠ \"${path}\" produced no flight payload — soft navigations to it will render per request.`);\n }\n\n written.push(path);\n }\n }\n\n return { written, skipped };\n}\n"]}
|
package/package.json
CHANGED