zustand-querystring 0.0.1

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/lib/index.d.ts ADDED
@@ -0,0 +1 @@
1
+ export { queryString, QueryStringOptions } from './middleware.js';
package/lib/index.js ADDED
@@ -0,0 +1 @@
1
+ export { queryString } from './middleware.js';
@@ -0,0 +1,11 @@
1
+ import { StateCreator, StoreMutatorIdentifier } from 'zustand/vanilla';
2
+ type DeepSelect<T> = T extends object ? {
3
+ [P in keyof T]?: DeepSelect<T[P]> | boolean;
4
+ } : boolean;
5
+ export interface QueryStringOptions<T> {
6
+ url?: string;
7
+ select?: (pathname: string) => DeepSelect<T>;
8
+ }
9
+ type QueryString = <T, Mps extends [StoreMutatorIdentifier, unknown][] = [], Mcs extends [StoreMutatorIdentifier, unknown][] = []>(initializer: StateCreator<T, Mps, Mcs>, options?: QueryStringOptions<T>) => StateCreator<T, Mps, Mcs>;
10
+ export declare const queryString: QueryString;
11
+ export {};
@@ -0,0 +1,115 @@
1
+ import { parse, stringify } from './parser.js';
2
+ import { mergeWith, isEqual } from 'lodash-es';
3
+ const compact = (newState, initialState) => {
4
+ const output = {};
5
+ Object.keys(newState).forEach(key => {
6
+ if (newState[key] !== null &&
7
+ newState[key] !== undefined &&
8
+ typeof newState[key] !== 'function' &&
9
+ !isEqual(newState[key], initialState[key])) {
10
+ if (typeof newState[key] === 'object' && !Array.isArray(newState[key])) {
11
+ const value = compact(newState[key], initialState[key]);
12
+ if (value && Object.keys(value).length > 0) {
13
+ output[key] = value;
14
+ }
15
+ }
16
+ else {
17
+ output[key] = newState[key];
18
+ }
19
+ }
20
+ });
21
+ if (Object.keys(output).length === 0) {
22
+ return null;
23
+ }
24
+ return output;
25
+ };
26
+ const translateSelectionToState = (selection, state) => Object.keys(selection).reduce((acc, key) => {
27
+ const value = selection[key];
28
+ if (typeof value === 'boolean') {
29
+ if (value) {
30
+ acc[key] = state[key];
31
+ }
32
+ }
33
+ else {
34
+ acc[key] = translateSelectionToState(value, state[key]);
35
+ }
36
+ return acc;
37
+ }, {});
38
+ const queryStringImpl = (fn, options) => (set, get, api) => {
39
+ const defaultedOptions = {
40
+ partialize: state => state,
41
+ ...options,
42
+ };
43
+ const url = defaultedOptions.url;
44
+ const initialState = get() ?? fn(set, get, api);
45
+ const getSelectedState = () => {
46
+ if (defaultedOptions.select) {
47
+ const selection = defaultedOptions.select(window.location.pathname);
48
+ // translate the selection to state
49
+ const selectedState = translateSelectionToState(selection, get());
50
+ return selectedState;
51
+ }
52
+ return get();
53
+ };
54
+ const initialize = (url, _set = set) => {
55
+ try {
56
+ const queryString = url.split('?')[1]?.slice(2);
57
+ if (!queryString) {
58
+ return fn(_set, get, api);
59
+ }
60
+ const parsed = parse(queryString);
61
+ const currentValue = get() ?? fn(_set, get, api);
62
+ const merged = mergeWith(currentValue, parsed);
63
+ set(merged, true);
64
+ return merged;
65
+ }
66
+ catch (error) {
67
+ console.error(error);
68
+ if (typeof window !== 'undefined') {
69
+ window.history.replaceState(null, '', window.location.pathname);
70
+ }
71
+ return fn(_set, get, api);
72
+ }
73
+ };
74
+ if (typeof window !== 'undefined') {
75
+ const setQuery = () => {
76
+ const selectedState = getSelectedState();
77
+ if (!selectedState) {
78
+ return;
79
+ }
80
+ const compactedSelectedState = compact(selectedState, initialState);
81
+ if (!compactedSelectedState) {
82
+ window.history.replaceState(null, '', window.location.pathname);
83
+ return;
84
+ }
85
+ const stringified = stringify(compactedSelectedState);
86
+ if (stringified) {
87
+ // console.log('set query', stringified);
88
+ // console.log('parse query', parse(stringified));
89
+ window.history.replaceState(null, '', `?q=${stringified}`);
90
+ }
91
+ };
92
+ //TODO: find a better way to do this
93
+ let previousUrl = '';
94
+ setInterval(() => {
95
+ if (window.location.href !== previousUrl) {
96
+ previousUrl = window.location.href;
97
+ setQuery();
98
+ }
99
+ }, 50);
100
+ const originalSetState = api.setState;
101
+ api.setState = (...args) => {
102
+ originalSetState(...args);
103
+ setQuery();
104
+ };
105
+ return initialize(window.location.href, (...args) => {
106
+ set(...args);
107
+ setQuery();
108
+ });
109
+ }
110
+ if (url) {
111
+ return initialize(url);
112
+ }
113
+ return fn(set, get, api);
114
+ };
115
+ export const queryString = queryStringImpl;
@@ -0,0 +1,2 @@
1
+ export declare function stringify(input: unknown, recursive?: boolean): any;
2
+ export declare function parse(str: string): {} | null;
package/lib/parser.js ADDED
@@ -0,0 +1,134 @@
1
+ const keyStringifyRegexp = /([=:@$/])/g;
2
+ const valueStringifyRegexp = /([&;/])/g;
3
+ const keyParseRegexp = /[=:@$]/;
4
+ const valueParseRegexp = /[&;]/;
5
+ function encodeString(str, regexp) {
6
+ return encodeURI(str.replace(regexp, '/$1'));
7
+ }
8
+ function trim(res) {
9
+ return typeof res === 'string' ? res.replace(/;+$/g, '') : res;
10
+ }
11
+ export function stringify(input, recursive) {
12
+ if (!recursive) {
13
+ return trim(stringify(input, true));
14
+ }
15
+ // Function
16
+ if (typeof input === 'function') {
17
+ return;
18
+ }
19
+ // Number, Boolean or Null
20
+ if (typeof input === 'number' ||
21
+ input === true ||
22
+ input === false ||
23
+ input === null) {
24
+ return ':' + input;
25
+ }
26
+ const res = [];
27
+ // Array
28
+ if (Array.isArray(input)) {
29
+ for (const elem of input) {
30
+ typeof elem === 'undefined'
31
+ ? res.push(':null')
32
+ : res.push(stringify(elem, true));
33
+ }
34
+ return '@' + res.join('&') + ';';
35
+ }
36
+ // Object
37
+ if (typeof input === 'object') {
38
+ for (const [key, value] of Object.entries(input)) {
39
+ const stringifiedValue = stringify(value, true);
40
+ if (stringifiedValue) {
41
+ res.push(encodeString(key, keyStringifyRegexp) + stringifiedValue);
42
+ }
43
+ }
44
+ return '$' + res.join('&') + ';';
45
+ }
46
+ // undefined
47
+ if (typeof input === 'undefined') {
48
+ return;
49
+ }
50
+ // String
51
+ return '=' + encodeString(input.toString(), valueStringifyRegexp);
52
+ }
53
+ export function parse(str) {
54
+ let pos = 0;
55
+ str = decodeURI(str);
56
+ function readToken(regexp) {
57
+ let token = '';
58
+ for (; pos !== str.length; ++pos) {
59
+ if (str.charAt(pos) === '/') {
60
+ pos += 1;
61
+ if (pos === str.length) {
62
+ token += ';';
63
+ break;
64
+ }
65
+ }
66
+ else if (str.charAt(pos).match(regexp)) {
67
+ break;
68
+ }
69
+ token += str.charAt(pos);
70
+ }
71
+ return token;
72
+ }
73
+ function parseToken() {
74
+ const type = str.charAt(pos++);
75
+ // String
76
+ if (type === '=') {
77
+ return readToken(valueParseRegexp);
78
+ }
79
+ // Number, Boolean or Null
80
+ if (type === ':') {
81
+ const value = readToken(valueParseRegexp);
82
+ if (value === 'true') {
83
+ return true;
84
+ }
85
+ if (value === 'false') {
86
+ return false;
87
+ }
88
+ const parsedValue = parseFloat(value);
89
+ return isNaN(parsedValue) ? null : parsedValue;
90
+ }
91
+ // Array
92
+ if (type === '@') {
93
+ const res = [];
94
+ loop: {
95
+ // empty array
96
+ if (pos >= str.length || str.charAt(pos) === ';') {
97
+ break loop;
98
+ }
99
+ // parse array items
100
+ while (1) {
101
+ res.push(parseToken());
102
+ if (pos >= str.length || str.charAt(pos) === ';') {
103
+ break loop;
104
+ }
105
+ pos += 1;
106
+ }
107
+ }
108
+ pos += 1;
109
+ return res;
110
+ }
111
+ // Object
112
+ if (type === '$') {
113
+ const res = {};
114
+ loop: {
115
+ if (pos >= str.length || str.charAt(pos) === ';') {
116
+ break loop;
117
+ }
118
+ while (1) {
119
+ var name = readToken(keyParseRegexp);
120
+ res[name] = parseToken();
121
+ if (pos >= str.length || str.charAt(pos) === ';') {
122
+ break loop;
123
+ }
124
+ pos += 1;
125
+ }
126
+ }
127
+ pos += 1;
128
+ return res;
129
+ }
130
+ // Error
131
+ throw new Error('Unexpected char ' + type);
132
+ }
133
+ return parseToken();
134
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "zustand-querystring",
3
+ "version": "0.0.1",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "keywords": [
7
+ "zustand",
8
+ "querystring"
9
+ ],
10
+ "repository": {
11
+ "type": "github",
12
+ "url": "https://github.com/nitedani/zustand-querystring"
13
+ },
14
+ "exports": {
15
+ ".": "./lib/index.js"
16
+ },
17
+ "typesVersions": {
18
+ "*": {
19
+ "*": [
20
+ "lib/index.d.ts"
21
+ ]
22
+ }
23
+ },
24
+ "files": [
25
+ "lib"
26
+ ],
27
+ "dependencies": {
28
+ "lodash-es": "^4.17.21"
29
+ },
30
+ "prettier": {
31
+ "singleQuote": true,
32
+ "arrowParens": "avoid"
33
+ },
34
+ "devDependencies": {
35
+ "@types/lodash-es": "^4.17.6",
36
+ "rimraf": "^3.0.2",
37
+ "typescript": "^4.9.3",
38
+ "zustand": "^4.1.4"
39
+ },
40
+ "scripts": {
41
+ "build": "rimraf lib && tsc -b"
42
+ }
43
+ }