@real-router/core 0.45.1 → 0.45.3

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 (87) hide show
  1. package/dist/cjs/Router-Dh1xgFLI.d.ts.map +1 -0
  2. package/dist/cjs/RouterValidator-TUi8eT8Q.d.ts.map +1 -0
  3. package/dist/cjs/api.d.ts.map +1 -0
  4. package/dist/cjs/index.d.ts.map +1 -0
  5. package/dist/cjs/utils.d.ts.map +1 -0
  6. package/dist/cjs/validation.d.ts.map +1 -0
  7. package/dist/esm/Router-BPkXwb1J.d.mts.map +1 -0
  8. package/dist/esm/RouterValidator-DphcVMEp.d.mts.map +1 -0
  9. package/dist/esm/api.d.mts.map +1 -0
  10. package/dist/esm/index.d.mts.map +1 -0
  11. package/dist/esm/utils.d.mts.map +1 -0
  12. package/dist/esm/validation.d.mts.map +1 -0
  13. package/package.json +9 -12
  14. package/src/Router.ts +684 -0
  15. package/src/RouterError.ts +324 -0
  16. package/src/api/cloneRouter.ts +77 -0
  17. package/src/api/getDependenciesApi.ts +168 -0
  18. package/src/api/getLifecycleApi.ts +65 -0
  19. package/src/api/getPluginApi.ts +167 -0
  20. package/src/api/getRoutesApi.ts +573 -0
  21. package/src/api/helpers.ts +10 -0
  22. package/src/api/index.ts +16 -0
  23. package/src/api/types.ts +12 -0
  24. package/src/constants.ts +87 -0
  25. package/src/createRouter.ts +32 -0
  26. package/src/fsm/index.ts +5 -0
  27. package/src/fsm/routerFSM.ts +120 -0
  28. package/src/getNavigator.ts +30 -0
  29. package/src/guards.ts +46 -0
  30. package/src/helpers.ts +179 -0
  31. package/src/index.ts +50 -0
  32. package/src/internals.ts +173 -0
  33. package/src/namespaces/DependenciesNamespace/dependenciesStore.ts +30 -0
  34. package/src/namespaces/DependenciesNamespace/index.ts +5 -0
  35. package/src/namespaces/EventBusNamespace/EventBusNamespace.ts +311 -0
  36. package/src/namespaces/EventBusNamespace/index.ts +5 -0
  37. package/src/namespaces/EventBusNamespace/types.ts +11 -0
  38. package/src/namespaces/NavigationNamespace/NavigationNamespace.ts +405 -0
  39. package/src/namespaces/NavigationNamespace/constants.ts +55 -0
  40. package/src/namespaces/NavigationNamespace/index.ts +5 -0
  41. package/src/namespaces/NavigationNamespace/transition/completeTransition.ts +100 -0
  42. package/src/namespaces/NavigationNamespace/transition/errorHandling.ts +124 -0
  43. package/src/namespaces/NavigationNamespace/transition/guardPhase.ts +221 -0
  44. package/src/namespaces/NavigationNamespace/types.ts +100 -0
  45. package/src/namespaces/OptionsNamespace/OptionsNamespace.ts +28 -0
  46. package/src/namespaces/OptionsNamespace/constants.ts +19 -0
  47. package/src/namespaces/OptionsNamespace/helpers.ts +50 -0
  48. package/src/namespaces/OptionsNamespace/index.ts +7 -0
  49. package/src/namespaces/OptionsNamespace/validators.ts +13 -0
  50. package/src/namespaces/PluginsNamespace/PluginsNamespace.ts +291 -0
  51. package/src/namespaces/PluginsNamespace/constants.ts +34 -0
  52. package/src/namespaces/PluginsNamespace/index.ts +7 -0
  53. package/src/namespaces/PluginsNamespace/types.ts +22 -0
  54. package/src/namespaces/PluginsNamespace/validators.ts +28 -0
  55. package/src/namespaces/RouteLifecycleNamespace/RouteLifecycleNamespace.ts +377 -0
  56. package/src/namespaces/RouteLifecycleNamespace/index.ts +5 -0
  57. package/src/namespaces/RouteLifecycleNamespace/types.ts +10 -0
  58. package/src/namespaces/RouterLifecycleNamespace/RouterLifecycleNamespace.ts +81 -0
  59. package/src/namespaces/RouterLifecycleNamespace/constants.ts +25 -0
  60. package/src/namespaces/RouterLifecycleNamespace/index.ts +5 -0
  61. package/src/namespaces/RouterLifecycleNamespace/types.ts +26 -0
  62. package/src/namespaces/RoutesNamespace/RoutesNamespace.ts +535 -0
  63. package/src/namespaces/RoutesNamespace/constants.ts +6 -0
  64. package/src/namespaces/RoutesNamespace/forwardChain.ts +34 -0
  65. package/src/namespaces/RoutesNamespace/helpers.ts +126 -0
  66. package/src/namespaces/RoutesNamespace/index.ts +11 -0
  67. package/src/namespaces/RoutesNamespace/routeGuards.ts +62 -0
  68. package/src/namespaces/RoutesNamespace/routesStore.ts +346 -0
  69. package/src/namespaces/RoutesNamespace/types.ts +81 -0
  70. package/src/namespaces/StateNamespace/StateNamespace.ts +211 -0
  71. package/src/namespaces/StateNamespace/helpers.ts +24 -0
  72. package/src/namespaces/StateNamespace/index.ts +5 -0
  73. package/src/namespaces/StateNamespace/types.ts +15 -0
  74. package/src/namespaces/index.ts +35 -0
  75. package/src/stateMetaStore.ts +15 -0
  76. package/src/transitionPath.ts +436 -0
  77. package/src/typeGuards.ts +59 -0
  78. package/src/types/RouterValidator.ts +154 -0
  79. package/src/types.ts +69 -0
  80. package/src/utils/getStaticPaths.ts +50 -0
  81. package/src/utils/index.ts +5 -0
  82. package/src/utils/serializeState.ts +22 -0
  83. package/src/validation.ts +12 -0
  84. package/src/wiring/RouterWiringBuilder.ts +261 -0
  85. package/src/wiring/index.ts +7 -0
  86. package/src/wiring/types.ts +47 -0
  87. package/src/wiring/wireRouter.ts +26 -0
