diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..d643319 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,28 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + + - package-ecosystem: npm + directories: + - / + - /docs + - /docs/typedoc + schedule: + interval: weekly + # Gives new releases a few days to be vetted before they're proposed. + cooldown: + default-days: 3 + # Upgrade the Vite+ toolchain with `vp migrate`, which re-pins these together. + ignore: + - dependency-name: vite-plus + - dependency-name: "@voidzero-dev/vite-plus-core" + - dependency-name: vitest + - dependency-name: "@vitest/*" + groups: + dependencies: + update-types: + - minor + - patch diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4ad720c..a1152df 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,6 +2,12 @@ name: CI on: push: + branches: + - main + - "release/npm/**" + tags: + - "v*" + pull_request: workflow_call: workflow_dispatch: inputs: @@ -10,8 +16,11 @@ on: required: true type: string +permissions: + contents: read + jobs: - lint: + build: runs-on: ubuntu-24.04 outputs: version: ${{ steps.version.outputs.version }} @@ -20,9 +29,13 @@ jobs: uses: actions/checkout@v7 with: fetch-depth: 0 + persist-credentials: false - name: Build Reason - run: "echo ref: ${{github.ref}} event: ${{github.event_name}}" + env: + GIT_REF: ${{ github.ref }} + EVENT_NAME: ${{ github.event_name }} + run: 'echo "ref: $GIT_REF event: $EVENT_NAME"' - name: Build Version id: version @@ -38,88 +51,110 @@ 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 + if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$ ]]; then + echo "::error::'$version' is not a valid semver version." + exit 1 + fi + 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.22.0 with: - deno-version: v2.x + node-version-file: .node-version + cache: true + + - name: Check + run: vp check - - name: Code Style - run: deno fmt --check + - name: Test + run: vp test --project unit --coverage + + - name: Set Package Version + env: + VERSION: ${{ steps.version.outputs.version }} + run: vp pm version "$VERSION" -- --no-git-tag-version --no-git-checks + + - name: Pack + run: | + vp pack + vp pm pack --out "$RUNNER_TEMP/fetchclient.tgz" - - name: Linting - run: deno lint + - name: Show Package + run: tar -xOzf "$RUNNER_TEMP/fetchclient.tgz" package/package.json - - name: Type Check - run: deno task check + - name: Upload Package + uses: actions/upload-artifact@v7 + with: + name: npm-package + path: ${{ runner.temp }}/fetchclient.tgz + if-no-files-found: error - deno: + # Calls real third-party APIs, so it reports problems without gating publishing. + integration: runs-on: ubuntu-24.04 - needs: lint - permissions: - contents: read - id-token: write steps: - name: Checkout uses: actions/checkout@v7 with: - fetch-depth: 0 + persist-credentials: false - - name: Setup Deno - uses: denoland/setup-deno@v2 + - name: Setup Vite+ + uses: voidzero-dev/setup-vp@v1.22.0 with: - deno-version: v2.x + node-version-file: .node-version + cache: true - - name: Tests - run: | - deno test --allow-net --coverage=cov/ src/tests - deno coverage cov/ - - - name: Publish Release Package - if: startsWith(github.ref, 'refs/tags/v') - run: deno publish --set-version ${{ needs.lint.outputs.version }} + - name: Integration Tests + run: vp test --project integration - npm: + docs: runs-on: ubuntu-24.04 - needs: lint - permissions: - contents: read - packages: write - id-token: write steps: - name: Checkout uses: actions/checkout@v7 with: - fetch-depth: 0 + persist-credentials: false - - uses: denoland/setup-deno@v2 + - name: Setup Vite+ + uses: voidzero-dev/setup-vp@v1.22.0 with: - deno-version: v2.x + node-version-file: .node-version + cache: true + + - name: Build Documentation + run: vp run docs:build - - name: Build NPM Package - run: deno task build --set-version ${{ needs.lint.outputs.version }} + publish: + runs-on: ubuntu-24.04 + 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: + # No checkout here: npm refuses to run inside the repo because devEngines pins pnpm. + - name: Download Package + uses: actions/download-artifact@v8 + with: + name: npm-package - - name: Add NPM Package Metadata + - name: Resolve Dist Tag + id: dist-tag env: - REPOSITORY_URL: https://github.com/${{ github.repository }} + VERSION: ${{ needs.build.outputs.version }} + IS_RELEASE: ${{ startsWith(github.ref, 'refs/tags/v') || startsWith(github.ref, 'refs/heads/release/npm/') || github.event_name == 'workflow_dispatch' }} 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 + if [ "$IS_RELEASE" = "true" ] && [[ "$VERSION" != *-* ]]; then + tag=latest + else + tag=next + fi + echo "tag=$tag" >> "$GITHUB_OUTPUT" + echo "### Dist tag: $tag" >> "$GITHUB_STEP_SUMMARY" + + - name: Setup npm Registry uses: actions/setup-node@v7 with: node-version: "24.x" @@ -128,10 +163,11 @@ jobs: - name: Publish Release Package 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 + env: + DIST_TAG: ${{ steps.dist-tag.outputs.tag }} + run: npm publish fetchclient.tgz --provenance --access public --tag "$DIST_TAG" - - 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,13 +175,14 @@ 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' + env: + VERSION: ${{ needs.build.outputs.version }} + DIST_TAG: ${{ steps.dist-tag.outputs.tag }} + NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - package="@foundatiofx/fetchclient@${{ needs.lint.outputs.version }}" + package="@foundatiofx/fetchclient@$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 - npm publish fetchclient.tgz --tag ${{ (startsWith(github.ref, 'refs/tags/v') || startsWith(github.ref, 'refs/heads/release/npm/') || github.event_name == 'workflow_dispatch') && 'latest' || 'next' }} --access public + npm publish fetchclient.tgz --tag "$DIST_TAG" --access public fi - env: - NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index f0d8da6..483bb20 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.22.0 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..9406ee1 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -5,9 +5,17 @@ on: branches: - main +permissions: + contents: read + +# Let an in-progress deployment finish instead of cancelling it. +concurrency: + group: pages + cancel-in-progress: false + jobs: deploy-docs: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: contents: read pages: write @@ -21,20 +29,16 @@ jobs: - name: Checkout uses: actions/checkout@v7 with: - fetch-depth: 0 + persist-credentials: false - - name: Setup Node.js - uses: actions/setup-node@v7 + - name: Setup Vite+ + uses: voidzero-dev/setup-vp@v1.22.0 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..5593da3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,17 @@ -.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 +.vitest/ 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..86e0fc5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,23 @@ { - "deno.enable": true, - "deno.lint": true, - "deno.cacheOnSave": true, + "npm.scriptRunner": "vp", + "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..e0dff5d 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,18 @@ 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` (all), `vp test --project unit` (offline; gates CI and + publishing), `vp test --project integration` (hits real APIs in + `tests/integration/`) +- 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` +- Upgrade Vite+: `vp migrate` (re-pins `vite-plus`, its `vite` core alias, and + the `vitest`/`@vitest/*` catalog entries together; Dependabot skips these) ## 5) Common tasks (examples) @@ -74,8 +81,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 +111,36 @@ 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. + +## Running Scripts + +Run TypeScript and JavaScript files with `vpx `, for example `vpx ./scripts/seed.ts --dry-run`. It handles enums, decorators, JSX, and tsconfig `paths`, so do not add `tsx` or `ts-node` to run scripts. `vpx --watch ` reruns the file when a file it uses changes, in place of `tsx watch`. `vp node ` runs plain Node.js. + +## 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