@rshono/core 1.0.0-rc.19 → 1.0.0-rc.20

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 (151) hide show
  1. package/README.md +93 -2
  2. package/dist/builder/env-shadow-loader.cjs +41 -1
  3. package/dist/builder/page-entry-loader.cjs +28 -1
  4. package/dist/builder/page-files.d.ts +0 -1
  5. package/dist/builder/public-env.d.ts +0 -1
  6. package/dist/builder/react-versions.d.ts +0 -1
  7. package/dist/builder/rspack-config.d.ts +0 -1
  8. package/dist/builder/rspack-config.js +4 -0
  9. package/dist/builder/rspack-config.js.map +1 -1
  10. package/dist/builder/server-only-imports.d.ts +0 -1
  11. package/dist/cli/build.d.ts +0 -1
  12. package/dist/cli/build.js +37 -35
  13. package/dist/cli/build.js.map +1 -1
  14. package/dist/cli/dev.d.ts +0 -1
  15. package/dist/cli/dev.js +15 -1
  16. package/dist/cli/dev.js.map +1 -1
  17. package/dist/cli/exit.d.ts +12 -0
  18. package/dist/cli/exit.js +16 -0
  19. package/dist/cli/exit.js.map +1 -0
  20. package/dist/cli/index.d.ts +0 -1
  21. package/dist/cli/index.js +74 -20
  22. package/dist/cli/index.js.map +1 -1
  23. package/dist/cli/start.d.ts +0 -1
  24. package/dist/cli/start.js +3 -2
  25. package/dist/cli/start.js.map +1 -1
  26. package/dist/config.d.ts +0 -1
  27. package/dist/deploy/aws-lambda/runtime.d.ts +0 -1
  28. package/dist/deploy/build-marker.d.ts +7 -2
  29. package/dist/deploy/build-marker.js +6 -0
  30. package/dist/deploy/build-marker.js.map +1 -1
  31. package/dist/deploy/cloudflare/build.d.ts +0 -1
  32. package/dist/deploy/cloudflare/build.js +6 -0
  33. package/dist/deploy/cloudflare/build.js.map +1 -1
  34. package/dist/deploy/cloudflare/runtime.d.ts +0 -1
  35. package/dist/deploy/cloudflare/runtime.js +24 -2
  36. package/dist/deploy/cloudflare/runtime.js.map +1 -1
  37. package/dist/deploy/contract.d.ts +20 -2
  38. package/dist/deploy/contract.js.map +1 -1
  39. package/dist/deploy/filesystem.d.ts +0 -1
  40. package/dist/deploy/node/runtime.d.ts +0 -1
  41. package/dist/deploy/presets.d.ts +7 -1
  42. package/dist/deploy/presets.js +13 -2
  43. package/dist/deploy/presets.js.map +1 -1
  44. package/dist/deploy/public-paths.d.ts +42 -0
  45. package/dist/deploy/public-paths.js +66 -0
  46. package/dist/deploy/public-paths.js.map +1 -0
  47. package/dist/deploy/vercel/build.d.ts +0 -1
  48. package/dist/deploy/vercel/build.js +6 -0
  49. package/dist/deploy/vercel/build.js.map +1 -1
  50. package/dist/deploy/vercel/runtime.d.ts +0 -1
  51. package/dist/index.d.ts +0 -1
  52. package/dist/router.d.ts +20 -5
  53. package/dist/router.js.map +1 -1
  54. package/dist/runtime/boundaries.d.ts +0 -1
  55. package/dist/runtime/client.d.ts +0 -1
  56. package/dist/runtime/context.d.ts +15 -2
  57. package/dist/runtime/context.js +15 -1
  58. package/dist/runtime/context.js.map +1 -1
  59. package/dist/runtime/control.d.ts +15 -1
  60. package/dist/runtime/control.js +17 -0
  61. package/dist/runtime/control.js.map +1 -1
  62. package/dist/runtime/dev-protocol.d.ts +0 -1
  63. package/dist/runtime/empty-server-app.d.ts +0 -1
  64. package/dist/runtime/entry.client.d.ts +0 -1
  65. package/dist/runtime/entry.client.js +31 -4
  66. package/dist/runtime/entry.client.js.map +1 -1
  67. package/dist/runtime/entry.rsc.d.ts +6 -4
  68. package/dist/runtime/entry.rsc.js +473 -56
  69. package/dist/runtime/entry.rsc.js.map +1 -1
  70. package/dist/runtime/entry.ssr.d.ts +19 -5
  71. package/dist/runtime/entry.ssr.js +22 -47
  72. package/dist/runtime/entry.ssr.js.map +1 -1
  73. package/dist/runtime/failure-document.d.ts +11 -0
  74. package/dist/runtime/failure-document.js +35 -0
  75. package/dist/runtime/failure-document.js.map +1 -0
  76. package/dist/runtime/flight-inject.d.ts +0 -1
  77. package/dist/runtime/flight-inject.js +15 -9
  78. package/dist/runtime/flight-inject.js.map +1 -1
  79. package/dist/runtime/hot-update.d.ts +0 -1
  80. package/dist/runtime/navigation.d.ts +6 -1
  81. package/dist/runtime/navigation.js +6 -0
  82. package/dist/runtime/navigation.js.map +1 -1
  83. package/dist/runtime/request.d.ts +23 -1
  84. package/dist/runtime/request.js +39 -0
  85. package/dist/runtime/request.js.map +1 -1
  86. package/dist/runtime/server.d.ts +0 -1
  87. package/dist/runtime/validate-entries.d.ts +0 -1
  88. package/dist/runtime/validate-entries.js +114 -10
  89. package/dist/runtime/validate-entries.js.map +1 -1
  90. package/dist/server/headers.d.ts +37 -4
  91. package/dist/server/headers.js +49 -13
  92. package/dist/server/headers.js.map +1 -1
  93. package/dist/server/load-config.d.ts +0 -1
  94. package/dist/server/load-env.d.ts +0 -1
  95. package/dist/server/prerendered.d.ts +14 -2
  96. package/dist/server/prerendered.js +42 -4
  97. package/dist/server/prerendered.js.map +1 -1
  98. package/dist/server/server-config.d.ts +0 -1
  99. package/dist/server/shutdown.d.ts +0 -1
  100. package/dist/server/ssg.d.ts +6 -1
  101. package/dist/server/ssg.js +30 -7
  102. package/dist/server/ssg.js.map +1 -1
  103. package/dist/server/static.d.ts +0 -1
  104. package/dist/server/static.js +5 -5
  105. package/dist/server/static.js.map +1 -1
  106. package/package.json +1 -1
  107. package/dist/builder/page-files.d.ts.map +0 -1
  108. package/dist/builder/public-env.d.ts.map +0 -1
  109. package/dist/builder/react-versions.d.ts.map +0 -1
  110. package/dist/builder/rspack-config.d.ts.map +0 -1
  111. package/dist/builder/server-only-imports.d.ts.map +0 -1
  112. package/dist/cli/build.d.ts.map +0 -1
  113. package/dist/cli/dev.d.ts.map +0 -1
  114. package/dist/cli/index.d.ts.map +0 -1
  115. package/dist/cli/start.d.ts.map +0 -1
  116. package/dist/config.d.ts.map +0 -1
  117. package/dist/deploy/aws-lambda/runtime.d.ts.map +0 -1
  118. package/dist/deploy/build-marker.d.ts.map +0 -1
  119. package/dist/deploy/cloudflare/build.d.ts.map +0 -1
  120. package/dist/deploy/cloudflare/runtime.d.ts.map +0 -1
  121. package/dist/deploy/contract.d.ts.map +0 -1
  122. package/dist/deploy/filesystem.d.ts.map +0 -1
  123. package/dist/deploy/node/runtime.d.ts.map +0 -1
  124. package/dist/deploy/presets.d.ts.map +0 -1
  125. package/dist/deploy/vercel/build.d.ts.map +0 -1
  126. package/dist/deploy/vercel/runtime.d.ts.map +0 -1
  127. package/dist/index.d.ts.map +0 -1
  128. package/dist/router.d.ts.map +0 -1
  129. package/dist/runtime/boundaries.d.ts.map +0 -1
  130. package/dist/runtime/client.d.ts.map +0 -1
  131. package/dist/runtime/context.d.ts.map +0 -1
  132. package/dist/runtime/control.d.ts.map +0 -1
  133. package/dist/runtime/dev-protocol.d.ts.map +0 -1
  134. package/dist/runtime/empty-server-app.d.ts.map +0 -1
  135. package/dist/runtime/entry.client.d.ts.map +0 -1
  136. package/dist/runtime/entry.rsc.d.ts.map +0 -1
  137. package/dist/runtime/entry.ssr.d.ts.map +0 -1
  138. package/dist/runtime/flight-inject.d.ts.map +0 -1
  139. package/dist/runtime/hot-update.d.ts.map +0 -1
  140. package/dist/runtime/navigation.d.ts.map +0 -1
  141. package/dist/runtime/request.d.ts.map +0 -1
  142. package/dist/runtime/server.d.ts.map +0 -1
  143. package/dist/runtime/validate-entries.d.ts.map +0 -1
  144. package/dist/server/headers.d.ts.map +0 -1
  145. package/dist/server/load-config.d.ts.map +0 -1
  146. package/dist/server/load-env.d.ts.map +0 -1
  147. package/dist/server/prerendered.d.ts.map +0 -1
  148. package/dist/server/server-config.d.ts.map +0 -1
  149. package/dist/server/shutdown.d.ts.map +0 -1
  150. package/dist/server/ssg.d.ts.map +0 -1
  151. package/dist/server/static.d.ts.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"failure-document.js","sourceRoot":"","sources":["../../src/runtime/failure-document.ts"],"names":[],"mappings":"AAAA,2FAA2F;AAC3F,MAAM,KAAK,GAAG,iBAAiB,CAAC,KAAK,CAAC;AAEtC,oFAAoF;AACpF,SAAS,UAAU,CAAC,IAAY;IAC9B,OAAO,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;AACrG,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,eAAe,CAAC,KAAc;IAC5C,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,IAAI,GAAG,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAC5H,OAAO,CACL,6DAA6D;QAC7D,sEAAsE;QACtE,mDAAmD;QACnD,qGAAqG;QACrG,iFAAiF;QACjF,6CAA6C;QAC7C,CAAC,KAAK;YACJ,CAAC,CAAC,2LAA2L;YAC7L,CAAC,CAAC,qEAAqE,CAAC;QAC1E,MAAM;QACN,CAAC,MAAM;YACL,CAAC,CAAC,mGAAmG;gBACnG,yGAAyG,UAAU,CAAC,MAAM,CAAC,QAAQ;YACrI,CAAC,CAAC,EAAE,CAAC;QACP,gBAAgB,CACjB,CAAC;AACJ,CAAC","sourcesContent":["// The baked config, not `process.env.NODE_ENV`: a deploy target need not have a `process`.\nconst isDev = __RSHONO_CONFIG__.isDev;\n\n/** Escapes text going into HTML body content — a stack trace is untrusted input. */\nfunction escapeHtml(text: string): string {\n return text.replace(/[&<>]/g, (char) => (char === '&' ? '&amp;' : char === '<' ? '&lt;' : '&gt;'));\n}\n\n/**\n * The framework's last-resort 500 document, for a client that asked for HTML and has nothing better to be\n * given: an app with no `error` page in its `routes.ts`, or one whose `error` page threw in its turn.\n *\n * Plain HTML with no client runtime and no stylesheet links. Both would have come from a render that failed,\n * and there is no payload to hydrate from — attaching the runtime would tear the page down over this very\n * message. A string rather than a component for the same reason: React is what failed.\n *\n * The detail is dev-only, matching how the app's `error` page redacts.\n */\nexport function failureDocument(error: unknown): string {\n const detail = isDev ? (error instanceof Error ? (error.stack ?? `${error.name}: ${error.message}`) : String(error)) : null;\n return (\n '<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"utf-8\">' +\n '<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">' +\n '<title>500 — Internal Server Error</title></head>' +\n '<body style=\"margin:0;padding:2rem;font:16px/1.6 system-ui,-apple-system,sans-serif;color:#18181b\">' +\n '<h1 style=\"margin:0 0 .5rem;font-size:1.25rem\">500 — Internal Server Error</h1>' +\n '<p style=\"margin:0 0 1.5rem;color:#52525b\">' +\n (isDev\n ? 'The request failed and the app’s error page did not answer it: either src/routes.ts declares no <code>error</code> page, or that page threw in its turn. Both are reported on the server.'\n : 'Something went wrong while handling this request. Please try again.') +\n '</p>' +\n (detail\n ? '<pre style=\"margin:0;padding:1rem;overflow:auto;background:#f4f4f5;border-left:3px solid #ef4444;' +\n `font:13px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;white-space:pre-wrap;word-break:break-word\">${escapeHtml(detail)}</pre>`\n : '') +\n '</body></html>'\n );\n}\n"]}
