diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml new file mode 100644 index 0000000..931d447 --- /dev/null +++ b/.github/workflows/preview.yml @@ -0,0 +1,38 @@ +name: preview + +on: + pull_request: + types: + - opened + - synchronize # Trigger on every push to the PR + +jobs: + preview: + runs-on: ubuntu-latest + permissions: + packages: write + contents: read + pull-requests: write + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + registry-url: "https://registry.npmjs.org" + - run: npm ci + - run: | + COMMIT_SHA=${{ github.sha }} + npm version 0.0.0-preview-${COMMIT_SHA::7} --no-git-tag-version + - run: npm publish --tag preview-${{ github.event.number }} --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + - run: | + VERSION=$(node -p "require('./package.json').version") + PR_NUMBER=${{ github.event.pull_request.number }} + COMMENT_BODY="Preview version \`$VERSION\` published to npm. To install, run: + \`\`\`bash + npm install @nsnanocat/util@$VERSION + \`\`\`" + gh pr comment $PR_NUMBER --body "$COMMENT_BODY" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/release-package-to-github.yml b/.github/workflows/release-package-to-github.yml deleted file mode 100644 index 1774884..0000000 --- a/.github/workflows/release-package-to-github.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Release Node.js Package to GitHub Package Registry - -on: - push: - # Sequence of patterns matched against refs/tags - tags: - - "v*" # Push events to matching v*, i.e. v1.0, v20.15.10 - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: 16 - - run: npm ci - - run: npm test - - publish-gpr: - needs: build - runs-on: ubuntu-latest - permissions: - packages: write - contents: read - steps: - - uses: actions/checkout@v4 - - name: Update local package.json version from release tag - uses: BellCubeDev/update-package-version-by-release-tag@v2 - with: - version: ${{ github.ref_name }} - keep-v: "false" # If set to "true", will not remove any 'v' prefix from the version number. - ignore-semver-check: "false" # If set to "true", will not check if the version number is a valid semver version. - - uses: actions/setup-node@v4 - with: - registry-url: 'https://npm.pkg.github.com' - - run: npm ci - - run: npm publish - env: - NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release-package-to-npm.yml b/.github/workflows/release-package.yml similarity index 66% rename from .github/workflows/release-package-to-npm.yml rename to .github/workflows/release-package.yml index fdb3557..1148c1a 100644 --- a/.github/workflows/release-package-to-npm.yml +++ b/.github/workflows/release-package.yml @@ -1,4 +1,4 @@ -name: Release Node.js Package to NPM Registry +name: Release on: push: @@ -7,22 +7,14 @@ on: - "v*" # Push events to matching v*, i.e. v1.0, v20.15.10 jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: 16 - - run: npm ci - - run: npm test - - publish-gpr: - needs: build + publish: runs-on: ubuntu-latest permissions: packages: write contents: read + strategy: + matrix: + registry: ["https://registry.npmjs.org", "https://npm.pkg.github.com"] steps: - uses: actions/checkout@v4 - name: Update local package.json version from release tag @@ -33,9 +25,9 @@ jobs: ignore-semver-check: "false" # If set to "true", will not check if the version number is a valid semver version. - uses: actions/setup-node@v4 with: - node-version: 16 - registry-url: 'https://registry.npmjs.org' + node-version: 20 + registry-url: ${{ matrix.registry }} - run: npm ci - run: npm publish --access public env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + NODE_AUTH_TOKEN: ${{ matrix.registry == 'https://registry.npmjs.org' && secrets.NPM_TOKEN || secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/biome.json b/biome.json index 37d3d39..2d4a3af 100644 --- a/biome.json +++ b/biome.json @@ -1,78 +1,53 @@ { - "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", - "files": { - "ignore": [ - "**/*.bundle.js" - ], - "ignoreUnknown": false - }, + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "organizeImports": { + "enabled": true + }, + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "formatter": { + "enabled": true, + "useEditorconfig": true, + "formatWithErrors": false, + "indentStyle": "space", + "indentWidth": 2, + "lineEnding": "lf", + "lineWidth": 120, + "attributePosition": "auto", + "bracketSpacing": true + }, + "javascript": { "formatter": { - "enabled": true, - "indentStyle": "tab", - "indentWidth": 2, - "lineEnding": "lf", - "lineWidth": 320 - }, - "javascript": { - "formatter": { - "arrowParentheses": "asNeeded", - "bracketSameLine": true, - "quoteStyle": "double" - } - }, - "json": { - "parser": { - "allowComments": true, - "allowTrailingCommas": true - } - }, - "linter": { - "enabled": true, - "rules": { - "complexity": { - "noForEach": "off", - "noStaticOnlyClass": "off", - "noUselessSwitchCase": "off", - "useArrowFunction": "info", - "useFlatMap": "off", - "useLiteralKeys": "info" - }, - "correctness": { - "noInnerDeclarations": "info", - "noSelfAssign": "off", - "noSwitchDeclarations": "info", - "noUnsafeOptionalChaining": "info" - }, - "performance": { - "noDelete": "info" - }, - "recommended": true, - "style": { - "noNegationElse": "off", - "noParameterAssign": "off", - "noUselessElse": "off", - "noVar": "info", - "useDefaultParameterLast": "info", - "useForOf": "error", - "useNodejsImportProtocol": "error", - "useNumberNamespace": "error", - "useSingleVarDeclarator": "off" - }, - "suspicious": { - "noAssignInExpressions": "info", - "noDoubleEquals": "info", - "noFallthroughSwitchClause": "info", - "noGlobalIsNan": "off", - "useDefaultSwitchClauseLast": "off" - } - } - }, - "organizeImports": { - "enabled": true - }, - "vcs": { - "clientKind": "git", - "enabled": true, - "useIgnoreFile": true + "jsxQuoteStyle": "double", + "quoteProperties": "asNeeded", + "trailingCommas": "all", + "semicolons": "always", + "arrowParentheses": "always", + "bracketSameLine": false, + "quoteStyle": "single", + "attributePosition": "auto", + "bracketSpacing": true } + }, + "css": { + "parser": { + "cssModules": true + } + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "complexity": { + "noUselessSwitchCase": "off", + "noForEach": "off" + }, + "suspicious": { + "noExplicitAny": "warn" + } + } + } } diff --git a/getStorage.mjs b/getStorage.mjs deleted file mode 100644 index f2f1528..0000000 --- a/getStorage.mjs +++ /dev/null @@ -1,98 +0,0 @@ -import { Console } from "./polyfill/Console.mjs"; -import { Lodash as _ } from "./polyfill/Lodash.mjs"; -import { Storage } from "./polyfill/Storage.mjs"; - -/** - * Get Storage Variables - * @link https://github.com/NanoCat-Me/utils/blob/main/getStorage.mjs - * @author VirgilClyne - * @param {string} key - Persistent Store Key - * @param {array | string} names - Platform Names - * @param {object} database - Default Database - * @return {object} { Settings, Caches, Configs } - */ -export function getStorage(key, names, database) { - names = [names].flat(Number.POSITIVE_INFINITY); - //Console.log("☑️ getStorage"); - /***************** Default *****************/ - const Store = { Settings: database?.Default?.Settings || {}, Configs: database?.Default?.Configs || {}, Caches: {} }; - //Console.debug("Default", `Store.Settings类型: ${typeof Store.Settings}`, `Store.Settings: ${JSON.stringify(Store.Settings)}`); - /***************** Database *****************/ - names.forEach(name => { - Store.Settings = { ...Store.Settings, ...database?.[name]?.Settings }; - Store.Configs = { ...Store.Configs, ...database?.[name]?.Configs }; - }); - //Console.debug("Database", `Store.Settings类型: ${typeof Store.Settings}`, `Store.Settings: ${JSON.stringify(Store.Settings)}`); - /***************** Argument *****************/ - switch (typeof $argument) { - // biome-ignore lint/suspicious/noFallthroughSwitchClause: - case "string": - $argument = Object.fromEntries($argument.split("&").map(item => item.split("=", 2).map(i => i.replace(/\"/g, "")))); - case "object": { - const argument = {}; - Object.keys($argument).forEach(key => _.set(argument, key, $argument[key])); - //Console.debug(`✅ $argument`, `argument: ${JSON.stringify(argument)}`); - Store.Settings = { ...Store.Settings, ...argument }; - break; - } - case "undefined": - break; - } - //Console.debug("$argument", `Store.Settings类型: ${typeof Store.Settings}`, `Store.Settings: ${JSON.stringify(Store.Settings)}`); - /***************** BoxJs *****************/ - // 包装为局部变量,用完释放内存 - // BoxJs的清空操作返回假值空字符串, 逻辑或操作符会在左侧操作数为假值时返回右侧操作数。 - const BoxJs = Storage.getItem(key); - if (BoxJs) { - //Console.debug("BoxJs", `BoxJs类型: ${typeof BoxJs}`, `BoxJs内容: ${JSON.stringify(BoxJs || {})}`); - names.forEach(name => { - switch (typeof BoxJs?.[name]?.Settings) { - // biome-ignore lint/suspicious/noFallthroughSwitchClause: - case "string": - BoxJs[name].Settings = JSON.parse(BoxJs[name].Settings || "{}"); - case "object": - Store.Settings = { ...Store.Settings, ...BoxJs[name].Settings }; - break; - case "undefined": - break; - } - switch (typeof BoxJs?.[name]?.Caches) { - // biome-ignore lint/suspicious/noFallthroughSwitchClause: - case "string": - BoxJs[name].Caches = JSON.parse(BoxJs[name].Caches || "{}"); - case "object": - Store.Caches = { ...Store.Caches, ...BoxJs[name].Caches }; - break; - case "undefined": - break; - } - }); - //Console.debug("BoxJs", `Store.Settings类型: ${typeof Store.Settings}`, `Store.Settings: ${JSON.stringify(Store.Settings)}`); - } - /***************** traverseObject *****************/ - traverseObject(Store.Settings, (key, value) => { - //Console.debug("☑️ traverseObject", `${key}: ${typeof value}`, `${key}: ${JSON.stringify(value)}`); - if (value === "true" || value === "false") - value = JSON.parse(value); // 字符串转Boolean - else if (typeof value === "string") { - if (value.includes(",")) - value = value.split(",").map(item => string2number(item)); // 字符串转数组转数字 - else value = string2number(value); // 字符串转数字 - } - return value; - }); - //Console.debug("✅ traverseObject", `Store.Settings类型: ${typeof Store.Settings}`, `Store.Settings: ${JSON.stringify(Store.Settings)}`); - return Store; -} - -function traverseObject(o, c) { - for (const t in o) { - const n = o[t]; - o[t] = "object" === typeof n && null !== n ? traverseObject(n, c) : c(t, n); - } - return o; -} -function string2number(string) { - if (/^\d+$/.test(string)) string = Number.parseInt(string, 10); - return string; -} diff --git a/index.js b/index.js deleted file mode 100644 index 84c68c3..0000000 --- a/index.js +++ /dev/null @@ -1,11 +0,0 @@ -export * from "./lib/app.mjs"; -export * from "./lib/done.mjs"; -export * from "./lib/notification.mjs"; -export * from "./lib/time.mjs"; -export * from "./lib/wait.mjs"; -export * from "./polyfill/Console.mjs"; -export * from "./polyfill/fetch.mjs"; -export * from "./polyfill/Lodash.mjs"; -export * from "./polyfill/StatusTexts.mjs"; -export * from "./polyfill/Storage.mjs"; -export * from "./getStorage.mjs"; diff --git a/lib/app.mjs b/lib/app.mjs deleted file mode 100644 index d74211d..0000000 --- a/lib/app.mjs +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Current app name - * - * @type {("Quantumult X" | "Loon" | "Shadowrocket" | "Node.js" | "Egern" | "Surge" | "Stash")} - */ -export const $app = (() => { - const keys = Object.keys(globalThis); - switch (true) { - case keys.includes("$task"): - return "Quantumult X"; - case keys.includes("$loon"): - return "Loon"; - case keys.includes("$rocket"): - return "Shadowrocket"; - case typeof module !== "undefined": - return "Node.js"; - case keys.includes("Egern"): - return "Egern"; - case keys.includes("$environment"): - if ($environment["surge-version"]) return "Surge"; - if ($environment["stash-version"]) return "Stash"; - return undefined; - default: - return undefined; - } -})(); diff --git a/lib/done.mjs b/lib/done.mjs deleted file mode 100644 index 1bb5098..0000000 --- a/lib/done.mjs +++ /dev/null @@ -1,67 +0,0 @@ -import { $app } from "./app.mjs"; -import { Console } from "../polyfill/Console.mjs"; -import { Lodash as _ } from "../polyfill/Lodash.mjs"; -import { StatusTexts } from "../polyfill/StatusTexts.mjs"; - -/** - * Complete the script execution - * - * @export - * @param {object} object - * @returns {void} - */ -export function done(object = {}) { - switch ($app) { - case "Surge": - if (object.policy) _.set(object, "headers.X-Surge-Policy", object.policy); - Console.log("🚩 执行结束!", `🕛 ${new Date().getTime() / 1000 - $script.startTime} 秒`); - $done(object); - break; - case "Loon": - if (object.policy) object.node = object.policy; - Console.log("🚩 执行结束!", `🕛 ${(new Date() - $script.startTime) / 1000} 秒`); - $done(object); - break; - case "Stash": - if (object.policy) _.set(object, "headers.X-Stash-Selected-Proxy", encodeURI(object.policy)); - Console.log("🚩 执行结束!", `🕛 ${(new Date() - $script.startTime) / 1000} 秒`); - $done(object); - break; - case "Egern": - Console.log("🚩 执行结束!"); - $done(object); - break; - case "Shadowrocket": - Console.log("🚩 执行结束!"); - $done(object); - break; - case "Quantumult X": - if (object.policy) _.set(object, "opts.policy", object.policy); - object = _.pick(object, ["status", "url", "headers", "body", "bodyBytes"]); - switch (typeof object.status) { - case "number": - object.status = `HTTP/1.1 ${object.status} ${StatusTexts[object.status]}`; - break; - case "string": - case "undefined": - break; - default: - throw new TypeError(`${Function.name}: 参数类型错误, status 必须为数字或字符串`); - } - if (object.body instanceof ArrayBuffer) { - object.bodyBytes = object.body; - object.body = undefined; - } else if (ArrayBuffer.isView(object.body)) { - object.bodyBytes = object.body.buffer.slice(object.body.byteOffset, object.body.byteLength + object.body.byteOffset); - object.body = undefined; - } else if (object.body) object.bodyBytes = undefined; - Console.log("🚩 执行结束!"); - $done(object); - break; - case "Node.js": - default: - Console.log("🚩 执行结束!"); - process.exit(1); - break; - } -} diff --git a/lib/environment.mjs b/lib/environment.mjs deleted file mode 100644 index 5b70418..0000000 --- a/lib/environment.mjs +++ /dev/null @@ -1,34 +0,0 @@ -import { $app } from "./app.mjs"; - -export const $environment = environment(); -export function environment() { - switch ($app) { - case "Surge": - $environment.app = "Surge"; - return $environment; - case "Stash": - $environment.app = "Stash"; - return $environment; - case "Egern": - $environment.app = "Egern"; - return $environment; - case "Loon": { - const environment = $loon.split(" "); - return { - device: environment[0], - ios: environment[1], - "loon-version": environment[2], - app: "Loon", - }; - } - case "Quantumult X": - return { - app: "Quantumult X", - }; - case "Node.js": - process.env.app = "Node.js"; - return process.env; - default: - return {}; - } -} diff --git a/lib/index.js b/lib/index.js deleted file mode 100644 index c73a06f..0000000 --- a/lib/index.js +++ /dev/null @@ -1,5 +0,0 @@ -export * from "./app.mjs"; -export * from "./done.mjs"; -export * from "./notification.mjs"; -export * from "./time.mjs"; -export * from "./wait.mjs"; diff --git a/lib/notification.mjs b/lib/notification.mjs deleted file mode 100644 index 940caad..0000000 --- a/lib/notification.mjs +++ /dev/null @@ -1,151 +0,0 @@ -import { $app } from "./app.mjs"; -import { Console } from "../polyfill/Console.mjs"; - -/** - * 系统通知 - * - * > 通知参数: 同时支持 QuanX 和 Loon 两种格式, EnvJs根据运行环境自动转换, Surge 环境不支持多媒体通知 - * - * 示例: - * $.msg(title, subtitle, body, "twitter://") - * $.msg(title, subtitle, body, { "open-url": "twitter://", "media-url": "https://github.githubassets.com/images/modules/open_graph/github-mark.png" }) - * $.msg(title, subtitle, body, { "open-url": "https://bing.com", "media-url": "https://github.githubassets.com/images/modules/open_graph/github-mark.png" }) - * - * @param {string} title 标题 - * @param {string} subtitle 副标题 - * @param {string} body 内容 - * @param {*} mutableContent 通知扩展字段 - * - */ -export function notification(title = `ℹ️ ${$app} 通知`, subtitle = "", body = "", content = {}) { - const mutableContent = MutableContent(content); - switch ($app) { - case "Surge": - case "Loon": - case "Stash": - case "Egern": - case "Shadowrocket": - default: - $notification.post(title, subtitle, body, mutableContent); - break; - case "Quantumult X": - $notify(title, subtitle, body, mutableContent); - break; - case "Node.js": - break; - } - Console.group("📣 系统通知"); - Console.log(title, subtitle, body, JSON.stringify(mutableContent, null, 2)); - Console.groupEnd(); -} - -const MutableContent = content => { - const mutableContent = {}; - switch (typeof content) { - case undefined: - break; - case "string": - case "number": - case "boolean": - switch ($app) { - case "Surge": - case "Stash": - case "Egern": - default: - mutableContent.url = content; - break; - case "Loon": - case "Shadowrocket": - mutableContent.openUrl = content; - break; - case "Quantumult X": - mutableContent["open-url"] = content; - break; - case "Node.js": - break; - } - break; - case "object": { - const openUrl = content.open || content["open-url"] || content.url || content.openUrl; - const copyUrl = content.copy || content["update-pasteboard"] || content.updatePasteboard; - const mediaUrl = content.media || content["media-url"] || content.mediaUrl; - switch ($app) { - case "Surge": - case "Stash": - case "Egern": - case "Shadowrocket": - default: { - if (openUrl) { - mutableContent.action = "open-url"; - mutableContent.url = openUrl; - } - if (copyUrl) { - mutableContent.action = "clipboard"; - mutableContent.text = copyUrl; - } - if (mediaUrl) { - switch (true) { - case mediaUrl.startsWith("http"): // http 开头的网络地址 - mutableContent["media-url"] = mediaUrl; - break; - case mediaUrl.startsWith("data:"): { - // data 开头的 Base64 编码 - // data:image/png;base64,iVBORw0KGgo... - const base64RegExp = /^data:(?\w+\/\w+);base64,(?.+)/; - const { MIME, Base64 } = mediaUrl.match(base64RegExp).groups; - mutableContent["media-base64"] = Base64; - mutableContent["media-base64-mime"] = content.mime || MIME; - break; - } - default: { - mutableContent["media-base64"] = mediaUrl; - // https://stackoverflow.com/questions/57976898/how-to-get-mime-type-from-base-64-string - switch (true) { - case mediaUrl.startsWith("CiVQREYt"): - case mediaUrl.startsWith("JVBERi0"): - mutableContent["media-base64-mime"] = "application/pdf"; - break; - case mediaUrl.startsWith("R0lGODdh"): - case mediaUrl.startsWith("R0lGODlh"): - mutableContent["media-base64-mime"] = "image/gif"; - break; - case mediaUrl.startsWith("iVBORw0KGgo"): - mutableContent["media-base64-mime"] = "image/png"; - break; - case mediaUrl.startsWith("/9j/"): - mutableContent["media-base64-mime"] = "image/jpg"; - break; - case mediaUrl.startsWith("Qk02U"): - mutableContent["media-base64-mime"] = "image/bmp"; - break; - } - break; - } - } - } - if (content["auto-dismiss"]) mutableContent["auto-dismiss"] = content["auto-dismiss"]; - if (content.sound) mutableContent.sound = content.sound; - break; - } - case "Loon": { - if (openUrl) mutableContent.openUrl = openUrl; - if (mediaUrl?.startsWith("http")) mutableContent.mediaUrl = mediaUrl; - break; - } - case "Quantumult X": { - if (openUrl) mutableContent["open-url"] = openUrl; - if (mediaUrl?.startsWith("http")) mutableContent["media-url"] = mediaUrl; - if (copyUrl) mutableContent["update-pasteboard"] = copyUrl; - break; - } - case "Node.js": - break; - } - break; - } - default: - Console.error(`不支持的通知参数类型: ${typeof content}`, ""); - break; - } - return mutableContent; -}; diff --git a/lib/runScript.mjs b/lib/runScript.mjs deleted file mode 100644 index ec7aeeb..0000000 --- a/lib/runScript.mjs +++ /dev/null @@ -1,27 +0,0 @@ -import { Console } from "../polyfill/Console.mjs"; -import { fetch } from "../polyfill/fetch.mjs"; -import { Lodash as _ } from "../polyfill/Lodash.mjs"; -import { Storage } from "../polyfill/Storage.mjs"; - -export async function runScript(script, runOpts) { - let httpapi = Storage.getItem("@chavy_boxjs_userCfgs.httpapi"); - httpapi = httpapi?.replace?.(/\n/g, "")?.trim(); - let httpapi_timeout = Storage.getItem("@chavy_boxjs_userCfgs.httpapi_timeout"); - httpapi_timeout = httpapi_timeout * 1 ?? 20; - httpapi_timeout = runOpts?.timeout ?? httpapi_timeout; - const [password, address] = httpapi.split("@"); - const request = { - url: `http://${address}/v1/scripting/evaluate`, - body: { - script_text: script, - mock_type: "cron", - timeout: httpapi_timeout, - }, - headers: { "X-Key": password, Accept: "*/*" }, - timeout: httpapi_timeout, - }; - await fetch(request).then( - response => response.body, - error => Console.error(error), - ); -} diff --git a/lib/time.mjs b/lib/time.mjs deleted file mode 100644 index 9272670..0000000 --- a/lib/time.mjs +++ /dev/null @@ -1,30 +0,0 @@ -/** - * time - * 时间格式化 - * [version of ISO8601]{@link https://262.ecma-international.org/5.1/#sec-15.9.1.15} - * 示例:time("yyyy-MM-dd qq HH:mm:ss.S") YYYY-MM-DDTHH:mm:ss.sssZ - * :time("yyyyMMddHHmmssS") - * YY:年 MM:月 dd:日 S:季 HH:时 m:分 ss:秒 sss:毫秒 Z:时区 - * 其中y可选0-4位占位符、S可选0-1位占位符,其余可选0-2位占位符 - * @param {string} format 格式化参数 - * @param {number} ts 可选: 根据指定时间戳返回格式化日期 - * - */ -export function time(format, ts) { - const date = ts ? new Date(ts) : new Date(); - const Time = { - YY: date.getFullYear().toString().substring(3), - yyyy: date.getFullYear().toString(), - MM: (date.getMonth() + 1).toString().padStart(2, "0"), - dd: date.getDate().toString().padStart(2, "0"), - HH: date.getHours().toString().padStart(2, "0"), - mm: date.getMinutes().toString().padStart(2, "0"), - sss: date.getMilliseconds().toString().padStart(3, "0"), - ss: date.getSeconds().toString().padStart(2, "0"), - S: `${Math.floor(date.getMonth() / 3) + 1}`, - }; - for (const [key, value] of Object.entries(Time)) { - format = format.replace(key, value); - } - return format; -} diff --git a/lib/wait.mjs b/lib/wait.mjs deleted file mode 100644 index 260e674..0000000 --- a/lib/wait.mjs +++ /dev/null @@ -1,10 +0,0 @@ -/** - * wait - * - * @export - * @param {number} [delay=1000] - * @returns {Promise} - */ -export function wait(delay = 1000) { - return new Promise(resolve => setTimeout(resolve, delay)); -} diff --git a/package-lock.json b/package-lock.json index c7860e7..ac9cbba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,37 +1,843 @@ { - "name": "@nsnanocat/util", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "@nsnanocat/util", - "license": "Apache-2.0", - "dependencies": { - "pako": "^2.1.0" - }, - "devDependencies": { - "typescript": "^5.6.3" - } - }, - "node_modules/pako": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", - "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", - "license": "(MIT AND Zlib)" - }, - "node_modules/typescript": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", - "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - } - } + "name": "@nsnanocat/util", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@nsnanocat/util", + "license": "Apache-2.0", + "dependencies": { + "type-fest": "^4.31.0" + }, + "devDependencies": { + "@biomejs/biome": "^1.9.2", + "@types/node": "^22.10.2", + "fetch-cookie": "^3.1.0", + "node-fetch": "^3.3.2", + "rimraf": "^6.0.1", + "typescript": "^5.6.3" + }, + "optionalDependencies": { + "fetch-cookie": "^3.1.0", + "node-fetch": "^3.3.2" + } + }, + "node_modules/@biomejs/biome": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.9.4.tgz", + "integrity": "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==", + "dev": true, + "hasInstallScript": true, + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "1.9.4", + "@biomejs/cli-darwin-x64": "1.9.4", + "@biomejs/cli-linux-arm64": "1.9.4", + "@biomejs/cli-linux-arm64-musl": "1.9.4", + "@biomejs/cli-linux-x64": "1.9.4", + "@biomejs/cli-linux-x64-musl": "1.9.4", + "@biomejs/cli-win32-arm64": "1.9.4", + "@biomejs/cli-win32-x64": "1.9.4" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.9.4.tgz", + "integrity": "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.9.4.tgz", + "integrity": "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.9.4.tgz", + "integrity": "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.9.4.tgz", + "integrity": "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.9.4.tgz", + "integrity": "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.9.4.tgz", + "integrity": "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.9.4.tgz", + "integrity": "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.9.4.tgz", + "integrity": "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@types/node": { + "version": "22.10.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", + "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", + "dev": true, + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/fetch-cookie": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fetch-cookie/-/fetch-cookie-3.1.0.tgz", + "integrity": "sha512-s/XhhreJpqH0ftkGVcQt8JE9bqk+zRn4jF5mPJXWZeQMCI5odV9K+wEWYbnzFPHgQZlvPSMjS4n4yawWE8RINw==", + "dev": true, + "dependencies": { + "set-cookie-parser": "^2.4.8", + "tough-cookie": "^5.0.0" + } + }, + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dev": true, + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/glob": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.1.tgz", + "integrity": "sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^4.0.1", + "minimatch": "^10.0.0", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/jackspeak": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.2.tgz", + "integrity": "sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dev": true, + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "dev": true, + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.2.tgz", + "integrity": "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==", + "dev": true, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/rimraf": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", + "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==", + "dev": true, + "dependencies": { + "glob": "^11.0.0", + "package-json-from-dist": "^1.0.0" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "dev": true + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/tldts": { + "version": "6.1.70", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.70.tgz", + "integrity": "sha512-/W1YVgYVJd9ZDjey5NXadNh0mJXkiUMUue9Zebd0vpdo1sU+H4zFFTaJ1RKD4N6KFoHfcXy6l+Vu7bh+bdWCzA==", + "dev": true, + "dependencies": { + "tldts-core": "^6.1.70" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.70", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.70.tgz", + "integrity": "sha512-RNnIXDB1FD4T9cpQRErEqw6ZpjLlGdMOitdV+0xtbsnwr4YFka1zpc7D4KD+aAn8oSG5JyFrdasZTE04qDE9Yg==", + "dev": true + }, + "node_modules/tough-cookie": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.0.0.tgz", + "integrity": "sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==", + "dev": true, + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/type-fest": { + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.31.0.tgz", + "integrity": "sha512-yCxltHW07Nkhv/1F6wWBr8kz+5BGMfP+RbRSYFnegVb0qV/UMT0G0ElBloPVerqn4M2ZV80Ir1FtCcYv1cT6vQ==", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + } + } } diff --git a/package.json b/package.json index 61c084c..1f90887 100644 --- a/package.json +++ b/package.json @@ -1,35 +1,47 @@ { - "name": "@nsnanocat/util", - "description": "Pure JS's util module for well-known iOS network tools", - "author": "VirgilClyne ", - "homepage": "https://NSNanoCat.github.io/util", - "keywords": [ - "loon", - "quantumult", - "surge", - "shadowrocket", - "stash", - "egern" - ], - "license": "Apache-2.0", - "bugs": "https://github.com/NSNanoCat/util/issues", - "main": "index.js", - "type": "module", - "scripts": { - "tsc:build": "npx tsc", - "test": "exit 0" - }, - "repository": { - "type": "git", - "url": "https://github.com/NSNanoCat/util.git" - }, - "files": [ - "index.js", - "lib", - "polyfill", - "getStorage.mjs" - ], - "devDependencies": { - "typescript": "^5.6.3" - } + "name": "@nsnanocat/util", + "description": "Pure JS's util module for well-known iOS network tools", + "author": "VirgilClyne ", + "homepage": "https://NSNanoCat.github.io/util", + "keywords": [ + "loon", + "quantumult", + "surge", + "shadowrocket", + "stash", + "egern" + ], + "license": "Apache-2.0", + "bugs": "https://github.com/NSNanoCat/util/issues", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "scripts": { + "prebuild": "rimraf dist", + "build": "tsc", + "check": "biome check --write", + "format": "biome format --write", + "prepublishOnly": "npm run build" + }, + "repository": { + "type": "git", + "url": "https://github.com/NSNanoCat/util.git" + }, + "files": [ + "dist" + ], + "dependencies": { + "type-fest": "^4.31.0" + }, + "devDependencies": { + "@biomejs/biome": "^1.9.2", + "@types/node": "^22.10.2", + "fetch-cookie": "^3.1.0", + "node-fetch": "^3.3.2", + "rimraf": "^6.0.1", + "typescript": "^5.6.3" + }, + "optionalDependencies": { + "fetch-cookie": "^3.1.0", + "node-fetch": "^3.3.2" + } } diff --git a/polyfill/Console.mjs b/polyfill/Console.mjs deleted file mode 100644 index 9affeb6..0000000 --- a/polyfill/Console.mjs +++ /dev/null @@ -1,175 +0,0 @@ -import { $app } from "../lib/app.mjs"; - -export class Console { - static #counts = new Map([]); - static #groups = []; - static #times = new Map([]); - - static clear = () => {}; - - static count = (label = "default") => { - switch (Console.#counts.has(label)) { - case true: - Console.#counts.set(label, Console.#counts.get(label) + 1); - break; - case false: - Console.#counts.set(label, 0); - break; - } - Console.log(`${label}: ${Console.#counts.get(label)}`); - }; - - static countReset = (label = "default") => { - switch (Console.#counts.has(label)) { - case true: - Console.#counts.set(label, 0); - Console.log(`${label}: ${Console.#counts.get(label)}`); - break; - case false: - Console.warn(`Counter "${label}" doesn’t exist`); - break; - } - }; - - static debug = (...msg) => { - if (Console.#level < 4) return; - msg = msg.map(m => `🅱️ ${m}`); - Console.log(...msg); - }; - - static error(...msg) { - if (Console.#level < 1) return; - switch ($app) { - case "Surge": - case "Loon": - case "Stash": - case "Egern": - case "Shadowrocket": - case "Quantumult X": - default: - msg = msg.map(m => `❌ ${m}`); - break; - case "Node.js": - msg = msg.map(m => `❌ ${m.stack}`); - break; - } - Console.log(...msg); - } - - static exception = (...msg) => Console.error(...msg); - - static group = label => Console.#groups.unshift(label); - - static groupEnd = () => Console.#groups.shift(); - - static info(...msg) { - if (Console.#level < 3) return; - msg = msg.map(m => `ℹ️ ${m}`); - Console.log(...msg); - } - - static #level = 3; - - static get logLevel() { - switch (Console.#level) { - case 0: - return "OFF"; - case 1: - return "ERROR"; - case 2: - return "WARN"; - case 3: - default: - return "INFO"; - case 4: - return "DEBUG"; - case 5: - return "ALL"; - } - } - - static set logLevel(level) { - switch (typeof level) { - case "string": - level = level.toLowerCase(); - break; - case "number": - break; - case "undefined": - default: - level = "warn"; - break; - } - switch (level) { - case 0: - case "off": - Console.#level = 0; - break; - case 1: - case "error": - Console.#level = 1; - break; - case 2: - case "warn": - case "warning": - default: - Console.#level = 2; - break; - case 3: - case "info": - Console.#level = 3; - break; - case 4: - case "debug": - Console.#level = 4; - break; - case 5: - case "all": - Console.#level = 5; - break; - } - } - - static log = (...msg) => { - if (Console.#level === 0) return; - msg = msg.map(log => { - switch (typeof log) { - case "object": - log = JSON.stringify(log); - break; - case "bigint": - case "number": - case "boolean": - case "string": - log = log.toString(); - break; - case "undefined": - default: - break; - } - return log; - }); - Console.#groups.forEach(group => { - msg = msg.map(log => ` ${log}`); - msg.unshift(`▼ ${group}:`); - }); - msg = ["", ...msg]; - console.log(msg.join("\n")); - }; - - static time = (label = "default") => Console.#times.set(label, Date.now()); - - static timeEnd = (label = "default") => Console.#times.delete(label); - - static timeLog = (label = "default") => { - const time = Console.#times.get(label); - if (time) Console.log(`${label}: ${Date.now() - time}ms`); - else Console.warn(`Timer "${label}" doesn’t exist`); - }; - - static warn(...msg) { - if (Console.#level < 2) return; - msg = msg.map(m => `⚠️ ${m}`); - Console.log(...msg); - } -} diff --git a/polyfill/Lodash.mjs b/polyfill/Lodash.mjs deleted file mode 100644 index 1e2f71e..0000000 --- a/polyfill/Lodash.mjs +++ /dev/null @@ -1,72 +0,0 @@ -/* https://www.lodashjs.com */ -export class Lodash { - static escape(string) { - const map = { - "&": "&", - "<": "<", - ">": ">", - '"': """, - "'": "'", - }; - return string.replace(/[&<>"']/g, m => map[m]); - } - - static get(object = {}, path = "", defaultValue = undefined) { - // translate array case to dot case, then split with . - // a[0].b -> a.0.b -> ['a', '0', 'b'] - if (!Array.isArray(path)) path = Lodash.toPath(path); - - const result = path.reduce((previousValue, currentValue) => { - return Object(previousValue)[currentValue]; // null undefined get attribute will throwError, Object() can return a object - }, object); - return result === undefined ? defaultValue : result; - } - - static omit(object = {}, paths = []) { - if (!Array.isArray(paths)) paths = [paths.toString()]; - paths.forEach(path => Lodash.unset(object, path)); - return object; - } - - static pick(object = {}, paths = []) { - if (!Array.isArray(paths)) paths = [paths.toString()]; - const filteredEntries = Object.entries(object).filter(([key, value]) => paths.includes(key)); - return Object.fromEntries(filteredEntries); - } - - static set(object, path, value) { - if (!Array.isArray(path)) path = Lodash.toPath(path); - path.slice(0, -1).reduce((previousValue, currentValue, currentIndex) => (Object(previousValue[currentValue]) === previousValue[currentValue] ? previousValue[currentValue] : (previousValue[currentValue] = /^\d+$/.test(path[currentIndex + 1]) ? [] : {})), object)[path[path.length - 1]] = value; - return object; - } - - static toPath(value) { - return value - .replace(/\[(\d+)\]/g, ".$1") - .split(".") - .filter(Boolean); - } - - static unescape(string) { - const map = { - "&": "&", - "<": "<", - ">": ">", - """: '"', - "'": "'", - }; - return string.replace(/&|<|>|"|'/g, m => map[m]); - } - - static unset(object = {}, path = "") { - if (!Array.isArray(path)) path = Lodash.toPath(path); - const result = path.reduce((previousValue, currentValue, currentIndex) => { - if (currentIndex === path.length - 1) { - delete previousValue[currentValue]; - return true; - } - return Object(previousValue)[currentValue]; - }, object); - return result; - } -} diff --git a/polyfill/StatusTexts.mjs b/polyfill/StatusTexts.mjs deleted file mode 100644 index 26a3d71..0000000 --- a/polyfill/StatusTexts.mjs +++ /dev/null @@ -1,62 +0,0 @@ -export const StatusTexts = { - 100: "Continue", - 101: "Switching Protocols", - 102: "Processing", - 103: "Early Hints", - 200: "OK", - 201: "Created", - 202: "Accepted", - 203: "Non-Authoritative Information", - 204: "No Content", - 205: "Reset Content", - 206: "Partial Content", - 207: "Multi-Status", - 208: "Already Reported", - 226: "IM Used", - 300: "Multiple Choices", - 301: "Moved Permanently", - 302: "Found", - 304: "Not Modified", - 307: "Temporary Redirect", - 308: "Permanent Redirect", - 400: "Bad Request", - 401: "Unauthorized", - 402: "Payment Required", - 403: "Forbidden", - 404: "Not Found", - 405: "Method Not Allowed", - 406: "Not Acceptable", - 407: "Proxy Authentication Required", - 408: "Request Timeout", - 409: "Conflict", - 410: "Gone", - 411: "Length Required", - 412: "Precondition Failed", - 413: "Content Too Large", - 414: "URI Too Long", - 415: "Unsupported Media Type", - 416: "Range Not Satisfiable", - 417: "Expectation Failed", - 418: "I'm a teapot", - 421: "Misdirected Request", - 422: "Unprocessable Entity", - 423: "Locked", - 424: "Failed Dependency", - 425: "Too Early", - 426: "Upgrade Required", - 428: "Precondition Required", - 429: "Too Many Requests", - 431: "Request Header Fields Too Large", - 451: "Unavailable For Legal Reasons", - 500: "Internal Server Error", - 501: "Not Implemented", - 502: "Bad Gateway", - 503: "Service Unavailable", - 504: "Gateway Timeout", - 505: "HTTP Version Not Supported", - 506: "Variant Also Negotiates", - 507: "Insufficient Storage", - 508: "Loop Detected", - 510: "Not Extended", - 511: "Network Authentication Required", -}; diff --git a/polyfill/Storage.mjs b/polyfill/Storage.mjs deleted file mode 100644 index 116d70a..0000000 --- a/polyfill/Storage.mjs +++ /dev/null @@ -1,257 +0,0 @@ -import { $app } from "../lib/app.mjs"; -import { Lodash as _ } from "./Lodash.mjs"; - -/** - * Storage - * - * @link https://developer.mozilla.org/zh-CN/docs/Web/API/Storage/setItem - * @export - * @class Storage - * @typedef {Storage} - */ -export class Storage { - /** - * data - * - * @static - * @type {file} - */ - static data = null; - static dataFile = "box.dat"; - /** - * nameRegex - * - * @static - * @type {regexp} - */ - static #nameRegex = /^@(?[^.]+)(?:\.(?.*))?$/; - - /** - * getItem - * - * @static - * @param {string} keyName - * @param {*} [defaultValue] - * @returns {*} - */ - static getItem(keyName, defaultValue = null) { - let keyValue = defaultValue; - // 如果以 @ - switch (keyName.startsWith("@")) { - case true: { - const { key, path } = keyName.match(Storage.#nameRegex)?.groups; - keyName = key; - let value = Storage.getItem(keyName, {}); - if (typeof value !== "object") value = {}; - keyValue = _.get(value, path); - try { - keyValue = JSON.parse(keyValue); - } catch (e) {} - break; - } - default: - switch ($app) { - case "Surge": - case "Loon": - case "Stash": - case "Egern": - case "Shadowrocket": - keyValue = $persistentStore.read(keyName); - break; - case "Quantumult X": - keyValue = $prefs.valueForKey(keyName); - break; - case "Node.js": - Storage.data = Storage.#loaddata(Storage.dataFile); - keyValue = Storage.data?.[keyName]; - break; - default: - keyValue = Storage.data?.[keyName] || null; - break; - } - try { - keyValue = JSON.parse(keyValue); - } catch (e) { - // do nothing - } - break; - } - return keyValue ?? defaultValue; - } - - /** - * setItem - * - * @static - * @param {string} keyName - * @param {*} keyValue - * @returns {boolean} - */ - static setItem(keyName = new String(), keyValue = new String()) { - let result = false; - switch (typeof keyValue) { - case "object": - keyValue = JSON.stringify(keyValue); - break; - default: - keyValue = String(keyValue); - break; - } - switch (keyName.startsWith("@")) { - case true: { - const { key, path } = keyName.match(Storage.#nameRegex)?.groups; - keyName = key; - let value = Storage.getItem(keyName, {}); - if (typeof value !== "object") value = {}; - _.set(value, path, keyValue); - result = Storage.setItem(keyName, value); - break; - } - default: - switch ($app) { - case "Surge": - case "Loon": - case "Stash": - case "Egern": - case "Shadowrocket": - result = $persistentStore.write(keyValue, keyName); - break; - case "Quantumult X": - result = $prefs.setValueForKey(keyValue, keyName); - break; - case "Node.js": - Storage.data = Storage.#loaddata(Storage.dataFile); - Storage.data[keyName] = keyValue; - Storage.#writedata(Storage.dataFile); - result = true; - break; - default: - result = Storage.data?.[keyName] || null; - break; - } - break; - } - return result; - } - - /** - * removeItem - * - * @static - * @param {string} keyName - * @returns {boolean} - */ - static removeItem(keyName) { - let result = false; - switch (keyName.startsWith("@")) { - case true: { - const { key, path } = keyName.match(Storage.#nameRegex)?.groups; - keyName = key; - let value = Storage.getItem(keyName); - if (typeof value !== "object") value = {}; - keyValue = _.unset(value, path); - result = Storage.setItem(keyName, value); - break; - } - default: - switch ($app) { - case "Surge": - case "Loon": - case "Stash": - case "Egern": - case "Shadowrocket": - result = false; - break; - case "Quantumult X": - result = $prefs.removeValueForKey(keyName); - break; - case "Node.js": - result = false; - break; - default: - result = false; - break; - } - break; - } - return result; - } - - /** - * clear - * - * @static - * @returns {boolean} - */ - static clear() { - let result = false; - switch ($app) { - case "Surge": - case "Loon": - case "Stash": - case "Egern": - case "Shadowrocket": - result = false; - break; - case "Quantumult X": - result = $prefs.removeAllValues(); - break; - case "Node.js": - result = false; - break; - default: - result = false; - break; - } - return result; - } - - /** - * #loaddata - * - * @param {string} dataFile - * @returns {*} - */ - static #loaddata = dataFile => { - if ($app === "Node.js") { - this.fs = this.fs ? this.fs : require("node:fs"); - this.path = this.path ? this.path : require("node:path"); - const curDirDataFilePath = this.path.resolve(dataFile); - const rootDirDataFilePath = this.path.resolve(process.cwd(), dataFile); - const isCurDirDataFile = this.fs.existsSync(curDirDataFilePath); - const isRootDirDataFile = !isCurDirDataFile && this.fs.existsSync(rootDirDataFilePath); - if (isCurDirDataFile || isRootDirDataFile) { - const datPath = isCurDirDataFile ? curDirDataFilePath : rootDirDataFilePath; - try { - return JSON.parse(this.fs.readFileSync(datPath)); - } catch (e) { - return {}; - } - } else return {}; - } else return {}; - }; - - /** - * #writedata - * - * @param {string} [dataFile=this.dataFile] - */ - static #writedata = (dataFile = this.dataFile) => { - if ($app === "Node.js") { - this.fs = this.fs ? this.fs : require("node:fs"); - this.path = this.path ? this.path : require("node:path"); - const curDirDataFilePath = this.path.resolve(dataFile); - const rootDirDataFilePath = this.path.resolve(process.cwd(), dataFile); - const isCurDirDataFile = this.fs.existsSync(curDirDataFilePath); - const isRootDirDataFile = !isCurDirDataFile && this.fs.existsSync(rootDirDataFilePath); - const jsondata = JSON.stringify(this.data); - if (isCurDirDataFile) { - this.fs.writeFileSync(curDirDataFilePath, jsondata); - } else if (isRootDirDataFile) { - this.fs.writeFileSync(rootDirDataFilePath, jsondata); - } else { - this.fs.writeFileSync(curDirDataFilePath, jsondata); - } - } - }; -} diff --git a/polyfill/fetch.mjs b/polyfill/fetch.mjs deleted file mode 100644 index 57fa70f..0000000 --- a/polyfill/fetch.mjs +++ /dev/null @@ -1,202 +0,0 @@ -import { $app } from "../lib/app.mjs"; -import { Console } from "./Console.mjs"; -import { Lodash as _ } from "./Lodash.mjs"; -import { StatusTexts } from "./StatusTexts.mjs"; - -/** - * fetch - * - * @link https://developer.mozilla.org/zh-CN/docs/Web/API/Fetch_API - * @export - * @async - * @param {object|string} resource - * @param {object} [options] - * @returns {Promise} - */ -export async function fetch(resource, options = {}) { - // 初始化参数 - switch (typeof resource) { - case "object": - resource = { ...options, ...resource }; - break; - case "string": - resource = { ...options, url: resource }; - break; - case "undefined": - default: - throw new TypeError(`${Function.name}: 参数类型错误, resource 必须为对象或字符串`); - } - // 自动判断请求方法 - if (!resource.method) { - resource.method = "GET"; - if (resource.body ?? resource.bodyBytes) resource.method = "POST"; - } - // 移除请求头中的部分参数, 让其自动生成 - delete resource.headers?.Host; - delete resource.headers?.[":authority"]; - delete resource.headers?.["Content-Length"]; - delete resource.headers?.["content-length"]; - // 定义请求方法(小写) - const method = resource.method.toLocaleLowerCase(); - // 转换请求超时时间参数 - if (!resource.timeout) resource.timeout = 5; - if (resource.timeout) { - resource.timeout = Number.parseInt(resource.timeout, 10); - // 转换为秒,大于500视为毫秒,小于等于500视为秒 - if (resource.timeout > 500) resource.timeout = Math.round(resource.timeout / 1000); - } - // 判断平台 - switch ($app) { - case "Loon": - case "Surge": - case "Stash": - case "Egern": - case "Shadowrocket": - default: - // 转换请求参数 - if (resource.timeout) { - switch ($app) { - case "Loon": - resource.timeout = resource.timeout * 1000; - break; - case "Shadowrocket": - case "Stash": - case "Egern": - case "Surge": - default: - break; - } - } - if (resource.policy) { - switch ($app) { - case "Loon": - resource.node = resource.policy; - break; - case "Stash": - _.set(resource, "headers.X-Stash-Selected-Proxy", encodeURI(resource.policy)); - break; - case "Shadowrocket": - _.set(resource, "headers.X-Surge-Proxy", resource.policy); - break; - } - } - if (typeof resource.redirection === "boolean") resource["auto-redirect"] = resource.redirection; - // 转换请求体 - if (resource.bodyBytes && !resource.body) { - resource.body = resource.bodyBytes; - resource.bodyBytes = undefined; - } - // 判断是否请求二进制响应体 - switch ((resource.headers?.Accept || resource.headers?.accept)?.split(";")?.[0]) { - case "application/protobuf": - case "application/x-protobuf": - case "application/vnd.google.protobuf": - case "application/vnd.apple.flatbuffer": - case "application/grpc": - case "application/grpc+proto": - case "application/octet-stream": - resource["binary-mode"] = true; - break; - } - // 发送请求 - return await new Promise((resolve, reject) => { - $httpClient[method](resource, (error, response, body) => { - if (error) reject(error); - else { - response.ok = /^2\d\d$/.test(response.status); - response.statusCode = response.status; - response.statusText = StatusTexts[response.status]; - if (body) { - response.body = body; - if (resource["binary-mode"] == true) response.bodyBytes = body; - } - resolve(response); - } - }); - }); - case "Quantumult X": - // 转换请求参数 - if (resource.policy) _.set(resource, "opts.policy", resource.policy); - if (typeof resource["auto-redirect"] === "boolean") _.set(resource, "opts.redirection", resource["auto-redirect"]); - // 转换请求体 - if (resource.body instanceof ArrayBuffer) { - resource.bodyBytes = resource.body; - resource.body = undefined; - } else if (ArrayBuffer.isView(resource.body)) { - resource.bodyBytes = resource.body.buffer.slice(resource.body.byteOffset, resource.body.byteLength + resource.body.byteOffset); - resource.body = undefined; - } else if (resource.body) resource.bodyBytes = undefined; - // 发送请求 - return Promise.race([ - await $task.fetch(resource).then( - response => { - response.ok = /^2\d\d$/.test(response.statusCode); - response.status = response.statusCode; - response.statusText = StatusTexts[response.status]; - switch ((response.headers?.["Content-Type"] ?? response.headers?.["content-type"])?.split(";")?.[0]) { - case "application/protobuf": - case "application/x-protobuf": - case "application/vnd.google.protobuf": - case "application/vnd.apple.flatbuffer": - case "application/grpc": - case "application/grpc+proto": - case "application/octet-stream": - response.body = response.bodyBytes; - break; - case undefined: - default: - break; - } - response.bodyBytes = undefined; - return response; - }, - reason => Promise.reject(reason.error), - ), - new Promise((resolve, reject) => { - setTimeout(() => { - reject(new Error(`${Function.name}: 请求超时, 请检查网络后重试`)); - }, resource.timeout); - }), - ]); - case "Node.js": { - const nodeFetch = globalThis.fetch ? globalThis.fetch : require("node-fetch"); - const fetchCookie = globalThis.fetchCookie ? globalThis.fetchCookie : require("fetch-cookie").default; - const fetch = fetchCookie(nodeFetch); - // 转换请求参数 - resource.timeout = resource.timeout * 1000; - resource.redirect = resource.redirection ? "follow" : "manual"; - const { url, ...options } = resource; - // 发送请求 - return Promise.race([ - await fetch(url, options) - .then(async response => { - const bodyBytes = await response.arrayBuffer(); - let headers; - try { - headers = response.headers.raw(); - } catch { - headers = Array.from(response.headers.entries()).reduce((acc, [key, value]) => { - acc[key] = acc[key] ? [...acc[key], value] : [value]; - return acc; - }, {}); - } - return { - ok: response.ok ?? /^2\d\d$/.test(response.status), - status: response.status, - statusCode: response.status, - statusText: response.statusText, - body: new TextDecoder("utf-8").decode(bodyBytes), - bodyBytes: bodyBytes, - headers: Object.fromEntries(Object.entries(headers).map(([key, value]) => [key, key.toLowerCase() !== "set-cookie" ? value.toString() : value])), - }; - }) - .catch(error => Promise.reject(error.message)), - new Promise((resolve, reject) => { - setTimeout(() => { - reject(new Error(`${Function.name}: 请求超时, 请检查网络后重试`)); - }, resource.timeout); - }), - ]); - } - } -} diff --git a/polyfill/index.js b/polyfill/index.js deleted file mode 100644 index a4dc2d5..0000000 --- a/polyfill/index.js +++ /dev/null @@ -1,5 +0,0 @@ -export * from "./Console.mjs"; -export * from "./fetch.mjs"; -export * from "./Lodash.mjs"; -export * from "./StatusTexts.mjs"; -export * from "./Storage.mjs"; diff --git a/src/env.d.ts b/src/env.d.ts new file mode 100644 index 0000000..5b271d6 --- /dev/null +++ b/src/env.d.ts @@ -0,0 +1,11 @@ +interface Environment { + 'loon-version'?: string; + 'surge-version'?: string; + 'stash-version'?: string; +} + +// biome-ignore lint/suspicious/useNamespaceKeyword: This is a global declaration file +declare module globalThis { + // biome-ignore lint/style/noVar: This is a global declaration file + var $environment: Environment; +} diff --git a/src/getStorage.mts b/src/getStorage.mts new file mode 100644 index 0000000..d754872 --- /dev/null +++ b/src/getStorage.mts @@ -0,0 +1,107 @@ +import { set } from './polyfill/Lodash.mjs'; +import { Storage } from './polyfill/Storage.mjs'; + +type Database = Record; + +interface StoreType< + Settings extends Record = Record, + Configs extends Record = Record, + Caches extends Record = Record, +> { + Settings: Settings; + Configs: Configs; + Caches: Caches; +} + +declare const $argument: string | object; + +export function getStorage< + Settings extends Record = Record, + Configs extends Record = Record, + Caches extends Record = Record, +>(key: string, names: string | string[], database: Database): StoreType { + const nameList = Array.isArray(names) ? names : [names]; + + const Store = { + Settings: database?.Default?.Settings || {}, + Configs: database?.Default?.Configs || {}, + Caches: {}, + } as StoreType; + + nameList.forEach((name) => { + Store.Settings = { ...Store.Settings, ...database?.[name]?.Settings }; + Store.Configs = { ...Store.Configs, ...database?.[name]?.Configs }; + }); + + if (typeof $argument === 'string') { + const parsedArgument = Object.fromEntries( + $argument.split('&').map((item) => item.split('=', 2).map((i) => i.replace(/\"/g, ''))), + ); + Object.keys(parsedArgument).forEach((key) => set(Store.Settings, key, parsedArgument[key])); + } else if (typeof $argument === 'object') { + Object.keys($argument).forEach((key) => set(Store.Settings, key, $argument[key as keyof typeof $argument])); + } + + /***************** BoxJs *****************/ + // 包装为局部变量,用完释放内存 + // BoxJs的清空操作返回假值空字符串, 逻辑或操作符会在左侧操作数为假值时返回右侧操作数。 + const BoxJs = Storage.getItem(key); + if (BoxJs) { + //Console.debug("BoxJs", `BoxJs类型: ${typeof BoxJs}`, `BoxJs内容: ${JSON.stringify(BoxJs || {})}`); + nameList.forEach((name) => { + const boxSettings = BoxJs?.[name]?.Settings; + const boxCaches = BoxJs?.[name]?.Caches; + + if (typeof boxSettings === 'string') { + BoxJs[name].Settings = JSON.parse(boxSettings || '{}'); + } + if (boxSettings) { + Store.Settings = { ...Store.Settings, ...BoxJs[name].Settings }; + } + + if (typeof boxCaches === 'string') { + BoxJs[name].Caches = JSON.parse(boxCaches || '{}'); + } + if (boxCaches) { + Store.Caches = { ...Store.Caches, ...BoxJs[name].Caches }; + } + }); + } + + /***************** traverseObject *****************/ + traverseObject(Store.Settings, (key, value) => { + //Console.debug("☑️ traverseObject", `${key}: ${typeof value}`, `${key}: ${JSON.stringify(value)}`); + let transformedValue = value; + if (transformedValue === 'true' || transformedValue === 'false') + transformedValue = JSON.parse(transformedValue); // 字符串转Boolean + else if (typeof transformedValue === 'string') { + if (transformedValue.includes(',')) + transformedValue = transformedValue.split(',').map((item) => string2number(item)); // 字符串转数组转数字 + else transformedValue = string2number(transformedValue); // 字符串转数字 + } + return transformedValue; + }); + + return Store; +} + +/** + * Recursively traverse and transform object properties. + */ +function traverseObject(obj: Record, callback: (key: string, value: any) => any): Record { + Object.entries(obj).forEach(([key, value]) => { + if (value && typeof value === 'object') { + obj[key] = traverseObject(value, callback); + } else { + obj[key] = callback(key, value); + } + }); + return obj; +} + +/** + * Convert string to number if applicable. + */ +function string2number(value: string): number | string { + return /^\d+$/.test(value) ? Number(value) : value; +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..92bb0fe --- /dev/null +++ b/src/index.ts @@ -0,0 +1,3 @@ +export * from './lib/index.mjs'; +export * from './polyfill/index.mjs'; +export * from './getStorage.mjs'; diff --git a/src/lib/app.mts b/src/lib/app.mts new file mode 100644 index 0000000..dfb2c88 --- /dev/null +++ b/src/lib/app.mts @@ -0,0 +1,29 @@ +/** + * Current app name + */ +export const $app: 'Quantumult X' | 'Loon' | 'Shadowrocket' | 'Egern' | 'Surge' | 'Stash' | 'Node.js' | undefined = + (() => { + if ('$task' in globalThis) { + return 'Quantumult X'; + } + if ('$loon' in globalThis) { + return 'Loon'; + } + if ('$rocket' in globalThis) { + return 'Shadowrocket'; + } + if ('Egern' in globalThis) { + return 'Egern'; + } + if ('$environment' in globalThis) { + if (globalThis.$environment['surge-version']) { + return 'Surge'; + } + if (globalThis.$environment['stash-version']) { + return 'Stash'; + } + } + if (typeof module !== 'undefined') { + return 'Node.js'; + } + })(); diff --git a/src/lib/done.mts b/src/lib/done.mts new file mode 100644 index 0000000..985e9e4 --- /dev/null +++ b/src/lib/done.mts @@ -0,0 +1,114 @@ +import { pick, set } from '../polyfill/Lodash.mjs'; +import { Console } from '../polyfill/Console.mjs'; +import { StatusTexts } from '../polyfill/StatusTexts.mjs'; +import { $app } from './app.mjs'; + +interface DoneObject { + status?: number | string; + url?: string; + headers?: Record; + body?: ArrayBuffer | ArrayBufferView | string; + bodyBytes?: ArrayBuffer; + policy?: string; + node?: string; + opts?: { + policy?: string; + }; +} + +declare const $done: (object: DoneObject) => void; +declare const $script: { + startTime: number; +}; + +const transformQuantumultXBody = (object: DoneObject): Partial => { + if (object.body instanceof ArrayBuffer) { + return { bodyBytes: object.body, body: undefined }; + } + if (ArrayBuffer.isView(object.body)) { + return { + bodyBytes: object.body.buffer.slice(object.body.byteOffset, object.body.byteOffset + object.body.byteLength), + body: undefined, + }; + } + return { bodyBytes: undefined }; +}; + +const transformQuantumultXStatus = (object: DoneObject): Partial => { + if (typeof object.status === 'number') { + return { + status: `HTTP/1.1 ${object.status} ${StatusTexts[object.status as keyof typeof StatusTexts]}`, + }; + } + if (typeof object.status !== 'string' && object.status !== undefined) { + throw new TypeError(`${done.name}: 参数类型错误, status 必须为数字或字符串`); + } + return {}; +}; + +const handleDoneFactory = (startTime?: number) => { + return (result: DoneObject) => { + Console.log('🚩 执行结束!', startTime ? `🕛 ${((Date.now() - startTime) / 1000).toFixed(3)} 秒` : undefined); + $done(result); + }; +}; + +/** + * Complete the script execution + */ +export function done(object: DoneObject = {}): void { + let startTime = $script?.startTime; + if ($app === 'Surge') { + startTime *= 1000; + } + + const handleDone = handleDoneFactory(startTime); + + switch ($app) { + case 'Surge': + if (object.policy) { + set(object, 'headers.X-Surge-Policy', object.policy); + } + handleDone(object); + break; + + case 'Loon': + if (object.policy) { + object.node = object.policy; + } + handleDone(object); + break; + + case 'Stash': + if (object.policy) { + set(object, 'headers.X-Stash-Selected-Proxy', encodeURI(object.policy)); + } + handleDone(object); + break; + + case 'Egern': + handleDone(object); + break; + + case 'Shadowrocket': + handleDone(object); + break; + + case 'Quantumult X': { + const transformedObject = { + ...pick(object, ['status', 'url', 'headers', 'body', 'bodyBytes']), + ...transformQuantumultXStatus(object), + ...transformQuantumultXBody(object), + }; + if (object.policy) { + set(transformedObject, 'opts.policy', object.policy); + } + handleDone(transformedObject); + break; + } + case 'Node.js': + default: + Console.log('🚩 执行结束!'); + process.exit(1); + } +} diff --git a/src/lib/environment.mts b/src/lib/environment.mts new file mode 100644 index 0000000..80323aa --- /dev/null +++ b/src/lib/environment.mts @@ -0,0 +1,47 @@ +import { $app } from './app.mjs'; + +interface Environment extends globalThis.Environment { + app?: string; + device?: string; + ios?: string; + [key: string]: string | undefined; +} + +declare const $loon: string; +declare const $environment: Environment; + +export function environment(): Environment { + switch ($app) { + case 'Surge': + $environment.app = 'Surge'; + return $environment; + case 'Stash': + $environment.app = 'Stash'; + return $environment; + case 'Egern': + $environment.app = 'Egern'; + return $environment; + case 'Loon': { + const environment = $loon.split(' '); + return { + device: environment[0], + ios: environment[1], + 'loon-version': environment[2], + app: 'Loon', + }; + } + case 'Quantumult X': + return { + app: 'Quantumult X', + }; + case 'Node.js': + return { + ...process.env, + app: 'Node.js', + }; + default: + return {}; + } +} + +// export const $environment = environment(); diff --git a/src/lib/index.mts b/src/lib/index.mts new file mode 100644 index 0000000..3618c51 --- /dev/null +++ b/src/lib/index.mts @@ -0,0 +1,5 @@ +export * from './app.mjs'; +export * from './done.mjs'; +export * from './notification.mjs'; +export * from './time.mjs'; +export * from './wait.mjs'; diff --git a/src/lib/notification.mts b/src/lib/notification.mts new file mode 100644 index 0000000..2511ca8 --- /dev/null +++ b/src/lib/notification.mts @@ -0,0 +1,148 @@ +import { Console } from '../polyfill/Console.mjs'; +import { $app } from './app.mjs'; + +interface NotificationContent { + open?: string; + 'open-url'?: string; + url?: string; + openUrl?: string; + copy?: string; + 'update-pasteboard'?: string; + updatePasteboard?: string; + media?: string; + 'media-url'?: string; + mediaUrl?: string; + 'auto-dismiss'?: boolean; + sound?: string; + mime?: string; +} + +declare const $notify: (title: string, subtitle: string, body: string, content: NotificationContent) => void; +declare const $notification: { + post: (title: string, subtitle: string, body: string, content: NotificationContent) => void; +}; + +/** + * 系统通知 + */ +export function notification( + title = `ℹ️ ${$app} 通知`, + subtitle = '', + body = '', + content: string | number | boolean | NotificationContent = {}, +): void { + const mutableContent = getMutableContent(content); + + switch ($app) { + case 'Quantumult X': + $notify(title, subtitle, body, mutableContent); + break; + case 'Node.js': + break; + default: + $notification.post(title, subtitle, body, mutableContent); + break; + } + Console.group('📣 系统通知'); + Console.log(title, subtitle, body, JSON.stringify(mutableContent, null, 2)); + Console.groupEnd(); +} + +function getMutableContent(content: string | number | boolean | NotificationContent): Record { + const mutableContent: Record = {}; + + switch (typeof content) { + case 'string': + case 'number': + case 'boolean': + assignSimpleContent(mutableContent, content); + break; + + case 'object': + if (content) { + assignObjectContent(mutableContent, content); + } + break; + + default: + Console.error(`不支持的通知参数类型: ${typeof content}`); + break; + } + + return mutableContent; +} + +function assignSimpleContent(mutableContent: Record, content: string | number | boolean): void { + switch ($app) { + case 'Quantumult X': + mutableContent['open-url'] = content; + break; + case 'Loon': + case 'Shadowrocket': + mutableContent.openUrl = content; + break; + default: + mutableContent.url = content; + break; + } +} + +function assignObjectContent(mutableContent: Record, content: NotificationContent): void { + const openUrl = content.open || content['open-url'] || content.url || content.openUrl; + const copyUrl = content.copy || content['update-pasteboard'] || content.updatePasteboard; + const mediaUrl = content.media || content['media-url'] || content.mediaUrl; + + switch ($app) { + case 'Quantumult X': + if (openUrl) mutableContent['open-url'] = openUrl; + if (mediaUrl?.startsWith('http')) mutableContent['media-url'] = mediaUrl; + if (copyUrl) mutableContent['update-pasteboard'] = copyUrl; + break; + + case 'Loon': + if (openUrl) mutableContent.openUrl = openUrl; + if (mediaUrl?.startsWith('http')) mutableContent.mediaUrl = mediaUrl; + break; + + default: + if (openUrl) { + mutableContent.action = 'open-url'; + mutableContent.url = openUrl; + } + if (copyUrl) { + mutableContent.action = 'clipboard'; + mutableContent.text = copyUrl; + } + if (mediaUrl) { + handleMediaContent(mutableContent, mediaUrl, content.mime); + } + if (content['auto-dismiss']) mutableContent['auto-dismiss'] = content['auto-dismiss']; + if (content.sound) mutableContent.sound = content.sound; + break; + } +} + +function handleMediaContent(mutableContent: Record, mediaUrl: string, mime?: string): void { + if (mediaUrl.startsWith('http')) { + mutableContent['media-url'] = mediaUrl; + } else if (mediaUrl.startsWith('data:')) { + const base64RegExp = /^data:(?\w+\/\w+);base64,(?.+)/; + const match = mediaUrl.match(base64RegExp); + if (match?.groups) { + mutableContent['media-base64'] = match.groups.Base64; + mutableContent['media-base64-mime'] = mime || match.groups.MIME; + } + } else { + mutableContent['media-base64'] = mediaUrl; + mutableContent['media-base64-mime'] = detectMimeType(mediaUrl); + } +} + +function detectMimeType(base64: string): string { + if (base64.startsWith('JVBERi0')) return 'application/pdf'; + if (base64.startsWith('R0lGODdh') || base64.startsWith('R0lGODlh')) return 'image/gif'; + if (base64.startsWith('iVBORw0KGgo')) return 'image/png'; + if (base64.startsWith('/9j/')) return 'image/jpeg'; + if (base64.startsWith('Qk02U')) return 'image/bmp'; + return 'application/octet-stream'; +} diff --git a/src/lib/runScript.mts b/src/lib/runScript.mts new file mode 100644 index 0000000..9b6f16e --- /dev/null +++ b/src/lib/runScript.mts @@ -0,0 +1,49 @@ +import { Console } from '../polyfill/Console.mjs'; +import { Storage } from '../polyfill/Storage.mjs'; +import { fetch } from '../polyfill/fetch.mjs'; + +export async function runScript(script: string, runOpts: { timeout?: number } = {}) { + try { + // 获取 httpapi 配置 + const httpapi = Storage.getItem('@chavy_boxjs_userCfgs.httpapi')?.replace(/\n/g, '')?.trim(); + if (!httpapi) { + throw new Error('httpapi 配置未找到,请检查配置项!'); + } + + // 设置超时时间,优先使用参数传入的值 + const httpapiTimeoutFromConfig = Number.parseInt( + Storage.getItem('@chavy_boxjs_userCfgs.httpapi_timeout') || '20', + 10, + ); + const timeout = runOpts.timeout ?? httpapiTimeoutFromConfig; + + // 解析 httpapi 的地址和密码 + const [password, address] = httpapi.split('@'); + if (!password || !address) { + throw new Error('httpapi 配置格式错误,应为 password@address 格式!'); + } + + // 构建请求对象 + const request = { + url: `http://${address}/v1/scripting/evaluate`, + body: JSON.stringify({ + script_text: script, + mock_type: 'cron', + timeout, + }), + headers: { + 'X-Key': password, + Accept: '*/*', + }, + timeout, + }; + + // 发起请求 + const response = await fetch(request); + return response.body; // 返回响应体 + } catch (error) { + // 捕获错误并打印 + Console.error('运行脚本时发生错误:', (error as Error).message); + throw error; // 如果需要,可以重新抛出错误 + } +} diff --git a/src/lib/time.mts b/src/lib/time.mts new file mode 100644 index 0000000..0729605 --- /dev/null +++ b/src/lib/time.mts @@ -0,0 +1,31 @@ +/** + * time + * 时间格式化 + * [version of ISO8601]{@link https://262.ecma-international.org/5.1/#sec-15.9.1.15} + * 示例:time("yyyy-MM-dd qq HH:mm:ss.S") YYYY-MM-DDTHH:mm:ss.sssZ + * :time("yyyyMMddHHmmssS") + * YY:年 MM:月 dd:日 S:季 HH:时 m:分 ss:秒 sss:毫秒 Z:时区 + * 其中y可选0-4位占位符、S可选0-1位占位符,其余可选0-2位占位符 + * @param {string} format 格式化参数 + * @param {number} ts 可选: 根据指定时间戳返回格式化日期 + * + */ +export function time(format: string, ts: number): string { + const date = ts ? new Date(ts) : new Date(); + const Time = { + YY: date.getFullYear().toString().substring(3), + yyyy: date.getFullYear().toString(), + MM: (date.getMonth() + 1).toString().padStart(2, '0'), + dd: date.getDate().toString().padStart(2, '0'), + HH: date.getHours().toString().padStart(2, '0'), + mm: date.getMinutes().toString().padStart(2, '0'), + sss: date.getMilliseconds().toString().padStart(3, '0'), + ss: date.getSeconds().toString().padStart(2, '0'), + S: `${Math.floor(date.getMonth() / 3) + 1}`, + }; + let result = format; + for (const [key, value] of Object.entries(Time)) { + result = result.replace(key, value); + } + return result; +} diff --git a/src/lib/wait.mts b/src/lib/wait.mts new file mode 100644 index 0000000..ab7f13c --- /dev/null +++ b/src/lib/wait.mts @@ -0,0 +1,6 @@ +/** + * wait + */ +export function wait(delay = 1000) { + return new Promise((resolve) => setTimeout(resolve, delay)); +} diff --git a/src/polyfill/Console.mts b/src/polyfill/Console.mts new file mode 100644 index 0000000..cd44110 --- /dev/null +++ b/src/polyfill/Console.mts @@ -0,0 +1,187 @@ +import { $app } from '../lib/app.mjs'; + +class ConsoleFactory { + #counts = new Map([]); + #groups: string[] = []; + #times = new Map([]); + + clear = () => {}; + + count = (label = 'default') => { + if (this.#counts.has(label)) { + this.#counts.set(label, this.#counts.get(label) ?? 0 + 1); + } else { + this.#counts.set(label, 0); + } + this.log(`${label}: ${this.#counts.get(label)}`); + }; + + countReset = (label = 'default') => { + switch (this.#counts.has(label)) { + case true: + this.#counts.set(label, 0); + this.log(`${label}: ${this.#counts.get(label)}`); + break; + case false: + this.warn(`Counter "${label}" doesn’t exist`); + break; + } + }; + + debug = (...args: any[]) => { + if (this.#level < 4) return; + this.log(...args.map((m) => `🅱️ ${m}`)); + }; + + error(...msg: any[]) { + if (this.#level < 1) return; + switch ($app) { + case 'Surge': + case 'Loon': + case 'Stash': + case 'Egern': + case 'Shadowrocket': + case 'Quantumult X': + case 'Node.js': + this.log( + ...msg.map((m) => { + if (m instanceof Error) { + return `❌ ${m.stack}`; + } + return `❌ ${m}`; + }), + ); + break; + default: + this.log(...msg.map((m) => `❌ ${m}`)); + break; + } + } + + exception = (...msg: any[]) => this.error(...msg); + + group = (label: string) => this.#groups.unshift(label); + + groupEnd = () => this.#groups.shift(); + + info(...msg: any[]) { + if (this.#level < 3) return; + this.log(...msg.map((m) => `ℹ️ ${m}`)); + } + + #level = 3; + + get logLevel() { + switch (this.#level) { + case 0: + return 'OFF'; + case 1: + return 'ERROR'; + case 2: + return 'WARN'; + case 4: + return 'DEBUG'; + case 5: + return 'ALL'; + case 3: + default: + return 'INFO'; + } + } + + set logLevel(_level: string | number) { + let level = _level; + switch (typeof level) { + case 'string': + level = level.toLowerCase(); + break; + case 'number': + break; + case 'undefined': + default: + level = 'warn'; + break; + } + switch (level) { + case 0: + case 'off': + this.#level = 0; + break; + case 1: + case 'error': + this.#level = 1; + break; + case 3: + case 'info': + this.#level = 3; + break; + case 4: + case 'debug': + this.#level = 4; + break; + case 5: + case 'all': + this.#level = 5; + break; + case 2: + case 'warn': + case 'warning': + default: + this.#level = 2; + break; + } + } + + log = (...args: any[]) => { + if (this.#level === 0) return; + let msg = args; + msg = msg.map((item) => { + let log = item; + switch (typeof log) { + case 'object': + log = JSON.stringify(log); + break; + case 'bigint': + case 'number': + case 'boolean': + case 'string': + log = log.toString(); + break; + case 'undefined': + default: + break; + } + return log; + }); + this.#groups.forEach((group) => { + msg = msg.map((log) => ` ${log}`); + msg.unshift(`▼ ${group}:`); + }); + msg = ['', ...msg]; + console.log(msg.join('\n')); + }; + + time = (label = 'default') => this.#times.set(label, Date.now()); + + timeEnd = (label = 'default') => this.#times.delete(label); + + timeLog = (label = 'default') => { + const time = this.#times.get(label); + if (time) { + this.log(`${label}: ${Date.now() - time}ms`); + } else { + this.warn(`Timer "${label}" doesn’t exist`); + } + }; + + warn(...args: any[]) { + if (this.#level < 2) { + return; + } + let msg = args; + msg = msg.map((m) => `⚠️ ${m}`); + this.log(...msg); + } +} + +export const Console = new ConsoleFactory(); diff --git a/src/polyfill/Lodash.mts b/src/polyfill/Lodash.mts new file mode 100644 index 0000000..0ed2178 --- /dev/null +++ b/src/polyfill/Lodash.mts @@ -0,0 +1,105 @@ +import type { ToPath } from 'type-fest/source/get'; +import type { Get, Paths, PickDeep } from 'type-fest'; + +const ESCAPE_MAP = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', +} as const; + +const UNESCAPE_MAP = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + ''': "'", +} as const; + +const ESCAPE_REGEX = /[&<>"']/g; +const UNESCAPE_REGEX = /&(amp|lt|gt|quot|#39);/g; + +// biome-ignore lint/suspicious/noShadowRestrictedNames: +export const escape = (str: string) => { + if (!ESCAPE_REGEX.test(str)) return str; + + ESCAPE_REGEX.lastIndex = 0; + return str.replace(ESCAPE_REGEX, (match) => ESCAPE_MAP[match as keyof typeof ESCAPE_MAP]); +}; + +// biome-ignore lint/suspicious/noShadowRestrictedNames: +export const unescape = (str: string) => { + if (!UNESCAPE_REGEX.test(str)) return str; + + UNESCAPE_REGEX.lastIndex = 0; + return str.replace(UNESCAPE_REGEX, (match) => UNESCAPE_MAP[match as keyof typeof UNESCAPE_MAP]); +}; + +export const toPath = (value: T) => + value + .replace(/\[(\d+)\]/g, '.$1') + .split('.') + .filter(Boolean) as ToPath; + +export const get = (obj: T, path: Path, defaultValue?: Get) => { + const getPath = Array.isArray(path) ? path : toPath(path); + const result = getPath.reduce((previousValue, currentValue) => Object(previousValue)[currentValue], obj); + return result === undefined ? defaultValue : result; +}; + +export const set = (obj: T, path: Path, value: Get) => { + const setPath = (Array.isArray(path) ? path : toPath(path)) as string[]; + + setPath.slice(0, -1).reduce( + (prev, key, index) => { + if (typeof prev[key] !== 'object' || prev[key] === null) { + prev[key] = /^\d+$/.test(setPath[index + 1]) ? [] : {}; + } + return prev[key]; + }, + obj as Record, + )[setPath[setPath.length - 1]] = value; + + return obj; +}; + +export const unset = (obj: T, path: Path) => { + const unsetPath = Array.isArray(path) ? path : toPath(path); + return unsetPath.reduce((previousValue, currentValue, currentIndex) => { + if (currentIndex === path.length - 1) { + delete previousValue[currentValue as Path]; + return true; + } + return Object(previousValue)[currentValue]; + }, obj); +}; + +export const omit = & string>>(obj: T, paths: PathArray) => { + const result = { ...obj }; + paths.forEach((path) => unset(result, path)); + return result as Omit; +}; + +export const pick = & string>>(obj: T, paths: PathArray) => { + return Object.entries(obj) + .filter(([key]) => paths.includes(key as PathArray[number])) + .reduce( + (result, [key, value]) => { + (result as any)[key] = value; + return result; + }, + {} as PickDeep, + ); +}; + +export const Lodash = { + escape, + unescape, + toPath, + get, + set, + unset, + omit, + pick, +}; diff --git a/src/polyfill/StatusTexts.mts b/src/polyfill/StatusTexts.mts new file mode 100644 index 0000000..ad4f864 --- /dev/null +++ b/src/polyfill/StatusTexts.mts @@ -0,0 +1,62 @@ +export const StatusTexts = { + 100: 'Continue', + 101: 'Switching Protocols', + 102: 'Processing', + 103: 'Early Hints', + 200: 'OK', + 201: 'Created', + 202: 'Accepted', + 203: 'Non-Authoritative Information', + 204: 'No Content', + 205: 'Reset Content', + 206: 'Partial Content', + 207: 'Multi-Status', + 208: 'Already Reported', + 226: 'IM Used', + 300: 'Multiple Choices', + 301: 'Moved Permanently', + 302: 'Found', + 304: 'Not Modified', + 307: 'Temporary Redirect', + 308: 'Permanent Redirect', + 400: 'Bad Request', + 401: 'Unauthorized', + 402: 'Payment Required', + 403: 'Forbidden', + 404: 'Not Found', + 405: 'Method Not Allowed', + 406: 'Not Acceptable', + 407: 'Proxy Authentication Required', + 408: 'Request Timeout', + 409: 'Conflict', + 410: 'Gone', + 411: 'Length Required', + 412: 'Precondition Failed', + 413: 'Content Too Large', + 414: 'URI Too Long', + 415: 'Unsupported Media Type', + 416: 'Range Not Satisfiable', + 417: 'Expectation Failed', + 418: "I'm a teapot", + 421: 'Misdirected Request', + 422: 'Unprocessable Entity', + 423: 'Locked', + 424: 'Failed Dependency', + 425: 'Too Early', + 426: 'Upgrade Required', + 428: 'Precondition Required', + 429: 'Too Many Requests', + 431: 'Request Header Fields Too Large', + 451: 'Unavailable For Legal Reasons', + 500: 'Internal Server Error', + 501: 'Not Implemented', + 502: 'Bad Gateway', + 503: 'Service Unavailable', + 504: 'Gateway Timeout', + 505: 'HTTP Version Not Supported', + 506: 'Variant Also Negotiates', + 507: 'Insufficient Storage', + 508: 'Loop Detected', + 510: 'Not Extended', + 511: 'Network Authentication Required', +}; diff --git a/src/polyfill/Storage.mts b/src/polyfill/Storage.mts new file mode 100644 index 0000000..e0ff44c --- /dev/null +++ b/src/polyfill/Storage.mts @@ -0,0 +1,186 @@ +import { get, set, unset } from './Lodash.mjs'; +import { $app } from '../lib/app.mjs'; + +declare const $persistentStore: { + read: (key: string) => string | null; + write: (value: string, key: string) => boolean; +}; + +declare const $prefs: { + valueForKey: (key: string) => string | null; + setValueForKey: (value: string, key: string) => boolean; + removeValueForKey: (key: string) => boolean; + removeAllValues: () => boolean; +}; + +interface StorageData { + [key: string]: any; +} + +export class StorageClass { + private data: StorageData | null = null; + private readonly dataFile: string = 'box.dat'; + private readonly nameRegex = /^@(?[^.]+)(?:\.(?.*))?$/; + + constructor() { + if ($app === 'Node.js') { + this.data = this.#loadData(this.dataFile) + } + } + + public getItem(keyName: string, defaultValue = null as T): T { + let keyValue = defaultValue; + + if (keyName.startsWith('@')) { + const { key, path } = keyName.match(this.nameRegex)?.groups || {}; + if (key) { + let value = this.getItem(key, {}); + if (typeof value !== 'object') { + value = {}; + } + keyValue = get(value, path); + try { + keyValue = JSON.parse(keyValue as string); + } catch { + // Ignore parse error + } + } + } else { + switch ($app) { + case 'Surge': + case 'Loon': + case 'Stash': + case 'Egern': + case 'Shadowrocket': + keyValue = $persistentStore.read(keyName) as T; + break; + case 'Quantumult X': + keyValue = $prefs.valueForKey(keyName) as T; + break; + case 'Node.js': + this.data = this.data || {}; + keyValue = this.data[keyName]; + break; + default: + keyValue = null as T; + break; + } + + try { + keyValue = JSON.parse(keyValue as string); + } catch { + // Ignore parse error + } + } + + return keyValue ?? defaultValue; + } + + public setItem(keyName: string, value: any): boolean { + let keyValue = value; + if (typeof keyValue === 'object') { + keyValue = JSON.stringify(keyValue); + } else { + keyValue = String(keyValue); + } + + if (keyName.startsWith('@')) { + const { key, path } = keyName.match(this.nameRegex)?.groups || {}; + if (key) { + let value = this.getItem(key, {}); + if (typeof value !== 'object') value = {}; + set(value, path, keyValue); + return this.setItem(keyName, value); + } + } else { + switch ($app) { + case 'Surge': + case 'Loon': + case 'Stash': + case 'Egern': + case 'Shadowrocket': + return $persistentStore.write(keyValue, keyName); + case 'Quantumult X': + return $prefs.setValueForKey(keyValue, keyName); + case 'Node.js': + this.data = this.data || {}; + this.data[keyName] = keyValue; + this.#writeData(this.dataFile); + return true; + default: + return false; + } + } + + return false; + } + + public removeItem(keyName: string): boolean { + if (keyName.startsWith('@')) { + const { key, path } = keyName.match(this.nameRegex)?.groups || {}; + if (key) { + let value = this.getItem(key); + if (typeof value !== 'object') value = {}; + unset(value, path); + return this.setItem(key, value); + } + } else { + switch ($app) { + case 'Quantumult X': + return $prefs.removeValueForKey(keyName); + default: + return false; + } + } + + return false; + } + + public clear(): boolean { + switch ($app) { + case 'Quantumult X': + return $prefs.removeAllValues(); + default: + return false; + } + } + + #getNodeModule() { + if ($app === 'Node.js') { + // biome-ignore lint/style/useNodejsImportProtocol: + const fs = require('fs') + // biome-ignore lint/style/useNodejsImportProtocol: + const path = require('path') + return { fs, path } + } + return null; + } + + #loadData(dataFile: string): StorageData { + const { fs, path } = this.#getNodeModule() ?? {}; + if (!fs || !path) { + return {}; + } + const curDirDataFilePath = path.resolve(dataFile); + const rootDirDataFilePath = path.resolve(process.cwd(), dataFile); + if (fs.existsSync(curDirDataFilePath)) { + return JSON.parse(fs.readFileSync(curDirDataFilePath, 'utf-8')) || {}; + } + if (fs.existsSync(rootDirDataFilePath)) { + return JSON.parse(fs.readFileSync(rootDirDataFilePath, 'utf-8')) || {}; + } + return {}; + } + + #writeData(dataFile: string) { + const { fs, path } = this.#getNodeModule() ?? {}; + if (!fs || !path) { + return + } + const dataFilePath = path.resolve(dataFile); + fs.writeFileSync(dataFilePath, JSON.stringify(this.data), 'utf-8'); + } +} + +// 导出初始化后的实例 +export const Storage = new StorageClass(); \ No newline at end of file diff --git a/src/polyfill/fetch-node.mts b/src/polyfill/fetch-node.mts new file mode 100644 index 0000000..75d84c4 --- /dev/null +++ b/src/polyfill/fetch-node.mts @@ -0,0 +1,8 @@ +export const getNodeFetch = () => { + const nodeFetch = globalThis.fetch ? globalThis.fetch : require('node-fetch'); + const fetchCookie = (globalThis as any).fetchCookie ? (globalThis as any).fetchCookie : require('fetch-cookie').default; + const fetch = fetchCookie(nodeFetch); + return { + fetch, + }; +}; diff --git a/src/polyfill/fetch.mts b/src/polyfill/fetch.mts new file mode 100644 index 0000000..151de1a --- /dev/null +++ b/src/polyfill/fetch.mts @@ -0,0 +1,234 @@ +import { set } from './Lodash.mjs'; +import { $app } from '../lib/app.mjs'; +import { StatusTexts } from './StatusTexts.mjs'; + +declare const $task: { + fetch: (options: FetchOptions) => Promise; +}; + +declare const $httpClient: { + [method: string]: (resource: any, callback: (error: any, response: any, body: any) => void) => void; +}; + +interface FetchOptions { + url?: string; + method?: string; + headers?: Record; + body?: any; + bodyBytes?: ArrayBuffer; + timeout: number; + policy?: string; + node?: string; + redirection?: boolean; + redirect?: string; + 'auto-redirect'?: boolean; + 'binary-mode'?: boolean; +} + +interface FetchResponse { + ok: boolean; + status: number; + statusCode: number; + statusText: string; + body: T; + bodyBytes?: ArrayBuffer; + headers: Record; +} + +// 定义需要二进制模式的 MIME 类型 +const binaryMimeTypes = [ + 'application/protobuf', + 'application/x-protobuf', + 'application/vnd.google.protobuf', + 'application/vnd.apple.flatbuffer', + 'application/grpc', + 'application/grpc+proto', + 'application/octet-stream', +]; + +export async function fetch( + resource: string | FetchOptions, + options: FetchOptions = { + timeout: 5, + }, +): Promise> { + let params = { ...options }; + // 初始化参数 + if (typeof resource === 'string') { + params.url = resource; + } else if (typeof resource === 'object') { + params = { ...params, ...resource }; + } else { + throw new TypeError(`${Function.name}: 参数类型错误, resource 必须为对象或字符串`); + } + + // 自动判断请求方法 + if (!params.method) { + params.method = 'GET'; + if (params.body || params.bodyBytes) { + params.method = 'POST'; + } + } + + // 移除请求头中的部分参数, 让其自动生成 + if (params.headers) { + params.headers.Host = undefined; + params.headers[':authority'] = undefined; + params.headers['Content-Length'] = undefined; + params.headers['content-length'] = undefined; + } + + // 定义请求方法(小写) + const method = params.method.toLocaleLowerCase(); + + // 转换请求超时时间参数 + if (params.timeout) { + params.timeout = Number.parseInt(params.timeout.toString(), 10); + // 转换为秒,大于500视为毫秒,小于等于500视为秒 + if (params.timeout > 500) { + params.timeout = Math.round(params.timeout / 1000); + } + } + + if ($app === 'Quantumult X') { + // 转换请求参数 + if (params.policy) { + set(params, 'opts.policy', params.policy); + } + if (typeof params['auto-redirect'] === 'boolean') { + set(params, 'opts.redirection', params['auto-redirect']); + } + // 转换请求体 + if (params.body instanceof ArrayBuffer) { + params.bodyBytes = params.body; + params.body = undefined; + } else if (ArrayBuffer.isView(params.body)) { + params.bodyBytes = params.body.buffer.slice( + params.body.byteOffset, + params.body.byteLength + params.body.byteOffset, + ); + params.body = undefined; + } else if (params.body) { + params.bodyBytes = undefined; + } + // 发送请求 + return Promise.race([ + await $task.fetch(params).then( + (response) => { + response.ok = /^2\d\d$/.test(response.statusCode); + response.status = response.statusCode; + response.statusText = StatusTexts[response.status as keyof typeof StatusTexts]; + if ( + binaryMimeTypes.includes( + (response.headers?.['Content-Type'] ?? response.headers?.['content-type'])?.split(';')?.[0], + ) + ) { + response.body = response.bodyBytes; + } + response.bodyBytes = undefined; + return response; + }, + (reason) => Promise.reject(reason.error), + ), + new Promise((_, reject) => { + setTimeout(() => { + reject(new Error(`${Function.name}: 请求超时, 请检查网络后重试`)); + }, params.timeout); + }), + ]); + } + + if ($app === 'Node.js') { + const fetch = await import('./fetch-node.mjs').then((module) => module.getNodeFetch().fetch); + // 转换请求参数 + params.timeout = (params.timeout ?? 5) * 1000; + params.redirect = params.redirection ? 'follow' : 'manual'; + const { url, ...options } = params; + // 发送请求 + return Promise.race([ + await fetch(url as string, options as any) + .then(async (response: any) => { + const bodyBytes = await response.arrayBuffer(); + let headers: any; + try { + headers = response.headers.raw(); + } catch { + headers = Array.from(response.headers.entries()).reduce((acc, item) => { + const [key, value] = item as [string, string]; + acc[key] = acc[key] ? [...acc[key], value] : [value]; + return acc; + }, {}); + } + return { + ok: response.ok ?? /^2\d\d$/.test(response.status), + status: response.status, + statusCode: response.status, + statusText: response.statusText, + body: new TextDecoder('utf-8').decode(bodyBytes), + bodyBytes: bodyBytes, + headers: Object.fromEntries( + Object.entries(headers).map(([key, value]) => [ + key, + key.toLowerCase() !== 'set-cookie' ? (value as any).toString() : value, + ]), + ), + }; + }) + .catch((error: Error) => Promise.reject(error.message)), + new Promise((resolve, reject) => { + setTimeout(() => { + reject(new Error(`${Function.name}: 请求超时, 请检查网络后重试`)); + }, params.timeout); + }), + ]) as unknown as FetchResponse; + } + + if ($app === 'Loon') { + params.timeout *= 1000; + } + if (params.policy) { + switch ($app) { + case 'Loon': + params.node = params.policy; + break; + case 'Stash': + set(params, 'headers.X-Stash-Selected-Proxy', encodeURI(params.policy)); + break; + case 'Shadowrocket': + set(params, 'headers.X-Surge-Proxy', params.policy); + break; + default: + break; + } + } + if (typeof params.redirection === 'boolean') { + params['auto-redirect'] = params.redirection; + } + // 转换请求体 + if (params.bodyBytes && !params.body) { + params.body = params.bodyBytes; + params.bodyBytes = undefined; + } + // 判断是否请求二进制响应体 + if (binaryMimeTypes.includes((params.headers?.Accept || params.headers?.accept)?.split(';')?.[0] ?? '')) { + params['binary-mode'] = true; + } + return new Promise((resolve, reject) => { + $httpClient[method](params, (error, response, body) => { + if (error) { + reject(error); + } else { + response.ok = /^2\d\d$/.test(response.status); + response.statusCode = response.status; + response.statusText = StatusTexts[response.status as keyof typeof StatusTexts]; + if (body) { + response.body = body; + if (params['binary-mode']) { + response.bodyBytes = body; + } + } + resolve(response); + } + }); + }); +} diff --git a/src/polyfill/index.mts b/src/polyfill/index.mts new file mode 100644 index 0000000..8d9cf16 --- /dev/null +++ b/src/polyfill/index.mts @@ -0,0 +1,5 @@ +export * from './Console.mjs'; +export * from './fetch.mjs'; +export * from './Lodash.mjs'; +export * from './StatusTexts.mjs'; +export * from './Storage.mjs'; diff --git a/test/test.js b/test/test.js index 578c5c7..7bc21e1 100644 --- a/test/test.js +++ b/test/test.js @@ -1,18 +1,29 @@ //import { $app, Console, done, fetch, getStorage, gRPC, Lodash as _, notification, Storage, time, wait } from "@nsnanocat/util"; -import { $app, Console, done, fetch, getStorage, gRPC, Lodash as _, notification, Storage, time, wait } from "../index.js"; +import { + $app, + Console, + Storage, + Lodash as _, + done, + fetch, + gRPC, + getStorage, + notification, + time, + wait, +} from '../index.js'; const request = { - status: 200, - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - status: "ok", - }), - opt: { - method: "GET", - }, - ok: true, + status: 200, + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + status: 'ok', + }), + opt: { + method: 'GET', + }, + ok: true, }; done(request); - diff --git a/tsconfig.json b/tsconfig.json index ca246d1..5ae4d73 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,23 +1,17 @@ { - // Change this to match your project - "include": [ - "polyfill/URL.mts", - "polyfill/URLSearchParams.mts", - ], - "compilerOptions": { - // Tells TypeScript to read JS files, as - // normally they are ignored as source files - "allowJs": true, - // Generate d.ts files - "declaration": true, - // This compiler run should - // only output d.ts files - "emitDeclarationOnly": false, - // go to js file when using IDE functions like - // "Go to Definition" in VSCode - "declarationMap": true, - "target": "ESNext", - "module": "ESNext", - "allowImportingTsExtensions": true, - }, -} + "compilerOptions": { + "lib": ["ES2021"], + "module": "ESNext", + "target": "ES2021", + "noEmit": false, + "strict": true, + "skipLibCheck": true, + "isolatedModules": true, + "resolveJsonModule": true, + "moduleResolution": "bundler", + "useDefineForClassFields": true, + "outDir": "./dist", + "declaration": true + }, + "include": ["src"] +} \ No newline at end of file