@real-router/persistent-params-plugin 0.1.34 → 0.1.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/metafile-cjs.json +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 +3 -3
- package/src/factory.ts +106 -0
- package/src/index.ts +1 -1
- package/src/param-utils.ts +82 -0
- package/src/plugin.ts +105 -268
- package/src/validation.ts +128 -0
- package/src/constants.ts +0 -7
- package/src/utils.ts +0 -250
package/src/utils.ts
DELETED
|
@@ -1,250 +0,0 @@
|
|
|
1
|
-
// packages/persistent-params-plugin/modules/utils.ts
|
|
2
|
-
|
|
3
|
-
import { isPrimitiveValue } from "type-guards";
|
|
4
|
-
|
|
5
|
-
import type { PersistentParamsConfig } from "./types";
|
|
6
|
-
import type { Params } from "@real-router/core";
|
|
7
|
-
|
|
8
|
-
const INVALID_PARAM_KEY_REGEX = /[\s#%&/=?\\]/;
|
|
9
|
-
const INVALID_CHARS_MESSAGE = String.raw`Cannot contain: = & ? # % / \ or whitespace`;
|
|
10
|
-
|
|
11
|
-
export function validateParamKey(key: string): void {
|
|
12
|
-
if (INVALID_PARAM_KEY_REGEX.test(key)) {
|
|
13
|
-
throw new TypeError(
|
|
14
|
-
`[@real-router/persistent-params-plugin] Invalid parameter name "${key}". ${INVALID_CHARS_MESSAGE}`,
|
|
15
|
-
);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Validates params configuration structure and values.
|
|
21
|
-
* Ensures all parameter names are non-empty strings and all default values are primitives.
|
|
22
|
-
*
|
|
23
|
-
* @param config - Configuration to validate
|
|
24
|
-
* @returns true if configuration is valid
|
|
25
|
-
*/
|
|
26
|
-
/**
|
|
27
|
-
* Validates params configuration structure and values.
|
|
28
|
-
* Ensures all parameter names are non-empty strings and all default values are primitives.
|
|
29
|
-
*
|
|
30
|
-
* @param config - Configuration to validate
|
|
31
|
-
* @returns true if configuration is valid
|
|
32
|
-
*/
|
|
33
|
-
export function isValidParamsConfig(
|
|
34
|
-
config: unknown,
|
|
35
|
-
): config is PersistentParamsConfig {
|
|
36
|
-
if (config === null || config === undefined) {
|
|
37
|
-
return false;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Array configuration: all items must be non-empty strings
|
|
41
|
-
if (Array.isArray(config)) {
|
|
42
|
-
return config.every((item) => {
|
|
43
|
-
if (typeof item !== "string" || item.length === 0) {
|
|
44
|
-
return false;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
try {
|
|
48
|
-
validateParamKey(item);
|
|
49
|
-
|
|
50
|
-
return true;
|
|
51
|
-
} catch {
|
|
52
|
-
return false;
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Object configuration: must be plain object with primitive values
|
|
58
|
-
if (typeof config === "object") {
|
|
59
|
-
// Reject non-plain objects (Date, Map, etc.)
|
|
60
|
-
if (Object.getPrototypeOf(config) !== Object.prototype) {
|
|
61
|
-
return false;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// All keys must be non-empty strings, all values must be primitives
|
|
65
|
-
return Object.entries(config).every(([key, value]) => {
|
|
66
|
-
// Check key is non-empty string
|
|
67
|
-
if (typeof key !== "string" || key.length === 0) {
|
|
68
|
-
return false;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Validate key doesn't contain special characters
|
|
72
|
-
try {
|
|
73
|
-
validateParamKey(key);
|
|
74
|
-
} catch {
|
|
75
|
-
return false;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Validate value is primitive (NaN/Infinity already rejected by isPrimitiveValue)
|
|
79
|
-
return isPrimitiveValue(value);
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return false;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Validates parameter value before persisting.
|
|
88
|
-
* Throws descriptive TypeError if value is not valid for URL parameters.
|
|
89
|
-
*
|
|
90
|
-
* @param key - Parameter name for error messages
|
|
91
|
-
* @param value - Value to validate
|
|
92
|
-
* @throws {TypeError} If value is null, array, object, or other non-primitive type
|
|
93
|
-
*/
|
|
94
|
-
export function validateParamValue(key: string, value: unknown): void {
|
|
95
|
-
if (value === null) {
|
|
96
|
-
throw new TypeError(
|
|
97
|
-
`[@real-router/persistent-params-plugin] Parameter "${key}" cannot be null. ` +
|
|
98
|
-
`Use undefined to remove the parameter from persistence.`,
|
|
99
|
-
);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (value !== undefined && !isPrimitiveValue(value)) {
|
|
103
|
-
const actualType = Array.isArray(value) ? "array" : typeof value;
|
|
104
|
-
|
|
105
|
-
throw new TypeError(
|
|
106
|
-
`[@real-router/persistent-params-plugin] Parameter "${key}" must be a primitive value ` +
|
|
107
|
-
`(string, number, or boolean), got ${actualType}. ` +
|
|
108
|
-
`Objects and arrays are not supported in URL parameters.`,
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Safely extracts own properties from params object.
|
|
115
|
-
* Uses Object.hasOwn to prevent prototype pollution attacks.
|
|
116
|
-
*
|
|
117
|
-
* @param params - Parameters object (may contain inherited properties)
|
|
118
|
-
* @returns New object with only own properties
|
|
119
|
-
*
|
|
120
|
-
* @example
|
|
121
|
-
* const malicious = Object.create({ __proto__: { admin: true } });
|
|
122
|
-
* malicious.mode = 'dev';
|
|
123
|
-
* const safe = extractOwnParams(malicious); // { mode: 'dev' } (no __proto__)
|
|
124
|
-
*/
|
|
125
|
-
export function extractOwnParams(params: Params): Params {
|
|
126
|
-
const result: Params = {};
|
|
127
|
-
|
|
128
|
-
for (const key in params) {
|
|
129
|
-
// Only process own properties, skip inherited ones
|
|
130
|
-
if (Object.hasOwn(params, key)) {
|
|
131
|
-
result[key] = params[key];
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return result;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Parses path into base path and query string components.
|
|
140
|
-
* Handles edge cases like leading ?, multiple ?, empty path.
|
|
141
|
-
*
|
|
142
|
-
* @param path - Path to parse (e.g., "/route?param=value")
|
|
143
|
-
* @returns Object with basePath and queryString
|
|
144
|
-
*
|
|
145
|
-
* @example
|
|
146
|
-
* parseQueryString('/users?page=1') // { basePath: '/users', queryString: 'page=1' }
|
|
147
|
-
* parseQueryString('?existing') // { basePath: '', queryString: 'existing' }
|
|
148
|
-
* parseQueryString('/path') // { basePath: '/path', queryString: '' }
|
|
149
|
-
*/
|
|
150
|
-
export function parseQueryString(path: string): {
|
|
151
|
-
basePath: string;
|
|
152
|
-
queryString: string;
|
|
153
|
-
} {
|
|
154
|
-
const questionMarkIndex = path.indexOf("?");
|
|
155
|
-
|
|
156
|
-
// No query string
|
|
157
|
-
if (questionMarkIndex === -1) {
|
|
158
|
-
return { basePath: path, queryString: "" };
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Path starts with ? (edge case)
|
|
162
|
-
if (questionMarkIndex === 0) {
|
|
163
|
-
return { basePath: "", queryString: path.slice(1) };
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// Normal case: path?query
|
|
167
|
-
return {
|
|
168
|
-
basePath: path.slice(0, questionMarkIndex),
|
|
169
|
-
queryString: path.slice(questionMarkIndex + 1),
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Builds query string from parameter names.
|
|
175
|
-
* Preserves existing query parameters and appends new ones.
|
|
176
|
-
*
|
|
177
|
-
* @param existingQuery - Existing query string (without leading ?)
|
|
178
|
-
* @param paramNames - Parameter names to append
|
|
179
|
-
* @returns Combined query string
|
|
180
|
-
*
|
|
181
|
-
* @example
|
|
182
|
-
* buildQueryString('existing=1', ['mode', 'lang']) // 'existing=1&mode&lang'
|
|
183
|
-
* buildQueryString('', ['mode']) // 'mode'
|
|
184
|
-
*/
|
|
185
|
-
export function buildQueryString(
|
|
186
|
-
existingQuery: string,
|
|
187
|
-
paramNames: readonly string[],
|
|
188
|
-
): string {
|
|
189
|
-
if (paramNames.length === 0) {
|
|
190
|
-
return existingQuery;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const separator = existingQuery ? "&" : "";
|
|
194
|
-
|
|
195
|
-
return existingQuery + separator + paramNames.join("&");
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Merges persistent and current parameters into a single Params object.
|
|
200
|
-
* Keys explicitly set to `undefined` in current params are removed from result.
|
|
201
|
-
*
|
|
202
|
-
* Creates a new immutable object - does not mutate input parameters.
|
|
203
|
-
*
|
|
204
|
-
* @param persistent - Frozen persistent parameters
|
|
205
|
-
* @param current - Current parameters from navigation
|
|
206
|
-
* @returns New Params object with merged values
|
|
207
|
-
*
|
|
208
|
-
* @example
|
|
209
|
-
* const persistent = { lang: 'en', theme: 'dark' };
|
|
210
|
-
* const current = { theme: 'light', mode: 'dev' };
|
|
211
|
-
* mergeParams(persistent, current); // { lang: 'en', theme: 'light', mode: 'dev' }
|
|
212
|
-
*
|
|
213
|
-
* @example
|
|
214
|
-
* // Removing parameters with undefined
|
|
215
|
-
* const persistent = { lang: 'en', theme: 'dark' };
|
|
216
|
-
* const current = { theme: undefined };
|
|
217
|
-
* mergeParams(persistent, current); // { lang: 'en' } (theme removed)
|
|
218
|
-
*/
|
|
219
|
-
export function mergeParams(
|
|
220
|
-
persistent: Readonly<Params>,
|
|
221
|
-
current: Params,
|
|
222
|
-
): Params {
|
|
223
|
-
// Safely extract own properties from current params
|
|
224
|
-
const safeCurrentParams = extractOwnParams(current);
|
|
225
|
-
|
|
226
|
-
// Start with persistent params, but EXCLUDE undefined values
|
|
227
|
-
// (undefined values don't appear in URLs, so we shouldn't include them)
|
|
228
|
-
const result: Params = {};
|
|
229
|
-
|
|
230
|
-
for (const key in persistent) {
|
|
231
|
-
if (Object.hasOwn(persistent, key) && persistent[key] !== undefined) {
|
|
232
|
-
result[key] = persistent[key];
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// Apply current params
|
|
237
|
-
for (const key of Object.keys(safeCurrentParams)) {
|
|
238
|
-
const value = safeCurrentParams[key];
|
|
239
|
-
|
|
240
|
-
if (value === undefined) {
|
|
241
|
-
// Remove param if explicitly set to undefined
|
|
242
|
-
delete result[key];
|
|
243
|
-
} else {
|
|
244
|
-
// Add or update param
|
|
245
|
-
result[key] = value;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
return result;
|
|
250
|
-
}
|