@rangojs/router 0.0.0-experimental.13 → 0.0.0-experimental.13221847

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