@real-router/core 0.45.1 → 0.45.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 (75) hide show
  1. package/package.json +7 -10
  2. package/src/Router.ts +684 -0
  3. package/src/RouterError.ts +324 -0
  4. package/src/api/cloneRouter.ts +77 -0
  5. package/src/api/getDependenciesApi.ts +168 -0
  6. package/src/api/getLifecycleApi.ts +65 -0
  7. package/src/api/getPluginApi.ts +167 -0
  8. package/src/api/getRoutesApi.ts +573 -0
  9. package/src/api/helpers.ts +10 -0
  10. package/src/api/index.ts +16 -0
  11. package/src/api/types.ts +12 -0
  12. package/src/constants.ts +87 -0
  13. package/src/createRouter.ts +32 -0
  14. package/src/fsm/index.ts +5 -0
  15. package/src/fsm/routerFSM.ts +120 -0
  16. package/src/getNavigator.ts +30 -0
  17. package/src/guards.ts +46 -0
  18. package/src/helpers.ts +179 -0
  19. package/src/index.ts +50 -0
  20. package/src/internals.ts +173 -0
  21. package/src/namespaces/DependenciesNamespace/dependenciesStore.ts +30 -0
  22. package/src/namespaces/DependenciesNamespace/index.ts +5 -0
  23. package/src/namespaces/EventBusNamespace/EventBusNamespace.ts +311 -0
  24. package/src/namespaces/EventBusNamespace/index.ts +5 -0
  25. package/src/namespaces/EventBusNamespace/types.ts +11 -0
  26. package/src/namespaces/NavigationNamespace/NavigationNamespace.ts +405 -0
  27. package/src/namespaces/NavigationNamespace/constants.ts +55 -0
  28. package/src/namespaces/NavigationNamespace/index.ts +5 -0
  29. package/src/namespaces/NavigationNamespace/transition/completeTransition.ts +100 -0
  30. package/src/namespaces/NavigationNamespace/transition/errorHandling.ts +124 -0
  31. package/src/namespaces/NavigationNamespace/transition/guardPhase.ts +221 -0
  32. package/src/namespaces/NavigationNamespace/types.ts +100 -0
  33. package/src/namespaces/OptionsNamespace/OptionsNamespace.ts +28 -0
  34. package/src/namespaces/OptionsNamespace/constants.ts +19 -0
  35. package/src/namespaces/OptionsNamespace/helpers.ts +50 -0
  36. package/src/namespaces/OptionsNamespace/index.ts +7 -0
  37. package/src/namespaces/OptionsNamespace/validators.ts +13 -0
  38. package/src/namespaces/PluginsNamespace/PluginsNamespace.ts +291 -0
  39. package/src/namespaces/PluginsNamespace/constants.ts +34 -0
  40. package/src/namespaces/PluginsNamespace/index.ts +7 -0
  41. package/src/namespaces/PluginsNamespace/types.ts +22 -0
  42. package/src/namespaces/PluginsNamespace/validators.ts +28 -0
  43. package/src/namespaces/RouteLifecycleNamespace/RouteLifecycleNamespace.ts +377 -0
  44. package/src/namespaces/RouteLifecycleNamespace/index.ts +5 -0
  45. package/src/namespaces/RouteLifecycleNamespace/types.ts +10 -0
  46. package/src/namespaces/RouterLifecycleNamespace/RouterLifecycleNamespace.ts +81 -0
  47. package/src/namespaces/RouterLifecycleNamespace/constants.ts +25 -0
  48. package/src/namespaces/RouterLifecycleNamespace/index.ts +5 -0
  49. package/src/namespaces/RouterLifecycleNamespace/types.ts +26 -0
  50. package/src/namespaces/RoutesNamespace/RoutesNamespace.ts +535 -0
  51. package/src/namespaces/RoutesNamespace/constants.ts +6 -0
  52. package/src/namespaces/RoutesNamespace/forwardChain.ts +34 -0
  53. package/src/namespaces/RoutesNamespace/helpers.ts +126 -0
  54. package/src/namespaces/RoutesNamespace/index.ts +11 -0
  55. package/src/namespaces/RoutesNamespace/routeGuards.ts +62 -0
  56. package/src/namespaces/RoutesNamespace/routesStore.ts +346 -0
  57. package/src/namespaces/RoutesNamespace/types.ts +81 -0
  58. package/src/namespaces/StateNamespace/StateNamespace.ts +211 -0
  59. package/src/namespaces/StateNamespace/helpers.ts +24 -0
  60. package/src/namespaces/StateNamespace/index.ts +5 -0
  61. package/src/namespaces/StateNamespace/types.ts +15 -0
  62. package/src/namespaces/index.ts +35 -0
  63. package/src/stateMetaStore.ts +15 -0
  64. package/src/transitionPath.ts +436 -0
  65. package/src/typeGuards.ts +59 -0
  66. package/src/types/RouterValidator.ts +154 -0
  67. package/src/types.ts +69 -0
  68. package/src/utils/getStaticPaths.ts +50 -0
  69. package/src/utils/index.ts +5 -0
  70. package/src/utils/serializeState.ts +22 -0
  71. package/src/validation.ts +12 -0
  72. package/src/wiring/RouterWiringBuilder.ts +261 -0
  73. package/src/wiring/index.ts +7 -0
  74. package/src/wiring/types.ts +47 -0
  75. package/src/wiring/wireRouter.ts +26 -0
