@rshono/core 1.0.0-rc.16 → 1.0.0-rc.18
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 +29 -5
- package/dist/builder/env-shadow-loader.cjs +58 -2
- package/dist/builder/rspack-config.d.ts.map +1 -1
- package/dist/builder/rspack-config.js +46 -6
- package/dist/builder/rspack-config.js.map +1 -1
- package/dist/cli/build.d.ts.map +1 -1
- package/dist/cli/build.js +24 -1
- package/dist/cli/build.js.map +1 -1
- package/dist/cli/dev.d.ts.map +1 -1
- package/dist/cli/dev.js +4 -0
- package/dist/cli/dev.js.map +1 -1
- package/dist/cli/index.js +14 -8
- package/dist/cli/index.js.map +1 -1
- package/dist/config.d.ts +10 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/deploy/cloudflare/runtime.d.ts.map +1 -1
- package/dist/deploy/cloudflare/runtime.js +31 -5
- package/dist/deploy/cloudflare/runtime.js.map +1 -1
- package/dist/deploy/contract.d.ts +6 -2
- 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 +2 -1
- package/dist/deploy/filesystem.js.map +1 -1
- package/dist/deploy/node/runtime.d.ts.map +1 -1
- package/dist/deploy/node/runtime.js +3 -4
- package/dist/deploy/node/runtime.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/router.d.ts +60 -7
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js.map +1 -1
- package/dist/runtime/context.d.ts +51 -7
- package/dist/runtime/context.d.ts.map +1 -1
- package/dist/runtime/context.js +49 -5
- package/dist/runtime/context.js.map +1 -1
- package/dist/runtime/entry.client.js +230 -277
- 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 +187 -39
- package/dist/runtime/entry.rsc.js.map +1 -1
- package/dist/runtime/flight-inject.d.ts +1 -1
- package/dist/runtime/flight-inject.d.ts.map +1 -1
- package/dist/runtime/flight-inject.js +207 -40
- package/dist/runtime/flight-inject.js.map +1 -1
- package/dist/runtime/navigation.d.ts +4 -0
- package/dist/runtime/navigation.d.ts.map +1 -1
- package/dist/runtime/navigation.js.map +1 -1
- package/dist/runtime/request.d.ts.map +1 -1
- package/dist/runtime/request.js +10 -0
- package/dist/runtime/request.js.map +1 -1
- package/dist/runtime/validate-entries.d.ts +33 -0
- package/dist/runtime/validate-entries.d.ts.map +1 -0
- package/dist/runtime/validate-entries.js +185 -0
- package/dist/runtime/validate-entries.js.map +1 -0
- package/dist/server/prerendered.d.ts +30 -6
- package/dist/server/prerendered.d.ts.map +1 -1
- package/dist/server/prerendered.js +71 -13
- package/dist/server/prerendered.js.map +1 -1
- package/dist/server/server-config.d.ts +12 -0
- package/dist/server/server-config.d.ts.map +1 -1
- package/dist/server/server-config.js +25 -0
- package/dist/server/server-config.js.map +1 -1
- package/dist/server/ssg.d.ts.map +1 -1
- package/dist/server/ssg.js +151 -25
- package/dist/server/ssg.js.map +1 -1
- package/dist/server/static.d.ts.map +1 -1
- package/dist/server/static.js +3 -1
- package/dist/server/static.js.map +1 -1
- package/package.json +7 -6
|
@@ -13,27 +13,85 @@ export const VARIANTS = {
|
|
|
13
13
|
flight: { file: 'index.rsc', headers: { RSC: '1' }, contentType: 'text/x-component' },
|
|
14
14
|
};
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* The index the build leaves beside the pages, naming every file it wrote — one `files` array of
|
|
17
|
+
* {@link ssgFilePath} names.
|
|
18
|
+
*
|
|
19
|
+
* It exists so a reader can tell "this store has no page for that path" without asking the store, which is
|
|
20
|
+
* the difference between a `Map` lookup and a failed `readFile` (or, without a filesystem, a failed store
|
|
21
|
+
* fetch) on **every** request to a static route the build could not prerender — misses are deliberately not
|
|
22
|
+
* cached, so that one never warms up. Absent is not an error: a build from an older rshono has none, and a
|
|
23
|
+
* reader that finds none looks the way it always did.
|
|
24
|
+
*/
|
|
25
|
+
export const SSG_MANIFEST_FILE = 'manifest.json';
|
|
26
|
+
/**
|
|
27
|
+
* What a decoded path segment may not hold if it is to be one portable file name. `\ / : * ? " < > |` are
|
|
28
|
+
* refused by Windows and `/` by every host, so a `staticPaths` value containing one fails the build rather
|
|
29
|
+
* than writing a page on one machine and not on another — and a route pattern, whose params and wildcards are
|
|
30
|
+
* spelled with `:` and `*`, falls out of the same rule.
|
|
31
|
+
*/
|
|
32
|
+
const UNPORTABLE_SEGMENT = /[\\/:*?"<>|]/;
|
|
33
|
+
/**
|
|
34
|
+
* Decodes one path segment the way a URL does, treating a malformed escape as the literal text it is:
|
|
35
|
+
* `decodeURIComponent('%')` throws, and a request path is whatever the client chose to send.
|
|
36
|
+
*/
|
|
37
|
+
function decodeSegment(segment) {
|
|
38
|
+
try {
|
|
39
|
+
return decodeURIComponent(segment);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return segment;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Whether one decoded segment can be a directory in the prerender tree: not empty, not a relative-path
|
|
47
|
+
* marker, and made only of characters a file name can hold — {@link UNPORTABLE_SEGMENT}, plus the control
|
|
48
|
+
* characters, which a percent-escape is free to carry into a path and no filesystem wants in a name.
|
|
49
|
+
*/
|
|
50
|
+
function isStorableSegment(segment) {
|
|
51
|
+
if (segment === '' || segment === '.' || segment === '..')
|
|
52
|
+
return false;
|
|
53
|
+
return !UNPORTABLE_SEGMENT.test(segment) && ![...segment].some((char) => char < ' ');
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Where a path's prerendered output lives, relative to the output root — or `null` for a path no single file
|
|
57
|
+
* can answer.
|
|
58
|
+
*
|
|
59
|
+
* **The one canonical form**, shared by the build that writes the file and by every deploy target that reads
|
|
60
|
+
* it back, because the two drifting apart is invisible: the build reports the page as prerendered and every
|
|
61
|
+
* request afterwards falls through to SSR, forever. Each segment is stored *decoded*, so `/docs/café` is
|
|
62
|
+
* `docs/café` whether the caller holds the path percent-encoded — the build, which interpolates `staticPaths`
|
|
63
|
+
* values into a URL — or already decoded, as Hono's `c.req.path` is: it runs `decodeURI` over any path
|
|
64
|
+
* containing a `%`. Decoded is also the form every other static file server addresses, so the tree stays
|
|
65
|
+
* servable by something that is not this framework.
|
|
18
66
|
*
|
|
19
67
|
* Always `/`-separated: the same string addresses a file on a filesystem, which accepts forward slashes on
|
|
20
68
|
* Windows too, and a key in an asset store, where a backslash would be the wrong character.
|
|
69
|
+
*
|
|
70
|
+
* `null` is everything that is not one file: a `.` or `..` segment — so traversal is a miss rather than a
|
|
71
|
+
* lookup, which a store addressed by key, with no `resolve()` to fall back on, depends on — an empty segment,
|
|
72
|
+
* and any segment holding a character a file name cannot.
|
|
21
73
|
*/
|
|
22
|
-
export function ssgFilePath(
|
|
23
|
-
if (/[:*]/.test(routePath))
|
|
24
|
-
return null;
|
|
25
|
-
const trimmed = routePath.replace(/^\/+|\/+$/g, '');
|
|
74
|
+
export function ssgFilePath(path, variant = 'html') {
|
|
26
75
|
const file = VARIANTS[variant].file;
|
|
27
|
-
|
|
76
|
+
const trimmed = path.replace(/^\/+|\/+$/g, '');
|
|
77
|
+
if (trimmed === '')
|
|
78
|
+
return file;
|
|
79
|
+
const segments = [];
|
|
80
|
+
for (const encoded of trimmed.split('/')) {
|
|
81
|
+
const segment = decodeSegment(encoded);
|
|
82
|
+
if (!isStorableSegment(segment))
|
|
83
|
+
return null;
|
|
84
|
+
segments.push(segment);
|
|
85
|
+
}
|
|
86
|
+
return `${segments.join('/')}/${file}`;
|
|
28
87
|
}
|
|
29
88
|
/**
|
|
30
|
-
* {@link ssgFilePath}
|
|
31
|
-
*
|
|
89
|
+
* {@link ssgFilePath} as a URL path, for a store addressed by one rather than by key. Every segment is escaped
|
|
90
|
+
* on the way out and the store decodes it back, so a page reached through a URL lands on the same file a
|
|
91
|
+
* filesystem target opens by name.
|
|
32
92
|
*/
|
|
33
|
-
export function
|
|
34
|
-
|
|
35
|
-
return null;
|
|
36
|
-
return ssgFilePath(requestPath, variant);
|
|
93
|
+
export function ssgAssetPath(relPath) {
|
|
94
|
+
return relPath.split('/').map(encodeURIComponent).join('/');
|
|
37
95
|
}
|
|
38
96
|
/** The default cache budget: enough for a large documentation site's working set, small enough to be ignorable. */
|
|
39
97
|
const DEFAULT_CACHE_BYTES = 32 * 1024 * 1024;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prerendered.js","sourceRoot":"","sources":["../../src/server/prerendered.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Everything about a prerendered page that doesn't need a filesystem: where the build puts it, and what it\n * looks like once read back.\n *\n * Apart from `ssg.ts`, which does the reading and writing through `node:fs`, because a deploy target without a\n * filesystem reads the same layout out of an asset store — and importing it from there would drag `node:fs`\n * 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 soft\n * navigation asks the same URL for a flight payload. Without both, every in-app click would re-render a page\n * that was already built.\n */\nexport type PrerenderVariant = 'html' | 'flight';\n\nexport const VARIANTS = {\n // `headers` is what the build asks each representation for, so it stays the same request the browser makes:\n // a document is the default and a flight payload is the `RSC` header. See `runtime/request.ts`.\n html: { file: 'index.html', headers: {}, contentType: 'text/html' },\n flight: { file: 'index.rsc', headers: { RSC: '1' }, contentType: 'text/x-component' },\n} as const satisfies Record<PrerenderVariant, { file: string; headers: Record<string, string>; contentType: string }>;\n\n/**\n * Where a route's prerendered output lives, relative to the output root — or `null` for a path that cannot be\n * prerendered at all, one with a param or a wildcard left in it.\n *\n * Always `/`-separated: the same string addresses a file on a filesystem, which accepts forward slashes on\n * Windows too, and a key in an asset store, where a backslash would 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. A store\n * addressed by key has no `resolve()` to fall back on, so `..` is 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/** The default cache budget: enough for a large documentation site's working set, small enough to be ignorable. */\nconst DEFAULT_CACHE_BYTES = 32 * 1024 * 1024;\n\n/**\n * A bounded, insertion-ordered cache of prerendered pages, so a site with thousands of them keeps a working set\n * rather than the whole build in memory. Only *hits* are stored — caching misses would let anyone mint entries\n * by requesting paths that don't exist — and the files never change while the server is up.\n *\n * Bounded by **bytes**, not by entry count: a count says nothing about how much memory it stands for, and a\n * page can be any size. At 128 entries — the previous limit — a documentation site with half-megabyte pages\n * retained ~64 MB across the two variants, with nothing about the number to suggest it.\n *\n * A page larger than the whole budget is served and not stored, rather than evicting everything to hold one.\n */\nexport function createPageCache(maxBytes = DEFAULT_CACHE_BYTES): {\n get(key: string): PrerenderedPage | undefined;\n set(key: string, page: PrerenderedPage): void;\n} {\n const pages = new Map<string, PrerenderedPage>();\n let bytes = 0;\n return {\n get: (key) => pages.get(key),\n set(key, page) {\n const size = page.body.byteLength;\n if (size > maxBytes) return;\n // Deleted before it is set, so re-setting an existing key moves it to the *back*: `Map.set` on a key it\n // already holds keeps that key's original position, which would leave the entry just stored sitting at\n // the front as the next thing evicted — a store whose eviction could drop the write that caused it.\n const replaced = pages.get(key);\n if (replaced) {\n bytes -= replaced.body.byteLength;\n pages.delete(key);\n }\n pages.set(key, page);\n bytes += size;\n // Insertion-ordered, so the first key is the oldest.\n while (bytes > maxBytes) {\n const oldest = pages.keys().next().value!;\n bytes -= pages.get(oldest)!.body.byteLength;\n pages.delete(oldest);\n }\n },\n };\n}\n\n/**\n * A weak `ETag` for a page body — see {@link PrerenderedPage.etag} for why weak. Web Crypto rather than\n * `node:crypto`, so one implementation serves both a Node server and `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. `storeEtag` is the validator the\n * store supplied, where it has one: it already describes these exact bytes, so it is weakened rather than\n * replaced by a fresh hash.\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. Bytes rather\n * than a string, because this cache entry is served verbatim to every request that hits it — as a string,\n * each would pay a fresh UTF-8 encode of the whole page.\n */\n body: Uint8Array<ArrayBuffer>;\n /**\n * `Content-Length` for {@link body}, in bytes. Served explicitly because Hono sets no length for an\n * in-memory body, and a proxy in front is entitled to make decisions with it.\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 them, so a\n * strong validator would differ per coding and a cache would treat the 200 and the 304 that revalidates it\n * as different pages. A weak tag says \"the same representation\", which stays true across codings.\n */\n etag: string;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"prerendered.js","sourceRoot":"","sources":["../../src/server/prerendered.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Everything about a prerendered page that doesn't need a filesystem: where the build puts it, and what it\n * looks like once read back.\n *\n * Apart from `ssg.ts`, which does the reading and writing through `node:fs`, because a deploy target without a\n * filesystem reads the same layout out of an asset store — and importing it from there would drag `node:fs`\n * 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 soft\n * navigation asks the same URL for a flight payload. Without both, every in-app click would re-render a page\n * that was already built.\n */\nexport type PrerenderVariant = 'html' | 'flight';\n\nexport const VARIANTS = {\n // `headers` is what the build asks each representation for, so it stays the same request the browser makes:\n // a document is the default and a flight payload is the `RSC` header. See `runtime/request.ts`.\n html: { file: 'index.html', headers: {}, contentType: 'text/html' },\n flight: { file: 'index.rsc', headers: { RSC: '1' }, contentType: 'text/x-component' },\n} as const satisfies Record<PrerenderVariant, { file: string; headers: Record<string, string>; contentType: string }>;\n\n/**\n * The index the build leaves beside the pages, naming every file it wrote — one `files` array of\n * {@link ssgFilePath} names.\n *\n * It exists so a reader can tell \"this store has no page for that path\" without asking the store, which is\n * the difference between a `Map` lookup and a failed `readFile` (or, without a filesystem, a failed store\n * fetch) on **every** request to a static route the build could not prerender — misses are deliberately not\n * cached, so that one never warms up. Absent is not an error: a build from an older rshono has none, and a\n * reader that finds none looks the way it always did.\n */\nexport const SSG_MANIFEST_FILE = 'manifest.json';\n\n/**\n * What a decoded path segment may not hold if it is to be one portable file name. `\\ / : * ? \" < > |` are\n * refused by Windows and `/` by every host, so a `staticPaths` value containing one fails the build rather\n * than writing a page on one machine and not on another — and a route pattern, whose params and wildcards are\n * spelled with `:` and `*`, falls out of the same rule.\n */\nconst UNPORTABLE_SEGMENT = /[\\\\/:*?\"<>|]/;\n\n/**\n * Decodes one path segment the way a URL does, treating a malformed escape as the literal text it is:\n * `decodeURIComponent('%')` throws, and a request path is whatever the client chose to send.\n */\nfunction decodeSegment(segment: string): string {\n try {\n return decodeURIComponent(segment);\n } catch {\n return segment;\n }\n}\n\n/**\n * Whether one decoded segment can be a directory in the prerender tree: not empty, not a relative-path\n * marker, and made only of characters a file name can hold — {@link UNPORTABLE_SEGMENT}, plus the control\n * characters, which a percent-escape is free to carry into a path and no filesystem wants in a name.\n */\nfunction isStorableSegment(segment: string): boolean {\n if (segment === '' || segment === '.' || segment === '..') return false;\n return !UNPORTABLE_SEGMENT.test(segment) && ![...segment].some((char) => char < ' ');\n}\n\n/**\n * Where a path's prerendered output lives, relative to the output root — or `null` for a path no single file\n * can answer.\n *\n * **The one canonical form**, shared by the build that writes the file and by every deploy target that reads\n * it back, because the two drifting apart is invisible: the build reports the page as prerendered and every\n * request afterwards falls through to SSR, forever. Each segment is stored *decoded*, so `/docs/café` is\n * `docs/café` whether the caller holds the path percent-encoded — the build, which interpolates `staticPaths`\n * values into a URL — or already decoded, as Hono's `c.req.path` is: it runs `decodeURI` over any path\n * containing a `%`. Decoded is also the form every other static file server addresses, so the tree stays\n * servable by something that is not this framework.\n *\n * Always `/`-separated: the same string addresses a file on a filesystem, which accepts forward slashes on\n * Windows too, and a key in an asset store, where a backslash would be the wrong character.\n *\n * `null` is everything that is not one file: a `.` or `..` segment — so traversal is a miss rather than a\n * lookup, which a store addressed by key, with no `resolve()` to fall back on, depends on — an empty segment,\n * and any segment holding a character a file name cannot.\n */\nexport function ssgFilePath(path: string, variant: PrerenderVariant = 'html'): string | null {\n const file = VARIANTS[variant].file;\n const trimmed = path.replace(/^\\/+|\\/+$/g, '');\n if (trimmed === '') return file;\n\n const segments: string[] = [];\n for (const encoded of trimmed.split('/')) {\n const segment = decodeSegment(encoded);\n if (!isStorableSegment(segment)) return null;\n segments.push(segment);\n }\n return `${segments.join('/')}/${file}`;\n}\n\n/**\n * {@link ssgFilePath} as a URL path, for a store addressed by one rather than by key. Every segment is escaped\n * on the way out and the store decodes it back, so a page reached through a URL lands on the same file a\n * filesystem target opens by name.\n */\nexport function ssgAssetPath(relPath: string): string {\n return relPath.split('/').map(encodeURIComponent).join('/');\n}\n\n/** The default cache budget: enough for a large documentation site's working set, small enough to be ignorable. */\nconst DEFAULT_CACHE_BYTES = 32 * 1024 * 1024;\n\n/**\n * A bounded, insertion-ordered cache of prerendered pages, so a site with thousands of them keeps a working set\n * rather than the whole build in memory. Only *hits* are stored — caching misses would let anyone mint entries\n * by requesting paths that don't exist — and the files never change while the server is up.\n *\n * Bounded by **bytes**, not by entry count: a count says nothing about how much memory it stands for, and a\n * page can be any size. At 128 entries — the previous limit — a documentation site with half-megabyte pages\n * retained ~64 MB across the two variants, with nothing about the number to suggest it.\n *\n * A page larger than the whole budget is served and not stored, rather than evicting everything to hold one.\n */\nexport function createPageCache(maxBytes = DEFAULT_CACHE_BYTES): {\n get(key: string): PrerenderedPage | undefined;\n set(key: string, page: PrerenderedPage): void;\n} {\n const pages = new Map<string, PrerenderedPage>();\n let bytes = 0;\n return {\n get: (key) => pages.get(key),\n set(key, page) {\n const size = page.body.byteLength;\n if (size > maxBytes) return;\n // Deleted before it is set, so re-setting an existing key moves it to the *back*: `Map.set` on a key it\n // already holds keeps that key's original position, which would leave the entry just stored sitting at\n // the front as the next thing evicted — a store whose eviction could drop the write that caused it.\n const replaced = pages.get(key);\n if (replaced) {\n bytes -= replaced.body.byteLength;\n pages.delete(key);\n }\n pages.set(key, page);\n bytes += size;\n // Insertion-ordered, so the first key is the oldest.\n while (bytes > maxBytes) {\n const oldest = pages.keys().next().value!;\n bytes -= pages.get(oldest)!.body.byteLength;\n pages.delete(oldest);\n }\n },\n };\n}\n\n/**\n * A weak `ETag` for a page body — see {@link PrerenderedPage.etag} for why weak. Web Crypto rather than\n * `node:crypto`, so one implementation serves both a Node server and `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. `storeEtag` is the validator the\n * store supplied, where it has one: it already describes these exact bytes, so it is weakened rather than\n * replaced by a fresh hash.\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. Bytes rather\n * than a string, because this cache entry is served verbatim to every request that hits it — as a string,\n * each would pay a fresh UTF-8 encode of the whole page.\n */\n body: Uint8Array<ArrayBuffer>;\n /**\n * `Content-Length` for {@link body}, in bytes. Served explicitly because Hono sets no length for an\n * in-memory body, and a proxy in front is entitled to make decisions with it.\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 them, so a\n * strong validator would differ per coding and a cache would treat the 200 and the 304 that revalidates it\n * as different pages. A weak tag says \"the same representation\", which stays true across codings.\n */\n etag: string;\n}\n"]}
|
|
@@ -27,6 +27,18 @@ export declare const SERVER_DEFAULTS: {
|
|
|
27
27
|
readonly port: 3000;
|
|
28
28
|
readonly host: '0.0.0.0';
|
|
29
29
|
};
|
|
30
|
+
/**
|
|
31
|
+
* Reads a port out of `--port` or `PORT`, so both are read the same way wherever the address is resolved.
|
|
32
|
+
*
|
|
33
|
+
* A blank value is `undefined` — "unset", falling through to {@link SERVER_DEFAULTS}. An empty `PORT` is
|
|
34
|
+
* common in CI images and container templates, and the other reading of it is a silent failure: `Number('')`
|
|
35
|
+
* is `0`, which binds a random free port and then reports success.
|
|
36
|
+
*
|
|
37
|
+
* @param source how to name the value in the error — the flag or the variable it came from.
|
|
38
|
+
* @throws RangeError for anything present that is not a port, rather than letting `NaN` reach Node as a raw
|
|
39
|
+
* `ERR_SOCKET_BAD_PORT` with a bundler frame in the stack.
|
|
40
|
+
*/
|
|
41
|
+
export declare function parsePort(value: string | undefined, source: string): number | undefined;
|
|
30
42
|
/**
|
|
31
43
|
* Resolves the user's config into the {@link ServerConfig} baked into the bundle. `isDev` is a build-time input
|
|
32
44
|
* rather than a config field because it decides one thing the user should not have to: `trustProxy` is forced on
|
|
@@ -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;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B;;;OAGG;IACH,KAAK,EAAE,OAAO,CAAC;IACf,oGAAoG;IACpG,UAAU,EAAE,OAAO,CAAC;IACpB;;;;OAIG;IACH,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,eAAO,MAAM,eAAe;aAC1B,IAAI,EAAE,IAAI;aACV,IAAI,EAAE,SAAS;CACP,CAAC;
|
|
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;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B;;;OAGG;IACH,KAAK,EAAE,OAAO,CAAC;IACf,oGAAoG;IACpG,UAAU,EAAE,OAAO,CAAC;IACpB;;;;OAIG;IACH,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,eAAO,MAAM,eAAe;aAC1B,IAAI,EAAE,IAAI;aACV,IAAI,EAAE,SAAS;CACP,CAAC;AAKX;;;;;;;;;;GAUG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAUvF;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,YAAY,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,YAAY,CAM7H"}
|
|
@@ -6,6 +6,31 @@ export const SERVER_DEFAULTS = {
|
|
|
6
6
|
port: 3000,
|
|
7
7
|
host: '0.0.0.0',
|
|
8
8
|
};
|
|
9
|
+
/** The highest port a TCP listener can bind. `0` is the lowest, and means "any free port". */
|
|
10
|
+
const MAX_PORT = 65535;
|
|
11
|
+
/**
|
|
12
|
+
* Reads a port out of `--port` or `PORT`, so both are read the same way wherever the address is resolved.
|
|
13
|
+
*
|
|
14
|
+
* A blank value is `undefined` — "unset", falling through to {@link SERVER_DEFAULTS}. An empty `PORT` is
|
|
15
|
+
* common in CI images and container templates, and the other reading of it is a silent failure: `Number('')`
|
|
16
|
+
* is `0`, which binds a random free port and then reports success.
|
|
17
|
+
*
|
|
18
|
+
* @param source how to name the value in the error — the flag or the variable it came from.
|
|
19
|
+
* @throws RangeError for anything present that is not a port, rather than letting `NaN` reach Node as a raw
|
|
20
|
+
* `ERR_SOCKET_BAD_PORT` with a bundler frame in the stack.
|
|
21
|
+
*/
|
|
22
|
+
export function parsePort(value, source) {
|
|
23
|
+
const text = value?.trim();
|
|
24
|
+
if (!text)
|
|
25
|
+
return undefined;
|
|
26
|
+
// Digits only: `Number` alone would also accept `0x50`, `1e3`, `+80` and `3.0`, none of which anyone
|
|
27
|
+
// typed meaning the port they would get.
|
|
28
|
+
const port = /^\d+$/.test(text) ? Number(text) : Number.NaN;
|
|
29
|
+
if (Number.isNaN(port) || port > MAX_PORT) {
|
|
30
|
+
throw new RangeError(`invalid ${source} ${JSON.stringify(value)} — expected an integer between 0 and ${MAX_PORT}.`);
|
|
31
|
+
}
|
|
32
|
+
return port;
|
|
33
|
+
}
|
|
9
34
|
/**
|
|
10
35
|
* Resolves the user's config into the {@link ServerConfig} baked into the bundle. `isDev` is a build-time input
|
|
11
36
|
* rather than a config field because it decides one thing the user should not have to: `trustProxy` is forced on
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server-config.js","sourceRoot":"","sources":["../../src/server/server-config.ts"],"names":[],"mappings":"AAuBA;;;GAGG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B,IAAI,EAAE,IAAI;IACV,IAAI,EAAE,SAAS;CACP,CAAC;AAEX;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CAAC,MAAoB,EAAE,EAAE,KAAK,EAAE,MAAM,EAAsC;IAC7G,OAAO;QACL,KAAK;QACL,UAAU,EAAE,KAAK,IAAI,CAAC,MAAM,CAAC,UAAU,IAAI,KAAK,CAAC;QACjD,MAAM;KACP,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 — produced once by\n * {@link resolveServerConfig} and compiled in as the `__RSHONO_CONFIG__` literal. There is no runtime env-var\n * interface for these, and nothing here is a per-request concern: those are Hono middleware in `src/server.ts`.\n */\nexport interface ServerConfig {\n /**\n * `true` when the bundle came from `rshono dev`. Baked in rather than read from `process.env.NODE_ENV`,\n * because a deploy target need not have a `process`.\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 * The output directory this bundle was written to, relative to the project root. Carried into the bundle\n * because `deploy/filesystem.ts` locates the assets and prerendered pages beside it at runtime, and only the\n * compiler knows which directory it used.\n */\n outDir: string;\n}\n\n/**\n * The framework's built-in defaults. `port` and `host` are not config fields — they come from `PORT` / `HOST`\n * or `--port` wherever the address is resolved, and this is what those fall back to.\n */\nexport const SERVER_DEFAULTS = {\n port: 3000,\n host: '0.0.0.0',\n} as const;\n\n/**\n * Resolves the user's config into the {@link ServerConfig} baked into the bundle. `isDev` is a build-time input\n * rather than a config field because it decides one thing the user should not have to: `trustProxy` is forced on\n * under `rshono dev`, where the framework's own localhost proxy is the only way in.\n */\nexport function resolveServerConfig(config: RshonoConfig, { isDev, outDir }: { isDev: boolean; outDir: string }): ServerConfig {\n return {\n isDev,\n trustProxy: isDev || (config.trustProxy ?? false),\n outDir,\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"server-config.js","sourceRoot":"","sources":["../../src/server/server-config.ts"],"names":[],"mappings":"AAuBA;;;GAGG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B,IAAI,EAAE,IAAI;IACV,IAAI,EAAE,SAAS;CACP,CAAC;AAEX,8FAA8F;AAC9F,MAAM,QAAQ,GAAG,KAAK,CAAC;AAEvB;;;;;;;;;;GAUG;AACH,MAAM,UAAU,SAAS,CAAC,KAAyB,EAAE,MAAc;IACjE,MAAM,IAAI,GAAG,KAAK,EAAE,IAAI,EAAE,CAAC;IAC3B,IAAI,CAAC,IAAI;QAAE,OAAO,SAAS,CAAC;IAC5B,qGAAqG;IACrG,yCAAyC;IACzC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;IAC5D,IAAI,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,IAAI,GAAG,QAAQ,EAAE,CAAC;QAC1C,MAAM,IAAI,UAAU,CAAC,WAAW,MAAM,IAAI,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,wCAAwC,QAAQ,GAAG,CAAC,CAAC;IACtH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CAAC,MAAoB,EAAE,EAAE,KAAK,EAAE,MAAM,EAAsC;IAC7G,OAAO;QACL,KAAK;QACL,UAAU,EAAE,KAAK,IAAI,CAAC,MAAM,CAAC,UAAU,IAAI,KAAK,CAAC;QACjD,MAAM;KACP,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 — produced once by\n * {@link resolveServerConfig} and compiled in as the `__RSHONO_CONFIG__` literal. There is no runtime env-var\n * interface for these, and nothing here is a per-request concern: those are Hono middleware in `src/server.ts`.\n */\nexport interface ServerConfig {\n /**\n * `true` when the bundle came from `rshono dev`. Baked in rather than read from `process.env.NODE_ENV`,\n * because a deploy target need not have a `process`.\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 * The output directory this bundle was written to, relative to the project root. Carried into the bundle\n * because `deploy/filesystem.ts` locates the assets and prerendered pages beside it at runtime, and only the\n * compiler knows which directory it used.\n */\n outDir: string;\n}\n\n/**\n * The framework's built-in defaults. `port` and `host` are not config fields — they come from `PORT` / `HOST`\n * or `--port` wherever the address is resolved, and this is what those fall back to.\n */\nexport const SERVER_DEFAULTS = {\n port: 3000,\n host: '0.0.0.0',\n} as const;\n\n/** The highest port a TCP listener can bind. `0` is the lowest, and means \"any free port\". */\nconst MAX_PORT = 65535;\n\n/**\n * Reads a port out of `--port` or `PORT`, so both are read the same way wherever the address is resolved.\n *\n * A blank value is `undefined` — \"unset\", falling through to {@link SERVER_DEFAULTS}. An empty `PORT` is\n * common in CI images and container templates, and the other reading of it is a silent failure: `Number('')`\n * is `0`, which binds a random free port and then reports success.\n *\n * @param source how to name the value in the error — the flag or the variable it came from.\n * @throws RangeError for anything present that is not a port, rather than letting `NaN` reach Node as a raw\n * `ERR_SOCKET_BAD_PORT` with a bundler frame in the stack.\n */\nexport function parsePort(value: string | undefined, source: string): number | undefined {\n const text = value?.trim();\n if (!text) return undefined;\n // Digits only: `Number` alone would also accept `0x50`, `1e3`, `+80` and `3.0`, none of which anyone\n // typed meaning the port they would get.\n const port = /^\\d+$/.test(text) ? Number(text) : Number.NaN;\n if (Number.isNaN(port) || port > MAX_PORT) {\n throw new RangeError(`invalid ${source} ${JSON.stringify(value)} — expected an integer between 0 and ${MAX_PORT}.`);\n }\n return port;\n}\n\n/**\n * Resolves the user's config into the {@link ServerConfig} baked into the bundle. `isDev` is a build-time input\n * rather than a config field because it decides one thing the user should not have to: `trustProxy` is forced on\n * under `rshono dev`, where the framework's own localhost proxy is the only way in.\n */\nexport function resolveServerConfig(config: RshonoConfig, { isDev, outDir }: { isDev: boolean; outDir: string }): ServerConfig {\n return {\n isDev,\n trustProxy: isDev || (config.trustProxy ?? false),\n outDir,\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;AAQ1B;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAUrE;
|
|
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;AAQ1B;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAUrE;AAsDD,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAyB,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAgC9I;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,oGAAoG;IACpG,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AA4DD,wBAAsB,qBAAqB,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,CA0H/F"}
|
package/dist/server/ssg.js
CHANGED
|
@@ -2,7 +2,7 @@ import { mkdirSync, writeFileSync } from 'node:fs';
|
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import { dirname, join, resolve, sep } from 'node:path';
|
|
4
4
|
import { isPageRoute } from '../router.js';
|
|
5
|
-
import { createPageCache,
|
|
5
|
+
import { createPageCache, ssgFilePath, SSG_MANIFEST_FILE, toPrerenderedPage, VARIANTS, } from './prerendered.js';
|
|
6
6
|
/**
|
|
7
7
|
* Stand-in origin for a build that declared no `siteUrl`. Obviously wrong rather than a guess, so a page that
|
|
8
8
|
* bakes it into a canonical tag is easy to spot; the build warns as well.
|
|
@@ -24,23 +24,30 @@ export function resolveSiteOrigin(siteUrl) {
|
|
|
24
24
|
}
|
|
25
25
|
return parsed.origin;
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* A route's path with its params filled in, ready to render and to store.
|
|
29
|
+
*
|
|
30
|
+
* Throws for a pattern `staticPaths` cannot fill and for a set that does not fill it. Both are warned about
|
|
31
|
+
* and skipped by the caller, which already names the route — so these messages are the reason alone, written
|
|
32
|
+
* to be read at the end of that line.
|
|
33
|
+
*/
|
|
27
34
|
function interpolatePath(pattern, params) {
|
|
28
35
|
return pattern
|
|
29
36
|
.split('/')
|
|
30
37
|
.map((segment) => {
|
|
31
38
|
if (!segment.startsWith(':')) {
|
|
32
39
|
if (segment.includes('*')) {
|
|
33
|
-
throw new Error(`
|
|
40
|
+
throw new Error(`wildcard segments are not supported by staticPaths`);
|
|
34
41
|
}
|
|
35
42
|
return segment;
|
|
36
43
|
}
|
|
37
44
|
const name = segment.slice(1);
|
|
38
45
|
if (!/^\w+$/.test(name)) {
|
|
39
|
-
throw new Error(`
|
|
46
|
+
throw new Error(`optional/regex params are not supported by staticPaths`);
|
|
40
47
|
}
|
|
41
48
|
const value = params[name];
|
|
42
49
|
if (value === undefined) {
|
|
43
|
-
throw new Error(`staticPaths
|
|
50
|
+
throw new Error(`staticPaths returned a param set without "${name}"`);
|
|
44
51
|
}
|
|
45
52
|
return encodeURIComponent(value);
|
|
46
53
|
})
|
|
@@ -48,6 +55,23 @@ function interpolatePath(pattern, params) {
|
|
|
48
55
|
}
|
|
49
56
|
/** Prerendered pages, keyed by the request that produced them (see {@link readPrerendered}). */
|
|
50
57
|
const pageCache = createPageCache();
|
|
58
|
+
/** One in-flight or settled read of each store's {@link SSG_MANIFEST_FILE}; the file cannot change while the server is up. */
|
|
59
|
+
const storeIndexes = new Map();
|
|
60
|
+
/**
|
|
61
|
+
* What `ssgDir` holds, as {@link ssgFilePath} names it — or `null` where the build left no manifest, which
|
|
62
|
+
* is every build made before there was one. A `null` gates nothing, so those keep the behaviour they had.
|
|
63
|
+
*/
|
|
64
|
+
function storeIndex(ssgDir) {
|
|
65
|
+
let pending = storeIndexes.get(ssgDir);
|
|
66
|
+
if (!pending) {
|
|
67
|
+
// One `catch` for both failures worth the same answer: no manifest, and a manifest that is not one.
|
|
68
|
+
pending = readFile(join(ssgDir, SSG_MANIFEST_FILE), 'utf8')
|
|
69
|
+
.then((text) => new Set(JSON.parse(text).files ?? []))
|
|
70
|
+
.catch(() => null);
|
|
71
|
+
storeIndexes.set(ssgDir, pending);
|
|
72
|
+
}
|
|
73
|
+
return pending;
|
|
74
|
+
}
|
|
51
75
|
export async function readPrerendered(ssgDir, requestPath, variant = 'html') {
|
|
52
76
|
// Keyed by what the request carried rather than by the resolved filename, so a hit costs one Map lookup. Safe
|
|
53
77
|
// because only *hits* are cached: an entry exists only if this exact key already passed the checks below.
|
|
@@ -55,9 +79,15 @@ export async function readPrerendered(ssgDir, requestPath, variant = 'html') {
|
|
|
55
79
|
const cached = pageCache.get(key);
|
|
56
80
|
if (cached)
|
|
57
81
|
return cached;
|
|
58
|
-
const relPath =
|
|
82
|
+
const relPath = ssgFilePath(requestPath, variant);
|
|
59
83
|
if (relPath === null)
|
|
60
84
|
return null;
|
|
85
|
+
// Before the store is touched: a `render: 'static'` route whose build wrote nothing — no `staticPaths`, a
|
|
86
|
+
// param the build never saw, a page that did not render cleanly — falls through to SSR on every request,
|
|
87
|
+
// and without this each of those pays a failed read first, forever.
|
|
88
|
+
const index = await storeIndex(ssgDir);
|
|
89
|
+
if (index && !index.has(relPath))
|
|
90
|
+
return null;
|
|
61
91
|
// Belt and braces over the shared traversal guard: this proves the resolved file is under the root.
|
|
62
92
|
const root = resolve(ssgDir);
|
|
63
93
|
const file = resolve(root, relPath);
|
|
@@ -76,6 +106,40 @@ export async function readPrerendered(ssgDir, requestPath, variant = 'html') {
|
|
|
76
106
|
pageCache.set(key, page);
|
|
77
107
|
return page;
|
|
78
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Records what the pass wrote, beside what it wrote. Written by the same pass for the same reason
|
|
111
|
+
* {@link ssgFilePath} is one function: an index that disagrees with the store is invisible, so the two are
|
|
112
|
+
* produced together or not at all.
|
|
113
|
+
*/
|
|
114
|
+
function writeManifest(ssgDir, files) {
|
|
115
|
+
mkdirSync(ssgDir, { recursive: true });
|
|
116
|
+
writeFileSync(join(ssgDir, SSG_MANIFEST_FILE), `${JSON.stringify({ files }, null, 2)}\n`);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* How many paths are rendered at once.
|
|
120
|
+
*
|
|
121
|
+
* A prerender pass spends its time inside the app — a page's own data fetching is what each render awaits —
|
|
122
|
+
* so a few hundred `staticPaths` entries rendered one at a time pay all of that latency in series.
|
|
123
|
+
* Unbounded is the other wrong answer: it points the whole site at the app's database in one go. Eight keeps
|
|
124
|
+
* a build's peak load recognisable while hiding most of the wait.
|
|
125
|
+
*/
|
|
126
|
+
const RENDER_CONCURRENCY = 8;
|
|
127
|
+
/**
|
|
128
|
+
* `items.map(run)` with at most `limit` running at once, results in input order.
|
|
129
|
+
*
|
|
130
|
+
* A worker pool rather than fixed batches: a batch is only as fast as its slowest member, so one slow page
|
|
131
|
+
* would idle the rest of its batch.
|
|
132
|
+
*/
|
|
133
|
+
async function mapBounded(items, limit, run) {
|
|
134
|
+
const results = new Array(items.length);
|
|
135
|
+
let next = 0;
|
|
136
|
+
const worker = async () => {
|
|
137
|
+
for (let index = next++; index < items.length; index = next++)
|
|
138
|
+
results[index] = await run(items[index]);
|
|
139
|
+
};
|
|
140
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
|
|
141
|
+
return results;
|
|
142
|
+
}
|
|
79
143
|
async function renderVariant(fetch, url, variant) {
|
|
80
144
|
const response = await fetch(new Request(url, { headers: VARIANTS[variant].headers }));
|
|
81
145
|
if (response.status !== 200)
|
|
@@ -95,6 +159,12 @@ export async function prerenderStaticRoutes(options) {
|
|
|
95
159
|
}
|
|
96
160
|
const written = [];
|
|
97
161
|
const skipped = [];
|
|
162
|
+
/** Every file written, as the reader names it — {@link writeManifest}. */
|
|
163
|
+
const files = [];
|
|
164
|
+
/** Every path to render, in route order, each one once. Built before anything renders — see below. */
|
|
165
|
+
const targets = [];
|
|
166
|
+
/** So a `staticPaths` entry that repeats — or that a second route also produces — is rendered once. */
|
|
167
|
+
const queued = new Set();
|
|
98
168
|
for (const route of staticRoutes) {
|
|
99
169
|
let paths;
|
|
100
170
|
if (!/[:*]/.test(route.path)) {
|
|
@@ -106,33 +176,89 @@ export async function prerenderStaticRoutes(options) {
|
|
|
106
176
|
skipped.push(route.path);
|
|
107
177
|
continue;
|
|
108
178
|
}
|
|
109
|
-
|
|
179
|
+
try {
|
|
180
|
+
paths = (await route.staticPaths()).map((params) => interpolatePath(route.path, params));
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
// A route whose paths cannot be computed — a wildcard or a regex/optional param, which `staticPaths`
|
|
184
|
+
// has no way to fill, or a set it filled wrongly — is unprerenderable, not unservable. It gets the
|
|
185
|
+
// same answer as the branch above rather than killing a build the route would have survived: the
|
|
186
|
+
// page is still there, rendered per request.
|
|
187
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
188
|
+
console.warn(` ⚠ Static route "${route.path}" will SSR per request — ${reason.replace(/\.$/, '')}.`);
|
|
189
|
+
skipped.push(route.path);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
110
192
|
}
|
|
193
|
+
let duplicates = 0;
|
|
111
194
|
for (const path of paths) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
console.warn(` ⚠ "${path}" rendered ${document.reason} at build time — skipping, will SSR per request.`);
|
|
115
|
-
skipped.push(path);
|
|
195
|
+
if (queued.has(path)) {
|
|
196
|
+
duplicates++;
|
|
116
197
|
continue;
|
|
117
198
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
write('flight', flight.body);
|
|
129
|
-
}
|
|
130
|
-
else {
|
|
131
|
-
console.warn(` ⚠ "${path}" produced no flight payload — soft navigations to it will render per request.`);
|
|
199
|
+
queued.add(path);
|
|
200
|
+
// Resolved *before* the render, so a path no file can hold fails the build naming the path, rather than
|
|
201
|
+
// after the work and as a bare `join(dir, null)` TypeError. The same call answers for the reader, which
|
|
202
|
+
// is the whole point: a page written under a name the request path never resolves to is a page the build
|
|
203
|
+
// reports as prerendered and nothing ever serves. Resolved in this pass rather than the next one for a
|
|
204
|
+
// second reason too: which of several bad paths fails the build should not depend on render order.
|
|
205
|
+
const relPath = ssgFilePath(path, 'html');
|
|
206
|
+
if (relPath === null) {
|
|
207
|
+
throw new Error(`Cannot prerender "${path}" for route "${route.path}": a path segment is "." or "..", or holds a ` +
|
|
208
|
+
`character a portable file name cannot — one of \\ / : * ? " < > | or a control character.`);
|
|
132
209
|
}
|
|
133
|
-
|
|
210
|
+
targets.push({ path, relPath });
|
|
211
|
+
}
|
|
212
|
+
if (duplicates > 0) {
|
|
213
|
+
console.warn(` ⚠ Static route "${route.path}" repeated ${duplicates} path${duplicates === 1 ? '' : 's'} — each page is prerendered once.`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const outcomes = await mapBounded(targets, RENDER_CONCURRENCY, async ({ path, relPath }) => {
|
|
217
|
+
/** Buffered, so a concurrent pass logs in the same order a serial one did. */
|
|
218
|
+
const warnings = [];
|
|
219
|
+
const document = await renderVariant(fetch, origin + path, 'html');
|
|
220
|
+
if (!document.ok) {
|
|
221
|
+
warnings.push(` ⚠ "${path}" rendered ${document.reason} at build time — skipping, will SSR per request.`);
|
|
222
|
+
return { path, ok: false, files: [], warnings };
|
|
223
|
+
}
|
|
224
|
+
// Both representations of a page share its directory and differ only in the file name.
|
|
225
|
+
const pageDir = join(ssgDir, dirname(relPath));
|
|
226
|
+
const wrote = [];
|
|
227
|
+
const write = (variant, body) => {
|
|
228
|
+
mkdirSync(pageDir, { recursive: true });
|
|
229
|
+
writeFileSync(join(pageDir, VARIANTS[variant].file), body);
|
|
230
|
+
// `ssgFilePath` again rather than the `join` above: the manifest is read against what a *request*
|
|
231
|
+
// resolves to, which is that function's output and not a filesystem path.
|
|
232
|
+
wrote.push(ssgFilePath(path, variant));
|
|
233
|
+
};
|
|
234
|
+
write('html', document.body);
|
|
235
|
+
// The soft-navigation representation of the same page. Best-effort: the document is valid on its own, and
|
|
236
|
+
// serving falls back to rendering the flight payload per request.
|
|
237
|
+
const flight = await renderVariant(fetch, origin + path, 'flight');
|
|
238
|
+
if (flight.ok) {
|
|
239
|
+
write('flight', flight.body);
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
warnings.push(` ⚠ "${path}" produced no flight payload — soft navigations to it will render per request.`);
|
|
243
|
+
}
|
|
244
|
+
return { path, ok: true, files: wrote, warnings };
|
|
245
|
+
});
|
|
246
|
+
// Folded back in target order rather than completion order, so a concurrent pass writes the same manifest
|
|
247
|
+
// and prints the same log a serial one did.
|
|
248
|
+
for (const outcome of outcomes) {
|
|
249
|
+
for (const warning of outcome.warnings)
|
|
250
|
+
console.warn(warning);
|
|
251
|
+
if (!outcome.ok) {
|
|
252
|
+
skipped.push(outcome.path);
|
|
253
|
+
continue;
|
|
134
254
|
}
|
|
255
|
+
files.push(...outcome.files);
|
|
256
|
+
written.push(outcome.path);
|
|
135
257
|
}
|
|
258
|
+
// Only where the app has static routes at all: an empty manifest would be a store, and a build with
|
|
259
|
+
// nothing to prerender should leave no `ssg/` directory for a preset to carry around.
|
|
260
|
+
if (staticRoutes.length > 0)
|
|
261
|
+
writeManifest(ssgDir, files);
|
|
136
262
|
return { written, skipped };
|
|
137
263
|
}
|
|
138
264
|
//# sourceMappingURL=ssg.js.map
|
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 declared no `siteUrl`. Obviously wrong rather than a guess, so a page that\n * bakes it into a canonical tag is easy to spot; the build warns as well.\n */\nconst DEFAULT_SSG_ORIGIN = 'http://localhost';\n\n/**\n * Resolves `siteUrl` to the origin prerendering renders against. A path is rejected rather than dropped:\n * `'https://example.com/docs'` means the author expects a base path, which is not 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 rather than by the resolved filename, so a hit costs one Map lookup. Safe\n // because only *hits* are cached: an entry 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 over the shared traversal guard: this proves the resolved file is under the root.\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. Copied out of the Buffer rather than kept as one,\n // because `readFile` can hand back a view into Node's shared pool and this is retained for 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 /** The origin absolute URLs in the output are built against — `siteUrl` from `rshono.config.ts`. */\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. Discriminated on `ok` because both\n * callers have to tell \"the app rendered this\" from \"it did not\", and only one cares why.\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: VARIANTS[variant].headers }));\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: the document is valid on its own, and\n // serving falls back to rendering the 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"]}
|
|
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 ssgFilePath,\n SSG_MANIFEST_FILE,\n toPrerenderedPage,\n VARIANTS,\n type PrerenderedPage,\n type PrerenderVariant,\n} from './prerendered.js';\n\n/**\n * Stand-in origin for a build that declared no `siteUrl`. Obviously wrong rather than a guess, so a page that\n * bakes it into a canonical tag is easy to spot; the build warns as well.\n */\nconst DEFAULT_SSG_ORIGIN = 'http://localhost';\n\n/**\n * Resolves `siteUrl` to the origin prerendering renders against. A path is rejected rather than dropped:\n * `'https://example.com/docs'` means the author expects a base path, which is not 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\n/**\n * A route's path with its params filled in, ready to render and to store.\n *\n * Throws for a pattern `staticPaths` cannot fill and for a set that does not fill it. Both are warned about\n * and skipped by the caller, which already names the route — so these messages are the reason alone, written\n * to be read at the end of that line.\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(`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(`optional/regex params are not supported by staticPaths`);\n }\n const value = params[name];\n if (value === undefined) {\n throw new Error(`staticPaths 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\n/** One in-flight or settled read of each store's {@link SSG_MANIFEST_FILE}; the file cannot change while the server is up. */\nconst storeIndexes = new Map<string, Promise<ReadonlySet<string> | null>>();\n\n/**\n * What `ssgDir` holds, as {@link ssgFilePath} names it — or `null` where the build left no manifest, which\n * is every build made before there was one. A `null` gates nothing, so those keep the behaviour they had.\n */\nfunction storeIndex(ssgDir: string): Promise<ReadonlySet<string> | null> {\n let pending = storeIndexes.get(ssgDir);\n if (!pending) {\n // One `catch` for both failures worth the same answer: no manifest, and a manifest that is not one.\n pending = readFile(join(ssgDir, SSG_MANIFEST_FILE), 'utf8')\n .then((text) => new Set((JSON.parse(text) as { files?: string[] }).files ?? []) as ReadonlySet<string>)\n .catch(() => null);\n storeIndexes.set(ssgDir, pending);\n }\n return pending;\n}\n\nexport async function readPrerendered(ssgDir: string, requestPath: string, variant: PrerenderVariant = 'html'): Promise<PrerenderedPage | null> {\n // Keyed by what the request carried rather than by the resolved filename, so a hit costs one Map lookup. Safe\n // because only *hits* are cached: an entry 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 = ssgFilePath(requestPath, variant);\n if (relPath === null) return null;\n\n // Before the store is touched: a `render: 'static'` route whose build wrote nothing — no `staticPaths`, a\n // param the build never saw, a page that did not render cleanly — falls through to SSR on every request,\n // and without this each of those pays a failed read first, forever.\n const index = await storeIndex(ssgDir);\n if (index && !index.has(relPath)) return null;\n // Belt and braces over the shared traversal guard: this proves the resolved file is under the root.\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. Copied out of the Buffer rather than kept as one,\n // because `readFile` can hand back a view into Node's shared pool and this is retained for 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 /** The origin absolute URLs in the output are built against — `siteUrl` from `rshono.config.ts`. */\n siteUrl?: string;\n}\n\nexport interface PrerenderResult {\n written: string[];\n skipped: string[];\n}\n\n/**\n * Records what the pass wrote, beside what it wrote. Written by the same pass for the same reason\n * {@link ssgFilePath} is one function: an index that disagrees with the store is invisible, so the two are\n * produced together or not at all.\n */\nfunction writeManifest(ssgDir: string, files: readonly string[]): void {\n mkdirSync(ssgDir, { recursive: true });\n writeFileSync(join(ssgDir, SSG_MANIFEST_FILE), `${JSON.stringify({ files }, null, 2)}\\n`);\n}\n\n/**\n * How many paths are rendered at once.\n *\n * A prerender pass spends its time inside the app — a page's own data fetching is what each render awaits —\n * so a few hundred `staticPaths` entries rendered one at a time pay all of that latency in series.\n * Unbounded is the other wrong answer: it points the whole site at the app's database in one go. Eight keeps\n * a build's peak load recognisable while hiding most of the wait.\n */\nconst RENDER_CONCURRENCY = 8;\n\n/**\n * `items.map(run)` with at most `limit` running at once, results in input order.\n *\n * A worker pool rather than fixed batches: a batch is only as fast as its slowest member, so one slow page\n * would idle the rest of its batch.\n */\nasync function mapBounded<T, R>(items: readonly T[], limit: number, run: (item: T) => Promise<R>): Promise<R[]> {\n const results = new Array<R>(items.length);\n let next = 0;\n const worker = async (): Promise<void> => {\n for (let index = next++; index < items.length; index = next++) results[index] = await run(items[index]);\n };\n await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));\n return results;\n}\n\n/**\n * One representation of a path, as the app answered for it at build time. Discriminated on `ok` because both\n * callers have to tell \"the app rendered this\" from \"it did not\", and only one cares why.\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: VARIANTS[variant].headers }));\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\n/** A path the pass will render, resolved far enough that nothing left can fail the build. */\ninterface PrerenderTarget {\n path: string;\n /** Where a request for it will look — {@link ssgFilePath}, resolved before the render rather than after. */\n relPath: string;\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 /** Every file written, as the reader names it — {@link writeManifest}. */\n const files: string[] = [];\n\n /** Every path to render, in route order, each one once. Built before anything renders — see below. */\n const targets: PrerenderTarget[] = [];\n /** So a `staticPaths` entry that repeats — or that a second route also produces — is rendered once. */\n const queued = new Set<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 try {\n paths = (await route.staticPaths()).map((params) => interpolatePath(route.path, params));\n } catch (error) {\n // A route whose paths cannot be computed — a wildcard or a regex/optional param, which `staticPaths`\n // has no way to fill, or a set it filled wrongly — is unprerenderable, not unservable. It gets the\n // same answer as the branch above rather than killing a build the route would have survived: the\n // page is still there, rendered per request.\n const reason = error instanceof Error ? error.message : String(error);\n console.warn(` ⚠ Static route \"${route.path}\" will SSR per request — ${reason.replace(/\\.$/, '')}.`);\n skipped.push(route.path);\n continue;\n }\n }\n\n let duplicates = 0;\n for (const path of paths) {\n if (queued.has(path)) {\n duplicates++;\n continue;\n }\n queued.add(path);\n // Resolved *before* the render, so a path no file can hold fails the build naming the path, rather than\n // after the work and as a bare `join(dir, null)` TypeError. The same call answers for the reader, which\n // is the whole point: a page written under a name the request path never resolves to is a page the build\n // reports as prerendered and nothing ever serves. Resolved in this pass rather than the next one for a\n // second reason too: which of several bad paths fails the build should not depend on render order.\n const relPath = ssgFilePath(path, 'html');\n if (relPath === null) {\n throw new Error(\n `Cannot prerender \"${path}\" for route \"${route.path}\": a path segment is \".\" or \"..\", or holds a ` +\n `character a portable file name cannot — one of \\\\ / : * ? \" < > | or a control character.`,\n );\n }\n targets.push({ path, relPath });\n }\n if (duplicates > 0) {\n console.warn(` ⚠ Static route \"${route.path}\" repeated ${duplicates} path${duplicates === 1 ? '' : 's'} — each page is prerendered once.`);\n }\n }\n\n const outcomes = await mapBounded(targets, RENDER_CONCURRENCY, async ({ path, relPath }) => {\n /** Buffered, so a concurrent pass logs in the same order a serial one did. */\n const warnings: string[] = [];\n const document = await renderVariant(fetch, origin + path, 'html');\n if (!document.ok) {\n warnings.push(` ⚠ \"${path}\" rendered ${document.reason} at build time — skipping, will SSR per request.`);\n return { path, ok: false as const, files: [], warnings };\n }\n\n // Both representations of a page share its directory and differ only in the file name.\n const pageDir = join(ssgDir, dirname(relPath));\n const wrote: string[] = [];\n const write = (variant: PrerenderVariant, body: string) => {\n mkdirSync(pageDir, { recursive: true });\n writeFileSync(join(pageDir, VARIANTS[variant].file), body);\n // `ssgFilePath` again rather than the `join` above: the manifest is read against what a *request*\n // resolves to, which is that function's output and not a filesystem path.\n wrote.push(ssgFilePath(path, variant)!);\n };\n write('html', document.body);\n\n // The soft-navigation representation of the same page. Best-effort: the document is valid on its own, and\n // serving falls back to rendering the 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 warnings.push(` ⚠ \"${path}\" produced no flight payload — soft navigations to it will render per request.`);\n }\n\n return { path, ok: true as const, files: wrote, warnings };\n });\n\n // Folded back in target order rather than completion order, so a concurrent pass writes the same manifest\n // and prints the same log a serial one did.\n for (const outcome of outcomes) {\n for (const warning of outcome.warnings) console.warn(warning);\n if (!outcome.ok) {\n skipped.push(outcome.path);\n continue;\n }\n files.push(...outcome.files);\n written.push(outcome.path);\n }\n\n // Only where the app has static routes at all: an empty manifest would be a store, and a build with\n // nothing to prerender should leave no `ssg/` directory for a preset to carry around.\n if (staticRoutes.length > 0) writeManifest(ssgDir, files);\n\n return { written, skipped };\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"static.d.ts","sourceRoot":"","sources":["../../src/server/static.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAC9C,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAe5B,UAAU,aAAa;IACrB,yCAAyC;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,qFAAqF;IACrF,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,+EAA+E;AAC/E,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"static.d.ts","sourceRoot":"","sources":["../../src/server/static.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAC9C,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAe5B,UAAU,aAAa;IACrB,yCAAyC;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,qFAAqF;IACrF,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,+EAA+E;AAC/E,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI,CAWlE;AAED,yGAAyG;AACzG,wBAAgB,oBAAoB,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,aAAa,GAAG,iBAAiB,CAStF"}
|
package/dist/server/static.js
CHANGED
|
@@ -13,7 +13,9 @@ function cacheControl(isDev) {
|
|
|
13
13
|
export function createStaticAssetsApp(options) {
|
|
14
14
|
const { root, isDev } = options;
|
|
15
15
|
const app = new Hono();
|
|
16
|
-
|
|
16
|
+
// `GET` alone: Hono dispatches a `HEAD` as a `GET` and strips the body, so a `HEAD` registration beside it
|
|
17
|
+
// is never reached — see `HTTPMethod`.
|
|
18
|
+
app.get('/*', cacheControl(isDev), serveStatic({ root, rewriteRequestPath: (path) => path.replace(/^\/_static/, '') }), (c) => c.text('Not Found', 404));
|
|
17
19
|
return app;
|
|
18
20
|
}
|
|
19
21
|
/** Middleware serving the app's `public/` files verbatim at the web root, for paths no route claimed. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"static.js","sourceRoot":"","sources":["../../src/server/static.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAC;AAE7D,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,MAAM,cAAc,GAAG,kBAAkB,CAAC;AAE1C,SAAS,YAAY,CAAC,KAAc;IAClC,OAAO,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE;QACvB,MAAM,IAAI,EAAE,CAAC;QACb,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,KAAK,GAAG;YAAE,OAAO;QACzD,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CACf,eAAe,EACf,KAAK,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,qCAAqC,CAAC,CAAC,CAAC,qBAAqB,CACrH,CAAC;IACJ,CAAC,CAAC;AACJ,CAAC;AASD,+EAA+E;AAC/E,MAAM,UAAU,qBAAqB,CAAC,OAAsB;IAC1D,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC;IAChC,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;IAEvB,
|
|
1
|
+
{"version":3,"file":"static.js","sourceRoot":"","sources":["../../src/server/static.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAC;AAE7D,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,MAAM,cAAc,GAAG,kBAAkB,CAAC;AAE1C,SAAS,YAAY,CAAC,KAAc;IAClC,OAAO,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE;QACvB,MAAM,IAAI,EAAE,CAAC;QACb,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,KAAK,GAAG;YAAE,OAAO;QACzD,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CACf,eAAe,EACf,KAAK,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,qCAAqC,CAAC,CAAC,CAAC,qBAAqB,CACrH,CAAC;IACJ,CAAC,CAAC;AACJ,CAAC;AASD,+EAA+E;AAC/E,MAAM,UAAU,qBAAqB,CAAC,OAAsB;IAC1D,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC;IAChC,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;IAEvB,2GAA2G;IAC3G,uCAAuC;IACvC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,YAAY,CAAC,KAAK,CAAC,EAAE,WAAW,CAAC,EAAE,IAAI,EAAE,kBAAkB,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAC5H,CAAC,CAAC,IAAI,CAAC,WAAW,EAAE,GAAG,CAAC,CACzB,CAAC;IAEF,OAAO,GAAG,CAAC;AACb,CAAC;AAED,yGAAyG;AACzG,MAAM,UAAU,oBAAoB,CAAC,EAAE,IAAI,EAAE,KAAK,EAAiB;IACjE,MAAM,KAAK,GAAG,WAAW,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC;IACpC,OAAO,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE;QACvB,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;QACpC,IAAI,MAAM,YAAY,QAAQ,IAAI,CAAC,MAAM,CAAC,MAAM,KAAK,GAAG,IAAI,MAAM,CAAC,MAAM,KAAK,GAAG,CAAC,EAAE,CAAC;YACnF,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,eAAe,EAAE,KAAK,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC;QAClF,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC,CAAC;AACJ,CAAC","sourcesContent":["import { serveStatic } from '@hono/node-server/serve-static';\nimport type { MiddlewareHandler } from 'hono';\nimport { Hono } from 'hono';\n\nconst CONTENT_HASHED = /\\.[0-9a-f]{8,}\\./;\n\nfunction cacheControl(isDev: boolean): MiddlewareHandler {\n return async (c, next) => {\n await next();\n if (c.res.status !== 200 && c.res.status !== 206) return;\n c.res.headers.set(\n 'Cache-Control',\n isDev ? 'no-cache' : CONTENT_HASHED.test(c.req.path) ? 'public, max-age=31536000, immutable' : 'public, max-age=300',\n );\n };\n}\n\ninterface StaticOptions {\n /** Directory the files are read from. */\n root: string;\n /** Dev serves revalidated (`no-cache`); a build serves hashed files as immutable. */\n isDev: boolean;\n}\n\n/** A sub-app serving the hashed client bundle, to be mounted at `/_static`. */\nexport function createStaticAssetsApp(options: StaticOptions): Hono {\n const { root, isDev } = options;\n const app = new Hono();\n\n // `GET` alone: Hono dispatches a `HEAD` as a `GET` and strips the body, so a `HEAD` registration beside it\n // is never reached — see `HTTPMethod`.\n app.get('/*', cacheControl(isDev), serveStatic({ root, rewriteRequestPath: (path) => path.replace(/^\\/_static/, '') }), (c) =>\n c.text('Not Found', 404),\n );\n\n return app;\n}\n\n/** Middleware serving the app's `public/` files verbatim at the web root, for paths no route claimed. */\nexport function createPublicFallback({ root, isDev }: StaticOptions): MiddlewareHandler {\n const serve = serveStatic({ root });\n return async (c, next) => {\n const result = await serve(c, next);\n if (result instanceof Response && (result.status === 200 || result.status === 206)) {\n result.headers.set('Cache-Control', isDev ? 'no-cache' : 'public, max-age=300');\n }\n return result;\n };\n}\n"]}
|