@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,54 @@
|
|
|
1
|
+
// packages/real-router/modules/transition/mergeStates.ts
|
|
2
|
+
|
|
3
|
+
import type { Params, State, StateMeta } from "@real-router/types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Merges two states with toState taking priority over fromState.
|
|
7
|
+
*
|
|
8
|
+
* Priority order for state fields: toState > fromState
|
|
9
|
+
* Priority order for meta fields: toState.meta > fromState.meta > defaults
|
|
10
|
+
*
|
|
11
|
+
* Special case: meta.params are merged (not replaced):
|
|
12
|
+
* { ...toState.meta.params, ...fromState.meta.params }
|
|
13
|
+
*
|
|
14
|
+
* @param toState - Target state (higher priority)
|
|
15
|
+
* @param fromState - Source state (lower priority)
|
|
16
|
+
* @returns New merged state object
|
|
17
|
+
*/
|
|
18
|
+
export const mergeStates = (toState: State, fromState: State): State => {
|
|
19
|
+
const toMeta = toState.meta;
|
|
20
|
+
const fromMeta = fromState.meta;
|
|
21
|
+
|
|
22
|
+
// Optimization #1: Conditional merge for params
|
|
23
|
+
// Use spread only when both are defined
|
|
24
|
+
const toParams = toMeta?.params;
|
|
25
|
+
const fromParams = fromMeta?.params;
|
|
26
|
+
|
|
27
|
+
// Both have params - need to merge; otherwise use whichever is defined
|
|
28
|
+
const metaParams: Params =
|
|
29
|
+
toParams && fromParams
|
|
30
|
+
? { ...toParams, ...fromParams }
|
|
31
|
+
: (toParams ?? fromParams ?? {});
|
|
32
|
+
|
|
33
|
+
// Optimization #2: Build meta with defaults, then apply fromMeta, then toMeta
|
|
34
|
+
// Note: StateMeta can have custom fields added by guards/middleware, so we preserve them
|
|
35
|
+
const resultMeta: StateMeta = {
|
|
36
|
+
// Defaults first
|
|
37
|
+
id: 1,
|
|
38
|
+
options: {},
|
|
39
|
+
// fromMeta fields (lower priority, may include custom fields)
|
|
40
|
+
...fromMeta,
|
|
41
|
+
// toMeta fields (higher priority, may include custom fields)
|
|
42
|
+
...toMeta,
|
|
43
|
+
// Explicitly set params to our merged version (override spread)
|
|
44
|
+
params: metaParams,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Optimization #4: Copy all toState fields (including custom ones)
|
|
48
|
+
// then explicitly set meta to our merged version
|
|
49
|
+
// Note: State can have custom fields added by middleware, so we must preserve them
|
|
50
|
+
return {
|
|
51
|
+
...toState,
|
|
52
|
+
meta: resultMeta,
|
|
53
|
+
};
|
|
54
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// packages/real-router/modules/transition/processLifecycleResult.ts
|
|
2
|
+
|
|
3
|
+
import { isPromise, isState } from "type-guards";
|
|
4
|
+
|
|
5
|
+
import { errorCodes } from "../../../constants";
|
|
6
|
+
import { RouterError } from "../../../RouterError";
|
|
7
|
+
|
|
8
|
+
import type { SyncErrorMetadata } from "./wrapSyncError";
|
|
9
|
+
import type { State, ActivationFn } from "@real-router/types";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Builds error metadata from a caught promise rejection.
|
|
13
|
+
* Extracts message, stack, and cause from Error instances.
|
|
14
|
+
*/
|
|
15
|
+
function buildErrorMetadata(
|
|
16
|
+
error_: unknown,
|
|
17
|
+
errorData: SyncErrorMetadata,
|
|
18
|
+
): SyncErrorMetadata {
|
|
19
|
+
if (error_ instanceof Error) {
|
|
20
|
+
return {
|
|
21
|
+
...errorData,
|
|
22
|
+
message: error_.message,
|
|
23
|
+
stack: error_.stack,
|
|
24
|
+
// Error.cause requires ES2022+ - safely access it if present
|
|
25
|
+
...("cause" in error_ &&
|
|
26
|
+
error_.cause !== undefined && { cause: error_.cause }),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (error_ && typeof error_ === "object") {
|
|
31
|
+
return { ...errorData, ...error_ };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return errorData;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Helper: Lifecycle results Processing Function
|
|
38
|
+
export const processLifecycleResult = async (
|
|
39
|
+
result: ReturnType<ActivationFn>,
|
|
40
|
+
currentState: State,
|
|
41
|
+
segment?: string,
|
|
42
|
+
): Promise<State> => {
|
|
43
|
+
const errorData = segment ? { segment } : {};
|
|
44
|
+
|
|
45
|
+
if (result === undefined) {
|
|
46
|
+
return currentState;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (typeof result === "boolean") {
|
|
50
|
+
if (result) {
|
|
51
|
+
return currentState;
|
|
52
|
+
} else {
|
|
53
|
+
throw new RouterError(errorCodes.TRANSITION_ERR, errorData);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (isState(result)) {
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (isPromise<State | boolean | undefined>(result)) {
|
|
62
|
+
// Optimization: single try/catch instead of .then(onFulfill, onReject)
|
|
63
|
+
try {
|
|
64
|
+
const resVal = await result;
|
|
65
|
+
|
|
66
|
+
return await processLifecycleResult(resVal, currentState, segment);
|
|
67
|
+
} catch (error_: unknown) {
|
|
68
|
+
throw new RouterError(
|
|
69
|
+
errorCodes.TRANSITION_ERR,
|
|
70
|
+
buildErrorMetadata(error_, errorData),
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// This should never be reached - all valid ActivationFn return types are handled above
|
|
76
|
+
// If we get here, it means the activation function returned an unexpected type
|
|
77
|
+
throw new RouterError(errorCodes.TRANSITION_ERR, {
|
|
78
|
+
...errorData,
|
|
79
|
+
message: `Invalid lifecycle result type: ${typeof result}`,
|
|
80
|
+
});
|
|
81
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// packages/real-router/modules/transition/wrapSyncError.ts
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Error metadata structure for transition errors.
|
|
5
|
+
* Contains information extracted from caught exceptions.
|
|
6
|
+
*/
|
|
7
|
+
export interface SyncErrorMetadata {
|
|
8
|
+
[key: string]: unknown;
|
|
9
|
+
message?: string;
|
|
10
|
+
stack?: string | undefined;
|
|
11
|
+
cause?: unknown;
|
|
12
|
+
segment?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Reserved properties that conflict with RouterError constructor
|
|
16
|
+
// Issue #39: Filter these when wrapping sync errors to avoid TypeError
|
|
17
|
+
const reservedRouterErrorProps = new Set([
|
|
18
|
+
"code",
|
|
19
|
+
"segment",
|
|
20
|
+
"path",
|
|
21
|
+
"redirect",
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Wraps a synchronously thrown value into structured error metadata.
|
|
26
|
+
*
|
|
27
|
+
* This helper extracts useful debugging information from various thrown values:
|
|
28
|
+
* - Error instances: extracts message, stack, and cause (ES2022+)
|
|
29
|
+
* - Plain objects: spreads properties into metadata
|
|
30
|
+
* - Primitives (string, number, etc.): returns minimal metadata
|
|
31
|
+
*
|
|
32
|
+
* @param thrown - The value caught in a try-catch block
|
|
33
|
+
* @param segment - Optional route segment name (for lifecycle hooks)
|
|
34
|
+
* @returns Structured error metadata for RouterError
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```typescript
|
|
38
|
+
* try {
|
|
39
|
+
* hookFn();
|
|
40
|
+
* } catch (error) {
|
|
41
|
+
* const metadata = wrapSyncError(error, "users.profile");
|
|
42
|
+
* throw new RouterError(errorCodes.TRANSITION_ERR, metadata);
|
|
43
|
+
* }
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export function wrapSyncError(
|
|
47
|
+
thrown: unknown,
|
|
48
|
+
segment?: string,
|
|
49
|
+
): SyncErrorMetadata {
|
|
50
|
+
// Base metadata - always include segment if provided
|
|
51
|
+
const base: SyncErrorMetadata = segment ? { segment } : {};
|
|
52
|
+
|
|
53
|
+
// Handle Error instances - extract all useful properties
|
|
54
|
+
if (thrown instanceof Error) {
|
|
55
|
+
return {
|
|
56
|
+
...base,
|
|
57
|
+
message: thrown.message,
|
|
58
|
+
stack: thrown.stack,
|
|
59
|
+
// Error.cause requires ES2022+ - safely access if present
|
|
60
|
+
...("cause" in thrown &&
|
|
61
|
+
thrown.cause !== undefined && { cause: thrown.cause }),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Handle plain objects - spread properties into metadata, filtering reserved props
|
|
66
|
+
if (thrown && typeof thrown === "object") {
|
|
67
|
+
const filtered: Record<string, unknown> = {};
|
|
68
|
+
|
|
69
|
+
for (const [key, value] of Object.entries(thrown)) {
|
|
70
|
+
// Issue #39: Skip reserved properties to avoid RouterError constructor TypeError
|
|
71
|
+
if (!reservedRouterErrorProps.has(key)) {
|
|
72
|
+
filtered[key] = value;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return { ...base, ...filtered };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Primitives (string, number, boolean, null, undefined, symbol, bigint)
|
|
80
|
+
// Return base metadata only - the primitive value isn't useful as metadata
|
|
81
|
+
return base;
|
|
82
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// packages/core/src/namespaces/NavigationNamespace/types.ts
|
|
2
|
+
|
|
3
|
+
import type { BuildStateResultWithSegments } from "../../types";
|
|
4
|
+
import type {
|
|
5
|
+
ActivationFn,
|
|
6
|
+
Middleware,
|
|
7
|
+
NavigationOptions,
|
|
8
|
+
Options,
|
|
9
|
+
Params,
|
|
10
|
+
State,
|
|
11
|
+
StateMetaInput,
|
|
12
|
+
TransitionPhase,
|
|
13
|
+
} from "@real-router/types";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Dependencies injected into NavigationNamespace.
|
|
17
|
+
*
|
|
18
|
+
* These are function references from other namespaces/facade,
|
|
19
|
+
* avoiding the need to pass the entire Router object.
|
|
20
|
+
*/
|
|
21
|
+
export interface NavigationDependencies {
|
|
22
|
+
/** Get router options */
|
|
23
|
+
getOptions: () => Options;
|
|
24
|
+
|
|
25
|
+
/** Check if route exists */
|
|
26
|
+
hasRoute: (name: string) => boolean;
|
|
27
|
+
|
|
28
|
+
/** Get current state */
|
|
29
|
+
getState: () => State | undefined;
|
|
30
|
+
|
|
31
|
+
/** Set router state */
|
|
32
|
+
setState: (state?: State) => void;
|
|
33
|
+
|
|
34
|
+
/** Build state with segments from route name and params */
|
|
35
|
+
buildStateWithSegments: <P extends Params = Params>(
|
|
36
|
+
routeName: string,
|
|
37
|
+
routeParams: P,
|
|
38
|
+
) => BuildStateResultWithSegments<P> | undefined;
|
|
39
|
+
|
|
40
|
+
/** Make state object with path and meta */
|
|
41
|
+
makeState: <P extends Params = Params, MP extends Params = Params>(
|
|
42
|
+
name: string,
|
|
43
|
+
params?: P,
|
|
44
|
+
path?: string,
|
|
45
|
+
meta?: StateMetaInput<MP>,
|
|
46
|
+
) => State<P, MP>;
|
|
47
|
+
|
|
48
|
+
/** Build path from route name and params */
|
|
49
|
+
buildPath: (route: string, params?: Params) => string;
|
|
50
|
+
|
|
51
|
+
/** Check if states are equal */
|
|
52
|
+
areStatesEqual: (
|
|
53
|
+
state1: State | undefined,
|
|
54
|
+
state2: State | undefined,
|
|
55
|
+
ignoreQueryParams?: boolean,
|
|
56
|
+
) => boolean;
|
|
57
|
+
|
|
58
|
+
/** Get a dependency by name (untyped — used only for resolveOption) */
|
|
59
|
+
getDependency: (name: string) => unknown;
|
|
60
|
+
|
|
61
|
+
/** Start transition and send NAVIGATE event to routerFSM */
|
|
62
|
+
startTransition: (toState: State, fromState: State | undefined) => void;
|
|
63
|
+
|
|
64
|
+
/** Cancel navigation if transition is running */
|
|
65
|
+
cancelNavigation: () => void;
|
|
66
|
+
|
|
67
|
+
/** Send COMPLETE event to routerFSM */
|
|
68
|
+
sendTransitionDone: (
|
|
69
|
+
state: State,
|
|
70
|
+
fromState: State | undefined,
|
|
71
|
+
opts: NavigationOptions,
|
|
72
|
+
) => void;
|
|
73
|
+
|
|
74
|
+
/** Send FAIL event to routerFSM (transition blocked) */
|
|
75
|
+
sendTransitionBlocked: (
|
|
76
|
+
toState: State,
|
|
77
|
+
fromState: State | undefined,
|
|
78
|
+
error: unknown,
|
|
79
|
+
) => void;
|
|
80
|
+
|
|
81
|
+
/** Send FAIL event to routerFSM (transition error) */
|
|
82
|
+
sendTransitionError: (
|
|
83
|
+
toState: State,
|
|
84
|
+
fromState: State | undefined,
|
|
85
|
+
error: unknown,
|
|
86
|
+
) => void;
|
|
87
|
+
|
|
88
|
+
/** Emit TRANSITION_ERROR event to listeners */
|
|
89
|
+
emitTransitionError: (
|
|
90
|
+
toState: State | undefined,
|
|
91
|
+
fromState: State | undefined,
|
|
92
|
+
error: unknown,
|
|
93
|
+
) => void;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface TransitionOutput {
|
|
97
|
+
state: State;
|
|
98
|
+
meta: {
|
|
99
|
+
phase: TransitionPhase;
|
|
100
|
+
segments: {
|
|
101
|
+
deactivated: string[];
|
|
102
|
+
activated: string[];
|
|
103
|
+
intersection: string;
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Dependencies required for the transition function.
|
|
110
|
+
*/
|
|
111
|
+
export interface TransitionDependencies {
|
|
112
|
+
/** Get lifecycle functions (canDeactivate, canActivate maps) */
|
|
113
|
+
getLifecycleFunctions: () => [
|
|
114
|
+
Map<string, ActivationFn>,
|
|
115
|
+
Map<string, ActivationFn>,
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
/** Get middleware functions array */
|
|
119
|
+
getMiddlewareFunctions: () => Middleware[];
|
|
120
|
+
|
|
121
|
+
/** Check if router is active (for cancellation check on stop()) */
|
|
122
|
+
isActive: () => boolean;
|
|
123
|
+
|
|
124
|
+
/** Check if a transition is currently in progress */
|
|
125
|
+
isTransitioning: () => boolean;
|
|
126
|
+
|
|
127
|
+
/** Clear canDeactivate guard for a route */
|
|
128
|
+
clearCanDeactivate: (name: string) => void;
|
|
129
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// packages/core/src/namespaces/NavigationNamespace/validators.ts
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Static validation functions for NavigationNamespace.
|
|
5
|
+
* Called by Router facade before instance methods.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { getTypeDescription, isNavigationOptions } from "type-guards";
|
|
9
|
+
|
|
10
|
+
import type { NavigationOptions, State } from "@real-router/types";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Validates navigate route name argument.
|
|
14
|
+
*/
|
|
15
|
+
export function validateNavigateArgs(name: unknown): asserts name is string {
|
|
16
|
+
if (typeof name !== "string") {
|
|
17
|
+
throw new TypeError(
|
|
18
|
+
`[router.navigate] Invalid route name: expected string, got ${getTypeDescription(name)}`,
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Validates navigateToState arguments.
|
|
25
|
+
*/
|
|
26
|
+
export function validateNavigateToStateArgs(
|
|
27
|
+
toState: unknown,
|
|
28
|
+
fromState: unknown,
|
|
29
|
+
opts: unknown,
|
|
30
|
+
): void {
|
|
31
|
+
// toState must be a valid state object
|
|
32
|
+
if (
|
|
33
|
+
!toState ||
|
|
34
|
+
typeof toState !== "object" ||
|
|
35
|
+
typeof (toState as State).name !== "string" ||
|
|
36
|
+
typeof (toState as State).path !== "string"
|
|
37
|
+
) {
|
|
38
|
+
throw new TypeError(
|
|
39
|
+
`[router.navigateToState] Invalid toState: expected State object with name and path`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// fromState can be undefined or a valid state
|
|
44
|
+
if (
|
|
45
|
+
fromState !== undefined &&
|
|
46
|
+
(!fromState ||
|
|
47
|
+
typeof fromState !== "object" ||
|
|
48
|
+
typeof (fromState as State).name !== "string")
|
|
49
|
+
) {
|
|
50
|
+
throw new TypeError(
|
|
51
|
+
`[router.navigateToState] Invalid fromState: expected State object or undefined`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// opts must be an object
|
|
56
|
+
if (typeof opts !== "object" || opts === null) {
|
|
57
|
+
throw new TypeError(
|
|
58
|
+
`[router.navigateToState] Invalid opts: expected NavigationOptions object, got ${getTypeDescription(opts)}`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Validates navigateToDefault arguments.
|
|
65
|
+
*/
|
|
66
|
+
export function validateNavigateToDefaultArgs(opts: unknown): void {
|
|
67
|
+
// If opts is provided, it must be an object (NavigationOptions)
|
|
68
|
+
if (opts !== undefined && (typeof opts !== "object" || opts === null)) {
|
|
69
|
+
throw new TypeError(
|
|
70
|
+
`[router.navigateToDefault] Invalid options: ${getTypeDescription(opts)}. Expected NavigationOptions object.`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Validates that opts is a valid NavigationOptions object.
|
|
77
|
+
*/
|
|
78
|
+
export function validateNavigationOptions(
|
|
79
|
+
opts: unknown,
|
|
80
|
+
methodName: string,
|
|
81
|
+
): asserts opts is NavigationOptions {
|
|
82
|
+
if (!isNavigationOptions(opts)) {
|
|
83
|
+
throw new TypeError(
|
|
84
|
+
`[router.${methodName}] Invalid options: ${getTypeDescription(opts)}. Expected NavigationOptions object.`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// packages/core/src/namespaces/OptionsNamespace/OptionsNamespace.ts
|
|
2
|
+
|
|
3
|
+
import { defaultOptions } from "./constants";
|
|
4
|
+
import { deepFreeze } from "./helpers";
|
|
5
|
+
import { validateOptions } from "./validators";
|
|
6
|
+
|
|
7
|
+
import type { Options } from "@real-router/types";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Independent namespace for managing router options.
|
|
11
|
+
*
|
|
12
|
+
* Options are immutable after construction.
|
|
13
|
+
* Static methods handle validation (called by facade).
|
|
14
|
+
* Instance methods provide read-only access.
|
|
15
|
+
*/
|
|
16
|
+
export class OptionsNamespace {
|
|
17
|
+
readonly #options: Readonly<Options>;
|
|
18
|
+
|
|
19
|
+
constructor(initialOptions: Partial<Options> = {}) {
|
|
20
|
+
// Note: validation should be done by facade before calling constructor
|
|
21
|
+
this.#options = deepFreeze({
|
|
22
|
+
...defaultOptions,
|
|
23
|
+
...initialOptions,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// =========================================================================
|
|
28
|
+
// Static validation methods (called by facade before instance methods)
|
|
29
|
+
// Proxy to functions in validators.ts for separation of concerns
|
|
30
|
+
// =========================================================================
|
|
31
|
+
|
|
32
|
+
static validateOptions(
|
|
33
|
+
options: unknown,
|
|
34
|
+
methodName: string,
|
|
35
|
+
): asserts options is Partial<Options> {
|
|
36
|
+
validateOptions(options, methodName);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// =========================================================================
|
|
40
|
+
// Instance methods (read-only access)
|
|
41
|
+
// =========================================================================
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Returns the frozen options object.
|
|
45
|
+
* Safe to return directly - mutations will throw in strict mode.
|
|
46
|
+
*/
|
|
47
|
+
get(): Readonly<Options> {
|
|
48
|
+
return this.#options;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// packages/core/src/namespaces/OptionsNamespace/constants.ts
|
|
2
|
+
|
|
3
|
+
import type { Options } from "@real-router/types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Default options for the router.
|
|
7
|
+
*/
|
|
8
|
+
export const defaultOptions: Options = {
|
|
9
|
+
defaultRoute: "",
|
|
10
|
+
defaultParams: {},
|
|
11
|
+
trailingSlash: "preserve",
|
|
12
|
+
queryParamsMode: "loose",
|
|
13
|
+
queryParams: {
|
|
14
|
+
arrayFormat: "none",
|
|
15
|
+
booleanFormat: "none",
|
|
16
|
+
nullFormat: "default",
|
|
17
|
+
},
|
|
18
|
+
urlParamsEncoding: "default",
|
|
19
|
+
allowNotFound: true,
|
|
20
|
+
rewritePathOnMatch: true,
|
|
21
|
+
noValidate: false,
|
|
22
|
+
} satisfies Options;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Valid values for string enum options.
|
|
26
|
+
* Used for runtime validation in constructor options.
|
|
27
|
+
*/
|
|
28
|
+
export const VALID_OPTION_VALUES = {
|
|
29
|
+
trailingSlash: ["strict", "never", "always", "preserve"] as const,
|
|
30
|
+
queryParamsMode: ["default", "strict", "loose"] as const,
|
|
31
|
+
urlParamsEncoding: ["default", "uri", "uriComponent", "none"] as const,
|
|
32
|
+
} as const;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Valid keys and values for queryParams option.
|
|
36
|
+
*/
|
|
37
|
+
export const VALID_QUERY_PARAMS = {
|
|
38
|
+
arrayFormat: ["none", "brackets", "index", "comma"] as const,
|
|
39
|
+
booleanFormat: ["none", "string", "empty-true"] as const,
|
|
40
|
+
nullFormat: ["default", "hidden"] as const,
|
|
41
|
+
} as const;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// packages/core/src/namespaces/OptionsNamespace/helpers.ts
|
|
2
|
+
|
|
3
|
+
import type { Options, Params } from "@real-router/types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Recursively freezes an object and all nested objects.
|
|
7
|
+
* Only freezes plain objects, not primitives or special objects.
|
|
8
|
+
*/
|
|
9
|
+
export function deepFreeze<T extends object>(obj: T): Readonly<T> {
|
|
10
|
+
Object.freeze(obj);
|
|
11
|
+
|
|
12
|
+
for (const key of Object.keys(obj)) {
|
|
13
|
+
const value = (obj as Record<string, unknown>)[key];
|
|
14
|
+
|
|
15
|
+
if (value && typeof value === "object" && value.constructor === Object) {
|
|
16
|
+
deepFreeze(value);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return obj;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Resolves an option value that can be static or a callback.
|
|
25
|
+
* If the value is a function, calls it with getDependency and returns the result.
|
|
26
|
+
* Otherwise, returns the value as-is.
|
|
27
|
+
*/
|
|
28
|
+
export function resolveOption(
|
|
29
|
+
value: Options["defaultRoute"],
|
|
30
|
+
getDependency: (name: string) => unknown,
|
|
31
|
+
): string;
|
|
32
|
+
|
|
33
|
+
export function resolveOption(
|
|
34
|
+
value: Options["defaultParams"],
|
|
35
|
+
getDependency: (name: string) => unknown,
|
|
36
|
+
): Params;
|
|
37
|
+
|
|
38
|
+
// eslint-disable-next-line sonarjs/function-return-type -- overloads: string for defaultRoute, Params for defaultParams
|
|
39
|
+
export function resolveOption(
|
|
40
|
+
value: Options["defaultRoute"] | Options["defaultParams"],
|
|
41
|
+
getDependency: (name: string) => unknown,
|
|
42
|
+
): string | Params {
|
|
43
|
+
if (typeof value === "function") {
|
|
44
|
+
// Runtime getDependency is (name: string) => unknown, but DefaultRouteCallback<object>
|
|
45
|
+
// expects <K extends keyof object>(name: K) => object[K] where keyof object = never.
|
|
46
|
+
// Cast needed to bridge generic constraint mismatch.
|
|
47
|
+
return value(getDependency as never);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// packages/core/src/namespaces/OptionsNamespace/index.ts
|
|
2
|
+
|
|
3
|
+
export { OptionsNamespace } from "./OptionsNamespace";
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
defaultOptions,
|
|
7
|
+
VALID_OPTION_VALUES,
|
|
8
|
+
VALID_QUERY_PARAMS,
|
|
9
|
+
} from "./constants";
|
|
10
|
+
|
|
11
|
+
export { deepFreeze, resolveOption } from "./helpers";
|