@rshono/core 1.0.0-rc.2 → 1.0.0-rc.21
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/LICENSE +21 -0
- package/README.md +348 -158
- package/bin/rshono.mjs +3 -4
- package/dist/builder/env-shadow-loader.cjs +123 -8
- package/dist/builder/page-entry-loader.cjs +28 -1
- package/dist/builder/page-files.d.ts +0 -1
- package/dist/builder/page-files.js +7 -3
- package/dist/builder/page-files.js.map +1 -1
- package/dist/builder/public-env.d.ts +7 -1
- package/dist/builder/public-env.js +7 -0
- package/dist/builder/public-env.js.map +1 -1
- package/dist/builder/react-versions.d.ts +12 -0
- package/dist/builder/react-versions.js +74 -0
- package/dist/builder/react-versions.js.map +1 -0
- package/dist/builder/rspack-config.d.ts +13 -4
- package/dist/builder/rspack-config.js +136 -53
- package/dist/builder/rspack-config.js.map +1 -1
- package/dist/builder/server-only-imports.d.ts +32 -0
- package/dist/builder/server-only-imports.js +51 -0
- package/dist/builder/server-only-imports.js.map +1 -0
- package/dist/cli/build.d.ts +2 -3
- package/dist/cli/build.js +51 -13
- package/dist/cli/build.js.map +1 -1
- package/dist/cli/dev.d.ts +2 -3
- package/dist/cli/dev.js +149 -45
- package/dist/cli/dev.js.map +1 -1
- package/dist/cli/exit.d.ts +12 -0
- package/dist/cli/exit.js +16 -0
- package/dist/cli/exit.js.map +1 -0
- package/dist/cli/index.d.ts +0 -1
- package/dist/cli/index.js +91 -30
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/start.d.ts +0 -1
- package/dist/cli/start.js +15 -18
- package/dist/cli/start.js.map +1 -1
- package/dist/config.d.ts +72 -78
- package/dist/config.js +17 -1
- package/dist/config.js.map +1 -1
- package/dist/deploy/aws-lambda/runtime.d.ts +4 -7
- 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 +9 -6
- package/dist/deploy/build-marker.js +8 -4
- package/dist/deploy/build-marker.js.map +1 -1
- package/dist/deploy/cloudflare/build.d.ts +0 -1
- package/dist/deploy/cloudflare/build.js +17 -13
- package/dist/deploy/cloudflare/build.js.map +1 -1
- package/dist/deploy/cloudflare/runtime.d.ts +2 -6
- package/dist/deploy/cloudflare/runtime.js +73 -68
- package/dist/deploy/cloudflare/runtime.js.map +1 -1
- package/dist/deploy/contract.d.ts +57 -41
- package/dist/deploy/contract.js.map +1 -1
- package/dist/deploy/filesystem.d.ts +3 -6
- package/dist/deploy/filesystem.js +23 -20
- package/dist/deploy/filesystem.js.map +1 -1
- package/dist/deploy/node/runtime.d.ts +5 -3
- package/dist/deploy/node/runtime.js +23 -9
- package/dist/deploy/node/runtime.js.map +1 -1
- package/dist/deploy/presets.d.ts +34 -30
- package/dist/deploy/presets.js +56 -44
- package/dist/deploy/presets.js.map +1 -1
- package/dist/deploy/public-paths.d.ts +42 -0
- package/dist/deploy/public-paths.js +66 -0
- package/dist/deploy/public-paths.js.map +1 -0
- package/dist/deploy/vercel/build.d.ts +0 -1
- package/dist/deploy/vercel/build.js +25 -16
- package/dist/deploy/vercel/build.js.map +1 -1
- package/dist/deploy/vercel/runtime.d.ts +10 -7
- package/dist/deploy/vercel/runtime.js +48 -11
- package/dist/deploy/vercel/runtime.js.map +1 -1
- package/dist/index.d.ts +19 -17
- package/dist/index.js +20 -10
- package/dist/index.js.map +1 -1
- package/dist/router.d.ts +212 -120
- package/dist/router.js +3 -8
- package/dist/router.js.map +1 -1
- package/dist/runtime/boundaries.d.ts +62 -48
- package/dist/runtime/boundaries.js +32 -34
- package/dist/runtime/boundaries.js.map +1 -1
- package/dist/runtime/client.d.ts +20 -11
- package/dist/runtime/client.js +20 -10
- package/dist/runtime/client.js.map +1 -1
- package/dist/runtime/context.d.ts +304 -123
- package/dist/runtime/context.js +377 -149
- package/dist/runtime/context.js.map +1 -1
- package/dist/runtime/control.d.ts +15 -1
- package/dist/runtime/control.js +24 -0
- package/dist/runtime/control.js.map +1 -1
- package/dist/runtime/dev-protocol.d.ts +4 -9
- package/dist/runtime/dev-protocol.js.map +1 -1
- package/dist/runtime/empty-server-app.d.ts +0 -1
- package/dist/runtime/entry.client.d.ts +4 -1
- package/dist/runtime/entry.client.js +455 -313
- package/dist/runtime/entry.client.js.map +1 -1
- package/dist/runtime/entry.rsc.d.ts +15 -7
- package/dist/runtime/entry.rsc.js +811 -252
- package/dist/runtime/entry.rsc.js.map +1 -1
- package/dist/runtime/entry.ssr.d.ts +27 -10
- package/dist/runtime/entry.ssr.js +30 -48
- package/dist/runtime/entry.ssr.js.map +1 -1
- package/dist/runtime/failure-document.d.ts +11 -0
- package/dist/runtime/failure-document.js +35 -0
- package/dist/runtime/failure-document.js.map +1 -0
- package/dist/runtime/flight-inject.d.ts +23 -0
- package/dist/runtime/flight-inject.js +403 -0
- package/dist/runtime/flight-inject.js.map +1 -0
- package/dist/runtime/hot-update.d.ts +44 -0
- package/dist/runtime/hot-update.js +44 -0
- package/dist/runtime/hot-update.js.map +1 -0
- package/dist/runtime/navigation.d.ts +46 -54
- package/dist/runtime/navigation.js +22 -63
- package/dist/runtime/navigation.js.map +1 -1
- package/dist/runtime/request.d.ts +42 -8
- package/dist/runtime/request.js +84 -7
- package/dist/runtime/request.js.map +1 -1
- package/dist/runtime/server.d.ts +20 -18
- package/dist/runtime/server.js +22 -17
- package/dist/runtime/server.js.map +1 -1
- package/dist/runtime/validate-entries.d.ts +77 -0
- package/dist/runtime/validate-entries.js +405 -0
- package/dist/runtime/validate-entries.js.map +1 -0
- package/dist/server/headers.d.ts +41 -15
- package/dist/server/headers.js +53 -24
- package/dist/server/headers.js.map +1 -1
- package/dist/server/load-config.d.ts +4 -5
- package/dist/server/load-config.js +22 -14
- package/dist/server/load-config.js.map +1 -1
- package/dist/server/load-env.d.ts +0 -1
- package/dist/server/prerendered.d.ts +88 -34
- package/dist/server/prerendered.js +184 -24
- package/dist/server/prerendered.js.map +1 -1
- package/dist/server/server-config.d.ts +35 -50
- package/dist/server/server-config.js +27 -74
- package/dist/server/server-config.js.map +1 -1
- package/dist/server/shutdown.d.ts +5 -4
- package/dist/server/shutdown.js +5 -3
- package/dist/server/shutdown.js.map +1 -1
- package/dist/server/ssg.d.ts +9 -7
- package/dist/server/ssg.js +209 -65
- package/dist/server/ssg.js.map +1 -1
- package/dist/server/static.d.ts +0 -1
- package/dist/server/static.js +7 -1
- package/dist/server/static.js.map +1 -1
- package/package.json +21 -23
- package/dist/builder/page-files.d.ts.map +0 -1
- package/dist/builder/public-env.d.ts.map +0 -1
- package/dist/builder/rspack-config.d.ts.map +0 -1
- package/dist/cli/build.d.ts.map +0 -1
- package/dist/cli/dev.d.ts.map +0 -1
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/cli/start.d.ts.map +0 -1
- package/dist/config.d.ts.map +0 -1
- package/dist/deploy/aws-lambda/runtime.d.ts.map +0 -1
- package/dist/deploy/build-marker.d.ts.map +0 -1
- package/dist/deploy/bun/runtime.d.ts +0 -11
- package/dist/deploy/bun/runtime.d.ts.map +0 -1
- package/dist/deploy/bun/runtime.js +0 -22
- package/dist/deploy/bun/runtime.js.map +0 -1
- package/dist/deploy/cloudflare/build.d.ts.map +0 -1
- package/dist/deploy/cloudflare/runtime.d.ts.map +0 -1
- package/dist/deploy/contract.d.ts.map +0 -1
- package/dist/deploy/deno/runtime.d.ts +0 -11
- package/dist/deploy/deno/runtime.d.ts.map +0 -1
- package/dist/deploy/deno/runtime.js +0 -16
- package/dist/deploy/deno/runtime.js.map +0 -1
- package/dist/deploy/filesystem.d.ts.map +0 -1
- package/dist/deploy/listen.d.ts +0 -20
- package/dist/deploy/listen.d.ts.map +0 -1
- package/dist/deploy/listen.js +0 -24
- package/dist/deploy/listen.js.map +0 -1
- package/dist/deploy/netlify/build.d.ts +0 -8
- package/dist/deploy/netlify/build.d.ts.map +0 -1
- package/dist/deploy/netlify/build.js +0 -52
- package/dist/deploy/netlify/build.js.map +0 -1
- package/dist/deploy/netlify/runtime.d.ts +0 -13
- package/dist/deploy/netlify/runtime.d.ts.map +0 -1
- package/dist/deploy/netlify/runtime.js +0 -24
- package/dist/deploy/netlify/runtime.js.map +0 -1
- package/dist/deploy/node/runtime.d.ts.map +0 -1
- package/dist/deploy/presets.d.ts.map +0 -1
- package/dist/deploy/vercel/build.d.ts.map +0 -1
- package/dist/deploy/vercel/runtime.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/router.d.ts.map +0 -1
- package/dist/runtime/boundaries.d.ts.map +0 -1
- package/dist/runtime/client.d.ts.map +0 -1
- package/dist/runtime/context.d.ts.map +0 -1
- package/dist/runtime/control.d.ts.map +0 -1
- package/dist/runtime/dev-protocol.d.ts.map +0 -1
- package/dist/runtime/empty-server-app.d.ts.map +0 -1
- package/dist/runtime/entry.client.d.ts.map +0 -1
- package/dist/runtime/entry.rsc.d.ts.map +0 -1
- package/dist/runtime/entry.ssr.d.ts.map +0 -1
- package/dist/runtime/navigation.d.ts.map +0 -1
- package/dist/runtime/request.d.ts.map +0 -1
- package/dist/runtime/server.d.ts.map +0 -1
- package/dist/server/compress.d.ts +0 -15
- package/dist/server/compress.d.ts.map +0 -1
- package/dist/server/compress.js +0 -76
- package/dist/server/compress.js.map +0 -1
- package/dist/server/headers.d.ts.map +0 -1
- package/dist/server/load-config.d.ts.map +0 -1
- package/dist/server/load-env.d.ts.map +0 -1
- package/dist/server/prerendered.d.ts.map +0 -1
- package/dist/server/server-config.d.ts.map +0 -1
- package/dist/server/shutdown.d.ts.map +0 -1
- package/dist/server/ssg.d.ts.map +0 -1
- package/dist/server/static.d.ts.map +0 -1
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// The baked config, not `process.env.NODE_ENV`: a deploy target need not have a `process`.
|
|
2
|
+
const isDev = __RSHONO_CONFIG__.isDev;
|
|
3
|
+
/** Escapes text going into HTML body content — a stack trace is untrusted input. */
|
|
4
|
+
function escapeHtml(text) {
|
|
5
|
+
return text.replace(/[&<>]/g, (char) => (char === '&' ? '&' : char === '<' ? '<' : '>'));
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The framework's last-resort 500 document, for a client that asked for HTML and has nothing better to be
|
|
9
|
+
* given: an app with no `error` page in its `routes.ts`, or one whose `error` page threw in its turn.
|
|
10
|
+
*
|
|
11
|
+
* Plain HTML with no client runtime and no stylesheet links. Both would have come from a render that failed,
|
|
12
|
+
* and there is no payload to hydrate from — attaching the runtime would tear the page down over this very
|
|
13
|
+
* message. A string rather than a component for the same reason: React is what failed.
|
|
14
|
+
*
|
|
15
|
+
* The detail is dev-only, matching how the app's `error` page redacts.
|
|
16
|
+
*/
|
|
17
|
+
export function failureDocument(error) {
|
|
18
|
+
const detail = isDev ? (error instanceof Error ? (error.stack ?? `${error.name}: ${error.message}`) : String(error)) : null;
|
|
19
|
+
return ('<!DOCTYPE html><html lang="en"><head><meta charset="utf-8">' +
|
|
20
|
+
'<meta name="viewport" content="width=device-width, initial-scale=1">' +
|
|
21
|
+
'<title>500 — Internal Server Error</title></head>' +
|
|
22
|
+
'<body style="margin:0;padding:2rem;font:16px/1.6 system-ui,-apple-system,sans-serif;color:#18181b">' +
|
|
23
|
+
'<h1 style="margin:0 0 .5rem;font-size:1.25rem">500 — Internal Server Error</h1>' +
|
|
24
|
+
'<p style="margin:0 0 1.5rem;color:#52525b">' +
|
|
25
|
+
(isDev
|
|
26
|
+
? 'The request failed and the app’s error page did not answer it: either src/routes.ts declares no <code>error</code> page, or that page threw in its turn. Both are reported on the server.'
|
|
27
|
+
: 'Something went wrong while handling this request. Please try again.') +
|
|
28
|
+
'</p>' +
|
|
29
|
+
(detail
|
|
30
|
+
? '<pre style="margin:0;padding:1rem;overflow:auto;background:#f4f4f5;border-left:3px solid #ef4444;' +
|
|
31
|
+
`font:13px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;white-space:pre-wrap;word-break:break-word">${escapeHtml(detail)}</pre>`
|
|
32
|
+
: '') +
|
|
33
|
+
'</body></html>');
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=failure-document.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"failure-document.js","sourceRoot":"","sources":["../../src/runtime/failure-document.ts"],"names":[],"mappings":"AAAA,2FAA2F;AAC3F,MAAM,KAAK,GAAG,iBAAiB,CAAC,KAAK,CAAC;AAEtC,oFAAoF;AACpF,SAAS,UAAU,CAAC,IAAY;IAC9B,OAAO,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;AACrG,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,eAAe,CAAC,KAAc;IAC5C,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,IAAI,GAAG,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAC5H,OAAO,CACL,6DAA6D;QAC7D,sEAAsE;QACtE,mDAAmD;QACnD,qGAAqG;QACrG,iFAAiF;QACjF,6CAA6C;QAC7C,CAAC,KAAK;YACJ,CAAC,CAAC,2LAA2L;YAC7L,CAAC,CAAC,qEAAqE,CAAC;QAC1E,MAAM;QACN,CAAC,MAAM;YACL,CAAC,CAAC,mGAAmG;gBACnG,yGAAyG,UAAU,CAAC,MAAM,CAAC,QAAQ;YACrI,CAAC,CAAC,EAAE,CAAC;QACP,gBAAgB,CACjB,CAAC;AACJ,CAAC","sourcesContent":["// 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 framework's last-resort 500 document, for a client that asked for HTML and has nothing better to be\n * given: an app with no `error` page in its `routes.ts`, or one whose `error` page threw in its turn.\n *\n * Plain HTML with no client runtime and no stylesheet links. Both would have come from a render that failed,\n * and there is no payload to hydrate from — attaching the runtime would tear the page down over this very\n * message. A string rather than a component for the same reason: React is what failed.\n *\n * The detail is dev-only, matching how the app's `error` page redacts.\n */\nexport function failureDocument(error: unknown): string {\n const detail = isDev ? (error instanceof Error ? (error.stack ?? `${error.name}: ${error.message}`) : String(error)) : null;\n return (\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 ? 'The request failed and the app’s error page did not answer it: either src/routes.ts declares no <code>error</code> page, or that page threw in its turn. Both are reported on the server.'\n : 'Something went wrong while handling this request. 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 );\n}\n"]}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
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
|
+
*
|
|
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.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
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.
|
|
13
|
+
*
|
|
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.
|
|
16
|
+
*/
|
|
17
|
+
export type CancellableTransformer<I, O> = Transformer<I, O> & {
|
|
18
|
+
cancel?: (reason?: unknown) => void;
|
|
19
|
+
};
|
|
20
|
+
export declare function injectFlightPayload(rscStream: ReadableStream<Uint8Array>, options?: {
|
|
21
|
+
nonce?: string;
|
|
22
|
+
onDone?: () => void;
|
|
23
|
+
}): ReadableWritablePair<Uint8Array, Uint8Array>;
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
/**
|
|
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
|
+
*
|
|
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.
|
|
9
|
+
*/
|
|
10
|
+
const encoder = new TextEncoder();
|
|
11
|
+
/** What React closes an `<html>` document with, and what this module re-emits after the last payload script. */
|
|
12
|
+
const TRAILER = '</body></html>';
|
|
13
|
+
const TRAILER_BYTES = encoder.encode(TRAILER);
|
|
14
|
+
const hasSetImmediate = typeof setImmediate === 'function';
|
|
15
|
+
const schedule = hasSetImmediate ? setImmediate : (fn) => setTimeout(fn, 0);
|
|
16
|
+
const unschedule = (handle) => {
|
|
17
|
+
if (hasSetImmediate)
|
|
18
|
+
clearImmediate(handle);
|
|
19
|
+
else
|
|
20
|
+
clearTimeout(handle);
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
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/`.
|
|
26
|
+
*/
|
|
27
|
+
function escapeScript(script) {
|
|
28
|
+
return script.includes('<') ? script.replace(/<!--/g, '<\\!--').replace(/<\/(script)/gi, '</\\$1') : script;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
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.
|
|
33
|
+
*/
|
|
34
|
+
function latin1(chunk) {
|
|
35
|
+
let out = '';
|
|
36
|
+
for (let at = 0; at < chunk.length; at += 8192)
|
|
37
|
+
out += String.fromCharCode(...chunk.subarray(at, at + 8192));
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
/** Whether `buffer`'s first `length` bytes end with the document trailer. */
|
|
41
|
+
function endsWithTrailer(buffer, length) {
|
|
42
|
+
if (length < TRAILER_BYTES.length)
|
|
43
|
+
return false;
|
|
44
|
+
const from = length - TRAILER_BYTES.length;
|
|
45
|
+
for (let i = 0; i < TRAILER_BYTES.length; i++) {
|
|
46
|
+
if (buffer[from + i] !== TRAILER_BYTES[i])
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* How many of `buffer`'s last bytes could still turn into the document trailer — the length of the longest
|
|
53
|
+
* suffix of `buffer[0..length)` that is a prefix of it, `TRAILER_BYTES.length` for the whole thing.
|
|
54
|
+
*
|
|
55
|
+
* At most 14 bytes are ever held back, and in practice 0: a React flush ends with a closed tag, not with the
|
|
56
|
+
* start of one. Cheaper than it looks, too — the first comparison rejects every suffix whose first byte is
|
|
57
|
+
* not `<`.
|
|
58
|
+
*/
|
|
59
|
+
function trailerPrefixLength(buffer, length) {
|
|
60
|
+
candidate: for (let take = Math.min(length, TRAILER_BYTES.length); take > 0; take--) {
|
|
61
|
+
const from = length - take;
|
|
62
|
+
for (let i = 0; i < take; i++)
|
|
63
|
+
if (buffer[from + i] !== TRAILER_BYTES[i])
|
|
64
|
+
continue candidate;
|
|
65
|
+
return take;
|
|
66
|
+
}
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The characters a CSP nonce may be made of: base64 and base64url, which is what every generator of one emits
|
|
71
|
+
* — Hono's `secureHeaders()`, the only source the framework reads, produces base64 of 16 random bytes.
|
|
72
|
+
*
|
|
73
|
+
* The tag below is built by hand rather than by React, so this is the one attribute value in a rendered
|
|
74
|
+
* document that nothing else escapes. The value is not attacker-controlled today, but the framework does not
|
|
75
|
+
* own where it comes from: `c.get('secureHeadersNonce')` is an ordinary context variable that any middleware
|
|
76
|
+
* can set, and a nonce carrying a `"` would close the attribute and open a script-injection point in the
|
|
77
|
+
* document. Anything outside this set is dropped rather than escaped, because a value made of other characters
|
|
78
|
+
* is not a nonce at all: a page whose payload scripts the policy then refuses is the visible failure to have,
|
|
79
|
+
* where an escaped garbage nonce would be a silent one.
|
|
80
|
+
*/
|
|
81
|
+
const NONCE_CHARS = /^[A-Za-z0-9+/=_-]+$/;
|
|
82
|
+
export function injectFlightPayload(rscStream, options = {}) {
|
|
83
|
+
const { nonce, onDone } = options;
|
|
84
|
+
const safeNonce = nonce !== undefined && NONCE_CHARS.test(nonce) ? nonce : undefined;
|
|
85
|
+
const scriptOpen = `<script${safeNonce ? ` nonce="${safeNonce}"` : ''}>(self.__FLIGHT_DATA||=[]).push(`;
|
|
86
|
+
const scriptClose = ')</script>';
|
|
87
|
+
const { promise: flightWritten, resolve: flightDone } = Promise.withResolvers();
|
|
88
|
+
let startedFlight = false;
|
|
89
|
+
const batch = [];
|
|
90
|
+
let boundary = null;
|
|
91
|
+
/**
|
|
92
|
+
* The tail of the last batch that could still be the start of the document trailer, carried into the next
|
|
93
|
+
* one. See {@link emitBatch}.
|
|
94
|
+
*/
|
|
95
|
+
let carry = null;
|
|
96
|
+
/**
|
|
97
|
+
* Set once the consumer has gone away, so nothing tries to enqueue into a readable that cannot take it.
|
|
98
|
+
*
|
|
99
|
+
* The `cancel` hook alone cannot set this: per the Streams standard, cancelling the readable after the
|
|
100
|
+
* close algorithm has started skips the transformer's `cancel` entirely — and `flush` awaiting the whole
|
|
101
|
+
* payload is precisely that window. So a failed enqueue counts as the signal too.
|
|
102
|
+
*/
|
|
103
|
+
let cancelled = false;
|
|
104
|
+
/** Held so {@link cancelled} can release the teed RSC branch rather than leaving it to be pumped. */
|
|
105
|
+
let flightReader = null;
|
|
106
|
+
/**
|
|
107
|
+
* One permit per chunk the consumer has asked for: the backpressure a `TransformStream` alone does not give
|
|
108
|
+
* this module.
|
|
109
|
+
*
|
|
110
|
+
* A transform's writable side parks a write while its readable's queue is over the mark, which covers the
|
|
111
|
+
* HTML — but only once something has been enqueued, and `transform` defers that to a macrotask. So a
|
|
112
|
+
* producer that writes a whole document without ever yielding the microtask queue is admitted in full, and
|
|
113
|
+
* {@link writeFlight}, which pumps the payload into the same controller from a detached promise, is never
|
|
114
|
+
* gated at all: measured, a consumer that read one chunk and stalled still pulled all 500 chunks of a test
|
|
115
|
+
* payload into the queue, where this brings it down to 3. `controller.desiredSize` is no help — a transform
|
|
116
|
+
* readable's high-water mark is 0, so it is never positive.
|
|
117
|
+
*
|
|
118
|
+
* The wrapper readable this function returns is the missing signal: its `pull` runs exactly when the
|
|
119
|
+
* consumer wants another chunk, and releases one permit. Both producers — the HTML batcher and the payload
|
|
120
|
+
* pump — take one before they enqueue, so a stalled client parks React instead of filling the process. The
|
|
121
|
+
* Streams standard calls `pull` again only once the previous call has settled, and a call only settles once
|
|
122
|
+
* a chunk has arrived, so permits are handed out one at a time and the queue is bounded at a chunk.
|
|
123
|
+
*
|
|
124
|
+
* What is left buffered is one React flush, because a flush has to leave here as a single chunk (see
|
|
125
|
+
* {@link emitBatch}) — the same bound React itself holds while it builds one. The two enqueues in `flush`
|
|
126
|
+
* are ungated for a related reason: the response is over by then, and parking its last two chunks on a
|
|
127
|
+
* permit would only add a way for it not to end. They are also why the count can reach two at the very
|
|
128
|
+
* end — an ungated enqueue settles a pull without spending its permit — which is harmless, there being
|
|
129
|
+
* nothing left to produce.
|
|
130
|
+
*
|
|
131
|
+
* The cost is one extra stream hop and a microtask per chunk: 3.6ms → 4.0ms to push a 187 kB page through
|
|
132
|
+
* this module with nothing else in the way, which is not the shape of a real response, where the socket
|
|
133
|
+
* dominates by orders of magnitude.
|
|
134
|
+
*/
|
|
135
|
+
let permits = 0;
|
|
136
|
+
const waiting = [];
|
|
137
|
+
/** Returns nothing when a permit was already free, so the common case costs no microtask. */
|
|
138
|
+
function takePermit() {
|
|
139
|
+
// Nothing will release another permit once the consumer is gone, and there is nothing left to protect:
|
|
140
|
+
// the caller goes on to a failing enqueue, which is what tears the pipeline down.
|
|
141
|
+
if (cancelled)
|
|
142
|
+
return;
|
|
143
|
+
if (permits > 0) {
|
|
144
|
+
permits--;
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
return new Promise((resolve) => waiting.push(resolve));
|
|
148
|
+
}
|
|
149
|
+
function releasePermit() {
|
|
150
|
+
const next = waiting.shift();
|
|
151
|
+
if (next)
|
|
152
|
+
next();
|
|
153
|
+
else
|
|
154
|
+
permits++;
|
|
155
|
+
}
|
|
156
|
+
/** Fires `onDone` at most once, however the response ended — both cancel paths can reach it. */
|
|
157
|
+
let ended = false;
|
|
158
|
+
function reportDone() {
|
|
159
|
+
if (ended)
|
|
160
|
+
return;
|
|
161
|
+
ended = true;
|
|
162
|
+
onDone?.();
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* The consumer has gone away: stop producing, release everything the request was holding, and unpark both
|
|
166
|
+
* producers so they see {@link cancelled} rather than a permit that will never come.
|
|
167
|
+
*
|
|
168
|
+
* Called from the wrapper readable's `cancel`, which is reliable, and from the transformer's, which the
|
|
169
|
+
* standard skips once the close algorithm has started — so it has to be idempotent.
|
|
170
|
+
*/
|
|
171
|
+
function teardown(reason) {
|
|
172
|
+
cancelled = true;
|
|
173
|
+
if (boundary) {
|
|
174
|
+
unschedule(boundary);
|
|
175
|
+
boundary = null;
|
|
176
|
+
}
|
|
177
|
+
batch.length = 0;
|
|
178
|
+
carry = null;
|
|
179
|
+
for (const resolve of waiting.splice(0))
|
|
180
|
+
resolve();
|
|
181
|
+
// Otherwise the teed RSC branch keeps being pumped for a response nobody will read, and the tee's
|
|
182
|
+
// other half buffers every chunk waiting for this one to catch up.
|
|
183
|
+
flightReader?.cancel(reason).catch(() => { });
|
|
184
|
+
// Unparks `flush` if it is waiting on a payload that will now never arrive.
|
|
185
|
+
flightDone();
|
|
186
|
+
reportDone();
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Emits the HTML buffered since the last boundary, holding back the document trailer for `flush` to re-emit
|
|
190
|
+
* after the payload scripts.
|
|
191
|
+
*
|
|
192
|
+
* The batch is joined before the trailer is looked for, so one React split across two views is still found.
|
|
193
|
+
* A trailer split across two *batches* is not a shape React produces — it writes its final flush in one
|
|
194
|
+
* synchronous run — but this injector exists because `rsc-html-stream` made a narrower version of that same
|
|
195
|
+
* assumption and was wrong (see the module header), so anything that could still become a trailer is held
|
|
196
|
+
* back in {@link carry} rather than assumed not to be. Only `final`, the call from `flush`, may emit such a
|
|
197
|
+
* tail: by then there is no next batch for it to complete.
|
|
198
|
+
*
|
|
199
|
+
* A tail that never does complete therefore leaves after the payload scripts rather than before them, which
|
|
200
|
+
* is the deliberate half of the trade: releasing it the moment a script wants to go out is the very bug this
|
|
201
|
+
* guards, because the next batch may be the rest of the trailer. It costs at most 13 bytes of a truncated
|
|
202
|
+
* document arriving late, still inside `<body>`.
|
|
203
|
+
*/
|
|
204
|
+
function emitBatch(controller, final = false) {
|
|
205
|
+
boundary = null;
|
|
206
|
+
let total = carry?.byteLength ?? 0;
|
|
207
|
+
for (const chunk of batch)
|
|
208
|
+
total += chunk.byteLength;
|
|
209
|
+
if (total === 0) {
|
|
210
|
+
batch.length = 0;
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const joined = new Uint8Array(total);
|
|
214
|
+
let at = 0;
|
|
215
|
+
if (carry) {
|
|
216
|
+
joined.set(carry, at);
|
|
217
|
+
at += carry.byteLength;
|
|
218
|
+
carry = null;
|
|
219
|
+
}
|
|
220
|
+
for (const chunk of batch) {
|
|
221
|
+
joined.set(chunk, at);
|
|
222
|
+
at += chunk.byteLength;
|
|
223
|
+
}
|
|
224
|
+
batch.length = 0;
|
|
225
|
+
const held = final ? (endsWithTrailer(joined, total) ? TRAILER_BYTES.length : 0) : trailerPrefixLength(joined, total);
|
|
226
|
+
const end = total - held;
|
|
227
|
+
// Copied rather than a `subarray`, which would keep the whole joined flush alive for 14 bytes.
|
|
228
|
+
if (held > 0 && !final)
|
|
229
|
+
carry = joined.slice(end);
|
|
230
|
+
if (end > 0)
|
|
231
|
+
controller.enqueue(joined.subarray(0, end));
|
|
232
|
+
}
|
|
233
|
+
async function writeFlight(controller) {
|
|
234
|
+
const reader = (flightReader = rscStream.getReader());
|
|
235
|
+
// One chunk at a time, deliberately *not* `{ stream: true }`: anything that is not valid UTF-8 on its
|
|
236
|
+
// own — a binary row's bytes, or a chunk that split a multi-byte character — throws, and the catch below
|
|
237
|
+
// re-encodes that whole chunk byte-exactly.
|
|
238
|
+
//
|
|
239
|
+
// Streaming is what makes that fallback wrong. A chunk ending mid-character leaves its lead bytes inside
|
|
240
|
+
// the decoder to be emitted with the next chunk; when the next chunk is a binary row and throws, those
|
|
241
|
+
// bytes are in neither `decode()`'s return nor `value`, and the client reassembles a payload short by
|
|
242
|
+
// them. It also left the end-of-stream flush able to throw from outside any `try`.
|
|
243
|
+
//
|
|
244
|
+
// `ignoreBOM` because a per-chunk decode re-runs the BOM check on every call: without it any chunk whose
|
|
245
|
+
// first three bytes are EF BB BF would silently lose them.
|
|
246
|
+
const decoder = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });
|
|
247
|
+
const push = async (literal) => {
|
|
248
|
+
const permit = takePermit();
|
|
249
|
+
if (permit)
|
|
250
|
+
await permit;
|
|
251
|
+
if (cancelled)
|
|
252
|
+
return;
|
|
253
|
+
controller.enqueue(encoder.encode(scriptOpen + literal + scriptClose));
|
|
254
|
+
};
|
|
255
|
+
for (;;) {
|
|
256
|
+
if (cancelled)
|
|
257
|
+
return;
|
|
258
|
+
const { done, value } = await reader.read();
|
|
259
|
+
if (done)
|
|
260
|
+
break;
|
|
261
|
+
// Only the decode is guarded: a `push` inside the same `try` would answer a dead controller by
|
|
262
|
+
// re-encoding the chunk and enqueueing it again.
|
|
263
|
+
let literal;
|
|
264
|
+
try {
|
|
265
|
+
literal = escapeScript(JSON.stringify(decoder.decode(value)));
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
literal = `Uint8Array.from(atob(${JSON.stringify(btoa(latin1(value)))}), m => m.codePointAt(0))`;
|
|
269
|
+
}
|
|
270
|
+
if (cancelled)
|
|
271
|
+
return;
|
|
272
|
+
// A failed enqueue means the consumer is gone: release the RSC branch so `flush` unparks now rather
|
|
273
|
+
// than whenever the payload would have ended on its own.
|
|
274
|
+
try {
|
|
275
|
+
await push(literal);
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
cancelled = true;
|
|
279
|
+
reader.cancel().catch(() => { });
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
// No end-of-stream flush: a non-streaming decoder holds nothing between calls, so there is nothing left
|
|
284
|
+
// to emit — and the flush this replaces could throw, erroring the response mid-document. See above.
|
|
285
|
+
}
|
|
286
|
+
const transformer = {
|
|
287
|
+
async transform(chunk, controller) {
|
|
288
|
+
if (boundary) {
|
|
289
|
+
batch.push(chunk);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
// The permit is taken once per *batch*, not once per chunk: React writes a whole flush in one microtask
|
|
293
|
+
// cascade and the flush has to leave here as one chunk (see `emitBatch`), so a per-chunk gate would
|
|
294
|
+
// hand the consumer one chunk per read instead. `transform` is never re-entered concurrently — the
|
|
295
|
+
// writable side serializes it on the promise this returns — so nothing else can claim the batch in
|
|
296
|
+
// between.
|
|
297
|
+
const permit = takePermit();
|
|
298
|
+
if (permit)
|
|
299
|
+
await permit;
|
|
300
|
+
batch.push(chunk);
|
|
301
|
+
// A macrotask, not a microtask: React writes a whole flush in one synchronous run but `pipeThrough`
|
|
302
|
+
// delivers it one microtask at a time, and a script injected between two chunks lands inside a tag.
|
|
303
|
+
boundary = schedule(() => {
|
|
304
|
+
try {
|
|
305
|
+
emitBatch(controller);
|
|
306
|
+
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
// A dead consumer, all but always. `teardown` rather than `flightDone` alone: a payload pump parked
|
|
309
|
+
// on a permit has to be unparked too, and the RSC branch it holds released.
|
|
310
|
+
controller.error(error);
|
|
311
|
+
teardown(error);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
if (!startedFlight) {
|
|
315
|
+
startedFlight = true;
|
|
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
|
+
}
|
|
322
|
+
});
|
|
323
|
+
},
|
|
324
|
+
async flush(controller) {
|
|
325
|
+
// Both of these have to happen *before* the await, and in this order.
|
|
326
|
+
try {
|
|
327
|
+
// Anything still batched — or carried — belongs ahead of the payload scripts, which sit at the end of
|
|
328
|
+
// `<body>`. Called unconditionally: a `carry` outlives its boundary, and an empty batch returns early.
|
|
329
|
+
if (boundary)
|
|
330
|
+
unschedule(boundary);
|
|
331
|
+
emitBatch(controller, true);
|
|
332
|
+
// A writable side that closed without ever reaching `transform` — an HTML stream with no chunks at
|
|
333
|
+
// all — leaves nothing to have started the payload, and `flightDone` is only ever called from that
|
|
334
|
+
// chain or from `cancel`. Without this the await below parks on a promise nothing will settle and the
|
|
335
|
+
// response never ends. `cancelled` short-circuits it: there is nowhere to write the payload to.
|
|
336
|
+
if (!startedFlight) {
|
|
337
|
+
startedFlight = true;
|
|
338
|
+
if (cancelled)
|
|
339
|
+
flightDone();
|
|
340
|
+
else
|
|
341
|
+
void writeFlight(controller)
|
|
342
|
+
.catch((error) => controller.error(error))
|
|
343
|
+
.then(flightDone);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
347
|
+
// The consumer went away while the batch was being emitted, so there is nothing left to write to and
|
|
348
|
+
// nothing to wait for. Settled explicitly rather than left dangling, so the payload chain is released.
|
|
349
|
+
teardown(error);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
await flightWritten;
|
|
353
|
+
// That await spans the whole payload, and the consumer can go away inside it — with `cancel` skipped
|
|
354
|
+
// (see `cancelled`), a throwing enqueue is the only signal. Unguarded it rejects `flush`, which
|
|
355
|
+
// nothing owns and which surfaces as an unhandled rejection.
|
|
356
|
+
try {
|
|
357
|
+
if (!cancelled)
|
|
358
|
+
controller.enqueue(encoder.encode(TRAILER));
|
|
359
|
+
}
|
|
360
|
+
catch {
|
|
361
|
+
// Nowhere left to put the trailer. A response the client abandoned is not a fault.
|
|
362
|
+
cancelled = true;
|
|
363
|
+
flightReader?.cancel().catch(() => { });
|
|
364
|
+
}
|
|
365
|
+
finally {
|
|
366
|
+
// A `finally` because `onDone` releases the abort forwarder in `renderComponent`, however this ended.
|
|
367
|
+
reportDone();
|
|
368
|
+
}
|
|
369
|
+
},
|
|
370
|
+
cancel(reason) {
|
|
371
|
+
teardown(reason);
|
|
372
|
+
},
|
|
373
|
+
};
|
|
374
|
+
const inner = new TransformStream(transformer);
|
|
375
|
+
const innerReader = inner.readable.getReader();
|
|
376
|
+
/**
|
|
377
|
+
* A pull-driven wrapper around the transform's readable, and the only reason this function does not simply
|
|
378
|
+
* return the transform: `pull` runs once per chunk the consumer takes, which is the demand signal the two
|
|
379
|
+
* producers park on (see {@link takePermit}). It is also the one cancel notification the standard never
|
|
380
|
+
* skips, which is what makes {@link teardown} reliable.
|
|
381
|
+
*/
|
|
382
|
+
const readable = new ReadableStream({
|
|
383
|
+
async pull(controller) {
|
|
384
|
+
releasePermit();
|
|
385
|
+
const { done, value } = await innerReader.read();
|
|
386
|
+
try {
|
|
387
|
+
if (done)
|
|
388
|
+
controller.close();
|
|
389
|
+
else
|
|
390
|
+
controller.enqueue(value);
|
|
391
|
+
}
|
|
392
|
+
catch {
|
|
393
|
+
// Cancelled while this pull was in flight. `teardown` has already run; there is nobody to hand it to.
|
|
394
|
+
}
|
|
395
|
+
},
|
|
396
|
+
cancel(reason) {
|
|
397
|
+
teardown(reason);
|
|
398
|
+
return innerReader.cancel(reason);
|
|
399
|
+
},
|
|
400
|
+
});
|
|
401
|
+
return { readable, writable: inner.writable };
|
|
402
|
+
}
|
|
403
|
+
//# sourceMappingURL=flight-inject.js.map
|
|
@@ -0,0 +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"]}
|
|
@@ -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
|
+
/** 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>;
|
|
@@ -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"]}
|