@rshono/core 1.0.0-rc.2 → 1.0.0-rc.4
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 +45 -29
- package/dist/builder/rspack-config.d.ts +3 -3
- package/dist/builder/rspack-config.js +3 -3
- 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.js.map +1 -1
- package/dist/cli/index.js +11 -10
- package/dist/cli/index.js.map +1 -1
- package/dist/config.d.ts +4 -17
- package/dist/config.d.ts.map +1 -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 +14 -13
- 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 +1 -3
- package/dist/deploy/filesystem.js.map +1 -1
- package/dist/deploy/node/runtime.d.ts +6 -2
- package/dist/deploy/node/runtime.d.ts.map +1 -1
- package/dist/deploy/node/runtime.js +30 -5
- package/dist/deploy/node/runtime.js.map +1 -1
- package/dist/deploy/presets.d.ts +5 -1
- package/dist/deploy/presets.d.ts.map +1 -1
- package/dist/deploy/presets.js +15 -28
- 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 +4 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -3
- package/dist/index.js.map +1 -1
- package/dist/router.d.ts +14 -15
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +5 -6
- package/dist/router.js.map +1 -1
- package/dist/runtime/client.d.ts +5 -6
- package/dist/runtime/client.d.ts.map +1 -1
- package/dist/runtime/client.js +5 -6
- package/dist/runtime/client.js.map +1 -1
- package/dist/runtime/context.d.ts +36 -40
- package/dist/runtime/context.d.ts.map +1 -1
- package/dist/runtime/context.js +41 -53
- package/dist/runtime/context.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 +161 -147
- 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 +80 -63
- 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 +10 -39
- package/dist/runtime/navigation.d.ts.map +1 -1
- package/dist/runtime/navigation.js +7 -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 +2 -2
- package/dist/runtime/server.d.ts.map +1 -1
- package/dist/runtime/server.js +2 -2
- 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 +52 -9
- package/dist/server/prerendered.d.ts.map +1 -1
- package/dist/server/prerendered.js +57 -0
- package/dist/server/prerendered.js.map +1 -1
- package/dist/server/server-config.d.ts +11 -18
- package/dist/server/server-config.d.ts.map +1 -1
- package/dist/server/server-config.js +9 -12
- 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 +11 -23
- 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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The request context: {@link
|
|
2
|
+
* The request context: {@link getRequestContext} and the {@link RequestContext} wrapper it returns,
|
|
3
3
|
* the {@link redirect} / {@link notFound} control-flow helpers, and the
|
|
4
4
|
* {@link onServerError} reporting funnel — plus the `@internal` plumbing that binds
|
|
5
5
|
* a request to the async context in the first place.
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* here is safe in a `'use client'` module — those run in the browser, with no bound
|
|
10
10
|
* request context.
|
|
11
11
|
*/
|
|
12
|
-
import type { Context, Env
|
|
12
|
+
import type { Context, Env } from 'hono';
|
|
13
13
|
import type { CookieOptions } from 'hono/utils/cookie';
|
|
14
14
|
/**
|
|
15
15
|
* HTTP status codes accepted by {@link redirect}.
|
|
@@ -22,21 +22,21 @@ import type { CookieOptions } from 'hono/utils/cookie';
|
|
|
22
22
|
export type RedirectStatus = 301 | 302 | 303 | 307 | 308;
|
|
23
23
|
/**
|
|
24
24
|
* Runs `fn` with the given Hono {@link Context} bound as the ambient request
|
|
25
|
-
* context, so that {@link
|
|
25
|
+
* context, so that {@link getRequestContext} resolves to it anywhere in the call tree.
|
|
26
26
|
*
|
|
27
27
|
* Framework internal — the request handler wraps every render and action in
|
|
28
|
-
* this. Application code should reach for {@link
|
|
28
|
+
* this. Application code should reach for {@link getRequestContext} instead.
|
|
29
29
|
*
|
|
30
30
|
* @internal
|
|
31
31
|
*/
|
|
32
32
|
export declare function runWithContext<T>(c: Context, fn: () => T): T;
|
|
33
33
|
/**
|
|
34
34
|
* Reads the matched route params, returning an empty object when there is no
|
|
35
|
-
* active route match (rather than throwing)
|
|
36
|
-
*
|
|
35
|
+
* active route match (rather than throwing), so the fallback behaviour stays in
|
|
36
|
+
* one place.
|
|
37
37
|
*
|
|
38
|
-
* Framework internal —
|
|
39
|
-
* `
|
|
38
|
+
* Framework internal — the request renderer calls this to build a page's `params`
|
|
39
|
+
* prop. Read them from that prop (or `ctx.raw.req.param()` outside a page) instead.
|
|
40
40
|
*
|
|
41
41
|
* @internal
|
|
42
42
|
*/
|
|
@@ -51,7 +51,7 @@ export declare function readParams(c: Context): Record<string, string>;
|
|
|
51
51
|
* dictate the origin of every absolute URL the app builds — canonical tags, emails, redirects — and
|
|
52
52
|
* poison a shared cache with them. So the default is to ignore them entirely.
|
|
53
53
|
*
|
|
54
|
-
* Framework internal — prefer {@link
|
|
54
|
+
* Framework internal — prefer {@link RequestContext.url}, which caches the result per request.
|
|
55
55
|
*
|
|
56
56
|
* @internal
|
|
57
57
|
*/
|
|
@@ -59,49 +59,52 @@ export declare function publicUrl(c: Context): URL;
|
|
|
59
59
|
/**
|
|
60
60
|
* The environment available to a request: Cloudflare/Workers `Bindings` merged
|
|
61
61
|
* with process env vars. Values not declared in `Bindings` are typed as
|
|
62
|
-
* `string | undefined`. See {@link
|
|
62
|
+
* `string | undefined`. See {@link RequestContext.env}.
|
|
63
63
|
*/
|
|
64
64
|
export type EnvVars<E extends Env> = E['Bindings'] & Record<string, string | undefined>;
|
|
65
65
|
/**
|
|
66
66
|
* Ergonomic, read-mostly wrapper around Hono's {@link Context} for use inside
|
|
67
67
|
* server components and server actions.
|
|
68
68
|
*
|
|
69
|
-
* Obtain one with {@link
|
|
69
|
+
* Obtain one with {@link getRequestContext}, or — in a page component — take it straight
|
|
70
70
|
* off the `ctx` prop, which is this same object. Never construct it yourself. One
|
|
71
71
|
* instance is reused for the lifetime of a request, so its lazy getters
|
|
72
|
-
* ({@link
|
|
72
|
+
* ({@link RequestContext.url}, {@link RequestContext.env}) are computed at most once.
|
|
73
73
|
*
|
|
74
74
|
* @typeParam E - The Hono {@link Env} describing this app's `Bindings` and
|
|
75
|
-
* `Variables`, so {@link
|
|
75
|
+
* `Variables`, so {@link RequestContext.var} and {@link RequestContext.env} stay typed.
|
|
76
76
|
*
|
|
77
77
|
* @example
|
|
78
78
|
* ```tsx
|
|
79
|
-
* import {
|
|
79
|
+
* import { getRequestContext } from '@rshono/core/server';
|
|
80
80
|
*
|
|
81
81
|
* export default async function Whoami() {
|
|
82
|
-
* const ctx =
|
|
82
|
+
* const ctx = getRequestContext();
|
|
83
83
|
* const session = ctx.cookies.get('session');
|
|
84
84
|
* return <p>{ctx.url.pathname} — {session ?? 'anonymous'}</p>;
|
|
85
85
|
* }
|
|
86
86
|
* ```
|
|
87
87
|
*/
|
|
88
|
-
export declare class
|
|
88
|
+
export declare class RequestContext<E extends Env = Env> {
|
|
89
89
|
#private;
|
|
90
90
|
constructor(c: Context<E>);
|
|
91
91
|
/**
|
|
92
|
-
* The underlying Hono {@link Context}. Escape hatch for anything this wrapper does not expose
|
|
92
|
+
* The underlying Hono {@link Context}. Escape hatch for anything this wrapper does not expose — and
|
|
93
|
+
* deliberately most things. `req`, `method`, `params` and a `header()` setter used to sit on this class
|
|
94
|
+
* as one-line pass-throughs to `c.req`, `c.req.method`, `c.req.param()` and `c.header()`; they are
|
|
95
|
+
* reachable through here (`ctx.raw.req`, `ctx.raw.header(…)`) and adding no name of their own is the
|
|
96
|
+
* point. What stays below is what this wrapper actually *does*: a proxy-aware cached URL, an env that
|
|
97
|
+
* merges runtime bindings over process env, and cookies without a second import.
|
|
93
98
|
*
|
|
94
99
|
* A getter over a private field rather than a plain property, so it is not an *own enumerable*
|
|
95
100
|
* one — which matters more than it looks. React's diagnostic for a value that cannot be sent to a
|
|
96
101
|
* client component (`describeObjectForErrorMessage`) walks `Object.keys` recursively with no depth
|
|
97
102
|
* limit and no cycle guard, and the Hono context graph reaches the socket and the whole server
|
|
98
|
-
* through `req.raw` and `env`. While this was a plain property, passing a `
|
|
103
|
+
* through `req.raw` and `env`. While this was a plain property, passing a `RequestContext` to a `'use client'`
|
|
99
104
|
* component blew the stack *inside that message builder* — so React's actual, accurate "you cannot
|
|
100
105
|
* pass this" error never got printed. Hidden from `Object.keys`, the walk stops here.
|
|
101
106
|
*/
|
|
102
107
|
get raw(): Context<E>;
|
|
103
|
-
/** The parsed Hono request (`c.req`) — headers, body parsing, param access, etc. */
|
|
104
|
-
get req(): HonoRequest;
|
|
105
108
|
/**
|
|
106
109
|
* The browser-facing request URL, proxy-header aware (see {@link publicUrl}) —
|
|
107
110
|
* read `url.pathname`, `url.searchParams` and the rest off it. Parsed once and
|
|
@@ -109,13 +112,6 @@ export declare class Ctx<E extends Env = Env> {
|
|
|
109
112
|
* it as read-only for that reason.
|
|
110
113
|
*/
|
|
111
114
|
get url(): URL;
|
|
112
|
-
/** The HTTP method of the request, e.g. `GET` or `POST`. */
|
|
113
|
-
get method(): string;
|
|
114
|
-
/**
|
|
115
|
-
* Matched route params, e.g. `{ id }` for a `/users/[id]` route. Returns an
|
|
116
|
-
* empty object when there is no active route match (rather than throwing).
|
|
117
|
-
*/
|
|
118
|
-
get params(): Record<string, string>;
|
|
119
115
|
/**
|
|
120
116
|
* Typed variables set by middleware via `c.set('user', …)`, read here as
|
|
121
117
|
* `ctx.var.user`. Type them by parameterising this class's {@link Env}.
|
|
@@ -125,17 +121,15 @@ export declare class Ctx<E extends Env = Env> {
|
|
|
125
121
|
* Environment for the request: process env vars merged with runtime bindings
|
|
126
122
|
* (bindings win on conflict). Computed once and cached.
|
|
127
123
|
*
|
|
128
|
-
* @example `const key =
|
|
124
|
+
* @example `const key = getRequestContext().env.STRIPE_SECRET_KEY;`
|
|
129
125
|
*/
|
|
130
126
|
get env(): EnvVars<E>;
|
|
131
|
-
/** Sets a response header. Thin pass-through to `c.header(name, value)`. */
|
|
132
|
-
header(name: string, value: string): void;
|
|
133
127
|
/**
|
|
134
128
|
* Read and write request/response cookies.
|
|
135
129
|
*
|
|
136
130
|
* @example
|
|
137
131
|
* ```ts
|
|
138
|
-
* const ctx =
|
|
132
|
+
* const ctx = getRequestContext();
|
|
139
133
|
* ctx.cookies.get('session'); // string | undefined
|
|
140
134
|
* ctx.cookies.set('session', id, { httpOnly: true, sameSite: 'Lax', path: '/' });
|
|
141
135
|
* ctx.cookies.delete('session', { path: '/' });
|
|
@@ -153,7 +147,7 @@ export declare class Ctx<E extends Env = Env> {
|
|
|
153
147
|
};
|
|
154
148
|
}
|
|
155
149
|
/**
|
|
156
|
-
* Returns the {@link
|
|
150
|
+
* Returns the {@link RequestContext} for the current request.
|
|
157
151
|
*
|
|
158
152
|
* This is the primary entry point for reading request data from a server
|
|
159
153
|
* component or server action — the URL, cookies, params, env, and middleware
|
|
@@ -164,7 +158,7 @@ export declare class Ctx<E extends Env = Env> {
|
|
|
164
158
|
* import is for everywhere else: a nested server component, or a `'use server'`
|
|
165
159
|
* action module — neither of which receives props from the framework.
|
|
166
160
|
*
|
|
167
|
-
* @typeParam E - The app's Hono {@link Env}, to type {@link
|
|
161
|
+
* @typeParam E - The app's Hono {@link Env}, to type {@link RequestContext.var} and {@link RequestContext.env}.
|
|
168
162
|
* @throws If called at module load, where there is no ambient context to resolve.
|
|
169
163
|
* @throws If called while prerendering a `render: 'static'` route, which has no
|
|
170
164
|
* per-request context at build time — mark the route `render: 'dynamic'` instead.
|
|
@@ -172,15 +166,15 @@ export declare class Ctx<E extends Env = Env> {
|
|
|
172
166
|
* @example
|
|
173
167
|
* ```ts
|
|
174
168
|
* 'use server';
|
|
175
|
-
* import {
|
|
169
|
+
* import { getRequestContext, redirect } from '@rshono/core/server';
|
|
176
170
|
*
|
|
177
171
|
* export async function login(form: FormData) {
|
|
178
|
-
*
|
|
172
|
+
* getRequestContext().cookies.set('session', String(form.get('email')), { httpOnly: true });
|
|
179
173
|
* redirect('/dashboard');
|
|
180
174
|
* }
|
|
181
175
|
* ```
|
|
182
176
|
*/
|
|
183
|
-
export declare function
|
|
177
|
+
export declare function getRequestContext<E extends Env = Env>(): RequestContext<E>;
|
|
184
178
|
/**
|
|
185
179
|
* Redirects the request to `location` by throwing a control signal that the
|
|
186
180
|
* framework catches and turns into an HTTP redirect response.
|
|
@@ -195,7 +189,7 @@ export declare function getContext<E extends Env = Env>(): Ctx<E>;
|
|
|
195
189
|
*
|
|
196
190
|
* @example
|
|
197
191
|
* ```ts
|
|
198
|
-
* const session =
|
|
192
|
+
* const session = getRequestContext().cookies.get('session');
|
|
199
193
|
* if (!session) redirect('/login');
|
|
200
194
|
* // session is defined below this line
|
|
201
195
|
* ```
|
|
@@ -209,9 +203,11 @@ export declare function redirect(location: string, status?: RedirectStatus): nev
|
|
|
209
203
|
*
|
|
210
204
|
* @example
|
|
211
205
|
* ```tsx
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
206
|
+
* export default async function Page({ params }: PageProps<'/users/:id'>) {
|
|
207
|
+
* const user = await db.user.find(params.id);
|
|
208
|
+
* if (!user) notFound();
|
|
209
|
+
* return <Profile user={user} />; // user is non-null here
|
|
210
|
+
* }
|
|
215
211
|
* ```
|
|
216
212
|
*/
|
|
217
213
|
export declare function notFound(): never;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/runtime/context.ts"],"names":[],"mappings":"AACA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/runtime/context.ts"],"names":[],"mappings":"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"}
|
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,13 +14,13 @@ 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
19
|
/**
|
|
20
20
|
* `process.env`, snapshotted on first read.
|
|
21
21
|
*
|
|
22
22
|
* It is not a plain object — every enumeration crosses into the host environment, which made
|
|
23
|
-
* spreading it (~20µs) by far the most expensive thing {@link
|
|
23
|
+
* spreading it (~20µs) by far the most expensive thing {@link RequestContext.env} did, once per request that
|
|
24
24
|
* touched it. Snapshotted lazily rather than at module load because `loadEnvFiles()` runs *after*
|
|
25
25
|
* this module is imported, so an eager copy would miss everything from `.env`. The trade-off: a
|
|
26
26
|
* `process.env` mutation after the first `ctx.env` read is not picked up.
|
|
@@ -34,7 +34,7 @@ function processEnv() {
|
|
|
34
34
|
* rather than a server handling real requests. `build.ts` sets `RSHONO_PRERENDER`
|
|
35
35
|
* before importing the app bundle and starting the prerender pass; the app bundle
|
|
36
36
|
* inlines its own copy of this module, so a shared `process.env` (not a module-level
|
|
37
|
-
* flag) is what reliably crosses that boundary. Read by {@link
|
|
37
|
+
* flag) is what reliably crosses that boundary. Read by {@link getRequestContext} to turn a
|
|
38
38
|
* static route's request-context read into a clear build-time error instead of
|
|
39
39
|
* silently baking synthetic build-time values (a `localhost` URL, no cookies, build
|
|
40
40
|
* env) into the snapshot.
|
|
@@ -42,10 +42,10 @@ function processEnv() {
|
|
|
42
42
|
const prerendering = typeof process !== 'undefined' && !!process.env?.RSHONO_PRERENDER;
|
|
43
43
|
/**
|
|
44
44
|
* Runs `fn` with the given Hono {@link Context} bound as the ambient request
|
|
45
|
-
* context, so that {@link
|
|
45
|
+
* context, so that {@link getRequestContext} resolves to it anywhere in the call tree.
|
|
46
46
|
*
|
|
47
47
|
* Framework internal — the request handler wraps every render and action in
|
|
48
|
-
* this. Application code should reach for {@link
|
|
48
|
+
* this. Application code should reach for {@link getRequestContext} instead.
|
|
49
49
|
*
|
|
50
50
|
* @internal
|
|
51
51
|
*/
|
|
@@ -54,11 +54,11 @@ export function runWithContext(c, fn) {
|
|
|
54
54
|
}
|
|
55
55
|
/**
|
|
56
56
|
* Reads the matched route params, returning an empty object when there is no
|
|
57
|
-
* active route match (rather than throwing)
|
|
58
|
-
*
|
|
57
|
+
* active route match (rather than throwing), so the fallback behaviour stays in
|
|
58
|
+
* one place.
|
|
59
59
|
*
|
|
60
|
-
* Framework internal —
|
|
61
|
-
* `
|
|
60
|
+
* 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.
|
|
62
62
|
*
|
|
63
63
|
* @internal
|
|
64
64
|
*/
|
|
@@ -89,7 +89,7 @@ const trustProxy = typeof __RSHONO_CONFIG__ !== 'undefined' && __RSHONO_CONFIG__
|
|
|
89
89
|
* dictate the origin of every absolute URL the app builds — canonical tags, emails, redirects — and
|
|
90
90
|
* poison a shared cache with them. So the default is to ignore them entirely.
|
|
91
91
|
*
|
|
92
|
-
* Framework internal — prefer {@link
|
|
92
|
+
* Framework internal — prefer {@link RequestContext.url}, which caches the result per request.
|
|
93
93
|
*
|
|
94
94
|
* @internal
|
|
95
95
|
*/
|
|
@@ -116,26 +116,26 @@ export function publicUrl(c) {
|
|
|
116
116
|
* Ergonomic, read-mostly wrapper around Hono's {@link Context} for use inside
|
|
117
117
|
* server components and server actions.
|
|
118
118
|
*
|
|
119
|
-
* Obtain one with {@link
|
|
119
|
+
* Obtain one with {@link getRequestContext}, or — in a page component — take it straight
|
|
120
120
|
* off the `ctx` prop, which is this same object. Never construct it yourself. One
|
|
121
121
|
* instance is reused for the lifetime of a request, so its lazy getters
|
|
122
|
-
* ({@link
|
|
122
|
+
* ({@link RequestContext.url}, {@link RequestContext.env}) are computed at most once.
|
|
123
123
|
*
|
|
124
124
|
* @typeParam E - The Hono {@link Env} describing this app's `Bindings` and
|
|
125
|
-
* `Variables`, so {@link
|
|
125
|
+
* `Variables`, so {@link RequestContext.var} and {@link RequestContext.env} stay typed.
|
|
126
126
|
*
|
|
127
127
|
* @example
|
|
128
128
|
* ```tsx
|
|
129
|
-
* import {
|
|
129
|
+
* import { getRequestContext } from '@rshono/core/server';
|
|
130
130
|
*
|
|
131
131
|
* export default async function Whoami() {
|
|
132
|
-
* const ctx =
|
|
132
|
+
* const ctx = getRequestContext();
|
|
133
133
|
* const session = ctx.cookies.get('session');
|
|
134
134
|
* return <p>{ctx.url.pathname} — {session ?? 'anonymous'}</p>;
|
|
135
135
|
* }
|
|
136
136
|
* ```
|
|
137
137
|
*/
|
|
138
|
-
export class
|
|
138
|
+
export class RequestContext {
|
|
139
139
|
#raw;
|
|
140
140
|
#url;
|
|
141
141
|
#env;
|
|
@@ -143,23 +143,24 @@ export class Ctx {
|
|
|
143
143
|
this.#raw = c;
|
|
144
144
|
}
|
|
145
145
|
/**
|
|
146
|
-
* The underlying Hono {@link Context}. Escape hatch for anything this wrapper does not expose
|
|
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.
|
|
147
152
|
*
|
|
148
153
|
* A getter over a private field rather than a plain property, so it is not an *own enumerable*
|
|
149
154
|
* one — which matters more than it looks. React's diagnostic for a value that cannot be sent to a
|
|
150
155
|
* client component (`describeObjectForErrorMessage`) walks `Object.keys` recursively with no depth
|
|
151
156
|
* 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 `
|
|
157
|
+
* through `req.raw` and `env`. While this was a plain property, passing a `RequestContext` to a `'use client'`
|
|
153
158
|
* component blew the stack *inside that message builder* — so React's actual, accurate "you cannot
|
|
154
159
|
* pass this" error never got printed. Hidden from `Object.keys`, the walk stops here.
|
|
155
160
|
*/
|
|
156
161
|
get raw() {
|
|
157
162
|
return this.#raw;
|
|
158
163
|
}
|
|
159
|
-
/** The parsed Hono request (`c.req`) — headers, body parsing, param access, etc. */
|
|
160
|
-
get req() {
|
|
161
|
-
return this.#raw.req;
|
|
162
|
-
}
|
|
163
164
|
/**
|
|
164
165
|
* The browser-facing request URL, proxy-header aware (see {@link publicUrl}) —
|
|
165
166
|
* read `url.pathname`, `url.searchParams` and the rest off it. Parsed once and
|
|
@@ -169,17 +170,6 @@ export class Ctx {
|
|
|
169
170
|
get url() {
|
|
170
171
|
return (this.#url ??= publicUrl(this.#raw));
|
|
171
172
|
}
|
|
172
|
-
/** The HTTP method of the request, e.g. `GET` or `POST`. */
|
|
173
|
-
get method() {
|
|
174
|
-
return this.#raw.req.method;
|
|
175
|
-
}
|
|
176
|
-
/**
|
|
177
|
-
* Matched route params, e.g. `{ id }` for a `/users/[id]` route. Returns an
|
|
178
|
-
* empty object when there is no active route match (rather than throwing).
|
|
179
|
-
*/
|
|
180
|
-
get params() {
|
|
181
|
-
return readParams(this.#raw);
|
|
182
|
-
}
|
|
183
173
|
/**
|
|
184
174
|
* Typed variables set by middleware via `c.set('user', …)`, read here as
|
|
185
175
|
* `ctx.var.user`. Type them by parameterising this class's {@link Env}.
|
|
@@ -191,7 +181,7 @@ export class Ctx {
|
|
|
191
181
|
* Environment for the request: process env vars merged with runtime bindings
|
|
192
182
|
* (bindings win on conflict). Computed once and cached.
|
|
193
183
|
*
|
|
194
|
-
* @example `const key =
|
|
184
|
+
* @example `const key = getRequestContext().env.STRIPE_SECRET_KEY;`
|
|
195
185
|
*/
|
|
196
186
|
get env() {
|
|
197
187
|
if (this.#env)
|
|
@@ -200,16 +190,12 @@ export class Ctx {
|
|
|
200
190
|
// The snapshot is shared, so hand it back as-is when there are no bindings to merge over it.
|
|
201
191
|
return (this.#env = (bindings ? { ...processEnv(), ...bindings } : processEnv()));
|
|
202
192
|
}
|
|
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
193
|
/**
|
|
208
194
|
* Read and write request/response cookies.
|
|
209
195
|
*
|
|
210
196
|
* @example
|
|
211
197
|
* ```ts
|
|
212
|
-
* const ctx =
|
|
198
|
+
* const ctx = getRequestContext();
|
|
213
199
|
* ctx.cookies.get('session'); // string | undefined
|
|
214
200
|
* ctx.cookies.set('session', id, { httpOnly: true, sameSite: 'Lax', path: '/' });
|
|
215
201
|
* ctx.cookies.delete('session', { path: '/' });
|
|
@@ -229,7 +215,7 @@ export class Ctx {
|
|
|
229
215
|
};
|
|
230
216
|
}
|
|
231
217
|
/**
|
|
232
|
-
* Returns the {@link
|
|
218
|
+
* Returns the {@link RequestContext} for the current request.
|
|
233
219
|
*
|
|
234
220
|
* This is the primary entry point for reading request data from a server
|
|
235
221
|
* component or server action — the URL, cookies, params, env, and middleware
|
|
@@ -240,7 +226,7 @@ export class Ctx {
|
|
|
240
226
|
* import is for everywhere else: a nested server component, or a `'use server'`
|
|
241
227
|
* action module — neither of which receives props from the framework.
|
|
242
228
|
*
|
|
243
|
-
* @typeParam E - The app's Hono {@link Env}, to type {@link
|
|
229
|
+
* @typeParam E - The app's Hono {@link Env}, to type {@link RequestContext.var} and {@link RequestContext.env}.
|
|
244
230
|
* @throws If called at module load, where there is no ambient context to resolve.
|
|
245
231
|
* @throws If called while prerendering a `render: 'static'` route, which has no
|
|
246
232
|
* per-request context at build time — mark the route `render: 'dynamic'` instead.
|
|
@@ -248,28 +234,28 @@ export class Ctx {
|
|
|
248
234
|
* @example
|
|
249
235
|
* ```ts
|
|
250
236
|
* 'use server';
|
|
251
|
-
* import {
|
|
237
|
+
* import { getRequestContext, redirect } from '@rshono/core/server';
|
|
252
238
|
*
|
|
253
239
|
* export async function login(form: FormData) {
|
|
254
|
-
*
|
|
240
|
+
* getRequestContext().cookies.set('session', String(form.get('email')), { httpOnly: true });
|
|
255
241
|
* redirect('/dashboard');
|
|
256
242
|
* }
|
|
257
243
|
* ```
|
|
258
244
|
*/
|
|
259
|
-
export function
|
|
245
|
+
export function getRequestContext() {
|
|
260
246
|
if (prerendering) {
|
|
261
|
-
throw new Error("[rshono]
|
|
247
|
+
throw new Error("[rshono] getRequestContext() was called while prerendering a `render: 'static'` route. A static page " +
|
|
262
248
|
'is rendered once at build time, so it has no per-request context to read (URL, cookies, ' +
|
|
263
249
|
"headers, env). Change this route to `render: 'dynamic'` so it renders per request, or remove " +
|
|
264
|
-
'the
|
|
250
|
+
'the getRequestContext() call.');
|
|
265
251
|
}
|
|
266
252
|
const c = contextStorage.getStore();
|
|
267
253
|
if (!c) {
|
|
268
|
-
throw new Error('[rshono]
|
|
254
|
+
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
255
|
}
|
|
270
256
|
let ctx = wrappers.get(c);
|
|
271
257
|
if (!ctx) {
|
|
272
|
-
ctx = new
|
|
258
|
+
ctx = new RequestContext(c);
|
|
273
259
|
wrappers.set(c, ctx);
|
|
274
260
|
}
|
|
275
261
|
return ctx;
|
|
@@ -288,7 +274,7 @@ export function getContext() {
|
|
|
288
274
|
*
|
|
289
275
|
* @example
|
|
290
276
|
* ```ts
|
|
291
|
-
* const session =
|
|
277
|
+
* const session = getRequestContext().cookies.get('session');
|
|
292
278
|
* if (!session) redirect('/login');
|
|
293
279
|
* // session is defined below this line
|
|
294
280
|
* ```
|
|
@@ -304,9 +290,11 @@ export function redirect(location, status = 303) {
|
|
|
304
290
|
*
|
|
305
291
|
* @example
|
|
306
292
|
* ```tsx
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
*
|
|
293
|
+
* export default async function Page({ params }: PageProps<'/users/:id'>) {
|
|
294
|
+
* const user = await db.user.find(params.id);
|
|
295
|
+
* if (!user) notFound();
|
|
296
|
+
* return <Profile user={user} />; // user is non-null here
|
|
297
|
+
* }
|
|
310
298
|
* ```
|
|
311
299
|
*/
|
|
312
300
|
export function notFound() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context.js","sourceRoot":"","sources":["../../src/runtime/context.ts"],"names":[],"mappings":"AAAA,oDAAoD;AACpD;;;;;;;;;;GAUG;AAGH,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAEjE,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAY9D,MAAM,cAAc,GAAG,IAAI,iBAAiB,EAAW,CAAC;AAExD,sHAAsH;AACtH,MAAM,QAAQ,GAAG,IAAI,OAAO,EAAgB,CAAC;AAE7C;;;;;;;;GAQG;AACH,IAAI,WAA2D,CAAC;AAEhE,SAAS,UAAU;IACjB,OAAO,CAAC,WAAW,KAAK,OAAO,OAAO,KAAK,WAAW,IAAI,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;AACnG,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,YAAY,GAAG,OAAO,OAAO,KAAK,WAAW,IAAI,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,gBAAgB,CAAC;AAEvF;;;;;;;;GAQG;AACH,MAAM,UAAU,cAAc,CAAI,CAAU,EAAE,EAAW;IACvD,OAAO,cAAc,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AACnC,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,UAAU,CAAC,CAAU;IACnC,IAAI,CAAC;QACH,OAAO,CAAC,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC;IACvB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,6FAA6F;AAC7F,SAAS,mBAAmB,CAAC,MAA0B;IACrD,MAAM,KAAK,GAAG,MAAM,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC;IAC5C,OAAO,KAAK,IAAI,SAAS,CAAC;AAC5B,CAAC;AAED,wFAAwF;AACxF,2GAA2G;AAC3G,yGAAyG;AACzG,MAAM,UAAU,GAAG,OAAO,iBAAiB,KAAK,WAAW,IAAI,iBAAiB,CAAC,UAAU,CAAC;AAE5F;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,SAAS,CAAC,CAAU;IAClC,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC/B,IAAI,CAAC,UAAU;QAAE,OAAO,GAAG,CAAC;IAE5B,MAAM,aAAa,GAAG,mBAAmB,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,kBAAkB,CAAC,CAAC,CAAC;IAC5E,gGAAgG;IAChG,6FAA6F;IAC7F,MAAM,SAAS,GAAG,aAAa,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,UAAU,aAAa,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAC9E,IAAI,SAAS,EAAE,CAAC;QACd,GAAG,CAAC,QAAQ,GAAG,SAAS,CAAC,QAAQ,CAAC;QAClC,GAAG,CAAC,IAAI,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC,8DAA8D;IAC3F,CAAC;IAED,8FAA8F;IAC9F,sEAAsE;IACtE,MAAM,cAAc,GAAG,mBAAmB,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,mBAAmB,CAAC,CAAC,CAAC;IAC9E,IAAI,cAAc,KAAK,MAAM,IAAI,cAAc,KAAK,OAAO;QAAE,GAAG,CAAC,QAAQ,GAAG,cAAc,CAAC;IAE3F,OAAO,GAAG,CAAC;AACb,CAAC;AASD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,OAAO,GAAG;IACd,IAAI,CAAa;IACjB,IAAI,CAAO;IACX,IAAI,CAAc;IAElB,YAAY,CAAa;QACvB,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC;IAChB,CAAC;IAED;;;;;;;;;;OAUG;IACH,IAAI,GAAG;QACL,OAAO,IAAI,CAAC,IAAI,CAAC;IACnB,CAAC;IAED,oFAAoF;IACpF,IAAI,GAAG;QACL,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;IACvB,CAAC;IAED;;;;;OAKG;IACH,IAAI,GAAG;QACL,OAAO,CAAC,IAAI,CAAC,IAAI,KAAK,SAAS,CAAC,IAAI,CAAC,IAAe,CAAC,CAAC,CAAC;IACzD,CAAC;IAED,4DAA4D;IAC5D,IAAI,MAAM;QACR,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC;IAC9B,CAAC;IAED;;;OAGG;IACH,IAAI,MAAM;QACR,OAAO,UAAU,CAAC,IAAI,CAAC,IAAe,CAAC,CAAC;IAC1C,CAAC;IAED;;;OAGG;IACH,IAAI,GAAG;QACL,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;IACvB,CAAC;IAED;;;;;OAKG;IACH,IAAI,GAAG;QACL,IAAI,IAAI,CAAC,IAAI;YAAE,OAAO,IAAI,CAAC,IAAI,CAAC;QAChC,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,GAA0C,CAAC;QACtE,6FAA6F;QAC7F,OAAO,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,GAAG,UAAU,EAAE,EAAE,GAAG,QAAQ,EAAE,CAAC,CAAC,CAAC,UAAU,EAAE,CAAe,CAAC,CAAC;IAClG,CAAC;IAED,4EAA4E;IAC5E,MAAM,CAAC,IAAY,EAAE,KAAa;QAChC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IAChC,CAAC;IAED;;;;;;;;;;OAUG;IACH,OAAO,GAAG;QACR,+DAA+D;QAC/D,GAAG,EAAE,CAAC,IAAY,EAAsB,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC;QACrE,wDAAwD;QACxD,GAAG,EAAE,GAA2B,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC;QACvD,6GAA6G;QAC7G,GAAG,EAAE,CAAC,IAAY,EAAE,KAAa,EAAE,OAAuB,EAAQ,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,CAAC;QAC/G,gGAAgG;QAChG,MAAM,EAAE,CAAC,IAAY,EAAE,OAAuB,EAAQ,EAAE;YACtD,YAAY,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;QACzC,CAAC;KACF,CAAC;CACH;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,UAAU,UAAU;IACxB,IAAI,YAAY,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CACb,gGAAgG;YAC9F,0FAA0F;YAC1F,+FAA+F;YAC/F,wBAAwB,CAC3B,CAAC;IACJ,CAAC;IACD,MAAM,CAAC,GAAG,cAAc,CAAC,QAAQ,EAAE,CAAC;IACpC,IAAI,CAAC,CAAC,EAAE,CAAC;QACP,MAAM,IAAI,KAAK,CACb,qIAAqI,CACtI,CAAC;IACJ,CAAC;IACD,IAAI,GAAG,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IAC1B,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,GAAG,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC;QACjB,QAAQ,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IACvB,CAAC;IACD,OAAO,GAAwB,CAAC;AAClC,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,QAAQ,CAAC,QAAgB,EAAE,MAAM,GAAmB,GAAG;IACrE,MAAM,IAAI,cAAc,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;AAC7C,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,QAAQ;IACtB,MAAM,IAAI,cAAc,EAAE,CAAC;AAC7B,CAAC;AAwBD,IAAI,YAA4C,CAAC;AAEjD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,aAAa,CAAC,OAA2B;IACvD,YAAY,GAAG,OAAO,CAAC;AACzB,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAc,EAAE,IAA8C;IAC9F,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;IACnC,IAAI,CAAC,YAAY;QAAE,OAAO;IAC1B,IAAI,CAAC;QACH,YAAY,CAAC,KAAK,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;IACtE,CAAC;IAAC,OAAO,YAAY,EAAE,CAAC;QACtB,OAAO,CAAC,KAAK,CAAC,2CAA2C,EAAE,YAAY,CAAC,CAAC;IAC3E,CAAC;AACH,CAAC","sourcesContent":["/// <reference path=\"../types/rshono-config.d.ts\" />\n/**\n * The request context: {@link getContext} and the {@link Ctx} 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, HonoRequest } 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 Ctx} per Hono {@link Context}, so repeated `getContext()` calls in a request share its lazy getters. */\nconst wrappers = new WeakMap<Context, Ctx>();\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 Ctx.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 getContext} 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 getContext} 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 getContext} 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). Shared by {@link Ctx.params} and the\n * request renderer so the fallback behaviour stays in one place.\n *\n * Framework internal — read params from {@link Ctx.params} or a page's\n * `PageProps` 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 Ctx.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 Ctx.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 getContext}, 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 Ctx.url}, {@link Ctx.env}) are computed at most once.\n *\n * @typeParam E - The Hono {@link Env} describing this app's `Bindings` and\n * `Variables`, so {@link Ctx.var} and {@link Ctx.env} stay typed.\n *\n * @example\n * ```tsx\n * import { getContext } from '@rshono/core/server';\n *\n * export default async function Whoami() {\n * const ctx = getContext();\n * const session = ctx.cookies.get('session');\n * return <p>{ctx.url.pathname} — {session ?? 'anonymous'}</p>;\n * }\n * ```\n */\nexport class Ctx<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.\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 `Ctx` 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 /** The parsed Hono request (`c.req`) — headers, body parsing, param access, etc. */\n get req(): HonoRequest {\n return this.#raw.req;\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 /** The HTTP method of the request, e.g. `GET` or `POST`. */\n get method(): string {\n return this.#raw.req.method;\n }\n\n /**\n * Matched route params, e.g. `{ id }` for a `/users/[id]` route. Returns an\n * empty object when there is no active route match (rather than throwing).\n */\n get params(): Record<string, string> {\n return readParams(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 = getContext().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 /** Sets a response header. Thin pass-through to `c.header(name, value)`. */\n header(name: string, value: string): void {\n this.#raw.header(name, value);\n }\n\n /**\n * Read and write request/response cookies.\n *\n * @example\n * ```ts\n * const ctx = getContext();\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 Ctx} 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 Ctx.var} and {@link Ctx.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 { getContext, redirect } from '@rshono/core/server';\n *\n * export async function login(form: FormData) {\n * getContext().cookies.set('session', String(form.get('email')), { httpOnly: true });\n * redirect('/dashboard');\n * }\n * ```\n */\nexport function getContext<E extends Env = Env>(): Ctx<E> {\n if (prerendering) {\n throw new Error(\n \"[rshono] getContext() 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 getContext() call.',\n );\n }\n const c = contextStorage.getStore();\n if (!c) {\n throw new Error(\n '[rshono] getContext() 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 Ctx(c);\n wrappers.set(c, ctx);\n }\n return ctx as unknown as Ctx<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 = getContext().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 * const user = await db.user.find(getContext().params.id);\n * if (!user) notFound();\n * return <Profile user={user} />; // user is non-null here\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":"AAAA,oDAAoD;AACpD;;;;;;;;;;GAUG;AAGH,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAEjE,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAY9D,MAAM,cAAc,GAAG,IAAI,iBAAiB,EAAW,CAAC;AAExD,wIAAwI;AACxI,MAAM,QAAQ,GAAG,IAAI,OAAO,EAA2B,CAAC;AAExD;;;;;;;;GAQG;AACH,IAAI,WAA2D,CAAC;AAEhE,SAAS,UAAU;IACjB,OAAO,CAAC,WAAW,KAAK,OAAO,OAAO,KAAK,WAAW,IAAI,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;AACnG,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,YAAY,GAAG,OAAO,OAAO,KAAK,WAAW,IAAI,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,gBAAgB,CAAC;AAEvF;;;;;;;;GAQG;AACH,MAAM,UAAU,cAAc,CAAI,CAAU,EAAE,EAAW;IACvD,OAAO,cAAc,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AACnC,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,UAAU,CAAC,CAAU;IACnC,IAAI,CAAC;QACH,OAAO,CAAC,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC;IACvB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,6FAA6F;AAC7F,SAAS,mBAAmB,CAAC,MAA0B;IACrD,MAAM,KAAK,GAAG,MAAM,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC;IAC5C,OAAO,KAAK,IAAI,SAAS,CAAC;AAC5B,CAAC;AAED,wFAAwF;AACxF,2GAA2G;AAC3G,yGAAyG;AACzG,MAAM,UAAU,GAAG,OAAO,iBAAiB,KAAK,WAAW,IAAI,iBAAiB,CAAC,UAAU,CAAC;AAE5F;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,SAAS,CAAC,CAAU;IAClC,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC/B,IAAI,CAAC,UAAU;QAAE,OAAO,GAAG,CAAC;IAE5B,MAAM,aAAa,GAAG,mBAAmB,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,kBAAkB,CAAC,CAAC,CAAC;IAC5E,gGAAgG;IAChG,6FAA6F;IAC7F,MAAM,SAAS,GAAG,aAAa,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,UAAU,aAAa,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAC9E,IAAI,SAAS,EAAE,CAAC;QACd,GAAG,CAAC,QAAQ,GAAG,SAAS,CAAC,QAAQ,CAAC;QAClC,GAAG,CAAC,IAAI,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC,8DAA8D;IAC3F,CAAC;IAED,8FAA8F;IAC9F,sEAAsE;IACtE,MAAM,cAAc,GAAG,mBAAmB,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,mBAAmB,CAAC,CAAC,CAAC;IAC9E,IAAI,cAAc,KAAK,MAAM,IAAI,cAAc,KAAK,OAAO;QAAE,GAAG,CAAC,QAAQ,GAAG,cAAc,CAAC;IAE3F,OAAO,GAAG,CAAC;AACb,CAAC;AASD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,OAAO,cAAc;IACzB,IAAI,CAAa;IACjB,IAAI,CAAO;IACX,IAAI,CAAc;IAElB,YAAY,CAAa;QACvB,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC;IAChB,CAAC;IAED;;;;;;;;;;;;;;;OAeG;IACH,IAAI,GAAG;QACL,OAAO,IAAI,CAAC,IAAI,CAAC;IACnB,CAAC;IAED;;;;;OAKG;IACH,IAAI,GAAG;QACL,OAAO,CAAC,IAAI,CAAC,IAAI,KAAK,SAAS,CAAC,IAAI,CAAC,IAAe,CAAC,CAAC,CAAC;IACzD,CAAC;IAED;;;OAGG;IACH,IAAI,GAAG;QACL,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;IACvB,CAAC;IAED;;;;;OAKG;IACH,IAAI,GAAG;QACL,IAAI,IAAI,CAAC,IAAI;YAAE,OAAO,IAAI,CAAC,IAAI,CAAC;QAChC,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,GAA0C,CAAC;QACtE,6FAA6F;QAC7F,OAAO,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,GAAG,UAAU,EAAE,EAAE,GAAG,QAAQ,EAAE,CAAC,CAAC,CAAC,UAAU,EAAE,CAAe,CAAC,CAAC;IAClG,CAAC;IAED;;;;;;;;;;OAUG;IACH,OAAO,GAAG;QACR,+DAA+D;QAC/D,GAAG,EAAE,CAAC,IAAY,EAAsB,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC;QACrE,wDAAwD;QACxD,GAAG,EAAE,GAA2B,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC;QACvD,6GAA6G;QAC7G,GAAG,EAAE,CAAC,IAAY,EAAE,KAAa,EAAE,OAAuB,EAAQ,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,CAAC;QAC/G,gGAAgG;QAChG,MAAM,EAAE,CAAC,IAAY,EAAE,OAAuB,EAAQ,EAAE;YACtD,YAAY,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;QACzC,CAAC;KACF,CAAC;CACH;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,UAAU,iBAAiB;IAC/B,IAAI,YAAY,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CACb,uGAAuG;YACrG,0FAA0F;YAC1F,+FAA+F;YAC/F,+BAA+B,CAClC,CAAC;IACJ,CAAC;IACD,MAAM,CAAC,GAAG,cAAc,CAAC,QAAQ,EAAE,CAAC;IACpC,IAAI,CAAC,CAAC,EAAE,CAAC;QACP,MAAM,IAAI,KAAK,CACb,4IAA4I,CAC7I,CAAC;IACJ,CAAC;IACD,IAAI,GAAG,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IAC1B,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,GAAG,GAAG,IAAI,cAAc,CAAC,CAAC,CAAC,CAAC;QAC5B,QAAQ,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IACvB,CAAC;IACD,OAAO,GAAmC,CAAC;AAC7C,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,QAAQ,CAAC,QAAgB,EAAE,MAAM,GAAmB,GAAG;IACrE,MAAM,IAAI,cAAc,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;AAC7C,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,QAAQ;IACtB,MAAM,IAAI,cAAc,EAAE,CAAC;AAC7B,CAAC;AAwBD,IAAI,YAA4C,CAAC;AAEjD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,aAAa,CAAC,OAA2B;IACvD,YAAY,GAAG,OAAO,CAAC;AACzB,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAc,EAAE,IAA8C;IAC9F,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;IACnC,IAAI,CAAC,YAAY;QAAE,OAAO;IAC1B,IAAI,CAAC;QACH,YAAY,CAAC,KAAK,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;IACtE,CAAC;IAAC,OAAO,YAAY,EAAE,CAAC;QACtB,OAAO,CAAC,KAAK,CAAC,2CAA2C,EAAE,YAAY,CAAC,CAAC;IAC3E,CAAC;AACH,CAAC","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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entry.client.d.ts","sourceRoot":"","sources":["../../src/runtime/entry.client.tsx"],"names":[],"mappings":""}
|
|
1
|
+
{"version":3,"file":"entry.client.d.ts","sourceRoot":"","sources":["../../src/runtime/entry.client.tsx"],"names":[],"mappings":"AAiBA,OAAO,CAAC,MAAM,CAAC;IACb,6FAA6F;IAC7F,IAAI,aAAa,EAAE,KAAK,CAAC,MAAM,GAAG,UAAU,CAAC,GAAG,SAAS,CAAC;CAC3D"}
|