From 28dee14eefcf01585234759574cd1047e0a635aa Mon Sep 17 00:00:00 2001 From: Temidayo Gabriel Date: Fri, 1 May 2026 05:53:09 -0400 Subject: [PATCH 1/5] chore: introduce vitest for stable test suite + coverage baseline --- .github/workflows/test.yml | 26 + .gitignore | 3 +- package-lock.json | 1431 ++++++++++++++++++++++++++- package.json | 9 +- test-real-api.ts | 363 ------- tests/basic.test.ts | 44 + tests/concurrent.test.ts | 55 + tests/error.test.ts | 27 + tests/headers.test.ts | 68 ++ tests/integration/full-flow.test.ts | 298 ++++++ tests/json.test.ts | 30 + tests/query.test.ts | 22 + tests/refresh.test.ts | 79 ++ tests/utils.ts | 22 + vitest.config.ts | 20 + 15 files changed, 2099 insertions(+), 398 deletions(-) create mode 100644 .github/workflows/test.yml delete mode 100644 test-real-api.ts create mode 100644 tests/basic.test.ts create mode 100644 tests/concurrent.test.ts create mode 100644 tests/error.test.ts create mode 100644 tests/headers.test.ts create mode 100644 tests/integration/full-flow.test.ts create mode 100644 tests/json.test.ts create mode 100644 tests/query.test.ts create mode 100644 tests/refresh.test.ts create mode 100644 tests/utils.ts create mode 100644 vitest.config.ts diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..6951025 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,26 @@ +name: Test Suite + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [18.x, 20.x, 22.x] + + steps: + - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + - name: Install dependencies + run: npm ci + - name: Run tests + run: npm run test diff --git a/.gitignore b/.gitignore index f1f7bed..d12d7a9 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ node_modules .DS_Store *.tgz *.log -.env \ No newline at end of file +.env +coverage \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 4866a81..2f60c4e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,8 +10,104 @@ "license": "MIT", "devDependencies": { "@types/node": "^25.6.0", + "@vitest/coverage-v8": "^4.1.5", "tsup": "^8.5.1", - "typescript": "^6.0.3" + "typescript": "^6.0.3", + "vitest": "^4.1.5" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "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/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" } }, "node_modules/@esbuild/aix-ppc64": { @@ -495,6 +591,299 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz", + "integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz", + "integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz", + "integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz", + "integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz", + "integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz", + "integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz", + "integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz", + "integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz", + "integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz", + "integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==", + "dev": true, + "license": "MIT" + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.60.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", @@ -845,23 +1234,203 @@ "win32" ] }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "dev": true, "license": "MIT" }, - "node_modules/@types/node": { - "version": "25.6.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", - "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", "dev": true, "license": "MIT", "dependencies": { "undici-types": "~7.19.0" } }, + "node_modules/@vitest/coverage-v8": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.5.tgz", + "integrity": "sha512-38C0/Ddb7HcRG0Z4/DUem8x57d2p9jYgp18mkaYswEOQBGsI1CG4f/hjm0ZCeaJfWhSZ4k7jgs29V1Zom7Ki9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.1.5", + "ast-v8-to-istanbul": "^1.0.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.2", + "obug": "^2.1.1", + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.1.5", + "vitest": "4.1.5" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.5.tgz", + "integrity": "sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.5", + "@vitest/utils": "4.1.5", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.5.tgz", + "integrity": "sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.5", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.5.tgz", + "integrity": "sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.5.tgz", + "integrity": "sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.5", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.5.tgz", + "integrity": "sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.5", + "@vitest/utils": "4.1.5", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.5.tgz", + "integrity": "sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.5.tgz", + "integrity": "sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.5", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", @@ -882,6 +1451,28 @@ "dev": true, "license": "MIT" }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-v8-to-istanbul": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.0.tgz", + "integrity": "sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, "node_modules/bundle-require": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", @@ -908,6 +1499,16 @@ "node": ">=8" } }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -951,6 +1552,13 @@ "node": "^14.18.0 || >=16.10.0" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -969,6 +1577,23 @@ } } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, "node_modules/esbuild": { "version": "0.27.7", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", @@ -1011,6 +1636,26 @@ "@esbuild/win32-x64": "0.27.7" } }, + "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/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -1029,41 +1674,365 @@ } } }, - "node_modules/fix-dts-default-cjs-exports": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", - "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", + "node_modules/fix-dts-default-cjs-exports": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", + "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "rollup": "^4.34.8" + } + }, + "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" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "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/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "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/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "magic-string": "^0.30.17", - "mlly": "^1.7.4", - "rollup": "^4.34.8" + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], "dev": true, - "hasInstallScript": true, - "license": "MIT", + "license": "MPL-2.0", "optional": true, "os": [ - "darwin" + "win32" ], "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/joycon": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", - "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=10" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, "node_modules/lilconfig": { @@ -1106,6 +2075,34 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/magicast": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.2.tgz", + "integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mlly": { "version": "1.8.2", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", @@ -1138,6 +2135,25 @@ "thenify-all": "^1.0.0" } }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -1148,6 +2164,17 @@ "node": ">=0.10.0" } }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -1197,6 +2224,35 @@ "pathe": "^2.0.1" } }, + "node_modules/postcss": { + "version": "8.5.13", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.13.tgz", + "integrity": "sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/postcss-load-config": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", @@ -1264,6 +2320,40 @@ "node": ">=8" } }, + "node_modules/rolldown": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz", + "integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.127.0", + "@rolldown/pluginutils": "1.0.0-rc.17" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.17", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.17", + "@rolldown/binding-darwin-x64": "1.0.0-rc.17", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.17", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.17", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.17", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.17", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17" + } + }, "node_modules/rollup": { "version": "4.60.2", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", @@ -1309,6 +2399,26 @@ "fsevents": "~2.3.2" } }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/source-map": { "version": "0.7.6", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", @@ -1319,6 +2429,30 @@ "node": ">= 12" } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/sucrase": { "version": "3.35.1", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", @@ -1342,6 +2476,19 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -1365,6 +2512,13 @@ "node": ">=0.8" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyexec": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", @@ -1389,6 +2543,16 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -1406,6 +2570,14 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, "node_modules/tsup": { "version": "8.5.1", "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", @@ -1486,6 +2658,201 @@ "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", "dev": true, "license": "MIT" + }, + "node_modules/vite": { + "version": "8.0.10", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz", + "integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.10", + "rolldown": "1.0.0-rc.17", + "tinyglobby": "^0.2.16" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0 || ^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": "4.1.5", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.5.tgz", + "integrity": "sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.5", + "@vitest/mocker": "4.1.5", + "@vitest/pretty-format": "4.1.5", + "@vitest/runner": "4.1.5", + "@vitest/snapshot": "4.1.5", + "@vitest/spy": "4.1.5", + "@vitest/utils": "4.1.5", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.5", + "@vitest/browser-preview": "4.1.5", + "@vitest/browser-webdriverio": "4.1.5", + "@vitest/coverage-istanbul": "4.1.5", + "@vitest/coverage-v8": "4.1.5", + "@vitest/ui": "4.1.5", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.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/vitest/node_modules/tinyexec": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz", + "integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } } } } diff --git a/package.json b/package.json index aa94f0e..4b42e50 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,10 @@ "sideEffects": false, "scripts": { "build": "tsup", - "dev": "tsup src/index.ts --watch" + "dev": "tsup src/index.ts --watch", + "test": "vitest run", + "test:watch": "vitest", + "coverage": "vitest run --coverage" }, "keywords": [ "fetch", @@ -35,7 +38,9 @@ "license": "MIT", "devDependencies": { "@types/node": "^25.6.0", + "@vitest/coverage-v8": "^4.1.5", "tsup": "^8.5.1", - "typescript": "^6.0.3" + "typescript": "^6.0.3", + "vitest": "^4.1.5" } } diff --git a/test-real-api.ts b/test-real-api.ts deleted file mode 100644 index 9567f7f..0000000 --- a/test-real-api.ts +++ /dev/null @@ -1,363 +0,0 @@ -import { createClient } from "./src/index"; -import type { VfetchResponse, VfetchSuccess, VfetchError } from "./src/types"; - -// ============================================ -// Assertion helpers (STRICT) -// ============================================ - -function assert(condition: unknown, message: string): asserts condition { - if (!condition) throw new Error(message); -} - -function assertSuccess( - res: VfetchResponse, - message?: string, -): asserts res is VfetchSuccess { - if (!res.ok) { - throw new Error( - message ?? `Expected success but got error: ${res.error} (${res.status})`, - ); - } -} - -function assertError( - res: VfetchResponse, - message?: string, -): asserts res is VfetchError { - if (res.ok) { - throw new Error(message ?? "Expected error but got success"); - } -} - -// ============================================ -// Utils -// ============================================ - -const delay = (ms: number) => - new Promise((resolve) => setTimeout(resolve, ms)); - -type FetchInput = Parameters[0]; -type FetchInit = Parameters[1]; -type FetchMock = (input: FetchInput, init?: FetchInit) => Promise; - -async function withMockedFetch( - mock: FetchMock, - run: () => Promise, -): Promise { - const original = globalThis.fetch; - globalThis.fetch = mock as typeof fetch; - - try { - return await run(); - } finally { - globalThis.fetch = original; - } -} - -function jsonResponse(body: unknown, status = 200): Response { - return new Response(JSON.stringify(body), { - status, - headers: { "Content-Type": "application/json" }, - }); -} - -// ============================================ -// TEST 1: Basic Requests -// ============================================ - -async function testBasicRequests() { - console.log("\n=== Test 1: Basic Requests ==="); - - const api = createClient({ - baseURL: "https://dummyjson.com", - }); - - const product = await api.get<{ id: number }>("/products/1"); - assertSuccess(product); - assert(product.data.id === 1, "GET failed"); - - const created = await api.post<{ title: string }>("/products/add", { - title: "Test", - }); - assertSuccess(created); - assert(created.data.title === "Test", "POST failed"); - - const updated = await api.put<{ title: string }>("/products/1", { - title: "Updated", - }); - assertSuccess(updated); - assert(updated.data.title === "Updated", "PUT failed"); - - const deleted = await api.delete<{ isDeleted: boolean }>("/products/1"); - assertSuccess(deleted); - assert(deleted.data.isDeleted === true, "DELETE failed"); - - console.log("✅ Basic requests OK"); -} - -// ============================================ -// TEST 2: Query Params -// ============================================ - -async function testQueryParams() { - console.log("\n=== Test 2: Query Params ==="); - - const api = createClient({ - baseURL: "https://dummyjson.com", - }); - - const res = await api.get<{ products: unknown[] }>("/products", { - params: { limit: "2", skip: "1" }, - }); - - assertSuccess(res); - assert(res.data.products.length <= 2, "Query params failed"); - - console.log("✅ Query params OK"); -} - -// ============================================ -// TEST 3: Header + Token Injection -// ============================================ - -async function testHeaderMerging() { - console.log("\n=== Test 3: Header merging ==="); - - let capturedAuth: string | null = null; - - await withMockedFetch( - async (_, init) => { - const headers = - init?.headers instanceof Headers - ? init.headers - : new Headers(init?.headers); - - capturedAuth = headers.get("Authorization"); - - return jsonResponse({ ok: true }); - }, - async () => { - const api = createClient({ - baseURL: "https://x.com", - getToken: () => "abc123", - }); - - const res = await api.get("/test", { - headers: { "X-Test": "1" }, - }); - - assertSuccess(res); - }, - ); - - assert(capturedAuth === "Bearer abc123", "Auth header not merged correctly"); - - console.log("✅ Header merge OK"); -} - -// ============================================ -// TEST 4: Error Normalization -// ============================================ - -async function testErrorHandling() { - console.log("\n=== Test 4: Error handling ==="); - - const api = createClient({ - baseURL: "https://dummyjson.com", - }); - - const res = await api.get("/nonexistent"); - - assertError(res); - assert(res.status === 404, "Expected 404"); - - console.log("✅ Error normalization OK"); -} - -// ============================================ -// TEST 5: Refresh Flow (REAL EDGE) -// ============================================ - -async function testRefreshFlow() { - console.log("\n=== Test 5: Refresh flow ==="); - - let token = "expired"; - let refreshCalls = 0; - - await withMockedFetch( - async (input, init) => { - const url = - typeof input === "string" - ? input - : input instanceof URL - ? input.toString() - : input.url; - - if (url.endsWith("/refresh")) { - refreshCalls++; - token = "fresh"; - return jsonResponse({ accessToken: token }); - } - - if (url.endsWith("/me")) { - const headers = - init?.headers instanceof Headers - ? init.headers - : new Headers(init?.headers); - - const auth = headers.get("Authorization"); - - if (auth === "Bearer fresh") { - return jsonResponse({ id: 1 }); - } - - return jsonResponse({ message: "Unauthorized" }, 401); - } - - return jsonResponse({}, 404); - }, - async () => { - const api = createClient({ - baseURL: "https://api.test", - getToken: () => token, - onRefresh: async () => { - const res = await fetch("https://api.test/refresh"); - const data = (await res.json()) as { accessToken?: string }; - token = data.accessToken ?? ""; - return token; - }, - onAuthFailure: () => { - throw new Error("Auth failure triggered"); - }, - }); - - const res = await api.get<{ id: number }>("/me"); - - assertSuccess(res); - assert(res.data.id === 1, "Retry after refresh failed"); - assert(refreshCalls === 1, "Refresh should be called once"); - }, - ); - - console.log("✅ Refresh flow OK"); -} - -// ============================================ -// TEST 6: Concurrent Refresh Deduping -// ============================================ - -async function testConcurrentRefresh() { - console.log("\n=== Test 6: Concurrent refresh ==="); - - let token = "expired"; - let refreshCalls = 0; - - await withMockedFetch( - async (input, init) => { - const url = - typeof input === "string" - ? input - : input instanceof URL - ? input.toString() - : input.url; - - if (url.endsWith("/refresh")) { - refreshCalls++; - await delay(50); - token = "fresh"; - return jsonResponse({ accessToken: token }); - } - - if (url.endsWith("/me")) { - const headers = - init?.headers instanceof Headers - ? init.headers - : new Headers(init?.headers); - - const auth = headers.get("Authorization"); - - if (auth === "Bearer fresh") { - return jsonResponse({ id: 1 }); - } - - return jsonResponse({}, 401); - } - - return jsonResponse({}, 404); - }, - async () => { - const api = createClient({ - baseURL: "https://api.test", - getToken: () => token, - onRefresh: async () => { - const res = await fetch("https://api.test/refresh"); - const data = (await res.json()) as { accessToken?: string }; - token = data.accessToken ?? ""; - return token; - }, - }); - - const results = await Promise.all([ - api.get("/me"), - api.get("/me"), - api.get("/me"), - ]); - - assert( - results.every((r) => r.ok), - "All should succeed", - ); - assert(refreshCalls === 1, "Refresh must be deduped"); - }, - ); - - console.log("✅ Concurrent refresh OK"); -} - -// ============================================ -// TEST 7: Invalid JSON -// ============================================ - -async function testInvalidJSON() { - console.log("\n=== Test 7: Invalid JSON ==="); - - await withMockedFetch( - async () => { - return new Response("invalid-json", { status: 200 }); - }, - async () => { - const api = createClient({ - baseURL: "https://x.com", - }); - - const res = await api.get("/test"); - - assertError(res, "Should fail on invalid JSON"); - }, - ); - - console.log("✅ Invalid JSON handled"); -} - -// ============================================ -// RUNNER -// ============================================ - -async function runAllTests() { - console.log("🚀 Running vFetch test suite\n"); - - await testBasicRequests(); - await testQueryParams(); - await testHeaderMerging(); - await testErrorHandling(); - await testRefreshFlow(); - await testConcurrentRefresh(); - await testInvalidJSON(); - - console.log("\n✅ ALL TESTS PASSED — client is solid"); -} - -runAllTests().catch((err) => { - console.error("\n❌ TEST FAILED:", err); - process.exit(1); -}); diff --git a/tests/basic.test.ts b/tests/basic.test.ts new file mode 100644 index 0000000..22de05a --- /dev/null +++ b/tests/basic.test.ts @@ -0,0 +1,44 @@ +import { describe, it, expect } from 'vitest'; +import { createClient } from '../src'; + +describe('Basic Requests', () => { + const client = createClient({ baseURL: 'https://dummyjson.com' }); + + it('should perform GET request', async () => { + const res = await client.get<{ id: number }>('/products/1'); + expect(res.ok).toBe(true); + if (res.ok) { + expect(res.data.id).toBe(1); + expect(res.status).toBe(200); + } + }, 15000); + + it('should perform POST request', async () => { + const res = await client.post<{ id: number; title: string }>('/products/add', { title: 'Test Product' }); + expect(res.ok).toBe(true); + if (res.ok) { + expect(res.data.id).toBeDefined(); + expect(res.data.title).toBe('Test Product'); + // POST might return 200 or 201 + expect([200, 201]).toContain(res.status); + } + }, 15000); + + it('should perform PUT request', async () => { + const res = await client.put<{ id: number; title: string }>('/products/1', { title: 'Updated Product' }); + expect(res.ok).toBe(true); + if (res.ok) { + expect(res.data.id).toBe(1); + expect(res.data.title).toBe('Updated Product'); + } + }, 15000); + + it('should perform DELETE request', async () => { + const res = await client.delete<{ id: number; isDeleted: boolean }>('/products/1'); + expect(res.ok).toBe(true); + if (res.ok) { + expect(res.data.id).toBe(1); + expect(res.data.isDeleted).toBe(true); + } + }, 15000); +}); diff --git a/tests/concurrent.test.ts b/tests/concurrent.test.ts new file mode 100644 index 0000000..f84b2aa --- /dev/null +++ b/tests/concurrent.test.ts @@ -0,0 +1,55 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { createClient } from '../src'; +import { withMockedFetch, jsonResponse, FetchMock } from './utils'; + +describe('Concurrent Requests Refresh Deduplication', () => { + let fetchMock: FetchMock; + + beforeEach(() => { + fetchMock = withMockedFetch(); + }); + + afterEach(() => { + fetchMock.mockRestore(); + }); + + it('should deduplicate concurrent refresh calls', async () => { + // 3 concurrent calls all fail with 401 initially + fetchMock.mockResolvedValueOnce(new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401 })); + fetchMock.mockResolvedValueOnce(new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401 })); + fetchMock.mockResolvedValueOnce(new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401 })); + + // then all 3 retry and succeed + fetchMock.mockResolvedValueOnce(jsonResponse({ id: 1 })); + fetchMock.mockResolvedValueOnce(jsonResponse({ id: 2 })); + fetchMock.mockResolvedValueOnce(jsonResponse({ id: 3 })); + + const onRefresh = vi.fn().mockImplementation(async () => { + // Simulate some delay to allow concurrent requests to queue up + await new Promise(resolve => setTimeout(resolve, 50)); + return 'new-token'; + }); + + const client = createClient({ + baseURL: 'https://api.test.com', + onRefresh + }); + + // Fire 3 requests concurrently + const [res1, res2, res3] = await Promise.all([ + client.get<{ id: number }>('/test/1'), + client.get<{ id: number }>('/test/2'), + client.get<{ id: number }>('/test/3') + ]); + + expect(res1.ok).toBe(true); + expect(res2.ok).toBe(true); + expect(res3.ok).toBe(true); + + // Very important: onRefresh should only be called ONCE + expect(onRefresh).toHaveBeenCalledTimes(1); + + // total fetch calls should be 6 (3 initial + 3 retries) + expect(fetchMock).toHaveBeenCalledTimes(6); + }); +}); diff --git a/tests/error.test.ts b/tests/error.test.ts new file mode 100644 index 0000000..1f7865f --- /dev/null +++ b/tests/error.test.ts @@ -0,0 +1,27 @@ +import { describe, it, expect } from 'vitest'; +import { createClient } from '../src'; + +describe('Error Handling', () => { + const client = createClient({ baseURL: 'https://dummyjson.com' }); + + it('should handle 404 correctly', async () => { + const res = await client.get('/this-path-does-not-exist-12345'); + + expect(res.ok).toBe(false); + if (!res.ok) { + expect(res.status).toBe(404); + expect(res.error).toBeDefined(); + } + }, 15000); + + it('should handle network errors (e.g. invalid host)', async () => { + const badClient = createClient({ baseURL: 'https://invalid-host-that-does-not-exist.local' }); + const res = await badClient.get('/test'); + + expect(res.ok).toBe(false); + if (!res.ok) { + expect(res.status).toBe(0); + expect(typeof res.error).toBe('string'); + } + }, 15000); +}); diff --git a/tests/headers.test.ts b/tests/headers.test.ts new file mode 100644 index 0000000..211a433 --- /dev/null +++ b/tests/headers.test.ts @@ -0,0 +1,68 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { createClient } from '../src'; +import { withMockedFetch, jsonResponse, FetchMock } from './utils'; + +describe('Headers Handling', () => { + let fetchMock: FetchMock; + + beforeEach(() => { + fetchMock = withMockedFetch(); + }); + + afterEach(() => { + fetchMock.mockRestore(); + }); + + it('should merge global and request headers', async () => { + fetchMock.mockResolvedValueOnce(jsonResponse({ success: true })); + + const client = createClient({ + baseURL: 'https://api.test.com', + headers: { + 'X-Global': 'global-value', + 'X-Shared': 'from-global' + } + }); + + await client.get('/test', { + headers: { + 'X-Request': 'request-value', + 'X-Shared': 'from-request' + } + }); + + expect(fetchMock).toHaveBeenCalledTimes(1); + const callArgs = fetchMock.mock.calls[0]; + const options = callArgs[1] as RequestInit; + + expect(options).toBeDefined(); + if (options) { + const headers = new Headers(options.headers as HeadersInit); + expect(headers.get('X-Global')).toBe('global-value'); + expect(headers.get('X-Request')).toBe('request-value'); + expect(headers.get('X-Shared')).toBe('from-request'); + expect(headers.get('Content-Type')).toBe('application/json'); + } + }); + + it('should inject authorization header if getToken is provided', async () => { + fetchMock.mockResolvedValueOnce(jsonResponse({ success: true })); + + const client = createClient({ + baseURL: 'https://api.test.com', + getToken: () => 'my-secret-token' + }); + + await client.get('/test'); + + expect(fetchMock).toHaveBeenCalledTimes(1); + const callArgs = fetchMock.mock.calls[0]; + const options = callArgs[1] as RequestInit; + + expect(options).toBeDefined(); + if (options) { + const headers = new Headers(options.headers as HeadersInit); + expect(headers.get('Authorization')).toBe('Bearer my-secret-token'); + } + }); +}); diff --git a/tests/integration/full-flow.test.ts b/tests/integration/full-flow.test.ts new file mode 100644 index 0000000..07f16ac --- /dev/null +++ b/tests/integration/full-flow.test.ts @@ -0,0 +1,298 @@ +import { describe, it, expect } from "vitest"; +import { createClient } from "../../src"; +import type { + VfetchResponse, + VfetchSuccess, + VfetchError, +} from "../../src/types"; + +// ============================================ +// Type guards (kept, but adapted) +// ============================================ + +function assertSuccess( + res: VfetchResponse, +): asserts res is VfetchSuccess { + if (!res.ok) { + throw new Error(`Expected success: ${res.error} (${res.status})`); + } +} + +function assertError(res: VfetchResponse): asserts res is VfetchError { + if (res.ok) { + throw new Error("Expected error but got success"); + } +} + +// ============================================ +// Utils +// ============================================ + +const delay = (ms: number) => + new Promise((resolve) => setTimeout(resolve, ms)); + +type FetchInput = Parameters[0]; +type FetchInit = Parameters[1]; +type FetchMock = (input: FetchInput, init?: FetchInit) => Promise; + +async function withMockedFetch( + mock: FetchMock, + run: () => Promise, +): Promise { + const original = globalThis.fetch; + globalThis.fetch = mock as typeof fetch; + + try { + return await run(); + } finally { + globalThis.fetch = original; + } +} + +function jsonResponse(body: unknown, status = 200): Response { + return new Response(JSON.stringify(body), { + status, + headers: { "Content-Type": "application/json" }, + }); +} + +// ============================================ +// TEST SUITE +// ============================================ + +describe("vFetch Client", () => { + // -------------------------------------------- + // Basic Requests + // -------------------------------------------- + it("should handle basic CRUD requests", async () => { + const api = createClient({ + baseURL: "https://dummyjson.com", + }); + + const product = await api.get<{ id: number }>("/products/1"); + assertSuccess(product); + expect(product.data.id).toBe(1); + + const created = await api.post<{ title: string }>("/products/add", { + title: "Test", + }); + assertSuccess(created); + expect(created.data.title).toBe("Test"); + + const updated = await api.put<{ title: string }>("/products/1", { + title: "Updated", + }); + assertSuccess(updated); + expect(updated.data.title).toBe("Updated"); + + const deleted = await api.delete<{ isDeleted: boolean }>("/products/1"); + assertSuccess(deleted); + expect(deleted.data.isDeleted).toBe(true); + }); + + // -------------------------------------------- + // Query Params + // -------------------------------------------- + it("should handle query parameters correctly", async () => { + const api = createClient({ + baseURL: "https://dummyjson.com", + }); + + const res = await api.get<{ products: unknown[] }>("/products", { + params: { limit: "2", skip: "1" }, + }); + + assertSuccess(res); + expect(res.data.products.length).toBeLessThanOrEqual(2); + }); + + // -------------------------------------------- + // Header + Token Injection + // -------------------------------------------- + it("should merge headers and inject auth token", async () => { + let capturedAuth: string | null = null; + + await withMockedFetch( + async (_, init) => { + const headers = + init?.headers instanceof Headers + ? init.headers + : new Headers(init?.headers); + + capturedAuth = headers.get("Authorization"); + + return jsonResponse({ ok: true }); + }, + async () => { + const api = createClient({ + baseURL: "https://x.com", + getToken: () => "abc123", + }); + + const res = await api.get("/test", { + headers: { "X-Test": "1" }, + }); + + assertSuccess(res); + }, + ); + + expect(capturedAuth).toBe("Bearer abc123"); + }); + + // -------------------------------------------- + // Error Handling + // -------------------------------------------- + it("should normalize errors correctly", async () => { + const api = createClient({ + baseURL: "https://dummyjson.com", + }); + + const res = await api.get("/nonexistent"); + + assertError(res); + expect(res.status).toBe(404); + }); + + // -------------------------------------------- + // Refresh Flow + // -------------------------------------------- + it("should refresh token and retry request", async () => { + let token = "expired"; + let refreshCalls = 0; + + await withMockedFetch( + async (input, init) => { + const url = + typeof input === "string" + ? input + : input instanceof URL + ? input.toString() + : input.url; + + if (url.endsWith("/refresh")) { + refreshCalls++; + token = "fresh"; + return jsonResponse({ accessToken: token }); + } + + if (url.endsWith("/me")) { + const headers = + init?.headers instanceof Headers + ? init.headers + : new Headers(init?.headers); + + const auth = headers.get("Authorization"); + + if (auth === "Bearer fresh") { + return jsonResponse({ id: 1 }); + } + + return jsonResponse({}, 401); + } + + return jsonResponse({}, 404); + }, + async () => { + const api = createClient({ + baseURL: "https://api.test", + getToken: () => token, + onRefresh: async () => { + const res = await fetch("https://api.test/refresh"); + const data = (await res.json()) as { accessToken?: string }; + token = data.accessToken ?? ""; + return token; + }, + }); + + const res = await api.get<{ id: number }>("/me"); + + assertSuccess(res); + expect(res.data.id).toBe(1); + expect(refreshCalls).toBe(1); + }, + ); + }); + + // -------------------------------------------- + // Concurrent Refresh + // -------------------------------------------- + it("should dedupe refresh calls under concurrency", async () => { + let token = "expired"; + let refreshCalls = 0; + + await withMockedFetch( + async (input, init) => { + const url = + typeof input === "string" + ? input + : input instanceof URL + ? input.toString() + : input.url; + + if (url.endsWith("/refresh")) { + refreshCalls++; + await delay(50); + token = "fresh"; + return jsonResponse({ accessToken: token }); + } + + if (url.endsWith("/me")) { + const headers = + init?.headers instanceof Headers + ? init.headers + : new Headers(init?.headers); + + const auth = headers.get("Authorization"); + + if (auth === "Bearer fresh") { + return jsonResponse({ id: 1 }); + } + + return jsonResponse({}, 401); + } + + return jsonResponse({}, 404); + }, + async () => { + const api = createClient({ + baseURL: "https://api.test", + getToken: () => token, + onRefresh: async () => { + const res = await fetch("https://api.test/refresh"); + const data = (await res.json()) as { accessToken?: string }; + token = data.accessToken ?? ""; + return token; + }, + }); + + const results = await Promise.all([ + api.get("/me"), + api.get("/me"), + api.get("/me"), + ]); + + expect(results.every((r) => r.ok)).toBe(true); + expect(refreshCalls).toBe(1); + }, + ); + }); + + // -------------------------------------------- + // Invalid JSON + // -------------------------------------------- + it("should handle invalid JSON safely", async () => { + await withMockedFetch( + async () => new Response("invalid-json", { status: 200 }), + async () => { + const api = createClient({ + baseURL: "https://x.com", + }); + + const res = await api.get("/test"); + + assertError(res); + }, + ); + }); +}); diff --git a/tests/json.test.ts b/tests/json.test.ts new file mode 100644 index 0000000..e89a87f --- /dev/null +++ b/tests/json.test.ts @@ -0,0 +1,30 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { createClient } from '../src'; +import { withMockedFetch, FetchMock } from './utils'; + +describe('JSON Parsing', () => { + let fetchMock: FetchMock; + + beforeEach(() => { + fetchMock = withMockedFetch(); + }); + + afterEach(() => { + fetchMock.mockRestore(); + }); + + it('should normalize error on invalid JSON response', async () => { + // Returns 200 but body is invalid JSON + fetchMock.mockResolvedValueOnce(new Response('this is not json', { status: 200 })); + + const client = createClient({ baseURL: 'https://api.test.com' }); + const res = await client.get('/test'); + + expect(res.ok).toBe(false); + if (!res.ok) { + expect(res.status).toBe(200); + expect(typeof res.error).toBe('string'); + expect((res.error as string).toLowerCase()).toContain('failed to parse json response'); + } + }); +}); diff --git a/tests/query.test.ts b/tests/query.test.ts new file mode 100644 index 0000000..1bb72d5 --- /dev/null +++ b/tests/query.test.ts @@ -0,0 +1,22 @@ +import { describe, it, expect } from 'vitest'; +import { createClient } from '../src'; + +describe('Query Parameters', () => { + const client = createClient({ baseURL: 'https://dummyjson.com' }); + + it('should append query parameters correctly', async () => { + const res = await client.get<{ products: unknown[]; skip: number; limit: number }>('/products', { + params: { + limit: 5, + skip: 10, + } + }); + + expect(res.ok).toBe(true); + if (res.ok) { + expect(res.data.limit).toBe(5); + expect(res.data.skip).toBe(10); + expect(res.data.products.length).toBeLessThanOrEqual(5); + } + }, 15000); +}); diff --git a/tests/refresh.test.ts b/tests/refresh.test.ts new file mode 100644 index 0000000..4911fa5 --- /dev/null +++ b/tests/refresh.test.ts @@ -0,0 +1,79 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { createClient } from '../src'; +import { withMockedFetch, jsonResponse, FetchMock } from './utils'; + +describe('Token Refresh Flow', () => { + let fetchMock: FetchMock; + + beforeEach(() => { + fetchMock = withMockedFetch(); + }); + + afterEach(() => { + fetchMock.mockRestore(); + }); + + it('should retry request when refresh is successful', async () => { + // 1st call: returns 401 + fetchMock.mockResolvedValueOnce(new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401 })); + // 2nd call: returns 200 after refresh + fetchMock.mockResolvedValueOnce(jsonResponse({ success: true })); + + const onRefresh = vi.fn().mockResolvedValue('new-token'); + const onAuthFailure = vi.fn(); + + let token = 'old-token'; + + const client = createClient({ + baseURL: 'https://api.test.com', + getToken: () => token, + onRefresh: async () => { + token = await onRefresh(); + return token; + }, + onAuthFailure + }); + + const res = await client.get<{ success: boolean }>('/test'); + + expect(res.ok).toBe(true); + if (res.ok) { + expect(res.data.success).toBe(true); + } + + expect(onRefresh).toHaveBeenCalledTimes(1); + expect(onAuthFailure).not.toHaveBeenCalled(); + expect(fetchMock).toHaveBeenCalledTimes(2); + + // Check that the second call has the new token + const secondCallOpts = fetchMock.mock.calls[1][1] as RequestInit; + expect(secondCallOpts).toBeDefined(); + if (secondCallOpts) { + const headers = new Headers(secondCallOpts.headers as HeadersInit); + expect(headers.get('Authorization')).toBe('Bearer new-token'); + } + }); + + it('should call onAuthFailure if refresh fails', async () => { + fetchMock.mockResolvedValueOnce(new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401 })); + + const onRefresh = vi.fn().mockResolvedValue(null); + const onAuthFailure = vi.fn(); + + const client = createClient({ + baseURL: 'https://api.test.com', + onRefresh, + onAuthFailure + }); + + const res = await client.get('/test'); + + expect(res.ok).toBe(false); + if (!res.ok) { + expect(res.status).toBe(401); + } + expect(onRefresh).toHaveBeenCalledTimes(1); + expect(onAuthFailure).toHaveBeenCalledTimes(1); + expect(fetchMock).toHaveBeenCalledTimes(1); + }); +}); diff --git a/tests/utils.ts b/tests/utils.ts new file mode 100644 index 0000000..4fae119 --- /dev/null +++ b/tests/utils.ts @@ -0,0 +1,22 @@ +import { MockInstance, vi } from 'vitest'; + +export type FetchInput = RequestInfo | URL; +export type FetchInit = RequestInit; +export type FetchMock = MockInstance; + +export function withMockedFetch(): FetchMock { + const mock = vi.fn(); + vi.stubGlobal('fetch', mock); + return mock as unknown as FetchMock; +} + +export function jsonResponse(data: T, init?: ResponseInit): Response { + return new Response(JSON.stringify(data), { + status: 200, + headers: { + 'Content-Type': 'application/json', + ...(init?.headers || {}) + }, + ...init + }); +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..8dbd5d0 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,20 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + globals: true, + coverage: { + provider: "v8", + reporter: ["text", "html"], + include: ["src/**/*.ts"], + + thresholds: { + lines: 85, + branches: 75, + functions: 85, + statements: 85, + }, + }, + }, +}); From 6096e5778472846b015cceee399d37550d5c9cf5 Mon Sep 17 00:00:00 2001 From: Temidayo Gabriel Date: Fri, 1 May 2026 06:07:28 -0400 Subject: [PATCH 2/5] test: update unit test suite for core functionality and CI/CD workflow --- .github/workflows/test.yml | 13 +++- tests/basic.test.ts | 9 +++ tests/concurrent.test.ts | 55 +++++++++++++++ tests/error.test.ts | 136 ++++++++++++++++++++++++++++++++++++- tests/headers.test.ts | 32 +++++++++ tests/refresh.test.ts | 68 +++++++++++++++++++ 6 files changed, 309 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6951025..2e30e33 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,18 +9,25 @@ on: jobs: test: runs-on: ubuntu-latest + strategy: matrix: node-version: [18.x, 20.x, 22.x] steps: - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - cache: 'npm' + cache: "npm" + - name: Install dependencies run: npm ci - - name: Run tests - run: npm run test + + - name: Type check + run: npx tsc --noEmit + + - name: Run tests with coverage + run: npm run coverage diff --git a/tests/basic.test.ts b/tests/basic.test.ts index 22de05a..a1a84c6 100644 --- a/tests/basic.test.ts +++ b/tests/basic.test.ts @@ -24,6 +24,15 @@ describe('Basic Requests', () => { } }, 15000); + it('should perform PATCH request', async () => { + const res = await client.patch<{ id: number; title: string }>('/products/1', { title: 'Patched Product' }); + expect(res.ok).toBe(true); + if (res.ok) { + expect(res.data.id).toBe(1); + expect(res.data.title).toBe('Patched Product'); + } + }, 15000); + it('should perform PUT request', async () => { const res = await client.put<{ id: number; title: string }>('/products/1', { title: 'Updated Product' }); expect(res.ok).toBe(true); diff --git a/tests/concurrent.test.ts b/tests/concurrent.test.ts index f84b2aa..30c96eb 100644 --- a/tests/concurrent.test.ts +++ b/tests/concurrent.test.ts @@ -52,4 +52,59 @@ describe('Concurrent Requests Refresh Deduplication', () => { // total fetch calls should be 6 (3 initial + 3 retries) expect(fetchMock).toHaveBeenCalledTimes(6); }); + + it('should clear refresh promise so subsequent requests can trigger a new refresh', async () => { + // 1. Initial request fails with 401 + fetchMock.mockResolvedValueOnce(new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401 })); + + // 2. Refresh is triggered and fails + const onRefresh = vi.fn().mockRejectedValueOnce(new Error('Refresh failed')); + + const client = createClient({ + baseURL: 'https://api.test.com', + onRefresh + }); + + // First request + const res1 = await client.get('/test'); + expect(res1.ok).toBe(false); + expect(onRefresh).toHaveBeenCalledTimes(1); + + // 3. Second request later fails with 401 + fetchMock.mockResolvedValueOnce(new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401 })); + // 4. Refresh is triggered and succeeds this time + onRefresh.mockResolvedValueOnce('new-token'); + // 5. Retry succeeds + fetchMock.mockResolvedValueOnce(jsonResponse({ id: 2 })); + + const res2 = await client.get<{ id: number }>('/test'); + expect(res2.ok).toBe(true); + expect(onRefresh).toHaveBeenCalledTimes(2); // Should be called again! + }); + + it('should share in-flight refresh promise and catch its failure without unhandled rejections', async () => { + // 2 concurrent calls fail with 401 + fetchMock.mockResolvedValueOnce(new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401 })); + fetchMock.mockResolvedValueOnce(new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401 })); + + // Refresh is triggered and fails + const onRefresh = vi.fn().mockImplementation(async () => { + await new Promise(resolve => setTimeout(resolve, 50)); + throw new Error('Refresh completely failed'); + }); + + const client = createClient({ + baseURL: 'https://api.test.com', + onRefresh + }); + + const [res1, res2] = await Promise.all([ + client.get('/test/1'), + client.get('/test/2') + ]); + + expect(res1.ok).toBe(false); + expect(res2.ok).toBe(false); + expect(onRefresh).toHaveBeenCalledTimes(1); // Still deduplicated + }); }); diff --git a/tests/error.test.ts b/tests/error.test.ts index 1f7865f..2b0130b 100644 --- a/tests/error.test.ts +++ b/tests/error.test.ts @@ -1,5 +1,6 @@ -import { describe, it, expect } from 'vitest'; +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { createClient } from '../src'; +import { withMockedFetch, FetchMock } from './utils'; describe('Error Handling', () => { const client = createClient({ baseURL: 'https://dummyjson.com' }); @@ -24,4 +25,137 @@ describe('Error Handling', () => { expect(typeof res.error).toBe('string'); } }, 15000); + + describe('Mocked Error Edge Cases', () => { + let fetchMock: FetchMock; + + beforeEach(() => { + fetchMock = withMockedFetch(); + }); + + afterEach(() => { + fetchMock.mockRestore(); + }); + + it('should retry on network error and respect retry limit', async () => { + fetchMock.mockRejectedValue(new Error('Network failure')); + + const client = createClient({ + baseURL: 'https://api.test.com', + retry: 2, + retryDelay: 10 // small delay for fast test + }); + + const res = await client.get('/test'); + + expect(res.ok).toBe(false); + expect(fetchMock).toHaveBeenCalledTimes(3); // 1 initial + 2 retries + if (!res.ok) { + expect(res.status).toBe(0); + expect(res.error).toBe('Network failure'); + } + }); + + it('should handle HTTP error with empty body', async () => { + fetchMock.mockResolvedValueOnce(new Response(null, { status: 500 })); + + const client = createClient({ baseURL: 'https://api.test.com' }); + const res = await client.get('/test'); + + expect(res.ok).toBe(false); + if (!res.ok) { + expect(res.status).toBe(500); + expect(res.error).toContain('Request failed with status 500'); + } + }); + + it('should handle HTTP error with non-JSON body (string + fallback)', async () => { + fetchMock.mockResolvedValueOnce(new Response('Server Error String', { status: 500 })); + + const client = createClient({ baseURL: 'https://api.test.com' }); + const res = await client.get('/test'); + + expect(res.ok).toBe(false); + if (!res.ok) { + expect(res.status).toBe(500); + expect(res.error).toContain('Request failed with status 500'); + } + }); + + it('should handle network timeout or cancellation using AbortSignal', async () => { + const abortError = new Error('The operation was aborted'); + abortError.name = 'AbortError'; + fetchMock.mockRejectedValueOnce(abortError); + + const client = createClient({ baseURL: 'https://api.test.com' }); + const controller = new AbortController(); + controller.abort(); + + const res = await client.get('/test', { signal: controller.signal }); + + expect(res.ok).toBe(false); + if (!res.ok) { + expect(res.error).toBe('Request was cancelled'); + expect(res.status).toBe(0); + } + }); + + it('should capture unknown error types safely (string)', async () => { + fetchMock.mockRejectedValueOnce('Some weird string error'); + + const client = createClient({ baseURL: 'https://api.test.com' }); + const res = await client.get('/test'); + + expect(res.ok).toBe(false); + if (!res.ok) { + expect(res.error).toBe('Some weird string error'); + } + }); + + it('should capture unknown error types safely (non-string)', async () => { + fetchMock.mockRejectedValueOnce({ weird: 'object' }); + + const client = createClient({ baseURL: 'https://api.test.com' }); + const res = await client.get('/test'); + + expect(res.ok).toBe(false); + if (!res.ok) { + expect(res.error).toBe('An unknown error occurred'); + } + }); + + it('should handle request timeout', async () => { + fetchMock.mockImplementationOnce(async (input, init) => { + await new Promise((resolve) => setTimeout(resolve, 50)); + if ((init as RequestInit)?.signal?.aborted) { + const err = new Error('The operation was aborted'); + err.name = 'AbortError'; + throw err; + } + return new Response('ok'); + }); + + const client = createClient({ baseURL: 'https://api.test.com' }); + const res = await client.get('/test', { timeout: 10 }); // 10ms timeout + + expect(res.ok).toBe(false); + if (!res.ok) { + expect(res.error).toBe('Request timed out'); + expect(res.status).toBe(0); + } + }); + + it('should handle JSON error body without message or error properties', async () => { + fetchMock.mockResolvedValueOnce(new Response(JSON.stringify({ weirdField: true }), { status: 400 })); + + const client = createClient({ baseURL: 'https://api.test.com' }); + const res = await client.get('/test'); + + expect(res.ok).toBe(false); + if (!res.ok) { + expect(res.error).toBe('Request failed with status 400'); + expect(res.status).toBe(400); + } + }); + }); }); diff --git a/tests/headers.test.ts b/tests/headers.test.ts index 211a433..a8a1ee6 100644 --- a/tests/headers.test.ts +++ b/tests/headers.test.ts @@ -65,4 +65,36 @@ describe('Headers Handling', () => { expect(headers.get('Authorization')).toBe('Bearer my-secret-token'); } }); + + it('should not inject authorization header if getToken returns undefined', async () => { + fetchMock.mockResolvedValueOnce(jsonResponse({ success: true })); + + const client = createClient({ + baseURL: 'https://api.test.com', + getToken: () => undefined + }); + + await client.get('/test'); + + const callArgs = fetchMock.mock.calls[0]; + const options = callArgs[1] as RequestInit; + const headers = new Headers(options.headers as HeadersInit); + expect(headers.has('Authorization')).toBe(false); + }); + + it('should not inject authorization header if getToken returns empty string', async () => { + fetchMock.mockResolvedValueOnce(jsonResponse({ success: true })); + + const client = createClient({ + baseURL: 'https://api.test.com', + getToken: () => '' + }); + + await client.get('/test'); + + const callArgs = fetchMock.mock.calls[0]; + const options = callArgs[1] as RequestInit; + const headers = new Headers(options.headers as HeadersInit); + expect(headers.has('Authorization')).toBe(false); + }); }); diff --git a/tests/refresh.test.ts b/tests/refresh.test.ts index 4911fa5..4bc9d0d 100644 --- a/tests/refresh.test.ts +++ b/tests/refresh.test.ts @@ -76,4 +76,72 @@ describe('Token Refresh Flow', () => { expect(onAuthFailure).toHaveBeenCalledTimes(1); expect(fetchMock).toHaveBeenCalledTimes(1); }); + + it('should handle request without refresh configured (no onRefresh)', async () => { + fetchMock.mockResolvedValueOnce(new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401 })); + + const onAuthFailure = vi.fn(); + const client = createClient({ + baseURL: 'https://api.test.com', + onAuthFailure + // no onRefresh + }); + + const res = await client.get('/test'); + + expect(res.ok).toBe(false); + if (!res.ok) { + expect(res.status).toBe(401); + } + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(onAuthFailure).toHaveBeenCalledTimes(1); + }); + + it('should call onAuthFailure if onRefresh throws an error', async () => { + fetchMock.mockResolvedValueOnce(new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401 })); + + const onRefresh = vi.fn().mockRejectedValue(new Error('Refresh failed')); + const onAuthFailure = vi.fn(); + + const client = createClient({ + baseURL: 'https://api.test.com', + onRefresh, + onAuthFailure + }); + + const res = await client.get('/test'); + + expect(res.ok).toBe(false); + if (!res.ok) { + expect(res.status).toBe(401); + } + expect(onRefresh).toHaveBeenCalledTimes(1); + expect(onAuthFailure).toHaveBeenCalledTimes(1); + }); + + it('should call onAuthFailure if retry request after refresh ALSO returns 401', async () => { + // 1st call: returns 401 + fetchMock.mockResolvedValueOnce(new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401 })); + // 2nd call (retry): returns 401 again + fetchMock.mockResolvedValueOnce(new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401 })); + + const onRefresh = vi.fn().mockResolvedValue('invalid-new-token'); + const onAuthFailure = vi.fn(); + + const client = createClient({ + baseURL: 'https://api.test.com', + onRefresh, + onAuthFailure + }); + + const res = await client.get('/test'); + + expect(res.ok).toBe(false); + if (!res.ok) { + expect(res.status).toBe(401); + } + expect(onRefresh).toHaveBeenCalledTimes(1); + expect(onAuthFailure).toHaveBeenCalledTimes(1); + expect(fetchMock).toHaveBeenCalledTimes(2); + }); }); From 4f8bbea8dbcdd50ffdad3f205e9eafb083ba9e63 Mon Sep 17 00:00:00 2001 From: Temidayo Gabriel Date: Fri, 1 May 2026 06:34:34 -0400 Subject: [PATCH 3/5] chore: add rolldown to devDependencies in package.json --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 4b42e50..9c48100 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "@vitest/coverage-v8": "^4.1.5", "tsup": "^8.5.1", "typescript": "^6.0.3", - "vitest": "^4.1.5" + "vitest": "^4.1.5", + "rolldown": "^0.13.0" } } From f390ec3a98e99d07269d741f6b928bbf345b9def Mon Sep 17 00:00:00 2001 From: Temidayo Gabriel Date: Fri, 1 May 2026 06:48:48 -0400 Subject: [PATCH 4/5] build: upgrade rolldown to v1.0.0-rc.18 --- package-lock.json | 415 ++++++++++++++++++++++++++++++++++++++++------ package.json | 4 +- 2 files changed, 364 insertions(+), 55 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2f60c4e..d5a491a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "devDependencies": { "@types/node": "^25.6.0", "@vitest/coverage-v8": "^4.1.5", + "rolldown": "^1.0.0-rc.18", "tsup": "^8.5.1", "typescript": "^6.0.3", "vitest": "^4.1.5" @@ -638,9 +639,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz", - "integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==", + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.18.tgz", + "integrity": "sha512-apJq2ktnGp27nSInMR5Vcj8kY6xJzDAvfdIFlpDcAK/w4cDO58qVoi1YQsES/SKiFNge/6e4CUzgjfHduYqWpQ==", "cpu": [ "arm64" ], @@ -655,9 +656,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz", - "integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==", + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.18.tgz", + "integrity": "sha512-5Ofot8xbs+pxRHJqm9/9N/4sTQOvdrwEsmPE9pdLEEoAbdZtG6F2LMDfO1sp6ZAtXJuJV/21ew2srq3W8NXB5g==", "cpu": [ "x64" ], @@ -672,9 +673,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz", - "integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==", + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.18.tgz", + "integrity": "sha512-7h8eeOTT1eyqJyx64BFCnWZpNm486hGWt2sqeLLgDxA0xI1oGZ9H7gK1S85uNGmBhkdPwa/6reTxfFFKvIsebw==", "cpu": [ "x64" ], @@ -689,9 +690,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz", - "integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==", + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.18.tgz", + "integrity": "sha512-eRcm/HVt9U/JFu5RKAEKwGQYtDCKWLiaH6wOnsSEp6NMBb/3Os8LgHZlNyzMpFVNmiiMFlfb2zEnebfzJrHFmg==", "cpu": [ "arm" ], @@ -706,9 +707,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz", - "integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==", + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.18.tgz", + "integrity": "sha512-SOrT/cT4ukTmgnrEz/Hg3m7LBnuCLW9psDeMKrimRWY4I8DmnO7Lco8W2vtqPmMkbVu8iJ+g4GFLVLLOVjJ9DQ==", "cpu": [ "arm64" ], @@ -723,9 +724,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz", - "integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==", + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.18.tgz", + "integrity": "sha512-QWjdxN1HJCpBTAcZ5N5F7wju3gVPzRzSpmGzx7na0c/1qpN9CFil+xt+l9lV/1M6/gqHSNXCiqPfwhVJPeLnug==", "cpu": [ "arm64" ], @@ -774,9 +775,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz", - "integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==", + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.18.tgz", + "integrity": "sha512-uCo8ElcCIAMyYAZyuIZ81oFkhTSIllNvUCHCAlbhlN4ji3uC28h7IIdlXyIvGO7HsuqnV9p3rD/bpH7XhIyhRw==", "cpu": [ "x64" ], @@ -791,9 +792,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz", - "integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==", + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.18.tgz", + "integrity": "sha512-XNOQZtuE6yUIvx4rwGemwh8kpL1xvU41FXy/s9K7T/3JVcqGzo3NfKM2HrbrGgfPYGFW42f07Wk++aOC6B9NWA==", "cpu": [ "x64" ], @@ -825,9 +826,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz", - "integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==", + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.18.tgz", + "integrity": "sha512-+J9YGmc+czgqlhYmwun3S3O0FIZhsH8ep2456xwjAdIOmuJxM7xz4P4PtrxU+Bz17a/5bqPA8o3HAAoX0teUdg==", "cpu": [ "wasm32" ], @@ -844,9 +845,9 @@ } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz", - "integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==", + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.18.tgz", + "integrity": "sha512-zsu47DgU0FQzSwi6sU9dZoEdUv7pc1AptSEz/Z8HBg54sV0Pbs3N0+CrIbTsgiu6EyoaNN9CHboqbLaz9lhOyQ==", "cpu": [ "arm64" ], @@ -861,9 +862,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz", - "integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==", + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.18.tgz", + "integrity": "sha512-7H+3yqGgmnlDTRRhw/xpYY9J1kf4GC681nVc4GqKhExZTDrVVrV2tsOR9kso0fvgBdcTCcQShx4SLLoHgaLwhg==", "cpu": [ "x64" ], @@ -2321,14 +2322,14 @@ } }, "node_modules/rolldown": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz", - "integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==", + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.18.tgz", + "integrity": "sha512-phmyKBpuBdRYDf4hgyynGAYn/rDDe+iZXKVJ7WX5b1zQzpLkP5oJRPGsfJuHdzPMlyyEO/4sPW6yfSx2gf7lVg==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.127.0", - "@rolldown/pluginutils": "1.0.0-rc.17" + "@oxc-project/types": "=0.128.0", + "@rolldown/pluginutils": "1.0.0-rc.18" }, "bin": { "rolldown": "bin/cli.mjs" @@ -2337,23 +2338,108 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-rc.17", - "@rolldown/binding-darwin-arm64": "1.0.0-rc.17", - "@rolldown/binding-darwin-x64": "1.0.0-rc.17", - "@rolldown/binding-freebsd-x64": "1.0.0-rc.17", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17", - "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17", - "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17", - "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17", - "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17", - "@rolldown/binding-linux-x64-musl": "1.0.0-rc.17", - "@rolldown/binding-openharmony-arm64": "1.0.0-rc.17", - "@rolldown/binding-wasm32-wasi": "1.0.0-rc.17", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17", - "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17" + "@rolldown/binding-android-arm64": "1.0.0-rc.18", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.18", + "@rolldown/binding-darwin-x64": "1.0.0-rc.18", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.18", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.18", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.18", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.18", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.18", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.18", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.18", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.18", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.18", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.18", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.18", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.18" + } + }, + "node_modules/rolldown/node_modules/@oxc-project/types": { + "version": "0.128.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.128.0.tgz", + "integrity": "sha512-huv1Y/LzBJkBVHt3OlC7u0zHBW9qXf1FdD7sGmc1rXc2P1mTwHssYv7jyGx5KAACSCH+9B3Bhn6Z9luHRvf7pQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/rolldown/node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.18.tgz", + "integrity": "sha512-lIDyUAfD7U3+BWKzdxMbJcsYHuqXqmGz40aeRqvuAm3y5TkJSYTBW2RDrn65DJFPQqVjUAUqq5uz8urzQ8aBdQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/rolldown/node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.18.tgz", + "integrity": "sha512-ugCOyj7a4d9h3q9B+wXmf6g3a68UsjGh6dob5DHevHGMwDUbhsYNbSPxJsENcIttJZ9jv7qGM2UesLw5jqIhdg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, + "node_modules/rolldown/node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.18.tgz", + "integrity": "sha512-kKWRhbsotpXkGbcd5dllUWg5gEXcDAa8u5YnP9AV5DYNbvJHGzzuwv7dpmhc8NqKMJldl0a+x76IHbspEpEmdA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/rolldown/node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.18.tgz", + "integrity": "sha512-tSn/kzrfa7tNOXr7sEacDBN4YsIqTyLqh45IO0nHDwtpKIDNDJr+VFojt+4klSpChxB29JLyduSsE0MKEwa65A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.18.tgz", + "integrity": "sha512-CUY5Mnhe64xQBGZEEXQ5WyZwsc1JU3vAZLIxtrsBt3LO6UOb+C8GunVKqe9sT8NeWb4lqSaoJtp2xo6GxT1MNw==", + "dev": true, + "license": "MIT" + }, "node_modules/rollup": { "version": "4.60.2", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", @@ -2737,6 +2823,229 @@ } } }, + "node_modules/vite/node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz", + "integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz", + "integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz", + "integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz", + "integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz", + "integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz", + "integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz", + "integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz", + "integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/rolldown": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz", + "integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.127.0", + "@rolldown/pluginutils": "1.0.0-rc.17" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.17", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.17", + "@rolldown/binding-darwin-x64": "1.0.0-rc.17", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.17", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.17", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.17", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.17", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17" + } + }, "node_modules/vitest": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.5.tgz", diff --git a/package.json b/package.json index 9c48100..d10c264 100644 --- a/package.json +++ b/package.json @@ -39,9 +39,9 @@ "devDependencies": { "@types/node": "^25.6.0", "@vitest/coverage-v8": "^4.1.5", + "rolldown": "^1.0.0-rc.18", "tsup": "^8.5.1", "typescript": "^6.0.3", - "vitest": "^4.1.5", - "rolldown": "^0.13.0" + "vitest": "^4.1.5" } } From 05185e8f0e888fb229e262c0a39cf07d4938728c Mon Sep 17 00:00:00 2001 From: Temidayo Gabriel Date: Fri, 1 May 2026 06:54:31 -0400 Subject: [PATCH 5/5] update github actions node version --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2e30e33..8dc8e29 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: - node-version: [18.x, 20.x, 22.x] + node-version: [20.x, 22.x] steps: - uses: actions/checkout@v4