@real-router/core 0.22.0 → 0.23.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 (80) hide show
  1. package/README.md +1 -3
  2. package/dist/cjs/index.d.ts +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/index.d.mts +1 -1
  7. package/dist/esm/index.mjs +1 -1
  8. package/dist/esm/index.mjs.map +1 -1
  9. package/dist/esm/metafile-esm.json +1 -1
  10. package/package.json +7 -5
  11. package/src/Router.ts +1174 -0
  12. package/src/RouterError.ts +324 -0
  13. package/src/constants.ts +112 -0
  14. package/src/createRouter.ts +32 -0
  15. package/src/fsm/index.ts +5 -0
  16. package/src/fsm/routerFSM.ts +129 -0
  17. package/src/getNavigator.ts +15 -0
  18. package/src/helpers.ts +194 -0
  19. package/src/index.ts +46 -0
  20. package/src/namespaces/CloneNamespace/CloneNamespace.ts +120 -0
  21. package/src/namespaces/CloneNamespace/index.ts +3 -0
  22. package/src/namespaces/CloneNamespace/types.ts +46 -0
  23. package/src/namespaces/DependenciesNamespace/DependenciesNamespace.ts +250 -0
  24. package/src/namespaces/DependenciesNamespace/index.ts +3 -0
  25. package/src/namespaces/DependenciesNamespace/validators.ts +105 -0
  26. package/src/namespaces/EventBusNamespace/EventBusNamespace.ts +272 -0
  27. package/src/namespaces/EventBusNamespace/index.ts +5 -0
  28. package/src/namespaces/EventBusNamespace/types.ts +11 -0
  29. package/src/namespaces/MiddlewareNamespace/MiddlewareNamespace.ts +206 -0
  30. package/src/namespaces/MiddlewareNamespace/index.ts +5 -0
  31. package/src/namespaces/MiddlewareNamespace/types.ts +28 -0
  32. package/src/namespaces/MiddlewareNamespace/validators.ts +96 -0
  33. package/src/namespaces/NavigationNamespace/NavigationNamespace.ts +308 -0
  34. package/src/namespaces/NavigationNamespace/index.ts +5 -0
  35. package/src/namespaces/NavigationNamespace/transition/executeLifecycleHooks.ts +84 -0
  36. package/src/namespaces/NavigationNamespace/transition/executeMiddleware.ts +56 -0
  37. package/src/namespaces/NavigationNamespace/transition/index.ts +107 -0
  38. package/src/namespaces/NavigationNamespace/transition/makeError.ts +37 -0
  39. package/src/namespaces/NavigationNamespace/transition/mergeStates.ts +54 -0
  40. package/src/namespaces/NavigationNamespace/transition/processLifecycleResult.ts +81 -0
  41. package/src/namespaces/NavigationNamespace/transition/wrapSyncError.ts +82 -0
  42. package/src/namespaces/NavigationNamespace/types.ts +129 -0
  43. package/src/namespaces/NavigationNamespace/validators.ts +87 -0
  44. package/src/namespaces/OptionsNamespace/OptionsNamespace.ts +50 -0
  45. package/src/namespaces/OptionsNamespace/constants.ts +41 -0
  46. package/src/namespaces/OptionsNamespace/helpers.ts +51 -0
  47. package/src/namespaces/OptionsNamespace/index.ts +11 -0
  48. package/src/namespaces/OptionsNamespace/validators.ts +252 -0
  49. package/src/namespaces/PluginsNamespace/PluginsNamespace.ts +325 -0
  50. package/src/namespaces/PluginsNamespace/constants.ts +35 -0
  51. package/src/namespaces/PluginsNamespace/index.ts +7 -0
  52. package/src/namespaces/PluginsNamespace/types.ts +32 -0
  53. package/src/namespaces/PluginsNamespace/validators.ts +79 -0
  54. package/src/namespaces/RouteLifecycleNamespace/RouteLifecycleNamespace.ts +389 -0
  55. package/src/namespaces/RouteLifecycleNamespace/index.ts +5 -0
  56. package/src/namespaces/RouteLifecycleNamespace/types.ts +17 -0
  57. package/src/namespaces/RouteLifecycleNamespace/validators.ts +65 -0
  58. package/src/namespaces/RouterLifecycleNamespace/RouterLifecycleNamespace.ts +140 -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 +23 -0
  62. package/src/namespaces/RoutesNamespace/RoutesNamespace.ts +1482 -0
  63. package/src/namespaces/RoutesNamespace/constants.ts +14 -0
  64. package/src/namespaces/RoutesNamespace/helpers.ts +532 -0
  65. package/src/namespaces/RoutesNamespace/index.ts +9 -0
  66. package/src/namespaces/RoutesNamespace/stateBuilder.ts +70 -0
  67. package/src/namespaces/RoutesNamespace/types.ts +82 -0
  68. package/src/namespaces/RoutesNamespace/validators.ts +331 -0
  69. package/src/namespaces/StateNamespace/StateNamespace.ts +317 -0
  70. package/src/namespaces/StateNamespace/helpers.ts +43 -0
  71. package/src/namespaces/StateNamespace/index.ts +5 -0
  72. package/src/namespaces/StateNamespace/types.ts +15 -0
  73. package/src/namespaces/index.ts +42 -0
  74. package/src/transitionPath.ts +441 -0
  75. package/src/typeGuards.ts +74 -0
  76. package/src/types.ts +194 -0
  77. package/src/wiring/RouterWiringBuilder.ts +235 -0
  78. package/src/wiring/index.ts +7 -0
  79. package/src/wiring/types.ts +53 -0
  80. package/src/wiring/wireRouter.ts +29 -0
