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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +13 -3
  2. package/dist/builder/rspack-config.d.ts.map +1 -1
  3. package/dist/builder/rspack-config.js +5 -1
  4. package/dist/builder/rspack-config.js.map +1 -1
  5. package/dist/builder/server-only-imports.d.ts +33 -0
  6. package/dist/builder/server-only-imports.d.ts.map +1 -0
  7. package/dist/builder/server-only-imports.js +51 -0
  8. package/dist/builder/server-only-imports.js.map +1 -0
  9. package/dist/cli/build.d.ts.map +1 -1
  10. package/dist/cli/build.js +18 -9
  11. package/dist/cli/build.js.map +1 -1
  12. package/dist/cli/dev.d.ts.map +1 -1
  13. package/dist/cli/dev.js +45 -0
  14. package/dist/cli/dev.js.map +1 -1
  15. package/dist/deploy/presets.d.ts +8 -0
  16. package/dist/deploy/presets.d.ts.map +1 -1
  17. package/dist/deploy/presets.js +1 -0
  18. package/dist/deploy/presets.js.map +1 -1
  19. package/dist/router.d.ts +33 -18
  20. package/dist/router.d.ts.map +1 -1
  21. package/dist/router.js.map +1 -1
  22. package/dist/runtime/boundaries.d.ts +11 -4
  23. package/dist/runtime/boundaries.d.ts.map +1 -1
  24. package/dist/runtime/boundaries.js.map +1 -1
  25. package/dist/runtime/context.d.ts +9 -2
  26. package/dist/runtime/context.d.ts.map +1 -1
  27. package/dist/runtime/context.js +12 -3
  28. package/dist/runtime/context.js.map +1 -1
  29. package/dist/runtime/entry.client.js +124 -19
  30. package/dist/runtime/entry.client.js.map +1 -1
  31. package/dist/runtime/entry.rsc.d.ts +7 -0
  32. package/dist/runtime/entry.rsc.d.ts.map +1 -1
  33. package/dist/runtime/entry.rsc.js +17 -20
  34. package/dist/runtime/entry.rsc.js.map +1 -1
  35. package/dist/runtime/validate-entries.d.ts +49 -4
  36. package/dist/runtime/validate-entries.d.ts.map +1 -1
  37. package/dist/runtime/validate-entries.js +118 -2
  38. package/dist/runtime/validate-entries.js.map +1 -1
  39. package/dist/server/prerendered.d.ts +0 -5
  40. package/dist/server/prerendered.d.ts.map +1 -1
  41. package/dist/server/prerendered.js +1 -1
  42. package/dist/server/prerendered.js.map +1 -1
  43. package/dist/server/server-config.d.ts +8 -1
  44. package/dist/server/server-config.d.ts.map +1 -1
  45. package/dist/server/server-config.js +2 -1
  46. package/dist/server/server-config.js.map +1 -1
  47. package/dist/server/ssg.d.ts.map +1 -1
  48. package/dist/server/ssg.js +24 -7
  49. package/dist/server/ssg.js.map +1 -1
  50. package/dist/server/static.d.ts.map +1 -1
  51. package/dist/server/static.js +5 -1
  52. package/dist/server/static.js.map +1 -1
  53. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"context.js","sourceRoot":"","sources":["../../src/runtime/context.ts"],"names":[],"mappings":"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","sourcesContent":["// `__RSHONO_CONFIG__` is a global const, and an `import` cannot bring one into scope — a path reference is\n// the only way to reach it, which is the whole reason that file is separate. See its header.\n// eslint-disable-next-line @typescript-eslint/triple-slash-reference\n/// <reference path=\"../types/rshono-config.d.ts\" />\n/**\n * The request context: {@link getRequestContext} and the {@link RequestContext} it returns, the\n * {@link redirect} / {@link notFound} control-flow helpers, and the {@link onServerError} reporting\n * funnel — plus the `@internal` plumbing that binds a request to the async context.\n *\n * The public half is re-exported by `runtime/server.ts`, which is what `@rshono/core/server` resolves\n * to; an app imports that.\n */\n\nimport type { Context, Env } from 'hono';\nimport { deleteCookie, getCookie, setCookie } from 'hono/cookie';\nimport type { CookieOptions } from 'hono/utils/cookie';\nimport { AsyncLocalStorage } from 'node:async_hooks';\nimport { NotFoundSignal, RedirectSignal } from './control.js';\n\n/**\n * HTTP status codes accepted by {@link redirect}.\n *\n * - `301` Moved Permanently, `308` Permanent Redirect — cacheable, permanent.\n * - `302` Found, `307` Temporary Redirect — temporary.\n * - `303` See Other — the default; forces a `GET` on the target, which is what you almost always want\n * after a form action (post/redirect/get).\n *\n * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status#redirection_messages | MDN — redirection status codes}\n */\nexport type RedirectStatus = 301 | 302 | 303 | 307 | 308;\n\nconst contextStorage = new AsyncLocalStorage<Context>();\n\n/** One {@link RequestContext} per Hono {@link Context}, so repeated `getRequestContext()` calls share its lazy getters. */\nconst wrappers = new WeakMap<Context, RequestContext>();\n\n// Keyed on the Hono context rather than held as a field, so marking a request never forces the lazily\n// built `RequestContext` wrapper into existence.\nconst rendering = new WeakSet<Context>();\n\n/**\n * Marks the request as having entered its page render, which is what makes\n * {@link RequestContext.setHeader} and `ctx.cookies.set()` start throwing.\n *\n * @internal\n */\nexport function beginPageRender(c: Context): void {\n rendering.add(c);\n}\n\n/** The shared refusal for a response write that arrived too late — the message names where it belongs instead. */\nfunction tooLateToWrite(call: string): never {\n throw new Error(\n `[rshono] ${call} was called while rendering a page, which is too late to affect the response. ` +\n 'A page streams, so its response head is already committed by the time the component runs — the ' +\n 'write would land on a full page load and be silently dropped on a soft navigation. Do it from a ' +\n \"'use server' action instead; or, in middleware and { type: 'endpoint' } routes — which are handed \" +\n \"Hono's `c` directly and run outside the request context — with `c.header(…)` / `setCookie(c, …)`.\",\n );\n}\n\n/** The shared refusal for a Hono `Context` member a page has no way to use. See the stubs on {@link RequestContext}. */\nfunction notOnContext(call: string, instead: string): never {\n throw new Error(\n `[rshono] ctx.${call} does not exist. A page returns JSX and the framework builds the response from it, ` +\n `so Hono's response builders have nothing to return to. ${instead}`,\n );\n}\n\n// Snapshotted rather than spread per request: enumerating `process.env` crosses into the host\n// environment (~20µs). Lazily, because `loadEnvFiles()` runs after this module is imported — so a\n// mutation after the first `ctx.env` read is not picked up.\nlet envSnapshot: Record<string, string | undefined> | undefined;\n\nfunction processEnv(): Record<string, string | undefined> {\n return (envSnapshot ??= typeof process !== 'undefined' && process.env ? { ...process.env } : {});\n}\n\n// Set by `build.ts` before it imports the app bundle, which inlines its own copy of this module — so\n// `process.env` is what crosses that boundary rather than a module-level flag.\nconst prerendering = typeof process !== 'undefined' && !!process.env?.RSHONO_PRERENDER;\n\n/**\n * Runs `fn` with `c` bound as the ambient request context, so {@link getRequestContext} resolves to it\n * anywhere in the call tree.\n *\n * @internal\n */\nexport function runWithContext<T>(c: Context, fn: () => T): T {\n return contextStorage.run(c, fn);\n}\n\n/**\n * The matched route params, or an empty object when there is no active match.\n *\n * @internal\n */\nexport function readParams(c: Context): Record<string, string> {\n try {\n return c.req.param();\n } catch {\n return {};\n }\n}\n\n/** A proxy chain appends to these headers, so the client-facing value is the first entry. */\nfunction firstForwardedValue(header: string | undefined): string | undefined {\n const first = header?.split(',')[0]?.trim();\n return first || undefined;\n}\n\n// Read through `typeof`: DefinePlugin inlines this into the server bundle, but the module is also the\n// public `@rshono/core/server` entry, which tooling can load without one. Absent means don't trust.\nconst trustProxy = typeof __RSHONO_CONFIG__ !== 'undefined' && __RSHONO_CONFIG__.trustProxy;\n\n/**\n * The browser-facing {@link URL} for a request, resolved from Hono's {@link Context} — a fresh\n * instance per call.\n *\n * `c.req.url` is the internal address the server was reached on, which is wrong behind a proxy;\n * `X-Forwarded-Host` / `-Proto` correct it, but only when `trustProxy` is enabled in\n * `rshono.config.ts` — they are client-supplied, so trusting them unconditionally would let anyone\n * dictate the origin of every absolute URL the app builds.\n *\n * This is the form for **middleware**, which is handed `c` and runs outside the request context — and\n * so the way to give Hono's own middleware the origin the browser actually used. In a server component\n * or action, prefer {@link RequestContext.url}, the same value cached per request.\n *\n * @param c - The Hono {@link Context} for the request.\n * @returns The browser-facing URL — proxy-corrected under `trustProxy`, `c.req.url` otherwise.\n *\n * @example\n * ```ts\n * // src/server.ts — a CSRF check that still works behind a proxy that rewrites Host\n * import { publicUrl } from '@rshono/core/server';\n * import { csrf } from 'hono/csrf';\n *\n * server.use(csrf({ origin: (origin, c) => origin === publicUrl(c).origin }));\n * ```\n *\n * @see {@link https://www.rshono.com/docs/configuration#proxy-headers | Docs — proxy headers}\n */\nexport function publicUrl(c: Context): URL {\n const url = new URL(c.req.url);\n if (!trustProxy) return url;\n\n const forwardedHost = firstForwardedValue(c.req.header('x-forwarded-host'));\n // Parsed, not assigned to `url.host`: that setter keeps the existing port when the new value has none.\n const forwarded = forwardedHost ? URL.parse(`http://${forwardedHost}`) : null;\n if (forwarded) {\n url.hostname = forwarded.hostname;\n url.port = forwarded.port;\n }\n\n // Only the two schemes a browser could have requested; anything else leaves the scheme alone.\n const forwardedProto = firstForwardedValue(c.req.header('x-forwarded-proto'));\n if (forwardedProto === 'http' || forwardedProto === 'https') url.protocol = forwardedProto;\n\n return url;\n}\n\n/**\n * The environment available to a request: Workers `Bindings` merged with process env vars. Values not\n * declared in `Bindings` are typed as `string | undefined`. See {@link RequestContext.env}.\n *\n * @see {@link https://hono.dev/docs/getting-started/cloudflare-workers#bindings | Hono — bindings}\n */\nexport type EnvVars<E extends Env> = E['Bindings'] & Record<string, string | undefined>;\n\n/**\n * Read-mostly wrapper around Hono's {@link Context}, for server components and server actions.\n *\n * Obtain one with {@link getRequestContext}, or take it off a page's `ctx` prop — the same object.\n * Never construct it yourself. One instance is reused for the whole request, so its lazy getters\n * ({@link RequestContext.url}, {@link RequestContext.env}) are computed at most once.\n *\n * The eight members that only throw — `redirect`, `notFound`, `json`, `text`, `html`, `body`, `status`,\n * `header` — are **permanent, and exist to throw**. Every one of them is a silent no-op when reached through\n * {@link RequestContext.hono} from a page, so a stub that names the thing that does work is the difference\n * between a message and a page that renders while quietly ignoring half of what it was asked for. They carry\n * `@deprecated` for the strike-through an editor draws with it, not because they are on the way out: nothing\n * will un-deprecate or remove them, and dropping them would leave `ctx.redirect('/x')` as\n * \"property does not exist\", which says what is wrong and not what to do.\n *\n * @typeParam E - The Hono {@link Env} describing this app's `Bindings` and `Variables`, so\n * {@link RequestContext.var} and {@link RequestContext.env} stay typed.\n *\n * @example\n * ```tsx\n * import { getRequestContext } from '@rshono/core/server';\n *\n * export default async function Whoami() {\n * const ctx = getRequestContext();\n * const session = ctx.cookies.get('session');\n * return <p>{ctx.url.pathname} — {session ?? 'anonymous'}</p>;\n * }\n * ```\n *\n * @see {@link https://www.rshono.com/docs/api#rshonocoreserver | Docs — `@rshono/core/server`}\n * @see {@link https://hono.dev/docs/api/context | Hono — Context}, reachable in full via {@link RequestContext.hono}\n */\nexport class RequestContext<E extends Env = Env> {\n #raw: Context<E>;\n #url?: URL;\n #env?: EnvVars<E>;\n #params?: Record<string, string>;\n\n /**\n * One instance is created per request and handed out by {@link getRequestContext} or the `ctx` page\n * prop. Application code never calls this.\n *\n * @internal\n */\n constructor(c: Context<E>) {\n this.#raw = c;\n }\n\n /**\n * The underlying Hono {@link Context} — the escape hatch for what this wrapper does not expose, such\n * as `executionCtx.waitUntil()` on Workers.\n *\n * Its response builders (`redirect`, `json`, `body`, `status`, …) still do nothing from inside a\n * page: reaching them through here bypasses the errors the stubs on this class throw, it does not\n * make them work.\n *\n * @example\n * ```ts\n * getRequestContext().hono.executionCtx.waitUntil(logAsync()); // Workers\n * ```\n *\n * @see {@link https://hono.dev/docs/api/context | Hono — Context}\n */\n // Every member here is a getter or method so that none is *own enumerable*: React's \"you cannot pass\n // this to a client component\" diagnostic walks `Object.keys` recursively with no cycle guard, and the\n // Hono context graph reaches the socket through `req.raw`. `cookies` is the one own property, and it\n // is a shallow object of four functions.\n get hono(): Context<E> {\n return this.#raw;\n }\n\n /**\n * The parsed request — method, headers, path params, query and the body readers. Hono's\n * {@link Context.req}, unwrapped, so `ctx.req.header('authorization')` rather than\n * `ctx.hono.req.header(…)`.\n *\n * Reads only; setting a *response* header is {@link RequestContext.setHeader}, deliberately spelled\n * differently.\n *\n * @example\n * ```ts\n * const ctx = getRequestContext();\n * ctx.req.method; // 'GET'\n * ctx.req.header('authorization'); // string | undefined\n * ctx.req.query('tab'); // string | undefined\n * ```\n *\n * @see {@link https://hono.dev/docs/api/request | Hono — HonoRequest}\n */\n get req(): Context<E>['req'] {\n return this.#raw.req;\n }\n\n /**\n * Matched route params for this request, e.g. `{ id: '42' }` for `/profile/:id`. Empty when no route\n * matched.\n *\n * A page is handed the same record as its `params` prop, typed key-by-key from its route path, and\n * that is the better read where it exists. This is for everywhere else — a nested server component,\n * or a `'use server'` action.\n */\n get params(): Record<string, string> {\n return (this.#params ??= readParams(this.#raw as Context));\n }\n\n /**\n * The browser-facing request URL. Parsed once and cached, so every read within a request returns the\n * same instance — treat it as read-only.\n *\n * `X-Forwarded-Host` / `-Proto` are honoured only when `trustProxy` is enabled in\n * `rshono.config.ts`, since any client can send them.\n *\n * @example `const tab = getRequestContext().url.searchParams.get('tab');`\n *\n * @see {@link https://www.rshono.com/docs/configuration#proxy-headers | Docs — proxy headers}\n */\n get url(): URL {\n return (this.#url ??= publicUrl(this.#raw as Context));\n }\n\n /**\n * Typed variables set by middleware via `c.set('user', …)`, read here as `ctx.var.user`. Type them by\n * parameterising this class's {@link Env}.\n *\n * @example\n * ```ts\n * type AppEnv = { Variables: { user: { id: string } } };\n * const { user } = getRequestContext<AppEnv>().var; // typed, set by your middleware\n * ```\n *\n * @see {@link https://hono.dev/docs/api/context#var | Hono — c.var}\n * @see {@link https://www.rshono.com/docs/hono#typing-the-context | Docs — typing the context}\n */\n get var(): Readonly<E['Variables']> {\n return this.#raw.var;\n }\n\n /**\n * Environment for the request: process env vars merged with runtime bindings, which win on conflict.\n * Computed once and cached.\n *\n * @example `const key = getRequestContext().env.STRIPE_SECRET_KEY;`\n *\n * @see {@link https://hono.dev/docs/api/context#env | Hono — c.env}\n * @see {@link https://www.rshono.com/docs/configuration#environment-and-secrets | Docs — environment and secrets}\n */\n get env(): EnvVars<E> {\n if (this.#env) return this.#env;\n const bindings = this.#raw.env as Record<string, unknown> | undefined;\n return (this.#env = (bindings ? { ...processEnv(), ...bindings } : processEnv()) as EnvVars<E>);\n }\n\n /**\n * Read and write request/response cookies.\n *\n * @example\n * ```ts\n * const ctx = getRequestContext();\n * ctx.cookies.get('session'); // string | undefined\n * ctx.cookies.set('session', id, { httpOnly: true, sameSite: 'Lax', path: '/' });\n * ctx.cookies.delete('session', { path: '/' });\n * ```\n *\n * @see {@link https://hono.dev/docs/helpers/cookie | Hono — cookie helper}, which this wraps\n */\n cookies = {\n /** Reads a single cookie by name, or `undefined` if absent. Safe anywhere, a page included. */\n get: (name: string): string | undefined => getCookie(this.#raw, name),\n /** Reads every cookie as a `{ name: value }` record. Safe anywhere, a page included. */\n all: (): Record<string, string> => getCookie(this.#raw),\n /**\n * Sets a cookie on the response. See Hono's {@link CookieOptions} for `path`, `httpOnly`, `maxAge`\n * and the rest.\n *\n * **Throws inside a page render** — a `Set-Cookie` is a special case of\n * {@link RequestContext.setHeader}. Set cookies from a `'use server'` action, or with Hono's\n * `setCookie(c, …)` in middleware and endpoint routes.\n *\n * @throws If called while a page is rendering, where it could not reach the browser reliably.\n *\n * @see {@link https://hono.dev/docs/helpers/cookie#options | Hono — cookie options}\n */\n set: (name: string, value: string, options?: CookieOptions): void => {\n this.#assertWritable('ctx.cookies.set()');\n setCookie(this.#raw, name, value, options);\n },\n /**\n * Clears a cookie. Pass the same `path`/`domain` it was set with so the browser matches it.\n * Throws inside a page render, exactly as `set` does.\n *\n * @throws If called while a page is rendering.\n */\n delete: (name: string, options?: CookieOptions): void => {\n this.#assertWritable('ctx.cookies.delete()');\n deleteCookie(this.#raw, name, options);\n },\n };\n\n #assertWritable(call: string): void {\n if (rendering.has(this.#raw as Context)) tooLateToWrite(call);\n }\n\n /**\n * Sets a header on the response — from a `'use server'` action, which is the one place a request\n * context exists *and* the response is still open.\n *\n * From inside a page it throws: a page streams, so its response head is already committed by then,\n * and the write would land on a full page load but vanish on a soft navigation.\n *\n * Middleware and `{ type: 'endpoint' }` routes are handed Hono's `c` directly and use `c.header(…)`.\n * That is also where a header belonging to the *page* goes — `Cache-Control`, `X-Robots-Tag` — since\n * middleware runs before the render.\n *\n * @param name - Header name, case-insensitive.\n * @param value - Header value.\n * @param options - `{ append: true }` to add another value rather than replace.\n * @throws If called while a page is rendering, where it could not reach the browser reliably.\n *\n * @example\n * ```ts\n * 'use server';\n * export async function logout() {\n * const ctx = getRequestContext();\n * ctx.cookies.delete('session', { path: '/' });\n * ctx.setHeader('clear-site-data', '\"cache\", \"storage\"');\n * redirect('/');\n * }\n * ```\n */\n setHeader(name: string, value: string, options?: { append?: boolean }): void {\n this.#assertWritable('ctx.setHeader()');\n this.#raw.header(name, value, options);\n }\n\n // Hono's response builders, restated as errors naming what to use instead — through `ctx.hono` every\n // one of them is a silent no-op from a page. Permanent, deliberately: see the class doc. `@deprecated`\n // strikes them through in autocomplete; the unread `..._args` is so `ctx.redirect('/x')` reaches the\n // thrown message rather than an arity error.\n\n /** @deprecated Not available on a page's context — use `redirect()` from `@rshono/core/server`. */\n redirect(..._args: unknown[]): never {\n return notOnContext(\n 'redirect(location, status?)',\n \"Use `redirect()` from '@rshono/core/server', which throws a signal the framework turns into a real redirect.\",\n );\n }\n\n /** @deprecated Not available on a page's context — use `notFound()` from `@rshono/core/server`. */\n notFound(..._args: unknown[]): never {\n return notOnContext('notFound()', \"Use `notFound()` from '@rshono/core/server', which aborts the render and shows the app's not-found page.\");\n }\n\n /** @deprecated A page renders JSX. For a JSON response, use an `{ type: 'endpoint' }` route. */\n json(..._args: unknown[]): never {\n return notOnContext('json(object)', \"For a JSON response use an { type: 'endpoint' } route; to read the request body use `ctx.req.json()`.\");\n }\n\n /** @deprecated A page renders JSX. For a text response, use an `{ type: 'endpoint' }` route. */\n text(..._args: unknown[]): never {\n return notOnContext('text(string)', \"For a text response use an { type: 'endpoint' } route; to read the request body use `ctx.req.text()`.\");\n }\n\n /** @deprecated A page renders JSX, which the framework turns into HTML for you. */\n html(..._args: unknown[]): never {\n return notOnContext('html(string)', \"A page's JSX is already its HTML; for a hand-built HTML response use an { type: 'endpoint' } route.\");\n }\n\n /** @deprecated Not available on a page's context. To read the request body, use `ctx.req`. */\n body(..._args: unknown[]): never {\n return notOnContext(\n 'body(data, …)',\n \"To read the *request* body use `ctx.req.json()` / `ctx.req.text()` / `ctx.req.formData()`; to build a response, use an { type: 'endpoint' } route.\",\n );\n }\n\n /** @deprecated A page's status is set by the framework — use `notFound()`, or an endpoint route. */\n status(..._args: unknown[]): never {\n return notOnContext(\n 'status(code)',\n \"A page's status is the framework's: 200, 404 via `notFound()`, 500 when it throws. For any other code use an { type: 'endpoint' } route.\",\n );\n }\n\n /** @deprecated Renamed — use `ctx.setHeader(name, value)`, which is valid from a `'use server'` action. */\n header(..._args: unknown[]): never {\n return notOnContext(\n 'header(name, value)',\n \"Use `ctx.setHeader(name, value)` from a 'use server' action, or `c.header(…)` in middleware — a page renders too late to set one.\",\n );\n }\n}\n\n/**\n * The {@link RequestContext} for the current request — URL, cookies, params, env and middleware\n * variables — read from a server component or a server action. Memoised per request, so repeated calls\n * return the same instance.\n *\n * A page is handed that same object as its `ctx` prop, so this import is for everywhere else: a nested\n * server component, or a `'use server'` action module.\n *\n * @typeParam E - The app's Hono {@link Env}, to type {@link RequestContext.var} and {@link RequestContext.env}.\n * @throws If called at module load, where there is no ambient context to resolve.\n * @throws If called while prerendering a `render: 'static'` route, which has no\n * per-request context at build time — mark the route `render: 'dynamic'` instead.\n *\n * @example\n * ```ts\n * 'use server';\n * import { getRequestContext, redirect } from '@rshono/core/server';\n *\n * export async function login(form: FormData) {\n * getRequestContext().cookies.set('session', String(form.get('email')), { httpOnly: true });\n * redirect('/dashboard');\n * }\n * ```\n *\n * @see {@link https://www.rshono.com/docs/api#rshonocoreserver | Docs — `@rshono/core/server`}\n */\nexport function getRequestContext<E extends Env = Env>(): RequestContext<E> {\n if (prerendering) {\n throw new Error(\n \"[rshono] getRequestContext() was called while prerendering a `render: 'static'` route. A static page \" +\n 'is rendered once at build time, so it has no per-request context to read (URL, cookies, ' +\n \"headers, env). Change this route to `render: 'dynamic'` so it renders per request, or remove \" +\n 'the getRequestContext() call.',\n );\n }\n const c = contextStorage.getStore();\n if (!c) {\n throw new Error(\n '[rshono] getRequestContext() was called outside a request. It only works inside a server component or a server action, not at module load.',\n );\n }\n let ctx = wrappers.get(c);\n if (!ctx) {\n ctx = new RequestContext(c);\n wrappers.set(c, ctx);\n }\n return ctx as unknown as RequestContext<E>;\n}\n\n/**\n * Redirects the request to `location`, by throwing a control signal the framework turns into an HTTP\n * redirect.\n *\n * Because it throws it never returns, so TypeScript narrows away everything after the call and there\n * is nothing to `return`. Don't wrap it in a `try/catch` that swallows the signal.\n *\n * @param location - Absolute path or URL to redirect to, e.g. `/dashboard`.\n * @param status - Redirect {@link RedirectStatus}; defaults to `303` (See Other), which is what makes\n * the browser follow up with a `GET` after a form action.\n *\n * @example\n * ```ts\n * const session = getRequestContext().cookies.get('session');\n * if (!session) redirect('/login');\n * // session is defined below this line\n * ```\n */\nexport function redirect(location: string, status: RedirectStatus = 303): never {\n throw new RedirectSignal(location, status);\n}\n\n/**\n * Aborts the current render with a 404, rendering the app's `notFound` page.\n *\n * Like {@link redirect} it throws a control signal and never returns, so TypeScript narrows away\n * everything after the call. Don't catch-and-swallow it.\n *\n * @example\n * ```tsx\n * export default async function Page({ params }: PageProps<'/users/:id'>) {\n * const user = await db.user.find(params.id);\n * if (!user) notFound();\n * return <Profile user={user} />; // user is non-null here\n * }\n * ```\n */\nexport function notFound(): never {\n throw new NotFoundSignal();\n}\n\n/**\n * Which stage of a request produced an error handed to a {@link ServerErrorHandler}.\n *\n * - `action` — a `'use server'` function threw. React sends the client an opaque marker with no\n * message in production, so this is the only place the real error is visible.\n * - `render` — a server component threw while the flight payload was being produced.\n * - `ssr` — SSR failed before the HTML shell could be sent, so the `error` page was unreachable too.\n * - `request` — anything else that reached the top-level handler, a thrown endpoint route included.\n */\nexport type ServerErrorSource = 'action' | 'render' | 'ssr' | 'request';\n\n/**\n * What an {@link ServerErrorHandler} is told about an error, beyond the error itself.\n *\n * @typeParam E - The app's Hono {@link Env}, to type {@link ServerErrorContext.hono}'s `var` and `env`.\n */\nexport interface ServerErrorContext<E extends Env = Env> {\n /** The stage that produced it — see {@link ServerErrorSource}. */\n source: ServerErrorSource;\n /** The request being served, for the URL, method and headers. */\n request: Request;\n /**\n * The Hono {@link Context} for this request — `hono.var` for whatever middleware put there, such as a\n * request id to correlate the report on, and `hono.env` for a platform that passes bindings.\n *\n * Handed over rather than left to {@link getRequestContext}, which a handler cannot reach: an error with\n * `source: 'request'` is reported from the top-level handler, which runs outside the ambient context.\n */\n hono: Context<E>;\n /**\n * Holds the invocation open until `promise` settles, where the platform has something to ask.\n *\n * Reporting is what this hook exists for, and on a serverless platform a report started here is cut off\n * the moment the response ends unless something keeps the invocation alive. On Cloudflare Workers that is\n * `executionCtx.waitUntil`, which this calls. On the `node` and `vercel` targets there is nothing to hold\n * open — the process outlives the response — so it is a no-op and the report finishes on its own. On\n * `aws-lambda` it is a no-op as well, because `hono/aws-lambda`'s streaming handler exposes no execution\n * context to ask; a slow report there is best-effort, so prefer a tracker that batches over one that\n * round-trips per error.\n *\n * A rejection is logged rather than propagated: reporting can never fail a request.\n */\n waitUntil: (promise: Promise<unknown>) => void;\n}\n\n/** Handler registered with {@link onServerError}. Called for the side effect; its return value is ignored. */\nexport type ServerErrorHandler<E extends Env = Env> = (error: unknown, context: ServerErrorContext<E>) => void;\n\nlet errorHandler: ServerErrorHandler | undefined;\n\n/**\n * The platform's \"keep this invocation alive\" hook, or a no-op where there is none.\n *\n * `c.executionCtx` *throws* rather than answering `undefined` where a platform has no execution context, so\n * a handler that reached for it itself would have its report swallowed by the guard in\n * {@link reportServerError} — on exactly the platforms where nothing needed holding open.\n */\nfunction keepAlive(c: Context, promise: Promise<unknown>): void {\n // Caught here rather than left to the platform: under `--unhandled-rejections=strict` a rejected report\n // would end the process, and a failed report must never be worse than no report.\n const settled = Promise.resolve(promise).catch((error) => {\n console.error('[rshono] a promise passed to the onServerError waitUntil rejected:', error);\n });\n try {\n c.executionCtx.waitUntil(settled);\n } catch {\n // No execution context: nothing here cuts the work off, so there is nothing to hold open.\n }\n}\n\n/**\n * Errors already forwarded, so one fault is reported once however many stages it crosses.\n *\n * A thrown server action is reported where it is known to be an action and then re-thrown, which lands it in\n * the top-level handler as well — and a funnel that counts the same error twice, under two different\n * `source`s, is worse than one that only ever names the outer stage.\n */\nconst alreadyReported = new WeakSet<object>();\n\n/**\n * Registers a handler for every error the framework catches, so they can reach an error tracker\n * (Sentry, Datadog, a log pipeline) instead of only `stderr`.\n *\n * Call it once, at the top level of `src/server.ts`, which is imported as the server starts.\n * Registering again replaces the previous handler. Errors still go to `stderr` either way, and a\n * handler that throws is caught and logged — reporting can never fail a request.\n *\n * @typeParam E - The app's Hono {@link Env}, to type the `hono` context the handler is given.\n *\n * @example\n * ```ts\n * // src/server.ts\n * import * as Sentry from '@sentry/node';\n * import { onServerError } from '@rshono/core/server';\n *\n * onServerError((error, { source, request, hono, waitUntil }) => {\n * // `waitUntil` so a serverless invocation is not frozen before the report is sent.\n * waitUntil(\n * Sentry.captureException(error, {\n * tags: { source, requestId: hono.var.requestId },\n * extra: { url: request.url },\n * }),\n * );\n * });\n * ```\n *\n * @see {@link https://www.rshono.com/docs/hono#error-reporting | Docs — error reporting}\n */\nexport function onServerError<E extends Env = Env>(handler: ServerErrorHandler<E>): void {\n errorHandler = handler as unknown as ServerErrorHandler;\n}\n\n/**\n * Logs an error and forwards it to the registered {@link ServerErrorHandler} — the single funnel every\n * caught server-side error goes through.\n *\n * @internal\n */\nexport function reportServerError(error: unknown, info: { source: ServerErrorSource; hono: Context; message: string }): void {\n // The first stage to recognise it wins, since that is the one that knows what it was. A primitive throw\n // cannot be tracked and is reported wherever it is caught.\n if (typeof error === 'object' && error !== null) {\n if (alreadyReported.has(error)) return;\n alreadyReported.add(error);\n }\n console.error(info.message, error);\n if (!errorHandler) return;\n try {\n errorHandler(error, {\n source: info.source,\n request: info.hono.req.raw,\n // The handler was registered for the app's own `Env`, which `onServerError` erased to store it; this\n // puts the context back in the shape it was registered with. Same trade as {@link getRequestContext}.\n hono: info.hono as Context<Env>,\n waitUntil: (promise) => keepAlive(info.hono, promise),\n });\n } catch (handlerError) {\n console.error('[rshono] the onServerError handler threw:', handlerError);\n }\n}\n"]}
