@nsnanocat/util 2.1.7 → 2.2.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
@@ -116,7 +116,7 @@ import {
116
116
  ### `lib/app.mjs` 与 `lib/environment.mjs`(平台识别与环境)
117
117
 
118
118
  #### `$app`
119
- - 类型:`"Quantumult X" | "Loon" | "Shadowrocket" | "Node.js" | "Egern" | "Surge" | "Stash" | undefined`
119
+ - 类型:`"Quantumult X" | "Loon" | "Shadowrocket" | "Egern" | "Surge" | "Stash" | "Node.js" | undefined`
120
120
  - 角色:核心模块。库内所有存在平台行为差异的模块都会先读取 `$app` 再分流(如 `done`、`notification`、`fetch`、`Storage`、`Console`、`environment`)。
121
121
  - 读取方式:
122
122
 
@@ -130,10 +130,12 @@ console.log(appName);
130
130
  1. 存在 `$task` -> `Quantumult X`
131
131
  2. 存在 `$loon` -> `Loon`
132
132
  3. 存在 `$rocket` -> `Shadowrocket`
133
- 4. 存在 `module` -> `Node.js`
134
- 5. 存在 `Egern` -> `Egern`
135
- 6. 存在 `$environment` 且有 `surge-version` -> `Surge`
136
- 7. 存在 `$environment` 且有 `stash-version` -> `Stash`
133
+ 4. 存在 `Egern` -> `Egern`
134
+ 5. 存在 `$environment` 且有 `surge-version` -> `Surge`
135
+ 6. 存在 `$environment` 且有 `stash-version` -> `Stash`
136
+ 7. 存在 `process.versions.node` -> `Node.js`
137
+ 8. 默认回落 -> `undefined`
138
+ - 实现细节:内部使用 `'key' in globalThis` 检测平台标记,避免 `Object.keys(globalThis)` 漏掉不可枚举全局变量;因此在 Workers / Vercel 风格全局对象存在时,只要 `process.versions.node` 可用,仍会识别为 `Node.js`。
137
139
 
138
140
  #### `$environment` / `environment()`
139
141
  - 路径:`lib/environment.mjs`(未从包主入口导出)
@@ -221,6 +223,7 @@ console.log($argument); // { mode: "on", a: { b: "1" } }
221
223
  - Quantumult X 会丢弃未在白名单内的字段。
222
224
  - Quantumult X 的 `status` 在部分场景要求完整状态行(如 `HTTP/1.1 200 OK`),本库会在传入数字状态码时自动拼接(依赖 `StatusTexts`)。
223
225
  - Node.js 不调用 `$done`,而是直接退出进程,且退出码固定为 `1`。
226
+ - 未识别平台(`$app === undefined`)只记录结束日志,不会尝试调用 `$done` 或退出进程。
224
227
 
225
228
  ### `lib/notification.mjs`
226
229
 
@@ -381,6 +384,7 @@ const store = getStorage("@my_box", ["YouTube", "Global"], database);
381
384
  - `timeout`
382
385
  - `policy`
383
386
  - `redirection` / `auto-redirect`
387
+ - `auto-cookie`(仅 Node.js 分支识别;默认启用,传入 `false` / `0` / `-1` 可关闭)
384
388
 
385
389
  说明:下表是各 App 原生 HTTP 接口的差异补充,以及本库 `fetch` 的内部映射方式。调用方使用统一入参即可。
386
390
 
@@ -394,7 +398,7 @@ const store = getStorage("@my_box", ["YouTube", "Global"], database);
394
398
  | Egern | `$httpClient[method]` | 秒 | 无专门映射 | `auto-redirect` | 同上 |
395
399
  | Shadowrocket | `$httpClient[method]` | 秒 | `headers.X-Surge-Proxy` | `auto-redirect` | 同上 |
396
400
  | Quantumult X | `$task.fetch` | 毫秒(内部乘 1000) | `opts.policy` | `opts.redirection` | `body(ArrayBuffer/TypedArray)` 转 `bodyBytes`;响应按 `Content-Type` 恢复到 `body` |
397
- | Node.js | `fetch` + `fetch-cookie` | 毫秒(内部乘 1000) | 无 | `redirect: follow/manual` | 返回 `body`(UTF-8 string) + `bodyBytes`(ArrayBuffer) |
401
+ | Node.js | `globalThis.fetch`(不存在时回退 `node-fetch`);默认按需包裹 `fetch-cookie` | 毫秒(内部乘 1000) | 无 | `redirect: follow/manual` | 返回 `body`(UTF-8 string) + `bodyBytes`(ArrayBuffer) |
398
402
 
399
403
  返回对象(统一后)常见字段:
400
404
  - `ok`
@@ -408,7 +412,8 @@ const store = getStorage("@my_box", ["YouTube", "Global"], database);
408
412
  不可用/差异点:
409
413
  - `policy` 在 Surge / Egern / Node.js 分支没有额外适配逻辑。
410
414
  - `redirection` 在部分平台会映射为 `auto-redirect` 或 `opts.redirection`。
411
- - Node.js 分支依赖 `globalThis.fetch` / `globalThis.fetchCookie` 或 `node-fetch` + `fetch-cookie`。
415
+ - Node.js 分支优先复用 `globalThis.fetch`;若不存在则回退到 `node-fetch`,并在 `auto-cookie` 未关闭时按需包裹 `fetch-cookie`。
416
+ - 传入 `timeout` 时,`5` 和 `5000` 都会被接受;库会先将用户输入归一化,再按平台要求转换为秒或毫秒。
412
417
  - 返回结构是统一兼容结构,不等同于浏览器 `Response` 对象。
413
418
 
414
419
  ### `polyfill/Storage.mjs`
@@ -631,7 +636,6 @@ console.log(value); // 1
631
636
 
632
637
  - `lib/argument.mjs` 为 `$argument` 标准化模块,`import` 时会按规则重写全局 `$argument`。
633
638
  - `lib/done.mjs` 在 Node.js 固定 `process.exit(1)`。
634
- - `polyfill/fetch.mjs` 的超时保护使用了 `Promise.race`,但当前实现里请求 Promise 先被 `await`,可能导致超时行为与预期不完全一致。
635
639
  - `Storage.removeItem("@a.b")` 分支存在未声明变量写入风险;如要大量使用路径删除,建议先本地验证。
636
640
  - `lib/runScript.mjs` 未从包主入口导出,需要按文件路径直接导入。
637
641
 
package/lib/app.mjs CHANGED
@@ -1,36 +1,43 @@
1
1
  /**
2
- * 当前运行平台名称。
3
- * Current runtime platform name.
2
+ * 当前运行平台名称(脚本平台优先,模块系统次之)。
3
+ * Current runtime platform name (script platform first, module system second).
4
4
  *
5
5
  * 识别顺序:
6
6
  * Detection order:
7
7
  * 1) `$task` -> Quantumult X
8
8
  * 2) `$loon` -> Loon
9
9
  * 3) `$rocket` -> Shadowrocket
10
- * 4) `module` -> Node.js
11
- * 5) `Egern` -> Egern
12
- * 6) `$environment["surge-version"]` -> Surge
13
- * 7) `$environment["stash-version"]` -> Stash
10
+ * 4) `Egern` -> Egern
11
+ * 5) `$environment["surge-version"]` -> Surge
12
+ * 6) `$environment["stash-version"]` -> Stash
13
+ * 7) `process.versions.node` -> Node.js
14
+ * 8) 默认回落 -> undefined
15
+ * default fallback -> undefined
14
16
  *
15
- * @type {("Quantumult X" | "Loon" | "Shadowrocket" | "Node.js" | "Egern" | "Surge" | "Stash" | undefined)}
17
+ * 说明:
18
+ * Notes:
19
+ * - 使用 `'key' in globalThis`,避免 `Object.keys` 对不可枚举全局变量漏检。
20
+ * - Use `'key' in globalThis` to avoid missing non-enumerable globals with `Object.keys`.
21
+ *
22
+ * @type {("Quantumult X" | "Loon" | "Shadowrocket" | "Egern" | "Surge" | "Stash" | "Node.js" | undefined)}
16
23
  */
17
24
  export const $app = (() => {
18
- const keys = Object.keys(globalThis);
25
+ const has = (key) => key in globalThis;
19
26
  switch (true) {
20
- case keys.includes("$task"):
27
+ case has("$task"):
21
28
  return "Quantumult X";
22
- case keys.includes("$loon"):
29
+ case has("$loon"):
23
30
  return "Loon";
24
- case keys.includes("$rocket"):
31
+ case has("$rocket"):
25
32
  return "Shadowrocket";
26
- case typeof module !== "undefined":
27
- return "Node.js";
28
- case keys.includes("Egern"):
33
+ case has("Egern"):
29
34
  return "Egern";
30
- case keys.includes("$environment"):
31
- if ($environment["surge-version"]) return "Surge";
32
- if ($environment["stash-version"]) return "Stash";
33
- return undefined;
35
+ case Boolean(globalThis.$environment?.["surge-version"]):
36
+ return "Surge";
37
+ case Boolean(globalThis.$environment?.["stash-version"]):
38
+ return "Stash";
39
+ case Boolean(globalThis.process?.versions?.node):
40
+ return "Node.js";
34
41
  default:
35
42
  return undefined;
36
43
  }
package/lib/done.mjs CHANGED
@@ -26,6 +26,8 @@ import { StatusTexts } from "../polyfill/StatusTexts.mjs";
26
26
  * - This is the call entry and native `$done` differences are handled internally
27
27
  * - Node.js 不调用 `$done`,而是直接退出进程
28
28
  * - Node.js does not call `$done`; it exits the process directly
29
+ * - 未识别平台仅记录结束日志,不会强制退出
30
+ * - Unknown runtimes only log completion and do not force an exit
29
31
  *
30
32
  * @param {DonePayload} [object={}] 统一响应对象 / Unified response object.
31
33
  * @returns {void}
@@ -79,9 +81,11 @@ export function done(object = {}) {
79
81
  $done(object);
80
82
  break;
81
83
  case "Node.js":
82
- default:
83
84
  Console.log("🚩 执行结束!");
84
85
  process.exit(1);
85
86
  break;
87
+ default:
88
+ Console.log("🚩 执行结束!");
89
+ break;
86
90
  }
87
91
  }
package/package.json CHANGED
@@ -41,5 +41,5 @@
41
41
  "registry": "https://registry.npmjs.org/",
42
42
  "access": "public"
43
43
  },
44
- "version": "2.1.7"
44
+ "version": "2.2.0"
45
45
  }