@@ -0,0 +1,14 @@
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 = "";
7
+
8
+ /**
9
+ * Cache for validated route names to skip regex validation on repeated calls.
10
+ * Key insight: validateRouteName() regex takes ~40ns, but cache lookup is ~1ns.
11
+ * This cache is module-level (shared across all router instances) since route name
12
+ * validity is independent of router instance.
13
+ */
14
+ export const validatedRouteNames = new Set<string>();
@@ -0,0 +1,532 @@
1
+ // packages/core/src/namespaces/RoutesNamespace/helpers.ts
2
+
3
+ import { getSegmentsByName } from "route-tree";
4
+ import { getTypeDescription } from "type-guards";
5
+
6
+ import type { RouteConfig } from "./types";
7
+ import type { Route } from "../../types";
8
+ import type {
9
+ DefaultDependencies,
10
+ ForwardToCallback,
11
+ Params,
12
+ } from "@real-router/types";
13
+ import type { RouteDefinition, RouteTree } from "route-tree";
14
+
15
+ /**
16
+ * Creates an empty RouteConfig.
17
+ */
18
+ export function createEmptyConfig(): RouteConfig {
19
+ return {
20
+ decoders: Object.create(null) as Record<string, (params: Params) => Params>,
21
+ encoders: Object.create(null) as Record<string, (params: Params) => Params>,
22
+ defaultParams: Object.create(null) as Record<string, Params>,
23
+ forwardMap: Object.create(null) as Record<string, string>,
24
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
25
+ forwardFnMap: Object.create(null) as Record<string, ForwardToCallback<any>>,
26
+ };
27
+ }
28
+
29
+ // ============================================================================
30
+ // Route Tree Helpers
31
+ // ============================================================================
32
+
33
+ /**
34
+ * Checks if all params from source exist with same values in target.
35
+ * Small function body allows V8 inlining.
36
+ */
37
+ export function paramsMatch(source: Params, target: Params): boolean {
38
+ for (const key in source) {
39
+ if (source[key] !== target[key]) {
40
+ return false;
41
+ }
42
+ }
43
+
44
+ return true;
45
+ }
46
+
47
+ /**
48
+ * Checks params match, skipping keys present in skipKeys.
49
+ */
50
+ export function paramsMatchExcluding(
51
+ source: Params,
52
+ target: Params,
53
+ skipKeys: Params,
54
+ ): boolean {
55
+ for (const key in source) {
56
+ if (key in skipKeys) {
57
+ continue;
58
+ }
59
+ if (source[key] !== target[key]) {
60
+ return false;
61
+ }
62
+ }
63
+
64
+ return true;
65
+ }
66
+
67
+ /**
68
+ * Sanitizes a route by keeping only essential properties.
69
+ */
70
+ export function sanitizeRoute<Dependencies extends DefaultDependencies>(
71
+ route: Route<Dependencies>,
72
+ ): RouteDefinition {
73
+ const sanitized: RouteDefinition = {
74
+ name: route.name,
75
+ path: route.path,
76
+ };
77
+
78
+ if (route.children) {
79
+ sanitized.children = route.children.map((child) => sanitizeRoute(child));
80
+ }
81
+
82
+ return sanitized;
83
+ }
84
+
85
+ /**
86
+ * Recursively removes a route from definitions array.
87
+ */
88
+ export function removeFromDefinitions(
89
+ definitions: RouteDefinition[],
90
+ routeName: string,
91
+ parentPrefix = "",
92
+ ): boolean {
93
+ for (let i = 0; i < definitions.length; i++) {
94
+ const route = definitions[i];
95
+ const fullName = parentPrefix
96
+ ? `${parentPrefix}.${route.name}`
97
+ : route.name;
98
+
99
+ if (fullName === routeName) {
100
+ definitions.splice(i, 1);
101
+
102
+ return true;
103
+ }
104
+
105
+ if (
106
+ route.children &&
107
+ routeName.startsWith(`${fullName}.`) &&
108
+ removeFromDefinitions(route.children, routeName, fullName)
109
+ ) {
110
+ return true;
111
+ }
112
+ }
113
+
114
+ return false;
115
+ }
116
+
117
+ /**
118
+ * Clears configuration entries that match the predicate.
119
+ */
120
+ export function clearConfigEntries<T>(
121
+ config: Record<string, T>,
122
+ matcher: (key: string) => boolean,
123
+ ): void {
124
+ for (const key of Object.keys(config)) {
125
+ if (matcher(key)) {
126
+ delete config[key];
127
+ }
128
+ }
129
+ }
130
+
131
+ // ============================================================================
132
+ // Route Property Validation
133
+ // ============================================================================
134
+
135
+ /**
136
+ * Validates forwardTo property type and async status.
137
+ */
138
+ function validateForwardToProperty(forwardTo: unknown, fullName: string): void {
139
+ if (forwardTo === undefined) {
140
+ return;
141
+ }
142
+
143
+ if (typeof forwardTo === "function") {
144
+ const isNativeAsync =
145
+ (forwardTo as { constructor: { name: string } }).constructor.name ===
146
+ "AsyncFunction";
147
+ const isTranspiledAsync = forwardTo.toString().includes("__awaiter");
148
+
149
+ if (isNativeAsync || isTranspiledAsync) {
150
+ throw new TypeError(
151
+ `[router.addRoute] forwardTo callback cannot be async for route "${fullName}". ` +
152
+ `Async functions break matchPath/buildPath.`,
153
+ );
154
+ }
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Validates route properties for addRoute.
160
+ * Throws TypeError if any property is invalid.
161
+ *
162
+ * @param route - Route to validate
163
+ * @param fullName - Full route name (with parent prefix)
164
+ * @throws {TypeError} If canActivate/canDeactivate is not a function
165
+ * @throws {TypeError} If defaultParams is not a plain object
166
+ * @throws {TypeError} If decodeParams/encodeParams is async
167
+ */
168
+ export function validateRouteProperties<
169
+ Dependencies extends DefaultDependencies,
170
+ >(route: Route<Dependencies>, fullName: string): void {
171
+ // Validate canActivate is a function
172
+ if (
173
+ route.canActivate !== undefined &&
174
+ typeof route.canActivate !== "function"
175
+ ) {
176
+ throw new TypeError(
177
+ `[router.addRoute] canActivate must be a function for route "${fullName}", ` +
178
+ `got ${getTypeDescription(route.canActivate)}`,
179
+ );
180
+ }
181
+
182
+ // Validate canDeactivate is a function
183
+ if (
184
+ route.canDeactivate !== undefined &&
185
+ typeof route.canDeactivate !== "function"
186
+ ) {
187
+ throw new TypeError(
188
+ `[router.addRoute] canDeactivate must be a function for route "${fullName}", ` +
189
+ `got ${getTypeDescription(route.canDeactivate)}`,
190
+ );
191
+ }
192
+
193
+ // Validate defaultParams is a plain object
194
+ // Runtime check for invalid types passed via `as any`
195
+ if (route.defaultParams !== undefined) {
196
+ const params: unknown = route.defaultParams;
197
+
198
+ if (
199
+ params === null ||
200
+ typeof params !== "object" ||
201
+ Array.isArray(params)
202
+ ) {
203
+ throw new TypeError(
204
+ `[router.addRoute] defaultParams must be an object for route "${fullName}", ` +
205
+ `got ${getTypeDescription(route.defaultParams)}`,
206
+ );
207
+ }
208
+ }
209
+
210
+ // Validate decodeParams is not async (sync required for matchPath/buildPath)
211
+ if (route.decodeParams?.constructor.name === "AsyncFunction") {
212
+ throw new TypeError(
213
+ `[router.addRoute] decodeParams cannot be async for route "${fullName}". Async functions break matchPath/buildPath.`,
214
+ );
215
+ }
216
+
217
+ // Validate encodeParams is not async (sync required for matchPath/buildPath)
218
+ if (route.encodeParams?.constructor.name === "AsyncFunction") {
219
+ throw new TypeError(
220
+ `[router.addRoute] encodeParams cannot be async for route "${fullName}". Async functions break matchPath/buildPath.`,
221
+ );
222
+ }
223
+
224
+ // Validate forwardTo type and async
225
+ validateForwardToProperty(route.forwardTo, fullName);
226
+
227
+ // Recursively validate children
228
+ if (route.children) {
229
+ for (const child of route.children) {
230
+ const childFullName = `${fullName}.${child.name}`;
231
+
232
+ validateRouteProperties(child, childFullName);
233
+ }
234
+ }
235
+ }
236
+
237
+ // ============================================================================
238
+ // ForwardTo Validation
239
+ // ============================================================================
240
+
241
+ /**
242
+ * Extracts parameter names from a path string.
243
+ * Matches :param and *splat patterns.
244
+ */
245
+ function extractParamsFromPath(path: string): Set<string> {
246
+ const params = new Set<string>();
247
+ const paramRegex = /[*:]([A-Z_a-z]\w*)/g;
248
+ let match;
249
+
250
+ while ((match = paramRegex.exec(path)) !== null) {
251
+ params.add(match[1]);
252
+ }
253
+
254
+ return params;
255
+ }
256
+
257
+ /**
258
+ * Extracts all parameters from multiple path segments.
259
+ */
260
+ function extractParamsFromPaths(paths: readonly string[]): Set<string> {
261
+ const params = new Set<string>();
262
+
263
+ for (const path of paths) {
264
+ for (const param of extractParamsFromPath(path)) {
265
+ params.add(param);
266
+ }
267
+ }
268
+
269
+ return params;
270
+ }
271
+
272
+ /**
273
+ * Collects all path segments for a route from batch definitions.
274
+ * Traverses parent routes to include inherited path segments.
275
+ */
276
+ function collectPathsToRoute<Dependencies extends DefaultDependencies>(
277
+ routes: readonly Route<Dependencies>[],
278
+ routeName: string,
279
+ parentName = "",
280
+ paths: string[] = [],
281
+ ): string[] {
282
+ for (const route of routes) {
283
+ const fullName = parentName ? `${parentName}.${route.name}` : route.name;
284
+ const currentPaths = [...paths, route.path];
285
+
286
+ if (fullName === routeName) {
287
+ return currentPaths;
288
+ }
289
+
290
+ if (route.children && routeName.startsWith(`${fullName}.`)) {
291
+ return collectPathsToRoute(
292
+ route.children,
293
+ routeName,
294
+ fullName,
295
+ currentPaths,
296
+ );
297
+ }
298
+ }
299
+
300
+ /* v8 ignore next -- @preserve unreachable: callers validate existence */
301
+ throw new Error(
302
+ `[internal] collectPathsToRoute: route "${routeName}" not found`,
303
+ );
304
+ }
305
+
306
+ /**
307
+ * Collects all route names from a batch of routes (including children).
308
+ */
309
+ function collectRouteNames<Dependencies extends DefaultDependencies>(
310
+ routes: readonly Route<Dependencies>[],
311
+ parentName = "",
312
+ ): Set<string> {
313
+ const names = new Set<string>();
314
+
315
+ for (const route of routes) {
316
+ const fullName = parentName ? `${parentName}.${route.name}` : route.name;
317
+
318
+ names.add(fullName);
319
+
320
+ if (route.children) {
321
+ for (const childName of collectRouteNames(route.children, fullName)) {
322
+ names.add(childName);
323
+ }
324
+ }
325
+ }
326
+
327
+ return names;
328
+ }
329
+
330
+ /**
331
+ * Collects all forwardTo mappings from a batch of routes (including children).
332
+ * Only collects string forwardTo values; callbacks are handled separately.
333
+ */
334
+ function collectForwardMappings<Dependencies extends DefaultDependencies>(
335
+ routes: readonly Route<Dependencies>[],
336
+ parentName = "",
337
+ ): Map<string, string> {
338
+ const mappings = new Map<string, string>();
339
+
340
+ for (const route of routes) {
341
+ const fullName = parentName ? `${parentName}.${route.name}` : route.name;
342
+
343
+ if (route.forwardTo && typeof route.forwardTo === "string") {
344
+ mappings.set(fullName, route.forwardTo);
345
+ }
346
+
347
+ if (route.children) {
348
+ for (const [key, value] of collectForwardMappings(
349
+ route.children,
350
+ fullName,
351
+ )) {
352
+ mappings.set(key, value);
353
+ }
354
+ }
355
+ }
356
+
357
+ return mappings;
358
+ }
359
+
360
+ /**
361
+ * Extracts required path parameters from route segments.
362
+ */
363
+ function getRequiredParams(segments: readonly RouteTree[]): Set<string> {
364
+ const params = new Set<string>();
365
+
366
+ for (const segment of segments) {
367
+ // Named routes always have parsers (null only for root without path)
368
+ for (const param of segment.paramMeta.urlParams) {
369
+ params.add(param);
370
+ }
371
+
372
+ for (const param of segment.paramMeta.spatParams) {
373
+ params.add(param);
374
+ }
375
+ }
376
+
377
+ return params;
378
+ }
379
+
380
+ /**
381
+ * Checks if a route exists in the tree by navigating through children Map.
382
+ */
383
+ function routeExistsInTree(tree: RouteTree, routeName: string): boolean {
384
+ const segments = routeName.split(".");
385
+ let current: RouteTree | undefined = tree;
386
+
387
+ for (const segment of segments) {
388
+ current = current.children.get(segment);
389
+
390
+ if (!current) {
391
+ return false;
392
+ }
393
+ }
394
+
395
+ return true;
396
+ }
397
+
398
+ /**
399
+ * Gets the target route parameters for validation.
400
+ */
401
+ function getTargetParams<Dependencies extends DefaultDependencies>(
402
+ targetRoute: string,
403
+ existsInTree: boolean,
404
+ tree: RouteTree,
405
+ routes: readonly Route<Dependencies>[],
406
+ ): Set<string> {
407
+ if (existsInTree) {
408
+ const toSegments = getSegmentsByName(tree, targetRoute);
409
+
410
+ // toSegments won't be null since we checked existsInTree
411
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
412
+ return getRequiredParams(toSegments!);
413
+ }
414
+
415
+ // Target is in batch
416
+ return extractParamsFromPaths(collectPathsToRoute(routes, targetRoute));
417
+ }
418
+
419
+ /**
420
+ * Validates a single forward mapping for target existence and param compatibility.
421
+ */
422
+ function validateSingleForward<Dependencies extends DefaultDependencies>(
423
+ fromRoute: string,
424
+ targetRoute: string,
425
+ routes: readonly Route<Dependencies>[],
426
+ batchNames: Set<string>,
427
+ tree: RouteTree,
428
+ ): void {
429
+ const existsInTree = routeExistsInTree(tree, targetRoute);
430
+ const existsInBatch = batchNames.has(targetRoute);
431
+
432
+ if (!existsInTree && !existsInBatch) {
433
+ throw new Error(
434
+ `[router.addRoute] forwardTo target "${targetRoute}" does not exist ` +
435
+ `for route "${fromRoute}"`,
436
+ );
437
+ }
438
+
439
+ // Get source params
440
+ const fromParams = extractParamsFromPaths(
441
+ collectPathsToRoute(routes, fromRoute),
442
+ );
443
+
444
+ // Get target params
445
+ const toParams = getTargetParams(targetRoute, existsInTree, tree, routes);
446
+
447
+ // Check for missing params
448
+ const missingParams = [...toParams].filter((p) => !fromParams.has(p));
449
+
450
+ if (missingParams.length > 0) {
451
+ throw new Error(
452
+ `[router.addRoute] forwardTo target "${targetRoute}" requires params ` +
453
+ `[${missingParams.join(", ")}] that are not available in source route "${fromRoute}"`,
454
+ );
455
+ }
456
+ }
457
+
458
+ /**
459
+ * Resolves a forwardTo chain to its final destination.
460
+ * Detects cycles and enforces max depth.
461
+ */
462
+ export function resolveForwardChain(
463
+ startRoute: string,
464
+ forwardMap: Record<string, string>,
465
+ maxDepth = 100,
466
+ ): string {
467
+ const visited = new Set<string>();
468
+ const chain: string[] = [startRoute];
469
+ let current = startRoute;
470
+
471
+ while (forwardMap[current]) {
472
+ const next = forwardMap[current];
473
+
474
+ if (visited.has(next)) {
475
+ const cycleStart = chain.indexOf(next);
476
+ const cycle = [...chain.slice(cycleStart), next];
477
+
478
+ throw new Error(`Circular forwardTo: ${cycle.join(" → ")}`);
479
+ }
480
+
481
+ visited.add(current);
482
+ chain.push(next);
483
+ current = next;
484
+
485
+ if (chain.length > maxDepth) {
486
+ throw new Error(
487
+ `forwardTo chain exceeds maximum depth (${maxDepth}): ${chain.join(" → ")}`,
488
+ );
489
+ }
490
+ }
491
+
492
+ return current;
493
+ }
494
+
495
+ /**
496
+ * Validates forwardTo targets and cycles BEFORE any modifications.
497
+ * This ensures atomicity - if validation fails, no routes are added.
498
+ *
499
+ * @param routes - Routes to validate
500
+ * @param existingForwardMap - Current forwardMap from router.config
501
+ * @param tree - Current route tree
502
+ *
503
+ * @throws {Error} If forwardTo target doesn't exist
504
+ * @throws {Error} If circular forwardTo is detected
505
+ */
506
+ export function validateForwardToTargets<
507
+ Dependencies extends DefaultDependencies,
508
+ >(
509
+ routes: readonly Route<Dependencies>[],
510
+ existingForwardMap: Record<string, string>,
511
+ tree: RouteTree,
512
+ ): void {
513
+ const batchNames = collectRouteNames(routes);
514
+ const batchForwards = collectForwardMappings(routes);
515
+
516
+ // Merge with existing forwardMap for cycle detection
517
+ const combinedForwardMap: Record<string, string> = { ...existingForwardMap };
518
+
519
+ for (const [from, to] of batchForwards) {
520
+ combinedForwardMap[from] = to;
521
+ }
522
+
523
+ // Validate each forwardTo target exists and params are compatible
524
+ for (const [fromRoute, targetRoute] of batchForwards) {
525
+ validateSingleForward(fromRoute, targetRoute, routes, batchNames, tree);
526
+ }
527
+
528
+ // Check for cycles in the combined forwardMap
529
+ for (const fromRoute of Object.keys(combinedForwardMap)) {
530
+ resolveForwardChain(fromRoute, combinedForwardMap);
531
+ }
532
+ }
@@ -0,0 +1,9 @@
1
+ // packages/core/src/namespaces/RoutesNamespace/index.ts
2
+
3
+ export { RoutesNamespace } from "./RoutesNamespace";
4
+
5
+ export { DEFAULT_ROUTE_NAME, validatedRouteNames } from "./constants";
6
+
7
+ export { createEmptyConfig } from "./helpers";
8
+
9
+ export type { RouteConfig, RoutesDependencies } from "./types";
@@ -0,0 +1,70 @@
1
+ // packages/real-router/modules/core/stateBuilder.ts
2
+
3
+ /**
4
+ * State Builder Utilities.
5
+ *
6
+ * Functions for building RouteTreeState from raw route segments.
7
+ * This module handles the conversion from low-level route-node data
8
+ * to the higher-level state representation used by real-router.
9
+ *
10
+ * @module core/stateBuilder
11
+ */
12
+
13
+ import type { RouteParams, RouteTreeState } from "route-tree";
14
+
15
+ /**
16
+ * Builds a dot-separated route name from segments.
17
+ *
18
+ * @param segments - Array of route segments with names
19
+ * @returns Dot-separated route name (e.g., "users.profile")
20
+ *
21
+ * @example
22
+ * ```typescript
23
+ * const segments = [{ name: "users" }, { name: "profile" }];
24
+ * buildNameFromSegments(segments); // "users.profile"
25
+ * ```
26
+ */
27
+ export function buildNameFromSegments(
28
+ segments: readonly { fullName: string }[],
29
+ ): string {
30
+ return segments.at(-1)?.fullName ?? "";
31
+ }
32
+
33
+ /**
34
+ * Creates a RouteTreeState from a match result.
35
+ *
36
+ * This function is the primary way to build a RouteTreeState when
37
+ * you have a result from matcher.match().
38
+ *
39
+ * @param matchResult - Result from matcher.match() containing segments and params
40
+ * @param matchResult.segments - Matched route segments
41
+ * @param matchResult.params - Matched route params
42
+ * @param matchResult.meta - Matched route meta
43
+ * @param name - Optional explicit name (if not provided, built from segments)
44
+ * @returns RouteTreeState with name, params, and meta
45
+ *
46
+ * @example
47
+ * ```typescript
48
+ * const matchResult = matcher.match("/users/123");
49
+ * if (matchResult) {
50
+ * const state = createRouteState(matchResult);
51
+ * // { name: "users.profile", params: { id: "123" }, meta: {...} }
52
+ * }
53
+ * ```
54
+ */
55
+ export function createRouteState<P extends RouteParams = RouteParams>(
56
+ matchResult: {
57
+ readonly segments: readonly { fullName: string }[];
58
+ readonly params: Readonly<Record<string, unknown>>;
59
+ readonly meta: Readonly<Record<string, Record<string, "url" | "query">>>;
60
+ },
61
+ name?: string,
62
+ ): RouteTreeState<P> {
63
+ const resolvedName = name ?? buildNameFromSegments(matchResult.segments);
64
+
65
+ return {
66
+ name: resolvedName,
67
+ params: matchResult.params as P,
68
+ meta: matchResult.meta as Record<string, Record<string, "url" | "query">>,
69
+ };
70
+ }
@@ -0,0 +1,82 @@
1
+ // packages/core/src/namespaces/RoutesNamespace/types.ts
2
+
3
+ import type { ActivationFnFactory } from "../../types";
4
+ import type {
5
+ DefaultDependencies,
6
+ ForwardToCallback,
7
+ Params,
8
+ SimpleState,
9
+ State,
10
+ StateMetaInput,
11
+ } from "@real-router/types";
12
+
13
+ /**
14
+ * Dependencies injected into RoutesNamespace.
15
+ *
16
+ * These are function references from the Router facade,
17
+ * avoiding the need to pass the entire Router object.
18
+ */
19
+ export interface RoutesDependencies<
20
+ Dependencies extends DefaultDependencies = DefaultDependencies,
21
+ > {
22
+ /** Register canActivate handler for a route */
23
+ addActivateGuard: (
24
+ name: string,
25
+ handler: ActivationFnFactory<Dependencies>,
26
+ ) => void;
27
+
28
+ /** Register canDeactivate handler for a route */
29
+ addDeactivateGuard: (
30
+ name: string,
31
+ handler: ActivationFnFactory<Dependencies>,
32
+ ) => void;
33
+
34
+ /** Create state object */
35
+ makeState: <P extends Params = Params, MP extends Params = Params>(
36
+ name: string,
37
+ params?: P,
38
+ path?: string,
39
+ meta?: StateMetaInput<MP>,
40
+ ) => State<P, MP>;
41
+
42
+ /** Get current router state */
43
+ getState: () => State | undefined;
44
+
45
+ /** Compare two states for equality */
46
+ areStatesEqual: (
47
+ state1: State | undefined,
48
+ state2: State | undefined,
49
+ ignoreQueryParams?: boolean,
50
+ ) => boolean;
51
+
52
+ /** Get a dependency by name */
53
+ getDependency: <K extends keyof Dependencies>(name: K) => Dependencies[K];
54
+
55
+ /** Forward state through facade (allows plugin interception) */
56
+ forwardState: <P extends Params = Params>(
57
+ name: string,
58
+ params: P,
59
+ ) => SimpleState<P>;
60
+ }
61
+
62
+ /**
63
+ * Configuration storage for routes.
64
+ * Stores decoders, encoders, default params, and forward mappings.
65
+ */
66
+ export interface RouteConfig {
67
+ /** Custom param decoders per route */
68
+ decoders: Record<string, (params: Params) => Params>;
69
+
70
+ /** Custom param encoders per route */
71
+ encoders: Record<string, (params: Params) => Params>;
72
+
73
+ /** Default params per route */
74
+ defaultParams: Record<string, Params>;
75
+
76
+ /** Forward mappings (source -> target) */
77
+ forwardMap: Record<string, string>;
78
+
79
+ /** Dynamic forward callbacks (source -> callback) */
80
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
81
+ forwardFnMap: Record<string, ForwardToCallback<any>>;
82
+ }