@rshono/core 1.0.0-rc.0

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 (189) hide show
  1. package/README.md +273 -0
  2. package/bin/rshono.mjs +15 -0
  3. package/dist/builder/env-shadow-loader.cjs +26 -0
  4. package/dist/builder/page-entry-loader.cjs +7 -0
  5. package/dist/builder/page-files.d.ts +2 -0
  6. package/dist/builder/page-files.d.ts.map +1 -0
  7. package/dist/builder/page-files.js +45 -0
  8. package/dist/builder/page-files.js.map +1 -0
  9. package/dist/builder/public-env.d.ts +2 -0
  10. package/dist/builder/public-env.d.ts.map +1 -0
  11. package/dist/builder/public-env.js +5 -0
  12. package/dist/builder/public-env.js.map +1 -0
  13. package/dist/builder/rspack-config.d.ts +14 -0
  14. package/dist/builder/rspack-config.d.ts.map +1 -0
  15. package/dist/builder/rspack-config.js +235 -0
  16. package/dist/builder/rspack-config.js.map +1 -0
  17. package/dist/cli/build.d.ts +11 -0
  18. package/dist/cli/build.d.ts.map +1 -0
  19. package/dist/cli/build.js +64 -0
  20. package/dist/cli/build.js.map +1 -0
  21. package/dist/cli/dev.d.ts +9 -0
  22. package/dist/cli/dev.d.ts.map +1 -0
  23. package/dist/cli/dev.js +210 -0
  24. package/dist/cli/dev.js.map +1 -0
  25. package/dist/cli/index.d.ts +2 -0
  26. package/dist/cli/index.d.ts.map +1 -0
  27. package/dist/cli/index.js +77 -0
  28. package/dist/cli/index.js.map +1 -0
  29. package/dist/cli/start.d.ts +8 -0
  30. package/dist/cli/start.d.ts.map +1 -0
  31. package/dist/cli/start.js +39 -0
  32. package/dist/cli/start.js.map +1 -0
  33. package/dist/config.d.ts +121 -0
  34. package/dist/config.d.ts.map +1 -0
  35. package/dist/config.js +5 -0
  36. package/dist/config.js.map +1 -0
  37. package/dist/deploy/aws-lambda/runtime.d.ts +14 -0
  38. package/dist/deploy/aws-lambda/runtime.d.ts.map +1 -0
  39. package/dist/deploy/aws-lambda/runtime.js +24 -0
  40. package/dist/deploy/aws-lambda/runtime.js.map +1 -0
  41. package/dist/deploy/build-marker.d.ts +15 -0
  42. package/dist/deploy/build-marker.d.ts.map +1 -0
  43. package/dist/deploy/build-marker.js +26 -0
  44. package/dist/deploy/build-marker.js.map +1 -0
  45. package/dist/deploy/bun/runtime.d.ts +11 -0
  46. package/dist/deploy/bun/runtime.d.ts.map +1 -0
  47. package/dist/deploy/bun/runtime.js +22 -0
  48. package/dist/deploy/bun/runtime.js.map +1 -0
  49. package/dist/deploy/cloudflare/build.d.ts +7 -0
  50. package/dist/deploy/cloudflare/build.d.ts.map +1 -0
  51. package/dist/deploy/cloudflare/build.js +74 -0
  52. package/dist/deploy/cloudflare/build.js.map +1 -0
  53. package/dist/deploy/cloudflare/runtime.d.ts +10 -0
  54. package/dist/deploy/cloudflare/runtime.d.ts.map +1 -0
  55. package/dist/deploy/cloudflare/runtime.js +129 -0
  56. package/dist/deploy/cloudflare/runtime.js.map +1 -0
  57. package/dist/deploy/contract.d.ts +65 -0
  58. package/dist/deploy/contract.d.ts.map +1 -0
  59. package/dist/deploy/contract.js +2 -0
  60. package/dist/deploy/contract.js.map +1 -0
  61. package/dist/deploy/deno/runtime.d.ts +11 -0
  62. package/dist/deploy/deno/runtime.d.ts.map +1 -0
  63. package/dist/deploy/deno/runtime.js +16 -0
  64. package/dist/deploy/deno/runtime.js.map +1 -0
  65. package/dist/deploy/filesystem.d.ts +10 -0
  66. package/dist/deploy/filesystem.d.ts.map +1 -0
  67. package/dist/deploy/filesystem.js +47 -0
  68. package/dist/deploy/filesystem.js.map +1 -0
  69. package/dist/deploy/listen.d.ts +20 -0
  70. package/dist/deploy/listen.d.ts.map +1 -0
  71. package/dist/deploy/listen.js +24 -0
  72. package/dist/deploy/listen.js.map +1 -0
  73. package/dist/deploy/netlify/build.d.ts +8 -0
  74. package/dist/deploy/netlify/build.d.ts.map +1 -0
  75. package/dist/deploy/netlify/build.js +52 -0
  76. package/dist/deploy/netlify/build.js.map +1 -0
  77. package/dist/deploy/netlify/runtime.d.ts +13 -0
  78. package/dist/deploy/netlify/runtime.d.ts.map +1 -0
  79. package/dist/deploy/netlify/runtime.js +24 -0
  80. package/dist/deploy/netlify/runtime.js.map +1 -0
  81. package/dist/deploy/node/runtime.d.ts +7 -0
  82. package/dist/deploy/node/runtime.d.ts.map +1 -0
  83. package/dist/deploy/node/runtime.js +36 -0
  84. package/dist/deploy/node/runtime.js.map +1 -0
  85. package/dist/deploy/presets.d.ts +88 -0
  86. package/dist/deploy/presets.d.ts.map +1 -0
  87. package/dist/deploy/presets.js +110 -0
  88. package/dist/deploy/presets.js.map +1 -0
  89. package/dist/deploy/vercel/build.d.ts +3 -0
  90. package/dist/deploy/vercel/build.d.ts.map +1 -0
  91. package/dist/deploy/vercel/build.js +56 -0
  92. package/dist/deploy/vercel/build.js.map +1 -0
  93. package/dist/deploy/vercel/runtime.d.ts +14 -0
  94. package/dist/deploy/vercel/runtime.d.ts.map +1 -0
  95. package/dist/deploy/vercel/runtime.js +26 -0
  96. package/dist/deploy/vercel/runtime.js.map +1 -0
  97. package/dist/index.d.ts +23 -0
  98. package/dist/index.d.ts.map +1 -0
  99. package/dist/index.js +17 -0
  100. package/dist/index.js.map +1 -0
  101. package/dist/router.d.ts +337 -0
  102. package/dist/router.d.ts.map +1 -0
  103. package/dist/router.js +18 -0
  104. package/dist/router.js.map +1 -0
  105. package/dist/runtime/boundaries.d.ts +102 -0
  106. package/dist/runtime/boundaries.d.ts.map +1 -0
  107. package/dist/runtime/boundaries.js +101 -0
  108. package/dist/runtime/boundaries.js.map +1 -0
  109. package/dist/runtime/client.d.ts +16 -0
  110. package/dist/runtime/client.d.ts.map +1 -0
  111. package/dist/runtime/client.js +16 -0
  112. package/dist/runtime/client.js.map +1 -0
  113. package/dist/runtime/context.d.ts +271 -0
  114. package/dist/runtime/context.d.ts.map +1 -0
  115. package/dist/runtime/context.js +360 -0
  116. package/dist/runtime/context.js.map +1 -0
  117. package/dist/runtime/control.d.ts +18 -0
  118. package/dist/runtime/control.d.ts.map +1 -0
  119. package/dist/runtime/control.js +39 -0
  120. package/dist/runtime/control.js.map +1 -0
  121. package/dist/runtime/dev-protocol.d.ts +22 -0
  122. package/dist/runtime/dev-protocol.d.ts.map +1 -0
  123. package/dist/runtime/dev-protocol.js +2 -0
  124. package/dist/runtime/dev-protocol.js.map +1 -0
  125. package/dist/runtime/empty-server-app.d.ts +3 -0
  126. package/dist/runtime/empty-server-app.d.ts.map +1 -0
  127. package/dist/runtime/empty-server-app.js +2 -0
  128. package/dist/runtime/empty-server-app.js.map +1 -0
  129. package/dist/runtime/entry.client.d.ts +2 -0
  130. package/dist/runtime/entry.client.d.ts.map +1 -0
  131. package/dist/runtime/entry.client.js +453 -0
  132. package/dist/runtime/entry.client.js.map +1 -0
  133. package/dist/runtime/entry.rsc.d.ts +32 -0
  134. package/dist/runtime/entry.rsc.d.ts.map +1 -0
  135. package/dist/runtime/entry.rsc.js +466 -0
  136. package/dist/runtime/entry.rsc.js.map +1 -0
  137. package/dist/runtime/entry.ssr.d.ts +25 -0
  138. package/dist/runtime/entry.ssr.d.ts.map +1 -0
  139. package/dist/runtime/entry.ssr.js +86 -0
  140. package/dist/runtime/entry.ssr.js.map +1 -0
  141. package/dist/runtime/navigation.d.ts +114 -0
  142. package/dist/runtime/navigation.d.ts.map +1 -0
  143. package/dist/runtime/navigation.js +112 -0
  144. package/dist/runtime/navigation.js.map +1 -0
  145. package/dist/runtime/request.d.ts +35 -0
  146. package/dist/runtime/request.d.ts.map +1 -0
  147. package/dist/runtime/request.js +39 -0
  148. package/dist/runtime/request.js.map +1 -0
  149. package/dist/runtime/server.d.ts +24 -0
  150. package/dist/runtime/server.d.ts.map +1 -0
  151. package/dist/runtime/server.js +24 -0
  152. package/dist/runtime/server.js.map +1 -0
  153. package/dist/server/compress.d.ts +15 -0
  154. package/dist/server/compress.d.ts.map +1 -0
  155. package/dist/server/compress.js +76 -0
  156. package/dist/server/compress.js.map +1 -0
  157. package/dist/server/headers.d.ts +24 -0
  158. package/dist/server/headers.d.ts.map +1 -0
  159. package/dist/server/headers.js +44 -0
  160. package/dist/server/headers.js.map +1 -0
  161. package/dist/server/load-config.d.ts +7 -0
  162. package/dist/server/load-config.d.ts.map +1 -0
  163. package/dist/server/load-config.js +41 -0
  164. package/dist/server/load-config.js.map +1 -0
  165. package/dist/server/load-env.d.ts +2 -0
  166. package/dist/server/load-env.d.ts.map +1 -0
  167. package/dist/server/load-env.js +10 -0
  168. package/dist/server/load-env.js.map +1 -0
  169. package/dist/server/prerendered.d.ts +67 -0
  170. package/dist/server/prerendered.d.ts.map +1 -0
  171. package/dist/server/prerendered.js +39 -0
  172. package/dist/server/prerendered.js.map +1 -0
  173. package/dist/server/server-config.d.ts +72 -0
  174. package/dist/server/server-config.d.ts.map +1 -0
  175. package/dist/server/server-config.js +94 -0
  176. package/dist/server/server-config.js.map +1 -0
  177. package/dist/server/shutdown.d.ts +7 -0
  178. package/dist/server/shutdown.d.ts.map +1 -0
  179. package/dist/server/shutdown.js +11 -0
  180. package/dist/server/shutdown.js.map +1 -0
  181. package/dist/server/ssg.d.ts +25 -0
  182. package/dist/server/ssg.d.ts.map +1 -0
  183. package/dist/server/ssg.js +160 -0
  184. package/dist/server/ssg.js.map +1 -0
  185. package/dist/server/static.d.ts +14 -0
  186. package/dist/server/static.d.ts.map +1 -0
  187. package/dist/server/static.js +30 -0
  188. package/dist/server/static.js.map +1 -0
  189. package/package.json +86 -0