@@ -0,0 +1,535 @@
1
+ // packages/core/src/namespaces/RoutesNamespace/RoutesNamespace.ts
2
+
3
+ import { DEFAULT_ROUTE_NAME } from "./constants";
4
+ import { paramsMatch, paramsMatchExcluding } from "./helpers";
5
+ import {
6
+ createRoutesStore,
7
+ rebuildTreeInPlace,
8
+ resetStore,
9
+ } from "./routesStore";
10
+ import { constants } from "../../constants";
11
+ import { getTransitionPath } from "../../transitionPath";
12
+
13
+ import type { RoutesStore } from "./routesStore";
14
+ import type { RoutesDependencies } from "./types";
15
+ import type { Route } from "../../types";
16
+ import type { RouteLifecycleNamespace } from "../RouteLifecycleNamespace";
17
+ import type {
18
+ DefaultDependencies,
19
+ ForwardToCallback,
20
+ Options,
21
+ Params,
22
+ State,
23
+ } from "@real-router/types";
24
+ import type {
25
+ CreateMatcherOptions,
26
+ RouteParams,
27
+ RouteTree,
28
+ RouteTreeState,
29
+ } from "route-tree";
30
+
31
+ function collectUrlParamsArray(segments: readonly RouteTree[]): string[] {
32
+ const params: string[] = [];
33
+
34
+ for (const segment of segments) {
35
+ for (const param of segment.paramMeta.urlParams) {
36
+ params.push(param);
37
+ }
38
+ }
39
+
40
+ return params;
41
+ }
42
+
43
+ export function buildNameFromSegments(
44
+ segments: readonly { fullName: string }[],
45
+ ): string {
46
+ return segments.at(-1)?.fullName ?? "";
47
+ }
48
+
49
+ export function createRouteState<P extends RouteParams = RouteParams>(
50
+ matchResult: {
51
+ readonly segments: readonly { fullName: string }[];
52
+ readonly params: Readonly<Record<string, unknown>>;
53
+ readonly meta: Readonly<Record<string, Record<string, "url" | "query">>>;
54
+ },
55
+ name?: string,
56
+ ): RouteTreeState<P> {
57
+ const resolvedName = name ?? buildNameFromSegments(matchResult.segments);
58
+
59
+ return {
60
+ name: resolvedName,
61
+ params: matchResult.params as P,
62
+ meta: matchResult.meta as Record<string, Record<string, "url" | "query">>,
63
+ };
64
+ }
65
+
66
+ interface CachedBuildPathOpts {
67
+ readonly trailingSlash?: "always" | "never" | undefined;
68
+ readonly queryParamsMode?: "default" | "strict" | "loose" | undefined;
69
+ }
70
+
71
+ /**
72
+ * Independent namespace for managing routes.
73
+ *
74
+ * Static methods handle validation (called by facade).
75
+ * Instance methods handle storage and business logic.
76
+ */
77
+ export class RoutesNamespace<
78
+ Dependencies extends DefaultDependencies = DefaultDependencies,
79
+ > {
80
+ readonly #store: RoutesStore<Dependencies>;
81
+ #cachedBuildPathOpts: CachedBuildPathOpts | undefined;
82
+
83
+ get #deps(): RoutesDependencies<Dependencies> {
84
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
85
+ return this.#store.depsStore!;
86
+ }
87
+
88
+ constructor(
89
+ routes: Route<Dependencies>[] = [],
90
+ matcherOptions?: CreateMatcherOptions,
91
+ ) {
92
+ this.#store = createRoutesStore(routes, matcherOptions);
93
+ }
94
+
95
+ /**
96
+ * Creates a predicate function to check if a route node should be updated.
97
+ * Note: Argument validation is done by facade (Router.ts) via validateShouldUpdateNodeArgs.
98
+ */
99
+ static shouldUpdateNode(
100
+ nodeName: string,
101
+ ): (toState: State, fromState?: State) => boolean {
102
+ return (toState: State, fromState?: State): boolean => {
103
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
104
+ if (!(toState && typeof toState === "object" && "name" in toState)) {
105
+ throw new TypeError(
106
+ "[router.shouldUpdateNode] toState must be valid State object",
107
+ );
108
+ }
109
+
110
+ if (toState.transition?.reload) {
111
+ return true;
112
+ }
113
+
114
+ if (nodeName === DEFAULT_ROUTE_NAME && !fromState) {
115
+ return true;
116
+ }
117
+
118
+ const { intersection, toActivate, toDeactivate } = getTransitionPath(
119
+ toState,
120
+ fromState,
121
+ );
122
+
123
+ if (nodeName === intersection) {
124
+ return true;
125
+ }
126
+
127
+ if (toActivate.includes(nodeName)) {
128
+ return true;
129
+ }
130
+
131
+ return toDeactivate.includes(nodeName);
132
+ };
133
+ }
134
+
135
+ // =========================================================================
136
+ // Dependency injection
137
+ // =========================================================================
138
+
139
+ /**
140
+ * Sets dependencies and registers pending canActivate handlers.
141
+ * canActivate handlers from initial routes are deferred until deps are set.
142
+ */
143
+ setDependencies(deps: RoutesDependencies<Dependencies>): void {
144
+ this.#store.depsStore = deps;
145
+
146
+ for (const [routeName, handler] of this.#store.pendingCanActivate) {
147
+ deps.addActivateGuard(routeName, handler);
148
+ }
149
+
150
+ this.#store.pendingCanActivate.clear();
151
+
152
+ for (const [routeName, handler] of this.#store.pendingCanDeactivate) {
153
+ deps.addDeactivateGuard(routeName, handler);
154
+ }
155
+
156
+ this.#store.pendingCanDeactivate.clear();
157
+ }
158
+
159
+ /**
160
+ * Sets the lifecycle namespace reference.
161
+ */
162
+ setLifecycleNamespace(
163
+ namespace: RouteLifecycleNamespace<Dependencies> | undefined,
164
+ ): void {
165
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
166
+ this.#store.lifecycleNamespace = namespace!;
167
+ }
168
+
169
+ // =========================================================================
170
+ // Route tree operations
171
+ // =========================================================================
172
+
173
+ setRootPath(newRootPath: string): void {
174
+ this.#store.rootPath = newRootPath;
175
+ rebuildTreeInPlace(this.#store);
176
+ }
177
+
178
+ hasRoute(name: string): boolean {
179
+ return this.#store.matcher.hasRoute(name);
180
+ }
181
+
182
+ clearRoutes(): void {
183
+ resetStore(this.#store);
184
+ }
185
+
186
+ // =========================================================================
187
+ // Path operations
188
+ // =========================================================================
189
+
190
+ /**
191
+ * Builds a URL path for a route.
192
+ * Note: Argument validation is done by facade (Router.ts) via validateBuildPathArgs.
193
+ *
194
+ * @param route - Route name
195
+ * @param params - Route parameters
196
+ * @param options - Router options
197
+ */
198
+ buildPath(route: string, params?: Params, options?: Options): string {
199
+ if (route === constants.UNKNOWN_ROUTE) {
200
+ return typeof params?.path === "string" ? params.path : "";
201
+ }
202
+
203
+ const paramsWithDefault = Object.hasOwn(
204
+ this.#store.config.defaultParams,
205
+ route,
206
+ )
207
+ ? { ...this.#store.config.defaultParams[route], ...params }
208
+ : /* v8 ignore next -- @preserve: V8 can't track ?? branch in ternary; covered by buildPath tests without params */ (params ??
209
+ {});
210
+
211
+ const encodedParams =
212
+ typeof this.#store.config.encoders[route] === "function"
213
+ ? this.#store.config.encoders[route]({ ...paramsWithDefault })
214
+ : paramsWithDefault;
215
+
216
+ return this.#store.matcher.buildPath(
217
+ route,
218
+ encodedParams,
219
+ this.#getBuildPathOptions(options),
220
+ );
221
+ }
222
+
223
+ /**
224
+ * Matches a URL path to a route in the tree.
225
+ * Note: Argument validation is done by facade (Router.ts) via validateMatchPathArgs.
226
+ */
227
+ matchPath<P extends Params = Params>(
228
+ path: string,
229
+ options?: Options,
230
+ ): State<P> | undefined {
231
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Router.ts always passes options
232
+ const opts = options!;
233
+
234
+ const matchResult = this.#store.matcher.match(path);
235
+
236
+ if (!matchResult) {
237
+ return undefined;
238
+ }
239
+
240
+ const routeState = createRouteState(matchResult);
241
+ const { name, params, meta } = routeState;
242
+
243
+ const decodedParams =
244
+ typeof this.#store.config.decoders[name] === "function"
245
+ ? this.#store.config.decoders[name](params as Params)
246
+ : params;
247
+
248
+ const { name: routeName, params: routeParams } = this.#deps.forwardState<P>(
249
+ name,
250
+ decodedParams as P,
251
+ );
252
+
253
+ let builtPath = path;
254
+
255
+ if (opts.rewritePathOnMatch) {
256
+ const buildParams =
257
+ typeof this.#store.config.encoders[routeName] === "function"
258
+ ? this.#store.config.encoders[routeName]({
259
+ ...(routeParams as Params),
260
+ })
261
+ : (routeParams as Record<string, unknown>);
262
+
263
+ const ts = opts.trailingSlash;
264
+
265
+ builtPath = this.#store.matcher.buildPath(routeName, buildParams, {
266
+ trailingSlash: ts === "never" || ts === "always" ? ts : undefined,
267
+ queryParamsMode: opts.queryParamsMode,
268
+ });
269
+ }
270
+
271
+ return this.#deps.makeState<P>(routeName, routeParams, builtPath, meta);
272
+ }
273
+
274
+ /**
275
+ * Applies forwardTo and returns resolved state with merged defaultParams.
276
+ *
277
+ * Merges params in order:
278
+ * 1. Source route defaultParams
279
+ * 2. Provided params
280
+ * 3. Target route defaultParams (after resolving forwardTo)
281
+ */
282
+ forwardState<P extends Params = Params>(
283
+ name: string,
284
+ params: P,
285
+ ): { name: string; params: P } {
286
+ if (Object.hasOwn(this.#store.config.forwardFnMap, name)) {
287
+ const paramsWithSourceDefaults = this.#mergeDefaultParams(name, params);
288
+ const dynamicForward = this.#store.config.forwardFnMap[name];
289
+ const resolved = this.#resolveDynamicForward(
290
+ name,
291
+ dynamicForward,
292
+ params,
293
+ );
294
+
295
+ return {
296
+ name: resolved,
297
+ params: this.#mergeDefaultParams(resolved, paramsWithSourceDefaults),
298
+ };
299
+ }
300
+
301
+ const staticForward = this.#store.resolvedForwardMap[name] ?? name;
302
+
303
+ if (
304
+ staticForward !== name &&
305
+ Object.hasOwn(this.#store.config.forwardFnMap, staticForward)
306
+ ) {
307
+ const paramsWithSourceDefaults = this.#mergeDefaultParams(name, params);
308
+ const targetDynamicForward =
309
+ this.#store.config.forwardFnMap[staticForward];
310
+ const resolved = this.#resolveDynamicForward(
311
+ staticForward,
312
+ targetDynamicForward,
313
+ params,
314
+ );
315
+
316
+ return {
317
+ name: resolved,
318
+ params: this.#mergeDefaultParams(resolved, paramsWithSourceDefaults),
319
+ };
320
+ }
321
+
322
+ if (staticForward !== name) {
323
+ const paramsWithSourceDefaults = this.#mergeDefaultParams(name, params);
324
+
325
+ return {
326
+ name: staticForward,
327
+ params: this.#mergeDefaultParams(
328
+ staticForward,
329
+ paramsWithSourceDefaults,
330
+ ),
331
+ };
332
+ }
333
+
334
+ return { name, params: this.#mergeDefaultParams(name, params) };
335
+ }
336
+
337
+ /**
338
+ * Builds a RouteTreeState from already-resolved route name and params.
339
+ * Called by Router.buildState after forwardState is applied at facade level.
340
+ * This allows plugins to intercept forwardState.
341
+ */
342
+ buildStateResolved(
343
+ resolvedName: string,
344
+ resolvedParams: Params,
345
+ ): RouteTreeState | undefined {
346
+ const segments = this.#store.matcher.getSegmentsByName(resolvedName);
347
+
348
+ if (!segments) {
349
+ return undefined;
350
+ }
351
+
352
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
353
+ const meta = this.#store.matcher.getMetaByName(resolvedName)!;
354
+
355
+ return createRouteState(
356
+ { segments, params: resolvedParams, meta },
357
+ resolvedName,
358
+ );
359
+ }
360
+
361
+ // =========================================================================
362
+ // Query operations
363
+ // =========================================================================
364
+
365
+ /**
366
+ * Checks if a route is currently active.
367
+ */
368
+ isActiveRoute(
369
+ name: string,
370
+ params: Params = {},
371
+ strictEquality = false,
372
+ ignoreQueryParams = true,
373
+ ): boolean {
374
+ // Note: empty string check is handled by Router.ts facade
375
+ const activeState = this.#deps.getState();
376
+
377
+ if (!activeState) {
378
+ return false;
379
+ }
380
+
381
+ const activeName = activeState.name;
382
+
383
+ // Fast path: check if routes are related before expensive operations
384
+ if (
385
+ activeName !== name &&
386
+ !activeName.startsWith(`${name}.`) &&
387
+ !name.startsWith(`${activeName}.`)
388
+ ) {
389
+ return false;
390
+ }
391
+
392
+ const defaultParams = this.#store.config.defaultParams[name] as
393
+ | Params
394
+ | undefined;
395
+
396
+ // Exact match case
397
+ if (strictEquality || activeName === name) {
398
+ const effectiveParams = defaultParams
399
+ ? { ...defaultParams, ...params }
400
+ : params;
401
+
402
+ const targetState: State = {
403
+ name,
404
+ params: effectiveParams,
405
+ path: "",
406
+ };
407
+
408
+ return this.#deps.areStatesEqual(
409
+ targetState,
410
+ activeState,
411
+ ignoreQueryParams,
412
+ );
413
+ }
414
+
415
+ // Hierarchical check: activeState is a descendant of target (name)
416
+ const activeParams = activeState.params;
417
+
418
+ if (!paramsMatch(params, activeParams)) {
419
+ return false;
420
+ }
421
+
422
+ // Check defaultParams (skip keys already in params)
423
+ return (
424
+ !defaultParams ||
425
+ paramsMatchExcluding(defaultParams, activeParams, params)
426
+ );
427
+ }
428
+
429
+ getMetaForState(
430
+ name: string,
431
+ ): Record<string, Record<string, "url" | "query">> | undefined {
432
+ return this.#store.matcher.hasRoute(name)
433
+ ? this.#store.matcher.getMetaByName(name)
434
+ : undefined;
435
+ }
436
+
437
+ getUrlParams(name: string): string[] {
438
+ const segments = this.#store.matcher.getSegmentsByName(name);
439
+
440
+ if (!segments) {
441
+ return [];
442
+ }
443
+
444
+ return collectUrlParamsArray(segments as readonly RouteTree[]);
445
+ }
446
+
447
+ getStore(): RoutesStore<Dependencies> {
448
+ return this.#store;
449
+ }
450
+
451
+ #mergeDefaultParams<P extends Params = Params>(
452
+ routeName: string,
453
+ params: P,
454
+ ): P {
455
+ if (Object.hasOwn(this.#store.config.defaultParams, routeName)) {
456
+ return {
457
+ ...this.#store.config.defaultParams[routeName],
458
+ ...params,
459
+ } as P;
460
+ }
461
+
462
+ return params;
463
+ }
464
+
465
+ #getBuildPathOptions(options?: Options): CachedBuildPathOpts {
466
+ if (this.#cachedBuildPathOpts) {
467
+ return this.#cachedBuildPathOpts;
468
+ }
469
+
470
+ const ts = options?.trailingSlash;
471
+
472
+ this.#cachedBuildPathOpts = Object.freeze({
473
+ trailingSlash: ts === "never" || ts === "always" ? ts : undefined,
474
+ queryParamsMode: options?.queryParamsMode,
475
+ });
476
+
477
+ return this.#cachedBuildPathOpts;
478
+ }
479
+
480
+ #resolveDynamicForward(
481
+ startName: string,
482
+ startFn: ForwardToCallback<Dependencies>,
483
+ params: Params,
484
+ ): string {
485
+ const visited = new Set<string>([startName]);
486
+
487
+ let current = startFn(this.#deps.getDependency, params);
488
+ let depth = 0;
489
+ const MAX_DEPTH = 100;
490
+
491
+ if (typeof current !== "string") {
492
+ throw new TypeError(
493
+ `forwardTo callback must return a string, got ${typeof current}`,
494
+ );
495
+ }
496
+
497
+ while (depth < MAX_DEPTH) {
498
+ if (this.#store.matcher.getSegmentsByName(current) === undefined) {
499
+ throw new Error(`Route "${current}" does not exist`);
500
+ }
501
+
502
+ if (visited.has(current)) {
503
+ const chain = [...visited, current].join(" → ");
504
+
505
+ throw new Error(`Circular forwardTo detected: ${chain}`);
506
+ }
507
+
508
+ visited.add(current);
509
+
510
+ if (Object.hasOwn(this.#store.config.forwardFnMap, current)) {
511
+ const fn = this.#store.config.forwardFnMap[
512
+ current
513
+ ] as ForwardToCallback<Dependencies>;
514
+
515
+ current = fn(this.#deps.getDependency, params);
516
+
517
+ depth++;
518
+ continue;
519
+ }
520
+
521
+ const staticForward = this.#store.config.forwardMap[current];
522
+
523
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
524
+ if (staticForward !== undefined) {
525
+ current = staticForward;
526
+ depth++;
527
+ continue;
528
+ }
529
+
530
+ return current;
531
+ }
532
+
533
+ throw new Error(`forwardTo exceeds maximum depth of ${MAX_DEPTH}`);
534
+ }
535
+ }
@@ -0,0 +1,6 @@
1
+ // packages/core/src/namespaces/RoutesNamespace/constants.ts
2
+
3
+ /**
4
+ * Default route name for the root node.
5
+ */
6
+ export const DEFAULT_ROUTE_NAME = "";
@@ -0,0 +1,34 @@
1
+ // packages/core/src/namespaces/RoutesNamespace/forwardChain.ts
2
+
3
+ export function resolveForwardChain(
4
+ startRoute: string,
5
+ forwardMap: Record<string, string>,
6
+ maxDepth = 100,
7
+ ): string {
8
+ const visited = new Set<string>();
9
+ const chain: string[] = [startRoute];
10
+ let current = startRoute;
11
+
12
+ while (forwardMap[current]) {
13
+ const next = forwardMap[current];
14
+
15
+ if (visited.has(next)) {
16
+ const cycleStart = chain.indexOf(next);
17
+ const cycle = [...chain.slice(cycleStart), next];
18
+
19
+ throw new Error(`Circular forwardTo: ${cycle.join(" → ")}`);
20
+ }
21
+
22
+ visited.add(current);
23
+ chain.push(next);
24
+ current = next;
25
+
26
+ if (chain.length > maxDepth) {
27
+ throw new Error(
28
+ `forwardTo chain exceeds maximum depth (${maxDepth}): ${chain.join(" → ")}`,
29
+ );
30
+ }
31
+ }
32
+
33
+ return current;
34
+ }
@@ -0,0 +1,126 @@
1
+ // packages/core/src/namespaces/RoutesNamespace/helpers.ts
2
+
3
+ import type { RouteConfig } from "./types";
4
+ import type { Route } from "../../types";
5
+ import type {
6
+ DefaultDependencies,
7
+ ForwardToCallback,
8
+ Params,
9
+ } from "@real-router/types";
10
+ import type { RouteDefinition } from "route-tree";
11
+
12
+ /**
13
+ * Creates an empty RouteConfig.
14
+ */
15
+ export function createEmptyConfig(): RouteConfig {
16
+ return {
17
+ decoders: Object.create(null) as Record<string, (params: Params) => Params>,
18
+ encoders: Object.create(null) as Record<string, (params: Params) => Params>,
19
+ defaultParams: Object.create(null) as Record<string, Params>,
20
+ forwardMap: Object.create(null) as Record<string, string>,
21
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
22
+ forwardFnMap: Object.create(null) as Record<string, ForwardToCallback<any>>,
23
+ };
24
+ }
25
+
26
+ // ============================================================================
27
+ // Route Tree Helpers
28
+ // ============================================================================
29
+
30
+ /**
31
+ * Checks if all params from source exist with same values in target.
32
+ * Small function body allows V8 inlining.
33
+ */
34
+ export function paramsMatch(source: Params, target: Params): boolean {
35
+ for (const key in source) {
36
+ if (source[key] !== target[key]) {
37
+ return false;
38
+ }
39
+ }
40
+
41
+ return true;
42
+ }
43
+
44
+ /**
45
+ * Checks params match, skipping keys present in skipKeys.
46
+ */
47
+ export function paramsMatchExcluding(
48
+ source: Params,
49
+ target: Params,
50
+ skipKeys: Params,
51
+ ): boolean {
52
+ for (const key in source) {
53
+ if (key in skipKeys) {
54
+ continue;
55
+ }
56
+ if (source[key] !== target[key]) {
57
+ return false;
58
+ }
59
+ }
60
+
61
+ return true;
62
+ }
63
+
64
+ /**
65
+ * Sanitizes a route by keeping only essential properties.
66
+ */
67
+ export function sanitizeRoute<Dependencies extends DefaultDependencies>(
68
+ route: Route<Dependencies>,
69
+ ): RouteDefinition {
70
+ const sanitized: RouteDefinition = {
71
+ name: route.name,
72
+ path: route.path,
73
+ };
74
+
75
+ if (route.children) {
76
+ sanitized.children = route.children.map((child) => sanitizeRoute(child));
77
+ }
78
+
79
+ return sanitized;
80
+ }
81
+
82
+ /**
83
+ * Recursively removes a route from definitions array.
84
+ */
85
+ export function removeFromDefinitions(
86
+ definitions: RouteDefinition[],
87
+ routeName: string,
88
+ parentPrefix = "",
89
+ ): boolean {
90
+ for (let i = 0; i < definitions.length; i++) {
91
+ const route = definitions[i];
92
+ const fullName = parentPrefix
93
+ ? `${parentPrefix}.${route.name}`
94
+ : route.name;
95
+
96
+ if (fullName === routeName) {
97
+ definitions.splice(i, 1);
98
+
99
+ return true;
100
+ }
101
+
102
+ if (
103
+ route.children &&
104
+ routeName.startsWith(`${fullName}.`) &&
105
+ removeFromDefinitions(route.children, routeName, fullName)
106
+ ) {
107
+ return true;
108
+ }
109
+ }
110
+
111
+ return false;
112
+ }
113
+
114
+ /**
115
+ * Clears configuration entries that match the predicate.
116
+ */
117
+ export function clearConfigEntries<T>(
118
+ config: Record<string, T>,
119
+ matcher: (key: string) => boolean,
120
+ ): void {
121
+ for (const key of Object.keys(config)) {
122
+ if (matcher(key)) {
123
+ delete config[key];
124
+ }
125
+ }
126
+ }
@@ -0,0 +1,11 @@
1
+ // packages/core/src/namespaces/RoutesNamespace/index.ts
2
+
3
+ export { RoutesNamespace } from "./RoutesNamespace";
4
+
5
+ export { DEFAULT_ROUTE_NAME } from "./constants";
6
+
7
+ export { createEmptyConfig } from "./helpers";
8
+
9
+ export type { RouteConfig, RoutesDependencies } from "./types";
10
+
11
+ export type { RoutesStore } from "./routesStore";