@real-router/persistent-params-plugin 0.1.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/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Oleg Ivanov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
package/README.md ADDED
@@ -0,0 +1,495 @@
1
+ # @real-router/persistent-params-plugin
2
+
3
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-blue.svg)](https://www.typescriptlang.org/)
5
+
6
+ A plugin that automatically persists and applies specified query parameters across all navigation transitions.
7
+
8
+ ## Problem
9
+
10
+ In SPA applications, certain query parameters should persist between routes:
11
+
12
+ ```typescript
13
+ // Without plugin:
14
+ router.navigate("products", { id: "1", lang: "en", theme: "dark" });
15
+ // URL: /products/1?lang=en&theme=dark
16
+
17
+ router.navigate("cart", { id: "2" });
18
+ // URL: /cart/2 ← lang and theme are lost
19
+ ```
20
+
21
+ ## Solution
22
+
23
+ ```typescript
24
+ router.usePlugin(persistentParamsPluginFactory(["lang", "theme"]));
25
+
26
+ router.navigate("products", { id: "1", lang: "en", theme: "dark" });
27
+ // URL: /products/1?lang=en&theme=dark
28
+
29
+ router.navigate("cart", { id: "2" });
30
+ // URL: /cart/2?lang=en&theme=dark ← automatically added
31
+ ```
32
+
33
+ ## Installation
34
+
35
+ ```bash
36
+ npm install @real-router/persistent-params-plugin
37
+ # or
38
+ pnpm add @real-router/persistent-params-plugin
39
+ # or
40
+ yarn add @real-router/persistent-params-plugin
41
+ # or
42
+ bun add @real-router/persistent-params-plugin
43
+ ```
44
+
45
+ ## Quick Start
46
+
47
+ ```typescript
48
+ import { createRouter } from "@real-router/core";
49
+ import { persistentParamsPluginFactory } from "@real-router/persistent-params-plugin";
50
+
51
+ const router = createRouter([
52
+ { name: "home", path: "/" },
53
+ { name: "products", path: "/products/:id" },
54
+ { name: "cart", path: "/cart" },
55
+ ]);
56
+
57
+ // Option 1: Specify parameter names
58
+ router.usePlugin(persistentParamsPluginFactory(["lang", "theme"]));
59
+
60
+ // Option 2: Specify default values
61
+ router.usePlugin(
62
+ persistentParamsPluginFactory({
63
+ lang: "en",
64
+ theme: "light",
65
+ }),
66
+ );
67
+
68
+ router.start();
69
+ ```
70
+
71
+ ## API
72
+
73
+ ### `persistentParamsPluginFactory(config?)`
74
+
75
+ #### Parameters
76
+
77
+ **`config`**: `string[] | Record<string, string | number | boolean>` (optional, defaults to `{}`)
78
+
79
+ - **Array of strings**: parameter names to persist (initial values are `undefined`)
80
+ - **Object**: parameter names with default values
81
+
82
+ #### Returns
83
+
84
+ `PluginFactory` — plugin factory for `router.usePlugin()`
85
+
86
+ #### Throws
87
+
88
+ - `TypeError`: invalid configuration (not an array of strings or object with primitives)
89
+ - `Error`: plugin already initialized on this router
90
+
91
+ ### Configuration
92
+
93
+ #### Array of Parameters
94
+
95
+ ```typescript
96
+ persistentParamsPluginFactory(["mode", "debug", "apiUrl"]);
97
+ ```
98
+
99
+ Parameters are saved after first use:
100
+
101
+ ```typescript
102
+ router.navigate("route1", { mode: "dev" });
103
+ // Saved: mode=dev
104
+
105
+ router.navigate("route2", {});
106
+ // URL includes: ?mode=dev
107
+ ```
108
+
109
+ #### Object with Defaults
110
+
111
+ ```typescript
112
+ persistentParamsPluginFactory({
113
+ mode: "prod",
114
+ lang: "en",
115
+ debug: false,
116
+ });
117
+ ```
118
+
119
+ Default values are applied immediately:
120
+
121
+ ```typescript
122
+ router.start();
123
+ router.navigate("route1", {});
124
+ // URL: /route1?mode=prod&lang=en&debug=false
125
+ ```
126
+
127
+ #### Empty Configuration
128
+
129
+ ```typescript
130
+ // Valid but does nothing
131
+ persistentParamsPluginFactory([]);
132
+ persistentParamsPluginFactory({});
133
+ ```
134
+
135
+ ## Behavior
136
+
137
+ ### Parameter Persistence
138
+
139
+ Parameters are automatically added to all transitions:
140
+
141
+ ```typescript
142
+ router.usePlugin(persistentParamsPluginFactory(["lang"]));
143
+
144
+ router.navigate("products", { id: "1", lang: "en" });
145
+ // Saved: lang=en
146
+
147
+ router.navigate("cart", { id: "2" });
148
+ // URL: /cart/2?lang=en
149
+
150
+ router.navigate("checkout", {});
151
+ // URL: /checkout?lang=en
152
+ ```
153
+
154
+ ### Updating Values
155
+
156
+ New value overwrites the saved one:
157
+
158
+ ```typescript
159
+ router.navigate("route1", { mode: "dev" });
160
+ // Saved: mode=dev
161
+
162
+ router.navigate("route2", { mode: "prod" });
163
+ // Saved: mode=prod (updated)
164
+
165
+ router.navigate("route3", {});
166
+ // URL: /route3?mode=prod
167
+ ```
168
+
169
+ ### Removing Parameters
170
+
171
+ Explicitly passing `undefined` removes the parameter:
172
+
173
+ ```typescript
174
+ router.navigate("route1", { mode: "dev", lang: "en" });
175
+ // Saved: mode=dev, lang=en
176
+
177
+ router.navigate("route2", { lang: undefined });
178
+ // Saved: mode=dev (lang removed)
179
+
180
+ router.navigate("route3", {});
181
+ // URL: /route3?mode=dev
182
+ ```
183
+
184
+ ### Value Priority
185
+
186
+ Explicitly passed value takes precedence:
187
+
188
+ ```typescript
189
+ // Saved: mode=dev
190
+ router.navigate("route", { mode: "test" });
191
+ // URL: /route?mode=test (explicit value used)
192
+ ```
193
+
194
+ ### Non-Tracked Parameters
195
+
196
+ Parameters outside configuration are not saved:
197
+
198
+ ```typescript
199
+ router.usePlugin(persistentParamsPluginFactory(["mode"]));
200
+
201
+ router.navigate("route1", { mode: "dev", temp: "value" });
202
+ // Saved: mode=dev
203
+ // temp is ignored
204
+
205
+ router.navigate("route2", {});
206
+ // URL: /route2?mode=dev (temp absent)
207
+ ```
208
+
209
+ ### Extraction from URL
210
+
211
+ Parameters are extracted from initial URL:
212
+
213
+ ```typescript
214
+ router.usePlugin(persistentParamsPluginFactory(["lang", "theme"]));
215
+ router.start("/products/1?lang=fr&theme=dark");
216
+ // Saved: lang=fr, theme=dark
217
+
218
+ router.navigate("cart", {});
219
+ // URL: /cart?lang=fr&theme=dark
220
+ ```
221
+
222
+ ## Integration with Router API
223
+
224
+ The plugin intercepts two router methods:
225
+
226
+ ### `router.buildPath()`
227
+
228
+ ```typescript
229
+ router.usePlugin(persistentParamsPluginFactory({ mode: "dev" }));
230
+
231
+ router.buildPath("products", { id: "1" });
232
+ // Returns: '/products/1?mode=dev'
233
+
234
+ router.buildPath("products", { id: "1", mode: "test" });
235
+ // Returns: '/products/1?mode=test' (explicit value)
236
+ ```
237
+
238
+ ### `router.forwardState()`
239
+
240
+ The plugin intercepts `forwardState()` which is used internally by `buildState()`, `buildStateWithSegments()`, and `navigate()`. This ensures persistent parameters are applied to all state-building operations.
241
+
242
+ ```typescript
243
+ router.usePlugin(persistentParamsPluginFactory({ mode: "dev" }));
244
+
245
+ // All of these include persistent params:
246
+ router.buildState("products", { id: "1" });
247
+ // state.params: { id: '1', mode: 'dev' }
248
+
249
+ router.navigate("products", { id: "1" });
250
+ // URL: /products/1?mode=dev
251
+ ```
252
+
253
+ ## Type Validation
254
+
255
+ ### Allowed Types
256
+
257
+ Only primitives: `string`, `number`, `boolean`:
258
+
259
+ ```typescript
260
+ // ✅ Valid
261
+ router.navigate("route", { mode: "dev" }); // string
262
+ router.navigate("route", { page: 42 }); // number
263
+ router.navigate("route", { debug: true }); // boolean
264
+ router.navigate("route", { mode: undefined }); // removal
265
+ ```
266
+
267
+ ### Forbidden Types
268
+
269
+ ```typescript
270
+ // ❌ TypeError
271
+ router.navigate("route", { data: { nested: 1 } }); // object
272
+ router.navigate("route", { items: [1, 2, 3] }); // array
273
+ router.navigate("route", { fn: () => {} }); // function
274
+ router.navigate("route", { date: new Date() }); // Date
275
+ router.navigate("route", { mode: null }); // null
276
+ ```
277
+
278
+ Reason: only primitives can be serialized in URL query string.
279
+
280
+ ### Parameter Name Validation
281
+
282
+ Parameter names must not contain special characters that could cause URL confusion:
283
+
284
+ ```typescript
285
+ // ✅ Valid parameter names
286
+ persistentParamsPluginFactory(["mode", "lang", "user_id", "api-key"]);
287
+
288
+ // ❌ Invalid parameter names (TypeError)
289
+ persistentParamsPluginFactory(["mode=dev"]); // contains =
290
+ persistentParamsPluginFactory(["param&other"]); // contains &
291
+ persistentParamsPluginFactory(["query?"]); // contains ?
292
+ persistentParamsPluginFactory(["hash#"]); // contains #
293
+ persistentParamsPluginFactory(["encoded%20"]); // contains %
294
+ persistentParamsPluginFactory(["path/to"]); // contains /
295
+ persistentParamsPluginFactory(["back\\slash"]); // contains \
296
+ persistentParamsPluginFactory(["with space"]); // contains whitespace
297
+ ```
298
+
299
+ **Forbidden characters**: `=`, `&`, `?`, `#`, `%`, `/`, `\`, and whitespace (space, tab, newline, carriage return)
300
+
301
+ **Why**: These characters have special meaning in URLs and would cause parsing issues or confusion.
302
+
303
+ ## Security
304
+
305
+ ### Prototype Pollution Protection
306
+
307
+ ```typescript
308
+ const malicious = Object.create({ __proto__: { isAdmin: true } });
309
+ malicious.mode = "dev";
310
+
311
+ router.navigate("route", malicious);
312
+ // Result: only mode=dev (inherited properties ignored)
313
+
314
+ // Global prototype not polluted
315
+ ({}).isAdmin === undefined; // true
316
+ ```
317
+
318
+ ### Constructor Pollution Protection
319
+
320
+ ```typescript
321
+ router.navigate("route", {
322
+ constructor: { prototype: { polluted: true } },
323
+ });
324
+ // TypeError: Parameter "constructor" must be a primitive value
325
+ ```
326
+
327
+ ### Safe State Management
328
+
329
+ The plugin safely manages persistent parameters to prevent accidental mutations and ensure consistent behavior across navigation.
330
+
331
+ ## Lifecycle
332
+
333
+ ### Initialization
334
+
335
+ ```typescript
336
+ const unsubscribe = router.usePlugin(persistentParamsPluginFactory(["mode"]));
337
+ ```
338
+
339
+ ### Double Initialization Protection
340
+
341
+ ```typescript
342
+ router.usePlugin(persistentParamsPluginFactory(["mode"]));
343
+
344
+ router.usePlugin(persistentParamsPluginFactory(["mode"]));
345
+ // Error: Plugin already initialized on this router
346
+ ```
347
+
348
+ ### Teardown
349
+
350
+ Cleanly removes the plugin and restores original router behavior:
351
+
352
+ ```typescript
353
+ const unsubscribe = router.usePlugin(persistentParamsPluginFactory(["mode"]));
354
+
355
+ // Work with plugin...
356
+
357
+ unsubscribe();
358
+ // Router methods restored to original behavior
359
+ // Plugin can be reinitialized with new configuration
360
+ ```
361
+
362
+ ### Reinitialization
363
+
364
+ ```typescript
365
+ const unsub1 = router.usePlugin(persistentParamsPluginFactory(["mode"]));
366
+ unsub1();
367
+
368
+ // Now can add with new configuration
369
+ const unsub2 = router.usePlugin(persistentParamsPluginFactory(["theme"]));
370
+ ```
371
+
372
+ ## Usage Examples
373
+
374
+ ### Multilingual Application
375
+
376
+ ```typescript
377
+ router.usePlugin(persistentParamsPluginFactory({ lang: "en" }));
378
+
379
+ // User changes language
380
+ router.navigate("settings", { lang: "fr" });
381
+
382
+ // Language persists across all transitions
383
+ router.navigate("products", { id: "1" }); // ?lang=fr
384
+ router.navigate("cart"); // ?lang=fr
385
+ router.navigate("checkout"); // ?lang=fr
386
+ ```
387
+
388
+ ### Development Mode
389
+
390
+ ```typescript
391
+ router.usePlugin(persistentParamsPluginFactory(["debug", "apiMock"]));
392
+
393
+ // Developer opens URL with flags
394
+ router.start("/?debug=true&apiMock=local");
395
+
396
+ // Flags persist in all routes
397
+ router.navigate("products"); // ?debug=true&apiMock=local
398
+ router.navigate("cart"); // ?debug=true&apiMock=local
399
+ ```
400
+
401
+ ### UTM Tracking
402
+
403
+ ```typescript
404
+ router.usePlugin(
405
+ persistentParamsPluginFactory(["utm_source", "utm_medium", "utm_campaign"]),
406
+ );
407
+
408
+ // User arrives from ad
409
+ router.start("/?utm_source=google&utm_medium=cpc&utm_campaign=spring2024");
410
+
411
+ // UTM tags persist across all transitions
412
+ router.navigate("products"); // includes utm_*
413
+ router.navigate("cart"); // includes utm_*
414
+ router.navigate("checkout"); // includes utm_*
415
+ ```
416
+
417
+ ### Filters and Sorting
418
+
419
+ ```typescript
420
+ router.usePlugin(persistentParamsPluginFactory(["sortBy", "order", "view"]));
421
+
422
+ // User configures display
423
+ router.navigate("products", {
424
+ category: "electronics",
425
+ sortBy: "price",
426
+ order: "asc",
427
+ view: "grid",
428
+ });
429
+
430
+ // Settings persist when changing categories
431
+ router.navigate("products", { category: "books" });
432
+ // URL: /products?category=books&sortBy=price&order=asc&view=grid
433
+ ```
434
+
435
+ ### Combination with Other Plugins
436
+
437
+ ```typescript
438
+ import { browserPluginFactory } from "@real-router/browser-plugin";
439
+ import { loggerPlugin } from "@real-router/logger-plugin";
440
+ import { persistentParamsPluginFactory } from "@real-router/persistent-params-plugin";
441
+
442
+ router.usePlugin(browserPluginFactory());
443
+ router.usePlugin(loggerPlugin);
444
+ router.usePlugin(persistentParamsPluginFactory(["lang", "theme"]));
445
+
446
+ // All plugins work together
447
+ ```
448
+
449
+ ## Error Handling
450
+
451
+ The plugin validates input and throws meaningful errors for invalid usage:
452
+
453
+ ```typescript
454
+ try {
455
+ router.usePlugin(persistentParamsPluginFactory(["mode"]));
456
+ router.usePlugin(persistentParamsPluginFactory(["mode"]));
457
+ } catch (error) {
458
+ // Error: Plugin already initialized on this router
459
+ }
460
+
461
+ try {
462
+ router.navigate("route", { mode: { nested: "value" } });
463
+ } catch (error) {
464
+ // TypeError: Parameter "mode" must be a primitive value
465
+ }
466
+ ```
467
+
468
+ ## TypeScript
469
+
470
+ The plugin is fully typed:
471
+
472
+ ```typescript
473
+ import {
474
+ persistentParamsPluginFactory,
475
+ type PersistentParamsConfig,
476
+ } from "@real-router/persistent-params-plugin";
477
+
478
+ // Configuration types
479
+ const config1: PersistentParamsConfig = ["mode", "lang"];
480
+ const config2: PersistentParamsConfig = { mode: "dev", lang: "en" };
481
+
482
+ // Type inference
483
+ router.usePlugin(persistentParamsPluginFactory(["mode"]));
484
+ router.navigate("route", { mode: "dev" }); // type-safe
485
+ ```
486
+
487
+ ## Related Packages
488
+
489
+ - [@real-router/core](https://www.npmjs.com/package/@real-router/core) — Core router
490
+ - [@real-router/react](https://www.npmjs.com/package/@real-router/react) — React integration
491
+ - [@real-router/browser-plugin](https://www.npmjs.com/package/@real-router/browser-plugin) — Browser history
492
+
493
+ ## License
494
+
495
+ MIT © [Oleg Ivanov](https://github.com/greydragon888)
@@ -0,0 +1,66 @@
1
+ import { PluginFactory } from '@real-router/core';
2
+
3
+ /**
4
+ * Configuration for persistent parameters' plugin.
5
+ * Can be either an array of parameter names or an object with default values.
6
+ *
7
+ * @example
8
+ * // Array of parameter names (initial values undefined)
9
+ * persistentParamsPlugin(['lang', 'theme'])
10
+ *
11
+ * @example
12
+ * // Object with default values
13
+ * persistentParamsPlugin({ lang: 'en', theme: 'light' })
14
+ */
15
+ type PersistentParamsConfig = string[] | Record<string, string | number | boolean>;
16
+
17
+ /**
18
+ * Factory for the persistent parameters' plugin.
19
+ *
20
+ * This plugin allows you to specify certain route parameters to be persisted across
21
+ * all navigation transitions. Persisted parameters are automatically merged into
22
+ * route parameters when building paths or states.
23
+ *
24
+ * Key features:
25
+ * - Automatic persistence of query parameters across navigations
26
+ * - Support for default values
27
+ * - Type-safe (only primitives: string, number, boolean)
28
+ * - Immutable internal state
29
+ * - Protection against prototype pollution
30
+ * - Full teardown support (can be safely unsubscribed)
31
+ *
32
+ * If a persisted parameter is explicitly set to `undefined` during navigation,
33
+ * it will be removed from the persisted state and omitted from subsequent URLs.
34
+ *
35
+ * The plugin also adjusts the router's root path to include query parameters for
36
+ * all persistent params, ensuring correct URL construction.
37
+ *
38
+ * @param params - Either an array of parameter names (strings) to persist,
39
+ * or an object mapping parameter names to initial values.
40
+ * If an array, initial values will be `undefined`.
41
+ *
42
+ * @returns A PluginFactory that creates the persistent params plugin instance.
43
+ *
44
+ * @example
45
+ * // Persist parameters without default values
46
+ * router.usePlugin(persistentParamsPlugin(['mode', 'lang']));
47
+ *
48
+ * @example
49
+ * // Persist parameters with default values
50
+ * router.usePlugin(persistentParamsPlugin({ mode: 'dev', lang: 'en' }));
51
+ *
52
+ * @example
53
+ * // Removing a persisted parameter
54
+ * router.navigate('route', { mode: undefined }); // mode will be removed
55
+ *
56
+ * @example
57
+ * // Unsubscribing (full cleanup)
58
+ * const unsubscribe = router.usePlugin(persistentParamsPlugin(['mode']));
59
+ * unsubscribe(); // Restores original router state
60
+ *
61
+ * @throws {TypeError} If params is not a valid array of strings or object with primitives
62
+ * @throws {Error} If plugin is already initialized on this router instance
63
+ */
64
+ declare function persistentParamsPluginFactory(params?: PersistentParamsConfig): PluginFactory;
65
+
66
+ export { type PersistentParamsConfig, persistentParamsPluginFactory };
@@ -0,0 +1 @@
1
+ var r=require("type-guards"),e=Symbol("persistent-params-plugin"),t=/[\s#%&/=?\\]/,n=String.raw`Cannot contain: = & ? # % / \ or whitespace`;function o(r){if(t.test(r))throw new TypeError(`[@real-router/persistent-params-plugin] Invalid parameter name "${r}". ${n}`)}function a(e,t){if(null===t)throw new TypeError(`[@real-router/persistent-params-plugin] Parameter "${e}" cannot be null. Use undefined to remove the parameter from persistence.`);if(void 0!==t&&!r.isPrimitiveValue(t)){const r=Array.isArray(t)?"array":typeof t;throw new TypeError(`[@real-router/persistent-params-plugin] Parameter "${e}" must be a primitive value (string, number, or boolean), got ${r}. Objects and arrays are not supported in URL parameters.`)}}function s(r){const e={};for(const t in r)Object.hasOwn(r,t)&&(e[t]=r[t]);return e}exports.persistentParamsPluginFactory=function(t={}){if(null==(n=t)||!(Array.isArray(n)?n.every(r=>{if("string"!=typeof r||0===r.length)return!1;try{return o(r),!0}catch{return!1}}):"object"==typeof n&&Object.getPrototypeOf(n)===Object.prototype&&Object.entries(n).every(([e,t])=>{if("string"!=typeof e||0===e.length)return!1;try{o(e)}catch{return!1}return r.isPrimitiveValue(t)}))){let r;throw r=null===t?"null":Array.isArray(t)?"array with invalid items":typeof t,new TypeError(`[@real-router/persistent-params-plugin] Invalid params configuration. Expected array of non-empty strings or object with primitive values, got ${r}.`)}var n;return Array.isArray(t)&&0===t.length?()=>({}):Array.isArray(t)||0!==Object.keys(t).length?r=>{if(e in r)throw new Error("[@real-router/persistent-params-plugin] Plugin already initialized on this router. To reconfigure, first unsubscribe the existing plugin using the returned unsubscribe function.");let n;if(r[e]=!0,Array.isArray(t)){const r={};for(const e of t)r[e]=void 0;n=Object.freeze(r)}else n=Object.freeze({...t});const o=new Set(Array.isArray(t)?[...t]:Object.keys(t)),i=r.buildPath.bind(r),c=r.forwardState.bind(r),u=r.getRootPath();try{const{basePath:e,queryString:t}=function(r){const e=r.indexOf("?");return-1===e?{basePath:r,queryString:""}:0===e?{basePath:"",queryString:r.slice(1)}:{basePath:r.slice(0,e),queryString:r.slice(e+1)}}(u),n=(p=t,0===(l=[...o]).length?p:p+(p?"&":"")+l.join("&"));r.setRootPath(`${e}?${n}`)}catch(t){throw delete r[e],new Error(`[@real-router/persistent-params-plugin] Failed to update root path: ${t instanceof Error?t.message:String(t)}`,{cause:t})}var p,l;function f(r){const e=s(r),t=[];for(const r of Object.keys(e)){const n=e[r];void 0===n&&o.has(r)?t.push(r):a(r,n)}if(t.length>0){for(const r of t)o.delete(r);const r={...n};for(const e of t)delete r[e];n=Object.freeze(r)}return function(r,e){const t=s(e),n={};for(const e in r)Object.hasOwn(r,e)&&void 0!==r[e]&&(n[e]=r[e]);for(const r of Object.keys(t)){const e=t[r];void 0===e?delete n[r]:n[r]=e}return n}(n,e)}return r.buildPath=(r,e={})=>i(r,f(e)),r.forwardState=(r,e)=>{const t=c(r,e);return{...t,params:f(t.params)}},{onTransitionSuccess(r){try{const e={},t=[];let s=!1;for(const i of o){const o=r.params[i];Object.hasOwn(r.params,i)&&void 0!==o?(a(i,o),n[i]!==o&&(e[i]=o,s=!0)):Object.hasOwn(n,i)&&void 0!==n[i]&&(t.push(i),s=!0)}if(s){const r={...n,...e};for(const e of t)delete r[e];n=Object.freeze(r)}}catch(r){console.error("persistent-params-plugin","Error updating persistent params:",r)}},teardown(){try{r.buildPath=i,r.forwardState=c,r.setRootPath(u),delete r[e]}catch(r){console.error("persistent-params-plugin","Error during teardown:",r)}}}}:()=>({})};//# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/constants.ts","../../src/utils.ts","../../src/plugin.ts"],"names":["isPrimitiveValue"],"mappings":"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","file":"index.js","sourcesContent":["// packages/persistent-params-plugin/modules/constants.ts\n\n/**\n * Symbol to mark router as initialized with this plugin.\n * Prevents double initialization and memory leaks from method wrapping.\n */\nexport const PLUGIN_MARKER = Symbol(\"persistent-params-plugin\");\n","// packages/persistent-params-plugin/modules/utils.ts\n\nimport { isPrimitiveValue } from \"type-guards\";\n\nimport type { PersistentParamsConfig } from \"./types\";\nimport type { Params } from \"@real-router/core\";\n\nconst INVALID_PARAM_KEY_REGEX = /[\\s#%&/=?\\\\]/;\nconst INVALID_CHARS_MESSAGE = String.raw`Cannot contain: = & ? # % / \\ or whitespace`;\n\nexport function validateParamKey(key: string): void {\n if (INVALID_PARAM_KEY_REGEX.test(key)) {\n throw new TypeError(\n `[@real-router/persistent-params-plugin] Invalid parameter name \"${key}\". ${INVALID_CHARS_MESSAGE}`,\n );\n }\n}\n\n/**\n * Validates params configuration structure and values.\n * Ensures all parameter names are non-empty strings and all default values are primitives.\n *\n * @param config - Configuration to validate\n * @returns true if configuration is valid\n */\n/**\n * Validates params configuration structure and values.\n * Ensures all parameter names are non-empty strings and all default values are primitives.\n *\n * @param config - Configuration to validate\n * @returns true if configuration is valid\n */\nexport function isValidParamsConfig(\n config: unknown,\n): config is PersistentParamsConfig {\n if (config === null || config === undefined) {\n return false;\n }\n\n // Array configuration: all items must be non-empty strings\n if (Array.isArray(config)) {\n return config.every((item) => {\n if (typeof item !== \"string\" || item.length === 0) {\n return false;\n }\n\n try {\n validateParamKey(item);\n\n return true;\n } catch {\n return false;\n }\n });\n }\n\n // Object configuration: must be plain object with primitive values\n if (typeof config === \"object\") {\n // Reject non-plain objects (Date, Map, etc.)\n if (Object.getPrototypeOf(config) !== Object.prototype) {\n return false;\n }\n\n // All keys must be non-empty strings, all values must be primitives\n return Object.entries(config).every(([key, value]) => {\n // Check key is non-empty string\n if (typeof key !== \"string\" || key.length === 0) {\n return false;\n }\n\n // Validate key doesn't contain special characters\n try {\n validateParamKey(key);\n } catch {\n return false;\n }\n\n // Validate value is primitive (NaN/Infinity already rejected by isPrimitiveValue)\n return isPrimitiveValue(value);\n });\n }\n\n return false;\n}\n\n/**\n * Validates parameter value before persisting.\n * Throws descriptive TypeError if value is not valid for URL parameters.\n *\n * @param key - Parameter name for error messages\n * @param value - Value to validate\n * @throws {TypeError} If value is null, array, object, or other non-primitive type\n */\nexport function validateParamValue(key: string, value: unknown): void {\n if (value === null) {\n throw new TypeError(\n `[@real-router/persistent-params-plugin] Parameter \"${key}\" cannot be null. ` +\n `Use undefined to remove the parameter from persistence.`,\n );\n }\n\n if (value !== undefined && !isPrimitiveValue(value)) {\n const actualType = Array.isArray(value) ? \"array\" : typeof value;\n\n throw new TypeError(\n `[@real-router/persistent-params-plugin] Parameter \"${key}\" must be a primitive value ` +\n `(string, number, or boolean), got ${actualType}. ` +\n `Objects and arrays are not supported in URL parameters.`,\n );\n }\n}\n\n/**\n * Safely extracts own properties from params object.\n * Uses Object.hasOwn to prevent prototype pollution attacks.\n *\n * @param params - Parameters object (may contain inherited properties)\n * @returns New object with only own properties\n *\n * @example\n * const malicious = Object.create({ __proto__: { admin: true } });\n * malicious.mode = 'dev';\n * const safe = extractOwnParams(malicious); // { mode: 'dev' } (no __proto__)\n */\nexport function extractOwnParams(params: Params): Params {\n const result: Params = {};\n\n for (const key in params) {\n // Only process own properties, skip inherited ones\n if (Object.hasOwn(params, key)) {\n result[key] = params[key];\n }\n }\n\n return result;\n}\n\n/**\n * Parses path into base path and query string components.\n * Handles edge cases like leading ?, multiple ?, empty path.\n *\n * @param path - Path to parse (e.g., \"/route?param=value\")\n * @returns Object with basePath and queryString\n *\n * @example\n * parseQueryString('/users?page=1') // { basePath: '/users', queryString: 'page=1' }\n * parseQueryString('?existing') // { basePath: '', queryString: 'existing' }\n * parseQueryString('/path') // { basePath: '/path', queryString: '' }\n */\nexport function parseQueryString(path: string): {\n basePath: string;\n queryString: string;\n} {\n const questionMarkIndex = path.indexOf(\"?\");\n\n // No query string\n if (questionMarkIndex === -1) {\n return { basePath: path, queryString: \"\" };\n }\n\n // Path starts with ? (edge case)\n if (questionMarkIndex === 0) {\n return { basePath: \"\", queryString: path.slice(1) };\n }\n\n // Normal case: path?query\n return {\n basePath: path.slice(0, questionMarkIndex),\n queryString: path.slice(questionMarkIndex + 1),\n };\n}\n\n/**\n * Builds query string from parameter names.\n * Preserves existing query parameters and appends new ones.\n *\n * @param existingQuery - Existing query string (without leading ?)\n * @param paramNames - Parameter names to append\n * @returns Combined query string\n *\n * @example\n * buildQueryString('existing=1', ['mode', 'lang']) // 'existing=1&mode&lang'\n * buildQueryString('', ['mode']) // 'mode'\n */\nexport function buildQueryString(\n existingQuery: string,\n paramNames: readonly string[],\n): string {\n if (paramNames.length === 0) {\n return existingQuery;\n }\n\n const separator = existingQuery ? \"&\" : \"\";\n\n return existingQuery + separator + paramNames.join(\"&\");\n}\n\n/**\n * Merges persistent and current parameters into a single Params object.\n * Keys explicitly set to `undefined` in current params are removed from result.\n *\n * Creates a new immutable object - does not mutate input parameters.\n *\n * @param persistent - Frozen persistent parameters\n * @param current - Current parameters from navigation\n * @returns New Params object with merged values\n *\n * @example\n * const persistent = { lang: 'en', theme: 'dark' };\n * const current = { theme: 'light', mode: 'dev' };\n * mergeParams(persistent, current); // { lang: 'en', theme: 'light', mode: 'dev' }\n *\n * @example\n * // Removing parameters with undefined\n * const persistent = { lang: 'en', theme: 'dark' };\n * const current = { theme: undefined };\n * mergeParams(persistent, current); // { lang: 'en' } (theme removed)\n */\nexport function mergeParams(\n persistent: Readonly<Params>,\n current: Params,\n): Params {\n // Safely extract own properties from current params\n const safeCurrentParams = extractOwnParams(current);\n\n // Start with persistent params, but EXCLUDE undefined values\n // (undefined values don't appear in URLs, so we shouldn't include them)\n const result: Params = {};\n\n for (const key in persistent) {\n if (Object.hasOwn(persistent, key) && persistent[key] !== undefined) {\n result[key] = persistent[key];\n }\n }\n\n // Apply current params\n for (const key of Object.keys(safeCurrentParams)) {\n const value = safeCurrentParams[key];\n\n if (value === undefined) {\n // Remove param if explicitly set to undefined\n delete result[key];\n } else {\n // Add or update param\n result[key] = value;\n }\n }\n\n return result;\n}\n","// packages/persistent-params-plugin/modules/plugin.ts\n\nimport { PLUGIN_MARKER } from \"./constants\";\nimport {\n buildQueryString,\n extractOwnParams,\n isValidParamsConfig,\n mergeParams,\n parseQueryString,\n validateParamValue,\n} from \"./utils\";\n\nimport type { PersistentParamsConfig } from \"./types\";\nimport type { Params, PluginFactory, Plugin } from \"@real-router/core\";\n\n/**\n * Factory for the persistent parameters' plugin.\n *\n * This plugin allows you to specify certain route parameters to be persisted across\n * all navigation transitions. Persisted parameters are automatically merged into\n * route parameters when building paths or states.\n *\n * Key features:\n * - Automatic persistence of query parameters across navigations\n * - Support for default values\n * - Type-safe (only primitives: string, number, boolean)\n * - Immutable internal state\n * - Protection against prototype pollution\n * - Full teardown support (can be safely unsubscribed)\n *\n * If a persisted parameter is explicitly set to `undefined` during navigation,\n * it will be removed from the persisted state and omitted from subsequent URLs.\n *\n * The plugin also adjusts the router's root path to include query parameters for\n * all persistent params, ensuring correct URL construction.\n *\n * @param params - Either an array of parameter names (strings) to persist,\n * or an object mapping parameter names to initial values.\n * If an array, initial values will be `undefined`.\n *\n * @returns A PluginFactory that creates the persistent params plugin instance.\n *\n * @example\n * // Persist parameters without default values\n * router.usePlugin(persistentParamsPlugin(['mode', 'lang']));\n *\n * @example\n * // Persist parameters with default values\n * router.usePlugin(persistentParamsPlugin({ mode: 'dev', lang: 'en' }));\n *\n * @example\n * // Removing a persisted parameter\n * router.navigate('route', { mode: undefined }); // mode will be removed\n *\n * @example\n * // Unsubscribing (full cleanup)\n * const unsubscribe = router.usePlugin(persistentParamsPlugin(['mode']));\n * unsubscribe(); // Restores original router state\n *\n * @throws {TypeError} If params is not a valid array of strings or object with primitives\n * @throws {Error} If plugin is already initialized on this router instance\n */\nexport function persistentParamsPluginFactory(\n params: PersistentParamsConfig = {},\n): PluginFactory {\n // Validate input configuration\n if (!isValidParamsConfig(params)) {\n let actualType: string;\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (params === null) {\n actualType = \"null\";\n } else if (Array.isArray(params)) {\n actualType = \"array with invalid items\";\n } else {\n actualType = typeof params;\n }\n\n throw new TypeError(\n `[@real-router/persistent-params-plugin] Invalid params configuration. ` +\n `Expected array of non-empty strings or object with primitive values, got ${actualType}.`,\n );\n }\n\n // Empty configuration - valid but does nothing\n if (Array.isArray(params) && params.length === 0) {\n return () => ({});\n }\n\n if (!Array.isArray(params) && Object.keys(params).length === 0) {\n return () => ({});\n }\n\n return (router): Plugin => {\n // Check if plugin is already initialized on this router\n if (PLUGIN_MARKER in router) {\n throw new Error(\n `[@real-router/persistent-params-plugin] Plugin already initialized on this router. ` +\n `To reconfigure, first unsubscribe the existing plugin using the returned unsubscribe function.`,\n );\n }\n\n // Mark router as initialized\n (router as Record<symbol, boolean>)[PLUGIN_MARKER] = true;\n\n // Initialize frozen persistent parameters\n let persistentParams: Readonly<Params>;\n\n if (Array.isArray(params)) {\n const initial: Params = {};\n\n for (const param of params) {\n initial[param] = undefined;\n }\n\n persistentParams = Object.freeze(initial);\n } else {\n persistentParams = Object.freeze({ ...params });\n }\n\n // Track parameter names\n const paramNamesSet = new Set<string>(\n Array.isArray(params) ? [...params] : Object.keys(params),\n );\n\n // Store original router methods for restoration\n const originalBuildPath = router.buildPath.bind(router);\n const originalForwardState = router.forwardState.bind(router);\n const originalRootPath = router.getRootPath();\n\n // Update router root path to include query parameters for persistent params\n try {\n const { basePath, queryString } = parseQueryString(originalRootPath);\n // Note: newQueryString is always non-empty here because:\n // - Empty params are handled by early returns at lines 94-100\n // - So paramNamesSet always has at least one element\n // - So buildQueryString always returns a non-empty string\n const newQueryString = buildQueryString(queryString, [...paramNamesSet]);\n\n router.setRootPath(`${basePath}?${newQueryString}`);\n } catch (error) {\n // Rollback initialization marker on error\n delete (router as Record<symbol, boolean>)[PLUGIN_MARKER];\n\n throw new Error(\n `[@real-router/persistent-params-plugin] Failed to update root path: ${error instanceof Error ? error.message : String(error)}`,\n { cause: error },\n );\n }\n\n /**\n * Merges persistent parameters with current navigation parameters.\n * Validates all parameter values before merging.\n *\n * @param additionalParams - Parameters passed during navigation\n * @returns Merged parameters object\n * @throws {TypeError} If any parameter value is invalid (not a primitive)\n */\n\n function withPersistentParams(additionalParams: Params): Params {\n // Extract safe params (prevent prototype pollution)\n const safeParams = extractOwnParams(additionalParams);\n\n // Validate and collect parameters to remove in a single pass\n const paramsToRemove: string[] = [];\n\n for (const key of Object.keys(safeParams)) {\n const value = safeParams[key];\n\n // If undefined and tracked, mark for removal (skip validation)\n if (value === undefined && paramNamesSet.has(key)) {\n paramsToRemove.push(key);\n } else {\n // Validate all other parameters\n validateParamValue(key, value);\n }\n }\n\n // Process all removals in one batch\n if (paramsToRemove.length > 0) {\n // Remove from both Set\n for (const key of paramsToRemove) {\n paramNamesSet.delete(key);\n }\n\n // Update persistentParams once (batch freeze)\n const newParams: Params = { ...persistentParams };\n\n for (const key of paramsToRemove) {\n delete newParams[key];\n }\n\n persistentParams = Object.freeze(newParams);\n }\n\n // Merge persistent and current params\n return mergeParams(persistentParams, safeParams);\n }\n\n // Override router methods to inject persistent params\n // buildPath: needed for direct buildPath() calls (doesn't go through forwardState)\n router.buildPath = (routeName, buildPathParams = {}) =>\n originalBuildPath(routeName, withPersistentParams(buildPathParams));\n\n // forwardState: intercepts params normalization for buildState, buildStateWithSegments, and navigate\n // This is the central point where params are normalized before state creation\n router.forwardState = <P extends Params = Params>(\n routeName: string,\n routeParams: P,\n ) => {\n const result = originalForwardState(routeName, routeParams);\n\n return {\n ...result,\n params: withPersistentParams(result.params) as P,\n };\n };\n\n return {\n /**\n * Updates persistent parameters after successful transition.\n * Only processes parameters that are tracked and have changed.\n *\n * @param toState - Target state after successful transition\n */\n onTransitionSuccess(toState) {\n try {\n // Collect changed parameters and removals\n const updates: Params = {};\n const removals: string[] = [];\n let hasChanges = false;\n\n for (const key of paramNamesSet) {\n const value = toState.params[key];\n\n // If parameter is not in state params or is undefined, mark for removal\n if (!Object.hasOwn(toState.params, key) || value === undefined) {\n // Only mark as removal if it currently exists in persistentParams\n if (\n Object.hasOwn(persistentParams, key) &&\n persistentParams[key] !== undefined\n ) {\n removals.push(key);\n hasChanges = true;\n }\n\n continue;\n }\n\n // Validate type before storing\n validateParamValue(key, value);\n\n // Only update if value actually changed\n if (persistentParams[key] !== value) {\n updates[key] = value;\n hasChanges = true;\n }\n }\n\n // Create new frozen object only if there were changes\n if (hasChanges) {\n const newParams: Params = { ...persistentParams, ...updates };\n\n // Remove parameters that were set to undefined\n for (const key of removals) {\n delete newParams[key];\n }\n\n persistentParams = Object.freeze(newParams);\n }\n } catch (error) {\n // Log error but don't break navigation\n console.error(\n \"persistent-params-plugin\",\n \"Error updating persistent params:\",\n error,\n );\n }\n },\n\n /**\n * Cleanup function to restore original router state.\n * Restores all overridden methods and paths.\n * Called when plugin is unsubscribed.\n */\n teardown() {\n try {\n // Restore original methods\n router.buildPath = originalBuildPath;\n router.forwardState = originalForwardState;\n\n // Restore original root path\n router.setRootPath(originalRootPath);\n\n // Remove initialization marker\n delete (router as Record<symbol, boolean>)[PLUGIN_MARKER];\n } catch (error) {\n console.error(\n \"persistent-params-plugin\",\n \"Error during teardown:\",\n error,\n );\n }\n },\n };\n };\n}\n"]}
@@ -0,0 +1 @@
1
+ {"inputs":{"../../node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.6_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js":{"bytes":569,"imports":[],"format":"esm"},"src/types.ts":{"bytes":515,"imports":[{"path":"/Users/olegivanov/WebstormProjects/real-router/node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.6_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js","kind":"import-statement","external":true}],"format":"esm"},"src/constants.ts":{"bytes":263,"imports":[{"path":"/Users/olegivanov/WebstormProjects/real-router/node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.6_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js","kind":"import-statement","external":true}],"format":"esm"},"src/utils.ts":{"bytes":7512,"imports":[{"path":"type-guards","kind":"import-statement","external":true},{"path":"/Users/olegivanov/WebstormProjects/real-router/node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.6_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js","kind":"import-statement","external":true}],"format":"esm"},"src/plugin.ts":{"bytes":10453,"imports":[{"path":"src/constants.ts","kind":"import-statement","original":"./constants"},{"path":"src/utils.ts","kind":"import-statement","original":"./utils"},{"path":"/Users/olegivanov/WebstormProjects/real-router/node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.6_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js","kind":"import-statement","external":true}],"format":"esm"},"src/index.ts":{"bytes":164,"imports":[{"path":"src/types.ts","kind":"import-statement","original":"./types"},{"path":"src/plugin.ts","kind":"import-statement","original":"./plugin"},{"path":"/Users/olegivanov/WebstormProjects/real-router/node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.6_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js","kind":"import-statement","external":true}],"format":"esm"}},"outputs":{"dist/cjs/index.js.map":{"imports":[],"exports":[],"inputs":{},"bytes":24119},"dist/cjs/index.js":{"imports":[{"path":"type-guards","kind":"import-statement","external":true}],"exports":["PersistentParamsConfig","persistentParamsPluginFactory"],"entryPoint":"src/index.ts","inputs":{"src/constants.ts":{"bytesInOutput":72},"src/utils.ts":{"bytesInOutput":3096},"src/plugin.ts":{"bytesInOutput":5217},"src/index.ts":{"bytesInOutput":0}},"bytes":8484}}}
@@ -0,0 +1,66 @@
1
+ import { PluginFactory } from '@real-router/core';
2
+
3
+ /**
4
+ * Configuration for persistent parameters' plugin.
5
+ * Can be either an array of parameter names or an object with default values.
6
+ *
7
+ * @example
8
+ * // Array of parameter names (initial values undefined)
9
+ * persistentParamsPlugin(['lang', 'theme'])
10
+ *
11
+ * @example
12
+ * // Object with default values
13
+ * persistentParamsPlugin({ lang: 'en', theme: 'light' })
14
+ */
15
+ type PersistentParamsConfig = string[] | Record<string, string | number | boolean>;
16
+
17
+ /**
18
+ * Factory for the persistent parameters' plugin.
19
+ *
20
+ * This plugin allows you to specify certain route parameters to be persisted across
21
+ * all navigation transitions. Persisted parameters are automatically merged into
22
+ * route parameters when building paths or states.
23
+ *
24
+ * Key features:
25
+ * - Automatic persistence of query parameters across navigations
26
+ * - Support for default values
27
+ * - Type-safe (only primitives: string, number, boolean)
28
+ * - Immutable internal state
29
+ * - Protection against prototype pollution
30
+ * - Full teardown support (can be safely unsubscribed)
31
+ *
32
+ * If a persisted parameter is explicitly set to `undefined` during navigation,
33
+ * it will be removed from the persisted state and omitted from subsequent URLs.
34
+ *
35
+ * The plugin also adjusts the router's root path to include query parameters for
36
+ * all persistent params, ensuring correct URL construction.
37
+ *
38
+ * @param params - Either an array of parameter names (strings) to persist,
39
+ * or an object mapping parameter names to initial values.
40
+ * If an array, initial values will be `undefined`.
41
+ *
42
+ * @returns A PluginFactory that creates the persistent params plugin instance.
43
+ *
44
+ * @example
45
+ * // Persist parameters without default values
46
+ * router.usePlugin(persistentParamsPlugin(['mode', 'lang']));
47
+ *
48
+ * @example
49
+ * // Persist parameters with default values
50
+ * router.usePlugin(persistentParamsPlugin({ mode: 'dev', lang: 'en' }));
51
+ *
52
+ * @example
53
+ * // Removing a persisted parameter
54
+ * router.navigate('route', { mode: undefined }); // mode will be removed
55
+ *
56
+ * @example
57
+ * // Unsubscribing (full cleanup)
58
+ * const unsubscribe = router.usePlugin(persistentParamsPlugin(['mode']));
59
+ * unsubscribe(); // Restores original router state
60
+ *
61
+ * @throws {TypeError} If params is not a valid array of strings or object with primitives
62
+ * @throws {Error} If plugin is already initialized on this router instance
63
+ */
64
+ declare function persistentParamsPluginFactory(params?: PersistentParamsConfig): PluginFactory;
65
+
66
+ export { type PersistentParamsConfig, persistentParamsPluginFactory };
@@ -0,0 +1 @@
1
+ import{isPrimitiveValue as r}from"type-guards";var e=Symbol("persistent-params-plugin"),t=/[\s#%&/=?\\]/,n=String.raw`Cannot contain: = & ? # % / \ or whitespace`;function o(r){if(t.test(r))throw new TypeError(`[@real-router/persistent-params-plugin] Invalid parameter name "${r}". ${n}`)}function a(e,t){if(null===t)throw new TypeError(`[@real-router/persistent-params-plugin] Parameter "${e}" cannot be null. Use undefined to remove the parameter from persistence.`);if(void 0!==t&&!r(t)){const r=Array.isArray(t)?"array":typeof t;throw new TypeError(`[@real-router/persistent-params-plugin] Parameter "${e}" must be a primitive value (string, number, or boolean), got ${r}. Objects and arrays are not supported in URL parameters.`)}}function s(r){const e={};for(const t in r)Object.hasOwn(r,t)&&(e[t]=r[t]);return e}function i(t={}){if(null==(n=t)||!(Array.isArray(n)?n.every(r=>{if("string"!=typeof r||0===r.length)return!1;try{return o(r),!0}catch{return!1}}):"object"==typeof n&&Object.getPrototypeOf(n)===Object.prototype&&Object.entries(n).every(([e,t])=>{if("string"!=typeof e||0===e.length)return!1;try{o(e)}catch{return!1}return r(t)}))){let r;throw r=null===t?"null":Array.isArray(t)?"array with invalid items":typeof t,new TypeError(`[@real-router/persistent-params-plugin] Invalid params configuration. Expected array of non-empty strings or object with primitive values, got ${r}.`)}var n;return Array.isArray(t)&&0===t.length?()=>({}):Array.isArray(t)||0!==Object.keys(t).length?r=>{if(e in r)throw new Error("[@real-router/persistent-params-plugin] Plugin already initialized on this router. To reconfigure, first unsubscribe the existing plugin using the returned unsubscribe function.");let n;if(r[e]=!0,Array.isArray(t)){const r={};for(const e of t)r[e]=void 0;n=Object.freeze(r)}else n=Object.freeze({...t});const o=new Set(Array.isArray(t)?[...t]:Object.keys(t)),i=r.buildPath.bind(r),c=r.forwardState.bind(r),u=r.getRootPath();try{const{basePath:e,queryString:t}=function(r){const e=r.indexOf("?");return-1===e?{basePath:r,queryString:""}:0===e?{basePath:"",queryString:r.slice(1)}:{basePath:r.slice(0,e),queryString:r.slice(e+1)}}(u),n=(p=t,0===(l=[...o]).length?p:p+(p?"&":"")+l.join("&"));r.setRootPath(`${e}?${n}`)}catch(t){throw delete r[e],new Error(`[@real-router/persistent-params-plugin] Failed to update root path: ${t instanceof Error?t.message:String(t)}`,{cause:t})}var p,l;function f(r){const e=s(r),t=[];for(const r of Object.keys(e)){const n=e[r];void 0===n&&o.has(r)?t.push(r):a(r,n)}if(t.length>0){for(const r of t)o.delete(r);const r={...n};for(const e of t)delete r[e];n=Object.freeze(r)}return function(r,e){const t=s(e),n={};for(const e in r)Object.hasOwn(r,e)&&void 0!==r[e]&&(n[e]=r[e]);for(const r of Object.keys(t)){const e=t[r];void 0===e?delete n[r]:n[r]=e}return n}(n,e)}return r.buildPath=(r,e={})=>i(r,f(e)),r.forwardState=(r,e)=>{const t=c(r,e);return{...t,params:f(t.params)}},{onTransitionSuccess(r){try{const e={},t=[];let s=!1;for(const i of o){const o=r.params[i];Object.hasOwn(r.params,i)&&void 0!==o?(a(i,o),n[i]!==o&&(e[i]=o,s=!0)):Object.hasOwn(n,i)&&void 0!==n[i]&&(t.push(i),s=!0)}if(s){const r={...n,...e};for(const e of t)delete r[e];n=Object.freeze(r)}}catch(r){console.error("persistent-params-plugin","Error updating persistent params:",r)}},teardown(){try{r.buildPath=i,r.forwardState=c,r.setRootPath(u),delete r[e]}catch(r){console.error("persistent-params-plugin","Error during teardown:",r)}}}}:()=>({})}export{i as persistentParamsPluginFactory};//# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/constants.ts","../../src/utils.ts","../../src/plugin.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["// packages/persistent-params-plugin/modules/constants.ts\n\n/**\n * Symbol to mark router as initialized with this plugin.\n * Prevents double initialization and memory leaks from method wrapping.\n */\nexport const PLUGIN_MARKER = Symbol(\"persistent-params-plugin\");\n","// packages/persistent-params-plugin/modules/utils.ts\n\nimport { isPrimitiveValue } from \"type-guards\";\n\nimport type { PersistentParamsConfig } from \"./types\";\nimport type { Params } from \"@real-router/core\";\n\nconst INVALID_PARAM_KEY_REGEX = /[\\s#%&/=?\\\\]/;\nconst INVALID_CHARS_MESSAGE = String.raw`Cannot contain: = & ? # % / \\ or whitespace`;\n\nexport function validateParamKey(key: string): void {\n if (INVALID_PARAM_KEY_REGEX.test(key)) {\n throw new TypeError(\n `[@real-router/persistent-params-plugin] Invalid parameter name \"${key}\". ${INVALID_CHARS_MESSAGE}`,\n );\n }\n}\n\n/**\n * Validates params configuration structure and values.\n * Ensures all parameter names are non-empty strings and all default values are primitives.\n *\n * @param config - Configuration to validate\n * @returns true if configuration is valid\n */\n/**\n * Validates params configuration structure and values.\n * Ensures all parameter names are non-empty strings and all default values are primitives.\n *\n * @param config - Configuration to validate\n * @returns true if configuration is valid\n */\nexport function isValidParamsConfig(\n config: unknown,\n): config is PersistentParamsConfig {\n if (config === null || config === undefined) {\n return false;\n }\n\n // Array configuration: all items must be non-empty strings\n if (Array.isArray(config)) {\n return config.every((item) => {\n if (typeof item !== \"string\" || item.length === 0) {\n return false;\n }\n\n try {\n validateParamKey(item);\n\n return true;\n } catch {\n return false;\n }\n });\n }\n\n // Object configuration: must be plain object with primitive values\n if (typeof config === \"object\") {\n // Reject non-plain objects (Date, Map, etc.)\n if (Object.getPrototypeOf(config) !== Object.prototype) {\n return false;\n }\n\n // All keys must be non-empty strings, all values must be primitives\n return Object.entries(config).every(([key, value]) => {\n // Check key is non-empty string\n if (typeof key !== \"string\" || key.length === 0) {\n return false;\n }\n\n // Validate key doesn't contain special characters\n try {\n validateParamKey(key);\n } catch {\n return false;\n }\n\n // Validate value is primitive (NaN/Infinity already rejected by isPrimitiveValue)\n return isPrimitiveValue(value);\n });\n }\n\n return false;\n}\n\n/**\n * Validates parameter value before persisting.\n * Throws descriptive TypeError if value is not valid for URL parameters.\n *\n * @param key - Parameter name for error messages\n * @param value - Value to validate\n * @throws {TypeError} If value is null, array, object, or other non-primitive type\n */\nexport function validateParamValue(key: string, value: unknown): void {\n if (value === null) {\n throw new TypeError(\n `[@real-router/persistent-params-plugin] Parameter \"${key}\" cannot be null. ` +\n `Use undefined to remove the parameter from persistence.`,\n );\n }\n\n if (value !== undefined && !isPrimitiveValue(value)) {\n const actualType = Array.isArray(value) ? \"array\" : typeof value;\n\n throw new TypeError(\n `[@real-router/persistent-params-plugin] Parameter \"${key}\" must be a primitive value ` +\n `(string, number, or boolean), got ${actualType}. ` +\n `Objects and arrays are not supported in URL parameters.`,\n );\n }\n}\n\n/**\n * Safely extracts own properties from params object.\n * Uses Object.hasOwn to prevent prototype pollution attacks.\n *\n * @param params - Parameters object (may contain inherited properties)\n * @returns New object with only own properties\n *\n * @example\n * const malicious = Object.create({ __proto__: { admin: true } });\n * malicious.mode = 'dev';\n * const safe = extractOwnParams(malicious); // { mode: 'dev' } (no __proto__)\n */\nexport function extractOwnParams(params: Params): Params {\n const result: Params = {};\n\n for (const key in params) {\n // Only process own properties, skip inherited ones\n if (Object.hasOwn(params, key)) {\n result[key] = params[key];\n }\n }\n\n return result;\n}\n\n/**\n * Parses path into base path and query string components.\n * Handles edge cases like leading ?, multiple ?, empty path.\n *\n * @param path - Path to parse (e.g., \"/route?param=value\")\n * @returns Object with basePath and queryString\n *\n * @example\n * parseQueryString('/users?page=1') // { basePath: '/users', queryString: 'page=1' }\n * parseQueryString('?existing') // { basePath: '', queryString: 'existing' }\n * parseQueryString('/path') // { basePath: '/path', queryString: '' }\n */\nexport function parseQueryString(path: string): {\n basePath: string;\n queryString: string;\n} {\n const questionMarkIndex = path.indexOf(\"?\");\n\n // No query string\n if (questionMarkIndex === -1) {\n return { basePath: path, queryString: \"\" };\n }\n\n // Path starts with ? (edge case)\n if (questionMarkIndex === 0) {\n return { basePath: \"\", queryString: path.slice(1) };\n }\n\n // Normal case: path?query\n return {\n basePath: path.slice(0, questionMarkIndex),\n queryString: path.slice(questionMarkIndex + 1),\n };\n}\n\n/**\n * Builds query string from parameter names.\n * Preserves existing query parameters and appends new ones.\n *\n * @param existingQuery - Existing query string (without leading ?)\n * @param paramNames - Parameter names to append\n * @returns Combined query string\n *\n * @example\n * buildQueryString('existing=1', ['mode', 'lang']) // 'existing=1&mode&lang'\n * buildQueryString('', ['mode']) // 'mode'\n */\nexport function buildQueryString(\n existingQuery: string,\n paramNames: readonly string[],\n): string {\n if (paramNames.length === 0) {\n return existingQuery;\n }\n\n const separator = existingQuery ? \"&\" : \"\";\n\n return existingQuery + separator + paramNames.join(\"&\");\n}\n\n/**\n * Merges persistent and current parameters into a single Params object.\n * Keys explicitly set to `undefined` in current params are removed from result.\n *\n * Creates a new immutable object - does not mutate input parameters.\n *\n * @param persistent - Frozen persistent parameters\n * @param current - Current parameters from navigation\n * @returns New Params object with merged values\n *\n * @example\n * const persistent = { lang: 'en', theme: 'dark' };\n * const current = { theme: 'light', mode: 'dev' };\n * mergeParams(persistent, current); // { lang: 'en', theme: 'light', mode: 'dev' }\n *\n * @example\n * // Removing parameters with undefined\n * const persistent = { lang: 'en', theme: 'dark' };\n * const current = { theme: undefined };\n * mergeParams(persistent, current); // { lang: 'en' } (theme removed)\n */\nexport function mergeParams(\n persistent: Readonly<Params>,\n current: Params,\n): Params {\n // Safely extract own properties from current params\n const safeCurrentParams = extractOwnParams(current);\n\n // Start with persistent params, but EXCLUDE undefined values\n // (undefined values don't appear in URLs, so we shouldn't include them)\n const result: Params = {};\n\n for (const key in persistent) {\n if (Object.hasOwn(persistent, key) && persistent[key] !== undefined) {\n result[key] = persistent[key];\n }\n }\n\n // Apply current params\n for (const key of Object.keys(safeCurrentParams)) {\n const value = safeCurrentParams[key];\n\n if (value === undefined) {\n // Remove param if explicitly set to undefined\n delete result[key];\n } else {\n // Add or update param\n result[key] = value;\n }\n }\n\n return result;\n}\n","// packages/persistent-params-plugin/modules/plugin.ts\n\nimport { PLUGIN_MARKER } from \"./constants\";\nimport {\n buildQueryString,\n extractOwnParams,\n isValidParamsConfig,\n mergeParams,\n parseQueryString,\n validateParamValue,\n} from \"./utils\";\n\nimport type { PersistentParamsConfig } from \"./types\";\nimport type { Params, PluginFactory, Plugin } from \"@real-router/core\";\n\n/**\n * Factory for the persistent parameters' plugin.\n *\n * This plugin allows you to specify certain route parameters to be persisted across\n * all navigation transitions. Persisted parameters are automatically merged into\n * route parameters when building paths or states.\n *\n * Key features:\n * - Automatic persistence of query parameters across navigations\n * - Support for default values\n * - Type-safe (only primitives: string, number, boolean)\n * - Immutable internal state\n * - Protection against prototype pollution\n * - Full teardown support (can be safely unsubscribed)\n *\n * If a persisted parameter is explicitly set to `undefined` during navigation,\n * it will be removed from the persisted state and omitted from subsequent URLs.\n *\n * The plugin also adjusts the router's root path to include query parameters for\n * all persistent params, ensuring correct URL construction.\n *\n * @param params - Either an array of parameter names (strings) to persist,\n * or an object mapping parameter names to initial values.\n * If an array, initial values will be `undefined`.\n *\n * @returns A PluginFactory that creates the persistent params plugin instance.\n *\n * @example\n * // Persist parameters without default values\n * router.usePlugin(persistentParamsPlugin(['mode', 'lang']));\n *\n * @example\n * // Persist parameters with default values\n * router.usePlugin(persistentParamsPlugin({ mode: 'dev', lang: 'en' }));\n *\n * @example\n * // Removing a persisted parameter\n * router.navigate('route', { mode: undefined }); // mode will be removed\n *\n * @example\n * // Unsubscribing (full cleanup)\n * const unsubscribe = router.usePlugin(persistentParamsPlugin(['mode']));\n * unsubscribe(); // Restores original router state\n *\n * @throws {TypeError} If params is not a valid array of strings or object with primitives\n * @throws {Error} If plugin is already initialized on this router instance\n */\nexport function persistentParamsPluginFactory(\n params: PersistentParamsConfig = {},\n): PluginFactory {\n // Validate input configuration\n if (!isValidParamsConfig(params)) {\n let actualType: string;\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (params === null) {\n actualType = \"null\";\n } else if (Array.isArray(params)) {\n actualType = \"array with invalid items\";\n } else {\n actualType = typeof params;\n }\n\n throw new TypeError(\n `[@real-router/persistent-params-plugin] Invalid params configuration. ` +\n `Expected array of non-empty strings or object with primitive values, got ${actualType}.`,\n );\n }\n\n // Empty configuration - valid but does nothing\n if (Array.isArray(params) && params.length === 0) {\n return () => ({});\n }\n\n if (!Array.isArray(params) && Object.keys(params).length === 0) {\n return () => ({});\n }\n\n return (router): Plugin => {\n // Check if plugin is already initialized on this router\n if (PLUGIN_MARKER in router) {\n throw new Error(\n `[@real-router/persistent-params-plugin] Plugin already initialized on this router. ` +\n `To reconfigure, first unsubscribe the existing plugin using the returned unsubscribe function.`,\n );\n }\n\n // Mark router as initialized\n (router as Record<symbol, boolean>)[PLUGIN_MARKER] = true;\n\n // Initialize frozen persistent parameters\n let persistentParams: Readonly<Params>;\n\n if (Array.isArray(params)) {\n const initial: Params = {};\n\n for (const param of params) {\n initial[param] = undefined;\n }\n\n persistentParams = Object.freeze(initial);\n } else {\n persistentParams = Object.freeze({ ...params });\n }\n\n // Track parameter names\n const paramNamesSet = new Set<string>(\n Array.isArray(params) ? [...params] : Object.keys(params),\n );\n\n // Store original router methods for restoration\n const originalBuildPath = router.buildPath.bind(router);\n const originalForwardState = router.forwardState.bind(router);\n const originalRootPath = router.getRootPath();\n\n // Update router root path to include query parameters for persistent params\n try {\n const { basePath, queryString } = parseQueryString(originalRootPath);\n // Note: newQueryString is always non-empty here because:\n // - Empty params are handled by early returns at lines 94-100\n // - So paramNamesSet always has at least one element\n // - So buildQueryString always returns a non-empty string\n const newQueryString = buildQueryString(queryString, [...paramNamesSet]);\n\n router.setRootPath(`${basePath}?${newQueryString}`);\n } catch (error) {\n // Rollback initialization marker on error\n delete (router as Record<symbol, boolean>)[PLUGIN_MARKER];\n\n throw new Error(\n `[@real-router/persistent-params-plugin] Failed to update root path: ${error instanceof Error ? error.message : String(error)}`,\n { cause: error },\n );\n }\n\n /**\n * Merges persistent parameters with current navigation parameters.\n * Validates all parameter values before merging.\n *\n * @param additionalParams - Parameters passed during navigation\n * @returns Merged parameters object\n * @throws {TypeError} If any parameter value is invalid (not a primitive)\n */\n\n function withPersistentParams(additionalParams: Params): Params {\n // Extract safe params (prevent prototype pollution)\n const safeParams = extractOwnParams(additionalParams);\n\n // Validate and collect parameters to remove in a single pass\n const paramsToRemove: string[] = [];\n\n for (const key of Object.keys(safeParams)) {\n const value = safeParams[key];\n\n // If undefined and tracked, mark for removal (skip validation)\n if (value === undefined && paramNamesSet.has(key)) {\n paramsToRemove.push(key);\n } else {\n // Validate all other parameters\n validateParamValue(key, value);\n }\n }\n\n // Process all removals in one batch\n if (paramsToRemove.length > 0) {\n // Remove from both Set\n for (const key of paramsToRemove) {\n paramNamesSet.delete(key);\n }\n\n // Update persistentParams once (batch freeze)\n const newParams: Params = { ...persistentParams };\n\n for (const key of paramsToRemove) {\n delete newParams[key];\n }\n\n persistentParams = Object.freeze(newParams);\n }\n\n // Merge persistent and current params\n return mergeParams(persistentParams, safeParams);\n }\n\n // Override router methods to inject persistent params\n // buildPath: needed for direct buildPath() calls (doesn't go through forwardState)\n router.buildPath = (routeName, buildPathParams = {}) =>\n originalBuildPath(routeName, withPersistentParams(buildPathParams));\n\n // forwardState: intercepts params normalization for buildState, buildStateWithSegments, and navigate\n // This is the central point where params are normalized before state creation\n router.forwardState = <P extends Params = Params>(\n routeName: string,\n routeParams: P,\n ) => {\n const result = originalForwardState(routeName, routeParams);\n\n return {\n ...result,\n params: withPersistentParams(result.params) as P,\n };\n };\n\n return {\n /**\n * Updates persistent parameters after successful transition.\n * Only processes parameters that are tracked and have changed.\n *\n * @param toState - Target state after successful transition\n */\n onTransitionSuccess(toState) {\n try {\n // Collect changed parameters and removals\n const updates: Params = {};\n const removals: string[] = [];\n let hasChanges = false;\n\n for (const key of paramNamesSet) {\n const value = toState.params[key];\n\n // If parameter is not in state params or is undefined, mark for removal\n if (!Object.hasOwn(toState.params, key) || value === undefined) {\n // Only mark as removal if it currently exists in persistentParams\n if (\n Object.hasOwn(persistentParams, key) &&\n persistentParams[key] !== undefined\n ) {\n removals.push(key);\n hasChanges = true;\n }\n\n continue;\n }\n\n // Validate type before storing\n validateParamValue(key, value);\n\n // Only update if value actually changed\n if (persistentParams[key] !== value) {\n updates[key] = value;\n hasChanges = true;\n }\n }\n\n // Create new frozen object only if there were changes\n if (hasChanges) {\n const newParams: Params = { ...persistentParams, ...updates };\n\n // Remove parameters that were set to undefined\n for (const key of removals) {\n delete newParams[key];\n }\n\n persistentParams = Object.freeze(newParams);\n }\n } catch (error) {\n // Log error but don't break navigation\n console.error(\n \"persistent-params-plugin\",\n \"Error updating persistent params:\",\n error,\n );\n }\n },\n\n /**\n * Cleanup function to restore original router state.\n * Restores all overridden methods and paths.\n * Called when plugin is unsubscribed.\n */\n teardown() {\n try {\n // Restore original methods\n router.buildPath = originalBuildPath;\n router.forwardState = originalForwardState;\n\n // Restore original root path\n router.setRootPath(originalRootPath);\n\n // Remove initialization marker\n delete (router as Record<symbol, boolean>)[PLUGIN_MARKER];\n } catch (error) {\n console.error(\n \"persistent-params-plugin\",\n \"Error during teardown:\",\n error,\n );\n }\n },\n };\n };\n}\n"]}
@@ -0,0 +1 @@
1
+ {"inputs":{"src/types.ts":{"bytes":515,"imports":[],"format":"esm"},"src/constants.ts":{"bytes":263,"imports":[],"format":"esm"},"src/utils.ts":{"bytes":7512,"imports":[{"path":"type-guards","kind":"import-statement","external":true}],"format":"esm"},"src/plugin.ts":{"bytes":10453,"imports":[{"path":"src/constants.ts","kind":"import-statement","original":"./constants"},{"path":"src/utils.ts","kind":"import-statement","original":"./utils"}],"format":"esm"},"src/index.ts":{"bytes":164,"imports":[{"path":"src/types.ts","kind":"import-statement","original":"./types"},{"path":"src/plugin.ts","kind":"import-statement","original":"./plugin"}],"format":"esm"}},"outputs":{"dist/esm/index.mjs.map":{"imports":[],"exports":[],"inputs":{},"bytes":24119},"dist/esm/index.mjs":{"imports":[{"path":"type-guards","kind":"import-statement","external":true}],"exports":["PersistentParamsConfig","persistentParamsPluginFactory"],"entryPoint":"src/index.ts","inputs":{"src/constants.ts":{"bytesInOutput":72},"src/utils.ts":{"bytesInOutput":3096},"src/plugin.ts":{"bytesInOutput":5217},"src/index.ts":{"bytesInOutput":0}},"bytes":8484}}}
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@real-router/persistent-params-plugin",
3
+ "version": "0.1.0",
4
+ "type": "commonjs",
5
+ "description": "Persist query parameters across route transitions",
6
+ "main": "./dist/cjs/index.js",
7
+ "module": "./dist/esm/index.mjs",
8
+ "types": "./dist/esm/index.d.mts",
9
+ "exports": {
10
+ ".": {
11
+ "types": {
12
+ "import": "./dist/esm/index.d.mts",
13
+ "require": "./dist/cjs/index.d.ts"
14
+ },
15
+ "import": "./dist/esm/index.mjs",
16
+ "require": "./dist/cjs/index.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist"
21
+ ],
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/greydragon888/real-router.git"
25
+ },
26
+ "keywords": [
27
+ "real-router",
28
+ "params",
29
+ "persistence",
30
+ "query-params",
31
+ "url-params",
32
+ "state"
33
+ ],
34
+ "author": {
35
+ "name": "Oleg Ivanov",
36
+ "email": "greydragon888@gmail.com",
37
+ "url": "https://github.com/greydragon888"
38
+ },
39
+ "license": "MIT",
40
+ "bugs": {
41
+ "url": "https://github.com/greydragon888/real-router/issues"
42
+ },
43
+ "homepage": "https://github.com/greydragon888/real-router",
44
+ "scripts": {
45
+ "test": "vitest",
46
+ "build": "tsup",
47
+ "type-check": "tsc --noEmit",
48
+ "lint": "eslint --cache --ext .ts src/ tests/ --fix --max-warnings 0",
49
+ "lint:package": "publint",
50
+ "lint:types": "attw --pack ."
51
+ },
52
+ "sideEffects": false,
53
+ "dependencies": {
54
+ "@real-router/core": "workspace:^",
55
+ "type-guards": "workspace:^"
56
+ }
57
+ }