@rshono/core 1.0.0-rc.6 → 1.0.0-rc.7

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 (39) hide show
  1. package/README.md +165 -164
  2. package/dist/config.d.ts +45 -1
  3. package/dist/config.d.ts.map +1 -1
  4. package/dist/config.js +17 -1
  5. package/dist/config.js.map +1 -1
  6. package/dist/deploy/contract.d.ts +12 -7
  7. package/dist/deploy/contract.d.ts.map +1 -1
  8. package/dist/deploy/contract.js.map +1 -1
  9. package/dist/index.d.ts +2 -0
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +2 -0
  12. package/dist/index.js.map +1 -1
  13. package/dist/router.d.ts +67 -24
  14. package/dist/router.d.ts.map +1 -1
  15. package/dist/router.js.map +1 -1
  16. package/dist/runtime/boundaries.d.ts +10 -0
  17. package/dist/runtime/boundaries.d.ts.map +1 -1
  18. package/dist/runtime/boundaries.js +6 -0
  19. package/dist/runtime/boundaries.js.map +1 -1
  20. package/dist/runtime/client.d.ts +5 -3
  21. package/dist/runtime/client.d.ts.map +1 -1
  22. package/dist/runtime/client.js +5 -3
  23. package/dist/runtime/client.js.map +1 -1
  24. package/dist/runtime/context.d.ts +165 -23
  25. package/dist/runtime/context.d.ts.map +1 -1
  26. package/dist/runtime/context.js +244 -24
  27. package/dist/runtime/context.js.map +1 -1
  28. package/dist/runtime/entry.rsc.d.ts.map +1 -1
  29. package/dist/runtime/entry.rsc.js +7 -2
  30. package/dist/runtime/entry.rsc.js.map +1 -1
  31. package/dist/runtime/navigation.d.ts +11 -0
  32. package/dist/runtime/navigation.d.ts.map +1 -1
  33. package/dist/runtime/navigation.js +3 -0
  34. package/dist/runtime/navigation.js.map +1 -1
  35. package/dist/runtime/server.d.ts +4 -10
  36. package/dist/runtime/server.d.ts.map +1 -1
  37. package/dist/runtime/server.js +10 -10
  38. package/dist/runtime/server.js.map +1 -1
  39. package/package.json +1 -1
@@ -16,6 +16,50 @@ import { NotFoundSignal, RedirectSignal } from './control.js';
16
16
  const contextStorage = new AsyncLocalStorage();
17
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
+ * Worth being long: the failure it replaces was silent *and* inconsistent — a page setting a cookie
45
+ * got it on a full page load and lost it on a soft navigation, because the flight stream's response
46
+ * head is committed before the first line of the page component runs. There is no fixing that from
47
+ * inside the render (React starts flight work in a microtask, and neither Hono nor the Node adapter
48
+ * will re-read headers once the response exists), so the honest move is to refuse both ways round
49
+ * and say where the write does belong.
50
+ */
51
+ function tooLateToWrite(call) {
52
+ throw new Error(`[rshono] ${call} was called while rendering a page, which is too late to affect the response. ` +
53
+ 'A page streams, so its response head is already committed by the time the component runs — the ' +
54
+ 'write would land on a full page load and be silently dropped on a soft navigation. Do it from a ' +
55
+ "'use server' action instead; or, in middleware and { type: 'endpoint' } routes — which are handed " +
56
+ "Hono's `c` directly and run outside the request context — with `c.header(…)` / `setCookie(c, …)`.");
57
+ }
58
+ /** The shared explanation for a Hono `Context` member that a page has no way to use. See the stubs on {@link RequestContext}. */
59
+ function notOnContext(call, instead) {
60
+ throw new Error(`[rshono] ctx.${call} does not exist. A page returns JSX and the framework builds the response from it, ` +
61
+ `so Hono's response builders have nothing to return to. ${instead}`);
62
+ }
19
63
  /**
20
64
  * `process.env`, snapshotted on first read.
21
65
  *
@@ -58,7 +102,8 @@ export function runWithContext(c, fn) {
58
102
  * one place.
59
103
  *
60
104
  * Framework internal — the request renderer calls this to build a page's `params`
61
- * prop. Read them from that prop (or `ctx.raw.req.param()` outside a page) instead.
105
+ * prop, and {@link RequestContext.params} caches it. Read them from that prop, or
106
+ * from `ctx.params` outside a page.
62
107
  *
63
108
  * @internal
64
109
  */
@@ -134,38 +179,95 @@ export function publicUrl(c) {
134
179
  * return <p>{ctx.url.pathname} — {session ?? 'anonymous'}</p>;
135
180
  * }
