@rshono/core 1.0.0-rc.16 → 1.0.0-rc.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -5
- package/dist/builder/env-shadow-loader.cjs +58 -2
- package/dist/builder/rspack-config.d.ts.map +1 -1
- package/dist/builder/rspack-config.js +46 -6
- package/dist/builder/rspack-config.js.map +1 -1
- package/dist/cli/build.d.ts.map +1 -1
- package/dist/cli/build.js +24 -1
- package/dist/cli/build.js.map +1 -1
- package/dist/cli/dev.d.ts.map +1 -1
- package/dist/cli/dev.js +4 -0
- package/dist/cli/dev.js.map +1 -1
- package/dist/cli/index.js +14 -8
- package/dist/cli/index.js.map +1 -1
- package/dist/config.d.ts +10 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/deploy/cloudflare/runtime.d.ts.map +1 -1
- package/dist/deploy/cloudflare/runtime.js +31 -5
- package/dist/deploy/cloudflare/runtime.js.map +1 -1
- package/dist/deploy/contract.d.ts +6 -2
- package/dist/deploy/contract.d.ts.map +1 -1
- package/dist/deploy/contract.js.map +1 -1
- package/dist/deploy/filesystem.d.ts.map +1 -1
- package/dist/deploy/filesystem.js +2 -1
- package/dist/deploy/filesystem.js.map +1 -1
- package/dist/deploy/node/runtime.d.ts.map +1 -1
- package/dist/deploy/node/runtime.js +3 -4
- package/dist/deploy/node/runtime.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/router.d.ts +60 -7
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js.map +1 -1
- package/dist/runtime/context.d.ts +51 -7
- package/dist/runtime/context.d.ts.map +1 -1
- package/dist/runtime/context.js +49 -5
- package/dist/runtime/context.js.map +1 -1
- package/dist/runtime/entry.client.js +230 -277
- package/dist/runtime/entry.client.js.map +1 -1
- package/dist/runtime/entry.rsc.d.ts.map +1 -1
- package/dist/runtime/entry.rsc.js +187 -39
- package/dist/runtime/entry.rsc.js.map +1 -1
- package/dist/runtime/flight-inject.d.ts +1 -1
- package/dist/runtime/flight-inject.d.ts.map +1 -1
- package/dist/runtime/flight-inject.js +207 -40
- package/dist/runtime/flight-inject.js.map +1 -1
- package/dist/runtime/navigation.d.ts +4 -0
- package/dist/runtime/navigation.d.ts.map +1 -1
- package/dist/runtime/navigation.js.map +1 -1
- package/dist/runtime/request.d.ts.map +1 -1
- package/dist/runtime/request.js +10 -0
- package/dist/runtime/request.js.map +1 -1
- package/dist/runtime/validate-entries.d.ts +33 -0
- package/dist/runtime/validate-entries.d.ts.map +1 -0
- package/dist/runtime/validate-entries.js +185 -0
- package/dist/runtime/validate-entries.js.map +1 -0
- package/dist/server/prerendered.d.ts +30 -6
- package/dist/server/prerendered.d.ts.map +1 -1
- package/dist/server/prerendered.js +71 -13
- package/dist/server/prerendered.js.map +1 -1
- package/dist/server/server-config.d.ts +12 -0
- package/dist/server/server-config.d.ts.map +1 -1
- package/dist/server/server-config.js +25 -0
- package/dist/server/server-config.js.map +1 -1
- package/dist/server/ssg.d.ts.map +1 -1
- package/dist/server/ssg.js +151 -25
- package/dist/server/ssg.js.map +1 -1
- package/dist/server/static.d.ts.map +1 -1
- package/dist/server/static.js +3 -1
- package/dist/server/static.js.map +1 -1
- package/package.json +7 -6
|
@@ -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\nexport function injectFlightPayload(\n rscStream: ReadableStream<Uint8Array>,\n options: { nonce?: string; onDone?: () => void } = {},\n): TransformStream<Uint8Array, Uint8Array> {\n const { nonce, onDone } = options;\n const scriptOpen = `<script${nonce ? ` nonce=\"${nonce}\"` : ''}>(self.__FLIGHT_DATA||=[]).push(`;\n const scriptClose = ')</script>';\n\n const { promise: flightWritten, resolve: flightDone } = Promise.withResolvers<void>();\n let startedFlight = false;\n\n const batch: Uint8Array[] = [];\n let boundary: TaskHandle | null = null;\n\n /**\n * Set once the consumer has gone away, so nothing tries to enqueue into a readable that cannot take it.\n *\n * The `cancel` hook alone cannot set this: per the Streams standard, cancelling the readable after the\n * close algorithm has started skips the transformer's `cancel` entirely — and `flush` awaiting the whole\n * payload is precisely that window. So a failed enqueue counts as the signal too.\n */\n let cancelled = false;\n /** Held so {@link cancelled} can release the teed RSC branch rather than leaving it to be pumped. */\n let flightReader: ReadableStreamDefaultReader<Uint8Array> | null = null;\n\n /**\n * Emits the HTML buffered since the last boundary, holding back the document trailer for\n * {@link TransformStream.flush} to re-emit after the payload scripts.\n *\n * The batch is joined before the trailer is looked for, so one React split across two views is still\n * found. React writes its final flush in one synchronous run, so a trailer split across *batches* is not\n * a shape it produces.\n */\n function emitBatch(controller: TransformStreamDefaultController<Uint8Array>): void {\n boundary = null;\n let total = 0;\n for (const chunk of batch) total += chunk.byteLength;\n if (total === 0) {\n batch.length = 0;\n return;\n }\n\n const joined = new Uint8Array(total);\n let at = 0;\n for (const chunk of batch) {\n joined.set(chunk, at);\n at += chunk.byteLength;\n }\n batch.length = 0;\n\n const end = endsWithTrailer(joined, total) ? total - TRAILER_BYTES.length : total;\n if (end > 0) controller.enqueue(joined.subarray(0, end));\n }\n\n async function writeFlight(controller: TransformStreamDefaultController<Uint8Array>): Promise<void> {\n const reader = (flightReader = rscStream.getReader());\n // `fatal`, so a chunk that split a multi-byte character throws instead of emitting U+FFFD; the catch\n // below falls back to a byte-exact encoding for it.\n const decoder = new TextDecoder('utf-8', { fatal: true });\n const push = (literal: string) => controller.enqueue(encoder.encode(scriptOpen + literal + scriptClose));\n for (;;) {\n if (cancelled) return;\n const { done, value } = await reader.read();\n if (done) break;\n // Only the decode is guarded: a `push` inside the same `try` would answer a dead controller by\n // re-encoding the chunk and enqueueing it again.\n let literal: string;\n try {\n literal = escapeScript(JSON.stringify(decoder.decode(value, { stream: true })));\n } catch {\n literal = `Uint8Array.from(atob(${JSON.stringify(btoa(latin1(value)))}), m => m.codePointAt(0))`;\n }\n if (cancelled) return;\n // A failed enqueue means the consumer is gone: release the RSC branch so `flush` unparks now rather\n // than whenever the payload would have ended on its own.\n try {\n push(literal);\n } catch {\n cancelled = true;\n reader.cancel().catch(() => {});\n return;\n }\n }\n if (cancelled) return;\n const remaining = decoder.decode();\n if (remaining.length) push(escapeScript(JSON.stringify(remaining)));\n }\n\n const transformer: CancellableTransformer<Uint8Array, Uint8Array> = {\n transform(chunk, controller) {\n batch.push(chunk);\n if (boundary) return;\n // A macrotask, not a microtask: React writes a whole flush in one synchronous run but `pipeThrough`\n // delivers it one microtask at a time, and a script injected between two chunks lands inside a tag.\n boundary = schedule(() => {\n try {\n emitBatch(controller);\n } catch (error) {\n controller.error(error);\n flightDone();\n return;\n }\n if (!startedFlight) {\n startedFlight = true;\n // Deliberately not awaited: this runs inside a scheduled callback with nothing to return to, and\n // the chain already routes a write failure to `controller.error` before settling `flightDone`.\n void writeFlight(controller)\n .catch((error) => controller.error(error))\n .then(flightDone);\n }\n });\n },\n async flush(controller) {\n // Both of these have to happen *before* the await, and in this order.\n try {\n // Anything still batched belongs ahead of the payload scripts, which sit at the end of `<body>`.\n if (boundary) {\n unschedule(boundary);\n boundary = null;\n emitBatch(controller);\n }\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 {\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 cancelled = true;\n flightReader?.cancel().catch(() => {});\n flightDone();\n onDone?.();\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 onDone?.();\n }\n },\n cancel(reason) {\n cancelled = true;\n if (boundary) {\n unschedule(boundary);\n boundary = null;\n }\n batch.length = 0;\n // Otherwise the teed RSC branch keeps being pumped for a response nobody will read, and the tee's\n // other half buffers every chunk waiting for this one to catch up.\n flightReader?.cancel(reason).catch(() => {});\n // Unparks `flush` if it is waiting on a payload that will now never arrive.\n flightDone();\n onDone?.();\n },\n };\n return new TransformStream<Uint8Array, Uint8Array>(transformer);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"flight-inject.js","sourceRoot":"","sources":["../../src/runtime/flight-inject.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAWH,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAC;AAElC,gHAAgH;AAChH,MAAM,OAAO,GAAG,gBAAgB,CAAC;AACjC,MAAM,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;AAQ9C,MAAM,eAAe,GAAG,OAAO,YAAY,KAAK,UAAU,CAAC;AAC3D,MAAM,QAAQ,GAAmC,eAAe,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,UAAU,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;AAC5G,MAAM,UAAU,GAAG,CAAC,MAAkB,EAAQ,EAAE;IAC9C,IAAI,eAAe;QAAE,cAAc,CAAC,MAAyC,CAAC,CAAC;;QAC1E,YAAY,CAAC,MAAuC,CAAC,CAAC;AAC7D,CAAC,CAAC;AAEF;;;;GAIG;AACH,SAAS,YAAY,CAAC,MAAc;IAClC,OAAO,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,OAAO,CAAC,eAAe,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;AAC9G,CAAC;AAED;;;GAGG;AACH,SAAS,MAAM,CAAC,KAAiB;IAC/B,IAAI,GAAG,GAAG,EAAE,CAAC;IACb,KAAK,IAAI,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,KAAK,CAAC,MAAM,EAAE,EAAE,IAAI,IAAI;QAAE,GAAG,IAAI,MAAM,CAAC,YAAY,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,EAAE,EAAE,EAAE,GAAG,IAAI,CAAC,CAAC,CAAC;IAC7G,OAAO,GAAG,CAAC;AACb,CAAC;AAED,6EAA6E;AAC7E,SAAS,eAAe,CAAC,MAAkB,EAAE,MAAc;IACzD,IAAI,MAAM,GAAG,aAAa,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IAChD,MAAM,IAAI,GAAG,MAAM,GAAG,aAAa,CAAC,MAAM,CAAC;IAC3C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,aAAa,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAC9C,IAAI,MAAM,CAAC,IAAI,GAAG,CAAC,CAAC,KAAK,aAAa,CAAC,CAAC,CAAC;YAAE,OAAO,KAAK,CAAC;IAC1D,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,mBAAmB,CAAC,MAAkB,EAAE,MAAc;IAC7D,SAAS,EAAE,KAAK,IAAI,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,MAAM,CAAC,EAAE,IAAI,GAAG,CAAC,EAAE,IAAI,EAAE,EAAE,CAAC;QACpF,MAAM,IAAI,GAAG,MAAM,GAAG,IAAI,CAAC;QAC3B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,EAAE;YAAE,IAAI,MAAM,CAAC,IAAI,GAAG,CAAC,CAAC,KAAK,aAAa,CAAC,CAAC,CAAC;gBAAE,SAAS,SAAS,CAAC;QAC7F,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,GAAG,qBAAqB,CAAC;AAE1C,MAAM,UAAU,mBAAmB,CACjC,SAAqC,EACrC,OAAO,GAA4C,EAAE;IAErD,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC;IAClC,MAAM,SAAS,GAAG,KAAK,KAAK,SAAS,IAAI,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;IACrF,MAAM,UAAU,GAAG,UAAU,SAAS,CAAC,CAAC,CAAC,WAAW,SAAS,GAAG,CAAC,CAAC,CAAC,EAAE,kCAAkC,CAAC;IACxG,MAAM,WAAW,GAAG,YAAY,CAAC;IAEjC,MAAM,EAAE,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC,aAAa,EAAQ,CAAC;IACtF,IAAI,aAAa,GAAG,KAAK,CAAC;IAE1B,MAAM,KAAK,GAAiB,EAAE,CAAC;IAC/B,IAAI,QAAQ,GAAsB,IAAI,CAAC;IACvC;;;OAGG;IACH,IAAI,KAAK,GAAsB,IAAI,CAAC;IAEpC;;;;;;OAMG;IACH,IAAI,SAAS,GAAG,KAAK,CAAC;IACtB,qGAAqG;IACrG,IAAI,YAAY,GAAmD,IAAI,CAAC;IAExE;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA4BG;IACH,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,MAAM,OAAO,GAAsB,EAAE,CAAC;IAEtC,6FAA6F;IAC7F,SAAS,UAAU;QACjB,uGAAuG;QACvG,kFAAkF;QAClF,IAAI,SAAS;YAAE,OAAO;QACtB,IAAI,OAAO,GAAG,CAAC,EAAE,CAAC;YAChB,OAAO,EAAE,CAAC;YACV,OAAO;QACT,CAAC;QACD,OAAO,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC;IAC/D,CAAC;IAED,SAAS,aAAa;QACpB,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC;QAC7B,IAAI,IAAI;YAAE,IAAI,EAAE,CAAC;;YACZ,OAAO,EAAE,CAAC;IACjB,CAAC;IAED,gGAAgG;IAChG,IAAI,KAAK,GAAG,KAAK,CAAC;IAClB,SAAS,UAAU;QACjB,IAAI,KAAK;YAAE,OAAO;QAClB,KAAK,GAAG,IAAI,CAAC;QACb,MAAM,EAAE,EAAE,CAAC;IACb,CAAC;IAED;;;;;;OAMG;IACH,SAAS,QAAQ,CAAC,MAAgB;QAChC,SAAS,GAAG,IAAI,CAAC;QACjB,IAAI,QAAQ,EAAE,CAAC;YACb,UAAU,CAAC,QAAQ,CAAC,CAAC;YACrB,QAAQ,GAAG,IAAI,CAAC;QAClB,CAAC;QACD,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC;QACjB,KAAK,GAAG,IAAI,CAAC;QACb,KAAK,MAAM,OAAO,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;YAAE,OAAO,EAAE,CAAC;QACnD,kGAAkG;QAClG,mEAAmE;QACnE,YAAY,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QAC7C,4EAA4E;QAC5E,UAAU,EAAE,CAAC;QACb,UAAU,EAAE,CAAC;IACf,CAAC;IAED;;;;;;;;;;;;;;;OAeG;IACH,SAAS,SAAS,CAAC,UAAwD,EAAE,KAAK,GAAG,KAAK;QACxF,QAAQ,GAAG,IAAI,CAAC;QAChB,IAAI,KAAK,GAAG,KAAK,EAAE,UAAU,IAAI,CAAC,CAAC;QACnC,KAAK,MAAM,KAAK,IAAI,KAAK;YAAE,KAAK,IAAI,KAAK,CAAC,UAAU,CAAC;QACrD,IAAI,KAAK,KAAK,CAAC,EAAE,CAAC;YAChB,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC;YACjB,OAAO;QACT,CAAC;QAED,MAAM,MAAM,GAAG,IAAI,UAAU,CAAC,KAAK,CAAC,CAAC;QACrC,IAAI,EAAE,GAAG,CAAC,CAAC;QACX,IAAI,KAAK,EAAE,CAAC;YACV,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;YACtB,EAAE,IAAI,KAAK,CAAC,UAAU,CAAC;YACvB,KAAK,GAAG,IAAI,CAAC;QACf,CAAC;QACD,KAAK,MAAM,KAAK,IAAI,KAAK,EAAE,CAAC;YAC1B,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;YACtB,EAAE,IAAI,KAAK,CAAC,UAAU,CAAC;QACzB,CAAC;QACD,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC;QAEjB,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,mBAAmB,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;QACtH,MAAM,GAAG,GAAG,KAAK,GAAG,IAAI,CAAC;QACzB,+FAA+F;QAC/F,IAAI,IAAI,GAAG,CAAC,IAAI,CAAC,KAAK;YAAE,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAClD,IAAI,GAAG,GAAG,CAAC;YAAE,UAAU,CAAC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;IAC3D,CAAC;IAED,KAAK,UAAU,WAAW,CAAC,UAAwD;QACjF,MAAM,MAAM,GAAG,CAAC,YAAY,GAAG,SAAS,CAAC,SAAS,EAAE,CAAC,CAAC;QACtD,qGAAqG;QACrG,oDAAoD;QACpD,MAAM,OAAO,GAAG,IAAI,WAAW,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QAC1D,MAAM,IAAI,GAAG,KAAK,EAAE,OAAe,EAAiB,EAAE;YACpD,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;YAC5B,IAAI,MAAM;gBAAE,MAAM,MAAM,CAAC;YACzB,IAAI,SAAS;gBAAE,OAAO;YACtB,UAAU,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,UAAU,GAAG,OAAO,GAAG,WAAW,CAAC,CAAC,CAAC;QACzE,CAAC,CAAC;QACF,SAAS,CAAC;YACR,IAAI,SAAS;gBAAE,OAAO;YACtB,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;YAC5C,IAAI,IAAI;gBAAE,MAAM;YAChB,+FAA+F;YAC/F,iDAAiD;YACjD,IAAI,OAAe,CAAC;YACpB,IAAI,CAAC;gBACH,OAAO,GAAG,YAAY,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;YAClF,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,GAAG,wBAAwB,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,2BAA2B,CAAC;YACnG,CAAC;YACD,IAAI,SAAS;gBAAE,OAAO;YACtB,oGAAoG;YACpG,yDAAyD;YACzD,IAAI,CAAC;gBACH,MAAM,IAAI,CAAC,OAAO,CAAC,CAAC;YACtB,CAAC;YAAC,MAAM,CAAC;gBACP,SAAS,GAAG,IAAI,CAAC;gBACjB,MAAM,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;gBAChC,OAAO;YACT,CAAC;QACH,CAAC;QACD,IAAI,SAAS;YAAE,OAAO;QACtB,MAAM,SAAS,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;QACnC,IAAI,SAAS,CAAC,MAAM;YAAE,MAAM,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;IAC5E,CAAC;IAED,MAAM,WAAW,GAAmD;QAClE,KAAK,CAAC,SAAS,CAAC,KAAK,EAAE,UAAU;YAC/B,IAAI,QAAQ,EAAE,CAAC;gBACb,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;gBAClB,OAAO;YACT,CAAC;YACD,wGAAwG;YACxG,oGAAoG;YACpG,mGAAmG;YACnG,mGAAmG;YACnG,WAAW;YACX,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;YAC5B,IAAI,MAAM;gBAAE,MAAM,MAAM,CAAC;YACzB,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAClB,oGAAoG;YACpG,oGAAoG;YACpG,QAAQ,GAAG,QAAQ,CAAC,GAAG,EAAE;gBACvB,IAAI,CAAC;oBACH,SAAS,CAAC,UAAU,CAAC,CAAC;gBACxB,CAAC;gBAAC,OAAO,KAAK,EAAE,CAAC;oBACf,oGAAoG;oBACpG,4EAA4E;oBAC5E,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;oBACxB,QAAQ,CAAC,KAAK,CAAC,CAAC;oBAChB,OAAO;gBACT,CAAC;gBACD,IAAI,CAAC,aAAa,EAAE,CAAC;oBACnB,aAAa,GAAG,IAAI,CAAC;oBACrB,iGAAiG;oBACjG,+FAA+F;oBAC/F,KAAK,WAAW,CAAC,UAAU,CAAC;yBACzB,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;yBACzC,IAAI,CAAC,UAAU,CAAC,CAAC;gBACtB,CAAC;YACH,CAAC,CAAC,CAAC;QACL,CAAC;QACD,KAAK,CAAC,KAAK,CAAC,UAAU;YACpB,sEAAsE;YACtE,IAAI,CAAC;gBACH,sGAAsG;gBACtG,uGAAuG;gBACvG,IAAI,QAAQ;oBAAE,UAAU,CAAC,QAAQ,CAAC,CAAC;gBACnC,SAAS,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;gBAC5B,mGAAmG;gBACnG,mGAAmG;gBACnG,sGAAsG;gBACtG,gGAAgG;gBAChG,IAAI,CAAC,aAAa,EAAE,CAAC;oBACnB,aAAa,GAAG,IAAI,CAAC;oBACrB,IAAI,SAAS;wBAAE,UAAU,EAAE,CAAC;;wBAE1B,KAAK,WAAW,CAAC,UAAU,CAAC;6BACzB,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;6BACzC,IAAI,CAAC,UAAU,CAAC,CAAC;gBACxB,CAAC;YACH,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,qGAAqG;gBACrG,uGAAuG;gBACvG,QAAQ,CAAC,KAAK,CAAC,CAAC;gBAChB,OAAO;YACT,CAAC;YAED,MAAM,aAAa,CAAC;YACpB,qGAAqG;YACrG,gGAAgG;YAChG,6DAA6D;YAC7D,IAAI,CAAC;gBACH,IAAI,CAAC,SAAS;oBAAE,UAAU,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;YAC9D,CAAC;YAAC,MAAM,CAAC;gBACP,mFAAmF;gBACnF,SAAS,GAAG,IAAI,CAAC;gBACjB,YAAY,EAAE,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;YACzC,CAAC;oBAAS,CAAC;gBACT,sGAAsG;gBACtG,UAAU,EAAE,CAAC;YACf,CAAC;QACH,CAAC;QACD,MAAM,CAAC,MAAM;YACX,QAAQ,CAAC,MAAM,CAAC,CAAC;QACnB,CAAC;KACF,CAAC;IAEF,MAAM,KAAK,GAAG,IAAI,eAAe,CAAyB,WAAW,CAAC,CAAC;IACvE,MAAM,WAAW,GAAG,KAAK,CAAC,QAAQ,CAAC,SAAS,EAAE,CAAC;IAC/C;;;;;OAKG;IACH,MAAM,QAAQ,GAAG,IAAI,cAAc,CAAa;QAC9C,KAAK,CAAC,IAAI,CAAC,UAAU;YACnB,aAAa,EAAE,CAAC;YAChB,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,WAAW,CAAC,IAAI,EAAE,CAAC;YACjD,IAAI,CAAC;gBACH,IAAI,IAAI;oBAAE,UAAU,CAAC,KAAK,EAAE,CAAC;;oBACxB,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;YACjC,CAAC;YAAC,MAAM,CAAC;gBACP,sGAAsG;YACxG,CAAC;QACH,CAAC;QACD,MAAM,CAAC,MAAM;YACX,QAAQ,CAAC,MAAM,CAAC,CAAC;YACjB,OAAO,WAAW,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QACpC,CAAC;KACF,CAAC,CAAC;IACH,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAC;AAChD,CAAC","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"]}
|
|
@@ -6,6 +6,10 @@ import { type ReactNode } from 'react';
|
|
|
6
6
|
* client component state outside the changed subtree survives. Off-site hrefs — and a traversal that leaves
|
|
7
7
|
* the app — fall back to a full load.
|
|
8
8
|
*
|
|
9
|
+
* Soft navigation is the browser's
|
|
10
|
+
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Navigation_API | Navigation API}; where that is
|
|
11
|
+
* missing, every action below is still correct and simply performs a real browser load.
|
|
12
|
+
*
|
|
9
13
|
* @example
|
|
10
14
|
* ```tsx
|
|
11
15
|
* const { router } = useNavigation();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"navigation.d.ts","sourceRoot":"","sources":["../../src/runtime/navigation.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAsC,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAE3E
|
|
1
|
+
{"version":3,"file":"navigation.d.ts","sourceRoot":"","sources":["../../src/runtime/navigation.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAsC,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAE3E;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,WAAW,gBAAgB;IAC/B,0DAA0D;IAC1D,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,sFAAsF;IACtF,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,+FAA+F;IAC/F,IAAI,IAAI,IAAI,CAAC;IACb,6FAA6F;IAC7F,OAAO,IAAI,IAAI,CAAC;IAChB,sFAAsF;IACtF,OAAO,IAAI,IAAI,CAAC;IAChB,kGAAkG;IAClG,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,oGAAoG;AACpG,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,GAAG,EAAE,GAAG,CAAC;IACT,yFAAyF;IACzF,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,4DAA4D;IAC5D,MAAM,EAAE,gBAAgB,CAAC;CAC1B;AAMD;;;;GAIG;AACH,eAAO,MAAM,aAAa,2CAAiD,CAAC;AAI5E;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,QAAQ,EAAE,SAAS,CAAA;CAAE,+BAK/H;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAgB,aAAa,IAAI,eAAe,CAQ/C"}
|
|
@@ -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;
|
|
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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"request.d.ts","sourceRoot":"","sources":["../../src/runtime/request.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"request.d.ts","sourceRoot":"","sources":["../../src/runtime/request.ts"],"names":[],"mappings":"AA0BA;;;;;;;;GAQG;AACH,MAAM,MAAM,aAAa,GAAG;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,KAAK,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,aAAa,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC;AAExI;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAU1H;AAID,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,aAAa,CAQlE;AAED,gHAAgH;AAChH,wBAAgB,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAEpD;AAED,mGAAmG;AACnG,eAAO,MAAM,eAAe,QAAQ,CAAC;AAErC,sFAAsF;AACtF,wBAAgB,QAAQ,CAAC,aAAa,EAAE,aAAa,GAAG,OAAO,CAE9D;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAEzD;AAED,6EAA6E;AAC7E,wBAAgB,eAAe,CAAC,aAAa,EAAE,aAAa,GAAG,aAAa,IAAI,OAAO,CAAC,aAAa,EAAE;IAAE,IAAI,EAAE,aAAa,GAAG,YAAY,CAAA;CAAE,CAAC,CAE7I"}
|
package/dist/runtime/request.js
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The header that names the server action a client-initiated call wants to run, and so the header that
|
|
3
|
+
* decides which of the two action branches a POST takes.
|
|
4
|
+
*
|
|
5
|
+
* That choice is a security boundary, not just a dispatch detail. It is deliberately *not* a CORS-safelisted
|
|
6
|
+
* header: a page on another origin cannot send one without a successful preflight, and the framework answers
|
|
7
|
+
* no preflight, so the `rsc-action` branch is unreachable cross-origin and carries no origin check of its
|
|
8
|
+
* own. The `form-action` branch is the forgeable one — a form post needs no preflight — and that is the one
|
|
9
|
+
* `refusesCrossSiteForm` stands in front of. See `SECURITY.md`.
|
|
10
|
+
*/
|
|
1
11
|
const HEADER_ACTION_ID = 'x-rsc-action';
|
|
2
12
|
/**
|
|
3
13
|
* The header that asks a page URL for its flight payload rather than its HTML document, and the value it
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"request.js","sourceRoot":"","sources":["../../src/runtime/request.ts"],"names":[],"mappings":"AAAA,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":["
|
|
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"]}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two modules an app hands the framework — `src/routes.ts` and `src/server.ts` — checked before
|
|
3
|
+
* anything is built on them.
|
|
4
|
+
*
|
|
5
|
+
* Both arrive through a build-time alias that carries no type the compiler can hold on to, so they used to
|
|
6
|
+
* be cast and the mistake found later, somewhere else: a `routes` array that skipped `defineRoutes` surfaced
|
|
7
|
+
* as `TypeError: nN is not iterable` from a minified bundle, and a `src/server.ts` exporting the wrong thing
|
|
8
|
+
* as `Cannot read properties of undefined (reading 'map')` from inside Hono. Neither names the file to open.
|
|
9
|
+
* The mistakes that produced no error at all were worse: a duplicated `path` whose second entry silently
|
|
10
|
+
* never ran, a `staticPaths` on a route that renders per request.
|
|
11
|
+
*
|
|
12
|
+
* Everything here runs once, at module load — during `rshono build`, which imports the server bundle for the
|
|
13
|
+
* prerender pass; at `rshono dev` startup; and when a deployed server boots.
|
|
14
|
+
*/
|
|
15
|
+
import type { Hono } from 'hono';
|
|
16
|
+
import { type RouteConfig } from '../router.js';
|
|
17
|
+
/**
|
|
18
|
+
* Normalises and checks what `src/routes.ts` exported as `routes`.
|
|
19
|
+
*
|
|
20
|
+
* A bare array is accepted alongside a {@link RouteConfig}, because the docs present both and
|
|
21
|
+
* `defineRoutes` is an identity function over the array form — which makes leaving it off look equivalent
|
|
22
|
+
* right up to the point the build fails somewhere unrelated.
|
|
23
|
+
*/
|
|
24
|
+
export declare function validateRoutesModule(exported: unknown): RouteConfig;
|
|
25
|
+
/**
|
|
26
|
+
* The Hono app `src/server.ts` default-exports, or `null` for an app that has no `src/server.ts` — the
|
|
27
|
+
* fallback module the alias resolves to instead default-exports exactly that.
|
|
28
|
+
*
|
|
29
|
+
* Duck-typed rather than `instanceof Hono`: what the framework does with the value is `app.route('/', …)`,
|
|
30
|
+
* which reads `routes` off it and mounts the handlers, so those are what have to be there.
|
|
31
|
+
*/
|
|
32
|
+
export declare function validateServerApp(exported: unknown): Hono | null;
|
|
33
|
+
//# sourceMappingURL=validate-entries.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate-entries.d.ts","sourceRoot":"","sources":["../../src/runtime/validate-entries.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AACjC,OAAO,EAA2B,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAmJzE;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,OAAO,GAAG,WAAW,CAcnE;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,GAAG,IAAI,CAOhE"}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two modules an app hands the framework — `src/routes.ts` and `src/server.ts` — checked before
|
|
3
|
+
* anything is built on them.
|
|
4
|
+
*
|
|
5
|
+
* Both arrive through a build-time alias that carries no type the compiler can hold on to, so they used to
|
|
6
|
+
* be cast and the mistake found later, somewhere else: a `routes` array that skipped `defineRoutes` surfaced
|
|
7
|
+
* as `TypeError: nN is not iterable` from a minified bundle, and a `src/server.ts` exporting the wrong thing
|
|
8
|
+
* as `Cannot read properties of undefined (reading 'map')` from inside Hono. Neither names the file to open.
|
|
9
|
+
* The mistakes that produced no error at all were worse: a duplicated `path` whose second entry silently
|
|
10
|
+
* never ran, a `staticPaths` on a route that renders per request.
|
|
11
|
+
*
|
|
12
|
+
* Everything here runs once, at module load — during `rshono build`, which imports the server bundle for the
|
|
13
|
+
* prerender pass; at `rshono dev` startup; and when a deployed server boots.
|
|
14
|
+
*/
|
|
15
|
+
import { isPageRoute } from '../router.js';
|
|
16
|
+
/** Every method an endpoint route can name, `'all'` aside — which is also what `'all'` is expanded to below. */
|
|
17
|
+
const CONCRETE_METHODS = ['get', 'post', 'put', 'patch', 'delete', 'options'];
|
|
18
|
+
const METHODS = new Set([...CONCRETE_METHODS, 'all']);
|
|
19
|
+
/** What a page route's `render` accepts. */
|
|
20
|
+
const RENDER_MODES = ['static', 'dynamic'];
|
|
21
|
+
function fail(message) {
|
|
22
|
+
throw new Error(`[rshono] ${message}`);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* How a route is named in a message. Always by position, because that is what a duplicate `path` needs to be
|
|
26
|
+
* told apart, and by path as well wherever there is one.
|
|
27
|
+
*/
|
|
28
|
+
function label(route, index) {
|
|
29
|
+
const path = route?.path;
|
|
30
|
+
return typeof path === 'string' ? `routes[${index}] ("${path}")` : `routes[${index}]`;
|
|
31
|
+
}
|
|
32
|
+
/** Refuses a key that belongs to the *other* kind of route — see {@link validateRoute}. */
|
|
33
|
+
function refuseForeignKeys(route, name, keys, advice) {
|
|
34
|
+
for (const key of keys) {
|
|
35
|
+
if (route[key] !== undefined)
|
|
36
|
+
fail(`src/routes.ts: ${name} has \`${key}\`, which ${advice}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Checks one entry of the route table.
|
|
41
|
+
*
|
|
42
|
+
* The cross-kind key checks are the ones TypeScript cannot make: excess-property checking against a union
|
|
43
|
+
* accepts any key present in *some* member, so `staticPaths` on an `{ type: 'endpoint' }` route type-checks
|
|
44
|
+
* and is then ignored at runtime — which is indistinguishable from prerendering that quietly does nothing.
|
|
45
|
+
*/
|
|
46
|
+
function validateRoute(route, index) {
|
|
47
|
+
if (route === null || typeof route !== 'object') {
|
|
48
|
+
fail(`src/routes.ts: routes[${index}] is ${route === null ? 'null' : typeof route}, not a route object.`);
|
|
49
|
+
}
|
|
50
|
+
const entry = route;
|
|
51
|
+
const name = label(route, index);
|
|
52
|
+
if (typeof entry.path !== 'string' || !entry.path.startsWith('/')) {
|
|
53
|
+
fail(`src/routes.ts: ${name} needs a \`path\` starting with "/".`);
|
|
54
|
+
}
|
|
55
|
+
if (entry.type !== undefined && entry.type !== 'endpoint') {
|
|
56
|
+
fail(`src/routes.ts: ${name} has type ${JSON.stringify(entry.type)} — the only \`type\` is 'endpoint', and a page route omits it.`);
|
|
57
|
+
}
|
|
58
|
+
if (entry.type === 'endpoint') {
|
|
59
|
+
if (typeof entry.server !== 'function') {
|
|
60
|
+
fail(`src/routes.ts: ${name} is an endpoint, so it needs \`server\` — a function importing the module that exports \`handler\`.`);
|
|
61
|
+
}
|
|
62
|
+
if (entry.method !== undefined)
|
|
63
|
+
validateMethod(entry.method, name);
|
|
64
|
+
refuseForeignKeys(entry, name, ['component', 'render', 'staticPaths'], "only a page route has — an endpoint's `server` handler decides its own response.");
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
if (typeof entry.component !== 'function') {
|
|
68
|
+
fail(`src/routes.ts: ${name} needs \`component\` — a function importing the page module, e.g. \`() => import('./pages/home')\`.`);
|
|
69
|
+
}
|
|
70
|
+
if (entry.render !== undefined && (typeof entry.render !== 'string' || !RENDER_MODES.includes(entry.render))) {
|
|
71
|
+
fail(`src/routes.ts: ${name} has render ${JSON.stringify(entry.render)} — it is 'static' or 'dynamic'.`);
|
|
72
|
+
}
|
|
73
|
+
if (entry.staticPaths !== undefined) {
|
|
74
|
+
if (typeof entry.staticPaths !== 'function')
|
|
75
|
+
fail(`src/routes.ts: ${name} has a \`staticPaths\` that is not a function.`);
|
|
76
|
+
if (entry.render !== 'static') {
|
|
77
|
+
fail(`src/routes.ts: ${name} has \`staticPaths\` but is not \`render: 'static'\`, so it renders per request and the paths are never built.`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
refuseForeignKeys(entry, name, ['server', 'method'], "only an endpoint route has — add `type: 'endpoint'` if that is what this is.");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Checks an endpoint's `method`, which is one method or a list of them.
|
|
85
|
+
*
|
|
86
|
+
* A list is checked member by member so the message names the bad one rather than printing the array, and
|
|
87
|
+
* `'all'` is refused inside one: a list meaning every method is a list its author did not mean to write.
|
|
88
|
+
*/
|
|
89
|
+
function validateMethod(method, name) {
|
|
90
|
+
if (Array.isArray(method)) {
|
|
91
|
+
if (method.length === 0)
|
|
92
|
+
fail(`src/routes.ts: ${name} has an empty \`method\` list — omit it to answer every method.`);
|
|
93
|
+
for (const entry of method) {
|
|
94
|
+
if (entry === 'all') {
|
|
95
|
+
fail(`src/routes.ts: ${name} has 'all' inside a \`method\` list — use \`method: 'all'\`, or list the methods it answers.`);
|
|
96
|
+
}
|
|
97
|
+
validateMethod(entry, name);
|
|
98
|
+
}
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (typeof method !== 'string' || !METHODS.has(method)) {
|
|
102
|
+
const meantHead = typeof method === 'string' && method.toLowerCase() === 'head';
|
|
103
|
+
fail(`src/routes.ts: ${name} has method ${JSON.stringify(method)}, which is not one of ${[...METHODS].join(', ')}.` +
|
|
104
|
+
(meantHead ? " A HEAD is dispatched as a GET, so use 'get' — a route registered for HEAD alone answers neither." : ''));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/** The methods a route answers, with `'all'` expanded and a list flattened, so two can be compared. */
|
|
108
|
+
function methodsOf(route) {
|
|
109
|
+
// A page is registered for GET and POST: the POST is how a `<form action={serverAction}>` reaches it.
|
|
110
|
+
if (isPageRoute(route))
|
|
111
|
+
return ['get', 'post'];
|
|
112
|
+
const methods = [route.method ?? 'all'].flat();
|
|
113
|
+
return methods.includes('all') ? CONCRETE_METHODS : methods;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Refuses a route the table already answers in full. Hono matches in registration order, so a later entry
|
|
117
|
+
* whose every method and path is spoken for is dead code — and the build that produced it exits 0 with
|
|
118
|
+
* nothing to say, which is how a duplicated `path` survives.
|
|
119
|
+
*
|
|
120
|
+
* Method by method, and only when *all* of them are taken: one path split between a `'get'` endpoint and a
|
|
121
|
+
* `'post'` one shadows nothing, and neither does a catch-all registered behind a route that claims one
|
|
122
|
+
* method of it — that one still answers the rest.
|
|
123
|
+
*/
|
|
124
|
+
function assertNothingIsShadowed(routes) {
|
|
125
|
+
/** `"<method> <path>"` → the first route answering it. */
|
|
126
|
+
const claimed = new Map();
|
|
127
|
+
routes.forEach((route, index) => {
|
|
128
|
+
const name = label(route, index);
|
|
129
|
+
const methods = methodsOf(route);
|
|
130
|
+
const claimants = methods.map((method) => claimed.get(`${method} ${route.path}`));
|
|
131
|
+
if (claimants.every((claimant) => claimant !== undefined)) {
|
|
132
|
+
const by = [...new Set(claimants)].join(' and ');
|
|
133
|
+
const verbs = methods.map((method) => method.toUpperCase()).join(', ');
|
|
134
|
+
fail(`src/routes.ts: ${name} would never run — ${by} already answers ${verbs} ${route.path}, and Hono matches in registration order.`);
|
|
135
|
+
}
|
|
136
|
+
for (const method of methods) {
|
|
137
|
+
const key = `${method} ${route.path}`;
|
|
138
|
+
if (!claimed.has(key))
|
|
139
|
+
claimed.set(key, name);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
function validateFallbackPage(page, field) {
|
|
144
|
+
if (page === undefined || page === null)
|
|
145
|
+
return;
|
|
146
|
+
if (typeof page !== 'object' || typeof page.component !== 'function') {
|
|
147
|
+
fail(`src/routes.ts: \`${field}\` must be a page — \`{ component: () => import('./pages/${field}') }\`.`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Normalises and checks what `src/routes.ts` exported as `routes`.
|
|
152
|
+
*
|
|
153
|
+
* A bare array is accepted alongside a {@link RouteConfig}, because the docs present both and
|
|
154
|
+
* `defineRoutes` is an identity function over the array form — which makes leaving it off look equivalent
|
|
155
|
+
* right up to the point the build fails somewhere unrelated.
|
|
156
|
+
*/
|
|
157
|
+
export function validateRoutesModule(exported) {
|
|
158
|
+
const config = (Array.isArray(exported) ? { routes: exported } : exported);
|
|
159
|
+
if (config === null || config === undefined || typeof config !== 'object' || !Array.isArray(config.routes)) {
|
|
160
|
+
fail('src/routes.ts must export `routes` as a route array, or as a { routes, notFound?, error? } object — ' +
|
|
161
|
+
'wrap it in `defineRoutes(…)` to have TypeScript check it for you.');
|
|
162
|
+
}
|
|
163
|
+
config.routes.forEach(validateRoute);
|
|
164
|
+
assertNothingIsShadowed(config.routes);
|
|
165
|
+
validateFallbackPage(config.notFound, 'notFound');
|
|
166
|
+
validateFallbackPage(config.error, 'error');
|
|
167
|
+
return config;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* The Hono app `src/server.ts` default-exports, or `null` for an app that has no `src/server.ts` — the
|
|
171
|
+
* fallback module the alias resolves to instead default-exports exactly that.
|
|
172
|
+
*
|
|
173
|
+
* Duck-typed rather than `instanceof Hono`: what the framework does with the value is `app.route('/', …)`,
|
|
174
|
+
* which reads `routes` off it and mounts the handlers, so those are what have to be there.
|
|
175
|
+
*/
|
|
176
|
+
export function validateServerApp(exported) {
|
|
177
|
+
const app = exported?.default;
|
|
178
|
+
if (app === null || app === undefined)
|
|
179
|
+
return null;
|
|
180
|
+
if (typeof app !== 'object' || typeof app.fetch !== 'function' || !Array.isArray(app.routes)) {
|
|
181
|
+
fail('src/server.ts must `export default` a Hono app — `const server = new Hono(); … export default server;`.');
|
|
182
|
+
}
|
|
183
|
+
return app;
|
|
184
|
+
}
|
|
185
|
+
//# sourceMappingURL=validate-entries.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate-entries.js","sourceRoot":"","sources":["../../src/runtime/validate-entries.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The two modules an app hands the framework — `src/routes.ts` and `src/server.ts` — checked before\n * anything is built on them.\n *\n * Both arrive through a build-time alias that carries no type the compiler can hold on to, so they used to\n * be cast and the mistake found later, somewhere else: a `routes` array that skipped `defineRoutes` surfaced\n * as `TypeError: nN is not iterable` from a minified bundle, and a `src/server.ts` exporting the wrong thing\n * as `Cannot read properties of undefined (reading 'map')` from inside Hono. Neither names the file to open.\n * The mistakes that produced no error at all were worse: a duplicated `path` whose second entry silently\n * never ran, a `staticPaths` on a route that renders per request.\n *\n * Everything here runs once, at module load — during `rshono build`, which imports the server bundle for the\n * prerender pass; at `rshono dev` startup; and when a deployed server boots.\n */\n\nimport type { Hono } from 'hono';\nimport { isPageRoute, type Route, type RouteConfig } from '../router.js';\n\n/** Every method an endpoint route can name, `'all'` aside — which is also what `'all'` is expanded to below. */\nconst CONCRETE_METHODS = ['get', 'post', 'put', 'patch', 'delete', 'options'];\nconst METHODS = new Set([...CONCRETE_METHODS, 'all']);\n\n/** What a page route's `render` accepts. */\nconst RENDER_MODES = ['static', 'dynamic'];\n\nfunction fail(message: string): never {\n throw new Error(`[rshono] ${message}`);\n}\n\n/**\n * How a route is named in a message. Always by position, because that is what a duplicate `path` needs to be\n * told apart, and by path as well wherever there is one.\n */\nfunction label(route: unknown, index: number): string {\n const path = (route as { path?: unknown } | null)?.path;\n return typeof path === 'string' ? `routes[${index}] (\"${path}\")` : `routes[${index}]`;\n}\n\n/** Refuses a key that belongs to the *other* kind of route — see {@link validateRoute}. */\nfunction refuseForeignKeys(route: Record<string, unknown>, name: string, keys: readonly string[], advice: string): void {\n for (const key of keys) {\n if (route[key] !== undefined) fail(`src/routes.ts: ${name} has \\`${key}\\`, which ${advice}`);\n }\n}\n\n/**\n * Checks one entry of the route table.\n *\n * The cross-kind key checks are the ones TypeScript cannot make: excess-property checking against a union\n * accepts any key present in *some* member, so `staticPaths` on an `{ type: 'endpoint' }` route type-checks\n * and is then ignored at runtime — which is indistinguishable from prerendering that quietly does nothing.\n */\nfunction validateRoute(route: unknown, index: number): void {\n if (route === null || typeof route !== 'object') {\n fail(`src/routes.ts: routes[${index}] is ${route === null ? 'null' : typeof route}, not a route object.`);\n }\n const entry = route as Record<string, unknown>;\n const name = label(route, index);\n\n if (typeof entry.path !== 'string' || !entry.path.startsWith('/')) {\n fail(`src/routes.ts: ${name} needs a \\`path\\` starting with \"/\".`);\n }\n if (entry.type !== undefined && entry.type !== 'endpoint') {\n fail(`src/routes.ts: ${name} has type ${JSON.stringify(entry.type)} — the only \\`type\\` is 'endpoint', and a page route omits it.`);\n }\n\n if (entry.type === 'endpoint') {\n if (typeof entry.server !== 'function') {\n fail(`src/routes.ts: ${name} is an endpoint, so it needs \\`server\\` — a function importing the module that exports \\`handler\\`.`);\n }\n if (entry.method !== undefined) validateMethod(entry.method, name);\n refuseForeignKeys(\n entry,\n name,\n ['component', 'render', 'staticPaths'],\n \"only a page route has — an endpoint's `server` handler decides its own response.\",\n );\n } else {\n if (typeof entry.component !== 'function') {\n fail(`src/routes.ts: ${name} needs \\`component\\` — a function importing the page module, e.g. \\`() => import('./pages/home')\\`.`);\n }\n if (entry.render !== undefined && (typeof entry.render !== 'string' || !RENDER_MODES.includes(entry.render))) {\n fail(`src/routes.ts: ${name} has render ${JSON.stringify(entry.render)} — it is 'static' or 'dynamic'.`);\n }\n if (entry.staticPaths !== undefined) {\n if (typeof entry.staticPaths !== 'function') fail(`src/routes.ts: ${name} has a \\`staticPaths\\` that is not a function.`);\n if (entry.render !== 'static') {\n fail(`src/routes.ts: ${name} has \\`staticPaths\\` but is not \\`render: 'static'\\`, so it renders per request and the paths are never built.`);\n }\n }\n refuseForeignKeys(entry, name, ['server', 'method'], \"only an endpoint route has — add `type: 'endpoint'` if that is what this is.\");\n }\n}\n\n/**\n * Checks an endpoint's `method`, which is one method or a list of them.\n *\n * A list is checked member by member so the message names the bad one rather than printing the array, and\n * `'all'` is refused inside one: a list meaning every method is a list its author did not mean to write.\n */\nfunction validateMethod(method: unknown, name: string): void {\n if (Array.isArray(method)) {\n if (method.length === 0) fail(`src/routes.ts: ${name} has an empty \\`method\\` list — omit it to answer every method.`);\n for (const entry of method as unknown[]) {\n if (entry === 'all') {\n fail(`src/routes.ts: ${name} has 'all' inside a \\`method\\` list — use \\`method: 'all'\\`, or list the methods it answers.`);\n }\n validateMethod(entry, name);\n }\n return;\n }\n if (typeof method !== 'string' || !METHODS.has(method)) {\n const meantHead = typeof method === 'string' && method.toLowerCase() === 'head';\n fail(\n `src/routes.ts: ${name} has method ${JSON.stringify(method)}, which is not one of ${[...METHODS].join(', ')}.` +\n (meantHead ? \" A HEAD is dispatched as a GET, so use 'get' — a route registered for HEAD alone answers neither.\" : ''),\n );\n }\n}\n\n/** The methods a route answers, with `'all'` expanded and a list flattened, so two can be compared. */\nfunction methodsOf(route: Route): readonly string[] {\n // A page is registered for GET and POST: the POST is how a `<form action={serverAction}>` reaches it.\n if (isPageRoute(route)) return ['get', 'post'];\n const methods = [route.method ?? 'all'].flat();\n return methods.includes('all') ? CONCRETE_METHODS : methods;\n}\n\n/**\n * Refuses a route the table already answers in full. Hono matches in registration order, so a later entry\n * whose every method and path is spoken for is dead code — and the build that produced it exits 0 with\n * nothing to say, which is how a duplicated `path` survives.\n *\n * Method by method, and only when *all* of them are taken: one path split between a `'get'` endpoint and a\n * `'post'` one shadows nothing, and neither does a catch-all registered behind a route that claims one\n * method of it — that one still answers the rest.\n */\nfunction assertNothingIsShadowed(routes: readonly Route[]): void {\n /** `\"<method> <path>\"` → the first route answering it. */\n const claimed = new Map<string, string>();\n routes.forEach((route, index) => {\n const name = label(route, index);\n const methods = methodsOf(route);\n const claimants = methods.map((method) => claimed.get(`${method} ${route.path}`));\n if (claimants.every((claimant) => claimant !== undefined)) {\n const by = [...new Set(claimants)].join(' and ');\n const verbs = methods.map((method) => method.toUpperCase()).join(', ');\n fail(`src/routes.ts: ${name} would never run — ${by} already answers ${verbs} ${route.path}, and Hono matches in registration order.`);\n }\n for (const method of methods) {\n const key = `${method} ${route.path}`;\n if (!claimed.has(key)) claimed.set(key, name);\n }\n });\n}\n\nfunction validateFallbackPage(page: unknown, field: 'notFound' | 'error'): void {\n if (page === undefined || page === null) return;\n if (typeof page !== 'object' || typeof (page as { component?: unknown }).component !== 'function') {\n fail(`src/routes.ts: \\`${field}\\` must be a page — \\`{ component: () => import('./pages/${field}') }\\`.`);\n }\n}\n\n/**\n * Normalises and checks what `src/routes.ts` exported as `routes`.\n *\n * A bare array is accepted alongside a {@link RouteConfig}, because the docs present both and\n * `defineRoutes` is an identity function over the array form — which makes leaving it off look equivalent\n * right up to the point the build fails somewhere unrelated.\n */\nexport function validateRoutesModule(exported: unknown): RouteConfig {\n const config = (Array.isArray(exported) ? { routes: exported as Route[] } : exported) as RouteConfig | null | undefined;\n if (config === null || config === undefined || typeof config !== 'object' || !Array.isArray(config.routes)) {\n fail(\n 'src/routes.ts must export `routes` as a route array, or as a { routes, notFound?, error? } object — ' +\n 'wrap it in `defineRoutes(…)` to have TypeScript check it for you.',\n );\n }\n\n config.routes.forEach(validateRoute);\n assertNothingIsShadowed(config.routes);\n validateFallbackPage(config.notFound, 'notFound');\n validateFallbackPage(config.error, 'error');\n return config;\n}\n\n/**\n * The Hono app `src/server.ts` default-exports, or `null` for an app that has no `src/server.ts` — the\n * fallback module the alias resolves to instead default-exports exactly that.\n *\n * Duck-typed rather than `instanceof Hono`: what the framework does with the value is `app.route('/', …)`,\n * which reads `routes` off it and mounts the handlers, so those are what have to be there.\n */\nexport function validateServerApp(exported: unknown): Hono | null {\n const app = (exported as { default?: unknown } | null)?.default;\n if (app === null || app === undefined) return null;\n if (typeof app !== 'object' || typeof (app as Hono).fetch !== 'function' || !Array.isArray((app as Hono).routes)) {\n fail('src/server.ts must `export default` a Hono app — `const server = new Hono(); … export default server;`.');\n }\n return app as Hono;\n}\n"]}
|
|
@@ -27,18 +27,42 @@ export declare const VARIANTS: {
|
|
|
27
27
|
};
|
|
28
28
|
};
|
|
29
29
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
30
|
+
* The index the build leaves beside the pages, naming every file it wrote — one `files` array of
|
|
31
|
+
* {@link ssgFilePath} names.
|
|
32
|
+
*
|
|
33
|
+
* It exists so a reader can tell "this store has no page for that path" without asking the store, which is
|
|
34
|
+
* the difference between a `Map` lookup and a failed `readFile` (or, without a filesystem, a failed store
|
|
35
|
+
* fetch) on **every** request to a static route the build could not prerender — misses are deliberately not
|
|
36
|
+
* cached, so that one never warms up. Absent is not an error: a build from an older rshono has none, and a
|
|
37
|
+
* reader that finds none looks the way it always did.
|
|
38
|
+
*/
|
|
39
|
+
export declare const SSG_MANIFEST_FILE = "manifest.json";
|
|
40
|
+
/**
|
|
41
|
+
* Where a path's prerendered output lives, relative to the output root — or `null` for a path no single file
|
|
42
|
+
* can answer.
|
|
43
|
+
*
|
|
44
|
+
* **The one canonical form**, shared by the build that writes the file and by every deploy target that reads
|
|
45
|
+
* it back, because the two drifting apart is invisible: the build reports the page as prerendered and every
|
|
46
|
+
* request afterwards falls through to SSR, forever. Each segment is stored *decoded*, so `/docs/café` is
|
|
47
|
+
* `docs/café` whether the caller holds the path percent-encoded — the build, which interpolates `staticPaths`
|
|
48
|
+
* values into a URL — or already decoded, as Hono's `c.req.path` is: it runs `decodeURI` over any path
|
|
49
|
+
* containing a `%`. Decoded is also the form every other static file server addresses, so the tree stays
|
|
50
|
+
* servable by something that is not this framework.
|
|
32
51
|
*
|
|
33
52
|
* Always `/`-separated: the same string addresses a file on a filesystem, which accepts forward slashes on
|
|
34
53
|
* Windows too, and a key in an asset store, where a backslash would be the wrong character.
|
|
54
|
+
*
|
|
55
|
+
* `null` is everything that is not one file: a `.` or `..` segment — so traversal is a miss rather than a
|
|
56
|
+
* lookup, which a store addressed by key, with no `resolve()` to fall back on, depends on — an empty segment,
|
|
57
|
+
* and any segment holding a character a file name cannot.
|
|
35
58
|
*/
|
|
36
|
-
export declare function ssgFilePath(
|
|
59
|
+
export declare function ssgFilePath(path: string, variant?: PrerenderVariant): string | null;
|
|
37
60
|
/**
|
|
38
|
-
* {@link ssgFilePath}
|
|
39
|
-
*
|
|
61
|
+
* {@link ssgFilePath} as a URL path, for a store addressed by one rather than by key. Every segment is escaped
|
|
62
|
+
* on the way out and the store decodes it back, so a page reached through a URL lands on the same file a
|
|
63
|
+
* filesystem target opens by name.
|
|
40
64
|
*/
|
|
41
|
-
export declare function
|
|
65
|
+
export declare function ssgAssetPath(relPath: string): string;
|
|
42
66
|
/**
|
|
43
67
|
* A bounded, insertion-ordered cache of prerendered pages, so a site with thousands of them keeps a working set
|
|
44
68
|
* rather than the whole build in memory. Only *hits* are stored — caching misses would let anyone mint entries
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prerendered.d.ts","sourceRoot":"","sources":["../../src/server/prerendered.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEjD,eAAO,MAAM,QAAQ;;iBAGX,IAAI,EAAE,YAAY;iBAAE,OAAO;iBAAM,WAAW,EAAE,WAAW;;;iBACvD,IAAI,EAAE,WAAW;iBAAE,OAAO;qBAAI,GAAG,EAAE,GAAG;;iBAAI,WAAW,EAAE,kBAAkB;;CACgC,CAAC;AAEtH
|
|
1
|
+
{"version":3,"file":"prerendered.d.ts","sourceRoot":"","sources":["../../src/server/prerendered.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEjD,eAAO,MAAM,QAAQ;;iBAGX,IAAI,EAAE,YAAY;iBAAE,OAAO;iBAAM,WAAW,EAAE,WAAW;;;iBACvD,IAAI,EAAE,WAAW;iBAAE,OAAO;qBAAI,GAAG,EAAE,GAAG;;iBAAI,WAAW,EAAE,kBAAkB;;CACgC,CAAC;AAEtH;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB,kBAAkB,CAAC;AAgCjD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAyB,GAAG,MAAM,GAAG,IAAI,CAY3F;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEpD;AAKD;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,QAAQ,SAAsB,GAAG;IAC/D,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAAC;IAC9C,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,GAAG,IAAI,CAAC;CAC/C,CA0BA;AAED;;;GAGG;AACH,wBAAsB,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC,WAAW,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,CAO7E;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,UAAU,CAAC,WAAW,CAAC,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,eAAe,CAAC,CAM1H;AAED,mGAAmG;AACnG,MAAM,WAAW,eAAe;IAC9B;;;;OAIG;IACH,IAAI,EAAE,UAAU,CAAC,WAAW,CAAC,CAAC;IAC9B;;;OAGG;IACH,aAAa,EAAE,MAAM,CAAC;IACtB;;;;;;OAMG;IACH,IAAI,EAAE,MAAM,CAAC;CACd"}
|