@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.
Files changed (208) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +348 -158
  3. package/bin/rshono.mjs +3 -4
  4. package/dist/builder/env-shadow-loader.cjs +123 -8
  5. package/dist/builder/page-entry-loader.cjs +28 -1
  6. package/dist/builder/page-files.d.ts +0 -1
  7. package/dist/builder/page-files.js +7 -3
  8. package/dist/builder/page-files.js.map +1 -1
  9. package/dist/builder/public-env.d.ts +7 -1
  10. package/dist/builder/public-env.js +7 -0
  11. package/dist/builder/public-env.js.map +1 -1
  12. package/dist/builder/react-versions.d.ts +12 -0
  13. package/dist/builder/react-versions.js +74 -0
  14. package/dist/builder/react-versions.js.map +1 -0
  15. package/dist/builder/rspack-config.d.ts +13 -4
  16. package/dist/builder/rspack-config.js +136 -53
  17. package/dist/builder/rspack-config.js.map +1 -1
  18. package/dist/builder/server-only-imports.d.ts +32 -0
  19. package/dist/builder/server-only-imports.js +51 -0
  20. package/dist/builder/server-only-imports.js.map +1 -0
  21. package/dist/cli/build.d.ts +2 -3
  22. package/dist/cli/build.js +51 -13
  23. package/dist/cli/build.js.map +1 -1
  24. package/dist/cli/dev.d.ts +2 -3
  25. package/dist/cli/dev.js +149 -45
  26. package/dist/cli/dev.js.map +1 -1
  27. package/dist/cli/exit.d.ts +12 -0
  28. package/dist/cli/exit.js +16 -0
  29. package/dist/cli/exit.js.map +1 -0
  30. package/dist/cli/index.d.ts +0 -1
  31. package/dist/cli/index.js +91 -30
  32. package/dist/cli/index.js.map +1 -1
  33. package/dist/cli/start.d.ts +0 -1
  34. package/dist/cli/start.js +15 -18
  35. package/dist/cli/start.js.map +1 -1
  36. package/dist/config.d.ts +72 -78
  37. package/dist/config.js +17 -1
  38. package/dist/config.js.map +1 -1
  39. package/dist/deploy/aws-lambda/runtime.d.ts +4 -7
  40. package/dist/deploy/aws-lambda/runtime.js +5 -8
  41. package/dist/deploy/aws-lambda/runtime.js.map +1 -1
  42. package/dist/deploy/build-marker.d.ts +9 -6
  43. package/dist/deploy/build-marker.js +8 -4
  44. package/dist/deploy/build-marker.js.map +1 -1
  45. package/dist/deploy/cloudflare/build.d.ts +0 -1
  46. package/dist/deploy/cloudflare/build.js +17 -13
  47. package/dist/deploy/cloudflare/build.js.map +1 -1
  48. package/dist/deploy/cloudflare/runtime.d.ts +2 -6
  49. package/dist/deploy/cloudflare/runtime.js +73 -68
  50. package/dist/deploy/cloudflare/runtime.js.map +1 -1
  51. package/dist/deploy/contract.d.ts +57 -41
  52. package/dist/deploy/contract.js.map +1 -1
  53. package/dist/deploy/filesystem.d.ts +3 -6
  54. package/dist/deploy/filesystem.js +23 -20
  55. package/dist/deploy/filesystem.js.map +1 -1
  56. package/dist/deploy/node/runtime.d.ts +5 -3
  57. package/dist/deploy/node/runtime.js +23 -9
  58. package/dist/deploy/node/runtime.js.map +1 -1
  59. package/dist/deploy/presets.d.ts +34 -30
  60. package/dist/deploy/presets.js +56 -44
  61. package/dist/deploy/presets.js.map +1 -1
  62. package/dist/deploy/public-paths.d.ts +42 -0
  63. package/dist/deploy/public-paths.js +66 -0
  64. package/dist/deploy/public-paths.js.map +1 -0
  65. package/dist/deploy/vercel/build.d.ts +0 -1
  66. package/dist/deploy/vercel/build.js +25 -16
  67. package/dist/deploy/vercel/build.js.map +1 -1
  68. package/dist/deploy/vercel/runtime.d.ts +10 -7
  69. package/dist/deploy/vercel/runtime.js +48 -11
  70. package/dist/deploy/vercel/runtime.js.map +1 -1
  71. package/dist/index.d.ts +19 -17
  72. package/dist/index.js +20 -10
  73. package/dist/index.js.map +1 -1
  74. package/dist/router.d.ts +212 -120
  75. package/dist/router.js +3 -8
  76. package/dist/router.js.map +1 -1
  77. package/dist/runtime/boundaries.d.ts +62 -48
  78. package/dist/runtime/boundaries.js +32 -34
  79. package/dist/runtime/boundaries.js.map +1 -1
  80. package/dist/runtime/client.d.ts +20 -11
  81. package/dist/runtime/client.js +20 -10
  82. package/dist/runtime/client.js.map +1 -1
  83. package/dist/runtime/context.d.ts +304 -123
  84. package/dist/runtime/context.js +377 -149
  85. package/dist/runtime/context.js.map +1 -1
  86. package/dist/runtime/control.d.ts +15 -1
  87. package/dist/runtime/control.js +24 -0
  88. package/dist/runtime/control.js.map +1 -1
  89. package/dist/runtime/dev-protocol.d.ts +4 -9
  90. package/dist/runtime/dev-protocol.js.map +1 -1
  91. package/dist/runtime/empty-server-app.d.ts +0 -1
  92. package/dist/runtime/entry.client.d.ts +4 -1
  93. package/dist/runtime/entry.client.js +455 -313
  94. package/dist/runtime/entry.client.js.map +1 -1
  95. package/dist/runtime/entry.rsc.d.ts +15 -7
  96. package/dist/runtime/entry.rsc.js +811 -252
  97. package/dist/runtime/entry.rsc.js.map +1 -1
  98. package/dist/runtime/entry.ssr.d.ts +27 -10
  99. package/dist/runtime/entry.ssr.js +30 -48
  100. package/dist/runtime/entry.ssr.js.map +1 -1
  101. package/dist/runtime/failure-document.d.ts +11 -0
  102. package/dist/runtime/failure-document.js +35 -0
  103. package/dist/runtime/failure-document.js.map +1 -0
  104. package/dist/runtime/flight-inject.d.ts +23 -0
  105. package/dist/runtime/flight-inject.js +403 -0
  106. package/dist/runtime/flight-inject.js.map +1 -0
  107. package/dist/runtime/hot-update.d.ts +44 -0
  108. package/dist/runtime/hot-update.js +44 -0
  109. package/dist/runtime/hot-update.js.map +1 -0
  110. package/dist/runtime/navigation.d.ts +46 -54
  111. package/dist/runtime/navigation.js +22 -63
  112. package/dist/runtime/navigation.js.map +1 -1
  113. package/dist/runtime/request.d.ts +42 -8
  114. package/dist/runtime/request.js +84 -7
  115. package/dist/runtime/request.js.map +1 -1
  116. package/dist/runtime/server.d.ts +20 -18
  117. package/dist/runtime/server.js +22 -17
  118. package/dist/runtime/server.js.map +1 -1
  119. package/dist/runtime/validate-entries.d.ts +77 -0
  120. package/dist/runtime/validate-entries.js +405 -0
  121. package/dist/runtime/validate-entries.js.map +1 -0
  122. package/dist/server/headers.d.ts +41 -15
  123. package/dist/server/headers.js +53 -24
  124. package/dist/server/headers.js.map +1 -1
  125. package/dist/server/load-config.d.ts +4 -5
  126. package/dist/server/load-config.js +22 -14
  127. package/dist/server/load-config.js.map +1 -1
  128. package/dist/server/load-env.d.ts +0 -1
  129. package/dist/server/prerendered.d.ts +88 -34
  130. package/dist/server/prerendered.js +184 -24
  131. package/dist/server/prerendered.js.map +1 -1
  132. package/dist/server/server-config.d.ts +35 -50
  133. package/dist/server/server-config.js +27 -74
  134. package/dist/server/server-config.js.map +1 -1
  135. package/dist/server/shutdown.d.ts +5 -4
  136. package/dist/server/shutdown.js +5 -3
  137. package/dist/server/shutdown.js.map +1 -1
  138. package/dist/server/ssg.d.ts +9 -7
  139. package/dist/server/ssg.js +209 -65
  140. package/dist/server/ssg.js.map +1 -1
  141. package/dist/server/static.d.ts +0 -1
  142. package/dist/server/static.js +7 -1
  143. package/dist/server/static.js.map +1 -1
  144. package/package.json +21 -23
  145. package/dist/builder/page-files.d.ts.map +0 -1
  146. package/dist/builder/public-env.d.ts.map +0 -1
  147. package/dist/builder/rspack-config.d.ts.map +0 -1
  148. package/dist/cli/build.d.ts.map +0 -1
  149. package/dist/cli/dev.d.ts.map +0 -1
  150. package/dist/cli/index.d.ts.map +0 -1
  151. package/dist/cli/start.d.ts.map +0 -1
  152. package/dist/config.d.ts.map +0 -1
  153. package/dist/deploy/aws-lambda/runtime.d.ts.map +0 -1
  154. package/dist/deploy/build-marker.d.ts.map +0 -1
  155. package/dist/deploy/bun/runtime.d.ts +0 -11
  156. package/dist/deploy/bun/runtime.d.ts.map +0 -1
  157. package/dist/deploy/bun/runtime.js +0 -22
  158. package/dist/deploy/bun/runtime.js.map +0 -1
  159. package/dist/deploy/cloudflare/build.d.ts.map +0 -1
  160. package/dist/deploy/cloudflare/runtime.d.ts.map +0 -1
  161. package/dist/deploy/contract.d.ts.map +0 -1
  162. package/dist/deploy/deno/runtime.d.ts +0 -11
  163. package/dist/deploy/deno/runtime.d.ts.map +0 -1
  164. package/dist/deploy/deno/runtime.js +0 -16
  165. package/dist/deploy/deno/runtime.js.map +0 -1
  166. package/dist/deploy/filesystem.d.ts.map +0 -1
  167. package/dist/deploy/listen.d.ts +0 -20
  168. package/dist/deploy/listen.d.ts.map +0 -1
  169. package/dist/deploy/listen.js +0 -24
  170. package/dist/deploy/listen.js.map +0 -1
  171. package/dist/deploy/netlify/build.d.ts +0 -8
  172. package/dist/deploy/netlify/build.d.ts.map +0 -1
  173. package/dist/deploy/netlify/build.js +0 -52
  174. package/dist/deploy/netlify/build.js.map +0 -1
  175. package/dist/deploy/netlify/runtime.d.ts +0 -13
  176. package/dist/deploy/netlify/runtime.d.ts.map +0 -1
  177. package/dist/deploy/netlify/runtime.js +0 -24
  178. package/dist/deploy/netlify/runtime.js.map +0 -1
  179. package/dist/deploy/node/runtime.d.ts.map +0 -1
  180. package/dist/deploy/presets.d.ts.map +0 -1
  181. package/dist/deploy/vercel/build.d.ts.map +0 -1
  182. package/dist/deploy/vercel/runtime.d.ts.map +0 -1
  183. package/dist/index.d.ts.map +0 -1
  184. package/dist/router.d.ts.map +0 -1
  185. package/dist/runtime/boundaries.d.ts.map +0 -1
  186. package/dist/runtime/client.d.ts.map +0 -1
  187. package/dist/runtime/context.d.ts.map +0 -1
  188. package/dist/runtime/control.d.ts.map +0 -1
  189. package/dist/runtime/dev-protocol.d.ts.map +0 -1
  190. package/dist/runtime/empty-server-app.d.ts.map +0 -1
  191. package/dist/runtime/entry.client.d.ts.map +0 -1
  192. package/dist/runtime/entry.rsc.d.ts.map +0 -1
  193. package/dist/runtime/entry.ssr.d.ts.map +0 -1
  194. package/dist/runtime/navigation.d.ts.map +0 -1
  195. package/dist/runtime/request.d.ts.map +0 -1
  196. package/dist/runtime/server.d.ts.map +0 -1
  197. package/dist/server/compress.d.ts +0 -15
  198. package/dist/server/compress.d.ts.map +0 -1
  199. package/dist/server/compress.js +0 -76
  200. package/dist/server/compress.js.map +0 -1
  201. package/dist/server/headers.d.ts.map +0 -1
  202. package/dist/server/load-config.d.ts.map +0 -1
  203. package/dist/server/load-env.d.ts.map +0 -1
  204. package/dist/server/prerendered.d.ts.map +0 -1
  205. package/dist/server/server-config.d.ts.map +0 -1
  206. package/dist/server/shutdown.d.ts.map +0 -1
  207. package/dist/server/ssg.d.ts.map +0 -1
  208. 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 { Ctx } from './runtime/context.js';
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
- * per `:param` segment, `Record<string, never>` for a path with no params.
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
- * Paths use Hono's syntax, so `:id`, `:id{[0-9]+}` and `*` all work. You rarely
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
- * argument to get `params` typed key-by-key; without it `params` falls back to
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
- * The location props (`url` and `params`) mirror what a `'use client'` component
33
- * gets from `useNavigation()` same names, same types so moving a read across
34
- * the server/client line is a copy-paste.
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 Ctx.var} and
38
- * {@link Ctx.env} on {@link PageProps.ctx}.
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
- * (`X-Forwarded-Host` / `-Proto`). Read `url.pathname`, `url.searchParams` and
55
- * the rest off it.
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 prerendered page it is the build-time URL: a `render: 'static'` route is
62
- * rendered once, against `siteUrl` and with no query string, and that one file
63
- * then answers every request whatever its own query. So `url.searchParams` is
64
- * always empty there read the query from `useNavigation().url` in a
65
- * `'use client'` component instead, or mark the route `render: 'dynamic'`.
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 very object `getContext()` returns, handed to the
72
- * page so cookies, headers, env and middleware variables are reachable without
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, and never serialized: React renders a server component and puts
76
- * its *output* on the wire, not its props. It is also deliberately a
77
- * non-enumerable property, which has three consequences worth knowing:
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
- * - It **cannot be handed to a `'use client'` component** it wraps the live
80
- * request and response, which do not exist in the browser. Passing it
81
- * explicitly (`<Counter ctx={ctx} />`) fails the render with React's *"Only
82
- * plain objects can be passed to Client Components"*. Read what you need on
83
- * the server and pass plain values down.
84
- * - Spreading the page's props instead (`<Counter {...props} />`) drops `ctx`
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
- * per-request context at build time. Mark the route `render: 'dynamic'` (or use
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: Ctx<E>;
94
+ ctx: RequestContext<E>;
105
95
  }
