@rshono/core 1.0.0-rc.2 → 1.0.0-rc.21
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/LICENSE +21 -0
- package/README.md +348 -158
- package/bin/rshono.mjs +3 -4
- package/dist/builder/env-shadow-loader.cjs +123 -8
- package/dist/builder/page-entry-loader.cjs +28 -1
- package/dist/builder/page-files.d.ts +0 -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 +7 -1
- package/dist/builder/public-env.js +7 -0
- package/dist/builder/public-env.js.map +1 -1
- package/dist/builder/react-versions.d.ts +12 -0
- package/dist/builder/react-versions.js +74 -0
- package/dist/builder/react-versions.js.map +1 -0
- package/dist/builder/rspack-config.d.ts +13 -4
- package/dist/builder/rspack-config.js +136 -53
- package/dist/builder/rspack-config.js.map +1 -1
- package/dist/builder/server-only-imports.d.ts +32 -0
- package/dist/builder/server-only-imports.js +51 -0
- package/dist/builder/server-only-imports.js.map +1 -0
- package/dist/cli/build.d.ts +2 -3
- package/dist/cli/build.js +51 -13
- package/dist/cli/build.js.map +1 -1
- package/dist/cli/dev.d.ts +2 -3
- package/dist/cli/dev.js +149 -45
- package/dist/cli/dev.js.map +1 -1
- package/dist/cli/exit.d.ts +12 -0
- package/dist/cli/exit.js +16 -0
- package/dist/cli/exit.js.map +1 -0
- package/dist/cli/index.d.ts +0 -1
- package/dist/cli/index.js +91 -30
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/start.d.ts +0 -1
- package/dist/cli/start.js +15 -18
- package/dist/cli/start.js.map +1 -1
- package/dist/config.d.ts +72 -78
- package/dist/config.js +17 -1
- package/dist/config.js.map +1 -1
- package/dist/deploy/aws-lambda/runtime.d.ts +4 -7
- package/dist/deploy/aws-lambda/runtime.js +5 -8
- package/dist/deploy/aws-lambda/runtime.js.map +1 -1
- package/dist/deploy/build-marker.d.ts +9 -6
- package/dist/deploy/build-marker.js +8 -4
- package/dist/deploy/build-marker.js.map +1 -1
- package/dist/deploy/cloudflare/build.d.ts +0 -1
- package/dist/deploy/cloudflare/build.js +17 -13
- package/dist/deploy/cloudflare/build.js.map +1 -1
- package/dist/deploy/cloudflare/runtime.d.ts +2 -6
- package/dist/deploy/cloudflare/runtime.js +73 -68
- package/dist/deploy/cloudflare/runtime.js.map +1 -1
- package/dist/deploy/contract.d.ts +57 -41
- package/dist/deploy/contract.js.map +1 -1
- package/dist/deploy/filesystem.d.ts +3 -6
- package/dist/deploy/filesystem.js +23 -20
- package/dist/deploy/filesystem.js.map +1 -1
- package/dist/deploy/node/runtime.d.ts +5 -3
- package/dist/deploy/node/runtime.js +23 -9
- package/dist/deploy/node/runtime.js.map +1 -1
- package/dist/deploy/presets.d.ts +34 -30
- package/dist/deploy/presets.js +56 -44
- package/dist/deploy/presets.js.map +1 -1
- package/dist/deploy/public-paths.d.ts +42 -0
- package/dist/deploy/public-paths.js +66 -0
- package/dist/deploy/public-paths.js.map +1 -0
- package/dist/deploy/vercel/build.d.ts +0 -1
- package/dist/deploy/vercel/build.js +25 -16
- package/dist/deploy/vercel/build.js.map +1 -1
- package/dist/deploy/vercel/runtime.d.ts +10 -7
- package/dist/deploy/vercel/runtime.js +48 -11
- package/dist/deploy/vercel/runtime.js.map +1 -1
- package/dist/index.d.ts +19 -17
- package/dist/index.js +20 -10
- package/dist/index.js.map +1 -1
- package/dist/router.d.ts +212 -120
- package/dist/router.js +3 -8
- package/dist/router.js.map +1 -1
- package/dist/runtime/boundaries.d.ts +62 -48
- package/dist/runtime/boundaries.js +32 -34
- package/dist/runtime/boundaries.js.map +1 -1
- package/dist/runtime/client.d.ts +20 -11
- package/dist/runtime/client.js +20 -10
- package/dist/runtime/client.js.map +1 -1
- package/dist/runtime/context.d.ts +304 -123
- package/dist/runtime/context.js +377 -149
- package/dist/runtime/context.js.map +1 -1
- package/dist/runtime/control.d.ts +15 -1
- package/dist/runtime/control.js +24 -0
- package/dist/runtime/control.js.map +1 -1
- package/dist/runtime/dev-protocol.d.ts +4 -9
- package/dist/runtime/dev-protocol.js.map +1 -1
- package/dist/runtime/empty-server-app.d.ts +0 -1
- package/dist/runtime/entry.client.d.ts +4 -1
- package/dist/runtime/entry.client.js +455 -313
- package/dist/runtime/entry.client.js.map +1 -1
- package/dist/runtime/entry.rsc.d.ts +15 -7
- package/dist/runtime/entry.rsc.js +811 -252
- package/dist/runtime/entry.rsc.js.map +1 -1
- package/dist/runtime/entry.ssr.d.ts +27 -10
- package/dist/runtime/entry.ssr.js +30 -48
- package/dist/runtime/entry.ssr.js.map +1 -1
- package/dist/runtime/failure-document.d.ts +11 -0
- package/dist/runtime/failure-document.js +35 -0
- package/dist/runtime/failure-document.js.map +1 -0
- package/dist/runtime/flight-inject.d.ts +23 -0
- package/dist/runtime/flight-inject.js +403 -0
- package/dist/runtime/flight-inject.js.map +1 -0
- package/dist/runtime/hot-update.d.ts +44 -0
- package/dist/runtime/hot-update.js +44 -0
- package/dist/runtime/hot-update.js.map +1 -0
- package/dist/runtime/navigation.d.ts +46 -54
- package/dist/runtime/navigation.js +22 -63
- package/dist/runtime/navigation.js.map +1 -1
- package/dist/runtime/request.d.ts +42 -8
- package/dist/runtime/request.js +84 -7
- package/dist/runtime/request.js.map +1 -1
- package/dist/runtime/server.d.ts +20 -18
- package/dist/runtime/server.js +22 -17
- package/dist/runtime/server.js.map +1 -1
- package/dist/runtime/validate-entries.d.ts +77 -0
- package/dist/runtime/validate-entries.js +405 -0
- package/dist/runtime/validate-entries.js.map +1 -0
- package/dist/server/headers.d.ts +41 -15
- package/dist/server/headers.js +53 -24
- package/dist/server/headers.js.map +1 -1
- package/dist/server/load-config.d.ts +4 -5
- package/dist/server/load-config.js +22 -14
- package/dist/server/load-config.js.map +1 -1
- package/dist/server/load-env.d.ts +0 -1
- package/dist/server/prerendered.d.ts +88 -34
- package/dist/server/prerendered.js +184 -24
- package/dist/server/prerendered.js.map +1 -1
- package/dist/server/server-config.d.ts +35 -50
- package/dist/server/server-config.js +27 -74
- package/dist/server/server-config.js.map +1 -1
- package/dist/server/shutdown.d.ts +5 -4
- package/dist/server/shutdown.js +5 -3
- package/dist/server/shutdown.js.map +1 -1
- package/dist/server/ssg.d.ts +9 -7
- package/dist/server/ssg.js +209 -65
- package/dist/server/ssg.js.map +1 -1
- package/dist/server/static.d.ts +0 -1
- package/dist/server/static.js +7 -1
- package/dist/server/static.js.map +1 -1
- package/package.json +21 -23
- package/dist/builder/page-files.d.ts.map +0 -1
- package/dist/builder/public-env.d.ts.map +0 -1
- package/dist/builder/rspack-config.d.ts.map +0 -1
- package/dist/cli/build.d.ts.map +0 -1
- package/dist/cli/dev.d.ts.map +0 -1
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/cli/start.d.ts.map +0 -1
- package/dist/config.d.ts.map +0 -1
- package/dist/deploy/aws-lambda/runtime.d.ts.map +0 -1
- package/dist/deploy/build-marker.d.ts.map +0 -1
- 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/cloudflare/build.d.ts.map +0 -1
- package/dist/deploy/cloudflare/runtime.d.ts.map +0 -1
- package/dist/deploy/contract.d.ts.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/filesystem.d.ts.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/deploy/node/runtime.d.ts.map +0 -1
- package/dist/deploy/presets.d.ts.map +0 -1
- package/dist/deploy/vercel/build.d.ts.map +0 -1
- package/dist/deploy/vercel/runtime.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/router.d.ts.map +0 -1
- package/dist/runtime/boundaries.d.ts.map +0 -1
- package/dist/runtime/client.d.ts.map +0 -1
- package/dist/runtime/context.d.ts.map +0 -1
- package/dist/runtime/control.d.ts.map +0 -1
- package/dist/runtime/dev-protocol.d.ts.map +0 -1
- package/dist/runtime/empty-server-app.d.ts.map +0 -1
- package/dist/runtime/entry.client.d.ts.map +0 -1
- package/dist/runtime/entry.rsc.d.ts.map +0 -1
- package/dist/runtime/entry.ssr.d.ts.map +0 -1
- package/dist/runtime/navigation.d.ts.map +0 -1
- package/dist/runtime/request.d.ts.map +0 -1
- package/dist/runtime/server.d.ts.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/server/headers.d.ts.map +0 -1
- package/dist/server/load-config.d.ts.map +0 -1
- package/dist/server/load-env.d.ts.map +0 -1
- package/dist/server/prerendered.d.ts.map +0 -1
- package/dist/server/server-config.d.ts.map +0 -1
- package/dist/server/shutdown.d.ts.map +0 -1
- package/dist/server/ssg.d.ts.map +0 -1
- package/dist/server/static.d.ts.map +0 -1
package/dist/router.d.ts
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import type { Env, Handler } from 'hono';
|
|
2
2
|
import type { ParamKeys, ParamKeyToRecord } from 'hono/types';
|
|
3
3
|
import type { ReactNode } from 'react';
|
|
4
|
-
import type {
|
|
4
|
+
import type { RequestContext } from './runtime/context.js';
|
|
5
5
|
type Simplify<T> = {
|
|
6
6
|
[K in keyof T]: T[K];
|
|
7
7
|
} & {};
|
|
8
8
|
type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
|
|
9
9
|
/**
|
|
10
|
-
* The `params` record implied by a route path pattern — one required `string` key
|
|
11
|
-
*
|
|
10
|
+
* The `params` record implied by a route path pattern — one required `string` key per `:param`
|
|
11
|
+
* segment, `Record<string, never>` for a path with none. Paths use Hono's syntax, so `:id`,
|
|
12
|
+
* `:id{[0-9]+}` and `*` all work.
|
|
12
13
|
*
|
|
13
|
-
*
|
|
14
|
-
* name this type directly; {@link PageProps} applies it for you.
|
|
14
|
+
* You rarely name this directly; {@link PageProps} applies it for you.
|
|
15
15
|
*
|
|
16
16
|
* @typeParam P - The literal route path, e.g. `'/users/:id/posts/:postId'`.
|
|
17
17
|
*
|
|
@@ -19,23 +19,21 @@ type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) exten
|
|
|
19
19
|
* ```ts
|
|
20
20
|
* type P = PathParams<'/users/:id/posts/:postId'>; // { id: string; postId: string }
|
|
21
21
|
* ```
|
|
22
|
+
*
|
|
23
|
+
* @see {@link https://hono.dev/docs/api/routing#path-parameter | Hono — path parameters}
|
|
22
24
|
*/
|
|
23
25
|
export type PathParams<P extends string> = ParamKeys<P> extends never ? Record<string, never> : Simplify<UnionToIntersection<ParamKeyToRecord<ParamKeys<P>>>>;
|
|
24
26
|
/**
|
|
25
|
-
* Props every page component receives. Pass the route's path as the type
|
|
26
|
-
*
|
|
27
|
-
* an open `Record<string, string>`.
|
|
28
|
-
*
|
|
29
|
-
* `defineRoutes` checks each page's props against `PageProps<path>` at compile
|
|
30
|
-
* time, so a mismatched path literal is a type error at the route definition.
|
|
27
|
+
* Props every page component receives. Pass the route's path as the type argument to get `params`
|
|
28
|
+
* typed key-by-key; without it `params` falls back to an open `Record<string, string>`.
|
|
31
29
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* the server/client line
|
|
30
|
+
* `defineRoutes` checks each page's props against `PageProps<path>`, so a mismatched path literal is
|
|
31
|
+
* a type error at the route definition. `url` and `params` mirror what `useNavigation()` gives a
|
|
32
|
+
* `'use client'` component, so a read moves across the server/client line unchanged.
|
|
35
33
|
*
|
|
36
34
|
* @typeParam Path - The literal path this page is mounted at, e.g. `'/profile/:id'`.
|
|
37
|
-
* @typeParam E - The app's Hono {@link Env}, to type {@link
|
|
38
|
-
* {@link
|
|
35
|
+
* @typeParam E - The app's Hono {@link Env}, to type {@link RequestContext.var} and
|
|
36
|
+
* {@link RequestContext.env} on {@link PageProps.ctx}.
|
|
39
37
|
*
|
|
40
38
|
* @example
|
|
41
39
|
* ```tsx
|
|
@@ -47,50 +45,42 @@ export type PathParams<P extends string> = ParamKeys<P> extends never ? Record<s
|
|
|
47
45
|
* return <Layout>{user.name} — {tab}</Layout>;
|
|
48
46
|
* }
|
|
49
47
|
* ```
|
|
48
|
+
*
|
|
49
|
+
* @see {@link https://www.rshono.com/docs/pages#page-props | Docs — page props}
|
|
50
50
|
*/
|
|
51
51
|
export interface PageProps<Path extends string = string, E extends Env = Env> {
|
|
52
52
|
/**
|
|
53
|
-
* The absolute browser-facing request {@link URL}, proxy-header aware
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* A fresh instance per request that nothing else holds, so mutating it is local
|
|
58
|
-
* to the page — but note it is *not* serializable, so a `'use client'` component
|
|
59
|
-
* has to be handed `url.href` rather than `url`.
|
|
53
|
+
* The absolute browser-facing request {@link URL}, proxy-header aware (`X-Forwarded-Host` /
|
|
54
|
+
* `-Proto`). A fresh instance per request, so mutating it is local to the page; it is not
|
|
55
|
+
* serializable, so hand a `'use client'` component `url.href` rather than `url`.
|
|
60
56
|
*
|
|
61
|
-
* On a
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* `'use client'` component
|
|
57
|
+
* On a `render: 'static'` route this is the build-time URL — rendered once against `siteUrl`, so the
|
|
58
|
+
* origin is `siteUrl`'s and `url.searchParams` is always empty, on first paint and after a soft
|
|
59
|
+
* navigation alike. **`useNavigation().url` is the same frozen URL, not a way around it**: the payload
|
|
60
|
+
* carries one `href` and both readings come from it. Mark the route `render: 'dynamic'` if the page
|
|
61
|
+
* depends on the query; a `'use client'` component that only wants it after hydration can read
|
|
62
|
+
* `location.search` in an effect.
|
|
66
63
|
*/
|
|
67
64
|
url: URL;
|
|
68
65
|
/** Matched route params for this request, e.g. `{ id: '42' }` for `/profile/:id`. */
|
|
69
66
|
params: string extends Path ? Record<string, string> : PathParams<Path>;
|
|
70
67
|
/**
|
|
71
|
-
* The request context — the
|
|
72
|
-
*
|
|
73
|
-
* an import.
|
|
68
|
+
* The request context — the object `getRequestContext()` returns, handed to the page so cookies,
|
|
69
|
+
* headers, env and middleware variables are reachable without an import.
|
|
74
70
|
*
|
|
75
|
-
* Server-only,
|
|
76
|
-
*
|
|
77
|
-
*
|
|
71
|
+
* Server-only, non-enumerable (a `{...props}` spread and `JSON.stringify` both skip it) and never
|
|
72
|
+
* serialized. Passing it to a `'use client'` component fails the render, because it wraps the live
|
|
73
|
+
* request — read what you need here and pass plain values down.
|
|
78
74
|
*
|
|
79
|
-
* -
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
* silently rather than failing, since the spread copies enumerables only.
|
|
86
|
-
* (That spread still fails, mind — on `url`, which is enumerable and just as
|
|
87
|
-
* unserializable. Pass the values you need.)
|
|
88
|
-
* - `Object.keys(props)`, `JSON.stringify(props)` and friends don't see it.
|
|
75
|
+
* Non-enumerable is the one place this API breaks a JavaScript expectation, and it is unavoidable: an
|
|
76
|
+
* enumerable `ctx` would put `ctx.hono.env` — every binding and secret — into React's dev-only
|
|
77
|
+
* serialization of a server component's props, which walks own enumerable properties. So `<Child
|
|
78
|
+
* {...props} />` hands a **server** child `ctx: undefined` with no error, while the type says otherwise.
|
|
79
|
+
* Nested server components are meant to call `getRequestContext()` for the same object rather than
|
|
80
|
+
* receive it, which is also the fix if a spread has already cost you an afternoon.
|
|
89
81
|
*
|
|
90
|
-
* Reading it on a `render: 'static'` route throws: a prerendered page has no
|
|
91
|
-
*
|
|
92
|
-
* the `url` / `params` props, which are available either way — with the
|
|
93
|
-
* build-time caveats noted on `url`).
|
|
82
|
+
* Reading it on a `render: 'static'` route throws: a prerendered page has no per-request context.
|
|
83
|
+
* Mark the route `render: 'dynamic'`, or use the `url` / `params` props.
|
|
94
84
|
*
|
|
95
85
|
* @example
|
|
96
86
|
* ```tsx
|
|
@@ -101,34 +91,44 @@ export interface PageProps<Path extends string = string, E extends Env = Env> {
|
|
|
101
91
|
* }
|
|
102
92
|
* ```
|
|
103
93
|
*/
|
|
104
|
-
ctx:
|
|
94
|
+
ctx: RequestContext<E>;
|
|
105
95
|
}
|
|
106
96
|
/**
|
|
107
|
-
* A page: a React **server component**
|
|
108
|
-
*
|
|
109
|
-
* data directly.
|
|
97
|
+
* A page: a React **server component** rendering the entire document (`<html>…</html>`), usually via
|
|
98
|
+
* a shared layout. It may be `async` and await data directly.
|
|
110
99
|
*
|
|
111
|
-
* Each page module
|
|
112
|
-
*
|
|
100
|
+
* Each page module default-exports exactly one. Interactive parts belong in `'use client'` components
|
|
101
|
+
* the page imports — only those ship JS.
|
|
113
102
|
*
|
|
114
103
|
* @typeParam P - The component's props; for a page these are {@link PageProps}.
|
|
104
|
+
*
|
|
105
|
+
* @see {@link https://react.dev/reference/rsc/server-components | React — Server Components}
|
|
106
|
+
* @see {@link https://www.rshono.com/docs/pages | Docs — pages}
|
|
115
107
|
*/
|
|
116
108
|
export type PageComponent<P = any> = (props: P) => ReactNode | Promise<ReactNode>;
|
|
117
109
|
/**
|
|
118
|
-
* The shape an `{ type: 'endpoint' }` route's server module must have: a single
|
|
119
|
-
*
|
|
120
|
-
* safe
|
|
110
|
+
* The shape an `{ type: 'endpoint' }` route's server module must have: a single named `handler`
|
|
111
|
+
* export. It only ever loads on the server, so importing a database client or reading secrets from it
|
|
112
|
+
* is safe.
|
|
121
113
|
*
|
|
122
114
|
* @example
|
|
123
115
|
* ```ts
|
|
124
116
|
* // src/health.ts
|
|
125
|
-
* import type { Handler } from '
|
|
117
|
+
* import type { Handler } from 'hono';
|
|
126
118
|
*
|
|
127
119
|
* export const handler: Handler = (c) => c.json({ ok: true });
|
|
128
120
|
* ```
|
|
121
|
+
*
|
|
122
|
+
* @see {@link https://www.rshono.com/docs/routing#endpoint-routes | Docs — endpoint routes}
|
|
129
123
|
*/
|
|
130
124
|
export interface EndpointServerModule {
|
|
131
|
-
/**
|
|
125
|
+
/**
|
|
126
|
+
* A Hono {@link Handler} for every request the route matches. It is passed Hono's `Context`, so the
|
|
127
|
+
* request, the response builders (`c.json`, `c.text`, `c.body`) and middleware variables are all
|
|
128
|
+
* reached through it.
|
|
129
|
+
*
|
|
130
|
+
* @see {@link https://hono.dev/docs/api/context | Hono — Context}
|
|
131
|
+
*/
|
|
132
132
|
handler: Handler;
|
|
133
133
|
}
|
|
134
134
|
/**
|
|
@@ -143,19 +143,27 @@ export interface EndpointServerModule {
|
|
|
143
143
|
export interface PageRoute {
|
|
144
144
|
/** Discriminates a page from an endpoint; optional because `'page'` is the default. */
|
|
145
145
|
type?: 'page';
|
|
146
|
-
/**
|
|
146
|
+
/**
|
|
147
|
+
* Hono-style path pattern, e.g. `/`, `/profile/:id`, `/files/*`. Routes are matched in
|
|
148
|
+
* declaration order.
|
|
149
|
+
*
|
|
150
|
+
* @see {@link https://hono.dev/docs/api/routing | Hono — routing}
|
|
151
|
+
*/
|
|
147
152
|
path: string;
|
|
148
153
|
/**
|
|
149
|
-
* Dynamic import of the page module, whose default export is the
|
|
150
|
-
*
|
|
154
|
+
* Dynamic import of the page module, whose default export is the {@link PageComponent}.
|
|
155
|
+
*
|
|
156
|
+
* Write it inline as `() => import('…')`: the framework detects that exact form and injects the
|
|
157
|
+
* `'use server-entry'` directive that attaches the page's client JS and CSS. Wire the component up
|
|
158
|
+
* any other way — a variable, a barrel re-export, a computed specifier — and you have to put
|
|
159
|
+
* `'use server-entry'` on the page module's first line yourself.
|
|
151
160
|
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
* yourself; the framework throws a descriptive error when neither happened.
|
|
161
|
+
* @example
|
|
162
|
+
* ```ts
|
|
163
|
+
* component: () => import('./components/profile')
|
|
164
|
+
* ```
|
|
165
|
+
*
|
|
166
|
+
* @see {@link https://www.rshono.com/docs/pages#the-use-server-entry-directive | Docs — the `'use server-entry'` directive}
|
|
159
167
|
*/
|
|
160
168
|
component: () => Promise<{
|
|
161
169
|
default: PageComponent;
|
|
@@ -163,13 +171,17 @@ export interface PageRoute {
|
|
|
163
171
|
/** `'static'` prerenders the route at build time; `'dynamic'` (the default) renders per request. */
|
|
164
172
|
render?: 'static' | 'dynamic';
|
|
165
173
|
/**
|
|
166
|
-
* For a `render: 'static'` route with params: the param sets to prerender, one
|
|
167
|
-
*
|
|
168
|
-
*
|
|
174
|
+
* For a `render: 'static'` route with params: the param sets to prerender, one page each. Runs at
|
|
175
|
+
* build time on the server, so it may hit a database or read the filesystem.
|
|
176
|
+
*
|
|
177
|
+
* A parameterised static route without this falls back to rendering per request, with a build
|
|
178
|
+
* warning. Wildcard (`*`), optional and regex params cannot be prerendered.
|
|
169
179
|
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
180
|
+
* **Every value has to be one portable file name**, since that is what a prerendered page is stored as,
|
|
181
|
+
* and the build fails naming the value rather than writing a page nothing will serve. So: no
|
|
182
|
+
* `\ / : * ? " < > |` or control characters, no trailing `.` or space, and not a reserved Windows device
|
|
183
|
+
* name (`CON`, `NUL`, `COM1`, …) — the last two enforced everywhere, so a build that works on macOS is
|
|
184
|
+
* not one that fails in CI on Windows.
|
|
173
185
|
*
|
|
174
186
|
* @example
|
|
175
187
|
* ```ts
|
|
@@ -180,6 +192,8 @@ export interface PageRoute {
|
|
|
180
192
|
* staticPaths: async () => (await db.docs.all()).map((d) => ({ slug: d.slug })),
|
|
181
193
|
* }
|
|
182
194
|
* ```
|
|
195
|
+
*
|
|
196
|
+
* @see {@link https://www.rshono.com/docs/routing#static-rendering | Docs — static rendering}
|
|
183
197
|
*/
|
|
184
198
|
staticPaths?: () => Array<Record<string, string>> | Promise<Array<Record<string, string>>>;
|
|
185
199
|
}
|
|
@@ -192,37 +206,55 @@ export interface PageRoute {
|
|
|
192
206
|
* ```ts
|
|
193
207
|
* { type: 'endpoint', path: '/api/health', server: () => import('./health') }
|
|
194
208
|
* ```
|
|
209
|
+
*
|
|
210
|
+
* @see {@link https://www.rshono.com/docs/routing#endpoint-routes | Docs — endpoint routes}
|
|
195
211
|
*/
|
|
196
212
|
export interface EndpointRoute {
|
|
197
213
|
/** Marks this route as an endpoint rather than a page. Required. */
|
|
198
214
|
type: 'endpoint';
|
|
199
|
-
/**
|
|
215
|
+
/**
|
|
216
|
+
* Hono-style path pattern, e.g. `/api/health`, `/api/users/:id`.
|
|
217
|
+
*
|
|
218
|
+
* @see {@link https://hono.dev/docs/api/routing | Hono — routing}
|
|
219
|
+
*/
|
|
200
220
|
path: string;
|
|
201
|
-
/**
|
|
202
|
-
|
|
221
|
+
/**
|
|
222
|
+
* HTTP method to match, or a list of them. Defaults to `'all'` — every method.
|
|
223
|
+
*
|
|
224
|
+
* There is no `'head'`: Hono dispatches a `HEAD` as a `GET` and strips the body off the response, so a
|
|
225
|
+
* `HEAD` is already answered by the `'get'` handler (and by `'all'`), and a route registered for `HEAD`
|
|
226
|
+
* alone would never be reached.
|
|
227
|
+
*
|
|
228
|
+
* A list is how a two-method endpoint says so; `'all'` inside one is refused, since it is either the
|
|
229
|
+
* whole thing or a mistake. A method the route does not name gets Hono's 404 rather than the handler.
|
|
230
|
+
*
|
|
231
|
+
* @example
|
|
232
|
+
* ```ts
|
|
233
|
+
* { type: 'endpoint', path: '/api/session', method: ['get', 'delete'], server: () => import('./session') }
|
|
234
|
+
* ```
|
|
235
|
+
*/
|
|
236
|
+
method?: HTTPMethod | readonly HTTPMethod[];
|
|
203
237
|
/** Dynamic import of the {@link EndpointServerModule} exporting `handler`. */
|
|
204
238
|
server: () => Promise<EndpointServerModule>;
|
|
205
239
|
}
|
|
206
|
-
/**
|
|
207
|
-
|
|
240
|
+
/**
|
|
241
|
+
* HTTP methods an {@link EndpointRoute} can match. `'all'` matches every method.
|
|
242
|
+
*
|
|
243
|
+
* No `'head'`, deliberately — see {@link EndpointRoute.method}. A `HEAD` reaches the `'get'` handler.
|
|
244
|
+
*/
|
|
245
|
+
export type HTTPMethod = 'get' | 'post' | 'put' | 'patch' | 'delete' | 'options' | 'all';
|
|
208
246
|
/** Any entry in the `routes` array: a {@link PageRoute} or an {@link EndpointRoute}. */
|
|
209
247
|
export type Route = PageRoute | EndpointRoute;
|
|
210
248
|
/**
|
|
211
|
-
*
|
|
212
|
-
*
|
|
249
|
+
* Narrows a {@link Route} to a {@link PageRoute} — `type` is optional on page routes, so anything not
|
|
250
|
+
* explicitly `'endpoint'` is one.
|
|
213
251
|
*
|
|
214
|
-
* @
|
|
215
|
-
* ```ts
|
|
216
|
-
* for (const route of routes) {
|
|
217
|
-
* if (isPageRoute(route)) console.log(route.render ?? 'dynamic');
|
|
218
|
-
* }
|
|
219
|
-
* ```
|
|
252
|
+
* @internal
|
|
220
253
|
*/
|
|
221
254
|
export declare function isPageRoute(route: Route): route is PageRoute;
|
|
222
255
|
/**
|
|
223
|
-
* A page the framework falls back to rather than routes to — `notFound` and
|
|
224
|
-
*
|
|
225
|
-
* `component`, without a path of its own.
|
|
256
|
+
* A page the framework falls back to rather than routes to — `notFound` and `error` in
|
|
257
|
+
* {@link RouteConfig}. Same contract as a {@link PageRoute} `component`, without a path of its own.
|
|
226
258
|
*/
|
|
227
259
|
export interface FallbackPage {
|
|
228
260
|
/** Dynamic import of the page module; its default export is the {@link PageComponent}. */
|
|
@@ -231,19 +263,24 @@ export interface FallbackPage {
|
|
|
231
263
|
}>;
|
|
232
264
|
}
|
|
233
265
|
/**
|
|
234
|
-
* The error detail handed to the `error` page. Redacted in production
|
|
235
|
-
*
|
|
236
|
-
* you get the real message plus the stack.
|
|
266
|
+
* The error detail handed to the `error` page. Redacted in production — a generic
|
|
267
|
+
* `'Internal Server Error'` and no `stack`; in dev, the real message and stack.
|
|
237
268
|
*/
|
|
238
|
-
export interface
|
|
269
|
+
export interface ErrorPageInfo {
|
|
239
270
|
/** The thrown error's message in dev; `'Internal Server Error'` in production. */
|
|
240
271
|
message: string;
|
|
241
|
-
/**
|
|
272
|
+
/**
|
|
273
|
+
* The stack trace — **dev only**, and `undefined` in every build. Optional for that reason rather than
|
|
274
|
+
* because some errors lack one, so a page that renders it should guard on it, not on a mode flag.
|
|
275
|
+
*/
|
|
242
276
|
stack?: string;
|
|
243
277
|
}
|
|
244
278
|
/**
|
|
245
|
-
* Props for the `error` page declared in {@link RouteConfig.error} — the usual
|
|
246
|
-
*
|
|
279
|
+
* Props for the `error` page declared in {@link RouteConfig.error} — the usual {@link PageProps} plus
|
|
280
|
+
* the redaction-aware {@link ErrorPageInfo}.
|
|
281
|
+
*
|
|
282
|
+
* In a build `error.message` is the generic `'Internal Server Error'` and `error.stack` is `undefined`, so
|
|
283
|
+
* the page below guards on the stack rather than on a mode flag — there is no mode flag to guard on.
|
|
247
284
|
*
|
|
248
285
|
* @typeParam E - The app's Hono {@link Env}, forwarded to {@link PageProps.ctx}.
|
|
249
286
|
*
|
|
@@ -252,12 +289,21 @@ export interface ErrorInfo {
|
|
|
252
289
|
* import type { ErrorPageProps } from '@rshono/core';
|
|
253
290
|
*
|
|
254
291
|
* export default function ServerError({ error }: ErrorPageProps) {
|
|
255
|
-
* return
|
|
292
|
+
* return (
|
|
293
|
+
* <html>
|
|
294
|
+
* <body>
|
|
295
|
+
* <h1>Something went wrong</h1>
|
|
296
|
+
* <p>{error.message}</p>
|
|
297
|
+
* {error.stack && <pre>{error.stack}</pre>}
|
|
298
|
+
* </body>
|
|
299
|
+
* </html>
|
|
300
|
+
* );
|
|
256
301
|
* }
|
|
257
302
|
* ```
|
|
258
303
|
*/
|
|
259
304
|
export type ErrorPageProps<E extends Env = Env> = PageProps<string, E> & {
|
|
260
|
-
error
|
|
305
|
+
/** The error that failed the request, redacted in production — see {@link ErrorPageInfo}. */
|
|
306
|
+
error: ErrorPageInfo;
|
|
261
307
|
};
|
|
262
308
|
/**
|
|
263
309
|
* The object form accepted by {@link defineRoutes}: the route table plus the two
|
|
@@ -265,42 +311,89 @@ export type ErrorPageProps<E extends Env = Env> = PageProps<string, E> & {
|
|
|
265
311
|
*
|
|
266
312
|
* @typeParam TRoutes - Inferred tuple of route literals, which is what makes the
|
|
267
313
|
* per-route `path` → props check possible.
|
|
314
|
+
*
|
|
315
|
+
* @see {@link https://www.rshono.com/docs/routing#notfound-and-error | Docs — notFound and error pages}
|
|
268
316
|
*/
|
|
269
317
|
export interface RouteConfig<TRoutes extends readonly Route[] = readonly Route[]> {
|
|
270
318
|
/** Every page and endpoint in the app, matched in order. */
|
|
271
319
|
routes: TRoutes;
|
|
272
320
|
/** Page rendered with a 404 status for unmatched paths and for `notFound()` calls. */
|
|
273
321
|
notFound?: FallbackPage;
|
|
274
|
-
/**
|
|
322
|
+
/**
|
|
323
|
+
* Page rendered with a 500 status when a request throws — a page component, a page module that will not
|
|
324
|
+
* load, an endpoint, a server action, or middleware. Receives {@link ErrorPageProps}.
|
|
325
|
+
*
|
|
326
|
+
* It is a *fresh* render, with its own flight payload, so it hydrates and behaves like any other page.
|
|
327
|
+
* An app that declares none gets the framework's plain 500 document instead. If the `error` page itself
|
|
328
|
+
* throws, that is reported too and the framework's document answers.
|
|
329
|
+
*/
|
|
275
330
|
error?: FallbackPage;
|
|
276
331
|
}
|
|
332
|
+
/**
|
|
333
|
+
* The same check for `staticPaths`, whose param sets have to fill the route's own path: a key that does not
|
|
334
|
+
* is otherwise a build-time throw from `interpolatePath` rather than a type error.
|
|
335
|
+
*
|
|
336
|
+
* Keys only, not full assignability, because the declared field type is `Record<string, string>` and a
|
|
337
|
+
* `staticPaths` annotated as returning exactly that has to stay accepted — an index signature carries no
|
|
338
|
+
* key to check, so it passes. Skipped where the path has no params, because `staticPaths` is not called for
|
|
339
|
+
* such a route at all and an error there would be about the wrong thing.
|
|
340
|
+
*/
|
|
341
|
+
type ValidateStaticPaths<R, P extends string> = ParamKeys<P> extends never ? R : R extends {
|
|
342
|
+
staticPaths: () => infer Sets;
|
|
343
|
+
} ? Awaited<Sets> extends ReadonlyArray<infer Set> ? [keyof PathParams<P>] extends [keyof Set] ? R : R & {
|
|
344
|
+
staticPaths: `every param set staticPaths returns needs the params of '${P}'`;
|
|
345
|
+
} : R : R;
|
|
277
346
|
type ValidateRoute<R> = R extends {
|
|
278
347
|
path: infer P extends string;
|
|
279
348
|
component: () => Promise<{
|
|
280
349
|
default: PageComponent<infer CP>;
|
|
281
350
|
}>;
|
|
282
|
-
} ? [
|
|
351
|
+
} ? [
|
|
352
|
+
PageProps<P, any>
|
|
353
|
+
] extends [CP] ? ValidateStaticPaths<R, P> : R & {
|
|
283
354
|
component: `component props are not satisfied by PageProps<'${P}'>`;
|
|
284
355
|
} : R;
|
|
285
356
|
type ValidateRoutes<TRoutes extends readonly Route[]> = {
|
|
286
357
|
[K in keyof TRoutes]: ValidateRoute<TRoutes[K]>;
|
|
287
358
|
};
|
|
288
359
|
/**
|
|
289
|
-
*
|
|
290
|
-
*
|
|
360
|
+
* Refuses a key that is not a {@link RouteConfig} field — a typo'd `notfound` is otherwise a fallback page
|
|
361
|
+
* that never renders, and nothing at runtime looks for one.
|
|
291
362
|
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
363
|
+
* Excess-property checking did this while the object form was an overload of its own with a concrete
|
|
364
|
+
* parameter type. It cannot once the parameter is generic and inferred from the argument, because the
|
|
365
|
+
* inferred type *has* the extra key. The message here is the better one anyway: it lands on the field.
|
|
366
|
+
*/
|
|
367
|
+
type ValidateConfigKeys<T> = Exclude<keyof T, keyof RouteConfig> extends never ? T : T & Record<Exclude<keyof T, keyof RouteConfig>, 'not a defineRoutes field — the fields are routes, notFound and error'>;
|
|
368
|
+
/** The check for whichever of the two accepted shapes was passed. */
|
|
369
|
+
type ValidateInput<T> = T extends readonly Route[] ? ValidateRoutes<T> : T extends {
|
|
370
|
+
routes: infer R extends readonly Route[];
|
|
371
|
+
} ? ValidateConfigKeys<T> & {
|
|
372
|
+
routes: ValidateRoutes<R>;
|
|
373
|
+
} : T;
|
|
374
|
+
/** The route tuple inside either shape, so the return type carries the literals through both. */
|
|
375
|
+
type RoutesOf<T> = T extends readonly Route[] ? T : T extends {
|
|
376
|
+
routes: infer R extends readonly Route[];
|
|
377
|
+
} ? R : readonly Route[];
|
|
378
|
+
/**
|
|
379
|
+
* Declares the app's route table. Export the result as `routes` from `src/routes.ts` — the one file
|
|
380
|
+
* rshono requires. It only ever runs on the server, so importing server-only modules from it (inside
|
|
381
|
+
* `staticPaths`, say) is safe.
|
|
382
|
+
*
|
|
383
|
+
* Takes a {@link RouteConfig} — the `routes` array plus the optional `notFound` and `error` pages — or a
|
|
384
|
+
* bare {@link Route} array as shorthand for an app with neither.
|
|
294
385
|
*
|
|
295
|
-
* Beyond typing the config,
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
* PageProps<'/…'>`. Fix it by matching the page's `PageProps<Path>` type
|
|
299
|
-
* argument to the path it's mounted at.
|
|
386
|
+
* Beyond typing the config, every page is cross-checked against its own path: props not satisfied by
|
|
387
|
+
* `PageProps<'<its path>'>` make the `component` field a type error, and a `staticPaths` whose param sets do
|
|
388
|
+
* not fill the path makes that field one.
|
|
300
389
|
*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
390
|
+
* **One signature over both shapes, deliberately.** The array form used to be a second overload, which meant
|
|
391
|
+
* a mistake inside a bare array was reported as an overload-resolution failure whose *first* line was the
|
|
392
|
+
* object form's complaint — "Property 'routes' is missing" — pointing at a change the author should not make.
|
|
393
|
+
* The real message was on line 8. A single signature reports the argument once, at the field that is wrong.
|
|
394
|
+
*
|
|
395
|
+
* @param input - A {@link RouteConfig}, or the bare `routes` array.
|
|
396
|
+
* @returns The config, unchanged and fully typed; an array is wrapped as `{ routes }`.
|
|
304
397
|
*
|
|
305
398
|
* @example
|
|
306
399
|
* ```ts
|
|
@@ -324,14 +417,13 @@ type ValidateRoutes<TRoutes extends readonly Route[]> = {
|
|
|
324
417
|
* });
|
|
325
418
|
* ```
|
|
326
419
|
*
|
|
327
|
-
* @example
|
|
420
|
+
* @example
|
|
328
421
|
* ```ts
|
|
422
|
+
* // The shorthand, for an app with no notFound or error page.
|
|
329
423
|
* export const routes = defineRoutes([{ path: '/', component: () => import('./components/home') }]);
|
|
330
424
|
* ```
|
|
425
|
+
*
|
|
426
|
+
* @see {@link https://www.rshono.com/docs/routing | Docs — routing}
|
|
331
427
|
*/
|
|
332
|
-
export declare function defineRoutes<const
|
|
333
|
-
routes: ValidateRoutes<TRoutes>;
|
|
334
|
-
}): RouteConfig<TRoutes>;
|
|
335
|
-
export declare function defineRoutes<const TRoutes extends readonly Route[]>(routes: TRoutes & ValidateRoutes<TRoutes>): RouteConfig<TRoutes>;
|
|
428
|
+
export declare function defineRoutes<const T extends readonly Route[] | RouteConfig<readonly Route[]>>(input: T & ValidateInput<T>): RouteConfig<RoutesOf<T>>;
|
|
336
429
|
export {};
|
|
337
|
-
//# sourceMappingURL=router.d.ts.map
|
package/dist/router.js
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Narrows a {@link Route} to a {@link PageRoute} — `type` is optional on page routes, so anything not
|
|
3
|
+
* explicitly `'endpoint'` is one.
|
|
4
4
|
*
|
|
5
|
-
* @
|
|
6
|
-
* ```ts
|
|
7
|
-
* for (const route of routes) {
|
|
8
|
-
* if (isPageRoute(route)) console.log(route.render ?? 'dynamic');
|
|
9
|
-
* }
|
|
10
|
-
* ```
|
|
5
|
+
* @internal
|
|
11
6
|
*/
|
|
12
7
|
export function isPageRoute(route) {
|
|
13
8
|
return route.type !== 'endpoint';
|
package/dist/router.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router.js","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AA0NA;;;;;;;;;;GAUG;AACH,MAAM,UAAU,WAAW,CAAC,KAAY;IACtC,OAAO,KAAK,CAAC,IAAI,KAAK,UAAU,CAAC;AACnC,CAAC;AAwHD,MAAM,UAAU,YAAY,CAAC,KAAqC;IAChE,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC,CAAE,KAAqB,CAAC;AAC3E,CAAC","sourcesContent":["import type { Env, Handler } from 'hono';\nimport type { ParamKeys, ParamKeyToRecord } from 'hono/types';\nimport type { ReactNode } from 'react';\n// Type-only, so this stays a build-time module: the import is erased and none of `context.ts`'s\n// runtime machinery (AsyncLocalStorage, hono/cookie) is pulled in by importing `@rshono/core`.\nimport type { Ctx } from './runtime/context.js';\n\ntype Simplify<T> = { [K in keyof T]: T[K] } & {};\ntype UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never;\n\n/**\n * The `params` record implied by a route path pattern — one required `string` key\n * per `:param` segment, `Record<string, never>` for a path with no params.\n *\n * Paths use Hono's syntax, so `:id`, `:id{[0-9]+}` and `*` all work. You rarely\n * name this type directly; {@link PageProps} applies it for you.\n *\n * @typeParam P - The literal route path, e.g. `'/users/:id/posts/:postId'`.\n *\n * @example\n * ```ts\n * type P = PathParams<'/users/:id/posts/:postId'>; // { id: string; postId: string }\n * ```\n */\nexport type PathParams<P extends string> =\n ParamKeys<P> extends never ? Record<string, never> : Simplify<UnionToIntersection<ParamKeyToRecord<ParamKeys<P>>>>;\n\n/**\n * Props every page component receives. Pass the route's path as the type\n * argument to get `params` typed key-by-key; without it `params` falls back to\n * an open `Record<string, string>`.\n *\n * `defineRoutes` checks each page's props against `PageProps<path>` at compile\n * time, so a mismatched path literal is a type error at the route definition.\n *\n * The location props (`url` and `params`) mirror what a `'use client'` component\n * gets from `useNavigation()` — same names, same types — so moving a read across\n * the server/client line is a copy-paste.\n *\n * @typeParam Path - The literal path this page is mounted at, e.g. `'/profile/:id'`.\n * @typeParam E - The app's Hono {@link Env}, to type {@link Ctx.var} and\n * {@link Ctx.env} on {@link PageProps.ctx}.\n *\n * @example\n * ```tsx\n * import type { PageProps } from '@rshono/core';\n *\n * export default async function Profile({ params, url }: PageProps<'/profile/:id'>) {\n * const user = await db.getUser(params.id); // params.id is string\n * const tab = url.searchParams.get('tab') ?? 'overview';\n * return <Layout>{user.name} — {tab}</Layout>;\n * }\n * ```\n */\nexport interface PageProps<Path extends string = string, E extends Env = Env> {\n /**\n * The absolute browser-facing request {@link URL}, proxy-header aware\n * (`X-Forwarded-Host` / `-Proto`). Read `url.pathname`, `url.searchParams` and\n * the rest off it.\n *\n * A fresh instance per request that nothing else holds, so mutating it is local\n * to the page — but note it is *not* serializable, so a `'use client'` component\n * has to be handed `url.href` rather than `url`.\n *\n * On a prerendered page it is the build-time URL: a `render: 'static'` route is\n * rendered once, against `siteUrl` and with no query string, and that one file\n * then answers every request whatever its own query. So `url.searchParams` is\n * always empty there — read the query from `useNavigation().url` in a\n * `'use client'` component instead, or mark the route `render: 'dynamic'`.\n */\n url: URL;\n /** Matched route params for this request, e.g. `{ id: '42' }` for `/profile/:id`. */\n params: string extends Path ? Record<string, string> : PathParams<Path>;\n /**\n * The request context — the very object `getContext()` returns, handed to the\n * page so cookies, headers, env and middleware variables are reachable without\n * an import.\n *\n * Server-only, and never serialized: React renders a server component and puts\n * its *output* on the wire, not its props. It is also deliberately a\n * non-enumerable property, which has three consequences worth knowing:\n *\n * - It **cannot be handed to a `'use client'` component** — it wraps the live\n * request and response, which do not exist in the browser. Passing it\n * explicitly (`<Counter ctx={ctx} />`) fails the render with React's *\"Only\n * plain objects … can be passed to Client Components\"*. Read what you need on\n * the server and pass plain values down.\n * - Spreading the page's props instead (`<Counter {...props} />`) drops `ctx`\n * silently rather than failing, since the spread copies enumerables only.\n * (That spread still fails, mind — on `url`, which is enumerable and just as\n * unserializable. Pass the values you need.)\n * - `Object.keys(props)`, `JSON.stringify(props)` and friends don't see it.\n *\n * Reading it on a `render: 'static'` route throws: a prerendered page has no\n * per-request context at build time. Mark the route `render: 'dynamic'` (or use\n * the `url` / `params` props, which are available either way — with the\n * build-time caveats noted on `url`).\n *\n * @example\n * ```tsx\n * export default function Dashboard({ ctx }: PageProps) {\n * const session = ctx.cookies.get('session');\n * if (!session) redirect('/login');\n * return <Layout>Signed in as {session}</Layout>;\n * }\n * ```\n */\n ctx: Ctx<E>;\n}\n\n/**\n * A page: a React **server component** that renders the entire document\n * (`<html>…</html>`), usually via a shared layout. It may be `async` and await\n * data directly.\n *\n * Each page module must default-export exactly one of these. Interactive parts\n * belong in `'use client'` components the page imports — only those ship JS.\n *\n * @typeParam P - The component's props; for a page these are {@link PageProps}.\n */\nexport type PageComponent<P = any> = (props: P) => ReactNode | Promise<ReactNode>;\n\n/**\n * The shape an `{ type: 'endpoint' }` route's server module must have: a single\n * named `handler` export. The module only ever loads on the server, so it is\n * safe to import a database client or read secrets from it.\n *\n * @example\n * ```ts\n * // src/health.ts\n * import type { Handler } from '@rshono/core';\n *\n * export const handler: Handler = (c) => c.json({ ok: true });\n * ```\n */\nexport interface EndpointServerModule {\n /** A Hono {@link Handler} handling every request matched by the route. */\n handler: Handler;\n}\n\n/**\n * A page route — a path rendered by a server component. This is the default\n * route kind, so `type` can be omitted.\n *\n * @example\n * ```ts\n * { path: '/profile/:id', component: () => import('./components/profile') }\n * ```\n */\nexport interface PageRoute {\n /** Discriminates a page from an endpoint; optional because `'page'` is the default. */\n type?: 'page';\n /** Hono-style path pattern, e.g. `/`, `/profile/:id`, `/files/*`. */\n path: string;\n /**\n * Dynamic import of the page module, whose default export is the\n * {@link PageComponent}.\n *\n * Write it inline as shown — the framework detects that exact\n * `() => import('…')` form and injects Rspack's `'use server-entry'`\n * directive into the module for you (that directive is what attaches the\n * page's client JS/CSS, giving per-page code splitting). If you wire the\n * component up any other way — a variable, a barrel re-export, a computed\n * specifier — add `'use server-entry'` as the first line of the page module\n * yourself; the framework throws a descriptive error when neither happened.\n */\n component: () => Promise<{ default: PageComponent }>;\n /** `'static'` prerenders the route at build time; `'dynamic'` (the default) renders per request. */\n render?: 'static' | 'dynamic';\n /**\n * For a `render: 'static'` route with params: the param sets to prerender, one\n * HTML file each. Runs at build time only, on the server, so it may hit a\n * database or read the filesystem.\n *\n * A parameterised static route without `staticPaths` falls back to rendering\n * per request (with a build warning). Wildcard (`*`), optional and regex\n * params can't be prerendered.\n *\n * @example\n * ```ts\n * {\n * path: '/docs/:slug',\n * render: 'static',\n * component: () => import('./components/documentation'),\n * staticPaths: async () => (await db.docs.all()).map((d) => ({ slug: d.slug })),\n * }\n * ```\n */\n staticPaths?: () => Array<Record<string, string>> | Promise<Array<Record<string, string>>>;\n}\n\n/**\n * An endpoint route — a path served by a raw Hono handler instead of a React\n * component. Use it for JSON APIs, webhooks, redirects, feeds, or anything that\n * isn't an HTML page.\n *\n * @example\n * ```ts\n * { type: 'endpoint', path: '/api/health', server: () => import('./health') }\n * ```\n */\nexport interface EndpointRoute {\n /** Marks this route as an endpoint rather than a page. Required. */\n type: 'endpoint';\n /** Hono-style path pattern, e.g. `/api/health`, `/api/users/:id`. */\n path: string;\n /** HTTP method to match. Defaults to `'all'` — every method. */\n method?: HTTPMethod;\n /** Dynamic import of the {@link EndpointServerModule} exporting `handler`. */\n server: () => Promise<EndpointServerModule>;\n}\n\n/** HTTP methods an {@link EndpointRoute} can match. `'all'` matches every method. */\nexport type HTTPMethod = 'get' | 'post' | 'put' | 'patch' | 'delete' | 'head' | 'options' | 'all';\n\n/** Any entry in the `routes` array: a {@link PageRoute} or an {@link EndpointRoute}. */\nexport type Route = PageRoute | EndpointRoute;\n\n/**\n * Type guard narrowing a {@link Route} to a {@link PageRoute}. Because `type` is\n * optional on page routes, anything not explicitly `'endpoint'` is a page.\n *\n * @example\n * ```ts\n * for (const route of routes) {\n * if (isPageRoute(route)) console.log(route.render ?? 'dynamic');\n * }\n * ```\n */\nexport function isPageRoute(route: Route): route is PageRoute {\n return route.type !== 'endpoint';\n}\n\n/**\n * A page the framework falls back to rather than routes to — `notFound` and\n * `error` in {@link RouteConfig}. Same contract as a {@link PageRoute}\n * `component`, without a path of its own.\n */\nexport interface FallbackPage {\n /** Dynamic import of the page module; its default export is the {@link PageComponent}. */\n component: () => Promise<{ default: PageComponent }>;\n}\n\n/**\n * The error detail handed to the `error` page. Redacted in production: the\n * message is a generic `'Internal Server Error'` and there is no `stack`. In dev\n * you get the real message plus the stack.\n */\nexport interface ErrorInfo {\n /** The thrown error's message in dev; `'Internal Server Error'` in production. */\n message: string;\n /** The stack trace. Present in dev only. */\n stack?: string;\n}\n\n/**\n * Props for the `error` page declared in {@link RouteConfig.error} — the usual\n * {@link PageProps} plus the redaction-aware {@link ErrorInfo}.\n *\n * @typeParam E - The app's Hono {@link Env}, forwarded to {@link PageProps.ctx}.\n *\n * @example\n * ```tsx\n * import type { ErrorPageProps } from '@rshono/core';\n *\n * export default function ServerError({ error }: ErrorPageProps) {\n * return <html><body><h1>Something went wrong</h1><p>{error.message}</p></body></html>;\n * }\n * ```\n */\nexport type ErrorPageProps<E extends Env = Env> = PageProps<string, E> & { error: ErrorInfo };\n\n/**\n * The object form accepted by {@link defineRoutes}: the route table plus the two\n * optional framework-owned pages.\n *\n * @typeParam TRoutes - Inferred tuple of route literals, which is what makes the\n * per-route `path` → props check possible.\n */\nexport interface RouteConfig<TRoutes extends readonly Route[] = readonly Route[]> {\n /** Every page and endpoint in the app, matched in order. */\n routes: TRoutes;\n /** Page rendered with a 404 status for unmatched paths and for `notFound()` calls. */\n notFound?: FallbackPage;\n /** Page rendered with a 500 status when a request throws. Receives {@link ErrorPageProps}. */\n error?: FallbackPage;\n}\n\n// `PageProps<P, any>`, not `PageProps<P>`: this check is about the *path* matching the page's\n// `params`, and pinning the Env to the default would additionally demand that a page declaring its\n// own (`PageProps<'/x', MyEnv>`, to type `ctx.var`) accept a `Ctx<Env>` — which it doesn't, so every\n// such page would fail its own route check. `any` makes `ctx` compatible either way.\ntype ValidateRoute<R> = R extends {\n path: infer P extends string;\n component: () => Promise<{ default: PageComponent<infer CP> }>;\n}\n ? [PageProps<P, any>] extends [CP]\n ? R\n : R & { component: `component props are not satisfied by PageProps<'${P}'>` }\n : R;\n\ntype ValidateRoutes<TRoutes extends readonly Route[]> = { [K in keyof TRoutes]: ValidateRoute<TRoutes[K]> };\n\n/**\n * Declares the app's route table. Default-export the result as `routes` from\n * `src/routes.ts` — the one file rshono requires.\n *\n * `routes.ts` only ever runs on the server, so importing server-only modules\n * from it (e.g. inside `staticPaths`) is safe.\n *\n * Beyond typing the config, this cross-checks every page against its own path:\n * if a component's props aren't satisfied by `PageProps<'<its path>'>`, the\n * `component` field errors with `component props are not satisfied by\n * PageProps<'/…'>`. Fix it by matching the page's `PageProps<Path>` type\n * argument to the path it's mounted at.\n *\n * @param config - A {@link RouteConfig}, or a bare {@link Route} array as\n * shorthand when there are no `notFound` / `error` pages.\n * @returns The config, unchanged and fully typed.\n *\n * @example\n * ```ts\n * // src/routes.ts\n * import { defineRoutes } from '@rshono/core';\n *\n * export const routes = defineRoutes({\n * routes: [\n * { path: '/', component: () => import('./components/home') },\n * { path: '/profile/:id', component: () => import('./components/profile') },\n * {\n * path: '/docs/:slug',\n * render: 'static',\n * component: () => import('./components/documentation'),\n * staticPaths: async () => [{ slug: 'getting-started' }, { slug: 'deployment' }],\n * },\n * { type: 'endpoint', path: '/api/health', server: () => import('./health') },\n * ],\n * notFound: { component: () => import('./components/404') },\n * error: { component: () => import('./components/500') },\n * });\n * ```\n *\n * @example Array shorthand\n * ```ts\n * export const routes = defineRoutes([{ path: '/', component: () => import('./components/home') }]);\n * ```\n */\nexport function defineRoutes<const TRoutes extends readonly Route[]>(\n config: RouteConfig<TRoutes> & { routes: ValidateRoutes<TRoutes> },\n): RouteConfig<TRoutes>;\nexport function defineRoutes<const TRoutes extends readonly Route[]>(routes: TRoutes & ValidateRoutes<TRoutes>): RouteConfig<TRoutes>;\nexport function defineRoutes(input: readonly Route[] | RouteConfig): RouteConfig {\n return Array.isArray(input) ? { routes: input } : (input as RouteConfig);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"router.js","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAkQA;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,KAAY;IACtC,OAAO,KAAK,CAAC,IAAI,KAAK,UAAU,CAAC;AACnC,CAAC;AA6LD,MAAM,UAAU,YAAY,CAAC,KAAqC;IAChE,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC,CAAE,KAAqB,CAAC;AAC3E,CAAC","sourcesContent":["import type { Env, Handler } from 'hono';\nimport type { ParamKeys, ParamKeyToRecord } from 'hono/types';\nimport type { ReactNode } from 'react';\n// Type-only, so importing `@rshono/core` pulls in none of `context.ts`'s runtime machinery.\nimport type { RequestContext } from './runtime/context.js';\n\ntype Simplify<T> = { [K in keyof T]: T[K] } & {};\ntype UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never;\n\n/**\n * The `params` record implied by a route path pattern — one required `string` key per `:param`\n * segment, `Record<string, never>` for a path with none. Paths use Hono's syntax, so `:id`,\n * `:id{[0-9]+}` and `*` all work.\n *\n * You rarely name this directly; {@link PageProps} applies it for you.\n *\n * @typeParam P - The literal route path, e.g. `'/users/:id/posts/:postId'`.\n *\n * @example\n * ```ts\n * type P = PathParams<'/users/:id/posts/:postId'>; // { id: string; postId: string }\n * ```\n *\n * @see {@link https://hono.dev/docs/api/routing#path-parameter | Hono — path parameters}\n */\nexport type PathParams<P extends string> =\n ParamKeys<P> extends never ? Record<string, never> : Simplify<UnionToIntersection<ParamKeyToRecord<ParamKeys<P>>>>;\n\n/**\n * Props every page component receives. Pass the route's path as the type argument to get `params`\n * typed key-by-key; without it `params` falls back to an open `Record<string, string>`.\n *\n * `defineRoutes` checks each page's props against `PageProps<path>`, so a mismatched path literal is\n * a type error at the route definition. `url` and `params` mirror what `useNavigation()` gives a\n * `'use client'` component, so a read moves across the server/client line unchanged.\n *\n * @typeParam Path - The literal path this page is mounted at, e.g. `'/profile/:id'`.\n * @typeParam E - The app's Hono {@link Env}, to type {@link RequestContext.var} and\n * {@link RequestContext.env} on {@link PageProps.ctx}.\n *\n * @example\n * ```tsx\n * import type { PageProps } from '@rshono/core';\n *\n * export default async function Profile({ params, url }: PageProps<'/profile/:id'>) {\n * const user = await db.getUser(params.id); // params.id is string\n * const tab = url.searchParams.get('tab') ?? 'overview';\n * return <Layout>{user.name} — {tab}</Layout>;\n * }\n * ```\n *\n * @see {@link https://www.rshono.com/docs/pages#page-props | Docs — page props}\n */\nexport interface PageProps<Path extends string = string, E extends Env = Env> {\n /**\n * The absolute browser-facing request {@link URL}, proxy-header aware (`X-Forwarded-Host` /\n * `-Proto`). A fresh instance per request, so mutating it is local to the page; it is not\n * serializable, so hand a `'use client'` component `url.href` rather than `url`.\n *\n * On a `render: 'static'` route this is the build-time URL — rendered once against `siteUrl`, so the\n * origin is `siteUrl`'s and `url.searchParams` is always empty, on first paint and after a soft\n * navigation alike. **`useNavigation().url` is the same frozen URL, not a way around it**: the payload\n * carries one `href` and both readings come from it. Mark the route `render: 'dynamic'` if the page\n * depends on the query; a `'use client'` component that only wants it after hydration can read\n * `location.search` in an effect.\n */\n url: URL;\n /** Matched route params for this request, e.g. `{ id: '42' }` for `/profile/:id`. */\n params: string extends Path ? Record<string, string> : PathParams<Path>;\n /**\n * The request context — the object `getRequestContext()` returns, handed to the page so cookies,\n * headers, env and middleware variables are reachable without an import.\n *\n * Server-only, non-enumerable (a `{...props}` spread and `JSON.stringify` both skip it) and never\n * serialized. Passing it to a `'use client'` component fails the render, because it wraps the live\n * request — read what you need here and pass plain values down.\n *\n * Non-enumerable is the one place this API breaks a JavaScript expectation, and it is unavoidable: an\n * enumerable `ctx` would put `ctx.hono.env` — every binding and secret — into React's dev-only\n * serialization of a server component's props, which walks own enumerable properties. So `<Child\n * {...props} />` hands a **server** child `ctx: undefined` with no error, while the type says otherwise.\n * Nested server components are meant to call `getRequestContext()` for the same object rather than\n * receive it, which is also the fix if a spread has already cost you an afternoon.\n *\n * Reading it on a `render: 'static'` route throws: a prerendered page has no per-request context.\n * Mark the route `render: 'dynamic'`, or use the `url` / `params` props.\n *\n * @example\n * ```tsx\n * export default function Dashboard({ ctx }: PageProps) {\n * const session = ctx.cookies.get('session');\n * if (!session) redirect('/login');\n * return <Layout>Signed in as {session}</Layout>;\n * }\n * ```\n */\n ctx: RequestContext<E>;\n}\n\n/**\n * A page: a React **server component** rendering the entire document (`<html>…</html>`), usually via\n * a shared layout. It may be `async` and await data directly.\n *\n * Each page module default-exports exactly one. Interactive parts belong in `'use client'` components\n * the page imports — only those ship JS.\n *\n * @typeParam P - The component's props; for a page these are {@link PageProps}.\n *\n * @see {@link https://react.dev/reference/rsc/server-components | React — Server Components}\n * @see {@link https://www.rshono.com/docs/pages | Docs — pages}\n */\n// `any`, not `unknown`: this default is what an unparameterised `PageComponent` means in a user's own\n// annotation, and `unknown` props would reject every component that declares the props it actually takes.\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type PageComponent<P = any> = (props: P) => ReactNode | Promise<ReactNode>;\n\n/**\n * The shape an `{ type: 'endpoint' }` route's server module must have: a single named `handler`\n * export. It only ever loads on the server, so importing a database client or reading secrets from it\n * is safe.\n *\n * @example\n * ```ts\n * // src/health.ts\n * import type { Handler } from 'hono';\n *\n * export const handler: Handler = (c) => c.json({ ok: true });\n * ```\n *\n * @see {@link https://www.rshono.com/docs/routing#endpoint-routes | Docs — endpoint routes}\n */\nexport interface EndpointServerModule {\n /**\n * A Hono {@link Handler} for every request the route matches. It is passed Hono's `Context`, so the\n * request, the response builders (`c.json`, `c.text`, `c.body`) and middleware variables are all\n * reached through it.\n *\n * @see {@link https://hono.dev/docs/api/context | Hono — Context}\n */\n handler: Handler;\n}\n\n/**\n * A page route — a path rendered by a server component. This is the default\n * route kind, so `type` can be omitted.\n *\n * @example\n * ```ts\n * { path: '/profile/:id', component: () => import('./components/profile') }\n * ```\n */\nexport interface PageRoute {\n /** Discriminates a page from an endpoint; optional because `'page'` is the default. */\n type?: 'page';\n /**\n * Hono-style path pattern, e.g. `/`, `/profile/:id`, `/files/*`. Routes are matched in\n * declaration order.\n *\n * @see {@link https://hono.dev/docs/api/routing | Hono — routing}\n */\n path: string;\n /**\n * Dynamic import of the page module, whose default export is the {@link PageComponent}.\n *\n * Write it inline as `() => import('…')`: the framework detects that exact form and injects the\n * `'use server-entry'` directive that attaches the page's client JS and CSS. Wire the component up\n * any other way — a variable, a barrel re-export, a computed specifier — and you have to put\n * `'use server-entry'` on the page module's first line yourself.\n *\n * @example\n * ```ts\n * component: () => import('./components/profile')\n * ```\n *\n * @see {@link https://www.rshono.com/docs/pages#the-use-server-entry-directive | Docs — the `'use server-entry'` directive}\n */\n component: () => Promise<{ default: PageComponent }>;\n /** `'static'` prerenders the route at build time; `'dynamic'` (the default) renders per request. */\n render?: 'static' | 'dynamic';\n /**\n * For a `render: 'static'` route with params: the param sets to prerender, one page each. Runs at\n * build time on the server, so it may hit a database or read the filesystem.\n *\n * A parameterised static route without this falls back to rendering per request, with a build\n * warning. Wildcard (`*`), optional and regex params cannot be prerendered.\n *\n * **Every value has to be one portable file name**, since that is what a prerendered page is stored as,\n * and the build fails naming the value rather than writing a page nothing will serve. So: no\n * `\\ / : * ? \" < > |` or control characters, no trailing `.` or space, and not a reserved Windows device\n * name (`CON`, `NUL`, `COM1`, …) — the last two enforced everywhere, so a build that works on macOS is\n * not one that fails in CI on Windows.\n *\n * @example\n * ```ts\n * {\n * path: '/docs/:slug',\n * render: 'static',\n * component: () => import('./components/documentation'),\n * staticPaths: async () => (await db.docs.all()).map((d) => ({ slug: d.slug })),\n * }\n * ```\n *\n * @see {@link https://www.rshono.com/docs/routing#static-rendering | Docs — static rendering}\n */\n staticPaths?: () => Array<Record<string, string>> | Promise<Array<Record<string, string>>>;\n}\n\n/**\n * An endpoint route — a path served by a raw Hono handler instead of a React\n * component. Use it for JSON APIs, webhooks, redirects, feeds, or anything that\n * isn't an HTML page.\n *\n * @example\n * ```ts\n * { type: 'endpoint', path: '/api/health', server: () => import('./health') }\n * ```\n *\n * @see {@link https://www.rshono.com/docs/routing#endpoint-routes | Docs — endpoint routes}\n */\nexport interface EndpointRoute {\n /** Marks this route as an endpoint rather than a page. Required. */\n type: 'endpoint';\n /**\n * Hono-style path pattern, e.g. `/api/health`, `/api/users/:id`.\n *\n * @see {@link https://hono.dev/docs/api/routing | Hono — routing}\n */\n path: string;\n /**\n * HTTP method to match, or a list of them. Defaults to `'all'` — every method.\n *\n * There is no `'head'`: Hono dispatches a `HEAD` as a `GET` and strips the body off the response, so a\n * `HEAD` is already answered by the `'get'` handler (and by `'all'`), and a route registered for `HEAD`\n * alone would never be reached.\n *\n * A list is how a two-method endpoint says so; `'all'` inside one is refused, since it is either the\n * whole thing or a mistake. A method the route does not name gets Hono's 404 rather than the handler.\n *\n * @example\n * ```ts\n * { type: 'endpoint', path: '/api/session', method: ['get', 'delete'], server: () => import('./session') }\n * ```\n */\n method?: HTTPMethod | readonly HTTPMethod[];\n /** Dynamic import of the {@link EndpointServerModule} exporting `handler`. */\n server: () => Promise<EndpointServerModule>;\n}\n\n/**\n * HTTP methods an {@link EndpointRoute} can match. `'all'` matches every method.\n *\n * No `'head'`, deliberately — see {@link EndpointRoute.method}. A `HEAD` reaches the `'get'` handler.\n */\nexport type HTTPMethod = 'get' | 'post' | 'put' | 'patch' | 'delete' | 'options' | 'all';\n\n/** Any entry in the `routes` array: a {@link PageRoute} or an {@link EndpointRoute}. */\nexport type Route = PageRoute | EndpointRoute;\n\n/**\n * Narrows a {@link Route} to a {@link PageRoute} — `type` is optional on page routes, so anything not\n * explicitly `'endpoint'` is one.\n *\n * @internal\n */\nexport function isPageRoute(route: Route): route is PageRoute {\n return route.type !== 'endpoint';\n}\n\n/**\n * A page the framework falls back to rather than routes to — `notFound` and `error` in\n * {@link RouteConfig}. Same contract as a {@link PageRoute} `component`, without a path of its own.\n */\nexport interface FallbackPage {\n /** Dynamic import of the page module; its default export is the {@link PageComponent}. */\n component: () => Promise<{ default: PageComponent }>;\n}\n\n/**\n * The error detail handed to the `error` page. Redacted in production — a generic\n * `'Internal Server Error'` and no `stack`; in dev, the real message and stack.\n */\nexport interface ErrorPageInfo {\n /** The thrown error's message in dev; `'Internal Server Error'` in production. */\n message: string;\n /**\n * The stack trace — **dev only**, and `undefined` in every build. Optional for that reason rather than\n * because some errors lack one, so a page that renders it should guard on it, not on a mode flag.\n */\n stack?: string;\n}\n\n/**\n * Props for the `error` page declared in {@link RouteConfig.error} — the usual {@link PageProps} plus\n * the redaction-aware {@link ErrorPageInfo}.\n *\n * In a build `error.message` is the generic `'Internal Server Error'` and `error.stack` is `undefined`, so\n * the page below guards on the stack rather than on a mode flag — there is no mode flag to guard on.\n *\n * @typeParam E - The app's Hono {@link Env}, forwarded to {@link PageProps.ctx}.\n *\n * @example\n * ```tsx\n * import type { ErrorPageProps } from '@rshono/core';\n *\n * export default function ServerError({ error }: ErrorPageProps) {\n * return (\n * <html>\n * <body>\n * <h1>Something went wrong</h1>\n * <p>{error.message}</p>\n * {error.stack && <pre>{error.stack}</pre>}\n * </body>\n * </html>\n * );\n * }\n * ```\n */\nexport type ErrorPageProps<E extends Env = Env> = PageProps<string, E> & {\n /** The error that failed the request, redacted in production — see {@link ErrorPageInfo}. */\n error: ErrorPageInfo;\n};\n\n/**\n * The object form accepted by {@link defineRoutes}: the route table plus the two\n * optional framework-owned pages.\n *\n * @typeParam TRoutes - Inferred tuple of route literals, which is what makes the\n * per-route `path` → props check possible.\n *\n * @see {@link https://www.rshono.com/docs/routing#notfound-and-error | Docs — notFound and error pages}\n */\nexport interface RouteConfig<TRoutes extends readonly Route[] = readonly Route[]> {\n /** Every page and endpoint in the app, matched in order. */\n routes: TRoutes;\n /** Page rendered with a 404 status for unmatched paths and for `notFound()` calls. */\n notFound?: FallbackPage;\n /**\n * Page rendered with a 500 status when a request throws — a page component, a page module that will not\n * load, an endpoint, a server action, or middleware. Receives {@link ErrorPageProps}.\n *\n * It is a *fresh* render, with its own flight payload, so it hydrates and behaves like any other page.\n * An app that declares none gets the framework's plain 500 document instead. If the `error` page itself\n * throws, that is reported too and the framework's document answers.\n */\n error?: FallbackPage;\n}\n\n/**\n * The same check for `staticPaths`, whose param sets have to fill the route's own path: a key that does not\n * is otherwise a build-time throw from `interpolatePath` rather than a type error.\n *\n * Keys only, not full assignability, because the declared field type is `Record<string, string>` and a\n * `staticPaths` annotated as returning exactly that has to stay accepted — an index signature carries no\n * key to check, so it passes. Skipped where the path has no params, because `staticPaths` is not called for\n * such a route at all and an error there would be about the wrong thing.\n */\ntype ValidateStaticPaths<R, P extends string> =\n ParamKeys<P> extends never\n ? R\n : R extends { staticPaths: () => infer Sets }\n ? Awaited<Sets> extends ReadonlyArray<infer Set>\n ? [keyof PathParams<P>] extends [keyof Set]\n ? R\n : R & { staticPaths: `every param set staticPaths returns needs the params of '${P}'` }\n : R\n : R;\n\n// `PageProps<P, any>`, not `PageProps<P>`: only the *path* is being checked, and pinning the Env would\n// fail every page that declares its own (`PageProps<'/x', MyEnv>`, to type `ctx.var`).\ntype ValidateRoute<R> = R extends {\n path: infer P extends string;\n component: () => Promise<{ default: PageComponent<infer CP> }>;\n}\n ? // eslint-disable-next-line @typescript-eslint/no-explicit-any -- the Env is deliberately unpinned; see above.\n [PageProps<P, any>] extends [CP]\n ? ValidateStaticPaths<R, P>\n : R & { component: `component props are not satisfied by PageProps<'${P}'>` }\n : R;\n\ntype ValidateRoutes<TRoutes extends readonly Route[]> = { [K in keyof TRoutes]: ValidateRoute<TRoutes[K]> };\n\n/**\n * Refuses a key that is not a {@link RouteConfig} field — a typo'd `notfound` is otherwise a fallback page\n * that never renders, and nothing at runtime looks for one.\n *\n * Excess-property checking did this while the object form was an overload of its own with a concrete\n * parameter type. It cannot once the parameter is generic and inferred from the argument, because the\n * inferred type *has* the extra key. The message here is the better one anyway: it lands on the field.\n */\ntype ValidateConfigKeys<T> =\n Exclude<keyof T, keyof RouteConfig> extends never\n ? T\n : T & Record<Exclude<keyof T, keyof RouteConfig>, 'not a defineRoutes field — the fields are routes, notFound and error'>;\n\n/** The check for whichever of the two accepted shapes was passed. */\ntype ValidateInput<T> = T extends readonly Route[]\n ? ValidateRoutes<T>\n : T extends { routes: infer R extends readonly Route[] }\n ? ValidateConfigKeys<T> & { routes: ValidateRoutes<R> }\n : T;\n\n/** The route tuple inside either shape, so the return type carries the literals through both. */\ntype RoutesOf<T> = T extends readonly Route[] ? T : T extends { routes: infer R extends readonly Route[] } ? R : readonly Route[];\n\n/**\n * Declares the app's route table. Export the result as `routes` from `src/routes.ts` — the one file\n * rshono requires. It only ever runs on the server, so importing server-only modules from it (inside\n * `staticPaths`, say) is safe.\n *\n * Takes a {@link RouteConfig} — the `routes` array plus the optional `notFound` and `error` pages — or a\n * bare {@link Route} array as shorthand for an app with neither.\n *\n * Beyond typing the config, every page is cross-checked against its own path: props not satisfied by\n * `PageProps<'<its path>'>` make the `component` field a type error, and a `staticPaths` whose param sets do\n * not fill the path makes that field one.\n *\n * **One signature over both shapes, deliberately.** The array form used to be a second overload, which meant\n * a mistake inside a bare array was reported as an overload-resolution failure whose *first* line was the\n * object form's complaint — \"Property 'routes' is missing\" — pointing at a change the author should not make.\n * The real message was on line 8. A single signature reports the argument once, at the field that is wrong.\n *\n * @param input - A {@link RouteConfig}, or the bare `routes` array.\n * @returns The config, unchanged and fully typed; an array is wrapped as `{ routes }`.\n *\n * @example\n * ```ts\n * // src/routes.ts\n * import { defineRoutes } from '@rshono/core';\n *\n * export const routes = defineRoutes({\n * routes: [\n * { path: '/', component: () => import('./components/home') },\n * { path: '/profile/:id', component: () => import('./components/profile') },\n * {\n * path: '/docs/:slug',\n * render: 'static',\n * component: () => import('./components/documentation'),\n * staticPaths: async () => [{ slug: 'getting-started' }, { slug: 'deployment' }],\n * },\n * { type: 'endpoint', path: '/api/health', server: () => import('./health') },\n * ],\n * notFound: { component: () => import('./components/404') },\n * error: { component: () => import('./components/500') },\n * });\n * ```\n *\n * @example\n * ```ts\n * // The shorthand, for an app with no notFound or error page.\n * export const routes = defineRoutes([{ path: '/', component: () => import('./components/home') }]);\n * ```\n *\n * @see {@link https://www.rshono.com/docs/routing | Docs — routing}\n */\nexport function defineRoutes<const T extends readonly Route[] | RouteConfig<readonly Route[]>>(input: T & ValidateInput<T>): RouteConfig<RoutesOf<T>>;\nexport function defineRoutes(input: readonly Route[] | RouteConfig): RouteConfig {\n return Array.isArray(input) ? { routes: input } : (input as RouteConfig);\n}\n"]}
|