@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.
Files changed (45) hide show
  1. package/README.md +0 -15
  2. package/dist/cjs/index.d.ts +12 -13
  3. package/dist/cjs/index.js +1 -1
  4. package/dist/cjs/index.js.map +1 -1
  5. package/dist/cjs/metafile-cjs.json +1 -1
  6. package/dist/esm/index.d.mts +12 -13
  7. package/dist/esm/index.mjs +1 -1
  8. package/dist/esm/index.mjs.map +1 -1
  9. package/dist/esm/metafile-esm.json +1 -1
  10. package/package.json +4 -4
  11. package/src/Router.ts +14 -57
  12. package/src/RouterError.ts +1 -1
  13. package/src/constants.ts +1 -3
  14. package/src/helpers.ts +1 -1
  15. package/src/index.ts +3 -3
  16. package/src/namespaces/CloneNamespace/CloneNamespace.ts +6 -6
  17. package/src/namespaces/CloneNamespace/types.ts +5 -9
  18. package/src/namespaces/DependenciesNamespace/DependenciesNamespace.ts +2 -4
  19. package/src/namespaces/NavigationNamespace/NavigationNamespace.ts +1 -0
  20. package/src/namespaces/NavigationNamespace/transition/{wrapSyncError.ts → errorHandling.ts} +23 -17
  21. package/src/namespaces/NavigationNamespace/transition/executeLifecycleHooks.ts +14 -55
  22. package/src/namespaces/NavigationNamespace/transition/index.ts +6 -24
  23. package/src/namespaces/NavigationNamespace/types.ts +2 -9
  24. package/src/namespaces/PluginsNamespace/PluginsNamespace.ts +2 -2
  25. package/src/namespaces/RouteLifecycleNamespace/RouteLifecycleNamespace.ts +28 -43
  26. package/src/namespaces/RouteLifecycleNamespace/validators.ts +3 -3
  27. package/src/namespaces/RoutesNamespace/RoutesNamespace.ts +53 -34
  28. package/src/namespaces/RoutesNamespace/stateBuilder.ts +1 -1
  29. package/src/namespaces/RoutesNamespace/types.ts +3 -3
  30. package/src/namespaces/index.ts +0 -2
  31. package/src/transitionPath.ts +4 -40
  32. package/src/typeGuards.ts +1 -2
  33. package/src/types.ts +8 -8
  34. package/src/wiring/RouterWiringBuilder.ts +1 -17
  35. package/src/wiring/types.ts +0 -3
  36. package/src/wiring/wireRouter.ts +0 -1
  37. package/src/namespaces/MiddlewareNamespace/MiddlewareNamespace.ts +0 -221
  38. package/src/namespaces/MiddlewareNamespace/constants.ts +0 -3
  39. package/src/namespaces/MiddlewareNamespace/index.ts +0 -5
  40. package/src/namespaces/MiddlewareNamespace/types.ts +0 -28
  41. package/src/namespaces/MiddlewareNamespace/validators.ts +0 -95
  42. package/src/namespaces/NavigationNamespace/transition/executeMiddleware.ts +0 -56
  43. package/src/namespaces/NavigationNamespace/transition/makeError.ts +0 -37
  44. package/src/namespaces/NavigationNamespace/transition/mergeStates.ts +0 -54
  45. package/src/namespaces/NavigationNamespace/transition/processLifecycleResult.ts +0 -81
@@ -1,32 +1,26 @@
1
- // packages/real-router/modules/transition/executeLifecycleHooks.ts
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 { State, ActivationFn } from "@real-router/types";
5
+ import type { GuardFn, State } from "@real-router/types";
13
6
 
14
7
  // Helper: execution of the Lifecycle Hooks group