@@ -17,6 +17,7 @@ import { StatusTexts } from "./StatusTexts.mjs";
17
17
  * @property {string} [policy] 指定策略 / Preferred policy.
18
18
  * @property {boolean} [redirection] 是否跟随重定向 / Whether to follow redirects.
19
19
  * @property {boolean} ["auto-redirect"] 平台重定向字段 / Platform redirect flag.
20
+ * @property {boolean|number|string} ["auto-cookie"] Node.js Cookie 开关 / Node.js Cookie toggle.
20
21
  * @property {Record<string, any>} [opts] 平台扩展字段 / Platform extension fields.
21
22
  */
22
23
 
@@ -56,6 +57,8 @@ import { StatusTexts } from "./StatusTexts.mjs";
56
57
  * Known differences from Web `fetch`:
57
58
  * - 支持 `policy`、`auto-redirect` 等平台扩展字段
58
59
  * - Supports platform extension fields like `policy` and `auto-redirect`
60
+ * - Node.js 分支默认启用 Cookie 透传,可通过 `auto-cookie` 关闭
61
+ * - Node.js enables Cookie forwarding by default and can disable it via `auto-cookie`
59
62
  * - 非浏览器平台通过 `$httpClient/$task` 实现,不是原生 Fetch 实现
60
63
  * - Non-browser platforms use `$httpClient/$task` instead of native Fetch engine
