@real-router/core 0.45.1 → 0.45.3
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/dist/cjs/Router-Dh1xgFLI.d.ts.map +1 -0
- package/dist/cjs/RouterValidator-TUi8eT8Q.d.ts.map +1 -0
- package/dist/cjs/api.d.ts.map +1 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/utils.d.ts.map +1 -0
- package/dist/cjs/validation.d.ts.map +1 -0
- package/dist/esm/Router-BPkXwb1J.d.mts.map +1 -0
- package/dist/esm/RouterValidator-DphcVMEp.d.mts.map +1 -0
- package/dist/esm/api.d.mts.map +1 -0
- package/dist/esm/index.d.mts.map +1 -0
- package/dist/esm/utils.d.mts.map +1 -0
- package/dist/esm/validation.d.mts.map +1 -0
- package/package.json +9 -12
- package/src/Router.ts +684 -0
- package/src/RouterError.ts +324 -0
- package/src/api/cloneRouter.ts +77 -0
- package/src/api/getDependenciesApi.ts +168 -0
- package/src/api/getLifecycleApi.ts +65 -0
- package/src/api/getPluginApi.ts +167 -0
- package/src/api/getRoutesApi.ts +573 -0
- package/src/api/helpers.ts +10 -0
- package/src/api/index.ts +16 -0
- package/src/api/types.ts +12 -0
- package/src/constants.ts +87 -0
- package/src/createRouter.ts +32 -0
- package/src/fsm/index.ts +5 -0
- package/src/fsm/routerFSM.ts +120 -0
- package/src/getNavigator.ts +30 -0
- package/src/guards.ts +46 -0
- package/src/helpers.ts +179 -0
- package/src/index.ts +50 -0
- package/src/internals.ts +173 -0
- package/src/namespaces/DependenciesNamespace/dependenciesStore.ts +30 -0
- package/src/namespaces/DependenciesNamespace/index.ts +5 -0
- package/src/namespaces/EventBusNamespace/EventBusNamespace.ts +311 -0
- package/src/namespaces/EventBusNamespace/index.ts +5 -0
- package/src/namespaces/EventBusNamespace/types.ts +11 -0
- package/src/namespaces/NavigationNamespace/NavigationNamespace.ts +405 -0
- package/src/namespaces/NavigationNamespace/constants.ts +55 -0
- package/src/namespaces/NavigationNamespace/index.ts +5 -0
- package/src/namespaces/NavigationNamespace/transition/completeTransition.ts +100 -0
- package/src/namespaces/NavigationNamespace/transition/errorHandling.ts +124 -0
- package/src/namespaces/NavigationNamespace/transition/guardPhase.ts +221 -0
- package/src/namespaces/NavigationNamespace/types.ts +100 -0
- package/src/namespaces/OptionsNamespace/OptionsNamespace.ts +28 -0
- package/src/namespaces/OptionsNamespace/constants.ts +19 -0
- package/src/namespaces/OptionsNamespace/helpers.ts +50 -0
- package/src/namespaces/OptionsNamespace/index.ts +7 -0
- package/src/namespaces/OptionsNamespace/validators.ts +13 -0
- package/src/namespaces/PluginsNamespace/PluginsNamespace.ts +291 -0
- package/src/namespaces/PluginsNamespace/constants.ts +34 -0
- package/src/namespaces/PluginsNamespace/index.ts +7 -0
- package/src/namespaces/PluginsNamespace/types.ts +22 -0
- package/src/namespaces/PluginsNamespace/validators.ts +28 -0
- package/src/namespaces/RouteLifecycleNamespace/RouteLifecycleNamespace.ts +377 -0
- package/src/namespaces/RouteLifecycleNamespace/index.ts +5 -0
- package/src/namespaces/RouteLifecycleNamespace/types.ts +10 -0
- package/src/namespaces/RouterLifecycleNamespace/RouterLifecycleNamespace.ts +81 -0
- package/src/namespaces/RouterLifecycleNamespace/constants.ts +25 -0
- package/src/namespaces/RouterLifecycleNamespace/index.ts +5 -0
- package/src/namespaces/RouterLifecycleNamespace/types.ts +26 -0
- package/src/namespaces/RoutesNamespace/RoutesNamespace.ts +535 -0
- package/src/namespaces/RoutesNamespace/constants.ts +6 -0
- package/src/namespaces/RoutesNamespace/forwardChain.ts +34 -0
- package/src/namespaces/RoutesNamespace/helpers.ts +126 -0
- package/src/namespaces/RoutesNamespace/index.ts +11 -0
- package/src/namespaces/RoutesNamespace/routeGuards.ts +62 -0
- package/src/namespaces/RoutesNamespace/routesStore.ts +346 -0
- package/src/namespaces/RoutesNamespace/types.ts +81 -0
- package/src/namespaces/StateNamespace/StateNamespace.ts +211 -0
- package/src/namespaces/StateNamespace/helpers.ts +24 -0
- package/src/namespaces/StateNamespace/index.ts +5 -0
- package/src/namespaces/StateNamespace/types.ts +15 -0
- package/src/namespaces/index.ts +35 -0
- package/src/stateMetaStore.ts +15 -0
- package/src/transitionPath.ts +436 -0
- package/src/typeGuards.ts +59 -0
- package/src/types/RouterValidator.ts +154 -0
- package/src/types.ts +69 -0
- package/src/utils/getStaticPaths.ts +50 -0
- package/src/utils/index.ts +5 -0
- package/src/utils/serializeState.ts +22 -0
- package/src/validation.ts +12 -0
- package/src/wiring/RouterWiringBuilder.ts +261 -0
- package/src/wiring/index.ts +7 -0
- package/src/wiring/types.ts +47 -0
- package/src/wiring/wireRouter.ts +26 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// packages/core/src/createRouter.ts
|
|
2
|
+
|
|
3
|
+
import { Router } from "./Router";
|
|
4
|
+
|
|
5
|
+
import type { Route } from "./types";
|
|
6
|
+
import type { DefaultDependencies, Options } from "@real-router/types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Creates a new router instance.
|
|
10
|
+
*
|
|
11
|
+
* @param routes - Array of route definitions
|
|
12
|
+
* @param options - Router configuration options
|
|
13
|
+
* @param dependencies - Dependencies to inject into the router
|
|
14
|
+
* @returns A new Router instance
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* const router = createRouter([
|
|
18
|
+
* { name: 'home', path: '/' },
|
|
19
|
+
* { name: 'users', path: '/users' },
|
|
20
|
+
* ]);
|
|
21
|
+
*
|
|
22
|
+
* router.start('/');
|
|
23
|
+
*/
|
|
24
|
+
export const createRouter = <
|
|
25
|
+
Dependencies extends DefaultDependencies = DefaultDependencies,
|
|
26
|
+
>(
|
|
27
|
+
routes: Route<Dependencies>[] = [],
|
|
28
|
+
options: Partial<Options> = {},
|
|
29
|
+
dependencies: Dependencies = {} as Dependencies,
|
|
30
|
+
): Router<Dependencies> => {
|
|
31
|
+
return new Router<Dependencies>(routes, options, dependencies);
|
|
32
|
+
};
|
package/src/fsm/index.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// packages/core/src/fsm/routerFSM.ts
|
|
2
|
+
|
|
3
|
+
import { FSM } from "@real-router/fsm";
|
|
4
|
+
|
|
5
|
+
import type { FSMConfig } from "@real-router/fsm";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Router FSM states.
|
|
9
|
+
*
|
|
10
|
+
* - IDLE: Router not started or stopped
|
|
11
|
+
* - STARTING: Router is initializing
|
|
12
|
+
* - READY: Router is ready for navigation
|
|
13
|
+
* - TRANSITION_STARTED: Navigation in progress (before deactivation guards)
|
|
14
|
+
* - LEAVE_APPROVED: Deactivation guards passed, activation guards pending
|
|
15
|
+
* - DISPOSED: Router has been disposed (R2+)
|
|
16
|
+
*/
|
|
17
|
+
export const routerStates = {
|
|
18
|
+
IDLE: "IDLE",
|
|
19
|
+
STARTING: "STARTING",
|
|
20
|
+
READY: "READY",
|
|
21
|
+
TRANSITION_STARTED: "TRANSITION_STARTED",
|
|
22
|
+
LEAVE_APPROVED: "LEAVE_APPROVED",
|
|
23
|
+
DISPOSED: "DISPOSED",
|
|
24
|
+
} as const;
|
|
25
|
+
|
|
26
|
+
export type RouterState = (typeof routerStates)[keyof typeof routerStates];
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Router FSM events.
|
|
30
|
+
*
|
|
31
|
+
* - START: Begin router initialization
|
|
32
|
+
* - STARTED: Router initialization complete
|
|
33
|
+
* - NAVIGATE: Begin navigation
|
|
34
|
+
* - COMPLETE: Navigation completed successfully
|
|
35
|
+
* - FAIL: Navigation or initialization failed
|
|
36
|
+
* - CANCEL: Navigation cancelled
|
|
37
|
+
* - STOP: Stop router
|
|
38
|
+
* - DISPOSE: Dispose router (R2+)
|
|
39
|
+
*/
|
|
40
|
+
export const routerEvents = {
|
|
41
|
+
START: "START",
|
|
42
|
+
STARTED: "STARTED",
|
|
43
|
+
NAVIGATE: "NAVIGATE",
|
|
44
|
+
LEAVE_APPROVE: "LEAVE_APPROVE",
|
|
45
|
+
COMPLETE: "COMPLETE",
|
|
46
|
+
FAIL: "FAIL",
|
|
47
|
+
CANCEL: "CANCEL",
|
|
48
|
+
STOP: "STOP",
|
|
49
|
+
DISPOSE: "DISPOSE",
|
|
50
|
+
} as const;
|
|
51
|
+
|
|
52
|
+
export type RouterEvent = (typeof routerEvents)[keyof typeof routerEvents];
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Typed payloads for router FSM events.
|
|
56
|
+
*
|
|
57
|
+
* Events without entries have no payload.
|
|
58
|
+
*/
|
|
59
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type -- payloads stored in EventBusNamespace fields (N8+N9 optimization)
|
|
60
|
+
export interface RouterPayloads {}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Router FSM configuration.
|
|
64
|
+
*
|
|
65
|
+
* Transitions:
|
|
66
|
+
* - IDLE → STARTING (START), DISPOSED (DISPOSE)
|
|
67
|
+
* - STARTING → READY (STARTED), IDLE (FAIL)
|
|
68
|
+
* - READY → TRANSITION_STARTED (NAVIGATE), READY (FAIL, self-loop for early validation errors), IDLE (STOP)
|
|
69
|
+
* - TRANSITION_STARTED → LEAVE_APPROVED (LEAVE_APPROVE), TRANSITION_STARTED (NAVIGATE, self-loop), READY (CANCEL, FAIL)
|
|
70
|
+
* - LEAVE_APPROVED → READY (COMPLETE, CANCEL, FAIL), TRANSITION_STARTED (NAVIGATE)
|
|
71
|
+
* - DISPOSED → (no transitions)
|
|
72
|
+
*/
|
|
73
|
+
const routerFSMConfig: FSMConfig<RouterState, RouterEvent, null> = {
|
|
74
|
+
initial: routerStates.IDLE,
|
|
75
|
+
context: null,
|
|
76
|
+
transitions: {
|
|
77
|
+
[routerStates.IDLE]: {
|
|
78
|
+
[routerEvents.START]: routerStates.STARTING,
|
|
79
|
+
[routerEvents.DISPOSE]: routerStates.DISPOSED,
|
|
80
|
+
},
|
|
81
|
+
[routerStates.STARTING]: {
|
|
82
|
+
[routerEvents.STARTED]: routerStates.READY,
|
|
83
|
+
[routerEvents.FAIL]: routerStates.IDLE,
|
|
84
|
+
},
|
|
85
|
+
[routerStates.READY]: {
|
|
86
|
+
[routerEvents.NAVIGATE]: routerStates.TRANSITION_STARTED,
|
|
87
|
+
[routerEvents.FAIL]: routerStates.READY,
|
|
88
|
+
[routerEvents.STOP]: routerStates.IDLE,
|
|
89
|
+
},
|
|
90
|
+
[routerStates.TRANSITION_STARTED]: {
|
|
91
|
+
[routerEvents.NAVIGATE]: routerStates.TRANSITION_STARTED,
|
|
92
|
+
[routerEvents.LEAVE_APPROVE]: routerStates.LEAVE_APPROVED,
|
|
93
|
+
[routerEvents.CANCEL]: routerStates.READY,
|
|
94
|
+
[routerEvents.FAIL]: routerStates.READY,
|
|
95
|
+
},
|
|
96
|
+
[routerStates.LEAVE_APPROVED]: {
|
|
97
|
+
[routerEvents.NAVIGATE]: routerStates.TRANSITION_STARTED,
|
|
98
|
+
[routerEvents.COMPLETE]: routerStates.READY,
|
|
99
|
+
[routerEvents.CANCEL]: routerStates.READY,
|
|
100
|
+
[routerEvents.FAIL]: routerStates.READY,
|
|
101
|
+
},
|
|
102
|
+
[routerStates.DISPOSED]: {},
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Factory function to create a router FSM instance.
|
|
108
|
+
*
|
|
109
|
+
* @returns FSM instance with initial state "IDLE"
|
|
110
|
+
*/
|
|
111
|
+
export function createRouterFSM(): FSM<
|
|
112
|
+
RouterState,
|
|
113
|
+
RouterEvent,
|
|
114
|
+
null,
|
|
115
|
+
RouterPayloads
|
|
116
|
+
> {
|
|
117
|
+
return new FSM<RouterState, RouterEvent, null, RouterPayloads>(
|
|
118
|
+
routerFSMConfig,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Navigator,
|
|
3
|
+
DefaultDependencies,
|
|
4
|
+
Router,
|
|
5
|
+
} from "@real-router/types";
|
|
6
|
+
|
|
7
|
+
const cache = new WeakMap<Router, Navigator>();
|
|
8
|
+
|
|
9
|
+
export const getNavigator = <
|
|
10
|
+
Dependencies extends DefaultDependencies = DefaultDependencies,
|
|
11
|
+
>(
|
|
12
|
+
router: Router<Dependencies>,
|
|
13
|
+
): Navigator => {
|
|
14
|
+
let nav = cache.get(router);
|
|
15
|
+
|
|
16
|
+
if (!nav) {
|
|
17
|
+
nav = Object.freeze({
|
|
18
|
+
navigate: router.navigate,
|
|
19
|
+
getState: router.getState,
|
|
20
|
+
isActiveRoute: router.isActiveRoute,
|
|
21
|
+
canNavigateTo: router.canNavigateTo,
|
|
22
|
+
subscribe: router.subscribe,
|
|
23
|
+
subscribeLeave: router.subscribeLeave,
|
|
24
|
+
isLeaveApproved: router.isLeaveApproved,
|
|
25
|
+
} as Navigator);
|
|
26
|
+
cache.set(router, nav);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return nav;
|
|
30
|
+
};
|
package/src/guards.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// packages/core/src/guards.ts
|
|
2
|
+
|
|
3
|
+
import type { Route } from "./types";
|
|
4
|
+
import type { RouterValidator } from "./types/RouterValidator";
|
|
5
|
+
|
|
6
|
+
export function guardDependencies(deps: unknown): void {
|
|
7
|
+
if (
|
|
8
|
+
!deps ||
|
|
9
|
+
typeof deps !== "object" ||
|
|
10
|
+
(deps as { constructor: unknown }).constructor !== Object
|
|
11
|
+
) {
|
|
12
|
+
throw new TypeError("dependencies must be a plain object");
|
|
13
|
+
}
|
|
14
|
+
for (const key in deps as Record<string, unknown>) {
|
|
15
|
+
if (Object.getOwnPropertyDescriptor(deps, key)?.get) {
|
|
16
|
+
throw new TypeError(`dependencies cannot contain getters: "${key}"`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/* eslint-disable @typescript-eslint/no-explicit-any -- accepts any Route type */
|
|
22
|
+
export function guardRouteStructure(
|
|
23
|
+
routes: Route<any>[],
|
|
24
|
+
validator?: RouterValidator | null,
|
|
25
|
+
): void {
|
|
26
|
+
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
27
|
+
for (const route of routes) {
|
|
28
|
+
const routeValue: unknown = route;
|
|
29
|
+
|
|
30
|
+
if (
|
|
31
|
+
routeValue === null ||
|
|
32
|
+
typeof routeValue !== "object" ||
|
|
33
|
+
Array.isArray(routeValue)
|
|
34
|
+
) {
|
|
35
|
+
throw new TypeError("route must be a non-array object");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
validator?.routes.guardRouteCallbacks(route as Route);
|
|
39
|
+
validator?.routes.guardNoAsyncCallbacks(route as Route);
|
|
40
|
+
const children = (route as Route).children;
|
|
41
|
+
|
|
42
|
+
if (children) {
|
|
43
|
+
guardRouteStructure(children, validator);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/helpers.ts
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// packages/core/src/helpers.ts
|
|
2
|
+
|
|
3
|
+
import { DEFAULT_LIMITS } from "./constants";
|
|
4
|
+
|
|
5
|
+
import type { Limits } from "./types";
|
|
6
|
+
import type { State, LimitsConfig } from "@real-router/types";
|
|
7
|
+
|
|
8
|
+
// =============================================================================
|
|
9
|
+
// State Helpers
|
|
10
|
+
// =============================================================================
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Structural type guard for State object.
|
|
14
|
+
* Only checks required fields exist with correct types.
|
|
15
|
+
* Does NOT validate params serializability (allows circular refs).
|
|
16
|
+
*
|
|
17
|
+
* Use `isState` from type-guards for full validation (serializable params).
|
|
18
|
+
* Use this for internal operations like deepFreezeState that handle any object structure.
|
|
19
|
+
*
|
|
20
|
+
* @param value - Value to check
|
|
21
|
+
* @returns true if value has State structure
|
|
22
|
+
* @internal
|
|
23
|
+
*/
|
|
24
|
+
function isStateStructural(value: unknown): value is State {
|
|
25
|
+
if (value === null || typeof value !== "object") {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const obj = value as Record<string, unknown>;
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
typeof obj.name === "string" &&
|
|
33
|
+
typeof obj.path === "string" &&
|
|
34
|
+
typeof obj.params === "object" &&
|
|
35
|
+
obj.params !== null
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Deep freezes State object to prevent mutations.
|
|
41
|
+
* Creates a deep clone first, then recursively freezes the clone and all nested objects.
|
|
42
|
+
* Uses simple recursive freezing after cloning (no need for WeakSet since clone has no circular refs).
|
|
43
|
+
*
|
|
44
|
+
* @param state - The State object to freeze
|
|
45
|
+
* @returns A frozen deep clone of the state
|
|
46
|
+
* @throws {TypeError} If state is not a valid State object
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* const state = { name: 'home', params: {}, path: '/' };
|
|
50
|
+
* const frozen = deepFreezeState(state);
|
|
51
|
+
* // frozen.params is now immutable
|
|
52
|
+
* // original state is unchanged
|
|
53
|
+
*/
|
|
54
|
+
export function deepFreezeState<T extends State>(state: T): T {
|
|
55
|
+
// Early return for null/undefined
|
|
56
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
57
|
+
if (!state) {
|
|
58
|
+
return state;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Validate State structure (structural check, allows circular refs)
|
|
62
|
+
if (!isStateStructural(state)) {
|
|
63
|
+
throw new TypeError(
|
|
64
|
+
`[deepFreezeState] Expected valid State object, got: ${typeof state}`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Create a deep clone to avoid mutating the original
|
|
69
|
+
// structuredClone preserves circular references, so we need to track visited objects
|
|
70
|
+
const clonedState = structuredClone(state);
|
|
71
|
+
|
|
72
|
+
// WeakSet to track visited objects (prevent infinite recursion with circular refs)
|
|
73
|
+
const visited = new WeakSet<object>();
|
|
74
|
+
|
|
75
|
+
// Recursive freeze function with circular reference protection
|
|
76
|
+
function freezeClonedRecursive(obj: unknown): void {
|
|
77
|
+
// Skip primitives, null, undefined
|
|
78
|
+
// Note: typeof undefined === "undefined" !== "object", so checking undefined is redundant
|
|
79
|
+
if (obj === null || typeof obj !== "object") {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Skip already visited objects (circular reference protection)
|
|
84
|
+
if (visited.has(obj)) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Mark as visited
|
|
89
|
+
visited.add(obj);
|
|
90
|
+
|
|
91
|
+
// Freeze the object/array itself
|
|
92
|
+
Object.freeze(obj);
|
|
93
|
+
|
|
94
|
+
// Iterate without Object.values() allocation
|
|
95
|
+
if (Array.isArray(obj)) {
|
|
96
|
+
for (const item of obj) {
|
|
97
|
+
freezeClonedRecursive(item);
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
for (const key in obj) {
|
|
101
|
+
freezeClonedRecursive((obj as Record<string, unknown>)[key]);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Freeze the entire cloned state tree
|
|
107
|
+
freezeClonedRecursive(clonedState);
|
|
108
|
+
|
|
109
|
+
return clonedState;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// WeakSet to track already frozen root objects for O(1) re-freeze check
|
|
113
|
+
const frozenRoots = new WeakSet<object>();
|
|
114
|
+
|
|
115
|
+
// Module-scope recursive freeze function - better JIT optimization, no allocation per call
|
|
116
|
+
function freezeRecursive(obj: unknown): void {
|
|
117
|
+
// Skip primitives, null
|
|
118
|
+
if (obj === null || typeof obj !== "object") {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Skip already frozen objects (handles potential shared refs)
|
|
123
|
+
if (Object.isFrozen(obj)) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Freeze the object/array
|
|
128
|
+
Object.freeze(obj);
|
|
129
|
+
|
|
130
|
+
// Iterate without Object.values() allocation
|
|
131
|
+
if (Array.isArray(obj)) {
|
|
132
|
+
for (const item of obj) {
|
|
133
|
+
freezeRecursive(item);
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
for (const key in obj) {
|
|
137
|
+
freezeRecursive((obj as Record<string, unknown>)[key]);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Freezes State object in-place without cloning.
|
|
144
|
+
* Optimized for hot paths where state is known to be a fresh object.
|
|
145
|
+
*
|
|
146
|
+
* IMPORTANT: Only use this when you know the state is a fresh object
|
|
147
|
+
* that hasn't been exposed to external code yet (e.g., from makeState()).
|
|
148
|
+
*
|
|
149
|
+
* @param state - The State object to freeze (must be a fresh object)
|
|
150
|
+
* @returns The same state object, now frozen
|
|
151
|
+
* @internal
|
|
152
|
+
*/
|
|
153
|
+
export function freezeStateInPlace<T extends State>(state: T): T {
|
|
154
|
+
// Early return for null/undefined - state from makeState() is never null
|
|
155
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
156
|
+
if (!state) {
|
|
157
|
+
return state;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Fast path: already processed root object - O(1) check
|
|
161
|
+
if (frozenRoots.has(state)) {
|
|
162
|
+
return state;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
freezeRecursive(state);
|
|
166
|
+
|
|
167
|
+
// Mark root as processed for future calls
|
|
168
|
+
frozenRoots.add(state);
|
|
169
|
+
|
|
170
|
+
return state;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Merges user limits with defaults.
|
|
175
|
+
* Returns frozen object for immutability.
|
|
176
|
+
*/
|
|
177
|
+
export function createLimits(userLimits: Partial<LimitsConfig> = {}): Limits {
|
|
178
|
+
return { ...DEFAULT_LIMITS, ...userLimits };
|
|
179
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// packages/core/src/index.ts
|
|
2
|
+
|
|
3
|
+
// Router-dependent types (re-exported from @real-router/types)
|
|
4
|
+
|
|
5
|
+
export type {
|
|
6
|
+
BuildStateResultWithSegments,
|
|
7
|
+
GuardFnFactory,
|
|
8
|
+
PluginFactory,
|
|
9
|
+
Route,
|
|
10
|
+
RouteConfigUpdate,
|
|
11
|
+
} from "./types";
|
|
12
|
+
|
|
13
|
+
export type { RouterValidator } from "./types/RouterValidator";
|
|
14
|
+
|
|
15
|
+
// Router class (replaces Router interface from core-types)
|
|
16
|
+
export { Router } from "./Router";
|
|
17
|
+
|
|
18
|
+
// Types (re-exported from core-types - no Router dependency)
|
|
19
|
+
export type {
|
|
20
|
+
Config,
|
|
21
|
+
DefaultDependencies,
|
|
22
|
+
GuardFn,
|
|
23
|
+
Listener,
|
|
24
|
+
Navigator,
|
|
25
|
+
NavigationOptions,
|
|
26
|
+
Options,
|
|
27
|
+
Params,
|
|
28
|
+
Plugin,
|
|
29
|
+
SimpleState,
|
|
30
|
+
State,
|
|
31
|
+
SubscribeFn,
|
|
32
|
+
SubscribeState,
|
|
33
|
+
Subscription,
|
|
34
|
+
Unsubscribe,
|
|
35
|
+
} from "@real-router/types";
|
|
36
|
+
|
|
37
|
+
export type { ErrorCodes, Constants } from "./constants";
|
|
38
|
+
|
|
39
|
+
export { events, constants, errorCodes, UNKNOWN_ROUTE } from "./constants";
|
|
40
|
+
|
|
41
|
+
// RouterError class (migrated from router-error package)
|
|
42
|
+
export { RouterError } from "./RouterError";
|
|
43
|
+
|
|
44
|
+
export { createRouter } from "./createRouter";
|
|
45
|
+
|
|
46
|
+
export { getNavigator } from "./getNavigator";
|
|
47
|
+
|
|
48
|
+
export { resolveForwardChain } from "./namespaces/RoutesNamespace/forwardChain";
|
|
49
|
+
|
|
50
|
+
export type { RouteTree } from "route-tree";
|
package/src/internals.ts
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import type { DependenciesStore } from "./namespaces/DependenciesNamespace";
|
|
2
|
+
import type { RoutesStore } from "./namespaces/RoutesNamespace";
|
|
3
|
+
import type { Router as RouterClass } from "./Router";
|
|
4
|
+
import type { EventMethodMap, GuardFnFactory, PluginFactory } from "./types";
|
|
5
|
+
import type { RouterValidator } from "./types/RouterValidator";
|
|
6
|
+
import type {
|
|
7
|
+
DefaultDependencies,
|
|
8
|
+
EventName,
|
|
9
|
+
Options,
|
|
10
|
+
Params,
|
|
11
|
+
Plugin,
|
|
12
|
+
Router as RouterInterface,
|
|
13
|
+
RouteTreeState,
|
|
14
|
+
SimpleState,
|
|
15
|
+
State,
|
|
16
|
+
Unsubscribe,
|
|
17
|
+
} from "@real-router/types";
|
|
18
|
+
import type { RouteTree } from "route-tree";
|
|
19
|
+
|
|
20
|
+
export interface RouterInternals<
|
|
21
|
+
D extends DefaultDependencies = DefaultDependencies,
|
|
22
|
+
> {
|
|
23
|
+
readonly makeState: <P extends Params = Params>(
|
|
24
|
+
name: string,
|
|
25
|
+
params?: P,
|
|
26
|
+
path?: string,
|
|
27
|
+
meta?: Record<string, Record<string, "url" | "query">>,
|
|
28
|
+
) => State<P>;
|
|
29
|
+
|
|
30
|
+
readonly forwardState: <P extends Params = Params>(
|
|
31
|
+
routeName: string,
|
|
32
|
+
routeParams: P,
|
|
33
|
+
) => SimpleState<P>;
|
|
34
|
+
|
|
35
|
+
readonly buildStateResolved: (
|
|
36
|
+
resolvedName: string,
|
|
37
|
+
resolvedParams: Params,
|
|
38
|
+
) => RouteTreeState | undefined;
|
|
39
|
+
|
|
40
|
+
readonly matchPath: <P extends Params = Params>(
|
|
41
|
+
path: string,
|
|
42
|
+
options?: Options,
|
|
43
|
+
) => State<P> | undefined;
|
|
44
|
+
|
|
45
|
+
readonly getOptions: () => Options;
|
|
46
|
+
|
|
47
|
+
readonly addEventListener: <E extends EventName>(
|
|
48
|
+
eventName: E,
|
|
49
|
+
cb: Plugin[EventMethodMap[E]],
|
|
50
|
+
) => Unsubscribe;
|
|
51
|
+
|
|
52
|
+
readonly buildPath: (route: string, params?: Params) => string;
|
|
53
|
+
|
|
54
|
+
readonly start: (path: string) => Promise<State>;
|
|
55
|
+
|
|
56
|
+
/* eslint-disable @typescript-eslint/no-explicit-any -- heterogeneous map: stores different InterceptorFn<M> types under different keys */
|
|
57
|
+
readonly interceptors: Map<
|
|
58
|
+
string,
|
|
59
|
+
((next: (...args: any[]) => any, ...args: any[]) => any)[]
|
|
60
|
+
>;
|
|
61
|
+
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
62
|
+
|
|
63
|
+
readonly setRootPath: (rootPath: string) => void;
|
|
64
|
+
readonly getRootPath: () => string;
|
|
65
|
+
|
|
66
|
+
readonly getTree: () => RouteTree;
|
|
67
|
+
|
|
68
|
+
readonly isDisposed: () => boolean;
|
|
69
|
+
|
|
70
|
+
validator: RouterValidator | null;
|
|
71
|
+
|
|
72
|
+
// Dependencies (issue #172)
|
|
73
|
+
readonly dependenciesGetStore: () => DependenciesStore<D>;
|
|
74
|
+
|
|
75
|
+
// Clone support (issue #173)
|
|
76
|
+
readonly cloneOptions: () => Options;
|
|
77
|
+
readonly cloneDependencies: () => Record<string, unknown>;
|
|
78
|
+
readonly getLifecycleFactories: () => [
|
|
79
|
+
Record<string, GuardFnFactory<D>>,
|
|
80
|
+
Record<string, GuardFnFactory<D>>,
|
|
81
|
+
];
|
|
82
|
+
readonly getPluginFactories: () => PluginFactory<D>[];
|
|
83
|
+
|
|
84
|
+
// Consolidated route data store (issue #174 Phase 2)
|
|
85
|
+
readonly routeGetStore: () => RoutesStore<D>;
|
|
86
|
+
|
|
87
|
+
// Cross-namespace state (issue #174)
|
|
88
|
+
readonly getStateName: () => string | undefined;
|
|
89
|
+
readonly isTransitioning: () => boolean;
|
|
90
|
+
readonly clearState: () => void;
|
|
91
|
+
readonly setState: (state: State) => void;
|
|
92
|
+
readonly routerExtensions: { keys: string[] }[];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- existential type: stores RouterInternals for all Dependencies types
|
|
96
|
+
const internals = new WeakMap<object, RouterInternals<any>>();
|
|
97
|
+
|
|
98
|
+
export function getInternals<D extends DefaultDependencies>(
|
|
99
|
+
router: RouterInterface<D>,
|
|
100
|
+
): RouterInternals<D> {
|
|
101
|
+
const ctx = internals.get(router);
|
|
102
|
+
|
|
103
|
+
if (!ctx) {
|
|
104
|
+
throw new TypeError(
|
|
105
|
+
"[real-router] Invalid router instance — not found in internals registry",
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return ctx as RouterInternals<D>;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function registerInternals<D extends DefaultDependencies>(
|
|
113
|
+
router: RouterClass<D>,
|
|
114
|
+
ctx: RouterInternals<D>,
|
|
115
|
+
): void {
|
|
116
|
+
internals.set(router, ctx);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-argument -- internal chain execution: type safety enforced at public API boundary (PluginApi.addInterceptor) */
|
|
120
|
+
function executeInterceptorChain<T>(
|
|
121
|
+
interceptors: ((next: (...args: any[]) => any, ...args: any[]) => any)[],
|
|
122
|
+
original: (...args: any[]) => T,
|
|
123
|
+
args: any[],
|
|
124
|
+
): T {
|
|
125
|
+
let chain = original as (...args: any[]) => any;
|
|
126
|
+
|
|
127
|
+
for (const interceptor of interceptors) {
|
|
128
|
+
const prev = chain;
|
|
129
|
+
|
|
130
|
+
chain = (...chainArgs: any[]) => interceptor(prev, ...chainArgs);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return chain(...args) as T;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function createInterceptable<T extends (...args: any[]) => any>(
|
|
137
|
+
name: string,
|
|
138
|
+
original: T,
|
|
139
|
+
interceptors: Map<
|
|
140
|
+
string,
|
|
141
|
+
((next: (...args: any[]) => any, ...args: any[]) => any)[]
|
|
142
|
+
>,
|
|
143
|
+
): T {
|
|
144
|
+
return ((...args: any[]) => {
|
|
145
|
+
const chain = interceptors.get(name);
|
|
146
|
+
|
|
147
|
+
if (!chain || chain.length === 0) {
|
|
148
|
+
return original(...args);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return executeInterceptorChain(chain, original, args);
|
|
152
|
+
}) as T;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function createInterceptable2<A, B, R>(
|
|
156
|
+
name: string,
|
|
157
|
+
original: (a: A, b: B) => R,
|
|
158
|
+
interceptors: Map<
|
|
159
|
+
string,
|
|
160
|
+
((next: (...args: any[]) => any, ...args: any[]) => any)[]
|
|
161
|
+
>,
|
|
162
|
+
): (a: A, b: B) => R {
|
|
163
|
+
return (arg1: A, arg2: B) => {
|
|
164
|
+
const chain = interceptors.get(name);
|
|
165
|
+
|
|
166
|
+
if (!chain || chain.length === 0) {
|
|
167
|
+
return original(arg1, arg2);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return executeInterceptorChain(chain, original, [arg1, arg2]);
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-argument */
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { DEFAULT_LIMITS } from "../../constants";
|
|
2
|
+
|
|
3
|
+
import type { Limits } from "../../types";
|
|
4
|
+
import type { DefaultDependencies } from "@real-router/types";
|
|
5
|
+
|
|
6
|
+
export interface DependenciesStore<
|
|
7
|
+
Dependencies extends DefaultDependencies = DefaultDependencies,
|
|
8
|
+
> {
|
|
9
|
+
dependencies: Partial<Dependencies>;
|
|
10
|
+
limits: Limits;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function createDependenciesStore<
|
|
14
|
+
Dependencies extends DefaultDependencies = DefaultDependencies,
|
|
15
|
+
>(
|
|
16
|
+
initialDependencies: Partial<Dependencies> = {} as Dependencies,
|
|
17
|
+
): DependenciesStore<Dependencies> {
|
|
18
|
+
const dependencies = Object.create(null) as Partial<Dependencies>;
|
|
19
|
+
|
|
20
|
+
for (const key in initialDependencies) {
|
|
21
|
+
if (initialDependencies[key] !== undefined) {
|
|
22
|
+
dependencies[key] = initialDependencies[key];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
dependencies,
|
|
28
|
+
limits: DEFAULT_LIMITS,
|
|
29
|
+
};
|
|
30
|
+
}
|