@rshono/core 1.0.0-rc.2 → 1.0.0-rc.4
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 +45 -29
- package/dist/builder/rspack-config.d.ts +3 -3
- package/dist/builder/rspack-config.js +3 -3
- package/dist/builder/rspack-config.js.map +1 -1
- package/dist/cli/build.d.ts +2 -2
- package/dist/cli/build.js.map +1 -1
- package/dist/cli/dev.d.ts +2 -2
- package/dist/cli/dev.js.map +1 -1
- package/dist/cli/index.js +11 -10
- package/dist/cli/index.js.map +1 -1
- package/dist/config.d.ts +4 -17
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/deploy/cloudflare/build.js +1 -1
- package/dist/deploy/cloudflare/build.js.map +1 -1
- package/dist/deploy/cloudflare/runtime.d.ts.map +1 -1
- package/dist/deploy/cloudflare/runtime.js +7 -37
- package/dist/deploy/cloudflare/runtime.js.map +1 -1
- package/dist/deploy/contract.d.ts +14 -13
- package/dist/deploy/contract.d.ts.map +1 -1
- package/dist/deploy/contract.js.map +1 -1
- package/dist/deploy/filesystem.d.ts +1 -1
- package/dist/deploy/filesystem.d.ts.map +1 -1
- package/dist/deploy/filesystem.js +1 -3
- package/dist/deploy/filesystem.js.map +1 -1
- package/dist/deploy/node/runtime.d.ts +6 -2
- package/dist/deploy/node/runtime.d.ts.map +1 -1
- package/dist/deploy/node/runtime.js +30 -5
- package/dist/deploy/node/runtime.js.map +1 -1
- package/dist/deploy/presets.d.ts +5 -1
- package/dist/deploy/presets.d.ts.map +1 -1
- package/dist/deploy/presets.js +15 -28
- package/dist/deploy/presets.js.map +1 -1
- package/dist/deploy/vercel/runtime.d.ts.map +1 -1
- package/dist/deploy/vercel/runtime.js +0 -3
- package/dist/deploy/vercel/runtime.js.map +1 -1
- package/dist/index.d.ts +4 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -3
- package/dist/index.js.map +1 -1
- package/dist/router.d.ts +14 -15
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +5 -6
- package/dist/router.js.map +1 -1
- package/dist/runtime/client.d.ts +5 -6
- package/dist/runtime/client.d.ts.map +1 -1
- package/dist/runtime/client.js +5 -6
- package/dist/runtime/client.js.map +1 -1
- package/dist/runtime/context.d.ts +36 -40
- package/dist/runtime/context.d.ts.map +1 -1
- package/dist/runtime/context.js +41 -53
- package/dist/runtime/context.js.map +1 -1
- package/dist/runtime/entry.client.d.ts +4 -0
- package/dist/runtime/entry.client.d.ts.map +1 -1
- package/dist/runtime/entry.client.js +161 -147
- package/dist/runtime/entry.client.js.map +1 -1
- package/dist/runtime/entry.rsc.d.ts.map +1 -1
- package/dist/runtime/entry.rsc.js +80 -63
- package/dist/runtime/entry.rsc.js.map +1 -1
- package/dist/runtime/entry.ssr.d.ts +9 -1
- package/dist/runtime/entry.ssr.d.ts.map +1 -1
- package/dist/runtime/entry.ssr.js +36 -18
- package/dist/runtime/entry.ssr.js.map +1 -1
- package/dist/runtime/flight-inject.d.ts +31 -0
- package/dist/runtime/flight-inject.d.ts.map +1 -0
- package/dist/runtime/flight-inject.js +221 -0
- package/dist/runtime/flight-inject.js.map +1 -0
- package/dist/runtime/navigation.d.ts +10 -39
- package/dist/runtime/navigation.d.ts.map +1 -1
- package/dist/runtime/navigation.js +7 -53
- package/dist/runtime/navigation.js.map +1 -1
- package/dist/runtime/request.d.ts +6 -0
- package/dist/runtime/request.d.ts.map +1 -1
- package/dist/runtime/request.js +8 -0
- package/dist/runtime/request.js.map +1 -1
- package/dist/runtime/server.d.ts +2 -2
- package/dist/runtime/server.d.ts.map +1 -1
- package/dist/runtime/server.js +2 -2
- package/dist/runtime/server.js.map +1 -1
- package/dist/server/headers.d.ts +9 -9
- package/dist/server/headers.js +9 -9
- 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 +22 -11
- package/dist/server/load-config.js.map +1 -1
- package/dist/server/prerendered.d.ts +52 -9
- package/dist/server/prerendered.d.ts.map +1 -1
- package/dist/server/prerendered.js +57 -0
- package/dist/server/prerendered.js.map +1 -1
- package/dist/server/server-config.d.ts +11 -18
- package/dist/server/server-config.d.ts.map +1 -1
- package/dist/server/server-config.js +9 -12
- package/dist/server/server-config.js.map +1 -1
- package/dist/server/ssg.d.ts +2 -2
- package/dist/server/ssg.d.ts.map +1 -1
- package/dist/server/ssg.js +11 -23
- package/dist/server/ssg.js.map +1 -1
- package/package.json +13 -16
- 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/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/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/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
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
import { jsx as _jsx
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { renderToReadableStream } from 'react-dom/server';
|
|
4
4
|
import { createFromReadableStream } from 'react-server-dom-rspack/client';
|
|
5
|
-
import { injectRSCPayload } from 'rsc-html-stream/server';
|
|
6
5
|
import { isControlDigest } from './control.js';
|
|
6
|
+
import { injectFlightPayload } from './flight-inject.js';
|
|
7
7
|
// From the baked config, not `process.env.NODE_ENV`: this is a property of the build, and a deploy
|
|
8
8
|
// target need not have a `process` at all.
|
|
9
9
|
const isDev = __RSHONO_CONFIG__.isDev;
|
|
10
|
+
/** Escapes text going into HTML body content — a stack trace is untrusted input. */
|
|
11
|
+
function escapeHtml(text) {
|
|
12
|
+
return text.replace(/[&<>]/g, (char) => (char === '&' ? '&' : char === '<' ? '<' : '>'));
|
|
13
|
+
}
|
|
10
14
|
/**
|
|
11
15
|
* The last-resort 500 document, for when SSR fails before a single byte of the real shell was sent.
|
|
12
16
|
*
|
|
@@ -15,23 +19,37 @@ const isDev = __RSHONO_CONFIG__.isDev;
|
|
|
15
19
|
* `document` — would tear the page down, blanking the very message being rendered here. Styling is
|
|
16
20
|
* inline because the page's stylesheet links were part of the render that just failed.
|
|
17
21
|
*
|
|
22
|
+
* A string rather than a component, for the same reason: React is what just failed, and rendering
|
|
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
|
+
*
|
|
18
26
|
* The detail is dev-only. In production this stays a generic message, matching how the `error` page
|
|
19
27
|
* from routes.ts redacts.
|
|
20
28
|
*/
|
|
21
|
-
function
|
|
29
|
+
function failureDocument(error) {
|
|
22
30
|
const detail = isDev ? (error instanceof Error ? (error.stack ?? `${error.name}: ${error.message}`) : String(error)) : null;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
const html = '<!DOCTYPE html><html lang="en"><head><meta charset="utf-8">' +
|
|
32
|
+
'<meta name="viewport" content="width=device-width, initial-scale=1">' +
|
|
33
|
+
'<title>500 — Internal Server Error</title></head>' +
|
|
34
|
+
'<body style="margin:0;padding:2rem;font:16px/1.6 system-ui,-apple-system,sans-serif;color:#18181b">' +
|
|
35
|
+
'<h1 style="margin:0 0 .5rem;font-size:1.25rem">500 — Internal Server Error</h1>' +
|
|
36
|
+
'<p style="margin:0 0 1.5rem;color:#52525b">' +
|
|
37
|
+
(isDev
|
|
38
|
+
? 'Server-side rendering failed before the page shell could be sent, so the app’s error page could not be reached either.'
|
|
39
|
+
: 'Something went wrong while rendering this page. Please try again.') +
|
|
40
|
+
'</p>' +
|
|
41
|
+
(detail
|
|
42
|
+
? '<pre style="margin:0;padding:1rem;overflow:auto;background:#f4f4f5;border-left:3px solid #ef4444;' +
|
|
43
|
+
`font:13px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;white-space:pre-wrap;word-break:break-word">${escapeHtml(detail)}</pre>`
|
|
44
|
+
: '') +
|
|
45
|
+
'</body></html>';
|
|
46
|
+
const bytes = new TextEncoder().encode(html);
|
|
47
|
+
return new ReadableStream({
|
|
48
|
+
start(controller) {
|
|
49
|
+
controller.enqueue(bytes);
|
|
50
|
+
controller.close();
|
|
51
|
+
},
|
|
52
|
+
});
|
|
35
53
|
}
|
|
36
54
|
export async function renderHTML(rscStream, options) {
|
|
37
55
|
// One copy is rendered to HTML here; the other rides along in that HTML for the client to hydrate from.
|
|
@@ -53,7 +71,7 @@ export async function renderHTML(rscStream, options) {
|
|
|
53
71
|
if (typeof error?.digest === 'string')
|
|
54
72
|
return;
|
|
55
73
|
if (options.signal?.aborted)
|
|
56
|
-
return; // an abort is the
|
|
74
|
+
return; // an abort is the client leaving, not a fault
|
|
57
75
|
reported = error;
|
|
58
76
|
options.onError?.(error);
|
|
59
77
|
};
|
|
@@ -78,9 +96,9 @@ export async function renderHTML(rscStream, options) {
|
|
|
78
96
|
if (!options.signal?.aborted && error !== reported)
|
|
79
97
|
options.onShellError?.(error);
|
|
80
98
|
status = 500;
|
|
81
|
-
htmlStream =
|
|
99
|
+
htmlStream = failureDocument(error);
|
|
82
100
|
}
|
|
83
|
-
const responseStream = htmlStream.pipeThrough(
|
|
101
|
+
const responseStream = htmlStream.pipeThrough(injectFlightPayload(rscForClient, { nonce: options.nonce, onDone: options.onDone }));
|
|
84
102
|
return { stream: responseStream, status };
|
|
85
103
|
}
|
|
86
104
|
//# sourceMappingURL=entry.ssr.js.map
|
|
@@ -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,
|
|
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\n * is compiled into the SSR layer, which gets its own instance of every module it imports, so a\n * handler registered through `@rshono/core/server` isn't reachable from in here.\n */\n onShellError?: (error: unknown) => void;\n /**\n * Called for an error that happened during SSR and *originated* in SSR — a client component that\n * threw while rendering on the server, whether a boundary went on to contain it or it took the\n * shell down with it. See {@link renderHTML} for why the ones that didn't originate here are\n * dropped instead.\n */\n onError?: (error: unknown) => void;\n /**\n * Called once the response stream has ended, however it ended.\n *\n * The RSC layer uses it to detach the listener forwarding client-disconnect aborts into this render —\n * see `renderComponent`. Without a hook here that listener outlives the request and retains the whole\n * rendered tree, so this is load-bearing rather than a convenience.\n */\n onDone?: () => void;\n}\n\n// From the baked config, not `process.env.NODE_ENV`: this is a property of the build, and a deploy\n// target need not have a `process` at all.\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 single byte of the real shell was sent.\n *\n * Deliberately plain HTML with no client runtime attached: the flight payload comes from the same\n * failed render, so hydrating it would mismatch and React — whose root container is the whole\n * `document` — would tear the page down, blanking the very message being rendered here. Styling is\n * inline because the page's stylesheet links were part of the render that just failed.\n *\n * A string rather than a component, for the same reason: React is what just failed, and rendering\n * this through it again is a dependency the fallback does not need. It must end with the document\n * trailer, which is what `injectFlightPayload` holds back and re-emits.\n *\n * The detail is dev-only. In production this stays a generic message, matching how the `error` page\n * from routes.ts 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, including ones an error boundary\n // contained — and with no handler installed it logs each one itself. Almost all of them are errors\n // it read out of the flight payload, where they arrive as React's redacted stand-in: a `digest`\n // and no message. The RSC layer has already reported the real one in full, so the default handler\n // prints an alarming, detail-free duplicate for a request a boundary handled perfectly well. Only\n // an error carrying no digest started life in this render — a client component that threw during\n // SSR — and that 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 // Deliberately returns nothing, so the digest React gives the client's `onRecoverableError`\n // stays exactly what it was before a handler was installed here.\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\n // through: an error out of the flight payload, whose detail the RSC layer alone has.\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"]}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Carrying the flight payload inside the HTML document, so the browser hydrates from bytes it
|
|
3
|
+
* already has rather than fetching the page a second time.
|
|
4
|
+
*
|
|
5
|
+
* The wire format is `rsc-html-stream`'s — a run of
|
|
6
|
+
* `<script>(self.__FLIGHT_DATA||=[]).push("…")</script>` tags — but the implementation is
|
|
7
|
+
* first-party, because that package's `injectRSCPayload` tests each HTML chunk for the document
|
|
8
|
+
* trailer with `endsWith`. React's byte writer packs its output into 2 kB views and splits any write
|
|
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`.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* {@link Transformer} plus the `cancel` hook the Streams standard added for a cancelled readable side.
|
|
18
|
+
*
|
|
19
|
+
* Node calls it — verified on 22.x — but the bundled lib types have not caught up, so it is declared
|
|
20
|
+
* here rather than reached for with an `any`. It matters because `cancel` is the only notification that
|
|
21
|
+
* a response ended *without* finishing, and both users of it release a listener that would otherwise
|
|
22
|
+
* outlive the request.
|
|
23
|
+
*/
|
|
24
|
+
export type CancellableTransformer<I, O> = Transformer<I, O> & {
|
|
25
|
+
cancel?: (reason?: unknown) => void;
|
|
26
|
+
};
|
|
27
|
+
export declare function injectFlightPayload(rscStream: ReadableStream<Uint8Array>, options?: {
|
|
28
|
+
nonce?: string;
|
|
29
|
+
onDone?: () => void;
|
|
30
|
+
}): TransformStream<Uint8Array, Uint8Array>;
|
|
31
|
+
//# sourceMappingURL=flight-inject.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flight-inject.d.ts","sourceRoot":"","sources":["../../src/runtime/flight-inject.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH;;;;;;;GAOG;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;AAwDvG,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,CAwJzC"}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Carrying the flight payload inside the HTML document, so the browser hydrates from bytes it
|
|
3
|
+
* already has rather than fetching the page a second time.
|
|
4
|
+
*
|
|
5
|
+
* The wire format is `rsc-html-stream`'s — a run of
|
|
6
|
+
* `<script>(self.__FLIGHT_DATA||=[]).push("…")</script>` tags — but the implementation is
|
|
7
|
+
* first-party, because that package's `injectRSCPayload` tests each HTML chunk for the document
|
|
8
|
+
* trailer with `endsWith`. React's byte writer packs its output into 2 kB views and splits any write
|
|
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`.
|
|
15
|
+
*/
|
|
16
|
+
const encoder = new TextEncoder();
|
|
17
|
+
/** What React closes an `<html>` document with, and what this module re-emits after the last payload script. */
|
|
18
|
+
const TRAILER = '</body></html>';
|
|
19
|
+
const TRAILER_BYTES = encoder.encode(TRAILER);
|
|
20
|
+
const hasSetImmediate = typeof setImmediate === 'function';
|
|
21
|
+
const schedule = hasSetImmediate ? setImmediate : (fn) => setTimeout(fn, 0);
|
|
22
|
+
const unschedule = (handle) => {
|
|
23
|
+
if (hasSetImmediate)
|
|
24
|
+
clearImmediate(handle);
|
|
25
|
+
else
|
|
26
|
+
clearTimeout(handle);
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Escapes the two sequences that would end a `<script>` element early.
|
|
30
|
+
*
|
|
31
|
+
* Guarded by an `includes('<')` because a flight payload usually has no `<` in it at all, and the
|
|
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).
|
|
34
|
+
*/
|
|
35
|
+
function escapeScript(script) {
|
|
36
|
+
return script.includes('<') ? script.replace(/<!--/g, '<\\!--').replace(/<\/(script)/gi, '</\\$1') : script;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
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. Only reached for a chunk that split a multi-byte
|
|
43
|
+
* character, so it is off the hot path and the slice size is a stack-safety choice, not a tuned one.
|
|
44
|
+
*/
|
|
45
|
+
function latin1(chunk) {
|
|
46
|
+
let out = '';
|
|
47
|
+
for (let at = 0; at < chunk.length; at += 8192)
|
|
48
|
+
out += String.fromCharCode(...chunk.subarray(at, at + 8192));
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
/** Whether `buffer`'s first `length` bytes end with the document trailer. */
|
|
52
|
+
function endsWithTrailer(buffer, length) {
|
|
53
|
+
if (length < TRAILER_BYTES.length)
|
|
54
|
+
return false;
|
|
55
|
+
const from = length - TRAILER_BYTES.length;
|
|
56
|
+
for (let i = 0; i < TRAILER_BYTES.length; i++) {
|
|
57
|
+
if (buffer[from + i] !== TRAILER_BYTES[i])
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
export function injectFlightPayload(rscStream, options = {}) {
|
|
63
|
+
const { nonce, onDone } = options;
|
|
64
|
+
const scriptOpen = `<script${nonce ? ` nonce="${nonce}"` : ''}>(self.__FLIGHT_DATA||=[]).push(`;
|
|
65
|
+
const scriptClose = ')</script>';
|
|
66
|
+
const { promise: flightWritten, resolve: flightDone } = Promise.withResolvers();
|
|
67
|
+
let startedFlight = false;
|
|
68
|
+
const batch = [];
|
|
69
|
+
let boundary = null;
|
|
70
|
+
/**
|
|
71
|
+
* Set once the consumer has gone away, so nothing downstream tries to enqueue into a readable that
|
|
72
|
+
* can no longer take it.
|
|
73
|
+
*
|
|
74
|
+
* It cannot simply be the `cancel` hook that sets this. Per the Streams standard, cancelling the
|
|
75
|
+
* readable *after* the close algorithm has started returns the pending finish promise without
|
|
76
|
+
* running the transformer's `cancel` at all — and `flush` awaiting the whole flight payload is
|
|
77
|
+
* precisely that window. So a failed enqueue is also treated as the signal, wherever one happens.
|
|
78
|
+
*/
|
|
79
|
+
let cancelled = false;
|
|
80
|
+
/** Held so {@link cancelled} can release the teed RSC branch rather than leaving it to be pumped. */
|
|
81
|
+
let flightReader = null;
|
|
82
|
+
/**
|
|
83
|
+
* Emits the HTML buffered since the last boundary, holding back the document trailer for
|
|
84
|
+
* {@link TransformStream.flush} to re-emit after the payload scripts.
|
|
85
|
+
*
|
|
86
|
+
* The batch is joined before the trailer is looked for, so a trailer React split across two views
|
|
87
|
+
* is still found — that is the whole reason this module is not `rsc-html-stream/server`. React
|
|
88
|
+
* writes its final flush in one synchronous run, so every chunk of the trailer lands in the same
|
|
89
|
+
* batch; a trailer split *across* batches is not a shape React produces.
|
|
90
|
+
*/
|
|
91
|
+
function emitBatch(controller) {
|
|
92
|
+
boundary = null;
|
|
93
|
+
let total = 0;
|
|
94
|
+
for (const chunk of batch)
|
|
95
|
+
total += chunk.byteLength;
|
|
96
|
+
if (total === 0) {
|
|
97
|
+
batch.length = 0;
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const joined = new Uint8Array(total);
|
|
101
|
+
let at = 0;
|
|
102
|
+
for (const chunk of batch) {
|
|
103
|
+
joined.set(chunk, at);
|
|
104
|
+
at += chunk.byteLength;
|
|
105
|
+
}
|
|
106
|
+
batch.length = 0;
|
|
107
|
+
const end = endsWithTrailer(joined, total) ? total - TRAILER_BYTES.length : total;
|
|
108
|
+
if (end > 0)
|
|
109
|
+
controller.enqueue(joined.subarray(0, end));
|
|
110
|
+
}
|
|
111
|
+
async function writeFlight(controller) {
|
|
112
|
+
const reader = (flightReader = rscStream.getReader());
|
|
113
|
+
// `fatal`, so a chunk that split a multi-byte character throws rather than emitting U+FFFD and
|
|
114
|
+
// corrupting the payload — the catch below falls back to a byte-exact encoding for it.
|
|
115
|
+
const decoder = new TextDecoder('utf-8', { fatal: true });
|
|
116
|
+
const push = (literal) => controller.enqueue(encoder.encode(scriptOpen + literal + scriptClose));
|
|
117
|
+
for (;;) {
|
|
118
|
+
if (cancelled)
|
|
119
|
+
return;
|
|
120
|
+
const { done, value } = await reader.read();
|
|
121
|
+
if (done)
|
|
122
|
+
break;
|
|
123
|
+
// Only the *decode* is guarded. Wrapping the `push` in the same `try` conflated a split
|
|
124
|
+
// multi-byte character with a controller nobody is reading any more, and answered the second by
|
|
125
|
+
// re-encoding the chunk and enqueueing it again — which throws in turn, out of the catch.
|
|
126
|
+
let literal;
|
|
127
|
+
try {
|
|
128
|
+
literal = escapeScript(JSON.stringify(decoder.decode(value, { stream: true })));
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
literal = `Uint8Array.from(atob(${JSON.stringify(btoa(latin1(value)))}), m => m.codePointAt(0))`;
|
|
132
|
+
}
|
|
133
|
+
if (cancelled)
|
|
134
|
+
return;
|
|
135
|
+
// A failed enqueue means the consumer is gone. Stop pumping and release the RSC branch, so
|
|
136
|
+
// `flush` unparks now rather than whenever the flight payload would have ended on its own.
|
|
137
|
+
try {
|
|
138
|
+
push(literal);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
cancelled = true;
|
|
142
|
+
reader.cancel().catch(() => { });
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (cancelled)
|
|
147
|
+
return;
|
|
148
|
+
const remaining = decoder.decode();
|
|
149
|
+
if (remaining.length)
|
|
150
|
+
push(escapeScript(JSON.stringify(remaining)));
|
|
151
|
+
}
|
|
152
|
+
const transformer = {
|
|
153
|
+
transform(chunk, controller) {
|
|
154
|
+
batch.push(chunk);
|
|
155
|
+
if (boundary)
|
|
156
|
+
return;
|
|
157
|
+
// A macrotask, not a microtask: React writes a whole flush into its stream in one synchronous
|
|
158
|
+
// run, but `pipeThrough` delivers those chunks to us one microtask at a time. Only a task
|
|
159
|
+
// boundary guarantees the flush has arrived in full — a script injected between two of its
|
|
160
|
+
// chunks would land inside a tag.
|
|
161
|
+
boundary = schedule(() => {
|
|
162
|
+
try {
|
|
163
|
+
emitBatch(controller);
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
controller.error(error);
|
|
167
|
+
flightDone();
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (!startedFlight) {
|
|
171
|
+
startedFlight = true;
|
|
172
|
+
writeFlight(controller)
|
|
173
|
+
.catch((error) => controller.error(error))
|
|
174
|
+
.then(flightDone);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
},
|
|
178
|
+
async flush(controller) {
|
|
179
|
+
await flightWritten;
|
|
180
|
+
// That await spans the entire flight payload, and the consumer can go away inside it — a
|
|
181
|
+
// browser's stop button, a navigation away, a proxy timeout. `cancel` below is *not* what tells
|
|
182
|
+
// us so (see `cancelled`), which leaves the enqueue throwing `ERR_INVALID_STATE` as the only
|
|
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.
|
|
185
|
+
try {
|
|
186
|
+
if (boundary) {
|
|
187
|
+
unschedule(boundary);
|
|
188
|
+
emitBatch(controller);
|
|
189
|
+
}
|
|
190
|
+
if (!cancelled)
|
|
191
|
+
controller.enqueue(encoder.encode(TRAILER));
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
// Nowhere left to put the trailer. A response the client abandoned is not a fault.
|
|
195
|
+
cancelled = true;
|
|
196
|
+
flightReader?.cancel().catch(() => { });
|
|
197
|
+
}
|
|
198
|
+
finally {
|
|
199
|
+
// Unconditional, and the reason this is a `finally`: `onDone` is what releases the abort
|
|
200
|
+
// forwarder in `renderComponent`, so it has to run however the response ended.
|
|
201
|
+
onDone?.();
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
cancel(reason) {
|
|
205
|
+
cancelled = true;
|
|
206
|
+
if (boundary) {
|
|
207
|
+
unschedule(boundary);
|
|
208
|
+
boundary = null;
|
|
209
|
+
}
|
|
210
|
+
batch.length = 0;
|
|
211
|
+
// Without this the teed RSC branch keeps being pumped for a response nobody will read, and the
|
|
212
|
+
// tee's other half buffers every chunk waiting for this one to catch up.
|
|
213
|
+
flightReader?.cancel(reason).catch(() => { });
|
|
214
|
+
// Unparks `flush` if it is waiting on a payload that will now never arrive.
|
|
215
|
+
flightDone();
|
|
216
|
+
onDone?.();
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
return new TransformStream(transformer);
|
|
220
|
+
}
|
|
221
|
+
//# 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\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 that\n * a response ended *without* finishing, and both users of it release a listener that would otherwise\n * 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. Only reached for a chunk that split a multi-byte\n * character, so it is off the hot path and the slice size is a stack-safety choice, not a tuned one.\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"]}
|
|
@@ -6,22 +6,18 @@ import { type ReactNode } from 'react';
|
|
|
6
6
|
* payload is fetched and applied in place, so client component state outside the
|
|
7
7
|
* changed subtree survives. Off-site or non-HTTP hrefs fall back to a full load.
|
|
8
8
|
*/
|
|
9
|
-
export interface
|
|
9
|
+
export interface NavigationRouter {
|
|
10
10
|
/** Navigates to `href` and pushes a new history entry. */
|
|
11
11
|
push(href: string): void;
|
|
12
12
|
/** Navigates to `href`, replacing the current history entry instead of adding one. */
|
|
13
13
|
replace(href: string): void;
|
|
14
|
-
/** Goes back one history entry — `history.back()`. */
|
|
15
|
-
back(): void;
|
|
16
|
-
/** Goes forward one history entry — `history.forward()`. */
|
|
17
|
-
forward(): void;
|
|
18
14
|
/** Re-fetches the current route from the server, re-running its server components. */
|
|
19
15
|
refresh(): void;
|
|
20
16
|
/** `true` while a soft navigation is in flight — use it to disable controls or show a spinner. */
|
|
21
17
|
pending: boolean;
|
|
22
18
|
}
|
|
23
|
-
/** The current location plus the {@link
|
|
24
|
-
export interface
|
|
19
|
+
/** The current location plus the {@link NavigationRouter}, as returned by {@link useNavigation}. */
|
|
20
|
+
export interface NavigationState {
|
|
25
21
|
/**
|
|
26
22
|
* The full current {@link URL} — read `url.pathname`, `url.searchParams` and the
|
|
27
23
|
* rest off it. A fresh instance per navigation, so mutating it affects nothing
|
|
@@ -31,16 +27,16 @@ export interface Navigation {
|
|
|
31
27
|
/** Matched route params for the current page, e.g. `{ id: '42' }` for `/profile/:id`. */
|
|
32
28
|
params: Record<string, string>;
|
|
33
29
|
/** Imperative navigation actions and the `pending` flag. */
|
|
34
|
-
router:
|
|
30
|
+
router: NavigationRouter;
|
|
35
31
|
}
|
|
36
32
|
/**
|
|
37
|
-
* Carries the live {@link
|
|
33
|
+
* Carries the live {@link NavigationRouter} implementation from the hydration runtime down
|
|
38
34
|
* to {@link RouterProvider}. Framework internal — read the router through
|
|
39
35
|
* {@link useNavigation} instead.
|
|
40
36
|
*
|
|
41
37
|
* @internal
|
|
42
38
|
*/
|
|
43
|
-
export declare const RouterContext: import("react").Context<
|
|
39
|
+
export declare const RouterContext: import("react").Context<NavigationRouter>;
|
|
44
40
|
/**
|
|
45
41
|
* Publishes the per-render location and params so {@link useNavigation} can read
|
|
46
42
|
* them. Framework internal — the RSC entry wraps every page in one.
|
|
@@ -62,7 +58,7 @@ export declare function RouterProvider({ href, params, children }: {
|
|
|
62
58
|
* `pending` flag that is `true` while a client navigation is in flight.
|
|
63
59
|
*
|
|
64
60
|
* Hooks can't run in a server component; read the same URL data there from
|
|
65
|
-
* `
|
|
61
|
+
* `getRequestContext()` (`@rshono/core/server`) instead.
|
|
66
62
|
*
|
|
67
63
|
* @example
|
|
68
64
|
* ```tsx
|
|
@@ -80,35 +76,10 @@ export declare function RouterProvider({ href, params, children }: {
|
|
|
80
76
|
* }
|
|
81
77
|
* ```
|
|
82
78
|
*
|
|
83
|
-
* @returns The current {@link
|
|
84
|
-
* ({@link
|
|
85
|
-
* `pending`.
|
|
79
|
+
* @returns The current {@link NavigationState}: `url` and `params`, plus `router`
|
|
80
|
+
* ({@link NavigationRouter}) with `push` / `replace` / `refresh` / `pending`.
|
|
86
81
|
* @throws If called outside a page's React tree, where there is no navigation
|
|
87
82
|
* context to read.
|
|
88
83
|
*/
|
|
89
|
-
export declare function useNavigation():
|
|
90
|
-
export interface NavigationProgressProps {
|
|
91
|
-
/** Bar color. Defaults to a neutral blue. */
|
|
92
|
-
color?: string;
|
|
93
|
-
/** Bar height in pixels. Defaults to `3`. */
|
|
94
|
-
height?: number;
|
|
95
|
-
}
|
|
96
|
-
/**
|
|
97
|
-
* An opt-in top progress bar that appears while a client navigation is in
|
|
98
|
-
* flight (driven by {@link Router.pending}). Drop one instance in your root
|
|
99
|
-
* layout; it renders nothing on the server and stays invisible until the first
|
|
100
|
-
* soft navigation, so there's no hydration flicker.
|
|
101
|
-
*
|
|
102
|
-
* @example
|
|
103
|
-
* ```tsx
|
|
104
|
-
* import { NavigationProgress } from '@rshono/core/client';
|
|
105
|
-
*
|
|
106
|
-
* // in your layout, once:
|
|
107
|
-
* <body>
|
|
108
|
-
* <NavigationProgress />
|
|
109
|
-
* {children}
|
|
110
|
-
* </body>
|
|
111
|
-
* ```
|
|
112
|
-
*/
|
|
113
|
-
export declare function NavigationProgress({ color, height }?: NavigationProgressProps): ReactNode;
|
|
84
|
+
export declare function useNavigation(): NavigationState;
|
|
114
85
|
//# sourceMappingURL=navigation.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"navigation.d.ts","sourceRoot":"","sources":["../../src/runtime/navigation.tsx"],"names":[],"mappings":"AAEA,OAAO,
|
|
1
|
+
{"version":3,"file":"navigation.d.ts","sourceRoot":"","sources":["../../src/runtime/navigation.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAsC,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAE3E;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAC/B,0DAA0D;IAC1D,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,sFAAsF;IACtF,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,sFAAsF;IACtF,OAAO,IAAI,IAAI,CAAC;IAChB,kGAAkG;IAClG,OAAO,EAAE,OAAO,CAAC;CAClB;AAKD,oGAAoG;AACpG,MAAM,WAAW,eAAe;IAC9B;;;;OAIG;IACH,GAAG,EAAE,GAAG,CAAC;IACT,yFAAyF;IACzF,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,4DAA4D;IAC5D,MAAM,EAAE,gBAAgB,CAAC;CAC1B;AAMD;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,2CAAiD,CAAC;AAI5E;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,QAAQ,EAAE,SAAS,CAAA;CAAE,+BAK/H;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAgB,aAAa,IAAI,eAAe,CAQ/C"}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
-
import { createContext, useContext,
|
|
3
|
+
import { createContext, useContext, useMemo } from 'react';
|
|
4
4
|
const noop = () => { };
|
|
5
|
-
const defaultRouter = { push: noop, replace: noop,
|
|
5
|
+
const defaultRouter = { push: noop, replace: noop, refresh: noop, pending: false };
|
|
6
6
|
/**
|
|
7
|
-
* Carries the live {@link
|
|
7
|
+
* Carries the live {@link NavigationRouter} implementation from the hydration runtime down
|
|
8
8
|
* to {@link RouterProvider}. Framework internal — read the router through
|
|
9
9
|
* {@link useNavigation} instead.
|
|
10
10
|
*
|
|
@@ -33,7 +33,7 @@ export function RouterProvider({ href, params, children }) {
|
|
|
33
33
|
* `pending` flag that is `true` while a client navigation is in flight.
|
|
34
34
|
*
|
|
35
35
|
* Hooks can't run in a server component; read the same URL data there from
|
|
36
|
-
* `
|
|
36
|
+
* `getRequestContext()` (`@rshono/core/server`) instead.
|
|
37
37
|
*
|
|
38
38
|
* @example
|
|
39
39
|
* ```tsx
|
|
@@ -51,62 +51,16 @@ export function RouterProvider({ href, params, children }) {
|
|
|
51
51
|
* }
|
|
52
52
|
* ```
|
|
53
53
|
*
|
|
54
|
-
* @returns The current {@link
|
|
55
|
-
* ({@link
|
|
56
|
-
* `pending`.
|
|
54
|
+
* @returns The current {@link NavigationState}: `url` and `params`, plus `router`
|
|
55
|
+
* ({@link NavigationRouter}) with `push` / `replace` / `refresh` / `pending`.
|
|
57
56
|
* @throws If called outside a page's React tree, where there is no navigation
|
|
58
57
|
* context to read.
|
|
59
58
|
*/
|
|
60
59
|
export function useNavigation() {
|
|
61
60
|
const value = useContext(NavigationContext);
|
|
62
61
|
if (!value) {
|
|
63
|
-
throw new Error("[rshono] useNavigation() must be called inside a 'use client' component rendered by a page. In a server component, read the URL from
|
|
62
|
+
throw new Error("[rshono] useNavigation() must be called inside a 'use client' component rendered by a page. In a server component, read the URL from getRequestContext() instead.");
|
|
64
63
|
}
|
|
65
64
|
return value;
|
|
66
65
|
}
|
|
67
|
-
/**
|
|
68
|
-
* An opt-in top progress bar that appears while a client navigation is in
|
|
69
|
-
* flight (driven by {@link Router.pending}). Drop one instance in your root
|
|
70
|
-
* layout; it renders nothing on the server and stays invisible until the first
|
|
71
|
-
* soft navigation, so there's no hydration flicker.
|
|
72
|
-
*
|
|
73
|
-
* @example
|
|
74
|
-
* ```tsx
|
|
75
|
-
* import { NavigationProgress } from '@rshono/core/client';
|
|
76
|
-
*
|
|
77
|
-
* // in your layout, once:
|
|
78
|
-
* <body>
|
|
79
|
-
* <NavigationProgress />
|
|
80
|
-
* {children}
|
|
81
|
-
* </body>
|
|
82
|
-
* ```
|
|
83
|
-
*/
|
|
84
|
-
export function NavigationProgress({ color = '#3b82f6', height = 3 } = {}) {
|
|
85
|
-
const { router } = useNavigation();
|
|
86
|
-
const [bar, setBar] = useState({ width: 0, opacity: 0 });
|
|
87
|
-
useEffect(() => {
|
|
88
|
-
if (router.pending) {
|
|
89
|
-
// Jump in, then creep toward — but never reach — the end while we wait.
|
|
90
|
-
setBar({ width: 15, opacity: 1 });
|
|
91
|
-
const ramp = setTimeout(() => setBar({ width: 85, opacity: 1 }), 80);
|
|
92
|
-
return () => clearTimeout(ramp);
|
|
93
|
-
}
|
|
94
|
-
// Done: snap to full, then fade out. (No-op if it was never shown.)
|
|
95
|
-
setBar((b) => (b.opacity === 0 ? b : { width: 100, opacity: 1 }));
|
|
96
|
-
const hide = setTimeout(() => setBar({ width: 0, opacity: 0 }), 220);
|
|
97
|
-
return () => clearTimeout(hide);
|
|
98
|
-
}, [router.pending]);
|
|
99
|
-
return (_jsx("div", { "data-rshono-progress": "", "aria-hidden": "true", style: {
|
|
100
|
-
position: 'fixed',
|
|
101
|
-
top: 0,
|
|
102
|
-
left: 0,
|
|
103
|
-
height,
|
|
104
|
-
width: `${bar.width}%`,
|
|
105
|
-
opacity: bar.opacity,
|
|
106
|
-
background: color,
|
|
107
|
-
zIndex: 2147483647,
|
|
108
|
-
pointerEvents: 'none',
|
|
109
|
-
transition: 'width 200ms ease-out, opacity 200ms ease-out',
|
|
110
|
-
} }));
|
|
111
|
-
}
|
|
112
66
|
//# sourceMappingURL=navigation.js.map
|