@real-router/core 0.38.0 → 0.40.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 +27 -5
- package/dist/cjs/Router-B-Pev7K2.d.ts +46 -0
- package/dist/cjs/RouterValidator-mx2Zooya.d.ts +136 -0
- package/dist/cjs/api.d.ts +2 -1
- package/dist/cjs/api.js +1 -1
- package/dist/cjs/api.js.map +1 -1
- package/dist/cjs/index.d-y2b-8_3Y.d.ts +236 -0
- package/dist/cjs/index.d.ts +7 -24
- 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/cjs/utils.d.ts +6 -1
- package/dist/cjs/utils.js +1 -1
- package/dist/cjs/utils.js.map +1 -1
- package/dist/cjs/validation.d.ts +184 -0
- package/dist/cjs/validation.js +1 -0
- package/dist/cjs/validation.js.map +1 -0
- package/dist/esm/Router-B-Pev7K2.d.mts +46 -0
- package/dist/esm/RouterValidator-mx2Zooya.d.mts +136 -0
- package/dist/esm/api.d.mts +2 -1
- package/dist/esm/api.mjs +1 -1
- package/dist/esm/api.mjs.map +1 -1
- package/dist/esm/chunk-5QXFUUDL.mjs +1 -0
- package/dist/esm/chunk-5QXFUUDL.mjs.map +1 -0
- package/dist/esm/chunk-HHIXK5UM.mjs +1 -0
- package/dist/esm/chunk-HHIXK5UM.mjs.map +1 -0
- package/dist/esm/chunk-QUUNDESP.mjs +1 -0
- package/dist/esm/chunk-QUUNDESP.mjs.map +1 -0
- package/dist/esm/chunk-RA5VYM7M.mjs +1 -0
- package/dist/esm/chunk-RA5VYM7M.mjs.map +1 -0
- package/dist/esm/index.d-y2b-8_3Y.d.mts +236 -0
- package/dist/esm/index.d.mts +7 -24
- package/dist/esm/index.mjs +1 -1
- package/dist/esm/metafile-esm.json +1 -1
- package/dist/esm/utils.d.mts +6 -1
- package/dist/esm/utils.mjs +1 -1
- package/dist/esm/utils.mjs.map +1 -1
- package/dist/esm/validation.d.mts +184 -0
- package/dist/esm/validation.mjs +1 -0
- package/dist/esm/validation.mjs.map +1 -0
- package/package.json +18 -5
- package/src/Router.ts +73 -99
- package/src/api/cloneRouter.ts +1 -30
- package/src/api/getDependenciesApi.ts +45 -86
- package/src/api/getLifecycleApi.ts +24 -19
- package/src/api/getPluginApi.ts +20 -28
- package/src/api/getRoutesApi.ts +49 -106
- package/src/constants.ts +0 -30
- package/src/guards.ts +46 -0
- package/src/helpers.ts +0 -17
- package/src/index.ts +4 -0
- package/src/internals.ts +6 -5
- package/src/namespaces/EventBusNamespace/EventBusNamespace.ts +2 -2
- package/src/namespaces/NavigationNamespace/NavigationNamespace.ts +0 -25
- package/src/namespaces/OptionsNamespace/OptionsNamespace.ts +4 -26
- package/src/namespaces/OptionsNamespace/constants.ts +0 -20
- package/src/namespaces/OptionsNamespace/index.ts +1 -5
- package/src/namespaces/OptionsNamespace/validators.ts +6 -245
- package/src/namespaces/PluginsNamespace/PluginsNamespace.ts +18 -59
- package/src/namespaces/PluginsNamespace/constants.ts +3 -6
- package/src/namespaces/PluginsNamespace/validators.ts +2 -57
- package/src/namespaces/RouteLifecycleNamespace/RouteLifecycleNamespace.ts +27 -84
- package/src/namespaces/RouterLifecycleNamespace/RouterLifecycleNamespace.ts +0 -16
- package/src/namespaces/RoutesNamespace/RoutesNamespace.ts +3 -12
- package/src/namespaces/RoutesNamespace/constants.ts +0 -8
- package/src/namespaces/RoutesNamespace/forwardChain.ts +34 -0
- package/src/namespaces/RoutesNamespace/index.ts +1 -1
- package/src/namespaces/RoutesNamespace/routeGuards.ts +62 -0
- package/src/namespaces/RoutesNamespace/routesStore.ts +7 -51
- package/src/namespaces/StateNamespace/StateNamespace.ts +0 -33
- package/src/namespaces/StateNamespace/helpers.ts +1 -1
- package/src/namespaces/index.ts +0 -3
- package/src/typeGuards.ts +1 -15
- package/src/types/RouterValidator.ts +155 -0
- package/src/utils/getStaticPaths.ts +50 -0
- package/src/utils/index.ts +4 -0
- package/src/validation.ts +12 -0
- package/src/wiring/RouterWiringBuilder.ts +32 -9
- package/dist/cjs/index.d-DDimDpYc.d.ts +0 -165
- package/dist/esm/chunk-CG7TKDP3.mjs +0 -1
- package/dist/esm/chunk-CG7TKDP3.mjs.map +0 -1
- package/dist/esm/index.d-DDimDpYc.d.mts +0 -165
- package/src/namespaces/DependenciesNamespace/validators.ts +0 -103
- package/src/namespaces/EventBusNamespace/validators.ts +0 -36
- package/src/namespaces/NavigationNamespace/validators.ts +0 -47
- package/src/namespaces/RouteLifecycleNamespace/validators.ts +0 -65
- package/src/namespaces/RoutesNamespace/forwardToValidation.ts +0 -408
- package/src/namespaces/RoutesNamespace/validators.ts +0 -566
- package/src/namespaces/StateNamespace/validators.ts +0 -46
|
@@ -1,252 +1,13 @@
|
|
|
1
1
|
// packages/core/src/namespaces/OptionsNamespace/validators.ts
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Minimal crash guard for options.
|
|
5
|
+
* Full DX validation moved to @real-router/validation-plugin (retrospective pattern).
|
|
6
6
|
*/
|
|
7
|
-
|
|
8
|
-
import { isObjKey, getTypeDescription } from "type-guards";
|
|
9
|
-
|
|
10
|
-
import {
|
|
11
|
-
defaultOptions,
|
|
12
|
-
VALID_OPTION_VALUES,
|
|
13
|
-
VALID_QUERY_PARAMS,
|
|
14
|
-
} from "./constants";
|
|
15
|
-
import { LIMIT_BOUNDS } from "../../constants";
|
|
16
|
-
|
|
17
|
-
import type { LimitsConfig, Options } from "@real-router/types";
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Validates that value is a plain object without getters.
|
|
21
|
-
*/
|
|
22
|
-
export function validatePlainObject(
|
|
23
|
-
value: unknown,
|
|
24
|
-
optionName: string,
|
|
25
|
-
methodName: string,
|
|
26
|
-
): asserts value is Record<string, unknown> {
|
|
27
|
-
if (!value || typeof value !== "object" || value.constructor !== Object) {
|
|
28
|
-
throw new TypeError(
|
|
29
|
-
`[router.${methodName}] Invalid type for "${optionName}": ` +
|
|
30
|
-
`expected plain object, got ${getTypeDescription(value)}`,
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Getters can throw, return different values, or have side effects
|
|
35
|
-
for (const key in value) {
|
|
36
|
-
if (Object.getOwnPropertyDescriptor(value, key)?.get) {
|
|
37
|
-
throw new TypeError(
|
|
38
|
-
`[router.${methodName}] Getters not allowed in "${optionName}": "${key}"`,
|
|
39
|
-
);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Validates queryParams keys and values against allowed enums.
|
|
46
|
-
*/
|
|
47
|
-
export function validateQueryParams(
|
|
48
|
-
value: Record<string, unknown>,
|
|
49
|
-
methodName: string,
|
|
50
|
-
): void {
|
|
51
|
-
for (const key in value) {
|
|
52
|
-
if (!isObjKey(key, VALID_QUERY_PARAMS)) {
|
|
53
|
-
const validKeys = Object.keys(VALID_QUERY_PARAMS)
|
|
54
|
-
.map((k) => `"${k}"`)
|
|
55
|
-
.join(", ");
|
|
56
|
-
|
|
57
|
-
throw new TypeError(
|
|
58
|
-
`[router.${methodName}] Unknown queryParams key: "${key}". ` +
|
|
59
|
-
`Valid keys: ${validKeys}`,
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const paramValue = value[key];
|
|
64
|
-
const validValues = VALID_QUERY_PARAMS[key];
|
|
65
|
-
const isValid = (validValues as readonly string[]).includes(
|
|
66
|
-
paramValue as string,
|
|
67
|
-
);
|
|
68
|
-
|
|
69
|
-
if (!isValid) {
|
|
70
|
-
const allowedValues = validValues.map((v) => `"${v}"`).join(", ");
|
|
71
|
-
|
|
72
|
-
throw new TypeError(
|
|
73
|
-
`[router.${methodName}] Invalid value for queryParams.${key}: ` +
|
|
74
|
-
`expected one of ${allowedValues}, got "${String(paramValue)}"`,
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Validates string enum options against allowed values.
|
|
82
|
-
*/
|
|
83
|
-
export function validateEnumOption(
|
|
84
|
-
optionName: keyof typeof VALID_OPTION_VALUES,
|
|
85
|
-
value: unknown,
|
|
86
|
-
methodName: string,
|
|
87
|
-
): void {
|
|
88
|
-
const validValues = VALID_OPTION_VALUES[optionName];
|
|
89
|
-
const isValid = (validValues as readonly string[]).includes(value as string);
|
|
90
|
-
|
|
91
|
-
if (!isValid) {
|
|
92
|
-
const allowedValues = validValues.map((v) => `"${v}"`).join(", ");
|
|
93
|
-
|
|
94
|
-
throw new TypeError(
|
|
95
|
-
`[router.${methodName}] Invalid value for "${optionName}": ` +
|
|
96
|
-
`expected one of ${allowedValues}, got "${String(value)}"`,
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Validates a single option value against expected type and constraints.
|
|
103
|
-
* Skips validation for unknown options - validateOptionExists handles that.
|
|
104
|
-
*/
|
|
105
|
-
export function validateOptionValue(
|
|
106
|
-
optionName: keyof Options,
|
|
107
|
-
value: unknown,
|
|
108
|
-
methodName: string,
|
|
109
|
-
): void {
|
|
110
|
-
// Allow callback functions for dynamic default route/params options
|
|
111
|
-
// MUST be first check — before object branch (L140) which would reject
|
|
112
|
-
// functions via validatePlainObject for defaultParams (default = {})
|
|
113
|
-
if (
|
|
114
|
-
typeof value === "function" &&
|
|
115
|
-
(optionName === "defaultRoute" || optionName === "defaultParams")
|
|
116
|
-
) {
|
|
117
|
-
return; // Valid — callback resolved at runtime
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const expectedValue = defaultOptions[optionName];
|
|
121
|
-
|
|
122
|
-
// For object options - ensure plain objects only (not null, arrays, Date, etc)
|
|
123
|
-
if (expectedValue && typeof expectedValue === "object") {
|
|
124
|
-
validatePlainObject(value, optionName, methodName);
|
|
125
|
-
|
|
126
|
-
if (optionName === "queryParams") {
|
|
127
|
-
validateQueryParams(value, methodName);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// For primitives - typeof check first
|
|
134
|
-
if (typeof value !== typeof expectedValue) {
|
|
135
|
-
throw new TypeError(
|
|
136
|
-
`[router.${methodName}] Invalid type for "${optionName}": ` +
|
|
137
|
-
`expected ${typeof expectedValue}, got ${typeof value}`,
|
|
138
|
-
);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// For string enum options - validate against allowed values
|
|
142
|
-
if (optionName in VALID_OPTION_VALUES) {
|
|
143
|
-
validateEnumOption(
|
|
144
|
-
optionName as keyof typeof VALID_OPTION_VALUES,
|
|
145
|
-
value,
|
|
146
|
-
methodName,
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Validates optional fields not in defaultOptions.
|
|
153
|
-
* Note: logger is handled before validateOptions in Router constructor.
|
|
154
|
-
*/
|
|
155
|
-
function validateOptionalField(
|
|
156
|
-
key: string,
|
|
157
|
-
value: unknown,
|
|
158
|
-
methodName: string,
|
|
159
|
-
): boolean {
|
|
160
|
-
if (key === "limits") {
|
|
161
|
-
if (value !== undefined) {
|
|
162
|
-
validateLimits(value, methodName);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
return true;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
throw new TypeError(`[router.${methodName}] Unknown option: "${key}"`);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Validates a partial options object.
|
|
173
|
-
* Called by facade before constructor/withOptions.
|
|
174
|
-
*/
|
|
175
|
-
export function validateOptions(
|
|
7
|
+
export function validateOptionsIsObject(
|
|
176
8
|
options: unknown,
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
!options ||
|
|
181
|
-
typeof options !== "object" ||
|
|
182
|
-
options.constructor !== Object
|
|
183
|
-
) {
|
|
184
|
-
throw new TypeError(
|
|
185
|
-
`[router.${methodName}] Invalid options: expected plain object, got ${getTypeDescription(options)}`,
|
|
186
|
-
);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
for (const [key, value] of Object.entries(options)) {
|
|
190
|
-
// Skip optional fields that aren't in defaultOptions (limits, logger, etc.)
|
|
191
|
-
if (!isObjKey(key, defaultOptions)) {
|
|
192
|
-
validateOptionalField(key, value, methodName);
|
|
193
|
-
continue;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// Skip undefined values for conditional configuration
|
|
197
|
-
if (value === undefined) {
|
|
198
|
-
continue;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
validateOptionValue(key, value, methodName);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Validates that a limit value is within bounds.
|
|
207
|
-
*/
|
|
208
|
-
export function validateLimitValue(
|
|
209
|
-
limitName: keyof LimitsConfig,
|
|
210
|
-
value: unknown,
|
|
211
|
-
methodName: string,
|
|
212
|
-
): void {
|
|
213
|
-
if (typeof value !== "number" || !Number.isInteger(value)) {
|
|
214
|
-
throw new TypeError(
|
|
215
|
-
`[router.${methodName}]: limit "${limitName}" must be an integer, got ${String(value)}`,
|
|
216
|
-
);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const bounds = LIMIT_BOUNDS[limitName];
|
|
220
|
-
|
|
221
|
-
if (value < bounds.min || value > bounds.max) {
|
|
222
|
-
throw new RangeError(
|
|
223
|
-
`[router.${methodName}]: limit "${limitName}" must be between ${bounds.min} and ${bounds.max}, got ${value}`,
|
|
224
|
-
);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Validates a partial limits object.
|
|
230
|
-
*/
|
|
231
|
-
export function validateLimits(
|
|
232
|
-
limits: unknown,
|
|
233
|
-
methodName: string,
|
|
234
|
-
): asserts limits is Partial<LimitsConfig> {
|
|
235
|
-
if (!limits || typeof limits !== "object" || limits.constructor !== Object) {
|
|
236
|
-
throw new TypeError(
|
|
237
|
-
`[router.${methodName}]: invalid limits: expected plain object, got ${typeof limits}`,
|
|
238
|
-
);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
for (const [key, value] of Object.entries(limits)) {
|
|
242
|
-
if (!Object.hasOwn(LIMIT_BOUNDS, key)) {
|
|
243
|
-
throw new TypeError(`[router.${methodName}]: unknown limit: "${key}"`);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
if (value === undefined) {
|
|
247
|
-
continue;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
validateLimitValue(key as keyof LimitsConfig, value, methodName);
|
|
9
|
+
): asserts options is Record<string, unknown> {
|
|
10
|
+
if (!options || typeof options !== "object" || Array.isArray(options)) {
|
|
11
|
+
throw new TypeError("[router.constructor] options must be a plain object");
|
|
251
12
|
}
|
|
252
13
|
}
|
|
@@ -3,16 +3,12 @@
|
|
|
3
3
|
import { logger } from "@real-router/logger";
|
|
4
4
|
|
|
5
5
|
import { EVENTS_MAP, EVENT_METHOD_NAMES, LOGGER_CONTEXT } from "./constants";
|
|
6
|
-
import {
|
|
7
|
-
validatePlugin,
|
|
8
|
-
validatePluginLimit,
|
|
9
|
-
validateUsePluginArgs,
|
|
10
|
-
} from "./validators";
|
|
6
|
+
import { validatePlugin } from "./validators";
|
|
11
7
|
import { DEFAULT_LIMITS } from "../../constants";
|
|
12
|
-
import { computeThresholds } from "../../helpers";
|
|
13
8
|
|
|
14
9
|
import type { PluginsDependencies } from "./types";
|
|
15
10
|
import type { Limits, PluginFactory } from "../../types";
|
|
11
|
+
import type { RouterValidator } from "../../types/RouterValidator";
|
|
16
12
|
import type {
|
|
17
13
|
DefaultDependencies,
|
|
18
14
|
Plugin,
|
|
@@ -33,30 +29,17 @@ export class PluginsNamespace<
|
|
|
33
29
|
|
|
34
30
|
#deps!: PluginsDependencies<Dependencies>;
|
|
35
31
|
#limits: Limits = DEFAULT_LIMITS;
|
|
32
|
+
#getValidator: (() => RouterValidator | null) | null = null;
|
|
36
33
|
|
|
37
34
|
// =========================================================================
|
|
38
35
|
// Static validation methods (called by facade before instance methods)
|
|
39
36
|
// Proxy to functions in validators.ts for separation of concerns
|
|
40
37
|
// =========================================================================
|
|
41
38
|
|
|
42
|
-
static validateUsePluginArgs<D extends DefaultDependencies>(
|
|
43
|
-
plugins: unknown[],
|
|
44
|
-
): asserts plugins is PluginFactory<D>[] {
|
|
45
|
-
validateUsePluginArgs<D>(plugins);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
39
|
static validatePlugin(plugin: Plugin): void {
|
|
49
40
|
validatePlugin(plugin);
|
|
50
41
|
}
|
|
51
42
|
|
|
52
|
-
static validatePluginLimit(
|
|
53
|
-
currentCount: number,
|
|
54
|
-
newCount: number,
|
|
55
|
-
maxPlugins?: number,
|
|
56
|
-
): void {
|
|
57
|
-
validatePluginLimit(currentCount, newCount, maxPlugins);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
43
|
static validateNoDuplicatePlugins<D extends DefaultDependencies>(
|
|
61
44
|
newFactories: PluginFactory<D>[],
|
|
62
45
|
hasPlugin: (factory: PluginFactory<D>) => boolean,
|
|
@@ -81,6 +64,12 @@ export class PluginsNamespace<
|
|
|
81
64
|
|
|
82
65
|
setLimits(limits: Limits): void {
|
|
83
66
|
this.#limits = limits;
|
|
67
|
+
// eslint-disable-next-line sonarjs/void-use -- @preserve: limits passed to validator via RouterInternals; void suppresses TS6133 until plugin implements validateCountThresholds
|
|
68
|
+
void this.#limits;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
setValidatorGetter(getter: () => RouterValidator | null): void {
|
|
72
|
+
this.#getValidator = getter;
|
|
84
73
|
}
|
|
85
74
|
|
|
86
75
|
// =========================================================================
|
|
@@ -91,6 +80,7 @@ export class PluginsNamespace<
|
|
|
91
80
|
* Returns the number of registered plugins.
|
|
92
81
|
* Used by facade for limit validation.
|
|
93
82
|
*/
|
|
83
|
+
/* v8 ignore next 3 -- @preserve: only called via validator interface (ctx.validator?.plugins.validatePluginLimit), not reachable without validation plugin */
|
|
94
84
|
count(): number {
|
|
95
85
|
return this.#plugins.size;
|
|
96
86
|
}
|
|
@@ -104,7 +94,9 @@ export class PluginsNamespace<
|
|
|
104
94
|
*/
|
|
105
95
|
use(...factories: PluginFactory<Dependencies>[]): Unsubscribe {
|
|
106
96
|
// Emit warnings for count thresholds (not validation, just warnings)
|
|
107
|
-
this.#
|
|
97
|
+
this.#getValidator?.()?.plugins.validateCountThresholds(
|
|
98
|
+
this.#plugins.size + factories.length,
|
|
99
|
+
);
|
|
108
100
|
|
|
109
101
|
// Fast path for single plugin (common case)
|
|
110
102
|
if (factories.length === 1) {
|
|
@@ -209,6 +201,7 @@ export class PluginsNamespace<
|
|
|
209
201
|
* Checks if a plugin factory is registered.
|
|
210
202
|
* Used internally by validation to avoid array allocation.
|
|
211
203
|
*/
|
|
204
|
+
/* v8 ignore next 3 -- @preserve: only called via validator interface, not reachable without validation plugin */
|
|
212
205
|
has(factory: PluginFactory<Dependencies>): boolean {
|
|
213
206
|
return this.#plugins.has(factory);
|
|
214
207
|
}
|
|
@@ -233,32 +226,6 @@ export class PluginsNamespace<
|
|
|
233
226
|
// Private methods
|
|
234
227
|
// =========================================================================
|
|
235
228
|
|
|
236
|
-
#checkCountThresholds(newCount: number): void {
|
|
237
|
-
const maxPlugins = this.#limits.maxPlugins;
|
|
238
|
-
|
|
239
|
-
if (maxPlugins === 0) {
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
const totalCount = newCount + this.#plugins.size;
|
|
244
|
-
|
|
245
|
-
const { warn, error } = computeThresholds(maxPlugins);
|
|
246
|
-
|
|
247
|
-
if (totalCount >= error) {
|
|
248
|
-
logger.error(
|
|
249
|
-
LOGGER_CONTEXT,
|
|
250
|
-
`${totalCount} plugins registered! ` +
|
|
251
|
-
`This is excessive and will impact performance. ` +
|
|
252
|
-
`Hard limit at ${maxPlugins}.`,
|
|
253
|
-
);
|
|
254
|
-
} else if (totalCount >= warn) {
|
|
255
|
-
logger.warn(
|
|
256
|
-
LOGGER_CONTEXT,
|
|
257
|
-
`${totalCount} plugins registered. ` + `Consider if all are necessary.`,
|
|
258
|
-
);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
229
|
/**
|
|
263
230
|
* Deduplicates batch with warning for duplicates within batch.
|
|
264
231
|
* Validation (existing duplicates) is done by facade.
|
|
@@ -270,10 +237,7 @@ export class PluginsNamespace<
|
|
|
270
237
|
|
|
271
238
|
for (const plugin of plugins) {
|
|
272
239
|
if (seenInBatch.has(plugin)) {
|
|
273
|
-
|
|
274
|
-
LOGGER_CONTEXT,
|
|
275
|
-
"Duplicate factory in batch, will be registered once",
|
|
276
|
-
);
|
|
240
|
+
this.#getValidator?.()?.plugins.warnBatchDuplicates(plugins);
|
|
277
241
|
} else {
|
|
278
242
|
seenInBatch.add(plugin);
|
|
279
243
|
}
|
|
@@ -286,6 +250,7 @@ export class PluginsNamespace<
|
|
|
286
250
|
const appliedPlugin = this.#deps.compileFactory(pluginFactory);
|
|
287
251
|
|
|
288
252
|
PluginsNamespace.validatePlugin(appliedPlugin);
|
|
253
|
+
this.#getValidator?.()?.plugins.validatePluginKeys(appliedPlugin);
|
|
289
254
|
|
|
290
255
|
Object.freeze(appliedPlugin);
|
|
291
256
|
|
|
@@ -304,16 +269,10 @@ export class PluginsNamespace<
|
|
|
304
269
|
);
|
|
305
270
|
|
|
306
271
|
if (methodName === "onStart" && this.#deps.canNavigate()) {
|
|
307
|
-
|
|
308
|
-
LOGGER_CONTEXT,
|
|
309
|
-
"Router already started, onStart will not be called",
|
|
310
|
-
);
|
|
272
|
+
this.#getValidator?.()?.plugins.warnPluginAfterStart(methodName);
|
|
311
273
|
}
|
|
312
274
|
} else {
|
|
313
|
-
|
|
314
|
-
LOGGER_CONTEXT,
|
|
315
|
-
`Property '${methodName}' is not a function, skipping`,
|
|
316
|
-
);
|
|
275
|
+
this.#getValidator?.()?.plugins.warnPluginMethodType(methodName);
|
|
317
276
|
}
|
|
318
277
|
}
|
|
319
278
|
}
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
// packages/core/src/namespaces/PluginsNamespace/constants.ts
|
|
2
2
|
|
|
3
|
-
import { isObjKey } from "type-guards";
|
|
4
|
-
|
|
5
3
|
import {
|
|
6
4
|
events as EVENTS_CONST,
|
|
7
5
|
plugins as PLUGINS_CONST,
|
|
@@ -27,9 +25,8 @@ export const EVENTS_MAP = {
|
|
|
27
25
|
/**
|
|
28
26
|
* Plugin method names that correspond to router events.
|
|
29
27
|
*/
|
|
30
|
-
export const EVENT_METHOD_NAMES = Object.keys(
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
);
|
|
28
|
+
export const EVENT_METHOD_NAMES = Object.keys(
|
|
29
|
+
EVENTS_MAP,
|
|
30
|
+
) as (keyof typeof EVENTS_MAP)[];
|
|
34
31
|
|
|
35
32
|
export const LOGGER_CONTEXT = "router.usePlugin";
|
|
@@ -5,28 +5,7 @@
|
|
|
5
5
|
* Called by Router facade before instance methods.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
import { EVENTS_MAP } from "./constants";
|
|
11
|
-
import { DEFAULT_LIMITS } from "../../constants";
|
|
12
|
-
|
|
13
|
-
import type { PluginFactory } from "../../types";
|
|
14
|
-
import type { DefaultDependencies, Plugin } from "@real-router/types";
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Validates usePlugin arguments - all must be functions.
|
|
18
|
-
*/
|
|
19
|
-
export function validateUsePluginArgs<D extends DefaultDependencies>(
|
|
20
|
-
plugins: unknown[],
|
|
21
|
-
): asserts plugins is PluginFactory<D>[] {
|
|
22
|
-
for (const [i, plugin] of plugins.entries()) {
|
|
23
|
-
if (typeof plugin !== "function") {
|
|
24
|
-
throw new TypeError(
|
|
25
|
-
`[router.usePlugin] Expected plugin factory function at index ${i}, got ${getTypeDescription(plugin)}`,
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
}
|
|
8
|
+
import type { Plugin } from "@real-router/types";
|
|
30
9
|
|
|
31
10
|
/**
|
|
32
11
|
* Validates that a plugin factory returned a valid plugin object.
|
|
@@ -35,9 +14,7 @@ export function validatePlugin(plugin: Plugin): void {
|
|
|
35
14
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
36
15
|
if (!(plugin && typeof plugin === "object") || Array.isArray(plugin)) {
|
|
37
16
|
throw new TypeError(
|
|
38
|
-
`[router.usePlugin] Plugin factory must return an object, got ${
|
|
39
|
-
plugin,
|
|
40
|
-
)}`,
|
|
17
|
+
`[router.usePlugin] Plugin factory must return an object, got ${typeof plugin}`,
|
|
41
18
|
);
|
|
42
19
|
}
|
|
43
20
|
|
|
@@ -48,36 +25,4 @@ export function validatePlugin(plugin: Plugin): void {
|
|
|
48
25
|
`Factory returned a Promise instead of a plugin object.`,
|
|
49
26
|
);
|
|
50
27
|
}
|
|
51
|
-
|
|
52
|
-
for (const key in plugin) {
|
|
53
|
-
if (!(key === "teardown" || isObjKey<typeof EVENTS_MAP>(key, EVENTS_MAP))) {
|
|
54
|
-
throw new TypeError(
|
|
55
|
-
`[router.usePlugin] Unknown property '${key}'. ` +
|
|
56
|
-
`Plugin must only contain event handlers and optional teardown.`,
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Validates that adding new plugins won't exceed the hard limit.
|
|
64
|
-
*/
|
|
65
|
-
export function validatePluginLimit(
|
|
66
|
-
currentCount: number,
|
|
67
|
-
newCount: number,
|
|
68
|
-
maxPlugins: number = DEFAULT_LIMITS.maxPlugins,
|
|
69
|
-
): void {
|
|
70
|
-
if (maxPlugins === 0) {
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const totalCount = currentCount + newCount;
|
|
75
|
-
|
|
76
|
-
if (totalCount > maxPlugins) {
|
|
77
|
-
throw new Error(
|
|
78
|
-
`[router.usePlugin] Plugin limit exceeded (${maxPlugins}). ` +
|
|
79
|
-
`Current: ${currentCount}, Attempting to add: ${newCount}. ` +
|
|
80
|
-
`This indicates an architectural problem. Consider consolidating plugins.`,
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
28
|
}
|