@@ -0,0 +1,337 @@
1
+ import type { Env, Handler } from 'hono';
2
+ import type { ParamKeys, ParamKeyToRecord } from 'hono/types';
3
+ import type { ReactNode } from 'react';
4
+ import type { Ctx } from './runtime/context.js';
5
+ type Simplify<T> = {
6
+ [K in keyof T]: T[K];
7
+ } & {};
8
+ type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
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.
12
+ *
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.
15
+ *
16
+ * @typeParam P - The literal route path, e.g. `'/users/:id/posts/:postId'`.
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * type P = PathParams<'/users/:id/posts/:postId'>; // { id: string; postId: string }
21
+ * ```
22
+ */
23
+ export type PathParams<P extends string> = ParamKeys<P> extends never ? Record<string, never> : Simplify<UnionToIntersection<ParamKeyToRecord<ParamKeys<P>>>>;
24
+ /**
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.
31
+ *
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.
35
+ *
36
+ * @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}.
39
+ *
40
+ * @example
41
+ * ```tsx
42
+ * import type { PageProps } from '@rshono/core';
43
+ *
44
+ * export default async function Profile({ params, url }: PageProps<'/profile/:id'>) {
45
+ * const user = await db.getUser(params.id); // params.id is string
46
+ * const tab = url.searchParams.get('tab') ?? 'overview';
47
+ * return <Layout>{user.name} — {tab}</Layout>;
48
+ * }
49
+ * ```
50
+ */
51
+ export interface PageProps<Path extends string = string, E extends Env = Env> {
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`.
60
+ *
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'`.
66
+ */
67
+ url: URL;
68
+ /** Matched route params for this request, e.g. `{ id: '42' }` for `/profile/:id`. */
69
+ params: string extends Path ? Record<string, string> : PathParams<Path>;
70
+ /**
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.
74
+ *
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:
78
+ *
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.
89
+ *
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`).
94
+ *
95
+ * @example
96
+ * ```tsx
97
+ * export default function Dashboard({ ctx }: PageProps) {
98
+ * const session = ctx.cookies.get('session');
99
+ * if (!session) redirect('/login');
100
+ * return <Layout>Signed in as {session}</Layout>;
101
+ * }
102
+ * ```
103
+ */
104
+ ctx: Ctx<E>;
105
+ }
106
+ /**
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.
110
+ *
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.
113
+ *
114
+ * @typeParam P - The component's props; for a page these are {@link PageProps}.
115
+ */
116
+ export type PageComponent<P = any> = (props: P) => ReactNode | Promise<ReactNode>;
117
+ /**
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.
121
+ *
122
+ * @example
123
+ * ```ts
124
+ * // src/health.ts
125
+ * import type { Handler } from '@rshono/core';
126
+ *
127
+ * export const handler: Handler = (c) => c.json({ ok: true });
128
+ * ```
129
+ */
130
+ export interface EndpointServerModule {
131
+ /** A Hono {@link Handler} handling every request matched by the route. */
132
+ handler: Handler;
133
+ }
134
+ /**
135
+ * A page route — a path rendered by a server component. This is the default
136
+ * route kind, so `type` can be omitted.
137
+ *
138
+ * @example
139
+ * ```ts
140
+ * { path: '/profile/:id', component: () => import('./components/profile') }
141
+ * ```
142
+ */
143
+ export interface PageRoute {
144
+ /** Discriminates a page from an endpoint; optional because `'page'` is the default. */
145
+ type?: 'page';
146
+ /** Hono-style path pattern, e.g. `/`, `/profile/:id`, `/files/*`. */
147
+ path: string;
148
+ /**
149
+ * Dynamic import of the page module, whose default export is the
150
+ * {@link PageComponent}.
151
+ *
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
+ * specifier — add `'use server-entry'` as the first line of the page module
158
+ * yourself; the framework throws a descriptive error when neither happened.
159
+ */
160
+ component: () => Promise<{
161
+ default: PageComponent;
162
+ }>;
163
+ /** `'static'` prerenders the route at build time; `'dynamic'` (the default) renders per request. */
164
+ render?: 'static' | 'dynamic';
165
+ /**
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.
169
+ *
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.
173
+ *
174
+ * @example
175
+ * ```ts
176
+ * {
177
+ * path: '/docs/:slug',
178
+ * render: 'static',
179
+ * component: () => import('./components/documentation'),
180
+ * staticPaths: async () => (await db.docs.all()).map((d) => ({ slug: d.slug })),
181
+ * }
182
+ * ```
183
+ */
184
+ staticPaths?: () => Array<Record<string, string>> | Promise<Array<Record<string, string>>>;
185
+ }
186
+ /**
187
+ * An endpoint route — a path served by a raw Hono handler instead of a React
188
+ * component. Use it for JSON APIs, webhooks, redirects, feeds, or anything that
189
+ * isn't an HTML page.
190
+ *
191
+ * @example
192
+ * ```ts
193
+ * { type: 'endpoint', path: '/api/health', server: () => import('./health') }
194
+ * ```
195
+ */
196
+ export interface EndpointRoute {
197
+ /** Marks this route as an endpoint rather than a page. Required. */
198
+ type: 'endpoint';
199
+ /** Hono-style path pattern, e.g. `/api/health`, `/api/users/:id`. */
200
+ path: string;
201
+ /** HTTP method to match. Defaults to `'all'` — every method. */
202
+ method?: HTTPMethod;
203
+ /** Dynamic import of the {@link EndpointServerModule} exporting `handler`. */
204
+ server: () => Promise<EndpointServerModule>;
205
+ }
206
+ /** HTTP methods an {@link EndpointRoute} can match. `'all'` matches every method. */
207
+ export type HTTPMethod = 'get' | 'post' | 'put' | 'patch' | 'delete' | 'head' | 'options' | 'all';
208
+ /** Any entry in the `routes` array: a {@link PageRoute} or an {@link EndpointRoute}. */
209
+ export type Route = PageRoute | EndpointRoute;
210
+ /**
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.
213
+ *
214
+ * @example
215
+ * ```ts
216
+ * for (const route of routes) {
217
+ * if (isPageRoute(route)) console.log(route.render ?? 'dynamic');
218
+ * }
219
+ * ```
220
+ */
221
+ export declare function isPageRoute(route: Route): route is PageRoute;
222
+ /**
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.
226
+ */
227
+ export interface FallbackPage {
228
+ /** Dynamic import of the page module; its default export is the {@link PageComponent}. */
229
+ component: () => Promise<{
230
+ default: PageComponent;
231
+ }>;
232
+ }
233
+ /**
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.
237
+ */
238
+ export interface ErrorInfo {
239
+ /** The thrown error's message in dev; `'Internal Server Error'` in production. */
240
+ message: string;
241
+ /** The stack trace. Present in dev only. */
242
+ stack?: string;
243
+ }
244
+ /**
245
+ * Props for the `error` page declared in {@link RouteConfig.error} — the usual
246
+ * {@link PageProps} plus the redaction-aware {@link ErrorInfo}.
247
+ *
248
+ * @typeParam E - The app's Hono {@link Env}, forwarded to {@link PageProps.ctx}.
249
+ *
250
+ * @example
251
+ * ```tsx
252
+ * import type { ErrorPageProps } from '@rshono/core';
253
+ *
254
+ * export default function ServerError({ error }: ErrorPageProps) {
255
+ * return <html><body><h1>Something went wrong</h1><p>{error.message}</p></body></html>;
256
+ * }
257
+ * ```
258
+ */
259
+ export type ErrorPageProps<E extends Env = Env> = PageProps<string, E> & {
260
+ error: ErrorInfo;
261
+ };
262
+ /**
263
+ * The object form accepted by {@link defineRoutes}: the route table plus the two
264
+ * optional framework-owned pages.
265
+ *
266
+ * @typeParam TRoutes - Inferred tuple of route literals, which is what makes the
267
+ * per-route `path` → props check possible.
268
+ */
269
+ export interface RouteConfig<TRoutes extends readonly Route[] = readonly Route[]> {
270
+ /** Every page and endpoint in the app, matched in order. */
271
+ routes: TRoutes;
272
+ /** Page rendered with a 404 status for unmatched paths and for `notFound()` calls. */
273
+ notFound?: FallbackPage;
274
+ /** Page rendered with a 500 status when a request throws. Receives {@link ErrorPageProps}. */
275
+ error?: FallbackPage;
276
+ }
277
+ type ValidateRoute<R> = R extends {
278
+ path: infer P extends string;
279
+ component: () => Promise<{
280
+ default: PageComponent<infer CP>;
281
+ }>;
282
+ } ? [PageProps<P, any>] extends [CP] ? R : R & {
283
+ component: `component props are not satisfied by PageProps<'${P}'>`;
284
+ } : R;
285
+ type ValidateRoutes<TRoutes extends readonly Route[]> = {
286
+ [K in keyof TRoutes]: ValidateRoute<TRoutes[K]>;
287
+ };
288
+ /**
289
+ * Declares the app's route table. Default-export the result as `routes` from
290
+ * `src/routes.ts` — the one file rshono requires.
291
+ *
292
+ * `routes.ts` only ever runs on the server, so importing server-only modules
293
+ * from it (e.g. inside `staticPaths`) is safe.
294
+ *
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.
300
+ *
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.
304
+ *
305
+ * @example
306
+ * ```ts
307
+ * // src/routes.ts
308
+ * import { defineRoutes } from '@rshono/core';
309
+ *
310
+ * export const routes = defineRoutes({
311
+ * routes: [
312
+ * { path: '/', component: () => import('./components/home') },
313
+ * { path: '/profile/:id', component: () => import('./components/profile') },
314
+ * {
315
+ * path: '/docs/:slug',
316
+ * render: 'static',
317
+ * component: () => import('./components/documentation'),
318
+ * staticPaths: async () => [{ slug: 'getting-started' }, { slug: 'deployment' }],
319
+ * },
320
+ * { type: 'endpoint', path: '/api/health', server: () => import('./health') },
321
+ * ],
322
+ * notFound: { component: () => import('./components/404') },
323
+ * error: { component: () => import('./components/500') },
324
+ * });
325
+ * ```
326
+ *
327
+ * @example Array shorthand
328
+ * ```ts
329
+ * export const routes = defineRoutes([{ path: '/', component: () => import('./components/home') }]);
330
+ * ```
331
+ */
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>;
336
+ export {};
337
+ //# sourceMappingURL=router.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"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"}
package/dist/router.js ADDED
@@ -0,0 +1,18 @@
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.
4
+ *
5
+ * @example
6
+ * ```ts
7
+ * for (const route of routes) {
8
+ * if (isPageRoute(route)) console.log(route.render ?? 'dynamic');
9
+ * }
10
+ * ```
11
+ */
12
+ export function isPageRoute(route) {
13
+ return route.type !== 'endpoint';
14
+ }
15
+ export function defineRoutes(input) {
16
+ return Array.isArray(input) ? { routes: input } : input;
17
+ }
18
+ //# sourceMappingURL=router.js.map
@@ -0,0 +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"]}
@@ -0,0 +1,102 @@
1
+ import { Component, type ReactNode } from 'react';
2
+ /**
3
+ * What an {@link ErrorBoundary} / {@link Boundary} renders once a child throws.
4
+ * Either a static node, or a render function that also gets a `reset` callback
5
+ * to clear the error and re-render the children (e.g. a "Try again" button).
6
+ *
7
+ * The render-function form only works when the boundary is used from a `'use
8
+ * client'` component — functions can't cross the server→client boundary. From a
9
+ * server component, pass a `ReactNode`.
10
+ */
11
+ export type ErrorFallback = ReactNode | ((error: Error, reset: () => void) => ReactNode);
12
+ export interface ErrorBoundaryProps {
13
+ /**
14
+ * Rendered in place of the children after one of them throws. Omit it to
15
+ * report the error via `onError` and re-throw to the next boundary out (or
16
+ * the global error page) instead of handling it here.
17
+ */
18
+ fallback?: ErrorFallback;
19
+ /** Called with the caught error (for logging / reporting). */
20
+ onError?: (error: Error) => void;
21
+ /**
22
+ * When any value in this array changes while the boundary is showing its
23
+ * fallback, the error is cleared automatically. Pass the current pathname to
24
+ * recover when the user navigates away: `resetKeys={[useNavigation().url.pathname]}`.
25
+ */
26
+ resetKeys?: readonly unknown[];
27
+ children: ReactNode;
28
+ }
29
+ interface ErrorBoundaryState {
30
+ error: Error | null;
31
+ }
32
+ /**
33
+ * A general-purpose error boundary. Catches errors thrown while rendering its
34
+ * children — a client island that blew up, or a server component that rejected
35
+ * on a soft navigation — and renders `fallback` in their place instead of
36
+ * tearing down the whole page.
37
+ *
38
+ * It's a `'use client'` component (React error boundaries must be), so drop it
39
+ * anywhere in the tree from a server or client component. Use {@link Boundary}
40
+ * when you also want a Suspense loading fallback in the same wrapper.
41
+ *
42
+ * @example
43
+ * ```tsx
44
+ * import { ErrorBoundary } from '@rshono/core/client';
45
+ *
46
+ * <ErrorBoundary fallback={(error, reset) => (
47
+ * <div role="alert">
48
+ * <p>{error.message}</p>
49
+ * <button onClick={reset}>Try again</button>
50
+ * </div>
51
+ * )}>
52
+ * <RiskyWidget />
53
+ * </ErrorBoundary>
54
+ * ```
55
+ */
56
+ export declare class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
57
+ state: ErrorBoundaryState;
58
+ static getDerivedStateFromError(error: Error): ErrorBoundaryState;
59
+ componentDidCatch(error: Error): void;
60
+ componentDidUpdate(prev: ErrorBoundaryProps): void;
61
+ reset: () => void;
62
+ render(): ReactNode;
63
+ }
64
+ export interface BoundaryProps {
65
+ /** Suspense fallback, shown while the children (or their data) are still loading. */
66
+ loading?: ReactNode;
67
+ /** Error fallback, shown if a child throws. See {@link ErrorFallback}. */
68
+ error?: ErrorFallback;
69
+ /** Called with the caught error. */
70
+ onError?: (error: Error) => void;
71
+ /** Clears the error fallback when any value changes — see {@link ErrorBoundaryProps.resetKeys}. */
72
+ resetKeys?: readonly unknown[];
73
+ children: ReactNode;
74
+ }
75
+ /**
76
+ * A loading + error boundary in one wrapper — the common case for an async
77
+ * section of a page. It always renders the same shape:
78
+ *
79
+ * ```tsx
80
+ * <ErrorBoundary fallback={error}>
81
+ * <Suspense fallback={loading}>{children}</Suspense>
82
+ * </ErrorBoundary>
83
+ * ```
84
+ *
85
+ * so `error` catches anything the children throw (including while suspended) and
86
+ * `loading` shows until they resolve. Both fallbacks are optional: omit
87
+ * `loading` and nothing shows while loading; omit `error` and thrown errors
88
+ * propagate to the next boundary out (or the global error page) rather than
89
+ * being caught here.
90
+ *
91
+ * @example
92
+ * ```tsx
93
+ * import { Boundary } from '@rshono/core/client';
94
+ *
95
+ * <Boundary loading={<Spinner />} error={(e, reset) => <Retry onClick={reset} />}>
96
+ * <SlowServerComponent />
97
+ * </Boundary>
98
+ * ```
99
+ */
100
+ export declare function Boundary({ loading, error, onError, resetKeys, children }: BoundaryProps): ReactNode;
101
+ export {};
102
+ //# sourceMappingURL=boundaries.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"boundaries.d.ts","sourceRoot":"","sources":["../../src/runtime/boundaries.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAY,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAa5D;;;;;;;;GAQG;AACH,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,IAAI,KAAK,SAAS,CAAC,CAAC;AAEzF,MAAM,WAAW,kBAAkB;IACjC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,8DAA8D;IAC9D,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACjC;;;;OAIG;IACH,SAAS,CAAC,EAAE,SAAS,OAAO,EAAE,CAAC;IAC/B,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED,UAAU,kBAAkB;IAC1B,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CACrB;AAMD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,qBAAa,aAAc,SAAQ,SAAS,CAAC,kBAAkB,EAAE,kBAAkB,CAAC;IAClF,KAAK,EAAE,kBAAkB,CAAmB;IAE5C,MAAM,CAAC,wBAAwB,CAAC,KAAK,EAAE,KAAK,GAAG,kBAAkB,CAEhE;IAED,iBAAiB,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAGpC;IAED,kBAAkB,CAAC,IAAI,EAAE,kBAAkB,GAAG,IAAI,CAKjD;IAED,KAAK,QAAO,IAAI,CAEd;IAEF,MAAM,IAAI,SAAS,CASlB;CACF;AAED,MAAM,WAAW,aAAa;IAC5B,qFAAqF;IACrF,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,0EAA0E;IAC1E,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,oCAAoC;IACpC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACjC,mGAAmG;IACnG,SAAS,CAAC,EAAE,SAAS,OAAO,EAAE,CAAC;IAC/B,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,QAAQ,CAAC,EAAE,OAAc,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE,aAAa,GAAG,SAAS,CAM1G"}
@@ -0,0 +1,101 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { Component, Suspense } from 'react';
4
+ import { isControlDigest } from './control.js';
5
+ /**
6
+ * `redirect()` and `notFound()` reach the browser as a thrown error carrying a control digest.
7
+ * They are navigation, not failure, so no boundary may absorb one — otherwise a `redirect()` from
8
+ * a component inside a `<Boundary>` would render "something went wrong" instead of navigating.
9
+ * They're re-thrown to the root, where the runtime turns the digest into a real navigation.
10
+ */
11
+ function isControlError(error) {
12
+ return isControlDigest(error?.digest);
13
+ }
14
+ function keysChanged(a, b) {
15
+ return a.length !== b.length || a.some((value, i) => !Object.is(value, b[i]));
16
+ }
17
+ /**
18
+ * A general-purpose error boundary. Catches errors thrown while rendering its
19
+ * children — a client island that blew up, or a server component that rejected
20
+ * on a soft navigation — and renders `fallback` in their place instead of
21
+ * tearing down the whole page.
22
+ *
23
+ * It's a `'use client'` component (React error boundaries must be), so drop it
24
+ * anywhere in the tree from a server or client component. Use {@link Boundary}
25
+ * when you also want a Suspense loading fallback in the same wrapper.
26
+ *
27
+ * @example
28
+ * ```tsx
29
+ * import { ErrorBoundary } from '@rshono/core/client';
30
+ *
31
+ * <ErrorBoundary fallback={(error, reset) => (
32
+ * <div role="alert">
33
+ * <p>{error.message}</p>
34
+ * <button onClick={reset}>Try again</button>
35
+ * </div>
36
+ * )}>
37
+ * <RiskyWidget />
38
+ * </ErrorBoundary>
39
+ * ```
40
+ */
41
+ export class ErrorBoundary extends Component {
42
+ state = { error: null };
43
+ static getDerivedStateFromError(error) {
44
+ return { error };
45
+ }
46
+ componentDidCatch(error) {
47
+ if (isControlError(error))
48
+ return; // a redirect isn't an error to report
49
+ this.props.onError?.(error);
50
+ }
51
+ componentDidUpdate(prev) {
52
+ const { resetKeys } = this.props;
53
+ if (this.state.error && prev.resetKeys && resetKeys && keysChanged(prev.resetKeys, resetKeys)) {
54
+ this.reset();
55
+ }
56
+ }
57
+ reset = () => {
58
+ this.setState({ error: null });
59
+ };
60
+ render() {
61
+ const { error } = this.state;
62
+ if (error !== null) {
63
+ if (isControlError(error))
64
+ throw error; // navigation in flight — never show a fallback for it
65
+ const { fallback } = this.props;
66
+ if (fallback === undefined)
67
+ throw error; // no local fallback → propagate to an outer boundary
68
+ return typeof fallback === 'function' ? fallback(error, this.reset) : fallback;
69
+ }
70
+ return this.props.children;
71
+ }
72
+ }
73
+ /**
74
+ * A loading + error boundary in one wrapper — the common case for an async
75
+ * section of a page. It always renders the same shape:
76
+ *
77
+ * ```tsx
78
+ * <ErrorBoundary fallback={error}>
79
+ * <Suspense fallback={loading}>{children}</Suspense>
80
+ * </ErrorBoundary>
81
+ * ```
82
+ *
83
+ * so `error` catches anything the children throw (including while suspended) and
84
+ * `loading` shows until they resolve. Both fallbacks are optional: omit
85
+ * `loading` and nothing shows while loading; omit `error` and thrown errors
86
+ * propagate to the next boundary out (or the global error page) rather than
87
+ * being caught here.
88
+ *
89
+ * @example
90
+ * ```tsx
91
+ * import { Boundary } from '@rshono/core/client';
92
+ *
93
+ * <Boundary loading={<Spinner />} error={(e, reset) => <Retry onClick={reset} />}>
94
+ * <SlowServerComponent />
95
+ * </Boundary>
96
+ * ```
97
+ */
98
+ export function Boundary({ loading = null, error, onError, resetKeys, children }) {
99
+ return (_jsx(ErrorBoundary, { fallback: error, onError: onError, resetKeys: resetKeys, children: _jsx(Suspense, { fallback: loading, children: children }) }));
100
+ }
101
+ //# sourceMappingURL=boundaries.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"boundaries.js","sourceRoot":"","sources":["../../src/runtime/boundaries.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAkB,MAAM,OAAO,CAAC;AAC5D,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAE/C;;;;;GAKG;AACH,SAAS,cAAc,CAAC,KAAc;IACpC,OAAO,eAAe,CAAE,KAAqC,EAAE,MAAM,CAAC,CAAC;AACzE,CAAC;AAmCD,SAAS,WAAW,CAAC,CAAqB,EAAE,CAAqB;IAC/D,OAAO,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAChF,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,OAAO,aAAc,SAAQ,SAAiD;IAClF,KAAK,GAAuB,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IAE5C,MAAM,CAAC,wBAAwB,CAAC,KAAY;QAC1C,OAAO,EAAE,KAAK,EAAE,CAAC;IACnB,CAAC;IAED,iBAAiB,CAAC,KAAY;QAC5B,IAAI,cAAc,CAAC,KAAK,CAAC;YAAE,OAAO,CAAC,sCAAsC;QACzE,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,CAAC;IAC9B,CAAC;IAED,kBAAkB,CAAC,IAAwB;QACzC,MAAM,EAAE,SAAS,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC;QACjC,IAAI,IAAI,CAAC,KAAK,CAAC,KAAK,IAAI,IAAI,CAAC,SAAS,IAAI,SAAS,IAAI,WAAW,CAAC,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,EAAE,CAAC;YAC9F,IAAI,CAAC,KAAK,EAAE,CAAC;QACf,CAAC;IACH,CAAC;IAED,KAAK,GAAG,GAAS,EAAE;QACjB,IAAI,CAAC,QAAQ,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACjC,CAAC,CAAC;IAEF,MAAM;QACJ,MAAM,EAAE,KAAK,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC;QAC7B,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;YACnB,IAAI,cAAc,CAAC,KAAK,CAAC;gBAAE,MAAM,KAAK,CAAC,CAAC,sDAAsD;YAC9F,MAAM,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC;YAChC,IAAI,QAAQ,KAAK,SAAS;gBAAE,MAAM,KAAK,CAAC,CAAC,qDAAqD;YAC9F,OAAO,OAAO,QAAQ,KAAK,UAAU,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;QACjF,CAAC;QACD,OAAO,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC;IAC7B,CAAC;CACF;AAcD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,UAAU,QAAQ,CAAC,EAAE,OAAO,GAAG,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAiB;IAC7F,OAAO,CACL,KAAC,aAAa,IAAC,QAAQ,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,YACpE,KAAC,QAAQ,IAAC,QAAQ,EAAE,OAAO,YAAG,QAAQ,GAAY,GACpC,CACjB,CAAC;AACJ,CAAC","sourcesContent":["'use client';\n\nimport { Component, Suspense, type ReactNode } from 'react';\nimport { isControlDigest } from './control.js';\n\n/**\n * `redirect()` and `notFound()` reach the browser as a thrown error carrying a control digest.\n * They are navigation, not failure, so no boundary may absorb one — otherwise a `redirect()` from\n * a component inside a `<Boundary>` would render \"something went wrong\" instead of navigating.\n * They're re-thrown to the root, where the runtime turns the digest into a real navigation.\n */\nfunction isControlError(error: unknown): boolean {\n return isControlDigest((error as { digest?: unknown } | null)?.digest);\n}\n\n/**\n * What an {@link ErrorBoundary} / {@link Boundary} renders once a child throws.\n * Either a static node, or a render function that also gets a `reset` callback\n * to clear the error and re-render the children (e.g. a \"Try again\" button).\n *\n * The render-function form only works when the boundary is used from a `'use\n * client'` component — functions can't cross the server→client boundary. From a\n * server component, pass a `ReactNode`.\n */\nexport type ErrorFallback = ReactNode | ((error: Error, reset: () => void) => ReactNode);\n\nexport interface ErrorBoundaryProps {\n /**\n * Rendered in place of the children after one of them throws. Omit it to\n * report the error via `onError` and re-throw to the next boundary out (or\n * the global error page) instead of handling it here.\n */\n fallback?: ErrorFallback;\n /** Called with the caught error (for logging / reporting). */\n onError?: (error: Error) => void;\n /**\n * When any value in this array changes while the boundary is showing its\n * fallback, the error is cleared automatically. Pass the current pathname to\n * recover when the user navigates away: `resetKeys={[useNavigation().url.pathname]}`.\n */\n resetKeys?: readonly unknown[];\n children: ReactNode;\n}\n\ninterface ErrorBoundaryState {\n error: Error | null;\n}\n\nfunction keysChanged(a: readonly unknown[], b: readonly unknown[]): boolean {\n return a.length !== b.length || a.some((value, i) => !Object.is(value, b[i]));\n}\n\n/**\n * A general-purpose error boundary. Catches errors thrown while rendering its\n * children — a client island that blew up, or a server component that rejected\n * on a soft navigation — and renders `fallback` in their place instead of\n * tearing down the whole page.\n *\n * It's a `'use client'` component (React error boundaries must be), so drop it\n * anywhere in the tree from a server or client component. Use {@link Boundary}\n * when you also want a Suspense loading fallback in the same wrapper.\n *\n * @example\n * ```tsx\n * import { ErrorBoundary } from '@rshono/core/client';\n *\n * <ErrorBoundary fallback={(error, reset) => (\n * <div role=\"alert\">\n * <p>{error.message}</p>\n * <button onClick={reset}>Try again</button>\n * </div>\n * )}>\n * <RiskyWidget />\n * </ErrorBoundary>\n * ```\n */\nexport class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {\n state: ErrorBoundaryState = { error: null };\n\n static getDerivedStateFromError(error: Error): ErrorBoundaryState {\n return { error };\n }\n\n componentDidCatch(error: Error): void {\n if (isControlError(error)) return; // a redirect isn't an error to report\n this.props.onError?.(error);\n }\n\n componentDidUpdate(prev: ErrorBoundaryProps): void {\n const { resetKeys } = this.props;\n if (this.state.error && prev.resetKeys && resetKeys && keysChanged(prev.resetKeys, resetKeys)) {\n this.reset();\n }\n }\n\n reset = (): void => {\n this.setState({ error: null });\n };\n\n render(): ReactNode {\n const { error } = this.state;\n if (error !== null) {\n if (isControlError(error)) throw error; // navigation in flight — never show a fallback for it\n const { fallback } = this.props;\n if (fallback === undefined) throw error; // no local fallback → propagate to an outer boundary\n return typeof fallback === 'function' ? fallback(error, this.reset) : fallback;\n }\n return this.props.children;\n }\n}\n\nexport interface BoundaryProps {\n /** Suspense fallback, shown while the children (or their data) are still loading. */\n loading?: ReactNode;\n /** Error fallback, shown if a child throws. See {@link ErrorFallback}. */\n error?: ErrorFallback;\n /** Called with the caught error. */\n onError?: (error: Error) => void;\n /** Clears the error fallback when any value changes — see {@link ErrorBoundaryProps.resetKeys}. */\n resetKeys?: readonly unknown[];\n children: ReactNode;\n}\n\n/**\n * A loading + error boundary in one wrapper — the common case for an async\n * section of a page. It always renders the same shape:\n *\n * ```tsx\n * <ErrorBoundary fallback={error}>\n * <Suspense fallback={loading}>{children}</Suspense>\n * </ErrorBoundary>\n * ```\n *\n * so `error` catches anything the children throw (including while suspended) and\n * `loading` shows until they resolve. Both fallbacks are optional: omit\n * `loading` and nothing shows while loading; omit `error` and thrown errors\n * propagate to the next boundary out (or the global error page) rather than\n * being caught here.\n *\n * @example\n * ```tsx\n * import { Boundary } from '@rshono/core/client';\n *\n * <Boundary loading={<Spinner />} error={(e, reset) => <Retry onClick={reset} />}>\n * <SlowServerComponent />\n * </Boundary>\n * ```\n */\nexport function Boundary({ loading = null, error, onError, resetKeys, children }: BoundaryProps): ReactNode {\n return (\n <ErrorBoundary fallback={error} onError={onError} resetKeys={resetKeys}>\n <Suspense fallback={loading}>{children}</Suspense>\n </ErrorBoundary>\n );\n}\n"]}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * `@rshono/core/client` — the browser-side surface, for use from `'use client'`
3
+ * modules: {@link useNavigation} for the current URL and soft navigation, and
4
+ * {@link Boundary} / {@link ErrorBoundary} / {@link NavigationProgress} as
5
+ * components.
6
+ *
7
+ * Every export is itself a `'use client'` module, so a server component can
8
+ * render {@link Boundary} or {@link NavigationProgress} directly — but the hook
9
+ * needs a client component. In a server component, read the same request data
10
+ * from `getContext()` in `@rshono/core/server`.
11
+ *
12
+ * @packageDocumentation
13
+ */
14
+ export { NavigationProgress, useNavigation, type Navigation, type NavigationProgressProps, type Router } from './navigation.js';
15
+ export { Boundary, ErrorBoundary, type BoundaryProps, type ErrorBoundaryProps, type ErrorFallback } from './boundaries.js';
16
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/runtime/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,KAAK,UAAU,EAAE,KAAK,uBAAuB,EAAE,KAAK,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAChI,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,KAAK,aAAa,EAAE,KAAK,kBAAkB,EAAE,KAAK,aAAa,EAAE,MAAM,iBAAiB,CAAC"}