diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5d374db..5303b34 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,38 +10,12 @@ on: branches: - main -jobs: - unit-tests: - name: Unit Tests - runs-on: ubuntu-latest - timeout-minutes: 5 - if: ${{ github.event.action == 'opened' || github.event.action == 'ready_for_review' || github.event.action == 'synchronize' || (github.event.action == 'labeled' && github.event.label.name == 'force ci') }} - - steps: - - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 - with: - egress-policy: block - allowed-endpoints: > - api.github.com:443 - github.com:443 - registry.npmjs.org:443 +permissions: + contents: read - - name: Git Checkout - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - - name: Setup Node - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: - node-version: lts/* - cache: 'npm' - - - name: Install dependencies - run: npm install - - - name: Run Tests - run: node --run test:unit - e2e-test: - name: E2E Tests +jobs: + tests: + name: Tests runs-on: ubuntu-latest timeout-minutes: 5 if: ${{ github.event.action == 'opened' || github.event.action == 'ready_for_review' || github.event.action == 'synchronize' || (github.event.action == 'labeled' && github.event.label.name == 'force ci') }} @@ -69,4 +43,4 @@ jobs: run: npm install - name: Run Tests - run: node --run test:e2e + run: node --run test diff --git a/COLLABORATOR_GUIDE.md b/COLLABORATOR_GUIDE.md index b25e305..0cf6445 100644 --- a/COLLABORATOR_GUIDE.md +++ b/COLLABORATOR_GUIDE.md @@ -62,32 +62,36 @@ The Worker also uses several other Open Source libraries (not limited to) listed ### Structure of this Repository -- `src/` - Worker -- `scripts/` - Multi-purpose scripts -- `tests/` - Tests for the worker +- `dev-bucket/` - A recreation of the contents in the R2 bucket that the worker reads from +- `docs/` - Documentation on things relating to the worker +- `e2e-tests/` - End-to-End tests for the worker +- `scripts/` - Miscellaneous scripts +- `src/` - Code for the worker ## Testing -Each new feature or bug fix should be accompanied by a unit or E2E test (when valuable). We use Node's test runner and Miniflare for our E2E tests. +Each new feature or bug fix should be accompanied by a unit and/or E2E test (when valuable). +We use [Vitest](https://vitest.dev/guide/) for both. ### Unit Testing -Unit Tests are fundamental to ensure that code changes do not disrupt the functionalities of the Worker: +Unit tests are important to ensure that individual parts of the Worker are acting as expected. -- Unit Tests should be written as `.test.ts` files in the [tests/unit/](./tests/unit/) directory. +- They should be written in the same directory as the file that is being tested. +- They should be named `.test.ts`. - They should cover utilities as well as any component that can be broken down and individually tested. -- We utilize Node's [Test Runner](https://nodejs.org/api/test.html) and [Assert](https://nodejs.org/api/assert.html) APIs. -- External services used in the worker should be mocked with Undici. If the service cannot be mocked with Undici, it should be mocked with a HTTP server via Node's `createServer` API. +- They should make use of [Vitest's APIs](https://vitest.dev/guide/). +- External services used in the worker should be mocked with Undici. ### End-to-End Testing -E2E Tests are fundamental to ensure that requests made to the worker behave as expected: +E2E tests are important to ensure that requests made to the worker as a whole behave as expected. -- E2E Tests should be written as `.test.ts` files in the [tests/e2e/](./tests/e2e/) directory. -- They should cover the various contexts of a request that could be sent to the worker from an external client. -- We utilize Node's [Test Runner](https://nodejs.org/api/test.html) and [Assert](https://nodejs.org/api/assert.html) APIs. -- A local version of the Worker is ran with Miniflare. -- Like Unit Tests, any external services should be mocked for these tests as well. +- They should be written as `.test.ts` files in the [e2e-tests/](./e2e-tests/) directory. +- They should cover the various contexts in which a request could be sent to the worker from an external client. +- We utilitize [Cloudflare's Vitest Integration](https://developers.cloudflare.com/workers/testing/vitest-integration/) to run a local version of the worker for these. +- The contents of the [dev-bucket/](./dev-bucket/) folder are read and put into an in-memory R2 bucket mock for these tests. Anything in that directory is available in a test. +- External services used in the worker should be mocked with Undici. ## Remarks on Structure and Background diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cc567bd..f15a400 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -83,12 +83,11 @@ The steps below will give you a general idea of how to prepare your local enviro git merge upstream/main ``` -9. Run the following to confirm that linting and formatting are passing. +9. Run the following to confirm that linting, formatting, and tests are passing. ```bash node --run format - node --run test:unit - node --run test:e2e + node --run test ``` 10. To run the worker locally, see [Dev Setup](./docs/dev-setup.md). @@ -115,8 +114,8 @@ This repository contains a few scripts and commands for performing numerous task - `node --run format` Formats the code to the repository's standards. - `node --run lint` Lints the code to the repository's standards. -- `node --run test:unit` Runs the [Unit Tests](./COLLABORATOR_GUIDE.md#unit-tests) to ensure individual components are working as expected. -- `node --run test:e2e` Runs the [E2E Tests](./COLLABORATOR_GUIDE.md#e2e-tests) to ensure requests act as expected. +- `node --run test` Run all tests (denoted by the `.test.ts` file extension) once +- `node --run test:watch` Run all tests (denoted by the `.test.ts` file extension) once, then again on any file changes. - `node --run build:mustache` Compiles the Mustache templates. **Required for any changes to the templates to take affect**. diff --git a/dev-bucket/README.md b/dev-bucket/README.md new file mode 100644 index 0000000..e0a16b3 --- /dev/null +++ b/dev-bucket/README.md @@ -0,0 +1,6 @@ +# dev-bucket + +This is a very slimmed down recreation of the `dist-prod` bucket meant for testing the Release Worker. +The files here have little to no content, they're here just to build the structure of the bucket itself so that the Release Worker can be used in development easier. + +For more information on the structure of the bucket, read [R2](../docs/r2.md) and [Release Process](../docs/release-process.md). diff --git a/dev-bucket/metrics/index.html b/dev-bucket/metrics/index.html new file mode 100644 index 0000000..7178822 --- /dev/null +++ b/dev-bucket/metrics/index.html @@ -0,0 +1,9 @@ + + + + Document + + +

metrics/index.html

+ + \ No newline at end of file diff --git a/dev-bucket/metrics/logs/nodejs.org-access.log.20241222.0000000000.csv b/dev-bucket/metrics/logs/nodejs.org-access.log.20241222.0000000000.csv new file mode 100644 index 0000000..e69de29 diff --git a/dev-bucket/nodejs/chakracore-nightly/index.json b/dev-bucket/nodejs/chakracore-nightly/index.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/dev-bucket/nodejs/chakracore-nightly/index.json @@ -0,0 +1 @@ +{} diff --git a/dev-bucket/nodejs/chakracore-nightly/v10.13.0-nightly2018112084bd6f3c82/SHASUMS256.txt b/dev-bucket/nodejs/chakracore-nightly/v10.13.0-nightly2018112084bd6f3c82/SHASUMS256.txt new file mode 100644 index 0000000..e69de29 diff --git a/dev-bucket/nodejs/chakracore-nightly/v10.13.0-nightly2018112084bd6f3c82/docs/api/index.html b/dev-bucket/nodejs/chakracore-nightly/v10.13.0-nightly2018112084bd6f3c82/docs/api/index.html new file mode 100644 index 0000000..56de0b2 --- /dev/null +++ b/dev-bucket/nodejs/chakracore-nightly/v10.13.0-nightly2018112084bd6f3c82/docs/api/index.html @@ -0,0 +1,9 @@ + + + + Document + + +

nodejs/chakracore-nightly/v10.13.0-nightly2018112084bd6f3c82/docs/api/index.html

+ + diff --git a/dev-bucket/nodejs/chakracore-nightly/v10.13.0-nightly2018112084bd6f3c82/win-arm64/.gitkeep b/dev-bucket/nodejs/chakracore-nightly/v10.13.0-nightly2018112084bd6f3c82/win-arm64/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/dev-bucket/nodejs/chakracore-nightly/v10.13.0-nightly2018112084bd6f3c82/win-x64/.gitkeep b/dev-bucket/nodejs/chakracore-nightly/v10.13.0-nightly2018112084bd6f3c82/win-x64/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/dev-bucket/nodejs/chakracore-nightly/v10.13.0-nightly2018112084bd6f3c82/win-x86/.gitkeep b/dev-bucket/nodejs/chakracore-nightly/v10.13.0-nightly2018112084bd6f3c82/win-x86/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/dev-bucket/nodejs/chakracore-rc/index.json b/dev-bucket/nodejs/chakracore-rc/index.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/dev-bucket/nodejs/chakracore-rc/index.json @@ -0,0 +1 @@ +{} diff --git a/dev-bucket/nodejs/chakracore-rc/v10.0.0-rc.0/SHASUMS256.txt b/dev-bucket/nodejs/chakracore-rc/v10.0.0-rc.0/SHASUMS256.txt new file mode 100644 index 0000000..e69de29 diff --git a/dev-bucket/nodejs/chakracore-rc/v10.0.0-rc.0/win-arm64/.gitkeep b/dev-bucket/nodejs/chakracore-rc/v10.0.0-rc.0/win-arm64/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/dev-bucket/nodejs/chakracore-rc/v10.0.0-rc.0/win-x64/.gitkeep b/dev-bucket/nodejs/chakracore-rc/v10.0.0-rc.0/win-x64/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/dev-bucket/nodejs/chakracore-rc/v10.0.0-rc.0/win-x86/.gitkeep b/dev-bucket/nodejs/chakracore-rc/v10.0.0-rc.0/win-x86/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/dev-bucket/nodejs/chakracore-release/index.json b/dev-bucket/nodejs/chakracore-release/index.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/dev-bucket/nodejs/chakracore-release/index.json @@ -0,0 +1 @@ +{} diff --git a/dev-bucket/nodejs/chakracore-release/v10.0.0/SHASUMS256.txt b/dev-bucket/nodejs/chakracore-release/v10.0.0/SHASUMS256.txt new file mode 100644 index 0000000..e69de29 diff --git a/dev-bucket/nodejs/chakracore-release/v10.0.0/docs/api/index.html b/dev-bucket/nodejs/chakracore-release/v10.0.0/docs/api/index.html new file mode 100644 index 0000000..428d363 --- /dev/null +++ b/dev-bucket/nodejs/chakracore-release/v10.0.0/docs/api/index.html @@ -0,0 +1,9 @@ + + + + Document + + +

nodejs/chakracore-release/v10.0.0/docs/api/index.html

+ + diff --git a/dev-bucket/nodejs/chakracore-release/v10.0.0/win-arm64/.gitkeep b/dev-bucket/nodejs/chakracore-release/v10.0.0/win-arm64/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/dev-bucket/nodejs/chakracore-release/v10.0.0/win-x64/.gitkeep b/dev-bucket/nodejs/chakracore-release/v10.0.0/win-x64/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/dev-bucket/nodejs/chakracore-release/v10.0.0/win-x86/.gitkeep b/dev-bucket/nodejs/chakracore-release/v10.0.0/win-x86/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/dev-bucket/nodejs/docs/v0.0.1/index.html b/dev-bucket/nodejs/docs/v0.0.1/index.html new file mode 100644 index 0000000..8b84318 --- /dev/null +++ b/dev-bucket/nodejs/docs/v0.0.1/index.html @@ -0,0 +1,9 @@ + + + + Document + + +

nodejs/docs/v0.0.1/index.html

+ + \ No newline at end of file diff --git a/dev-bucket/nodejs/nightly/index.json b/dev-bucket/nodejs/nightly/index.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/dev-bucket/nodejs/nightly/index.json @@ -0,0 +1 @@ +{} diff --git a/dev-bucket/nodejs/nightly/v24.0.0-nightly20241219756077867b/SHASUMS256.txt b/dev-bucket/nodejs/nightly/v24.0.0-nightly20241219756077867b/SHASUMS256.txt new file mode 100644 index 0000000..e69de29 diff --git a/dev-bucket/nodejs/nightly/v24.0.0-nightly20241219756077867b/docs/api/index.html b/dev-bucket/nodejs/nightly/v24.0.0-nightly20241219756077867b/docs/api/index.html new file mode 100644 index 0000000..61a2856 --- /dev/null +++ b/dev-bucket/nodejs/nightly/v24.0.0-nightly20241219756077867b/docs/api/index.html @@ -0,0 +1,9 @@ + + + + Document + + +

nodejs/nightly/v24.0.0-nightly20241219756077867b/docs/api/index.html

+ + diff --git a/dev-bucket/nodejs/nightly/v24.0.0-nightly20241219756077867b/docs/apilinks.json b/dev-bucket/nodejs/nightly/v24.0.0-nightly20241219756077867b/docs/apilinks.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/dev-bucket/nodejs/nightly/v24.0.0-nightly20241219756077867b/docs/apilinks.json @@ -0,0 +1 @@ +{} diff --git a/dev-bucket/nodejs/nightly/v24.0.0-nightly20241219756077867b/win-arm64/.gitkeep b/dev-bucket/nodejs/nightly/v24.0.0-nightly20241219756077867b/win-arm64/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/dev-bucket/nodejs/nightly/v24.0.0-nightly20241219756077867b/win-x64/.gitkeep b/dev-bucket/nodejs/nightly/v24.0.0-nightly20241219756077867b/win-x64/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/dev-bucket/nodejs/rc/index.json b/dev-bucket/nodejs/rc/index.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/dev-bucket/nodejs/rc/index.json @@ -0,0 +1 @@ +{} diff --git a/dev-bucket/nodejs/rc/v23.0.0-rc.3/SHASUMS256.txt b/dev-bucket/nodejs/rc/v23.0.0-rc.3/SHASUMS256.txt new file mode 100644 index 0000000..e69de29 diff --git a/dev-bucket/nodejs/rc/v23.0.0-rc.3/docs/api/index.html b/dev-bucket/nodejs/rc/v23.0.0-rc.3/docs/api/index.html new file mode 100644 index 0000000..80f1072 --- /dev/null +++ b/dev-bucket/nodejs/rc/v23.0.0-rc.3/docs/api/index.html @@ -0,0 +1,9 @@ + + + + Document + + +

nodejs/rc/v23.0.0-rc.3/docs/api/index.html

+ + diff --git a/dev-bucket/nodejs/rc/v23.0.0-rc.3/docs/apilinks.json b/dev-bucket/nodejs/rc/v23.0.0-rc.3/docs/apilinks.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/dev-bucket/nodejs/rc/v23.0.0-rc.3/docs/apilinks.json @@ -0,0 +1 @@ +{} diff --git a/dev-bucket/nodejs/rc/v23.0.0-rc.3/win-arm64/.gitkeep b/dev-bucket/nodejs/rc/v23.0.0-rc.3/win-arm64/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/dev-bucket/nodejs/rc/v23.0.0-rc.3/win-x64/.gitkeep b/dev-bucket/nodejs/rc/v23.0.0-rc.3/win-x64/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/dev-bucket/nodejs/release/index.json b/dev-bucket/nodejs/release/index.json new file mode 100644 index 0000000..138e088 --- /dev/null +++ b/dev-bucket/nodejs/release/index.json @@ -0,0 +1 @@ +{ "hello": true } diff --git a/dev-bucket/nodejs/release/v20.0.0/SHASUMS256.txt b/dev-bucket/nodejs/release/v20.0.0/SHASUMS256.txt new file mode 100644 index 0000000..e69de29 diff --git a/dev-bucket/nodejs/release/v20.0.0/docs/api/index.html b/dev-bucket/nodejs/release/v20.0.0/docs/api/index.html new file mode 100644 index 0000000..87922d5 --- /dev/null +++ b/dev-bucket/nodejs/release/v20.0.0/docs/api/index.html @@ -0,0 +1,9 @@ + + + + Document + + +

nodejs/release/v20.0.0/docs/api/index.html

+ + diff --git a/dev-bucket/nodejs/release/v20.0.0/docs/apilinks.json b/dev-bucket/nodejs/release/v20.0.0/docs/apilinks.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/dev-bucket/nodejs/release/v20.0.0/docs/apilinks.json @@ -0,0 +1 @@ +{} diff --git a/dev-bucket/nodejs/release/v20.0.0/win-arm64/.gitkeep b/dev-bucket/nodejs/release/v20.0.0/win-arm64/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/dev-bucket/nodejs/release/v20.0.0/win-x64/.gitkeep b/dev-bucket/nodejs/release/v20.0.0/win-x64/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/dev-bucket/nodejs/release/v20.0.0/win-x86/.gitkeep b/dev-bucket/nodejs/release/v20.0.0/win-x86/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/dev-bucket/nodejs/test/index.json b/dev-bucket/nodejs/test/index.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/dev-bucket/nodejs/test/index.json @@ -0,0 +1 @@ +{} diff --git a/dev-bucket/nodejs/test/v24.0.0-test6af5c4e2b40/SHASUMS256.txt b/dev-bucket/nodejs/test/v24.0.0-test6af5c4e2b40/SHASUMS256.txt new file mode 100644 index 0000000..e69de29 diff --git a/dev-bucket/nodejs/test/v24.0.0-test6af5c4e2b40/win-arm64/.gitkeep b/dev-bucket/nodejs/test/v24.0.0-test6af5c4e2b40/win-arm64/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/dev-bucket/nodejs/test/v24.0.0-test6af5c4e2b40/win-x64/.gitkeep b/dev-bucket/nodejs/test/v24.0.0-test6af5c4e2b40/win-x64/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/dev-bucket/nodejs/v8-canary/index.json b/dev-bucket/nodejs/v8-canary/index.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/dev-bucket/nodejs/v8-canary/index.json @@ -0,0 +1 @@ +{} diff --git a/dev-bucket/nodejs/v8-canary/v24.0.0-v8-canary202412221f947c1730/SHASUMS256.txt b/dev-bucket/nodejs/v8-canary/v24.0.0-v8-canary202412221f947c1730/SHASUMS256.txt new file mode 100644 index 0000000..e69de29 diff --git a/dev-bucket/nodejs/v8-canary/v24.0.0-v8-canary202412221f947c1730/docs/api/index.html b/dev-bucket/nodejs/v8-canary/v24.0.0-v8-canary202412221f947c1730/docs/api/index.html new file mode 100644 index 0000000..8a0b6b3 --- /dev/null +++ b/dev-bucket/nodejs/v8-canary/v24.0.0-v8-canary202412221f947c1730/docs/api/index.html @@ -0,0 +1,9 @@ + + + + Document + + +

nodejs/v8-canary/v24.0.0-v8-canary202412221f947c1730/docs/api/index.html

+ + diff --git a/dev-bucket/nodejs/v8-canary/v24.0.0-v8-canary202412221f947c1730/docs/apilinks.json b/dev-bucket/nodejs/v8-canary/v24.0.0-v8-canary202412221f947c1730/docs/apilinks.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/dev-bucket/nodejs/v8-canary/v24.0.0-v8-canary202412221f947c1730/docs/apilinks.json @@ -0,0 +1 @@ +{} diff --git a/e2e-tests/README.md b/e2e-tests/README.md new file mode 100644 index 0000000..93f32e6 --- /dev/null +++ b/e2e-tests/README.md @@ -0,0 +1,10 @@ +# e2e-tests + +End-to-End tests for the Release Worker. + +## Table of Contents + +- [file.test.ts](./file.test.ts) - Tests for requests specific to files. +- [directory.test.ts](./directory.test.ts) - Tests for requests specific to directories. +- [fallback.test.ts](./fallback.test.ts) - Tests for falling back to the origin server if needed. +- [misc.test.ts](./misc.test.ts) - Miscellaneous tests not pertaining specifically to files or directories. diff --git a/e2e-tests/directory.test.ts b/e2e-tests/directory.test.ts new file mode 100644 index 0000000..22eb76e --- /dev/null +++ b/e2e-tests/directory.test.ts @@ -0,0 +1,137 @@ +import { env, fetchMock, createExecutionContext } from 'cloudflare:test'; +import { test, beforeAll, afterEach, expect } from 'vitest'; +import { populateR2WithDevBucket } from './util'; +import worker from '../src/worker'; +import type { Env } from '../src/env'; +import { CACHE_HEADERS } from '../src/constants/cache'; + +const mockedEnv: Env = { + ...env, + ENVIRONMENT: 'e2e-tests', + CACHING: false, + LOG_ERRORS: true, + S3_ENDPOINT: 'https://s3.mock', + S3_ACCESS_KEY_ID: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + S3_ACCESS_KEY_SECRET: + 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', +}; + +const s3Url = new URL(mockedEnv.S3_ENDPOINT); +s3Url.host = `${mockedEnv.BUCKET_NAME}.${s3Url.host}`; + +const S3_DIRECTORY_RESULT = ` + + dist-prod + + + 1000 + false + + nodejs/release/v1.0.0/latest/ + + + "asd123" + nodejs/release/v1.0.0/index.json + 2023-09-12T05:43:00.000Z + 18 + +`; + +const S3_EMPTY_DIRECTORY_RESULT = ` + + dist-prod + + + 1000 + false +`; + +beforeAll(async () => { + fetchMock.activate(); + fetchMock.disableNetConnect(); + + await populateR2WithDevBucket(); +}); + +afterEach(() => { + fetchMock.assertNoPendingInterceptors(); +}); + +// These paths are cached and don't send requests to S3 +for (const path of ['/dist/', '/docs/']) { + test(`GET \`${path}\` returns 200`, async () => { + const ctx = createExecutionContext(); + + const res = await worker.fetch( + new Request(`https://localhost${path}`), + mockedEnv, + ctx + ); + + // Consume body promise + await res.text(); + + expect(res.status).toBe(200); + }); +} + +for (const path of ['/api/', '/download/', '/metrics/']) { + test(`GET \`${path}\` returns 200`, async () => { + fetchMock + .get(s3Url.origin) + .intercept({ + path: /.*/, + }) + .reply(200, S3_DIRECTORY_RESULT); + + const ctx = createExecutionContext(); + + const res = await worker.fetch( + new Request(`https://localhost${path}`), + mockedEnv, + ctx + ); + + // Consume body promise + await res.text(); + + expect(res.status).toBe(200); + }); +} + +test('GET `/dist/unknown-directory/` returns 404', async () => { + fetchMock + .get(s3Url.origin) + .intercept({ + path: /.*/, + query: { + prefix: 'nodejs/release/unknown-directory/', + }, + }) + .reply(200, S3_EMPTY_DIRECTORY_RESULT); + + const ctx = createExecutionContext(); + + const res = await worker.fetch( + new Request('https://localhost/dist/unknown-directory/'), + mockedEnv, + ctx + ); + + expect(res.status).toBe(404); + expect(res.headers.get('cache-control')).toStrictEqual(CACHE_HEADERS.failure); + expect(await res.text()).toStrictEqual('Directory not found'); +}); + +test('GET `/dist` redirects to `/dist/`', async () => { + const ctx = createExecutionContext(); + + const res = await worker.fetch( + new Request('https://localhost/dist'), + mockedEnv, + ctx + ); + + expect(res.status).toBe(301); + expect(res.headers.get('location')).toStrictEqual('https://localhost/dist/'); +}); diff --git a/e2e-tests/fallback.test.ts b/e2e-tests/fallback.test.ts new file mode 100644 index 0000000..a14290c --- /dev/null +++ b/e2e-tests/fallback.test.ts @@ -0,0 +1,106 @@ +import { env, fetchMock, createExecutionContext } from 'cloudflare:test'; +import { test, beforeAll, afterEach, expect, vi } from 'vitest'; +import { populateR2WithDevBucket } from './util'; +import worker from '../src/worker'; +import type { Env } from '../src/env'; +import { CACHE_HEADERS } from '../src/constants/cache'; + +const mockedEnv: Env = { + ...env, + ENVIRONMENT: 'e2e-tests', + CACHING: false, + LOG_ERRORS: false, + S3_ENDPOINT: 'https://s3.mock', + S3_ACCESS_KEY_ID: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + S3_ACCESS_KEY_SECRET: + 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + ORIGIN_HOST: 'https://origin.mock', +}; + +const s3Url = new URL(mockedEnv.S3_ENDPOINT); +s3Url.host = `${mockedEnv.BUCKET_NAME}.${s3Url.host}`; + +beforeAll(async () => { + fetchMock.activate(); + fetchMock.disableNetConnect(); + + await populateR2WithDevBucket(); +}); + +afterEach(() => { + fetchMock.assertNoPendingInterceptors(); +}); + +test('grabs file from fallback server if r2 request fails', async () => { + vi.spyOn(env.R2_BUCKET, 'get').mockImplementation(() => { + throw new TypeError('This should be thrown.'); + }); + + let originCalled = false; + const originResponse = '{ "asd": true }'; + fetchMock + .get(mockedEnv.ORIGIN_HOST) + .intercept({ + path: '/dist/index.json', + }) + .reply(() => { + originCalled = true; + + return { + statusCode: 200, + data: originResponse, + }; + }); + + const ctx = createExecutionContext(); + + const res = await worker.fetch( + new Request('https://localhost/dist/index.json'), + mockedEnv, + ctx + ); + + expect(res.status).toBe(200); + expect(res.headers.get('cache-control')).toStrictEqual(CACHE_HEADERS.success); + expect(originCalled).toBeTruthy(); + expect(await res.text()).toStrictEqual(originResponse); +}); + +test('grabs directory from fallback server if r2 request fails', async () => { + fetchMock + .get(s3Url.origin) + .intercept({ + path: /.*/, + }) + .reply(500, '') + .times(1); + + let originCalled = false; + const originResponse = ''; + fetchMock + .get(mockedEnv.ORIGIN_HOST) + .intercept({ + path: '/dist/v20.0.0/', + }) + .reply(() => { + originCalled = true; + + return { + statusCode: 200, + data: originResponse, + }; + }); + + const ctx = createExecutionContext(); + + const res = await worker.fetch( + new Request('https://localhost/dist/v20.0.0/'), + mockedEnv, + ctx + ); + + expect(res.status).toBe(200); + expect(res.headers.get('cache-control')).toStrictEqual(CACHE_HEADERS.failure); + expect(originCalled).toBeTruthy(); + expect(await res.text()).toStrictEqual(originResponse); +}); diff --git a/e2e-tests/file.test.ts b/e2e-tests/file.test.ts new file mode 100644 index 0000000..8d446a2 --- /dev/null +++ b/e2e-tests/file.test.ts @@ -0,0 +1,303 @@ +import { env, createExecutionContext } from 'cloudflare:test'; +import { test, beforeAll, expect } from 'vitest'; +import { populateR2WithDevBucket } from './util'; +import worker from '../src/worker'; +import type { Env } from '../src/env'; +import { CACHE_HEADERS } from '../src/constants/cache'; + +const mockedEnv: Env = { + ...env, + ENVIRONMENT: 'e2e-tests', + CACHING: false, + LOG_ERRORS: true, +}; + +beforeAll(async () => { + await populateR2WithDevBucket(); +}); + +test('GET `/dist/index.json` returns 200', async () => { + const ctx = createExecutionContext(); + + const res = await worker.fetch( + new Request('https://localhost/dist/index.json'), + mockedEnv, + ctx + ); + + // Consume the body promise + await res.text(); + + expect(res.status).toBe(200); + expect(res.headers.get('cache-control')).toStrictEqual(CACHE_HEADERS.success); +}); + +test('GET `/dist/asd123.json` returns 404', async () => { + const ctx = createExecutionContext(); + + const res = await worker.fetch( + new Request('https://localhost/dist/asd123.json'), + mockedEnv, + ctx + ); + + expect(res.status).toBe(404); + expect(res.headers.get('cache-control')).toStrictEqual(CACHE_HEADERS.failure); + expect(await res.text()).toStrictEqual('File not found'); +}); + +test('`if-modified-since` header', async () => { + const ctx = createExecutionContext(); + + let lastModified: string; + + // Make first request to grab its last modified date + { + const res = await worker.fetch( + new Request('https://localhost/dist/index.json'), + mockedEnv, + ctx + ); + + // Consume the body promise + await res.text(); + + expect(res.status).toBe(200); + + lastModified = res.headers.get('last-modified')!; + expect(lastModified).not.toBeNull(); + } + + // Returns a 304 when if-modified-since >= the file's last modified + { + const date = new Date(lastModified); + date.setMinutes(date.getMinutes() + 1); + + const res = await worker.fetch( + new Request('https://localhost/dist/index.json', { + headers: { + 'if-modified-since': date.toUTCString(), + }, + }), + mockedEnv, + ctx + ); + + // Consume the body promise + await res.text(); + + expect(res.status).toBe(304); + } + + // Returns a 200 when if-modified-since is <= the file's last modified + { + const res = await worker.fetch( + new Request('https://localhost/dist/index.json', { + headers: { + 'if-modified-since': new Date(0).toUTCString(), + }, + }), + env, + ctx + ); + + // Consume the body promise + await res.text(); + + expect(res.status).toBe(200); + } +}); + +test('`if-unmodified-since` header', async () => { + const ctx = createExecutionContext(); + + let lastModified: string; + + // Make first request to grab its last modified date + { + const res = await worker.fetch( + new Request('https://localhost/dist/index.json'), + mockedEnv, + ctx + ); + + // Consume the body promise + await res.text(); + + expect(res.status).toBe(200); + + lastModified = res.headers.get('last-modified')!; + expect(lastModified).not.toBeNull(); + } + + { + const res = await worker.fetch( + new Request('https://localhost/dist/index.json', { + headers: { + 'if-unmodified-since': new Date(0).toUTCString(), + }, + }), + env, + ctx + ); + + expect(res.status).toBe(412); + } + + { + const date = new Date(lastModified); + date.setMinutes(date.getMinutes() + 1); + + const res = await worker.fetch( + new Request('https://localhost/dist/index.json', { + headers: { + 'if-unmodified-since': date.toUTCString(), + }, + }), + mockedEnv, + ctx + ); + + // Consume the body promise + await res.text(); + + expect(res.status).toBe(200); + } +}); + +test('`if-match` header', async () => { + const ctx = createExecutionContext(); + + let etag: string; + + { + const res = await worker.fetch( + new Request('https://localhost/dist/index.json'), + mockedEnv, + ctx + ); + + // Consume the body promise + await res.text(); + + expect(res.status).toBe(200); + + etag = res.headers.get('etag')!; + expect(etag).not.toBeNull(); + } + + // Non-matching etag returns a 304 + { + const randomEtag = crypto.randomUUID().replaceAll('-', ''); + expect(etag).not.toStrictEqual(randomEtag); + + const res = await worker.fetch( + new Request('https://localhost/dist/index.json', { + headers: { 'if-match': `"${randomEtag}"` }, + }), + mockedEnv, + ctx + ); + + // Consume the body promise + await res.text(); + + expect(res.status).toBe(304); + expect(res.headers.get('cache-control')).toStrictEqual( + CACHE_HEADERS.failure + ); + } + + // Matching etag returns 200 + { + const res = await worker.fetch( + new Request('https://localhost/dist/index.json', { + headers: { 'if-match': etag }, + }), + mockedEnv, + ctx + ); + + // Consume the body promise + await res.text(); + + expect(res.status).toBe(200); + expect(res.headers.get('cache-control')).toStrictEqual( + CACHE_HEADERS.success + ); + } +}); + +test('`if-none-match` header', async () => { + const ctx = createExecutionContext(); + + let etag: string; + + { + const res = await worker.fetch( + new Request('https://localhost/dist/index.json'), + mockedEnv, + ctx + ); + + // Consume body promise + await res.text(); + + expect(res.status).toBe(200); + + etag = res.headers.get('etag')!; + expect(etag).not.toBeNull(); + } + + // Request w/ random etag returns 200 + { + const randomEtag = crypto.randomUUID().replaceAll('-', ''); + expect(etag).not.toStrictEqual(randomEtag); + + const res = await worker.fetch( + new Request('https://localhost/dist/index.json', { + headers: { 'if-none-match': `"${randomEtag}"` }, + }), + mockedEnv, + ctx + ); + + // Consume body promise + await res.text(); + + expect(res.status).toBe(200); + } + + // Request w/ matching etag returns 304 + { + const res = await worker.fetch( + new Request('https://localhost/dist/index.json', { + headers: { 'if-none-match': etag }, + }), + mockedEnv, + ctx + ); + + // Consume body promise + await res.text(); + + expect(res.status).toBe(304); + } +}); + +test('`range` header', async () => { + const ctx = createExecutionContext(); + + const res = await worker.fetch( + new Request('https://localhost/dist/index.json', { + headers: { + range: 'bytes=0-7', + }, + }), + mockedEnv, + ctx + ); + + expect(res.status).toBe(206); + expect(await res.text()).toBe('{ "hello'); +}); diff --git a/e2e-tests/misc.test.ts b/e2e-tests/misc.test.ts new file mode 100644 index 0000000..aed9aed --- /dev/null +++ b/e2e-tests/misc.test.ts @@ -0,0 +1,37 @@ +import { env, fetchMock, createExecutionContext } from 'cloudflare:test'; +import { test, beforeAll, expect } from 'vitest'; +import { populateR2WithDevBucket } from './util'; +import worker from '../src/worker'; +import type { Env } from '../src/env'; + +const mockedEnv: Env = { + ...env, + ENVIRONMENT: 'e2e-tests', + CACHING: false, + LOG_ERRORS: true, +}; + +beforeAll(async () => { + fetchMock.activate(); + fetchMock.disableNetConnect(); + + await populateR2WithDevBucket(); +}); + +// Ensure methods we don't support are handled properly +for (const method of ['POST', 'PATCH', 'PUT', 'DELETE', 'PROPFIND']) { + test(`${method} \`/\` returns a 405`, async () => { + const ctx = createExecutionContext(); + + const res = await worker.fetch( + new Request('https://localhost/', { method }), + mockedEnv, + ctx + ); + + // Consume body promise + await res.text(); + + expect(res.status).toBe(405); + }); +} diff --git a/e2e-tests/tsconfig.json b/e2e-tests/tsconfig.json new file mode 100644 index 0000000..ba0ebde --- /dev/null +++ b/e2e-tests/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "types": ["@cloudflare/vitest-pool-workers", "../vitest-types.d.ts"] + } +} diff --git a/e2e-tests/util.ts b/e2e-tests/util.ts new file mode 100644 index 0000000..9339f2e --- /dev/null +++ b/e2e-tests/util.ts @@ -0,0 +1,44 @@ +import { env } from 'cloudflare:test'; +import { inject } from 'vitest'; +import type { Env } from '../env'; +import type { Directory } from '../../vitest-setup'; + +async function populateR2BucketDirectory(directory: Directory): Promise { + const promises: Array> = []; + + for (const path of Object.keys(directory.files)) { + const file = directory.files[path]; + + promises.push( + env.R2_BUCKET.put(path, file.contents, { + customMetadata: { + // This is added by rclone when copying the release assets to the + // bucket. + mtime: `${file.lastModified}`, + }, + }) + ); + } + + promises.push( + ...Object.values(directory.subdirectories).map(populateR2BucketDirectory) + ); + + await Promise.all(promises); +} + +/** + * Writes the contents of the dev bucket into the R2 bucket given in {@link env} + */ +export async function populateR2WithDevBucket(): Promise { + // Grab the contents of the dev bucket + const devBucket = inject('devBucket'); + + // Write it to R2 + await populateR2BucketDirectory(devBucket); +} + +declare module 'cloudflare:test' { + // eslint-disable-next-line @typescript-eslint/no-empty-object-type + interface ProvidedEnv extends Env {} +} diff --git a/package-lock.json b/package-lock.json index ab4edc4..f56b592 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "toucan-js": "^4.1.1" }, "devDependencies": { + "@cloudflare/vitest-pool-workers": "^0.9.0", "@eslint/eslintrc": "^3.2.0", "@eslint/js": "^9.32.0", "@reporters/github": "^1.7.2", @@ -31,6 +32,7 @@ "prettier": "^3.6.2", "tsx": "^4.20.5", "typescript": "^5.8.3", + "vitest": "^3.2.4", "wrangler": "^4.33.1" } }, @@ -930,14 +932,14 @@ } }, "node_modules/@cloudflare/unenv-preset": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.7.0.tgz", - "integrity": "sha512-0JbEj+KTCQ4nTIWg2q8Bou+fPxzG6/zwU5O/w6Cld6WEjLl+716foT+2bjg48h09hMtjTKkJdAh1m4LybBKGCg==", + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.7.3.tgz", + "integrity": "sha512-tsQQagBKjvpd9baa6nWVIv399ejiqcrUBBW6SZx6Z22+ymm+Odv5+cFimyuCsD/fC1fQTwfRmwXBNpzvHSeGCw==", "dev": true, "license": "MIT OR Apache-2.0", "peerDependencies": { - "unenv": "2.0.0-rc.19", - "workerd": "^1.20250816.0" + "unenv": "2.0.0-rc.21", + "workerd": "^1.20250828.1" }, "peerDependenciesMeta": { "workerd": { @@ -945,10 +947,31 @@ } } }, + "node_modules/@cloudflare/vitest-pool-workers": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@cloudflare/vitest-pool-workers/-/vitest-pool-workers-0.9.0.tgz", + "integrity": "sha512-HZ1s1vcaQPLcRUqZICO7q/s9co2mdZL3uC+M0pwSApkekFQEmhV9YJHwBYtpxjnEsZWkpXfkypy81vwq0Fg1zQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "birpc": "0.2.14", + "cjs-module-lexer": "^1.2.3", + "devalue": "^5.3.2", + "miniflare": "4.20250906.1", + "semver": "^7.7.1", + "wrangler": "4.36.0", + "zod": "^3.22.3" + }, + "peerDependencies": { + "@vitest/runner": "2.0.x - 3.2.x", + "@vitest/snapshot": "2.0.x - 3.2.x", + "vitest": "2.0.x - 3.2.x" + } + }, "node_modules/@cloudflare/workerd-darwin-64": { - "version": "1.20250823.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20250823.0.tgz", - "integrity": "sha512-yRLJc1cQNqQYcDViOk7kpTXnR5XuBP7B/Ms5KBdlQ6eTr2Vsg9mfKqWKInjzY8/Cx+p+Sic2Tbld42gcYkiM2A==", + "version": "1.20250906.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20250906.0.tgz", + "integrity": "sha512-E+X/YYH9BmX0ew2j/mAWFif2z05NMNuhCTlNYEGLkqMe99K15UewBqajL9pMcMUKxylnlrEoK3VNxl33DkbnPA==", "cpu": [ "x64" ], @@ -963,9 +986,9 @@ } }, "node_modules/@cloudflare/workerd-darwin-arm64": { - "version": "1.20250823.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20250823.0.tgz", - "integrity": "sha512-KJnikUe6J29Ga1QMPKNCc8eHD56DdBlu5XE5LoBH/AYRrbS5UI1d5F844hUWoFKJb8KRaPIH9F849HZWfNa1vw==", + "version": "1.20250906.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20250906.0.tgz", + "integrity": "sha512-X5apsZ1SFW4FYTM19ISHf8005FJMPfrcf4U5rO0tdj+TeJgQgXuZ57IG0WeW7SpLVeBo8hM6WC8CovZh41AfnA==", "cpu": [ "arm64" ], @@ -980,9 +1003,9 @@ } }, "node_modules/@cloudflare/workerd-linux-64": { - "version": "1.20250823.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20250823.0.tgz", - "integrity": "sha512-4QFXq4eDWEAK5QjGxRe0XUTBax1Fgarc08HETL6q0y/KPZp2nOTLfjLjklTn/qEiztafNFoJEIwhkiknHeOi/g==", + "version": "1.20250906.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20250906.0.tgz", + "integrity": "sha512-rlKzWgsLnlQ5Nt9W69YBJKcmTmZbOGu0edUsenXPmc6wzULUxoQpi7ZE9k3TfTonJx4WoQsQlzCUamRYFsX+0Q==", "cpu": [ "x64" ], @@ -997,9 +1020,9 @@ } }, "node_modules/@cloudflare/workerd-linux-arm64": { - "version": "1.20250823.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20250823.0.tgz", - "integrity": "sha512-sODSrSVe4W/maoBu76qb0sJGBhxhSM2Q2tg/+G7q1IPgRZSzArMKIPrW6nBnmBrrG1O0X6aoAdID6w5hfuEM4g==", + "version": "1.20250906.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20250906.0.tgz", + "integrity": "sha512-DdedhiQ+SeLzpg7BpcLrIPEZ33QKioJQ1wvL4X7nuLzEB9rWzS37NNNahQzc1+44rhG4fyiHbXBPOeox4B9XVA==", "cpu": [ "arm64" ], @@ -1014,9 +1037,9 @@ } }, "node_modules/@cloudflare/workerd-windows-64": { - "version": "1.20250823.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20250823.0.tgz", - "integrity": "sha512-WaNqUOXUnrcEI+i2NI4+okA9CrJMI9n2XTfVtDg/pLvcA/ZPTz23MEFMZU1splr4SslS1th1NBO38RMPnDB4rA==", + "version": "1.20250906.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20250906.0.tgz", + "integrity": "sha512-Q8Qjfs8jGVILnZL6vUpQ90q/8MTCYaGR3d1LGxZMBqte8Vr7xF3KFHPEy7tFs0j0mMjnqCYzlofmPNY+9ZaDRg==", "cpu": [ "x64" ], @@ -1030,15 +1053,6 @@ "node": ">=16" } }, - "node_modules/@cloudflare/workers-types": { - "version": "4.20250831.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20250831.0.tgz", - "integrity": "sha512-68ExGPHQaNix9yhg3L+xkHjlHieigW59Ujad+y8ZtR3lcUoZ+4jzCLciUrVHfSD/zfQg02urBYqj+oshnmfcMg==", - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "peer": true - }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -1500,9 +1514,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", "dev": true, "license": "MIT", "dependencies": { @@ -1603,9 +1617,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.34.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.34.0.tgz", - "integrity": "sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==", + "version": "9.35.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.35.0.tgz", + "integrity": "sha512-30iXE9whjlILfWobBkNerJo+TXYsgVM5ERQwMcMKCHckHflCmf7wXDAHlARoWnh0s1U72WqlbeyE7iAcCzuCPw==", "dev": true, "license": "MIT", "engines": { @@ -2161,10 +2175,11 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.19", @@ -2250,9 +2265,9 @@ } }, "node_modules/@poppinss/dumper/node_modules/supports-color": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.0.tgz", - "integrity": "sha512-5eG9FQjEjDbAlI5+kdpdyPIBMRH4GfTVDGREVupaZHmVoppknhM29b/S9BkQz7cathp85BVgRi/As3Siln7e0Q==", + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", "dev": true, "license": "MIT", "engines": { @@ -2279,6 +2294,300 @@ "stack-utils": "^2.0.6" } }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.50.1.tgz", + "integrity": "sha512-HJXwzoZN4eYTdD8bVV22DN8gsPCAj3V20NHKOs8ezfXanGpmVPR7kalUHd+Y31IJp9stdB87VKPFbsGY3H/2ag==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.50.1.tgz", + "integrity": "sha512-PZlsJVcjHfcH53mOImyt3bc97Ep3FJDXRpk9sMdGX0qgLmY0EIWxCag6EigerGhLVuL8lDVYNnSo8qnTElO4xw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.50.1.tgz", + "integrity": "sha512-xc6i2AuWh++oGi4ylOFPmzJOEeAa2lJeGUGb4MudOtgfyyjr4UPNK+eEWTPLvmPJIY/pgw6ssFIox23SyrkkJw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.50.1.tgz", + "integrity": "sha512-2ofU89lEpDYhdLAbRdeyz/kX3Y2lpYc6ShRnDjY35bZhd2ipuDMDi6ZTQ9NIag94K28nFMofdnKeHR7BT0CATw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.50.1.tgz", + "integrity": "sha512-wOsE6H2u6PxsHY/BeFHA4VGQN3KUJFZp7QJBmDYI983fgxq5Th8FDkVuERb2l9vDMs1D5XhOrhBrnqcEY6l8ZA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.50.1.tgz", + "integrity": "sha512-A/xeqaHTlKbQggxCqispFAcNjycpUEHP52mwMQZUNqDUJFFYtPHCXS1VAG29uMlDzIVr+i00tSFWFLivMcoIBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.50.1.tgz", + "integrity": "sha512-54v4okehwl5TaSIkpp97rAHGp7t3ghinRd/vyC1iXqXMfjYUTm7TfYmCzXDoHUPTTf36L8pr0E7YsD3CfB3ZDg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.50.1.tgz", + "integrity": "sha512-p/LaFyajPN/0PUHjv8TNyxLiA7RwmDoVY3flXHPSzqrGcIp/c2FjwPPP5++u87DGHtw+5kSH5bCJz0mvXngYxw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.50.1.tgz", + "integrity": "sha512-2AbMhFFkTo6Ptna1zO7kAXXDLi7H9fGTbVaIq2AAYO7yzcAsuTNWPHhb2aTA6GPiP+JXh85Y8CiS54iZoj4opw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.50.1.tgz", + "integrity": "sha512-Cgef+5aZwuvesQNw9eX7g19FfKX5/pQRIyhoXLCiBOrWopjo7ycfB292TX9MDcDijiuIJlx1IzJz3IoCPfqs9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.50.1.tgz", + "integrity": "sha512-RPhTwWMzpYYrHrJAS7CmpdtHNKtt2Ueo+BlLBjfZEhYBhK00OsEqM08/7f+eohiF6poe0YRDDd8nAvwtE/Y62Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.50.1.tgz", + "integrity": "sha512-eSGMVQw9iekut62O7eBdbiccRguuDgiPMsw++BVUg+1K7WjZXHOg/YOT9SWMzPZA+w98G+Fa1VqJgHZOHHnY0Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.50.1.tgz", + "integrity": "sha512-S208ojx8a4ciIPrLgazF6AgdcNJzQE4+S9rsmOmDJkusvctii+ZvEuIC4v/xFqzbuP8yDjn73oBlNDgF6YGSXQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.50.1.tgz", + "integrity": "sha512-3Ag8Ls1ggqkGUvSZWYcdgFwriy2lWo+0QlYgEFra/5JGtAd6C5Hw59oojx1DeqcA2Wds2ayRgvJ4qxVTzCHgzg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.50.1.tgz", + "integrity": "sha512-t9YrKfaxCYe7l7ldFERE1BRg/4TATxIg+YieHQ966jwvo7ddHJxPj9cNFWLAzhkVsbBvNA4qTbPVNsZKBO4NSg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.50.1.tgz", + "integrity": "sha512-MCgtFB2+SVNuQmmjHf+wfI4CMxy3Tk8XjA5Z//A0AKD7QXUYFMQcns91K6dEHBvZPCnhJSyDWLApk40Iq/H3tA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.50.1.tgz", + "integrity": "sha512-nEvqG+0jeRmqaUMuwzlfMKwcIVffy/9KGbAGyoa26iu6eSngAYQ512bMXuqqPrlTyfqdlB9FVINs93j534UJrg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.50.1.tgz", + "integrity": "sha512-RDsLm+phmT3MJd9SNxA9MNuEAO/J2fhW8GXk62G/B4G7sLVumNFbRwDL6v5NrESb48k+QMqdGbHgEtfU0LCpbA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.50.1.tgz", + "integrity": "sha512-hpZB/TImk2FlAFAIsoElM3tLzq57uxnGYwplg6WDyAxbYczSi8O2eQ+H2Lx74504rwKtZ3N2g4bCUkiamzS6TQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.50.1.tgz", + "integrity": "sha512-SXjv8JlbzKM0fTJidX4eVsH+Wmnp0/WcD8gJxIZyR6Gay5Qcsmdbi9zVtnbkGPG8v2vMR1AD06lGWy5FLMcG7A==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.50.1.tgz", + "integrity": "sha512-StxAO/8ts62KZVRAm4JZYq9+NqNsV7RvimNK+YM7ry//zebEH6meuugqW/P5OFUCjyQgui+9fUxT6d5NShvMvA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@sentry/core": { "version": "8.9.2", "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.9.2.tgz", @@ -3058,11 +3367,29 @@ "dev": true, "license": "CC0-1.0" }, + "node_modules/@types/chai": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz", + "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*" + } + }, + "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.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", - "dev": true + "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/json-schema": { "version": "7.0.15", @@ -3079,9 +3406,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.3.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz", - "integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==", + "version": "24.3.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.3.tgz", + "integrity": "sha512-GKBNHjoNw3Kra1Qg5UXttsY5kiWMEfoHq2TmXb+b1rcm6N7B3wTrFYIf/oSZ1xNQ+hVVijgLkiDZh7jRRsh+Gw==", "dev": true, "license": "MIT", "dependencies": { @@ -3094,17 +3421,17 @@ "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.41.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.41.0.tgz", - "integrity": "sha512-8fz6oa6wEKZrhXWro/S3n2eRJqlRcIa6SlDh59FXJ5Wp5XRZ8B9ixpJDcjadHq47hMx0u+HW6SNa6LjJQ6NLtw==", + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.43.0.tgz", + "integrity": "sha512-8tg+gt7ENL7KewsKMKDHXR1vm8tt9eMxjJBYINf6swonlWgkYn5NwyIgXpbbDxTNU5DgpDFfj95prcTq2clIQQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.41.0", - "@typescript-eslint/type-utils": "8.41.0", - "@typescript-eslint/utils": "8.41.0", - "@typescript-eslint/visitor-keys": "8.41.0", + "@typescript-eslint/scope-manager": "8.43.0", + "@typescript-eslint/type-utils": "8.43.0", + "@typescript-eslint/utils": "8.43.0", + "@typescript-eslint/visitor-keys": "8.43.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -3118,7 +3445,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.41.0", + "@typescript-eslint/parser": "^8.43.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } @@ -3134,16 +3461,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.42.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.42.0.tgz", - "integrity": "sha512-r1XG74QgShUgXph1BYseJ+KZd17bKQib/yF3SR+demvytiRXrwd12Blnz5eYGm8tXaeRdd4x88MlfwldHoudGg==", + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.43.0.tgz", + "integrity": "sha512-B7RIQiTsCBBmY+yW4+ILd6mF5h1FUwJsVvpqkrgpszYifetQ2Ke+Z4u6aZh0CblkUGIdR59iYVyXqqZGkZ3aBw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.42.0", - "@typescript-eslint/types": "8.42.0", - "@typescript-eslint/typescript-estree": "8.42.0", - "@typescript-eslint/visitor-keys": "8.42.0", + "@typescript-eslint/scope-manager": "8.43.0", + "@typescript-eslint/types": "8.43.0", + "@typescript-eslint/typescript-estree": "8.43.0", + "@typescript-eslint/visitor-keys": "8.43.0", "debug": "^4.3.4" }, "engines": { @@ -3158,15 +3485,16 @@ "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { - "version": "8.42.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.42.0.tgz", - "integrity": "sha512-51+x9o78NBAVgQzOPd17DkNTnIzJ8T/O2dmMBLoK9qbY0Gm52XJcdJcCl18ExBMiHo6jPMErUQWUv5RLE51zJw==", + "node_modules/@typescript-eslint/project-service": { + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.43.0.tgz", + "integrity": "sha512-htB/+D/BIGoNTQYffZw4uM4NzzuolCoaA/BusuSIcC8YjmBYQioew5VUZAYdAETPjeed0hqCaW7EHg+Robq8uw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.42.0", - "@typescript-eslint/visitor-keys": "8.42.0" + "@typescript-eslint/tsconfig-utils": "^8.43.0", + "@typescript-eslint/types": "^8.43.0", + "debug": "^4.3.4" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3174,17 +3502,20 @@ "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.42.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.42.0.tgz", - "integrity": "sha512-3WbiuzoEowaEn8RSnhJBrxSwX8ULYE9CXaPepS2C2W3NSA5NNIvBaslpBSBElPq0UGr0xVJlXFWOAKIkyylydQ==", + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.43.0.tgz", + "integrity": "sha512-daSWlQ87ZhsjrbMLvpuuMAt3y4ba57AuvadcR7f3nl8eS3BjRc8L9VLxFLk92RL5xdXOg6IQ+qKjjqNEimGuAg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.42.0", - "eslint-visitor-keys": "^4.2.1" + "@typescript-eslint/types": "8.43.0", + "@typescript-eslint/visitor-keys": "8.43.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3194,30 +3525,12 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/parser/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.42.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.42.0.tgz", - "integrity": "sha512-vfVpLHAhbPjilrabtOSNcUDmBboQNrJUiNAGoImkZKnMjs2TIcWG33s4Ds0wY3/50aZmTMqJa6PiwkwezaAklg==", + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.43.0.tgz", + "integrity": "sha512-ALC2prjZcj2YqqL5X/bwWQmHA2em6/94GcbB/KKu5SX3EBDOsqztmmX1kMkvAJHzxk7TazKzJfFiEIagNV3qEA==", "dev": true, "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.42.0", - "@typescript-eslint/types": "^8.42.0", - "debug": "^4.3.4" - }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -3229,15 +3542,18 @@ "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.41.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.41.0.tgz", - "integrity": "sha512-n6m05bXn/Cd6DZDGyrpXrELCPVaTnLdPToyhBoFkLIMznRUQUEQdSp96s/pcWSQdqOhrgR1mzJ+yItK7T+WPMQ==", + "node_modules/@typescript-eslint/type-utils": { + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.43.0.tgz", + "integrity": "sha512-qaH1uLBpBuBBuRf8c1mLJ6swOfzCXryhKND04Igr4pckzSEW9JX5Aw9AgW00kwfjWJF0kk0ps9ExKTfvXfw4Qg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.41.0", - "@typescript-eslint/visitor-keys": "8.41.0" + "@typescript-eslint/types": "8.43.0", + "@typescript-eslint/typescript-estree": "8.43.0", + "@typescript-eslint/utils": "8.43.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3245,12 +3561,16 @@ "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/scope-manager/node_modules/@typescript-eslint/types": { - "version": "8.41.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.41.0.tgz", - "integrity": "sha512-9EwxsWdVqh42afLbHP90n2VdHaWU/oWgbH2P0CfcNfdKL7CuKpwMQGjwev56vWu9cSKU7FWSu6r9zck6CVfnag==", + "node_modules/@typescript-eslint/types": { + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.43.0.tgz", + "integrity": "sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw==", "dev": true, "license": "MIT", "engines": { @@ -3261,12 +3581,24 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.42.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.42.0.tgz", - "integrity": "sha512-kHeFUOdwAJfUmYKjR3CLgZSglGHjbNTi1H8sTYRYV2xX6eNz4RyJ2LIgsDLKf8Yi0/GL1WZAC/DgZBeBft8QAQ==", + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.43.0.tgz", + "integrity": "sha512-7Vv6zlAhPb+cvEpP06WXXy/ZByph9iL6BQRBDj4kmBsW98AqEeQHlj/13X+sZOrKSo9/rNKH4Ul4f6EICREFdw==", "dev": true, "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.43.0", + "@typescript-eslint/tsconfig-utils": "8.43.0", + "@typescript-eslint/types": "8.43.0", + "@typescript-eslint/visitor-keys": "8.43.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -3278,114 +3610,7 @@ "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.41.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.41.0.tgz", - "integrity": "sha512-63qt1h91vg3KsjVVonFJWjgSK7pZHSQFKH6uwqxAH9bBrsyRhO6ONoKyXxyVBzG1lJnFAJcKAcxLS54N1ee1OQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.41.0", - "@typescript-eslint/typescript-estree": "8.41.0", - "@typescript-eslint/utils": "8.41.0", - "debug": "^4.3.4", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/project-service": { - "version": "8.41.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.41.0.tgz", - "integrity": "sha512-b8V9SdGBQzQdjJ/IO3eDifGpDBJfvrNTp2QD9P2BeqWTGrRibgfgIlBSw6z3b6R7dPzg752tOs4u/7yCLxksSQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.41.0", - "@typescript-eslint/types": "^8.41.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.41.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.41.0.tgz", - "integrity": "sha512-TDhxYFPUYRFxFhuU5hTIJk+auzM/wKvWgoNYOPcOf6i4ReYlOoYN8q1dV5kOTjNQNJgzWN3TUUQMtlLOcUgdUw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { - "version": "8.41.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.41.0.tgz", - "integrity": "sha512-9EwxsWdVqh42afLbHP90n2VdHaWU/oWgbH2P0CfcNfdKL7CuKpwMQGjwev56vWu9cSKU7FWSu6r9zck6CVfnag==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.41.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.41.0.tgz", - "integrity": "sha512-D43UwUYJmGhuwHfY7MtNKRZMmfd8+p/eNSfFe6tH5mbVDto+VQCayeAt35rOx3Cs6wxD16DQtIKw/YXxt5E0UQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.41.0", - "@typescript-eslint/tsconfig-utils": "8.41.0", - "@typescript-eslint/types": "8.41.0", - "@typescript-eslint/visitor-keys": "8.41.0", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/brace-expansion": { + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", @@ -3395,7 +3620,7 @@ "balanced-match": "^1.0.0" } }, - "node_modules/@typescript-eslint/type-utils/node_modules/minimatch": { + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", @@ -3411,37 +3636,17 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@typescript-eslint/types": { - "version": "8.42.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.42.0.tgz", - "integrity": "sha512-LdtAWMiFmbRLNP7JNeY0SqEtJvGMYSzfiWBSmx+VSZ1CH+1zyl8Mmw1TT39OrtsRvIYShjJWzTDMPWZJCpwBlw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.42.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.42.0.tgz", - "integrity": "sha512-ku/uYtT4QXY8sl9EDJETD27o3Ewdi72hcXg1ah/kkUgBvAYHLwj2ofswFFNXS+FL5G+AGkxBtvGt8pFBHKlHsQ==", + "node_modules/@typescript-eslint/utils": { + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.43.0.tgz", + "integrity": "sha512-S1/tEmkUeeswxd0GGcnwuVQPFWo8NzZTOMxCvw8BX7OMxnNae+i8Tm7REQen/SwUIPoPqfKn7EaZ+YLpiB3k9g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.42.0", - "@typescript-eslint/tsconfig-utils": "8.42.0", - "@typescript-eslint/types": "8.42.0", - "@typescript-eslint/visitor-keys": "8.42.0", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.1.0" + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.43.0", + "@typescript-eslint/types": "8.43.0", + "@typescript-eslint/typescript-estree": "8.43.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3451,17 +3656,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.42.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.42.0.tgz", - "integrity": "sha512-3WbiuzoEowaEn8RSnhJBrxSwX8ULYE9CXaPepS2C2W3NSA5NNIvBaslpBSBElPq0UGr0xVJlXFWOAKIkyylydQ==", + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.43.0.tgz", + "integrity": "sha512-T+S1KqRD4sg/bHfLwrpF/K3gQLBM1n7Rp7OjjikjTEssI2YJzQpi5WXoynOaQ93ERIuq3O8RBTOUYDKszUCEHw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.42.0", + "@typescript-eslint/types": "8.43.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -3472,17 +3678,7 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/eslint-visitor-keys": { + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", @@ -3495,197 +3691,119 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.41.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.41.0.tgz", - "integrity": "sha512-udbCVstxZ5jiPIXrdH+BZWnPatjlYwJuJkDA4Tbo3WyYLh8NvB+h/bKeSZHDOFKfphsZYJQqaFtLeXEqurQn1A==", + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.41.0", - "@typescript-eslint/types": "8.41.0", - "@typescript-eslint/typescript-estree": "8.41.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "url": "https://opencollective.com/vitest" } }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/project-service": { - "version": "8.41.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.41.0.tgz", - "integrity": "sha512-b8V9SdGBQzQdjJ/IO3eDifGpDBJfvrNTp2QD9P2BeqWTGrRibgfgIlBSw6z3b6R7dPzg752tOs4u/7yCLxksSQ==", + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.41.0", - "@typescript-eslint/types": "^8.41.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.41.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.41.0.tgz", - "integrity": "sha512-TDhxYFPUYRFxFhuU5hTIJk+auzM/wKvWgoNYOPcOf6i4ReYlOoYN8q1dV5kOTjNQNJgzWN3TUUQMtlLOcUgdUw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } } }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": { - "version": "8.41.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.41.0.tgz", - "integrity": "sha512-9EwxsWdVqh42afLbHP90n2VdHaWU/oWgbH2P0CfcNfdKL7CuKpwMQGjwev56vWu9cSKU7FWSu6r9zck6CVfnag==", + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", "dev": true, "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "dependencies": { + "tinyrainbow": "^2.0.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "url": "https://opencollective.com/vitest" } }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.41.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.41.0.tgz", - "integrity": "sha512-D43UwUYJmGhuwHfY7MtNKRZMmfd8+p/eNSfFe6tH5mbVDto+VQCayeAt35rOx3Cs6wxD16DQtIKw/YXxt5E0UQ==", + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.41.0", - "@typescript-eslint/tsconfig-utils": "8.41.0", - "@typescript-eslint/types": "8.41.0", - "@typescript-eslint/visitor-keys": "8.41.0", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "url": "https://opencollective.com/vitest" } }, - "node_modules/@typescript-eslint/utils/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://opencollective.com/vitest" } }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.41.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.41.0.tgz", - "integrity": "sha512-+GeGMebMCy0elMNg67LRNoVnUFPIm37iu5CmHESVx56/9Jsfdpsvbv605DQ81Pi/x11IdKUsS5nzgTYbCQU9fg==", + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.41.0", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "tinyspy": "^4.0.3" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "url": "https://opencollective.com/vitest" } }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/@typescript-eslint/types": { - "version": "8.41.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.41.0.tgz", - "integrity": "sha512-9EwxsWdVqh42afLbHP90n2VdHaWU/oWgbH2P0CfcNfdKL7CuKpwMQGjwev56vWu9cSKU7FWSu6r9zck6CVfnag==", + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", "dev": true, "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://opencollective.com/vitest" } }, "node_modules/acorn": { @@ -3768,12 +3886,32 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "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/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/birpc": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-0.2.14.tgz", + "integrity": "sha512-37FHE8rqsYM5JEKCnXFyHpBCzvgHEExwVVTq+nUmloInU7l8ezD1TpOhKpS8oe1DTYFqEK27rFZVKG43oTqXRA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/blake3-wasm": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", @@ -3815,6 +3953,16 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -3835,6 +3983,23 @@ "tslib": "^2.0.3" } }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -3851,6 +4016,23 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, "node_modules/clean-css": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", @@ -3944,9 +4126,9 @@ } }, "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", "dependencies": { @@ -3961,6 +4143,16 @@ } } }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -3984,6 +4176,13 @@ "node": ">=8" } }, + "node_modules/devalue": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.3.2.tgz", + "integrity": "sha512-UDsjUbpQn9kvm68slnrs+mfxwFkIflOhkanmyabZ8zOYk8SMEIbJ3TK+88g70hSIeytu4y18f0z/hYHMTrXIWw==", + "dev": true, + "license": "MIT" + }, "node_modules/dot-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", @@ -4030,6 +4229,13 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/esbuild": { "version": "0.25.4", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", @@ -4084,19 +4290,19 @@ } }, "node_modules/eslint": { - "version": "9.34.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.34.0.tgz", - "integrity": "sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==", + "version": "9.35.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.35.0.tgz", + "integrity": "sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.3.1", "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.34.0", + "@eslint/js": "9.35.0", "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -4212,6 +4418,7 @@ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -4305,6 +4512,16 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -4327,6 +4544,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/expect-type": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", + "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/exsolve": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", @@ -4672,9 +4899,9 @@ } }, "node_modules/is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", "dev": true, "license": "MIT" }, @@ -4745,6 +4972,13 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -4828,6 +5062,13 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lower-case": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", @@ -4847,6 +5088,16 @@ "node": "20 || >=22" } }, + "node_modules/magic-string": { + "version": "0.30.19", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", + "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -4885,9 +5136,9 @@ } }, "node_modules/miniflare": { - "version": "4.20250823.1", - "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20250823.1.tgz", - "integrity": "sha512-qjbF69XXyHXk4R//q0a9MLraKE9MLKZ/94k6jKcfouJ0g+se7VyMzCBryeWA534+ZAlNM4Ay5gqYr1v3Wk6ctQ==", + "version": "4.20250906.1", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20250906.1.tgz", + "integrity": "sha512-yuPHog7j+GKHtRaKKF3Mpwvb5SVtvmkQpY/f9Ue0xhG/fYQcaxTKVO6RAB1pUN1jSyvmDOxVEAFFVoni8GYl3g==", "dev": true, "license": "MIT", "dependencies": { @@ -4898,8 +5149,8 @@ "glob-to-regexp": "0.4.1", "sharp": "^0.33.5", "stoppable": "1.1.0", - "undici": "^7.10.0", - "workerd": "1.20250823.0", + "undici": "7.14.0", + "workerd": "1.20250906.0", "ws": "8.18.0", "youch": "4.1.0-beta.10", "zod": "3.22.3" @@ -4948,6 +5199,25 @@ "mustache": "bin/mustache" } }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "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/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -5120,6 +5390,23 @@ "dev": true, "license": "MIT" }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -5133,6 +5420,35 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "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/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -5240,6 +5556,47 @@ "node": ">=0.10.0" } }, + "node_modules/rollup": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.50.1.tgz", + "integrity": "sha512-78E9voJHwnXQMiQdiqswVLZwJIzdBKJ1GdI5Zx6XwoFKUIk09/sSrr+05QFzvYb8q6Y9pPV45zzDuYa3907TZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.50.1", + "@rollup/rollup-android-arm64": "4.50.1", + "@rollup/rollup-darwin-arm64": "4.50.1", + "@rollup/rollup-darwin-x64": "4.50.1", + "@rollup/rollup-freebsd-arm64": "4.50.1", + "@rollup/rollup-freebsd-x64": "4.50.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.50.1", + "@rollup/rollup-linux-arm-musleabihf": "4.50.1", + "@rollup/rollup-linux-arm64-gnu": "4.50.1", + "@rollup/rollup-linux-arm64-musl": "4.50.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.50.1", + "@rollup/rollup-linux-ppc64-gnu": "4.50.1", + "@rollup/rollup-linux-riscv64-gnu": "4.50.1", + "@rollup/rollup-linux-riscv64-musl": "4.50.1", + "@rollup/rollup-linux-s390x-gnu": "4.50.1", + "@rollup/rollup-linux-x64-gnu": "4.50.1", + "@rollup/rollup-linux-x64-musl": "4.50.1", + "@rollup/rollup-openharmony-arm64": "4.50.1", + "@rollup/rollup-win32-arm64-msvc": "4.50.1", + "@rollup/rollup-win32-ia32-msvc": "4.50.1", + "@rollup/rollup-win32-x64-msvc": "4.50.1", + "fsevents": "~2.3.2" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -5265,9 +5622,9 @@ } }, "node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "dev": true, "license": "ISC", "bin": { @@ -5338,6 +5695,13 @@ "node": ">=8" } }, + "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/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -5351,9 +5715,9 @@ } }, "node_modules/simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", "dev": true, "license": "MIT", "dependencies": { @@ -5369,6 +5733,16 @@ "node": ">=0.10.0" } }, + "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/source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", @@ -5400,6 +5774,20 @@ "node": ">=8" } }, + "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": "3.9.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", + "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", + "dev": true, + "license": "MIT" + }, "node_modules/stoppable": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", @@ -5519,6 +5907,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-literal": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.0.0.tgz", + "integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/strnum": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", @@ -5578,6 +5979,98 @@ "node": ">=10" } }, + "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", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.3.tgz", + "integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -5684,9 +6177,9 @@ "license": "MIT" }, "node_modules/undici": { - "version": "7.15.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.15.0.tgz", - "integrity": "sha512-7oZJCPvvMvTd0OlqWsIxTuItTpJBpU1tcbVl24FMn3xt3+VSunwUasmfPJRE57oNO1KsZ4PgA1xTdAX4hq8NyQ==", + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.14.0.tgz", + "integrity": "sha512-Vqs8HTzjpQXZeXdpsfChQTlafcMQaaIwnGwLam1wudSSjlJeQ3bw1j+TLPePgrCnCpUXx7Ba5Pdpf5OBih62NQ==", "dev": true, "license": "MIT", "engines": { @@ -5701,9 +6194,9 @@ "license": "MIT" }, "node_modules/unenv": { - "version": "2.0.0-rc.19", - "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.19.tgz", - "integrity": "sha512-t/OMHBNAkknVCI7bVB9OWjUUAwhVv9vsPIAGnNUxnu3FxPQN11rjh0sksLMzc3g7IlTgvHmOTl4JM7JHpcv5wA==", + "version": "2.0.0-rc.21", + "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.21.tgz", + "integrity": "sha512-Wj7/AMtE9MRnAXa6Su3Lk0LNCfqDYgfwVjwRFVum9U7wsto1imuHqk4kTm7Jni+5A0Hn7dttL6O/zjvUvoo+8A==", "dev": true, "license": "MIT", "dependencies": { @@ -5732,6 +6225,221 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/vite": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.5.tgz", + "integrity": "sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "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", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.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 + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "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/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -5747,10 +6455,27 @@ "node": ">= 8" } }, + "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" + } + }, "node_modules/workerd": { - "version": "1.20250823.0", - "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20250823.0.tgz", - "integrity": "sha512-95lToK9zeaC7bX5ZmlP/wz6zqoUPBk3hhec1JjEMGZrxsXY9cPRkjWNCcjDctQ17U97vjMcY/ymchgx7w8Cfmg==", + "version": "1.20250906.0", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20250906.0.tgz", + "integrity": "sha512-ryVyEaqXPPsr/AxccRmYZZmDAkfQVjhfRqrNTlEeN8aftBk6Ca1u7/VqmfOayjCXrA+O547TauebU+J3IpvFXw==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", @@ -5761,28 +6486,28 @@ "node": ">=16" }, "optionalDependencies": { - "@cloudflare/workerd-darwin-64": "1.20250823.0", - "@cloudflare/workerd-darwin-arm64": "1.20250823.0", - "@cloudflare/workerd-linux-64": "1.20250823.0", - "@cloudflare/workerd-linux-arm64": "1.20250823.0", - "@cloudflare/workerd-windows-64": "1.20250823.0" + "@cloudflare/workerd-darwin-64": "1.20250906.0", + "@cloudflare/workerd-darwin-arm64": "1.20250906.0", + "@cloudflare/workerd-linux-64": "1.20250906.0", + "@cloudflare/workerd-linux-arm64": "1.20250906.0", + "@cloudflare/workerd-windows-64": "1.20250906.0" } }, "node_modules/wrangler": { - "version": "4.33.1", - "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.33.1.tgz", - "integrity": "sha512-8x/3Tbt+/raBMm0+vRyAHSGu2kF1QjeiSrx47apgPk/AzSBcXI9YuUUdGrKnozMYZlEbOxdBQOMyuRRDTyNmOg==", + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.36.0.tgz", + "integrity": "sha512-J1sZh7ePy7BtzvIyt9ufiL6aQOW6OE0VEi9YJiyXOuaXDKrR7V5HJBTsraNdFDqXgi30mYGGBVs0mgZHGRhTBA==", "dev": true, "license": "MIT OR Apache-2.0", "dependencies": { "@cloudflare/kv-asset-handler": "0.4.0", - "@cloudflare/unenv-preset": "2.7.0", + "@cloudflare/unenv-preset": "2.7.3", "blake3-wasm": "2.1.5", "esbuild": "0.25.4", - "miniflare": "4.20250823.1", + "miniflare": "4.20250906.1", "path-to-regexp": "6.3.0", - "unenv": "2.0.0-rc.19", - "workerd": "1.20250823.0" + "unenv": "2.0.0-rc.21", + "workerd": "1.20250906.0" }, "bin": { "wrangler": "bin/wrangler.js", @@ -5795,7 +6520,7 @@ "fsevents": "~2.3.2" }, "peerDependencies": { - "@cloudflare/workers-types": "^4.20250823.0" + "@cloudflare/workers-types": "^4.20250906.0" }, "peerDependenciesMeta": { "@cloudflare/workers-types": { diff --git a/package.json b/package.json index 3b10d21..0ab9a85 100644 --- a/package.json +++ b/package.json @@ -8,13 +8,13 @@ "format": "prettier --check --write \"**/*.{ts,js,mjs,json,md}\"", "prettier": "prettier --check \"**/*.{ts,js,mjs,json,md}\"", "lint": "eslint ./src", - "test": "node --run test:unit && node --run test:e2e", - "test:unit": "node --test --test-reporter=@reporters/github --test-reporter-destination=stdout --test-reporter=spec --test-reporter-destination=stdout --import=tsx ./tests/unit/index.test.ts", - "test:e2e": "wrangler deploy --dry-run --outdir=dist && node --test --test-reporter=@reporters/github --test-reporter-destination=stdout --test-reporter=spec --test-reporter-destination=stdout --import=tsx ./tests/e2e/index.test.ts", + "test": "vitest --run", + "test:watch": "vitest", "build:mustache": "node scripts/compile-mustache.mjs", "build:workers-types": "wrangler types --include-env=false && node --run format" }, "devDependencies": { + "@cloudflare/vitest-pool-workers": "^0.9.0", "@eslint/eslintrc": "^3.2.0", "@eslint/js": "^9.32.0", "@reporters/github": "^1.7.2", @@ -32,6 +32,7 @@ "prettier": "^3.6.2", "tsx": "^4.20.5", "typescript": "^5.8.3", + "vitest": "^3.2.4", "wrangler": "^4.33.1" }, "dependencies": { diff --git a/scripts/constants.mjs b/scripts/constants.mjs index 977fdc1..7c5eb4f 100644 --- a/scripts/constants.mjs +++ b/scripts/constants.mjs @@ -1,5 +1,8 @@ 'use strict'; +import { dirname, join } from 'node:path'; +import { readdir, readFile, stat } from 'node:fs/promises'; + export const ENDPOINT = process.env.ENDPOINT ?? 'https://07be8d2fbc940503ca1be344714cb0d1.r2.cloudflarestorage.com'; @@ -13,3 +16,5 @@ export const R2_RETRY_COUNT = 3; export const RELEASE_DIR = 'nodejs/release/'; export const DOCS_DIR = 'nodejs/docs/'; + +export const DEV_BUCKET_PATH = join(import.meta.dirname, '..', 'dev-bucket'); diff --git a/src/env.ts b/src/env.ts index c4eaff7..a929e02 100644 --- a/src/env.ts +++ b/src/env.ts @@ -3,31 +3,45 @@ export interface Env { * Environment the worker is running in */ ENVIRONMENT: 'dev' | 'staging' | 'prod' | 'e2e-tests'; + + /** + * Should caching be enabled? + */ + CACHING: boolean; + + LOG_ERRORS?: boolean; + /** * R2 bucket we read from */ R2_BUCKET: R2Bucket; + /** * Endpoint to hit when using the S3 api. */ S3_ENDPOINT: string; + /** * Id of the api token used for the S3 api. * The token needs >=Object Read only permissions */ S3_ACCESS_KEY_ID: string; + /** * Secret of the api token used for the S3 api */ S3_ACCESS_KEY_SECRET: string; + /** * Bucket name */ BUCKET_NAME: string; + /** * Sentry DSN, used for error monitoring * If missing, Sentry isn't used */ SENTRY_DSN?: string; + ORIGIN_HOST: string; } diff --git a/src/middleware/cacheMiddleware.ts b/src/middleware/cacheMiddleware.ts index 4889935..5ef0886 100644 --- a/src/middleware/cacheMiddleware.ts +++ b/src/middleware/cacheMiddleware.ts @@ -1,4 +1,3 @@ -import { isCacheEnabled } from '../utils/cache'; import type { Middleware } from './middleware'; /** @@ -30,7 +29,7 @@ export function cached(middleware: Middleware): Middleware { }, }); - if (!isCacheEnabled(ctx.env)) { + if (!ctx.env.CACHING) { return middleware.handle(request, ctx, next); } diff --git a/src/middleware/r2Middleware.ts b/src/middleware/r2Middleware.ts index 0ad7206..67962f9 100644 --- a/src/middleware/r2Middleware.ts +++ b/src/middleware/r2Middleware.ts @@ -44,7 +44,6 @@ async function handleDirectory( return Response.redirect(`${url}/`, 301); } - // todo remove listpaths option? const result = await getProvider(ctx).readDirectory(r2Path); if (result === undefined) { diff --git a/src/middleware/substituteMiddleware.test.ts b/src/middleware/substituteMiddleware.test.ts new file mode 100644 index 0000000..dd7b03c --- /dev/null +++ b/src/middleware/substituteMiddleware.test.ts @@ -0,0 +1,43 @@ +import { describe, test, expect } from 'vitest'; +import { SubtitutionMiddleware } from './subtituteMiddleware'; +import type { Request as WorkerRequest } from '../routes/request'; +import type { Router } from '../routes'; + +describe('SubtituteMiddleware', () => { + test('correctly substitutes url `/dist/latest` to `/dist/v1.0.0`', () => { + const originalUrl = 'https://localhost/dist/latest'; + + const originalRequest: Partial = new Request(originalUrl); + originalRequest.urlObj = new URL(originalUrl); + + const router: Partial = { + handle: (substitutedRequest: WorkerRequest, _, unsubstitutedUrl) => { + // Has the url been substituted? (latest -> v1.0.0) + // strictEqual(substitutedRequest.url, 'https://localhost/dist/v1.0.0'); + expect(substitutedRequest.url).toStrictEqual( + 'https://localhost/dist/v1.0.0' + ); + + // Was the original url saved? + // strictEqual(unsubstitutedUrl, originalRequest.urlObj); + expect(unsubstitutedUrl).toStrictEqual(originalRequest.urlObj); + + return Promise.resolve(new Response()); + }, + }; + + // Sanity pre-checks + expect(originalRequest.unsubstitutedUrl).toStrictEqual(undefined); + expect(originalRequest.urlObj!.pathname).toStrictEqual('/dist/latest'); + + // @ts-expect-error full router not needed + const middleware = new SubtitutionMiddleware(router, 'latest', 'v1.0.0'); + + // @ts-expect-error full router & ctx not needed + middleware.handle(originalRequest, { + sentry: { + addBreadcrumb: () => {}, + }, + }); + }); +}); diff --git a/src/providers/s3Provider.ts b/src/providers/s3Provider.ts index 5f0ccad..c4ba763 100644 --- a/src/providers/s3Provider.ts +++ b/src/providers/s3Provider.ts @@ -1,4 +1,5 @@ import { ListObjectsV2Command, S3Client } from '@aws-sdk/client-s3'; +import { FetchHttpHandler } from '@smithy/fetch-http-handler'; import type { Context } from '../context'; import type { File, @@ -38,6 +39,7 @@ export class S3Provider implements Provider { accessKeyId: ctx.env.S3_ACCESS_KEY_ID, secretAccessKey: ctx.env.S3_ACCESS_KEY_SECRET, }, + requestHandler: new FetchHttpHandler(), }); } diff --git a/tests/unit/router/router.test.ts b/src/routes/router.test.ts similarity index 56% rename from tests/unit/router/router.test.ts rename to src/routes/router.test.ts index 0553b95..b45ed8e 100644 --- a/tests/unit/router/router.test.ts +++ b/src/routes/router.test.ts @@ -1,26 +1,26 @@ -import assert from 'node:assert'; -import { it } from 'node:test'; -import { Router } from '../../../src/routes/router'; -import { Middleware } from '../../../src/middleware/middleware'; +import { expect, test } from 'vitest'; +import type { Middleware } from '../middleware/middleware'; +import { type Context } from '../context'; +import { Router } from './router'; -it('middleware chains properly', async () => { - const callOrdered: string[] = []; +test('middleware chains properly', async () => { + const callOrder: string[] = []; const firstMiddleware: Middleware = { handle: (_, _2, next) => { - callOrdered.push('first'); + callOrder.push('first'); return next(); }, }; const secondMiddleware: Middleware = { handle: (_, _2, next) => { - callOrdered.push('second'); + callOrder.push('second'); return next(); }, }; const thirdMiddleware: Middleware = { handle: () => { - callOrdered.push('third'); + callOrder.push('third'); return Promise.resolve(new Response('cool response')); }, }; @@ -28,13 +28,15 @@ it('middleware chains properly', async () => { const router = new Router(); router.get('/', [firstMiddleware, secondMiddleware, thirdMiddleware]); - // @ts-expect-error context - const response = await router.handle(new Request('http://localhost/'), {}); - assert.strictEqual(await response.text(), 'cool response'); - assert.deepStrictEqual(callOrdered, ['first', 'second', 'third']); + // @ts-expect-error don't need a complete context + const ctx: Context = {}; + + const response = await router.handle(new Request('http://localhost/'), ctx); + expect(await response.text()).toStrictEqual('cool response'); + expect(callOrder).toStrictEqual(['first', 'second', 'third']); }); -it('errors in middleware get skipped & reported', async () => { +test('errors in middleware get skipped & reported', async () => { const errorToThrow = new Error('error from first middleware'); const firstMiddleware: Middleware = { handle: () => { @@ -42,7 +44,7 @@ it('errors in middleware get skipped & reported', async () => { }, }; const secondMiddleware: Middleware = { - handle: (_, _2, next) => { + handle: (_, _2, _3) => { return Promise.resolve(new Response('response from second middleware')); }, }; @@ -51,13 +53,17 @@ it('errors in middleware get skipped & reported', async () => { router.get('/', [firstMiddleware, secondMiddleware]); const response = await router.handle(new Request('http://localhost/'), { + // @ts-expect-error missing properties we don't need + env: {}, sentry: { // @ts-expect-error incorrect signature but it's fine captureException(exception) { // Make sure sentry gets the error - assert.strictEqual(exception, errorToThrow); + expect(exception).toStrictEqual(errorToThrow); }, }, }); - assert.strictEqual(await response.text(), 'response from second middleware'); + expect(await response.text()).toStrictEqual( + 'response from second middleware' + ); }); diff --git a/src/routes/router.ts b/src/routes/router.ts index 5d12d18..c87c088 100644 --- a/src/routes/router.ts +++ b/src/routes/router.ts @@ -74,7 +74,6 @@ function buildMiddlewareChain(middlewares: Middleware[]): MiddlewareChain { }; // Link the middlewares in reverse order for simplicity sakes - // @ts-expect-error TODO: update types so toReversed is recognized for (const middleware of middlewares.toReversed()) { const wrappedMiddleware = errorHandled(middleware); @@ -99,6 +98,7 @@ async function callMiddlewareChain( ): Promise { // Parse url here so we don't have to do it multiple times later on const url = parseUrl(request); + if (url === undefined) { return responses.badRequest(); } @@ -126,6 +126,7 @@ function errorHandled(middleware: Middleware): Middleware { if (ctx.sentry !== undefined) { ctx.sentry.captureException(err); } + return next(); } }, diff --git a/src/utils/cache.ts b/src/utils/cache.ts deleted file mode 100644 index d71998c..0000000 --- a/src/utils/cache.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { Env } from '../env'; - -/** - * @param env Worker env - * @returns True if we want to either cache files or - * directory listings - */ -export function isCacheEnabled(env: Pick): boolean { - return env.ENVIRONMENT !== 'e2e-tests'; -} diff --git a/src/utils/directoryListing.ts b/src/utils/directoryListing.ts index e7d0e11..f4c9420 100644 --- a/src/utils/directoryListing.ts +++ b/src/utils/directoryListing.ts @@ -50,12 +50,9 @@ type TableElement = { function renderSubdirectory(name: string): TableElement { // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String#utf-16_characters_unicode_code_points_and_grapheme_clusters - // @ts-expect-error isWellFormed not recognized - // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions const wellFormedName: string = name.isWellFormed() ? name - : // @ts-expect-error toWellFormed not recognized - name.toWellFormed(); + : name.toWellFormed(); const href = encodeURIComponent( wellFormedName.substring(0, wellFormedName.length - 1) diff --git a/src/utils/http-date.test.ts b/src/utils/http-date.test.ts new file mode 100644 index 0000000..0836fef --- /dev/null +++ b/src/utils/http-date.test.ts @@ -0,0 +1,79 @@ +import { test, expect } from 'vitest'; +import { parseHttpDate } from './http-date'; + +test('IMF-fixdate', () => { + const values = { + 'Sun, 06 Nov 1994 08:49:37 GMT': new Date(Date.UTC(1994, 10, 6, 8, 49, 37)), + 'Thu, 18 Aug 1950 02:01:18 GMT': new Date(Date.UTC(1950, 7, 18, 2, 1, 18)), + 'Wed, 11 Dec 2024 23:20:57 GMT': new Date( + Date.UTC(2024, 11, 11, 23, 20, 57) + ), + 'Wed, aa Dec 2024 23:20:57 GMT': undefined, // NaN daty + 'aaa, 06 Dec 2024 23:20:57 GMT': undefined, // Invalid day name + 'Wed, 01 aaa 2024 23:20:57 GMT': undefined, // Invalid month + 'Wed, 6 Dec 2024 23:20:07 GMT': undefined, // No leading zero + 'Wed, 06 Dec 2024 3:20:07 GMT': undefined, // No leading zero + 'Wed, 06 Dec 2024 23:1:07 GMT': undefined, // No leading zero + 'Wed, 06 Dec 2024 23:01:7 GMT': undefined, // No leading zero + 'Wed, 06 Dec aaaa 23:01:07 GMT': undefined, // NaN year + 'Wed, 06 Dec 2024 aa:01:07 GMT': undefined, // NaN hour + 'Wed, 06 Dec 2024 23:aa:07 GMT': undefined, // NaN min + 'Wed, 06 Dec 2024 23:01:aa GMT': undefined, // NaN sec + } as const; + + for (const date of Object.keys(values) as Array) { + expect(parseHttpDate(date)).toStrictEqual(values[date]); + } +}); + +test('RFC850', () => { + const values = { + 'Sunday, 06-Nov-94 08:49:37 GMT': new Date( + Date.UTC(1994, 10, 6, 8, 49, 37) + ), + 'Thursday, 18-Aug-50 02:01:18 GMT': new Date( + Date.UTC(2050, 7, 18, 2, 1, 18) + ), + 'Wednesday, 11-Dec-24 23:20:57 GMT': new Date( + Date.UTC(2024, 11, 11, 23, 20, 57) + ), + 'Wednesday, aa Dec 2024 23:20:57 GMT': undefined, // NaN daty + 'aaa, 06 Dec 2024 23:20:57 GMT': undefined, // Invalid day name + 'Wednesday, 01-aaa-24 23:20:57 GMT': undefined, // Invalid month + 'Wednesday, 6-Dec-24 23:20:07 GMT': undefined, // No leading zero + 'Wednesday, 06-Dec-24 3:20:07 GMT': undefined, // No leading zero + 'Wednesday, 06-Dec-24 23:1:07 GMT': undefined, // No leading zero + 'Wednesday, 06-Dec-24 23:01:7 GMT': undefined, // No leading zero + 'Wednesday, 06 Dec-aa 23:01:07 GMT': undefined, // NaN year + 'Wednesday, 06-Dec-24 aa:01:07 GMT': undefined, // NaN hour + 'Wednesday, 06-Dec-24 23:aa:07 GMT': undefined, // NaN min + 'Wednesday, 06-Dec-24 23:01:aa GMT': undefined, // NaN sec + }; + + for (const date of Object.keys(values) as Array) { + expect(parseHttpDate(date)).toStrictEqual(values[date]); + } +}); + +test('asctime()', () => { + const values = { + 'Sun Nov 6 08:49:37 1994': new Date(Date.UTC(1994, 10, 6, 8, 49, 37)), + 'Thu Aug 18 02:01:18 1950': new Date(Date.UTC(1950, 7, 18, 2, 1, 18)), + 'Wed Dec 11 23:20:57 2024': new Date(Date.UTC(2024, 11, 11, 23, 20, 57)), + 'Wed Dec aa 23:20:57 2024': undefined, // NaN daty + 'aaa Dec 06 23:20:57 2024': undefined, // Invalid day name + 'Wed aaa 01 23:20:57 2024': undefined, // Invalid month + 'Wed Dec 6 23:20:07 2024': undefined, // No leading zero + 'Wed Dec 06 3:20:07 2024': undefined, // No leading zero + 'Wed Dec 06 23:1:07 2024': undefined, // No leading zero + 'Wed Dec 06 23:01:7 2024': undefined, // No leading zero + 'Wed 06 Dec 23:01:07 aaaa': undefined, // NaN year + 'Wed Dec 06 aa:01:07 2024': undefined, // NaN hour + 'Wed Dec 06 23:aa:07 2024': undefined, // NaN min + 'Wed Dec 06 23:01:aa 2024': undefined, // NaN sec + }; + + for (const date of Object.keys(values) as Array) { + expect(parseHttpDate(date)).toStrictEqual(values[date]); + } +}); diff --git a/src/utils/memo.test.ts b/src/utils/memo.test.ts new file mode 100644 index 0000000..352155f --- /dev/null +++ b/src/utils/memo.test.ts @@ -0,0 +1,18 @@ +import { expect, test } from 'vitest'; +import { once } from './memo'; + +test('once()', () => { + let callCount = 0; + const getString = once(() => { + callCount++; + return 'asd123'; + }); + + const str = getString(); + expect(str).toStrictEqual('asd123'); + expect(callCount).toEqual(1); + + const str2 = getString(); + expect(str2).toStrictEqual(str); + expect(callCount).toEqual(1); +}); diff --git a/src/utils/object.test.ts b/src/utils/object.test.ts new file mode 100644 index 0000000..61c7872 --- /dev/null +++ b/src/utils/object.test.ts @@ -0,0 +1,29 @@ +import { describe, test, expect } from 'vitest'; +import { toReadableBytes } from './object'; + +describe('toReadableBytes', () => { + test('converts 10 bytes to `10 B`', () => { + const result = toReadableBytes(10); + expect(result).toStrictEqual('10 B'); + }); + + test('converts 1 KiB to `1.0 KB`', () => { + const result = toReadableBytes(1024); + expect(result).toStrictEqual('1.0 KB'); + }); + + test('converts 1 MiB to `1.0 MB`', () => { + const result = toReadableBytes(1024 * 1024); + expect(result).toStrictEqual('1.0 MB'); + }); + + test('converts 1 GiB to `1.1 GB`', () => { + const result = toReadableBytes(1024 * 1024 * 1024); + expect(result).toStrictEqual('1.1 GB'); + }); + + test('converts 1 TiB to `1.1 TB`', () => { + const result = toReadableBytes(1024 * 1024 * 1024 * 1024); + expect(result).toStrictEqual('1.1 TB'); + }); +}); diff --git a/src/utils/path.test.ts b/src/utils/path.test.ts new file mode 100644 index 0000000..32704d6 --- /dev/null +++ b/src/utils/path.test.ts @@ -0,0 +1,36 @@ +import { describe, test, expect } from 'vitest'; +import { isDirectoryPath } from './path'; + +describe('isDirectoryPath', () => { + test('returns true for `/dist/`', () => { + expect(isDirectoryPath('/dist/')).toEqual(true); + }); + + test('returns true for `/dist`', () => { + expect(isDirectoryPath('/dist')).toEqual(true); + }); + + test('returns true for `/dist/latest-v20.x`', () => { + expect(isDirectoryPath('/dist/latest-v20.x')).toEqual(true); + }); + + test('returns true for `/dist/v20.20.2`', () => { + expect(isDirectoryPath('/dist/v20.20.2')).toEqual(true); + }); + + test('returns false for `/dist/index.json`', () => { + expect(isDirectoryPath('/dist/index.json')).toEqual(false); + }); + + // https://github.com/nodejs/release-cloudflare-worker/issues/71 + test('returns false for `/download/release/latest/win-x64/node_pdb.7z`', () => { + expect( + isDirectoryPath('/download/release/latest/win-x64/node_pdb.7z') + ).toEqual(false); + }); + + // https://github.com/nodejs/release-cloudflare-worker/issues/99 + test('returns true for `/docs/latest/api`', () => { + expect(isDirectoryPath('/docs/latest/api')).toEqual(true); + }); +}); diff --git a/src/utils/request.test.ts b/src/utils/request.test.ts new file mode 100644 index 0000000..057db40 --- /dev/null +++ b/src/utils/request.test.ts @@ -0,0 +1,73 @@ +import { describe, expect, test } from 'vitest'; +import { parseConditionalHeaders, parseRangeHeader } from './request'; + +describe('parseRangeHeader', () => { + test('`bytes=0-10`', () => { + const result = parseRangeHeader('bytes=0-10'); + + expect(result).toStrictEqual({ + offset: 0, + length: 11, + }); + }); + + test('`bytes=0-10, 15-20, 20-30`', () => { + const result = parseRangeHeader('bytes=0-10, 15-20, 20-30'); + expect(result).toBeDefined(); + + expect(result).toStrictEqual({ + offset: 0, + length: 11, + }); + }); + + test('`bytes=0-`', () => { + const result = parseRangeHeader('bytes=0-'); + expect(result).toBeDefined(); + + expect(result).toStrictEqual({ + offset: 0, + }); + }); + + test('`bytes=-10`', () => { + const result = parseRangeHeader('bytes=-10'); + + expect(result).toStrictEqual({ suffix: 10 }); + }); + + test('`bytes=-`', () => { + const result = parseRangeHeader('bytes=-'); + + expect(result).toStrictEqual(undefined); + }); + + test('`some-other-unit=-`', () => { + const result = parseRangeHeader('some-other-unit=-'); + + expect(result).toStrictEqual(undefined); + }); + + test('`bytes=10-0`', () => { + const result = parseRangeHeader('bytes=10-0'); + + expect(result).toStrictEqual(undefined); + }); +}); + +describe('parseConditionalHeaders', () => { + test('invalid dates', () => { + const headers = new Headers({ + 'if-modified-since': 'asd', + 'if-unmodified-since': 'asd', + }); + + expect(parseConditionalHeaders(headers)).toStrictEqual({ + ifMatch: undefined, + ifNoneMatch: undefined, + ifModifiedSince: undefined, + ifUnmodifiedSince: undefined, + range: undefined, + }); + }); +}); diff --git a/src/worker.ts b/src/worker.ts index 13eef27..4c616de 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -1,4 +1,3 @@ -import { WorkerEntrypoint } from 'cloudflare:workers'; import { Toucan } from 'toucan-js'; import type { Env } from './env'; import responses from './responses'; @@ -9,22 +8,41 @@ import { registerRoutes } from './routes'; const router: Router = new Router(); registerRoutes(router); -export default class extends WorkerEntrypoint { - async fetch(request: Request): Promise { +export default { + async fetch( + request: Request, + env: Env, + ctx: ExecutionContext + ): Promise { const sentry = new Toucan({ - dsn: this.env.SENTRY_DSN, + dsn: env.SENTRY_DSN, request, - context: this.ctx, + context: ctx, requestDataOptions: { allowedHeaders: true, }, }); + if (env.LOG_ERRORS === true) { + const originalCaptureException = sentry.captureException.bind(sentry); + + sentry.captureException = (exception, hint): string => { + const exceptionStr = + exception instanceof Error ? exception.stack : exception; + + console.error( + `sentry.captureException called (hint=${hint}): ${exceptionStr}` + ); + + return originalCaptureException(exception, hint); + }; + } + try { const context: Context = { sentry, - env: this.env, - execution: this.ctx, + env: env, + execution: ctx, }; return await router.handle(request, context); @@ -32,7 +50,7 @@ export default class extends WorkerEntrypoint { // Send to sentry, if it's disabled this will just noop sentry.captureException(e); - return responses.internalServerError(e, this.env); + return responses.internalServerError(e, env); } - } -} + }, +}; diff --git a/tests/README.md b/tests/README.md deleted file mode 100644 index c34c68b..0000000 --- a/tests/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Tests - -Tests use Node's builtin test runner. See [https://nodejs.org/api/test.html](https://nodejs.org/api/test.html) for documentation. diff --git a/tests/e2e/README.md b/tests/e2e/README.md deleted file mode 100644 index 488d730..0000000 --- a/tests/e2e/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# End-to-End Tests - -These are for testing the entire worker from client to server. -The [Miniflare 3 API](https://latest.miniflare.dev) is used to spin up a local -[Workerd](https://github.com/cloudflare/workerd) instance that the tests interact with. - -Test file names correspond to what's being tested. For example, [./directory.test.ts](./directory.test.ts) -contains tests related to directory listing or directories as a whole. - -## Adding a New Test File - -Create the file and name it to something that's fitting to what it will be testing. -For simplicity sakes, take a look at the already existing test files and copy the structure. -Make sure to import the new test file into [./index.test.ts](./index.test.ts). - -## Adding a New Test - -Each E2E test has one big `describe` call that contains all of its tests, add tests in that. -We usually prefer the `it` alias for defining tests. diff --git a/tests/e2e/directory.test.ts b/tests/e2e/directory.test.ts deleted file mode 100644 index 97a7783..0000000 --- a/tests/e2e/directory.test.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { after, before, describe, it } from 'node:test'; -import assert from 'node:assert'; -import { readFileSync } from 'node:fs'; -import { readFile } from 'node:fs/promises'; -import http from 'http'; -import { Miniflare } from 'miniflare'; - -/** - * We make use of the S3 api for directory listing due to - * a size limit in the responses that bindings can return. - * So, we need to mock the ListObjectsV2 call that we send - * to the S3 api. - */ -async function startS3Mock(): Promise { - const server = http.createServer((req, res) => { - const url = new URL(req.url!, `http://${req.headers.host}`); - - let xmlFilePath = './tests/e2e/test-data/expected-s3/'; - - // Check if it's a path that's supposed to exist in - // later tests. If so, return a S3 response indicating that - // the path exists. Otherwise return a S3 response indicating - // that the path doesn't exist - const r2Prefix = url.searchParams.get('prefix')!; - - let doesFolderExist = - [ - 'nodejs/release/v1.0.0/', - 'nodejs/', - 'nodejs/docs/', - 'metrics/', - ].includes(r2Prefix) || r2Prefix.endsWith('/docs/api/'); - - if (doesFolderExist) { - xmlFilePath += 'ListObjectsV2-exists.xml'; - } else { - xmlFilePath += 'ListObjectsV2-does-not-exist.xml'; - } - - const listObjectsResponse = readFileSync(xmlFilePath, { - encoding: 'utf-8', - }); - - res.write(listObjectsResponse); - res.end(); - }); - server.listen(8080); - - return server; -} - -describe('Directory Tests (Restricted Directory Listing)', () => { - let s3Mock: http.Server; - let mf: Miniflare; - let url: URL; - before(async () => { - s3Mock = await startS3Mock(); - - // Setup miniflare - mf = new Miniflare({ - scriptPath: './dist/worker.js', - modules: true, - bindings: { - ENVIRONMENT: 'e2e-tests', - BUCKET_NAME: 'dist-prod', - // S3_ENDPOINT needs to be an ip here otherwise s3 sdk will try to hit - // the bucket's subdomain (e.g. http://dist-prod.localhost) - S3_ENDPOINT: 'http://127.0.0.1:8080', - S3_ACCESS_KEY_ID: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', - S3_ACCESS_KEY_SECRET: - 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', - }, - r2Persist: './tests/e2e/test-data', - r2Buckets: ['R2_BUCKET'], - }); - - // Wait for it Miniflare to start - url = await mf.ready; - }); - - it('redirects `/dist` to `/dist/` and returns expected html', async () => { - const originalRes = await mf.dispatchFetch(`${url}dist`, { - redirect: 'manual', - }); - - assert.strictEqual(originalRes.status, 301); - const res = await mf.dispatchFetch(originalRes.headers.get('location')!); - assert.strictEqual(res.status, 200); - assert.strictEqual( - res.headers.get('cache-control'), - 'public, max-age=3600, s-maxage=14400' - ); - }); - - it('`/dist/v1.0.0/` returns expected html', async () => { - const [res, expectedHtml] = await Promise.all([ - mf.dispatchFetch(`${url}dist/v1.0.0/`), - readFile('./tests/e2e/test-data/expected-html/dist.txt', { - encoding: 'utf-8', - }), - ]); - - assert.strictEqual(res.status, 200); - assert.strictEqual( - res.headers.get('cache-control'), - 'public, max-age=3600, s-maxage=14400' - ); - - // Assert that the html matches what we're expecting - // to be returned. If this passes, we can assume - // it'll pass for the other listings and therefore - // don't need to test it over and over again - const body = await res.text(); - assert.strictEqual( - body.replaceAll('\r', ''), - expectedHtml.replaceAll('\r', '') - ); - }); - - it('allows `/dist/`', async () => { - const res = await mf.dispatchFetch(`${url}dist/`); - - assert.strictEqual(res.status, 200); - }); - - it('allows HEAD `/dist` and returns no body', async () => { - const res = await mf.dispatchFetch(`${url}dist/`, { - method: 'HEAD', - }); - assert.strictEqual(res.status, 200); - - const body = await res.text(); - assert.strictEqual(body.length, 0); - }); - - it('redirects `/download` to `/download/`', async () => { - const originalRes = await mf.dispatchFetch(`${url}download`, { - redirect: 'manual', - }); - assert.strictEqual(originalRes.status, 301); - const res = await mf.dispatchFetch(originalRes.headers.get('location')!); - assert.strictEqual(res.status, 200); - }); - - it('allows `/download/`', async () => { - const res = await mf.dispatchFetch(`${url}download/`); - assert.strictEqual(res.status, 200); - }); - - it('allows `/docs/`', async () => { - const res = await mf.dispatchFetch(`${url}docs/`); - assert.strictEqual(res.status, 200); - }); - - it('allows `/api/`', async () => { - const res = await mf.dispatchFetch(`${url}api/`); - assert.strictEqual(res.status, 200); - }); - - it('redirects `/metrics` to `/metrics/`', async () => { - const originalRes = await mf.dispatchFetch(`${url}metrics`, { - redirect: 'manual', - }); - assert.strictEqual(originalRes.status, 301); - const res = await mf.dispatchFetch(originalRes.headers.get('location')!); - assert.strictEqual(res.status, 200); - }); - - it('allows `/metrics/`', async () => { - const res = await mf.dispatchFetch(`${url}metrics/`); - assert.strictEqual(res.status, 200); - }); - - it('returns 404 for unknown directory', async () => { - const res = await mf.dispatchFetch(`${url}/dist/asd123/`); - assert.strictEqual(res.status, 404); - - const body = await res.text(); - assert.strictEqual(body, 'Directory not found'); - assert.strictEqual( - res.headers.get('cache-control'), - 'private, no-cache, no-store, max-age=0, must-revalidate' - ); - }); - - // Cleanup Miniflare - after(async () => { - await mf.dispose(); - s3Mock.close(); - }); -}); diff --git a/tests/e2e/fallback.test.ts b/tests/e2e/fallback.test.ts deleted file mode 100644 index 3fe02a9..0000000 --- a/tests/e2e/fallback.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { after, before, describe, it } from 'node:test'; -import assert from 'node:assert'; -import { Miniflare } from 'miniflare'; -import http from 'node:http'; - -const FILE_PATH_TO_TEST = 'dist/index.json'; -let fallbackFilePathHit = false; - -const DIRECTORY_TO_TEST = 'download/v1.0.0/'; -let fallbackDirectoryPathHit = false; - -function startfallbackMock(): http.Server { - const server = http.createServer((req, res) => { - const url = new URL(req.url!, `http://${req.headers.host}`); - - if (url.pathname === `/${FILE_PATH_TO_TEST}`) { - fallbackFilePathHit = true; - res.write('test file'); - } else if (url.pathname === `/${DIRECTORY_TO_TEST}`) { - fallbackDirectoryPathHit = true; - res.write('test directory'); - } - - res.end(); - }); - server.listen(8081); - - return server; -} - -describe('Fallback tests', () => { - let fallbackMock: http.Server; - let mf: Miniflare; - let url: URL; - before(async () => { - // Start www mock - fallbackMock = startfallbackMock(); - - // Setup miniflare - mf = new Miniflare({ - scriptPath: './dist/worker.js', - modules: true, - bindings: { - ENVIRONMENT: 'e2e-tests', - ORIGIN_HOST: `http://127.0.0.1:8081`, - }, - }); - - // Wait for Miniflare to start - url = await mf.ready; - }); - - it('grabs file from fallback server if r2 requests fail', async () => { - const res = await mf.dispatchFetch(url + FILE_PATH_TO_TEST); - assert.strictEqual(res.status, 200); - assert.strictEqual(fallbackFilePathHit, true); - - const body = await res.text(); - assert.strictEqual(body, 'test file'); - }); - - it('grabs directory from fallback server if r2 requests fail', async () => { - const res = await mf.dispatchFetch(url + DIRECTORY_TO_TEST); - assert.strictEqual(res.status, 200); - assert.strictEqual(fallbackDirectoryPathHit, true); - - const body = await res.text(); - assert.strictEqual(body, 'test directory'); - }); - - // Cleanup Miniflare and fallback mock - after(async () => { - await mf.dispose(); - fallbackMock.close(); - }); -}); diff --git a/tests/e2e/file.test.ts b/tests/e2e/file.test.ts deleted file mode 100644 index ef2aec5..0000000 --- a/tests/e2e/file.test.ts +++ /dev/null @@ -1,190 +0,0 @@ -import { after, before, describe, it } from 'node:test'; -import assert from 'node:assert'; -import { Miniflare } from 'miniflare'; - -describe('File Tests', () => { - let mf: Miniflare; - let url: URL; - before(async () => { - // Setup miniflare - mf = new Miniflare({ - scriptPath: './dist/worker.js', - modules: true, - bindings: { - ENVIRONMENT: 'e2e-tests', - }, - r2Persist: './tests/e2e/test-data', - r2Buckets: ['R2_BUCKET'], - }); - - // Wait for Miniflare to start - url = await mf.ready; - }); - - it('`/dist/index.json` returns expected body, status code, and headers', async () => { - const res = await mf.dispatchFetch(`${url}dist/index.json`); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.headers.get('content-type'), 'application/json'); - assert.strictEqual( - res.headers.get('cache-control'), - 'public, max-age=3600, s-maxage=14400' - ); - assert.strictEqual(res.headers.has('etag'), true); - assert.strictEqual(res.headers.has('last-modified'), true); - assert.strictEqual(res.headers.has('content-type'), true); - - const body = await res.text(); - assert.strictEqual(body, `{ hello: 'world' }`); - }); - - it('HEAD `/dist/index.json` returns no body and status code 200', async () => { - const res = await mf.dispatchFetch(`${url}dist/index.json`, { - method: 'HEAD', - }); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.headers.get('content-type'), 'application/json'); - assert.strictEqual( - res.headers.get('cache-control'), - 'public, max-age=3600, s-maxage=14400' - ); - assert.strictEqual(res.headers.has('etag'), true); - assert.strictEqual(res.headers.has('last-modified'), true); - assert.strictEqual(res.headers.has('content-type'), true); - assert.strictEqual(res.headers.has('x-cache-status'), false); - - const body = await res.text(); - assert.strictEqual(body.length, 0); - }); - - it('returns 404 for unknown file', async () => { - const res = await mf.dispatchFetch(`${url}dist/asd123.json`); - assert.strictEqual(res.status, 404); - - const body = await res.text(); - assert.strictEqual(body, 'File not found'); - assert.strictEqual( - res.headers.get('cache-control'), - 'private, no-cache, no-store, max-age=0, must-revalidate' - ); - }); - - /** - * R2 supports all conditional headers except If-Range - * @see https://developers.cloudflare.com/r2/api/workers/workers-api-reference/#conditional-operations - * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Conditional_requests#conditional_headers - */ - it('handles if-modified-since correctly', async () => { - let res = await mf.dispatchFetch(`${url}dist/index.json`); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.headers.has('last-modified'), true); - - const date = new Date(res.headers.get('last-modified')!); - date.setSeconds(date.getSeconds() + 1); - - // Make sure it returns a 304 when If-Modified-Since - // >= file last modified - res = await mf.dispatchFetch(`${url}dist/index.json`, { - headers: { - 'if-modified-since': date.toUTCString(), - }, - }); - assert.strictEqual(res.status, 304); - - // Make sure it returns a 200 w/ the file contents - // when If-Modified-Since < file last modified - res = await mf.dispatchFetch(`${url}dist/index.json`, { - headers: { - 'if-modified-since': new Date(0).toUTCString(), - }, - }); - assert.strictEqual(res.status, 200); - }); - - it('handles if-unmodified-since header correctly', async () => { - const res = await mf.dispatchFetch(`${url}dist/index.json`, { - headers: { - 'if-unmodified-since': new Date(0).toUTCString(), - }, - }); - assert.strictEqual(res.status, 412); - assert.strictEqual( - res.headers.get('cache-control'), - 'private, no-cache, no-store, max-age=0, must-revalidate' - ); - }); - - it('handles if-match correctly', async () => { - let res = await mf.dispatchFetch(`${url}dist/index.json`); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.headers.has('etag'), true); - - const originalETag = res.headers.get('etag')!; - - res = await mf.dispatchFetch(`${url}dist/index.json`, { - headers: { - 'if-match': '"asd"', - }, - }); - assert.strictEqual(res.status, 304); - assert.strictEqual( - res.headers.get('cache-control'), - 'private, no-cache, no-store, max-age=0, must-revalidate' - ); - - // If-Match w/ valid etag returns 200 - res = await mf.dispatchFetch(`${url}dist/index.json`, { - headers: { - 'if-match': originalETag, - }, - }); - assert.strictEqual(res.status, 200); - }); - - it('handles if-none-match correctly', async () => { - let res = await mf.dispatchFetch(`${url}dist/index.json`); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.headers.has('etag'), true); - - const originalETag = res.headers.get('etag')!; - - res = await mf.dispatchFetch(`${url}dist/index.json`, { - headers: { - 'if-none-match': '"asd"', - }, - }); - assert.strictEqual(res.status, 200); - - // If-None-Match w/ valid etag returns 304 or 412 - res = await mf.dispatchFetch(`${url}dist/index.json`, { - headers: { - 'if-none-match': originalETag, - }, - }); - assert(res.status === 304); - }); - - it('handles range header correctly', async () => { - const res = await mf.dispatchFetch(`${url}dist/index.json`, { - headers: { - range: 'bytes=0-7', - }, - }); - assert.strictEqual(res.status, 206); - - const body = await res.text(); - assert.strictEqual(body, '{ hello:'); - }); - - it('sends a 405 for anything other than GET or HEAD', async () => { - // Doesn't need to be all-inclusive - for (const method of ['POST', 'PATCH', 'DELETE', 'PROPFIND']) { - const res = await mf.dispatchFetch(`${url}`, { - method: method, - }); - assert.strictEqual(res.status, 405, method); - } - }); - - // Cleanup Miniflare - after(async () => mf.dispose()); -}); diff --git a/tests/e2e/index.test.ts b/tests/e2e/index.test.ts deleted file mode 100644 index 99d5418..0000000 --- a/tests/e2e/index.test.ts +++ /dev/null @@ -1,3 +0,0 @@ -import './directory.test'; -import './file.test'; -import './fallback.test'; diff --git a/tests/e2e/test-data/R2_BUCKET/README.md b/tests/e2e/test-data/R2_BUCKET/README.md deleted file mode 100644 index 338fdcf..0000000 --- a/tests/e2e/test-data/R2_BUCKET/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# R2_BUCKET - -This is a local mock of a R2 bucket with the dist folder's contents. diff --git a/tests/e2e/test-data/R2_BUCKET/blobs/1e4de7990b4ef01257964a2f7d12b8551db3af2fd134f82c2b3f872824a9aac500000cea511db274 b/tests/e2e/test-data/R2_BUCKET/blobs/1e4de7990b4ef01257964a2f7d12b8551db3af2fd134f82c2b3f872824a9aac500000cea511db274 deleted file mode 100644 index f7ab40c..0000000 --- a/tests/e2e/test-data/R2_BUCKET/blobs/1e4de7990b4ef01257964a2f7d12b8551db3af2fd134f82c2b3f872824a9aac500000cea511db274 +++ /dev/null @@ -1 +0,0 @@ -asd123 \ No newline at end of file diff --git a/tests/e2e/test-data/R2_BUCKET/blobs/2b8de93bc9a59f7b74004036973e87ede6a8985145b5b10c87db0b71310736d500000cea51c8d30c b/tests/e2e/test-data/R2_BUCKET/blobs/2b8de93bc9a59f7b74004036973e87ede6a8985145b5b10c87db0b71310736d500000cea51c8d30c deleted file mode 100644 index f7ab40c..0000000 --- a/tests/e2e/test-data/R2_BUCKET/blobs/2b8de93bc9a59f7b74004036973e87ede6a8985145b5b10c87db0b71310736d500000cea51c8d30c +++ /dev/null @@ -1 +0,0 @@ -asd123 \ No newline at end of file diff --git a/tests/e2e/test-data/R2_BUCKET/blobs/452c0bddf19b5647831280c01a090c815526157e8eb6cc71b76f951576ddcfff00000cea50e229d4 b/tests/e2e/test-data/R2_BUCKET/blobs/452c0bddf19b5647831280c01a090c815526157e8eb6cc71b76f951576ddcfff00000cea50e229d4 deleted file mode 100644 index ee64844..0000000 --- a/tests/e2e/test-data/R2_BUCKET/blobs/452c0bddf19b5647831280c01a090c815526157e8eb6cc71b76f951576ddcfff00000cea50e229d4 +++ /dev/null @@ -1 +0,0 @@ -123asd \ No newline at end of file diff --git a/tests/e2e/test-data/R2_BUCKET/blobs/755deadb8510a4bcf8b632106ce3bb2884cbb19628ff06a4b2c4cd7a15e9711800000cea51dc0fa8 b/tests/e2e/test-data/R2_BUCKET/blobs/755deadb8510a4bcf8b632106ce3bb2884cbb19628ff06a4b2c4cd7a15e9711800000cea51dc0fa8 deleted file mode 100644 index f7ab40c..0000000 --- a/tests/e2e/test-data/R2_BUCKET/blobs/755deadb8510a4bcf8b632106ce3bb2884cbb19628ff06a4b2c4cd7a15e9711800000cea51dc0fa8 +++ /dev/null @@ -1 +0,0 @@ -asd123 \ No newline at end of file diff --git a/tests/e2e/test-data/R2_BUCKET/blobs/7637f4431321361e34e8bdba83dfc0b7c4aad8c3619d895b7fcfac1cbea3bae700000cea51681f30 b/tests/e2e/test-data/R2_BUCKET/blobs/7637f4431321361e34e8bdba83dfc0b7c4aad8c3619d895b7fcfac1cbea3bae700000cea51681f30 deleted file mode 100644 index f7ab40c..0000000 --- a/tests/e2e/test-data/R2_BUCKET/blobs/7637f4431321361e34e8bdba83dfc0b7c4aad8c3619d895b7fcfac1cbea3bae700000cea51681f30 +++ /dev/null @@ -1 +0,0 @@ -asd123 \ No newline at end of file diff --git a/tests/e2e/test-data/R2_BUCKET/blobs/7cd7aaa22139ae89634e0a4f695745e7d195ce9c16d9c865684739f46029ba3900000cea5198fe20 b/tests/e2e/test-data/R2_BUCKET/blobs/7cd7aaa22139ae89634e0a4f695745e7d195ce9c16d9c865684739f46029ba3900000cea5198fe20 deleted file mode 100644 index f7ab40c..0000000 --- a/tests/e2e/test-data/R2_BUCKET/blobs/7cd7aaa22139ae89634e0a4f695745e7d195ce9c16d9c865684739f46029ba3900000cea5198fe20 +++ /dev/null @@ -1 +0,0 @@ -asd123 \ No newline at end of file diff --git a/tests/e2e/test-data/R2_BUCKET/blobs/84ec646e6ee82c6a2b627a3c7988a1e630e43c3b53644c20f30f90cc1bdc4ff300000cea51b2f884 b/tests/e2e/test-data/R2_BUCKET/blobs/84ec646e6ee82c6a2b627a3c7988a1e630e43c3b53644c20f30f90cc1bdc4ff300000cea51b2f884 deleted file mode 100644 index f7ab40c..0000000 --- a/tests/e2e/test-data/R2_BUCKET/blobs/84ec646e6ee82c6a2b627a3c7988a1e630e43c3b53644c20f30f90cc1bdc4ff300000cea51b2f884 +++ /dev/null @@ -1 +0,0 @@ -asd123 \ No newline at end of file diff --git a/tests/e2e/test-data/R2_BUCKET/blobs/8dc8517f83d7d2ac8a3030da0eedd2c4956c4e3e0391bd3b94cc0701c7ddb54100000cea520b6ca8 b/tests/e2e/test-data/R2_BUCKET/blobs/8dc8517f83d7d2ac8a3030da0eedd2c4956c4e3e0391bd3b94cc0701c7ddb54100000cea520b6ca8 deleted file mode 100644 index f7ab40c..0000000 --- a/tests/e2e/test-data/R2_BUCKET/blobs/8dc8517f83d7d2ac8a3030da0eedd2c4956c4e3e0391bd3b94cc0701c7ddb54100000cea520b6ca8 +++ /dev/null @@ -1 +0,0 @@ -asd123 \ No newline at end of file diff --git a/tests/e2e/test-data/R2_BUCKET/blobs/9c101170154bc7872e5b93675a74706d03205b791f115401e3b7d2a2a4cd597c00000cea51381330 b/tests/e2e/test-data/R2_BUCKET/blobs/9c101170154bc7872e5b93675a74706d03205b791f115401e3b7d2a2a4cd597c00000cea51381330 deleted file mode 100644 index f7ab40c..0000000 --- a/tests/e2e/test-data/R2_BUCKET/blobs/9c101170154bc7872e5b93675a74706d03205b791f115401e3b7d2a2a4cd597c00000cea51381330 +++ /dev/null @@ -1 +0,0 @@ -asd123 \ No newline at end of file diff --git a/tests/e2e/test-data/R2_BUCKET/blobs/9fbf67cc8db78f5ca67b84b435be22b5a814b29ff344e19a7e4da43500357d7e00000cea5181ac34 b/tests/e2e/test-data/R2_BUCKET/blobs/9fbf67cc8db78f5ca67b84b435be22b5a814b29ff344e19a7e4da43500357d7e00000cea5181ac34 deleted file mode 100644 index f7ab40c..0000000 --- a/tests/e2e/test-data/R2_BUCKET/blobs/9fbf67cc8db78f5ca67b84b435be22b5a814b29ff344e19a7e4da43500357d7e00000cea5181ac34 +++ /dev/null @@ -1 +0,0 @@ -asd123 \ No newline at end of file diff --git a/tests/e2e/test-data/R2_BUCKET/blobs/b11cbe7ea145ec48630b0d87fbd25b304348a215548c998c4798e73ce8dca44e00000cea51509568 b/tests/e2e/test-data/R2_BUCKET/blobs/b11cbe7ea145ec48630b0d87fbd25b304348a215548c998c4798e73ce8dca44e00000cea51509568 deleted file mode 100644 index f7ab40c..0000000 --- a/tests/e2e/test-data/R2_BUCKET/blobs/b11cbe7ea145ec48630b0d87fbd25b304348a215548c998c4798e73ce8dca44e00000cea51509568 +++ /dev/null @@ -1 +0,0 @@ -asd123 \ No newline at end of file diff --git a/tests/e2e/test-data/R2_BUCKET/blobs/b2b1ba500a2c700706ab817da2f848631db25d34190b88c7c7cb7dd3d63364ca00000cea5084db80 b/tests/e2e/test-data/R2_BUCKET/blobs/b2b1ba500a2c700706ab817da2f848631db25d34190b88c7c7cb7dd3d63364ca00000cea5084db80 deleted file mode 100644 index 1365cc7..0000000 --- a/tests/e2e/test-data/R2_BUCKET/blobs/b2b1ba500a2c700706ab817da2f848631db25d34190b88c7c7cb7dd3d63364ca00000cea5084db80 +++ /dev/null @@ -1 +0,0 @@ -{ hello: 'world' } \ No newline at end of file diff --git a/tests/e2e/test-data/R2_BUCKET/blobs/b9e14d8b54ea621f6549fee94f557cdb43646966979dcf3ac380ba0273de9de400000cea50ffe460 b/tests/e2e/test-data/R2_BUCKET/blobs/b9e14d8b54ea621f6549fee94f557cdb43646966979dcf3ac380ba0273de9de400000cea50ffe460 deleted file mode 100644 index f7ab40c..0000000 --- a/tests/e2e/test-data/R2_BUCKET/blobs/b9e14d8b54ea621f6549fee94f557cdb43646966979dcf3ac380ba0273de9de400000cea50ffe460 +++ /dev/null @@ -1 +0,0 @@ -asd123 \ No newline at end of file diff --git a/tests/e2e/test-data/R2_BUCKET/blobs/e62d82792f96282b1c49fb7cdc70824ff825620ac2fd80566a62d51df363c3a700000cea51ef17ec b/tests/e2e/test-data/R2_BUCKET/blobs/e62d82792f96282b1c49fb7cdc70824ff825620ac2fd80566a62d51df363c3a700000cea51ef17ec deleted file mode 100644 index f7ab40c..0000000 --- a/tests/e2e/test-data/R2_BUCKET/blobs/e62d82792f96282b1c49fb7cdc70824ff825620ac2fd80566a62d51df363c3a700000cea51ef17ec +++ /dev/null @@ -1 +0,0 @@ -asd123 \ No newline at end of file diff --git a/tests/e2e/test-data/R2_BUCKET/blobs/f34b8641aebf56ffe850eaf5d26f9604418e4d48de4766cd903ce22f7485848900000cea50b18770 b/tests/e2e/test-data/R2_BUCKET/blobs/f34b8641aebf56ffe850eaf5d26f9604418e4d48de4766cd903ce22f7485848900000cea50b18770 deleted file mode 100644 index f7ab40c..0000000 --- a/tests/e2e/test-data/R2_BUCKET/blobs/f34b8641aebf56ffe850eaf5d26f9604418e4d48de4766cd903ce22f7485848900000cea50b18770 +++ /dev/null @@ -1 +0,0 @@ -asd123 \ No newline at end of file diff --git a/tests/e2e/test-data/expected-html/README.md b/tests/e2e/test-data/expected-html/README.md deleted file mode 100644 index e6e09c2..0000000 --- a/tests/e2e/test-data/expected-html/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# expected-html - -Expected html for listing responses. These should be 1:1 to what the worker returns diff --git a/tests/e2e/test-data/expected-html/dist.txt b/tests/e2e/test-data/expected-html/dist.txt deleted file mode 100644 index b3786e0..0000000 --- a/tests/e2e/test-data/expected-html/dist.txt +++ /dev/null @@ -1,5 +0,0 @@ -Index of /dist/v1.0.0/

