diff --git a/.github/workflows/code-coverage.yaml b/.github/workflows/code-coverage.yaml index 9804ac5..de20c71 100644 --- a/.github/workflows/code-coverage.yaml +++ b/.github/workflows/code-coverage.yaml @@ -30,7 +30,7 @@ jobs: node-version: 22 - name: Install Dependencies - run: npm install pnpm -g && pnpm install + run: npm install pnpm@10 -g && pnpm install - name: Build run: pnpm build diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 145ef9d..b121005 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -28,7 +28,7 @@ jobs: node-version: 22 - name: Install Dependencies - run: npm install pnpm -g && pnpm install + run: npm install pnpm@10 -g && pnpm install - name: Build run: pnpm build diff --git a/.github/workflows/tests-nodejs-22.yaml b/.github/workflows/tests-nodejs-22.yaml index 3644796..1764286 100644 --- a/.github/workflows/tests-nodejs-22.yaml +++ b/.github/workflows/tests-nodejs-22.yaml @@ -34,7 +34,7 @@ jobs: node-version: ${{ matrix.node-version }} - name: Install Dependencies - run: npm install pnpm -g && pnpm install + run: npm install pnpm@10 -g && pnpm install - name: Build run: pnpm build diff --git a/.github/workflows/tests-nodejs-20.yaml b/.github/workflows/tests-nodejs-26.yaml similarity index 86% rename from .github/workflows/tests-nodejs-20.yaml rename to .github/workflows/tests-nodejs-26.yaml index 01e9952..00ca5ff 100644 --- a/.github/workflows/tests-nodejs-20.yaml +++ b/.github/workflows/tests-nodejs-26.yaml @@ -24,7 +24,7 @@ jobs: strategy: matrix: - node-version: ['20'] + node-version: ['26'] steps: - uses: actions/checkout@v4 @@ -34,11 +34,10 @@ jobs: node-version: ${{ matrix.node-version }} - name: Install Dependencies - run: npm install pnpm -g && pnpm install + run: npm install pnpm@10 -g && pnpm install - - name: Build + - name: Build run: pnpm build - - name: Testing + - name: Testing run: pnpm test - diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index b07196f..e0e82f0 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -34,7 +34,7 @@ jobs: node-version: ${{ matrix.node-version }} - name: Install Dependencies - run: npm install pnpm -g && pnpm install + run: npm install pnpm@10 -g && pnpm install - name: Build run: pnpm build diff --git a/package.json b/package.json index 635eac8..c8ab563 100644 --- a/package.json +++ b/package.json @@ -39,15 +39,15 @@ "node": ">=20.12.0" }, "devDependencies": { - "@biomejs/biome": "^2.4.11", + "@biomejs/biome": "^2.4.15", "@faker-js/faker": "^10.4.0", "@types/node": "^25.6.0", - "@vitest/coverage-v8": "^4.1.4", + "@vitest/coverage-v8": "^4.1.5", "rimraf": "^6.1.3", "tsd": "^0.33.0", "tsdown": "^0.21.7", "typescript": "^6.0.2", - "vitest": "^4.1.4" + "vitest": "^4.1.5" }, "files": [ "dist", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 602c5fc..4e018ca 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,8 +22,8 @@ importers: version: 10.3.1 devDependencies: '@biomejs/biome': - specifier: ^2.4.11 - version: 2.4.11 + specifier: ^2.4.15 + version: 2.4.15 '@faker-js/faker': specifier: ^10.4.0 version: 10.4.0 @@ -31,8 +31,8 @@ importers: specifier: ^25.6.0 version: 25.6.0 '@vitest/coverage-v8': - specifier: ^4.1.4 - version: 4.1.4(vitest@4.1.4) + specifier: ^4.1.5 + version: 4.1.5(vitest@4.1.5) rimraf: specifier: ^6.1.3 version: 6.1.3 @@ -46,8 +46,8 @@ importers: specifier: ^6.0.2 version: 6.0.2 vitest: - specifier: ^4.1.4 - version: 4.1.4(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4)(vite@7.3.1(@types/node@25.6.0)(jiti@2.6.1)(terser@5.39.0)) + specifier: ^4.1.5 + version: 4.1.5(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5)(vite@7.3.1(@types/node@25.6.0)(jiti@2.6.1)(terser@5.39.0)) packages: @@ -97,59 +97,59 @@ packages: resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} - '@biomejs/biome@2.4.11': - resolution: {integrity: sha512-nWxHX8tf3Opb/qRgZpBbsTOqOodkbrkJ7S+JxJAruxOReaDPPmPuLBAGQ8vigyUgo0QBB+oQltNEAvalLcjggA==} + '@biomejs/biome@2.4.15': + resolution: {integrity: sha512-j5VH3a/h/HXTKBM50MDMxRCzkeLv9S2XJcW2WgnZT1+xyisi+0bISrXR82gCX+8S9lvK0skEvHJRN+3Ktr2hlw==} engines: {node: '>=14.21.3'} hasBin: true - '@biomejs/cli-darwin-arm64@2.4.11': - resolution: {integrity: sha512-wOt+ed+L2dgZanWyL6i29qlXMc088N11optzpo10peayObBaAshbTcxKUchzEMp9QSY8rh5h6VfAFE3WTS1rqg==} + '@biomejs/cli-darwin-arm64@2.4.15': + resolution: {integrity: sha512-rF3PPqLq1yoST79zaQbDjVJwsuIeci/O+9bgNmC5QpgOqz6aqYuzA4abyAGx+mgyiDXn4A049xAN8gijbuR1Qg==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [darwin] - '@biomejs/cli-darwin-x64@2.4.11': - resolution: {integrity: sha512-gZ6zR8XmZlExfi/Pz/PffmdpWOQ8Qhy7oBztgkR8/ylSRyLwfRPSadmiVCV8WQ8PoJ2MWUy2fgID9zmtgUUJmw==} + '@biomejs/cli-darwin-x64@2.4.15': + resolution: {integrity: sha512-/5KHXYMfSJs1fNXiX30xFtI8JcCFV6zaVVLxOa0M2sfqBKHkpQhRTv94yxQWxeTY2lzo2OuTlNvPC+hDQt2wcQ==} engines: {node: '>=14.21.3'} cpu: [x64] os: [darwin] - '@biomejs/cli-linux-arm64-musl@2.4.11': - resolution: {integrity: sha512-+Sbo1OAmlegtdwqFE8iOxFIWLh1B3OEgsuZfBpyyN/kWuqZ8dx9ZEes6zVnDMo+zRHF2wLynRVhoQmV7ohxl2Q==} + '@biomejs/cli-linux-arm64-musl@2.4.15': + resolution: {integrity: sha512-ZPcxznxm0pogHBLZhYntyR3sR+MrZjqJIKEr7ZqVen0Rl+P/4upVmfYXjftizi9RoqZntg33fv/1fbdhbYXpEQ==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] libc: [musl] - '@biomejs/cli-linux-arm64@2.4.11': - resolution: {integrity: sha512-avdJaEElXrKceK0va9FkJ4P5ci3N01TGkc6ni3P8l3BElqbOz42Wg2IyX3gbh0ZLEd4HVKEIrmuVu/AMuSeFFA==} + '@biomejs/cli-linux-arm64@2.4.15': + resolution: {integrity: sha512-owaAMZD/T4LrD0ELNCk0Km3qrRHuM0X6EAyVE1FSqGY0rbLoiDLrO4Us2tllm6cAeB2Ioa9C2C08NZPdr8+0Ug==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] libc: [glibc] - '@biomejs/cli-linux-x64-musl@2.4.11': - resolution: {integrity: sha512-bexd2IklK7ZgPhrz6jXzpIL6dEAH9MlJU1xGTrypx+FICxrXUp4CqtwfiuoDKse+UlgAlWtzML3jrMqeEAHEhA==} + '@biomejs/cli-linux-x64-musl@2.4.15': + resolution: {integrity: sha512-CNq/9W38SYSH023lfcQ4KKU8K0YX8T//FZUhcgtMMRABDojx5XsMV7jlweAvGSl389wJQB29Qo6Zb/a+jdvt+w==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] libc: [musl] - '@biomejs/cli-linux-x64@2.4.11': - resolution: {integrity: sha512-TagWV0iomp5LnEnxWFg4nQO+e52Fow349vaX0Q/PIcX6Zhk4GGBgp3qqZ8PVkpC+cuehRctMf3+6+FgQ8jCEFQ==} + '@biomejs/cli-linux-x64@2.4.15': + resolution: {integrity: sha512-0jj7THz12GbUOLmMibktK6DZjqz2zV64KFxyBtcFTKPiiOIY0a7vns1elpO1dERvxpsZ5ik0oFfz0oGwFde1+g==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] libc: [glibc] - '@biomejs/cli-win32-arm64@2.4.11': - resolution: {integrity: sha512-RJhaTnY8byzxDt4bDVb7AFPHkPcjOPK3xBip4ZRTrN3TEfyhjLRm3r3mqknqydgVTB74XG8l4jMLwEACEeihVg==} + '@biomejs/cli-win32-arm64@2.4.15': + resolution: {integrity: sha512-ouhkYdlhp/1GghEJPdWwD/Vi3gQ1nFxuSpMolWsbq3Lsq3QUR4jl6UdhhscdCugKU5vOEuMiJhvKj66O0OCq+w==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [win32] - '@biomejs/cli-win32-x64@2.4.11': - resolution: {integrity: sha512-A8D3JM/00C2KQgUV3oj8Ba15EHEYwebAGCy5Sf9GAjr5Y3+kJIYOiESoqRDeuRZueuMdCsbLZIUqmPhpYXJE9A==} + '@biomejs/cli-win32-x64@2.4.15': + resolution: {integrity: sha512-zBrGq5mx5wwpnow4+2BxUvleDM+GNd4sLbPaMapsSLQLD0NGRCquqPBTgN+7XkUteHvj7M+BstuI8tmnV7+HgQ==} engines: {node: '>=14.21.3'} cpu: [x64] os: [win32] @@ -667,20 +667,20 @@ packages: '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} - '@vitest/coverage-v8@4.1.4': - resolution: {integrity: sha512-x7FptB5oDruxNPDNY2+S8tCh0pcq7ymCe1gTHcsp733jYjrJl8V1gMUlVysuCD9Kz46Xz9t1akkv08dPcYDs1w==} + '@vitest/coverage-v8@4.1.5': + resolution: {integrity: sha512-38C0/Ddb7HcRG0Z4/DUem8x57d2p9jYgp18mkaYswEOQBGsI1CG4f/hjm0ZCeaJfWhSZ4k7jgs29V1Zom7Ki9A==} peerDependencies: - '@vitest/browser': 4.1.4 - vitest: 4.1.4 + '@vitest/browser': 4.1.5 + vitest: 4.1.5 peerDependenciesMeta: '@vitest/browser': optional: true - '@vitest/expect@4.1.4': - resolution: {integrity: sha512-iPBpra+VDuXmBFI3FMKHSFXp3Gx5HfmSCE8X67Dn+bwephCnQCaB7qWK2ldHa+8ncN8hJU8VTMcxjPpyMkUjww==} + '@vitest/expect@4.1.5': + resolution: {integrity: sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw==} - '@vitest/mocker@4.1.4': - resolution: {integrity: sha512-R9HTZBhW6yCSGbGQnDnH3QHfJxokKN4KB+Yvk9Q1le7eQNYwiCyKxmLmurSpFy6BzJanSLuEUDrD+j97Q+ZLPg==} + '@vitest/mocker@4.1.5': + resolution: {integrity: sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw==} peerDependencies: msw: ^2.4.9 vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -690,20 +690,20 @@ packages: vite: optional: true - '@vitest/pretty-format@4.1.4': - resolution: {integrity: sha512-ddmDHU0gjEUyEVLxtZa7xamrpIefdEETu3nZjWtHeZX4QxqJ7tRxSteHVXJOcr8jhiLoGAhkK4WJ3WqBpjx42A==} + '@vitest/pretty-format@4.1.5': + resolution: {integrity: sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==} - '@vitest/runner@4.1.4': - resolution: {integrity: sha512-xTp7VZ5aXP5ZJrn15UtJUWlx6qXLnGtF6jNxHepdPHpMfz/aVPx+htHtgcAL2mDXJgKhpoo2e9/hVJsIeFbytQ==} + '@vitest/runner@4.1.5': + resolution: {integrity: sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ==} - '@vitest/snapshot@4.1.4': - resolution: {integrity: sha512-MCjCFgaS8aZz+m5nTcEcgk/xhWv0rEH4Yl53PPlMXOZ1/Ka2VcZU6CJ+MgYCZbcJvzGhQRjVrGQNZqkGPttIKw==} + '@vitest/snapshot@4.1.5': + resolution: {integrity: sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ==} - '@vitest/spy@4.1.4': - resolution: {integrity: sha512-XxNdAsKW7C+FLydqFJLb5KhJtl3PGCMmYwFRfhvIgxJvLSXhhVI1zM8f1qD3Zg7RCjTSzDVyct6sghs9UEgBEQ==} + '@vitest/spy@4.1.5': + resolution: {integrity: sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ==} - '@vitest/utils@4.1.4': - resolution: {integrity: sha512-13QMT+eysM5uVGa1rG4kegGYNp6cnQcsTc67ELFbhNLQO+vgsygtYJx2khvdt4gVQqSSpC/KT5FZZxUpP3Oatw==} + '@vitest/utils@4.1.5': + resolution: {integrity: sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==} acorn@8.16.0: resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} @@ -1522,20 +1522,20 @@ packages: yaml: optional: true - vitest@4.1.4: - resolution: {integrity: sha512-tFuJqTxKb8AvfyqMfnavXdzfy3h3sWZRWwfluGbkeR7n0HUev+FmNgZ8SDrRBTVrVCjgH5cA21qGbCffMNtWvg==} + vitest@4.1.5: + resolution: {integrity: sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true 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.4 - '@vitest/browser-preview': 4.1.4 - '@vitest/browser-webdriverio': 4.1.4 - '@vitest/coverage-istanbul': 4.1.4 - '@vitest/coverage-v8': 4.1.4 - '@vitest/ui': 4.1.4 + '@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 @@ -1620,39 +1620,39 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} - '@biomejs/biome@2.4.11': + '@biomejs/biome@2.4.15': optionalDependencies: - '@biomejs/cli-darwin-arm64': 2.4.11 - '@biomejs/cli-darwin-x64': 2.4.11 - '@biomejs/cli-linux-arm64': 2.4.11 - '@biomejs/cli-linux-arm64-musl': 2.4.11 - '@biomejs/cli-linux-x64': 2.4.11 - '@biomejs/cli-linux-x64-musl': 2.4.11 - '@biomejs/cli-win32-arm64': 2.4.11 - '@biomejs/cli-win32-x64': 2.4.11 + '@biomejs/cli-darwin-arm64': 2.4.15 + '@biomejs/cli-darwin-x64': 2.4.15 + '@biomejs/cli-linux-arm64': 2.4.15 + '@biomejs/cli-linux-arm64-musl': 2.4.15 + '@biomejs/cli-linux-x64': 2.4.15 + '@biomejs/cli-linux-x64-musl': 2.4.15 + '@biomejs/cli-win32-arm64': 2.4.15 + '@biomejs/cli-win32-x64': 2.4.15 - '@biomejs/cli-darwin-arm64@2.4.11': + '@biomejs/cli-darwin-arm64@2.4.15': optional: true - '@biomejs/cli-darwin-x64@2.4.11': + '@biomejs/cli-darwin-x64@2.4.15': optional: true - '@biomejs/cli-linux-arm64-musl@2.4.11': + '@biomejs/cli-linux-arm64-musl@2.4.15': optional: true - '@biomejs/cli-linux-arm64@2.4.11': + '@biomejs/cli-linux-arm64@2.4.15': optional: true - '@biomejs/cli-linux-x64-musl@2.4.11': + '@biomejs/cli-linux-x64-musl@2.4.15': optional: true - '@biomejs/cli-linux-x64@2.4.11': + '@biomejs/cli-linux-x64@2.4.15': optional: true - '@biomejs/cli-win32-arm64@2.4.11': + '@biomejs/cli-win32-arm64@2.4.15': optional: true - '@biomejs/cli-win32-x64@2.4.11': + '@biomejs/cli-win32-x64@2.4.15': optional: true '@cacheable/memory@2.0.8': @@ -1995,10 +1995,10 @@ snapshots: '@types/normalize-package-data@2.4.4': {} - '@vitest/coverage-v8@4.1.4(vitest@4.1.4)': + '@vitest/coverage-v8@4.1.5(vitest@4.1.5)': dependencies: '@bcoe/v8-coverage': 1.0.2 - '@vitest/utils': 4.1.4 + '@vitest/utils': 4.1.5 ast-v8-to-istanbul: 1.0.0 istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 @@ -2007,46 +2007,46 @@ snapshots: obug: 2.1.1 std-env: 4.0.0 tinyrainbow: 3.1.0 - vitest: 4.1.4(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4)(vite@7.3.1(@types/node@25.6.0)(jiti@2.6.1)(terser@5.39.0)) + vitest: 4.1.5(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5)(vite@7.3.1(@types/node@25.6.0)(jiti@2.6.1)(terser@5.39.0)) - '@vitest/expect@4.1.4': + '@vitest/expect@4.1.5': dependencies: '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 - '@vitest/spy': 4.1.4 - '@vitest/utils': 4.1.4 + '@vitest/spy': 4.1.5 + '@vitest/utils': 4.1.5 chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.4(vite@7.3.1(@types/node@25.6.0)(jiti@2.6.1)(terser@5.39.0))': + '@vitest/mocker@4.1.5(vite@7.3.1(@types/node@25.6.0)(jiti@2.6.1)(terser@5.39.0))': dependencies: - '@vitest/spy': 4.1.4 + '@vitest/spy': 4.1.5 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: vite: 7.3.1(@types/node@25.6.0)(jiti@2.6.1)(terser@5.39.0) - '@vitest/pretty-format@4.1.4': + '@vitest/pretty-format@4.1.5': dependencies: tinyrainbow: 3.1.0 - '@vitest/runner@4.1.4': + '@vitest/runner@4.1.5': dependencies: - '@vitest/utils': 4.1.4 + '@vitest/utils': 4.1.5 pathe: 2.0.3 - '@vitest/snapshot@4.1.4': + '@vitest/snapshot@4.1.5': dependencies: - '@vitest/pretty-format': 4.1.4 - '@vitest/utils': 4.1.4 + '@vitest/pretty-format': 4.1.5 + '@vitest/utils': 4.1.5 magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@4.1.4': {} + '@vitest/spy@4.1.5': {} - '@vitest/utils@4.1.4': + '@vitest/utils@4.1.5': dependencies: - '@vitest/pretty-format': 4.1.4 + '@vitest/pretty-format': 4.1.5 convert-source-map: 2.0.0 tinyrainbow: 3.1.0 @@ -2842,15 +2842,15 @@ snapshots: jiti: 2.6.1 terser: 5.39.0 - vitest@4.1.4(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4)(vite@7.3.1(@types/node@25.6.0)(jiti@2.6.1)(terser@5.39.0)): + vitest@4.1.5(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5)(vite@7.3.1(@types/node@25.6.0)(jiti@2.6.1)(terser@5.39.0)): dependencies: - '@vitest/expect': 4.1.4 - '@vitest/mocker': 4.1.4(vite@7.3.1(@types/node@25.6.0)(jiti@2.6.1)(terser@5.39.0)) - '@vitest/pretty-format': 4.1.4 - '@vitest/runner': 4.1.4 - '@vitest/snapshot': 4.1.4 - '@vitest/spy': 4.1.4 - '@vitest/utils': 4.1.4 + '@vitest/expect': 4.1.5 + '@vitest/mocker': 4.1.5(vite@7.3.1(@types/node@25.6.0)(jiti@2.6.1)(terser@5.39.0)) + '@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 @@ -2866,7 +2866,7 @@ snapshots: why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 25.6.0 - '@vitest/coverage-v8': 4.1.4(vitest@4.1.4) + '@vitest/coverage-v8': 4.1.5(vitest@4.1.5) transitivePeerDependencies: - msw diff --git a/src/base-service.ts b/src/base-service.ts index 861c102..2a2d1b1 100644 --- a/src/base-service.ts +++ b/src/base-service.ts @@ -93,15 +93,19 @@ export class BaseService extends Hookified { data: any, config?: FetchRequestInit, ): Promise> { - const response = await this._net.post(url, data, config); - return { - data: response.data, - status: response.response.status, - statusText: response.response.statusText, - headers: response.response.headers, - config: config as any, - request: undefined, - }; + try { + const response = await this._net.post(url, data, config); + return { + data: response.data, + status: response.response.status, + statusText: response.response.statusText, + headers: response.response.headers, + config: config as any, + request: undefined, + }; + } catch (error) { + throw await this._enrichFetchError("POST", url, data, config, error); + } } public async put( @@ -109,15 +113,19 @@ export class BaseService extends Hookified { data: any, config?: FetchRequestInit, ): Promise> { - const response = await this._net.put(url, data, config); - return { - data: response.data, - status: response.response.status, - statusText: response.response.statusText, - headers: response.response.headers, - config: config as any, - request: undefined, - }; + try { + const response = await this._net.put(url, data, config); + return { + data: response.data, + status: response.response.status, + statusText: response.response.statusText, + headers: response.response.headers, + config: config as any, + request: undefined, + }; + } catch (error) { + throw await this._enrichFetchError("PUT", url, data, config, error); + } } public async delete( @@ -146,33 +154,43 @@ export class BaseService extends Hookified { } } - // Use fetch directly for DELETE to handle 204 status codes properly - const response = await this._net.fetch(url, { - ...restConfig, - headers, - body, - method: "DELETE", - }); + try { + // Use fetch directly for DELETE to handle 204 status codes properly + const response = await this._net.fetch(url, { + ...restConfig, + headers, + body, + method: "DELETE", + }); - let data: T | undefined; - if (response.status !== 204) { - const text = await response.text(); - /* v8 ignore next -- @preserve */ - try { - data = text ? JSON.parse(text) : undefined; - } catch { - data = text as any; + let data: T | undefined; + if (response.status !== 204) { + const text = await response.text(); + /* v8 ignore next -- @preserve */ + try { + data = text ? JSON.parse(text) : undefined; + } catch { + data = text as any; + } } - } - return { - data: data as T, - status: response.status, - statusText: response.statusText, - headers: response.headers, - config: config as any, - request: undefined, - }; + return { + data: data as T, + status: response.status, + statusText: response.statusText, + headers: response.headers, + config: config as any, + request: undefined, + }; + } catch (error) { + throw await this._enrichFetchError( + "DELETE", + url, + configData, + config, + error, + ); + } } public async patch( @@ -180,15 +198,81 @@ export class BaseService extends Hookified { data: any, config?: FetchRequestInit, ): Promise> { - const response = await this._net.patch(url, data, config); - return { - data: response.data, - status: response.response.status, - statusText: response.response.statusText, - headers: response.response.headers, - config: config as any, - request: undefined, - }; + try { + const response = await this._net.patch(url, data, config); + return { + data: response.data, + status: response.response.status, + statusText: response.response.statusText, + headers: response.response.headers, + config: config as any, + request: undefined, + }; + } catch (error) { + throw await this._enrichFetchError("PATCH", url, data, config, error); + } + } + + /* v8 ignore start -- @preserve */ + private _serializeBody( + data: any, + headers: Record, + ): BodyInit | undefined { + if (data === undefined || data === null) { + return undefined; + } + if ( + typeof data === "string" || + data instanceof FormData || + data instanceof URLSearchParams || + data instanceof Blob + ) { + return data; + } + if (!headers["Content-Type"] && !headers["content-type"]) { + headers["content-type"] = "application/json"; + } + return JSON.stringify(data); + } + /* v8 ignore stop -- @preserve */ + + private async _enrichFetchError( + method: string, + url: string, + data: any, + config: FetchRequestInit | undefined, + originalError: unknown, + ): Promise { + // `@cacheable/net` always throws `Error` instances; this cast keeps + // the helper focused on the diagnostic flow. + const error = originalError as Error; + // `@cacheable/net` discards the response body when a request returns a + // non-2xx status, leaving only "Fetch failed with status N". Re-issue + // the request with native fetch so the body is available for diagnostics. + const match = error.message.match(/Fetch failed with status (\d+)/); + /* v8 ignore next 3 -- @preserve */ + if (!match) { + return error; + } + try { + const headers: Record = { + ...(config?.headers as Record | undefined), + }; + const body = this._serializeBody(data, headers); + const res = await fetch(url, { method, headers, body }); + /* v8 ignore start -- @preserve */ + if (res.ok) { + // The retry unexpectedly succeeded — surface the original error. + return error; + } + /* v8 ignore stop -- @preserve */ + const text = await res.text(); + return new Error(`Fetch failed with status ${res.status}: ${text}`); + /* v8 ignore start -- @preserve */ + } catch { + return error; + } + /* v8 ignore stop -- @preserve */ } public createHeaders(apiKey?: string): Record { diff --git a/src/link.ts b/src/link.ts index a923f20..268cbcc 100644 --- a/src/link.ts +++ b/src/link.ts @@ -494,21 +494,41 @@ export class Link extends BaseService { const url = this.getUri(this._organizationId, code, "qrs"); const headers = this.createHeaders(this._apiKey); - - // biome-ignore lint/suspicious/noExplicitAny: this is valid for body - const body: Record = { - title: options?.title, - backgroundColor: options?.backgroundColor, - color: options?.color, - size: options?.size, - logo: options?.logo, - }; - - const response = await this.post(url, body, { headers }); + // The QR endpoint requires multipart/form-data; let fetch set the + // Content-Type with the correct boundary by removing the JSON default. + delete headers["content-type"]; + + // Always send backgroundColor/color/size (their documented defaults + // when not overridden) — the multipart parser rejects an empty body + // with "body must be object". title and logo remain truly optional. + const body = new FormData(); + if (options?.title !== undefined) { + body.append("title", options.title); + } + body.append("backgroundColor", options?.backgroundColor ?? "#ffffff"); + body.append("color", options?.color ?? "#000000"); + body.append("size", options?.size ?? QrSize.MEDIUM); + /* v8 ignore next 4 -- @preserve */ + if (options?.logo !== undefined) { + const logoBytes = Buffer.from(options.logo, "base64"); + body.append("logo", new Blob([new Uint8Array(logoBytes)]), "logo"); + } + + // `@cacheable/net@2.0.7` coerces `FormData` to its `toString()` value + // before sending, so we use native fetch directly for this multipart + // request. + const rawResponse = await fetch(url, { method: "POST", headers, body }); + const text = await rawResponse.text(); + /* v8 ignore next 5 -- @preserve */ + if (!rawResponse.ok) { + throw new Error( + `Fetch failed with status ${rawResponse.status}: ${text}`, + ); + } /* v8 ignore next -- @preserve */ - if (response.status === 201) { - const result = response.data as CreateQrCodeResponse; + if (rawResponse.status === 201) { + const result = JSON.parse(text) as CreateQrCodeResponse; if (result.qrCode) { const buffer = Buffer.from(result.qrCode, "base64"); @@ -519,7 +539,7 @@ export class Link extends BaseService { } /* v8 ignore next -- @preserve */ - throw new Error(`Failed to create QR code: ${response.statusText}`); + throw new Error(`Failed to create QR code: ${rawResponse.statusText}`); } /** diff --git a/test/base-service.test.ts b/test/base-service.test.ts index 75f30bb..0e33372 100644 --- a/test/base-service.test.ts +++ b/test/base-service.test.ts @@ -142,4 +142,52 @@ describe("BaseService", () => { expect(response.data).toBe("This is plain text, not JSON"); expect(typeof response.data).toBe("string"); }); + + test( + "post should include response body in error message on 4xx", + async () => { + const service = new BaseService(); + const url = `${mockHttpUrl}/status/400`; + await expect(service.post(url, { foo: "bar" })).rejects.toThrow( + /Fetch failed with status 400/, + ); + }, + testTimeout, + ); + + test( + "put should include response body in error message on 4xx", + async () => { + const service = new BaseService(); + const url = `${mockHttpUrl}/status/400`; + await expect(service.put(url, { foo: "bar" })).rejects.toThrow( + /Fetch failed with status 400/, + ); + }, + testTimeout, + ); + + test( + "patch should include response body in error message on 4xx", + async () => { + const service = new BaseService(); + const url = `${mockHttpUrl}/status/400`; + await expect(service.patch(url, { foo: "bar" })).rejects.toThrow( + /Fetch failed with status 400/, + ); + }, + testTimeout, + ); + + test( + "delete should include response body in error message on 4xx", + async () => { + const service = new BaseService(); + const url = `${mockHttpUrl}/status/400`; + await expect( + service.delete(url, { data: { foo: "bar" } }), + ).rejects.toThrow(/Fetch failed with status 400/); + }, + testTimeout, + ); });