@rangojs/router 0.0.0-experimental.10

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 (172) hide show
  1. package/CLAUDE.md +43 -0
  2. package/README.md +19 -0
  3. package/dist/bin/rango.js +227 -0
  4. package/dist/vite/index.js +3039 -0
  5. package/package.json +171 -0
  6. package/skills/caching/SKILL.md +191 -0
  7. package/skills/debug-manifest/SKILL.md +108 -0
  8. package/skills/document-cache/SKILL.md +180 -0
  9. package/skills/fonts/SKILL.md +165 -0
  10. package/skills/hooks/SKILL.md +442 -0
  11. package/skills/intercept/SKILL.md +190 -0
  12. package/skills/layout/SKILL.md +213 -0
  13. package/skills/links/SKILL.md +180 -0
  14. package/skills/loader/SKILL.md +246 -0
  15. package/skills/middleware/SKILL.md +202 -0
  16. package/skills/mime-routes/SKILL.md +124 -0
  17. package/skills/parallel/SKILL.md +228 -0
  18. package/skills/prerender/SKILL.md +283 -0
  19. package/skills/rango/SKILL.md +54 -0
  20. package/skills/response-routes/SKILL.md +358 -0
  21. package/skills/route/SKILL.md +173 -0
  22. package/skills/router-setup/SKILL.md +346 -0
  23. package/skills/tailwind/SKILL.md +129 -0
  24. package/skills/theme/SKILL.md +78 -0
  25. package/skills/typesafety/SKILL.md +394 -0
  26. package/src/__internal.ts +175 -0
  27. package/src/bin/rango.ts +24 -0
  28. package/src/browser/event-controller.ts +876 -0
  29. package/src/browser/index.ts +18 -0
  30. package/src/browser/link-interceptor.ts +121 -0
  31. package/src/browser/lru-cache.ts +69 -0
  32. package/src/browser/merge-segment-loaders.ts +126 -0
  33. package/src/browser/navigation-bridge.ts +913 -0
  34. package/src/browser/navigation-client.ts +165 -0
  35. package/src/browser/navigation-store.ts +823 -0
  36. package/src/browser/partial-update.ts +600 -0
  37. package/src/browser/react/Link.tsx +248 -0
  38. package/src/browser/react/NavigationProvider.tsx +346 -0
  39. package/src/browser/react/ScrollRestoration.tsx +94 -0
  40. package/src/browser/react/context.ts +53 -0
  41. package/src/browser/react/index.ts +52 -0
  42. package/src/browser/react/location-state-shared.ts +120 -0
  43. package/src/browser/react/location-state.ts +62 -0
  44. package/src/browser/react/mount-context.ts +32 -0
  45. package/src/browser/react/use-action.ts +240 -0
  46. package/src/browser/react/use-client-cache.ts +56 -0
  47. package/src/browser/react/use-handle.ts +203 -0
  48. package/src/browser/react/use-href.tsx +40 -0
  49. package/src/browser/react/use-link-status.ts +134 -0
  50. package/src/browser/react/use-mount.ts +31 -0
  51. package/src/browser/react/use-navigation.ts +140 -0
  52. package/src/browser/react/use-segments.ts +188 -0
  53. package/src/browser/request-controller.ts +164 -0
  54. package/src/browser/rsc-router.tsx +352 -0
  55. package/src/browser/scroll-restoration.ts +324 -0
  56. package/src/browser/segment-structure-assert.ts +67 -0
  57. package/src/browser/server-action-bridge.ts +762 -0
  58. package/src/browser/shallow.ts +35 -0
  59. package/src/browser/types.ts +478 -0
  60. package/src/build/generate-manifest.ts +377 -0
  61. package/src/build/generate-route-types.ts +828 -0
  62. package/src/build/index.ts +36 -0
  63. package/src/build/route-trie.ts +239 -0
  64. package/src/cache/cache-scope.ts +563 -0
  65. package/src/cache/cf/cf-cache-store.ts +428 -0
  66. package/src/cache/cf/index.ts +19 -0
  67. package/src/cache/document-cache.ts +340 -0
  68. package/src/cache/index.ts +58 -0
  69. package/src/cache/memory-segment-store.ts +150 -0
  70. package/src/cache/memory-store.ts +253 -0
  71. package/src/cache/types.ts +392 -0
  72. package/src/client.rsc.tsx +83 -0
  73. package/src/client.tsx +643 -0
  74. package/src/component-utils.ts +76 -0
  75. package/src/components/DefaultDocument.tsx +23 -0
  76. package/src/debug.ts +233 -0
  77. package/src/default-error-boundary.tsx +88 -0
  78. package/src/deps/browser.ts +8 -0
  79. package/src/deps/html-stream-client.ts +2 -0
  80. package/src/deps/html-stream-server.ts +2 -0
  81. package/src/deps/rsc.ts +10 -0
  82. package/src/deps/ssr.ts +2 -0
  83. package/src/errors.ts +295 -0
  84. package/src/handle.ts +130 -0
  85. package/src/handles/MetaTags.tsx +193 -0
  86. package/src/handles/index.ts +6 -0
  87. package/src/handles/meta.ts +247 -0
  88. package/src/host/cookie-handler.ts +159 -0
  89. package/src/host/errors.ts +97 -0
  90. package/src/host/index.ts +56 -0
  91. package/src/host/pattern-matcher.ts +214 -0
  92. package/src/host/router.ts +330 -0
  93. package/src/host/testing.ts +79 -0
  94. package/src/host/types.ts +138 -0
  95. package/src/host/utils.ts +25 -0
  96. package/src/href-client.ts +202 -0
  97. package/src/href-context.ts +33 -0
  98. package/src/index.rsc.ts +121 -0
  99. package/src/index.ts +165 -0
  100. package/src/loader.rsc.ts +207 -0
  101. package/src/loader.ts +47 -0
  102. package/src/network-error-thrower.tsx +21 -0
  103. package/src/outlet-context.ts +15 -0
  104. package/src/prerender/param-hash.ts +35 -0
  105. package/src/prerender/store.ts +40 -0
  106. package/src/prerender.ts +156 -0
  107. package/src/reverse.ts +267 -0
  108. package/src/root-error-boundary.tsx +277 -0
  109. package/src/route-content-wrapper.tsx +193 -0
  110. package/src/route-definition.ts +1431 -0
  111. package/src/route-map-builder.ts +242 -0
  112. package/src/route-types.ts +220 -0
  113. package/src/router/error-handling.ts +287 -0
  114. package/src/router/handler-context.ts +158 -0
  115. package/src/router/intercept-resolution.ts +387 -0
  116. package/src/router/loader-resolution.ts +327 -0
  117. package/src/router/manifest.ts +216 -0
  118. package/src/router/match-api.ts +621 -0
  119. package/src/router/match-context.ts +264 -0
  120. package/src/router/match-middleware/background-revalidation.ts +236 -0
  121. package/src/router/match-middleware/cache-lookup.ts +382 -0
  122. package/src/router/match-middleware/cache-store.ts +276 -0
  123. package/src/router/match-middleware/index.ts +81 -0
  124. package/src/router/match-middleware/intercept-resolution.ts +281 -0
  125. package/src/router/match-middleware/segment-resolution.ts +184 -0
  126. package/src/router/match-pipelines.ts +214 -0
  127. package/src/router/match-result.ts +213 -0
  128. package/src/router/metrics.ts +62 -0
  129. package/src/router/middleware.ts +791 -0
  130. package/src/router/pattern-matching.ts +407 -0
  131. package/src/router/revalidation.ts +190 -0
  132. package/src/router/router-context.ts +301 -0
  133. package/src/router/segment-resolution.ts +1315 -0
  134. package/src/router/trie-matching.ts +172 -0
  135. package/src/router/types.ts +163 -0
  136. package/src/router.gen.ts +6 -0
  137. package/src/router.ts +2423 -0
  138. package/src/rsc/handler.ts +1443 -0
  139. package/src/rsc/helpers.ts +64 -0
  140. package/src/rsc/index.ts +56 -0
  141. package/src/rsc/nonce.ts +18 -0
  142. package/src/rsc/types.ts +236 -0
  143. package/src/segment-system.tsx +442 -0
  144. package/src/server/context.ts +466 -0
  145. package/src/server/handle-store.ts +229 -0
  146. package/src/server/loader-registry.ts +174 -0
  147. package/src/server/request-context.ts +554 -0
  148. package/src/server/root-layout.tsx +10 -0
  149. package/src/server/tsconfig.json +14 -0
  150. package/src/server.ts +171 -0
  151. package/src/ssr/index.tsx +296 -0
  152. package/src/theme/ThemeProvider.tsx +291 -0
  153. package/src/theme/ThemeScript.tsx +61 -0
  154. package/src/theme/constants.ts +59 -0
  155. package/src/theme/index.ts +58 -0
  156. package/src/theme/theme-context.ts +70 -0
  157. package/src/theme/theme-script.ts +152 -0
  158. package/src/theme/types.ts +182 -0
  159. package/src/theme/use-theme.ts +44 -0
  160. package/src/types.ts +1757 -0
  161. package/src/urls.gen.ts +8 -0
  162. package/src/urls.ts +1282 -0
  163. package/src/use-loader.tsx +346 -0
  164. package/src/vite/expose-action-id.ts +344 -0
  165. package/src/vite/expose-handle-id.ts +209 -0
  166. package/src/vite/expose-loader-id.ts +426 -0
  167. package/src/vite/expose-location-state-id.ts +177 -0
  168. package/src/vite/expose-prerender-handler-id.ts +429 -0
  169. package/src/vite/index.ts +2068 -0
  170. package/src/vite/package-resolution.ts +125 -0
  171. package/src/vite/version.d.ts +12 -0
  172. package/src/vite/virtual-entries.ts +114 -0