Index of /dist/v1.0.0/


-../
-latest/                                                           -                   -
-index.json                                         12 Sept 2023, 05:43                 18 B
-

\ No newline at end of file diff --git a/tests/e2e/test-data/expected-s3/ListObjectsV2-does-not-exist.xml b/tests/e2e/test-data/expected-s3/ListObjectsV2-does-not-exist.xml deleted file mode 100644 index d040ff3..0000000 --- a/tests/e2e/test-data/expected-s3/ListObjectsV2-does-not-exist.xml +++ /dev/null @@ -1,7 +0,0 @@ - - dist-prod - - - 1000 - false - diff --git a/tests/e2e/test-data/expected-s3/ListObjectsV2-exists.xml b/tests/e2e/test-data/expected-s3/ListObjectsV2-exists.xml deleted file mode 100644 index 6787ab9..0000000 --- a/tests/e2e/test-data/expected-s3/ListObjectsV2-exists.xml +++ /dev/null @@ -1,16 +0,0 @@ - - dist-prod - - - 1000 - false - - nodejs/release/v1.0.0/latest/ - - - "asd123" - nodejs/release/v1.0.0/index.json - 2023-09-12T05:43:00.000Z - 18 - - diff --git a/tests/e2e/test-data/miniflare-R2BucketObject/268e5651c16ea1cde8d42e4b761db201609165df409b5ded2104c71350680395.sqlite b/tests/e2e/test-data/miniflare-R2BucketObject/268e5651c16ea1cde8d42e4b761db201609165df409b5ded2104c71350680395.sqlite deleted file mode 100644 index 5784792..0000000 Binary files a/tests/e2e/test-data/miniflare-R2BucketObject/268e5651c16ea1cde8d42e4b761db201609165df409b5ded2104c71350680395.sqlite and /dev/null differ diff --git a/tests/e2e/test-data/miniflare-R2BucketObject/268e5651c16ea1cde8d42e4b761db201609165df409b5ded2104c71350680395.sqlite-shm b/tests/e2e/test-data/miniflare-R2BucketObject/268e5651c16ea1cde8d42e4b761db201609165df409b5ded2104c71350680395.sqlite-shm deleted file mode 100644 index 5ef7f63..0000000 Binary files a/tests/e2e/test-data/miniflare-R2BucketObject/268e5651c16ea1cde8d42e4b761db201609165df409b5ded2104c71350680395.sqlite-shm and /dev/null differ diff --git a/tests/e2e/test-data/miniflare-R2BucketObject/268e5651c16ea1cde8d42e4b761db201609165df409b5ded2104c71350680395.sqlite-wal b/tests/e2e/test-data/miniflare-R2BucketObject/268e5651c16ea1cde8d42e4b761db201609165df409b5ded2104c71350680395.sqlite-wal deleted file mode 100644 index 43a4d84..0000000 Binary files a/tests/e2e/test-data/miniflare-R2BucketObject/268e5651c16ea1cde8d42e4b761db201609165df409b5ded2104c71350680395.sqlite-wal and /dev/null differ diff --git a/tests/tsconfig.json b/tests/tsconfig.json deleted file mode 100644 index df890c6..0000000 --- a/tests/tsconfig.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "extends": "../tsconfig.json", - "compilerOptions": { - "types": ["node"] - } -} diff --git a/tests/unit/README.md b/tests/unit/README.md deleted file mode 100644 index 2127a5e..0000000 --- a/tests/unit/README.md +++ /dev/null @@ -1,26 +0,0 @@ -# Unit Tests - -These are for testing specific functions in the worker. - -Test file names correspond to the file name in the [`src`](../../src) directory that is being tested. -For example, [`./util.test.ts`](./util.test.ts) tests functions defined from [`src/util.ts`](../../src/util.ts). - -## Adding a New Test File - -Create the file and name it the same name as the file in the [`src`](../../src) directory. -See [Adding a New Test](#adding-a-new-test) for testing the functions. -Make sure to import the new test file into [./index.test.ts](./index.test.ts). - -## Adding a New Test - -Each function has its tests wrapped in a `describe` call just for neatness. -We usually prefer the `it` alias for defining tests. For example, tests for the -function `doSomething` would look something like this: - -```js -describe('doSomething', () => { - it('does something', () => { - /*...*/ - }); -}); -``` diff --git a/tests/unit/index.test.ts b/tests/unit/index.test.ts deleted file mode 100644 index 683c96b..0000000 --- a/tests/unit/index.test.ts +++ /dev/null @@ -1,7 +0,0 @@ -import './utils/object.test'; -import './utils/path.test'; -import './utils/request.test'; -import './utils/memo.test'; -import './utils/http-date.test'; -import './router/router.test'; -import './middleware/substituteMiddleware.test'; diff --git a/tests/unit/middleware/substituteMiddleware.test.ts b/tests/unit/middleware/substituteMiddleware.test.ts deleted file mode 100644 index ad92dc4..0000000 --- a/tests/unit/middleware/substituteMiddleware.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -import assert from 'node:assert'; -import { it } from 'node:test'; -import { SubtitutionMiddleware } from '../../../src/middleware/subtituteMiddleware'; -import type { Request as WorkerRequest } from '../../../src/routes/request'; -import type { Router } from '../../../src/routes'; - -it('correctly substitutes url `/dist/latest` to `/dist/v1.0.0`', async () => { - const originalUrl = 'https://localhost/dist/latest'; - - const originalRequest: Partial = new Request(originalUrl); - originalRequest.urlObj = new URL(originalUrl); - - const router: Partial = { - handle: (substitutedRequest: WorkerRequest, _, unsubstitutedUrl) => { - // Is the url is now substituted (latest -> v1.0.0) - assert.strictEqual( - substitutedRequest.url, - 'https://localhost/dist/v1.0.0' - ); - - // Did we save the unsubstituted path? - assert.strictEqual(unsubstitutedUrl, originalRequest.urlObj); - - return Promise.resolve(new Response()); - }, - }; - - // Pre-checks for sanity - assert.strictEqual(originalRequest.unsubstitutedUrl, undefined); - assert.strictEqual(originalRequest.urlObj!.pathname, '/dist/latest'); - - // @ts-expect-error full router not needed - const middleware = new SubtitutionMiddleware(router, 'latest', 'v1.0.0'); - - // @ts-expect-error full request & ctx not needed - middleware.handle(originalRequest, { - sentry: { - addBreadcrumb: () => {}, - }, - }); -}); diff --git a/tests/unit/utils/http-date.test.ts b/tests/unit/utils/http-date.test.ts deleted file mode 100644 index 12b6f38..0000000 --- a/tests/unit/utils/http-date.test.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { describe, test } from 'node:test'; -import { deepStrictEqual } from 'node:assert'; -import { parseHttpDate } from '../../../src/utils/http-date'; - -describe('parseHttpDate', () => { - test('IMF-fixdate', () => { - const values = { - 'Sun, 06 Nov 1994 08:49:37 GMT': new Date( - Date.UTC(1994, 10, 6, 8, 49, 37) - ), - 'Thu, 18 Aug 1950 02:01:18 GMT': new Date( - Date.UTC(1950, 7, 18, 2, 1, 18) - ), - 'Wed, 11 Dec 2024 23:20:57 GMT': new Date( - Date.UTC(2024, 11, 11, 23, 20, 57) - ), - 'Wed, aa Dec 2024 23:20:57 GMT': undefined, // NaN daty - 'aaa, 06 Dec 2024 23:20:57 GMT': undefined, // Invalid day name - 'Wed, 01 aaa 2024 23:20:57 GMT': undefined, // Invalid month - 'Wed, 6 Dec 2024 23:20:07 GMT': undefined, // No leading zero - 'Wed, 06 Dec 2024 3:20:07 GMT': undefined, // No leading zero - 'Wed, 06 Dec 2024 23:1:07 GMT': undefined, // No leading zero - 'Wed, 06 Dec 2024 23:01:7 GMT': undefined, // No leading zero - 'Wed, 06 Dec aaaa 23:01:07 GMT': undefined, // NaN year - 'Wed, 06 Dec 2024 aa:01:07 GMT': undefined, // NaN hour - 'Wed, 06 Dec 2024 23:aa:07 GMT': undefined, // NaN min - 'Wed, 06 Dec 2024 23:01:aa GMT': undefined, // NaN sec - }; - - for (const date of Object.keys(values)) { - // @ts-expect-error date isn't type keyof values - deepStrictEqual(parseHttpDate(date), values[date], date); - } - }); - - test('RFC850', () => { - const values = { - 'Sunday, 06-Nov-94 08:49:37 GMT': new Date( - Date.UTC(1994, 10, 6, 8, 49, 37) - ), - 'Thursday, 18-Aug-50 02:01:18 GMT': new Date( - Date.UTC(2050, 7, 18, 2, 1, 18) - ), - 'Wednesday, 11-Dec-24 23:20:57 GMT': new Date( - Date.UTC(2024, 11, 11, 23, 20, 57) - ), - 'Wednesday, aa Dec 2024 23:20:57 GMT': undefined, // NaN daty - 'aaa, 06 Dec 2024 23:20:57 GMT': undefined, // Invalid day name - 'Wednesday, 01-aaa-24 23:20:57 GMT': undefined, // Invalid month - 'Wednesday, 6-Dec-24 23:20:07 GMT': undefined, // No leading zero - 'Wednesday, 06-Dec-24 3:20:07 GMT': undefined, // No leading zero - 'Wednesday, 06-Dec-24 23:1:07 GMT': undefined, // No leading zero - 'Wednesday, 06-Dec-24 23:01:7 GMT': undefined, // No leading zero - 'Wednesday, 06 Dec-aa 23:01:07 GMT': undefined, // NaN year - 'Wednesday, 06-Dec-24 aa:01:07 GMT': undefined, // NaN hour - 'Wednesday, 06-Dec-24 23:aa:07 GMT': undefined, // NaN min - 'Wednesday, 06-Dec-24 23:01:aa GMT': undefined, // NaN sec - }; - - for (const date of Object.keys(values)) { - // @ts-expect-error date isn't type keyof values - deepStrictEqual(parseHttpDate(date), values[date], date); - } - }); - - test('asctime()', () => { - const values = { - 'Sun Nov 6 08:49:37 1994': new Date(Date.UTC(1994, 10, 6, 8, 49, 37)), - 'Thu Aug 18 02:01:18 1950': new Date(Date.UTC(1950, 7, 18, 2, 1, 18)), - 'Wed Dec 11 23:20:57 2024': new Date(Date.UTC(2024, 11, 11, 23, 20, 57)), - 'Wed Dec aa 23:20:57 2024': undefined, // NaN daty - 'aaa Dec 06 23:20:57 2024': undefined, // Invalid day name - 'Wed aaa 01 23:20:57 2024': undefined, // Invalid month - 'Wed Dec 6 23:20:07 2024': undefined, // No leading zero - 'Wed Dec 06 3:20:07 2024': undefined, // No leading zero - 'Wed Dec 06 23:1:07 2024': undefined, // No leading zero - 'Wed Dec 06 23:01:7 2024': undefined, // No leading zero - 'Wed 06 Dec 23:01:07 aaaa': undefined, // NaN year - 'Wed Dec 06 aa:01:07 2024': undefined, // NaN hour - 'Wed Dec 06 23:aa:07 2024': undefined, // NaN min - 'Wed Dec 06 23:01:aa 2024': undefined, // NaN sec - }; - - for (const date of Object.keys(values)) { - // @ts-expect-error date isn't type keyof values - deepStrictEqual(parseHttpDate(date), values[date], date); - } - }); -}); diff --git a/tests/unit/utils/memo.test.ts b/tests/unit/utils/memo.test.ts deleted file mode 100644 index 6cdc2ab..0000000 --- a/tests/unit/utils/memo.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import assert from 'node:assert'; -import { it } from 'node:test'; -import { once } from '../../../src/utils/memo'; - -it('once works', () => { - let callCount = 0; - const getString = once(() => { - callCount++; - return 'asd123'; - }); - - const str = getString(); - assert.strictEqual(str, 'asd123'); - assert.strictEqual(callCount, 1); - - const str2 = getString(); - assert.equal(str, str2); - assert.strictEqual(str2, 'asd123'); - assert.strictEqual(callCount, 1); -}); diff --git a/tests/unit/utils/object.test.ts b/tests/unit/utils/object.test.ts deleted file mode 100644 index a86a9a4..0000000 --- a/tests/unit/utils/object.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import assert from 'node:assert'; -import { describe, it } from 'node:test'; -import { toReadableBytes } from '../../../src/utils/object'; - -describe('toReadableBytes', () => { - it('converts 10 to `10 B`', () => { - const result = toReadableBytes(10); - assert.strictEqual(result, '10 B'); - }); - - it('converts 1024 to `1.0 KB`', () => { - const result = toReadableBytes(1024); - assert.strictEqual(result, '1.0 KB'); - }); -}); diff --git a/tests/unit/utils/path.test.ts b/tests/unit/utils/path.test.ts deleted file mode 100644 index 887280d..0000000 --- a/tests/unit/utils/path.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -import assert from 'node:assert'; -import { describe, it } from 'node:test'; -import { isDirectoryPath } from '../../../src/utils/path'; - -describe('isDirectoryPath', () => { - it('returns true for `/dist/`', () => { - assert.strictEqual(isDirectoryPath('/dist/'), true); - }); - - it('returns true for `/dist`', () => { - assert.strictEqual(isDirectoryPath('/dist'), true); - }); - - it('returns true for `/dist/latest-v20.x`', () => { - assert.strictEqual(isDirectoryPath('/dist/latest-v20.x'), true); - }); - - it('returns true for `/dist/v20.20.2`', () => { - assert.strictEqual(isDirectoryPath('/dist/v20.20.2'), true); - }); - - it('returns false for `/dist/index.json`', () => { - assert.strictEqual(isDirectoryPath('/dist/index.json'), false); - }); - - // https://github.com/nodejs/release-cloudflare-worker/issues/71 - it('returns false for `/download/release/latest/win-x64/node_pdb.7z`', () => { - assert.strictEqual( - isDirectoryPath('/download/release/latest/win-x64/node_pdb.7z'), - false - ); - }); - - // https://github.com/nodejs/release-cloudflare-worker/issues/99 - it('returns true for `/docs/latest/api`', () => { - assert.strictEqual(isDirectoryPath('/docs/latest/api'), true); - }); -}); diff --git a/tests/unit/utils/request.test.ts b/tests/unit/utils/request.test.ts deleted file mode 100644 index dba7372..0000000 --- a/tests/unit/utils/request.test.ts +++ /dev/null @@ -1,71 +0,0 @@ -import assert from 'node:assert'; -import { describe, it } from 'node:test'; -import { - parseConditionalHeaders, - parseRangeHeader, -} from '../../../src/utils/request'; - -describe('parseRangeHeader', () => { - it('`bytes=0-10`', () => { - const result = parseRangeHeader('bytes=0-10'); - assert.notStrictEqual(result, undefined); - - assert.strictEqual(result.offset, 0); - assert.strictEqual(result.length, 11); - }); - - it('`bytes=0-10, 15-20, 20-30`', () => { - const result = parseRangeHeader('bytes=0-10, 15-20, 20-30'); - assert.notStrictEqual(result, undefined); - - assert.strictEqual(result.offset, 0); - assert.strictEqual(result.length, 11); - }); - - it('`bytes=0-`', () => { - const result = parseRangeHeader('bytes=0-'); - assert.notStrictEqual(result, undefined); - - assert.strictEqual(result.offset, 0); - assert.strictEqual(result.length, undefined); - }); - - it('`bytes=-10`', () => { - const result = parseRangeHeader('bytes=-10'); - assert.notStrictEqual(result, undefined); - - assert.strictEqual(result.suffix, 10); - }); - - it('`bytes=-`', () => { - const result = parseRangeHeader('bytes=-'); - assert.strictEqual(result, undefined); - }); - - it('`some-other-unit=-`', () => { - const result = parseRangeHeader('some-other-unit=-'); - assert.strictEqual(result, undefined); - }); - - it('`bytes=10-0`', () => { - const result = parseRangeHeader('bytes=10-0'); - assert.strictEqual(result, undefined); - }); -}); - -describe('parseConditionalHeaders', () => { - it('invalid dates', () => { - const headers = new Headers({ - 'if-modified-since': 'asd', - 'if-unmodified-since': 'asd', - }); - - assert.deepStrictEqual(parseConditionalHeaders(headers), { - ifMatch: undefined, - ifNoneMatch: undefined, - ifModifiedSince: undefined, - ifUnmodifiedSince: undefined, - range: undefined, - }); - }); -}); diff --git a/tsconfig.json b/tsconfig.json index 124b634..8054bd2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,9 +1,9 @@ { "compilerOptions": { - "target": "es2021", - "lib": ["es2021", "es2022"], + "target": "esnext", + "lib": ["esnext"], "module": "esnext", - "moduleResolution": "node", + "moduleResolution": "Bundler", "types": ["./worker-configuration.d.ts"], "resolveJsonModule": true, "allowJs": true, @@ -13,6 +13,7 @@ "allowSyntheticDefaultImports": true, "forceConsistentCasingInFileNames": true, "strict": true, - "skipLibCheck": true + "skipLibCheck": true, + "erasableSyntaxOnly": true } } diff --git a/vitest-setup.ts b/vitest-setup.ts new file mode 100644 index 0000000..b13c849 --- /dev/null +++ b/vitest-setup.ts @@ -0,0 +1,64 @@ +// This file is used to setup things before we switch into a wranglerd isolate. +// It is ran in Node.js and has access to Node's apis. + +import { dirname, join } from 'node:path'; +import { readdir, readFile, stat } from 'node:fs/promises'; +import type { TestProject } from 'vitest/node'; + +const DEV_BUCKET_PATH = join(import.meta.dirname, 'dev-bucket'); + +/** + * This is called when Vitest is ran and allows us to pass in data that we need + * for the tests + */ +export default async function setup(project: TestProject) { + // Get the contents of the dev bucket + const devBucket = await listDirectory(DEV_BUCKET_PATH); + + // Expose it for the tests + project.provide('devBucket', devBucket); +} + +interface File { + size: number; + lastModified: number; + contents: string; +} + +export interface Directory { + name: string; + subdirectories: Record; + files: Record; +} + +async function listDirectory(directoryPath: string): Promise { + const directory: Directory = { + name: dirname(directoryPath), + subdirectories: {}, + files: {}, + }; + + const paths = await readdir(directoryPath, { recursive: true }); + + for (const path of paths) { + const relativePath = join(directoryPath, path); + + const statResult = await stat(relativePath); + + if (statResult.isFile()) { + const contents = await readFile(relativePath, 'utf8'); + + directory.files[path] = { + size: contents.length, + lastModified: Math.floor(Date.now() / 1000), + contents, + }; + } else { + const subdirectory = await listDirectory(relativePath); + + directory.subdirectories[path] = subdirectory; + } + } + + return directory; +} diff --git a/vitest-types.d.ts b/vitest-types.d.ts new file mode 100644 index 0000000..f309286 --- /dev/null +++ b/vitest-types.d.ts @@ -0,0 +1,12 @@ +import { Env } from './src/env'; +import { Directory } from './vitest-setup'; + +declare module 'vitest' { + export interface ProvidedContext { + devBucket: Directory; + } +} + +declare module 'cloudflare:test' { + interface ProvidedEnv extends Env {} +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..dc2e304 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,17 @@ +import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config'; + +export default defineWorkersConfig({ + test: { + globalSetup: ['./vitest-setup.ts'], + poolOptions: { + workers: { + wrangler: { + configPath: './wrangler.jsonc', + }, + miniflare: { + r2Buckets: ['R2_BUCKET'], + }, + }, + }, + }, +}); diff --git a/wrangler.jsonc b/wrangler.jsonc index bd71d98..74418a2 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -3,11 +3,12 @@ "name": "dist-worker", "main": "src/worker.ts", "compatibility_date": "2024-09-05", - "account_id": "07be8d2fbc940503ca1be344714cb0d1", "logpush": true, "vars": { "workers_dev": true, "ENVIRONMENT": "dev", + "CACHING": false, + "LOG_ERRORS": true, "S3_ENDPOINT": "https://07be8d2fbc940503ca1be344714cb0d1.r2.cloudflarestorage.com", "BUCKET_NAME": "dist-prod", "ORIGIN_HOST": "https://origin.nodejs.org" @@ -21,9 +22,11 @@ ], "env": { "staging": { + "account_id": "07be8d2fbc940503ca1be344714cb0d1", "vars": { "workers_dev": true, "ENVIRONMENT": "staging", + "CACHING": true, "S3_ENDPOINT": "https://07be8d2fbc940503ca1be344714cb0d1.r2.cloudflarestorage.com", "BUCKET_NAME": "dist-prod", "ORIGIN_HOST": "https://origin.nodejs.org" @@ -37,6 +40,7 @@ ] }, "prod": { + "account_id": "07be8d2fbc940503ca1be344714cb0d1", "tail_consumers": [ { "service": "dist-worker-prod-tail" @@ -45,6 +49,7 @@ "vars": { "workers_dev": false, "ENVIRONMENT": "prod", + "CACHING": true, "S3_ENDPOINT": "https://07be8d2fbc940503ca1be344714cb0d1.r2.cloudflarestorage.com", "BUCKET_NAME": "dist-prod", "ORIGIN_HOST": "https://origin.nodejs.org" @@ -58,4 +63,4 @@ ] } } -} +} \ No newline at end of file