136
181
  * ```
182
+ *
183
+ * @see {@link https://www.rshono.com/docs/api#rshonocoreserver | Docs — `@rshono/core/server`}
184
+ * @see {@link https://hono.dev/docs/api/context | Hono — Context}, reachable in full via {@link RequestContext.hono}
137
185
  */
138
186
  export class RequestContext {
139
187
  #raw;
140
188
  #url;
141
189
  #env;
190
+ #params;
191
+ /**
192
+ * Framework internal — one instance is created per request and handed to you by
193
+ * {@link getRequestContext} or the `ctx` page prop. Application code never calls this.
194
+ *
195
+ * @internal
196
+ */
142
197
  constructor(c) {
143
198
  this.#raw = c;
144
199
  }
145
200
  /**
146
- * The underlying Hono {@link Context}. Escape hatch for anything this wrapper does not expose — and
147
- * deliberately most things. `req`, `method`, `params` and a `header()` setter used to sit on this class
148
- * as one-line pass-throughs to `c.req`, `c.req.method`, `c.req.param()` and `c.header()`; they are
149
- * reachable through here (`ctx.raw.req`, `ctx.raw.header(…)`) and adding no name of their own is the
150
- * point. What stays below is what this wrapper actually *does*: a proxy-aware cached URL, an env that
151
- * merges runtime bindings over process env, and cookies without a second import.
201
+ * The underlying Hono {@link Context} — the escape hatch for the long tail this wrapper does not
202
+ * expose. Everything a page or action actually reaches for has a home of its own now
203
+ * ({@link RequestContext.req}, {@link RequestContext.params}, {@link RequestContext.setHeader}), so
204
+ * this is for what is left: `executionCtx.waitUntil()` on Workers, and whatever Hono adds next.
205
+ *
206
+ * Be aware that the response builders on it (`redirect`, `notFound`, `json`, `body`, `status`, …)
207
+ * still do nothing from inside a page, for the reason the stubs on this class explain — reaching
208
+ * them through here bypasses the error, it does not make them work.
152
209
  *
153
- * A getter over a private field rather than a plain property, so it is not an *own enumerable*
154
- * one — which matters more than it looks. React's diagnostic for a value that cannot be sent to a
155
- * client component (`describeObjectForErrorMessage`) walks `Object.keys` recursively with no depth
156
- * limit and no cycle guard, and the Hono context graph reaches the socket and the whole server
157
- * through `req.raw` and `env`. While this was a plain property, passing a `RequestContext` to a `'use client'`
158
- * component blew the stack *inside that message builder* — so React's actual, accurate "you cannot
159
- * pass this" error never got printed. Hidden from `Object.keys`, the walk stops here.
210
+ * @example
211
+ * ```ts
212
+ * getRequestContext().hono.executionCtx.waitUntil(logAsync()); // Workers
213
+ * ```
214
+ *
215
+ * @see {@link https://hono.dev/docs/api/context | Hono — Context}
160
216
  */
161
- get raw() {
217
+ // A getter over a private field rather than a plain property, so it is not an *own enumerable*
218
+ // one — which matters more than it looks. React's diagnostic for a value that cannot be sent to a
219
+ // client component (`describeObjectForErrorMessage`) walks `Object.keys` recursively with no depth
220
+ // limit and no cycle guard, and the Hono context graph reaches the socket and the whole server
221
+ // through `req.raw` and `env`. While this was a plain property, passing a `RequestContext` to a
222
+ // `'use client'` component blew the stack *inside that message builder* — so React's actual,
223
+ // accurate "you cannot pass this" error never got printed. Hidden from `Object.keys`, the walk
224
+ // stops here. Every member added since is a prototype getter or method for the same reason;
225
+ // `cookies` is the one own enumerable property, and it is a shallow object of four functions.
226
+ get hono() {
162
227
  return this.#raw;
163
228
  }
164
229
  /**
165
- * The browser-facing request URL, proxy-header aware (see {@link publicUrl}) —
166
- * read `url.pathname`, `url.searchParams` and the rest off it. Parsed once and
167
- * cached, so the same instance comes back on every read within a request; treat
168
- * it as read-only for that reason.
230
+ * The parsed request — method, headers, path params, query and the body readers. Hono's
231
+ * {@link Context.req}, unwrapped, so `ctx.req.header('authorization')` rather than
232
+ * `ctx.hono.req.header(…)`.
233
+ *
234
+ * Reads only. Setting a response header is {@link RequestContext.setHeader}, which is a different
235
+ * thing living in a different place on purpose — Hono's `c.header()` writing the *response* while
236
+ * `c.req.header()` reads the *request* is a well-worn source of confusion.
237
+ *
238
+ * @example
239
+ * ```ts
240
+ * const ctx = getRequestContext();
241
+ * ctx.req.method; // 'GET'
242
+ * ctx.req.header('authorization'); // string | undefined
243
+ * ctx.req.query('tab'); // string | undefined
244
+ * ```
245
+ *
246
+ * @see {@link https://hono.dev/docs/api/request | Hono — HonoRequest}
247
+ */
248
+ get req() {
249
+ return this.#raw.req;
250
+ }
251
+ /**
252
+ * Matched route params for this request, e.g. `{ id: '42' }` for `/profile/:id`.
253
+ *
254
+ * A **page** is handed these as its `params` prop, typed key-by-key from its route path, and that
255
+ * is the better read where it is available. This is the same record for everywhere else — a nested
256
+ * server component, or a `'use server'` action — which get no props from the framework. Empty when
257
+ * there is no active route match rather than throwing.
258
+ */
259
+ get params() {
260
+ return (this.#params ??= readParams(this.#raw));
261
+ }
262
+ /**
263
+ * The browser-facing request URL — read `url.pathname`, `url.searchParams` and the
264
+ * rest off it. Parsed once and cached, so the same instance comes back on every
265
+ * read within a request; treat it as read-only for that reason.
266
+ *
267
+ * `X-Forwarded-Host` / `-Proto` are honoured only when `trustProxy` is enabled in
268
+ * `rshono.config.ts`, since any client can send them.
269
+ *
270
+ * @see {@link https://www.rshono.com/docs/configuration#proxy-headers | Docs — proxy headers}
169
271
  */
170
272
  get url() {
171
273
  return (this.#url ??= publicUrl(this.#raw));
@@ -173,6 +275,15 @@ export class RequestContext {
173
275
  /**
174
276
  * Typed variables set by middleware via `c.set('user', …)`, read here as
175
277
  * `ctx.var.user`. Type them by parameterising this class's {@link Env}.
278
+ *
279
+ * @example
280
+ * ```ts
281
+ * type AppEnv = { Variables: { user: { id: string } } };
282
+ * const { user } = getRequestContext<AppEnv>().var; // typed, set by your middleware
283
+ * ```
284
+ *
285
+ * @see {@link https://hono.dev/docs/api/context#var | Hono — c.var}
286
+ * @see {@link https://www.rshono.com/docs/hono#typing-the-context | Docs — typing the context}
176
287
  */
177
288
  get var() {
178
289
  return this.#raw.var;
@@ -182,6 +293,9 @@ export class RequestContext {
182
293
  * (bindings win on conflict). Computed once and cached.
183
294
  *
184
295
  * @example `const key = getRequestContext().env.STRIPE_SECRET_KEY;`
296
+ *
297
+ * @see {@link https://hono.dev/docs/api/context#env | Hono — c.env}
298
+ * @see {@link https://www.rshono.com/docs/configuration#environment-and-secrets | Docs — environment and secrets}
185
299
  */
186
300
  get env() {
187
301
  if (this.#env)
@@ -200,19 +314,121 @@ export class RequestContext {
200
314
  * ctx.cookies.set('session', id, { httpOnly: true, sameSite: 'Lax', path: '/' });
201
315
  * ctx.cookies.delete('session', { path: '/' });
202
316
  * ```
