@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,42 @@
1
+ // packages/core/src/namespaces/index.ts
2
+
3
+ export { DependenciesNamespace } from "./DependenciesNamespace";
4
+
5
+ export {
6
+ deepFreeze,
7
+ defaultOptions,
8
+ OptionsNamespace,
9
+ VALID_OPTION_VALUES,
10
+ VALID_QUERY_PARAMS,
11
+ } from "./OptionsNamespace";
12
+
13
+ export { StateNamespace } from "./StateNamespace";
14
+
15
+ export { MiddlewareNamespace } from "./MiddlewareNamespace";
16
+
17
+ export {
18
+ PluginsNamespace,
19
+ EVENTS_MAP,
20
+ EVENT_METHOD_NAMES,
21
+ } from "./PluginsNamespace";
22
+
23
+ export { RouteLifecycleNamespace } from "./RouteLifecycleNamespace";
24
+
25
+ export {
26
+ RoutesNamespace,
27
+ DEFAULT_ROUTE_NAME,
28
+ validatedRouteNames,
29
+ createEmptyConfig,
30
+ } from "./RoutesNamespace";
31
+
32
+ export type { RouteConfig } from "./RoutesNamespace";
33
+
34
+ export { NavigationNamespace } from "./NavigationNamespace";
35
+
36
+ export { RouterLifecycleNamespace } from "./RouterLifecycleNamespace";
37
+
38
+ export { CloneNamespace } from "./CloneNamespace";
39
+
40
+ export type { ApplyConfigFn, CloneData, RouterFactory } from "./CloneNamespace";
41
+
42
+ export { EventBusNamespace } from "./EventBusNamespace";
@@ -0,0 +1,441 @@
1
+ // packages/real-router/modules/transitionPath.ts
2
+
3
+ import type { Params, State } from "@real-router/types";
4
+
5
+ /**
6
+ * Parameters extracted from a route segment.
7
+ * Maps parameter names to their string values.
8
+ */
9
+ type SegmentParams = Record<string, string>;
10
+
11
+ /**
12
+ * Represents a transition path between two router states.
13
+ * Contains information about which route segments need to be activated/deactivated.
14
+ */
15
+ interface TransitionPath {
16
+ /** The common ancestor route segment where paths diverge */
17
+ intersection: string;
18
+ /** Route segments that need to be deactivated (in reverse order) */
19
+ toDeactivate: string[];
20
+ /** Route segments that need to be activated (in order) */
21
+ toActivate: string[];
22
+ }
23
+
24
+ // Constants for better maintainability
25
+ const ROUTE_SEGMENT_SEPARATOR = ".";
26
+ const EMPTY_INTERSECTION = "";
27
+ const DEFAULT_ROUTE_NAME = "";
28
+
29
+ /**
30
+ * Builds a reversed copy of a string array.
31
+ * Optimization: single pass instead of creating intermediate array with .toReversed().
32
+ *
33
+ * @param arr - Source array
34
+ * @returns New array with elements in reverse order
35
+ * @internal
36
+ */
37
+ function reverseArray(arr: string[]): string[] {
38
+ const len = arr.length;
39
+ const result: string[] = [];
40
+
41
+ for (let i = len - 1; i >= 0; i--) {
42
+ result.push(arr[i]);
43
+ }
44
+
45
+ return result;
46
+ }
47
+
48
+ /**
49
+ * Handles conversion of route names with many segments (5+).
50
+ * Internal helper for nameToIDs function.
51
+ *
52
+ * Uses optimized hybrid approach: split to get segments, then slice original
53
+ * string to build cumulative paths. This approach is 65-81% faster than
54
+ * string concatenation for typical cases (5-10 segments).
55
+ *
56
+ * @param name - Route name with 5 or more segments
57
+ * @returns Array of cumulative segment IDs
58
+ * @throws {Error} If route depth exceeds maximum allowed
59
+ * @internal
60
+ */
61
+ function nameToIDsGeneral(name: string): string[] {
62
+ // We know there are at least 5 segments at this point (after fast paths)
63
+ const segments = name.split(ROUTE_SEGMENT_SEPARATOR);
64
+ const segmentCount = segments.length;
65
+
66
+ // First segment is always just itself
67
+ const ids: string[] = [segments[0]];
68
+
69
+ // Calculate cumulative lengths and slice from original string
70
+ // This avoids repeated string concatenation (O(k²) → O(k))
71
+ let cumulativeLen = segments[0].length;
72
+
73
+ for (let i = 1; i < segmentCount - 1; i++) {
74
+ cumulativeLen += 1 + segments[i].length; // +1 for dot separator
75
+ ids.push(name.slice(0, cumulativeLen));
76
+ }
77
+
78
+ // Last segment is always the full route name
79
+ ids.push(name);
80
+
81
+ return ids;
82
+ }
83
+
84
+ /**
85
+ * Extracts segment-specific parameters from a state object.
86
+ * Only includes parameters that are valid for serialization (primitives).
87
+ *
88
+ * @param name - Segment name to extract parameters for
89
+ * @param state - State containing the parameters
90
+ * @returns Object with extracted segment parameters
91
+ */
92
+ function extractSegmentParams(name: string, state: State): SegmentParams {
93
+ const keys = state.meta?.params[name];
94
+
95
+ // No parameters defined for this segment
96
+ if (!keys || typeof keys !== "object") {
97
+ return {};
98
+ }
99
+
100
+ const result: SegmentParams = {};
101
+
102
+ for (const key in keys as Params) {
103
+ // Skip inherited properties
104
+ if (!Object.hasOwn(keys, key)) {
105
+ continue;
106
+ }
107
+
108
+ // Skip undefined values for consistent behavior (treat { key: undefined } same as missing key)
109
+ // Edge case: can appear from manual State creation or object merging
110
+ // @ts-expect-error Params type doesn't allow undefined, but it can appear at runtime
111
+ if (keys[key] === undefined) {
112
+ continue;
113
+ }
114
+
115
+ const value = state.params[key];
116
+
117
+ // Skip null/undefined values
118
+ if (value == null) {
119
+ continue;
120
+ }
121
+
122
+ // Only include primitives in segment params comparison.
123
+ // Complex types (arrays, nested objects) are handled by query param serialization.
124
+ // Note: symbol/function/bigint are rejected by isParams validation before reaching this code.
125
+ if (
126
+ typeof value === "string" ||
127
+ typeof value === "number" ||
128
+ typeof value === "boolean"
129
+ ) {
130
+ result[key] = String(value);
131
+ }
132
+ // Complex types silently skipped - they're serialized as query params elsewhere
133
+ }
134
+
135
+ return result;
136
+ }
137
+
138
+ /**
139
+ * Finds the point where two state paths diverge based on segments and parameters.
140
+ * Compares both segment names and their parameters to find the first difference.
141
+ *
142
+ * @param toState - Target state
143
+ * @param fromState - Source state
144
+ * @param toStateIds - Segment IDs for target state
145
+ * @param fromStateIds - Segment IDs for source state
146
+ * @param maxI - Maximum index to check (minimum of both arrays)
147
+ * @returns Index of first difference, or maxI if all checked segments match
148
+ */
149
+ function pointOfDifference(
150
+ toState: State,
151
+ fromState: State,
152
+ toStateIds: string[],
153
+ fromStateIds: string[],
154
+ maxI: number,
155
+ ): number {
156
+ for (let i = 0; i < maxI; i++) {
157
+ const toSegment = toStateIds[i];
158
+ const fromSegment = fromStateIds[i];
159
+
160
+ // Different segment names - immediate difference
161
+ if (toSegment !== fromSegment) {
162
+ return i;
163
+ }
164
+
165
+ // Same segment name - check parameters
166
+ const toParams = extractSegmentParams(toSegment, toState);
167
+ const fromParams = extractSegmentParams(fromSegment, fromState);
168
+
169
+ // Fast check: different number of parameters
170
+ const toKeys = Object.keys(toParams);
171
+ const fromKeys = Object.keys(fromParams);
172
+
173
+ if (toKeys.length !== fromKeys.length) {
174
+ return i;
175
+ }
176
+
177
+ // Detailed check: compare parameter values
178
+ for (const key of toKeys) {
179
+ if (toParams[key] !== fromParams[key]) {
180
+ return i;
181
+ }
182
+ }
183
+ }
184
+
185
+ return maxI;
186
+ }
187
+
188
+ /**
189
+ * Converts a route name to an array of hierarchical segment identifiers.
190
+ * Each segment ID includes all parent segments in the path.
191
+ *
192
+ * @param name - Route name in dot notation (e.g., 'users.profile.edit')
193
+ * @returns Array of cumulative segment IDs
194
+ * @throws {Error} If route depth exceeds maximum allowed depth
195
+ *
196
+ * @example
197
+ * // Simple route
198
+ * nameToIDs('users');
199
+ * // Returns: ['users']
200
+ *
201
+ * @example
202
+ * // Nested route
203
+ * nameToIDs('users.profile.edit');
204
+ * // Returns: ['users', 'users.profile', 'users.profile.edit']
205
+ *
206
+ * @example
207
+ * // Empty string (root route)
208
+ * nameToIDs('');
209
+ * // Returns: ['']
210
+ *
211
+ * @remarks
212
+ * Input parameter is NOT validated in this function for performance reasons.
213
+ * Validation significantly slows down nameToIDs execution.
214
+ * The input should be validated by the function/method that calls nameToIDs.
215
+ */
216
+ export function nameToIDs(name: string): string[] {
217
+ // ===== FAST PATH 1: Empty string (root route) =====
218
+ // Most common in initial navigation
219
+ if (!name) {
220
+ return [DEFAULT_ROUTE_NAME];
221
+ }
222
+
223
+ // ===== FAST PATH 2: Single segment (no dots) =====
224
+ // Very common: 'home', 'users', 'settings', etc.
225
+ const firstDot = name.indexOf(ROUTE_SEGMENT_SEPARATOR);
226
+
227
+ if (firstDot === -1) {
228
+ return [name];
229
+ }
230
+
231
+ // ===== FAST PATH 3: Two segments =====
232
+ // Common: 'users.list', 'admin.dashboard', etc.
233
+ const secondDot = name.indexOf(ROUTE_SEGMENT_SEPARATOR, firstDot + 1);
234
+
235
+ if (secondDot === -1) {
236
+ return [
237
+ name.slice(0, firstDot), // 'users'
238
+ name, // 'users.list'
239
+ ];
240
+ }
241
+
242
+ // ===== FAST PATH 4: Three segments =====
243
+ // Common: 'users.profile.edit', 'app.settings.general', etc.
244
+ const thirdDot = name.indexOf(ROUTE_SEGMENT_SEPARATOR, secondDot + 1);
245
+
246
+ if (thirdDot === -1) {
247
+ return [
248
+ name.slice(0, firstDot), // 'users'
249
+ name.slice(0, secondDot), // 'users.profile'
250
+ name, // 'users.profile.edit'
251
+ ];
252
+ }
253
+
254
+ // ===== FAST PATH 5: Four segments =====
255
+ const fourthDot = name.indexOf(ROUTE_SEGMENT_SEPARATOR, thirdDot + 1);
256
+
257
+ if (fourthDot === -1) {
258
+ return [
259
+ name.slice(0, firstDot),
260
+ name.slice(0, secondDot),
261
+ name.slice(0, thirdDot),
262
+ name,
263
+ ];
264
+ }
265
+
266
+ // ===== STANDARD PATH: 5+ segments =====
267
+ // Less common, use general algorithm with optimizations
268
+ return nameToIDsGeneral(name);
269
+ }
270
+
271
+ /**
272
+ * Calculates the transition path between two router states.
273
+ * Determines which route segments need to be deactivated and activated
274
+ * to transition from one state to another.
275
+ *
276
+ * @param toState - Target state to transition to
277
+ * @param fromState - Current state to transition from (optional)
278
+ * @returns Transition path with intersection and segments to activate/deactivate
279
+ *
280
+ * @throws {TypeError} When toState is null or undefined
281
+ * @throws {TypeError} When toState is not an object
282
+ * @throws {TypeError} When toState.name is missing or not a string
283
+ * @throws {TypeError} When toState.params is missing or not an object
284
+ * @throws {TypeError} When toState.path is missing or not a string
285
+ * @throws {TypeError} When toState.name contains invalid route format:
286
+ * - Contains only whitespace (e.g., " ")
287
+ * - Has consecutive dots (e.g., "users..profile")
288
+ * - Has leading/trailing dots (e.g., ".users" or "users.")
289
+ * - Segments don't match pattern [a-zA-Z_][a-zA-Z0-9_-]* (e.g., "users.123")
290
+ * - Contains spaces or special characters (e.g., "users profile")
291
+ * - Exceeds maximum length (8192 characters)
292
+ * @throws {TypeError} When fromState is provided and has any of the validation errors listed above for toState
293
+ *
294
+ * @example
295
+ * // ✅ Valid calls
296
+ * getTransitionPath({ name: 'users.profile', params: {}, path: '/users/profile' });
297
+ * getTransitionPath(toState, fromState);
298
+ * getTransitionPath({ name: '', params: {}, path: '/' }); // root route
299
+ *
300
+ * @example
301
+ * // ❌ Invalid calls that throw TypeError
302
+ * getTransitionPath(null); // toState is null
303
+ * getTransitionPath(undefined); // toState is undefined
304
+ * getTransitionPath({}); // missing required fields
305
+ * getTransitionPath({ name: 123, params: {}, path: '/' }); // name not a string
306
+ * getTransitionPath({ name: 'home', path: '/' }); // missing params
307
+ * getTransitionPath({ name: 'users..profile', params: {}, path: '/' }); // consecutive dots
308
+ * getTransitionPath({ name: '.users', params: {}, path: '/' }); // leading dot
309
+ * getTransitionPath({ name: 'users.', params: {}, path: '/' }); // trailing dot
310
+ * getTransitionPath({ name: 'users profile', params: {}, path: '/' }); // contains space
311
+ * getTransitionPath({ name: 'users.123', params: {}, path: '/' }); // segment starts with number
312
+ * getTransitionPath(validToState, { name: 'invalid..route', params: {}, path: '/' }); // fromState invalid
313
+ *
314
+ * @example
315
+ * // Full activation (no fromState)
316
+ * getTransitionPath(makeState('users.profile'));
317
+ * // Returns: {
318
+ * // intersection: '',
319
+ * // toActivate: ['users', 'users.profile'],
320
+ * // toDeactivate: []
321
+ * // }
322
+ *
323
+ * @example
324
+ * // Partial transition with common ancestor
325
+ * getTransitionPath(
326
+ * makeState('users.profile'),
327
+ * makeState('users.list')
328
+ * );
329
+ * // Returns: {
330
+ * // intersection: 'users',
331
+ * // toActivate: ['users.profile'],
332
+ * // toDeactivate: ['users.list']
333
+ * // }
334
+ *
335
+ * @example
336
+ * // Complete route change
337
+ * getTransitionPath(
338
+ * makeState('admin.dashboard'),
339
+ * makeState('users.profile')
340
+ * );
341
+ * // Returns: {
342
+ * // intersection: '',
343
+ * // toActivate: ['admin', 'admin.dashboard'],
344
+ * // toDeactivate: ['users.profile', 'users']
345
+ * // }
346
+ */
347
+ export function getTransitionPath(
348
+ toState: State,
349
+ fromState?: State,
350
+ ): TransitionPath {
351
+ // ===== FAST PATH 1: Initial navigation (no fromState) =====
352
+ // This is the best performing case in benchmarks (5M ops/sec)
353
+ if (!fromState) {
354
+ return {
355
+ intersection: EMPTY_INTERSECTION,
356
+ toActivate: nameToIDs(toState.name),
357
+ toDeactivate: [],
358
+ };
359
+ }
360
+
361
+ const toStateOptions = toState.meta?.options ?? {};
362
+
363
+ // ===== FAST PATH 2: Force reload =====
364
+ // Skip all optimization when reload is requested
365
+ if (toStateOptions.reload) {
366
+ return {
367
+ intersection: EMPTY_INTERSECTION,
368
+ toActivate: nameToIDs(toState.name),
369
+ toDeactivate: reverseArray(nameToIDs(fromState.name)),
370
+ };
371
+ }
372
+
373
+ // ===== FAST PATH 3: Missing meta or meta.params requires full reload =====
374
+ // Check if meta or meta.params is actually missing (not just empty)
375
+ const toHasMeta = toState.meta?.params !== undefined;
376
+ const fromHasMeta = fromState.meta?.params !== undefined;
377
+
378
+ if (!toHasMeta && !fromHasMeta) {
379
+ // Both states missing meta.params - require full reload
380
+ return {
381
+ intersection: EMPTY_INTERSECTION,
382
+ toActivate: nameToIDs(toState.name),
383
+ toDeactivate: reverseArray(nameToIDs(fromState.name)),
384
+ };
385
+ }
386
+
387
+ // ===== FAST PATH 4: Same routes with empty meta.params =====
388
+ // If both have empty meta.params {}, no parameter checking needed
389
+ if (toState.name === fromState.name && toHasMeta && fromHasMeta) {
390
+ const toParamsEmpty =
391
+ toState.meta && Object.keys(toState.meta.params).length === 0;
392
+ const fromParamsEmpty =
393
+ fromState.meta && Object.keys(fromState.meta.params).length === 0;
394
+
395
+ if (toParamsEmpty && fromParamsEmpty) {
396
+ // Both have empty params - no transition needed
397
+ return {
398
+ intersection: toState.name,
399
+ toActivate: [],
400
+ toDeactivate: [],
401
+ };
402
+ }
403
+ }
404
+
405
+ // ===== STANDARD PATH: Routes with parameters =====
406
+ // Use original algorithm for complex cases with parameters
407
+ const toStateIds = nameToIDs(toState.name);
408
+ const fromStateIds = nameToIDs(fromState.name);
409
+ const maxI = Math.min(fromStateIds.length, toStateIds.length);
410
+
411
+ // Find where paths diverge based on segments and parameters
412
+ // not obvious validate toState and fromState
413
+ const i = pointOfDifference(
414
+ toState,
415
+ fromState,
416
+ toStateIds,
417
+ fromStateIds,
418
+ maxI,
419
+ );
420
+
421
+ // Optimization: Build deactivation list in reverse order directly
422
+ // instead of slice(i).toReversed() which creates 2 arrays
423
+ const toDeactivate: string[] = [];
424
+
425
+ for (let j = fromStateIds.length - 1; j >= i; j--) {
426
+ toDeactivate.push(fromStateIds[j]);
427
+ }
428
+
429
+ // Build activation list (forward order for proper initialization)
430
+ const toActivate = toStateIds.slice(i);
431
+
432
+ // Determine intersection point (common ancestor)
433
+ // Note: fromState is guaranteed to be defined here (early return on line 366)
434
+ const intersection = i > 0 ? fromStateIds[i - 1] : EMPTY_INTERSECTION;
435
+
436
+ return {
437
+ intersection,
438
+ toDeactivate,
439
+ toActivate,
440
+ };
441
+ }
@@ -0,0 +1,74 @@
1
+ // packages/real-router/modules/typeGuards.ts
2
+
3
+ /**
4
+ * Re-export common type guards from centralized type-guards package
5
+ */
6
+ import type { LoggerConfig, LogLevelConfig } from "@real-router/logger";
7
+
8
+ export {
9
+ isObjKey,
10
+ isString,
11
+ isState,
12
+ isParams,
13
+ isNavigationOptions,
14
+ isPromise,
15
+ isBoolean,
16
+ validateRouteName,
17
+ } from "type-guards";
18
+
19
+ /**
20
+ * RealRouter-specific type guards for logger configuration
21
+ */
22
+
23
+ const VALID_LEVELS_SET = new Set<string>(["all", "warn-error", "error-only"]);
24
+
25
+ function isValidLevel(value: unknown): value is LogLevelConfig {
26
+ return typeof value === "string" && VALID_LEVELS_SET.has(value);
27
+ }
28
+
29
+ function formatValue(value: unknown): string {
30
+ if (typeof value === "string") {
31
+ return `"${value}"`;
32
+ }
33
+ if (typeof value === "object") {
34
+ return JSON.stringify(value);
35
+ }
36
+
37
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string
38
+ return String(value);
39
+ }
40
+
41
+ export function isLoggerConfig(config: unknown): config is LoggerConfig {
42
+ if (typeof config !== "object" || config === null) {
43
+ throw new TypeError("Logger config must be an object");
44
+ }
45
+
46
+ const obj = config;
47
+
48
+ // Check for unknown properties
49
+ for (const key of Object.keys(obj)) {
50
+ if (key !== "level" && key !== "callback") {
51
+ throw new TypeError(`Unknown logger config property: "${key}"`);
52
+ }
53
+ }
54
+
55
+ // Validate level if present
56
+ if ("level" in obj && obj.level !== undefined && !isValidLevel(obj.level)) {
57
+ throw new TypeError(
58
+ `Invalid logger level: ${formatValue(obj.level)}. Expected: "all" | "warn-error" | "error-only"`,
59
+ );
60
+ }
61
+
62
+ // Validate callback if present
63
+ if (
64
+ "callback" in obj &&
65
+ obj.callback !== undefined &&
66
+ typeof obj.callback !== "function"
67
+ ) {
68
+ throw new TypeError(
69
+ `Logger callback must be a function, got ${typeof obj.callback}`,
70
+ );
71
+ }
72
+
73
+ return true;
74
+ }