From 27e1fa3b3003f12a5b6ad3d0539c29360dbc28a7 Mon Sep 17 00:00:00 2001 From: Dave Jong Date: Wed, 9 Sep 2026 10:17:48 +0200 Subject: [PATCH] Retire Node 18, and take the patched test runner The package engines field is a consumer support contract. The package now declares Node 20 or newer, validates the packed artifact at the exact floor read from engines.node, and no longer claims or tests Node 18. The artifact may still execute on 18, but installation there is outside the supported range. The declared-floor job installs no repository development dependencies. It consumes a tarball built on the release runtime and runs an explicit six-shape package/runtime contract: ESM and CommonJS imports, screening through both published module formats, the installed CLI, and export-map encapsulation. The selector refuses a missing shape or one that acquires a fixture dependency, so the floor check cannot silently narrow. The ordinary Node 22 consumer matrix continues to run all nine shapes. The maintainer toolchain is a separate contract: Vitest 4.1.11 and its Vite/Rolldown dependencies require ^20.19.0 or >=22.12.0. Validate pins both exact boundaries beside the current supported lines and installs with engine-strict. The test-runner update takes the patched release for GHSA-82fw-gwwq-j7x9. The build target follows the consumer floor at node20. Runtime comments refer to the availability of web crypto rather than to a particular Node release, because the fallback remains relevant to edge runtimes. Releasing this compatibility change requires 0.5.0. RELEASING states that a compatibility break is at least a minor while the package is 0.x and a major from 1.0 onward. --- .github/workflows/ci.yml | 131 ++- .gitignore | 3 + CONTRIBUTING.md | 14 +- RELEASING.md | 15 + package-lock.json | 956 ++++++++++++++---- package.json | 4 +- scripts/compat-matrix.mjs | 227 ++++- scripts/engines-floor.mjs | 36 + src/protect/runtime.js | 5 +- tests/compat-matrix-contract.test.ts | 36 + tests/engines-floor.test.ts | 46 + .../protect/detection-event-identity.test.ts | 3 +- tsup.config.ts | 6 +- 13 files changed, 1208 insertions(+), 274 deletions(-) create mode 100644 scripts/engines-floor.mjs create mode 100644 tests/compat-matrix-contract.test.ts create mode 100644 tests/engines-floor.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3a7a3de6..a55c2d38 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -173,18 +173,31 @@ jobs: validate: name: Validate on Node ${{ matrix.node-version }} runs-on: ubuntu-latest + env: + # The maintainer runtime range is a claim like any other, and npm treats an `engines` mismatch as + # a warning unless told otherwise โ€” so a tool that raises its floor would keep this job green + # until it happened to reach an API the runtime lacks. Strict, so the install is the check. + NPM_CONFIG_ENGINE_STRICT: 'true' strategy: fail-fast: false matrix: node-version: - # The floor `engines` claims, through to the current release line. 18 and 20 are past their - # upstream support, and are kept deliberately: this package exists to protect apps on whatever - # runtime a builder platform happens to give them, so the oldest runtime it claims to support is - # the one where a regression matters most. That is not theoretical โ€” the Node 18 job is what - # caught a detection field arriving empty there because the runtime exposes no global `crypto`. - - 18.x + # Run with the maintainer toolchain, whose runtime range is `^20.19.0 || >=22.12.0` โ€” the + # Vite/Rolldown the test runner brings. That is a union, not a floor: Node 21, and 22.0 + # through 22.11, are outside it. So both exact ends are pinned, because a floating `20.x` or + # `22.x` would stay green after something starts requiring a version released after the one + # documented, and the floating lines are kept beside them for the current releases. + # + # The floor `engines` claims for CONSUMERS is a different number and is tested by + # `declared-floor`, which installs none of this. + # + # 20 is past its upstream support and is kept deliberately: this package exists to protect + # apps on whatever runtime a builder platform gives them, so the oldest line it claims is + # where a regression matters most. + - 20.19.0 - 20.x + - 22.12.0 - 22.x - 24.x - 26.x @@ -257,6 +270,106 @@ jobs: - name: Audit published dependency tree run: npm audit --omit=dev --audit-level=moderate + # The tarball, built once on the version releases are built with, for the floor job below to consume. + # + # Separate because packing runs `prepare`, which needs the repository's development dependencies โ€” + # and those are not installable on every runtime a consumer may be on. A floor job that installed them + # would be testing the maintainer toolchain at that version, which is the opposite of the question. + pack: + name: ๐Ÿ“ฆ Pack the artifact + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Setup Node + uses: actions/setup-node@v7 + with: + node-version-file: .node-version + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Pack + # The directory first: `--pack-destination` does not create one, and npm's failure for a missing + # destination is an ENOENT on the tarball it was about to write. + run: | + mkdir -p packed + npm pack --pack-destination ./packed + + - name: Upload + uses: actions/upload-artifact@v7 + with: + name: packed-tarball + path: packed/*.tgz + if-no-files-found: error + retention-days: 1 + + # The floor `engines.node` claims, tested with the published artifact and without this repository's + # devDependencies. + # + # `--self-contained` runs six explicitly named package/runtime shapes whose fixtures install nothing + # but the tarball, including guard screening through both published module formats. The compiler probes + # install `typescript` and `@types/node` at floating versions, and this job is strict, so a floor either + # of them raises later would turn it red over something that is not this package. The ordinary consumer + # matrix runs all nine shapes on Node 22; only this declared-floor job narrows the set. + # + # `engines` is a CONSUMER contract: npm checks it when someone installs this package. The maintainer + # toolchain is a different question with a different answer โ€” the test runner's Vite/Rolldown need + # 20.19 โ€” and letting that decide `engines` would understate what the artifact supports. So the claim + # is tested the way it is made: install the tarball on the lowest version it names, and put a request + # through the guard. No root `npm ci` here, by design. + # + # The version is DERIVED from the manifest, not written here: a hard-coded one keeps testing the old + # floor when the claim moves, and tests above the new floor when it drops. And `engine-strict` makes + # `engines` refuse rather than warn, so a runtime the package does not admit fails this job instead of + # passing it with a warning. + # + # One manager, not the five above: the question here is the runtime, and whether managers agree is + # already answered by that matrix. + declared-floor: + name: ๐Ÿ“ฆ Consumers on the declared Node floor + needs: pack + runs-on: ubuntu-latest + env: + NPM_CONFIG_ENGINE_STRICT: 'true' + + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: The floor `engines.node` claims + id: floor + run: | + floor="$(node scripts/engines-floor.mjs)" + # An empty value would reach `setup-node` as "no version asked for", which resolves to + # whatever the runner already has โ€” a green job on a runtime nobody named. + if [ -z "${floor}" ]; then + echo "::error::Could not read a floor out of engines.node." + exit 1 + fi + echo "engines.node admits ${floor} as its lowest version" + echo "version=${floor}" >> "$GITHUB_OUTPUT" + + - name: Setup Node + uses: actions/setup-node@v7 + with: + node-version: ${{ steps.floor.outputs.version }} + + - name: Download the packed artifact + uses: actions/download-artifact@v8 + with: + name: packed-tarball + path: packed + + - name: Install the tarball and exercise the guard + run: | + tarball="$(ls packed/*.tgz)" + echo "consuming ${tarball} on $(node -v), engine-strict on" + node scripts/compat-matrix.mjs --manager npm --self-contained --tarball "${tarball}" + # One status for branch protection to require. # # Requiring the jobs above directly means branch protection names a matrix label โ€” `Consumers on npm @@ -274,6 +387,8 @@ jobs: needs: - capability-contract - consumers + - pack + - declared-floor - bundled-consumer - windows-smoke - validate @@ -295,8 +410,8 @@ jobs: # otherwise leave a green required check that verifies nothing at all โ€” the one failure mode a # gate must not have, since it is indistinguishable from a working one. count=$(printf '%s' "$RESULTS" | python3 -c 'import json, sys; print(len(json.load(sys.stdin)))') - if [ "$count" -lt 6 ]; then - echo "::error::This gate is standing on ${count} job(s); it is meant to require 6. A required check that verifies nothing passes exactly when something is broken." + if [ "$count" -lt 8 ]; then + echo "::error::This gate is standing on ${count} job(s); it is meant to require 8. A required check that verifies nothing passes exactly when something is broken." exit 1 fi diff --git a/.gitignore b/.gitignore index e3744b62..117d26cb 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,6 @@ test-build/.work/ # dependency graph, where its advisories cannot be told apart from advisories about the shipped package. examples/protect/package-lock.json .public-types-check/ + +# Where CI packs the artifact for the consumer jobs to install. +packed/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ea312a9e..f2e482ab 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,8 +29,18 @@ nvm use "$(cat .node-version)" # or: fnm use / nodenv local npm ci ``` -Consumers are supported from Node 18 โ€” `engines.node` is that contract, and it is a different question -from the version this repository is developed and released on. +Consumers are supported from Node 20 โ€” `engines.node` is that contract, and CI tests it at exactly that +floor (`Consumers on the declared Node floor`) with the published artifact and without the +devDependencies below. The version that job runs on is read out of `engines.node`, so moving the claim +moves the test; installs there run with `engine-strict`, so the claim refuses rather than warns. + +Working on the repository needs more than consuming it does: **`^20.19.0 || >=22.12.0`**, which is what +the test runner's Vite and Rolldown require. It is a union rather than a floor โ€” Node 21, and 22.0 +through 22.11, are outside it โ€” and CI pins both exact ends (`20.19.0`, `22.12.0`) beside the floating +lines, installing with `engine-strict` so a tool that raises its own floor fails the install rather than +warning. That is a maintainer requirement and deliberately not `engines`: letting it set the consumer +contract would understate what the artifact supports. `.node-version` (24) is the version releases are +built with, which is a third question again. ## The loop diff --git a/RELEASING.md b/RELEASING.md index 9d84e768..3b019e79 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -45,6 +45,21 @@ gh workflow run Release -f bump=patch No version math, no `npm view` lookup, no chance of colliding with an existing version โ€” the workflow does all of that. +**`patch` is the default, and it is the wrong choice for some changes.** Raising +the `engines.node` floor, removing or renaming an export, and changing what a +shipped default does are all compatibility breaks, and which bump they need +depends on where the version is: + +- **while this package is `0.x`** โ€” at least a `minor`. A caret range on a `0.x` + version does not cross the minor, so `0.5.0` is what keeps the break away from + an installer resolving `^0.4.x`. +- **from `1.0` onward** โ€” a `major`. A caret range then spans every minor, so a + minor would deliver the break to exactly the installers it has to be kept from. + +The release that carries the floor move to `>=20` is therefore **`0.5.0`**: +`gh workflow run Release -f bump=minor`. The workflow cannot infer any of this, +so it is the caller's to pass. + `Release` triggers `Publish` explicitly via `workflow_dispatch` rather than relying on the release event. This is deliberate: GitHub does **not** fire `release`-triggered workflows for releases created by the built-in diff --git a/package-lock.json b/package-lock.json index 41f4316f..1764a965 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,11 +16,11 @@ "fastify": "^5.12.1", "tsup": "^8.0.0", "typescript": "^5.4.0", - "vitest": "^3.0.0", + "vitest": "^4.1.11", "yaml": "^2.9.0" }, "engines": { - "node": ">=18" + "node": ">=20" } }, "node_modules/@esbuild/aix-ppc64": { @@ -621,6 +621,16 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@oxc-project/types": { + "version": "0.148.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.148.0.tgz", + "integrity": "sha512-Nm4s/jB+4FpFsPhWGEC4h7rzksesmtnMXomo6rCMcg/b8zLQuOziRgkCS1fxDCXOlJB/6Q8oABOZ/OP6RIPj9A==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, "node_modules/@pinojs/redact": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", @@ -628,6 +638,268 @@ "dev": true, "license": "MIT" }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.7.tgz", + "integrity": "sha512-EypzgnYCwyVY4NDHKzGmNJT5b+XaQEBniHxsMdeIQLB/tcCzZnhqrzHpZFbX9iaxx+5RiB8caATBtfvZP7zVxQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.7.tgz", + "integrity": "sha512-l17HE9EweWaqJZhuUuNBN/FzM62xw+DECVnJyvMsxn8vJFAGLy5QfLDoYAcronkAN8VxKZHezDpulHDPx95vFw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.7.tgz", + "integrity": "sha512-8ED8ELFvHXc6OCETIn4gXObPiaR6bckM/ipXtbzlPVDRMBfEGjCKgO90F9YtfdpDatVx/ZQw7aZ1vUMf/+T3Mw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.7.tgz", + "integrity": "sha512-/WPripjtiAIZ2tWY7ddijORT0Ujg87wxWW/qcoFVCKAWVDPhtY0xr7Dj0M3GyNGz60jGwTElhro/mkF9dT7dDQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.7.tgz", + "integrity": "sha512-14DI4NcqpvbICxSnGLx3PmtDaWqRP/KGSGb6C+JLLVPeZRl6dKdHba3pGsqT3vpdTqhEYIPG0MMQ8c0xYqoJxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.7.tgz", + "integrity": "sha512-bxrWIRvHWQvbJwi+VIie/kDJmQxcNE6xxWwZdqF/ExVAigtHkv54WTLQPb+QsZdnFy18fg7JPfWGL0RH6vwIlQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.7.tgz", + "integrity": "sha512-toOY2BChBZyuxU7OYX6Tn389di4IzAqPTycVcci0O7FSfBqzRB3RZn+K5Is6ANf4tmgRd/K1yZTsNTXbkXsnLg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.7.tgz", + "integrity": "sha512-lAIXTH/aiLRLxsTgQvfhjo4K1ydWIp00+V0voOr9beb/9ZmkUFrSIb03dXNFRgMNvkE6oGsF10ioQ6UsI+vS5Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.7.tgz", + "integrity": "sha512-kdnwS28Pkenp/mZMRwjXXXwxQ7pIsm+bF919LUK93BOyhcLsrVKdP2p9fxpiPNPAbNuch8ypQt0pm2P2LYCAGg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.7.tgz", + "integrity": "sha512-516OdsyLdr5E65paF3yBF55t8mfm9+gmtCsK3xI7XKXIT7EfRlHhxL8K/NR6Hu8BWSgF5+1w74lTL0+nxcc8Qw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.7.tgz", + "integrity": "sha512-r8/z8n7GFaYRln3xmP1Cxy0HH/HLM0uBUPkEuSVEfKGDA89M0FsZRZJRSwe/tJjRx+fpH/gjorfhB8tmEbSFLA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.7.tgz", + "integrity": "sha512-pAsE8iiDxUg1xBqdhrTfg45AVDVpirjz00sblEYClGNNcMnDb+e8beQgqIAw6LvauX/APvgxUnwrgun/YYGBhw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.7.tgz", + "integrity": "sha512-lTcIYmmnQQA8Or/2DatS6oSqcdLHvendjS+zLu+FwgToynWMRSmQdpM65fTANJgIS4mjbMOo5KT2lnT9SAb96w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.7.tgz", + "integrity": "sha512-e3Gu3WxbNk/UqQhxqU7YIYO+9ZBvWNz3U+h/qRFosscMFzdRPbXYSaSWgSnklv2fz1TgzBTcti2z35c/7irsHw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.7.tgz", + "integrity": "sha512-W/jg5qoRSqjsEv0+dZi4e687mcHqmVuU0P4fK6qS/xjetW2Gmc1W8j//z5nAeNcC8Ttm0hV46IjcYeuVwYhuiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.60.3", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.3.tgz", @@ -978,6 +1250,13 @@ "win32" ] }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -1014,39 +1293,40 @@ } }, "node_modules/@vitest/expect": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.6.tgz", - "integrity": "sha512-1+7q9BtaKzEmO+fmNT3kYvoNn5Y71XWAx2Q5HRim4tTVRQVRv4uJFAQ5FbK0OPUeNP/WmVCpxYxoJdvuHVjzBQ==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz", + "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==", "dev": true, "license": "MIT", "dependencies": { + "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "3.2.6", - "@vitest/utils": "3.2.6", - "chai": "^5.2.0", - "tinyrainbow": "^2.0.0" + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/mocker": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.6.tgz", - "integrity": "sha512-EZOrpDbkKotFAP7wPAQV1UIyoGOk4oX7ynWhBhLB7v+meMHbQhU16oPpIYGTTe4oFlhpryGpgpcZP/sin3hYuw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz", + "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.2.6", + "@vitest/spy": "4.1.11", "estree-walker": "^3.0.3", - "magic-string": "^0.30.17" + "magic-string": "^0.30.21" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "msw": { @@ -1058,42 +1338,42 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.6.tgz", - "integrity": "sha512-lb7XXXzmm2h2ASzFnRvQpDo6onT1NmMJA3tkGTWiBFtRJ9lxGY3d3mm/Apt36gej2bkkOVLL/yTOtufDaFa/jA==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz", + "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^2.0.0" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.6.tgz", - "integrity": "sha512-HYcoSj1w5tcgUnzoF0HcyaAQjpA1gj9ftUJ7iSJSuipc02jW9gKkigwZbjFldAfYHA1fa8UZVRftdMY5msWM9Q==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz", + "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.2.6", - "pathe": "^2.0.3", - "strip-literal": "^3.0.0" + "@vitest/utils": "4.1.11", + "pathe": "^2.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/snapshot": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.6.tgz", - "integrity": "sha512-H+ZjNTWGpObenh0YnlBctAPnJSI20P81PL8BPzWpx54YXLLTm8hEsWawtcYLMrwvpK48hGxLLbCS+1KRXhsKhw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz", + "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.6", - "magic-string": "^0.30.17", + "@vitest/pretty-format": "4.1.11", + "@vitest/utils": "4.1.11", + "magic-string": "^0.30.21", "pathe": "^2.0.3" }, "funding": { @@ -1101,28 +1381,25 @@ } }, "node_modules/@vitest/spy": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.6.tgz", - "integrity": "sha512-oq6BbH68WzcWmwtBrU9nqLeaXTR4XwJF7FSLkKEZo4i6eoXcrxjcwSuTvWBIRUTC6VC72nXYunzqgZA+IKdtxg==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz", + "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==", "dev": true, "license": "MIT", - "dependencies": { - "tinyspy": "^4.0.3" - }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.6.tgz", - "integrity": "sha512-lI23nIs4bnT3T8NIoh+vFaz5s2/DdP0Jgt2jxwgWljvwn82cLJtyi/If+fjFyoLMGIOz0U/fKvWE0d4jsNQEfg==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz", + "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.6", - "loupe": "^3.1.4", - "tinyrainbow": "^2.0.0" + "@vitest/pretty-format": "4.1.11", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" @@ -1275,32 +1552,15 @@ } }, "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==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "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/check-error": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", - "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - } - }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -1344,6 +1604,13 @@ "node": "^14.18.0 || >=16.10.0" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/cookie": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", @@ -1376,16 +1643,6 @@ } } }, - "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/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -1396,10 +1653,20 @@ "node": ">=6" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", "dev": true, "license": "MIT" }, @@ -1655,13 +1922,6 @@ "node": ">=10" } }, - "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/json-schema-ref-resolver": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", @@ -1728,6 +1988,267 @@ ], "license": "MIT" }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -1758,13 +2279,6 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, - "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/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -1836,6 +2350,20 @@ "node": ">=0.10.0" } }, + "node_modules/obug": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", + "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/on-exit-leak-free": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", @@ -1853,16 +2381,6 @@ "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", @@ -1871,9 +2389,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "dev": true, "license": "MIT", "engines": { @@ -1946,9 +2464,9 @@ } }, "node_modules/postcss": { - "version": "8.5.25", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz", - "integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==", + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", "dev": true, "funding": [ { @@ -1966,7 +2484,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.16", + "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -2113,6 +2631,40 @@ "dev": true, "license": "MIT" }, + "node_modules/rolldown": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.7.tgz", + "integrity": "sha512-g0EtLvBjTUB7jhyV0S/TCup3v/XSVl45vUIGbOGU4QPiyjTenCe4mKuFvW9fEgYmS2Fo42AUssRmNuMziXdrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.148.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.7", + "@rolldown/binding-android-arm64": "1.2.7", + "@rolldown/binding-darwin-arm64": "1.2.7", + "@rolldown/binding-darwin-x64": "1.2.7", + "@rolldown/binding-freebsd-x64": "1.2.7", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.7", + "@rolldown/binding-linux-arm64-gnu": "1.2.7", + "@rolldown/binding-linux-arm64-musl": "1.2.7", + "@rolldown/binding-linux-ppc64-gnu": "1.2.7", + "@rolldown/binding-linux-s390x-gnu": "1.2.7", + "@rolldown/binding-linux-x64-gnu": "1.2.7", + "@rolldown/binding-linux-x64-musl": "1.2.7", + "@rolldown/binding-openharmony-arm64": "1.2.7", + "@rolldown/binding-win32-arm64-msvc": "1.2.7", + "@rolldown/binding-win32-x64-msvc": "1.2.7" + } + }, "node_modules/rollup": { "version": "4.60.3", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.3.tgz", @@ -2283,25 +2835,12 @@ "license": "MIT" }, "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", "dev": true, "license": "MIT" }, - "node_modules/strip-literal": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", - "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "js-tokens": "^9.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, "node_modules/sucrase": { "version": "3.35.1", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", @@ -2383,9 +2922,9 @@ "license": "MIT" }, "node_modules/tinyglobby": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { @@ -2399,30 +2938,10 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "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.4", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", - "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", + "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", "dev": true, "license": "MIT", "engines": { @@ -2538,18 +3057,17 @@ "license": "MIT" }, "node_modules/vite": { - "version": "7.3.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", - "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz", + "integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.27.0 || ^0.28.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.26", + "rolldown": "~1.2.4", + "tinyglobby": "^0.2.17" }, "bin": { "vite": "bin/vite.js" @@ -2565,9 +3083,10 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.4.0 || ^0.5.0", + "esbuild": "^0.27.0 || ^0.28.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", @@ -2580,13 +3099,16 @@ "@types/node": { "optional": true }, - "jiti": { + "@vitejs/devtools": { "optional": true }, - "less": { + "esbuild": { "optional": true }, - "lightningcss": { + "jiti": { + "optional": true + }, + "less": { "optional": true }, "sass": { @@ -2612,89 +3134,80 @@ } } }, - "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/vitest": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.6.tgz", - "integrity": "sha512-xejya+bT/j/+R/AGa1XOfRxLmNUlLtlwjRsFUILF+xHfzElmGcmFydy2gqqIrd62ptIEfwVMofd19uNWD9L7Nw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz", + "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==", "dev": true, "license": "MIT", "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/expect": "3.2.6", - "@vitest/mocker": "3.2.6", - "@vitest/pretty-format": "^3.2.6", - "@vitest/runner": "3.2.6", - "@vitest/snapshot": "3.2.6", - "@vitest/spy": "3.2.6", - "@vitest/utils": "3.2.6", - "chai": "^5.2.0", - "debug": "^4.4.1", - "expect-type": "^1.2.1", - "magic-string": "^0.30.17", + "@vitest/expect": "4.1.11", + "@vitest/mocker": "4.1.11", + "@vitest/pretty-format": "4.1.11", + "@vitest/runner": "4.1.11", + "@vitest/snapshot": "4.1.11", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", "pathe": "^2.0.3", - "picomatch": "^4.0.2", - "std-env": "^3.9.0", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", "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", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "bin": { "vitest": "vitest.mjs" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + "node": "^20.0.0 || ^22.0.0 || >=24.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.6", - "@vitest/ui": "3.2.6", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.11", + "@vitest/browser-preview": "4.1.11", + "@vitest/browser-webdriverio": "4.1.11", + "@vitest/coverage-istanbul": "4.1.11", + "@vitest/coverage-v8": "4.1.11", + "@vitest/ui": "4.1.11", "happy-dom": "*", - "jsdom": "*" + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "@edge-runtime/vm": { "optional": true }, - "@types/debug": { + "@opentelemetry/api": { "optional": true }, "@types/node": { "optional": true }, - "@vitest/browser": { + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { "optional": true }, "@vitest/ui": { @@ -2705,9 +3218,22 @@ }, "jsdom": { "optional": true + }, + "vite": { + "optional": false } } }, + "node_modules/vitest/node_modules/tinyexec": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", diff --git a/package.json b/package.json index 75a8bd51..4d1c97ab 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,7 @@ "rule-contract:check": "node scripts/emit-rule-contract.mjs --check" }, "engines": { - "node": ">=18" + "node": ">=20" }, "publishConfig": { "access": "public", @@ -95,7 +95,7 @@ "fastify": "^5.12.1", "tsup": "^8.0.0", "typescript": "^5.4.0", - "vitest": "^3.0.0", + "vitest": "^4.1.11", "yaml": "^2.9.0" }, "overrides": { diff --git a/scripts/compat-matrix.mjs b/scripts/compat-matrix.mjs index c181b302..0c635710 100644 --- a/scripts/compat-matrix.mjs +++ b/scripts/compat-matrix.mjs @@ -9,17 +9,52 @@ // Such failures are invisible from inside the repository and total for the consumer: the source suite is // green while nothing can import the package. Each shape below states the consumption path it holds open. // -// Run: node scripts/compat-matrix.mjs [--manager npm|pnpm|yarn|bun] +// Run: node scripts/compat-matrix.mjs [--manager npm|pnpm|yarn|bun] [--tarball FILE] [--self-contained] import { execFileSync } from 'node:child_process'; -import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync, existsSync, readdirSync } from 'node:fs'; +import { + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + readdirSync, + realpathSync, + rmSync, + statSync, + writeFileSync, +} from 'node:fs'; import { tmpdir } from 'node:os'; import path from 'node:path'; +import { fileURLToPath } from 'node:url'; const manager = (() => { const i = process.argv.indexOf('--manager'); return i === -1 ? 'npm' : process.argv[i + 1]; })(); +/** + * Only the six runtime/package shapes that form the declared-floor contract. + * + * For a job pinned to a runtime โ€” the declared consumer floor โ€” the compiler probes are a liability + * rather than coverage: they install `typescript` and `@types/node` at floating versions, so a floor + * either of those raises later would turn that job red over something that is not this package. The + * ordinary consumer matrix still runs all nine shapes on Node 22; only the declared-floor job narrows + * the set to probes that install nothing but the tarball. + */ +const selfContainedOnly = process.argv.includes('--self-contained'); + +/** + * A tarball built elsewhere, or nothing to build one here. + * + * Packing runs `prepare`, which needs the repository's development dependencies โ€” and the runtime a + * consumer is on is not necessarily one those can be installed on. Given a tarball, this script installs + * and exercises it and touches nothing else, which is what lets a consumer runtime be tested as a + * consumer runtime. + */ +const prebuilt = (() => { + const i = process.argv.indexOf('--tarball'); + return i === -1 ? null : path.resolve(process.argv[i + 1]); +})(); + const ROOT = process.cwd(); const WINDOWS = process.platform === 'win32'; @@ -99,11 +134,6 @@ const PREPARE = { }, }; -if (!INSTALL[manager]) { - console.error(`Unknown manager: ${manager}. Known: ${Object.keys(INSTALL).join(', ')}`); - process.exit(2); -} - const PROBE_TS = ` import { buildWirePayload, collectHostingEnvKeys } from '@patchstack/connect'; import { createProtection } from '@patchstack/connect/protect'; @@ -119,13 +149,44 @@ const TSCONFIG = (module_, resolution, types) => JSON.stringify({ }, }, null, 2); +/** + * A rule, a request that matches it, and one that does not. + * + * Importing the package proves the artifact resolves. It does not prove the guard runs: a build that + * throws on construction, or screens nothing, imports exactly as well. So this constructs a protection, + * puts a request through it both ways, and stops it โ€” the smallest thing that fails when the guard is + * broken rather than merely absent. + */ +const SCREENING_BODY = ` +const rules = { + firewall: [{ + id: 'consumer-probe', + title: 'a rule the probe supplies itself', + rule_v2: [{ parameter: 'get.q', match: { type: 'contains', value: 'boom' } }], + }], + whitelists: [], +}; + +const protection = await createProtection({ rules, mode: 'block' }); +try { + const guard = protection.fetchGuard(); + const blocked = await guard(new Request('https://app.test/search?q=boom')); + if (!blocked || blocked.status !== 403) throw new Error('a matching rule did not block: ' + (blocked && blocked.status)); + + const allowed = await guard(new Request('https://app.test/search?q=hello')); + if (allowed) throw new Error('a request matching nothing was blocked with ' + allowed.status); +} finally { + await protection.stop(); +} +`; + /** * Each consumer shape, and what it is here to prove. * * `cjs-ts-no-node-types` is not redundant with `cjs-ts`: every other TypeScript fixture installs * `@types/node`, so it is the only one that holds the public declarations free of it. */ -const SHAPES = [ +export const SHAPES = [ { name: 'esm-js', why: 'ESM JavaScript import', pkg: { type: 'module' }, deps: [], @@ -138,6 +199,23 @@ const SHAPES = [ files: { 'probe.cjs': "const r = require('@patchstack/connect');\nconst p = require('@patchstack/connect/protect');\nif (typeof r.buildWirePayload !== 'function') throw new Error('root export missing');\nif (typeof p.createProtection !== 'function') throw new Error('protect export missing');\n" }, check: (dir) => run('node', ['probe.cjs'], dir), }, + { + name: 'esm-js-screens', why: 'the ESM build constructs a guard that blocks and allows', + pkg: { type: 'module' }, deps: [], + files: { + 'screen.mjs': `import { createProtection } from '@patchstack/connect/protect';\n${SCREENING_BODY}`, + }, + check: (dir) => run('node', ['screen.mjs'], dir), + }, + { + name: 'cjs-js-screens', why: 'the CommonJS build constructs a guard that blocks and allows', + pkg: {}, deps: [], + files: { + // A separate build from the ESM one above, so it is a separate question. + 'screen.cjs': `const { createProtection } = require('@patchstack/connect/protect');\n(async () => {${SCREENING_BODY}})().catch((err) => {\n console.error(err);\n process.exit(1);\n});\n`, + }, + check: (dir) => run('node', ['screen.cjs'], dir), + }, { name: 'esm-ts', why: 'ESM TypeScript compilation', pkg: { type: 'module' }, deps: ['typescript@5', '@types/node'], @@ -208,6 +286,48 @@ const SHAPES = [ }, ]; +/** + * The exact package/runtime probes that must run at the Node version declared in `engines.node`. + * + * Explicit names make this a coverage contract rather than a side effect of a fixture's current + * dependencies. In particular, both published module formats must construct and exercise a guard at + * the floor; import-only probes cannot stand in for those two behaviours. + */ +export const FLOOR_SHAPE_NAMES = Object.freeze([ + 'esm-js', + 'cjs-js', + 'esm-js-screens', + 'cjs-js-screens', + 'cli', + 'encapsulation', +]); + +/** Resolve and validate the declared-floor suite before any fixture is installed. */ +export function floorShapesOf(shapes) { + const byName = new Map(); + + for (const shape of shapes) { + if (byName.has(shape.name)) throw new Error(`consumer shape is named more than once: ${shape.name}`); + byName.set(shape.name, shape); + } + + const selected = FLOOR_SHAPE_NAMES.map((name) => { + const shape = byName.get(name); + if (!shape) throw new Error(`declared-floor consumer shape is missing: ${name}`); + return shape; + }); + + for (const shape of selected) { + if (shape.deps.length !== 0) { + throw new Error( + `declared-floor consumer shape ${shape.name} must install only the tarball; found: ${shape.deps.join(', ')}`, + ); + } + } + + return selected; +} + // A local binary is addressed by path, so on Windows it needs the shim's extension spelled out. const localBin = (dir, name) => path.join(dir, 'node_modules', '.bin', WINDOWS ? `${name}.cmd` : name); const tsc = (dir) => localBin(dir, 'tsc'); @@ -223,46 +343,71 @@ function packTarball(into) { return path.join(into, found[0]); } -const work = mkdtempSync(path.join(tmpdir(), 'ps-compat-')); -let failures = 0; +function main() { + if (!INSTALL[manager]) { + console.error(`Unknown manager: ${manager}. Known: ${Object.keys(INSTALL).join(', ')}`); + return 2; + } -try { - const tarball = packTarball(work); + const shapes = selfContainedOnly ? floorShapesOf(SHAPES) : SHAPES; + const work = mkdtempSync(path.join(tmpdir(), 'ps-compat-')); + let failures = 0; - // Which manager, at which version. The label alone says "Yarn", and Classic and Berry resolve - // differently enough that a pass under one is not a pass under the other. - let managerVersion = 'unknown'; try { - managerVersion = run(manager, ['--version'], ROOT).trim().split('\n').pop(); - } catch { /* the install below fails loudly if the manager is missing */ } + // A file, not merely something that exists: an unexpanded glob resolves to a directory, and npm + // would install that directory as the package โ€” a pass that proves nothing about the artifact. + if (prebuilt && !(existsSync(prebuilt) && statSync(prebuilt).isFile())) { + throw new Error(`--tarball needs a packed file; ${prebuilt} is not one`); + } + const tarball = prebuilt ?? packTarball(work); - console.log(`packed ${path.basename(tarball)}`); - console.log(`node ${process.version} ยท ${manager} ${managerVersion} ยท ${process.platform}\n`); + // Which manager, at which version. The label alone says "Yarn", and Classic and Berry resolve + // differently enough that a pass under one is not a pass under the other. + let managerVersion = 'unknown'; + try { + managerVersion = run(manager, ['--version'], ROOT).trim().split('\n').pop(); + } catch { /* the install below fails loudly if the manager is missing */ } - for (const shape of SHAPES) { - const dir = path.join(work, shape.name); - mkdirSync(dir, { recursive: true }); - writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ name: `consumer-${shape.name}`, private: true, ...shape.pkg }, null, 2)); - for (const [file, body] of Object.entries(shape.files ?? {})) writeFileSync(path.join(dir, file), body); + console.log(`${prebuilt ? 'given' : 'packed'} ${path.basename(tarball)}`); + console.log(`node ${process.version} ยท ${manager} ${managerVersion} ยท ${process.platform}\n`); - try { - PREPARE[manager]?.(dir); - const [cmd, args] = INSTALL[manager](tarball, shape.deps); - run(cmd, args, dir); - shape.check(dir); - console.log(` ok ${shape.name.padEnd(22)} ${shape.why}`); - } catch (error) { - failures++; - const detail = `${error.stdout ?? ''}${error.stderr ?? ''}`.trim() || error.message; - console.log(` FAIL ${shape.name.padEnd(22)} ${shape.why}`); - console.log(detail.split('\n').slice(0, 8).map((l) => ` ${l}`).join('\n')); + for (const shape of shapes) { + const dir = path.join(work, shape.name); + mkdirSync(dir, { recursive: true }); + writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ name: `consumer-${shape.name}`, private: true, ...shape.pkg }, null, 2)); + for (const [file, body] of Object.entries(shape.files ?? {})) writeFileSync(path.join(dir, file), body); + + try { + PREPARE[manager]?.(dir); + const [cmd, args] = INSTALL[manager](tarball, shape.deps); + run(cmd, args, dir); + shape.check(dir); + console.log(` ok ${shape.name.padEnd(22)} ${shape.why}`); + } catch (error) { + failures++; + const detail = `${error.stdout ?? ''}${error.stderr ?? ''}`.trim() || error.message; + console.log(` FAIL ${shape.name.padEnd(22)} ${shape.why}`); + console.log(detail.split('\n').slice(0, 8).map((l) => ` ${l}`).join('\n')); + } } + } finally { + rmSync(work, { recursive: true, force: true }); + } + + console.log(failures === 0 + ? `\nall ${shapes.length} ${selfContainedOnly ? 'declared-floor ' : ''}consumer shapes work with ${manager}` + : `\n${failures} of ${shapes.length} ${selfContainedOnly ? 'declared-floor ' : ''}consumer shapes FAILED with ${manager}`); + return failures === 0 ? 0 : 1; +} + +function invokedDirectly() { + const self = realpathSync(fileURLToPath(import.meta.url)); + + try { + return realpathSync(process.argv[1] ?? '') === self; + } catch { + return false; } -} finally { - rmSync(work, { recursive: true, force: true }); } -console.log(failures === 0 - ? `\nall ${SHAPES.length} consumer shapes work with ${manager}` - : `\n${failures} of ${SHAPES.length} consumer shapes FAILED with ${manager}`); -process.exit(failures === 0 ? 0 : 1); +if (invokedDirectly()) process.exitCode = main(); diff --git a/scripts/engines-floor.mjs b/scripts/engines-floor.mjs new file mode 100644 index 00000000..705185ea --- /dev/null +++ b/scripts/engines-floor.mjs @@ -0,0 +1,36 @@ +// The exact lowest Node version `engines.node` admits, for CI to install and test the claim on. +// +// A hard-coded version in the workflow is not bound to the claim: raise the floor and the job keeps +// testing the old one, lower it and the job tests above the new one. Deriving it means the claim and the +// runtime it is tested on cannot drift apart โ€” moving `engines.node` moves this job. +// +// Only `>=` with one to three components is read, because that is the shape of a floor. Anything else โ€” +// a caret, a range with an upper bound, a disjunction โ€” is refused rather than guessed at: guessing +// would put a number in front of CI that nobody stated, and a green job on the wrong runtime is worse +// than a red one that says why. +import { readFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const manifest = path.join(process.cwd(), 'package.json'); +const declared = JSON.parse(readFileSync(manifest, 'utf8')).engines?.node; + +export function floorOf(range) { + if (typeof range !== 'string') throw new Error('engines.node is not declared'); + const match = /^>=\s*(\d+)(?:\.(\d+))?(?:\.(\d+))?$/.exec(range.trim()); + if (!match) { + throw new Error( + `engines.node is "${range}", which is not a plain \`>=\` floor. ` + + 'Either express it as one, or decide deliberately which version CI should test the claim on.', + ); + } + const [, major, minor = '0', patch = '0'] = match; + + return `${major}.${minor}.${patch}`; +} + +// `fileURLToPath`, not `new URL(...).pathname`: the pathname keeps a path's spaces percent-encoded, so +// the comparison fails and this prints nothing while exiting 0 โ€” a value the caller then reads as empty. +if (path.resolve(process.argv[1] ?? '') === fileURLToPath(import.meta.url)) { + process.stdout.write(`${floorOf(declared)}\n`); +} diff --git a/src/protect/runtime.js b/src/protect/runtime.js index fd666f19..9ff9e460 100644 --- a/src/protect/runtime.js +++ b/src/protect/runtime.js @@ -1988,8 +1988,9 @@ const eventIdentities = new WeakMap(); /** * 32 hexadecimal characters naming one call. * - * Web crypto where the runtime has it. Where it does not โ€” Node 18 exposes no global `crypto` โ€” the - * clock and `Math.random` stand in, which is what this package already does for its reporter's own + * Web crypto where the runtime has it. Where it does not โ€” this package runs on edge runtimes too, and + * what they expose varies โ€” the clock and `Math.random` stand in, as this package already does for its + * reporter's own * instance id and is enough here for the same reason: this identity is never a secret and never a * boundary. Nothing is authorised by holding it and nothing is denied by guessing it. What it has to do * is not collide between two calls, and a millisecond plus eighty-odd bits does that. diff --git a/tests/compat-matrix-contract.test.ts b/tests/compat-matrix-contract.test.ts new file mode 100644 index 00000000..d8e1de6c --- /dev/null +++ b/tests/compat-matrix-contract.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it } from 'vitest'; +import { FLOOR_SHAPE_NAMES, SHAPES, floorShapesOf } from '../scripts/compat-matrix.mjs'; + +const EXPECTED_FLOOR_SHAPES = [ + 'esm-js', + 'cjs-js', + 'esm-js-screens', + 'cjs-js-screens', + 'cli', + 'encapsulation', +]; + +describe('the declared-floor consumer suite', () => { + it('runs the exact package and runtime probes promised by CI', () => { + expect(FLOOR_SHAPE_NAMES).toEqual(EXPECTED_FLOOR_SHAPES); + expect(floorShapesOf(SHAPES).map((shape) => shape.name)).toEqual(EXPECTED_FLOOR_SHAPES); + }); + + it.each(['esm-js-screens', 'cjs-js-screens'])('refuses to omit %s when it gains a fixture dependency', (name) => { + const changed = SHAPES.map((shape) => + shape.name === name ? { ...shape, deps: ['fixture-package'] } : shape, + ); + + expect(() => floorShapesOf(changed)).toThrow( + `declared-floor consumer shape ${name} must install only the tarball`, + ); + }); + + it('refuses an incomplete suite instead of reporting fewer successful shapes', () => { + const incomplete = SHAPES.filter((shape) => shape.name !== 'esm-js-screens'); + + expect(() => floorShapesOf(incomplete)).toThrow( + 'declared-floor consumer shape is missing: esm-js-screens', + ); + }); +}); diff --git a/tests/engines-floor.test.ts b/tests/engines-floor.test.ts new file mode 100644 index 00000000..4e419a0a --- /dev/null +++ b/tests/engines-floor.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { execFileSync } from 'node:child_process'; +import { floorOf } from '../scripts/engines-floor.mjs'; + +/** + * The version CI installs to test what `engines.node` claims. + * + * Read from the manifest rather than written down anywhere else: a floor tested on a version somebody + * typed is a floor that stops being tested the moment the claim moves. + */ +describe('the exact version a `>=` floor admits', () => { + for (const [range, floor] of [ + ['>=20', '20.0.0'], + ['>=20.19', '20.19.0'], + ['>=20.19.4', '20.19.4'], + ['>=21', '21.0.0'], + ['>= 22', '22.0.0'], + ] as const) { + it(`${range} -> ${floor}`, () => { + expect(floorOf(range)).toBe(floor); + }); + } + + // Refused rather than guessed: each of these has a lowest admitted version that is not what a naive + // read of the leading number would say, and CI must not run on a number nobody stated. + for (const range of ['^20', '~20.19', '>=20 <22', '20.x', '*', '>=20 || >=22', '', undefined]) { + it(`refuses ${JSON.stringify(range)}`, () => { + expect(() => floorOf(range as never)).toThrow(); + }); + } + + it('prints the floor THIS manifest declares, which is what CI installs', () => { + // The one assertion that binds the workflow's input to the claim. Printing some version proves the + // script runs; printing the version this manifest admits is what makes the job test the floor + // rather than a number that happens to be written somewhere. A value hard-coded in the script, or a + // manifest whose range this cannot read, fails here rather than in a workflow log. + const declared = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).engines?.node; + const printed = execFileSync(process.execPath, ['scripts/engines-floor.mjs'], { + cwd: new URL('..', import.meta.url), + encoding: 'utf8', + }).trim(); + + expect(printed).toBe(floorOf(declared)); + }); +}); diff --git a/tests/protect/detection-event-identity.test.ts b/tests/protect/detection-event-identity.test.ts index 355fb154..19032f95 100644 --- a/tests/protect/detection-event-identity.test.ts +++ b/tests/protect/detection-event-identity.test.ts @@ -87,7 +87,8 @@ describe('the reported identity', () => { }); it('mints one on a runtime with no web crypto', async () => { - // Node 18 exposes no global `crypto`, and a guard there must still be able to group its detections. + // Not every runtime this package supports exposes web crypto, and a guard on one of those must still + // be able to group its detections. // The clock and `Math.random` stand in: this identity is never a secret and never a boundary, so // what it has to do is not collide between two calls. const original = globalThis.crypto; diff --git a/tsup.config.ts b/tsup.config.ts index 8f13a714..eefb58eb 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -23,7 +23,7 @@ export default defineConfig([ clean: true, sourcemap: true, esbuildOptions: noEmbeddedSources, - target: 'node18', + target: 'node20', }, { entry: { cli: 'src/cli.ts' }, @@ -31,7 +31,7 @@ export default defineConfig([ clean: false, sourcemap: true, esbuildOptions: noEmbeddedSources, - target: 'node18', + target: 'node20', banner: { js: '#!/usr/bin/env node' }, // `map` parses the target app's source with a TypeScript compiler resolved at RUNTIME (the app's // own `typescript`, or the environment's). Never bundle the compiler into the CLI โ€” it's a heavy @@ -46,6 +46,6 @@ export default defineConfig([ clean: false, sourcemap: true, esbuildOptions: noEmbeddedSources, - target: 'node18', + target: 'node20', }, ]);