@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.
- package/README.md +1 -3
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/metafile-cjs.json +1 -1
- package/dist/esm/index.d.mts +1 -1
- package/dist/esm/index.mjs +1 -1
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/metafile-esm.json +1 -1
- package/package.json +7 -5
- package/src/Router.ts +1174 -0
- package/src/RouterError.ts +324 -0
- package/src/constants.ts +112 -0
- package/src/createRouter.ts +32 -0
- package/src/fsm/index.ts +5 -0
- package/src/fsm/routerFSM.ts +129 -0
- package/src/getNavigator.ts +15 -0
- package/src/helpers.ts +194 -0
- package/src/index.ts +46 -0
- package/src/namespaces/CloneNamespace/CloneNamespace.ts +120 -0
- package/src/namespaces/CloneNamespace/index.ts +3 -0
- package/src/namespaces/CloneNamespace/types.ts +46 -0
- package/src/namespaces/DependenciesNamespace/DependenciesNamespace.ts +250 -0
- package/src/namespaces/DependenciesNamespace/index.ts +3 -0
- package/src/namespaces/DependenciesNamespace/validators.ts +105 -0
- package/src/namespaces/EventBusNamespace/EventBusNamespace.ts +272 -0
- package/src/namespaces/EventBusNamespace/index.ts +5 -0
- package/src/namespaces/EventBusNamespace/types.ts +11 -0
- package/src/namespaces/MiddlewareNamespace/MiddlewareNamespace.ts +206 -0
- package/src/namespaces/MiddlewareNamespace/index.ts +5 -0
- package/src/namespaces/MiddlewareNamespace/types.ts +28 -0
- package/src/namespaces/MiddlewareNamespace/validators.ts +96 -0
- package/src/namespaces/NavigationNamespace/NavigationNamespace.ts +308 -0
- package/src/namespaces/NavigationNamespace/index.ts +5 -0
- package/src/namespaces/NavigationNamespace/transition/executeLifecycleHooks.ts +84 -0
- package/src/namespaces/NavigationNamespace/transition/executeMiddleware.ts +56 -0
- package/src/namespaces/NavigationNamespace/transition/index.ts +107 -0
- package/src/namespaces/NavigationNamespace/transition/makeError.ts +37 -0
- package/src/namespaces/NavigationNamespace/transition/mergeStates.ts +54 -0
- package/src/namespaces/NavigationNamespace/transition/processLifecycleResult.ts +81 -0
- package/src/namespaces/NavigationNamespace/transition/wrapSyncError.ts +82 -0
- package/src/namespaces/NavigationNamespace/types.ts +129 -0
- package/src/namespaces/NavigationNamespace/validators.ts +87 -0
- package/src/namespaces/OptionsNamespace/OptionsNamespace.ts +50 -0
- package/src/namespaces/OptionsNamespace/constants.ts +41 -0
- package/src/namespaces/OptionsNamespace/helpers.ts +51 -0
- package/src/namespaces/OptionsNamespace/index.ts +11 -0
- package/src/namespaces/OptionsNamespace/validators.ts +252 -0
- package/src/namespaces/PluginsNamespace/PluginsNamespace.ts +325 -0
- package/src/namespaces/PluginsNamespace/constants.ts +35 -0
- package/src/namespaces/PluginsNamespace/index.ts +7 -0
- package/src/namespaces/PluginsNamespace/types.ts +32 -0
- package/src/namespaces/PluginsNamespace/validators.ts +79 -0
- package/src/namespaces/RouteLifecycleNamespace/RouteLifecycleNamespace.ts +389 -0
- package/src/namespaces/RouteLifecycleNamespace/index.ts +5 -0
- package/src/namespaces/RouteLifecycleNamespace/types.ts +17 -0
- package/src/namespaces/RouteLifecycleNamespace/validators.ts +65 -0
- package/src/namespaces/RouterLifecycleNamespace/RouterLifecycleNamespace.ts +140 -0
- package/src/namespaces/RouterLifecycleNamespace/constants.ts +25 -0
- package/src/namespaces/RouterLifecycleNamespace/index.ts +5 -0
- package/src/namespaces/RouterLifecycleNamespace/types.ts +23 -0
- package/src/namespaces/RoutesNamespace/RoutesNamespace.ts +1482 -0
- package/src/namespaces/RoutesNamespace/constants.ts +14 -0
- package/src/namespaces/RoutesNamespace/helpers.ts +532 -0
- package/src/namespaces/RoutesNamespace/index.ts +9 -0
- package/src/namespaces/RoutesNamespace/stateBuilder.ts +70 -0
- package/src/namespaces/RoutesNamespace/types.ts +82 -0
- package/src/namespaces/RoutesNamespace/validators.ts +331 -0
- package/src/namespaces/StateNamespace/StateNamespace.ts +317 -0
- package/src/namespaces/StateNamespace/helpers.ts +43 -0
- package/src/namespaces/StateNamespace/index.ts +5 -0
- package/src/namespaces/StateNamespace/types.ts +15 -0
- package/src/namespaces/index.ts +42 -0
- package/src/transitionPath.ts +441 -0
- package/src/typeGuards.ts +74 -0
- package/src/types.ts +194 -0
- package/src/wiring/RouterWiringBuilder.ts +235 -0
- package/src/wiring/index.ts +7 -0
- package/src/wiring/types.ts +53 -0
- 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
|
+
}
|