@rshono/core 1.0.0-rc.0 → 1.0.0-rc.10

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 (143) hide show
  1. package/README.md +202 -159
  2. package/dist/builder/page-files.d.ts.map +1 -1
  3. package/dist/builder/page-files.js +7 -3
  4. package/dist/builder/page-files.js.map +1 -1
  5. package/dist/builder/public-env.d.ts +6 -0
  6. package/dist/builder/public-env.d.ts.map +1 -1
  7. package/dist/builder/public-env.js +6 -0
  8. package/dist/builder/public-env.js.map +1 -1
  9. package/dist/builder/rspack-config.d.ts +3 -3
  10. package/dist/builder/rspack-config.d.ts.map +1 -1
  11. package/dist/builder/rspack-config.js +62 -16
  12. package/dist/builder/rspack-config.js.map +1 -1
  13. package/dist/cli/build.d.ts +2 -2
  14. package/dist/cli/build.js.map +1 -1
  15. package/dist/cli/dev.d.ts +2 -2
  16. package/dist/cli/dev.d.ts.map +1 -1
  17. package/dist/cli/dev.js +63 -35
  18. package/dist/cli/dev.js.map +1 -1
  19. package/dist/cli/index.js +10 -10
  20. package/dist/cli/index.js.map +1 -1
  21. package/dist/config.d.ts +58 -70
  22. package/dist/config.d.ts.map +1 -1
  23. package/dist/config.js +17 -1
  24. package/dist/config.js.map +1 -1
  25. package/dist/deploy/cloudflare/build.js +1 -1
  26. package/dist/deploy/cloudflare/build.js.map +1 -1
  27. package/dist/deploy/cloudflare/runtime.d.ts.map +1 -1
  28. package/dist/deploy/cloudflare/runtime.js +7 -37
  29. package/dist/deploy/cloudflare/runtime.js.map +1 -1
  30. package/dist/deploy/contract.d.ts +22 -16
  31. package/dist/deploy/contract.d.ts.map +1 -1
  32. package/dist/deploy/contract.js.map +1 -1
  33. package/dist/deploy/filesystem.d.ts +1 -1
  34. package/dist/deploy/filesystem.d.ts.map +1 -1
  35. package/dist/deploy/filesystem.js +7 -10
  36. package/dist/deploy/filesystem.js.map +1 -1
  37. package/dist/deploy/node/runtime.d.ts +4 -0
  38. package/dist/deploy/node/runtime.d.ts.map +1 -1
  39. package/dist/deploy/node/runtime.js +24 -3
  40. package/dist/deploy/node/runtime.js.map +1 -1
  41. package/dist/deploy/presets.d.ts +3 -3
  42. package/dist/deploy/presets.d.ts.map +1 -1
  43. package/dist/deploy/presets.js +13 -30
  44. package/dist/deploy/presets.js.map +1 -1
  45. package/dist/deploy/vercel/runtime.d.ts.map +1 -1
  46. package/dist/deploy/vercel/runtime.js +0 -3
  47. package/dist/deploy/vercel/runtime.js.map +1 -1
  48. package/dist/index.d.ts +6 -9
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +8 -3
  51. package/dist/index.js.map +1 -1
  52. package/dist/router.d.ts +85 -48
  53. package/dist/router.d.ts.map +1 -1
  54. package/dist/router.js +4 -6
  55. package/dist/router.js.map +1 -1
  56. package/dist/runtime/boundaries.d.ts +39 -25
  57. package/dist/runtime/boundaries.d.ts.map +1 -1
  58. package/dist/runtime/boundaries.js +22 -17
  59. package/dist/runtime/boundaries.js.map +1 -1
  60. package/dist/runtime/client.d.ts +9 -8
  61. package/dist/runtime/client.d.ts.map +1 -1
  62. package/dist/runtime/client.js +9 -8
  63. package/dist/runtime/client.js.map +1 -1
  64. package/dist/runtime/context.d.ts +216 -70
  65. package/dist/runtime/context.d.ts.map +1 -1
  66. package/dist/runtime/context.js +299 -93
  67. package/dist/runtime/context.js.map +1 -1
  68. package/dist/runtime/control.d.ts.map +1 -1
  69. package/dist/runtime/control.js +7 -0
  70. package/dist/runtime/control.js.map +1 -1
  71. package/dist/runtime/dev-protocol.d.ts.map +1 -1
  72. package/dist/runtime/dev-protocol.js.map +1 -1
  73. package/dist/runtime/entry.client.d.ts +4 -0
  74. package/dist/runtime/entry.client.d.ts.map +1 -1
  75. package/dist/runtime/entry.client.js +190 -154
  76. package/dist/runtime/entry.client.js.map +1 -1
  77. package/dist/runtime/entry.rsc.d.ts.map +1 -1
  78. package/dist/runtime/entry.rsc.js +145 -159
  79. package/dist/runtime/entry.rsc.js.map +1 -1
  80. package/dist/runtime/entry.ssr.d.ts +9 -1
  81. package/dist/runtime/entry.ssr.d.ts.map +1 -1
  82. package/dist/runtime/entry.ssr.js +36 -18
  83. package/dist/runtime/entry.ssr.js.map +1 -1
  84. package/dist/runtime/flight-inject.d.ts +31 -0
  85. package/dist/runtime/flight-inject.d.ts.map +1 -0
  86. package/dist/runtime/flight-inject.js +221 -0
  87. package/dist/runtime/flight-inject.js.map +1 -0
  88. package/dist/runtime/navigation.d.ts +20 -38
  89. package/dist/runtime/navigation.d.ts.map +1 -1
  90. package/dist/runtime/navigation.js +10 -53
  91. package/dist/runtime/navigation.js.map +1 -1
  92. package/dist/runtime/request.d.ts +6 -0
  93. package/dist/runtime/request.d.ts.map +1 -1
  94. package/dist/runtime/request.js +8 -0
  95. package/dist/runtime/request.js.map +1 -1
  96. package/dist/runtime/server.d.ts +7 -12
  97. package/dist/runtime/server.d.ts.map +1 -1
  98. package/dist/runtime/server.js +15 -12
  99. package/dist/runtime/server.js.map +1 -1
  100. package/dist/server/headers.d.ts +9 -9
  101. package/dist/server/headers.js +9 -9
  102. package/dist/server/headers.js.map +1 -1
  103. package/dist/server/load-config.d.ts +2 -2
  104. package/dist/server/load-config.d.ts.map +1 -1
  105. package/dist/server/load-config.js +22 -11
  106. package/dist/server/load-config.js.map +1 -1
  107. package/dist/server/prerendered.d.ts +43 -15
  108. package/dist/server/prerendered.d.ts.map +1 -1
  109. package/dist/server/prerendered.js +47 -0
  110. package/dist/server/prerendered.js.map +1 -1
  111. package/dist/server/server-config.d.ts +13 -39
  112. package/dist/server/server-config.d.ts.map +1 -1
  113. package/dist/server/server-config.js +5 -74
  114. package/dist/server/server-config.js.map +1 -1
  115. package/dist/server/ssg.d.ts +2 -2
  116. package/dist/server/ssg.d.ts.map +1 -1
  117. package/dist/server/ssg.js +21 -34
  118. package/dist/server/ssg.js.map +1 -1
  119. package/package.json +13 -16
  120. package/dist/deploy/bun/runtime.d.ts +0 -11
  121. package/dist/deploy/bun/runtime.d.ts.map +0 -1
  122. package/dist/deploy/bun/runtime.js +0 -22
  123. package/dist/deploy/bun/runtime.js.map +0 -1
  124. package/dist/deploy/deno/runtime.d.ts +0 -11
  125. package/dist/deploy/deno/runtime.d.ts.map +0 -1
  126. package/dist/deploy/deno/runtime.js +0 -16
  127. package/dist/deploy/deno/runtime.js.map +0 -1
  128. package/dist/deploy/listen.d.ts +0 -20
  129. package/dist/deploy/listen.d.ts.map +0 -1
  130. package/dist/deploy/listen.js +0 -24
  131. package/dist/deploy/listen.js.map +0 -1
  132. package/dist/deploy/netlify/build.d.ts +0 -8
  133. package/dist/deploy/netlify/build.d.ts.map +0 -1
  134. package/dist/deploy/netlify/build.js +0 -52
  135. package/dist/deploy/netlify/build.js.map +0 -1
  136. package/dist/deploy/netlify/runtime.d.ts +0 -13
  137. package/dist/deploy/netlify/runtime.d.ts.map +0 -1
  138. package/dist/deploy/netlify/runtime.js +0 -24
  139. package/dist/deploy/netlify/runtime.js.map +0 -1
  140. package/dist/server/compress.d.ts +0 -15
  141. package/dist/server/compress.d.ts.map +0 -1
  142. package/dist/server/compress.js +0 -76
  143. package/dist/server/compress.js.map +0 -1
