@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
|
@@ -1,49 +1,20 @@
|
|
|
1
1
|
// packages/core/src/namespaces/RoutesNamespace/RoutesNamespace.ts
|
|
2
2
|
|
|
3
|
-
import { logger } from "@real-router/logger";
|
|
4
|
-
import {
|
|
5
|
-
createMatcher,
|
|
6
|
-
createRouteTree,
|
|
7
|
-
nodeToDefinition,
|
|
8
|
-
routeTreeToDefinitions,
|
|
9
|
-
} from "route-tree";
|
|
10
3
|
import { isString, validateRouteName } from "type-guards";
|
|
11
4
|
|
|
12
5
|
import { DEFAULT_ROUTE_NAME, validatedRouteNames } from "./constants";
|
|
6
|
+
import { paramsMatch, paramsMatchExcluding } from "./helpers";
|
|
13
7
|
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
removeFromDefinitions,
|
|
19
|
-
resolveForwardChain,
|
|
20
|
-
sanitizeRoute,
|
|
21
|
-
} from "./helpers";
|
|
22
|
-
import { createRouteState } from "./stateBuilder";
|
|
23
|
-
import {
|
|
24
|
-
validateRemoveRouteArgs,
|
|
25
|
-
validateSetRootPathArgs,
|
|
26
|
-
validateAddRouteArgs,
|
|
27
|
-
validateParentOption,
|
|
28
|
-
validateIsActiveRouteArgs,
|
|
29
|
-
validateStateBuilderArgs,
|
|
30
|
-
validateUpdateRouteBasicArgs,
|
|
31
|
-
validateUpdateRoutePropertyTypes,
|
|
32
|
-
validateBuildPathArgs,
|
|
33
|
-
validateMatchPathArgs,
|
|
34
|
-
validateShouldUpdateNodeArgs,
|
|
35
|
-
validateRoutes,
|
|
36
|
-
} from "./validators";
|
|
8
|
+
createRoutesStore,
|
|
9
|
+
rebuildTreeInPlace,
|
|
10
|
+
resetStore,
|
|
11
|
+
} from "./routesStore";
|
|
37
12
|
import { constants } from "../../constants";
|
|
38
13
|
import { getTransitionPath } from "../../transitionPath";
|
|
39
14
|
|
|
40
|
-
import type {
|
|
41
|
-
import type {
|
|
42
|
-
|
|
43
|
-
GuardFnFactory,
|
|
44
|
-
Route,
|
|
45
|
-
RouteConfigUpdate,
|
|
46
|
-
} from "../../types";
|
|
15
|
+
import type { RoutesStore } from "./routesStore";
|
|
16
|
+
import type { RoutesDependencies } from "./types";
|
|
17
|
+
import type { BuildStateResultWithSegments, Route } from "../../types";
|
|
47
18
|
import type { RouteLifecycleNamespace } from "../RouteLifecycleNamespace";
|
|
48
19
|
import type {
|
|
49
20
|
DefaultDependencies,
|
|
@@ -54,14 +25,48 @@ import type {
|
|
|
54
25
|
} from "@real-router/types";
|
|
55
26
|
import type {
|
|
56
27
|
CreateMatcherOptions,
|
|
57
|
-
|
|
58
|
-
RouteDefinition,
|
|
28
|
+
RouteParams,
|
|
59
29
|
RouteTree,
|
|
60
30
|
RouteTreeState,
|
|
61
31
|
} from "route-tree";
|
|
62
32
|
|
|
63
33
|
const EMPTY_OPTIONS = Object.freeze({});
|
|
64
34
|
|
|
35
|
+
function collectUrlParamsArray(segments: readonly RouteTree[]): string[] {
|
|
36
|
+
const params: string[] = [];
|
|
37
|
+
|
|
38
|
+
for (const segment of segments) {
|
|
39
|
+
for (const param of segment.paramMeta.urlParams) {
|
|
40
|
+
params.push(param);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return params;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function buildNameFromSegments(
|
|
48
|
+
segments: readonly { fullName: string }[],
|
|
49
|
+
): string {
|
|
50
|
+
return segments.at(-1)?.fullName ?? "";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function createRouteState<P extends RouteParams = RouteParams>(
|
|
54
|
+
matchResult: {
|
|
55
|
+
readonly segments: readonly { fullName: string }[];
|
|
56
|
+
readonly params: Readonly<Record<string, unknown>>;
|
|
57
|
+
readonly meta: Readonly<Record<string, Record<string, "url" | "query">>>;
|
|
58
|
+
},
|
|
59
|
+
name?: string,
|
|
60
|
+
): RouteTreeState<P> {
|
|
61
|
+
const resolvedName = name ?? buildNameFromSegments(matchResult.segments);
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
name: resolvedName,
|
|
65
|
+
params: matchResult.params as P,
|
|
66
|
+
meta: matchResult.meta as Record<string, Record<string, "url" | "query">>,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
65
70
|
/**
|
|
66
71
|
* Independent namespace for managing routes.
|
|
67
72
|
*
|
|
@@ -71,188 +76,59 @@ const EMPTY_OPTIONS = Object.freeze({});
|
|
|
71
76
|
export class RoutesNamespace<
|
|
72
77
|
Dependencies extends DefaultDependencies = DefaultDependencies,
|
|
73
78
|
> {
|
|
74
|
-
|
|
75
|
-
// Private instance fields
|
|
76
|
-
// =========================================================================
|
|
79
|
+
readonly #store: RoutesStore<Dependencies>;
|
|
77
80
|
|
|
78
|
-
readonly #definitions: RouteDefinition[] = [];
|
|
79
|
-
readonly #config: RouteConfig = createEmptyConfig();
|
|
80
|
-
#resolvedForwardMap: Record<string, string> = Object.create(null) as Record<
|
|
81
|
-
string,
|
|
82
|
-
string
|
|
83
|
-
>;
|
|
84
|
-
|
|
85
|
-
#routeCustomFields: Record<string, Record<string, unknown>> = Object.create(
|
|
86
|
-
null,
|
|
87
|
-
) as Record<string, Record<string, unknown>>;
|
|
88
|
-
|
|
89
|
-
// Pending canActivate handlers that need to be registered after router is set
|
|
90
|
-
// Key: route name, Value: canActivate factory
|
|
91
|
-
readonly #pendingCanActivate = new Map<
|
|
92
|
-
string,
|
|
93
|
-
GuardFnFactory<Dependencies>
|
|
94
|
-
>();
|
|
95
|
-
|
|
96
|
-
// Pending canDeactivate handlers that need to be registered after router is set
|
|
97
|
-
// Key: route name, Value: canDeactivate factory
|
|
98
|
-
readonly #pendingCanDeactivate = new Map<
|
|
99
|
-
string,
|
|
100
|
-
GuardFnFactory<Dependencies>
|
|
101
|
-
>();
|
|
102
|
-
|
|
103
|
-
#rootPath = "";
|
|
104
|
-
#tree: RouteTree;
|
|
105
|
-
#matcher: Matcher;
|
|
106
|
-
readonly #matcherOptions: CreateMatcherOptions | undefined;
|
|
107
|
-
|
|
108
|
-
// Dependencies injected via setDependencies (for facade method calls)
|
|
109
|
-
#depsStore: RoutesDependencies<Dependencies> | undefined;
|
|
110
|
-
|
|
111
|
-
// Lifecycle handlers reference (set after construction)
|
|
112
|
-
#lifecycleNamespace!: RouteLifecycleNamespace<Dependencies>;
|
|
113
|
-
|
|
114
|
-
// When true, skips validation for production performance
|
|
115
|
-
readonly #noValidate: boolean;
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Gets dependencies or throws if not initialized.
|
|
119
|
-
*/
|
|
120
81
|
get #deps(): RoutesDependencies<Dependencies> {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
throw new Error(
|
|
124
|
-
"[real-router] RoutesNamespace: dependencies not initialized",
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return this.#depsStore;
|
|
82
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
83
|
+
return this.#store.depsStore!;
|
|
129
84
|
}
|
|
130
85
|
|
|
131
|
-
// =========================================================================
|
|
132
|
-
// Constructor
|
|
133
|
-
// =========================================================================
|
|
134
|
-
|
|
135
86
|
constructor(
|
|
136
87
|
routes: Route<Dependencies>[] = [],
|
|
137
88
|
noValidate = false,
|
|
138
89
|
matcherOptions?: CreateMatcherOptions,
|
|
139
90
|
) {
|
|
140
|
-
this.#
|
|
141
|
-
this.#matcherOptions = matcherOptions;
|
|
142
|
-
|
|
143
|
-
// Sanitize routes to store only essential properties
|
|
144
|
-
for (const route of routes) {
|
|
145
|
-
this.#definitions.push(sanitizeRoute(route));
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Create initial tree
|
|
149
|
-
this.#tree = createRouteTree(
|
|
150
|
-
DEFAULT_ROUTE_NAME,
|
|
151
|
-
this.#rootPath,
|
|
152
|
-
this.#definitions,
|
|
153
|
-
);
|
|
154
|
-
|
|
155
|
-
// Initialize matcher with options and register tree
|
|
156
|
-
this.#matcher = createMatcher(matcherOptions);
|
|
157
|
-
this.#matcher.registerTree(this.#tree);
|
|
158
|
-
|
|
159
|
-
// Register handlers for all routes (defaultParams, encoders, decoders, forwardTo)
|
|
160
|
-
// Note: canActivate handlers are registered later when #lifecycleNamespace is set
|
|
161
|
-
this.#registerAllRouteHandlers(routes);
|
|
162
|
-
|
|
163
|
-
// Validate and cache forwardTo chains (detect cycles)
|
|
164
|
-
// Skip validation in noValidate mode for production performance
|
|
165
|
-
if (noValidate) {
|
|
166
|
-
// Still need to cache resolved forwards, just skip validation
|
|
167
|
-
this.#cacheForwardMap();
|
|
168
|
-
} else {
|
|
169
|
-
this.#validateAndCacheForwardMap();
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// =========================================================================
|
|
174
|
-
// Static validation methods (delegated to validators.ts)
|
|
175
|
-
// TypeScript requires explicit method declarations for assertion functions
|
|
176
|
-
// =========================================================================
|
|
177
|
-
|
|
178
|
-
static validateRemoveRouteArgs(name: unknown): asserts name is string {
|
|
179
|
-
validateRemoveRouteArgs(name);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
static validateSetRootPathArgs(
|
|
183
|
-
rootPath: unknown,
|
|
184
|
-
): asserts rootPath is string {
|
|
185
|
-
validateSetRootPathArgs(rootPath);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- accepts any Route type
|
|
189
|
-
static validateAddRouteArgs(routes: readonly Route<any>[]): void {
|
|
190
|
-
validateAddRouteArgs(routes);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
static validateParentOption(parent: unknown): asserts parent is string {
|
|
194
|
-
validateParentOption(parent);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
static validateIsActiveRouteArgs(
|
|
198
|
-
name: unknown,
|
|
199
|
-
params: unknown,
|
|
200
|
-
strictEquality: unknown,
|
|
201
|
-
ignoreQueryParams: unknown,
|
|
202
|
-
): void {
|
|
203
|
-
validateIsActiveRouteArgs(name, params, strictEquality, ignoreQueryParams);
|
|
91
|
+
this.#store = createRoutesStore(routes, noValidate, matcherOptions);
|
|
204
92
|
}
|
|
205
93
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
94
|
+
/**
|
|
95
|
+
* Creates a predicate function to check if a route node should be updated.
|
|
96
|
+
* Note: Argument validation is done by facade (Router.ts) via validateShouldUpdateNodeArgs.
|
|
97
|
+
*/
|
|
98
|
+
static shouldUpdateNode(
|
|
99
|
+
nodeName: string,
|
|
100
|
+
): (toState: State, fromState?: State) => boolean {
|
|
101
|
+
return (toState: State, fromState?: State): boolean => {
|
|
102
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
103
|
+
if (!(toState && typeof toState === "object" && "name" in toState)) {
|
|
104
|
+
throw new TypeError(
|
|
105
|
+
"[router.shouldUpdateNode] toState must be valid State object",
|
|
106
|
+
);
|
|
107
|
+
}
|
|
213
108
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
): asserts updates is RouteConfigUpdate<Deps> {
|
|
218
|
-
validateUpdateRouteBasicArgs<Deps>(name, updates);
|
|
219
|
-
}
|
|
109
|
+
if (toState.meta?.options.reload) {
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
220
112
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
decodeParams: unknown,
|
|
225
|
-
encodeParams: unknown,
|
|
226
|
-
): void {
|
|
227
|
-
validateUpdateRoutePropertyTypes(
|
|
228
|
-
forwardTo,
|
|
229
|
-
defaultParams,
|
|
230
|
-
decodeParams,
|
|
231
|
-
encodeParams,
|
|
232
|
-
);
|
|
233
|
-
}
|
|
113
|
+
if (nodeName === DEFAULT_ROUTE_NAME && !fromState) {
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
234
116
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
117
|
+
const { intersection, toActivate, toDeactivate } = getTransitionPath(
|
|
118
|
+
toState,
|
|
119
|
+
fromState,
|
|
120
|
+
);
|
|
238
121
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
122
|
+
if (nodeName === intersection) {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
242
125
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
validateShouldUpdateNodeArgs(nodeName);
|
|
247
|
-
}
|
|
126
|
+
if (toActivate.includes(nodeName)) {
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
248
129
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
tree?: RouteTree,
|
|
252
|
-
forwardMap?: Record<string, string>,
|
|
253
|
-
parentName?: string,
|
|
254
|
-
): void {
|
|
255
|
-
validateRoutes(routes, tree, forwardMap, parentName);
|
|
130
|
+
return toDeactivate.includes(nodeName);
|
|
131
|
+
};
|
|
256
132
|
}
|
|
257
133
|
|
|
258
134
|
// =========================================================================
|
|
@@ -264,23 +140,19 @@ export class RoutesNamespace<
|
|
|
264
140
|
* canActivate handlers from initial routes are deferred until deps are set.
|
|
265
141
|
*/
|
|
266
142
|
setDependencies(deps: RoutesDependencies<Dependencies>): void {
|
|
267
|
-
this.#depsStore = deps;
|
|
143
|
+
this.#store.depsStore = deps;
|
|
268
144
|
|
|
269
|
-
|
|
270
|
-
for (const [routeName, handler] of this.#pendingCanActivate) {
|
|
145
|
+
for (const [routeName, handler] of this.#store.pendingCanActivate) {
|
|
271
146
|
deps.addActivateGuard(routeName, handler);
|
|
272
147
|
}
|
|
273
148
|
|
|
274
|
-
|
|
275
|
-
this.#pendingCanActivate.clear();
|
|
149
|
+
this.#store.pendingCanActivate.clear();
|
|
276
150
|
|
|
277
|
-
|
|
278
|
-
for (const [routeName, handler] of this.#pendingCanDeactivate) {
|
|
151
|
+
for (const [routeName, handler] of this.#store.pendingCanDeactivate) {
|
|
279
152
|
deps.addDeactivateGuard(routeName, handler);
|
|
280
153
|
}
|
|
281
154
|
|
|
282
|
-
|
|
283
|
-
this.#pendingCanDeactivate.clear();
|
|
155
|
+
this.#store.pendingCanDeactivate.clear();
|
|
284
156
|
}
|
|
285
157
|
|
|
286
158
|
/**
|
|
@@ -290,218 +162,24 @@ export class RoutesNamespace<
|
|
|
290
162
|
namespace: RouteLifecycleNamespace<Dependencies> | undefined,
|
|
291
163
|
): void {
|
|
292
164
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
293
|
-
this.#lifecycleNamespace = namespace!;
|
|
165
|
+
this.#store.lifecycleNamespace = namespace!;
|
|
294
166
|
}
|
|
295
167
|
|
|
296
168
|
// =========================================================================
|
|
297
169
|
// Route tree operations
|
|
298
170
|
// =========================================================================
|
|
299
171
|
|
|
300
|
-
/**
|
|
301
|
-
* Returns the root path.
|
|
302
|
-
*/
|
|
303
|
-
getRootPath(): string {
|
|
304
|
-
return this.#rootPath;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
/**
|
|
308
|
-
* Returns the route tree.
|
|
309
|
-
* Used by facade for state-dependent validation.
|
|
310
|
-
*/
|
|
311
|
-
getTree(): RouteTree {
|
|
312
|
-
return this.#tree;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
/**
|
|
316
|
-
* Returns the forward record (route name -> forward target).
|
|
317
|
-
* Used by facade for state-dependent validation.
|
|
318
|
-
*/
|
|
319
|
-
getForwardRecord(): Record<string, string> {
|
|
320
|
-
return this.#config.forwardMap;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
/**
|
|
324
|
-
* Sets the root path and rebuilds the tree.
|
|
325
|
-
*/
|
|
326
172
|
setRootPath(newRootPath: string): void {
|
|
327
|
-
this.#rootPath = newRootPath;
|
|
328
|
-
this.#
|
|
173
|
+
this.#store.rootPath = newRootPath;
|
|
174
|
+
rebuildTreeInPlace(this.#store);
|
|
329
175
|
}
|
|
330
176
|
|
|
331
|
-
/**
|
|
332
|
-
* Checks if a route exists.
|
|
333
|
-
*/
|
|
334
177
|
hasRoute(name: string): boolean {
|
|
335
|
-
return this.#matcher.hasRoute(name);
|
|
178
|
+
return this.#store.matcher.hasRoute(name);
|
|
336
179
|
}
|
|
337
180
|
|
|
338
|
-
/**
|
|
339
|
-
* Gets a route by name with all its configuration.
|
|
340
|
-
*/
|
|
341
|
-
getRoute(name: string): Route<Dependencies> | undefined {
|
|
342
|
-
const segments = this.#matcher.getSegmentsByName(name);
|
|
343
|
-
|
|
344
|
-
if (!segments) {
|
|
345
|
-
return undefined;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
const targetNode = this.#getLastSegment(segments as readonly RouteTree[]);
|
|
349
|
-
const definition = nodeToDefinition(targetNode);
|
|
350
|
-
|
|
351
|
-
return this.#enrichRoute(definition, name);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
getRouteConfig(name: string): Record<string, unknown> | undefined {
|
|
355
|
-
if (!this.#matcher.hasRoute(name)) {
|
|
356
|
-
return undefined;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
return this.#routeCustomFields[name];
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
getRouteCustomFields(): Record<string, Record<string, unknown>> {
|
|
363
|
-
return this.#routeCustomFields;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
/**
|
|
367
|
-
* Adds one or more routes to the router.
|
|
368
|
-
* Input already validated by facade (properties and state-dependent checks).
|
|
369
|
-
*
|
|
370
|
-
* @param routes - Routes to add
|
|
371
|
-
* @param parentName - Optional parent route fullName for nesting
|
|
372
|
-
*/
|
|
373
|
-
addRoutes(routes: Route<Dependencies>[], parentName?: string): void {
|
|
374
|
-
// Add to definitions
|
|
375
|
-
if (parentName) {
|
|
376
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
377
|
-
const parentDef = this.#findDefinition(this.#definitions, parentName)!;
|
|
378
|
-
|
|
379
|
-
parentDef.children ??= [];
|
|
380
|
-
for (const route of routes) {
|
|
381
|
-
parentDef.children.push(sanitizeRoute(route));
|
|
382
|
-
}
|
|
383
|
-
} else {
|
|
384
|
-
for (const route of routes) {
|
|
385
|
-
this.#definitions.push(sanitizeRoute(route));
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
// Register handlers
|
|
390
|
-
this.#registerAllRouteHandlers(routes, parentName ?? "");
|
|
391
|
-
|
|
392
|
-
// Rebuild tree
|
|
393
|
-
this.#rebuildTree();
|
|
394
|
-
|
|
395
|
-
// Validate and cache forwardTo chains
|
|
396
|
-
this.#refreshForwardMap();
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
/**
|
|
400
|
-
* Removes a route and all its children.
|
|
401
|
-
*
|
|
402
|
-
* @param name - Route name (already validated)
|
|
403
|
-
* @returns true if removed, false if not found
|
|
404
|
-
*/
|
|
405
|
-
removeRoute(name: string): boolean {
|
|
406
|
-
const wasRemoved = removeFromDefinitions(this.#definitions, name);
|
|
407
|
-
|
|
408
|
-
if (!wasRemoved) {
|
|
409
|
-
return false;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
// Clear configurations for removed route
|
|
413
|
-
this.#clearRouteConfigurations(name);
|
|
414
|
-
|
|
415
|
-
// Rebuild tree
|
|
416
|
-
this.#rebuildTree();
|
|
417
|
-
|
|
418
|
-
// Revalidate forward chains
|
|
419
|
-
this.#refreshForwardMap();
|
|
420
|
-
|
|
421
|
-
return true;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
/**
|
|
425
|
-
* Updates a route's configuration in place without rebuilding the tree.
|
|
426
|
-
* This is used by Router.updateRoute to directly modify config entries
|
|
427
|
-
* without destroying other routes' forwardMap references.
|
|
428
|
-
*
|
|
429
|
-
* @param name - Route name
|
|
430
|
-
* @param updates - Config updates to apply
|
|
431
|
-
* @param updates.forwardTo - Forward target route name (null to clear)
|
|
432
|
-
* @param updates.defaultParams - Default parameters (null to clear)
|
|
433
|
-
* @param updates.decodeParams - Params decoder function (null to clear)
|
|
434
|
-
* @param updates.encodeParams - Params encoder function (null to clear)
|
|
435
|
-
*/
|
|
436
|
-
|
|
437
|
-
updateRouteConfig(
|
|
438
|
-
name: string,
|
|
439
|
-
updates: {
|
|
440
|
-
forwardTo?: string | ForwardToCallback<Dependencies> | null | undefined;
|
|
441
|
-
defaultParams?: Params | null | undefined;
|
|
442
|
-
decodeParams?: ((params: Params) => Params) | null | undefined;
|
|
443
|
-
encodeParams?: ((params: Params) => Params) | null | undefined;
|
|
444
|
-
},
|
|
445
|
-
): void {
|
|
446
|
-
// Update forwardTo
|
|
447
|
-
if (updates.forwardTo !== undefined) {
|
|
448
|
-
this.#updateForwardTo(name, updates.forwardTo);
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
// Update defaultParams
|
|
452
|
-
if (updates.defaultParams !== undefined) {
|
|
453
|
-
if (updates.defaultParams === null) {
|
|
454
|
-
delete this.#config.defaultParams[name];
|
|
455
|
-
} else {
|
|
456
|
-
this.#config.defaultParams[name] = updates.defaultParams;
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
// Update decoders with fallback wrapper
|
|
461
|
-
// Runtime guard: fallback to params if decoder returns undefined (bad user code)
|
|
462
|
-
if (updates.decodeParams !== undefined) {
|
|
463
|
-
if (updates.decodeParams === null) {
|
|
464
|
-
delete this.#config.decoders[name];
|
|
465
|
-
} else {
|
|
466
|
-
const decoder = updates.decodeParams;
|
|
467
|
-
|
|
468
|
-
this.#config.decoders[name] = (params: Params): Params =>
|
|
469
|
-
(decoder(params) as Params | undefined) ?? params;
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
// Update encoders with fallback wrapper
|
|
474
|
-
// Runtime guard: fallback to params if encoder returns undefined (bad user code)
|
|
475
|
-
if (updates.encodeParams !== undefined) {
|
|
476
|
-
if (updates.encodeParams === null) {
|
|
477
|
-
delete this.#config.encoders[name];
|
|
478
|
-
} else {
|
|
479
|
-
const encoder = updates.encodeParams;
|
|
480
|
-
|
|
481
|
-
this.#config.encoders[name] = (params: Params): Params =>
|
|
482
|
-
(encoder(params) as Params | undefined) ?? params;
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
/**
|
|
488
|
-
* Clears all routes from the router.
|
|
489
|
-
*/
|
|
490
181
|
clearRoutes(): void {
|
|
491
|
-
this.#
|
|
492
|
-
|
|
493
|
-
// Reset config to empty null-prototype objects
|
|
494
|
-
Object.assign(this.#config, createEmptyConfig());
|
|
495
|
-
|
|
496
|
-
// Clear forward cache
|
|
497
|
-
this.#resolvedForwardMap = Object.create(null) as Record<string, string>;
|
|
498
|
-
this.#routeCustomFields = Object.create(null) as Record<
|
|
499
|
-
string,
|
|
500
|
-
Record<string, unknown>
|
|
501
|
-
>;
|
|
502
|
-
|
|
503
|
-
// Rebuild empty tree
|
|
504
|
-
this.#rebuildTree();
|
|
182
|
+
resetStore(this.#store);
|
|
505
183
|
}
|
|
506
184
|
|
|
507
185
|
// =========================================================================
|
|
@@ -521,22 +199,22 @@ export class RoutesNamespace<
|
|
|
521
199
|
return isString(params?.path) ? params.path : "";
|
|
522
200
|
}
|
|
523
201
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
202
|
+
const paramsWithDefault = Object.hasOwn(
|
|
203
|
+
this.#store.config.defaultParams,
|
|
204
|
+
route,
|
|
205
|
+
)
|
|
206
|
+
? { ...this.#store.config.defaultParams[route], ...params }
|
|
527
207
|
: (params ?? {});
|
|
528
208
|
|
|
529
|
-
// Apply custom encoder if defined
|
|
530
209
|
const encodedParams =
|
|
531
|
-
typeof this.#config.encoders[route] === "function"
|
|
532
|
-
? this.#config.encoders[route]({ ...paramsWithDefault })
|
|
210
|
+
typeof this.#store.config.encoders[route] === "function"
|
|
211
|
+
? this.#store.config.encoders[route]({ ...paramsWithDefault })
|
|
533
212
|
: paramsWithDefault;
|
|
534
213
|
|
|
535
|
-
// Map core trailingSlash to matcher: "preserve"/"strict" → default (no change)
|
|
536
214
|
const ts = options?.trailingSlash;
|
|
537
215
|
const trailingSlash = ts === "never" || ts === "always" ? ts : undefined;
|
|
538
216
|
|
|
539
|
-
return this.#matcher.buildPath(route, encodedParams, {
|
|
217
|
+
return this.#store.matcher.buildPath(route, encodedParams, {
|
|
540
218
|
trailingSlash,
|
|
541
219
|
queryParamsMode: options?.queryParamsMode,
|
|
542
220
|
});
|
|
@@ -553,7 +231,7 @@ export class RoutesNamespace<
|
|
|
553
231
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Router.ts always passes options
|
|
554
232
|
const opts = options!;
|
|
555
233
|
|
|
556
|
-
const matchResult = this.#matcher.match(path);
|
|
234
|
+
const matchResult = this.#store.matcher.match(path);
|
|
557
235
|
|
|
558
236
|
if (!matchResult) {
|
|
559
237
|
return undefined;
|
|
@@ -563,8 +241,8 @@ export class RoutesNamespace<
|
|
|
563
241
|
const { name, params, meta } = routeState;
|
|
564
242
|
|
|
565
243
|
const decodedParams =
|
|
566
|
-
typeof this.#config.decoders[name] === "function"
|
|
567
|
-
? this.#config.decoders[name](params as Params)
|
|
244
|
+
typeof this.#store.config.decoders[name] === "function"
|
|
245
|
+
? this.#store.config.decoders[name](params as Params)
|
|
568
246
|
: params;
|
|
569
247
|
|
|
570
248
|
const { name: routeName, params: routeParams } = this.#deps.forwardState<P>(
|
|
@@ -576,15 +254,15 @@ export class RoutesNamespace<
|
|
|
576
254
|
|
|
577
255
|
if (opts.rewritePathOnMatch) {
|
|
578
256
|
const buildParams =
|
|
579
|
-
typeof this.#config.encoders[routeName] === "function"
|
|
580
|
-
? this.#config.encoders[routeName]({
|
|
257
|
+
typeof this.#store.config.encoders[routeName] === "function"
|
|
258
|
+
? this.#store.config.encoders[routeName]({
|
|
581
259
|
...(routeParams as Params),
|
|
582
260
|
})
|
|
583
261
|
: (routeParams as Record<string, unknown>);
|
|
584
262
|
|
|
585
263
|
const ts = opts.trailingSlash;
|
|
586
264
|
|
|
587
|
-
builtPath = this.#matcher.buildPath(routeName, buildParams, {
|
|
265
|
+
builtPath = this.#store.matcher.buildPath(routeName, buildParams, {
|
|
588
266
|
trailingSlash: ts === "never" || ts === "always" ? ts : undefined,
|
|
589
267
|
queryParamsMode: opts.queryParamsMode,
|
|
590
268
|
});
|
|
@@ -608,10 +286,9 @@ export class RoutesNamespace<
|
|
|
608
286
|
name: string,
|
|
609
287
|
params: P,
|
|
610
288
|
): { name: string; params: P } {
|
|
611
|
-
|
|
612
|
-
if (Object.hasOwn(this.#config.forwardFnMap, name)) {
|
|
289
|
+
if (Object.hasOwn(this.#store.config.forwardFnMap, name)) {
|
|
613
290
|
const paramsWithSourceDefaults = this.#mergeDefaultParams(name, params);
|
|
614
|
-
const dynamicForward = this.#config.forwardFnMap[name];
|
|
291
|
+
const dynamicForward = this.#store.config.forwardFnMap[name];
|
|
615
292
|
const resolved = this.#resolveDynamicForward(
|
|
616
293
|
name,
|
|
617
294
|
dynamicForward,
|
|
@@ -624,16 +301,15 @@ export class RoutesNamespace<
|
|
|
624
301
|
};
|
|
625
302
|
}
|
|
626
303
|
|
|
627
|
-
|
|
628
|
-
const staticForward = this.#resolvedForwardMap[name] ?? name;
|
|
304
|
+
const staticForward = this.#store.resolvedForwardMap[name] ?? name;
|
|
629
305
|
|
|
630
|
-
// Path 3: Mixed chain (static target has dynamic forward)
|
|
631
306
|
if (
|
|
632
307
|
staticForward !== name &&
|
|
633
|
-
Object.hasOwn(this.#config.forwardFnMap, staticForward)
|
|
308
|
+
Object.hasOwn(this.#store.config.forwardFnMap, staticForward)
|
|
634
309
|
) {
|
|
635
310
|
const paramsWithSourceDefaults = this.#mergeDefaultParams(name, params);
|
|
636
|
-
const targetDynamicForward =
|
|
311
|
+
const targetDynamicForward =
|
|
312
|
+
this.#store.config.forwardFnMap[staticForward];
|
|
637
313
|
const resolved = this.#resolveDynamicForward(
|
|
638
314
|
staticForward,
|
|
639
315
|
targetDynamicForward,
|
|
@@ -646,7 +322,6 @@ export class RoutesNamespace<
|
|
|
646
322
|
};
|
|
647
323
|
}
|
|
648
324
|
|
|
649
|
-
// Path 4: Static forward only
|
|
650
325
|
if (staticForward !== name) {
|
|
651
326
|
const paramsWithSourceDefaults = this.#mergeDefaultParams(name, params);
|
|
652
327
|
|
|
@@ -659,7 +334,6 @@ export class RoutesNamespace<
|
|
|
659
334
|
};
|
|
660
335
|
}
|
|
661
336
|
|
|
662
|
-
// No forward - merge own defaults
|
|
663
337
|
return { name, params: this.#mergeDefaultParams(name, params) };
|
|
664
338
|
}
|
|
665
339
|
|
|
@@ -672,14 +346,14 @@ export class RoutesNamespace<
|
|
|
672
346
|
resolvedName: string,
|
|
673
347
|
resolvedParams: Params,
|
|
674
348
|
): RouteTreeState | undefined {
|
|
675
|
-
const segments = this.#matcher.getSegmentsByName(resolvedName);
|
|
349
|
+
const segments = this.#store.matcher.getSegmentsByName(resolvedName);
|
|
676
350
|
|
|
677
351
|
if (!segments) {
|
|
678
352
|
return undefined;
|
|
679
353
|
}
|
|
680
354
|
|
|
681
355
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
682
|
-
const meta = this.#matcher.getMetaByName(resolvedName)!;
|
|
356
|
+
const meta = this.#store.matcher.getMetaByName(resolvedName)!;
|
|
683
357
|
|
|
684
358
|
return createRouteState(
|
|
685
359
|
{ segments, params: resolvedParams, meta },
|
|
@@ -687,23 +361,18 @@ export class RoutesNamespace<
|
|
|
687
361
|
);
|
|
688
362
|
}
|
|
689
363
|
|
|
690
|
-
/**
|
|
691
|
-
* Builds a RouteTreeState with segments from already-resolved route name and params.
|
|
692
|
-
* Called by Router.buildStateWithSegments after forwardState is applied at facade level.
|
|
693
|
-
* This allows plugins to intercept forwardState.
|
|
694
|
-
*/
|
|
695
364
|
buildStateWithSegmentsResolved<P extends Params = Params>(
|
|
696
365
|
resolvedName: string,
|
|
697
366
|
resolvedParams: P,
|
|
698
367
|
): BuildStateResultWithSegments<P> | undefined {
|
|
699
|
-
const segments = this.#matcher.getSegmentsByName(resolvedName);
|
|
368
|
+
const segments = this.#store.matcher.getSegmentsByName(resolvedName);
|
|
700
369
|
|
|
701
370
|
if (!segments) {
|
|
702
371
|
return undefined;
|
|
703
372
|
}
|
|
704
373
|
|
|
705
374
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
706
|
-
const meta = this.#matcher.getMetaByName(resolvedName)!;
|
|
375
|
+
const meta = this.#store.matcher.getMetaByName(resolvedName)!;
|
|
707
376
|
const state = createRouteState<P>(
|
|
708
377
|
{
|
|
709
378
|
segments: segments as readonly RouteTree[],
|
|
@@ -753,7 +422,7 @@ export class RoutesNamespace<
|
|
|
753
422
|
return false;
|
|
754
423
|
}
|
|
755
424
|
|
|
756
|
-
const defaultParams = this.#config.defaultParams[name] as
|
|
425
|
+
const defaultParams = this.#store.config.defaultParams[name] as
|
|
757
426
|
| Params
|
|
758
427
|
| undefined;
|
|
759
428
|
|
|
@@ -790,311 +459,45 @@ export class RoutesNamespace<
|
|
|
790
459
|
);
|
|
791
460
|
}
|
|
792
461
|
|
|
793
|
-
/**
|
|
794
|
-
* Creates a predicate function to check if a route node should be updated.
|
|
795
|
-
* Note: Argument validation is done by facade (Router.ts) via validateShouldUpdateNodeArgs.
|
|
796
|
-
*/
|
|
797
|
-
shouldUpdateNode(
|
|
798
|
-
nodeName: string,
|
|
799
|
-
): (toState: State, fromState?: State) => boolean {
|
|
800
|
-
return (toState: State, fromState?: State): boolean => {
|
|
801
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
802
|
-
if (!(toState && typeof toState === "object" && "name" in toState)) {
|
|
803
|
-
throw new TypeError(
|
|
804
|
-
"[router.shouldUpdateNode] toState must be valid State object",
|
|
805
|
-
);
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
if (toState.meta?.options.reload) {
|
|
809
|
-
return true;
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
if (nodeName === DEFAULT_ROUTE_NAME && !fromState) {
|
|
813
|
-
return true;
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
const { intersection, toActivate, toDeactivate } = getTransitionPath(
|
|
817
|
-
toState,
|
|
818
|
-
fromState,
|
|
819
|
-
);
|
|
820
|
-
|
|
821
|
-
if (nodeName === intersection) {
|
|
822
|
-
return true;
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
if (toActivate.includes(nodeName)) {
|
|
826
|
-
return true;
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
return toDeactivate.includes(nodeName);
|
|
830
|
-
};
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
/**
|
|
834
|
-
* Returns the config object.
|
|
835
|
-
*/
|
|
836
|
-
getConfig(): RouteConfig {
|
|
837
|
-
return this.#config;
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
/**
|
|
841
|
-
* Returns URL params for a route.
|
|
842
|
-
* Used by StateNamespace.
|
|
843
|
-
*/
|
|
844
462
|
getUrlParams(name: string): string[] {
|
|
845
|
-
const segments = this.#matcher.getSegmentsByName(name);
|
|
463
|
+
const segments = this.#store.matcher.getSegmentsByName(name);
|
|
846
464
|
|
|
847
465
|
if (!segments) {
|
|
848
466
|
return [];
|
|
849
467
|
}
|
|
850
468
|
|
|
851
|
-
return
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
/**
|
|
855
|
-
* Returns the resolved forward map.
|
|
856
|
-
*/
|
|
857
|
-
getResolvedForwardMap(): Record<string, string> {
|
|
858
|
-
return this.#resolvedForwardMap;
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
/**
|
|
862
|
-
* Sets resolved forward map (used by clone).
|
|
863
|
-
*/
|
|
864
|
-
setResolvedForwardMap(map: Record<string, string>): void {
|
|
865
|
-
Object.assign(this.#resolvedForwardMap, map);
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
/**
|
|
869
|
-
* Applies cloned route config from source router.
|
|
870
|
-
* Used by clone to copy decoders, encoders, defaultParams, forwardMap,
|
|
871
|
-
* forwardFnMap and resolvedForwardMap into this namespace's config.
|
|
872
|
-
*/
|
|
873
|
-
applyClonedConfig(
|
|
874
|
-
config: RouteConfig,
|
|
875
|
-
resolvedForwardMap: Record<string, string>,
|
|
876
|
-
routeCustomFields: Record<string, Record<string, unknown>>,
|
|
877
|
-
): void {
|
|
878
|
-
Object.assign(this.#config.decoders, config.decoders);
|
|
879
|
-
Object.assign(this.#config.encoders, config.encoders);
|
|
880
|
-
Object.assign(this.#config.defaultParams, config.defaultParams);
|
|
881
|
-
Object.assign(this.#config.forwardMap, config.forwardMap);
|
|
882
|
-
Object.assign(this.#config.forwardFnMap, config.forwardFnMap);
|
|
883
|
-
this.setResolvedForwardMap({ ...resolvedForwardMap });
|
|
884
|
-
Object.assign(this.#routeCustomFields, routeCustomFields);
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
/**
|
|
888
|
-
* Creates a clone of the routes for a new router (from tree).
|
|
889
|
-
*/
|
|
890
|
-
cloneRoutes(): Route<Dependencies>[] {
|
|
891
|
-
return routeTreeToDefinitions(this.#tree) as Route<Dependencies>[];
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
// =========================================================================
|
|
895
|
-
// Public validation methods (used by Router facade)
|
|
896
|
-
// =========================================================================
|
|
897
|
-
|
|
898
|
-
/**
|
|
899
|
-
* Validates that forwardTo target doesn't require params that source doesn't have.
|
|
900
|
-
* Used by updateRoute for forwardTo validation.
|
|
901
|
-
*/
|
|
902
|
-
validateForwardToParamCompatibility(
|
|
903
|
-
sourceName: string,
|
|
904
|
-
targetName: string,
|
|
905
|
-
): void {
|
|
906
|
-
const sourceSegments = this.#getSegmentsOrThrow(sourceName);
|
|
907
|
-
const targetSegments = this.#getSegmentsOrThrow(targetName);
|
|
908
|
-
|
|
909
|
-
// Get source and target URL params using helper
|
|
910
|
-
const sourceParams = this.#collectUrlParams(sourceSegments);
|
|
911
|
-
const targetParams = this.#collectUrlParamsArray(targetSegments);
|
|
912
|
-
|
|
913
|
-
// Check if target requires params that source doesn't have
|
|
914
|
-
const missingParams = targetParams.filter(
|
|
915
|
-
(param) => !sourceParams.has(param),
|
|
916
|
-
);
|
|
917
|
-
|
|
918
|
-
if (missingParams.length > 0) {
|
|
919
|
-
throw new Error(
|
|
920
|
-
`[real-router] forwardTo target "${targetName}" requires params ` +
|
|
921
|
-
`[${missingParams.join(", ")}] that are not available in source route "${sourceName}"`,
|
|
922
|
-
);
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
/**
|
|
927
|
-
* Validates that adding forwardTo doesn't create a cycle.
|
|
928
|
-
* Creates a test map with the new entry and uses resolveForwardChain
|
|
929
|
-
* to detect cycles before any mutation happens.
|
|
930
|
-
* Used by updateRoute for forwardTo validation.
|
|
931
|
-
*/
|
|
932
|
-
validateForwardToCycle(sourceName: string, targetName: string): void {
|
|
933
|
-
// Create a test map with the new entry to validate BEFORE mutation
|
|
934
|
-
const testMap = {
|
|
935
|
-
...this.#config.forwardMap,
|
|
936
|
-
[sourceName]: targetName,
|
|
937
|
-
};
|
|
938
|
-
|
|
939
|
-
// resolveForwardChain will throw if cycle is detected or max depth exceeded
|
|
940
|
-
resolveForwardChain(sourceName, testMap);
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
/**
|
|
944
|
-
* Validates removeRoute constraints.
|
|
945
|
-
* Returns false if removal should be blocked (route is active).
|
|
946
|
-
* Logs warnings for edge cases.
|
|
947
|
-
*
|
|
948
|
-
* @param name - Route name to remove
|
|
949
|
-
* @param currentStateName - Current active route name (or undefined)
|
|
950
|
-
* @param isNavigating - Whether navigation is in progress
|
|
951
|
-
* @returns true if removal can proceed, false if blocked
|
|
952
|
-
*/
|
|
953
|
-
validateRemoveRoute(
|
|
954
|
-
name: string,
|
|
955
|
-
currentStateName: string | undefined,
|
|
956
|
-
isNavigating: boolean,
|
|
957
|
-
): boolean {
|
|
958
|
-
// Check if trying to remove currently active route (or its parent)
|
|
959
|
-
if (currentStateName) {
|
|
960
|
-
const isExactMatch = currentStateName === name;
|
|
961
|
-
const isParentOfCurrent = currentStateName.startsWith(`${name}.`);
|
|
962
|
-
|
|
963
|
-
if (isExactMatch || isParentOfCurrent) {
|
|
964
|
-
const suffix = isExactMatch ? "" : ` (current: "${currentStateName}")`;
|
|
965
|
-
|
|
966
|
-
logger.warn(
|
|
967
|
-
"router.removeRoute",
|
|
968
|
-
`Cannot remove route "${name}" — it is currently active${suffix}. Navigate away first.`,
|
|
969
|
-
);
|
|
970
|
-
|
|
971
|
-
return false;
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
// Warn if navigation is in progress (but allow removal)
|
|
976
|
-
if (isNavigating) {
|
|
977
|
-
logger.warn(
|
|
978
|
-
"router.removeRoute",
|
|
979
|
-
`Route "${name}" removed while navigation is in progress. This may cause unexpected behavior.`,
|
|
980
|
-
);
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
return true;
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
/**
|
|
987
|
-
* Validates clearRoutes operation.
|
|
988
|
-
* Returns false if operation should be blocked (navigation in progress).
|
|
989
|
-
*
|
|
990
|
-
* @param isNavigating - Whether navigation is in progress
|
|
991
|
-
* @returns true if clearRoutes can proceed, false if blocked
|
|
992
|
-
*/
|
|
993
|
-
validateClearRoutes(isNavigating: boolean): boolean {
|
|
994
|
-
if (isNavigating) {
|
|
995
|
-
logger.error(
|
|
996
|
-
"router.clearRoutes",
|
|
997
|
-
"Cannot clear routes while navigation is in progress. Wait for navigation to complete.",
|
|
998
|
-
);
|
|
999
|
-
|
|
1000
|
-
return false;
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
return true;
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
/**
|
|
1007
|
-
* Validates updateRoute instance-level constraints (route existence, forwardTo).
|
|
1008
|
-
* Called after static validation passes.
|
|
1009
|
-
*
|
|
1010
|
-
* @param name - Route name (already validated by static method)
|
|
1011
|
-
* @param forwardTo - Cached forwardTo value (to avoid calling getter twice)
|
|
1012
|
-
*/
|
|
1013
|
-
validateUpdateRoute(
|
|
1014
|
-
name: string,
|
|
1015
|
-
forwardTo: string | ForwardToCallback<Dependencies> | null | undefined,
|
|
1016
|
-
): void {
|
|
1017
|
-
// Validate route exists
|
|
1018
|
-
if (!this.hasRoute(name)) {
|
|
1019
|
-
throw new ReferenceError(
|
|
1020
|
-
`[real-router] updateRoute: route "${name}" does not exist`,
|
|
1021
|
-
);
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
// Validate forwardTo target exists and is valid (only for string forwardTo)
|
|
1025
|
-
if (
|
|
1026
|
-
forwardTo !== undefined &&
|
|
1027
|
-
forwardTo !== null &&
|
|
1028
|
-
typeof forwardTo === "string"
|
|
1029
|
-
) {
|
|
1030
|
-
if (!this.hasRoute(forwardTo)) {
|
|
1031
|
-
throw new Error(
|
|
1032
|
-
`[real-router] updateRoute: forwardTo target "${forwardTo}" does not exist`,
|
|
1033
|
-
);
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
// Check forwardTo param compatibility
|
|
1037
|
-
this.validateForwardToParamCompatibility(name, forwardTo);
|
|
1038
|
-
|
|
1039
|
-
// Check for cycle detection
|
|
1040
|
-
this.validateForwardToCycle(name, forwardTo);
|
|
1041
|
-
}
|
|
469
|
+
return collectUrlParamsArray(segments as readonly RouteTree[]);
|
|
1042
470
|
}
|
|
1043
471
|
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
// =========================================================================
|
|
1047
|
-
|
|
1048
|
-
#updateForwardTo(
|
|
1049
|
-
name: string,
|
|
1050
|
-
forwardTo: string | ForwardToCallback<Dependencies> | null,
|
|
1051
|
-
): void {
|
|
1052
|
-
if (forwardTo === null) {
|
|
1053
|
-
delete this.#config.forwardMap[name];
|
|
1054
|
-
delete this.#config.forwardFnMap[name];
|
|
1055
|
-
} else if (typeof forwardTo === "string") {
|
|
1056
|
-
delete this.#config.forwardFnMap[name];
|
|
1057
|
-
this.#config.forwardMap[name] = forwardTo;
|
|
1058
|
-
} else {
|
|
1059
|
-
delete this.#config.forwardMap[name];
|
|
1060
|
-
this.#config.forwardFnMap[name] = forwardTo;
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
this.#refreshForwardMap();
|
|
472
|
+
getStore(): RoutesStore<Dependencies> {
|
|
473
|
+
return this.#store;
|
|
1064
474
|
}
|
|
1065
475
|
|
|
1066
|
-
/**
|
|
1067
|
-
* Merges route's defaultParams with provided params.
|
|
1068
|
-
*/
|
|
1069
476
|
#mergeDefaultParams<P extends Params = Params>(
|
|
1070
477
|
routeName: string,
|
|
1071
478
|
params: P,
|
|
1072
479
|
): P {
|
|
1073
|
-
if (Object.hasOwn(this.#config.defaultParams, routeName)) {
|
|
1074
|
-
return {
|
|
480
|
+
if (Object.hasOwn(this.#store.config.defaultParams, routeName)) {
|
|
481
|
+
return {
|
|
482
|
+
...this.#store.config.defaultParams[routeName],
|
|
483
|
+
...params,
|
|
484
|
+
} as P;
|
|
1075
485
|
}
|
|
1076
486
|
|
|
1077
487
|
return params;
|
|
1078
488
|
}
|
|
1079
489
|
|
|
1080
|
-
/**
|
|
1081
|
-
* Resolves dynamic forwardTo chain with cycle detection and max depth.
|
|
1082
|
-
* Throws if cycle detected, max depth exceeded, or invalid return type.
|
|
1083
|
-
*/
|
|
1084
490
|
#resolveDynamicForward(
|
|
1085
491
|
startName: string,
|
|
1086
|
-
|
|
1087
|
-
startFn: ForwardToCallback<any>,
|
|
492
|
+
startFn: ForwardToCallback<Dependencies>,
|
|
1088
493
|
params: Params,
|
|
1089
494
|
): string {
|
|
1090
495
|
const visited = new Set<string>([startName]);
|
|
1091
496
|
|
|
1092
|
-
|
|
1093
|
-
let current = startFn(this.#deps.getDependency as any, params);
|
|
497
|
+
let current = startFn(this.#deps.getDependency, params);
|
|
1094
498
|
let depth = 0;
|
|
1095
499
|
const MAX_DEPTH = 100;
|
|
1096
500
|
|
|
1097
|
-
// Validate initial return type
|
|
1098
501
|
if (typeof current !== "string") {
|
|
1099
502
|
throw new TypeError(
|
|
1100
503
|
`forwardTo callback must return a string, got ${typeof current}`,
|
|
@@ -1102,13 +505,10 @@ export class RoutesNamespace<
|
|
|
1102
505
|
}
|
|
1103
506
|
|
|
1104
507
|
while (depth < MAX_DEPTH) {
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
if (this.#matcher.getSegmentsByName(current) === undefined) {
|
|
508
|
+
if (this.#store.matcher.getSegmentsByName(current) === undefined) {
|
|
1108
509
|
throw new Error(`Route "${current}" does not exist`);
|
|
1109
510
|
}
|
|
1110
511
|
|
|
1111
|
-
// Check for cycle
|
|
1112
512
|
if (visited.has(current)) {
|
|
1113
513
|
const chain = [...visited, current].join(" → ");
|
|
1114
514
|
|
|
@@ -1117,19 +517,18 @@ export class RoutesNamespace<
|
|
|
1117
517
|
|
|
1118
518
|
visited.add(current);
|
|
1119
519
|
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
520
|
+
if (Object.hasOwn(this.#store.config.forwardFnMap, current)) {
|
|
521
|
+
const fn = this.#store.config.forwardFnMap[
|
|
522
|
+
current
|
|
523
|
+
] as ForwardToCallback<Dependencies>;
|
|
1123
524
|
|
|
1124
|
-
|
|
1125
|
-
current = fn(this.#deps.getDependency as any, params);
|
|
525
|
+
current = fn(this.#deps.getDependency, params);
|
|
1126
526
|
|
|
1127
527
|
depth++;
|
|
1128
528
|
continue;
|
|
1129
529
|
}
|
|
1130
530
|
|
|
1131
|
-
|
|
1132
|
-
const staticForward = this.#config.forwardMap[current];
|
|
531
|
+
const staticForward = this.#store.config.forwardMap[current];
|
|
1133
532
|
|
|
1134
533
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
1135
534
|
if (staticForward !== undefined) {
|
|
@@ -1138,364 +537,9 @@ export class RoutesNamespace<
|
|
|
1138
537
|
continue;
|
|
1139
538
|
}
|
|
1140
539
|
|
|
1141
|
-
// No more forwards - return current
|
|
1142
540
|
return current;
|
|
1143
541
|
}
|
|
1144
542
|
|
|
1145
543
|
throw new Error(`forwardTo exceeds maximum depth of ${MAX_DEPTH}`);
|
|
1146
544
|
}
|
|
1147
|
-
|
|
1148
|
-
#rebuildTree(): void {
|
|
1149
|
-
this.#tree = createRouteTree(
|
|
1150
|
-
DEFAULT_ROUTE_NAME,
|
|
1151
|
-
this.#rootPath,
|
|
1152
|
-
this.#definitions,
|
|
1153
|
-
);
|
|
1154
|
-
|
|
1155
|
-
// Re-register tree in matcher (creates new instance, preserving options if set)
|
|
1156
|
-
this.#matcher = createMatcher(this.#matcherOptions);
|
|
1157
|
-
this.#matcher.registerTree(this.#tree);
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
/**
|
|
1161
|
-
* Gets segments by name or throws if not found.
|
|
1162
|
-
* Use when route existence has been validated by hasRoute() beforehand.
|
|
1163
|
-
*/
|
|
1164
|
-
#getSegmentsOrThrow(name: string): readonly RouteTree[] {
|
|
1165
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
1166
|
-
return this.#matcher.getSegmentsByName(name)! as readonly RouteTree[];
|
|
1167
|
-
}
|
|
1168
|
-
|
|
1169
|
-
/**
|
|
1170
|
-
* Gets last segment from segments array.
|
|
1171
|
-
* Use when segments array is guaranteed to be non-empty.
|
|
1172
|
-
*/
|
|
1173
|
-
#getLastSegment(segments: readonly RouteTree[]): RouteTree {
|
|
1174
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
1175
|
-
return segments.at(-1)!;
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
/**
|
|
1179
|
-
* Collects URL params from segments into a Set.
|
|
1180
|
-
*/
|
|
1181
|
-
#collectUrlParams(segments: readonly RouteTree[]): Set<string> {
|
|
1182
|
-
const params = new Set<string>();
|
|
1183
|
-
|
|
1184
|
-
for (const segment of segments) {
|
|
1185
|
-
// Named routes always have parsers (null only for root without path)
|
|
1186
|
-
for (const param of segment.paramMeta.urlParams) {
|
|
1187
|
-
params.add(param);
|
|
1188
|
-
}
|
|
1189
|
-
}
|
|
1190
|
-
|
|
1191
|
-
return params;
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
/**
|
|
1195
|
-
* Collects URL params from segments into an array.
|
|
1196
|
-
*
|
|
1197
|
-
* @param segments - Non-null segments (caller must validate existence first)
|
|
1198
|
-
*/
|
|
1199
|
-
#collectUrlParamsArray(segments: readonly RouteTree[]): string[] {
|
|
1200
|
-
const params: string[] = [];
|
|
1201
|
-
|
|
1202
|
-
for (const segment of segments) {
|
|
1203
|
-
// Named routes always have parsers (null only for root without path)
|
|
1204
|
-
for (const param of segment.paramMeta.urlParams) {
|
|
1205
|
-
params.push(param);
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
return params;
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
|
-
/**
|
|
1213
|
-
* Refreshes forward map cache, conditionally validating based on noValidate flag.
|
|
1214
|
-
*/
|
|
1215
|
-
#refreshForwardMap(): void {
|
|
1216
|
-
if (this.#noValidate) {
|
|
1217
|
-
this.#cacheForwardMap();
|
|
1218
|
-
} else {
|
|
1219
|
-
this.#validateAndCacheForwardMap();
|
|
1220
|
-
}
|
|
1221
|
-
}
|
|
1222
|
-
|
|
1223
|
-
#validateAndCacheForwardMap(): void {
|
|
1224
|
-
// Clear existing cache
|
|
1225
|
-
this.#resolvedForwardMap = Object.create(null) as Record<string, string>;
|
|
1226
|
-
|
|
1227
|
-
// Resolve all chains
|
|
1228
|
-
for (const fromRoute of Object.keys(this.#config.forwardMap)) {
|
|
1229
|
-
this.#resolvedForwardMap[fromRoute] = resolveForwardChain(
|
|
1230
|
-
fromRoute,
|
|
1231
|
-
this.#config.forwardMap,
|
|
1232
|
-
);
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
/**
|
|
1237
|
-
* Caches forward chains without validation (noValidate mode).
|
|
1238
|
-
* Simply resolves chains without cycle detection or max depth checks.
|
|
1239
|
-
*/
|
|
1240
|
-
#cacheForwardMap(): void {
|
|
1241
|
-
// Clear existing cache
|
|
1242
|
-
this.#resolvedForwardMap = Object.create(null) as Record<string, string>;
|
|
1243
|
-
|
|
1244
|
-
// Resolve chains without validation
|
|
1245
|
-
for (const fromRoute of Object.keys(this.#config.forwardMap)) {
|
|
1246
|
-
let current = fromRoute;
|
|
1247
|
-
|
|
1248
|
-
while (this.#config.forwardMap[current]) {
|
|
1249
|
-
current = this.#config.forwardMap[current];
|
|
1250
|
-
}
|
|
1251
|
-
|
|
1252
|
-
this.#resolvedForwardMap[fromRoute] = current;
|
|
1253
|
-
}
|
|
1254
|
-
}
|
|
1255
|
-
|
|
1256
|
-
#clearRouteConfigurations(routeName: string): void {
|
|
1257
|
-
const shouldClear = (n: string): boolean =>
|
|
1258
|
-
n === routeName || n.startsWith(`${routeName}.`);
|
|
1259
|
-
|
|
1260
|
-
clearConfigEntries(this.#config.decoders, shouldClear);
|
|
1261
|
-
clearConfigEntries(this.#config.encoders, shouldClear);
|
|
1262
|
-
clearConfigEntries(this.#config.defaultParams, shouldClear);
|
|
1263
|
-
clearConfigEntries(this.#config.forwardMap, shouldClear);
|
|
1264
|
-
clearConfigEntries(this.#config.forwardFnMap, shouldClear);
|
|
1265
|
-
clearConfigEntries(this.#routeCustomFields, shouldClear);
|
|
1266
|
-
|
|
1267
|
-
// Clear forwardMap entries pointing TO deleted route
|
|
1268
|
-
clearConfigEntries(this.#config.forwardMap, (key) =>
|
|
1269
|
-
shouldClear(this.#config.forwardMap[key]),
|
|
1270
|
-
);
|
|
1271
|
-
|
|
1272
|
-
// Clear lifecycle handlers
|
|
1273
|
-
const [canDeactivateFactories, canActivateFactories] =
|
|
1274
|
-
this.#lifecycleNamespace.getFactories();
|
|
1275
|
-
|
|
1276
|
-
for (const n of Object.keys(canActivateFactories)) {
|
|
1277
|
-
if (shouldClear(n)) {
|
|
1278
|
-
this.#lifecycleNamespace.clearCanActivate(n);
|
|
1279
|
-
}
|
|
1280
|
-
}
|
|
1281
|
-
|
|
1282
|
-
for (const n of Object.keys(canDeactivateFactories)) {
|
|
1283
|
-
if (shouldClear(n)) {
|
|
1284
|
-
this.#lifecycleNamespace.clearCanDeactivate(n);
|
|
1285
|
-
}
|
|
1286
|
-
}
|
|
1287
|
-
}
|
|
1288
|
-
|
|
1289
|
-
#registerAllRouteHandlers(
|
|
1290
|
-
routes: readonly Route<Dependencies>[],
|
|
1291
|
-
parentName = "",
|
|
1292
|
-
): void {
|
|
1293
|
-
for (const route of routes) {
|
|
1294
|
-
const fullName = parentName ? `${parentName}.${route.name}` : route.name;
|
|
1295
|
-
|
|
1296
|
-
this.#registerSingleRouteHandlers(route, fullName);
|
|
1297
|
-
|
|
1298
|
-
if (route.children) {
|
|
1299
|
-
this.#registerAllRouteHandlers(route.children, fullName);
|
|
1300
|
-
}
|
|
1301
|
-
}
|
|
1302
|
-
}
|
|
1303
|
-
|
|
1304
|
-
#registerSingleRouteHandlers(
|
|
1305
|
-
route: Route<Dependencies>,
|
|
1306
|
-
fullName: string,
|
|
1307
|
-
): void {
|
|
1308
|
-
const standardKeys = new Set([
|
|
1309
|
-
"name",
|
|
1310
|
-
"path",
|
|
1311
|
-
"children",
|
|
1312
|
-
"canActivate",
|
|
1313
|
-
"canDeactivate",
|
|
1314
|
-
"forwardTo",
|
|
1315
|
-
"encodeParams",
|
|
1316
|
-
"decodeParams",
|
|
1317
|
-
"defaultParams",
|
|
1318
|
-
]);
|
|
1319
|
-
const customFields = Object.fromEntries(
|
|
1320
|
-
Object.entries(route).filter(([k]) => !standardKeys.has(k)),
|
|
1321
|
-
);
|
|
1322
|
-
|
|
1323
|
-
if (Object.keys(customFields).length > 0) {
|
|
1324
|
-
this.#routeCustomFields[fullName] = customFields;
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
// Register canActivate via deps.canActivate (allows tests to spy on router.canActivate)
|
|
1328
|
-
if (route.canActivate) {
|
|
1329
|
-
// Note: Uses #depsStore directly because this method is called from constructor
|
|
1330
|
-
// before setDependencies(). The getter #deps would throw if deps not set.
|
|
1331
|
-
if (this.#depsStore) {
|
|
1332
|
-
// Deps available, register immediately
|
|
1333
|
-
this.#depsStore.addActivateGuard(fullName, route.canActivate);
|
|
1334
|
-
} else {
|
|
1335
|
-
// Deps not set yet, store for later registration
|
|
1336
|
-
this.#pendingCanActivate.set(fullName, route.canActivate);
|
|
1337
|
-
}
|
|
1338
|
-
}
|
|
1339
|
-
|
|
1340
|
-
// Register canDeactivate via deps.canDeactivate (allows tests to spy on router.canDeactivate)
|
|
1341
|
-
if (route.canDeactivate) {
|
|
1342
|
-
// Note: Uses #depsStore directly because this method is called from constructor
|
|
1343
|
-
// before setDependencies(). The getter #deps would throw if deps not set.
|
|
1344
|
-
if (this.#depsStore) {
|
|
1345
|
-
// Deps available, register immediately
|
|
1346
|
-
this.#depsStore.addDeactivateGuard(fullName, route.canDeactivate);
|
|
1347
|
-
} else {
|
|
1348
|
-
// Deps not set yet, store for later registration
|
|
1349
|
-
this.#pendingCanDeactivate.set(fullName, route.canDeactivate);
|
|
1350
|
-
}
|
|
1351
|
-
}
|
|
1352
|
-
|
|
1353
|
-
// Register forwardTo
|
|
1354
|
-
if (route.forwardTo) {
|
|
1355
|
-
this.#registerForwardTo(route, fullName);
|
|
1356
|
-
}
|
|
1357
|
-
|
|
1358
|
-
// Register transformers with fallback wrapper
|
|
1359
|
-
if (route.decodeParams) {
|
|
1360
|
-
this.#config.decoders[fullName] = (params: Params): Params =>
|
|
1361
|
-
route.decodeParams?.(params) ?? params;
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
|
-
if (route.encodeParams) {
|
|
1365
|
-
this.#config.encoders[fullName] = (params: Params): Params =>
|
|
1366
|
-
route.encodeParams?.(params) ?? params;
|
|
1367
|
-
}
|
|
1368
|
-
|
|
1369
|
-
// Register defaults
|
|
1370
|
-
if (route.defaultParams) {
|
|
1371
|
-
this.#config.defaultParams[fullName] = route.defaultParams;
|
|
1372
|
-
}
|
|
1373
|
-
}
|
|
1374
|
-
|
|
1375
|
-
#findDefinition(
|
|
1376
|
-
definitions: RouteDefinition[],
|
|
1377
|
-
fullName: string,
|
|
1378
|
-
parentPrefix = "",
|
|
1379
|
-
): RouteDefinition | undefined {
|
|
1380
|
-
for (const def of definitions) {
|
|
1381
|
-
const currentFullName = parentPrefix
|
|
1382
|
-
? `${parentPrefix}.${def.name}`
|
|
1383
|
-
: def.name;
|
|
1384
|
-
|
|
1385
|
-
if (currentFullName === fullName) {
|
|
1386
|
-
return def;
|
|
1387
|
-
}
|
|
1388
|
-
if (def.children && fullName.startsWith(`${currentFullName}.`)) {
|
|
1389
|
-
return this.#findDefinition(def.children, fullName, currentFullName);
|
|
1390
|
-
}
|
|
1391
|
-
}
|
|
1392
|
-
|
|
1393
|
-
/* v8 ignore next -- @preserve: defensive return, callers validate route exists before calling */
|
|
1394
|
-
return undefined;
|
|
1395
|
-
}
|
|
1396
|
-
|
|
1397
|
-
#registerForwardTo(route: Route<Dependencies>, fullName: string): void {
|
|
1398
|
-
if (route.canActivate) {
|
|
1399
|
-
/* v8 ignore next -- @preserve: edge case, both string and function tested separately */
|
|
1400
|
-
const forwardTarget =
|
|
1401
|
-
typeof route.forwardTo === "string" ? route.forwardTo : "[dynamic]";
|
|
1402
|
-
|
|
1403
|
-
logger.warn(
|
|
1404
|
-
"real-router",
|
|
1405
|
-
`Route "${fullName}" has both forwardTo and canActivate. ` +
|
|
1406
|
-
`canActivate will be ignored because forwardTo creates a redirect (industry standard). ` +
|
|
1407
|
-
`Move canActivate to the target route "${forwardTarget}".`,
|
|
1408
|
-
);
|
|
1409
|
-
}
|
|
1410
|
-
|
|
1411
|
-
if (route.canDeactivate) {
|
|
1412
|
-
/* v8 ignore next -- @preserve: edge case, both string and function tested separately */
|
|
1413
|
-
const forwardTarget =
|
|
1414
|
-
typeof route.forwardTo === "string" ? route.forwardTo : "[dynamic]";
|
|
1415
|
-
|
|
1416
|
-
logger.warn(
|
|
1417
|
-
"real-router",
|
|
1418
|
-
`Route "${fullName}" has both forwardTo and canDeactivate. ` +
|
|
1419
|
-
`canDeactivate will be ignored because forwardTo creates a redirect (industry standard). ` +
|
|
1420
|
-
`Move canDeactivate to the target route "${forwardTarget}".`,
|
|
1421
|
-
);
|
|
1422
|
-
}
|
|
1423
|
-
|
|
1424
|
-
// Async validation ALWAYS runs (even with noValidate=true)
|
|
1425
|
-
if (typeof route.forwardTo === "function") {
|
|
1426
|
-
const isNativeAsync =
|
|
1427
|
-
(route.forwardTo as { constructor: { name: string } }).constructor
|
|
1428
|
-
.name === "AsyncFunction";
|
|
1429
|
-
const isTranspiledAsync = route.forwardTo
|
|
1430
|
-
.toString()
|
|
1431
|
-
.includes("__awaiter");
|
|
1432
|
-
|
|
1433
|
-
if (isNativeAsync || isTranspiledAsync) {
|
|
1434
|
-
throw new TypeError(
|
|
1435
|
-
`forwardTo callback cannot be async for route "${fullName}". ` +
|
|
1436
|
-
`Async functions break matchPath/buildPath.`,
|
|
1437
|
-
);
|
|
1438
|
-
}
|
|
1439
|
-
}
|
|
1440
|
-
|
|
1441
|
-
// forwardTo is guaranteed to exist at this point
|
|
1442
|
-
if (typeof route.forwardTo === "string") {
|
|
1443
|
-
this.#config.forwardMap[fullName] = route.forwardTo;
|
|
1444
|
-
} else {
|
|
1445
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
1446
|
-
this.#config.forwardFnMap[fullName] = route.forwardTo!;
|
|
1447
|
-
}
|
|
1448
|
-
}
|
|
1449
|
-
|
|
1450
|
-
#enrichRoute(
|
|
1451
|
-
routeDef: RouteDefinition,
|
|
1452
|
-
routeName: string,
|
|
1453
|
-
): Route<Dependencies> {
|
|
1454
|
-
const route: Route<Dependencies> = {
|
|
1455
|
-
name: routeDef.name,
|
|
1456
|
-
path: routeDef.path,
|
|
1457
|
-
};
|
|
1458
|
-
|
|
1459
|
-
const forwardToFn = this.#config.forwardFnMap[routeName];
|
|
1460
|
-
const forwardToStr = this.#config.forwardMap[routeName];
|
|
1461
|
-
|
|
1462
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
1463
|
-
if (forwardToFn !== undefined) {
|
|
1464
|
-
route.forwardTo = forwardToFn;
|
|
1465
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
1466
|
-
} else if (forwardToStr !== undefined) {
|
|
1467
|
-
route.forwardTo = forwardToStr;
|
|
1468
|
-
}
|
|
1469
|
-
|
|
1470
|
-
if (routeName in this.#config.defaultParams) {
|
|
1471
|
-
route.defaultParams = this.#config.defaultParams[routeName];
|
|
1472
|
-
}
|
|
1473
|
-
|
|
1474
|
-
if (routeName in this.#config.decoders) {
|
|
1475
|
-
route.decodeParams = this.#config.decoders[routeName];
|
|
1476
|
-
}
|
|
1477
|
-
|
|
1478
|
-
if (routeName in this.#config.encoders) {
|
|
1479
|
-
route.encodeParams = this.#config.encoders[routeName];
|
|
1480
|
-
}
|
|
1481
|
-
|
|
1482
|
-
const [canDeactivateFactories, canActivateFactories] =
|
|
1483
|
-
this.#lifecycleNamespace.getFactories();
|
|
1484
|
-
|
|
1485
|
-
if (routeName in canActivateFactories) {
|
|
1486
|
-
route.canActivate = canActivateFactories[routeName];
|
|
1487
|
-
}
|
|
1488
|
-
|
|
1489
|
-
if (routeName in canDeactivateFactories) {
|
|
1490
|
-
route.canDeactivate = canDeactivateFactories[routeName];
|
|
1491
|
-
}
|
|
1492
|
-
|
|
1493
|
-
if (routeDef.children) {
|
|
1494
|
-
route.children = routeDef.children.map((child) =>
|
|
1495
|
-
this.#enrichRoute(child, `${routeName}.${child.name}`),
|
|
1496
|
-
);
|
|
1497
|
-
}
|
|
1498
|
-
|
|
1499
|
-
return route;
|
|
1500
|
-
}
|
|
1501
545
|
}
|