@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,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";