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', }, ]);