package/src/urls.ts ADDED
@@ -0,0 +1,1282 @@
1
+ /**
2
+ * Django-inspired URL patterns for @rangojs/router
3
+ *
4
+ * This module provides `urls()` and `path()` for defining routes with
5
+ * URL patterns visible at the definition site.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * // urls/blog.ts
10
+ * export const blogPatterns = urls(({ path, layout, loader }) => [
11
+ * layout(BlogLayout, () => [
12
+ * path("/", BlogIndex, { name: "index" }),
13
+ * path("/:slug", BlogPost, { name: "post" }, () => [
14
+ * loader(PostLoader),
15
+ * ]),
16
+ * ]),
17
+ * ]);
18
+ *
19
+ * // urls/index.ts
20
+ * export const urlpatterns = urls(({ path, layout, include }) => [
21
+ * layout(RootLayout, () => [
22
+ * path("/", HomePage, { name: "home" }),
23
+ * include("/blog", blogPatterns, { name: "blog" }),
24
+ * ]),
25
+ * ]);
26
+ * ```
27
+ */
28
+ import type { ReactNode } from "react";
29
+ import type {
30
+ DefaultEnv,
31
+ ErrorBoundaryHandler,
32
+ ExtractParams,
33
+ Handler,
34
+ HandlerContext,
35
+ LoaderDefinition,
36
+ MiddlewareFn,
37
+ NotFoundBoundaryHandler,
38
+ PartialCacheOptions,
39
+ RouterEnv,
40
+ ShouldRevalidateFn,
41
+ TrailingSlashMode,
42
+ } from "./types.js";
43
+ import type { CookieOptions } from "./router/middleware.js";
44
+ import type {
45
+ AllUseItems,
46
+ LayoutItem,
47
+ TypedLayoutItem,
48
+ RouteItem,
49
+ TypedRouteItem,
50
+ ParallelItem,
51
+ InterceptItem,
52
+ MiddlewareItem,
53
+ RevalidateItem,
54
+ LoaderItem,
55
+ LoadingItem,
56
+ ErrorBoundaryItem,
57
+ NotFoundBoundaryItem,
58
+ LayoutUseItem,
59
+ RouteUseItem,
60
+ ResponseRouteUseItem,
61
+ ParallelUseItem,
62
+ InterceptUseItem,
63
+ LoaderUseItem,
64
+ WhenItem,
65
+ CacheItem,
66
+ TypedCacheItem,
67
+ IncludeItem,
68
+ TypedIncludeItem,
69
+ IncludeBrand,
70
+ UrlPatternsBrand,
71
+ } from "./route-types.js";
72
+ import {
73
+ getContext,
74
+ runWithPrefixes,
75
+ getUrlPrefix,
76
+ getNamePrefix,
77
+ type EntryData,
78
+ type InterceptEntry,
79
+ type InterceptWhenFn,
80
+ } from "./server/context";
81
+ import { invariant } from "./errors";
82
+ import {
83
+ isPrerenderHandler,
84
+ type PrerenderHandlerDefinition,
85
+ } from "./prerender.js";
86
+
87
+ // ============================================================================
88
+ // Response Route Symbol and Types
89
+ // ============================================================================
90
+
91
+ /**
92
+ * Symbol marking a route as a response route (non-RSC).
93
+ * Stored on PathOptions and UrlPatterns to signal the trie to short-circuit.
94
+ */
95
+ export const RESPONSE_TYPE: unique symbol = Symbol.for(
96
+ "rangojs.responseType",
97
+ ) as any;
98
+
99
+ /**
100
+ * Handler that must return Response (not ReactNode).
101
+ * Used by path.image(), path.stream(), path.any() (binary/streaming data).
102
+ */
103
+ export type ResponseHandler<TParams = Record<string, string>, TEnv = any> = (
104
+ ctx: ResponseHandlerContext<TParams, TEnv>,
105
+ ) => Response | Promise<Response>;
106
+
107
+ /**
108
+ * JSON-serializable value type for auto-wrap support.
109
+ */
110
+ export type JsonValue =
111
+ | string
112
+ | number
113
+ | boolean
114
+ | null
115
+ | JsonValue[]
116
+ | { [key: string]: JsonValue };
117
+
118
+ /**
119
+ * Handler for JSON response routes.
120
+ * Can return a plain JSON-serializable value (auto-wrapped) or Response (pass-through).
121
+ */
122
+ export type JsonResponseHandler<
123
+ TParams = Record<string, string>,
124
+ TEnv = any,
125
+ > = (
126
+ ctx: ResponseHandlerContext<TParams, TEnv>,
127
+ ) => JsonValue | Response | Promise<JsonValue | Response>;
128
+
129
+ /**
130
+ * Handler for text-based response routes (text, html, xml).
131
+ * Can return a string (auto-wrapped) or Response (pass-through).
132
+ */
133
+ export type TextResponseHandler<
134
+ TParams = Record<string, string>,
135
+ TEnv = any,
136
+ > = (
137
+ ctx: ResponseHandlerContext<TParams, TEnv>,
138
+ ) => string | Response | Promise<string | Response>;
139
+
140
+ /**
141
+ * Lighter handler context for response routes.
142
+ * No ctx.use() (no loaders). Supports setting response headers and cookies
143
+ * without constructing a full Response object.
144
+ */
145
+ export interface ResponseHandlerContext<
146
+ TParams = Record<string, string>,
147
+ TEnv = any,
148
+ > {
149
+ request: Request;
150
+ params: TParams;
151
+ /** @internal Phantom property for params type invariance. Prevents mounting handlers on wrong routes. */
152
+ readonly _paramCheck?: (params: TParams) => TParams;
153
+ /** Platform bindings (DB, KV, secrets, etc.) extracted from RouterEnv. */
154
+ env: TEnv extends RouterEnv<infer B, any> ? B : {};
155
+ /** Query parameters from the URL (system params like `_rsc*` are filtered). */
156
+ searchParams: URLSearchParams;
157
+ /** The full URL object (with system params filtered). */
158
+ url: URL;
159
+ /** The pathname portion of the request URL. */
160
+ pathname: string;
161
+ reverse: (name: string, params?: Record<string, string>) => string;
162
+ /** Read a variable set by middleware via ctx.set(key, value). */
163
+ get: (key: string) => unknown;
164
+ /** Set a response header. Merged into the auto-wrapped or pass-through Response. */
165
+ header: (name: string, value: string) => void;
166
+ /** Set a cookie on the response. */
167
+ setCookie: (name: string, value: string, options?: CookieOptions) => void;
168
+ }
169
+
170
+
171
+ // ============================================================================
172
+ // Types
173
+ // ============================================================================
174
+
175
+ /**
176
+ * Sentinel type for unnamed routes.
177
+ * Using a branded string instead of `never` prevents TypeScript from
178
+ * widening array type inference when mixing named and unnamed routes.
179
+ */
180
+ export type UnnamedRoute = "$unnamed";
181
+
182
+ /**
183
+ * Options for path() function
184
+ */
185
+ export interface PathOptions<TName extends string = string> {
186
+ /** Route name for href() lookups */
187
+ name?: TName;
188
+ /** Trailing slash behavior: "never" (redirect /path/ to /path), "always" (redirect /path to /path/), "ignore" (match both) */
189
+ trailingSlash?: TrailingSlashMode;
190
+ /** Response type marker (set by path.json(), etc.) */
191
+ [RESPONSE_TYPE]?: string;
192
+ }
193
+
194
+ /**
195
+ * Internal representation of a URL pattern definition
196
+ */
197
+ export interface PathDefinition {
198
+ pattern: string;
199
+ name?: string;
200
+ handler: ReactNode | Handler<any, any, any>;
201
+ use?: RouteUseItem[];
202
+ }
203
+
204
+ /**
205
+ * Result of urls() - contains the route definitions
206
+ */
207
+ export interface UrlPatterns<
208
+ TEnv = any,
209
+ TRoutes extends Record<string, string> = Record<string, string>,
210
+ TResponses extends Record<string, unknown> = Record<string, unknown>,
211
+ > {
212
+ /** Internal: route definitions */
213
+ readonly definitions: PathDefinition[];
214
+ /** Internal: compiled handler function */
215
+ readonly handler: () => AllUseItems[];
216
+ /** Internal: trailing slash config per route name */
217
+ readonly trailingSlash: Record<string, TrailingSlashMode>;
218
+ /** Brand for type checking */
219
+ readonly [UrlPatternsBrand]: void;
220
+ /** Environment type brand (phantom) */
221
+ readonly _env?: TEnv;
222
+ /** Routes type brand (phantom) - carries route name -> pattern mapping */
223
+ readonly _routes?: TRoutes;
224
+ /** Responses type brand (phantom) - carries route name -> response data type mapping */
225
+ readonly _responses?: TResponses;
226
+ }
227
+
228
+ /**
229
+ * Options for include()
230
+ */
231
+ export interface IncludeOptions<TNamePrefix extends string = string> {
232
+ /** Name prefix for all routes in this pattern set */
233
+ name?: TNamePrefix;
234
+ }
235
+
236
+ // ============================================================================
237
+ // Route Type Extraction Utilities
238
+ // ============================================================================
239
+
240
+ /**
241
+ * Prefix route names with a given prefix (e.g., "blog" + "post" = "blog.post")
242
+ *
243
+ * Filters out plain `string` index signatures to prevent dynamically-generated
244
+ * routes from poisoning the route map. When TypeScript encounters very large
245
+ * route sets (5000+ routes via Array.from), it may give up computing specific
246
+ * types and fall back to Record<string, string>. Without filtering, PrefixRoutes
247
+ * would map `string` to `${prefix}.${string}`, creating an index signature that
248
+ * accepts ANY prefixed name and defeats type-safe route checking.
249
+ *
250
+ * Uses `string extends K` (conservative filter):
251
+ * - Drops `string` keys (TypeScript fallback) -> prevents `[x: `site.${string}`]`
252
+ * - Keeps template literal patterns like `item${number}` from Array.from loops,
253
+ * which are imprecise but still allow writing paths like `/shop/product/1`
254
+ *
255
+ * A more aggressive alternative (`{} extends Record<K, 1>`) would also drop
256
+ * template literal patterns. We chose conservative because loop-generated routes
257
+ * with `${number}` patterns still provide some value: they don't appear in
258
+ * named-routes.gen.ts or IDE autocomplete, but they do let you manually write
259
+ * valid paths without type errors.
260
+ */
261
+ type PrefixRoutes<
262
+ TRoutes extends Record<string, string>,
263
+ TPrefix extends string,
264
+ > = TPrefix extends ""
265
+ ? TRoutes
266
+ : {
267
+ [K in keyof TRoutes as K extends string
268
+ ? string extends K
269
+ ? never
270
+ : `${TPrefix}.${K}`
271
+ : never]: TRoutes[K];
272
+ };
273
+
274
+ /**
275
+ * Prefix route patterns with a URL prefix (e.g., "/blog" + "/:slug" = "/blog/:slug")
276
+ */
277
+ type PrefixPatterns<
278
+ TRoutes extends Record<string, string>,
279
+ TUrlPrefix extends string,
280
+ > = {
281
+ [K in keyof TRoutes]: TRoutes[K] extends string
282
+ ? `${TUrlPrefix}${TRoutes[K]}`
283
+ : TRoutes[K];
284
+ };
285
+
286
+ /**
287
+ * Depth counter for limiting recursion (max 40 levels)
288
+ * Supports up to 40 sibling items at any level of a urls() call
289
+ * Note: Higher values hit TypeScript's internal recursion limits
290
+ */
291
+ type Depth = [
292
+ never,
293
+ 0,
294
+ 1,
295
+ 2,
296
+ 3,
297
+ 4,
298
+ 5,
299
+ 6,
300
+ 7,
301
+ 8,
302
+ 9,
303
+ 10,
304
+ 11,
305
+ 12,
306
+ 13,
307
+ 14,
308
+ 15,
309
+ 16,
310
+ 17,
311
+ 18,
312
+ 19,
313
+ 20,
314
+ 21,
315
+ 22,
316
+ 23,
317
+ 24,
318
+ 25,
319
+ 26,
320
+ 27,
321
+ 28,
322
+ 29,
323
+ 30,
324
+ 31,
325
+ 32,
326
+ 33,
327
+ 34,
328
+ 35,
329
+ 36,
330
+ 37,
331
+ 38,
332
+ 39,
333
+ ];
334
+
335
+ /**
336
+ * Force TypeScript to eagerly evaluate a type.
337
+ * This helps with interface extension by creating a "concrete" object type.
338
+ */
339
+ type Simplify<T> =
340
+ T extends Record<string, string> ? { [K in keyof T]: T[K] } : T;
341
+
342
+ /**
343
+ * Convert a union type to an intersection type.
344
+ * Used to combine route maps from multiple siblings without recursive tuple processing.
345
+ */
346
+ type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
347
+ k: infer I,
348
+ ) => void
349
+ ? I
350
+ : never;
351
+
352
+ /**
353
+ * Extract routes from a single item (path, include, layout, cache with children)
354
+ * D is the current depth level for nested layouts/caches
355
+ */
356
+ type ExtractRoutesFromItem<T, D extends number = 40> = [D] extends [never]
357
+ ? {} // Max depth reached, stop recursion
358
+ : // TypedRouteItem: extract name -> pattern (exclude unnamed routes)
359
+ T extends TypedRouteItem<infer TName, infer TPattern>
360
+ ? TName extends string
361
+ ? TName extends UnnamedRoute
362
+ ? {} // Exclude unnamed routes from type map
363
+ : { [K in TName]: TPattern }
364
+ : {}
365
+ : // TypedIncludeItem: extract prefixed routes (both name and URL prefix)
366
+ T extends TypedIncludeItem<
367
+ infer TRoutes,
368
+ infer TNamePrefix,
369
+ infer TUrlPrefix
370
+ >
371
+ ? TNamePrefix extends string
372
+ ? TUrlPrefix extends string
373
+ ? PrefixRoutes<PrefixPatterns<TRoutes, TUrlPrefix>, TNamePrefix>
374
+ : PrefixRoutes<TRoutes, TNamePrefix>
375
+ : TUrlPrefix extends string
376
+ ? PrefixPatterns<TRoutes, TUrlPrefix>
377
+ : TRoutes
378
+ : // TypedLayoutItem: extract child routes from phantom type
379
+ T extends TypedLayoutItem<infer TChildRoutes>
380
+ ? TChildRoutes
381
+ : // TypedCacheItem: extract child routes from phantom type
382
+ T extends TypedCacheItem<infer TChildRoutes>
383
+ ? TChildRoutes
384
+ : // Fallback (won't extract routes)
385
+ {};
386
+
387
+ /**
388
+ * Extract routes from an array of items using mapped types.
389
+ * Uses UnionToIntersection to combine routes without recursive tuple processing,
390
+ * removing the sibling limit that was caused by TypeScript recursion limits.
391
+ * D is passed to ExtractRoutesFromItem for nested depth tracking.
392
+ */
393
+ type ExtractRoutesFromItems<
394
+ T extends readonly any[],
395
+ D extends number = 40,
396
+ > = T extends readonly any[]
397
+ ? UnionToIntersection<
398
+ { [K in keyof T]: ExtractRoutesFromItem<T[K], D> }[number]
399
+ > extends infer R
400
+ ? R extends Record<string, string>
401
+ ? R
402
+ : {}
403
+ : {}
404
+ : {};
405
+
406
+ /**
407
+ * Main utility: extract route map from urls() callback return type
408
+ * Uses mapped types for sibling processing (no sibling limit).
409
+ * Uses Simplify to force eager evaluation for interface extension compatibility.
410
+ */
411
+ export type ExtractRoutes<T extends readonly any[]> = ExtractRoutesFromItems<
412
+ T,
413
+ 40
414
+ >;
415
+
416
+ // ============================================================================
417
+ // Response Type Extraction Utilities
418
+ // ============================================================================
419
+
420
+ /**
421
+ * Prefix keys of a Record<string, unknown> with a dot-separated prefix.
422
+ * Used for response type maps through include().
423
+ * Same index signature filter as PrefixRoutes (see comment there).
424
+ */
425
+ type PrefixKeys<
426
+ T extends Record<string, unknown>,
427
+ TPrefix extends string,
428
+ > = TPrefix extends ""
429
+ ? T
430
+ : {
431
+ [K in keyof T as K extends string
432
+ ? string extends K
433
+ ? never
434
+ : `${TPrefix}.${K}`
435
+ : never]: T[K];
436
+ };
437
+
438
+ /**
439
+ * Extract response data types from a single item.
440
+ * Parallel to ExtractRoutesFromItem but extracts name -> TData mapping.
441
+ */
442
+ type ExtractResponsesFromItem<T, D extends number = 40> = [D] extends [never]
443
+ ? {}
444
+ : T extends TypedRouteItem<infer TName, any, infer TData>
445
+ ? TName extends string
446
+ ? TName extends UnnamedRoute
447
+ ? {}
448
+ : { [K in TName]: TData }
449
+ : {}
450
+ : T extends TypedIncludeItem<any, infer TNamePrefix, any, infer TResponses>
451
+ ? TNamePrefix extends string
452
+ ? TResponses extends Record<string, unknown>
453
+ ? PrefixKeys<TResponses, TNamePrefix>
454
+ : {}
455
+ : TResponses extends Record<string, unknown>
456
+ ? TResponses
457
+ : {}
458
+ : T extends TypedLayoutItem<any, infer TChildResponses>
459
+ ? TChildResponses extends Record<string, unknown>
460
+ ? TChildResponses
461
+ : {}
462
+ : T extends TypedCacheItem<any, infer TChildResponses>
463
+ ? TChildResponses extends Record<string, unknown>
464
+ ? TChildResponses
465
+ : {}
466
+ : {};
467
+
468
+ /**
469
+ * Extract responses from an array of items using mapped types.
470
+ * Parallel to ExtractRoutesFromItems.
471
+ */
472
+ type ExtractResponsesFromItems<
473
+ T extends readonly any[],
474
+ D extends number = 40,
475
+ > = T extends readonly any[]
476
+ ? UnionToIntersection<
477
+ { [K in keyof T]: ExtractResponsesFromItem<T[K], D> }[number]
478
+ > extends infer R
479
+ ? R extends Record<string, unknown>
480
+ ? R
481
+ : {}
482
+ : {}
483
+ : {};
484
+
485
+ /**
486
+ * Main utility: extract response data type map from urls() callback return type.
487
+ * Parallel to ExtractRoutes.
488
+ */
489
+ export type ExtractResponses<T extends readonly any[]> =
490
+ ExtractResponsesFromItems<T, 40>;
491
+
492
+ // ============================================================================
493
+ // Path Helpers Type
494
+ // ============================================================================
495
+
496
+ /**
497
+ * Helpers provided by urls()
498
+ */
499
+ /**
500
+ * Base path function signature for defining routes with URL patterns.
501
+ */
502
+ export type PathFn<TEnv> = <
503
+ const TPattern extends string,
504
+ const TName extends string = UnnamedRoute,
505
+ TParams = ExtractParams<TPattern>,
506
+ >(
507
+ pattern: TPattern,
508
+ handler:
509
+ | ReactNode
510
+ | ((ctx: HandlerContext<TParams, TEnv>) => ReactNode | Promise<ReactNode> | Response | Promise<Response>)
511
+ | PrerenderHandlerDefinition<TParams>,
512
+ optionsOrUse?: PathOptions<TName> | (() => RouteUseItem[]),
513
+ use?: () => RouteUseItem[],
514
+ // Generic handler bypass: when handler uses index-signature params
515
+ // (e.g. Handler<Record<string, any>>), skip the biconditional.
516
+ // `string extends keyof TParams` is true for index signatures,
517
+ // false for concrete params ({id: string}) and empty ({}).
518
+ ) => string extends keyof TParams
519
+ ? TypedRouteItem<TName, TPattern>
520
+ : ExtractParams<TPattern> extends TParams
521
+ ? TParams extends ExtractParams<TPattern>
522
+ ? TypedRouteItem<TName, TPattern>
523
+ : { __error: `Handler params do not match pattern "${TPattern}"` }
524
+ : { __error: `Handler params do not match pattern "${TPattern}"` };
525
+
526
+ /**
527
+ * Path function for response routes that must return Response (image, stream, any).
528
+ * Handler must return Response, not ReactNode. Uses lighter ResponseHandlerContext.
529
+ * Use items restricted to middleware() and cache() only.
530
+ */
531
+ export type ResponsePathFn<TEnv> = <
532
+ const TPattern extends string,
533
+ const TName extends string = UnnamedRoute,
534
+ >(
535
+ pattern: TPattern,
536
+ handler: ResponseHandler<ExtractParams<TPattern>, TEnv>,
537
+ optionsOrUse?: PathOptions<TName> | (() => ResponseRouteUseItem[]),
538
+ use?: () => ResponseRouteUseItem[],
539
+ ) => TypedRouteItem<TName, TPattern>;
540
+
541
+ /**
542
+ * Path function for JSON response routes (path.json()).
543
+ * Handler can return plain JSON-serializable values or Response.
544
+ * TData is inferred from the handler's return type (excluding Response/Promise wrappers).
545
+ */
546
+ export type JsonResponsePathFn<TEnv> = <
547
+ const TPattern extends string,
548
+ const TName extends string = UnnamedRoute,
549
+ TData = unknown,
550
+ >(
551
+ pattern: TPattern,
552
+ handler: (
553
+ ctx: ResponseHandlerContext<ExtractParams<TPattern>, TEnv>,
554
+ ) => TData | Response | Promise<TData | Response>,
555
+ optionsOrUse?: PathOptions<TName> | (() => ResponseRouteUseItem[]),
556
+ use?: () => ResponseRouteUseItem[],
557
+ ) => TypedRouteItem<TName, TPattern, TData>;
558
+
559
+ /**
560
+ * Path function for text-based response routes (path.text(), path.html(), path.xml()).
561
+ * Handler can return a string or Response. TData is always `string`.
562
+ */
563
+ export type TextResponsePathFn<TEnv> = <
564
+ const TPattern extends string,
565
+ const TName extends string = UnnamedRoute,
566
+ >(
567
+ pattern: TPattern,
568
+ handler: TextResponseHandler<ExtractParams<TPattern>, TEnv>,
569
+ optionsOrUse?: PathOptions<TName> | (() => ResponseRouteUseItem[]),
570
+ use?: () => ResponseRouteUseItem[],
571
+ ) => TypedRouteItem<TName, TPattern, string>;
572
+
573
+ /**
574
+ * Base include function signature.
575
+ */
576
+ export type IncludeFn<TEnv> = <
577
+ TRoutes extends Record<string, string>,
578
+ const TUrlPrefix extends string,
579
+ const TNamePrefix extends string = never,
580
+ TResponses extends Record<string, unknown> = Record<string, unknown>,
581
+ >(
582
+ prefix: TUrlPrefix,
583
+ patterns: UrlPatterns<TEnv, TRoutes, TResponses>,
584
+ options?: IncludeOptions<TNamePrefix>,
585
+ ) => TypedIncludeItem<TRoutes, TNamePrefix, TUrlPrefix, TResponses>;
586
+
587
+ export type PathHelpers<TEnv> = {
588
+ /**
589
+ * Define a route with URL pattern at definition site
590
+ *
591
+ * @example
592
+ * ```typescript
593
+ * // Pattern and component only
594
+ * path("/about", AboutPage)
595
+ *
596
+ * // With options
597
+ * path("/:slug", PostPage, { name: "post" })
598
+ *
599
+ * // With children (loaders, middleware, etc.)
600
+ * path("/:slug", PostPage, { name: "post" }, () => [
601
+ * loader(PostLoader),
602
+ * ])
603
+ * ```
604
+ */
605
+ path: PathFn<TEnv> & {
606
+ json: JsonResponsePathFn<TEnv>;
607
+ text: TextResponsePathFn<TEnv>;
608
+ html: TextResponsePathFn<TEnv>;
609
+ xml: TextResponsePathFn<TEnv>;
610
+ md: TextResponsePathFn<TEnv>;
611
+ image: ResponsePathFn<TEnv>;
612
+ stream: ResponsePathFn<TEnv>;
613
+ any: ResponsePathFn<TEnv>;
614
+ };
615
+
616
+ /**
617
+ * Define a layout that wraps child routes
618
+ */
619
+ layout: {
620
+ (component: ReactNode | Handler<any, any, TEnv>): TypedLayoutItem<{}, {}>;
621
+ <const TChildren extends readonly LayoutUseItem[]>(
622
+ component: ReactNode | Handler<any, any, TEnv>,
623
+ use: () => TChildren,
624
+ ): TypedLayoutItem<ExtractRoutes<TChildren>, ExtractResponses<TChildren>>;
625
+ };
626
+
627
+ /**
628
+ * Include nested URL patterns with optional name prefix
629
+ *
630
+ * ```typescript
631
+ * // Without name - routes keep local names
632
+ * include("/blog", blogPatterns)
633
+ *
634
+ * // With name - routes are prefixed (e.g., "index" → "blog.index")
635
+ * include("/blog", blogPatterns, { name: "blog" })
636
+ * ```
637
+ */
638
+ include: IncludeFn<TEnv>;
639
+
640
+ /**
641
+ * Define parallel routes that render simultaneously in named slots
642
+ */
643
+ parallel: <
644
+ TSlots extends Record<`@${string}`, Handler<any, any, TEnv> | ReactNode>,
645
+ >(
646
+ slots: TSlots,
647
+ use?: () => ParallelUseItem[],
648
+ ) => ParallelItem;
649
+
650
+ /**
651
+ * Define an intercepting route for soft navigation
652
+ * Note: routeName must match a named path() in this urlpatterns
653
+ */
654
+ intercept: (
655
+ slotName: `@${string}`,
656
+ routeName: string,
657
+ handler: ReactNode | Handler<any, any, TEnv>,
658
+ use?: () => InterceptUseItem[],
659
+ ) => InterceptItem;
660
+
661
+ /**
662
+ * Attach middleware to the current route/layout
663
+ */
664
+ middleware: (...fns: MiddlewareFn<TEnv>[]) => MiddlewareItem;
665
+
666
+ /**
667
+ * Control when a segment should revalidate during navigation
668
+ */
669
+ revalidate: (fn: ShouldRevalidateFn<any, TEnv>) => RevalidateItem;
670
+
671
+ /**
672
+ * Attach a data loader to the current route/layout
673
+ */
674
+ loader: <TData>(
675
+ loaderDef: LoaderDefinition<TData>,
676
+ use?: () => LoaderUseItem[],
677
+ ) => LoaderItem;
678
+
679
+ /**
680
+ * Attach a loading component to the current route/layout
681
+ */
682
+ loading: (component: ReactNode, options?: { ssr?: boolean }) => LoadingItem;
683
+
684
+ /**
685
+ * Attach an error boundary to catch errors in this segment
686
+ */
687
+ errorBoundary: (
688
+ fallback: ReactNode | ErrorBoundaryHandler,
689
+ ) => ErrorBoundaryItem;
690
+
691
+ /**
692
+ * Attach a not-found boundary to handle notFound() calls
693
+ */
694
+ notFoundBoundary: (
695
+ fallback: ReactNode | NotFoundBoundaryHandler,
696
+ ) => NotFoundBoundaryItem;
697
+
698
+ /**
699
+ * Define a condition for when an intercept should activate
700
+ */
701
+ when: (fn: InterceptWhenFn) => WhenItem;
702
+
703
+ /**
704
+ * Define cache configuration for segments
705
+ */
706
+ cache: {
707
+ (): TypedCacheItem<{}, {}>;
708
+ <const TChildren extends readonly AllUseItems[]>(
709
+ children: () => TChildren,
710
+ ): TypedCacheItem<ExtractRoutes<TChildren>, ExtractResponses<TChildren>>;
711
+ (options: PartialCacheOptions | false): TypedCacheItem<{}, {}>;
712
+ <const TChildren extends readonly AllUseItems[]>(
713
+ options: PartialCacheOptions | false,
714
+ use: () => TChildren,
715
+ ): TypedCacheItem<ExtractRoutes<TChildren>, ExtractResponses<TChildren>>;
716
+ };
717
+ };
718
+
719
+ // ============================================================================
720
+ // Helper Implementations
721
+ // ============================================================================
722
+
723
+ /**
724
+ * Check if a value is a valid use item
725
+ */
726
+ const isValidUseItem = (item: any): item is AllUseItems | undefined | null => {
727
+ return (
728
+ typeof item === "undefined" ||
729
+ item === null ||
730
+ (item &&
731
+ typeof item === "object" &&
732
+ "type" in item &&
733
+ [
734
+ "layout",
735
+ "route",
736
+ "middleware",
737
+ "revalidate",
738
+ "parallel",
739
+ "intercept",
740
+ "loader",
741
+ "loading",
742
+ "errorBoundary",
743
+ "notFoundBoundary",
744
+ "when",
745
+ "cache",
746
+ "include",
747
+ ].includes(item.type))
748
+ );
749
+ };
750
+
751
+ /**
752
+ * Apply URL prefix to a pattern
753
+ * Handles edge cases like "/" patterns and double slashes
754
+ */
755
+ function applyUrlPrefix(prefix: string, pattern: string): string {
756
+ if (!prefix) return pattern;
757
+ if (pattern === "/") return prefix;
758
+ if (prefix.endsWith("/") && pattern.startsWith("/")) {
759
+ return prefix + pattern.slice(1);
760
+ }
761
+ return prefix + pattern;
762
+ }
763
+
764
+ /**
765
+ * Apply name prefix to a route name
766
+ */
767
+ function applyNamePrefix(prefix: string | undefined, name: string): string {
768
+ if (!prefix) return name;
769
+ return `${prefix}.${name}`;
770
+ }
771
+
772
+ /**
773
+ * Create path() helper
774
+ *
775
+ * The path() function is the key new feature - it combines URL pattern
776
+ * with handler at the definition site.
777
+ */
778
+ /**
779
+ * Resolve response type from path options (set by path.json(), path.text(), etc.)
780
+ */
781
+ function resolveResponseType(
782
+ options: PathOptions | undefined,
783
+ ): string | undefined {
784
+ return options?.[RESPONSE_TYPE];
785
+ }
786
+
787
+ function createPathHelper<TEnv>(): PathFn<TEnv> {
788
+ return ((
789
+ pattern: string,
790
+ handler: ReactNode | Handler<any, any, TEnv>,
791
+ optionsOrUse?: PathOptions | (() => RouteUseItem[]),
792
+ maybeUse?: () => RouteUseItem[],
793
+ ): RouteItem => {
794
+ const store = getContext();
795
+ const ctx = store.getStore();
796
+ if (!ctx) throw new Error("path() must be called inside urls()");
797
+
798
+ // Determine options and use based on argument types
799
+ let options: PathOptions | undefined;
800
+ let use: (() => RouteUseItem[]) | undefined;
801
+
802
+ if (typeof optionsOrUse === "function") {
803
+ // path(pattern, handler, use)
804
+ use = optionsOrUse as () => RouteUseItem[];
805
+ } else if (typeof optionsOrUse === "object") {
806
+ // path(pattern, handler, options) or path(pattern, handler, options, use)
807
+ options = optionsOrUse as PathOptions;
808
+ use = maybeUse;
809
+ }
810
+
811
+ // Get prefixes from context (set by include())
812
+ const urlPrefix = getUrlPrefix();
813
+ const namePrefix = getNamePrefix();
814
+
815
+ // Apply URL prefix to pattern
816
+ const prefixedPattern = applyUrlPrefix(urlPrefix, pattern);
817
+
818
+ // Generate route name - use provided name or generate from pattern
819
+ const localName =
820
+ options?.name || `$path_${pattern.replace(/[/:*?]/g, "_")}`;
821
+ // Apply name prefix if set (from include())
822
+ const routeName = applyNamePrefix(namePrefix, localName);
823
+
824
+ const namespace = `${ctx.namespace}.${store.getNextIndex("route")}.${routeName}`;
825
+
826
+ // Per-request pruning: skip registration for routes that won't be rendered.
827
+ // forRoute is set by loadManifest() to the matched route name. During
828
+ // evaluateLazyEntry() (route matching), forRoute is unset so all routes
829
+ // register normally. We still increment counters to keep shortCodes stable
830
+ // across different routes (needed for segment reconciliation on navigation).
831
+ //
832
+ // include() does not need its own forRoute pruning. include() creates lazy
833
+ // entries that defer handler execution until route matching. When the lazy
834
+ // handler eventually runs inside loadManifest(), this path() check already
835
+ // covers all routes defined inside the include.
836
+ if (ctx.forRoute && routeName !== ctx.forRoute) {
837
+ store.getShortCode("route");
838
+ return { type: "route" } as RouteItem;
839
+ }
840
+
841
+ // Ensure handler is always a function (wrap ReactNode or extract from prerender def)
842
+ const wrappedHandler: Handler<any, any, TEnv> =
843
+ typeof handler === "function"
844
+ ? (handler as Handler<any, any, TEnv>)
845
+ : isPrerenderHandler(handler)
846
+ ? (handler.handler as Handler<any, any, TEnv>)
847
+ : () => handler;
848
+
849
+ const entry = {
850
+ id: namespace,
851
+ shortCode: store.getShortCode("route"),
852
+ type: "route" as const,
853
+ parent: ctx.parent,
854
+ handler: wrappedHandler,
855
+ // Store the PREFIXED pattern for route matching
856
+ pattern: prefixedPattern,
857
+ loading: undefined,
858
+ middleware: [],
859
+ revalidate: [],
860
+ errorBoundary: [],
861
+ notFoundBoundary: [],
862
+ layout: [],
863
+ parallel: [],
864
+ intercept: [],
865
+ loader: [],
866
+ ...(urlPrefix ? { mountPath: urlPrefix } : {}),
867
+ ...(isPrerenderHandler(handler)
868
+ ? {
869
+ isPrerender: true as const,
870
+ prerenderDef: handler as PrerenderHandlerDefinition,
871
+ }
872
+ : {}),
873
+ ...(resolveResponseType(options)
874
+ ? { responseType: resolveResponseType(options) }
875
+ : {}),
876
+ };
877
+
878
+ // Check for duplicate route names (TypeScript should catch this, but runtime check too)
879
+ invariant(
880
+ ctx.manifest.get(routeName) === undefined,
881
+ `Duplicate route name: ${routeName} at ${namespace}`,
882
+ );
883
+
884
+ // Register route entry with prefixed name
885
+ ctx.manifest.set(routeName, entry);
886
+
887
+ // Also store pattern in a separate map for URL generation
888
+ if (ctx.patterns) {
889
+ ctx.patterns.set(routeName, prefixedPattern);
890
+ }
891
+
892
+ // Store pattern grouped by URL prefix for separate entry creation
893
+ if (ctx.patternsByPrefix) {
894
+ const urlPrefix = getUrlPrefix() || "";
895
+ if (!ctx.patternsByPrefix.has(urlPrefix)) {
896
+ ctx.patternsByPrefix.set(urlPrefix, new Map());
897
+ }
898
+ ctx.patternsByPrefix.get(urlPrefix)!.set(routeName, prefixedPattern);
899
+ }
900
+
901
+ // Store trailing slash config if specified
902
+ if (options?.trailingSlash && ctx.trailingSlash) {
903
+ ctx.trailingSlash.set(routeName, options.trailingSlash);
904
+ }
905
+
906
+ // Run use callback if provided
907
+ if (use && typeof use === "function") {
908
+ const result = store.run(namespace, entry, use);
909
+ invariant(
910
+ Array.isArray(result) && result.every((item) => isValidUseItem(item)),
911
+ `path() use() callback must return an array of use items [${namespace}]`,
912
+ );
913
+ return { name: namespace, type: "route", uses: result } as RouteItem;
914
+ }
915
+
916
+ return { name: namespace, type: "route" } as RouteItem;
917
+ }) as PathFn<TEnv>;
918
+ }
919
+
920
+ /**
921
+ * Attach response type tag methods (.json, .text, .html, .xml, .md, .image, .stream, .any) to a path helper.
922
+ * Each tag wraps the original path() call with the RESPONSE_TYPE option set.
923
+ */
924
+ function attachPathResponseTags<TEnv>(pathFn: PathFn<TEnv>): PathFn<TEnv> & {
925
+ json: JsonResponsePathFn<TEnv>;
926
+ text: TextResponsePathFn<TEnv>;
927
+ html: TextResponsePathFn<TEnv>;
928
+ xml: TextResponsePathFn<TEnv>;
929
+ md: TextResponsePathFn<TEnv>;
930
+ image: ResponsePathFn<TEnv>;
931
+ stream: ResponsePathFn<TEnv>;
932
+ any: ResponsePathFn<TEnv>;
933
+ } {
934
+ function createTagged(responseType: string): ResponsePathFn<TEnv> {
935
+ return ((
936
+ pattern: string,
937
+ handler: any,
938
+ optionsOrUse?: any,
939
+ maybeUse?: any,
940
+ ) => {
941
+ let options: PathOptions;
942
+ let use: (() => any[]) | undefined;
943
+
944
+ if (typeof optionsOrUse === "function") {
945
+ options = { [RESPONSE_TYPE]: responseType };
946
+ use = optionsOrUse;
947
+ } else {
948
+ options = { ...optionsOrUse, [RESPONSE_TYPE]: responseType };
949
+ use = maybeUse;
950
+ }
951
+
952
+ return pathFn(pattern, handler, options, use);
953
+ }) as ResponsePathFn<TEnv>;
954
+ }
955
+
956
+ const extended = pathFn as any;
957
+ extended.json = createTagged("json");
958
+ extended.text = createTagged("text");
959
+ extended.html = createTagged("html");
960
+ extended.xml = createTagged("xml");
961
+ extended.md = createTagged("md");
962
+ extended.image = createTagged("image");
963
+ extended.stream = createTagged("stream");
964
+ extended.any = createTagged("any");
965
+ return extended;
966
+ }
967
+
968
+ /**
969
+ * Process an IncludeItem by executing its nested patterns with prefixes
970
+ * This expands the include into actual route registrations
971
+ */
972
+ function processIncludeItem(item: IncludeItem): AllUseItems[] {
973
+ const { prefix, patterns, options } = item;
974
+ const namePrefix = options?.name;
975
+
976
+ // Execute the nested patterns' handler with URL and name prefixes
977
+ // The urlPrefix being set tells nested urls() to skip RootLayout wrapping
978
+ return runWithPrefixes(prefix, namePrefix, () => {
979
+ // Call the nested patterns' handler - this registers routes with prefixed patterns/names
980
+ return (patterns as UrlPatterns).handler();
981
+ });
982
+ }
983
+
984
+ /**
985
+ * Recursively process items, expanding any IncludeItems
986
+ * Returns items with IncludeItems expanded into actual route items
987
+ *
988
+ * Lazy includes are kept as-is (not expanded) for the router to handle later.
989
+ */
990
+ function processItems(items: readonly AllUseItems[]): AllUseItems[] {
991
+ const result: AllUseItems[] = [];
992
+
993
+ for (const item of items) {
994
+ if (!item) continue;
995
+
996
+ if (item.type === "include") {
997
+ const includeItem = item as IncludeItem & {
998
+ _expanded?: AllUseItems[];
999
+ lazy?: boolean;
1000
+ };
1001
+
1002
+ // Lazy includes are NOT expanded here - kept for router to handle
1003
+ if (includeItem.lazy) {
1004
+ result.push(item);
1005
+ continue;
1006
+ }
1007
+
1008
+ // Eager includes are already expanded during include() call
1009
+ if (includeItem._expanded) {
1010
+ // Items were expanded immediately - just process them recursively
1011
+ result.push(...processItems(includeItem._expanded));
1012
+ } else {
1013
+ // Fallback for legacy include items without _expanded
1014
+ const expanded = processIncludeItem(item as IncludeItem);
1015
+ result.push(...processItems(expanded));
1016
+ }
1017
+ } else if (item.type === "layout" && (item as any).uses) {
1018
+ // Process nested items in layout
1019
+ const layoutItem = item as any;
1020
+ layoutItem.uses = processItems(layoutItem.uses);
1021
+ result.push(layoutItem);
1022
+ } else {
1023
+ result.push(item);
1024
+ }
1025
+ }
1026
+
1027
+ return result;
1028
+ }
1029
+
1030
+ /**
1031
+ * Create include() helper for composing URL patterns
1032
+ *
1033
+ * By default, include() IMMEDIATELY expands the nested patterns. This ensures
1034
+ * that routes from included patterns inherit the correct parent context
1035
+ * (the layout they're included in).
1036
+ *
1037
+ * With `lazy: true`, patterns are NOT expanded at definition time. Instead,
1038
+ * they're evaluated on first request that matches the prefix. This improves
1039
+ * cold start time for apps with many routes.
1040
+ */
1041
+ function createIncludeHelper<TEnv>(): IncludeFn<TEnv> {
1042
+ return (
1043
+ prefix: string,
1044
+ patterns: UrlPatterns<TEnv>,
1045
+ options?: IncludeOptions,
1046
+ ): IncludeItem => {
1047
+ const store = getContext();
1048
+ const ctx = store.getStore();
1049
+ if (!ctx) throw new Error("include() must be called inside urls()");
1050
+
1051
+ const namePrefix = options?.name;
1052
+ const name = `$include_${prefix.replace(/[/:*?]/g, "_")}`;
1053
+
1054
+ // Capture context for deferred evaluation
1055
+ const capturedUrlPrefix = getUrlPrefix();
1056
+ const capturedNamePrefix = getNamePrefix();
1057
+ const capturedParent = ctx.parent;
1058
+ const fullPrefix = capturedUrlPrefix ? capturedUrlPrefix + prefix : prefix;
1059
+ const fullNamePrefix = namePrefix
1060
+ ? capturedNamePrefix
1061
+ ? `${capturedNamePrefix}.${namePrefix}`
1062
+ : namePrefix
1063
+ : capturedNamePrefix;
1064
+
1065
+ // Track this include for build-time manifest generation
1066
+ if (ctx.trackedIncludes) {
1067
+ ctx.trackedIncludes.push({
1068
+ prefix,
1069
+ fullPrefix,
1070
+ namePrefix: fullNamePrefix,
1071
+ patterns,
1072
+ lazy: true,
1073
+ });
1074
+ }
1075
+
1076
+ // All includes are lazy - patterns are evaluated on first matching request
1077
+ // This improves cold start time significantly for large route sets
1078
+ return {
1079
+ type: "include",
1080
+ name,
1081
+ prefix,
1082
+ patterns,
1083
+ options,
1084
+ lazy: true,
1085
+ _lazyContext: {
1086
+ urlPrefix: capturedUrlPrefix,
1087
+ namePrefix: fullNamePrefix,
1088
+ parent: capturedParent,
1089
+ },
1090
+ } as IncludeItem;
1091
+ };
1092
+ }
1093
+
1094
+ // ============================================================================
1095
+ // Re-use existing helpers from route-definition.ts
1096
+ // ============================================================================
1097
+
1098
+ // Import the helper creation functions from route-definition
1099
+ import { createRouteHelpers } from "./route-definition.js";
1100
+
1101
+ // ============================================================================
1102
+ // urls() Main Entry Point
1103
+ // ============================================================================
1104
+
1105
+ /**
1106
+ * Define URL patterns with Django-inspired syntax
1107
+ *
1108
+ * Replaces map() as the entry point for route definitions.
1109
+ * URL patterns are now visible at the definition site via path().
1110
+ *
1111
+ * @example
1112
+ * ```typescript
1113
+ * export const blogPatterns = urls(({ path, layout, loader }) => [
1114
+ * layout(BlogLayout, () => [
1115
+ * path("/", BlogIndex, { name: "index" }),
1116
+ * path("/:slug", BlogPost, { name: "post" }, () => [
1117
+ * loader(PostLoader),
1118
+ * ]),
1119
+ * ]),
1120
+ * ]);
1121
+ * ```
1122
+ */
1123
+ export function urls<
1124
+ TEnv = DefaultEnv,
1125
+ const TItems extends readonly AllUseItems[] = readonly AllUseItems[],
1126
+ >(
1127
+ builder: (helpers: PathHelpers<TEnv>) => TItems,
1128
+ ): UrlPatterns<TEnv, ExtractRoutes<TItems>, ExtractResponses<TItems>> {
1129
+ // Collect path definitions during build
1130
+ const definitions: PathDefinition[] = [];
1131
+
1132
+ // Create the handler function that will be called by the router
1133
+ const handler = () => {
1134
+ invariant(
1135
+ typeof builder === "function",
1136
+ "urls() expects a builder function as its argument",
1137
+ );
1138
+
1139
+ // Get base helpers from the existing route-definition module
1140
+ const baseHelpers = createRouteHelpers<any, TEnv>();
1141
+
1142
+ // Create the path helper (with .json, .text, .html, .xml, .image, .stream, .any tags)
1143
+ const pathHelper = attachPathResponseTags(createPathHelper<TEnv>());
1144
+
1145
+ // Create the include helper
1146
+ const includeHelper = createIncludeHelper<TEnv>();
1147
+
1148
+ // Combine all helpers
1149
+ // Note: layout and cache are cast to their typed versions - phantom types don't affect runtime
1150
+ const helpers: PathHelpers<TEnv> = {
1151
+ path: pathHelper as any,
1152
+ include: includeHelper as any,
1153
+ layout: baseHelpers.layout as PathHelpers<TEnv>["layout"],
1154
+ parallel: baseHelpers.parallel,
1155
+ intercept: baseHelpers.intercept as PathHelpers<TEnv>["intercept"],
1156
+ middleware: baseHelpers.middleware,
1157
+ revalidate: baseHelpers.revalidate,
1158
+ loader: baseHelpers.loader,
1159
+ loading: baseHelpers.loading,
1160
+ errorBoundary: baseHelpers.errorBoundary,
1161
+ notFoundBoundary: baseHelpers.notFoundBoundary,
1162
+ when: baseHelpers.when,
1163
+ cache: baseHelpers.cache as PathHelpers<TEnv>["cache"],
1164
+ };
1165
+
1166
+ // Execute builder directly - manifest.ts handles RootLayout wrapping
1167
+ // for inline handlers (non-Promise results).
1168
+ // For nested include() calls, routes inherit the outer RootLayout.
1169
+ const builderResult = builder(helpers);
1170
+ return processItems(builderResult);
1171
+ };
1172
+
1173
+ // trailingSlash config is populated when handler() runs
1174
+ // We expose it via a getter that reads from the context after handler execution
1175
+ return {
1176
+ definitions,
1177
+ handler,
1178
+ get trailingSlash() {
1179
+ // Get the trailingSlash map from the current context
1180
+ // This will be populated after handler() is called
1181
+ const store = getContext();
1182
+ const ctx = store.context.getStore();
1183
+ if (!ctx?.trailingSlash) {
1184
+ return {};
1185
+ }
1186
+ return Object.fromEntries(ctx.trailingSlash);
1187
+ },
1188
+ } as UrlPatterns<TEnv, ExtractRoutes<TItems>, ExtractResponses<TItems>>;
1189
+ }
1190
+
1191
+
1192
+ // ============================================================================
1193
+ // Type Utilities for path()
1194
+ // ============================================================================
1195
+
1196
+ /**
1197
+ * Extract route names from a UrlPatterns result
1198
+ * Used for type-safe href() generation
1199
+ */
1200
+ export type ExtractRouteNames<T extends UrlPatterns<any>> =
1201
+ T extends UrlPatterns<infer _TEnv>
1202
+ ? string // For now, will be refined with full implementation
1203
+ : never;
1204
+
1205
+ /**
1206
+ * Extract params for a specific route name
1207
+ */
1208
+ export type ExtractPathParams<
1209
+ T extends UrlPatterns<any>,
1210
+ K extends string,
1211
+ > = ExtractParams<string>; // Will be refined with pattern tracking
1212
+
1213
+ // ============================================================================
1214
+ // Response Envelope Types
1215
+ // ============================================================================
1216
+
1217
+ /**
1218
+ * Error shape returned in the `{ error }` side of a JSON response envelope.
1219
+ */
1220
+ export interface ResponseError {
1221
+ message: string;
1222
+ code?: string;
1223
+ type?: string;
1224
+ stack?: string;
1225
+ }
1226
+
1227
+ /**
1228
+ * Discriminated union envelope for JSON response routes.
1229
+ * Consumers check `result.error` to discriminate between success and failure.
1230
+ *
1231
+ * @example
1232
+ * ```typescript
1233
+ * const result: ResponseEnvelope<Product> = await fetch(url).then(r => r.json());
1234
+ * if (result.error) {
1235
+ * console.log(result.error.message, result.error.code);
1236
+ * return;
1237
+ * }
1238
+ * result.data.name // fully typed
1239
+ * ```
1240
+ */
1241
+ export type ResponseEnvelope<T> =
1242
+ | { data: T; error?: undefined }
1243
+ | { data?: undefined; error: ResponseError };
1244
+
1245
+ // ============================================================================
1246
+ // Response Type Consumer Utilities
1247
+ // ============================================================================
1248
+
1249
+ /**
1250
+ * Extract the response data type for a named route from a UrlPatterns instance.
1251
+ * Wraps in ResponseEnvelope since JSON response routes return enveloped data.
1252
+ *
1253
+ * @example
1254
+ * ```typescript
1255
+ * const apiPatterns = urls(({ path }) => [
1256
+ * path.json("/health", (ctx) => ({ status: "ok", timestamp: Date.now() }), { name: "health" }),
1257
+ * ]);
1258
+ *
1259
+ * type HealthData = RouteResponse<typeof apiPatterns, "health">;
1260
+ * // ResponseEnvelope<{ status: string; timestamp: number }>
1261
+ * ```
1262
+ */
1263
+ export type RouteResponse<TPatterns, TName extends string> = TPatterns extends {
1264
+ readonly _responses?: infer R;
1265
+ }
1266
+ ? TName extends keyof R
1267
+ ? ResponseEnvelope<Exclude<R[TName], Response>>
1268
+ : never
1269
+ : never;
1270
+
1271
+ // ============================================================================
1272
+ // Exports
1273
+ // ============================================================================
1274
+
1275
+ export type {
1276
+ AllUseItems,
1277
+ IncludeItem,
1278
+ TypedRouteItem,
1279
+ TypedIncludeItem,
1280
+ TypedLayoutItem,
1281
+ TypedCacheItem,
1282
+ } from "./route-types.js";