@lytjs/common-query 6.5.0 → 6.7.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/README.md CHANGED
@@ -1,93 +1,93 @@
1
- # @lytjs/common-query
2
-
3
- URL 查询字符串解析与构建工具,提供轻量级的 URL 处理能力。
4
-
5
- ## 安装
6
-
7
- ```bash
8
- pnpm add @lytjs/common-query
9
- ```
10
-
11
- ## API
12
-
13
- ### `parseQueryString(search: string): Record<string, string>`
14
-
15
- 解析 URL 查询字符串为对象。
16
-
17
- ```typescript
18
- import { parseQueryString } from '@lytjs/common-query';
19
-
20
- parseQueryString('?key=value&key2=value2');
21
- // { key: 'value', key2: 'value2' }
22
-
23
- parseQueryString('key=value&key2=value2');
24
- // { key: 'value', key2: 'value2' }
25
-
26
- parseQueryString('?name=hello%20world');
27
- // { name: 'hello world' }
28
- ```
29
-
30
- ### `stringifyQueryString(params: Record<string, string | number | boolean>): string`
31
-
32
- 将对象序列化为查询字符串。
33
-
34
- ```typescript
35
- import { stringifyQueryString } from '@lytjs/common-query';
36
-
37
- stringifyQueryString({ a: '1', b: '2' });
38
- // 'a=1&b=2'
39
-
40
- stringifyQueryString({ name: 'hello world' });
41
- // 'name=hello%20world'
42
- ```
43
-
44
- ### `parseURL(url: string): ParsedURL`
45
-
46
- 解析完整 URL,返回结构化信息。
47
-
48
- ```typescript
49
- import { parseURL } from '@lytjs/common-query';
50
-
51
- parseURL('https://example.com:8080/path?name=test#section');
52
- // {
53
- // protocol: 'https://',
54
- // host: 'example.com:8080',
55
- // hostname: 'example.com',
56
- // port: '8080',
57
- // pathname: '/path',
58
- // search: '?name=test',
59
- // hash: '#section',
60
- // searchParams: { name: 'test' },
61
- // origin: 'https://example.com:8080',
62
- // href: 'https://example.com:8080/path?name=test#section'
63
- // }
64
- ```
65
-
66
- ### `buildURL(base: string, params?, hash?): string`
67
-
68
- 构建完整 URL,支持合并已有查询参数。
69
-
70
- ```typescript
71
- import { buildURL } from '@lytjs/common-query';
72
-
73
- buildURL('https://example.com/path', { a: '1', b: '2' });
74
- // 'https://example.com/path?a=1&b=2'
75
-
76
- buildURL('https://example.com/path?existing=1', { new: '2' });
77
- // 'https://example.com/path?existing=1&new=2'
78
-
79
- buildURL('/path', { page: 1 }, 'top');
80
- // '/path?page=1#top'
81
- ```
82
-
83
- ## 特性
84
-
85
- - 零运行时依赖
86
- - 体积 < 2KB(min+gzip)
87
- - 支持绝对 URL 和相对 URL
88
- - 自动编解码 URI 组件
89
- - TypeScript 类型完整
90
-
91
- ## License
92
-
93
- MIT
1
+ # @lytjs/common-query
2
+
3
+ URL 查询字符串解析与构建工具,提供轻量级的 URL 处理能力。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ pnpm add @lytjs/common-query
9
+ ```
10
+
11
+ ## API
12
+
13
+ ### `parseQueryString(search: string): Record<string, string>`
14
+
15
+ 解析 URL 查询字符串为对象。
16
+
17
+ ```typescript
18
+ import { parseQueryString } from '@lytjs/common-query';
19
+
20
+ parseQueryString('?key=value&key2=value2');
21
+ // { key: 'value', key2: 'value2' }
22
+
23
+ parseQueryString('key=value&key2=value2');
24
+ // { key: 'value', key2: 'value2' }
25
+
26
+ parseQueryString('?name=hello%20world');
27
+ // { name: 'hello world' }
28
+ ```
29
+
30
+ ### `stringifyQueryString(params: Record<string, string | number | boolean>): string`
31
+
32
+ 将对象序列化为查询字符串。
33
+
34
+ ```typescript
35
+ import { stringifyQueryString } from '@lytjs/common-query';
36
+
37
+ stringifyQueryString({ a: '1', b: '2' });
38
+ // 'a=1&b=2'
39
+
40
+ stringifyQueryString({ name: 'hello world' });
41
+ // 'name=hello%20world'
42
+ ```
43
+
44
+ ### `parseURL(url: string): ParsedURL`
45
+
46
+ 解析完整 URL,返回结构化信息。
47
+
48
+ ```typescript
49
+ import { parseURL } from '@lytjs/common-query';
50
+
51
+ parseURL('https://example.com:8080/path?name=test#section');
52
+ // {
53
+ // protocol: 'https://',
54
+ // host: 'example.com:8080',
55
+ // hostname: 'example.com',
56
+ // port: '8080',
57
+ // pathname: '/path',
58
+ // search: '?name=test',
59
+ // hash: '#section',
60
+ // searchParams: { name: 'test' },
61
+ // origin: 'https://example.com:8080',
62
+ // href: 'https://example.com:8080/path?name=test#section'
63
+ // }
64
+ ```
65
+
66
+ ### `buildURL(base: string, params?, hash?): string`
67
+
68
+ 构建完整 URL,支持合并已有查询参数。
69
+
70
+ ```typescript
71
+ import { buildURL } from '@lytjs/common-query';
72
+
73
+ buildURL('https://example.com/path', { a: '1', b: '2' });
74
+ // 'https://example.com/path?a=1&b=2'
75
+
76
+ buildURL('https://example.com/path?existing=1', { new: '2' });
77
+ // 'https://example.com/path?existing=1&new=2'
78
+
79
+ buildURL('/path', { page: 1 }, 'top');
80
+ // '/path?page=1#top'
81
+ ```
82
+
83
+ ## 特性
84
+
85
+ - 零运行时依赖
86
+ - 体积 < 2KB(min+gzip)
87
+ - 支持绝对 URL 和相对 URL
88
+ - 自动编解码 URI 组件
89
+ - TypeScript 类型完整
90
+
91
+ ## License
92
+
93
+ MIT
package/dist/index.cjs CHANGED
@@ -1,10 +1,46 @@
1
1
  'use strict';
2
2
 
3
3
  // src/index.ts
4
- function parseQueryString(search) {
4
+ function parseQueryString(search, options) {
5
5
  if (!search) return {};
6
6
  const str = search.startsWith("?") ? search.slice(1) : search;
7
7
  if (!str) return {};
8
+ const supportArrays = options?.supportArrays ?? false;
9
+ if (supportArrays) {
10
+ const result2 = {};
11
+ const pairs2 = str.split("&");
12
+ for (const pair of pairs2) {
13
+ if (!pair) continue;
14
+ const eqIndex = pair.indexOf("=");
15
+ let key;
16
+ let value;
17
+ if (eqIndex === -1) {
18
+ key = pair;
19
+ value = "";
20
+ } else {
21
+ key = pair.slice(0, eqIndex);
22
+ value = pair.slice(eqIndex + 1);
23
+ }
24
+ try {
25
+ key = decodeURIComponent(key);
26
+ } catch {
27
+ }
28
+ try {
29
+ value = decodeURIComponent(value);
30
+ } catch {
31
+ }
32
+ if (result2[key]) {
33
+ if (Array.isArray(result2[key])) {
34
+ result2[key].push(value);
35
+ } else {
36
+ result2[key] = [result2[key], value];
37
+ }
38
+ } else {
39
+ result2[key] = value;
40
+ }
41
+ }
42
+ return result2;
43
+ }
8
44
  const result = {};
9
45
  const pairs = str.split("&");
10
46
  for (const pair of pairs) {
@@ -31,19 +67,26 @@ function parseQueryString(search) {
31
67
  }
32
68
  return result;
33
69
  }
70
+ function parseQueryStringWithArrays(search) {
71
+ return parseQueryString(search, { supportArrays: true });
72
+ }
34
73
  function stringifyQueryString(params) {
35
74
  const keys = Object.keys(params);
36
75
  if (keys.length === 0) return "";
37
76
  const parts = [];
38
77
  for (const key of keys) {
39
78
  const value = params[key];
40
- parts.push(
41
- encodeURIComponent(key) + "=" + encodeURIComponent(String(value))
42
- );
79
+ if (Array.isArray(value)) {
80
+ for (const item of value) {
81
+ parts.push(encodeURIComponent(key) + "=" + encodeURIComponent(String(item)));
82
+ }
83
+ } else {
84
+ parts.push(encodeURIComponent(key) + "=" + encodeURIComponent(String(value)));
85
+ }
43
86
  }
44
87
  return parts.join("&");
45
88
  }
46
- function parseURL(url) {
89
+ function parseURL(url, _options) {
47
90
  let rest = url;
48
91
  let hash = "";
49
92
  const hashIndex = rest.indexOf("#");
@@ -99,7 +142,7 @@ function parseURL(url) {
99
142
  } else {
100
143
  pathname = rest;
101
144
  }
102
- const searchParams = parseQueryString(search);
145
+ const searchParams = parseQueryString(search, { supportArrays: true });
103
146
  const origin = protocol ? protocol + host : "";
104
147
  return {
105
148
  protocol,
@@ -130,10 +173,17 @@ function buildURL(base, params, hash) {
130
173
  existingSearch = baseWithoutHash.slice(searchIdx + 1);
131
174
  baseWithoutSearch = baseWithoutHash.slice(0, searchIdx);
132
175
  }
133
- const mergedParams = parseQueryString(existingSearch);
176
+ const mergedParams = parseQueryStringWithArrays(existingSearch);
134
177
  if (params) {
135
178
  for (const key of Object.keys(params)) {
136
- mergedParams[key] = String(params[key]);
179
+ const value = params[key];
180
+ if (value !== void 0 && value !== null) {
181
+ if (Array.isArray(value)) {
182
+ mergedParams[key] = value.map((v) => String(v));
183
+ } else {
184
+ mergedParams[key] = String(value);
185
+ }
186
+ }
137
187
  }
138
188
  }
139
189
  const queryString = stringifyQueryString(mergedParams);
@@ -150,6 +200,7 @@ function buildURL(base, params, hash) {
150
200
 
151
201
  exports.buildURL = buildURL;
152
202
  exports.parseQueryString = parseQueryString;
203
+ exports.parseQueryStringWithArrays = parseQueryStringWithArrays;
153
204
  exports.parseURL = parseURL;
154
205
  exports.stringifyQueryString = stringifyQueryString;
155
206
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\r\n * @lytjs/common-query\r\n * URL 查询字符串解析与构建工具\r\n */\r\n\r\n/**\r\n * 解析后的 URL 结构\r\n */\r\nexport interface ParsedURL {\r\n protocol: string;\r\n host: string;\r\n hostname: string;\r\n port: string;\r\n pathname: string;\r\n search: string;\r\n hash: string;\r\n searchParams: Record<string, string>;\r\n origin: string;\r\n href: string;\r\n}\r\n\r\n/**\r\n * 解析 URL 查询字符串为对象\r\n *\r\n * @param search - 查询字符串,支持 `?key=value` 和 `key=value` 格式\r\n * @returns 解析后的键值对对象\r\n */\r\nexport function parseQueryString(search: string): Record<string, string> {\r\n if (!search) return {};\r\n\r\n const str = search.startsWith('?') ? search.slice(1) : search;\r\n if (!str) return {};\r\n\r\n const result: Record<string, string> = {};\r\n\r\n const pairs = str.split('&');\r\n for (const pair of pairs) {\r\n if (!pair) continue;\r\n const eqIndex = pair.indexOf('=');\r\n let key: string;\r\n let value: string;\r\n if (eqIndex === -1) {\r\n key = pair;\r\n value = '';\r\n } else {\r\n key = pair.slice(0, eqIndex);\r\n value = pair.slice(eqIndex + 1);\r\n }\r\n try {\r\n key = decodeURIComponent(key);\r\n } catch {\r\n // keep key as-is if decode fails\r\n }\r\n try {\r\n value = decodeURIComponent(value);\r\n } catch {\r\n // keep value as-is if decode fails\r\n }\r\n result[key] = value;\r\n }\r\n\r\n return result;\r\n}\r\n\r\n/**\r\n * 将对象序列化为查询字符串\r\n *\r\n * @param params - 键值对对象\r\n * @returns 序列化后的查询字符串(不含前导 ?)\r\n */\r\nexport function stringifyQueryString(\r\n params: Record<string, string | number | boolean>,\r\n): string {\r\n const keys = Object.keys(params);\r\n if (keys.length === 0) return '';\r\n\r\n const parts: string[] = [];\r\n for (const key of keys) {\r\n const value = params[key];\r\n parts.push(\r\n encodeURIComponent(key) + '=' + encodeURIComponent(String(value)),\r\n );\r\n }\r\n\r\n return parts.join('&');\r\n}\r\n\r\n/**\r\n * 解析完整 URL\r\n *\r\n * @param url - URL 字符串,支持绝对 URL 和相对 URL\r\n * @returns 解析后的 ParsedURL 对象\r\n */\r\nexport function parseURL(url: string): ParsedURL {\r\n let rest = url;\r\n\r\n // Extract hash\r\n let hash = '';\r\n const hashIndex = rest.indexOf('#');\r\n if (hashIndex !== -1) {\r\n hash = rest.slice(hashIndex);\r\n rest = rest.slice(0, hashIndex);\r\n }\r\n\r\n // Extract search\r\n let search = '';\r\n const searchIndex = rest.indexOf('?');\r\n if (searchIndex !== -1) {\r\n search = rest.slice(searchIndex);\r\n rest = rest.slice(0, searchIndex);\r\n }\r\n\r\n // Extract protocol\r\n let protocol = '';\r\n const protocolIndex = rest.indexOf('://');\r\n if (protocolIndex !== -1) {\r\n protocol = rest.slice(0, protocolIndex + 3);\r\n rest = rest.slice(protocolIndex + 3);\r\n }\r\n\r\n // Extract pathname (everything after host)\r\n let pathname = '';\r\n let host = '';\r\n let hostname = '';\r\n let port = '';\r\n\r\n if (protocol) {\r\n // Absolute URL: extract host and pathname\r\n const slashIndex = rest.indexOf('/');\r\n if (slashIndex !== -1) {\r\n host = rest.slice(0, slashIndex);\r\n pathname = rest.slice(slashIndex);\r\n } else {\r\n host = rest;\r\n pathname = '';\r\n }\r\n\r\n // Parse host into hostname and port\r\n // Handle IPv6 addresses like [::1]:8080\r\n if (host.startsWith('[')) {\r\n const bracketEnd = host.indexOf(']');\r\n if (bracketEnd !== -1) {\r\n hostname = host.slice(0, bracketEnd + 1);\r\n if (host.length > bracketEnd + 1 && host[bracketEnd + 1] === ':') {\r\n port = host.slice(bracketEnd + 2);\r\n }\r\n } else {\r\n hostname = host;\r\n }\r\n } else {\r\n const colonIndex = host.lastIndexOf(':');\r\n if (colonIndex !== -1) {\r\n hostname = host.slice(0, colonIndex);\r\n port = host.slice(colonIndex + 1);\r\n } else {\r\n hostname = host;\r\n port = '';\r\n }\r\n }\r\n } else {\r\n // Relative URL\r\n pathname = rest;\r\n }\r\n\r\n const searchParams = parseQueryString(search);\r\n const origin = protocol ? protocol + host : '';\r\n\r\n return {\r\n protocol,\r\n host,\r\n hostname,\r\n port,\r\n pathname,\r\n search,\r\n hash,\r\n searchParams,\r\n origin,\r\n href: url,\r\n };\r\n}\r\n\r\n/**\r\n * 构建完整 URL\r\n *\r\n * @param base - 基础 URL\r\n * @param params - 查询参数(可选)\r\n * @param hash - hash 片段(可选,不含 #)\r\n * @returns 构建后的完整 URL\r\n */\r\nexport function buildURL(\r\n base: string,\r\n params?: Record<string, string | number | boolean>,\r\n hash?: string,\r\n): string {\r\n if (!params && !hash) return base;\r\n\r\n // Extract and remove existing hash from base\r\n let baseWithoutHash = base;\r\n let existingHash = '';\r\n const hashIdx = base.indexOf('#');\r\n if (hashIdx !== -1) {\r\n existingHash = base.slice(hashIdx);\r\n baseWithoutHash = base.slice(0, hashIdx);\r\n }\r\n\r\n // Parse existing query params from base (without hash)\r\n let existingSearch = '';\r\n let baseWithoutSearch = baseWithoutHash;\r\n const searchIdx = baseWithoutHash.indexOf('?');\r\n if (searchIdx !== -1) {\r\n existingSearch = baseWithoutHash.slice(searchIdx + 1);\r\n baseWithoutSearch = baseWithoutHash.slice(0, searchIdx);\r\n }\r\n\r\n // Merge existing and new params\r\n const mergedParams = parseQueryString(existingSearch);\r\n if (params) {\r\n for (const key of Object.keys(params)) {\r\n mergedParams[key] = String(params[key]);\r\n }\r\n }\r\n\r\n const queryString = stringifyQueryString(mergedParams);\r\n let result = baseWithoutSearch;\r\n if (queryString) {\r\n result += '?' + queryString;\r\n }\r\n // Use provided hash, or keep existing hash if no new hash is given\r\n const finalHash = hash !== undefined ? '#' + encodeURIComponent(hash) : existingHash;\r\n if (finalHash) {\r\n result += finalHash;\r\n }\r\n\r\n return result;\r\n}\r\n"]}
1
+ {"version":3,"sources":["../src/index.ts"],"names":["result","pairs"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * @lytjs/common-query\n * URL 查询字符串解析与构建工具\n */\n\n/**\n * 解析后的 URL 结构\n */\nexport interface ParsedURL {\n protocol: string;\n host: string;\n hostname: string;\n port: string;\n pathname: string;\n search: string;\n hash: string;\n searchParams: Record<string, string | string[]>;\n origin: string;\n href: string;\n}\n\n/**\n * 解析 URL 查询字符串为对象\n *\n * @param search - 查询字符串,支持 `?key=value` 和 `key=value` 格式\n * @param options - 解析选项\n * @returns 解析后的键值对对象\n */\nexport function parseQueryString(\n search: string,\n options?: { supportArrays?: boolean },\n): Record<string, string> | Record<string, string | string[]> {\n if (!search) return {};\n\n const str = search.startsWith('?') ? search.slice(1) : search;\n if (!str) return {};\n\n const supportArrays = options?.supportArrays ?? false;\n\n if (supportArrays) {\n const result: Record<string, string | string[]> = {};\n const pairs = str.split('&');\n for (const pair of pairs) {\n if (!pair) continue;\n const eqIndex = pair.indexOf('=');\n let key: string;\n let value: string;\n if (eqIndex === -1) {\n key = pair;\n value = '';\n } else {\n key = pair.slice(0, eqIndex);\n value = pair.slice(eqIndex + 1);\n }\n try {\n key = decodeURIComponent(key);\n } catch {\n // keep key as-is if decode fails\n }\n try {\n value = decodeURIComponent(value);\n } catch {\n // keep value as-is if decode fails\n }\n\n if (result[key]) {\n if (Array.isArray(result[key])) {\n (result[key] as string[]).push(value);\n } else {\n result[key] = [result[key] as string, value];\n }\n } else {\n result[key] = value;\n }\n }\n return result;\n }\n\n // 保持原有的行为(向后兼容)\n const result: Record<string, string> = {};\n const pairs = str.split('&');\n for (const pair of pairs) {\n if (!pair) continue;\n const eqIndex = pair.indexOf('=');\n let key: string;\n let value: string;\n if (eqIndex === -1) {\n key = pair;\n value = '';\n } else {\n key = pair.slice(0, eqIndex);\n value = pair.slice(eqIndex + 1);\n }\n try {\n key = decodeURIComponent(key);\n } catch {\n // keep key as-is if decode fails\n }\n try {\n value = decodeURIComponent(value);\n } catch {\n // keep value as-is if decode fails\n }\n result[key] = value;\n }\n return result;\n}\n\n/**\n * 解析 URL 查询字符串为对象,支持数组值(重复键)\n * 这是 parseQueryString 支持数组的便捷版本\n *\n * @param search - 查询字符串,支持 `?key=value` 和 `key=value` 格式\n * @returns 解析后的键值对对象\n */\nexport function parseQueryStringWithArrays(search: string): Record<string, string | string[]> {\n return parseQueryString(search, { supportArrays: true }) as Record<string, string | string[]>;\n}\n\n/**\n * 将对象序列化为查询字符串\n *\n * @param params - 键值对对象,支持数组值\n * @returns 序列化后的查询字符串(不含前导 ?)\n */\nexport function stringifyQueryString(\n params: Record<string, string | number | boolean | Array<string | number | boolean>>,\n): string {\n const keys = Object.keys(params);\n if (keys.length === 0) return '';\n\n const parts: string[] = [];\n for (const key of keys) {\n const value = params[key];\n if (Array.isArray(value)) {\n for (const item of value) {\n parts.push(encodeURIComponent(key) + '=' + encodeURIComponent(String(item)));\n }\n } else {\n parts.push(encodeURIComponent(key) + '=' + encodeURIComponent(String(value)));\n }\n }\n\n return parts.join('&');\n}\n\n/**\n * 解析完整 URL\n *\n * @param url - URL 字符串,支持绝对 URL 和相对 URL\n * @returns 解析后的 ParsedURL 对象\n */\nexport function parseURL(url: string, _options?: { supportArrays?: boolean }): ParsedURL {\n let rest = url;\n\n // Extract hash\n let hash = '';\n const hashIndex = rest.indexOf('#');\n if (hashIndex !== -1) {\n hash = rest.slice(hashIndex);\n rest = rest.slice(0, hashIndex);\n }\n\n // Extract search\n let search = '';\n const searchIndex = rest.indexOf('?');\n if (searchIndex !== -1) {\n search = rest.slice(searchIndex);\n rest = rest.slice(0, searchIndex);\n }\n\n // Extract protocol\n let protocol = '';\n const protocolIndex = rest.indexOf('://');\n if (protocolIndex !== -1) {\n protocol = rest.slice(0, protocolIndex + 3);\n rest = rest.slice(protocolIndex + 3);\n }\n\n // Extract pathname (everything after host)\n let pathname = '';\n let host = '';\n let hostname = '';\n let port = '';\n\n if (protocol) {\n // Absolute URL: extract host and pathname\n const slashIndex = rest.indexOf('/');\n if (slashIndex !== -1) {\n host = rest.slice(0, slashIndex);\n pathname = rest.slice(slashIndex);\n } else {\n host = rest;\n pathname = '';\n }\n\n // Parse host into hostname and port\n // Handle IPv6 addresses like [::1]:8080\n if (host.startsWith('[')) {\n const bracketEnd = host.indexOf(']');\n if (bracketEnd !== -1) {\n hostname = host.slice(0, bracketEnd + 1);\n if (host.length > bracketEnd + 1 && host[bracketEnd + 1] === ':') {\n port = host.slice(bracketEnd + 2);\n }\n } else {\n hostname = host;\n }\n } else {\n const colonIndex = host.lastIndexOf(':');\n if (colonIndex !== -1) {\n hostname = host.slice(0, colonIndex);\n port = host.slice(colonIndex + 1);\n } else {\n hostname = host;\n port = '';\n }\n }\n } else {\n // Relative URL\n pathname = rest;\n }\n\n const searchParams = parseQueryString(search, { supportArrays: true }) as Record<\n string,\n string | string[]\n >;\n const origin = protocol ? protocol + host : '';\n\n return {\n protocol,\n host,\n hostname,\n port,\n pathname,\n search,\n hash,\n searchParams,\n origin,\n href: url,\n };\n}\n\n/**\n * 构建完整 URL\n *\n * @param base - 基础 URL\n * @param params - 查询参数(可选)\n * @param hash - hash 片段(可选,不含 #)\n * @returns 构建后的完整 URL\n */\nexport function buildURL(\n base: string,\n params?: Record<string, string | number | boolean | Array<string | number | boolean>>,\n hash?: string,\n): string {\n if (!params && !hash) return base;\n\n // Extract and remove existing hash from base\n let baseWithoutHash = base;\n let existingHash = '';\n const hashIdx = base.indexOf('#');\n if (hashIdx !== -1) {\n existingHash = base.slice(hashIdx);\n baseWithoutHash = base.slice(0, hashIdx);\n }\n\n // Parse existing query params from base (without hash)\n let existingSearch = '';\n let baseWithoutSearch = baseWithoutHash;\n const searchIdx = baseWithoutHash.indexOf('?');\n if (searchIdx !== -1) {\n existingSearch = baseWithoutHash.slice(searchIdx + 1);\n baseWithoutSearch = baseWithoutHash.slice(0, searchIdx);\n }\n\n // Merge existing and new params\n const mergedParams = parseQueryStringWithArrays(existingSearch);\n if (params) {\n for (const key of Object.keys(params)) {\n const value = params[key];\n if (value !== undefined && value !== null) {\n if (Array.isArray(value)) {\n mergedParams[key] = value.map((v) => String(v));\n } else {\n mergedParams[key] = String(value);\n }\n }\n }\n }\n\n const queryString = stringifyQueryString(mergedParams);\n let result = baseWithoutSearch;\n if (queryString) {\n result += '?' + queryString;\n }\n // Use provided hash, or keep existing hash if no new hash is given\n const finalHash = hash !== undefined ? '#' + encodeURIComponent(hash) : existingHash;\n if (finalHash) {\n result += finalHash;\n }\n\n return result;\n}\n"]}
package/dist/index.d.mts CHANGED
@@ -13,7 +13,7 @@ interface ParsedURL {
13
13
  pathname: string;
14
14
  search: string;
15
15
  hash: string;
16
- searchParams: Record<string, string>;
16
+ searchParams: Record<string, string | string[]>;
17
17
  origin: string;
18
18
  href: string;
19
19
  }
@@ -21,23 +21,36 @@ interface ParsedURL {
21
21
  * 解析 URL 查询字符串为对象
22
22
  *
23
23
  * @param search - 查询字符串,支持 `?key=value` 和 `key=value` 格式
24
+ * @param options - 解析选项
24
25
  * @returns 解析后的键值对对象
25
26
  */
26
- declare function parseQueryString(search: string): Record<string, string>;
27
+ declare function parseQueryString(search: string, options?: {
28
+ supportArrays?: boolean;
29
+ }): Record<string, string> | Record<string, string | string[]>;
30
+ /**
31
+ * 解析 URL 查询字符串为对象,支持数组值(重复键)
32
+ * 这是 parseQueryString 支持数组的便捷版本
33
+ *
34
+ * @param search - 查询字符串,支持 `?key=value` 和 `key=value` 格式
35
+ * @returns 解析后的键值对对象
36
+ */
37
+ declare function parseQueryStringWithArrays(search: string): Record<string, string | string[]>;
27
38
  /**
28
39
  * 将对象序列化为查询字符串
29
40
  *
30
- * @param params - 键值对对象
41
+ * @param params - 键值对对象,支持数组值
31
42
  * @returns 序列化后的查询字符串(不含前导 ?)
32
43
  */
33
- declare function stringifyQueryString(params: Record<string, string | number | boolean>): string;
44
+ declare function stringifyQueryString(params: Record<string, string | number | boolean | Array<string | number | boolean>>): string;
34
45
  /**
35
46
  * 解析完整 URL
36
47
  *
37
48
  * @param url - URL 字符串,支持绝对 URL 和相对 URL
38
49
  * @returns 解析后的 ParsedURL 对象
39
50
  */
40
- declare function parseURL(url: string): ParsedURL;
51
+ declare function parseURL(url: string, _options?: {
52
+ supportArrays?: boolean;
53
+ }): ParsedURL;
41
54
  /**
42
55
  * 构建完整 URL
43
56
  *
@@ -46,6 +59,6 @@ declare function parseURL(url: string): ParsedURL;
46
59
  * @param hash - hash 片段(可选,不含 #)
47
60
  * @returns 构建后的完整 URL
48
61
  */
49
- declare function buildURL(base: string, params?: Record<string, string | number | boolean>, hash?: string): string;
62
+ declare function buildURL(base: string, params?: Record<string, string | number | boolean | Array<string | number | boolean>>, hash?: string): string;
50
63
 
51
- export { type ParsedURL, buildURL, parseQueryString, parseURL, stringifyQueryString };
64
+ export { type ParsedURL, buildURL, parseQueryString, parseQueryStringWithArrays, parseURL, stringifyQueryString };
package/dist/index.d.ts CHANGED
@@ -13,7 +13,7 @@ interface ParsedURL {
13
13
  pathname: string;
14
14
  search: string;
15
15
  hash: string;
16
- searchParams: Record<string, string>;
16
+ searchParams: Record<string, string | string[]>;
17
17
  origin: string;
18
18
  href: string;
19
19
  }
@@ -21,23 +21,36 @@ interface ParsedURL {
21
21
  * 解析 URL 查询字符串为对象
22
22
  *
23
23
  * @param search - 查询字符串,支持 `?key=value` 和 `key=value` 格式
24
+ * @param options - 解析选项
24
25
  * @returns 解析后的键值对对象
25
26
  */
26
- declare function parseQueryString(search: string): Record<string, string>;
27
+ declare function parseQueryString(search: string, options?: {
28
+ supportArrays?: boolean;
29
+ }): Record<string, string> | Record<string, string | string[]>;
30
+ /**
31
+ * 解析 URL 查询字符串为对象,支持数组值(重复键)
32
+ * 这是 parseQueryString 支持数组的便捷版本
33
+ *
34
+ * @param search - 查询字符串,支持 `?key=value` 和 `key=value` 格式
35
+ * @returns 解析后的键值对对象
36
+ */
37
+ declare function parseQueryStringWithArrays(search: string): Record<string, string | string[]>;
27
38
  /**
28
39
  * 将对象序列化为查询字符串
29
40
  *
30
- * @param params - 键值对对象
41
+ * @param params - 键值对对象,支持数组值
31
42
  * @returns 序列化后的查询字符串(不含前导 ?)
32
43
  */
33
- declare function stringifyQueryString(params: Record<string, string | number | boolean>): string;
44
+ declare function stringifyQueryString(params: Record<string, string | number | boolean | Array<string | number | boolean>>): string;
34
45
  /**
35
46
  * 解析完整 URL
36
47
  *
37
48
  * @param url - URL 字符串,支持绝对 URL 和相对 URL
38
49
  * @returns 解析后的 ParsedURL 对象
39
50
  */
40
- declare function parseURL(url: string): ParsedURL;
51
+ declare function parseURL(url: string, _options?: {
52
+ supportArrays?: boolean;
53
+ }): ParsedURL;
41
54
  /**
42
55
  * 构建完整 URL
43
56
  *
@@ -46,6 +59,6 @@ declare function parseURL(url: string): ParsedURL;
46
59
  * @param hash - hash 片段(可选,不含 #)
47
60
  * @returns 构建后的完整 URL
48
61
  */
49
- declare function buildURL(base: string, params?: Record<string, string | number | boolean>, hash?: string): string;
62
+ declare function buildURL(base: string, params?: Record<string, string | number | boolean | Array<string | number | boolean>>, hash?: string): string;
50
63
 
51
- export { type ParsedURL, buildURL, parseQueryString, parseURL, stringifyQueryString };
64
+ export { type ParsedURL, buildURL, parseQueryString, parseQueryStringWithArrays, parseURL, stringifyQueryString };
package/dist/index.mjs CHANGED
@@ -1,8 +1,44 @@
1
1
  // src/index.ts
2
- function parseQueryString(search) {
2
+ function parseQueryString(search, options) {
3
3
  if (!search) return {};
4
4
  const str = search.startsWith("?") ? search.slice(1) : search;
5
5
  if (!str) return {};
6
+ const supportArrays = options?.supportArrays ?? false;
7
+ if (supportArrays) {
8
+ const result2 = {};
9
+ const pairs2 = str.split("&");
10
+ for (const pair of pairs2) {
11
+ if (!pair) continue;
12
+ const eqIndex = pair.indexOf("=");
13
+ let key;
14
+ let value;
15
+ if (eqIndex === -1) {
16
+ key = pair;
17
+ value = "";
18
+ } else {
19
+ key = pair.slice(0, eqIndex);
20
+ value = pair.slice(eqIndex + 1);
21
+ }
22
+ try {
23
+ key = decodeURIComponent(key);
24
+ } catch {
25
+ }
26
+ try {
27
+ value = decodeURIComponent(value);
28
+ } catch {
29
+ }
30
+ if (result2[key]) {
31
+ if (Array.isArray(result2[key])) {
32
+ result2[key].push(value);
33
+ } else {
34
+ result2[key] = [result2[key], value];
35
+ }
36
+ } else {
37
+ result2[key] = value;
38
+ }
39
+ }
40
+ return result2;
41
+ }
6
42
  const result = {};
7
43
  const pairs = str.split("&");
8
44
  for (const pair of pairs) {
@@ -29,19 +65,26 @@ function parseQueryString(search) {
29
65
  }
30
66
  return result;
31
67
  }
68
+ function parseQueryStringWithArrays(search) {
69
+ return parseQueryString(search, { supportArrays: true });
70
+ }
32
71
  function stringifyQueryString(params) {
33
72
  const keys = Object.keys(params);
34
73
  if (keys.length === 0) return "";
35
74
  const parts = [];
36
75
  for (const key of keys) {
37
76
  const value = params[key];
38
- parts.push(
39
- encodeURIComponent(key) + "=" + encodeURIComponent(String(value))
40
- );
77
+ if (Array.isArray(value)) {
78
+ for (const item of value) {
79
+ parts.push(encodeURIComponent(key) + "=" + encodeURIComponent(String(item)));
80
+ }
81
+ } else {
82
+ parts.push(encodeURIComponent(key) + "=" + encodeURIComponent(String(value)));
83
+ }
41
84
  }
42
85
  return parts.join("&");
43
86
  }
44
- function parseURL(url) {
87
+ function parseURL(url, _options) {
45
88
  let rest = url;
46
89
  let hash = "";
47
90
  const hashIndex = rest.indexOf("#");
@@ -97,7 +140,7 @@ function parseURL(url) {
97
140
  } else {
98
141
  pathname = rest;
99
142
  }
100
- const searchParams = parseQueryString(search);
143
+ const searchParams = parseQueryString(search, { supportArrays: true });
101
144
  const origin = protocol ? protocol + host : "";
102
145
  return {
103
146
  protocol,
@@ -128,10 +171,17 @@ function buildURL(base, params, hash) {
128
171
  existingSearch = baseWithoutHash.slice(searchIdx + 1);
129
172
  baseWithoutSearch = baseWithoutHash.slice(0, searchIdx);
130
173
  }
131
- const mergedParams = parseQueryString(existingSearch);
174
+ const mergedParams = parseQueryStringWithArrays(existingSearch);
132
175
  if (params) {
133
176
  for (const key of Object.keys(params)) {
134
- mergedParams[key] = String(params[key]);
177
+ const value = params[key];
178
+ if (value !== void 0 && value !== null) {
179
+ if (Array.isArray(value)) {
180
+ mergedParams[key] = value.map((v) => String(v));
181
+ } else {
182
+ mergedParams[key] = String(value);
183
+ }
184
+ }
135
185
  }
136
186
  }
137
187
  const queryString = stringifyQueryString(mergedParams);
@@ -146,6 +196,6 @@ function buildURL(base, params, hash) {
146
196
  return result;
147
197
  }
148
198
 
149
- export { buildURL, parseQueryString, parseURL, stringifyQueryString };
199
+ export { buildURL, parseQueryString, parseQueryStringWithArrays, parseURL, stringifyQueryString };
150
200
  //# sourceMappingURL=index.mjs.map
151
201
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\r\n * @lytjs/common-query\r\n * URL 查询字符串解析与构建工具\r\n */\r\n\r\n/**\r\n * 解析后的 URL 结构\r\n */\r\nexport interface ParsedURL {\r\n protocol: string;\r\n host: string;\r\n hostname: string;\r\n port: string;\r\n pathname: string;\r\n search: string;\r\n hash: string;\r\n searchParams: Record<string, string>;\r\n origin: string;\r\n href: string;\r\n}\r\n\r\n/**\r\n * 解析 URL 查询字符串为对象\r\n *\r\n * @param search - 查询字符串,支持 `?key=value` 和 `key=value` 格式\r\n * @returns 解析后的键值对对象\r\n */\r\nexport function parseQueryString(search: string): Record<string, string> {\r\n if (!search) return {};\r\n\r\n const str = search.startsWith('?') ? search.slice(1) : search;\r\n if (!str) return {};\r\n\r\n const result: Record<string, string> = {};\r\n\r\n const pairs = str.split('&');\r\n for (const pair of pairs) {\r\n if (!pair) continue;\r\n const eqIndex = pair.indexOf('=');\r\n let key: string;\r\n let value: string;\r\n if (eqIndex === -1) {\r\n key = pair;\r\n value = '';\r\n } else {\r\n key = pair.slice(0, eqIndex);\r\n value = pair.slice(eqIndex + 1);\r\n }\r\n try {\r\n key = decodeURIComponent(key);\r\n } catch {\r\n // keep key as-is if decode fails\r\n }\r\n try {\r\n value = decodeURIComponent(value);\r\n } catch {\r\n // keep value as-is if decode fails\r\n }\r\n result[key] = value;\r\n }\r\n\r\n return result;\r\n}\r\n\r\n/**\r\n * 将对象序列化为查询字符串\r\n *\r\n * @param params - 键值对对象\r\n * @returns 序列化后的查询字符串(不含前导 ?)\r\n */\r\nexport function stringifyQueryString(\r\n params: Record<string, string | number | boolean>,\r\n): string {\r\n const keys = Object.keys(params);\r\n if (keys.length === 0) return '';\r\n\r\n const parts: string[] = [];\r\n for (const key of keys) {\r\n const value = params[key];\r\n parts.push(\r\n encodeURIComponent(key) + '=' + encodeURIComponent(String(value)),\r\n );\r\n }\r\n\r\n return parts.join('&');\r\n}\r\n\r\n/**\r\n * 解析完整 URL\r\n *\r\n * @param url - URL 字符串,支持绝对 URL 和相对 URL\r\n * @returns 解析后的 ParsedURL 对象\r\n */\r\nexport function parseURL(url: string): ParsedURL {\r\n let rest = url;\r\n\r\n // Extract hash\r\n let hash = '';\r\n const hashIndex = rest.indexOf('#');\r\n if (hashIndex !== -1) {\r\n hash = rest.slice(hashIndex);\r\n rest = rest.slice(0, hashIndex);\r\n }\r\n\r\n // Extract search\r\n let search = '';\r\n const searchIndex = rest.indexOf('?');\r\n if (searchIndex !== -1) {\r\n search = rest.slice(searchIndex);\r\n rest = rest.slice(0, searchIndex);\r\n }\r\n\r\n // Extract protocol\r\n let protocol = '';\r\n const protocolIndex = rest.indexOf('://');\r\n if (protocolIndex !== -1) {\r\n protocol = rest.slice(0, protocolIndex + 3);\r\n rest = rest.slice(protocolIndex + 3);\r\n }\r\n\r\n // Extract pathname (everything after host)\r\n let pathname = '';\r\n let host = '';\r\n let hostname = '';\r\n let port = '';\r\n\r\n if (protocol) {\r\n // Absolute URL: extract host and pathname\r\n const slashIndex = rest.indexOf('/');\r\n if (slashIndex !== -1) {\r\n host = rest.slice(0, slashIndex);\r\n pathname = rest.slice(slashIndex);\r\n } else {\r\n host = rest;\r\n pathname = '';\r\n }\r\n\r\n // Parse host into hostname and port\r\n // Handle IPv6 addresses like [::1]:8080\r\n if (host.startsWith('[')) {\r\n const bracketEnd = host.indexOf(']');\r\n if (bracketEnd !== -1) {\r\n hostname = host.slice(0, bracketEnd + 1);\r\n if (host.length > bracketEnd + 1 && host[bracketEnd + 1] === ':') {\r\n port = host.slice(bracketEnd + 2);\r\n }\r\n } else {\r\n hostname = host;\r\n }\r\n } else {\r\n const colonIndex = host.lastIndexOf(':');\r\n if (colonIndex !== -1) {\r\n hostname = host.slice(0, colonIndex);\r\n port = host.slice(colonIndex + 1);\r\n } else {\r\n hostname = host;\r\n port = '';\r\n }\r\n }\r\n } else {\r\n // Relative URL\r\n pathname = rest;\r\n }\r\n\r\n const searchParams = parseQueryString(search);\r\n const origin = protocol ? protocol + host : '';\r\n\r\n return {\r\n protocol,\r\n host,\r\n hostname,\r\n port,\r\n pathname,\r\n search,\r\n hash,\r\n searchParams,\r\n origin,\r\n href: url,\r\n };\r\n}\r\n\r\n/**\r\n * 构建完整 URL\r\n *\r\n * @param base - 基础 URL\r\n * @param params - 查询参数(可选)\r\n * @param hash - hash 片段(可选,不含 #)\r\n * @returns 构建后的完整 URL\r\n */\r\nexport function buildURL(\r\n base: string,\r\n params?: Record<string, string | number | boolean>,\r\n hash?: string,\r\n): string {\r\n if (!params && !hash) return base;\r\n\r\n // Extract and remove existing hash from base\r\n let baseWithoutHash = base;\r\n let existingHash = '';\r\n const hashIdx = base.indexOf('#');\r\n if (hashIdx !== -1) {\r\n existingHash = base.slice(hashIdx);\r\n baseWithoutHash = base.slice(0, hashIdx);\r\n }\r\n\r\n // Parse existing query params from base (without hash)\r\n let existingSearch = '';\r\n let baseWithoutSearch = baseWithoutHash;\r\n const searchIdx = baseWithoutHash.indexOf('?');\r\n if (searchIdx !== -1) {\r\n existingSearch = baseWithoutHash.slice(searchIdx + 1);\r\n baseWithoutSearch = baseWithoutHash.slice(0, searchIdx);\r\n }\r\n\r\n // Merge existing and new params\r\n const mergedParams = parseQueryString(existingSearch);\r\n if (params) {\r\n for (const key of Object.keys(params)) {\r\n mergedParams[key] = String(params[key]);\r\n }\r\n }\r\n\r\n const queryString = stringifyQueryString(mergedParams);\r\n let result = baseWithoutSearch;\r\n if (queryString) {\r\n result += '?' + queryString;\r\n }\r\n // Use provided hash, or keep existing hash if no new hash is given\r\n const finalHash = hash !== undefined ? '#' + encodeURIComponent(hash) : existingHash;\r\n if (finalHash) {\r\n result += finalHash;\r\n }\r\n\r\n return result;\r\n}\r\n"]}
1
+ {"version":3,"sources":["../src/index.ts"],"names":["result","pairs"],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * @lytjs/common-query\n * URL 查询字符串解析与构建工具\n */\n\n/**\n * 解析后的 URL 结构\n */\nexport interface ParsedURL {\n protocol: string;\n host: string;\n hostname: string;\n port: string;\n pathname: string;\n search: string;\n hash: string;\n searchParams: Record<string, string | string[]>;\n origin: string;\n href: string;\n}\n\n/**\n * 解析 URL 查询字符串为对象\n *\n * @param search - 查询字符串,支持 `?key=value` 和 `key=value` 格式\n * @param options - 解析选项\n * @returns 解析后的键值对对象\n */\nexport function parseQueryString(\n search: string,\n options?: { supportArrays?: boolean },\n): Record<string, string> | Record<string, string | string[]> {\n if (!search) return {};\n\n const str = search.startsWith('?') ? search.slice(1) : search;\n if (!str) return {};\n\n const supportArrays = options?.supportArrays ?? false;\n\n if (supportArrays) {\n const result: Record<string, string | string[]> = {};\n const pairs = str.split('&');\n for (const pair of pairs) {\n if (!pair) continue;\n const eqIndex = pair.indexOf('=');\n let key: string;\n let value: string;\n if (eqIndex === -1) {\n key = pair;\n value = '';\n } else {\n key = pair.slice(0, eqIndex);\n value = pair.slice(eqIndex + 1);\n }\n try {\n key = decodeURIComponent(key);\n } catch {\n // keep key as-is if decode fails\n }\n try {\n value = decodeURIComponent(value);\n } catch {\n // keep value as-is if decode fails\n }\n\n if (result[key]) {\n if (Array.isArray(result[key])) {\n (result[key] as string[]).push(value);\n } else {\n result[key] = [result[key] as string, value];\n }\n } else {\n result[key] = value;\n }\n }\n return result;\n }\n\n // 保持原有的行为(向后兼容)\n const result: Record<string, string> = {};\n const pairs = str.split('&');\n for (const pair of pairs) {\n if (!pair) continue;\n const eqIndex = pair.indexOf('=');\n let key: string;\n let value: string;\n if (eqIndex === -1) {\n key = pair;\n value = '';\n } else {\n key = pair.slice(0, eqIndex);\n value = pair.slice(eqIndex + 1);\n }\n try {\n key = decodeURIComponent(key);\n } catch {\n // keep key as-is if decode fails\n }\n try {\n value = decodeURIComponent(value);\n } catch {\n // keep value as-is if decode fails\n }\n result[key] = value;\n }\n return result;\n}\n\n/**\n * 解析 URL 查询字符串为对象,支持数组值(重复键)\n * 这是 parseQueryString 支持数组的便捷版本\n *\n * @param search - 查询字符串,支持 `?key=value` 和 `key=value` 格式\n * @returns 解析后的键值对对象\n */\nexport function parseQueryStringWithArrays(search: string): Record<string, string | string[]> {\n return parseQueryString(search, { supportArrays: true }) as Record<string, string | string[]>;\n}\n\n/**\n * 将对象序列化为查询字符串\n *\n * @param params - 键值对对象,支持数组值\n * @returns 序列化后的查询字符串(不含前导 ?)\n */\nexport function stringifyQueryString(\n params: Record<string, string | number | boolean | Array<string | number | boolean>>,\n): string {\n const keys = Object.keys(params);\n if (keys.length === 0) return '';\n\n const parts: string[] = [];\n for (const key of keys) {\n const value = params[key];\n if (Array.isArray(value)) {\n for (const item of value) {\n parts.push(encodeURIComponent(key) + '=' + encodeURIComponent(String(item)));\n }\n } else {\n parts.push(encodeURIComponent(key) + '=' + encodeURIComponent(String(value)));\n }\n }\n\n return parts.join('&');\n}\n\n/**\n * 解析完整 URL\n *\n * @param url - URL 字符串,支持绝对 URL 和相对 URL\n * @returns 解析后的 ParsedURL 对象\n */\nexport function parseURL(url: string, _options?: { supportArrays?: boolean }): ParsedURL {\n let rest = url;\n\n // Extract hash\n let hash = '';\n const hashIndex = rest.indexOf('#');\n if (hashIndex !== -1) {\n hash = rest.slice(hashIndex);\n rest = rest.slice(0, hashIndex);\n }\n\n // Extract search\n let search = '';\n const searchIndex = rest.indexOf('?');\n if (searchIndex !== -1) {\n search = rest.slice(searchIndex);\n rest = rest.slice(0, searchIndex);\n }\n\n // Extract protocol\n let protocol = '';\n const protocolIndex = rest.indexOf('://');\n if (protocolIndex !== -1) {\n protocol = rest.slice(0, protocolIndex + 3);\n rest = rest.slice(protocolIndex + 3);\n }\n\n // Extract pathname (everything after host)\n let pathname = '';\n let host = '';\n let hostname = '';\n let port = '';\n\n if (protocol) {\n // Absolute URL: extract host and pathname\n const slashIndex = rest.indexOf('/');\n if (slashIndex !== -1) {\n host = rest.slice(0, slashIndex);\n pathname = rest.slice(slashIndex);\n } else {\n host = rest;\n pathname = '';\n }\n\n // Parse host into hostname and port\n // Handle IPv6 addresses like [::1]:8080\n if (host.startsWith('[')) {\n const bracketEnd = host.indexOf(']');\n if (bracketEnd !== -1) {\n hostname = host.slice(0, bracketEnd + 1);\n if (host.length > bracketEnd + 1 && host[bracketEnd + 1] === ':') {\n port = host.slice(bracketEnd + 2);\n }\n } else {\n hostname = host;\n }\n } else {\n const colonIndex = host.lastIndexOf(':');\n if (colonIndex !== -1) {\n hostname = host.slice(0, colonIndex);\n port = host.slice(colonIndex + 1);\n } else {\n hostname = host;\n port = '';\n }\n }\n } else {\n // Relative URL\n pathname = rest;\n }\n\n const searchParams = parseQueryString(search, { supportArrays: true }) as Record<\n string,\n string | string[]\n >;\n const origin = protocol ? protocol + host : '';\n\n return {\n protocol,\n host,\n hostname,\n port,\n pathname,\n search,\n hash,\n searchParams,\n origin,\n href: url,\n };\n}\n\n/**\n * 构建完整 URL\n *\n * @param base - 基础 URL\n * @param params - 查询参数(可选)\n * @param hash - hash 片段(可选,不含 #)\n * @returns 构建后的完整 URL\n */\nexport function buildURL(\n base: string,\n params?: Record<string, string | number | boolean | Array<string | number | boolean>>,\n hash?: string,\n): string {\n if (!params && !hash) return base;\n\n // Extract and remove existing hash from base\n let baseWithoutHash = base;\n let existingHash = '';\n const hashIdx = base.indexOf('#');\n if (hashIdx !== -1) {\n existingHash = base.slice(hashIdx);\n baseWithoutHash = base.slice(0, hashIdx);\n }\n\n // Parse existing query params from base (without hash)\n let existingSearch = '';\n let baseWithoutSearch = baseWithoutHash;\n const searchIdx = baseWithoutHash.indexOf('?');\n if (searchIdx !== -1) {\n existingSearch = baseWithoutHash.slice(searchIdx + 1);\n baseWithoutSearch = baseWithoutHash.slice(0, searchIdx);\n }\n\n // Merge existing and new params\n const mergedParams = parseQueryStringWithArrays(existingSearch);\n if (params) {\n for (const key of Object.keys(params)) {\n const value = params[key];\n if (value !== undefined && value !== null) {\n if (Array.isArray(value)) {\n mergedParams[key] = value.map((v) => String(v));\n } else {\n mergedParams[key] = String(value);\n }\n }\n }\n }\n\n const queryString = stringifyQueryString(mergedParams);\n let result = baseWithoutSearch;\n if (queryString) {\n result += '?' + queryString;\n }\n // Use provided hash, or keep existing hash if no new hash is given\n const finalHash = hash !== undefined ? '#' + encodeURIComponent(hash) : existingHash;\n if (finalHash) {\n result += finalHash;\n }\n\n return result;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lytjs/common-query",
3
- "version": "6.5.0",
3
+ "version": "6.7.0",
4
4
  "description": "URL query string parsing and building utilities for LytJS",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.mjs",
package/src/env.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- // 全局 __DEV__ 声明
2
- // 规范版本位于 @lytjs/shared-types/src/global.d.ts
3
- // 此处保留直接声明以确保 tsup DTS 构建时类型可用
4
- declare const __DEV__: boolean;
1
+ // 全局 __DEV__ 声明
2
+ // 规范版本位于 @lytjs/shared-types/src/global.d.ts
3
+ // 此处保留直接声明以确保 tsup DTS 构建时类型可用
4
+ declare const __DEV__: boolean;
package/src/index.ts CHANGED
@@ -1,235 +1,304 @@
1
- /**
2
- * @lytjs/common-query
3
- * URL 查询字符串解析与构建工具
4
- */
5
-
6
- /**
7
- * 解析后的 URL 结构
8
- */
9
- export interface ParsedURL {
10
- protocol: string;
11
- host: string;
12
- hostname: string;
13
- port: string;
14
- pathname: string;
15
- search: string;
16
- hash: string;
17
- searchParams: Record<string, string>;
18
- origin: string;
19
- href: string;
20
- }
21
-
22
- /**
23
- * 解析 URL 查询字符串为对象
24
- *
25
- * @param search - 查询字符串,支持 `?key=value` 和 `key=value` 格式
26
- * @returns 解析后的键值对对象
27
- */
28
- export function parseQueryString(search: string): Record<string, string> {
29
- if (!search) return {};
30
-
31
- const str = search.startsWith('?') ? search.slice(1) : search;
32
- if (!str) return {};
33
-
34
- const result: Record<string, string> = {};
35
-
36
- const pairs = str.split('&');
37
- for (const pair of pairs) {
38
- if (!pair) continue;
39
- const eqIndex = pair.indexOf('=');
40
- let key: string;
41
- let value: string;
42
- if (eqIndex === -1) {
43
- key = pair;
44
- value = '';
45
- } else {
46
- key = pair.slice(0, eqIndex);
47
- value = pair.slice(eqIndex + 1);
48
- }
49
- try {
50
- key = decodeURIComponent(key);
51
- } catch {
52
- // keep key as-is if decode fails
53
- }
54
- try {
55
- value = decodeURIComponent(value);
56
- } catch {
57
- // keep value as-is if decode fails
58
- }
59
- result[key] = value;
60
- }
61
-
62
- return result;
63
- }
64
-
65
- /**
66
- * 将对象序列化为查询字符串
67
- *
68
- * @param params - 键值对对象
69
- * @returns 序列化后的查询字符串(不含前导 ?)
70
- */
71
- export function stringifyQueryString(
72
- params: Record<string, string | number | boolean>,
73
- ): string {
74
- const keys = Object.keys(params);
75
- if (keys.length === 0) return '';
76
-
77
- const parts: string[] = [];
78
- for (const key of keys) {
79
- const value = params[key];
80
- parts.push(
81
- encodeURIComponent(key) + '=' + encodeURIComponent(String(value)),
82
- );
83
- }
84
-
85
- return parts.join('&');
86
- }
87
-
88
- /**
89
- * 解析完整 URL
90
- *
91
- * @param url - URL 字符串,支持绝对 URL 和相对 URL
92
- * @returns 解析后的 ParsedURL 对象
93
- */
94
- export function parseURL(url: string): ParsedURL {
95
- let rest = url;
96
-
97
- // Extract hash
98
- let hash = '';
99
- const hashIndex = rest.indexOf('#');
100
- if (hashIndex !== -1) {
101
- hash = rest.slice(hashIndex);
102
- rest = rest.slice(0, hashIndex);
103
- }
104
-
105
- // Extract search
106
- let search = '';
107
- const searchIndex = rest.indexOf('?');
108
- if (searchIndex !== -1) {
109
- search = rest.slice(searchIndex);
110
- rest = rest.slice(0, searchIndex);
111
- }
112
-
113
- // Extract protocol
114
- let protocol = '';
115
- const protocolIndex = rest.indexOf('://');
116
- if (protocolIndex !== -1) {
117
- protocol = rest.slice(0, protocolIndex + 3);
118
- rest = rest.slice(protocolIndex + 3);
119
- }
120
-
121
- // Extract pathname (everything after host)
122
- let pathname = '';
123
- let host = '';
124
- let hostname = '';
125
- let port = '';
126
-
127
- if (protocol) {
128
- // Absolute URL: extract host and pathname
129
- const slashIndex = rest.indexOf('/');
130
- if (slashIndex !== -1) {
131
- host = rest.slice(0, slashIndex);
132
- pathname = rest.slice(slashIndex);
133
- } else {
134
- host = rest;
135
- pathname = '';
136
- }
137
-
138
- // Parse host into hostname and port
139
- // Handle IPv6 addresses like [::1]:8080
140
- if (host.startsWith('[')) {
141
- const bracketEnd = host.indexOf(']');
142
- if (bracketEnd !== -1) {
143
- hostname = host.slice(0, bracketEnd + 1);
144
- if (host.length > bracketEnd + 1 && host[bracketEnd + 1] === ':') {
145
- port = host.slice(bracketEnd + 2);
146
- }
147
- } else {
148
- hostname = host;
149
- }
150
- } else {
151
- const colonIndex = host.lastIndexOf(':');
152
- if (colonIndex !== -1) {
153
- hostname = host.slice(0, colonIndex);
154
- port = host.slice(colonIndex + 1);
155
- } else {
156
- hostname = host;
157
- port = '';
158
- }
159
- }
160
- } else {
161
- // Relative URL
162
- pathname = rest;
163
- }
164
-
165
- const searchParams = parseQueryString(search);
166
- const origin = protocol ? protocol + host : '';
167
-
168
- return {
169
- protocol,
170
- host,
171
- hostname,
172
- port,
173
- pathname,
174
- search,
175
- hash,
176
- searchParams,
177
- origin,
178
- href: url,
179
- };
180
- }
181
-
182
- /**
183
- * 构建完整 URL
184
- *
185
- * @param base - 基础 URL
186
- * @param params - 查询参数(可选)
187
- * @param hash - hash 片段(可选,不含 #)
188
- * @returns 构建后的完整 URL
189
- */
190
- export function buildURL(
191
- base: string,
192
- params?: Record<string, string | number | boolean>,
193
- hash?: string,
194
- ): string {
195
- if (!params && !hash) return base;
196
-
197
- // Extract and remove existing hash from base
198
- let baseWithoutHash = base;
199
- let existingHash = '';
200
- const hashIdx = base.indexOf('#');
201
- if (hashIdx !== -1) {
202
- existingHash = base.slice(hashIdx);
203
- baseWithoutHash = base.slice(0, hashIdx);
204
- }
205
-
206
- // Parse existing query params from base (without hash)
207
- let existingSearch = '';
208
- let baseWithoutSearch = baseWithoutHash;
209
- const searchIdx = baseWithoutHash.indexOf('?');
210
- if (searchIdx !== -1) {
211
- existingSearch = baseWithoutHash.slice(searchIdx + 1);
212
- baseWithoutSearch = baseWithoutHash.slice(0, searchIdx);
213
- }
214
-
215
- // Merge existing and new params
216
- const mergedParams = parseQueryString(existingSearch);
217
- if (params) {
218
- for (const key of Object.keys(params)) {
219
- mergedParams[key] = String(params[key]);
220
- }
221
- }
222
-
223
- const queryString = stringifyQueryString(mergedParams);
224
- let result = baseWithoutSearch;
225
- if (queryString) {
226
- result += '?' + queryString;
227
- }
228
- // Use provided hash, or keep existing hash if no new hash is given
229
- const finalHash = hash !== undefined ? '#' + encodeURIComponent(hash) : existingHash;
230
- if (finalHash) {
231
- result += finalHash;
232
- }
233
-
234
- return result;
235
- }
1
+ /**
2
+ * @lytjs/common-query
3
+ * URL 查询字符串解析与构建工具
4
+ */
5
+
6
+ /**
7
+ * 解析后的 URL 结构
8
+ */
9
+ export interface ParsedURL {
10
+ protocol: string;
11
+ host: string;
12
+ hostname: string;
13
+ port: string;
14
+ pathname: string;
15
+ search: string;
16
+ hash: string;
17
+ searchParams: Record<string, string | string[]>;
18
+ origin: string;
19
+ href: string;
20
+ }
21
+
22
+ /**
23
+ * 解析 URL 查询字符串为对象
24
+ *
25
+ * @param search - 查询字符串,支持 `?key=value` 和 `key=value` 格式
26
+ * @param options - 解析选项
27
+ * @returns 解析后的键值对对象
28
+ */
29
+ export function parseQueryString(
30
+ search: string,
31
+ options?: { supportArrays?: boolean },
32
+ ): Record<string, string> | Record<string, string | string[]> {
33
+ if (!search) return {};
34
+
35
+ const str = search.startsWith('?') ? search.slice(1) : search;
36
+ if (!str) return {};
37
+
38
+ const supportArrays = options?.supportArrays ?? false;
39
+
40
+ if (supportArrays) {
41
+ const result: Record<string, string | string[]> = {};
42
+ const pairs = str.split('&');
43
+ for (const pair of pairs) {
44
+ if (!pair) continue;
45
+ const eqIndex = pair.indexOf('=');
46
+ let key: string;
47
+ let value: string;
48
+ if (eqIndex === -1) {
49
+ key = pair;
50
+ value = '';
51
+ } else {
52
+ key = pair.slice(0, eqIndex);
53
+ value = pair.slice(eqIndex + 1);
54
+ }
55
+ try {
56
+ key = decodeURIComponent(key);
57
+ } catch {
58
+ // keep key as-is if decode fails
59
+ }
60
+ try {
61
+ value = decodeURIComponent(value);
62
+ } catch {
63
+ // keep value as-is if decode fails
64
+ }
65
+
66
+ if (result[key]) {
67
+ if (Array.isArray(result[key])) {
68
+ (result[key] as string[]).push(value);
69
+ } else {
70
+ result[key] = [result[key] as string, value];
71
+ }
72
+ } else {
73
+ result[key] = value;
74
+ }
75
+ }
76
+ return result;
77
+ }
78
+
79
+ // 保持原有的行为(向后兼容)
80
+ const result: Record<string, string> = {};
81
+ const pairs = str.split('&');
82
+ for (const pair of pairs) {
83
+ if (!pair) continue;
84
+ const eqIndex = pair.indexOf('=');
85
+ let key: string;
86
+ let value: string;
87
+ if (eqIndex === -1) {
88
+ key = pair;
89
+ value = '';
90
+ } else {
91
+ key = pair.slice(0, eqIndex);
92
+ value = pair.slice(eqIndex + 1);
93
+ }
94
+ try {
95
+ key = decodeURIComponent(key);
96
+ } catch {
97
+ // keep key as-is if decode fails
98
+ }
99
+ try {
100
+ value = decodeURIComponent(value);
101
+ } catch {
102
+ // keep value as-is if decode fails
103
+ }
104
+ result[key] = value;
105
+ }
106
+ return result;
107
+ }
108
+
109
+ /**
110
+ * 解析 URL 查询字符串为对象,支持数组值(重复键)
111
+ * 这是 parseQueryString 支持数组的便捷版本
112
+ *
113
+ * @param search - 查询字符串,支持 `?key=value` 和 `key=value` 格式
114
+ * @returns 解析后的键值对对象
115
+ */
116
+ export function parseQueryStringWithArrays(search: string): Record<string, string | string[]> {
117
+ return parseQueryString(search, { supportArrays: true }) as Record<string, string | string[]>;
118
+ }
119
+
120
+ /**
121
+ * 将对象序列化为查询字符串
122
+ *
123
+ * @param params - 键值对对象,支持数组值
124
+ * @returns 序列化后的查询字符串(不含前导 ?)
125
+ */
126
+ export function stringifyQueryString(
127
+ params: Record<string, string | number | boolean | Array<string | number | boolean>>,
128
+ ): string {
129
+ const keys = Object.keys(params);
130
+ if (keys.length === 0) return '';
131
+
132
+ const parts: string[] = [];
133
+ for (const key of keys) {
134
+ const value = params[key];
135
+ if (Array.isArray(value)) {
136
+ for (const item of value) {
137
+ parts.push(encodeURIComponent(key) + '=' + encodeURIComponent(String(item)));
138
+ }
139
+ } else {
140
+ parts.push(encodeURIComponent(key) + '=' + encodeURIComponent(String(value)));
141
+ }
142
+ }
143
+
144
+ return parts.join('&');
145
+ }
146
+
147
+ /**
148
+ * 解析完整 URL
149
+ *
150
+ * @param url - URL 字符串,支持绝对 URL 和相对 URL
151
+ * @returns 解析后的 ParsedURL 对象
152
+ */
153
+ export function parseURL(url: string, _options?: { supportArrays?: boolean }): ParsedURL {
154
+ let rest = url;
155
+
156
+ // Extract hash
157
+ let hash = '';
158
+ const hashIndex = rest.indexOf('#');
159
+ if (hashIndex !== -1) {
160
+ hash = rest.slice(hashIndex);
161
+ rest = rest.slice(0, hashIndex);
162
+ }
163
+
164
+ // Extract search
165
+ let search = '';
166
+ const searchIndex = rest.indexOf('?');
167
+ if (searchIndex !== -1) {
168
+ search = rest.slice(searchIndex);
169
+ rest = rest.slice(0, searchIndex);
170
+ }
171
+
172
+ // Extract protocol
173
+ let protocol = '';
174
+ const protocolIndex = rest.indexOf('://');
175
+ if (protocolIndex !== -1) {
176
+ protocol = rest.slice(0, protocolIndex + 3);
177
+ rest = rest.slice(protocolIndex + 3);
178
+ }
179
+
180
+ // Extract pathname (everything after host)
181
+ let pathname = '';
182
+ let host = '';
183
+ let hostname = '';
184
+ let port = '';
185
+
186
+ if (protocol) {
187
+ // Absolute URL: extract host and pathname
188
+ const slashIndex = rest.indexOf('/');
189
+ if (slashIndex !== -1) {
190
+ host = rest.slice(0, slashIndex);
191
+ pathname = rest.slice(slashIndex);
192
+ } else {
193
+ host = rest;
194
+ pathname = '';
195
+ }
196
+
197
+ // Parse host into hostname and port
198
+ // Handle IPv6 addresses like [::1]:8080
199
+ if (host.startsWith('[')) {
200
+ const bracketEnd = host.indexOf(']');
201
+ if (bracketEnd !== -1) {
202
+ hostname = host.slice(0, bracketEnd + 1);
203
+ if (host.length > bracketEnd + 1 && host[bracketEnd + 1] === ':') {
204
+ port = host.slice(bracketEnd + 2);
205
+ }
206
+ } else {
207
+ hostname = host;
208
+ }
209
+ } else {
210
+ const colonIndex = host.lastIndexOf(':');
211
+ if (colonIndex !== -1) {
212
+ hostname = host.slice(0, colonIndex);
213
+ port = host.slice(colonIndex + 1);
214
+ } else {
215
+ hostname = host;
216
+ port = '';
217
+ }
218
+ }
219
+ } else {
220
+ // Relative URL
221
+ pathname = rest;
222
+ }
223
+
224
+ const searchParams = parseQueryString(search, { supportArrays: true }) as Record<
225
+ string,
226
+ string | string[]
227
+ >;
228
+ const origin = protocol ? protocol + host : '';
229
+
230
+ return {
231
+ protocol,
232
+ host,
233
+ hostname,
234
+ port,
235
+ pathname,
236
+ search,
237
+ hash,
238
+ searchParams,
239
+ origin,
240
+ href: url,
241
+ };
242
+ }
243
+
244
+ /**
245
+ * 构建完整 URL
246
+ *
247
+ * @param base - 基础 URL
248
+ * @param params - 查询参数(可选)
249
+ * @param hash - hash 片段(可选,不含 #)
250
+ * @returns 构建后的完整 URL
251
+ */
252
+ export function buildURL(
253
+ base: string,
254
+ params?: Record<string, string | number | boolean | Array<string | number | boolean>>,
255
+ hash?: string,
256
+ ): string {
257
+ if (!params && !hash) return base;
258
+
259
+ // Extract and remove existing hash from base
260
+ let baseWithoutHash = base;
261
+ let existingHash = '';
262
+ const hashIdx = base.indexOf('#');
263
+ if (hashIdx !== -1) {
264
+ existingHash = base.slice(hashIdx);
265
+ baseWithoutHash = base.slice(0, hashIdx);
266
+ }
267
+
268
+ // Parse existing query params from base (without hash)
269
+ let existingSearch = '';
270
+ let baseWithoutSearch = baseWithoutHash;
271
+ const searchIdx = baseWithoutHash.indexOf('?');
272
+ if (searchIdx !== -1) {
273
+ existingSearch = baseWithoutHash.slice(searchIdx + 1);
274
+ baseWithoutSearch = baseWithoutHash.slice(0, searchIdx);
275
+ }
276
+
277
+ // Merge existing and new params
278
+ const mergedParams = parseQueryStringWithArrays(existingSearch);
279
+ if (params) {
280
+ for (const key of Object.keys(params)) {
281
+ const value = params[key];
282
+ if (value !== undefined && value !== null) {
283
+ if (Array.isArray(value)) {
284
+ mergedParams[key] = value.map((v) => String(v));
285
+ } else {
286
+ mergedParams[key] = String(value);
287
+ }
288
+ }
289
+ }
290
+ }
291
+
292
+ const queryString = stringifyQueryString(mergedParams);
293
+ let result = baseWithoutSearch;
294
+ if (queryString) {
295
+ result += '?' + queryString;
296
+ }
297
+ // Use provided hash, or keep existing hash if no new hash is given
298
+ const finalHash = hash !== undefined ? '#' + encodeURIComponent(hash) : existingHash;
299
+ if (finalHash) {
300
+ result += finalHash;
301
+ }
302
+
303
+ return result;
304
+ }