@rangojs/router 0.0.0-experimental.2

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 (155) hide show
  1. package/CLAUDE.md +7 -0
  2. package/README.md +19 -0
  3. package/dist/vite/index.js +1298 -0
  4. package/package.json +140 -0
  5. package/skills/caching/SKILL.md +319 -0
  6. package/skills/document-cache/SKILL.md +152 -0
  7. package/skills/hooks/SKILL.md +359 -0
  8. package/skills/intercept/SKILL.md +292 -0
  9. package/skills/layout/SKILL.md +216 -0
  10. package/skills/loader/SKILL.md +365 -0
  11. package/skills/middleware/SKILL.md +442 -0
  12. package/skills/parallel/SKILL.md +255 -0
  13. package/skills/route/SKILL.md +141 -0
  14. package/skills/router-setup/SKILL.md +403 -0
  15. package/skills/theme/SKILL.md +54 -0
  16. package/skills/typesafety/SKILL.md +352 -0
  17. package/src/__mocks__/version.ts +6 -0
  18. package/src/__tests__/component-utils.test.ts +76 -0
  19. package/src/__tests__/route-definition.test.ts +63 -0
  20. package/src/__tests__/urls.test.tsx +436 -0
  21. package/src/browser/event-controller.ts +876 -0
  22. package/src/browser/index.ts +18 -0
  23. package/src/browser/link-interceptor.ts +121 -0
  24. package/src/browser/lru-cache.ts +69 -0
  25. package/src/browser/merge-segment-loaders.ts +126 -0
  26. package/src/browser/navigation-bridge.ts +893 -0
  27. package/src/browser/navigation-client.ts +162 -0
  28. package/src/browser/navigation-store.ts +823 -0
  29. package/src/browser/partial-update.ts +559 -0
  30. package/src/browser/react/Link.tsx +248 -0
  31. package/src/browser/react/NavigationProvider.tsx +275 -0
  32. package/src/browser/react/ScrollRestoration.tsx +94 -0
  33. package/src/browser/react/context.ts +53 -0
  34. package/src/browser/react/index.ts +52 -0
  35. package/src/browser/react/location-state-shared.ts +120 -0
  36. package/src/browser/react/location-state.ts +62 -0
  37. package/src/browser/react/use-action.ts +240 -0
  38. package/src/browser/react/use-client-cache.ts +56 -0
  39. package/src/browser/react/use-handle.ts +178 -0
  40. package/src/browser/react/use-href.tsx +208 -0
  41. package/src/browser/react/use-link-status.ts +134 -0
  42. package/src/browser/react/use-navigation.ts +150 -0
  43. package/src/browser/react/use-segments.ts +188 -0
  44. package/src/browser/request-controller.ts +164 -0
  45. package/src/browser/rsc-router.tsx +353 -0
  46. package/src/browser/scroll-restoration.ts +324 -0
  47. package/src/browser/server-action-bridge.ts +747 -0
  48. package/src/browser/shallow.ts +35 -0
  49. package/src/browser/types.ts +464 -0
  50. package/src/cache/__tests__/document-cache.test.ts +522 -0
  51. package/src/cache/__tests__/memory-segment-store.test.ts +487 -0
  52. package/src/cache/__tests__/memory-store.test.ts +484 -0
  53. package/src/cache/cache-scope.ts +565 -0
  54. package/src/cache/cf/__tests__/cf-cache-store.test.ts +428 -0
  55. package/src/cache/cf/cf-cache-store.ts +428 -0
  56. package/src/cache/cf/index.ts +19 -0
  57. package/src/cache/document-cache.ts +340 -0
  58. package/src/cache/index.ts +58 -0
  59. package/src/cache/memory-segment-store.ts +150 -0
  60. package/src/cache/memory-store.ts +253 -0
  61. package/src/cache/types.ts +387 -0
  62. package/src/client.rsc.tsx +88 -0
  63. package/src/client.tsx +621 -0
  64. package/src/component-utils.ts +76 -0
  65. package/src/components/DefaultDocument.tsx +23 -0
  66. package/src/default-error-boundary.tsx +88 -0
  67. package/src/deps/browser.ts +8 -0
  68. package/src/deps/html-stream-client.ts +2 -0
  69. package/src/deps/html-stream-server.ts +2 -0
  70. package/src/deps/rsc.ts +10 -0
  71. package/src/deps/ssr.ts +2 -0
  72. package/src/errors.ts +259 -0
  73. package/src/handle.ts +120 -0
  74. package/src/handles/MetaTags.tsx +193 -0
  75. package/src/handles/index.ts +6 -0
  76. package/src/handles/meta.ts +247 -0
  77. package/src/href-client.ts +128 -0
  78. package/src/href-context.ts +33 -0
  79. package/src/href.ts +177 -0
  80. package/src/index.rsc.ts +79 -0
  81. package/src/index.ts +87 -0
  82. package/src/loader.rsc.ts +204 -0
  83. package/src/loader.ts +47 -0
  84. package/src/network-error-thrower.tsx +21 -0
  85. package/src/outlet-context.ts +15 -0
  86. package/src/root-error-boundary.tsx +277 -0
  87. package/src/route-content-wrapper.tsx +198 -0
  88. package/src/route-definition.ts +1371 -0
  89. package/src/route-map-builder.ts +146 -0
  90. package/src/route-types.ts +198 -0
  91. package/src/route-utils.ts +89 -0
  92. package/src/router/__tests__/match-context.test.ts +104 -0
  93. package/src/router/__tests__/match-pipelines.test.ts +537 -0
  94. package/src/router/__tests__/match-result.test.ts +566 -0
  95. package/src/router/__tests__/on-error.test.ts +935 -0
  96. package/src/router/__tests__/pattern-matching.test.ts +577 -0
  97. package/src/router/error-handling.ts +287 -0
  98. package/src/router/handler-context.ts +158 -0
  99. package/src/router/loader-resolution.ts +326 -0
  100. package/src/router/manifest.ts +138 -0
  101. package/src/router/match-context.ts +264 -0
  102. package/src/router/match-middleware/background-revalidation.ts +236 -0
  103. package/src/router/match-middleware/cache-lookup.ts +261 -0
  104. package/src/router/match-middleware/cache-store.ts +266 -0
  105. package/src/router/match-middleware/index.ts +81 -0
  106. package/src/router/match-middleware/intercept-resolution.ts +268 -0
  107. package/src/router/match-middleware/segment-resolution.ts +174 -0
  108. package/src/router/match-pipelines.ts +214 -0
  109. package/src/router/match-result.ts +214 -0
  110. package/src/router/metrics.ts +62 -0
  111. package/src/router/middleware.test.ts +1355 -0
  112. package/src/router/middleware.ts +748 -0
  113. package/src/router/pattern-matching.ts +272 -0
  114. package/src/router/revalidation.ts +190 -0
  115. package/src/router/router-context.ts +299 -0
  116. package/src/router/types.ts +96 -0
  117. package/src/router.ts +3876 -0
  118. package/src/rsc/__tests__/helpers.test.ts +175 -0
  119. package/src/rsc/handler.ts +1060 -0
  120. package/src/rsc/helpers.ts +64 -0
  121. package/src/rsc/index.ts +56 -0
  122. package/src/rsc/nonce.ts +18 -0
  123. package/src/rsc/types.ts +237 -0
  124. package/src/segment-system.tsx +456 -0
  125. package/src/server/__tests__/request-context.test.ts +171 -0
  126. package/src/server/context.ts +417 -0
  127. package/src/server/handle-store.ts +230 -0
  128. package/src/server/loader-registry.ts +174 -0
  129. package/src/server/request-context.ts +554 -0
  130. package/src/server/root-layout.tsx +10 -0
  131. package/src/server/tsconfig.json +14 -0
  132. package/src/server.ts +146 -0
  133. package/src/ssr/__tests__/ssr-handler.test.tsx +188 -0
  134. package/src/ssr/index.tsx +234 -0
  135. package/src/theme/ThemeProvider.tsx +291 -0
  136. package/src/theme/ThemeScript.tsx +61 -0
  137. package/src/theme/__tests__/theme.test.ts +120 -0
  138. package/src/theme/constants.ts +55 -0
  139. package/src/theme/index.ts +58 -0
  140. package/src/theme/theme-context.ts +70 -0
  141. package/src/theme/theme-script.ts +152 -0
  142. package/src/theme/types.ts +182 -0
  143. package/src/theme/use-theme.ts +44 -0
  144. package/src/types.ts +1561 -0
  145. package/src/urls.ts +726 -0
  146. package/src/use-loader.tsx +346 -0
  147. package/src/vite/__tests__/expose-loader-id.test.ts +117 -0
  148. package/src/vite/expose-action-id.ts +344 -0
  149. package/src/vite/expose-handle-id.ts +209 -0
  150. package/src/vite/expose-loader-id.ts +357 -0
  151. package/src/vite/expose-location-state-id.ts +177 -0
  152. package/src/vite/index.ts +787 -0
  153. package/src/vite/package-resolution.ts +125 -0
  154. package/src/vite/version.d.ts +12 -0
  155. package/src/vite/virtual-entries.ts +109 -0
