@real-router/persistent-params-plugin 0.1.35 → 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/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
- }