@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.
- package/README.md +202 -159
- package/dist/builder/page-files.d.ts.map +1 -1
- package/dist/builder/page-files.js +7 -3
- package/dist/builder/page-files.js.map +1 -1
- package/dist/builder/public-env.d.ts +6 -0
- package/dist/builder/public-env.d.ts.map +1 -1
- package/dist/builder/public-env.js +6 -0
- package/dist/builder/public-env.js.map +1 -1
- package/dist/builder/rspack-config.d.ts +3 -3
- package/dist/builder/rspack-config.d.ts.map +1 -1
- package/dist/builder/rspack-config.js +62 -16
- package/dist/builder/rspack-config.js.map +1 -1
- package/dist/cli/build.d.ts +2 -2
- package/dist/cli/build.js.map +1 -1
- package/dist/cli/dev.d.ts +2 -2
- package/dist/cli/dev.d.ts.map +1 -1
- package/dist/cli/dev.js +63 -35
- package/dist/cli/dev.js.map +1 -1
- package/dist/cli/index.js +10 -10
- package/dist/cli/index.js.map +1 -1
- package/dist/config.d.ts +58 -70
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +17 -1
- package/dist/config.js.map +1 -1
- package/dist/deploy/cloudflare/build.js +1 -1
- package/dist/deploy/cloudflare/build.js.map +1 -1
- package/dist/deploy/cloudflare/runtime.d.ts.map +1 -1
- package/dist/deploy/cloudflare/runtime.js +7 -37
- package/dist/deploy/cloudflare/runtime.js.map +1 -1
- package/dist/deploy/contract.d.ts +22 -16
- package/dist/deploy/contract.d.ts.map +1 -1
- package/dist/deploy/contract.js.map +1 -1
- package/dist/deploy/filesystem.d.ts +1 -1
- package/dist/deploy/filesystem.d.ts.map +1 -1
- package/dist/deploy/filesystem.js +7 -10
- package/dist/deploy/filesystem.js.map +1 -1
- package/dist/deploy/node/runtime.d.ts +4 -0
- package/dist/deploy/node/runtime.d.ts.map +1 -1
- package/dist/deploy/node/runtime.js +24 -3
- package/dist/deploy/node/runtime.js.map +1 -1
- package/dist/deploy/presets.d.ts +3 -3
- package/dist/deploy/presets.d.ts.map +1 -1
- package/dist/deploy/presets.js +13 -30
- package/dist/deploy/presets.js.map +1 -1
- package/dist/deploy/vercel/runtime.d.ts.map +1 -1
- package/dist/deploy/vercel/runtime.js +0 -3
- package/dist/deploy/vercel/runtime.js.map +1 -1
- package/dist/index.d.ts +6 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -3
- package/dist/index.js.map +1 -1
- package/dist/router.d.ts +85 -48
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +4 -6
- package/dist/router.js.map +1 -1
- package/dist/runtime/boundaries.d.ts +39 -25
- package/dist/runtime/boundaries.d.ts.map +1 -1
- package/dist/runtime/boundaries.js +22 -17
- package/dist/runtime/boundaries.js.map +1 -1
- package/dist/runtime/client.d.ts +9 -8
- package/dist/runtime/client.d.ts.map +1 -1
- package/dist/runtime/client.js +9 -8
- package/dist/runtime/client.js.map +1 -1
- package/dist/runtime/context.d.ts +216 -70
- package/dist/runtime/context.d.ts.map +1 -1
- package/dist/runtime/context.js +299 -93
- package/dist/runtime/context.js.map +1 -1
- package/dist/runtime/control.d.ts.map +1 -1
- package/dist/runtime/control.js +7 -0
- package/dist/runtime/control.js.map +1 -1
- package/dist/runtime/dev-protocol.d.ts.map +1 -1
- package/dist/runtime/dev-protocol.js.map +1 -1
- package/dist/runtime/entry.client.d.ts +4 -0
- package/dist/runtime/entry.client.d.ts.map +1 -1
- package/dist/runtime/entry.client.js +190 -154
- package/dist/runtime/entry.client.js.map +1 -1
- package/dist/runtime/entry.rsc.d.ts.map +1 -1
- package/dist/runtime/entry.rsc.js +145 -159
- package/dist/runtime/entry.rsc.js.map +1 -1
- package/dist/runtime/entry.ssr.d.ts +9 -1
- package/dist/runtime/entry.ssr.d.ts.map +1 -1
- package/dist/runtime/entry.ssr.js +36 -18
- package/dist/runtime/entry.ssr.js.map +1 -1
- package/dist/runtime/flight-inject.d.ts +31 -0
- package/dist/runtime/flight-inject.d.ts.map +1 -0
- package/dist/runtime/flight-inject.js +221 -0
- package/dist/runtime/flight-inject.js.map +1 -0
- package/dist/runtime/navigation.d.ts +20 -38
- package/dist/runtime/navigation.d.ts.map +1 -1
- package/dist/runtime/navigation.js +10 -53
- package/dist/runtime/navigation.js.map +1 -1
- package/dist/runtime/request.d.ts +6 -0
- package/dist/runtime/request.d.ts.map +1 -1
- package/dist/runtime/request.js +8 -0
- package/dist/runtime/request.js.map +1 -1
- package/dist/runtime/server.d.ts +7 -12
- package/dist/runtime/server.d.ts.map +1 -1
- package/dist/runtime/server.js +15 -12
- package/dist/runtime/server.js.map +1 -1
- package/dist/server/headers.d.ts +9 -9
- package/dist/server/headers.js +9 -9
- package/dist/server/headers.js.map +1 -1
- package/dist/server/load-config.d.ts +2 -2
- package/dist/server/load-config.d.ts.map +1 -1
- package/dist/server/load-config.js +22 -11
- package/dist/server/load-config.js.map +1 -1
- package/dist/server/prerendered.d.ts +43 -15
- package/dist/server/prerendered.d.ts.map +1 -1
- package/dist/server/prerendered.js +47 -0
- package/dist/server/prerendered.js.map +1 -1
- package/dist/server/server-config.d.ts +13 -39
- package/dist/server/server-config.d.ts.map +1 -1
- package/dist/server/server-config.js +5 -74
- package/dist/server/server-config.js.map +1 -1
- package/dist/server/ssg.d.ts +2 -2
- package/dist/server/ssg.d.ts.map +1 -1
- package/dist/server/ssg.js +21 -34
- package/dist/server/ssg.js.map +1 -1
- package/package.json +13 -16
- package/dist/deploy/bun/runtime.d.ts +0 -11
- package/dist/deploy/bun/runtime.d.ts.map +0 -1
- package/dist/deploy/bun/runtime.js +0 -22
- package/dist/deploy/bun/runtime.js.map +0 -1
- package/dist/deploy/deno/runtime.d.ts +0 -11
- package/dist/deploy/deno/runtime.d.ts.map +0 -1
- package/dist/deploy/deno/runtime.js +0 -16
- package/dist/deploy/deno/runtime.js.map +0 -1
- package/dist/deploy/listen.d.ts +0 -20
- package/dist/deploy/listen.d.ts.map +0 -1
- package/dist/deploy/listen.js +0 -24
- package/dist/deploy/listen.js.map +0 -1
- package/dist/deploy/netlify/build.d.ts +0 -8
- package/dist/deploy/netlify/build.d.ts.map +0 -1
- package/dist/deploy/netlify/build.js +0 -52
- package/dist/deploy/netlify/build.js.map +0 -1
- package/dist/deploy/netlify/runtime.d.ts +0 -13
- package/dist/deploy/netlify/runtime.d.ts.map +0 -1
- package/dist/deploy/netlify/runtime.js +0 -24
- package/dist/deploy/netlify/runtime.js.map +0 -1
- package/dist/server/compress.d.ts +0 -15
- package/dist/server/compress.d.ts.map +0 -1
- package/dist/server/compress.js +0 -76
- package/dist/server/compress.js.map +0 -1
package/dist/runtime/context.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/// <reference path="../types/rshono-config.d.ts" />
|
|
2
2
|
/**
|
|
3
|
-
* The request context: {@link
|
|
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
|
|
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
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* `
|
|
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
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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 —
|
|
61
|
-
*
|
|
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
|
-
*
|
|
87
|
-
* `
|
|
88
|
-
*
|
|
89
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
|
180
|
+
* `Variables`, so {@link RequestContext.var} and {@link RequestContext.env} stay typed.
|
|
126
181
|
*
|
|
127
182
|
* @example
|
|
128
183
|
* ```tsx
|
|
129
|
-
* import {
|
|
184
|
+
* import { getRequestContext } from '@rshono/core/server';
|
|
130
185
|
*
|
|
131
186
|
* export default async function Whoami() {
|
|
132
|
-
* const ctx =
|
|
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
|
|
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}
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
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
|
|
170
|
-
return (this.#
|
|
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
|
-
*
|
|
178
|
-
*
|
|
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
|
|
181
|
-
return
|
|
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 =
|
|
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 =
|
|
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
|
-
/**
|
|
224
|
-
|
|
225
|
-
|
|
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
|
|
233
|
-
*
|
|
234
|
-
*
|
|
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
|
|
240
|
-
*
|
|
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
|
|
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 {
|
|
451
|
+
* import { getRequestContext, redirect } from '@rshono/core/server';
|
|
252
452
|
*
|
|
253
453
|
* export async function login(form: FormData) {
|
|
254
|
-
*
|
|
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
|
|
461
|
+
export function getRequestContext() {
|
|
260
462
|
if (prerendering) {
|
|
261
|
-
throw new Error("[rshono]
|
|
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
|
|
466
|
+
'the getRequestContext() call.');
|
|
265
467
|
}
|
|
266
468
|
const c = contextStorage.getStore();
|
|
267
469
|
if (!c) {
|
|
268
|
-
throw new Error('[rshono]
|
|
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
|
|
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 =
|
|
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
|
-
*
|
|
308
|
-
*
|
|
309
|
-
*
|
|
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;
|