@real-router/core 0.36.0 → 0.36.2

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 (35) hide show
  1. package/dist/cjs/api.js +1 -1
  2. package/dist/cjs/api.js.map +1 -1
  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/api.mjs +1 -1
  7. package/dist/esm/api.mjs.map +1 -1
  8. package/dist/esm/chunk-PKKD6URG.mjs +1 -0
  9. package/dist/esm/chunk-PKKD6URG.mjs.map +1 -0
  10. package/dist/esm/index.mjs +1 -1
  11. package/dist/esm/metafile-esm.json +1 -1
  12. package/package.json +4 -4
  13. package/src/Router.ts +33 -12
  14. package/src/api/getPluginApi.ts +10 -4
  15. package/src/constants.ts +2 -0
  16. package/src/fsm/routerFSM.ts +2 -21
  17. package/src/internals.ts +21 -2
  18. package/src/namespaces/EventBusNamespace/EventBusNamespace.ts +50 -39
  19. package/src/namespaces/NavigationNamespace/NavigationNamespace.ts +221 -153
  20. package/src/namespaces/NavigationNamespace/constants.ts +55 -0
  21. package/src/namespaces/NavigationNamespace/transition/completeTransition.ts +100 -0
  22. package/src/namespaces/NavigationNamespace/transition/errorHandling.ts +34 -0
  23. package/src/namespaces/NavigationNamespace/transition/guardPhase.ts +214 -0
  24. package/src/namespaces/NavigationNamespace/types.ts +14 -30
  25. package/src/namespaces/RouteLifecycleNamespace/RouteLifecycleNamespace.ts +6 -1
  26. package/src/namespaces/RoutesNamespace/RoutesNamespace.ts +36 -35
  27. package/src/namespaces/RoutesNamespace/forwardToValidation.ts +2 -5
  28. package/src/namespaces/RoutesNamespace/types.ts +1 -2
  29. package/src/namespaces/StateNamespace/StateNamespace.ts +13 -17
  30. package/src/transitionPath.ts +68 -39
  31. package/src/wiring/RouterWiringBuilder.ts +8 -11
  32. package/dist/esm/chunk-AP4ME3HM.mjs +0 -1
  33. package/dist/esm/chunk-AP4ME3HM.mjs.map +0 -1
  34. package/src/namespaces/NavigationNamespace/transition/executeLifecycleGuards.ts +0 -52
  35. package/src/namespaces/NavigationNamespace/transition/index.ts +0 -93
