@real-router/core 0.55.0 → 0.57.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 (145) hide show
  1. package/dist/cjs/Router-BSGzVINO.js +6 -0
  2. package/dist/cjs/Router-BSGzVINO.js.map +1 -0
  3. package/dist/{esm/Router-Dg-zk8AS.d.mts → cjs/Router-hW6ivqrX.d.ts} +2 -2
  4. package/dist/cjs/Router-hW6ivqrX.d.ts.map +1 -0
  5. package/dist/cjs/api.d.ts +2 -2
  6. package/dist/cjs/api.d.ts.map +1 -1
  7. package/dist/cjs/api.js +1 -1
  8. package/dist/cjs/api.js.map +1 -1
  9. package/dist/cjs/{cloneRouter-C9Rth_8U.js → cloneRouter-7z-60z_f.js} +2 -2
  10. package/dist/cjs/{cloneRouter-C9Rth_8U.js.map → cloneRouter-7z-60z_f.js.map} +1 -1
  11. package/dist/cjs/{index-C-i6vx5Y.d.ts → index-BWUmnecT.d.ts} +1 -2
  12. package/dist/cjs/index-BWUmnecT.d.ts.map +1 -0
  13. package/dist/cjs/{RouterError-WhCzIWuc.d.ts → index-CYpAZCoc.d.ts} +19 -2
  14. package/dist/cjs/index-CYpAZCoc.d.ts.map +1 -0
  15. package/dist/cjs/{index-K1U_fqfJ.d.ts → index-D2WRiyWS.d.ts} +2 -2
  16. package/dist/cjs/index-D2WRiyWS.d.ts.map +1 -0
  17. package/dist/cjs/index.d.ts +5 -5
  18. package/dist/cjs/index.js +1 -1
  19. package/dist/cjs/{internals-CWMOL1B8.js → internals-DJjgSePy.js} +2 -2
  20. package/dist/cjs/internals-DJjgSePy.js.map +1 -0
  21. package/dist/cjs/utils.d.ts +1 -1
  22. package/dist/cjs/utils.js +1 -1
  23. package/dist/cjs/utils.js.map +1 -1
  24. package/dist/cjs/validation.d.ts +17 -5
  25. package/dist/cjs/validation.d.ts.map +1 -1
  26. package/dist/cjs/validation.js +1 -1
  27. package/dist/esm/Router-B7txWo9N.mjs +6 -0
  28. package/dist/esm/Router-B7txWo9N.mjs.map +1 -0
  29. package/dist/{cjs/Router-Dg-zk8AS.d.ts → esm/Router-hW6ivqrX.d.mts} +2 -2
  30. package/dist/esm/Router-hW6ivqrX.d.mts.map +1 -0
  31. package/dist/esm/api.d.mts +2 -2
  32. package/dist/esm/api.d.mts.map +1 -1
  33. package/dist/esm/api.mjs +1 -1
  34. package/dist/esm/api.mjs.map +1 -1
  35. package/dist/esm/{cloneRouter-BYNiwchg.mjs → cloneRouter-BNCQ7tIa.mjs} +2 -2
  36. package/dist/esm/{cloneRouter-BYNiwchg.mjs.map → cloneRouter-BNCQ7tIa.mjs.map} +1 -1
  37. package/dist/esm/{index-C-i6vx5Y.d.mts → index-BWUmnecT.d.mts} +1 -2
  38. package/dist/esm/index-BWUmnecT.d.mts.map +1 -0
  39. package/dist/esm/{RouterError-WhCzIWuc.d.mts → index-CYpAZCoc.d.mts} +19 -2
  40. package/dist/esm/index-CYpAZCoc.d.mts.map +1 -0
  41. package/dist/esm/{index-DKzxav48.d.mts → index-CjWKWPY6.d.mts} +2 -2
  42. package/dist/esm/index-CjWKWPY6.d.mts.map +1 -0
  43. package/dist/esm/index.d.mts +5 -5
  44. package/dist/esm/index.mjs +1 -1
  45. package/dist/esm/index.mjs.map +1 -1
  46. package/dist/esm/{internals-DT4mneSz.mjs → internals-C8mRvTxc.mjs} +2 -2
  47. package/dist/esm/internals-C8mRvTxc.mjs.map +1 -0
  48. package/dist/esm/utils.d.mts +1 -1
  49. package/dist/esm/utils.mjs +1 -1
  50. package/dist/esm/utils.mjs.map +1 -1
  51. package/dist/esm/validation.d.mts +17 -5
  52. package/dist/esm/validation.d.mts.map +1 -1
  53. package/dist/esm/validation.mjs +1 -1
  54. package/package.json +3 -4
  55. package/dist/cjs/Router-C7eE1kIK.js +0 -6
  56. package/dist/cjs/Router-C7eE1kIK.js.map +0 -1
  57. package/dist/cjs/Router-Dg-zk8AS.d.ts.map +0 -1
  58. package/dist/cjs/RouterError-WhCzIWuc.d.ts.map +0 -1
  59. package/dist/cjs/index-C-i6vx5Y.d.ts.map +0 -1
  60. package/dist/cjs/index-K1U_fqfJ.d.ts.map +0 -1
  61. package/dist/cjs/internals-CWMOL1B8.js.map +0 -1
  62. package/dist/esm/Router-Dg-zk8AS.d.mts.map +0 -1
  63. package/dist/esm/Router-DiZbYMLx.mjs +0 -6
  64. package/dist/esm/Router-DiZbYMLx.mjs.map +0 -1
  65. package/dist/esm/RouterError-WhCzIWuc.d.mts.map +0 -1
  66. package/dist/esm/index-C-i6vx5Y.d.mts.map +0 -1
  67. package/dist/esm/index-DKzxav48.d.mts.map +0 -1
  68. package/dist/esm/internals-DT4mneSz.mjs.map +0 -1
  69. package/src/Router.ts +0 -725
  70. package/src/RouterError.ts +0 -324
  71. package/src/api/cloneRouter.ts +0 -159
  72. package/src/api/getDependenciesApi.ts +0 -160
  73. package/src/api/getLifecycleApi.ts +0 -65
  74. package/src/api/getPluginApi.ts +0 -228
  75. package/src/api/getRoutesApi.ts +0 -546
  76. package/src/api/helpers.ts +0 -10
  77. package/src/api/index.ts +0 -16
  78. package/src/api/types.ts +0 -12
  79. package/src/constants.ts +0 -101
  80. package/src/createRouter.ts +0 -32
  81. package/src/fsm/index.ts +0 -5
  82. package/src/fsm/routerFSM.ts +0 -130
  83. package/src/getNavigator.ts +0 -30
  84. package/src/guards.ts +0 -46
  85. package/src/helpers.ts +0 -197
  86. package/src/index.ts +0 -50
  87. package/src/internals.ts +0 -200
  88. package/src/namespaces/DependenciesNamespace/dependenciesStore.ts +0 -30
  89. package/src/namespaces/DependenciesNamespace/index.ts +0 -5
  90. package/src/namespaces/EventBusNamespace/EventBusNamespace.ts +0 -485
  91. package/src/namespaces/EventBusNamespace/index.ts +0 -5
  92. package/src/namespaces/EventBusNamespace/types.ts +0 -11
  93. package/src/namespaces/NavigationNamespace/NavigationNamespace.ts +0 -552
  94. package/src/namespaces/NavigationNamespace/constants.ts +0 -55
  95. package/src/namespaces/NavigationNamespace/index.ts +0 -5
  96. package/src/namespaces/NavigationNamespace/transition/completeTransition.ts +0 -108
  97. package/src/namespaces/NavigationNamespace/transition/errorHandling.ts +0 -124
  98. package/src/namespaces/NavigationNamespace/transition/guardPhase.ts +0 -283
  99. package/src/namespaces/NavigationNamespace/types.ts +0 -110
  100. package/src/namespaces/OptionsNamespace/OptionsNamespace.ts +0 -28
  101. package/src/namespaces/OptionsNamespace/constants.ts +0 -19
  102. package/src/namespaces/OptionsNamespace/helpers.ts +0 -50
  103. package/src/namespaces/OptionsNamespace/index.ts +0 -7
  104. package/src/namespaces/OptionsNamespace/validators.ts +0 -13
  105. package/src/namespaces/PluginsNamespace/PluginsNamespace.ts +0 -291
  106. package/src/namespaces/PluginsNamespace/constants.ts +0 -34
  107. package/src/namespaces/PluginsNamespace/index.ts +0 -7
  108. package/src/namespaces/PluginsNamespace/types.ts +0 -22
  109. package/src/namespaces/PluginsNamespace/validators.ts +0 -28
  110. package/src/namespaces/RouteLifecycleNamespace/RouteLifecycleNamespace.ts +0 -558
  111. package/src/namespaces/RouteLifecycleNamespace/index.ts +0 -5
  112. package/src/namespaces/RouteLifecycleNamespace/types.ts +0 -10
  113. package/src/namespaces/RouterLifecycleNamespace/RouterLifecycleNamespace.ts +0 -81
  114. package/src/namespaces/RouterLifecycleNamespace/constants.ts +0 -25
  115. package/src/namespaces/RouterLifecycleNamespace/index.ts +0 -5
  116. package/src/namespaces/RouterLifecycleNamespace/types.ts +0 -30
  117. package/src/namespaces/RoutesNamespace/RoutesNamespace.ts +0 -582
  118. package/src/namespaces/RoutesNamespace/constants.ts +0 -6
  119. package/src/namespaces/RoutesNamespace/forwardChain.ts +0 -34
  120. package/src/namespaces/RoutesNamespace/helpers.ts +0 -204
  121. package/src/namespaces/RoutesNamespace/index.ts +0 -11
  122. package/src/namespaces/RoutesNamespace/routeGuards.ts +0 -62
  123. package/src/namespaces/RoutesNamespace/routesStore.ts +0 -566
  124. package/src/namespaces/RoutesNamespace/types.ts +0 -81
  125. package/src/namespaces/StateNamespace/StateNamespace.ts +0 -224
  126. package/src/namespaces/StateNamespace/helpers.ts +0 -24
  127. package/src/namespaces/StateNamespace/index.ts +0 -5
  128. package/src/namespaces/StateNamespace/types.ts +0 -15
  129. package/src/namespaces/index.ts +0 -35
  130. package/src/stateMetaStore.ts +0 -15
  131. package/src/transitionPath.ts +0 -436
  132. package/src/typeGuards.ts +0 -59
  133. package/src/types/RouterValidator.ts +0 -156
  134. package/src/types.ts +0 -69
  135. package/src/utils/createRequestScope.ts +0 -174
  136. package/src/utils/getStaticPaths.ts +0 -50
  137. package/src/utils/hydrateRouter.ts +0 -89
  138. package/src/utils/index.ts +0 -27
  139. package/src/utils/serializeRouterState.ts +0 -120
  140. package/src/utils/serializeState.ts +0 -63
  141. package/src/validation.ts +0 -12
  142. package/src/wiring/RouterWiringBuilder.ts +0 -275
  143. package/src/wiring/index.ts +0 -7
  144. package/src/wiring/types.ts +0 -47
  145. package/src/wiring/wireRouter.ts +0 -26
