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