From dea061a5ac8b2f83152cab3334404eb6832e9ffb Mon Sep 17 00:00:00 2001 From: Shinrai Date: Sat, 3 Oct 2026 19:28:15 -0700 Subject: [PATCH 1/2] test: migrate the suite to @cldmv/vitest-runner and enable coverage Replace mocha + chai with the fleet-standard vitest setup: - Port test/wisp.spec.mjs to tests/wisp.test.vitest.mjs (same 23 cases, same assertions) and move the fixtures to tests/fixtures/. The node:test CommonJS entry check moves to tests/cjs/entry.test.cjs and still runs after vitest in `test` and `coverage`. - Add .configs/vitest.config.mjs and tests/run-vitest.mjs. The config disables Vite's module runner (experimental.viteModuleRunner/nodeLoader) so src/ is loaded by Node's own import(): the module runner ignores import attributes and loads JSON itself, which would test Vite instead of wisp's with/assert/fs strategies. Coverage covers src/ and both entry points, with a json-summary reporter for the badge. - Add characterization tests for input forms, base resolution, the legacy assert strategy, the fs fallback and its errors, the structuredClone fallback, the caller resolver's frame selection, and the index.cjs version guard. Coverage is 100% lines/statements/branches/functions. - Mark the resolveWith non-string guard as unreachable (both wrappers call path.isAbsolute first, which already throws). - Scripts: test, test:watch, coverage, ci:coverage; drop mocha and chai. - ESLint test globals move from mocha on test/**/*.mjs to the vitest set on tests/**/*.test.vitest.mjs; fixture ignores follow the move. - ci.yml: enable the coverage badge and PR coverage comment, and build the coverage legs with `npm run build:types` (wisp ships src/ directly). --- .configs/eslint.config.mjs | 19 +- .configs/fix-headers.json | 2 +- .configs/vitest.config.mjs | 54 + .github/workflows/ci.yml | 20 +- .github/workflows/publish.yml | 2 +- .prettierignore | 2 +- package-lock.json | 1544 ++++++++++++++--- package.json | 15 +- src/lib/resolve-from-caller.mjs | 2 + tests/cjs-entry.test.vitest.mjs | 66 + {test => tests/cjs}/entry.test.cjs | 12 +- {test => tests}/fixtures/caller.js | 0 {test => tests}/fixtures/invalid.json | 0 tests/fixtures/module.mjs | 2 + {test => tests}/fixtures/nested/ok.json | 0 tests/fixtures/no-default.mjs | 2 + {test => tests}/fixtures/sample.json | 0 tests/import-strategies.test.vitest.mjs | 272 +++ tests/resolve-from-caller.test.vitest.mjs | 224 +++ tests/run-vitest.mjs | 67 + .../wisp.test.vitest.mjs | 60 +- tsconfig.json | 2 +- types/lib/resolve-from-caller.d.mts.map | 2 +- 23 files changed, 2052 insertions(+), 317 deletions(-) create mode 100644 .configs/vitest.config.mjs create mode 100644 tests/cjs-entry.test.vitest.mjs rename {test => tests/cjs}/entry.test.cjs (84%) rename {test => tests}/fixtures/caller.js (100%) rename {test => tests}/fixtures/invalid.json (100%) create mode 100644 tests/fixtures/module.mjs rename {test => tests}/fixtures/nested/ok.json (100%) create mode 100644 tests/fixtures/no-default.mjs rename {test => tests}/fixtures/sample.json (100%) create mode 100644 tests/import-strategies.test.vitest.mjs create mode 100644 tests/resolve-from-caller.test.vitest.mjs create mode 100644 tests/run-vitest.mjs rename test/wisp.spec.mjs => tests/wisp.test.vitest.mjs (76%) diff --git a/.configs/eslint.config.mjs b/.configs/eslint.config.mjs index 63429eb..f680449 100644 --- a/.configs/eslint.config.mjs +++ b/.configs/eslint.config.mjs @@ -36,7 +36,7 @@ export default defineConfig([ "reference/**", "*.min.*", // Deliberately-non-JS data files with a .js extension (fallback-loading tests); test data, not source. - "test/fixtures/**", + "tests/fixtures/**", "**/package-lock.json", // Copy file patterns "*copy/", @@ -73,7 +73,22 @@ export default defineConfig([ }, { files: ["**/*.js"], languageOptions: { sourceType: "commonjs" } }, { files: ["**/*.{js,mjs,cjs}"], languageOptions: { globals: { ...globals.node, ...globals.browser } } }, - { files: ["test/**/*.mjs"], languageOptions: { globals: { ...globals.mocha } } }, + { + files: ["tests/**/*.test.vitest.mjs"], + languageOptions: { + globals: { + beforeAll: true, + beforeEach: true, + afterAll: true, + afterEach: true, + describe: true, + it: true, + expect: true, + test: true, + vi: true + } + } + }, { files: ["**/*.json"], plugins: { json }, language: "json/json", extends: ["json/recommended"] }, { files: ["**/*.jsonc"], plugins: { json }, language: "json/jsonc", extends: ["json/recommended"] }, { files: ["**/*.json5"], plugins: { json }, language: "json/json5", extends: ["json/recommended"] }, diff --git a/.configs/fix-headers.json b/.configs/fix-headers.json index a9588e8..33ba6d5 100644 --- a/.configs/fix-headers.json +++ b/.configs/fix-headers.json @@ -1,4 +1,4 @@ { "extends": "@cldmv/configs/fix-headers.json", - "excludeFolders": ["test/fixtures"] + "excludeFolders": ["tests/fixtures"] } diff --git a/.configs/vitest.config.mjs b/.configs/vitest.config.mjs new file mode 100644 index 0000000..867e8b2 --- /dev/null +++ b/.configs/vitest.config.mjs @@ -0,0 +1,54 @@ +/** + * + * @Project: @cldmv/wisp + * @Filename: /.configs/vitest.config.mjs + * @Date: 2026-10-03T19:21:07-07:00 (1791080467) + * @Author: Nate Corcoran + * @Email: + * ----- + * @Last modified by: Nate Corcoran (Shinrai@users.noreply.github.com) + * @Last modified time: 2026-10-03T19:27:20-07:00 (1791080840) + * ----- + * @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved. + * + */ + +import { defineConfig } from "vitest/config"; +import { fileURLToPath } from "node:url"; +import path from "node:path"; + +// Anchor the project root to the package directory so include/exclude work no +// matter what cwd vitest is invoked from. +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); + +export default defineConfig({ + root, + test: { + include: ["tests/**/*.test.vitest.mjs"], + exclude: ["node_modules"], + environment: "node", + testTimeout: 30000, + // Run test files and the code under test through Node's native import() + // instead of Vite's module runner. wisp's behaviour depends on Node's real + // dynamic-import semantics (import attributes `with` / legacy `assert`, JSON + // module loading); the module runner ignores import attributes and loads JSON + // itself, so under it the suite would exercise Vite rather than Node. + // nodeLoader: false also skips Vitest's own loader transform (only needed for + // vi.mock / import.meta.vitest, which this suite does not use), so V8 coverage is + // measured against the files exactly as Node runs them. + experimental: { + viteModuleRunner: false, + nodeLoader: false + }, + // "dot" keeps CI logs to one character per test file; the final + // "Test Files X passed" / "Tests Y passed" summary is unaffected. + reporters: ["dot"], + coverage: { + provider: "v8", + // wisp publishes src/ directly (no dist build), plus the two entry points. + include: ["src/**/*.mjs", "index.mjs", "index.cjs"], + exclude: ["**/*.json", "tests/**", "src/types/**"], + reporter: ["text", "html", "json-summary", "json"] + } + } +}); diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 96ed70b..2d59b92 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -234,7 +234,7 @@ jobs: # per-version regression. workflow_dispatch can still opt out (set false). lts_only_matrix: ${{ github.event.inputs.lts_only_matrix != 'false' }} package_manager: ${{ github.event.inputs.package_manager || 'npm' }} - test_command: "npm test" # mocha (test/**/*.mjs) + test_command: "npm test" # vitest via @cldmv/vitest-runner (tests/**/*.test.vitest.mjs), then the node:test CJS entry check # test_command: "NODE_OPTIONS='--conditions=slothlet-dev' npm test" # Override NODE_OPTIONS only # test_command: "NODE_ENV=test npm test" # Override NODE_ENV only # test_command: "NODE_ENV=test NODE_OPTIONS='--conditions=slothlet-dev' npm test" # Override both @@ -253,8 +253,13 @@ jobs: # wisp's "build" is type-declaration generation. `build:types` (tsc) both # emits types/ AND type-checks (tsc fails on type errors), so it carries # the type check on every matrix node. Not `build:ci` — that also re-runs - # mocha, which `test_command` above already ran. + # the test suite, which `test_command` above already ran. build_command: "npm run build:types" + # The coverage legs run their own build step before measuring. wisp ships + # src/ directly (no bundle), so its real build is the same type-declaration + # generation as the CI build; it leaves src/ in place, which is what the + # coverage include measures. + coverage_build_command: "npm run build:types" skip_performance_tests: false skip_matrix_tests: false @@ -264,10 +269,9 @@ jobs: # Only runs on direct pushes to default_branch — PRs and feature branches # are automatically skipped so coverage always reflects merged master code. # Requires: the coverage_command produces coverage/coverage-summary.json - # wisp has no coverage tooling (mocha only, no `ci:coverage` script, no - # coverage-summary.json, and no `badges` branch), so the coverage-badge - # job is disabled. Enable it once a coverage command is added. - enable_coverage_badge: false + # `npm run ci:coverage` runs the vitest suite with v8 coverage, whose + # json-summary reporter writes coverage/coverage-summary.json. + enable_coverage_badge: ${{ github.event.inputs.enable_coverage_badge != 'false' }} default_branch: ${{ github.event.inputs.default_branch || 'master' }} # Badge only pushed when a push lands on this branch coverage_command: ${{ github.event.inputs.coverage_command || 'npm run ci:coverage' }} coverage_summary_path: ${{ github.event.inputs.coverage_summary_path || 'coverage/coverage-summary.json' }} @@ -283,9 +287,7 @@ jobs: # Injects a Shields.io badge + breakdown table directly into the PR body # on every push to the PR branch. Only fires on pull_request events; # skipped automatically on push and workflow_dispatch. No files committed. - # Disabled for the same reason as enable_coverage_badge above — no - # coverage data is produced, so there is nothing to inject into the PR. - enable_coverage_pr_comment: false + enable_coverage_pr_comment: ${{ github.event.inputs.enable_coverage_pr_comment != 'false' }} # Authentication & Bot Configuration # The workflow supports automatic App token detection for enhanced permissions and proper attribution: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 8fde91b..0cd2fef 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -110,7 +110,7 @@ jobs: github_packages_publish_command: "" min_node_version: ${{ github.event.inputs.min_node_version || '22.12.0' }} max_node_major: ${{ github.event.inputs.max_node_major || '' }} # blank ⇒ inherit the CLDMV/.github reusable default - test_command: "npm test" # mocha (test/**/*.mjs) + test_command: "npm test" # vitest via @cldmv/vitest-runner (tests/**/*.test.vitest.mjs), then the node:test CJS entry check # test_command: "NODE_OPTIONS='--conditions=slothlet-dev' npm test" # Override NODE_OPTIONS only # test_command: "NODE_ENV=test npm test" # Override NODE_ENV only # test_command: "NODE_ENV=test NODE_OPTIONS='--conditions=slothlet-dev' npm test" # Override both diff --git a/.prettierignore b/.prettierignore index 7e3640f..2e10d52 100644 --- a/.prettierignore +++ b/.prettierignore @@ -15,4 +15,4 @@ package-lock.json *.yml *.yaml reference/ -test/fixtures/ +tests/fixtures/ diff --git a/package-lock.json b/package-lock.json index 0150b47..b2d1eac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,17 +14,18 @@ "@cldmv/fix-headers": "^2.1.2", "@cldmv/jsonv": "^1.1.3", "@cldmv/prettier-plugin-jsonv": "^1.1.3", + "@cldmv/vitest-runner": "^1.5.3", "@eslint/css": "^2.0.0", "@eslint/js": "^10.0.1", "@eslint/json": "^2.1.0", "@eslint/markdown": "^8.0.3", "@types/node": "^26.6.2", - "chai": "^6.2.2", + "@vitest/coverage-v8": "^5.0.3", "eslint": "^10.12.0", "globals": "^17.13.0", - "mocha": "^12.0.1", "prettier": "^3.9.9", - "typescript": "^6.0.3" + "typescript": "^6.0.3", + "vitest": "^5.0.3" }, "engines": { "node": ">=16.0.0" @@ -34,6 +35,66 @@ "url": "https://github.com/sponsors/shinrai" } }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", + "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@cacheable/memory": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz", @@ -153,6 +214,25 @@ "prettier": "^3.0.0" } }, + "node_modules/@cldmv/vitest-runner": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@cldmv/vitest-runner/-/vitest-runner-1.5.3.tgz", + "integrity": "sha512-99+r+7ricaaCjLSYaRzE9d4Ab/NAG6Vgu8fNpGdep9n+DAi98NH//iHsdQjw9yaVh2IXvp37IBRKR8VTETbyZg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "chalk": "^6.0.1" + }, + "bin": { + "vitest-runner": "bin/vitest-runner.mjs" + }, + "engines": { + "node": ">=22.12.0" + }, + "peerDependencies": { + "vitest": ">=1.0.0" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.10.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", @@ -428,6 +508,34 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@keyv/bigmap": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", @@ -452,6 +560,324 @@ "dev": true, "license": "MIT" }, + "node_modules/@oxc-project/types": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.152.0.tgz", + "integrity": "sha512-oM/5rLBm2tPkg0iBgkH/FOeR3PCDpY19GTgAZjMFM8h9WI9VW7cLgzp6nwtarYKmovavIQZ+Fe/RKX/8C8O/Rw==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.12.tgz", + "integrity": "sha512-dB/a1214qKfHMXCpgqR4OZT+jS4kTyEXbQGJPqzobt5EwH5rX080pxE37alt3RzvR1bf1Yz/yGqRfrYAxuPw0A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.12.tgz", + "integrity": "sha512-7KHFgQ5VJxIHcLlrwrc3Xbds7oTNQT7Pgi9gQCJKrd2VGab/UksIOYp6VD8MzCstGxOKMgNamPwUCfxPdP1OHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.12.tgz", + "integrity": "sha512-3YIhqHD96nA5SaYNRBR16HnGv4oavZvXfD/ayHM+oYZ0WD/8lBAtf6zQua4kEyAvpqrluKXl0lnOBoiNby7x9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.12.tgz", + "integrity": "sha512-UuuJ35MFw4gmFOrE9pEqIV+K3syIKveph+Qc1/ljHZVdoDW4pz/JHR/eMVom+TZGl/5OOvGJOWaOCVt3ZfqhxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.12.tgz", + "integrity": "sha512-uMvssit0a4W+/7D8CbHUvG719mH3R2jwXAlh/XcPvuHTE0g++LymF88DCGNX0HM2rBOn0xrzgXktIB6fLSJBTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.12.tgz", + "integrity": "sha512-XcFu0R0xWnwzSf4IQgFH1rJIckPN1pLy2R+4r9IDB7Yfu/ys9cVqfa4pBrMHj7a3gl8mIR4nRNPg0e5IvEVs6g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.12.tgz", + "integrity": "sha512-260UrKgn8tz39ak+SMDOirKzr7V04M9dWPw5llW00SwBivCZoWcRBKV1d8cXnRkUmSZA3BdiUmBHWk7734Ulpw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.12.tgz", + "integrity": "sha512-5YK1I9SqDkbPgc1IA8BgDl34suqUS2q0KWnBrirm0E51YjOs6eo6dV6jbQfNE/argHRSvd0QUGgtpIoYx+WWpw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.12.tgz", + "integrity": "sha512-Rkcrmp7eFRg74yL5fXEU91JEWbdEPLevWwGtXpmhbjlD1StScbWTmO94Bhly+Mo+ketKYkdmM1vNUKeWSlx8cQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.12.tgz", + "integrity": "sha512-qvK4DuAsQc2BSjlx+Xr+IzOIvvxbGZqxFwdWfG6F518Erj0GGISyQbJ6pIappnOxlNPzNHvo/L0BwB30GZ+zVw==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.12.tgz", + "integrity": "sha512-Q9uLBO53Xd4QIq1WOycVQyPP1O4HhraEV2qqb3uTrnVw6QZih9duY4vNXOivL1xoUS1/z+W8eF4NMfl2a8Sdjw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.12.tgz", + "integrity": "sha512-3IBxWFMjbOZskDPKv8Lf9BCnahlKuHthWkYnyIxOH/QcJrFcS4EmcenthApkwr/5+nEqZlLzeYbxeMaX7A5u4g==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.12.tgz", + "integrity": "sha512-xtX61xg4LKPkPWilZU1ynKClz5Gj4bf74LML4r3eVLWumKnGjoEr1OSHQhMdbBDoYTi+yjrujvpZe2pUnqCrrA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.12.tgz", + "integrity": "sha512-At7fPB6PCaIjzgIhEZFxuT+BBFqiQibJDT4d3PhiR3f4E7bbMZF4aKblbFfEM3sETRDd1YiQx/+U/g/B/ou5Ew==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.12.tgz", + "integrity": "sha512-WIw2haVKwjuYdXkHaoC0mF8Le71TuCBxjrdKqLbJGctbBABj+ClfmNvtbOnzpq3RokNo5+V1qhtSzJyXorsklQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "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/debug": { "version": "4.1.13", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", @@ -462,6 +888,13 @@ "@types/ms": "*" } }, + "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", @@ -534,6 +967,96 @@ "dev": true, "license": "MIT" }, + "node_modules/@vitest/coverage-v8": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-5.0.3.tgz", + "integrity": "sha512-+klsyz7BvT1vCU28Zkfzms1Ia78XD0V41U3FUtRaa3S+vOr/EXvK1O6BvVD7l1RzEjm6SONR2aybOvDDf9u0mQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/istanbul-lib-coverage": "^1.0.0", + "@vitest/istanbul-lib-report": "^1.0.0", + "ast-v8-to-istanbul": "^1.0.5", + "magicast": "^0.5.4", + "obug": "^2.1.4", + "std-env": "^4.2.0", + "tinyrainbow": "^3.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "5.0.3", + "vitest": "5.0.3" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/istanbul-lib-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@vitest/istanbul-lib-coverage/-/istanbul-lib-coverage-1.0.2.tgz", + "integrity": "sha512-9J/JMwOf9AoJhAywhrn7ScKTL38hsWQP/qPG60OtaAFcQ5OXPwKsxZFlbnuCKmZ61m8/lGgHYnFpdyQZUvG/iA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22" + } + }, + "node_modules/@vitest/istanbul-lib-report": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@vitest/istanbul-lib-report/-/istanbul-lib-report-1.0.2.tgz", + "integrity": "sha512-gUsfXZJbzPamoIY5TvHFiMMoXESBrUMo+xqaj+rYrWI69+EnvRlYBlP96ZnHPY4vX8kyUpgAnFUCg5wZG/HkDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/istanbul-lib-coverage": "1.0.2" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@vitest/mocker": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.3.tgz", + "integrity": "sha512-T8sWAIbkSyAjkwTcaEc3Iu0o9A27X1/kdXrizhZkGuSKScRQtRzclfAMpOTcGdXCsqxeWlpGy3XjqaW8CpLORg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.31", + "@vitest/spy": "5.0.3", + "estree-walker": "^3.0.3", + "magic-string": "^1.2.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/spy": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.3.tgz", + "integrity": "sha512-XhFysQTB8AZ+P4gMi+Lpo99vg2AZi0qKpaB9yXQl37+CaMEAPO3iH/wGVnSyL5MPERiLezpqTVtrR6UZH5GCXg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/acorn": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", @@ -574,12 +1097,27 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/argparse": { + "node_modules/assertion-error": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "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/ast-v8-to-istanbul": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.7.tgz", + "integrity": "sha512-kFL68AG6ajd8fg248zwM9GQrUWEp79gsmjum34OEXjs4yHuUMZfYKwOLW9GMmB4oNvVrj+EAGxsP7ye2UR9UlA==", "dev": true, - "license": "Python-2.0" + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } }, "node_modules/balanced-match": { "version": "4.0.4", @@ -604,13 +1142,6 @@ "node": "20 || >=22" } }, - "node_modules/browser-stdout": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", - "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", - "dev": true, - "license": "ISC" - }, "node_modules/cacheable": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.5.0.tgz", @@ -637,15 +1168,28 @@ } }, "node_modules/chai": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", - "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.3.0.tgz", + "integrity": "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==", "dev": true, "license": "MIT", "engines": { "node": ">=18" } }, + "node_modules/chalk": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-6.0.1.tgz", + "integrity": "sha512-/Ce6KNm3vIbWdMlNna6RVIZ/ICQxnJxCicet5LBKK9ZffBkqzDw0xh9EiKSljdRtiIQ1S1z4YgcscUUGzNCWrA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/character-entities": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", @@ -657,22 +1201,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/chokidar": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", - "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", - "dev": true, - "license": "MIT", - "dependencies": { - "readdirp": "^5.0.0" - }, - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/commander": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", @@ -747,6 +1275,17 @@ "node": ">=6" } }, + "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", + "peer": true, + "engines": { + "node": ">=8" + } + }, "node_modules/devlop": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", @@ -761,15 +1300,12 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/diff": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-9.0.0.tgz", - "integrity": "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==", + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } + "license": "MIT" }, "node_modules/escape-string-regexp": { "version": "4.0.0", @@ -1007,6 +1543,16 @@ "node": ">=4.0" } }, + "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/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -1017,6 +1563,16 @@ "node": ">=0.10.0" } }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "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", @@ -1052,6 +1608,24 @@ "url": "https://github.com/sponsors/wooorm" } }, + "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": "11.1.5", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.5.tgz", @@ -1062,19 +1636,6 @@ "flat-cache": "^6.1.23" } }, - "node_modules/find-up-simple": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz", - "integrity": "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/flat-cache": { "version": "6.1.23", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.23.tgz", @@ -1103,6 +1664,22 @@ "node": ">=0.4.x" } }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/github-slugger": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", @@ -1110,24 +1687,6 @@ "dev": true, "license": "ISC" }, - "node_modules/glob": { - "version": "13.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", - "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "minimatch": "^10.2.2", - "minipass": "^7.1.3", - "path-scurry": "^2.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -1154,16 +1713,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/hashery": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz", @@ -1227,115 +1776,358 @@ "node": ">=0.10.0" } }, - "node_modules/is-path-inside": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", - "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/katex": { + "version": "0.16.47", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz", + "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", + "dev": true, + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, "engines": { - "node": ">=12" + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "peer": true, + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, "engines": { - "node": ">=10" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], "dev": true, - "license": "ISC" + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/js-yaml": { - "version": "5.4.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.4.2.tgz", - "integrity": "sha512-m+aqu+LwO1O6sIopafj8HUVl5aawITwZQe/yHpMCKjaWBaA/d07B/QdMb3529REftiU+RMMHL3Vlsw3hON7vWg==", + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/puzrin" - }, - { - "type": "github", - "url": "https://github.com/sponsors/nodeca" - } + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" ], - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" + "peer": true, + "engines": { + "node": ">= 12.0.0" }, - "bin": { - "js-yaml": "bin/js-yaml.mjs" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], "dev": true, - "license": "MIT" + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT" + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/katex": { - "version": "0.16.47", - "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz", - "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], "dev": true, - "funding": [ - "https://opencollective.com/katex", - "https://github.com/sponsors/katex" + "libc": [ + "glibc" ], - "license": "MIT", - "dependencies": { - "commander": "^8.3.0" + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" }, - "bin": { - "katex": "cli.js" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/keyv": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", - "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "@keyv/serialize": "^1.1.1" + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, "engines": { - "node": ">= 0.8.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, "node_modules/longest-streak": { @@ -1349,14 +2141,26 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/lru-cache": { - "version": "11.5.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", - "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "node_modules/magic-string": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.2.tgz", + "integrity": "sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g==", "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, + "node_modules/magicast": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.5.tgz", + "integrity": "sha512-UicdXN8zQ3JHlxVq+28afMXPr1z7WNY6+7EJnzTdQWkTAlMLF5fNCCKxJHBQwGaNGR11581EiQmQzx73+MvszA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "source-map-js": "^1.2.1" } }, "node_modules/markdown-table": { @@ -2310,54 +3114,33 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=16 || 14 >=14.17" - } + "license": "MIT" }, - "node_modules/mocha": { - "version": "12.0.2", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-12.0.2.tgz", - "integrity": "sha512-SjAulGHxlMJLCD1bhvJBAiYwYxglydRzA9PDQ5MALq1oLRMMtIhxSQJ0olCFgA7c9YbTdxC0KOV0wl8JEDYVhA==", + "node_modules/nanoid": { + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", - "dependencies": { - "browser-stdout": "^1.3.1", - "chokidar": "^5.0.0", - "debug": "^4.3.5", - "diff": "^9.0.0", - "find-up-simple": "^1.0.1", - "glob": "^13.0.0", - "is-path-inside": "^4.0.0", - "is-unicode-supported": "^0.1.0", - "js-yaml": "^5.0.0", - "minimatch": "^10.2.2", - "ms": "^2.1.3", - "picocolors": "^1.1.1", - "serialize-javascript": "^7.1.1", - "strip-json-comments": "^5.0.3", - "supports-color": "^8.1.1", - "workerpool": "^10.0.0" - }, + "peer": true, "bin": { - "mocha": "bin/mocha.js" + "nanoid": "bin/nanoid.cjs" }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -2365,6 +3148,20 @@ "dev": true, "license": "MIT" }, + "node_modules/obug": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", + "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -2403,29 +3200,56 @@ "node": ">=8" } }, - "node_modules/path-scurry": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", - "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "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": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, + "license": "ISC", + "peer": true + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", "engines": { - "node": "18 || 20 || >=22" + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", "dev": true, - "license": "ISC" + "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", + "peer": true, + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } }, "node_modules/prelude-ls": { "version": "1.2.1", @@ -2483,28 +3307,39 @@ "dev": true, "license": "MIT" }, - "node_modules/readdirp": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz", - "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", + "node_modules/rolldown": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.12.tgz", + "integrity": "sha512-8wafseiaG80xmXSfqidUNqZcylTlhmPZZt+za2m+js2sFZ8dTNlhIOV2WcbIPx2hgwPBJpEUGFAMZ9bgBBLTSQ==", "dev": true, "license": "MIT", - "engines": { - "node": ">= 20.19.0" + "peer": true, + "dependencies": { + "@oxc-project/types": "=0.152.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/serialize-javascript": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.1.2.tgz", - "integrity": "sha512-GL2BWwVa6JydKO6l/ljVgjAZF4QJ3S7dWDWi53s5GZT8PJzD2fNxi67HANQGKAqPrwEpL5ba7gUBGi0Ls/sEoQ==", - "dev": true, - "license": "BSD-3-Clause", "engines": { - "node": ">=20.0.0" + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.12", + "@rolldown/binding-android-arm64": "1.2.12", + "@rolldown/binding-darwin-arm64": "1.2.12", + "@rolldown/binding-darwin-x64": "1.2.12", + "@rolldown/binding-freebsd-x64": "1.2.12", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.12", + "@rolldown/binding-linux-arm64-gnu": "1.2.12", + "@rolldown/binding-linux-arm64-musl": "1.2.12", + "@rolldown/binding-linux-ppc64-gnu": "1.2.12", + "@rolldown/binding-linux-s390x-gnu": "1.2.12", + "@rolldown/binding-linux-x64-gnu": "1.2.12", + "@rolldown/binding-linux-x64-musl": "1.2.12", + "@rolldown/binding-openharmony-arm64": "1.2.12", + "@rolldown/binding-win32-arm64-msvc": "1.2.12", + "@rolldown/binding-win32-x64-msvc": "1.2.12" } }, "node_modules/shebang-command": { @@ -2540,33 +3375,58 @@ "node": ">=0.10.0" } }, - "node_modules/strip-json-comments": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", - "integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==", + "node_modules/std-env": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", + "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.2.0.tgz", + "integrity": "sha512-78U2TlB2CnVenajOFzf3BKSm0J6oz5L0NV7g32LCPccvYc0lbWvys4d3uUUCS2B1N8PAf2+aekR8i1KbC3HO7Q==", "dev": true, "license": "MIT", "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=20.0.0" } }, - "node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "node_modules/tinyexec": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { - "has-flag": "^4.0.0" + "fdir": "^6.5.0", + "picomatch": "^4.0.4" }, "engines": { - "node": ">=10" + "node": ">=12.0.0" }, "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.2.0.tgz", + "integrity": "sha512-LgO3D9yZJjApUiuUfl9iFAwrtaX4+lok3wJIqttGoKCHlWUqHqbQpnxCf82L8FjgKsh4iGo78hqJwgL8F6To2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" } }, "node_modules/type-check": { @@ -2687,6 +3547,168 @@ "punycode": "^2.1.0" } }, + "node_modules/vite": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.2.tgz", + "integrity": "sha512-SQr1x6W5vVSbROg7vsyXIaxK9b0G7zsT68acdWWRmnBUsgDieLCRG+Rep9WdZgcposvv/GSnr4GUUBqB3vXq6w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.28", + "rolldown": "~1.2.11", + "tinyglobby": "^0.2.17" + }, + "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.7.1", + "esbuild": "^0.27.0 || ^0.28.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": "5.0.3", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.3.tgz", + "integrity": "sha512-xMw97S3rjdtj5dkVat7jCsqWBpvchs3RlpQctUqwJD0KkERk40vz2fJ77lDwW/Vzh/pk18eItYAzkodhSes3jQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/mocker": "5.0.3", + "chai": "^6.2.2", + "es-module-lexer": "^2.3.2", + "expect-type": "^1.4.0", + "magic-string": "^1.2.3", + "obug": "^2.1.4", + "picomatch": "^4.0.7", + "std-env": "^4.2.0", + "tinybench": "^6.1.4", + "tinyexec": "^1.3.0", + "tinyglobby": "^0.2.17", + "why-is-node-running": "3.2.1" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^22.12.0 || ^24.0.0 || >=26.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "5.0.3", + "@vitest/browser-preview": "5.0.3", + "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", + "@vitest/coverage-istanbul": "5.0.3", + "@vitest/coverage-v8": "5.0.3", + "@vitest/ui": "5.0.3", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.4.0 || ^7.0.0 || ^8.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/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "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", @@ -2703,6 +3725,19 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-3.2.1.tgz", + "integrity": "sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==", + "dev": true, + "license": "MIT", + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=20.11" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -2713,13 +3748,6 @@ "node": ">=0.10.0" } }, - "node_modules/workerpool": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-10.0.3.tgz", - "integrity": "sha512-6z2Iis68Wqth93/G/wJP9u+R3O+d2XTlgWChGCwuT1qLbBsOYueGRZuJ++v3mtDP5KjYdy+WzvWC+VWETSVXJA==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/package.json b/package.json index 1ac1492..34d3ce7 100644 --- a/package.json +++ b/package.json @@ -10,9 +10,11 @@ } }, "scripts": { - "test": "mocha --recursive \"test/**/*.mjs\" && npm run test:cjs", - "test:cjs": "node --test test/entry.test.cjs", - "test:watch": "mocha --watch \"test/**/*.mjs\"", + "test": "node tests/run-vitest.mjs && npm run test:cjs", + "test:cjs": "node --test tests/cjs/entry.test.cjs", + "test:watch": "vitest --config .configs/vitest.config.mjs", + "coverage": "node tests/run-vitest.mjs --coverage-quiet && npm run test:cjs", + "ci:coverage": "npm run coverage", "lint": "eslint --config .configs/eslint.config.mjs .", "build:types": "tsc", "test:types": "tsc --noEmit", @@ -75,17 +77,18 @@ "@cldmv/fix-headers": "^2.1.2", "@cldmv/jsonv": "^1.1.3", "@cldmv/prettier-plugin-jsonv": "^1.1.3", + "@cldmv/vitest-runner": "^1.5.3", "@eslint/css": "^2.0.0", "@eslint/js": "^10.0.1", "@eslint/json": "^2.1.0", "@eslint/markdown": "^8.0.3", "@types/node": "^26.6.2", - "chai": "^6.2.2", + "@vitest/coverage-v8": "^5.0.3", "eslint": "^10.12.0", "globals": "^17.13.0", - "mocha": "^12.0.1", "prettier": "^3.9.9", - "typescript": "^6.0.3" + "typescript": "^6.0.3", + "vitest": "^5.0.3" }, "publishConfig": { "access": "public" diff --git a/src/lib/resolve-from-caller.mjs b/src/lib/resolve-from-caller.mjs index 84ba42e..646f8f6 100644 --- a/src/lib/resolve-from-caller.mjs +++ b/src/lib/resolve-from-caller.mjs @@ -323,6 +323,8 @@ function pickFallbackBaseFile() { * ); */ function resolveWith(rel, makePrimary, exists, makeFallback) { + // Unreachable through the exported wrappers: both call path.isAbsolute(rel) first, which already throws a TypeError for a non-string. + /* v8 ignore next */ if (typeof rel !== "string") throw new TypeError("rel must be a string"); // absolute / already-URL cases are handled in the public wrappers diff --git a/tests/cjs-entry.test.vitest.mjs b/tests/cjs-entry.test.vitest.mjs new file mode 100644 index 0000000..7215e0d --- /dev/null +++ b/tests/cjs-entry.test.vitest.mjs @@ -0,0 +1,66 @@ +/** + * + * @Project: @cldmv/wisp + * @Filename: /tests/cjs-entry.test.vitest.mjs + * @Date: 2026-10-03T19:27:00-07:00 (1791080820) + * @Author: Nate Corcoran + * @Email: + * ----- + * @Last modified by: Nate Corcoran (Shinrai@users.noreply.github.com) + * @Last modified time: 2026-10-03T19:27:20-07:00 (1791080840) + * ----- + * @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved. + * + */ + +/** + * @fileoverview Characterization tests for the CommonJS entry point (index.cjs). + * @module @cldmv/wisp.test.cjs-entry + * @internal + * @private + * + * @description + * tests/cjs/entry.test.cjs checks a real CommonJS consumer under `node --test`. These tests load + * the same entry through createRequire inside the Vitest run so its version guard is exercised + * and measured: the guard branch is driven by overriding the `process.features.require_module` + * getter for one load. + */ + +import { describe, it, expect, afterEach } from "vitest"; +import { createRequire } from "node:module"; +import * as esm from "../index.mjs"; + +const require = createRequire(import.meta.url); +const entry = require.resolve("../index.cjs"); + +afterEach(() => { + delete require.cache[entry]; +}); + +describe("index.cjs", () => { + it("re-exports the ESM entry's functions", () => { + const cjs = require("../index.cjs"); + expect(cjs).toBe(esm.default); + expect(cjs.default).toBe(esm.default); + expect(cjs.wisp).toBe(esm.wisp); + expect(cjs.wispSync).toBe(esm.wispSync); + }); + + it("throws ERR_REQUIRE_ESM with guidance when require(esm) is unavailable", () => { + const original = Object.getOwnPropertyDescriptor(process.features, "require_module"); + Object.defineProperty(process.features, "require_module", { configurable: true, enumerable: true, get: () => false }); + let err; + try { + require("../index.cjs"); + } catch (e) { + err = e; + } finally { + Object.defineProperty(process.features, "require_module", original); + } + expect(err).toBeInstanceOf(Error); + expect(err.code).toBe("ERR_REQUIRE_ESM"); + expect(err.message).toMatch( + /^@cldmv\/wisp: require\(\) needs Node\.js \^20\.19\.0 or >=22\.12\.0 \(this is v[\d.]+\)\. On older Node\.js, load the package with import\(\) instead\.$/ + ); + }); +}); diff --git a/test/entry.test.cjs b/tests/cjs/entry.test.cjs similarity index 84% rename from test/entry.test.cjs rename to tests/cjs/entry.test.cjs index b338f88..c904136 100644 --- a/test/entry.test.cjs +++ b/tests/cjs/entry.test.cjs @@ -1,7 +1,7 @@ /** * * @Project: @cldmv/wisp - * @Filename: /test/entry.test.cjs + * @Filename: /tests/cjs/entry.test.cjs * @Date: 2026-10-03T10:24:25-07:00 (1791048265) * @Author: Nate Corcoran * @Email: @@ -14,8 +14,8 @@ */ /** - * CommonJS entry tests. These run under Node's own test runner (`node --test`), not Mocha: - * Mocha's `test/**\/*.mjs` glob only picks up ESM specs, so it cannot show whether a plain + * CommonJS entry tests. These run under Node's own test runner (`node --test`), not Vitest: + * Vitest loads every file through its own module pipeline, so it cannot show whether a plain * `require()` of the package works the way it does for a CommonJS consumer. */ "use strict"; @@ -25,11 +25,11 @@ const assert = require("node:assert/strict"); const { spawnSync } = require("node:child_process"); const path = require("node:path"); -const repoRoot = path.resolve(__dirname, ".."); +const repoRoot = path.resolve(__dirname, "..", ".."); test("require() returns the same wisp object as import", async () => { - const cjs = require("../index.cjs"); - const esm = await import("../index.mjs"); + const cjs = require("../../index.cjs"); + const esm = await import("../../index.mjs"); assert.equal(cjs, esm.default); assert.equal(cjs.default, esm.default); diff --git a/test/fixtures/caller.js b/tests/fixtures/caller.js similarity index 100% rename from test/fixtures/caller.js rename to tests/fixtures/caller.js diff --git a/test/fixtures/invalid.json b/tests/fixtures/invalid.json similarity index 100% rename from test/fixtures/invalid.json rename to tests/fixtures/invalid.json diff --git a/tests/fixtures/module.mjs b/tests/fixtures/module.mjs new file mode 100644 index 0000000..68e1476 --- /dev/null +++ b/tests/fixtures/module.mjs @@ -0,0 +1,2 @@ +export const named = "value"; +export default { kind: "module", list: [1, 2] }; diff --git a/test/fixtures/nested/ok.json b/tests/fixtures/nested/ok.json similarity index 100% rename from test/fixtures/nested/ok.json rename to tests/fixtures/nested/ok.json diff --git a/tests/fixtures/no-default.mjs b/tests/fixtures/no-default.mjs new file mode 100644 index 0000000..b89a4b7 --- /dev/null +++ b/tests/fixtures/no-default.mjs @@ -0,0 +1,2 @@ +export const named = "value"; +export const other = 2; diff --git a/test/fixtures/sample.json b/tests/fixtures/sample.json similarity index 100% rename from test/fixtures/sample.json rename to tests/fixtures/sample.json diff --git a/tests/import-strategies.test.vitest.mjs b/tests/import-strategies.test.vitest.mjs new file mode 100644 index 0000000..0b9ef24 --- /dev/null +++ b/tests/import-strategies.test.vitest.mjs @@ -0,0 +1,272 @@ +/** + * + * @Project: @cldmv/wisp + * @Filename: /tests/import-strategies.test.vitest.mjs + * @Date: 2026-10-03T19:24:23-07:00 (1791080663) + * @Author: Nate Corcoran + * @Email: + * ----- + * @Last modified by: Nate Corcoran (Shinrai@users.noreply.github.com) + * @Last modified time: 2026-10-03T19:27:21-07:00 (1791080841) + * ----- + * @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved. + * + */ + +/** + * @fileoverview Characterization tests for wisp's input handling and its three load strategies: + * `import()` with `with` attributes, legacy `assert` attributes, and the fs.readFile fallback. + * @module @cldmv/wisp.test.import-strategies + * @internal + * @private + * + * @description + * Node.js 22+ ignores the legacy `assert` key, so a module that `with { type }` rejects + * (an unsupported `type` such as "javascript") is imported by the `assert` attempt with no + * attributes at all. That is how these tests reach the second strategy on a current Node.js. + */ + +import { describe, it, expect, vi, afterEach } from "vitest"; +import path from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { wisp, wispSync } from "../index.mjs"; + +const fixtures = path.join(path.dirname(fileURLToPath(import.meta.url)), "fixtures"); +const sample = path.join(fixtures, "sample.json"); +const sampleData = { foo: "bar", nested: { ok: true } }; +const fixturesBaseHref = pathToFileURL(fixtures + path.sep).href; + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +describe("input forms", () => { + it("accepts a URL instance", async () => { + expect(await wisp(pathToFileURL(sample))).toEqual(sampleData); + expect(wispSync(pathToFileURL(sample))).toEqual(sampleData); + }); + + it("accepts a file:// URL string", async () => { + expect(await wisp(pathToFileURL(sample).href)).toEqual(sampleData); + expect(wispSync(pathToFileURL(sample).href)).toEqual(sampleData); + }); + + it("resolves a relative path against a string base", async () => { + expect(await wisp("sample.json", { base: fixturesBaseHref })).toEqual(sampleData); + expect(wispSync("nested/ok.json", { base: fixturesBaseHref })).toEqual({ ok: true }); + }); + + it("resolves a relative path against a URL base", async () => { + const base = new URL(fixturesBaseHref); + expect(await wisp("./nested/ok.json", { base })).toEqual({ ok: true }); + expect(wispSync("./sample.json", { base })).toEqual(sampleData); + }); + + it("stringifies a non-string, non-URL input before resolving it", async () => { + const input = { toString: () => sample }; + expect(await wisp(input)).toEqual(sampleData); + expect(wispSync(input)).toEqual(sampleData); + }); +}); + +describe("import() with `with` attributes", () => { + it("returns the module's default export when no reviver or validate is given", async () => { + const a = await wisp(sample); + const b = await wisp(sample); + // The JSON module is cached by Node, so both calls share the same object. + expect(a).toBe(b); + }); + + it("returns a deep clone when a reviver or validate is given", async () => { + const shared = await wisp(sample); + const cloned = await wisp(sample, { validate: () => {} }); + expect(cloned).toEqual(shared); + expect(cloned).not.toBe(shared); + expect(cloned.nested).not.toBe(shared.nested); + }); + + it("applies the reviver and validate together", async () => { + const seen = []; + const data = await wisp(sample, { + reviver: (key, value) => (key === "ok" ? "revived" : value), + validate: (val) => seen.push(val.nested.ok) + }); + expect(data.nested.ok).toBe("revived"); + expect(seen).toEqual(["revived"]); + }); + + it("reports a non-Error validation failure with the thrown value", async () => { + const validate = () => { + throw "plain rejection"; + }; + await expect(wisp(sample, { validate })).rejects.toThrow( + /^@cldmv\/wisp: Failed to load JSON file at .*sample\.json: @cldmv\/wisp: plain rejection$/ + ); + }); + + it("keeps the validation error as the cause", async () => { + const original = new Error("nope"); + const err = await wisp(sample, { + validate: () => { + throw original; + } + }).catch((e) => e); + expect(err.cause).toBe(original); + }); +}); + +describe("import() with legacy `assert` attributes", () => { + // Node.js only checks import attributes on a module's first load; once a URL is in the module + // cache, a later `with { type: "javascript" }` import of it succeeds. Each test therefore loads + // its own URL (a unique query string makes a separate module instance) so the `with` attempt + // really fails and the `assert` attempt is the one that loads the module. + let counter = 0; + const fresh = (file) => `${pathToFileURL(path.join(fixtures, file)).href}?case=${++counter}`; + const moduleFile = "module.mjs"; + const noDefaultFile = "no-default.mjs"; + + it("loads a module whose type `with` rejects, returning its default export", async () => { + const data = await wisp(fresh(moduleFile), { type: "javascript" }); + expect(data).toEqual({ kind: "module", list: [1, 2] }); + }); + + it("returns the module namespace when there is no default export", async () => { + const ns = await wisp(fresh(noDefaultFile), { type: "javascript" }); + expect(ns.named).toBe("value"); + expect(ns.other).toBe(2); + }); + + it("returns a cloned, revived and validated default export", async () => { + const seen = []; + const data = await wisp(fresh(moduleFile), { + type: "javascript", + reviver: (key, value) => (key === "kind" ? "revived" : value), + validate: (val) => seen.push(val.kind) + }); + expect(data).toEqual({ kind: "revived", list: [1, 2] }); + expect(seen).toEqual(["revived"]); + }); + + it("applies a reviver without a validate", async () => { + const data = await wisp(fresh(moduleFile), { + type: "javascript", + reviver: (key, value) => (Array.isArray(value) ? value.length : value) + }); + expect(data).toEqual({ kind: "module", list: 2 }); + }); + + it("serves an already-loaded module through the `with` attempt", async () => { + // Second load of the same URL: Node.js skips the attribute check for a cached module. + const url = fresh(noDefaultFile); + const first = await wisp(url, { type: "javascript" }); + const second = await wisp(url, { type: "javascript" }); + expect(second).toBe(first); + // A reviver needs a clone, which a namespace object cannot give, on every strategy. + await expect(wisp(url, { type: "javascript", reviver: (key, value) => value })).rejects.toThrow( + /^@cldmv\/wisp: Unsupported type 'javascript'/ + ); + }); + + it("cannot clone a namespace without a default export, so a reviver ends in the unsupported-type error", async () => { + // structuredClone rejects a module namespace object; that failure is swallowed like any + // other strategy failure, and the fs.readFile strategy only handles type "json". + await expect(wisp(fresh(noDefaultFile), { type: "javascript", reviver: (key, value) => value })).rejects.toThrow( + /^@cldmv\/wisp: Unsupported type 'javascript' or failed to load module at file:.*no-default\.mjs\?case=\d+$/ + ); + }); + + it("reports a validation failure on a non-JSON module as an unsupported type", async () => { + // The `assert` attempt's validation error is swallowed like any other failure, and the + // fs.readFile strategy only handles type "json", so the final error is the unsupported-type one. + const validate = () => { + throw "rejected"; + }; + await expect(wisp(fresh(moduleFile), { type: "javascript", validate })).rejects.toThrow( + /^@cldmv\/wisp: Unsupported type 'javascript' or failed to load module at file:.*module\.mjs\?case=\d+$/ + ); + await expect( + wisp(fresh(moduleFile), { + type: "javascript", + validate: () => { + throw new Error("rejected as Error"); + } + }) + ).rejects.toThrow(/^@cldmv\/wisp: Unsupported type 'javascript'/); + }); +}); + +describe("fs.readFile fallback", () => { + it("reads JSON content that import() cannot load (a .js file holding JSON)", async () => { + const data = await wisp(path.join(fixtures, "caller.js"), { reviver: (key, value) => (key === "caller" ? "revived" : value) }); + expect(data).toEqual({ caller: "revived" }); + }); + + it("throws a wisp error naming the file when the primary is missing and there is no fallback", async () => { + const missing = path.join(fixtures, "missing.json"); + const err = await wisp(missing).catch((e) => e); + expect(err).toBeInstanceOf(Error); + expect(err.message).toMatch(/^@cldmv\/wisp: Failed to load JSON file at file:.*missing\.json: ENOENT/); + expect(err.cause.code).toBe("ENOENT"); + }); + + it("throws a wisp error when the primary is invalid JSON and there is no fallback", async () => { + const err = await wisp(path.join(fixtures, "invalid.json")).catch((e) => e); + expect(err.message).toMatch(/^@cldmv\/wisp: Failed to load JSON file at .*invalid\.json: /); + expect(err.cause).toBeInstanceOf(SyntaxError); + }); + + it("does not fall back a second time when the fallback is also missing", async () => { + const err = await wisp(path.join(fixtures, "missing.json"), { fallback: path.join(fixtures, "also-missing.json") }).catch((e) => e); + expect(err.message).toMatch(/^@cldmv\/wisp: Failed to load JSON file at .*also-missing\.json: ENOENT/); + }); + + it("rejects an unsupported type for a file no strategy can load", async () => { + await expect(wisp(sample, { type: "javascript" })).rejects.toThrow( + /^@cldmv\/wisp: Unsupported type 'javascript' or failed to load module at file:.*sample\.json$/ + ); + }); + + it("deep-clones with JSON when structuredClone is unavailable", async () => { + vi.stubGlobal("structuredClone", undefined); + const data = await wisp(sample, { validate: () => {} }); + expect(data).toEqual(sampleData); + }); +}); + +describe("wispSync", () => { + it("throws a wisp error when the primary is missing and there is no fallback", () => { + expect(() => wispSync(path.join(fixtures, "missing.json"))).toThrow( + /^@cldmv\/wisp: Failed to load JSON file at file:.*missing\.json: ENOENT/ + ); + }); + + it("throws a wisp error when the primary is invalid JSON and there is no fallback", () => { + expect(() => wispSync(path.join(fixtures, "invalid.json"))).toThrow(/^@cldmv\/wisp: Failed to load JSON file at .*invalid\.json: /); + }); + + it("reports a non-Error validation failure with the thrown value", () => { + expect(() => + wispSync(sample, { + validate: () => { + throw "sync plain rejection"; + } + }) + ).toThrow(/: @cldmv\/wisp: sync plain rejection$/); + }); + + it("returns a fresh clone on every call", () => { + const a = wispSync(sample); + const b = wispSync(sample); + expect(a).toEqual(b); + expect(a).not.toBe(b); + }); + + it("deep-clones with JSON when structuredClone is unavailable", () => { + vi.stubGlobal("structuredClone", undefined); + expect(wispSync(sample)).toEqual(sampleData); + }); + + it("ignores the type option and always parses JSON", () => { + expect(wispSync(path.join(fixtures, "caller.js"), { type: "javascript" })).toEqual({ caller: "ok" }); + }); +}); diff --git a/tests/resolve-from-caller.test.vitest.mjs b/tests/resolve-from-caller.test.vitest.mjs new file mode 100644 index 0000000..64ad041 --- /dev/null +++ b/tests/resolve-from-caller.test.vitest.mjs @@ -0,0 +1,224 @@ +/** + * + * @Project: @cldmv/wisp + * @Filename: /tests/resolve-from-caller.test.vitest.mjs + * @Date: 2026-10-03T19:24:51-07:00 (1791080691) + * @Author: Nate Corcoran + * @Email: + * ----- + * @Last modified by: Nate Corcoran (Shinrai@users.noreply.github.com) + * @Last modified time: 2026-10-03T19:27:21-07:00 (1791080841) + * ----- + * @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved. + * + */ + +/** + * @fileoverview Characterization tests for the caller-relative path resolver. + * @module @cldmv/wisp.test.resolve-from-caller + * @internal + * @private + * + * @description + * The resolver picks its base file from V8 CallSite frames. Real stacks cover the plain + * cases; the frame-selection state machine is driven with synthetic CallSite lists by + * pinning `Error.prepareStackTrace` (which `getStack` installs and restores) for one call. + */ + +import { describe, it, expect, vi, afterEach } from "vitest"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { toFsPath, getStack, resolvePathFromCaller, resolveUrlFromCaller } from "../src/lib/resolve-from-caller.mjs"; + +const here = fileURLToPath(import.meta.url); +const testsDir = path.dirname(here); +const repoRoot = path.dirname(testsDir); +const resolverFile = path.join(repoRoot, "src", "lib", "resolve-from-caller.mjs"); + +/** + * Runs `fn` while every stack trace V8 formats is replaced by `frames`. + * @param {Array|undefined} frames - Synthetic CallSite objects (or undefined for an empty stack value). + * @param {() => any} fn - Code to run with the synthetic stack in place. + * @returns {any} Whatever `fn` returns. + */ +function withStack(frames, fn) { + const original = Object.getOwnPropertyDescriptor(Error, "prepareStackTrace"); + Object.defineProperty(Error, "prepareStackTrace", { + configurable: true, + get: () => () => frames, + // getStack assigns its own formatter and then restores the old one; both writes are ignored here. + set: () => {} + }); + try { + return fn(); + } finally { + if (original) Object.defineProperty(Error, "prepareStackTrace", original); + else delete Error.prepareStackTrace; + } +} + +const frame = (file) => ({ getFileName: () => file }); + +afterEach(() => { + vi.restoreAllMocks(); +}); + +describe("toFsPath", () => { + it("converts a file:// URL to a filesystem path", () => { + expect(toFsPath(pathToFileURL(here).href)).toBe(here); + }); + + it("returns a plain path unchanged, stringified", () => { + expect(toFsPath("/some/path.json")).toBe("/some/path.json"); + expect(toFsPath({ toString: () => "/from/object" })).toBe("/from/object"); + }); + + it("returns null for falsy input", () => { + expect(toFsPath(null)).toBeNull(); + expect(toFsPath(undefined)).toBeNull(); + expect(toFsPath("")).toBeNull(); + }); +}); + +describe("getStack", () => { + it("returns V8 CallSite objects, starting at the caller when no function is skipped", () => { + const stack = getStack(); + expect(Array.isArray(stack)).toBe(true); + expect(typeof stack[0].getFileName).toBe("function"); + expect(toFsPath(stack[0].getFileName())).toBe(resolverFile); + }); + + it("drops the frames above the skipped function", () => { + function marker() { + return getStack(marker); + } + const stack = marker(); + expect(toFsPath(stack[0].getFileName())).toBe(here); + expect(stack.some((cs) => cs.getFunctionName() === "marker")).toBe(false); + }); + + it("restores the previous Error.prepareStackTrace", () => { + const before = Error.prepareStackTrace; + getStack(); + expect(Error.prepareStackTrace).toBe(before); + }); + + it("returns an empty array when the stack formatter yields nothing", () => { + expect(withStack(undefined, () => getStack())).toEqual([]); + }); +}); + +describe("resolvePathFromCaller", () => { + it("converts a file:// URL short-circuit to a path", () => { + expect(resolvePathFromCaller(pathToFileURL("/abs/file.json").href)).toBe(path.resolve("/abs/file.json")); + }); + + it("returns an absolute path unchanged", () => { + expect(resolvePathFromCaller("/abs/file.json")).toBe("/abs/file.json"); + }); + + it("resolves an existing relative path from the calling file", () => { + expect(resolvePathFromCaller("./fixtures/sample.json")).toBe(path.join(testsDir, "fixtures", "sample.json")); + }); + + it("resolves a missing relative path from the calling file too", () => { + expect(resolvePathFromCaller("./fixtures/does-not-exist.json")).toBe(path.join(testsDir, "fixtures", "does-not-exist.json")); + }); + + it("throws for a non-string path", () => { + expect(() => resolvePathFromCaller(/** @type {any} */ (42))).toThrow(TypeError); + }); +}); + +describe("resolveUrlFromCaller", () => { + it("returns a file:// URL unchanged", () => { + expect(resolveUrlFromCaller("file:///abs/file.json")).toBe("file:///abs/file.json"); + }); + + it("converts an absolute path to a file:// URL", () => { + expect(resolveUrlFromCaller("/abs/file.json")).toBe(pathToFileURL("/abs/file.json").href); + }); + + it("resolves an existing relative path from the calling file", () => { + expect(resolveUrlFromCaller("./fixtures/sample.json")).toBe(pathToFileURL(path.join(testsDir, "fixtures", "sample.json")).href); + }); + + it("resolves a missing relative path from the calling file too", () => { + expect(resolveUrlFromCaller("./nope.json")).toBe(pathToFileURL(path.join(testsDir, "nope.json")).href); + }); +}); + +describe("base-file selection from the call stack", () => { + const inSrc = path.join(repoRoot, "src", "wisp.mjs"); + const inDist = path.join(repoRoot, "dist", "wisp.mjs"); + const indexMjs = path.join(repoRoot, "index.mjs"); + const userFile = path.join(testsDir, "fixtures", "nested", "user.mjs"); + + it("picks the first frame after the stack leaves src/", () => { + const frames = [frame(resolverFile), frame(inSrc), frame(userFile), frame(here)]; + expect(withStack(frames, () => resolvePathFromCaller("./ok.json"))).toBe(path.join(testsDir, "fixtures", "nested", "ok.json")); + }); + + it("treats dist/ like src/", () => { + const frames = [frame(inDist), frame(userFile)]; + expect(withStack(frames, () => resolvePathFromCaller("./ok.json"))).toBe(path.join(testsDir, "fixtures", "nested", "ok.json")); + }); + + it("returns the package's index file when that is the first frame outside src/", () => { + const frames = [frame(inSrc), frame(indexMjs), frame(userFile)]; + expect(withStack(frames, () => resolvePathFromCaller("./package.json"))).toBe(path.join(repoRoot, "package.json")); + }); + + it("skips frames without a file name, node:internal frames, and the resolver itself", () => { + const frames = [ + null, + frame(undefined), + frame("node:internal/modules/esm/loader"), + frame(pathToFileURL(resolverFile).href), + frame(inSrc), + frame(pathToFileURL(userFile).href) + ]; + expect(withStack(frames, () => resolveUrlFromCaller("./ok.json"))).toBe( + pathToFileURL(path.join(testsDir, "fixtures", "nested", "ok.json")).href + ); + }); + + it("falls back to the first usable frame when the stack never leaves src/", () => { + // Leaving an index file without leaving src/ does not select a base, so the fallback picks the first real frame. + const frames = [frame("node:internal/x"), frame(indexMjs), frame(userFile)]; + expect(withStack(frames, () => resolvePathFromCaller("./x.json"))).toBe(path.join(repoRoot, "x.json")); + }); + + it("switches to the fallback frame when the primary candidate does not exist", () => { + // The primary base is the user file, but ./missing.json does not exist next to it, so the + // result is built from the fallback base (the first usable frame) without a second existence check. + const frames = [frame(inSrc), frame(userFile)]; + expect(withStack(frames, () => resolvePathFromCaller("./missing.json"))).toBe(path.join(repoRoot, "src", "missing.json")); + }); + + it("resolves from the resolver's own directory when the stack has no usable frames", () => { + expect(withStack([], () => resolvePathFromCaller("./x.json"))).toBe(path.join(repoRoot, "src", "lib", "x.json")); + expect(withStack([null, frame(undefined), frame(resolverFile), frame("node:internal/y")], () => resolveUrlFromCaller("./y.json"))).toBe( + pathToFileURL(path.join(repoRoot, "src", "lib", "y.json")).href + ); + }); + + it("ignores frames outside the package when looking for src/", () => { + const outside = path.join(path.parse(repoRoot).root, "elsewhere", "src", "lib.mjs"); + const frames = [frame(outside), frame(userFile)]; + // The outside file is not in this package's src/, so no primary base is found and the fallback takes the first frame. + expect(withStack(frames, () => resolvePathFromCaller("./z.json"))).toBe(path.join(path.dirname(outside), "z.json")); + }); +}); + +describe("package root detection", () => { + it("still resolves caller-relative paths when no package.json is found above the resolver", async () => { + // A copy of the resolver bundled somewhere without a package.json above it has no package root: + // nothing is classified as src/ or an index file, so every lookup goes through the fallback frame. + vi.spyOn(fs, "existsSync").mockReturnValue(false); + const mod = await import(`../src/lib/resolve-from-caller.mjs?no-package-root=${Date.now()}`); + const frames = [frame(path.join(repoRoot, "src", "wisp.mjs")), frame(here)]; + expect(withStack(frames, () => mod.resolvePathFromCaller("./x.json"))).toBe(path.join(repoRoot, "src", "x.json")); + }); +}); diff --git a/tests/run-vitest.mjs b/tests/run-vitest.mjs new file mode 100644 index 0000000..215f31c --- /dev/null +++ b/tests/run-vitest.mjs @@ -0,0 +1,67 @@ +/** + * + * @Project: @cldmv/wisp + * @Filename: /tests/run-vitest.mjs + * @Date: 2026-10-03T19:27:27-07:00 (1791080847) + * @Author: Nate Corcoran + * @Email: + * ----- + * @Last modified by: Nate Corcoran (Shinrai@users.noreply.github.com) + * @Last modified time: 2026-10-03T19:27:27-07:00 (1791080847) + * ----- + * @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved. + * + */ + +/** + * @fileoverview OOM-safe Vitest runner for wisp — delegates to + * @cldmv/vitest-runner, which spawns each test file in its own child process and + * (under coverage) uses a blob-per-file + `--mergeReports` strategy so a single + * process never holds coverage data for the whole suite. + * + * Usage: + * node tests/run-vitest.mjs # run all tests + * node tests/run-vitest.mjs --coverage # with coverage (verbose) + * node tests/run-vitest.mjs --coverage-quiet # with coverage (progress bar + summary) + * node tests/run-vitest.mjs # filter by path/name + * + * Args before a `--` delimiter are forwarded to Vitest; args after it are test + * patterns. A value-taking flag needs the delimiter, e.g.: + * node tests/run-vitest.mjs --reporter verbose -- + */ +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { run } from "@cldmv/vitest-runner"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const argv = process.argv.slice(2); + +// A `--` delimiter separates forwarded Vitest args (before it) from test patterns +// (after it), so a value-taking flag such as `--reporter verbose` isn't misread as a +// pattern. Without a `--`, the legacy heuristic applies: non-flag tokens are test +// patterns and flag tokens are forwarded to vitest. +const delimiter = argv.indexOf("--"); +const forwarded = delimiter === -1 ? argv.filter((a) => a.startsWith("-")) : argv.slice(0, delimiter); +const testPatterns = delimiter === -1 ? argv.filter((a) => !a.startsWith("-")) : argv.slice(delimiter + 1); + +const coverageQuiet = forwarded.includes("--coverage-quiet"); +const coverage = coverageQuiet || forwarded.includes("--coverage"); +const passthrough = forwarded.filter((a) => a !== "--coverage" && a !== "--coverage-quiet"); + +// VITEST_WORKERS overrides the worker count; ignore an unset / invalid / non-positive value. +const parsedWorkers = parseInt(process.env.VITEST_WORKERS ?? "", 10); +const workers = Number.isInteger(parsedWorkers) && parsedWorkers > 0 ? parsedWorkers : 4; + +const code = await run({ + cwd: root, + testDir: "tests", + vitestConfig: ".configs/vitest.config.mjs", + // CLDMV standing convention: vitest test files are always named `*.test.vitest.mjs`. + testFilePattern: /\.test\.vitest\.mjs$/, + testPatterns, + workers, + coverageQuiet, + vitestArgs: [...(coverage ? ["--coverage"] : []), ...passthrough], + nodeEnv: process.env.NODE_ENV || "development" +}); +process.exit(code); diff --git a/test/wisp.spec.mjs b/tests/wisp.test.vitest.mjs similarity index 76% rename from test/wisp.spec.mjs rename to tests/wisp.test.vitest.mjs index 654c178..e4ec3b1 100644 --- a/test/wisp.spec.mjs +++ b/tests/wisp.test.vitest.mjs @@ -1,7 +1,7 @@ /** * * @Project: @cldmv/wisp - * @Filename: /test/wisp.spec.mjs + * @Filename: /tests/wisp.test.vitest.mjs * @Date: 2025-10-30T15:06:33-07:00 (1761861993) * @Author: Nate Corcoran * @Email: @@ -24,27 +24,28 @@ * path resolution, validation, and CJS interop. */ -import { expect } from "chai"; +import { describe, it, expect } from "vitest"; import { wisp, wispSync } from "../index.mjs"; import path from "node:path"; +import { fileURLToPath } from "node:url"; -const testDir = path.join(process.cwd(), "test"); +const testDir = path.dirname(fileURLToPath(import.meta.url)); describe("wisp", () => { it("loads JSON via wisp() async", async () => { const data = await wisp(path.resolve(testDir, "fixtures/sample.json")); - expect(data).to.deep.equal({ foo: "bar", nested: { ok: true } }); + expect(data).toEqual({ foo: "bar", nested: { ok: true } }); }); it("loads JSON via wispSync() sync", () => { const data = wispSync(path.resolve(testDir, "fixtures/sample.json")); - expect(data).to.deep.equal({ foo: "bar", nested: { ok: true } }); + expect(data).toEqual({ foo: "bar", nested: { ok: true } }); }); it("handles absolute paths", async () => { const absPath = path.resolve(testDir, "fixtures/sample.json"); const data = await wisp(absPath); - expect(data.foo).to.equal("bar"); + expect(data.foo).toBe("bar"); }); it("validates successfully", async () => { @@ -52,7 +53,7 @@ describe("wisp", () => { if (!val.foo) throw new Error("missing foo"); }; const data = await wisp(path.resolve(testDir, "fixtures/sample.json"), { validate }); - expect(data.foo).to.equal("bar"); + expect(data.foo).toBe("bar"); }); it("throws on validation failure", async () => { @@ -63,26 +64,26 @@ describe("wisp", () => { await wisp(path.resolve(testDir, "fixtures/sample.json"), { validate }); throw new Error("Expected error was not thrown"); } catch (error) { - expect(error.message).to.include("@cldmv/wisp: test error"); + expect(error.message).toContain("@cldmv/wisp: test error"); } }); it("uses reviver function", async () => { const reviver = (key, value) => (key === "foo" ? "modified" : value); const data = await wisp(path.resolve(testDir, "fixtures/sample.json"), { reviver }); - expect(data.foo).to.equal("modified"); + expect(data.foo).toBe("modified"); }); it("loads nested JSON", async () => { const data = await wisp(path.resolve(testDir, "fixtures/nested/ok.json")); - expect(data).to.deep.equal({ ok: true }); + expect(data).toEqual({ ok: true }); }); it("handles fallback paths", async () => { const data = await wisp(path.resolve(testDir, "fixtures/nonexistent.json"), { fallback: path.resolve(testDir, "fixtures/sample.json") }); - expect(data.foo).to.equal("bar"); + expect(data.foo).toBe("bar"); }); it("does not use the fallback when the primary fails validation", async () => { @@ -95,13 +96,13 @@ describe("wisp", () => { } catch (e) { err = e; } - expect(err, "expected the validation error to be thrown").to.be.an("error"); - expect(err.message).to.match(/^@cldmv\/wisp: Failed to load JSON file at .*sample\.json: @cldmv\/wisp: primary rejected$/); + expect(err, "expected the validation error to be thrown").toBeInstanceOf(Error); + expect(err.message).toMatch(/^@cldmv\/wisp: Failed to load JSON file at .*sample\.json: @cldmv\/wisp: primary rejected$/); }); it("uses the fallback when the primary is invalid JSON", async () => { const data = await wisp(path.resolve(testDir, "fixtures/invalid.json"), { fallback: path.resolve(testDir, "fixtures/sample.json") }); - expect(data.foo).to.equal("bar"); + expect(data.foo).toBe("bar"); }); it("validates the fallback data when the primary is missing", async () => { @@ -114,27 +115,27 @@ describe("wisp", () => { } catch (e) { err = e; } - expect(err, "expected the validation error to be thrown").to.be.an("error"); - expect(err.message).to.include("@cldmv/wisp: fallback rejected"); + expect(err, "expected the validation error to be thrown").toBeInstanceOf(Error); + expect(err.message).toContain("@cldmv/wisp: fallback rejected"); }); it("resolves caller path correctly", async () => { const callerPath = path.resolve(testDir, "fixtures/caller.js"); const data = await wisp(callerPath); - expect(data).to.deep.equal({ caller: "ok" }); + expect(data).toEqual({ caller: "ok" }); }); it("resolves relative paths from caller location", async () => { // This was the core issue - relative paths should resolve from the caller, not from src/ const data = await wisp("./fixtures/sample.json"); - expect(data).to.deep.equal({ foo: "bar", nested: { ok: true } }); + expect(data).toEqual({ foo: "bar", nested: { ok: true } }); }); }); describe("wispSync", () => { it("loads JSON synchronously", () => { const data = wispSync(path.resolve(testDir, "fixtures/sample.json")); - expect(data).to.deep.equal({ foo: "bar", nested: { ok: true } }); + expect(data).toEqual({ foo: "bar", nested: { ok: true } }); }); it("validates synchronously", () => { @@ -142,27 +143,27 @@ describe("wispSync", () => { if (!val.foo) throw new Error("missing foo"); }; const data = wispSync(path.resolve(testDir, "fixtures/sample.json"), { validate }); - expect(data.foo).to.equal("bar"); + expect(data.foo).toBe("bar"); }); it("throws on sync validation failure", () => { const validate = () => { throw new Error("sync test error"); }; - expect(() => wispSync(path.resolve(testDir, "fixtures/sample.json"), { validate })).to.throw("@cldmv/wisp: sync test error"); + expect(() => wispSync(path.resolve(testDir, "fixtures/sample.json"), { validate })).toThrow("@cldmv/wisp: sync test error"); }); it("uses reviver in sync", () => { const reviver = (key, value) => (key === "foo" ? "sync modified" : value); const data = wispSync(path.resolve(testDir, "fixtures/sample.json"), { reviver }); - expect(data.foo).to.equal("sync modified"); + expect(data.foo).toBe("sync modified"); }); it("handles fallback paths synchronously", () => { const data = wispSync(path.resolve(testDir, "fixtures/nonexistent.json"), { fallback: path.resolve(testDir, "fixtures/sample.json") }); - expect(data.foo).to.equal("bar"); + expect(data.foo).toBe("bar"); }); it("does not use the fallback when the primary fails validation in sync", () => { @@ -171,12 +172,12 @@ describe("wispSync", () => { }; expect(() => wispSync(path.resolve(testDir, "fixtures/sample.json"), { validate, fallback: path.resolve(testDir, "fixtures/nested/ok.json") }) - ).to.throw(/^@cldmv\/wisp: Failed to load JSON file at .*sample\.json: @cldmv\/wisp: primary rejected$/); + ).toThrow(/^@cldmv\/wisp: Failed to load JSON file at .*sample\.json: @cldmv\/wisp: primary rejected$/); }); it("uses the fallback when the primary is invalid JSON in sync", () => { const data = wispSync(path.resolve(testDir, "fixtures/invalid.json"), { fallback: path.resolve(testDir, "fixtures/sample.json") }); - expect(data.foo).to.equal("bar"); + expect(data.foo).toBe("bar"); }); it("validates the fallback data when the primary is missing in sync", () => { @@ -185,21 +186,18 @@ describe("wispSync", () => { }; expect(() => wispSync(path.resolve(testDir, "fixtures/nonexistent.json"), { validate, fallback: path.resolve(testDir, "fixtures/sample.json") }) - ).to.throw("@cldmv/wisp: fallback rejected"); + ).toThrow("@cldmv/wisp: fallback rejected"); }); it("resolves caller path correctly in sync", () => { const callerPath = path.resolve(testDir, "fixtures/caller.js"); const data = wispSync(callerPath); - expect(data).to.deep.equal({ caller: "ok" }); + expect(data).toEqual({ caller: "ok" }); }); it("resolves relative paths from caller location in sync", () => { // This was the core issue - relative paths should resolve from the caller, not from src/ const data = wispSync("./fixtures/sample.json"); - expect(data).to.deep.equal({ foo: "bar", nested: { ok: true } }); + expect(data).toEqual({ foo: "bar", nested: { ok: true } }); }); }); - -// Note: CJS interop test would require separate CJS test file or dynamic require -// For now, assuming the exports are correct as per index.cjs diff --git a/tsconfig.json b/tsconfig.json index f39544c..062d7ae 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,5 +23,5 @@ "types": ["node"] }, "include": ["src/**/*"], - "exclude": ["node_modules", "test", "types"] + "exclude": ["node_modules", "tests", "types"] } diff --git a/types/lib/resolve-from-caller.d.mts.map b/types/lib/resolve-from-caller.d.mts.map index 88c051a..ad8bc86 100644 --- a/types/lib/resolve-from-caller.d.mts.map +++ b/types/lib/resolve-from-caller.d.mts.map @@ -1 +1 @@ -{"version":3,"file":"resolve-from-caller.d.mts","sourceRoot":"","sources":["../../src/lib/resolve-from-caller.mjs"],"names":[],"mappings":"AAuFA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,6CAhCa,KAAK,CAAC,QAAQ,CAAC,CA0C3B;AA4MD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,2CAzCW,MAAM,GACJ,MAAM,CAsDlB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,0CA1CW,MAAM,GACJ,MAAM,CAuDlB;AAtXM,4BAvBI,GAAG,GACD,MAAM,GAAC,IAAI,CAsB+F;;iBA0XzG,MAAY,MAAM,GAAC,SAAS;mBAC5B,MAAY,MAAM,GAAC,SAAS;qBAC5B,MAAY,MAAM,GAAC,SAAS;iBAC5B,MAAY,MAAM,GAAC,SAAS;mBAC5B,MAAY,MAAM,GAAC,SAAS;8BAC5B,MAAY,MAAM,GAAC,SAAS;qBAC5B,MAAY,MAAM,GAAC,SAAS;cAC5B,MAAY,OAAO,GAAC,SAAS;YAC7B,MAAY,OAAO,GAAC,SAAS;mBAC7B,MAAY,OAAO,GAAC,SAAS;gBAC7B,MAAY,OAAO,GAAC,SAAS;aAC7B,MAAY,OAAO,GAAC,SAAS;kBAC7B,MAAY,OAAO,GAAC,SAAS;qBAC7B,MAAY,MAAM,GAAC,SAAS"} \ No newline at end of file +{"version":3,"file":"resolve-from-caller.d.mts","sourceRoot":"","sources":["../../src/lib/resolve-from-caller.mjs"],"names":[],"mappings":"AAuFA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,6CAhCa,KAAK,CAAC,QAAQ,CAAC,CA0C3B;AA8MD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,2CAzCW,MAAM,GACJ,MAAM,CAsDlB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,0CA1CW,MAAM,GACJ,MAAM,CAuDlB;AAxXM,4BAvBI,GAAG,GACD,MAAM,GAAC,IAAI,CAsB+F;;iBA4XzG,MAAY,MAAM,GAAC,SAAS;mBAC5B,MAAY,MAAM,GAAC,SAAS;qBAC5B,MAAY,MAAM,GAAC,SAAS;iBAC5B,MAAY,MAAM,GAAC,SAAS;mBAC5B,MAAY,MAAM,GAAC,SAAS;8BAC5B,MAAY,MAAM,GAAC,SAAS;qBAC5B,MAAY,MAAM,GAAC,SAAS;cAC5B,MAAY,OAAO,GAAC,SAAS;YAC7B,MAAY,OAAO,GAAC,SAAS;mBAC7B,MAAY,OAAO,GAAC,SAAS;gBAC7B,MAAY,OAAO,GAAC,SAAS;aAC7B,MAAY,OAAO,GAAC,SAAS;kBAC7B,MAAY,OAAO,GAAC,SAAS;qBAC7B,MAAY,MAAM,GAAC,SAAS"} \ No newline at end of file From 046a4a94405929492e1400fc2e345ead7994bbf9 Mon Sep 17 00:00:00 2001 From: Shinrai Date: Sat, 3 Oct 2026 19:54:48 -0700 Subject: [PATCH 2/2] test: run the assert-strategy tests only where a failed import is not cached The legacy assert strategy is reached by importing an unsupported type, which 'with' rejects; on Node.js 24+ the assert retry of the same URL then loads it. Node.js 20 and 22 cache the failed import, so the retry fails with the same error and wisp reports the unsupported type. CI's Node.js 22.12 leg failed those five tests. They now run on Node.js 24+, and older versions check the unsupported-type error instead. --- tests/import-strategies.test.vitest.mjs | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/tests/import-strategies.test.vitest.mjs b/tests/import-strategies.test.vitest.mjs index 0b9ef24..be0fb98 100644 --- a/tests/import-strategies.test.vitest.mjs +++ b/tests/import-strategies.test.vitest.mjs @@ -21,9 +21,10 @@ * @private * * @description - * Node.js 22+ ignores the legacy `assert` key, so a module that `with { type }` rejects - * (an unsupported `type` such as "javascript") is imported by the `assert` attempt with no - * attributes at all. That is how these tests reach the second strategy on a current Node.js. + * On Node.js 24+, a module that `with { type }` rejects (an unsupported `type` such as + * "javascript") is imported by the `assert` retry of the same URL, because a failed import is + * not cached. That is how these tests reach the second strategy. Node.js 20 and 22 cache the + * failure, so there the tests check that wisp reports the unsupported type instead. */ import { describe, it, expect, vi, afterEach } from "vitest"; @@ -115,7 +116,14 @@ describe("import() with `with` attributes", () => { }); }); -describe("import() with legacy `assert` attributes", () => { +// Node.js 24+ does not cache a failed import: after `with { type: "javascript" }` rejects a URL, +// the `assert` retry of that same URL loads it. Node.js 20 and 22 cache the failure, so the retry +// is rejected with the same error and wisp reports the unsupported type. The `assert` strategy can +// only be reached this way on Node.js 24+, so those tests run there; older versions check the error. +const nodeMajor = Number(process.versions.node.split(".")[0]); +const retriesFailedImport = nodeMajor >= 24; + +describe.runIf(retriesFailedImport)("import() with legacy `assert` attributes", () => { // Node.js only checks import attributes on a module's first load; once a URL is in the module // cache, a later `with { type: "javascript" }` import of it succeeds. Each test therefore loads // its own URL (a unique query string makes a separate module instance) so the `with` attempt @@ -195,6 +203,15 @@ describe("import() with legacy `assert` attributes", () => { }); }); +describe.skipIf(retriesFailedImport)("import() of an unsupported type on Node.js < 24", () => { + let counter = 0; + const fresh = (file) => `${pathToFileURL(path.join(fixtures, file)).href}?legacy=${++counter}`; + + it("reports the unsupported type, because the failed `with` import is cached for that URL", async () => { + await expect(wisp(fresh("module.mjs"), { type: "javascript" })).rejects.toThrow(/^@cldmv\/wisp: Unsupported type 'javascript'/); + await expect(wisp(fresh("no-default.mjs"), { type: "javascript" })).rejects.toThrow(/^@cldmv\/wisp: Unsupported type 'javascript'/); + }); +}); describe("fs.readFile fallback", () => { it("reads JSON content that import() cannot load (a .js file holding JSON)", async () => { const data = await wisp(path.join(fixtures, "caller.js"), { reviver: (key, value) => (key === "caller" ? "revived" : value) });