15
- export const executeLifecycleHooks = async (
16
- hooks: Map<string, ActivationFn>,
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<State> => {
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 currentState;
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
- const result = hookFn(currentState, fromState);
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
- return currentState;
84
- };
39
+ if (!result) {
40
+ throw new RouterError(errorCode, { segment });
41
+ }
42
+ }
43
+ }
@@ -1,7 +1,6 @@
1
- // packages/real-router/modules/transition/index.ts
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
- currentState = await executeLifecycleHooks(
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
- currentState = await executeLifecycleHooks(
52
+ await executeLifecycleHooks(
58
53
  canActivateFunctions,
59
- currentState,
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: currentState,
79
+ state: toState,
98
80
  meta: {
99
- phase: "middleware",
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
- ActivationFn,
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
- * Unlike {@link MiddlewareNamespace.clearAll}, active disposal is required
226
- * plugins have an active lifecycle (event subscriptions, teardown hooks).
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, isPromise, getTypeDescription } from "type-guards";
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 { ActivationFnFactory, Limits } from "../../types";
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 an activation function factory.
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
- ): ActivationFnFactory<Dependencies> {
30
- const activationFn: ActivationFn = () => value;
25
+ ): GuardFnFactory<Dependencies> {
26
+ const guardFn: GuardFn = () => value;
31
27
 
32
- return () => activationFn;
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
- ActivationFnFactory<Dependencies>
42
+ GuardFnFactory<Dependencies>
47
43
  >();
48
44
  readonly #canActivateFactories = new Map<
49
45
  string,
50
- ActivationFnFactory<Dependencies>
46
+ GuardFnFactory<Dependencies>
51
47
  >();
52
- readonly #canDeactivateFunctions = new Map<string, ActivationFn>();
53
- readonly #canActivateFunctions = new Map<string, ActivationFn>();
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 ActivationFnFactory<D> | boolean {
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: ActivationFnFactory<Dependencies> | boolean,
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: ActivationFnFactory<Dependencies> | boolean,
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, ActivationFnFactory<Dependencies>>,
212
- Record<string, ActivationFnFactory<Dependencies>>,
207
+ Record<string, GuardFnFactory<Dependencies>>,
208
+ Record<string, GuardFnFactory<Dependencies>>,
213
209
  ] {
214
- const deactivateRecord: Record<
215
- string,
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, ActivationFn>, Map<string, ActivationFn>] {
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: ActivationFnFactory<Dependencies> | boolean,
283
- factories: Map<string, ActivationFnFactory<Dependencies>>,
284
- functions: Map<string, ActivationFn>,
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, ActivationFn>,
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
- if (isPromise(result)) {
351
- logger.warn(
352
- `router.${methodName}`,
353
- `Guard for "${name}" returned a Promise. Sync check cannot resolve async guards — returning false.`,
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
- // Guard returned void/State — permissive default
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 { ActivationFnFactory } from "../../types";
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 ActivationFnFactory<D> | boolean {
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 middleware for cross-cutting concerns.`,
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
- readonly #resolvedForwardMap: Record<string, string> = Object.create(
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
- ActivationFnFactory<Dependencies>
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
- ActivationFnFactory<Dependencies>
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
- // Clear all config entries
477
- for (const key in this.#config.decoders) {
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
- for (const key in this.#resolvedForwardMap) {
499
- delete this.#resolvedForwardMap[key];
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
- for (const key in this.#resolvedForwardMap) {
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
- for (const key in this.#resolvedForwardMap) {
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,4 +1,4 @@
1
- // packages/real-router/modules/core/stateBuilder.ts
1
+ // packages/core/src/namespaces/RoutesNamespace/stateBuilder.ts
2
2
 
3
3
  /**
4
4
  * State Builder Utilities.
@@ -1,6 +1,6 @@
1
1
  // packages/core/src/namespaces/RoutesNamespace/types.ts
2
2
 
3
- import type { ActivationFnFactory } from "../../types";
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: ActivationFnFactory<Dependencies>,
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: ActivationFnFactory<Dependencies>,
31
+ handler: GuardFnFactory<Dependencies>,
32
32
  ) => void;
33
33
 
34
34
  /** Create state object */
@@ -12,8 +12,6 @@ export {
12
12
 
13
13
  export { StateNamespace } from "./StateNamespace";
14
14
 
15
- export { MiddlewareNamespace } from "./MiddlewareNamespace";
16
-
17
15
  export {
18
16
  PluginsNamespace,
19
17
  EVENTS_MAP,
@@ -1,6 +1,6 @@
1
- // packages/real-router/modules/transitionPath.ts
1
+ // packages/core/src/transitionPath.ts
2
2
 
3
- import type { Params, State } from "@real-router/types";
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 in keys as Params) {
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
- // Fast check: different number of parameters
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);