@real-router/core 0.23.1 → 0.25.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 +0 -15
- package/dist/cjs/index.d.ts +12 -13
- 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 +12 -13
- 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 +4 -4
- package/src/Router.ts +14 -57
- package/src/RouterError.ts +1 -1
- package/src/constants.ts +1 -3
- package/src/helpers.ts +1 -1
- package/src/index.ts +3 -3
- package/src/namespaces/CloneNamespace/CloneNamespace.ts +6 -6
- package/src/namespaces/CloneNamespace/types.ts +5 -9
- package/src/namespaces/DependenciesNamespace/DependenciesNamespace.ts +2 -4
- package/src/namespaces/NavigationNamespace/NavigationNamespace.ts +1 -0
- package/src/namespaces/NavigationNamespace/transition/{wrapSyncError.ts → errorHandling.ts} +23 -17
- package/src/namespaces/NavigationNamespace/transition/executeLifecycleHooks.ts +14 -55
- package/src/namespaces/NavigationNamespace/transition/index.ts +6 -24
- package/src/namespaces/NavigationNamespace/types.ts +2 -9
- package/src/namespaces/PluginsNamespace/PluginsNamespace.ts +2 -2
- package/src/namespaces/RouteLifecycleNamespace/RouteLifecycleNamespace.ts +28 -43
- package/src/namespaces/RouteLifecycleNamespace/validators.ts +3 -3
- package/src/namespaces/RoutesNamespace/RoutesNamespace.ts +53 -34
- package/src/namespaces/RoutesNamespace/stateBuilder.ts +1 -1
- package/src/namespaces/RoutesNamespace/types.ts +3 -3
- package/src/namespaces/index.ts +0 -2
- package/src/transitionPath.ts +4 -40
- package/src/typeGuards.ts +1 -2
- package/src/types.ts +8 -8
- package/src/wiring/RouterWiringBuilder.ts +1 -17
- package/src/wiring/types.ts +0 -3
- package/src/wiring/wireRouter.ts +0 -1
- package/src/namespaces/MiddlewareNamespace/MiddlewareNamespace.ts +0 -221
- package/src/namespaces/MiddlewareNamespace/constants.ts +0 -3
- package/src/namespaces/MiddlewareNamespace/index.ts +0 -5
- package/src/namespaces/MiddlewareNamespace/types.ts +0 -28
- package/src/namespaces/MiddlewareNamespace/validators.ts +0 -95
- package/src/namespaces/NavigationNamespace/transition/executeMiddleware.ts +0 -56
- package/src/namespaces/NavigationNamespace/transition/makeError.ts +0 -37
- package/src/namespaces/NavigationNamespace/transition/mergeStates.ts +0 -54
- package/src/namespaces/NavigationNamespace/transition/processLifecycleResult.ts +0 -81
|
@@ -1,32 +1,26 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { logger } from "@real-router/logger";
|
|
4
|
-
import { isState } from "type-guards";
|
|
5
|
-
|
|
6
|
-
import { rethrowAsRouterError } from "./makeError";
|
|
7
|
-
import { mergeStates } from "./mergeStates";
|
|
8
|
-
import { processLifecycleResult } from "./processLifecycleResult";
|
|
1
|
+
import { rethrowAsRouterError } from "./errorHandling";
|
|
9
2
|
import { errorCodes } from "../../../constants";
|
|
10
3
|
import { RouterError } from "../../../RouterError";
|
|
11
4
|
|
|
12
|
-
import type {
|
|
5
|
+
import type { GuardFn, State } from "@real-router/types";
|
|
13
6
|
|
|
14
7
|
// Helper: execution of the Lifecycle Hooks group
|
|
15
|
-
export
|
|
16
|
-
hooks: Map<string,
|
|
8
|
+
export async function executeLifecycleHooks(
|
|
9
|
+
hooks: Map<string, GuardFn>,
|
|
17
10
|
toState: State,
|
|
18
11
|
fromState: State | undefined,
|
|
19
12
|
segments: string[],
|
|
20
13
|
errorCode: string,
|
|
21
14
|
isCancelled: () => boolean,
|
|
22
|
-
): Promise<
|
|
23
|
-
let currentState = toState;
|
|
15
|
+
): Promise<void> {
|
|
24
16
|
const segmentsToProcess = segments.filter((name) => hooks.has(name));
|
|
25
17
|
|
|
26
18
|
if (segmentsToProcess.length === 0) {
|
|
27
|
-
return
|
|
19
|
+
return;
|
|
28
20
|
}
|
|
29
21
|
|
|
22
|
+
let result: boolean | undefined;
|
|
23
|
+
|
|
30
24
|
for (const segment of segmentsToProcess) {
|
|
31
25
|
if (isCancelled()) {
|
|
32
26
|
throw new RouterError(errorCodes.TRANSITION_CANCELLED);
|
|
@@ -37,48 +31,13 @@ export const executeLifecycleHooks = async (
|
|
|
37
31
|
const hookFn = hooks.get(segment)!;
|
|
38
32
|
|
|
39
33
|
try {
|
|
40
|
-
|
|
41
|
-
const newState = await processLifecycleResult(
|
|
42
|
-
result,
|
|
43
|
-
currentState,
|
|
44
|
-
segment,
|
|
45
|
-
);
|
|
46
|
-
|
|
47
|
-
// Optimization: Early return for undefined newState (most common case ~90%+)
|
|
48
|
-
// This avoids isState() call and subsequent checks
|
|
49
|
-
if (newState !== currentState && isState(newState)) {
|
|
50
|
-
// Guards cannot redirect to a different route
|
|
51
|
-
if (newState.name !== currentState.name) {
|
|
52
|
-
throw new RouterError(errorCode, {
|
|
53
|
-
message:
|
|
54
|
-
"Guards cannot redirect to different route. Use middleware.",
|
|
55
|
-
attemptedRedirect: {
|
|
56
|
-
name: newState.name,
|
|
57
|
-
params: newState.params,
|
|
58
|
-
path: newState.path,
|
|
59
|
-
},
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Same route - safe to merge (param modifications, meta changes)
|
|
64
|
-
const hasChanged =
|
|
65
|
-
newState.params !== currentState.params ||
|
|
66
|
-
newState.path !== currentState.path;
|
|
67
|
-
|
|
68
|
-
if (hasChanged) {
|
|
69
|
-
logger.error(
|
|
70
|
-
"core:transition",
|
|
71
|
-
"Warning: State mutated during transition",
|
|
72
|
-
{ from: currentState, to: newState },
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
currentState = mergeStates(newState, currentState);
|
|
77
|
-
}
|
|
34
|
+
result = await hookFn(toState, fromState);
|
|
78
35
|
} catch (error: unknown) {
|
|
79
36
|
rethrowAsRouterError(error, errorCode, segment);
|
|
80
37
|
}
|
|
81
|
-
}
|
|
82
38
|
|
|
83
|
-
|
|
84
|
-
};
|
|
39
|
+
if (!result) {
|
|
40
|
+
throw new RouterError(errorCode, { segment });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
// packages/
|
|
1
|
+
// packages/core/src/namespaces/NavigationNamespace/transition/index.ts
|
|
2
2
|
|
|
3
3
|
import { executeLifecycleHooks } from "./executeLifecycleHooks";
|
|
4
|
-
import { executeMiddleware } from "./executeMiddleware";
|
|
5
4
|
import { constants, errorCodes } from "../../../constants";
|
|
6
5
|
import { RouterError } from "../../../RouterError";
|
|
7
6
|
import { getTransitionPath, nameToIDs } from "../../../transitionPath";
|
|
@@ -18,7 +17,6 @@ export async function transition(
|
|
|
18
17
|
// We're caching the necessary data
|
|
19
18
|
const [canDeactivateFunctions, canActivateFunctions] =
|
|
20
19
|
deps.getLifecycleFunctions();
|
|
21
|
-
const middlewareFunctions = deps.getMiddlewareFunctions();
|
|
22
20
|
const isUnknownRoute = toState.name === constants.UNKNOWN_ROUTE;
|
|
23
21
|
|
|
24
22
|
// State management functions
|
|
@@ -34,12 +32,9 @@ export async function transition(
|
|
|
34
32
|
const shouldDeactivate =
|
|
35
33
|
fromState && !opts.forceDeactivate && toDeactivate.length > 0;
|
|
36
34
|
const shouldActivate = !isUnknownRoute && toActivate.length > 0;
|
|
37
|
-
const shouldRunMiddleware = middlewareFunctions.length > 0;
|
|
38
|
-
|
|
39
|
-
let currentState = toState;
|
|
40
35
|
|
|
41
36
|
if (shouldDeactivate) {
|
|
42
|
-
|
|
37
|
+
await executeLifecycleHooks(
|
|
43
38
|
canDeactivateFunctions,
|
|
44
39
|
toState,
|
|
45
40
|
fromState,
|
|
@@ -54,9 +49,9 @@ export async function transition(
|
|
|
54
49
|
}
|
|
55
50
|
|
|
56
51
|
if (shouldActivate) {
|
|
57
|
-
|
|
52
|
+
await executeLifecycleHooks(
|
|
58
53
|
canActivateFunctions,
|
|
59
|
-
|
|
54
|
+
toState,
|
|
60
55
|
fromState,
|
|
61
56
|
toActivate,
|
|
62
57
|
errorCodes.CANNOT_ACTIVATE,
|
|
@@ -68,19 +63,6 @@ export async function transition(
|
|
|
68
63
|
throw new RouterError(errorCodes.TRANSITION_CANCELLED);
|
|
69
64
|
}
|
|
70
65
|
|
|
71
|
-
if (shouldRunMiddleware) {
|
|
72
|
-
currentState = await executeMiddleware(
|
|
73
|
-
middlewareFunctions,
|
|
74
|
-
currentState,
|
|
75
|
-
fromState,
|
|
76
|
-
isCancelled,
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (isCancelled()) {
|
|
81
|
-
throw new RouterError(errorCodes.TRANSITION_CANCELLED);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
66
|
// Automatic cleaning of inactive segments
|
|
85
67
|
if (fromState) {
|
|
86
68
|
const activeSegments = nameToIDs(toState.name);
|
|
@@ -94,9 +76,9 @@ export async function transition(
|
|
|
94
76
|
}
|
|
95
77
|
|
|
96
78
|
return {
|
|
97
|
-
state:
|
|
79
|
+
state: toState,
|
|
98
80
|
meta: {
|
|
99
|
-
phase: "
|
|
81
|
+
phase: "activating",
|
|
100
82
|
segments: {
|
|
101
83
|
deactivated: toDeactivate,
|
|
102
84
|
activated: toActivate,
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import type { BuildStateResultWithSegments } from "../../types";
|
|
4
4
|
import type {
|
|
5
|
-
|
|
6
|
-
Middleware,
|
|
5
|
+
GuardFn,
|
|
7
6
|
NavigationOptions,
|
|
8
7
|
Options,
|
|
9
8
|
Params,
|
|
@@ -110,13 +109,7 @@ export interface TransitionOutput {
|
|
|
110
109
|
*/
|
|
111
110
|
export interface TransitionDependencies {
|
|
112
111
|
/** Get lifecycle functions (canDeactivate, canActivate maps) */
|
|
113
|
-
getLifecycleFunctions: () => [
|
|
114
|
-
Map<string, ActivationFn>,
|
|
115
|
-
Map<string, ActivationFn>,
|
|
116
|
-
];
|
|
117
|
-
|
|
118
|
-
/** Get middleware functions array */
|
|
119
|
-
getMiddlewareFunctions: () => Middleware[];
|
|
112
|
+
getLifecycleFunctions: () => [Map<string, GuardFn>, Map<string, GuardFn>];
|
|
120
113
|
|
|
121
114
|
/** Check if router is active (for cancellation check on stop()) */
|
|
122
115
|
isActive: () => boolean;
|
|
@@ -222,8 +222,8 @@ export class PluginsNamespace<
|
|
|
222
222
|
/**
|
|
223
223
|
* Disposes all registered plugins by running their teardown callbacks
|
|
224
224
|
* and removing event listener subscriptions.
|
|
225
|
-
*
|
|
226
|
-
*
|
|
225
|
+
* Active disposal is required because plugins have an active lifecycle
|
|
226
|
+
* (event subscriptions, teardown hooks).
|
|
227
227
|
* Named "dispose" (not "clear") because there is active cleanup to perform.
|
|
228
228
|
*/
|
|
229
229
|
disposeAll(): void {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// packages/core/src/namespaces/RouteLifecycleNamespace/RouteLifecycleNamespace.ts
|
|
2
2
|
|
|
3
3
|
import { logger } from "@real-router/logger";
|
|
4
|
-
import { isBoolean,
|
|
4
|
+
import { isBoolean, getTypeDescription } from "type-guards";
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
7
|
validateHandler,
|
|
@@ -13,23 +13,19 @@ import { computeThresholds } from "../../helpers";
|
|
|
13
13
|
|
|
14
14
|
import type { RouteLifecycleDependencies } from "./types";
|
|
15
15
|
import type { Router } from "../../Router";
|
|
16
|
-
import type {
|
|
17
|
-
import type {
|
|
18
|
-
ActivationFn,
|
|
19
|
-
DefaultDependencies,
|
|
20
|
-
State,
|
|
21
|
-
} from "@real-router/types";
|
|
16
|
+
import type { GuardFnFactory, Limits } from "../../types";
|
|
17
|
+
import type { DefaultDependencies, GuardFn, State } from "@real-router/types";
|
|
22
18
|
|
|
23
19
|
/**
|
|
24
|
-
* Converts a boolean value to
|
|
20
|
+
* Converts a boolean value to a guard function factory.
|
|
25
21
|
* Used for the shorthand syntax where true/false is passed instead of a function.
|
|
26
22
|
*/
|
|
27
23
|
function booleanToFactory<Dependencies extends DefaultDependencies>(
|
|
28
24
|
value: boolean,
|
|
29
|
-
):
|
|
30
|
-
const
|
|
25
|
+
): GuardFnFactory<Dependencies> {
|
|
26
|
+
const guardFn: GuardFn = () => value;
|
|
31
27
|
|
|
32
|
-
return () =>
|
|
28
|
+
return () => guardFn;
|
|
33
29
|
}
|
|
34
30
|
|
|
35
31
|
/**
|
|
@@ -43,14 +39,14 @@ export class RouteLifecycleNamespace<
|
|
|
43
39
|
> {
|
|
44
40
|
readonly #canDeactivateFactories = new Map<
|
|
45
41
|
string,
|
|
46
|
-
|
|
42
|
+
GuardFnFactory<Dependencies>
|
|
47
43
|
>();
|
|
48
44
|
readonly #canActivateFactories = new Map<
|
|
49
45
|
string,
|
|
50
|
-
|
|
46
|
+
GuardFnFactory<Dependencies>
|
|
51
47
|
>();
|
|
52
|
-
readonly #canDeactivateFunctions = new Map<string,
|
|
53
|
-
readonly #canActivateFunctions = new Map<string,
|
|
48
|
+
readonly #canDeactivateFunctions = new Map<string, GuardFn>();
|
|
49
|
+
readonly #canActivateFunctions = new Map<string, GuardFn>();
|
|
54
50
|
|
|
55
51
|
readonly #registering = new Set<string>();
|
|
56
52
|
|
|
@@ -65,7 +61,7 @@ export class RouteLifecycleNamespace<
|
|
|
65
61
|
static validateHandler<D extends DefaultDependencies>(
|
|
66
62
|
handler: unknown,
|
|
67
63
|
methodName: string,
|
|
68
|
-
): asserts handler is
|
|
64
|
+
): asserts handler is GuardFnFactory<D> | boolean {
|
|
69
65
|
validateHandler<D>(handler, methodName);
|
|
70
66
|
}
|
|
71
67
|
|
|
@@ -95,7 +91,7 @@ export class RouteLifecycleNamespace<
|
|
|
95
91
|
*/
|
|
96
92
|
addCanActivate(
|
|
97
93
|
name: string,
|
|
98
|
-
handler:
|
|
94
|
+
handler: GuardFnFactory<Dependencies> | boolean,
|
|
99
95
|
skipValidation: boolean,
|
|
100
96
|
): void {
|
|
101
97
|
if (!skipValidation) {
|
|
@@ -137,7 +133,7 @@ export class RouteLifecycleNamespace<
|
|
|
137
133
|
*/
|
|
138
134
|
addCanDeactivate(
|
|
139
135
|
name: string,
|
|
140
|
-
handler:
|
|
136
|
+
handler: GuardFnFactory<Dependencies> | boolean,
|
|
141
137
|
skipValidation: boolean,
|
|
142
138
|
): void {
|
|
143
139
|
if (!skipValidation) {
|
|
@@ -208,17 +204,11 @@ export class RouteLifecycleNamespace<
|
|
|
208
204
|
* @returns Tuple of [canDeactivateFactories, canActivateFactories]
|
|
209
205
|
*/
|
|
210
206
|
getFactories(): [
|
|
211
|
-
Record<string,
|
|
212
|
-
Record<string,
|
|
207
|
+
Record<string, GuardFnFactory<Dependencies>>,
|
|
208
|
+
Record<string, GuardFnFactory<Dependencies>>,
|
|
213
209
|
] {
|
|
214
|
-
const deactivateRecord: Record<
|
|
215
|
-
|
|
216
|
-
ActivationFnFactory<Dependencies>
|
|
217
|
-
> = {};
|
|
218
|
-
const activateRecord: Record<
|
|
219
|
-
string,
|
|
220
|
-
ActivationFnFactory<Dependencies>
|
|
221
|
-
> = {};
|
|
210
|
+
const deactivateRecord: Record<string, GuardFnFactory<Dependencies>> = {};
|
|
211
|
+
const activateRecord: Record<string, GuardFnFactory<Dependencies>> = {};
|
|
222
212
|
|
|
223
213
|
for (const [name, factory] of this.#canDeactivateFactories) {
|
|
224
214
|
deactivateRecord[name] = factory;
|
|
@@ -236,7 +226,7 @@ export class RouteLifecycleNamespace<
|
|
|
236
226
|
*
|
|
237
227
|
* @returns Tuple of [canDeactivateFunctions, canActivateFunctions] as Maps
|
|
238
228
|
*/
|
|
239
|
-
getFunctions(): [Map<string,
|
|
229
|
+
getFunctions(): [Map<string, GuardFn>, Map<string, GuardFn>] {
|
|
240
230
|
return [this.#canDeactivateFunctions, this.#canActivateFunctions];
|
|
241
231
|
}
|
|
242
232
|
|
|
@@ -279,9 +269,9 @@ export class RouteLifecycleNamespace<
|
|
|
279
269
|
#registerHandler(
|
|
280
270
|
type: "activate" | "deactivate",
|
|
281
271
|
name: string,
|
|
282
|
-
handler:
|
|
283
|
-
factories: Map<string,
|
|
284
|
-
functions: Map<string,
|
|
272
|
+
handler: GuardFnFactory<Dependencies> | boolean,
|
|
273
|
+
factories: Map<string, GuardFnFactory<Dependencies>>,
|
|
274
|
+
functions: Map<string, GuardFn>,
|
|
285
275
|
methodName: string,
|
|
286
276
|
isOverwrite: boolean,
|
|
287
277
|
): void {
|
|
@@ -328,7 +318,7 @@ export class RouteLifecycleNamespace<
|
|
|
328
318
|
}
|
|
329
319
|
|
|
330
320
|
#checkGuardSync(
|
|
331
|
-
functions: Map<string,
|
|
321
|
+
functions: Map<string, GuardFn>,
|
|
332
322
|
name: string,
|
|
333
323
|
toState: State,
|
|
334
324
|
fromState: State | undefined,
|
|
@@ -347,17 +337,12 @@ export class RouteLifecycleNamespace<
|
|
|
347
337
|
return result;
|
|
348
338
|
}
|
|
349
339
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
);
|
|
355
|
-
|
|
356
|
-
return false;
|
|
357
|
-
}
|
|
340
|
+
logger.warn(
|
|
341
|
+
`router.${methodName}`,
|
|
342
|
+
`Guard for "${name}" returned a Promise. Sync check cannot resolve async guards — returning false.`,
|
|
343
|
+
);
|
|
358
344
|
|
|
359
|
-
|
|
360
|
-
return true;
|
|
345
|
+
return false;
|
|
361
346
|
} catch {
|
|
362
347
|
return false;
|
|
363
348
|
}
|
|
@@ -9,7 +9,7 @@ import { isBoolean, getTypeDescription } from "type-guards";
|
|
|
9
9
|
|
|
10
10
|
import { DEFAULT_LIMITS } from "../../constants";
|
|
11
11
|
|
|
12
|
-
import type {
|
|
12
|
+
import type { GuardFnFactory } from "../../types";
|
|
13
13
|
import type { DefaultDependencies } from "@real-router/types";
|
|
14
14
|
|
|
15
15
|
/**
|
|
@@ -18,7 +18,7 @@ import type { DefaultDependencies } from "@real-router/types";
|
|
|
18
18
|
export function validateHandler<D extends DefaultDependencies>(
|
|
19
19
|
handler: unknown,
|
|
20
20
|
methodName: string,
|
|
21
|
-
): asserts handler is
|
|
21
|
+
): asserts handler is GuardFnFactory<D> | boolean {
|
|
22
22
|
if (!isBoolean(handler) && typeof handler !== "function") {
|
|
23
23
|
throw new TypeError(
|
|
24
24
|
`[router.${methodName}] Handler must be a boolean or factory function, ` +
|
|
@@ -59,7 +59,7 @@ export function validateHandlerLimit(
|
|
|
59
59
|
throw new Error(
|
|
60
60
|
`[router.${methodName}] Lifecycle handler limit exceeded (${maxLifecycleHandlers}). ` +
|
|
61
61
|
`This indicates too many routes with individual handlers. ` +
|
|
62
|
-
`Consider using
|
|
62
|
+
`Consider using plugins for cross-cutting concerns.`,
|
|
63
63
|
);
|
|
64
64
|
}
|
|
65
65
|
}
|
|
@@ -39,8 +39,8 @@ import { getTransitionPath } from "../../transitionPath";
|
|
|
39
39
|
|
|
40
40
|
import type { RouteConfig, RoutesDependencies } from "./types";
|
|
41
41
|
import type {
|
|
42
|
-
ActivationFnFactory,
|
|
43
42
|
BuildStateResultWithSegments,
|
|
43
|
+
GuardFnFactory,
|
|
44
44
|
Route,
|
|
45
45
|
RouteConfigUpdate,
|
|
46
46
|
} from "../../types";
|
|
@@ -77,22 +77,27 @@ export class RoutesNamespace<
|
|
|
77
77
|
|
|
78
78
|
readonly #definitions: RouteDefinition[] = [];
|
|
79
79
|
readonly #config: RouteConfig = createEmptyConfig();
|
|
80
|
-
|
|
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(
|
|
81
86
|
null,
|
|
82
|
-
) as Record<string, string
|
|
87
|
+
) as Record<string, Record<string, unknown>>;
|
|
83
88
|
|
|
84
89
|
// Pending canActivate handlers that need to be registered after router is set
|
|
85
90
|
// Key: route name, Value: canActivate factory
|
|
86
91
|
readonly #pendingCanActivate = new Map<
|
|
87
92
|
string,
|
|
88
|
-
|
|
93
|
+
GuardFnFactory<Dependencies>
|
|
89
94
|
>();
|
|
90
95
|
|
|
91
96
|
// Pending canDeactivate handlers that need to be registered after router is set
|
|
92
97
|
// Key: route name, Value: canDeactivate factory
|
|
93
98
|
readonly #pendingCanDeactivate = new Map<
|
|
94
99
|
string,
|
|
95
|
-
|
|
100
|
+
GuardFnFactory<Dependencies>
|
|
96
101
|
>();
|
|
97
102
|
|
|
98
103
|
#rootPath = "";
|
|
@@ -346,6 +351,18 @@ export class RoutesNamespace<
|
|
|
346
351
|
return this.#enrichRoute(definition, name);
|
|
347
352
|
}
|
|
348
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
|
+
|
|
349
366
|
/**
|
|
350
367
|
* Adds one or more routes to the router.
|
|
351
368
|
* Input already validated by facade (properties and state-dependent checks).
|
|
@@ -473,31 +490,15 @@ export class RoutesNamespace<
|
|
|
473
490
|
clearRoutes(): void {
|
|
474
491
|
this.#definitions.length = 0;
|
|
475
492
|
|
|
476
|
-
//
|
|
477
|
-
|
|
478
|
-
delete this.#config.decoders[key];
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
for (const key in this.#config.encoders) {
|
|
482
|
-
delete this.#config.encoders[key];
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
for (const key in this.#config.defaultParams) {
|
|
486
|
-
delete this.#config.defaultParams[key];
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
for (const key in this.#config.forwardMap) {
|
|
490
|
-
delete this.#config.forwardMap[key];
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
for (const key in this.#config.forwardFnMap) {
|
|
494
|
-
delete this.#config.forwardFnMap[key];
|
|
495
|
-
}
|
|
493
|
+
// Reset config to empty null-prototype objects
|
|
494
|
+
Object.assign(this.#config, createEmptyConfig());
|
|
496
495
|
|
|
497
496
|
// Clear forward cache
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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
|
+
>;
|
|
501
502
|
|
|
502
503
|
// Rebuild empty tree
|
|
503
504
|
this.#rebuildTree();
|
|
@@ -872,6 +873,7 @@ export class RoutesNamespace<
|
|
|
872
873
|
applyClonedConfig(
|
|
873
874
|
config: RouteConfig,
|
|
874
875
|
resolvedForwardMap: Record<string, string>,
|
|
876
|
+
routeCustomFields: Record<string, Record<string, unknown>>,
|
|
875
877
|
): void {
|
|
876
878
|
Object.assign(this.#config.decoders, config.decoders);
|
|
877
879
|
Object.assign(this.#config.encoders, config.encoders);
|
|
@@ -879,6 +881,7 @@ export class RoutesNamespace<
|
|
|
879
881
|
Object.assign(this.#config.forwardMap, config.forwardMap);
|
|
880
882
|
Object.assign(this.#config.forwardFnMap, config.forwardFnMap);
|
|
881
883
|
this.setResolvedForwardMap({ ...resolvedForwardMap });
|
|
884
|
+
Object.assign(this.#routeCustomFields, routeCustomFields);
|
|
882
885
|
}
|
|
883
886
|
|
|
884
887
|
/**
|
|
@@ -1219,9 +1222,7 @@ export class RoutesNamespace<
|
|
|
1219
1222
|
|
|
1220
1223
|
#validateAndCacheForwardMap(): void {
|
|
1221
1224
|
// Clear existing cache
|
|
1222
|
-
|
|
1223
|
-
delete this.#resolvedForwardMap[key];
|
|
1224
|
-
}
|
|
1225
|
+
this.#resolvedForwardMap = Object.create(null) as Record<string, string>;
|
|
1225
1226
|
|
|
1226
1227
|
// Resolve all chains
|
|
1227
1228
|
for (const fromRoute of Object.keys(this.#config.forwardMap)) {
|
|
@@ -1238,9 +1239,7 @@ export class RoutesNamespace<
|
|
|
1238
1239
|
*/
|
|
1239
1240
|
#cacheForwardMap(): void {
|
|
1240
1241
|
// Clear existing cache
|
|
1241
|
-
|
|
1242
|
-
delete this.#resolvedForwardMap[key];
|
|
1243
|
-
}
|
|
1242
|
+
this.#resolvedForwardMap = Object.create(null) as Record<string, string>;
|
|
1244
1243
|
|
|
1245
1244
|
// Resolve chains without validation
|
|
1246
1245
|
for (const fromRoute of Object.keys(this.#config.forwardMap)) {
|
|
@@ -1263,6 +1262,7 @@ export class RoutesNamespace<
|
|
|
1263
1262
|
clearConfigEntries(this.#config.defaultParams, shouldClear);
|
|
1264
1263
|
clearConfigEntries(this.#config.forwardMap, shouldClear);
|
|
1265
1264
|
clearConfigEntries(this.#config.forwardFnMap, shouldClear);
|
|
1265
|
+
clearConfigEntries(this.#routeCustomFields, shouldClear);
|
|
1266
1266
|
|
|
1267
1267
|
// Clear forwardMap entries pointing TO deleted route
|
|
1268
1268
|
clearConfigEntries(this.#config.forwardMap, (key) =>
|
|
@@ -1305,6 +1305,25 @@ export class RoutesNamespace<
|
|
|
1305
1305
|
route: Route<Dependencies>,
|
|
1306
1306
|
fullName: string,
|
|
1307
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
|
+
|
|
1308
1327
|
// Register canActivate via deps.canActivate (allows tests to spy on router.canActivate)
|
|
1309
1328
|
if (route.canActivate) {
|
|
1310
1329
|
// Note: Uses #depsStore directly because this method is called from constructor
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// packages/core/src/namespaces/RoutesNamespace/types.ts
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type { GuardFnFactory } from "../../types";
|
|
4
4
|
import type {
|
|
5
5
|
DefaultDependencies,
|
|
6
6
|
ForwardToCallback,
|
|
@@ -22,13 +22,13 @@ export interface RoutesDependencies<
|
|
|
22
22
|
/** Register canActivate handler for a route */
|
|
23
23
|
addActivateGuard: (
|
|
24
24
|
name: string,
|
|
25
|
-
handler:
|
|
25
|
+
handler: GuardFnFactory<Dependencies>,
|
|
26
26
|
) => void;
|
|
27
27
|
|
|
28
28
|
/** Register canDeactivate handler for a route */
|
|
29
29
|
addDeactivateGuard: (
|
|
30
30
|
name: string,
|
|
31
|
-
handler:
|
|
31
|
+
handler: GuardFnFactory<Dependencies>,
|
|
32
32
|
) => void;
|
|
33
33
|
|
|
34
34
|
/** Create state object */
|
package/src/namespaces/index.ts
CHANGED
package/src/transitionPath.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// packages/
|
|
1
|
+
// packages/core/src/transitionPath.ts
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type { State } from "@real-router/types";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Parameters extracted from a route segment.
|
|
@@ -99,19 +99,7 @@ function extractSegmentParams(name: string, state: State): SegmentParams {
|
|
|
99
99
|
|
|
100
100
|
const result: SegmentParams = {};
|
|
101
101
|
|
|
102
|
-
for (const key
|
|
103
|
-
// Skip inherited properties
|
|
104
|
-
if (!Object.hasOwn(keys, key)) {
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Skip undefined values for consistent behavior (treat { key: undefined } same as missing key)
|
|
109
|
-
// Edge case: can appear from manual State creation or object merging
|
|
110
|
-
// @ts-expect-error Params type doesn't allow undefined, but it can appear at runtime
|
|
111
|
-
if (keys[key] === undefined) {
|
|
112
|
-
continue;
|
|
113
|
-
}
|
|
114
|
-
|
|
102
|
+
for (const key of Object.keys(keys)) {
|
|
115
103
|
const value = state.params[key];
|
|
116
104
|
|
|
117
105
|
// Skip null/undefined values
|
|
@@ -166,15 +154,9 @@ function pointOfDifference(
|
|
|
166
154
|
const toParams = extractSegmentParams(toSegment, toState);
|
|
167
155
|
const fromParams = extractSegmentParams(fromSegment, fromState);
|
|
168
156
|
|
|
169
|
-
//
|
|
157
|
+
// Compare parameter values
|
|
170
158
|
const toKeys = Object.keys(toParams);
|
|
171
|
-
const fromKeys = Object.keys(fromParams);
|
|
172
159
|
|
|
173
|
-
if (toKeys.length !== fromKeys.length) {
|
|
174
|
-
return i;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// Detailed check: compare parameter values
|
|
178
160
|
for (const key of toKeys) {
|
|
179
161
|
if (toParams[key] !== fromParams[key]) {
|
|
180
162
|
return i;
|
|
@@ -384,24 +366,6 @@ export function getTransitionPath(
|
|
|
384
366
|
};
|
|
385
367
|
}
|
|
386
368
|
|
|
387
|
-
// ===== FAST PATH 4: Same routes with empty meta.params =====
|
|
388
|
-
// If both have empty meta.params {}, no parameter checking needed
|
|
389
|
-
if (toState.name === fromState.name && toHasMeta && fromHasMeta) {
|
|
390
|
-
const toParamsEmpty =
|
|
391
|
-
toState.meta && Object.keys(toState.meta.params).length === 0;
|
|
392
|
-
const fromParamsEmpty =
|
|
393
|
-
fromState.meta && Object.keys(fromState.meta.params).length === 0;
|
|
394
|
-
|
|
395
|
-
if (toParamsEmpty && fromParamsEmpty) {
|
|
396
|
-
// Both have empty params - no transition needed
|
|
397
|
-
return {
|
|
398
|
-
intersection: toState.name,
|
|
399
|
-
toActivate: [],
|
|
400
|
-
toDeactivate: [],
|
|
401
|
-
};
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
|
|
405
369
|
// ===== STANDARD PATH: Routes with parameters =====
|
|
406
370
|
// Use original algorithm for complex cases with parameters
|
|
407
371
|
const toStateIds = nameToIDs(toState.name);
|