@@ -21,4 +21,3 @@ export declare function injectFlightPayload(rscStream: ReadableStream<Uint8Array
21
21
  nonce?: string;
22
22
  onDone?: () => void;
23
23
  }): ReadableWritablePair<Uint8Array, Uint8Array>;
24
- //# sourceMappingURL=flight-inject.d.ts.map
@@ -232,9 +232,18 @@ export function injectFlightPayload(rscStream, options = {}) {
232
232
  }
233
233
  async function writeFlight(controller) {
234
234
  const reader = (flightReader = rscStream.getReader());
235
- // `fatal`, so a chunk that split a multi-byte character throws instead of emitting U+FFFD; the catch
236
- // below falls back to a byte-exact encoding for it.
237
- const decoder = new TextDecoder('utf-8', { fatal: true });
235
+ // One chunk at a time, deliberately *not* `{ stream: true }`: anything that is not valid UTF-8 on its
236
+ // own — a binary row's bytes, or a chunk that split a multi-byte character — throws, and the catch below
237
+ // re-encodes that whole chunk byte-exactly.
238
+ //
239
+ // Streaming is what makes that fallback wrong. A chunk ending mid-character leaves its lead bytes inside
240
+ // the decoder to be emitted with the next chunk; when the next chunk is a binary row and throws, those
241
+ // bytes are in neither `decode()`'s return nor `value`, and the client reassembles a payload short by
242
+ // them. It also left the end-of-stream flush able to throw from outside any `try`.
243
+ //
244
+ // `ignoreBOM` because a per-chunk decode re-runs the BOM check on every call: without it any chunk whose
245
+ // first three bytes are EF BB BF would silently lose them.
246
+ const decoder = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });
238
247
  const push = async (literal) => {
239
248
  const permit = takePermit();
240
249
  if (permit)
@@ -253,7 +262,7 @@ export function injectFlightPayload(rscStream, options = {}) {
253
262
  // re-encoding the chunk and enqueueing it again.
254
263
  let literal;
255
264
  try {
256
- literal = escapeScript(JSON.stringify(decoder.decode(value, { stream: true })));
265
+ literal = escapeScript(JSON.stringify(decoder.decode(value)));
257
266
  }
258
267
  catch {
259
268
  literal = `Uint8Array.from(atob(${JSON.stringify(btoa(latin1(value)))}), m => m.codePointAt(0))`;
@@ -271,11 +280,8 @@ export function injectFlightPayload(rscStream, options = {}) {
271
280
  return;
272
281
  }
273
282
  }
274
- if (cancelled)
275
- return;
276
- const remaining = decoder.decode();
277
- if (remaining.length)
278
- await push(escapeScript(JSON.stringify(remaining)));
283
+ // No end-of-stream flush: a non-streaming decoder holds nothing between calls, so there is nothing left
284
+ // to emit — and the flush this replaces could throw, erroring the response mid-document. See above.
279
285
  }
280
286
  const transformer = {
281
287
  async transform(chunk, controller) {
@@ -1 +1 @@
1
- {"version":3,"file":"flight-inject.js","sourceRoot":"","sources":["../../src/runtime/flight-inject.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Carrying the flight payload inside the HTML document, so the browser hydrates from bytes it already has\n * rather than fetching the page twice. The reader is `readFlightPayload` in `entry.client.tsx`.\n *\n * The wire format is `rsc-html-stream`'s — a run of `<script>(self.__FLIGHT_DATA||=[]).push(\"…\")</script>`\n * tags — but the implementation is first-party, because that package tests each HTML chunk for the\n * document trailer with `endsWith`: React's byte writer splits any write straddling its 2 kB views, so\n * `</body></html>` can arrive as two chunks and the document ends up with two trailers.\n */\n\n/**\n * {@link Transformer} plus the `cancel` hook the Streams standard added for a cancelled readable side —\n * declared here because the bundled lib types have not caught up with what Node calls.\n *\n * It matters because `cancel` is the only notification that a response ended *without* finishing, and both\n * users of it release a listener that would otherwise outlive the request.\n */\nexport type CancellableTransformer<I, O> = Transformer<I, O> & { cancel?: (reason?: unknown) => void };\n\nconst encoder = new TextEncoder();\n\n/** What React closes an `<html>` document with, and what this module re-emits after the last payload script. */\nconst TRAILER = '</body></html>';\nconst TRAILER_BYTES = encoder.encode(TRAILER);\n\n/**\n * A macrotask boundary: `setImmediate` where there is one, which is the current turn's check phase rather\n * than a timer, and `setTimeout` as the portable fallback. A Node timer has a 1ms floor that every HTML\n * flush would pay — 4.7ms → 3.0ms time-to-last-byte on a 30 kB payload.\n */\ntype TaskHandle = ReturnType<typeof setTimeout> | ReturnType<typeof setImmediate>;\nconst hasSetImmediate = typeof setImmediate === 'function';\nconst schedule: (fn: () => void) => TaskHandle = hasSetImmediate ? setImmediate : (fn) => setTimeout(fn, 0);\nconst unschedule = (handle: TaskHandle): void => {\n if (hasSetImmediate) clearImmediate(handle as ReturnType<typeof setImmediate>);\n else clearTimeout(handle as ReturnType<typeof setTimeout>);\n};\n\n/**\n * Escapes the two sequences that would end a `<script>` element early. Guarded by `includes('<')`, which is\n * far cheaper than two regex passes over 30 kB of payload that usually has no `<` in it. `</script` becomes\n * `</\\script`, not `<\\/script`, which would break the valid JS `0</script/`.\n */\nfunction escapeScript(script: string): string {\n return script.includes('<') ? script.replace(/<!--/g, '<\\\\!--').replace(/<\\/(script)/gi, '</\\\\$1') : script;\n}\n\n/**\n * The bytes of `chunk` as a latin1 string, which is the form `btoa` takes. Sliced because\n * `String.fromCharCode(...chunk)` passes one argument per byte and overflows the stack.\n */\nfunction latin1(chunk: Uint8Array): string {\n let out = '';\n for (let at = 0; at < chunk.length; at += 8192) out += String.fromCharCode(...chunk.subarray(at, at + 8192));\n return out;\n}\n\n/** Whether `buffer`'s first `length` bytes end with the document trailer. */\nfunction endsWithTrailer(buffer: Uint8Array, length: number): boolean {\n if (length < TRAILER_BYTES.length) return false;\n const from = length - TRAILER_BYTES.length;\n for (let i = 0; i < TRAILER_BYTES.length; i++) {\n if (buffer[from + i] !== TRAILER_BYTES[i]) return false;\n }\n return true;\n}\n\n/**\n * How many of `buffer`'s last bytes could still turn into the document trailer — the length of the longest\n * suffix of `buffer[0..length)` that is a prefix of it, `TRAILER_BYTES.length` for the whole thing.\n *\n * At most 14 bytes are ever held back, and in practice 0: a React flush ends with a closed tag, not with the\n * start of one. Cheaper than it looks, too — the first comparison rejects every suffix whose first byte is\n * not `<`.\n */\nfunction trailerPrefixLength(buffer: Uint8Array, length: number): number {\n candidate: for (let take = Math.min(length, TRAILER_BYTES.length); take > 0; take--) {\n const from = length - take;\n for (let i = 0; i < take; i++) if (buffer[from + i] !== TRAILER_BYTES[i]) continue candidate;\n return take;\n }\n return 0;\n}\n\n/**\n * The characters a CSP nonce may be made of: base64 and base64url, which is what every generator of one emits\n * — Hono's `secureHeaders()`, the only source the framework reads, produces base64 of 16 random bytes.\n *\n * The tag below is built by hand rather than by React, so this is the one attribute value in a rendered\n * document that nothing else escapes. The value is not attacker-controlled today, but the framework does not\n * own where it comes from: `c.get('secureHeadersNonce')` is an ordinary context variable that any middleware\n * can set, and a nonce carrying a `\"` would close the attribute and open a script-injection point in the\n * document. Anything outside this set is dropped rather than escaped, because a value made of other characters\n * is not a nonce at all: a page whose payload scripts the policy then refuses is the visible failure to have,\n * where an escaped garbage nonce would be a silent one.\n */\nconst NONCE_CHARS = /^[A-Za-z0-9+/=_-]+$/;\n\nexport function injectFlightPayload(\n rscStream: ReadableStream<Uint8Array>,\n options: { nonce?: string; onDone?: () => void } = {},\n): ReadableWritablePair<Uint8Array, Uint8Array> {\n const { nonce, onDone } = options;\n const safeNonce = nonce !== undefined && NONCE_CHARS.test(nonce) ? nonce : undefined;\n const scriptOpen = `<script${safeNonce ? ` nonce=\"${safeNonce}\"` : ''}>(self.__FLIGHT_DATA||=[]).push(`;\n const scriptClose = ')</script>';\n\n const { promise: flightWritten, resolve: flightDone } = Promise.withResolvers<void>();\n let startedFlight = false;\n\n const batch: Uint8Array[] = [];\n let boundary: TaskHandle | null = null;\n /**\n * The tail of the last batch that could still be the start of the document trailer, carried into the next\n * one. See {@link emitBatch}.\n */\n let carry: Uint8Array | null = null;\n\n /**\n * Set once the consumer has gone away, so nothing tries to enqueue into a readable that cannot take it.\n *\n * The `cancel` hook alone cannot set this: per the Streams standard, cancelling the readable after the\n * close algorithm has started skips the transformer's `cancel` entirely — and `flush` awaiting the whole\n * payload is precisely that window. So a failed enqueue counts as the signal too.\n */\n let cancelled = false;\n /** Held so {@link cancelled} can release the teed RSC branch rather than leaving it to be pumped. */\n let flightReader: ReadableStreamDefaultReader<Uint8Array> | null = null;\n\n /**\n * One permit per chunk the consumer has asked for: the backpressure a `TransformStream` alone does not give\n * this module.\n *\n * A transform's writable side parks a write while its readable's queue is over the mark, which covers the\n * HTML — but only once something has been enqueued, and `transform` defers that to a macrotask. So a\n * producer that writes a whole document without ever yielding the microtask queue is admitted in full, and\n * {@link writeFlight}, which pumps the payload into the same controller from a detached promise, is never\n * gated at all: measured, a consumer that read one chunk and stalled still pulled all 500 chunks of a test\n * payload into the queue, where this brings it down to 3. `controller.desiredSize` is no help — a transform\n * readable's high-water mark is 0, so it is never positive.\n *\n * The wrapper readable this function returns is the missing signal: its `pull` runs exactly when the\n * consumer wants another chunk, and releases one permit. Both producers — the HTML batcher and the payload\n * pump — take one before they enqueue, so a stalled client parks React instead of filling the process. The\n * Streams standard calls `pull` again only once the previous call has settled, and a call only settles once\n * a chunk has arrived, so permits are handed out one at a time and the queue is bounded at a chunk.\n *\n * What is left buffered is one React flush, because a flush has to leave here as a single chunk (see\n * {@link emitBatch}) — the same bound React itself holds while it builds one. The two enqueues in `flush`\n * are ungated for a related reason: the response is over by then, and parking its last two chunks on a\n * permit would only add a way for it not to end. They are also why the count can reach two at the very\n * end — an ungated enqueue settles a pull without spending its permit — which is harmless, there being\n * nothing left to produce.\n *\n * The cost is one extra stream hop and a microtask per chunk: 3.6ms → 4.0ms to push a 187 kB page through\n * this module with nothing else in the way, which is not the shape of a real response, where the socket\n * dominates by orders of magnitude.\n */\n let permits = 0;\n const waiting: Array<() => void> = [];\n\n /** Returns nothing when a permit was already free, so the common case costs no microtask. */\n function takePermit(): Promise<void> | undefined {\n // Nothing will release another permit once the consumer is gone, and there is nothing left to protect:\n // the caller goes on to a failing enqueue, which is what tears the pipeline down.\n if (cancelled) return;\n if (permits > 0) {\n permits--;\n return;\n }\n return new Promise<void>((resolve) => waiting.push(resolve));\n }\n\n function releasePermit(): void {\n const next = waiting.shift();\n if (next) next();\n else permits++;\n }\n\n /** Fires `onDone` at most once, however the response ended — both cancel paths can reach it. */\n let ended = false;\n function reportDone(): void {\n if (ended) return;\n ended = true;\n onDone?.();\n }\n\n /**\n * The consumer has gone away: stop producing, release everything the request was holding, and unpark both\n * producers so they see {@link cancelled} rather than a permit that will never come.\n *\n * Called from the wrapper readable's `cancel`, which is reliable, and from the transformer's, which the\n * standard skips once the close algorithm has started — so it has to be idempotent.\n */\n function teardown(reason?: unknown): void {\n cancelled = true;\n if (boundary) {\n unschedule(boundary);\n boundary = null;\n }\n batch.length = 0;\n carry = null;\n for (const resolve of waiting.splice(0)) resolve();\n // Otherwise the teed RSC branch keeps being pumped for a response nobody will read, and the tee's\n // other half buffers every chunk waiting for this one to catch up.\n flightReader?.cancel(reason).catch(() => {});\n // Unparks `flush` if it is waiting on a payload that will now never arrive.\n flightDone();\n reportDone();\n }\n\n /**\n * Emits the HTML buffered since the last boundary, holding back the document trailer for `flush` to re-emit\n * after the payload scripts.\n *\n * The batch is joined before the trailer is looked for, so one React split across two views is still found.\n * A trailer split across two *batches* is not a shape React produces — it writes its final flush in one\n * synchronous run — but this injector exists because `rsc-html-stream` made a narrower version of that same\n * assumption and was wrong (see the module header), so anything that could still become a trailer is held\n * back in {@link carry} rather than assumed not to be. Only `final`, the call from `flush`, may emit such a\n * tail: by then there is no next batch for it to complete.\n *\n * A tail that never does complete therefore leaves after the payload scripts rather than before them, which\n * is the deliberate half of the trade: releasing it the moment a script wants to go out is the very bug this\n * guards, because the next batch may be the rest of the trailer. It costs at most 13 bytes of a truncated\n * document arriving late, still inside `<body>`.\n */\n function emitBatch(controller: TransformStreamDefaultController<Uint8Array>, final = false): void {\n boundary = null;\n let total = carry?.byteLength ?? 0;\n for (const chunk of batch) total += chunk.byteLength;\n if (total === 0) {\n batch.length = 0;\n return;\n }\n\n const joined = new Uint8Array(total);\n let at = 0;\n if (carry) {\n joined.set(carry, at);\n at += carry.byteLength;\n carry = null;\n }\n for (const chunk of batch) {\n joined.set(chunk, at);\n at += chunk.byteLength;\n }\n batch.length = 0;\n\n const held = final ? (endsWithTrailer(joined, total) ? TRAILER_BYTES.length : 0) : trailerPrefixLength(joined, total);\n const end = total - held;\n // Copied rather than a `subarray`, which would keep the whole joined flush alive for 14 bytes.\n if (held > 0 && !final) carry = joined.slice(end);\n if (end > 0) controller.enqueue(joined.subarray(0, end));\n }\n\n async function writeFlight(controller: TransformStreamDefaultController<Uint8Array>): Promise<void> {\n const reader = (flightReader = rscStream.getReader());\n // `fatal`, so a chunk that split a multi-byte character throws instead of emitting U+FFFD; the catch\n // below falls back to a byte-exact encoding for it.\n const decoder = new TextDecoder('utf-8', { fatal: true });\n const push = async (literal: string): Promise<void> => {\n const permit = takePermit();\n if (permit) await permit;\n if (cancelled) return;\n controller.enqueue(encoder.encode(scriptOpen + literal + scriptClose));\n };\n for (;;) {\n if (cancelled) return;\n const { done, value } = await reader.read();\n if (done) break;\n // Only the decode is guarded: a `push` inside the same `try` would answer a dead controller by\n // re-encoding the chunk and enqueueing it again.\n let literal: string;\n try {\n literal = escapeScript(JSON.stringify(decoder.decode(value, { stream: true })));\n } catch {\n literal = `Uint8Array.from(atob(${JSON.stringify(btoa(latin1(value)))}), m => m.codePointAt(0))`;\n }\n if (cancelled) return;\n // A failed enqueue means the consumer is gone: release the RSC branch so `flush` unparks now rather\n // than whenever the payload would have ended on its own.\n try {\n await 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) await push(escapeScript(JSON.stringify(remaining)));\n }\n\n const transformer: CancellableTransformer<Uint8Array, Uint8Array> = {\n async transform(chunk, controller) {\n if (boundary) {\n batch.push(chunk);\n return;\n }\n // The permit is taken once per *batch*, not once per chunk: React writes a whole flush in one microtask\n // cascade and the flush has to leave here as one chunk (see `emitBatch`), so a per-chunk gate would\n // hand the consumer one chunk per read instead. `transform` is never re-entered concurrently — the\n // writable side serializes it on the promise this returns — so nothing else can claim the batch in\n // between.\n const permit = takePermit();\n if (permit) await permit;\n batch.push(chunk);\n // A macrotask, not a microtask: React writes a whole flush in one synchronous run but `pipeThrough`\n // delivers it one microtask at a time, and a script injected between two chunks lands inside a tag.\n boundary = schedule(() => {\n try {\n emitBatch(controller);\n } catch (error) {\n // A dead consumer, all but always. `teardown` rather than `flightDone` alone: a payload pump parked\n // on a permit has to be unparked too, and the RSC branch it holds released.\n controller.error(error);\n teardown(error);\n return;\n }\n if (!startedFlight) {\n startedFlight = true;\n // Deliberately not awaited: this runs inside a scheduled callback with nothing to return to, and\n // the chain already routes a write failure to `controller.error` before settling `flightDone`.\n void writeFlight(controller)\n .catch((error) => controller.error(error))\n .then(flightDone);\n }\n });\n },\n async flush(controller) {\n // Both of these have to happen *before* the await, and in this order.\n try {\n // Anything still batched — or carried — belongs ahead of the payload scripts, which sit at the end of\n // `<body>`. Called unconditionally: a `carry` outlives its boundary, and an empty batch returns early.\n if (boundary) unschedule(boundary);\n emitBatch(controller, true);\n // A writable side that closed without ever reaching `transform` — an HTML stream with no chunks at\n // all — leaves nothing to have started the payload, and `flightDone` is only ever called from that\n // chain or from `cancel`. Without this the await below parks on a promise nothing will settle and the\n // response never ends. `cancelled` short-circuits it: there is nowhere to write the payload to.\n if (!startedFlight) {\n startedFlight = true;\n if (cancelled) flightDone();\n else\n void writeFlight(controller)\n .catch((error) => controller.error(error))\n .then(flightDone);\n }\n } catch (error) {\n // The consumer went away while the batch was being emitted, so there is nothing left to write to and\n // nothing to wait for. Settled explicitly rather than left dangling, so the payload chain is released.\n teardown(error);\n return;\n }\n\n await flightWritten;\n // That await spans the whole payload, and the consumer can go away inside it — with `cancel` skipped\n // (see `cancelled`), a throwing enqueue is the only signal. Unguarded it rejects `flush`, which\n // nothing owns and which surfaces as an unhandled rejection.\n try {\n if (!cancelled) controller.enqueue(encoder.encode(TRAILER));\n } catch {\n // Nowhere left to put the trailer. A response the client abandoned is not a fault.\n cancelled = true;\n flightReader?.cancel().catch(() => {});\n } finally {\n // A `finally` because `onDone` releases the abort forwarder in `renderComponent`, however this ended.\n reportDone();\n }\n },\n cancel(reason) {\n teardown(reason);\n },\n };\n\n const inner = new TransformStream<Uint8Array, Uint8Array>(transformer);\n const innerReader = inner.readable.getReader();\n /**\n * A pull-driven wrapper around the transform's readable, and the only reason this function does not simply\n * return the transform: `pull` runs once per chunk the consumer takes, which is the demand signal the two\n * producers park on (see {@link takePermit}). It is also the one cancel notification the standard never\n * skips, which is what makes {@link teardown} reliable.\n */\n const readable = new ReadableStream<Uint8Array>({\n async pull(controller) {\n releasePermit();\n const { done, value } = await innerReader.read();\n try {\n if (done) controller.close();\n else controller.enqueue(value);\n } catch {\n // Cancelled while this pull was in flight. `teardown` has already run; there is nobody to hand it to.\n }\n },\n cancel(reason) {\n teardown(reason);\n return innerReader.cancel(reason);\n },\n });\n return { readable, writable: inner.writable };\n}\n"]}
1
+ {"version":3,"file":"flight-inject.js","sourceRoot":"","sources":["../../src/runtime/flight-inject.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Carrying the flight payload inside the HTML document, so the browser hydrates from bytes it already has\n * rather than fetching the page twice. The reader is `readFlightPayload` in `entry.client.tsx`.\n *\n * The wire format is `rsc-html-stream`'s — a run of `<script>(self.__FLIGHT_DATA||=[]).push(\"…\")</script>`\n * tags — but the implementation is first-party, because that package tests each HTML chunk for the\n * document trailer with `endsWith`: React's byte writer splits any write straddling its 2 kB views, so\n * `</body></html>` can arrive as two chunks and the document ends up with two trailers.\n */\n\n/**\n * {@link Transformer} plus the `cancel` hook the Streams standard added for a cancelled readable side —\n * declared here because the bundled lib types have not caught up with what Node calls.\n *\n * It matters because `cancel` is the only notification that a response ended *without* finishing, and both\n * users of it release a listener that would otherwise outlive the request.\n */\nexport type CancellableTransformer<I, O> = Transformer<I, O> & { cancel?: (reason?: unknown) => void };\n\nconst encoder = new TextEncoder();\n\n/** What React closes an `<html>` document with, and what this module re-emits after the last payload script. */\nconst TRAILER = '</body></html>';\nconst TRAILER_BYTES = encoder.encode(TRAILER);\n\n/**\n * A macrotask boundary: `setImmediate` where there is one, which is the current turn's check phase rather\n * than a timer, and `setTimeout` as the portable fallback. A Node timer has a 1ms floor that every HTML\n * flush would pay — 4.7ms → 3.0ms time-to-last-byte on a 30 kB payload.\n */\ntype TaskHandle = ReturnType<typeof setTimeout> | ReturnType<typeof setImmediate>;\nconst hasSetImmediate = typeof setImmediate === 'function';\nconst schedule: (fn: () => void) => TaskHandle = hasSetImmediate ? setImmediate : (fn) => setTimeout(fn, 0);\nconst unschedule = (handle: TaskHandle): void => {\n if (hasSetImmediate) clearImmediate(handle as ReturnType<typeof setImmediate>);\n else clearTimeout(handle as ReturnType<typeof setTimeout>);\n};\n\n/**\n * Escapes the two sequences that would end a `<script>` element early. Guarded by `includes('<')`, which is\n * far cheaper than two regex passes over 30 kB of payload that usually has no `<` in it. `</script` becomes\n * `</\\script`, not `<\\/script`, which would break the valid JS `0</script/`.\n */\nfunction escapeScript(script: string): string {\n return script.includes('<') ? script.replace(/<!--/g, '<\\\\!--').replace(/<\\/(script)/gi, '</\\\\$1') : script;\n}\n\n/**\n * The bytes of `chunk` as a latin1 string, which is the form `btoa` takes. Sliced because\n * `String.fromCharCode(...chunk)` passes one argument per byte and overflows the stack.\n */\nfunction latin1(chunk: Uint8Array): string {\n let out = '';\n for (let at = 0; at < chunk.length; at += 8192) out += String.fromCharCode(...chunk.subarray(at, at + 8192));\n return out;\n}\n\n/** Whether `buffer`'s first `length` bytes end with the document trailer. */\nfunction endsWithTrailer(buffer: Uint8Array, length: number): boolean {\n if (length < TRAILER_BYTES.length) return false;\n const from = length - TRAILER_BYTES.length;\n for (let i = 0; i < TRAILER_BYTES.length; i++) {\n if (buffer[from + i] !== TRAILER_BYTES[i]) return false;\n }\n return true;\n}\n\n/**\n * How many of `buffer`'s last bytes could still turn into the document trailer — the length of the longest\n * suffix of `buffer[0..length)` that is a prefix of it, `TRAILER_BYTES.length` for the whole thing.\n *\n * At most 14 bytes are ever held back, and in practice 0: a React flush ends with a closed tag, not with the\n * start of one. Cheaper than it looks, too — the first comparison rejects every suffix whose first byte is\n * not `<`.\n */\nfunction trailerPrefixLength(buffer: Uint8Array, length: number): number {\n candidate: for (let take = Math.min(length, TRAILER_BYTES.length); take > 0; take--) {\n const from = length - take;\n for (let i = 0; i < take; i++) if (buffer[from + i] !== TRAILER_BYTES[i]) continue candidate;\n return take;\n }\n return 0;\n}\n\n/**\n * The characters a CSP nonce may be made of: base64 and base64url, which is what every generator of one emits\n * — Hono's `secureHeaders()`, the only source the framework reads, produces base64 of 16 random bytes.\n *\n * The tag below is built by hand rather than by React, so this is the one attribute value in a rendered\n * document that nothing else escapes. The value is not attacker-controlled today, but the framework does not\n * own where it comes from: `c.get('secureHeadersNonce')` is an ordinary context variable that any middleware\n * can set, and a nonce carrying a `\"` would close the attribute and open a script-injection point in the\n * document. Anything outside this set is dropped rather than escaped, because a value made of other characters\n * is not a nonce at all: a page whose payload scripts the policy then refuses is the visible failure to have,\n * where an escaped garbage nonce would be a silent one.\n */\nconst NONCE_CHARS = /^[A-Za-z0-9+/=_-]+$/;\n\nexport function injectFlightPayload(\n rscStream: ReadableStream<Uint8Array>,\n options: { nonce?: string; onDone?: () => void } = {},\n): ReadableWritablePair<Uint8Array, Uint8Array> {\n const { nonce, onDone } = options;\n const safeNonce = nonce !== undefined && NONCE_CHARS.test(nonce) ? nonce : undefined;\n const scriptOpen = `<script${safeNonce ? ` nonce=\"${safeNonce}\"` : ''}>(self.__FLIGHT_DATA||=[]).push(`;\n const scriptClose = ')</script>';\n\n const { promise: flightWritten, resolve: flightDone } = Promise.withResolvers<void>();\n let startedFlight = false;\n\n const batch: Uint8Array[] = [];\n let boundary: TaskHandle | null = null;\n /**\n * The tail of the last batch that could still be the start of the document trailer, carried into the next\n * one. See {@link emitBatch}.\n */\n let carry: Uint8Array | null = null;\n\n /**\n * Set once the consumer has gone away, so nothing tries to enqueue into a readable that cannot take it.\n *\n * The `cancel` hook alone cannot set this: per the Streams standard, cancelling the readable after the\n * close algorithm has started skips the transformer's `cancel` entirely — and `flush` awaiting the whole\n * payload is precisely that window. So a failed enqueue counts as the signal too.\n */\n let cancelled = false;\n /** Held so {@link cancelled} can release the teed RSC branch rather than leaving it to be pumped. */\n let flightReader: ReadableStreamDefaultReader<Uint8Array> | null = null;\n\n /**\n * One permit per chunk the consumer has asked for: the backpressure a `TransformStream` alone does not give\n * this module.\n *\n * A transform's writable side parks a write while its readable's queue is over the mark, which covers the\n * HTML — but only once something has been enqueued, and `transform` defers that to a macrotask. So a\n * producer that writes a whole document without ever yielding the microtask queue is admitted in full, and\n * {@link writeFlight}, which pumps the payload into the same controller from a detached promise, is never\n * gated at all: measured, a consumer that read one chunk and stalled still pulled all 500 chunks of a test\n * payload into the queue, where this brings it down to 3. `controller.desiredSize` is no help — a transform\n * readable's high-water mark is 0, so it is never positive.\n *\n * The wrapper readable this function returns is the missing signal: its `pull` runs exactly when the\n * consumer wants another chunk, and releases one permit. Both producers — the HTML batcher and the payload\n * pump — take one before they enqueue, so a stalled client parks React instead of filling the process. The\n * Streams standard calls `pull` again only once the previous call has settled, and a call only settles once\n * a chunk has arrived, so permits are handed out one at a time and the queue is bounded at a chunk.\n *\n * What is left buffered is one React flush, because a flush has to leave here as a single chunk (see\n * {@link emitBatch}) — the same bound React itself holds while it builds one. The two enqueues in `flush`\n * are ungated for a related reason: the response is over by then, and parking its last two chunks on a\n * permit would only add a way for it not to end. They are also why the count can reach two at the very\n * end — an ungated enqueue settles a pull without spending its permit — which is harmless, there being\n * nothing left to produce.\n *\n * The cost is one extra stream hop and a microtask per chunk: 3.6ms → 4.0ms to push a 187 kB page through\n * this module with nothing else in the way, which is not the shape of a real response, where the socket\n * dominates by orders of magnitude.\n */\n let permits = 0;\n const waiting: Array<() => void> = [];\n\n /** Returns nothing when a permit was already free, so the common case costs no microtask. */\n function takePermit(): Promise<void> | undefined {\n // Nothing will release another permit once the consumer is gone, and there is nothing left to protect:\n // the caller goes on to a failing enqueue, which is what tears the pipeline down.\n if (cancelled) return;\n if (permits > 0) {\n permits--;\n return;\n }\n return new Promise<void>((resolve) => waiting.push(resolve));\n }\n\n function releasePermit(): void {\n const next = waiting.shift();\n if (next) next();\n else permits++;\n }\n\n /** Fires `onDone` at most once, however the response ended — both cancel paths can reach it. */\n let ended = false;\n function reportDone(): void {\n if (ended) return;\n ended = true;\n onDone?.();\n }\n\n /**\n * The consumer has gone away: stop producing, release everything the request was holding, and unpark both\n * producers so they see {@link cancelled} rather than a permit that will never come.\n *\n * Called from the wrapper readable's `cancel`, which is reliable, and from the transformer's, which the\n * standard skips once the close algorithm has started — so it has to be idempotent.\n */\n function teardown(reason?: unknown): void {\n cancelled = true;\n if (boundary) {\n unschedule(boundary);\n boundary = null;\n }\n batch.length = 0;\n carry = null;\n for (const resolve of waiting.splice(0)) resolve();\n // Otherwise the teed RSC branch keeps being pumped for a response nobody will read, and the tee's\n // other half buffers every chunk waiting for this one to catch up.\n flightReader?.cancel(reason).catch(() => {});\n // Unparks `flush` if it is waiting on a payload that will now never arrive.\n flightDone();\n reportDone();\n }\n\n /**\n * Emits the HTML buffered since the last boundary, holding back the document trailer for `flush` to re-emit\n * after the payload scripts.\n *\n * The batch is joined before the trailer is looked for, so one React split across two views is still found.\n * A trailer split across two *batches* is not a shape React produces — it writes its final flush in one\n * synchronous run — but this injector exists because `rsc-html-stream` made a narrower version of that same\n * assumption and was wrong (see the module header), so anything that could still become a trailer is held\n * back in {@link carry} rather than assumed not to be. Only `final`, the call from `flush`, may emit such a\n * tail: by then there is no next batch for it to complete.\n *\n * A tail that never does complete therefore leaves after the payload scripts rather than before them, which\n * is the deliberate half of the trade: releasing it the moment a script wants to go out is the very bug this\n * guards, because the next batch may be the rest of the trailer. It costs at most 13 bytes of a truncated\n * document arriving late, still inside `<body>`.\n */\n function emitBatch(controller: TransformStreamDefaultController<Uint8Array>, final = false): void {\n boundary = null;\n let total = carry?.byteLength ?? 0;\n for (const chunk of batch) total += chunk.byteLength;\n if (total === 0) {\n batch.length = 0;\n return;\n }\n\n const joined = new Uint8Array(total);\n let at = 0;\n if (carry) {\n joined.set(carry, at);\n at += carry.byteLength;\n carry = null;\n }\n for (const chunk of batch) {\n joined.set(chunk, at);\n at += chunk.byteLength;\n }\n batch.length = 0;\n\n const held = final ? (endsWithTrailer(joined, total) ? TRAILER_BYTES.length : 0) : trailerPrefixLength(joined, total);\n const end = total - held;\n // Copied rather than a `subarray`, which would keep the whole joined flush alive for 14 bytes.\n if (held > 0 && !final) carry = joined.slice(end);\n if (end > 0) controller.enqueue(joined.subarray(0, end));\n }\n\n async function writeFlight(controller: TransformStreamDefaultController<Uint8Array>): Promise<void> {\n const reader = (flightReader = rscStream.getReader());\n // One chunk at a time, deliberately *not* `{ stream: true }`: anything that is not valid UTF-8 on its\n // own — a binary row's bytes, or a chunk that split a multi-byte character — throws, and the catch below\n // re-encodes that whole chunk byte-exactly.\n //\n // Streaming is what makes that fallback wrong. A chunk ending mid-character leaves its lead bytes inside\n // the decoder to be emitted with the next chunk; when the next chunk is a binary row and throws, those\n // bytes are in neither `decode()`'s return nor `value`, and the client reassembles a payload short by\n // them. It also left the end-of-stream flush able to throw from outside any `try`.\n //\n // `ignoreBOM` because a per-chunk decode re-runs the BOM check on every call: without it any chunk whose\n // first three bytes are EF BB BF would silently lose them.\n const decoder = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });\n const push = async (literal: string): Promise<void> => {\n const permit = takePermit();\n if (permit) await permit;\n if (cancelled) return;\n controller.enqueue(encoder.encode(scriptOpen + literal + scriptClose));\n };\n for (;;) {\n if (cancelled) return;\n const { done, value } = await reader.read();\n if (done) break;\n // Only the decode is guarded: a `push` inside the same `try` would answer a dead controller by\n // re-encoding the chunk and enqueueing it again.\n let literal: string;\n try {\n literal = escapeScript(JSON.stringify(decoder.decode(value)));\n } catch {\n literal = `Uint8Array.from(atob(${JSON.stringify(btoa(latin1(value)))}), m => m.codePointAt(0))`;\n }\n if (cancelled) return;\n // A failed enqueue means the consumer is gone: release the RSC branch so `flush` unparks now rather\n // than whenever the payload would have ended on its own.\n try {\n await push(literal);\n } catch {\n cancelled = true;\n reader.cancel().catch(() => {});\n return;\n }\n }\n // No end-of-stream flush: a non-streaming decoder holds nothing between calls, so there is nothing left\n // to emit — and the flush this replaces could throw, erroring the response mid-document. See above.\n }\n\n const transformer: CancellableTransformer<Uint8Array, Uint8Array> = {\n async transform(chunk, controller) {\n if (boundary) {\n batch.push(chunk);\n return;\n }\n // The permit is taken once per *batch*, not once per chunk: React writes a whole flush in one microtask\n // cascade and the flush has to leave here as one chunk (see `emitBatch`), so a per-chunk gate would\n // hand the consumer one chunk per read instead. `transform` is never re-entered concurrently — the\n // writable side serializes it on the promise this returns — so nothing else can claim the batch in\n // between.\n const permit = takePermit();\n if (permit) await permit;\n batch.push(chunk);\n // A macrotask, not a microtask: React writes a whole flush in one synchronous run but `pipeThrough`\n // delivers it one microtask at a time, and a script injected between two chunks lands inside a tag.\n boundary = schedule(() => {\n try {\n emitBatch(controller);\n } catch (error) {\n // A dead consumer, all but always. `teardown` rather than `flightDone` alone: a payload pump parked\n // on a permit has to be unparked too, and the RSC branch it holds released.\n controller.error(error);\n teardown(error);\n return;\n }\n if (!startedFlight) {\n startedFlight = true;\n // Deliberately not awaited: this runs inside a scheduled callback with nothing to return to, and\n // the chain already routes a write failure to `controller.error` before settling `flightDone`.\n void writeFlight(controller)\n .catch((error) => controller.error(error))\n .then(flightDone);\n }\n });\n },\n async flush(controller) {\n // Both of these have to happen *before* the await, and in this order.\n try {\n // Anything still batched — or carried — belongs ahead of the payload scripts, which sit at the end of\n // `<body>`. Called unconditionally: a `carry` outlives its boundary, and an empty batch returns early.\n if (boundary) unschedule(boundary);\n emitBatch(controller, true);\n // A writable side that closed without ever reaching `transform` — an HTML stream with no chunks at\n // all — leaves nothing to have started the payload, and `flightDone` is only ever called from that\n // chain or from `cancel`. Without this the await below parks on a promise nothing will settle and the\n // response never ends. `cancelled` short-circuits it: there is nowhere to write the payload to.\n if (!startedFlight) {\n startedFlight = true;\n if (cancelled) flightDone();\n else\n void writeFlight(controller)\n .catch((error) => controller.error(error))\n .then(flightDone);\n }\n } catch (error) {\n // The consumer went away while the batch was being emitted, so there is nothing left to write to and\n // nothing to wait for. Settled explicitly rather than left dangling, so the payload chain is released.\n teardown(error);\n return;\n }\n\n await flightWritten;\n // That await spans the whole payload, and the consumer can go away inside it — with `cancel` skipped\n // (see `cancelled`), a throwing enqueue is the only signal. Unguarded it rejects `flush`, which\n // nothing owns and which surfaces as an unhandled rejection.\n try {\n if (!cancelled) controller.enqueue(encoder.encode(TRAILER));\n } catch {\n // Nowhere left to put the trailer. A response the client abandoned is not a fault.\n cancelled = true;\n flightReader?.cancel().catch(() => {});\n } finally {\n // A `finally` because `onDone` releases the abort forwarder in `renderComponent`, however this ended.\n reportDone();\n }\n },\n cancel(reason) {\n teardown(reason);\n },\n };\n\n const inner = new TransformStream<Uint8Array, Uint8Array>(transformer);\n const innerReader = inner.readable.getReader();\n /**\n * A pull-driven wrapper around the transform's readable, and the only reason this function does not simply\n * return the transform: `pull` runs once per chunk the consumer takes, which is the demand signal the two\n * producers park on (see {@link takePermit}). It is also the one cancel notification the standard never\n * skips, which is what makes {@link teardown} reliable.\n */\n const readable = new ReadableStream<Uint8Array>({\n async pull(controller) {\n releasePermit();\n const { done, value } = await innerReader.read();\n try {\n if (done) controller.close();\n else controller.enqueue(value);\n } catch {\n // Cancelled while this pull was in flight. `teardown` has already run; there is nobody to hand it to.\n }\n },\n cancel(reason) {\n teardown(reason);\n return innerReader.cancel(reason);\n },\n });\n return { readable, writable: inner.writable };\n}\n"]}
@@ -42,4 +42,3 @@ export interface ReloadReason {
42
42
  * would re-request the same 404 for as long as the tab stays open.
43
43
  */
44
44
  export declare function walkHotUpdates(hot: HotRuntime, currentHash: () => string, targetHash: () => string | undefined): Promise<ReloadReason | null>;
45
- //# sourceMappingURL=hot-update.d.ts.map
@@ -71,6 +71,12 @@ export declare function RouterProvider({ href, params, children }: {
71
71
  * during SSR — no hydration flicker — and update on every navigation. `router` holds the imperative
72
72
  * actions plus a `pending` flag, `true` while a soft navigation is in flight.
73
73
  *
74
+ * **On a `render: 'static'` route `url` is frozen at build time**, origin included and query empty. The
75
+ * payload is one prerendered set of bytes and this reads the `href` in it, so it is the page's own
76
+ * `PageProps.url` — the same value, not a live one. A page whose output depends on the query wants
77
+ * `render: 'dynamic'`; a component that only needs it after hydration can read `location.search` in an
78
+ * effect.
79
+ *
74
80
  * Hooks can't run in a server component; read the same data there from `getRequestContext()`.
75
81
  *
76
82
  * @example
@@ -98,4 +104,3 @@ export declare function RouterProvider({ href, params, children }: {
98
104
  * @see {@link https://www.rshono.com/docs/pages#client-components | Docs — client components}
99
105
  */
100
106
  export declare function useNavigation(): NavigationState;
101
- //# sourceMappingURL=navigation.d.ts.map
@@ -29,6 +29,12 @@ export function RouterProvider({ href, params, children }) {
29
29
  * during SSR — no hydration flicker — and update on every navigation. `router` holds the imperative
30
30
  * actions plus a `pending` flag, `true` while a soft navigation is in flight.
31
31
  *
32
+ * **On a `render: 'static'` route `url` is frozen at build time**, origin included and query empty. The
33
+ * payload is one prerendered set of bytes and this reads the `href` in it, so it is the page's own
34
+ * `PageProps.url` — the same value, not a live one. A page whose output depends on the query wants
35
+ * `render: 'dynamic'`; a component that only needs it after hydration can read `location.search` in an
36
+ * effect.
37
+ *
32
38
  * Hooks can't run in a server component; read the same data there from `getRequestContext()`.
33
39
  *
34
40
  * @example
@@ -1 +1 @@
1
- {"version":3,"file":"navigation.js","sourceRoot":"","sources":["../../src/runtime/navigation.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,OAAO,EAAkB,MAAM,OAAO,CAAC;AAmD3E,MAAM,IAAI,GAAG,GAAG,EAAE,GAAE,CAAC,CAAC;AAEtB,MAAM,aAAa,GAAqB,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;AAEhI;;;;GAIG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,aAAa,CAAmB,aAAa,CAAC,CAAC;AAE5E,MAAM,iBAAiB,GAAG,aAAa,CAAyB,IAAI,CAAC,CAAC;AAEtE;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAyE;IAC9H,MAAM,MAAM,GAAG,UAAU,CAAC,aAAa,CAAC,CAAC;IACzC,MAAM,KAAK,GAAG,OAAO,CAAkB,GAAG,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,IAAI,GAAG,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAE/G,OAAO,KAAC,iBAAiB,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,YAAG,QAAQ,GAA8B,CAAC;AAC3F,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,MAAM,UAAU,aAAa;IAC3B,MAAM,KAAK,GAAG,UAAU,CAAC,iBAAiB,CAAC,CAAC;IAC5C,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CACb,mKAAmK,CACpK,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC","sourcesContent":["'use client';\n\nimport { createContext, useContext, useMemo, type ReactNode } from 'react';\n\n/**\n * Imperative navigation actions, reached as `useNavigation().router`.\n *\n * Every action is a **soft** navigation: the page's flight payload is fetched and applied in place, so\n * client component state outside the changed subtree survives. Off-site hrefs — and a traversal that leaves\n * the app — fall back to a full load.\n *\n * Soft navigation is the browser's\n * {@link https://developer.mozilla.org/en-US/docs/Web/API/Navigation_API | Navigation API}; where that is\n * missing, every action below is still correct and simply performs a real browser load.\n *\n * @example\n * ```tsx\n * const { router } = useNavigation();\n * router.push('/dashboard'); // navigate, new history entry\n * router.replace('/login'); // navigate, no new entry\n * router.back(); // one entry back, as the browser's button does\n * router.forward(); // one entry forward\n * router.refresh(); // re-run this route's server components\n * ```\n */\nexport interface NavigationRouter {\n /** Navigates to `href` and pushes a new history entry. */\n push(href: string): void;\n /** Navigates to `href`, replacing the current history entry instead of adding one. */\n replace(href: string): void;\n /** Steps one entry back in the browser's session history. Nothing to go back to is a no-op. */\n back(): void;\n /** Steps one entry forward in the browser's session history. A no-op on the newest entry. */\n forward(): void;\n /** Re-fetches the current route from the server, re-running its server components. */\n refresh(): void;\n /** `true` while a soft navigation is in flight — use it to disable controls or show a spinner. */\n pending: boolean;\n}\n\n/** The current location plus the {@link NavigationRouter}, as returned by {@link useNavigation}. */\nexport interface NavigationState {\n /**\n * The full current {@link URL}. A fresh instance per navigation, so mutating it affects nothing else\n * — it is not written back to the address bar.\n */\n url: URL;\n /** Matched route params for the current page, e.g. `{ id: '42' }` for `/profile/:id`. */\n params: Record<string, string>;\n /** Imperative navigation actions and the `pending` flag. */\n router: NavigationRouter;\n}\n\nconst noop = () => {};\n\nconst defaultRouter: NavigationRouter = { push: noop, replace: noop, back: noop, forward: noop, refresh: noop, pending: false };\n\n/**\n * Carries the live {@link NavigationRouter} from the hydration runtime down to {@link RouterProvider}.\n *\n * @internal\n */\nexport const RouterContext = createContext<NavigationRouter>(defaultRouter);\n\nconst NavigationContext = createContext<NavigationState | null>(null);\n\n/**\n * Publishes the per-render location and params for {@link useNavigation} to read. The RSC entry wraps\n * every page in one.\n *\n * @internal\n */\nexport function RouterProvider({ href, params, children }: { href: string; params: Record<string, string>; children: ReactNode }) {\n const router = useContext(RouterContext);\n const value = useMemo<NavigationState>(() => ({ url: new URL(href), params, router }), [href, params, router]);\n\n return <NavigationContext.Provider value={value}>{children}</NavigationContext.Provider>;\n}\n\n/**\n * Reactive access to the current URL and programmatic navigation, in one hook. Call it from a\n * `'use client'` component.\n *\n * `url` and `params` are computed on the server and travel in the flight payload, so they are correct\n * during SSR — no hydration flicker — and update on every navigation. `router` holds the imperative\n * actions plus a `pending` flag, `true` while a soft navigation is in flight.\n *\n * Hooks can't run in a server component; read the same data there from `getRequestContext()`.\n *\n * @example\n * ```tsx\n * 'use client';\n * import { useNavigation } from '@rshono/core/client';\n *\n * export function NextPage() {\n * const { url, router } = useNavigation();\n * const page = Number(url.searchParams.get('page') ?? '1');\n * return (\n * <button disabled={router.pending} onClick={() => router.push(`${url.pathname}?page=${page + 1}`)}>\n * Next {router.pending ? '…' : ''}\n * </button>\n * );\n * }\n * ```\n *\n * @returns The current {@link NavigationState}: `url` and `params`, plus `router`\n * ({@link NavigationRouter}) with `push` / `replace` / `back` / `forward` / `refresh` / `pending`.\n * @throws If called outside a page's React tree, where there is no navigation\n * context to read.\n *\n * @see {@link https://www.rshono.com/docs/api#rshonocoreclient | Docs — `@rshono/core/client`}\n * @see {@link https://www.rshono.com/docs/pages#client-components | Docs — client components}\n */\nexport function useNavigation(): NavigationState {\n const value = useContext(NavigationContext);\n if (!value) {\n throw new Error(\n \"[rshono] useNavigation() must be called inside a 'use client' component rendered by a page. In a server component, read the URL from getRequestContext() instead.\",\n );\n }\n return value;\n}\n"]}
1
+ {"version":3,"file":"navigation.js","sourceRoot":"","sources":["../../src/runtime/navigation.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,OAAO,EAAkB,MAAM,OAAO,CAAC;AAmD3E,MAAM,IAAI,GAAG,GAAG,EAAE,GAAE,CAAC,CAAC;AAEtB,MAAM,aAAa,GAAqB,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;AAEhI;;;;GAIG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,aAAa,CAAmB,aAAa,CAAC,CAAC;AAE5E,MAAM,iBAAiB,GAAG,aAAa,CAAyB,IAAI,CAAC,CAAC;AAEtE;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAyE;IAC9H,MAAM,MAAM,GAAG,UAAU,CAAC,aAAa,CAAC,CAAC;IACzC,MAAM,KAAK,GAAG,OAAO,CAAkB,GAAG,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,IAAI,GAAG,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAE/G,OAAO,KAAC,iBAAiB,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,YAAG,QAAQ,GAA8B,CAAC;AAC3F,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,MAAM,UAAU,aAAa;IAC3B,MAAM,KAAK,GAAG,UAAU,CAAC,iBAAiB,CAAC,CAAC;IAC5C,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CACb,mKAAmK,CACpK,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC","sourcesContent":["'use client';\n\nimport { createContext, useContext, useMemo, type ReactNode } from 'react';\n\n/**\n * Imperative navigation actions, reached as `useNavigation().router`.\n *\n * Every action is a **soft** navigation: the page's flight payload is fetched and applied in place, so\n * client component state outside the changed subtree survives. Off-site hrefs — and a traversal that leaves\n * the app — fall back to a full load.\n *\n * Soft navigation is the browser's\n * {@link https://developer.mozilla.org/en-US/docs/Web/API/Navigation_API | Navigation API}; where that is\n * missing, every action below is still correct and simply performs a real browser load.\n *\n * @example\n * ```tsx\n * const { router } = useNavigation();\n * router.push('/dashboard'); // navigate, new history entry\n * router.replace('/login'); // navigate, no new entry\n * router.back(); // one entry back, as the browser's button does\n * router.forward(); // one entry forward\n * router.refresh(); // re-run this route's server components\n * ```\n */\nexport interface NavigationRouter {\n /** Navigates to `href` and pushes a new history entry. */\n push(href: string): void;\n /** Navigates to `href`, replacing the current history entry instead of adding one. */\n replace(href: string): void;\n /** Steps one entry back in the browser's session history. Nothing to go back to is a no-op. */\n back(): void;\n /** Steps one entry forward in the browser's session history. A no-op on the newest entry. */\n forward(): void;\n /** Re-fetches the current route from the server, re-running its server components. */\n refresh(): void;\n /** `true` while a soft navigation is in flight — use it to disable controls or show a spinner. */\n pending: boolean;\n}\n\n/** The current location plus the {@link NavigationRouter}, as returned by {@link useNavigation}. */\nexport interface NavigationState {\n /**\n * The full current {@link URL}. A fresh instance per navigation, so mutating it affects nothing else\n * — it is not written back to the address bar.\n */\n url: URL;\n /** Matched route params for the current page, e.g. `{ id: '42' }` for `/profile/:id`. */\n params: Record<string, string>;\n /** Imperative navigation actions and the `pending` flag. */\n router: NavigationRouter;\n}\n\nconst noop = () => {};\n\nconst defaultRouter: NavigationRouter = { push: noop, replace: noop, back: noop, forward: noop, refresh: noop, pending: false };\n\n/**\n * Carries the live {@link NavigationRouter} from the hydration runtime down to {@link RouterProvider}.\n *\n * @internal\n */\nexport const RouterContext = createContext<NavigationRouter>(defaultRouter);\n\nconst NavigationContext = createContext<NavigationState | null>(null);\n\n/**\n * Publishes the per-render location and params for {@link useNavigation} to read. The RSC entry wraps\n * every page in one.\n *\n * @internal\n */\nexport function RouterProvider({ href, params, children }: { href: string; params: Record<string, string>; children: ReactNode }) {\n const router = useContext(RouterContext);\n const value = useMemo<NavigationState>(() => ({ url: new URL(href), params, router }), [href, params, router]);\n\n return <NavigationContext.Provider value={value}>{children}</NavigationContext.Provider>;\n}\n\n/**\n * Reactive access to the current URL and programmatic navigation, in one hook. Call it from a\n * `'use client'` component.\n *\n * `url` and `params` are computed on the server and travel in the flight payload, so they are correct\n * during SSR — no hydration flicker — and update on every navigation. `router` holds the imperative\n * actions plus a `pending` flag, `true` while a soft navigation is in flight.\n *\n * **On a `render: 'static'` route `url` is frozen at build time**, origin included and query empty. The\n * payload is one prerendered set of bytes and this reads the `href` in it, so it is the page's own\n * `PageProps.url` — the same value, not a live one. A page whose output depends on the query wants\n * `render: 'dynamic'`; a component that only needs it after hydration can read `location.search` in an\n * effect.\n *\n * Hooks can't run in a server component; read the same data there from `getRequestContext()`.\n *\n * @example\n * ```tsx\n * 'use client';\n * import { useNavigation } from '@rshono/core/client';\n *\n * export function NextPage() {\n * const { url, router } = useNavigation();\n * const page = Number(url.searchParams.get('page') ?? '1');\n * return (\n * <button disabled={router.pending} onClick={() => router.push(`${url.pathname}?page=${page + 1}`)}>\n * Next {router.pending ? '…' : ''}\n * </button>\n * );\n * }\n * ```\n *\n * @returns The current {@link NavigationState}: `url` and `params`, plus `router`\n * ({@link NavigationRouter}) with `push` / `replace` / `back` / `forward` / `refresh` / `pending`.\n * @throws If called outside a page's React tree, where there is no navigation\n * context to read.\n *\n * @see {@link https://www.rshono.com/docs/api#rshonocoreclient | Docs — `@rshono/core/client`}\n * @see {@link https://www.rshono.com/docs/pages#client-components | Docs — client components}\n */\nexport function useNavigation(): NavigationState {\n const value = useContext(NavigationContext);\n if (!value) {\n throw new Error(\n \"[rshono] useNavigation() must be called inside a 'use client' component rendered by a page. In a server component, read the URL from getRequestContext() instead.\",\n );\n }\n return value;\n}\n"]}
@@ -28,6 +28,29 @@ export declare function createRscRequest(urlString: string, action?: {
28
28
  id: string;
29
29
  body: BodyInit;
30
30
  }, signal?: AbortSignal): Request;
31
+ /**
32
+ * Whether a POST arrived in a shape a browser `<form>` could have produced: one of the three `enctype`
33
+ * values, and no header of its own to need a preflight for.
34
+ *
35
+ * The framework's cross-site refusal runs on this rather than on the {@link RenderRequest} classification,
36
+ * because what makes a form post forgeable from another site is its shape and not what happens to be in the
37
+ * body. Keyed the other way, `text/plain` — the one enctype React never writes — was classified `document`
38
+ * and never reached the refusal, while the README promised that a page route refuses *every* cross-site form
39
+ * post. No action could run through it, since `decodeAction` is only called for the two above; what it cost
40
+ * was a forced authenticated page render, and a claim about the boundary that was not true as written.
41
+ *
42
+ * `x-rsc-action` excludes the client-initiated shape, which needs no check of its own: that header is not
43
+ * CORS-safelisted, so a cross-origin caller needs a preflight the framework never answers.
44
+ *
45
+ * **The same test {@link parseRenderRequest} makes**, and it has to be: this decides what is *refused* and
46
+ * that decides what is *decoded*, so a header the two read differently is a request that takes the form path
47
+ * without passing the form check. A present-but-empty `x-rsc-action` was exactly that — `has` said "the
48
+ * client-initiated shape, already covered" while `get` was falsy, so the request fell through to
49
+ * `form-action` and ran the action it carried with the cross-site refusal skipped. Not reachable from a
50
+ * browser, since an empty header is still a header and still needs the preflight; refused anyway, because a
51
+ * predicate that fails open on a shape nothing produces today is one bad day from being wrong.
52
+ */
53
+ export declare function isBrowserFormPost(request: Request): boolean;
31
54
  export declare function parseRenderRequest(request: Request): RenderRequest;
32
55
  /** Whether the client asked for a flight payload. For GET paths that only need the boolean, without parsing. */
33
56
  export declare function asksForRsc(request: Request): boolean;
@@ -44,4 +67,3 @@ export declare function requestWantsRsc(request: Request): boolean;
44
67
  export declare function isActionRequest(renderRequest: RenderRequest): renderRequest is Extract<RenderRequest, {
45
68
  kind: 'form-action' | 'rsc-action';
46
69
  }>;
47
- //# sourceMappingURL=request.d.ts.map
@@ -41,7 +41,46 @@ export function createRscRequest(urlString, action, signal) {
41
41
  signal,
42
42
  });
43
43
  }
44
+ /**
45
+ * The two content types React writes a form action as, and so the two a POST must carry to be *decoded* as
46
+ * one.
47
+ *
48
+ * Deliberately narrower than the set {@link isBrowserFormPost} refuses on. `decodeAction` reaches the body
49
+ * through `request.formData()`, which throws on a `text/plain` one — so classifying that enctype as a
50
+ * `form-action` would turn every same-origin `text/plain` POST into a 400 where the page used to render.
51
+ * What the framework decodes and what it refuses are two different questions.
52
+ */
44
53
  const FORM_CONTENT_TYPES = /^(?:multipart\/form-data|application\/x-www-form-urlencoded)/i;
54
+ /** The third `enctype` a browser `<form>` can send, which React never writes and this never decodes. */
55
+ const PLAIN_TEXT_ENCTYPE = /^text\/plain/i;
56
+ /**
57
+ * Whether a POST arrived in a shape a browser `<form>` could have produced: one of the three `enctype`
58
+ * values, and no header of its own to need a preflight for.
59
+ *
60
+ * The framework's cross-site refusal runs on this rather than on the {@link RenderRequest} classification,
61
+ * because what makes a form post forgeable from another site is its shape and not what happens to be in the
62
+ * body. Keyed the other way, `text/plain` — the one enctype React never writes — was classified `document`
63
+ * and never reached the refusal, while the README promised that a page route refuses *every* cross-site form
64
+ * post. No action could run through it, since `decodeAction` is only called for the two above; what it cost
65
+ * was a forced authenticated page render, and a claim about the boundary that was not true as written.
66
+ *
67
+ * `x-rsc-action` excludes the client-initiated shape, which needs no check of its own: that header is not
68
+ * CORS-safelisted, so a cross-origin caller needs a preflight the framework never answers.
69
+ *
70
+ * **The same test {@link parseRenderRequest} makes**, and it has to be: this decides what is *refused* and
71
+ * that decides what is *decoded*, so a header the two read differently is a request that takes the form path
72
+ * without passing the form check. A present-but-empty `x-rsc-action` was exactly that — `has` said "the
73
+ * client-initiated shape, already covered" while `get` was falsy, so the request fell through to
74
+ * `form-action` and ran the action it carried with the cross-site refusal skipped. Not reachable from a
75
+ * browser, since an empty header is still a header and still needs the preflight; refused anyway, because a
76
+ * predicate that fails open on a shape nothing produces today is one bad day from being wrong.
77
+ */
78
+ export function isBrowserFormPost(request) {
79
+ if (request.method !== 'POST' || request.headers.get(HEADER_ACTION_ID))
80
+ return false;
81
+ const contentType = request.headers.get('content-type') ?? '';
82
+ return FORM_CONTENT_TYPES.test(contentType) || PLAIN_TEXT_ENCTYPE.test(contentType);
83
+ }
45
84
  export function parseRenderRequest(request) {
46
85
  if (request.method === 'POST') {
47
86
  const actionId = request.headers.get(HEADER_ACTION_ID);
@@ -1 +1 @@
1
- {"version":3,"file":"request.js","sourceRoot":"","sources":["../../src/runtime/request.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,MAAM,gBAAgB,GAAG,cAAc,CAAC;AAExC;;;;;;;;;;GAUG;AACH,MAAM,UAAU,GAAG,KAAK,CAAC;AACzB,MAAM,gBAAgB,GAAG,GAAG,CAAC;AAa7B;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,SAAiB,EAAE,MAAuC,EAAE,MAAoB;IAC/G,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IAChD,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,EAAE,CAAC,UAAU,CAAC,EAAE,gBAAgB,EAAE,CAAC,CAAC;IAChE,IAAI,MAAM;QAAE,OAAO,CAAC,GAAG,CAAC,gBAAgB,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC;IACrD,OAAO,IAAI,OAAO,CAAC,GAAG,EAAE;QACtB,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK;QAC/B,OAAO;QACP,IAAI,EAAE,MAAM,EAAE,IAAI;QAClB,MAAM;KACP,CAAC,CAAC;AACL,CAAC;AAED,MAAM,kBAAkB,GAAG,+DAA+D,CAAC;AAE3F,MAAM,UAAU,kBAAkB,CAAC,OAAgB;IACjD,IAAI,OAAO,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;QAC9B,MAAM,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;QACvD,IAAI,QAAQ;YAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,QAAQ,EAAE,CAAC;QACtD,IAAI,kBAAkB,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC;YAAE,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC;QACvG,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC;IAC9B,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,UAAU,EAAE,CAAC;AAC5D,CAAC;AAED,gHAAgH;AAChH,MAAM,UAAU,UAAU,CAAC,OAAgB;IACzC,OAAO,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,KAAK,gBAAgB,CAAC;AAC9D,CAAC;AAED,mGAAmG;AACnG,MAAM,CAAC,MAAM,eAAe,GAAG,KAAK,CAAC;AAErC,sFAAsF;AACtF,MAAM,UAAU,QAAQ,CAAC,aAA4B;IACnD,OAAO,aAAa,CAAC,IAAI,KAAK,KAAK,IAAI,aAAa,CAAC,IAAI,KAAK,YAAY,CAAC;AAC7E,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,OAAgB;IAC9C,OAAO,QAAQ,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAC,CAAC;AAC/C,CAAC;AAED,6EAA6E;AAC7E,MAAM,UAAU,eAAe,CAAC,aAA4B;IAC1D,OAAO,aAAa,CAAC,IAAI,KAAK,aAAa,IAAI,aAAa,CAAC,IAAI,KAAK,YAAY,CAAC;AACrF,CAAC","sourcesContent":["/**\n * The header that names the server action a client-initiated call wants to run, and so the header that\n * decides which of the two action branches a POST takes.\n *\n * That choice is a security boundary, not just a dispatch detail. It is deliberately *not* a CORS-safelisted\n * header: a page on another origin cannot send one without a successful preflight, and the framework answers\n * no preflight, so the `rsc-action` branch is unreachable cross-origin and carries no origin check of its\n * own. The `form-action` branch is the forgeable one — a form post needs no preflight — and that is the one\n * `refusesCrossSiteForm` stands in front of. See `SECURITY.md`.\n */\nconst HEADER_ACTION_ID = 'x-rsc-action';\n\n/**\n * The header that asks a page URL for its flight payload rather than its HTML document, and the value it\n * carries. One URL answers as either, so this is what the response `Vary`s on.\n *\n * A header of its own rather than `Accept: text/x-component`, which is what this used to be. Content\n * negotiation was the more standards-shaped mechanism and it cost too much: a prerendered page is served\n * `public, max-age=300`, and `Vary: Accept` on a publicly cacheable response is close to a cache-disabling\n * header — real browsers send long `Accept` strings that differ by vendor and version, so a CDN keyed on it\n * stores a copy per variant of the same bytes, and some shared caches decline to store a `Vary` they consider\n * high-cardinality at all. This one has exactly two states.\n */\nconst HEADER_RSC = 'rsc';\nconst HEADER_RSC_VALUE = '1';\n\n/**\n * The four shapes an incoming render request can take, as a discriminated union so that illegal\n * combinations — an action id on a GET — are unrepresentable.\n *\n * - `document` — a normal navigation; respond with a full SSR HTML document.\n * - `rsc` — a soft-navigation flight fetch (`RSC: 1`).\n * - `form-action` — a progressive-enhancement `<form>` POST (no JavaScript).\n * - `rsc-action` — a client-initiated server-action call carrying an action id.\n */\nexport type RenderRequest = { kind: 'document' } | { kind: 'rsc' } | { kind: 'form-action' } | { kind: 'rsc-action'; actionId: string };\n\n/**\n * Builds the `Request` the client sends to ask a page for its flight payload, optionally carrying a server\n * action.\n *\n * `signal` is for a navigation, which a later one supersedes — an action is never abandoned that way, so its\n * caller passes none.\n */\nexport function createRscRequest(urlString: string, action?: { id: string; body: BodyInit }, signal?: AbortSignal): Request {\n const url = new URL(urlString, location.origin);\n const headers = new Headers({ [HEADER_RSC]: HEADER_RSC_VALUE });\n if (action) headers.set(HEADER_ACTION_ID, action.id);\n return new Request(url, {\n method: action ? 'POST' : 'GET',\n headers,\n body: action?.body,\n signal,\n });\n}\n\nconst FORM_CONTENT_TYPES = /^(?:multipart\\/form-data|application\\/x-www-form-urlencoded)/i;\n\nexport function parseRenderRequest(request: Request): RenderRequest {\n if (request.method === 'POST') {\n const actionId = request.headers.get(HEADER_ACTION_ID);\n if (actionId) return { kind: 'rsc-action', actionId };\n if (FORM_CONTENT_TYPES.test(request.headers.get('content-type') ?? '')) return { kind: 'form-action' };\n return { kind: 'document' };\n }\n return { kind: asksForRsc(request) ? 'rsc' : 'document' };\n}\n\n/** Whether the client asked for a flight payload. For GET paths that only need the boolean, without parsing. */\nexport function asksForRsc(request: Request): boolean {\n return request.headers.get(HEADER_RSC) === HEADER_RSC_VALUE;\n}\n\n/** The header a response has to `Vary` on, since one page URL answers as either representation. */\nexport const RSC_VARY_HEADER = 'RSC';\n\n/** True when the response should be a flight payload rather than an HTML document. */\nexport function wantsRsc(renderRequest: RenderRequest): boolean {\n return renderRequest.kind === 'rsc' || renderRequest.kind === 'rsc-action';\n}\n\n/**\n * {@link wantsRsc} straight off the request, for the error and control-signal paths — reached from Hono's\n * `onError` / `notFound`, so they have no parsed {@link RenderRequest} to hand.\n */\nexport function requestWantsRsc(request: Request): boolean {\n return wantsRsc(parseRenderRequest(request));\n}\n\n/** True when the request carries a server action to run before rendering. */\nexport function isActionRequest(renderRequest: RenderRequest): renderRequest is Extract<RenderRequest, { kind: 'form-action' | 'rsc-action' }> {\n return renderRequest.kind === 'form-action' || renderRequest.kind === 'rsc-action';\n}\n"]}
1
+ {"version":3,"file":"request.js","sourceRoot":"","sources":["../../src/runtime/request.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The header that names the server action a client-initiated call wants to run, and so the header that\n * decides which of the two action branches a POST takes.\n *\n * That choice is a security boundary, not just a dispatch detail. It is deliberately *not* a CORS-safelisted\n * header: a page on another origin cannot send one without a successful preflight, and the framework answers\n * no preflight, so the `rsc-action` branch is unreachable cross-origin and carries no origin check of its\n * own. The `form-action` branch is the forgeable one — a form post needs no preflight — and that is the one\n * `refusesCrossSiteForm` stands in front of. See `SECURITY.md`.\n */\nconst HEADER_ACTION_ID = 'x-rsc-action';\n\n/**\n * The header that asks a page URL for its flight payload rather than its HTML document, and the value it\n * carries. One URL answers as either, so this is what the response `Vary`s on.\n *\n * A header of its own rather than `Accept: text/x-component`, which is what this used to be. Content\n * negotiation was the more standards-shaped mechanism and it cost too much: a prerendered page is served\n * `public, max-age=300`, and `Vary: Accept` on a publicly cacheable response is close to a cache-disabling\n * header — real browsers send long `Accept` strings that differ by vendor and version, so a CDN keyed on it\n * stores a copy per variant of the same bytes, and some shared caches decline to store a `Vary` they consider\n * high-cardinality at all. This one has exactly two states.\n */\nconst HEADER_RSC = 'rsc';\nconst HEADER_RSC_VALUE = '1';\n\n/**\n * The four shapes an incoming render request can take, as a discriminated union so that illegal\n * combinations — an action id on a GET — are unrepresentable.\n *\n * - `document` — a normal navigation; respond with a full SSR HTML document.\n * - `rsc` — a soft-navigation flight fetch (`RSC: 1`).\n * - `form-action` — a progressive-enhancement `<form>` POST (no JavaScript).\n * - `rsc-action` — a client-initiated server-action call carrying an action id.\n */\nexport type RenderRequest = { kind: 'document' } | { kind: 'rsc' } | { kind: 'form-action' } | { kind: 'rsc-action'; actionId: string };\n\n/**\n * Builds the `Request` the client sends to ask a page for its flight payload, optionally carrying a server\n * action.\n *\n * `signal` is for a navigation, which a later one supersedes — an action is never abandoned that way, so its\n * caller passes none.\n */\nexport function createRscRequest(urlString: string, action?: { id: string; body: BodyInit }, signal?: AbortSignal): Request {\n const url = new URL(urlString, location.origin);\n const headers = new Headers({ [HEADER_RSC]: HEADER_RSC_VALUE });\n if (action) headers.set(HEADER_ACTION_ID, action.id);\n return new Request(url, {\n method: action ? 'POST' : 'GET',\n headers,\n body: action?.body,\n signal,\n });\n}\n\n/**\n * The two content types React writes a form action as, and so the two a POST must carry to be *decoded* as\n * one.\n *\n * Deliberately narrower than the set {@link isBrowserFormPost} refuses on. `decodeAction` reaches the body\n * through `request.formData()`, which throws on a `text/plain` one — so classifying that enctype as a\n * `form-action` would turn every same-origin `text/plain` POST into a 400 where the page used to render.\n * What the framework decodes and what it refuses are two different questions.\n */\nconst FORM_CONTENT_TYPES = /^(?:multipart\\/form-data|application\\/x-www-form-urlencoded)/i;\n\n/** The third `enctype` a browser `<form>` can send, which React never writes and this never decodes. */\nconst PLAIN_TEXT_ENCTYPE = /^text\\/plain/i;\n\n/**\n * Whether a POST arrived in a shape a browser `<form>` could have produced: one of the three `enctype`\n * values, and no header of its own to need a preflight for.\n *\n * The framework's cross-site refusal runs on this rather than on the {@link RenderRequest} classification,\n * because what makes a form post forgeable from another site is its shape and not what happens to be in the\n * body. Keyed the other way, `text/plain` — the one enctype React never writes — was classified `document`\n * and never reached the refusal, while the README promised that a page route refuses *every* cross-site form\n * post. No action could run through it, since `decodeAction` is only called for the two above; what it cost\n * was a forced authenticated page render, and a claim about the boundary that was not true as written.\n *\n * `x-rsc-action` excludes the client-initiated shape, which needs no check of its own: that header is not\n * CORS-safelisted, so a cross-origin caller needs a preflight the framework never answers.\n *\n * **The same test {@link parseRenderRequest} makes**, and it has to be: this decides what is *refused* and\n * that decides what is *decoded*, so a header the two read differently is a request that takes the form path\n * without passing the form check. A present-but-empty `x-rsc-action` was exactly that — `has` said \"the\n * client-initiated shape, already covered\" while `get` was falsy, so the request fell through to\n * `form-action` and ran the action it carried with the cross-site refusal skipped. Not reachable from a\n * browser, since an empty header is still a header and still needs the preflight; refused anyway, because a\n * predicate that fails open on a shape nothing produces today is one bad day from being wrong.\n */\nexport function isBrowserFormPost(request: Request): boolean {\n if (request.method !== 'POST' || request.headers.get(HEADER_ACTION_ID)) return false;\n const contentType = request.headers.get('content-type') ?? '';\n return FORM_CONTENT_TYPES.test(contentType) || PLAIN_TEXT_ENCTYPE.test(contentType);\n}\n\nexport function parseRenderRequest(request: Request): RenderRequest {\n if (request.method === 'POST') {\n const actionId = request.headers.get(HEADER_ACTION_ID);\n if (actionId) return { kind: 'rsc-action', actionId };\n if (FORM_CONTENT_TYPES.test(request.headers.get('content-type') ?? '')) return { kind: 'form-action' };\n return { kind: 'document' };\n }\n return { kind: asksForRsc(request) ? 'rsc' : 'document' };\n}\n\n/** Whether the client asked for a flight payload. For GET paths that only need the boolean, without parsing. */\nexport function asksForRsc(request: Request): boolean {\n return request.headers.get(HEADER_RSC) === HEADER_RSC_VALUE;\n}\n\n/** The header a response has to `Vary` on, since one page URL answers as either representation. */\nexport const RSC_VARY_HEADER = 'RSC';\n\n/** True when the response should be a flight payload rather than an HTML document. */\nexport function wantsRsc(renderRequest: RenderRequest): boolean {\n return renderRequest.kind === 'rsc' || renderRequest.kind === 'rsc-action';\n}\n\n/**\n * {@link wantsRsc} straight off the request, for the error and control-signal paths — reached from Hono's\n * `onError` / `notFound`, so they have no parsed {@link RenderRequest} to hand.\n */\nexport function requestWantsRsc(request: Request): boolean {\n return wantsRsc(parseRenderRequest(request));\n}\n\n/** True when the request carries a server action to run before rendering. */\nexport function isActionRequest(renderRequest: RenderRequest): renderRequest is Extract<RenderRequest, { kind: 'form-action' | 'rsc-action' }> {\n return renderRequest.kind === 'form-action' || renderRequest.kind === 'rsc-action';\n}\n"]}
@@ -24,4 +24,3 @@
24
24
  * @packageDocumentation
25
25
  */
26
26
  export { getRequestContext, notFound, onServerError, publicUrl, redirect, type EnvVars, type RedirectStatus, type RequestContext, type ServerErrorContext, type ServerErrorHandler, type ServerErrorSource, } from './context.js';
27
- //# sourceMappingURL=server.d.ts.map
@@ -75,4 +75,3 @@ export declare function assertEndpointModule(mod: {
75
75
  * at build time, so the prerender pass demands the import and fails the build when it throws.
76
76
  */
77
77
  export declare function assertRouteModules(config: RouteConfig): Promise<void>;
78
- //# sourceMappingURL=validate-entries.d.ts.map
@@ -20,6 +20,17 @@ const CONCRETE_METHODS = ['get', 'post', 'put', 'patch', 'delete', 'options'];
20
20
  const METHODS = new Set([...CONCRETE_METHODS, 'all']);
21
21
  /** What a page route's `render` accepts. */
22
22
  const RENDER_MODES = ['static', 'dynamic'];
23
+ /**
24
+ * The one path prefix the framework claims on every deploy target: the hashed client bundle, mounted by
25
+ * `runtime.mountStaticAssets` ahead of the route table, ending in a terminal 404 — so nothing under it
26
+ * reaches a page. Written out here rather than imported, because the mounts live in the deploy runtimes
27
+ * (`deploy/filesystem.ts`, `deploy/cloudflare/runtime.ts`) and in `cli/dev.ts`, none of which this module
28
+ * can pull in.
29
+ *
30
+ * It is the only one. `/_rshono/hmr` is dev-only and an exact path rather than a prefix, and Cloudflare's
31
+ * `__ssg` is an asset path no app would name.
32
+ */
33
+ const RESERVED_PREFIX = '/_static';
23
34
  function fail(message) {
24
35
  throw new Error(`[rshono] ${message}`);
25
36
  }
@@ -115,30 +126,123 @@ function methodsOf(route) {
115
126
  return methods.includes('all') ? CONCRETE_METHODS : methods;
116
127
  }
117
128
  /**
118
- * Refuses a route the table already answers in full. Hono matches in registration order, so a later entry
119
- * whose every method and path is spoken for is dead code — and the build that produced it exits 0 with
120
- * nothing to say, which is how a duplicated `path` survives.
129
+ * A parameter's *name*, which is not part of the pattern Hono matches on. Anchored to a segment boundary so
130
+ * a `:` inside a `{regex}` constraint is not mistaken for one, and stopped by `{` and `?` — both of which
131
+ * *are* part of the pattern and have to survive into the key.
132
+ */
133
+ const PARAM_NAME = /(?<=\/):[^/{?]+/g;
134
+ /**
135
+ * What Hono matches on, rather than what was typed — so two spellings of one pattern hash alike.
136
+ *
137
+ * - **A parameter's name is not part of it.** `/u/:id` and `/u/:name` are one route, and whichever is
138
+ * registered second never runs. A `{regex}` constraint is kept, because that one *is* part of the
139
+ * pattern: `/a/:id{[0-9]+}` and `/a/:name` are genuinely different routes and both can answer.
140
+ * - **A `*` before the last segment matches exactly one non-empty segment** — the same thing an
141
+ * unconstrained parameter matches. Checked against Hono 4.13: `/a/[*]/c` and `/a/:id/c` both answer
142
+ * `/a/b/c`, both 404 `/a//c`, and both 404 `/a/b/x/c`.
143
+ *
144
+ * A *trailing* `*` is left alone: it claims a whole subtree rather than one segment, which is
145
+ * {@link subtreeOf}'s half of the job.
146
+ */
147
+ function patternKey(path) {
148
+ const segments = path.replace(PARAM_NAME, ':').split('/');
149
+ return segments.map((segment, index) => (segment === '*' && index < segments.length - 1 ? ':' : segment)).join('/');
150
+ }
151
+ /**
152
+ * Every key a route answers. More than one only for a trailing optional parameter: `/a/:id?` answers
153
+ * `/a/x` *and* `/a`, and — checked — neither `/a/` nor `/a/x/y`. Both have to be spoken for before it is
154
+ * dead, which is why this is a list rather than a key.
155
+ */
156
+ function keysFor(path) {
157
+ const key = patternKey(path);
158
+ if (!key.endsWith('/:?'))
159
+ return [key];
160
+ return [key.slice(0, -1), key.slice(0, -3) || '/'];
161
+ }
162
+ /**
163
+ * The subtree a trailing `*` claims, or `null` for a path without one. `/a/*` answers `/a/b` and
164
+ * `/a/b/c/d`, and the bare `/a` as well — but not `/ab`, so the boundary is the `/` and a prefix test has
165
+ * to respect it.
166
+ */
167
+ function subtreeOf(key) {
168
+ return key.endsWith('/*') ? key.slice(0, -2) : null;
169
+ }
170
+ /** Whether the subtree claimed by a trailing `*` at `prefix` contains `key`. */
171
+ function covers(prefix, key) {
172
+ return key === prefix || key.startsWith(`${prefix}/`);
173
+ }
174
+ /**
175
+ * Whether a path sits inside {@link RESERVED_PREFIX}'s subtree, which is the prefix and everything below
176
+ * it. The boundary is the `/`: `/_staticky` is an ordinary path and answers normally.
177
+ */
178
+ function isReserved(path) {
179
+ return path === RESERVED_PREFIX || path.startsWith(`${RESERVED_PREFIX}/`);
180
+ }
181
+ /**
182
+ * Refuses a route the table already answers in full, or that the framework's own mount already answers.
183
+ * Hono matches in registration order, so a later entry whose every method and path is spoken for is dead
184
+ * code — and the build that produced it exits 0 with nothing to say, which is how a duplicated `path`
185
+ * survives.
186
+ *
187
+ * The framework's mount is checked first and by name. The rest of this function compares the app's routes
188
+ * with *each other*, so the prefixes the framework registers ahead of them were invisible to it: a route at
189
+ * `/_static/thing` built clean and then 404'd, on every target and in dev. Only a literal path is refused —
190
+ * a parameterised route that happens to match under the prefix, `/:section/thing` or a root `/*`, still
191
+ * answers everything else and is none of this rule's business.
121
192
  *
122
193
  * Method by method, and only when *all* of them are taken: one path split between a `'get'` endpoint and a
123
194
  * `'post'` one shadows nothing, and neither does a catch-all registered behind a route that claims one
124
195
  * method of it — that one still answers the rest.
196
+ *
197
+ * Keyed on the *pattern* rather than on the path as written, because a dead route need not be a duplicated
198
+ * string. Two shapes reached a build this way: `/u/:id` followed by `/u/:name`, which is the same route
199
+ * twice under two spellings, and a wildcard registered *ahead* of a concrete path — `/a/*` then `/a/b` —
200
+ * where the second can never be reached. The doc comment above used to discuss only the opposite order, a
201
+ * catch-all registered behind a route, which is the case that is fine.
202
+ *
203
+ * **Where the line is drawn**, for whoever widens this next. A `{regex}` constraint is kept in the key and
204
+ * compared as text, so `/a/:id{[0-9]+}` then `/a/:n{[0-9]+}` is caught and `/a/:id{[0-9]+}` then
205
+ * `/a/:id{\d+}` is not — the second pair is equivalent, and the later route is dead, and this accepts it.
206
+ * Deciding regex equivalence in general is undecidable-adjacent and not a route validator's job. The same
207
+ * goes for a constraint containing a `/`, which the key carries through unchanged rather than interpreting.
208
+ * Both leave the error on the safe side: an unreachable route slips through, and a route that can still
209
+ * answer is never refused. Anything added here should keep that asymmetry — a false refusal fails a build
210
+ * that was correct, which is much worse than the hole it would close.
125
211
  */
126
212
  function assertNothingIsShadowed(routes) {
127
- /** `"<method> <path>"` → the first route answering it. */
213
+ /** `"<method> <key>"` → the first route answering it. */
128
214
  const claimed = new Map();
215
+ /** The trailing-`*` routes already registered, which claim a whole subtree rather than single keys. */
216
+ const subtrees = [];
217
+ const claimantOf = (method, key) => claimed.get(`${method} ${key}`) ?? subtrees.find((subtree) => subtree.method === method && covers(subtree.prefix, key));
129
218
  routes.forEach((route, index) => {
130
219
  const name = label(route, index);
220
+ if (isReserved(route.path)) {
221
+ fail(`src/routes.ts: ${name} would never run — the framework serves the client bundle at ${RESERVED_PREFIX}, ` +
222
+ `mounted ahead of the route table and answering that whole subtree, so ${RESERVED_PREFIX} is reserved on every ` +
223
+ 'deploy target and under `rshono dev`. Give the route a path of its own.');
224
+ }
131
225
  const methods = methodsOf(route);
132
- const claimants = methods.map((method) => claimed.get(`${method} ${route.path}`));
226
+ const keys = keysFor(route.path);
227
+ const claimants = methods.flatMap((method) => keys.map((key) => claimantOf(method, key)));
133
228
  if (claimants.every((claimant) => claimant !== undefined)) {
134
- const by = [...new Set(claimants)].join(' and ');
229
+ const by = [...new Set(claimants.map((claimant) => claimant.name))].join(' and ');
135
230
  const verbs = methods.map((method) => method.toUpperCase()).join(', ');
136
- fail(`src/routes.ts: ${name} would never run — ${by} already answers ${verbs} ${route.path}, and Hono matches in registration order.`);
231
+ // Only when the two are spelled differently, which is the part that reads as a false alarm: an exact
232
+ // duplicate needs no explaining, and this sentence would only get in its way.
233
+ const why = claimants.some((claimant) => claimant.path === route.path)
234
+ ? ''
235
+ : ' Hono matches on the pattern, not the spelling — a parameter’s name is not part of it, and a trailing `*` answers its whole subtree, the bare prefix included.';
236
+ fail(`src/routes.ts: ${name} would never run — ${by} already answers ${verbs} ${route.path}, and Hono matches in registration order.${why}`);
137
237
  }
138
238
  for (const method of methods) {
139
- const key = `${method} ${route.path}`;
140
- if (!claimed.has(key))
141
- claimed.set(key, name);
239
+ for (const key of keys) {
240
+ if (!claimed.has(`${method} ${key}`))
241
+ claimed.set(`${method} ${key}`, { name, path: route.path });
242
+ }
243
+ const prefix = subtreeOf(patternKey(route.path));
244
+ if (prefix !== null)
245
+ subtrees.push({ method, prefix, name, path: route.path });
142
246
  }
143
247
  });
144
248
  }