package/src/Router.ts DELETED
@@ -1,725 +0,0 @@
1
- // packages/core/src/Router.ts
2
-
3
- /**
4
- * Router class - facade with integrated namespaces.
5
- *
6
- * All functionality is now provided by namespace classes.
7
- */
8
-
9
- import { logger } from "@real-router/logger";
10
- import { EventEmitter } from "event-emitter";
11
-
12
- import { EMPTY_PARAMS, errorCodes } from "./constants";
13
- import { createRouterFSM } from "./fsm";
14
- import { guardDependencies, guardRouteStructure } from "./guards";
15
- import { createLimits, normalizeParams } from "./helpers";
16
- import {
17
- createInterceptable,
18
- createInterceptable2,
19
- getInternals,
20
- registerInternals,
21
- } from "./internals";
22
- import {
23
- EventBusNamespace,
24
- NavigationNamespace,
25
- OptionsNamespace,
26
- PluginsNamespace,
27
- RouteLifecycleNamespace,
28
- RouterLifecycleNamespace,
29
- RoutesNamespace,
30
- StateNamespace,
31
- createDependenciesStore,
32
- } from "./namespaces";
33
- import { CACHED_ALREADY_STARTED_ERROR } from "./namespaces/RouterLifecycleNamespace/constants";
34
- import { RouterError } from "./RouterError";
35
- import { getTransitionPath } from "./transitionPath";
36
- import { isLoggerConfig } from "./typeGuards";
37
- import { RouterWiringBuilder, wireRouter } from "./wiring";
38
-
39
- import type { RouterInternals } from "./internals";
40
- import type { DependenciesStore } from "./namespaces";
41
- import type { Limits, PluginFactory, Route, RouterEventMap } from "./types";
42
- import type {
43
- DefaultDependencies,
44
- LeaveFn,
45
- NavigationOptions,
46
- Options,
47
- Params,
48
- Router as RouterInterface,
49
- State,
50
- SubscribeFn,
51
- Unsubscribe,
52
- } from "@real-router/types";
53
- import type { CreateMatcherOptions } from "route-tree";
54
-
55
- const EMPTY_OPTS: Readonly<NavigationOptions> = Object.freeze({});
56
-
57
- /**
58
- * Router class with integrated namespace architecture.
59
- *
60
- * All functionality is provided by namespace classes:
61
- * - OptionsNamespace: getOptions (immutable)
62
- * - DependenciesStore: get/set/remove dependencies
63
- * - EventEmitter: subscribe
64
- * - StateNamespace: state storage (getState, setState, getPreviousState)
65
- * - RoutesNamespace: route tree operations
66
- * - RouteLifecycleNamespace: canActivate/canDeactivate guards
67
- * - PluginsNamespace: plugin lifecycle
68
- * - NavigationNamespace: navigate
69
- * - RouterLifecycleNamespace: start, stop, isStarted
70
- *
71
- * @internal This class implementation is internal. Use createRouter() instead.
72
- */
73
- export class Router<
74
- Dependencies extends DefaultDependencies = DefaultDependencies,
75
- > implements RouterInterface<Dependencies> {
76
- [key: string]: unknown;
77
-
78
- // ============================================================================
79
- // Namespaces
80
- // ============================================================================
81
-
82
- readonly #options: OptionsNamespace;
83
- readonly #limits: Limits;
84
- readonly #dependenciesStore: DependenciesStore<Dependencies>;
85
- readonly #state: StateNamespace;
86
- readonly #routes: RoutesNamespace<Dependencies>;
87
- readonly #routeLifecycle: RouteLifecycleNamespace<Dependencies>;
88
- readonly #plugins: PluginsNamespace<Dependencies>;
89
- readonly #navigation: NavigationNamespace;
90
- readonly #lifecycle: RouterLifecycleNamespace;
91
-
92
- readonly #eventBus: EventBusNamespace;
93
-
94
- // ============================================================================
95
- // Constructor
96
- // ============================================================================
97
-
98
- /**
99
- * @param routes - Route definitions
100
- * @param options - Router options
101
- * @param dependencies - DI dependencies
102
- */
103
- constructor(
104
- routes: Route<Dependencies>[] = [],
105
- options: Partial<Options> = {},
106
- dependencies: Dependencies = {} as Dependencies,
107
- ) {
108
- // Configure logger if provided
109
- if (options.logger && isLoggerConfig(options.logger)) {
110
- logger.configure(options.logger);
111
- delete options.logger;
112
- }
113
-
114
- // =========================================================================
115
- // Validate inputs before creating namespaces
116
- // =========================================================================
117
-
118
- // Always validate options
119
- OptionsNamespace.validateOptionsIsObject(options);
120
-
121
- // Unconditional guard-level validation before creating namespaces
122
- guardDependencies(dependencies);
123
-
124
- if (routes.length > 0) {
125
- guardRouteStructure(routes);
126
- }
127
-
128
- // =========================================================================
129
- // Create Namespaces
130
- // =========================================================================
131
-
132
- this.#options = new OptionsNamespace(options);
133
- this.#limits = createLimits(options.limits);
134
- this.#dependenciesStore =
135
- createDependenciesStore<Dependencies>(dependencies);
136
- this.#state = new StateNamespace();
137
- this.#routes = new RoutesNamespace<Dependencies>(
138
- routes,
139
- deriveMatcherOptions(this.#options.get()),
140
- );
141
- this.#routeLifecycle = new RouteLifecycleNamespace<Dependencies>();
142
- this.#plugins = new PluginsNamespace<Dependencies>();
143
- this.#navigation = new NavigationNamespace();
144
- this.#lifecycle = new RouterLifecycleNamespace();
145
-
146
- // =========================================================================
147
- // Initialize EventBus
148
- // =========================================================================
149
-
150
- const routerFSM = createRouterFSM();
151
- // eslint-disable-next-line unicorn/prefer-event-target
152
- const emitter = new EventEmitter<RouterEventMap>({
153
- onListenerError: (eventName, error) => {
154
- logger.error("Router", `Error in listener for ${eventName}:`, error);
155
- },
156
- onListenerWarn: (eventName, count) => {
157
- logger.warn(
158
- "router.addEventListener",
159
- `Event "${eventName}" has ${count} listeners — possible memory leak`,
160
- );
161
- },
162
- });
163
-
164
- this.#eventBus = new EventBusNamespace({ routerFSM, emitter });
165
-
166
- // =========================================================================
167
- // Wire Dependencies
168
- // =========================================================================
169
-
170
- wireRouter(
171
- new RouterWiringBuilder<Dependencies>({
172
- router: this,
173
- options: this.#options,
174
- limits: this.#limits,
175
- dependenciesStore: this.#dependenciesStore,
176
- state: this.#state,
177
- routes: this.#routes,
178
- routeLifecycle: this.#routeLifecycle,
179
- plugins: this.#plugins,
180
- navigation: this.#navigation,
181
- lifecycle: this.#lifecycle,
182
- eventBus: this.#eventBus,
183
- }),
184
- );
185
-
186
- // =========================================================================
187
- // Register Internals (WeakMap for plugin/infrastructure access)
188
- // =========================================================================
189
-
190
- const interceptorsMap: RouterInternals["interceptors"] = new Map();
191
-
192
- registerInternals(this, {
193
- makeState: (name, params, path, meta) =>
194
- this.#state.makeState(name, params, path, meta),
195
- // `as unknown as` is required: createInterceptable2 returns a
196
- // non-generic `(a: A, b: B) => R`, but RouterInternals["forwardState"]
197
- // is declared with a generic parameter `<P extends Params = Params>`,
198
- // which tsc will not infer from the non-generic source. Sonar S4325
199
- // misclassifies this as a redundant cast.
200
- forwardState: createInterceptable2(
201
- "forwardState",
202
- (name: string, params: Params) =>
203
- this.#routes.forwardState(name, params),
204
- interceptorsMap,
205
- ) as unknown as RouterInternals["forwardState"],
206
- buildStateResolved: (name, params) =>
207
- this.#routes.buildStateResolved(name, params),
208
- matchPath: (path, matchOptions) =>
209
- this.#routes.matchPath(path, matchOptions),
210
- getOptions: () => this.#options.get(),
211
- addEventListener: (eventName, cb) =>
212
- this.#eventBus.addEventListener(eventName, cb),
213
- buildPath: createInterceptable2(
214
- "buildPath",
215
- (route: string, params?: Params) =>
216
- this.#routes.buildPath(
217
- route,
218
- params ?? EMPTY_PARAMS,
219
- this.#options.get(),
220
- ),
221
- interceptorsMap,
222
- ),
223
- emitTransitionError: (error) => {
224
- this.#eventBus.sendFailSafe(undefined, this.#state.get(), error);
225
- },
226
- start: createInterceptable(
227
- "start",
228
- (path: string) => {
229
- return this.#lifecycle.start(path);
230
- },
231
- interceptorsMap,
232
- ),
233
- navigateToState: (state, navOpts) => {
234
- // Plugin-only navigation primitive (#525). Mirrors the same
235
- // unhandled-rejection suppression and lastSync* bookkeeping used by
236
- // the public Router.navigate facade so plugin call-sites can
237
- // fire-and-forget the returned promise (popstate handlers do).
238
- const promiseState = this.#navigation.navigateToState(
239
- state,
240
- navOpts ?? EMPTY_OPTS,
241
- );
242
-
243
- if (this.#navigation.lastSyncResolved) {
244
- this.#navigation.lastSyncResolved = false;
245
- } else if (this.#navigation.lastSyncRejected) {
246
- this.#navigation.lastSyncRejected = false;
247
- } else {
248
- Router.#suppressUnhandledRejection(promiseState);
249
- }
250
-
251
- return promiseState;
252
- },
253
- interceptors: interceptorsMap,
254
- setRootPath: (rootPath) => {
255
- this.#routes.setRootPath(rootPath);
256
- },
257
- getRootPath: () => this.#routes.getStore().rootPath,
258
- getTree: () => this.#routes.getStore().tree,
259
- isDisposed: () => this.#eventBus.isDisposed(),
260
- validator: null,
261
- // Dependencies (issue #172)
262
- dependenciesGetStore: () => this.#dependenciesStore,
263
- // Clone support (issue #173)
264
- cloneOptions: () => ({ ...this.#options.get() }),
265
- cloneDependencies: () => ({ ...this.#dependenciesStore.dependencies }),
266
- getLifecycleFactories: () => this.#routeLifecycle.getFactories(),
267
- getPluginFactories: () => this.#plugins.getAll(),
268
- routeGetStore: () => this.#routes.getStore(),
269
- // Cross-namespace state (issue #174)
270
- getStateName: () => this.#state.get()?.name,
271
- isTransitioning: () => this.#eventBus.isTransitioning(),
272
- clearState: () => {
273
- this.#state.set(undefined);
274
- },
275
- setState: (state) => {
276
- this.#state.set(state);
277
- },
278
- routerExtensions: [],
279
- contextClaimRecords: new Set(),
280
- hydrationState: null,
281
- });
282
-
283
- // =========================================================================
284
- // Bind Public Methods
285
- // =========================================================================
286
- // All public methods that access private fields must be bound to preserve
287
- // `this` context when methods are extracted as references.
288
- // See: https://github.com/tc39/proposal-bind-operator
289
- // =========================================================================
290
-
291
- // Path & State Building
292
- this.isActiveRoute = this.isActiveRoute.bind(this);
293
- this.buildPath = this.buildPath.bind(this);
294
-
295
- // State Management
296
- this.getState = this.getState.bind(this);
297
- this.getPreviousState = this.getPreviousState.bind(this);
298
- this.areStatesEqual = this.areStatesEqual.bind(this);
299
- this.shouldUpdateNode = this.shouldUpdateNode.bind(this);
300
-
301
- // Router Lifecycle
302
- this.isActive = this.isActive.bind(this);
303
- this.start = this.start.bind(this);
304
- this.stop = this.stop.bind(this);
305
- this.dispose = this.dispose.bind(this);
306
-
307
- // Route Lifecycle (Guards)
308
- this.canNavigateTo = this.canNavigateTo.bind(this);
309
-
310
- // Plugins
311
- this.usePlugin = this.usePlugin.bind(this);
312
-
313
- // Navigation
314
- this.navigate = this.navigate.bind(this);
315
- this.navigateToDefault = this.navigateToDefault.bind(this);
316
- this.navigateToNotFound = this.navigateToNotFound.bind(this);
317
-
318
- // Subscription
319
- this.subscribe = this.subscribe.bind(this);
320
- this.subscribeLeave = this.subscribeLeave.bind(this);
321
- this.isLeaveApproved = this.isLeaveApproved.bind(this);
322
- }
323
-
324
- // ============================================================================
325
- // Path & State Building
326
- // ============================================================================
327
-
328
- isActiveRoute(
329
- name: string,
330
- params?: Params,
331
- strictEquality?: boolean,
332
- ignoreQueryParams?: boolean,
333
- ): boolean {
334
- getInternals(this).validator?.routes.validateIsActiveRouteArgs(
335
- name,
336
- params,
337
- strictEquality,
338
- ignoreQueryParams,
339
- );
340
-
341
- getInternals(this).validator?.routes.validateRouteName(
342
- name,
343
- "isActiveRoute",
344
- );
345
-
346
- // Empty string is special case - warn and return false (root node is not a parent)
347
- if (name === "") {
348
- logger.warn(
349
- "real-router",
350
- 'isActiveRoute("") called with empty string. Root node is not considered a parent of any route.',
351
- );
352
-
353
- return false;
354
- }
355
-
356
- return this.#routes.isActiveRoute(
357
- name,
358
- params,
359
- strictEquality,
360
- ignoreQueryParams,
361
- );
362
- }
363
-
364
- buildPath(route: string, params?: Params): string {
365
- const ctx = getInternals(this);
366
-
367
- ctx.validator?.routes.validateBuildPathArgs(route);
368
- ctx.validator?.navigation.validateParams(params, "buildPath");
369
-
370
- return ctx.buildPath(route, normalizeParams(params));
371
- }
372
-
373
- // ============================================================================
374
- // State Management (delegated to StateNamespace)
375
- // ============================================================================
376
-
377
- getState<P extends Params = Params>(): State<P> | undefined {
378
- return this.#state.get<P>();
379
- }
380
-
381
- getPreviousState(): State | undefined {
382
- return this.#state.getPrevious();
383
- }
384
-
385
- areStatesEqual(
386
- state1: State | undefined,
387
- state2: State | undefined,
388
- ignoreQueryParams = true,
389
- ): boolean {
390
- getInternals(this).validator?.state.validateAreStatesEqualArgs(
391
- state1,
392
- state2,
393
- ignoreQueryParams,
394
- );
395
-
396
- return this.#state.areStatesEqual(state1, state2, ignoreQueryParams);
397
- }
398
-
399
- shouldUpdateNode(
400
- nodeName: string,
401
- ): (toState: State, fromState?: State) => boolean {
402
- getInternals(this).validator?.routes.validateShouldUpdateNodeArgs(nodeName);
403
-
404
- return RoutesNamespace.shouldUpdateNode(nodeName);
405
- }
406
-
407
- // ============================================================================
408
- // Router Lifecycle
409
- // ============================================================================
410
-
411
- isActive(): boolean {
412
- return this.#eventBus.isActive();
413
- }
414
-
415
- start(startPath: string): Promise<State> {
416
- if (!this.#eventBus.canStart()) {
417
- return Promise.reject(CACHED_ALREADY_STARTED_ERROR);
418
- }
419
-
420
- getInternals(this).validator?.navigation.validateStartArgs(startPath);
421
-
422
- this.#eventBus.sendStart();
423
-
424
- // Convert sync interceptor throws to rejections so the recovery branch in
425
- // .catch is reachable; otherwise the throw escapes synchronously, FSM is
426
- // left in STARTING, and the router is permanently bricked (#668).
427
- let internalStart: Promise<State>;
428
-
429
- try {
430
- internalStart = getInternals(this).start(startPath);
431
- } catch (syncError: unknown) {
432
- // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors -- preserve original throw shape from user-provided start interceptor
433
- internalStart = Promise.reject(syncError);
434
- }
435
-
436
- const promiseState = internalStart.catch((error: unknown) => {
437
- if (this.#eventBus.isReady()) {
438
- this.#lifecycle.stop();
439
- this.#eventBus.sendStop();
440
- } else if (this.#eventBus.isStarting()) {
441
- this.#eventBus.sendFail(undefined, undefined, error);
442
- }
443
-
444
- throw error;
445
- });
446
-
447
- Router.#suppressUnhandledRejection(promiseState);
448
-
449
- return promiseState;
450
- }
451
-
452
- stop(): this {
453
- this.#navigation.abortCurrentNavigation();
454
- this.#eventBus.sendCancelIfPossible(this.#state.get());
455
-
456
- if (!this.#eventBus.isReady() && !this.#eventBus.isTransitioning()) {
457
- return this;
458
- }
459
-
460
- this.#lifecycle.stop();
461
- this.#eventBus.sendStop();
462
-
463
- return this;
464
- }
465
-
466
- dispose(): void {
467
- if (this.#eventBus.isDisposed()) {
468
- return;
469
- }
470
-
471
- this.#navigation.abortCurrentNavigation();
472
- this.#eventBus.sendCancelIfPossible(this.#state.get());
473
-
474
- if (this.#eventBus.isReady() || this.#eventBus.isTransitioning()) {
475
- this.#lifecycle.stop();
476
- this.#eventBus.sendStop();
477
- }
478
-
479
- this.#eventBus.sendDispose();
480
- this.#eventBus.clearAll();
481
-
482
- this.#plugins.disposeAll();
483
-
484
- // Safety net: clean up extensions plugins failed to remove in teardown
485
- const ctx = getInternals(this);
486
-
487
- for (const extension of ctx.routerExtensions) {
488
- for (const key of extension.keys) {
489
- delete (this as Record<string, unknown>)[key];
490
- }
491
- }
492
-
493
- ctx.routerExtensions.length = 0;
494
-
495
- // Safety net: release context namespace claims plugins failed to release in teardown
496
- ctx.contextClaimRecords.clear();
497
-
498
- this.#routes.clearRoutes();
499
- this.#routeLifecycle.clearAll();
500
- this.#state.reset();
501
- this.#dependenciesStore.dependencies = Object.create(
502
- null,
503
- ) as Partial<Dependencies>;
504
-
505
- this.#markDisposed();
506
- }
507
-
508
- // ============================================================================
509
- // Route Lifecycle (Guards)
510
- // ============================================================================
511
-
512
- canNavigateTo(name: string, params?: Params): boolean {
513
- const ctx = getInternals(this);
514
-
515
- ctx.validator?.routes.validateRouteName(name, "canNavigateTo");
516
- ctx.validator?.navigation.validateParams(params, "canNavigateTo");
517
-
518
- if (!this.#routes.hasRoute(name)) {
519
- return false;
520
- }
521
-
522
- const { name: resolvedName, params: resolvedParams } = ctx.forwardState(
523
- name,
524
- params ?? {},
525
- );
526
- const toState = this.#state.makeState(resolvedName, resolvedParams);
527
- const fromState = this.#state.get();
528
-
529
- const { toDeactivate, toActivate } = getTransitionPath(toState, fromState);
530
-
531
- return this.#routeLifecycle.canNavigateTo(
532
- toDeactivate,
533
- toActivate,
534
- toState,
535
- fromState,
536
- );
537
- }
538
-
539
- // ============================================================================
540
- // Plugins
541
- // ============================================================================
542
-
543
- usePlugin(
544
- ...plugins: (PluginFactory<Dependencies> | false | null | undefined)[]
545
- ): Unsubscribe {
546
- const filtered = plugins.filter(Boolean) as PluginFactory<Dependencies>[];
547
-
548
- if (filtered.length === 0) {
549
- return () => {};
550
- }
551
-
552
- const ctx = getInternals(this);
553
-
554
- ctx.validator?.plugins.validatePluginLimit(
555
- this.#plugins.count(),
556
- this.#limits,
557
- );
558
- for (const plugin of filtered) {
559
- ctx.validator?.plugins.validateNoDuplicatePlugins(
560
- plugin,
561
- this.#plugins.getAll(),
562
- );
563
- }
564
-
565
- return this.#plugins.use(...filtered);
566
- }
567
-
568
- // ============================================================================
569
- // Subscription (backed by EventEmitter)
570
- // ============================================================================
571
-
572
- subscribe(listener: SubscribeFn): Unsubscribe {
573
- EventBusNamespace.validateSubscribeListener(listener);
574
-
575
- return this.#eventBus.subscribe(listener);
576
- }
577
-
578
- subscribeLeave(listener: LeaveFn): Unsubscribe {
579
- EventBusNamespace.validateSubscribeLeaveListener(listener);
580
-
581
- return this.#eventBus.subscribeLeave(listener);
582
- }
583
-
584
- isLeaveApproved(): boolean {
585
- return this.#eventBus.isLeaveApproved();
586
- }
587
-
588
- // ============================================================================
589
- // Navigation
590
- // ============================================================================
591
-
592
- navigate(
593
- routeName: string,
594
- routeParams?: Params,
595
- options?: NavigationOptions,
596
- ): Promise<State> {
597
- const ctx = getInternals(this);
598
-
599
- ctx.validator?.navigation.validateNavigateArgs(routeName);
600
- ctx.validator?.navigation.validateParams(routeParams, "navigate");
601
-
602
- const opts = options ?? EMPTY_OPTS;
603
-
604
- ctx.validator?.navigation.validateNavigationOptions(opts, "navigate");
605
-
606
- const promiseState = this.#navigation.navigate(
607
- routeName,
608
- routeParams ?? EMPTY_PARAMS,
609
- opts,
610
- );
611
-
612
- if (this.#navigation.lastSyncResolved) {
613
- this.#navigation.lastSyncResolved = false;
614
- } else if (this.#navigation.lastSyncRejected) {
615
- // Cached rejection — already pre-suppressed at module load, skip .catch()
616
- this.#navigation.lastSyncRejected = false;
617
- } else {
618
- Router.#suppressUnhandledRejection(promiseState);
619
- }
620
-
621
- return promiseState;
622
- }
623
-
624
- navigateToDefault(options?: NavigationOptions): Promise<State> {
625
- const ctx = getInternals(this);
626
-
627
- ctx.validator?.navigation.validateNavigateToDefaultArgs(options);
628
-
629
- const opts = options ?? EMPTY_OPTS;
630
-
631
- ctx.validator?.navigation.validateNavigationOptions(
632
- opts,
633
- "navigateToDefault",
634
- );
635
-
636
- const promiseState = this.#navigation.navigateToDefault(opts);
637
-
638
- if (this.#navigation.lastSyncResolved) {
639
- this.#navigation.lastSyncResolved = false;
640
- } else if (this.#navigation.lastSyncRejected) {
641
- this.#navigation.lastSyncRejected = false;
642
- } else {
643
- Router.#suppressUnhandledRejection(promiseState);
644
- }
645
-
646
- return promiseState;
647
- }
648
-
649
- navigateToNotFound(path?: string): State {
650
- if (!this.#eventBus.isActive()) {
651
- throw new RouterError(errorCodes.ROUTER_NOT_STARTED);
652
- }
653
-
654
- if (path !== undefined && typeof path !== "string") {
655
- throw new TypeError(
656
- `[router.navigateToNotFound] path must be a string, got ${typeof path}`,
657
- );
658
- }
659
-
660
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- isActive() guarantees state exists
661
- const resolvedPath = path ?? this.#state.get()!.path;
662
-
663
- return this.#navigation.navigateToNotFound(resolvedPath);
664
- }
665
-
666
- /**
667
- * Pre-allocated callback for #suppressUnhandledRejection.
668
- * Avoids creating a new closure on every navigate() call.
669
- */
670
- static readonly #onSuppressedError = (error: unknown): void => {
671
- if (
672
- error instanceof RouterError &&
673
- (error.code === errorCodes.SAME_STATES ||
674
- error.code === errorCodes.TRANSITION_CANCELLED ||
675
- error.code === errorCodes.ROUTER_NOT_STARTED ||
676
- error.code === errorCodes.ROUTE_NOT_FOUND)
677
- ) {
678
- return;
679
- }
680
-
681
- logger.error("router.navigate", "Unexpected navigation error", error);
682
- };
683
-
684
- /**
685
- * Fire-and-forget safety: prevents unhandled rejection warnings
686
- * when navigate/navigateToDefault is called without await.
687
- * Expected errors are silently suppressed; unexpected ones are logged.
688
- */
689
- static #suppressUnhandledRejection(promise: Promise<State>): void {
690
- promise.catch(Router.#onSuppressedError);
691
- }
692
-
693
- #markDisposed(): void {
694
- this.navigate = throwDisposed;
695
- this.navigateToDefault = throwDisposed;
696
- this.navigateToNotFound = throwDisposed;
697
- this.start = throwDisposed;
698
- this.stop = throwDisposed;
699
- this.usePlugin = throwDisposed;
700
-
701
- this.subscribe = throwDisposed;
702
- this.subscribeLeave = throwDisposed;
703
- this.canNavigateTo = throwDisposed;
704
- }
705
- }
706
-
707
- function throwDisposed(): never {
708
- throw new RouterError(errorCodes.ROUTER_DISPOSED);
709
- }
710
-
711
- /**
712
- * Derives CreateMatcherOptions from router Options.
713
- * Maps core option names to matcher option names.
714
- */
715
- function deriveMatcherOptions(
716
- options: Readonly<Options>,
717
- ): CreateMatcherOptions {
718
- return {
719
- strictTrailingSlash: options.trailingSlash === "strict",
720
- strictQueryParams: options.queryParamsMode === "strict",
721
- urlParamsEncoding: options.urlParamsEncoding,
722
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
723
- queryParams: options.queryParams!,
724
- };
725
- }