@@ -0,0 +1,573 @@
1
+ import { logger } from "@real-router/logger";
2
+
3
+ import { throwIfDisposed } from "./helpers";
4
+ import { guardRouteStructure } from "../guards";
5
+ import { createInterceptable, getInternals } from "../internals";
6
+ import {
7
+ clearConfigEntries,
8
+ removeFromDefinitions,
9
+ sanitizeRoute,
10
+ } from "../namespaces/RoutesNamespace/helpers";
11
+ import {
12
+ validateClearRoutes,
13
+ validateRemoveRoute,
14
+ } from "../namespaces/RoutesNamespace/routeGuards";
15
+ import {
16
+ clearRouteData,
17
+ refreshForwardMap,
18
+ registerAllRouteHandlers,
19
+ } from "../namespaces/RoutesNamespace/routesStore";
20
+
21
+ import type { RoutesApi } from "./types";
22
+ import type { RouterInternals } from "../internals";
23
+ import type { RouteLifecycleNamespace, RouteConfig } from "../namespaces";
24
+ import type { RoutesStore } from "../namespaces/RoutesNamespace";
25
+ import type { GuardFnFactory, Route } from "../types";
26
+ import type {
27
+ DefaultDependencies,
28
+ ForwardToCallback,
29
+ Params,
30
+ Router,
31
+ } from "@real-router/types";
32
+ import type { RouteDefinition, RouteTree } from "route-tree";
33
+
34
+ // ============================================================================
35
+ // Helpers
36
+ // ============================================================================
37
+
38
+ /**
39
+ * Recursively finds a route definition by its full dotted name.
40
+ */
41
+ function findDefinition(
42
+ definitions: RouteDefinition[],
43
+ fullName: string,
44
+ parentPrefix = "",
45
+ ): RouteDefinition | undefined {
46
+ for (const def of definitions) {
47
+ const currentFullName = parentPrefix
48
+ ? `${parentPrefix}.${def.name}`
49
+ : def.name;
50
+
51
+ if (currentFullName === fullName) {
52
+ return def;
53
+ }
54
+
55
+ if (def.children && fullName.startsWith(`${currentFullName}.`)) {
56
+ return findDefinition(def.children, fullName, currentFullName);
57
+ }
58
+ }
59
+
60
+ /* v8 ignore next -- @preserve: defensive return, callers validate route exists before calling */
61
+ return undefined;
62
+ }
63
+
64
+ /**
65
+ * Clears all config entries and lifecycle handlers for a removed route
66
+ * (and all its descendants).
67
+ */
68
+ function clearRouteConfigurations<
69
+ Dependencies extends DefaultDependencies = DefaultDependencies,
70
+ >(
71
+ routeName: string,
72
+ config: RouteConfig,
73
+ routeCustomFields: Record<string, Record<string, unknown>>,
74
+ lifecycleNamespace: RouteLifecycleNamespace<Dependencies>,
75
+ ): void {
76
+ const shouldClear = (name: string): boolean =>
77
+ name === routeName || name.startsWith(`${routeName}.`);
78
+
79
+ clearConfigEntries(config.decoders, shouldClear);
80
+ clearConfigEntries(config.encoders, shouldClear);
81
+ clearConfigEntries(config.defaultParams, shouldClear);
82
+ clearConfigEntries(config.forwardMap, shouldClear);
83
+ clearConfigEntries(config.forwardFnMap, shouldClear);
84
+ clearConfigEntries(routeCustomFields, shouldClear);
85
+
86
+ // Clear forwardMap entries pointing TO the deleted route (or its descendants)
87
+ clearConfigEntries(config.forwardMap, (key) =>
88
+ shouldClear(config.forwardMap[key]),
89
+ );
90
+
91
+ // Clear lifecycle handlers
92
+ const [canDeactivateFactories, canActivateFactories] =
93
+ lifecycleNamespace.getFactories();
94
+
95
+ for (const name of Object.keys(canActivateFactories)) {
96
+ if (shouldClear(name)) {
97
+ lifecycleNamespace.clearCanActivate(name);
98
+ }
99
+ }
100
+
101
+ for (const name of Object.keys(canDeactivateFactories)) {
102
+ if (shouldClear(name)) {
103
+ lifecycleNamespace.clearCanDeactivate(name);
104
+ }
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Updates forwardTo for a route in config and returns the refreshed resolved
110
+ * forward map (REPLACE semantics — caller must call ctx.setResolvedForwardMap).
111
+ */
112
+ function updateForwardTo<
113
+ Dependencies extends DefaultDependencies = DefaultDependencies,
114
+ >(
115
+ name: string,
116
+ forwardTo: string | ForwardToCallback<Dependencies> | null,
117
+ config: RouteConfig,
118
+ refreshForwardMapFn: (config: RouteConfig) => Record<string, string>,
119
+ ): Record<string, string> {
120
+ if (forwardTo === null) {
121
+ delete config.forwardMap[name];
122
+ delete config.forwardFnMap[name];
123
+ } else if (typeof forwardTo === "string") {
124
+ delete config.forwardFnMap[name];
125
+ config.forwardMap[name] = forwardTo;
126
+ } else {
127
+ delete config.forwardMap[name];
128
+ config.forwardFnMap[name] = forwardTo;
129
+ }
130
+
131
+ return refreshForwardMapFn(config);
132
+ }
133
+
134
+ /**
135
+ * Builds a full Route object from a bare RouteDefinition by re-attaching
136
+ * config entries and lifecycle factories.
137
+ *
138
+ * RECURSIVE — call with the factories tuple obtained ONCE from
139
+ * `lifecycleNamespace.getFactories()` and pass it through to children.
140
+ */
141
+ function enrichRoute<
142
+ Dependencies extends DefaultDependencies = DefaultDependencies,
143
+ >(
144
+ routeDef: RouteDefinition,
145
+ routeName: string,
146
+ config: RouteConfig,
147
+ factories: [
148
+ Record<string, GuardFnFactory<Dependencies>>,
149
+ Record<string, GuardFnFactory<Dependencies>>,
150
+ ],
151
+ ): Route<Dependencies> {
152
+ const route: Route<Dependencies> = {
153
+ name: routeDef.name,
154
+ path: routeDef.path,
155
+ };
156
+
157
+ const forwardToFn = config.forwardFnMap[routeName];
158
+ const forwardToStr = config.forwardMap[routeName];
159
+
160
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
161
+ if (forwardToFn !== undefined) {
162
+ route.forwardTo = forwardToFn;
163
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
164
+ } else if (forwardToStr !== undefined) {
165
+ route.forwardTo = forwardToStr;
166
+ }
167
+
168
+ if (routeName in config.defaultParams) {
169
+ route.defaultParams = config.defaultParams[routeName];
170
+ }
171
+
172
+ if (routeName in config.decoders) {
173
+ route.decodeParams = config.decoders[routeName];
174
+ }
175
+
176
+ if (routeName in config.encoders) {
177
+ route.encodeParams = config.encoders[routeName];
178
+ }
179
+
180
+ const [canDeactivateFactories, canActivateFactories] = factories;
181
+
182
+ if (routeName in canActivateFactories) {
183
+ route.canActivate = canActivateFactories[routeName];
184
+ }
185
+
186
+ if (routeName in canDeactivateFactories) {
187
+ route.canDeactivate = canDeactivateFactories[routeName];
188
+ }
189
+
190
+ if (routeDef.children) {
191
+ route.children = routeDef.children.map((child) =>
192
+ enrichRoute(child, `${routeName}.${child.name}`, config, factories),
193
+ );
194
+ }
195
+
196
+ return route;
197
+ }
198
+
199
+ // ============================================================================
200
+ // CRUD operations
201
+ // ============================================================================
202
+
203
+ /**
204
+ * Adds one or more routes to the router.
205
+ * Input already validated by facade.
206
+ */
207
+ function addRoutes<
208
+ Dependencies extends DefaultDependencies = DefaultDependencies,
209
+ >(
210
+ store: RoutesStore<Dependencies>,
211
+ routes: Route<Dependencies>[],
212
+ parentName?: string,
213
+ ): void {
214
+ if (parentName) {
215
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
216
+ const parentDef = findDefinition(store.definitions, parentName)!;
217
+
218
+ parentDef.children ??= [];
219
+
220
+ for (const route of routes) {
221
+ parentDef.children.push(sanitizeRoute(route));
222
+ }
223
+ } else {
224
+ for (const route of routes) {
225
+ store.definitions.push(sanitizeRoute(route));
226
+ }
227
+ }
228
+
229
+ registerAllRouteHandlers(
230
+ routes,
231
+ store.config,
232
+ store.routeCustomFields,
233
+ store.pendingCanActivate,
234
+ store.pendingCanDeactivate,
235
+ store.depsStore,
236
+ parentName ?? "",
237
+ );
238
+
239
+ store.treeOperations.commitTreeChanges(store);
240
+ }
241
+
242
+ /**
243
+ * Atomically replaces all routes with a new set.
244
+ * Follows RFC 6-step semantics for HMR support.
245
+ */
246
+ function replaceRoutes<
247
+ Dependencies extends DefaultDependencies = DefaultDependencies,
248
+ >(
249
+ store: RoutesStore<Dependencies>,
250
+ routes: Route<Dependencies>[],
251
+ ctx: RouterInternals<Dependencies>,
252
+ currentPath: string | undefined,
253
+ ): void {
254
+ // Step 2: Clear route data (WITHOUT tree rebuild)
255
+ clearRouteData(store);
256
+
257
+ // Step 3: Clear definition lifecycle handlers (preserve external guards)
258
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- guaranteed set after wiring
259
+ store.lifecycleNamespace!.clearDefinitionGuards();
260
+
261
+ // Step 4: Register new routes
262
+ for (const route of routes) {
263
+ store.definitions.push(sanitizeRoute(route));
264
+ }
265
+
266
+ registerAllRouteHandlers(
267
+ routes,
268
+ store.config,
269
+ store.routeCustomFields,
270
+ store.pendingCanActivate,
271
+ store.pendingCanDeactivate,
272
+ store.depsStore,
273
+ "",
274
+ );
275
+
276
+ // Step 5: One tree rebuild
277
+ store.treeOperations.commitTreeChanges(store);
278
+
279
+ // Step 6: Revalidate state
280
+ if (currentPath !== undefined) {
281
+ const revalidated = ctx.matchPath(currentPath, ctx.getOptions());
282
+
283
+ if (revalidated) {
284
+ ctx.setState(revalidated);
285
+ } else {
286
+ ctx.clearState();
287
+ }
288
+ }
289
+ }
290
+
291
+ /**
292
+ * Removes a route and all its children.
293
+ *
294
+ * @returns true if removed, false if not found
295
+ */
296
+ function removeRoute<
297
+ Dependencies extends DefaultDependencies = DefaultDependencies,
298
+ >(store: RoutesStore<Dependencies>, name: string): boolean {
299
+ const wasRemoved = removeFromDefinitions(store.definitions, name);
300
+
301
+ if (!wasRemoved) {
302
+ return false;
303
+ }
304
+
305
+ clearRouteConfigurations(
306
+ name,
307
+ store.config,
308
+ store.routeCustomFields,
309
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
310
+ store.lifecycleNamespace!,
311
+ );
312
+
313
+ store.treeOperations.commitTreeChanges(store);
314
+
315
+ return true;
316
+ }
317
+
318
+ /**
319
+ * Updates a route's configuration in place.
320
+ */
321
+ function updateRouteConfig<
322
+ Dependencies extends DefaultDependencies = DefaultDependencies,
323
+ >(
324
+ store: RoutesStore<Dependencies>,
325
+ name: string,
326
+ updates: {
327
+ forwardTo?: string | ForwardToCallback<Dependencies> | null | undefined;
328
+ defaultParams?: Params | null | undefined;
329
+ decodeParams?: ((params: Params) => Params) | null | undefined;
330
+ encodeParams?: ((params: Params) => Params) | null | undefined;
331
+ },
332
+ ): void {
333
+ if (updates.forwardTo !== undefined) {
334
+ store.resolvedForwardMap = updateForwardTo(
335
+ name,
336
+ updates.forwardTo,
337
+ store.config,
338
+ (config) => refreshForwardMap(config),
339
+ );
340
+ }
341
+
342
+ if (updates.defaultParams !== undefined) {
343
+ if (updates.defaultParams === null) {
344
+ delete store.config.defaultParams[name];
345
+ } else {
346
+ store.config.defaultParams[name] = updates.defaultParams;
347
+ }
348
+ }
349
+
350
+ if (updates.decodeParams !== undefined) {
351
+ if (updates.decodeParams === null) {
352
+ delete store.config.decoders[name];
353
+ } else {
354
+ const decoder = updates.decodeParams;
355
+
356
+ store.config.decoders[name] = (params: Params): Params =>
357
+ (decoder(params) as Params | undefined) ?? params;
358
+ }
359
+ }
360
+
361
+ if (updates.encodeParams !== undefined) {
362
+ if (updates.encodeParams === null) {
363
+ delete store.config.encoders[name];
364
+ } else {
365
+ const encoder = updates.encodeParams;
366
+
367
+ store.config.encoders[name] = (params: Params): Params =>
368
+ (encoder(params) as Params | undefined) ?? params;
369
+ }
370
+ }
371
+ }
372
+
373
+ /**
374
+ * Gets a route by name with all its configuration.
375
+ */
376
+ function getRoute<
377
+ Dependencies extends DefaultDependencies = DefaultDependencies,
378
+ >(
379
+ store: RoutesStore<Dependencies>,
380
+ name: string,
381
+ ): Route<Dependencies> | undefined {
382
+ const segments = store.matcher.getSegmentsByName(name);
383
+
384
+ if (!segments) {
385
+ return undefined;
386
+ }
387
+
388
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- segments is non-empty (checked above)
389
+ const targetNode = segments.at(-1)! as RouteTree;
390
+ const definition = store.treeOperations.nodeToDefinition(targetNode);
391
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
392
+ const factories = store.lifecycleNamespace!.getFactories();
393
+
394
+ return enrichRoute(definition, name, store.config, factories);
395
+ }
396
+
397
+ // ============================================================================
398
+ // API factory
399
+ // ============================================================================
400
+
401
+ export function getRoutesApi<
402
+ Dependencies extends DefaultDependencies = DefaultDependencies,
403
+ >(router: Router<Dependencies>): RoutesApi<Dependencies> {
404
+ const ctx = getInternals(router);
405
+
406
+ const store = ctx.routeGetStore();
407
+
408
+ const interceptableAdd = createInterceptable(
409
+ "add",
410
+ (routeArray: Route<Dependencies>[], options?: { parent?: string }) => {
411
+ addRoutes(store, routeArray, options?.parent);
412
+ },
413
+ ctx.interceptors,
414
+ );
415
+
416
+ return {
417
+ add: (routes, options) => {
418
+ throwIfDisposed(ctx.isDisposed);
419
+
420
+ const routeArray = Array.isArray(routes) ? routes : [routes];
421
+ const parentName = options?.parent;
422
+
423
+ guardRouteStructure(routeArray, ctx.validator);
424
+
425
+ if (parentName !== undefined) {
426
+ ctx.validator?.routes.validateParentOption(parentName, store.tree);
427
+ }
428
+
429
+ ctx.validator?.routes.throwIfInternalRouteInArray(routeArray, "addRoute");
430
+ ctx.validator?.routes.validateAddRouteArgs(routeArray);
431
+ ctx.validator?.routes.validateRoutes(routeArray, store);
432
+
433
+ interceptableAdd(
434
+ routeArray,
435
+ parentName === undefined ? undefined : { parent: parentName },
436
+ );
437
+ },
438
+
439
+ remove: (name) => {
440
+ throwIfDisposed(ctx.isDisposed);
441
+
442
+ ctx.validator?.routes.validateRemoveRouteArgs(name);
443
+ ctx.validator?.routes.throwIfInternalRoute(name, "removeRoute");
444
+
445
+ const canRemove = validateRemoveRoute(
446
+ name,
447
+ ctx.getStateName(),
448
+ ctx.isTransitioning(),
449
+ );
450
+
451
+ if (!canRemove) {
452
+ return;
453
+ }
454
+
455
+ const wasRemoved = removeRoute(store, name);
456
+
457
+ if (!wasRemoved) {
458
+ logger.warn(
459
+ "router.removeRoute",
460
+ `Route "${name}" not found. No changes made.`,
461
+ );
462
+ }
463
+ },
464
+
465
+ update: (name, updates) => {
466
+ throwIfDisposed(ctx.isDisposed);
467
+
468
+ ctx.validator?.routes.validateUpdateRouteBasicArgs(name, updates);
469
+ ctx.validator?.routes.throwIfInternalRoute(name, "updateRoute");
470
+
471
+ const {
472
+ forwardTo,
473
+ defaultParams,
474
+ decodeParams,
475
+ encodeParams,
476
+ canActivate,
477
+ canDeactivate,
478
+ } = updates;
479
+
480
+ ctx.validator?.routes.validateUpdateRoutePropertyTypes(name, updates);
481
+
482
+ /* v8 ignore next 6 -- @preserve: race condition guard, mirrors Router.updateRoute() same-path guard tested via Router.ts unit tests */
483
+ if (ctx.isTransitioning()) {
484
+ logger.error(
485
+ "router.updateRoute",
486
+ `Updating route "${name}" while navigation is in progress. This may cause unexpected behavior.`,
487
+ );
488
+ }
489
+
490
+ ctx.validator?.routes.validateUpdateRoute(name, updates, store);
491
+
492
+ updateRouteConfig(store, name, {
493
+ forwardTo,
494
+ defaultParams,
495
+ decodeParams,
496
+ encodeParams,
497
+ });
498
+
499
+ if (canActivate !== undefined) {
500
+ if (canActivate === null) {
501
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- guaranteed set after wiring
502
+ store.lifecycleNamespace!.clearCanActivate(name);
503
+ } else {
504
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- guaranteed set after wiring
505
+ store.lifecycleNamespace!.addCanActivate(name, canActivate, true);
506
+ }
507
+ }
508
+
509
+ if (canDeactivate !== undefined) {
510
+ if (canDeactivate === null) {
511
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- guaranteed set after wiring
512
+ store.lifecycleNamespace!.clearCanDeactivate(name);
513
+ } else {
514
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- guaranteed set after wiring
515
+ store.lifecycleNamespace!.addCanDeactivate(name, canDeactivate, true);
516
+ }
517
+ }
518
+ },
519
+
520
+ clear: () => {
521
+ throwIfDisposed(ctx.isDisposed);
522
+
523
+ const canClear = validateClearRoutes(ctx.isTransitioning());
524
+
525
+ /* v8 ignore next 3 -- @preserve: race condition guard, mirrors Router.clearRoutes() same-path guard tested via validateClearRoutes unit tests */
526
+ if (!canClear) {
527
+ return;
528
+ }
529
+
530
+ store.treeOperations.resetStore(store);
531
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- guaranteed set after wiring
532
+ store.lifecycleNamespace!.clearAll();
533
+ ctx.clearState();
534
+ },
535
+
536
+ has: (name) => {
537
+ ctx.validator?.routes.validateRouteName(name, "hasRoute");
538
+
539
+ return store.matcher.hasRoute(name);
540
+ },
541
+
542
+ get: (name) => {
543
+ ctx.validator?.routes.validateRouteName(name, "getRoute");
544
+
545
+ return getRoute(store, name);
546
+ },
547
+
548
+ replace: (routes) => {
549
+ throwIfDisposed(ctx.isDisposed);
550
+
551
+ const routeArray = Array.isArray(routes) ? routes : [routes];
552
+
553
+ const canReplace = validateClearRoutes(ctx.isTransitioning());
554
+
555
+ if (!canReplace) {
556
+ return;
557
+ }
558
+
559
+ guardRouteStructure(routeArray, ctx.validator);
560
+
561
+ ctx.validator?.routes.throwIfInternalRouteInArray(
562
+ routeArray,
563
+ "replaceRoutes",
564
+ );
565
+ ctx.validator?.routes.validateAddRouteArgs(routeArray);
566
+ ctx.validator?.routes.validateRoutes(routeArray, store);
567
+
568
+ const currentPath = router.getState()?.path;
569
+
570
+ replaceRoutes(store, routeArray, ctx, currentPath);
571
+ },
572
+ };
573
+ }
@@ -0,0 +1,10 @@
1
+ // packages/core/src/api/helpers.ts
2
+
3
+ import { errorCodes } from "../constants";
4
+ import { RouterError } from "../RouterError";
5
+
6
+ export function throwIfDisposed(isDisposed: () => boolean): void {
7
+ if (isDisposed()) {
8
+ throw new RouterError(errorCodes.ROUTER_DISPOSED);
9
+ }
10
+ }
@@ -0,0 +1,16 @@
1
+ export { getPluginApi } from "./getPluginApi";
2
+
3
+ export { getRoutesApi } from "./getRoutesApi";
4
+
5
+ export { getDependenciesApi } from "./getDependenciesApi";
6
+
7
+ export { getLifecycleApi } from "./getLifecycleApi";
8
+
9
+ export { cloneRouter } from "./cloneRouter";
10
+
11
+ export type {
12
+ PluginApi,
13
+ RoutesApi,
14
+ DependenciesApi,
15
+ LifecycleApi,
16
+ } from "./types";
@@ -0,0 +1,12 @@
1
+ import type { PluginApi as BasePluginApi } from "@real-router/types";
2
+ import type { RouteTree } from "route-tree";
3
+
4
+ export interface PluginApi extends Omit<BasePluginApi, "getTree"> {
5
+ getTree: () => RouteTree;
6
+ }
7
+
8
+ export {
9
+ type RoutesApi,
10
+ type LifecycleApi,
11
+ type DependenciesApi,
12
+ } from "@real-router/types";
@@ -0,0 +1,87 @@
1
+ // packages/core/src/constants.ts
2
+
3
+ import type {
4
+ EventToNameMap,
5
+ EventToPluginMap,
6
+ ErrorCodeToValueMap,
7
+ ErrorCodeKeys,
8
+ ErrorCodeValues,
9
+ } from "@real-router/types";
10
+
11
+ export type ConstantsKeys = "UNKNOWN_ROUTE";
12
+
13
+ export type Constants = Record<ConstantsKeys, string>;
14
+
15
+ // =============================================================================
16
+ // Error Codes (migrated from router-error)
17
+ // =============================================================================
18
+
19
+ export type ErrorCodes = Record<ErrorCodeKeys, ErrorCodeValues>;
20
+
21
+ /**
22
+ * Error codes for router operations.
23
+ * Used to identify specific failure scenarios in navigation and lifecycle.
24
+ * Frozen to prevent accidental modifications.
25
+ */
26
+ export const errorCodes: ErrorCodeToValueMap = Object.freeze({
27
+ ROUTER_NOT_STARTED: "NOT_STARTED", // navigate() called before start()
28
+ NO_START_PATH_OR_STATE: "NO_START_PATH_OR_STATE", // start() without initial route
29
+ ROUTER_ALREADY_STARTED: "ALREADY_STARTED", // start() called twice
30
+ ROUTE_NOT_FOUND: "ROUTE_NOT_FOUND", // Navigation to non-existent route
31
+ SAME_STATES: "SAME_STATES", // Navigate to current route without reload
32
+ CANNOT_DEACTIVATE: "CANNOT_DEACTIVATE", // canDeactivate guard blocked navigation
33
+ CANNOT_ACTIVATE: "CANNOT_ACTIVATE", // canActivate guard blocked navigation
34
+ TRANSITION_ERR: "TRANSITION_ERR", // Generic transition failure
35
+ TRANSITION_CANCELLED: "CANCELLED", // Navigation cancelled by user or new navigation
36
+ ROUTER_DISPOSED: "DISPOSED", // Router has been disposed
37
+ PLUGIN_CONFLICT: "PLUGIN_CONFLICT", // Plugin tried to extend router with already-existing property
38
+ });
39
+
40
+ /**
41
+ * General router constants.
42
+ * Special route names and identifiers.
43
+ */
44
+ export const UNKNOWN_ROUTE = "@@router/UNKNOWN_ROUTE";
45
+
46
+ export const constants: Constants = {
47
+ UNKNOWN_ROUTE,
48
+ };
49
+
50
+ /**
51
+ * Plugin method names.
52
+ * Maps to methods that plugins can implement to hook into router lifecycle.
53
+ */
54
+ export const plugins: EventToPluginMap = {
55
+ ROUTER_START: "onStart", // Plugin method called when router starts
56
+ ROUTER_STOP: "onStop", // Plugin method called when router stops
57
+ TRANSITION_START: "onTransitionStart", // Plugin method called when navigation begins
58
+ TRANSITION_LEAVE_APPROVE: "onTransitionLeaveApprove", // Plugin method called when deactivation guards pass
59
+ TRANSITION_CANCEL: "onTransitionCancel", // Plugin method called when navigation cancelled
60
+ TRANSITION_SUCCESS: "onTransitionSuccess", // Plugin method called when navigation succeeds
61
+ TRANSITION_ERROR: "onTransitionError", // Plugin method called when navigation fails
62
+ };
63
+
64
+ /**
65
+ * Event names for router event system.
66
+ * Used with addEventListener/removeEventListener for reactive subscriptions.
67
+ */
68
+ export const events: EventToNameMap = {
69
+ ROUTER_START: "$start", // Emitted when router.start() succeeds
70
+ ROUTER_STOP: "$stop", // Emitted when router.stop() is called
71
+ TRANSITION_START: "$$start", // Emitted when navigation begins
72
+ TRANSITION_LEAVE_APPROVE: "$$leaveApprove", // Emitted when deactivation guards pass
73
+ TRANSITION_CANCEL: "$$cancel", // Emitted when navigation is cancelled
74
+ TRANSITION_SUCCESS: "$$success", // Emitted when navigation completes successfully
75
+ TRANSITION_ERROR: "$$error", // Emitted when navigation fails
76
+ };
77
+
78
+ export const DEFAULT_LIMITS = {
79
+ maxDependencies: 100,
80
+ maxPlugins: 50,
81
+ maxListeners: 10_000,
82
+ warnListeners: 1000,
83
+ maxEventDepth: 5,
84
+ maxLifecycleHandlers: 200,
85
+ } as const;
86
+
87
+ export const EMPTY_PARAMS: Readonly<Record<string, never>> = Object.freeze({});