@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.
Files changed (123) hide show
  1. package/README.md +45 -29
  2. package/dist/builder/rspack-config.d.ts +3 -3
  3. package/dist/builder/rspack-config.js +3 -3
  4. package/dist/builder/rspack-config.js.map +1 -1
  5. package/dist/cli/build.d.ts +2 -2
  6. package/dist/cli/build.js.map +1 -1
  7. package/dist/cli/dev.d.ts +2 -2
  8. package/dist/cli/dev.js.map +1 -1
  9. package/dist/cli/index.js +11 -10
  10. package/dist/cli/index.js.map +1 -1
  11. package/dist/config.d.ts +4 -17
  12. package/dist/config.d.ts.map +1 -1
  13. package/dist/config.js.map +1 -1
  14. package/dist/deploy/cloudflare/build.js +1 -1
  15. package/dist/deploy/cloudflare/build.js.map +1 -1
  16. package/dist/deploy/cloudflare/runtime.d.ts.map +1 -1
  17. package/dist/deploy/cloudflare/runtime.js +7 -37
  18. package/dist/deploy/cloudflare/runtime.js.map +1 -1
  19. package/dist/deploy/contract.d.ts +14 -13
  20. package/dist/deploy/contract.d.ts.map +1 -1
  21. package/dist/deploy/contract.js.map +1 -1
  22. package/dist/deploy/filesystem.d.ts +1 -1
  23. package/dist/deploy/filesystem.d.ts.map +1 -1
  24. package/dist/deploy/filesystem.js +1 -3
  25. package/dist/deploy/filesystem.js.map +1 -1
  26. package/dist/deploy/node/runtime.d.ts +6 -2
  27. package/dist/deploy/node/runtime.d.ts.map +1 -1
  28. package/dist/deploy/node/runtime.js +30 -5
  29. package/dist/deploy/node/runtime.js.map +1 -1
  30. package/dist/deploy/presets.d.ts +5 -1
  31. package/dist/deploy/presets.d.ts.map +1 -1
  32. package/dist/deploy/presets.js +15 -28
  33. package/dist/deploy/presets.js.map +1 -1
  34. package/dist/deploy/vercel/runtime.d.ts.map +1 -1
  35. package/dist/deploy/vercel/runtime.js +0 -3
  36. package/dist/deploy/vercel/runtime.js.map +1 -1
  37. package/dist/index.d.ts +4 -9
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +6 -3
  40. package/dist/index.js.map +1 -1
  41. package/dist/router.d.ts +14 -15
  42. package/dist/router.d.ts.map +1 -1
  43. package/dist/router.js +5 -6
  44. package/dist/router.js.map +1 -1
  45. package/dist/runtime/client.d.ts +5 -6
  46. package/dist/runtime/client.d.ts.map +1 -1
  47. package/dist/runtime/client.js +5 -6
  48. package/dist/runtime/client.js.map +1 -1
  49. package/dist/runtime/context.d.ts +36 -40
  50. package/dist/runtime/context.d.ts.map +1 -1
  51. package/dist/runtime/context.js +41 -53
  52. package/dist/runtime/context.js.map +1 -1
  53. package/dist/runtime/entry.client.d.ts +4 -0
  54. package/dist/runtime/entry.client.d.ts.map +1 -1
  55. package/dist/runtime/entry.client.js +161 -147
  56. package/dist/runtime/entry.client.js.map +1 -1
  57. package/dist/runtime/entry.rsc.d.ts.map +1 -1
  58. package/dist/runtime/entry.rsc.js +80 -63
  59. package/dist/runtime/entry.rsc.js.map +1 -1
  60. package/dist/runtime/entry.ssr.d.ts +9 -1
  61. package/dist/runtime/entry.ssr.d.ts.map +1 -1
  62. package/dist/runtime/entry.ssr.js +36 -18
  63. package/dist/runtime/entry.ssr.js.map +1 -1
  64. package/dist/runtime/flight-inject.d.ts +31 -0
  65. package/dist/runtime/flight-inject.d.ts.map +1 -0
  66. package/dist/runtime/flight-inject.js +221 -0
  67. package/dist/runtime/flight-inject.js.map +1 -0
  68. package/dist/runtime/navigation.d.ts +10 -39
  69. package/dist/runtime/navigation.d.ts.map +1 -1
  70. package/dist/runtime/navigation.js +7 -53
  71. package/dist/runtime/navigation.js.map +1 -1
  72. package/dist/runtime/request.d.ts +6 -0
  73. package/dist/runtime/request.d.ts.map +1 -1
  74. package/dist/runtime/request.js +8 -0
  75. package/dist/runtime/request.js.map +1 -1
  76. package/dist/runtime/server.d.ts +2 -2
  77. package/dist/runtime/server.d.ts.map +1 -1
  78. package/dist/runtime/server.js +2 -2
  79. package/dist/runtime/server.js.map +1 -1
  80. package/dist/server/headers.d.ts +9 -9
  81. package/dist/server/headers.js +9 -9
  82. package/dist/server/headers.js.map +1 -1
  83. package/dist/server/load-config.d.ts +2 -2
  84. package/dist/server/load-config.d.ts.map +1 -1
  85. package/dist/server/load-config.js +22 -11
  86. package/dist/server/load-config.js.map +1 -1
  87. package/dist/server/prerendered.d.ts +52 -9
  88. package/dist/server/prerendered.d.ts.map +1 -1
  89. package/dist/server/prerendered.js +57 -0
  90. package/dist/server/prerendered.js.map +1 -1
  91. package/dist/server/server-config.d.ts +11 -18
  92. package/dist/server/server-config.d.ts.map +1 -1
  93. package/dist/server/server-config.js +9 -12
  94. package/dist/server/server-config.js.map +1 -1
  95. package/dist/server/ssg.d.ts +2 -2
  96. package/dist/server/ssg.d.ts.map +1 -1
  97. package/dist/server/ssg.js +11 -23
  98. package/dist/server/ssg.js.map +1 -1
  99. package/package.json +13 -16
  100. package/dist/deploy/bun/runtime.d.ts +0 -11
  101. package/dist/deploy/bun/runtime.d.ts.map +0 -1
  102. package/dist/deploy/bun/runtime.js +0 -22
  103. package/dist/deploy/bun/runtime.js.map +0 -1
  104. package/dist/deploy/deno/runtime.d.ts +0 -11
  105. package/dist/deploy/deno/runtime.d.ts.map +0 -1
  106. package/dist/deploy/deno/runtime.js +0 -16
  107. package/dist/deploy/deno/runtime.js.map +0 -1
  108. package/dist/deploy/listen.d.ts +0 -20
  109. package/dist/deploy/listen.d.ts.map +0 -1
  110. package/dist/deploy/listen.js +0 -24
  111. package/dist/deploy/listen.js.map +0 -1
  112. package/dist/deploy/netlify/build.d.ts +0 -8
  113. package/dist/deploy/netlify/build.d.ts.map +0 -1
  114. package/dist/deploy/netlify/build.js +0 -52
  115. package/dist/deploy/netlify/build.js.map +0 -1
  116. package/dist/deploy/netlify/runtime.d.ts +0 -13
  117. package/dist/deploy/netlify/runtime.d.ts.map +0 -1
  118. package/dist/deploy/netlify/runtime.js +0 -24
  119. package/dist/deploy/netlify/runtime.js.map +0 -1
  120. package/dist/server/compress.d.ts +0 -15
  121. package/dist/server/compress.d.ts.map +0 -1
  122. package/dist/server/compress.js +0 -76
  123. package/dist/server/compress.js.map +0 -1