317
+ *
318
+ * @see {@link https://hono.dev/docs/helpers/cookie | Hono — cookie helper}, which this wraps
203
319
  */
204
320
  cookies = {
205
- /** Reads a single cookie by name, or `undefined` if absent. */
321
+ /** Reads a single cookie by name, or `undefined` if absent. Safe anywhere, a page included. */
206
322
  get: (name) => getCookie(this.#raw, name),
207
- /** Reads every cookie as a `{ name: value }` record. */
323
+ /** Reads every cookie as a `{ name: value }` record. Safe anywhere, a page included. */
208
324
  all: () => getCookie(this.#raw),
209
- /** Sets a cookie on the response. See Hono's {@link CookieOptions} for `path`, `httpOnly`, `maxAge`, etc. */
210
- set: (name, value, options) => setCookie(this.#raw, name, value, options),
211
- /** Clears a cookie. Pass the same `path`/`domain` it was set with so the browser matches it. */
325
+ /**
326
+ * Sets a cookie on the response. See Hono's {@link CookieOptions} for `path`, `httpOnly`,
327
+ * `maxAge`, etc.
328
+ *
329
+ * **Throws inside a page render** — see {@link RequestContext.setHeader}, of which a `Set-Cookie`
330
+ * is a special case. Set cookies from a `'use server'` action, or with Hono's `setCookie(c, …)`
331
+ * in middleware and endpoint routes.
332
+ *
333
+ * @throws If called while a page is rendering, where it could not reach the browser reliably.
334
+ *
335
+ * @see {@link https://hono.dev/docs/helpers/cookie#options | Hono — cookie options}
336
+ */
337
+ set: (name, value, options) => {
338
+ this.#assertWritable('ctx.cookies.set()');
339
+ setCookie(this.#raw, name, value, options);
340
+ },
341
+ /**
342
+ * Clears a cookie. Pass the same `path`/`domain` it was set with so the browser matches it.
343
+ * Throws inside a page render, exactly as `set` does.
344
+ *
345
+ * @throws If called while a page is rendering.
346
+ */
212
347
  delete: (name, options) => {
348
+ this.#assertWritable('ctx.cookies.delete()');
213
349
  deleteCookie(this.#raw, name, options);
214
350
  },
215
351
  };
352
+ /** Guards every write that has to reach the response head. See {@link tooLateToWrite}. */
353
+ #assertWritable(call) {
354
+ if (rendering.has(this.#raw))
355
+ tooLateToWrite(call);
356
+ }
357
+ /**
358
+ * Sets a header on the response — from a `'use server'` action, which is the one place a request
359
+ * context exists *and* the response is still open.
360
+ *
361
+ * From inside a page it throws. By then the response head is committed: a page streams, and on a
362
+ * soft navigation the flight response is built before the component's first line runs. Hono's
363
+ * `c.header()` fails there silently and inconsistently — landing on a full page load, vanishing on
364
+ * a soft navigation — so this refuses rather than doing it half the time.
365
+ *
366
+ * Middleware and `{ type: 'endpoint' }` routes run outside the request context (no
367
+ * {@link getRequestContext} there) but are handed Hono's `c` directly, so they set headers with
368
+ * `c.header(…)`. That is also where a header belonging to the *page* rather than to one action
369
+ * goes — `Cache-Control`, `X-Robots-Tag` — since middleware runs before the render.
370
+ *
371
+ * @param name - Header name, case-insensitive.
372
+ * @param value - Header value.
373
+ * @param options - `{ append: true }` to add another value rather than replace.
374
+ * @throws If called while a page is rendering, where it could not reach the browser reliably.
375
+ *
376
+ * @example
377
+ * ```ts
378
+ * 'use server';
379
+ * export async function logout() {
380
+ * const ctx = getRequestContext();
381
+ * ctx.cookies.delete('session', { path: '/' });
382
+ * ctx.setHeader('clear-site-data', '"cache", "storage"');
383
+ * redirect('/');
384
+ * }
385
+ * ```
386
+ */
387
+ setHeader(name, value, options) {
388
+ this.#assertWritable('ctx.setHeader()');
389
+ this.#raw.header(name, value, options);
390
+ }
391
+ // Hono's response builders, restated as errors that name the thing to use instead. A page returns
392
+ // JSX and `renderComponent` builds the response from it, so every one of these is a silent no-op
393
+ // through `ctx.hono` — which is exactly the confusion this class exists to remove. They are
394
+ // `@deprecated` so an editor strikes them through in autocomplete: visible, and visibly wrong.
395
+ //
396
+ // Each takes `...args: unknown[]` it never reads, so that `ctx.redirect('/dashboard')` reaches the
397
+ // message below instead of stopping at "Expected 0 arguments, but got 1" — an arity complaint that
398
+ // says nothing about what to do. The `@deprecated` strike-through is the compile-time signal; the
399
+ // thrown message is the one that explains.
400
+ /** @deprecated Not available on a page's context — use `redirect()` from `@rshono/core/server`. */
401
+ redirect(...args) {
402
+ return notOnContext('redirect(location, status?)', "Use `redirect()` from '@rshono/core/server', which throws a signal the framework turns into a real redirect.");
403
+ }
404
+ /** @deprecated Not available on a page's context — use `notFound()` from `@rshono/core/server`. */
405
+ notFound(...args) {
406
+ return notOnContext('notFound()', "Use `notFound()` from '@rshono/core/server', which aborts the render and shows the app's not-found page.");
407
+ }
408
+ /** @deprecated A page renders JSX. For a JSON response, use an `{ type: 'endpoint' }` route. */
409
+ json(...args) {
410
+ return notOnContext('json(object)', "For a JSON response use an { type: 'endpoint' } route; to read the request body use `ctx.req.json()`.");
411
+ }
412
+ /** @deprecated A page renders JSX. For a text response, use an `{ type: 'endpoint' }` route. */
413
+ text(...args) {
414
+ return notOnContext('text(string)', "For a text response use an { type: 'endpoint' } route; to read the request body use `ctx.req.text()`.");
415
+ }
416
+ /** @deprecated A page renders JSX, which the framework turns into HTML for you. */
417
+ html(...args) {
418
+ return notOnContext('html(string)', "A page's JSX is already its HTML; for a hand-built HTML response use an { type: 'endpoint' } route.");
419
+ }
420
+ /** @deprecated Not available on a page's context. To read the request body, use `ctx.req`. */
421
+ body(...args) {
422
+ 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.");
423
+ }
424
+ /** @deprecated A page's status is set by the framework — use `notFound()`, or an endpoint route. */
425
+ status(...args) {
426
+ 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.");
427
+ }
428
+ /** @deprecated Renamed — use `ctx.setHeader(name, value)`, which is valid from a `'use server'` action. */
429
+ header(...args) {
430
+ 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.");
431
+ }
216
432
  }
217
433
  /**
218
434
  * Returns the {@link RequestContext} for the current request.
@@ -241,6 +457,8 @@ export class RequestContext {
241
457
  * redirect('/dashboard');
242
458
  * }
243
459
  * ```
460
+ *
461
+ * @see {@link https://www.rshono.com/docs/api#rshonocoreserver | Docs — `@rshono/core/server`}
244
462
  */
245
463
  export function getRequestContext() {
246
464
  if (prerendering) {
@@ -322,6 +540,8 @@ let errorHandler;
322
540
  * Sentry.captureException(error, { tags: { source }, extra: { url: request.url } });
323
541
  * });
324
542
  * ```
543
+ *
544
+ * @see {@link https://www.rshono.com/docs/hono#error-reporting | Docs — error reporting}
325
545
  */
326
546
  export function onServerError(handler) {
327
547
  errorHandler = handler;
@@ -1 +1 @@
1
- {"version":3,"file":"context.js","sourceRoot":"","sources":["../../src/runtime/context.ts"],"names":[],"mappings":"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","sourcesContent":["/// <reference path=\"../types/rshono-config.d.ts\" />\n/**\n * The request context: {@link getRequestContext} and the {@link RequestContext} wrapper it returns,\n * the {@link redirect} / {@link notFound} control-flow helpers, and the\n * {@link onServerError} reporting funnel — plus the `@internal` plumbing that binds\n * a request to the async context in the first place.\n *\n * The public half of this module is re-exported by `runtime/server.ts`, which is\n * what the `@rshono/core/server` subpath resolves to; import *that* from an app. Nothing\n * here is safe in a `'use client'` module — those run in the browser, with no bound\n * request context.\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\n * you almost always want after a form action (post/redirect/get).\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 in a request share its lazy getters. */\nconst wrappers = new WeakMap<Context, RequestContext>();\n\n/**\n * `process.env`, snapshotted on first read.\n *\n * It is not a plain object — every enumeration crosses into the host environment, which made\n * spreading it (~20µs) by far the most expensive thing {@link RequestContext.env} did, once per request that\n * touched it. Snapshotted lazily rather than at module load because `loadEnvFiles()` runs *after*\n * this module is imported, so an eager copy would miss everything from `.env`. The trade-off: a\n * `process.env` mutation after the first `ctx.env` read is not picked up.\n */\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/**\n * True when this process is the SSG build prerendering `render: 'static'` routes,\n * rather than a server handling real requests. `build.ts` sets `RSHONO_PRERENDER`\n * before importing the app bundle and starting the prerender pass; the app bundle\n * inlines its own copy of this module, so a shared `process.env` (not a module-level\n * flag) is what reliably crosses that boundary. Read by {@link getRequestContext} to turn a\n * static route's request-context read into a clear build-time error instead of\n * silently baking synthetic build-time values (a `localhost` URL, no cookies, build\n * env) into the snapshot.\n */\nconst prerendering = typeof process !== 'undefined' && !!process.env?.RSHONO_PRERENDER;\n\n/**\n * Runs `fn` with the given Hono {@link Context} bound as the ambient request\n * context, so that {@link getRequestContext} resolves to it anywhere in the call tree.\n *\n * Framework internal — the request handler wraps every render and action in\n * this. Application code should reach for {@link getRequestContext} instead.\n *\n * @internal\n */\nexport function runWithContext<T>(c: Context, fn: () => T): T {\n return contextStorage.run(c, fn);\n}\n\n/**\n * Reads the matched route params, returning an empty object when there is no\n * active route match (rather than throwing), so the fallback behaviour stays in\n * one place.\n *\n * Framework internal — the request renderer calls this to build a page's `params`\n * prop. Read them from that prop (or `ctx.raw.req.param()` outside a page) instead.\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// DefinePlugin inlines the config into the server bundle, but this module is the public\n// `@rshono/core/server` entry and could be loaded by tooling that doesn't (a unit test, a one-off script).\n// Read through `typeof` so that degrades to the safe answer — don't trust — instead of a ReferenceError.\nconst trustProxy = typeof __RSHONO_CONFIG__ !== 'undefined' && __RSHONO_CONFIG__.trustProxy;\n\n/**\n * Resolves the browser-facing {@link URL} for a request.\n *\n * `c.req.url` reflects the internal address the server was reached on, which is wrong behind a\n * proxy or load balancer. `X-Forwarded-Host` / `X-Forwarded-Proto` fix that up — **but only when\n * `trustProxy` is enabled in `rshono.config.ts`** (always the case under `rshono dev`). Those\n * headers are client-supplied: honouring them unconditionally lets anyone who can reach the server\n * dictate the origin of every absolute URL the app builds — canonical tags, emails, redirects — and\n * poison a shared cache with them. So the default is to ignore them entirely.\n *\n * Framework internal — prefer {@link RequestContext.url}, which caches the result per request.\n *\n * @internal\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 rather than assigned to `url.host`, because that setter *keeps the existing port* when\n // the new value has none — leaving the internal port on the public URL (`example.com:3000`).\n const forwarded = forwardedHost ? URL.parse(`http://${forwardedHost}`) : null;\n if (forwarded) {\n url.hostname = forwarded.hostname;\n url.port = forwarded.port; // '' when the forwarded host carries no port, which clears it\n }\n\n // Restricted to the two schemes a browser can actually have requested; anything else (a proxy\n // sending junk, or a client trying its luck) 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: Cloudflare/Workers `Bindings` merged\n * with process env vars. Values not declared in `Bindings` are typed as\n * `string | undefined`. See {@link RequestContext.env}.\n */\nexport type EnvVars<E extends Env> = E['Bindings'] & Record<string, string | undefined>;\n\n/**\n * Ergonomic, read-mostly wrapper around Hono's {@link Context} for use inside\n * server components and server actions.\n *\n * Obtain one with {@link getRequestContext}, or — in a page component — take it straight\n * off the `ctx` prop, which is this same object. Never construct it yourself. One\n * instance is reused for the lifetime of a request, so its lazy getters\n * ({@link RequestContext.url}, {@link RequestContext.env}) are computed at most once.\n *\n * @typeParam E - The Hono {@link Env} describing this app's `Bindings` and\n * `Variables`, so {@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 */\nexport class RequestContext<E extends Env = Env> {\n #raw: Context<E>;\n #url?: URL;\n #env?: EnvVars<E>;\n\n constructor(c: Context<E>) {\n this.#raw = c;\n }\n\n /**\n * The underlying Hono {@link Context}. Escape hatch for anything this wrapper does not expose — and\n * deliberately most things. `req`, `method`, `params` and a `header()` setter used to sit on this class\n * as one-line pass-throughs to `c.req`, `c.req.method`, `c.req.param()` and `c.header()`; they are\n * reachable through here (`ctx.raw.req`, `ctx.raw.header(…)`) and adding no name of their own is the\n * point. What stays below is what this wrapper actually *does*: a proxy-aware cached URL, an env that\n * merges runtime bindings over process env, and cookies without a second import.\n *\n * A getter over a private field rather than a plain property, so it is not an *own enumerable*\n * one — which matters more than it looks. React's diagnostic for a value that cannot be sent to a\n * client component (`describeObjectForErrorMessage`) walks `Object.keys` recursively with no depth\n * limit and no cycle guard, and the Hono context graph reaches the socket and the whole server\n * through `req.raw` and `env`. While this was a plain property, passing a `RequestContext` to a `'use client'`\n * component blew the stack *inside that message builder* — so React's actual, accurate \"you cannot\n * pass this\" error never got printed. Hidden from `Object.keys`, the walk stops here.\n */\n get raw(): Context<E> {\n return this.#raw;\n }\n\n /**\n * The browser-facing request URL, proxy-header aware (see {@link publicUrl}) —\n * read `url.pathname`, `url.searchParams` and the rest off it. Parsed once and\n * cached, so the same instance comes back on every read within a request; treat\n * it as read-only for that reason.\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\n * `ctx.var.user`. Type them by parameterising this class's {@link Env}.\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\n * (bindings win on conflict). Computed once and cached.\n *\n * @example `const key = getRequestContext().env.STRIPE_SECRET_KEY;`\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 // The snapshot is shared, so hand it back as-is when there are no bindings to merge over it.\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 cookies = {\n /** Reads a single cookie by name, or `undefined` if absent. */\n get: (name: string): string | undefined => getCookie(this.#raw, name),\n /** Reads every cookie as a `{ name: value }` record. */\n all: (): Record<string, string> => getCookie(this.#raw),\n /** Sets a cookie on the response. See Hono's {@link CookieOptions} for `path`, `httpOnly`, `maxAge`, etc. */\n set: (name: string, value: string, options?: CookieOptions): void => setCookie(this.#raw, name, value, options),\n /** Clears a cookie. Pass the same `path`/`domain` it was set with so the browser matches it. */\n delete: (name: string, options?: CookieOptions): void => {\n deleteCookie(this.#raw, name, options);\n },\n };\n}\n\n/**\n * Returns the {@link RequestContext} for the current request.\n *\n * This is the primary entry point for reading request data from a server\n * component or server action — the URL, cookies, params, env, and middleware\n * variables. The returned wrapper is memoised per request, so repeated calls in\n * the same request are cheap and return the same instance.\n *\n * A **page** component is handed the very same object as its `ctx` prop, so this\n * import is for everywhere else: a nested server component, or a `'use server'`\n * action module — neither of which receives props from the framework.\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 */\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 that the\n * framework catches and turns into an HTTP redirect response.\n *\n * Because it throws, it never returns — TypeScript narrows away any code after\n * the call, and you do not need to `return` it. Do not wrap it in a `try/catch`\n * 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),\n * the correct choice after a form action so the browser follows up with a `GET`.\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 not-found page.\n *\n * Like {@link redirect}, this throws a control signal and never returns, so\n * TypeScript narrows away everything after the call. Do not 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 an {@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, including a thrown endpoint route.\n */\nexport type ServerErrorSource = 'action' | 'render' | 'ssr' | 'request';\n\n/** What an {@link ServerErrorHandler} is told about an error, beyond the error itself. */\nexport interface ServerErrorContext {\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\n/** Handler registered with {@link onServerError}. Called for the side effect; its return value is ignored. */\nexport type ServerErrorHandler = (error: unknown, context: ServerErrorContext) => void;\n\nlet errorHandler: ServerErrorHandler | undefined;\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`** — that module is imported as the server\n * starts, before any request is served. Registering again replaces the previous handler.\n *\n * Errors are still written to `stderr` either way, so a handler adds a destination rather than\n * replacing one. A handler that throws is caught and logged: reporting must never be able to fail\n * a request.\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 }) => {\n * Sentry.captureException(error, { tags: { source }, extra: { url: request.url } });\n * });\n * ```\n */\nexport function onServerError(handler: ServerErrorHandler): void {\n errorHandler = handler;\n}\n\n/**\n * Logs an error and forwards it to the registered {@link ServerErrorHandler}.\n *\n * Framework internal — the single funnel every caught server-side error goes through, so that\n * adding a reporting destination is one registration rather than a hook per call site.\n *\n * @internal\n */\nexport function reportServerError(error: unknown, info: ServerErrorContext & { message: string }): void {\n console.error(info.message, error);\n if (!errorHandler) return;\n try {\n errorHandler(error, { source: info.source, request: info.request });\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":["/// <reference path=\"../types/rshono-config.d.ts\" />\n/**\n * The request context: {@link getRequestContext} and the {@link RequestContext} wrapper it returns,\n * the {@link redirect} / {@link notFound} control-flow helpers, and the\n * {@link onServerError} reporting funnel — plus the `@internal` plumbing that binds\n * a request to the async context in the first place.\n *\n * The public half of this module is re-exported by `runtime/server.ts`, which is\n * what the `@rshono/core/server` subpath resolves to; import *that* from an app. Nothing\n * here is safe in a `'use client'` module — those run in the browser, with no bound\n * request context.\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\n * you almost always want 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 in a request share its lazy getters. */\nconst wrappers = new WeakMap<Context, RequestContext>();\n\n/**\n * Requests whose page render has begun — the point past which nothing can change the response head.\n *\n * A `WeakSet` keyed on the Hono {@link Context} rather than a field on {@link RequestContext},\n * so marking a request costs nothing for the pages that never read their context: the wrapper is\n * built lazily by {@link getRequestContext} and this must not be what forces it into existence.\n */\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 * Framework internal — `renderComponent` calls this immediately before handing the page to React.\n * Everything that legitimately writes to the response (middleware, a `'use server'` action, an\n * endpoint route) has already run by then, so none of them are affected.\n *\n * @internal\n */\nexport function beginPageRender(c: Context): void {\n rendering.add(c);\n}\n\n/**\n * The shared explanation for a response mutation that arrived too late, thrown by\n * {@link RequestContext.setHeader} and the `cookies` writers.\n *\n * Worth being long: the failure it replaces was silent *and* inconsistent — a page setting a cookie\n * got it on a full page load and lost it on a soft navigation, because the flight stream's response\n * head is committed before the first line of the page component runs. There is no fixing that from\n * inside the render (React starts flight work in a microtask, and neither Hono nor the Node adapter\n * will re-read headers once the response exists), so the honest move is to refuse both ways round\n * and say where the write does belong.\n */\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 explanation for a Hono `Context` member that 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/**\n * `process.env`, snapshotted on first read.\n *\n * It is not a plain object — every enumeration crosses into the host environment, which made\n * spreading it (~20µs) by far the most expensive thing {@link RequestContext.env} did, once per request that\n * touched it. Snapshotted lazily rather than at module load because `loadEnvFiles()` runs *after*\n * this module is imported, so an eager copy would miss everything from `.env`. The trade-off: a\n * `process.env` mutation after the first `ctx.env` read is not picked up.\n */\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/**\n * True when this process is the SSG build prerendering `render: 'static'` routes,\n * rather than a server handling real requests. `build.ts` sets `RSHONO_PRERENDER`\n * before importing the app bundle and starting the prerender pass; the app bundle\n * inlines its own copy of this module, so a shared `process.env` (not a module-level\n * flag) is what reliably crosses that boundary. Read by {@link getRequestContext} to turn a\n * static route's request-context read into a clear build-time error instead of\n * silently baking synthetic build-time values (a `localhost` URL, no cookies, build\n * env) into the snapshot.\n */\nconst prerendering = typeof process !== 'undefined' && !!process.env?.RSHONO_PRERENDER;\n\n/**\n * Runs `fn` with the given Hono {@link Context} bound as the ambient request\n * context, so that {@link getRequestContext} resolves to it anywhere in the call tree.\n *\n * Framework internal — the request handler wraps every render and action in\n * this. Application code should reach for {@link getRequestContext} instead.\n *\n * @internal\n */\nexport function runWithContext<T>(c: Context, fn: () => T): T {\n return contextStorage.run(c, fn);\n}\n\n/**\n * Reads the matched route params, returning an empty object when there is no\n * active route match (rather than throwing), so the fallback behaviour stays in\n * one place.\n *\n * Framework internal — the request renderer calls this to build a page's `params`\n * prop, and {@link RequestContext.params} caches it. Read them from that prop, or\n * from `ctx.params` outside a page.\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// DefinePlugin inlines the config into the server bundle, but this module is the public\n// `@rshono/core/server` entry and could be loaded by tooling that doesn't (a unit test, a one-off script).\n// Read through `typeof` so that degrades to the safe answer — don't trust — instead of a ReferenceError.\nconst trustProxy = typeof __RSHONO_CONFIG__ !== 'undefined' && __RSHONO_CONFIG__.trustProxy;\n\n/**\n * Resolves the browser-facing {@link URL} for a request.\n *\n * `c.req.url` reflects the internal address the server was reached on, which is wrong behind a\n * proxy or load balancer. `X-Forwarded-Host` / `X-Forwarded-Proto` fix that up — **but only when\n * `trustProxy` is enabled in `rshono.config.ts`** (always the case under `rshono dev`). Those\n * headers are client-supplied: honouring them unconditionally lets anyone who can reach the server\n * dictate the origin of every absolute URL the app builds — canonical tags, emails, redirects — and\n * poison a shared cache with them. So the default is to ignore them entirely.\n *\n * Framework internal — prefer {@link RequestContext.url}, which caches the result per request.\n *\n * @internal\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 rather than assigned to `url.host`, because that setter *keeps the existing port* when\n // the new value has none — leaving the internal port on the public URL (`example.com:3000`).\n const forwarded = forwardedHost ? URL.parse(`http://${forwardedHost}`) : null;\n if (forwarded) {\n url.hostname = forwarded.hostname;\n url.port = forwarded.port; // '' when the forwarded host carries no port, which clears it\n }\n\n // Restricted to the two schemes a browser can actually have requested; anything else (a proxy\n // sending junk, or a client trying its luck) 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: Cloudflare/Workers `Bindings` merged\n * with process env vars. Values not declared in `Bindings` are typed as\n * `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 * Ergonomic, read-mostly wrapper around Hono's {@link Context} for use inside\n * server components and server actions.\n *\n * Obtain one with {@link getRequestContext}, or — in a page component — take it straight\n * off the `ctx` prop, which is this same object. Never construct it yourself. One\n * instance is reused for the lifetime of a request, so its lazy getters\n * ({@link RequestContext.url}, {@link RequestContext.env}) are computed at most once.\n *\n * @typeParam E - The Hono {@link Env} describing this app's `Bindings` and\n * `Variables`, so {@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 * Framework internal — one instance is created per request and handed to you by\n * {@link getRequestContext} or the `ctx` page 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 the long tail this wrapper does not\n * expose. Everything a page or action actually reaches for has a home of its own now\n * ({@link RequestContext.req}, {@link RequestContext.params}, {@link RequestContext.setHeader}), so\n * this is for what is left: `executionCtx.waitUntil()` on Workers, and whatever Hono adds next.\n *\n * Be aware that the response builders on it (`redirect`, `notFound`, `json`, `body`, `status`, …)\n * still do nothing from inside a page, for the reason the stubs on this class explain — reaching\n * them through here bypasses the error, it does not 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 // A getter over a private field rather than a plain property, so it is not an *own enumerable*\n // one — which matters more than it looks. React's diagnostic for a value that cannot be sent to a\n // client component (`describeObjectForErrorMessage`) walks `Object.keys` recursively with no depth\n // limit and no cycle guard, and the Hono context graph reaches the socket and the whole server\n // through `req.raw` and `env`. While this was a plain property, passing a `RequestContext` to a\n // `'use client'` component blew the stack *inside that message builder* — so React's actual,\n // accurate \"you cannot pass this\" error never got printed. Hidden from `Object.keys`, the walk\n // stops here. Every member added since is a prototype getter or method for the same reason;\n // `cookies` is the one own enumerable property, and it 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}, which is a different\n * thing living in a different place on purpose — Hono's `c.header()` writing the *response* while\n * `c.req.header()` reads the *request* is a well-worn source of confusion.\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`.\n *\n * A **page** is handed these as its `params` prop, typed key-by-key from its route path, and that\n * is the better read where it is available. This is the same record for everywhere else — a nested\n * server component, or a `'use server'` action — which get no props from the framework. Empty when\n * there is no active route match rather than throwing.\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 — read `url.pathname`, `url.searchParams` and the\n * rest off it. Parsed once and cached, so the same instance comes back on every\n * read within a request; treat it as read-only for that reason.\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 * @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\n * `ctx.var.user`. Type them by 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\n * (bindings win on conflict). 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 // The snapshot is shared, so hand it back as-is when there are no bindings to merge over it.\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`,\n * `maxAge`, etc.\n *\n * **Throws inside a page render** — see {@link RequestContext.setHeader}, of which a `Set-Cookie`\n * is a special case. Set cookies from a `'use server'` action, or with Hono's `setCookie(c, …)`\n * 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 /** Guards every write that has to reach the response head. See {@link tooLateToWrite}. */\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. By then the response head is committed: a page streams, and on a\n * soft navigation the flight response is built before the component's first line runs. Hono's\n * `c.header()` fails there silently and inconsistently — landing on a full page load, vanishing on\n * a soft navigation — so this refuses rather than doing it half the time.\n *\n * Middleware and `{ type: 'endpoint' }` routes run outside the request context (no\n * {@link getRequestContext} there) but are handed Hono's `c` directly, so they set headers with\n * `c.header(…)`. That is also where a header belonging to the *page* rather than to one action\n * goes — `Cache-Control`, `X-Robots-Tag` — since 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 that name the thing to use instead. A page returns\n // JSX and `renderComponent` builds the response from it, so every one of these is a silent no-op\n // through `ctx.hono` — which is exactly the confusion this class exists to remove. They are\n // `@deprecated` so an editor strikes them through in autocomplete: visible, and visibly wrong.\n //\n // Each takes `...args: unknown[]` it never reads, so that `ctx.redirect('/dashboard')` reaches the\n // message below instead of stopping at \"Expected 0 arguments, but got 1\" — an arity complaint that\n // says nothing about what to do. The `@deprecated` strike-through is the compile-time signal; the\n // thrown message is the one that explains.\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 * Returns the {@link RequestContext} for the current request.\n *\n * This is the primary entry point for reading request data from a server\n * component or server action — the URL, cookies, params, env, and middleware\n * variables. The returned wrapper is memoised per request, so repeated calls in\n * the same request are cheap and return the same instance.\n *\n * A **page** component is handed the very same object as its `ctx` prop, so this\n * import is for everywhere else: a nested server component, or a `'use server'`\n * action module — neither of which receives props from the framework.\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 that the\n * framework catches and turns into an HTTP redirect response.\n *\n * Because it throws, it never returns — TypeScript narrows away any code after\n * the call, and you do not need to `return` it. Do not wrap it in a `try/catch`\n * 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),\n * the correct choice after a form action so the browser follows up with a `GET`.\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 not-found page.\n *\n * Like {@link redirect}, this throws a control signal and never returns, so\n * TypeScript narrows away everything after the call. Do not 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 an {@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, including a thrown endpoint route.\n */\nexport type ServerErrorSource = 'action' | 'render' | 'ssr' | 'request';\n\n/** What an {@link ServerErrorHandler} is told about an error, beyond the error itself. */\nexport interface ServerErrorContext {\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\n/** Handler registered with {@link onServerError}. Called for the side effect; its return value is ignored. */\nexport type ServerErrorHandler = (error: unknown, context: ServerErrorContext) => void;\n\nlet errorHandler: ServerErrorHandler | undefined;\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`** — that module is imported as the server\n * starts, before any request is served. Registering again replaces the previous handler.\n *\n * Errors are still written to `stderr` either way, so a handler adds a destination rather than\n * replacing one. A handler that throws is caught and logged: reporting must never be able to fail\n * a request.\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 }) => {\n * Sentry.captureException(error, { tags: { source }, extra: { url: request.url } });\n * });\n * ```\n *\n * @see {@link https://www.rshono.com/docs/hono#error-reporting | Docs — error reporting}\n */\nexport function onServerError(handler: ServerErrorHandler): void {\n errorHandler = handler;\n}\n\n/**\n * Logs an error and forwards it to the registered {@link ServerErrorHandler}.\n *\n * Framework internal — the single funnel every caught server-side error goes through, so that\n * adding a reporting destination is one registration rather than a hook per call site.\n *\n * @internal\n */\nexport function reportServerError(error: unknown, info: ServerErrorContext & { message: string }): void {\n console.error(info.message, error);\n if (!errorHandler) return;\n try {\n errorHandler(error, { source: info.source, request: info.request });\n } catch (handlerError) {\n console.error('[rshono] the onServerError handler threw:', handlerError);\n }\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"entry.rsc.d.ts","sourceRoot":"","sources":["../../src/runtime/entry.rsc.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAG5B,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAwBvD,OAAO,EAA0F,KAAK,KAAK,EAAoB,MAAM,cAAc,CAAC;AA2CpJ,eAAO,MAAM,MAAM,EAAE,SAAS,KAAK,EAAuB,CAAC;AAE3D,0HAA0H;AAC1H,MAAM,MAAM,YAAY,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAC;AAExF,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,EAAE,KAAK,CAAC,SAAS,CAAC;IACtB,WAAW,CAAC,EAAE,YAAY,CAAC;IAC3B,SAAS,CAAC,EAAE,cAAc,CAAC;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AA0cF,eAAO,MAAM,GAAG,4EAAa,CAAC;AAE9B;;;;;;;GAOG"}
1
+ {"version":3,"file":"entry.rsc.d.ts","sourceRoot":"","sources":["../../src/runtime/entry.rsc.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAG5B,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAwBvD,OAAO,EAA0F,KAAK,KAAK,EAAoB,MAAM,cAAc,CAAC;AA2CpJ,eAAO,MAAM,MAAM,EAAE,SAAS,KAAK,EAAuB,CAAC;AAE3D,0HAA0H;AAC1H,MAAM,MAAM,YAAY,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAC;AAExF,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,EAAE,KAAK,CAAC,SAAS,CAAC;IACtB,WAAW,CAAC,EAAE,YAAY,CAAC;IAC3B,SAAS,CAAC,EAAE,cAAc,CAAC;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AAgdF,eAAO,MAAM,GAAG,4EAAa,CAAC;AAE9B;;;;;;;GAOG"}
@@ -17,7 +17,7 @@ import { routes as userRoutes } from '@rshono/routes';
17
17
  import * as serverAppModule from '@rshono/server-app';
18
18
  import { isPageRoute } from '../router.js';
19
19
  import { appendVary, etagMatches } from '../server/headers.js';
20
- import { getRequestContext, publicUrl, readParams, reportServerError, runWithContext } from './context.js';
20
+ import { beginPageRender, getRequestContext, publicUrl, readParams, reportServerError, runWithContext } from './context.js';
21
21
  import { isControlSignal, RedirectSignal } from './control.js';
22
22
  import { renderHTML } from './entry.ssr.js';
23
23
  import { RouterProvider } from './navigation.js';
@@ -170,7 +170,7 @@ function releaseWhenDone(stream, done) {
170
170
  * prerendering" error rather than a bare `undefined` — one explanation, in one place.
171
171
  * - **Non-enumerable**, so React's *development-only* serialization of a server component's props
172
172
  * (the debug channel behind component stacks and the performance track) skips it. That walks own
173
- * enumerable properties, and `ctx.raw` is the Hono {@link Context} — whose `env` holds the
173
+ * enumerable properties, and `ctx.hono` is the Hono {@link Context} — whose `env` holds the
174
174
  * runtime's bindings. An enumerable `ctx` ships every one of them, secrets included, to the
175
175
  * browser in dev, and grows a small page's flight payload by well over 10 kB. Production never
176
176
  * serializes a server component's props at all, so this is the dev half of the same guarantee.
@@ -211,6 +211,11 @@ async function renderComponent(c, Page, opts) {
211
211
  const root = (_jsxs(_Fragment, { children: [nonce && _jsx("meta", { property: "csp-nonce", nonce: nonce }), Page.entryCssFiles?.map((href) => (_jsx("link", { rel: "stylesheet", href: href, precedence: "default" }, href))), _jsx(RouterProvider, { href: props.url.href, params: props.params, children: jsx(Page, props) })] }));
212
212
  // `notFound` only when it is true, so the flight payload of an ordinary page doesn't carry the key.
213
213
  const rscPayload = { root, formState: opts.formState, returnValue: opts.returnValue, ...(opts.notFound ? { notFound: true } : null) };
214
+ // Past this line the response head belongs to the framework, so `ctx.setHeader()` and
215
+ // `ctx.cookies.set()` start throwing rather than writing somewhere nothing will read. It has to be
216
+ // the last thing before the render: a server action ran earlier in `renderPage` and legitimately
217
+ // writes to the response, and so does any middleware that wrapped this handler.
218
+ beginPageRender(c);
214
219
  let controlSignal;
215
220
  const rscStream = renderToReadableStream(rscPayload, {
216
221
  temporaryReferences: opts.temporaryReferences,