@nsnanocat/util 2.6.2 → 2.6.4

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
@@ -90,6 +90,7 @@ import {
90
90
  - `lib/environment.mjs`
91
91
  - `lib/runScript.mjs`
92
92
  - `getStorage.mjs`(薯条项目自用,仅当你的存储结构与薯条项目一致时再使用;请通过子路径 `@nsnanocat/util/getStorage.mjs` 导入)
93
+ - `polyfill/Storage.cjs`(Node.js / Worker CJS 入口;请通过子路径 `@nsnanocat/util/polyfill/Storage` 导入)
93
94
 
94
95
  ## 模块依赖关系
95
96
 
@@ -108,7 +109,7 @@ import {
108
109
  | `getStorage.mjs` | `lib/argument.mjs`, `polyfill/Console.mjs`, `polyfill/Lodash.mjs`, `polyfill/Storage.mjs` | `Console.debug`, `Console.logLevel`, `Lodash.merge`, `Storage.getItem` | 先标准化 `$argument`,再合并默认配置/持久化配置/运行参数 |
109
110
  | `polyfill/Console.mjs` | `lib/app.mjs` | `$app` | 日志在 Worker / Node.js 与 iOS 脚本环境使用不同错误输出策略 |
110
111
  | `polyfill/fetch.mjs` | `lib/app.mjs`, `polyfill/Lodash.mjs`, `polyfill/StatusTexts.mjs`, `polyfill/Console.mjs` | `$app`, `Lodash.set`, `StatusTexts`(`Console` 当前版本未实际调用) | 按平台选请求引擎并做参数映射、响应结构统一 |
111
- | `polyfill/Storage.mjs` | `lib/app.mjs`, `polyfill/Lodash.mjs` | `$app`, `Lodash.get`, `Lodash.set`, `Lodash.unset` | 按平台选持久化后端并支持 `@key.path` 读写 |
112
+ | `polyfill/Storage.mjs` | `lib/app.mjs`, `polyfill/Lodash.mjs` | `$app`, `Lodash.get`, `Lodash.set`, `Lodash.unset` | ESM 路径下按平台选持久化后端并支持 `@key.path` 读写(Node.js 请走 `Storage.cjs`) |
112
113
  | `polyfill/Lodash.mjs` | 无 | 无 | 提供路径/合并等基础能力,被多个模块复用 |
113
114
  | `polyfill/qs.mjs` | `polyfill/Lodash.mjs` | `Lodash.get`, `Lodash.set`, `Lodash.toPath` | 提供查询字符串与对象之间的解析/序列化能力 |
114
115
  | `polyfill/StatusTexts.mjs` | 无 | 无 | 提供 HTTP 状态文案,供 `fetch/done` 使用 |
@@ -369,7 +370,11 @@ const store = getStorage("@my_box", ["YouTube", "Global"], database);
369
370
 
370
371
  ### `polyfill/fetch.mjs`
371
372
 
372
- `fetch` 是仿照 Web API `Window.fetch` 设计的跨平台适配实现:
373
+ `fetch` 现已拆分为 ESM / CJS 两条运行路径:
374
+ - `polyfill/fetch.mjs`:仅用于 iOS 脚本平台(Quantumult X / Loon / Surge / Stash / Egern / Shadowrocket)
375
+ - `polyfill/fetch.cjs`:用于 Worker / Node.js
376
+
377
+ `polyfill/fetch.mjs` 仍仿照 Web API `Window.fetch` 设计:
373
378
  - 参考文档:https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch
374
379
  - 中文文档:https://developer.mozilla.org/zh-CN/docs/Web/API/Window/fetch
375
380
  - 目标:尽量保持 Web `fetch` 调用习惯,同时补齐各平台扩展参数映射
@@ -394,7 +399,7 @@ const store = getStorage("@my_box", ["YouTube", "Global"], database);
394
399
  - `timeout`
395
400
  - `policy`
396
401
  - `redirection` / `auto-redirect`
397
- - `auto-cookie`(Worker / Node.js 共享分支识别;默认启用,传入 `false` / `0` / `-1` 可关闭)
402
+ - `auto-cookie`(仅 CJS 的 Worker / Node.js 分支识别;默认启用,传入 `false` / `0` / `-1` 可关闭)
398
403
 
399
404
  说明:下表是各 App 原生 HTTP 接口的差异补充,以及本库 `fetch` 的内部映射方式。调用方使用统一入参即可。
400
405
 
@@ -408,8 +413,6 @@ const store = getStorage("@my_box", ["YouTube", "Global"], database);
408
413
  | Egern | `$httpClient[method]` | 秒 | 无专门映射 | `auto-redirect` | 同上 |
409
414
  | Shadowrocket | `$httpClient[method]` | 秒 | `headers.X-Surge-Proxy` | `auto-redirect` | 同上 |
410
415
  | Quantumult X | `$task.fetch` | 毫秒(内部乘 1000) | `opts.policy` | `opts.redirection` | `body(ArrayBuffer/TypedArray)` 转 `bodyBytes`;响应按 `Content-Type` 恢复到 `body` |
411
- | Worker | `globalThis.fetch`(不存在时回退 `node-fetch`);共享 `auto-cookie` 处理 | 毫秒(内部乘 1000) | 无 | `redirect: follow/manual` | 返回 `body`(UTF-8 string) + `bodyBytes`(ArrayBuffer) |
412
- | Node.js | `globalThis.fetch`(不存在时回退 `node-fetch`);默认按需包裹 `fetch-cookie` | 毫秒(内部乘 1000) | 无 | `redirect: follow/manual` | 返回 `body`(UTF-8 string) + `bodyBytes`(ArrayBuffer) |
413
416
 
414
417
  返回对象(统一后)常见字段:
415
418
  - `ok`
@@ -421,15 +424,22 @@ const store = getStorage("@my_box", ["YouTube", "Global"], database);
421
424
  - `bodyBytes`
422
425
 
423
426
  不可用/差异点:
424
- - `policy` 在 Surge / Egern / Worker / Node.js 分支没有额外适配逻辑。
427
+ - `policy` 在 Surge / Egern 分支没有额外适配逻辑。
425
428
  - `redirection` 在部分平台会映射为 `auto-redirect` 或 `opts.redirection`。
426
- - Worker / Node.js 共享基于 `fetch` 的请求分支;若 `globalThis.fetch` 不存在则回退到 `node-fetch`,并在 `auto-cookie` 未关闭时按需包裹 `fetch-cookie`。
427
429
  - 传入 `timeout` 时,`5` 和 `5000` 都会被接受;库会先将用户输入归一化,再按平台要求转换为秒或毫秒。
428
430
  - 返回结构是统一兼容结构,不等同于浏览器 `Response` 对象。
429
431
 
432
+ Worker / Node.js 使用说明:
433
+ - 请通过 CJS 入口调用:`require("@nsnanocat/util").fetch` 或 `require("@nsnanocat/util/polyfill/fetch").fetch`
434
+ - CJS 分支会处理 `auto-cookie`,并将响应归一化为 `ok/status/statusText/body/bodyBytes`
435
+
430
436
  ### `polyfill/Storage.mjs`
431
437
 
432
- `Storage` 是仿照 Web Storage 接口(`Storage`)设计的跨平台持久化适配器:
438
+ `Storage` 已拆分为 ESM / CJS 两条运行路径:
439
+ - `polyfill/Storage.mjs`:用于 iOS 脚本平台 + Worker
440
+ - `polyfill/Storage.cjs`:用于 Worker / Node.js(含 `box.dat` 文件读写)
441
+
442
+ `polyfill/Storage.mjs` 仍仿照 Web Storage 接口(`Storage`)设计:
433
443
  - 参考文档:https://developer.mozilla.org/en-US/docs/Web/API/Storage
434
444
  - 中文文档:https://developer.mozilla.org/zh-CN/docs/Web/API/Storage
435
445
  - 目标:统一 VPN App 脚本环境中的持久化读写接口,并尽量贴近 Web Storage 行为
@@ -447,23 +457,31 @@ const store = getStorage("@my_box", ["YouTube", "Global"], database);
447
457
  - Quantumult X:可用(`$prefs.removeValueForKey`)。
448
458
  - Surge:通过 `$persistentStore.write(null, keyName)` 删除。
449
459
  - Worker:可用(仅删除内存缓存中的对应 key,不持久化)。
450
- - Node.js:可用(删除 `box.dat` 中对应 key 并落盘)。
460
+ - Node.js:ESM 路径不支持,会提示改用 CJS 入口。
451
461
  - Loon / Stash / Egern / Shadowrocket:返回 `false`。
452
462
 
453
463
  #### `Storage.clear()`
454
464
  - Quantumult X:可用(`$prefs.removeAllValues`)。
455
465
  - Worker:可用(仅清空内存缓存,不持久化)。
456
- - Node.js:可用(清空 `box.dat` 并落盘)。
466
+ - Node.js:ESM 路径不支持,会提示改用 CJS 入口。
457
467
  - 其他平台:返回 `false`。
458
468
 
459
- #### Worker / Node.js 特性
469
+ #### Worker 特性(ESM)
460
470
  - Worker:使用进程内内存缓存,不写文件。
471
+
472
+ #### Node.js 特性(CJS)
461
473
  - 数据文件默认:`box.dat`。
462
474
  - 读取路径优先级:当前目录 -> `process.cwd()`。
463
475
 
476
+ Node.js 使用说明:
477
+ - 请通过 CJS 入口调用:`require("@nsnanocat/util").Storage` 或 `require("@nsnanocat/util/polyfill/Storage").Storage`
478
+ - CJS 的 `Storage` 分支支持 `box.dat` 读写与落盘
479
+
464
480
  与 Web Storage 的行为差异:
465
481
  - 支持 `@key.path` 深路径读写(Web Storage 原生不支持)。
466
- - `removeItem/clear` 仅部分平台可用(目前为 Quantumult X、Worker、Node.js,以及 Surge 的 `removeItem`)。
482
+ - `removeItem/clear` 仅部分平台可用:
483
+ - ESM 路径:Quantumult X、Worker,以及 Surge 的 `removeItem`
484
+ - CJS 路径:Worker、Node.js
467
485
  - `getItem` 会尝试 `JSON.parse`,`setItem` 写入对象会 `JSON.stringify`。
468
486
 
469
487
  平台后端映射:
@@ -472,8 +490,8 @@ const store = getStorage("@my_box", ["YouTube", "Global"], database);
472
490
  | --- | --- |
473
491
  | Surge / Loon / Stash / Egern / Shadowrocket | `$persistentStore.read/write` |
474
492
  | Quantumult X | `$prefs.valueForKey/setValueForKey` |
475
- | Worker | 进程内内存缓存 |
476
- | Node.js | 本地 `box.dat` |
493
+ | Worker(ESM/CJS) | 进程内内存缓存 |
494
+ | Node.js(仅 CJS) | 本地 `box.dat` |
477
495
 
478
496
  ### `polyfill/Console.mjs`
479
497
 
package/getStorage.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import "./lib/argument.mjs";
2
2
  import { Console } from "./polyfill/Console.mjs";
3
3
  import { Lodash as _ } from "./polyfill/Lodash.mjs";
4
- import { Storage } from "./polyfill/Storage.js";
4
+ import { Storage } from "./polyfill/Storage.mjs";
5
5
 
6
6
  /**
7
7
  * 存储配置读取与合并结果。
package/index.cjs ADDED
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+
3
+ const { fetch } = require("./polyfill/fetch.cjs");
4
+ const { Storage } = require("./polyfill/Storage.cjs");
5
+
6
+ module.exports = {
7
+ fetch,
8
+ Storage,
9
+ };
package/index.js CHANGED
@@ -5,11 +5,11 @@ export * from "./lib/notification.mjs";
5
5
  export * from "./lib/time.mjs";
6
6
  export * from "./lib/wait.mjs";
7
7
  export * from "./polyfill/Console.mjs";
8
- export * from "./polyfill/fetch.js";
8
+ export * from "./polyfill/fetch.mjs";
9
9
  export * from "./polyfill/Lodash.mjs";
10
10
  export * from "./polyfill/qs.mjs";
11
11
  export * from "./polyfill/StatusTexts.mjs";
12
- export * from "./polyfill/Storage.js";
12
+ export * from "./polyfill/Storage.mjs";
13
13
 
14
14
  /**
15
15
  * 已标准化的 `$argument` 快照。
package/package.json CHANGED
@@ -14,10 +14,27 @@
14
14
  "license": "Apache-2.0",
15
15
  "bugs": "https://github.com/NSNanoCat/util/issues",
16
16
  "main": "index.js",
17
+ "exports": {
18
+ ".": {
19
+ "import": "./index.js",
20
+ "require": "./index.cjs"
21
+ },
22
+ "./polyfill/fetch": {
23
+ "import": "./polyfill/fetch.mjs",
24
+ "require": "./polyfill/fetch.cjs"
25
+ },
26
+ "./polyfill/Storage": {
27
+ "import": "./polyfill/Storage.mjs",
28
+ "require": "./polyfill/Storage.cjs"
29
+ },
30
+ "./*": "./*"
31
+ },
17
32
  "types": "types/nsnanocat-util.d.ts",
18
33
  "scripts": {
19
34
  "tsc:build": "npx tsc",
20
35
  "test": "node --test test/*.test.js",
36
+ "test:fetch": "node --test test/fetch.test.js",
37
+ "test:fetch:cjs": "node --test test/fetch.test.cjs",
21
38
  "test:merge": "node --test test/Lodash.merge.test.js",
22
39
  "test:argument": "node --test test/argument.test.js",
23
40
  "deprecate": "npm deprecate -f '@nsnanocat/util@0.0.0-preview' \"this package has been deprecated\""
@@ -27,12 +44,17 @@
27
44
  "url": "git+https://github.com/NSNanoCat/util.git"
28
45
  },
29
46
  "files": [
47
+ "index.cjs",
30
48
  "index.js",
31
49
  "lib",
32
50
  "polyfill",
33
51
  "getStorage.mjs",
34
52
  "types"
35
53
  ],
54
+ "dependencies": {
55
+ "fetch-cookie": "^3.2.0",
56
+ "node-fetch": "^3.3.2"
57
+ },
36
58
  "devDependencies": {
37
59
  "@biomejs/biome": "2.4.6",
38
60
  "typescript": "^5.9.3"
@@ -41,5 +63,5 @@
41
63
  "registry": "https://registry.npmjs.org/",
42
64
  "access": "public"
43
65
  },
44
- "version": "2.6.2"
66
+ "version": "2.6.4"
45
67
  }
@@ -0,0 +1,220 @@
1
+ "use strict";
2
+
3
+ const { Lodash: _ } = require("./Lodash.mjs");
4
+
5
+ /**
6
+ * 仅面向 Worker / Node.js 的持久化存储适配器(CJS 版本)。
7
+ * Persistent storage adapter for Worker / Node.js only (CJS version).
8
+ */
9
+ class Storage {
10
+ /**
11
+ * Worker / Node.js 环境下的内存数据缓存。
12
+ * In-memory data cache for Worker / Node.js runtime.
13
+ *
14
+ * @type {Record<string, any>|null}
15
+ */
16
+ static data = null;
17
+
18
+ /**
19
+ * Node.js 持久化文件名。
20
+ * Data file name used in Node.js.
21
+ *
22
+ * @type {string}
23
+ */
24
+ static dataFile = "box.dat";
25
+
26
+ /**
27
+ * `@key.path` 解析正则。
28
+ * Regex for `@key.path` parsing.
29
+ *
30
+ * @type {RegExp}
31
+ */
32
+ static #nameRegex = /^@(?<key>[^.]+)(?:\.(?<path>.*))?$/;
33
+
34
+ /**
35
+ * 读取存储值。
36
+ * Read value from persistent storage.
37
+ *
38
+ * @param {string} keyName 键名或路径键 / Key or path key.
39
+ * @param {*} [defaultValue=null] 默认值 / Default value when key is missing.
40
+ * @returns {*}
41
+ */
42
+ static getItem(keyName, defaultValue = null) {
43
+ let keyValue = defaultValue;
44
+ switch (keyName.startsWith("@")) {
45
+ case true: {
46
+ const { key, path } = keyName.match(Storage.#nameRegex)?.groups;
47
+ keyName = key;
48
+ let value = Storage.getItem(keyName, {});
49
+ if (typeof value !== "object") value = {};
50
+ keyValue = _.get(value, path);
51
+ try {
52
+ keyValue = JSON.parse(keyValue);
53
+ } catch {}
54
+ break;
55
+ }
56
+ default:
57
+ if (typeof process !== "undefined" && process.versions?.node) {
58
+ Storage.data = Storage.#loaddata(Storage.dataFile);
59
+ keyValue = Storage.data?.[keyName];
60
+ } else {
61
+ Storage.data = Storage.data ?? {};
62
+ keyValue = Storage.data[keyName];
63
+ }
64
+ try {
65
+ keyValue = JSON.parse(keyValue);
66
+ } catch {}
67
+ break;
68
+ }
69
+ return keyValue ?? defaultValue;
70
+ }
71
+
72
+ /**
73
+ * 写入存储值。
74
+ * Write value into persistent storage.
75
+ *
76
+ * @param {string} keyName 键名或路径键 / Key or path key.
77
+ * @param {*} keyValue 写入值 / Value to store.
78
+ * @returns {boolean}
79
+ */
80
+ static setItem(keyName = new String(), keyValue = new String()) {
81
+ let result = false;
82
+ switch (typeof keyValue) {
83
+ case "object":
84
+ keyValue = JSON.stringify(keyValue);
85
+ break;
86
+ default:
87
+ keyValue = String(keyValue);
88
+ break;
89
+ }
90
+ switch (keyName.startsWith("@")) {
91
+ case true: {
92
+ const { key, path } = keyName.match(Storage.#nameRegex)?.groups;
93
+ keyName = key;
94
+ let value = Storage.getItem(keyName, {});
95
+ if (typeof value !== "object") value = {};
96
+ _.set(value, path, keyValue);
97
+ result = Storage.setItem(keyName, value);
98
+ break;
99
+ }
100
+ default:
101
+ if (typeof process !== "undefined" && process.versions?.node) {
102
+ Storage.data = Storage.#loaddata(Storage.dataFile);
103
+ Storage.data[keyName] = keyValue;
104
+ Storage.#writedata(Storage.dataFile);
105
+ result = true;
106
+ } else {
107
+ Storage.data = Storage.data ?? {};
108
+ Storage.data[keyName] = keyValue;
109
+ result = true;
110
+ }
111
+ break;
112
+ }
113
+ return result;
114
+ }
115
+
116
+ /**
117
+ * 删除存储值。
118
+ * Remove value from persistent storage.
119
+ *
120
+ * @param {string} keyName 键名或路径键 / Key or path key.
121
+ * @returns {boolean}
122
+ */
123
+ static removeItem(keyName) {
124
+ let result = false;
125
+ switch (keyName.startsWith("@")) {
126
+ case true: {
127
+ const { key, path } = keyName.match(Storage.#nameRegex)?.groups;
128
+ keyName = key;
129
+ let value = Storage.getItem(keyName);
130
+ if (typeof value !== "object") value = {};
131
+ _.unset(value, path);
132
+ result = Storage.setItem(keyName, value);
133
+ break;
134
+ }
135
+ default:
136
+ if (typeof process !== "undefined" && process.versions?.node) {
137
+ Storage.data = Storage.#loaddata(Storage.dataFile);
138
+ delete Storage.data[keyName];
139
+ Storage.#writedata(Storage.dataFile);
140
+ result = true;
141
+ } else {
142
+ Storage.data = Storage.data ?? {};
143
+ delete Storage.data[keyName];
144
+ result = true;
145
+ }
146
+ break;
147
+ }
148
+ return result;
149
+ }
150
+
151
+ /**
152
+ * 清空存储。
153
+ * Clear storage.
154
+ *
155
+ * @returns {boolean}
156
+ */
157
+ static clear() {
158
+ if (typeof process !== "undefined" && process.versions?.node) {
159
+ Storage.data = Storage.#loaddata(Storage.dataFile);
160
+ Storage.data = {};
161
+ Storage.#writedata(Storage.dataFile);
162
+ return true;
163
+ }
164
+ Storage.data = {};
165
+ return true;
166
+ }
167
+
168
+ /**
169
+ * 从 Node.js 数据文件加载 JSON。
170
+ * Load JSON data from Node.js data file.
171
+ *
172
+ * @private
173
+ * @param {string} dataFile 数据文件名 / Data file name.
174
+ * @returns {Record<string, any>}
175
+ */
176
+ static #loaddata = dataFile => {
177
+ const fs = require("node:fs");
178
+ const path = require("node:path");
179
+ const curDirDataFilePath = path.resolve(dataFile);
180
+ const rootDirDataFilePath = path.resolve(process.cwd(), dataFile);
181
+ const isCurDirDataFile = fs.existsSync(curDirDataFilePath);
182
+ const isRootDirDataFile = !isCurDirDataFile && fs.existsSync(rootDirDataFilePath);
183
+ if (isCurDirDataFile || isRootDirDataFile) {
184
+ const datPath = isCurDirDataFile ? curDirDataFilePath : rootDirDataFilePath;
185
+ try {
186
+ return JSON.parse(fs.readFileSync(datPath));
187
+ } catch {
188
+ return {};
189
+ }
190
+ }
191
+ return {};
192
+ };
193
+
194
+ /**
195
+ * 将内存数据写入 Node.js 数据文件。
196
+ * Persist in-memory data to Node.js data file.
197
+ *
198
+ * @private
199
+ * @param {string} [dataFile=this.dataFile] 数据文件名 / Data file name.
200
+ * @returns {void}
201
+ */
202
+ static #writedata = (dataFile = this.dataFile) => {
203
+ const fs = require("node:fs");
204
+ const path = require("node:path");
205
+ const curDirDataFilePath = path.resolve(dataFile);
206
+ const rootDirDataFilePath = path.resolve(process.cwd(), dataFile);
207
+ const isCurDirDataFile = fs.existsSync(curDirDataFilePath);
208
+ const isRootDirDataFile = !isCurDirDataFile && fs.existsSync(rootDirDataFilePath);
209
+ const jsondata = JSON.stringify(this.data);
210
+ if (isCurDirDataFile) {
211
+ fs.writeFileSync(curDirDataFilePath, jsondata);
212
+ } else if (isRootDirDataFile) {
213
+ fs.writeFileSync(rootDirDataFilePath, jsondata);
214
+ } else {
215
+ fs.writeFileSync(curDirDataFilePath, jsondata);
216
+ }
217
+ };
218
+ }
219
+
220
+ module.exports = { Storage };
@@ -82,7 +82,7 @@ export class Storage {
82
82
  keyValue = _.get(value, path);
83
83
  try {
84
84
  keyValue = JSON.parse(keyValue);
85
- } catch (e) {}
85
+ } catch {}
86
86
  break;
87
87
  }
88
88
  default:
@@ -102,16 +102,14 @@ export class Storage {
102
102
  keyValue = Storage.data[keyName];
103
103
  break;
104
104
  case "Node.js":
105
- Storage.data = Storage.#loaddata(Storage.dataFile);
106
- keyValue = Storage.data?.[keyName];
107
- break;
105
+ throw new Error(`${Storage.name}.getItem: ESM 版本不支持 Node.js,请改用 CJS 入口`);
108
106
  default:
109
107
  keyValue = Storage.data?.[keyName] || null;
110
108
  break;
111
109
  }
112
110
  try {
113
111
  keyValue = JSON.parse(keyValue);
114
- } catch (e) {
112
+ } catch {
115
113
  // do nothing
116
114
  }
117
115
  break;
@@ -165,11 +163,7 @@ export class Storage {
165
163
  result = true;
166
164
  break;
167
165
  case "Node.js":
168
- Storage.data = Storage.#loaddata(Storage.dataFile);
169
- Storage.data[keyName] = keyValue;
170
- Storage.#writedata(Storage.dataFile);
171
- result = true;
172
- break;
166
+ throw new Error(`${Storage.name}.setItem: ESM 版本不支持 Node.js,请改用 CJS 入口`);
173
167
  default:
174
168
  result = Storage.data?.[keyName] || null;
175
169
  break;
@@ -200,7 +194,7 @@ export class Storage {
200
194
  keyName = key;
201
195
  let value = Storage.getItem(keyName);
202
196
  if (typeof value !== "object") value = {};
203
- keyValue = _.unset(value, path);
197
+ _.unset(value, path);
204
198
  result = Storage.setItem(keyName, value);
205
199
  break;
206
200
  }
@@ -224,12 +218,7 @@ export class Storage {
224
218
  result = true;
225
219
  break;
226
220
  case "Node.js":
227
- // result = false;
228
- Storage.data = Storage.#loaddata(Storage.dataFile);
229
- delete Storage.data[keyName];
230
- Storage.#writedata(Storage.dataFile);
231
- result = true;
232
- break;
221
+ throw new Error(`${Storage.name}.removeItem: ESM 版本不支持 Node.js,请改用 CJS 入口`);
233
222
  default:
234
223
  result = false;
235
224
  break;
@@ -263,12 +252,7 @@ export class Storage {
263
252
  result = true;
264
253
  break;
265
254
  case "Node.js":
266
- // result = false;
267
- Storage.data = Storage.#loaddata(Storage.dataFile);
268
- Storage.data = {};
269
- Storage.#writedata(Storage.dataFile);
270
- result = true;
271
- break;
255
+ throw new Error(`${Storage.name}.clear: ESM 版本不支持 Node.js,请改用 CJS 入口`);
272
256
  default:
273
257
  result = false;
274
258
  break;
@@ -276,57 +260,4 @@ export class Storage {
276
260
  return result;
277
261
  }
278
262
 
279
- /**
280
- * 从 Node.js 数据文件加载 JSON。
281
- * Load JSON data from Node.js data file.
282
- *
283
- * @private
284
- * @param {string} dataFile 数据文件名 / Data file name.
285
- * @returns {Record<string, any>}
286
- */
287
- static #loaddata = dataFile => {
288
- if ($app === "Node.js") {
289
- this.fs = this.fs ? this.fs : require("fs");
290
- this.path = this.path ? this.path : require("path");
291
- const curDirDataFilePath = this.path.resolve(dataFile);
292
- const rootDirDataFilePath = this.path.resolve(process.cwd(), dataFile);
293
- const isCurDirDataFile = this.fs.existsSync(curDirDataFilePath);
294
- const isRootDirDataFile = !isCurDirDataFile && this.fs.existsSync(rootDirDataFilePath);
295
- if (isCurDirDataFile || isRootDirDataFile) {
296
- const datPath = isCurDirDataFile ? curDirDataFilePath : rootDirDataFilePath;
297
- try {
298
- return JSON.parse(this.fs.readFileSync(datPath));
299
- } catch (e) {
300
- return {};
301
- }
302
- } else return {};
303
- } else return {};
304
- };
305
-
306
- /**
307
- * 将内存数据写入 Node.js 数据文件。
308
- * Persist in-memory data to Node.js data file.
309
- *
310
- * @private
311
- * @param {string} [dataFile=this.dataFile] 数据文件名 / Data file name.
312
- * @returns {void}
313
- */
314
- static #writedata = (dataFile = this.dataFile) => {
315
- if ($app === "Node.js") {
316
- this.fs = this.fs ? this.fs : require("fs");
317
- this.path = this.path ? this.path : require("path");
318
- const curDirDataFilePath = this.path.resolve(dataFile);
319
- const rootDirDataFilePath = this.path.resolve(process.cwd(), dataFile);
320
- const isCurDirDataFile = this.fs.existsSync(curDirDataFilePath);
321
- const isRootDirDataFile = !isCurDirDataFile && this.fs.existsSync(rootDirDataFilePath);
322
- const jsondata = JSON.stringify(this.data);
323
- if (isCurDirDataFile) {
324
- this.fs.writeFileSync(curDirDataFilePath, jsondata);
325
- } else if (isRootDirDataFile) {
326
- this.fs.writeFileSync(rootDirDataFilePath, jsondata);
327
- } else {
328
- this.fs.writeFileSync(curDirDataFilePath, jsondata);
329
- }
330
- }
331
- };
332
263
  }
@@ -0,0 +1,128 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * 统一请求参数。
5
+ * Unified request payload.
6
+ *
7
+ * @typedef {object} FetchRequest
8
+ * @property {string} url 请求地址 / Request URL.
9
+ * @property {string} [method] 请求方法 / HTTP method.
10
+ * @property {Record<string, any>} [headers] 请求头 / Request headers.
11
+ * @property {string|ArrayBuffer|ArrayBufferView|object} [body] 请求体 / Request body.
12
+ * @property {ArrayBuffer} [bodyBytes] 二进制请求体 / Binary request body.
13
+ * @property {number|string} [timeout] 超时(秒或毫秒)/ Timeout (seconds or milliseconds).
14
+ * @property {boolean} [redirection] 是否跟随重定向 / Whether to follow redirects.
15
+ * @property {boolean|number|string} ["auto-cookie"] Worker / Node.js Cookie 开关 / Worker / Node.js Cookie toggle.
16
+ */
17
+
18
+ /**
19
+ * 统一响应结构。
20
+ * Unified response payload.
21
+ *
22
+ * @typedef {object} FetchResponse
23
+ * @property {boolean} ok 请求是否成功 / Whether request is successful.
24
+ * @property {number} status 状态码 / HTTP status code.
25
+ * @property {number} [statusCode] 状态码别名 / Status code alias.
26
+ * @property {string} [statusText] 状态文本 / HTTP status text.
27
+ * @property {Record<string, any>} [headers] 响应头 / Response headers.
28
+ * @property {string|ArrayBuffer} [body] 响应体 / Response body.
29
+ * @property {ArrayBuffer} [bodyBytes] 二进制响应体 / Binary response body.
30
+ */
31
+
32
+ /**
33
+ * 仅面向 Worker / Node.js 的 `fetch` 适配层(CJS 版本)。
34
+ * `fetch` adapter for Worker / Node.js only (CJS version).
35
+ *
36
+ * @async
37
+ * @param {FetchRequest|string} resource 请求对象或 URL / Request object or URL string.
38
+ * @param {Partial<FetchRequest>} [options={}] 追加参数 / Extra options.
39
+ * @returns {Promise<FetchResponse>}
40
+ */
41
+ async function fetch(resource, options = {}) {
42
+ switch (typeof resource) {
43
+ case "object":
44
+ resource = { ...options, ...resource };
45
+ break;
46
+ case "string":
47
+ resource = { ...options, url: resource };
48
+ break;
49
+ case "undefined":
50
+ default:
51
+ throw new TypeError(`${Function.name}: 参数类型错误, resource 必须为对象或字符串`);
52
+ }
53
+
54
+ if (!resource.method) {
55
+ resource.method = "GET";
56
+ if (resource.body ?? resource.bodyBytes) resource.method = "POST";
57
+ }
58
+ delete resource.headers?.Host;
59
+ delete resource.headers?.[":authority"];
60
+ delete resource.headers?.["Content-Length"];
61
+ delete resource.headers?.["content-length"];
62
+
63
+ if (!resource.timeout) resource.timeout = 5;
64
+ if (resource.timeout) {
65
+ resource.timeout = Number.parseInt(resource.timeout, 10);
66
+ if (resource.timeout > 500) resource.timeout = Math.round(resource.timeout / 1000);
67
+ resource.timeout = resource.timeout * 1000;
68
+ }
69
+
70
+ if (!globalThis.fetch) {
71
+ throw new Error(`${Function.name}: 当前 Node.js 运行时缺少全局 fetch,请升级 Node.js 版本`);
72
+ }
73
+
74
+ switch (resource["auto-cookie"]) {
75
+ case undefined:
76
+ case "true":
77
+ case true:
78
+ case "1":
79
+ case 1:
80
+ default:
81
+ if (!globalThis.fetch?.cookieJar) globalThis.fetch = require("fetch-cookie").default(globalThis.fetch);
82
+ break;
83
+ case "false":
84
+ case false:
85
+ case "0":
86
+ case 0:
87
+ case "-1":
88
+ case -1:
89
+ break;
90
+ }
91
+
92
+ resource.redirect = resource.redirection ? "follow" : "manual";
93
+ const { url, ...fetchOptions } = resource;
94
+
95
+ return Promise.race([
96
+ globalThis
97
+ .fetch(url, fetchOptions)
98
+ .then(async response => {
99
+ const bodyBytes = await response.arrayBuffer();
100
+ let headers;
101
+ try {
102
+ headers = response.headers.raw();
103
+ } catch {
104
+ headers = Array.from(response.headers.entries()).reduce((acc, [key, value]) => {
105
+ acc[key] = acc[key] ? [...acc[key], value] : [value];
106
+ return acc;
107
+ }, {});
108
+ }
109
+ return {
110
+ ok: response.ok ?? /^2\d\d$/.test(response.status),
111
+ status: response.status,
112
+ statusCode: response.status,
113
+ statusText: response.statusText,
114
+ body: new TextDecoder("utf-8").decode(bodyBytes),
115
+ bodyBytes,
116
+ headers: Object.fromEntries(Object.entries(headers).map(([key, value]) => [key, key.toLowerCase() !== "set-cookie" ? value.toString() : value])),
117
+ };
118
+ })
119
+ .catch(error => Promise.reject(error.message)),
120
+ new Promise((resolve, reject) => {
121
+ setTimeout(() => {
122
+ reject(new Error(`${Function.name}: 请求超时, 请检查网络后重试`));
123
+ }, resource.timeout);
124
+ }),
125
+ ]);
126
+ }
127
+
128
+ module.exports = { fetch };
@@ -1,5 +1,4 @@
1
1
  import { $app } from "../lib/app.mjs";
2
- import { Console } from "./Console.mjs";
3
2
  import { Lodash as _ } from "./Lodash.mjs";
4
3
  import { StatusTexts } from "./StatusTexts.mjs";
5
4
 
@@ -17,7 +16,6 @@ import { StatusTexts } from "./StatusTexts.mjs";
17
16
  * @property {string} [policy] 指定策略 / Preferred policy.
18
17
  * @property {boolean} [redirection] 是否跟随重定向 / Whether to follow redirects.
19
18
  * @property {boolean} ["auto-redirect"] 平台重定向字段 / Platform redirect flag.
20
- * @property {boolean|number|string} ["auto-cookie"] Worker / Node.js Cookie 开关 / Worker / Node.js Cookie toggle.
21
19
  * @property {Record<string, any>} [opts] 平台扩展字段 / Platform extension fields.
22
20
  */
23
21
 
@@ -36,35 +34,8 @@ import { StatusTexts } from "./StatusTexts.mjs";
36
34
  */
37
35
 
38
36
  /**
39
- * 跨平台 `fetch` 适配层。
40
- * Cross-platform `fetch` adapter.
41
- *
42
- * 设计目标:
43
- * Design goal:
44
- * - 仿照 Web API `fetch`(`Window.fetch`)接口设计
45
- * - Modeled after Web API `fetch` (`Window.fetch`)
46
- * - 统一 VPN App、Worker 与 Node.js 环境中的请求调用
47
- * - Unify request calls across VPN apps, Worker, and Node.js
48
- *
49
- * 功能:
50
- * Features:
51
- * - 统一 Quantumult X / Loon / Surge / Stash / Egern / Shadowrocket / Worker / Node.js 请求接口
52
- * - Normalize request APIs across Quantumult X / Loon / Surge / Stash / Egern / Shadowrocket / Worker / Node.js
53
- * - 统一返回体字段(`ok/status/statusText/body/bodyBytes`)
54
- * - Normalize response fields (`ok/status/statusText/body/bodyBytes`)
55
- *
56
- * 与 Web `fetch` 的已知差异:
57
- * Known differences from Web `fetch`:
58
- * - 支持 `policy`、`auto-redirect` 等平台扩展字段
59
- * - Supports platform extension fields like `policy` and `auto-redirect`
60
- * - Worker / Node.js 共享基于 `fetch` 的请求分支
61
- * - Worker / Node.js share the `fetch`-based request branch
62
- * - `auto-cookie` 在 Worker / Node.js 共享分支中识别
63
- * - `auto-cookie` is recognized by the shared Worker / Node.js branch
64
- * - 非浏览器平台通过 `$httpClient/$task` 实现,不是原生 Fetch 实现
65
- * - Non-browser platforms use `$httpClient/$task` instead of native Fetch engine
66
- * - 返回结构包含 `statusCode/bodyBytes` 等兼容字段
67
- * - Response includes compatibility fields like `statusCode/bodyBytes`
37
+ * 仅面向 iOS 脚本平台的 `fetch` 适配层(ESM 版本)。
38
+ * `fetch` adapter for iOS script platforms only (ESM version).
68
39
  *
69
40
  * @link https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch
70
41
  * @link https://developer.mozilla.org/zh-CN/docs/Web/API/Window/fetch
@@ -74,8 +45,6 @@ import { StatusTexts } from "./StatusTexts.mjs";
74
45
  * @returns {Promise<FetchResponse>}
75
46
  */
76
47
  export async function fetch(resource, options = {}) {
77
- // 初始化参数。
78
- // Initialize request input.
79
48
  switch (typeof resource) {
80
49
  case "object":
81
50
  resource = { ...options, ...resource };
@@ -87,63 +56,39 @@ export async function fetch(resource, options = {}) {
87
56
  default:
88
57
  throw new TypeError(`${Function.name}: 参数类型错误, resource 必须为对象或字符串`);
89
58
  }
90
- // 自动判断请求方法。
91
- // Infer the HTTP method automatically.
59
+
92
60
  if (!resource.method) {
93
61
  resource.method = "GET";
94
62
  if (resource.body ?? resource.bodyBytes) resource.method = "POST";
95
63
  }
96
- // 移除需要由底层实现自动生成的请求头。
97
- // Remove headers that should be generated by the underlying runtime.
98
64
  delete resource.headers?.Host;
99
65
  delete resource.headers?.[":authority"];
100
66
  delete resource.headers?.["Content-Length"];
101
67
  delete resource.headers?.["content-length"];
102
- // 统一请求方法为小写,方便后续索引平台 API。
103
- // Normalize the method to lowercase for platform API lookups.
104
68
  const method = resource.method.toLocaleLowerCase();
105
- // 默认请求超时时间为 5 秒。
106
- // Default request timeout to 5 seconds.
69
+
107
70
  if (!resource.timeout) resource.timeout = 5;
108
- // 智能矫正请求超时时间,兼容用户输入的秒或毫秒。
109
- // Normalize timeout input so both seconds and milliseconds are accepted.
110
71
  if (resource.timeout) {
111
72
  resource.timeout = Number.parseInt(resource.timeout, 10);
112
- // 统一先转换为秒,大于 500 视为毫秒输入。
113
- // Convert to seconds first and treat values above 500 as milliseconds.
114
73
  if (resource.timeout > 500) resource.timeout = Math.round(resource.timeout / 1000);
115
74
  }
116
- // 某些平台要求毫秒级超时,进行二次换算。
117
- // Some platforms expect timeout in milliseconds, so convert again.
118
75
  if (resource.timeout) {
119
76
  switch ($app) {
120
77
  case "Loon":
121
78
  case "Quantumult X":
122
- case "Worker":
123
- case "Node.js":
124
- // 这些平台要求毫秒,因此把秒重新换算为毫秒。
125
- // These platforms expect milliseconds, so convert seconds back to milliseconds.
126
79
  resource.timeout = resource.timeout * 1000;
127
80
  break;
128
- case "Shadowrocket":
129
- case "Stash":
130
- case "Egern":
131
- case "Surge":
132
81
  default:
133
82
  break;
134
83
  }
135
84
  }
136
- // 根据当前平台选择请求实现。
137
- // Select the request engine for the current platform.
85
+
138
86
  switch ($app) {
139
87
  case "Loon":
140
88
  case "Surge":
141
89
  case "Stash":
142
90
  case "Egern":
143
91
  case "Shadowrocket":
144
- default:
145
- // 转换通用请求参数到 `$httpClient` 语义。
146
- // Map shared request fields to `$httpClient` semantics.
147
92
  if (resource.policy) {
148
93
  switch ($app) {
149
94
  case "Loon":
@@ -158,14 +103,10 @@ export async function fetch(resource, options = {}) {
158
103
  }
159
104
  }
160
105
  if (typeof resource.redirection === "boolean") resource["auto-redirect"] = resource.redirection;
161
- // 优先把 `bodyBytes` 映射回 `$httpClient` 能接受的 `body`。
162
- // Prefer mapping `bodyBytes` back to the `body` field expected by `$httpClient`.
163
106
  if (resource.bodyBytes && !resource.body) {
164
107
  resource.body = resource.bodyBytes;
165
108
  resource.bodyBytes = undefined;
166
109
  }
167
- // 根据 `Accept` 推断是否需要二进制响应体。
168
- // Infer whether the response should be treated as binary from `Accept`.
169
110
  switch ((resource.headers?.Accept || resource.headers?.accept)?.split(";")?.[0]) {
170
111
  case "application/protobuf":
171
112
  case "application/x-protobuf":
@@ -177,8 +118,6 @@ export async function fetch(resource, options = {}) {
177
118
  resource["binary-mode"] = true;
178
119
  break;
179
120
  }
180
- // 发送 `$httpClient` 请求并归一化返回结构。
181
- // Send the `$httpClient` request and normalize the response payload.
182
121
  return new Promise((resolve, reject) => {
183
122
  globalThis.$httpClient[method](resource, (error, response, body) => {
184
123
  if (error) reject(error);
@@ -195,12 +134,8 @@ export async function fetch(resource, options = {}) {
195
134
  });
196
135
  });
197
136
  case "Quantumult X":
198
- // 转换 Quantumult X 专有请求参数。
199
- // Map request fields to Quantumult X specific options.
200
137
  if (resource.policy) _.set(resource, "opts.policy", resource.policy);
201
138
  if (typeof resource["auto-redirect"] === "boolean") _.set(resource, "opts.redirection", resource["auto-redirect"]);
202
- // Quantumult X 使用 `bodyBytes` 传输二进制请求体。
203
- // Quantumult X uses `bodyBytes` for binary request payloads.
204
139
  if (resource.body instanceof ArrayBuffer) {
205
140
  resource.bodyBytes = resource.body;
206
141
  resource.body = undefined;
@@ -208,8 +143,6 @@ export async function fetch(resource, options = {}) {
208
143
  resource.bodyBytes = resource.body.buffer.slice(resource.body.byteOffset, resource.body.byteLength + resource.body.byteOffset);
209
144
  resource.body = undefined;
210
145
  } else if (resource.body) resource.bodyBytes = undefined;
211
- // 发送请求,并用 `Promise.race` 提供统一超时保护。
212
- // Send the request and enforce timeout with `Promise.race`.
213
146
  return Promise.race([
214
147
  globalThis.$task.fetch(resource).then(
215
148
  response => {
@@ -242,66 +175,9 @@ export async function fetch(resource, options = {}) {
242
175
  }),
243
176
  ]);
244
177
  case "Worker":
245
- case "Node.js": {
246
- // Worker 复用宿主 `fetch`;Node.js 优先复用原生 `fetch`,缺失时再回退到 `node-fetch`。
247
- // Worker reuses host `fetch`; Node.js reuses native `fetch` first and falls back to `node-fetch`.
248
- if (!globalThis.fetch) globalThis.fetch = require("node-fetch");
249
- switch (resource["auto-cookie"]) {
250
- case undefined:
251
- case "true":
252
- case true:
253
- case "1":
254
- case 1:
255
- default:
256
- // 仅在尚未包裹 CookieJar 时注入 `fetch-cookie`,避免重复包装。
257
- // Inject `fetch-cookie` only once when a cookie jar is not already attached.
258
- if (!globalThis.fetch?.cookieJar) globalThis.fetch = require("fetch-cookie").default(globalThis.fetch);
259
- break;
260
- case "false":
261
- case false:
262
- case "0":
263
- case 0:
264
- case "-1":
265
- case -1:
266
- break;
267
- }
268
- // 将通用字段映射到 Worker / Node.js Fetch 语义。
269
- // Map shared fields to Worker / Node.js Fetch semantics.
270
- resource.redirect = resource.redirection ? "follow" : "manual";
271
- const { url, ...options } = resource;
272
- // 发起请求并归一化响应头、文本与二进制响应体。
273
- // Send the request and normalize headers, text, and binary response data.
274
- return Promise.race([
275
- globalThis
276
- .fetch(url, options)
277
- .then(async response => {
278
- const bodyBytes = await response.arrayBuffer();
279
- let headers;
280
- try {
281
- headers = response.headers.raw();
282
- } catch {
283
- headers = Array.from(response.headers.entries()).reduce((acc, [key, value]) => {
284
- acc[key] = acc[key] ? [...acc[key], value] : [value];
285
- return acc;
286
- }, {});
287
- }
288
- return {
289
- ok: response.ok ?? /^2\d\d$/.test(response.status),
290
- status: response.status,
291
- statusCode: response.status,
292
- statusText: response.statusText,
293
- body: new TextDecoder("utf-8").decode(bodyBytes),
294
- bodyBytes: bodyBytes,
295
- headers: Object.fromEntries(Object.entries(headers).map(([key, value]) => [key, key.toLowerCase() !== "set-cookie" ? value.toString() : value])),
296
- };
297
- })
298
- .catch(error => Promise.reject(error.message)),
299
- new Promise((resolve, reject) => {
300
- setTimeout(() => {
301
- reject(new Error(`${Function.name}: 请求超时, 请检查网络后重试`));
302
- }, resource.timeout);
303
- }),
304
- ]);
305
- }
178
+ case "Node.js":
179
+ throw new Error(`${Function.name}: ESM 版本不支持 Worker/Node.js,请改用 CJS 入口`);
180
+ default:
181
+ throw new Error(`${Function.name}: 当前平台不支持`);
306
182
  }
307
183
  }
@@ -1,4 +1,4 @@
1
- import { fetch as fetchRuntime } from "./fetch.js";
1
+ import { fetch as fetchRuntime } from "./fetch.mjs";
2
2
  import type { Fetch } from "./fetch.d.ts";
3
3
 
4
4
  export type { Fetch, FetchRequest, FetchResponse } from "./fetch.d.ts";
@@ -3,9 +3,9 @@
3
3
  * Aggregated exports for polyfill modules.
4
4
  */
5
5
  export { Console } from "./Console.mjs";
6
- export { fetch } from "./fetch.js";
7
- export type { Fetch, FetchRequest, FetchResponse } from "./fetch.mjs";
6
+ export { fetch } from "./fetch.mjs";
7
+ export type { Fetch, FetchRequest, FetchResponse } from "./fetch.d.ts";
8
8
  export { Lodash } from "./Lodash.mjs";
9
9
  export { qs } from "./qs.mjs";
10
10
  export { StatusTexts } from "./StatusTexts.mjs";
11
- export { Storage } from "./Storage.js";
11
+ export { Storage } from "./Storage.mjs";
package/polyfill/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export * from "./Console.mjs";
2
- export * from "./fetch.js";
2
+ export * from "./fetch.mjs";
3
3
  export * from "./Lodash.mjs";
4
4
  export * from "./qs.mjs";
5
5
  export * from "./StatusTexts.mjs";
6
- export * from "./Storage.js";
6
+ export * from "./Storage.mjs";