@@ -0,0 +1,214 @@
1
+ import { handleGuardError } from "./errorHandling";
2
+ import { errorCodes } from "../../../constants";
3
+ import { RouterError } from "../../../RouterError";
4
+
5
+ import type { GuardFn, State } from "@real-router/types";
6
+
7
+ async function resolveAsyncGuard(
8
+ promise: Promise<boolean>,
9
+ errorCode: string,
10
+ segment: string,
11
+ ): Promise<void> {
12
+ let result: boolean;
13
+
14
+ try {
15
+ result = await promise;
16
+ } catch (error: unknown) {
17
+ handleGuardError(error, errorCode, segment);
18
+
19
+ return; // unreachable — handleGuardError returns never
20
+ }
21
+
22
+ if (!result) {
23
+ throw new RouterError(errorCode, { segment });
24
+ }
25
+ }
26
+
27
+ async function resolveRemainingGuards(
28
+ guards: Map<string, GuardFn>,
29
+ segments: string[],
30
+ errorCode: string,
31
+ toState: State,
32
+ fromState: State | undefined,
33
+ signal: AbortSignal | undefined,
34
+ isActive: () => boolean,
35
+ startIndex: number,
36
+ firstResult: Promise<boolean>,
37
+ firstSegment: string,
38
+ ): Promise<void> {
39
+ await resolveAsyncGuard(firstResult, errorCode, firstSegment);
40
+
41
+ for (let i = startIndex; i < segments.length; i++) {
42
+ if (!isActive()) {
43
+ throw new RouterError(errorCodes.TRANSITION_CANCELLED);
44
+ }
45
+
46
+ const segment = segments[i];
47
+ const guardFn = guards.get(segment);
48
+
49
+ if (!guardFn) {
50
+ continue;
51
+ }
52
+
53
+ let guardResult: boolean | Promise<boolean> = false;
54
+
55
+ try {
56
+ guardResult = guardFn(toState, fromState, signal);
57
+ } catch (error: unknown) {
58
+ handleGuardError(error, errorCode, segment);
59
+ }
60
+
61
+ if (guardResult instanceof Promise) {
62
+ await resolveAsyncGuard(guardResult, errorCode, segment);
63
+ continue;
64
+ }
65
+
66
+ if (!guardResult) {
67
+ throw new RouterError(errorCode, { segment });
68
+ }
69
+ }
70
+ }
71
+
72
+ async function finishAsyncPipeline(
73
+ deactivateCompletion: Promise<void>,
74
+ activateGuards: Map<string, GuardFn>,
75
+ toActivate: string[],
76
+ shouldActivate: boolean,
77
+ toState: State,
78
+ fromState: State | undefined,
79
+ signal: AbortSignal,
80
+ isActive: () => boolean,
81
+ ): Promise<void> {
82
+ await deactivateCompletion;
83
+
84
+ if (!isActive()) {
85
+ throw new RouterError(errorCodes.TRANSITION_CANCELLED);
86
+ }
87
+
88
+ if (shouldActivate) {
89
+ const pending = runGuards(
90
+ activateGuards,
91
+ toActivate,
92
+ errorCodes.CANNOT_ACTIVATE,
93
+ toState,
94
+ fromState,
95
+ signal,
96
+ isActive,
97
+ );
98
+
99
+ if (pending !== undefined) {
100
+ await pending;
101
+ }
102
+
103
+ if (!isActive()) {
104
+ throw new RouterError(errorCodes.TRANSITION_CANCELLED);
105
+ }
106
+ }
107
+ }
108
+
109
+ export function executeGuardPipeline(
110
+ deactivateGuards: Map<string, GuardFn>,
111
+ activateGuards: Map<string, GuardFn>,
112
+ toDeactivate: string[],
113
+ toActivate: string[],
114
+ shouldDeactivate: boolean,
115
+ shouldActivate: boolean,
116
+ toState: State,
117
+ fromState: State | undefined,
118
+ signal: AbortSignal,
119
+ isActive: () => boolean,
120
+ ): Promise<void> | undefined {
121
+ if (shouldDeactivate) {
122
+ const pending = runGuards(
123
+ deactivateGuards,
124
+ toDeactivate,
125
+ errorCodes.CANNOT_DEACTIVATE,
126
+ toState,
127
+ fromState,
128
+ signal,
129
+ isActive,
130
+ );
131
+
132
+ if (pending !== undefined) {
133
+ return finishAsyncPipeline(
134
+ pending,
135
+ activateGuards,
136
+ toActivate,
137
+ shouldActivate,
138
+ toState,
139
+ fromState,
140
+ signal,
141
+ isActive,
142
+ );
143
+ }
144
+ }
145
+
146
+ if (!isActive()) {
147
+ throw new RouterError(errorCodes.TRANSITION_CANCELLED);
148
+ }
149
+
150
+ if (shouldActivate) {
151
+ return runGuards(
152
+ activateGuards,
153
+ toActivate,
154
+ errorCodes.CANNOT_ACTIVATE,
155
+ toState,
156
+ fromState,
157
+ signal,
158
+ isActive,
159
+ );
160
+ }
161
+
162
+ return undefined;
163
+ }
164
+
165
+ function runGuards(
166
+ guards: Map<string, GuardFn>,
167
+ segments: string[],
168
+ errorCode: string,
169
+ toState: State,
170
+ fromState: State | undefined,
171
+ signal: AbortSignal | undefined,
172
+ isActive: () => boolean,
173
+ ): Promise<void> | undefined {
174
+ for (const [i, segment] of segments.entries()) {
175
+ if (!isActive()) {
176
+ throw new RouterError(errorCodes.TRANSITION_CANCELLED);
177
+ }
178
+
179
+ const guardFn = guards.get(segment);
180
+
181
+ if (!guardFn) {
182
+ continue;
183
+ }
184
+
185
+ let guardResult: boolean | Promise<boolean> = false;
186
+
187
+ try {
188
+ guardResult = guardFn(toState, fromState, signal);
189
+ } catch (error: unknown) {
190
+ handleGuardError(error, errorCode, segment);
191
+ }
192
+
193
+ if (guardResult instanceof Promise) {
194
+ return resolveRemainingGuards(
195
+ guards,
196
+ segments,
197
+ errorCode,
198
+ toState,
199
+ fromState,
200
+ signal,
201
+ isActive,
202
+ i + 1,
203
+ guardResult,
204
+ segment,
205
+ );
206
+ }
207
+
208
+ if (!guardResult) {
209
+ throw new RouterError(errorCode, { segment });
210
+ }
211
+ }
212
+
213
+ return undefined;
214
+ }
@@ -1,16 +1,23 @@
1
1
  // packages/core/src/namespaces/NavigationNamespace/types.ts
