@nsnanocat/util 2.3.0 → 2.4.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
@@ -5,7 +5,7 @@
5
5
  核心目标:
6
6
  - 统一不同平台的 HTTP、通知、持久化、结束脚本等调用方式。
7
7
  - 在一个脚本里尽量少写平台分支。
8
- - 提供一组可直接复用的 polyfill(`fetch` / `Storage` / `KV` / `Console` / `Lodash`)。
8
+ - 提供一组可直接复用的 polyfill(`fetch` / `Storage` / `KV` / `Console` / `Lodash` / `qs`)。
9
9
 
10
10
  ## 目录
11
11
  - [安装与导入](#安装与导入)
@@ -66,6 +66,7 @@ import {
66
66
  Console, // 统一日志工具(支持 logLevel)
67
67
  Lodash as _, // Lodash 建议按官方示例惯例使用 `_` 作为工具对象别名
68
68
  KV, // Cloudflare Workers KV 异步适配器(显式传入 namespace binding)
69
+ qs, // 查询字符串工具(parse / stringify)
69
70
  Storage, // 统一持久化存储接口(适配 $prefs / $persistentStore / 内存 / 文件)
70
71
  } from "@nsnanocat/util";
71
72
  ```
@@ -83,6 +84,7 @@ import {
83
84
  - `polyfill/fetch.mjs`
84
85
  - `polyfill/KV.mjs`
85
86
  - `polyfill/Lodash.mjs`
87
+ - `polyfill/qs.mjs`
86
88
  - `polyfill/StatusTexts.mjs`
87
89
  - `polyfill/Storage.mjs`
88
90
 
@@ -101,7 +103,7 @@ import {
101
103
  | --- | --- | --- | --- |
102
104
  | `lib/app.mjs` | 无 | 无 | 核心平台识别源头,供其他差异模块分流 |
103
105
  | `lib/environment.mjs` | `lib/app.mjs` | `$app` | 按平台生成统一 `$environment`(尤其补齐 `app` 字段) |
104
- | `lib/argument.mjs` | `polyfill/Console.mjs`, `polyfill/Lodash.mjs` | `Console.debug`, `Console.logLevel`, `Lodash.set` | 统一 `$argument` 结构并支持深路径写入 |
106
+ | `lib/argument.mjs` | `polyfill/Console.mjs`, `polyfill/qs.mjs` | `Console.debug`, `Console.logLevel`, `qs.parse` | 统一 `$argument` 结构,并委托 `qs.parse` 处理字符串/对象/空值输入 |
105
107
  | `lib/done.mjs` | `lib/app.mjs`, `polyfill/Console.mjs`, `polyfill/Lodash.mjs`, `polyfill/StatusTexts.mjs` | `$app`, `Console.log`, `Lodash.set`, `Lodash.pick`, `StatusTexts` | 将各平台 `$done` 参数格式拉平并兼容状态码/策略字段 |
106
108
  | `lib/notification.mjs` | `lib/app.mjs`, `polyfill/Console.mjs` | `$app`, `Console.group`, `Console.log`, `Console.groupEnd`, `Console.error` | 将通知参数映射到各平台通知接口并统一日志输出 |
107
109
  | `lib/runScript.mjs` | `polyfill/Console.mjs`, `polyfill/fetch.mjs`, `polyfill/Storage.mjs`, `polyfill/Lodash.mjs` | `Console.error`, `fetch`, `Storage.getItem`(`Lodash` 当前版本未实际调用) | 读取 BoxJS 配置并发起统一 HTTP 调用执行脚本 |
@@ -111,6 +113,7 @@ import {
111
113
  | `polyfill/KV.mjs` | `lib/app.mjs`, `polyfill/Lodash.mjs`, `polyfill/Storage.mjs` | `$app`, `Lodash.get`, `Lodash.set`, `Lodash.unset`, `Storage` | 为 Cloudflare Workers KV 提供异步适配,并在非 Worker 平台回退到 `Storage` |
112
114
  | `polyfill/Storage.mjs` | `lib/app.mjs`, `polyfill/Lodash.mjs` | `$app`, `Lodash.get`, `Lodash.set`, `Lodash.unset` | 按平台选持久化后端并支持 `@key.path` 读写 |
113
115
  | `polyfill/Lodash.mjs` | 无 | 无 | 提供路径/合并等基础能力,被多个模块复用 |
116
+ | `polyfill/qs.mjs` | `polyfill/Lodash.mjs` | `Lodash.get`, `Lodash.set`, `Lodash.toPath` | 提供查询字符串与对象之间的解析/序列化能力 |
114
117
  | `polyfill/StatusTexts.mjs` | 无 | 无 | 提供 HTTP 状态文案,供 `fetch/done` 使用 |
115
118
  | `index.js` / `lib/index.js` / `polyfill/index.js` | 多个模块 | `export *` | 聚合导出,不含业务逻辑 |
116
119
 
@@ -173,6 +176,7 @@ console.log(environment()); // 当前环境对象
173
176
  - 通过包入口导入(`import ... from "@nsnanocat/util"`)时会自动执行本模块。
174
177
  - JSCore 环境不支持 `await import`,请使用静态导入或直接走包入口导入。
175
178
  - 读取到的 `$argument` 会按 URL Params 样式格式化为对象,并支持深路径。
179
+ - 内部实现统一委托给 `qs.parse(globalThis.$argument)`。
176
180
  - 你也可以通过 `import { $argument } from "@nsnanocat/util"` 读取当前已标准化的 `$argument` 快照。
177
181
  - 平台输入差异说明:
178
182
  - Surge / Stash / Egern:脚本参数通常以字符串形式传入(如 `a=1&b=2`)。
@@ -658,6 +662,53 @@ console.log(value); // 1
658
662
  - `undefined` 不覆盖,`null` 会覆盖。
659
663
  - 直接修改目标对象(mutates target)。
660
664
 
665
+ ### `polyfill/qs.mjs`
666
+
667
+ 当前实现为项目内使用的轻量子集,不追求与官方 `qs` 完全一致。
668
+
669
+ #### `qs.parse(query)`
670
+ - 签名:`qs.parse(query?: string | object | null): object`
671
+ - 作用:将查询字符串或对象输入归一化为支持深路径的对象。
672
+ - 依赖:内部使用 `Lodash.set` 展开路径。
673
+
674
+ 当前行为:
675
+ - 当 `query` 为 `string` 时:
676
+ - 按 `&` / `=` 切分。
677
+ - 去掉值中的双引号。
678
+ - 使用点路径或数组下标路径展开对象。
679
+ - 当 `query` 为 `object` 时:
680
+ - 将 key 当路径写入新对象(`{"a.b":"1"}` -> `{ a: { b: "1" } }`)。
681
+ - 当 `query` 为 `null` 或 `undefined` 时:
682
+ - 返回 `{}`。
683
+
684
+ ```js
685
+ import { qs } from "@nsnanocat/util";
686
+
687
+ console.log(qs.parse("mode=on&a.b=1"));
688
+ // { mode: "on", a: { b: "1" } }
689
+
690
+ console.log(qs.parse({ "list[0]": "x", "list[1]": "y" }));
691
+ // { list: ["x", "y"] }
692
+ ```
693
+
694
+ #### `qs.stringify(object)`
695
+ - 签名:`qs.stringify(object?: object): string`
696
+ - 作用:将对象按深路径展开为查询字符串。
697
+ - 依赖:内部使用 `Lodash.get` 与 `Lodash.toPath` 读取并格式化路径。
698
+
699
+ 当前行为:
700
+ - 普通对象输出为点路径(`a.b=1`)。
701
+ - 数组输出为索引路径(`list[0]=x`)。
702
+ - 值始终执行 `encodeURIComponent` 编码。
703
+ - `null` 会序列化为空值(`key=`),`undefined` 会跳过。
704
+
705
+ ```js
706
+ import { qs } from "@nsnanocat/util";
707
+
708
+ console.log(qs.stringify({ a: { b: "1" }, list: ["x", "y"] }));
709
+ // a.b=1&list%5B0%5D=x&list%5B1%5D=y
710
+ ```
711
+
661
712
  ### `polyfill/StatusTexts.mjs`
662
713
 
663
714
  #### `StatusTexts`
package/index.js CHANGED
@@ -8,6 +8,7 @@ export * from "./polyfill/Console.mjs";
8
8
  export * from "./polyfill/fetch.mjs";
9
9
  export * from "./polyfill/KV.mjs";
10
10
  export * from "./polyfill/Lodash.mjs";
11
+ export * from "./polyfill/qs.mjs";
11
12
  export * from "./polyfill/StatusTexts.mjs";
12
13
  export * from "./polyfill/Storage.mjs";
13
14
 
package/lib/argument.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Console } from "../polyfill/Console.mjs";
2
- import { Lodash as _ } from "../polyfill/Lodash.mjs";
2
+ import { qs } from "../polyfill/qs.mjs";
3
3
 
4
4
  /**
5
5
  * 统一 `$argument` 输入格式并展开深路径。
@@ -28,27 +28,7 @@ import { Lodash as _ } from "../polyfill/Lodash.mjs";
28
28
  */
29
29
  (() => {
30
30
  Console.debug("☑️ $argument");
31
- switch (typeof globalThis.$argument) {
32
- case "string": {
33
- const argument = Object.fromEntries(globalThis.$argument.split("&").map(item => item.split("=", 2).map(i => i.replace(/\"/g, ""))));
34
- globalThis.$argument = {};
35
- Object.keys(argument).forEach(key => _.set(globalThis.$argument, key, argument[key]));
36
- break;
37
- }
38
- case "object": {
39
- if (globalThis.$argument === null) {
40
- globalThis.$argument = {};
41
- break;
42
- }
43
- const argument = {};
44
- Object.keys(globalThis.$argument).forEach(key => _.set(argument, key, globalThis.$argument[key]));
45
- globalThis.$argument = argument;
46
- break;
47
- }
48
- case "undefined":
49
- globalThis.$argument = {};
50
- break;
51
- }
31
+ globalThis.$argument = qs.parse(globalThis.$argument);
52
32
  if (globalThis.$argument.LogLevel) Console.logLevel = globalThis.$argument.LogLevel;
53
33
  Console.debug("✅ $argument", `$argument: ${JSON.stringify(globalThis.$argument)}`);
54
34
  })();
package/package.json CHANGED
@@ -42,5 +42,5 @@
42
42
  "registry": "https://registry.npmjs.org/",
43
43
  "access": "public"
44
44
  },
45
- "version": "2.3.0"
45
+ "version": "2.4.0"
46
46
  }
package/polyfill/KV.mjs CHANGED
@@ -75,7 +75,7 @@ export class KV {
75
75
  default:
76
76
  switch ($app) {
77
77
  case "Worker":
78
- keyValue = await this.#getNamespace().get(keyName);
78
+ keyValue = await this.namespace.get(keyName);
79
79
  break;
80
80
  default:
81
81
  keyValue = Storage.getItem(keyName, defaultValue);
@@ -111,7 +111,7 @@ export class KV {
111
111
  default:
112
112
  switch ($app) {
113
113
  case "Worker":
114
- await this.#getNamespace().put(keyName, keyValue);
114
+ await this.namespace.put(keyName, keyValue);
115
115
  result = true;
116
116
  break;
117
117
  default:
@@ -145,7 +145,7 @@ export class KV {
145
145
  default:
146
146
  switch ($app) {
147
147
  case "Worker":
148
- await this.#getNamespace().delete(keyName);
148
+ await this.namespace.delete(keyName);
149
149
  result = true;
150
150
  break;
151
151
  default:
@@ -176,35 +176,13 @@ export class KV {
176
176
  */
177
177
  async list(options = {}) {
178
178
  switch ($app) {
179
- case "Worker": {
180
- const namespace = this.#getNamespace();
181
- if (typeof namespace.list !== "function") throw new TypeError("KV namespace binding with list() is required in Worker runtime.");
182
- return await namespace.list(options);
183
- }
179
+ case "Worker":
180
+ return await this.namespace.list(options);
184
181
  default:
185
182
  throw new TypeError("KV.list() is only supported in Worker runtime.");
186
183
  }
187
184
  }
188
185
 
189
- /**
190
- * 解析 Worker 所需的 namespace 绑定。
191
- * Resolve the namespace binding required by Workers.
192
- *
193
- * @private
194
- * @returns {{ get(key: string): Promise<string|null>; put(key: string, value: string): Promise<void>; delete(key: string): Promise<void>; list?(options?: { prefix?: string; limit?: number; cursor?: string }): Promise<{ keys: { name: string; expiration?: number; metadata?: object }[]; list_complete: boolean; cursor: string }> }}
195
- */
196
- #getNamespace() {
197
- if (
198
- !this.namespace ||
199
- typeof this.namespace.get !== "function" ||
200
- typeof this.namespace.put !== "function" ||
201
- typeof this.namespace.delete !== "function"
202
- ) {
203
- throw new TypeError("KV namespace binding is required in Worker runtime.");
204
- }
205
- return this.namespace;
206
- }
207
-
208
186
  /**
209
187
  * 尝试将字符串反序列化为原始值。
210
188
  * Try to deserialize a string into its original value.
package/polyfill/index.js CHANGED
@@ -2,5 +2,6 @@ export * from "./Console.mjs";
2
2
  export * from "./fetch.mjs";
3
3
  export * from "./KV.mjs";
4
4
  export * from "./Lodash.mjs";
5
+ export * from "./qs.mjs";
5
6
  export * from "./StatusTexts.mjs";
6
7
  export * from "./Storage.mjs";
@@ -0,0 +1,142 @@
1
+ import { Lodash as _ } from "./Lodash.mjs";
2
+
3
+ /* https://github.com/ljharb/qs */
4
+ /**
5
+ * 轻量 `qs` 查询字符串工具。
6
+ * Lightweight `qs` query-string utilities.
7
+ *
8
+ * 说明:
9
+ * Notes:
10
+ * - 参考 `qs` 的 `parse` / `stringify` 接口设计
11
+ * - Modeled after the `qs` `parse` / `stringify` API
12
+ * - `parse` 保持当前项目原有 `$argument` 字符串解析语义
13
+ * - `parse` preserves the existing `$argument` string parsing semantics
14
+ * - `stringify` 基于项目内 `Lodash` 路径能力展开对象
15
+ * - `stringify` expands objects via the in-project `Lodash` path helpers
16
+ *
17
+ * 参考:
18
+ * Reference:
19
+ * - https://github.com/ljharb/qs
20
+ * - https://www.npmjs.com/package/qs
21
+ */
22
+ export class qs {
23
+ /**
24
+ * 将查询字符串解析为对象。
25
+ * Parse a query string into an object.
26
+ *
27
+ * @param {string | Record<string, unknown> | null | undefined} [query=""] 查询字符串或对象 / Query string or object.
28
+ * @returns {Record<string, unknown>}
29
+ */
30
+ static parse(query) {
31
+ let result = {};
32
+ switch (typeof query) {
33
+ case "string": {
34
+ const obj = Object.fromEntries(query.split("&").map(item => item.split("=", 2).map(i => i.replace(/\"/g, ""))));
35
+ Object.keys(obj).forEach(key => _.set(result, key, obj[key]));
36
+ break;
37
+ }
38
+ case "object": {
39
+ switch (query) {
40
+ case null:
41
+ break;
42
+ default: {
43
+ const obj = {};
44
+ Object.keys(query).forEach(key => _.set(obj, key, query[key]));
45
+ result = obj;
46
+ break;
47
+ }
48
+ }
49
+ break;
50
+ }
51
+ case "undefined":
52
+ result = {};
53
+ break;
54
+ }
55
+ return result;
56
+ }
57
+
58
+ /**
59
+ * 将对象序列化为查询字符串。
60
+ * Serialize an object into a query string.
61
+ *
62
+ * @param {Record<string, unknown>} [object={}] 输入对象 / Input object.
63
+ * @returns {string}
64
+ */
65
+ static stringify(object = {}) {
66
+ if (!object || typeof object !== "object") return "";
67
+
68
+ const entries = [];
69
+ Object.keys(object).forEach(key => qs.#collect(object, key, entries));
70
+
71
+ if (entries.length === 0) return "";
72
+ return entries
73
+ .map(([key, value]) => `${qs.#encode(qs.#formatPath(key))}=${qs.#encode(value)}`)
74
+ .join("&");
75
+ }
76
+
77
+ /**
78
+ * 收集待序列化的键值对。
79
+ * Collect key-value pairs for stringification.
80
+ *
81
+ * @param {Record<string, unknown>} object 输入对象 / Input object.
82
+ * @param {string} path 当前路径 / Current path.
83
+ * @param {[string, string][]} entries 输出数组 / Output entries.
84
+ * @returns {void}
85
+ */
86
+ static #collect(object, path, entries) {
87
+ const value = _.get(object, path);
88
+ if (value === undefined) return;
89
+ if (value === null) {
90
+ entries.push([path, ""]);
91
+ return;
92
+ }
93
+ if (Array.isArray(value)) {
94
+ value.forEach((item, index) => {
95
+ if (item === undefined) return;
96
+ qs.#collect(object, `${path}[${index}]`, entries);
97
+ });
98
+ return;
99
+ }
100
+ if (qs.#isPlainObject(value)) {
101
+ Object.keys(value).forEach(key => qs.#collect(object, `${path}.${key}`, entries));
102
+ return;
103
+ }
104
+ entries.push([path, String(value)]);
105
+ }
106
+
107
+ /**
108
+ * 使用 `Lodash.toPath` 规范化输出路径。
109
+ * Normalize output path via `Lodash.toPath`.
110
+ *
111
+ * @param {string} path 原始路径 / Raw path.
112
+ * @returns {string}
113
+ */
114
+ static #formatPath(path) {
115
+ const [head, ...tail] = _.toPath(path);
116
+ return tail.reduce((result, segment) => (/^\d+$/.test(segment) ? `${result}[${segment}]` : `${result}.${segment}`), head);
117
+ }
118
+
119
+ /**
120
+ * 判断值是否为普通对象。
121
+ * Check whether a value is a plain object.
122
+ *
123
+ * @param {unknown} value 输入值 / Input value.
124
+ * @returns {boolean}
125
+ */
126
+ static #isPlainObject(value) {
127
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
128
+ const proto = Object.getPrototypeOf(value);
129
+ return proto === null || proto === Object.prototype;
130
+ }
131
+
132
+ /**
133
+ * 编码查询字符串片段。
134
+ * Encode a query-string fragment.
135
+ *
136
+ * @param {string} value 原始值 / Raw value.
137
+ * @returns {string}
138
+ */
139
+ static #encode(value) {
140
+ return encodeURIComponent(value);
141
+ }
142
+ }
@@ -30,5 +30,11 @@ declare module "@nsnanocat/util" {
30
30
  static unset(object?: Record<string, unknown>, path?: string | string[]): boolean;
31
31
  }
32
32
 
33
+ export class qs {
34
+ static parse(query?: string | Record<string, unknown> | null): Record<string, unknown>;
35
+
36
+ static stringify(object?: Record<string, unknown>): string;
37
+ }
38
+
33
39
  export const StatusTexts: Record<number, string>;
34
40
  }