61
64
  * - 返回结构包含 `statusCode/bodyBytes` 等兼容字段
@@ -69,7 +72,8 @@ import { StatusTexts } from "./StatusTexts.mjs";
69
72
  * @returns {Promise<FetchResponse>}
70
73
  */
71
74
  export async function fetch(resource, options = {}) {
72
- // 初始化参数
75
+ // 初始化参数。
76
+ // Initialize request input.
73
77
  switch (typeof resource) {
74
78
  case "object":
75
79
  resource = { ...options, ...resource };
@@ -81,26 +85,34 @@ export async function fetch(resource, options = {}) {
81
85
  default:
82
86
  throw new TypeError(`${Function.name}: 参数类型错误, resource 必须为对象或字符串`);
83
87
  }
84
- // 自动判断请求方法
88
+ // 自动判断请求方法。
89
+ // Infer the HTTP method automatically.
85
90
  if (!resource.method) {
86
91
  resource.method = "GET";
87
92
  if (resource.body ?? resource.bodyBytes) resource.method = "POST";
88
93
  }
89
- // 移除请求头中的部分参数, 让其自动生成
94
+ // 移除需要由底层实现自动生成的请求头。
95
+ // Remove headers that should be generated by the underlying runtime.
90
96
  delete resource.headers?.Host;
91
97
  delete resource.headers?.[":authority"];
92
98
  delete resource.headers?.["Content-Length"];
93
99
  delete resource.headers?.["content-length"];
94
- // 定义请求方法(小写)
100
+ // 统一请求方法为小写,方便后续索引平台 API。
101
+ // Normalize the method to lowercase for platform API lookups.
95
102
  const method = resource.method.toLocaleLowerCase();
96
- // 转换请求超时时间参数
103
+ // 默认请求超时时间为 5 秒。
104
+ // Default request timeout to 5 seconds.
97
105
  if (!resource.timeout) resource.timeout = 5;
106
+ // 智能矫正请求超时时间,兼容用户输入的秒或毫秒。
107
+ // Normalize timeout input so both seconds and milliseconds are accepted.
98
108
  if (resource.timeout) {
99
109
  resource.timeout = Number.parseInt(resource.timeout, 10);
100
- // 转换为秒,大于500视为毫秒,小于等于500视为秒
110
+ // 统一先转换为秒,大于 500 视为毫秒输入。
111
+ // Convert to seconds first and treat values above 500 as milliseconds.
101
112
  if (resource.timeout > 500) resource.timeout = Math.round(resource.timeout / 1000);
102
113
  }
103
- // 判断平台
114
+ // 根据当前平台选择请求实现。
115
+ // Select the request engine for the current platform.
104
116
  switch ($app) {
105
117
  case "Loon":
106
118
  case "Surge":
@@ -108,10 +120,15 @@ export async function fetch(resource, options = {}) {
108
120
  case "Egern":
109
121
  case "Shadowrocket":
110
122
  default:
111
- // 转换请求参数
123
+ // 转换通用请求参数到 `$httpClient` 语义。
124
+ // Map shared request fields to `$httpClient` semantics.
112
125
  if (resource.timeout) {
113
126
  switch ($app) {
114
127
  case "Loon":
128
+ case "Quantumult X":
129
+ case "Node.js":
130
+ // 这些平台要求毫秒,因此把秒重新换算为毫秒。
131
+ // These platforms expect milliseconds, so convert seconds back to milliseconds.
115
132
  resource.timeout = resource.timeout * 1000;
116
133
  break;
117
134
  case "Shadowrocket":
@@ -136,12 +153,14 @@ export async function fetch(resource, options = {}) {
136
153
  }
137
154
  }
138
155
  if (typeof resource.redirection === "boolean") resource["auto-redirect"] = resource.redirection;
139
- // 转换请求体
156
+ // 优先把 `bodyBytes` 映射回 `$httpClient` 能接受的 `body`。
157
+ // Prefer mapping `bodyBytes` back to the `body` field expected by `$httpClient`.
140
158
  if (resource.bodyBytes && !resource.body) {
141
159
  resource.body = resource.bodyBytes;
142
160
  resource.bodyBytes = undefined;
143
161
  }
144
- // 判断是否请求二进制响应体
162
+ // 根据 `Accept` 推断是否需要二进制响应体。
163
+ // Infer whether the response should be treated as binary from `Accept`.
145
164
  switch ((resource.headers?.Accept || resource.headers?.accept)?.split(";")?.[0]) {
146
165
  case "application/protobuf":
147
166
  case "application/x-protobuf":
@@ -153,9 +172,10 @@ export async function fetch(resource, options = {}) {
153
172
  resource["binary-mode"] = true;
154
173
  break;
155
174
  }
156
- // 发送请求
157
- return await new Promise((resolve, reject) => {
158
- $httpClient[method](resource, (error, response, body) => {
175
+ // 发送 `$httpClient` 请求并归一化返回结构。
176
+ // Send the `$httpClient` request and normalize the response payload.
177
+ return new Promise((resolve, reject) => {
178
+ globalThis.$httpClient[method](resource, (error, response, body) => {
159
179
  if (error) reject(error);
160
180
  else {
161
181
  response.ok = /^2\d\d$/.test(response.status);
@@ -170,11 +190,12 @@ export async function fetch(resource, options = {}) {
170
190
  });
171
191
  });
172
192
  case "Quantumult X":
173
- // 转换请求参数
174
- resource.timeout = resource.timeout * 1000;
193
+ // 转换 Quantumult X 专有请求参数。
194
+ // Map request fields to Quantumult X specific options.
175
195
  if (resource.policy) _.set(resource, "opts.policy", resource.policy);
176
196
  if (typeof resource["auto-redirect"] === "boolean") _.set(resource, "opts.redirection", resource["auto-redirect"]);
177
- // 转换请求体
197
+ // Quantumult X 使用 `bodyBytes` 传输二进制请求体。
198
+ // Quantumult X uses `bodyBytes` for binary request payloads.
178
199
  if (resource.body instanceof ArrayBuffer) {
179
200
  resource.bodyBytes = resource.body;
180
201
  resource.body = undefined;
@@ -182,9 +203,10 @@ export async function fetch(resource, options = {}) {
182
203
  resource.bodyBytes = resource.body.buffer.slice(resource.body.byteOffset, resource.body.byteLength + resource.body.byteOffset);
183
204
  resource.body = undefined;
184
205
  } else if (resource.body) resource.bodyBytes = undefined;
185
- // 发送请求
206
+ // 发送请求,并用 `Promise.race` 提供统一超时保护。
207
+ // Send the request and enforce timeout with `Promise.race`.
186
208
  return Promise.race([
187
- await $task.fetch(resource).then(
209
+ globalThis.$task.fetch(resource).then(
188
210
  response => {
189
211
  response.ok = /^2\d\d$/.test(response.statusCode);
190
212
  response.status = response.statusCode;
@@ -215,16 +237,37 @@ export async function fetch(resource, options = {}) {
215
237
  }),
216
238
  ]);
217
239
  case "Node.js": {
218
- const nodeFetch = globalThis.fetch ? globalThis.fetch : require("node-fetch");
219
- const fetchCookie = globalThis.fetchCookie ? globalThis.fetchCookie : require("fetch-cookie").default;
220
- const fetch = fetchCookie(nodeFetch);
221
- // 转换请求参数
222
- resource.timeout = resource.timeout * 1000;
240
+ // Node.js 优先复用原生/宿主 `fetch`,缺失时再回退到 `node-fetch`。
241
+ // Reuse host `fetch` in Node.js when available and fall back to `node-fetch` otherwise.
242
+ if (!globalThis.fetch) globalThis.fetch = require("node-fetch");
243
+ switch (resource["auto-cookie"]) {
244
+ case undefined:
245
+ case "true":
246
+ case true:
247
+ case "1":
248
+ case 1:
249
+ default:
250
+ // 仅在尚未包裹 CookieJar 时注入 `fetch-cookie`,避免重复包装。
251
+ // Inject `fetch-cookie` only once when a cookie jar is not already attached.
252
+ if (!globalThis.fetch?.cookieJar) globalThis.fetch = require("fetch-cookie").default(globalThis.fetch);
253
+ break;
254
+ case "false":
255
+ case false:
256
+ case "0":
257
+ case 0:
258
+ case "-1":
259
+ case -1:
260
+ break;
261
+ }
262
+ // 将通用字段映射到 Node.js Fetch 语义。
263
+ // Map shared fields to Node.js Fetch semantics.
223
264
  resource.redirect = resource.redirection ? "follow" : "manual";
224
265
  const { url, ...options } = resource;
225
- // 发送请求
266
+ // 发起请求并归一化响应头、文本与二进制响应体。
267
+ // Send the request and normalize headers, text, and binary response data.
226
268
  return Promise.race([
227
- await fetch(url, options)
269
+ globalThis
270
+ .fetch(url, options)
228
271
  .then(async response => {
229
272
  const bodyBytes = await response.arrayBuffer();
230
273
  let headers;
@@ -1,5 +1,12 @@
1
1
  declare module "@nsnanocat/util" {
2
- export type AppName = "Quantumult X" | "Loon" | "Shadowrocket" | "Node.js" | "Egern" | "Surge" | "Stash";
2
+ export type AppName =
3
+ | "Quantumult X"
4
+ | "Loon"
5
+ | "Shadowrocket"
6
+ | "Egern"
7
+ | "Surge"
8
+ | "Stash"
9
+ | "Node.js";
3
10
 
4
11
  export const $app: AppName | undefined;
5
12
  export const $argument: Record<string, unknown>;
@@ -54,6 +61,7 @@ declare module "@nsnanocat/util" {
54
61
  policy?: string;
55
62
  redirection?: boolean;
56
63
  "auto-redirect"?: boolean;
64
+ "auto-cookie"?: boolean | number | string;
57
65
  opts?: Record<string, unknown>;
58
66
  [key: string]: unknown;
59
67
  }