@real-router/persistent-params-plugin 0.1.1 → 0.1.3

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 CHANGED
@@ -3,31 +3,21 @@
3
3
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
4
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-blue.svg)](https://www.typescriptlang.org/)
5
5
 
6
- A plugin that automatically persists and applies specified query parameters across all navigation transitions.
6
+ Automatically persists query parameters across all navigation transitions.
7
7
 
8
- ## Problem
9
-
10
- In SPA applications, certain query parameters should persist between routes:
8
+ ## Problem & Solution
11
9
 
12
10
  ```typescript
13
11
  // 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
- ```
12
+ router.navigate("products", { lang: "en", theme: "dark" });
13
+ router.navigate("cart");
14
+ // URL: /cart ← lang and theme are lost
20
15
 
21
- ## Solution
22
-
23
- ```typescript
16
+ // With plugin:
24
17
  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
18
+ router.navigate("products", { lang: "en", theme: "dark" });
19
+ router.navigate("cart");
20
+ // URL: /cart?lang=en&theme=dark ← automatically preserved
31
21
  ```
32
22
 
33
23
  ## Installation
@@ -48,16 +38,12 @@ bun add @real-router/persistent-params-plugin
48
38
  import { createRouter } from "@real-router/core";
49
39
  import { persistentParamsPluginFactory } from "@real-router/persistent-params-plugin";
50
40
 
51
- const router = createRouter([
52
- { name: "home", path: "/" },
53
- { name: "products", path: "/products/:id" },
54
- { name: "cart", path: "/cart" },
55
- ]);
41
+ const router = createRouter(routes);
56
42
 
57
- // Option 1: Specify parameter names
43
+ // Option 1: Parameter names (values set on first use)
58
44
  router.usePlugin(persistentParamsPluginFactory(["lang", "theme"]));
59
45
 
60
- // Option 2: Specify default values
46
+ // Option 2: With default values
61
47
  router.usePlugin(
62
48
  persistentParamsPluginFactory({
63
49
  lang: "en",
@@ -68,334 +54,65 @@ router.usePlugin(
68
54
  router.start();
69
55
  ```
70
56
 
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:
57
+ ---
100
58
 
101
- ```typescript
102
- router.navigate("route1", { mode: "dev" });
103
- // Saved: mode=dev
104
-
105
- router.navigate("route2", {});
106
- // URL includes: ?mode=dev
107
- ```
59
+ ## Configuration
108
60
 
109
- #### Object with Defaults
61
+ | Config Type | Description | Example |
62
+ | --------------------------- | ------------------------------------------- | ------------------- |
63
+ | `string[]` | Parameter names, initial values `undefined` | `["lang", "theme"]` |
64
+ | `Record<string, primitive>` | Parameter names with defaults | `{ lang: "en" }` |
110
65
 
111
- ```typescript
112
- persistentParamsPluginFactory({
113
- mode: "prod",
114
- lang: "en",
115
- debug: false,
116
- });
117
- ```
66
+ **Allowed value types:** `string`, `number`, `boolean`, `undefined` (to remove)
118
67
 
119
- Default values are applied immediately:
68
+ See [Wiki](https://github.com/greydragon888/real-router/wiki/persistent-params-plugin#3-configuration-options) for details.
120
69
 
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
- ```
70
+ ---
134
71
 
135
72
  ## Behavior
136
73
 
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
74
+ ### Persistence
268
75
 
269
76
  ```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
77
+ router.navigate("page1", { lang: "en" }); // Saved: lang=en
78
+ router.navigate("page2"); // URL: /page2?lang=en
276
79
  ```
277
80
 
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:
81
+ ### Update
283
82
 
284
83
  ```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
84
+ router.navigate("page", { lang: "fr" }); // Updates saved value
297
85
  ```
298
86
 
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
87
+ ### Remove
306
88
 
307
89
  ```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
90
+ router.navigate("page", { lang: undefined }); // Removes from persistent params
316
91
  ```
317
92
 
318
- ### Constructor Pollution Protection
93
+ ### Priority
319
94
 
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
95
+ Explicit values override saved ones:
334
96
 
335
97
  ```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
98
+ // Saved: lang=en
99
+ router.navigate("page", { lang: "de" }); // URL: /page?lang=de
360
100
  ```
361
101
 
362
- ### Reinitialization
102
+ See [Wiki](https://github.com/greydragon888/real-router/wiki/persistent-params-plugin#8-behavior) for edge cases and guarantees.
363
103
 
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
- ```
104
+ ---
371
105
 
372
106
  ## Usage Examples
373
107
 
374
- ### Multilingual Application
108
+ ### Multilingual App
375
109
 
376
110
  ```typescript
377
111
  router.usePlugin(persistentParamsPluginFactory({ lang: "en" }));
378
112
 
379
- // User changes language
380
113
  router.navigate("settings", { lang: "fr" });
381
-
382
- // Language persists across all transitions
383
- router.navigate("products", { id: "1" }); // ?lang=fr
114
+ router.navigate("products"); // ?lang=fr
384
115
  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
116
  ```
400
117
 
401
118
  ### UTM Tracking
@@ -405,89 +122,40 @@ router.usePlugin(
405
122
  persistentParamsPluginFactory(["utm_source", "utm_medium", "utm_campaign"]),
406
123
  );
407
124
 
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_*
125
+ // User arrives: /?utm_source=google&utm_medium=cpc
126
+ router.navigate("products"); // UTM params preserved
127
+ router.navigate("checkout"); // UTM params preserved
415
128
  ```
416
129
 
417
- ### Filters and Sorting
130
+ ---
418
131
 
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
132
+ ## Lifecycle
436
133
 
437
134
  ```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"]));
135
+ const unsubscribe = router.usePlugin(persistentParamsPluginFactory(["mode"]));
445
136
 
446
- // All plugins work together
137
+ // Later: restore original router behavior
138
+ unsubscribe();
447
139
  ```
448
140
 
449
- ## Error Handling
141
+ **Note:** Double initialization throws an error. Call `unsubscribe()` first.
450
142
 
451
- The plugin validates input and throws meaningful errors for invalid usage:
143
+ ---
452
144
 
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
- ```
145
+ ## Documentation
467
146
 
468
- ## TypeScript
147
+ Full documentation on [Wiki](https://github.com/greydragon888/real-router/wiki/persistent-params-plugin):
469
148
 
470
- The plugin is fully typed:
149
+ - [Configuration Options](https://github.com/greydragon888/real-router/wiki/persistent-params-plugin#3-configuration-options)
150
+ - [Lifecycle Hooks](https://github.com/greydragon888/real-router/wiki/persistent-params-plugin#4-lifecycle-hooks)
151
+ - [Behavior & Edge Cases](https://github.com/greydragon888/real-router/wiki/persistent-params-plugin#8-behavior)
152
+ - [Migration from router5](https://github.com/greydragon888/real-router/wiki/persistent-params-plugin#11-migration-from-router5)
471
153
 
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
- ```
154
+ ---
486
155
 
487
156
  ## Related Packages
488
157
 
489
158
  - [@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
159
  - [@real-router/browser-plugin](https://www.npmjs.com/package/@real-router/browser-plugin) — Browser history
492
160
 
493
161
  ## License