1
+ {"version":3,"file":"context.js","sourceRoot":"","sources":["../../src/runtime/context.ts"],"names":[],"mappings":"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","sourcesContent":["// `__RSHONO_CONFIG__` is a global const, and an `import` cannot bring one into scope — a path reference is\n// the only way to reach it, which is the whole reason that file is separate. See its header.\n// eslint-disable-next-line @typescript-eslint/triple-slash-reference\n/// <reference path=\"../types/rshono-config.d.ts\" />\n/**\n * The request context: {@link getRequestContext} and the {@link RequestContext} it returns, the\n * {@link redirect} / {@link notFound} control-flow helpers, and the {@link onServerError} reporting\n * funnel — plus the `@internal` plumbing that binds a request to the async context.\n *\n * The public half is re-exported by `runtime/server.ts`, which is what `@rshono/core/server` resolves\n * to; an app imports that.\n */\n\nimport type { Context, Env } from 'hono';\nimport { deleteCookie, getCookie, setCookie } from 'hono/cookie';\nimport type { CookieOptions } from 'hono/utils/cookie';\nimport { AsyncLocalStorage } from 'node:async_hooks';\nimport { NotFoundSignal, RedirectSignal } from './control.js';\n\n/**\n * HTTP status codes accepted by {@link redirect}.\n *\n * - `301` Moved Permanently, `308` Permanent Redirect — cacheable, permanent.\n * - `302` Found, `307` Temporary Redirect — temporary.\n * - `303` See Other — the default; forces a `GET` on the target, which is what you almost always want\n * after a form action (post/redirect/get).\n *\n * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status#redirection_messages | MDN — redirection status codes}\n */\nexport type RedirectStatus = 301 | 302 | 303 | 307 | 308;\n\nconst contextStorage = new AsyncLocalStorage<Context>();\n\n/** One {@link RequestContext} per Hono {@link Context}, so repeated `getRequestContext()` calls share its lazy getters. */\nconst wrappers = new WeakMap<Context, RequestContext>();\n\n// Keyed on the Hono context rather than held as a field, so marking a request never forces the lazily\n// built `RequestContext` wrapper into existence.\nconst rendering = new WeakSet<Context>();\n\n/**\n * Marks the request as having entered its page render, which is what makes\n * {@link RequestContext.setHeader} and `ctx.cookies.set()` start throwing.\n *\n * @internal\n */\nexport function beginPageRender(c: Context): void {\n rendering.add(c);\n}\n\n/** The shared refusal for a response write that arrived too late — the message names where it belongs instead. */\nfunction tooLateToWrite(call: string): never {\n throw new Error(\n `[rshono] ${call} was called while rendering a page, which is too late to affect the response. ` +\n 'A page streams, so its response head is already committed by the time the component runs — the ' +\n 'write would land on a full page load and be silently dropped on a soft navigation. Do it from a ' +\n \"'use server' action instead; or, in middleware and { type: 'endpoint' } routes — which are handed \" +\n \"Hono's `c` directly and run outside the request context — with `c.header(…)` / `setCookie(c, …)`.\",\n );\n}\n\n/** The shared refusal for a Hono `Context` member a page has no way to use. See the stubs on {@link RequestContext}. */\nfunction notOnContext(call: string, instead: string): never {\n throw new Error(\n `[rshono] ctx.${call} does not exist. A page returns JSX and the framework builds the response from it, ` +\n `so Hono's response builders have nothing to return to. ${instead}`,\n );\n}\n\n// Snapshotted rather than spread per request: enumerating `process.env` crosses into the host\n// environment (~20µs). Lazily, because `loadEnvFiles()` runs after this module is imported — so a\n// mutation after the first `ctx.env` read is not picked up.\nlet envSnapshot: Record<string, string | undefined> | undefined;\n\nfunction processEnv(): Record<string, string | undefined> {\n return (envSnapshot ??= typeof process !== 'undefined' && process.env ? { ...process.env } : {});\n}\n\n// Set by `build.ts` before it imports the app bundle, which inlines its own copy of this module — so\n// `process.env` is what crosses that boundary rather than a module-level flag.\nconst prerendering = typeof process !== 'undefined' && !!process.env?.RSHONO_PRERENDER;\n\n/**\n * Runs `fn` with `c` bound as the ambient request context, so {@link getRequestContext} resolves to it\n * anywhere in the call tree.\n *\n * @internal\n */\nexport function runWithContext<T>(c: Context, fn: () => T): T {\n return contextStorage.run(c, fn);\n}\n\n/**\n * The matched route params, or an empty object when there is no active match.\n *\n * @internal\n */\nexport function readParams(c: Context): Record<string, string> {\n try {\n return c.req.param();\n } catch {\n return {};\n }\n}\n\n/** A proxy chain appends to these headers, so the client-facing value is the first entry. */\nfunction firstForwardedValue(header: string | undefined): string | undefined {\n const first = header?.split(',')[0]?.trim();\n return first || undefined;\n}\n\n// Read through `typeof`: DefinePlugin inlines this into the server bundle, but the module is also the\n// public `@rshono/core/server` entry, which tooling can load without one. Absent means don't trust.\nconst trustProxy = typeof __RSHONO_CONFIG__ !== 'undefined' && __RSHONO_CONFIG__.trustProxy;\n\n// Read the same way, for the same reason. Absent means the platform passes no bindings — see the getter.\nconst envBindings = typeof __RSHONO_CONFIG__ !== 'undefined' && __RSHONO_CONFIG__.envBindings;\n\n/**\n * The browser-facing {@link URL} for a request, resolved from Hono's {@link Context} — a fresh\n * instance per call.\n *\n * `c.req.url` is the internal address the server was reached on, which is wrong behind a proxy;\n * `X-Forwarded-Host` / `-Proto` correct it, but only when `trustProxy` is enabled in\n * `rshono.config.ts` — they are client-supplied, so trusting them unconditionally would let anyone\n * dictate the origin of every absolute URL the app builds.\n *\n * This is the form for **middleware**, which is handed `c` and runs outside the request context — and\n * so the way to give Hono's own middleware the origin the browser actually used. In a server component\n * or action, prefer {@link RequestContext.url}, the same value cached per request.\n *\n * @param c - The Hono {@link Context} for the request.\n * @returns The browser-facing URL — proxy-corrected under `trustProxy`, `c.req.url` otherwise.\n *\n * @example\n * ```ts\n * // src/server.ts — a CSRF check that still works behind a proxy that rewrites Host\n * import { publicUrl } from '@rshono/core/server';\n * import { csrf } from 'hono/csrf';\n *\n * server.use(csrf({ origin: (origin, c) => origin === publicUrl(c).origin }));\n * ```\n *\n * @see {@link https://www.rshono.com/docs/configuration#proxy-headers | Docs — proxy headers}\n */\nexport function publicUrl(c: Context): URL {\n const url = new URL(c.req.url);\n if (!trustProxy) return url;\n\n const forwardedHost = firstForwardedValue(c.req.header('x-forwarded-host'));\n // Parsed, not assigned to `url.host`: that setter keeps the existing port when the new value has none.\n const forwarded = forwardedHost ? URL.parse(`http://${forwardedHost}`) : null;\n if (forwarded) {\n url.hostname = forwarded.hostname;\n url.port = forwarded.port;\n }\n\n // Only the two schemes a browser could have requested; anything else leaves the scheme alone.\n const forwardedProto = firstForwardedValue(c.req.header('x-forwarded-proto'));\n if (forwardedProto === 'http' || forwardedProto === 'https') url.protocol = forwardedProto;\n\n return url;\n}\n\n/**\n * The environment available to a request: Workers `Bindings` merged with process env vars. Values not\n * declared in `Bindings` are typed as `string | undefined`. See {@link RequestContext.env}.\n *\n * @see {@link https://hono.dev/docs/getting-started/cloudflare-workers#bindings | Hono — bindings}\n */\nexport type EnvVars<E extends Env> = E['Bindings'] & Record<string, string | undefined>;\n\n/**\n * Read-mostly wrapper around Hono's {@link Context}, for server components and server actions.\n *\n * Obtain one with {@link getRequestContext}, or take it off a page's `ctx` prop — the same object.\n * Never construct it yourself. One instance is reused for the whole request, so its lazy getters\n * ({@link RequestContext.url}, {@link RequestContext.env}) are computed at most once.\n *\n * The eight members that only throw — `redirect`, `notFound`, `json`, `text`, `html`, `body`, `status`,\n * `header` — are **permanent, and exist to throw**. Every one of them is a silent no-op when reached through\n * {@link RequestContext.hono} from a page, so a stub that names the thing that does work is the difference\n * between a message and a page that renders while quietly ignoring half of what it was asked for. They carry\n * `@deprecated` for the strike-through an editor draws with it, not because they are on the way out: nothing\n * will un-deprecate or remove them, and dropping them would leave `ctx.redirect('/x')` as\n * \"property does not exist\", which says what is wrong and not what to do.\n *\n * @typeParam E - The Hono {@link Env} describing this app's `Bindings` and `Variables`, so\n * {@link RequestContext.var} and {@link RequestContext.env} stay typed.\n *\n * @example\n * ```tsx\n * import { getRequestContext } from '@rshono/core/server';\n *\n * export default async function Whoami() {\n * const ctx = getRequestContext();\n * const session = ctx.cookies.get('session');\n * return <p>{ctx.url.pathname} — {session ?? 'anonymous'}</p>;\n * }\n * ```\n *\n * @see {@link https://www.rshono.com/docs/api#rshonocoreserver | Docs — `@rshono/core/server`}\n * @see {@link https://hono.dev/docs/api/context | Hono — Context}, reachable in full via {@link RequestContext.hono}\n */\nexport class RequestContext<E extends Env = Env> {\n #raw: Context<E>;\n #url?: URL;\n #env?: EnvVars<E>;\n #params?: Record<string, string>;\n\n /**\n * One instance is created per request and handed out by {@link getRequestContext} or the `ctx` page\n * prop. Application code never calls this.\n *\n * @internal\n */\n constructor(c: Context<E>) {\n this.#raw = c;\n }\n\n /**\n * The underlying Hono {@link Context} — the escape hatch for what this wrapper does not expose, such\n * as `executionCtx.waitUntil()` on Workers.\n *\n * Its response builders (`redirect`, `json`, `body`, `status`, …) still do nothing from inside a\n * page: reaching them through here bypasses the errors the stubs on this class throw, it does not\n * make them work.\n *\n * @example\n * ```ts\n * getRequestContext().hono.executionCtx.waitUntil(logAsync()); // Workers\n * ```\n *\n * @see {@link https://hono.dev/docs/api/context | Hono — Context}\n */\n // Every member here is a getter or method so that none is *own enumerable*: React's \"you cannot pass\n // this to a client component\" diagnostic walks `Object.keys` recursively with no cycle guard, and the\n // Hono context graph reaches the socket through `req.raw`. `cookies` is the one own property, and it\n // is a shallow object of four functions.\n get hono(): Context<E> {\n return this.#raw;\n }\n\n /**\n * The parsed request — method, headers, path params, query and the body readers. Hono's\n * {@link Context.req}, unwrapped, so `ctx.req.header('authorization')` rather than\n * `ctx.hono.req.header(…)`.\n *\n * Reads only; setting a *response* header is {@link RequestContext.setHeader}, deliberately spelled\n * differently.\n *\n * @example\n * ```ts\n * const ctx = getRequestContext();\n * ctx.req.method; // 'GET'\n * ctx.req.header('authorization'); // string | undefined\n * ctx.req.query('tab'); // string | undefined\n * ```\n *\n * @see {@link https://hono.dev/docs/api/request | Hono — HonoRequest}\n */\n get req(): Context<E>['req'] {\n return this.#raw.req;\n }\n\n /**\n * Matched route params for this request, e.g. `{ id: '42' }` for `/profile/:id`. Empty when no route\n * matched.\n *\n * A page is handed the same record as its `params` prop, typed key-by-key from its route path, and\n * that is the better read where it exists. This is for everywhere else — a nested server component,\n * or a `'use server'` action.\n */\n get params(): Record<string, string> {\n return (this.#params ??= readParams(this.#raw as Context));\n }\n\n /**\n * The browser-facing request URL. Parsed once and cached, so every read within a request returns the\n * same instance — treat it as read-only.\n *\n * `X-Forwarded-Host` / `-Proto` are honoured only when `trustProxy` is enabled in\n * `rshono.config.ts`, since any client can send them.\n *\n * @example `const tab = getRequestContext().url.searchParams.get('tab');`\n *\n * @see {@link https://www.rshono.com/docs/configuration#proxy-headers | Docs — proxy headers}\n */\n get url(): URL {\n return (this.#url ??= publicUrl(this.#raw as Context));\n }\n\n /**\n * Typed variables set by middleware via `c.set('user', …)`, read here as `ctx.var.user`. Type them by\n * parameterising this class's {@link Env}.\n *\n * @example\n * ```ts\n * type AppEnv = { Variables: { user: { id: string } } };\n * const { user } = getRequestContext<AppEnv>().var; // typed, set by your middleware\n * ```\n *\n * @see {@link https://hono.dev/docs/api/context#var | Hono — c.var}\n * @see {@link https://www.rshono.com/docs/hono#typing-the-context | Docs — typing the context}\n */\n get var(): Readonly<E['Variables']> {\n return this.#raw.var;\n }\n\n /**\n * Environment for the request: process env vars, merged on a bindings platform with the bindings, which\n * win on conflict. Computed once and cached.\n *\n * Bindings are merged **only where the platform supplies them** — `deploy: 'cloudflare'`, today. Hono's\n * `c.env` is whatever the host passed as the second argument to `app.fetch`, and off Workers that is the\n * adapter's own private state: `{ incoming, outgoing }` on Node and Vercel, the entire invocation —\n * headers, cookies, `authorization` — on Lambda. Merging it would make `ctx.env` uncloneable on one and\n * a disclosure vector on the other, both behind names this type declares `string | undefined`. Reach for\n * {@link RequestContext.hono}`.env` if you really do want the adapter's argument.\n *\n * @example `const key = getRequestContext().env.STRIPE_SECRET_KEY;`\n *\n * @see {@link https://hono.dev/docs/api/context#env | Hono — c.env}\n * @see {@link https://www.rshono.com/docs/configuration#environment-and-secrets | Docs — environment and secrets}\n */\n get env(): EnvVars<E> {\n if (this.#env) return this.#env;\n const bindings = envBindings ? (this.#raw.env as Record<string, unknown> | undefined) : undefined;\n return (this.#env = (bindings ? { ...processEnv(), ...bindings } : processEnv()) as EnvVars<E>);\n }\n\n /**\n * Read and write request/response cookies.\n *\n * @example\n * ```ts\n * const ctx = getRequestContext();\n * ctx.cookies.get('session'); // string | undefined\n * ctx.cookies.set('session', id, { httpOnly: true, sameSite: 'Lax', path: '/' });\n * ctx.cookies.delete('session', { path: '/' });\n * ```\n *\n * @see {@link https://hono.dev/docs/helpers/cookie | Hono — cookie helper}, which this wraps\n */\n cookies = {\n /** Reads a single cookie by name, or `undefined` if absent. Safe anywhere, a page included. */\n get: (name: string): string | undefined => getCookie(this.#raw, name),\n /** Reads every cookie as a `{ name: value }` record. Safe anywhere, a page included. */\n all: (): Record<string, string> => getCookie(this.#raw),\n /**\n * Sets a cookie on the response. See Hono's {@link CookieOptions} for `path`, `httpOnly`, `maxAge`\n * and the rest.\n *\n * **Throws inside a page render** — a `Set-Cookie` is a special case of\n * {@link RequestContext.setHeader}. Set cookies from a `'use server'` action, or with Hono's\n * `setCookie(c, …)` in middleware and endpoint routes.\n *\n * @throws If called while a page is rendering, where it could not reach the browser reliably.\n *\n * @see {@link https://hono.dev/docs/helpers/cookie#options | Hono — cookie options}\n */\n set: (name: string, value: string, options?: CookieOptions): void => {\n this.#assertWritable('ctx.cookies.set()');\n setCookie(this.#raw, name, value, options);\n },\n /**\n * Clears a cookie. Pass the same `path`/`domain` it was set with so the browser matches it.\n * Throws inside a page render, exactly as `set` does.\n *\n * @throws If called while a page is rendering.\n */\n delete: (name: string, options?: CookieOptions): void => {\n this.#assertWritable('ctx.cookies.delete()');\n deleteCookie(this.#raw, name, options);\n },\n };\n\n #assertWritable(call: string): void {\n if (rendering.has(this.#raw as Context)) tooLateToWrite(call);\n }\n\n /**\n * Sets a header on the response — from a `'use server'` action, which is the one place a request\n * context exists *and* the response is still open.\n *\n * From inside a page it throws: a page streams, so its response head is already committed by then,\n * and the write would land on a full page load but vanish on a soft navigation.\n *\n * Middleware and `{ type: 'endpoint' }` routes are handed Hono's `c` directly and use `c.header(…)`.\n * That is also where a header belonging to the *page* goes — `Cache-Control`, `X-Robots-Tag` — since\n * middleware runs before the render.\n *\n * @param name - Header name, case-insensitive.\n * @param value - Header value.\n * @param options - `{ append: true }` to add another value rather than replace.\n * @throws If called while a page is rendering, where it could not reach the browser reliably.\n *\n * @example\n * ```ts\n * 'use server';\n * export async function logout() {\n * const ctx = getRequestContext();\n * ctx.cookies.delete('session', { path: '/' });\n * ctx.setHeader('clear-site-data', '\"cache\", \"storage\"');\n * redirect('/');\n * }\n * ```\n */\n setHeader(name: string, value: string, options?: { append?: boolean }): void {\n this.#assertWritable('ctx.setHeader()');\n this.#raw.header(name, value, options);\n }\n\n // Hono's response builders, restated as errors naming what to use instead — through `ctx.hono` every\n // one of them is a silent no-op from a page. Permanent, deliberately: see the class doc. `@deprecated`\n // strikes them through in autocomplete; the unread `..._args` is so `ctx.redirect('/x')` reaches the\n // thrown message rather than an arity error.\n\n /** @deprecated Not available on a page's context — use `redirect()` from `@rshono/core/server`. */\n redirect(..._args: unknown[]): never {\n return notOnContext(\n 'redirect(location, status?)',\n \"Use `redirect()` from '@rshono/core/server', which throws a signal the framework turns into a real redirect.\",\n );\n }\n\n /** @deprecated Not available on a page's context — use `notFound()` from `@rshono/core/server`. */\n notFound(..._args: unknown[]): never {\n return notOnContext('notFound()', \"Use `notFound()` from '@rshono/core/server', which aborts the render and shows the app's not-found page.\");\n }\n\n /** @deprecated A page renders JSX. For a JSON response, use an `{ type: 'endpoint' }` route. */\n json(..._args: unknown[]): never {\n return notOnContext('json(object)', \"For a JSON response use an { type: 'endpoint' } route; to read the request body use `ctx.req.json()`.\");\n }\n\n /** @deprecated A page renders JSX. For a text response, use an `{ type: 'endpoint' }` route. */\n text(..._args: unknown[]): never {\n return notOnContext('text(string)', \"For a text response use an { type: 'endpoint' } route; to read the request body use `ctx.req.text()`.\");\n }\n\n /** @deprecated A page renders JSX, which the framework turns into HTML for you. */\n html(..._args: unknown[]): never {\n return notOnContext('html(string)', \"A page's JSX is already its HTML; for a hand-built HTML response use an { type: 'endpoint' } route.\");\n }\n\n /** @deprecated Not available on a page's context. To read the request body, use `ctx.req`. */\n body(..._args: unknown[]): never {\n return notOnContext(\n 'body(data, …)',\n \"To read the *request* body use `ctx.req.json()` / `ctx.req.text()` / `ctx.req.formData()`; to build a response, use an { type: 'endpoint' } route.\",\n );\n }\n\n /** @deprecated A page's status is set by the framework — use `notFound()`, or an endpoint route. */\n status(..._args: unknown[]): never {\n return notOnContext(\n 'status(code)',\n \"A page's status is the framework's: 200, 404 via `notFound()`, 500 when it throws. For any other code use an { type: 'endpoint' } route.\",\n );\n }\n\n /** @deprecated Renamed — use `ctx.setHeader(name, value)`, which is valid from a `'use server'` action. */\n header(..._args: unknown[]): never {\n return notOnContext(\n 'header(name, value)',\n \"Use `ctx.setHeader(name, value)` from a 'use server' action, or `c.header(…)` in middleware — a page renders too late to set one.\",\n );\n }\n}\n\n/**\n * The {@link RequestContext} for the current request — URL, cookies, params, env and middleware\n * variables — read from a server component or a server action. Memoised per request, so repeated calls\n * return the same instance.\n *\n * A page is handed that same object as its `ctx` prop, so this import is for everywhere else: a nested\n * server component, or a `'use server'` action module.\n *\n * @typeParam E - The app's Hono {@link Env}, to type {@link RequestContext.var} and {@link RequestContext.env}.\n * @throws If called at module load, where there is no ambient context to resolve.\n * @throws If called while prerendering a `render: 'static'` route, which has no\n * per-request context at build time — mark the route `render: 'dynamic'` instead.\n *\n * @example\n * ```ts\n * 'use server';\n * import { getRequestContext, redirect } from '@rshono/core/server';\n *\n * export async function login(form: FormData) {\n * getRequestContext().cookies.set('session', String(form.get('email')), { httpOnly: true });\n * redirect('/dashboard');\n * }\n * ```\n *\n * @see {@link https://www.rshono.com/docs/api#rshonocoreserver | Docs — `@rshono/core/server`}\n */\nexport function getRequestContext<E extends Env = Env>(): RequestContext<E> {\n if (prerendering) {\n throw new Error(\n \"[rshono] getRequestContext() was called while prerendering a `render: 'static'` route. A static page \" +\n 'is rendered once at build time, so it has no per-request context to read (URL, cookies, ' +\n \"headers, env). Change this route to `render: 'dynamic'` so it renders per request, or remove \" +\n 'the getRequestContext() call.',\n );\n }\n const c = contextStorage.getStore();\n if (!c) {\n throw new Error(\n '[rshono] getRequestContext() was called outside a request. It only works inside a server component or a server action, not at module load.',\n );\n }\n let ctx = wrappers.get(c);\n if (!ctx) {\n ctx = new RequestContext(c);\n wrappers.set(c, ctx);\n }\n return ctx as unknown as RequestContext<E>;\n}\n\n/**\n * Redirects the request to `location`, by throwing a control signal the framework turns into an HTTP\n * redirect.\n *\n * Because it throws it never returns, so TypeScript narrows away everything after the call and there\n * is nothing to `return`. Don't wrap it in a `try/catch` that swallows the signal.\n *\n * @param location - Absolute path or URL to redirect to, e.g. `/dashboard`.\n * @param status - Redirect {@link RedirectStatus}; defaults to `303` (See Other), which is what makes\n * the browser follow up with a `GET` after a form action.\n *\n * @example\n * ```ts\n * const session = getRequestContext().cookies.get('session');\n * if (!session) redirect('/login');\n * // session is defined below this line\n * ```\n */\nexport function redirect(location: string, status: RedirectStatus = 303): never {\n throw new RedirectSignal(location, status);\n}\n\n/**\n * Aborts the current render with a 404, rendering the app's `notFound` page.\n *\n * Like {@link redirect} it throws a control signal and never returns, so TypeScript narrows away\n * everything after the call. Don't catch-and-swallow it.\n *\n * @example\n * ```tsx\n * export default async function Page({ params }: PageProps<'/users/:id'>) {\n * const user = await db.user.find(params.id);\n * if (!user) notFound();\n * return <Profile user={user} />; // user is non-null here\n * }\n * ```\n */\nexport function notFound(): never {\n throw new NotFoundSignal();\n}\n\n/**\n * Which stage of a request produced an error handed to a {@link ServerErrorHandler}.\n *\n * - `action` — a `'use server'` function threw. React sends the client an opaque marker with no\n * message in production, so this is the only place the real error is visible.\n * - `render` — a server component threw while the flight payload was being produced.\n * - `ssr` — SSR failed before the HTML shell could be sent, so the `error` page was unreachable too.\n * - `request` — anything else that reached the top-level handler, a thrown endpoint route included.\n */\nexport type ServerErrorSource = 'action' | 'render' | 'ssr' | 'request';\n\n/**\n * What an {@link ServerErrorHandler} is told about an error, beyond the error itself.\n *\n * @typeParam E - The app's Hono {@link Env}, to type {@link ServerErrorContext.hono}'s `var` and `env`.\n */\nexport interface ServerErrorContext<E extends Env = Env> {\n /** The stage that produced it — see {@link ServerErrorSource}. */\n source: ServerErrorSource;\n /** The request being served, for the URL, method and headers. */\n request: Request;\n /**\n * The Hono {@link Context} for this request — `hono.var` for whatever middleware put there, such as a\n * request id to correlate the report on, and `hono.env` for a platform that passes bindings.\n *\n * Handed over rather than left to {@link getRequestContext}, which a handler cannot reach: an error with\n * `source: 'request'` is reported from the top-level handler, which runs outside the ambient context.\n */\n hono: Context<E>;\n /**\n * Holds the invocation open until `promise` settles, where the platform has something to ask.\n *\n * Reporting is what this hook exists for, and on a serverless platform a report started here is cut off\n * the moment the response ends unless something keeps the invocation alive. On Cloudflare Workers that is\n * `executionCtx.waitUntil`, which this calls. On the `node` and `vercel` targets there is nothing to hold\n * open — the process outlives the response — so it is a no-op and the report finishes on its own. On\n * `aws-lambda` it is a no-op as well, because `hono/aws-lambda`'s streaming handler exposes no execution\n * context to ask; a slow report there is best-effort, so prefer a tracker that batches over one that\n * round-trips per error.\n *\n * A rejection is logged rather than propagated: reporting can never fail a request.\n */\n waitUntil: (promise: Promise<unknown>) => void;\n}\n\n/** Handler registered with {@link onServerError}. Called for the side effect; its return value is ignored. */\nexport type ServerErrorHandler<E extends Env = Env> = (error: unknown, context: ServerErrorContext<E>) => void;\n\nlet errorHandler: ServerErrorHandler | undefined;\n\n/**\n * The platform's \"keep this invocation alive\" hook, or a no-op where there is none.\n *\n * `c.executionCtx` *throws* rather than answering `undefined` where a platform has no execution context, so\n * a handler that reached for it itself would have its report swallowed by the guard in\n * {@link reportServerError} — on exactly the platforms where nothing needed holding open.\n */\nfunction keepAlive(c: Context, promise: Promise<unknown>): void {\n // Caught here rather than left to the platform: under `--unhandled-rejections=strict` a rejected report\n // would end the process, and a failed report must never be worse than no report.\n const settled = Promise.resolve(promise).catch((error) => {\n console.error('[rshono] a promise passed to the onServerError waitUntil rejected:', error);\n });\n try {\n c.executionCtx.waitUntil(settled);\n } catch {\n // No execution context: nothing here cuts the work off, so there is nothing to hold open.\n }\n}\n\n/**\n * Errors already forwarded, so one fault is reported once however many stages it crosses.\n *\n * A thrown server action is reported where it is known to be an action and then re-thrown, which lands it in\n * the top-level handler as well — and a funnel that counts the same error twice, under two different\n * `source`s, is worse than one that only ever names the outer stage.\n */\nconst alreadyReported = new WeakSet<object>();\n\n/**\n * Registers a handler for every error the framework catches, so they can reach an error tracker\n * (Sentry, Datadog, a log pipeline) instead of only `stderr`.\n *\n * Call it once, at the top level of `src/server.ts`, which is imported as the server starts.\n * Registering again replaces the previous handler. Errors still go to `stderr` either way, and a\n * handler that throws is caught and logged — reporting can never fail a request.\n *\n * @typeParam E - The app's Hono {@link Env}, to type the `hono` context the handler is given.\n *\n * @example\n * ```ts\n * // src/server.ts\n * import * as Sentry from '@sentry/node';\n * import { onServerError } from '@rshono/core/server';\n *\n * onServerError((error, { source, request, hono, waitUntil }) => {\n * // `waitUntil` so a serverless invocation is not frozen before the report is sent.\n * waitUntil(\n * Sentry.captureException(error, {\n * tags: { source, requestId: hono.var.requestId },\n * extra: { url: request.url },\n * }),\n * );\n * });\n * ```\n *\n * @see {@link https://www.rshono.com/docs/hono#error-reporting | Docs — error reporting}\n */\nexport function onServerError<E extends Env = Env>(handler: ServerErrorHandler<E>): void {\n errorHandler = handler as unknown as ServerErrorHandler;\n}\n\n/**\n * Logs an error and forwards it to the registered {@link ServerErrorHandler} — the single funnel every\n * caught server-side error goes through.\n *\n * @internal\n */\nexport function reportServerError(error: unknown, info: { source: ServerErrorSource; hono: Context; message: string }): void {\n // The first stage to recognise it wins, since that is the one that knows what it was. A primitive throw\n // cannot be tracked and is reported wherever it is caught.\n if (typeof error === 'object' && error !== null) {\n if (alreadyReported.has(error)) return;\n alreadyReported.add(error);\n }\n console.error(info.message, error);\n if (!errorHandler) return;\n try {\n errorHandler(error, {\n source: info.source,\n request: info.hono.req.raw,\n // The handler was registered for the app's own `Env`, which `onServerError` erased to store it; this\n // puts the context back in the shape it was registered with. Same trade as {@link getRequestContext}.\n hono: info.hono as Context<Env>,\n waitUntil: (promise) => keepAlive(info.hono, promise),\n });\n } catch (handlerError) {\n console.error('[rshono] the onServerError handler threw:', handlerError);\n }\n}\n"]}
@@ -49,14 +49,13 @@ function overlayHost() {
49
49
  return document.body;
50
50
  }
