@lytjs/common-query 6.0.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 ADDED
@@ -0,0 +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
package/dist/index.cjs ADDED
@@ -0,0 +1,156 @@
1
+ 'use strict';
2
+
3
+ // src/index.ts
4
+ function parseQueryString(search) {
5
+ if (!search) return {};
6
+ const str = search.startsWith("?") ? search.slice(1) : search;
7
+ if (!str) return {};
8
+ const result = {};
9
+ const pairs = str.split("&");
10
+ for (const pair of pairs) {
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
+ result[key] = value;
31
+ }
32
+ return result;
33
+ }
34
+ function stringifyQueryString(params) {
35
+ const keys = Object.keys(params);
36
+ if (keys.length === 0) return "";
37
+ const parts = [];
38
+ for (const key of keys) {
39
+ const value = params[key];
40
+ parts.push(
41
+ encodeURIComponent(key) + "=" + encodeURIComponent(String(value))
42
+ );
43
+ }
44
+ return parts.join("&");
45
+ }
46
+ function parseURL(url) {
47
+ let rest = url;
48
+ let hash = "";
49
+ const hashIndex = rest.indexOf("#");
50
+ if (hashIndex !== -1) {
51
+ hash = rest.slice(hashIndex);
52
+ rest = rest.slice(0, hashIndex);
53
+ }
54
+ let search = "";
55
+ const searchIndex = rest.indexOf("?");
56
+ if (searchIndex !== -1) {
57
+ search = rest.slice(searchIndex);
58
+ rest = rest.slice(0, searchIndex);
59
+ }
60
+ let protocol = "";
61
+ const protocolIndex = rest.indexOf("://");
62
+ if (protocolIndex !== -1) {
63
+ protocol = rest.slice(0, protocolIndex + 3);
64
+ rest = rest.slice(protocolIndex + 3);
65
+ }
66
+ let pathname = "";
67
+ let host = "";
68
+ let hostname = "";
69
+ let port = "";
70
+ if (protocol) {
71
+ const slashIndex = rest.indexOf("/");
72
+ if (slashIndex !== -1) {
73
+ host = rest.slice(0, slashIndex);
74
+ pathname = rest.slice(slashIndex);
75
+ } else {
76
+ host = rest;
77
+ pathname = "";
78
+ }
79
+ if (host.startsWith("[")) {
80
+ const bracketEnd = host.indexOf("]");
81
+ if (bracketEnd !== -1) {
82
+ hostname = host.slice(0, bracketEnd + 1);
83
+ if (host.length > bracketEnd + 1 && host[bracketEnd + 1] === ":") {
84
+ port = host.slice(bracketEnd + 2);
85
+ }
86
+ } else {
87
+ hostname = host;
88
+ }
89
+ } else {
90
+ const colonIndex = host.lastIndexOf(":");
91
+ if (colonIndex !== -1) {
92
+ hostname = host.slice(0, colonIndex);
93
+ port = host.slice(colonIndex + 1);
94
+ } else {
95
+ hostname = host;
96
+ port = "";
97
+ }
98
+ }
99
+ } else {
100
+ pathname = rest;
101
+ }
102
+ const searchParams = parseQueryString(search);
103
+ const origin = protocol ? protocol + host : "";
104
+ return {
105
+ protocol,
106
+ host,
107
+ hostname,
108
+ port,
109
+ pathname,
110
+ search,
111
+ hash,
112
+ searchParams,
113
+ origin,
114
+ href: url
115
+ };
116
+ }
117
+ function buildURL(base, params, hash) {
118
+ if (!params && !hash) return base;
119
+ let baseWithoutHash = base;
120
+ let existingHash = "";
121
+ const hashIdx = base.indexOf("#");
122
+ if (hashIdx !== -1) {
123
+ existingHash = base.slice(hashIdx);
124
+ baseWithoutHash = base.slice(0, hashIdx);
125
+ }
126
+ let existingSearch = "";
127
+ let baseWithoutSearch = baseWithoutHash;
128
+ const searchIdx = baseWithoutHash.indexOf("?");
129
+ if (searchIdx !== -1) {
130
+ existingSearch = baseWithoutHash.slice(searchIdx + 1);
131
+ baseWithoutSearch = baseWithoutHash.slice(0, searchIdx);
132
+ }
133
+ const mergedParams = parseQueryString(existingSearch);
134
+ if (params) {
135
+ for (const key of Object.keys(params)) {
136
+ mergedParams[key] = String(params[key]);
137
+ }
138
+ }
139
+ const queryString = stringifyQueryString(mergedParams);
140
+ let result = baseWithoutSearch;
141
+ if (queryString) {
142
+ result += "?" + queryString;
143
+ }
144
+ const finalHash = hash !== void 0 ? "#" + encodeURIComponent(hash) : existingHash;
145
+ if (finalHash) {
146
+ result += finalHash;
147
+ }
148
+ return result;
149
+ }
150
+
151
+ exports.buildURL = buildURL;
152
+ exports.parseQueryString = parseQueryString;
153
+ exports.parseURL = parseURL;
154
+ exports.stringifyQueryString = stringifyQueryString;
155
+ //# sourceMappingURL=index.cjs.map
156
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +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"]}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * @lytjs/common-query
3
+ * URL 查询字符串解析与构建工具
4
+ */
5
+ /**
6
+ * 解析后的 URL 结构
7
+ */
8
+ interface ParsedURL {
9
+ protocol: string;
10
+ host: string;
11
+ hostname: string;
12
+ port: string;
13
+ pathname: string;
14
+ search: string;
15
+ hash: string;
16
+ searchParams: Record<string, string>;
17
+ origin: string;
18
+ href: string;
19
+ }
20
+ /**
21
+ * 解析 URL 查询字符串为对象
22
+ *
23
+ * @param search - 查询字符串,支持 `?key=value` 和 `key=value` 格式
24
+ * @returns 解析后的键值对对象
25
+ */
26
+ declare function parseQueryString(search: string): Record<string, string>;
27
+ /**
28
+ * 将对象序列化为查询字符串
29
+ *
30
+ * @param params - 键值对对象
31
+ * @returns 序列化后的查询字符串(不含前导 ?)
32
+ */
33
+ declare function stringifyQueryString(params: Record<string, string | number | boolean>): string;
34
+ /**
35
+ * 解析完整 URL
36
+ *
37
+ * @param url - URL 字符串,支持绝对 URL 和相对 URL
38
+ * @returns 解析后的 ParsedURL 对象
39
+ */
40
+ declare function parseURL(url: string): ParsedURL;
41
+ /**
42
+ * 构建完整 URL
43
+ *
44
+ * @param base - 基础 URL
45
+ * @param params - 查询参数(可选)
46
+ * @param hash - hash 片段(可选,不含 #)
47
+ * @returns 构建后的完整 URL
48
+ */
49
+ declare function buildURL(base: string, params?: Record<string, string | number | boolean>, hash?: string): string;
50
+
51
+ export { type ParsedURL, buildURL, parseQueryString, parseURL, stringifyQueryString };
@@ -0,0 +1,51 @@
1
+ /**
2
+ * @lytjs/common-query
3
+ * URL 查询字符串解析与构建工具
4
+ */
5
+ /**
6
+ * 解析后的 URL 结构
7
+ */
8
+ interface ParsedURL {
9
+ protocol: string;
10
+ host: string;
11
+ hostname: string;
12
+ port: string;
13
+ pathname: string;
14
+ search: string;
15
+ hash: string;
16
+ searchParams: Record<string, string>;
17
+ origin: string;
18
+ href: string;
19
+ }
20
+ /**
21
+ * 解析 URL 查询字符串为对象
22
+ *
23
+ * @param search - 查询字符串,支持 `?key=value` 和 `key=value` 格式
24
+ * @returns 解析后的键值对对象
25
+ */
26
+ declare function parseQueryString(search: string): Record<string, string>;
27
+ /**
28
+ * 将对象序列化为查询字符串
29
+ *
30
+ * @param params - 键值对对象
31
+ * @returns 序列化后的查询字符串(不含前导 ?)
32
+ */
33
+ declare function stringifyQueryString(params: Record<string, string | number | boolean>): string;
34
+ /**
35
+ * 解析完整 URL
36
+ *
37
+ * @param url - URL 字符串,支持绝对 URL 和相对 URL
38
+ * @returns 解析后的 ParsedURL 对象
39
+ */
40
+ declare function parseURL(url: string): ParsedURL;
41
+ /**
42
+ * 构建完整 URL
43
+ *
44
+ * @param base - 基础 URL
45
+ * @param params - 查询参数(可选)
46
+ * @param hash - hash 片段(可选,不含 #)
47
+ * @returns 构建后的完整 URL
48
+ */
49
+ declare function buildURL(base: string, params?: Record<string, string | number | boolean>, hash?: string): string;
50
+
51
+ export { type ParsedURL, buildURL, parseQueryString, parseURL, stringifyQueryString };
package/dist/index.mjs ADDED
@@ -0,0 +1,151 @@
1
+ // src/index.ts
2
+ function parseQueryString(search) {
3
+ if (!search) return {};
4
+ const str = search.startsWith("?") ? search.slice(1) : search;
5
+ if (!str) return {};
6
+ const result = {};
7
+ const pairs = str.split("&");
8
+ for (const pair of pairs) {
9
+ if (!pair) continue;
10
+ const eqIndex = pair.indexOf("=");
11
+ let key;
12
+ let value;
13
+ if (eqIndex === -1) {
14
+ key = pair;
15
+ value = "";
16
+ } else {
17
+ key = pair.slice(0, eqIndex);
18
+ value = pair.slice(eqIndex + 1);
19
+ }
20
+ try {
21
+ key = decodeURIComponent(key);
22
+ } catch {
23
+ }
24
+ try {
25
+ value = decodeURIComponent(value);
26
+ } catch {
27
+ }
28
+ result[key] = value;
29
+ }
30
+ return result;
31
+ }
32
+ function stringifyQueryString(params) {
33
+ const keys = Object.keys(params);
34
+ if (keys.length === 0) return "";
35
+ const parts = [];
36
+ for (const key of keys) {
37
+ const value = params[key];
38
+ parts.push(
39
+ encodeURIComponent(key) + "=" + encodeURIComponent(String(value))
40
+ );
41
+ }
42
+ return parts.join("&");
43
+ }
44
+ function parseURL(url) {
45
+ let rest = url;
46
+ let hash = "";
47
+ const hashIndex = rest.indexOf("#");
48
+ if (hashIndex !== -1) {
49
+ hash = rest.slice(hashIndex);
50
+ rest = rest.slice(0, hashIndex);
51
+ }
52
+ let search = "";
53
+ const searchIndex = rest.indexOf("?");
54
+ if (searchIndex !== -1) {
55
+ search = rest.slice(searchIndex);
56
+ rest = rest.slice(0, searchIndex);
57
+ }
58
+ let protocol = "";
59
+ const protocolIndex = rest.indexOf("://");
60
+ if (protocolIndex !== -1) {
61
+ protocol = rest.slice(0, protocolIndex + 3);
62
+ rest = rest.slice(protocolIndex + 3);
63
+ }
64
+ let pathname = "";
65
+ let host = "";
66
+ let hostname = "";
67
+ let port = "";
68
+ if (protocol) {
69
+ const slashIndex = rest.indexOf("/");
70
+ if (slashIndex !== -1) {
71
+ host = rest.slice(0, slashIndex);
72
+ pathname = rest.slice(slashIndex);
73
+ } else {
74
+ host = rest;
75
+ pathname = "";
76
+ }
77
+ if (host.startsWith("[")) {
78
+ const bracketEnd = host.indexOf("]");
79
+ if (bracketEnd !== -1) {
80
+ hostname = host.slice(0, bracketEnd + 1);
81
+ if (host.length > bracketEnd + 1 && host[bracketEnd + 1] === ":") {
82
+ port = host.slice(bracketEnd + 2);
83
+ }
84
+ } else {
85
+ hostname = host;
86
+ }
87
+ } else {
88
+ const colonIndex = host.lastIndexOf(":");
89
+ if (colonIndex !== -1) {
90
+ hostname = host.slice(0, colonIndex);
91
+ port = host.slice(colonIndex + 1);
92
+ } else {
93
+ hostname = host;
94
+ port = "";
95
+ }
96
+ }
97
+ } else {
98
+ pathname = rest;
99
+ }
100
+ const searchParams = parseQueryString(search);
101
+ const origin = protocol ? protocol + host : "";
102
+ return {
103
+ protocol,
104
+ host,
105
+ hostname,
106
+ port,
107
+ pathname,
108
+ search,
109
+ hash,
110
+ searchParams,
111
+ origin,
112
+ href: url
113
+ };
114
+ }
115
+ function buildURL(base, params, hash) {
116
+ if (!params && !hash) return base;
117
+ let baseWithoutHash = base;
118
+ let existingHash = "";
119
+ const hashIdx = base.indexOf("#");
120
+ if (hashIdx !== -1) {
121
+ existingHash = base.slice(hashIdx);
122
+ baseWithoutHash = base.slice(0, hashIdx);
123
+ }
124
+ let existingSearch = "";
125
+ let baseWithoutSearch = baseWithoutHash;
126
+ const searchIdx = baseWithoutHash.indexOf("?");
127
+ if (searchIdx !== -1) {
128
+ existingSearch = baseWithoutHash.slice(searchIdx + 1);
129
+ baseWithoutSearch = baseWithoutHash.slice(0, searchIdx);
130
+ }
131
+ const mergedParams = parseQueryString(existingSearch);
132
+ if (params) {
133
+ for (const key of Object.keys(params)) {
134
+ mergedParams[key] = String(params[key]);
135
+ }
136
+ }
137
+ const queryString = stringifyQueryString(mergedParams);
138
+ let result = baseWithoutSearch;
139
+ if (queryString) {
140
+ result += "?" + queryString;
141
+ }
142
+ const finalHash = hash !== void 0 ? "#" + encodeURIComponent(hash) : existingHash;
143
+ if (finalHash) {
144
+ result += finalHash;
145
+ }
146
+ return result;
147
+ }
148
+
149
+ export { buildURL, parseQueryString, parseURL, stringifyQueryString };
150
+ //# sourceMappingURL=index.mjs.map
151
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +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"]}
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@lytjs/common-query",
3
+ "version": "6.0.0",
4
+ "description": "URL query string parsing and building utilities for LytJS",
5
+ "main": "./dist/index.cjs",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.cjs"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "src"
18
+ ],
19
+ "scripts": {
20
+ "test": "vitest run",
21
+ "test:watch": "vitest",
22
+ "test:coverage": "vitest run --coverage",
23
+ "lint": "eslint \"src/**/*.ts\"",
24
+ "build": "tsup",
25
+ "type-check": "tsc --noEmit",
26
+ "clean": "rm -rf dist"
27
+ },
28
+ "sideEffects": false,
29
+ "license": "MIT",
30
+ "devDependencies": {
31
+ "tsup": "^8.4.0",
32
+ "typescript": "^5.8.2",
33
+ "vitest": "^3.0.7"
34
+ }
35
+ }
package/src/env.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ // 全局 __DEV__ 声明
2
+ // 规范版本位于 @lytjs/shared-types/src/global.d.ts
3
+ // 此处保留直接声明以确保 tsup DTS 构建时类型可用
4
+ declare const __DEV__: boolean;
package/src/index.ts ADDED
@@ -0,0 +1,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>;
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
+ }