@@ -1,6 +1,6 @@
1
1
  /// <reference path="../types/rshono-config.d.ts" />
2
2
  /**
3
- * The request context: {@link getContext} and the {@link Ctx} wrapper it returns,
3
+ * The request context: {@link getRequestContext} and the {@link RequestContext} wrapper it returns,
4
4
  * the {@link redirect} / {@link notFound} control-flow helpers, and the
5
5
  * {@link onServerError} reporting funnel — plus the `@internal` plumbing that binds
6
6
  * a request to the async context in the first place.
@@ -14,38 +14,77 @@ import { deleteCookie, getCookie, setCookie } from 'hono/cookie';
14
14
  import { AsyncLocalStorage } from 'node:async_hooks';
15
15
  import { NotFoundSignal, RedirectSignal } from './control.js';
16
16
  const contextStorage = new AsyncLocalStorage();
17
- /** One {@link Ctx} per Hono {@link Context}, so repeated `getContext()` calls in a request share its lazy getters. */
17
+ /** One {@link RequestContext} per Hono {@link Context}, so repeated `getRequestContext()` calls in a request share its lazy getters. */
18
18
  const wrappers = new WeakMap();
19
+ /**
20
+ * Requests whose page render has begun — the point past which nothing can change the response head.
21
+ *
22
+ * A `WeakSet` keyed on the Hono {@link Context} rather than a field on {@link RequestContext},
23
+ * so marking a request costs nothing for the pages that never read their context: the wrapper is
24
+ * built lazily by {@link getRequestContext} and this must not be what forces it into existence.
25
+ */
26
+ const rendering = new WeakSet();
27
+ /**
28
+ * Marks the request as having entered its page render, which is what makes
29
+ * {@link RequestContext.setHeader} and `ctx.cookies.set()` start throwing.
30
+ *
31
+ * Framework internal — `renderComponent` calls this immediately before handing the page to React.
32
+ * Everything that legitimately writes to the response (middleware, a `'use server'` action, an
33
+ * endpoint route) has already run by then, so none of them are affected.
34
+ *
35
+ * @internal
36
+ */
37
+ export function beginPageRender(c) {
38
+ rendering.add(c);
39
+ }
40
+ /**
41
+ * The shared explanation for a response mutation that arrived too late, thrown by
42
+ * {@link RequestContext.setHeader} and the `cookies` writers.
43
+ *
44
+ * Refusing beats the alternative, which was silent *and* inconsistent: a page setting a cookie got it
45
+ * on a full page load and lost it on a soft navigation, because the flight stream's response head is
46
+ * committed before the page component's first line runs. Nothing inside the render can fix that, so
47
+ * the message says where the write does belong instead.
48
+ */
49
+ function tooLateToWrite(call) {
50
+ throw new Error(`[rshono] ${call} was called while rendering a page, which is too late to affect the response. ` +
51
+ 'A page streams, so its response head is already committed by the time the component runs — the ' +
52
+ 'write would land on a full page load and be silently dropped on a soft navigation. Do it from a ' +
53
+ "'use server' action instead; or, in middleware and { type: 'endpoint' } routes — which are handed " +
54
+ "Hono's `c` directly and run outside the request context — with `c.header(…)` / `setCookie(c, …)`.");
55
+ }
56
+ /** The shared explanation for a Hono `Context` member that a page has no way to use. See the stubs on {@link RequestContext}. */
57
+ function notOnContext(call, instead) {
58
+ throw new Error(`[rshono] ctx.${call} does not exist. A page returns JSX and the framework builds the response from it, ` +
59
+ `so Hono's response builders have nothing to return to. ${instead}`);
60
+ }
19
61
  /**
20
62
  * `process.env`, snapshotted on first read.
21
63
  *
22
- * It is not a plain object — every enumeration crosses into the host environment, which made
23
- * spreading it (~20µs) by far the most expensive thing {@link Ctx.env} did, once per request that
24
- * touched it. Snapshotted lazily rather than at module load because `loadEnvFiles()` runs *after*
25
- * this module is imported, so an eager copy would miss everything from `.env`. The trade-off: a
26
- * `process.env` mutation after the first `ctx.env` read is not picked up.
64
+ * Enumerating it crosses into the host environment, which made the spread (~20µs) by far the most
65
+ * expensive thing {@link RequestContext.env} did, once per request that touched it. Lazily rather
66
+ * than at module load, because `loadEnvFiles()` runs *after* this module is imported and an eager
67
+ * copy would miss everything from `.env`. The trade-off: a `process.env` mutation after the first
68
+ * `ctx.env` read is not picked up.
27
69
  */
