From b848fce8cf05dd3ba989a642906539c694033a7f Mon Sep 17 00:00:00 2001 From: Matthew Sweeney Date: Fri, 20 Mar 2026 13:36:39 +0000 Subject: [PATCH 1/2] Add CLAUDE.md with build, lint, and convention quick-reference Co-Authored-By: Claude Opus 4.6 (1M context) --- CLAUDE.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..7ea467245 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,29 @@ +# CLAUDE.md + +## Build & Dev + +```bash +npm install # Install dependencies +npm run dev # Dev build (no minification) +npm run build # Production build + Firefox ZIP +``` + +## Lint + +```bash +npx eslint . +``` + +Style: ES2025, double quotes, semicolons, Unix line breaks. + +## Code Conventions + +- ES6 modules throughout (`"type": "module"`) +- async/await for async operations +- IPC via `browser.runtime.sendMessage({ method: "module.action", ... })` +- UI strings via `browser.i18n.getMessage()` +- Copyright headers on all source files + +## No Tests + +There is no test suite in this repository currently. From b3bbaf983dd0438ac9d3ae3cb2ea1b5c3bcec288 Mon Sep 17 00:00:00 2001 From: Matthew Sweeney Date: Fri, 20 Mar 2026 13:52:48 +0000 Subject: [PATCH 2/2] Add Vitest test suite for pure-logic modules Test srcset parser, MHTML utilities, yabson serialization round-trips, and config/download helpers. Extract pure functions from config.js and downloads.js into config-utils.js to make them importable without browser API dependencies. Co-Authored-By: Claude Opus 4.6 (1M context) --- CLAUDE.md | 9 +- package-lock.json | 1242 ++++++++++++++++++++++++++++++++++- package.json | 9 +- src/core/bg/config-utils.js | 99 +++ src/core/bg/config.js | 58 +- src/core/bg/downloads.js | 9 +- test/config-utils.test.js | 123 ++++ test/mhtml-util.test.js | 185 ++++++ test/srcset-parser.test.js | 100 +++ test/yabson.test.js | 148 +++++ vitest.config.js | 7 + 11 files changed, 1900 insertions(+), 89 deletions(-) create mode 100644 src/core/bg/config-utils.js create mode 100644 test/config-utils.test.js create mode 100644 test/mhtml-util.test.js create mode 100644 test/srcset-parser.test.js create mode 100644 test/yabson.test.js create mode 100644 vitest.config.js diff --git a/CLAUDE.md b/CLAUDE.md index 7ea467245..29e0a0916 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -24,6 +24,11 @@ Style: ES2025, double quotes, semicolons, Unix line breaks. - UI strings via `browser.i18n.getMessage()` - Copyright headers on all source files -## No Tests +## Tests -There is no test suite in this repository currently. +```bash +npm test # Run all tests once +npm run test:watch # Run tests in watch mode +``` + +Tests live in `test/` and use Vitest. Pure-logic modules are tested (srcset parser, MHTML utilities, yabson serialization, config/download helpers). diff --git a/package-lock.json b/package-lock.json index 3a6a40242..6e50a83bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,42 @@ "@rollup/plugin-node-resolve": "16.0.3", "@rollup/plugin-terser": "0.4.4", "eslint": "^10.0.0", - "rollup": "^4.57.1" + "rollup": "^4.57.1", + "vitest": "^4.1.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz", + "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", + "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", + "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" } }, "node_modules/@eslint-community/eslint-utils": { @@ -227,6 +262,295 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", + "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.120.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.120.0.tgz", + "integrity": "sha512-k1YNu55DuvAip/MGE1FTsIuU3FUCn6v/ujG9V7Nq5Df/kX2CWb13hhwD0lmJGMGqE+bE1MXvv9SZVnMzEXlWcg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.10.tgz", + "integrity": "sha512-jOHxwXhxmFKuXztiu1ORieJeTbx5vrTkcOkkkn2d35726+iwhrY1w/+nYY/AGgF12thg33qC3R1LMBF5tHTZHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.10.tgz", + "integrity": "sha512-gED05Teg/vtTZbIJBc4VNMAxAFDUPkuO/rAIyyxZjTj1a1/s6z5TII/5yMGZ0uLRCifEtwUQn8OlYzuYc0m70w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.10.tgz", + "integrity": "sha512-rI15NcM1mA48lqrIxVkHfAqcyFLcQwyXWThy+BQ5+mkKKPvSO26ir+ZDp36AgYoYVkqvMcdS8zOE6SeBsR9e8A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.10.tgz", + "integrity": "sha512-XZRXHdTa+4ME1MuDVp021+doQ+z6Ei4CCFmNc5/sKbqb8YmkiJdj8QKlV3rCI0AJtAeSB5n0WGPuJWNL9p/L2w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.10.tgz", + "integrity": "sha512-R0SQMRluISSLzFE20sPWYHVmJdDQnRyc/FzSCN72BqQmh2SOZUFG+N3/vBZpR4C6WpEUVYJLrYUXaj43sJsNLA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.10.tgz", + "integrity": "sha512-Y1reMrV/o+cwpduYhJuOE3OMKx32RMYCidf14y+HssARRmhDuWXJ4yVguDg2R/8SyyGNo+auzz64LnPK9Hq6jg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.10.tgz", + "integrity": "sha512-vELN+HNb2IzuzSBUOD4NHmP9yrGwl1DVM29wlQvx1OLSclL0NgVWnVDKl/8tEks79EFek/kebQKnNJkIAA4W2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.10.tgz", + "integrity": "sha512-ZqrufYTgzxbHwpqOjzSsb0UV/aV2TFIY5rP8HdsiPTv/CuAgCRjM6s9cYFwQ4CNH+hf9Y4erHW1GjZuZ7WoI7w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.10.tgz", + "integrity": "sha512-gSlmVS1FZJSRicA6IyjoRoKAFK7IIHBs7xJuHRSmjImqk3mPPWbR7RhbnfH2G6bcmMEllCt2vQ/7u9e6bBnByg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.10.tgz", + "integrity": "sha512-eOCKUpluKgfObT2pHjztnaWEIbUabWzk3qPZ5PuacuPmr4+JtQG4k2vGTY0H15edaTnicgU428XW/IH6AimcQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.10.tgz", + "integrity": "sha512-Xdf2jQbfQowJnLcgYfD/m0Uu0Qj5OdxKallD78/IPPfzaiaI4KRAwZzHcKQ4ig1gtg1SuzC7jovNiM2TzQsBXA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.10.tgz", + "integrity": "sha512-o1hYe8hLi1EY6jgPFyxQgQ1wcycX+qz8eEbVmot2hFkgUzPxy9+kF0u0NIQBeDq+Mko47AkaFFaChcvZa9UX9Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.10.tgz", + "integrity": "sha512-Ugv9o7qYJudqQO5Y5y2N2SOo6S4WiqiNOpuQyoPInnhVzCY+wi/GHltcLHypG9DEUYMB0iTB/huJrpadiAcNcA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.10.tgz", + "integrity": "sha512-7UODQb4fQUNT/vmgDZBl3XOBAIOutP5R3O/rkxg0aLfEGQ4opbCgU5vOw/scPe4xOqBwL9fw7/RP1vAMZ6QlAQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.10.tgz", + "integrity": "sha512-PYxKHMVHOb5NJuDL53vBUl1VwUjymDcYI6rzpIni0C9+9mTiJedvUxSk7/RPp7OOAm3v+EjgMu9bIy3N6b408w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.10.tgz", + "integrity": "sha512-UkVDEFk1w3mveXeKgaTuYfKWtPbvgck1dT8TUG3bnccrH0XtLTuAyfCoks4Q/M5ZGToSVJTIQYCzy2g/atAOeg==", + "dev": true, + "license": "MIT" + }, "node_modules/@rollup/plugin-node-resolve": { "version": "16.0.3", "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.3.tgz", @@ -648,6 +972,42 @@ "win32" ] }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/esrecurse": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", @@ -676,34 +1036,156 @@ "dev": true, "license": "MIT" }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "node_modules/@vitest/expect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.0.tgz", + "integrity": "sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==", "dev": true, "license": "MIT", - "peer": true, - "bin": { - "acorn": "bin/acorn" + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.0", + "@vitest/utils": "4.1.0", + "chai": "^6.2.2", + "tinyrainbow": "^3.0.3" }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "node_modules/@vitest/mocker": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.0.tgz", + "integrity": "sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.0", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.0.tgz", + "integrity": "sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.0.tgz", + "integrity": "sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.0", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.0.tgz", + "integrity": "sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.0", + "@vitest/utils": "4.1.0", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.0.tgz", + "integrity": "sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.0.tgz", + "integrity": "sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.0", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -717,6 +1199,16 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -747,6 +1239,16 @@ "dev": true, "license": "MIT" }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -754,6 +1256,13 @@ "dev": true, "license": "MIT" }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -804,6 +1313,23 @@ "node": ">=0.10.0" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true, + "license": "MIT" + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -823,7 +1349,6 @@ "integrity": "sha512-uYixubwmqJZH+KLVYIVKY1JQt7tysXhtj21WSvjcSmU5SVNzMus1bgLe+pAt816yQ8opKfheVVoPLqvVMGejYw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", @@ -977,6 +1502,16 @@ "node": ">=0.10.0" } }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -998,6 +1533,24 @@ "dev": true, "license": "MIT" }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -1218,6 +1771,267 @@ "node": ">= 0.8.0" } }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -1234,6 +2048,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/minimatch": { "version": "10.2.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.2.tgz", @@ -1257,6 +2081,25 @@ "dev": true, "license": "MIT" }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -1264,6 +2107,17 @@ "dev": true, "license": "MIT" }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -1341,6 +2195,20 @@ "dev": true, "license": "MIT" }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, "node_modules/picomatch": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", @@ -1354,6 +2222,35 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -1405,13 +2302,46 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/rolldown": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.10.tgz", + "integrity": "sha512-q7j6vvarRFmKpgJUT8HCAUljkgzEp4LAhPlJUvQhA5LA1SUL36s5QCysMutErzL3EbNOZOkoziSx9iZC4FddKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.120.0", + "@rolldown/pluginutils": "1.0.0-rc.10" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.10", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.10", + "@rolldown/binding-darwin-x64": "1.0.0-rc.10", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.10", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.10", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.10", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.10", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.10", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.10", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.10", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.10", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.10", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.10", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.10", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.10" + } + }, "node_modules/rollup": { "version": "4.59.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -1505,6 +2435,13 @@ "node": ">=8" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/single-file-core": { "version": "1.5.84", "resolved": "https://registry.npmjs.org/single-file-core/-/single-file-core-1.5.84.tgz", @@ -1531,6 +2468,16 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", @@ -1542,6 +2489,20 @@ "source-map": "^0.6.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz", + "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==", + "dev": true, + "license": "MIT" + }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -1574,6 +2535,58 @@ "node": ">=10" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", + "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -1597,6 +2610,166 @@ "punycode": "^2.1.0" } }, + "node_modules/vite": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.1.tgz", + "integrity": "sha512-wt+Z2qIhfFt85uiyRt5LPU4oVEJBXj8hZNWKeqFG4gRG/0RaRGJ7njQCwzFVjO+v4+Ipmf5CY7VdmZRAYYBPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.8", + "rolldown": "1.0.0-rc.10", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.0.tgz", + "integrity": "sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.0", + "@vitest/mocker": "4.1.0", + "@vitest/pretty-format": "4.1.0", + "@vitest/runner": "4.1.0", + "@vitest/snapshot": "4.1.0", + "@vitest/spy": "4.1.0", + "@vitest/utils": "4.1.0", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.0", + "@vitest/browser-preview": "4.1.0", + "@vitest/browser-webdriverio": "4.1.0", + "@vitest/ui": "4.1.0", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -1613,6 +2786,23 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", diff --git a/package.json b/package.json index ba7666bd7..5a7640dd2 100644 --- a/package.json +++ b/package.json @@ -6,17 +6,20 @@ "license": "AGPL-3.0-or-later", "scripts": { "dev": "npx rollup -c rollup.config.dev.js", - "build": "./build-extension.sh" + "build": "./build-extension.sh", + "test": "vitest run", + "test:watch": "vitest" }, "type": "module", "dependencies": { "single-file-core": "1.5.84" }, "devDependencies": { - "eslint": "^10.0.0", "@rollup/plugin-node-resolve": "16.0.3", "@rollup/plugin-terser": "0.4.4", - "rollup": "^4.57.1" + "eslint": "^10.0.0", + "rollup": "^4.57.1", + "vitest": "^4.1.0" }, "overrides": { "terser": "^5.15.0" diff --git a/src/core/bg/config-utils.js b/src/core/bg/config-utils.js new file mode 100644 index 000000000..62e43c7d4 --- /dev/null +++ b/src/core/bg/config-utils.js @@ -0,0 +1,99 @@ +/* + * Copyright 2010-2020 Gildas Lormeau + * contact : gildas.lormeau gmail.com + * + * This file is part of SingleFile. + * + * The code in this file is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * (GNU AGPL) as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * The code in this file is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero + * General Public License for more details. + * + * As additional permission under GNU AGPL version 3 section 7, you may + * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU + * AGPL normally required by section 4, provided you include this license + * notice and a URL through which recipients can access the Corresponding + * Source. + */ + +const REGEXP_RULE_PREFIX = "regexp:"; + +const MIGRATION_DEFAULT_VARIABLES_VALUES = { + "page-title": "No title", + "page-heading": "No heading", + "page-language": "No language", + "page-description": "No description", + "page-author": "No author", + "page-creator": "No creator", + "page-publisher": "No publisher", + "url-hash": "No hash", + "url-host": "No host", + "url-hostname": "No hostname", + "url-href": "No href", + "url-href-digest-sha-1": "No hash", + "url-href-flat": "No href", + "url-referrer": "No referrer", + "url-referrer-flat": "No referrer", + "url-password": "No password", + "url-pathname": "No pathname", + "url-pathname-flat": "No pathname", + "url-port": "No port", + "url-protocol": "No protocol", + "url-search": "No search", + "url-username": "No username", + "tab-id": "No tab id", + "tab-index": "No tab index", + "url-last-segment": "No last segment" +}; + +const REGEXP_ESCAPE = /([{}()^$&.*?/+|[\\\\]|\]|-)/g; + +function updateFilenameTemplate(template) { + try { + Object.keys(MIGRATION_DEFAULT_VARIABLES_VALUES).forEach(variable => { + const value = MIGRATION_DEFAULT_VARIABLES_VALUES[variable]; + template = template.replaceAll(`{${variable}}`, `%if-empty<{${variable}}|${value}>`); + }); + return template; + // eslint-disable-next-line no-unused-vars + } catch (error) { + // ignored + } +} + +function sortRules(ruleLeft, ruleRight) { + return ruleRight.url.length - ruleLeft.url.length; +} + +function testRegExpRule(rule) { + return rule.url.toLowerCase().startsWith(REGEXP_RULE_PREFIX); +} + +function isSameArray(arrayLeft, arrayRight) { + return arrayLeft.length == arrayRight.length && arrayLeft.every((value, index) => value == arrayRight[index]); +} + +function encodeSharpCharacter(path) { + return path.replace(/#/g, "%23"); +} + +function getRegExp(string) { + return string.replace(REGEXP_ESCAPE, "\\$1"); +} + +export { + REGEXP_RULE_PREFIX, + MIGRATION_DEFAULT_VARIABLES_VALUES, + REGEXP_ESCAPE, + updateFilenameTemplate, + sortRules, + testRegExpRule, + isSameArray, + encodeSharpCharacter, + getRegExp +}; diff --git a/src/core/bg/config.js b/src/core/bg/config.js index 25b4c3de6..098a676a1 100644 --- a/src/core/bg/config.js +++ b/src/core/bg/config.js @@ -25,11 +25,18 @@ import { download } from "./download-util.js"; import * as tabsData from "./tabs-data.js"; +import { + REGEXP_RULE_PREFIX, + MIGRATION_DEFAULT_VARIABLES_VALUES, + updateFilenameTemplate, + sortRules, + testRegExpRule, + isSameArray +} from "./config-utils.js"; const CURRENT_PROFILE_NAME = "-"; const DEFAULT_PROFILE_NAME = "__Default_Settings__"; const DISABLED_PROFILE_NAME = "__Disabled_Settings__"; -const REGEXP_RULE_PREFIX = "regexp:"; const PROFILE_NAME_PREFIX = "profile_"; const IS_NOT_SAFARI = !/Safari/.test(navigator.userAgent) || /Chrome/.test(navigator.userAgent) || /Vivaldi/.test(navigator.userAgent) || /OPR/.test(navigator.userAgent); @@ -210,33 +217,6 @@ const DEFAULT_RULES = [{ "autoSaveProfile": "__Disabled_Settings__" }]; -const MIGRATION_DEFAULT_VARIABLES_VALUES = { - "page-title": "No title", - "page-heading": "No heading", - "page-language": "No language", - "page-description": "No description", - "page-author": "No author", - "page-creator": "No creator", - "page-publisher": "No publisher", - "url-hash": "No hash", - "url-host": "No host", - "url-hostname": "No hostname", - "url-href": "No href", - "url-href-digest-sha-1": "No hash", - "url-href-flat": "No href", - "url-referrer": "No referrer", - "url-referrer-flat": "No referrer", - "url-password": "No password", - "url-pathname": "No pathname", - "url-pathname-flat": "No pathname", - "url-port": "No port", - "url-protocol": "No protocol", - "url-search": "No search", - "url-username": "No username", - "tab-id": "No tab id", - "tab-index": "No tab index", - "url-last-segment": "No last segment" -}; let configStorage; let pendingUpgradePromise = upgrade(); @@ -321,18 +301,6 @@ async function upgrade() { }); } -function updateFilenameTemplate(template) { - try { - Object.keys(MIGRATION_DEFAULT_VARIABLES_VALUES).forEach(variable => { - const value = MIGRATION_DEFAULT_VARIABLES_VALUES[variable]; - template = template.replaceAll(`{${variable}}`, `%if-empty<{${variable}}|${value}>`); - }); - return template; - // eslint-disable-next-line no-unused-vars - } catch (error) { - // ignored - } -} async function getRule(url, ignoreWildcard) { const { rules } = await configStorage.get(["rules"]); @@ -353,13 +321,6 @@ async function getConfig() { return { profiles, rules, maxParallelWorkers, processInForeground }; } -function sortRules(ruleLeft, ruleRight) { - return ruleRight.url.length - ruleLeft.url.length; -} - -function testRegExpRule(rule) { - return rule.url.toLowerCase().startsWith(REGEXP_RULE_PREFIX); -} async function onMessage(message) { if (message.method.endsWith(".get")) { @@ -733,6 +694,3 @@ async function importConfig(config) { await upgrade(); } -function isSameArray(arrayLeft, arrayRight) { - return arrayLeft.length == arrayRight.length && arrayLeft.every((value, index) => value == arrayRight[index]); -} \ No newline at end of file diff --git a/src/core/bg/downloads.js b/src/core/bg/downloads.js index 1586c98b6..57c43d112 100644 --- a/src/core/bg/downloads.js +++ b/src/core/bg/downloads.js @@ -40,13 +40,13 @@ import { MCP } from "./../../lib/mcp/mcp.js"; import { download } from "./download-util.js"; import * as yabson from "./../../lib/yabson/yabson.js"; import { RestFormApi } from "../../lib/../lib/rest-form-api/index.js"; +import { encodeSharpCharacter, getRegExp } from "./config-utils.js"; const partialContents = new Map(); const tabData = new Map(); const SCOPES = ["https://www.googleapis.com/auth/drive.file"]; const CONFLICT_ACTION_SKIP = "skip"; const CONFLICT_ACTION_UNIQUIFY = "uniquify"; -const REGEXP_ESCAPE = /([{}()^$&.*?/+|[\\\\]|\]|-)/g; let GDRIVE_CLIENT_ID = "207618107333-h1220p1oasj3050kr5r416661adm091a.apps.googleusercontent.com"; let GDRIVE_CLIENT_KEY = "VQJ8Gq8Vxx72QyxPyeLtWvUt"; const DROPBOX_CLIENT_ID = "s50p6litdvuzrtb"; @@ -421,13 +421,6 @@ async function downloadCompressedContent(message, tab) { } } -function encodeSharpCharacter(path) { - return path.replace(/#/g, "%23"); -} - -function getRegExp(string) { - return string.replace(REGEXP_ESCAPE, "\\$1"); -} async function getAuthInfo(authOptions, force) { let authInfo = await config.getAuthInfo(); diff --git a/test/config-utils.test.js b/test/config-utils.test.js new file mode 100644 index 000000000..7f99c2dc0 --- /dev/null +++ b/test/config-utils.test.js @@ -0,0 +1,123 @@ +import { describe, it, expect } from "vitest"; +import { + updateFilenameTemplate, + sortRules, + testRegExpRule, + isSameArray, + encodeSharpCharacter, + getRegExp +} from "../src/core/bg/config-utils.js"; + +describe("config-utils", () => { + describe("updateFilenameTemplate", () => { + it("wraps known variables with %if-empty", () => { + const result = updateFilenameTemplate("{page-title}"); + expect(result).toBe("%if-empty<{page-title}|No title>"); + }); + + it("wraps multiple variables", () => { + const result = updateFilenameTemplate("{page-title} - {page-author}"); + expect(result).toBe("%if-empty<{page-title}|No title> - %if-empty<{page-author}|No author>"); + }); + + it("leaves unrecognized variables unchanged", () => { + const result = updateFilenameTemplate("{custom-var}"); + expect(result).toBe("{custom-var}"); + }); + + it("handles template with no variables", () => { + const result = updateFilenameTemplate("static-name"); + expect(result).toBe("static-name"); + }); + }); + + describe("sortRules", () => { + it("sorts longer URLs first (higher specificity)", () => { + const rules = [ + { url: "short" }, + { url: "much-longer-url" }, + { url: "medium-url" } + ]; + const sorted = [...rules].sort(sortRules); + expect(sorted[0].url).toBe("much-longer-url"); + expect(sorted[2].url).toBe("short"); + }); + + it("returns 0 for equal-length URLs", () => { + expect(sortRules({ url: "abc" }, { url: "xyz" })).toBe(0); + }); + }); + + describe("testRegExpRule", () => { + it("returns true for regexp: prefixed rules", () => { + expect(testRegExpRule({ url: "regexp:.*example\\.com" })).toBe(true); + }); + + it("returns true case-insensitively", () => { + expect(testRegExpRule({ url: "REGEXP:.*example" })).toBe(true); + }); + + it("returns false for plain URL rules", () => { + expect(testRegExpRule({ url: "https://example.com" })).toBe(false); + }); + + it("returns false for wildcard rules", () => { + expect(testRegExpRule({ url: "*" })).toBe(false); + }); + }); + + describe("isSameArray", () => { + it("returns true for equal arrays", () => { + expect(isSameArray([1, 2, 3], [1, 2, 3])).toBe(true); + }); + + it("returns false for different values", () => { + expect(isSameArray([1, 2, 3], [1, 2, 4])).toBe(false); + }); + + it("returns false for different lengths", () => { + expect(isSameArray([1, 2], [1, 2, 3])).toBe(false); + }); + + it("returns true for empty arrays", () => { + expect(isSameArray([], [])).toBe(true); + }); + + it("uses loose equality", () => { + expect(isSameArray([1, "2"], [1, 2])).toBe(true); + }); + }); + + describe("encodeSharpCharacter", () => { + it("encodes # as %23", () => { + expect(encodeSharpCharacter("file#name")).toBe("file%23name"); + }); + + it("encodes multiple # characters", () => { + expect(encodeSharpCharacter("a#b#c")).toBe("a%23b%23c"); + }); + + it("returns string unchanged without #", () => { + expect(encodeSharpCharacter("filename")).toBe("filename"); + }); + }); + + describe("getRegExp", () => { + it("escapes special regex characters", () => { + expect(getRegExp("file.name")).toBe("file\\.name"); + expect(getRegExp("a+b")).toBe("a\\+b"); + expect(getRegExp("a*b")).toBe("a\\*b"); + expect(getRegExp("a?b")).toBe("a\\?b"); + expect(getRegExp("(group)")).toBe("\\(group\\)"); + expect(getRegExp("[class]")).toBe("\\[class\\]"); + expect(getRegExp("{brace}")).toBe("\\{brace\\}"); + expect(getRegExp("a^b")).toBe("a\\^b"); + expect(getRegExp("a$b")).toBe("a\\$b"); + expect(getRegExp("a|b")).toBe("a\\|b"); + }); + + it("leaves plain strings unchanged", () => { + expect(getRegExp("simple")).toBe("simple"); + }); + }); +}); diff --git a/test/mhtml-util.test.js b/test/mhtml-util.test.js new file mode 100644 index 000000000..7f9731fea --- /dev/null +++ b/test/mhtml-util.test.js @@ -0,0 +1,185 @@ +import { describe, it, expect } from "vitest"; +import { + getCharset, + replaceCharset, + isDocument, + isStylesheet, + isText, + getBoundary, + decodeQuotedPrintable, + decodeMimeHeader, + resolvePath, + indexOf, + isLineFeed, + endsWithCRLF, + startsWithBoundary +} from "../src/lib/mhtml-to-html/util.js"; + +describe("mhtml-to-html/util", () => { + describe("getCharset", () => { + it("extracts charset from content-type", () => { + expect(getCharset("text/html; charset=utf-8")).toBe("utf-8"); + }); + + it("extracts quoted charset", () => { + expect(getCharset("text/html; charset=\"UTF-8\"")).toBe("utf-8"); + }); + + it("returns undefined when no charset", () => { + expect(getCharset("text/html")).toBeUndefined(); + }); + }); + + describe("replaceCharset", () => { + it("replaces charset in content-type", () => { + expect(replaceCharset("text/html; charset=utf-8", "iso-8859-1")).toBe("text/html; charset=iso-8859-1"); + }); + }); + + describe("content-type classification", () => { + it("isDocument matches text/html", () => { + expect(isDocument("text/html")).toBe(true); + }); + + it("isDocument matches application/xhtml+xml", () => { + expect(isDocument("application/xhtml+xml")).toBe(true); + }); + + it("isDocument rejects text/plain", () => { + expect(isDocument("text/plain")).toBe(false); + }); + + it("isStylesheet matches text/css", () => { + expect(isStylesheet("text/css")).toBe(true); + }); + + it("isStylesheet rejects text/html", () => { + expect(isStylesheet("text/html")).toBe(false); + }); + + it("isText matches text/* types", () => { + expect(isText("text/plain")).toBe(true); + expect(isText("text/html")).toBe(true); + expect(isText("text/css")).toBe(true); + }); + + it("isText rejects non-text types", () => { + expect(isText("image/png")).toBe(false); + }); + }); + + describe("getBoundary", () => { + it("extracts boundary from content-type", () => { + expect(getBoundary("multipart/related;boundary=----=_Part_123")).toBe("----=_Part_123"); + }); + + it("extracts quoted boundary", () => { + expect(getBoundary("multipart/related;boundary=\"----=_Part_123\"")).toBe("----=_Part_123"); + }); + + it("returns undefined when no boundary", () => { + expect(getBoundary("text/html")).toBeUndefined(); + }); + }); + + describe("decodeQuotedPrintable", () => { + it("decodes =3D to equals sign", () => { + const input = new Uint8Array([0x3D, 0x33, 0x44]); // =3D + const result = decodeQuotedPrintable(input); + expect(result).toEqual(new Uint8Array([0x3D])); // = + }); + + it("passes through normal bytes unchanged", () => { + const input = new Uint8Array([0x48, 0x65, 0x6C, 0x6C, 0x6F]); // Hello + const result = decodeQuotedPrintable(input); + expect(result).toEqual(input); + }); + + it("handles mixed encoded and plain bytes", () => { + const input = new Uint8Array([0x41, 0x3D, 0x34, 0x32, 0x43]); // A=42C + const result = decodeQuotedPrintable(input); + expect(result).toEqual(new Uint8Array([0x41, 0x42, 0x43])); // ABC + }); + }); + + describe("decodeMimeHeader", () => { + it("decodes base64 MIME encoded word", () => { + // =?utf-8?B?SGVsbG8=?= is "Hello" in base64 + expect(decodeMimeHeader("=?utf-8?B?SGVsbG8=?=")).toBe("Hello"); + }); + + it("returns empty string for null input", () => { + expect(decodeMimeHeader(null)).toBe(""); + }); + + it("returns plain string as-is", () => { + expect(decodeMimeHeader("plain text")).toBe("plain text"); + }); + }); + + describe("resolvePath", () => { + it("resolves relative URL against base", () => { + expect(resolvePath("image.png", "https://example.com/page/")).toBe("https://example.com/page/image.png"); + }); + + it("resolves absolute path against base", () => { + expect(resolvePath("/image.png", "https://example.com/page/")).toBe("https://example.com/image.png"); + }); + + it("returns data: URIs unchanged", () => { + const dataUri = "data:image/png;base64,abc123"; + expect(resolvePath(dataUri, "https://example.com/")).toBe(dataUri); + }); + + it("returns path as-is when no base", () => { + expect(resolvePath("image.png")).toBe("image.png"); + }); + + it("handles protocol-relative URLs", () => { + const result = resolvePath("//cdn.example.com/img.png", "https://example.com/"); + expect(result).toBe("https://cdn.example.com/img.png"); + }); + }); + + describe("byte-level utilities", () => { + it("indexOf finds string in byte array", () => { + const encoder = new TextEncoder(); + const array = encoder.encode("Hello World"); + expect(indexOf(array, "World")).toBe(6); + }); + + it("indexOf returns -1 when not found", () => { + const encoder = new TextEncoder(); + const array = encoder.encode("Hello"); + expect(indexOf(array, "World")).toBe(-1); + }); + + it("isLineFeed detects CRLF", () => { + expect(isLineFeed(new Uint8Array([0x0D, 0x0A]))).toBe(true); + }); + + it("isLineFeed detects LF", () => { + expect(isLineFeed(new Uint8Array([0x0A]))).toBe(true); + }); + + it("isLineFeed rejects other bytes", () => { + expect(isLineFeed(new Uint8Array([0x41]))).toBe(false); + }); + + it("endsWithCRLF detects CRLF at end", () => { + expect(endsWithCRLF(new Uint8Array([0x41, 0x0D, 0x0A]))).toBe(true); + }); + + it("endsWithCRLF rejects when no CRLF at end", () => { + expect(endsWithCRLF(new Uint8Array([0x41, 0x42]))).toBe(false); + }); + + it("startsWithBoundary detects -- prefix", () => { + expect(startsWithBoundary(new Uint8Array([0x2D, 0x2D, 0x41]))).toBe(true); + }); + + it("startsWithBoundary rejects non-boundary", () => { + expect(startsWithBoundary(new Uint8Array([0x41, 0x42]))).toBe(false); + }); + }); +}); diff --git a/test/srcset-parser.test.js b/test/srcset-parser.test.js new file mode 100644 index 000000000..42eedd544 --- /dev/null +++ b/test/srcset-parser.test.js @@ -0,0 +1,100 @@ +import { describe, it, expect } from "vitest"; +import { parse, serialize } from "../src/lib/mhtml-to-html/srcset-parser.js"; + +describe("srcset-parser", () => { + describe("parse", () => { + it("parses a single URL with no descriptor", () => { + const result = parse("image.png"); + expect(result).toEqual([{ url: "image.png" }]); + }); + + it("parses a single URL with width descriptor", () => { + const result = parse("image.png 400w"); + expect(result).toEqual([{ url: "image.png", w: 400 }]); + }); + + it("parses a single URL with pixel density descriptor", () => { + const result = parse("image.png 2x"); + expect(result).toEqual([{ url: "image.png", d: 2 }]); + }); + + it("parses multiple candidates", () => { + const result = parse("small.png 320w, medium.png 640w, large.png 1024w"); + expect(result).toEqual([ + { url: "small.png", w: 320 }, + { url: "medium.png", w: 640 }, + { url: "large.png", w: 1024 } + ]); + }); + + it("parses mixed width and density descriptors", () => { + const result = parse("low.png 1x, high.png 2x"); + expect(result).toEqual([ + { url: "low.png", d: 1 }, + { url: "high.png", d: 2 } + ]); + }); + + it("handles leading and trailing whitespace", () => { + const result = parse(" image.png 400w "); + expect(result).toEqual([{ url: "image.png", w: 400 }]); + }); + + it("handles extra whitespace between candidates", () => { + const result = parse("a.png 1x , b.png 2x"); + expect(result).toEqual([ + { url: "a.png", d: 1 }, + { url: "b.png", d: 2 } + ]); + }); + + it("returns empty array for empty input", () => { + const result = parse(""); + expect(result).toEqual([]); + }); + + it("returns empty array for whitespace-only input", () => { + const result = parse(" "); + expect(result).toEqual([]); + }); + + it("parses floating-point density descriptors", () => { + const result = parse("image.png 1.5x"); + expect(result).toEqual([{ url: "image.png", d: 1.5 }]); + }); + }); + + describe("serialize", () => { + it("serializes a single URL with no descriptor", () => { + expect(serialize([{ url: "image.png" }])).toBe("image.png"); + }); + + it("serializes width descriptors", () => { + expect(serialize([{ url: "image.png", w: 400 }])).toBe("image.png 400w"); + }); + + it("serializes density descriptors", () => { + expect(serialize([{ url: "image.png", d: 2 }])).toBe("image.png 2x"); + }); + + it("serializes multiple candidates", () => { + const result = serialize([ + { url: "small.png", w: 320 }, + { url: "large.png", w: 1024 } + ]); + expect(result).toBe("small.png 320w, large.png 1024w"); + }); + }); + + describe("round-trip", () => { + it("preserves width descriptors through parse/serialize", () => { + const input = "small.png 320w, large.png 1024w"; + expect(serialize(parse(input))).toBe(input); + }); + + it("preserves density descriptors through parse/serialize", () => { + const input = "low.png 1x, high.png 2x"; + expect(serialize(parse(input))).toBe(input); + }); + }); +}); diff --git a/test/yabson.test.js b/test/yabson.test.js new file mode 100644 index 000000000..c19422783 --- /dev/null +++ b/test/yabson.test.js @@ -0,0 +1,148 @@ +import { describe, it, expect } from "vitest"; +import { serialize, parse } from "../src/lib/yabson/yabson.js"; + +async function roundTrip(value) { + const serialized = await serialize(value); + return parse(serialized); +} + +describe("yabson", () => { + describe("primitives", () => { + it("round-trips a string", async () => { + expect(await roundTrip("hello")).toBe("hello"); + }); + + it("round-trips an empty string", async () => { + expect(await roundTrip("")).toBe(""); + }); + + it("round-trips an integer", async () => { + expect(await roundTrip(42)).toBe(42); + }); + + it("round-trips a float", async () => { + expect(await roundTrip(3.14)).toBeCloseTo(3.14); + }); + + it("round-trips a negative number", async () => { + expect(await roundTrip(-100)).toBe(-100); + }); + + it("round-trips zero", async () => { + expect(await roundTrip(0)).toBe(0); + }); + + it("round-trips true", async () => { + expect(await roundTrip(true)).toBe(true); + }); + + it("round-trips false", async () => { + expect(await roundTrip(false)).toBe(false); + }); + + it("round-trips null", async () => { + expect(await roundTrip(null)).toBeNull(); + }); + + it("round-trips undefined", async () => { + expect(await roundTrip(undefined)).toBeUndefined(); + }); + + it("round-trips NaN", async () => { + expect(await roundTrip(NaN)).toBeNaN(); + }); + }); + + describe("containers", () => { + it("round-trips a plain object", async () => { + const obj = { a: 1, b: "two", c: true }; + expect(await roundTrip(obj)).toEqual(obj); + }); + + it("round-trips an empty object", async () => { + expect(await roundTrip({})).toEqual({}); + }); + + it("round-trips an array", async () => { + expect(await roundTrip([1, 2, 3])).toEqual([1, 2, 3]); + }); + + it("round-trips an empty array", async () => { + expect(await roundTrip([])).toEqual([]); + }); + + it("round-trips nested structures", async () => { + const nested = { a: [1, { b: [2, 3] }], c: { d: { e: "deep" } } }; + expect(await roundTrip(nested)).toEqual(nested); + }); + }); + + describe("special types", () => { + it("round-trips a Date", async () => { + const date = new Date("2024-01-15T12:00:00Z"); + const result = await roundTrip(date); + expect(result).toBeInstanceOf(Date); + expect(result.getTime()).toBe(date.getTime()); + }); + + it("round-trips a RegExp", async () => { + const regex = /foo.*bar/gi; + const result = await roundTrip(regex); + expect(result).toBeInstanceOf(RegExp); + expect(result.source).toBe(regex.source); + expect(result.flags).toBe(regex.flags); + }); + + it("round-trips a Map", async () => { + const map = new Map([["key1", "value1"], ["key2", 42]]); + const result = await roundTrip(map); + expect(result).toBeInstanceOf(Map); + expect(result.get("key1")).toBe("value1"); + expect(result.get("key2")).toBe(42); + }); + + it("round-trips a Set", async () => { + const set = new Set([1, 2, 3, "four"]); + const result = await roundTrip(set); + expect(result).toBeInstanceOf(Set); + expect(result.has(1)).toBe(true); + expect(result.has("four")).toBe(true); + expect(result.size).toBe(4); + }); + + it("round-trips an Error", async () => { + const error = new Error("test error"); + const result = await roundTrip(error); + expect(result).toBeInstanceOf(Error); + expect(result.message).toBe("test error"); + }); + }); + + describe("typed arrays", () => { + it("round-trips a Uint8Array", async () => { + const arr = new Uint8Array([1, 2, 3, 255]); + const result = await roundTrip(arr); + expect(result).toBeInstanceOf(Uint8Array); + expect(Array.from(result)).toEqual([1, 2, 3, 255]); + }); + + it("round-trips a Float64Array", async () => { + const arr = new Float64Array([1.1, 2.2, 3.3]); + const result = await roundTrip(arr); + expect(result).toBeInstanceOf(Float64Array); + expect(Array.from(result)).toEqual([1.1, 2.2, 3.3]); + }); + }); + + describe("edge cases", () => { + it("round-trips a large string", async () => { + const large = "x".repeat(100000); + expect(await roundTrip(large)).toBe(large); + }); + + it("round-trips an array with mixed types", async () => { + const mixed = [1, "two", true, null, { a: 1 }]; + expect(await roundTrip(mixed)).toEqual(mixed); + }); + }); +}); diff --git a/vitest.config.js b/vitest.config.js new file mode 100644 index 000000000..e26154197 --- /dev/null +++ b/vitest.config.js @@ -0,0 +1,7 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["test/**/*.test.js"] + } +});