@@ -1,12 +1,16 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
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 === '&' ? '&amp;' : char === '<' ? '&lt;' : '&gt;'));
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 SsrFailureDocument({ error }) {
29
+ function failureDocument(error) {
22
30
  const detail = isDev ? (error instanceof Error ? (error.stack ?? `${error.name}: ${error.message}`) : String(error)) : null;
23
- return (_jsxs("html", { lang: "en", children: [_jsxs("head", { children: [_jsx("meta", { charSet: "utf-8" }), _jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1" }), _jsx("title", { children: "500 \u2014 Internal Server Error" })] }), _jsxs("body", { style: { margin: 0, padding: '2rem', font: '16px/1.6 system-ui, -apple-system, sans-serif', color: '#18181b' }, children: [_jsx("h1", { style: { margin: '0 0 0.5rem', fontSize: '1.25rem' }, children: "500 \u2014 Internal Server Error" }), _jsx("p", { style: { margin: '0 0 1.5rem', color: '#52525b' }, children: isDev
24
- ? 'Server-side rendering failed before the page shell could be sent, so the app’s error page could not be reached either.'
25
- : 'Something went wrong while rendering this page. Please try again.' }), detail && (_jsx("pre", { style: {
26
- margin: 0,
27
- padding: '1rem',
28
- overflow: 'auto',
29
- background: '#f4f4f5',
30
- borderLeft: '3px solid #ef4444',
31
- font: '13px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace',
32
- whiteSpace: 'pre-wrap',
33
- wordBreak: 'break-word',
34
- }, children: detail }))] })] }));
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 deadline or the client leaving, not a fault
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 = await renderToReadableStream(_jsx(SsrFailureDocument, { error: error }), { nonce: options.nonce });
99
+ htmlStream = failureDocument(error);
82
100
  }
