@real-router/persistent-params-plugin 0.1.49 → 0.1.50

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@real-router/persistent-params-plugin",
3
- "version": "0.1.49",
3
+ "version": "0.1.50",
4
4
  "type": "commonjs",
5
5
  "description": "Persist query parameters across route transitions",
6
6
  "main": "./dist/cjs/index.js",
@@ -18,8 +18,7 @@
18
18
  }
19
19
  },
20
20
  "files": [
21
- "dist",
22
- "src"
21
+ "dist"
23
22
  ],
24
23
  "repository": {
25
24
  "type": "git",
@@ -45,7 +44,7 @@
45
44
  "homepage": "https://github.com/greydragon888/real-router",
46
45
  "sideEffects": false,
47
46
  "dependencies": {
48
- "@real-router/core": "^0.45.0"
47
+ "@real-router/core": "^0.45.1"
49
48
  },
50
49
  "devDependencies": {
51
50
  "type-guards": "^0.4.5"
@@ -56,7 +55,7 @@
56
55
  "build": "tsdown --config-loader unrun",
57
56
  "type-check": "tsc --noEmit",
58
57
  "lint": "eslint --cache --ext .ts src/ tests/ --fix --max-warnings 0",
59
- "lint:package": "publint",
58
+ "lint:package": "bash ../../scripts/publint-filter.sh",
60
59
  "lint:types": "attw --pack .",
61
60
  "build:dist-only": "tsdown --config-loader unrun"
62
61
  }
package/src/constants.ts DELETED
@@ -1,5 +0,0 @@
1
- // packages/persistent-params-plugin/src/constants.ts
2
-
3
- export const LOGGER_CONTEXT = "persistent-params-plugin";
4
-
5
- export const ERROR_PREFIX = `[@real-router/${LOGGER_CONTEXT}]`;
package/src/factory.ts DELETED
@@ -1,98 +0,0 @@
1
- // packages/persistent-params-plugin/src/factory.ts
2
-
3
- import { getPluginApi } from "@real-router/core/api";
4
-
5
- import { PersistentParamsPlugin } from "./plugin";
6
- import { validateConfig } from "./validation";
7
-
8
- import type { PersistentParamsConfig } from "./types";
9
- import type { Params, PluginFactory, Plugin } from "@real-router/core";
10
-
11
- // Shared singleton — frozen by core on first use. Do not add properties.
12
- const EMPTY_PLUGIN: Plugin = {};
13
- const noop: PluginFactory = () => EMPTY_PLUGIN;
14
-
15
- /**
16
- * Factory for the persistent parameters' plugin.
17
- *
18
- * This plugin allows you to specify certain route parameters to be persisted across
19
- * all navigation transitions. Persisted parameters are automatically merged into
20
- * route parameters when building paths or states.
21
- *
22
- * Key features:
23
- * - Automatic persistence of query parameters across navigations
24
- * - Support for default values
25
- * - Type-safe (only primitives: string, number, boolean)
26
- * - Immutable internal state
27
- * - Protection against prototype pollution
28
- * - Full teardown support (can be safely unsubscribed)
29
- *
30
- * If a persisted parameter is explicitly set to `undefined` during navigation,
31
- * it will be removed from the persisted state and omitted from subsequent URLs.
32
- *
33
- * The plugin also adjusts the router's root path to include query parameters for
34
- * all persistent params, ensuring correct URL construction.
35
- *
36
- * @param params - Either an array of parameter names (strings) to persist,
37
- * or an object mapping parameter names to initial values.
38
- * If an array, initial values will be `undefined`.
39
- *
40
- * @returns A PluginFactory that creates the persistent params plugin instance.
41
- *
42
- * @example
43
- * // Persist parameters without default values
44
- * router.usePlugin(persistentParamsPlugin(['mode', 'lang']));
45
- *
46
- * @example
47
- * // Persist parameters with default values
48
- * router.usePlugin(persistentParamsPlugin({ mode: 'dev', lang: 'en' }));
49
- *
50
- * @example
51
- * // Removing a persisted parameter
52
- * router.navigate('route', { mode: undefined }); // mode will be removed
53
- *
54
- * @example
55
- * // Unsubscribing (full cleanup)
56
- * const unsubscribe = router.usePlugin(persistentParamsPlugin(['mode']));
57
- * unsubscribe(); // Restores original router state
58
- *
59
- * @throws {TypeError} If params is not a valid array of strings or object with primitives
60
- * @throws {Error} If plugin is already initialized on this router instance
61
- */
62
- export function persistentParamsPluginFactory(
63
- params: PersistentParamsConfig = {},
64
- ): PluginFactory {
65
- validateConfig(params);
66
-
67
- const paramNames = Array.isArray(params) ? params : Object.keys(params);
68
-
69
- if (paramNames.length === 0) {
70
- return noop;
71
- }
72
-
73
- const initialParams: Params = {};
74
-
75
- if (Array.isArray(params)) {
76
- for (const param of params) {
77
- initialParams[param] = undefined;
78
- }
79
- } else {
80
- Object.assign(initialParams, params);
81
- }
82
-
83
- Object.freeze(initialParams);
84
-
85
- const paramNamesSet = new Set<string>(paramNames);
86
-
87
- return (router): Plugin => {
88
- const api = getPluginApi(router);
89
- const plugin = new PersistentParamsPlugin(
90
- api,
91
- initialParams,
92
- new Set(paramNamesSet),
93
- api.getRootPath(),
94
- );
95
-
96
- return plugin.getPlugin();
97
- };
98
- }
package/src/index.ts DELETED
@@ -1,5 +0,0 @@
1
- // packages/persistent-params-plugin/src/index.ts
2
-
3
- export type { PersistentParamsConfig } from "./types";
4
-
5
- export { persistentParamsPluginFactory } from "./factory";
@@ -1,62 +0,0 @@
1
- // packages/persistent-params-plugin/src/param-utils.ts
2
-
3
- import type { Params } from "@real-router/core";
4
-
5
- /**
6
- * Safely extracts own properties from params object.
7
- * Uses Object.hasOwn to prevent prototype pollution attacks.
8
- *
9
- * @param params - Parameters object (may contain inherited properties)
10
- * @returns New object with only own properties
11
- *
12
- * @example
13
- * const malicious = Object.create({ __proto__: { admin: true } });
14
- * malicious.mode = 'dev';
15
- * const safe = extractOwnParams(malicious); // { mode: 'dev' } (no __proto__)
16
- */
17
- export function extractOwnParams(params: Params): Params {
18
- const result: Params = {};
19
-
20
- for (const key in params) {
21
- /* v8 ignore next -- @preserve: core validates params prototype — inherited keys never reach here */
22
- if (Object.hasOwn(params, key)) {
23
- result[key] = params[key];
24
- }
25
- }
26
-
27
- return result;
28
- }
29
-
30
- /**
31
- * Merges persistent and current parameters into a single Params object.
32
- *
33
- * IMPORTANT: `current` must be pre-sanitized via `extractOwnParams()` by the caller.
34
- * This function does NOT perform prototype pollution protection on its own.
35
- *
36
- * @param persistent - Frozen persistent parameters
37
- * @param current - Pre-sanitized current parameters (own properties only)
38
- */
39
- export function mergeParams(
40
- persistent: Readonly<Params>,
41
- current: Params,
42
- ): Params {
43
- const result: Params = {};
44
-
45
- for (const key in persistent) {
46
- if (Object.hasOwn(persistent, key) && persistent[key] !== undefined) {
47
- result[key] = persistent[key];
48
- }
49
- }
50
-
51
- for (const key of Object.keys(current)) {
52
- const value = current[key];
53
-
54
- if (value === undefined) {
55
- delete result[key];
56
- } else {
57
- result[key] = value;
58
- }
59
- }
60
-
61
- return result;
62
- }
package/src/plugin.ts DELETED
@@ -1,148 +0,0 @@
1
- // packages/persistent-params-plugin/src/plugin.ts
2
-
3
- import { ERROR_PREFIX } from "./constants";
4
- import { extractOwnParams, mergeParams } from "./param-utils";
5
- import { validateParamValue } from "./validation";
6
-
7
- import type { Params, State, Plugin } from "@real-router/core";
8
- import type { PluginApi } from "@real-router/core/api";
9
-
10
- export class PersistentParamsPlugin {
11
- readonly #api: PluginApi;
12
- readonly #paramNamesSet: Set<string>;
13
- readonly #originalRootPath: string;
14
- readonly #removeBuildPathInterceptor: () => void;
15
- readonly #removeForwardStateInterceptor: () => void;
16
-
17
- #persistentParams: Readonly<Params>;
18
-
19
- constructor(
20
- api: PluginApi,
21
- persistentParams: Readonly<Params>,
22
- paramNamesSet: Set<string>,
23
- originalRootPath: string,
24
- ) {
25
- this.#api = api;
26
- this.#persistentParams = persistentParams;
27
- this.#paramNamesSet = paramNamesSet;
28
- this.#originalRootPath = originalRootPath;
29
-
30
- let removeBuildPath: (() => void) | undefined;
31
- let removeForwardState: (() => void) | undefined;
32
-
33
- try {
34
- api.setRootPath(`${originalRootPath}?${[...paramNamesSet].join("&")}`);
35
-
36
- removeBuildPath = api.addInterceptor(
37
- "buildPath",
38
- (next, route, navParams) =>
39
- next(route, this.#withPersistentParams(navParams ?? {})),
40
- );
41
-
42
- removeForwardState = api.addInterceptor(
43
- "forwardState",
44
- (next, routeName, routeParams) => {
45
- const result = next(routeName, routeParams);
46
-
47
- return {
48
- ...result,
49
- params: this.#withPersistentParams(result.params),
50
- };
51
- },
52
- );
53
- } /* v8 ignore start -- @preserve: rollback on partial initialization failure */ catch (error) {
54
- removeBuildPath?.();
55
- removeForwardState?.();
56
- api.setRootPath(originalRootPath);
57
-
58
- throw new Error(
59
- `${ERROR_PREFIX} Failed to initialize: ${error instanceof Error ? error.message : String(error)}`,
60
- { cause: error },
61
- );
62
- } /* v8 ignore stop */
63
-
64
- this.#removeBuildPathInterceptor = removeBuildPath;
65
- this.#removeForwardStateInterceptor = removeForwardState;
66
- }
67
-
68
- getPlugin(): Plugin {
69
- return {
70
- onTransitionSuccess: (toState) => {
71
- this.#onTransitionSuccess(toState);
72
- },
73
- teardown: () => {
74
- this.#teardown();
75
- },
76
- };
77
- }
78
-
79
- #withPersistentParams(additionalParams: Params): Params {
80
- const safeParams = extractOwnParams(additionalParams);
81
- let newParams: Params | undefined;
82
-
83
- for (const key of Object.keys(safeParams)) {
84
- const value = safeParams[key];
85
-
86
- if (value === undefined && this.#paramNamesSet.has(key)) {
87
- this.#paramNamesSet.delete(key);
88
- newParams ??= { ...this.#persistentParams };
89
- delete newParams[key];
90
- } else {
91
- validateParamValue(key, value);
92
- }
93
- }
94
-
95
- if (newParams) {
96
- this.#persistentParams = Object.freeze(newParams);
97
- }
98
-
99
- return mergeParams(this.#persistentParams, safeParams);
100
- }
101
-
102
- #onTransitionSuccess(toState: State): void {
103
- let newParams: Params | undefined;
104
-
105
- for (const key of this.#paramNamesSet) {
106
- const value = toState.params[key];
107
-
108
- if (!Object.hasOwn(toState.params, key) || value === undefined) {
109
- /* v8 ignore next 4 -- @preserve: defensive removal for states committed via navigateToState bypassing forwardState */
110
- if (
111
- Object.hasOwn(this.#persistentParams, key) &&
112
- this.#persistentParams[key] !== undefined
113
- ) {
114
- newParams ??= { ...this.#persistentParams };
115
- delete newParams[key];
116
- }
117
-
118
- continue;
119
- }
120
-
121
- validateParamValue(key, value);
122
-
123
- if (this.#persistentParams[key] !== value) {
124
- newParams ??= { ...this.#persistentParams };
125
- newParams[key] = value;
126
- }
127
- }
128
-
129
- if (newParams) {
130
- this.#persistentParams = Object.freeze(newParams);
131
- }
132
- }
133
-
134
- #teardown(): void {
135
- this.#removeBuildPathInterceptor();
136
- this.#removeForwardStateInterceptor();
137
-
138
- /* v8 ignore start -- @preserve: setRootPath throws RouterError(ROUTER_DISPOSED) during router.dispose() */
139
- try {
140
- this.#api.setRootPath(this.#originalRootPath);
141
- } catch {
142
- // Expected during router.dispose(): FSM enters DISPOSED before plugin teardown,
143
- // so setRootPath's throwIfDisposed() check throws. Restoring rootPath on a
144
- // destroyed router is unnecessary — swallow silently.
145
- }
146
- /* v8 ignore stop */
147
- }
148
- }
package/src/types.ts DELETED
@@ -1,17 +0,0 @@
1
- // packages/persistent-params-plugin/src/types.ts
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
- export type PersistentParamsConfig =
16
- | string[]
17
- | Record<string, string | number | boolean>;
package/src/validation.ts DELETED
@@ -1,130 +0,0 @@
1
- // packages/persistent-params-plugin/src/validation.ts
2
-
3
- import { isPrimitiveValue } from "type-guards";
4
-
5
- import { ERROR_PREFIX } from "./constants";
6
-
7
- import type { PersistentParamsConfig } from "./types";
8
-
9
- const INVALID_PARAM_KEY_REGEX = /[\s#%&/=?\\]/;
10
- const INVALID_CHARS_MESSAGE = String.raw`Cannot contain: = & ? # % / \ or whitespace`;
11
-
12
- export function validateParamKey(key: string): void {
13
- if (INVALID_PARAM_KEY_REGEX.test(key)) {
14
- throw new TypeError(
15
- `${ERROR_PREFIX} Invalid parameter name "${key}". ${INVALID_CHARS_MESSAGE}`,
16
- );
17
- }
18
- }
19
-
20
- /**
21
- * Validates params configuration structure and values.
22
- * Ensures all parameter names are non-empty strings and all default values are primitives.
23
- *
24
- * @param config - Configuration to validate
25
- * @returns true if configuration is valid
26
- */
27
- export function isValidParamsConfig(
28
- config: unknown,
29
- ): config is PersistentParamsConfig {
30
- if (config === null || config === undefined) {
31
- return false;
32
- }
33
-
34
- // Array configuration: all items must be non-empty strings
35
- if (Array.isArray(config)) {
36
- return config.every((item) => {
37
- if (typeof item !== "string" || item.length === 0) {
38
- return false;
39
- }
40
-
41
- try {
42
- validateParamKey(item);
43
-
44
- return true;
45
- } catch {
46
- return false;
47
- }
48
- });
49
- }
50
-
51
- // Object configuration: must be plain object with primitive values
52
- if (typeof config === "object") {
53
- // Reject non-plain objects (Date, Map, etc.)
54
- if (Object.getPrototypeOf(config) !== Object.prototype) {
55
- return false;
56
- }
57
-
58
- // All keys must be non-empty strings, all values must be primitives
59
- return Object.entries(config).every(([key, value]) => {
60
- // Check key is non-empty string
61
- if (typeof key !== "string" || key.length === 0) {
62
- return false;
63
- }
64
-
65
- // Validate key doesn't contain special characters
66
- try {
67
- validateParamKey(key);
68
- } catch {
69
- return false;
70
- }
71
-
72
- // Validate value is primitive (NaN/Infinity already rejected by isPrimitiveValue)
73
- return isPrimitiveValue(value);
74
- });
75
- }
76
-
77
- return false;
78
- }
79
-
80
- /**
81
- * Validates parameter value before persisting.
82
- * Throws descriptive TypeError if value is not valid for URL parameters.
83
- *
84
- * @param key - Parameter name for error messages
85
- * @param value - Value to validate
86
- * @throws {TypeError} If value is null, array, object, or other non-primitive type
87
- */
88
- export function validateParamValue(key: string, value: unknown): void {
89
- if (value === null) {
90
- throw new TypeError(
91
- `${ERROR_PREFIX} Parameter "${key}" cannot be null. ` +
92
- `Use undefined to remove the parameter from persistence.`,
93
- );
94
- }
95
-
96
- if (value !== undefined && !isPrimitiveValue(value)) {
97
- const actualType = Array.isArray(value) ? "array" : typeof value;
98
-
99
- throw new TypeError(
100
- `${ERROR_PREFIX} Parameter "${key}" must be a primitive value ` +
101
- `(string, number, or boolean), got ${actualType}. ` +
102
- `Objects and arrays are not supported in URL parameters.`,
103
- );
104
- }
105
- }
106
-
107
- /**
108
- * Validates the params configuration and throws a descriptive error if invalid.
109
- *
110
- * @param params - Configuration to validate
111
- * @throws {TypeError} If params is not a valid configuration
112
- */
113
- export function validateConfig(params: unknown): void {
114
- if (!isValidParamsConfig(params)) {
115
- let actualType: string;
116
-
117
- if (params === null) {
118
- actualType = "null";
119
- } else if (Array.isArray(params)) {
120
- actualType = "array with invalid items";
121
- } else {
122
- actualType = typeof params;
123
- }
124
-
125
- throw new TypeError(
126
- `${ERROR_PREFIX} Invalid params configuration. ` +
127
- `Expected array of non-empty strings or object with primitive values, got ${actualType}.`,
128
- );
129
- }
130
- }