@rshono/core 1.0.0-rc.22 → 1.0.0-rc.24

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.
@@ -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 * How a build-time document render tells the prerender pass that the app minted a CSP nonce for its path.\n *\n * The other half of the same contract as {@link VARIANTS}, and it exists because the pass cannot see the\n * request it made: it renders through the app's own middleware inside the server bundle, in a module graph of\n * its own, and keeps the body alone. What it needs to know is whether a *request* for this path will mint a\n * nonce, because a document that will is one the framework re-renders per request — so the file the pass is\n * about to write is one no deployment will read, and saying \"prerendered\" about it is a lie by omission.\n *\n * Set only while `RSHONO_PRERENDER` is in the environment, which is `rshono build`'s own process, so it never\n * reaches a deployed response; and never stored, since only the body is.\n */\nexport const PRERENDER_NONCE_HEADER = 'x-rshono-prerender-nonce';\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 * The DOS device names Windows still reserves, with or without an extension and whatever the case — so\n * `con`, `CON` and `Con.txt` are all the console. `mkdir` on one fails with `EINVAL`, naming a path the\n * author never wrote, which is why they are refused here with the rest.\n *\n * The list is Microsoft's, superscripts included: `CON PRN AUX NUL`, `COM0`–`COM9`, `COM¹ COM² COM³`, and\n * the same three suffixes for `LPT`. A documentation slug of `con` is a plausible thing to write and\n * builds fine on Linux today; refusing it everywhere is the same trade `:` and `*` already make — portable\n * or not at all, rather than a build that works on the author's machine and fails in CI on Windows.\n */\nconst RESERVED_DEVICE_NAME = /^(?:con|prn|aux|nul|com[0-9¹²³]|lpt[0-9¹²³])(?:\\..*)?$/i;\n\n/**\n * A trailing `.` or space, which Win32 **strips** rather than refusing: `docs/x.` is stored as `docs/x`, so\n * the page is written where no reader will look for it — \"a page the build reports as prerendered and\n * nothing ever serves\", which is the failure {@link ssgFilePath} exists to prevent. The silent half of this\n * rule, and the more important one: the reserved names above at least fail loudly.\n */\nconst TRAILING_STRIPPED = /[. ]$/;\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, on every host rather than on this\n * one: not empty, not a relative-path marker, made only of characters a file name can hold\n * ({@link UNPORTABLE_SEGMENT}, plus the control characters, which a percent-escape is free to carry into a\n * path and no filesystem wants in a name), not a {@link RESERVED_DEVICE_NAME}, and not ending in a\n * character Win32 would {@link TRAILING_STRIPPED | strip}.\n *\n * The last two are Windows rules that no macOS or Linux build can discover for itself — which is exactly\n * why they belong in a check rather than in a caveat.\n */\nfunction isStorableSegment(segment: string): boolean {\n if (segment === '' || segment === '.' || segment === '..') return false;\n if (RESERVED_DEVICE_NAME.test(segment) || TRAILING_STRIPPED.test(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 {@link isStorableSegment} refuses as unportable.\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 */\nasync 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 html: { file: 'index.html' },\n flight: { file: 'index.rsc' },\n} as const satisfies Record<PrerenderVariant, { file: string }>;\n\n/**\n * How a build-time document render tells the prerender pass that the app minted a CSP nonce for its path.\n *\n * The pass cannot see the request it made: it renders through the app's own middleware inside the server\n * bundle, in a module graph of its own, and keeps the body alone. What it needs to know is whether a\n * *request* for this path will mint a nonce, because a document that will is one the framework re-renders per\n * request — so the file the pass is about to write is one no deployment will read, and saying \"prerendered\"\n * about it is a lie by omission.\n *\n * Set only while `RSHONO_PRERENDER` is in the environment, which is `rshono build`'s own process, so it never\n * reaches a deployed response; and never stored, since only the body is.\n */\nexport const PRERENDER_NONCE_HEADER = 'x-rshono-prerender-nonce';\n\n/**\n * Process-local bridge from the server bundle's build-time HTML render to the SSG writer.\n *\n * The CLI and the app bundle are separate module graphs, so a module-local symbol would not be shared between\n * them. `Symbol.for` keeps this internal contract available to both graphs without putting the Flight bytes in\n * an HTTP header or changing the public response body. It is attached to the response stream because Hono may\n * copy the `Response` object while it finalizes a handler result.\n */\nconst PRERENDER_FLIGHT: unique symbol = Symbol.for('rshono.prerender.flight-stream');\n\ntype PrerenderResponseBody = ReadableStream<Uint8Array> & {\n [PRERENDER_FLIGHT]?: Promise<Uint8Array>;\n};\n\nexport function setPrerenderFlight(response: Response, flight: Promise<Uint8Array>): void {\n if (response.body) Object.defineProperty(response.body, PRERENDER_FLIGHT, { value: flight });\n}\n\nexport function getPrerenderFlight(response: Response): Promise<Uint8Array> | undefined {\n return (response.body as PrerenderResponseBody | null)?.[PRERENDER_FLIGHT];\n}\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 * The DOS device names Windows still reserves, with or without an extension and whatever the case — so\n * `con`, `CON` and `Con.txt` are all the console. `mkdir` on one fails with `EINVAL`, naming a path the\n * author never wrote, which is why they are refused here with the rest.\n *\n * The list is Microsoft's, superscripts included: `CON PRN AUX NUL`, `COM0`–`COM9`, `COM¹ COM² COM³`, and\n * the same three suffixes for `LPT`. A documentation slug of `con` is a plausible thing to write and\n * builds fine on Linux today; refusing it everywhere is the same trade `:` and `*` already make — portable\n * or not at all, rather than a build that works on the author's machine and fails in CI on Windows.\n */\nconst RESERVED_DEVICE_NAME = /^(?:con|prn|aux|nul|com[0-9¹²³]|lpt[0-9¹²³])(?:\\..*)?$/i;\n\n/**\n * A trailing `.` or space, which Win32 **strips** rather than refusing: `docs/x.` is stored as `docs/x`, so\n * the page is written where no reader will look for it — \"a page the build reports as prerendered and\n * nothing ever serves\", which is the failure {@link ssgFilePath} exists to prevent. The silent half of this\n * rule, and the more important one: the reserved names above at least fail loudly.\n */\nconst TRAILING_STRIPPED = /[. ]$/;\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, on every host rather than on this\n * one: not empty, not a relative-path marker, made only of characters a file name can hold\n * ({@link UNPORTABLE_SEGMENT}, plus the control characters, which a percent-escape is free to carry into a\n * path and no filesystem wants in a name), not a {@link RESERVED_DEVICE_NAME}, and not ending in a\n * character Win32 would {@link TRAILING_STRIPPED | strip}.\n *\n * The last two are Windows rules that no macOS or Linux build can discover for itself — which is exactly\n * why they belong in a check rather than in a caveat.\n */\nfunction isStorableSegment(segment: string): boolean {\n if (segment === '' || segment === '.' || segment === '..') return false;\n if (RESERVED_DEVICE_NAME.test(segment) || TRAILING_STRIPPED.test(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 {@link isStorableSegment} refuses as unportable.\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 */\nasync 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"]}
@@ -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, PRERENDER_NONCE_HEADER, ssgFilePath, SSG_MANIFEST_FILE, toPrerenderedPage, VARIANTS, } from './prerendered.js';
5
+ import { createPageCache, getPrerenderFlight, PRERENDER_NONCE_HEADER, 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.
@@ -140,18 +140,41 @@ async function mapBounded(items, limit, run) {
140
140
  await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
141
141
  return results;
142
142
  }
143
- async function renderVariant(fetch, url, variant) {
144
- const response = await fetch(new Request(url, { headers: VARIANTS[variant].headers }));
145
- if (response.status !== 200)
143
+ /** A missing or failed capture degrades only soft navigation; the document remains a valid prerender. */
144
+ async function renderSharedFlight(capture) {
145
+ if (!capture)
146
+ return { ok: false, reason: 'the shared Flight payload was unavailable' };
147
+ try {
148
+ return { ok: true, body: await capture };
149
+ }
150
+ catch (error) {
151
+ const reason = error instanceof Error && error.message ? `the shared Flight capture failed: ${error.message}` : 'the shared Flight capture failed';
152
+ return { ok: false, reason };
153
+ }
154
+ }
155
+ /**
156
+ * Makes the one build-time request for a concrete path and keeps the shared Flight branch with its document.
157
+ *
158
+ * The response body is still HTML; the optional branch is only consumed by the SSG writer. A missing or
159
+ * rejected branch therefore degrades Flight to a request-time render without discarding the document.
160
+ */
161
+ async function renderDocument(fetch, url) {
162
+ const response = await fetch(new Request(url));
163
+ const flight = getPrerenderFlight(response);
164
+ if (response.status !== 200) {
165
+ void response.body?.cancel().catch(() => { });
146
166
  return { ok: false, reason: `${response.status}`, failed: response.status >= 500 };
147
- if (!(response.headers.get('Content-Type') ?? '').includes(VARIANTS[variant].contentType)) {
148
- return { ok: false, reason: `a non-${VARIANTS[variant].contentType} response`, failed: false };
149
167
  }
150
- // Bytes, not `response.text()`. That is a *non-fatal* UTF-8 decode, so every byte of a binary row in a
151
- // flight payload — `emitChunk` puts raw typed-array bytes on the wire — becomes U+FFFD, and writing the
152
- // string back out spends three real bytes on each one. The page is then prerendered and unparseable.
153
- // The header is the framework saying this render minted a CSP nonce; on a flight payload it is never set.
154
- return { ok: true, body: new Uint8Array(await response.arrayBuffer()), nonced: response.headers.get(PRERENDER_NONCE_HEADER) !== null };
168
+ if (!(response.headers.get('Content-Type') ?? '').includes('text/html')) {
169
+ void response.body?.cancel().catch(() => { });
170
+ return { ok: false, reason: 'a non-text/html response', failed: false };
171
+ }
172
+ return {
173
+ ok: true,
174
+ body: new Uint8Array(await response.arrayBuffer()),
175
+ nonced: response.headers.get(PRERENDER_NONCE_HEADER) !== null,
176
+ flight,
177
+ };
155
178
  }
156
179
  export async function prerenderStaticRoutes(options) {
157
180
  const { routes, fetch, ssgDir } = options;
@@ -224,7 +247,7 @@ export async function prerenderStaticRoutes(options) {
224
247
  const outcomes = await mapBounded(targets, RENDER_CONCURRENCY, async ({ path, relPath }) => {
225
248
  /** Buffered, so a concurrent pass logs in the same order a serial one did. */
226
249
  const warnings = [];
227
- const document = await renderVariant(fetch, origin + path, 'html');
250
+ const document = await renderDocument(fetch, origin + path);
228
251
  if (!document.ok) {
229
252
  // A 5xx fails the build instead of being warned about — see {@link RenderedVariant}.
230
253
  if (!document.failed)
@@ -242,16 +265,15 @@ export async function prerenderStaticRoutes(options) {
242
265
  wrote.push(ssgFilePath(path, variant));
243
266
  };
244
267
  write('html', document.body);
245
- // The soft-navigation representation of the same page. Best-effort: the document is valid on its own, and
246
- // serving falls back to rendering the flight payload per request.
247
- const flight = await renderVariant(fetch, origin + path, 'flight');
268
+ // The inline Flight reader captured these bytes from the same RSC evaluation. Best-effort: the document
269
+ // remains valid if capture fails, and soft navigation then renders Flight per request.
270
+ const flight = await renderSharedFlight(document.flight);
248
271
  if (flight.ok) {
249
272
  write('flight', flight.body);
250
273
  }
251
274
  else {
252
- // Not fatal the way a failed *document* is, even for a 5xx: the page is on disk and serves, and only
253
- // soft navigation to it degrades to rendering per request. The reason is named because the two cases
254
- // want different things done about them.
275
+ // Not fatal: the page is on disk and serves, and only soft navigation to it degrades to rendering per
276
+ // request. The reason is named because a missing shared branch is different from a failed document.
255
277
  warnings.push(` ⚠ "${path}" produced no flight payload (${flight.reason}) — soft navigations to it will render per request.`);
256
278
  }
257
279
  return { path, ok: true, files: wrote, warnings, failure: undefined, nonced: document.nonced };
@@ -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 PRERENDER_NONCE_HEADER,\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 * The subset of {@link written} whose *document* the deployment will not serve, because the app mints a CSP\n * nonce on that path — see {@link PRERENDER_NONCE_HEADER}. Their flight payloads are served from disk like\n * any other, so these are half-prerendered pages rather than skipped ones, and the caller says so.\n */\n flightOnly: 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 *\n * `failed` splits the second case in two. A 404, a 3xx or a payload of the wrong type is a page this pass\n * cannot *store*, and it still serves per request — the skip is honest. A 5xx is the app throwing, and this\n * pass renders a page exactly as a request does, so per request it will throw again: skipping it prints\n * \"will SSR per request\" over a route that will 500 forever, and exits 0.\n */\ntype RenderedVariant = { ok: true; body: Uint8Array; nonced: boolean } | { ok: false; reason: string; failed: boolean };\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}`, failed: response.status >= 500 };\n if (!(response.headers.get('Content-Type') ?? '').includes(VARIANTS[variant].contentType)) {\n return { ok: false, reason: `a non-${VARIANTS[variant].contentType} response`, failed: false };\n }\n // Bytes, not `response.text()`. That is a *non-fatal* UTF-8 decode, so every byte of a binary row in a\n // flight payload — `emitChunk` puts raw typed-array bytes on the wire — becomes U+FFFD, and writing the\n // string back out spends three real bytes on each one. The page is then prerendered and unparseable.\n // The header is the framework saying this render minted a CSP nonce; on a flight payload it is never set.\n return { ok: true, body: new Uint8Array(await response.arrayBuffer()), nonced: response.headers.get(PRERENDER_NONCE_HEADER) !== null };\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 /** Pages whose document was written and will never be read — see {@link PrerenderResult.flightOnly}. */\n const flightOnly: 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 `[rshono] Cannot prerender \"${path}\" for route \"${route.path}\": a path segment is \".\" or \"..\", or cannot be ` +\n 'one portable file name. It must not hold \\\\ / : * ? \" < > | or a control character, must not end in ' +\n '\".\" or a space (Windows stores the name without it, so the page lands where nothing looks for it), ' +\n 'and must not be a reserved device name — CON, PRN, AUX, NUL, COM0-9 or LPT0-9, with or without an extension.',\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 // A 5xx fails the build instead of being warned about — see {@link RenderedVariant}.\n if (!document.failed) warnings.push(` ⚠ \"${path}\" rendered ${document.reason} at build time — skipping, will SSR per request.`);\n return { path, ok: false as const, files: [], warnings, failure: document.failed ? document.reason : undefined, nonced: false };\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: Uint8Array) => {\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 // Not fatal the way a failed *document* is, even for a 5xx: the page is on disk and serves, and only\n // soft navigation to it degrades to rendering per request. The reason is named because the two cases\n // want different things done about them.\n warnings.push(` ⚠ \"${path}\" produced no flight payload (${flight.reason}) — soft navigations to it will render per request.`);\n }\n\n return { path, ok: true as const, files: wrote, warnings, failure: undefined, nonced: document.nonced };\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 const failures: string[] = [];\n for (const outcome of outcomes) {\n for (const warning of outcome.warnings) console.warn(warning);\n if (outcome.failure !== undefined) failures.push(`\"${outcome.path}\" rendered ${outcome.failure}`);\n if (!outcome.ok) {\n skipped.push(outcome.path);\n continue;\n }\n files.push(...outcome.files);\n written.push(outcome.path);\n if (outcome.nonced) flightOnly.push(outcome.path);\n }\n\n // Raised together, and in target order, so which of several failing pages is reported does not depend on\n // which render finished first. The render's own error is already in the build log above: the framework's\n // `onError` reported it when it turned the throw into the 500 counted here.\n if (failures.length > 0) {\n throw new Error(\n `[rshono] ${failures.length} page${failures.length === 1 ? '' : 's'} failed to render while prerendering:\\n` +\n failures.map((failure) => ` • ${failure}`).join('\\n') +\n '\\n Prerendering renders a page the way a request does, so this is not a build-only failure — the route' +\n '\\n would answer the same status per request. The error it threw is logged above.',\n );\n }\n\n // After the failure check, so a build that is about to stop does not spend three lines on advice. Once\n // rather than per page: under a global nonce policy this is every static page in the app, and the same\n // sentence repeated a hundred times says no more than one does — the caller's summary line is what names\n // them, marked there.\n //\n // The documents are written all the same. Whether a nonce is minted is a request-time decision, and an\n // app is free to switch its policy on from the environment, so a build that saw one is not proof the\n // deployment has one — dropping the file would cost a page its prerender on the strength of a guess.\n if (flightOnly.length > 0) {\n console.warn(\n ` ⚠ ${flightOnly.length} page(s) mint a CSP nonce, so the framework re-renders their document per request:\\n` +\n ` only the flight payload is served from disk. The documents are written anyway, in case the policy is\\n` +\n ` off where this is deployed. Marked \"flight only\" in the summary below.`,\n );\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, flightOnly };\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 getPrerenderFlight,\n PRERENDER_NONCE_HEADER,\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 * The subset of {@link written} whose *document* the deployment will not serve, because the app mints a CSP\n * nonce on that path — see {@link PRERENDER_NONCE_HEADER}. Their flight payloads are served from disk like\n * any other, so these are half-prerendered pages rather than skipped ones, and the caller says so.\n */\n flightOnly: 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. `failed` splits the failure case in\n * two: a 404, a 3xx or a payload of the wrong type is a page this pass cannot store, and it still serves per\n * request — the skip is honest. A 5xx is the app throwing, and this pass renders a page exactly as a request\n * does, so per request it will throw again: skipping it prints \"will SSR per request\" over a route that will\n * 500 forever, and exits 0.\n */\ntype RenderedVariant = { ok: true; body: Uint8Array; nonced: boolean; flight?: Promise<Uint8Array> } | { ok: false; reason: string; failed: boolean };\n\n/** A missing or failed capture degrades only soft navigation; the document remains a valid prerender. */\nasync function renderSharedFlight(capture: Promise<Uint8Array> | undefined): Promise<{ ok: true; body: Uint8Array } | { ok: false; reason: string }> {\n if (!capture) return { ok: false, reason: 'the shared Flight payload was unavailable' };\n try {\n return { ok: true, body: await capture };\n } catch (error) {\n const reason =\n error instanceof Error && error.message ? `the shared Flight capture failed: ${error.message}` : 'the shared Flight capture failed';\n return { ok: false, reason };\n }\n}\n\n/**\n * Makes the one build-time request for a concrete path and keeps the shared Flight branch with its document.\n *\n * The response body is still HTML; the optional branch is only consumed by the SSG writer. A missing or\n * rejected branch therefore degrades Flight to a request-time render without discarding the document.\n */\nasync function renderDocument(fetch: PrerenderOptions['fetch'], url: string): Promise<RenderedVariant> {\n const response = await fetch(new Request(url));\n const flight = getPrerenderFlight(response);\n if (response.status !== 200) {\n void response.body?.cancel().catch(() => {});\n return { ok: false, reason: `${response.status}`, failed: response.status >= 500 };\n }\n if (!(response.headers.get('Content-Type') ?? '').includes('text/html')) {\n void response.body?.cancel().catch(() => {});\n return { ok: false, reason: 'a non-text/html response', failed: false };\n }\n return {\n ok: true,\n body: new Uint8Array(await response.arrayBuffer()),\n nonced: response.headers.get(PRERENDER_NONCE_HEADER) !== null,\n flight,\n };\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 /** Pages whose document was written and will never be read — see {@link PrerenderResult.flightOnly}. */\n const flightOnly: 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 `[rshono] Cannot prerender \"${path}\" for route \"${route.path}\": a path segment is \".\" or \"..\", or cannot be ` +\n 'one portable file name. It must not hold \\\\ / : * ? \" < > | or a control character, must not end in ' +\n '\".\" or a space (Windows stores the name without it, so the page lands where nothing looks for it), ' +\n 'and must not be a reserved device name — CON, PRN, AUX, NUL, COM0-9 or LPT0-9, with or without an extension.',\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 renderDocument(fetch, origin + path);\n if (!document.ok) {\n // A 5xx fails the build instead of being warned about — see {@link RenderedVariant}.\n if (!document.failed) warnings.push(` ⚠ \"${path}\" rendered ${document.reason} at build time — skipping, will SSR per request.`);\n return { path, ok: false as const, files: [], warnings, failure: document.failed ? document.reason : undefined, nonced: false };\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: Uint8Array) => {\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 inline Flight reader captured these bytes from the same RSC evaluation. Best-effort: the document\n // remains valid if capture fails, and soft navigation then renders Flight per request.\n const flight = await renderSharedFlight(document.flight);\n if (flight.ok) {\n write('flight', flight.body);\n } else {\n // Not fatal: the page is on disk and serves, and only soft navigation to it degrades to rendering per\n // request. The reason is named because a missing shared branch is different from a failed document.\n warnings.push(` ⚠ \"${path}\" produced no flight payload (${flight.reason}) — soft navigations to it will render per request.`);\n }\n\n return { path, ok: true as const, files: wrote, warnings, failure: undefined, nonced: document.nonced };\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 const failures: string[] = [];\n for (const outcome of outcomes) {\n for (const warning of outcome.warnings) console.warn(warning);\n if (outcome.failure !== undefined) failures.push(`\"${outcome.path}\" rendered ${outcome.failure}`);\n if (!outcome.ok) {\n skipped.push(outcome.path);\n continue;\n }\n files.push(...outcome.files);\n written.push(outcome.path);\n if (outcome.nonced) flightOnly.push(outcome.path);\n }\n\n // Raised together, and in target order, so which of several failing pages is reported does not depend on\n // which render finished first. The render's own error is already in the build log above: the framework's\n // `onError` reported it when it turned the throw into the 500 counted here.\n if (failures.length > 0) {\n throw new Error(\n `[rshono] ${failures.length} page${failures.length === 1 ? '' : 's'} failed to render while prerendering:\\n` +\n failures.map((failure) => ` • ${failure}`).join('\\n') +\n '\\n Prerendering renders a page the way a request does, so this is not a build-only failure — the route' +\n '\\n would answer the same status per request. The error it threw is logged above.',\n );\n }\n\n // After the failure check, so a build that is about to stop does not spend three lines on advice. Once\n // rather than per page: under a global nonce policy this is every static page in the app, and the same\n // sentence repeated a hundred times says no more than one does — the caller's summary line is what names\n // them, marked there.\n //\n // The documents are written all the same. Whether a nonce is minted is a request-time decision, and an\n // app is free to switch its policy on from the environment, so a build that saw one is not proof the\n // deployment has one — dropping the file would cost a page its prerender on the strength of a guess.\n if (flightOnly.length > 0) {\n console.warn(\n ` ⚠ ${flightOnly.length} page(s) mint a CSP nonce, so the framework re-renders their document per request:\\n` +\n ` only the flight payload is served from disk. The documents are written anyway, in case the policy is\\n` +\n ` off where this is deployed. Marked \"flight only\" in the summary below.`,\n );\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, flightOnly };\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rshono/core",
3
- "version": "1.0.0-rc.22",
3
+ "version": "1.0.0-rc.24",
4
4
  "description": "Minimalist web framework — Hono + Rspack + React Server Components",
5
5
  "author": "Lasse <lasse@lassetange.com> (https://www.lassetange.com)",
6
6
  "license": "MIT",
@@ -52,17 +52,17 @@
52
52
  },
53
53
  "dependencies": {
54
54
  "@hono/node-server": "^2.1.1",
55
- "@rspack/core": "2.2.6",
55
+ "@rspack/core": "2.2.7",
56
56
  "@rspack/plugin-react-refresh": "2.0.2",
57
57
  "react-refresh": "0.18.0",
58
58
  "react-server-dom-rspack": "0.1.0"
59
59
  },
60
60
  "devDependencies": {
61
61
  "@playwright/test": "^1.63.0",
62
- "@types/node": "^26.5.1",
62
+ "@types/node": "^26.6.3",
63
63
  "@types/react": "^19.2.18",
64
64
  "@types/react-dom": "^19.2.5",
65
- "hono": "^4.13.7",
65
+ "hono": "^4.13.10",
66
66
  "react": "19.2.8",
67
67
  "react-dom": "19.2.8",
68
68
  "typescript": "^7.0.2"