@real-router/core 0.25.4 → 0.26.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 (49) hide show
  1. package/README.md +163 -325
  2. package/dist/cjs/index.d.ts +47 -178
  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 +47 -178
  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 +3 -3
  11. package/src/Router.ts +84 -574
  12. package/src/api/cloneRouter.ts +106 -0
  13. package/src/api/getDependenciesApi.ts +216 -0
  14. package/src/api/getLifecycleApi.ts +67 -0
  15. package/src/api/getPluginApi.ts +118 -0
  16. package/src/api/getRoutesApi.ts +566 -0
  17. package/src/api/index.ts +16 -0
  18. package/src/api/types.ts +7 -0
  19. package/src/getNavigator.ts +5 -2
  20. package/src/index.ts +17 -3
  21. package/src/internals.ts +115 -0
  22. package/src/namespaces/DependenciesNamespace/dependenciesStore.ts +30 -0
  23. package/src/namespaces/DependenciesNamespace/index.ts +3 -1
  24. package/src/namespaces/DependenciesNamespace/validators.ts +2 -4
  25. package/src/namespaces/EventBusNamespace/EventBusNamespace.ts +1 -20
  26. package/src/namespaces/EventBusNamespace/validators.ts +36 -0
  27. package/src/namespaces/NavigationNamespace/NavigationNamespace.ts +1 -10
  28. package/src/namespaces/NavigationNamespace/transition/errorHandling.ts +2 -0
  29. package/src/namespaces/NavigationNamespace/transition/{executeLifecycleHooks.ts → executeLifecycleGuards.ts} +9 -7
  30. package/src/namespaces/NavigationNamespace/transition/index.ts +3 -3
  31. package/src/namespaces/RouteLifecycleNamespace/RouteLifecycleNamespace.ts +1 -16
  32. package/src/namespaces/RoutesNamespace/RoutesNamespace.ts +133 -1089
  33. package/src/namespaces/RoutesNamespace/forwardToValidation.ts +411 -0
  34. package/src/namespaces/RoutesNamespace/helpers.ts +1 -407
  35. package/src/namespaces/RoutesNamespace/index.ts +2 -0
  36. package/src/namespaces/RoutesNamespace/routesStore.ts +388 -0
  37. package/src/namespaces/RoutesNamespace/validators.ts +209 -3
  38. package/src/namespaces/StateNamespace/StateNamespace.ts +1 -44
  39. package/src/namespaces/StateNamespace/validators.ts +46 -0
  40. package/src/namespaces/index.ts +3 -5
  41. package/src/types.ts +12 -138
  42. package/src/wiring/RouterWiringBuilder.ts +30 -36
  43. package/src/wiring/types.ts +3 -6
  44. package/src/wiring/wireRouter.ts +0 -1
  45. package/src/namespaces/CloneNamespace/CloneNamespace.ts +0 -120
  46. package/src/namespaces/CloneNamespace/index.ts +0 -3
  47. package/src/namespaces/CloneNamespace/types.ts +0 -42
  48. package/src/namespaces/DependenciesNamespace/DependenciesNamespace.ts +0 -248
  49. package/src/namespaces/RoutesNamespace/stateBuilder.ts +0 -70
