@real-router/core 0.25.4 → 0.26.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 (49) hide show
  1. package/README.md +163 -325
  2. package/dist/cjs/index.d.ts +47 -178
  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 +47 -178
  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 +3 -3
  11. package/src/Router.ts +84 -574
  12. package/src/api/cloneRouter.ts +106 -0
  13. package/src/api/getDependenciesApi.ts +216 -0
  14. package/src/api/getLifecycleApi.ts +67 -0
  15. package/src/api/getPluginApi.ts +118 -0
  16. package/src/api/getRoutesApi.ts +566 -0
  17. package/src/api/index.ts +16 -0
  18. package/src/api/types.ts +7 -0
  19. package/src/getNavigator.ts +5 -2
  20. package/src/index.ts +17 -3
  21. package/src/internals.ts +115 -0
  22. package/src/namespaces/DependenciesNamespace/dependenciesStore.ts +30 -0
  23. package/src/namespaces/DependenciesNamespace/index.ts +3 -1
  24. package/src/namespaces/DependenciesNamespace/validators.ts +2 -4
  25. package/src/namespaces/EventBusNamespace/EventBusNamespace.ts +1 -20
  26. package/src/namespaces/EventBusNamespace/validators.ts +36 -0
  27. package/src/namespaces/NavigationNamespace/NavigationNamespace.ts +1 -10
  28. package/src/namespaces/NavigationNamespace/transition/errorHandling.ts +2 -0
  29. package/src/namespaces/NavigationNamespace/transition/{executeLifecycleHooks.ts → executeLifecycleGuards.ts} +9 -7
  30. package/src/namespaces/NavigationNamespace/transition/index.ts +3 -3
  31. package/src/namespaces/RouteLifecycleNamespace/RouteLifecycleNamespace.ts +1 -16
  32. package/src/namespaces/RoutesNamespace/RoutesNamespace.ts +133 -1089
  33. package/src/namespaces/RoutesNamespace/forwardToValidation.ts +411 -0
  34. package/src/namespaces/RoutesNamespace/helpers.ts +1 -407
  35. package/src/namespaces/RoutesNamespace/index.ts +2 -0
  36. package/src/namespaces/RoutesNamespace/routesStore.ts +388 -0
  37. package/src/namespaces/RoutesNamespace/validators.ts +209 -3
  38. package/src/namespaces/StateNamespace/StateNamespace.ts +1 -44
  39. package/src/namespaces/StateNamespace/validators.ts +46 -0
  40. package/src/namespaces/index.ts +3 -5
  41. package/src/types.ts +12 -138
  42. package/src/wiring/RouterWiringBuilder.ts +30 -36
  43. package/src/wiring/types.ts +3 -6
  44. package/src/wiring/wireRouter.ts +0 -1
  45. package/src/namespaces/CloneNamespace/CloneNamespace.ts +0 -120
  46. package/src/namespaces/CloneNamespace/index.ts +0 -3
  47. package/src/namespaces/CloneNamespace/types.ts +0 -42
  48. package/src/namespaces/DependenciesNamespace/DependenciesNamespace.ts +0 -248
  49. package/src/namespaces/RoutesNamespace/stateBuilder.ts +0 -70
