@rshono/core 1.0.0-rc.11 → 1.0.0-rc.13
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 +2 -1
- package/bin/rshono.mjs +3 -4
- package/dist/builder/env-shadow-loader.cjs +5 -5
- package/dist/builder/page-files.js +5 -5
- package/dist/builder/page-files.js.map +1 -1
- package/dist/builder/public-env.d.ts +5 -4
- package/dist/builder/public-env.d.ts.map +1 -1
- package/dist/builder/public-env.js +5 -4
- package/dist/builder/public-env.js.map +1 -1
- package/dist/builder/rspack-config.d.ts +10 -0
- package/dist/builder/rspack-config.d.ts.map +1 -1
- package/dist/builder/rspack-config.js +38 -40
- package/dist/builder/rspack-config.js.map +1 -1
- package/dist/cli/build.js +4 -4
- package/dist/cli/build.js.map +1 -1
- package/dist/cli/dev.d.ts.map +1 -1
- package/dist/cli/dev.js +50 -37
- package/dist/cli/dev.js.map +1 -1
- package/dist/cli/index.js +4 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/start.d.ts.map +1 -1
- package/dist/cli/start.js +2 -3
- package/dist/cli/start.js.map +1 -1
- package/dist/config.d.ts +28 -31
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -2
- package/dist/config.js.map +1 -1
- package/dist/deploy/aws-lambda/runtime.d.ts +4 -6
- package/dist/deploy/aws-lambda/runtime.d.ts.map +1 -1
- package/dist/deploy/aws-lambda/runtime.js +5 -8
- package/dist/deploy/aws-lambda/runtime.js.map +1 -1
- package/dist/deploy/build-marker.d.ts +3 -5
- package/dist/deploy/build-marker.d.ts.map +1 -1
- package/dist/deploy/build-marker.js +3 -5
- package/dist/deploy/build-marker.js.map +1 -1
- package/dist/deploy/cloudflare/build.d.ts.map +1 -1
- package/dist/deploy/cloudflare/build.js +6 -10
- package/dist/deploy/cloudflare/build.js.map +1 -1
- package/dist/deploy/cloudflare/runtime.d.ts +2 -5
- package/dist/deploy/cloudflare/runtime.d.ts.map +1 -1
- package/dist/deploy/cloudflare/runtime.js +19 -32
- package/dist/deploy/cloudflare/runtime.js.map +1 -1
- package/dist/deploy/contract.d.ts +25 -42
- package/dist/deploy/contract.d.ts.map +1 -1
- package/dist/deploy/contract.js.map +1 -1
- package/dist/deploy/filesystem.d.ts +3 -5
- package/dist/deploy/filesystem.d.ts.map +1 -1
- package/dist/deploy/filesystem.js +12 -15
- package/dist/deploy/filesystem.js.map +1 -1
- package/dist/deploy/node/runtime.d.ts +4 -5
- package/dist/deploy/node/runtime.d.ts.map +1 -1
- package/dist/deploy/node/runtime.js +9 -15
- package/dist/deploy/node/runtime.js.map +1 -1
- package/dist/deploy/presets.d.ts +19 -29
- package/dist/deploy/presets.d.ts.map +1 -1
- package/dist/deploy/presets.js +18 -25
- package/dist/deploy/presets.js.map +1 -1
- package/dist/deploy/vercel/build.d.ts.map +1 -1
- package/dist/deploy/vercel/build.js +9 -12
- package/dist/deploy/vercel/build.js.map +1 -1
- package/dist/deploy/vercel/runtime.d.ts +4 -7
- package/dist/deploy/vercel/runtime.d.ts.map +1 -1
- package/dist/deploy/vercel/runtime.js +4 -7
- package/dist/deploy/vercel/runtime.js.map +1 -1
- package/dist/index.d.ts +13 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -12
- package/dist/index.js.map +1 -1
- package/dist/router.d.ts +65 -93
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +2 -5
- package/dist/router.js.map +1 -1
- package/dist/runtime/boundaries.d.ts +24 -30
- package/dist/runtime/boundaries.d.ts.map +1 -1
- package/dist/runtime/boundaries.js +15 -22
- package/dist/runtime/boundaries.js.map +1 -1
- package/dist/runtime/client.d.ts +16 -7
- package/dist/runtime/client.d.ts.map +1 -1
- package/dist/runtime/client.js +16 -7
- package/dist/runtime/client.js.map +1 -1
- package/dist/runtime/context.d.ts +89 -118
- package/dist/runtime/context.d.ts.map +1 -1
- package/dist/runtime/context.js +107 -167
- package/dist/runtime/context.js.map +1 -1
- package/dist/runtime/control.js +3 -3
- package/dist/runtime/control.js.map +1 -1
- package/dist/runtime/dev-protocol.d.ts +4 -8
- package/dist/runtime/dev-protocol.d.ts.map +1 -1
- package/dist/runtime/dev-protocol.js.map +1 -1
- package/dist/runtime/entry.client.d.ts.map +1 -1
- package/dist/runtime/entry.client.js +92 -120
- package/dist/runtime/entry.client.js.map +1 -1
- package/dist/runtime/entry.rsc.d.ts +5 -6
- package/dist/runtime/entry.rsc.d.ts.map +1 -1
- package/dist/runtime/entry.rsc.js +67 -122
- package/dist/runtime/entry.rsc.js.map +1 -1
- package/dist/runtime/entry.ssr.d.ts +7 -12
- package/dist/runtime/entry.ssr.d.ts.map +1 -1
- package/dist/runtime/entry.ssr.js +13 -24
- package/dist/runtime/entry.ssr.js.map +1 -1
- package/dist/runtime/flight-inject.d.ts +10 -17
- package/dist/runtime/flight-inject.d.ts.map +1 -1
- package/dist/runtime/flight-inject.js +35 -53
- package/dist/runtime/flight-inject.js.map +1 -1
- package/dist/runtime/hot-update.d.ts +45 -0
- package/dist/runtime/hot-update.d.ts.map +1 -0
- package/dist/runtime/hot-update.js +44 -0
- package/dist/runtime/hot-update.js.map +1 -0
- package/dist/runtime/navigation.d.ts +14 -19
- package/dist/runtime/navigation.d.ts.map +1 -1
- package/dist/runtime/navigation.js +9 -13
- package/dist/runtime/navigation.js.map +1 -1
- package/dist/runtime/request.d.ts +4 -6
- package/dist/runtime/request.d.ts.map +1 -1
- package/dist/runtime/request.js +2 -3
- package/dist/runtime/request.js.map +1 -1
- package/dist/runtime/server.d.ts +18 -10
- package/dist/runtime/server.d.ts.map +1 -1
- package/dist/runtime/server.js +21 -19
- package/dist/runtime/server.js.map +1 -1
- package/dist/server/headers.d.ts +8 -15
- package/dist/server/headers.d.ts.map +1 -1
- package/dist/server/headers.js +8 -15
- package/dist/server/headers.js.map +1 -1
- package/dist/server/load-config.d.ts +2 -2
- package/dist/server/load-config.d.ts.map +1 -1
- package/dist/server/load-config.js +6 -9
- package/dist/server/load-config.js.map +1 -1
- package/dist/server/prerendered.d.ts +30 -43
- package/dist/server/prerendered.d.ts.map +1 -1
- package/dist/server/prerendered.js +20 -29
- package/dist/server/prerendered.js.map +1 -1
- package/dist/server/server-config.d.ts +18 -25
- package/dist/server/server-config.d.ts.map +1 -1
- package/dist/server/server-config.js +7 -11
- package/dist/server/server-config.js.map +1 -1
- package/dist/server/shutdown.d.ts +2 -3
- package/dist/server/shutdown.d.ts.map +1 -1
- package/dist/server/shutdown.js +2 -3
- package/dist/server/shutdown.js.map +1 -1
- package/dist/server/ssg.d.ts +3 -6
- package/dist/server/ssg.d.ts.map +1 -1
- package/dist/server/ssg.js +11 -20
- package/dist/server/ssg.js.map +1 -1
- package/package.json +1 -1
|
@@ -4,27 +4,21 @@ import { renderToReadableStream } from 'react-dom/server';
|
|
|
4
4
|
import { createFromReadableStream } from 'react-server-dom-rspack/client';
|
|
5
5
|
import { isControlDigest } from './control.js';
|
|
6
6
|
import { injectFlightPayload } from './flight-inject.js';
|
|
7
|
-
//
|
|
8
|
-
// target need not have a `process` at all.
|
|
7
|
+
// The baked config, not `process.env.NODE_ENV`: a deploy target need not have a `process`.
|
|
9
8
|
const isDev = __RSHONO_CONFIG__.isDev;
|
|
10
9
|
/** Escapes text going into HTML body content — a stack trace is untrusted input. */
|
|
11
10
|
function escapeHtml(text) {
|
|
12
11
|
return text.replace(/[&<>]/g, (char) => (char === '&' ? '&' : char === '<' ? '<' : '>'));
|
|
13
12
|
}
|
|
14
13
|
/**
|
|
15
|
-
* The last-resort 500 document, for when SSR fails before a
|
|
14
|
+
* The last-resort 500 document, for when SSR fails before a byte of the real shell was sent.
|
|
16
15
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
16
|
+
* Plain HTML with no client runtime and no stylesheet links: both came from the render that just failed,
|
|
17
|
+
* and hydrating a mismatched payload would tear the page down over this very message. A string rather than
|
|
18
|
+
* a component for the same reason — React is what failed. It must end with the document trailer, which
|
|
19
|
+
* `injectFlightPayload` holds back and re-emits.
|
|
21
20
|
*
|
|
22
|
-
*
|
|
23
|
-
* this through it again is a dependency the fallback does not need. It must end with the document
|
|
24
|
-
* trailer, which is what `injectFlightPayload` holds back and re-emits.
|
|
25
|
-
*
|
|
26
|
-
* The detail is dev-only. In production this stays a generic message, matching how the `error` page
|
|
27
|
-
* from routes.ts redacts.
|
|
21
|
+
* The detail is dev-only, matching how the app's `error` page redacts.
|
|
28
22
|
*/
|
|
29
23
|
function failureDocument(error) {
|
|
30
24
|
const detail = isDev ? (error instanceof Error ? (error.stack ?? `${error.name}: ${error.message}`) : String(error)) : null;
|
|
@@ -59,13 +53,10 @@ export async function renderHTML(rscStream, options) {
|
|
|
59
53
|
payload ??= createFromReadableStream(rscForSsr, options.nonce ? { nonce: options.nonce } : undefined);
|
|
60
54
|
return React.use(payload).root;
|
|
61
55
|
}
|
|
62
|
-
// React hands `onError` every error it meets while streaming,
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
// prints an alarming, detail-free duplicate for a request a boundary handled perfectly well. Only
|
|
67
|
-
// an error carrying no digest started life in this render — a client component that threw during
|
|
68
|
-
// SSR — and that one nothing else will report.
|
|
56
|
+
// React hands `onError` every error it meets while streaming, contained ones included. Almost all arrive
|
|
57
|
+
// out of the flight payload as its redacted stand-in — a `digest` and no message — and the RSC layer has
|
|
58
|
+
// already reported those in full. Only an error carrying no digest started life in this render, and that
|
|
59
|
+
// one nothing else will report.
|
|
69
60
|
let reported;
|
|
70
61
|
const onError = (error) => {
|
|
71
62
|
if (typeof error?.digest === 'string')
|
|
@@ -83,16 +74,14 @@ export async function renderHTML(rscStream, options) {
|
|
|
83
74
|
formState: options.formState,
|
|
84
75
|
signal: options.signal,
|
|
85
76
|
nonce: options.nonce,
|
|
86
|
-
//
|
|
87
|
-
// stays exactly what it was before a handler was installed here.
|
|
77
|
+
// Returns nothing, so the digest React gives the client's `onRecoverableError` is unchanged.
|
|
88
78
|
onError,
|
|
89
79
|
});
|
|
90
80
|
}
|
|
91
81
|
catch (error) {
|
|
92
82
|
if (isControlDigest(error?.digest))
|
|
93
83
|
throw error;
|
|
94
|
-
// `onError` runs first for the failure that aborts the shell, so this reports only what it let
|
|
95
|
-
// through: an error out of the flight payload, whose detail the RSC layer alone has.
|
|
84
|
+
// `onError` runs first for the failure that aborts the shell, so this reports only what it let through.
|
|
96
85
|
if (!options.signal?.aborted && error !== reported)
|
|
97
86
|
options.onShellError?.(error);
|
|
98
87
|
status = 500;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entry.ssr.js","sourceRoot":"","sources":["../../src/runtime/entry.ssr.tsx"],"names":[],"mappings":";AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAC1D,OAAO,EAAE,wBAAwB,EAAE,MAAM,gCAAgC,CAAC;AAC1E,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"entry.ssr.js","sourceRoot":"","sources":["../../src/runtime/entry.ssr.tsx"],"names":[],"mappings":"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","sourcesContent":["import React from 'react';\nimport type { ReactFormState } from 'react-dom/client';\nimport { renderToReadableStream } from 'react-dom/server';\nimport { createFromReadableStream } from 'react-server-dom-rspack/client';\nimport { isControlDigest } from './control.js';\nimport { injectFlightPayload } from './flight-inject.js';\nimport type { RscPayload } from './entry.rsc.js';\n\nexport interface RenderHTMLOptions {\n bootstrapScripts?: string[];\n formState?: ReactFormState;\n signal?: AbortSignal;\n nonce?: string;\n /**\n * Called when SSR fails before the shell is sent. Reporting is the RSC layer's job: this module is\n * compiled into the SSR layer, which gets its own instance of every module it imports, so a handler\n * registered through `@rshono/core/server` is not reachable from here.\n */\n onShellError?: (error: unknown) => void;\n /**\n * Called for an error that *originated* in SSR — a client component that threw while rendering on the\n * server. See {@link renderHTML} for why the others are dropped.\n */\n onError?: (error: unknown) => void;\n /**\n * Called once the response stream has ended, however it ended. Load-bearing: the RSC layer uses it to\n * detach the abort forwarder that would otherwise retain the whole rendered tree.\n */\n onDone?: () => void;\n}\n\n// The baked config, not `process.env.NODE_ENV`: a deploy target need not have a `process`.\nconst isDev = __RSHONO_CONFIG__.isDev;\n\n/** Escapes text going into HTML body content — a stack trace is untrusted input. */\nfunction escapeHtml(text: string): string {\n return text.replace(/[&<>]/g, (char) => (char === '&' ? '&' : char === '<' ? '<' : '>'));\n}\n\n/**\n * The last-resort 500 document, for when SSR fails before a byte of the real shell was sent.\n *\n * Plain HTML with no client runtime and no stylesheet links: both came from the render that just failed,\n * and hydrating a mismatched payload would tear the page down over this very message. A string rather than\n * a component for the same reason — React is what failed. It must end with the document trailer, which\n * `injectFlightPayload` holds back and re-emits.\n *\n * The detail is dev-only, matching how the app's `error` page redacts.\n */\nfunction failureDocument(error: unknown): ReadableStream<Uint8Array> {\n const detail = isDev ? (error instanceof Error ? (error.stack ?? `${error.name}: ${error.message}`) : String(error)) : null;\n const html =\n '<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"utf-8\">' +\n '<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">' +\n '<title>500 — Internal Server Error</title></head>' +\n '<body style=\"margin:0;padding:2rem;font:16px/1.6 system-ui,-apple-system,sans-serif;color:#18181b\">' +\n '<h1 style=\"margin:0 0 .5rem;font-size:1.25rem\">500 — Internal Server Error</h1>' +\n '<p style=\"margin:0 0 1.5rem;color:#52525b\">' +\n (isDev\n ? 'Server-side rendering failed before the page shell could be sent, so the app’s error page could not be reached either.'\n : 'Something went wrong while rendering this page. Please try again.') +\n '</p>' +\n (detail\n ? '<pre style=\"margin:0;padding:1rem;overflow:auto;background:#f4f4f5;border-left:3px solid #ef4444;' +\n `font:13px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;white-space:pre-wrap;word-break:break-word\">${escapeHtml(detail)}</pre>`\n : '') +\n '</body></html>';\n const bytes = new TextEncoder().encode(html);\n return new ReadableStream<Uint8Array>({\n start(controller) {\n controller.enqueue(bytes);\n controller.close();\n },\n });\n}\n\nexport async function renderHTML(rscStream: ReadableStream<Uint8Array>, options: RenderHTMLOptions) {\n // One copy is rendered to HTML here; the other rides along in that HTML for the client to hydrate from.\n const [rscForSsr, rscForClient] = rscStream.tee();\n\n let payload: Promise<RscPayload>;\n function SsrRoot() {\n payload ??= createFromReadableStream<RscPayload>(rscForSsr, options.nonce ? { nonce: options.nonce } : undefined);\n return React.use(payload).root;\n }\n\n // React hands `onError` every error it meets while streaming, contained ones included. Almost all arrive\n // out of the flight payload as its redacted stand-in — a `digest` and no message — and the RSC layer has\n // already reported those in full. Only an error carrying no digest started life in this render, and that\n // one nothing else will report.\n let reported: unknown;\n const onError = (error: unknown): void => {\n if (typeof (error as { digest?: unknown } | null)?.digest === 'string') return;\n if (options.signal?.aborted) return; // an abort is the client leaving, not a fault\n reported = error;\n options.onError?.(error);\n };\n\n let htmlStream: ReadableStream<Uint8Array>;\n let status: number | undefined;\n try {\n htmlStream = await renderToReadableStream(<SsrRoot />, {\n bootstrapScripts: options.bootstrapScripts,\n formState: options.formState,\n signal: options.signal,\n nonce: options.nonce,\n // Returns nothing, so the digest React gives the client's `onRecoverableError` is unchanged.\n onError,\n });\n } catch (error) {\n if (isControlDigest((error as { digest?: unknown } | null)?.digest)) throw error;\n // `onError` runs first for the failure that aborts the shell, so this reports only what it let through.\n if (!options.signal?.aborted && error !== reported) options.onShellError?.(error);\n status = 500;\n htmlStream = failureDocument(error);\n }\n\n const responseStream = htmlStream.pipeThrough(injectFlightPayload(rscForClient, { nonce: options.nonce, onDone: options.onDone }));\n\n return { stream: responseStream, status };\n}\n"]}
|
|
@@ -1,25 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Carrying the flight payload inside the HTML document, so the browser hydrates from bytes it
|
|
3
|
-
*
|
|
2
|
+
* Carrying the flight payload inside the HTML document, so the browser hydrates from bytes it already has
|
|
3
|
+
* rather than fetching the page twice. The reader is `readFlightPayload` in `entry.client.tsx`.
|
|
4
4
|
*
|
|
5
|
-
* The wire format is `rsc-html-stream`'s — a run of
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* that straddles one, so `</body></html>` can arrive as two chunks; upstream then fails to hold it
|
|
10
|
-
* back, and the document ends up with two trailers and the payload script after the first of them.
|
|
11
|
-
* A page's byte layout is deterministic, so that is not an intermittent fault — an unlucky page is
|
|
12
|
-
* malformed on every request. `test/unit.test.mjs` pins the split-trailer cases.
|
|
13
|
-
*
|
|
14
|
-
* The reader for this format is `readFlightPayload` in `entry.client.tsx`.
|
|
5
|
+
* The wire format is `rsc-html-stream`'s — a run of `<script>(self.__FLIGHT_DATA||=[]).push("…")</script>`
|
|
6
|
+
* tags — but the implementation is first-party, because that package tests each HTML chunk for the
|
|
7
|
+
* document trailer with `endsWith`: React's byte writer splits any write straddling its 2 kB views, so
|
|
8
|
+
* `</body></html>` can arrive as two chunks and the document ends up with two trailers.
|
|
15
9
|
*/
|
|
16
10
|
/**
|
|
17
|
-
* {@link Transformer} plus the `cancel` hook the Streams standard added for a cancelled readable side
|
|
11
|
+
* {@link Transformer} plus the `cancel` hook the Streams standard added for a cancelled readable side —
|
|
12
|
+
* declared here because the bundled lib types have not caught up with what Node calls.
|
|
18
13
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* that a response ended *without* finishing, and both users of it release a listener that would
|
|
22
|
-
* otherwise outlive the request.
|
|
14
|
+
* It matters because `cancel` is the only notification that a response ended *without* finishing, and both
|
|
15
|
+
* users of it release a listener that would otherwise outlive the request.
|
|
23
16
|
*/
|
|
24
17
|
export type CancellableTransformer<I, O> = Transformer<I, O> & {
|
|
25
18
|
cancel?: (reason?: unknown) => void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"flight-inject.d.ts","sourceRoot":"","sources":["../../src/runtime/flight-inject.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"flight-inject.d.ts","sourceRoot":"","sources":["../../src/runtime/flight-inject.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;;;GAMG;AACH,MAAM,MAAM,sBAAsB,CAAC,CAAC,EAAE,CAAC,IAAI,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG;IAAE,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,OAAO,KAAK,IAAI,CAAA;CAAE,CAAC;AAkDvG,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,cAAc,CAAC,UAAU,CAAC,EACrC,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;CAAO,GACpD,eAAe,CAAC,UAAU,EAAE,UAAU,CAAC,CAiJzC"}
|
|
@@ -1,17 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Carrying the flight payload inside the HTML document, so the browser hydrates from bytes it
|
|
3
|
-
*
|
|
2
|
+
* Carrying the flight payload inside the HTML document, so the browser hydrates from bytes it already has
|
|
3
|
+
* rather than fetching the page twice. The reader is `readFlightPayload` in `entry.client.tsx`.
|
|
4
4
|
*
|
|
5
|
-
* The wire format is `rsc-html-stream`'s — a run of
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* that straddles one, so `</body></html>` can arrive as two chunks; upstream then fails to hold it
|
|
10
|
-
* back, and the document ends up with two trailers and the payload script after the first of them.
|
|
11
|
-
* A page's byte layout is deterministic, so that is not an intermittent fault — an unlucky page is
|
|
12
|
-
* malformed on every request. `test/unit.test.mjs` pins the split-trailer cases.
|
|
13
|
-
*
|
|
14
|
-
* The reader for this format is `readFlightPayload` in `entry.client.tsx`.
|
|
5
|
+
* The wire format is `rsc-html-stream`'s — a run of `<script>(self.__FLIGHT_DATA||=[]).push("…")</script>`
|
|
6
|
+
* tags — but the implementation is first-party, because that package tests each HTML chunk for the
|
|
7
|
+
* document trailer with `endsWith`: React's byte writer splits any write straddling its 2 kB views, so
|
|
8
|
+
* `</body></html>` can arrive as two chunks and the document ends up with two trailers.
|
|
15
9
|
*/
|
|
16
10
|
const encoder = new TextEncoder();
|
|
17
11
|
/** What React closes an `<html>` document with, and what this module re-emits after the last payload script. */
|
|
@@ -26,21 +20,16 @@ const unschedule = (handle) => {
|
|
|
26
20
|
clearTimeout(handle);
|
|
27
21
|
};
|
|
28
22
|
/**
|
|
29
|
-
* Escapes the two sequences that would end a `<script>` element early.
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* scan is far cheaper than two regex passes over 30 kB. `</script` becomes `</\script` rather than
|
|
33
|
-
* `<\/script`, which would break the valid JS `0</script/` (a regexp literal).
|
|
23
|
+
* Escapes the two sequences that would end a `<script>` element early. Guarded by `includes('<')`, which is
|
|
24
|
+
* far cheaper than two regex passes over 30 kB of payload that usually has no `<` in it. `</script` becomes
|
|
25
|
+
* `</\script`, not `<\/script`, which would break the valid JS `0</script/`.
|
|
34
26
|
*/
|
|
35
27
|
function escapeScript(script) {
|
|
36
28
|
return script.includes('<') ? script.replace(/<!--/g, '<\\!--').replace(/<\/(script)/gi, '</\\$1') : script;
|
|
37
29
|
}
|
|
38
30
|
/**
|
|
39
|
-
* The bytes of `chunk` as a latin1 string, which is the form `btoa` takes.
|
|
40
|
-
*
|
|
41
|
-
* Sliced rather than `String.fromCharCode(...chunk)`, which passes one argument per byte and
|
|
42
|
-
* overflows the call stack on a chunk of any size. The slice width is a stack-safety choice, not a
|
|
43
|
-
* tuned one — this is only reached for a chunk that split a multi-byte character.
|
|
31
|
+
* The bytes of `chunk` as a latin1 string, which is the form `btoa` takes. Sliced because
|
|
32
|
+
* `String.fromCharCode(...chunk)` passes one argument per byte and overflows the stack.
|
|
44
33
|
*/
|
|
45
34
|
function latin1(chunk) {
|
|
46
35
|
let out = '';
|
|
@@ -68,13 +57,11 @@ export function injectFlightPayload(rscStream, options = {}) {
|
|
|
68
57
|
const batch = [];
|
|
69
58
|
let boundary = null;
|
|
70
59
|
/**
|
|
71
|
-
* Set once the consumer has gone away, so nothing
|
|
72
|
-
* can no longer take it.
|
|
60
|
+
* Set once the consumer has gone away, so nothing tries to enqueue into a readable that cannot take it.
|
|
73
61
|
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
* precisely that window. So a failed enqueue is also treated as the signal, wherever one happens.
|
|
62
|
+
* The `cancel` hook alone cannot set this: per the Streams standard, cancelling the readable after the
|
|
63
|
+
* close algorithm has started skips the transformer's `cancel` entirely — and `flush` awaiting the whole
|
|
64
|
+
* payload is precisely that window. So a failed enqueue counts as the signal too.
|
|
78
65
|
*/
|
|
79
66
|
let cancelled = false;
|
|
80
67
|
/** Held so {@link cancelled} can release the teed RSC branch rather than leaving it to be pumped. */
|
|
@@ -83,10 +70,9 @@ export function injectFlightPayload(rscStream, options = {}) {
|
|
|
83
70
|
* Emits the HTML buffered since the last boundary, holding back the document trailer for
|
|
84
71
|
* {@link TransformStream.flush} to re-emit after the payload scripts.
|
|
85
72
|
*
|
|
86
|
-
* The batch is joined before the trailer is looked for, so
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
* batch; a trailer split *across* batches is not a shape React produces.
|
|
73
|
+
* The batch is joined before the trailer is looked for, so one React split across two views is still
|
|
74
|
+
* found. React writes its final flush in one synchronous run, so a trailer split across *batches* is not
|
|
75
|
+
* a shape it produces.
|
|
90
76
|
*/
|
|
91
77
|
function emitBatch(controller) {
|
|
92
78
|
boundary = null;
|
|
@@ -110,8 +96,8 @@ export function injectFlightPayload(rscStream, options = {}) {
|
|
|
110
96
|
}
|
|
111
97
|
async function writeFlight(controller) {
|
|
112
98
|
const reader = (flightReader = rscStream.getReader());
|
|
113
|
-
// `fatal`, so a chunk that split a multi-byte character throws
|
|
114
|
-
//
|
|
99
|
+
// `fatal`, so a chunk that split a multi-byte character throws instead of emitting U+FFFD; the catch
|
|
100
|
+
// below falls back to a byte-exact encoding for it.
|
|
115
101
|
const decoder = new TextDecoder('utf-8', { fatal: true });
|
|
116
102
|
const push = (literal) => controller.enqueue(encoder.encode(scriptOpen + literal + scriptClose));
|
|
117
103
|
for (;;) {
|
|
@@ -120,9 +106,8 @@ export function injectFlightPayload(rscStream, options = {}) {
|
|
|
120
106
|
const { done, value } = await reader.read();
|
|
121
107
|
if (done)
|
|
122
108
|
break;
|
|
123
|
-
// Only the
|
|
124
|
-
//
|
|
125
|
-
// re-encoding the chunk and enqueueing it again — which throws in turn, out of the catch.
|
|
109
|
+
// Only the decode is guarded: a `push` inside the same `try` would answer a dead controller by
|
|
110
|
+
// re-encoding the chunk and enqueueing it again.
|
|
126
111
|
let literal;
|
|
127
112
|
try {
|
|
128
113
|
literal = escapeScript(JSON.stringify(decoder.decode(value, { stream: true })));
|
|
@@ -132,8 +117,8 @@ export function injectFlightPayload(rscStream, options = {}) {
|
|
|
132
117
|
}
|
|
133
118
|
if (cancelled)
|
|
134
119
|
return;
|
|
135
|
-
// A failed enqueue means the consumer is gone
|
|
136
|
-
//
|
|
120
|
+
// A failed enqueue means the consumer is gone: release the RSC branch so `flush` unparks now rather
|
|
121
|
+
// than whenever the payload would have ended on its own.
|
|
137
122
|
try {
|
|
138
123
|
push(literal);
|
|
139
124
|
}
|
|
@@ -154,10 +139,8 @@ export function injectFlightPayload(rscStream, options = {}) {
|
|
|
154
139
|
batch.push(chunk);
|
|
155
140
|
if (boundary)
|
|
156
141
|
return;
|
|
157
|
-
// A macrotask, not a microtask: React writes a whole flush
|
|
158
|
-
//
|
|
159
|
-
// boundary guarantees the flush has arrived in full — a script injected between two of its
|
|
160
|
-
// chunks would land inside a tag.
|
|
142
|
+
// A macrotask, not a microtask: React writes a whole flush in one synchronous run but `pipeThrough`
|
|
143
|
+
// delivers it one microtask at a time, and a script injected between two chunks lands inside a tag.
|
|
161
144
|
boundary = schedule(() => {
|
|
162
145
|
try {
|
|
163
146
|
emitBatch(controller);
|
|
@@ -169,7 +152,9 @@ export function injectFlightPayload(rscStream, options = {}) {
|
|
|
169
152
|
}
|
|
170
153
|
if (!startedFlight) {
|
|
171
154
|
startedFlight = true;
|
|
172
|
-
|
|
155
|
+
// Deliberately not awaited: this runs inside a scheduled callback with nothing to return to, and
|
|
156
|
+
// the chain already routes a write failure to `controller.error` before settling `flightDone`.
|
|
157
|
+
void writeFlight(controller)
|
|
173
158
|
.catch((error) => controller.error(error))
|
|
174
159
|
.then(flightDone);
|
|
175
160
|
}
|
|
@@ -177,11 +162,9 @@ export function injectFlightPayload(rscStream, options = {}) {
|
|
|
177
162
|
},
|
|
178
163
|
async flush(controller) {
|
|
179
164
|
await flightWritten;
|
|
180
|
-
// That await spans the
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
// signal. Unguarded it rejects `flush`, and nothing owns that rejection: it surfaces as an
|
|
184
|
-
// unhandled one and, where the host does not swallow it, takes the process down.
|
|
165
|
+
// That await spans the whole payload, and the consumer can go away inside it — with `cancel` skipped
|
|
166
|
+
// (see `cancelled`), a throwing enqueue is the only signal. Unguarded it rejects `flush`, which
|
|
167
|
+
// nothing owns and which surfaces as an unhandled rejection.
|
|
185
168
|
try {
|
|
186
169
|
if (boundary) {
|
|
187
170
|
unschedule(boundary);
|
|
@@ -196,8 +179,7 @@ export function injectFlightPayload(rscStream, options = {}) {
|
|
|
196
179
|
flightReader?.cancel().catch(() => { });
|
|
197
180
|
}
|
|
198
181
|
finally {
|
|
199
|
-
//
|
|
200
|
-
// forwarder in `renderComponent`, so it has to run however the response ended.
|
|
182
|
+
// A `finally` because `onDone` releases the abort forwarder in `renderComponent`, however this ended.
|
|
201
183
|
onDone?.();
|
|
202
184
|
}
|
|
203
185
|
},
|
|
@@ -208,8 +190,8 @@ export function injectFlightPayload(rscStream, options = {}) {
|
|
|
208
190
|
boundary = null;
|
|
209
191
|
}
|
|
210
192
|
batch.length = 0;
|
|
211
|
-
//
|
|
212
|
-
//
|
|
193
|
+
// Otherwise the teed RSC branch keeps being pumped for a response nobody will read, and the tee's
|
|
194
|
+
// other half buffers every chunk waiting for this one to catch up.
|
|
213
195
|
flightReader?.cancel(reason).catch(() => { });
|
|
214
196
|
// Unparks `flush` if it is waiting on a payload that will now never arrive.
|
|
215
197
|
flightDone();
|
|
@@ -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\n * already has rather than fetching the page a second time.\n *\n * The wire format is `rsc-html-stream`'s — a run of\n * `<script>(self.__FLIGHT_DATA||=[]).push(\"…\")</script>` tags — but the implementation is\n * first-party, because that package's `injectRSCPayload` tests each HTML chunk for the document\n * trailer with `endsWith`. React's byte writer packs its output into 2 kB views and splits any write\n * that straddles one, so `</body></html>` can arrive as two chunks; upstream then fails to hold it\n * back, and the document ends up with two trailers and the payload script after the first of them.\n * A page's byte layout is deterministic, so that is not an intermittent fault — an unlucky page is\n * malformed on every request. `test/unit.test.mjs` pins the split-trailer cases.\n *\n * The reader for this format is `readFlightPayload` in `entry.client.tsx`.\n */\n\n/**\n * {@link Transformer} plus the `cancel` hook the Streams standard added for a cancelled readable side.\n *\n * Node calls it (verified on 22.x) but the bundled lib types have not caught up, so it is declared\n * here rather than reached for with an `any`. It matters because `cancel` is the only notification\n * that a response ended *without* finishing, and both users of it release a listener that would\n * 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 (Node, Bun, Deno's node compat), which is\n * the current turn's check phase rather than a timer; `setTimeout` is the portable fallback that\n * every other runtime has. A Node timer has a 1ms floor and every HTML flush would pay it — worth\n * the two lines: uncontended time-to-last-byte on a 30 kB payload, 4.7ms → 3.0ms.\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.\n *\n * Guarded by an `includes('<')` because a flight payload usually has no `<` in it at all, and the\n * scan is far cheaper than two regex passes over 30 kB. `</script` becomes `</\\script` rather than\n * `<\\/script`, which would break the valid JS `0</script/` (a regexp literal).\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.\n *\n * Sliced rather than `String.fromCharCode(...chunk)`, which passes one argument per byte and\n * overflows the call stack on a chunk of any size. The slice width is a stack-safety choice, not a\n * tuned one — this is only reached for a chunk that split a multi-byte character.\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\nexport function injectFlightPayload(\n rscStream: ReadableStream<Uint8Array>,\n options: { nonce?: string; onDone?: () => void } = {},\n): TransformStream<Uint8Array, Uint8Array> {\n const { nonce, onDone } = options;\n const scriptOpen = `<script${nonce ? ` nonce=\"${nonce}\"` : ''}>(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 /**\n * Set once the consumer has gone away, so nothing downstream tries to enqueue into a readable that\n * can no longer take it.\n *\n * It cannot simply be the `cancel` hook that sets this. Per the Streams standard, cancelling the\n * readable *after* the close algorithm has started returns the pending finish promise without\n * running the transformer's `cancel` at all — and `flush` awaiting the whole flight payload is\n * precisely that window. So a failed enqueue is also treated as the signal, wherever one happens.\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 * Emits the HTML buffered since the last boundary, holding back the document trailer for\n * {@link TransformStream.flush} to re-emit after the payload scripts.\n *\n * The batch is joined before the trailer is looked for, so a trailer React split across two views\n * is still found — that is the whole reason this module is not `rsc-html-stream/server`. React\n * writes its final flush in one synchronous run, so every chunk of the trailer lands in the same\n * batch; a trailer split *across* batches is not a shape React produces.\n */\n function emitBatch(controller: TransformStreamDefaultController<Uint8Array>): void {\n boundary = null;\n let total = 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 for (const chunk of batch) {\n joined.set(chunk, at);\n at += chunk.byteLength;\n }\n batch.length = 0;\n\n const end = endsWithTrailer(joined, total) ? total - TRAILER_BYTES.length : total;\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 // `fatal`, so a chunk that split a multi-byte character throws rather than emitting U+FFFD and\n // corrupting the payload — the catch below falls back to a byte-exact encoding for it.\n const decoder = new TextDecoder('utf-8', { fatal: true });\n const push = (literal: string) => controller.enqueue(encoder.encode(scriptOpen + literal + scriptClose));\n for (;;) {\n if (cancelled) return;\n const { done, value } = await reader.read();\n if (done) break;\n // Only the *decode* is guarded. Wrapping the `push` in the same `try` conflated a split\n // multi-byte character with a controller nobody is reading any more, and answered the second by\n // re-encoding the chunk and enqueueing it again — which throws in turn, out of the catch.\n let literal: string;\n try {\n literal = escapeScript(JSON.stringify(decoder.decode(value, { stream: true })));\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. Stop pumping and release the RSC branch, so\n // `flush` unparks now rather than whenever the flight payload would have ended on its own.\n try {\n push(literal);\n } catch {\n cancelled = true;\n reader.cancel().catch(() => {});\n return;\n }\n }\n if (cancelled) return;\n const remaining = decoder.decode();\n if (remaining.length) push(escapeScript(JSON.stringify(remaining)));\n }\n\n const transformer: CancellableTransformer<Uint8Array, Uint8Array> = {\n transform(chunk, controller) {\n batch.push(chunk);\n if (boundary) return;\n // A macrotask, not a microtask: React writes a whole flush into its stream in one synchronous\n // run, but `pipeThrough` delivers those chunks to us one microtask at a time. Only a task\n // boundary guarantees the flush has arrived in full — a script injected between two of its\n // chunks would land inside a tag.\n boundary = schedule(() => {\n try {\n emitBatch(controller);\n } catch (error) {\n controller.error(error);\n flightDone();\n return;\n }\n if (!startedFlight) {\n startedFlight = true;\n writeFlight(controller)\n .catch((error) => controller.error(error))\n .then(flightDone);\n }\n });\n },\n async flush(controller) {\n await flightWritten;\n // That await spans the entire flight payload, and the consumer can go away inside it — a\n // browser's stop button, a navigation away, a proxy timeout. `cancel` below is *not* what tells\n // us so (see `cancelled`), which leaves the enqueue throwing `ERR_INVALID_STATE` as the only\n // signal. Unguarded it rejects `flush`, and nothing owns that rejection: it surfaces as an\n // unhandled one and, where the host does not swallow it, takes the process down.\n try {\n if (boundary) {\n unschedule(boundary);\n emitBatch(controller);\n }\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 // Unconditional, and the reason this is a `finally`: `onDone` is what releases the abort\n // forwarder in `renderComponent`, so it has to run however the response ended.\n onDone?.();\n }\n },\n cancel(reason) {\n cancelled = true;\n if (boundary) {\n unschedule(boundary);\n boundary = null;\n }\n batch.length = 0;\n // Without this the teed RSC branch keeps being pumped for a response nobody will read, and the\n // tee's 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 onDone?.();\n },\n };\n return new TransformStream<Uint8Array, Uint8Array>(transformer);\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\nexport function injectFlightPayload(\n rscStream: ReadableStream<Uint8Array>,\n options: { nonce?: string; onDone?: () => void } = {},\n): TransformStream<Uint8Array, Uint8Array> {\n const { nonce, onDone } = options;\n const scriptOpen = `<script${nonce ? ` nonce=\"${nonce}\"` : ''}>(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 /**\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 * Emits the HTML buffered since the last boundary, holding back the document trailer for\n * {@link TransformStream.flush} to re-emit 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\n * found. React writes its final flush in one synchronous run, so a trailer split across *batches* is not\n * a shape it produces.\n */\n function emitBatch(controller: TransformStreamDefaultController<Uint8Array>): void {\n boundary = null;\n let total = 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 for (const chunk of batch) {\n joined.set(chunk, at);\n at += chunk.byteLength;\n }\n batch.length = 0;\n\n const end = endsWithTrailer(joined, total) ? total - TRAILER_BYTES.length : total;\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 // `fatal`, so a chunk that split a multi-byte character throws instead of emitting U+FFFD; the catch\n // below falls back to a byte-exact encoding for it.\n const decoder = new TextDecoder('utf-8', { fatal: true });\n const push = (literal: string) => controller.enqueue(encoder.encode(scriptOpen + literal + scriptClose));\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, { stream: true })));\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 push(literal);\n } catch {\n cancelled = true;\n reader.cancel().catch(() => {});\n return;\n }\n }\n if (cancelled) return;\n const remaining = decoder.decode();\n if (remaining.length) push(escapeScript(JSON.stringify(remaining)));\n }\n\n const transformer: CancellableTransformer<Uint8Array, Uint8Array> = {\n transform(chunk, controller) {\n batch.push(chunk);\n if (boundary) return;\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 controller.error(error);\n flightDone();\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 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 (boundary) {\n unschedule(boundary);\n emitBatch(controller);\n }\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 onDone?.();\n }\n },\n cancel(reason) {\n cancelled = true;\n if (boundary) {\n unschedule(boundary);\n boundary = null;\n }\n batch.length = 0;\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 onDone?.();\n },\n };\n return new TransformStream<Uint8Array, Uint8Array>(transformer);\n}\n"]}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Walking the page from the build it is running to the build the dev server has announced. Its own module
|
|
3
|
+
* because every decision in it is about *giving up correctly*, which is worth testing on its own.
|
|
4
|
+
*
|
|
5
|
+
* Dev-only: the sole caller sits behind `import.meta.webpackHot`, which a production build replaces with
|
|
6
|
+
* `false`.
|
|
7
|
+
*/
|
|
8
|
+
/** The slice of `import.meta.webpackHot` the walk uses. */
|
|
9
|
+
export interface HotRuntime {
|
|
10
|
+
/**
|
|
11
|
+
* Fetches the update manifest for the build the page is running and, with `autoApply`, applies it.
|
|
12
|
+
*
|
|
13
|
+
* Resolves with the updated module ids, or with **null** when the manifest 404s — which the runtime
|
|
14
|
+
* treats as "nothing to do" rather than an error. Rejects only when an update was found and could not be
|
|
15
|
+
* applied.
|
|
16
|
+
*/
|
|
17
|
+
check(autoApply?: boolean): Promise<Array<string | number> | null>;
|
|
18
|
+
status(): string;
|
|
19
|
+
}
|
|
20
|
+
/** Why the page has to be reloaded rather than patched — `error` only where one was thrown. */
|
|
21
|
+
export interface ReloadReason {
|
|
22
|
+
reason: string;
|
|
23
|
+
error?: unknown;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Advances the page to `targetHash()`, applying one build's worth of updates per round, and reports whether
|
|
27
|
+
* it got there.
|
|
28
|
+
*
|
|
29
|
+
* Both hashes are read through functions because both move underneath this loop: an applied update rewrites
|
|
30
|
+
* `__webpack_hash__`, and a save landing mid-walk moves the target further out. Reading them fresh each
|
|
31
|
+
* round is what lets one walk absorb a burst of saves.
|
|
32
|
+
*
|
|
33
|
+
* Returns `null` once the page is on the target build, or the reason it cannot get there — every one of
|
|
34
|
+
* which is a reload, because the alternative is a page that silently stops updating:
|
|
35
|
+
*
|
|
36
|
+
* - **An update was already in flight.** `check` may only be called from `idle`.
|
|
37
|
+
* - **`check` rejected.** An update was found and could not be applied — usually a module that declines
|
|
38
|
+
* them, which is the ordinary cost of changing something react-refresh cannot patch.
|
|
39
|
+
* - **`check` resolved with null, or applied without moving the hash.** The chain of `*.hot-update.json`
|
|
40
|
+
* files leading to the target is broken — as it is after a dev-server restart, whose first act is to wipe
|
|
41
|
+
* the output directory an already-open tab would ask for. The hash cannot move from there, so retrying
|
|
42
|
+
* would re-request the same 404 for as long as the tab stays open.
|
|
43
|
+
*/
|
|
44
|
+
export declare function walkHotUpdates(hot: HotRuntime, currentHash: () => string, targetHash: () => string | undefined): Promise<ReloadReason | null>;
|
|
45
|
+
//# sourceMappingURL=hot-update.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hot-update.d.ts","sourceRoot":"","sources":["../../src/runtime/hot-update.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,2DAA2D;AAC3D,MAAM,WAAW,UAAU;IACzB;;;;;;OAMG;IACH,KAAK,CAAC,SAAS,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC;IACnE,MAAM,IAAI,MAAM,CAAC;CAClB;AAED,+FAA+F;AAC/F,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,cAAc,CAAC,GAAG,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,MAAM,EAAE,UAAU,EAAE,MAAM,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAanJ"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Walking the page from the build it is running to the build the dev server has announced. Its own module
|
|
3
|
+
* because every decision in it is about *giving up correctly*, which is worth testing on its own.
|
|
4
|
+
*
|
|
5
|
+
* Dev-only: the sole caller sits behind `import.meta.webpackHot`, which a production build replaces with
|
|
6
|
+
* `false`.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Advances the page to `targetHash()`, applying one build's worth of updates per round, and reports whether
|
|
10
|
+
* it got there.
|
|
11
|
+
*
|
|
12
|
+
* Both hashes are read through functions because both move underneath this loop: an applied update rewrites
|
|
13
|
+
* `__webpack_hash__`, and a save landing mid-walk moves the target further out. Reading them fresh each
|
|
14
|
+
* round is what lets one walk absorb a burst of saves.
|
|
15
|
+
*
|
|
16
|
+
* Returns `null` once the page is on the target build, or the reason it cannot get there — every one of
|
|
17
|
+
* which is a reload, because the alternative is a page that silently stops updating:
|
|
18
|
+
*
|
|
19
|
+
* - **An update was already in flight.** `check` may only be called from `idle`.
|
|
20
|
+
* - **`check` rejected.** An update was found and could not be applied — usually a module that declines
|
|
21
|
+
* them, which is the ordinary cost of changing something react-refresh cannot patch.
|
|
22
|
+
* - **`check` resolved with null, or applied without moving the hash.** The chain of `*.hot-update.json`
|
|
23
|
+
* files leading to the target is broken — as it is after a dev-server restart, whose first act is to wipe
|
|
24
|
+
* the output directory an already-open tab would ask for. The hash cannot move from there, so retrying
|
|
25
|
+
* would re-request the same 404 for as long as the tab stays open.
|
|
26
|
+
*/
|
|
27
|
+
export async function walkHotUpdates(hot, currentHash, targetHash) {
|
|
28
|
+
while (targetHash() !== undefined && targetHash() !== currentHash()) {
|
|
29
|
+
if (hot.status() !== 'idle')
|
|
30
|
+
return { reason: 'a hot update was already in flight' };
|
|
31
|
+
const before = currentHash();
|
|
32
|
+
let applied;
|
|
33
|
+
try {
|
|
34
|
+
applied = await hot.check(true);
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
return { reason: 'a hot update failed to apply', error };
|
|
38
|
+
}
|
|
39
|
+
if (applied === null || currentHash() === before)
|
|
40
|
+
return { reason: 'this build cannot be applied on top of the page' };
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=hot-update.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hot-update.js","sourceRoot":"","sources":["../../src/runtime/hot-update.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAqBH;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,GAAe,EAAE,WAAyB,EAAE,UAAoC;IACnH,OAAO,UAAU,EAAE,KAAK,SAAS,IAAI,UAAU,EAAE,KAAK,WAAW,EAAE,EAAE,CAAC;QACpE,IAAI,GAAG,CAAC,MAAM,EAAE,KAAK,MAAM;YAAE,OAAO,EAAE,MAAM,EAAE,oCAAoC,EAAE,CAAC;QACrF,MAAM,MAAM,GAAG,WAAW,EAAE,CAAC;QAC7B,IAAI,OAAsC,CAAC;QAC3C,IAAI,CAAC;YACH,OAAO,GAAG,MAAM,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAClC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,EAAE,MAAM,EAAE,8BAA8B,EAAE,KAAK,EAAE,CAAC;QAC3D,CAAC;QACD,IAAI,OAAO,KAAK,IAAI,IAAI,WAAW,EAAE,KAAK,MAAM;YAAE,OAAO,EAAE,MAAM,EAAE,iDAAiD,EAAE,CAAC;IACzH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC","sourcesContent":["/**\n * Walking the page from the build it is running to the build the dev server has announced. Its own module\n * because every decision in it is about *giving up correctly*, which is worth testing on its own.\n *\n * Dev-only: the sole caller sits behind `import.meta.webpackHot`, which a production build replaces with\n * `false`.\n */\n\n/** The slice of `import.meta.webpackHot` the walk uses. */\nexport interface HotRuntime {\n /**\n * Fetches the update manifest for the build the page is running and, with `autoApply`, applies it.\n *\n * Resolves with the updated module ids, or with **null** when the manifest 404s — which the runtime\n * treats as \"nothing to do\" rather than an error. Rejects only when an update was found and could not be\n * applied.\n */\n check(autoApply?: boolean): Promise<Array<string | number> | null>;\n status(): string;\n}\n\n/** Why the page has to be reloaded rather than patched — `error` only where one was thrown. */\nexport interface ReloadReason {\n reason: string;\n error?: unknown;\n}\n\n/**\n * Advances the page to `targetHash()`, applying one build's worth of updates per round, and reports whether\n * it got there.\n *\n * Both hashes are read through functions because both move underneath this loop: an applied update rewrites\n * `__webpack_hash__`, and a save landing mid-walk moves the target further out. Reading them fresh each\n * round is what lets one walk absorb a burst of saves.\n *\n * Returns `null` once the page is on the target build, or the reason it cannot get there — every one of\n * which is a reload, because the alternative is a page that silently stops updating:\n *\n * - **An update was already in flight.** `check` may only be called from `idle`.\n * - **`check` rejected.** An update was found and could not be applied — usually a module that declines\n * them, which is the ordinary cost of changing something react-refresh cannot patch.\n * - **`check` resolved with null, or applied without moving the hash.** The chain of `*.hot-update.json`\n * files leading to the target is broken — as it is after a dev-server restart, whose first act is to wipe\n * the output directory an already-open tab would ask for. The hash cannot move from there, so retrying\n * would re-request the same 404 for as long as the tab stays open.\n */\nexport async function walkHotUpdates(hot: HotRuntime, currentHash: () => string, targetHash: () => string | undefined): Promise<ReloadReason | null> {\n while (targetHash() !== undefined && targetHash() !== currentHash()) {\n if (hot.status() !== 'idle') return { reason: 'a hot update was already in flight' };\n const before = currentHash();\n let applied: Array<string | number> | null;\n try {\n applied = await hot.check(true);\n } catch (error) {\n return { reason: 'a hot update failed to apply', error };\n }\n if (applied === null || currentHash() === before) return { reason: 'this build cannot be applied on top of the page' };\n }\n return null;\n}\n"]}
|
|
@@ -2,9 +2,9 @@ import { type ReactNode } from 'react';
|
|
|
2
2
|
/**
|
|
3
3
|
* Imperative navigation actions, reached as `useNavigation().router`.
|
|
4
4
|
*
|
|
5
|
-
* `push` / `replace` / `refresh` are **soft** navigations: the new page's flight
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* `push` / `replace` / `refresh` are **soft** navigations: the new page's flight payload is fetched and
|
|
6
|
+
* applied in place, so client component state outside the changed subtree survives. Off-site hrefs fall
|
|
7
|
+
* back to a full load.
|
|
8
8
|
*
|
|
9
9
|
* @example
|
|
10
10
|
* ```tsx
|
|
@@ -27,9 +27,8 @@ export interface NavigationRouter {
|
|
|
27
27
|
/** The current location plus the {@link NavigationRouter}, as returned by {@link useNavigation}. */
|
|
28
28
|
export interface NavigationState {
|
|
29
29
|
/**
|
|
30
|
-
* The full current {@link URL}
|
|
31
|
-
*
|
|
32
|
-
* else; it is not written back to the address bar either.
|
|
30
|
+
* The full current {@link URL}. A fresh instance per navigation, so mutating it affects nothing else
|
|
31
|
+
* — it is not written back to the address bar.
|
|
33
32
|
*/
|
|
34
33
|
url: URL;
|
|
35
34
|
/** Matched route params for the current page, e.g. `{ id: '42' }` for `/profile/:id`. */
|
|
@@ -38,16 +37,14 @@ export interface NavigationState {
|
|
|
38
37
|
router: NavigationRouter;
|
|
39
38
|
}
|
|
40
39
|
/**
|
|
41
|
-
* Carries the live {@link NavigationRouter}
|
|
42
|
-
* to {@link RouterProvider}. Framework internal — read the router through
|
|
43
|
-
* {@link useNavigation} instead.
|
|
40
|
+
* Carries the live {@link NavigationRouter} from the hydration runtime down to {@link RouterProvider}.
|
|
44
41
|
*
|
|
45
42
|
* @internal
|
|
46
43
|
*/
|
|
47
44
|
export declare const RouterContext: import("react").Context<NavigationRouter>;
|
|
48
45
|
/**
|
|
49
|
-
* Publishes the per-render location and params
|
|
50
|
-
*
|
|
46
|
+
* Publishes the per-render location and params for {@link useNavigation} to read. The RSC entry wraps
|
|
47
|
+
* every page in one.
|
|
51
48
|
*
|
|
52
49
|
* @internal
|
|
53
50
|
*/
|
|
@@ -57,16 +54,14 @@ export declare function RouterProvider({ href, params, children }: {
|
|
|
57
54
|
children: ReactNode;
|
|
58
55
|
}): import("react").JSX.Element;
|
|
59
56
|
/**
|
|
60
|
-
* Reactive access to the current URL and programmatic navigation, in one hook.
|
|
57
|
+
* Reactive access to the current URL and programmatic navigation, in one hook. Call it from a
|
|
58
|
+
* `'use client'` component.
|
|
61
59
|
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* every navigation. The `router` sub-object holds the imperative actions plus a
|
|
66
|
-
* `pending` flag that is `true` while a client navigation is in flight.
|
|
60
|
+
* `url` and `params` are computed on the server and travel in the flight payload, so they are correct
|
|
61
|
+
* during SSR — no hydration flicker — and update on every navigation. `router` holds the imperative
|
|
62
|
+
* actions plus a `pending` flag, `true` while a soft navigation is in flight.
|
|
67
63
|
*
|
|
68
|
-
* Hooks can't run in a server component; read the same
|
|
69
|
-
* `getRequestContext()` (`@rshono/core/server`) instead.
|
|
64
|
+
* Hooks can't run in a server component; read the same data there from `getRequestContext()`.
|
|
70
65
|
*
|
|
71
66
|
* @example
|
|
72
67
|
* ```tsx
|