@@ -0,0 +1,388 @@
1
+ // packages/core/src/namespaces/RoutesNamespace/routesStore.ts
2
+
3
+ import { logger } from "@real-router/logger";
4
+ import { createMatcher, createRouteTree, nodeToDefinition } from "route-tree";
5
+
6
+ import { DEFAULT_ROUTE_NAME } from "./constants";
7
+ import { resolveForwardChain } from "./forwardToValidation";
8
+ import { createEmptyConfig, sanitizeRoute } from "./helpers";
9
+ import { validateRoutes } from "./validators";
10
+
11
+ import type { RouteConfig, RoutesDependencies } from "./types";
12
+ import type { GuardFnFactory, Route } from "../../types";
13
+ import type { RouteLifecycleNamespace } from "../RouteLifecycleNamespace";
14
+ import type { DefaultDependencies, Params } from "@real-router/types";
15
+ import type {
16
+ CreateMatcherOptions,
17
+ Matcher,
18
+ RouteDefinition,
19
+ RouteTree,
20
+ } from "route-tree";
21
+
22
+ // =============================================================================
23
+ // Interfaces
24
+ // =============================================================================
25
+
26
+ export interface RoutesStore<
27
+ Dependencies extends DefaultDependencies = DefaultDependencies,
28
+ > {
29
+ readonly definitions: RouteDefinition[];
30
+ readonly config: RouteConfig;
31
+ tree: RouteTree;
32
+ matcher: Matcher;
33
+ resolvedForwardMap: Record<string, string>;
34
+ routeCustomFields: Record<string, Record<string, unknown>>;
35
+ rootPath: string;
36
+ readonly matcherOptions: CreateMatcherOptions | undefined;
37
+ depsStore: RoutesDependencies<Dependencies> | undefined;
38
+ lifecycleNamespace: RouteLifecycleNamespace<Dependencies> | undefined;
39
+ readonly pendingCanActivate: Map<string, GuardFnFactory<Dependencies>>;
40
+ readonly pendingCanDeactivate: Map<string, GuardFnFactory<Dependencies>>;
41
+ readonly treeOperations: {
42
+ readonly commitTreeChanges: (
43
+ store: RoutesStore<Dependencies>,
44
+ noValidate: boolean,
45
+ ) => void;
46
+ readonly resetStore: (store: RoutesStore<Dependencies>) => void;
47
+ readonly nodeToDefinition: (node: RouteTree) => RouteDefinition;
48
+ readonly validateRoutes: (
49
+ routes: Route<Dependencies>[],
50
+ tree?: RouteTree,
51
+ forwardMap?: Record<string, string>,
52
+ parentName?: string,
53
+ ) => void;
54
+ };
55
+ }
56
+
57
+ // =============================================================================
58
+ // Tree operations
59
+ // =============================================================================
60
+
61
+ function rebuildTree(
62
+ definitions: RouteDefinition[],
63
+ rootPath: string,
64
+ matcherOptions: CreateMatcherOptions | undefined,
65
+ ): { tree: RouteTree; matcher: Matcher } {
66
+ const tree = createRouteTree(DEFAULT_ROUTE_NAME, rootPath, definitions);
67
+ const matcher = createMatcher(matcherOptions);
68
+
69
+ matcher.registerTree(tree);
70
+
71
+ return { tree, matcher };
72
+ }
73
+
74
+ export function commitTreeChanges<
75
+ Dependencies extends DefaultDependencies = DefaultDependencies,
76
+ >(store: RoutesStore<Dependencies>, noValidate: boolean): void {
77
+ const result = rebuildTree(
78
+ store.definitions,
79
+ store.rootPath,
80
+ store.matcherOptions,
81
+ );
82
+
83
+ store.tree = result.tree;
84
+ store.matcher = result.matcher;
85
+ store.resolvedForwardMap = refreshForwardMap(store.config, noValidate);
86
+ }
87
+
88
+ export function rebuildTreeInPlace<
89
+ Dependencies extends DefaultDependencies = DefaultDependencies,
90
+ >(store: RoutesStore<Dependencies>): void {
91
+ const result = rebuildTree(
92
+ store.definitions,
93
+ store.rootPath,
94
+ store.matcherOptions,
95
+ );
96
+
97
+ store.tree = result.tree;
98
+ store.matcher = result.matcher;
99
+ }
100
+
101
+ // =============================================================================
102
+ // Store reset
103
+ // =============================================================================
104
+
105
+ /**
106
+ * Clears all routes and resets config.
107
+ * Does NOT clear lifecycle handlers or state — caller handles that.
108
+ */
109
+ export function resetStore<
110
+ Dependencies extends DefaultDependencies = DefaultDependencies,
111
+ >(store: RoutesStore<Dependencies>): void {
112
+ store.definitions.length = 0;
113
+
114
+ Object.assign(store.config, createEmptyConfig());
115
+
116
+ store.resolvedForwardMap = Object.create(null) as Record<string, string>;
117
+ store.routeCustomFields = Object.create(null) as Record<
118
+ string,
119
+ Record<string, unknown>
120
+ >;
121
+
122
+ rebuildTreeInPlace(store);
123
+ }
124
+
125
+ // =============================================================================
126
+ // Forward map
127
+ // =============================================================================
128
+
129
+ export function refreshForwardMap(
130
+ config: RouteConfig,
131
+ noValidate: boolean,
132
+ ): Record<string, string> {
133
+ if (noValidate) {
134
+ return cacheForwardMap(config);
135
+ }
136
+
137
+ return validateAndCacheForwardMap(config);
138
+ }
139
+
140
+ function validateAndCacheForwardMap(
141
+ config: RouteConfig,
142
+ ): Record<string, string> {
143
+ const map = Object.create(null) as Record<string, string>;
144
+
145
+ for (const fromRoute of Object.keys(config.forwardMap)) {
146
+ map[fromRoute] = resolveForwardChain(fromRoute, config.forwardMap);
147
+ }
148
+
149
+ return map;
150
+ }
151
+
152
+ function cacheForwardMap(config: RouteConfig): Record<string, string> {
153
+ const map = Object.create(null) as Record<string, string>;
154
+
155
+ for (const fromRoute of Object.keys(config.forwardMap)) {
156
+ let current = fromRoute;
157
+
158
+ while (config.forwardMap[current]) {
159
+ current = config.forwardMap[current];
160
+ }
161
+
162
+ map[fromRoute] = current;
163
+ }
164
+
165
+ return map;
166
+ }
167
+
168
+ // =============================================================================
169
+ // Route handler registration
170
+ // =============================================================================
171
+
172
+ function registerForwardTo<Dependencies extends DefaultDependencies>(
173
+ route: Route<Dependencies>,
174
+ fullName: string,
175
+ config: RouteConfig,
176
+ ): void {
177
+ if (route.canActivate) {
178
+ /* v8 ignore next -- @preserve: edge case, both string and function tested separately */
179
+ const forwardTarget =
180
+ typeof route.forwardTo === "string" ? route.forwardTo : "[dynamic]";
181
+
182
+ logger.warn(
183
+ "real-router",
184
+ `Route "${fullName}" has both forwardTo and canActivate. ` +
185
+ `canActivate will be ignored because forwardTo creates a redirect (industry standard). ` +
186
+ `Move canActivate to the target route "${forwardTarget}".`,
187
+ );
188
+ }
189
+
190
+ if (route.canDeactivate) {
191
+ /* v8 ignore next -- @preserve: edge case, both string and function tested separately */
192
+ const forwardTarget =
193
+ typeof route.forwardTo === "string" ? route.forwardTo : "[dynamic]";
194
+
195
+ logger.warn(
196
+ "real-router",
197
+ `Route "${fullName}" has both forwardTo and canDeactivate. ` +
198
+ `canDeactivate will be ignored because forwardTo creates a redirect (industry standard). ` +
199
+ `Move canDeactivate to the target route "${forwardTarget}".`,
200
+ );
201
+ }
202
+
203
+ // Async validation ALWAYS runs (even with noValidate=true)
204
+ if (typeof route.forwardTo === "function") {
205
+ const isNativeAsync =
206
+ (route.forwardTo as { constructor: { name: string } }).constructor
207
+ .name === "AsyncFunction";
208
+ const isTranspiledAsync = route.forwardTo.toString().includes("__awaiter");
209
+
210
+ if (isNativeAsync || isTranspiledAsync) {
211
+ throw new TypeError(
212
+ `forwardTo callback cannot be async for route "${fullName}". ` +
213
+ `Async functions break matchPath/buildPath.`,
214
+ );
215
+ }
216
+ }
217
+
218
+ // forwardTo is guaranteed to exist at this point
219
+ if (typeof route.forwardTo === "string") {
220
+ config.forwardMap[fullName] = route.forwardTo;
221
+ } else {
222
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
223
+ config.forwardFnMap[fullName] = route.forwardTo!;
224
+ }
225
+ }
226
+
227
+ function registerSingleRouteHandlers<Dependencies extends DefaultDependencies>(
228
+ route: Route<Dependencies>,
229
+ fullName: string,
230
+ config: RouteConfig,
231
+ routeCustomFields: Record<string, Record<string, unknown>>,
232
+ pendingCanActivate: Map<string, GuardFnFactory<Dependencies>>,
233
+ pendingCanDeactivate: Map<string, GuardFnFactory<Dependencies>>,
234
+ depsStore: RoutesDependencies<Dependencies> | undefined,
235
+ ): void {
236
+ const standardKeys = new Set([
237
+ "name",
238
+ "path",
239
+ "children",
240
+ "canActivate",
241
+ "canDeactivate",
242
+ "forwardTo",
243
+ "encodeParams",
244
+ "decodeParams",
245
+ "defaultParams",
246
+ ]);
247
+ const customFields = Object.fromEntries(
248
+ Object.entries(route).filter(([k]) => !standardKeys.has(k)),
249
+ );
250
+
251
+ if (Object.keys(customFields).length > 0) {
252
+ routeCustomFields[fullName] = customFields;
253
+ }
254
+
255
+ if (route.canActivate) {
256
+ if (depsStore) {
257
+ depsStore.addActivateGuard(fullName, route.canActivate);
258
+ } else {
259
+ pendingCanActivate.set(fullName, route.canActivate);
260
+ }
261
+ }
262
+
263
+ if (route.canDeactivate) {
264
+ if (depsStore) {
265
+ depsStore.addDeactivateGuard(fullName, route.canDeactivate);
266
+ } else {
267
+ pendingCanDeactivate.set(fullName, route.canDeactivate);
268
+ }
269
+ }
270
+
271
+ if (route.forwardTo) {
272
+ registerForwardTo(route, fullName, config);
273
+ }
274
+
275
+ if (route.decodeParams) {
276
+ config.decoders[fullName] = (params: Params): Params =>
277
+ route.decodeParams?.(params) ?? params;
278
+ }
279
+
280
+ if (route.encodeParams) {
281
+ config.encoders[fullName] = (params: Params): Params =>
282
+ route.encodeParams?.(params) ?? params;
283
+ }
284
+
285
+ if (route.defaultParams) {
286
+ config.defaultParams[fullName] = route.defaultParams;
287
+ }
288
+ }
289
+
290
+ export function registerAllRouteHandlers<
291
+ Dependencies extends DefaultDependencies,
292
+ >(
293
+ routes: readonly Route<Dependencies>[],
294
+ config: RouteConfig,
295
+ routeCustomFields: Record<string, Record<string, unknown>>,
296
+ pendingCanActivate: Map<string, GuardFnFactory<Dependencies>>,
297
+ pendingCanDeactivate: Map<string, GuardFnFactory<Dependencies>>,
298
+ depsStore: RoutesDependencies<Dependencies> | undefined,
299
+ parentName = "",
300
+ ): void {
301
+ for (const route of routes) {
302
+ const fullName = parentName ? `${parentName}.${route.name}` : route.name;
303
+
304
+ registerSingleRouteHandlers(
305
+ route,
306
+ fullName,
307
+ config,
308
+ routeCustomFields,
309
+ pendingCanActivate,
310
+ pendingCanDeactivate,
311
+ depsStore,
312
+ );
313
+
314
+ if (route.children) {
315
+ registerAllRouteHandlers(
316
+ route.children,
317
+ config,
318
+ routeCustomFields,
319
+ pendingCanActivate,
320
+ pendingCanDeactivate,
321
+ depsStore,
322
+ fullName,
323
+ );
324
+ }
325
+ }
326
+ }
327
+
328
+ // =============================================================================
329
+ // Factory
330
+ // =============================================================================
331
+
332
+ export function createRoutesStore<
333
+ Dependencies extends DefaultDependencies = DefaultDependencies,
334
+ >(
335
+ routes: Route<Dependencies>[],
336
+ noValidate: boolean,
337
+ matcherOptions?: CreateMatcherOptions,
338
+ ): RoutesStore<Dependencies> {
339
+ const definitions: RouteDefinition[] = [];
340
+ const config: RouteConfig = createEmptyConfig();
341
+ const routeCustomFields: Record<
342
+ string,
343
+ Record<string, unknown>
344
+ > = Object.create(null) as Record<string, Record<string, unknown>>;
345
+ const pendingCanActivate = new Map<string, GuardFnFactory<Dependencies>>();
346
+ const pendingCanDeactivate = new Map<string, GuardFnFactory<Dependencies>>();
347
+
348
+ for (const route of routes) {
349
+ definitions.push(sanitizeRoute(route));
350
+ }
351
+
352
+ const { tree, matcher } = rebuildTree(definitions, "", matcherOptions);
353
+
354
+ registerAllRouteHandlers(
355
+ routes,
356
+ config,
357
+ routeCustomFields,
358
+ pendingCanActivate,
359
+ pendingCanDeactivate,
360
+ undefined,
361
+ "",
362
+ );
363
+
364
+ const resolvedForwardMap: Record<string, string> = noValidate
365
+ ? cacheForwardMap(config)
366
+ : validateAndCacheForwardMap(config);
367
+
368
+ return {
369
+ definitions,
370
+ config,
371
+ tree,
372
+ matcher,
373
+ resolvedForwardMap,
374
+ routeCustomFields,
375
+ rootPath: "",
376
+ matcherOptions,
377
+ depsStore: undefined,
378
+ lifecycleNamespace: undefined,
379
+ pendingCanActivate,
380
+ pendingCanDeactivate,
381
+ treeOperations: {
382
+ commitTreeChanges,
383
+ resetStore,
384
+ nodeToDefinition,
385
+ validateRoutes,
386
+ },
387
+ };
388
+ }
@@ -7,6 +7,7 @@
7
7
  * Extracted from RoutesNamespace class for better separation of concerns.
