@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/urls.ts ADDED
@@ -0,0 +1,726 @@
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
+ LoaderDefinition,
35
+ MiddlewareFn,
36
+ NotFoundBoundaryHandler,
37
+ PartialCacheOptions,
38
+ ShouldRevalidateFn,
39
+ TrailingSlashMode,
40
+ } from "./types.js";
41
+ import type {
42
+ AllUseItems,
43
+ LayoutItem,
44
+ TypedLayoutItem,
45
+ RouteItem,
46
+ TypedRouteItem,
47
+ ParallelItem,
48
+ InterceptItem,
49
+ MiddlewareItem,
50
+ RevalidateItem,
51
+ LoaderItem,
52
+ LoadingItem,
53
+ ErrorBoundaryItem,
54
+ NotFoundBoundaryItem,
55
+ LayoutUseItem,
56
+ RouteUseItem,
57
+ ParallelUseItem,
58
+ InterceptUseItem,
59
+ LoaderUseItem,
60
+ WhenItem,
61
+ CacheItem,
62
+ TypedCacheItem,
63
+ IncludeItem,
64
+ TypedIncludeItem,
65
+ IncludeBrand,
66
+ UrlPatternsBrand,
67
+ } from "./route-types.js";
68
+ import {
69
+ getContext,
70
+ runWithPrefixes,
71
+ getUrlPrefix,
72
+ getNamePrefix,
73
+ type EntryData,
74
+ type InterceptEntry,
75
+ type InterceptWhenFn,
76
+ } from "./server/context";
77
+ import { invariant } from "./errors";
78
+
79
+ // ============================================================================
80
+ // Types
81
+ // ============================================================================
82
+
83
+ /**
84
+ * Sentinel type for unnamed routes.
85
+ * Using a branded string instead of `never` prevents TypeScript from
86
+ * widening array type inference when mixing named and unnamed routes.
87
+ */
88
+ export type UnnamedRoute = "$unnamed";
89
+
90
+ /**
91
+ * Options for path() function
92
+ */
93
+ export interface PathOptions<TName extends string = string> {
94
+ /** Route name for href() lookups */
95
+ name?: TName;
96
+ /** Trailing slash behavior: "never" (redirect /path/ to /path), "always" (redirect /path to /path/), "ignore" (match both) */
97
+ trailingSlash?: TrailingSlashMode;
98
+ }
99
+
100
+ /**
101
+ * Internal representation of a URL pattern definition
102
+ */
103
+ export interface PathDefinition {
104
+ pattern: string;
105
+ name?: string;
106
+ handler: ReactNode | Handler<any, any>;
107
+ use?: RouteUseItem[];
108
+ }
109
+
110
+ /**
111
+ * Result of urls() - contains the route definitions
112
+ */
113
+ export interface UrlPatterns<
114
+ TEnv = any,
115
+ TRoutes extends Record<string, string> = Record<string, string>
116
+ > {
117
+ /** Internal: route definitions */
118
+ readonly definitions: PathDefinition[];
119
+ /** Internal: compiled handler function */
120
+ readonly handler: () => AllUseItems[];
121
+ /** Internal: trailing slash config per route name */
122
+ readonly trailingSlash: Record<string, TrailingSlashMode>;
123
+ /** Brand for type checking */
124
+ readonly [UrlPatternsBrand]: void;
125
+ /** Environment type brand (phantom) */
126
+ readonly _env?: TEnv;
127
+ /** Routes type brand (phantom) - carries route name -> pattern mapping */
128
+ readonly _routes?: TRoutes;
129
+ }
130
+
131
+ /**
132
+ * Options for include()
133
+ */
134
+ export interface IncludeOptions<TNamePrefix extends string = string> {
135
+ /** Name prefix for all routes in this pattern set */
136
+ name?: TNamePrefix;
137
+ }
138
+
139
+ // ============================================================================
140
+ // Route Type Extraction Utilities
141
+ // ============================================================================
142
+
143
+ /**
144
+ * Prefix route names with a given prefix (e.g., "blog" + "post" = "blog.post")
145
+ */
146
+ type PrefixRoutes<
147
+ TRoutes extends Record<string, string>,
148
+ TPrefix extends string
149
+ > = TPrefix extends ""
150
+ ? TRoutes
151
+ : {
152
+ [K in keyof TRoutes as K extends string ? `${TPrefix}.${K}` : never]: TRoutes[K];
153
+ };
154
+
155
+ /**
156
+ * Prefix route patterns with a URL prefix (e.g., "/blog" + "/:slug" = "/blog/:slug")
157
+ */
158
+ type PrefixPatterns<
159
+ TRoutes extends Record<string, string>,
160
+ TUrlPrefix extends string
161
+ > = {
162
+ [K in keyof TRoutes]: TRoutes[K] extends string
163
+ ? `${TUrlPrefix}${TRoutes[K]}`
164
+ : TRoutes[K];
165
+ };
166
+
167
+ /**
168
+ * Depth counter for limiting recursion (max 5 levels to avoid infinite types)
169
+ */
170
+ type Depth = [never, 0, 1, 2, 3, 4];
171
+
172
+ /**
173
+ * Extract routes from a single item (path, include, layout, cache with children)
174
+ * D is the current depth level (0-4), stops at 5
175
+ */
176
+ type ExtractRoutesFromItem<T, D extends number = 5> = [D] extends [never]
177
+ ? {} // Max depth reached, stop recursion
178
+ : // TypedRouteItem: extract name -> pattern (exclude unnamed routes)
179
+ T extends TypedRouteItem<infer TName, infer TPattern>
180
+ ? TName extends string
181
+ ? TName extends UnnamedRoute
182
+ ? {} // Exclude unnamed routes from type map
183
+ : { [K in TName]: TPattern }
184
+ : {}
185
+ // TypedIncludeItem: extract prefixed routes
186
+ : T extends TypedIncludeItem<infer TRoutes, infer TPrefix>
187
+ ? TPrefix extends string
188
+ ? PrefixRoutes<TRoutes, TPrefix>
189
+ : TRoutes
190
+ // TypedLayoutItem: extract child routes from phantom type
191
+ : T extends TypedLayoutItem<infer TChildRoutes>
192
+ ? TChildRoutes
193
+ // TypedCacheItem: extract child routes from phantom type
194
+ : T extends TypedCacheItem<infer TChildRoutes>
195
+ ? TChildRoutes
196
+ // Fallback (won't extract routes)
197
+ : {};
198
+
199
+ /**
200
+ * Extract routes from an array of items (union of all extracted routes)
201
+ * D is the current depth level
202
+ */
203
+ type ExtractRoutesFromItems<
204
+ T extends readonly any[],
205
+ D extends number = 5
206
+ > = [D] extends [never]
207
+ ? {} // Max depth reached
208
+ : T extends readonly [infer First, ...infer Rest]
209
+ ? ExtractRoutesFromItem<First, D> &
210
+ ExtractRoutesFromItems<Rest, Depth[D]>
211
+ : {};
212
+
213
+ /**
214
+ * Main utility: extract route map from urls() callback return type
215
+ * Supports up to 5 levels of nesting
216
+ */
217
+ export type ExtractRoutes<T extends readonly any[]> = ExtractRoutesFromItems<T, 5>;
218
+
219
+ // ============================================================================
220
+ // Path Helpers Type
221
+ // ============================================================================
222
+
223
+ /**
224
+ * Helpers provided by urls()
225
+ */
226
+ export type PathHelpers<TEnv> = {
227
+ /**
228
+ * Define a route with URL pattern at definition site
229
+ *
230
+ * @example
231
+ * ```typescript
232
+ * // Pattern and component only
233
+ * path("/about", AboutPage)
234
+ *
235
+ * // With options
236
+ * path("/:slug", PostPage, { name: "post" })
237
+ *
238
+ * // With children (loaders, middleware, etc.)
239
+ * path("/:slug", PostPage, { name: "post" }, () => [
240
+ * loader(PostLoader),
241
+ * ])
242
+ * ```
243
+ */
244
+ path: <const TPattern extends string, const TName extends string = UnnamedRoute>(
245
+ pattern: TPattern,
246
+ handler: ReactNode | Handler<ExtractParams<TPattern>, TEnv>,
247
+ optionsOrUse?: PathOptions<TName> | (() => RouteUseItem[]),
248
+ use?: () => RouteUseItem[]
249
+ ) => TypedRouteItem<TName, TPattern>;
250
+
251
+ /**
252
+ * Define a layout that wraps child routes
253
+ */
254
+ layout: <const TChildren extends readonly LayoutUseItem[] = readonly LayoutUseItem[]>(
255
+ component: ReactNode | Handler<any, TEnv>,
256
+ use?: () => TChildren
257
+ ) => TypedLayoutItem<ExtractRoutes<TChildren>>;
258
+
259
+ /**
260
+ * Include nested URL patterns with optional name prefix
261
+ *
262
+ * ```typescript
263
+ * // Without name - routes keep local names
264
+ * include("/blog", blogPatterns)
265
+ *
266
+ * // With name - routes are prefixed (e.g., "index" → "blog.index")
267
+ * include("/blog", blogPatterns, { name: "blog" })
268
+ * ```
269
+ */
270
+ include: <
271
+ TRoutes extends Record<string, string>,
272
+ const TNamePrefix extends string = never
273
+ >(
274
+ prefix: string,
275
+ patterns: UrlPatterns<TEnv, TRoutes>,
276
+ options?: IncludeOptions<TNamePrefix>
277
+ ) => TypedIncludeItem<TRoutes, TNamePrefix>;
278
+
279
+ /**
280
+ * Define parallel routes that render simultaneously in named slots
281
+ */
282
+ parallel: <TSlots extends Record<`@${string}`, Handler<any, TEnv> | ReactNode>>(
283
+ slots: TSlots,
284
+ use?: () => ParallelUseItem[]
285
+ ) => ParallelItem;
286
+
287
+ /**
288
+ * Define an intercepting route for soft navigation
289
+ * Note: routeName must match a named path() in this urlpatterns
290
+ */
291
+ intercept: (
292
+ slotName: `@${string}`,
293
+ routeName: string,
294
+ handler: ReactNode | Handler<any, TEnv>,
295
+ use?: () => InterceptUseItem[]
296
+ ) => InterceptItem;
297
+
298
+ /**
299
+ * Attach middleware to the current route/layout
300
+ */
301
+ middleware: (...fns: MiddlewareFn<TEnv>[]) => MiddlewareItem;
302
+
303
+ /**
304
+ * Control when a segment should revalidate during navigation
305
+ */
306
+ revalidate: (fn: ShouldRevalidateFn<any, TEnv>) => RevalidateItem;
307
+
308
+ /**
309
+ * Attach a data loader to the current route/layout
310
+ */
311
+ loader: <TData>(
312
+ loaderDef: LoaderDefinition<TData>,
313
+ use?: () => LoaderUseItem[]
314
+ ) => LoaderItem;
315
+
316
+ /**
317
+ * Attach a loading component to the current route/layout
318
+ */
319
+ loading: (component: ReactNode, options?: { ssr?: boolean }) => LoadingItem;
320
+
321
+ /**
322
+ * Attach an error boundary to catch errors in this segment
323
+ */
324
+ errorBoundary: (
325
+ fallback: ReactNode | ErrorBoundaryHandler
326
+ ) => ErrorBoundaryItem;
327
+
328
+ /**
329
+ * Attach a not-found boundary to handle notFound() calls
330
+ */
331
+ notFoundBoundary: (
332
+ fallback: ReactNode | NotFoundBoundaryHandler
333
+ ) => NotFoundBoundaryItem;
334
+
335
+ /**
336
+ * Define a condition for when an intercept should activate
337
+ */
338
+ when: (fn: InterceptWhenFn) => WhenItem;
339
+
340
+ /**
341
+ * Define cache configuration for segments
342
+ */
343
+ cache: {
344
+ (): CacheItem;
345
+ <const TChildren extends readonly AllUseItems[] = readonly AllUseItems[]>(
346
+ children: () => TChildren
347
+ ): TypedCacheItem<ExtractRoutes<TChildren>>;
348
+ <const TChildren extends readonly AllUseItems[] = readonly AllUseItems[]>(
349
+ options: PartialCacheOptions | false,
350
+ use?: () => TChildren
351
+ ): TypedCacheItem<ExtractRoutes<TChildren>>;
352
+ };
353
+ };
354
+
355
+ // ============================================================================
356
+ // Helper Implementations
357
+ // ============================================================================
358
+
359
+ /**
360
+ * Check if a value is a valid use item
361
+ */
362
+ const isValidUseItem = (item: any): item is AllUseItems | undefined | null => {
363
+ return (
364
+ typeof item === "undefined" ||
365
+ item === null ||
366
+ (item &&
367
+ typeof item === "object" &&
368
+ "type" in item &&
369
+ [
370
+ "layout",
371
+ "route",
372
+ "middleware",
373
+ "revalidate",
374
+ "parallel",
375
+ "intercept",
376
+ "loader",
377
+ "loading",
378
+ "errorBoundary",
379
+ "notFoundBoundary",
380
+ "when",
381
+ "cache",
382
+ "include",
383
+ ].includes(item.type))
384
+ );
385
+ };
386
+
387
+ /**
388
+ * Apply URL prefix to a pattern
389
+ * Handles edge cases like "/" patterns and double slashes
390
+ */
391
+ function applyUrlPrefix(prefix: string, pattern: string): string {
392
+ if (!prefix) return pattern;
393
+ if (pattern === "/") return prefix;
394
+ if (prefix.endsWith("/") && pattern.startsWith("/")) {
395
+ return prefix + pattern.slice(1);
396
+ }
397
+ return prefix + pattern;
398
+ }
399
+
400
+ /**
401
+ * Apply name prefix to a route name
402
+ */
403
+ function applyNamePrefix(prefix: string | undefined, name: string): string {
404
+ if (!prefix) return name;
405
+ return `${prefix}.${name}`;
406
+ }
407
+
408
+ /**
409
+ * Create path() helper
410
+ *
411
+ * The path() function is the key new feature - it combines URL pattern
412
+ * with handler at the definition site.
413
+ */
414
+ function createPathHelper<TEnv>(): PathHelpers<TEnv>["path"] {
415
+ return ((
416
+ pattern: string,
417
+ handler: ReactNode | Handler<any, TEnv>,
418
+ optionsOrUse?: PathOptions | (() => RouteUseItem[]),
419
+ maybeUse?: () => RouteUseItem[]
420
+ ): RouteItem => {
421
+ const store = getContext();
422
+ const ctx = store.getStore();
423
+ if (!ctx) throw new Error("path() must be called inside urls()");
424
+
425
+ // Determine options and use based on argument types
426
+ let options: PathOptions | undefined;
427
+ let use: (() => RouteUseItem[]) | undefined;
428
+
429
+ if (typeof optionsOrUse === "function") {
430
+ // path(pattern, handler, use)
431
+ use = optionsOrUse as () => RouteUseItem[];
432
+ } else if (typeof optionsOrUse === "object") {
433
+ // path(pattern, handler, options) or path(pattern, handler, options, use)
434
+ options = optionsOrUse as PathOptions;
435
+ use = maybeUse;
436
+ }
437
+
438
+ // Get prefixes from context (set by include())
439
+ const urlPrefix = getUrlPrefix();
440
+ const namePrefix = getNamePrefix();
441
+
442
+ // Apply URL prefix to pattern
443
+ const prefixedPattern = applyUrlPrefix(urlPrefix, pattern);
444
+
445
+ // Generate route name - use provided name or generate from pattern
446
+ const localName = options?.name || `$path_${pattern.replace(/[/:*?]/g, "_")}`;
447
+ // Apply name prefix if set (from include())
448
+ const routeName = applyNamePrefix(namePrefix, localName);
449
+
450
+ const namespace = `${ctx.namespace}.${store.getNextIndex("route")}.${routeName}`;
451
+
452
+ // Ensure handler is always a function (wrap ReactNode if needed)
453
+ const wrappedHandler: Handler<any, TEnv> =
454
+ typeof handler === "function"
455
+ ? (handler as Handler<any, TEnv>)
456
+ : () => handler;
457
+
458
+ const entry = {
459
+ id: namespace,
460
+ shortCode: store.getShortCode("route"),
461
+ type: "route" as const,
462
+ parent: ctx.parent,
463
+ handler: wrappedHandler,
464
+ // Store the PREFIXED pattern for route matching
465
+ pattern: prefixedPattern,
466
+ loading: undefined,
467
+ middleware: [],
468
+ revalidate: [],
469
+ errorBoundary: [],
470
+ notFoundBoundary: [],
471
+ layout: [],
472
+ parallel: [],
473
+ intercept: [],
474
+ loader: [],
475
+ };
476
+
477
+ // Check for duplicate route names (TypeScript should catch this, but runtime check too)
478
+ invariant(
479
+ ctx.manifest.get(routeName) === undefined,
480
+ `Duplicate route name: ${routeName} at ${namespace}`
481
+ );
482
+
483
+ // Register route entry with prefixed name
484
+ ctx.manifest.set(routeName, entry);
485
+
486
+ // Also store pattern in a separate map for URL generation
487
+ if (ctx.patterns) {
488
+ ctx.patterns.set(routeName, prefixedPattern);
489
+ }
490
+
491
+ // Store trailing slash config if specified
492
+ if (options?.trailingSlash && ctx.trailingSlash) {
493
+ ctx.trailingSlash.set(routeName, options.trailingSlash);
494
+ }
495
+
496
+ // Run use callback if provided
497
+ if (use && typeof use === "function") {
498
+ const result = store.run(namespace, entry, use);
499
+ invariant(
500
+ Array.isArray(result) && result.every((item) => isValidUseItem(item)),
501
+ `path() use() callback must return an array of use items [${namespace}]`
502
+ );
503
+ return { name: namespace, type: "route", uses: result } as RouteItem;
504
+ }
505
+
506
+ return { name: namespace, type: "route" } as RouteItem;
507
+ }) as PathHelpers<TEnv>["path"];
508
+ }
509
+
510
+ /**
511
+ * Process an IncludeItem by executing its nested patterns with prefixes
512
+ * This expands the include into actual route registrations
513
+ */
514
+ function processIncludeItem(item: IncludeItem): AllUseItems[] {
515
+ const { prefix, patterns, options } = item;
516
+ const namePrefix = options?.name;
517
+
518
+ // Execute the nested patterns' handler with URL and name prefixes
519
+ // The urlPrefix being set tells nested urls() to skip RootLayout wrapping
520
+ return runWithPrefixes(prefix, namePrefix, () => {
521
+ // Call the nested patterns' handler - this registers routes with prefixed patterns/names
522
+ return (patterns as UrlPatterns).handler();
523
+ });
524
+ }
525
+
526
+ /**
527
+ * Recursively process items, expanding any IncludeItems
528
+ * Returns items with IncludeItems expanded into actual route items
529
+ */
530
+ function processItems(items: readonly AllUseItems[]): AllUseItems[] {
531
+ const result: AllUseItems[] = [];
532
+
533
+ for (const item of items) {
534
+ if (!item) continue;
535
+
536
+ if (item.type === "include") {
537
+ // Include items are already expanded during include() call
538
+ // Just extract the expanded items
539
+ const includeItem = item as IncludeItem & { _expanded?: AllUseItems[] };
540
+ if (includeItem._expanded) {
541
+ // Items were expanded immediately - just process them recursively
542
+ result.push(...processItems(includeItem._expanded));
543
+ } else {
544
+ // Fallback for legacy include items without _expanded
545
+ const expanded = processIncludeItem(item as IncludeItem);
546
+ result.push(...processItems(expanded));
547
+ }
548
+ } else if (item.type === "layout" && (item as any).uses) {
549
+ // Process nested items in layout
550
+ const layoutItem = item as any;
551
+ layoutItem.uses = processItems(layoutItem.uses);
552
+ result.push(layoutItem);
553
+ } else {
554
+ result.push(item);
555
+ }
556
+ }
557
+
558
+ return result;
559
+ }
560
+
561
+ /**
562
+ * Create include() helper for composing URL patterns
563
+ *
564
+ * Unlike other helpers that return items for later processing,
565
+ * include() IMMEDIATELY expands the nested patterns. This ensures
566
+ * that routes from included patterns inherit the correct parent context
567
+ * (the layout they're included in).
568
+ */
569
+ function createIncludeHelper<TEnv>(): PathHelpers<TEnv>["include"] {
570
+ return (
571
+ prefix: string,
572
+ patterns: UrlPatterns<TEnv>,
573
+ options?: IncludeOptions
574
+ ): IncludeItem => {
575
+ const store = getContext();
576
+ const ctx = store.getStore();
577
+ if (!ctx) throw new Error("include() must be called inside urls()");
578
+
579
+ const namePrefix = options?.name;
580
+
581
+ // IMMEDIATELY expand the nested patterns with the current context
582
+ // This ensures routes inherit the correct parent (e.g., UserRootLayout)
583
+ const expandedItems = runWithPrefixes(prefix, namePrefix, () => {
584
+ return (patterns as UrlPatterns).handler();
585
+ });
586
+
587
+ // Return a marker item that contains the expanded items
588
+ // processItems will extract these expanded items
589
+ const name = `$include_${prefix.replace(/[/:*?]/g, "_")}`;
590
+ return {
591
+ type: "include",
592
+ name,
593
+ prefix,
594
+ patterns,
595
+ options,
596
+ // Store expanded items for processItems to extract
597
+ _expanded: expandedItems,
598
+ } as IncludeItem & { _expanded: AllUseItems[] };
599
+ };
600
+ }
601
+
602
+ // ============================================================================
603
+ // Re-use existing helpers from route-definition.ts
604
+ // ============================================================================
605
+
606
+ // Import the helper creation functions from route-definition
607
+ import {
608
+ createRouteHelpers,
609
+ } from "./route-definition.js";
610
+
611
+ // ============================================================================
612
+ // urls() Main Entry Point
613
+ // ============================================================================
614
+
615
+ /**
616
+ * Define URL patterns with Django-inspired syntax
617
+ *
618
+ * Replaces map() as the entry point for route definitions.
619
+ * URL patterns are now visible at the definition site via path().
620
+ *
621
+ * @example
622
+ * ```typescript
623
+ * export const blogPatterns = urls(({ path, layout, loader }) => [
624
+ * layout(BlogLayout, () => [
625
+ * path("/", BlogIndex, { name: "index" }),
626
+ * path("/:slug", BlogPost, { name: "post" }, () => [
627
+ * loader(PostLoader),
628
+ * ]),
629
+ * ]),
630
+ * ]);
631
+ * ```
632
+ */
633
+ export function urls<
634
+ TEnv = DefaultEnv,
635
+ const TItems extends readonly AllUseItems[] = readonly AllUseItems[]
636
+ >(
637
+ builder: (helpers: PathHelpers<TEnv>) => TItems
638
+ ): UrlPatterns<TEnv, ExtractRoutes<TItems>> {
639
+ // Collect path definitions during build
640
+ const definitions: PathDefinition[] = [];
641
+
642
+ // Create the handler function that will be called by the router
643
+ const handler = () => {
644
+ invariant(
645
+ typeof builder === "function",
646
+ "urls() expects a builder function as its argument"
647
+ );
648
+
649
+ // Get base helpers from the existing route-definition module
650
+ const baseHelpers = createRouteHelpers<any, TEnv>();
651
+
652
+ // Create the path helper
653
+ const pathHelper = createPathHelper<TEnv>();
654
+
655
+ // Create the include helper
656
+ const includeHelper = createIncludeHelper<TEnv>();
657
+
658
+ // Combine all helpers
659
+ // Note: layout and cache are cast to their typed versions - phantom types don't affect runtime
660
+ const helpers: PathHelpers<TEnv> = {
661
+ path: pathHelper,
662
+ include: includeHelper,
663
+ layout: baseHelpers.layout as PathHelpers<TEnv>["layout"],
664
+ parallel: baseHelpers.parallel,
665
+ intercept: baseHelpers.intercept as PathHelpers<TEnv>["intercept"],
666
+ middleware: baseHelpers.middleware,
667
+ revalidate: baseHelpers.revalidate,
668
+ loader: baseHelpers.loader,
669
+ loading: baseHelpers.loading,
670
+ errorBoundary: baseHelpers.errorBoundary,
671
+ notFoundBoundary: baseHelpers.notFoundBoundary,
672
+ when: baseHelpers.when,
673
+ cache: baseHelpers.cache as PathHelpers<TEnv>["cache"],
674
+ };
675
+
676
+ // Execute builder directly - manifest.ts handles RootLayout wrapping
677
+ // for inline handlers (non-Promise results).
678
+ // For nested include() calls, routes inherit the outer RootLayout.
679
+ const builderResult = builder(helpers);
680
+ return processItems(builderResult);
681
+ };
682
+
683
+ // trailingSlash config is populated when handler() runs
684
+ // We expose it via a getter that reads from the context after handler execution
685
+ return {
686
+ definitions,
687
+ handler,
688
+ get trailingSlash() {
689
+ // Get the trailingSlash map from the current context
690
+ // This will be populated after handler() is called
691
+ const store = getContext();
692
+ const ctx = store.context.getStore();
693
+ if (!ctx?.trailingSlash) {
694
+ return {};
695
+ }
696
+ return Object.fromEntries(ctx.trailingSlash);
697
+ },
698
+ } as UrlPatterns<TEnv, ExtractRoutes<TItems>>;
699
+ }
700
+
701
+ // ============================================================================
702
+ // Type Utilities for path()
703
+ // ============================================================================
704
+
705
+ /**
706
+ * Extract route names from a UrlPatterns result
707
+ * Used for type-safe href() generation
708
+ */
709
+ export type ExtractRouteNames<T extends UrlPatterns<any>> =
710
+ T extends UrlPatterns<infer _TEnv>
711
+ ? string // For now, will be refined with full implementation
712
+ : never;
713
+
714
+ /**
715
+ * Extract params for a specific route name
716
+ */
717
+ export type ExtractPathParams<
718
+ T extends UrlPatterns<any>,
719
+ K extends string
720
+ > = ExtractParams<string>; // Will be refined with pattern tracking
721
+
722
+ // ============================================================================
723
+ // Exports
724
+ // ============================================================================
725
+
726
+ export type { AllUseItems, IncludeItem, TypedRouteItem, TypedIncludeItem, TypedLayoutItem, TypedCacheItem } from "./route-types.js";