28
70
  let envSnapshot;
29
71
  function processEnv() {
30
72
  return (envSnapshot ??= typeof process !== 'undefined' && process.env ? { ...process.env } : {});
31
73
  }
32
74
  /**
33
- * True when this process is the SSG build prerendering `render: 'static'` routes,
34
- * rather than a server handling real requests. `build.ts` sets `RSHONO_PRERENDER`
35
- * before importing the app bundle and starting the prerender pass; the app bundle
36
- * inlines its own copy of this module, so a shared `process.env` (not a module-level
37
- * flag) is what reliably crosses that boundary. Read by {@link getContext} to turn a
38
- * static route's request-context read into a clear build-time error instead of
39
- * silently baking synthetic build-time values (a `localhost` URL, no cookies, build
40
- * env) into the snapshot.
75
+ * True when this process is the SSG build prerendering `render: 'static'` routes rather than a server
76
+ * handling real requests. `build.ts` sets `RSHONO_PRERENDER` before importing the app bundle, which
77
+ * inlines its own copy of this module — so `process.env` is what crosses that boundary, not a
78
+ * module-level flag. {@link getRequestContext} reads it to fail loudly instead of baking synthetic
79
+ * build-time values (a `localhost` URL, no cookies, build env) into the prerendered page.
41
80
  */
42
81
  const prerendering = typeof process !== 'undefined' && !!process.env?.RSHONO_PRERENDER;
43
82
  /**
44
83
  * Runs `fn` with the given Hono {@link Context} bound as the ambient request
45
- * context, so that {@link getContext} resolves to it anywhere in the call tree.
84
+ * context, so that {@link getRequestContext} resolves to it anywhere in the call tree.
46
85
  *
47
86
  * Framework internal — the request handler wraps every render and action in
48
- * this. Application code should reach for {@link getContext} instead.
87
+ * this. Application code should reach for {@link getRequestContext} instead.
49
88
  *
50
89
  * @internal
51
90
  */