8
8
  */
9
9
 
10
+ import { logger } from "@real-router/logger";
10
11
  import { validateRoute } from "route-tree";
11
12
  import {
12
13
  isString,
@@ -15,11 +16,19 @@ import {
15
16
  getTypeDescription,
16
17
  } from "type-guards";
17
18
 
18
- import { validateRouteProperties, validateForwardToTargets } from "./helpers";
19
+ import {
20
+ resolveForwardChain,
21
+ validateForwardToTargets,
22
+ validateRouteProperties,
23
+ } from "./forwardToValidation";
19
24
 
25
+ import type { RouteConfig } from "./types";
20
26
  import type { Route, RouteConfigUpdate } from "../../types";
21
- import type { DefaultDependencies } from "@real-router/types";
22
- import type { RouteTree } from "route-tree";
27
+ import type {
28
+ DefaultDependencies,
29
+ ForwardToCallback,
30
+ } from "@real-router/types";
31
+ import type { Matcher, RouteTree } from "route-tree";
23
32
 
24
33
  /**
25
34
  * Validates removeRoute arguments.
@@ -329,3 +338,200 @@ export function validateRoutes<Dependencies extends DefaultDependencies>(
329
338
  validateForwardToTargets(routes, forwardMap, tree);
330
339
  }
331
340
  }
341
+
342
+ // ============================================================================
343
+ // Instance-level validators (moved from routesCrud.ts)
344
+ // ============================================================================
345
+
346
+ /**
347
+ * Collects URL params from segments into a Set.
348
+ */
349
+ function collectUrlParams(segments: readonly RouteTree[]): Set<string> {
350
+ const params = new Set<string>();
351
+
352
+ for (const segment of segments) {
353
+ for (const param of segment.paramMeta.urlParams) {
354
+ params.add(param);
355
+ }
356
+ }
357
+
358
+ return params;
359
+ }
360
+
361
+ /**
362
+ * Validates removeRoute constraints.
363
+ * Returns false if removal should be blocked (route is active).
364
+ * Logs warnings for edge cases.
365
+ *
366
+ * @param name - Route name to remove
367
+ * @param currentStateName - Current active route name (or undefined)
368
+ * @param isNavigating - Whether navigation is in progress
369
+ * @returns true if removal can proceed, false if blocked
370
+ */
371
+ export function validateRemoveRoute(
372
+ name: string,
373
+ currentStateName: string | undefined,
374
+ isNavigating: boolean,
375
+ ): boolean {
376
+ // Check if trying to remove currently active route (or its parent)
377
+ if (currentStateName) {
378
+ const isExactMatch = currentStateName === name;
379
+ const isParentOfCurrent = currentStateName.startsWith(`${name}.`);
380
+
381
+ if (isExactMatch || isParentOfCurrent) {
382
+ const suffix = isExactMatch ? "" : ` (current: "${currentStateName}")`;
383
+
384
+ logger.warn(
385
+ "router.removeRoute",
386
+ `Cannot remove route "${name}" — it is currently active${suffix}. Navigate away first.`,
387
+ );
388
+
389
+ return false;
390
+ }
391
+ }
392
+
393
+ // Warn if navigation is in progress (but allow removal)
394
+ if (isNavigating) {
395
+ logger.warn(
396
+ "router.removeRoute",
397
+ `Route "${name}" removed while navigation is in progress. This may cause unexpected behavior.`,
398
+ );
399
+ }
400
+
401
+ return true;
402
+ }
403
+
404
+ /**
405
+ * Validates clearRoutes operation.
406
+ * Returns false if operation should be blocked (navigation in progress).
407
+ *
408
+ * @param isNavigating - Whether navigation is in progress
409
+ * @returns true if clearRoutes can proceed, false if blocked
410
+ */
411
+ export function validateClearRoutes(isNavigating: boolean): boolean {
412
+ if (isNavigating) {
413
+ logger.error(
414
+ "router.clearRoutes",
415
+ "Cannot clear routes while navigation is in progress. Wait for navigation to complete.",
416
+ );
417
+
418
+ return false;
419
+ }
420
+
421
+ return true;
422
+ }
423
+
424
+ /**
425
+ * Validates that forwardTo target doesn't require params that source doesn't have.
426
+ *
427
+ * @param sourceName - Source route name
428
+ * @param targetName - Target route name
429
+ * @param matcher - Current route matcher
430
+ */
431
+ export function validateForwardToParamCompatibility(
432
+ sourceName: string,
433
+ targetName: string,
434
+ matcher: Matcher,
435
+ ): void {
436
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
437
+ const sourceSegments = matcher.getSegmentsByName(
438
+ sourceName,
439
+ )! as readonly RouteTree[];
440
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
441
+ const targetSegments = matcher.getSegmentsByName(
442
+ targetName,
443
+ )! as readonly RouteTree[];
444
+
445
+ // Get source URL params as a Set for O(1) lookup
446
+ const sourceParams = collectUrlParams(sourceSegments);
447
+
448
+ // Build target URL params array (inline — no separate helper needed)
449
+ const targetParams: string[] = [];
450
+
451
+ for (const segment of targetSegments) {
452
+ for (const param of segment.paramMeta.urlParams) {
453
+ targetParams.push(param);
454
+ }
455
+ }
456
+
457
+ // Check if target requires params that source doesn't have
458
+ const missingParams = targetParams.filter(
459
+ (param) => !sourceParams.has(param),
460
+ );
461
+
462
+ if (missingParams.length > 0) {
463
+ throw new Error(
464
+ `[real-router] forwardTo target "${targetName}" requires params ` +
465
+ `[${missingParams.join(", ")}] that are not available in source route "${sourceName}"`,
466
+ );
467
+ }
468
+ }
469
+
470
+ /**
471
+ * Validates that adding forwardTo doesn't create a cycle.
472
+ * Creates a test map with the new entry and uses resolveForwardChain
473
+ * to detect cycles before any mutation happens.
474
+ *
475
+ * @param sourceName - Source route name
476
+ * @param targetName - Target route name
477
+ * @param config - Current route config (forwardMap read-only in this call)
478
+ */
479
+ export function validateForwardToCycle(
480
+ sourceName: string,
481
+ targetName: string,
482
+ config: RouteConfig,
483
+ ): void {
484
+ // Create a test map with the new entry to validate BEFORE mutation
485
+ const testMap = {
486
+ ...config.forwardMap,
487
+ [sourceName]: targetName,
488
+ };
489
+
490
+ // resolveForwardChain will throw if cycle is detected or max depth exceeded
491
+ resolveForwardChain(sourceName, testMap);
492
+ }
493
+
494
+ /**
495
+ * Validates updateRoute instance-level constraints (route existence, forwardTo).
496
+ *
497
+ * @param name - Route name (already validated by static method)
498
+ * @param forwardTo - Cached forwardTo value
499
+ * @param hasRoute - Function to check route existence
500
+ * @param matcher - Current route matcher
501
+ * @param config - Current route config
502
+ */
503
+ export function validateUpdateRoute<
504
+ Dependencies extends DefaultDependencies = DefaultDependencies,
505
+ >(
506
+ name: string,
507
+ forwardTo: string | ForwardToCallback<Dependencies> | null | undefined,
508
+ hasRoute: (n: string) => boolean,
509
+ matcher: Matcher,
510
+ config: RouteConfig,
511
+ ): void {
512
+ // Validate route exists
513
+ if (!hasRoute(name)) {
514
+ throw new ReferenceError(
515
+ `[real-router] updateRoute: route "${name}" does not exist`,
516
+ );
517
+ }
518
+
519
+ // Validate forwardTo target exists and is valid (only for string forwardTo)
520
+ if (
521
+ forwardTo !== undefined &&
522
+ forwardTo !== null &&
523
+ typeof forwardTo === "string"
524
+ ) {
525
+ if (!hasRoute(forwardTo)) {
526
+ throw new Error(
527
+ `[real-router] updateRoute: forwardTo target "${forwardTo}" does not exist`,
528
+ );
529
+ }
530
+
531
+ // Check forwardTo param compatibility
532
+ validateForwardToParamCompatibility(name, forwardTo, matcher);
533
+
534
+ // Check for cycle detection
535
+ validateForwardToCycle(name, forwardTo, config);
536
+ }
537
+ }
@@ -1,11 +1,6 @@
1
1
  // packages/core/src/namespaces/StateNamespace/StateNamespace.ts
2
2
 
3
- import {
4
- getTypeDescription,
5
- isParams,
6
- isString,
7
- validateState,
8
- } from "type-guards";
3
+ import { getTypeDescription, validateState } from "type-guards";
9
4
 
10
5
  import { areParamValuesEqual, getUrlParamsFromMeta } from "./helpers";
11
6
  import { constants } from "../../constants";
@@ -56,44 +51,6 @@ export class StateNamespace {
56
51
  // Static validation methods (called by facade before instance methods)
57
52
  // =========================================================================
58
53
 
59
- /**
60
- * Validates makeState arguments.
61
- */
62
- static validateMakeStateArgs(
63
- name: unknown,
64
- params: unknown,
65
- path: unknown,
66
- forceId: unknown,
67
- ): void {
68
- // Validate name is a string
69
- if (!isString(name)) {
70
- throw new TypeError(
71
- `[router.makeState] Invalid name: ${getTypeDescription(name)}. Expected string.`,
72
- );
73
- }
74
-
75
- // Validate params if provided
76
- if (params !== undefined && !isParams(params)) {
77
- throw new TypeError(
78
- `[router.makeState] Invalid params: ${getTypeDescription(params)}. Expected plain object.`,
79
- );
80
- }
81
-
82
- // Validate path if provided
83
- if (path !== undefined && !isString(path)) {
84
- throw new TypeError(
85
- `[router.makeState] Invalid path: ${getTypeDescription(path)}. Expected string.`,
86
- );
87
- }
88
-
89
- // Validate forceId if provided
90
- if (forceId !== undefined && typeof forceId !== "number") {
91
- throw new TypeError(
92
- `[router.makeState] Invalid forceId: ${getTypeDescription(forceId)}. Expected number.`,
93
- );
94
- }
95
- }
96
-
97
54
  /**
98
55
  * Validates areStatesEqual arguments.
99
56
  */