2
2
 
3
- import type { BuildStateResultWithSegments } from "../../types";
4
3
  import type {
5
4
  GuardFn,
6
5
  NavigationOptions,
7
6
  Options,
8
7
  Params,
9
8
  State,
10
- StateMetaInput,
11
- TransitionPhase,
12
9
  } from "@real-router/types";
13
10
 
11
+ export interface NavigationContext {
12
+ toState: State;
13
+ fromState: State | undefined;
14
+ opts: NavigationOptions;
15
+ toDeactivate: string[];
16
+ toActivate: string[];
17
+ intersection: string;
18
+ canDeactivateFunctions: Map<string, GuardFn>;
19
+ }
20
+
14
21
  /**
15
22
  * Dependencies injected into NavigationNamespace.
16
23
  *
@@ -30,22 +37,11 @@ export interface NavigationDependencies {
30
37
  /** Set router state */
31
38
  setState: (state: State) => void;
32
39
 
33
- /** Build state with segments from route name and params */
34
- buildStateWithSegments: <P extends Params = Params>(
40
+ /** Build complete navigate state: forwardState + route check + buildPath + makeState in one step */
41
+ buildNavigateState: (
35
42
  routeName: string,
36
- routeParams: P,
37
- ) => BuildStateResultWithSegments<P> | undefined;
38
-
39
- /** Make state object with path and meta */
40
- makeState: <P extends Params = Params, MP extends Params = Params>(
41
- name: string,
42
- params?: P,
43
- path?: string,
44
- meta?: StateMetaInput<MP>,
45
- ) => State<P, MP>;
46
-
47
- /** Build path from route name and params */
48
- buildPath: (route: string, params?: Params) => string;
43
+ routeParams: Params,
44
+ ) => State | undefined;
49
45
 
50
46
  /** Check if states are equal */
51
47
  areStatesEqual: (
@@ -106,15 +102,3 @@ export interface NavigationDependencies {
106
102
  /** Clear canDeactivate guard for a route */
107
103
  clearCanDeactivate: (name: string) => void;
108
104
  }
109
-
110
- export interface TransitionOutput {
111
- state: State;
112
- meta: {
113
- phase: TransitionPhase;
114
- segments: {
115
- deactivated: string[];
116
- activated: string[];
117
- intersection: string;
118
- };
119
- };
120
- }
@@ -42,6 +42,11 @@ export class RouteLifecycleNamespace<
42
42
  >();
43
43
  readonly #canDeactivateFunctions = new Map<string, GuardFn>();
44
44
  readonly #canActivateFunctions = new Map<string, GuardFn>();
45
+ // Cached tuple — Maps never change reference, so this is stable
46
+ readonly #functionsTuple: [Map<string, GuardFn>, Map<string, GuardFn>] = [
47
+ this.#canDeactivateFunctions,
48
+ this.#canActivateFunctions,
49
+ ];
45
50
 
46
51
  readonly #registering = new Set<string>();
47
52
  readonly #definitionActivateGuardNames = new Set<string>();
@@ -249,7 +254,7 @@ export class RouteLifecycleNamespace<
249
254
  * @returns Tuple of [canDeactivateFunctions, canActivateFunctions] as Maps
250
255
  */