51
51
  /**
52
- * Paints the reason for an uncaught render error over the blank page it leaves behind — the full stack in
53
- * dev, a generic notice and a reload button in production.
52
+ * Paints a full-viewport panel over whatever is on screen, and returns the box for the caller to fill.
54
53
  *
55
- * DOM calls rather than React (the renderer is what just failed), and `textContent` rather than
56
- * `innerHTML` (an error message is untrusted input).
54
+ * DOM calls rather than React (one caller runs because the renderer just failed), and `textContent` rather
55
+ * than `innerHTML` (an error message is untrusted input). Queued on a macrotask: React's teardown runs after
56
+ * the callback that reaches here returns, and would remove a node appended inline.
57
57
  */
58
- function showFatal(error, componentStack) {
59
- // Queued: React's teardown runs after this callback returns and would remove a node appended inline.
58
+ function paintOverlay(fill) {
60
59
  setTimeout(() => {
61
60
  const host = overlayHost();
62
61
  host.querySelector('[data-rshono-fatal]')?.remove();
@@ -66,10 +65,24 @@ function showFatal(error, componentStack) {
66
65
  box.style.cssText =
67
66
  'position:fixed;inset:0;z-index:2147483647;overflow:auto;padding:1.5rem;background:#18181b;color:#f4f4f5;' +
68
67
  'font:14px/1.6 ui-monospace,SFMono-Regular,Menlo,monospace;text-align:left';
69
- const title = document.createElement('div');
70
- title.textContent = isDev ? 'Unhandled error' : 'Something went wrong';
71
- title.style.cssText = 'font-size:1.0625rem;font-weight:700;color:#f87171;margin:0 0 0.75rem';
72
- box.appendChild(title);
68
+ fill(box);
69
+ host.appendChild(box);
70
+ }, 0);
71
+ }
72
+ /** The overlay's heading. */
73
+ function overlayTitle(text) {
74
+ const title = document.createElement('div');
75
+ title.textContent = text;
76
+ title.style.cssText = 'font-size:1.0625rem;font-weight:700;color:#f87171;margin:0 0 0.75rem';
77
+ return title;
78
+ }
79
+ /**
80
+ * Paints the reason for an uncaught render error over the blank page it leaves behind — the full stack in
81
+ * dev, a generic notice and a reload button in production.
82
+ */
83
+ function showFatal(error, componentStack) {
84
+ paintOverlay((box) => {
85
+ box.appendChild(overlayTitle(isDev ? 'Unhandled error' : 'Something went wrong'));
73
86
  if (isDev) {
74
87
  const detail = document.createElement('pre');
75
88
  detail.style.cssText = 'margin:0;white-space:pre-wrap;word-break:break-word';
@@ -88,10 +101,30 @@ function showFatal(error, componentStack) {
88
101
  reload.textContent = 'Reload page';
89
102
  reload.style.cssText =
90
103
  'margin-top:1.25rem;padding:0.5rem 1rem;font:inherit;color:#18181b;background:#f4f4f5;border:0;border-radius:4px;cursor:pointer';
91
- reload.addEventListener('click', () => window.location.reload());
104
+ reload.addEventListener('click', () => loadOutsideRouter(() => window.location.reload()));
92
105
  box.appendChild(reload);
93
- host.appendChild(box);
94
- }, 0);
106
+ });
107
+ }
108
+ /**
109
+ * The end of the line for a `notFound()` that arrived too late to be a 404 and did not survive a reload.
110
+ *
111
+ * No reload button, unlike {@link showFatal}: the reload has already been spent, and the second identical
112
+ * response is what brought us here. "Page not found" is what the server was trying to say, so it is what the
113
+ * visitor is told; the reason it could not say it properly is a message for whoever wrote the page, and dev is
114
+ * where they are.
115
+ */
116
+ function showLateNotFound() {
117
+ paintOverlay((box) => {
118
+ box.appendChild(overlayTitle('Page not found'));
119
+ const message = document.createElement('p');
120
+ message.textContent = isDev
121
+ ? 'notFound() was raised from a boundary that resolved after the page shell had been sent, so the response ' +
122
+ 'could not be a 404 — and reloading rendered the same page again. Decide before the render starts ' +
123
+ 'streaming: in Hono middleware, or in the page component body above the boundary.'
124
+ : 'This page is not available.';
125
+ message.style.cssText = 'margin:0;color:#d4d4d8';
126
+ box.appendChild(message);
127
+ });
95
128
  }
96
129
  /** What every flight response is typed as. The charset and any other parameters follow it. */
97
130
  const FLIGHT_CONTENT_TYPE = 'text/x-component';
@@ -144,6 +177,27 @@ function settle(result) {
144
177
  void result.committed?.catch(ignore);
145
178
  void result.finished?.catch(ignore);
146
179
  }
180
+ /** Set by {@link loadOutsideRouter}, read and cleared by the `navigate` listener. */
181
+ let bypassRouter = false;
182
+ /**
183
+ * Performs a navigation the router below must **not** intercept, and returns having asked for it.
184
+ *
185
+ * `location.reload()` and `location.assign()` fire a `navigate` event like any other navigation, and
186
+ * `listenNavigation` intercepts a `reload` on purpose — that is what `router.refresh()` is. Every caller here
187
+ * is reaching for a *new document* precisely because the current one cannot be repaired: the React root a
188
+ * soft load would render into is the thing that just failed, or is about to be torn down. Intercepted, the
189
+ * escape hatch becomes a payload fetch that lands nowhere — which is how a late `notFound()` left the tab on
190
+ * its Suspense fallback with no second document ever arriving, and how a late `redirect()` moved the address
191
+ * bar to a page it then failed to render.
192
+ *
193
+ * One-shot: the listener clears the flag on the next event it sees. If that event never comes — a navigation
194
+ * the browser refuses — the cost is that the *next* navigation is a full load rather than a soft one, on a
195
+ * document that was on its way out anyway.
196
+ */
197
+ function loadOutsideRouter(navigate) {
198
+ bypassRouter = true;
199
+ navigate();
200
+ }
147
201
  // The imperative actions behind `useNavigation().router`. Each one only *asks*: the browser turns it into a
148
202
  // `navigate` event, which is where `listenNavigation` answers it — so a `router.push` and a link click reach
149
203
  // the same code by the same route, and inherit the same fetch, scroll and `pending` flag.
@@ -181,6 +235,51 @@ function refresh() {
181
235
  else
182
236
  window.location.reload();
183
237
  }
238
+ /** How long the recovery reload is given to replace this document before the panel is painted instead. */
239
+ const RELOAD_GRACE_MS = 2000;
240
+ /**
241
+ * Spends the one reload a late `notFound()` gets, or paints if it has already been spent for this URL.
242
+ *
243
+ * `redirect()` is terminal on the client — there is somewhere to navigate to — and `notFound()` is not: the
244
+ * response is already committed as a 200, so the only recovery left is asking for the page again and hoping
245
+ * the signal comes early enough this time to be a real 404. That works where the lateness was incidental, a
246
+ * boundary that happened to resolve after the shell on a slow request. Where it is structural — a page that
247
+ * always signals from a late boundary — the reload gets a byte-identical response and reloads again, and the
248
+ * tab spins until the visitor leaves. In production nothing is logged, because the warning that explains this
249
+ * is `isDev`-only.
250
+ *
251
+ * So it is bounded: one reload per URL per tab, then {@link showLateNotFound}. `sessionStorage` because the
252
+ * value has to outlive the document it is written in and must not outlive the tab, and keyed by URL so a
253
+ * second page's late signal still gets its own attempt.
254
+ */
255
+ function reloadOnceForLateNotFound() {
256
+ const key = `rshono:late-not-found:${documentUrl()}`;
257
+ let spent;
258
+ try {
259
+ spent = sessionStorage.getItem(key) !== null;
260
+ if (!spent)
261
+ sessionStorage.setItem(key, '1');
262
+ }
263
+ catch {
264
+ // Storage can throw outright where site data is blocked, and a page that cannot count its reloads has
265
+ // to pick a side. It picks the terminating one: a message on a page that might have recovered is a
266
+ // worse outcome than a reload loop only by a lot less.
267
+ spent = true;
268
+ }
269
+ if (spent) {
270
+ showLateNotFound();
271
+ return;
272
+ }
273
+ loadOutsideRouter(() => window.location.reload());
274
+ // The reload wins this race whenever it happens at all: the document goes away and takes the timer with
275
+ // it. What this covers is a reload that does not happen — swallowed by an interceptor, refused by the
276
+ // browser, held by a `beforeunload` — which used to leave the visitor on a Suspense fallback with nothing
277
+ // coming and nothing said. The panel is the honest answer in that case too.
278
+ setTimeout(() => {
279
+ if (!document.querySelector('[data-rshono-fatal]'))
280
+ showLateNotFound();
281
+ }, RELOAD_GRACE_MS);
282
+ }
184
283
  /**
185
284
  * Turns a control-signal digest — how `redirect()` / `notFound()` reach the browser — into a real
186
285
  * navigation. Returns false for anything else, so callers fall through to their own handling.
@@ -194,10 +293,10 @@ function handleControlDigest(error, { hard = false } = {}) {
194
293
  return false;
195
294
  const redirect = parseRedirectDigest(digest);
196
295
  if (!redirect) {
197
- window.location.reload();
296
+ reloadOnceForLateNotFound();
198
297
  }
199
298
  else if (hard) {
200
- window.location.assign(new URL(redirect.location, window.location.href).href);
299
+ loadOutsideRouter(() => window.location.assign(new URL(redirect.location, window.location.href).href));
201
300
  }
202
301
  else {
203
302
  push(redirect.location);
@@ -283,6 +382,12 @@ function listenNavigation() {
283
382
  if (!canSoftNavigate)
284
383
  return () => { };
285
384
  const onNavigate = (event) => {
385
+ // Cleared as it is consumed, whatever this event turns out to be: the flag names one navigation, and the
386
+ // one it named is the one that just arrived.
387
+ if (bypassRouter) {
388
+ bypassRouter = false;
389
+ return;
390
+ }
286
391
  if (!event.canIntercept || leaveToBrowser(event))
287
392
  return;
288
393
  // A push or a traversal lands on a new page, so the browser resets the scroll offset — or restores the
@@ -295,7 +400,7 @@ function listenNavigation() {
295
400
  focusReset: inPlace ? 'manual' : 'after-transition',
296
401
  // The URL commits before the handler runs, so a failure leaves the address bar describing a page the
297
402
  // document is not showing. A real load is the only way back to agreement.
298
- handler: () => loadPayload(event.destination.url, event.signal).catch(() => window.location.reload()),
403
+ handler: () => loadPayload(event.destination.url, event.signal).catch(() => loadOutsideRouter(() => window.location.reload())),
299
404
  });
300
405
  };
301
406
  navigation.addEventListener('navigate', onNavigate);
@@ -417,7 +522,7 @@ function initDevRefresh() {
417
522
  let targetHash;
418
523
  function reload(reason, error) {
419
524
  console.warn(`[rshono] ${reason} — reloading`, ...(error === undefined ? [] : [error]));
420
- window.location.reload();
525
+ loadOutsideRouter(() => window.location.reload());
421
526
  }
422
527
  async function applyClientUpdate() {
423
528
  const giveUp = await walkHotUpdates(hot, () => __webpack_hash__, () => targetHash);
@@ -430,7 +535,7 @@ function initDevRefresh() {
430
535
  targetHash = message.hash ?? targetHash;
431
536
  if (connectedOnce) {
432
537
  await applyClientUpdate();
433
- await loadPayload(window.location.href).catch(() => window.location.reload());
538
+ await loadPayload(window.location.href).catch(() => loadOutsideRouter(() => window.location.reload()));
434
539
  }
435
540
  connectedOnce = true;
436
541
  break;
@@ -440,7 +545,7 @@ function initDevRefresh() {
440
545
  break;
441
546
  case 'rsc-update':
442
547
  console.log('[rshono] server components updated');
443
- await loadPayload(window.location.href).catch(() => window.location.reload());
548
+ await loadPayload(window.location.href).catch(() => loadOutsideRouter(() => window.location.reload()));
444
549
  break;
445
550
  }
446
551
  }