@real-router/core 0.25.4 → 0.26.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 +163 -325
- package/dist/cjs/index.d.ts +47 -178
- 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 +47 -178
- 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 +3 -3
- package/src/Router.ts +84 -574
- package/src/api/cloneRouter.ts +106 -0
- package/src/api/getDependenciesApi.ts +216 -0
- package/src/api/getLifecycleApi.ts +67 -0
- package/src/api/getPluginApi.ts +118 -0
- package/src/api/getRoutesApi.ts +566 -0
- package/src/api/index.ts +16 -0
- package/src/api/types.ts +7 -0
- package/src/getNavigator.ts +5 -2
- package/src/index.ts +17 -3
- package/src/internals.ts +115 -0
- package/src/namespaces/DependenciesNamespace/dependenciesStore.ts +30 -0
- package/src/namespaces/DependenciesNamespace/index.ts +3 -1
- package/src/namespaces/DependenciesNamespace/validators.ts +2 -4
- package/src/namespaces/EventBusNamespace/EventBusNamespace.ts +1 -20
- package/src/namespaces/EventBusNamespace/validators.ts +36 -0
- package/src/namespaces/NavigationNamespace/NavigationNamespace.ts +1 -10
- package/src/namespaces/NavigationNamespace/transition/errorHandling.ts +2 -0
- package/src/namespaces/NavigationNamespace/transition/{executeLifecycleHooks.ts → executeLifecycleGuards.ts} +9 -7
- package/src/namespaces/NavigationNamespace/transition/index.ts +3 -3
- package/src/namespaces/RouteLifecycleNamespace/RouteLifecycleNamespace.ts +1 -16
- package/src/namespaces/RoutesNamespace/RoutesNamespace.ts +133 -1089
- package/src/namespaces/RoutesNamespace/forwardToValidation.ts +411 -0
- package/src/namespaces/RoutesNamespace/helpers.ts +1 -407
- package/src/namespaces/RoutesNamespace/index.ts +2 -0
- package/src/namespaces/RoutesNamespace/routesStore.ts +388 -0
- package/src/namespaces/RoutesNamespace/validators.ts +209 -3
- package/src/namespaces/StateNamespace/StateNamespace.ts +1 -44
- package/src/namespaces/StateNamespace/validators.ts +46 -0
- package/src/namespaces/index.ts +3 -5
- package/src/types.ts +12 -138
- package/src/wiring/RouterWiringBuilder.ts +30 -36
- package/src/wiring/types.ts +3 -6
- package/src/wiring/wireRouter.ts +0 -1
- package/src/namespaces/CloneNamespace/CloneNamespace.ts +0 -120
- package/src/namespaces/CloneNamespace/index.ts +0 -3
- package/src/namespaces/CloneNamespace/types.ts +0 -42
- package/src/namespaces/DependenciesNamespace/DependenciesNamespace.ts +0 -248
- package/src/namespaces/RoutesNamespace/stateBuilder.ts +0 -70
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
// packages/core/src/namespaces/RoutesNamespace/routesStore.ts
|
|
2
|
+
|
|
3
|
+
import { logger } from "@real-router/logger";
|
|
4
|
+
import { createMatcher, createRouteTree, nodeToDefinition } from "route-tree";
|
|
5
|
+
|
|
6
|
+
import { DEFAULT_ROUTE_NAME } from "./constants";
|
|
7
|
+
import { resolveForwardChain } from "./forwardToValidation";
|
|
8
|
+
import { createEmptyConfig, sanitizeRoute } from "./helpers";
|
|
9
|
+
import { validateRoutes } from "./validators";
|
|
10
|
+
|
|
11
|
+
import type { RouteConfig, RoutesDependencies } from "./types";
|
|
12
|
+
import type { GuardFnFactory, Route } from "../../types";
|
|
13
|
+
import type { RouteLifecycleNamespace } from "../RouteLifecycleNamespace";
|
|
14
|
+
import type { DefaultDependencies, Params } from "@real-router/types";
|
|
15
|
+
import type {
|
|
16
|
+
CreateMatcherOptions,
|
|
17
|
+
Matcher,
|
|
18
|
+
RouteDefinition,
|
|
19
|
+
RouteTree,
|
|
20
|
+
} from "route-tree";
|
|
21
|
+
|
|
22
|
+
// =============================================================================
|
|
23
|
+
// Interfaces
|
|
24
|
+
// =============================================================================
|
|
25
|
+
|
|
26
|
+
export interface RoutesStore<
|
|
27
|
+
Dependencies extends DefaultDependencies = DefaultDependencies,
|
|
28
|
+
> {
|
|
29
|
+
readonly definitions: RouteDefinition[];
|
|
30
|
+
readonly config: RouteConfig;
|
|
31
|
+
tree: RouteTree;
|
|
32
|
+
matcher: Matcher;
|
|
33
|
+
resolvedForwardMap: Record<string, string>;
|
|
34
|
+
routeCustomFields: Record<string, Record<string, unknown>>;
|
|
35
|
+
rootPath: string;
|
|
36
|
+
readonly matcherOptions: CreateMatcherOptions | undefined;
|
|
37
|
+
depsStore: RoutesDependencies<Dependencies> | undefined;
|
|
38
|
+
lifecycleNamespace: RouteLifecycleNamespace<Dependencies> | undefined;
|
|
39
|
+
readonly pendingCanActivate: Map<string, GuardFnFactory<Dependencies>>;
|
|
40
|
+
readonly pendingCanDeactivate: Map<string, GuardFnFactory<Dependencies>>;
|
|
41
|
+
readonly treeOperations: {
|
|
42
|
+
readonly commitTreeChanges: (
|
|
43
|
+
store: RoutesStore<Dependencies>,
|
|
44
|
+
noValidate: boolean,
|
|
45
|
+
) => void;
|
|
46
|
+
readonly resetStore: (store: RoutesStore<Dependencies>) => void;
|
|
47
|
+
readonly nodeToDefinition: (node: RouteTree) => RouteDefinition;
|
|
48
|
+
readonly validateRoutes: (
|
|
49
|
+
routes: Route<Dependencies>[],
|
|
50
|
+
tree?: RouteTree,
|
|
51
|
+
forwardMap?: Record<string, string>,
|
|
52
|
+
parentName?: string,
|
|
53
|
+
) => void;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// =============================================================================
|
|
58
|
+
// Tree operations
|
|
59
|
+
// =============================================================================
|
|
60
|
+
|
|
61
|
+
function rebuildTree(
|
|
62
|
+
definitions: RouteDefinition[],
|
|
63
|
+
rootPath: string,
|
|
64
|
+
matcherOptions: CreateMatcherOptions | undefined,
|
|
65
|
+
): { tree: RouteTree; matcher: Matcher } {
|
|
66
|
+
const tree = createRouteTree(DEFAULT_ROUTE_NAME, rootPath, definitions);
|
|
67
|
+
const matcher = createMatcher(matcherOptions);
|
|
68
|
+
|
|
69
|
+
matcher.registerTree(tree);
|
|
70
|
+
|
|
71
|
+
return { tree, matcher };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function commitTreeChanges<
|
|
75
|
+
Dependencies extends DefaultDependencies = DefaultDependencies,
|
|
76
|
+
>(store: RoutesStore<Dependencies>, noValidate: boolean): void {
|
|
77
|
+
const result = rebuildTree(
|
|
78
|
+
store.definitions,
|
|
79
|
+
store.rootPath,
|
|
80
|
+
store.matcherOptions,
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
store.tree = result.tree;
|
|
84
|
+
store.matcher = result.matcher;
|
|
85
|
+
store.resolvedForwardMap = refreshForwardMap(store.config, noValidate);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function rebuildTreeInPlace<
|
|
89
|
+
Dependencies extends DefaultDependencies = DefaultDependencies,
|
|
90
|
+
>(store: RoutesStore<Dependencies>): void {
|
|
91
|
+
const result = rebuildTree(
|
|
92
|
+
store.definitions,
|
|
93
|
+
store.rootPath,
|
|
94
|
+
store.matcherOptions,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
store.tree = result.tree;
|
|
98
|
+
store.matcher = result.matcher;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// =============================================================================
|
|
102
|
+
// Store reset
|
|
103
|
+
// =============================================================================
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Clears all routes and resets config.
|
|
107
|
+
* Does NOT clear lifecycle handlers or state — caller handles that.
|
|
108
|
+
*/
|
|
109
|
+
export function resetStore<
|
|
110
|
+
Dependencies extends DefaultDependencies = DefaultDependencies,
|
|
111
|
+
>(store: RoutesStore<Dependencies>): void {
|
|
112
|
+
store.definitions.length = 0;
|
|
113
|
+
|
|
114
|
+
Object.assign(store.config, createEmptyConfig());
|
|
115
|
+
|
|
116
|
+
store.resolvedForwardMap = Object.create(null) as Record<string, string>;
|
|
117
|
+
store.routeCustomFields = Object.create(null) as Record<
|
|
118
|
+
string,
|
|
119
|
+
Record<string, unknown>
|
|
120
|
+
>;
|
|
121
|
+
|
|
122
|
+
rebuildTreeInPlace(store);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// =============================================================================
|
|
126
|
+
// Forward map
|
|
127
|
+
// =============================================================================
|
|
128
|
+
|
|
129
|
+
export function refreshForwardMap(
|
|
130
|
+
config: RouteConfig,
|
|
131
|
+
noValidate: boolean,
|
|
132
|
+
): Record<string, string> {
|
|
133
|
+
if (noValidate) {
|
|
134
|
+
return cacheForwardMap(config);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return validateAndCacheForwardMap(config);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function validateAndCacheForwardMap(
|
|
141
|
+
config: RouteConfig,
|
|
142
|
+
): Record<string, string> {
|
|
143
|
+
const map = Object.create(null) as Record<string, string>;
|
|
144
|
+
|
|
145
|
+
for (const fromRoute of Object.keys(config.forwardMap)) {
|
|
146
|
+
map[fromRoute] = resolveForwardChain(fromRoute, config.forwardMap);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return map;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function cacheForwardMap(config: RouteConfig): Record<string, string> {
|
|
153
|
+
const map = Object.create(null) as Record<string, string>;
|
|
154
|
+
|
|
155
|
+
for (const fromRoute of Object.keys(config.forwardMap)) {
|
|
156
|
+
let current = fromRoute;
|
|
157
|
+
|
|
158
|
+
while (config.forwardMap[current]) {
|
|
159
|
+
current = config.forwardMap[current];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
map[fromRoute] = current;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return map;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// =============================================================================
|
|
169
|
+
// Route handler registration
|
|
170
|
+
// =============================================================================
|
|
171
|
+
|
|
172
|
+
function registerForwardTo<Dependencies extends DefaultDependencies>(
|
|
173
|
+
route: Route<Dependencies>,
|
|
174
|
+
fullName: string,
|
|
175
|
+
config: RouteConfig,
|
|
176
|
+
): void {
|
|
177
|
+
if (route.canActivate) {
|
|
178
|
+
/* v8 ignore next -- @preserve: edge case, both string and function tested separately */
|
|
179
|
+
const forwardTarget =
|
|
180
|
+
typeof route.forwardTo === "string" ? route.forwardTo : "[dynamic]";
|
|
181
|
+
|
|
182
|
+
logger.warn(
|
|
183
|
+
"real-router",
|
|
184
|
+
`Route "${fullName}" has both forwardTo and canActivate. ` +
|
|
185
|
+
`canActivate will be ignored because forwardTo creates a redirect (industry standard). ` +
|
|
186
|
+
`Move canActivate to the target route "${forwardTarget}".`,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (route.canDeactivate) {
|
|
191
|
+
/* v8 ignore next -- @preserve: edge case, both string and function tested separately */
|
|
192
|
+
const forwardTarget =
|
|
193
|
+
typeof route.forwardTo === "string" ? route.forwardTo : "[dynamic]";
|
|
194
|
+
|
|
195
|
+
logger.warn(
|
|
196
|
+
"real-router",
|
|
197
|
+
`Route "${fullName}" has both forwardTo and canDeactivate. ` +
|
|
198
|
+
`canDeactivate will be ignored because forwardTo creates a redirect (industry standard). ` +
|
|
199
|
+
`Move canDeactivate to the target route "${forwardTarget}".`,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Async validation ALWAYS runs (even with noValidate=true)
|
|
204
|
+
if (typeof route.forwardTo === "function") {
|
|
205
|
+
const isNativeAsync =
|
|
206
|
+
(route.forwardTo as { constructor: { name: string } }).constructor
|
|
207
|
+
.name === "AsyncFunction";
|
|
208
|
+
const isTranspiledAsync = route.forwardTo.toString().includes("__awaiter");
|
|
209
|
+
|
|
210
|
+
if (isNativeAsync || isTranspiledAsync) {
|
|
211
|
+
throw new TypeError(
|
|
212
|
+
`forwardTo callback cannot be async for route "${fullName}". ` +
|
|
213
|
+
`Async functions break matchPath/buildPath.`,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// forwardTo is guaranteed to exist at this point
|
|
219
|
+
if (typeof route.forwardTo === "string") {
|
|
220
|
+
config.forwardMap[fullName] = route.forwardTo;
|
|
221
|
+
} else {
|
|
222
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
223
|
+
config.forwardFnMap[fullName] = route.forwardTo!;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function registerSingleRouteHandlers<Dependencies extends DefaultDependencies>(
|
|
228
|
+
route: Route<Dependencies>,
|
|
229
|
+
fullName: string,
|
|
230
|
+
config: RouteConfig,
|
|
231
|
+
routeCustomFields: Record<string, Record<string, unknown>>,
|
|
232
|
+
pendingCanActivate: Map<string, GuardFnFactory<Dependencies>>,
|
|
233
|
+
pendingCanDeactivate: Map<string, GuardFnFactory<Dependencies>>,
|
|
234
|
+
depsStore: RoutesDependencies<Dependencies> | undefined,
|
|
235
|
+
): void {
|
|
236
|
+
const standardKeys = new Set([
|
|
237
|
+
"name",
|
|
238
|
+
"path",
|
|
239
|
+
"children",
|
|
240
|
+
"canActivate",
|
|
241
|
+
"canDeactivate",
|
|
242
|
+
"forwardTo",
|
|
243
|
+
"encodeParams",
|
|
244
|
+
"decodeParams",
|
|
245
|
+
"defaultParams",
|
|
246
|
+
]);
|
|
247
|
+
const customFields = Object.fromEntries(
|
|
248
|
+
Object.entries(route).filter(([k]) => !standardKeys.has(k)),
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
if (Object.keys(customFields).length > 0) {
|
|
252
|
+
routeCustomFields[fullName] = customFields;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (route.canActivate) {
|
|
256
|
+
if (depsStore) {
|
|
257
|
+
depsStore.addActivateGuard(fullName, route.canActivate);
|
|
258
|
+
} else {
|
|
259
|
+
pendingCanActivate.set(fullName, route.canActivate);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (route.canDeactivate) {
|
|
264
|
+
if (depsStore) {
|
|
265
|
+
depsStore.addDeactivateGuard(fullName, route.canDeactivate);
|
|
266
|
+
} else {
|
|
267
|
+
pendingCanDeactivate.set(fullName, route.canDeactivate);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (route.forwardTo) {
|
|
272
|
+
registerForwardTo(route, fullName, config);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (route.decodeParams) {
|
|
276
|
+
config.decoders[fullName] = (params: Params): Params =>
|
|
277
|
+
route.decodeParams?.(params) ?? params;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (route.encodeParams) {
|
|
281
|
+
config.encoders[fullName] = (params: Params): Params =>
|
|
282
|
+
route.encodeParams?.(params) ?? params;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (route.defaultParams) {
|
|
286
|
+
config.defaultParams[fullName] = route.defaultParams;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function registerAllRouteHandlers<
|
|
291
|
+
Dependencies extends DefaultDependencies,
|
|
292
|
+
>(
|
|
293
|
+
routes: readonly Route<Dependencies>[],
|
|
294
|
+
config: RouteConfig,
|
|
295
|
+
routeCustomFields: Record<string, Record<string, unknown>>,
|
|
296
|
+
pendingCanActivate: Map<string, GuardFnFactory<Dependencies>>,
|
|
297
|
+
pendingCanDeactivate: Map<string, GuardFnFactory<Dependencies>>,
|
|
298
|
+
depsStore: RoutesDependencies<Dependencies> | undefined,
|
|
299
|
+
parentName = "",
|
|
300
|
+
): void {
|
|
301
|
+
for (const route of routes) {
|
|
302
|
+
const fullName = parentName ? `${parentName}.${route.name}` : route.name;
|
|
303
|
+
|
|
304
|
+
registerSingleRouteHandlers(
|
|
305
|
+
route,
|
|
306
|
+
fullName,
|
|
307
|
+
config,
|
|
308
|
+
routeCustomFields,
|
|
309
|
+
pendingCanActivate,
|
|
310
|
+
pendingCanDeactivate,
|
|
311
|
+
depsStore,
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
if (route.children) {
|
|
315
|
+
registerAllRouteHandlers(
|
|
316
|
+
route.children,
|
|
317
|
+
config,
|
|
318
|
+
routeCustomFields,
|
|
319
|
+
pendingCanActivate,
|
|
320
|
+
pendingCanDeactivate,
|
|
321
|
+
depsStore,
|
|
322
|
+
fullName,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// =============================================================================
|
|
329
|
+
// Factory
|
|
330
|
+
// =============================================================================
|
|
331
|
+
|
|
332
|
+
export function createRoutesStore<
|
|
333
|
+
Dependencies extends DefaultDependencies = DefaultDependencies,
|
|
334
|
+
>(
|
|
335
|
+
routes: Route<Dependencies>[],
|
|
336
|
+
noValidate: boolean,
|
|
337
|
+
matcherOptions?: CreateMatcherOptions,
|
|
338
|
+
): RoutesStore<Dependencies> {
|
|
339
|
+
const definitions: RouteDefinition[] = [];
|
|
340
|
+
const config: RouteConfig = createEmptyConfig();
|
|
341
|
+
const routeCustomFields: Record<
|
|
342
|
+
string,
|
|
343
|
+
Record<string, unknown>
|
|
344
|
+
> = Object.create(null) as Record<string, Record<string, unknown>>;
|
|
345
|
+
const pendingCanActivate = new Map<string, GuardFnFactory<Dependencies>>();
|
|
346
|
+
const pendingCanDeactivate = new Map<string, GuardFnFactory<Dependencies>>();
|
|
347
|
+
|
|
348
|
+
for (const route of routes) {
|
|
349
|
+
definitions.push(sanitizeRoute(route));
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const { tree, matcher } = rebuildTree(definitions, "", matcherOptions);
|
|
353
|
+
|
|
354
|
+
registerAllRouteHandlers(
|
|
355
|
+
routes,
|
|
356
|
+
config,
|
|
357
|
+
routeCustomFields,
|
|
358
|
+
pendingCanActivate,
|
|
359
|
+
pendingCanDeactivate,
|
|
360
|
+
undefined,
|
|
361
|
+
"",
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
const resolvedForwardMap: Record<string, string> = noValidate
|
|
365
|
+
? cacheForwardMap(config)
|
|
366
|
+
: validateAndCacheForwardMap(config);
|
|
367
|
+
|
|
368
|
+
return {
|
|
369
|
+
definitions,
|
|
370
|
+
config,
|
|
371
|
+
tree,
|
|
372
|
+
matcher,
|
|
373
|
+
resolvedForwardMap,
|
|
374
|
+
routeCustomFields,
|
|
375
|
+
rootPath: "",
|
|
376
|
+
matcherOptions,
|
|
377
|
+
depsStore: undefined,
|
|
378
|
+
lifecycleNamespace: undefined,
|
|
379
|
+
pendingCanActivate,
|
|
380
|
+
pendingCanDeactivate,
|
|
381
|
+
treeOperations: {
|
|
382
|
+
commitTreeChanges,
|
|
383
|
+
resetStore,
|
|
384
|
+
nodeToDefinition,
|
|
385
|
+
validateRoutes,
|
|
386
|
+
},
|
|
387
|
+
};
|
|
388
|
+
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* Extracted from RoutesNamespace class for better separation of concerns.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { logger } from "@real-router/logger";
|
|
10
11
|
import { validateRoute } from "route-tree";
|
|
11
12
|
import {
|
|
12
13
|
isString,
|
|
@@ -15,11 +16,19 @@ import {
|
|
|
15
16
|
getTypeDescription,
|
|
16
17
|
} from "type-guards";
|
|
17
18
|
|
|
18
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
resolveForwardChain,
|
|
21
|
+
validateForwardToTargets,
|
|
22
|
+
validateRouteProperties,
|
|
23
|
+
} from "./forwardToValidation";
|
|
19
24
|
|
|
25
|
+
import type { RouteConfig } from "./types";
|
|
20
26
|
import type { Route, RouteConfigUpdate } from "../../types";
|
|
21
|
-
import type {
|
|
22
|
-
|
|
27
|
+
import type {
|
|
28
|
+
DefaultDependencies,
|
|
29
|
+
ForwardToCallback,
|
|
30
|
+
} from "@real-router/types";
|
|
31
|
+
import type { Matcher, RouteTree } from "route-tree";
|
|
23
32
|
|
|
24
33
|
/**
|
|
25
34
|
* Validates removeRoute arguments.
|
|
@@ -329,3 +338,200 @@ export function validateRoutes<Dependencies extends DefaultDependencies>(
|
|
|
329
338
|
validateForwardToTargets(routes, forwardMap, tree);
|
|
330
339
|
}
|
|
331
340
|
}
|
|
341
|
+
|
|
342
|
+
// ============================================================================
|
|
343
|
+
// Instance-level validators (moved from routesCrud.ts)
|
|
344
|
+
// ============================================================================
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Collects URL params from segments into a Set.
|
|
348
|
+
*/
|
|
349
|
+
function collectUrlParams(segments: readonly RouteTree[]): Set<string> {
|
|
350
|
+
const params = new Set<string>();
|
|
351
|
+
|
|
352
|
+
for (const segment of segments) {
|
|
353
|
+
for (const param of segment.paramMeta.urlParams) {
|
|
354
|
+
params.add(param);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return params;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Validates removeRoute constraints.
|
|
363
|
+
* Returns false if removal should be blocked (route is active).
|
|
364
|
+
* Logs warnings for edge cases.
|
|
365
|
+
*
|
|
366
|
+
* @param name - Route name to remove
|
|
367
|
+
* @param currentStateName - Current active route name (or undefined)
|
|
368
|
+
* @param isNavigating - Whether navigation is in progress
|
|
369
|
+
* @returns true if removal can proceed, false if blocked
|
|
370
|
+
*/
|
|
371
|
+
export function validateRemoveRoute(
|
|
372
|
+
name: string,
|
|
373
|
+
currentStateName: string | undefined,
|
|
374
|
+
isNavigating: boolean,
|
|
375
|
+
): boolean {
|
|
376
|
+
// Check if trying to remove currently active route (or its parent)
|
|
377
|
+
if (currentStateName) {
|
|
378
|
+
const isExactMatch = currentStateName === name;
|
|
379
|
+
const isParentOfCurrent = currentStateName.startsWith(`${name}.`);
|
|
380
|
+
|
|
381
|
+
if (isExactMatch || isParentOfCurrent) {
|
|
382
|
+
const suffix = isExactMatch ? "" : ` (current: "${currentStateName}")`;
|
|
383
|
+
|
|
384
|
+
logger.warn(
|
|
385
|
+
"router.removeRoute",
|
|
386
|
+
`Cannot remove route "${name}" — it is currently active${suffix}. Navigate away first.`,
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Warn if navigation is in progress (but allow removal)
|
|
394
|
+
if (isNavigating) {
|
|
395
|
+
logger.warn(
|
|
396
|
+
"router.removeRoute",
|
|
397
|
+
`Route "${name}" removed while navigation is in progress. This may cause unexpected behavior.`,
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return true;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Validates clearRoutes operation.
|
|
406
|
+
* Returns false if operation should be blocked (navigation in progress).
|
|
407
|
+
*
|
|
408
|
+
* @param isNavigating - Whether navigation is in progress
|
|
409
|
+
* @returns true if clearRoutes can proceed, false if blocked
|
|
410
|
+
*/
|
|
411
|
+
export function validateClearRoutes(isNavigating: boolean): boolean {
|
|
412
|
+
if (isNavigating) {
|
|
413
|
+
logger.error(
|
|
414
|
+
"router.clearRoutes",
|
|
415
|
+
"Cannot clear routes while navigation is in progress. Wait for navigation to complete.",
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
return false;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return true;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Validates that forwardTo target doesn't require params that source doesn't have.
|
|
426
|
+
*
|
|
427
|
+
* @param sourceName - Source route name
|
|
428
|
+
* @param targetName - Target route name
|
|
429
|
+
* @param matcher - Current route matcher
|
|
430
|
+
*/
|
|
431
|
+
export function validateForwardToParamCompatibility(
|
|
432
|
+
sourceName: string,
|
|
433
|
+
targetName: string,
|
|
434
|
+
matcher: Matcher,
|
|
435
|
+
): void {
|
|
436
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
437
|
+
const sourceSegments = matcher.getSegmentsByName(
|
|
438
|
+
sourceName,
|
|
439
|
+
)! as readonly RouteTree[];
|
|
440
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
441
|
+
const targetSegments = matcher.getSegmentsByName(
|
|
442
|
+
targetName,
|
|
443
|
+
)! as readonly RouteTree[];
|
|
444
|
+
|
|
445
|
+
// Get source URL params as a Set for O(1) lookup
|
|
446
|
+
const sourceParams = collectUrlParams(sourceSegments);
|
|
447
|
+
|
|
448
|
+
// Build target URL params array (inline — no separate helper needed)
|
|
449
|
+
const targetParams: string[] = [];
|
|
450
|
+
|
|
451
|
+
for (const segment of targetSegments) {
|
|
452
|
+
for (const param of segment.paramMeta.urlParams) {
|
|
453
|
+
targetParams.push(param);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Check if target requires params that source doesn't have
|
|
458
|
+
const missingParams = targetParams.filter(
|
|
459
|
+
(param) => !sourceParams.has(param),
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
if (missingParams.length > 0) {
|
|
463
|
+
throw new Error(
|
|
464
|
+
`[real-router] forwardTo target "${targetName}" requires params ` +
|
|
465
|
+
`[${missingParams.join(", ")}] that are not available in source route "${sourceName}"`,
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Validates that adding forwardTo doesn't create a cycle.
|
|
472
|
+
* Creates a test map with the new entry and uses resolveForwardChain
|
|
473
|
+
* to detect cycles before any mutation happens.
|
|
474
|
+
*
|
|
475
|
+
* @param sourceName - Source route name
|
|
476
|
+
* @param targetName - Target route name
|
|
477
|
+
* @param config - Current route config (forwardMap read-only in this call)
|
|
478
|
+
*/
|
|
479
|
+
export function validateForwardToCycle(
|
|
480
|
+
sourceName: string,
|
|
481
|
+
targetName: string,
|
|
482
|
+
config: RouteConfig,
|
|
483
|
+
): void {
|
|
484
|
+
// Create a test map with the new entry to validate BEFORE mutation
|
|
485
|
+
const testMap = {
|
|
486
|
+
...config.forwardMap,
|
|
487
|
+
[sourceName]: targetName,
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
// resolveForwardChain will throw if cycle is detected or max depth exceeded
|
|
491
|
+
resolveForwardChain(sourceName, testMap);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Validates updateRoute instance-level constraints (route existence, forwardTo).
|
|
496
|
+
*
|
|
497
|
+
* @param name - Route name (already validated by static method)
|
|
498
|
+
* @param forwardTo - Cached forwardTo value
|
|
499
|
+
* @param hasRoute - Function to check route existence
|
|
500
|
+
* @param matcher - Current route matcher
|
|
501
|
+
* @param config - Current route config
|
|
502
|
+
*/
|
|
503
|
+
export function validateUpdateRoute<
|
|
504
|
+
Dependencies extends DefaultDependencies = DefaultDependencies,
|
|
505
|
+
>(
|
|
506
|
+
name: string,
|
|
507
|
+
forwardTo: string | ForwardToCallback<Dependencies> | null | undefined,
|
|
508
|
+
hasRoute: (n: string) => boolean,
|
|
509
|
+
matcher: Matcher,
|
|
510
|
+
config: RouteConfig,
|
|
511
|
+
): void {
|
|
512
|
+
// Validate route exists
|
|
513
|
+
if (!hasRoute(name)) {
|
|
514
|
+
throw new ReferenceError(
|
|
515
|
+
`[real-router] updateRoute: route "${name}" does not exist`,
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Validate forwardTo target exists and is valid (only for string forwardTo)
|
|
520
|
+
if (
|
|
521
|
+
forwardTo !== undefined &&
|
|
522
|
+
forwardTo !== null &&
|
|
523
|
+
typeof forwardTo === "string"
|
|
524
|
+
) {
|
|
525
|
+
if (!hasRoute(forwardTo)) {
|
|
526
|
+
throw new Error(
|
|
527
|
+
`[real-router] updateRoute: forwardTo target "${forwardTo}" does not exist`,
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Check forwardTo param compatibility
|
|
532
|
+
validateForwardToParamCompatibility(name, forwardTo, matcher);
|
|
533
|
+
|
|
534
|
+
// Check for cycle detection
|
|
535
|
+
validateForwardToCycle(name, forwardTo, config);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
// packages/core/src/namespaces/StateNamespace/StateNamespace.ts
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
getTypeDescription,
|
|
5
|
-
isParams,
|
|
6
|
-
isString,
|
|
7
|
-
validateState,
|
|
8
|
-
} from "type-guards";
|
|
3
|
+
import { getTypeDescription, validateState } from "type-guards";
|
|
9
4
|
|
|
10
5
|
import { areParamValuesEqual, getUrlParamsFromMeta } from "./helpers";
|
|
11
6
|
import { constants } from "../../constants";
|
|
@@ -56,44 +51,6 @@ export class StateNamespace {
|
|
|
56
51
|
// Static validation methods (called by facade before instance methods)
|
|
57
52
|
// =========================================================================
|
|
58
53
|
|
|
59
|
-
/**
|
|
60
|
-
* Validates makeState arguments.
|
|
61
|
-
*/
|
|
62
|
-
static validateMakeStateArgs(
|
|
63
|
-
name: unknown,
|
|
64
|
-
params: unknown,
|
|
65
|
-
path: unknown,
|
|
66
|
-
forceId: unknown,
|
|
67
|
-
): void {
|
|
68
|
-
// Validate name is a string
|
|
69
|
-
if (!isString(name)) {
|
|
70
|
-
throw new TypeError(
|
|
71
|
-
`[router.makeState] Invalid name: ${getTypeDescription(name)}. Expected string.`,
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Validate params if provided
|
|
76
|
-
if (params !== undefined && !isParams(params)) {
|
|
77
|
-
throw new TypeError(
|
|
78
|
-
`[router.makeState] Invalid params: ${getTypeDescription(params)}. Expected plain object.`,
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Validate path if provided
|
|
83
|
-
if (path !== undefined && !isString(path)) {
|
|
84
|
-
throw new TypeError(
|
|
85
|
-
`[router.makeState] Invalid path: ${getTypeDescription(path)}. Expected string.`,
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Validate forceId if provided
|
|
90
|
-
if (forceId !== undefined && typeof forceId !== "number") {
|
|
91
|
-
throw new TypeError(
|
|
92
|
-
`[router.makeState] Invalid forceId: ${getTypeDescription(forceId)}. Expected number.`,
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
54
|
/**
|
|
98
55
|
* Validates areStatesEqual arguments.
|
|
99
56
|
*/
|