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