83
- const responseStream = htmlStream.pipeThrough(injectRSCPayload(rscForClient, options.nonce ? { nonce: options.nonce } : undefined));
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":"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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 { injectRSCPayload } from 'rsc-html-stream/server';\nimport { isControlDigest } from './control.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\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/**\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 * The detail is dev-only. In production this stays a generic message, matching how the `error` page\n * from routes.ts redacts.\n */\nfunction SsrFailureDocument({ error }: { error: unknown }) {\n const detail = isDev ? (error instanceof Error ? (error.stack ?? `${error.name}: ${error.message}`) : String(error)) : null;\n return (\n <html lang=\"en\">\n <head>\n <meta charSet=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>500 — Internal Server Error</title>\n </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 0.5rem', fontSize: '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\n style={{\n margin: 0,\n padding: '1rem',\n overflow: 'auto',\n background: '#f4f4f5',\n borderLeft: '3px solid #ef4444',\n font: '13px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace',\n whiteSpace: 'pre-wrap',\n wordBreak: 'break-word',\n }}\n >\n {detail}\n </pre>\n )}\n </body>\n </html>\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 deadline or 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 = await renderToReadableStream(<SsrFailureDocument error={error} />, { nonce: options.nonce });\n }\n\n const responseStream = htmlStream.pipeThrough(injectRSCPayload(rscForClient, options.nonce ? { nonce: options.nonce } : undefined));\n\n return { stream: responseStream, status };\n}\n"]}
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 === '&' ? '&amp;' : char === '<' ? '&lt;' : '&gt;'));\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 Router {
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 Router}, as returned by {@link useNavigation}. */
24
- export interface Navigation {
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: Router;
30
+ router: NavigationRouter;
35
31
  }
36
32
  /**
37
- * Carries the live {@link Router} implementation from the hydration runtime down
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<Router>;
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
- * `getContext()` (`@rshono/core/server`) instead.
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 Navigation}: `url` and `params`, plus `router`
84
- * ({@link Router}) with `push` / `replace` / `back` / `forward` / `refresh` /
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(): Navigation;
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,EAA2D,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAEhG;;;;;;GAMG;AACH,MAAM,WAAW,MAAM;IACrB,0DAA0D;IAC1D,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,sFAAsF;IACtF,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,sDAAsD;IACtD,IAAI,IAAI,IAAI,CAAC;IACb,4DAA4D;IAC5D,OAAO,IAAI,IAAI,CAAC;IAChB,sFAAsF;IACtF,OAAO,IAAI,IAAI,CAAC;IAChB,kGAAkG;IAClG,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,0FAA0F;AAC1F,MAAM,WAAW,UAAU;IACzB;;;;OAIG;IACH,GAAG,EAAE,GAAG,CAAC;IACT,yFAAyF;IACzF,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,4DAA4D;IAC5D,MAAM,EAAE,MAAM,CAAC;CAChB;AAMD;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,iCAAuC,CAAC;AAIlE;;;;;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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAgB,aAAa,IAAI,UAAU,CAQ1C;AAED,MAAM,WAAW,uBAAuB;IACtC,6CAA6C;IAC7C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6CAA6C;IAC7C,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,KAAiB,EAAE,MAAU,EAAE,GAAE,uBAA4B,GAAG,SAAS,CAmC7G"}
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, useEffect, useMemo, useState } from 'react';
3
+ import { createContext, useContext, useMemo } from 'react';
4
4
  const noop = () => { };
5
- const defaultRouter = { push: noop, replace: noop, back: noop, forward: noop, refresh: noop, pending: false };
5
+ const defaultRouter = { push: noop, replace: noop, refresh: noop, pending: false };
6
6
  /**
7
- * Carries the live {@link Router} implementation from the hydration runtime down
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
- * `getContext()` (`@rshono/core/server`) instead.
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 Navigation}: `url` and `params`, plus `router`
55
- * ({@link Router}) with `push` / `replace` / `back` / `forward` / `refresh` /
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 getContext() instead.");
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