zustand-querystring 0.3.2 → 0.4.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/README.md CHANGED
@@ -59,9 +59,9 @@ export const useStore = create<Store>()(
59
59
  querystring options:
60
60
 
61
61
  - <b>select</b> - the select option controls what part of the state is synced with the query string
62
- - <b>key: string</b> - the key option controls how the state is stored in the querystring (default: 'state')
62
+ - <b>key: string | false</b> - the key option controls how the state is stored in the querystring (default: 'state'). Set to `false` for standalone mode where each state key becomes a separate query parameter.
63
63
  - <b>url</b> - the url option is used to provide the request url on the server side render
64
- - <b>format</b> - custom format for stringify/parse (default: JSON-based format)
64
+ - <b>format</b> - custom format for stringify/parse (default: JSON-based format). Use `readable` from `zustand-querystring/format/readable` for human-readable URLs.
65
65
  - <b>syncNull: boolean</b> - when true, null values that differ from initial state are synced to URL (default: false)
66
66
  - <b>syncUndefined: boolean</b> - when true, undefined values that differ from initial state are synced to URL (default: false)
67
67
 
@@ -4,7 +4,11 @@
4
4
  * Inspired by URLON (https://github.com/cerebral/urlon)
5
5
  * Copyright (c) 2021 Cerebral - MIT License
6
6
  */
7
- declare function stringify(value: unknown): string;
8
- declare function parse<T = unknown>(input: string): T;
7
+ declare function stringify(value: unknown, standalone?: boolean): string;
8
+ declare function parse<T = unknown>(input: string, standalone?: boolean): T;
9
+ declare const readable: {
10
+ stringify: typeof stringify;
11
+ parse: typeof parse;
12
+ };
9
13
 
10
- export { parse, stringify };
14
+ export { parse, readable, stringify };
@@ -4,7 +4,11 @@
4
4
  * Inspired by URLON (https://github.com/cerebral/urlon)
5
5
  * Copyright (c) 2021 Cerebral - MIT License
6
6
  */
7
- declare function stringify(value: unknown): string;
8
- declare function parse<T = unknown>(input: string): T;
7
+ declare function stringify(value: unknown, standalone?: boolean): string;
8
+ declare function parse<T = unknown>(input: string, standalone?: boolean): T;
9
+ declare const readable: {
10
+ stringify: typeof stringify;
11
+ parse: typeof parse;
12
+ };
9
13
 
10
- export { parse, stringify };
14
+ export { parse, readable, stringify };
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  var readable_exports = {};
21
21
  __export(readable_exports, {
22
22
  parse: () => parse,
23
+ readable: () => readable,
23
24
  stringify: () => stringify
24
25
  });
25
26
  module.exports = __toCommonJS(readable_exports);
@@ -30,27 +31,38 @@ var TYPE_PRIMITIVE = ":";
30
31
  var SEPARATOR = ",";
31
32
  var TERMINATOR = "~";
32
33
  var ESCAPE = "/";
33
- var DATE_PREFIX = "!Date:";
34
- var SEP_CHAR = SEPARATOR[0];
34
+ var DATE_PREFIX = "D";
35
35
  var esc = (c) => /[.$^*+?()[\]{}|\\]/.test(c) ? `\\${c}` : c;
36
- var KEY_STOP = new RegExp(`[${TYPE_STRING}${TYPE_PRIMITIVE}${TYPE_ARRAY}${esc(TYPE_OBJECT)}${esc(SEP_CHAR)}]`);
37
- var VALUE_STOP = new RegExp(`[${esc(SEP_CHAR)}${TERMINATOR}]`);
38
- var KEY_ESCAPE = new RegExp(`([${TYPE_STRING}${TYPE_PRIMITIVE}${TYPE_ARRAY}${esc(TYPE_OBJECT)}${ESCAPE}${esc(SEP_CHAR)}])`, "g");
39
- var VALUE_ESCAPE = new RegExp(`([${esc(SEP_CHAR)}${TERMINATOR}${ESCAPE}])`, "g");
36
+ var KEY_STOP = new RegExp(
37
+ `[${TYPE_STRING}${TYPE_PRIMITIVE}${TYPE_ARRAY}${esc(TYPE_OBJECT)}${esc(SEPARATOR)}]`
38
+ );
39
+ var VALUE_STOP = new RegExp(`[${esc(SEPARATOR)}${TERMINATOR}]`);
40
+ var KEY_ESCAPE = new RegExp(
41
+ `([${TYPE_STRING}${TYPE_PRIMITIVE}${TYPE_ARRAY}${esc(TYPE_OBJECT)}${ESCAPE}${esc(SEPARATOR)}])`,
42
+ "g"
43
+ );
44
+ var VALUE_ESCAPE = new RegExp(
45
+ `([${esc(SEPARATOR)}${TERMINATOR}${ESCAPE}])`,
46
+ "g"
47
+ );
40
48
  function escapeStr(str, pattern) {
41
49
  return encodeURI(str.replace(pattern, `${ESCAPE}$1`));
42
50
  }
43
- function cleanResult(str) {
51
+ function cleanResult(str, standalone) {
44
52
  while (str.endsWith(TERMINATOR)) str = str.slice(0, -1);
45
- if (str.startsWith(TYPE_OBJECT) || str.startsWith(TYPE_ARRAY)) {
46
- str = str.slice(1);
53
+ const datePattern = new RegExp(`^${esc(TYPE_STRING)}${DATE_PREFIX}-?\\d+$`);
54
+ if (standalone && datePattern.test(str)) {
55
+ return str.slice(1);
56
+ }
57
+ if (!standalone && str.startsWith(TYPE_OBJECT)) {
58
+ return str.slice(1);
47
59
  }
48
60
  return str;
49
61
  }
50
- function stringify(value) {
51
- return cleanResult(serialize(value));
62
+ function stringify(value, standalone = false) {
63
+ return cleanResult(serialize(value, standalone), standalone);
52
64
  }
53
- function serialize(value) {
65
+ function serialize(value, standalone, inArray = false) {
54
66
  if (value === null) return `${TYPE_PRIMITIVE}null`;
55
67
  if (value === void 0) return `${TYPE_PRIMITIVE}undefined`;
56
68
  if (typeof value === "function") return "";
@@ -61,107 +73,166 @@ function serialize(value) {
61
73
  return `${TYPE_PRIMITIVE}${value}`;
62
74
  }
63
75
  if (value instanceof Date) {
64
- return `${TYPE_STRING}${DATE_PREFIX}${escapeStr(value.toISOString(), VALUE_ESCAPE)}`;
76
+ return `${TYPE_STRING}${DATE_PREFIX}${value.getTime()}`;
65
77
  }
66
78
  if (Array.isArray(value)) {
67
- const items = value.map((v) => serialize(v));
79
+ const items = value.map((v) => serialize(v, standalone, true));
68
80
  return `${TYPE_ARRAY}${items.join(SEPARATOR)}${TERMINATOR}`;
69
81
  }
70
82
  if (typeof value === "object") {
71
83
  const entries = [];
72
84
  for (const [k, v] of Object.entries(value)) {
73
- const val = serialize(v);
85
+ const val = serialize(v, false, false);
74
86
  if (val || v === void 0) {
75
87
  entries.push(`${escapeStr(k, KEY_ESCAPE)}${val}`);
76
88
  }
77
89
  }
78
90
  return `${TYPE_OBJECT}${entries.join(SEPARATOR)}${TERMINATOR}`;
79
91
  }
80
- return `${TYPE_STRING}${escapeStr(String(value), VALUE_ESCAPE)}`;
92
+ const strVal = String(value);
93
+ let escaped = escapeStr(strVal, VALUE_ESCAPE);
94
+ const datePattern = new RegExp(`^${DATE_PREFIX}-?\\d`);
95
+ if (datePattern.test(strVal)) {
96
+ escaped = ESCAPE + escaped;
97
+ }
98
+ return standalone || inArray ? escaped : `${TYPE_STRING}${escaped}`;
81
99
  }
82
- function parse(input) {
100
+ function parse(input, standalone = false) {
83
101
  const str = decodeURI(input);
102
+ if (str.length === 0) {
103
+ return standalone ? "" : {};
104
+ }
84
105
  const first = str[0];
85
106
  const hasMarker = first === TYPE_STRING || first === TYPE_PRIMITIVE || first === TYPE_ARRAY || first === TYPE_OBJECT;
107
+ let source;
108
+ if (hasMarker) {
109
+ source = str;
110
+ } else if (standalone) {
111
+ const datePattern = new RegExp(`^${DATE_PREFIX}-?\\d+$`);
112
+ if (datePattern.test(str)) {
113
+ const timestamp = parseInt(str.slice(DATE_PREFIX.length), 10);
114
+ if (!isNaN(timestamp)) {
115
+ return new Date(timestamp);
116
+ }
117
+ }
118
+ return str;
119
+ } else {
120
+ const markers = `${TYPE_STRING}${TYPE_PRIMITIVE}${TYPE_ARRAY}${TYPE_OBJECT}`;
121
+ const objectPattern = new RegExp(
122
+ `[^${ESCAPE}${markers}${SEPARATOR}${TERMINATOR}][${markers}]`
123
+ );
124
+ source = objectPattern.test(str) ? `${TYPE_OBJECT}${str}` : `${TYPE_STRING}${str}`;
125
+ }
86
126
  let pos = 0;
87
- const source = hasMarker ? str : `${TYPE_OBJECT}${str}`;
88
- function readUntil(pattern) {
127
+ function peek() {
128
+ return source[pos] || "";
129
+ }
130
+ function advance() {
131
+ pos++;
132
+ }
133
+ function readUntil(stopPattern, checkEscape = false) {
89
134
  let result = "";
135
+ let wasEscaped = false;
90
136
  while (pos < source.length) {
91
- const char = source[pos];
92
- if (char === ESCAPE) {
93
- pos++;
94
- result += pos < source.length ? source[pos++] : TERMINATOR;
137
+ const ch = peek();
138
+ if (ch === ESCAPE) {
139
+ if (checkEscape && pos + 1 < source.length && source[pos + 1] === DATE_PREFIX) {
140
+ wasEscaped = true;
141
+ }
142
+ advance();
143
+ if (pos < source.length) {
144
+ result += peek();
145
+ advance();
146
+ }
95
147
  continue;
96
148
  }
97
- if (pattern.test(char)) break;
98
- result += char;
99
- pos++;
100
- }
101
- return result;
102
- }
103
- function skipSeparator() {
104
- if (source[pos] === SEPARATOR) {
105
- pos++;
149
+ if (stopPattern.test(ch)) break;
150
+ result += ch;
151
+ advance();
106
152
  }
153
+ return { value: result, wasEscaped };
107
154
  }
108
155
  function parseString() {
109
- const val = readUntil(VALUE_STOP);
110
- if (val.startsWith(DATE_PREFIX)) {
111
- return new Date(val.slice(DATE_PREFIX.length));
156
+ const { value, wasEscaped } = readUntil(VALUE_STOP, true);
157
+ const datePattern = new RegExp(`^${DATE_PREFIX}-?\\d+$`);
158
+ if (!wasEscaped && datePattern.test(value)) {
159
+ const timestamp = parseInt(value.slice(DATE_PREFIX.length), 10);
160
+ if (!isNaN(timestamp)) {
161
+ return new Date(timestamp);
162
+ }
112
163
  }
113
- return val;
164
+ return value;
114
165
  }
115
166
  function parsePrimitive() {
116
- const val = readUntil(VALUE_STOP);
117
- if (val === "null") return null;
118
- if (val === "undefined") return void 0;
119
- if (val === "true") return true;
120
- if (val === "false") return false;
121
- return parseFloat(val);
167
+ const { value } = readUntil(VALUE_STOP, false);
168
+ if (value === "null") return null;
169
+ if (value === "undefined") return void 0;
170
+ if (value === "true") return true;
171
+ if (value === "false") return false;
172
+ return parseFloat(value);
122
173
  }
123
174
  function parseArray() {
124
175
  const result = [];
125
- while (pos < source.length && source[pos] !== TERMINATOR) {
126
- result.push(parseValue());
127
- if (pos < source.length && source[pos] !== TERMINATOR) {
128
- skipSeparator();
176
+ let lastWasSeparator = false;
177
+ while (pos < source.length && peek() !== TERMINATOR) {
178
+ if (peek() === SEPARATOR) {
179
+ result.push("");
180
+ advance();
181
+ lastWasSeparator = true;
182
+ continue;
183
+ }
184
+ lastWasSeparator = false;
185
+ const ch = peek();
186
+ if (ch === TYPE_PRIMITIVE || ch === TYPE_ARRAY || ch === TYPE_OBJECT || ch === TYPE_STRING) {
187
+ result.push(parseValue());
188
+ } else {
189
+ result.push(parseString());
190
+ }
191
+ if (pos < source.length && peek() === SEPARATOR) {
192
+ advance();
193
+ lastWasSeparator = true;
129
194
  }
130
195
  }
131
- if (source[pos] === TERMINATOR) pos++;
196
+ if (lastWasSeparator && (peek() === TERMINATOR || pos >= source.length)) {
197
+ result.push("");
198
+ }
199
+ if (peek() === TERMINATOR) advance();
132
200
  return result;
133
201
  }
134
202
  function parseObject() {
135
203
  const result = {};
136
- while (pos < source.length && source[pos] !== TERMINATOR) {
137
- const key = readUntil(KEY_STOP);
204
+ while (pos < source.length && peek() !== TERMINATOR) {
205
+ const { value: key } = readUntil(KEY_STOP, false);
138
206
  result[key] = parseValue();
139
- if (pos < source.length && source[pos] !== TERMINATOR) {
140
- skipSeparator();
207
+ if (pos < source.length && peek() !== TERMINATOR && peek() === SEPARATOR) {
208
+ advance();
141
209
  }
142
210
  }
143
- if (source[pos] === TERMINATOR) pos++;
211
+ if (peek() === TERMINATOR) advance();
144
212
  return result;
145
213
  }
146
214
  function parseValue() {
147
- const type = source[pos++];
215
+ const type = peek();
216
+ advance();
148
217
  switch (type) {
149
- case TYPE_STRING:
150
- return parseString();
151
218
  case TYPE_PRIMITIVE:
152
219
  return parsePrimitive();
153
220
  case TYPE_ARRAY:
154
221
  return parseArray();
155
222
  case TYPE_OBJECT:
156
223
  return parseObject();
224
+ case TYPE_STRING:
225
+ return parseString();
157
226
  default:
158
- throw new Error(`Unexpected type "${type}" at position ${pos - 1}`);
227
+ throw new Error(`Unexpected type "${type}" at position ${pos}`);
159
228
  }
160
229
  }
161
230
  return parseValue();
162
231
  }
232
+ var readable = { stringify, parse };
163
233
  // Annotate the CommonJS export names for ESM import in node:
164
234
  0 && (module.exports = {
165
235
  parse,
236
+ readable,
166
237
  stringify
167
238
  });
@@ -6,27 +6,38 @@ var TYPE_PRIMITIVE = ":";
6
6
  var SEPARATOR = ",";
7
7
  var TERMINATOR = "~";
8
8
  var ESCAPE = "/";
9
- var DATE_PREFIX = "!Date:";
10
- var SEP_CHAR = SEPARATOR[0];
9
+ var DATE_PREFIX = "D";
11
10
  var esc = (c) => /[.$^*+?()[\]{}|\\]/.test(c) ? `\\${c}` : c;
12
- var KEY_STOP = new RegExp(`[${TYPE_STRING}${TYPE_PRIMITIVE}${TYPE_ARRAY}${esc(TYPE_OBJECT)}${esc(SEP_CHAR)}]`);
13
- var VALUE_STOP = new RegExp(`[${esc(SEP_CHAR)}${TERMINATOR}]`);
14
- var KEY_ESCAPE = new RegExp(`([${TYPE_STRING}${TYPE_PRIMITIVE}${TYPE_ARRAY}${esc(TYPE_OBJECT)}${ESCAPE}${esc(SEP_CHAR)}])`, "g");
15
- var VALUE_ESCAPE = new RegExp(`([${esc(SEP_CHAR)}${TERMINATOR}${ESCAPE}])`, "g");
11
+ var KEY_STOP = new RegExp(
12
+ `[${TYPE_STRING}${TYPE_PRIMITIVE}${TYPE_ARRAY}${esc(TYPE_OBJECT)}${esc(SEPARATOR)}]`
13
+ );
14
+ var VALUE_STOP = new RegExp(`[${esc(SEPARATOR)}${TERMINATOR}]`);
15
+ var KEY_ESCAPE = new RegExp(
16
+ `([${TYPE_STRING}${TYPE_PRIMITIVE}${TYPE_ARRAY}${esc(TYPE_OBJECT)}${ESCAPE}${esc(SEPARATOR)}])`,
17
+ "g"
18
+ );
19
+ var VALUE_ESCAPE = new RegExp(
20
+ `([${esc(SEPARATOR)}${TERMINATOR}${ESCAPE}])`,
21
+ "g"
22
+ );
16
23
  function escapeStr(str, pattern) {
17
24
  return encodeURI(str.replace(pattern, `${ESCAPE}$1`));
18
25
  }
19
- function cleanResult(str) {
26
+ function cleanResult(str, standalone) {
20
27
  while (str.endsWith(TERMINATOR)) str = str.slice(0, -1);
21
- if (str.startsWith(TYPE_OBJECT) || str.startsWith(TYPE_ARRAY)) {
22
- str = str.slice(1);
28
+ const datePattern = new RegExp(`^${esc(TYPE_STRING)}${DATE_PREFIX}-?\\d+$`);
29
+ if (standalone && datePattern.test(str)) {
30
+ return str.slice(1);
31
+ }
32
+ if (!standalone && str.startsWith(TYPE_OBJECT)) {
33
+ return str.slice(1);
23
34
  }
24
35
  return str;
25
36
  }
26
- function stringify(value) {
27
- return cleanResult(serialize(value));
37
+ function stringify(value, standalone = false) {
38
+ return cleanResult(serialize(value, standalone), standalone);
28
39
  }
29
- function serialize(value) {
40
+ function serialize(value, standalone, inArray = false) {
30
41
  if (value === null) return `${TYPE_PRIMITIVE}null`;
31
42
  if (value === void 0) return `${TYPE_PRIMITIVE}undefined`;
32
43
  if (typeof value === "function") return "";
@@ -37,106 +48,165 @@ function serialize(value) {
37
48
  return `${TYPE_PRIMITIVE}${value}`;
38
49
  }
39
50
  if (value instanceof Date) {
40
- return `${TYPE_STRING}${DATE_PREFIX}${escapeStr(value.toISOString(), VALUE_ESCAPE)}`;
51
+ return `${TYPE_STRING}${DATE_PREFIX}${value.getTime()}`;
41
52
  }
42
53
  if (Array.isArray(value)) {
43
- const items = value.map((v) => serialize(v));
54
+ const items = value.map((v) => serialize(v, standalone, true));
44
55
  return `${TYPE_ARRAY}${items.join(SEPARATOR)}${TERMINATOR}`;
45
56
  }
46
57
  if (typeof value === "object") {
47
58
  const entries = [];
48
59
  for (const [k, v] of Object.entries(value)) {
49
- const val = serialize(v);
60
+ const val = serialize(v, false, false);
50
61
  if (val || v === void 0) {
51
62
  entries.push(`${escapeStr(k, KEY_ESCAPE)}${val}`);
52
63
  }
53
64
  }
54
65
  return `${TYPE_OBJECT}${entries.join(SEPARATOR)}${TERMINATOR}`;
55
66
  }
56
- return `${TYPE_STRING}${escapeStr(String(value), VALUE_ESCAPE)}`;
67
+ const strVal = String(value);
68
+ let escaped = escapeStr(strVal, VALUE_ESCAPE);
69
+ const datePattern = new RegExp(`^${DATE_PREFIX}-?\\d`);
70
+ if (datePattern.test(strVal)) {
71
+ escaped = ESCAPE + escaped;
72
+ }
73
+ return standalone || inArray ? escaped : `${TYPE_STRING}${escaped}`;
57
74
  }
58
- function parse(input) {
75
+ function parse(input, standalone = false) {
59
76
  const str = decodeURI(input);
77
+ if (str.length === 0) {
78
+ return standalone ? "" : {};
79
+ }
60
80
  const first = str[0];
61
81
  const hasMarker = first === TYPE_STRING || first === TYPE_PRIMITIVE || first === TYPE_ARRAY || first === TYPE_OBJECT;
82
+ let source;
83
+ if (hasMarker) {
84
+ source = str;
85
+ } else if (standalone) {
86
+ const datePattern = new RegExp(`^${DATE_PREFIX}-?\\d+$`);
87
+ if (datePattern.test(str)) {
88
+ const timestamp = parseInt(str.slice(DATE_PREFIX.length), 10);
89
+ if (!isNaN(timestamp)) {
90
+ return new Date(timestamp);
91
+ }
92
+ }
93
+ return str;
94
+ } else {
95
+ const markers = `${TYPE_STRING}${TYPE_PRIMITIVE}${TYPE_ARRAY}${TYPE_OBJECT}`;
96
+ const objectPattern = new RegExp(
97
+ `[^${ESCAPE}${markers}${SEPARATOR}${TERMINATOR}][${markers}]`
98
+ );
99
+ source = objectPattern.test(str) ? `${TYPE_OBJECT}${str}` : `${TYPE_STRING}${str}`;
100
+ }
62
101
  let pos = 0;
63
- const source = hasMarker ? str : `${TYPE_OBJECT}${str}`;
64
- function readUntil(pattern) {
102
+ function peek() {
103
+ return source[pos] || "";
104
+ }
105
+ function advance() {
106
+ pos++;
107
+ }
108
+ function readUntil(stopPattern, checkEscape = false) {
65
109
  let result = "";
110
+ let wasEscaped = false;
66
111
  while (pos < source.length) {
67
- const char = source[pos];
68
- if (char === ESCAPE) {
69
- pos++;
70
- result += pos < source.length ? source[pos++] : TERMINATOR;
112
+ const ch = peek();
113
+ if (ch === ESCAPE) {
114
+ if (checkEscape && pos + 1 < source.length && source[pos + 1] === DATE_PREFIX) {
115
+ wasEscaped = true;
116
+ }
117
+ advance();
118
+ if (pos < source.length) {
119
+ result += peek();
120
+ advance();
121
+ }
71
122
  continue;
72
123
  }
73
- if (pattern.test(char)) break;
74
- result += char;
75
- pos++;
76
- }
77
- return result;
78
- }
79
- function skipSeparator() {
80
- if (source[pos] === SEPARATOR) {
81
- pos++;
124
+ if (stopPattern.test(ch)) break;
125
+ result += ch;
126
+ advance();
82
127
  }
128
+ return { value: result, wasEscaped };
83
129
  }
84
130
  function parseString() {
85
- const val = readUntil(VALUE_STOP);
86
- if (val.startsWith(DATE_PREFIX)) {
87
- return new Date(val.slice(DATE_PREFIX.length));
131
+ const { value, wasEscaped } = readUntil(VALUE_STOP, true);
132
+ const datePattern = new RegExp(`^${DATE_PREFIX}-?\\d+$`);
133
+ if (!wasEscaped && datePattern.test(value)) {
134
+ const timestamp = parseInt(value.slice(DATE_PREFIX.length), 10);
135
+ if (!isNaN(timestamp)) {
136
+ return new Date(timestamp);
137
+ }
88
138
  }
89
- return val;
139
+ return value;
90
140
  }
91
141
  function parsePrimitive() {
92
- const val = readUntil(VALUE_STOP);
93
- if (val === "null") return null;
94
- if (val === "undefined") return void 0;
95
- if (val === "true") return true;
96
- if (val === "false") return false;
97
- return parseFloat(val);
142
+ const { value } = readUntil(VALUE_STOP, false);
143
+ if (value === "null") return null;
144
+ if (value === "undefined") return void 0;
145
+ if (value === "true") return true;
146
+ if (value === "false") return false;
147
+ return parseFloat(value);
98
148
  }
99
149
  function parseArray() {
100
150
  const result = [];
101
- while (pos < source.length && source[pos] !== TERMINATOR) {
102
- result.push(parseValue());
103
- if (pos < source.length && source[pos] !== TERMINATOR) {
104
- skipSeparator();
151
+ let lastWasSeparator = false;
152
+ while (pos < source.length && peek() !== TERMINATOR) {
153
+ if (peek() === SEPARATOR) {
154
+ result.push("");
155
+ advance();
156
+ lastWasSeparator = true;
157
+ continue;
158
+ }
159
+ lastWasSeparator = false;
160
+ const ch = peek();
161
+ if (ch === TYPE_PRIMITIVE || ch === TYPE_ARRAY || ch === TYPE_OBJECT || ch === TYPE_STRING) {
162
+ result.push(parseValue());
163
+ } else {
164
+ result.push(parseString());
165
+ }
166
+ if (pos < source.length && peek() === SEPARATOR) {
167
+ advance();
168
+ lastWasSeparator = true;
105
169
  }
106
170
  }
107
- if (source[pos] === TERMINATOR) pos++;
171
+ if (lastWasSeparator && (peek() === TERMINATOR || pos >= source.length)) {
172
+ result.push("");
173
+ }
174
+ if (peek() === TERMINATOR) advance();
108
175
  return result;
109
176
  }
110
177
  function parseObject() {
111
178
  const result = {};
112
- while (pos < source.length && source[pos] !== TERMINATOR) {
113
- const key = readUntil(KEY_STOP);
179
+ while (pos < source.length && peek() !== TERMINATOR) {
180
+ const { value: key } = readUntil(KEY_STOP, false);
114
181
  result[key] = parseValue();
115
- if (pos < source.length && source[pos] !== TERMINATOR) {
116
- skipSeparator();
182
+ if (pos < source.length && peek() !== TERMINATOR && peek() === SEPARATOR) {
183
+ advance();
117
184
  }
118
185
  }
119
- if (source[pos] === TERMINATOR) pos++;
186
+ if (peek() === TERMINATOR) advance();
120
187
  return result;
121
188
  }
122
189
  function parseValue() {
123
- const type = source[pos++];
190
+ const type = peek();
191
+ advance();
124
192
  switch (type) {
125
- case TYPE_STRING:
126
- return parseString();
127
193
  case TYPE_PRIMITIVE:
128
194
  return parsePrimitive();
129
195
  case TYPE_ARRAY:
130
196
  return parseArray();
131
197
  case TYPE_OBJECT:
132
198
  return parseObject();
199
+ case TYPE_STRING:
200
+ return parseString();
133
201
  default:
134
- throw new Error(`Unexpected type "${type}" at position ${pos - 1}`);
202
+ throw new Error(`Unexpected type "${type}" at position ${pos}`);
135
203
  }
136
204
  }
137
205
  return parseValue();
138
206
  }
207
+ var readable = { stringify, parse };
139
208
  export {
140
209
  parse,
210
+ readable,
141
211
  stringify
142
212
  };
package/dist/index.d.mts CHANGED
@@ -3,17 +3,15 @@ import { StoreMutatorIdentifier, StateCreator } from 'zustand/vanilla';
3
3
  type DeepSelect<T> = T extends object ? {
4
4
  [P in keyof T]?: DeepSelect<T[P]> | boolean;
5
5
  } : boolean;
6
- type DeepPartial<T> = T extends object ? {
7
- [P in keyof T]?: DeepPartial<T[P]>;
8
- } : T;
6
+ interface QueryStringFormat {
7
+ stringify: (value: any, standalone?: boolean) => string;
8
+ parse: (value: string, standalone?: boolean) => any;
9
+ }
9
10
  interface QueryStringOptions<T> {
10
11
  url?: string;
11
12
  select?: (pathname: string) => DeepSelect<T>;
12
- key?: string;
13
- format?: {
14
- stringify: (value: DeepPartial<T>) => string;
15
- parse: (value: string) => DeepPartial<T>;
16
- };
13
+ key?: string | false;
14
+ format?: QueryStringFormat;
17
15
  syncNull?: boolean;
18
16
  syncUndefined?: boolean;
19
17
  }
package/dist/index.d.ts CHANGED
@@ -3,17 +3,15 @@ import { StoreMutatorIdentifier, StateCreator } from 'zustand/vanilla';
3
3
  type DeepSelect<T> = T extends object ? {
4
4
  [P in keyof T]?: DeepSelect<T[P]> | boolean;
5
5
  } : boolean;
6
- type DeepPartial<T> = T extends object ? {
7
- [P in keyof T]?: DeepPartial<T[P]>;
8
- } : T;
6
+ interface QueryStringFormat {
7
+ stringify: (value: any, standalone?: boolean) => string;
8
+ parse: (value: string, standalone?: boolean) => any;
9
+ }
9
10
  interface QueryStringOptions<T> {
10
11
  url?: string;
11
12
  select?: (pathname: string) => DeepSelect<T>;
12
- key?: string;
13
- format?: {
14
- stringify: (value: DeepPartial<T>) => string;
15
- parse: (value: string) => DeepPartial<T>;
16
- };
13
+ key?: string | false;
14
+ format?: QueryStringFormat;
17
15
  syncNull?: boolean;
18
16
  syncUndefined?: boolean;
19
17
  }
package/dist/index.js CHANGED
@@ -40,22 +40,25 @@ function parse(str) {
40
40
  // src/middleware.ts
41
41
  var compact = (newState, initialState, syncNull = false, syncUndefined = false) => {
42
42
  const output = {};
43
+ const removed = [];
43
44
  Object.keys(newState).forEach((key) => {
44
45
  const newValue = newState[key];
45
46
  const initialValue = initialState[key];
46
47
  if (typeof newValue !== "function" && !(0, import_lodash_es.isEqual)(newValue, initialValue) && (syncNull || newValue !== null) && (syncUndefined || newValue !== void 0)) {
47
48
  const isPlainObject = typeof newValue === "object" && newValue !== null && newValue !== void 0 && !Array.isArray(newValue) && newValue.constructor === Object;
48
49
  if (isPlainObject && initialValue && typeof initialValue === "object") {
49
- const value = compact(newValue, initialValue, syncNull, syncUndefined);
50
- if (value && Object.keys(value).length > 0) {
51
- output[key] = value;
50
+ const result = compact(newValue, initialValue, syncNull, syncUndefined);
51
+ if (result.output && Object.keys(result.output).length > 0) {
52
+ output[key] = result.output;
52
53
  }
53
54
  } else {
54
55
  output[key] = newValue;
55
56
  }
57
+ } else {
58
+ removed.push(key);
56
59
  }
57
60
  });
58
- return output;
61
+ return { output, removed };
59
62
  };
60
63
  var translateSelectionToState = (selection, state) => {
61
64
  if (typeof state !== "object" || !state) {
@@ -87,7 +90,32 @@ var queryStringImpl = (fn, options) => (set, get, api) => {
87
90
  syncUndefined: false,
88
91
  ...options
89
92
  };
90
- const getStateFromUrl = (url) => {
93
+ const standalone = !defaultedOptions.key;
94
+ if (typeof window !== "undefined" && standalone) {
95
+ if (!window.__ZUSTAND_QUERYSTRING_KEYS__) {
96
+ window.__ZUSTAND_QUERYSTRING_KEYS__ = /* @__PURE__ */ new Map();
97
+ }
98
+ }
99
+ const getStateFromUrl = (url, initialState) => {
100
+ if (standalone) {
101
+ const params2 = url.search.slice(1).split("&").filter(Boolean);
102
+ const state = {};
103
+ const initialKeys = new Set(Object.keys(initialState));
104
+ params2.forEach((param) => {
105
+ const eqIndex = param.indexOf("=");
106
+ if (eqIndex === -1) return;
107
+ const key = decodeURI(param.slice(0, eqIndex));
108
+ const value = param.slice(eqIndex + 1);
109
+ if (!initialKeys.has(key)) return;
110
+ try {
111
+ const parsed = defaultedOptions.format.parse(value, true);
112
+ state[key] = parsed;
113
+ } catch (error) {
114
+ console.error("[getStateFromUrl] error parsing key:", key, error);
115
+ }
116
+ });
117
+ return Object.keys(state).length > 0 ? state : null;
118
+ }
91
119
  const params = url.search.slice(1).split("&");
92
120
  for (const param of params) {
93
121
  const eqIndex = param.indexOf("=");
@@ -95,7 +123,8 @@ var queryStringImpl = (fn, options) => (set, get, api) => {
95
123
  const key = param.slice(0, eqIndex);
96
124
  if (key === defaultedOptions.key) {
97
125
  const value = param.slice(eqIndex + 1);
98
- return value ? defaultedOptions.format.parse(value) : null;
126
+ const parsed = value ? defaultedOptions.format.parse(value, false) : null;
127
+ return parsed;
99
128
  }
100
129
  }
101
130
  return null;
@@ -110,18 +139,25 @@ var queryStringImpl = (fn, options) => (set, get, api) => {
110
139
  };
111
140
  const initialize = (url, initialState) => {
112
141
  try {
113
- const stateFromURl = getStateFromUrl(url);
142
+ const stateFromURl = getStateFromUrl(url, initialState);
114
143
  if (!stateFromURl) {
115
144
  return initialState;
116
145
  }
146
+ const selected = getSelectedState(stateFromURl, url.pathname);
117
147
  const merged = (0, import_lodash_es.mergeWith)(
118
148
  {},
119
149
  initialState,
120
- getSelectedState(stateFromURl, url.pathname)
150
+ selected,
151
+ (_objValue, srcValue) => {
152
+ if (Array.isArray(srcValue)) {
153
+ return srcValue;
154
+ }
155
+ return void 0;
156
+ }
121
157
  );
122
158
  return merged;
123
159
  } catch (error) {
124
- console.error(error);
160
+ console.error("[initialize] error:", error);
125
161
  return initialState;
126
162
  }
127
163
  };
@@ -134,10 +170,33 @@ var queryStringImpl = (fn, options) => (set, get, api) => {
134
170
  get,
135
171
  api
136
172
  );
173
+ if (standalone) {
174
+ const registry = window.__ZUSTAND_QUERYSTRING_KEYS__;
175
+ const stateKeys = Object.keys(initialState).filter(
176
+ (k) => typeof initialState[k] !== "function"
177
+ );
178
+ const conflicts = [];
179
+ for (const key of stateKeys) {
180
+ if (registry.has(key)) {
181
+ const existing = registry.get(key);
182
+ const current = defaultedOptions.format;
183
+ if (existing !== current) {
184
+ conflicts.push(key);
185
+ }
186
+ } else {
187
+ registry.set(key, defaultedOptions.format);
188
+ }
189
+ }
190
+ if (conflicts.length > 0) {
191
+ throw new Error(
192
+ `[zustand-querystring] Standalone mode conflict: Multiple stores are using the following keys with different formats: ${conflicts.map((k) => `"${k}"`).join(", ")}. This will cause parsing errors. Please use unique state keys or the same format for all stores sharing keys.`
193
+ );
194
+ }
195
+ }
137
196
  const setQuery = () => {
138
197
  const url = new URL(window.location.href);
139
198
  const selectedState = getSelectedState(get(), url.pathname);
140
- const newCompacted = compact(
199
+ const { output: newCompacted } = compact(
141
200
  selectedState,
142
201
  initialState,
143
202
  defaultedOptions.syncNull,
@@ -145,21 +204,31 @@ var queryStringImpl = (fn, options) => (set, get, api) => {
145
204
  );
146
205
  const previous = url.search;
147
206
  const params = url.search.slice(1).split("&").filter(Boolean);
148
- let stateIndex = -1;
149
- const otherParams = params.filter((p, i) => {
150
- const [key] = p.split("=", 1);
151
- if (key === defaultedOptions.key) {
152
- stateIndex = i;
153
- return false;
207
+ const managedKeys = standalone ? new Set(Object.keys(selectedState).map(encodeURI)) : /* @__PURE__ */ new Set([defaultedOptions.key]);
208
+ const valuesToWrite = standalone ? new Map(
209
+ Object.entries(newCompacted).map(([k, v]) => [encodeURI(k), v])
210
+ ) : Object.keys(newCompacted).length ? /* @__PURE__ */ new Map([[defaultedOptions.key, newCompacted]]) : /* @__PURE__ */ new Map();
211
+ const result = [];
212
+ params.forEach((p) => {
213
+ const key = p.split("=")[0];
214
+ if (!managedKeys.has(key)) {
215
+ result.push(p);
216
+ return;
217
+ }
218
+ if (valuesToWrite.has(key)) {
219
+ const encoded = defaultedOptions.format.stringify(
220
+ valuesToWrite.get(key),
221
+ standalone
222
+ );
223
+ result.push(`${key}=${encoded}`);
224
+ valuesToWrite.delete(key);
154
225
  }
155
- return true;
156
226
  });
157
- if (Object.keys(newCompacted).length) {
158
- const value = defaultedOptions.format.stringify(newCompacted);
159
- const position = stateIndex === -1 ? otherParams.length : stateIndex;
160
- otherParams.splice(position, 0, `${defaultedOptions.key}=${value}`);
161
- }
162
- url.search = otherParams.length ? "?" + otherParams.join("&") : "";
227
+ valuesToWrite.forEach((value, key) => {
228
+ const encoded = defaultedOptions.format.stringify(value, standalone);
229
+ result.push(`${key}=${encoded}`);
230
+ });
231
+ url.search = result.length ? "?" + result.join("&") : "";
163
232
  if (url.search !== previous) {
164
233
  history.replaceState(history.state, "", url);
165
234
  }
package/dist/index.mjs CHANGED
@@ -12,22 +12,25 @@ function parse(str) {
12
12
  // src/middleware.ts
13
13
  var compact = (newState, initialState, syncNull = false, syncUndefined = false) => {
14
14
  const output = {};
15
+ const removed = [];
15
16
  Object.keys(newState).forEach((key) => {
16
17
  const newValue = newState[key];
17
18
  const initialValue = initialState[key];
18
19
  if (typeof newValue !== "function" && !isEqual(newValue, initialValue) && (syncNull || newValue !== null) && (syncUndefined || newValue !== void 0)) {
19
20
  const isPlainObject = typeof newValue === "object" && newValue !== null && newValue !== void 0 && !Array.isArray(newValue) && newValue.constructor === Object;
20
21
  if (isPlainObject && initialValue && typeof initialValue === "object") {
21
- const value = compact(newValue, initialValue, syncNull, syncUndefined);
22
- if (value && Object.keys(value).length > 0) {
23
- output[key] = value;
22
+ const result = compact(newValue, initialValue, syncNull, syncUndefined);
23
+ if (result.output && Object.keys(result.output).length > 0) {
24
+ output[key] = result.output;
24
25
  }
25
26
  } else {
26
27
  output[key] = newValue;
27
28
  }
29
+ } else {
30
+ removed.push(key);
28
31
  }
29
32
  });
30
- return output;
33
+ return { output, removed };
31
34
  };
32
35
  var translateSelectionToState = (selection, state) => {
33
36
  if (typeof state !== "object" || !state) {
@@ -59,7 +62,32 @@ var queryStringImpl = (fn, options) => (set, get, api) => {
59
62
  syncUndefined: false,
60
63
  ...options
61
64
  };
62
- const getStateFromUrl = (url) => {
65
+ const standalone = !defaultedOptions.key;
66
+ if (typeof window !== "undefined" && standalone) {
67
+ if (!window.__ZUSTAND_QUERYSTRING_KEYS__) {
68
+ window.__ZUSTAND_QUERYSTRING_KEYS__ = /* @__PURE__ */ new Map();
69
+ }
70
+ }
71
+ const getStateFromUrl = (url, initialState) => {
72
+ if (standalone) {
73
+ const params2 = url.search.slice(1).split("&").filter(Boolean);
74
+ const state = {};
75
+ const initialKeys = new Set(Object.keys(initialState));
76
+ params2.forEach((param) => {
77
+ const eqIndex = param.indexOf("=");
78
+ if (eqIndex === -1) return;
79
+ const key = decodeURI(param.slice(0, eqIndex));
80
+ const value = param.slice(eqIndex + 1);
81
+ if (!initialKeys.has(key)) return;
82
+ try {
83
+ const parsed = defaultedOptions.format.parse(value, true);
84
+ state[key] = parsed;
85
+ } catch (error) {
86
+ console.error("[getStateFromUrl] error parsing key:", key, error);
87
+ }
88
+ });
89
+ return Object.keys(state).length > 0 ? state : null;
90
+ }
63
91
  const params = url.search.slice(1).split("&");
64
92
  for (const param of params) {
65
93
  const eqIndex = param.indexOf("=");
@@ -67,7 +95,8 @@ var queryStringImpl = (fn, options) => (set, get, api) => {
67
95
  const key = param.slice(0, eqIndex);
68
96
  if (key === defaultedOptions.key) {
69
97
  const value = param.slice(eqIndex + 1);
70
- return value ? defaultedOptions.format.parse(value) : null;
98
+ const parsed = value ? defaultedOptions.format.parse(value, false) : null;
99
+ return parsed;
71
100
  }
72
101
  }
73
102
  return null;
@@ -82,18 +111,25 @@ var queryStringImpl = (fn, options) => (set, get, api) => {
82
111
  };
83
112
  const initialize = (url, initialState) => {
84
113
  try {
85
- const stateFromURl = getStateFromUrl(url);
114
+ const stateFromURl = getStateFromUrl(url, initialState);
86
115
  if (!stateFromURl) {
87
116
  return initialState;
88
117
  }
118
+ const selected = getSelectedState(stateFromURl, url.pathname);
89
119
  const merged = mergeWith(
90
120
  {},
91
121
  initialState,
92
- getSelectedState(stateFromURl, url.pathname)
122
+ selected,
123
+ (_objValue, srcValue) => {
124
+ if (Array.isArray(srcValue)) {
125
+ return srcValue;
126
+ }
127
+ return void 0;
128
+ }
93
129
  );
94
130
  return merged;
95
131
  } catch (error) {
96
- console.error(error);
132
+ console.error("[initialize] error:", error);
97
133
  return initialState;
98
134
  }
99
135
  };
@@ -106,10 +142,33 @@ var queryStringImpl = (fn, options) => (set, get, api) => {
106
142
  get,
107
143
  api
108
144
  );
145
+ if (standalone) {
146
+ const registry = window.__ZUSTAND_QUERYSTRING_KEYS__;
147
+ const stateKeys = Object.keys(initialState).filter(
148
+ (k) => typeof initialState[k] !== "function"
149
+ );
150
+ const conflicts = [];
151
+ for (const key of stateKeys) {
152
+ if (registry.has(key)) {
153
+ const existing = registry.get(key);
154
+ const current = defaultedOptions.format;
155
+ if (existing !== current) {
156
+ conflicts.push(key);
157
+ }
158
+ } else {
159
+ registry.set(key, defaultedOptions.format);
160
+ }
161
+ }
162
+ if (conflicts.length > 0) {
163
+ throw new Error(
164
+ `[zustand-querystring] Standalone mode conflict: Multiple stores are using the following keys with different formats: ${conflicts.map((k) => `"${k}"`).join(", ")}. This will cause parsing errors. Please use unique state keys or the same format for all stores sharing keys.`
165
+ );
166
+ }
167
+ }
109
168
  const setQuery = () => {
110
169
  const url = new URL(window.location.href);
111
170
  const selectedState = getSelectedState(get(), url.pathname);
112
- const newCompacted = compact(
171
+ const { output: newCompacted } = compact(
113
172
  selectedState,
114
173
  initialState,
115
174
  defaultedOptions.syncNull,
@@ -117,21 +176,31 @@ var queryStringImpl = (fn, options) => (set, get, api) => {
117
176
  );
118
177
  const previous = url.search;
119
178
  const params = url.search.slice(1).split("&").filter(Boolean);
120
- let stateIndex = -1;
121
- const otherParams = params.filter((p, i) => {
122
- const [key] = p.split("=", 1);
123
- if (key === defaultedOptions.key) {
124
- stateIndex = i;
125
- return false;
179
+ const managedKeys = standalone ? new Set(Object.keys(selectedState).map(encodeURI)) : /* @__PURE__ */ new Set([defaultedOptions.key]);
180
+ const valuesToWrite = standalone ? new Map(
181
+ Object.entries(newCompacted).map(([k, v]) => [encodeURI(k), v])
182
+ ) : Object.keys(newCompacted).length ? /* @__PURE__ */ new Map([[defaultedOptions.key, newCompacted]]) : /* @__PURE__ */ new Map();
183
+ const result = [];
184
+ params.forEach((p) => {
185
+ const key = p.split("=")[0];
186
+ if (!managedKeys.has(key)) {
187
+ result.push(p);
188
+ return;
189
+ }
190
+ if (valuesToWrite.has(key)) {
191
+ const encoded = defaultedOptions.format.stringify(
192
+ valuesToWrite.get(key),
193
+ standalone
194
+ );
195
+ result.push(`${key}=${encoded}`);
196
+ valuesToWrite.delete(key);
126
197
  }
127
- return true;
128
198
  });
129
- if (Object.keys(newCompacted).length) {
130
- const value = defaultedOptions.format.stringify(newCompacted);
131
- const position = stateIndex === -1 ? otherParams.length : stateIndex;
132
- otherParams.splice(position, 0, `${defaultedOptions.key}=${value}`);
133
- }
134
- url.search = otherParams.length ? "?" + otherParams.join("&") : "";
199
+ valuesToWrite.forEach((value, key) => {
200
+ const encoded = defaultedOptions.format.stringify(value, standalone);
201
+ result.push(`${key}=${encoded}`);
202
+ });
203
+ url.search = result.length ? "?" + result.join("&") : "";
135
204
  if (url.search !== previous) {
136
205
  history.replaceState(history.state, "", url);
137
206
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zustand-querystring",
3
- "version": "0.3.2",
3
+ "version": "0.4.1",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",