@@ -53,12 +92,10 @@ export function runWithContext(c, fn) {
53
92
  return contextStorage.run(c, fn);
54
93
  }
55
94
  /**
56
- * Reads the matched route params, returning an empty object when there is no
57
- * active route match (rather than throwing). Shared by {@link Ctx.params} and the
58
- * request renderer so the fallback behaviour stays in one place.
95
+ * The matched route params, or an empty object when there is no active match.
59
96
  *
60
- * Framework internal — read params from {@link Ctx.params} or a page's
61
- * `PageProps` instead.
97
+ * Framework internal — the request renderer calls this to build a page's `params` prop, and
98
+ * {@link RequestContext.params} caches it. Read them from that prop, or from `ctx.params`.
62
99
  *
63
100
  * @internal
64
101
  */
@@ -80,18 +117,36 @@ function firstForwardedValue(header) {
80
117
  // Read through `typeof` so that degrades to the safe answer — don't trust — instead of a ReferenceError.
81
118
  const trustProxy = typeof __RSHONO_CONFIG__ !== 'undefined' && __RSHONO_CONFIG__.trustProxy;
82
119
  /**
83
- * Resolves the browser-facing {@link URL} for a request.
120
+ * Resolves the browser-facing {@link URL} for a request, from a Hono {@link Context}.
84
121
  *
85
- * `c.req.url` reflects the internal address the server was reached on, which is wrong behind a
86
- * proxy or load balancer. `X-Forwarded-Host` / `X-Forwarded-Proto` fix that up — **but only when
87
- * `trustProxy` is enabled in `rshono.config.ts`** (always the case under `rshono dev`). Those
88
- * headers are client-supplied: honouring them unconditionally lets anyone who can reach the server
89
- * dictate the origin of every absolute URL the app builds — canonical tags, emails, redirects — and
90
- * poison a shared cache with them. So the default is to ignore them entirely.
122
+ * `c.req.url` reflects the internal address the server was reached on, which is wrong behind a proxy.
123
+ * `X-Forwarded-Host` / `X-Forwarded-Proto` fix that up — **but only when `trustProxy` is enabled in
124
+ * `rshono.config.ts`** (always so under `rshono dev`). They are client-supplied, so honouring them
125
+ * unconditionally would let anyone who can reach the server dictate the origin of every absolute URL
126
+ * the app builds, and poison a shared cache with it.
91
127
  *
92
- * Framework internal — prefer {@link Ctx.url}, which caches the result per request.
128
+ * This is the form for **middleware**, which is handed `c` and runs outside the request context. In
129
+ * a server component or a `'use server'` action prefer {@link RequestContext.url}, which is this same
130
+ * value cached per request.
93
131
  *
94
- * @internal
132
+ * Its main use is giving Hono's own middleware the origin the browser actually used, since they all
133
+ * read `c.req.url` on their own and so see the internal one:
134
+ *
135
+ * @example
136
+ * ```ts
137
+ * // src/server.ts — a CSRF check that still works behind a proxy that rewrites Host
138
+ * import { publicUrl } from '@rshono/core/server';
139
+ * import { csrf } from 'hono/csrf';
140
+ *
141
+ * server.use(csrf({ origin: (origin, c) => origin === publicUrl(c).origin }));
142
+ * ```
143
+ *
144
+ * A fresh instance per call, so mutating it disturbs nothing else.
145
+ *
146
+ * @param c - The Hono {@link Context} for the request.
147
+ * @returns The browser-facing URL — proxy-corrected under `trustProxy`, `c.req.url` otherwise.
148
+ *
149
+ * @see {@link https://www.rshono.com/docs/configuration#proxy-headers | Docs — proxy headers}
95
150
  */
96
151
  export function publicUrl(c) {
97
152
  const url = new URL(c.req.url);
@@ -116,73 +171,125 @@ export function publicUrl(c) {
116
171
  * Ergonomic, read-mostly wrapper around Hono's {@link Context} for use inside
117
172
  * server components and server actions.
118
173
  *
119
- * Obtain one with {@link getContext}, or — in a page component — take it straight
174
+ * Obtain one with {@link getRequestContext}, or — in a page component — take it straight
120
175
  * off the `ctx` prop, which is this same object. Never construct it yourself. One
121
176
  * instance is reused for the lifetime of a request, so its lazy getters
122
- * ({@link Ctx.url}, {@link Ctx.env}) are computed at most once.
177
+ * ({@link RequestContext.url}, {@link RequestContext.env}) are computed at most once.
123
178
  *
124
179
  * @typeParam E - The Hono {@link Env} describing this app's `Bindings` and
125
- * `Variables`, so {@link Ctx.var} and {@link Ctx.env} stay typed.
180
+ * `Variables`, so {@link RequestContext.var} and {@link RequestContext.env} stay typed.
126
181
  *
127
182
  * @example
128
183
  * ```tsx
129
- * import { getContext } from '@rshono/core/server';
184
+ * import { getRequestContext } from '@rshono/core/server';
130
185
  *
131
186
  * export default async function Whoami() {
132
- * const ctx = getContext();
187
+ * const ctx = getRequestContext();
133
188
  * const session = ctx.cookies.get('session');
134
189
  * return <p>{ctx.url.pathname} — {session ?? 'anonymous'}</p>;
135
190
  * }
136
191
  * ```
192
+ *
193
+ * @see {@link https://www.rshono.com/docs/api#rshonocoreserver | Docs — `@rshono/core/server`}
194
+ * @see {@link https://hono.dev/docs/api/context | Hono — Context}, reachable in full via {@link RequestContext.hono}
137
195
  */
138
- export class Ctx {
196
+ export class RequestContext {
139
197
  #raw;
140
198
  #url;
141
199
  #env;
200
+ #params;
201
+ /**
202
+ * Framework internal — one instance is created per request and handed to you by
203
+ * {@link getRequestContext} or the `ctx` page prop. Application code never calls this.
204
+ *
205
+ * @internal
206
+ */
142
207
  constructor(c) {
143
208
  this.#raw = c;
144
209
  }
145
210
  /**
146
- * The underlying Hono {@link Context}. Escape hatch for anything this wrapper does not expose.
211
+ * The underlying Hono {@link Context} — the escape hatch for what this wrapper does not expose,
212
+ * such as `executionCtx.waitUntil()` on Workers.
213
+ *
214
+ * Its response builders (`redirect`, `notFound`, `json`, `body`, `status`, …) still do nothing from
215
+ * inside a page, for the reason the stubs on this class explain: reaching them through here
216
+ * bypasses the error, it does not make them work.
217
+ *
218
+ * @example
219
+ * ```ts
220
+ * getRequestContext().hono.executionCtx.waitUntil(logAsync()); // Workers
221
+ * ```
147
222
  *
148
- * A getter over a private field rather than a plain property, so it is not an *own enumerable*
149
- * one — which matters more than it looks. React's diagnostic for a value that cannot be sent to a
150
- * client component (`describeObjectForErrorMessage`) walks `Object.keys` recursively with no depth
151
- * limit and no cycle guard, and the Hono context graph reaches the socket and the whole server
152
- * through `req.raw` and `env`. While this was a plain property, passing a `Ctx` to a `'use client'`
153
- * component blew the stack *inside that message builder* — so React's actual, accurate "you cannot
154
- * pass this" error never got printed. Hidden from `Object.keys`, the walk stops here.
223
+ * @see {@link https://hono.dev/docs/api/context | Hono — Context}
155
224
  */
156
- get raw() {
225
+ // A prototype getter rather than a plain property, so it is not *own enumerable*. React's
226
+ // diagnostic for a value that cannot be sent to a client component walks `Object.keys` recursively
227
+ // with no depth limit and no cycle guard, and the Hono context graph reaches the socket and the
228
+ // whole server through `req.raw` and `env` — as a plain property this blew the stack inside the
229
+ // message builder, so React's accurate "you cannot pass this" error never got printed. Every member
230
+ // here is a getter or method for the same reason; `cookies` is the one own enumerable property, and
231
+ // it is a shallow object of four functions.
232
+ get hono() {
157
233
  return this.#raw;
158
234
  }
159
- /** The parsed Hono request (`c.req`) — headers, body parsing, param access, etc. */
235
+ /**
236
+ * The parsed request — method, headers, path params, query and the body readers. Hono's
237
+ * {@link Context.req}, unwrapped, so `ctx.req.header('authorization')` rather than
238
+ * `ctx.hono.req.header(…)`.
239
+ *
240
+ * Reads only. Setting a *response* header is {@link RequestContext.setHeader}, deliberately in a
241
+ * different place — Hono's `c.header()` writing the response while `c.req.header()` reads the
242
+ * request is a well-worn source of confusion.
243
+ *
244
+ * @example
245
+ * ```ts
246
+ * const ctx = getRequestContext();
247
+ * ctx.req.method; // 'GET'
248
+ * ctx.req.header('authorization'); // string | undefined
249
+ * ctx.req.query('tab'); // string | undefined
250
+ * ```
251
+ *
252
+ * @see {@link https://hono.dev/docs/api/request | Hono — HonoRequest}
253
+ */
160
254
  get req() {
161
255
  return this.#raw.req;
162
256
  }
163
257
  /**
164
- * The browser-facing request URL, proxy-header aware (see {@link publicUrl}) —
165
- * read `url.pathname`, `url.searchParams` and the rest off it. Parsed once and
166
- * cached, so the same instance comes back on every read within a request; treat
167
- * it as read-only for that reason.
258
+ * Matched route params for this request, e.g. `{ id: '42' }` for `/profile/:id`. Empty when there
259
+ * is no active route match.
260
+ *
261
+ * A **page** is handed the same record as its `params` prop, typed key-by-key from its route path,
262
+ * and that is the better read where it is available. This is for everywhere else — a nested server
263
+ * component, or a `'use server'` action — which get no props from the framework.
168
264
  */
169
- get url() {
170
- return (this.#url ??= publicUrl(this.#raw));
171
- }
172
- /** The HTTP method of the request, e.g. `GET` or `POST`. */
173
- get method() {
174
- return this.#raw.req.method;
265
+ get params() {
266
+ return (this.#params ??= readParams(this.#raw));
175
267
  }
176
268
  /**
177
- * Matched route params, e.g. `{ id }` for a `/users/[id]` route. Returns an
178
- * empty object when there is no active route match (rather than throwing).
269
+ * The browser-facing request URL — read `url.pathname`, `url.searchParams` and the
270
+ * rest off it. Parsed once and cached, so the same instance comes back on every
271
+ * read within a request; treat it as read-only for that reason.
272
+ *
273
+ * `X-Forwarded-Host` / `-Proto` are honoured only when `trustProxy` is enabled in
274
+ * `rshono.config.ts`, since any client can send them.
275
+ *
276
+ * @see {@link https://www.rshono.com/docs/configuration#proxy-headers | Docs — proxy headers}
179
277
  */
180
- get params() {
181
- return readParams(this.#raw);
278
+ get url() {
279
+ return (this.#url ??= publicUrl(this.#raw));
182
280
  }
183
281
  /**
184
282
  * Typed variables set by middleware via `c.set('user', …)`, read here as
185
283
  * `ctx.var.user`. Type them by parameterising this class's {@link Env}.
284
+ *
285
+ * @example
286
+ * ```ts
287
+ * type AppEnv = { Variables: { user: { id: string } } };
288
+ * const { user } = getRequestContext<AppEnv>().var; // typed, set by your middleware
289
+ * ```
290
+ *
291
+ * @see {@link https://hono.dev/docs/api/context#var | Hono — c.var}
292
+ * @see {@link https://www.rshono.com/docs/hono#typing-the-context | Docs — typing the context}
186
293
  */
187
294
  get var() {
188
295
  return this.#raw.var;
@@ -191,7 +298,10 @@ export class Ctx {
191
298
  * Environment for the request: process env vars merged with runtime bindings
192
299
  * (bindings win on conflict). Computed once and cached.
193
300
  *
194
- * @example `const key = getContext().env.STRIPE_SECRET_KEY;`
301
+ * @example `const key = getRequestContext().env.STRIPE_SECRET_KEY;`
302
+ *
303
+ * @see {@link https://hono.dev/docs/api/context#env | Hono — c.env}
304
+ * @see {@link https://www.rshono.com/docs/configuration#environment-and-secrets | Docs — environment and secrets}
195
305
  */
196
306
  get env() {
197
307
  if (this.#env)
@@ -200,47 +310,137 @@ export class Ctx {
200
310
  // The snapshot is shared, so hand it back as-is when there are no bindings to merge over it.
201
311
  return (this.#env = (bindings ? { ...processEnv(), ...bindings } : processEnv()));
202
312
  }
203
- /** Sets a response header. Thin pass-through to `c.header(name, value)`. */
204
- header(name, value) {
205
- this.#raw.header(name, value);
206
- }
207
313
  /**
208
314
  * Read and write request/response cookies.
209
315
  *
210
316
  * @example
211
317
  * ```ts
212
- * const ctx = getContext();
318
+ * const ctx = getRequestContext();
213
319
  * ctx.cookies.get('session'); // string | undefined
214
320
  * ctx.cookies.set('session', id, { httpOnly: true, sameSite: 'Lax', path: '/' });
215
321
  * ctx.cookies.delete('session', { path: '/' });
216
322
  * ```
323
+ *
324
+ * @see {@link https://hono.dev/docs/helpers/cookie | Hono — cookie helper}, which this wraps
217
325
  */
218
326
  cookies = {
219
- /** Reads a single cookie by name, or `undefined` if absent. */
327
+ /** Reads a single cookie by name, or `undefined` if absent. Safe anywhere, a page included. */
220
328
  get: (name) => getCookie(this.#raw, name),
221
- /** Reads every cookie as a `{ name: value }` record. */
329
+ /** Reads every cookie as a `{ name: value }` record. Safe anywhere, a page included. */
222
330
  all: () => getCookie(this.#raw),
223
- /** Sets a cookie on the response. See Hono's {@link CookieOptions} for `path`, `httpOnly`, `maxAge`, etc. */
224
- set: (name, value, options) => setCookie(this.#raw, name, value, options),
225
- /** Clears a cookie. Pass the same `path`/`domain` it was set with so the browser matches it. */
331
+ /**
332
+ * Sets a cookie on the response. See Hono's {@link CookieOptions} for `path`, `httpOnly`,
333
+ * `maxAge`, etc.
334
+ *
335
+ * **Throws inside a page render** — see {@link RequestContext.setHeader}, of which a `Set-Cookie`
336
+ * is a special case. Set cookies from a `'use server'` action, or with Hono's `setCookie(c, …)`
337
+ * in middleware and endpoint routes.
338
+ *
339
+ * @throws If called while a page is rendering, where it could not reach the browser reliably.
340
+ *
341
+ * @see {@link https://hono.dev/docs/helpers/cookie#options | Hono — cookie options}
342
+ */
343
+ set: (name, value, options) => {
344
+ this.#assertWritable('ctx.cookies.set()');
345
+ setCookie(this.#raw, name, value, options);
346
+ },
347
+ /**
348
+ * Clears a cookie. Pass the same `path`/`domain` it was set with so the browser matches it.
349
+ * Throws inside a page render, exactly as `set` does.
350
+ *
351
+ * @throws If called while a page is rendering.
352
+ */
226
353
  delete: (name, options) => {
354
+ this.#assertWritable('ctx.cookies.delete()');
227
355
  deleteCookie(this.#raw, name, options);
228
356
  },
229
357
  };
358
+ /** Guards every write that has to reach the response head. See {@link tooLateToWrite}. */
359
+ #assertWritable(call) {
360
+ if (rendering.has(this.#raw))
361
+ tooLateToWrite(call);
362
+ }
363
+ /**
364
+ * Sets a header on the response — from a `'use server'` action, which is the one place a request
365
+ * context exists *and* the response is still open.
366
+ *
367
+ * From inside a page it throws: a page streams, so by then the response head is committed. Hono's
368
+ * `c.header()` fails there silently and inconsistently — landing on a full page load, vanishing on
369
+ * a soft navigation — so this refuses rather than doing it half the time.
370
+ *
371
+ * Middleware and `{ type: 'endpoint' }` routes run outside the request context but are handed
372
+ * Hono's `c` directly, so they use `c.header(…)`. That is also where a header belonging to the
373
+ * *page* rather than to one action goes — `Cache-Control`, `X-Robots-Tag` — since middleware runs
374
+ * before the render.
375
+ *
376
+ * @param name - Header name, case-insensitive.
377
+ * @param value - Header value.
378
+ * @param options - `{ append: true }` to add another value rather than replace.
379
+ * @throws If called while a page is rendering, where it could not reach the browser reliably.
380
+ *
381
+ * @example
382
+ * ```ts
383
+ * 'use server';
384
+ * export async function logout() {
385
+ * const ctx = getRequestContext();
386
+ * ctx.cookies.delete('session', { path: '/' });
387
+ * ctx.setHeader('clear-site-data', '"cache", "storage"');
388
+ * redirect('/');
389
+ * }
390
+ * ```
391
+ */
392
+ setHeader(name, value, options) {
393
+ this.#assertWritable('ctx.setHeader()');
394
+ this.#raw.header(name, value, options);
395
+ }
396
+ // Hono's response builders, restated as errors naming what to use instead. A page returns JSX and
397
+ // `renderComponent` builds the response from it, so every one of these is a silent no-op through
398
+ // `ctx.hono`. `@deprecated` is the compile-time signal — an editor strikes them through in
399
+ // autocomplete — and the thrown message is the one that explains. Each takes `...args: unknown[]`
400
+ // it never reads so that `ctx.redirect('/dashboard')` reaches that message rather than stopping at
401
+ // "Expected 0 arguments, but got 1".
402
+ /** @deprecated Not available on a page's context — use `redirect()` from `@rshono/core/server`. */
403
+ redirect(...args) {
404
+ return notOnContext('redirect(location, status?)', "Use `redirect()` from '@rshono/core/server', which throws a signal the framework turns into a real redirect.");
405
+ }
406
+ /** @deprecated Not available on a page's context — use `notFound()` from `@rshono/core/server`. */
407
+ notFound(...args) {
408
+ return notOnContext('notFound()', "Use `notFound()` from '@rshono/core/server', which aborts the render and shows the app's not-found page.");
409
+ }
410
+ /** @deprecated A page renders JSX. For a JSON response, use an `{ type: 'endpoint' }` route. */
411
+ json(...args) {
412
+ return notOnContext('json(object)', "For a JSON response use an { type: 'endpoint' } route; to read the request body use `ctx.req.json()`.");
413
+ }
414
+ /** @deprecated A page renders JSX. For a text response, use an `{ type: 'endpoint' }` route. */
415
+ text(...args) {
416
+ return notOnContext('text(string)', "For a text response use an { type: 'endpoint' } route; to read the request body use `ctx.req.text()`.");
417
+ }
418
+ /** @deprecated A page renders JSX, which the framework turns into HTML for you. */
419
+ html(...args) {
420
+ return notOnContext('html(string)', "A page's JSX is already its HTML; for a hand-built HTML response use an { type: 'endpoint' } route.");
421
+ }
422
+ /** @deprecated Not available on a page's context. To read the request body, use `ctx.req`. */
423
+ body(...args) {
424
+ return notOnContext('body(data, …)', "To read the *request* body use `ctx.req.json()` / `ctx.req.text()` / `ctx.req.formData()`; to build a response, use an { type: 'endpoint' } route.");
425
+ }
426
+ /** @deprecated A page's status is set by the framework — use `notFound()`, or an endpoint route. */
427
+ status(...args) {
428
+ return notOnContext('status(code)', "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.");
429
+ }
430
+ /** @deprecated Renamed — use `ctx.setHeader(name, value)`, which is valid from a `'use server'` action. */
431
+ header(...args) {
432
+ return notOnContext('header(name, value)', "Use `ctx.setHeader(name, value)` from a 'use server' action, or `c.header(…)` in middleware — a page renders too late to set one.");
433
+ }
230
434
  }
231
435
  /**
232
- * Returns the {@link Ctx} for the current request.
233
- *
234
- * This is the primary entry point for reading request data from a server
235
- * component or server action — the URL, cookies, params, env, and middleware
236
- * variables. The returned wrapper is memoised per request, so repeated calls in
237
- * the same request are cheap and return the same instance.
436
+ * Returns the {@link RequestContext} for the current request — the URL, cookies, params, env and
437
+ * middleware variables, read from a server component or a server action. Memoised per request, so
438
+ * repeated calls return the same instance.
238
439
  *
239
- * A **page** component is handed the very same object as its `ctx` prop, so this
240
- * import is for everywhere else: a nested server component, or a `'use server'`
241
- * action module — neither of which receives props from the framework.
440
+ * A **page** component is handed that same object as its `ctx` prop, so this import is for everywhere
441
+ * else: a nested server component, or a `'use server'` action module.
242
442
  *
243
- * @typeParam E - The app's Hono {@link Env}, to type {@link Ctx.var} and {@link Ctx.env}.
443
+ * @typeParam E - The app's Hono {@link Env}, to type {@link RequestContext.var} and {@link RequestContext.env}.
244
444
  * @throws If called at module load, where there is no ambient context to resolve.
245
445
  * @throws If called while prerendering a `render: 'static'` route, which has no
246
446
  * per-request context at build time — mark the route `render: 'dynamic'` instead.
@@ -248,28 +448,30 @@ export class Ctx {
248
448
  * @example
249
449
  * ```ts
250
450
  * 'use server';
251
- * import { getContext, redirect } from '@rshono/core/server';
451
+ * import { getRequestContext, redirect } from '@rshono/core/server';
252
452
  *
253
453
  * export async function login(form: FormData) {
254
- * getContext().cookies.set('session', String(form.get('email')), { httpOnly: true });
454
+ * getRequestContext().cookies.set('session', String(form.get('email')), { httpOnly: true });
255
455
  * redirect('/dashboard');
256
456
  * }
257
457
  * ```
458
+ *
459
+ * @see {@link https://www.rshono.com/docs/api#rshonocoreserver | Docs — `@rshono/core/server`}
258
460
  */
259
- export function getContext() {
461
+ export function getRequestContext() {
260
462
  if (prerendering) {
261
- throw new Error("[rshono] getContext() was called while prerendering a `render: 'static'` route. A static page " +
463
+ throw new Error("[rshono] getRequestContext() was called while prerendering a `render: 'static'` route. A static page " +
262
464
  'is rendered once at build time, so it has no per-request context to read (URL, cookies, ' +
263
465
  "headers, env). Change this route to `render: 'dynamic'` so it renders per request, or remove " +
264
- 'the getContext() call.');
466
+ 'the getRequestContext() call.');
265
467
  }
266
468
  const c = contextStorage.getStore();
267
469
  if (!c) {
268
- throw new Error('[rshono] getContext() was called outside a request. It only works inside a server component or a server action, not at module load.');
470
+ throw new Error('[rshono] getRequestContext() was called outside a request. It only works inside a server component or a server action, not at module load.');
269
471
  }
270
472
  let ctx = wrappers.get(c);
271
473
  if (!ctx) {
272
- ctx = new Ctx(c);
474
+ ctx = new RequestContext(c);
273
475
  wrappers.set(c, ctx);
274
476
  }
275
477
  return ctx;
@@ -288,7 +490,7 @@ export function getContext() {
288
490
  *
289
491
  * @example
290
492
  * ```ts
291
- * const session = getContext().cookies.get('session');
493
+ * const session = getRequestContext().cookies.get('session');
292
494
  * if (!session) redirect('/login');
293
495
  * // session is defined below this line
294
496
  * ```
@@ -304,9 +506,11 @@ export function redirect(location, status = 303) {
304
506
  *
305
507
  * @example
306
508
  * ```tsx
307
- * const user = await db.user.find(getContext().params.id);
308
- * if (!user) notFound();
309
- * return <Profile user={user} />; // user is non-null here
509
+ * export default async function Page({ params }: PageProps<'/users/:id'>) {
510
+ * const user = await db.user.find(params.id);
511
+ * if (!user) notFound();
512
+ * return <Profile user={user} />; // user is non-null here
513
+ * }
310
514
  * ```
311
515
  */
312
516
  export function notFound() {
@@ -334,6 +538,8 @@ let errorHandler;
334
538
  * Sentry.captureException(error, { tags: { source }, extra: { url: request.url } });
335
539
  * });
336
540
  * ```
541
+ *
542
+ * @see {@link https://www.rshono.com/docs/hono#error-reporting | Docs — error reporting}
337
543
  */
338
544
  export function onServerError(handler) {
339
545
  errorHandler = handler;