@@ -0,0 +1,106 @@
1
+ import { routeTreeToDefinitions } from "route-tree";
2
+ import { getTypeDescription } from "type-guards";
3
+
4
+ import { errorCodes } from "../constants";
5
+ import { getInternals } from "../internals";
6
+ import { Router as RouterClass } from "../Router";
7
+ import { RouterError } from "../RouterError";
8
+ import { getLifecycleApi } from "./getLifecycleApi";
9
+
10
+ import type { Route } from "../types";
11
+ import type { DefaultDependencies, Router } from "@real-router/types";
12
+
13
+ function validateCloneArgs(dependencies: unknown): void {
14
+ if (dependencies === undefined) {
15
+ return;
16
+ }
17
+
18
+ if (
19
+ !(
20
+ dependencies &&
21
+ typeof dependencies === "object" &&
22
+ dependencies.constructor === Object
23
+ )
24
+ ) {
25
+ throw new TypeError(
26
+ `[cloneRouter] Invalid dependencies: expected plain object or undefined, received ${getTypeDescription(dependencies)}`,
27
+ );
28
+ }
29
+
30
+ for (const key in dependencies) {
31
+ if (Object.getOwnPropertyDescriptor(dependencies, key)?.get) {
32
+ throw new TypeError(
33
+ `[cloneRouter] Getters not allowed in dependencies: "${key}"`,
34
+ );
35
+ }
36
+ }
37
+ }
38
+
39
+ export function cloneRouter<
40
+ Dependencies extends DefaultDependencies = DefaultDependencies,
41
+ >(
42
+ router: Router<Dependencies>,
43
+ dependencies?: Dependencies,
44
+ ): RouterClass<Dependencies> {
45
+ const ctx = getInternals(router);
46
+
47
+ if (ctx.isDisposed()) {
48
+ throw new RouterError(errorCodes.ROUTER_DISPOSED);
49
+ }
50
+
51
+ if (!ctx.noValidate) {
52
+ validateCloneArgs(dependencies);
53
+ }
54
+
55
+ // Get source store directly
56
+ const sourceStore = ctx.routeGetStore();
57
+ const routes = routeTreeToDefinitions(sourceStore.tree);
58
+ const routeConfig = sourceStore.config;
59
+ const resolvedForwardMap = sourceStore.resolvedForwardMap;
60
+ const routeCustomFields = sourceStore.routeCustomFields;
61
+
62
+ const options = ctx.cloneOptions();
63
+ const sourceDeps = ctx.cloneDependencies();
64
+ const [canDeactivateFactories, canActivateFactories] =
65
+ ctx.getLifecycleFactories();
66
+ const pluginFactories = ctx.getPluginFactories();
67
+
68
+ const mergedDeps = {
69
+ ...sourceDeps,
70
+ ...dependencies,
71
+ } as Dependencies;
72
+
73
+ const newRouter = new RouterClass<Dependencies>(
74
+ routes as Route<Dependencies>[],
75
+ options,
76
+ mergedDeps,
77
+ );
78
+
79
+ const lifecycle = getLifecycleApi(newRouter);
80
+
81
+ for (const [name, handler] of Object.entries(canDeactivateFactories)) {
82
+ lifecycle.addDeactivateGuard(name, handler);
83
+ }
84
+
85
+ for (const [name, handler] of Object.entries(canActivateFactories)) {
86
+ lifecycle.addActivateGuard(name, handler);
87
+ }
88
+
89
+ if (pluginFactories.length > 0) {
90
+ newRouter.usePlugin(...pluginFactories);
91
+ }
92
+
93
+ const newCtx = getInternals(newRouter);
94
+ const newStore = newCtx.routeGetStore();
95
+
96
+ // Apply cloned config directly to new store
97
+ Object.assign(newStore.config.decoders, routeConfig.decoders);
98
+ Object.assign(newStore.config.encoders, routeConfig.encoders);
99
+ Object.assign(newStore.config.defaultParams, routeConfig.defaultParams);
100
+ Object.assign(newStore.config.forwardMap, routeConfig.forwardMap);
101
+ Object.assign(newStore.config.forwardFnMap, routeConfig.forwardFnMap);
102
+ Object.assign(newStore.resolvedForwardMap, resolvedForwardMap);
103
+ Object.assign(newStore.routeCustomFields, routeCustomFields);
104
+
105
+ return newRouter;
106
+ }
@@ -0,0 +1,216 @@
1
+ import { logger } from "@real-router/logger";
2
+ import { getTypeDescription } from "type-guards";
3
+
4
+ import { errorCodes } from "../constants";
5
+ import { computeThresholds } from "../helpers";
6
+ import { getInternals } from "../internals";
7
+ import {
8
+ validateDependenciesObject,
9
+ validateDependencyExists,
10
+ validateDependencyLimit,
11
+ validateDependencyName,
12
+ validateSetDependencyArgs,
13
+ } from "../namespaces/DependenciesNamespace/validators";
14
+ import { RouterError } from "../RouterError";
15
+
16
+ import type { DependenciesApi } from "./types";
17
+ import type { DependenciesStore } from "../namespaces/DependenciesNamespace/dependenciesStore";
18
+ import type { DefaultDependencies, Router } from "@real-router/types";
19
+
20
+ function throwIfDisposed(isDisposed: () => boolean): void {
21
+ if (isDisposed()) {
22
+ throw new RouterError(errorCodes.ROUTER_DISPOSED);
23
+ }
24
+ }
25
+
26
+ // =============================================================================
27
+ // Module-private CRUD functions
28
+ // =============================================================================
29
+
30
+ function checkDependencyCount(
31
+ store: DependenciesStore,
32
+ methodName: string,
33
+ ): void {
34
+ const maxDependencies = store.limits.maxDependencies;
35
+
36
+ if (maxDependencies === 0) {
37
+ return;
38
+ }
39
+
40
+ const currentCount = Object.keys(store.dependencies).length;
41
+
42
+ const { warn, error } = computeThresholds(maxDependencies);
43
+
44
+ if (currentCount === warn) {
45
+ logger.warn(
46
+ `router.${methodName}`,
47
+ `${warn} dependencies registered. ` + `Consider if all are necessary.`,
48
+ );
49
+ } else if (currentCount === error) {
50
+ logger.error(
51
+ `router.${methodName}`,
52
+ `${error} dependencies registered! ` +
53
+ `This indicates architectural problems. ` +
54
+ `Hard limit at ${maxDependencies}.`,
55
+ );
56
+ } else if (currentCount >= maxDependencies) {
57
+ throw new Error(
58
+ `[router.${methodName}] Dependency limit exceeded (${maxDependencies}). ` +
59
+ `Current: ${currentCount}. This is likely a bug in your code. ` +
60
+ `If you genuinely need more dependencies, your architecture needs refactoring.`,
61
+ );
62
+ }
63
+ }
64
+
65
+ function setDependency(
66
+ store: DependenciesStore,
67
+ dependencyName: string,
68
+ dependencyValue: unknown,
69
+ ): boolean {
70
+ // undefined = "don't set" (feature for conditional setting)
71
+ if (dependencyValue === undefined) {
72
+ return false;
73
+ }
74
+
75
+ const isNewKey = !Object.hasOwn(store.dependencies, dependencyName);
76
+
77
+ if (isNewKey) {
78
+ // Only check limit when adding new keys (overwrites don't increase count)
79
+ checkDependencyCount(store, "setDependency");
80
+ } else {
81
+ const oldValue = (store.dependencies as Record<string, unknown>)[
82
+ dependencyName
83
+ ];
84
+ const isChanging = oldValue !== dependencyValue;
85
+ // Special case for NaN idempotency (NaN !== NaN is always true)
86
+ const bothAreNaN = Number.isNaN(oldValue) && Number.isNaN(dependencyValue);
87
+
88
+ if (isChanging && !bothAreNaN) {
89
+ logger.warn(
90
+ "router.setDependency",
91
+ "Router dependency already exists and is being overwritten:",
92
+ dependencyName,
93
+ );
94
+ }
95
+ }
96
+
97
+ (store.dependencies as Record<string, unknown>)[dependencyName] =
98
+ dependencyValue;
99
+
100
+ return true;
101
+ }
102
+
103
+ function setMultipleDependencies(
104
+ store: DependenciesStore,
105
+ deps: Record<string, unknown>,
106
+ ): void {
107
+ const overwrittenKeys: string[] = [];
108
+
109
+ for (const key in deps) {
110
+ if (deps[key] !== undefined) {
111
+ if (Object.hasOwn(store.dependencies, key)) {
112
+ overwrittenKeys.push(key);
113
+ }
114
+
115
+ (store.dependencies as Record<string, unknown>)[key] = deps[key];
116
+ }
117
+ }
118
+
119
+ if (overwrittenKeys.length > 0) {
120
+ logger.warn(
121
+ "router.setDependencies",
122
+ "Overwritten:",
123
+ overwrittenKeys.join(", "),
124
+ );
125
+ }
126
+ }
127
+
128
+ // =============================================================================
129
+ // Public API factory
130
+ // =============================================================================
131
+
132
+ export function getDependenciesApi<
133
+ Dependencies extends DefaultDependencies = DefaultDependencies,
134
+ >(router: Router<Dependencies>): DependenciesApi<Dependencies> {
135
+ const ctx = getInternals(router);
136
+
137
+ return {
138
+ get: (name) => {
139
+ if (!ctx.noValidate) {
140
+ validateDependencyName(name, "getDependency");
141
+ }
142
+
143
+ const store = ctx.dependenciesGetStore();
144
+ const value = (store.dependencies as Record<string, unknown>)[
145
+ name as string
146
+ ];
147
+
148
+ if (!ctx.noValidate) {
149
+ validateDependencyExists(value, name as string);
150
+ }
151
+
152
+ return value as Dependencies[typeof name];
153
+ },
154
+ getAll: () => ({ ...ctx.dependenciesGetStore().dependencies }),
155
+ set: (name, value) => {
156
+ throwIfDisposed(ctx.isDisposed);
157
+
158
+ if (!ctx.noValidate) {
159
+ validateSetDependencyArgs(name);
160
+ }
161
+
162
+ setDependency(ctx.dependenciesGetStore(), name as string, value);
163
+ },
164
+ setAll: (deps) => {
165
+ throwIfDisposed(ctx.isDisposed);
166
+
167
+ const store = ctx.dependenciesGetStore();
168
+
169
+ if (!ctx.noValidate) {
170
+ validateDependenciesObject(deps, "setDependencies");
171
+ validateDependencyLimit(
172
+ Object.keys(store.dependencies).length,
173
+ Object.keys(deps).length,
174
+ "setDependencies",
175
+ store.limits.maxDependencies,
176
+ );
177
+ }
178
+
179
+ setMultipleDependencies(store, deps as Record<string, unknown>);
180
+ },
181
+ remove: (name) => {
182
+ throwIfDisposed(ctx.isDisposed);
183
+
184
+ if (!ctx.noValidate) {
185
+ validateDependencyName(name, "removeDependency");
186
+ }
187
+
188
+ const store = ctx.dependenciesGetStore();
189
+
190
+ if (!Object.hasOwn(store.dependencies, name as string)) {
191
+ logger.warn(
192
+ `router.removeDependency`,
193
+ `Attempted to remove non-existent dependency: "${getTypeDescription(name)}"`,
194
+ );
195
+ }
196
+
197
+ delete (store.dependencies as Record<string, unknown>)[name as string];
198
+ },
199
+ reset: () => {
200
+ throwIfDisposed(ctx.isDisposed);
201
+ const store = ctx.dependenciesGetStore();
202
+
203
+ store.dependencies = Object.create(null) as Partial<Dependencies>;
204
+ },
205
+ has: (name) => {
206
+ if (!ctx.noValidate) {
207
+ validateDependencyName(name, "hasDependency");
208
+ }
209
+
210
+ return Object.hasOwn(
211
+ ctx.dependenciesGetStore().dependencies,
212
+ name as string,
213
+ );
214
+ },
215
+ };
216
+ }
@@ -0,0 +1,67 @@
1
+ import { validateRouteName } from "type-guards";
2
+
3
+ import { errorCodes } from "../constants";
4
+ import { getInternals } from "../internals";
5
+ import { validateHandler } from "../namespaces/RouteLifecycleNamespace/validators";
6
+ import { RouterError } from "../RouterError";
7
+
8
+ import type { LifecycleApi } from "./types";
9
+ import type { DefaultDependencies, Router } from "@real-router/types";
10
+
11
+ function throwIfDisposed(isDisposed: () => boolean): void {
12
+ if (isDisposed()) {
13
+ throw new RouterError(errorCodes.ROUTER_DISPOSED);
14
+ }
15
+ }
16
+
17
+ export function getLifecycleApi<
18
+ Dependencies extends DefaultDependencies = DefaultDependencies,
19
+ >(router: Router<Dependencies>): LifecycleApi<Dependencies> {
20
+ const ctx = getInternals(router);
21
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- guaranteed set after wiring
22
+ const lifecycleNamespace = ctx.routeGetStore().lifecycleNamespace!;
23
+
24
+ return {
25
+ addActivateGuard(name, handler) {
26
+ throwIfDisposed(ctx.isDisposed);
27
+
28
+ if (!ctx.noValidate) {
29
+ validateRouteName(name, "addActivateGuard");
30
+ validateHandler(handler, "addActivateGuard");
31
+ }
32
+
33
+ lifecycleNamespace.addCanActivate(name, handler, ctx.noValidate);
34
+ },
35
+
36
+ addDeactivateGuard(name, handler) {
37
+ throwIfDisposed(ctx.isDisposed);
38
+
39
+ if (!ctx.noValidate) {
40
+ validateRouteName(name, "addDeactivateGuard");
41
+ validateHandler(handler, "addDeactivateGuard");
42
+ }
43
+
44
+ lifecycleNamespace.addCanDeactivate(name, handler, ctx.noValidate);
45
+ },
46
+
47
+ removeActivateGuard(name) {
48
+ throwIfDisposed(ctx.isDisposed);
49
+
50
+ if (!ctx.noValidate) {
51
+ validateRouteName(name, "removeActivateGuard");
52
+ }
53
+
54
+ lifecycleNamespace.clearCanActivate(name);
55
+ },
56
+
57
+ removeDeactivateGuard(name) {
58
+ throwIfDisposed(ctx.isDisposed);
59
+
60
+ if (!ctx.noValidate) {
61
+ validateRouteName(name, "removeDeactivateGuard");
62
+ }
63
+
64
+ lifecycleNamespace.clearCanDeactivate(name);
65
+ },
66
+ };
67
+ }
@@ -0,0 +1,118 @@
1
+ import { errorCodes } from "../constants";
2
+ import { getInternals } from "../internals";
3
+ import { validateListenerArgs } from "../namespaces/EventBusNamespace/validators";
4
+ import { validateNavigateToStateArgs } from "../namespaces/NavigationNamespace/validators";
5
+ import {
6
+ validateMatchPathArgs,
7
+ validateSetRootPathArgs,
8
+ validateStateBuilderArgs,
9
+ } from "../namespaces/RoutesNamespace/validators";
10
+ import { validateMakeStateArgs } from "../namespaces/StateNamespace/validators";
11
+ import { RouterError } from "../RouterError";
12
+
13
+ import type { PluginApi } from "./types";
14
+ import type { DefaultDependencies, Router } from "@real-router/types";
15
+
16
+ function throwIfDisposed(isDisposed: () => boolean): void {
17
+ if (isDisposed()) {
18
+ throw new RouterError(errorCodes.ROUTER_DISPOSED);
19
+ }
20
+ }
21
+
22
+ export function getPluginApi<
23
+ Dependencies extends DefaultDependencies = DefaultDependencies,
24
+ >(router: Router<Dependencies>): PluginApi {
25
+ const ctx = getInternals(router);
26
+
27
+ return {
28
+ makeState: (name, params, path, meta, forceId) => {
29
+ if (!ctx.noValidate) {
30
+ validateMakeStateArgs(name, params, path, forceId);
31
+ }
32
+
33
+ return ctx.makeState(name, params, path, meta, forceId);
34
+ },
35
+ buildState: (routeName, routeParams) => {
36
+ if (!ctx.noValidate) {
37
+ validateStateBuilderArgs(routeName, routeParams, "buildState");
38
+ }
39
+
40
+ const { name, params } = ctx.forwardState(routeName, routeParams);
41
+
42
+ return ctx.buildStateResolved(name, params);
43
+ },
44
+ forwardState: (routeName, routeParams) => {
45
+ if (!ctx.noValidate) {
46
+ validateStateBuilderArgs(routeName, routeParams, "forwardState");
47
+ }
48
+
49
+ return ctx.forwardState(routeName, routeParams);
50
+ },
51
+ matchPath: (path) => {
52
+ if (!ctx.noValidate) {
53
+ validateMatchPathArgs(path);
54
+ }
55
+
56
+ return ctx.matchPath(path, ctx.getOptions());
57
+ },
58
+ setRootPath: (rootPath) => {
59
+ throwIfDisposed(ctx.isDisposed);
60
+
61
+ if (!ctx.noValidate) {
62
+ validateSetRootPathArgs(rootPath);
63
+ }
64
+
65
+ ctx.setRootPath(rootPath);
66
+ },
67
+ getRootPath: ctx.getRootPath,
68
+ navigateToState: (toState, fromState, opts) => {
69
+ throwIfDisposed(ctx.isDisposed);
70
+
71
+ if (!ctx.noValidate) {
72
+ validateNavigateToStateArgs(toState, fromState, opts);
73
+ }
74
+
75
+ return ctx.navigateToState(toState, fromState, opts);
76
+ },
77
+ addEventListener: (eventName, cb) => {
78
+ throwIfDisposed(ctx.isDisposed);
79
+
80
+ if (!ctx.noValidate) {
81
+ validateListenerArgs(eventName, cb);
82
+ }
83
+
84
+ return ctx.addEventListener(eventName, cb);
85
+ },
86
+ buildNavigationState: (name, params = {}) => {
87
+ if (!ctx.noValidate) {
88
+ validateStateBuilderArgs(name, params, "buildNavigationState");
89
+ }
90
+
91
+ const { name: resolvedName, params: resolvedParams } = ctx.forwardState(
92
+ name,
93
+ params,
94
+ );
95
+ const routeInfo = ctx.buildStateResolved(resolvedName, resolvedParams);
96
+
97
+ if (!routeInfo) {
98
+ return;
99
+ }
100
+
101
+ return ctx.makeState(
102
+ routeInfo.name,
103
+ routeInfo.params,
104
+ ctx.buildPath(routeInfo.name, routeInfo.params),
105
+ {
106
+ params: routeInfo.meta,
107
+ options: {},
108
+ },
109
+ );
110
+ },
111
+ getOptions: ctx.getOptions,
112
+ getTree: ctx.getTree,
113
+ getForwardState: () => ctx.forwardState,
114
+ setForwardState: (fn) => {
115
+ ctx.forwardState = fn;
116
+ },
117
+ };
118
+ }