106
96
  /**
107
- * A page: a React **server component** that renders the entire document
108
- * (`<html>…</html>`), usually via a shared layout. It may be `async` and await
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 must default-export exactly one of these. Interactive parts
112
- * belong in `'use client'` components the page imports — only those ship JS.
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
- * named `handler` export. The module only ever loads on the server, so it is
120
- * safe to import a database client or read secrets from it.
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 '@rshono/core';
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
- /** A Hono {@link Handler} handling every request matched by the route. */
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
- /** Hono-style path pattern, e.g. `/`, `/profile/:id`, `/files/*`. */
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
- * {@link PageComponent}.
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
- * Write it inline as shown — the framework detects that exact
153
- * `() => import('…')` form and injects Rspack's `'use server-entry'`
154
- * directive into the module for you (that directive is what attaches the
155
- * page's client JS/CSS, giving per-page code splitting). If you wire the
156
- * component up any other way — a variable, a barrel re-export, a computed
157
- * specifieradd `'use server-entry'` as the first line of the page module
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
- * HTML file each. Runs at build time only, on the server, so it may hit a
168
- * database or read the filesystem.
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
- * A parameterised static route without `staticPaths` falls back to rendering
171
- * per request (with a build warning). Wildcard (`*`), optional and regex
172
- * params can't be prerendered.
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
- /** Hono-style path pattern, e.g. `/api/health`, `/api/users/:id`. */
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
- /** HTTP method to match. Defaults to `'all'` — every method. */
202
- method?: HTTPMethod;
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
- /** HTTP methods an {@link EndpointRoute} can match. `'all'` matches every method. */
207
- export type HTTPMethod = 'get' | 'post' | 'put' | 'patch' | 'delete' | 'head' | 'options' | 'all';
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
- * Type guard narrowing a {@link Route} to a {@link PageRoute}. Because `type` is
212
- * optional on page routes, anything not explicitly `'endpoint'` is a page.
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
- * @example
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
- * `error` in {@link RouteConfig}. Same contract as a {@link PageRoute}
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: the
235
- * message is a generic `'Internal Server Error'` and there is no `stack`. In dev
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 ErrorInfo {
269
+ export interface ErrorPageInfo {
239
270
  /** The thrown error's message in dev; `'Internal Server Error'` in production. */
240
271
  message: string;
241
- /** The stack trace. Present in dev only. */
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
- * {@link PageProps} plus the redaction-aware {@link ErrorInfo}.
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 <html><body><h1>Something went wrong</h1><p>{error.message}</p></body></html>;
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: ErrorInfo;
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
- /** Page rendered with a 500 status when a request throws. Receives {@link ErrorPageProps}. */
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
- } ? [PageProps<P, any>] extends [CP] ? R : R & {
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
- * Declares the app's route table. Default-export the result as `routes` from
290
- * `src/routes.ts` the one file rshono requires.
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
- * `routes.ts` only ever runs on the server, so importing server-only modules
293
- * from it (e.g. inside `staticPaths`) is safe.
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, this cross-checks every page against its own path:
296
- * if a component's props aren't satisfied by `PageProps<'<its path>'>`, the
297
- * `component` field errors with `component props are not satisfied by
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
- * @param config - A {@link RouteConfig}, or a bare {@link Route} array as
302
- * shorthand when there are no `notFound` / `error` pages.
303
- * @returns The config, unchanged and fully typed.
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 Array shorthand
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 TRoutes extends readonly Route[]>(config: RouteConfig<TRoutes> & {
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
- * Type guard narrowing a {@link Route} to a {@link PageRoute}. Because `type` is
3
- * optional on page routes, anything not explicitly `'endpoint'` is a page.
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
- * @example
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';
@@ -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"]}