package/src/types.ts ADDED
@@ -0,0 +1,1561 @@
1
+ import type { ReactNode } from "react";
2
+ import type { AllUseItems } from "./route-types.js";
3
+ import type { Handle } from "./handle.js";
4
+ import type { MiddlewareFn } from "./router/middleware.js";
5
+ import type { Theme } from "./theme/types.js";
6
+ export type { MiddlewareFn } from "./router/middleware.js";
7
+
8
+ /**
9
+ * Props for the Document component that wraps the entire application.
10
+ */
11
+ export type DocumentProps = {
12
+ children: ReactNode;
13
+ };
14
+
15
+ /**
16
+ * Global namespace for module augmentation
17
+ *
18
+ * Users can augment this to provide type-safe context globally:
19
+ *
20
+ * @example
21
+ * ```typescript
22
+ * // In router.tsx or env.d.ts
23
+ * declare global {
24
+ * namespace RSCRouter {
25
+ * interface Env extends RouterEnv<AppBindings, AppVariables> {}
26
+ * }
27
+ * }
28
+ *
29
+ * // Now all handlers have type-safe context without imports!
30
+ * export default map<typeof shopRoutes>({
31
+ * [middleware('*', 'auth')]: [
32
+ * (ctx, next) => {
33
+ * ctx.set('user', ...) // Type-safe!
34
+ * }
35
+ * ]
36
+ * })
37
+ * ```
38
+ */
39
+ declare global {
40
+ namespace RSCRouter {
41
+ // eslint-disable-next-line @typescript-eslint/no-empty-interface
42
+ interface Env {
43
+ // Empty by default - users augment with their RouterEnv
44
+ }
45
+
46
+ // eslint-disable-next-line @typescript-eslint/no-empty-interface
47
+ interface RegisteredRoutes {
48
+ // Empty by default - users augment with their merged route maps for type-safe href()
49
+ }
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Get registered routes or fallback to generic Record<string, string>
55
+ * When RSCRouter.RegisteredRoutes is augmented, provides autocomplete for route names
56
+ * When not augmented, allows any string (no autocomplete)
57
+ */
58
+ export type GetRegisteredRoutes = keyof RSCRouter.RegisteredRoutes extends never
59
+ ? Record<string, string>
60
+ : RSCRouter.RegisteredRoutes;
61
+
62
+ /**
63
+ * Default environment type - uses global augmentation if available, any otherwise
64
+ */
65
+ export type DefaultEnv = keyof RSCRouter.Env extends never ? any : RSCRouter.Env;
66
+
67
+ /**
68
+ * Router environment (Hono-inspired type-safe context)
69
+ *
70
+ * @template TBindings - Platform bindings (DB, KV, secrets, etc.)
71
+ * @template TVariables - Middleware-injected variables (user, permissions, etc.)
72
+ *
73
+ * @example
74
+ * ```typescript
75
+ * interface AppBindings {
76
+ * DB: D1Database;
77
+ * KV: KVNamespace;
78
+ * STRIPE_KEY: string;
79
+ * }
80
+ *
81
+ * interface AppVariables {
82
+ * user?: { id: string; name: string };
83
+ * permissions?: string[];
84
+ * }
85
+ *
86
+ * type AppEnv = RouterEnv<AppBindings, AppVariables>;
87
+ * const router = createRSCRouter<AppEnv>();
88
+ * ```
89
+ */
90
+ export interface RouterEnv<TBindings = {}, TVariables = {}> {
91
+ Bindings: TBindings;
92
+ Variables: TVariables;
93
+ }
94
+
95
+ /**
96
+ * Parse constraint values into a union type
97
+ * "a|b|c" → "a" | "b" | "c"
98
+ */
99
+ type ParseConstraint<T extends string> =
100
+ T extends `${infer First}|${infer Rest}`
101
+ ? First | ParseConstraint<Rest>
102
+ : T;
103
+
104
+ /**
105
+ * Extract param info from a param segment
106
+ *
107
+ * Handles:
108
+ * - :param → { name: "param", optional: false, type: string }
109
+ * - :param? → { name: "param", optional: true, type: string }
110
+ * - :param(a|b) → { name: "param", optional: false, type: "a" | "b" }
111
+ * - :param(a|b)? → { name: "param", optional: true, type: "a" | "b" }
112
+ */
113
+ type ExtractParamInfo<T extends string> =
114
+ // Optional + constrained: :param(a|b)?
115
+ T extends `${infer Name}(${infer Constraint})?`
116
+ ? { name: Name; optional: true; type: ParseConstraint<Constraint> }
117
+ // Constrained only: :param(a|b)
118
+ : T extends `${infer Name}(${infer Constraint})`
119
+ ? { name: Name; optional: false; type: ParseConstraint<Constraint> }
120
+ // Optional only: :param?
121
+ : T extends `${infer Name}?`
122
+ ? { name: Name; optional: true; type: string }
123
+ // Required: :param
124
+ : { name: T; optional: false; type: string };
125
+
126
+ /**
127
+ * Build param object from info
128
+ */
129
+ type ParamFromInfo<Info> =
130
+ Info extends { name: infer N extends string; optional: true; type: infer V }
131
+ ? { [K in N]?: V }
132
+ : Info extends { name: infer N extends string; optional: false; type: infer V }
133
+ ? { [K in N]: V }
134
+ : never;
135
+
136
+ /**
137
+ * Merge two param objects
138
+ */
139
+ type MergeParams<A, B> = {
140
+ [K in keyof A | keyof B]: K extends keyof A
141
+ ? K extends keyof B
142
+ ? A[K] | B[K]
143
+ : A[K]
144
+ : K extends keyof B
145
+ ? B[K]
146
+ : never;
147
+ };
148
+
149
+ /**
150
+ * Extract route params from a pattern with depth limit to prevent infinite recursion
151
+ *
152
+ * Supports:
153
+ * - Required params: /:slug → { slug: string }
154
+ * - Optional params: /:locale? → { locale?: string }
155
+ * - Constrained params: /:locale(en|gb) → { locale: "en" | "gb" }
156
+ * - Optional + constrained: /:locale(en|gb)? → { locale?: "en" | "gb" }
157
+ *
158
+ * @example
159
+ * ExtractParams<"/products/:id"> // { id: string }
160
+ * ExtractParams<"/:locale?/blog/:slug"> // { locale?: string; slug: string }
161
+ * ExtractParams<"/:locale(en|gb)/blog"> // { locale: "en" | "gb" }
162
+ * ExtractParams<"/:locale(en|gb)?/blog/:slug"> // { locale?: "en" | "gb"; slug: string }
163
+ */
164
+ export type ExtractParams<
165
+ T extends string,
166
+ Depth extends readonly unknown[] = []
167
+ > = Depth['length'] extends 10
168
+ ? { [key: string]: string | undefined } // Fallback to generic params if too deep
169
+ // Match param with remaining path: :param.../rest
170
+ : T extends `${infer _Start}:${infer Param}/${infer Rest}`
171
+ ? MergeParams<
172
+ ParamFromInfo<ExtractParamInfo<Param>>,
173
+ ExtractParams<`/${Rest}`, readonly [...Depth, unknown]>
174
+ >
175
+ // Match param at end: :param...
176
+ : T extends `${infer _Start}:${infer Param}`
177
+ ? ParamFromInfo<ExtractParamInfo<Param>>
178
+ : {};
179
+
180
+ /**
181
+ * Route definition - maps route names to patterns
182
+ */
183
+ /**
184
+ * Trailing slash handling mode
185
+ * - "never": Redirect URLs with trailing slash to without
186
+ * - "always": Redirect URLs without trailing slash to with
187
+ * - "ignore": Match both with and without trailing slash
188
+ */
189
+ export type TrailingSlashMode = "never" | "always" | "ignore";
190
+
191
+ /**
192
+ * Route configuration object (alternative to string path)
193
+ */
194
+ export type RouteConfig = {
195
+ path: string;
196
+ trailingSlash?: TrailingSlashMode;
197
+ };
198
+
199
+ /**
200
+ * Route definition options (global defaults)
201
+ */
202
+ export type RouteDefinitionOptions = {
203
+ trailingSlash?: TrailingSlashMode;
204
+ };
205
+
206
+ export type RouteDefinition = {
207
+ [key: string]: string | RouteConfig | RouteDefinition;
208
+ };
209
+
210
+ /**
211
+ * Recursively flatten nested routes with depth limit to prevent infinite recursion
212
+ * Transforms: { products: { detail: "/product/:slug" } } => { "products.detail": "/product/:slug" }
213
+ * Also handles RouteConfig objects: { api: { path: "/api" } } => { "api": "/api" }
214
+ */
215
+ type FlattenRoutes<
216
+ T extends RouteDefinition,
217
+ Prefix extends string = "",
218
+ Depth extends readonly unknown[] = []
219
+ > = Depth['length'] extends 5
220
+ ? never
221
+ : {
222
+ [K in keyof T]: T[K] extends string
223
+ ? Record<`${Prefix}${K & string}`, T[K]>
224
+ : T[K] extends RouteConfig
225
+ ? Record<`${Prefix}${K & string}`, T[K]['path']>
226
+ : T[K] extends RouteDefinition
227
+ ? FlattenRoutes<T[K], `${Prefix}${K & string}.`, readonly [...Depth, unknown]>
228
+ : never;
229
+ }[keyof T];
230
+
231
+ /**
232
+ * Union to intersection helper
233
+ */
234
+ type UnionToIntersection<U> = (
235
+ U extends unknown ? (k: U) => void : never
236
+ ) extends (k: infer I) => void
237
+ ? I
238
+ : never;
239
+
240
+ /**
241
+ * Resolved route map - flattened route definitions with full paths
242
+ */
243
+ export type ResolvedRouteMap<T extends RouteDefinition> = UnionToIntersection<FlattenRoutes<T>>;
244
+
245
+ /**
246
+ * Handler function that receives context and returns React content
247
+ */
248
+ export type Handler<TParams = {}, TEnv = any> = (
249
+ ctx: HandlerContext<TParams, TEnv>
250
+ ) => ReactNode | Promise<ReactNode>;
251
+
252
+ /**
253
+ * Context passed to handlers (Hono-inspired type-safe context)
254
+ *
255
+ * Provides type-safe access to:
256
+ * - Route params (from URL pattern)
257
+ * - Request data (request, searchParams, pathname, url)
258
+ * - Platform bindings (env.DB, env.KV, env.SECRETS)
259
+ * - Middleware variables (var.user, var.permissions)
260
+ * - Getter/setter for variables (get('user'), set('user', ...))
261
+ *
262
+ * **Important:** System parameters (query params starting with `_rsc`) are filtered out.
263
+ * Handlers see only user-facing query params. Access raw request via `_originalRequest`.
264
+ *
265
+ * @example
266
+ * ```typescript
267
+ * const handler = (ctx: HandlerContext<{ slug: string }, AppEnv>) => {
268
+ * ctx.params.slug // Route param (string)
269
+ * ctx.env.DB // Binding (D1Database)
270
+ * ctx.var.user // Variable (User | undefined)
271
+ * ctx.get('user') // Alternative getter
272
+ * ctx.set('user', {...}) // Setter
273
+ *
274
+ * // Clean URLs (system params filtered):
275
+ * ctx.url // No _rsc* params
276
+ * ctx.searchParams // No _rsc* params
277
+ *
278
+ * // Advanced: access raw request
279
+ * ctx._originalRequest // Full request with all params
280
+ * }
281
+ * ```
282
+ */
283
+ export type HandlerContext<TParams = {}, TEnv = any> = {
284
+ params: TParams;
285
+ request: Request;
286
+ searchParams: URLSearchParams; // Filtered (no _rsc* params)
287
+ pathname: string;
288
+ url: URL; // Filtered (no _rsc* params)
289
+ env: TEnv extends RouterEnv<infer B, any> ? B : {};
290
+ var: TEnv extends RouterEnv<any, infer V> ? V : {};
291
+ get: TEnv extends RouterEnv<any, infer V>
292
+ ? <K extends keyof V>(key: K) => V[K]
293
+ : (key: string) => any;
294
+ set: TEnv extends RouterEnv<any, infer V>
295
+ ? <K extends keyof V>(key: K, value: V[K]) => void
296
+ : (key: string, value: any) => void;
297
+ _originalRequest: Request; // Raw request (includes all system params)
298
+ /**
299
+ * Stub response for setting headers/cookies.
300
+ * Headers set here are merged into the final response.
301
+ *
302
+ * @example
303
+ * ```typescript
304
+ * route("product", (ctx) => {
305
+ * ctx.res.headers.set("Cache-Control", "s-maxage=60");
306
+ * return <ProductPage />;
307
+ * });
308
+ * ```
309
+ */
310
+ res: Response;
311
+ /**
312
+ * Shorthand for ctx.res.headers - response headers.
313
+ * Headers set here are merged into the final response.
314
+ *
315
+ * @example
316
+ * ```typescript
317
+ * route("product", (ctx) => {
318
+ * ctx.headers.set("Cache-Control", "s-maxage=60");
319
+ * return <ProductPage />;
320
+ * });
321
+ * ```
322
+ */
323
+ headers: Headers;
324
+ /**
325
+ * Access loader data or push handle data.
326
+ *
327
+ * For loaders: Returns a promise that resolves to the loader data.
328
+ * Loaders are executed in parallel and memoized per request.
329
+ *
330
+ * For handles: Returns a push function to add data for this segment.
331
+ * Handle data accumulates across all matched route segments.
332
+ * Push accepts: direct value, Promise, or async callback (executed immediately).
333
+ *
334
+ * @example
335
+ * ```typescript
336
+ * // Loader usage
337
+ * route("cart", async (ctx) => {
338
+ * const cart = await ctx.use(CartLoader);
339
+ * return <CartPage cart={cart} />;
340
+ * });
341
+ *
342
+ * // Handle usage - direct value
343
+ * route("shop", (ctx) => {
344
+ * const push = ctx.use(Breadcrumbs);
345
+ * push({ label: "Shop", href: "/shop" });
346
+ * return <ShopPage />;
347
+ * });
348
+ *
349
+ * // Handle usage - Promise
350
+ * route("product", (ctx) => {
351
+ * const push = ctx.use(Breadcrumbs);
352
+ * push(fetchProductBreadcrumb(ctx.params.id));
353
+ * return <ProductPage />;
354
+ * });
355
+ *
356
+ * // Handle usage - async callback (executed immediately)
357
+ * route("product", (ctx) => {
358
+ * const push = ctx.use(Breadcrumbs);
359
+ * push(async () => {
360
+ * const product = await db.getProduct(ctx.params.id);
361
+ * return { label: product.name, href: `/product/${product.id}` };
362
+ * });
363
+ * return <ProductPage />;
364
+ * });
365
+ * ```
366
+ */
367
+ use: {
368
+ <T, TLoaderParams = any>(loader: LoaderDefinition<T, TLoaderParams>): Promise<T>;
369
+ <TData, TAccumulated = TData[]>(handle: Handle<TData, TAccumulated>): (
370
+ data: TData | Promise<TData> | (() => Promise<TData>)
371
+ ) => void;
372
+ };
373
+ /**
374
+ * Internal: Current segment ID for handle data attribution.
375
+ * Set by the router before calling each handler.
376
+ * @internal
377
+ */
378
+ _currentSegmentId?: string;
379
+ /**
380
+ * Current theme (from cookie or default).
381
+ * Only available when theme is enabled in router config.
382
+ *
383
+ * @example
384
+ * ```typescript
385
+ * route("settings", (ctx) => {
386
+ * const currentTheme = ctx.theme; // "light" | "dark" | "system" | undefined
387
+ * return <SettingsPage theme={currentTheme} />;
388
+ * });
389
+ * ```
390
+ */
391
+ theme?: Theme;
392
+ /**
393
+ * Set the theme (only available when theme is enabled in router config).
394
+ * Sets a cookie with the new theme value.
395
+ *
396
+ * @example
397
+ * ```typescript
398
+ * route("settings", async (ctx) => {
399
+ * if (ctx.request.method === "POST") {
400
+ * const formData = await ctx.request.formData();
401
+ * const newTheme = formData.get("theme") as Theme;
402
+ * ctx.setTheme?.(newTheme);
403
+ * }
404
+ * return <SettingsPage />;
405
+ * });
406
+ * ```
407
+ */
408
+ setTheme?: (theme: Theme) => void;
409
+ /**
410
+ * Generate URLs from route names with scoped resolution.
411
+ *
412
+ * Resolution priority:
413
+ * 1. Path-based (`/about`) → Use directly
414
+ * 2. Absolute name (`shop.cart`) → Global lookup (contains dot)
415
+ * 3. Local name (`index`) → Prepend current name prefix, then lookup
416
+ *
417
+ * @example
418
+ * ```typescript
419
+ * // In a handler within blogPatterns (mounted at /blog with name "blog"):
420
+ * route("post", (ctx) => {
421
+ * const homeUrl = ctx.href("index"); // → "/blog/"
422
+ * const postUrl = ctx.href("post", { slug: "hello" }); // → "/blog/hello"
423
+ * const cartUrl = ctx.href("shop.cart"); // → "/shop/cart"
424
+ * const aboutUrl = ctx.href("/about"); // → "/about"
425
+ * return <PostPage />;
426
+ * });
427
+ * ```
428
+ */
429
+ href: (name: string, params?: Record<string, string>) => string;
430
+ };
431
+
432
+ /**
433
+ * Generic params type - flexible object with string keys
434
+ * Users can narrow this by explicitly typing their params:
435
+ *
436
+ * @example
437
+ * ```typescript
438
+ * [revalidate('post')]: (({ currentParams, nextParams }: RevalidateParams<{ slug: string }>) => {
439
+ * currentParams.slug // typed as string
440
+ * return currentParams.slug !== nextParams.slug;
441
+ * })
442
+ * ```
443
+ */
444
+ export type GenericParams = { [key: string]: string | undefined };
445
+
446
+ /**
447
+ * Helper type for revalidation handler params
448
+ * Allows inline type annotation for stricter param typing
449
+ *
450
+ * @example
451
+ * ```typescript
452
+ * [revalidate('post')]: (params: RevalidateParams<{ slug: string }>) => {
453
+ * params.currentParams.slug // typed as string
454
+ * return params.defaultShouldRevalidate;
455
+ * }
456
+ * ```
457
+ */
458
+ export type RevalidateParams<TParams = GenericParams, TEnv = any> = Parameters<ShouldRevalidateFn<TParams, TEnv>>[0];
459
+
460
+ /**
461
+ * Should revalidate function signature (inspired by React Router)
462
+ *
463
+ * Determines whether a route segment should re-render during partial navigation.
464
+ * Multiple revalidation functions can be defined per route - they execute in order.
465
+ *
466
+ * **Return Types:**
467
+ * - `boolean` - Hard decision: immediately returns this value (short-circuits)
468
+ * - `{ defaultShouldRevalidate: boolean }` - Soft decision: updates suggestion for next revalidator
469
+ *
470
+ * **Execution Flow:**
471
+ * 1. Start with built-in `defaultShouldRevalidate` (true if params changed)
472
+ * 2. Execute global revalidators first, then route-specific
473
+ * 3. Hard decision (boolean): stop immediately and use that value
474
+ * 4. Soft decision (object): update suggestion and continue to next revalidator
475
+ * 5. If all return soft decisions: use the final suggestion
476
+ *
477
+ * @param args.currentParams - Previous route params (generic by default, can be narrowed)
478
+ * @param args.currentUrl - Previous URL
479
+ * @param args.nextParams - Next route params (generic by default, can be narrowed)
480
+ * @param args.nextUrl - Next URL
481
+ * @param args.defaultShouldRevalidate - Current suggestion (updated by soft decisions)
482
+ * @param args.context - App context (db, user, etc.)
483
+ * @param args.actionResult - Result from action (future support)
484
+ * @param args.formData - Form data from action (future support)
485
+ * @param args.formMethod - HTTP method from action (future support)
486
+ *
487
+ * @returns Hard decision (boolean) or soft suggestion (object)
488
+ *
489
+ * @example
490
+ * ```typescript
491
+ * // Hard decision - definitive answer
492
+ * [revalidate('post')]: ({ currentParams, nextParams }) => {
493
+ * return currentParams.slug !== nextParams.slug; // boolean - short-circuits
494
+ * }
495
+ *
496
+ * // Soft decision - allows downstream revalidators to override
497
+ * [revalidate('*', 'global')]: ({ defaultShouldRevalidate }) => {
498
+ * return { defaultShouldRevalidate: true }; // object - continues to next
499
+ * }
500
+ *
501
+ * // Explicit typing for stricter params
502
+ * [revalidate('post')]: ((params: RevalidateParams<{ slug: string }>) => {
503
+ * return params.currentParams.slug !== params.nextParams.slug;
504
+ * })
505
+ * ```
506
+ */
507
+ export type ShouldRevalidateFn<TParams = GenericParams, TEnv = any> = (args: {
508
+ currentParams: TParams;
509
+ currentUrl: URL;
510
+ nextParams: TParams;
511
+ nextUrl: URL;
512
+ defaultShouldRevalidate: boolean;
513
+ context: HandlerContext<TParams, TEnv>;
514
+ // Segment metadata (which segment is being evaluated):
515
+ segmentType: 'layout' | 'route' | 'parallel';
516
+ layoutName?: string; // Layout name (e.g., "root", "shop", "auth") - only for layouts
517
+ slotName?: string; // Slot name (e.g., "@sidebar", "@modal") - only for parallels
518
+ // Action context (populated when revalidation triggered by server action):
519
+ actionId?: string; // Action identifier (e.g., "src/actions.ts#addToCart")
520
+ actionUrl?: URL; // URL where action was executed
521
+ actionResult?: any; // Return value from action execution
522
+ formData?: FormData; // FormData from action request
523
+ method?: string; // Request method: 'GET' for navigation, 'POST' for actions
524
+ routeName?: string; // Route name where action was executed (e.g., "products.detail")
525
+ // Stale cache revalidation (SWR pattern):
526
+ stale?: boolean; // True if this is a stale cache revalidation request
527
+ }) => boolean | { defaultShouldRevalidate: boolean };
528
+
529
+ /**
530
+ * Middleware function signature
531
+ *
532
+ * Middleware can either call `next()` to continue the pipeline,
533
+ * or return a Response to short-circuit and skip remaining middleware + handler.
534
+ *
535
+ * **Short-Circuit Patterns:**
536
+ * - `return redirect('/login')` - Soft redirect (SPA navigation)
537
+ * - `return Response.redirect('/login', 302)` - Hard redirect (full page reload)
538
+ * - `return new Response('Unauthorized', { status: 401 })` - Error response
539
+ *
540
+ * @param TParams - Route params (defaults to GenericParams, can be narrowed with satisfies)
541
+ * @param TEnv - Environment type
542
+ *
543
+ * @example
544
+ * ```typescript
545
+ * [middleware('checkout.*', 'auth')]: [
546
+ * (ctx, next) => {
547
+ * if (!ctx.get('user')) {
548
+ * return redirect('/login'); // Soft redirect - short-circuit
549
+ * }
550
+ * next(); // Continue pipeline
551
+ * }
552
+ * ]
553
+ * ```
554
+ */
555
+ // MiddlewareFn is imported from "./router/middleware.js" and re-exported
556
+
557
+ /**
558
+ * Extract all route keys from a route definition (includes flattened nested routes)
559
+ */
560
+ export type RouteKeys<T extends RouteDefinition> = keyof ResolvedRouteMap<T> & string;
561
+
562
+ /**
563
+ * Valid layout value - component or handler function
564
+ * Note: Arrays are not supported. Use separate layout() declarations with unique names instead.
565
+ */
566
+ type LayoutValue<TEnv = any> =
567
+ | ReactNode
568
+ | Handler<any, TEnv>;
569
+
570
+ /**
571
+ * Helper to extract params from a route key using the resolved (flattened) route map
572
+ */
573
+ export type ExtractRouteParams<T extends RouteDefinition, K extends string> =
574
+ K extends keyof ResolvedRouteMap<T>
575
+ ? ResolvedRouteMap<T>[K] extends string
576
+ ? ExtractParams<ResolvedRouteMap<T>[K]>
577
+ : GenericParams
578
+ : GenericParams;
579
+
580
+ /**
581
+ * Handlers object that maps route names to handler functions with type-safe string patterns
582
+ */
583
+ export type HandlersForRouteMap<T extends RouteDefinition, TEnv = any> = {
584
+ // Route handlers - type-safe params extracted from route patterns
585
+ [K in RouteKeys<T>]?: Handler<ExtractRouteParams<T, K & string>, TEnv>;
586
+ } & {
587
+ // Layout patterns: $layout.{routeName}.{layoutName}
588
+ [K in `$layout.${RouteKeys<T> | '*'}.${string}`]?: LayoutValue<TEnv>;
589
+ } & {
590
+ // Parallel route patterns: $parallel.{routeName}.{parallelName}
591
+ [K in `$parallel.${RouteKeys<T>}.${string}`]?: Record<
592
+ `@${string}`,
593
+ Handler<
594
+ K extends `$parallel.${infer RouteKey}.${string}`
595
+ ? RouteKey extends RouteKeys<T>
596
+ ? ExtractRouteParams<T, RouteKey & string>
597
+ : GenericParams
598
+ : GenericParams,
599
+ TEnv
600
+ >
601
+ >;
602
+ } & {
603
+ // Global parallel routes (with '*') use GenericParams
604
+ [K in `$parallel.${"*"}.${string}`]?: Record<`@${string}`, Handler<GenericParams, TEnv>>;
605
+ } & {
606
+ // Middleware patterns: $middleware.{routeName}.{middlewareName}
607
+ [K in `$middleware.${RouteKeys<T> | '*'}.${string}`]?: MiddlewareFn<TEnv, GenericParams>[];
608
+ } & {
609
+ // Route revalidate patterns: $revalidate.route.{routeName}.{revalidateName}
610
+ [K in `$revalidate.route.${RouteKeys<T> | '*'}.${string}`]?: ShouldRevalidateFn<GenericParams, TEnv>;
611
+ } & {
612
+ // Layout revalidate patterns: $revalidate.layout.{routeName}.{layoutName}.{revalidateName}
613
+ [K in `$revalidate.layout.${RouteKeys<T> | '*'}.${string}.${string}`]?: ShouldRevalidateFn<GenericParams, TEnv>;
614
+ } & {
615
+ // Parallel revalidate patterns: $revalidate.parallel.{routeName}.{parallelName}.{slotName}.{revalidateName}
616
+ [K in `$revalidate.parallel.${RouteKeys<T> | '*'}.${string}.${string}.${string}`]?: ShouldRevalidateFn<GenericParams, TEnv>;
617
+ };
618
+
619
+ /**
620
+ * Error information passed to error boundary fallback components
621
+ */
622
+ export interface ErrorInfo {
623
+ /** Error message (always available) */
624
+ message: string;
625
+ /** Error name/type (e.g., "RouteNotFoundError", "MiddlewareError") */
626
+ name: string;
627
+ /** Optional error code for programmatic handling */
628
+ code?: string;
629
+ /** Stack trace (only in development) */
630
+ stack?: string;
631
+ /** Original error cause if available */
632
+ cause?: unknown;
633
+ /** Segment ID where the error occurred */
634
+ segmentId: string;
635
+ /** Segment type where the error occurred */
636
+ segmentType: "layout" | "route" | "parallel" | "loader" | "middleware" | "cache";
637
+ }
638
+
639
+ /**
640
+ * Props passed to server-side error boundary fallback components
641
+ *
642
+ * Server error boundaries don't have a reset function since the error
643
+ * occurred during server rendering. Users can navigate away or refresh.
644
+ *
645
+ * @example
646
+ * ```typescript
647
+ * function ProductErrorFallback({ error }: ErrorBoundaryFallbackProps) {
648
+ * return (
649
+ * <div>
650
+ * <h2>Something went wrong loading the product</h2>
651
+ * <p>{error.message}</p>
652
+ * <a href="/">Go home</a>
653
+ * </div>
654
+ * );
655
+ * }
656
+ * ```
657
+ */
658
+ export interface ErrorBoundaryFallbackProps {
659
+ /** Error information */
660
+ error: ErrorInfo;
661
+ }
662
+
663
+ /**
664
+ * Error boundary handler - receives error info and returns fallback UI
665
+ */
666
+ export type ErrorBoundaryHandler = (props: ErrorBoundaryFallbackProps) => ReactNode;
667
+
668
+ /**
669
+ * Props passed to client-side error boundary fallback components
670
+ *
671
+ * Client error boundaries have a reset function that clears the error state
672
+ * and re-renders the children.
673
+ *
674
+ * @example
675
+ * ```typescript
676
+ * function ClientErrorFallback({ error, reset }: ClientErrorBoundaryFallbackProps) {
677
+ * return (
678
+ * <div>
679
+ * <h2>Something went wrong</h2>
680
+ * <p>{error.message}</p>
681
+ * <button onClick={reset}>Try again</button>
682
+ * </div>
683
+ * );
684
+ * }
685
+ * ```
686
+ */
687
+ export interface ClientErrorBoundaryFallbackProps {
688
+ /** Error information */
689
+ error: ErrorInfo;
690
+ /** Function to reset error state and retry rendering */
691
+ reset: () => void;
692
+ }
693
+
694
+ /**
695
+ * Wrapped loader data result for deferred resolution with error handling.
696
+ * When loaders are deferred to client-side resolution, errors need to be
697
+ * wrapped so the client can handle them appropriately.
698
+ */
699
+ export type LoaderDataResult<T = unknown> =
700
+ | { __loaderResult: true; ok: true; data: T }
701
+ | { __loaderResult: true; ok: false; error: ErrorInfo; fallback: ReactNode | null };
702
+
703
+ /**
704
+ * Type guard to check if a value is a wrapped loader result
705
+ */
706
+ export function isLoaderDataResult(value: unknown): value is LoaderDataResult {
707
+ return (
708
+ typeof value === "object" &&
709
+ value !== null &&
710
+ "__loaderResult" in value &&
711
+ (value as any).__loaderResult === true
712
+ );
713
+ }
714
+
715
+ /**
716
+ * Not found information passed to notFound boundary fallback components
717
+ */
718
+ export interface NotFoundInfo {
719
+ /** Not found message */
720
+ message: string;
721
+ /** Segment ID where notFound was thrown */
722
+ segmentId: string;
723
+ /** Segment type where notFound was thrown */
724
+ segmentType: "layout" | "route" | "parallel" | "loader" | "middleware" | "cache";
725
+ /** The pathname that triggered the not found */
726
+ pathname?: string;
727
+ }
728
+
729
+ /**
730
+ * Props passed to notFound boundary fallback components
731
+ *
732
+ * @example
733
+ * ```typescript
734
+ * function ProductNotFound({ notFound }: NotFoundBoundaryFallbackProps) {
735
+ * return (
736
+ * <div>
737
+ * <h2>Product Not Found</h2>
738
+ * <p>{notFound.message}</p>
739
+ * <a href="/products">Browse all products</a>
740
+ * </div>
741
+ * );
742
+ * }
743
+ * ```
744
+ */
745
+ export interface NotFoundBoundaryFallbackProps {
746
+ /** Not found information */
747
+ notFound: NotFoundInfo;
748
+ }
749
+
750
+ /**
751
+ * NotFound boundary handler - receives not found info and returns fallback UI
752
+ */
753
+ export type NotFoundBoundaryHandler = (props: NotFoundBoundaryFallbackProps) => ReactNode;
754
+
755
+ /**
756
+ * Resolved segment with component
757
+ *
758
+ * Segment types:
759
+ * - layout: Wraps child content via <Outlet />
760
+ * - route: The leaf content for a URL
761
+ * - parallel: Named slots rendered via <ParallelOutlet name="@slot" />
762
+ * - loader: Data segment (no visual rendering, carries loaderData)
763
+ * - error: Error fallback segment (replaces failed segment with error UI)
764
+ * - notFound: Not found fallback segment (replaces segment when data not found)
765
+ */
766
+ export interface ResolvedSegment {
767
+ id: string;
768
+ namespace: string; // Optional namespace for segment (used for parallel groups)
769
+ type: "layout" | "route" | "parallel" | "loader" | "error" | "notFound";
770
+ index: number;
771
+ component: ReactNode | Promise<ReactNode>; // Component or handler promise
772
+ loading?: ReactNode; // Loading component for this segment (shown during navigation)
773
+ layout?: ReactNode; // Layout element to wrap content (used by intercept segments)
774
+ params?: Record<string, string>;
775
+ slot?: string; // For parallel segments: '@sidebar', '@modal', etc.
776
+ belongsToRoute?: boolean; // True if segment belongs to the matched route (route itself + its children)
777
+ layoutName?: string; // For layouts: the layout name identifier
778
+ parallelName?: string; // For parallels: the parallel group name (used to match with revalidations)
779
+ // Loader-specific fields
780
+ loaderId?: string; // For loaders: the loader $$id identifier
781
+ loaderData?: any; // For loaders: the resolved data from loader execution
782
+ // Intercept loader fields (for streaming loader data in parallel segments)
783
+ loaderDataPromise?: Promise<any[]> | any[]; // Loader data promise or resolved array
784
+ loaderIds?: string[]; // IDs ($$id) of loaders for this segment
785
+ // Error-specific fields
786
+ error?: ErrorInfo; // For error segments: the error information
787
+ // NotFound-specific fields
788
+ notFoundInfo?: NotFoundInfo; // For notFound segments: the not found information
789
+ }
790
+
791
+ /**
792
+ * Segment metadata (without component)
793
+ */
794
+ export interface SegmentMetadata {
795
+ id: string;
796
+ type: "layout" | "route" | "parallel" | "loader" | "error" | "notFound";
797
+ index: number;
798
+ params?: Record<string, string>;
799
+ slot?: string;
800
+ loaderId?: string;
801
+ error?: ErrorInfo;
802
+ notFoundInfo?: NotFoundInfo;
803
+ }
804
+
805
+ // Note: route symbols are now defined in route-definition.ts
806
+ // as properties on the route() function
807
+
808
+ /**
809
+ * State of a named slot (e.g., @modal, @sidebar)
810
+ * Used for intercepting routes where slots render alternative content
811
+ */
812
+ export interface SlotState {
813
+ /**
814
+ * Whether the slot is currently active (has content to render)
815
+ */
816
+ active: boolean;
817
+ /**
818
+ * Segments for this slot when active
819
+ */
820
+ segments?: ResolvedSegment[];
821
+ }
822
+
823
+ /**
824
+ * Props passed to the root layout component
825
+ */
826
+ export interface RootLayoutProps {
827
+ children: ReactNode;
828
+ }
829
+
830
+ /**
831
+ * Router match result
832
+ */
833
+ export interface MatchResult {
834
+ segments: ResolvedSegment[];
835
+ matched: string[];
836
+ diff: string[];
837
+ /**
838
+ * Merged route params from all matched segments
839
+ * Available for use by the handler after route matching
840
+ */
841
+ params: Record<string, string>;
842
+ /**
843
+ * The matched route name (includes name prefix from include()).
844
+ * Used by useHref() for local name resolution.
845
+ */
846
+ routeName?: string;
847
+ /**
848
+ * Map of route names to URL patterns.
849
+ * Used by useHref() to resolve route names to URLs.
850
+ */
851
+ routeMap?: Record<string, string>;
852
+ /**
853
+ * Server-Timing header value (only present when debugPerformance is enabled)
854
+ * Can be added to response headers for DevTools integration
855
+ */
856
+ serverTiming?: string;
857
+ /**
858
+ * State of named slots for this route match
859
+ * Key is slot name (e.g., "@modal"), value is slot state
860
+ * Slots are used for intercepting routes during soft navigation
861
+ */
862
+ slots?: Record<string, SlotState>;
863
+ /**
864
+ * Redirect URL for trailing slash normalization.
865
+ * When set, the RSC handler should return a 308 redirect to this URL
866
+ * instead of rendering the page.
867
+ */
868
+ redirect?: string;
869
+ /**
870
+ * Route-level middleware collected from the matched entry tree.
871
+ * These run with the same onion-style execution as app-level middleware,
872
+ * wrapping the entire RSC response creation.
873
+ */
874
+ routeMiddleware?: Array<{
875
+ handler: import("./router/middleware.js").MiddlewareFn;
876
+ params: Record<string, string>;
877
+ }>;
878
+ }
879
+
880
+ /**
881
+ * Internal route entry stored in router
882
+ */
883
+ export interface RouteEntry<TEnv = any> {
884
+ prefix: string;
885
+ routes: ResolvedRouteMap<any>;
886
+ /**
887
+ * Trailing slash config per route key
888
+ * If not specified for a route, defaults to pattern-based detection
889
+ */
890
+ trailingSlash?: Record<string, TrailingSlashMode>;
891
+ handler: () =>
892
+ | Array<AllUseItems>
893
+ | Promise<{ default: () => Array<AllUseItems> }>
894
+ | Promise<() => Array<AllUseItems>>;
895
+ mountIndex: number;
896
+ }
897
+
898
+ /**
899
+ * Type-safe route handler helper for specific routes
900
+ *
901
+ * Automatically extracts the correct param types from your route definition.
902
+ *
903
+ * @template TRoutes - Your route definition object (e.g., typeof shopRoutes)
904
+ * @template K - The route key (e.g., "cart", "products.detail")
905
+ * @template TEnv - Environment type (defaults to global RSCRouter.Env)
906
+ *
907
+ * @example
908
+ * ```typescript
909
+ * import { RouteHandler } from "rsc-router";
910
+ * import { shopRoutes } from "./routes.js";
911
+ *
912
+ * export const cartRoute: RouteHandler<typeof shopRoutes, "cart"> = (ctx) => {
913
+ * // ctx.params is typed correctly for the cart route
914
+ * // ctx.get('user') is type-safe via global augmentation
915
+ * return <CartPage />;
916
+ * }
917
+ *
918
+ * export const productRoute: RouteHandler<typeof shopRoutes, "products.detail"> = (ctx) => {
919
+ * // ctx.params.slug is automatically typed as string
920
+ * return <ProductDetail slug={ctx.params.slug} />;
921
+ * }
922
+ * ```
923
+ */
924
+ export type RouteHandler<
925
+ TRoutes extends RouteDefinition,
926
+ K extends keyof TRoutes,
927
+ TEnv = DefaultEnv
928
+ > = Handler<ExtractRouteParams<TRoutes, K & string>, TEnv>;
929
+
930
+ /**
931
+ * Type-safe revalidation function helper for specific routes
932
+ *
933
+ * Automatically extracts the correct param types from your route definition.
934
+ *
935
+ * @template TRoutes - Your route definition object (e.g., typeof shopRoutes)
936
+ * @template K - The route key (e.g., "cart", "products.detail")
937
+ * @template TEnv - Environment type (defaults to global RSCRouter.Env)
938
+ *
939
+ * @example
940
+ * ```typescript
941
+ * import { RouteRevalidateFn } from "rsc-router";
942
+ * import { shopRoutes } from "./routes.js";
943
+ *
944
+ * export const cartRevalidation: RouteRevalidateFn<typeof shopRoutes, "cart"> = ({
945
+ * currentParams,
946
+ * nextParams
947
+ * }) => {
948
+ * // params are typed correctly for the cart route
949
+ * return true; // Always revalidate cart
950
+ * }
951
+ *
952
+ * export const productRevalidation: RouteRevalidateFn<typeof shopRoutes, "products.detail"> = ({
953
+ * currentParams,
954
+ * nextParams
955
+ * }) => {
956
+ * // currentParams.slug and nextParams.slug are automatically typed
957
+ * return currentParams.slug !== nextParams.slug;
958
+ * }
959
+ * ```
960
+ */
961
+ export type RouteRevalidateFn<
962
+ TRoutes extends RouteDefinition,
963
+ K extends keyof TRoutes,
964
+ TEnv = DefaultEnv
965
+ > = ShouldRevalidateFn<ExtractRouteParams<TRoutes, K & string>, TEnv>;
966
+
967
+ /**
968
+ * Type-safe middleware function helper for specific routes
969
+ *
970
+ * Automatically extracts the correct param types from your route definition.
971
+ *
972
+ * @template TRoutes - Your route definition object (e.g., typeof shopRoutes)
973
+ * @template K - The route key (e.g., "checkout.index", "account.orders")
974
+ * @template TEnv - Environment type (defaults to global RSCRouter.Env)
975
+ *
976
+ * @example
977
+ * ```typescript
978
+ * import { RouteMiddlewareFn } from "rsc-router";
979
+ * import { shopRoutes } from "./routes.js";
980
+ *
981
+ * export const checkoutMiddleware: RouteMiddlewareFn<typeof shopRoutes, "checkout.index"> = async (ctx, next) => {
982
+ * // ctx.params is typed correctly for checkout.index route
983
+ * // ctx.get('user') is type-safe via global augmentation
984
+ * if (!ctx.get('user')) {
985
+ * return redirect('/login');
986
+ * }
987
+ * await next();
988
+ * // ctx.res available here for header modification
989
+ * }
990
+ *
991
+ * export const productMiddleware: RouteMiddlewareFn<typeof shopRoutes, "products.detail"> = async (ctx, next) => {
992
+ * // ctx.params.slug is automatically typed as string
993
+ * console.log('Viewing product:', ctx.params.slug);
994
+ * const response = await next();
995
+ * response.headers.set('X-Product', ctx.params.slug);
996
+ * return response;
997
+ * }
998
+ * ```
999
+ */
1000
+ export type RouteMiddlewareFn<
1001
+ TRoutes extends RouteDefinition,
1002
+ K extends keyof TRoutes,
1003
+ TEnv = DefaultEnv
1004
+ > = MiddlewareFn<TEnv, ExtractRouteParams<TRoutes, K & string>>;
1005
+
1006
+ // ============================================================================
1007
+ // Cache Types
1008
+ // ============================================================================
1009
+
1010
+ /**
1011
+ * Context passed to cache condition/key/tags functions.
1012
+ *
1013
+ * This is a subset of RequestContext that's guaranteed to be available
1014
+ * during cache key generation (before middleware runs).
1015
+ *
1016
+ * Note: While the full RequestContext is passed, middleware-set variables
1017
+ * (ctx.var, ctx.get()) may not be populated yet since cache lookup
1018
+ * happens before middleware execution.
1019
+ */
1020
+ export type { RequestContext as CacheContext } from "./server/request-context.js";
1021
+
1022
+ /**
1023
+ * Cache configuration options for cache() DSL
1024
+ *
1025
+ * Controls how segments, layouts, and loaders are cached.
1026
+ * Cache configuration inherits down the route tree unless overridden.
1027
+ *
1028
+ * @example
1029
+ * ```typescript
1030
+ * // Basic caching with TTL
1031
+ * cache({ ttl: 60 }, () => [
1032
+ * layout(<BlogLayout />),
1033
+ * route("post/:slug"),
1034
+ * ])
1035
+ *
1036
+ * // With stale-while-revalidate
1037
+ * cache({ ttl: 60, swr: 300 }, () => [
1038
+ * route("product/:id"),
1039
+ * ])
1040
+ *
1041
+ * // Conditional caching
1042
+ * cache({
1043
+ * ttl: 300,
1044
+ * condition: (ctx) => !ctx.request.headers.get('x-preview'),
1045
+ * }, () => [...])
1046
+ *
1047
+ * // Custom cache key
1048
+ * cache({
1049
+ * ttl: 300,
1050
+ * key: (ctx) => `product-${ctx.params.id}-${ctx.searchParams.get('variant')}`,
1051
+ * }, () => [...])
1052
+ *
1053
+ * // With tags for invalidation
1054
+ * cache({
1055
+ * ttl: 300,
1056
+ * tags: (ctx) => [`product:${ctx.params.id}`, 'products'],
1057
+ * }, () => [...])
1058
+ * ```
1059
+ */
1060
+ export interface CacheOptions<TEnv = unknown> {
1061
+ /**
1062
+ * Time-to-live in seconds.
1063
+ * After this period, cached content is considered stale.
1064
+ */
1065
+ ttl: number;
1066
+
1067
+ /**
1068
+ * Stale-while-revalidate window in seconds (after TTL).
1069
+ * During this window, stale content is served immediately while
1070
+ * fresh content is fetched in the background via waitUntil.
1071
+ *
1072
+ * @example
1073
+ * // TTL: 60s, SWR: 300s
1074
+ * // 0-60s: FRESH (serve from cache)
1075
+ * // 60-360s: STALE (serve from cache, revalidate in background)
1076
+ * // 360s+: EXPIRED (cache miss, fetch fresh)
1077
+ */
1078
+ swr?: number;
1079
+
1080
+ /**
1081
+ * Override the cache store for this boundary.
1082
+ * When specified, this boundary and its children use this store
1083
+ * instead of the app-level store from handler config.
1084
+ *
1085
+ * Useful for:
1086
+ * - Different backends per route section (memory vs KV vs Redis)
1087
+ * - Loader-specific caching strategies
1088
+ * - Hot data in fast cache, cold data in larger/slower cache
1089
+ *
1090
+ * @example
1091
+ * ```typescript
1092
+ * const kvStore = new CloudflareKVStore(env.CACHE_KV);
1093
+ * const memoryStore = new MemorySegmentCacheStore({ defaults: { ttl: 10 } });
1094
+ *
1095
+ * // Fast memory cache for hot data
1096
+ * cache({ store: memoryStore }, () => [
1097
+ * route("dashboard"),
1098
+ * ])
1099
+ *
1100
+ * // KV for larger, less frequently accessed data
1101
+ * cache({ store: kvStore, ttl: 3600 }, () => [
1102
+ * route("archive/:year"),
1103
+ * ])
1104
+ * ```
1105
+ */
1106
+ store?: import("./cache/types.js").SegmentCacheStore;
1107
+
1108
+ /**
1109
+ * Conditional cache read function.
1110
+ * Return false to skip cache for this request (always fetch fresh).
1111
+ *
1112
+ * Has access to full RequestContext including env, request, params, cookies, etc.
1113
+ * Note: Middleware-set variables (ctx.var) may not be populated yet.
1114
+ *
1115
+ * @example
1116
+ * ```typescript
1117
+ * condition: (ctx) => {
1118
+ * // Skip cache for preview mode
1119
+ * if (ctx.request.headers.get('x-preview')) return false;
1120
+ * // Skip cache for authenticated users
1121
+ * if (ctx.request.headers.has('authorization')) return false;
1122
+ * return true;
1123
+ * }
1124
+ * ```
1125
+ */
1126
+ condition?: (ctx: import("./server/request-context.js").RequestContext<TEnv>) => boolean;
1127
+
1128
+ /**
1129
+ * Custom cache key function - FULL OVERRIDE.
1130
+ * Bypasses default key generation AND store's keyGenerator.
1131
+ *
1132
+ * Has access to full RequestContext including env, request, params, cookies, etc.
1133
+ * Note: Middleware-set variables (ctx.var) may not be populated yet.
1134
+ *
1135
+ * @example
1136
+ * ```typescript
1137
+ * // Include query params in cache key
1138
+ * key: (ctx) => `product-${ctx.params.id}-${ctx.searchParams.get('variant')}`
1139
+ *
1140
+ * // Include env bindings
1141
+ * key: (ctx) => `${ctx.env.REGION}:product:${ctx.params.id}`
1142
+ *
1143
+ * // Include cookies
1144
+ * key: (ctx) => `${ctx.cookie('locale')}:${ctx.pathname}`
1145
+ * ```
1146
+ */
1147
+ key?: (ctx: import("./server/request-context.js").RequestContext<TEnv>) => string | Promise<string>;
1148
+
1149
+ /**
1150
+ * Tags for cache invalidation.
1151
+ * Can be a static array or a function that returns tags.
1152
+ *
1153
+ * @example
1154
+ * ```typescript
1155
+ * // Static tags
1156
+ * tags: ['products', 'catalog']
1157
+ *
1158
+ * // Dynamic tags
1159
+ * tags: (ctx) => [`product:${ctx.params.id}`, 'products']
1160
+ * ```
1161
+ */
1162
+ tags?: string[] | ((ctx: import("./server/request-context.js").RequestContext<TEnv>) => string[]);
1163
+ }
1164
+
1165
+ /**
1166
+ * Partial cache options for cache() DSL.
1167
+ *
1168
+ * When `ttl` is not specified, it will use the default from cache config.
1169
+ * This allows cache() calls to inherit app-level defaults:
1170
+ *
1171
+ * @example
1172
+ * ```typescript
1173
+ * // App-level default (in handler config)
1174
+ * cache: { store: myStore, defaults: { ttl: 60 } }
1175
+ *
1176
+ * // Route-level (inherits ttl from defaults)
1177
+ * cache(() => [
1178
+ * route("products"),
1179
+ * ])
1180
+ *
1181
+ * // Override with explicit ttl
1182
+ * cache({ ttl: 300 }, () => [...])
1183
+ * ```
1184
+ */
1185
+ export type PartialCacheOptions<TEnv = unknown> = Partial<CacheOptions<TEnv>>;
1186
+
1187
+ /**
1188
+ * Cache entry configuration stored in EntryData.
1189
+ * Represents the resolved cache config for a segment.
1190
+ */
1191
+ export interface EntryCacheConfig {
1192
+ /** Cache options (false means caching disabled for this entry) - ttl is optional, uses defaults */
1193
+ options: PartialCacheOptions | false;
1194
+ }
1195
+
1196
+ // ============================================================================
1197
+ // Loader Types
1198
+ // ============================================================================
1199
+
1200
+ /**
1201
+ * Context passed to loader functions during execution
1202
+ *
1203
+ * Loaders run after middleware but before handlers, so they have access
1204
+ * to middleware-set variables via get().
1205
+ *
1206
+ * @template TParams - Route params type (e.g., { slug: string })
1207
+ * @template TEnv - Environment type for bindings/variables
1208
+ *
1209
+ * @example
1210
+ * ```typescript
1211
+ * const CartLoader = createLoader(async (ctx) => {
1212
+ * "use server";
1213
+ * const user = ctx.get("user"); // From auth middleware
1214
+ * return await db.cart.get(user.id);
1215
+ * });
1216
+ *
1217
+ * // With typed params:
1218
+ * const ProductLoader = createLoader<Product, { slug: string }>(async (ctx) => {
1219
+ * "use server";
1220
+ * const { slug } = ctx.params; // slug is typed as string
1221
+ * return await db.products.findBySlug(slug);
1222
+ * });
1223
+ * ```
1224
+ */
1225
+ export type LoaderContext<TParams = Record<string, string | undefined>, TEnv = any, TBody = unknown> = {
1226
+ params: TParams;
1227
+ request: Request;
1228
+ searchParams: URLSearchParams;
1229
+ pathname: string;
1230
+ url: URL;
1231
+ env: TEnv extends RouterEnv<infer B, any> ? B : {};
1232
+ var: TEnv extends RouterEnv<any, infer V> ? V : {};
1233
+ get: TEnv extends RouterEnv<any, infer V>
1234
+ ? <K extends keyof V>(key: K) => V[K]
1235
+ : (key: string) => any;
1236
+ /**
1237
+ * Access another loader's data (returns promise since loaders run in parallel)
1238
+ */
1239
+ use: <T, TLoaderParams = any>(loader: LoaderDefinition<T, TLoaderParams>) => Promise<T>;
1240
+ /**
1241
+ * HTTP method (GET, POST, PUT, PATCH, DELETE)
1242
+ * Available when loader is called via load({ method: "POST", ... })
1243
+ */
1244
+ method: string;
1245
+ /**
1246
+ * Request body for POST/PUT/PATCH/DELETE requests
1247
+ * Available when loader is called via load({ method: "POST", body: {...} })
1248
+ */
1249
+ body: TBody | undefined;
1250
+ };
1251
+
1252
+ /**
1253
+ * Loader function signature
1254
+ *
1255
+ * @template T - The return type of the loader
1256
+ * @template TParams - Route params type (defaults to generic Record)
1257
+ * @template TEnv - Environment type for bindings/variables
1258
+ *
1259
+ * @example
1260
+ * ```typescript
1261
+ * const myLoader: LoaderFn<{ items: Item[] }> = async (ctx) => {
1262
+ * "use server";
1263
+ * return { items: await db.items.list() };
1264
+ * };
1265
+ *
1266
+ * // With typed params:
1267
+ * const productLoader: LoaderFn<Product, { slug: string }> = async (ctx) => {
1268
+ * "use server";
1269
+ * const { slug } = ctx.params; // typed as string
1270
+ * return await db.products.findBySlug(slug);
1271
+ * };
1272
+ * ```
1273
+ */
1274
+ export type LoaderFn<T, TParams = Record<string, string | undefined>, TEnv = any> = (
1275
+ ctx: LoaderContext<TParams, TEnv>
1276
+ ) => Promise<T> | T;
1277
+
1278
+ /**
1279
+ * Loader definition object
1280
+ *
1281
+ * Created via createLoader(). Contains the loader name and function.
1282
+ * On client builds, the fn is stripped by the bundler (via "use server" directive).
1283
+ *
1284
+ * @template T - The return type of the loader
1285
+ * @template TParams - Route params type (for type-safe params access)
1286
+ *
1287
+ * @example
1288
+ * ```typescript
1289
+ * // Definition (same file works on server and client)
1290
+ * export const CartLoader = createLoader(async (ctx) => {
1291
+ * "use server";
1292
+ * return await db.cart.get(ctx.get("user").id);
1293
+ * });
1294
+ *
1295
+ * // With typed params:
1296
+ * export const ProductLoader = createLoader<Product, { slug: string }>(async (ctx) => {
1297
+ * "use server";
1298
+ * const { slug } = ctx.params; // slug is typed as string
1299
+ * return await db.products.findBySlug(slug);
1300
+ * });
1301
+ *
1302
+ * // Server usage
1303
+ * const cart = ctx.use(CartLoader);
1304
+ *
1305
+ * // Client usage (fn is stripped, only name remains)
1306
+ * const cart = useLoader(CartLoader);
1307
+ * ```
1308
+ */
1309
+ /**
1310
+ * Options for fetchable loaders
1311
+ *
1312
+ * Middleware uses the same MiddlewareFn signature as route/app middleware,
1313
+ * enabling reuse of the same middleware functions everywhere.
1314
+ */
1315
+ export type FetchableLoaderOptions = {
1316
+ fetchable?: true;
1317
+ middleware?: MiddlewareFn[];
1318
+ };
1319
+
1320
+ /**
1321
+ * Options for load() calls - type-safe union based on method
1322
+ */
1323
+ export type LoadOptions =
1324
+ | {
1325
+ method?: "GET";
1326
+ params?: Record<string, string>;
1327
+ }
1328
+ | {
1329
+ method: "POST" | "PUT" | "PATCH" | "DELETE";
1330
+ params?: Record<string, string>;
1331
+ body?: FormData | Record<string, any>;
1332
+ };
1333
+
1334
+ /**
1335
+ * Context passed to loader action on server
1336
+ */
1337
+ export type LoaderActionContext = {
1338
+ method: string;
1339
+ params: Record<string, string>;
1340
+ body?: FormData | Record<string, any>;
1341
+ formData?: FormData;
1342
+ };
1343
+
1344
+ /**
1345
+ * @deprecated Use MiddlewareFn instead for fetchable loader middleware.
1346
+ * This type is kept for backwards compatibility but will be removed in a future version.
1347
+ *
1348
+ * Fetchable loaders now use the same middleware signature as routes,
1349
+ * enabling middleware reuse across routes and loaders.
1350
+ */
1351
+ export type LoaderMiddlewareFn = (
1352
+ ctx: LoaderActionContext,
1353
+ next: () => Promise<void>
1354
+ ) => Response | Promise<Response> | void | Promise<void>;
1355
+
1356
+ /**
1357
+ * Loader action function type - server action for form-based fetching
1358
+ * This is a server action that can be passed to useActionState or form action prop.
1359
+ *
1360
+ * The signature (prevState, formData) is required for useActionState compatibility.
1361
+ * When used with useActionState, React passes the previous state as the first argument.
1362
+ */
1363
+ export type LoaderAction<T> = (prevState: T | null, formData: FormData) => Promise<T>;
1364
+
1365
+ export type LoaderDefinition<T = any, TParams = Record<string, string | undefined>> = {
1366
+ __brand: "loader";
1367
+ $$id: string; // Injected by Vite plugin (exposeLoaderId) - unique identifier
1368
+ fn?: LoaderFn<T, TParams, any>; // Optional - stripped on client via "use server"
1369
+ action?: LoaderAction<T>; // Optional - for fetchable loaders
1370
+ };
1371
+
1372
+ // ============================================================================
1373
+ // Error Handling Types
1374
+ // ============================================================================
1375
+
1376
+ /**
1377
+ * Phase where the error occurred during request handling.
1378
+ *
1379
+ * Coverage notes:
1380
+ * - "routing": Invoked when route matching fails (router.ts, rsc/handler.ts)
1381
+ * - "manifest": Reserved for manifest loading errors (not currently invoked)
1382
+ * - "middleware": Reserved for middleware execution errors (errors propagate to handler phase)
1383
+ * - "loader": Invoked when loader execution fails (router.ts via wrapLoaderWithErrorHandling, rsc/handler.ts)
1384
+ * - "handler": Invoked when route/layout handler execution fails (router.ts)
1385
+ * - "rendering": Invoked during SSR rendering errors (ssr/index.tsx, separate callback)
1386
+ * - "action": Invoked when server action execution fails (rsc/handler.ts, router.ts)
1387
+ * - "revalidation": Invoked when revalidation fails (router.ts, conditional with action)
1388
+ * - "unknown": Fallback for unclassified errors (not currently invoked)
1389
+ */
1390
+ export type ErrorPhase =
1391
+ | "routing" // During route matching
1392
+ | "manifest" // During manifest loading (reserved, not currently invoked)
1393
+ | "middleware" // During middleware execution (errors propagate to handler phase)
1394
+ | "loader" // During loader execution
1395
+ | "handler" // During route/layout handler execution
1396
+ | "rendering" // During RSC/SSR rendering (SSR handler uses separate callback)
1397
+ | "action" // During server action execution
1398
+ | "revalidation" // During revalidation evaluation
1399
+ | "unknown"; // Fallback for unclassified errors
1400
+
1401
+ /**
1402
+ * Comprehensive context passed to onError callback
1403
+ *
1404
+ * Provides all available information about where and when an error occurred
1405
+ * during request handling. The callback can use this for logging, monitoring,
1406
+ * error tracking services, or custom error responses.
1407
+ *
1408
+ * @example
1409
+ * ```typescript
1410
+ * const router = createRSCRouter<AppEnv>({
1411
+ * onError: (context) => {
1412
+ * // Log to error tracking service
1413
+ * errorTracker.capture({
1414
+ * error: context.error,
1415
+ * phase: context.phase,
1416
+ * url: context.request.url,
1417
+ * route: context.routeKey,
1418
+ * userId: context.env?.user?.id,
1419
+ * });
1420
+ *
1421
+ * // Log to console with context
1422
+ * console.error(`[${context.phase}] Error in ${context.routeKey}:`, {
1423
+ * message: context.error.message,
1424
+ * segment: context.segmentId,
1425
+ * duration: context.duration,
1426
+ * });
1427
+ * },
1428
+ * });
1429
+ * ```
1430
+ */
1431
+ export interface OnErrorContext<TEnv = any> {
1432
+ /**
1433
+ * The error that occurred
1434
+ */
1435
+ error: Error;
1436
+
1437
+ /**
1438
+ * Phase where the error occurred
1439
+ */
1440
+ phase: ErrorPhase;
1441
+
1442
+ /**
1443
+ * The original request
1444
+ */
1445
+ request: Request;
1446
+
1447
+ /**
1448
+ * Parsed URL from the request
1449
+ */
1450
+ url: URL;
1451
+
1452
+ /**
1453
+ * Request pathname
1454
+ */
1455
+ pathname: string;
1456
+
1457
+ /**
1458
+ * HTTP method
1459
+ */
1460
+ method: string;
1461
+
1462
+ /**
1463
+ * Matched route key (if available)
1464
+ * e.g., "shop.products.detail"
1465
+ */
1466
+ routeKey?: string;
1467
+
1468
+ /**
1469
+ * Route params (if available)
1470
+ * e.g., { slug: "headphones" }
1471
+ */
1472
+ params?: Record<string, string>;
1473
+
1474
+ /**
1475
+ * Segment ID where error occurred (if available)
1476
+ * e.g., "M1L0" for a layout, "M1R0" for a route
1477
+ */
1478
+ segmentId?: string;
1479
+
1480
+ /**
1481
+ * Segment type where error occurred (if available)
1482
+ */
1483
+ segmentType?: "layout" | "route" | "parallel" | "loader" | "middleware";
1484
+
1485
+ /**
1486
+ * Loader name (if error occurred in a loader)
1487
+ */
1488
+ loaderName?: string;
1489
+
1490
+ /**
1491
+ * Middleware name/id (if error occurred in middleware)
1492
+ */
1493
+ middlewareId?: string;
1494
+
1495
+ /**
1496
+ * Action ID (if error occurred during server action)
1497
+ * e.g., "src/actions.ts#addToCart"
1498
+ */
1499
+ actionId?: string;
1500
+
1501
+ /**
1502
+ * Environment/bindings (platform context)
1503
+ */
1504
+ env?: TEnv;
1505
+
1506
+ /**
1507
+ * Duration from request start to error (milliseconds)
1508
+ */
1509
+ duration?: number;
1510
+
1511
+ /**
1512
+ * Whether this is a partial/navigation request
1513
+ */
1514
+ isPartial?: boolean;
1515
+
1516
+ /**
1517
+ * Whether an error boundary caught the error
1518
+ * If true, the error was handled and a fallback UI was rendered
1519
+ */
1520
+ handledByBoundary?: boolean;
1521
+
1522
+ /**
1523
+ * Stack trace (if available)
1524
+ */
1525
+ stack?: string;
1526
+
1527
+ /**
1528
+ * Additional metadata specific to the error phase
1529
+ */
1530
+ metadata?: Record<string, unknown>;
1531
+ }
1532
+
1533
+ /**
1534
+ * Callback function for error handling
1535
+ *
1536
+ * Called whenever an error occurs during request handling.
1537
+ * The callback is for notification/logging purposes - it cannot
1538
+ * modify the error handling flow (use errorBoundary for that).
1539
+ *
1540
+ * @param context - Comprehensive error context
1541
+ *
1542
+ * @example
1543
+ * ```typescript
1544
+ * const onError: OnErrorCallback = (context) => {
1545
+ * // Send to error tracking service
1546
+ * Sentry.captureException(context.error, {
1547
+ * tags: {
1548
+ * phase: context.phase,
1549
+ * route: context.routeKey,
1550
+ * },
1551
+ * extra: {
1552
+ * url: context.url.toString(),
1553
+ * params: context.params,
1554
+ * duration: context.duration,
1555
+ * },
1556
+ * });
1557
+ * };
1558
+ * ```
1559
+ */
1560
+ export type OnErrorCallback<TEnv = any> = (context: OnErrorContext<TEnv>) => void | Promise<void>;
1561
+