251
256
  getFunctions(): [Map<string, GuardFn>, Map<string, GuardFn>] {
252
- return [this.#canDeactivateFunctions, this.#canActivateFunctions];
257
+ return this.#functionsTuple;
253
258
  }
254
259
 
255
260
  canNavigateTo(
@@ -14,7 +14,7 @@ import { getTransitionPath } from "../../transitionPath";
14
14
 
15
15
  import type { RoutesStore } from "./routesStore";
16
16
  import type { RoutesDependencies } from "./types";
17
- import type { BuildStateResultWithSegments, Route } from "../../types";
17
+ import type { Route } from "../../types";
18
18
  import type { RouteLifecycleNamespace } from "../RouteLifecycleNamespace";
19
19
  import type {
20
20
  DefaultDependencies,
@@ -65,6 +65,11 @@ export function createRouteState<P extends RouteParams = RouteParams>(
65
65
  };
66
66
  }
67
67
 
68
+ interface CachedBuildPathOpts {
69
+ readonly trailingSlash?: "always" | "never" | undefined;
70
+ readonly queryParamsMode?: "default" | "strict" | "loose" | undefined;
71
+ }
72
+
68
73
  /**
69
74
  * Independent namespace for managing routes.
70
75
  *
@@ -75,6 +80,7 @@ export class RoutesNamespace<
75
80
  Dependencies extends DefaultDependencies = DefaultDependencies,
76
81
  > {
77
82
  readonly #store: RoutesStore<Dependencies>;
83
+ #cachedBuildPathOpts: CachedBuildPathOpts | undefined;
78
84
 
79
85
  get #deps(): RoutesDependencies<Dependencies> {
80
86
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@@ -210,13 +216,11 @@ export class RoutesNamespace<
210
216
  ? this.#store.config.encoders[route]({ ...paramsWithDefault })
211
217
  : paramsWithDefault;
212
218
 
213
- const ts = options?.trailingSlash;
214
- const trailingSlash = ts === "never" || ts === "always" ? ts : undefined;
215
-
216
- return this.#store.matcher.buildPath(route, encodedParams, {
217
- trailingSlash,
218
- queryParamsMode: options?.queryParamsMode,
219
- });
219
+ return this.#store.matcher.buildPath(
220
+ route,
221
+ encodedParams,
222
+ this.#getBuildPathOptions(options),
223
+ );
220
224
  }
221
225
 
222
226
  /**
@@ -267,9 +271,7 @@ export class RoutesNamespace<
267
271
  });
268
272
  }
269
273
 
270
- return this.#deps.makeState<P, MP>(routeName, routeParams, builtPath, {
271
- params: meta as MP,
272
- });
274
+ return this.#deps.makeState<P, MP>(routeName, routeParams, builtPath, meta);
273
275
  }
274
276
 
275
277
  /**
@@ -359,30 +361,6 @@ export class RoutesNamespace<
359
361
  );
360
362
  }
361
363
 
362
- buildStateWithSegmentsResolved<P extends Params = Params>(
363
- resolvedName: string,
364
- resolvedParams: P,
365
- ): BuildStateResultWithSegments<P> | undefined {
366
- const segments = this.#store.matcher.getSegmentsByName(resolvedName);
367
-
368
- if (!segments) {
369
- return undefined;
370
- }
371
-
372
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
373
- const meta = this.#store.matcher.getMetaByName(resolvedName)!;
374
- const state = createRouteState<P>(
375
- {
376
- segments: segments as readonly RouteTree[],
377
- params: resolvedParams,
378
- meta,
379
- },
380
- resolvedName,
381
- );
382
-
383
- return { state, segments: segments as readonly RouteTree[] };
384
- }
385
-
386
364
  // =========================================================================
387
365
  // Query operations
388
366
  // =========================================================================
@@ -457,6 +435,14 @@ export class RoutesNamespace<
457
435
  );
458
436
  }
459
437
 
438
+ getMetaForState(
439
+ name: string,
440
+ ): Record<string, Record<string, "url" | "query">> | undefined {
441
+ return this.#store.matcher.hasRoute(name)
442
+ ? this.#store.matcher.getMetaByName(name)
443
+ : undefined;
444
+ }
445
+
460
446
  getUrlParams(name: string): string[] {
461
447
  const segments = this.#store.matcher.getSegmentsByName(name);
462
448
 
@@ -485,6 +471,21 @@ export class RoutesNamespace<
485
471
  return params;
486
472
  }
487
473
 
474
+ #getBuildPathOptions(options?: Options): CachedBuildPathOpts {
475
+ if (this.#cachedBuildPathOpts) {
476
+ return this.#cachedBuildPathOpts;
477
+ }
478
+
479
+ const ts = options?.trailingSlash;
480
+
481
+ this.#cachedBuildPathOpts = Object.freeze({
482
+ trailingSlash: ts === "never" || ts === "always" ? ts : undefined,
483
+ queryParamsMode: options?.queryParamsMode,
484
+ });
485
+
486
+ return this.#cachedBuildPathOpts;
487
+ }
488
+
488
489
  #resolveDynamicForward(
489
490
  startName: string,
490
491
  startFn: ForwardToCallback<Dependencies>,
@@ -284,11 +284,8 @@ function getTargetParams<Dependencies extends DefaultDependencies>(
284
284
  routes: readonly Route<Dependencies>[],
285
285
  ): Set<string> {
286
286
  if (existsInTree) {
287
- const toSegments = getSegmentsByName(tree, targetRoute);
288
-
289
- // toSegments won't be null since we checked existsInTree
290
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
291
- return getRequiredParams(toSegments!);
287
+ /* v8 ignore next -- @preserve: ?? fallback unreachable — existsInTree guarantees non-null */
288
+ return getRequiredParams(getSegmentsByName(tree, targetRoute) ?? []);
292
289
  }
293
290
 
294
291
  // Target is in batch
@@ -7,7 +7,6 @@ import type {
7
7
  Params,
8
8
  SimpleState,
9
9
  State,
10
- StateMetaInput,
11
10
  } from "@real-router/types";
12
11
 
13
12
  /**
@@ -36,7 +35,7 @@ export interface RoutesDependencies<
36
35
  name: string,
37
36
  params?: P,
38
37
  path?: string,
39
- meta?: StateMetaInput<MP>,
38
+ meta?: Record<string, Record<string, "url" | "query">>,
40
39
  ) => State<P, MP>;
41
40
 
42
41
  /** Get current router state */
@@ -3,10 +3,11 @@
3
3
  import { getTypeDescription, validateState } from "type-guards";
4
4
 
5
5
  import { areParamValuesEqual, getUrlParamsFromMeta } from "./helpers";
6
+ import { EMPTY_PARAMS } from "../../constants";
6
7
  import { freezeStateInPlace } from "../../helpers";
7
8
 
8
9
  import type { StateNamespaceDependencies } from "./types";
9
- import type { Params, State, StateMetaInput } from "@real-router/types";
10
+ import type { Params, State } from "@real-router/types";
10
11
  import type { RouteTreeStateMeta } from "route-tree";
11
12
 
12
13
  /**
@@ -85,7 +86,7 @@ export class StateNamespace {
85
86
  get<P extends Params = Params, MP extends Params = Params>():
86
87
  | State<P, MP>
87
88
  | undefined {
88
- return this.#frozenState as State<P, MP> | undefined;
89
+ return this.#frozenState as State<P, MP> | undefined; // NOSONAR -- generic narrowing needed for public API
89
90
  }
90
91
 
91
92
  /**
@@ -108,10 +109,8 @@ export class StateNamespace {
108
109
  /**
109
110
  * Returns the previous router state (before the last navigation).
110
111
  */
111
- getPrevious<P extends Params = Params, MP extends Params = Params>():
112
- | State<P, MP>
113
- | undefined {
114
- return this.#previousState as State<P, MP> | undefined;
112
+ getPrevious(): State | undefined {
113
+ return this.#previousState;
115
114
  }
116
115
 
117
116
  reset(): void {
@@ -144,15 +143,12 @@ export class StateNamespace {
144
143
  name: string,
145
144
  params?: P,
146
145
  path?: string,
147
- meta?: StateMetaInput<MP>,
146
+ meta?: RouteTreeStateMeta,
148
147
  forceId?: number,
148
+ skipFreeze?: boolean,
149
149
  ): State<P, MP> {
150
150
  const madeMeta = meta
151
- ? {
152
- ...meta,
153
- id: forceId ?? ++this.#stateId,
154
- params: meta.params,
155
- }
151
+ ? { id: forceId ?? ++this.#stateId, params: meta as unknown as MP }
156
152
  : undefined;
157
153
 
158
154
  // Optimization: O(1) lookup instead of O(depth) ancestor iteration
@@ -164,10 +160,10 @@ export class StateNamespace {
164
160
 
165
161
  if (hasDefaultParams) {
166
162
  mergedParams = { ...defaultParamsConfig[name], ...params } as P;
167
- } else if (params) {
168
- mergedParams = { ...params };
163
+ } else if (!params || params === EMPTY_PARAMS) {
164
+ mergedParams = EMPTY_PARAMS as P;
169
165
  } else {
170
- mergedParams = {} as P;
166
+ mergedParams = { ...params };
171
167
  }
172
168
 
173
169
  const state: State<P, MP> = {
@@ -177,7 +173,7 @@ export class StateNamespace {
177
173
  meta: madeMeta,
178
174
  };
179
175
 
180
- return freezeStateInPlace(state);
176
+ return skipFreeze ? state : freezeStateInPlace(state);
181
177
  }
182
178
 
183
179
  // =========================================================================
@@ -202,7 +198,7 @@ export class StateNamespace {
202
198
  }
203
199
 
204
200
  if (ignoreQueryParams) {
205
- const stateMeta = (state1.meta?.params ?? state2.meta?.params) as
201
+ const stateMeta = (state1.meta?.params ?? state2.meta?.params) as // NOSONAR -- narrowing from Params to RouteTreeStateMeta
206
202
  | RouteTreeStateMeta
207
203
  | undefined;
208
204