@rshono/core 1.0.0-rc.27 → 1.0.0-rc.29

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 CHANGED
@@ -102,10 +102,11 @@ export default async function Profile({ params, ctx }: PageProps<'/profile/:id'>
102
102
  ```
103
103
 
104
104
  - Pages receive `{ url, params, ctx }` — `PageProps<'/profile/:id'>` types `params.id`, and `url` is a real
105
- `URL`. The same pair reaches a `'use client'` component from `useNavigation()`, so a read moves across the
106
- boundary unchanged — except the fragment, which a browser never sends to the server: `PageProps.url` never
107
- has one, while `useNavigation().url.hash` is read from the address bar after hydration and follows in-page
108
- links and Back/Forward.
105
+ `URL`. `params`, and `url` during SSR and hydration, reach a `'use client'` component from
106
+ `useNavigation()`; after hydration that hook's `url` follows the browser address — in-page links,
107
+ Back/Forward, the live query — while on a `render: 'static'` page `PageProps.url` stays the build-time
108
+ URL. The fragment is the one part a browser never sends to the server: it is never in `PageProps.url`,
109
+ only in the hook's `url.hash` after hydration.
109
110
  - **`ctx` is the request context** — `ctx.req`, cookies, env, middleware variables, the proxy-aware URL. It is
110
111
  the same object `getRequestContext()` returns from `@rshono/core/server`, handed over so a page needs no
111
112
  import. Reading it on a `render: 'static'` page throws: a page rendered once at build time has no request.
package/dist/cli/dev.js CHANGED
@@ -13,6 +13,12 @@ import { SERVER_DEFAULTS } from '../server/server-config.js';
13
13
  import { createStaticAssetsApp } from '../server/static.js';
14
14
  import { exit } from './exit.js';
15
15
  const WORKER_READY_TIMEOUT_MS = 15_000;
16
+ /**
17
+ * How long the old worker gets to finish the requests it is serving before it is terminated anyway. The
18
+ * worker's own drain is unbounded — a request that never completes would otherwise hold the gate closed for
19
+ * as long as it likes — so this is the real limit on a dev rebuild's downtime.
20
+ */
21
+ const WORKER_STOP_TIMEOUT_MS = 5000;
16
22
  /**
17
23
  * Files whose contents are *compiled into* a build rather than read per request, so no rebuild picks a change
18
24
  * to them up: `.env` because the `PUBLIC_` view of it is baked into the client bundle by DefinePlugin and into
@@ -128,6 +134,31 @@ export async function devCommand(options) {
128
134
  const { promise, resolve } = Promise.withResolvers();
129
135
  return { promise, open: resolve };
130
136
  }
137
+ /**
138
+ * Asks a running worker to drain and stop, resolving when it has exited. The message is the graceful half
139
+ * (`deploy/node/runtime.ts` closes the listener and lets in-flight requests finish); `terminate()` is only the
140
+ * fallback for a worker that did not exit by the deadline. Terminating first is what cut streaming responses
141
+ * — and every session fetch and render behind them — mid-body on each save.
142
+ *
143
+ * A worker that has already exited is not an error: `exit` (if it is still to come) or the timer resolves
144
+ * this either way, and `postMessage` on a stopped worker is caught rather than allowed to reject the
145
+ * rebuild.
146
+ */
147
+ function stopWorker(worker) {
148
+ const { promise, resolve } = Promise.withResolvers();
149
+ const timer = setTimeout(() => void worker.terminate().then(() => resolve(), () => resolve()), WORKER_STOP_TIMEOUT_MS);
150
+ worker.once('exit', () => {
151
+ clearTimeout(timer);
152
+ resolve();
153
+ });
154
+ try {
155
+ worker.postMessage({ type: 'shutdown' });
156
+ }
157
+ catch {
158
+ // Already stopping: the listener above still resolves this.
159
+ }
160
+ return promise;
161
+ }
131
162
  function spawnWorker() {
132
163
  return new Promise((resolve, reject) => {
133
164
  const worker = new Worker(join(outDir, 'server', 'main.mjs'), {
@@ -186,7 +217,7 @@ export async function devCommand(options) {
186
217
  if (currentWorker) {
187
218
  const old = currentWorker;
188
219
  currentWorker = null;
189
- await old.terminate();
220
+ await stopWorker(old);
190
221
  }
191
222
  const { worker, port: newPort } = await spawnWorker();
192
223
  currentWorker = worker;
@@ -1 +1 @@
1
- {"version":3,"file":"dev.js","sourceRoot":"","sources":["../../src/cli/dev.ts"],"names":[],"mappings":"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","sourcesContent":["import { serve } from '@hono/node-server';\nimport { rspack, type Stats } from '@rspack/core';\nimport { Hono } from 'hono';\nimport { proxy } from 'hono/proxy';\nimport { watch } from 'node:fs';\nimport { mkdir, rm } from 'node:fs/promises';\nimport { createServer } from 'node:net';\nimport { join } from 'node:path';\nimport { Worker } from 'node:worker_threads';\nimport { DEV_OUT_DIR, createConfigs } from '../builder/rspack-config.js';\nimport type { RshonoConfig } from '../config.js';\nimport { NODE_PRESET } from '../deploy/presets.js';\nimport type { DevMessage } from '../runtime/dev-protocol.js';\nimport { SERVER_DEFAULTS } from '../server/server-config.js';\nimport { createStaticAssetsApp } from '../server/static.js';\nimport { exit } from './exit.js';\n\nconst WORKER_READY_TIMEOUT_MS = 15_000;\n\n/**\n * Files whose contents are *compiled into* a build rather than read per request, so no rebuild picks a change\n * to them up: `.env` because the `PUBLIC_` view of it is baked into the client bundle by DefinePlugin and into\n * the SSR layer by the env-shadow prelude, and `rshono.config.ts` because `ServerConfig` is a DefinePlugin\n * literal too. `.env` is worse than the config, which the docs at least describe as build-time: a page that\n * reads `process.env` rebuilds, serves, and shows the old value.\n */\nconst BAKED_IN_FILES = ['.env.local', '.env', 'rshono.config.ts', 'rshono.config.js', 'rshono.config.mjs'];\n\n/**\n * Watches those files, only to say that a restart is needed.\n *\n * Reloading them for real means re-exec'ing this process: `process.loadEnvFile` does not override a variable\n * that is already set, so the copy this process took at startup would survive an in-place reload — and the\n * worker is spawned with `env: process.env`, so it would inherit the stale value too. Choreographing that\n * against a bound port and a live SSE connection is a great deal of machinery to save one Ctrl-C; a line\n * saying which key to press costs nothing and loses nobody an afternoon.\n *\n * The directory is watched rather than each file, so a file that does not exist yet is still covered — and\n * because an editor's atomic save replaces the file, which a watch on the file itself does not survive. An\n * explicit `--config` outside the project root is not covered: this is a convenience, not a contract.\n */\nfunction warnOnBakedInFileChanges(rootDir: string): void {\n const warnedAt = new Map<string, number>();\n try {\n // `persistent: false`: the dev server is kept alive by its listener, and a watcher that outlived it\n // would hold the process open with nothing left to report to.\n watch(rootDir, { persistent: false }, (_event, filename) => {\n if (!filename || !BAKED_IN_FILES.includes(filename)) return;\n // One save fires several events — a write, a rename, an attribute change — and they are all the same\n // save. The window is short enough that a second real edit still gets its own line.\n const now = Date.now();\n if (now - (warnedAt.get(filename) ?? 0) < 250) return;\n warnedAt.set(filename, now);\n console.warn(` ⚠ ${filename} changed — restart \\`rshono dev\\` to pick it up. It is compiled into the build, not read per request.`);\n });\n } catch {\n // No watcher available: a filesystem that cannot, or a platform limit reached. Rebuilding still works,\n // so the dev server goes on without the reminder rather than refusing to start over one.\n }\n}\n\n/** An error as the dev server shows it in the browser: the stack where there is one, since this is dev. */\nfunction describe(error: unknown): string {\n return error instanceof Error ? (error.stack ?? error.message) : String(error);\n}\n\n/**\n * Whether nothing is listening on the port yet — checked before the output directory is emptied, because a\n * second `rshono dev` would otherwise wipe the chunks the running one is still importing and only then exit\n * on EADDRINUSE.\n */\nfunction portAvailable(port: number, hostname: string): Promise<boolean> {\n const { promise, resolve } = Promise.withResolvers<boolean>();\n const probe = createServer();\n probe.once('error', () => resolve(false));\n probe.listen(port, hostname, () => probe.close(() => resolve(true)));\n return promise;\n}\n\ninterface DevOptions {\n rootDir: string;\n port?: number;\n config: RshonoConfig;\n}\n\nexport async function devCommand(options: DevOptions): Promise<void> {\n const { rootDir, config } = options;\n const port = options.port ?? SERVER_DEFAULTS.port;\n // Its own directory, never `dist/`: a `rshono build` in another terminal must not be able to delete the\n // chunks this server is still importing.\n const outDir = join(rootDir, DEV_OUT_DIR);\n\n // Before the `rm` below, not at `serve` where the bind happens — see {@link portAvailable}.\n if (!(await portAvailable(port, '127.0.0.1'))) {\n console.error(` ✗ port ${port} is already in use`);\n return exit(1);\n }\n\n await rm(outDir, { recursive: true, force: true });\n await mkdir(join(outDir, 'static'), { recursive: true });\n\n const encoder = new TextEncoder();\n const sseClients = new Set<ReadableStreamDefaultController<Uint8Array>>();\n let clientHash: string | undefined;\n\n const sseChunk = (data: unknown) => encoder.encode(`data: ${JSON.stringify(data)}\\n\\n`);\n\n /**\n * Writes one already-encoded SSE frame to every open client. A failed write is what retires a client:\n * `cancel` is not always reached first when a browser navigates away.\n */\n function sendToAll(chunk: Uint8Array): void {\n for (const controller of sseClients) {\n try {\n controller.enqueue(chunk);\n } catch {\n sseClients.delete(controller);\n }\n }\n }\n\n function broadcast(message: DevMessage): void {\n sendToAll(sseChunk(message));\n }\n\n let serverComponentsChanged = false;\n const [clientConfig, serverConfig] = createConfigs({\n rootDir,\n isDev: true,\n config,\n // Always Node, whatever `deploy` says: that is a property of `build` output, and the dev server runs the\n // bundle in a worker thread of this process.\n preset: NODE_PRESET,\n onServerComponentChanges: () => {\n serverComponentsChanged = true;\n },\n });\n const compiler = rspack([clientConfig, serverConfig]);\n const [clientCompiler, serverCompiler] = compiler.compilers;\n\n let currentWorker: Worker | null = null;\n let workerPort: number | null = null;\n let workerGate = createGate();\n let restartChain: Promise<void> = Promise.resolve();\n\n function createGate() {\n const { promise, resolve } = Promise.withResolvers<{ error?: string }>();\n return { promise, open: resolve };\n }\n\n function spawnWorker(): Promise<{ worker: Worker; port: number }> {\n return new Promise((resolve, reject) => {\n const worker = new Worker(join(outDir, 'server', 'main.mjs'), {\n workerData: { port: 0, hostname: '127.0.0.1' },\n execArgv: ['--enable-source-maps'],\n env: process.env as Record<string, string>,\n });\n const timeout = setTimeout(() => {\n // Nothing waits on the termination: the promise below is already being rejected, and a worker that\n // never reported ready has no state left worth draining.\n void worker.terminate();\n reject(new Error(`server worker did not become ready within ${WORKER_READY_TIMEOUT_MS / 1000}s`));\n }, WORKER_READY_TIMEOUT_MS);\n\n let ready = false;\n worker.once('message', (message: { type?: string; port?: number }) => {\n if (message?.type === 'ready' && typeof message.port === 'number') {\n ready = true;\n clearTimeout(timeout);\n resolve({ worker, port: message.port });\n }\n });\n // `on`, not `once`: an error after the worker is ready has no pending promise left to reject, and a\n // consumed `once` would leave it unlistened — which Node turns into an uncaught exception here.\n worker.on('error', (error: Error) => {\n if (ready) {\n console.error(' ✗ server worker crashed:', error);\n return;\n }\n clearTimeout(timeout);\n reject(error);\n });\n worker.on('exit', (code) => {\n if (worker === currentWorker && code !== 0) {\n console.error(` ✗ server worker exited with code ${code} — waiting for the next rebuild`);\n currentWorker = null;\n // Opened with the reason rather than left closed, so a request arriving before the next rebuild sees\n // it instead of hanging. `hooks.invalid` installs a fresh closed gate as soon as a file changes.\n workerGate = createGate();\n workerGate.open({ error: `The server worker exited with code ${code}. See the terminal for the error it crashed with.` });\n }\n });\n });\n }\n\n serverCompiler.hooks.invalid.tap('rshono/gate', () => {\n workerGate = createGate();\n });\n\n serverCompiler.hooks.done.tapPromise('rshono/worker', async (stats: Stats) => {\n const gate = workerGate;\n restartChain = restartChain.then(async () => {\n if (stats.hasErrors()) {\n console.error(stats.toString({ preset: 'errors-warnings', colors: true }));\n gate.open({ error: stats.toString({ preset: 'errors-only', colors: false }) });\n return;\n }\n try {\n if (currentWorker) {\n const old = currentWorker;\n currentWorker = null;\n await old.terminate();\n }\n const { worker, port: newPort } = await spawnWorker();\n currentWorker = worker;\n workerPort = newPort;\n gate.open({});\n if (serverComponentsChanged) {\n serverComponentsChanged = false;\n broadcast({ type: 'rsc-update' });\n }\n } catch (error) {\n console.error(' ✗ failed to start server worker:', error);\n gate.open({ error: describe(error) });\n }\n });\n // No link in this queue may be left rejected: `.then` on a rejected promise short-circuits, and from there\n // no worker is spawned and no gate is opened again.\n restartChain = restartChain.catch((error) => {\n console.error(' ✗ dev server restart failed:', error);\n gate.open({ error: describe(error) });\n });\n await restartChain;\n });\n\n clientCompiler.hooks.done.tap('rshono/hmr', (stats: Stats) => {\n if (stats.hasErrors()) {\n console.error(stats.toString({ preset: 'errors-warnings', colors: true }));\n return;\n }\n clientHash = stats.hash ?? undefined;\n if (clientHash) broadcast({ type: 'client-built', hash: clientHash });\n });\n\n let firstBuild = true;\n compiler.watch([{}, {}] as never, (err, multiStats) => {\n if (err) {\n console.error(' ✗ build failed:', err);\n return;\n }\n if (multiStats && !multiStats.hasErrors()) {\n // Errors print themselves through the per-compiler `done` hooks; a warning has nowhere else to go, and\n // the env shadow reports what it cannot cover that way. See `env-shadow-loader.cjs`.\n if (multiStats.hasWarnings()) console.warn(multiStats.toString({ preset: 'errors-warnings', colors: true }));\n const seconds = Math.max(...multiStats.stats.map((s) => (s.endTime ?? 0) - (s.startTime ?? 0))) / 1000;\n console.log(` ${firstBuild ? '✓ built' : '✓ rebuilt'} in ${seconds.toFixed(1)}s`);\n firstBuild = false;\n }\n });\n\n const front = new Hono();\n\n // **Answered here, not proxied — so the app's own middleware does not see an asset request in dev.**\n // That is a divergence from a build, where `/_static` is mounted *inside* the app so an asset carries\n // HSTS, the app's CSP and anything else its middleware sets (`prod-config.test.mjs` asserts it). It is\n // kept deliberately, and the reason is the gate below: every request the front-end proxies first awaits\n // `workerGate.promise`. The client bundle is built by a compiler that finishes independently of the\n // server one, so proxying assets would park the browser's JS and CSS behind the *server* rebuild — a save\n // touching one server component would stall the whole page's assets, and a server bundle that failed to\n // build would take them down with it. A worse dev experience than the divergence.\n //\n // What it costs is real and worth knowing: a CSP is developed against a policy that does not apply to the\n // files it is most likely to break, so the first time it does is in a build. If this ever has to go, the\n // shape that keeps both properties is narrower than a plain move — proxy `/_static` only while a worker\n // is live, and answer locally while the gate is closed.\n front.route('/_static', createStaticAssetsApp({ root: join(outDir, 'static'), isDev: true }));\n\n front.get('/_rshono/hmr', () => {\n let ctrl: ReadableStreamDefaultController<Uint8Array>;\n const stream = new ReadableStream<Uint8Array>({\n start(controller) {\n ctrl = controller;\n sseClients.add(controller);\n controller.enqueue(encoder.encode('retry: 500\\n\\n'));\n controller.enqueue(sseChunk({ type: 'hello', hash: clientHash } satisfies DevMessage));\n },\n cancel() {\n sseClients.delete(ctrl);\n },\n });\n return new Response(stream, {\n headers: {\n 'content-type': 'text/event-stream',\n 'cache-control': 'no-cache',\n connection: 'keep-alive',\n },\n });\n });\n\n // An SSE comment, not a message: it keeps an idle connection off a proxy's timeout without the client\n // having to know a frame type that means nothing.\n const ping = encoder.encode(': ping\\n\\n');\n setInterval(() => sendToAll(ping), 15_000).unref();\n\n front.all('*', async (c) => {\n const { error } = await workerGate.promise;\n if (error || workerPort === null) {\n return c.text(`Build failed:\\n\\n${error ?? 'server not running'}`, 500);\n }\n\n const incoming = new URL(c.req.url);\n const target = `http://127.0.0.1:${workerPort}${incoming.pathname}${incoming.search}`;\n\n // Hono's proxy helper rather than a hand-rolled `fetch`: it carries the method, the streamed body (with\n // the `duplex` Node requires) and the client's abort signal, and strips the hop-by-hop headers on the way\n // back. `headers` replaces the set wholesale, so the request's own are spread back in first.\n const response = await proxy(target, {\n raw: c.req.raw,\n // The worker's redirects are the app's answer to this request, to be rewritten below and passed on.\n redirect: 'manual',\n headers: {\n ...c.req.header(),\n // This front-end *is* the proxy the app sits behind, which is why `trustProxy` is forced on in dev:\n // without these the app resolves every URL against the worker's random 127.0.0.1 port.\n 'x-forwarded-host': incoming.host,\n 'x-forwarded-proto': incoming.protocol.replace(':', ''),\n },\n });\n\n // A redirect to the worker's own address has to become a relative one, or the browser leaves the dev\n // server for a random port only this process knows about.\n const location = response.headers.get('location');\n const loc = location ? URL.parse(location, target) : null;\n if (loc && loc.host === `127.0.0.1:${workerPort}`) {\n response.headers.set('location', `${loc.pathname}${loc.search}${loc.hash}`);\n }\n return response;\n });\n\n warnOnBakedInFileChanges(rootDir);\n\n serve({ fetch: front.fetch, port, hostname: '127.0.0.1' }, (info) => {\n console.log(` ➜ rshono dev server: http://localhost:${info.port}`);\n });\n}\n"]}
1
+ {"version":3,"file":"dev.js","sourceRoot":"","sources":["../../src/cli/dev.ts"],"names":[],"mappings":"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","sourcesContent":["import { serve } from '@hono/node-server';\nimport { rspack, type Stats } from '@rspack/core';\nimport { Hono } from 'hono';\nimport { proxy } from 'hono/proxy';\nimport { watch } from 'node:fs';\nimport { mkdir, rm } from 'node:fs/promises';\nimport { createServer } from 'node:net';\nimport { join } from 'node:path';\nimport { Worker } from 'node:worker_threads';\nimport { DEV_OUT_DIR, createConfigs } from '../builder/rspack-config.js';\nimport type { RshonoConfig } from '../config.js';\nimport { NODE_PRESET } from '../deploy/presets.js';\nimport type { DevMessage, DevWorkerMessage } from '../runtime/dev-protocol.js';\nimport { SERVER_DEFAULTS } from '../server/server-config.js';\nimport { createStaticAssetsApp } from '../server/static.js';\nimport { exit } from './exit.js';\n\nconst WORKER_READY_TIMEOUT_MS = 15_000;\n/**\n * How long the old worker gets to finish the requests it is serving before it is terminated anyway. The\n * worker's own drain is unbounded — a request that never completes would otherwise hold the gate closed for\n * as long as it likes — so this is the real limit on a dev rebuild's downtime.\n */\nconst WORKER_STOP_TIMEOUT_MS = 5000;\n\n/**\n * Files whose contents are *compiled into* a build rather than read per request, so no rebuild picks a change\n * to them up: `.env` because the `PUBLIC_` view of it is baked into the client bundle by DefinePlugin and into\n * the SSR layer by the env-shadow prelude, and `rshono.config.ts` because `ServerConfig` is a DefinePlugin\n * literal too. `.env` is worse than the config, which the docs at least describe as build-time: a page that\n * reads `process.env` rebuilds, serves, and shows the old value.\n */\nconst BAKED_IN_FILES = ['.env.local', '.env', 'rshono.config.ts', 'rshono.config.js', 'rshono.config.mjs'];\n\n/**\n * Watches those files, only to say that a restart is needed.\n *\n * Reloading them for real means re-exec'ing this process: `process.loadEnvFile` does not override a variable\n * that is already set, so the copy this process took at startup would survive an in-place reload — and the\n * worker is spawned with `env: process.env`, so it would inherit the stale value too. Choreographing that\n * against a bound port and a live SSE connection is a great deal of machinery to save one Ctrl-C; a line\n * saying which key to press costs nothing and loses nobody an afternoon.\n *\n * The directory is watched rather than each file, so a file that does not exist yet is still covered — and\n * because an editor's atomic save replaces the file, which a watch on the file itself does not survive. An\n * explicit `--config` outside the project root is not covered: this is a convenience, not a contract.\n */\nfunction warnOnBakedInFileChanges(rootDir: string): void {\n const warnedAt = new Map<string, number>();\n try {\n // `persistent: false`: the dev server is kept alive by its listener, and a watcher that outlived it\n // would hold the process open with nothing left to report to.\n watch(rootDir, { persistent: false }, (_event, filename) => {\n if (!filename || !BAKED_IN_FILES.includes(filename)) return;\n // One save fires several events — a write, a rename, an attribute change — and they are all the same\n // save. The window is short enough that a second real edit still gets its own line.\n const now = Date.now();\n if (now - (warnedAt.get(filename) ?? 0) < 250) return;\n warnedAt.set(filename, now);\n console.warn(` ⚠ ${filename} changed — restart \\`rshono dev\\` to pick it up. It is compiled into the build, not read per request.`);\n });\n } catch {\n // No watcher available: a filesystem that cannot, or a platform limit reached. Rebuilding still works,\n // so the dev server goes on without the reminder rather than refusing to start over one.\n }\n}\n\n/** An error as the dev server shows it in the browser: the stack where there is one, since this is dev. */\nfunction describe(error: unknown): string {\n return error instanceof Error ? (error.stack ?? error.message) : String(error);\n}\n\n/**\n * Whether nothing is listening on the port yet — checked before the output directory is emptied, because a\n * second `rshono dev` would otherwise wipe the chunks the running one is still importing and only then exit\n * on EADDRINUSE.\n */\nfunction portAvailable(port: number, hostname: string): Promise<boolean> {\n const { promise, resolve } = Promise.withResolvers<boolean>();\n const probe = createServer();\n probe.once('error', () => resolve(false));\n probe.listen(port, hostname, () => probe.close(() => resolve(true)));\n return promise;\n}\n\ninterface DevOptions {\n rootDir: string;\n port?: number;\n config: RshonoConfig;\n}\n\nexport async function devCommand(options: DevOptions): Promise<void> {\n const { rootDir, config } = options;\n const port = options.port ?? SERVER_DEFAULTS.port;\n // Its own directory, never `dist/`: a `rshono build` in another terminal must not be able to delete the\n // chunks this server is still importing.\n const outDir = join(rootDir, DEV_OUT_DIR);\n\n // Before the `rm` below, not at `serve` where the bind happens — see {@link portAvailable}.\n if (!(await portAvailable(port, '127.0.0.1'))) {\n console.error(` ✗ port ${port} is already in use`);\n return exit(1);\n }\n\n await rm(outDir, { recursive: true, force: true });\n await mkdir(join(outDir, 'static'), { recursive: true });\n\n const encoder = new TextEncoder();\n const sseClients = new Set<ReadableStreamDefaultController<Uint8Array>>();\n let clientHash: string | undefined;\n\n const sseChunk = (data: unknown) => encoder.encode(`data: ${JSON.stringify(data)}\\n\\n`);\n\n /**\n * Writes one already-encoded SSE frame to every open client. A failed write is what retires a client:\n * `cancel` is not always reached first when a browser navigates away.\n */\n function sendToAll(chunk: Uint8Array): void {\n for (const controller of sseClients) {\n try {\n controller.enqueue(chunk);\n } catch {\n sseClients.delete(controller);\n }\n }\n }\n\n function broadcast(message: DevMessage): void {\n sendToAll(sseChunk(message));\n }\n\n let serverComponentsChanged = false;\n const [clientConfig, serverConfig] = createConfigs({\n rootDir,\n isDev: true,\n config,\n // Always Node, whatever `deploy` says: that is a property of `build` output, and the dev server runs the\n // bundle in a worker thread of this process.\n preset: NODE_PRESET,\n onServerComponentChanges: () => {\n serverComponentsChanged = true;\n },\n });\n const compiler = rspack([clientConfig, serverConfig]);\n const [clientCompiler, serverCompiler] = compiler.compilers;\n\n let currentWorker: Worker | null = null;\n let workerPort: number | null = null;\n let workerGate = createGate();\n let restartChain: Promise<void> = Promise.resolve();\n\n function createGate() {\n const { promise, resolve } = Promise.withResolvers<{ error?: string }>();\n return { promise, open: resolve };\n }\n\n /**\n * Asks a running worker to drain and stop, resolving when it has exited. The message is the graceful half\n * (`deploy/node/runtime.ts` closes the listener and lets in-flight requests finish); `terminate()` is only the\n * fallback for a worker that did not exit by the deadline. Terminating first is what cut streaming responses\n * — and every session fetch and render behind them — mid-body on each save.\n *\n * A worker that has already exited is not an error: `exit` (if it is still to come) or the timer resolves\n * this either way, and `postMessage` on a stopped worker is caught rather than allowed to reject the\n * rebuild.\n */\n function stopWorker(worker: Worker): Promise<void> {\n const { promise, resolve } = Promise.withResolvers<void>();\n const timer = setTimeout(\n () =>\n void worker.terminate().then(\n () => resolve(),\n () => resolve(),\n ),\n WORKER_STOP_TIMEOUT_MS,\n );\n worker.once('exit', () => {\n clearTimeout(timer);\n resolve();\n });\n try {\n worker.postMessage({ type: 'shutdown' } satisfies DevWorkerMessage);\n } catch {\n // Already stopping: the listener above still resolves this.\n }\n return promise;\n }\n\n function spawnWorker(): Promise<{ worker: Worker; port: number }> {\n return new Promise((resolve, reject) => {\n const worker = new Worker(join(outDir, 'server', 'main.mjs'), {\n workerData: { port: 0, hostname: '127.0.0.1' },\n execArgv: ['--enable-source-maps'],\n env: process.env as Record<string, string>,\n });\n const timeout = setTimeout(() => {\n // Nothing waits on the termination: the promise below is already being rejected, and a worker that\n // never reported ready has no state left worth draining.\n void worker.terminate();\n reject(new Error(`server worker did not become ready within ${WORKER_READY_TIMEOUT_MS / 1000}s`));\n }, WORKER_READY_TIMEOUT_MS);\n\n let ready = false;\n worker.once('message', (message: { type?: string; port?: number }) => {\n if (message?.type === 'ready' && typeof message.port === 'number') {\n ready = true;\n clearTimeout(timeout);\n resolve({ worker, port: message.port });\n }\n });\n // `on`, not `once`: an error after the worker is ready has no pending promise left to reject, and a\n // consumed `once` would leave it unlistened — which Node turns into an uncaught exception here.\n worker.on('error', (error: Error) => {\n if (ready) {\n console.error(' ✗ server worker crashed:', error);\n return;\n }\n clearTimeout(timeout);\n reject(error);\n });\n worker.on('exit', (code) => {\n if (worker === currentWorker && code !== 0) {\n console.error(` ✗ server worker exited with code ${code} — waiting for the next rebuild`);\n currentWorker = null;\n // Opened with the reason rather than left closed, so a request arriving before the next rebuild sees\n // it instead of hanging. `hooks.invalid` installs a fresh closed gate as soon as a file changes.\n workerGate = createGate();\n workerGate.open({ error: `The server worker exited with code ${code}. See the terminal for the error it crashed with.` });\n }\n });\n });\n }\n\n serverCompiler.hooks.invalid.tap('rshono/gate', () => {\n workerGate = createGate();\n });\n\n serverCompiler.hooks.done.tapPromise('rshono/worker', async (stats: Stats) => {\n const gate = workerGate;\n restartChain = restartChain.then(async () => {\n if (stats.hasErrors()) {\n console.error(stats.toString({ preset: 'errors-warnings', colors: true }));\n gate.open({ error: stats.toString({ preset: 'errors-only', colors: false }) });\n return;\n }\n try {\n if (currentWorker) {\n const old = currentWorker;\n currentWorker = null;\n await stopWorker(old);\n }\n const { worker, port: newPort } = await spawnWorker();\n currentWorker = worker;\n workerPort = newPort;\n gate.open({});\n if (serverComponentsChanged) {\n serverComponentsChanged = false;\n broadcast({ type: 'rsc-update' });\n }\n } catch (error) {\n console.error(' ✗ failed to start server worker:', error);\n gate.open({ error: describe(error) });\n }\n });\n // No link in this queue may be left rejected: `.then` on a rejected promise short-circuits, and from there\n // no worker is spawned and no gate is opened again.\n restartChain = restartChain.catch((error) => {\n console.error(' ✗ dev server restart failed:', error);\n gate.open({ error: describe(error) });\n });\n await restartChain;\n });\n\n clientCompiler.hooks.done.tap('rshono/hmr', (stats: Stats) => {\n if (stats.hasErrors()) {\n console.error(stats.toString({ preset: 'errors-warnings', colors: true }));\n return;\n }\n clientHash = stats.hash ?? undefined;\n if (clientHash) broadcast({ type: 'client-built', hash: clientHash });\n });\n\n let firstBuild = true;\n compiler.watch([{}, {}] as never, (err, multiStats) => {\n if (err) {\n console.error(' ✗ build failed:', err);\n return;\n }\n if (multiStats && !multiStats.hasErrors()) {\n // Errors print themselves through the per-compiler `done` hooks; a warning has nowhere else to go, and\n // the env shadow reports what it cannot cover that way. See `env-shadow-loader.cjs`.\n if (multiStats.hasWarnings()) console.warn(multiStats.toString({ preset: 'errors-warnings', colors: true }));\n const seconds = Math.max(...multiStats.stats.map((s) => (s.endTime ?? 0) - (s.startTime ?? 0))) / 1000;\n console.log(` ${firstBuild ? '✓ built' : '✓ rebuilt'} in ${seconds.toFixed(1)}s`);\n firstBuild = false;\n }\n });\n\n const front = new Hono();\n\n // **Answered here, not proxied — so the app's own middleware does not see an asset request in dev.**\n // That is a divergence from a build, where `/_static` is mounted *inside* the app so an asset carries\n // HSTS, the app's CSP and anything else its middleware sets (`prod-config.test.mjs` asserts it). It is\n // kept deliberately, and the reason is the gate below: every request the front-end proxies first awaits\n // `workerGate.promise`. The client bundle is built by a compiler that finishes independently of the\n // server one, so proxying assets would park the browser's JS and CSS behind the *server* rebuild — a save\n // touching one server component would stall the whole page's assets, and a server bundle that failed to\n // build would take them down with it. A worse dev experience than the divergence.\n //\n // What it costs is real and worth knowing: a CSP is developed against a policy that does not apply to the\n // files it is most likely to break, so the first time it does is in a build. If this ever has to go, the\n // shape that keeps both properties is narrower than a plain move — proxy `/_static` only while a worker\n // is live, and answer locally while the gate is closed.\n front.route('/_static', createStaticAssetsApp({ root: join(outDir, 'static'), isDev: true }));\n\n front.get('/_rshono/hmr', () => {\n let ctrl: ReadableStreamDefaultController<Uint8Array>;\n const stream = new ReadableStream<Uint8Array>({\n start(controller) {\n ctrl = controller;\n sseClients.add(controller);\n controller.enqueue(encoder.encode('retry: 500\\n\\n'));\n controller.enqueue(sseChunk({ type: 'hello', hash: clientHash } satisfies DevMessage));\n },\n cancel() {\n sseClients.delete(ctrl);\n },\n });\n return new Response(stream, {\n headers: {\n 'content-type': 'text/event-stream',\n 'cache-control': 'no-cache',\n connection: 'keep-alive',\n },\n });\n });\n\n // An SSE comment, not a message: it keeps an idle connection off a proxy's timeout without the client\n // having to know a frame type that means nothing.\n const ping = encoder.encode(': ping\\n\\n');\n setInterval(() => sendToAll(ping), 15_000).unref();\n\n front.all('*', async (c) => {\n const { error } = await workerGate.promise;\n if (error || workerPort === null) {\n return c.text(`Build failed:\\n\\n${error ?? 'server not running'}`, 500);\n }\n\n const incoming = new URL(c.req.url);\n const target = `http://127.0.0.1:${workerPort}${incoming.pathname}${incoming.search}`;\n\n // Hono's proxy helper rather than a hand-rolled `fetch`: it carries the method, the streamed body (with\n // the `duplex` Node requires) and the client's abort signal, and strips the hop-by-hop headers on the way\n // back. `headers` replaces the set wholesale, so the request's own are spread back in first.\n const response = await proxy(target, {\n raw: c.req.raw,\n // The worker's redirects are the app's answer to this request, to be rewritten below and passed on.\n redirect: 'manual',\n headers: {\n ...c.req.header(),\n // This front-end *is* the proxy the app sits behind, which is why `trustProxy` is forced on in dev:\n // without these the app resolves every URL against the worker's random 127.0.0.1 port.\n 'x-forwarded-host': incoming.host,\n 'x-forwarded-proto': incoming.protocol.replace(':', ''),\n },\n });\n\n // A redirect to the worker's own address has to become a relative one, or the browser leaves the dev\n // server for a random port only this process knows about.\n const location = response.headers.get('location');\n const loc = location ? URL.parse(location, target) : null;\n if (loc && loc.host === `127.0.0.1:${workerPort}`) {\n response.headers.set('location', `${loc.pathname}${loc.search}${loc.hash}`);\n }\n return response;\n });\n\n warnOnBakedInFileChanges(rootDir);\n\n serve({ fetch: front.fetch, port, hostname: '127.0.0.1' }, (info) => {\n console.log(` ➜ rshono dev server: http://localhost:${info.port}`);\n });\n}\n"]}
@@ -5,6 +5,8 @@ import { onShutdown } from '../../server/shutdown.js';
5
5
  import { fileSystemRuntime } from '../filesystem.js';
6
6
  /** Bound to every interface — so the address printed on start is `localhost`, not this. */
7
7
  const WILDCARD_HOST = '0.0.0.0';
8
+ /** How long a graceful stop waits for in-flight requests before the process is ended anyway. */
9
+ const DRAIN_TIMEOUT_MS = 3000;
8
10
  /**
9
11
  * The address to listen on: an explicit override (the dev server, which picks the port for its worker) beats
10
12
  * `PORT` / `HOST`, which beat the built-in default. `??` rather than `||`, so an explicit `PORT=0` — "any free
@@ -41,9 +43,21 @@ export const runtime = {
41
43
  console.log(` ➜ rshono serving on http://${host}:${info.port}`);
42
44
  }
43
45
  });
44
- onShutdown(() => {
46
+ // Stop accepting connections, let the in-flight ones finish, then exit. `boundMs` is the point at which
47
+ // the process is ended anyway; the signal path passes its own, and the dev worker passes nothing because
48
+ // its parent is the bound — `rshono dev` terminates a worker that misses its deadline. In a worker thread
49
+ // neither SIGINT nor SIGTERM is delivered, so the message below is the only way the dev server can ask.
50
+ const drain = (boundMs) => {
45
51
  server.close(() => process.exit(0));
46
- setTimeout(() => process.exit(0), 3000).unref();
52
+ if (boundMs !== undefined)
53
+ setTimeout(() => process.exit(0), boundMs).unref();
54
+ };
55
+ onShutdown(() => drain(DRAIN_TIMEOUT_MS));
56
+ // `rshono dev` replaces this worker on every server rebuild. Without the message it would have to
57
+ // `terminate()`, which severs every response still being streamed.
58
+ parentPort?.on('message', (message) => {
59
+ if (message?.type === 'shutdown')
60
+ drain(undefined);
47
61
  });
48
62
  },
49
63
  };
@@ -1 +1 @@
1
- {"version":3,"file":"runtime.js","sourceRoot":"","sources":["../../../src/deploy/node/runtime.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE1C,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAC3E,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAEtD,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAErD,2FAA2F;AAC3F,MAAM,aAAa,GAAG,SAAS,CAAC;AAEhC;;;;GAIG;AACH,SAAS,aAAa,CAAC,SAAgD;IACrE,OAAO;QACL,IAAI,EAAE,SAAS,EAAE,IAAI,IAAI,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,eAAe,CAAC,IAAI;QACpF,QAAQ,EAAE,SAAS,EAAE,QAAQ,IAAI,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,eAAe,CAAC,IAAI;KAC1E,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,OAAO,GAAkB;IACpC,GAAG,iBAAiB;IAEpB,QAAQ,CAAC,GAAS;QAChB,wGAAwG;QACxG,IAAI,OAAO,CAAC,GAAG,CAAC,gBAAgB;YAAE,OAAO;QAEzC,6FAA6F;QAC7F,MAAM,SAAS,GAAG,UAAyD,CAAC;QAC5E,MAAM,OAAO,GAAG,aAAa,CAAC,SAAS,IAAI,SAAS,CAAC,CAAC;QAEtD,MAAM,MAAM,GAAG,KAAK,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,GAAG,OAAO,EAAE,EAAE,CAAC,IAAI,EAAE,EAAE;YAC9D,IAAI,UAAU,EAAE,CAAC;gBACf,UAAU,CAAC,WAAW,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;YAC7D,CAAC;iBAAM,CAAC;gBACN,MAAM,IAAI,GAAG,OAAO,CAAC,QAAQ,KAAK,aAAa,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC;gBACjF,OAAO,CAAC,GAAG,CAAC,gCAAgC,IAAI,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;YACnE,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,UAAU,CAAC,GAAG,EAAE;YACd,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;YACpC,UAAU,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,KAAK,EAAE,CAAC;QAClD,CAAC,CAAC,CAAC;IACL,CAAC;CACF,CAAC","sourcesContent":["import { serve } from '@hono/node-server';\nimport type { Hono } from 'hono';\nimport { parentPort, workerData } from 'node:worker_threads';\nimport { parsePort, SERVER_DEFAULTS } from '../../server/server-config.js';\nimport { onShutdown } from '../../server/shutdown.js';\nimport type { DeployRuntime } from '../contract.js';\nimport { fileSystemRuntime } from '../filesystem.js';\n\n/** Bound to every interface — so the address printed on start is `localhost`, not this. */\nconst WILDCARD_HOST = '0.0.0.0';\n\n/**\n * The address to listen on: an explicit override (the dev server, which picks the port for its worker) beats\n * `PORT` / `HOST`, which beat the built-in default. `??` rather than `||`, so an explicit `PORT=0` — \"any free\n * port\" — is honoured, and so that {@link parsePort} is never consulted for a port the override already won.\n */\nfunction listenAddress(overrides?: { port?: number; hostname?: string }): { port: number; hostname: string } {\n return {\n port: overrides?.port ?? parsePort(process.env.PORT, 'PORT') ?? SERVER_DEFAULTS.port,\n hostname: overrides?.hostname ?? process.env.HOST ?? SERVER_DEFAULTS.host,\n };\n}\n\n/**\n * Node: a long-lived process that owns its own port, with a filesystem behind every asset — the shape the\n * framework was built against, and the only target `rshono dev` produces.\n *\n * Anything that runs a Node process runs this build, Bun and Deno included: the listener is\n * `@hono/node-server`, and both implement the `node:` APIs it needs.\n */\nexport const runtime: DeployRuntime = {\n ...fileSystemRuntime,\n\n serveApp(app: Hono): undefined {\n // The prerender pass renders through `app.fetch` directly, and a bound port would keep the build alive.\n if (process.env.RSHONO_PRERENDER) return;\n\n // The dev server runs this in a worker thread and picks the port itself, so its choice wins.\n const devWorker = workerData as { port?: number; hostname?: string } | null;\n const address = listenAddress(devWorker ?? undefined);\n\n const server = serve({ fetch: app.fetch, ...address }, (info) => {\n if (parentPort) {\n parentPort.postMessage({ type: 'ready', port: info.port });\n } else {\n const host = address.hostname === WILDCARD_HOST ? 'localhost' : address.hostname;\n console.log(` ➜ rshono serving on http://${host}:${info.port}`);\n }\n });\n\n onShutdown(() => {\n server.close(() => process.exit(0));\n setTimeout(() => process.exit(0), 3000).unref();\n });\n },\n};\n"]}
1
+ {"version":3,"file":"runtime.js","sourceRoot":"","sources":["../../../src/deploy/node/runtime.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE1C,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAC3E,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAGtD,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAErD,2FAA2F;AAC3F,MAAM,aAAa,GAAG,SAAS,CAAC;AAEhC,gGAAgG;AAChG,MAAM,gBAAgB,GAAG,IAAI,CAAC;AAE9B;;;;GAIG;AACH,SAAS,aAAa,CAAC,SAAgD;IACrE,OAAO;QACL,IAAI,EAAE,SAAS,EAAE,IAAI,IAAI,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,eAAe,CAAC,IAAI;QACpF,QAAQ,EAAE,SAAS,EAAE,QAAQ,IAAI,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,eAAe,CAAC,IAAI;KAC1E,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,OAAO,GAAkB;IACpC,GAAG,iBAAiB;IAEpB,QAAQ,CAAC,GAAS;QAChB,wGAAwG;QACxG,IAAI,OAAO,CAAC,GAAG,CAAC,gBAAgB;YAAE,OAAO;QAEzC,6FAA6F;QAC7F,MAAM,SAAS,GAAG,UAAyD,CAAC;QAC5E,MAAM,OAAO,GAAG,aAAa,CAAC,SAAS,IAAI,SAAS,CAAC,CAAC;QAEtD,MAAM,MAAM,GAAG,KAAK,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,GAAG,OAAO,EAAE,EAAE,CAAC,IAAI,EAAE,EAAE;YAC9D,IAAI,UAAU,EAAE,CAAC;gBACf,UAAU,CAAC,WAAW,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;YAC7D,CAAC;iBAAM,CAAC;gBACN,MAAM,IAAI,GAAG,OAAO,CAAC,QAAQ,KAAK,aAAa,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC;gBACjF,OAAO,CAAC,GAAG,CAAC,gCAAgC,IAAI,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;YACnE,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,wGAAwG;QACxG,yGAAyG;QACzG,0GAA0G;QAC1G,wGAAwG;QACxG,MAAM,KAAK,GAAG,CAAC,OAA2B,EAAQ,EAAE;YAClD,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;YACpC,IAAI,OAAO,KAAK,SAAS;gBAAE,UAAU,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,KAAK,EAAE,CAAC;QAChF,CAAC,CAAC;QAEF,UAAU,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC,CAAC;QAC1C,kGAAkG;QAClG,mEAAmE;QACnE,UAAU,EAAE,EAAE,CAAC,SAAS,EAAE,CAAC,OAAyB,EAAE,EAAE;YACtD,IAAI,OAAO,EAAE,IAAI,KAAK,UAAU;gBAAE,KAAK,CAAC,SAAS,CAAC,CAAC;QACrD,CAAC,CAAC,CAAC;IACL,CAAC;CACF,CAAC","sourcesContent":["import { serve } from '@hono/node-server';\nimport type { Hono } from 'hono';\nimport { parentPort, workerData } from 'node:worker_threads';\nimport { parsePort, SERVER_DEFAULTS } from '../../server/server-config.js';\nimport { onShutdown } from '../../server/shutdown.js';\nimport type { DevWorkerMessage } from '../../runtime/dev-protocol.js';\nimport type { DeployRuntime } from '../contract.js';\nimport { fileSystemRuntime } from '../filesystem.js';\n\n/** Bound to every interface — so the address printed on start is `localhost`, not this. */\nconst WILDCARD_HOST = '0.0.0.0';\n\n/** How long a graceful stop waits for in-flight requests before the process is ended anyway. */\nconst DRAIN_TIMEOUT_MS = 3000;\n\n/**\n * The address to listen on: an explicit override (the dev server, which picks the port for its worker) beats\n * `PORT` / `HOST`, which beat the built-in default. `??` rather than `||`, so an explicit `PORT=0` — \"any free\n * port\" — is honoured, and so that {@link parsePort} is never consulted for a port the override already won.\n */\nfunction listenAddress(overrides?: { port?: number; hostname?: string }): { port: number; hostname: string } {\n return {\n port: overrides?.port ?? parsePort(process.env.PORT, 'PORT') ?? SERVER_DEFAULTS.port,\n hostname: overrides?.hostname ?? process.env.HOST ?? SERVER_DEFAULTS.host,\n };\n}\n\n/**\n * Node: a long-lived process that owns its own port, with a filesystem behind every asset — the shape the\n * framework was built against, and the only target `rshono dev` produces.\n *\n * Anything that runs a Node process runs this build, Bun and Deno included: the listener is\n * `@hono/node-server`, and both implement the `node:` APIs it needs.\n */\nexport const runtime: DeployRuntime = {\n ...fileSystemRuntime,\n\n serveApp(app: Hono): undefined {\n // The prerender pass renders through `app.fetch` directly, and a bound port would keep the build alive.\n if (process.env.RSHONO_PRERENDER) return;\n\n // The dev server runs this in a worker thread and picks the port itself, so its choice wins.\n const devWorker = workerData as { port?: number; hostname?: string } | null;\n const address = listenAddress(devWorker ?? undefined);\n\n const server = serve({ fetch: app.fetch, ...address }, (info) => {\n if (parentPort) {\n parentPort.postMessage({ type: 'ready', port: info.port });\n } else {\n const host = address.hostname === WILDCARD_HOST ? 'localhost' : address.hostname;\n console.log(` ➜ rshono serving on http://${host}:${info.port}`);\n }\n });\n\n // Stop accepting connections, let the in-flight ones finish, then exit. `boundMs` is the point at which\n // the process is ended anyway; the signal path passes its own, and the dev worker passes nothing because\n // its parent is the bound — `rshono dev` terminates a worker that misses its deadline. In a worker thread\n // neither SIGINT nor SIGTERM is delivered, so the message below is the only way the dev server can ask.\n const drain = (boundMs: number | undefined): void => {\n server.close(() => process.exit(0));\n if (boundMs !== undefined) setTimeout(() => process.exit(0), boundMs).unref();\n };\n\n onShutdown(() => drain(DRAIN_TIMEOUT_MS));\n // `rshono dev` replaces this worker on every server rebuild. Without the message it would have to\n // `terminate()`, which severs every response still being streamed.\n parentPort?.on('message', (message: DevWorkerMessage) => {\n if (message?.type === 'shutdown') drain(undefined);\n });\n },\n};\n"]}
package/dist/router.d.ts CHANGED
@@ -28,8 +28,9 @@ export type PathParams<P extends string> = ParamKeys<P> extends never ? Record<s
28
28
  * typed key-by-key; without it `params` falls back to an open `Record<string, string>`.
29
29
  *
30
30
  * `defineRoutes` checks each page's props against `PageProps<path>`, so a mismatched path literal is
31
- * a type error at the route definition. `url` and `params` mirror what `useNavigation()` gives a
32
- * `'use client'` component, so a read moves across the server/client line unchanged.
31
+ * a type error at the route definition. `params`, and `url` during SSR and hydration, mirror what
32
+ * `useNavigation()` gives a `'use client'` component; after hydration that hook's `url` follows the
33
+ * browser address instead.
33
34
  *
34
35
  * @typeParam Path - The literal path this page is mounted at, e.g. `'/profile/:id'`.
35
36
  * @typeParam E - The app's Hono {@link Env}, to type {@link RequestContext.var} and
@@ -56,12 +57,12 @@ export interface PageProps<Path extends string = string, E extends Env = Env> {
56
57
  *
57
58
  * On a `render: 'static'` route this is the build-time URL — rendered once against `siteUrl`, so the
58
59
  * origin is `siteUrl`'s and `url.searchParams` is always empty, on first paint and after a soft
59
- * navigation alike. **`useNavigation().url` is the same frozen URL, not a way around it**: the payload
60
- * carries one `href` and both readings come from it. The fragment is the one part no payload can carry
61
- * — a browser never sends `#…` with the request — so this prop never has one, while
62
- * `useNavigation().url.hash` is read from the browser after hydration. Mark the route
63
- * `render: 'dynamic'` if the page depends on the query; a `'use client'` component that only wants it
64
- * after hydration can read `location.search` in an effect.
60
+ * navigation alike: the payload carries one build-time `href`. **`useNavigation()` reads that payload
61
+ * during SSR and hydration and then follows the browser address**, so a `'use client'` component can
62
+ * read the live query with no effect of its own. The fragment is the one part no payload can carry —
63
+ * a browser never sends `#…` with the request — so this prop never has one, while the hook's `url.hash`
64
+ * arrives from the address bar after hydration. Mark the route `render: 'dynamic'` if the page's server
65
+ * render depends on the query.
65
66
  */
66
67
  url: URL;
67
68
  /** Matched route params for this request, e.g. `{ id: '42' }` for `/profile/:id`. */
@@ -1 +1 @@
1
- {"version":3,"file":"router.js","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAoQA;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,KAAY;IACtC,OAAO,KAAK,CAAC,IAAI,KAAK,UAAU,CAAC;AACnC,CAAC;AA6LD,MAAM,UAAU,YAAY,CAAC,KAAqC;IAChE,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC,CAAE,KAAqB,CAAC;AAC3E,CAAC","sourcesContent":["import type { Env, Handler } from 'hono';\nimport type { ParamKeys, ParamKeyToRecord } from 'hono/types';\nimport type { ReactNode } from 'react';\n// Type-only, so importing `@rshono/core` pulls in none of `context.ts`'s runtime machinery.\nimport type { RequestContext } from './runtime/context.js';\n\ntype Simplify<T> = { [K in keyof T]: T[K] } & {};\ntype UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never;\n\n/**\n * The `params` record implied by a route path pattern — one required `string` key per `:param`\n * segment, `Record<string, never>` for a path with none. Paths use Hono's syntax, so `:id`,\n * `:id{[0-9]+}` and `*` all work.\n *\n * You rarely name this directly; {@link PageProps} applies it for you.\n *\n * @typeParam P - The literal route path, e.g. `'/users/:id/posts/:postId'`.\n *\n * @example\n * ```ts\n * type P = PathParams<'/users/:id/posts/:postId'>; // { id: string; postId: string }\n * ```\n *\n * @see {@link https://hono.dev/docs/api/routing#path-parameter | Hono — path parameters}\n */\nexport type PathParams<P extends string> =\n ParamKeys<P> extends never ? Record<string, never> : Simplify<UnionToIntersection<ParamKeyToRecord<ParamKeys<P>>>>;\n\n/**\n * Props every page component receives. Pass the route's path as the type argument to get `params`\n * typed key-by-key; without it `params` falls back to an open `Record<string, string>`.\n *\n * `defineRoutes` checks each page's props against `PageProps<path>`, so a mismatched path literal is\n * a type error at the route definition. `url` and `params` mirror what `useNavigation()` gives a\n * `'use client'` component, so a read moves across the server/client line unchanged.\n *\n * @typeParam Path - The literal path this page is mounted at, e.g. `'/profile/:id'`.\n * @typeParam E - The app's Hono {@link Env}, to type {@link RequestContext.var} and\n * {@link RequestContext.env} on {@link PageProps.ctx}.\n *\n * @example\n * ```tsx\n * import type { PageProps } from '@rshono/core';\n *\n * export default async function Profile({ params, url }: PageProps<'/profile/:id'>) {\n * const user = await db.getUser(params.id); // params.id is string\n * const tab = url.searchParams.get('tab') ?? 'overview';\n * return <Layout>{user.name} — {tab}</Layout>;\n * }\n * ```\n *\n * @see {@link https://www.rshono.com/docs/pages#page-props | Docs — page props}\n */\nexport interface PageProps<Path extends string = string, E extends Env = Env> {\n /**\n * The absolute browser-facing request {@link URL}, proxy-header aware (`X-Forwarded-Host` /\n * `-Proto`). A fresh instance per request, so mutating it is local to the page; it is not\n * serializable, so hand a `'use client'` component `url.href` rather than `url`.\n *\n * On a `render: 'static'` route this is the build-time URL — rendered once against `siteUrl`, so the\n * origin is `siteUrl`'s and `url.searchParams` is always empty, on first paint and after a soft\n * navigation alike. **`useNavigation().url` is the same frozen URL, not a way around it**: the payload\n * carries one `href` and both readings come from it. The fragment is the one part no payload can carry\n * — a browser never sends `#…` with the request — so this prop never has one, while\n * `useNavigation().url.hash` is read from the browser after hydration. Mark the route\n * `render: 'dynamic'` if the page depends on the query; a `'use client'` component that only wants it\n * after hydration can read `location.search` in an effect.\n */\n url: URL;\n /** Matched route params for this request, e.g. `{ id: '42' }` for `/profile/:id`. */\n params: string extends Path ? Record<string, string> : PathParams<Path>;\n /**\n * The request context — the object `getRequestContext()` returns, handed to the page so cookies,\n * headers, env and middleware variables are reachable without an import.\n *\n * Server-only, non-enumerable (a `{...props}` spread and `JSON.stringify` both skip it) and never\n * serialized. Passing it to a `'use client'` component fails the render, because it wraps the live\n * request — read what you need here and pass plain values down.\n *\n * Non-enumerable is the one place this API breaks a JavaScript expectation, and it is unavoidable: an\n * enumerable `ctx` would put `ctx.hono.env` — every binding and secret — into React's dev-only\n * serialization of a server component's props, which walks own enumerable properties. So `<Child\n * {...props} />` hands a **server** child `ctx: undefined` with no error, while the type says otherwise.\n * Nested server components are meant to call `getRequestContext()` for the same object rather than\n * receive it, which is also the fix if a spread has already cost you an afternoon.\n *\n * Reading it on a `render: 'static'` route throws: a prerendered page has no per-request context.\n * Mark the route `render: 'dynamic'`, or use the `url` / `params` props.\n *\n * @example\n * ```tsx\n * export default function Dashboard({ ctx }: PageProps) {\n * const session = ctx.cookies.get('session');\n * if (!session) redirect('/login');\n * return <Layout>Signed in as {session}</Layout>;\n * }\n * ```\n */\n ctx: RequestContext<E>;\n}\n\n/**\n * A page: a React **server component** rendering the entire document (`<html>…</html>`), usually via\n * a shared layout. It may be `async` and await data directly.\n *\n * Each page module default-exports exactly one. Interactive parts belong in `'use client'` components\n * the page imports — only those ship JS.\n *\n * @typeParam P - The component's props; for a page these are {@link PageProps}.\n *\n * @see {@link https://react.dev/reference/rsc/server-components | React — Server Components}\n * @see {@link https://www.rshono.com/docs/pages | Docs — pages}\n */\n// `any`, not `unknown`: this default is what an unparameterised `PageComponent` means in a user's own\n// annotation, and `unknown` props would reject every component that declares the props it actually takes.\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type PageComponent<P = any> = (props: P) => ReactNode | Promise<ReactNode>;\n\n/**\n * The shape an `{ type: 'endpoint' }` route's server module must have: a single named `handler`\n * export. It only ever loads on the server, so importing a database client or reading secrets from it\n * is safe.\n *\n * @example\n * ```ts\n * // src/health.ts\n * import type { Handler } from 'hono';\n *\n * export const handler: Handler = (c) => c.json({ ok: true });\n * ```\n *\n * @see {@link https://www.rshono.com/docs/routing#endpoint-routes | Docs — endpoint routes}\n */\nexport interface EndpointServerModule {\n /**\n * A Hono {@link Handler} for every request the route matches. It is passed Hono's `Context`, so the\n * request, the response builders (`c.json`, `c.text`, `c.body`) and middleware variables are all\n * reached through it.\n *\n * @see {@link https://hono.dev/docs/api/context | Hono — Context}\n */\n handler: Handler;\n}\n\n/**\n * A page route — a path rendered by a server component. This is the default\n * route kind, so `type` can be omitted.\n *\n * @example\n * ```ts\n * { path: '/profile/:id', component: () => import('./components/profile') }\n * ```\n */\nexport interface PageRoute {\n /** Discriminates a page from an endpoint; optional because `'page'` is the default. */\n type?: 'page';\n /**\n * Hono-style path pattern, e.g. `/`, `/profile/:id`, `/files/*`. Routes are matched in\n * declaration order.\n *\n * @see {@link https://hono.dev/docs/api/routing | Hono — routing}\n */\n path: string;\n /**\n * Dynamic import of the page module, whose default export is the {@link PageComponent}.\n *\n * Write it inline as `() => import('…')`: the framework detects that exact form and injects the\n * `'use server-entry'` directive that attaches the page's client JS and CSS. Wire the component up\n * any other way — a variable, a barrel re-export, a computed specifier — and you have to put\n * `'use server-entry'` on the page module's first line yourself.\n *\n * @example\n * ```ts\n * component: () => import('./components/profile')\n * ```\n *\n * @see {@link https://www.rshono.com/docs/pages#the-use-server-entry-directive | Docs — the `'use server-entry'` directive}\n */\n component: () => Promise<{ default: PageComponent }>;\n /** `'static'` prerenders the route at build time; `'dynamic'` (the default) renders per request. */\n render?: 'static' | 'dynamic';\n /**\n * For a `render: 'static'` route with params: the param sets to prerender, one page each. Runs at\n * build time on the server, so it may hit a database or read the filesystem.\n *\n * A parameterised static route without this falls back to rendering per request, with a build\n * warning. Wildcard (`*`), optional and regex params cannot be prerendered.\n *\n * **Every value has to be one portable file name**, since that is what a prerendered page is stored as,\n * and the build fails naming the value rather than writing a page nothing will serve. So: no\n * `\\ / : * ? \" < > |` or control characters, no trailing `.` or space, and not a reserved Windows device\n * name (`CON`, `NUL`, `COM1`, …) — the last two enforced everywhere, so a build that works on macOS is\n * not one that fails in CI on Windows.\n *\n * @example\n * ```ts\n * {\n * path: '/docs/:slug',\n * render: 'static',\n * component: () => import('./components/documentation'),\n * staticPaths: async () => (await db.docs.all()).map((d) => ({ slug: d.slug })),\n * }\n * ```\n *\n * @see {@link https://www.rshono.com/docs/routing#static-rendering | Docs — static rendering}\n */\n staticPaths?: () => Array<Record<string, string>> | Promise<Array<Record<string, string>>>;\n}\n\n/**\n * An endpoint route — a path served by a raw Hono handler instead of a React\n * component. Use it for JSON APIs, webhooks, redirects, feeds, or anything that\n * isn't an HTML page.\n *\n * @example\n * ```ts\n * { type: 'endpoint', path: '/api/health', server: () => import('./health') }\n * ```\n *\n * @see {@link https://www.rshono.com/docs/routing#endpoint-routes | Docs — endpoint routes}\n */\nexport interface EndpointRoute {\n /** Marks this route as an endpoint rather than a page. Required. */\n type: 'endpoint';\n /**\n * Hono-style path pattern, e.g. `/api/health`, `/api/users/:id`.\n *\n * @see {@link https://hono.dev/docs/api/routing | Hono — routing}\n */\n path: string;\n /**\n * HTTP method to match, or a list of them. Defaults to `'all'` — every method.\n *\n * There is no `'head'`: Hono dispatches a `HEAD` as a `GET` and strips the body off the response, so a\n * `HEAD` is already answered by the `'get'` handler (and by `'all'`), and a route registered for `HEAD`\n * alone would never be reached.\n *\n * A list is how a two-method endpoint says so; `'all'` inside one is refused, since it is either the\n * whole thing or a mistake. A method the route does not name gets Hono's 404 rather than the handler.\n *\n * @example\n * ```ts\n * { type: 'endpoint', path: '/api/session', method: ['get', 'delete'], server: () => import('./session') }\n * ```\n */\n method?: HTTPMethod | readonly HTTPMethod[];\n /** Dynamic import of the {@link EndpointServerModule} exporting `handler`. */\n server: () => Promise<EndpointServerModule>;\n}\n\n/**\n * HTTP methods an {@link EndpointRoute} can match. `'all'` matches every method.\n *\n * No `'head'`, deliberately — see {@link EndpointRoute.method}. A `HEAD` reaches the `'get'` handler.\n */\nexport type HTTPMethod = 'get' | 'post' | 'put' | 'patch' | 'delete' | 'options' | 'all';\n\n/** Any entry in the `routes` array: a {@link PageRoute} or an {@link EndpointRoute}. */\nexport type Route = PageRoute | EndpointRoute;\n\n/**\n * Narrows a {@link Route} to a {@link PageRoute} — `type` is optional on page routes, so anything not\n * explicitly `'endpoint'` is one.\n *\n * @internal\n */\nexport function isPageRoute(route: Route): route is PageRoute {\n return route.type !== 'endpoint';\n}\n\n/**\n * A page the framework falls back to rather than routes to — `notFound` and `error` in\n * {@link RouteConfig}. Same contract as a {@link PageRoute} `component`, without a path of its own.\n */\nexport interface FallbackPage {\n /** Dynamic import of the page module; its default export is the {@link PageComponent}. */\n component: () => Promise<{ default: PageComponent }>;\n}\n\n/**\n * The error detail handed to the `error` page. Redacted in production — a generic\n * `'Internal Server Error'` and no `stack`; in dev, the real message and stack.\n */\nexport interface ErrorPageInfo {\n /** The thrown error's message in dev; `'Internal Server Error'` in production. */\n message: string;\n /**\n * The stack trace — **dev only**, and `undefined` in every build. Optional for that reason rather than\n * because some errors lack one, so a page that renders it should guard on it, not on a mode flag.\n */\n stack?: string;\n}\n\n/**\n * Props for the `error` page declared in {@link RouteConfig.error} — the usual {@link PageProps} plus\n * the redaction-aware {@link ErrorPageInfo}.\n *\n * In a build `error.message` is the generic `'Internal Server Error'` and `error.stack` is `undefined`, so\n * the page below guards on the stack rather than on a mode flag — there is no mode flag to guard on.\n *\n * @typeParam E - The app's Hono {@link Env}, forwarded to {@link PageProps.ctx}.\n *\n * @example\n * ```tsx\n * import type { ErrorPageProps } from '@rshono/core';\n *\n * export default function ServerError({ error }: ErrorPageProps) {\n * return (\n * <html>\n * <body>\n * <h1>Something went wrong</h1>\n * <p>{error.message}</p>\n * {error.stack && <pre>{error.stack}</pre>}\n * </body>\n * </html>\n * );\n * }\n * ```\n */\nexport type ErrorPageProps<E extends Env = Env> = PageProps<string, E> & {\n /** The error that failed the request, redacted in production — see {@link ErrorPageInfo}. */\n error: ErrorPageInfo;\n};\n\n/**\n * The object form accepted by {@link defineRoutes}: the route table plus the two\n * optional framework-owned pages.\n *\n * @typeParam TRoutes - Inferred tuple of route literals, which is what makes the\n * per-route `path` → props check possible.\n *\n * @see {@link https://www.rshono.com/docs/routing#notfound-and-error | Docs — notFound and error pages}\n */\nexport interface RouteConfig<TRoutes extends readonly Route[] = readonly Route[]> {\n /** Every page and endpoint in the app, matched in order. */\n routes: TRoutes;\n /** Page rendered with a 404 status for unmatched paths and for `notFound()` calls. */\n notFound?: FallbackPage;\n /**\n * Page rendered with a 500 status when a request throws — a page component, a page module that will not\n * load, an endpoint, a server action, or middleware. Receives {@link ErrorPageProps}.\n *\n * It is a *fresh* render, with its own flight payload, so it hydrates and behaves like any other page.\n * An app that declares none gets the framework's plain 500 document instead. If the `error` page itself\n * throws, that is reported too and the framework's document answers.\n */\n error?: FallbackPage;\n}\n\n/**\n * The same check for `staticPaths`, whose param sets have to fill the route's own path: a key that does not\n * is otherwise a build-time throw from `interpolatePath` rather than a type error.\n *\n * Keys only, not full assignability, because the declared field type is `Record<string, string>` and a\n * `staticPaths` annotated as returning exactly that has to stay accepted — an index signature carries no\n * key to check, so it passes. Skipped where the path has no params, because `staticPaths` is not called for\n * such a route at all and an error there would be about the wrong thing.\n */\ntype ValidateStaticPaths<R, P extends string> =\n ParamKeys<P> extends never\n ? R\n : R extends { staticPaths: () => infer Sets }\n ? Awaited<Sets> extends ReadonlyArray<infer Set>\n ? [keyof PathParams<P>] extends [keyof Set]\n ? R\n : R & { staticPaths: `every param set staticPaths returns needs the params of '${P}'` }\n : R\n : R;\n\n// `PageProps<P, any>`, not `PageProps<P>`: only the *path* is being checked, and pinning the Env would\n// fail every page that declares its own (`PageProps<'/x', MyEnv>`, to type `ctx.var`).\ntype ValidateRoute<R> = R extends {\n path: infer P extends string;\n component: () => Promise<{ default: PageComponent<infer CP> }>;\n}\n ? // eslint-disable-next-line @typescript-eslint/no-explicit-any -- the Env is deliberately unpinned; see above.\n [PageProps<P, any>] extends [CP]\n ? ValidateStaticPaths<R, P>\n : R & { component: `component props are not satisfied by PageProps<'${P}'>` }\n : R;\n\ntype ValidateRoutes<TRoutes extends readonly Route[]> = { [K in keyof TRoutes]: ValidateRoute<TRoutes[K]> };\n\n/**\n * Refuses a key that is not a {@link RouteConfig} field — a typo'd `notfound` is otherwise a fallback page\n * that never renders, and nothing at runtime looks for one.\n *\n * Excess-property checking did this while the object form was an overload of its own with a concrete\n * parameter type. It cannot once the parameter is generic and inferred from the argument, because the\n * inferred type *has* the extra key. The message here is the better one anyway: it lands on the field.\n */\ntype ValidateConfigKeys<T> =\n Exclude<keyof T, keyof RouteConfig> extends never\n ? T\n : T & Record<Exclude<keyof T, keyof RouteConfig>, 'not a defineRoutes field — the fields are routes, notFound and error'>;\n\n/** The check for whichever of the two accepted shapes was passed. */\ntype ValidateInput<T> = T extends readonly Route[]\n ? ValidateRoutes<T>\n : T extends { routes: infer R extends readonly Route[] }\n ? ValidateConfigKeys<T> & { routes: ValidateRoutes<R> }\n : T;\n\n/** The route tuple inside either shape, so the return type carries the literals through both. */\ntype RoutesOf<T> = T extends readonly Route[] ? T : T extends { routes: infer R extends readonly Route[] } ? R : readonly Route[];\n\n/**\n * Declares the app's route table. Export the result as `routes` from `src/routes.ts` — the one file\n * rshono requires. It only ever runs on the server, so importing server-only modules from it (inside\n * `staticPaths`, say) is safe.\n *\n * Takes a {@link RouteConfig} — the `routes` array plus the optional `notFound` and `error` pages — or a\n * bare {@link Route} array as shorthand for an app with neither.\n *\n * Beyond typing the config, every page is cross-checked against its own path: props not satisfied by\n * `PageProps<'<its path>'>` make the `component` field a type error, and a `staticPaths` whose param sets do\n * not fill the path makes that field one.\n *\n * **One signature over both shapes, deliberately.** The array form used to be a second overload, which meant\n * a mistake inside a bare array was reported as an overload-resolution failure whose *first* line was the\n * object form's complaint — \"Property 'routes' is missing\" — pointing at a change the author should not make.\n * The real message was on line 8. A single signature reports the argument once, at the field that is wrong.\n *\n * @param input - A {@link RouteConfig}, or the bare `routes` array.\n * @returns The config, unchanged and fully typed; an array is wrapped as `{ routes }`.\n *\n * @example\n * ```ts\n * // src/routes.ts\n * import { defineRoutes } from '@rshono/core';\n *\n * export const routes = defineRoutes({\n * routes: [\n * { path: '/', component: () => import('./components/home') },\n * { path: '/profile/:id', component: () => import('./components/profile') },\n * {\n * path: '/docs/:slug',\n * render: 'static',\n * component: () => import('./components/documentation'),\n * staticPaths: async () => [{ slug: 'getting-started' }, { slug: 'deployment' }],\n * },\n * { type: 'endpoint', path: '/api/health', server: () => import('./health') },\n * ],\n * notFound: { component: () => import('./components/404') },\n * error: { component: () => import('./components/500') },\n * });\n * ```\n *\n * @example\n * ```ts\n * // The shorthand, for an app with no notFound or error page.\n * export const routes = defineRoutes([{ path: '/', component: () => import('./components/home') }]);\n * ```\n *\n * @see {@link https://www.rshono.com/docs/routing | Docs — routing}\n */\nexport function defineRoutes<const T extends readonly Route[] | RouteConfig<readonly Route[]>>(input: T & ValidateInput<T>): RouteConfig<RoutesOf<T>>;\nexport function defineRoutes(input: readonly Route[] | RouteConfig): RouteConfig {\n return Array.isArray(input) ? { routes: input } : (input as RouteConfig);\n}\n"]}
1
+ {"version":3,"file":"router.js","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAqQA;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,KAAY;IACtC,OAAO,KAAK,CAAC,IAAI,KAAK,UAAU,CAAC;AACnC,CAAC;AA6LD,MAAM,UAAU,YAAY,CAAC,KAAqC;IAChE,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC,CAAE,KAAqB,CAAC;AAC3E,CAAC","sourcesContent":["import type { Env, Handler } from 'hono';\nimport type { ParamKeys, ParamKeyToRecord } from 'hono/types';\nimport type { ReactNode } from 'react';\n// Type-only, so importing `@rshono/core` pulls in none of `context.ts`'s runtime machinery.\nimport type { RequestContext } from './runtime/context.js';\n\ntype Simplify<T> = { [K in keyof T]: T[K] } & {};\ntype UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never;\n\n/**\n * The `params` record implied by a route path pattern — one required `string` key per `:param`\n * segment, `Record<string, never>` for a path with none. Paths use Hono's syntax, so `:id`,\n * `:id{[0-9]+}` and `*` all work.\n *\n * You rarely name this directly; {@link PageProps} applies it for you.\n *\n * @typeParam P - The literal route path, e.g. `'/users/:id/posts/:postId'`.\n *\n * @example\n * ```ts\n * type P = PathParams<'/users/:id/posts/:postId'>; // { id: string; postId: string }\n * ```\n *\n * @see {@link https://hono.dev/docs/api/routing#path-parameter | Hono — path parameters}\n */\nexport type PathParams<P extends string> =\n ParamKeys<P> extends never ? Record<string, never> : Simplify<UnionToIntersection<ParamKeyToRecord<ParamKeys<P>>>>;\n\n/**\n * Props every page component receives. Pass the route's path as the type argument to get `params`\n * typed key-by-key; without it `params` falls back to an open `Record<string, string>`.\n *\n * `defineRoutes` checks each page's props against `PageProps<path>`, so a mismatched path literal is\n * a type error at the route definition. `params`, and `url` during SSR and hydration, mirror what\n * `useNavigation()` gives a `'use client'` component; after hydration that hook's `url` follows the\n * browser address instead.\n *\n * @typeParam Path - The literal path this page is mounted at, e.g. `'/profile/:id'`.\n * @typeParam E - The app's Hono {@link Env}, to type {@link RequestContext.var} and\n * {@link RequestContext.env} on {@link PageProps.ctx}.\n *\n * @example\n * ```tsx\n * import type { PageProps } from '@rshono/core';\n *\n * export default async function Profile({ params, url }: PageProps<'/profile/:id'>) {\n * const user = await db.getUser(params.id); // params.id is string\n * const tab = url.searchParams.get('tab') ?? 'overview';\n * return <Layout>{user.name} — {tab}</Layout>;\n * }\n * ```\n *\n * @see {@link https://www.rshono.com/docs/pages#page-props | Docs — page props}\n */\nexport interface PageProps<Path extends string = string, E extends Env = Env> {\n /**\n * The absolute browser-facing request {@link URL}, proxy-header aware (`X-Forwarded-Host` /\n * `-Proto`). A fresh instance per request, so mutating it is local to the page; it is not\n * serializable, so hand a `'use client'` component `url.href` rather than `url`.\n *\n * On a `render: 'static'` route this is the build-time URL — rendered once against `siteUrl`, so the\n * origin is `siteUrl`'s and `url.searchParams` is always empty, on first paint and after a soft\n * navigation alike: the payload carries one build-time `href`. **`useNavigation()` reads that payload\n * during SSR and hydration and then follows the browser address**, so a `'use client'` component can\n * read the live query with no effect of its own. The fragment is the one part no payload can carry —\n * a browser never sends `#…` with the request — so this prop never has one, while the hook's `url.hash`\n * arrives from the address bar after hydration. Mark the route `render: 'dynamic'` if the page's server\n * render depends on the query.\n */\n url: URL;\n /** Matched route params for this request, e.g. `{ id: '42' }` for `/profile/:id`. */\n params: string extends Path ? Record<string, string> : PathParams<Path>;\n /**\n * The request context — the object `getRequestContext()` returns, handed to the page so cookies,\n * headers, env and middleware variables are reachable without an import.\n *\n * Server-only, non-enumerable (a `{...props}` spread and `JSON.stringify` both skip it) and never\n * serialized. Passing it to a `'use client'` component fails the render, because it wraps the live\n * request — read what you need here and pass plain values down.\n *\n * Non-enumerable is the one place this API breaks a JavaScript expectation, and it is unavoidable: an\n * enumerable `ctx` would put `ctx.hono.env` — every binding and secret — into React's dev-only\n * serialization of a server component's props, which walks own enumerable properties. So `<Child\n * {...props} />` hands a **server** child `ctx: undefined` with no error, while the type says otherwise.\n * Nested server components are meant to call `getRequestContext()` for the same object rather than\n * receive it, which is also the fix if a spread has already cost you an afternoon.\n *\n * Reading it on a `render: 'static'` route throws: a prerendered page has no per-request context.\n * Mark the route `render: 'dynamic'`, or use the `url` / `params` props.\n *\n * @example\n * ```tsx\n * export default function Dashboard({ ctx }: PageProps) {\n * const session = ctx.cookies.get('session');\n * if (!session) redirect('/login');\n * return <Layout>Signed in as {session}</Layout>;\n * }\n * ```\n */\n ctx: RequestContext<E>;\n}\n\n/**\n * A page: a React **server component** rendering the entire document (`<html>…</html>`), usually via\n * a shared layout. It may be `async` and await data directly.\n *\n * Each page module default-exports exactly one. Interactive parts belong in `'use client'` components\n * the page imports — only those ship JS.\n *\n * @typeParam P - The component's props; for a page these are {@link PageProps}.\n *\n * @see {@link https://react.dev/reference/rsc/server-components | React — Server Components}\n * @see {@link https://www.rshono.com/docs/pages | Docs — pages}\n */\n// `any`, not `unknown`: this default is what an unparameterised `PageComponent` means in a user's own\n// annotation, and `unknown` props would reject every component that declares the props it actually takes.\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type PageComponent<P = any> = (props: P) => ReactNode | Promise<ReactNode>;\n\n/**\n * The shape an `{ type: 'endpoint' }` route's server module must have: a single named `handler`\n * export. It only ever loads on the server, so importing a database client or reading secrets from it\n * is safe.\n *\n * @example\n * ```ts\n * // src/health.ts\n * import type { Handler } from 'hono';\n *\n * export const handler: Handler = (c) => c.json({ ok: true });\n * ```\n *\n * @see {@link https://www.rshono.com/docs/routing#endpoint-routes | Docs — endpoint routes}\n */\nexport interface EndpointServerModule {\n /**\n * A Hono {@link Handler} for every request the route matches. It is passed Hono's `Context`, so the\n * request, the response builders (`c.json`, `c.text`, `c.body`) and middleware variables are all\n * reached through it.\n *\n * @see {@link https://hono.dev/docs/api/context | Hono — Context}\n */\n handler: Handler;\n}\n\n/**\n * A page route — a path rendered by a server component. This is the default\n * route kind, so `type` can be omitted.\n *\n * @example\n * ```ts\n * { path: '/profile/:id', component: () => import('./components/profile') }\n * ```\n */\nexport interface PageRoute {\n /** Discriminates a page from an endpoint; optional because `'page'` is the default. */\n type?: 'page';\n /**\n * Hono-style path pattern, e.g. `/`, `/profile/:id`, `/files/*`. Routes are matched in\n * declaration order.\n *\n * @see {@link https://hono.dev/docs/api/routing | Hono — routing}\n */\n path: string;\n /**\n * Dynamic import of the page module, whose default export is the {@link PageComponent}.\n *\n * Write it inline as `() => import('…')`: the framework detects that exact form and injects the\n * `'use server-entry'` directive that attaches the page's client JS and CSS. Wire the component up\n * any other way — a variable, a barrel re-export, a computed specifier — and you have to put\n * `'use server-entry'` on the page module's first line yourself.\n *\n * @example\n * ```ts\n * component: () => import('./components/profile')\n * ```\n *\n * @see {@link https://www.rshono.com/docs/pages#the-use-server-entry-directive | Docs — the `'use server-entry'` directive}\n */\n component: () => Promise<{ default: PageComponent }>;\n /** `'static'` prerenders the route at build time; `'dynamic'` (the default) renders per request. */\n render?: 'static' | 'dynamic';\n /**\n * For a `render: 'static'` route with params: the param sets to prerender, one page each. Runs at\n * build time on the server, so it may hit a database or read the filesystem.\n *\n * A parameterised static route without this falls back to rendering per request, with a build\n * warning. Wildcard (`*`), optional and regex params cannot be prerendered.\n *\n * **Every value has to be one portable file name**, since that is what a prerendered page is stored as,\n * and the build fails naming the value rather than writing a page nothing will serve. So: no\n * `\\ / : * ? \" < > |` or control characters, no trailing `.` or space, and not a reserved Windows device\n * name (`CON`, `NUL`, `COM1`, …) — the last two enforced everywhere, so a build that works on macOS is\n * not one that fails in CI on Windows.\n *\n * @example\n * ```ts\n * {\n * path: '/docs/:slug',\n * render: 'static',\n * component: () => import('./components/documentation'),\n * staticPaths: async () => (await db.docs.all()).map((d) => ({ slug: d.slug })),\n * }\n * ```\n *\n * @see {@link https://www.rshono.com/docs/routing#static-rendering | Docs — static rendering}\n */\n staticPaths?: () => Array<Record<string, string>> | Promise<Array<Record<string, string>>>;\n}\n\n/**\n * An endpoint route — a path served by a raw Hono handler instead of a React\n * component. Use it for JSON APIs, webhooks, redirects, feeds, or anything that\n * isn't an HTML page.\n *\n * @example\n * ```ts\n * { type: 'endpoint', path: '/api/health', server: () => import('./health') }\n * ```\n *\n * @see {@link https://www.rshono.com/docs/routing#endpoint-routes | Docs — endpoint routes}\n */\nexport interface EndpointRoute {\n /** Marks this route as an endpoint rather than a page. Required. */\n type: 'endpoint';\n /**\n * Hono-style path pattern, e.g. `/api/health`, `/api/users/:id`.\n *\n * @see {@link https://hono.dev/docs/api/routing | Hono — routing}\n */\n path: string;\n /**\n * HTTP method to match, or a list of them. Defaults to `'all'` — every method.\n *\n * There is no `'head'`: Hono dispatches a `HEAD` as a `GET` and strips the body off the response, so a\n * `HEAD` is already answered by the `'get'` handler (and by `'all'`), and a route registered for `HEAD`\n * alone would never be reached.\n *\n * A list is how a two-method endpoint says so; `'all'` inside one is refused, since it is either the\n * whole thing or a mistake. A method the route does not name gets Hono's 404 rather than the handler.\n *\n * @example\n * ```ts\n * { type: 'endpoint', path: '/api/session', method: ['get', 'delete'], server: () => import('./session') }\n * ```\n */\n method?: HTTPMethod | readonly HTTPMethod[];\n /** Dynamic import of the {@link EndpointServerModule} exporting `handler`. */\n server: () => Promise<EndpointServerModule>;\n}\n\n/**\n * HTTP methods an {@link EndpointRoute} can match. `'all'` matches every method.\n *\n * No `'head'`, deliberately — see {@link EndpointRoute.method}. A `HEAD` reaches the `'get'` handler.\n */\nexport type HTTPMethod = 'get' | 'post' | 'put' | 'patch' | 'delete' | 'options' | 'all';\n\n/** Any entry in the `routes` array: a {@link PageRoute} or an {@link EndpointRoute}. */\nexport type Route = PageRoute | EndpointRoute;\n\n/**\n * Narrows a {@link Route} to a {@link PageRoute} — `type` is optional on page routes, so anything not\n * explicitly `'endpoint'` is one.\n *\n * @internal\n */\nexport function isPageRoute(route: Route): route is PageRoute {\n return route.type !== 'endpoint';\n}\n\n/**\n * A page the framework falls back to rather than routes to — `notFound` and `error` in\n * {@link RouteConfig}. Same contract as a {@link PageRoute} `component`, without a path of its own.\n */\nexport interface FallbackPage {\n /** Dynamic import of the page module; its default export is the {@link PageComponent}. */\n component: () => Promise<{ default: PageComponent }>;\n}\n\n/**\n * The error detail handed to the `error` page. Redacted in production — a generic\n * `'Internal Server Error'` and no `stack`; in dev, the real message and stack.\n */\nexport interface ErrorPageInfo {\n /** The thrown error's message in dev; `'Internal Server Error'` in production. */\n message: string;\n /**\n * The stack trace — **dev only**, and `undefined` in every build. Optional for that reason rather than\n * because some errors lack one, so a page that renders it should guard on it, not on a mode flag.\n */\n stack?: string;\n}\n\n/**\n * Props for the `error` page declared in {@link RouteConfig.error} — the usual {@link PageProps} plus\n * the redaction-aware {@link ErrorPageInfo}.\n *\n * In a build `error.message` is the generic `'Internal Server Error'` and `error.stack` is `undefined`, so\n * the page below guards on the stack rather than on a mode flag — there is no mode flag to guard on.\n *\n * @typeParam E - The app's Hono {@link Env}, forwarded to {@link PageProps.ctx}.\n *\n * @example\n * ```tsx\n * import type { ErrorPageProps } from '@rshono/core';\n *\n * export default function ServerError({ error }: ErrorPageProps) {\n * return (\n * <html>\n * <body>\n * <h1>Something went wrong</h1>\n * <p>{error.message}</p>\n * {error.stack && <pre>{error.stack}</pre>}\n * </body>\n * </html>\n * );\n * }\n * ```\n */\nexport type ErrorPageProps<E extends Env = Env> = PageProps<string, E> & {\n /** The error that failed the request, redacted in production — see {@link ErrorPageInfo}. */\n error: ErrorPageInfo;\n};\n\n/**\n * The object form accepted by {@link defineRoutes}: the route table plus the two\n * optional framework-owned pages.\n *\n * @typeParam TRoutes - Inferred tuple of route literals, which is what makes the\n * per-route `path` → props check possible.\n *\n * @see {@link https://www.rshono.com/docs/routing#notfound-and-error | Docs — notFound and error pages}\n */\nexport interface RouteConfig<TRoutes extends readonly Route[] = readonly Route[]> {\n /** Every page and endpoint in the app, matched in order. */\n routes: TRoutes;\n /** Page rendered with a 404 status for unmatched paths and for `notFound()` calls. */\n notFound?: FallbackPage;\n /**\n * Page rendered with a 500 status when a request throws — a page component, a page module that will not\n * load, an endpoint, a server action, or middleware. Receives {@link ErrorPageProps}.\n *\n * It is a *fresh* render, with its own flight payload, so it hydrates and behaves like any other page.\n * An app that declares none gets the framework's plain 500 document instead. If the `error` page itself\n * throws, that is reported too and the framework's document answers.\n */\n error?: FallbackPage;\n}\n\n/**\n * The same check for `staticPaths`, whose param sets have to fill the route's own path: a key that does not\n * is otherwise a build-time throw from `interpolatePath` rather than a type error.\n *\n * Keys only, not full assignability, because the declared field type is `Record<string, string>` and a\n * `staticPaths` annotated as returning exactly that has to stay accepted — an index signature carries no\n * key to check, so it passes. Skipped where the path has no params, because `staticPaths` is not called for\n * such a route at all and an error there would be about the wrong thing.\n */\ntype ValidateStaticPaths<R, P extends string> =\n ParamKeys<P> extends never\n ? R\n : R extends { staticPaths: () => infer Sets }\n ? Awaited<Sets> extends ReadonlyArray<infer Set>\n ? [keyof PathParams<P>] extends [keyof Set]\n ? R\n : R & { staticPaths: `every param set staticPaths returns needs the params of '${P}'` }\n : R\n : R;\n\n// `PageProps<P, any>`, not `PageProps<P>`: only the *path* is being checked, and pinning the Env would\n// fail every page that declares its own (`PageProps<'/x', MyEnv>`, to type `ctx.var`).\ntype ValidateRoute<R> = R extends {\n path: infer P extends string;\n component: () => Promise<{ default: PageComponent<infer CP> }>;\n}\n ? // eslint-disable-next-line @typescript-eslint/no-explicit-any -- the Env is deliberately unpinned; see above.\n [PageProps<P, any>] extends [CP]\n ? ValidateStaticPaths<R, P>\n : R & { component: `component props are not satisfied by PageProps<'${P}'>` }\n : R;\n\ntype ValidateRoutes<TRoutes extends readonly Route[]> = { [K in keyof TRoutes]: ValidateRoute<TRoutes[K]> };\n\n/**\n * Refuses a key that is not a {@link RouteConfig} field — a typo'd `notfound` is otherwise a fallback page\n * that never renders, and nothing at runtime looks for one.\n *\n * Excess-property checking did this while the object form was an overload of its own with a concrete\n * parameter type. It cannot once the parameter is generic and inferred from the argument, because the\n * inferred type *has* the extra key. The message here is the better one anyway: it lands on the field.\n */\ntype ValidateConfigKeys<T> =\n Exclude<keyof T, keyof RouteConfig> extends never\n ? T\n : T & Record<Exclude<keyof T, keyof RouteConfig>, 'not a defineRoutes field — the fields are routes, notFound and error'>;\n\n/** The check for whichever of the two accepted shapes was passed. */\ntype ValidateInput<T> = T extends readonly Route[]\n ? ValidateRoutes<T>\n : T extends { routes: infer R extends readonly Route[] }\n ? ValidateConfigKeys<T> & { routes: ValidateRoutes<R> }\n : T;\n\n/** The route tuple inside either shape, so the return type carries the literals through both. */\ntype RoutesOf<T> = T extends readonly Route[] ? T : T extends { routes: infer R extends readonly Route[] } ? R : readonly Route[];\n\n/**\n * Declares the app's route table. Export the result as `routes` from `src/routes.ts` — the one file\n * rshono requires. It only ever runs on the server, so importing server-only modules from it (inside\n * `staticPaths`, say) is safe.\n *\n * Takes a {@link RouteConfig} — the `routes` array plus the optional `notFound` and `error` pages — or a\n * bare {@link Route} array as shorthand for an app with neither.\n *\n * Beyond typing the config, every page is cross-checked against its own path: props not satisfied by\n * `PageProps<'<its path>'>` make the `component` field a type error, and a `staticPaths` whose param sets do\n * not fill the path makes that field one.\n *\n * **One signature over both shapes, deliberately.** The array form used to be a second overload, which meant\n * a mistake inside a bare array was reported as an overload-resolution failure whose *first* line was the\n * object form's complaint — \"Property 'routes' is missing\" — pointing at a change the author should not make.\n * The real message was on line 8. A single signature reports the argument once, at the field that is wrong.\n *\n * @param input - A {@link RouteConfig}, or the bare `routes` array.\n * @returns The config, unchanged and fully typed; an array is wrapped as `{ routes }`.\n *\n * @example\n * ```ts\n * // src/routes.ts\n * import { defineRoutes } from '@rshono/core';\n *\n * export const routes = defineRoutes({\n * routes: [\n * { path: '/', component: () => import('./components/home') },\n * { path: '/profile/:id', component: () => import('./components/profile') },\n * {\n * path: '/docs/:slug',\n * render: 'static',\n * component: () => import('./components/documentation'),\n * staticPaths: async () => [{ slug: 'getting-started' }, { slug: 'deployment' }],\n * },\n * { type: 'endpoint', path: '/api/health', server: () => import('./health') },\n * ],\n * notFound: { component: () => import('./components/404') },\n * error: { component: () => import('./components/500') },\n * });\n * ```\n *\n * @example\n * ```ts\n * // The shorthand, for an app with no notFound or error page.\n * export const routes = defineRoutes([{ path: '/', component: () => import('./components/home') }]);\n * ```\n *\n * @see {@link https://www.rshono.com/docs/routing | Docs — routing}\n */\nexport function defineRoutes<const T extends readonly Route[] | RouteConfig<readonly Route[]>>(input: T & ValidateInput<T>): RouteConfig<RoutesOf<T>>;\nexport function defineRoutes(input: readonly Route[] | RouteConfig): RouteConfig {\n return Array.isArray(input) ? { routes: input } : (input as RouteConfig);\n}\n"]}
@@ -21,5 +21,5 @@
21
21
  *
22
22
  * @packageDocumentation
23
23
  */
24
- export { useNavigation, type NavigationRouter, type NavigationState } from './navigation.js';
24
+ export { useNavigation, type NavigationOutcome, type NavigationRouter, type NavigationState } from './navigation.js';
25
25
  export { AsyncBoundary, CatchBoundary, type AsyncBoundaryProps, type CatchBoundaryProps, type ErrorFallback } from './boundaries.js';
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/runtime/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,aAAa,EAA+C,MAAM,iBAAiB,CAAC;AAC7F,OAAO,EAAE,aAAa,EAAE,aAAa,EAAwE,MAAM,iBAAiB,CAAC","sourcesContent":["/**\n * `@rshono/core/client` — the browser-side surface: `useNavigation()` for the current URL and soft\n * navigation, plus the `<AsyncBoundary>` and `<CatchBoundary>` components.\n *\n * Every export is itself a `'use client'` module, so a server component can render `<AsyncBoundary>`\n * directly — but the hook needs a client component. In a server component, read the same request data\n * from `getRequestContext()` in `@rshono/core/server`.\n *\n * @example\n * ```tsx\n * 'use client';\n * import { useNavigation } from '@rshono/core/client';\n *\n * export function Tab() {\n * const { url, router } = useNavigation();\n * return <button onClick={() => router.push(`${url.pathname}?tab=details`)}>Details</button>;\n * }\n * ```\n *\n * @see {@link https://www.rshono.com/docs/api#rshonocoreclient | Docs — `@rshono/core/client`}\n *\n * @packageDocumentation\n */\n\nexport { useNavigation, type NavigationRouter, type NavigationState } from './navigation.js';\nexport { AsyncBoundary, CatchBoundary, type AsyncBoundaryProps, type CatchBoundaryProps, type ErrorFallback } from './boundaries.js';\n"]}
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/runtime/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,aAAa,EAAuE,MAAM,iBAAiB,CAAC;AACrH,OAAO,EAAE,aAAa,EAAE,aAAa,EAAwE,MAAM,iBAAiB,CAAC","sourcesContent":["/**\n * `@rshono/core/client` — the browser-side surface: `useNavigation()` for the current URL and soft\n * navigation, plus the `<AsyncBoundary>` and `<CatchBoundary>` components.\n *\n * Every export is itself a `'use client'` module, so a server component can render `<AsyncBoundary>`\n * directly — but the hook needs a client component. In a server component, read the same request data\n * from `getRequestContext()` in `@rshono/core/server`.\n *\n * @example\n * ```tsx\n * 'use client';\n * import { useNavigation } from '@rshono/core/client';\n *\n * export function Tab() {\n * const { url, router } = useNavigation();\n * return <button onClick={() => router.push(`${url.pathname}?tab=details`)}>Details</button>;\n * }\n * ```\n *\n * @see {@link https://www.rshono.com/docs/api#rshonocoreclient | Docs — `@rshono/core/client`}\n *\n * @packageDocumentation\n */\n\nexport { useNavigation, type NavigationOutcome, type NavigationRouter, type NavigationState } from './navigation.js';\nexport { AsyncBoundary, CatchBoundary, type AsyncBoundaryProps, type CatchBoundaryProps, type ErrorFallback } from './boundaries.js';\n"]}
@@ -57,7 +57,18 @@ export function parseRedirectDigest(digest) {
57
57
  if (sep === -1)
58
58
  return null;
59
59
  const status = Number(rest.slice(0, sep));
60
- const location = decodeURIComponent(rest.slice(sep + 1));
60
+ // A malformed escape is corruption between `RedirectSignal` and here — every target it encodes round
61
+ // trips — and letting `decodeURIComponent` throw would take the redirect off the wire entirely: this runs
62
+ // from React's error callbacks and from a rejected fetch, where a throw escapes as an unhandled rejection
63
+ // or out of the callback. The literal text is the better guess at the target, the same fallback
64
+ // `jumpToAnchor` uses for a malformed fragment; refusing the digest instead would read as a `notFound()`.
65
+ let location;
66
+ try {
67
+ location = decodeURIComponent(rest.slice(sep + 1));
68
+ }
69
+ catch {
70
+ location = rest.slice(sep + 1);
71
+ }
61
72
  return { location, status: Number.isFinite(status) ? status : 307 };
62
73
  }
63
74
  //# sourceMappingURL=control.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"control.js","sourceRoot":"","sources":["../../src/runtime/control.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,eAAe,GAAG,kBAAkB,CAAC;AAC3C,MAAM,gBAAgB,GAAG,kBAAkB,CAAC;AAE5C,MAAM,OAAO,cAAe,SAAQ,KAAK;IAG5B,QAAQ;IACR,MAAM;IAHR,MAAM,CAAS;IACxB,YACW,QAAgB,EAChB,MAAc;QAEvB,KAAK,CAAC,wBAAwB,QAAQ,EAAE,CAAC,CAAC;wBAHjC,QAAQ;sBACR,MAAM;QAGf,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;QAC7B,IAAI,CAAC,MAAM,GAAG,GAAG,eAAe,GAAG,MAAM,IAAI,kBAAkB,CAAC,QAAQ,CAAC,EAAE,CAAC;IAC9E,CAAC;CACF;AAED,MAAM,OAAO,cAAe,SAAQ,KAAK;IAC9B,MAAM,GAAG,gBAAgB,CAAC;IACnC;QACE,KAAK,CAAC,mBAAmB,CAAC,CAAC;QAC3B,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;IAC/B,CAAC;CACF;AAID,MAAM,UAAU,eAAe,CAAC,KAAc;IAC5C,OAAO,KAAK,YAAY,cAAc,IAAI,KAAK,YAAY,cAAc,CAAC;AAC5E,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,MAAe;IAC7C,OAAO,OAAO,MAAM,KAAK,QAAQ,IAAI,CAAC,MAAM,KAAK,gBAAgB,IAAI,MAAM,CAAC,UAAU,CAAC,eAAe,CAAC,CAAC,CAAC;AAC3G,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,eAAe,CAAC,KAAc;IAC5C,OAAO,OAAQ,KAAqC,EAAE,MAAM,KAAK,QAAQ,CAAC;AAC5E,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,MAAc;IAChD,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,eAAe,CAAC;QAAE,OAAO,IAAI,CAAC;IACrD,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC;IAClD,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAC9B,IAAI,GAAG,KAAK,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IAC5B,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;IAC1C,MAAM,QAAQ,GAAG,kBAAkB,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;IACzD,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC;AACtE,CAAC","sourcesContent":["/**\n * `redirect()` and `notFound()` as thrown errors, and the `digest` string that carries them.\n *\n * The digest is the only part that survives React — a server component's error reaches the browser as a\n * digest with no message — so the redirect target is encoded into it. The server matches the class, the\n * client matches the string.\n */\nconst REDIRECT_PREFIX = 'RSHONO_REDIRECT;';\nconst NOT_FOUND_DIGEST = 'RSHONO_NOT_FOUND';\n\nexport class RedirectSignal extends Error {\n readonly digest: string;\n constructor(\n readonly location: string,\n readonly status: number,\n ) {\n super(`[rshono] redirect to ${location}`);\n this.name = 'RedirectSignal';\n this.digest = `${REDIRECT_PREFIX}${status};${encodeURIComponent(location)}`;\n }\n}\n\nexport class NotFoundSignal extends Error {\n readonly digest = NOT_FOUND_DIGEST;\n constructor() {\n super('[rshono] notFound');\n this.name = 'NotFoundSignal';\n }\n}\n\nexport type ControlSignal = RedirectSignal | NotFoundSignal;\n\nexport function isControlSignal(error: unknown): error is ControlSignal {\n return error instanceof RedirectSignal || error instanceof NotFoundSignal;\n}\n\nexport function isControlDigest(digest: unknown): digest is string {\n return typeof digest === 'string' && (digest === NOT_FOUND_DIGEST || digest.startsWith(REDIRECT_PREFIX));\n}\n\n/**\n * Whether an error is React's stand-in for one that came out of a flight payload, rather than one that\n * started life where it was caught. Here, beside {@link isControlDigest}, because both are the same\n * question — what a `digest` tells you about where an error has been.\n *\n * A `digest` is what React puts on the far side of a payload boundary in place of the real error, so its\n * presence *is* the provenance: the layer that wrote the payload met the original and reported it in full.\n * Reporting the stand-in as well tags one fault with a second `source`, and in a build the copy carries no\n * message at all — React redacts it — so the second line says nothing the first did not.\n *\n * `reportServerError` cannot see this for itself: it de-duplicates on object identity, and a stand-in is a\n * different object. Every place a payload error can be reported a second time makes this test — the SSR\n * shell path, the top-level handler, and the `error` page's own catch.\n */\nexport function cameFromPayload(error: unknown): boolean {\n return typeof (error as { digest?: unknown } | null)?.digest === 'string';\n}\n\nexport function parseRedirectDigest(digest: string): { location: string; status: number } | null {\n if (!digest.startsWith(REDIRECT_PREFIX)) return null;\n const rest = digest.slice(REDIRECT_PREFIX.length);\n const sep = rest.indexOf(';');\n if (sep === -1) return null;\n const status = Number(rest.slice(0, sep));\n const location = decodeURIComponent(rest.slice(sep + 1));\n return { location, status: Number.isFinite(status) ? status : 307 };\n}\n"]}
1
+ {"version":3,"file":"control.js","sourceRoot":"","sources":["../../src/runtime/control.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,eAAe,GAAG,kBAAkB,CAAC;AAC3C,MAAM,gBAAgB,GAAG,kBAAkB,CAAC;AAE5C,MAAM,OAAO,cAAe,SAAQ,KAAK;IAG5B,QAAQ;IACR,MAAM;IAHR,MAAM,CAAS;IACxB,YACW,QAAgB,EAChB,MAAc;QAEvB,KAAK,CAAC,wBAAwB,QAAQ,EAAE,CAAC,CAAC;wBAHjC,QAAQ;sBACR,MAAM;QAGf,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;QAC7B,IAAI,CAAC,MAAM,GAAG,GAAG,eAAe,GAAG,MAAM,IAAI,kBAAkB,CAAC,QAAQ,CAAC,EAAE,CAAC;IAC9E,CAAC;CACF;AAED,MAAM,OAAO,cAAe,SAAQ,KAAK;IAC9B,MAAM,GAAG,gBAAgB,CAAC;IACnC;QACE,KAAK,CAAC,mBAAmB,CAAC,CAAC;QAC3B,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;IAC/B,CAAC;CACF;AAID,MAAM,UAAU,eAAe,CAAC,KAAc;IAC5C,OAAO,KAAK,YAAY,cAAc,IAAI,KAAK,YAAY,cAAc,CAAC;AAC5E,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,MAAe;IAC7C,OAAO,OAAO,MAAM,KAAK,QAAQ,IAAI,CAAC,MAAM,KAAK,gBAAgB,IAAI,MAAM,CAAC,UAAU,CAAC,eAAe,CAAC,CAAC,CAAC;AAC3G,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,eAAe,CAAC,KAAc;IAC5C,OAAO,OAAQ,KAAqC,EAAE,MAAM,KAAK,QAAQ,CAAC;AAC5E,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,MAAc;IAChD,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,eAAe,CAAC;QAAE,OAAO,IAAI,CAAC;IACrD,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC;IAClD,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAC9B,IAAI,GAAG,KAAK,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IAC5B,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;IAC1C,qGAAqG;IACrG,0GAA0G;IAC1G,0GAA0G;IAC1G,gGAAgG;IAChG,0GAA0G;IAC1G,IAAI,QAAgB,CAAC;IACrB,IAAI,CAAC;QACH,QAAQ,GAAG,kBAAkB,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;IACrD,CAAC;IAAC,MAAM,CAAC;QACP,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC;IACjC,CAAC;IACD,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC;AACtE,CAAC","sourcesContent":["/**\n * `redirect()` and `notFound()` as thrown errors, and the `digest` string that carries them.\n *\n * The digest is the only part that survives React — a server component's error reaches the browser as a\n * digest with no message — so the redirect target is encoded into it. The server matches the class, the\n * client matches the string.\n */\nconst REDIRECT_PREFIX = 'RSHONO_REDIRECT;';\nconst NOT_FOUND_DIGEST = 'RSHONO_NOT_FOUND';\n\nexport class RedirectSignal extends Error {\n readonly digest: string;\n constructor(\n readonly location: string,\n readonly status: number,\n ) {\n super(`[rshono] redirect to ${location}`);\n this.name = 'RedirectSignal';\n this.digest = `${REDIRECT_PREFIX}${status};${encodeURIComponent(location)}`;\n }\n}\n\nexport class NotFoundSignal extends Error {\n readonly digest = NOT_FOUND_DIGEST;\n constructor() {\n super('[rshono] notFound');\n this.name = 'NotFoundSignal';\n }\n}\n\nexport type ControlSignal = RedirectSignal | NotFoundSignal;\n\nexport function isControlSignal(error: unknown): error is ControlSignal {\n return error instanceof RedirectSignal || error instanceof NotFoundSignal;\n}\n\nexport function isControlDigest(digest: unknown): digest is string {\n return typeof digest === 'string' && (digest === NOT_FOUND_DIGEST || digest.startsWith(REDIRECT_PREFIX));\n}\n\n/**\n * Whether an error is React's stand-in for one that came out of a flight payload, rather than one that\n * started life where it was caught. Here, beside {@link isControlDigest}, because both are the same\n * question — what a `digest` tells you about where an error has been.\n *\n * A `digest` is what React puts on the far side of a payload boundary in place of the real error, so its\n * presence *is* the provenance: the layer that wrote the payload met the original and reported it in full.\n * Reporting the stand-in as well tags one fault with a second `source`, and in a build the copy carries no\n * message at all — React redacts it — so the second line says nothing the first did not.\n *\n * `reportServerError` cannot see this for itself: it de-duplicates on object identity, and a stand-in is a\n * different object. Every place a payload error can be reported a second time makes this test — the SSR\n * shell path, the top-level handler, and the `error` page's own catch.\n */\nexport function cameFromPayload(error: unknown): boolean {\n return typeof (error as { digest?: unknown } | null)?.digest === 'string';\n}\n\nexport function parseRedirectDigest(digest: string): { location: string; status: number } | null {\n if (!digest.startsWith(REDIRECT_PREFIX)) return null;\n const rest = digest.slice(REDIRECT_PREFIX.length);\n const sep = rest.indexOf(';');\n if (sep === -1) return null;\n const status = Number(rest.slice(0, sep));\n // A malformed escape is corruption between `RedirectSignal` and here — every target it encodes round\n // trips — and letting `decodeURIComponent` throw would take the redirect off the wire entirely: this runs\n // from React's error callbacks and from a rejected fetch, where a throw escapes as an unhandled rejection\n // or out of the callback. The literal text is the better guess at the target, the same fallback\n // `jumpToAnchor` uses for a malformed fragment; refusing the digest instead would read as a `notFound()`.\n let location: string;\n try {\n location = decodeURIComponent(rest.slice(sep + 1));\n } catch {\n location = rest.slice(sep + 1);\n }\n return { location, status: Number.isFinite(status) ? status : 307 };\n}\n"]}
@@ -15,3 +15,13 @@ export type DevMessage = {
15
15
  } | {
16
16
  type: 'rsc-update';
17
17
  };
18
+ /**
19
+ * The messages the dev server sends its own worker thread over `parentPort` — the other direction from the
20
+ * SSE channel above. Shared by `cli/dev.ts` and `deploy/node/runtime.ts` for the same reason.
21
+ *
22
+ * - `shutdown` — drain and stop. Between rebuilds the dev server asks the old worker to close its listener
23
+ * and exit once the requests it is serving have finished, rather than severing them with `terminate()`.
24
+ */
25
+ export type DevWorkerMessage = {
26
+ type: 'shutdown';
27
+ };
@@ -1 +1 @@
1
- {"version":3,"file":"dev-protocol.js","sourceRoot":"","sources":["../../src/runtime/dev-protocol.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * The messages the dev server pushes over the `/_rshono/hmr` SSE channel — one closed union shared by the\n * producer (`cli/dev.ts`) and the consumer (`runtime/entry.client.tsx`), so the wire protocol cannot drift.\n *\n * - `hello` — sent on (re)connect with the latest build hash; a mismatch means events were missed.\n * - `client-built` — the client bundle rebuilt; the client hot-applies the update.\n * - `rsc-update` — server component code changed; the client re-fetches the flight payload in place.\n */\nexport type DevMessage = { type: 'hello'; hash?: string } | { type: 'client-built'; hash: string } | { type: 'rsc-update' };\n"]}
1
+ {"version":3,"file":"dev-protocol.js","sourceRoot":"","sources":["../../src/runtime/dev-protocol.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * The messages the dev server pushes over the `/_rshono/hmr` SSE channel — one closed union shared by the\n * producer (`cli/dev.ts`) and the consumer (`runtime/entry.client.tsx`), so the wire protocol cannot drift.\n *\n * - `hello` — sent on (re)connect with the latest build hash; a mismatch means events were missed.\n * - `client-built` — the client bundle rebuilt; the client hot-applies the update.\n * - `rsc-update` — server component code changed; the client re-fetches the flight payload in place.\n */\nexport type DevMessage = { type: 'hello'; hash?: string } | { type: 'client-built'; hash: string } | { type: 'rsc-update' };\n\n/**\n * The messages the dev server sends its own worker thread over `parentPort` — the other direction from the\n * SSE channel above. Shared by `cli/dev.ts` and `deploy/node/runtime.ts` for the same reason.\n *\n * - `shutdown` — drain and stop. Between rebuilds the dev server asks the old worker to close its listener\n * and exit once the requests it is serving have finished, rather than severing them with `terminate()`.\n */\nexport type DevWorkerMessage = { type: 'shutdown' };\n"]}