@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
@@ -1,8 +1,5 @@
1
1
  // packages/core/src/namespaces/RoutesNamespace/helpers.ts
2
2
 
3
- import { getSegmentsByName } from "route-tree";
4
- import { getTypeDescription } from "type-guards";
5
-
6
3
  import type { RouteConfig } from "./types";
7
4
  import type { Route } from "../../types";
8
5
  import type {
@@ -10,7 +7,7 @@ import type {
10
7
  ForwardToCallback,
11
8
  Params,
12
9
  } from "@real-router/types";
13
- import type { RouteDefinition, RouteTree } from "route-tree";
10
+ import type { RouteDefinition } from "route-tree";
14
11
 
15
12
  /**
16
13
  * Creates an empty RouteConfig.
@@ -127,406 +124,3 @@ export function clearConfigEntries<T>(
127
124
  }
128
125
  }
129
126
  }
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
- }
@@ -7,3 +7,5 @@ export { DEFAULT_ROUTE_NAME, validatedRouteNames } from "./constants";
7
7
  export { createEmptyConfig } from "./helpers";
8
8
 
9
9
  export type { RouteConfig, RoutesDependencies } from "./types";
10
+
11
+ export type { RoutesStore } from "./routesStore";