From 8cb2fd55297d7caf28a87bc25f2f584870ba3d1e Mon Sep 17 00:00:00 2001 From: "Eric J. Smith" Date: Fri, 9 Oct 2026 17:08:23 -0500 Subject: [PATCH 01/11] Migrate from Deno to Vite+ Replace the Deno toolchain with Vite+ (vp): Oxfmt and Oxlint with type-aware checks via vp check, Vitest via vp test, and tsdown via vp pack. Use a pnpm workspace that includes the docs site. - Move entry points to src/index.ts and src/mocks/index.ts, and tests to tests/ - Convert tests from Deno.test and @std/assert to Vitest - Generate package exports with vp pack and validate them with publint and attw - Build and test with setup-vp in CI, then publish the packed tarball to npm (with provenance) and GitHub Packages from a separate job - Stop publishing to JSR - Add VS Code, pre-commit hook, and Dependabot config Co-Authored-By: Claude Opus 5.5 --- .github/dependabot.yml | 6 + .github/workflows/ci.yml | 105 +- .github/workflows/copilot-setup-steps.yml | 7 +- .github/workflows/docs.yml | 14 +- .gitignore | 18 +- .node-version | 1 + .vite-hooks/pre-commit | 1 + .vscode/extensions.json | 3 + .vscode/launch.json | 36 - .vscode/settings.json | 23 +- .vscode/tasks.json | 57 - AGENTS.md | 60 +- deno.json | 37 - deno.lock | 54 - docs/.vitepress/config.ts | 171 +- docs/guide/caching.md | 22 +- docs/guide/circuit-breaker.md | 10 +- docs/guide/configuration.md | 28 +- docs/guide/contributing.md | 29 +- docs/guide/getting-started.md | 32 +- docs/guide/middleware.md | 22 +- docs/guide/rate-limiting.md | 16 +- docs/guide/testing.md | 12 +- docs/guide/usage-examples.md | 35 +- docs/package-lock.json | 4550 ----------------- docs/package.json | 4 +- package.json | 64 + pnpm-lock.yaml | 4660 ++++++++++++++++++ pnpm-workspace.yaml | 14 + readme.md | 4 +- src/CircuitBreaker.ts | 37 +- src/CircuitBreakerMiddleware.ts | 3 +- src/DefaultHelpers.ts | 22 +- src/FetchClient.ts | 165 +- src/FetchClientCache.ts | 11 +- src/FetchClientError.ts | 19 +- src/FetchClientProvider.ts | 41 +- src/LinkHeader.ts | 22 +- src/RateLimitMiddleware.ts | 28 +- src/RateLimiter.ts | 57 +- src/ResponsePromise.ts | 13 +- src/RetryMiddleware.ts | 50 +- mod.ts => src/index.ts | 62 +- src/mocks/MockRegistry.ts | 34 +- src/mocks/MockResponseBuilder.ts | 14 +- src/mocks/{mod.ts => index.ts} | 0 src/mocks/types.ts | 2 +- src/tests/Caching.test.ts | 327 -- src/tests/ErrorHandling.test.ts | 353 -- src/tests/Integration.test.ts | 341 -- tests/Caching.test.ts | 297 ++ {src/tests => tests}/CircuitBreaker.test.ts | 213 +- tests/ErrorHandling.test.ts | 318 ++ {src/tests => tests}/HttpMethods.test.ts | 384 +- tests/Integration.test.ts | 292 ++ {src/tests => tests}/JsonParsing.test.ts | 150 +- {src/tests => tests}/Middleware.test.ts | 126 +- {src/tests => tests}/MockRegistry.test.ts | 211 +- {src/tests => tests}/Provider.test.ts | 175 +- {src/tests => tests}/RateLimit.test.ts | 254 +- {src/tests => tests}/RetryMiddleware.test.ts | 191 +- {src/tests => tests}/TimeoutAbort.test.ts | 38 +- {src/tests => tests}/UrlBuilding.test.ts | 54 +- tsconfig.json | 19 + vite.config.ts | 39 + 65 files changed, 7018 insertions(+), 7439 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .node-version create mode 100755 .vite-hooks/pre-commit create mode 100644 .vscode/extensions.json delete mode 100644 .vscode/launch.json delete mode 100644 .vscode/tasks.json delete mode 100644 deno.json delete mode 100644 deno.lock delete mode 100644 docs/package-lock.json create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml rename mod.ts => src/index.ts (76%) rename src/mocks/{mod.ts => index.ts} (100%) delete mode 100644 src/tests/Caching.test.ts delete mode 100644 src/tests/ErrorHandling.test.ts delete mode 100644 src/tests/Integration.test.ts create mode 100644 tests/Caching.test.ts rename {src/tests => tests}/CircuitBreaker.test.ts (67%) create mode 100644 tests/ErrorHandling.test.ts rename {src/tests => tests}/HttpMethods.test.ts (55%) create mode 100644 tests/Integration.test.ts rename {src/tests => tests}/JsonParsing.test.ts (52%) rename {src/tests => tests}/Middleware.test.ts (62%) rename {src/tests => tests}/MockRegistry.test.ts (62%) rename {src/tests => tests}/Provider.test.ts (71%) rename {src/tests => tests}/RateLimit.test.ts (58%) rename {src/tests => tests}/RetryMiddleware.test.ts (70%) rename {src/tests => tests}/TimeoutAbort.test.ts (65%) rename {src/tests => tests}/UrlBuilding.test.ts (70%) create mode 100644 tsconfig.json create mode 100644 vite.config.ts diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..ca79ca5 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4ad720c..1af5c97 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ on: type: string jobs: - lint: + build: runs-on: ubuntu-24.04 outputs: version: ${{ steps.version.outputs.version }} @@ -38,88 +38,55 @@ jobs: dotnet tool install --global minver-cli --version 7.0.0 version=$(minver --tag-prefix v) fi - echo "version=$version" >> $GITHUB_OUTPUT - echo "### Version: $version" >> $GITHUB_STEP_SUMMARY + echo "version=$version" >> "$GITHUB_OUTPUT" + echo "### Version: $version" >> "$GITHUB_STEP_SUMMARY" - - name: Setup Deno - uses: denoland/setup-deno@v2 + - name: Setup Vite+ + uses: voidzero-dev/setup-vp@v1.21.1 with: - deno-version: v2.x + node-version-file: .node-version + cache: true - - name: Code Style - run: deno fmt --check + - name: Check + run: vp check - - name: Linting - run: deno lint + - name: Test + run: vp test --coverage - - name: Type Check - run: deno task check + - name: Set Package Version + run: vp pm version "${{ steps.version.outputs.version }}" -- --no-git-tag-version --no-git-checks - deno: - runs-on: ubuntu-24.04 - needs: lint - permissions: - contents: read - id-token: write - steps: - - name: Checkout - uses: actions/checkout@v7 - with: - fetch-depth: 0 - - - name: Setup Deno - uses: denoland/setup-deno@v2 - with: - deno-version: v2.x - - - name: Tests + - name: Pack run: | - deno test --allow-net --coverage=cov/ src/tests - deno coverage cov/ + vp pack + vp pm pack --out "$RUNNER_TEMP/fetchclient.tgz" - - name: Publish Release Package - if: startsWith(github.ref, 'refs/tags/v') - run: deno publish --set-version ${{ needs.lint.outputs.version }} + - name: Show Package + run: tar -xOzf "$RUNNER_TEMP/fetchclient.tgz" package/package.json + + - name: Upload Package + uses: actions/upload-artifact@v7 + with: + name: npm-package + path: ${{ runner.temp }}/fetchclient.tgz + if-no-files-found: error - npm: + publish: runs-on: ubuntu-24.04 - needs: lint + needs: build + if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || startsWith(github.ref, 'refs/heads/release/npm/') || github.event_name == 'workflow_dispatch' permissions: contents: read packages: write id-token: write steps: - - name: Checkout - uses: actions/checkout@v7 + # No checkout here: npm refuses to run inside the repo because devEngines pins pnpm. + - name: Download Package + uses: actions/download-artifact@v8 with: - fetch-depth: 0 + name: npm-package - - uses: denoland/setup-deno@v2 - with: - deno-version: v2.x - - - name: Build NPM Package - run: deno task build --set-version ${{ needs.lint.outputs.version }} - - - name: Add NPM Package Metadata - env: - REPOSITORY_URL: https://github.com/${{ github.repository }} - run: | - package_dir=$(mktemp -d) - tar -xzf fetchclient.tgz -C "$package_dir" - PACKAGE_JSON="$package_dir/package/package.json" deno eval ' - const path = Deno.env.get("PACKAGE_JSON"); - const repositoryUrl = Deno.env.get("REPOSITORY_URL"); - const packageJson = JSON.parse(await Deno.readTextFile(path)); - packageJson.repository = { type: "git", url: repositoryUrl }; - await Deno.writeTextFile(path, `${JSON.stringify(packageJson, null, 2)}\n`); - ' - tar -czf fetchclient.tgz -C "$package_dir" package - - - name: Show NPM Package - run: tar -xOf fetchclient.tgz package/package.json - - - name: Setup Node.js environment + - name: Setup npm Registry uses: actions/setup-node@v7 with: node-version: "24.x" @@ -130,8 +97,7 @@ jobs: if: startsWith(github.ref, 'refs/tags/v') || startsWith(github.ref, 'refs/heads/release/npm/') || github.event_name == 'workflow_dispatch' run: npm publish fetchclient.tgz --provenance --access public - - name: Setup GitHub CI Node.js environment - if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || startsWith(github.ref, 'refs/heads/release/npm/') || github.event_name == 'workflow_dispatch' + - name: Setup GitHub Packages Registry uses: actions/setup-node@v7 with: node-version: "24.x" @@ -139,9 +105,8 @@ jobs: scope: "@foundatiofx" - name: Push GitHub CI Packages - if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || startsWith(github.ref, 'refs/heads/release/npm/') || github.event_name == 'workflow_dispatch' run: | - package="@foundatiofx/fetchclient@${{ needs.lint.outputs.version }}" + package="@foundatiofx/fetchclient@${{ needs.build.outputs.version }}" if npm view "$package" version --registry https://npm.pkg.github.com > /dev/null 2>&1; then echo "$package already exists in GitHub Packages; skipping publish." else diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index f0d8da6..bceb36f 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -20,7 +20,8 @@ jobs: - name: Checkout code uses: actions/checkout@v7 - - name: Setup Deno - uses: denoland/setup-deno@v2 + - name: Setup Vite+ + uses: voidzero-dev/setup-vp@v1.21.1 with: - deno-version: v2.x + node-version-file: .node-version + cache: true diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index ae6716f..91a053f 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -23,18 +23,14 @@ jobs: with: fetch-depth: 0 - - name: Setup Node.js - uses: actions/setup-node@v7 + - name: Setup Vite+ + uses: voidzero-dev/setup-vp@v1.21.1 with: - node-version: 24 - - - name: Install dependencies - run: npm ci --force - working-directory: docs + node-version-file: .node-version + cache: true - name: Build documentation - run: npm run build - working-directory: docs + run: vp run docs:build env: NODE_ENV: production diff --git a/.gitignore b/.gitignore index c7d8b97..e87591f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,16 @@ -.DS_Store -/npm +node_modules +dist +coverage +*.log *.tgz -*.lcov -/cov +.DS_Store +!.vscode/ +!.vscode/settings.json +!.vscode/extensions.json + +# dotenv environment variable files +.env +.env.* +!.env.example -nul .claude/settings.local.json diff --git a/.node-version b/.node-version new file mode 100644 index 0000000..a45fd52 --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +24 diff --git a/.vite-hooks/pre-commit b/.vite-hooks/pre-commit new file mode 100755 index 0000000..85fb65b --- /dev/null +++ b/.vite-hooks/pre-commit @@ -0,0 +1 @@ +vp staged diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..46cef3b --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["VoidZero.vite-plus-extension-pack"] +} diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index e23a68c..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "request": "launch", - "name": "Debug All Tests", - "type": "node", - "cwd": "${workspaceFolder}", - "env": {}, - "runtimeExecutable": "deno", - "runtimeArgs": [ - "test", - "--inspect-wait", - "--allow-all" - ], - "attachSimplePort": 9229 - }, - { - "request": "launch", - "name": "Debug Test", - "type": "node", - "program": "src/FetchClient.test.ts", - "cwd": "${workspaceFolder}", - "env": {}, - "runtimeExecutable": "deno", - "runtimeArgs": [ - "test", - "--inspect-wait", - "--allow-all", - "--filter", - "can use per-domain rate limiting with auto-update from headers" - ], - "attachSimplePort": 9229 - } - ] -} diff --git a/.vscode/settings.json b/.vscode/settings.json index e7ff59a..9248496 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,22 @@ { - "deno.enable": true, - "deno.lint": true, - "deno.cacheOnSave": true, + "editor.defaultFormatter": "oxc.oxc-vscode", + "[javascript]": { + "editor.defaultFormatter": "oxc.oxc-vscode" + }, + "[javascriptreact]": { + "editor.defaultFormatter": "oxc.oxc-vscode" + }, + "[typescript]": { + "editor.defaultFormatter": "oxc.oxc-vscode" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "oxc.oxc-vscode" + }, + "oxc.disableNestedConfig": true, + "oxc.fmt.disableNestedConfig": true, "editor.formatOnSave": true, - "editor.defaultFormatter": "denoland.vscode-deno" + "editor.formatOnSaveMode": "file", + "editor.codeActionsOnSave": { + "source.fixAll.oxc": "explicit" + } } diff --git a/.vscode/tasks.json b/.vscode/tasks.json deleted file mode 100644 index e81dfc9..0000000 --- a/.vscode/tasks.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "version": "2.0.0", - "tasks": [ - { - "type": "deno", - "command": "lint", - "problemMatcher": [ - "$deno-lint" - ], - "group": "test", - "label": "deno: lint" - }, - { - "type": "deno", - "command": "task", - "args": [ - "check" - ], - "problemMatcher": [ - "$deno" - ], - "group": { - "kind": "build", - "isDefault": true - }, - "label": "deno: check", - "detail": "$ deno check scripts/*.ts *.ts src/*.ts" - }, - { - "type": "deno", - "command": "task", - "args": [ - "build" - ], - "problemMatcher": [ - "$deno" - ], - "label": "deno: build", - "detail": "$ deno task build" - }, - { - "type": "deno", - "command": "test", - "args": [ - "--allow-net" - ], - "problemMatcher": [ - "$deno-test" - ], - "group": { - "kind": "test", - "isDefault": true - }, - "label": "deno: test" - } - ] -} diff --git a/AGENTS.md b/AGENTS.md index 43dac6d..300be5a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,7 +5,8 @@ concrete, and specific to FetchClient. ## 1) What this project is -- Deno-first, multi-runtime JSON fetch client with: +- Multi-runtime (Node, Deno, Bun, browsers) JSON fetch client published to npm, + with: - Typed responses (`FetchClientResponse`) - Middleware pipeline - Response caching @@ -14,7 +15,7 @@ concrete, and specific to FetchClient. ## 2) Codebase layout -- Public entry: `mod.ts` (re-exports from `src/*.ts`) +- Public entries: `src/index.ts` (`.`) and `src/mocks/index.ts` (`./mocks`) - Core client: `src/FetchClient.ts` - Provider & globals: `src/FetchClientProvider.ts`, `src/DefaultHelpers.ts` - Options & types: `src/RequestOptions.ts`, `src/FetchClientResponse.ts`, @@ -22,8 +23,9 @@ concrete, and specific to FetchClient. - Utilities: `src/FetchClientCache.ts`, `src/RateLimiter.ts`, `src/RateLimitMiddleware.ts`, `src/LinkHeader.ts`, `src/ObjectEvent.ts`, `src/Counter.ts` -- Tests: `src/*test.ts` -- Build tooling: tasks in `deno.json` +- Tests: `tests/*.test.ts` (Vitest via `vite-plus/test`) +- Tooling: Vite+ (`vp`), configured in `vite.config.ts`; pnpm workspace with + `docs/` (VitePress) ## 3) How it works (architecture) @@ -38,7 +40,7 @@ concrete, and specific to FetchClient. - Model validation: if `modelValidator` is set and body is object (non-FormData), validate before fetch; returning `ProblemDetails` short-circuits the request. -- Errors: unexpected non-2xx throws the Response (augmented); can be suppressed +- Errors: unexpected non-2xx throws `FetchClientError`; can be suppressed with `expectedStatusCodes`, `shouldThrowOnUnexpectedStatusCodes=false`, or `errorCallback` returning true. - Timeout/abort: merges `AbortSignal.timeout(options.timeout)` with any provided @@ -49,13 +51,14 @@ concrete, and specific to FetchClient. - Middleware order: `[provider.middleware, client.use(...), internal fetch]`. - Loading events: instance and provider expose `loading` via counters. -## 4) Dev workflows (Deno) +## 4) Dev workflows (Vite+) -- Run tests (net allowed): `deno task test` -- Type check: `deno task check` -- Lint/format: `deno task lint`, `deno task format-check` -- Build npm package: `deno task build --set-version 0.0.0` (uses `deno pack` and - emits `fetchclient.tgz`) +- Install: `vp install` +- Format, lint, and type check: `vp check` (`vp check --fix` to auto-fix) +- Run tests: `vp test` (integration tests hit real APIs) +- Build package: `vp pack` (emits `dist/`, regenerates `exports` in + `package.json`, and runs publint and attw) +- Docs: `vp run docs:dev`, `vp run docs:build` ## 5) Common tasks (examples) @@ -74,8 +77,9 @@ concrete, and specific to FetchClient. ## 6) Conventions & gotchas -- Thrown values are Response objects; catch and inspect `.status` and `.problem` - (not `Error`). +- Unexpected statuses throw `FetchClientError`; inspect `.status` and + `.response.problem`. Malformed JSON in a 2xx throws + `FetchClientDeserializationError`. - `meta.links` parsed from `Link` header (`next`/`previous` may be present). - Don’t break middleware order; provider middleware must run before client middleware. @@ -103,4 +107,32 @@ concrete, and specific to FetchClient. --- If something is unclear, prefer small, additive changes and tests in -`src/*test.ts`. Keep middleware order and error semantics consistent. +`tests/*.test.ts`. Keep middleware order and error semantics consistent. + + + +# Using Vite+, the Unified Toolchain for the Web + +This project is using Vite+, a unified toolchain built on top of Vite, Rolldown, Vitest, tsdown, Oxlint, Oxfmt, and Vite Task. Vite+ wraps runtime management, package management, and frontend tooling in a single global CLI called `vp`. Vite+ is distinct from Vite, and it invokes Vite through `vp dev` and `vp build`. Run `vp help` to print a list of commands and `vp --help` for information about a specific command. + +Docs are local at `node_modules/vite-plus/docs` or online at https://viteplus.dev/guide/. + +## Built-in Commands vs Scripts + +`vp ` runs a built-in command. `vp run ` runs a `package.json` script or a `vite.config.ts` task. Scripts cannot overwrite built-ins, so `vp dev` and `vp run dev` may do different things. Check `package.json` and `vite.config.ts` first, and run `vp run ` when the project defines a script or task with that name. + +## Tool Versions + +Run `vp toolchain` to show versions and relationships in the active Vite+ +release. Add a tool name to select part of the graph. For example, run +`vp toolchain vite`. Use `--global` to ignore the local `vite-plus` package. Use +`vp why ` to show the package-manager dependency graph. + +## Review Checklist + +- [ ] Run `vp install` after pulling remote changes and before getting started. +- [ ] Run `vp check` and `vp test` to format, lint, type check and test changes. +- [ ] Check if there are `vite.config.ts` tasks or `package.json` scripts necessary for validation, run via `vp run