@real-router/core 0.22.0 → 0.23.0
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/README.md +1 -3
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/metafile-cjs.json +1 -1
- package/dist/esm/index.d.mts +1 -1
- package/dist/esm/index.mjs +1 -1
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/metafile-esm.json +1 -1
- package/package.json +7 -5
- package/src/Router.ts +1174 -0
- package/src/RouterError.ts +324 -0
- package/src/constants.ts +112 -0
- package/src/createRouter.ts +32 -0
- package/src/fsm/index.ts +5 -0
- package/src/fsm/routerFSM.ts +129 -0
- package/src/getNavigator.ts +15 -0
- package/src/helpers.ts +194 -0
- package/src/index.ts +46 -0
- package/src/namespaces/CloneNamespace/CloneNamespace.ts +120 -0
- package/src/namespaces/CloneNamespace/index.ts +3 -0
- package/src/namespaces/CloneNamespace/types.ts +46 -0
- package/src/namespaces/DependenciesNamespace/DependenciesNamespace.ts +250 -0
- package/src/namespaces/DependenciesNamespace/index.ts +3 -0
- package/src/namespaces/DependenciesNamespace/validators.ts +105 -0
- package/src/namespaces/EventBusNamespace/EventBusNamespace.ts +272 -0
- package/src/namespaces/EventBusNamespace/index.ts +5 -0
- package/src/namespaces/EventBusNamespace/types.ts +11 -0
- package/src/namespaces/MiddlewareNamespace/MiddlewareNamespace.ts +206 -0
- package/src/namespaces/MiddlewareNamespace/index.ts +5 -0
- package/src/namespaces/MiddlewareNamespace/types.ts +28 -0
- package/src/namespaces/MiddlewareNamespace/validators.ts +96 -0
- package/src/namespaces/NavigationNamespace/NavigationNamespace.ts +308 -0
- package/src/namespaces/NavigationNamespace/index.ts +5 -0
- package/src/namespaces/NavigationNamespace/transition/executeLifecycleHooks.ts +84 -0
- package/src/namespaces/NavigationNamespace/transition/executeMiddleware.ts +56 -0
- package/src/namespaces/NavigationNamespace/transition/index.ts +107 -0
- package/src/namespaces/NavigationNamespace/transition/makeError.ts +37 -0
- package/src/namespaces/NavigationNamespace/transition/mergeStates.ts +54 -0
- package/src/namespaces/NavigationNamespace/transition/processLifecycleResult.ts +81 -0
- package/src/namespaces/NavigationNamespace/transition/wrapSyncError.ts +82 -0
- package/src/namespaces/NavigationNamespace/types.ts +129 -0
- package/src/namespaces/NavigationNamespace/validators.ts +87 -0
- package/src/namespaces/OptionsNamespace/OptionsNamespace.ts +50 -0
- package/src/namespaces/OptionsNamespace/constants.ts +41 -0
- package/src/namespaces/OptionsNamespace/helpers.ts +51 -0
- package/src/namespaces/OptionsNamespace/index.ts +11 -0
- package/src/namespaces/OptionsNamespace/validators.ts +252 -0
- package/src/namespaces/PluginsNamespace/PluginsNamespace.ts +325 -0
- package/src/namespaces/PluginsNamespace/constants.ts +35 -0
- package/src/namespaces/PluginsNamespace/index.ts +7 -0
- package/src/namespaces/PluginsNamespace/types.ts +32 -0
- package/src/namespaces/PluginsNamespace/validators.ts +79 -0
- package/src/namespaces/RouteLifecycleNamespace/RouteLifecycleNamespace.ts +389 -0
- package/src/namespaces/RouteLifecycleNamespace/index.ts +5 -0
- package/src/namespaces/RouteLifecycleNamespace/types.ts +17 -0
- package/src/namespaces/RouteLifecycleNamespace/validators.ts +65 -0
- package/src/namespaces/RouterLifecycleNamespace/RouterLifecycleNamespace.ts +140 -0
- package/src/namespaces/RouterLifecycleNamespace/constants.ts +25 -0
- package/src/namespaces/RouterLifecycleNamespace/index.ts +5 -0
- package/src/namespaces/RouterLifecycleNamespace/types.ts +23 -0
- package/src/namespaces/RoutesNamespace/RoutesNamespace.ts +1482 -0
- package/src/namespaces/RoutesNamespace/constants.ts +14 -0
- package/src/namespaces/RoutesNamespace/helpers.ts +532 -0
- package/src/namespaces/RoutesNamespace/index.ts +9 -0
- package/src/namespaces/RoutesNamespace/stateBuilder.ts +70 -0
- package/src/namespaces/RoutesNamespace/types.ts +82 -0
- package/src/namespaces/RoutesNamespace/validators.ts +331 -0
- package/src/namespaces/StateNamespace/StateNamespace.ts +317 -0
- package/src/namespaces/StateNamespace/helpers.ts +43 -0
- package/src/namespaces/StateNamespace/index.ts +5 -0
- package/src/namespaces/StateNamespace/types.ts +15 -0
- package/src/namespaces/index.ts +42 -0
- package/src/transitionPath.ts +441 -0
- package/src/typeGuards.ts +74 -0
- package/src/types.ts +194 -0
- package/src/wiring/RouterWiringBuilder.ts +235 -0
- package/src/wiring/index.ts +7 -0
- package/src/wiring/types.ts +53 -0
- package/src/wiring/wireRouter.ts +29 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// packages/core/src/namespaces/RoutesNamespace/constants.ts
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Default route name for the root node.
|
|
5
|
+
*/
|
|
6
|
+
export const DEFAULT_ROUTE_NAME = "";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Cache for validated route names to skip regex validation on repeated calls.
|
|
10
|
+
* Key insight: validateRouteName() regex takes ~40ns, but cache lookup is ~1ns.
|
|
11
|
+
* This cache is module-level (shared across all router instances) since route name
|
|
12
|
+
* validity is independent of router instance.
|
|
13
|
+
*/
|
|
14
|
+
export const validatedRouteNames = new Set<string>();
|
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
// packages/core/src/namespaces/RoutesNamespace/helpers.ts
|
|
2
|
+
|
|
3
|
+
import { getSegmentsByName } from "route-tree";
|
|
4
|
+
import { getTypeDescription } from "type-guards";
|
|
5
|
+
|
|
6
|
+
import type { RouteConfig } from "./types";
|
|
7
|
+
import type { Route } from "../../types";
|
|
8
|
+
import type {
|
|
9
|
+
DefaultDependencies,
|
|
10
|
+
ForwardToCallback,
|
|
11
|
+
Params,
|
|
12
|
+
} from "@real-router/types";
|
|
13
|
+
import type { RouteDefinition, RouteTree } from "route-tree";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Creates an empty RouteConfig.
|
|
17
|
+
*/
|
|
18
|
+
export function createEmptyConfig(): RouteConfig {
|
|
19
|
+
return {
|
|
20
|
+
decoders: Object.create(null) as Record<string, (params: Params) => Params>,
|
|
21
|
+
encoders: Object.create(null) as Record<string, (params: Params) => Params>,
|
|
22
|
+
defaultParams: Object.create(null) as Record<string, Params>,
|
|
23
|
+
forwardMap: Object.create(null) as Record<string, string>,
|
|
24
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
25
|
+
forwardFnMap: Object.create(null) as Record<string, ForwardToCallback<any>>,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ============================================================================
|
|
30
|
+
// Route Tree Helpers
|
|
31
|
+
// ============================================================================
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Checks if all params from source exist with same values in target.
|
|
35
|
+
* Small function body allows V8 inlining.
|
|
36
|
+
*/
|
|
37
|
+
export function paramsMatch(source: Params, target: Params): boolean {
|
|
38
|
+
for (const key in source) {
|
|
39
|
+
if (source[key] !== target[key]) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Checks params match, skipping keys present in skipKeys.
|
|
49
|
+
*/
|
|
50
|
+
export function paramsMatchExcluding(
|
|
51
|
+
source: Params,
|
|
52
|
+
target: Params,
|
|
53
|
+
skipKeys: Params,
|
|
54
|
+
): boolean {
|
|
55
|
+
for (const key in source) {
|
|
56
|
+
if (key in skipKeys) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (source[key] !== target[key]) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Sanitizes a route by keeping only essential properties.
|
|
69
|
+
*/
|
|
70
|
+
export function sanitizeRoute<Dependencies extends DefaultDependencies>(
|
|
71
|
+
route: Route<Dependencies>,
|
|
72
|
+
): RouteDefinition {
|
|
73
|
+
const sanitized: RouteDefinition = {
|
|
74
|
+
name: route.name,
|
|
75
|
+
path: route.path,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
if (route.children) {
|
|
79
|
+
sanitized.children = route.children.map((child) => sanitizeRoute(child));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return sanitized;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Recursively removes a route from definitions array.
|
|
87
|
+
*/
|
|
88
|
+
export function removeFromDefinitions(
|
|
89
|
+
definitions: RouteDefinition[],
|
|
90
|
+
routeName: string,
|
|
91
|
+
parentPrefix = "",
|
|
92
|
+
): boolean {
|
|
93
|
+
for (let i = 0; i < definitions.length; i++) {
|
|
94
|
+
const route = definitions[i];
|
|
95
|
+
const fullName = parentPrefix
|
|
96
|
+
? `${parentPrefix}.${route.name}`
|
|
97
|
+
: route.name;
|
|
98
|
+
|
|
99
|
+
if (fullName === routeName) {
|
|
100
|
+
definitions.splice(i, 1);
|
|
101
|
+
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (
|
|
106
|
+
route.children &&
|
|
107
|
+
routeName.startsWith(`${fullName}.`) &&
|
|
108
|
+
removeFromDefinitions(route.children, routeName, fullName)
|
|
109
|
+
) {
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Clears configuration entries that match the predicate.
|
|
119
|
+
*/
|
|
120
|
+
export function clearConfigEntries<T>(
|
|
121
|
+
config: Record<string, T>,
|
|
122
|
+
matcher: (key: string) => boolean,
|
|
123
|
+
): void {
|
|
124
|
+
for (const key of Object.keys(config)) {
|
|
125
|
+
if (matcher(key)) {
|
|
126
|
+
delete config[key];
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ============================================================================
|
|
132
|
+
// Route Property Validation
|
|
133
|
+
// ============================================================================
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Validates forwardTo property type and async status.
|
|
137
|
+
*/
|
|
138
|
+
function validateForwardToProperty(forwardTo: unknown, fullName: string): void {
|
|
139
|
+
if (forwardTo === undefined) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (typeof forwardTo === "function") {
|
|
144
|
+
const isNativeAsync =
|
|
145
|
+
(forwardTo as { constructor: { name: string } }).constructor.name ===
|
|
146
|
+
"AsyncFunction";
|
|
147
|
+
const isTranspiledAsync = forwardTo.toString().includes("__awaiter");
|
|
148
|
+
|
|
149
|
+
if (isNativeAsync || isTranspiledAsync) {
|
|
150
|
+
throw new TypeError(
|
|
151
|
+
`[router.addRoute] forwardTo callback cannot be async for route "${fullName}". ` +
|
|
152
|
+
`Async functions break matchPath/buildPath.`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Validates route properties for addRoute.
|
|
160
|
+
* Throws TypeError if any property is invalid.
|
|
161
|
+
*
|
|
162
|
+
* @param route - Route to validate
|
|
163
|
+
* @param fullName - Full route name (with parent prefix)
|
|
164
|
+
* @throws {TypeError} If canActivate/canDeactivate is not a function
|
|
165
|
+
* @throws {TypeError} If defaultParams is not a plain object
|
|
166
|
+
* @throws {TypeError} If decodeParams/encodeParams is async
|
|
167
|
+
*/
|
|
168
|
+
export function validateRouteProperties<
|
|
169
|
+
Dependencies extends DefaultDependencies,
|
|
170
|
+
>(route: Route<Dependencies>, fullName: string): void {
|
|
171
|
+
// Validate canActivate is a function
|
|
172
|
+
if (
|
|
173
|
+
route.canActivate !== undefined &&
|
|
174
|
+
typeof route.canActivate !== "function"
|
|
175
|
+
) {
|
|
176
|
+
throw new TypeError(
|
|
177
|
+
`[router.addRoute] canActivate must be a function for route "${fullName}", ` +
|
|
178
|
+
`got ${getTypeDescription(route.canActivate)}`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Validate canDeactivate is a function
|
|
183
|
+
if (
|
|
184
|
+
route.canDeactivate !== undefined &&
|
|
185
|
+
typeof route.canDeactivate !== "function"
|
|
186
|
+
) {
|
|
187
|
+
throw new TypeError(
|
|
188
|
+
`[router.addRoute] canDeactivate must be a function for route "${fullName}", ` +
|
|
189
|
+
`got ${getTypeDescription(route.canDeactivate)}`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Validate defaultParams is a plain object
|
|
194
|
+
// Runtime check for invalid types passed via `as any`
|
|
195
|
+
if (route.defaultParams !== undefined) {
|
|
196
|
+
const params: unknown = route.defaultParams;
|
|
197
|
+
|
|
198
|
+
if (
|
|
199
|
+
params === null ||
|
|
200
|
+
typeof params !== "object" ||
|
|
201
|
+
Array.isArray(params)
|
|
202
|
+
) {
|
|
203
|
+
throw new TypeError(
|
|
204
|
+
`[router.addRoute] defaultParams must be an object for route "${fullName}", ` +
|
|
205
|
+
`got ${getTypeDescription(route.defaultParams)}`,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Validate decodeParams is not async (sync required for matchPath/buildPath)
|
|
211
|
+
if (route.decodeParams?.constructor.name === "AsyncFunction") {
|
|
212
|
+
throw new TypeError(
|
|
213
|
+
`[router.addRoute] decodeParams cannot be async for route "${fullName}". Async functions break matchPath/buildPath.`,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Validate encodeParams is not async (sync required for matchPath/buildPath)
|
|
218
|
+
if (route.encodeParams?.constructor.name === "AsyncFunction") {
|
|
219
|
+
throw new TypeError(
|
|
220
|
+
`[router.addRoute] encodeParams cannot be async for route "${fullName}". Async functions break matchPath/buildPath.`,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Validate forwardTo type and async
|
|
225
|
+
validateForwardToProperty(route.forwardTo, fullName);
|
|
226
|
+
|
|
227
|
+
// Recursively validate children
|
|
228
|
+
if (route.children) {
|
|
229
|
+
for (const child of route.children) {
|
|
230
|
+
const childFullName = `${fullName}.${child.name}`;
|
|
231
|
+
|
|
232
|
+
validateRouteProperties(child, childFullName);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ============================================================================
|
|
238
|
+
// ForwardTo Validation
|
|
239
|
+
// ============================================================================
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Extracts parameter names from a path string.
|
|
243
|
+
* Matches :param and *splat patterns.
|
|
244
|
+
*/
|
|
245
|
+
function extractParamsFromPath(path: string): Set<string> {
|
|
246
|
+
const params = new Set<string>();
|
|
247
|
+
const paramRegex = /[*:]([A-Z_a-z]\w*)/g;
|
|
248
|
+
let match;
|
|
249
|
+
|
|
250
|
+
while ((match = paramRegex.exec(path)) !== null) {
|
|
251
|
+
params.add(match[1]);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return params;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Extracts all parameters from multiple path segments.
|
|
259
|
+
*/
|
|
260
|
+
function extractParamsFromPaths(paths: readonly string[]): Set<string> {
|
|
261
|
+
const params = new Set<string>();
|
|
262
|
+
|
|
263
|
+
for (const path of paths) {
|
|
264
|
+
for (const param of extractParamsFromPath(path)) {
|
|
265
|
+
params.add(param);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return params;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Collects all path segments for a route from batch definitions.
|
|
274
|
+
* Traverses parent routes to include inherited path segments.
|
|
275
|
+
*/
|
|
276
|
+
function collectPathsToRoute<Dependencies extends DefaultDependencies>(
|
|
277
|
+
routes: readonly Route<Dependencies>[],
|
|
278
|
+
routeName: string,
|
|
279
|
+
parentName = "",
|
|
280
|
+
paths: string[] = [],
|
|
281
|
+
): string[] {
|
|
282
|
+
for (const route of routes) {
|
|
283
|
+
const fullName = parentName ? `${parentName}.${route.name}` : route.name;
|
|
284
|
+
const currentPaths = [...paths, route.path];
|
|
285
|
+
|
|
286
|
+
if (fullName === routeName) {
|
|
287
|
+
return currentPaths;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (route.children && routeName.startsWith(`${fullName}.`)) {
|
|
291
|
+
return collectPathsToRoute(
|
|
292
|
+
route.children,
|
|
293
|
+
routeName,
|
|
294
|
+
fullName,
|
|
295
|
+
currentPaths,
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/* v8 ignore next -- @preserve unreachable: callers validate existence */
|
|
301
|
+
throw new Error(
|
|
302
|
+
`[internal] collectPathsToRoute: route "${routeName}" not found`,
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Collects all route names from a batch of routes (including children).
|
|
308
|
+
*/
|
|
309
|
+
function collectRouteNames<Dependencies extends DefaultDependencies>(
|
|
310
|
+
routes: readonly Route<Dependencies>[],
|
|
311
|
+
parentName = "",
|
|
312
|
+
): Set<string> {
|
|
313
|
+
const names = new Set<string>();
|
|
314
|
+
|
|
315
|
+
for (const route of routes) {
|
|
316
|
+
const fullName = parentName ? `${parentName}.${route.name}` : route.name;
|
|
317
|
+
|
|
318
|
+
names.add(fullName);
|
|
319
|
+
|
|
320
|
+
if (route.children) {
|
|
321
|
+
for (const childName of collectRouteNames(route.children, fullName)) {
|
|
322
|
+
names.add(childName);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return names;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Collects all forwardTo mappings from a batch of routes (including children).
|
|
332
|
+
* Only collects string forwardTo values; callbacks are handled separately.
|
|
333
|
+
*/
|
|
334
|
+
function collectForwardMappings<Dependencies extends DefaultDependencies>(
|
|
335
|
+
routes: readonly Route<Dependencies>[],
|
|
336
|
+
parentName = "",
|
|
337
|
+
): Map<string, string> {
|
|
338
|
+
const mappings = new Map<string, string>();
|
|
339
|
+
|
|
340
|
+
for (const route of routes) {
|
|
341
|
+
const fullName = parentName ? `${parentName}.${route.name}` : route.name;
|
|
342
|
+
|
|
343
|
+
if (route.forwardTo && typeof route.forwardTo === "string") {
|
|
344
|
+
mappings.set(fullName, route.forwardTo);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (route.children) {
|
|
348
|
+
for (const [key, value] of collectForwardMappings(
|
|
349
|
+
route.children,
|
|
350
|
+
fullName,
|
|
351
|
+
)) {
|
|
352
|
+
mappings.set(key, value);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return mappings;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Extracts required path parameters from route segments.
|
|
362
|
+
*/
|
|
363
|
+
function getRequiredParams(segments: readonly RouteTree[]): Set<string> {
|
|
364
|
+
const params = new Set<string>();
|
|
365
|
+
|
|
366
|
+
for (const segment of segments) {
|
|
367
|
+
// Named routes always have parsers (null only for root without path)
|
|
368
|
+
for (const param of segment.paramMeta.urlParams) {
|
|
369
|
+
params.add(param);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
for (const param of segment.paramMeta.spatParams) {
|
|
373
|
+
params.add(param);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return params;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Checks if a route exists in the tree by navigating through children Map.
|
|
382
|
+
*/
|
|
383
|
+
function routeExistsInTree(tree: RouteTree, routeName: string): boolean {
|
|
384
|
+
const segments = routeName.split(".");
|
|
385
|
+
let current: RouteTree | undefined = tree;
|
|
386
|
+
|
|
387
|
+
for (const segment of segments) {
|
|
388
|
+
current = current.children.get(segment);
|
|
389
|
+
|
|
390
|
+
if (!current) {
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Gets the target route parameters for validation.
|
|
400
|
+
*/
|
|
401
|
+
function getTargetParams<Dependencies extends DefaultDependencies>(
|
|
402
|
+
targetRoute: string,
|
|
403
|
+
existsInTree: boolean,
|
|
404
|
+
tree: RouteTree,
|
|
405
|
+
routes: readonly Route<Dependencies>[],
|
|
406
|
+
): Set<string> {
|
|
407
|
+
if (existsInTree) {
|
|
408
|
+
const toSegments = getSegmentsByName(tree, targetRoute);
|
|
409
|
+
|
|
410
|
+
// toSegments won't be null since we checked existsInTree
|
|
411
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
412
|
+
return getRequiredParams(toSegments!);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Target is in batch
|
|
416
|
+
return extractParamsFromPaths(collectPathsToRoute(routes, targetRoute));
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Validates a single forward mapping for target existence and param compatibility.
|
|
421
|
+
*/
|
|
422
|
+
function validateSingleForward<Dependencies extends DefaultDependencies>(
|
|
423
|
+
fromRoute: string,
|
|
424
|
+
targetRoute: string,
|
|
425
|
+
routes: readonly Route<Dependencies>[],
|
|
426
|
+
batchNames: Set<string>,
|
|
427
|
+
tree: RouteTree,
|
|
428
|
+
): void {
|
|
429
|
+
const existsInTree = routeExistsInTree(tree, targetRoute);
|
|
430
|
+
const existsInBatch = batchNames.has(targetRoute);
|
|
431
|
+
|
|
432
|
+
if (!existsInTree && !existsInBatch) {
|
|
433
|
+
throw new Error(
|
|
434
|
+
`[router.addRoute] forwardTo target "${targetRoute}" does not exist ` +
|
|
435
|
+
`for route "${fromRoute}"`,
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Get source params
|
|
440
|
+
const fromParams = extractParamsFromPaths(
|
|
441
|
+
collectPathsToRoute(routes, fromRoute),
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
// Get target params
|
|
445
|
+
const toParams = getTargetParams(targetRoute, existsInTree, tree, routes);
|
|
446
|
+
|
|
447
|
+
// Check for missing params
|
|
448
|
+
const missingParams = [...toParams].filter((p) => !fromParams.has(p));
|
|
449
|
+
|
|
450
|
+
if (missingParams.length > 0) {
|
|
451
|
+
throw new Error(
|
|
452
|
+
`[router.addRoute] forwardTo target "${targetRoute}" requires params ` +
|
|
453
|
+
`[${missingParams.join(", ")}] that are not available in source route "${fromRoute}"`,
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Resolves a forwardTo chain to its final destination.
|
|
460
|
+
* Detects cycles and enforces max depth.
|
|
461
|
+
*/
|
|
462
|
+
export function resolveForwardChain(
|
|
463
|
+
startRoute: string,
|
|
464
|
+
forwardMap: Record<string, string>,
|
|
465
|
+
maxDepth = 100,
|
|
466
|
+
): string {
|
|
467
|
+
const visited = new Set<string>();
|
|
468
|
+
const chain: string[] = [startRoute];
|
|
469
|
+
let current = startRoute;
|
|
470
|
+
|
|
471
|
+
while (forwardMap[current]) {
|
|
472
|
+
const next = forwardMap[current];
|
|
473
|
+
|
|
474
|
+
if (visited.has(next)) {
|
|
475
|
+
const cycleStart = chain.indexOf(next);
|
|
476
|
+
const cycle = [...chain.slice(cycleStart), next];
|
|
477
|
+
|
|
478
|
+
throw new Error(`Circular forwardTo: ${cycle.join(" → ")}`);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
visited.add(current);
|
|
482
|
+
chain.push(next);
|
|
483
|
+
current = next;
|
|
484
|
+
|
|
485
|
+
if (chain.length > maxDepth) {
|
|
486
|
+
throw new Error(
|
|
487
|
+
`forwardTo chain exceeds maximum depth (${maxDepth}): ${chain.join(" → ")}`,
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return current;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Validates forwardTo targets and cycles BEFORE any modifications.
|
|
497
|
+
* This ensures atomicity - if validation fails, no routes are added.
|
|
498
|
+
*
|
|
499
|
+
* @param routes - Routes to validate
|
|
500
|
+
* @param existingForwardMap - Current forwardMap from router.config
|
|
501
|
+
* @param tree - Current route tree
|
|
502
|
+
*
|
|
503
|
+
* @throws {Error} If forwardTo target doesn't exist
|
|
504
|
+
* @throws {Error} If circular forwardTo is detected
|
|
505
|
+
*/
|
|
506
|
+
export function validateForwardToTargets<
|
|
507
|
+
Dependencies extends DefaultDependencies,
|
|
508
|
+
>(
|
|
509
|
+
routes: readonly Route<Dependencies>[],
|
|
510
|
+
existingForwardMap: Record<string, string>,
|
|
511
|
+
tree: RouteTree,
|
|
512
|
+
): void {
|
|
513
|
+
const batchNames = collectRouteNames(routes);
|
|
514
|
+
const batchForwards = collectForwardMappings(routes);
|
|
515
|
+
|
|
516
|
+
// Merge with existing forwardMap for cycle detection
|
|
517
|
+
const combinedForwardMap: Record<string, string> = { ...existingForwardMap };
|
|
518
|
+
|
|
519
|
+
for (const [from, to] of batchForwards) {
|
|
520
|
+
combinedForwardMap[from] = to;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Validate each forwardTo target exists and params are compatible
|
|
524
|
+
for (const [fromRoute, targetRoute] of batchForwards) {
|
|
525
|
+
validateSingleForward(fromRoute, targetRoute, routes, batchNames, tree);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Check for cycles in the combined forwardMap
|
|
529
|
+
for (const fromRoute of Object.keys(combinedForwardMap)) {
|
|
530
|
+
resolveForwardChain(fromRoute, combinedForwardMap);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// packages/core/src/namespaces/RoutesNamespace/index.ts
|
|
2
|
+
|
|
3
|
+
export { RoutesNamespace } from "./RoutesNamespace";
|
|
4
|
+
|
|
5
|
+
export { DEFAULT_ROUTE_NAME, validatedRouteNames } from "./constants";
|
|
6
|
+
|
|
7
|
+
export { createEmptyConfig } from "./helpers";
|
|
8
|
+
|
|
9
|
+
export type { RouteConfig, RoutesDependencies } from "./types";
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// packages/real-router/modules/core/stateBuilder.ts
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* State Builder Utilities.
|
|
5
|
+
*
|
|
6
|
+
* Functions for building RouteTreeState from raw route segments.
|
|
7
|
+
* This module handles the conversion from low-level route-node data
|
|
8
|
+
* to the higher-level state representation used by real-router.
|
|
9
|
+
*
|
|
10
|
+
* @module core/stateBuilder
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { RouteParams, RouteTreeState } from "route-tree";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Builds a dot-separated route name from segments.
|
|
17
|
+
*
|
|
18
|
+
* @param segments - Array of route segments with names
|
|
19
|
+
* @returns Dot-separated route name (e.g., "users.profile")
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```typescript
|
|
23
|
+
* const segments = [{ name: "users" }, { name: "profile" }];
|
|
24
|
+
* buildNameFromSegments(segments); // "users.profile"
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export function buildNameFromSegments(
|
|
28
|
+
segments: readonly { fullName: string }[],
|
|
29
|
+
): string {
|
|
30
|
+
return segments.at(-1)?.fullName ?? "";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Creates a RouteTreeState from a match result.
|
|
35
|
+
*
|
|
36
|
+
* This function is the primary way to build a RouteTreeState when
|
|
37
|
+
* you have a result from matcher.match().
|
|
38
|
+
*
|
|
39
|
+
* @param matchResult - Result from matcher.match() containing segments and params
|
|
40
|
+
* @param matchResult.segments - Matched route segments
|
|
41
|
+
* @param matchResult.params - Matched route params
|
|
42
|
+
* @param matchResult.meta - Matched route meta
|
|
43
|
+
* @param name - Optional explicit name (if not provided, built from segments)
|
|
44
|
+
* @returns RouteTreeState with name, params, and meta
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```typescript
|
|
48
|
+
* const matchResult = matcher.match("/users/123");
|
|
49
|
+
* if (matchResult) {
|
|
50
|
+
* const state = createRouteState(matchResult);
|
|
51
|
+
* // { name: "users.profile", params: { id: "123" }, meta: {...} }
|
|
52
|
+
* }
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export function createRouteState<P extends RouteParams = RouteParams>(
|
|
56
|
+
matchResult: {
|
|
57
|
+
readonly segments: readonly { fullName: string }[];
|
|
58
|
+
readonly params: Readonly<Record<string, unknown>>;
|
|
59
|
+
readonly meta: Readonly<Record<string, Record<string, "url" | "query">>>;
|
|
60
|
+
},
|
|
61
|
+
name?: string,
|
|
62
|
+
): RouteTreeState<P> {
|
|
63
|
+
const resolvedName = name ?? buildNameFromSegments(matchResult.segments);
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
name: resolvedName,
|
|
67
|
+
params: matchResult.params as P,
|
|
68
|
+
meta: matchResult.meta as Record<string, Record<string, "url" | "query">>,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// packages/core/src/namespaces/RoutesNamespace/types.ts
|
|
2
|
+
|
|
3
|
+
import type { ActivationFnFactory } from "../../types";
|
|
4
|
+
import type {
|
|
5
|
+
DefaultDependencies,
|
|
6
|
+
ForwardToCallback,
|
|
7
|
+
Params,
|
|
8
|
+
SimpleState,
|
|
9
|
+
State,
|
|
10
|
+
StateMetaInput,
|
|
11
|
+
} from "@real-router/types";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Dependencies injected into RoutesNamespace.
|
|
15
|
+
*
|
|
16
|
+
* These are function references from the Router facade,
|
|
17
|
+
* avoiding the need to pass the entire Router object.
|
|
18
|
+
*/
|
|
19
|
+
export interface RoutesDependencies<
|
|
20
|
+
Dependencies extends DefaultDependencies = DefaultDependencies,
|
|
21
|
+
> {
|
|
22
|
+
/** Register canActivate handler for a route */
|
|
23
|
+
addActivateGuard: (
|
|
24
|
+
name: string,
|
|
25
|
+
handler: ActivationFnFactory<Dependencies>,
|
|
26
|
+
) => void;
|
|
27
|
+
|
|
28
|
+
/** Register canDeactivate handler for a route */
|
|
29
|
+
addDeactivateGuard: (
|
|
30
|
+
name: string,
|
|
31
|
+
handler: ActivationFnFactory<Dependencies>,
|
|
32
|
+
) => void;
|
|
33
|
+
|
|
34
|
+
/** Create state object */
|
|
35
|
+
makeState: <P extends Params = Params, MP extends Params = Params>(
|
|
36
|
+
name: string,
|
|
37
|
+
params?: P,
|
|
38
|
+
path?: string,
|
|
39
|
+
meta?: StateMetaInput<MP>,
|
|
40
|
+
) => State<P, MP>;
|
|
41
|
+
|
|
42
|
+
/** Get current router state */
|
|
43
|
+
getState: () => State | undefined;
|
|
44
|
+
|
|
45
|
+
/** Compare two states for equality */
|
|
46
|
+
areStatesEqual: (
|
|
47
|
+
state1: State | undefined,
|
|
48
|
+
state2: State | undefined,
|
|
49
|
+
ignoreQueryParams?: boolean,
|
|
50
|
+
) => boolean;
|
|
51
|
+
|
|
52
|
+
/** Get a dependency by name */
|
|
53
|
+
getDependency: <K extends keyof Dependencies>(name: K) => Dependencies[K];
|
|
54
|
+
|
|
55
|
+
/** Forward state through facade (allows plugin interception) */
|
|
56
|
+
forwardState: <P extends Params = Params>(
|
|
57
|
+
name: string,
|
|
58
|
+
params: P,
|
|
59
|
+
) => SimpleState<P>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Configuration storage for routes.
|
|
64
|
+
* Stores decoders, encoders, default params, and forward mappings.
|
|
65
|
+
*/
|
|
66
|
+
export interface RouteConfig {
|
|
67
|
+
/** Custom param decoders per route */
|
|
68
|
+
decoders: Record<string, (params: Params) => Params>;
|
|
69
|
+
|
|
70
|
+
/** Custom param encoders per route */
|
|
71
|
+
encoders: Record<string, (params: Params) => Params>;
|
|
72
|
+
|
|
73
|
+
/** Default params per route */
|
|
74
|
+
defaultParams: Record<string, Params>;
|
|
75
|
+
|
|
76
|
+
/** Forward mappings (source -> target) */
|
|
77
|
+
forwardMap: Record<string, string>;
|
|
78
|
+
|
|
79
|
+
/** Dynamic forward callbacks (source -> callback) */
|
|
80
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
81
|
+
forwardFnMap: Record<string, ForwardToCallback<any>>;
|
|
82
|
+
}
|