@rshono/core 1.0.0-rc.21 → 1.0.0-rc.23
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 +5 -2
- package/dist/runtime/entry.client.js +330 -46
- package/dist/runtime/entry.client.js.map +1 -1
- package/dist/runtime/entry.rsc.js +17 -5
- package/dist/runtime/entry.rsc.js.map +1 -1
- package/dist/runtime/entry.ssr.d.ts +11 -12
- package/dist/runtime/entry.ssr.js +18 -11
- package/dist/runtime/entry.ssr.js.map +1 -1
- package/dist/runtime/flight-inject.d.ts +4 -1
- package/dist/runtime/flight-inject.js +70 -20
- package/dist/runtime/flight-inject.js.map +1 -1
- package/dist/server/prerendered.d.ts +7 -11
- package/dist/server/prerendered.js +23 -9
- package/dist/server/prerendered.js.map +1 -1
- package/dist/server/ssg.js +40 -18
- package/dist/server/ssg.js.map +1 -1
- package/package.json +2 -2
|
@@ -79,8 +79,51 @@ function trailerPrefixLength(buffer, length) {
|
|
|
79
79
|
* where an escaped garbage nonce would be a silent one.
|
|
80
80
|
*/
|
|
81
81
|
const NONCE_CHARS = /^[A-Za-z0-9+/=_-]+$/;
|
|
82
|
+
function createFlightCapture() {
|
|
83
|
+
const { promise, resolve, reject } = Promise.withResolvers();
|
|
84
|
+
// A render can be abandoned before the SSG pass receives this promise. Observing it here leaves the
|
|
85
|
+
// original promise rejected for a later await without risking an unhandled rejection in that gap.
|
|
86
|
+
void promise.catch(() => { });
|
|
87
|
+
let chunks = [];
|
|
88
|
+
let byteLength = 0;
|
|
89
|
+
const fail = (reason) => {
|
|
90
|
+
if (!chunks)
|
|
91
|
+
return;
|
|
92
|
+
chunks = undefined;
|
|
93
|
+
reject(reason ?? new Error('Flight capture was cancelled'));
|
|
94
|
+
};
|
|
95
|
+
return {
|
|
96
|
+
promise,
|
|
97
|
+
push(chunk) {
|
|
98
|
+
if (!chunks)
|
|
99
|
+
return;
|
|
100
|
+
chunks.push(chunk);
|
|
101
|
+
byteLength += chunk.byteLength;
|
|
102
|
+
},
|
|
103
|
+
complete() {
|
|
104
|
+
const captured = chunks;
|
|
105
|
+
if (!captured)
|
|
106
|
+
return;
|
|
107
|
+
chunks = undefined;
|
|
108
|
+
try {
|
|
109
|
+
const bytes = new Uint8Array(byteLength);
|
|
110
|
+
let offset = 0;
|
|
111
|
+
for (const chunk of captured) {
|
|
112
|
+
bytes.set(chunk, offset);
|
|
113
|
+
offset += chunk.byteLength;
|
|
114
|
+
}
|
|
115
|
+
resolve(bytes);
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
reject(error);
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
fail,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
82
124
|
export function injectFlightPayload(rscStream, options = {}) {
|
|
83
125
|
const { nonce, onDone } = options;
|
|
126
|
+
const capture = options.captureFlight ? createFlightCapture() : undefined;
|
|
84
127
|
const safeNonce = nonce !== undefined && NONCE_CHARS.test(nonce) ? nonce : undefined;
|
|
85
128
|
const scriptOpen = `<script${safeNonce ? ` nonce="${safeNonce}"` : ''}>(self.__FLIGHT_DATA||=[]).push(`;
|
|
86
129
|
const scriptClose = ')</script>';
|
|
@@ -178,6 +221,7 @@ export function injectFlightPayload(rscStream, options = {}) {
|
|
|
178
221
|
carry = null;
|
|
179
222
|
for (const resolve of waiting.splice(0))
|
|
180
223
|
resolve();
|
|
224
|
+
capture?.fail(reason);
|
|
181
225
|
// Otherwise the teed RSC branch keeps being pumped for a response nobody will read, and the tee's
|
|
182
226
|
// other half buffers every chunk waiting for this one to catch up.
|
|
183
227
|
flightReader?.cancel(reason).catch(() => { });
|
|
@@ -256,8 +300,10 @@ export function injectFlightPayload(rscStream, options = {}) {
|
|
|
256
300
|
if (cancelled)
|
|
257
301
|
return;
|
|
258
302
|
const { done, value } = await reader.read();
|
|
259
|
-
if (done)
|
|
303
|
+
if (done) {
|
|
304
|
+
capture?.complete();
|
|
260
305
|
break;
|
|
306
|
+
}
|
|
261
307
|
// Only the decode is guarded: a `push` inside the same `try` would answer a dead controller by
|
|
262
308
|
// re-encoding the chunk and enqueueing it again.
|
|
263
309
|
let literal;
|
|
@@ -273,9 +319,11 @@ export function injectFlightPayload(rscStream, options = {}) {
|
|
|
273
319
|
// than whenever the payload would have ended on its own.
|
|
274
320
|
try {
|
|
275
321
|
await push(literal);
|
|
322
|
+
capture?.push(value);
|
|
276
323
|
}
|
|
277
|
-
catch {
|
|
324
|
+
catch (error) {
|
|
278
325
|
cancelled = true;
|
|
326
|
+
capture?.fail(error);
|
|
279
327
|
reader.cancel().catch(() => { });
|
|
280
328
|
return;
|
|
281
329
|
}
|
|
@@ -283,6 +331,21 @@ export function injectFlightPayload(rscStream, options = {}) {
|
|
|
283
331
|
// No end-of-stream flush: a non-streaming decoder holds nothing between calls, so there is nothing left
|
|
284
332
|
// to emit — and the flush this replaces could throw, erroring the response mid-document. See above.
|
|
285
333
|
}
|
|
334
|
+
function startFlight(controller) {
|
|
335
|
+
if (startedFlight)
|
|
336
|
+
return;
|
|
337
|
+
startedFlight = true;
|
|
338
|
+
if (cancelled) {
|
|
339
|
+
flightDone();
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
void writeFlight(controller)
|
|
343
|
+
.catch((error) => {
|
|
344
|
+
capture?.fail(error);
|
|
345
|
+
controller.error(error);
|
|
346
|
+
})
|
|
347
|
+
.then(flightDone);
|
|
348
|
+
}
|
|
286
349
|
const transformer = {
|
|
287
350
|
async transform(chunk, controller) {
|
|
288
351
|
if (boundary) {
|
|
@@ -311,14 +374,8 @@ export function injectFlightPayload(rscStream, options = {}) {
|
|
|
311
374
|
teardown(error);
|
|
312
375
|
return;
|
|
313
376
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
// Deliberately not awaited: this runs inside a scheduled callback with nothing to return to, and
|
|
317
|
-
// the chain already routes a write failure to `controller.error` before settling `flightDone`.
|
|
318
|
-
void writeFlight(controller)
|
|
319
|
-
.catch((error) => controller.error(error))
|
|
320
|
-
.then(flightDone);
|
|
321
|
-
}
|
|
377
|
+
// Deliberately not awaited: this runs inside a scheduled callback with nothing to return to.
|
|
378
|
+
startFlight(controller);
|
|
322
379
|
});
|
|
323
380
|
},
|
|
324
381
|
async flush(controller) {
|
|
@@ -333,15 +390,7 @@ export function injectFlightPayload(rscStream, options = {}) {
|
|
|
333
390
|
// all — leaves nothing to have started the payload, and `flightDone` is only ever called from that
|
|
334
391
|
// chain or from `cancel`. Without this the await below parks on a promise nothing will settle and the
|
|
335
392
|
// response never ends. `cancelled` short-circuits it: there is nowhere to write the payload to.
|
|
336
|
-
|
|
337
|
-
startedFlight = true;
|
|
338
|
-
if (cancelled)
|
|
339
|
-
flightDone();
|
|
340
|
-
else
|
|
341
|
-
void writeFlight(controller)
|
|
342
|
-
.catch((error) => controller.error(error))
|
|
343
|
-
.then(flightDone);
|
|
344
|
-
}
|
|
393
|
+
startFlight(controller);
|
|
345
394
|
}
|
|
346
395
|
catch (error) {
|
|
347
396
|
// The consumer went away while the batch was being emitted, so there is nothing left to write to and
|
|
@@ -398,6 +447,7 @@ export function injectFlightPayload(rscStream, options = {}) {
|
|
|
398
447
|
return innerReader.cancel(reason);
|
|
399
448
|
},
|
|
400
449
|
});
|
|
401
|
-
|
|
450
|
+
const pair = { readable, writable: inner.writable };
|
|
451
|
+
return capture ? { ...pair, capturedFlight: capture.promise } : pair;
|
|
402
452
|
}
|
|
403
453
|
//# sourceMappingURL=flight-inject.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"flight-inject.js","sourceRoot":"","sources":["../../src/runtime/flight-inject.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Carrying the flight payload inside the HTML document, so the browser hydrates from bytes it already has\n * rather than fetching the page twice. The reader is `readFlightPayload` in `entry.client.tsx`.\n *\n * The wire format is `rsc-html-stream`'s — a run of `<script>(self.__FLIGHT_DATA||=[]).push(\"…\")</script>`\n * tags — but the implementation is first-party, because that package tests each HTML chunk for the\n * document trailer with `endsWith`: React's byte writer splits any write straddling its 2 kB views, so\n * `</body></html>` can arrive as two chunks and the document ends up with two trailers.\n */\n\n/**\n * {@link Transformer} plus the `cancel` hook the Streams standard added for a cancelled readable side —\n * declared here because the bundled lib types have not caught up with what Node calls.\n *\n * It matters because `cancel` is the only notification that a response ended *without* finishing, and both\n * users of it release a listener that would otherwise outlive the request.\n */\nexport type CancellableTransformer<I, O> = Transformer<I, O> & { cancel?: (reason?: unknown) => void };\n\nconst encoder = new TextEncoder();\n\n/** What React closes an `<html>` document with, and what this module re-emits after the last payload script. */\nconst TRAILER = '</body></html>';\nconst TRAILER_BYTES = encoder.encode(TRAILER);\n\n/**\n * A macrotask boundary: `setImmediate` where there is one, which is the current turn's check phase rather\n * than a timer, and `setTimeout` as the portable fallback. A Node timer has a 1ms floor that every HTML\n * flush would pay — 4.7ms → 3.0ms time-to-last-byte on a 30 kB payload.\n */\ntype TaskHandle = ReturnType<typeof setTimeout> | ReturnType<typeof setImmediate>;\nconst hasSetImmediate = typeof setImmediate === 'function';\nconst schedule: (fn: () => void) => TaskHandle = hasSetImmediate ? setImmediate : (fn) => setTimeout(fn, 0);\nconst unschedule = (handle: TaskHandle): void => {\n if (hasSetImmediate) clearImmediate(handle as ReturnType<typeof setImmediate>);\n else clearTimeout(handle as ReturnType<typeof setTimeout>);\n};\n\n/**\n * Escapes the two sequences that would end a `<script>` element early. Guarded by `includes('<')`, which is\n * far cheaper than two regex passes over 30 kB of payload that usually has no `<` in it. `</script` becomes\n * `</\\script`, not `<\\/script`, which would break the valid JS `0</script/`.\n */\nfunction escapeScript(script: string): string {\n return script.includes('<') ? script.replace(/<!--/g, '<\\\\!--').replace(/<\\/(script)/gi, '</\\\\$1') : script;\n}\n\n/**\n * The bytes of `chunk` as a latin1 string, which is the form `btoa` takes. Sliced because\n * `String.fromCharCode(...chunk)` passes one argument per byte and overflows the stack.\n */\nfunction latin1(chunk: Uint8Array): string {\n let out = '';\n for (let at = 0; at < chunk.length; at += 8192) out += String.fromCharCode(...chunk.subarray(at, at + 8192));\n return out;\n}\n\n/** Whether `buffer`'s first `length` bytes end with the document trailer. */\nfunction endsWithTrailer(buffer: Uint8Array, length: number): boolean {\n if (length < TRAILER_BYTES.length) return false;\n const from = length - TRAILER_BYTES.length;\n for (let i = 0; i < TRAILER_BYTES.length; i++) {\n if (buffer[from + i] !== TRAILER_BYTES[i]) return false;\n }\n return true;\n}\n\n/**\n * How many of `buffer`'s last bytes could still turn into the document trailer — the length of the longest\n * suffix of `buffer[0..length)` that is a prefix of it, `TRAILER_BYTES.length` for the whole thing.\n *\n * At most 14 bytes are ever held back, and in practice 0: a React flush ends with a closed tag, not with the\n * start of one. Cheaper than it looks, too — the first comparison rejects every suffix whose first byte is\n * not `<`.\n */\nfunction trailerPrefixLength(buffer: Uint8Array, length: number): number {\n candidate: for (let take = Math.min(length, TRAILER_BYTES.length); take > 0; take--) {\n const from = length - take;\n for (let i = 0; i < take; i++) if (buffer[from + i] !== TRAILER_BYTES[i]) continue candidate;\n return take;\n }\n return 0;\n}\n\n/**\n * The characters a CSP nonce may be made of: base64 and base64url, which is what every generator of one emits\n * — Hono's `secureHeaders()`, the only source the framework reads, produces base64 of 16 random bytes.\n *\n * The tag below is built by hand rather than by React, so this is the one attribute value in a rendered\n * document that nothing else escapes. The value is not attacker-controlled today, but the framework does not\n * own where it comes from: `c.get('secureHeadersNonce')` is an ordinary context variable that any middleware\n * can set, and a nonce carrying a `\"` would close the attribute and open a script-injection point in the\n * document. Anything outside this set is dropped rather than escaped, because a value made of other characters\n * is not a nonce at all: a page whose payload scripts the policy then refuses is the visible failure to have,\n * where an escaped garbage nonce would be a silent one.\n */\nconst NONCE_CHARS = /^[A-Za-z0-9+/=_-]+$/;\n\nexport function injectFlightPayload(\n rscStream: ReadableStream<Uint8Array>,\n options: { nonce?: string; onDone?: () => void } = {},\n): ReadableWritablePair<Uint8Array, Uint8Array> {\n const { nonce, onDone } = options;\n const safeNonce = nonce !== undefined && NONCE_CHARS.test(nonce) ? nonce : undefined;\n const scriptOpen = `<script${safeNonce ? ` nonce=\"${safeNonce}\"` : ''}>(self.__FLIGHT_DATA||=[]).push(`;\n const scriptClose = ')</script>';\n\n const { promise: flightWritten, resolve: flightDone } = Promise.withResolvers<void>();\n let startedFlight = false;\n\n const batch: Uint8Array[] = [];\n let boundary: TaskHandle | null = null;\n /**\n * The tail of the last batch that could still be the start of the document trailer, carried into the next\n * one. See {@link emitBatch}.\n */\n let carry: Uint8Array | null = null;\n\n /**\n * Set once the consumer has gone away, so nothing tries to enqueue into a readable that cannot take it.\n *\n * The `cancel` hook alone cannot set this: per the Streams standard, cancelling the readable after the\n * close algorithm has started skips the transformer's `cancel` entirely — and `flush` awaiting the whole\n * payload is precisely that window. So a failed enqueue counts as the signal too.\n */\n let cancelled = false;\n /** Held so {@link cancelled} can release the teed RSC branch rather than leaving it to be pumped. */\n let flightReader: ReadableStreamDefaultReader<Uint8Array> | null = null;\n\n /**\n * One permit per chunk the consumer has asked for: the backpressure a `TransformStream` alone does not give\n * this module.\n *\n * A transform's writable side parks a write while its readable's queue is over the mark, which covers the\n * HTML — but only once something has been enqueued, and `transform` defers that to a macrotask. So a\n * producer that writes a whole document without ever yielding the microtask queue is admitted in full, and\n * {@link writeFlight}, which pumps the payload into the same controller from a detached promise, is never\n * gated at all: measured, a consumer that read one chunk and stalled still pulled all 500 chunks of a test\n * payload into the queue, where this brings it down to 3. `controller.desiredSize` is no help — a transform\n * readable's high-water mark is 0, so it is never positive.\n *\n * The wrapper readable this function returns is the missing signal: its `pull` runs exactly when the\n * consumer wants another chunk, and releases one permit. Both producers — the HTML batcher and the payload\n * pump — take one before they enqueue, so a stalled client parks React instead of filling the process. The\n * Streams standard calls `pull` again only once the previous call has settled, and a call only settles once\n * a chunk has arrived, so permits are handed out one at a time and the queue is bounded at a chunk.\n *\n * What is left buffered is one React flush, because a flush has to leave here as a single chunk (see\n * {@link emitBatch}) — the same bound React itself holds while it builds one. The two enqueues in `flush`\n * are ungated for a related reason: the response is over by then, and parking its last two chunks on a\n * permit would only add a way for it not to end. They are also why the count can reach two at the very\n * end — an ungated enqueue settles a pull without spending its permit — which is harmless, there being\n * nothing left to produce.\n *\n * The cost is one extra stream hop and a microtask per chunk: 3.6ms → 4.0ms to push a 187 kB page through\n * this module with nothing else in the way, which is not the shape of a real response, where the socket\n * dominates by orders of magnitude.\n */\n let permits = 0;\n const waiting: Array<() => void> = [];\n\n /** Returns nothing when a permit was already free, so the common case costs no microtask. */\n function takePermit(): Promise<void> | undefined {\n // Nothing will release another permit once the consumer is gone, and there is nothing left to protect:\n // the caller goes on to a failing enqueue, which is what tears the pipeline down.\n if (cancelled) return;\n if (permits > 0) {\n permits--;\n return;\n }\n return new Promise<void>((resolve) => waiting.push(resolve));\n }\n\n function releasePermit(): void {\n const next = waiting.shift();\n if (next) next();\n else permits++;\n }\n\n /** Fires `onDone` at most once, however the response ended — both cancel paths can reach it. */\n let ended = false;\n function reportDone(): void {\n if (ended) return;\n ended = true;\n onDone?.();\n }\n\n /**\n * The consumer has gone away: stop producing, release everything the request was holding, and unpark both\n * producers so they see {@link cancelled} rather than a permit that will never come.\n *\n * Called from the wrapper readable's `cancel`, which is reliable, and from the transformer's, which the\n * standard skips once the close algorithm has started — so it has to be idempotent.\n */\n function teardown(reason?: unknown): void {\n cancelled = true;\n if (boundary) {\n unschedule(boundary);\n boundary = null;\n }\n batch.length = 0;\n carry = null;\n for (const resolve of waiting.splice(0)) resolve();\n // Otherwise the teed RSC branch keeps being pumped for a response nobody will read, and the tee's\n // other half buffers every chunk waiting for this one to catch up.\n flightReader?.cancel(reason).catch(() => {});\n // Unparks `flush` if it is waiting on a payload that will now never arrive.\n flightDone();\n reportDone();\n }\n\n /**\n * Emits the HTML buffered since the last boundary, holding back the document trailer for `flush` to re-emit\n * after the payload scripts.\n *\n * The batch is joined before the trailer is looked for, so one React split across two views is still found.\n * A trailer split across two *batches* is not a shape React produces — it writes its final flush in one\n * synchronous run — but this injector exists because `rsc-html-stream` made a narrower version of that same\n * assumption and was wrong (see the module header), so anything that could still become a trailer is held\n * back in {@link carry} rather than assumed not to be. Only `final`, the call from `flush`, may emit such a\n * tail: by then there is no next batch for it to complete.\n *\n * A tail that never does complete therefore leaves after the payload scripts rather than before them, which\n * is the deliberate half of the trade: releasing it the moment a script wants to go out is the very bug this\n * guards, because the next batch may be the rest of the trailer. It costs at most 13 bytes of a truncated\n * document arriving late, still inside `<body>`.\n */\n function emitBatch(controller: TransformStreamDefaultController<Uint8Array>, final = false): void {\n boundary = null;\n let total = carry?.byteLength ?? 0;\n for (const chunk of batch) total += chunk.byteLength;\n if (total === 0) {\n batch.length = 0;\n return;\n }\n\n const joined = new Uint8Array(total);\n let at = 0;\n if (carry) {\n joined.set(carry, at);\n at += carry.byteLength;\n carry = null;\n }\n for (const chunk of batch) {\n joined.set(chunk, at);\n at += chunk.byteLength;\n }\n batch.length = 0;\n\n const held = final ? (endsWithTrailer(joined, total) ? TRAILER_BYTES.length : 0) : trailerPrefixLength(joined, total);\n const end = total - held;\n // Copied rather than a `subarray`, which would keep the whole joined flush alive for 14 bytes.\n if (held > 0 && !final) carry = joined.slice(end);\n if (end > 0) controller.enqueue(joined.subarray(0, end));\n }\n\n async function writeFlight(controller: TransformStreamDefaultController<Uint8Array>): Promise<void> {\n const reader = (flightReader = rscStream.getReader());\n // One chunk at a time, deliberately *not* `{ stream: true }`: anything that is not valid UTF-8 on its\n // own — a binary row's bytes, or a chunk that split a multi-byte character — throws, and the catch below\n // re-encodes that whole chunk byte-exactly.\n //\n // Streaming is what makes that fallback wrong. A chunk ending mid-character leaves its lead bytes inside\n // the decoder to be emitted with the next chunk; when the next chunk is a binary row and throws, those\n // bytes are in neither `decode()`'s return nor `value`, and the client reassembles a payload short by\n // them. It also left the end-of-stream flush able to throw from outside any `try`.\n //\n // `ignoreBOM` because a per-chunk decode re-runs the BOM check on every call: without it any chunk whose\n // first three bytes are EF BB BF would silently lose them.\n const decoder = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });\n const push = async (literal: string): Promise<void> => {\n const permit = takePermit();\n if (permit) await permit;\n if (cancelled) return;\n controller.enqueue(encoder.encode(scriptOpen + literal + scriptClose));\n };\n for (;;) {\n if (cancelled) return;\n const { done, value } = await reader.read();\n if (done) break;\n // Only the decode is guarded: a `push` inside the same `try` would answer a dead controller by\n // re-encoding the chunk and enqueueing it again.\n let literal: string;\n try {\n literal = escapeScript(JSON.stringify(decoder.decode(value)));\n } catch {\n literal = `Uint8Array.from(atob(${JSON.stringify(btoa(latin1(value)))}), m => m.codePointAt(0))`;\n }\n if (cancelled) return;\n // A failed enqueue means the consumer is gone: release the RSC branch so `flush` unparks now rather\n // than whenever the payload would have ended on its own.\n try {\n await push(literal);\n } catch {\n cancelled = true;\n reader.cancel().catch(() => {});\n return;\n }\n }\n // No end-of-stream flush: a non-streaming decoder holds nothing between calls, so there is nothing left\n // to emit — and the flush this replaces could throw, erroring the response mid-document. See above.\n }\n\n const transformer: CancellableTransformer<Uint8Array, Uint8Array> = {\n async transform(chunk, controller) {\n if (boundary) {\n batch.push(chunk);\n return;\n }\n // The permit is taken once per *batch*, not once per chunk: React writes a whole flush in one microtask\n // cascade and the flush has to leave here as one chunk (see `emitBatch`), so a per-chunk gate would\n // hand the consumer one chunk per read instead. `transform` is never re-entered concurrently — the\n // writable side serializes it on the promise this returns — so nothing else can claim the batch in\n // between.\n const permit = takePermit();\n if (permit) await permit;\n batch.push(chunk);\n // A macrotask, not a microtask: React writes a whole flush in one synchronous run but `pipeThrough`\n // delivers it one microtask at a time, and a script injected between two chunks lands inside a tag.\n boundary = schedule(() => {\n try {\n emitBatch(controller);\n } catch (error) {\n // A dead consumer, all but always. `teardown` rather than `flightDone` alone: a payload pump parked\n // on a permit has to be unparked too, and the RSC branch it holds released.\n controller.error(error);\n teardown(error);\n return;\n }\n if (!startedFlight) {\n startedFlight = true;\n // Deliberately not awaited: this runs inside a scheduled callback with nothing to return to, and\n // the chain already routes a write failure to `controller.error` before settling `flightDone`.\n void writeFlight(controller)\n .catch((error) => controller.error(error))\n .then(flightDone);\n }\n });\n },\n async flush(controller) {\n // Both of these have to happen *before* the await, and in this order.\n try {\n // Anything still batched — or carried — belongs ahead of the payload scripts, which sit at the end of\n // `<body>`. Called unconditionally: a `carry` outlives its boundary, and an empty batch returns early.\n if (boundary) unschedule(boundary);\n emitBatch(controller, true);\n // A writable side that closed without ever reaching `transform` — an HTML stream with no chunks at\n // all — leaves nothing to have started the payload, and `flightDone` is only ever called from that\n // chain or from `cancel`. Without this the await below parks on a promise nothing will settle and the\n // response never ends. `cancelled` short-circuits it: there is nowhere to write the payload to.\n if (!startedFlight) {\n startedFlight = true;\n if (cancelled) flightDone();\n else\n void writeFlight(controller)\n .catch((error) => controller.error(error))\n .then(flightDone);\n }\n } catch (error) {\n // The consumer went away while the batch was being emitted, so there is nothing left to write to and\n // nothing to wait for. Settled explicitly rather than left dangling, so the payload chain is released.\n teardown(error);\n return;\n }\n\n await flightWritten;\n // That await spans the whole payload, and the consumer can go away inside it — with `cancel` skipped\n // (see `cancelled`), a throwing enqueue is the only signal. Unguarded it rejects `flush`, which\n // nothing owns and which surfaces as an unhandled rejection.\n try {\n if (!cancelled) controller.enqueue(encoder.encode(TRAILER));\n } catch {\n // Nowhere left to put the trailer. A response the client abandoned is not a fault.\n cancelled = true;\n flightReader?.cancel().catch(() => {});\n } finally {\n // A `finally` because `onDone` releases the abort forwarder in `renderComponent`, however this ended.\n reportDone();\n }\n },\n cancel(reason) {\n teardown(reason);\n },\n };\n\n const inner = new TransformStream<Uint8Array, Uint8Array>(transformer);\n const innerReader = inner.readable.getReader();\n /**\n * A pull-driven wrapper around the transform's readable, and the only reason this function does not simply\n * return the transform: `pull` runs once per chunk the consumer takes, which is the demand signal the two\n * producers park on (see {@link takePermit}). It is also the one cancel notification the standard never\n * skips, which is what makes {@link teardown} reliable.\n */\n const readable = new ReadableStream<Uint8Array>({\n async pull(controller) {\n releasePermit();\n const { done, value } = await innerReader.read();\n try {\n if (done) controller.close();\n else controller.enqueue(value);\n } catch {\n // Cancelled while this pull was in flight. `teardown` has already run; there is nobody to hand it to.\n }\n },\n cancel(reason) {\n teardown(reason);\n return innerReader.cancel(reason);\n },\n });\n return { readable, writable: inner.writable };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"flight-inject.js","sourceRoot":"","sources":["../../src/runtime/flight-inject.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Carrying the flight payload inside the HTML document, so the browser hydrates from bytes it already has\n * rather than fetching the page twice. The reader is `readFlightPayload` in `entry.client.tsx`.\n *\n * The wire format is `rsc-html-stream`'s — a run of `<script>(self.__FLIGHT_DATA||=[]).push(\"…\")</script>`\n * tags — but the implementation is first-party, because that package tests each HTML chunk for the\n * document trailer with `endsWith`: React's byte writer splits any write straddling its 2 kB views, so\n * `</body></html>` can arrive as two chunks and the document ends up with two trailers.\n */\n\n/**\n * {@link Transformer} plus the `cancel` hook the Streams standard added for a cancelled readable side —\n * declared here because the bundled lib types have not caught up with what Node calls.\n *\n * It matters because `cancel` is the only notification that a response ended *without* finishing, and both\n * users of it release a listener that would otherwise outlive the request.\n */\nexport type CancellableTransformer<I, O> = Transformer<I, O> & { cancel?: (reason?: unknown) => void };\n\nconst encoder = new TextEncoder();\n\n/** What React closes an `<html>` document with, and what this module re-emits after the last payload script. */\nconst TRAILER = '</body></html>';\nconst TRAILER_BYTES = encoder.encode(TRAILER);\n\n/**\n * A macrotask boundary: `setImmediate` where there is one, which is the current turn's check phase rather\n * than a timer, and `setTimeout` as the portable fallback. A Node timer has a 1ms floor that every HTML\n * flush would pay — 4.7ms → 3.0ms time-to-last-byte on a 30 kB payload.\n */\ntype TaskHandle = ReturnType<typeof setTimeout> | ReturnType<typeof setImmediate>;\nconst hasSetImmediate = typeof setImmediate === 'function';\nconst schedule: (fn: () => void) => TaskHandle = hasSetImmediate ? setImmediate : (fn) => setTimeout(fn, 0);\nconst unschedule = (handle: TaskHandle): void => {\n if (hasSetImmediate) clearImmediate(handle as ReturnType<typeof setImmediate>);\n else clearTimeout(handle as ReturnType<typeof setTimeout>);\n};\n\n/**\n * Escapes the two sequences that would end a `<script>` element early. Guarded by `includes('<')`, which is\n * far cheaper than two regex passes over 30 kB of payload that usually has no `<` in it. `</script` becomes\n * `</\\script`, not `<\\/script`, which would break the valid JS `0</script/`.\n */\nfunction escapeScript(script: string): string {\n return script.includes('<') ? script.replace(/<!--/g, '<\\\\!--').replace(/<\\/(script)/gi, '</\\\\$1') : script;\n}\n\n/**\n * The bytes of `chunk` as a latin1 string, which is the form `btoa` takes. Sliced because\n * `String.fromCharCode(...chunk)` passes one argument per byte and overflows the stack.\n */\nfunction latin1(chunk: Uint8Array): string {\n let out = '';\n for (let at = 0; at < chunk.length; at += 8192) out += String.fromCharCode(...chunk.subarray(at, at + 8192));\n return out;\n}\n\n/** Whether `buffer`'s first `length` bytes end with the document trailer. */\nfunction endsWithTrailer(buffer: Uint8Array, length: number): boolean {\n if (length < TRAILER_BYTES.length) return false;\n const from = length - TRAILER_BYTES.length;\n for (let i = 0; i < TRAILER_BYTES.length; i++) {\n if (buffer[from + i] !== TRAILER_BYTES[i]) return false;\n }\n return true;\n}\n\n/**\n * How many of `buffer`'s last bytes could still turn into the document trailer — the length of the longest\n * suffix of `buffer[0..length)` that is a prefix of it, `TRAILER_BYTES.length` for the whole thing.\n *\n * At most 14 bytes are ever held back, and in practice 0: a React flush ends with a closed tag, not with the\n * start of one. Cheaper than it looks, too — the first comparison rejects every suffix whose first byte is\n * not `<`.\n */\nfunction trailerPrefixLength(buffer: Uint8Array, length: number): number {\n candidate: for (let take = Math.min(length, TRAILER_BYTES.length); take > 0; take--) {\n const from = length - take;\n for (let i = 0; i < take; i++) if (buffer[from + i] !== TRAILER_BYTES[i]) continue candidate;\n return take;\n }\n return 0;\n}\n\n/**\n * The characters a CSP nonce may be made of: base64 and base64url, which is what every generator of one emits\n * — Hono's `secureHeaders()`, the only source the framework reads, produces base64 of 16 random bytes.\n *\n * The tag below is built by hand rather than by React, so this is the one attribute value in a rendered\n * document that nothing else escapes. The value is not attacker-controlled today, but the framework does not\n * own where it comes from: `c.get('secureHeadersNonce')` is an ordinary context variable that any middleware\n * can set, and a nonce carrying a `\"` would close the attribute and open a script-injection point in the\n * document. Anything outside this set is dropped rather than escaped, because a value made of other characters\n * is not a nonce at all: a page whose payload scripts the policy then refuses is the visible failure to have,\n * where an escaped garbage nonce would be a silent one.\n */\nconst NONCE_CHARS = /^[A-Za-z0-9+/=_-]+$/;\n\nfunction createFlightCapture() {\n const { promise, resolve, reject } = Promise.withResolvers<Uint8Array>();\n // A render can be abandoned before the SSG pass receives this promise. Observing it here leaves the\n // original promise rejected for a later await without risking an unhandled rejection in that gap.\n void promise.catch(() => {});\n let chunks: Uint8Array[] | undefined = [];\n let byteLength = 0;\n\n const fail = (reason?: unknown): void => {\n if (!chunks) return;\n chunks = undefined;\n reject(reason ?? new Error('Flight capture was cancelled'));\n };\n\n return {\n promise,\n push(chunk: Uint8Array) {\n if (!chunks) return;\n chunks.push(chunk);\n byteLength += chunk.byteLength;\n },\n complete() {\n const captured = chunks;\n if (!captured) return;\n chunks = undefined;\n try {\n const bytes = new Uint8Array(byteLength);\n let offset = 0;\n for (const chunk of captured) {\n bytes.set(chunk, offset);\n offset += chunk.byteLength;\n }\n resolve(bytes);\n } catch (error) {\n reject(error);\n }\n },\n fail,\n };\n}\n\nexport function injectFlightPayload(\n rscStream: ReadableStream<Uint8Array>,\n options: { nonce?: string; onDone?: () => void; captureFlight?: boolean } = {},\n): ReadableWritablePair<Uint8Array, Uint8Array> & { capturedFlight?: Promise<Uint8Array> } {\n const { nonce, onDone } = options;\n const capture = options.captureFlight ? createFlightCapture() : undefined;\n const safeNonce = nonce !== undefined && NONCE_CHARS.test(nonce) ? nonce : undefined;\n const scriptOpen = `<script${safeNonce ? ` nonce=\"${safeNonce}\"` : ''}>(self.__FLIGHT_DATA||=[]).push(`;\n const scriptClose = ')</script>';\n\n const { promise: flightWritten, resolve: flightDone } = Promise.withResolvers<void>();\n let startedFlight = false;\n\n const batch: Uint8Array[] = [];\n let boundary: TaskHandle | null = null;\n /**\n * The tail of the last batch that could still be the start of the document trailer, carried into the next\n * one. See {@link emitBatch}.\n */\n let carry: Uint8Array | null = null;\n\n /**\n * Set once the consumer has gone away, so nothing tries to enqueue into a readable that cannot take it.\n *\n * The `cancel` hook alone cannot set this: per the Streams standard, cancelling the readable after the\n * close algorithm has started skips the transformer's `cancel` entirely — and `flush` awaiting the whole\n * payload is precisely that window. So a failed enqueue counts as the signal too.\n */\n let cancelled = false;\n /** Held so {@link cancelled} can release the teed RSC branch rather than leaving it to be pumped. */\n let flightReader: ReadableStreamDefaultReader<Uint8Array> | null = null;\n\n /**\n * One permit per chunk the consumer has asked for: the backpressure a `TransformStream` alone does not give\n * this module.\n *\n * A transform's writable side parks a write while its readable's queue is over the mark, which covers the\n * HTML — but only once something has been enqueued, and `transform` defers that to a macrotask. So a\n * producer that writes a whole document without ever yielding the microtask queue is admitted in full, and\n * {@link writeFlight}, which pumps the payload into the same controller from a detached promise, is never\n * gated at all: measured, a consumer that read one chunk and stalled still pulled all 500 chunks of a test\n * payload into the queue, where this brings it down to 3. `controller.desiredSize` is no help — a transform\n * readable's high-water mark is 0, so it is never positive.\n *\n * The wrapper readable this function returns is the missing signal: its `pull` runs exactly when the\n * consumer wants another chunk, and releases one permit. Both producers — the HTML batcher and the payload\n * pump — take one before they enqueue, so a stalled client parks React instead of filling the process. The\n * Streams standard calls `pull` again only once the previous call has settled, and a call only settles once\n * a chunk has arrived, so permits are handed out one at a time and the queue is bounded at a chunk.\n *\n * What is left buffered is one React flush, because a flush has to leave here as a single chunk (see\n * {@link emitBatch}) — the same bound React itself holds while it builds one. The two enqueues in `flush`\n * are ungated for a related reason: the response is over by then, and parking its last two chunks on a\n * permit would only add a way for it not to end. They are also why the count can reach two at the very\n * end — an ungated enqueue settles a pull without spending its permit — which is harmless, there being\n * nothing left to produce.\n *\n * The cost is one extra stream hop and a microtask per chunk: 3.6ms → 4.0ms to push a 187 kB page through\n * this module with nothing else in the way, which is not the shape of a real response, where the socket\n * dominates by orders of magnitude.\n */\n let permits = 0;\n const waiting: Array<() => void> = [];\n\n /** Returns nothing when a permit was already free, so the common case costs no microtask. */\n function takePermit(): Promise<void> | undefined {\n // Nothing will release another permit once the consumer is gone, and there is nothing left to protect:\n // the caller goes on to a failing enqueue, which is what tears the pipeline down.\n if (cancelled) return;\n if (permits > 0) {\n permits--;\n return;\n }\n return new Promise<void>((resolve) => waiting.push(resolve));\n }\n\n function releasePermit(): void {\n const next = waiting.shift();\n if (next) next();\n else permits++;\n }\n\n /** Fires `onDone` at most once, however the response ended — both cancel paths can reach it. */\n let ended = false;\n function reportDone(): void {\n if (ended) return;\n ended = true;\n onDone?.();\n }\n\n /**\n * The consumer has gone away: stop producing, release everything the request was holding, and unpark both\n * producers so they see {@link cancelled} rather than a permit that will never come.\n *\n * Called from the wrapper readable's `cancel`, which is reliable, and from the transformer's, which the\n * standard skips once the close algorithm has started — so it has to be idempotent.\n */\n function teardown(reason?: unknown): void {\n cancelled = true;\n if (boundary) {\n unschedule(boundary);\n boundary = null;\n }\n batch.length = 0;\n carry = null;\n for (const resolve of waiting.splice(0)) resolve();\n capture?.fail(reason);\n // Otherwise the teed RSC branch keeps being pumped for a response nobody will read, and the tee's\n // other half buffers every chunk waiting for this one to catch up.\n flightReader?.cancel(reason).catch(() => {});\n // Unparks `flush` if it is waiting on a payload that will now never arrive.\n flightDone();\n reportDone();\n }\n\n /**\n * Emits the HTML buffered since the last boundary, holding back the document trailer for `flush` to re-emit\n * after the payload scripts.\n *\n * The batch is joined before the trailer is looked for, so one React split across two views is still found.\n * A trailer split across two *batches* is not a shape React produces — it writes its final flush in one\n * synchronous run — but this injector exists because `rsc-html-stream` made a narrower version of that same\n * assumption and was wrong (see the module header), so anything that could still become a trailer is held\n * back in {@link carry} rather than assumed not to be. Only `final`, the call from `flush`, may emit such a\n * tail: by then there is no next batch for it to complete.\n *\n * A tail that never does complete therefore leaves after the payload scripts rather than before them, which\n * is the deliberate half of the trade: releasing it the moment a script wants to go out is the very bug this\n * guards, because the next batch may be the rest of the trailer. It costs at most 13 bytes of a truncated\n * document arriving late, still inside `<body>`.\n */\n function emitBatch(controller: TransformStreamDefaultController<Uint8Array>, final = false): void {\n boundary = null;\n let total = carry?.byteLength ?? 0;\n for (const chunk of batch) total += chunk.byteLength;\n if (total === 0) {\n batch.length = 0;\n return;\n }\n\n const joined = new Uint8Array(total);\n let at = 0;\n if (carry) {\n joined.set(carry, at);\n at += carry.byteLength;\n carry = null;\n }\n for (const chunk of batch) {\n joined.set(chunk, at);\n at += chunk.byteLength;\n }\n batch.length = 0;\n\n const held = final ? (endsWithTrailer(joined, total) ? TRAILER_BYTES.length : 0) : trailerPrefixLength(joined, total);\n const end = total - held;\n // Copied rather than a `subarray`, which would keep the whole joined flush alive for 14 bytes.\n if (held > 0 && !final) carry = joined.slice(end);\n if (end > 0) controller.enqueue(joined.subarray(0, end));\n }\n\n async function writeFlight(controller: TransformStreamDefaultController<Uint8Array>): Promise<void> {\n const reader = (flightReader = rscStream.getReader());\n // One chunk at a time, deliberately *not* `{ stream: true }`: anything that is not valid UTF-8 on its\n // own — a binary row's bytes, or a chunk that split a multi-byte character — throws, and the catch below\n // re-encodes that whole chunk byte-exactly.\n //\n // Streaming is what makes that fallback wrong. A chunk ending mid-character leaves its lead bytes inside\n // the decoder to be emitted with the next chunk; when the next chunk is a binary row and throws, those\n // bytes are in neither `decode()`'s return nor `value`, and the client reassembles a payload short by\n // them. It also left the end-of-stream flush able to throw from outside any `try`.\n //\n // `ignoreBOM` because a per-chunk decode re-runs the BOM check on every call: without it any chunk whose\n // first three bytes are EF BB BF would silently lose them.\n const decoder = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });\n const push = async (literal: string): Promise<void> => {\n const permit = takePermit();\n if (permit) await permit;\n if (cancelled) return;\n controller.enqueue(encoder.encode(scriptOpen + literal + scriptClose));\n };\n for (;;) {\n if (cancelled) return;\n const { done, value } = await reader.read();\n if (done) {\n capture?.complete();\n break;\n }\n // Only the decode is guarded: a `push` inside the same `try` would answer a dead controller by\n // re-encoding the chunk and enqueueing it again.\n let literal: string;\n try {\n literal = escapeScript(JSON.stringify(decoder.decode(value)));\n } catch {\n literal = `Uint8Array.from(atob(${JSON.stringify(btoa(latin1(value)))}), m => m.codePointAt(0))`;\n }\n if (cancelled) return;\n // A failed enqueue means the consumer is gone: release the RSC branch so `flush` unparks now rather\n // than whenever the payload would have ended on its own.\n try {\n await push(literal);\n capture?.push(value);\n } catch (error) {\n cancelled = true;\n capture?.fail(error);\n reader.cancel().catch(() => {});\n return;\n }\n }\n // No end-of-stream flush: a non-streaming decoder holds nothing between calls, so there is nothing left\n // to emit — and the flush this replaces could throw, erroring the response mid-document. See above.\n }\n\n function startFlight(controller: TransformStreamDefaultController<Uint8Array>): void {\n if (startedFlight) return;\n startedFlight = true;\n if (cancelled) {\n flightDone();\n return;\n }\n void writeFlight(controller)\n .catch((error) => {\n capture?.fail(error);\n controller.error(error);\n })\n .then(flightDone);\n }\n\n const transformer: CancellableTransformer<Uint8Array, Uint8Array> = {\n async transform(chunk, controller) {\n if (boundary) {\n batch.push(chunk);\n return;\n }\n // The permit is taken once per *batch*, not once per chunk: React writes a whole flush in one microtask\n // cascade and the flush has to leave here as one chunk (see `emitBatch`), so a per-chunk gate would\n // hand the consumer one chunk per read instead. `transform` is never re-entered concurrently — the\n // writable side serializes it on the promise this returns — so nothing else can claim the batch in\n // between.\n const permit = takePermit();\n if (permit) await permit;\n batch.push(chunk);\n // A macrotask, not a microtask: React writes a whole flush in one synchronous run but `pipeThrough`\n // delivers it one microtask at a time, and a script injected between two chunks lands inside a tag.\n boundary = schedule(() => {\n try {\n emitBatch(controller);\n } catch (error) {\n // A dead consumer, all but always. `teardown` rather than `flightDone` alone: a payload pump parked\n // on a permit has to be unparked too, and the RSC branch it holds released.\n controller.error(error);\n teardown(error);\n return;\n }\n // Deliberately not awaited: this runs inside a scheduled callback with nothing to return to.\n startFlight(controller);\n });\n },\n async flush(controller) {\n // Both of these have to happen *before* the await, and in this order.\n try {\n // Anything still batched — or carried — belongs ahead of the payload scripts, which sit at the end of\n // `<body>`. Called unconditionally: a `carry` outlives its boundary, and an empty batch returns early.\n if (boundary) unschedule(boundary);\n emitBatch(controller, true);\n // A writable side that closed without ever reaching `transform` — an HTML stream with no chunks at\n // all — leaves nothing to have started the payload, and `flightDone` is only ever called from that\n // chain or from `cancel`. Without this the await below parks on a promise nothing will settle and the\n // response never ends. `cancelled` short-circuits it: there is nowhere to write the payload to.\n startFlight(controller);\n } catch (error) {\n // The consumer went away while the batch was being emitted, so there is nothing left to write to and\n // nothing to wait for. Settled explicitly rather than left dangling, so the payload chain is released.\n teardown(error);\n return;\n }\n\n await flightWritten;\n // That await spans the whole payload, and the consumer can go away inside it — with `cancel` skipped\n // (see `cancelled`), a throwing enqueue is the only signal. Unguarded it rejects `flush`, which\n // nothing owns and which surfaces as an unhandled rejection.\n try {\n if (!cancelled) controller.enqueue(encoder.encode(TRAILER));\n } catch {\n // Nowhere left to put the trailer. A response the client abandoned is not a fault.\n cancelled = true;\n flightReader?.cancel().catch(() => {});\n } finally {\n // A `finally` because `onDone` releases the abort forwarder in `renderComponent`, however this ended.\n reportDone();\n }\n },\n cancel(reason) {\n teardown(reason);\n },\n };\n\n const inner = new TransformStream<Uint8Array, Uint8Array>(transformer);\n const innerReader = inner.readable.getReader();\n /**\n * A pull-driven wrapper around the transform's readable, and the only reason this function does not simply\n * return the transform: `pull` runs once per chunk the consumer takes, which is the demand signal the two\n * producers park on (see {@link takePermit}). It is also the one cancel notification the standard never\n * skips, which is what makes {@link teardown} reliable.\n */\n const readable = new ReadableStream<Uint8Array>({\n async pull(controller) {\n releasePermit();\n const { done, value } = await innerReader.read();\n try {\n if (done) controller.close();\n else controller.enqueue(value);\n } catch {\n // Cancelled while this pull was in flight. `teardown` has already run; there is nobody to hand it to.\n }\n },\n cancel(reason) {\n teardown(reason);\n return innerReader.cancel(reason);\n },\n });\n const pair = { readable, writable: inner.writable };\n return capture ? { ...pair, capturedFlight: capture.promise } : pair;\n}\n"]}
|
|
@@ -15,30 +15,26 @@ export type PrerenderVariant = 'html' | 'flight';
|
|
|
15
15
|
export declare const VARIANTS: {
|
|
16
16
|
readonly html: {
|
|
17
17
|
readonly file: 'index.html';
|
|
18
|
-
readonly headers: {};
|
|
19
|
-
readonly contentType: 'text/html';
|
|
20
18
|
};
|
|
21
19
|
readonly flight: {
|
|
22
20
|
readonly file: 'index.rsc';
|
|
23
|
-
readonly headers: {
|
|
24
|
-
readonly RSC: '1';
|
|
25
|
-
};
|
|
26
|
-
readonly contentType: 'text/x-component';
|
|
27
21
|
};
|
|
28
22
|
};
|
|
29
23
|
/**
|
|
30
24
|
* How a build-time document render tells the prerender pass that the app minted a CSP nonce for its path.
|
|
31
25
|
*
|
|
32
|
-
* The
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* about
|
|
26
|
+
* The pass cannot see the request it made: it renders through the app's own middleware inside the server
|
|
27
|
+
* bundle, in a module graph of its own, and keeps the body alone. What it needs to know is whether a
|
|
28
|
+
* *request* for this path will mint a nonce, because a document that will is one the framework re-renders per
|
|
29
|
+
* request — so the file the pass is about to write is one no deployment will read, and saying "prerendered"
|
|
30
|
+
* about it is a lie by omission.
|
|
37
31
|
*
|
|
38
32
|
* Set only while `RSHONO_PRERENDER` is in the environment, which is `rshono build`'s own process, so it never
|
|
39
33
|
* reaches a deployed response; and never stored, since only the body is.
|
|
40
34
|
*/
|
|
41
35
|
export declare const PRERENDER_NONCE_HEADER = "x-rshono-prerender-nonce";
|
|
36
|
+
export declare function setPrerenderFlight(response: Response, flight: Promise<Uint8Array>): void;
|
|
37
|
+
export declare function getPrerenderFlight(response: Response): Promise<Uint8Array> | undefined;
|
|
42
38
|
/**
|
|
43
39
|
* The index the build leaves beside the pages, naming every file it wrote — one `files` array of
|
|
44
40
|
* {@link ssgFilePath} names.
|
|
@@ -7,24 +7,38 @@
|
|
|
7
7
|
* into a bundle that has neither.
|
|
8
8
|
*/
|
|
9
9
|
export const VARIANTS = {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
html: { file: 'index.html', headers: {}, contentType: 'text/html' },
|
|
13
|
-
flight: { file: 'index.rsc', headers: { RSC: '1' }, contentType: 'text/x-component' },
|
|
10
|
+
html: { file: 'index.html' },
|
|
11
|
+
flight: { file: 'index.rsc' },
|
|
14
12
|
};
|
|
15
13
|
/**
|
|
16
14
|
* How a build-time document render tells the prerender pass that the app minted a CSP nonce for its path.
|
|
17
15
|
*
|
|
18
|
-
* The
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* about
|
|
16
|
+
* The pass cannot see the request it made: it renders through the app's own middleware inside the server
|
|
17
|
+
* bundle, in a module graph of its own, and keeps the body alone. What it needs to know is whether a
|
|
18
|
+
* *request* for this path will mint a nonce, because a document that will is one the framework re-renders per
|
|
19
|
+
* request — so the file the pass is about to write is one no deployment will read, and saying "prerendered"
|
|
20
|
+
* about it is a lie by omission.
|
|
23
21
|
*
|
|
24
22
|
* Set only while `RSHONO_PRERENDER` is in the environment, which is `rshono build`'s own process, so it never
|
|
25
23
|
* reaches a deployed response; and never stored, since only the body is.
|
|
26
24
|
*/
|
|
27
25
|
export const PRERENDER_NONCE_HEADER = 'x-rshono-prerender-nonce';
|
|
26
|
+
/**
|
|
27
|
+
* Process-local bridge from the server bundle's build-time HTML render to the SSG writer.
|
|
28
|
+
*
|
|
29
|
+
* The CLI and the app bundle are separate module graphs, so a module-local symbol would not be shared between
|
|
30
|
+
* them. `Symbol.for` keeps this internal contract available to both graphs without putting the Flight bytes in
|
|
31
|
+
* an HTTP header or changing the public response body. It is attached to the response stream because Hono may
|
|
32
|
+
* copy the `Response` object while it finalizes a handler result.
|
|
33
|
+
*/
|
|
34
|
+
const PRERENDER_FLIGHT = Symbol.for('rshono.prerender.flight-stream');
|
|
35
|
+
export function setPrerenderFlight(response, flight) {
|
|
36
|
+
if (response.body)
|
|
37
|
+
Object.defineProperty(response.body, PRERENDER_FLIGHT, { value: flight });
|
|
38
|
+
}
|
|
39
|
+
export function getPrerenderFlight(response) {
|
|
40
|
+
return response.body?.[PRERENDER_FLIGHT];
|
|
41
|
+
}
|
|
28
42
|
/**
|
|
29
43
|
* The index the build leaves beside the pages, naming every file it wrote — one `files` array of
|
|
30
44
|
* {@link ssgFilePath} names.
|
|
@@ -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"]}
|
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, 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
|
-
|
|
144
|
-
|
|
145
|
-
if (
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
return {
|
|
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
|
|
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
|
|
246
|
-
//
|
|
247
|
-
const flight = await
|
|
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
|
|
253
|
-
//
|
|
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 };
|