diff --git a/.changeset-released/bump-versions.txt b/.changeset-released/bump-versions.txt new file mode 100644 index 0000000000..45ebc73c1f --- /dev/null +++ b/.changeset-released/bump-versions.txt @@ -0,0 +1,17 @@ +attestation-first-min-release-age +auto-collect-minimum-release-age-exclude +cache-aware-minimum-release-age-gate +clear-password-padding +fix-11655-self-update-minimum-release-age +fix-global-allow-builds +fix-verify-deps-silent-install +floppy-parents-teach +gvs-engine-name-shell-node +gvs-engine-per-snapshot-runtime-pin +lockfile-verification-progress-logs +oidc-unresolved-env-placeholder +pmonfail-default-devengines-11676 +record-locally-resolved-lockfile-verified +revalidate-minimum-release-age +sync-env-lockfile-when-missing-11674 +warn-deprecated-pnpm-field-11677 diff --git a/.changeset-released/main.txt b/.changeset-released/main.txt index f8e7dcec2d..4f641d4838 100644 --- a/.changeset-released/main.txt +++ b/.changeset-released/main.txt @@ -1,7 +1,10 @@ audit-signatures check-deps-status-skip-engine-check +clear-resolutions-warning codeql-alert-159-followup codeql-security-fixes +credential-rebind-defense +deploy-skip-config-dependencies fix-11529-view-published-time fix-11561-publish-web-auth-proxy fix-11587-global-isolated-per-arg @@ -11,12 +14,24 @@ fix-named-catalog-upgrade fix-named-registry-vs-local-resolver fix-optimistic-lockfile-conflict gh-packages-prefix +git-fetcher-reject-non-sha-commits honest-moose-grin +integrity-mismatch-fails-by-default no-runtime-flag +olive-spies-listen +patch-path-traversal pnpm-bugs-command pnpm-owner-command +policy-handlers-loose-mode-message preserve-published-at-in-fast-path +registry-access-client-helper +reject-traversal-dependency-aliases +require-tarball-integrity +runtime-onfail-node-check +runtime-set-default-devengines runtime-set-workspace-root split-local-resolver +tidy-global-update-summary +tidy-trust-publishers update-cmd-shim-9-0-3 version-exit-finishes-workers diff --git a/.changeset-released/new-release.txt b/.changeset-released/new-release.txt new file mode 100644 index 0000000000..4fe1ff9cf1 --- /dev/null +++ b/.changeset-released/new-release.txt @@ -0,0 +1,10 @@ +dlx-fall-back-to-alias-when-manifest-missing +fix-cyclic-peer-determinism +fix-unaliased-deps-dropped-from-manifest +native-pkg-command +native-repo-command +native-set-script-command +prune-env-lockfile-on-config-dep-update +skip-manifest-obfuscation-opt-in +stage-publish +trust-lockfile-and-verifier-memory diff --git a/.changeset-released/release-11-2-1.txt b/.changeset-released/release-11-2-1.txt new file mode 100644 index 0000000000..852a8f9f7b --- /dev/null +++ b/.changeset-released/release-11-2-1.txt @@ -0,0 +1,3 @@ +config-deps-optional-subdep-snapshot-flag +pick-registry-unscoped-npm-alias +quiet-config-deps diff --git a/.changeset-released/release-11-2-2.txt b/.changeset-released/release-11-2-2.txt new file mode 100644 index 0000000000..3c60d809a3 --- /dev/null +++ b/.changeset-released/release-11-2-2.txt @@ -0,0 +1,2 @@ +fix-pacquet-outdated-lockfile-on-update +forward-install-flags-to-pacquet diff --git a/.changeset-released/release-11-2.txt b/.changeset-released/release-11-2.txt new file mode 100644 index 0000000000..9c1982cd38 --- /dev/null +++ b/.changeset-released/release-11-2.txt @@ -0,0 +1,11 @@ +cafile-resolve-against-npmrc-dir +config-deps-optional-subdeps +config-reader-registry-sync-npmrc +global-minimum-release-age-policy +injectworkspacepackages-prune-crashes +login-scope-flag +login-workspace-registry +minimum-release-age-modified-shortcut-inclusive +outdated-runtimes +pacquet-frozen-install-delegation +publish-config-access diff --git a/.changeset/cache-aware-minimum-release-age-gate.md b/.changeset/cache-aware-minimum-release-age-gate.md deleted file mode 100644 index e9c39f1d41..0000000000 --- a/.changeset/cache-aware-minimum-release-age-gate.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -"@pnpm/resolving.resolver-base": minor -"@pnpm/resolving.npm-resolver": minor -"@pnpm/resolving.default-resolver": minor -"@pnpm/installing.client": minor -"@pnpm/store.connection-manager": minor -"@pnpm/testing.temp-store": minor -"@pnpm/installing.deps-installer": minor -"pnpm": patch ---- - -Restructured the `minimumReleaseAge` lockfile revalidation gate around a generic `ResolutionVerifier` interface. Each resolver may now export a sibling verifier factory (today: `createNpmResolutionVerifier`) that re-checks an already-resolved lockfile entry against its policies; `createResolver`'s companion `createResolutionVerifier` combines them and the `Client` exposes the combined `verifyResolution` for the install layer to consume. The npm verifier reuses the same on-disk metadata mirror the resolver writes to, so steady-state installs pay only a headers-only conditional GET per locked package [#11675](https://github.com/pnpm/pnpm/issues/11675). diff --git a/.changeset/chilly-meteors-enter.md b/.changeset/chilly-meteors-enter.md new file mode 100644 index 0000000000..fc87695e8f --- /dev/null +++ b/.changeset/chilly-meteors-enter.md @@ -0,0 +1,5 @@ +--- +"@pnpm/releasing.commands": patch +--- + +Fix scoped packages without a publishConfig.access setting being published with public access. diff --git a/.changeset/fix-11655-self-update-minimum-release-age.md b/.changeset/fix-11655-self-update-minimum-release-age.md deleted file mode 100644 index 4378407562..0000000000 --- a/.changeset/fix-11655-self-update-minimum-release-age.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -"@pnpm/config.version-policy": minor -"@pnpm/deps.inspection.outdated": patch -"@pnpm/engine.pm.commands": patch -"@pnpm/exec.commands": patch -"@pnpm/installing.deps-resolver": patch -"pnpm": patch ---- - -Make `pnpm self-update` respect `minimumReleaseAge` (and `minimumReleaseAgeExclude`) when resolving which pnpm version to install. - -When the `latest` dist-tag points to a version newer than the configured age threshold, `self-update` now selects the newest mature version instead unless excluded by `minimumReleaseAgeExclude`. - -Also makes `dlx` and `outdated` surface invalid `minimumReleaseAgeExclude` patterns under the same `ERR_PNPM_INVALID_MINIMUM_RELEASE_AGE_EXCLUDE` error code already used by `install`, instead of leaking the internal `ERR_PNPM_INVALID_VERSION_UNION` / `ERR_PNPM_NAME_PATTERN_IN_VERSION_UNION` codes. diff --git a/.changeset/fix-global-allow-builds.md b/.changeset/fix-global-allow-builds.md deleted file mode 100644 index 694534fb1c..0000000000 --- a/.changeset/fix-global-allow-builds.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -"@pnpm/config.reader": patch -"pnpm": patch ---- - -**fix**: global installs respect global config build policy (e.g., `dangerouslyAllowAllBuilds` from config.yaml) when GVS is enabled [#9249](https://github.com/pnpm/pnpm/issues/9249). - -The global virtual-store (GVS) default `allowBuilds = {}` was applied before workspace manifest settings were read and before global config values (stripped by `extractAndRemoveDependencyBuildOptions`) were re-applied via `globalDepsBuildConfig`. This caused `hasDependencyBuildOptions` to return `true` (because `{}` is not null), blocking restoration of global config values like `dangerouslyAllowAllBuilds`. As a result, global installs skipped all build scripts even when the config explicitly allowed them. - -This fix moves the GVS default to **after** workspace manifest reading and `globalDepsBuildConfig` re-application, so that: -1. Workspace manifest `allowBuilds` takes precedence (if present) -2. Global config `dangerouslyAllowAllBuilds` is properly restored (if set and no workspace policy exists) -3. Empty `{}` is only applied as a last resort when no policy is configured anywhere diff --git a/.changeset/fix-minimum-release-age.md b/.changeset/fix-minimum-release-age.md new file mode 100644 index 0000000000..ad0cd04e34 --- /dev/null +++ b/.changeset/fix-minimum-release-age.md @@ -0,0 +1,6 @@ +--- +"@pnpm/resolving.npm-resolver": patch +"pnpm": patch +--- + +Fix `minimumReleaseAgeExclude` handling in npm resolution fast paths so excluded packages do not get pinned to stale versions. Excludes are honored consistently during `publishedBy` metadata selection and cache-mtime shortcuts. diff --git a/.changeset/floppy-parents-teach.md b/.changeset/floppy-parents-teach.md deleted file mode 100644 index 8eb8fcc95b..0000000000 --- a/.changeset/floppy-parents-teach.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@pnpm/lockfile.fs": patch -"pnpm": patch ---- - -Fix lockfile parsing failures when `pnpm-lock.yaml` contains CRLF line endings and multiple YAML documents [#11612](https://github.com/pnpm/pnpm/issues/11612). diff --git a/.changeset/gvs-engine-name-shell-node.md b/.changeset/gvs-engine-name-shell-node.md deleted file mode 100644 index da97ba3b04..0000000000 --- a/.changeset/gvs-engine-name-shell-node.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -"@pnpm/building.after-install": patch -"@pnpm/building.during-install": patch -"@pnpm/deps.graph-builder": patch -"@pnpm/deps.graph-hasher": patch -"@pnpm/engine.runtime.system-node-version": minor -"@pnpm/installing.deps-installer": patch -"@pnpm/installing.deps-resolver": patch -"@pnpm/installing.deps-restorer": patch -"pnpm": patch ---- - -**fix**: anchor the side-effects-cache key and global-virtual-store hash to the project's script-runner Node — `engines.runtime` pin when present, shell `node` otherwise — instead of pnpm's own runtime. - -`ENGINE_NAME` (the `;;node` prefix used as the side-effects-cache key and the engine portion of the GVS hash) was computed from `process.version` — the Node that runs pnpm itself. That was wrong in two situations: - -1. **`@pnpm/exe` SEA bundle.** The bundle has its own embedded Node, not the `node` on the user's `PATH` that actually spawns lifecycle scripts. Two pnpm installations on the same machine (one SEA, one npm-package) therefore disagreed on the cache key, partitioning the side-effects cache and the global virtual store across two Node majors even though both installs would run scripts on the same shell `node`. -2. **`engines.runtime` / `devEngines.runtime` pin.** When a project pins a Node version via `devEngines.runtime` (pnpm v11+), pnpm downloads that Node into `node_modules/node/` and uses it to run lifecycle scripts. But the hash still anchored to whichever Node ran pnpm itself, not to the pinned Node — so two installs of the same project with two different runner Nodes would still disagree on the GVS slot path even though scripts run on the same pinned Node. - -Three changes: - -- `@pnpm/engine.runtime.system-node-version` now exports `engineName(nodeVersion?)` and `findRuntimeNodeVersion(snapshotKeys)`. `engineName()` resolves the version in this order: explicit override → `getSystemNodeVersion()` (which already prefers `node --version` over `process.version` in SEA contexts) → `process.version`. `findRuntimeNodeVersion` scans an iterable of lockfile snapshot keys for a `node@runtime:` entry and returns its bare version string. -- `@pnpm/deps.graph-hasher`'s `calcDepState` and `calcGraphNodeHash`/`iterateHashedGraphNodes` now accept a `nodeVersion?` (in the options bag for the first, as a trailing parameter / ctx field for the others), forwarded to `engineName()`. The default (no override) preserves the pre-change behaviour. The legacy `ENGINE_NAME` constant in `@pnpm/constants` is unchanged so external consumers and existing tests keep working; in non-SEA, non-pinned contexts every value lines up. -- Every install-side caller of the graph-hasher (`@pnpm/installing.deps-resolver`, `@pnpm/installing.deps-restorer`, `@pnpm/installing.deps-installer`, `@pnpm/building.during-install`, `@pnpm/building.after-install`, `@pnpm/deps.graph-builder`) now derives the project's pinned runtime via `findRuntimeNodeVersion(Object.keys(graph))` once per invocation and threads it through. - -On upgrade, two one-time GVS slot churns are possible: - -- **SEA-pnpm users** without a runtime pin: slots that previously hashed under the embedded-Node major (e.g. `node26`) now hash under the shell-Node major (e.g. `node24`), matching what pacquet, the npm-published `pnpm` package, and any other pnpm-compatible tool already produce. -- **Projects with a `devEngines.runtime` pin**: slots that previously hashed under the runner's Node major now hash under the pinned Node major, matching what the lifecycle scripts will actually run on. - -In both cases the old slots become prune-eligible. diff --git a/.changeset/oidc-unresolved-env-placeholder.md b/.changeset/oidc-unresolved-env-placeholder.md deleted file mode 100644 index a316969016..0000000000 --- a/.changeset/oidc-unresolved-env-placeholder.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@pnpm/config.reader": patch -"pnpm": patch ---- - -Fixed `pnpm publish` failing with a 404 when authentication relied on OIDC trusted publishing alongside an `.npmrc` written by `actions/setup-node` (`_authToken=${NODE_AUTH_TOKEN}`) without `NODE_AUTH_TOKEN` being set. Unresolved `${VAR}` placeholders in auth values are now treated as empty rather than passed through verbatim, so the literal placeholder no longer surfaces as a bearer token when OIDC fallback is the intended auth source [#11513](https://github.com/pnpm/pnpm/issues/11513). diff --git a/.changeset/reuse-current-lockfile-when-wanted-missing.md b/.changeset/reuse-current-lockfile-when-wanted-missing.md new file mode 100644 index 0000000000..a93361a34a --- /dev/null +++ b/.changeset/reuse-current-lockfile-when-wanted-missing.md @@ -0,0 +1,9 @@ +--- +"@pnpm/installing.deps-installer": patch +"@pnpm/installing.context": patch +"pnpm": patch +--- + +Skip dependency re-resolution when `pnpm-lock.yaml` is missing but `node_modules/.pnpm/lock.yaml` exists and still satisfies the manifest. `pnpm install` now reuses the materialized snapshot to regenerate `pnpm-lock.yaml` instead of walking the registry to rebuild it from scratch, turning the cache+node_modules variation into a near-no-op for users who deleted the lockfile but kept the install [#11993](https://github.com/pnpm/pnpm/issues/11993). + +`--frozen-lockfile` still refuses to proceed when `pnpm-lock.yaml` is absent — the regenerated lockfile must be committed, so failing loudly is the correct behavior for CI. diff --git a/.changeset/revalidate-minimum-release-age.md b/.changeset/revalidate-minimum-release-age.md deleted file mode 100644 index cca69943e6..0000000000 --- a/.changeset/revalidate-minimum-release-age.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@pnpm/installing.deps-installer": minor -"pnpm": patch ---- - -`minimumReleaseAge` is now re-checked against `pnpm-lock.yaml` before any tarball is installed, so a freshly-published version pinned in the lockfile (e.g. by a developer who bypassed the policy locally) is no longer installed silently by other consumers or CI. Violating entries abort the install with `ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION`; `minimumReleaseAgeExclude` is honored. [#10438](https://github.com/pnpm/pnpm/issues/10438). diff --git a/.github/actions/rustup/action.yml b/.github/actions/rustup/action.yml index 8cd00e8f90..45c190becd 100644 --- a/.github/actions/rustup/action.yml +++ b/.github/actions/rustup/action.yml @@ -7,27 +7,21 @@ inputs: clippy: default: false required: false - type: boolean fmt: default: false required: false - type: boolean docs: default: false required: false - type: boolean restore-cache: default: true required: false - type: boolean save-cache: default: false required: false - type: boolean shared-key: default: 'warm' required: false - type: string runs: using: composite diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 912f45b574..9766284c7c 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -18,10 +18,23 @@ on: required: false default: '1' type: string + push: + # Build Bencher's continuous baseline for the `pnpm` testbed. + # Every merge to main re-runs the bench so PR comparisons have + # an up-to-date reference; cancel-in-progress stays off below + # so we never throw away a partial run. + branches: [main] permissions: contents: read pull-requests: write + checks: write + +# Don't cancel-in-progress — killing a bench mid-run wastes a long +# CI job and produces no usable data. +concurrency: + group: benchmark-${{ inputs.pr_number || github.ref }} + cancel-in-progress: false jobs: benchmark: @@ -52,13 +65,15 @@ jobs: with: runtime: node@26.0.0 - - name: Install hyperfine - run: | - wget -q https://github.com/sharkdp/hyperfine/releases/download/v1.18.0/hyperfine_1.18.0_amd64.deb - sudo dpkg -i hyperfine_1.18.0_amd64.deb + - name: Install Rust Toolchain + uses: ./.github/actions/rustup + with: + shared-key: pnpm-benchmark - - name: Compile - run: pnpm run compile + - name: Install hyperfine + uses: ./.github/actions/binstall + with: + packages: hyperfine@1.18.0 - name: Run benchmarks id: bench @@ -72,6 +87,68 @@ jobs: RUNS: ${{ inputs.runs }} WARMUP: ${{ inputs.warmup }} + - name: Install Bencher CLI + if: steps.bench.outputs.bench_dir != '' + uses: bencherdev/bencher@50fb1e138651a46d2fb704fab1adab38c181552e # v0.6.6 + + - name: Upload results to Bencher + if: steps.bench.outputs.bench_dir != '' + env: + BENCHER_API_TOKEN: ${{ secrets.BENCHER_API_TOKEN }} + BENCH_DIR: ${{ steps.bench.outputs.bench_dir }} + EVENT_NAME: ${{ github.event_name }} + INPUT_PR_NUMBER: ${{ inputs.pr_number }} + REF_NAME: ${{ github.ref_name }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + if [ -z "${BENCHER_API_TOKEN:-}" ]; then + echo "::notice::BENCHER_API_TOKEN not set, skipping Bencher upload" + exit 0 + fi + if [ ! -f "$BENCH_DIR/bencher-results.json" ]; then + echo "::warning::bencher-results.json not found, skipping upload" + exit 0 + fi + + # `bencher run --file` takes hyperfine JSON via the + # shell_hyperfine adapter. Branch policy: + # - push to main → record into the `main` branch (baseline) + # - workflow_dispatch with pr_number → record into `pr/`, + # forked from main at the latest baseline + # - workflow_dispatch without pr_number → record into the + # ref's branch name (e.g. a feature branch), forked from main + args=( + --project pnpm + --testbed pnpm + --adapter shell_hyperfine + --file "$BENCH_DIR/bencher-results.json" + --github-actions "$GITHUB_TOKEN" + ) + # `--start-point-clone-thresholds` so the forked branch inherits + # the threshold configured on main; `--err` so the workflow fails + # when a sample breaches the upper boundary. Main pushes skip + # both — by then the regression has already landed. + if [ "$EVENT_NAME" = "push" ] || [ "$REF_NAME" = "main" ]; then + args+=(--branch main) + elif [ -n "$INPUT_PR_NUMBER" ]; then + args+=( + --branch "pr/$INPUT_PR_NUMBER" + --start-point main + --start-point-reset + --start-point-clone-thresholds + --err + ) + else + args+=( + --branch "$REF_NAME" + --start-point main + --start-point-reset + --start-point-clone-thresholds + --err + ) + fi + bencher run "${args[@]}" + - name: Comment on PR if: steps.bench.outputs.bench_dir != '' env: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 36af72449d..86620cbc69 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,9 +15,6 @@ jobs: name: Compile & Lint runs-on: ubuntu-latest - env: - pnpm_config_verify_deps_before_run: false - steps: - name: Checkout Commit uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -25,10 +22,6 @@ jobs: persist-credentials: false - name: Install pnpm uses: pnpm/setup@b1cac37306e39c21283b9dd6cb0ac288fb35ba6b - with: - install: false - - name: pacquet install - run: pnx --config.minimum-release-age=0 pacquet install --frozen-lockfile - name: Compile TypeScript run: pn compile-only - name: Lint diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 669893dba8..df0d6ad316 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -48,7 +48,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 + uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -59,7 +59,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 + uses: github/codeql-action/autobuild@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -73,4 +73,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 + uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 diff --git a/.github/workflows/pacquet-cargo-unused.yml b/.github/workflows/pacquet-cargo-unused.yml index f3a90934f0..fb3e3f96a2 100644 --- a/.github/workflows/pacquet-cargo-unused.yml +++ b/.github/workflows/pacquet-cargo-unused.yml @@ -27,7 +27,7 @@ jobs: uses: ./.github/actions/rustup - name: Install cargo-unused-features - uses: taiki-e/install-action@e1c4cd42111751368541a7cb5db3522bd1f846a4 # v2.78.0 + uses: taiki-e/install-action@184183c2401be73c3bf42c2e61268aa5855379c1 # v2.78.1 with: tool: cargo-unused-features @@ -51,7 +51,7 @@ jobs: - name: Install cargo-udeps if: steps.filter.outputs.src == 'true' - uses: taiki-e/install-action@e1c4cd42111751368541a7cb5db3522bd1f846a4 # v2.78.0 + uses: taiki-e/install-action@184183c2401be73c3bf42c2e61268aa5855379c1 # v2.78.1 with: tool: cargo-udeps diff --git a/.github/workflows/pacquet-ci.yml b/.github/workflows/pacquet-ci.yml index 18af78518c..fac7ce0ae4 100644 --- a/.github/workflows/pacquet-ci.yml +++ b/.github/workflows/pacquet-ci.yml @@ -1,5 +1,12 @@ name: Pacquet CI +# Despite the name, this workflow covers every Rust crate in the +# repo — pacquet and pnpm-registry alike. They share the same cargo +# workspace, so `just test`, dylint, doc, etc. all see both stacks +# in one pass; running two workflows would just duplicate the work. +# Trigger paths intentionally include `registry/**` for the same +# reason. + permissions: contents: read @@ -9,6 +16,7 @@ on: types: [opened, synchronize] paths: - 'pacquet/**' + - 'registry/**' - 'Cargo.toml' - 'Cargo.lock' - 'rust-toolchain.toml' @@ -27,6 +35,7 @@ on: - main paths: - 'pacquet/**' + - 'registry/**' - 'Cargo.toml' - 'Cargo.lock' - 'rust-toolchain.toml' @@ -83,10 +92,10 @@ jobs: continue-on-error: true - name: Clippy - run: cargo clippy --locked -- -D warnings + run: cargo clippy --locked --workspace --all-targets -- -D warnings - name: Install just - uses: taiki-e/install-action@e1c4cd42111751368541a7cb5db3522bd1f846a4 # v2.78.0 + uses: taiki-e/install-action@184183c2401be73c3bf42c2e61268aa5855379c1 # v2.78.1 with: tool: just @@ -94,7 +103,7 @@ jobs: run: just install - name: Install cargo-nextest - uses: taiki-e/install-action@e1c4cd42111751368541a7cb5db3522bd1f846a4 # v2.78.0 + uses: taiki-e/install-action@184183c2401be73c3bf42c2e61268aa5855379c1 # v2.78.1 with: tool: cargo-nextest @@ -140,7 +149,7 @@ jobs: - uses: crate-ci/typos@5374cbf686e897b15713110e233094e2874de7ef # v1.46.1 with: - files: pacquet + files: pacquet registry deny: name: Cargo Deny @@ -160,7 +169,7 @@ jobs: - name: Install cargo-deny if: steps.filter.outputs.src == 'true' - uses: taiki-e/install-action@e1c4cd42111751368541a7cb5db3522bd1f846a4 # v2.78.0 + uses: taiki-e/install-action@184183c2401be73c3bf42c2e61268aa5855379c1 # v2.78.1 with: tool: cargo-deny @@ -256,7 +265,13 @@ jobs: - name: Install cargo-dylint and dylint-link uses: ./.github/actions/binstall with: - packages: cargo-dylint dylint-link + # Pin to 6.0.0 until trailofbits/dylint cuts a 6.0.2. The + # 6.0.1 binstall artifacts (published 2026-05-26 17:51 UTC) + # fail at driver bootstrap with `failed to read + # /home/runner/work/dylint/dylint/driver/Cargo.toml`, the + # path baked in by dylint's own CI checkout. Downstream + # runners don't have that workspace so the step aborts. + packages: cargo-dylint@6.0.0 dylint-link@6.0.0 - name: Run dylint # `-D warnings` must come in via `RUSTFLAGS`, not as a trailing diff --git a/.github/workflows/pacquet-codecov.yml b/.github/workflows/pacquet-codecov.yml index a245947d81..25b82c930f 100644 --- a/.github/workflows/pacquet-codecov.yml +++ b/.github/workflows/pacquet-codecov.yml @@ -11,11 +11,13 @@ on: types: [opened, synchronize] paths: - 'pacquet/**/*.rs' + - 'registry/**/*.rs' push: branches: - main paths: - 'pacquet/**/*.rs' + - 'registry/**/*.rs' concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -36,12 +38,12 @@ jobs: uses: ./.github/actions/rustup - name: Install cargo-llvm-cov - uses: taiki-e/install-action@e1c4cd42111751368541a7cb5db3522bd1f846a4 # v2.78.0 + uses: taiki-e/install-action@184183c2401be73c3bf42c2e61268aa5855379c1 # v2.78.1 with: tool: cargo-llvm-cov - name: Install cargo-nextest - uses: taiki-e/install-action@e1c4cd42111751368541a7cb5db3522bd1f846a4 # v2.78.0 + uses: taiki-e/install-action@184183c2401be73c3bf42c2e61268aa5855379c1 # v2.78.1 with: tool: cargo-nextest @@ -64,7 +66,7 @@ jobs: continue-on-error: true - name: Install just - uses: taiki-e/install-action@e1c4cd42111751368541a7cb5db3522bd1f846a4 # v2.78.0 + uses: taiki-e/install-action@184183c2401be73c3bf42c2e61268aa5855379c1 # v2.78.1 with: tool: just diff --git a/.github/workflows/pacquet-integrated-benchmark-comment.yml b/.github/workflows/pacquet-integrated-benchmark-comment.yml index 077716bef4..320c5e17fa 100644 --- a/.github/workflows/pacquet-integrated-benchmark-comment.yml +++ b/.github/workflows/pacquet-integrated-benchmark-comment.yml @@ -26,6 +26,7 @@ permissions: actions: read issues: write pull-requests: write + checks: write jobs: comment: @@ -131,3 +132,41 @@ jobs: edit-mode: replace comment-id: ${{ steps.fc.outputs.comment-id }} body-file: benchmark-artifact/SUMMARY.md + + - name: Install Bencher CLI + if: hashFiles('benchmark-artifact/bencher-results.json') != '' + uses: bencherdev/bencher@50fb1e138651a46d2fb704fab1adab38c181552e # v0.6.6 + + - name: Upload PR results to Bencher + # workflow_run runs in the base-repo privilege context, so + # BENCHER_API_TOKEN is available even for fork PRs. PR number + # comes from the trusted workflow_run payload (resolved in + # `meta` above), never from the artifact body. `--ci-number` + # is required here because Bencher can't infer the PR from a + # workflow_run event the way it does on pull_request. + if: hashFiles('benchmark-artifact/bencher-results.json') != '' + env: + BENCHER_API_TOKEN: ${{ secrets.BENCHER_API_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ steps.meta.outputs.pr }} + shell: bash + run: | + if [ -z "${BENCHER_API_TOKEN:-}" ]; then + echo "::notice::BENCHER_API_TOKEN not set, skipping Bencher upload" + exit 0 + fi + # `--start-point-clone-thresholds` so the PR branch inherits the + # threshold configured on main; `--err` so the action surfaces + # a regression on the PR check. + bencher run \ + --project pnpm \ + --testbed pacquet \ + --branch "pr/$PR_NUMBER" \ + --start-point main \ + --start-point-reset \ + --start-point-clone-thresholds \ + --err \ + --adapter shell_hyperfine \ + --file benchmark-artifact/bencher-results.json \ + --ci-number "$PR_NUMBER" \ + --github-actions "$GITHUB_TOKEN" diff --git a/.github/workflows/pacquet-integrated-benchmark.yml b/.github/workflows/pacquet-integrated-benchmark.yml index a154e286fb..4474b88c14 100644 --- a/.github/workflows/pacquet-integrated-benchmark.yml +++ b/.github/workflows/pacquet-integrated-benchmark.yml @@ -2,6 +2,25 @@ name: Pacquet Integrated-Benchmark on: workflow_dispatch: + push: + # Update Bencher's baseline for the `pacquet` testbed whenever + # something that could move pacquet's install perf lands on main. + # Paths mirror the pull_request filter below. + branches: [main] + paths: + - 'pacquet/**/*.rs' + - 'pacquet/**/Cargo.toml' + - 'Cargo.toml' + - 'Cargo.lock' + - 'rust-toolchain.toml' + - 'justfile' + - 'pacquet/tasks/registry-mock/package.json' + - 'pnpm-lock.yaml' + - 'pnpm-workspace.yaml' + - 'pacquet/tasks/integrated-benchmark/src/fixtures/**' + - '.github/actions/rustup/**' + - '.github/actions/binstall/**' + - '.github/workflows/pacquet-integrated-benchmark.yml' pull_request: types: [opened, synchronize] paths: @@ -29,6 +48,7 @@ concurrency: # the base-repo privilege context via `workflow_run`. permissions: contents: read + checks: write jobs: benchmark: @@ -38,6 +58,13 @@ jobs: os: [ubuntu-latest] # windows is skipped because of complexity, macos is skipped because of inconsistency name: Run benchmark on ${{ matrix.os }} runs-on: ${{ matrix.os }} + env: + # On main, HEAD *is* main — comparing them is wasted work, so the + # benchmark runs a single revision and the result is uploaded to + # Bencher as the new baseline. On every other ref (PRs and + # workflow_dispatch from a non-main branch), compare HEAD against + # main so the report shows the relative shift. + BENCHMARK_TARGETS: ${{ github.ref_name == 'main' && 'pacquet@HEAD' || 'pacquet@HEAD pacquet@main' }} steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -67,14 +94,21 @@ jobs: subsection 'Inspecting branches...' git branch - - name: Cache verdaccio + - name: Cache pnpm-registry storage + # Persists pnpm-registry's `--storage` dir across CI runs so + # cold-cache benchmark scenarios don't refetch ~2.3k unscoped + # packages from npmjs on every run. Replaces the older + # `~/.local/share/verdaccio/storage` cache from when the mock + # was Node + Verdaccio. Path matches `runtime_storage()` in + # `pacquet/tasks/registry-mock/src/dirs.rs` (defaults to + # `$HOME/.cache/pnpm-registry/storage`). uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: - key: integrated-benchmark-verdaccio-${{ hashFiles('pacquet/tasks/integrated-benchmark/src/fixtures/pnpm-lock.yaml') }} + key: integrated-benchmark-pnpm-registry-${{ hashFiles('pacquet/tasks/integrated-benchmark/src/fixtures/pnpm-lock.yaml') }} restore-keys: | - integrated-benchmark-verdaccio- + integrated-benchmark-pnpm-registry- path: | - ~/.local/share/verdaccio/storage + ~/.cache/pnpm-registry/storage timeout-minutes: 1 continue-on-error: true @@ -122,7 +156,7 @@ jobs: packages: hyperfine@1.18.0 - name: Install just - uses: taiki-e/install-action@e1c4cd42111751368541a7cb5db3522bd1f846a4 # v2.78.0 + uses: taiki-e/install-action@184183c2401be73c3bf42c2e61268aa5855379c1 # v2.78.1 with: tool: just @@ -135,9 +169,9 @@ jobs: - name: Precompile benchmark revisions shell: bash timeout-minutes: 15 - run: just integrated-benchmark --build-only HEAD main + run: just integrated-benchmark --build-only $BENCHMARK_TARGETS - - name: 'Benchmark: Frozen Lockfile' + - name: 'Benchmark: Isolated linker: fresh restore, cold cache + cold store' shell: bash # Hyperfine has no per-command timeout, so a single hanging # install takes the whole step down with the GHA default job @@ -145,30 +179,44 @@ jobs: # headroom, and a failure surfaces fast enough to iterate on. timeout-minutes: 10 run: | - just integrated-benchmark --scenario=frozen-lockfile --verdaccio --with-pnpm HEAD main - cp bench-work-env/BENCHMARK_REPORT.md bench-work-env/BENCHMARK_REPORT_FROZEN_LOCKFILE.md - cp bench-work-env/BENCHMARK_REPORT.json bench-work-env/BENCHMARK_REPORT_FROZEN_LOCKFILE.json + just integrated-benchmark --scenario=isolated-linker.fresh-restore.cold-cache.cold-store --registry=verdaccio $BENCHMARK_TARGETS + cp bench-work-env/BENCHMARK_REPORT.md bench-work-env/BENCHMARK_REPORT_ISOLATED_FRESH_RESTORE_COLD_CACHE_COLD_STORE.md + cp bench-work-env/BENCHMARK_REPORT.json bench-work-env/BENCHMARK_REPORT_ISOLATED_FRESH_RESTORE_COLD_CACHE_COLD_STORE.json - - name: 'Benchmark: Frozen Lockfile (Hot Cache)' + - name: 'Benchmark: Isolated linker: fresh restore, hot cache + hot store' shell: bash - # Same timeout rationale as the cold-cache step above. - # Mirrors pnpm's "Headless install (frozen lockfile, warm - # store+cache)" benchmark: each timed run wipes only - # `node_modules`, the per-revision store survives, and - # hyperfine's warmup run is what populates it on the first + # Same timeout rationale as the cold-cache step above. Each + # timed run wipes only `node_modules`; the per-revision store + # survives, and hyperfine's warmup populates it on the first # iteration so timed runs all see a hot store. timeout-minutes: 10 run: | - just integrated-benchmark --scenario=frozen-lockfile-hot-cache --verdaccio --with-pnpm HEAD main - cp bench-work-env/BENCHMARK_REPORT.md bench-work-env/BENCHMARK_REPORT_FROZEN_LOCKFILE_HOT_CACHE.md - cp bench-work-env/BENCHMARK_REPORT.json bench-work-env/BENCHMARK_REPORT_FROZEN_LOCKFILE_HOT_CACHE.json + just integrated-benchmark --scenario=isolated-linker.fresh-restore.hot-cache.hot-store --registry=verdaccio $BENCHMARK_TARGETS + cp bench-work-env/BENCHMARK_REPORT.md bench-work-env/BENCHMARK_REPORT_ISOLATED_FRESH_RESTORE_HOT_CACHE_HOT_STORE.md + cp bench-work-env/BENCHMARK_REPORT.json bench-work-env/BENCHMARK_REPORT_ISOLATED_FRESH_RESTORE_HOT_CACHE_HOT_STORE.json - # - name: 'Benchmark: Clean Install' - # shell: bash - # run: | - # just integrated-benchmark --scenario=clean-install --verdaccio --with-pnpm HEAD main - # cp bench-work-env/BENCHMARK_REPORT.md bench-work-env/BENCHMARK_REPORT_CLEAN_INSTALL.md - # cp bench-work-env/BENCHMARK_REPORT.json bench-work-env/BENCHMARK_REPORT_CLEAN_INSTALL.json + - name: 'Benchmark: Isolated linker: fresh install, cold cache + cold store' + shell: bash + # No-lockfile scenario: pacquet does fresh resolution against + # the registry (pnpm/pnpm#11832), so a single iteration runs + # longer than the restore steps. 20 min leaves headroom for the + # default hyperfine 1 warmup + 10 timed runs across both + # benchmark targets without losing the per-command timeout + # safety net the other steps document. + timeout-minutes: 20 + run: | + just integrated-benchmark --scenario=isolated-linker.fresh-install.cold-cache.cold-store --registry=verdaccio $BENCHMARK_TARGETS + cp bench-work-env/BENCHMARK_REPORT.md bench-work-env/BENCHMARK_REPORT_ISOLATED_FRESH_INSTALL_COLD_CACHE_COLD_STORE.md + cp bench-work-env/BENCHMARK_REPORT.json bench-work-env/BENCHMARK_REPORT_ISOLATED_FRESH_INSTALL_COLD_CACHE_COLD_STORE.json + + - name: 'Benchmark: Isolated linker: fresh install, hot cache + hot store' + shell: bash + # Same timeout rationale as the cold-cache install step above. + timeout-minutes: 20 + run: | + just integrated-benchmark --scenario=isolated-linker.fresh-install.hot-cache.hot-store --registry=verdaccio $BENCHMARK_TARGETS + cp bench-work-env/BENCHMARK_REPORT.md bench-work-env/BENCHMARK_REPORT_ISOLATED_FRESH_INSTALL_HOT_CACHE_HOT_STORE.md + cp bench-work-env/BENCHMARK_REPORT.json bench-work-env/BENCHMARK_REPORT_ISOLATED_FRESH_INSTALL_HOT_CACHE_HOT_STORE.json - name: Generate summary shell: bash @@ -176,52 +224,94 @@ jobs: ( echo '## Integrated-Benchmark Report (${{ runner.os }})' echo - echo '### Scenario: Frozen Lockfile' + echo '### Scenario: Isolated linker: fresh restore, cold cache + cold store' echo - cat bench-work-env/BENCHMARK_REPORT_FROZEN_LOCKFILE.md + cat bench-work-env/BENCHMARK_REPORT_ISOLATED_FRESH_RESTORE_COLD_CACHE_COLD_STORE.md echo echo '
BENCHMARK_REPORT.json' echo echo '```json' - jq 'del(.results[].exit_codes)' bench-work-env/BENCHMARK_REPORT_FROZEN_LOCKFILE.json + jq 'del(.results[].exit_codes)' bench-work-env/BENCHMARK_REPORT_ISOLATED_FRESH_RESTORE_COLD_CACHE_COLD_STORE.json echo '```' echo echo '
' echo - echo '### Scenario: Frozen Lockfile (Hot Cache)' + echo '### Scenario: Isolated linker: fresh restore, hot cache + hot store' echo - cat bench-work-env/BENCHMARK_REPORT_FROZEN_LOCKFILE_HOT_CACHE.md + cat bench-work-env/BENCHMARK_REPORT_ISOLATED_FRESH_RESTORE_HOT_CACHE_HOT_STORE.md echo echo '
BENCHMARK_REPORT.json' echo echo '```json' - jq 'del(.results[].exit_codes)' bench-work-env/BENCHMARK_REPORT_FROZEN_LOCKFILE_HOT_CACHE.json + jq 'del(.results[].exit_codes)' bench-work-env/BENCHMARK_REPORT_ISOLATED_FRESH_RESTORE_HOT_CACHE_HOT_STORE.json + echo '```' + echo + echo '
' + echo + echo '### Scenario: Isolated linker: fresh install, cold cache + cold store' + echo + cat bench-work-env/BENCHMARK_REPORT_ISOLATED_FRESH_INSTALL_COLD_CACHE_COLD_STORE.md + echo + echo '
BENCHMARK_REPORT.json' + echo + echo '```json' + jq 'del(.results[].exit_codes)' bench-work-env/BENCHMARK_REPORT_ISOLATED_FRESH_INSTALL_COLD_CACHE_COLD_STORE.json + echo '```' + echo + echo '
' + echo + echo '### Scenario: Isolated linker: fresh install, hot cache + hot store' + echo + cat bench-work-env/BENCHMARK_REPORT_ISOLATED_FRESH_INSTALL_HOT_CACHE_HOT_STORE.md + echo + echo '
BENCHMARK_REPORT.json' + echo + echo '```json' + jq 'del(.results[].exit_codes)' bench-work-env/BENCHMARK_REPORT_ISOLATED_FRESH_INSTALL_HOT_CACHE_HOT_STORE.json echo '```' echo echo '
' - # echo - # echo '### Scenario: Clean Install' - # echo - # cat bench-work-env/BENCHMARK_REPORT_CLEAN_INSTALL.md - # echo - # echo '
BENCHMARK_REPORT.json' - # echo - # echo '```json' - # jq 'del(.results[].exit_codes)' bench-work-env/BENCHMARK_REPORT_CLEAN_INSTALL.json - # echo '```' - # echo - # echo '
' ) > bench-work-env/SUMMARY.md + - name: Build Bencher-shaped report + # Combine the four scenario JSONs into one hyperfine-shaped + # file that Bencher's shell_hyperfine adapter accepts. We + # keep only the @HEAD result from each scenario and rename + # `.command` to the scenario name, so Bencher names the + # benchmark after the scenario instead of `pacquet@HEAD`. + shell: bash + run: | + scenarios=( + 'ISOLATED_FRESH_RESTORE_COLD_CACHE_COLD_STORE:isolated-linker.fresh-restore.cold-cache.cold-store' + 'ISOLATED_FRESH_RESTORE_HOT_CACHE_HOT_STORE:isolated-linker.fresh-restore.hot-cache.hot-store' + 'ISOLATED_FRESH_INSTALL_COLD_CACHE_COLD_STORE:isolated-linker.fresh-install.cold-cache.cold-store' + 'ISOLATED_FRESH_INSTALL_HOT_CACHE_HOT_STORE:isolated-linker.fresh-install.hot-cache.hot-store' + ) + inputs=() + for entry in "${scenarios[@]}"; do + file_tag="${entry%%:*}" + name="${entry#*:}" + src="bench-work-env/BENCHMARK_REPORT_${file_tag}.json" + dst="bench-work-env/${name}-bencher.json" + jq --arg s "$name" \ + '.results |= [.[] | select(.command == "pacquet@HEAD") | .command = $s]' \ + "$src" > "$dst" + inputs+=("$dst") + done + jq -s '{results: map(.results) | add}' \ + "${inputs[@]}" > bench-work-env/bencher-results.json + - name: Stage artifact contents - # The artifact carries only the rendered report. The PR number - # and runner OS are derived from the trusted workflow_run - # event payload in the comment-posting workflow, not from - # fork-controlled artifact contents. + # The artifact carries the rendered report plus the + # Bencher-shaped JSON consumed by the comment-posting + # workflow. The PR number and runner OS are derived from the + # trusted workflow_run event payload in that workflow, not + # from fork-controlled artifact contents. shell: bash run: | mkdir -p benchmark-artifact cp bench-work-env/SUMMARY.md benchmark-artifact/SUMMARY.md + cp bench-work-env/bencher-results.json benchmark-artifact/bencher-results.json - name: Upload benchmark report uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -230,3 +320,47 @@ jobs: path: benchmark-artifact/ if-no-files-found: error retention-days: 14 + + - name: Install Bencher CLI + if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' + uses: bencherdev/bencher@50fb1e138651a46d2fb704fab1adab38c181552e # v0.6.6 + + - name: Upload results to Bencher + # Runs on push and workflow_dispatch — both execute in the + # base-repo privilege context where secrets are available. + # PR runs (including from forks) skip this step and upload + # via the workflow_run comment workflow instead. + if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' + env: + BENCHER_API_TOKEN: ${{ secrets.BENCHER_API_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REF_NAME: ${{ github.ref_name }} + shell: bash + run: | + if [ -z "${BENCHER_API_TOKEN:-}" ]; then + echo "::notice::BENCHER_API_TOKEN not set, skipping Bencher upload" + exit 0 + fi + args=( + --project pnpm + --testbed pacquet + --adapter shell_hyperfine + --file bench-work-env/bencher-results.json + --github-actions "$GITHUB_TOKEN" + ) + # `--start-point-clone-thresholds` so the forked branch inherits + # the threshold configured on main; `--err` so the workflow fails + # when a sample breaches the upper boundary. Main pushes skip + # both — by then the regression has already landed. + if [ "$REF_NAME" = "main" ]; then + args+=(--branch main) + else + args+=( + --branch "$REF_NAME" + --start-point main + --start-point-reset + --start-point-clone-thresholds + --err + ) + fi + bencher run "${args[@]}" diff --git a/.github/workflows/pacquet-micro-benchmark.yml b/.github/workflows/pacquet-micro-benchmark.yml index 3f3d3aa58b..583bd3095b 100644 --- a/.github/workflows/pacquet-micro-benchmark.yml +++ b/.github/workflows/pacquet-micro-benchmark.yml @@ -86,7 +86,7 @@ jobs: steps: - name: Install critcmp - uses: taiki-e/install-action@e1c4cd42111751368541a7cb5db3522bd1f846a4 # v2.78.0 + uses: taiki-e/install-action@184183c2401be73c3bf42c2e61268aa5855379c1 # v2.78.1 with: tool: critcmp diff --git a/.github/workflows/pacquet-release-to-npm.yml b/.github/workflows/pacquet-release-to-npm.yml index 6393e6b808..82ee20453e 100644 --- a/.github/workflows/pacquet-release-to-npm.yml +++ b/.github/workflows/pacquet-release-to-npm.yml @@ -60,7 +60,7 @@ jobs: persist-credentials: false - name: Install cross - uses: taiki-e/install-action@e1c4cd42111751368541a7cb5db3522bd1f846a4 # v2.78.0 + uses: taiki-e/install-action@184183c2401be73c3bf42c2e61268aa5855379c1 # v2.78.1 with: tool: cross @@ -72,6 +72,28 @@ jobs: - name: Add Rust Target run: rustup target add ${{ matrix.target }} + - name: Validate version input + shell: bash + env: + VERSION: ${{ inputs.version }} + run: | + if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then + echo "::error::Invalid version: '$VERSION' (expected semver like 0.2.3 or 0.2.3-rc.1)" + exit 1 + fi + + - name: Inject version into pacquet/crates/cli/src/cli_args.rs + # The version reported by `pacquet --version` is a hardcoded clap + # attribute. Patch it here so the binary built for the matrix leg + # reports the version we're about to publish. + shell: bash + env: + VERSION: ${{ inputs.version }} + run: | + file=pacquet/crates/cli/src/cli_args.rs + perl -i -pe 's/#\[clap\(version = "[^"]*"\)\]/#[clap(version = "'"$VERSION"'")]/' "$file" + grep -F "version = \"$VERSION\"" "$file" + - name: Build with cross run: cross build -p pacquet-cli --bin pacquet --release --target=${{ matrix.target }} @@ -166,7 +188,7 @@ jobs: run: | node pacquet/npm/pacquet/scripts/generate-packages.mjs cat pacquet/npm/pacquet/package.json - for package in pacquet/npm/pacquet*; do cat $package/package.json ; echo ; done + for package in pacquet/npm/pacquet* pacquet/npm/pnpm-pacquet; do cat $package/package.json ; echo ; done - name: Publish npm packages as latest # Auth is via npm's trusted publishing: `id-token: write` above grants @@ -174,7 +196,12 @@ jobs: # so no NPM_TOKEN is needed. `--provenance` attaches the same OIDC # token to a provenance attestation on each tarball. # The trailing slash on $package/ changes it to publishing the directory. + # `pnpm-pacquet` is the `@pnpm/pacquet` scoped alias mirror — + # same shim and same `@pacquet/-` optional deps as the + # unscoped `pacquet`. Published alongside so users can adopt + # either name; the per-platform binary sub-packages stay under + # the `@pacquet/` scope (no need to mirror those). run: | - for package in pacquet/npm/pacquet*; do + for package in pacquet/npm/pacquet* pacquet/npm/pnpm-pacquet; do pnpm publish "$package/" --tag latest --access public --provenance --no-git-checks done diff --git a/.github/workflows/pnpr-release-to-npm.yml b/.github/workflows/pnpr-release-to-npm.yml new file mode 100644 index 0000000000..fbc6eb83e9 --- /dev/null +++ b/.github/workflows/pnpr-release-to-npm.yml @@ -0,0 +1,206 @@ +name: Release @pnpm/pnpr + +# Manual-trigger only. Type the version to publish — the workflow patches +# registry/npm/pnpr/package.json with it before generating per-platform +# packages and publishing. No git tag is created and no GitHub release +# asset is uploaded; npm is the authoritative artifact store. +on: + workflow_dispatch: + inputs: + version: + description: 'Version to publish (e.g. 0.2.3 or 0.2.3-rc.1)' + required: true + type: string + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +permissions: + contents: read + +jobs: + build: + permissions: + contents: read + id-token: write # needed for actions/attest-build-provenance + attestations: write + strategy: + matrix: + include: + - os: windows-latest + target: x86_64-pc-windows-msvc + code-target: win32-x64 + + - os: windows-latest + target: aarch64-pc-windows-msvc + code-target: win32-arm64 + + - os: ubuntu-latest + target: x86_64-unknown-linux-gnu + code-target: linux-x64 + + - os: ubuntu-latest + target: aarch64-unknown-linux-gnu + code-target: linux-arm64 + + - os: macos-latest + target: x86_64-apple-darwin + code-target: darwin-x64 + + - os: macos-latest + target: aarch64-apple-darwin + code-target: darwin-arm64 + + name: Package ${{ matrix.code-target }} + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Install cross + uses: taiki-e/install-action@184183c2401be73c3bf42c2e61268aa5855379c1 # v2.78.1 + with: + tool: cross + + - name: Cache + uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + with: + shared-key: release-${{ matrix.target }} + + - name: Add Rust Target + run: rustup target add ${{ matrix.target }} + + - name: Validate version input + shell: bash + env: + VERSION: ${{ inputs.version }} + run: | + if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then + echo "::error::Invalid version: '$VERSION' (expected semver like 0.2.3 or 0.2.3-rc.1)" + exit 1 + fi + + - name: Inject version into registry/crates/pnpm-registry/Cargo.toml + # `pnpm-registry --version` reads CARGO_PKG_VERSION via clap's + # derive `version` attribute. Patch the crate's `version = "..."` + # field so the binary built for the matrix leg reports the + # version we're about to publish. + shell: bash + env: + VERSION: ${{ inputs.version }} + run: | + file=registry/crates/pnpm-registry/Cargo.toml + perl -i -pe 's/^(version\s*=\s*)"[^"]*"/$1"$ENV{VERSION}"/' "$file" + grep -E '^version\s*=\s*"'"$VERSION"'"' "$file" + + - name: Build with cross + run: cross build -p pnpm-registry --bin pnpm-registry --release --target=${{ matrix.target }} + + # The binary is zipped to fix permission loss https://github.com/actions/upload-artifact#permission-loss + # The Rust crate is `pnpm-registry`, but the npm wrapper exposes + # the command as `pnpr` — rename here so the archive that + # generate-packages.mjs picks up is already named `pnpr-`. + - name: Archive Binary + if: runner.os == 'Windows' + shell: bash + run: | + BIN_NAME=pnpr-${{ matrix.code-target }} + mv target/${{ matrix.target }}/release/pnpm-registry.exe $BIN_NAME.exe + 7z a $BIN_NAME.zip $BIN_NAME.exe + + # The binary is zipped to fix permission loss https://github.com/actions/upload-artifact#permission-loss + - name: Archive Binary + if: runner.os != 'Windows' + run: | + BIN_NAME=pnpr-${{ matrix.code-target }} + mv target/${{ matrix.target }}/release/pnpm-registry $BIN_NAME + tar czf $BIN_NAME.tar.gz $BIN_NAME + + - name: Attest build provenance + uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 + with: + subject-path: | + pnpr-${{ matrix.code-target }}* + + - name: Upload Binary + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + if-no-files-found: error + name: binaries-${{ matrix.code-target }} + path: | + *.zip + *.tar.gz + + publish: + name: Publish + runs-on: ubuntu-latest + permissions: + # Required for npm provenance attestations via OIDC. + id-token: write + needs: + - build + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Validate version input + env: + VERSION: ${{ inputs.version }} + run: | + if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then + echo "::error::Invalid version: '$VERSION' (expected semver like 0.2.3 or 0.2.3-rc.1)" + exit 1 + fi + + - name: Inject version into registry/npm/pnpr/package.json + # The committed package.json has no `version` field; the version comes + # from the workflow_dispatch input. Patching it here means + # generate-packages.mjs (which copies the version into each + # per-platform manifest) and `pnpm publish` (which reads from + # package.json) both see the right value. + env: + VERSION: ${{ inputs.version }} + run: | + jq --arg v "$VERSION" '.version = $v' registry/npm/pnpr/package.json > registry/npm/pnpr/package.json.tmp + mv registry/npm/pnpr/package.json.tmp registry/npm/pnpr/package.json + cat registry/npm/pnpr/package.json + + - name: Install pnpm and Node + uses: pnpm/setup@b1cac37306e39c21283b9dd6cb0ac288fb35ba6b + with: + runtime: node@22 + install: false + + - name: Download Artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + pattern: binaries-* + merge-multiple: true + + - name: Unzip + uses: montudor/action-zip@0852c26906e00f8a315c704958823928d8018b28 # v1.0.0 + with: + args: unzip -qq *.zip -d . + + - name: Untar + run: ls *.gz | xargs -i tar xf {} + + - name: Generate npm packages + run: | + node registry/npm/pnpr/scripts/generate-packages.mjs + cat registry/npm/pnpr/package.json + for package in registry/npm/pnpr*; do cat $package/package.json ; echo ; done + + - name: Publish npm packages as latest + # Auth is via npm's trusted publishing: `id-token: write` above grants + # this job an OIDC token that pnpm/npm exchange with the registry, + # so no NPM_TOKEN is needed. `--provenance` attaches the same OIDC + # token to a provenance attestation on each tarball. + # The trailing slash on $package/ changes it to publishing the directory. + run: | + for package in registry/npm/pnpr*; do + pnpm publish "$package/" --tag latest --access public --provenance --no-git-checks + done diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 216f808428..42ccdcc27e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,7 +30,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - - uses: garnet-org/action@9e819143e63d6dda04bca2e90ac85e3cf0e5289d # v2 + - uses: garnet-org/action@2b7fc9d79b54f551b43358c27424a36064b3e078 # v2 with: api_token: ${{ secrets.GARNET_API_TOKEN }} - name: Install pnpm and Node @@ -97,7 +97,7 @@ jobs: if: startsWith(github.ref, 'refs/tags/') # `gh release create` could replace this, but the release pipeline is # sensitive and softprops/action-gh-release is widely battle-tested. - uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v2.5.0 # zizmor: ignore[superfluous-actions] + uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 # zizmor: ignore[superfluous-actions] with: draft: true files: dist/* diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8a4606e03a..34ff4f12a7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,9 +29,6 @@ jobs: runs-on: ${{ inputs.platform }} - env: - pnpm_config_verify_deps_before_run: false - steps: - name: Configure Git run: | @@ -43,16 +40,13 @@ jobs: with: persist-credentials: false - if: ${{ inputs.garnet }} - uses: garnet-org/action@9e819143e63d6dda04bca2e90ac85e3cf0e5289d # v2 + uses: garnet-org/action@2b7fc9d79b54f551b43358c27424a36064b3e078 # v2 with: api_token: ${{ secrets.GARNET_API_TOKEN }} - name: Install pnpm and Node uses: pnpm/setup@b1cac37306e39c21283b9dd6cb0ac288fb35ba6b with: - install: false runtime: node@${{ inputs.node }} - - name: pacquet install - run: pnx --config.minimum-release-age=0 pacquet install --frozen-lockfile --no-runtime - name: Verify Node version shell: bash env: diff --git a/.github/workflows/update-latest.yml b/.github/workflows/update-latest.yml index 35283fb820..cd2dc7b50a 100644 --- a/.github/workflows/update-latest.yml +++ b/.github/workflows/update-latest.yml @@ -18,7 +18,7 @@ jobs: environment: release runs-on: ubuntu-latest steps: - - uses: garnet-org/action@9e819143e63d6dda04bca2e90ac85e3cf0e5289d # v2 + - uses: garnet-org/action@2b7fc9d79b54f551b43358c27424a36064b3e078 # v2 with: api_token: ${{ secrets.GARNET_API_TOKEN }} - name: Setup Node diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml index 684d7a89f5..0fcad8586f 100644 --- a/.github/workflows/zizmor.yml +++ b/.github/workflows/zizmor.yml @@ -27,7 +27,7 @@ jobs: persist-credentials: false - name: Run zizmor - uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3 + uses: zizmorcore/zizmor-action@a16621b09c6db4281f81a93cb393b05dcd7b7165 # v0.5.5 with: # Fork PRs run with a read-only GITHUB_TOKEN, so SARIF upload to # Code scanning would fail. In that case, run zizmor anyway and diff --git a/.meta-updater/CHANGELOG.md b/.meta-updater/CHANGELOG.md index f118548d29..23dffe6a7b 100644 --- a/.meta-updater/CHANGELOG.md +++ b/.meta-updater/CHANGELOG.md @@ -1,5 +1,42 @@ # @pnpm-private/updater +## 1100.0.15 + +### Patch Changes + +- Updated dependencies [e8b3ae1] +- Updated dependencies [35d2355] + - @pnpm/workspace.projects-reader@1101.0.8 + - @pnpm/types@1101.2.0 + - @pnpm/lockfile.fs@1100.1.2 + - @pnpm/workspace.workspace-manifest-reader@1100.0.5 + +## 1100.0.14 + +### Patch Changes + +- @pnpm/workspace.projects-reader@1101.0.7 + +## 1100.0.13 + +### Patch Changes + +- Updated dependencies [9cb48bb] +- Updated dependencies [64afc92] + - @pnpm/lockfile.fs@1100.1.1 + - @pnpm/types@1101.1.1 + - @pnpm/workspace.projects-reader@1101.0.6 + - @pnpm/workspace.workspace-manifest-reader@1100.0.4 + +## 1100.0.12 + +### Patch Changes + +- Updated dependencies [6e93f35] +- Updated dependencies [2a9bd89] + - @pnpm/lockfile.fs@1100.1.0 + - @pnpm/workspace.projects-reader@1101.0.5 + ## 1100.0.11 ### Patch Changes diff --git a/.meta-updater/package.json b/.meta-updater/package.json index ffce5bc0fc..2d2dd0b3c6 100644 --- a/.meta-updater/package.json +++ b/.meta-updater/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm-private/updater", - "version": "1100.0.11", + "version": "1100.0.15", "private": true, "type": "module", "scripts": { diff --git a/AGENTS.md b/AGENTS.md index 8a11eaae38..214fb07cfe 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,7 +17,7 @@ When you change one side, do the equivalent change on the other in the same PR i "User-visible" means anything that affects the CLI surface or the on-disk contract: command-line flags and defaults, environment-variable handling, lockfile/manifest/state-file format, error codes and messages, log emissions parsed by `@pnpm/cli.default-reporter`, store layout, hook semantics. Pure internal refactors, perf wins, and TS-only test cleanups don't need mirroring. -**Scope caveat:** pacquet currently only implements `install`. Resolution and every other command (`update`, `add`, `remove`, `publish`, `exec`, `run`, `dlx`, `audit`, etc.) live only in the TypeScript code, so changes there don't need a pacquet-side port yet — they're outside pacquet's current surface area. The parity rule will widen as pacquet ports more commands; check what pacquet exposes before deciding whether your change is in scope. +**Scope caveat:** pacquet's current surface area is the dependency-management commands — `install`, `add`, `update`, and `remove`. Every other command (`publish`, `exec`, `run`, `dlx`, `audit`, etc.) lives only in the TypeScript code, so changes there don't need a pacquet-side port yet. The parity rule will widen as pacquet ports more commands; check what pacquet exposes before deciding whether your change is in scope. The pacquet-side obligation — pnpm is the source of truth, pacquet ports from it, never the other way around — is spelled out at [`pacquet/AGENTS.md`](./pacquet/AGENTS.md#the-cardinal-rule). @@ -186,6 +186,24 @@ To ensure your code adheres to the style guide, run: pnpm run lint ``` +### Comments + +Write code that explains itself. A reader should understand what a function does from its name, parameters, and types — not from prose above the call site. + +Defaults: + +- **Do not write a comment** that restates what the code already says. If renaming a variable, splitting a helper, or moving a check to a more obvious place would carry the information, do that instead. +- **Do not repeat documentation** at call sites that already lives on the callee. If the function has a JSDoc, the call site shouldn't re-explain what calling it does. Update the JSDoc once; let every call site benefit. +- **JSDoc is for the function's contract** — preconditions, postconditions, edge cases, why the function exists. Not for re-narrating the body. +- **Do not record past implementation shape, refactor history, or "the previous code did X" framing.** That's what `git log` and `git blame` are for. Describe the current contract — what the code is and what it guarantees — not what it replaced. Phrasings like "used to", "previously", "the original X", or a parenthetical naming a removed type belong in the commit message, not in the source. + +Write a comment only when: + +- The reason for the code is non-obvious from reading it (a hidden invariant, a workaround for a known bug, a deliberate exception to the surrounding pattern). +- The right name doesn't fit — e.g., a temporary technical constraint that's worth flagging but doesn't justify a new symbol. + +Before adding a comment, ask: "Could I rename, restructure, or extract instead?" If yes, do that. The bar for prose-in-code is high; the bar for prose-that-restates-code is "don't." + ## Common Gotchas ### Error Type Checking in Jest (TypeScript only) diff --git a/Cargo.lock b/Cargo.lock index 805f7dd60b..a94a773c2c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -148,9 +148,9 @@ dependencies = [ [[package]] name = "assert_cmd" -version = "2.2.1" +version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39bae1d3fa576f7c6519514180a72559268dd7d1fe104070956cb687bc6673bd" +checksum = "2aa3a22042e45de04255c7bf3626e239f450200fd0493c1e382263544b20aea6" dependencies = [ "anstyle", "bstr", @@ -217,6 +217,54 @@ dependencies = [ "fs_extra", ] +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", +] + [[package]] name = "backtrace" version = "0.3.76" @@ -253,6 +301,19 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bcrypt" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abaf6da45c74385272ddf00e1ac074c7d8a6c1a1dda376902bd6a427522a8b2c" +dependencies = [ + "base64 0.22.1", + "blowfish", + "getrandom 0.3.4", + "subtle", + "zeroize", +] + [[package]] name = "bitflags" version = "2.11.1" @@ -268,6 +329,16 @@ dependencies = [ "generic-array", ] +[[package]] +name = "blowfish" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" +dependencies = [ + "byteorder", + "cipher", +] + [[package]] name = "bstr" version = "1.12.1" @@ -291,6 +362,12 @@ version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.11.1" @@ -365,6 +442,16 @@ dependencies = [ "half", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clap" version = "4.6.1" @@ -1409,6 +1496,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + [[package]] name = "insta" version = "1.47.2" @@ -1668,6 +1764,12 @@ dependencies = [ "regex-automata", ] +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "memchr" version = "2.8.0" @@ -1727,6 +1829,12 @@ dependencies = [ "syn", ] +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -1971,6 +2079,37 @@ version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" +[[package]] +name = "pacquet-catalogs-config" +version = "0.0.1" +dependencies = [ + "derive_more", + "miette 7.6.0", + "pacquet-catalogs-types", + "pacquet-workspace", +] + +[[package]] +name = "pacquet-catalogs-protocol-parser" +version = "0.0.1" +dependencies = [ + "pacquet-catalogs-types", +] + +[[package]] +name = "pacquet-catalogs-resolver" +version = "0.0.1" +dependencies = [ + "derive_more", + "miette 7.6.0", + "pacquet-catalogs-protocol-parser", + "pacquet-catalogs-types", +] + +[[package]] +name = "pacquet-catalogs-types" +version = "0.0.1" + [[package]] name = "pacquet-cli" version = "0.0.1" @@ -2012,6 +2151,7 @@ version = "0.0.1" dependencies = [ "derive_more", "miette 7.6.0", + "pacquet-fs", "pipe-trait", "rayon", "serde_json", @@ -2028,6 +2168,7 @@ dependencies = [ "indexmap", "libc", "miette 7.6.0", + "node-semver", "pacquet-network", "pacquet-package-is-installable", "pacquet-patching", @@ -2037,11 +2178,57 @@ dependencies = [ "pretty_assertions", "serde", "serde-saphyr", + "serde_json", "smart-default", "tempfile", "tracing", ] +[[package]] +name = "pacquet-config-parse-overrides" +version = "0.0.1" +dependencies = [ + "derive_more", + "miette 7.6.0", + "pacquet-catalogs-resolver", + "pacquet-catalogs-types", + "pacquet-resolving-parse-wanted-dependency", +] + +[[package]] +name = "pacquet-crypto-hash" +version = "0.0.1" +dependencies = [ + "sha2", +] + +[[package]] +name = "pacquet-crypto-shasums-file" +version = "0.0.1" +dependencies = [ + "base64 0.22.1", + "derive_more", + "miette 7.6.0", + "pacquet-network", + "pretty_assertions", + "reqwest", + "tokio", +] + +[[package]] +name = "pacquet-deps-path" +version = "0.0.1" +dependencies = [ + "pacquet-crypto-hash", +] + +[[package]] +name = "pacquet-detect-libc" +version = "0.0.1" +dependencies = [ + "pretty_assertions", +] + [[package]] name = "pacquet-diagnostics" version = "0.0.1" @@ -2068,6 +2255,63 @@ dependencies = [ "tracing", ] +[[package]] +name = "pacquet-engine-runtime-bun-resolver" +version = "0.0.1" +dependencies = [ + "derive_more", + "miette 7.6.0", + "pacquet-crypto-shasums-file", + "pacquet-lockfile", + "pacquet-network", + "pacquet-resolving-npm-resolver", + "pacquet-resolving-resolver-base", + "pretty_assertions", + "reqwest", + "serde", + "serde_json", + "ssri", + "tokio", +] + +[[package]] +name = "pacquet-engine-runtime-deno-resolver" +version = "0.0.1" +dependencies = [ + "base64 0.22.1", + "derive_more", + "miette 7.6.0", + "pacquet-lockfile", + "pacquet-network", + "pacquet-resolving-npm-resolver", + "pacquet-resolving-resolver-base", + "pretty_assertions", + "reqwest", + "serde", + "serde_json", + "ssri", + "tokio", +] + +[[package]] +name = "pacquet-engine-runtime-node-resolver" +version = "0.0.1" +dependencies = [ + "derive_more", + "miette 7.6.0", + "node-semver", + "pacquet-crypto-shasums-file", + "pacquet-lockfile", + "pacquet-network", + "pacquet-resolving-resolver-base", + "pretty_assertions", + "reqwest", + "serde", + "serde_json", + "ssri", + "tokio", +] + [[package]] name = "pacquet-executor" version = "0.0.1" @@ -2082,14 +2326,27 @@ dependencies = [ "tracing", ] +[[package]] +name = "pacquet-exportable-manifest" +version = "0.0.1" +dependencies = [ + "derive_more", + "miette 7.6.0", + "pacquet-package-manifest", + "serde_json", + "tempfile", +] + [[package]] name = "pacquet-fs" version = "0.0.1" dependencies = [ "derive_more", + "dunce", "junction", "miette 7.6.0", "pathdiff", + "sha2", "tempfile", ] @@ -2120,6 +2377,7 @@ name = "pacquet-graph-hasher" version = "0.0.1" dependencies = [ "base64 0.22.1", + "pacquet-detect-libc", "pretty_assertions", "serde_json", "sha2", @@ -2148,6 +2406,7 @@ version = "0.0.1" dependencies = [ "derive_more", "node-semver", + "pacquet-crypto-hash", "pacquet-diagnostics", "pacquet-package-manifest", "pipe-trait", @@ -2161,6 +2420,42 @@ dependencies = [ "text-block-macros", ] +[[package]] +name = "pacquet-lockfile-preferred-versions" +version = "0.0.1" +dependencies = [ + "node-semver", + "pacquet-lockfile", + "pacquet-package-manifest", + "pacquet-resolving-resolver-base", + "pretty_assertions", + "serde_json", + "tempfile", +] + +[[package]] +name = "pacquet-lockfile-verification" +version = "0.0.1" +dependencies = [ + "chrono", + "derive_more", + "futures-util", + "insta", + "miette 7.6.0", + "pacquet-diagnostics", + "pacquet-lockfile", + "pacquet-reporter", + "pacquet-resolving-resolver-base", + "pretty_assertions", + "serde", + "serde-saphyr", + "serde_json", + "sha2", + "ssri", + "tempfile", + "tokio", +] + [[package]] name = "pacquet-micro-benchmark" version = "0.0.0" @@ -2190,6 +2485,7 @@ dependencies = [ "httpdate", "indexmap", "pacquet-diagnostics", + "pacquet-fs", "pacquet-testing-utils", "pathdiff", "pipe-trait", @@ -2231,6 +2527,7 @@ name = "pacquet-package-manager" version = "0.0.1" dependencies = [ "async-recursion", + "chrono", "dashmap", "derive_more", "dunce", @@ -2240,14 +2537,24 @@ dependencies = [ "insta", "miette 7.6.0", "node-semver", + "pacquet-catalogs-config", + "pacquet-catalogs-types", "pacquet-cmd-shim", "pacquet-config", + "pacquet-config-parse-overrides", + "pacquet-crypto-hash", + "pacquet-deps-path", "pacquet-directory-fetcher", + "pacquet-engine-runtime-bun-resolver", + "pacquet-engine-runtime-deno-resolver", + "pacquet-engine-runtime-node-resolver", "pacquet-executor", "pacquet-fs", "pacquet-git-fetcher", "pacquet-graph-hasher", "pacquet-lockfile", + "pacquet-lockfile-preferred-versions", + "pacquet-lockfile-verification", "pacquet-modules-yaml", "pacquet-network", "pacquet-package-is-installable", @@ -2257,6 +2564,13 @@ dependencies = [ "pacquet-registry", "pacquet-registry-mock", "pacquet-reporter", + "pacquet-resolving-default-resolver", + "pacquet-resolving-deps-resolver", + "pacquet-resolving-git-resolver", + "pacquet-resolving-local-resolver", + "pacquet-resolving-npm-resolver", + "pacquet-resolving-resolver-base", + "pacquet-resolving-tarball-resolver", "pacquet-store-dir", "pacquet-tarball", "pacquet-testing-utils", @@ -2305,6 +2619,7 @@ dependencies = [ "pretty_assertions", "sha2", "tempfile", + "text-block-macros", ] [[package]] @@ -2344,13 +2659,14 @@ version = "0.0.0" dependencies = [ "assert_cmd", "clap", + "home", "pipe-trait", "reqwest", "serde", "serde_json", "sysinfo", "tokio", - "which", + "walkdir", ] [[package]] @@ -2365,6 +2681,152 @@ dependencies = [ "serde_json", ] +[[package]] +name = "pacquet-resolving-default-resolver" +version = "0.0.1" +dependencies = [ + "derive_more", + "miette 7.6.0", + "pacquet-lockfile", + "pacquet-resolving-resolver-base", + "ssri", + "tokio", +] + +[[package]] +name = "pacquet-resolving-deps-resolver" +version = "0.0.1" +dependencies = [ + "async-recursion", + "derive_more", + "futures-util", + "miette 7.6.0", + "node-semver", + "pacquet-catalogs-resolver", + "pacquet-catalogs-types", + "pacquet-deps-path", + "pacquet-lockfile", + "pacquet-package-manifest", + "pacquet-patching", + "pacquet-resolving-parse-wanted-dependency", + "pacquet-resolving-resolver-base", + "pipe-trait", + "pretty_assertions", + "serde_json", + "tempfile", + "tokio", +] + +[[package]] +name = "pacquet-resolving-git-resolver" +version = "0.0.1" +dependencies = [ + "derive_more", + "miette 7.6.0", + "node-semver", + "pacquet-lockfile", + "pacquet-network", + "pacquet-resolving-resolver-base", + "pretty_assertions", + "reqwest", + "tokio", + "tracing", +] + +[[package]] +name = "pacquet-resolving-jsr-specifier-parser" +version = "0.0.1" +dependencies = [ + "derive_more", + "miette 7.6.0", +] + +[[package]] +name = "pacquet-resolving-local-resolver" +version = "0.0.1" +dependencies = [ + "derive_more", + "home", + "miette 7.6.0", + "pacquet-lockfile", + "pacquet-package-manifest", + "pacquet-resolving-default-resolver", + "pacquet-resolving-resolver-base", + "pacquet-testing-utils", + "pathdiff", + "pretty_assertions", + "serde_json", + "ssri", + "tempfile", + "tokio", + "tracing", +] + +[[package]] +name = "pacquet-resolving-npm-resolver" +version = "0.0.1" +dependencies = [ + "chrono", + "dashmap", + "derive_more", + "futures-util", + "indexmap", + "miette 7.6.0", + "mockito", + "node-semver", + "pacquet-config", + "pacquet-lockfile", + "pacquet-network", + "pacquet-registry", + "pacquet-resolving-default-resolver", + "pacquet-resolving-jsr-specifier-parser", + "pacquet-resolving-local-resolver", + "pacquet-resolving-resolver-base", + "pacquet-workspace-range-resolver", + "pacquet-workspace-spec", + "pipe-trait", + "pretty_assertions", + "reqwest", + "serde", + "serde_json", + "sha2", + "ssri", + "tempfile", + "tokio", + "tracing", +] + +[[package]] +name = "pacquet-resolving-parse-wanted-dependency" +version = "0.0.1" + +[[package]] +name = "pacquet-resolving-resolver-base" +version = "0.0.1" +dependencies = [ + "chrono", + "derive_more", + "pacquet-config", + "pacquet-lockfile", + "serde", + "serde_json", + "ssri", + "tokio", +] + +[[package]] +name = "pacquet-resolving-tarball-resolver" +version = "0.0.1" +dependencies = [ + "mockito", + "pacquet-lockfile", + "pacquet-network", + "pacquet-resolving-resolver-base", + "pretty_assertions", + "reqwest", + "tokio", +] + [[package]] name = "pacquet-store-dir" version = "0.0.1" @@ -2373,6 +2835,7 @@ dependencies = [ "derive_more", "dunce", "miette 7.6.0", + "pacquet-crypto-hash", "pacquet-fs", "pipe-trait", "pretty_assertions", @@ -2438,7 +2901,9 @@ version = "0.0.1" dependencies = [ "derive_more", "miette 7.6.0", + "pacquet-catalogs-types", "pacquet-package-manifest", + "pathdiff", "pretty_assertions", "serde", "serde-saphyr", @@ -2446,6 +2911,17 @@ dependencies = [ "wax", ] +[[package]] +name = "pacquet-workspace-range-resolver" +version = "0.0.1" +dependencies = [ + "node-semver", +] + +[[package]] +name = "pacquet-workspace-spec" +version = "0.0.1" + [[package]] name = "pacquet-workspace-state" version = "0.0.1" @@ -2556,6 +3032,38 @@ dependencies = [ "plotters-backend", ] +[[package]] +name = "pnpm-registry" +version = "0.0.1" +dependencies = [ + "axum", + "base64 0.22.1", + "bcrypt", + "clap", + "derive_more", + "futures-util", + "getrandom 0.3.4", + "indexmap", + "miette 7.6.0", + "mockito", + "pacquet-network", + "pipe-trait", + "reqwest", + "rusqlite", + "serde", + "serde-saphyr", + "serde_json", + "sha2", + "ssri", + "tempfile", + "tokio", + "tower", + "tower-http", + "tracing", + "tracing-subscriber", + "wax", +] + [[package]] name = "pori" version = "0.0.0" @@ -3179,6 +3687,17 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -3238,6 +3757,16 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + [[package]] name = "simd-adler32" version = "0.3.9" @@ -3597,6 +4126,7 @@ dependencies = [ "mio", "parking_lot", "pin-project-lite", + "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.61.2", @@ -3667,6 +4197,7 @@ dependencies = [ "tower", "tower-layer", "tower-service", + "tracing", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index c7bf3cd516..d604e255f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "2" -members = ["pacquet/crates/*", "pacquet/tasks/*"] +members = ["pacquet/crates/*", "pacquet/tasks/*", "registry/crates/*"] [workspace.package] authors = ["Yagiz Nizipli (fn: (x: V) => U, obj: Record): Record export = map diff --git a/__utils__/assert-project/CHANGELOG.md b/__utils__/assert-project/CHANGELOG.md index f4dbd15016..6fe67c5113 100644 --- a/__utils__/assert-project/CHANGELOG.md +++ b/__utils__/assert-project/CHANGELOG.md @@ -1,5 +1,32 @@ # @pnpm/assert-project +## 1100.0.11 + +### Patch Changes + +- Updated dependencies [35d2355] + - @pnpm/types@1101.2.0 + - @pnpm/installing.modules-yaml@1100.0.6 + - @pnpm/lockfile.types@1100.0.8 + - @pnpm/assert-store@1100.0.11 + +## 1100.0.10 + +### Patch Changes + +- Updated dependencies [64afc92] + - @pnpm/types@1101.1.1 + - @pnpm/lockfile.types@1100.0.7 + - @pnpm/installing.modules-yaml@1100.0.5 + - @pnpm/assert-store@1100.0.10 + +## 1100.0.9 + +### Patch Changes + +- @pnpm/lockfile.types@1100.0.6 +- @pnpm/assert-store@1100.0.9 + ## 1100.0.8 ### Patch Changes diff --git a/__utils__/assert-project/package.json b/__utils__/assert-project/package.json index 517cda1483..915ab9f012 100644 --- a/__utils__/assert-project/package.json +++ b/__utils__/assert-project/package.json @@ -1,7 +1,7 @@ { "name": "@pnpm/assert-project", "description": "Utils for testing projects that use pnpm", - "version": "1100.0.8", + "version": "1100.0.11", "author": { "name": "Zoltan Kochan", "email": "z@kochan.io", diff --git a/__utils__/assert-store/CHANGELOG.md b/__utils__/assert-store/CHANGELOG.md index b995eb0b31..9f483f6e3c 100644 --- a/__utils__/assert-store/CHANGELOG.md +++ b/__utils__/assert-store/CHANGELOG.md @@ -1,5 +1,23 @@ # @pnpm/assert-store +## 1100.0.11 + +### Patch Changes + +- @pnpm/store.cafs@1100.1.7 + +## 1100.0.10 + +### Patch Changes + +- @pnpm/store.cafs@1100.1.6 + +## 1100.0.9 + +### Patch Changes + +- @pnpm/store.cafs@1100.1.5 + ## 1100.0.8 ### Patch Changes diff --git a/__utils__/assert-store/package.json b/__utils__/assert-store/package.json index 836cdedf64..32935ca4ad 100644 --- a/__utils__/assert-store/package.json +++ b/__utils__/assert-store/package.json @@ -1,7 +1,7 @@ { "name": "@pnpm/assert-store", "description": "Utils for testing pnpm store", - "version": "1100.0.8", + "version": "1100.0.11", "bugs": { "url": "https://github.com/pnpm/pnpm/issues" }, diff --git a/__utils__/jest-config/CHANGELOG.md b/__utils__/jest-config/CHANGELOG.md index 2fa8a0a2eb..0384e0a855 100644 --- a/__utils__/jest-config/CHANGELOG.md +++ b/__utils__/jest-config/CHANGELOG.md @@ -1,5 +1,25 @@ # @pnpm/jest-config +## 1100.0.11 + +### Patch Changes + +- Updated dependencies [aa6149d] + - @pnpm/worker@1100.1.8 + - @pnpm/testing.registry-mock@1100.0.1 + +## 1100.0.10 + +### Patch Changes + +- @pnpm/worker@1100.1.7 + +## 1100.0.9 + +### Patch Changes + +- @pnpm/worker@1100.1.6 + ## 1100.0.8 ### Patch Changes diff --git a/__utils__/jest-config/package.json b/__utils__/jest-config/package.json index fe15dd091e..05ceef5bd2 100644 --- a/__utils__/jest-config/package.json +++ b/__utils__/jest-config/package.json @@ -1,16 +1,19 @@ { "name": "@pnpm/jest-config", - "version": "1100.0.8", + "version": "1100.0.11", "private": true, "main": "jest-preset.js", "type": "module", "dependencies": { "@babel/core": "catalog:", "@babel/plugin-transform-explicit-resource-management": "catalog:", + "@pnpm/pnpr": "catalog:", "@pnpm/registry-mock": "catalog:", + "@pnpm/testing.registry-mock": "workspace:*", "@pnpm/worker": "workspace:*", "amaro": "catalog:", "get-port": "catalog:", + "read-yaml-file": "catalog:", "tree-kill": "catalog:" }, "devDependencies": { diff --git a/__utils__/jest-config/with-registry/globalSetup.js b/__utils__/jest-config/with-registry/globalSetup.js index 5f49b2c1b9..86cc1db7c6 100644 --- a/__utils__/jest-config/with-registry/globalSetup.js +++ b/__utils__/jest-config/with-registry/globalSetup.js @@ -1,23 +1,50 @@ +import { spawn } from 'node:child_process' +import { createRequire } from 'node:module' import { scheduler } from 'node:timers/promises' +import { promisify } from 'node:util' + import getPort from 'get-port' -import { promisify } from 'util' +import { readYamlFileSync } from 'read-yaml-file' import treeKill from 'tree-kill' + const kill = promisify(treeKill) +const require = createRequire(import.meta.url) export default async () => { if (!process.env.PNPM_REGISTRY_MOCK_PORT) { process.env.PNPM_REGISTRY_MOCK_PORT = (await getPort({ from: 7700, to: 7800 })).toString() } - const { start, prepare } = await import('@pnpm/registry-mock') + + // We still call `prepare()` from `@pnpm/registry-mock`: it copies + // the read-only fixture `storage-cache` into a tempy directory + // and writes `registry/runtime-config-${port}.yaml` with the + // tempy path under `storage:`. That yaml is what + // `locations.storage()` reads when `getIntegrity` (also from + // registry-mock) is called from tests. We just don't launch + // verdaccio against it — we launch pnpm-registry instead. + const { prepare, REGISTRY_MOCK_CREDENTIALS } = await import('@pnpm/registry-mock') + const { addUser } = await import('@pnpm/testing.registry-mock') prepare() - const server = start({ - // Verdaccio stopped working properly on Node.js 22. - // You can test the issue by running: - // pnpm --filter=core run test test/install/auth.ts - useNodeVersion: '20.16.0', - stdio: 'inherit', - listen: process.env.PNPM_REGISTRY_MOCK_PORT, - }) + + const storage = readStoragePath(process.env.PNPM_REGISTRY_MOCK_PORT) + const bin = resolvePnpmRegistryBin() + + const server = spawn( + bin, + [ + '--listen', `127.0.0.1:${process.env.PNPM_REGISTRY_MOCK_PORT}`, + '--storage', storage, + '--upstream', process.env.PNPM_REGISTRY_MOCK_UPLINK ?? 'https://registry.npmjs.org', + '--public-url', `http://localhost:${process.env.PNPM_REGISTRY_MOCK_PORT}`, + // Match registry-mock's verdaccio config: a one-year TTL so + // the fixture packuments (mtime: whenever the npm tarball was + // built) never look stale and never trigger a re-fetch to + // npmjs.org that would 404. + '--packument-ttl-secs', '31536000', + ], + { stdio: 'inherit' } + ) + let killed = false server.on('error', (err) => { console.log(err) @@ -33,14 +60,9 @@ export default async () => { return kill(server.pid) } - // Verdaccio can take a bit of time to come online on Windows and during its - // first startup. Some tests will fail immediately if they begin running - // before Verdaccio starts. Wait for Verdaccio to become online before running - // any tests. await waitForServerOnline() // Register the test user and store the auth token for bearer-based tests - const { addUser, REGISTRY_MOCK_CREDENTIALS } = await import('@pnpm/registry-mock') const { token } = await addUser({ username: REGISTRY_MOCK_CREDENTIALS.username, password: REGISTRY_MOCK_CREDENTIALS.password, @@ -49,7 +71,53 @@ export default async () => { process.env.REGISTRY_MOCK_TOKEN = token } -const UNUSUAL_VERDACCIO_STARTUP_THRESHOLD = 15 // seconds +/** + * Read the `storage:` path that `@pnpm/registry-mock`'s `prepare()` + * just wrote into the runtime-config yaml. We can't import + * `locations.storage` from the registry-mock package — it isn't + * re-exported from its `index.ts` — but the file path is stable. + */ +function readStoragePath (port) { + const configPath = require.resolve( + `@pnpm/registry-mock/registry/runtime-config-${port}.yaml` + ) + const { storage } = readYamlFileSync(configPath) + return storage +} + +/** + * Locate the `pnpm-registry` binary. Lookup order: + * + * 1. `PNPM_REGISTRY_BIN` env var override (escape hatch for local + * Rust work — point it at `target/release/pnpm-registry` to test + * in-progress changes to the registry crate). + * 2. The platform binary that `pnpm install` pulled in as an + * optionalDependency of `@pnpm/pnpr` — i.e. + * `@pnpm/pnpr.-/pnpr[.exe]`. The resolution goes + * through the `@pnpm/pnpr` wrapper's path because the platform + * sub-package lives in the wrapper's own `node_modules`, not + * anywhere on the parent chain of this file. + */ +function resolvePnpmRegistryBin () { + if (process.env.PNPM_REGISTRY_BIN) { + return process.env.PNPM_REGISTRY_BIN + } + const ext = process.platform === 'win32' ? '.exe' : '' + const platformPkg = `@pnpm/pnpr.${process.platform}-${process.arch}` + try { + const wrapperRequire = createRequire(require.resolve('@pnpm/pnpr/bin/pnpr')) + return wrapperRequire.resolve(`${platformPkg}/pnpr${ext}`) + } catch { + throw new Error( + `pnpm-registry binary not found. The test suite expects ${platformPkg} ` + + 'to be installed (it ships as an optionalDependency of @pnpm/pnpr — ' + + 'run `pnpm install` at the repo root to pull it in), or set ' + + 'PNPM_REGISTRY_BIN to an absolute path to a locally-built binary.' + ) + } +} + +const UNUSUAL_REGISTRY_STARTUP_THRESHOLD = 15 // seconds async function waitForServerOnline () { const start = performance.now() @@ -59,17 +127,17 @@ async function waitForServerOnline () { await fetch(`http://localhost:${process.env.PNPM_REGISTRY_MOCK_PORT}`, { method: 'HEAD' }) const totalWait = (performance.now() - start) / 1000 - if (totalWait > UNUSUAL_VERDACCIO_STARTUP_THRESHOLD) { - console.warn(`Verdaccio required an unusually long amount of time to start: ${totalWait} seconds`) + if (totalWait > UNUSUAL_REGISTRY_STARTUP_THRESHOLD) { + console.warn(`pnpm-registry required an unusually long amount of time to start: ${totalWait} seconds`) } return } catch (err) { - // If the Verdaccio process hasn't begun listening yet, attempts to + // If pnpm-registry hasn't begun listening yet, attempts to // connect to the unbound port should throw ECONNREFUSED. If a different // error is observed, throw an error. if (err?.cause?.code !== 'ECONNREFUSED') { - throw new Error('Failed to bring Verdaccio online:', { cause: err }) + throw new Error('Failed to bring pnpm-registry online:', { cause: err }) } await scheduler.wait(delay) @@ -77,7 +145,7 @@ async function waitForServerOnline () { } const totalWait = (performance.now() - start) / 1000 - throw new Error(`Verdaccio did not come online after waiting ${totalWait} seconds`) + throw new Error(`pnpm-registry did not come online after waiting ${totalWait} seconds`) } function *exponentialBackoff (attempts = 15, base = 1.5, initialWait = 100) { @@ -85,3 +153,4 @@ function *exponentialBackoff (attempts = 15, base = 1.5, initialWait = 100) { yield initialWait * Math.pow(base, i) } } + diff --git a/__utils__/prepare/CHANGELOG.md b/__utils__/prepare/CHANGELOG.md index 3b0c45c855..08a4894961 100644 --- a/__utils__/prepare/CHANGELOG.md +++ b/__utils__/prepare/CHANGELOG.md @@ -1,5 +1,27 @@ # @pnpm/prepare +## 1100.0.11 + +### Patch Changes + +- Updated dependencies [35d2355] + - @pnpm/types@1101.2.0 + - @pnpm/assert-project@1100.0.11 + +## 1100.0.10 + +### Patch Changes + +- Updated dependencies [64afc92] + - @pnpm/types@1101.1.1 + - @pnpm/assert-project@1100.0.10 + +## 1100.0.9 + +### Patch Changes + +- @pnpm/assert-project@1100.0.9 + ## 1100.0.8 ### Patch Changes diff --git a/__utils__/prepare/package.json b/__utils__/prepare/package.json index e279dd670e..7d5766fe50 100644 --- a/__utils__/prepare/package.json +++ b/__utils__/prepare/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/prepare", - "version": "1100.0.8", + "version": "1100.0.11", "main": "lib/index.js", "types": "lib/index.d.ts", "type": "module", diff --git a/__utils__/scripts/CHANGELOG.md b/__utils__/scripts/CHANGELOG.md index 1c15762685..6547d85db1 100644 --- a/__utils__/scripts/CHANGELOG.md +++ b/__utils__/scripts/CHANGELOG.md @@ -1,5 +1,32 @@ # @pnpm/scripts +## 1100.0.11 + +### Patch Changes + +- Updated dependencies [e8b3ae1] + - @pnpm/workspace.projects-reader@1101.0.8 + - @pnpm/workspace.workspace-manifest-reader@1100.0.5 + +## 1100.0.10 + +### Patch Changes + +- @pnpm/workspace.projects-reader@1101.0.7 + +## 1100.0.9 + +### Patch Changes + +- @pnpm/workspace.projects-reader@1101.0.6 +- @pnpm/workspace.workspace-manifest-reader@1100.0.4 + +## 1100.0.8 + +### Patch Changes + +- @pnpm/workspace.projects-reader@1101.0.5 + ## 1100.0.7 ### Patch Changes diff --git a/__utils__/scripts/package.json b/__utils__/scripts/package.json index 3e43c3a28c..914b2e1e54 100644 --- a/__utils__/scripts/package.json +++ b/__utils__/scripts/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/scripts", - "version": "1100.0.7", + "version": "1100.0.11", "private": true, "type": "module", "scripts": { diff --git a/agent/client/CHANGELOG.md b/agent/client/CHANGELOG.md index 1c82f05ebe..45ee57f8f3 100644 --- a/agent/client/CHANGELOG.md +++ b/agent/client/CHANGELOG.md @@ -1,5 +1,30 @@ # @pnpm/agent.client +## 1.0.8 + +### Patch Changes + +- Updated dependencies [aa6149d] + - @pnpm/worker@1100.1.8 + - @pnpm/lockfile.types@1100.0.8 + - @pnpm/store.cafs@1100.1.7 + +## 1.0.7 + +### Patch Changes + +- @pnpm/lockfile.types@1100.0.7 +- @pnpm/store.cafs@1100.1.6 +- @pnpm/worker@1100.1.7 + +## 1.0.6 + +### Patch Changes + +- @pnpm/lockfile.types@1100.0.6 +- @pnpm/store.cafs@1100.1.5 +- @pnpm/worker@1100.1.6 + ## 1.0.5 ### Patch Changes diff --git a/agent/client/package.json b/agent/client/package.json index a8a7b54d61..2406c5b987 100644 --- a/agent/client/package.json +++ b/agent/client/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/agent.client", - "version": "1.0.5", + "version": "1.0.8", "description": "Client for pnpm agent server — sends store state, receives resolved lockfile and missing files", "keywords": [ "pnpm", diff --git a/agent/server/CHANGELOG.md b/agent/server/CHANGELOG.md index 550a7adbe3..2fa2c07140 100644 --- a/agent/server/CHANGELOG.md +++ b/agent/server/CHANGELOG.md @@ -1,5 +1,70 @@ # pnpm-agent +## 0.0.19 + +### Patch Changes + +- Updated dependencies [aa6149d] +- Updated dependencies [35d2355] + - @pnpm/installing.deps-installer@1101.5.0 + - @pnpm/types@1101.2.0 + - @pnpm/installing.client@1100.2.3 + - @pnpm/lockfile.fs@1100.1.2 + - @pnpm/lockfile.types@1100.0.8 + - @pnpm/store.cafs@1100.1.7 + - @pnpm/store.controller@1101.0.9 + +## 0.0.18 + +### Patch Changes + +- Updated dependencies [212315d] + - @pnpm/installing.deps-installer@1101.4.0 + - @pnpm/installing.client@1100.2.2 + - @pnpm/store.controller@1101.0.8 + +## 0.0.17 + +### Patch Changes + +- @pnpm/installing.client@1100.2.1 +- @pnpm/store.controller@1101.0.8 +- @pnpm/installing.deps-installer@1101.3.1 + +## 0.0.16 + +### Patch Changes + +- Updated dependencies [9cb48bb] +- Updated dependencies [1627943] +- Updated dependencies [b206a15] +- Updated dependencies [64afc92] + - @pnpm/lockfile.fs@1100.1.1 + - @pnpm/installing.client@1100.2.0 + - @pnpm/installing.deps-installer@1101.3.0 + - @pnpm/types@1101.1.1 + - @pnpm/store.controller@1101.0.8 + - @pnpm/lockfile.types@1100.0.7 + - @pnpm/store.cafs@1100.1.6 + +## 0.0.15 + +### Patch Changes + +- Updated dependencies [4195766] +- Updated dependencies [31538bf] +- Updated dependencies [6e93f35] +- Updated dependencies [3ddde2b] +- Updated dependencies [4a79336] +- Updated dependencies [2a9bd89] +- Updated dependencies [31538bf] + - @pnpm/installing.client@1100.1.0 + - @pnpm/installing.deps-installer@1101.2.0 + - @pnpm/lockfile.fs@1100.1.0 + - @pnpm/lockfile.types@1100.0.6 + - @pnpm/store.controller@1101.0.7 + - @pnpm/store.cafs@1100.1.5 + ## 0.0.14 ### Patch Changes diff --git a/agent/server/package.json b/agent/server/package.json index 7cd17f5eb9..d3a77640f8 100644 --- a/agent/server/package.json +++ b/agent/server/package.json @@ -1,6 +1,6 @@ { "name": "pnpm-agent", - "version": "0.0.14", + "version": "0.0.19", "description": "pnpm agent server for server-side resolution and store-aware downloads", "keywords": [ "pnpm", diff --git a/auth/commands/CHANGELOG.md b/auth/commands/CHANGELOG.md index de005ec7e5..a88ff1e44c 100644 --- a/auth/commands/CHANGELOG.md +++ b/auth/commands/CHANGELOG.md @@ -1,5 +1,54 @@ # @pnpm/auth.commands +## 1100.1.2 + +### Patch Changes + +- ae21758: Refactor the dist-tag-add and login (classic adduser) handlers to delegate their PUTs to a new shared package `@pnpm/registry-access.client`. Downstream tests in this monorepo now use these helpers (via `@pnpm/testing.registry-mock`) instead of `addDistTag` / `addUser` from `@pnpm/registry-mock`, which relied on the unmaintained `anonymous-npm-registry-client`. +- Updated dependencies [a23956e] +- Updated dependencies [35d2355] + - @pnpm/config.reader@1101.4.1 + - @pnpm/cli.utils@1101.0.8 + - @pnpm/network.fetch@1100.0.7 + - @pnpm/registry-access.client@1100.0.1 + +## 1100.1.1 + +### Patch Changes + +- Updated dependencies [3b62f9d] +- Updated dependencies [212315d] + - @pnpm/config.reader@1101.4.0 + - @pnpm/cli.utils@1101.0.7 + +## 1100.1.0 + +### Minor Changes + +- 56f3851: Implement the documented `pnpm login --scope ` flag. The scope is normalized (a leading `@` is added if missing; blank values are ignored) and an `@:registry=` mapping is written to the pnpm auth file alongside the auth token. Subsequent installs of `@/*` packages then route to the chosen registry. Previously `pnpm login --scope foo` errored with `Unknown option: 'scope'` despite the flag being listed in the online documentation [#11716](https://github.com/pnpm/pnpm/issues/11716). + +### Patch Changes + +- Updated dependencies [3687b0e] +- Updated dependencies [ced20cb] +- Updated dependencies [d1b340f] + - @pnpm/config.reader@1101.3.3 + - @pnpm/cli.utils@1101.0.6 + - @pnpm/network.fetch@1100.0.6 + +## 1100.0.14 + +### Patch Changes + +- Updated dependencies [020ac45] +- Updated dependencies [d3f8408] +- Updated dependencies [a62f959] +- Updated dependencies [ba2c884] +- Updated dependencies [8df408c] + - @pnpm/config.reader@1101.3.2 + - @pnpm/network.fetch@1100.0.5 + - @pnpm/cli.utils@1101.0.5 + ## 1100.0.13 ### Patch Changes diff --git a/auth/commands/package.json b/auth/commands/package.json index 2d555a739e..a14f80167e 100644 --- a/auth/commands/package.json +++ b/auth/commands/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/auth.commands", - "version": "1100.0.13", + "version": "1100.1.2", "description": "Commands for authentication with npm registries", "keywords": [ "pnpm", @@ -37,6 +37,7 @@ "@pnpm/error": "workspace:*", "@pnpm/network.fetch": "workspace:*", "@pnpm/network.web-auth": "workspace:*", + "@pnpm/registry-access.client": "workspace:*", "enquirer": "catalog:", "normalize-registry-url": "catalog:", "read-ini-file": "catalog:", diff --git a/auth/commands/src/login.ts b/auth/commands/src/login.ts index 17754bbeb5..cbffd2724e 100644 --- a/auth/commands/src/login.ts +++ b/auth/commands/src/login.ts @@ -15,6 +15,7 @@ import { type WebAuthFetchOptions, withOtpHandling, } from '@pnpm/network.web-auth' +import { addUser, AddUserHttpError, AddUserNoTokenError } from '@pnpm/registry-access.client' import enquirer from 'enquirer' import normalizeRegistryUrl from 'normalize-registry-url' import { readIniFile } from 'read-ini-file' @@ -24,7 +25,10 @@ import { writeIniFile } from 'write-ini-file' import { getRegistryConfigKey, safeReadIniFile } from './shared.js' export function rcOptionsTypes (): Record { - return { registry: allTypes.registry } + return { + registry: allTypes.registry, + scope: allTypes.scope, + } } export function cliOptionsTypes (): Record { @@ -46,11 +50,15 @@ export function help (): string { description: 'The registry to log in to', name: '--registry ', }, + { + description: 'Associate an operation with a scope for a scoped registry. The scope-to-registry mapping is recorded so future installs in the same scope use the chosen registry.', + name: '--scope ', + }, ], }, ], url: docsUrl('login'), - usages: ['pnpm login [--registry ]'], + usages: ['pnpm login [--registry ] [--scope ]'], }) } @@ -65,6 +73,7 @@ export type LoginCommandOptions = Pick & { registry?: string + scope?: string } export async function handler ( @@ -101,19 +110,7 @@ export interface LoginFetchResponseHeaders { export interface LoginFetchOptions { method?: 'GET' | 'POST' | 'PUT' - headers?: { - accept: 'application/json' - 'content-type': 'application/json' - - // Q: Why does pnpm send this header unconditionally? - // A: This header doesn't say "I prefer web-based authentication"; - // it only says "I am capable of web-based authentication". - // The npm CLI does the same: - // - 'npm-auth-type': 'web' - - 'npm-otp'?: string - } + headers?: Record body?: string retry?: { factor?: number @@ -202,11 +199,28 @@ export async function login ({ context = DEFAULT_CONTEXT, opts }: LoginParams): const settings = await safeReadIniFile(readIniFile, configPath) as Record const registryConfigKey = getRegistryConfigKey(registry) settings[`${registryConfigKey}:_authToken`] = token + // Persist the scope → registry mapping next to the auth token so subsequent + // installs for `@scope/*` packages route to this registry. `auth.ini` is + // already an allowed source of `@scope:registry=` (see config/reader). + const scopeKey = normalizeScope(opts.scope) + if (scopeKey != null) { + settings[`${scopeKey}:registry`] = registry + } await writeIniFile(configPath, settings) return `Logged in on ${registry}` } +// `--scope foo` and `--scope @foo` should both produce `@foo`. Empty / blank +// values are treated as unset so accidental whitespace doesn't write a broken +// `@:registry=` entry. +function normalizeScope (scope: string | undefined): string | undefined { + if (scope == null) return undefined + const trimmed = scope.trim() + if (trimmed === '' || trimmed === '@') return undefined + return trimmed.startsWith('@') ? trimmed : `@${trimmed}` +} + interface WebLoginParams { context: Pick fetchOptions: WebAuthFetchOptions @@ -291,45 +305,32 @@ async function classicLogin ({ throw new LoginMissingCredentialsError() } - const loginUrl = new URL(`-/user/org.couchdb.user:${encodeURIComponent(username)}`, registry).href - const token = await withOtpHandling({ context, fetchOptions, operation: async (otp?: string) => { - const response = await fetch(loginUrl, { - method: 'PUT', - headers: { - 'content-type': 'application/json', - accept: 'application/json', - 'npm-auth-type': 'web', - // Conditionally include npm-otp: some HTTP implementations coerce - // `undefined` to the string "undefined", which would send a bad header - // on the initial attempt (before OTP is known). - ...(otp != null ? { 'npm-otp': otp } : {}), - }, - body: JSON.stringify({ - _id: `org.couchdb.user:${username}`, - name: username, + try { + const result = await addUser({ + username, password, email, - type: 'user', - }), - }) - - if (!response.ok) { - await throwIfOtpRequired(globalWarn, response) - const text = await response.text() - throw new ClassicLoginError(response.status, text) + otp, + registryUrl: registry, + fetch, + }) + return result.token + } catch (err) { + if (err instanceof AddUserHttpError) { + if (err.status === 401 && err.responseHeaders.get('www-authenticate')?.includes('otp')) { + throw SyntheticOtpError.fromUnknownBody(globalWarn, err.responseJson) + } + throw new ClassicLoginError(err.status, err.responseText) + } + if (err instanceof AddUserNoTokenError) { + throw new LoginNoTokenError() + } + throw err } - - const body = await response.json() as { token?: string } - - if (!body.token) { - throw new LoginNoTokenError() - } - - return body.token }, }) @@ -338,25 +339,6 @@ async function classicLogin ({ return token } -/** - * Inspects a non-ok HTTP response for OTP requirements and throws an EOTP - * error when detected. This mirrors the behaviour of npm-registry-fetch, - * which checks the `www-authenticate` header for one-time password indicators. - */ -async function throwIfOtpRequired (globalWarn: LoginContext['globalWarn'], response: LoginFetchResponse): Promise { - if (response.status !== 401) return - - const wwwAuth = response.headers.get('www-authenticate') - if (!wwwAuth?.includes('otp')) return - - let body: unknown - try { - body = await response.json() - } catch {} - - throw SyntheticOtpError.fromUnknownBody(globalWarn, body) -} - class LoginNonInteractiveError extends PnpmError { constructor () { super('LOGIN_NON_INTERACTIVE', 'The login command requires an interactive terminal') diff --git a/auth/commands/test/login.test.ts b/auth/commands/test/login.test.ts index 901b35d32f..d3a477602c 100644 --- a/auth/commands/test/login.test.ts +++ b/auth/commands/test/login.test.ts @@ -141,6 +141,103 @@ describe('login', () => { ]) }) + it('should persist a scope→registry mapping when --scope is provided', async () => { + let savedSettings: Record = {} + const context = createMockContext({ + globalInfo: jest.fn(), + readIniFile: async () => ({}), + writeIniFile: async (_configPath, settings) => { + savedSettings = settings + }, + fetch: async url => { + if (url === 'https://my-org.example/-/v1/login') { + return createMockResponse({ + ok: true, + status: 200, + json: { + loginUrl: 'https://my-org.example/auth/login', + doneUrl: 'https://my-org.example/auth/done', + }, + }) + } + if (url === 'https://my-org.example/auth/done') { + return createMockResponse({ + ok: true, + status: 200, + json: { token: 'scoped-token' }, + }) + } + throw new Error(`Unexpected call to fetch: ${url}`) + }, + }) + // `--scope my-org` (no `@`) should be normalized to `@my-org` when written. + const opts = { configDir: '/mock/config', dir: '/mock', authConfig: {}, registry: 'https://my-org.example', scope: 'my-org' } + const result = await login({ context, opts }) + expect(result).toBe('Logged in on https://my-org.example/') + expect(savedSettings).toMatchObject({ + '//my-org.example/:_authToken': 'scoped-token', + '@my-org:registry': 'https://my-org.example/', + }) + }) + + it('should accept --scope with a leading @ and not double-prefix', async () => { + let savedSettings: Record = {} + const context = createMockContext({ + globalInfo: jest.fn(), + readIniFile: async () => ({}), + writeIniFile: async (_configPath, settings) => { + savedSettings = settings + }, + fetch: async url => { + if (url === 'https://my-org.example/-/v1/login') { + return createMockResponse({ + ok: true, + status: 200, + json: { + loginUrl: 'https://my-org.example/auth/login', + doneUrl: 'https://my-org.example/auth/done', + }, + }) + } + return createMockResponse({ ok: true, status: 200, json: { token: 'tok' } }) + }, + }) + const opts = { configDir: '/mock/config', dir: '/mock', authConfig: {}, registry: 'https://my-org.example', scope: '@my-org' } + await login({ context, opts }) + expect(savedSettings['@my-org:registry']).toBe('https://my-org.example/') + expect(savedSettings['@@my-org:registry']).toBeUndefined() + }) + + it('should not write a scope mapping when --scope is omitted', async () => { + let savedSettings: Record = {} + const context = createMockContext({ + globalInfo: jest.fn(), + readIniFile: async () => ({}), + writeIniFile: async (_configPath, settings) => { + savedSettings = settings + }, + fetch: async url => { + if (url === 'https://example.com/-/v1/login') { + return createMockResponse({ + ok: true, + status: 200, + json: { + loginUrl: 'https://example.com/auth/login', + doneUrl: 'https://example.com/auth/done', + }, + }) + } + return createMockResponse({ ok: true, status: 200, json: { token: 'tok' } }) + }, + }) + const opts = { configDir: '/mock/config', dir: '/mock', authConfig: {}, registry: 'https://example.com' } + await login({ context, opts }) + // No `@…:registry` key should be added when scope isn't passed. + for (const key of Object.keys(savedSettings)) { + expect(key.startsWith('@')).toBe(false) + } + }) + it('should fall back to classic login when web login returns 404', async () => { const fetchedUrls: string[] = [] const globalInfo = jest.fn() @@ -266,10 +363,10 @@ describe('login', () => { return createMockResponse({ ok: false, status: 401, - json: { + text: JSON.stringify({ authUrl: 'https://example.org/auth/web', doneUrl: 'https://example.org/auth/web/done', - }, + }), headers: { get: (name: string) => name === 'www-authenticate' ? 'OTP otp' : null }, }) } diff --git a/auth/commands/tsconfig.json b/auth/commands/tsconfig.json index dcfd90dfc1..3b02b3705a 100644 --- a/auth/commands/tsconfig.json +++ b/auth/commands/tsconfig.json @@ -26,6 +26,9 @@ }, { "path": "../../network/web-auth" + }, + { + "path": "../../registry-access/client" } ] } diff --git a/benchmarks/README.md b/benchmarks/README.md index 14cfe9f9ae..c6d62754b9 100644 --- a/benchmarks/README.md +++ b/benchmarks/README.md @@ -1,52 +1,77 @@ # pnpm Benchmarks -Compares `pnpm install` performance between the current branch and `main`. +Compares `pnpm install` performance between the current branch (`HEAD`) and +`main`, across the six scenarios listed below. + +This wrapper builds both pnpm revisions and runs hyperfine through the +shared Rust orchestrator at +[`pacquet/tasks/integrated-benchmark`](../pacquet/tasks/integrated-benchmark/), +so scenario / fixture / workspace / install-script / report generation +stay consistent with the pacquet benchmark. ## Prerequisites -- [hyperfine](https://github.com/sharkdp/hyperfine) — install via `brew install hyperfine` -- The current branch must be compiled (`pnpm run compile`) -- If providing a pre-existing main checkout path, it must also be compiled +- `cargo` (install Rust via [rustup](https://rustup.rs) if you don't have it). +- `hyperfine`, `pnpm`, `node`, `git` on `$PATH`. ## Usage ```sh -pnpm run compile ./benchmarks/bench.sh ``` -If a git worktree with `main` already exists, the script finds and uses it automatically. Otherwise it creates one at `../.pnpm-bench-main` (a sibling of the repo), installs dependencies, and compiles. +The script: -You can also point to a specific checkout of main: - -```sh -./benchmarks/bench.sh /path/to/main -``` +1. Builds the `integrated-benchmark` binary in release mode. +2. Clones the current repo into the temp work-env once per revision + (`HEAD` and `main`) and runs `pnpm install && pnpm run compile-only` + in each to produce `pnpm/dist/pnpm.mjs`. `compile-only` skips the + `update-manifests` pass that the root `compile` script does — it + would rewrite tracked files and trigger a second install per + revision, neither of which the bench needs. +3. Runs hyperfine on each scenario with `--registry=npm` (hits + `registry.npmjs.org` directly, no proxy — same as before). +4. Writes a per-scenario `BENCHMARK_REPORT.md` / `.json` and a + consolidated `results.md` into the temp work-env. The path is printed + at the end of the run. +5. Emits `bencher-results.json` — a hyperfine-shaped file with one + result per scenario (the `@HEAD` revision only, `command` renamed to + the scenario name) that the `Benchmarks` GitHub Actions workflow + uploads to [Bencher](https://bencher.dev) for continuous tracking. ## Scenarios -| # | Name | Lockfile | Store + Cache | Description | -|---|---|---|---|---| -| 1 | Headless | ✔ frozen | warm | Repeat install with warm store | -| 2 | Re-resolution | ✔ + add dep | warm | Add a new dependency to an existing lockfile | -| 3 | Full resolution | ✗ | warm | Resolve everything from scratch with warm store and cache | -| 4 | Headless cold | ✔ frozen | cold | Typical CI install — fetch all packages with lockfile | -| 5 | Cold install | ✗ | cold | True cold start — nothing cached | +Slugs follow `...` so the +leading segment groups runs by linker mode. Today there are two +groups (`isolated-linker.*` and `gvs-linker.*`); future scenarios +will add `hoisted-linker.*` and `pnp-linker.*`. -All scenarios use `--ignore-scripts` and isolated store/cache directories per variant. +Every current scenario starts with `node_modules` wiped — "fresh" +names that target state; future variants that begin with a populated +`node_modules` will use a different action prefix. -## Output +| # | Slug | Lockfile | Cache | Store | Description | +|---|---|---|---|---|---| +| 1 | `isolated-linker.fresh-restore.hot-cache.hot-store` | ✔ frozen | hot | hot | Restore from lockfile with both directories hot (repeat-headless shape) | +| 2 | `isolated-linker.fresh-add-dep.hot-cache.hot-store` | ✔ + add dep | hot | hot | `pnpm add ` against an existing lockfile | +| 3 | `isolated-linker.fresh-install.hot-cache.hot-store` | ✗ | hot | hot | Resolve from scratch with both directories hot | +| 4 | `isolated-linker.fresh-restore.cold-cache.cold-store` | ✔ frozen | cold | cold | Restore from lockfile with cold disks (typical CI shape) | +| 5 | `isolated-linker.fresh-install.cold-cache.cold-store` | ✗ | cold | cold | True cold start — no lockfile, nothing cached | +| 6 | `gvs-linker.fresh-restore.hot-cache.hot-store` | ✔ frozen | hot | hot + GVS | Frozen-lockfile restore with `enableGlobalVirtualStore: true`, pre-warmed GVS | -Results are printed to the terminal and saved as: +All scenarios use `--ignore-scripts` and isolated store/cache directories per revision. -- `results.md` — consolidated markdown table -- `-main.json` / `-branch.json` — raw hyperfine data +## Fixture -All files are written to a temp directory printed at the end of the run. +The fixture lives at [`fixture/`](./fixture/) — a synthetic +`package.json` with ~80 typical front-end dependencies, plus a committed +`pnpm-lock.yaml` (generated once with `pnpm install --lockfile-only`). +The lockfile is checked in so every CI run starts from the same +resolution graph regardless of registry drift. ## Configuration -Edit the variables at the top of `bench.sh`: +Environment variables read by `bench.sh`: - `WARMUP` — number of warmup runs before timing (default: 1) - `RUNS` — number of timed runs per benchmark (default: 10) diff --git a/benchmarks/bench.sh b/benchmarks/bench.sh index d079e0cc83..0475992ca4 100755 --- a/benchmarks/bench.sh +++ b/benchmarks/bench.sh @@ -1,290 +1,153 @@ #!/bin/bash set -euo pipefail -# Benchmark script for pnpm install performance. -# Compares the current (active) branch against a baseline checkout of main. +# Thin wrapper around `pacquet/tasks/integrated-benchmark`. Builds two +# pnpm revisions (the current branch and `main`) and runs hyperfine for +# each of the six scenarios that used to live in this script. # -# Prerequisites: -# - hyperfine (https://github.com/sharkdp/hyperfine) -# - The current branch must be compiled (pnpm run compile) +# Scenarios, registry choice, and runner behaviour are preserved exactly +# as before; the orchestration logic is shared with the pacquet bench. # -# Usage: -# ./benchmarks/bench.sh [path-to-main-checkout] +# Prerequisites: cargo, hyperfine, pnpm, node, git. # -# If no path is given, a git worktree for main is created automatically, -# dependencies are installed, and pnpm is compiled in it. +# Env vars: WARMUP (default 1), RUNS (default 10). # -# Examples: -# pnpm run compile -# ./benchmarks/bench.sh -# ./benchmarks/bench.sh /Volumes/src/pnpm/pnpm/main +# Usage: ./benchmarks/bench.sh -BRANCH_DIR="$(cd "$(dirname "$0")/.." && pwd)" - -if [ -n "${1:-}" ]; then - MAIN_DIR="$1" -else - # Look for an existing worktree that has main checked out - EXISTING=$(git -C "$BRANCH_DIR" worktree list --porcelain \ - | awk '/^worktree /{wt=$2} /^branch refs\/heads\/main$/{print wt}') - - if [ -n "$EXISTING" ]; then - MAIN_DIR="$EXISTING" - echo "── Using existing main worktree at $MAIN_DIR ──" - else - MAIN_DIR="$BRANCH_DIR/../.pnpm-bench-main" - echo "── Creating main worktree at $MAIN_DIR ──" - git -C "$BRANCH_DIR" worktree add "$MAIN_DIR" main - fi - - cd "$MAIN_DIR" - echo "Installing dependencies..." - pnpm install - echo "Compiling..." - pnpm run compile - echo "" - cd "$BRANCH_DIR" -fi - -BENCH_DIR="$(mktemp -d "${TMPDIR:-/tmp}/pnpm-bench.XXXXXX")" +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +FIXTURE_DIR="$REPO_ROOT/benchmarks/fixture" WARMUP="${WARMUP:-1}" RUNS="${RUNS:-10}" +BENCH_DIR="$(mktemp -d "${TMPDIR:-/tmp}/pnpm-bench.XXXXXX")" -# ── Per-variant configuration ───────────────────────────────────────────── - -resolve_pnpm_bin() { - local dir="$1" - if [ -f "$dir/pnpm/dist/pnpm.mjs" ]; then - echo "$dir/pnpm/dist/pnpm.mjs" - else - echo "$dir/pnpm/dist/pnpm.cjs" +for tool in cargo hyperfine pnpm node git; do + if ! command -v "$tool" >/dev/null; then + echo "error: $tool not on PATH" >&2 + exit 1 fi -} +done -VARIANTS=("main" "branch") -VARIANT_DIRS=("$MAIN_DIR" "$BRANCH_DIR") -VARIANT_BINS=("$(resolve_pnpm_bin "$MAIN_DIR")" "$(resolve_pnpm_bin "$BRANCH_DIR")") -VARIANT_PROJECTS=("$BENCH_DIR/project-main" "$BENCH_DIR/project-branch") -VARIANT_STORES=("$BENCH_DIR/store-main" "$BENCH_DIR/store-branch") -VARIANT_CACHES=("$BENCH_DIR/cache-main" "$BENCH_DIR/cache-branch") +echo "── Building integrated-benchmark ──" +cargo build --release --bin=integrated-benchmark --manifest-path "$REPO_ROOT/Cargo.toml" +BIN="$REPO_ROOT/target/release/integrated-benchmark" -# ── Validation ────────────────────────────────────────────────────────────── - -if ! command -v hyperfine &>/dev/null; then - echo "error: hyperfine is required. Install via: brew install hyperfine" >&2 - exit 1 +# Ensure `pnpm@main` resolves locally. `actions/checkout` only creates a +# local ref for the branch it checked out; on a workflow_dispatch run from +# a non-main branch (or after the optional PR-head checkout in +# `benchmark.yml`) there's no `refs/heads/main` for `git rev-parse` to +# hit. Skip the fetch entirely when the local ref already exists, and +# let the fetch surface its real error if it fails. +if ! git -C "$REPO_ROOT" rev-parse --verify --quiet refs/heads/main >/dev/null; then + echo "── Fetching main into local ref ──" + git -C "$REPO_ROOT" fetch --no-tags origin main:main fi -for bin in "${VARIANT_BINS[@]}"; do - if [ ! -f "$bin" ]; then - echo "error: compiled pnpm not found at $bin" >&2 - echo "Run 'pnpm run compile' in both repos first." >&2 - exit 1 +# Scenario list: `slug:Display label`. The slug matches the +# orchestrator's `--scenario` value (the clap-derived kebab-case name +# from `BenchmarkScenario`). All six start with `node_modules` wiped +# — "Fresh" names that target state. "Isolated linker" names the +# `nodeLinker` mode; alternatives (`hoisted`, `pnp`) and populated- +# node_modules counterparts are reserved for future scenarios. +SCENARIOS=( + "isolated-linker.fresh-restore.hot-cache.hot-store:Isolated linker: fresh restore, hot cache + hot store" + "isolated-linker.fresh-add-dep.hot-cache.hot-store:Isolated linker: fresh add new dep, hot cache + hot store" + "isolated-linker.fresh-install.hot-cache.hot-store:Isolated linker: fresh install, hot cache + hot store" + "isolated-linker.fresh-restore.cold-cache.cold-store:Isolated linker: fresh restore, cold cache + cold store" + "isolated-linker.fresh-install.cold-cache.cold-store:Isolated linker: fresh install, cold cache + cold store" + "gvs-linker.fresh-restore.hot-cache.hot-store:GVS linker: fresh restore, hot cache + hot store" +) + +# Pre-build both revisions once. Subsequent scenario invocations still +# re-enter the orchestrator's build step (sync_bench_repo + pnpm install +# + pnpm run compile), but `pnpm install` is a no-op on the populated +# node_modules and `tsgo --build` is incremental. `pnpm run bundle` +# (which produces pnpm/dist/pnpm.mjs) does run each time and is not +# incremental — accepted overhead in exchange for keeping the build +# path in one consistent place across pacquet and pnpm benches. +echo "── Pre-building pnpm revisions ──" +"$BIN" \ + --pnpm-repository "$REPO_ROOT" \ + --work-env "$BENCH_DIR/work-env" \ + --build-only \ + pnpm@HEAD pnpm@main + +# Pull mean ± stddev for each variant out of a hyperfine JSON into one +# table cell. Falls back to "n/a" if jq isn't on PATH, the file is +# missing, or the target isn't present in the JSON. +read_cell() { + local target=$1 + local json=$2 + if ! command -v jq >/dev/null; then + echo "n/a" + return fi -done - -for i in "${!VARIANTS[@]}"; do - # Run --version from BENCH_DIR to avoid pnpm's automatic version switching - # based on a packageManager field in the current directory. - echo "${VARIANTS[$i]}: $(cd "$BENCH_DIR" && node "${VARIANT_BINS[$i]}" --version) (${VARIANT_DIRS[$i]})" -done -echo "workdir: $BENCH_DIR" -echo "" - -# ── Project setup ─────────────────────────────────────────────────────────── -# Each variant gets its own project directory with isolated store and cache -# so there is no shared state between them. - -for i in "${!VARIANTS[@]}"; do - dir="${VARIANT_PROJECTS[$i]}" - mkdir -p "$dir" "${VARIANT_CACHES[$i]}" - cp "$BRANCH_DIR/benchmarks/fixture.package.json" "$dir/package.json" - printf "storeDir: %s\ncacheDir: %s\n" "${VARIANT_STORES[$i]}" "${VARIANT_CACHES[$i]}" > "$dir/pnpm-workspace.yaml" -done - -# Keep a pristine copy of package.json for the peek benchmark -cp "$BRANCH_DIR/benchmarks/fixture.package.json" "$BENCH_DIR/original-package.json" - -# ── Populate stores and caches ───────────────────────────────────────────── -# A full install populates both the content-addressable store and the -# registry metadata cache for each variant. - -for i in "${!VARIANTS[@]}"; do - label="${VARIANTS[$i]}" - dir="${VARIANT_PROJECTS[$i]}" - bin="${VARIANT_BINS[$i]}" - echo "Populating store and cache for $label..." - rm -rf "$dir/node_modules" "$dir/pnpm-lock.yaml" - cd "$dir" && node "$bin" install --ignore-scripts --no-frozen-lockfile - if [ ! -f "$dir/pnpm-lock.yaml" ]; then - echo "error: pnpm-lock.yaml was not created for $label in $dir" >&2 - exit 1 - fi - cp "$dir/pnpm-lock.yaml" "$BENCH_DIR/saved-lockfile-$label.yaml" -done - -# ── Helper ────────────────────────────────────────────────────────────────── -# run_bench -# -# Templates use placeholders that are substituted per variant: -# {project} → project directory -# {bin} → compiled pnpm binary -# {store} → store directory -# {cache} → cache directory -# {lockfile} → saved lockfile path - -run_bench() { - local bench_name=$1 - local prepare_tpl=$2 - local cmd_tpl=$3 - - for i in "${!VARIANTS[@]}"; do - local variant="${VARIANTS[$i]}" - local project="${VARIANT_PROJECTS[$i]}" - local bin="${VARIANT_BINS[$i]}" - local store="${VARIANT_STORES[$i]}" - local cache="${VARIANT_CACHES[$i]}" - local lockfile="$BENCH_DIR/saved-lockfile-$variant.yaml" - - local prepare="$prepare_tpl" - prepare="${prepare//\{project\}/$project}" - prepare="${prepare//\{bin\}/$bin}" - prepare="${prepare//\{store\}/$store}" - prepare="${prepare//\{cache\}/$cache}" - prepare="${prepare//\{lockfile\}/$lockfile}" - - local cmd="$cmd_tpl" - cmd="${cmd//\{project\}/$project}" - cmd="${cmd//\{bin\}/$bin}" - cmd="${cmd//\{store\}/$store}" - cmd="${cmd//\{cache\}/$cache}" - cmd="${cmd//\{lockfile\}/$lockfile}" - - echo "" - echo " $variant:" - hyperfine \ - --warmup "$WARMUP" \ - --runs "$RUNS" \ - --ignore-failure \ - --prepare "$prepare" \ - --command-name "$variant" \ - "$cmd" \ - --export-json "$BENCH_DIR/${bench_name}-${variant}.json" \ - || true - done + jq -r --arg t "$target" ' + [.results[] | select(.command == $t) + | "\((.mean*1000|round)/1000)s ± \((.stddev*1000|round)/1000)s"] + | first // "n/a" + ' "$json" 2>/dev/null || echo "n/a" } -# ── Benchmark 1: Headless install ────────────────────────────────────────── -# Lockfile present, node_modules deleted, store and cache warm. -# This is the common "CI install" or "fresh clone + install" path. +results_md="$BENCH_DIR/results.md" +{ + echo "# Benchmark Results" + echo + echo "| # | Scenario | main | HEAD |" + echo "|---|---|---|---|" +} > "$results_md" -echo "" -echo "━━━ Benchmark 1: Headless install (frozen lockfile, warm store+cache) ━━━" - -run_bench "headless" \ - "rm -rf {project}/node_modules && cp {lockfile} {project}/pnpm-lock.yaml" \ - "cd {project} && node {bin} install --frozen-lockfile --ignore-scripts >/dev/null 2>&1" - -# ── Benchmark 2: Re-resolution with existing lockfile ───────────────────── -# Lockfile present, add a new dependency to trigger re-resolution. -# Store and cache warm. This exercises the peekManifestFromStore path. - -echo "" -echo "━━━ Benchmark 2: Re-resolution (add dep to existing lockfile, warm store+cache) ━━━" - -run_bench "peek" \ - "rm -rf {project}/node_modules && cp {lockfile} {project}/pnpm-lock.yaml && cp $BENCH_DIR/original-package.json {project}/package.json" \ - "cd {project} && node {bin} add is-odd --ignore-scripts >/dev/null 2>&1" - -# ── Benchmark 3: Full resolution (warm store+cache) ────────────────────── -# No lockfile, no node_modules, store and cache warm. -# Resolution runs for all packages using cached registry metadata. - -echo "" -echo "━━━ Benchmark 3: Full resolution (no lockfile, warm store+cache) ━━━" - -run_bench "nolockfile" \ - "rm -rf {project}/node_modules {project}/pnpm-lock.yaml && cp $BENCH_DIR/original-package.json {project}/package.json" \ - "cd {project} && node {bin} install --ignore-scripts --no-frozen-lockfile >/dev/null 2>&1" - -# ── Benchmark 4: Headless cold (lockfile, no store, no cache) ───────────── -# Lockfile present, but store and cache are empty. -# This tests the fetch-from-registry + link path guided by a lockfile. - -echo "" -echo "━━━ Benchmark 4: Headless install (frozen lockfile, cold store+cache) ━━━" - -run_bench "headless-cold" \ - "rm -rf {project}/node_modules {store} {cache} && cp {lockfile} {project}/pnpm-lock.yaml" \ - "cd {project} && node {bin} install --frozen-lockfile --ignore-scripts >/dev/null 2>&1" - -# ── Benchmark 5: Cold install (no store, no cache, no lockfile) ─────────── -# Everything is deleted before each run. This is the true cold start. - -echo "" -echo "━━━ Benchmark 5: Cold install (no store, no cache, no lockfile) ━━━" - -run_bench "cold" \ - "rm -rf {project}/node_modules {project}/pnpm-lock.yaml {store} {cache} && cp $BENCH_DIR/original-package.json {project}/package.json" \ - "cd {project} && node {bin} install --ignore-scripts --no-frozen-lockfile >/dev/null 2>&1" - -# ── Benchmark 6: GVS warm reinstall ─────────────────────────────────────── -# Global virtual store enabled, GVS warm, node_modules deleted. -# This tests the reattach fast-path: all packages should be skipped -# (no fetch/import) because their GVS hash directories already exist. - -echo "" -echo "━━━ Benchmark 6: GVS warm reinstall (frozen lockfile, warm global virtual store) ━━━" - -# Set up separate GVS-enabled project directories per variant -GVS_PROJECTS=() -for i in "${!VARIANTS[@]}"; do - gvs_dir="$BENCH_DIR/project-gvs-${VARIANTS[$i]}" - mkdir -p "$gvs_dir" - cp "$BRANCH_DIR/benchmarks/fixture.package.json" "$gvs_dir/package.json" - printf "storeDir: %s\ncacheDir: %s\nenableGlobalVirtualStore: true\n" \ - "${VARIANT_STORES[$i]}" "${VARIANT_CACHES[$i]}" > "$gvs_dir/pnpm-workspace.yaml" - GVS_PROJECTS+=("$gvs_dir") - - # Warm the GVS with a full install - echo "Warming GVS for ${VARIANTS[$i]}..." - cd "$gvs_dir" && node "${VARIANT_BINS[$i]}" install --ignore-scripts --no-frozen-lockfile >/dev/null 2>&1 - cp "$gvs_dir/pnpm-lock.yaml" "$BENCH_DIR/saved-lockfile-gvs-${VARIANTS[$i]}.yaml" -done - -for i in "${!VARIANTS[@]}"; do - variant="${VARIANTS[$i]}" - gvs_project="${GVS_PROJECTS[$i]}" - bin="${VARIANT_BINS[$i]}" - lockfile="$BENCH_DIR/saved-lockfile-gvs-$variant.yaml" +i=1 +for entry in "${SCENARIOS[@]}"; do + scenario="${entry%%:*}" + label="${entry#*:}" echo "" - echo " $variant:" - hyperfine \ + echo "━━━ Benchmark $i: $label ━━━" + + "$BIN" \ + --scenario "$scenario" \ + --registry npm \ + --pnpm-repository "$REPO_ROOT" \ + --fixture-dir "$FIXTURE_DIR" \ + --work-env "$BENCH_DIR/work-env" \ --warmup "$WARMUP" \ --runs "$RUNS" \ --ignore-failure \ - --prepare "rm -rf $gvs_project/node_modules && cp $lockfile $gvs_project/pnpm-lock.yaml" \ - --command-name "$variant" \ - "cd $gvs_project && node $bin install --frozen-lockfile --ignore-scripts >/dev/null 2>&1" \ - --export-json "$BENCH_DIR/gvs-warm-${variant}.json" \ - || true + pnpm@main pnpm@HEAD + + cp "$BENCH_DIR/work-env/BENCHMARK_REPORT.md" "$BENCH_DIR/${scenario}.md" + cp "$BENCH_DIR/work-env/BENCHMARK_REPORT.json" "$BENCH_DIR/${scenario}.json" + + main_cell=$(read_cell "pnpm@main" "$BENCH_DIR/${scenario}.json") + head_cell=$(read_cell "pnpm@HEAD" "$BENCH_DIR/${scenario}.json") + echo "| $i | $label | $main_cell | $head_cell |" >> "$results_md" + + i=$((i + 1)) done -# ── Summary ───────────────────────────────────────────────────────────────── +# Combine the per-scenario hyperfine JSONs into one Bencher-shaped +# report. Keep only the @HEAD result from each scenario and rename +# `.command` to the scenario name so Bencher's shell_hyperfine adapter +# names the benchmark after the scenario instead of `pnpm@HEAD`. +if command -v jq >/dev/null; then + bencher_inputs=() + for entry in "${SCENARIOS[@]}"; do + scenario="${entry%%:*}" + jq --arg s "$scenario" \ + '.results |= [.[] | select(.command == "pnpm@HEAD") | .command = $s]' \ + "$BENCH_DIR/${scenario}.json" > "$BENCH_DIR/${scenario}-bencher.json" + bencher_inputs+=("$BENCH_DIR/${scenario}-bencher.json") + done + jq -s '{results: map(.results) | add}' \ + "${bencher_inputs[@]}" > "$BENCH_DIR/bencher-results.json" +else + echo "warning: jq not on PATH; skipping bencher-results.json generation" >&2 +fi -RESULTS_MD="$BENCH_DIR/results.md" - -echo "" +echo echo "━━━ Results ━━━" -node "$BRANCH_DIR/benchmarks/generate-results.js" "$BENCH_DIR" "$RESULTS_MD" -echo "" -echo "Results saved to: $RESULTS_MD" - -# Cleanup -for project in "${VARIANT_PROJECTS[@]}" "${GVS_PROJECTS[@]}"; do - rm -rf "$project/node_modules" -done -echo "" +cat "$results_md" +echo +echo "Results saved to: $results_md" echo "Temp directory kept at: $BENCH_DIR" echo "Remove with: rm -rf $BENCH_DIR" diff --git a/benchmarks/fixture.package.json b/benchmarks/fixture/package.json similarity index 100% rename from benchmarks/fixture.package.json rename to benchmarks/fixture/package.json diff --git a/benchmarks/fixture/pnpm-lock.yaml b/benchmarks/fixture/pnpm-lock.yaml new file mode 100644 index 0000000000..9690937e02 --- /dev/null +++ b/benchmarks/fixture/pnpm-lock.yaml @@ -0,0 +1,12144 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@babel/core': + specifier: ^7.24.0 + version: 7.29.0 + '@babel/plugin-transform-runtime': + specifier: ^7.24.0 + version: 7.29.0(@babel/core@7.29.0) + '@babel/preset-env': + specifier: ^7.24.0 + version: 7.29.5(@babel/core@7.29.0) + '@babel/preset-react': + specifier: ^7.23.3 + version: 7.28.5(@babel/core@7.29.0) + '@babel/preset-typescript': + specifier: ^7.23.3 + version: 7.28.5(@babel/core@7.29.0) + '@babel/runtime': + specifier: ^7.24.0 + version: 7.29.2 + '@emotion/react': + specifier: ^11.11.3 + version: 11.14.0(@types/react@18.3.28)(react@18.3.1) + '@emotion/styled': + specifier: ^11.11.0 + version: 11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1) + '@mui/material': + specifier: ^5.15.7 + version: 5.18.0(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@reduxjs/toolkit': + specifier: ^2.1.0 + version: 2.12.0(react-redux@9.3.0(@types/react@18.3.28)(react@18.3.1)(redux@5.0.1))(react@18.3.1) + '@tanstack/react-query': + specifier: ^5.18.1 + version: 5.100.11(react@18.3.1) + '@tanstack/react-table': + specifier: ^8.11.6 + version: 8.21.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@types/node': + specifier: ^20.11.10 + version: 20.19.41 + '@types/react': + specifier: ^18.2.48 + version: 18.3.28 + '@types/react-dom': + specifier: ^18.2.18 + version: 18.3.7(@types/react@18.3.28) + '@typescript-eslint/eslint-plugin': + specifier: ^7.0.1 + version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^7.0.1 + version: 7.18.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + autoprefixer: + specifier: ^10.4.17 + version: 10.5.0(postcss@8.5.14) + axios: + specifier: ^1.6.7 + version: 1.16.1 + chart.js: + specifier: ^4.4.1 + version: 4.5.1 + clean-webpack-plugin: + specifier: ^4.0.0 + version: 4.0.0(webpack@5.106.2) + css-loader: + specifier: ^6.9.1 + version: 6.11.0(webpack@5.106.2) + cssnano: + specifier: ^6.0.3 + version: 6.1.2(postcss@8.5.14) + date-fns: + specifier: ^3.3.1 + version: 3.6.0 + eslint: + specifier: ^9.9.0 + version: 9.39.4(jiti@2.7.0) + eslint-config-prettier: + specifier: ^9.1.0 + version: 9.1.2(eslint@9.39.4(jiti@2.7.0)) + eslint-plugin-prettier: + specifier: ^5.1.3 + version: 5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@9.1.2(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0))(prettier@3.8.3) + express: + specifier: ^4.18.2 + version: 4.22.2 + formik: + specifier: ^2.4.5 + version: 2.4.9(@types/react@18.3.28)(react@18.3.1) + framer-motion: + specifier: ^11.0.3 + version: 11.18.2(@emotion/is-prop-valid@1.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + html-webpack-plugin: + specifier: ^5.6.0 + version: 5.6.7(webpack@5.106.2) + husky: + specifier: ^9.0.7 + version: 9.1.7 + i18next: + specifier: ^23.8.2 + version: 23.16.8 + immer: + specifier: ^10.0.3 + version: 10.2.0 + jquery: + specifier: ^3.7.1 + version: 3.7.1 + js-cookie: + specifier: ^3.0.5 + version: 3.0.7 + less: + specifier: ^4.2.0 + version: 4.6.4 + less-loader: + specifier: ^12.2.0 + version: 12.3.2(less@4.6.4)(webpack@5.106.2) + lint-staged: + specifier: ^15.2.1 + version: 15.5.2 + lodash: + specifier: ^4.17.21 + version: 4.18.1 + mini-css-extract-plugin: + specifier: ^2.7.7 + version: 2.10.2(webpack@5.106.2) + mobx: + specifier: ^6.12.0 + version: 6.15.3 + mobx-react-lite: + specifier: ^4.0.5 + version: 4.1.1(mobx@6.15.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + moment: + specifier: ^2.30.1 + version: 2.30.1 + moment-timezone: + specifier: ^0.5.44 + version: 0.5.48 + postcss: + specifier: ^8.4.33 + version: 8.5.14 + postcss-loader: + specifier: ^8.1.0 + version: 8.2.1(postcss@8.5.14)(typescript@5.9.3)(webpack@5.106.2) + postcss-preset-env: + specifier: ^9.3.0 + version: 9.6.0(postcss@8.5.14) + prettier: + specifier: ^3.2.4 + version: 3.8.3 + qs: + specifier: ^6.11.2 + version: 6.15.2 + react: + specifier: ^18.2.0 + version: 18.3.1 + react-dnd: + specifier: ^16.0.1 + version: 16.0.1(@types/hoist-non-react-statics@3.3.7(@types/react@18.3.28))(@types/node@20.19.41)(@types/react@18.3.28)(react@18.3.1) + react-dnd-html5-backend: + specifier: ^16.0.1 + version: 16.0.1 + react-dom: + specifier: ^18.2.0 + version: 18.3.1(react@18.3.1) + react-dropzone: + specifier: ^14.2.3 + version: 14.4.1(react@18.3.1) + react-helmet-async: + specifier: ^2.0.4 + version: 2.0.5(react@18.3.1) + react-hook-form: + specifier: ^7.49.3 + version: 7.76.0(react@18.3.1) + react-i18next: + specifier: ^14.0.5 + version: 14.1.3(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-redux: + specifier: ^9.1.0 + version: 9.3.0(@types/react@18.3.28)(react@18.3.1)(redux@5.0.1) + react-router: + specifier: ^6.21.3 + version: 6.30.3(react@18.3.1) + react-router-dom: + specifier: ^6.21.3 + version: 6.30.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-select: + specifier: ^5.8.0 + version: 5.10.2(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-tooltip: + specifier: ^5.25.2 + version: 5.30.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-virtualized: + specifier: ^9.22.5 + version: 9.22.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + recharts: + specifier: ^2.12.0 + version: 2.15.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + redux: + specifier: ^5.0.1 + version: 5.0.1 + sass: + specifier: ^1.70.0 + version: 1.99.0 + sass-loader: + specifier: ^14.1.0 + version: 14.2.1(sass@1.99.0)(webpack@5.106.2) + sortablejs: + specifier: ^1.15.2 + version: 1.15.7 + style-loader: + specifier: ^3.3.4 + version: 3.3.4(webpack@5.106.2) + stylelint: + specifier: ^16.2.1 + version: 16.26.1(typescript@5.9.3) + stylelint-config-standard: + specifier: ^36.0.0 + version: 36.0.1(stylelint@16.26.1(typescript@5.9.3)) + terser-webpack-plugin: + specifier: ^5.3.10 + version: 5.6.0(cssnano@6.1.2(postcss@8.5.14))(postcss@8.5.14)(webpack@5.106.2) + typescript: + specifier: ^5.3.3 + version: 5.9.3 + uuid: + specifier: ^9.0.1 + version: 9.0.1 + webpack: + specifier: ^5.90.1 + version: 5.106.2(cssnano@6.1.2(postcss@8.5.14))(postcss@8.5.14)(webpack-cli@5.1.4) + webpack-bundle-analyzer: + specifier: ^4.10.1 + version: 4.10.2 + webpack-cli: + specifier: ^5.1.4 + version: 5.1.4(webpack-bundle-analyzer@4.10.2)(webpack-dev-server@4.15.2)(webpack@5.106.2) + webpack-dev-server: + specifier: ^4.15.2 + version: 4.15.2(webpack-cli@5.1.4)(webpack@5.106.2) + webpack-merge: + specifier: ^5.10.0 + version: 5.10.0 + whatwg-fetch: + specifier: ^3.6.20 + version: 3.6.20 + yup: + specifier: ^1.3.3 + version: 1.7.1 + zod: + specifier: ^3.22.4 + version: 3.25.76 + zustand: + specifier: ^4.5.0 + version: 4.5.7(@types/react@18.3.28)(immer@10.2.0)(react@18.3.1) + devDependencies: + '@testing-library/jest-dom': + specifier: ^6.4.1 + version: 6.9.1 + '@testing-library/react': + specifier: ^14.2.1 + version: 14.3.1(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@20.19.41)(babel-plugin-macros@3.1.0) + jest-environment-jsdom: + specifier: ^29.7.0 + version: 29.7.0 + +packages: + + '@adobe/css-tools@4.4.4': + resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==} + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.3': + resolution: {integrity: sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.27.3': + resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-create-class-features-plugin@7.29.3': + resolution: {integrity: sha512-RpLYy2sb51oNLjuu1iD3bwBqCBWUzjO0ocp+iaCP/lJtb2CPLcnC2Fftw+4sAzaMELGeWTgExSKADbdo0GFVzA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-create-regexp-features-plugin@7.28.5': + resolution: {integrity: sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-define-polyfill-provider@0.6.8': + resolution: {integrity: sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-member-expression-to-functions@7.28.5': + resolution: {integrity: sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-optimise-call-expression@7.27.1': + resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + + '@babel/helper-remap-async-to-generator@7.27.1': + resolution: {integrity: sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-replace-supers@7.28.6': + resolution: {integrity: sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-wrap-function@7.28.6': + resolution: {integrity: sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.2': + resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.3': + resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5': + resolution: {integrity: sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1': + resolution: {integrity: sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1': + resolution: {integrity: sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-safari-rest-destructuring-rhs-array@7.29.3': + resolution: {integrity: sha512-SRS46DFR4HqzUzCVgi90/xMoL+zeBDBvWdKYXSEzh79kXswNFEglUpMKxR04//dPqwYXWUBJ3mpUd933ru9Kmg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1': + resolution: {integrity: sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.13.0 + + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.6': + resolution: {integrity: sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2': + resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-async-generators@7.8.4': + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-bigint@7.8.3': + resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-properties@7.12.13': + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-static-block@7.14.5': + resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-assertions@7.28.6': + resolution: {integrity: sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-attributes@7.28.6': + resolution: {integrity: sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-meta@7.10.4': + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-json-strings@7.8.3': + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-jsx@7.28.6': + resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4': + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3': + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-numeric-separator@7.10.4': + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-object-rest-spread@7.8.3': + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3': + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-chaining@7.8.3': + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-private-property-in-object@7.14.5': + resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-top-level-await@7.14.5': + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.28.6': + resolution: {integrity: sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-unicode-sets-regex@7.18.6': + resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-arrow-functions@7.27.1': + resolution: {integrity: sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-async-generator-functions@7.29.0': + resolution: {integrity: sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-async-to-generator@7.28.6': + resolution: {integrity: sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-block-scoped-functions@7.27.1': + resolution: {integrity: sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-block-scoping@7.28.6': + resolution: {integrity: sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-class-properties@7.28.6': + resolution: {integrity: sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-class-static-block@7.28.6': + resolution: {integrity: sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.12.0 + + '@babel/plugin-transform-classes@7.28.6': + resolution: {integrity: sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-computed-properties@7.28.6': + resolution: {integrity: sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-destructuring@7.28.5': + resolution: {integrity: sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-dotall-regex@7.28.6': + resolution: {integrity: sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-duplicate-keys@7.27.1': + resolution: {integrity: sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.29.0': + resolution: {integrity: sha512-zBPcW2lFGxdiD8PUnPwJjag2J9otbcLQzvbiOzDxpYXyCuYX9agOwMPGn1prVH0a4qzhCKu24rlH4c1f7yA8rw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-dynamic-import@7.27.1': + resolution: {integrity: sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-explicit-resource-management@7.28.6': + resolution: {integrity: sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-exponentiation-operator@7.28.6': + resolution: {integrity: sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-export-namespace-from@7.27.1': + resolution: {integrity: sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-for-of@7.27.1': + resolution: {integrity: sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-function-name@7.27.1': + resolution: {integrity: sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-json-strings@7.28.6': + resolution: {integrity: sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-literals@7.27.1': + resolution: {integrity: sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-logical-assignment-operators@7.28.6': + resolution: {integrity: sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-member-expression-literals@7.27.1': + resolution: {integrity: sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-amd@7.27.1': + resolution: {integrity: sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-commonjs@7.28.6': + resolution: {integrity: sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-systemjs@7.29.4': + resolution: {integrity: sha512-N7QmZ0xRZfjHOfZeQLJjwgX2zS9pdGHSVl/cjSGlo4dXMqvurfxXDMKY4RqEKzPozV78VMcd0lxyG13mlbKc4w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-umd@7.27.1': + resolution: {integrity: sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-named-capturing-groups-regex@7.29.0': + resolution: {integrity: sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-new-target@7.27.1': + resolution: {integrity: sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-nullish-coalescing-operator@7.28.6': + resolution: {integrity: sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-numeric-separator@7.28.6': + resolution: {integrity: sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-object-rest-spread@7.28.6': + resolution: {integrity: sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-object-super@7.27.1': + resolution: {integrity: sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-optional-catch-binding@7.28.6': + resolution: {integrity: sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-optional-chaining@7.28.6': + resolution: {integrity: sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-parameters@7.27.7': + resolution: {integrity: sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-private-methods@7.28.6': + resolution: {integrity: sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-private-property-in-object@7.28.6': + resolution: {integrity: sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-property-literals@7.27.1': + resolution: {integrity: sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-display-name@7.28.0': + resolution: {integrity: sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-development@7.27.1': + resolution: {integrity: sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx@7.28.6': + resolution: {integrity: sha512-61bxqhiRfAACulXSLd/GxqmAedUSrRZIu/cbaT18T1CetkTmtDN15it7i80ru4DVqRK1WMxQhXs+Lf9kajm5Ow==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-pure-annotations@7.27.1': + resolution: {integrity: sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-regenerator@7.29.0': + resolution: {integrity: sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-regexp-modifiers@7.28.6': + resolution: {integrity: sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-reserved-words@7.27.1': + resolution: {integrity: sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-runtime@7.29.0': + resolution: {integrity: sha512-jlaRT5dJtMaMCV6fAuLbsQMSwz/QkvaHOHOSXRitGGwSpR1blCY4KUKoyP2tYO8vJcqYe8cEj96cqSztv3uF9w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-shorthand-properties@7.27.1': + resolution: {integrity: sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-spread@7.28.6': + resolution: {integrity: sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-sticky-regex@7.27.1': + resolution: {integrity: sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-template-literals@7.27.1': + resolution: {integrity: sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typeof-symbol@7.27.1': + resolution: {integrity: sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typescript@7.28.6': + resolution: {integrity: sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-escapes@7.27.1': + resolution: {integrity: sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-property-regex@7.28.6': + resolution: {integrity: sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-regex@7.27.1': + resolution: {integrity: sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-sets-regex@7.28.6': + resolution: {integrity: sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/preset-env@7.29.5': + resolution: {integrity: sha512-/69t2aEzGKHD76DyLbHysF/QH2LJOB8iFnYO37unDTKBTubzcMRv0f3H5EiN1Q6ajOd/eB7dAInF0qdFVS06kA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/preset-modules@0.1.6-no-external-plugins': + resolution: {integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==} + peerDependencies: + '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 + + '@babel/preset-react@7.28.5': + resolution: {integrity: sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/preset-typescript@7.28.5': + resolution: {integrity: sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.29.2': + resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@0.2.3': + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + + '@cacheable/memory@2.0.9': + resolution: {integrity: sha512-HdMx6DoGywB30vacDbBsITbIX4pgFqj1zsrV58jZBUw3klzkNoXhj7qOqAgledhxG7YZI5rBSJg7Zp8/VG0DuA==} + + '@cacheable/utils@2.4.1': + resolution: {integrity: sha512-eiFgzCbIneyMlLOmNG4g9xzF7Hv3Mga4LjxjcSC/ues6VYq2+gUbQI8JqNuw/ZM8tJIeIaBGpswAsqV2V7ApgA==} + + '@csstools/cascade-layer-name-parser@1.0.13': + resolution: {integrity: sha512-MX0yLTwtZzr82sQ0zOjqimpZbzjMaK/h2pmlrLK7DCzlmiZLYFpoO94WmN1akRVo6ll/TdpHb53vihHLUMyvng==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + '@csstools/css-parser-algorithms': ^2.7.1 + '@csstools/css-tokenizer': ^2.4.1 + + '@csstools/color-helpers@4.2.1': + resolution: {integrity: sha512-CEypeeykO9AN7JWkr1OEOQb0HRzZlPWGwV0Ya6DuVgFdDi6g3ma/cPZ5ZPZM4AWQikDpq/0llnGGlIL+j8afzw==} + engines: {node: ^14 || ^16 || >=18} + + '@csstools/css-calc@1.2.4': + resolution: {integrity: sha512-tfOuvUQeo7Hz+FcuOd3LfXVp+342pnWUJ7D2y8NUpu1Ww6xnTbHLpz018/y6rtbHifJ3iIEf9ttxXd8KG7nL0Q==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + '@csstools/css-parser-algorithms': ^2.7.1 + '@csstools/css-tokenizer': ^2.4.1 + + '@csstools/css-color-parser@2.0.5': + resolution: {integrity: sha512-lRZSmtl+DSjok3u9hTWpmkxFZnz7stkbZxzKc08aDUsdrWwhSgWo8yq9rq9DaFUtbAyAq2xnH92fj01S+pwIww==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + '@csstools/css-parser-algorithms': ^2.7.1 + '@csstools/css-tokenizer': ^2.4.1 + + '@csstools/css-parser-algorithms@2.7.1': + resolution: {integrity: sha512-2SJS42gxmACHgikc1WGesXLIT8d/q2l0UFM7TaEeIzdFCE/FPMtTiizcPGGJtlPo2xuQzY09OhrLTzRxqJqwGw==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + '@csstools/css-tokenizer': ^2.4.1 + + '@csstools/css-parser-algorithms@3.0.5': + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-syntax-patches-for-csstree@1.1.4': + resolution: {integrity: sha512-wgsqt92b7C7tQhIdPNxj0n9zuUbQlvAuI1exyzeNrOKOi62SD7ren8zqszmpVREjAOqg8cD2FqYhQfAuKjk4sw==} + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true + + '@csstools/css-tokenizer@2.4.1': + resolution: {integrity: sha512-eQ9DIktFJBhGjioABJRtUucoWR2mwllurfnM8LuNGAqX3ViZXaUchqk+1s7jjtkFiT9ySdACsFEA3etErkALUg==} + engines: {node: ^14 || ^16 || >=18} + + '@csstools/css-tokenizer@3.0.4': + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} + engines: {node: '>=18'} + + '@csstools/media-query-list-parser@2.1.13': + resolution: {integrity: sha512-XaHr+16KRU9Gf8XLi3q8kDlI18d5vzKSKCY510Vrtc9iNR0NJzbY9hhTmwhzYZj/ZwGL4VmB3TA9hJW0Um2qFA==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + '@csstools/css-parser-algorithms': ^2.7.1 + '@csstools/css-tokenizer': ^2.4.1 + + '@csstools/media-query-list-parser@4.0.3': + resolution: {integrity: sha512-HAYH7d3TLRHDOUQK4mZKf9k9Ph/m8Akstg66ywKR4SFAigjs3yBiUeZtFxywiTm5moZMAp/5W/ZuFnNXXYLuuQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/postcss-cascade-layers@4.0.6': + resolution: {integrity: sha512-Xt00qGAQyqAODFiFEJNkTpSUz5VfYqnDLECdlA/Vv17nl/OIV5QfTRHGAXrBGG5YcJyHpJ+GF9gF/RZvOQz4oA==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-color-function@3.0.19': + resolution: {integrity: sha512-d1OHEXyYGe21G3q88LezWWx31ImEDdmINNDy0LyLNN9ChgN2bPxoubUPiHf9KmwypBMaHmNcMuA/WZOKdZk/Lg==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-color-mix-function@2.0.19': + resolution: {integrity: sha512-mLvQlMX+keRYr16AuvuV8WYKUwF+D0DiCqlBdvhQ0KYEtcQl9/is9Ssg7RcIys8x0jIn2h1zstS4izckdZj9wg==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-content-alt-text@1.0.0': + resolution: {integrity: sha512-SkHdj7EMM/57GVvSxSELpUg7zb5eAndBeuvGwFzYtU06/QXJ/h9fuK7wO5suteJzGhm3GDF/EWPCdWV2h1IGHQ==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-exponential-functions@1.0.9': + resolution: {integrity: sha512-x1Avr15mMeuX7Z5RJUl7DmjhUtg+Amn5DZRD0fQ2TlTFTcJS8U1oxXQ9e5mA62S2RJgUU6db20CRoJyDvae2EQ==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-font-format-keywords@3.0.2': + resolution: {integrity: sha512-E0xz2sjm4AMCkXLCFvI/lyl4XO6aN1NCSMMVEOngFDJ+k2rDwfr6NDjWljk1li42jiLNChVX+YFnmfGCigZKXw==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-gamut-mapping@1.0.11': + resolution: {integrity: sha512-KrHGsUPXRYxboXmJ9wiU/RzDM7y/5uIefLWKFSc36Pok7fxiPyvkSHO51kh+RLZS1W5hbqw9qaa6+tKpTSxa5g==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-gradients-interpolation-method@4.0.20': + resolution: {integrity: sha512-ZFl2JBHano6R20KB5ZrB8KdPM2pVK0u+/3cGQ2T8VubJq982I2LSOvQ4/VtxkAXjkPkk1rXt4AD1ni7UjTZ1Og==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-hwb-function@3.0.18': + resolution: {integrity: sha512-3ifnLltR5C7zrJ+g18caxkvSRnu9jBBXCYgnBznRjxm6gQJGnnCO9H6toHfywNdNr/qkiVf2dymERPQLDnjLRQ==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-ic-unit@3.0.7': + resolution: {integrity: sha512-YoaNHH2wNZD+c+rHV02l4xQuDpfR8MaL7hD45iJyr+USwvr0LOheeytJ6rq8FN6hXBmEeoJBeXXgGmM8fkhH4g==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-initial@1.0.1': + resolution: {integrity: sha512-wtb+IbUIrIf8CrN6MLQuFR7nlU5C7PwuebfeEXfjthUha1+XZj2RVi+5k/lukToA24sZkYAiSJfHM8uG/UZIdg==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-is-pseudo-class@4.0.8': + resolution: {integrity: sha512-0aj591yGlq5Qac+plaWCbn5cpjs5Sh0daovYUKJUOMjIp70prGH/XPLp7QjxtbFXz3CTvb0H9a35dpEuIuUi3Q==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-light-dark-function@1.0.8': + resolution: {integrity: sha512-x0UtpCyVnERsplUeoaY6nEtp1HxTf4lJjoK/ULEm40DraqFfUdUSt76yoOyX5rGY6eeOUOkurHyYlFHVKv/pew==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-logical-float-and-clear@2.0.1': + resolution: {integrity: sha512-SsrWUNaXKr+e/Uo4R/uIsqJYt3DaggIh/jyZdhy/q8fECoJSKsSMr7nObSLdvoULB69Zb6Bs+sefEIoMG/YfOA==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-logical-overflow@1.0.1': + resolution: {integrity: sha512-Kl4lAbMg0iyztEzDhZuQw8Sj9r2uqFDcU1IPl+AAt2nue8K/f1i7ElvKtXkjhIAmKiy5h2EY8Gt/Cqg0pYFDCw==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-logical-overscroll-behavior@1.0.1': + resolution: {integrity: sha512-+kHamNxAnX8ojPCtV8WPcUP3XcqMFBSDuBuvT6MHgq7oX4IQxLIXKx64t7g9LiuJzE7vd06Q9qUYR6bh4YnGpQ==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-logical-resize@2.0.1': + resolution: {integrity: sha512-W5Gtwz7oIuFcKa5SmBjQ2uxr8ZoL7M2bkoIf0T1WeNqljMkBrfw1DDA8/J83k57NQ1kcweJEjkJ04pUkmyee3A==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-logical-viewport-units@2.0.11': + resolution: {integrity: sha512-ElITMOGcjQtvouxjd90WmJRIw1J7KMP+M+O87HaVtlgOOlDt1uEPeTeii8qKGe2AiedEp0XOGIo9lidbiU2Ogg==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-media-minmax@1.1.8': + resolution: {integrity: sha512-KYQCal2i7XPNtHAUxCECdrC7tuxIWQCW+s8eMYs5r5PaAiVTeKwlrkRS096PFgojdNCmHeG0Cb7njtuNswNf+w==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-media-queries-aspect-ratio-number-values@2.0.11': + resolution: {integrity: sha512-YD6jrib20GRGQcnOu49VJjoAnQ/4249liuz7vTpy/JfgqQ1Dlc5eD4HPUMNLOw9CWey9E6Etxwf/xc/ZF8fECA==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-nested-calc@3.0.2': + resolution: {integrity: sha512-ySUmPyawiHSmBW/VI44+IObcKH0v88LqFe0d09Sb3w4B1qjkaROc6d5IA3ll9kjD46IIX/dbO5bwFN/swyoyZA==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-normalize-display-values@3.0.2': + resolution: {integrity: sha512-fCapyyT/dUdyPtrelQSIV+d5HqtTgnNP/BEG9IuhgXHt93Wc4CfC1bQ55GzKAjWrZbgakMQ7MLfCXEf3rlZJOw==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-oklab-function@3.0.19': + resolution: {integrity: sha512-e3JxXmxjU3jpU7TzZrsNqSX4OHByRC3XjItV3Ieo/JEQmLg5rdOL4lkv/1vp27gXemzfNt44F42k/pn0FpE21Q==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-progressive-custom-properties@3.3.0': + resolution: {integrity: sha512-W2oV01phnILaRGYPmGFlL2MT/OgYjQDrL9sFlbdikMFi6oQkFki9B86XqEWR7HCsTZFVq7dbzr/o71B75TKkGg==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-relative-color-syntax@2.0.19': + resolution: {integrity: sha512-MxUMSNvio1WwuS6WRLlQuv6nNPXwIWUFzBBAvL/tBdWfiKjiJnAa6eSSN5gtaacSqUkQ/Ce5Z1OzLRfeaWhADA==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-scope-pseudo-class@3.0.1': + resolution: {integrity: sha512-3ZFonK2gfgqg29gUJ2w7xVw2wFJ1eNWVDONjbzGkm73gJHVCYK5fnCqlLr+N+KbEfv2XbWAO0AaOJCFB6Fer6A==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-stepped-value-functions@3.0.10': + resolution: {integrity: sha512-MZwo0D0TYrQhT5FQzMqfy/nGZ28D1iFtpN7Su1ck5BPHS95+/Y5O9S4kEvo76f2YOsqwYcT8ZGehSI1TnzuX2g==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-text-decoration-shorthand@3.0.7': + resolution: {integrity: sha512-+cptcsM5r45jntU6VjotnkC9GteFR7BQBfZ5oW7inLCxj7AfLGAzMbZ60hKTP13AULVZBdxky0P8um0IBfLHVA==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-trigonometric-functions@3.0.10': + resolution: {integrity: sha512-G9G8moTc2wiad61nY5HfvxLiM/myX0aYK4s1x8MQlPH29WDPxHQM7ghGgvv2qf2xH+rrXhztOmjGHJj4jsEqXw==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-unset-value@3.0.1': + resolution: {integrity: sha512-dbDnZ2ja2U8mbPP0Hvmt2RMEGBiF1H7oY6HYSpjteXJGihYwgxgTr6KRbbJ/V6c+4wd51M+9980qG4gKVn5ttg==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.4 + + '@csstools/selector-resolve-nested@1.1.0': + resolution: {integrity: sha512-uWvSaeRcHyeNenKg8tp17EVDRkpflmdyvbE0DHo6D/GdBb6PDnCYYU6gRpXhtICMGMcahQmj2zGxwFM/WC8hCg==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss-selector-parser: ^6.0.13 + + '@csstools/selector-specificity@3.1.1': + resolution: {integrity: sha512-a7cxGcJ2wIlMFLlh8z2ONm+715QkPHiyJcxwQlKOz/03GPw1COpfhcmC9wm4xlZfp//jWHNNMwzjtqHXVWU9KA==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss-selector-parser: ^6.0.13 + + '@csstools/selector-specificity@5.0.0': + resolution: {integrity: sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==} + engines: {node: '>=18'} + peerDependencies: + postcss-selector-parser: ^7.0.0 + + '@csstools/utilities@1.0.0': + resolution: {integrity: sha512-tAgvZQe/t2mlvpNosA4+CkMiZ2azISW5WPAcdSalZlEjQvUfghHxfQcrCiK/7/CrfAWVxyM88kGFYO82heIGDg==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.4 + + '@discoveryjs/json-ext@0.5.7': + resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} + engines: {node: '>=10.0.0'} + + '@dual-bundle/import-meta-resolve@4.2.1': + resolution: {integrity: sha512-id+7YRUgoUX6CgV0DtuhirQWodeeA7Lf4i2x71JS/vtA5pRb/hIGWlw+G6MeXvsM+MXrz0VAydTGElX1rAfgPg==} + + '@emotion/babel-plugin@11.13.5': + resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==} + + '@emotion/cache@11.14.0': + resolution: {integrity: sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==} + + '@emotion/hash@0.9.2': + resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==} + + '@emotion/is-prop-valid@1.4.0': + resolution: {integrity: sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==} + + '@emotion/memoize@0.9.0': + resolution: {integrity: sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==} + + '@emotion/react@11.14.0': + resolution: {integrity: sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==} + peerDependencies: + '@types/react': '*' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true + + '@emotion/serialize@1.3.3': + resolution: {integrity: sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==} + + '@emotion/sheet@1.4.0': + resolution: {integrity: sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==} + + '@emotion/styled@11.14.1': + resolution: {integrity: sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==} + peerDependencies: + '@emotion/react': ^11.0.0-rc.0 + '@types/react': '*' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true + + '@emotion/unitless@0.10.0': + resolution: {integrity: sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==} + + '@emotion/use-insertion-effect-with-fallbacks@1.2.0': + resolution: {integrity: sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==} + peerDependencies: + react: '>=16.8.0' + + '@emotion/utils@1.4.2': + resolution: {integrity: sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==} + + '@emotion/weak-memoize@0.4.0': + resolution: {integrity: sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==} + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.2': + resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.5': + resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.39.4': + resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@istanbuljs/load-nyc-config@1.1.0': + resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} + engines: {node: '>=8'} + + '@istanbuljs/schema@0.1.6': + resolution: {integrity: sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==} + engines: {node: '>=8'} + + '@jest/console@29.7.0': + resolution: {integrity: sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/core@29.7.0': + resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + '@jest/environment@29.7.0': + resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/expect-utils@29.7.0': + resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/expect@29.7.0': + resolution: {integrity: sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/fake-timers@29.7.0': + resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/globals@29.7.0': + resolution: {integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/reporters@29.7.0': + resolution: {integrity: sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + '@jest/schemas@29.6.3': + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/source-map@29.6.3': + resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/test-result@29.7.0': + resolution: {integrity: sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/test-sequencer@29.7.0': + resolution: {integrity: sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/transform@29.7.0': + resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/types@29.6.3': + resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/source-map@0.3.11': + resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@keyv/bigmap@1.3.1': + resolution: {integrity: sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==} + engines: {node: '>= 18'} + peerDependencies: + keyv: ^5.6.0 + + '@keyv/serialize@1.1.1': + resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} + + '@kurkle/color@0.3.4': + resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==} + + '@leichtgewicht/ip-codec@2.0.5': + resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==} + + '@mui/core-downloads-tracker@5.18.0': + resolution: {integrity: sha512-jbhwoQ1AY200PSSOrNXmrFCaSDSJWP7qk6urkTmIirvRXDROkqe+QwcLlUiw/PrREwsIF/vm3/dAXvjlMHF0RA==} + + '@mui/material@5.18.0': + resolution: {integrity: sha512-bbH/HaJZpFtXGvWg3TsBWG4eyt3gah3E7nCNU8GLyRjVoWcA91Vm/T+sjHfUcwgJSw9iLtucfHBoq+qW/T30aA==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@emotion/react': ^11.5.0 + '@emotion/styled': ^11.3.0 + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + '@types/react': + optional: true + + '@mui/private-theming@5.17.1': + resolution: {integrity: sha512-XMxU0NTYcKqdsG8LRmSoxERPXwMbp16sIXPcLVgLGII/bVNagX0xaheWAwFv8+zDK7tI3ajllkuD3GZZE++ICQ==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@mui/styled-engine@5.18.0': + resolution: {integrity: sha512-BN/vKV/O6uaQh2z5rXV+MBlVrEkwoS/TK75rFQ2mjxA7+NBo8qtTAOA4UaM0XeJfn7kh2wZ+xQw2HAx0u+TiBg==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@emotion/react': ^11.4.1 + '@emotion/styled': ^11.3.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + + '@mui/system@5.18.0': + resolution: {integrity: sha512-ojZGVcRWqWhu557cdO3pWHloIGJdzVtxs3rk0F9L+x55LsUjcMUVkEhiF7E4TMxZoF9MmIHGGs0ZX3FDLAf0Xw==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@emotion/react': ^11.5.0 + '@emotion/styled': ^11.3.0 + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + '@types/react': + optional: true + + '@mui/types@7.2.24': + resolution: {integrity: sha512-3c8tRt/CbWZ+pEg7QpSwbdxOk36EfmhbKf6AGZsD1EcLDLTSZoxxJ86FVtcjxvjuhdyBiWKSTGZFaXCnidO2kw==} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@mui/utils@5.17.1': + resolution: {integrity: sha512-jEZ8FTqInt2WzxDV8bhImWBqeQRD99c/id/fq83H0ER9tFl+sfZlaAoCdznGvbSQQ9ividMxqSV2c7cC1vBcQg==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@parcel/watcher-android-arm64@2.5.6': + resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.5.6': + resolution: {integrity: sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.5.6': + resolution: {integrity: sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.5.6': + resolution: {integrity: sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.5.6': + resolution: {integrity: sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-arm-musl@2.5.6': + resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + libc: [musl] + + '@parcel/watcher-linux-arm64-glibc@2.5.6': + resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-arm64-musl@2.5.6': + resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@parcel/watcher-linux-x64-glibc@2.5.6': + resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-x64-musl@2.5.6': + resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@parcel/watcher-win32-arm64@2.5.6': + resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.6': + resolution: {integrity: sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.5.6': + resolution: {integrity: sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.5.6': + resolution: {integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==} + engines: {node: '>= 10.0.0'} + + '@pkgr/core@0.2.9': + resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + + '@popperjs/core@2.11.8': + resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + + '@react-dnd/asap@5.0.2': + resolution: {integrity: sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A==} + + '@react-dnd/invariant@4.0.2': + resolution: {integrity: sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw==} + + '@react-dnd/shallowequal@4.0.2': + resolution: {integrity: sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==} + + '@reduxjs/toolkit@2.12.0': + resolution: {integrity: sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==} + peerDependencies: + react: ^16.9.0 || ^17.0.0 || ^18 || ^19 + react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 + peerDependenciesMeta: + react: + optional: true + react-redux: + optional: true + + '@remix-run/router@1.23.2': + resolution: {integrity: sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==} + engines: {node: '>=14.0.0'} + + '@sinclair/typebox@0.27.10': + resolution: {integrity: sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==} + + '@sinonjs/commons@3.0.1': + resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} + + '@sinonjs/fake-timers@10.3.0': + resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@standard-schema/utils@0.3.0': + resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} + + '@tanstack/query-core@5.100.11': + resolution: {integrity: sha512-lmE0994apShXPj8CUxgx4ch5yUJhE9k/+tVwihBvPOyerACWdBocfFg24t8+0RhtlTd7tEgchDkhlCxNssvDxw==} + + '@tanstack/react-query@5.100.11': + resolution: {integrity: sha512-J0f9s5x3LE1450nNNfYx+e/n0DMa0uOBdFJUy5r0RvmsXd4nB/n0rbHtHI1vYXhikNFan+wf51p6Tmp4c8ucrg==} + peerDependencies: + react: ^18 || ^19 + + '@tanstack/react-table@8.21.3': + resolution: {integrity: sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==} + engines: {node: '>=12'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + + '@tanstack/table-core@8.21.3': + resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} + engines: {node: '>=12'} + + '@testing-library/dom@9.3.4': + resolution: {integrity: sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==} + engines: {node: '>=14'} + + '@testing-library/jest-dom@6.9.1': + resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + + '@testing-library/react@14.3.1': + resolution: {integrity: sha512-H99XjUhWQw0lTgyMN05W3xQG1Nh4lq574D8keFf1dDoNTJgp66VbJozRaczoF+wsiaPJNt/TcnfpLGufGxSrZQ==} + engines: {node: '>=14'} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + + '@tootallnate/once@2.0.1': + resolution: {integrity: sha512-HqmEUIGRJ5fSXchkVgR5F7qn48bDBzv0kWj/Kfu5e6uci4UlEeng4331LnBkWffb++Ei3FOVLxo8JJWMFBDMeQ==} + engines: {node: '>= 10'} + + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/body-parser@1.19.6': + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + + '@types/bonjour@3.5.13': + resolution: {integrity: sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==} + + '@types/connect-history-api-fallback@1.5.4': + resolution: {integrity: sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/d3-array@3.2.2': + resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-shape@3.1.8': + resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + + '@types/eslint-scope@3.7.7': + resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} + + '@types/eslint@9.6.1': + resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/express-serve-static-core@4.19.8': + resolution: {integrity: sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==} + + '@types/express-serve-static-core@5.1.1': + resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==} + + '@types/express@4.17.25': + resolution: {integrity: sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==} + + '@types/glob@7.2.0': + resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} + + '@types/graceful-fs@4.1.9': + resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} + + '@types/hoist-non-react-statics@3.3.7': + resolution: {integrity: sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==} + peerDependencies: + '@types/react': '*' + + '@types/html-minifier-terser@6.1.0': + resolution: {integrity: sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==} + + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + + '@types/http-proxy@1.17.17': + resolution: {integrity: sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==} + + '@types/istanbul-lib-coverage@2.0.6': + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + + '@types/istanbul-lib-report@3.0.3': + resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} + + '@types/istanbul-reports@3.0.4': + resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + + '@types/jsdom@20.0.1': + resolution: {integrity: sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/mime@1.3.5': + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + + '@types/minimatch@6.0.0': + resolution: {integrity: sha512-zmPitbQ8+6zNutpwgcQuLcsEpn/Cj54Kbn7L5pX0Os5kdWplB7xPgEh/g+SWOB/qmows2gpuCaPyduq8ZZRnxA==} + deprecated: This is a stub types definition. minimatch provides its own type definitions, so you do not need this installed. + + '@types/node-forge@1.3.14': + resolution: {integrity: sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==} + + '@types/node@20.19.41': + resolution: {integrity: sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==} + + '@types/parse-json@4.0.2': + resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} + + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + + '@types/qs@6.15.1': + resolution: {integrity: sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + + '@types/react-dom@18.3.7': + resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} + peerDependencies: + '@types/react': ^18.0.0 + + '@types/react-transition-group@4.4.12': + resolution: {integrity: sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==} + peerDependencies: + '@types/react': '*' + + '@types/react@18.3.28': + resolution: {integrity: sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==} + + '@types/retry@0.12.0': + resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} + + '@types/send@0.17.6': + resolution: {integrity: sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==} + + '@types/send@1.2.1': + resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} + + '@types/serve-index@1.9.4': + resolution: {integrity: sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==} + + '@types/serve-static@1.15.10': + resolution: {integrity: sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==} + + '@types/sockjs@0.3.36': + resolution: {integrity: sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==} + + '@types/stack-utils@2.0.3': + resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + + '@types/tough-cookie@4.0.5': + resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + + '@types/use-sync-external-store@0.0.6': + resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + '@types/yargs-parser@21.0.3': + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + + '@types/yargs@17.0.35': + resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} + + '@typescript-eslint/eslint-plugin@7.18.0': + resolution: {integrity: sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + '@typescript-eslint/parser': ^7.0.0 + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/parser@7.18.0': + resolution: {integrity: sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/scope-manager@7.18.0': + resolution: {integrity: sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==} + engines: {node: ^18.18.0 || >=20.0.0} + + '@typescript-eslint/type-utils@7.18.0': + resolution: {integrity: sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/types@7.18.0': + resolution: {integrity: sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==} + engines: {node: ^18.18.0 || >=20.0.0} + + '@typescript-eslint/typescript-estree@7.18.0': + resolution: {integrity: sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/utils@7.18.0': + resolution: {integrity: sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + eslint: ^8.56.0 + + '@typescript-eslint/visitor-keys@7.18.0': + resolution: {integrity: sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==} + engines: {node: ^18.18.0 || >=20.0.0} + + '@webassemblyjs/ast@1.14.1': + resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} + + '@webassemblyjs/floating-point-hex-parser@1.13.2': + resolution: {integrity: sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==} + + '@webassemblyjs/helper-api-error@1.13.2': + resolution: {integrity: sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==} + + '@webassemblyjs/helper-buffer@1.14.1': + resolution: {integrity: sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==} + + '@webassemblyjs/helper-numbers@1.13.2': + resolution: {integrity: sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==} + + '@webassemblyjs/helper-wasm-bytecode@1.13.2': + resolution: {integrity: sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==} + + '@webassemblyjs/helper-wasm-section@1.14.1': + resolution: {integrity: sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==} + + '@webassemblyjs/ieee754@1.13.2': + resolution: {integrity: sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==} + + '@webassemblyjs/leb128@1.13.2': + resolution: {integrity: sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==} + + '@webassemblyjs/utf8@1.13.2': + resolution: {integrity: sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==} + + '@webassemblyjs/wasm-edit@1.14.1': + resolution: {integrity: sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==} + + '@webassemblyjs/wasm-gen@1.14.1': + resolution: {integrity: sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==} + + '@webassemblyjs/wasm-opt@1.14.1': + resolution: {integrity: sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==} + + '@webassemblyjs/wasm-parser@1.14.1': + resolution: {integrity: sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==} + + '@webassemblyjs/wast-printer@1.14.1': + resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} + + '@webpack-cli/configtest@2.1.1': + resolution: {integrity: sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==} + engines: {node: '>=14.15.0'} + peerDependencies: + webpack: 5.x.x + webpack-cli: 5.x.x + + '@webpack-cli/info@2.0.2': + resolution: {integrity: sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==} + engines: {node: '>=14.15.0'} + peerDependencies: + webpack: 5.x.x + webpack-cli: 5.x.x + + '@webpack-cli/serve@2.0.5': + resolution: {integrity: sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==} + engines: {node: '>=14.15.0'} + peerDependencies: + webpack: 5.x.x + webpack-cli: 5.x.x + webpack-dev-server: '*' + peerDependenciesMeta: + webpack-dev-server: + optional: true + + '@xtuc/ieee754@1.2.0': + resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} + + '@xtuc/long@4.2.2': + resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + + abab@2.0.6: + resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} + deprecated: Use your platform's native atob() and btoa() methods instead + + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + + acorn-globals@7.0.1: + resolution: {integrity: sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==} + + acorn-import-phases@1.0.4: + resolution: {integrity: sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==} + engines: {node: '>=10.13.0'} + peerDependencies: + acorn: ^8.14.0 + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn-walk@8.3.5: + resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==} + engines: {node: '>=0.4.0'} + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + + ajv-formats@2.1.1: + resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv-keywords@5.1.0: + resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} + peerDependencies: + ajv: ^8.8.2 + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + + ansi-escapes@7.3.0: + resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==} + engines: {node: '>=18'} + + ansi-html-community@0.0.8: + resolution: {integrity: sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==} + engines: {'0': node >= 0.8.0} + hasBin: true + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-query@5.1.3: + resolution: {integrity: sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + + array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + + array-union@1.0.2: + resolution: {integrity: sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==} + engines: {node: '>=0.10.0'} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + array-uniq@1.0.3: + resolution: {integrity: sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==} + engines: {node: '>=0.10.0'} + + astral-regex@2.0.0: + resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} + engines: {node: '>=8'} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + attr-accept@2.2.5: + resolution: {integrity: sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==} + engines: {node: '>=4'} + + autoprefixer@10.5.0: + resolution: {integrity: sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + axios@1.16.1: + resolution: {integrity: sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==} + + babel-jest@29.7.0: + resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.8.0 + + babel-plugin-istanbul@6.1.1: + resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} + engines: {node: '>=8'} + + babel-plugin-jest-hoist@29.6.3: + resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + babel-plugin-macros@3.1.0: + resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} + engines: {node: '>=10', npm: '>=6'} + + babel-plugin-polyfill-corejs2@0.4.17: + resolution: {integrity: sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-polyfill-corejs3@0.13.0: + resolution: {integrity: sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-polyfill-corejs3@0.14.2: + resolution: {integrity: sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-polyfill-regenerator@0.6.8: + resolution: {integrity: sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-preset-current-node-syntax@1.2.0: + resolution: {integrity: sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==} + peerDependencies: + '@babel/core': ^7.0.0 || ^8.0.0-0 + + babel-preset-jest@29.6.3: + resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.0.0 + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@2.0.0: + resolution: {integrity: sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + baseline-browser-mapping@2.10.31: + resolution: {integrity: sha512-MujYO3eP72uvmSE0i4wltsodRfIpZATP3jvzRNRGGxgzId7aVocVJJV3nf01qnzzKFGxQVC9bpWxl5cjxTr/7Q==} + engines: {node: '>=6.0.0'} + hasBin: true + + batch@0.6.1: + resolution: {integrity: sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + body-parser@1.20.5: + resolution: {integrity: sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + bonjour-service@1.3.0: + resolution: {integrity: sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + brace-expansion@1.1.14: + resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==} + + brace-expansion@2.1.0: + resolution: {integrity: sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==} + + brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} + engines: {node: 18 || 20 || >=22} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + bser@2.1.1: + resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + cacheable@2.3.5: + resolution: {integrity: sha512-EQfaKe09tl615iNvq/TBRWTFf1AKJNXYQSsMx0Z3EI0nA+pVsVPS8wJhnRlkbdacKPh1d0qVIhwTc2zsQNFEEg==} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.9: + resolution: {integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camel-case@4.1.2: + resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==} + + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + + caniuse-api@3.0.0: + resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==} + + caniuse-lite@1.0.30001793: + resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + char-regex@1.0.2: + resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} + engines: {node: '>=10'} + + chart.js@4.5.1: + resolution: {integrity: sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==} + engines: {pnpm: '>=8'} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + chrome-trace-event@1.0.4: + resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} + engines: {node: '>=6.0'} + + ci-info@3.9.0: + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} + engines: {node: '>=8'} + + cjs-module-lexer@1.4.3: + resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + + classnames@2.5.1: + resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} + + clean-css@5.3.3: + resolution: {integrity: sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==} + engines: {node: '>= 10.0'} + + clean-webpack-plugin@4.0.0: + resolution: {integrity: sha512-WuWE1nyTNAyW5T7oNyys2EN0cfP2fdRxhxnIQWiAp0bMabPdHhoGxM8A6YL2GhqwgrPnnaemVE7nv5XJ2Fhh2w==} + engines: {node: '>=10.0.0'} + peerDependencies: + webpack: '>=4.0.0 <6.0.0' + + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} + + cli-truncate@4.0.0: + resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==} + engines: {node: '>=18'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + clone-deep@4.0.1: + resolution: {integrity: sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==} + engines: {node: '>=6'} + + clsx@1.2.1: + resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==} + engines: {node: '>=6'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + co@4.6.0: + resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} + engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + + collect-v8-coverage@1.0.3: + resolution: {integrity: sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + colord@2.9.3: + resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} + + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + + commander@13.1.0: + resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} + engines: {node: '>=18'} + + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + + commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + + compressible@2.0.18: + resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} + engines: {node: '>= 0.6'} + + compression@1.8.1: + resolution: {integrity: sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==} + engines: {node: '>= 0.8.0'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + connect-history-api-fallback@2.0.0: + resolution: {integrity: sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==} + engines: {node: '>=0.8'} + + content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + convert-source-map@1.9.0: + resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie-signature@1.0.7: + resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + copy-anything@3.0.5: + resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==} + engines: {node: '>=12.13'} + + core-js-compat@3.49.0: + resolution: {integrity: sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + cosmiconfig@7.1.0: + resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} + engines: {node: '>=10'} + + cosmiconfig@9.0.1: + resolution: {integrity: sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + + create-jest@29.7.0: + resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + css-blank-pseudo@6.0.2: + resolution: {integrity: sha512-J/6m+lsqpKPqWHOifAFtKFeGLOzw3jR92rxQcwRUfA/eTuZzKfKlxOmYDx2+tqOPQAueNvBiY8WhAeHu5qNmTg==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.4 + + css-declaration-sorter@7.4.0: + resolution: {integrity: sha512-LTuzjPoyA2vMGKKcaOqKSp7Ub2eGrNfKiZH4LpezxpNrsICGCSFvsQOI29psISxNZtaXibkC2CXzrQ5enMeGGw==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.0.9 + + css-functions-list@3.3.3: + resolution: {integrity: sha512-8HFEBPKhOpJPEPu70wJJetjKta86Gw9+CCyCnB3sui2qQfOvRyqBy4IKLKKAwdMpWb2lHXWk9Wb4Z6AmaUT1Pg==} + engines: {node: '>=12'} + + css-has-pseudo@6.0.5: + resolution: {integrity: sha512-ZTv6RlvJJZKp32jPYnAJVhowDCrRrHUTAxsYSuUPBEDJjzws6neMnzkRblxtgmv1RgcV5dhH2gn7E3wA9Wt6lw==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.4 + + css-loader@6.11.0: + resolution: {integrity: sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==} + engines: {node: '>= 12.13.0'} + peerDependencies: + '@rspack/core': 0.x || 1.x + webpack: ^5.0.0 + peerDependenciesMeta: + '@rspack/core': + optional: true + webpack: + optional: true + + css-prefers-color-scheme@9.0.1: + resolution: {integrity: sha512-iFit06ochwCKPRiWagbTa1OAWCvWWVdEnIFd8BaRrgO8YrrNh4RAWUQTFcYX5tdFZgFl1DJ3iiULchZyEbnF4g==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.4 + + css-select@4.3.0: + resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==} + + css-select@5.2.2: + resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} + + css-tree@2.2.1: + resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + + css-tree@2.3.1: + resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + css-what@6.2.2: + resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} + engines: {node: '>= 6'} + + css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + + cssdb@8.9.0: + resolution: {integrity: sha512-J8jOU/hLjaXcO1LldOLraJSQpfLXRKof0I7mtbRyOy2AAXgqst0x9rlgi2qXeD6d0ou3ZLqcPAMqYVbpCbrxEw==} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + cssnano-preset-default@6.1.2: + resolution: {integrity: sha512-1C0C+eNaeN8OcHQa193aRgYexyJtU8XwbdieEjClw+J9d94E41LwT6ivKH0WT+fYwYWB0Zp3I3IZ7tI/BbUbrg==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + cssnano-utils@4.0.2: + resolution: {integrity: sha512-ZR1jHg+wZ8o4c3zqf1SIUSTIvm/9mU343FMR6Obe/unskbvpGhZOo1J6d/r8D1pzkRQYuwbcH3hToOuoA2G7oQ==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + cssnano@6.1.2: + resolution: {integrity: sha512-rYk5UeX7VAM/u0lNqewCdasdtPK81CgX8wJFLEIXHbV2oldWRgJAsZrdhRXkV1NJzA2g850KiFm9mMU2HxNxMA==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + csso@5.0.5: + resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + + cssom@0.3.8: + resolution: {integrity: sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==} + + cssom@0.5.0: + resolution: {integrity: sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==} + + cssstyle@2.3.0: + resolution: {integrity: sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==} + engines: {node: '>=8'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-format@3.1.2: + resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + data-urls@3.0.2: + resolution: {integrity: sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==} + engines: {node: '>=12'} + + date-fns@3.6.0: + resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==} + + debounce@1.2.1: + resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==} + + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decimal.js-light@2.5.1: + resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + dedent@1.7.2: + resolution: {integrity: sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + + deep-equal@2.2.3: + resolution: {integrity: sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==} + engines: {node: '>= 0.4'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + deepmerge@2.2.1: + resolution: {integrity: sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==} + engines: {node: '>=0.10.0'} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + default-gateway@6.0.3: + resolution: {integrity: sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==} + engines: {node: '>= 10'} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-lazy-prop@2.0.0: + resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} + engines: {node: '>=8'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + del@4.1.1: + resolution: {integrity: sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==} + engines: {node: '>=6'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + depd@1.1.2: + resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} + engines: {node: '>= 0.6'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + detect-newline@3.1.0: + resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} + engines: {node: '>=8'} + + detect-node@2.1.0: + resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} + + diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + dnd-core@16.0.1: + resolution: {integrity: sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==} + + dns-packet@5.6.1: + resolution: {integrity: sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==} + engines: {node: '>=6'} + + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + + dom-converter@0.2.0: + resolution: {integrity: sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==} + + dom-helpers@5.2.1: + resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + + dom-serializer@1.4.1: + resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==} + + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domexception@4.0.0: + resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==} + engines: {node: '>=12'} + deprecated: Use your platform's native DOMException instead + + domhandler@4.3.1: + resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} + engines: {node: '>= 4'} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@2.8.0: + resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + + dot-case@3.0.4: + resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + duplexer@0.1.2: + resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + electron-to-chromium@1.5.359: + resolution: {integrity: sha512-8lPELWuYZIWk7NDvCNthtmMw/7Q5Wu25NpM4djFMHBmk8DubPAtL4YTOp7ou0e7HyJtwkVlWv8XMLURnrtgJQw==} + + emittery@0.13.1: + resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} + engines: {node: '>=12'} + + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + enhanced-resolve@5.21.4: + resolution: {integrity: sha512-wE4fDO8OjJhrPFH69HUQStq5oKvGRTNXEyW+k5C/pUQLASSsTu7obd2V3GvCDgPcY9AWjhJ4jz9Kh7iRvrxhJg==} + engines: {node: '>=10.13.0'} + + entities@2.2.0: + resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + envinfo@7.21.0: + resolution: {integrity: sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==} + engines: {node: '>=4'} + hasBin: true + + environment@1.1.0: + resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} + engines: {node: '>=18'} + + errno@0.1.8: + resolution: {integrity: sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==} + hasBin: true + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-get-iterator@1.1.3: + resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==} + + es-module-lexer@2.1.0: + resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + escodegen@2.1.0: + resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} + engines: {node: '>=6.0'} + hasBin: true + + eslint-config-prettier@9.1.2: + resolution: {integrity: sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-plugin-prettier@5.5.5: + resolution: {integrity: sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + '@types/eslint': '>=8.0.0' + eslint: '>=8.0.0' + eslint-config-prettier: '>= 7.0.0 <10.0.0 || >=10.1.0' + prettier: '>=3.0.0' + peerDependenciesMeta: + '@types/eslint': + optional: true + eslint-config-prettier: + optional: true + + eslint-scope@5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.39.4: + resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + + execa@8.0.1: + resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} + engines: {node: '>=16.17'} + + exit@0.1.2: + resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} + engines: {node: '>= 0.8.0'} + + expect@29.7.0: + resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + express@4.22.2: + resolution: {integrity: sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==} + engines: {node: '>= 0.10.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + + fast-equals@5.4.0: + resolution: {integrity: sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==} + engines: {node: '>=6.0.0'} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-uri@3.1.2: + resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} + + fastest-levenshtein@1.0.16: + resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==} + engines: {node: '>= 4.9.1'} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + faye-websocket@0.11.4: + resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==} + engines: {node: '>=0.8.0'} + + fb-watchman@2.0.2: + resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + + file-entry-cache@11.1.3: + resolution: {integrity: sha512-oMbq0PD6VIiIwMF6LIa7MEwd/l9huKwmqRKXqmrkqIZv8CvRbfowL+L0ryAl8h//HfAS0zS+4SbYoRyAoA6BJA==} + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + file-selector@2.1.2: + resolution: {integrity: sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==} + engines: {node: '>= 12'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + finalhandler@1.3.2: + resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==} + engines: {node: '>= 0.8'} + + find-root@1.1.0: + resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flat-cache@6.1.22: + resolution: {integrity: sha512-N2dnzVJIphnNsjHcrxGW7DePckJ6haPrSFqpsBUhHYgwtKGVq4JrBGielEGD2fCVnsGm1zlBVZ8wGhkyuetgug==} + + flat@5.0.2: + resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} + hasBin: true + + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + + follow-redirects@1.16.0: + resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + + formik@2.4.9: + resolution: {integrity: sha512-5nI94BMnlFDdQRBY4Sz39WkhxajZJ57Fzs8wVbtsQlm5ScKIR1QLYqv/ultBnobObtlUyxpxoLodpixrsf36Og==} + peerDependencies: + react: '>=16.8.0' + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fraction.js@5.3.4: + resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} + + framer-motion@11.18.2: + resolution: {integrity: sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + + fs-monkey@1.1.0: + resolution: {integrity: sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-east-asian-width@1.6.0: + resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} + engines: {node: '>=18'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-package-type@0.1.0: + resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} + engines: {node: '>=8.0.0'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + + get-stream@8.0.1: + resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} + engines: {node: '>=16'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob-to-regexp@0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + global-modules@2.0.0: + resolution: {integrity: sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==} + engines: {node: '>=6'} + + global-prefix@3.0.0: + resolution: {integrity: sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==} + engines: {node: '>=6'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + globby@6.1.0: + resolution: {integrity: sha512-KVbFv2TQtbzCoxAnfD6JcHZTYCzyliEaaeM/gH8qQdkKr5s0OP9scEgvdcngyk7AVdY6YVW/TJHd+lQ/Df3Daw==} + engines: {node: '>=0.10.0'} + + globjoin@0.1.4: + resolution: {integrity: sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + gzip-size@6.0.0: + resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==} + engines: {node: '>=10'} + + handle-thing@2.0.1: + resolution: {integrity: sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==} + + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hashery@1.5.1: + resolution: {integrity: sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==} + engines: {node: '>=20'} + + hasown@2.0.3: + resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} + engines: {node: '>= 0.4'} + + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + + hoist-non-react-statics@3.3.2: + resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + + hookified@1.15.1: + resolution: {integrity: sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==} + + hookified@2.2.0: + resolution: {integrity: sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==} + + hpack.js@2.1.6: + resolution: {integrity: sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==} + + html-encoding-sniffer@3.0.0: + resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==} + engines: {node: '>=12'} + + html-entities@2.6.0: + resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + html-minifier-terser@6.1.0: + resolution: {integrity: sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==} + engines: {node: '>=12'} + hasBin: true + + html-parse-stringify@3.0.1: + resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} + + html-tags@3.3.1: + resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==} + engines: {node: '>=8'} + + html-webpack-plugin@5.6.7: + resolution: {integrity: sha512-md+vXtdCAe60s1k6AU3dUyMJnDxUyQAwfwPKoLisvgUF1IXjtlLsk2se54+qfL9Mdm26bbwvjJybpNx48NKRLw==} + engines: {node: '>=10.13.0'} + peerDependencies: + '@rspack/core': 0.x || 1.x + webpack: ^5.20.0 + peerDependenciesMeta: + '@rspack/core': + optional: true + webpack: + optional: true + + htmlparser2@6.1.0: + resolution: {integrity: sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==} + + http-deceiver@1.2.7: + resolution: {integrity: sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==} + + http-errors@1.8.1: + resolution: {integrity: sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==} + engines: {node: '>= 0.6'} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + http-parser-js@0.5.10: + resolution: {integrity: sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==} + + http-proxy-agent@5.0.0: + resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} + engines: {node: '>= 6'} + + http-proxy-middleware@2.0.9: + resolution: {integrity: sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@types/express': ^4.17.13 + peerDependenciesMeta: + '@types/express': + optional: true + + http-proxy@1.18.1: + resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==} + engines: {node: '>=8.0.0'} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + + human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + + human-signals@5.0.0: + resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} + engines: {node: '>=16.17.0'} + + husky@9.1.7: + resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} + engines: {node: '>=18'} + hasBin: true + + i18next@23.16.8: + resolution: {integrity: sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg==} + + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + icss-utils@5.1.0: + resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + image-size@0.5.5: + resolution: {integrity: sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==} + engines: {node: '>=0.10.0'} + hasBin: true + + immer@10.2.0: + resolution: {integrity: sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==} + + immer@11.1.8: + resolution: {integrity: sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA==} + + immutable@5.1.5: + resolution: {integrity: sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + import-local@3.2.0: + resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} + engines: {node: '>=8'} + hasBin: true + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + + interpret@3.1.1: + resolution: {integrity: sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==} + engines: {node: '>=10.13.0'} + + invariant@2.2.4: + resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + ipaddr.js@2.4.0: + resolution: {integrity: sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==} + engines: {node: '>= 10'} + + is-arguments@1.2.0: + resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} + engines: {node: '>= 0.4'} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.16.2: + resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-fullwidth-code-point@4.0.0: + resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==} + engines: {node: '>=12'} + + is-fullwidth-code-point@5.1.0: + resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==} + engines: {node: '>=18'} + + is-generator-fn@2.1.0: + resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} + engines: {node: '>=6'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-path-cwd@2.2.0: + resolution: {integrity: sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==} + engines: {node: '>=6'} + + is-path-in-cwd@2.1.0: + resolution: {integrity: sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==} + engines: {node: '>=6'} + + is-path-inside@2.1.0: + resolution: {integrity: sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==} + engines: {node: '>=6'} + + is-plain-obj@3.0.0: + resolution: {integrity: sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==} + engines: {node: '>=10'} + + is-plain-object@2.0.4: + resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} + engines: {node: '>=0.10.0'} + + is-plain-object@5.0.0: + resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} + engines: {node: '>=0.10.0'} + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-stream@3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + is-what@4.1.16: + resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} + engines: {node: '>=12.13'} + + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + isobject@3.0.1: + resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} + engines: {node: '>=0.10.0'} + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-instrument@5.2.1: + resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} + engines: {node: '>=8'} + + istanbul-lib-instrument@6.0.3: + resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==} + engines: {node: '>=10'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@4.0.1: + resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + + jest-changed-files@29.7.0: + resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-circus@29.7.0: + resolution: {integrity: sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-cli@29.7.0: + resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + jest-config@29.7.0: + resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@types/node': '*' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + ts-node: + optional: true + + jest-diff@29.7.0: + resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-docblock@29.7.0: + resolution: {integrity: sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-each@29.7.0: + resolution: {integrity: sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-environment-jsdom@29.7.0: + resolution: {integrity: sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + canvas: ^2.5.0 + peerDependenciesMeta: + canvas: + optional: true + + jest-environment-node@29.7.0: + resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-get-type@29.6.3: + resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-haste-map@29.7.0: + resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-leak-detector@29.7.0: + resolution: {integrity: sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-matcher-utils@29.7.0: + resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-message-util@29.7.0: + resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-mock@29.7.0: + resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-pnp-resolver@1.2.3: + resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} + engines: {node: '>=6'} + peerDependencies: + jest-resolve: '*' + peerDependenciesMeta: + jest-resolve: + optional: true + + jest-regex-util@29.6.3: + resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-resolve-dependencies@29.7.0: + resolution: {integrity: sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-resolve@29.7.0: + resolution: {integrity: sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-runner@29.7.0: + resolution: {integrity: sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-runtime@29.7.0: + resolution: {integrity: sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-snapshot@29.7.0: + resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-util@29.7.0: + resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-validate@29.7.0: + resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-watcher@29.7.0: + resolution: {integrity: sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-worker@27.5.1: + resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} + engines: {node: '>= 10.13.0'} + + jest-worker@29.7.0: + resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest@29.7.0: + resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + jquery@3.7.1: + resolution: {integrity: sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==} + + js-cookie@3.0.7: + resolution: {integrity: sha512-z/wZZgDrkNV1eA0ULjM/F9/50Ya8fbzgKneSpoPsXSGd0KnpdtHfOZWK+GcwLk+EZbS4F9RBhU+K2RgzuDaItw==} + engines: {node: '>=20'} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@3.14.2: + resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} + hasBin: true + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + jsdom@20.0.3: + resolution: {integrity: sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==} + engines: {node: '>=14'} + peerDependencies: + canvas: ^2.5.0 + peerDependenciesMeta: + canvas: + optional: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + keyv@5.6.0: + resolution: {integrity: sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==} + + kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + + known-css-properties@0.37.0: + resolution: {integrity: sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==} + + launch-editor@2.13.2: + resolution: {integrity: sha512-4VVDnbOpLXy/s8rdRCSXb+zfMeFR0WlJWpET1iA9CQdlZDfwyLjUuGQzXU4VeOoey6AicSAluWan7Etga6Kcmg==} + + less-loader@12.3.2: + resolution: {integrity: sha512-uLV5c702ff2jBvO7qewpkLRzkh/I9QW07ur2NKkv8TVTrtX2lrKjEbEU/LLXAn7cgpCIBbkfyUm4qYXCQs5/+w==} + engines: {node: '>= 18.12.0'} + peerDependencies: + '@rspack/core': 0.x || ^1.0.0 || ^2.0.0-0 + less: ^3.5.0 || ^4.0.0 + webpack: ^5.0.0 + peerDependenciesMeta: + '@rspack/core': + optional: true + webpack: + optional: true + + less@4.6.4: + resolution: {integrity: sha512-OJmO5+HxZLLw0RLzkqaNHzcgEAQG7C0y3aMbwtCzIUFZsLMNNq/1IdAdHEycQ58CwUO3jPTHmoN+tE5I7FQxNg==} + engines: {node: '>=18'} + hasBin: true + + leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + lint-staged@15.5.2: + resolution: {integrity: sha512-YUSOLq9VeRNAo/CTaVmhGDKG+LBtA8KF1X4K5+ykMSwWST1vDxJRB2kv2COgLb1fvpCo+A/y9A0G0znNVmdx4w==} + engines: {node: '>=18.12.0'} + hasBin: true + + listr2@8.3.3: + resolution: {integrity: sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ==} + engines: {node: '>=18.0.0'} + + loader-runner@4.3.2: + resolution: {integrity: sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==} + engines: {node: '>=6.11.5'} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash-es@4.18.1: + resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==} + + lodash.debounce@4.0.8: + resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + + lodash.memoize@4.1.2: + resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash.truncate@4.4.2: + resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==} + + lodash.uniq@4.5.0: + resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} + + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + + log-update@6.1.0: + resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} + engines: {node: '>=18'} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lower-case@2.0.2: + resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + + make-dir@2.1.0: + resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==} + engines: {node: '>=6'} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + makeerror@1.0.12: + resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mathml-tag-names@2.1.3: + resolution: {integrity: sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==} + + mdn-data@2.0.28: + resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==} + + mdn-data@2.0.30: + resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} + + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + + memfs@3.5.3: + resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==} + engines: {node: '>= 4.0.0'} + + memoize-one@6.0.0: + resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} + + meow@13.2.0: + resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==} + engines: {node: '>=18'} + + merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + mimic-fn@4.0.0: + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} + + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + + mini-css-extract-plugin@2.10.2: + resolution: {integrity: sha512-AOSS0IdEB95ayVkxn5oGzNQwqAi2J0Jb/kKm43t7H73s8+f5873g0yuj0PNvK4dO75mu5DHg4nlgp4k6Kga8eg==} + engines: {node: '>= 12.13.0'} + peerDependencies: + webpack: ^5.0.0 + + minimalistic-assert@1.0.1: + resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + + mobx-react-lite@4.1.1: + resolution: {integrity: sha512-iUxiMpsvNraCKXU+yPotsOncNNmyeS2B5DKL+TL6Tar/xm+wwNJAubJmtRSeAoYawdZqwv8Z/+5nPRHeQxTiXg==} + peerDependencies: + mobx: ^6.9.0 + react: ^16.8.0 || ^17 || ^18 || ^19 + react-dom: '*' + react-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + + mobx@6.15.3: + resolution: {integrity: sha512-6+ZSYDs5zgH5CdGfEU2q2Lsa5PztVryL1ys7kAImTU25n2A9LAMj/yneVsQpd03MfwMLDQF+7kakJR9Z+cQxSw==} + + moment-timezone@0.5.48: + resolution: {integrity: sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==} + + moment@2.30.1: + resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} + + motion-dom@11.18.1: + resolution: {integrity: sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==} + + motion-utils@11.18.1: + resolution: {integrity: sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==} + + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + multicast-dns@7.2.5: + resolution: {integrity: sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==} + hasBin: true + + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + needle@3.5.0: + resolution: {integrity: sha512-jaQyPKKk2YokHrEg+vFDYxXIHTCBgiZwSHOoVx/8V3GIBS8/VN6NdVRmg8q1ERtPkMvmOvebsgga4sAj5hls/w==} + engines: {node: '>= 4.4.x'} + hasBin: true + + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + + negotiator@0.6.4: + resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} + engines: {node: '>= 0.6'} + + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + + no-case@3.0.4: + resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} + + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + node-forge@1.4.0: + resolution: {integrity: sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==} + engines: {node: '>= 6.13.0'} + + node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + + node-releases@2.0.44: + resolution: {integrity: sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + + npm-run-path@5.3.0: + resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + nwsapi@2.2.23: + resolution: {integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-is@1.1.6: + resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + obuf@1.1.2: + resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + on-headers@1.1.0: + resolution: {integrity: sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + onetime@6.0.0: + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} + + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + + open@8.4.2: + resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} + engines: {node: '>=12'} + + opener@1.5.2: + resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==} + hasBin: true + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-map@2.1.0: + resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} + engines: {node: '>=6'} + + p-retry@4.6.2: + resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} + engines: {node: '>=8'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + param-case@3.0.4: + resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + parse-node-version@1.0.1: + resolution: {integrity: sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==} + engines: {node: '>= 0.10'} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + pascal-case@3.1.2: + resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-is-inside@1.0.2: + resolution: {integrity: sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-to-regexp@0.1.13: + resolution: {integrity: sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + pidtree@0.6.0: + resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} + engines: {node: '>=0.10'} + hasBin: true + + pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + + pify@4.0.1: + resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} + engines: {node: '>=6'} + + pinkie-promise@2.0.1: + resolution: {integrity: sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==} + engines: {node: '>=0.10.0'} + + pinkie@2.0.4: + resolution: {integrity: sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==} + engines: {node: '>=0.10.0'} + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + postcss-attribute-case-insensitive@6.0.3: + resolution: {integrity: sha512-KHkmCILThWBRtg+Jn1owTnHPnFit4OkqS+eKiGEOPIGke54DCeYGJ6r0Fx/HjfE9M9kznApCLcU0DvnPchazMQ==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.4 + + postcss-calc@9.0.1: + resolution: {integrity: sha512-TipgjGyzP5QzEhsOZUaIkeO5mKeMFpebWzRogWG/ysonUlnHcq5aJe0jOjpfzUU8PeSaBQnrE8ehR0QA5vs8PQ==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.2.2 + + postcss-clamp@4.1.0: + resolution: {integrity: sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow==} + engines: {node: '>=7.6.0'} + peerDependencies: + postcss: ^8.4.6 + + postcss-color-functional-notation@6.0.14: + resolution: {integrity: sha512-dNUX+UH4dAozZ8uMHZ3CtCNYw8fyFAmqqdcyxMr7PEdM9jLXV19YscoYO0F25KqZYhmtWKQ+4tKrIZQrwzwg7A==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.4 + + postcss-color-hex-alpha@9.0.4: + resolution: {integrity: sha512-XQZm4q4fNFqVCYMGPiBjcqDhuG7Ey2xrl99AnDJMyr5eDASsAGalndVgHZF8i97VFNy1GQeZc4q2ydagGmhelQ==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.4 + + postcss-color-rebeccapurple@9.0.3: + resolution: {integrity: sha512-ruBqzEFDYHrcVq3FnW3XHgwRqVMrtEPLBtD7K2YmsLKVc2jbkxzzNEctJKsPCpDZ+LeMHLKRDoSShVefGc+CkQ==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.4 + + postcss-colormin@6.1.0: + resolution: {integrity: sha512-x9yX7DOxeMAR+BgGVnNSAxmAj98NX/YxEMNFP+SDCEeNLb2r3i6Hh1ksMsnW8Ub5SLCpbescQqn9YEbE9554Sw==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-convert-values@6.1.0: + resolution: {integrity: sha512-zx8IwP/ts9WvUM6NkVSkiU902QZL1bwPhaVaLynPtCsOTqp+ZKbNi+s6XJg3rfqpKGA/oc7Oxk5t8pOQJcwl/w==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-custom-media@10.0.8: + resolution: {integrity: sha512-V1KgPcmvlGdxTel4/CyQtBJEFhMVpEmRGFrnVtgfGIHj5PJX9vO36eFBxKBeJn+aCDTed70cc+98Mz3J/uVdGQ==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.4 + + postcss-custom-properties@13.3.12: + resolution: {integrity: sha512-oPn/OVqONB2ZLNqN185LDyaVByELAA/u3l2CS2TS16x2j2XsmV4kd8U49+TMxmUsEU9d8fB/I10E6U7kB0L1BA==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.4 + + postcss-custom-selectors@7.1.12: + resolution: {integrity: sha512-ctIoprBMJwByYMGjXG0F7IT2iMF2hnamQ+aWZETyBM0aAlyaYdVZTeUkk8RB+9h9wP+NdN3f01lfvKl2ZSqC0g==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.4 + + postcss-dir-pseudo-class@8.0.1: + resolution: {integrity: sha512-uULohfWBBVoFiZXgsQA24JV6FdKIidQ+ZqxOouhWwdE+qJlALbkS5ScB43ZTjPK+xUZZhlaO/NjfCt5h4IKUfw==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.4 + + postcss-discard-comments@6.0.2: + resolution: {integrity: sha512-65w/uIqhSBBfQmYnG92FO1mWZjJ4GL5b8atm5Yw2UgrwD7HiNiSSNwJor1eCFGzUgYnN/iIknhNRVqjrrpuglw==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-discard-duplicates@6.0.3: + resolution: {integrity: sha512-+JA0DCvc5XvFAxwx6f/e68gQu/7Z9ud584VLmcgto28eB8FqSFZwtrLwB5Kcp70eIoWP/HXqz4wpo8rD8gpsTw==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-discard-empty@6.0.3: + resolution: {integrity: sha512-znyno9cHKQsK6PtxL5D19Fj9uwSzC2mB74cpT66fhgOadEUPyXFkbgwm5tvc3bt3NAy8ltE5MrghxovZRVnOjQ==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-discard-overridden@6.0.2: + resolution: {integrity: sha512-j87xzI4LUggC5zND7KdjsI25APtyMuynXZSujByMaav2roV6OZX+8AaCUcZSWqckZpjAjRyFDdpqybgjFO0HJQ==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-double-position-gradients@5.0.7: + resolution: {integrity: sha512-1xEhjV9u1s4l3iP5lRt1zvMjI/ya8492o9l/ivcxHhkO3nOz16moC4JpMxDUGrOs4R3hX+KWT7gKoV842cwRgg==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.4 + + postcss-focus-visible@9.0.1: + resolution: {integrity: sha512-N2VQ5uPz3Z9ZcqI5tmeholn4d+1H14fKXszpjogZIrFbhaq0zNAtq8sAnw6VLiqGbL8YBzsnu7K9bBkTqaRimQ==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.4 + + postcss-focus-within@8.0.1: + resolution: {integrity: sha512-NFU3xcY/xwNaapVb+1uJ4n23XImoC86JNwkY/uduytSl2s9Ekc2EpzmRR63+ExitnW3Mab3Fba/wRPCT5oDILA==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.4 + + postcss-font-variant@5.0.0: + resolution: {integrity: sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==} + peerDependencies: + postcss: ^8.1.0 + + postcss-gap-properties@5.0.1: + resolution: {integrity: sha512-k2z9Cnngc24c0KF4MtMuDdToROYqGMMUQGcE6V0odwjHyOHtaDBlLeRBV70y9/vF7KIbShrTRZ70JjsI1BZyWw==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.4 + + postcss-image-set-function@6.0.3: + resolution: {integrity: sha512-i2bXrBYzfbRzFnm+pVuxVePSTCRiNmlfssGI4H0tJQvDue+yywXwUxe68VyzXs7cGtMaH6MCLY6IbCShrSroCw==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.4 + + postcss-lab-function@6.0.19: + resolution: {integrity: sha512-vwln/mgvFrotJuGV8GFhpAOu9iGf3pvTBr6dLPDmUcqVD5OsQpEFyQMAFTxSxWXGEzBj6ld4pZ/9GDfEpXvo0g==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.4 + + postcss-loader@8.2.1: + resolution: {integrity: sha512-k98jtRzthjj3f76MYTs9JTpRqV1RaaMhEU0Lpw9OTmQZQdppg4B30VZ74BojuBHt3F4KyubHJoXCMUeM8Bqeow==} + engines: {node: '>= 18.12.0'} + peerDependencies: + '@rspack/core': 0.x || ^1.0.0 || ^2.0.0-0 + postcss: ^7.0.0 || ^8.0.1 + webpack: ^5.0.0 + peerDependenciesMeta: + '@rspack/core': + optional: true + webpack: + optional: true + + postcss-logical@7.0.1: + resolution: {integrity: sha512-8GwUQZE0ri0K0HJHkDv87XOLC8DE0msc+HoWLeKdtjDZEwpZ5xuK3QdV6FhmHSQW40LPkg43QzvATRAI3LsRkg==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.4 + + postcss-merge-longhand@6.0.5: + resolution: {integrity: sha512-5LOiordeTfi64QhICp07nzzuTDjNSO8g5Ksdibt44d+uvIIAE1oZdRn8y/W5ZtYgRH/lnLDlvi9F8btZcVzu3w==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-merge-rules@6.1.1: + resolution: {integrity: sha512-KOdWF0gju31AQPZiD+2Ar9Qjowz1LTChSjFFbS+e2sFgc4uHOp3ZvVX4sNeTlk0w2O31ecFGgrFzhO0RSWbWwQ==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-minify-font-values@6.1.0: + resolution: {integrity: sha512-gklfI/n+9rTh8nYaSJXlCo3nOKqMNkxuGpTn/Qm0gstL3ywTr9/WRKznE+oy6fvfolH6dF+QM4nCo8yPLdvGJg==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-minify-gradients@6.0.3: + resolution: {integrity: sha512-4KXAHrYlzF0Rr7uc4VrfwDJ2ajrtNEpNEuLxFgwkhFZ56/7gaE4Nr49nLsQDZyUe+ds+kEhf+YAUolJiYXF8+Q==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-minify-params@6.1.0: + resolution: {integrity: sha512-bmSKnDtyyE8ujHQK0RQJDIKhQ20Jq1LYiez54WiaOoBtcSuflfK3Nm596LvbtlFcpipMjgClQGyGr7GAs+H1uA==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-minify-selectors@6.0.4: + resolution: {integrity: sha512-L8dZSwNLgK7pjTto9PzWRoMbnLq5vsZSTu8+j1P/2GB8qdtGQfn+K1uSvFgYvgh83cbyxT5m43ZZhUMTJDSClQ==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-modules-extract-imports@3.1.0: + resolution: {integrity: sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + postcss-modules-local-by-default@4.2.0: + resolution: {integrity: sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + postcss-modules-scope@3.2.1: + resolution: {integrity: sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + postcss-modules-values@4.0.0: + resolution: {integrity: sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + postcss-nesting@12.1.5: + resolution: {integrity: sha512-N1NgI1PDCiAGWPTYrwqm8wpjv0bgDmkYHH72pNsqTCv9CObxjxftdYu6AKtGN+pnJa7FQjMm3v4sp8QJbFsYdQ==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.4 + + postcss-normalize-charset@6.0.2: + resolution: {integrity: sha512-a8N9czmdnrjPHa3DeFlwqst5eaL5W8jYu3EBbTTkI5FHkfMhFZh1EGbku6jhHhIzTA6tquI2P42NtZ59M/H/kQ==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-normalize-display-values@6.0.2: + resolution: {integrity: sha512-8H04Mxsb82ON/aAkPeq8kcBbAtI5Q2a64X/mnRRfPXBq7XeogoQvReqxEfc0B4WPq1KimjezNC8flUtC3Qz6jg==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-normalize-positions@6.0.2: + resolution: {integrity: sha512-/JFzI441OAB9O7VnLA+RtSNZvQ0NCFZDOtp6QPFo1iIyawyXg0YI3CYM9HBy1WvwCRHnPep/BvI1+dGPKoXx/Q==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-normalize-repeat-style@6.0.2: + resolution: {integrity: sha512-YdCgsfHkJ2jEXwR4RR3Tm/iOxSfdRt7jplS6XRh9Js9PyCR/aka/FCb6TuHT2U8gQubbm/mPmF6L7FY9d79VwQ==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-normalize-string@6.0.2: + resolution: {integrity: sha512-vQZIivlxlfqqMp4L9PZsFE4YUkWniziKjQWUtsxUiVsSSPelQydwS8Wwcuw0+83ZjPWNTl02oxlIvXsmmG+CiQ==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-normalize-timing-functions@6.0.2: + resolution: {integrity: sha512-a+YrtMox4TBtId/AEwbA03VcJgtyW4dGBizPl7e88cTFULYsprgHWTbfyjSLyHeBcK/Q9JhXkt2ZXiwaVHoMzA==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-normalize-unicode@6.1.0: + resolution: {integrity: sha512-QVC5TQHsVj33otj8/JD869Ndr5Xcc/+fwRh4HAsFsAeygQQXm+0PySrKbr/8tkDKzW+EVT3QkqZMfFrGiossDg==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-normalize-url@6.0.2: + resolution: {integrity: sha512-kVNcWhCeKAzZ8B4pv/DnrU1wNh458zBNp8dh4y5hhxih5RZQ12QWMuQrDgPRw3LRl8mN9vOVfHl7uhvHYMoXsQ==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-normalize-whitespace@6.0.2: + resolution: {integrity: sha512-sXZ2Nj1icbJOKmdjXVT9pnyHQKiSAyuNQHSgRCUgThn2388Y9cGVDR+E9J9iAYbSbLHI+UUwLVl1Wzco/zgv0Q==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-opacity-percentage@2.0.0: + resolution: {integrity: sha512-lyDrCOtntq5Y1JZpBFzIWm2wG9kbEdujpNt4NLannF+J9c8CgFIzPa80YQfdza+Y+yFfzbYj/rfoOsYsooUWTQ==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.2 + + postcss-ordered-values@6.0.2: + resolution: {integrity: sha512-VRZSOB+JU32RsEAQrO94QPkClGPKJEL/Z9PCBImXMhIeK5KAYo6slP/hBYlLgrCjFxyqvn5VC81tycFEDBLG1Q==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-overflow-shorthand@5.0.1: + resolution: {integrity: sha512-XzjBYKLd1t6vHsaokMV9URBt2EwC9a7nDhpQpjoPk2HRTSQfokPfyAS/Q7AOrzUu6q+vp/GnrDBGuj/FCaRqrQ==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.4 + + postcss-page-break@3.0.4: + resolution: {integrity: sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==} + peerDependencies: + postcss: ^8 + + postcss-place@9.0.1: + resolution: {integrity: sha512-JfL+paQOgRQRMoYFc2f73pGuG/Aw3tt4vYMR6UA3cWVMxivviPTnMFnFTczUJOA4K2Zga6xgQVE+PcLs64WC8Q==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.4 + + postcss-preset-env@9.6.0: + resolution: {integrity: sha512-Lxfk4RYjUdwPCYkc321QMdgtdCP34AeI94z+/8kVmqnTIlD4bMRQeGcMZgwz8BxHrzQiFXYIR5d7k/9JMs2MEA==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.4 + + postcss-pseudo-class-any-link@9.0.2: + resolution: {integrity: sha512-HFSsxIqQ9nA27ahyfH37cRWGk3SYyQLpk0LiWw/UGMV4VKT5YG2ONee4Pz/oFesnK0dn2AjcyequDbIjKJgB0g==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.4 + + postcss-reduce-initial@6.1.0: + resolution: {integrity: sha512-RarLgBK/CrL1qZags04oKbVbrrVK2wcxhvta3GCxrZO4zveibqbRPmm2VI8sSgCXwoUHEliRSbOfpR0b/VIoiw==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-reduce-transforms@6.0.2: + resolution: {integrity: sha512-sB+Ya++3Xj1WaT9+5LOOdirAxP7dJZms3GRcYheSPi1PiTMigsxHAdkrbItHxwYHr4kt1zL7mmcHstgMYT+aiA==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-replace-overflow-wrap@4.0.0: + resolution: {integrity: sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==} + peerDependencies: + postcss: ^8.0.3 + + postcss-resolve-nested-selector@0.1.6: + resolution: {integrity: sha512-0sglIs9Wmkzbr8lQwEyIzlDOOC9bGmfVKcJTaxv3vMmd3uo4o4DerC3En0bnmgceeql9BfC8hRkp7cg0fjdVqw==} + + postcss-safe-parser@7.0.1: + resolution: {integrity: sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==} + engines: {node: '>=18.0'} + peerDependencies: + postcss: ^8.4.31 + + postcss-selector-not@7.0.2: + resolution: {integrity: sha512-/SSxf/90Obye49VZIfc0ls4H0P6i6V1iHv0pzZH8SdgvZOPFkF37ef1r5cyWcMflJSFJ5bfuoluTnFnBBFiuSA==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.4 + + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + + postcss-selector-parser@7.1.1: + resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} + engines: {node: '>=4'} + + postcss-svgo@6.0.3: + resolution: {integrity: sha512-dlrahRmxP22bX6iKEjOM+c8/1p+81asjKT+V5lrgOH944ryx/OHpclnIbGsKVd3uWOXFLYJwCVf0eEkJGvO96g==} + engines: {node: ^14 || ^16 || >= 18} + peerDependencies: + postcss: ^8.4.31 + + postcss-unique-selectors@6.0.4: + resolution: {integrity: sha512-K38OCaIrO8+PzpArzkLKB42dSARtC2tmG6PvD4b1o1Q2E9Os8jzfWFfSy/rixsHwohtsDdFtAWGjFVFUdwYaMg==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.5.14: + resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier-linter-helpers@1.0.1: + resolution: {integrity: sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==} + engines: {node: '>=6.0.0'} + + prettier@3.8.3: + resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==} + engines: {node: '>=14'} + hasBin: true + + pretty-error@4.0.0: + resolution: {integrity: sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==} + + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + property-expr@2.0.6: + resolution: {integrity: sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + proxy-from-env@2.1.0: + resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} + engines: {node: '>=10'} + + prr@1.0.1: + resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==} + + psl@1.15.0: + resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + pure-rand@6.1.0: + resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + + qified@0.10.1: + resolution: {integrity: sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==} + engines: {node: '>=20'} + + qs@6.15.2: + resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==} + engines: {node: '>=0.6'} + + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@2.5.3: + resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} + engines: {node: '>= 0.8'} + + react-dnd-html5-backend@16.0.1: + resolution: {integrity: sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==} + + react-dnd@16.0.1: + resolution: {integrity: sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==} + peerDependencies: + '@types/hoist-non-react-statics': '>= 3.3.1' + '@types/node': '>= 12' + '@types/react': '>= 16' + react: '>= 16.14' + peerDependenciesMeta: + '@types/hoist-non-react-statics': + optional: true + '@types/node': + optional: true + '@types/react': + optional: true + + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + + react-dropzone@14.4.1: + resolution: {integrity: sha512-QDuV76v3uKbHiH34SpwifZ+gOLi1+RdsCO1kl5vxMT4wW8R82+sthjvBw4th3NHF/XX6FBsqDYZVNN+pnhaw0g==} + engines: {node: '>= 10.13'} + peerDependencies: + react: '>= 16.8 || 18.0.0' + + react-fast-compare@2.0.4: + resolution: {integrity: sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==} + + react-fast-compare@3.2.2: + resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} + + react-helmet-async@2.0.5: + resolution: {integrity: sha512-rYUYHeus+i27MvFE+Jaa4WsyBKGkL6qVgbJvSBoX8mbsWoABJXdEO0bZyi0F6i+4f0NuIb8AvqPMj3iXFHkMwg==} + peerDependencies: + react: ^16.6.0 || ^17.0.0 || ^18.0.0 + + react-hook-form@7.76.0: + resolution: {integrity: sha512-eKtLGgFeSgkHqQD8J59AMZ9a4uD1D83iSIzt4YlTGD7liDen5rrjcUO1rVIGd9yC1gofryjtHbv+4ny4hkLWlw==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + + react-i18next@14.1.3: + resolution: {integrity: sha512-wZnpfunU6UIAiJ+bxwOiTmBOAaB14ha97MjOEnLGac2RJ+h/maIYXZuTHlmyqQVX1UVHmU1YDTQ5vxLmwfXTjw==} + peerDependencies: + i18next: '>= 23.2.3' + react: '>= 16.8.0' + react-dom: '*' + react-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + + react-is@19.2.6: + resolution: {integrity: sha512-XjBR15BhXuylgWGuslhDKqlSayuqvqBX91BP8pauG8kd1zY8kotkNWbXksTCNRarse4kuGbe2kIY05ARtwNIvw==} + + react-lifecycles-compat@3.0.4: + resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==} + + react-redux@9.3.0: + resolution: {integrity: sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==} + peerDependencies: + '@types/react': ^18.2.25 || ^19 + react: ^18.0 || ^19 + redux: ^5.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + redux: + optional: true + + react-router-dom@6.30.3: + resolution: {integrity: sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + + react-router@6.30.3: + resolution: {integrity: sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: '>=16.8' + + react-select@5.10.2: + resolution: {integrity: sha512-Z33nHdEFWq9tfnfVXaiM12rbJmk+QjFEztWLtmXqQhz6Al4UZZ9xc0wiatmGtUOCCnHN0WizL3tCMYRENX4rVQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + react-smooth@4.0.4: + resolution: {integrity: sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + react-tooltip@5.30.1: + resolution: {integrity: sha512-1lSPLQXuVooePxadUpmcwLgOsF1mIty7UZTJ9XnyfX4drOzStYs4JMXnazcDLguQr41W5OUZddOp9kfvArdpEQ==} + peerDependencies: + react: '>=16.14.0' + react-dom: '>=16.14.0' + + react-transition-group@4.4.5: + resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} + peerDependencies: + react: '>=16.6.0' + react-dom: '>=16.6.0' + + react-virtualized@9.22.6: + resolution: {integrity: sha512-U5j7KuUQt3AaMatlMJ0UJddqSiX+Km0YJxSqbAzIiGw5EmNz0khMyqP2hzgu4+QUtm+QPIrxzUX4raJxmVJnHg==} + peerDependencies: + react: ^16.3.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.3.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + recharts-scale@0.4.5: + resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==} + + recharts@2.15.4: + resolution: {integrity: sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==} + engines: {node: '>=14'} + peerDependencies: + react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + rechoir@0.8.0: + resolution: {integrity: sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==} + engines: {node: '>= 10.13.0'} + + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + + redux-thunk@3.1.0: + resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} + peerDependencies: + redux: ^5.0.0 + + redux@4.2.1: + resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==} + + redux@5.0.1: + resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} + + regenerate-unicode-properties@10.2.2: + resolution: {integrity: sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==} + engines: {node: '>=4'} + + regenerate@1.4.2: + resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + + regexpu-core@6.4.0: + resolution: {integrity: sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==} + engines: {node: '>=4'} + + regjsgen@0.8.0: + resolution: {integrity: sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==} + + regjsparser@0.13.1: + resolution: {integrity: sha512-dLsljMd9sqwRkby8zhO1gSg3PnJIBFid8f4CQj/sXx+7cKx+E7u0PKhZ+U4wmhx7EfmtvnA318oVaIkAB1lRJw==} + hasBin: true + + relateurl@0.2.7: + resolution: {integrity: sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==} + engines: {node: '>= 0.10'} + + renderkid@3.0.0: + resolution: {integrity: sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + + reselect@5.2.0: + resolution: {integrity: sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==} + + resolve-cwd@3.0.0: + resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} + engines: {node: '>=8'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + resolve.exports@2.0.3: + resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} + engines: {node: '>=10'} + + resolve@1.22.12: + resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} + engines: {node: '>= 0.4'} + hasBin: true + + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + + retry@0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rimraf@2.7.1: + resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + sass-loader@14.2.1: + resolution: {integrity: sha512-G0VcnMYU18a4N7VoNDegg2OuMjYtxnqzQWARVWCIVSZwJeiL9kg8QMsuIZOplsJgTzZLF6jGxI3AClj8I9nRdQ==} + engines: {node: '>= 18.12.0'} + peerDependencies: + '@rspack/core': 0.x || 1.x + node-sass: ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 + sass: ^1.3.0 + sass-embedded: '*' + webpack: ^5.0.0 + peerDependenciesMeta: + '@rspack/core': + optional: true + node-sass: + optional: true + sass: + optional: true + sass-embedded: + optional: true + webpack: + optional: true + + sass@1.99.0: + resolution: {integrity: sha512-kgW13M54DUB7IsIRM5LvJkNlpH+WhMpooUcaWGFARkF1Tc82v9mIWkCbCYf+MBvpIUBSeSOTilpZjEPr2VYE6Q==} + engines: {node: '>=14.0.0'} + hasBin: true + + sax@1.6.0: + resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} + engines: {node: '>=11.0.0'} + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + + schema-utils@4.3.3: + resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} + engines: {node: '>= 10.13.0'} + + select-hose@2.0.0: + resolution: {integrity: sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==} + + selfsigned@2.4.1: + resolution: {integrity: sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==} + engines: {node: '>=10'} + + semver@5.7.2: + resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} + hasBin: true + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.8.0: + resolution: {integrity: sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==} + engines: {node: '>=10'} + hasBin: true + + send@0.19.2: + resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==} + engines: {node: '>= 0.8.0'} + + serve-index@1.9.2: + resolution: {integrity: sha512-KDj11HScOaLmrPxl70KYNW1PksP4Nb/CLL2yvC+Qd2kHMPEEpfc4Re2e4FOay+bC/+XQl/7zAcWON3JVo5v3KQ==} + engines: {node: '>= 0.8.0'} + + serve-static@1.16.3: + resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==} + engines: {node: '>= 0.8.0'} + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + shallow-clone@3.0.1: + resolution: {integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==} + engines: {node: '>=8'} + + shallowequal@1.1.0: + resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + shell-quote@1.8.3: + resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} + engines: {node: '>= 0.4'} + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + sirv@2.0.4: + resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==} + engines: {node: '>= 10'} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + slice-ansi@4.0.0: + resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} + engines: {node: '>=10'} + + slice-ansi@5.0.0: + resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} + engines: {node: '>=12'} + + slice-ansi@7.1.2: + resolution: {integrity: sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==} + engines: {node: '>=18'} + + sockjs@0.3.24: + resolution: {integrity: sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==} + + sortablejs@1.15.7: + resolution: {integrity: sha512-Kk8wLQPlS+yi1ZEf48a4+fzHa4yxjC30M/Sr2AnQu+f/MPwvvX9XjZ6OWejiz8crBsLwSq8GHqaxaET7u6ux0A==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.13: + resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.5.7: + resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} + engines: {node: '>=0.10.0'} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + spdy-transport@3.0.0: + resolution: {integrity: sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==} + + spdy@4.0.2: + resolution: {integrity: sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==} + engines: {node: '>=6.0.0'} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + stack-utils@2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} + + statuses@1.5.0: + resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} + engines: {node: '>= 0.6'} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + + string-argv@0.3.2: + resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} + engines: {node: '>=0.6.19'} + + string-length@4.0.2: + resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} + engines: {node: '>=10'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + strip-bom@4.0.0: + resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + engines: {node: '>=8'} + + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + + strip-final-newline@3.0.0: + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} + + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + style-loader@3.3.4: + resolution: {integrity: sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w==} + engines: {node: '>= 12.13.0'} + peerDependencies: + webpack: ^5.0.0 + + stylehacks@6.1.1: + resolution: {integrity: sha512-gSTTEQ670cJNoaeIp9KX6lZmm8LJ3jPB5yJmX8Zq/wQxOsAFXV3qjWzHas3YYk1qesuVIyYWWUpZ0vSE/dTSGg==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + stylelint-config-recommended@14.0.1: + resolution: {integrity: sha512-bLvc1WOz/14aPImu/cufKAZYfXs/A/owZfSMZ4N+16WGXLoX5lOir53M6odBxvhgmgdxCVnNySJmZKx73T93cg==} + engines: {node: '>=18.12.0'} + peerDependencies: + stylelint: ^16.1.0 + + stylelint-config-standard@36.0.1: + resolution: {integrity: sha512-8aX8mTzJ6cuO8mmD5yon61CWuIM4UD8Q5aBcWKGSf6kg+EC3uhB+iOywpTK4ca6ZL7B49en8yanOFtUW0qNzyw==} + engines: {node: '>=18.12.0'} + peerDependencies: + stylelint: ^16.1.0 + + stylelint@16.26.1: + resolution: {integrity: sha512-v20V59/crfc8sVTAtge0mdafI3AdnzQ2KsWe6v523L4OA1bJO02S7MO2oyXDCS6iWb9ckIPnqAFVItqSBQr7jw==} + engines: {node: '>=18.12.0'} + hasBin: true + + stylis@4.2.0: + resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + supports-hyperlinks@3.2.0: + resolution: {integrity: sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==} + engines: {node: '>=14.18'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + svg-tags@1.0.0: + resolution: {integrity: sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==} + + svgo@3.3.3: + resolution: {integrity: sha512-+wn7I4p7YgJhHs38k2TNjy1vCfPIfLIJWR5MnCStsN8WuuTcBnRKcMHQLMM2ijxGZmDoZwNv8ipl5aTTen62ng==} + engines: {node: '>=14.0.0'} + hasBin: true + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + + synckit@0.11.12: + resolution: {integrity: sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==} + engines: {node: ^14.18.0 || >=16.0.0} + + table@6.9.0: + resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} + engines: {node: '>=10.0.0'} + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + + terser-webpack-plugin@5.6.0: + resolution: {integrity: sha512-Eum+5ajkaOhf5KbM26osvv21kLD7BaGqQ1UA4Ami4arYwylmGUQTgHFpHDdmJod1q4QXa66p0to/FBKID+J1vA==} + engines: {node: '>= 10.13.0'} + peerDependencies: + '@minify-html/node': '*' + '@swc/core': '*' + '@swc/css': '*' + '@swc/html': '*' + clean-css: '*' + cssnano: '*' + csso: '*' + esbuild: '*' + html-minifier-terser: '*' + lightningcss: '*' + postcss: '*' + uglify-js: '*' + webpack: ^5.1.0 + peerDependenciesMeta: + '@minify-html/node': + optional: true + '@swc/core': + optional: true + '@swc/css': + optional: true + '@swc/html': + optional: true + clean-css: + optional: true + cssnano: + optional: true + csso: + optional: true + esbuild: + optional: true + html-minifier-terser: + optional: true + lightningcss: + optional: true + postcss: + optional: true + uglify-js: + optional: true + + terser@5.47.1: + resolution: {integrity: sha512-tPbLXTI6ohPASb/1YViL428oEHu6/qv1OxqYnfaonVCFHqx4+wCd95pHrQWsL5X4pl90CTyW9piSAsS2L0VoMw==} + engines: {node: '>=10'} + hasBin: true + + test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + + thunky@1.1.0: + resolution: {integrity: sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==} + + tiny-case@1.0.3: + resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==} + + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + + tiny-warning@1.0.3: + resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} + + tmpl@1.0.5: + resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + toposort@2.0.2: + resolution: {integrity: sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==} + + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + + tough-cookie@4.1.4: + resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} + engines: {node: '>=6'} + + tr46@3.0.0: + resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==} + engines: {node: '>=12'} + + ts-api-utils@1.4.3: + resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} + engines: {node: '>=16'} + peerDependencies: + typescript: '>=4.2.0' + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + type-fest@2.19.0: + resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} + engines: {node: '>=12.20'} + + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + unicode-canonical-property-names-ecmascript@2.0.1: + resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==} + engines: {node: '>=4'} + + unicode-match-property-ecmascript@2.0.0: + resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} + engines: {node: '>=4'} + + unicode-match-property-value-ecmascript@2.2.1: + resolution: {integrity: sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==} + engines: {node: '>=4'} + + unicode-property-aliases-ecmascript@2.2.0: + resolution: {integrity: sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==} + engines: {node: '>=4'} + + universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + + use-isomorphic-layout-effect@1.2.1: + resolution: {integrity: sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + utila@0.4.0: + resolution: {integrity: sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==} + + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). + hasBin: true + + uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). + hasBin: true + + v8-to-istanbul@9.3.0: + resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} + engines: {node: '>=10.12.0'} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + victory-vendor@36.9.2: + resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==} + + void-elements@3.1.0: + resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} + engines: {node: '>=0.10.0'} + + w3c-xmlserializer@4.0.0: + resolution: {integrity: sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==} + engines: {node: '>=14'} + + walker@1.0.8: + resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + + watchpack@2.5.1: + resolution: {integrity: sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==} + engines: {node: '>=10.13.0'} + + wbuf@1.7.3: + resolution: {integrity: sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==} + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + webpack-bundle-analyzer@4.10.2: + resolution: {integrity: sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw==} + engines: {node: '>= 10.13.0'} + hasBin: true + + webpack-cli@5.1.4: + resolution: {integrity: sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==} + engines: {node: '>=14.15.0'} + hasBin: true + peerDependencies: + '@webpack-cli/generators': '*' + webpack: 5.x.x + webpack-bundle-analyzer: '*' + webpack-dev-server: '*' + peerDependenciesMeta: + '@webpack-cli/generators': + optional: true + webpack-bundle-analyzer: + optional: true + webpack-dev-server: + optional: true + + webpack-dev-middleware@5.3.4: + resolution: {integrity: sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==} + engines: {node: '>= 12.13.0'} + peerDependencies: + webpack: ^4.0.0 || ^5.0.0 + + webpack-dev-server@4.15.2: + resolution: {integrity: sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==} + engines: {node: '>= 12.13.0'} + hasBin: true + peerDependencies: + webpack: ^4.37.0 || ^5.0.0 + webpack-cli: '*' + peerDependenciesMeta: + webpack: + optional: true + webpack-cli: + optional: true + + webpack-merge@5.10.0: + resolution: {integrity: sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==} + engines: {node: '>=10.0.0'} + + webpack-sources@3.4.1: + resolution: {integrity: sha512-eACpxRN02yaawnt+uUNIF7Qje6A9zArxBbcAJjK1PK3S9Ycg5jIuJ8pW4q8EMnwNZCEGltcjkRx1QzOxOkKD8A==} + engines: {node: '>=10.13.0'} + + webpack@5.106.2: + resolution: {integrity: sha512-wGN3qcrBQIFmQ/c0AiOAQBvrZ5lmY8vbbMv4Mxfgzqd/B6+9pXtLo73WuS1dSGXM5QYY3hZnIbvx+K1xxe6FyA==} + engines: {node: '>=10.13.0'} + hasBin: true + peerDependencies: + webpack-cli: '*' + peerDependenciesMeta: + webpack-cli: + optional: true + + websocket-driver@0.7.4: + resolution: {integrity: sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==} + engines: {node: '>=0.8.0'} + + websocket-extensions@0.1.4: + resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==} + engines: {node: '>=0.8.0'} + + whatwg-encoding@2.0.0: + resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} + engines: {node: '>=12'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation + + whatwg-fetch@3.6.20: + resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==} + + whatwg-mimetype@3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + + whatwg-url@11.0.0: + resolution: {integrity: sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==} + engines: {node: '>=12'} + + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.20: + resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==} + engines: {node: '>= 0.4'} + + which@1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + wildcard@2.0.1: + resolution: {integrity: sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==} + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} + engines: {node: '>=18'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + write-file-atomic@4.0.2: + resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + write-file-atomic@5.0.1: + resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + ws@7.5.10: + resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@8.20.1: + resolution: {integrity: sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xml-name-validator@4.0.0: + resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} + engines: {node: '>=12'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yaml@1.10.3: + resolution: {integrity: sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==} + engines: {node: '>= 6'} + + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + engines: {node: '>= 14.6'} + hasBin: true + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + yup@1.7.1: + resolution: {integrity: sha512-GKHFX2nXul2/4Dtfxhozv701jLQHdf6J34YDh2cEkpqoo8le5Mg6/LrdseVLrFarmFygZTlfIhHx/QKfb/QWXw==} + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + + zustand@4.5.7: + resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} + engines: {node: '>=12.7.0'} + peerDependencies: + '@types/react': '>=16.8' + immer: '>=9.0.6' + react: '>=16.8' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + +snapshots: + + '@adobe/css-tools@4.4.4': {} + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.3': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.29.2 + '@babel/parser': 7.29.3 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-annotate-as-pure@7.27.3': + dependencies: + '@babel/types': 7.29.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.3 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.29.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/traverse': 7.29.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-create-regexp-features-plugin@7.28.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + regexpu-core: 6.4.0 + semver: 6.3.1 + + '@babel/helper-define-polyfill-provider@0.6.8(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + debug: 4.4.3 + lodash.debounce: 4.0.8 + resolve: 1.22.12 + transitivePeerDependencies: + - supports-color + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-member-expression-to-functions@7.28.5': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.27.1': + dependencies: + '@babel/types': 7.29.0 + + '@babel/helper-plugin-utils@7.28.6': {} + + '@babel/helper-remap-async-to-generator@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-wrap-function': 7.28.6 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-replace-supers@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helper-wrap-function@7.28.6': + dependencies: + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helpers@7.29.2': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + + '@babel/parser@7.29.3': + dependencies: + '@babel/types': 7.29.0 + + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-bugfix-safari-rest-destructuring-rhs-array@7.29.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-transform-optional-chaining': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-import-assertions@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-import-attributes@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-async-generator-functions@7.29.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.29.0) + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-async-to-generator@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-block-scoped-functions@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-block-scoping@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-class-properties@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-class-static-block@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-classes@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-globals': 7.28.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0) + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-computed-properties@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/template': 7.28.6 + + '@babel/plugin-transform-destructuring@7.28.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-dotall-regex@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-duplicate-keys@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.29.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-dynamic-import@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-explicit-resource-management@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-exponentiation-operator@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-export-namespace-from@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-for-of@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-function-name@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-json-strings@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-literals@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-logical-assignment-operators@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-member-expression-literals@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-modules-amd@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-commonjs@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-systemjs@7.29.4(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-umd@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-named-capturing-groups-regex@7.29.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-new-target@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-nullish-coalescing-operator@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-numeric-separator@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-object-rest-spread@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.0) + '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.29.0) + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-object-super@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-optional-catch-binding@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-optional-chaining@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-parameters@7.27.7(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-private-methods@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-private-property-in-object@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-property-literals@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-react-display-name@7.28.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-react-jsx-development@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-transform-react-jsx': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-react-jsx@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-react-pure-annotations@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-regenerator@7.29.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-regexp-modifiers@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-reserved-words@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-runtime@7.29.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + babel-plugin-polyfill-corejs2: 0.4.17(@babel/core@7.29.0) + babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.29.0) + babel-plugin-polyfill-regenerator: 0.6.8(@babel/core@7.29.0) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-shorthand-properties@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-spread@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-sticky-regex@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-template-literals@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-typeof-symbol@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-typescript@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-unicode-escapes@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-unicode-property-regex@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-unicode-regex@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-unicode-sets-regex@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/preset-env@7.29.5(@babel/core@7.29.0)': + dependencies: + '@babel/compat-data': 7.29.3 + '@babel/core': 7.29.0 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-validator-option': 7.27.1 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.28.5(@babel/core@7.29.0) + '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-bugfix-safari-rest-destructuring-rhs-array': 7.29.3(@babel/core@7.29.0) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.0) + '@babel/plugin-syntax-import-assertions': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.29.0) + '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-async-generator-functions': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-transform-async-to-generator': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-block-scoped-functions': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-block-scoping': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-class-properties': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-class-static-block': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-classes': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-computed-properties': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.0) + '@babel/plugin-transform-dotall-regex': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-duplicate-keys': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-explicit-resource-management': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-exponentiation-operator': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-export-namespace-from': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-function-name': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-json-strings': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-literals': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-logical-assignment-operators': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-member-expression-literals': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-modules-amd': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-modules-systemjs': 7.29.4(@babel/core@7.29.0) + '@babel/plugin-transform-modules-umd': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-named-capturing-groups-regex': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-transform-new-target': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-nullish-coalescing-operator': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-numeric-separator': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-object-rest-spread': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-object-super': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-optional-catch-binding': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-optional-chaining': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.29.0) + '@babel/plugin-transform-private-methods': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-private-property-in-object': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-property-literals': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-regenerator': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-transform-regexp-modifiers': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-reserved-words': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-spread': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-sticky-regex': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-template-literals': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-typeof-symbol': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-unicode-escapes': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-unicode-property-regex': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-unicode-sets-regex': 7.28.6(@babel/core@7.29.0) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.29.0) + babel-plugin-polyfill-corejs2: 0.4.17(@babel/core@7.29.0) + babel-plugin-polyfill-corejs3: 0.14.2(@babel/core@7.29.0) + babel-plugin-polyfill-regenerator: 0.6.8(@babel/core@7.29.0) + core-js-compat: 3.49.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/types': 7.29.0 + esutils: 2.0.3 + + '@babel/preset-react@7.28.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-validator-option': 7.27.1 + '@babel/plugin-transform-react-display-name': 7.28.0(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx-development': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-react-pure-annotations': 7.27.1(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/preset-typescript@7.28.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-validator-option': 7.27.1 + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/runtime@7.29.2': {} + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.3 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@bcoe/v8-coverage@0.2.3': {} + + '@cacheable/memory@2.0.9': + dependencies: + '@cacheable/utils': 2.4.1 + '@keyv/bigmap': 1.3.1(keyv@5.6.0) + hookified: 1.15.1 + keyv: 5.6.0 + + '@cacheable/utils@2.4.1': + dependencies: + hashery: 1.5.1 + keyv: 5.6.0 + + '@csstools/cascade-layer-name-parser@1.0.13(@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1))(@csstools/css-tokenizer@2.4.1)': + dependencies: + '@csstools/css-parser-algorithms': 2.7.1(@csstools/css-tokenizer@2.4.1) + '@csstools/css-tokenizer': 2.4.1 + + '@csstools/color-helpers@4.2.1': {} + + '@csstools/css-calc@1.2.4(@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1))(@csstools/css-tokenizer@2.4.1)': + dependencies: + '@csstools/css-parser-algorithms': 2.7.1(@csstools/css-tokenizer@2.4.1) + '@csstools/css-tokenizer': 2.4.1 + + '@csstools/css-color-parser@2.0.5(@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1))(@csstools/css-tokenizer@2.4.1)': + dependencies: + '@csstools/color-helpers': 4.2.1 + '@csstools/css-calc': 1.2.4(@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1))(@csstools/css-tokenizer@2.4.1) + '@csstools/css-parser-algorithms': 2.7.1(@csstools/css-tokenizer@2.4.1) + '@csstools/css-tokenizer': 2.4.1 + + '@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1)': + dependencies: + '@csstools/css-tokenizer': 2.4.1 + + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-syntax-patches-for-csstree@1.1.4(css-tree@3.2.1)': + optionalDependencies: + css-tree: 3.2.1 + + '@csstools/css-tokenizer@2.4.1': {} + + '@csstools/css-tokenizer@3.0.4': {} + + '@csstools/media-query-list-parser@2.1.13(@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1))(@csstools/css-tokenizer@2.4.1)': + dependencies: + '@csstools/css-parser-algorithms': 2.7.1(@csstools/css-tokenizer@2.4.1) + '@csstools/css-tokenizer': 2.4.1 + + '@csstools/media-query-list-parser@4.0.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/postcss-cascade-layers@4.0.6(postcss@8.5.14)': + dependencies: + '@csstools/selector-specificity': 3.1.1(postcss-selector-parser@6.1.2) + postcss: 8.5.14 + postcss-selector-parser: 6.1.2 + + '@csstools/postcss-color-function@3.0.19(postcss@8.5.14)': + dependencies: + '@csstools/css-color-parser': 2.0.5(@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1))(@csstools/css-tokenizer@2.4.1) + '@csstools/css-parser-algorithms': 2.7.1(@csstools/css-tokenizer@2.4.1) + '@csstools/css-tokenizer': 2.4.1 + '@csstools/postcss-progressive-custom-properties': 3.3.0(postcss@8.5.14) + '@csstools/utilities': 1.0.0(postcss@8.5.14) + postcss: 8.5.14 + + '@csstools/postcss-color-mix-function@2.0.19(postcss@8.5.14)': + dependencies: + '@csstools/css-color-parser': 2.0.5(@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1))(@csstools/css-tokenizer@2.4.1) + '@csstools/css-parser-algorithms': 2.7.1(@csstools/css-tokenizer@2.4.1) + '@csstools/css-tokenizer': 2.4.1 + '@csstools/postcss-progressive-custom-properties': 3.3.0(postcss@8.5.14) + '@csstools/utilities': 1.0.0(postcss@8.5.14) + postcss: 8.5.14 + + '@csstools/postcss-content-alt-text@1.0.0(postcss@8.5.14)': + dependencies: + '@csstools/css-parser-algorithms': 2.7.1(@csstools/css-tokenizer@2.4.1) + '@csstools/css-tokenizer': 2.4.1 + '@csstools/postcss-progressive-custom-properties': 3.3.0(postcss@8.5.14) + '@csstools/utilities': 1.0.0(postcss@8.5.14) + postcss: 8.5.14 + + '@csstools/postcss-exponential-functions@1.0.9(postcss@8.5.14)': + dependencies: + '@csstools/css-calc': 1.2.4(@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1))(@csstools/css-tokenizer@2.4.1) + '@csstools/css-parser-algorithms': 2.7.1(@csstools/css-tokenizer@2.4.1) + '@csstools/css-tokenizer': 2.4.1 + postcss: 8.5.14 + + '@csstools/postcss-font-format-keywords@3.0.2(postcss@8.5.14)': + dependencies: + '@csstools/utilities': 1.0.0(postcss@8.5.14) + postcss: 8.5.14 + postcss-value-parser: 4.2.0 + + '@csstools/postcss-gamut-mapping@1.0.11(postcss@8.5.14)': + dependencies: + '@csstools/css-color-parser': 2.0.5(@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1))(@csstools/css-tokenizer@2.4.1) + '@csstools/css-parser-algorithms': 2.7.1(@csstools/css-tokenizer@2.4.1) + '@csstools/css-tokenizer': 2.4.1 + postcss: 8.5.14 + + '@csstools/postcss-gradients-interpolation-method@4.0.20(postcss@8.5.14)': + dependencies: + '@csstools/css-color-parser': 2.0.5(@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1))(@csstools/css-tokenizer@2.4.1) + '@csstools/css-parser-algorithms': 2.7.1(@csstools/css-tokenizer@2.4.1) + '@csstools/css-tokenizer': 2.4.1 + '@csstools/postcss-progressive-custom-properties': 3.3.0(postcss@8.5.14) + '@csstools/utilities': 1.0.0(postcss@8.5.14) + postcss: 8.5.14 + + '@csstools/postcss-hwb-function@3.0.18(postcss@8.5.14)': + dependencies: + '@csstools/css-color-parser': 2.0.5(@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1))(@csstools/css-tokenizer@2.4.1) + '@csstools/css-parser-algorithms': 2.7.1(@csstools/css-tokenizer@2.4.1) + '@csstools/css-tokenizer': 2.4.1 + '@csstools/postcss-progressive-custom-properties': 3.3.0(postcss@8.5.14) + '@csstools/utilities': 1.0.0(postcss@8.5.14) + postcss: 8.5.14 + + '@csstools/postcss-ic-unit@3.0.7(postcss@8.5.14)': + dependencies: + '@csstools/postcss-progressive-custom-properties': 3.3.0(postcss@8.5.14) + '@csstools/utilities': 1.0.0(postcss@8.5.14) + postcss: 8.5.14 + postcss-value-parser: 4.2.0 + + '@csstools/postcss-initial@1.0.1(postcss@8.5.14)': + dependencies: + postcss: 8.5.14 + + '@csstools/postcss-is-pseudo-class@4.0.8(postcss@8.5.14)': + dependencies: + '@csstools/selector-specificity': 3.1.1(postcss-selector-parser@6.1.2) + postcss: 8.5.14 + postcss-selector-parser: 6.1.2 + + '@csstools/postcss-light-dark-function@1.0.8(postcss@8.5.14)': + dependencies: + '@csstools/css-parser-algorithms': 2.7.1(@csstools/css-tokenizer@2.4.1) + '@csstools/css-tokenizer': 2.4.1 + '@csstools/postcss-progressive-custom-properties': 3.3.0(postcss@8.5.14) + '@csstools/utilities': 1.0.0(postcss@8.5.14) + postcss: 8.5.14 + + '@csstools/postcss-logical-float-and-clear@2.0.1(postcss@8.5.14)': + dependencies: + postcss: 8.5.14 + + '@csstools/postcss-logical-overflow@1.0.1(postcss@8.5.14)': + dependencies: + postcss: 8.5.14 + + '@csstools/postcss-logical-overscroll-behavior@1.0.1(postcss@8.5.14)': + dependencies: + postcss: 8.5.14 + + '@csstools/postcss-logical-resize@2.0.1(postcss@8.5.14)': + dependencies: + postcss: 8.5.14 + postcss-value-parser: 4.2.0 + + '@csstools/postcss-logical-viewport-units@2.0.11(postcss@8.5.14)': + dependencies: + '@csstools/css-tokenizer': 2.4.1 + '@csstools/utilities': 1.0.0(postcss@8.5.14) + postcss: 8.5.14 + + '@csstools/postcss-media-minmax@1.1.8(postcss@8.5.14)': + dependencies: + '@csstools/css-calc': 1.2.4(@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1))(@csstools/css-tokenizer@2.4.1) + '@csstools/css-parser-algorithms': 2.7.1(@csstools/css-tokenizer@2.4.1) + '@csstools/css-tokenizer': 2.4.1 + '@csstools/media-query-list-parser': 2.1.13(@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1))(@csstools/css-tokenizer@2.4.1) + postcss: 8.5.14 + + '@csstools/postcss-media-queries-aspect-ratio-number-values@2.0.11(postcss@8.5.14)': + dependencies: + '@csstools/css-parser-algorithms': 2.7.1(@csstools/css-tokenizer@2.4.1) + '@csstools/css-tokenizer': 2.4.1 + '@csstools/media-query-list-parser': 2.1.13(@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1))(@csstools/css-tokenizer@2.4.1) + postcss: 8.5.14 + + '@csstools/postcss-nested-calc@3.0.2(postcss@8.5.14)': + dependencies: + '@csstools/utilities': 1.0.0(postcss@8.5.14) + postcss: 8.5.14 + postcss-value-parser: 4.2.0 + + '@csstools/postcss-normalize-display-values@3.0.2(postcss@8.5.14)': + dependencies: + postcss: 8.5.14 + postcss-value-parser: 4.2.0 + + '@csstools/postcss-oklab-function@3.0.19(postcss@8.5.14)': + dependencies: + '@csstools/css-color-parser': 2.0.5(@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1))(@csstools/css-tokenizer@2.4.1) + '@csstools/css-parser-algorithms': 2.7.1(@csstools/css-tokenizer@2.4.1) + '@csstools/css-tokenizer': 2.4.1 + '@csstools/postcss-progressive-custom-properties': 3.3.0(postcss@8.5.14) + '@csstools/utilities': 1.0.0(postcss@8.5.14) + postcss: 8.5.14 + + '@csstools/postcss-progressive-custom-properties@3.3.0(postcss@8.5.14)': + dependencies: + postcss: 8.5.14 + postcss-value-parser: 4.2.0 + + '@csstools/postcss-relative-color-syntax@2.0.19(postcss@8.5.14)': + dependencies: + '@csstools/css-color-parser': 2.0.5(@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1))(@csstools/css-tokenizer@2.4.1) + '@csstools/css-parser-algorithms': 2.7.1(@csstools/css-tokenizer@2.4.1) + '@csstools/css-tokenizer': 2.4.1 + '@csstools/postcss-progressive-custom-properties': 3.3.0(postcss@8.5.14) + '@csstools/utilities': 1.0.0(postcss@8.5.14) + postcss: 8.5.14 + + '@csstools/postcss-scope-pseudo-class@3.0.1(postcss@8.5.14)': + dependencies: + postcss: 8.5.14 + postcss-selector-parser: 6.1.2 + + '@csstools/postcss-stepped-value-functions@3.0.10(postcss@8.5.14)': + dependencies: + '@csstools/css-calc': 1.2.4(@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1))(@csstools/css-tokenizer@2.4.1) + '@csstools/css-parser-algorithms': 2.7.1(@csstools/css-tokenizer@2.4.1) + '@csstools/css-tokenizer': 2.4.1 + postcss: 8.5.14 + + '@csstools/postcss-text-decoration-shorthand@3.0.7(postcss@8.5.14)': + dependencies: + '@csstools/color-helpers': 4.2.1 + postcss: 8.5.14 + postcss-value-parser: 4.2.0 + + '@csstools/postcss-trigonometric-functions@3.0.10(postcss@8.5.14)': + dependencies: + '@csstools/css-calc': 1.2.4(@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1))(@csstools/css-tokenizer@2.4.1) + '@csstools/css-parser-algorithms': 2.7.1(@csstools/css-tokenizer@2.4.1) + '@csstools/css-tokenizer': 2.4.1 + postcss: 8.5.14 + + '@csstools/postcss-unset-value@3.0.1(postcss@8.5.14)': + dependencies: + postcss: 8.5.14 + + '@csstools/selector-resolve-nested@1.1.0(postcss-selector-parser@6.1.2)': + dependencies: + postcss-selector-parser: 6.1.2 + + '@csstools/selector-specificity@3.1.1(postcss-selector-parser@6.1.2)': + dependencies: + postcss-selector-parser: 6.1.2 + + '@csstools/selector-specificity@5.0.0(postcss-selector-parser@7.1.1)': + dependencies: + postcss-selector-parser: 7.1.1 + + '@csstools/utilities@1.0.0(postcss@8.5.14)': + dependencies: + postcss: 8.5.14 + + '@discoveryjs/json-ext@0.5.7': {} + + '@dual-bundle/import-meta-resolve@4.2.1': {} + + '@emotion/babel-plugin@11.13.5': + dependencies: + '@babel/helper-module-imports': 7.28.6 + '@babel/runtime': 7.29.2 + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/serialize': 1.3.3 + babel-plugin-macros: 3.1.0 + convert-source-map: 1.9.0 + escape-string-regexp: 4.0.0 + find-root: 1.1.0 + source-map: 0.5.7 + stylis: 4.2.0 + transitivePeerDependencies: + - supports-color + + '@emotion/cache@11.14.0': + dependencies: + '@emotion/memoize': 0.9.0 + '@emotion/sheet': 1.4.0 + '@emotion/utils': 1.4.2 + '@emotion/weak-memoize': 0.4.0 + stylis: 4.2.0 + + '@emotion/hash@0.9.2': {} + + '@emotion/is-prop-valid@1.4.0': + dependencies: + '@emotion/memoize': 0.9.0 + + '@emotion/memoize@0.9.0': {} + + '@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.29.2 + '@emotion/babel-plugin': 11.13.5 + '@emotion/cache': 11.14.0 + '@emotion/serialize': 1.3.3 + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@18.3.1) + '@emotion/utils': 1.4.2 + '@emotion/weak-memoize': 0.4.0 + hoist-non-react-statics: 3.3.2 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + transitivePeerDependencies: + - supports-color + + '@emotion/serialize@1.3.3': + dependencies: + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/unitless': 0.10.0 + '@emotion/utils': 1.4.2 + csstype: 3.2.3 + + '@emotion/sheet@1.4.0': {} + + '@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.29.2 + '@emotion/babel-plugin': 11.13.5 + '@emotion/is-prop-valid': 1.4.0 + '@emotion/react': 11.14.0(@types/react@18.3.28)(react@18.3.1) + '@emotion/serialize': 1.3.3 + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@18.3.1) + '@emotion/utils': 1.4.2 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + transitivePeerDependencies: + - supports-color + + '@emotion/unitless@0.10.0': {} + + '@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@18.3.1)': + dependencies: + react: 18.3.1 + + '@emotion/utils@1.4.2': {} + + '@emotion/weak-memoize@0.4.0': {} + + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.7.0))': + dependencies: + eslint: 9.39.4(jiti@2.7.0) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.2': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.5': + dependencies: + ajv: 6.15.0 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.39.4': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + + '@floating-ui/core@1.7.5': + dependencies: + '@floating-ui/utils': 0.2.11 + + '@floating-ui/dom@1.7.6': + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 + + '@floating-ui/utils@0.2.11': {} + + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@istanbuljs/load-nyc-config@1.1.0': + dependencies: + camelcase: 5.3.1 + find-up: 4.1.0 + get-package-type: 0.1.0 + js-yaml: 3.14.2 + resolve-from: 5.0.0 + + '@istanbuljs/schema@0.1.6': {} + + '@jest/console@29.7.0': + dependencies: + '@jest/types': 29.6.3 + '@types/node': 20.19.41 + chalk: 4.1.2 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + + '@jest/core@29.7.0(babel-plugin-macros@3.1.0)': + dependencies: + '@jest/console': 29.7.0 + '@jest/reporters': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.19.41 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.9.0 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 29.7.0 + jest-config: 29.7.0(@types/node@20.19.41)(babel-plugin-macros@3.1.0) + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-resolve-dependencies: 29.7.0 + jest-runner: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + jest-watcher: 29.7.0 + micromatch: 4.0.8 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - ts-node + + '@jest/environment@29.7.0': + dependencies: + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.19.41 + jest-mock: 29.7.0 + + '@jest/expect-utils@29.7.0': + dependencies: + jest-get-type: 29.6.3 + + '@jest/expect@29.7.0': + dependencies: + expect: 29.7.0 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + + '@jest/fake-timers@29.7.0': + dependencies: + '@jest/types': 29.6.3 + '@sinonjs/fake-timers': 10.3.0 + '@types/node': 20.19.41 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-util: 29.7.0 + + '@jest/globals@29.7.0': + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/types': 29.6.3 + jest-mock: 29.7.0 + transitivePeerDependencies: + - supports-color + + '@jest/reporters@29.7.0': + dependencies: + '@bcoe/v8-coverage': 0.2.3 + '@jest/console': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.31 + '@types/node': 20.19.41 + chalk: 4.1.2 + collect-v8-coverage: 1.0.3 + exit: 0.1.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-instrument: 6.0.3 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 4.0.1 + istanbul-reports: 3.2.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + jest-worker: 29.7.0 + slash: 3.0.0 + string-length: 4.0.2 + strip-ansi: 6.0.1 + v8-to-istanbul: 9.3.0 + transitivePeerDependencies: + - supports-color + + '@jest/schemas@29.6.3': + dependencies: + '@sinclair/typebox': 0.27.10 + + '@jest/source-map@29.6.3': + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + callsites: 3.1.0 + graceful-fs: 4.2.11 + + '@jest/test-result@29.7.0': + dependencies: + '@jest/console': 29.7.0 + '@jest/types': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + collect-v8-coverage: 1.0.3 + + '@jest/test-sequencer@29.7.0': + dependencies: + '@jest/test-result': 29.7.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + slash: 3.0.0 + + '@jest/transform@29.7.0': + dependencies: + '@babel/core': 7.29.0 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.31 + babel-plugin-istanbul: 6.1.1 + chalk: 4.1.2 + convert-source-map: 2.0.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + micromatch: 4.0.8 + pirates: 4.0.7 + slash: 3.0.0 + write-file-atomic: 4.0.2 + transitivePeerDependencies: + - supports-color + + '@jest/types@29.6.3': + dependencies: + '@jest/schemas': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 20.19.41 + '@types/yargs': 17.0.35 + chalk: 4.1.2 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/source-map@0.3.11': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@keyv/bigmap@1.3.1(keyv@5.6.0)': + dependencies: + hashery: 1.5.1 + hookified: 1.15.1 + keyv: 5.6.0 + + '@keyv/serialize@1.1.1': {} + + '@kurkle/color@0.3.4': {} + + '@leichtgewicht/ip-codec@2.0.5': {} + + '@mui/core-downloads-tracker@5.18.0': {} + + '@mui/material@5.18.0(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.29.2 + '@mui/core-downloads-tracker': 5.18.0 + '@mui/system': 5.18.0(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1) + '@mui/types': 7.2.24(@types/react@18.3.28) + '@mui/utils': 5.17.1(@types/react@18.3.28)(react@18.3.1) + '@popperjs/core': 2.11.8 + '@types/react-transition-group': 4.4.12(@types/react@18.3.28) + clsx: 2.1.1 + csstype: 3.2.3 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-is: 19.2.6 + react-transition-group: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + optionalDependencies: + '@emotion/react': 11.14.0(@types/react@18.3.28)(react@18.3.1) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1) + '@types/react': 18.3.28 + + '@mui/private-theming@5.17.1(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.29.2 + '@mui/utils': 5.17.1(@types/react@18.3.28)(react@18.3.1) + prop-types: 15.8.1 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@mui/styled-engine@5.18.0(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.29.2 + '@emotion/cache': 11.14.0 + '@emotion/serialize': 1.3.3 + csstype: 3.2.3 + prop-types: 15.8.1 + react: 18.3.1 + optionalDependencies: + '@emotion/react': 11.14.0(@types/react@18.3.28)(react@18.3.1) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1) + + '@mui/system@5.18.0(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.29.2 + '@mui/private-theming': 5.17.1(@types/react@18.3.28)(react@18.3.1) + '@mui/styled-engine': 5.18.0(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(react@18.3.1) + '@mui/types': 7.2.24(@types/react@18.3.28) + '@mui/utils': 5.17.1(@types/react@18.3.28)(react@18.3.1) + clsx: 2.1.1 + csstype: 3.2.3 + prop-types: 15.8.1 + react: 18.3.1 + optionalDependencies: + '@emotion/react': 11.14.0(@types/react@18.3.28)(react@18.3.1) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1) + '@types/react': 18.3.28 + + '@mui/types@7.2.24(@types/react@18.3.28)': + optionalDependencies: + '@types/react': 18.3.28 + + '@mui/utils@5.17.1(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.29.2 + '@mui/types': 7.2.24(@types/react@18.3.28) + '@types/prop-types': 15.7.15 + clsx: 2.1.1 + prop-types: 15.8.1 + react: 18.3.1 + react-is: 19.2.6 + optionalDependencies: + '@types/react': 18.3.28 + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@parcel/watcher-android-arm64@2.5.6': + optional: true + + '@parcel/watcher-darwin-arm64@2.5.6': + optional: true + + '@parcel/watcher-darwin-x64@2.5.6': + optional: true + + '@parcel/watcher-freebsd-x64@2.5.6': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.5.6': + optional: true + + '@parcel/watcher-linux-arm-musl@2.5.6': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.5.6': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.5.6': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.5.6': + optional: true + + '@parcel/watcher-linux-x64-musl@2.5.6': + optional: true + + '@parcel/watcher-win32-arm64@2.5.6': + optional: true + + '@parcel/watcher-win32-ia32@2.5.6': + optional: true + + '@parcel/watcher-win32-x64@2.5.6': + optional: true + + '@parcel/watcher@2.5.6': + dependencies: + detect-libc: 2.1.2 + is-glob: 4.0.3 + node-addon-api: 7.1.1 + picomatch: 4.0.4 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.6 + '@parcel/watcher-darwin-arm64': 2.5.6 + '@parcel/watcher-darwin-x64': 2.5.6 + '@parcel/watcher-freebsd-x64': 2.5.6 + '@parcel/watcher-linux-arm-glibc': 2.5.6 + '@parcel/watcher-linux-arm-musl': 2.5.6 + '@parcel/watcher-linux-arm64-glibc': 2.5.6 + '@parcel/watcher-linux-arm64-musl': 2.5.6 + '@parcel/watcher-linux-x64-glibc': 2.5.6 + '@parcel/watcher-linux-x64-musl': 2.5.6 + '@parcel/watcher-win32-arm64': 2.5.6 + '@parcel/watcher-win32-ia32': 2.5.6 + '@parcel/watcher-win32-x64': 2.5.6 + optional: true + + '@pkgr/core@0.2.9': {} + + '@polka/url@1.0.0-next.29': {} + + '@popperjs/core@2.11.8': {} + + '@react-dnd/asap@5.0.2': {} + + '@react-dnd/invariant@4.0.2': {} + + '@react-dnd/shallowequal@4.0.2': {} + + '@reduxjs/toolkit@2.12.0(react-redux@9.3.0(@types/react@18.3.28)(react@18.3.1)(redux@5.0.1))(react@18.3.1)': + dependencies: + '@standard-schema/spec': 1.1.0 + '@standard-schema/utils': 0.3.0 + immer: 11.1.8 + redux: 5.0.1 + redux-thunk: 3.1.0(redux@5.0.1) + reselect: 5.2.0 + optionalDependencies: + react: 18.3.1 + react-redux: 9.3.0(@types/react@18.3.28)(react@18.3.1)(redux@5.0.1) + + '@remix-run/router@1.23.2': {} + + '@sinclair/typebox@0.27.10': {} + + '@sinonjs/commons@3.0.1': + dependencies: + type-detect: 4.0.8 + + '@sinonjs/fake-timers@10.3.0': + dependencies: + '@sinonjs/commons': 3.0.1 + + '@standard-schema/spec@1.1.0': {} + + '@standard-schema/utils@0.3.0': {} + + '@tanstack/query-core@5.100.11': {} + + '@tanstack/react-query@5.100.11(react@18.3.1)': + dependencies: + '@tanstack/query-core': 5.100.11 + react: 18.3.1 + + '@tanstack/react-table@8.21.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@tanstack/table-core': 8.21.3 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@tanstack/table-core@8.21.3': {} + + '@testing-library/dom@9.3.4': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/runtime': 7.29.2 + '@types/aria-query': 5.0.4 + aria-query: 5.1.3 + chalk: 4.1.2 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + pretty-format: 27.5.1 + + '@testing-library/jest-dom@6.9.1': + dependencies: + '@adobe/css-tools': 4.4.4 + aria-query: 5.3.2 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + picocolors: 1.1.1 + redent: 3.0.0 + + '@testing-library/react@14.3.1(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.29.2 + '@testing-library/dom': 9.3.4 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + + '@tootallnate/once@2.0.1': {} + + '@types/aria-query@5.0.4': {} + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/body-parser@1.19.6': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 20.19.41 + + '@types/bonjour@3.5.13': + dependencies: + '@types/node': 20.19.41 + + '@types/connect-history-api-fallback@1.5.4': + dependencies: + '@types/express-serve-static-core': 5.1.1 + '@types/node': 20.19.41 + + '@types/connect@3.4.38': + dependencies: + '@types/node': 20.19.41 + + '@types/d3-array@3.2.2': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.1': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-shape@3.1.8': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + + '@types/eslint-scope@3.7.7': + dependencies: + '@types/eslint': 9.6.1 + '@types/estree': 1.0.9 + + '@types/eslint@9.6.1': + dependencies: + '@types/estree': 1.0.9 + '@types/json-schema': 7.0.15 + + '@types/estree@1.0.9': {} + + '@types/express-serve-static-core@4.19.8': + dependencies: + '@types/node': 20.19.41 + '@types/qs': 6.15.1 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.1 + + '@types/express-serve-static-core@5.1.1': + dependencies: + '@types/node': 20.19.41 + '@types/qs': 6.15.1 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.1 + + '@types/express@4.17.25': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 4.19.8 + '@types/qs': 6.15.1 + '@types/serve-static': 1.15.10 + + '@types/glob@7.2.0': + dependencies: + '@types/minimatch': 6.0.0 + '@types/node': 20.19.41 + + '@types/graceful-fs@4.1.9': + dependencies: + '@types/node': 20.19.41 + + '@types/hoist-non-react-statics@3.3.7(@types/react@18.3.28)': + dependencies: + '@types/react': 18.3.28 + hoist-non-react-statics: 3.3.2 + + '@types/html-minifier-terser@6.1.0': {} + + '@types/http-errors@2.0.5': {} + + '@types/http-proxy@1.17.17': + dependencies: + '@types/node': 20.19.41 + + '@types/istanbul-lib-coverage@2.0.6': {} + + '@types/istanbul-lib-report@3.0.3': + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + + '@types/istanbul-reports@3.0.4': + dependencies: + '@types/istanbul-lib-report': 3.0.3 + + '@types/jsdom@20.0.1': + dependencies: + '@types/node': 20.19.41 + '@types/tough-cookie': 4.0.5 + parse5: 7.3.0 + + '@types/json-schema@7.0.15': {} + + '@types/mime@1.3.5': {} + + '@types/minimatch@6.0.0': + dependencies: + minimatch: 10.2.5 + + '@types/node-forge@1.3.14': + dependencies: + '@types/node': 20.19.41 + + '@types/node@20.19.41': + dependencies: + undici-types: 6.21.0 + + '@types/parse-json@4.0.2': {} + + '@types/prop-types@15.7.15': {} + + '@types/qs@6.15.1': {} + + '@types/range-parser@1.2.7': {} + + '@types/react-dom@18.3.7(@types/react@18.3.28)': + dependencies: + '@types/react': 18.3.28 + + '@types/react-transition-group@4.4.12(@types/react@18.3.28)': + dependencies: + '@types/react': 18.3.28 + + '@types/react@18.3.28': + dependencies: + '@types/prop-types': 15.7.15 + csstype: 3.2.3 + + '@types/retry@0.12.0': {} + + '@types/send@0.17.6': + dependencies: + '@types/mime': 1.3.5 + '@types/node': 20.19.41 + + '@types/send@1.2.1': + dependencies: + '@types/node': 20.19.41 + + '@types/serve-index@1.9.4': + dependencies: + '@types/express': 4.17.25 + + '@types/serve-static@1.15.10': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 20.19.41 + '@types/send': 0.17.6 + + '@types/sockjs@0.3.36': + dependencies: + '@types/node': 20.19.41 + + '@types/stack-utils@2.0.3': {} + + '@types/tough-cookie@4.0.5': {} + + '@types/use-sync-external-store@0.0.6': {} + + '@types/ws@8.18.1': + dependencies: + '@types/node': 20.19.41 + + '@types/yargs-parser@21.0.3': {} + + '@types/yargs@17.0.35': + dependencies: + '@types/yargs-parser': 21.0.3 + + '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 7.18.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 7.18.0 + '@typescript-eslint/type-utils': 7.18.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/utils': 7.18.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 7.18.0 + eslint: 9.39.4(jiti@2.7.0) + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare: 1.4.0 + ts-api-utils: 1.4.3(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@7.18.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 7.18.0 + '@typescript-eslint/types': 7.18.0 + '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 7.18.0 + debug: 4.4.3 + eslint: 9.39.4(jiti@2.7.0) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@7.18.0': + dependencies: + '@typescript-eslint/types': 7.18.0 + '@typescript-eslint/visitor-keys': 7.18.0 + + '@typescript-eslint/type-utils@7.18.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.9.3) + '@typescript-eslint/utils': 7.18.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + debug: 4.4.3 + eslint: 9.39.4(jiti@2.7.0) + ts-api-utils: 1.4.3(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@7.18.0': {} + + '@typescript-eslint/typescript-estree@7.18.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 7.18.0 + '@typescript-eslint/visitor-keys': 7.18.0 + debug: 4.4.3 + globby: 11.1.0 + is-glob: 4.0.3 + minimatch: 9.0.9 + semver: 7.8.0 + ts-api-utils: 1.4.3(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@7.18.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.7.0)) + '@typescript-eslint/scope-manager': 7.18.0 + '@typescript-eslint/types': 7.18.0 + '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.9.3) + eslint: 9.39.4(jiti@2.7.0) + transitivePeerDependencies: + - supports-color + - typescript + + '@typescript-eslint/visitor-keys@7.18.0': + dependencies: + '@typescript-eslint/types': 7.18.0 + eslint-visitor-keys: 3.4.3 + + '@webassemblyjs/ast@1.14.1': + dependencies: + '@webassemblyjs/helper-numbers': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + + '@webassemblyjs/floating-point-hex-parser@1.13.2': {} + + '@webassemblyjs/helper-api-error@1.13.2': {} + + '@webassemblyjs/helper-buffer@1.14.1': {} + + '@webassemblyjs/helper-numbers@1.13.2': + dependencies: + '@webassemblyjs/floating-point-hex-parser': 1.13.2 + '@webassemblyjs/helper-api-error': 1.13.2 + '@xtuc/long': 4.2.2 + + '@webassemblyjs/helper-wasm-bytecode@1.13.2': {} + + '@webassemblyjs/helper-wasm-section@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/wasm-gen': 1.14.1 + + '@webassemblyjs/ieee754@1.13.2': + dependencies: + '@xtuc/ieee754': 1.2.0 + + '@webassemblyjs/leb128@1.13.2': + dependencies: + '@xtuc/long': 4.2.2 + + '@webassemblyjs/utf8@1.13.2': {} + + '@webassemblyjs/wasm-edit@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/helper-wasm-section': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-opt': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + '@webassemblyjs/wast-printer': 1.14.1 + + '@webassemblyjs/wasm-gen@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + + '@webassemblyjs/wasm-opt@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + + '@webassemblyjs/wasm-parser@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-api-error': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + + '@webassemblyjs/wast-printer@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@xtuc/long': 4.2.2 + + '@webpack-cli/configtest@2.1.1(webpack-cli@5.1.4)(webpack@5.106.2)': + dependencies: + webpack: 5.106.2(cssnano@6.1.2(postcss@8.5.14))(postcss@8.5.14)(webpack-cli@5.1.4) + webpack-cli: 5.1.4(webpack-bundle-analyzer@4.10.2)(webpack-dev-server@4.15.2)(webpack@5.106.2) + + '@webpack-cli/info@2.0.2(webpack-cli@5.1.4)(webpack@5.106.2)': + dependencies: + webpack: 5.106.2(cssnano@6.1.2(postcss@8.5.14))(postcss@8.5.14)(webpack-cli@5.1.4) + webpack-cli: 5.1.4(webpack-bundle-analyzer@4.10.2)(webpack-dev-server@4.15.2)(webpack@5.106.2) + + '@webpack-cli/serve@2.0.5(webpack-cli@5.1.4)(webpack-dev-server@4.15.2)(webpack@5.106.2)': + dependencies: + webpack: 5.106.2(cssnano@6.1.2(postcss@8.5.14))(postcss@8.5.14)(webpack-cli@5.1.4) + webpack-cli: 5.1.4(webpack-bundle-analyzer@4.10.2)(webpack-dev-server@4.15.2)(webpack@5.106.2) + optionalDependencies: + webpack-dev-server: 4.15.2(webpack-cli@5.1.4)(webpack@5.106.2) + + '@xtuc/ieee754@1.2.0': {} + + '@xtuc/long@4.2.2': {} + + abab@2.0.6: {} + + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + + acorn-globals@7.0.1: + dependencies: + acorn: 8.16.0 + acorn-walk: 8.3.5 + + acorn-import-phases@1.0.4(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn-walk@8.3.5: + dependencies: + acorn: 8.16.0 + + acorn@8.16.0: {} + + agent-base@6.0.2: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + ajv-formats@2.1.1(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + + ajv-keywords@5.1.0(ajv@8.20.0): + dependencies: + ajv: 8.20.0 + fast-deep-equal: 3.1.3 + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.2 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + + ansi-escapes@7.3.0: + dependencies: + environment: 1.1.0 + + ansi-html-community@0.0.8: {} + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@5.2.0: {} + + ansi-styles@6.2.3: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.2 + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + argparse@2.0.1: {} + + aria-query@5.1.3: + dependencies: + deep-equal: 2.2.3 + + aria-query@5.3.2: {} + + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + + array-flatten@1.1.1: {} + + array-union@1.0.2: + dependencies: + array-uniq: 1.0.3 + + array-union@2.1.0: {} + + array-uniq@1.0.3: {} + + astral-regex@2.0.0: {} + + asynckit@0.4.0: {} + + attr-accept@2.2.5: {} + + autoprefixer@10.5.0(postcss@8.5.14): + dependencies: + browserslist: 4.28.2 + caniuse-lite: 1.0.30001793 + fraction.js: 5.3.4 + picocolors: 1.1.1 + postcss: 8.5.14 + postcss-value-parser: 4.2.0 + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + axios@1.16.1: + dependencies: + follow-redirects: 1.16.0 + form-data: 4.0.5 + https-proxy-agent: 5.0.1 + proxy-from-env: 2.1.0 + transitivePeerDependencies: + - debug + - supports-color + + babel-jest@29.7.0(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + '@jest/transform': 29.7.0 + '@types/babel__core': 7.20.5 + babel-plugin-istanbul: 6.1.1 + babel-preset-jest: 29.6.3(@babel/core@7.29.0) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-istanbul@6.1.1: + dependencies: + '@babel/helper-plugin-utils': 7.28.6 + '@istanbuljs/load-nyc-config': 1.1.0 + '@istanbuljs/schema': 0.1.6 + istanbul-lib-instrument: 5.2.1 + test-exclude: 6.0.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-jest-hoist@29.6.3: + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + '@types/babel__core': 7.20.5 + '@types/babel__traverse': 7.28.0 + + babel-plugin-macros@3.1.0: + dependencies: + '@babel/runtime': 7.29.2 + cosmiconfig: 7.1.0 + resolve: 1.22.12 + + babel-plugin-polyfill-corejs2@0.4.17(@babel/core@7.29.0): + dependencies: + '@babel/compat-data': 7.29.3 + '@babel/core': 7.29.0 + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.0) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-corejs3@0.13.0(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.0) + core-js-compat: 3.49.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-corejs3@0.14.2(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.0) + core-js-compat: 3.49.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-regenerator@0.6.8(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + babel-preset-current-node-syntax@1.2.0(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.29.0) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.29.0) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.29.0) + '@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.0) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.29.0) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.29.0) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.29.0) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.29.0) + + babel-preset-jest@29.6.3(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + babel-plugin-jest-hoist: 29.6.3 + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0) + + balanced-match@1.0.2: {} + + balanced-match@2.0.0: {} + + balanced-match@4.0.4: {} + + baseline-browser-mapping@2.10.31: {} + + batch@0.6.1: {} + + binary-extensions@2.3.0: {} + + body-parser@1.20.5: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.1 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.15.2 + raw-body: 2.5.3 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + bonjour-service@1.3.0: + dependencies: + fast-deep-equal: 3.1.3 + multicast-dns: 7.2.5 + + boolbase@1.0.0: {} + + brace-expansion@1.1.14: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.1.0: + dependencies: + balanced-match: 1.0.2 + + brace-expansion@5.0.6: + dependencies: + balanced-match: 4.0.4 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.31 + caniuse-lite: 1.0.30001793 + electron-to-chromium: 1.5.359 + node-releases: 2.0.44 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + + bser@2.1.1: + dependencies: + node-int64: 0.4.0 + + buffer-from@1.1.2: {} + + bytes@3.1.2: {} + + cacheable@2.3.5: + dependencies: + '@cacheable/memory': 2.0.9 + '@cacheable/utils': 2.4.1 + hookified: 1.15.1 + keyv: 5.6.0 + qified: 0.10.1 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.9: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + camel-case@4.1.2: + dependencies: + pascal-case: 3.1.2 + tslib: 2.8.1 + + camelcase@5.3.1: {} + + camelcase@6.3.0: {} + + caniuse-api@3.0.0: + dependencies: + browserslist: 4.28.2 + caniuse-lite: 1.0.30001793 + lodash.memoize: 4.1.2 + lodash.uniq: 4.5.0 + + caniuse-lite@1.0.30001793: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@5.6.2: {} + + char-regex@1.0.2: {} + + chart.js@4.5.1: + dependencies: + '@kurkle/color': 0.3.4 + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + chrome-trace-event@1.0.4: {} + + ci-info@3.9.0: {} + + cjs-module-lexer@1.4.3: {} + + classnames@2.5.1: {} + + clean-css@5.3.3: + dependencies: + source-map: 0.6.1 + + clean-webpack-plugin@4.0.0(webpack@5.106.2): + dependencies: + del: 4.1.1 + webpack: 5.106.2(cssnano@6.1.2(postcss@8.5.14))(postcss@8.5.14)(webpack-cli@5.1.4) + + cli-cursor@5.0.0: + dependencies: + restore-cursor: 5.1.0 + + cli-truncate@4.0.0: + dependencies: + slice-ansi: 5.0.0 + string-width: 7.2.0 + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clone-deep@4.0.1: + dependencies: + is-plain-object: 2.0.4 + kind-of: 6.0.3 + shallow-clone: 3.0.1 + + clsx@1.2.1: {} + + clsx@2.1.1: {} + + co@4.6.0: {} + + collect-v8-coverage@1.0.3: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + colord@2.9.3: {} + + colorette@2.0.20: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@10.0.1: {} + + commander@13.1.0: {} + + commander@2.20.3: {} + + commander@7.2.0: {} + + commander@8.3.0: {} + + compressible@2.0.18: + dependencies: + mime-db: 1.54.0 + + compression@1.8.1: + dependencies: + bytes: 3.1.2 + compressible: 2.0.18 + debug: 2.6.9 + negotiator: 0.6.4 + on-headers: 1.1.0 + safe-buffer: 5.2.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + concat-map@0.0.1: {} + + connect-history-api-fallback@2.0.0: {} + + content-disposition@0.5.4: + dependencies: + safe-buffer: 5.2.1 + + content-type@1.0.5: {} + + convert-source-map@1.9.0: {} + + convert-source-map@2.0.0: {} + + cookie-signature@1.0.7: {} + + cookie@0.7.2: {} + + copy-anything@3.0.5: + dependencies: + is-what: 4.1.16 + + core-js-compat@3.49.0: + dependencies: + browserslist: 4.28.2 + + core-util-is@1.0.3: {} + + cosmiconfig@7.1.0: + dependencies: + '@types/parse-json': 4.0.2 + import-fresh: 3.3.1 + parse-json: 5.2.0 + path-type: 4.0.0 + yaml: 1.10.3 + + cosmiconfig@9.0.1(typescript@5.9.3): + dependencies: + env-paths: 2.2.1 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + parse-json: 5.2.0 + optionalDependencies: + typescript: 5.9.3 + + create-jest@29.7.0(@types/node@20.19.41)(babel-plugin-macros@3.1.0): + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-config: 29.7.0(@types/node@20.19.41)(babel-plugin-macros@3.1.0) + jest-util: 29.7.0 + prompts: 2.4.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + css-blank-pseudo@6.0.2(postcss@8.5.14): + dependencies: + postcss: 8.5.14 + postcss-selector-parser: 6.1.2 + + css-declaration-sorter@7.4.0(postcss@8.5.14): + dependencies: + postcss: 8.5.14 + + css-functions-list@3.3.3: {} + + css-has-pseudo@6.0.5(postcss@8.5.14): + dependencies: + '@csstools/selector-specificity': 3.1.1(postcss-selector-parser@6.1.2) + postcss: 8.5.14 + postcss-selector-parser: 6.1.2 + postcss-value-parser: 4.2.0 + + css-loader@6.11.0(webpack@5.106.2): + dependencies: + icss-utils: 5.1.0(postcss@8.5.14) + postcss: 8.5.14 + postcss-modules-extract-imports: 3.1.0(postcss@8.5.14) + postcss-modules-local-by-default: 4.2.0(postcss@8.5.14) + postcss-modules-scope: 3.2.1(postcss@8.5.14) + postcss-modules-values: 4.0.0(postcss@8.5.14) + postcss-value-parser: 4.2.0 + semver: 7.8.0 + optionalDependencies: + webpack: 5.106.2(cssnano@6.1.2(postcss@8.5.14))(postcss@8.5.14)(webpack-cli@5.1.4) + + css-prefers-color-scheme@9.0.1(postcss@8.5.14): + dependencies: + postcss: 8.5.14 + + css-select@4.3.0: + dependencies: + boolbase: 1.0.0 + css-what: 6.2.2 + domhandler: 4.3.1 + domutils: 2.8.0 + nth-check: 2.1.1 + + css-select@5.2.2: + dependencies: + boolbase: 1.0.0 + css-what: 6.2.2 + domhandler: 5.0.3 + domutils: 3.2.2 + nth-check: 2.1.1 + + css-tree@2.2.1: + dependencies: + mdn-data: 2.0.28 + source-map-js: 1.2.1 + + css-tree@2.3.1: + dependencies: + mdn-data: 2.0.30 + source-map-js: 1.2.1 + + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.1 + + css-what@6.2.2: {} + + css.escape@1.5.1: {} + + cssdb@8.9.0: {} + + cssesc@3.0.0: {} + + cssnano-preset-default@6.1.2(postcss@8.5.14): + dependencies: + browserslist: 4.28.2 + css-declaration-sorter: 7.4.0(postcss@8.5.14) + cssnano-utils: 4.0.2(postcss@8.5.14) + postcss: 8.5.14 + postcss-calc: 9.0.1(postcss@8.5.14) + postcss-colormin: 6.1.0(postcss@8.5.14) + postcss-convert-values: 6.1.0(postcss@8.5.14) + postcss-discard-comments: 6.0.2(postcss@8.5.14) + postcss-discard-duplicates: 6.0.3(postcss@8.5.14) + postcss-discard-empty: 6.0.3(postcss@8.5.14) + postcss-discard-overridden: 6.0.2(postcss@8.5.14) + postcss-merge-longhand: 6.0.5(postcss@8.5.14) + postcss-merge-rules: 6.1.1(postcss@8.5.14) + postcss-minify-font-values: 6.1.0(postcss@8.5.14) + postcss-minify-gradients: 6.0.3(postcss@8.5.14) + postcss-minify-params: 6.1.0(postcss@8.5.14) + postcss-minify-selectors: 6.0.4(postcss@8.5.14) + postcss-normalize-charset: 6.0.2(postcss@8.5.14) + postcss-normalize-display-values: 6.0.2(postcss@8.5.14) + postcss-normalize-positions: 6.0.2(postcss@8.5.14) + postcss-normalize-repeat-style: 6.0.2(postcss@8.5.14) + postcss-normalize-string: 6.0.2(postcss@8.5.14) + postcss-normalize-timing-functions: 6.0.2(postcss@8.5.14) + postcss-normalize-unicode: 6.1.0(postcss@8.5.14) + postcss-normalize-url: 6.0.2(postcss@8.5.14) + postcss-normalize-whitespace: 6.0.2(postcss@8.5.14) + postcss-ordered-values: 6.0.2(postcss@8.5.14) + postcss-reduce-initial: 6.1.0(postcss@8.5.14) + postcss-reduce-transforms: 6.0.2(postcss@8.5.14) + postcss-svgo: 6.0.3(postcss@8.5.14) + postcss-unique-selectors: 6.0.4(postcss@8.5.14) + + cssnano-utils@4.0.2(postcss@8.5.14): + dependencies: + postcss: 8.5.14 + + cssnano@6.1.2(postcss@8.5.14): + dependencies: + cssnano-preset-default: 6.1.2(postcss@8.5.14) + lilconfig: 3.1.3 + postcss: 8.5.14 + + csso@5.0.5: + dependencies: + css-tree: 2.2.1 + + cssom@0.3.8: {} + + cssom@0.5.0: {} + + cssstyle@2.3.0: + dependencies: + cssom: 0.3.8 + + csstype@3.2.3: {} + + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-color@3.1.0: {} + + d3-ease@3.0.1: {} + + d3-format@3.1.2: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@3.1.0: {} + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.2 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + + data-urls@3.0.2: + dependencies: + abab: 2.0.6 + whatwg-mimetype: 3.0.0 + whatwg-url: 11.0.0 + + date-fns@3.6.0: {} + + debounce@1.2.1: {} + + debug@2.6.9: + dependencies: + ms: 2.0.0 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decimal.js-light@2.5.1: {} + + decimal.js@10.6.0: {} + + dedent@1.7.2(babel-plugin-macros@3.1.0): + optionalDependencies: + babel-plugin-macros: 3.1.0 + + deep-equal@2.2.3: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.9 + es-get-iterator: 1.1.3 + get-intrinsic: 1.3.0 + is-arguments: 1.2.0 + is-array-buffer: 3.0.5 + is-date-object: 1.1.0 + is-regex: 1.2.1 + is-shared-array-buffer: 1.0.4 + isarray: 2.0.5 + object-is: 1.1.6 + object-keys: 1.1.1 + object.assign: 4.1.7 + regexp.prototype.flags: 1.5.4 + side-channel: 1.1.0 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.20 + + deep-is@0.1.4: {} + + deepmerge@2.2.1: {} + + deepmerge@4.3.1: {} + + default-gateway@6.0.3: + dependencies: + execa: 5.1.1 + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-lazy-prop@2.0.0: {} + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + del@4.1.1: + dependencies: + '@types/glob': 7.2.0 + globby: 6.1.0 + is-path-cwd: 2.2.0 + is-path-in-cwd: 2.1.0 + p-map: 2.1.0 + pify: 4.0.1 + rimraf: 2.7.1 + + delayed-stream@1.0.0: {} + + depd@1.1.2: {} + + depd@2.0.0: {} + + destroy@1.2.0: {} + + detect-libc@2.1.2: + optional: true + + detect-newline@3.1.0: {} + + detect-node@2.1.0: {} + + diff-sequences@29.6.3: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + dnd-core@16.0.1: + dependencies: + '@react-dnd/asap': 5.0.2 + '@react-dnd/invariant': 4.0.2 + redux: 4.2.1 + + dns-packet@5.6.1: + dependencies: + '@leichtgewicht/ip-codec': 2.0.5 + + dom-accessibility-api@0.5.16: {} + + dom-accessibility-api@0.6.3: {} + + dom-converter@0.2.0: + dependencies: + utila: 0.4.0 + + dom-helpers@5.2.1: + dependencies: + '@babel/runtime': 7.29.2 + csstype: 3.2.3 + + dom-serializer@1.4.1: + dependencies: + domelementtype: 2.3.0 + domhandler: 4.3.1 + entities: 2.2.0 + + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domexception@4.0.0: + dependencies: + webidl-conversions: 7.0.0 + + domhandler@4.3.1: + dependencies: + domelementtype: 2.3.0 + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@2.8.0: + dependencies: + dom-serializer: 1.4.1 + domelementtype: 2.3.0 + domhandler: 4.3.1 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + + dot-case@3.0.4: + dependencies: + no-case: 3.0.4 + tslib: 2.8.1 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + duplexer@0.1.2: {} + + ee-first@1.1.1: {} + + electron-to-chromium@1.5.359: {} + + emittery@0.13.1: {} + + emoji-regex@10.6.0: {} + + emoji-regex@8.0.0: {} + + encodeurl@2.0.0: {} + + enhanced-resolve@5.21.4: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + + entities@2.2.0: {} + + entities@4.5.0: {} + + entities@6.0.1: {} + + env-paths@2.2.1: {} + + envinfo@7.21.0: {} + + environment@1.1.0: {} + + errno@0.1.8: + dependencies: + prr: 1.0.1 + optional: true + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-get-iterator@1.1.3: + dependencies: + call-bind: 1.0.9 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + is-arguments: 1.2.0 + is-map: 2.0.3 + is-set: 2.0.3 + is-string: 1.1.1 + isarray: 2.0.5 + stop-iteration-iterator: 1.1.0 + + es-module-lexer@2.1.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.3 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + escape-string-regexp@2.0.0: {} + + escape-string-regexp@4.0.0: {} + + escodegen@2.1.0: + dependencies: + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionalDependencies: + source-map: 0.6.1 + + eslint-config-prettier@9.1.2(eslint@9.39.4(jiti@2.7.0)): + dependencies: + eslint: 9.39.4(jiti@2.7.0) + + eslint-plugin-prettier@5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@9.1.2(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0))(prettier@3.8.3): + dependencies: + eslint: 9.39.4(jiti@2.7.0) + prettier: 3.8.3 + prettier-linter-helpers: 1.0.1 + synckit: 0.11.12 + optionalDependencies: + '@types/eslint': 9.6.1 + eslint-config-prettier: 9.1.2(eslint@9.39.4(jiti@2.7.0)) + + eslint-scope@5.1.1: + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint@9.39.4(jiti@2.7.0): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.7.0)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.2 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.5 + '@eslint/js': 9.39.4 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.9 + ajv: 6.15.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.7.0 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 4.2.1 + + esprima@4.0.1: {} + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@4.3.0: {} + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + etag@1.8.1: {} + + eventemitter3@4.0.7: {} + + eventemitter3@5.0.4: {} + + events@3.3.0: {} + + execa@5.1.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + execa@8.0.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 8.0.1 + human-signals: 5.0.0 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.3.0 + onetime: 6.0.0 + signal-exit: 4.1.0 + strip-final-newline: 3.0.0 + + exit@0.1.2: {} + + expect@29.7.0: + dependencies: + '@jest/expect-utils': 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + + express@4.22.2: + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.5 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.0.7 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.3.2 + fresh: 0.5.2 + http-errors: 2.0.1 + merge-descriptors: 1.0.3 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.13 + proxy-addr: 2.0.7 + qs: 6.15.2 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.19.2 + serve-static: 1.16.3 + setprototypeof: 1.2.0 + statuses: 2.0.2 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + fast-deep-equal@3.1.3: {} + + fast-diff@1.3.0: {} + + fast-equals@5.4.0: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-uri@3.1.2: {} + + fastest-levenshtein@1.0.16: {} + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + faye-websocket@0.11.4: + dependencies: + websocket-driver: 0.7.4 + + fb-watchman@2.0.2: + dependencies: + bser: 2.1.1 + + file-entry-cache@11.1.3: + dependencies: + flat-cache: 6.1.22 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + file-selector@2.1.2: + dependencies: + tslib: 2.8.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + finalhandler@1.3.2: + dependencies: + debug: 2.6.9 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + find-root@1.1.0: {} + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.2 + keyv: 4.5.4 + + flat-cache@6.1.22: + dependencies: + cacheable: 2.3.5 + flatted: 3.4.2 + hookified: 1.15.1 + + flat@5.0.2: {} + + flatted@3.4.2: {} + + follow-redirects@1.16.0: {} + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.3 + mime-types: 2.1.35 + + formik@2.4.9(@types/react@18.3.28)(react@18.3.1): + dependencies: + '@types/hoist-non-react-statics': 3.3.7(@types/react@18.3.28) + deepmerge: 2.2.1 + hoist-non-react-statics: 3.3.2 + lodash: 4.18.1 + lodash-es: 4.18.1 + react: 18.3.1 + react-fast-compare: 2.0.4 + tiny-warning: 1.0.3 + tslib: 2.8.1 + transitivePeerDependencies: + - '@types/react' + + forwarded@0.2.0: {} + + fraction.js@5.3.4: {} + + framer-motion@11.18.2(@emotion/is-prop-valid@1.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + motion-dom: 11.18.1 + motion-utils: 11.18.1 + tslib: 2.8.1 + optionalDependencies: + '@emotion/is-prop-valid': 1.4.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + fresh@0.5.2: {} + + fs-monkey@1.1.0: {} + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + functions-have-names@1.2.3: {} + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-east-asian-width@1.6.0: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.3 + math-intrinsics: 1.1.0 + + get-package-type@0.1.0: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-stream@6.0.1: {} + + get-stream@8.0.1: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob-to-regexp@0.4.1: {} + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.5 + once: 1.4.0 + path-is-absolute: 1.0.1 + + global-modules@2.0.0: + dependencies: + global-prefix: 3.0.0 + + global-prefix@3.0.0: + dependencies: + ini: 1.3.8 + kind-of: 6.0.3 + which: 1.3.1 + + globals@14.0.0: {} + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + globby@6.1.0: + dependencies: + array-union: 1.0.2 + glob: 7.2.3 + object-assign: 4.1.1 + pify: 2.3.0 + pinkie-promise: 2.0.1 + + globjoin@0.1.4: {} + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + graphemer@1.4.0: {} + + gzip-size@6.0.0: + dependencies: + duplexer: 0.1.2 + + handle-thing@2.0.1: {} + + has-bigints@1.1.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hashery@1.5.1: + dependencies: + hookified: 1.15.1 + + hasown@2.0.3: + dependencies: + function-bind: 1.1.2 + + he@1.2.0: {} + + hoist-non-react-statics@3.3.2: + dependencies: + react-is: 16.13.1 + + hookified@1.15.1: {} + + hookified@2.2.0: {} + + hpack.js@2.1.6: + dependencies: + inherits: 2.0.4 + obuf: 1.1.2 + readable-stream: 2.3.8 + wbuf: 1.7.3 + + html-encoding-sniffer@3.0.0: + dependencies: + whatwg-encoding: 2.0.0 + + html-entities@2.6.0: {} + + html-escaper@2.0.2: {} + + html-minifier-terser@6.1.0: + dependencies: + camel-case: 4.1.2 + clean-css: 5.3.3 + commander: 8.3.0 + he: 1.2.0 + param-case: 3.0.4 + relateurl: 0.2.7 + terser: 5.47.1 + + html-parse-stringify@3.0.1: + dependencies: + void-elements: 3.1.0 + + html-tags@3.3.1: {} + + html-webpack-plugin@5.6.7(webpack@5.106.2): + dependencies: + '@types/html-minifier-terser': 6.1.0 + html-minifier-terser: 6.1.0 + lodash: 4.18.1 + pretty-error: 4.0.0 + tapable: 2.3.3 + optionalDependencies: + webpack: 5.106.2(cssnano@6.1.2(postcss@8.5.14))(postcss@8.5.14)(webpack-cli@5.1.4) + + htmlparser2@6.1.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 4.3.1 + domutils: 2.8.0 + entities: 2.2.0 + + http-deceiver@1.2.7: {} + + http-errors@1.8.1: + dependencies: + depd: 1.1.2 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 1.5.0 + toidentifier: 1.0.1 + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + http-parser-js@0.5.10: {} + + http-proxy-agent@5.0.0: + dependencies: + '@tootallnate/once': 2.0.1 + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + http-proxy-middleware@2.0.9(@types/express@4.17.25): + dependencies: + '@types/http-proxy': 1.17.17 + http-proxy: 1.18.1 + is-glob: 4.0.3 + is-plain-obj: 3.0.0 + micromatch: 4.0.8 + optionalDependencies: + '@types/express': 4.17.25 + transitivePeerDependencies: + - debug + + http-proxy@1.18.1: + dependencies: + eventemitter3: 4.0.7 + follow-redirects: 1.16.0 + requires-port: 1.0.0 + transitivePeerDependencies: + - debug + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + human-signals@2.1.0: {} + + human-signals@5.0.0: {} + + husky@9.1.7: {} + + i18next@23.16.8: + dependencies: + '@babel/runtime': 7.29.2 + + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + icss-utils@5.1.0(postcss@8.5.14): + dependencies: + postcss: 8.5.14 + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + image-size@0.5.5: + optional: true + + immer@10.2.0: {} + + immer@11.1.8: {} + + immutable@5.1.5: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + import-local@3.2.0: + dependencies: + pkg-dir: 4.2.0 + resolve-cwd: 3.0.0 + + imurmurhash@0.1.4: {} + + indent-string@4.0.0: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + ini@1.3.8: {} + + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.3 + side-channel: 1.1.0 + + internmap@2.0.3: {} + + interpret@3.1.1: {} + + invariant@2.2.4: + dependencies: + loose-envify: 1.4.0 + + ipaddr.js@1.9.1: {} + + ipaddr.js@2.4.0: {} + + is-arguments@1.2.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-arrayish@0.2.1: {} + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-callable@1.2.7: {} + + is-core-module@2.16.2: + dependencies: + hasown: 2.0.3 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-docker@2.2.1: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-fullwidth-code-point@4.0.0: {} + + is-fullwidth-code-point@5.1.0: + dependencies: + get-east-asian-width: 1.6.0 + + is-generator-fn@2.1.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-map@2.0.3: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-number@7.0.0: {} + + is-path-cwd@2.2.0: {} + + is-path-in-cwd@2.1.0: + dependencies: + is-path-inside: 2.1.0 + + is-path-inside@2.1.0: + dependencies: + path-is-inside: 1.0.2 + + is-plain-obj@3.0.0: {} + + is-plain-object@2.0.4: + dependencies: + isobject: 3.0.1 + + is-plain-object@5.0.0: {} + + is-potential-custom-element-name@1.0.1: {} + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.3 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + + is-stream@2.0.1: {} + + is-stream@3.0.0: {} + + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-weakmap@2.0.2: {} + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-what@4.1.16: {} + + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 + + isarray@1.0.0: {} + + isarray@2.0.5: {} + + isexe@2.0.0: {} + + isobject@3.0.1: {} + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-instrument@5.2.1: + dependencies: + '@babel/core': 7.29.0 + '@babel/parser': 7.29.3 + '@istanbuljs/schema': 0.1.6 + istanbul-lib-coverage: 3.2.2 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + istanbul-lib-instrument@6.0.3: + dependencies: + '@babel/core': 7.29.0 + '@babel/parser': 7.29.3 + '@istanbuljs/schema': 0.1.6 + istanbul-lib-coverage: 3.2.2 + semver: 7.8.0 + transitivePeerDependencies: + - supports-color + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-lib-source-maps@4.0.1: + dependencies: + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + source-map: 0.6.1 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + jest-changed-files@29.7.0: + dependencies: + execa: 5.1.1 + jest-util: 29.7.0 + p-limit: 3.1.0 + + jest-circus@29.7.0(babel-plugin-macros@3.1.0): + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.19.41 + chalk: 4.1.2 + co: 4.6.0 + dedent: 1.7.2(babel-plugin-macros@3.1.0) + is-generator-fn: 2.1.0 + jest-each: 29.7.0 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + p-limit: 3.1.0 + pretty-format: 29.7.0 + pure-rand: 6.1.0 + slash: 3.0.0 + stack-utils: 2.0.6 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-cli@29.7.0(@types/node@20.19.41)(babel-plugin-macros@3.1.0): + dependencies: + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0) + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + chalk: 4.1.2 + create-jest: 29.7.0(@types/node@20.19.41)(babel-plugin-macros@3.1.0) + exit: 0.1.2 + import-local: 3.2.0 + jest-config: 29.7.0(@types/node@20.19.41)(babel-plugin-macros@3.1.0) + jest-util: 29.7.0 + jest-validate: 29.7.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + jest-config@29.7.0(@types/node@20.19.41)(babel-plugin-macros@3.1.0): + dependencies: + '@babel/core': 7.29.0 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.29.0) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0(babel-plugin-macros@3.1.0) + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 20.19.41 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-diff@29.7.0: + dependencies: + chalk: 4.1.2 + diff-sequences: 29.6.3 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + + jest-docblock@29.7.0: + dependencies: + detect-newline: 3.1.0 + + jest-each@29.7.0: + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + jest-get-type: 29.6.3 + jest-util: 29.7.0 + pretty-format: 29.7.0 + + jest-environment-jsdom@29.7.0: + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/jsdom': 20.0.1 + '@types/node': 20.19.41 + jest-mock: 29.7.0 + jest-util: 29.7.0 + jsdom: 20.0.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + jest-environment-node@29.7.0: + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.19.41 + jest-mock: 29.7.0 + jest-util: 29.7.0 + + jest-get-type@29.6.3: {} + + jest-haste-map@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/graceful-fs': 4.1.9 + '@types/node': 20.19.41 + anymatch: 3.1.3 + fb-watchman: 2.0.2 + graceful-fs: 4.2.11 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + jest-worker: 29.7.0 + micromatch: 4.0.8 + walker: 1.0.8 + optionalDependencies: + fsevents: 2.3.3 + + jest-leak-detector@29.7.0: + dependencies: + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + + jest-matcher-utils@29.7.0: + dependencies: + chalk: 4.1.2 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + + jest-message-util@29.7.0: + dependencies: + '@babel/code-frame': 7.29.0 + '@jest/types': 29.6.3 + '@types/stack-utils': 2.0.3 + chalk: 4.1.2 + graceful-fs: 4.2.11 + micromatch: 4.0.8 + pretty-format: 29.7.0 + slash: 3.0.0 + stack-utils: 2.0.6 + + jest-mock@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/node': 20.19.41 + jest-util: 29.7.0 + + jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): + optionalDependencies: + jest-resolve: 29.7.0 + + jest-regex-util@29.6.3: {} + + jest-resolve-dependencies@29.7.0: + dependencies: + jest-regex-util: 29.6.3 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + + jest-resolve@29.7.0: + dependencies: + chalk: 4.1.2 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-pnp-resolver: 1.2.3(jest-resolve@29.7.0) + jest-util: 29.7.0 + jest-validate: 29.7.0 + resolve: 1.22.12 + resolve.exports: 2.0.3 + slash: 3.0.0 + + jest-runner@29.7.0: + dependencies: + '@jest/console': 29.7.0 + '@jest/environment': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.19.41 + chalk: 4.1.2 + emittery: 0.13.1 + graceful-fs: 4.2.11 + jest-docblock: 29.7.0 + jest-environment-node: 29.7.0 + jest-haste-map: 29.7.0 + jest-leak-detector: 29.7.0 + jest-message-util: 29.7.0 + jest-resolve: 29.7.0 + jest-runtime: 29.7.0 + jest-util: 29.7.0 + jest-watcher: 29.7.0 + jest-worker: 29.7.0 + p-limit: 3.1.0 + source-map-support: 0.5.13 + transitivePeerDependencies: + - supports-color + + jest-runtime@29.7.0: + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/globals': 29.7.0 + '@jest/source-map': 29.6.3 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.19.41 + chalk: 4.1.2 + cjs-module-lexer: 1.4.3 + collect-v8-coverage: 1.0.3 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + strip-bom: 4.0.0 + transitivePeerDependencies: + - supports-color + + jest-snapshot@29.7.0: + dependencies: + '@babel/core': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) + '@babel/types': 7.29.0 + '@jest/expect-utils': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0) + chalk: 4.1.2 + expect: 29.7.0 + graceful-fs: 4.2.11 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + natural-compare: 1.4.0 + pretty-format: 29.7.0 + semver: 7.8.0 + transitivePeerDependencies: + - supports-color + + jest-util@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/node': 20.19.41 + chalk: 4.1.2 + ci-info: 3.9.0 + graceful-fs: 4.2.11 + picomatch: 2.3.2 + + jest-validate@29.7.0: + dependencies: + '@jest/types': 29.6.3 + camelcase: 6.3.0 + chalk: 4.1.2 + jest-get-type: 29.6.3 + leven: 3.1.0 + pretty-format: 29.7.0 + + jest-watcher@29.7.0: + dependencies: + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.19.41 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + emittery: 0.13.1 + jest-util: 29.7.0 + string-length: 4.0.2 + + jest-worker@27.5.1: + dependencies: + '@types/node': 20.19.41 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + jest-worker@29.7.0: + dependencies: + '@types/node': 20.19.41 + jest-util: 29.7.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + jest@29.7.0(@types/node@20.19.41)(babel-plugin-macros@3.1.0): + dependencies: + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0) + '@jest/types': 29.6.3 + import-local: 3.2.0 + jest-cli: 29.7.0(@types/node@20.19.41)(babel-plugin-macros@3.1.0) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + jiti@2.7.0: {} + + jquery@3.7.1: {} + + js-cookie@3.0.7: {} + + js-tokens@4.0.0: {} + + js-yaml@3.14.2: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + jsdom@20.0.3: + dependencies: + abab: 2.0.6 + acorn: 8.16.0 + acorn-globals: 7.0.1 + cssom: 0.5.0 + cssstyle: 2.3.0 + data-urls: 3.0.2 + decimal.js: 10.6.0 + domexception: 4.0.0 + escodegen: 2.1.0 + form-data: 4.0.5 + html-encoding-sniffer: 3.0.0 + http-proxy-agent: 5.0.0 + https-proxy-agent: 5.0.1 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.23 + parse5: 7.3.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 4.1.4 + w3c-xmlserializer: 4.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 2.0.0 + whatwg-mimetype: 3.0.0 + whatwg-url: 11.0.0 + ws: 8.20.1 + xml-name-validator: 4.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + keyv@5.6.0: + dependencies: + '@keyv/serialize': 1.1.1 + + kind-of@6.0.3: {} + + kleur@3.0.3: {} + + known-css-properties@0.37.0: {} + + launch-editor@2.13.2: + dependencies: + picocolors: 1.1.1 + shell-quote: 1.8.3 + + less-loader@12.3.2(less@4.6.4)(webpack@5.106.2): + dependencies: + less: 4.6.4 + optionalDependencies: + webpack: 5.106.2(cssnano@6.1.2(postcss@8.5.14))(postcss@8.5.14)(webpack-cli@5.1.4) + + less@4.6.4: + dependencies: + copy-anything: 3.0.5 + parse-node-version: 1.0.1 + optionalDependencies: + errno: 0.1.8 + graceful-fs: 4.2.11 + image-size: 0.5.5 + make-dir: 2.1.0 + mime: 1.6.0 + needle: 3.5.0 + source-map: 0.6.1 + + leven@3.1.0: {} + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + + lint-staged@15.5.2: + dependencies: + chalk: 5.6.2 + commander: 13.1.0 + debug: 4.4.3 + execa: 8.0.1 + lilconfig: 3.1.3 + listr2: 8.3.3 + micromatch: 4.0.8 + pidtree: 0.6.0 + string-argv: 0.3.2 + yaml: 2.9.0 + transitivePeerDependencies: + - supports-color + + listr2@8.3.3: + dependencies: + cli-truncate: 4.0.0 + colorette: 2.0.20 + eventemitter3: 5.0.4 + log-update: 6.1.0 + rfdc: 1.4.1 + wrap-ansi: 9.0.2 + + loader-runner@4.3.2: {} + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash-es@4.18.1: {} + + lodash.debounce@4.0.8: {} + + lodash.memoize@4.1.2: {} + + lodash.merge@4.6.2: {} + + lodash.truncate@4.4.2: {} + + lodash.uniq@4.5.0: {} + + lodash@4.18.1: {} + + log-update@6.1.0: + dependencies: + ansi-escapes: 7.3.0 + cli-cursor: 5.0.0 + slice-ansi: 7.1.2 + strip-ansi: 7.2.0 + wrap-ansi: 9.0.2 + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lower-case@2.0.2: + dependencies: + tslib: 2.8.1 + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lz-string@1.5.0: {} + + make-dir@2.1.0: + dependencies: + pify: 4.0.1 + semver: 5.7.2 + optional: true + + make-dir@4.0.0: + dependencies: + semver: 7.8.0 + + makeerror@1.0.12: + dependencies: + tmpl: 1.0.5 + + math-intrinsics@1.1.0: {} + + mathml-tag-names@2.1.3: {} + + mdn-data@2.0.28: {} + + mdn-data@2.0.30: {} + + mdn-data@2.27.1: {} + + media-typer@0.3.0: {} + + memfs@3.5.3: + dependencies: + fs-monkey: 1.1.0 + + memoize-one@6.0.0: {} + + meow@13.2.0: {} + + merge-descriptors@1.0.3: {} + + merge-stream@2.0.0: {} + + merge2@1.4.1: {} + + methods@1.1.2: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + mime-db@1.52.0: {} + + mime-db@1.54.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime@1.6.0: {} + + mimic-fn@2.1.0: {} + + mimic-fn@4.0.0: {} + + mimic-function@5.0.1: {} + + min-indent@1.0.1: {} + + mini-css-extract-plugin@2.10.2(webpack@5.106.2): + dependencies: + schema-utils: 4.3.3 + tapable: 2.3.3 + webpack: 5.106.2(cssnano@6.1.2(postcss@8.5.14))(postcss@8.5.14)(webpack-cli@5.1.4) + + minimalistic-assert@1.0.1: {} + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.6 + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.14 + + minimatch@9.0.9: + dependencies: + brace-expansion: 2.1.0 + + mobx-react-lite@4.1.1(mobx@6.15.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + mobx: 6.15.3 + react: 18.3.1 + use-sync-external-store: 1.6.0(react@18.3.1) + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) + + mobx@6.15.3: {} + + moment-timezone@0.5.48: + dependencies: + moment: 2.30.1 + + moment@2.30.1: {} + + motion-dom@11.18.1: + dependencies: + motion-utils: 11.18.1 + + motion-utils@11.18.1: {} + + mrmime@2.0.1: {} + + ms@2.0.0: {} + + ms@2.1.3: {} + + multicast-dns@7.2.5: + dependencies: + dns-packet: 5.6.1 + thunky: 1.1.0 + + nanoid@3.3.12: {} + + natural-compare@1.4.0: {} + + needle@3.5.0: + dependencies: + iconv-lite: 0.6.3 + sax: 1.6.0 + optional: true + + negotiator@0.6.3: {} + + negotiator@0.6.4: {} + + neo-async@2.6.2: {} + + no-case@3.0.4: + dependencies: + lower-case: 2.0.2 + tslib: 2.8.1 + + node-addon-api@7.1.1: + optional: true + + node-forge@1.4.0: {} + + node-int64@0.4.0: {} + + node-releases@2.0.44: {} + + normalize-path@3.0.0: {} + + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + + npm-run-path@5.3.0: + dependencies: + path-key: 4.0.0 + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + nwsapi@2.2.23: {} + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + object-is@1.1.6: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + obuf@1.1.2: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + on-headers@1.1.0: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + onetime@6.0.0: + dependencies: + mimic-fn: 4.0.0 + + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + + open@8.4.2: + dependencies: + define-lazy-prop: 2.0.0 + is-docker: 2.2.1 + is-wsl: 2.2.0 + + opener@1.5.2: {} + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-map@2.1.0: {} + + p-retry@4.6.2: + dependencies: + '@types/retry': 0.12.0 + retry: 0.13.1 + + p-try@2.2.0: {} + + param-case@3.0.4: + dependencies: + dot-case: 3.0.4 + tslib: 2.8.1 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.29.0 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + parse-node-version@1.0.1: {} + + parse5@7.3.0: + dependencies: + entities: 6.0.1 + + parseurl@1.3.3: {} + + pascal-case@3.1.2: + dependencies: + no-case: 3.0.4 + tslib: 2.8.1 + + path-exists@4.0.0: {} + + path-is-absolute@1.0.1: {} + + path-is-inside@1.0.2: {} + + path-key@3.1.1: {} + + path-key@4.0.0: {} + + path-parse@1.0.7: {} + + path-to-regexp@0.1.13: {} + + path-type@4.0.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + picomatch@4.0.4: + optional: true + + pidtree@0.6.0: {} + + pify@2.3.0: {} + + pify@4.0.1: {} + + pinkie-promise@2.0.1: + dependencies: + pinkie: 2.0.4 + + pinkie@2.0.4: {} + + pirates@4.0.7: {} + + pkg-dir@4.2.0: + dependencies: + find-up: 4.1.0 + + possible-typed-array-names@1.1.0: {} + + postcss-attribute-case-insensitive@6.0.3(postcss@8.5.14): + dependencies: + postcss: 8.5.14 + postcss-selector-parser: 6.1.2 + + postcss-calc@9.0.1(postcss@8.5.14): + dependencies: + postcss: 8.5.14 + postcss-selector-parser: 6.1.2 + postcss-value-parser: 4.2.0 + + postcss-clamp@4.1.0(postcss@8.5.14): + dependencies: + postcss: 8.5.14 + postcss-value-parser: 4.2.0 + + postcss-color-functional-notation@6.0.14(postcss@8.5.14): + dependencies: + '@csstools/css-color-parser': 2.0.5(@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1))(@csstools/css-tokenizer@2.4.1) + '@csstools/css-parser-algorithms': 2.7.1(@csstools/css-tokenizer@2.4.1) + '@csstools/css-tokenizer': 2.4.1 + '@csstools/postcss-progressive-custom-properties': 3.3.0(postcss@8.5.14) + '@csstools/utilities': 1.0.0(postcss@8.5.14) + postcss: 8.5.14 + + postcss-color-hex-alpha@9.0.4(postcss@8.5.14): + dependencies: + '@csstools/utilities': 1.0.0(postcss@8.5.14) + postcss: 8.5.14 + postcss-value-parser: 4.2.0 + + postcss-color-rebeccapurple@9.0.3(postcss@8.5.14): + dependencies: + '@csstools/utilities': 1.0.0(postcss@8.5.14) + postcss: 8.5.14 + postcss-value-parser: 4.2.0 + + postcss-colormin@6.1.0(postcss@8.5.14): + dependencies: + browserslist: 4.28.2 + caniuse-api: 3.0.0 + colord: 2.9.3 + postcss: 8.5.14 + postcss-value-parser: 4.2.0 + + postcss-convert-values@6.1.0(postcss@8.5.14): + dependencies: + browserslist: 4.28.2 + postcss: 8.5.14 + postcss-value-parser: 4.2.0 + + postcss-custom-media@10.0.8(postcss@8.5.14): + dependencies: + '@csstools/cascade-layer-name-parser': 1.0.13(@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1))(@csstools/css-tokenizer@2.4.1) + '@csstools/css-parser-algorithms': 2.7.1(@csstools/css-tokenizer@2.4.1) + '@csstools/css-tokenizer': 2.4.1 + '@csstools/media-query-list-parser': 2.1.13(@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1))(@csstools/css-tokenizer@2.4.1) + postcss: 8.5.14 + + postcss-custom-properties@13.3.12(postcss@8.5.14): + dependencies: + '@csstools/cascade-layer-name-parser': 1.0.13(@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1))(@csstools/css-tokenizer@2.4.1) + '@csstools/css-parser-algorithms': 2.7.1(@csstools/css-tokenizer@2.4.1) + '@csstools/css-tokenizer': 2.4.1 + '@csstools/utilities': 1.0.0(postcss@8.5.14) + postcss: 8.5.14 + postcss-value-parser: 4.2.0 + + postcss-custom-selectors@7.1.12(postcss@8.5.14): + dependencies: + '@csstools/cascade-layer-name-parser': 1.0.13(@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1))(@csstools/css-tokenizer@2.4.1) + '@csstools/css-parser-algorithms': 2.7.1(@csstools/css-tokenizer@2.4.1) + '@csstools/css-tokenizer': 2.4.1 + postcss: 8.5.14 + postcss-selector-parser: 6.1.2 + + postcss-dir-pseudo-class@8.0.1(postcss@8.5.14): + dependencies: + postcss: 8.5.14 + postcss-selector-parser: 6.1.2 + + postcss-discard-comments@6.0.2(postcss@8.5.14): + dependencies: + postcss: 8.5.14 + + postcss-discard-duplicates@6.0.3(postcss@8.5.14): + dependencies: + postcss: 8.5.14 + + postcss-discard-empty@6.0.3(postcss@8.5.14): + dependencies: + postcss: 8.5.14 + + postcss-discard-overridden@6.0.2(postcss@8.5.14): + dependencies: + postcss: 8.5.14 + + postcss-double-position-gradients@5.0.7(postcss@8.5.14): + dependencies: + '@csstools/postcss-progressive-custom-properties': 3.3.0(postcss@8.5.14) + '@csstools/utilities': 1.0.0(postcss@8.5.14) + postcss: 8.5.14 + postcss-value-parser: 4.2.0 + + postcss-focus-visible@9.0.1(postcss@8.5.14): + dependencies: + postcss: 8.5.14 + postcss-selector-parser: 6.1.2 + + postcss-focus-within@8.0.1(postcss@8.5.14): + dependencies: + postcss: 8.5.14 + postcss-selector-parser: 6.1.2 + + postcss-font-variant@5.0.0(postcss@8.5.14): + dependencies: + postcss: 8.5.14 + + postcss-gap-properties@5.0.1(postcss@8.5.14): + dependencies: + postcss: 8.5.14 + + postcss-image-set-function@6.0.3(postcss@8.5.14): + dependencies: + '@csstools/utilities': 1.0.0(postcss@8.5.14) + postcss: 8.5.14 + postcss-value-parser: 4.2.0 + + postcss-lab-function@6.0.19(postcss@8.5.14): + dependencies: + '@csstools/css-color-parser': 2.0.5(@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1))(@csstools/css-tokenizer@2.4.1) + '@csstools/css-parser-algorithms': 2.7.1(@csstools/css-tokenizer@2.4.1) + '@csstools/css-tokenizer': 2.4.1 + '@csstools/postcss-progressive-custom-properties': 3.3.0(postcss@8.5.14) + '@csstools/utilities': 1.0.0(postcss@8.5.14) + postcss: 8.5.14 + + postcss-loader@8.2.1(postcss@8.5.14)(typescript@5.9.3)(webpack@5.106.2): + dependencies: + cosmiconfig: 9.0.1(typescript@5.9.3) + jiti: 2.7.0 + postcss: 8.5.14 + semver: 7.8.0 + optionalDependencies: + webpack: 5.106.2(cssnano@6.1.2(postcss@8.5.14))(postcss@8.5.14)(webpack-cli@5.1.4) + transitivePeerDependencies: + - typescript + + postcss-logical@7.0.1(postcss@8.5.14): + dependencies: + postcss: 8.5.14 + postcss-value-parser: 4.2.0 + + postcss-merge-longhand@6.0.5(postcss@8.5.14): + dependencies: + postcss: 8.5.14 + postcss-value-parser: 4.2.0 + stylehacks: 6.1.1(postcss@8.5.14) + + postcss-merge-rules@6.1.1(postcss@8.5.14): + dependencies: + browserslist: 4.28.2 + caniuse-api: 3.0.0 + cssnano-utils: 4.0.2(postcss@8.5.14) + postcss: 8.5.14 + postcss-selector-parser: 6.1.2 + + postcss-minify-font-values@6.1.0(postcss@8.5.14): + dependencies: + postcss: 8.5.14 + postcss-value-parser: 4.2.0 + + postcss-minify-gradients@6.0.3(postcss@8.5.14): + dependencies: + colord: 2.9.3 + cssnano-utils: 4.0.2(postcss@8.5.14) + postcss: 8.5.14 + postcss-value-parser: 4.2.0 + + postcss-minify-params@6.1.0(postcss@8.5.14): + dependencies: + browserslist: 4.28.2 + cssnano-utils: 4.0.2(postcss@8.5.14) + postcss: 8.5.14 + postcss-value-parser: 4.2.0 + + postcss-minify-selectors@6.0.4(postcss@8.5.14): + dependencies: + postcss: 8.5.14 + postcss-selector-parser: 6.1.2 + + postcss-modules-extract-imports@3.1.0(postcss@8.5.14): + dependencies: + postcss: 8.5.14 + + postcss-modules-local-by-default@4.2.0(postcss@8.5.14): + dependencies: + icss-utils: 5.1.0(postcss@8.5.14) + postcss: 8.5.14 + postcss-selector-parser: 7.1.1 + postcss-value-parser: 4.2.0 + + postcss-modules-scope@3.2.1(postcss@8.5.14): + dependencies: + postcss: 8.5.14 + postcss-selector-parser: 7.1.1 + + postcss-modules-values@4.0.0(postcss@8.5.14): + dependencies: + icss-utils: 5.1.0(postcss@8.5.14) + postcss: 8.5.14 + + postcss-nesting@12.1.5(postcss@8.5.14): + dependencies: + '@csstools/selector-resolve-nested': 1.1.0(postcss-selector-parser@6.1.2) + '@csstools/selector-specificity': 3.1.1(postcss-selector-parser@6.1.2) + postcss: 8.5.14 + postcss-selector-parser: 6.1.2 + + postcss-normalize-charset@6.0.2(postcss@8.5.14): + dependencies: + postcss: 8.5.14 + + postcss-normalize-display-values@6.0.2(postcss@8.5.14): + dependencies: + postcss: 8.5.14 + postcss-value-parser: 4.2.0 + + postcss-normalize-positions@6.0.2(postcss@8.5.14): + dependencies: + postcss: 8.5.14 + postcss-value-parser: 4.2.0 + + postcss-normalize-repeat-style@6.0.2(postcss@8.5.14): + dependencies: + postcss: 8.5.14 + postcss-value-parser: 4.2.0 + + postcss-normalize-string@6.0.2(postcss@8.5.14): + dependencies: + postcss: 8.5.14 + postcss-value-parser: 4.2.0 + + postcss-normalize-timing-functions@6.0.2(postcss@8.5.14): + dependencies: + postcss: 8.5.14 + postcss-value-parser: 4.2.0 + + postcss-normalize-unicode@6.1.0(postcss@8.5.14): + dependencies: + browserslist: 4.28.2 + postcss: 8.5.14 + postcss-value-parser: 4.2.0 + + postcss-normalize-url@6.0.2(postcss@8.5.14): + dependencies: + postcss: 8.5.14 + postcss-value-parser: 4.2.0 + + postcss-normalize-whitespace@6.0.2(postcss@8.5.14): + dependencies: + postcss: 8.5.14 + postcss-value-parser: 4.2.0 + + postcss-opacity-percentage@2.0.0(postcss@8.5.14): + dependencies: + postcss: 8.5.14 + + postcss-ordered-values@6.0.2(postcss@8.5.14): + dependencies: + cssnano-utils: 4.0.2(postcss@8.5.14) + postcss: 8.5.14 + postcss-value-parser: 4.2.0 + + postcss-overflow-shorthand@5.0.1(postcss@8.5.14): + dependencies: + postcss: 8.5.14 + postcss-value-parser: 4.2.0 + + postcss-page-break@3.0.4(postcss@8.5.14): + dependencies: + postcss: 8.5.14 + + postcss-place@9.0.1(postcss@8.5.14): + dependencies: + postcss: 8.5.14 + postcss-value-parser: 4.2.0 + + postcss-preset-env@9.6.0(postcss@8.5.14): + dependencies: + '@csstools/postcss-cascade-layers': 4.0.6(postcss@8.5.14) + '@csstools/postcss-color-function': 3.0.19(postcss@8.5.14) + '@csstools/postcss-color-mix-function': 2.0.19(postcss@8.5.14) + '@csstools/postcss-content-alt-text': 1.0.0(postcss@8.5.14) + '@csstools/postcss-exponential-functions': 1.0.9(postcss@8.5.14) + '@csstools/postcss-font-format-keywords': 3.0.2(postcss@8.5.14) + '@csstools/postcss-gamut-mapping': 1.0.11(postcss@8.5.14) + '@csstools/postcss-gradients-interpolation-method': 4.0.20(postcss@8.5.14) + '@csstools/postcss-hwb-function': 3.0.18(postcss@8.5.14) + '@csstools/postcss-ic-unit': 3.0.7(postcss@8.5.14) + '@csstools/postcss-initial': 1.0.1(postcss@8.5.14) + '@csstools/postcss-is-pseudo-class': 4.0.8(postcss@8.5.14) + '@csstools/postcss-light-dark-function': 1.0.8(postcss@8.5.14) + '@csstools/postcss-logical-float-and-clear': 2.0.1(postcss@8.5.14) + '@csstools/postcss-logical-overflow': 1.0.1(postcss@8.5.14) + '@csstools/postcss-logical-overscroll-behavior': 1.0.1(postcss@8.5.14) + '@csstools/postcss-logical-resize': 2.0.1(postcss@8.5.14) + '@csstools/postcss-logical-viewport-units': 2.0.11(postcss@8.5.14) + '@csstools/postcss-media-minmax': 1.1.8(postcss@8.5.14) + '@csstools/postcss-media-queries-aspect-ratio-number-values': 2.0.11(postcss@8.5.14) + '@csstools/postcss-nested-calc': 3.0.2(postcss@8.5.14) + '@csstools/postcss-normalize-display-values': 3.0.2(postcss@8.5.14) + '@csstools/postcss-oklab-function': 3.0.19(postcss@8.5.14) + '@csstools/postcss-progressive-custom-properties': 3.3.0(postcss@8.5.14) + '@csstools/postcss-relative-color-syntax': 2.0.19(postcss@8.5.14) + '@csstools/postcss-scope-pseudo-class': 3.0.1(postcss@8.5.14) + '@csstools/postcss-stepped-value-functions': 3.0.10(postcss@8.5.14) + '@csstools/postcss-text-decoration-shorthand': 3.0.7(postcss@8.5.14) + '@csstools/postcss-trigonometric-functions': 3.0.10(postcss@8.5.14) + '@csstools/postcss-unset-value': 3.0.1(postcss@8.5.14) + autoprefixer: 10.5.0(postcss@8.5.14) + browserslist: 4.28.2 + css-blank-pseudo: 6.0.2(postcss@8.5.14) + css-has-pseudo: 6.0.5(postcss@8.5.14) + css-prefers-color-scheme: 9.0.1(postcss@8.5.14) + cssdb: 8.9.0 + postcss: 8.5.14 + postcss-attribute-case-insensitive: 6.0.3(postcss@8.5.14) + postcss-clamp: 4.1.0(postcss@8.5.14) + postcss-color-functional-notation: 6.0.14(postcss@8.5.14) + postcss-color-hex-alpha: 9.0.4(postcss@8.5.14) + postcss-color-rebeccapurple: 9.0.3(postcss@8.5.14) + postcss-custom-media: 10.0.8(postcss@8.5.14) + postcss-custom-properties: 13.3.12(postcss@8.5.14) + postcss-custom-selectors: 7.1.12(postcss@8.5.14) + postcss-dir-pseudo-class: 8.0.1(postcss@8.5.14) + postcss-double-position-gradients: 5.0.7(postcss@8.5.14) + postcss-focus-visible: 9.0.1(postcss@8.5.14) + postcss-focus-within: 8.0.1(postcss@8.5.14) + postcss-font-variant: 5.0.0(postcss@8.5.14) + postcss-gap-properties: 5.0.1(postcss@8.5.14) + postcss-image-set-function: 6.0.3(postcss@8.5.14) + postcss-lab-function: 6.0.19(postcss@8.5.14) + postcss-logical: 7.0.1(postcss@8.5.14) + postcss-nesting: 12.1.5(postcss@8.5.14) + postcss-opacity-percentage: 2.0.0(postcss@8.5.14) + postcss-overflow-shorthand: 5.0.1(postcss@8.5.14) + postcss-page-break: 3.0.4(postcss@8.5.14) + postcss-place: 9.0.1(postcss@8.5.14) + postcss-pseudo-class-any-link: 9.0.2(postcss@8.5.14) + postcss-replace-overflow-wrap: 4.0.0(postcss@8.5.14) + postcss-selector-not: 7.0.2(postcss@8.5.14) + + postcss-pseudo-class-any-link@9.0.2(postcss@8.5.14): + dependencies: + postcss: 8.5.14 + postcss-selector-parser: 6.1.2 + + postcss-reduce-initial@6.1.0(postcss@8.5.14): + dependencies: + browserslist: 4.28.2 + caniuse-api: 3.0.0 + postcss: 8.5.14 + + postcss-reduce-transforms@6.0.2(postcss@8.5.14): + dependencies: + postcss: 8.5.14 + postcss-value-parser: 4.2.0 + + postcss-replace-overflow-wrap@4.0.0(postcss@8.5.14): + dependencies: + postcss: 8.5.14 + + postcss-resolve-nested-selector@0.1.6: {} + + postcss-safe-parser@7.0.1(postcss@8.5.14): + dependencies: + postcss: 8.5.14 + + postcss-selector-not@7.0.2(postcss@8.5.14): + dependencies: + postcss: 8.5.14 + postcss-selector-parser: 6.1.2 + + postcss-selector-parser@6.1.2: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-selector-parser@7.1.1: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-svgo@6.0.3(postcss@8.5.14): + dependencies: + postcss: 8.5.14 + postcss-value-parser: 4.2.0 + svgo: 3.3.3 + + postcss-unique-selectors@6.0.4(postcss@8.5.14): + dependencies: + postcss: 8.5.14 + postcss-selector-parser: 6.1.2 + + postcss-value-parser@4.2.0: {} + + postcss@8.5.14: + dependencies: + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + prettier-linter-helpers@1.0.1: + dependencies: + fast-diff: 1.3.0 + + prettier@3.8.3: {} + + pretty-error@4.0.0: + dependencies: + lodash: 4.18.1 + renderkid: 3.0.0 + + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + + pretty-format@29.7.0: + dependencies: + '@jest/schemas': 29.6.3 + ansi-styles: 5.2.0 + react-is: 18.3.1 + + process-nextick-args@2.0.1: {} + + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + property-expr@2.0.6: {} + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + proxy-from-env@2.1.0: {} + + prr@1.0.1: + optional: true + + psl@1.15.0: + dependencies: + punycode: 2.3.1 + + punycode@2.3.1: {} + + pure-rand@6.1.0: {} + + qified@0.10.1: + dependencies: + hookified: 2.2.0 + + qs@6.15.2: + dependencies: + side-channel: 1.1.0 + + querystringify@2.2.0: {} + + queue-microtask@1.2.3: {} + + range-parser@1.2.1: {} + + raw-body@2.5.3: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + + react-dnd-html5-backend@16.0.1: + dependencies: + dnd-core: 16.0.1 + + react-dnd@16.0.1(@types/hoist-non-react-statics@3.3.7(@types/react@18.3.28))(@types/node@20.19.41)(@types/react@18.3.28)(react@18.3.1): + dependencies: + '@react-dnd/invariant': 4.0.2 + '@react-dnd/shallowequal': 4.0.2 + dnd-core: 16.0.1 + fast-deep-equal: 3.1.3 + hoist-non-react-statics: 3.3.2 + react: 18.3.1 + optionalDependencies: + '@types/hoist-non-react-statics': 3.3.7(@types/react@18.3.28) + '@types/node': 20.19.41 + '@types/react': 18.3.28 + + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + + react-dropzone@14.4.1(react@18.3.1): + dependencies: + attr-accept: 2.2.5 + file-selector: 2.1.2 + prop-types: 15.8.1 + react: 18.3.1 + + react-fast-compare@2.0.4: {} + + react-fast-compare@3.2.2: {} + + react-helmet-async@2.0.5(react@18.3.1): + dependencies: + invariant: 2.2.4 + react: 18.3.1 + react-fast-compare: 3.2.2 + shallowequal: 1.1.0 + + react-hook-form@7.76.0(react@18.3.1): + dependencies: + react: 18.3.1 + + react-i18next@14.1.3(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.29.2 + html-parse-stringify: 3.0.1 + i18next: 23.16.8 + react: 18.3.1 + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) + + react-is@16.13.1: {} + + react-is@17.0.2: {} + + react-is@18.3.1: {} + + react-is@19.2.6: {} + + react-lifecycles-compat@3.0.4: {} + + react-redux@9.3.0(@types/react@18.3.28)(react@18.3.1)(redux@5.0.1): + dependencies: + '@types/use-sync-external-store': 0.0.6 + react: 18.3.1 + use-sync-external-store: 1.6.0(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + redux: 5.0.1 + + react-router-dom@6.30.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@remix-run/router': 1.23.2 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-router: 6.30.3(react@18.3.1) + + react-router@6.30.3(react@18.3.1): + dependencies: + '@remix-run/router': 1.23.2 + react: 18.3.1 + + react-select@5.10.2(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.29.2 + '@emotion/cache': 11.14.0 + '@emotion/react': 11.14.0(@types/react@18.3.28)(react@18.3.1) + '@floating-ui/dom': 1.7.6 + '@types/react-transition-group': 4.4.12(@types/react@18.3.28) + memoize-one: 6.0.0 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-transition-group: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + use-isomorphic-layout-effect: 1.2.1(@types/react@18.3.28)(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - supports-color + + react-smooth@4.0.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + fast-equals: 5.4.0 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-transition-group: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + + react-tooltip@5.30.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@floating-ui/dom': 1.7.6 + classnames: 2.5.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.29.2 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + react-virtualized@9.22.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.29.2 + clsx: 1.2.1 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-lifecycles-compat: 3.0.4 + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.2 + + readdirp@4.1.2: {} + + recharts-scale@0.4.5: + dependencies: + decimal.js-light: 2.5.1 + + recharts@2.15.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + clsx: 2.1.1 + eventemitter3: 4.0.7 + lodash: 4.18.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-is: 18.3.1 + react-smooth: 4.0.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + recharts-scale: 0.4.5 + tiny-invariant: 1.3.3 + victory-vendor: 36.9.2 + + rechoir@0.8.0: + dependencies: + resolve: 1.22.12 + + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + + redux-thunk@3.1.0(redux@5.0.1): + dependencies: + redux: 5.0.1 + + redux@4.2.1: + dependencies: + '@babel/runtime': 7.29.2 + + redux@5.0.1: {} + + regenerate-unicode-properties@10.2.2: + dependencies: + regenerate: 1.4.2 + + regenerate@1.4.2: {} + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + + regexpu-core@6.4.0: + dependencies: + regenerate: 1.4.2 + regenerate-unicode-properties: 10.2.2 + regjsgen: 0.8.0 + regjsparser: 0.13.1 + unicode-match-property-ecmascript: 2.0.0 + unicode-match-property-value-ecmascript: 2.2.1 + + regjsgen@0.8.0: {} + + regjsparser@0.13.1: + dependencies: + jsesc: 3.1.0 + + relateurl@0.2.7: {} + + renderkid@3.0.0: + dependencies: + css-select: 4.3.0 + dom-converter: 0.2.0 + htmlparser2: 6.1.0 + lodash: 4.18.1 + strip-ansi: 6.0.1 + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + requires-port@1.0.0: {} + + reselect@5.2.0: {} + + resolve-cwd@3.0.0: + dependencies: + resolve-from: 5.0.0 + + resolve-from@4.0.0: {} + + resolve-from@5.0.0: {} + + resolve.exports@2.0.3: {} + + resolve@1.22.12: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.2 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + + retry@0.13.1: {} + + reusify@1.1.0: {} + + rfdc@1.4.1: {} + + rimraf@2.7.1: + dependencies: + glob: 7.2.3 + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-buffer@5.1.2: {} + + safe-buffer@5.2.1: {} + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + safer-buffer@2.1.2: {} + + sass-loader@14.2.1(sass@1.99.0)(webpack@5.106.2): + dependencies: + neo-async: 2.6.2 + optionalDependencies: + sass: 1.99.0 + webpack: 5.106.2(cssnano@6.1.2(postcss@8.5.14))(postcss@8.5.14)(webpack-cli@5.1.4) + + sass@1.99.0: + dependencies: + chokidar: 4.0.3 + immutable: 5.1.5 + source-map-js: 1.2.1 + optionalDependencies: + '@parcel/watcher': 2.5.6 + + sax@1.6.0: {} + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + + schema-utils@4.3.3: + dependencies: + '@types/json-schema': 7.0.15 + ajv: 8.20.0 + ajv-formats: 2.1.1(ajv@8.20.0) + ajv-keywords: 5.1.0(ajv@8.20.0) + + select-hose@2.0.0: {} + + selfsigned@2.4.1: + dependencies: + '@types/node-forge': 1.3.14 + node-forge: 1.4.0 + + semver@5.7.2: + optional: true + + semver@6.3.1: {} + + semver@7.8.0: {} + + send@0.19.2: + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.1 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-index@1.9.2: + dependencies: + accepts: 1.3.8 + batch: 0.6.1 + debug: 2.6.9 + escape-html: 1.0.3 + http-errors: 1.8.1 + mime-types: 2.1.35 + parseurl: 1.3.3 + transitivePeerDependencies: + - supports-color + + serve-static@1.16.3: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.2 + transitivePeerDependencies: + - supports-color + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + setprototypeof@1.2.0: {} + + shallow-clone@3.0.1: + dependencies: + kind-of: 6.0.3 + + shallowequal@1.1.0: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + shell-quote@1.8.3: {} + + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + + sirv@2.0.4: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + + sisteransi@1.0.5: {} + + slash@3.0.0: {} + + slice-ansi@4.0.0: + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + + slice-ansi@5.0.0: + dependencies: + ansi-styles: 6.2.3 + is-fullwidth-code-point: 4.0.0 + + slice-ansi@7.1.2: + dependencies: + ansi-styles: 6.2.3 + is-fullwidth-code-point: 5.1.0 + + sockjs@0.3.24: + dependencies: + faye-websocket: 0.11.4 + uuid: 8.3.2 + websocket-driver: 0.7.4 + + sortablejs@1.15.7: {} + + source-map-js@1.2.1: {} + + source-map-support@0.5.13: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.5.7: {} + + source-map@0.6.1: {} + + spdy-transport@3.0.0: + dependencies: + debug: 4.4.3 + detect-node: 2.1.0 + hpack.js: 2.1.6 + obuf: 1.1.2 + readable-stream: 3.6.2 + wbuf: 1.7.3 + transitivePeerDependencies: + - supports-color + + spdy@4.0.2: + dependencies: + debug: 4.4.3 + handle-thing: 2.0.1 + http-deceiver: 1.2.7 + select-hose: 2.0.0 + spdy-transport: 3.0.0 + transitivePeerDependencies: + - supports-color + + sprintf-js@1.0.3: {} + + stack-utils@2.0.6: + dependencies: + escape-string-regexp: 2.0.0 + + statuses@1.5.0: {} + + statuses@2.0.2: {} + + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + + string-argv@0.3.2: {} + + string-length@4.0.2: + dependencies: + char-regex: 1.0.2 + strip-ansi: 6.0.1 + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.6.0 + strip-ansi: 7.2.0 + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + + strip-bom@4.0.0: {} + + strip-final-newline@2.0.0: {} + + strip-final-newline@3.0.0: {} + + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + + strip-json-comments@3.1.1: {} + + style-loader@3.3.4(webpack@5.106.2): + dependencies: + webpack: 5.106.2(cssnano@6.1.2(postcss@8.5.14))(postcss@8.5.14)(webpack-cli@5.1.4) + + stylehacks@6.1.1(postcss@8.5.14): + dependencies: + browserslist: 4.28.2 + postcss: 8.5.14 + postcss-selector-parser: 6.1.2 + + stylelint-config-recommended@14.0.1(stylelint@16.26.1(typescript@5.9.3)): + dependencies: + stylelint: 16.26.1(typescript@5.9.3) + + stylelint-config-standard@36.0.1(stylelint@16.26.1(typescript@5.9.3)): + dependencies: + stylelint: 16.26.1(typescript@5.9.3) + stylelint-config-recommended: 14.0.1(stylelint@16.26.1(typescript@5.9.3)) + + stylelint@16.26.1(typescript@5.9.3): + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-syntax-patches-for-csstree': 1.1.4(css-tree@3.2.1) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/media-query-list-parser': 4.0.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.1) + '@dual-bundle/import-meta-resolve': 4.2.1 + balanced-match: 2.0.0 + colord: 2.9.3 + cosmiconfig: 9.0.1(typescript@5.9.3) + css-functions-list: 3.3.3 + css-tree: 3.2.1 + debug: 4.4.3 + fast-glob: 3.3.3 + fastest-levenshtein: 1.0.16 + file-entry-cache: 11.1.3 + global-modules: 2.0.0 + globby: 11.1.0 + globjoin: 0.1.4 + html-tags: 3.3.1 + ignore: 7.0.5 + imurmurhash: 0.1.4 + is-plain-object: 5.0.0 + known-css-properties: 0.37.0 + mathml-tag-names: 2.1.3 + meow: 13.2.0 + micromatch: 4.0.8 + normalize-path: 3.0.0 + picocolors: 1.1.1 + postcss: 8.5.14 + postcss-resolve-nested-selector: 0.1.6 + postcss-safe-parser: 7.0.1(postcss@8.5.14) + postcss-selector-parser: 7.1.1 + postcss-value-parser: 4.2.0 + resolve-from: 5.0.0 + string-width: 4.2.3 + supports-hyperlinks: 3.2.0 + svg-tags: 1.0.0 + table: 6.9.0 + write-file-atomic: 5.0.1 + transitivePeerDependencies: + - supports-color + - typescript + + stylis@4.2.0: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + supports-hyperlinks@3.2.0: + dependencies: + has-flag: 4.0.0 + supports-color: 7.2.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + svg-tags@1.0.0: {} + + svgo@3.3.3: + dependencies: + commander: 7.2.0 + css-select: 5.2.2 + css-tree: 2.3.1 + css-what: 6.2.2 + csso: 5.0.5 + picocolors: 1.1.1 + sax: 1.6.0 + + symbol-tree@3.2.4: {} + + synckit@0.11.12: + dependencies: + '@pkgr/core': 0.2.9 + + table@6.9.0: + dependencies: + ajv: 8.20.0 + lodash.truncate: 4.4.2 + slice-ansi: 4.0.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + tapable@2.3.3: {} + + terser-webpack-plugin@5.6.0(cssnano@6.1.2(postcss@8.5.14))(postcss@8.5.14)(webpack@5.106.2): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + jest-worker: 27.5.1 + schema-utils: 4.3.3 + terser: 5.47.1 + webpack: 5.106.2(cssnano@6.1.2(postcss@8.5.14))(postcss@8.5.14)(webpack-cli@5.1.4) + optionalDependencies: + cssnano: 6.1.2(postcss@8.5.14) + postcss: 8.5.14 + + terser@5.47.1: + dependencies: + '@jridgewell/source-map': 0.3.11 + acorn: 8.16.0 + commander: 2.20.3 + source-map-support: 0.5.21 + + test-exclude@6.0.0: + dependencies: + '@istanbuljs/schema': 0.1.6 + glob: 7.2.3 + minimatch: 3.1.5 + + thunky@1.1.0: {} + + tiny-case@1.0.3: {} + + tiny-invariant@1.3.3: {} + + tiny-warning@1.0.3: {} + + tmpl@1.0.5: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toidentifier@1.0.1: {} + + toposort@2.0.2: {} + + totalist@3.0.1: {} + + tough-cookie@4.1.4: + dependencies: + psl: 1.15.0 + punycode: 2.3.1 + universalify: 0.2.0 + url-parse: 1.5.10 + + tr46@3.0.0: + dependencies: + punycode: 2.3.1 + + ts-api-utils@1.4.3(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + tslib@2.8.1: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-detect@4.0.8: {} + + type-fest@0.21.3: {} + + type-fest@2.19.0: {} + + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + + typescript@5.9.3: {} + + undici-types@6.21.0: {} + + unicode-canonical-property-names-ecmascript@2.0.1: {} + + unicode-match-property-ecmascript@2.0.0: + dependencies: + unicode-canonical-property-names-ecmascript: 2.0.1 + unicode-property-aliases-ecmascript: 2.2.0 + + unicode-match-property-value-ecmascript@2.2.1: {} + + unicode-property-aliases-ecmascript@2.2.0: {} + + universalify@0.2.0: {} + + unpipe@1.0.0: {} + + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + url-parse@1.5.10: + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + + use-isomorphic-layout-effect@1.2.1(@types/react@18.3.28)(react@18.3.1): + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + use-sync-external-store@1.6.0(react@18.3.1): + dependencies: + react: 18.3.1 + + util-deprecate@1.0.2: {} + + utila@0.4.0: {} + + utils-merge@1.0.1: {} + + uuid@8.3.2: {} + + uuid@9.0.1: {} + + v8-to-istanbul@9.3.0: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@types/istanbul-lib-coverage': 2.0.6 + convert-source-map: 2.0.0 + + vary@1.1.2: {} + + victory-vendor@36.9.2: + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-ease': 3.0.2 + '@types/d3-interpolate': 3.0.4 + '@types/d3-scale': 4.0.9 + '@types/d3-shape': 3.1.8 + '@types/d3-time': 3.0.4 + '@types/d3-timer': 3.0.2 + d3-array: 3.2.4 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-timer: 3.0.1 + + void-elements@3.1.0: {} + + w3c-xmlserializer@4.0.0: + dependencies: + xml-name-validator: 4.0.0 + + walker@1.0.8: + dependencies: + makeerror: 1.0.12 + + watchpack@2.5.1: + dependencies: + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + + wbuf@1.7.3: + dependencies: + minimalistic-assert: 1.0.1 + + webidl-conversions@7.0.0: {} + + webpack-bundle-analyzer@4.10.2: + dependencies: + '@discoveryjs/json-ext': 0.5.7 + acorn: 8.16.0 + acorn-walk: 8.3.5 + commander: 7.2.0 + debounce: 1.2.1 + escape-string-regexp: 4.0.0 + gzip-size: 6.0.0 + html-escaper: 2.0.2 + opener: 1.5.2 + picocolors: 1.1.1 + sirv: 2.0.4 + ws: 7.5.10 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack-dev-server@4.15.2)(webpack@5.106.2): + dependencies: + '@discoveryjs/json-ext': 0.5.7 + '@webpack-cli/configtest': 2.1.1(webpack-cli@5.1.4)(webpack@5.106.2) + '@webpack-cli/info': 2.0.2(webpack-cli@5.1.4)(webpack@5.106.2) + '@webpack-cli/serve': 2.0.5(webpack-cli@5.1.4)(webpack-dev-server@4.15.2)(webpack@5.106.2) + colorette: 2.0.20 + commander: 10.0.1 + cross-spawn: 7.0.6 + envinfo: 7.21.0 + fastest-levenshtein: 1.0.16 + import-local: 3.2.0 + interpret: 3.1.1 + rechoir: 0.8.0 + webpack: 5.106.2(cssnano@6.1.2(postcss@8.5.14))(postcss@8.5.14)(webpack-cli@5.1.4) + webpack-merge: 5.10.0 + optionalDependencies: + webpack-bundle-analyzer: 4.10.2 + webpack-dev-server: 4.15.2(webpack-cli@5.1.4)(webpack@5.106.2) + + webpack-dev-middleware@5.3.4(webpack@5.106.2): + dependencies: + colorette: 2.0.20 + memfs: 3.5.3 + mime-types: 2.1.35 + range-parser: 1.2.1 + schema-utils: 4.3.3 + webpack: 5.106.2(cssnano@6.1.2(postcss@8.5.14))(postcss@8.5.14)(webpack-cli@5.1.4) + + webpack-dev-server@4.15.2(webpack-cli@5.1.4)(webpack@5.106.2): + dependencies: + '@types/bonjour': 3.5.13 + '@types/connect-history-api-fallback': 1.5.4 + '@types/express': 4.17.25 + '@types/serve-index': 1.9.4 + '@types/serve-static': 1.15.10 + '@types/sockjs': 0.3.36 + '@types/ws': 8.18.1 + ansi-html-community: 0.0.8 + bonjour-service: 1.3.0 + chokidar: 3.6.0 + colorette: 2.0.20 + compression: 1.8.1 + connect-history-api-fallback: 2.0.0 + default-gateway: 6.0.3 + express: 4.22.2 + graceful-fs: 4.2.11 + html-entities: 2.6.0 + http-proxy-middleware: 2.0.9(@types/express@4.17.25) + ipaddr.js: 2.4.0 + launch-editor: 2.13.2 + open: 8.4.2 + p-retry: 4.6.2 + rimraf: 3.0.2 + schema-utils: 4.3.3 + selfsigned: 2.4.1 + serve-index: 1.9.2 + sockjs: 0.3.24 + spdy: 4.0.2 + webpack-dev-middleware: 5.3.4(webpack@5.106.2) + ws: 8.20.1 + optionalDependencies: + webpack: 5.106.2(cssnano@6.1.2(postcss@8.5.14))(postcss@8.5.14)(webpack-cli@5.1.4) + webpack-cli: 5.1.4(webpack-bundle-analyzer@4.10.2)(webpack-dev-server@4.15.2)(webpack@5.106.2) + transitivePeerDependencies: + - bufferutil + - debug + - supports-color + - utf-8-validate + + webpack-merge@5.10.0: + dependencies: + clone-deep: 4.0.1 + flat: 5.0.2 + wildcard: 2.0.1 + + webpack-sources@3.4.1: {} + + webpack@5.106.2(cssnano@6.1.2(postcss@8.5.14))(postcss@8.5.14)(webpack-cli@5.1.4): + dependencies: + '@types/eslint-scope': 3.7.7 + '@types/estree': 1.0.9 + '@types/json-schema': 7.0.15 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + acorn: 8.16.0 + acorn-import-phases: 1.0.4(acorn@8.16.0) + browserslist: 4.28.2 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.21.4 + es-module-lexer: 2.1.0 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + loader-runner: 4.3.2 + mime-db: 1.54.0 + neo-async: 2.6.2 + schema-utils: 4.3.3 + tapable: 2.3.3 + terser-webpack-plugin: 5.6.0(cssnano@6.1.2(postcss@8.5.14))(postcss@8.5.14)(webpack@5.106.2) + watchpack: 2.5.1 + webpack-sources: 3.4.1 + optionalDependencies: + webpack-cli: 5.1.4(webpack-bundle-analyzer@4.10.2)(webpack-dev-server@4.15.2)(webpack@5.106.2) + transitivePeerDependencies: + - '@minify-html/node' + - '@swc/core' + - '@swc/css' + - '@swc/html' + - clean-css + - cssnano + - csso + - esbuild + - html-minifier-terser + - lightningcss + - postcss + - uglify-js + + websocket-driver@0.7.4: + dependencies: + http-parser-js: 0.5.10 + safe-buffer: 5.2.1 + websocket-extensions: 0.1.4 + + websocket-extensions@0.1.4: {} + + whatwg-encoding@2.0.0: + dependencies: + iconv-lite: 0.6.3 + + whatwg-fetch@3.6.20: {} + + whatwg-mimetype@3.0.0: {} + + whatwg-url@11.0.0: + dependencies: + tr46: 3.0.0 + webidl-conversions: 7.0.0 + + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.20: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + which@1.3.1: + dependencies: + isexe: 2.0.0 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + wildcard@2.0.1: {} + + word-wrap@1.2.5: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@9.0.2: + dependencies: + ansi-styles: 6.2.3 + string-width: 7.2.0 + strip-ansi: 7.2.0 + + wrappy@1.0.2: {} + + write-file-atomic@4.0.2: + dependencies: + imurmurhash: 0.1.4 + signal-exit: 3.0.7 + + write-file-atomic@5.0.1: + dependencies: + imurmurhash: 0.1.4 + signal-exit: 4.1.0 + + ws@7.5.10: {} + + ws@8.20.1: {} + + xml-name-validator@4.0.0: {} + + xmlchars@2.2.0: {} + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yaml@1.10.3: {} + + yaml@2.9.0: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yocto-queue@0.1.0: {} + + yup@1.7.1: + dependencies: + property-expr: 2.0.6 + tiny-case: 1.0.3 + toposort: 2.0.2 + type-fest: 2.19.0 + + zod@3.25.76: {} + + zustand@4.5.7(@types/react@18.3.28)(immer@10.2.0)(react@18.3.1): + dependencies: + use-sync-external-store: 1.6.0(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + immer: 10.2.0 + react: 18.3.1 diff --git a/benchmarks/generate-results.js b/benchmarks/generate-results.js deleted file mode 100644 index 1b62186127..0000000000 --- a/benchmarks/generate-results.js +++ /dev/null @@ -1,45 +0,0 @@ -const fs = require('fs') - -const benchDir = process.argv[2] -const outputFile = process.argv[3] - -const benchmarks = [ - ['headless', 'Headless (warm store+cache)'], - ['peek', 'Re-resolution (add dep, warm)'], - ['nolockfile', 'Full resolution (warm, no lockfile)'], - ['headless-cold', 'Headless (cold store+cache)'], - ['cold', 'Cold install (nothing warm)'], - ['gvs-warm', 'GVS warm reinstall (warm global store)'], -] - -function readResult (benchDir, name, variant) { - try { - const data = JSON.parse(fs.readFileSync(`${benchDir}/${name}-${variant}.json`, 'utf8')) - const r = data.results[0] - return `${r.mean.toFixed(3)}s ± ${r.stddev.toFixed(3)}s` - } catch (err) { - if (err && err.code !== 'ENOENT') { - console.error(`Warning: failed to read ${name}-${variant}: ${err.message}`) - } - return 'n/a' - } -} - -const lines = [ - '# Benchmark Results', - '', - '| # | Scenario | main | branch |', - '|---|---|---|---|', -] - -benchmarks.forEach(([name, label], i) => { - const mainCell = readResult(benchDir, name, 'main') - const branchCell = readResult(benchDir, name, 'branch') - lines.push(`| ${i + 1} | ${label} | ${mainCell} | ${branchCell} |`) -}) - -lines.push('') - -const output = lines.join('\n') -fs.writeFileSync(outputFile, output) -console.log(output) diff --git a/bins/linker/CHANGELOG.md b/bins/linker/CHANGELOG.md index ed9c25bbb1..3b082057e8 100644 --- a/bins/linker/CHANGELOG.md +++ b/bins/linker/CHANGELOG.md @@ -1,5 +1,43 @@ # @pnpm/link-bins +## 1100.0.10 + +### Patch Changes + +- Updated dependencies [a456dc7] +- Updated dependencies [35d2355] + - @pnpm/workspace.project-manifest-reader@1100.0.9 + - @pnpm/types@1101.2.0 + - @pnpm/bins.resolver@1100.0.5 + - @pnpm/pkg-manifest.reader@1100.0.5 + - @pnpm/pkg-manifest.utils@1100.2.1 + +## 1100.0.9 + +### Patch Changes + +- Updated dependencies [d7da112] + - @pnpm/workspace.project-manifest-reader@1100.0.8 + +## 1100.0.8 + +### Patch Changes + +- Updated dependencies [1627943] +- Updated dependencies [64afc92] + - @pnpm/pkg-manifest.utils@1100.2.0 + - @pnpm/types@1101.1.1 + - @pnpm/workspace.project-manifest-reader@1100.0.7 + - @pnpm/bins.resolver@1100.0.4 + - @pnpm/pkg-manifest.reader@1100.0.4 + +## 1100.0.7 + +### Patch Changes + +- @pnpm/pkg-manifest.utils@1100.1.4 +- @pnpm/workspace.project-manifest-reader@1100.0.6 + ## 1100.0.6 ### Patch Changes diff --git a/bins/linker/package.json b/bins/linker/package.json index 801b96618a..a04b0a4896 100644 --- a/bins/linker/package.json +++ b/bins/linker/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/bins.linker", - "version": "1100.0.6", + "version": "1100.0.10", "description": "Link bins to node_modules/.bin", "keywords": [ "pnpm", diff --git a/bins/remover/CHANGELOG.md b/bins/remover/CHANGELOG.md index c291c58334..9ee2ace847 100644 --- a/bins/remover/CHANGELOG.md +++ b/bins/remover/CHANGELOG.md @@ -1,5 +1,32 @@ # @pnpm/remove-bins +## 1100.0.6 + +### Patch Changes + +- Updated dependencies [35d2355] + - @pnpm/types@1101.2.0 + - @pnpm/bins.resolver@1100.0.5 + - @pnpm/core-loggers@1100.1.2 + - @pnpm/pkg-manifest.reader@1100.0.5 + +## 1100.0.5 + +### Patch Changes + +- Updated dependencies [64afc92] + - @pnpm/types@1101.1.1 + - @pnpm/bins.resolver@1100.0.4 + - @pnpm/core-loggers@1100.1.1 + - @pnpm/pkg-manifest.reader@1100.0.4 + +## 1100.0.4 + +### Patch Changes + +- Updated dependencies [4a79336] + - @pnpm/core-loggers@1100.1.0 + ## 1100.0.3 ### Patch Changes diff --git a/bins/remover/package.json b/bins/remover/package.json index 5f1e616f2b..46aa4fc02e 100644 --- a/bins/remover/package.json +++ b/bins/remover/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/bins.remover", - "version": "1100.0.3", + "version": "1100.0.6", "description": "Remove bins from .bin", "keywords": [ "pnpm", diff --git a/bins/resolver/CHANGELOG.md b/bins/resolver/CHANGELOG.md index e168d8bae0..501fa21664 100644 --- a/bins/resolver/CHANGELOG.md +++ b/bins/resolver/CHANGELOG.md @@ -1,5 +1,19 @@ # @pnpm/package-bins +## 1100.0.5 + +### Patch Changes + +- Updated dependencies [35d2355] + - @pnpm/types@1101.2.0 + +## 1100.0.4 + +### Patch Changes + +- Updated dependencies [64afc92] + - @pnpm/types@1101.1.1 + ## 1100.0.3 ### Patch Changes diff --git a/bins/resolver/package.json b/bins/resolver/package.json index bc851c2ba8..14885fc5a1 100644 --- a/bins/resolver/package.json +++ b/bins/resolver/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/bins.resolver", - "version": "1100.0.3", + "version": "1100.0.5", "description": "Returns bins of a package", "keywords": [ "pnpm", diff --git a/building/after-install/CHANGELOG.md b/building/after-install/CHANGELOG.md index ef6a6a361f..199abd0b83 100644 --- a/building/after-install/CHANGELOG.md +++ b/building/after-install/CHANGELOG.md @@ -1,5 +1,130 @@ # @pnpm/building.after-install +## 1101.0.17 + +### Patch Changes + +- Updated dependencies [a23956e] +- Updated dependencies [aa6149d] +- Updated dependencies [e55f4b5] +- Updated dependencies [35d2355] + - @pnpm/config.reader@1101.4.1 + - @pnpm/worker@1100.1.8 + - @pnpm/lockfile.utils@1100.0.10 + - @pnpm/types@1101.2.0 + - @pnpm/store.connection-manager@1100.2.4 + - @pnpm/bins.linker@1100.0.10 + - @pnpm/deps.graph-hasher@1100.2.2 + - @pnpm/building.pkg-requires-build@1100.0.5 + - @pnpm/building.policy@1100.0.7 + - @pnpm/config.normalize-registries@1100.0.5 + - @pnpm/core-loggers@1100.1.2 + - @pnpm/deps.path@1100.0.5 + - @pnpm/exec.lifecycle@1100.0.14 + - @pnpm/installing.context@1100.0.13 + - @pnpm/installing.modules-yaml@1100.0.6 + - @pnpm/lockfile.types@1100.0.8 + - @pnpm/lockfile.walker@1100.0.8 + - @pnpm/pkg-manifest.reader@1100.0.5 + - @pnpm/store.cafs@1100.1.7 + - @pnpm/store.controller-types@1100.1.2 + +## 1101.0.16 + +### Patch Changes + +- Updated dependencies [3b62f9d] +- Updated dependencies [212315d] + - @pnpm/config.reader@1101.4.0 + - @pnpm/bins.linker@1100.0.9 + - @pnpm/store.connection-manager@1100.2.3 + - @pnpm/exec.lifecycle@1100.0.13 + +## 1101.0.15 + +### Patch Changes + +- @pnpm/store.connection-manager@1100.2.2 + +## 1101.0.14 + +### Patch Changes + +- Updated dependencies [3687b0e] +- Updated dependencies [ced20cb] +- Updated dependencies [9cb48bb] +- Updated dependencies [d1b340f] +- Updated dependencies [64afc92] + - @pnpm/config.reader@1101.3.3 + - @pnpm/exec.lifecycle@1100.0.12 + - @pnpm/types@1101.1.1 + - @pnpm/store.connection-manager@1100.2.1 + - @pnpm/installing.context@1100.0.12 + - @pnpm/deps.graph-hasher@1100.2.1 + - @pnpm/lockfile.types@1100.0.7 + - @pnpm/lockfile.utils@1100.0.9 + - @pnpm/store.controller-types@1100.1.1 + - @pnpm/bins.linker@1100.0.8 + - @pnpm/building.pkg-requires-build@1100.0.4 + - @pnpm/building.policy@1100.0.6 + - @pnpm/config.normalize-registries@1100.0.4 + - @pnpm/core-loggers@1100.1.1 + - @pnpm/deps.path@1100.0.4 + - @pnpm/installing.modules-yaml@1100.0.5 + - @pnpm/lockfile.walker@1100.0.7 + - @pnpm/pkg-manifest.reader@1100.0.4 + - @pnpm/store.cafs@1100.1.6 + - @pnpm/worker@1100.1.7 + +## 1101.0.13 + +### Patch Changes + +- 3ddde2b: **fix**: anchor the side-effects-cache key and global-virtual-store hash to the project's script-runner Node — `engines.runtime` pin when present, shell `node` otherwise — instead of pnpm's own runtime. + + `ENGINE_NAME` (the `;;node` prefix used as the side-effects-cache key and the engine portion of the GVS hash) was computed from `process.version` — the Node that runs pnpm itself. That was wrong in two situations: + + 1. **`@pnpm/exe` SEA bundle.** The bundle has its own embedded Node, not the `node` on the user's `PATH` that actually spawns lifecycle scripts. Two pnpm installations on the same machine (one SEA, one npm-package) therefore disagreed on the cache key, partitioning the side-effects cache and the global virtual store across two Node majors even though both installs would run scripts on the same shell `node`. + 2. **`engines.runtime` / `devEngines.runtime` pin.** When a project pins a Node version via `devEngines.runtime` (pnpm v11+), pnpm downloads that Node into `node_modules/node/` and uses it to run lifecycle scripts. But the hash still anchored to whichever Node ran pnpm itself, not to the pinned Node — so two installs of the same project with two different runner Nodes would still disagree on the GVS slot path even though scripts run on the same pinned Node. + + Three changes: + + - `@pnpm/engine.runtime.system-node-version` now exports `engineName(nodeVersion?)`. Resolves the version in this order: explicit override → `getSystemNodeVersion()` (which already prefers `node --version` over `process.version` in SEA contexts) → `process.version`. + - `@pnpm/deps.graph-hasher` now exports `findRuntimeNodeVersion(snapshotKeys)` — scans an iterable of lockfile snapshot keys for a `node@runtime:` entry and returns its bare version string. `calcDepState` and `calcGraphNodeHash`/`iterateHashedGraphNodes` accept a `nodeVersion?` (in the options bag for the first, as a trailing parameter / ctx field for the others), forwarded to `engineName()`. The default (no override) preserves the pre-change behaviour. The legacy `ENGINE_NAME` constant in `@pnpm/constants` is unchanged so external consumers and existing tests keep working; in non-SEA, non-pinned contexts every value lines up. + - Every install-side caller of the graph-hasher (`@pnpm/installing.deps-resolver`, `@pnpm/installing.deps-restorer`, `@pnpm/installing.deps-installer`, `@pnpm/building.during-install`, `@pnpm/building.after-install`, `@pnpm/deps.graph-builder`) now derives the project's pinned runtime via `findRuntimeNodeVersion(Object.keys(graph))` once per invocation and threads it through. + + On upgrade, two one-time GVS slot churns are possible: + + - **SEA-pnpm users** without a runtime pin: slots that previously hashed under the embedded-Node major (e.g. `node26`) now hash under the shell-Node major (e.g. `node24`), matching what pacquet, the npm-published `pnpm` package, and any other pnpm-compatible tool already produce. + - **Projects with a `devEngines.runtime` pin**: slots that previously hashed under the runner's Node major now hash under the pinned Node major, matching what the lifecycle scripts will actually run on. + + In both cases the old slots become prune-eligible. + +- Updated dependencies [4195766] +- Updated dependencies [31538bf] +- Updated dependencies [020ac45] +- Updated dependencies [d3f8408] +- Updated dependencies [3ddde2b] +- Updated dependencies [5dc8be8] +- Updated dependencies [4a79336] +- Updated dependencies [a62f959] +- Updated dependencies [ba2c884] +- Updated dependencies [8df408c] + - @pnpm/store.controller-types@1100.1.0 + - @pnpm/store.connection-manager@1100.2.0 + - @pnpm/config.reader@1101.3.2 + - @pnpm/deps.graph-hasher@1100.2.0 + - @pnpm/core-loggers@1100.1.0 + - @pnpm/installing.context@1100.0.11 + - @pnpm/lockfile.types@1100.0.6 + - @pnpm/lockfile.utils@1100.0.8 + - @pnpm/exec.lifecycle@1100.0.11 + - @pnpm/store.cafs@1100.1.5 + - @pnpm/building.policy@1100.0.5 + - @pnpm/lockfile.walker@1100.0.6 + - @pnpm/worker@1100.1.6 + - @pnpm/bins.linker@1100.0.7 + ## 1101.0.12 ### Patch Changes diff --git a/building/after-install/package.json b/building/after-install/package.json index 6a69ad2172..1e417db8c3 100644 --- a/building/after-install/package.json +++ b/building/after-install/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/building.after-install", - "version": "1101.0.12", + "version": "1101.0.17", "description": "Rebuild packages that are already installed by running their lifecycle scripts", "keywords": [ "pnpm", @@ -41,7 +41,6 @@ "@pnpm/deps.graph-hasher": "workspace:*", "@pnpm/deps.graph-sequencer": "workspace:*", "@pnpm/deps.path": "workspace:*", - "@pnpm/engine.runtime.system-node-version": "workspace:*", "@pnpm/error": "workspace:*", "@pnpm/exec.lifecycle": "workspace:*", "@pnpm/installing.context": "workspace:*", diff --git a/building/after-install/src/index.ts b/building/after-install/src/index.ts index d201c3b74b..78f838b30c 100644 --- a/building/after-install/src/index.ts +++ b/building/after-install/src/index.ts @@ -10,10 +10,9 @@ import { WANTED_LOCKFILE, } from '@pnpm/constants' import { skippedOptionalDependencyLogger } from '@pnpm/core-loggers' -import { calcDepState, type DepsStateCache, lockfileToDepGraph } from '@pnpm/deps.graph-hasher' +import { calcDepState, type DepsStateCache, findRuntimeNodeVersion, lockfileToDepGraph } from '@pnpm/deps.graph-hasher' import { graphSequencer } from '@pnpm/deps.graph-sequencer' import * as dp from '@pnpm/deps.path' -import { findRuntimeNodeVersion } from '@pnpm/engine.runtime.system-node-version' import { PnpmError } from '@pnpm/error' import { runLifecycleHooksConcurrently, diff --git a/building/after-install/tsconfig.json b/building/after-install/tsconfig.json index 0216bd1f6d..3badc52cb6 100644 --- a/building/after-install/tsconfig.json +++ b/building/after-install/tsconfig.json @@ -39,9 +39,6 @@ { "path": "../../deps/path" }, - { - "path": "../../engine/runtime/system-node-version" - }, { "path": "../../exec/lifecycle" }, diff --git a/building/commands/CHANGELOG.md b/building/commands/CHANGELOG.md index 33e34c903d..f2910e96ca 100644 --- a/building/commands/CHANGELOG.md +++ b/building/commands/CHANGELOG.md @@ -1,5 +1,91 @@ # @pnpm/building.commands +## 1100.0.23 + +### Patch Changes + +- Updated dependencies [a23956e] +- Updated dependencies [aa6149d] +- Updated dependencies [572842a] +- Updated dependencies [35d2355] + - @pnpm/config.reader@1101.4.1 + - @pnpm/installing.commands@1100.6.0 + - @pnpm/types@1101.2.0 + - @pnpm/building.after-install@1101.0.17 + - @pnpm/store.connection-manager@1100.2.4 + - @pnpm/cli.utils@1101.0.8 + - @pnpm/config.writer@1100.0.10 + - @pnpm/deps.path@1100.0.5 + - @pnpm/installing.modules-yaml@1100.0.6 + - @pnpm/workspace.projects-sorter@1100.0.4 + +## 1100.0.22 + +### Patch Changes + +- Updated dependencies [3b62f9d] +- Updated dependencies [212315d] + - @pnpm/config.reader@1101.4.0 + - @pnpm/installing.commands@1100.5.0 + - @pnpm/cli.utils@1101.0.7 + - @pnpm/building.after-install@1101.0.16 + - @pnpm/store.connection-manager@1100.2.3 + +## 1100.0.21 + +### Patch Changes + +- Updated dependencies [881a865] + - @pnpm/installing.commands@1100.4.2 + +## 1100.0.20 + +### Patch Changes + +- @pnpm/installing.commands@1100.4.1 +- @pnpm/store.connection-manager@1100.2.2 +- @pnpm/building.after-install@1101.0.15 + +## 1100.0.19 + +### Patch Changes + +- Updated dependencies [3687b0e] +- Updated dependencies [ced20cb] +- Updated dependencies [a620557] +- Updated dependencies [d1b340f] +- Updated dependencies [b206a15] +- Updated dependencies [64afc92] + - @pnpm/config.reader@1101.3.3 + - @pnpm/installing.commands@1100.4.0 + - @pnpm/types@1101.1.1 + - @pnpm/building.after-install@1101.0.14 + - @pnpm/store.connection-manager@1100.2.1 + - @pnpm/cli.utils@1101.0.6 + - @pnpm/config.writer@1100.0.9 + - @pnpm/deps.path@1100.0.4 + - @pnpm/installing.modules-yaml@1100.0.5 + - @pnpm/workspace.projects-sorter@1100.0.3 + +## 1100.0.18 + +### Patch Changes + +- Updated dependencies [4195766] +- Updated dependencies [31538bf] +- Updated dependencies [020ac45] +- Updated dependencies [d3f8408] +- Updated dependencies [3ddde2b] +- Updated dependencies [a62f959] +- Updated dependencies [ba2c884] +- Updated dependencies [8df408c] + - @pnpm/installing.commands@1100.3.0 + - @pnpm/store.connection-manager@1100.2.0 + - @pnpm/config.reader@1101.3.2 + - @pnpm/building.after-install@1101.0.13 + - @pnpm/cli.utils@1101.0.5 + - @pnpm/config.writer@1100.0.8 + ## 1100.0.17 ### Patch Changes diff --git a/building/commands/package.json b/building/commands/package.json index 33c5d08857..51aaa3b3e6 100644 --- a/building/commands/package.json +++ b/building/commands/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/building.commands", - "version": "1100.0.17", + "version": "1100.0.23", "description": "Commands for rebuilding and managing dependency builds", "keywords": [ "pnpm", diff --git a/building/during-install/CHANGELOG.md b/building/during-install/CHANGELOG.md index 4d5fc17439..58f05457ed 100644 --- a/building/during-install/CHANGELOG.md +++ b/building/during-install/CHANGELOG.md @@ -1,5 +1,101 @@ # @pnpm/building.during-install +## 1101.0.14 + +### Patch Changes + +- Updated dependencies [a23956e] +- Updated dependencies [aa6149d] +- Updated dependencies [26a7d63] +- Updated dependencies [35d2355] + - @pnpm/config.reader@1101.4.1 + - @pnpm/worker@1100.1.8 + - @pnpm/patching.apply-patch@1100.0.1 + - @pnpm/types@1101.2.0 + - @pnpm/bins.linker@1100.0.10 + - @pnpm/deps.graph-hasher@1100.2.2 + - @pnpm/core-loggers@1100.1.2 + - @pnpm/deps.path@1100.0.5 + - @pnpm/exec.lifecycle@1100.0.14 + - @pnpm/pkg-manifest.reader@1100.0.5 + - @pnpm/store.controller-types@1100.1.2 + - @pnpm/fs.hard-link-dir@1100.0.1 + +## 1101.0.13 + +### Patch Changes + +- Updated dependencies [3b62f9d] +- Updated dependencies [212315d] + - @pnpm/config.reader@1101.4.0 + - @pnpm/bins.linker@1100.0.9 + - @pnpm/exec.lifecycle@1100.0.13 + +## 1101.0.12 + +### Patch Changes + +- Updated dependencies [3687b0e] +- Updated dependencies [ced20cb] +- Updated dependencies [9cb48bb] +- Updated dependencies [d1b340f] +- Updated dependencies [64afc92] + - @pnpm/config.reader@1101.3.3 + - @pnpm/exec.lifecycle@1100.0.12 + - @pnpm/types@1101.1.1 + - @pnpm/deps.graph-hasher@1100.2.1 + - @pnpm/store.controller-types@1100.1.1 + - @pnpm/bins.linker@1100.0.8 + - @pnpm/core-loggers@1100.1.1 + - @pnpm/deps.path@1100.0.4 + - @pnpm/pkg-manifest.reader@1100.0.4 + - @pnpm/worker@1100.1.7 + - @pnpm/fs.hard-link-dir@1100.0.1 + - @pnpm/patching.apply-patch@1100.0.0 + +## 1101.0.11 + +### Patch Changes + +- 3ddde2b: **fix**: anchor the side-effects-cache key and global-virtual-store hash to the project's script-runner Node — `engines.runtime` pin when present, shell `node` otherwise — instead of pnpm's own runtime. + + `ENGINE_NAME` (the `;;node` prefix used as the side-effects-cache key and the engine portion of the GVS hash) was computed from `process.version` — the Node that runs pnpm itself. That was wrong in two situations: + + 1. **`@pnpm/exe` SEA bundle.** The bundle has its own embedded Node, not the `node` on the user's `PATH` that actually spawns lifecycle scripts. Two pnpm installations on the same machine (one SEA, one npm-package) therefore disagreed on the cache key, partitioning the side-effects cache and the global virtual store across two Node majors even though both installs would run scripts on the same shell `node`. + 2. **`engines.runtime` / `devEngines.runtime` pin.** When a project pins a Node version via `devEngines.runtime` (pnpm v11+), pnpm downloads that Node into `node_modules/node/` and uses it to run lifecycle scripts. But the hash still anchored to whichever Node ran pnpm itself, not to the pinned Node — so two installs of the same project with two different runner Nodes would still disagree on the GVS slot path even though scripts run on the same pinned Node. + + Three changes: + + - `@pnpm/engine.runtime.system-node-version` now exports `engineName(nodeVersion?)`. Resolves the version in this order: explicit override → `getSystemNodeVersion()` (which already prefers `node --version` over `process.version` in SEA contexts) → `process.version`. + - `@pnpm/deps.graph-hasher` now exports `findRuntimeNodeVersion(snapshotKeys)` — scans an iterable of lockfile snapshot keys for a `node@runtime:` entry and returns its bare version string. `calcDepState` and `calcGraphNodeHash`/`iterateHashedGraphNodes` accept a `nodeVersion?` (in the options bag for the first, as a trailing parameter / ctx field for the others), forwarded to `engineName()`. The default (no override) preserves the pre-change behaviour. The legacy `ENGINE_NAME` constant in `@pnpm/constants` is unchanged so external consumers and existing tests keep working; in non-SEA, non-pinned contexts every value lines up. + - Every install-side caller of the graph-hasher (`@pnpm/installing.deps-resolver`, `@pnpm/installing.deps-restorer`, `@pnpm/installing.deps-installer`, `@pnpm/building.during-install`, `@pnpm/building.after-install`, `@pnpm/deps.graph-builder`) now derives the project's pinned runtime via `findRuntimeNodeVersion(Object.keys(graph))` once per invocation and threads it through. + + On upgrade, two one-time GVS slot churns are possible: + + - **SEA-pnpm users** without a runtime pin: slots that previously hashed under the embedded-Node major (e.g. `node26`) now hash under the shell-Node major (e.g. `node24`), matching what pacquet, the npm-published `pnpm` package, and any other pnpm-compatible tool already produce. + - **Projects with a `devEngines.runtime` pin**: slots that previously hashed under the runner's Node major now hash under the pinned Node major, matching what the lifecycle scripts will actually run on. + + In both cases the old slots become prune-eligible. + +- Updated dependencies [4195766] +- Updated dependencies [020ac45] +- Updated dependencies [d3f8408] +- Updated dependencies [3ddde2b] +- Updated dependencies [5dc8be8] +- Updated dependencies [4a79336] +- Updated dependencies [a62f959] +- Updated dependencies [ba2c884] +- Updated dependencies [8df408c] + - @pnpm/store.controller-types@1100.1.0 + - @pnpm/config.reader@1101.3.2 + - @pnpm/deps.graph-hasher@1100.2.0 + - @pnpm/core-loggers@1100.1.0 + - @pnpm/exec.lifecycle@1100.0.11 + - @pnpm/worker@1100.1.6 + - @pnpm/bins.linker@1100.0.7 + - @pnpm/fs.hard-link-dir@1100.0.1 + - @pnpm/patching.apply-patch@1100.0.0 + ## 1101.0.10 ### Patch Changes diff --git a/building/during-install/package.json b/building/during-install/package.json index 7612b22521..548343711b 100644 --- a/building/during-install/package.json +++ b/building/during-install/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/building.during-install", - "version": "1101.0.10", + "version": "1101.0.14", "description": "Build packages in node_modules", "keywords": [ "pnpm", @@ -39,7 +39,6 @@ "@pnpm/deps.graph-hasher": "workspace:*", "@pnpm/deps.graph-sequencer": "workspace:*", "@pnpm/deps.path": "workspace:*", - "@pnpm/engine.runtime.system-node-version": "workspace:*", "@pnpm/error": "workspace:*", "@pnpm/exec.lifecycle": "workspace:*", "@pnpm/fs.hard-link-dir": "workspace:*", diff --git a/building/during-install/src/index.ts b/building/during-install/src/index.ts index 4715786d09..e9f5b2f7ac 100644 --- a/building/during-install/src/index.ts +++ b/building/during-install/src/index.ts @@ -6,8 +6,7 @@ import util from 'node:util' import { linkBins, linkBinsOfPackages } from '@pnpm/bins.linker' import { getWorkspaceConcurrency } from '@pnpm/config.reader' import { skippedOptionalDependencyLogger } from '@pnpm/core-loggers' -import { calcDepState, type DepsStateCache } from '@pnpm/deps.graph-hasher' -import { findRuntimeNodeVersion } from '@pnpm/engine.runtime.system-node-version' +import { calcDepState, type DepsStateCache, findRuntimeNodeVersion } from '@pnpm/deps.graph-hasher' import { PnpmError } from '@pnpm/error' import { runPostinstallHooks } from '@pnpm/exec.lifecycle' import { logger } from '@pnpm/logger' diff --git a/building/during-install/tsconfig.json b/building/during-install/tsconfig.json index 4d758c2486..d1a31c7d32 100644 --- a/building/during-install/tsconfig.json +++ b/building/during-install/tsconfig.json @@ -36,9 +36,6 @@ { "path": "../../deps/path" }, - { - "path": "../../engine/runtime/system-node-version" - }, { "path": "../../exec/lifecycle" }, diff --git a/building/pkg-requires-build/CHANGELOG.md b/building/pkg-requires-build/CHANGELOG.md index 9b0b1835c0..ada34d7c71 100644 --- a/building/pkg-requires-build/CHANGELOG.md +++ b/building/pkg-requires-build/CHANGELOG.md @@ -1,5 +1,19 @@ # @pnpm/building.pkg-requires-build +## 1100.0.5 + +### Patch Changes + +- Updated dependencies [35d2355] + - @pnpm/types@1101.2.0 + +## 1100.0.4 + +### Patch Changes + +- Updated dependencies [64afc92] + - @pnpm/types@1101.1.1 + ## 1100.0.3 ### Patch Changes diff --git a/building/pkg-requires-build/package.json b/building/pkg-requires-build/package.json index 6f7b60d628..744c866c49 100644 --- a/building/pkg-requires-build/package.json +++ b/building/pkg-requires-build/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/building.pkg-requires-build", - "version": "1100.0.3", + "version": "1100.0.5", "description": "Checks if a package requires to be built", "keywords": [ "pnpm", diff --git a/building/policy/CHANGELOG.md b/building/policy/CHANGELOG.md index c578aeffe7..b275f93b9b 100644 --- a/building/policy/CHANGELOG.md +++ b/building/policy/CHANGELOG.md @@ -1,5 +1,30 @@ # @pnpm/building.policy +## 1100.0.7 + +### Patch Changes + +- Updated dependencies [35d2355] + - @pnpm/types@1101.2.0 + - @pnpm/config.version-policy@1100.1.2 + - @pnpm/deps.path@1100.0.5 + +## 1100.0.6 + +### Patch Changes + +- Updated dependencies [64afc92] + - @pnpm/types@1101.1.1 + - @pnpm/config.version-policy@1100.1.1 + - @pnpm/deps.path@1100.0.4 + +## 1100.0.5 + +### Patch Changes + +- Updated dependencies [b6e2c8c] + - @pnpm/config.version-policy@1100.1.0 + ## 1100.0.4 ### Patch Changes diff --git a/building/policy/package.json b/building/policy/package.json index 9c67db607d..d967c33dc9 100644 --- a/building/policy/package.json +++ b/building/policy/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/building.policy", - "version": "1100.0.4", + "version": "1100.0.7", "description": "Create a function for filtering out dependencies that are not allowed to be built", "keywords": [ "pnpm", diff --git a/cache/api/CHANGELOG.md b/cache/api/CHANGELOG.md index b997d5b5c6..48ebcd4ac8 100644 --- a/cache/api/CHANGELOG.md +++ b/cache/api/CHANGELOG.md @@ -1,5 +1,60 @@ # @pnpm/cache.api +## 1100.0.17 + +### Patch Changes + +- Updated dependencies [a23956e] +- Updated dependencies [35d2355] +- Updated dependencies [0721d64] + - @pnpm/config.reader@1101.4.1 + - @pnpm/resolving.npm-resolver@1101.3.3 + - @pnpm/store.cafs@1100.1.7 + +## 1100.0.16 + +### Patch Changes + +- Updated dependencies [3b62f9d] +- Updated dependencies [212315d] + - @pnpm/config.reader@1101.4.0 + - @pnpm/resolving.npm-resolver@1101.3.2 + +## 1100.0.15 + +### Patch Changes + +- @pnpm/resolving.npm-resolver@1101.3.1 + +## 1100.0.14 + +### Patch Changes + +- Updated dependencies [3687b0e] +- Updated dependencies [ced20cb] +- Updated dependencies [d1b340f] +- Updated dependencies [3a54205] +- Updated dependencies [1627943] + - @pnpm/config.reader@1101.3.3 + - @pnpm/resolving.npm-resolver@1101.3.0 + - @pnpm/store.cafs@1100.1.6 + +## 1100.0.13 + +### Patch Changes + +- Updated dependencies [963861c] +- Updated dependencies [4195766] +- Updated dependencies [31538bf] +- Updated dependencies [020ac45] +- Updated dependencies [d3f8408] +- Updated dependencies [a62f959] +- Updated dependencies [ba2c884] +- Updated dependencies [8df408c] + - @pnpm/resolving.npm-resolver@1101.2.0 + - @pnpm/config.reader@1101.3.2 + - @pnpm/store.cafs@1100.1.5 + ## 1100.0.12 ### Patch Changes diff --git a/cache/api/package.json b/cache/api/package.json index 9b5999398e..6e83b42684 100644 --- a/cache/api/package.json +++ b/cache/api/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/cache.api", - "version": "1100.0.12", + "version": "1100.0.17", "description": "API for controlling the cache", "keywords": [ "pnpm", diff --git a/cache/commands/CHANGELOG.md b/cache/commands/CHANGELOG.md index c46ad46eb8..374973cb54 100644 --- a/cache/commands/CHANGELOG.md +++ b/cache/commands/CHANGELOG.md @@ -1,5 +1,55 @@ # @pnpm/cache.commands +## 1100.0.18 + +### Patch Changes + +- Updated dependencies [a23956e] +- Updated dependencies [35d2355] + - @pnpm/config.reader@1101.4.1 + - @pnpm/cache.api@1100.0.17 + - @pnpm/cli.utils@1101.0.8 + +## 1100.0.17 + +### Patch Changes + +- Updated dependencies [3b62f9d] +- Updated dependencies [212315d] + - @pnpm/config.reader@1101.4.0 + - @pnpm/cli.utils@1101.0.7 + - @pnpm/cache.api@1100.0.16 + +## 1100.0.16 + +### Patch Changes + +- @pnpm/cache.api@1100.0.15 + +## 1100.0.15 + +### Patch Changes + +- Updated dependencies [3687b0e] +- Updated dependencies [ced20cb] +- Updated dependencies [d1b340f] + - @pnpm/config.reader@1101.3.3 + - @pnpm/cache.api@1100.0.14 + - @pnpm/cli.utils@1101.0.6 + +## 1100.0.14 + +### Patch Changes + +- Updated dependencies [020ac45] +- Updated dependencies [d3f8408] +- Updated dependencies [a62f959] +- Updated dependencies [ba2c884] +- Updated dependencies [8df408c] + - @pnpm/config.reader@1101.3.2 + - @pnpm/cache.api@1100.0.13 + - @pnpm/cli.utils@1101.0.5 + ## 1100.0.13 ### Patch Changes diff --git a/cache/commands/package.json b/cache/commands/package.json index 64380a416a..8f6201977b 100644 --- a/cache/commands/package.json +++ b/cache/commands/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/cache.commands", - "version": "1100.0.13", + "version": "1100.0.18", "description": "Commands for controlling the cache", "keywords": [ "pnpm", diff --git a/cli/commands/CHANGELOG.md b/cli/commands/CHANGELOG.md index 13ecad8d56..870f89f3e6 100644 --- a/cli/commands/CHANGELOG.md +++ b/cli/commands/CHANGELOG.md @@ -1,5 +1,52 @@ # @pnpm/cli.commands +## 1100.0.16 + +### Patch Changes + +- Updated dependencies [e8b3ae1] +- Updated dependencies [a23956e] +- Updated dependencies [35d2355] + - @pnpm/workspace.projects-reader@1101.0.8 + - @pnpm/config.reader@1101.4.1 + - @pnpm/cli.utils@1101.0.8 + - @pnpm/workspace.workspace-manifest-reader@1100.0.5 + +## 1100.0.15 + +### Patch Changes + +- Updated dependencies [3b62f9d] +- Updated dependencies [212315d] + - @pnpm/config.reader@1101.4.0 + - @pnpm/cli.utils@1101.0.7 + - @pnpm/workspace.projects-reader@1101.0.7 + +## 1100.0.14 + +### Patch Changes + +- Updated dependencies [3687b0e] +- Updated dependencies [ced20cb] +- Updated dependencies [d1b340f] + - @pnpm/config.reader@1101.3.3 + - @pnpm/cli.utils@1101.0.6 + - @pnpm/workspace.projects-reader@1101.0.6 + - @pnpm/workspace.workspace-manifest-reader@1100.0.4 + +## 1100.0.13 + +### Patch Changes + +- Updated dependencies [020ac45] +- Updated dependencies [d3f8408] +- Updated dependencies [a62f959] +- Updated dependencies [ba2c884] +- Updated dependencies [8df408c] + - @pnpm/config.reader@1101.3.2 + - @pnpm/cli.utils@1101.0.5 + - @pnpm/workspace.projects-reader@1101.0.5 + ## 1100.0.12 ### Patch Changes diff --git a/cli/commands/package.json b/cli/commands/package.json index 613ac8df5e..9591016dca 100644 --- a/cli/commands/package.json +++ b/cli/commands/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/cli.commands", - "version": "1100.0.12", + "version": "1100.0.16", "description": "Commands for pnpm CLI", "keywords": [ "pnpm", diff --git a/cli/default-reporter/CHANGELOG.md b/cli/default-reporter/CHANGELOG.md index 1716ecb2aa..fcaa43a4ec 100644 --- a/cli/default-reporter/CHANGELOG.md +++ b/cli/default-reporter/CHANGELOG.md @@ -1,5 +1,69 @@ # @pnpm/default-reporter +## 1100.2.3 + +### Patch Changes + +- Updated dependencies [a23956e] +- Updated dependencies [35d2355] + - @pnpm/config.reader@1101.4.1 + - @pnpm/types@1101.2.0 + - @pnpm/cli.meta@1100.0.5 + - @pnpm/core-loggers@1100.1.2 + - @pnpm/deps.inspection.peers-issues-renderer@1100.0.3 + +## 1100.2.2 + +### Patch Changes + +- Updated dependencies [3b62f9d] +- Updated dependencies [212315d] + - @pnpm/config.reader@1101.4.0 + +## 1100.2.1 + +### Patch Changes + +- Updated dependencies [3687b0e] +- Updated dependencies [ced20cb] +- Updated dependencies [d1b340f] +- Updated dependencies [64afc92] + - @pnpm/config.reader@1101.3.3 + - @pnpm/types@1101.1.1 + - @pnpm/cli.meta@1100.0.4 + - @pnpm/core-loggers@1100.1.1 + - @pnpm/deps.inspection.peers-issues-renderer@1100.0.2 + +## 1100.2.0 + +### Minor Changes + +- 4a79336: The lockfile verifier added in #11705 now emits `pnpm:lockfile-verification` log events (`status: 'started' | 'done'`) around the registry round-trip pass, and the default reporter renders them as a transient progress line so users can see that pnpm is doing work — on a cold registry cache the round-trip can take a noticeable beat, and the previous behavior was complete silence followed by either a long pause or an error. The cached short-circuit stays silent (no logs when no work happens), and the `done` line carries the number of distinct entries that were checked plus the elapsed time. + + Pacquet parity: not ported — pacquet doesn't carry the lockfile verifier yet (see the parity note on #11705). + +### Patch Changes + +- 4195766: Tightened the `minimumReleaseAge` story so the bypass becomes explicit on disk instead of silent, and removed the discover-by-loop dance for strict-mode users: + + 1. Fresh resolutions in loose mode (`minimumReleaseAgeStrict: false`) that fall back to a version newer than the cutoff auto-collect the picked `name@version` into the workspace manifest's `minimumReleaseAgeExclude`. A single info message lists the additions; entries already on the list are left alone. + 2. The post-resolution lockfile verifier introduced in #11583 now runs in loose mode too — every accepted-immature pin must be on `minimumReleaseAgeExclude`, just like strict mode requires. A lockfile produced under a weaker (or absent) policy that still has immature entries is rejected the same way strict mode would reject it. + 3. **Strict mode (interactive)** no longer aborts on the first immature pick. The resolver gathers every immature direct _and_ transitive in one pass; before peer-dependency resolution runs, pnpm prompts the user with the full list and asks whether to add them all to `minimumReleaseAgeExclude` and proceed. Approve → install continues and the workspace manifest is written at the end. Decline → resolution aborts before the lockfile or package.json is touched (tarballs already in the store stay, since the store is idempotent). This closes the [#10488](https://github.com/pnpm/pnpm/issues/10488) loop where security bumps to packages with platform-specific transitives (e.g. `next` + the `@next/swc-*` shims) made users re-run `pnpm add` once per transitive. + 4. **Strict mode (non-interactive / CI)** now aborts with the full immature set in the error message instead of the first pick. The resolver always collects every immature direct + transitive; the install command then throws `ERR_PNPM_NO_MATURE_MATCHING_VERSION` listing each entry's `name@version` and publish time. Deterministic CI behavior is preserved (same exit code, same error code), but the error pinpoints every offending entry instead of forcing the discover-by-loop dance. The expected workflow is interactive approval locally → the lockfile + workspace manifest get committed → CI runs cleanly against the populated exclude list. + + 5. **The lockfile verifier now also covers `trustPolicy: 'no-downgrade'`.** The same post-resolution gate that re-checks `minimumReleaseAge` on lockfile entries now re-runs `failIfTrustDowngraded` for every npm-registry entry whose name isn't on `trustPolicyExclude`. The two checks share a single full-metadata fetch per package, so the extra coverage doesn't cost an extra round trip when both policies are active. Resolver-time trust checks still run as before — this just closes the gap when an entry bypasses resolution (peek path, `--frozen-lockfile`, restored CI cache). + + Pacquet parity: not ported — pacquet's `minimumReleaseAge` policy is itself only stubbed today (see `pacquet/crates/package-manager/src/version_policy.rs`). The auto-exclude, loose-mode verifier, prompt, and the new trust-policy verifier check will travel with the broader policy port whenever that happens. + +- Updated dependencies [020ac45] +- Updated dependencies [d3f8408] +- Updated dependencies [4a79336] +- Updated dependencies [a62f959] +- Updated dependencies [ba2c884] +- Updated dependencies [8df408c] + - @pnpm/config.reader@1101.3.2 + - @pnpm/core-loggers@1100.1.0 + ## 1100.1.2 ### Patch Changes diff --git a/cli/default-reporter/package.json b/cli/default-reporter/package.json index 94b44452bf..a4e74c1110 100644 --- a/cli/default-reporter/package.json +++ b/cli/default-reporter/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/cli.default-reporter", - "version": "1100.1.2", + "version": "1100.2.3", "description": "The default reporter of pnpm", "keywords": [ "pnpm", diff --git a/cli/default-reporter/src/index.ts b/cli/default-reporter/src/index.ts index 65d13ba3a5..94015efea5 100644 --- a/cli/default-reporter/src/index.ts +++ b/cli/default-reporter/src/index.ts @@ -127,6 +127,7 @@ export function toOutput$ ( const deprecationPushStream = new Rx.Subject() const summaryPushStream = new Rx.Subject() const lifecyclePushStream = new Rx.Subject() + const lockfileVerificationPushStream = new Rx.Subject() const statsPushStream = new Rx.Subject() const packageImportMethodPushStream = new Rx.Subject() const installCheckPushStream = new Rx.Subject() @@ -170,6 +171,9 @@ export function toOutput$ ( case 'pnpm:lifecycle': lifecyclePushStream.next(log) break + case 'pnpm:lockfile-verification': + lockfileVerificationPushStream.next(log) + break case 'pnpm:stats': statsPushStream.next(log) break @@ -243,6 +247,7 @@ export function toOutput$ ( ignoredScripts: Rx.from(ignoredScriptsPushStream), lifecycle: Rx.from(lifecyclePushStream), link: Rx.from(linkPushStream), + lockfileVerification: Rx.from(lockfileVerificationPushStream), other, packageImportMethod: Rx.from(packageImportMethodPushStream), packageManifest: Rx.from(packageManifestPushStream), diff --git a/cli/default-reporter/src/reportError.ts b/cli/default-reporter/src/reportError.ts index 53a791faa2..424dacd47d 100644 --- a/cli/default-reporter/src/reportError.ts +++ b/cli/default-reporter/src/reportError.ts @@ -72,7 +72,12 @@ function getErrorInfo (logObj: Log, config?: Config): ErrorInfo | null { return { title: err.message, body: 'If you cannot fix this registry issue, then set "resolution-mode" to "highest".' } case 'ERR_PNPM_NO_MATCHING_VERSION': case 'ERR_PNPM_NO_MATURE_MATCHING_VERSION': - return formatNoMatchingVersion(err, logObj as unknown as { packageMeta: PackageMeta, immatureVersion?: string }) + // ERR_PNPM_NO_MATURE_MATCHING_VERSION used to come from the resolver + // with `packageMeta` attached; it now comes from the install / dlx / + // self-update callers as a plain PnpmError once the resolver has + // surfaced the violations. `packageMeta` may be undefined, in which + // case the formatter falls back to the bare title+message. + return formatNoMatchingVersion(err, logObj as unknown as { packageMeta?: PackageMeta }) case 'ERR_PNPM_RECURSIVE_FAIL': return formatRecursiveCommandSummary(logObj as any) // eslint-disable-line @typescript-eslint/no-explicit-any case 'ERR_PNPM_BAD_TARBALL_SIZE': @@ -134,11 +139,18 @@ interface PackageMeta { time?: Record } -function formatNoMatchingVersion (err: Error, msg: { packageMeta: PackageMeta, immatureVersion?: string }) { - const meta: PackageMeta = msg.packageMeta +function formatNoMatchingVersion (err: Error, msg: { packageMeta?: PackageMeta }) { + // Errors raised by the install/dlx/self-update layer after the resolver + // surfaces violations may not carry the original packageMeta. In that + // case the error message alone already names every offending entry, + // so we just echo it through without the registry-metadata appendix. + const meta = msg.packageMeta + if (!meta) { + return { title: err.message } + } const latestVersion = meta['dist-tags'].latest let output = `The latest release of ${meta.name} is "${latestVersion}".` - const latestTime = msg.packageMeta.time?.[latestVersion] + const latestTime = meta.time?.[latestVersion] if (latestTime) { output += ` Published at ${stringifyDate(latestTime)}` } @@ -150,7 +162,7 @@ function formatNoMatchingVersion (err: Error, msg: { packageMeta: PackageMeta, i if (tag !== 'latest') { const version = meta['dist-tags'][tag] output += ` * ${tag}: ${version}` - const time = msg.packageMeta.time?.[version] + const time = meta.time?.[version] if (time) { output += ` published at ${stringifyDate(time)}` } @@ -161,10 +173,6 @@ function formatNoMatchingVersion (err: Error, msg: { packageMeta: PackageMeta, i output += `${EOL}If you need the full list of all ${Object.keys(meta.versions).length} published versions run "pnpm view ${meta.name} versions".` - if (msg.immatureVersion) { - output += `${EOL}${EOL}If you want to install the matched version ignoring the time it was published, you can add the package name to the minimumReleaseAgeExclude setting. Read more about it: https://pnpm.io/settings#minimumreleaseageexclude` - } - return { title: err.message, body: output, diff --git a/cli/default-reporter/src/reporterForClient/index.ts b/cli/default-reporter/src/reporterForClient/index.ts index ae4c79b2f5..8e3af0bd18 100644 --- a/cli/default-reporter/src/reporterForClient/index.ts +++ b/cli/default-reporter/src/reporterForClient/index.ts @@ -13,6 +13,7 @@ import { reportIgnoredBuilds } from './reportIgnoredBuilds.js' import { reportInstallChecks } from './reportInstallChecks.js' import { reportInstallingConfigDeps } from './reportInstallingConfigDeps.js' import { reportLifecycleScripts } from './reportLifecycleScripts.js' +import { reportLockfileVerification } from './reportLockfileVerification.js' import { LOG_LEVEL_NUMBER, reportMisc } from './reportMisc.js' import { reportPeerDependencyIssues } from './reportPeerDependencyIssues.js' import { reportProgress } from './reportProgress.js' @@ -41,6 +42,7 @@ export function reporterForClient ( deprecation: Rx.Observable summary: Rx.Observable lifecycle: Rx.Observable + lockfileVerification: Rx.Observable stats: Rx.Observable installCheck: Rx.Observable installingConfigDeps: Rx.Observable @@ -130,6 +132,10 @@ export function reporterForClient ( }), reportInstallChecks(log$.installCheck, { cwd }), reportInstallingConfigDeps(log$.installingConfigDeps), + reportLockfileVerification(log$.lockfileVerification, { + cwd, + workspaceDir: opts.pnpmConfig?.workspaceDir, + }), reportScope(log$.scope, { isRecursive: opts.isRecursive, cmd: opts.cmd }), reportSkippedOptionalDependencies(log$.skippedOptionalDependency, { cwd }), reportHooks(log$.hook, { cwd, isRecursive: opts.isRecursive }), diff --git a/cli/default-reporter/src/reporterForClient/reportLockfileVerification.ts b/cli/default-reporter/src/reporterForClient/reportLockfileVerification.ts new file mode 100644 index 0000000000..c6d771032d --- /dev/null +++ b/cli/default-reporter/src/reporterForClient/reportLockfileVerification.ts @@ -0,0 +1,77 @@ +import path from 'node:path' + +import type { LockfileVerificationLog } from '@pnpm/core-loggers' +import chalk from 'chalk' +import normalize from 'normalize-path' +import prettyMs from 'pretty-ms' +import * as Rx from 'rxjs' +import { map } from 'rxjs/operators' + +export interface ReportLockfileVerificationOptions { + cwd: string + /** + * The workspace root, when one exists. Used as the "expected" + * location for the lockfile — when the lockfile lives there, the + * path is implied and we don't repeat it in the rendered message. + * Falls back to `cwd` for single-project installs. + */ + workspaceDir?: string +} + +export function reportLockfileVerification ( + lockfileVerification$: Rx.Observable, + opts: ReportLockfileVerificationOptions +): Rx.Observable> { + const expectedDir = opts.workspaceDir ?? opts.cwd + // A single inner observable so the `done` message overwrites the + // transient `started` message in ansi-diff mode. In appendOnly mode + // both lines are printed. + return Rx.of(lockfileVerification$.pipe( + map((log) => { + const path_ = formatLockfilePath(log.lockfilePath, opts.cwd, expectedDir) + const entries = `${log.entries} ${log.entries === 1 ? 'entry' : 'entries'}` + switch (log.status) { + case 'started': + return { + msg: `${chalk.cyan('?')} Verifying lockfile${path_} against supply-chain policies (${entries})...`, + } + case 'done': + return { + msg: `${chalk.green('✓')} Lockfile${path_} passes supply-chain policies (${entries} in ${prettyMs(log.elapsedMs)})`, + } + case 'failed': + // Brief one-liner so the transient `started` frame doesn't + // stay on screen above the detailed PnpmError block that the + // error reporter prints next. + return { + msg: `${chalk.red('✗')} Lockfile${path_} failed supply-chain policy check (${entries} in ${prettyMs(log.elapsedMs)})`, + } + } + }) + )) +} + +// Returns a leading-space-prefixed `at ` suffix only when the +// lockfile sits outside the obvious project/workspace root — otherwise +// the path is implied and printing it would just add noise to every +// install. Empty string when the path is omitted or matches the +// expected location. +// +// Uses `path.relative` rather than a strict `===` between +// `path.dirname(lockfilePath)` and `expectedDir`: relative path +// computation normalizes slash direction and trailing separators, so +// a workspaceDir like `C:/repo/` correctly matches a lockfilePath at +// `C:\repo\pnpm-lock.yaml` on Windows. The lockfile is considered +// "inside the expected dir" when the relative path is a bare file +// name (no separator) that doesn't escape upward. +function formatLockfilePath ( + lockfilePath: string | undefined, + cwd: string, + expectedDir: string +): string { + if (lockfilePath == null) return '' + const fromExpected = path.relative(expectedDir, lockfilePath) + const isDirectChild = !fromExpected.includes(path.sep) && !fromExpected.startsWith('..') + if (isDirectChild) return '' + return ` at ${normalize(path.relative(cwd, lockfilePath))}` +} diff --git a/cli/default-reporter/test/reportingLockfileVerification.ts b/cli/default-reporter/test/reportingLockfileVerification.ts new file mode 100644 index 0000000000..60d1339bd9 --- /dev/null +++ b/cli/default-reporter/test/reportingLockfileVerification.ts @@ -0,0 +1,150 @@ +import path from 'node:path' +import { stripVTControlCharacters as stripAnsi } from 'node:util' + +import { expect, test } from '@jest/globals' +import { toOutput$ } from '@pnpm/cli.default-reporter' +import type { Config, ConfigContext } from '@pnpm/config.reader' +import { lockfileVerificationLogger } from '@pnpm/core-loggers' +import { createStreamParser } from '@pnpm/logger' +import { firstValueFrom, take, toArray } from 'rxjs' + +test('prints lockfile verification in-progress and completion messages', async () => { + const cwd = '/repo' + const output$ = toOutput$({ + context: { + argv: ['install'], + config: { dir: cwd } as Config & ConfigContext, + }, + streamParser: createStreamParser(), + }) + + // Subscribe before emitting so we capture both the started and the + // done frame in ansi-diff mode. + const frames = firstValueFrom(output$.pipe(take(2), toArray())) + + const lockfilePath = path.join(cwd, 'pnpm-lock.yaml') + lockfileVerificationLogger.debug({ status: 'started', entries: 234, lockfilePath }) + lockfileVerificationLogger.debug({ + status: 'done', + entries: 234, + elapsedMs: 1234, + lockfilePath, + }) + + const [started, done] = await frames + expect(stripAnsi(started)).toBe('? Verifying lockfile against supply-chain policies (234 entries)...') + expect(stripAnsi(done)).toBe('✓ Lockfile passes supply-chain policies (234 entries in 1.2s)') +}) + +test('uses singular noun for one entry', async () => { + const output$ = toOutput$({ + context: { argv: ['install'] }, + streamParser: createStreamParser(), + }) + + const frames = firstValueFrom(output$.pipe(take(2), toArray())) + + lockfileVerificationLogger.debug({ status: 'started', entries: 1 }) + lockfileVerificationLogger.debug({ + status: 'done', + entries: 1, + elapsedMs: 42, + }) + + const [started, done] = await frames + expect(stripAnsi(started)).toBe('? Verifying lockfile against supply-chain policies (1 entry)...') + expect(stripAnsi(done)).toBe('✓ Lockfile passes supply-chain policies (1 entry in 42ms)') +}) + +test('prints relative path when lockfile lives outside the workspace root', async () => { + const cwd = '/repo/packages/app' + const workspaceDir = '/repo' + const output$ = toOutput$({ + context: { + argv: ['install'], + config: { dir: cwd, workspaceDir } as Config & ConfigContext, + }, + streamParser: createStreamParser(), + }) + + const frames = firstValueFrom(output$.pipe(take(2), toArray())) + + // Lockfile lives in a sibling dir, not at the workspace root. + const lockfilePath = '/repo/locks/pnpm-lock.yaml' + lockfileVerificationLogger.debug({ status: 'started', entries: 5, lockfilePath }) + lockfileVerificationLogger.debug({ + status: 'done', + entries: 5, + elapsedMs: 200, + lockfilePath, + }) + + const [started, done] = await frames + expect(stripAnsi(started)).toBe('? Verifying lockfile at ../../locks/pnpm-lock.yaml against supply-chain policies (5 entries)...') + expect(stripAnsi(done)).toBe('✓ Lockfile at ../../locks/pnpm-lock.yaml passes supply-chain policies (5 entries in 200ms)') +}) + +test('does not print path when running from workspace subdir and lockfile is at workspace root', async () => { + const cwd = '/repo/packages/app' + const workspaceDir = '/repo' + const output$ = toOutput$({ + context: { + argv: ['install'], + config: { dir: cwd, workspaceDir } as Config & ConfigContext, + }, + streamParser: createStreamParser(), + }) + + const frames = firstValueFrom(output$.pipe(take(1), toArray())) + + const lockfilePath = path.join(workspaceDir, 'pnpm-lock.yaml') + lockfileVerificationLogger.debug({ status: 'started', entries: 10, lockfilePath }) + + const [started] = await frames + expect(stripAnsi(started)).toBe('? Verifying lockfile against supply-chain policies (10 entries)...') +}) + +test('suppresses path when workspaceDir has a trailing separator', async () => { + const cwd = '/repo' + // Workspace dir with a trailing slash — strict === against + // path.dirname(lockfilePath) would mismatch; path.relative normalizes. + const workspaceDir = '/repo/' + const output$ = toOutput$({ + context: { + argv: ['install'], + config: { dir: cwd, workspaceDir } as Config & ConfigContext, + }, + streamParser: createStreamParser(), + }) + + const frames = firstValueFrom(output$.pipe(take(1), toArray())) + + lockfileVerificationLogger.debug({ + status: 'started', + entries: 3, + lockfilePath: '/repo/pnpm-lock.yaml', + }) + + const [started] = await frames + expect(stripAnsi(started)).toBe('? Verifying lockfile against supply-chain policies (3 entries)...') +}) + +test('emits a brief failure line on failed status', async () => { + const output$ = toOutput$({ + context: { argv: ['install'] }, + streamParser: createStreamParser(), + }) + + const frames = firstValueFrom(output$.pipe(take(2), toArray())) + + lockfileVerificationLogger.debug({ status: 'started', entries: 12 }) + lockfileVerificationLogger.debug({ + status: 'failed', + entries: 12, + elapsedMs: 800, + }) + + const [started, failed] = await frames + expect(stripAnsi(started)).toBe('? Verifying lockfile against supply-chain policies (12 entries)...') + expect(stripAnsi(failed)).toBe('✗ Lockfile failed supply-chain policy check (12 entries in 800ms)') +}) diff --git a/cli/meta/CHANGELOG.md b/cli/meta/CHANGELOG.md index 3857daf00b..5ef1786420 100644 --- a/cli/meta/CHANGELOG.md +++ b/cli/meta/CHANGELOG.md @@ -1,5 +1,19 @@ # @pnpm/cli-meta +## 1100.0.5 + +### Patch Changes + +- Updated dependencies [35d2355] + - @pnpm/types@1101.2.0 + +## 1100.0.4 + +### Patch Changes + +- Updated dependencies [64afc92] + - @pnpm/types@1101.1.1 + ## 1100.0.3 ### Patch Changes diff --git a/cli/meta/package.json b/cli/meta/package.json index a724df937c..fcb77ec8df 100644 --- a/cli/meta/package.json +++ b/cli/meta/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/cli.meta", - "version": "1100.0.3", + "version": "1100.0.5", "description": "Reads the metainfo of the currently running pnpm instance", "keywords": [ "pnpm", diff --git a/cli/utils/CHANGELOG.md b/cli/utils/CHANGELOG.md index 354c45e026..231022a183 100644 --- a/cli/utils/CHANGELOG.md +++ b/cli/utils/CHANGELOG.md @@ -1,5 +1,44 @@ # @pnpm/cli-utils +## 1101.0.8 + +### Patch Changes + +- Updated dependencies [a456dc7] +- Updated dependencies [35d2355] + - @pnpm/workspace.project-manifest-reader@1100.0.9 + - @pnpm/types@1101.2.0 + - @pnpm/config.package-is-installable@1100.0.7 + - @pnpm/cli.meta@1100.0.5 + - @pnpm/pkg-manifest.utils@1100.2.1 + +## 1101.0.7 + +### Patch Changes + +- Updated dependencies [d7da112] + - @pnpm/workspace.project-manifest-reader@1100.0.8 + +## 1101.0.6 + +### Patch Changes + +- Updated dependencies [1627943] +- Updated dependencies [64afc92] + - @pnpm/pkg-manifest.utils@1100.2.0 + - @pnpm/types@1101.1.1 + - @pnpm/workspace.project-manifest-reader@1100.0.7 + - @pnpm/cli.meta@1100.0.4 + - @pnpm/config.package-is-installable@1100.0.6 + +## 1101.0.5 + +### Patch Changes + +- @pnpm/config.package-is-installable@1100.0.5 +- @pnpm/pkg-manifest.utils@1100.1.4 +- @pnpm/workspace.project-manifest-reader@1100.0.6 + ## 1101.0.4 ### Patch Changes diff --git a/cli/utils/package.json b/cli/utils/package.json index c81373bf00..3869865f8a 100644 --- a/cli/utils/package.json +++ b/cli/utils/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/cli.utils", - "version": "1101.0.4", + "version": "1101.0.8", "description": "Utils for pnpm commands", "keywords": [ "pnpm", diff --git a/config/commands/CHANGELOG.md b/config/commands/CHANGELOG.md index 33dbf8ace5..b59c2b720e 100644 --- a/config/commands/CHANGELOG.md +++ b/config/commands/CHANGELOG.md @@ -1,5 +1,51 @@ # @pnpm/plugin-commands-config +## 1100.0.17 + +### Patch Changes + +- Updated dependencies [a23956e] +- Updated dependencies [35d2355] + - @pnpm/config.reader@1101.4.1 + - @pnpm/workspace.workspace-manifest-writer@1100.0.10 + - @pnpm/cli.utils@1101.0.8 + +## 1100.0.16 + +### Patch Changes + +- Updated dependencies [d7da112] +- Updated dependencies [3b62f9d] +- Updated dependencies [212315d] + - @pnpm/object.property-path@1100.1.0 + - @pnpm/config.reader@1101.4.0 + - @pnpm/cli.utils@1101.0.7 + - @pnpm/workspace.workspace-manifest-writer@1100.0.9 + +## 1100.0.15 + +### Patch Changes + +- Updated dependencies [3687b0e] +- Updated dependencies [ced20cb] +- Updated dependencies [d1b340f] + - @pnpm/config.reader@1101.3.3 + - @pnpm/cli.utils@1101.0.6 + - @pnpm/workspace.workspace-manifest-writer@1100.0.9 + +## 1100.0.14 + +### Patch Changes + +- Updated dependencies [020ac45] +- Updated dependencies [d3f8408] +- Updated dependencies [a62f959] +- Updated dependencies [ba2c884] +- Updated dependencies [8df408c] + - @pnpm/config.reader@1101.3.2 + - @pnpm/workspace.workspace-manifest-writer@1100.0.8 + - @pnpm/cli.utils@1101.0.5 + ## 1100.0.13 ### Patch Changes diff --git a/config/commands/package.json b/config/commands/package.json index 95af5f0e0b..af8454db0e 100644 --- a/config/commands/package.json +++ b/config/commands/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/config.commands", - "version": "1100.0.13", + "version": "1100.0.17", "description": "Commands for reading and writing settings to/from config files", "keywords": [ "pnpm", diff --git a/config/normalize-registries/CHANGELOG.md b/config/normalize-registries/CHANGELOG.md index 28450f46bc..88eff4cf55 100644 --- a/config/normalize-registries/CHANGELOG.md +++ b/config/normalize-registries/CHANGELOG.md @@ -1,5 +1,19 @@ # @pnpm/normalize-registries +## 1100.0.5 + +### Patch Changes + +- Updated dependencies [35d2355] + - @pnpm/types@1101.2.0 + +## 1100.0.4 + +### Patch Changes + +- Updated dependencies [64afc92] + - @pnpm/types@1101.1.1 + ## 1100.0.3 ### Patch Changes diff --git a/config/normalize-registries/package.json b/config/normalize-registries/package.json index bc786e029b..6632a4e220 100644 --- a/config/normalize-registries/package.json +++ b/config/normalize-registries/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/config.normalize-registries", - "version": "1100.0.3", + "version": "1100.0.5", "description": "Accepts a mapping of registry URLs and returns a mapping with the same URLs but normalized", "keywords": [ "pnpm", diff --git a/config/package-is-installable/CHANGELOG.md b/config/package-is-installable/CHANGELOG.md index ddb528e985..5c7872fb3c 100644 --- a/config/package-is-installable/CHANGELOG.md +++ b/config/package-is-installable/CHANGELOG.md @@ -1,5 +1,34 @@ # @pnpm/package-is-installable +## 1100.0.7 + +### Patch Changes + +- Updated dependencies [35d2355] + - @pnpm/engine.runtime.system-version@1100.0.0 + - @pnpm/types@1101.2.0 + - @pnpm/cli.meta@1100.0.5 + - @pnpm/core-loggers@1100.1.2 + +## 1100.0.6 + +### Patch Changes + +- Updated dependencies [64afc92] + - @pnpm/types@1101.1.1 + - @pnpm/cli.meta@1100.0.4 + - @pnpm/core-loggers@1100.1.1 + - @pnpm/engine.runtime.system-node-version@1100.1.1 + +## 1100.0.5 + +### Patch Changes + +- Updated dependencies [3ddde2b] +- Updated dependencies [4a79336] + - @pnpm/engine.runtime.system-node-version@1100.1.0 + - @pnpm/core-loggers@1100.1.0 + ## 1100.0.4 ### Patch Changes diff --git a/config/package-is-installable/package.json b/config/package-is-installable/package.json index 74beeee613..09df4cd436 100644 --- a/config/package-is-installable/package.json +++ b/config/package-is-installable/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/config.package-is-installable", - "version": "1100.0.4", + "version": "1100.0.7", "description": "Checks if a package is installable on the current system", "keywords": [ "pnpm", @@ -34,7 +34,7 @@ "dependencies": { "@pnpm/cli.meta": "workspace:*", "@pnpm/core-loggers": "workspace:*", - "@pnpm/engine.runtime.system-node-version": "workspace:*", + "@pnpm/engine.runtime.system-version": "workspace:*", "@pnpm/error": "workspace:*", "@pnpm/types": "workspace:*", "detect-libc": "catalog:", diff --git a/config/package-is-installable/src/index.ts b/config/package-is-installable/src/index.ts index cfcb77edbf..59310aa6ba 100644 --- a/config/package-is-installable/src/index.ts +++ b/config/package-is-installable/src/index.ts @@ -2,7 +2,7 @@ import { installCheckLogger, skippedOptionalDependencyLogger, } from '@pnpm/core-loggers' -import { getSystemNodeVersion } from '@pnpm/engine.runtime.system-node-version' +import { getSystemNodeVersion } from '@pnpm/engine.runtime.system-version' import type { SupportedArchitectures } from '@pnpm/types' import { checkEngine, UnsupportedEngineError, type WantedEngine } from './checkEngine.js' diff --git a/config/package-is-installable/tsconfig.json b/config/package-is-installable/tsconfig.json index 0f4979de1e..fc0dd041cb 100644 --- a/config/package-is-installable/tsconfig.json +++ b/config/package-is-installable/tsconfig.json @@ -25,7 +25,7 @@ "path": "../../core/types" }, { - "path": "../../engine/runtime/system-node-version" + "path": "../../engine/runtime/system-version" } ] } diff --git a/config/pick-registry-for-package/CHANGELOG.md b/config/pick-registry-for-package/CHANGELOG.md index 73ef2267a0..9c2dc47614 100644 --- a/config/pick-registry-for-package/CHANGELOG.md +++ b/config/pick-registry-for-package/CHANGELOG.md @@ -1,5 +1,25 @@ # @pnpm/pick-registry-for-package +## 1100.0.6 + +### Patch Changes + +- Updated dependencies [35d2355] + - @pnpm/types@1101.2.0 + +## 1100.0.5 + +### Patch Changes + +- 097983f: Fix `pickRegistryForPackage` returning the wrong registry for an unscoped `npm:` alias under a scoped local name. A manifest entry like `"@private/foo": "npm:lodash@^1"` was routing the `lodash` fetch through `registries["@private"]`, even though `lodash` is unscoped and doesn't live on that registry. The npm-alias branch now returns the alias target's own scope (or `null` for an unscoped target, falling through to `registries.default`) instead of leaking into the local key's scope. + +## 1100.0.4 + +### Patch Changes + +- Updated dependencies [64afc92] + - @pnpm/types@1101.1.1 + ## 1100.0.3 ### Patch Changes diff --git a/config/pick-registry-for-package/package.json b/config/pick-registry-for-package/package.json index 4ff5a2b842..358be91105 100644 --- a/config/pick-registry-for-package/package.json +++ b/config/pick-registry-for-package/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/config.pick-registry-for-package", - "version": "1100.0.3", + "version": "1100.0.6", "description": "Picks the right registry for the package from a registries config", "keywords": [ "pnpm", diff --git a/config/pick-registry-for-package/src/index.ts b/config/pick-registry-for-package/src/index.ts index 71cb954f87..e0cfb69d53 100644 --- a/config/pick-registry-for-package/src/index.ts +++ b/config/pick-registry-for-package/src/index.ts @@ -7,10 +7,15 @@ export function pickRegistryForPackage (registries: Registries, packageName: str function getScope (pkgName: string, bareSpecifier?: string): string | null { if (bareSpecifier?.startsWith('npm:')) { - bareSpecifier = bareSpecifier.slice(4) - if (bareSpecifier[0] === '@') { - return bareSpecifier.substring(0, bareSpecifier.indexOf('/')) + const target = bareSpecifier.slice(4) + if (target[0] === '@') { + return target.substring(0, target.indexOf('/')) } + // Unscoped `npm:` alias target (e.g. `"@private/foo": "npm:lodash@^1"`). + // The package being fetched is unscoped, so the local alias's scope must + // not drive registry routing — `lodash` doesn't live on the `@private` + // registry. Fall through to the default registry instead. + return null } if (pkgName[0] === '@') { return pkgName.substring(0, pkgName.indexOf('/')) diff --git a/config/pick-registry-for-package/test/index.spec.ts b/config/pick-registry-for-package/test/index.spec.ts index c82d41bc9d..719af0a209 100644 --- a/config/pick-registry-for-package/test/index.spec.ts +++ b/config/pick-registry-for-package/test/index.spec.ts @@ -10,3 +10,28 @@ test('pick correct scope', () => { expect(pickRegistryForPackage(registries, '@random/lodash')).toBe('https://registry.npmjs.org/') expect(pickRegistryForPackage(registries, '@random/lodash', 'npm:@private/lodash@1')).toBe('https://private.registry.com/') }) + +// An unscoped `npm:` alias target (e.g. `"@private/foo": "npm:lodash@^1"`) +// must NOT route through the local alias's scope: the fetched package is +// `lodash` (unscoped) and doesn't live on the `@private` registry. The npm- +// alias branch returns `null` in that case so the call falls through to +// `registries.default`. +test('unscoped npm-alias target routes to default, not the local alias scope', () => { + const registries = { + default: 'https://registry.npmjs.org/', + '@private': 'https://private.registry.com/', + } + expect(pickRegistryForPackage(registries, '@private/foo', 'npm:lodash@^1')).toBe('https://registry.npmjs.org/') +}) + +// Scoped local + scoped `npm:` target in a different scope: the target's +// scope wins. The package being fetched is `@scope2/bar`, so routing +// follows `@scope2`, not the local `@scope1/` slot. +test('scoped npm-alias target in different scope wins over local scope', () => { + const registries = { + default: 'https://registry.npmjs.org/', + '@scope1': 'https://scope1.registry/', + '@scope2': 'https://scope2.registry/', + } + expect(pickRegistryForPackage(registries, '@scope1/foo', 'npm:@scope2/bar@^1')).toBe('https://scope2.registry/') +}) diff --git a/config/reader/CHANGELOG.md b/config/reader/CHANGELOG.md index d7b6f33058..e1256908d5 100644 --- a/config/reader/CHANGELOG.md +++ b/config/reader/CHANGELOG.md @@ -1,5 +1,82 @@ # @pnpm/config +## 1101.4.1 + +### Patch Changes + +- a23956e: Fix a credential disclosure issue where an unscoped `_authToken` (or `_auth`, or `username` + `_password`, or `tokenHelper`) defined in one source — `~/.npmrc`, `~/.config/pnpm/auth.ini`, a workspace `.npmrc`, CLI flags, etc. — would be sent as an `Authorization` header to whichever registry a different (potentially untrusted) source named. The same fix extends to client TLS credentials (`cert`, `key`) so they aren't presented to a registry their author didn't choose. + + pnpm now rewrites each unscoped per-registry setting (`_authToken`, `_auth`, `username`, `_password`, `tokenHelper`, `cert`, `key`) to its URL-scoped form at load time, using the `registry=` value declared in the same source (or the npmjs default registry if the source declares none). A later layer overriding `registry=` therefore cannot pull an unscoped credential along, because it is already pinned to the URL its author intended. `ca`/`cafile` are intentionally not rescoped — they're trust anchors, not credentials, and corporate MITM-proxy setups rely on them applying globally. + + Every rescope emits a deprecation warning telling the user where the setting was pinned and how to write it directly. npm has rejected unscoped credentials outright since `npm@9`, and pnpm intends to remove support in a future major release. To target a specific registry, write the setting URL-scoped (e.g. `//registry.example.com/:_authToken=...` or `//registry.example.com/:cert=...`). + + `@pnpm/network.auth-header`: removed the `defaultRegistry` parameter from `createGetAuthHeaderByURI` and `getAuthHeadersFromCreds`. Now that credentials are URL-scoped at load time, the merged `configByUri` never contains the empty-string "default registry" placeholder slot, so re-keying it onto the merged default registry is no longer needed. + +- 35d2355: Validate `devEngines.runtime` and `engines.runtime` version ranges for `node`, `deno`, and `bun` when `onFail` is set to `error` or `warn`. Previously these settings only had an effect with `onFail: 'download'` — the `error` and `warn` modes silently did nothing [#11818](https://github.com/pnpm/pnpm/issues/11818). Violations now throw `ERR_PNPM_BAD_RUNTIME_VERSION`. +- Updated dependencies [a456dc7] +- Updated dependencies [35d2355] + - @pnpm/workspace.project-manifest-reader@1100.0.9 + - @pnpm/types@1101.2.0 + - @pnpm/hooks.pnpmfile@1100.0.11 + - @pnpm/pkg-manifest.utils@1100.2.1 + - @pnpm/workspace.workspace-manifest-reader@1100.0.5 + - @pnpm/catalogs.config@1100.0.0 + +## 1101.4.0 + +### Minor Changes + +- 3b62f9d: Add a `skip-manifest-obfuscation` option for `pnpm pack` and `pnpm publish`. When enabled, the original `packageManager` field and publish lifecycle scripts are kept in the packed/published manifest instead of being stripped. The pnpm-specific `pnpm` field continues to be omitted. +- 212315d: Added a new setting `trustLockfile`. When `true`, `pnpm install` skips the supply-chain verification pass that re-applies `minimumReleaseAge` / `trustPolicy='no-downgrade'` to every entry in the loaded lockfile. The install treats the lockfile as already-trusted — useful for closed-source projects where every commit comes from a trusted author, or for CI runs against an already-verified lockfile. Defaults to `false`; verification stays on by default. Set in `pnpm-workspace.yaml`. + + Also cut the memory footprint of the verification pass itself: the per-(registry, name) trust-meta cache previously retained the full packument — dependency graphs, scripts, README, and per-version manifests — for the entire install. On large workspaces (`~4k` lockfile entries with `minimumReleaseAge` + `trustPolicy: no-downgrade` enabled) this could OOM CI runners with a 2GB heap cap. The cache now stores only the fields the trust check actually reads (`time`, per-version `_npmUser.trustedPublisher`, `dist.attestations.provenance`). The abbreviated-metadata cache is similarly projected to just the package-level `modified` field and the set of currently-listed version names. Fixes [#11860](https://github.com/pnpm/pnpm/issues/11860). + +### Patch Changes + +- Updated dependencies [d7da112] + - @pnpm/workspace.project-manifest-reader@1100.0.8 + +## 1101.3.3 + +### Patch Changes + +- 3687b0e: Fix `cafile=` in `.npmrc` being read from the wrong directory when pnpm is invoked from a different cwd (e.g. `pnpm --dir install` from a CI wrapper or monorepo script). The path is now resolved against the directory of the `.npmrc` that declared it, not `process.cwd()`. Before this fix the CA file silently failed to load — the install proceeded without the configured CA and the user only saw TLS errors against a private registry, with no log line tying back to the wrongly resolved path [#11624](https://github.com/pnpm/pnpm/issues/11624). +- ced20cb: Fix `config.registry` getting a trailing slash appended when `registry` is set in `.npmrc` and no `registries.default` is provided by `pnpm-workspace.yaml`. The sync from `registries.default` to `config.registry` introduced in #11744 now only fires when the workspace manifest actually contributes a different default. +- d1b340f: Fixed `pnpm login` and `pnpm logout` ignoring `registries.default` from `pnpm-workspace.yaml` [#10099](https://github.com/pnpm/pnpm/issues/10099). +- Updated dependencies [1627943] +- Updated dependencies [64afc92] + - @pnpm/pkg-manifest.utils@1100.2.0 + - @pnpm/types@1101.1.1 + - @pnpm/workspace.project-manifest-reader@1100.0.7 + - @pnpm/hooks.pnpmfile@1100.0.10 + - @pnpm/workspace.workspace-manifest-reader@1100.0.4 + - @pnpm/catalogs.config@1100.0.0 + +## 1101.3.2 + +### Patch Changes + +- 020ac45: Allow redundant trailing base64 padding in `.npmrc` auth values and report invalid auth base64 with a pnpm error. +- d3f8408: **fix**: global installs respect global config build policy (e.g., `dangerouslyAllowAllBuilds` from config.yaml) when GVS is enabled [#9249](https://github.com/pnpm/pnpm/issues/9249). + + The global virtual-store (GVS) default `allowBuilds = {}` was applied before workspace manifest settings were read and before global config values (stripped by `extractAndRemoveDependencyBuildOptions`) were re-applied via `globalDepsBuildConfig`. This caused `hasDependencyBuildOptions` to return `true` (because `{}` is not null), blocking restoration of global config values like `dangerouslyAllowAllBuilds`. As a result, global installs skipped all build scripts even when the config explicitly allowed them. + + This fix moves the GVS default to **after** workspace manifest reading and `globalDepsBuildConfig` re-application, so that: + + 1. Workspace manifest `allowBuilds` takes precedence (if present) + 2. Global config `dangerouslyAllowAllBuilds` is properly restored (if set and no workspace policy exists) + 3. Empty `{}` is only applied as a last resort when no policy is configured anywhere + +- a62f959: Fixed `pnpm publish` failing with a 404 when authentication relied on OIDC trusted publishing alongside an `.npmrc` written by `actions/setup-node` (`_authToken=${NODE_AUTH_TOKEN}`) without `NODE_AUTH_TOKEN` being set. Unresolved `${VAR}` placeholders in auth values are now treated as empty rather than passed through verbatim, so the literal placeholder no longer surfaces as a bearer token when OIDC fallback is the intended auth source [#11513](https://github.com/pnpm/pnpm/issues/11513). +- ba2c884: Fix `devEngines.packageManager` (singular form, without `onFail`) defaulting to `onFail: "error"` instead of the documented `pmOnFail: "download"`. As a result, a project that pinned a different pnpm version via `devEngines.packageManager` and ran `pnpm install` from a mismatched pnpm version failed with a hard error, even though the migration table from `managePackageManagerVersions: true` to `pmOnFail: download (default)` promises the install would auto-download the wanted version [#11676](https://github.com/pnpm/pnpm/issues/11676). + + The array form of `devEngines.packageManager` keeps its existing per-element defaults (`error` for the last entry, `ignore` for the rest), since those reflect explicit prioritization by the user. Explicit `onFail` values continue to win. + +- 8df408c: Warn when `package.json` contains a legacy `pnpm` field with settings pnpm no longer reads from `package.json` (e.g. `pnpm.overrides`, `pnpm.patchedDependencies`). Previously these were silently ignored after the upgrade from v10, leaving users unaware that their overrides/patched dependencies had stopped taking effect [#11677](https://github.com/pnpm/pnpm/issues/11677). + - @pnpm/hooks.pnpmfile@1100.0.9 + - @pnpm/pkg-manifest.utils@1100.1.4 + - @pnpm/workspace.project-manifest-reader@1100.0.6 + ## 1101.3.1 ### Patch Changes diff --git a/config/reader/package.json b/config/reader/package.json index 3fb3643a0e..d3adae1102 100644 --- a/config/reader/package.json +++ b/config/reader/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/config.reader", - "version": "1101.3.1", + "version": "1101.4.1", "description": "Gets configuration options for pnpm", "keywords": [ "pnpm", @@ -37,6 +37,7 @@ "@pnpm/catalogs.types": "workspace:*", "@pnpm/config.env-replace": "catalog:", "@pnpm/config.matcher": "workspace:*", + "@pnpm/config.nerf-dart": "catalog:", "@pnpm/constants": "workspace:*", "@pnpm/error": "workspace:*", "@pnpm/hooks.pnpmfile": "workspace:*", diff --git a/config/reader/src/Config.ts b/config/reader/src/Config.ts index 87280d241f..7e8f9591ed 100644 --- a/config/reader/src/Config.ts +++ b/config/reader/src/Config.ts @@ -210,6 +210,7 @@ export interface Config extends OptionsFromRootManifest { modulesCacheMaxAge: number dlxCacheMaxAge: number embedReadme?: boolean + skipManifestObfuscation?: boolean gitShallowHosts?: string[] legacyDirFiltering?: boolean allowBuilds?: Record @@ -267,6 +268,7 @@ export interface Config extends OptionsFromRootManifest { minimumReleaseAgeStrict?: boolean fetchWarnTimeoutMs?: number fetchMinSpeedKiBps?: number + trustLockfile?: boolean trustPolicy?: TrustPolicy trustPolicyExclude?: string[] trustPolicyIgnoreAfter?: number diff --git a/config/reader/src/configFileKey.ts b/config/reader/src/configFileKey.ts index e2f84fdf74..a94cb64c63 100644 --- a/config/reader/src/configFileKey.ts +++ b/config/reader/src/configFileKey.ts @@ -61,6 +61,7 @@ export const pnpmConfigFileKeys = [ 'state-dir', 'store-dir', 'strict-dep-builds', + 'trust-lockfile', 'trust-policy', 'trust-policy-exclude', 'trust-policy-ignore-after', @@ -151,6 +152,7 @@ export const excludedPnpmKeys = [ 'test-pattern', 'changed-files-ignore-pattern', 'embed-readme', + 'skip-manifest-obfuscation', 'fail-if-no-match', 'sync-injected-deps-after-scripts', 'cpu', diff --git a/config/reader/src/index.ts b/config/reader/src/index.ts index 4b8c76572e..dc3372b6cb 100644 --- a/config/reader/src/index.ts +++ b/config/reader/src/index.ts @@ -38,7 +38,7 @@ import { isConfigFileKey } from './configFileKey.js' import { extractAndRemoveDependencyBuildOptions, hasDependencyBuildOptions } from './dependencyBuildOptions.js' import { getCacheDir, getConfigDir, getDataDir, getStateDir } from './dirs.js' import { parseEnvVars } from './env.js' -import { getDefaultCreds, getNetworkConfigs } from './getNetworkConfigs.js' +import { getNetworkConfigs } from './getNetworkConfigs.js' import { getOptionsFromPnpmSettings } from './getOptionsFromRootManifest.js' import { loadNpmrcConfig } from './loadNpmrcFiles.js' import { inheritDlxConfig, pickIniConfig } from './localConfig.js' @@ -208,6 +208,7 @@ export async function getConfig (opts: { 'workspace-concurrency': getDefaultWorkspaceConcurrency(), 'workspace-prefix': opts.workspaceDir, 'embed-readme': false, + 'skip-manifest-obfuscation': false, 'registry-supports-time-field': false, 'virtual-store-dir-max-length': isWindows() ? 60 : 120, 'virtual-store-only': false, @@ -320,11 +321,8 @@ export async function getConfig (opts: { ...networkConfigs.registries, } pnpmConfig.registries = { ...registriesFromNpmrc } - const defaultCreds = getDefaultCreds(pnpmConfig.authConfig) - pnpmConfig.configByUri = { - ...networkConfigs.configByUri, - ...defaultCreds ? { '': { creds: defaultCreds } } : {}, - } + pnpmConfig.configByUri = { ...networkConfigs.configByUri } + // tokenHelper must only come from user-level config (~/.npmrc or global auth.ini), // not project-level, to prevent project .npmrc from executing arbitrary commands. const userConfig = npmrcResult.userConfig as Record @@ -404,6 +402,10 @@ export async function getConfig (opts: { if (pnpmConfig.rootProjectManifest.workspaces?.length && !pnpmConfig.workspaceDir) { warnings.push('The "workspaces" field in package.json is not supported by pnpm. Create a "pnpm-workspace.yaml" file instead.') } + const ignoredPnpmFieldKeys = getIgnoredPnpmFieldKeys(pnpmConfig.rootProjectManifest) + if (ignoredPnpmFieldKeys.length > 0) { + warnings.push(`The "pnpm" field in package.json is no longer read by pnpm. The following keys were ignored: ${ignoredPnpmFieldKeys.map(k => `"pnpm.${k}"`).join(', ')}. See https://pnpm.io/settings for the new home of each setting.`) + } const wantedPmResult = getWantedPackageManager(pnpmConfig.rootProjectManifest) if (wantedPmResult.pm) { pnpmConfig.wantedPackageManager = wantedPmResult.pm @@ -457,6 +459,15 @@ export async function getConfig (opts: { } } + // Sync registries.default to the top-level registry property so that + // commands like login/logout that use opts.registry pick up the default + // registry configured in pnpm-workspace.yaml. Only sync when the workspace + // manifest actually contributed a different default than what .npmrc provided, + // and when registry was not explicitly set via CLI. + if (!explicitlySetKeys.has('registry') && pnpmConfig.registries.default !== registriesFromNpmrc.default) { + pnpmConfig.registry = pnpmConfig.registries.default + } + // omit some schema that the custom parser can't yet handle const envPnpmTypes = omit([ 'init-version', // the type is a private function named 'semver' @@ -657,9 +668,11 @@ export async function getConfig (opts: { // The `pmOnFail` config setting overrides whatever onFail the // wantedPackageManager carried, so users (and internal callers) can force - // a specific behavior without editing the manifest. Otherwise, the legacy - // `packageManager` field defaults to `download` — `devEngines.packageManager` - // already has onFail set during parsing. + // a specific behavior without editing the manifest. Otherwise, both the + // legacy `packageManager` field and singular `devEngines.packageManager` + // fall through to `download` (the documented default for `pmOnFail`); the + // array form of `devEngines.packageManager` already has its own per-element + // defaults applied during parsing. if (pnpmConfig.wantedPackageManager) { if (pnpmConfig.pmOnFail) { pnpmConfig.wantedPackageManager.onFail = pnpmConfig.pmOnFail @@ -748,6 +761,38 @@ function getWantedPackageManager (manifest: ProjectManifest): { pm?: WantedPacka return { warnings } } +// Settings that used to be read from the `pnpm` field of `package.json` in v10 +// but moved to `pnpm-workspace.yaml` in v11. Keys not in this set (e.g. `app`, +// or anything set by third-party tooling that piggybacks on the `pnpm` namespace) +// are left alone to avoid false-positive warnings. +const MIGRATED_PNPM_FIELD_KEYS = new Set([ + 'allowBuilds', + 'allowedDeprecatedVersions', + 'allowUnusedPatches', + 'auditConfig', + 'configDependencies', + 'executionEnv', + 'ignoredOptionalDependencies', + 'neverBuiltDependencies', + 'onlyBuiltDependencies', + 'onlyBuiltDependenciesFile', + 'overrides', + 'packageExtensions', + 'patchedDependencies', + 'peerDependencyRules', + 'requiredScripts', + 'supportedArchitectures', + 'updateConfig', +]) + +function getIgnoredPnpmFieldKeys (manifest: ProjectManifest): string[] { + const legacyField = (manifest as { pnpm?: unknown }).pnpm + if (legacyField == null || typeof legacyField !== 'object' || Array.isArray(legacyField)) { + return [] + } + return Object.keys(legacyField as Record).filter(k => MIGRATED_PNPM_FIELD_KEYS.has(k)) +} + export function parsePackageManager (packageManager: string): { name: string, version: string | undefined } { if (!packageManager.includes('@')) return { name: packageManager, version: undefined } const [name, pmReference] = packageManager.split('@') @@ -764,7 +809,7 @@ export function parsePackageManager (packageManager: string): { name: string, ve function parseDevEnginesPackageManager (devEngines?: DevEngines): EngineDependency | undefined { if (!devEngines?.packageManager) return undefined let pmEngine: EngineDependency | undefined - let onFail: 'ignore' | 'warn' | 'error' | 'download' + let onFail: 'ignore' | 'warn' | 'error' | 'download' | undefined if (Array.isArray(devEngines.packageManager)) { const engines = devEngines.packageManager if (engines.length === 0) return undefined @@ -781,7 +826,11 @@ function parseDevEnginesPackageManager (devEngines?: DevEngines): EngineDependen } } else { pmEngine = devEngines.packageManager - onFail = pmEngine.onFail ?? 'error' + // Singular form: leave onFail undefined when the user did not set it, so + // the central pmOnFail default ('download') applies. The array form keeps + // its own per-element defaults ('error' for the last entry, 'ignore' for + // the rest) because those reflect explicit prioritization by the user. + onFail = pmEngine.onFail } if (!pmEngine?.name) return undefined return { @@ -798,6 +847,7 @@ function getNodeVersionFromEnginesRuntime (manifest: ProjectManifest): string | const runtimes: EngineDependency[] = Array.isArray(enginesRuntime) ? enginesRuntime : [enginesRuntime] const nodeRuntime = runtimes.find((r) => r.name === 'node') if (nodeRuntime?.version == null) continue + if (!semver.validRange(nodeRuntime.version)) continue const minVersion = semver.minVersion(nodeRuntime.version) if (minVersion != null) { return minVersion.version @@ -835,4 +885,3 @@ function addSettingsFromWorkspaceManifestToConfig (pnpmConfig: Config & ConfigCo } pnpmConfig.catalogs = getCatalogsFromWorkspaceManifest(workspaceManifest) } - diff --git a/config/reader/src/loadNpmrcFiles.ts b/config/reader/src/loadNpmrcFiles.ts index 3092c9e5e3..dd95b4e228 100644 --- a/config/reader/src/loadNpmrcFiles.ts +++ b/config/reader/src/loadNpmrcFiles.ts @@ -3,9 +3,12 @@ import os from 'node:os' import path from 'node:path' import { envReplaceLossy } from '@pnpm/config.env-replace' +import { nerfDart } from '@pnpm/config.nerf-dart' +import normalizeRegistryUrl from 'normalize-registry-url' import { readIniFileSync } from 'read-ini-file' import { isNpmrcReadableKey } from './localConfig.js' +import { npmDefaults } from './npmDefaults.js' export interface NpmrcConfigResult { /** @@ -69,6 +72,11 @@ export function loadNpmrcConfig (opts: LoadNpmrcConfigOpts): NpmrcConfigResult { env ) + // Apply the same per-source rescope to CLI options so an unscoped + // `--_authToken` follows the same trust rule as one written into an .npmrc. + // We clone first to avoid mutating the caller's cliOptions object. + const cliOptions = rescopeUnscopedCreds({ ...opts.cliOptions }, '', warnings) + // Read pnpm builtin rc + inline defaults const pnpmBuiltinConfig: Record = { ...readAndFilterNpmrc( @@ -83,7 +91,7 @@ export function loadNpmrcConfig (opts: LoadNpmrcConfigOpts): NpmrcConfigResult { // Handle cafile: expand to ca certs. // Priority: CLI > workspace > auth.ini > user > defaults loadCAFile([ - opts.cliOptions, + cliOptions, workspaceNpmrc, pnpmAuthConfig, userConfig, @@ -93,7 +101,7 @@ export function loadNpmrcConfig (opts: LoadNpmrcConfigOpts): NpmrcConfigResult { // Merge all sources (lowest to highest priority): // builtin < defaults < user < auth.ini < workspace < CLI const mergedConfig: Record = {} - for (const source of [pnpmBuiltinConfig, opts.defaultOptions, userConfig, pnpmAuthConfig, workspaceNpmrc, opts.cliOptions]) { + for (const source of [pnpmBuiltinConfig, opts.defaultOptions, userConfig, pnpmAuthConfig, workspaceNpmrc, cliOptions]) { for (const [key, value] of Object.entries(source)) { if (isNpmrcReadableKey(key)) { mergedConfig[key] = value @@ -108,7 +116,7 @@ export function loadNpmrcConfig (opts: LoadNpmrcConfigOpts): NpmrcConfigResult { ...userConfig, ...pnpmAuthConfig, ...workspaceNpmrc, - ...opts.cliOptions, + ...cliOptions, } return { @@ -121,6 +129,35 @@ export function loadNpmrcConfig (opts: LoadNpmrcConfigOpts): NpmrcConfigResult { } } +// Per-registry rc keys that, when written without a `//host/` prefix, fall +// through to whatever default registry the merged config settles on. We +// rewrite each such key to its URL-scoped form at load time, pinning it to +// the `registry=` value declared in the same source. A later layer can +// still override the merged registry, but it cannot pull along a credential +// or client certificate authored for a different host. +// +// Two groups: +// * auth keys — `_authToken` etc. Pinned to prevent credential leaks. npm +// rejects these unscoped since npm@9 (ERR_INVALID_AUTH); pnpm keeps them +// working but warns so users migrate before a future major drops support. +// * client certificate keys — `cert`/`key` (inline PEM). Pinned to prevent +// a client certificate (and the identity it carries) being presented to +// the wrong host. The `certfile`/`keyfile` path variants are not in +// `NPM_AUTH_SETTINGS`, so unscoped forms never reach the merged config +// in the first place — only the URL-scoped `//host/:certfile=...` and +// `//host/:keyfile=...` forms are honored, and those are already pinned +// to their authoring registry by construction. +// +// `ca`/`cafile` are intentionally left unscoped-by-default: they're trust +// anchors, not credentials, and corporate MITM-proxy setups rely on them +// applying globally to every HTTPS request. The default registry override +// can't weaponize an unscoped CA (the attacker would need a cert signed +// by it), so the same pinning isn't warranted. +const UNSCOPED_RESCOPABLE_KEYS = [ + '_authToken', '_auth', 'username', '_password', 'tokenHelper', + 'cert', 'key', +] as const + function readAndFilterNpmrc ( filePath: string, warnings: string[], @@ -137,20 +174,85 @@ function readAndFilterNpmrc ( return {} } + const npmrcDir = path.dirname(filePath) const result: Record = {} for (const [rawKey, rawValue] of Object.entries(raw)) { // Apply ${VAR} substitution to both keys and values const key = substituteEnv(rawKey, env, warnings) - const value = typeof rawValue === 'string' + let value: unknown = typeof rawValue === 'string' ? substituteEnv(rawValue, env, warnings) : rawValue // Only keep auth/registry related keys if (isNpmrcReadableKey(key)) { + // A relative `cafile=` resolves against the .npmrc's directory rather + // than process.cwd(), so `pnpm --dir ` from a different cwd + // still finds it. See https://github.com/pnpm/pnpm/issues/11624. + if (key === 'cafile' && typeof value === 'string' && value !== '' && !path.isAbsolute(value)) { + value = path.resolve(npmrcDir, value) + } result[key] = value } } - return result + return rescopeUnscopedCreds(result, filePath, warnings) +} + +// Rewrite any unscoped per-registry keys in `source` to their URL-scoped +// equivalents (`//host[:port]/path/:=...`) using `source.registry` — +// or the builtin default registry if the source doesn't declare its own. +// This pins each layer's credential, client certificate, or CA setting to +// the registry that layer named (or the implicit npmjs default), so a +// later layer overriding `registry=` cannot pull a setting authored for +// one host along to a different host. A URL-scoped key for the same +// registry already present in `source` wins; we never overwrite an +// explicit scoped value. +// +// Each rewrite triggers a deprecation warning so users migrate to writing +// the URL-scoped form directly. npm has rejected unscoped credentials +// outright since `npm@9` (`ERR_INVALID_AUTH`). +function rescopeUnscopedCreds ( + source: Record, + sourceLabel: string, + warnings: string[] +): Record { + // Bail early if there's nothing to rescope. This skips the nerfDart call + // when a source like the builtin pnpmrc has only a `registry=` line — + // rescoping there would do nothing anyway. + if (!UNSCOPED_RESCOPABLE_KEYS.some(key => key in source)) { + return source + } + const rawRegistry = typeof source.registry === 'string' && source.registry !== '' ? source.registry : null + const fallbackRegistry = rawRegistry ?? npmDefaults.registry + let nerfedRegistry: string + try { + nerfedRegistry = nerfDart(normalizeRegistryUrl(fallbackRegistry)) + } catch { + // `registry=` resolved to something `URL` can't parse — often an + // unresolved `${VAR}` placeholder that left the string empty. Drop the + // unscoped keys (a bare token is unsafe to bind anywhere) and warn. + const dropped = UNSCOPED_RESCOPABLE_KEYS.filter(key => key in source) + for (const key of dropped) delete source[key] + warnings.push(`Unscoped per-registry settings (${dropped.join(', ')}) in "${sourceLabel}" were ignored: ` + + `the source's "registry" value (${JSON.stringify(source.registry)}) is not a parseable URL, so pnpm cannot pin them anywhere safe. ` + + 'Write them URL-scoped (e.g. "//registry.example.com/:_authToken=...") to send them to a specific registry.') + return source + } + const rescoped: string[] = [] + for (const key of UNSCOPED_RESCOPABLE_KEYS) { + if (!(key in source)) continue + const scopedKey = `${nerfedRegistry}:${key}` + if (!(scopedKey in source)) { + source[scopedKey] = source[key] + } + delete source[key] + rescoped.push(key) + } + if (rescoped.length > 0) { + warnings.push(`Unscoped per-registry settings (${rescoped.join(', ')}) in "${sourceLabel}" are deprecated. ` + + `pnpm pinned them to "${nerfedRegistry}" for this run, but a future release will stop supporting unscoped per-registry settings. ` + + `Write them as "${nerfedRegistry}:${rescoped[0]}=..." instead.`) + } + return source } // Use the lossy variant so unresolved `${VAR}` placeholders become '' (each diff --git a/config/reader/src/localConfig.ts b/config/reader/src/localConfig.ts index 42aa96bdda..c2c2aafaef 100644 --- a/config/reader/src/localConfig.ts +++ b/config/reader/src/localConfig.ts @@ -86,6 +86,7 @@ const SECURITY_POLICY_CFG_KEYS = [ 'minimumReleaseAgeExclude', 'minimumReleaseAgeIgnoreMissingTime', 'minimumReleaseAgeStrict', + 'trustLockfile', 'trustPolicy', 'trustPolicyExclude', 'trustPolicyIgnoreAfter', @@ -109,7 +110,6 @@ const NPM_AUTH_SETTINGS = [ '_authToken', '_password', 'email', - 'keyfile', 'username', ] diff --git a/config/reader/src/parseCreds.ts b/config/reader/src/parseCreds.ts index 2f3d8ebe67..e9bec19d32 100644 --- a/config/reader/src/parseCreds.ts +++ b/config/reader/src/parseCreds.ts @@ -59,7 +59,7 @@ function parseBasicAuth ({ authPassword, }: Pick): BasicAuth | undefined { if (authPairBase64) { - const pair = atob(authPairBase64) + const pair = decodeBase64Credential(authPairBase64, '_auth') const colonIndex = pair.indexOf(':') if (colonIndex < 0) { throw new AuthMissingSeparatorError() @@ -70,12 +70,44 @@ function parseBasicAuth ({ } if (authUsername && authPassword) { - return { username: authUsername, password: atob(authPassword) } + return { username: authUsername, password: decodeBase64Credential(authPassword, '_password') } } return undefined } +function decodeBase64Credential (value: string, key: '_auth' | '_password'): string { + try { + return atob(value) + } catch { + const normalizedValue = normalizeBase64Padding(value) + if (normalizedValue !== value) { + try { + return atob(normalizedValue) + } catch {} + } + throw new AuthBase64DecodeError(key) + } +} + +function normalizeBase64Padding (value: string): string { + let paddingStart = value.length + while (paddingStart > 0 && value[paddingStart - 1] === '=') { + paddingStart-- + } + + const valueWithoutPadding = value.slice(0, paddingStart) + if (!valueWithoutPadding) return value + + const remainder = valueWithoutPadding.length % 4 + if (remainder === 1) return value + + return valueWithoutPadding.padEnd( + valueWithoutPadding.length + (4 - remainder) % 4, + '=' + ) +} + export class AuthMissingSeparatorError extends PnpmError { constructor () { super('AUTH_MISSING_SEPARATOR', 'No separator found in the decoded form of _auth', { @@ -84,6 +116,14 @@ export class AuthMissingSeparatorError extends PnpmError { } } +export class AuthBase64DecodeError extends PnpmError { + constructor (key: '_auth' | '_password') { + super('AUTH_INVALID_BASE64', `Failed to decode ${key} as base64`, { + hint: `${key} must contain a base64-encoded ${key === '_auth' ? ':' : 'password'} value`, + }) + } +} + /** Characters reserved for more advanced features in the future. */ const RESERVED_CHARACTERS = new Set(['$', '%', '`', '"', "'"]) diff --git a/config/reader/src/types.ts b/config/reader/src/types.ts index 7be4a07038..7cc351f71f 100644 --- a/config/reader/src/types.ts +++ b/config/reader/src/types.ts @@ -119,6 +119,7 @@ export const pnpmTypes = { 'strict-dep-builds': Boolean, 'strict-store-pkg-content-check': Boolean, 'strict-peer-dependencies': Boolean, + 'trust-lockfile': Boolean, 'trust-policy': ['off', 'no-downgrade'] satisfies TrustPolicy[], 'trust-policy-exclude': [String, Array], 'trust-policy-ignore-after': Number, @@ -138,6 +139,7 @@ export const pnpmTypes = { 'test-pattern': [String, Array], 'changed-files-ignore-pattern': [String, Array], 'embed-readme': Boolean, + 'skip-manifest-obfuscation': Boolean, 'update-notifier': Boolean, 'agent': [null, String], 'registry-supports-time-field': Boolean, diff --git a/config/reader/test/fixtures/pkg-with-legacy-pnpm-field/package.json b/config/reader/test/fixtures/pkg-with-legacy-pnpm-field/package.json new file mode 100644 index 0000000000..adcad06407 --- /dev/null +++ b/config/reader/test/fixtures/pkg-with-legacy-pnpm-field/package.json @@ -0,0 +1,10 @@ +{ + "pnpm": { + "overrides": { + "lodash": "^4.17.21" + }, + "patchedDependencies": { + "is-odd": "patches/is-odd.patch" + } + } +} diff --git a/config/reader/test/fixtures/pkg-with-pnpm-app-field/package.json b/config/reader/test/fixtures/pkg-with-pnpm-app-field/package.json new file mode 100644 index 0000000000..9cfaa842a7 --- /dev/null +++ b/config/reader/test/fixtures/pkg-with-pnpm-app-field/package.json @@ -0,0 +1,7 @@ +{ + "pnpm": { + "app": { + "entry": "dist/index.js" + } + } +} diff --git a/config/reader/test/fixtures/pkg-with-unknown-pnpm-field/package.json b/config/reader/test/fixtures/pkg-with-unknown-pnpm-field/package.json new file mode 100644 index 0000000000..3e90c1aada --- /dev/null +++ b/config/reader/test/fixtures/pkg-with-unknown-pnpm-field/package.json @@ -0,0 +1,5 @@ +{ + "pnpm": { + "foo": "bar" + } +} diff --git a/config/reader/test/index.ts b/config/reader/test/index.ts index 66c0b91a64..78df132d24 100644 --- a/config/reader/test/index.ts +++ b/config/reader/test/index.ts @@ -166,6 +166,47 @@ test('runtimeOnFail=ignore overrides an existing onFail=download and removes nod expect(context.rootProjectManifest?.devDependencies?.node).toBeUndefined() }) +test('devEngines.packageManager without onFail resolves to the documented pmOnFail default "download" (#11676)', async () => { + prepare({ + devEngines: { + packageManager: { + name: 'pnpm', + version: '11.0.0', + }, + }, + }) + + const { context } = await getConfig({ + cliOptions: {}, + packageManager: { name: 'pnpm', version: '11.0.0' }, + }) + + expect(context.wantedPackageManager).toMatchObject({ + name: 'pnpm', + version: '11.0.0', + onFail: 'download', + }) +}) + +test('devEngines.packageManager with explicit onFail is respected (regression guard for #11676)', async () => { + prepare({ + devEngines: { + packageManager: { + name: 'pnpm', + version: '11.0.0', + onFail: 'error', + }, + }, + }) + + const { context } = await getConfig({ + cliOptions: {}, + packageManager: { name: 'pnpm', version: '11.0.0' }, + }) + + expect(context.wantedPackageManager?.onFail).toBe('error') +}) + test('throw error if --link-workspace-packages is used with --global', async () => { await expect(getConfig({ cliOptions: { @@ -297,15 +338,19 @@ test('.npmrc does not load pnpm settings', async () => { }, }) - // rc options appear as usual + // rc options appear as usual. Unscoped credentials (`username`, + // `_authToken`) are rescoped to the file's registry at load — the .npmrc + // here doesn't set its own `registry=`, so they pin to the npmjs default. expect(config.authConfig).toMatchObject({ '//my-org.registry.example.com:username': 'some-employee', '//my-org.registry.example.com:_authToken': 'some-employee-token', '@my-org:registry': 'https://my-org.registry.example.com', '@jsr:registry': 'https://not-actually-jsr.example.com', - username: 'example-user-name', - _authToken: 'example-auth-token', + '//registry.npmjs.org/:username': 'example-user-name', + '//registry.npmjs.org/:_authToken': 'example-auth-token', }) + expect(config.authConfig.username).toBeUndefined() + expect(config.authConfig._authToken).toBeUndefined() // workspace-specific settings are omitted expect(config.authConfig['dlx-cache-max-age']).toBeUndefined() @@ -581,6 +626,43 @@ test('pnpm-workspace.yaml registries override the same scope from .npmrc (#11492 expect(config.registries['@my-org']).toBe('https://from-workspace-yaml.example.com/') }) +test('pnpm-workspace.yaml registries.default is reflected in config.registry (#10099)', async () => { + prepareEmpty() + + writeYamlFileSync('pnpm-workspace.yaml', { + registries: { + default: 'https://private.example.com/', + }, + }) + + const { config } = await getConfig({ + cliOptions: {}, + packageManager: { name: 'pnpm', version: '1.0.0' }, + workspaceDir: process.cwd(), + }) + + expect(config.registry).toBe('https://private.example.com/') + expect(config.registries.default).toBe('https://private.example.com/') +}) + +test('CLI --registry overrides pnpm-workspace.yaml registries.default (#10099)', async () => { + prepareEmpty() + + writeYamlFileSync('pnpm-workspace.yaml', { + registries: { + default: 'https://workspace.example.com/', + }, + }) + + const { config } = await getConfig({ + cliOptions: { registry: 'https://cli.example.com/' }, + packageManager: { name: 'pnpm', version: '1.0.0' }, + workspaceDir: process.cwd(), + }) + + expect(config.registry).toBe('https://cli.example.com/') +}) + test('auth tokens from pnpm auth file override ~/.npmrc', async () => { prepareEmpty() @@ -776,6 +858,307 @@ describe('unresolved ${VAR} placeholders in .npmrc auth values', () => { }) }) +describe('unscoped credentials are pinned to the registry declared in their source file', () => { + // Each .npmrc / auth.ini gets its unscoped credential keys rewritten to + // URL-scoped form using the same source's `registry=` value (or the npmjs + // default if it has none). A later layer overriding `registry=` therefore + // cannot rebind the credential to its own registry — the credential is + // already pinned to the URL its author intended. + let originalXdg: string | undefined + let configHome: string + let userconfig: string + + beforeEach(() => { + prepareEmpty() + fs.mkdirSync('user-home') + userconfig = path.resolve('user-home', '.npmrc') + configHome = path.resolve('xdg-config') + fs.mkdirSync(path.join(configHome, 'pnpm'), { recursive: true }) + originalXdg = process.env.XDG_CONFIG_HOME + process.env.XDG_CONFIG_HOME = configHome + }) + + afterEach(() => { + if (originalXdg != null) { + process.env.XDG_CONFIG_HOME = originalXdg + } else { + delete process.env.XDG_CONFIG_HOME + } + }) + + test('pins user-level _authToken to that file\'s registry, never the workspace registry', async () => { + fs.writeFileSync(userconfig, 'registry=https://trusted.example.com/\n_authToken=user-secret\n', 'utf8') + fs.writeFileSync('.npmrc', 'registry=https://attacker.example.com/\n', 'utf8') + + const { config } = await getConfig({ + cliOptions: { userconfig }, + env: { ...env, XDG_CONFIG_HOME: configHome }, + packageManager: { name: 'pnpm', version: '1.0.0' }, + workspaceDir: process.cwd(), + }) + + expect(config.configByUri).toMatchObject({ + '//trusted.example.com/': { creds: { authToken: 'user-secret' } }, + }) + expect(config.configByUri['//attacker.example.com/']).toBeUndefined() + }) + + test('pins user-level _auth (basic) the same way', async () => { + // cspell:disable-next-line + fs.writeFileSync(userconfig, 'registry=https://trusted.example.com/\n_auth=dXNlcjpwYXNz\n', 'utf8') + fs.writeFileSync('.npmrc', 'registry=https://attacker.example.com/\n', 'utf8') + + const { config } = await getConfig({ + cliOptions: { userconfig }, + env: { ...env, XDG_CONFIG_HOME: configHome }, + packageManager: { name: 'pnpm', version: '1.0.0' }, + workspaceDir: process.cwd(), + }) + + expect(config.configByUri).toMatchObject({ + '//trusted.example.com/': { creds: { basicAuth: { username: 'user', password: 'pass' } } }, + }) + expect(config.configByUri['//attacker.example.com/']).toBeUndefined() + }) + + test('pins user-level username/_password the same way', async () => { + // cspell:disable-next-line + fs.writeFileSync(userconfig, 'registry=https://trusted.example.com/\nusername=alice\n_password=cGFzcw==\n', 'utf8') + fs.writeFileSync('.npmrc', 'registry=https://attacker.example.com/\n', 'utf8') + + const { config } = await getConfig({ + cliOptions: { userconfig }, + env: { ...env, XDG_CONFIG_HOME: configHome }, + packageManager: { name: 'pnpm', version: '1.0.0' }, + workspaceDir: process.cwd(), + }) + + expect(config.configByUri).toMatchObject({ + '//trusted.example.com/': { creds: { basicAuth: { username: 'alice', password: 'pass' } } }, + }) + expect(config.configByUri['//attacker.example.com/']).toBeUndefined() + }) + + test('auth.ini with no registry of its own falls back to the npmjs default', async () => { + // The split-file case: ~/.npmrc declares a registry but no creds; auth.ini + // declares an unscoped credential with no registry. Each file rescopes in + // isolation, so the credential pins to the builtin npmjs default — NOT to + // whatever the workspace later overrides the merged registry to. + fs.writeFileSync(userconfig, 'registry=https://trusted.example.com/\n', 'utf8') + fs.writeFileSync( + path.join(configHome, 'pnpm', 'auth.ini'), + '_authToken=user-secret\n', + 'utf8' + ) + fs.writeFileSync('.npmrc', 'registry=https://attacker.example.com/\n', 'utf8') + + const { config } = await getConfig({ + cliOptions: { userconfig }, + env: { ...env, XDG_CONFIG_HOME: configHome }, + packageManager: { name: 'pnpm', version: '1.0.0' }, + workspaceDir: process.cwd(), + }) + + expect(config.configByUri).toMatchObject({ + '//registry.npmjs.org/': { creds: { authToken: 'user-secret' } }, + }) + expect(config.configByUri['//attacker.example.com/']).toBeUndefined() + expect(config.configByUri['//trusted.example.com/']).toBeUndefined() + }) + + test('user-level credentials work when no workspace .npmrc exists', async () => { + fs.writeFileSync(userconfig, 'registry=https://trusted.example.com/\n_authToken=user-secret\n', 'utf8') + + const { config } = await getConfig({ + cliOptions: { userconfig }, + env: { ...env, XDG_CONFIG_HOME: configHome }, + packageManager: { name: 'pnpm', version: '1.0.0' }, + workspaceDir: process.cwd(), + }) + + expect(config.configByUri).toMatchObject({ + '//trusted.example.com/': { creds: { authToken: 'user-secret' } }, + }) + }) + + test('workspace-supplied unscoped credentials pin to the workspace registry', async () => { + fs.writeFileSync(userconfig, '', 'utf8') + fs.writeFileSync('.npmrc', 'registry=https://workspace.example.com/\n_authToken=workspace-token\n', 'utf8') + + const { config } = await getConfig({ + cliOptions: { userconfig }, + env: { ...env, XDG_CONFIG_HOME: configHome }, + packageManager: { name: 'pnpm', version: '1.0.0' }, + workspaceDir: process.cwd(), + }) + + expect(config.configByUri).toMatchObject({ + '//workspace.example.com/': { creds: { authToken: 'workspace-token' } }, + }) + }) + + test('explicit URL-scoped credentials pass through unchanged', async () => { + fs.writeFileSync( + userconfig, + 'registry=https://trusted.example.com/\n//trusted.example.com/:_authToken=user-secret\n', + 'utf8' + ) + fs.writeFileSync('.npmrc', 'registry=https://attacker.example.com/\n', 'utf8') + + const { config, warnings } = await getConfig({ + cliOptions: { userconfig }, + env: { ...env, XDG_CONFIG_HOME: configHome }, + packageManager: { name: 'pnpm', version: '1.0.0' }, + workspaceDir: process.cwd(), + }) + + expect(config.configByUri).toMatchObject({ + '//trusted.example.com/': { creds: { authToken: 'user-secret' } }, + }) + // URL-scoped tokens should NOT trigger the deprecation warning. + expect(warnings.join('\n')).not.toMatch(/deprecated/i) + }) + + test('CLI --registry override does not pull an unscoped user-level token along', async () => { + // Same trust boundary as the workspace case: an unscoped token is ambient + // and shouldn't follow whatever registry the CLI happens to point at. + fs.writeFileSync(userconfig, '_authToken=user-secret\n', 'utf8') + + const { config } = await getConfig({ + cliOptions: { userconfig, registry: 'https://attacker.example.com/' }, + env: { ...env, XDG_CONFIG_HOME: configHome }, + packageManager: { name: 'pnpm', version: '1.0.0' }, + workspaceDir: process.cwd(), + }) + + // The token rescoped to the npmjs default when the user file was read. + expect(config.configByUri).toMatchObject({ + '//registry.npmjs.org/': { creds: { authToken: 'user-secret' } }, + }) + expect(config.configByUri['//attacker.example.com/']).toBeUndefined() + }) + + test('pins inline client cert/key to the file\'s registry, never the workspace registry', async () => { + const inlineCert = '-----BEGIN CERTIFICATE-----\\ncertbody\\n-----END CERTIFICATE-----' + const inlineKey = '-----BEGIN PRIVATE KEY-----\\nkeybody\\n-----END PRIVATE KEY-----' + fs.writeFileSync( + userconfig, + `registry=https://trusted.example.com/\ncert=${inlineCert}\nkey=${inlineKey}\n`, + 'utf8' + ) + fs.writeFileSync('.npmrc', 'registry=https://attacker.example.com/\n', 'utf8') + + const { config } = await getConfig({ + cliOptions: { userconfig }, + env: { ...env, XDG_CONFIG_HOME: configHome }, + packageManager: { name: 'pnpm', version: '1.0.0' }, + workspaceDir: process.cwd(), + }) + + // `\n` escapes are expanded to real newlines by getNetworkConfigs. + expect(config.configByUri['//trusted.example.com/']?.tls).toMatchObject({ + cert: inlineCert.replace(/\\n/g, '\n'), + key: inlineKey.replace(/\\n/g, '\n'), + }) + expect(config.configByUri['//attacker.example.com/']).toBeUndefined() + }) + +}) + +describe('unscoped credential deprecation warning', () => { + // pnpm warns whenever it reads any unscoped auth value from an .npmrc / + // auth.ini, regardless of whether the rebind defense fires. URL-scoped tokens + // have been npm's recommended pattern since npm@9, and unscoped credentials + // are slated for removal in a future major release. + let originalXdg: string | undefined + let configHome: string + let userconfig: string + + beforeEach(() => { + prepareEmpty() + fs.mkdirSync('user-home') + userconfig = path.resolve('user-home', '.npmrc') + configHome = path.resolve('xdg-config') + fs.mkdirSync(path.join(configHome, 'pnpm'), { recursive: true }) + originalXdg = process.env.XDG_CONFIG_HOME + process.env.XDG_CONFIG_HOME = configHome + }) + + afterEach(() => { + if (originalXdg != null) { + process.env.XDG_CONFIG_HOME = originalXdg + } else { + delete process.env.XDG_CONFIG_HOME + } + }) + + test('warns about unscoped _authToken in user .npmrc', async () => { + fs.writeFileSync(userconfig, 'registry=https://example.com/\n_authToken=secret\n', 'utf8') + + const { warnings } = await getConfig({ + cliOptions: { userconfig }, + env: { ...env, XDG_CONFIG_HOME: configHome }, + packageManager: { name: 'pnpm', version: '1.0.0' }, + workspaceDir: process.cwd(), + }) + + expect(warnings.find(w => w.includes('Unscoped per-registry settings'))).toBeDefined() + expect(warnings.find(w => w.includes('_authToken'))).toBeDefined() + expect(warnings.find(w => w.includes(userconfig))).toBeDefined() + }) + + test('warns about unscoped _auth, username, _password', async () => { + // _auth and _password are base64-encoded per npm convention. + // cspell:disable-next-line + fs.writeFileSync(userconfig, '_auth=dXNlcjpwYXNz\nusername=alice\n_password=cGFzcw==\n', 'utf8') + + const { warnings } = await getConfig({ + cliOptions: { userconfig }, + env: { ...env, XDG_CONFIG_HOME: configHome }, + packageManager: { name: 'pnpm', version: '1.0.0' }, + workspaceDir: process.cwd(), + }) + + const warning = warnings.find(w => w.includes('Unscoped per-registry settings')) + expect(warning).toBeDefined() + expect(warning).toContain('_auth') + expect(warning).toContain('username') + expect(warning).toContain('_password') + }) + + test('warns about unscoped credentials in workspace .npmrc too', async () => { + fs.writeFileSync('.npmrc', 'registry=https://workspace.example.com/\n_authToken=workspace-token\n', 'utf8') + + const { warnings } = await getConfig({ + cliOptions: {}, + env: { ...env, XDG_CONFIG_HOME: configHome }, + packageManager: { name: 'pnpm', version: '1.0.0' }, + workspaceDir: process.cwd(), + }) + + const warning = warnings.find(w => w.includes('Unscoped per-registry settings')) + expect(warning).toBeDefined() + expect(warning).toContain(path.resolve('.npmrc')) + }) + + test('does not warn when only URL-scoped credentials are present', async () => { + fs.writeFileSync( + userconfig, + 'registry=https://example.com/\n//example.com/:_authToken=secret\n', + 'utf8' + ) + + const { warnings } = await getConfig({ + cliOptions: { userconfig }, + env: { ...env, XDG_CONFIG_HOME: configHome }, + packageManager: { name: 'pnpm', version: '1.0.0' }, + workspaceDir: process.cwd(), + }) + + expect(warnings.find(w => w.includes('Unscoped per-registry settings'))).toBeUndefined() + }) +}) + test('throw error if --save-prod is used with --save-peer', async () => { await expect(getConfig({ cliOptions: { @@ -1447,6 +1830,27 @@ test('getConfig() should read cafile', async () => { -----END CERTIFICATE-----`]) }) +// Regression for https://github.com/pnpm/pnpm/issues/11624. +test('getConfig() resolves a relative cafile= from .npmrc against the npmrc directory, not process.cwd()', async () => { + prepareEmpty() + const projectDir = path.resolve('project') + fs.mkdirSync(path.join(projectDir, 'certs'), { recursive: true }) + fs.writeFileSync( + path.join(projectDir, 'certs', 'ca.pem'), + 'relative-ca\n-----END CERTIFICATE-----' + ) + fs.writeFileSync(path.join(projectDir, '.npmrc'), 'cafile=certs/ca.pem\n') + + // process.cwd() is the prepareEmpty() root, *not* projectDir — i.e. the same + // shape as `pnpm --dir install` invoked from a sibling cwd. + const { config } = await getConfig({ + cliOptions: { dir: projectDir }, + packageManager: { name: 'pnpm', version: '1.0.0' }, + }) + + expect(config.ca).toStrictEqual(['relative-ca\n-----END CERTIFICATE-----']) +}) + test('getConfig() should read inline SSL certificates from .npmrc', async () => { prepareEmpty() @@ -1627,6 +2031,47 @@ test('do not return a warning if a package.json has workspaces field and there i expect(warnings).toStrictEqual([]) }) +test('return a warning if a package.json has a legacy "pnpm" field with ignored settings', async () => { + const prefix = f.find('pkg-with-legacy-pnpm-field') + const { warnings } = await getConfig({ + cliOptions: { dir: prefix }, + packageManager: { + name: 'pnpm', + version: '1.0.0', + }, + }) + + expect(warnings).toStrictEqual([ + 'The "pnpm" field in package.json is no longer read by pnpm. The following keys were ignored: "pnpm.overrides", "pnpm.patchedDependencies". See https://pnpm.io/settings for the new home of each setting.', + ]) +}) + +test('do not return a warning if a package.json "pnpm" field only contains keys that are still actively read (e.g. "pnpm.app")', async () => { + const prefix = f.find('pkg-with-pnpm-app-field') + const { warnings } = await getConfig({ + cliOptions: { dir: prefix }, + packageManager: { + name: 'pnpm', + version: '1.0.0', + }, + }) + + expect(warnings).toStrictEqual([]) +}) + +test('do not return a warning if a package.json "pnpm" field only contains keys unrelated to migrated settings (e.g. set by third-party tooling)', async () => { + const prefix = f.find('pkg-with-unknown-pnpm-field') + const { warnings } = await getConfig({ + cliOptions: { dir: prefix }, + packageManager: { + name: 'pnpm', + version: '1.0.0', + }, + }) + + expect(warnings).toStrictEqual([]) +}) + test('read PNPM_HOME defined in environment variables', async () => { const oldEnv = process.env const homeDir = './specified-dir' diff --git a/config/reader/test/parseCreds.test.ts b/config/reader/test/parseCreds.test.ts index 2398963f64..6b520c46a5 100644 --- a/config/reader/test/parseCreds.test.ts +++ b/config/reader/test/parseCreds.test.ts @@ -1,6 +1,7 @@ import { describe, expect, test } from '@jest/globals' import { + AuthBase64DecodeError, AuthMissingSeparatorError, type Creds, parseCreds, @@ -52,6 +53,23 @@ describe('parseCreds', () => { })).toThrow(new AuthMissingSeparatorError()) }) + test('authPairBase64 allows redundant trailing padding', () => { + expect(parseCreds({ + authPairBase64: `${btoa('foo:bar')}=`, + })).toStrictEqual({ + basicAuth: { + username: 'foo', + password: 'bar', + }, + } as Creds) + }) + + test('authPairBase64 must be base64', () => { + expect(() => parseCreds({ + authPairBase64: 'foo*bar', + })).toThrow(new AuthBase64DecodeError('_auth')) + }) + test('authUsername and authPassword', () => { expect(parseCreds({ authUsername: 'foo', @@ -72,6 +90,25 @@ describe('parseCreds', () => { })).toBeUndefined() }) + test('authPassword allows redundant trailing padding', () => { + expect(parseCreds({ + authUsername: 'foo', + authPassword: `${btoa('bar')}=`, + })).toStrictEqual({ + basicAuth: { + username: 'foo', + password: 'bar', + }, + } as Creds) + }) + + test('authPassword must be base64', () => { + expect(() => parseCreds({ + authUsername: 'foo', + authPassword: 'bar*baz', + })).toThrow(new AuthBase64DecodeError('_password')) + }) + test('tokenHelper', () => { expect(parseCreds({ tokenHelper: 'example-token-helper --foo --bar baz', diff --git a/config/version-policy/CHANGELOG.md b/config/version-policy/CHANGELOG.md index 00830cf1dc..152d570735 100644 --- a/config/version-policy/CHANGELOG.md +++ b/config/version-policy/CHANGELOG.md @@ -1,5 +1,29 @@ # @pnpm/config.version-policy +## 1100.1.2 + +### Patch Changes + +- Updated dependencies [35d2355] + - @pnpm/types@1101.2.0 + +## 1100.1.1 + +### Patch Changes + +- Updated dependencies [64afc92] + - @pnpm/types@1101.1.1 + +## 1100.1.0 + +### Minor Changes + +- b6e2c8c: Make `pnpm self-update` respect `minimumReleaseAge` (and `minimumReleaseAgeExclude`) when resolving which pnpm version to install. + + When the `latest` dist-tag points to a version newer than the configured age threshold, `self-update` now selects the newest mature version instead unless excluded by `minimumReleaseAgeExclude`. + + Also makes `dlx` and `outdated` surface invalid `minimumReleaseAgeExclude` patterns under the same `ERR_PNPM_INVALID_MINIMUM_RELEASE_AGE_EXCLUDE` error code already used by `install`, instead of leaking the internal `ERR_PNPM_INVALID_VERSION_UNION` / `ERR_PNPM_NAME_PATTERN_IN_VERSION_UNION` codes. + ## 1100.0.3 ### Patch Changes diff --git a/config/version-policy/package.json b/config/version-policy/package.json index 79c9a332d0..fe894967b5 100644 --- a/config/version-policy/package.json +++ b/config/version-policy/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/config.version-policy", - "version": "1100.0.3", + "version": "1100.1.2", "description": "Parses and evaluates package version policy specs and produces package-version matchers", "keywords": [ "pnpm", diff --git a/config/writer/CHANGELOG.md b/config/writer/CHANGELOG.md index 89e98681e9..48e9ed01c1 100644 --- a/config/writer/CHANGELOG.md +++ b/config/writer/CHANGELOG.md @@ -1,5 +1,27 @@ # @pnpm/config.config-writer +## 1100.0.10 + +### Patch Changes + +- Updated dependencies [35d2355] + - @pnpm/types@1101.2.0 + - @pnpm/workspace.workspace-manifest-writer@1100.0.10 + +## 1100.0.9 + +### Patch Changes + +- Updated dependencies [64afc92] + - @pnpm/types@1101.1.1 + - @pnpm/workspace.workspace-manifest-writer@1100.0.9 + +## 1100.0.8 + +### Patch Changes + +- @pnpm/workspace.workspace-manifest-writer@1100.0.8 + ## 1100.0.7 ### Patch Changes diff --git a/config/writer/package.json b/config/writer/package.json index 708926be7e..56b1664c98 100644 --- a/config/writer/package.json +++ b/config/writer/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/config.writer", - "version": "1100.0.7", + "version": "1100.0.10", "description": "Functions for updating the configuration settings", "keywords": [ "pnpm", diff --git a/core/core-loggers/CHANGELOG.md b/core/core-loggers/CHANGELOG.md index 8ba0944378..74e1adbe07 100644 --- a/core/core-loggers/CHANGELOG.md +++ b/core/core-loggers/CHANGELOG.md @@ -1,5 +1,27 @@ # @pnpm/core-loggers +## 1100.1.2 + +### Patch Changes + +- Updated dependencies [35d2355] + - @pnpm/types@1101.2.0 + +## 1100.1.1 + +### Patch Changes + +- Updated dependencies [64afc92] + - @pnpm/types@1101.1.1 + +## 1100.1.0 + +### Minor Changes + +- 4a79336: The lockfile verifier added in #11705 now emits `pnpm:lockfile-verification` log events (`status: 'started' | 'done'`) around the registry round-trip pass, and the default reporter renders them as a transient progress line so users can see that pnpm is doing work — on a cold registry cache the round-trip can take a noticeable beat, and the previous behavior was complete silence followed by either a long pause or an error. The cached short-circuit stays silent (no logs when no work happens), and the `done` line carries the number of distinct entries that were checked plus the elapsed time. + + Pacquet parity: not ported — pacquet doesn't carry the lockfile verifier yet (see the parity note on #11705). + ## 1100.0.2 ### Patch Changes diff --git a/core/core-loggers/package.json b/core/core-loggers/package.json index 8ce60ade15..c4db4ab70a 100644 --- a/core/core-loggers/package.json +++ b/core/core-loggers/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/core-loggers", - "version": "1100.0.2", + "version": "1100.1.2", "description": "Core loggers of pnpm", "keywords": [ "pnpm", diff --git a/core/core-loggers/src/all.ts b/core/core-loggers/src/all.ts index dcd5f8d646..55ed5b730c 100644 --- a/core/core-loggers/src/all.ts +++ b/core/core-loggers/src/all.ts @@ -8,6 +8,7 @@ export * from './installCheckLogger.js' export * from './installingConfigDeps.js' export * from './lifecycleLogger.js' export * from './linkLogger.js' +export * from './lockfileVerificationLogger.js' export * from './packageImportMethodLogger.js' export * from './packageManifestLogger.js' export * from './peerDependencyIssues.js' diff --git a/core/core-loggers/src/index.ts b/core/core-loggers/src/index.ts index 9dfce3ff69..2a544ebb5f 100644 --- a/core/core-loggers/src/index.ts +++ b/core/core-loggers/src/index.ts @@ -9,6 +9,7 @@ import type { InstallingConfigDepsLog, LifecycleLog, LinkLog, + LockfileVerificationLog, PackageImportMethodLog, PackageManifestLog, PeerDependencyIssuesLog, @@ -37,6 +38,7 @@ export type Log = | IgnoredScriptsLog | LifecycleLog | LinkLog + | LockfileVerificationLog | PackageManifestLog | PackageImportMethodLog | PeerDependencyIssuesLog diff --git a/core/core-loggers/src/lockfileVerificationLogger.ts b/core/core-loggers/src/lockfileVerificationLogger.ts new file mode 100644 index 0000000000..3383771e56 --- /dev/null +++ b/core/core-loggers/src/lockfileVerificationLogger.ts @@ -0,0 +1,56 @@ +import { + type LogBase, + logger, +} from '@pnpm/logger' + +export const lockfileVerificationLogger = logger('lockfile-verification') + +export interface LockfileVerificationMessageBase { + status: 'started' | 'done' | 'failed' + /** + * Absolute path of the lockfile being verified. Omitted only when + * the verifier is invoked without a path (today only in unit tests + * that skip the cache wiring); production code paths always pass it. + */ + lockfilePath?: string +} + +export interface LockfileVerificationStartedMessage extends LockfileVerificationMessageBase { + status: 'started' + /** Number of distinct (name, version, resolution) entries about to be verified. */ + entries: number +} + +export interface LockfileVerificationDoneMessage extends LockfileVerificationMessageBase { + status: 'done' + /** Number of distinct (name, version, resolution) entries that were verified. */ + entries: number + /** Milliseconds elapsed between the matching `started` event and `done`. */ + elapsedMs: number +} + +/** + * Terminal event emitted on every exit path that emitted `started` but + * didn't succeed — both policy violations (a `PnpmError` is about to be + * thrown with the breakdown) and unexpected throws from the registry + * fan-out. Lets the reporter close out the transient `started` frame + * in ansi-diff mode so it isn't left looking like a hung spinner above + * the failure output. + */ +export interface LockfileVerificationFailedMessage extends LockfileVerificationMessageBase { + status: 'failed' + /** Number of distinct (name, version, resolution) entries that were checked before the failure. */ + entries: number + /** Milliseconds elapsed between the matching `started` event and `failed`. */ + elapsedMs: number +} + +export type LockfileVerificationMessage = + | LockfileVerificationStartedMessage + | LockfileVerificationDoneMessage + | LockfileVerificationFailedMessage + +export type LockfileVerificationLog = + & { name: 'pnpm:lockfile-verification' } + & LogBase + & LockfileVerificationMessage diff --git a/core/types/CHANGELOG.md b/core/types/CHANGELOG.md index beddbee524..74b9eeb841 100644 --- a/core/types/CHANGELOG.md +++ b/core/types/CHANGELOG.md @@ -1,5 +1,17 @@ # @pnpm/types +## 1101.2.0 + +### Minor Changes + +- 35d2355: Validate `devEngines.runtime` and `engines.runtime` version ranges for `node`, `deno`, and `bun` when `onFail` is set to `error` or `warn`. Previously these settings only had an effect with `onFail: 'download'` — the `error` and `warn` modes silently did nothing [#11818](https://github.com/pnpm/pnpm/issues/11818). Violations now throw `ERR_PNPM_BAD_RUNTIME_VERSION`. + +## 1101.1.1 + +### Patch Changes + +- 64afc92: Honor `publishConfig.access` when publishing packages. + ## 1101.1.0 ### Minor Changes diff --git a/core/types/package.json b/core/types/package.json index d945be4b42..f881561769 100644 --- a/core/types/package.json +++ b/core/types/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/types", - "version": "1101.1.0", + "version": "1101.2.0", "description": "Basic types used by pnpm", "keywords": [ "pnpm", diff --git a/core/types/src/package.ts b/core/types/src/package.ts index b2fd69c8e8..cf929aff4c 100644 --- a/core/types/src/package.ts +++ b/core/types/src/package.ts @@ -53,6 +53,14 @@ export interface DependenciesMeta { } } +export const RUNTIME_NAMES = ['node', 'deno', 'bun'] as const + +export type RuntimeName = typeof RUNTIME_NAMES[number] + +export function isRuntimeAlias (alias: string): alias is RuntimeName { + return (RUNTIME_NAMES as readonly string[]).includes(alias) +} + export interface EngineDependency { name: string version?: string @@ -64,6 +72,7 @@ type DevEngineKey = 'os' | 'cpu' | 'libc' | 'runtime' | 'packageManager' export type DevEngines = Partial> export interface PublishConfig extends Record { + access?: 'public' | 'restricted' directory?: string linkDirectory?: boolean executableFiles?: string[] diff --git a/cspell.json b/cspell.json index 3db4fc4b86..6665bd6110 100644 --- a/cspell.json +++ b/cspell.json @@ -55,6 +55,8 @@ "denoland", "denolib", "deptype", + "desugared", + "desugars", "devextreme", "devowl", "dgimuvys", @@ -132,6 +134,7 @@ "idempotency", "imagetools", "imurmurhash", + "invalidformat", "ionicons", "isexe", "istvan", @@ -139,6 +142,7 @@ "jega", "jhcg", "jnbpamcxayl", + "junyi", "kebabcase", "kevva", "keyfile", @@ -213,6 +217,7 @@ "packlist", "packr", "packument", + "packuments", "pacquet", "paralleljs", "parallelly", @@ -231,6 +236,7 @@ "pnpmjs", "pnpmrc", "pnpmtest", + "pnpr", "polyfilling", "português", "posix", @@ -270,6 +276,7 @@ "rcompare", "redownload", "refclone", + "refetched", "reflattened", "reflink", "reflinked", @@ -277,16 +284,23 @@ "rehoist", "reimagining", "reka", + "Rekor", "relinks", "renderable", "replit", "reqheaders", + "rescopable", + "rescope", + "rescoped", + "rescopes", + "rescoping", "rimrafed", "rmgr", "rpmdevtools", "rpmlint", "rstacruz", "rushstack", + "rustup", "safecrlf", "scopeless", "sdiff", @@ -304,6 +318,7 @@ "sigstore", "sindresorhus", "sirv", + "SLSA", "soporan", "sopts", "spdxdocs", @@ -315,6 +330,7 @@ "stdtype", "streamsearch", "stringifying", + "subcmd", "subdep", "subdependencies", "subdependency", @@ -335,10 +351,12 @@ "teambit", "tempy", "testcase", + "tlog", "TLSV", "toctou", "todomvc", "toplevel", + "TOTP", "tsgo", "tsparticles", "typecheck", @@ -349,6 +367,7 @@ "unextractable", "uninstallation", "unnest", + "unparseable", "unreviewed", "unskip", "unstar", diff --git a/deps/compliance/audit/CHANGELOG.md b/deps/compliance/audit/CHANGELOG.md index 8983023f7f..1d9a2f9243 100644 --- a/deps/compliance/audit/CHANGELOG.md +++ b/deps/compliance/audit/CHANGELOG.md @@ -1,5 +1,48 @@ # @pnpm/audit +## 1101.0.10 + +### Patch Changes + +- Updated dependencies [e55f4b5] +- Updated dependencies [35d2355] + - @pnpm/lockfile.utils@1100.0.10 + - @pnpm/types@1101.2.0 + - @pnpm/lockfile.fs@1100.1.2 + - @pnpm/deps.path@1100.0.5 + - @pnpm/lockfile.detect-dep-types@1100.0.8 + - @pnpm/lockfile.types@1100.0.8 + - @pnpm/lockfile.walker@1100.0.8 + - @pnpm/network.fetch@1100.0.7 + +## 1101.0.9 + +### Patch Changes + +- Updated dependencies [9cb48bb] +- Updated dependencies [64afc92] + - @pnpm/lockfile.fs@1100.1.1 + - @pnpm/types@1101.1.1 + - @pnpm/lockfile.types@1100.0.7 + - @pnpm/lockfile.utils@1100.0.9 + - @pnpm/deps.path@1100.0.4 + - @pnpm/lockfile.detect-dep-types@1100.0.7 + - @pnpm/lockfile.walker@1100.0.7 + - @pnpm/network.fetch@1100.0.6 + +## 1101.0.8 + +### Patch Changes + +- Updated dependencies [6e93f35] +- Updated dependencies [2a9bd89] + - @pnpm/lockfile.fs@1100.1.0 + - @pnpm/lockfile.types@1100.0.6 + - @pnpm/lockfile.utils@1100.0.8 + - @pnpm/network.fetch@1100.0.5 + - @pnpm/lockfile.detect-dep-types@1100.0.6 + - @pnpm/lockfile.walker@1100.0.6 + ## 1101.0.7 ### Patch Changes diff --git a/deps/compliance/audit/package.json b/deps/compliance/audit/package.json index 40776b1fe0..ce0c019c9b 100644 --- a/deps/compliance/audit/package.json +++ b/deps/compliance/audit/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/deps.compliance.audit", - "version": "1101.0.7", + "version": "1101.0.10", "description": "Audit a lockfile", "keywords": [ "pnpm", diff --git a/deps/compliance/commands/CHANGELOG.md b/deps/compliance/commands/CHANGELOG.md index 3e4ccc9de6..a0309dc894 100644 --- a/deps/compliance/commands/CHANGELOG.md +++ b/deps/compliance/commands/CHANGELOG.md @@ -1,5 +1,117 @@ # @pnpm/deps.compliance.commands +## 1101.2.8 + +### Patch Changes + +- Updated dependencies [a23956e] +- Updated dependencies [aa6149d] +- Updated dependencies [a456dc7] +- Updated dependencies [572842a] +- Updated dependencies [e55f4b5] +- Updated dependencies [35d2355] + - @pnpm/config.reader@1101.4.1 + - @pnpm/network.auth-header@1101.0.0 + - @pnpm/installing.commands@1100.6.0 + - @pnpm/workspace.project-manifest-reader@1100.0.9 + - @pnpm/lockfile.utils@1100.0.10 + - @pnpm/types@1101.2.0 + - @pnpm/cli.utils@1101.0.8 + - @pnpm/deps.compliance.audit@1101.0.10 + - @pnpm/deps.compliance.license-scanner@1100.0.15 + - @pnpm/deps.compliance.sbom@1100.1.5 + - @pnpm/lockfile.fs@1100.1.2 + - @pnpm/cli.meta@1100.0.5 + - @pnpm/config.pick-registry-for-package@1100.0.6 + - @pnpm/config.writer@1100.0.10 + - @pnpm/lockfile.types@1100.0.8 + - @pnpm/lockfile.walker@1100.0.8 + - @pnpm/deps.security.signatures@1101.1.4 + +## 1101.2.7 + +### Patch Changes + +- Updated dependencies [d7da112] +- Updated dependencies [3b62f9d] +- Updated dependencies [212315d] + - @pnpm/workspace.project-manifest-reader@1100.0.8 + - @pnpm/config.reader@1101.4.0 + - @pnpm/installing.commands@1100.5.0 + - @pnpm/cli.utils@1101.0.7 + - @pnpm/deps.compliance.license-scanner@1100.0.14 + - @pnpm/deps.compliance.sbom@1100.1.4 + +## 1101.2.6 + +### Patch Changes + +- Updated dependencies [881a865] + - @pnpm/installing.commands@1100.4.2 + +## 1101.2.5 + +### Patch Changes + +- Updated dependencies [097983f] + - @pnpm/config.pick-registry-for-package@1100.0.5 + - @pnpm/installing.commands@1100.4.1 + +## 1101.2.4 + +### Patch Changes + +- Updated dependencies [3687b0e] +- Updated dependencies [ced20cb] +- Updated dependencies [a620557] +- Updated dependencies [9cb48bb] +- Updated dependencies [d1b340f] +- Updated dependencies [b206a15] +- Updated dependencies [64afc92] + - @pnpm/config.reader@1101.3.3 + - @pnpm/installing.commands@1100.4.0 + - @pnpm/lockfile.fs@1100.1.1 + - @pnpm/types@1101.1.1 + - @pnpm/deps.compliance.audit@1101.0.9 + - @pnpm/deps.compliance.license-scanner@1100.0.13 + - @pnpm/deps.compliance.sbom@1100.1.3 + - @pnpm/lockfile.types@1100.0.7 + - @pnpm/lockfile.utils@1100.0.9 + - @pnpm/cli.utils@1101.0.6 + - @pnpm/workspace.project-manifest-reader@1100.0.7 + - @pnpm/cli.meta@1100.0.4 + - @pnpm/config.pick-registry-for-package@1100.0.4 + - @pnpm/config.writer@1100.0.9 + - @pnpm/lockfile.walker@1100.0.7 + - @pnpm/network.auth-header@1100.0.3 + - @pnpm/deps.security.signatures@1101.1.3 + +## 1101.2.3 + +### Patch Changes + +- Updated dependencies [4195766] +- Updated dependencies [020ac45] +- Updated dependencies [d3f8408] +- Updated dependencies [6e93f35] +- Updated dependencies [a62f959] +- Updated dependencies [ba2c884] +- Updated dependencies [2a9bd89] +- Updated dependencies [8df408c] + - @pnpm/installing.commands@1100.3.0 + - @pnpm/config.reader@1101.3.2 + - @pnpm/lockfile.fs@1100.1.0 + - @pnpm/deps.compliance.sbom@1100.1.2 + - @pnpm/lockfile.types@1100.0.6 + - @pnpm/lockfile.utils@1100.0.8 + - @pnpm/deps.compliance.audit@1101.0.8 + - @pnpm/deps.compliance.license-scanner@1100.0.12 + - @pnpm/lockfile.walker@1100.0.6 + - @pnpm/cli.utils@1101.0.5 + - @pnpm/deps.security.signatures@1101.1.2 + - @pnpm/workspace.project-manifest-reader@1100.0.6 + - @pnpm/config.writer@1100.0.8 + ## 1101.2.2 ### Patch Changes diff --git a/deps/compliance/commands/package.json b/deps/compliance/commands/package.json index 2c73a1bd03..c4f34180a6 100644 --- a/deps/compliance/commands/package.json +++ b/deps/compliance/commands/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/deps.compliance.commands", - "version": "1101.2.2", + "version": "1101.2.8", "description": "pnpm commands for audit, licenses, and sbom", "keywords": [ "pnpm", @@ -80,6 +80,7 @@ "@pnpm/test-fixtures": "workspace:*", "@pnpm/testing.command-defaults": "workspace:*", "@pnpm/testing.mock-agent": "workspace:*", + "@pnpm/testing.registry-mock": "workspace:*", "@pnpm/workspace.projects-filter": "workspace:*", "@types/ramda": "catalog:", "@types/semver": "catalog:", diff --git a/deps/compliance/commands/src/audit/audit.ts b/deps/compliance/commands/src/audit/audit.ts index 3c6df079b6..f55d12274a 100644 --- a/deps/compliance/commands/src/audit/audit.ts +++ b/deps/compliance/commands/src/audit/audit.ts @@ -213,7 +213,7 @@ export async function handler (opts: AuditOptions, params: string[] = []): Promi const { envLockfile, include, lockfile } = await loadAuditContext(opts) const networkOptions = createAuditNetworkOptions(opts) let auditReport!: AuditReport - const getAuthHeader = createGetAuthHeaderByURI(opts.configByUri, opts.registries?.default) + const getAuthHeader = createGetAuthHeaderByURI(opts.configByUri) try { auditReport = await audit(lockfile, getAuthHeader, { dispatcherOptions: { diff --git a/deps/compliance/commands/src/audit/signatures.ts b/deps/compliance/commands/src/audit/signatures.ts index 4fa9d5f32e..7e14b63e58 100644 --- a/deps/compliance/commands/src/audit/signatures.ts +++ b/deps/compliance/commands/src/audit/signatures.ts @@ -20,7 +20,7 @@ export async function auditSignatures (opts: AuditOptions): Promise<{ exitCode: throw new PnpmError('AUDIT_NO_PACKAGES', 'No installed packages found to audit') } - const getAuthHeader = createGetAuthHeaderByURI(opts.configByUri, opts.registries?.default) + const getAuthHeader = createGetAuthHeaderByURI(opts.configByUri) const networkOptions = createAuditNetworkOptions(opts) const result = await verifySignatures(packages, getAuthHeader, { ca: networkOptions.ca, diff --git a/deps/compliance/commands/test/audit/fixWithUpdate.ts b/deps/compliance/commands/test/audit/fixWithUpdate.ts index d8d1f5ce9d..1350f577b2 100644 --- a/deps/compliance/commands/test/audit/fixWithUpdate.ts +++ b/deps/compliance/commands/test/audit/fixWithUpdate.ts @@ -3,9 +3,9 @@ import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, test } from '@jest/globals' import { audit } from '@pnpm/deps.compliance.commands' import { readWantedLockfile } from '@pnpm/lockfile.fs' -import { addDistTag } from '@pnpm/registry-mock' import { fixtures } from '@pnpm/test-fixtures' import { getMockAgent, setupMockAgent, teardownMockAgent } from '@pnpm/testing.mock-agent' +import { addDistTag } from '@pnpm/testing.registry-mock' import type { DepPath } from '@pnpm/types' import { readProjectManifest } from '@pnpm/workspace.project-manifest-reader' import { filterProjectsFromDir } from '@pnpm/workspace.projects-filter' diff --git a/deps/compliance/commands/tsconfig.json b/deps/compliance/commands/tsconfig.json index db85034ef8..acec8ace2e 100644 --- a/deps/compliance/commands/tsconfig.json +++ b/deps/compliance/commands/tsconfig.json @@ -82,6 +82,9 @@ { "path": "../../../testing/mock-agent" }, + { + "path": "../../../testing/registry-mock" + }, { "path": "../../../workspace/project-manifest-reader" }, diff --git a/deps/compliance/license-scanner/CHANGELOG.md b/deps/compliance/license-scanner/CHANGELOG.md index b51e3ff194..5f036e6a70 100644 --- a/deps/compliance/license-scanner/CHANGELOG.md +++ b/deps/compliance/license-scanner/CHANGELOG.md @@ -1,5 +1,59 @@ # @pnpm/license-scanner +## 1100.0.15 + +### Patch Changes + +- Updated dependencies [e55f4b5] +- Updated dependencies [35d2355] + - @pnpm/lockfile.utils@1100.0.10 + - @pnpm/types@1101.2.0 + - @pnpm/lockfile.fs@1100.1.2 + - @pnpm/config.package-is-installable@1100.0.7 + - @pnpm/deps.path@1100.0.5 + - @pnpm/lockfile.detect-dep-types@1100.0.8 + - @pnpm/lockfile.types@1100.0.8 + - @pnpm/lockfile.walker@1100.0.8 + - @pnpm/pkg-manifest.reader@1100.0.5 + - @pnpm/store.pkg-finder@1100.0.13 + +## 1100.0.14 + +### Patch Changes + +- @pnpm/store.pkg-finder@1100.0.12 + +## 1100.0.13 + +### Patch Changes + +- Updated dependencies [9cb48bb] +- Updated dependencies [64afc92] + - @pnpm/lockfile.fs@1100.1.1 + - @pnpm/types@1101.1.1 + - @pnpm/lockfile.types@1100.0.7 + - @pnpm/lockfile.utils@1100.0.9 + - @pnpm/store.pkg-finder@1100.0.11 + - @pnpm/config.package-is-installable@1100.0.6 + - @pnpm/deps.path@1100.0.4 + - @pnpm/lockfile.detect-dep-types@1100.0.7 + - @pnpm/lockfile.walker@1100.0.7 + - @pnpm/pkg-manifest.reader@1100.0.4 + +## 1100.0.12 + +### Patch Changes + +- Updated dependencies [6e93f35] +- Updated dependencies [2a9bd89] + - @pnpm/lockfile.fs@1100.1.0 + - @pnpm/lockfile.types@1100.0.6 + - @pnpm/lockfile.utils@1100.0.8 + - @pnpm/store.pkg-finder@1100.0.10 + - @pnpm/config.package-is-installable@1100.0.5 + - @pnpm/lockfile.detect-dep-types@1100.0.6 + - @pnpm/lockfile.walker@1100.0.6 + ## 1100.0.11 ### Patch Changes diff --git a/deps/compliance/license-scanner/package.json b/deps/compliance/license-scanner/package.json index b059bddeaf..af979b78f7 100644 --- a/deps/compliance/license-scanner/package.json +++ b/deps/compliance/license-scanner/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/deps.compliance.license-scanner", - "version": "1100.0.11", + "version": "1100.0.15", "description": "Check for licenses packages", "keywords": [ "pnpm", diff --git a/deps/compliance/sbom/CHANGELOG.md b/deps/compliance/sbom/CHANGELOG.md index 8b905ce0d4..3aad77a147 100644 --- a/deps/compliance/sbom/CHANGELOG.md +++ b/deps/compliance/sbom/CHANGELOG.md @@ -1,5 +1,54 @@ # @pnpm/deps.compliance.sbom +## 1100.1.5 + +### Patch Changes + +- Updated dependencies [e55f4b5] +- Updated dependencies [35d2355] + - @pnpm/lockfile.utils@1100.0.10 + - @pnpm/types@1101.2.0 + - @pnpm/lockfile.detect-dep-types@1100.0.8 + - @pnpm/lockfile.types@1100.0.8 + - @pnpm/lockfile.walker@1100.0.8 + - @pnpm/pkg-manifest.reader@1100.0.5 + - @pnpm/resolving.resolver-base@1100.3.1 + - @pnpm/store.pkg-finder@1100.0.13 + +## 1100.1.4 + +### Patch Changes + +- @pnpm/store.pkg-finder@1100.0.12 + +## 1100.1.3 + +### Patch Changes + +- Updated dependencies [1627943] +- Updated dependencies [64afc92] + - @pnpm/resolving.resolver-base@1100.3.0 + - @pnpm/types@1101.1.1 + - @pnpm/lockfile.types@1100.0.7 + - @pnpm/lockfile.utils@1100.0.9 + - @pnpm/store.pkg-finder@1100.0.11 + - @pnpm/lockfile.detect-dep-types@1100.0.7 + - @pnpm/lockfile.walker@1100.0.7 + - @pnpm/pkg-manifest.reader@1100.0.4 + +## 1100.1.2 + +### Patch Changes + +- Updated dependencies [4195766] +- Updated dependencies [31538bf] + - @pnpm/resolving.resolver-base@1100.2.0 + - @pnpm/lockfile.types@1100.0.6 + - @pnpm/lockfile.utils@1100.0.8 + - @pnpm/store.pkg-finder@1100.0.10 + - @pnpm/lockfile.detect-dep-types@1100.0.6 + - @pnpm/lockfile.walker@1100.0.6 + ## 1100.1.1 ### Patch Changes diff --git a/deps/compliance/sbom/package.json b/deps/compliance/sbom/package.json index a9c4b8b1ee..406c7973f6 100644 --- a/deps/compliance/sbom/package.json +++ b/deps/compliance/sbom/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/deps.compliance.sbom", - "version": "1100.1.1", + "version": "1100.1.5", "description": "Generate SBOM from pnpm lockfile", "keywords": [ "pnpm", diff --git a/deps/graph-builder/CHANGELOG.md b/deps/graph-builder/CHANGELOG.md index 1210ab059d..bd3c05270d 100644 --- a/deps/graph-builder/CHANGELOG.md +++ b/deps/graph-builder/CHANGELOG.md @@ -1,5 +1,79 @@ # @pnpm/deps.graph-builder +## 1100.0.12 + +### Patch Changes + +- Updated dependencies [e55f4b5] +- Updated dependencies [35d2355] + - @pnpm/lockfile.utils@1100.0.10 + - @pnpm/types@1101.2.0 + - @pnpm/deps.graph-hasher@1100.2.2 + - @pnpm/lockfile.fs@1100.1.2 + - @pnpm/config.package-is-installable@1100.0.7 + - @pnpm/core-loggers@1100.1.2 + - @pnpm/deps.path@1100.0.5 + - @pnpm/hooks.types@1100.0.9 + - @pnpm/installing.modules-yaml@1100.0.6 + - @pnpm/store.controller-types@1100.1.2 + - @pnpm/patching.config@1100.0.5 + +## 1100.0.11 + +### Patch Changes + +- Updated dependencies [9cb48bb] +- Updated dependencies [64afc92] + - @pnpm/lockfile.fs@1100.1.1 + - @pnpm/types@1101.1.1 + - @pnpm/deps.graph-hasher@1100.2.1 + - @pnpm/hooks.types@1100.0.8 + - @pnpm/lockfile.utils@1100.0.9 + - @pnpm/store.controller-types@1100.1.1 + - @pnpm/config.package-is-installable@1100.0.6 + - @pnpm/core-loggers@1100.1.1 + - @pnpm/deps.path@1100.0.4 + - @pnpm/installing.modules-yaml@1100.0.5 + - @pnpm/patching.config@1100.0.4 + +## 1100.0.10 + +### Patch Changes + +- 3ddde2b: **fix**: anchor the side-effects-cache key and global-virtual-store hash to the project's script-runner Node — `engines.runtime` pin when present, shell `node` otherwise — instead of pnpm's own runtime. + + `ENGINE_NAME` (the `;;node` prefix used as the side-effects-cache key and the engine portion of the GVS hash) was computed from `process.version` — the Node that runs pnpm itself. That was wrong in two situations: + + 1. **`@pnpm/exe` SEA bundle.** The bundle has its own embedded Node, not the `node` on the user's `PATH` that actually spawns lifecycle scripts. Two pnpm installations on the same machine (one SEA, one npm-package) therefore disagreed on the cache key, partitioning the side-effects cache and the global virtual store across two Node majors even though both installs would run scripts on the same shell `node`. + 2. **`engines.runtime` / `devEngines.runtime` pin.** When a project pins a Node version via `devEngines.runtime` (pnpm v11+), pnpm downloads that Node into `node_modules/node/` and uses it to run lifecycle scripts. But the hash still anchored to whichever Node ran pnpm itself, not to the pinned Node — so two installs of the same project with two different runner Nodes would still disagree on the GVS slot path even though scripts run on the same pinned Node. + + Three changes: + + - `@pnpm/engine.runtime.system-node-version` now exports `engineName(nodeVersion?)`. Resolves the version in this order: explicit override → `getSystemNodeVersion()` (which already prefers `node --version` over `process.version` in SEA contexts) → `process.version`. + - `@pnpm/deps.graph-hasher` now exports `findRuntimeNodeVersion(snapshotKeys)` — scans an iterable of lockfile snapshot keys for a `node@runtime:` entry and returns its bare version string. `calcDepState` and `calcGraphNodeHash`/`iterateHashedGraphNodes` accept a `nodeVersion?` (in the options bag for the first, as a trailing parameter / ctx field for the others), forwarded to `engineName()`. The default (no override) preserves the pre-change behaviour. The legacy `ENGINE_NAME` constant in `@pnpm/constants` is unchanged so external consumers and existing tests keep working; in non-SEA, non-pinned contexts every value lines up. + - Every install-side caller of the graph-hasher (`@pnpm/installing.deps-resolver`, `@pnpm/installing.deps-restorer`, `@pnpm/installing.deps-installer`, `@pnpm/building.during-install`, `@pnpm/building.after-install`, `@pnpm/deps.graph-builder`) now derives the project's pinned runtime via `findRuntimeNodeVersion(Object.keys(graph))` once per invocation and threads it through. + + On upgrade, two one-time GVS slot churns are possible: + + - **SEA-pnpm users** without a runtime pin: slots that previously hashed under the embedded-Node major (e.g. `node26`) now hash under the shell-Node major (e.g. `node24`), matching what pacquet, the npm-published `pnpm` package, and any other pnpm-compatible tool already produce. + - **Projects with a `devEngines.runtime` pin**: slots that previously hashed under the runner's Node major now hash under the pinned Node major, matching what the lifecycle scripts will actually run on. + + In both cases the old slots become prune-eligible. + +- Updated dependencies [4195766] +- Updated dependencies [6e93f35] +- Updated dependencies [3ddde2b] +- Updated dependencies [5dc8be8] +- Updated dependencies [4a79336] +- Updated dependencies [2a9bd89] + - @pnpm/store.controller-types@1100.1.0 + - @pnpm/lockfile.fs@1100.1.0 + - @pnpm/deps.graph-hasher@1100.2.0 + - @pnpm/core-loggers@1100.1.0 + - @pnpm/hooks.types@1100.0.7 + - @pnpm/lockfile.utils@1100.0.8 + - @pnpm/config.package-is-installable@1100.0.5 + ## 1100.0.9 ### Patch Changes diff --git a/deps/graph-builder/package.json b/deps/graph-builder/package.json index f175da38ef..6b40627bdc 100644 --- a/deps/graph-builder/package.json +++ b/deps/graph-builder/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/deps.graph-builder", - "version": "1100.0.9", + "version": "1100.0.12", "description": "A package for building a dependency graph from a lockfile", "keywords": [ "pnpm", @@ -35,7 +35,6 @@ "@pnpm/core-loggers": "workspace:*", "@pnpm/deps.graph-hasher": "workspace:*", "@pnpm/deps.path": "workspace:*", - "@pnpm/engine.runtime.system-node-version": "workspace:*", "@pnpm/hooks.types": "workspace:*", "@pnpm/installing.modules-yaml": "workspace:*", "@pnpm/lockfile.fs": "workspace:*", diff --git a/deps/graph-builder/src/iteratePkgsForVirtualStore.ts b/deps/graph-builder/src/iteratePkgsForVirtualStore.ts index 5233fe5e87..4a464dce3d 100644 --- a/deps/graph-builder/src/iteratePkgsForVirtualStore.ts +++ b/deps/graph-builder/src/iteratePkgsForVirtualStore.ts @@ -4,6 +4,7 @@ import { calcGraphNodeHash, type DepsGraph, type DepsStateCache, + findRuntimeNodeVersion, type HashedDepPath, iterateHashedGraphNodes, iteratePkgMeta, @@ -11,7 +12,6 @@ import { type PkgMetaAndSnapshot, } from '@pnpm/deps.graph-hasher' import * as dp from '@pnpm/deps.path' -import { findRuntimeNodeVersion } from '@pnpm/engine.runtime.system-node-version' import type { LockfileObject } from '@pnpm/lockfile.fs' import { nameVerFromPkgSnapshot, diff --git a/deps/graph-builder/tsconfig.json b/deps/graph-builder/tsconfig.json index b1089aa3dc..f4d32f77da 100644 --- a/deps/graph-builder/tsconfig.json +++ b/deps/graph-builder/tsconfig.json @@ -24,9 +24,6 @@ { "path": "../../core/types" }, - { - "path": "../../engine/runtime/system-node-version" - }, { "path": "../../hooks/types" }, diff --git a/deps/graph-hasher/CHANGELOG.md b/deps/graph-hasher/CHANGELOG.md index d2af412b7b..11caf1101e 100644 --- a/deps/graph-hasher/CHANGELOG.md +++ b/deps/graph-hasher/CHANGELOG.md @@ -1,5 +1,78 @@ # @pnpm/calc-dep-state +## 1100.2.2 + +### Patch Changes + +- Updated dependencies [e55f4b5] +- Updated dependencies [35d2355] + - @pnpm/lockfile.utils@1100.0.10 + - @pnpm/engine.runtime.system-version@1100.0.0 + - @pnpm/types@1101.2.0 + - @pnpm/deps.path@1100.0.5 + - @pnpm/lockfile.types@1100.0.8 + - @pnpm/resolving.resolver-base@1100.3.1 + +## 1100.2.1 + +### Patch Changes + +- Updated dependencies [1627943] +- Updated dependencies [64afc92] + - @pnpm/resolving.resolver-base@1100.3.0 + - @pnpm/types@1101.1.1 + - @pnpm/lockfile.types@1100.0.7 + - @pnpm/lockfile.utils@1100.0.9 + - @pnpm/deps.path@1100.0.4 + - @pnpm/engine.runtime.system-node-version@1100.1.1 + +## 1100.2.0 + +### Minor Changes + +- 5dc8be8: **fix**: resolve the GVS hash's engine portion per-snapshot when a dependency declares its own `engines.runtime`, instead of using an install-wide value. + + Pnpm's resolver desugars a dep's `engines.runtime` into `dependencies.node: 'runtime:'`, and the bin linker spawns that dep's lifecycle scripts through the pinned Node downloaded into `/node_modules/node/`. The GVS hash and the side-effects-cache key prefix were still anchored to the install-wide runtime — so a pinning snapshot's slot encoded the wrong Node major, and a reinstall on the same host could read the cached side-effects under a key whose `;;node` triple disagreed with the Node the build actually ran on. + + Per-snapshot resolution now matches what `bins/linker` already does on a per-package basis: + + - `@pnpm/deps.graph-hasher` adds `readSnapshotRuntimePin(children)` — reads the `node` entry from one snapshot's graph children and extracts the version from a `node@runtime:` value. Pairs with the existing `findRuntimeNodeVersion(snapshotKeys)` install-wide fallback (also now exported from `@pnpm/deps.graph-hasher` rather than `@pnpm/engine.runtime.system-node-version`, where it was a poor fit — `system-node-version` is about probing the host Node, not parsing lockfile-derived strings). + - `calcDepState` and `calcGraphNodeHash` consult `readSnapshotRuntimePin(graph[depPath].children)` first and only fall back to the install-wide `nodeVersion` parameter when the snapshot doesn't pin its own Node. + + Pacquet mirrors the same precedence at the `calc_graph_node_hash` call site in `package-manager/src/virtual_store_layout.rs` — a new `find_own_runtime_node_major(snapshot)` helper reads each snapshot's `dependencies` for a `node` entry with `Prefix::Runtime` and overrides the install-wide engine when present. + + On upgrade, snapshots of dependencies that declare their own `engines.runtime` re-hash under that dep's pinned Node instead of the install-wide value. The old slots become prune-eligible. Closes [#11690](https://github.com/pnpm/pnpm/issues/11690). + +### Patch Changes + +- 3ddde2b: **fix**: anchor the side-effects-cache key and global-virtual-store hash to the project's script-runner Node — `engines.runtime` pin when present, shell `node` otherwise — instead of pnpm's own runtime. + + `ENGINE_NAME` (the `;;node` prefix used as the side-effects-cache key and the engine portion of the GVS hash) was computed from `process.version` — the Node that runs pnpm itself. That was wrong in two situations: + + 1. **`@pnpm/exe` SEA bundle.** The bundle has its own embedded Node, not the `node` on the user's `PATH` that actually spawns lifecycle scripts. Two pnpm installations on the same machine (one SEA, one npm-package) therefore disagreed on the cache key, partitioning the side-effects cache and the global virtual store across two Node majors even though both installs would run scripts on the same shell `node`. + 2. **`engines.runtime` / `devEngines.runtime` pin.** When a project pins a Node version via `devEngines.runtime` (pnpm v11+), pnpm downloads that Node into `node_modules/node/` and uses it to run lifecycle scripts. But the hash still anchored to whichever Node ran pnpm itself, not to the pinned Node — so two installs of the same project with two different runner Nodes would still disagree on the GVS slot path even though scripts run on the same pinned Node. + + Three changes: + + - `@pnpm/engine.runtime.system-node-version` now exports `engineName(nodeVersion?)`. Resolves the version in this order: explicit override → `getSystemNodeVersion()` (which already prefers `node --version` over `process.version` in SEA contexts) → `process.version`. + - `@pnpm/deps.graph-hasher` now exports `findRuntimeNodeVersion(snapshotKeys)` — scans an iterable of lockfile snapshot keys for a `node@runtime:` entry and returns its bare version string. `calcDepState` and `calcGraphNodeHash`/`iterateHashedGraphNodes` accept a `nodeVersion?` (in the options bag for the first, as a trailing parameter / ctx field for the others), forwarded to `engineName()`. The default (no override) preserves the pre-change behaviour. The legacy `ENGINE_NAME` constant in `@pnpm/constants` is unchanged so external consumers and existing tests keep working; in non-SEA, non-pinned contexts every value lines up. + - Every install-side caller of the graph-hasher (`@pnpm/installing.deps-resolver`, `@pnpm/installing.deps-restorer`, `@pnpm/installing.deps-installer`, `@pnpm/building.during-install`, `@pnpm/building.after-install`, `@pnpm/deps.graph-builder`) now derives the project's pinned runtime via `findRuntimeNodeVersion(Object.keys(graph))` once per invocation and threads it through. + + On upgrade, two one-time GVS slot churns are possible: + + - **SEA-pnpm users** without a runtime pin: slots that previously hashed under the embedded-Node major (e.g. `node26`) now hash under the shell-Node major (e.g. `node24`), matching what pacquet, the npm-published `pnpm` package, and any other pnpm-compatible tool already produce. + - **Projects with a `devEngines.runtime` pin**: slots that previously hashed under the runner's Node major now hash under the pinned Node major, matching what the lifecycle scripts will actually run on. + + In both cases the old slots become prune-eligible. + +- Updated dependencies [4195766] +- Updated dependencies [31538bf] +- Updated dependencies [3ddde2b] + - @pnpm/resolving.resolver-base@1100.2.0 + - @pnpm/engine.runtime.system-node-version@1100.1.0 + - @pnpm/lockfile.types@1100.0.6 + - @pnpm/lockfile.utils@1100.0.8 + ## 1100.1.5 ### Patch Changes diff --git a/deps/graph-hasher/package.json b/deps/graph-hasher/package.json index 608d44cdfe..afbdcdb843 100644 --- a/deps/graph-hasher/package.json +++ b/deps/graph-hasher/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/deps.graph-hasher", - "version": "1100.1.5", + "version": "1100.2.2", "description": "Calculates the state of a dependency", "keywords": [ "pnpm", @@ -33,7 +33,7 @@ "dependencies": { "@pnpm/crypto.object-hasher": "workspace:*", "@pnpm/deps.path": "workspace:*", - "@pnpm/engine.runtime.system-node-version": "workspace:*", + "@pnpm/engine.runtime.system-version": "workspace:*", "@pnpm/lockfile.types": "workspace:*", "@pnpm/lockfile.utils": "workspace:*", "@pnpm/resolving.resolver-base": "workspace:*", diff --git a/deps/graph-hasher/src/index.ts b/deps/graph-hasher/src/index.ts index 9e9d12dd29..42b726ada1 100644 --- a/deps/graph-hasher/src/index.ts +++ b/deps/graph-hasher/src/index.ts @@ -1,12 +1,81 @@ import { hashObject, hashObjectWithoutSorting } from '@pnpm/crypto.object-hasher' import { getPkgIdWithPatchHash, refToRelative } from '@pnpm/deps.path' -import { engineName } from '@pnpm/engine.runtime.system-node-version' +import { engineName } from '@pnpm/engine.runtime.system-version' import type { LockfileObject, LockfileResolution, PackageSnapshot } from '@pnpm/lockfile.types' import { nameVerFromPkgSnapshot } from '@pnpm/lockfile.utils' import { resolvePlatformSelector, selectPlatformVariant } from '@pnpm/resolving.resolver-base' import type { AllowBuild, DepPath, PkgIdWithPatchHash, SupportedArchitectures } from '@pnpm/types' import { familySync } from 'detect-libc' +/** + * Strip the `node@runtime:` prefix and any peer-context suffix `(...)` + * from a single snapshot key, returning the bare Node version (e.g. + * `"22.11.0"`) — or `undefined` if the key isn't a Node runtime pin. + * + * Peer-suffixed (`node@runtime:22.11.0(node@22.11.0)`) and bare + * (`node@runtime:22.11.0`) forms must reduce to the same answer; the + * pacquet side relies on the same rule for GVS-hash parity. + */ +function extractRuntimeNodeVersion (snapshotKey: string): string | undefined { + const prefix = 'node@runtime:' + if (!snapshotKey.startsWith(prefix)) return undefined + const versionWithPeers = snapshotKey.slice(prefix.length) + const parenAt = versionWithPeers.indexOf('(') + return parenAt === -1 ? versionWithPeers : versionWithPeers.slice(0, parenAt) +} + +/** + * Scan an iterable of lockfile snapshot keys for the resolved + * `engines.runtime` / `devEngines.runtime` Node version and return + * its bare version string (e.g. `"22.11.0"`), or `undefined` when + * no snapshot pins a runtime. + * + * Pnpm's runtime resolver writes the pinned Node into the lockfile as + * a snapshot with key `node@runtime:[()]` + * (see [`engine/runtime/node-resolver/src/index.ts`](https://github.com/pnpm/pnpm/blob/29a42efc3b/engine/runtime/node-resolver/src/index.ts)). + * The first such key found is treated as authoritative. This is fine + * as an install-wide fallback (project-pin in the typical case), but + * snapshots that pin their own Node still need + * {@link readSnapshotRuntimePin} to get a per-snapshot result. + * + * Callers typically pass `Object.keys(lockfile.packages ?? {})` — the + * in-memory `LockfileObject` merges the on-disk `packages:` and + * `snapshots:` sections under a single `packages` field, so its keys + * include every snapshot key the install will hash. + */ +export function findRuntimeNodeVersion (snapshotKeys: Iterable): string | undefined { + for (const key of snapshotKeys) { + const version = extractRuntimeNodeVersion(key) + if (version != null) return version + } + return undefined +} + +/** + * Read a single graph node's own `engines.runtime` Node pin from its + * `children` map. The resolver desugars `engines.runtime` declared on + * a dependency's manifest into `dependencies.node: 'runtime:'` + * (see [`installing/deps-resolver/src/resolveDependencies.ts`](https://github.com/pnpm/pnpm/blob/29a42efc3b/installing/deps-resolver/src/resolveDependencies.ts)), + * which then becomes a `children.node` entry pointing at the + * `node@runtime:[(peers)]` snapshot key. + * + * Returns the bare version (e.g. `"22.11.0"`) when this snapshot pins + * its own Node — or `undefined` when it doesn't and the caller should + * fall back to the install-wide pin / host probe. + * + * Per-snapshot resolution matters because the bin linker routes + * lifecycle-script spawns for a pinning package through *that + * package's* downloaded Node — anchoring the snapshot's GVS engine + * hash to an install-wide value would produce the wrong + * side-effects-cache key for cross-pinning installs. + */ +export function readSnapshotRuntimePin ( + children: Record | undefined +): string | undefined { + const ref = children?.node + return ref != null ? extractRuntimeNodeVersion(ref) : undefined +} + export type DepsGraph = Record> export interface DepsGraphNode { @@ -31,18 +100,20 @@ export function calcDepState ( includeDepGraphHash: boolean supportedArchitectures?: SupportedArchitectures /** - * Resolved `engines.runtime` / `devEngines.runtime` Node version - * for the project being installed (e.g. `"22.11.0"`). When set, - * the side-effects-cache key reflects this script-runner Node - * rather than the Node that pnpm itself is running on — see - * {@link engineName} for the full resolution order. Typically - * computed once per install via {@link findRuntimeNodeVersion} - * over the lockfile's snapshot keys. + * Install-wide fallback `engines.runtime` / `devEngines.runtime` + * Node version (e.g. `"22.11.0"`). Used only when the snapshot at + * `depPath` doesn't itself pin a Node: per-snapshot pins take + * precedence so the side-effects-cache key reflects the actual + * script-runner Node the bin linker would spawn for the package + * (see {@link readSnapshotRuntimePin}). Typically computed once + * per install via {@link findRuntimeNodeVersion} over the + * lockfile's snapshot keys. */ nodeVersion?: string } ): string { - let result = engineName(opts.nodeVersion) + const ownPin = readSnapshotRuntimePin(depsGraph[depPath as T]?.children) + let result = engineName(ownPin ?? opts.nodeVersion) if (opts.includeDepGraphHash) { const depGraphHash = calcDepGraphHash(depsGraph, cache, new Set(), depPath, opts.supportedArchitectures) result += `;deps=${depGraphHash}` @@ -108,13 +179,15 @@ export function * iterateHashedGraphNodes ( allowBuild?: AllowBuild, supportedArchitectures?: SupportedArchitectures, /** - * Resolved `engines.runtime` / `devEngines.runtime` Node version - * for the project being installed. Forwarded as-is into each - * snapshot's [`calcGraphNodeHash`] call so the engine portion of - * the GVS hash reflects the Node that will actually run lifecycle - * scripts — typically obtained via [`findRuntimeNodeVersion`] - * over the lockfile's snapshot keys. `undefined` falls back to - * [`engineName`]'s default (system `node --version`, with + * Install-wide fallback `engines.runtime` / `devEngines.runtime` + * Node version. Used only for snapshots that don't pin their own + * Node; pinning snapshots get resolved per-snapshot via + * {@link readSnapshotRuntimePin} so the GVS engine hash matches + * the Node the bin linker would actually spawn for each package + * (see [`bins/linker/src/index.ts`](https://github.com/pnpm/pnpm/blob/29a42efc3b/bins/linker/src/index.ts)). + * Typically obtained via {@link findRuntimeNodeVersion} over the + * lockfile's snapshot keys. `undefined` falls back to + * {@link engineName}'s default (system `node --version`, with * `process.version` as a last resort). */ nodeVersion?: string @@ -164,7 +237,13 @@ export function calcGraphNodeHash ( // so they survive Node.js upgrades and architecture changes. const includeEngine = builtDepPaths === undefined || transitivelyRequiresBuild(graph, builtDepPaths, buildRequiredCache ??= {}, depPath, new Set()) - const engine = includeEngine ? engineName(nodeVersion) : null + // A snapshot that declares `engines.runtime` carries the desugared + // `node@runtime:` pin as a child; that's the Node the bin + // linker spawns for its lifecycle scripts, so it has to drive the + // engine portion of the hash too. Non-pinning siblings fall through + // to the install-wide value. + const ownPin = readSnapshotRuntimePin(graph[depPath]?.children) + const engine = includeEngine ? engineName(ownPin ?? nodeVersion) : null const deps = calcDepGraphHash(graph, cache, new Set(), depPath, supportedArchitectures) const hexDigest = hashObjectWithoutSorting({ engine, deps }, { encoding: 'hex' }) return formatGlobalVirtualStorePath(name, version, hexDigest) @@ -176,6 +255,28 @@ export function calcLeafGlobalVirtualStorePath (fullPkgId: string, name: string, return formatGlobalVirtualStorePath(name, version, hexDigest) } +/** + * `subdepIds` maps each direct child's alias to its full pkg id + * (`${name}@${version}:${integrity}`). Each child contributes a leaf hash + * (no transitive walk) to the parent's hash, so the resulting path differs + * whenever the set or versions of children change. One level deep only — + * use {@link calcGraphNodeHash} when full graph traversal is needed. + */ +export function calcGlobalVirtualStorePathWithSubdeps ( + fullPkgId: string, + name: string, + version: string, + subdepIds: Record +): string { + const childHashes: Record = {} + for (const [alias, childFullPkgId] of Object.entries(subdepIds)) { + childHashes[alias] = hashObject({ id: childFullPkgId, deps: {} }) + } + const depsHash = hashObject({ id: fullPkgId, deps: childHashes }) + const hexDigest = hashObjectWithoutSorting({ engine: null, deps: depsHash }, { encoding: 'hex' }) + return formatGlobalVirtualStorePath(name, version, hexDigest) +} + // Use @/ prefix for unscoped packages to maintain uniform 4-level directory depth // Scoped: @scope/pkg/version/hash // Unscoped: @/pkg/version/hash diff --git a/deps/graph-hasher/test/calcGraphNodeHash.test.ts b/deps/graph-hasher/test/calcGraphNodeHash.test.ts index 2553e1c4d9..6d1faa9fd4 100644 --- a/deps/graph-hasher/test/calcGraphNodeHash.test.ts +++ b/deps/graph-hasher/test/calcGraphNodeHash.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from '@jest/globals' import { hashObject, hashObjectWithoutSorting } from '@pnpm/crypto.object-hasher' import { calcGraphNodeHash, type DepsGraph, type DepsStateCache, type PkgMeta } from '@pnpm/deps.graph-hasher' -import { engineName } from '@pnpm/engine.runtime.system-node-version' +import { engineName } from '@pnpm/engine.runtime.system-version' import type { DepPath, PkgIdWithPatchHash } from '@pnpm/types' // Track the same script-runner-Node value the production code uses @@ -356,6 +356,125 @@ describe('calcGraphNodeHash', () => { expect(result).toMatch(/^@my-org\/my-package\/1\.2\.3\/[a-f0-9]+$/) }) + it('uses the snapshot\'s own engines.runtime pin over an install-wide fallback', () => { + // A dep that declares `engines.runtime: node@22` carries the + // desugared `node@runtime:22.11.0` DepPath as `children.node`. + // That snapshot's GVS hash has to anchor to its *own* pin — + // matching the Node the bin linker spawns for its lifecycle + // scripts (`bins/linker/src/index.ts`'s per-package + // `runtimeHasNodeDownloaded` branch) — instead of the + // install-wide `nodeVersion` fallback that PR #11689 introduced. + const graph: DepsGraph = { + ['pinned@1.0.0' as DepPath]: { + children: { node: 'node@runtime:22.11.0' as DepPath }, + fullPkgId: 'pinned@1.0.0:sha512-pinned', + }, + ['node@runtime:22.11.0' as DepPath]: { + children: {}, + fullPkgId: 'node@runtime:22.11.0:sha512-node22', + }, + } + const pkgMeta: PkgMeta = { + depPath: 'pinned@1.0.0' as DepPath, + name: 'pinned', + version: '1.0.0', + } + + const ownPinHash = calcGraphNodeHash({ graph, cache: {}, nodeVersion: '20.0.0' }, pkgMeta) + + const depsHash = hashObject({ + id: 'pinned@1.0.0:sha512-pinned', + deps: { + node: hashObject({ id: 'node@runtime:22.11.0:sha512-node22', deps: {} }), + }, + }) + const expected = hashObjectWithoutSorting( + { engine: `${process.platform};${process.arch};node22`, deps: depsHash }, + { encoding: 'hex' } + ) + expect(ownPinHash).toBe(`@/pinned/1.0.0/${expected}`) + }) + + it('falls back to the install-wide nodeVersion when the snapshot has no own pin', () => { + // A snapshot whose own children don't include a `node@runtime:` + // entry inherits the project-wide pin instead — mirrors the + // common case where only the root manifest declares + // `engines.runtime` and every transitive dep falls through. + const graph: DepsGraph = { + ['sibling@1.0.0' as DepPath]: { + children: { dep: 'dep@1.0.0' as DepPath }, + fullPkgId: 'sibling@1.0.0:sha512-sibling', + }, + ['dep@1.0.0' as DepPath]: { + children: {}, + fullPkgId: 'dep@1.0.0:sha512-dep', + }, + } + const pkgMeta: PkgMeta = { + depPath: 'sibling@1.0.0' as DepPath, + name: 'sibling', + version: '1.0.0', + } + + const fallbackHash = calcGraphNodeHash({ graph, cache: {}, nodeVersion: '20.5.0' }, pkgMeta) + + const depsHash = hashObject({ + id: 'sibling@1.0.0:sha512-sibling', + deps: { + dep: hashObject({ id: 'dep@1.0.0:sha512-dep', deps: {} }), + }, + }) + const expected = hashObjectWithoutSorting( + { engine: `${process.platform};${process.arch};node20`, deps: depsHash }, + { encoding: 'hex' } + ) + expect(fallbackHash).toBe(`@/sibling/1.0.0/${expected}`) + }) + + it('cross-pinning siblings produce distinct engine prefixes in the same install', () => { + // Two siblings with different `engines.runtime` declarations + // surface the bug this test guards: under PR #11689's + // install-wide resolution they'd share the same engine major in + // the GVS hash (whichever `findRuntimeNodeVersion` happened to + // match first), even though the bin linker would route their + // lifecycle scripts through different downloaded Nodes. + const graph: DepsGraph = { + ['pins-22@1.0.0' as DepPath]: { + children: { node: 'node@runtime:22.11.0' as DepPath }, + fullPkgId: 'pins-22@1.0.0:sha512-a', + }, + ['pins-20@1.0.0' as DepPath]: { + children: { node: 'node@runtime:20.18.0' as DepPath }, + fullPkgId: 'pins-20@1.0.0:sha512-b', + }, + ['node@runtime:22.11.0' as DepPath]: { + children: {}, + fullPkgId: 'node@runtime:22.11.0:sha512-node22', + }, + ['node@runtime:20.18.0' as DepPath]: { + children: {}, + fullPkgId: 'node@runtime:20.18.0:sha512-node20', + }, + } + const cache: DepsStateCache = {} + + const hash22 = calcGraphNodeHash( + { graph, cache, nodeVersion: '22.11.0' }, + { depPath: 'pins-22@1.0.0' as DepPath, name: 'pins-22', version: '1.0.0' } + ) + const hash20 = calcGraphNodeHash( + { graph, cache, nodeVersion: '22.11.0' }, + { depPath: 'pins-20@1.0.0' as DepPath, name: 'pins-20', version: '1.0.0' } + ) + + // The two slots must end up on different paths even though the + // install-wide fallback is the same — the engine portion of the + // hash diverges via each snapshot's own pin. + expect(hash22).not.toBe(hash20) + expect(hash22.startsWith('@/pins-22/1.0.0/')).toBe(true) + expect(hash20.startsWith('@/pins-20/1.0.0/')).toBe(true) + }) + it('should handle prerelease versions', () => { const graph: DepsGraph = { ['pkg@1.0.0-beta.1' as DepPath]: { diff --git a/deps/graph-hasher/test/calcLeafGlobalVirtualStorePath.test.ts b/deps/graph-hasher/test/calcLeafGlobalVirtualStorePath.test.ts new file mode 100644 index 0000000000..b29e7921cc --- /dev/null +++ b/deps/graph-hasher/test/calcLeafGlobalVirtualStorePath.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from '@jest/globals' +import { calcGlobalVirtualStorePathWithSubdeps, calcLeafGlobalVirtualStorePath } from '@pnpm/deps.graph-hasher' + +describe('calcLeafGlobalVirtualStorePath', () => { + it('returns a stable path for a leaf package', () => { + const path = calcLeafGlobalVirtualStorePath('foo@1.0.0:sha512-abc', 'foo', '1.0.0') + expect(path).toMatch(/^@\/foo\/1\.0\.0\/[a-f0-9]+$/) + }) +}) + +describe('calcGlobalVirtualStorePathWithSubdeps', () => { + it('equals the leaf path when no subdeps are passed', () => { + const leafPath = calcLeafGlobalVirtualStorePath('foo@1.0.0:sha512-abc', 'foo', '1.0.0') + const withEmptySubdeps = calcGlobalVirtualStorePathWithSubdeps('foo@1.0.0:sha512-abc', 'foo', '1.0.0', {}) + expect(withEmptySubdeps).toBe(leafPath) + }) + + it('produces a different path when an optional subdep changes version', () => { + const fullPkgId = 'foo@1.0.0:sha512-abc' + const withSubdepV1 = calcGlobalVirtualStorePathWithSubdeps(fullPkgId, 'foo', '1.0.0', { + 'foo-darwin-arm64': 'foo-darwin-arm64@1.0.0:sha512-aaa', + }) + const withSubdepV2 = calcGlobalVirtualStorePathWithSubdeps(fullPkgId, 'foo', '1.0.0', { + 'foo-darwin-arm64': 'foo-darwin-arm64@1.1.0:sha512-bbb', + }) + expect(withSubdepV1).not.toBe(withSubdepV2) + }) + + it('produces a different path when an optional subdep is added', () => { + const fullPkgId = 'foo@1.0.0:sha512-abc' + const noSubdeps = calcGlobalVirtualStorePathWithSubdeps(fullPkgId, 'foo', '1.0.0', {}) + const withSubdep = calcGlobalVirtualStorePathWithSubdeps(fullPkgId, 'foo', '1.0.0', { + 'foo-darwin-arm64': 'foo-darwin-arm64@1.0.0:sha512-aaa', + }) + expect(noSubdeps).not.toBe(withSubdep) + }) + + it('is order-independent across multiple subdeps', () => { + const fullPkgId = 'foo@1.0.0:sha512-abc' + const orderA = calcGlobalVirtualStorePathWithSubdeps(fullPkgId, 'foo', '1.0.0', { + 'foo-darwin-arm64': 'foo-darwin-arm64@1.0.0:sha512-aaa', + 'foo-linux-x64': 'foo-linux-x64@1.0.0:sha512-bbb', + }) + const orderB = calcGlobalVirtualStorePathWithSubdeps(fullPkgId, 'foo', '1.0.0', { + 'foo-linux-x64': 'foo-linux-x64@1.0.0:sha512-bbb', + 'foo-darwin-arm64': 'foo-darwin-arm64@1.0.0:sha512-aaa', + }) + expect(orderA).toBe(orderB) + }) +}) diff --git a/deps/graph-hasher/test/index.ts b/deps/graph-hasher/test/index.ts index f1f85d4f21..8197d46980 100644 --- a/deps/graph-hasher/test/index.ts +++ b/deps/graph-hasher/test/index.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from '@jest/globals' import { hashObject, hashObjectWithoutSorting } from '@pnpm/crypto.object-hasher' -import { calcDepState, calcGraphNodeHash } from '@pnpm/deps.graph-hasher' -import { engineName } from '@pnpm/engine.runtime.system-node-version' +import { calcDepState, calcGraphNodeHash, findRuntimeNodeVersion, readSnapshotRuntimePin } from '@pnpm/deps.graph-hasher' +import { engineName } from '@pnpm/engine.runtime.system-version' import type { DepPath, PkgIdWithPatchHash } from '@pnpm/types' // Match the function the production code uses (see @@ -56,6 +56,63 @@ test('calcDepState() when scripts are ignored', () => { })).toBe(ENGINE_NAME) }) +test('findRuntimeNodeVersion() pulls the pinned major from a node@runtime: snapshot key', () => { + // Mirrors pacquet's `find_runtime_node_major` helper; both must + // agree on the version-extraction rule or the two tools would + // hash GVS slots under different engine majors for the same + // project. The peer-suffixed form must reduce to the same bare + // version as the form without a peer suffix. + expect( + findRuntimeNodeVersion(['leftpad@1.3.0', 'node@runtime:22.11.0']) + ).toBe('22.11.0') + expect( + findRuntimeNodeVersion(['node@runtime:22.11.0(node@22.11.0)']) + ).toBe('22.11.0') + expect( + findRuntimeNodeVersion(['leftpad@1.3.0', 'is-positive@3.1.0']) + ).toBeUndefined() +}) + +test('readSnapshotRuntimePin() pulls the own pin from a graph node child', () => { + // The resolver desugars a dep's `engines.runtime` into + // `dependencies.node: 'runtime:'` and `refToRelative` + // encodes that into the `node@runtime:[(peers)]` DepPath + // the graph carries as `children.node`. The per-snapshot lookup + // reads back the bare version from there. Without this branch + // the GVS hash for the pinning snapshot would key under the + // install-wide Node, not the Node the bin linker spawns for it. + expect(readSnapshotRuntimePin({ node: 'node@runtime:22.11.0' })).toBe('22.11.0') + expect(readSnapshotRuntimePin({ node: 'node@runtime:22.11.0(node@22.11.0)' })).toBe('22.11.0') + expect(readSnapshotRuntimePin({ node: 'node@22.11.0' })).toBeUndefined() + expect(readSnapshotRuntimePin({ leftpad: 'leftpad@1.3.0' })).toBeUndefined() + expect(readSnapshotRuntimePin({})).toBeUndefined() + expect(readSnapshotRuntimePin(undefined)).toBeUndefined() +}) + +test('calcDepState() uses the snapshot\'s own engines.runtime pin', () => { + // A package whose graph node has `children.node = node@runtime:...` + // pinned its own Node via `engines.runtime`; the side-effects-cache + // key prefix has to encode *that* major (not the install-wide + // fallback) because the bin linker spawns lifecycle scripts on the + // package's pinned Node, not the install-wide one. + const graph = { + 'pinned@1.0.0': { + pkgIdWithPatchHash: 'pinned@1.0.0' as PkgIdWithPatchHash, + resolution: { integrity: '900' }, + children: { node: 'node@runtime:22.11.0' }, + }, + 'node@runtime:22.11.0': { + pkgIdWithPatchHash: 'node@runtime:22.11.0' as PkgIdWithPatchHash, + resolution: { integrity: '901' }, + children: {}, + }, + } + expect(calcDepState(graph, {}, 'pinned@1.0.0', { + includeDepGraphHash: false, + nodeVersion: '20.5.0', // install-wide fallback differs from own pin + })).toBe(`${process.platform};${process.arch};node22`) +}) + describe('calcGraphNodeHash', () => { const graphNodeGraph = { 'foo@1.0.0': { diff --git a/deps/graph-hasher/tsconfig.json b/deps/graph-hasher/tsconfig.json index 2f3f2385d8..d28b59d81a 100644 --- a/deps/graph-hasher/tsconfig.json +++ b/deps/graph-hasher/tsconfig.json @@ -16,7 +16,7 @@ "path": "../../crypto/object-hasher" }, { - "path": "../../engine/runtime/system-node-version" + "path": "../../engine/runtime/system-version" }, { "path": "../../lockfile/types" diff --git a/deps/inspection/commands/CHANGELOG.md b/deps/inspection/commands/CHANGELOG.md index 52af161454..cd6ca5f11b 100644 --- a/deps/inspection/commands/CHANGELOG.md +++ b/deps/inspection/commands/CHANGELOG.md @@ -1,5 +1,117 @@ # @pnpm/deps.inspection.commands +## 1100.3.1 + +### Patch Changes + +- Updated dependencies [a23956e] +- Updated dependencies [a456dc7] +- Updated dependencies [35d2355] +- Updated dependencies [440e155] +- Updated dependencies [0721d64] + - @pnpm/config.reader@1101.4.1 + - @pnpm/network.auth-header@1101.0.0 + - @pnpm/deps.inspection.list@1100.0.14 + - @pnpm/types@1101.2.0 + - @pnpm/global.commands@1100.0.22 + - @pnpm/resolving.npm-resolver@1101.3.3 + - @pnpm/resolving.default-resolver@1100.3.3 + - @pnpm/cli.utils@1101.0.8 + - @pnpm/deps.inspection.outdated@1100.1.3 + - @pnpm/lockfile.fs@1100.1.2 + - @pnpm/config.pick-registry-for-package@1100.0.6 + - @pnpm/deps.inspection.peers-checker@1100.0.11 + - @pnpm/deps.inspection.peers-issues-renderer@1100.0.3 + - @pnpm/global.packages@1100.0.5 + - @pnpm/installing.modules-yaml@1100.0.6 + - @pnpm/network.fetch@1100.0.7 + - @pnpm/resolving.registry.types@1100.0.5 + +## 1100.3.0 + +### Minor Changes + +- 22cb743: Implement `pnpm repo` command natively, following `npm repo` standards. + +### Patch Changes + +- Updated dependencies [3b62f9d] +- Updated dependencies [212315d] + - @pnpm/config.reader@1101.4.0 + - @pnpm/resolving.npm-resolver@1101.3.2 + - @pnpm/cli.utils@1101.0.7 + - @pnpm/deps.inspection.list@1100.0.13 + - @pnpm/global.commands@1100.0.21 + - @pnpm/deps.inspection.outdated@1100.1.2 + - @pnpm/resolving.default-resolver@1100.3.2 + +## 1100.2.5 + +### Patch Changes + +- Updated dependencies [097983f] + - @pnpm/config.pick-registry-for-package@1100.0.5 + - @pnpm/resolving.npm-resolver@1101.3.1 + - @pnpm/deps.inspection.outdated@1100.1.1 + - @pnpm/resolving.default-resolver@1100.3.1 + - @pnpm/global.commands@1100.0.20 + +## 1100.2.4 + +### Patch Changes + +- Updated dependencies [3687b0e] +- Updated dependencies [ced20cb] +- Updated dependencies [a620557] +- Updated dependencies [9cb48bb] +- Updated dependencies [d1b340f] +- Updated dependencies [3a54205] +- Updated dependencies [1627943] +- Updated dependencies [64afc92] + - @pnpm/config.reader@1101.3.3 + - @pnpm/global.commands@1100.0.19 + - @pnpm/lockfile.fs@1100.1.1 + - @pnpm/resolving.npm-resolver@1101.3.0 + - @pnpm/deps.inspection.outdated@1100.1.0 + - @pnpm/resolving.default-resolver@1100.3.0 + - @pnpm/types@1101.1.1 + - @pnpm/deps.inspection.list@1100.0.12 + - @pnpm/deps.inspection.peers-checker@1100.0.10 + - @pnpm/cli.utils@1101.0.6 + - @pnpm/config.pick-registry-for-package@1100.0.4 + - @pnpm/deps.inspection.peers-issues-renderer@1100.0.2 + - @pnpm/global.packages@1100.0.4 + - @pnpm/installing.modules-yaml@1100.0.5 + - @pnpm/network.auth-header@1100.0.3 + - @pnpm/network.fetch@1100.0.6 + - @pnpm/resolving.registry.types@1100.0.4 + +## 1100.2.3 + +### Patch Changes + +- Updated dependencies [963861c] +- Updated dependencies [4195766] +- Updated dependencies [31538bf] +- Updated dependencies [020ac45] +- Updated dependencies [b6e2c8c] +- Updated dependencies [d3f8408] +- Updated dependencies [6e93f35] +- Updated dependencies [a62f959] +- Updated dependencies [ba2c884] +- Updated dependencies [2a9bd89] +- Updated dependencies [8df408c] + - @pnpm/resolving.npm-resolver@1101.2.0 + - @pnpm/resolving.default-resolver@1100.2.0 + - @pnpm/deps.inspection.outdated@1100.0.16 + - @pnpm/config.reader@1101.3.2 + - @pnpm/lockfile.fs@1100.1.0 + - @pnpm/global.commands@1100.0.18 + - @pnpm/deps.inspection.list@1100.0.11 + - @pnpm/deps.inspection.peers-checker@1100.0.9 + - @pnpm/network.fetch@1100.0.5 + - @pnpm/cli.utils@1101.0.5 + ## 1100.2.2 ### Patch Changes diff --git a/deps/inspection/commands/package.json b/deps/inspection/commands/package.json index fbfaa09d6d..8f2643518a 100644 --- a/deps/inspection/commands/package.json +++ b/deps/inspection/commands/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/deps.inspection.commands", - "version": "1100.2.2", + "version": "1100.3.1", "description": "The list, ll, why, and outdated commands of pnpm", "keywords": [ "pnpm", @@ -76,6 +76,7 @@ "@pnpm/registry-mock": "catalog:", "@pnpm/test-fixtures": "workspace:*", "@pnpm/testing.command-defaults": "workspace:*", + "@pnpm/testing.registry-mock": "workspace:*", "@pnpm/workspace.projects-filter": "workspace:*", "@types/hosted-git-info": "catalog:", "@types/ramda": "catalog:", diff --git a/deps/inspection/commands/src/fetchPackageInfo.ts b/deps/inspection/commands/src/fetchPackageInfo.ts index bba10be945..e08d5e0f73 100644 --- a/deps/inspection/commands/src/fetchPackageInfo.ts +++ b/deps/inspection/commands/src/fetchPackageInfo.ts @@ -14,7 +14,7 @@ import type { PackageInRegistry } from '@pnpm/resolving.registry.types' export type ExtendedPackageInfo = PackageInRegistry & { author?: string - repository?: string + repository?: string | { url?: string, directory?: string } versions: string[] versionsCount?: number depsCount?: number @@ -52,7 +52,7 @@ export async function fetchPackageInfo ( } const registry = pickRegistryForPackage(opts.registries, packageName) const fetchFromRegistry = createFetchFromRegistry(opts) - const getAuthHeader = createGetAuthHeaderByURI(opts.configByUri ?? {}, opts.registries?.default) + const getAuthHeader = createGetAuthHeaderByURI(opts.configByUri ?? {}) const fetchResult = await fetchMetadataFromFromRegistry( { fetch: fetchFromRegistry, @@ -93,7 +93,6 @@ export async function fetchPackageInfo ( return { ...data, author: typeof data.author === 'object' ? (data.author as { name: string }).name : data.author, - repository: typeof data.repository === 'object' ? (data.repository as { url: string }).url : data.repository, versions, versionsCount: versions.length > 0 ? versions.length : undefined, depsCount: depsCount > 0 ? depsCount : undefined, diff --git a/deps/inspection/commands/src/index.ts b/deps/inspection/commands/src/index.ts index eaa94bdd8a..91353dabbe 100644 --- a/deps/inspection/commands/src/index.ts +++ b/deps/inspection/commands/src/index.ts @@ -3,4 +3,5 @@ export * as docs from './docs/index.js' export { list, ll, why } from './listing/index.js' export { outdated } from './outdated/index.js' export * as peers from './peers.js' +export * as repo from './repo/index.js' export * as view from './view/index.js' diff --git a/deps/inspection/commands/src/repo/index.ts b/deps/inspection/commands/src/repo/index.ts new file mode 100644 index 0000000000..37e20bf7a4 --- /dev/null +++ b/deps/inspection/commands/src/repo/index.ts @@ -0,0 +1,113 @@ +import { docsUrl, readProjectManifestOnly } from '@pnpm/cli.utils' +import { type Config, type ConfigContext, types as allTypes } from '@pnpm/config.reader' +import { PnpmError } from '@pnpm/error' +import HostedGit from 'hosted-git-info' +import open from 'open' +import { pick } from 'ramda' +import { renderHelp } from 'render-help' + +import { fetchPackageInfo } from '../fetchPackageInfo.js' + +export function rcOptionsTypes (): Record { + return pick(['registry'], allTypes) +} + +export function cliOptionsTypes (): Record { + return rcOptionsTypes() +} + +export const commandNames = ['repo'] + +export function help (): string { + return renderHelp({ + description: "Opens the URL of the package's repository in a browser.", + url: docsUrl('repo'), + usages: ['pnpm repo [ [ ...]]'], + }) +} + +export async function handler ( + opts: Config & ConfigContext & { dir: string }, + params: string[] +): Promise { + const urls = params.length === 0 + ? [await getRepoUrlFromCurrentProject(opts)] + : await Promise.all(params.map((spec) => getRepoUrlFromRegistry(opts, spec))) + for (const url of urls) { + // eslint-disable-next-line no-await-in-loop + await open(url) + } +} + +async function getRepoUrlFromCurrentProject ( + opts: Pick +): Promise { + const manifest = await readProjectManifestOnly(opts.dir, { + engineStrict: opts.engineStrict, + nodeVersion: opts.nodeVersion, + supportedArchitectures: opts.supportedArchitectures, + }) + const url = pickRepoUrl(manifest.repository) + if (!url) { + throw new PnpmError( + 'NO_REPO_URL', + 'The current project does not have a repository URL. Add a "repository" field to its manifest.' + ) + } + return url +} + +async function getRepoUrlFromRegistry ( + opts: Config & ConfigContext, + packageSpec: string +): Promise { + const info = await fetchPackageInfo(opts, packageSpec) + const url = pickRepoUrl(info.repository) + if (!url) { + throw new PnpmError('NO_REPO_URL', `The package "${info.name}" does not have a repository URL.`) + } + return url +} + +function pickRepoUrl ( + repository: string | { url?: string, directory?: string } | undefined +): string | undefined { + if (!repository) return undefined + const repoUrl = typeof repository === 'string' ? repository : repository.url + if (!repoUrl) return undefined + const directory = typeof repository === 'object' ? repository.directory : undefined + return repositoryToWebUrl(repoUrl, directory) +} + +function repositoryToWebUrl (rawUrl: string, directory?: string): string | undefined { + const hosted = HostedGit.fromUrl(rawUrl) + if (hosted != null) { + const url = directory ? hosted.browse(directory) : hosted.browse() + if (url && isHttpUrl(url)) { + return url + } + } + let parsed: URL + try { + parsed = new URL(rawUrl.replace(/^git\+/, '')) + } catch { + return undefined + } + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return undefined + parsed.search = '' + parsed.hash = '' + parsed.pathname = parsed.pathname.replace(/\/+$/, '').replace(/\.git$/, '') + if (directory) { + parsed.pathname += `/tree/HEAD/${directory.replace(/^\//, '')}` + } + return parsed.toString() +} + +function isHttpUrl (value: string): boolean { + try { + const { protocol } = new URL(value) + return protocol === 'http:' || protocol === 'https:' + } catch { + return false + } +} diff --git a/deps/inspection/commands/test/listing/recursive.ts b/deps/inspection/commands/test/listing/recursive.ts index bb2dd5bc83..6668890755 100644 --- a/deps/inspection/commands/test/listing/recursive.ts +++ b/deps/inspection/commands/test/listing/recursive.ts @@ -6,7 +6,7 @@ import { list, why } from '@pnpm/deps.inspection.commands' import type { PnpmError } from '@pnpm/error' import { install } from '@pnpm/installing.commands' import { prepare, preparePackages } from '@pnpm/prepare' -import { addDistTag } from '@pnpm/registry-mock' +import { addDistTag } from '@pnpm/testing.registry-mock' import { filterProjectsBySelectorObjectsFromDir } from '@pnpm/workspace.projects-filter' import { writeYamlFileSync } from 'write-yaml-file' diff --git a/deps/inspection/commands/test/outdated/minimumReleaseAge.test.ts b/deps/inspection/commands/test/outdated/minimumReleaseAge.test.ts new file mode 100644 index 0000000000..bae79b4c36 --- /dev/null +++ b/deps/inspection/commands/test/outdated/minimumReleaseAge.test.ts @@ -0,0 +1,74 @@ +/// +import fs from 'node:fs' +import path from 'node:path' +import { stripVTControlCharacters as stripAnsi } from 'node:util' + +import { expect, test } from '@jest/globals' +import { outdated } from '@pnpm/deps.inspection.commands' +import { tempDir } from '@pnpm/prepare' +import { REGISTRY_MOCK_PORT } from '@pnpm/registry-mock' +import { fixtures } from '@pnpm/test-fixtures' + +const f = fixtures(import.meta.dirname) +const hasOutdatedDepsFixture = f.find('has-outdated-deps') + +const REGISTRY_URL = `http://localhost:${REGISTRY_MOCK_PORT}` + +const OUTDATED_OPTIONS = { + cacheDir: 'cache', + fetchRetries: 1, + fetchRetryFactor: 1, + fetchRetryMaxtimeout: 60, + fetchRetryMintimeout: 10, + global: false, + networkConcurrency: 16, + offline: false, + configByUri: {}, + registries: { default: REGISTRY_URL }, + strictSsl: false, + tag: 'latest', + userAgent: '', + userConfig: {}, +} + +function loadHasOutdatedDeps (): void { + tempDir() + fs.mkdirSync(path.resolve('node_modules/.pnpm'), { recursive: true }) + fs.copyFileSync(path.join(hasOutdatedDepsFixture, 'node_modules/.pnpm/lock.yaml'), path.resolve('node_modules/.pnpm/lock.yaml')) + fs.copyFileSync(path.join(hasOutdatedDepsFixture, 'package.json'), path.resolve('package.json')) +} + +// A cutoff so far in the past that EVERY published version is "too new" to be +// mature — the same technique as the install-side minimumReleaseAge suite +// (allImmatureMinimumReleaseAge). Date-independent, so it does not depend on +// any registry-mock package's historical publish timestamps. +const allImmatureMinimumReleaseAge = Date.now() / (60 * 1000) + +test('pnpm outdated baseline (no minimumReleaseAge): newer versions are offered', async () => { + loadHasOutdatedDeps() + + const { output, exitCode } = await outdated.handler({ + ...OUTDATED_OPTIONS, + dir: process.cwd(), + }) + + // Sanity: without an age policy, outdated offers the newest registry versions. + expect(exitCode).toBe(1) + expect(stripAnsi(output)).toContain('is-negative') + expect(stripAnsi(output)).toContain('2.1.0') +}) + +test('pnpm outdated honors minimumReleaseAge: immature newer versions are not offered', async () => { + loadHasOutdatedDeps() + + const { output, exitCode } = await outdated.handler({ + ...OUTDATED_OPTIONS, + dir: process.cwd(), + minimumReleaseAge: allImmatureMinimumReleaseAge, + }) + + // With every version immature, no upgrade target is mature, so outdated has + // nothing to report — exitCode 0 and 2.1.0 must not appear as available. + expect(exitCode).toBe(0) + expect(stripAnsi(output)).not.toContain('2.1.0') +}) diff --git a/deps/inspection/commands/test/repo.ts b/deps/inspection/commands/test/repo.ts new file mode 100644 index 0000000000..ecd8da78be --- /dev/null +++ b/deps/inspection/commands/test/repo.ts @@ -0,0 +1,195 @@ +import fs from 'node:fs' +import path from 'node:path' + +import { expect, jest, test } from '@jest/globals' +import type { Config, ConfigContext } from '@pnpm/config.reader' +import { tempDir } from '@pnpm/prepare' +import { REGISTRY_MOCK_PORT } from '@pnpm/registry-mock' + +const mockOpen = jest.fn() +jest.unstable_mockModule('open', () => ({ + default: mockOpen, +})) + +const { repo } = await import('@pnpm/deps.inspection.commands') + +const REGISTRY_URL = `http://localhost:${REGISTRY_MOCK_PORT}` + +const BASE_OPTIONS = { + registries: { default: REGISTRY_URL }, +} as unknown as Config & ConfigContext & { dir: string } + +test('repo: command should be available', () => { + expect(repo.handler).toBeDefined() + expect(repo.help).toBeDefined() + expect(repo.commandNames).toEqual(['repo']) +}) + +test('repo: opens repository URL from local manifest', async () => { + mockOpen.mockClear() + const dir = tempDir() + fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ + name: 'test-pkg', + repository: 'https://github.com/test/pkg', + })) + await repo.handler({ ...BASE_OPTIONS, dir }, []) + expect(mockOpen).toHaveBeenCalledWith('https://github.com/test/pkg') +}) + +test('repo: opens repository object URL from local manifest', async () => { + mockOpen.mockClear() + const dir = tempDir() + fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ + name: 'test-pkg', + repository: { url: 'https://github.com/test/pkg' }, + })) + await repo.handler({ ...BASE_OPTIONS, dir }, []) + expect(mockOpen).toHaveBeenCalledWith('https://github.com/test/pkg') +}) + +test('repo: normalizes git+https repository URL with .git suffix', async () => { + mockOpen.mockClear() + const dir = tempDir() + fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ + name: 'test-pkg', + repository: { url: 'git+https://github.com/test/pkg.git' }, + })) + await repo.handler({ ...BASE_OPTIONS, dir }, []) + expect(mockOpen).toHaveBeenCalledWith('https://github.com/test/pkg') +}) + +test('repo: trims trailing slash from repository URL', async () => { + mockOpen.mockClear() + const dir = tempDir() + fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ + name: 'test-pkg', + repository: 'https://github.com/test/pkg/', + })) + await repo.handler({ ...BASE_OPTIONS, dir }, []) + expect(mockOpen).toHaveBeenCalledWith('https://github.com/test/pkg') +}) + +test('repo: resolves repository shorthand (owner/repo)', async () => { + mockOpen.mockClear() + const dir = tempDir() + fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ + name: 'test-pkg', + repository: 'test/pkg', + })) + await repo.handler({ ...BASE_OPTIONS, dir }, []) + expect(mockOpen).toHaveBeenCalledWith('https://github.com/test/pkg') +}) + +test('repo: resolves github: shorthand', async () => { + mockOpen.mockClear() + const dir = tempDir() + fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ + name: 'test-pkg', + repository: { url: 'github:test/pkg' }, + })) + await repo.handler({ ...BASE_OPTIONS, dir }, []) + expect(mockOpen).toHaveBeenCalledWith('https://github.com/test/pkg') +}) + +test('repo: resolves git+ssh:// repository URL', async () => { + mockOpen.mockClear() + const dir = tempDir() + fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ + name: 'test-pkg', + repository: { url: 'git+ssh://git@github.com/test/pkg.git' }, + })) + await repo.handler({ ...BASE_OPTIONS, dir }, []) + expect(mockOpen).toHaveBeenCalledWith('https://github.com/test/pkg') +}) + +test('repo: resolves gitlab: shorthand', async () => { + mockOpen.mockClear() + const dir = tempDir() + fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ + name: 'test-pkg', + repository: 'gitlab:test/pkg', + })) + await repo.handler({ ...BASE_OPTIONS, dir }, []) + expect(mockOpen).toHaveBeenCalledWith('https://gitlab.com/test/pkg') +}) + +test('repo: handles repository URL ending with .git/ (trailing slash after .git)', async () => { + mockOpen.mockClear() + const dir = tempDir() + fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ + name: 'test-pkg', + repository: { url: 'git+https://github.com/test/pkg.git/' }, + })) + await repo.handler({ ...BASE_OPTIONS, dir }, []) + expect(mockOpen).toHaveBeenCalledWith('https://github.com/test/pkg') +}) + +test('repo: uses fragment as branch in repository URL', async () => { + mockOpen.mockClear() + const dir = tempDir() + fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ + name: 'test-pkg', + repository: { url: 'git+https://github.com/test/pkg.git#main' }, + })) + await repo.handler({ ...BASE_OPTIONS, dir }, []) + expect(mockOpen).toHaveBeenCalledWith('https://github.com/test/pkg/tree/main') +}) + +test('repo: appends directory for monorepo packages', async () => { + mockOpen.mockClear() + const dir = tempDir() + fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ + name: 'test-pkg', + repository: { url: 'https://github.com/test/pkg', directory: 'packages/foo' }, + })) + await repo.handler({ ...BASE_OPTIONS, dir }, []) + expect(mockOpen).toHaveBeenCalledWith('https://github.com/test/pkg/tree/master/packages/foo') +}) + +test('repo: resolves shorthand with directory for monorepo packages', async () => { + mockOpen.mockClear() + const dir = tempDir() + fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ + name: 'test-pkg', + repository: { url: 'test/pkg', directory: 'packages/bar' }, + })) + await repo.handler({ ...BASE_OPTIONS, dir }, []) + expect(mockOpen).toHaveBeenCalledWith('https://github.com/test/pkg/tree/master/packages/bar') +}) + +test('repo: falls back to URL parsing for self-hosted git servers', async () => { + mockOpen.mockClear() + const dir = tempDir() + fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ + name: 'test-pkg', + repository: { url: 'git+https://git.example.com/test/pkg.git' }, + })) + await repo.handler({ ...BASE_OPTIONS, dir }, []) + expect(mockOpen).toHaveBeenCalledWith('https://git.example.com/test/pkg') +}) + +test('repo: throws when no repository URL is defined', async () => { + const dir = tempDir() + fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ + name: 'test-pkg', + })) + await expect( + repo.handler({ ...BASE_OPTIONS, dir }, []) + ).rejects.toMatchObject({ code: 'ERR_PNPM_NO_REPO_URL' }) +}) + +test('repo: throws when no package.json exists', async () => { + const dir = tempDir() + await expect( + repo.handler({ ...BASE_OPTIONS, dir }, []) + ).rejects.toMatchObject({ code: 'ERR_PNPM_NO_IMPORTER_MANIFEST_FOUND' }) +}) + +test('repo: looks up package on registry by name', async () => { + mockOpen.mockClear() + await repo.handler(BASE_OPTIONS, ['is-negative']) + expect(mockOpen).toHaveBeenCalledTimes(1) + const calledUrl = mockOpen.mock.calls[0][0] + expect(typeof calledUrl).toBe('string') + expect((calledUrl as string).startsWith('http')).toBe(true) +}) diff --git a/deps/inspection/commands/tsconfig.json b/deps/inspection/commands/tsconfig.json index 3f6ded79cd..631cb00d36 100644 --- a/deps/inspection/commands/tsconfig.json +++ b/deps/inspection/commands/tsconfig.json @@ -78,6 +78,9 @@ { "path": "../../../testing/command-defaults" }, + { + "path": "../../../testing/registry-mock" + }, { "path": "../../../workspace/projects-filter" }, diff --git a/deps/inspection/list/CHANGELOG.md b/deps/inspection/list/CHANGELOG.md index fae0feb043..bcc3fa107b 100644 --- a/deps/inspection/list/CHANGELOG.md +++ b/deps/inspection/list/CHANGELOG.md @@ -1,5 +1,47 @@ # @pnpm/list +## 1100.0.14 + +### Patch Changes + +- a456dc7: Limit concurrent project manifest reads while listing large workspaces to avoid `EMFILE` errors. +- Updated dependencies [a456dc7] +- Updated dependencies [35d2355] + - @pnpm/workspace.project-manifest-reader@1100.0.9 + - @pnpm/types@1101.2.0 + - @pnpm/deps.inspection.tree-builder@1100.0.12 + - @pnpm/lockfile.fs@1100.1.2 + - @pnpm/pkg-manifest.reader@1100.0.5 + +## 1100.0.13 + +### Patch Changes + +- Updated dependencies [d7da112] + - @pnpm/workspace.project-manifest-reader@1100.0.8 + +## 1100.0.12 + +### Patch Changes + +- Updated dependencies [9cb48bb] +- Updated dependencies [64afc92] + - @pnpm/lockfile.fs@1100.1.1 + - @pnpm/types@1101.1.1 + - @pnpm/deps.inspection.tree-builder@1100.0.11 + - @pnpm/workspace.project-manifest-reader@1100.0.7 + - @pnpm/pkg-manifest.reader@1100.0.4 + +## 1100.0.11 + +### Patch Changes + +- Updated dependencies [6e93f35] +- Updated dependencies [2a9bd89] + - @pnpm/lockfile.fs@1100.1.0 + - @pnpm/deps.inspection.tree-builder@1100.0.10 + - @pnpm/workspace.project-manifest-reader@1100.0.6 + ## 1100.0.10 ### Patch Changes diff --git a/deps/inspection/list/package.json b/deps/inspection/list/package.json index e13ae1351c..0ddb98401a 100644 --- a/deps/inspection/list/package.json +++ b/deps/inspection/list/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/deps.inspection.list", - "version": "1100.0.10", + "version": "1100.0.14", "description": "List installed packages in a symlinked `node_modules`", "keywords": [ "pnpm", diff --git a/deps/inspection/outdated/CHANGELOG.md b/deps/inspection/outdated/CHANGELOG.md index 669f203a89..fd7072f64f 100644 --- a/deps/inspection/outdated/CHANGELOG.md +++ b/deps/inspection/outdated/CHANGELOG.md @@ -1,5 +1,97 @@ # @pnpm/outdated +## 1100.1.3 + +### Patch Changes + +- Updated dependencies [e55f4b5] +- Updated dependencies [35d2355] +- Updated dependencies [0721d64] + - @pnpm/lockfile.utils@1100.0.10 + - @pnpm/types@1101.2.0 + - @pnpm/resolving.npm-resolver@1101.3.3 + - @pnpm/installing.client@1100.2.3 + - @pnpm/lockfile.fs@1100.1.2 + - @pnpm/config.version-policy@1100.1.2 + - @pnpm/deps.path@1100.0.5 + - @pnpm/hooks.read-package-hook@1100.0.5 + - @pnpm/pkg-manifest.utils@1100.2.1 + +## 1100.1.2 + +### Patch Changes + +- Updated dependencies [212315d] + - @pnpm/resolving.npm-resolver@1101.3.2 + - @pnpm/installing.client@1100.2.2 + +## 1100.1.1 + +### Patch Changes + +- @pnpm/resolving.npm-resolver@1101.3.1 +- @pnpm/installing.client@1100.2.1 + +## 1100.1.0 + +### Minor Changes + +- 1627943: `pnpm outdated` and `pnpm update --interactive` now report Node.js, Deno, and Bun runtimes installed as project dependencies (`runtime:` specifiers). Previously these were silently skipped because the npm specifier parser did not understand the `runtime:` protocol, so runtime versions never appeared in the outdated table or the interactive update picker. + + Internally, the outdated check is now resolver-driven: `@pnpm/resolving.resolver-base` defines a `ResolveLatestFunction` shape (with `LatestQuery` input — `{ wantedDependency, compatible? }` — and `LatestInfo` result — `{ latestManifest? }`), and every protocol resolver (npm, jsr, named-registry, git, tarball, local, node/bun/deno runtimes) exports its own `resolveLatest*` function alongside its `resolve*`. `@pnpm/resolving.default-resolver` composes them into a single dispatcher, exposed through `@pnpm/installing.client` as `createResolver(...).resolveLatest`. + + Each resolver decides whether it owns the dep and what "latest" means for its protocol; the outdated command derives `current` / `wanted` display values from the lockfile snapshot (`pkgSnapshot.version` for semver protocols, raw ref for URL-shaped ones) and uses raw ref equality for the "lockfile changed" check, so protocol knowledge stays inside each resolver instead of the command. + +### Patch Changes + +- Updated dependencies [9cb48bb] +- Updated dependencies [3a54205] +- Updated dependencies [1627943] +- Updated dependencies [64afc92] + - @pnpm/lockfile.fs@1100.1.1 + - @pnpm/resolving.npm-resolver@1101.3.0 + - @pnpm/installing.client@1100.2.0 + - @pnpm/pkg-manifest.utils@1100.2.0 + - @pnpm/types@1101.1.1 + - @pnpm/lockfile.utils@1100.0.9 + - @pnpm/config.version-policy@1100.1.1 + - @pnpm/deps.path@1100.0.4 + - @pnpm/hooks.read-package-hook@1100.0.4 + +## 1100.0.16 + +### Patch Changes + +- 4195766: Tightened the `minimumReleaseAge` story so the bypass becomes explicit on disk instead of silent, and removed the discover-by-loop dance for strict-mode users: + + 1. Fresh resolutions in loose mode (`minimumReleaseAgeStrict: false`) that fall back to a version newer than the cutoff auto-collect the picked `name@version` into the workspace manifest's `minimumReleaseAgeExclude`. A single info message lists the additions; entries already on the list are left alone. + 2. The post-resolution lockfile verifier introduced in #11583 now runs in loose mode too — every accepted-immature pin must be on `minimumReleaseAgeExclude`, just like strict mode requires. A lockfile produced under a weaker (or absent) policy that still has immature entries is rejected the same way strict mode would reject it. + 3. **Strict mode (interactive)** no longer aborts on the first immature pick. The resolver gathers every immature direct _and_ transitive in one pass; before peer-dependency resolution runs, pnpm prompts the user with the full list and asks whether to add them all to `minimumReleaseAgeExclude` and proceed. Approve → install continues and the workspace manifest is written at the end. Decline → resolution aborts before the lockfile or package.json is touched (tarballs already in the store stay, since the store is idempotent). This closes the [#10488](https://github.com/pnpm/pnpm/issues/10488) loop where security bumps to packages with platform-specific transitives (e.g. `next` + the `@next/swc-*` shims) made users re-run `pnpm add` once per transitive. + 4. **Strict mode (non-interactive / CI)** now aborts with the full immature set in the error message instead of the first pick. The resolver always collects every immature direct + transitive; the install command then throws `ERR_PNPM_NO_MATURE_MATCHING_VERSION` listing each entry's `name@version` and publish time. Deterministic CI behavior is preserved (same exit code, same error code), but the error pinpoints every offending entry instead of forcing the discover-by-loop dance. The expected workflow is interactive approval locally → the lockfile + workspace manifest get committed → CI runs cleanly against the populated exclude list. + + 5. **The lockfile verifier now also covers `trustPolicy: 'no-downgrade'`.** The same post-resolution gate that re-checks `minimumReleaseAge` on lockfile entries now re-runs `failIfTrustDowngraded` for every npm-registry entry whose name isn't on `trustPolicyExclude`. The two checks share a single full-metadata fetch per package, so the extra coverage doesn't cost an extra round trip when both policies are active. Resolver-time trust checks still run as before — this just closes the gap when an entry bypasses resolution (peek path, `--frozen-lockfile`, restored CI cache). + + Pacquet parity: not ported — pacquet's `minimumReleaseAge` policy is itself only stubbed today (see `pacquet/crates/package-manager/src/version_policy.rs`). The auto-exclude, loose-mode verifier, prompt, and the new trust-policy verifier check will travel with the broader policy port whenever that happens. + +- b6e2c8c: Make `pnpm self-update` respect `minimumReleaseAge` (and `minimumReleaseAgeExclude`) when resolving which pnpm version to install. + + When the `latest` dist-tag points to a version newer than the configured age threshold, `self-update` now selects the newest mature version instead unless excluded by `minimumReleaseAgeExclude`. + + Also makes `dlx` and `outdated` surface invalid `minimumReleaseAgeExclude` patterns under the same `ERR_PNPM_INVALID_MINIMUM_RELEASE_AGE_EXCLUDE` error code already used by `install`, instead of leaking the internal `ERR_PNPM_INVALID_VERSION_UNION` / `ERR_PNPM_NAME_PATTERN_IN_VERSION_UNION` codes. + +- Updated dependencies [963861c] +- Updated dependencies [4195766] +- Updated dependencies [31538bf] +- Updated dependencies [b6e2c8c] +- Updated dependencies [6e93f35] +- Updated dependencies [2a9bd89] + - @pnpm/resolving.npm-resolver@1101.2.0 + - @pnpm/installing.client@1100.1.0 + - @pnpm/config.version-policy@1100.1.0 + - @pnpm/lockfile.fs@1100.1.0 + - @pnpm/lockfile.utils@1100.0.8 + - @pnpm/pkg-manifest.utils@1100.1.4 + ## 1100.0.15 ### Patch Changes diff --git a/deps/inspection/outdated/package.json b/deps/inspection/outdated/package.json index cd8fe788e6..41b158725f 100644 --- a/deps/inspection/outdated/package.json +++ b/deps/inspection/outdated/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/deps.inspection.outdated", - "version": "1100.0.15", + "version": "1100.1.3", "description": "Check for outdated packages", "keywords": [ "pnpm", @@ -36,7 +36,6 @@ "@pnpm/catalogs.types": "workspace:*", "@pnpm/config.matcher": "workspace:*", "@pnpm/config.parse-overrides": "workspace:*", - "@pnpm/config.pick-registry-for-package": "workspace:*", "@pnpm/config.version-policy": "workspace:*", "@pnpm/constants": "workspace:*", "@pnpm/deps.path": "workspace:*", diff --git a/deps/inspection/outdated/src/createManifestGetter.ts b/deps/inspection/outdated/src/createManifestGetter.ts index 11ce02d677..9f0327dd24 100644 --- a/deps/inspection/outdated/src/createManifestGetter.ts +++ b/deps/inspection/outdated/src/createManifestGetter.ts @@ -29,7 +29,6 @@ export function createManifestGetter ( ...opts, configByUri: opts.configByUri, filterMetadata: false, // We need all the data from metadata for "outdated --long" to work. - strictPublishedByCheck: Boolean(opts.minimumReleaseAge) && opts.minimumReleaseAgeStrict === true, ignoreMissingTimeField: opts.minimumReleaseAgeIgnoreMissingTime, }) @@ -58,18 +57,22 @@ export async function getManifest ( publishedBy: opts.publishedBy, publishedByExclude: opts.publishedByExclude, }) + // No mature version found within range: the resolver fell back to the + // lowest immature pick and flagged it inline. `outdated` shouldn't + // present an immature version as "available", so treat it as no match + // — matching the pre-violation-collection behavior when the resolver + // threw `NO_MATURE_MATCHING_VERSION`. + if (resolution?.policyViolation?.code === 'MINIMUM_RELEASE_AGE_VIOLATION') { + return null + } return resolution?.manifest ?? null } catch (err) { const code = (err as { code?: string }).code - if (opts.publishedBy && ( - code === 'ERR_PNPM_NO_MATURE_MATCHING_VERSION' || - code === 'ERR_PNPM_NO_MATCHING_VERSION' - )) { - // No versions found that meet the minimumReleaseAge requirement. - // This can happen when all published versions (including the one the - // "latest" dist-tag points to) are newer than the minimumReleaseAge - // threshold, causing the resolver to throw NO_MATCHING_VERSION instead - // of NO_MATURE_MATCHING_VERSION. + if (opts.publishedBy && code === 'ERR_PNPM_NO_MATCHING_VERSION') { + // No version satisfies the range at all (not a maturity issue). + // Pre-violation-collection this branch also covered the maturity + // case via `NO_MATURE_MATCHING_VERSION`; with always-defer, that + // case is handled above as a `policyViolation`. return null } throw err diff --git a/deps/inspection/outdated/src/outdated.ts b/deps/inspection/outdated/src/outdated.ts index 22eac49f7d..41c7bc694d 100644 --- a/deps/inspection/outdated/src/outdated.ts +++ b/deps/inspection/outdated/src/outdated.ts @@ -7,33 +7,30 @@ import { import type { Catalogs } from '@pnpm/catalogs.types' import { createMatcher } from '@pnpm/config.matcher' import { parseOverrides } from '@pnpm/config.parse-overrides' -import { pickRegistryForPackage } from '@pnpm/config.pick-registry-for-package' -import { LOCKFILE_VERSION, WANTED_LOCKFILE } from '@pnpm/constants' +import { LOCKFILE_VERSION } from '@pnpm/constants' import * as dp from '@pnpm/deps.path' import { PnpmError } from '@pnpm/error' import { createReadPackageHook } from '@pnpm/hooks.read-package-hook' +import type { ResolveLatestDispatcher } from '@pnpm/installing.client' import { getLockfileImporterId, type LockfileObject, type ProjectSnapshot, } from '@pnpm/lockfile.fs' -import { nameVerFromPkgSnapshot } from '@pnpm/lockfile.utils' import { getAllDependenciesFromManifest } from '@pnpm/pkg-manifest.utils' -import { parseBareSpecifier } from '@pnpm/resolving.npm-resolver' import { DEPENDENCIES_FIELDS, type DependenciesField, + type DepPath, type IncludedDependencies, type PackageManifest, + type PackageVersionPolicy, type ProjectManifest, - type Registries, } from '@pnpm/types' import semver from 'semver' export * from './createManifestGetter.js' -export type GetLatestManifestFunction = (packageName: string, rangeOrTag: string) => Promise - export interface OutdatedPackage { alias: string belongsTo: DependenciesField @@ -49,7 +46,7 @@ export async function outdated ( catalogs?: Catalogs compatible?: boolean currentLockfile: LockfileObject | null - getLatestManifest: GetLatestManifestFunction + resolveLatest: ResolveLatestDispatcher ignoreDependencies?: string[] include?: IncludedDependencies lockfileDir: string @@ -58,7 +55,8 @@ export async function outdated ( minimumReleaseAge?: number minimumReleaseAgeExclude?: string[] prefix: string - registries: Registries + publishedBy?: Date + publishedByExclude?: PackageVersionPolicy wantedLockfile: LockfileObject | null } ): Promise { @@ -89,6 +87,14 @@ export async function outdated ( const ignoreDependenciesMatcher = opts.ignoreDependencies?.length ? createMatcher(opts.ignoreDependencies) : undefined + const resolveOpts = { + lockfileDir: opts.lockfileDir, + preferredVersions: {}, + projectDir: opts.prefix, + publishedBy: opts.publishedBy, + publishedByExclude: opts.publishedByExclude, + } + await Promise.all( DEPENDENCIES_FIELDS.map(async (depType) => { if ( @@ -107,41 +113,37 @@ export async function outdated ( await Promise.all( pkgs.map(async (alias) => { if (!allDeps[alias]) return - const ref = opts.wantedLockfile!.importers[importerId][depType]![alias] - - if ( - ref.startsWith('file:') || // ignoring linked packages. (For backward compatibility) - ignoreDependenciesMatcher?.(alias) - ) { - return - } - - const relativeDepPath = dp.refToRelative(ref, alias) - - // ignoring linked packages - if (relativeDepPath === null) return - - const pkgSnapshot = opts.wantedLockfile!.packages?.[relativeDepPath] - - if (pkgSnapshot == null) { - throw new Error(`Invalid ${WANTED_LOCKFILE} file. ${relativeDepPath} not found in packages field`) - } + const wantedRef = opts.wantedLockfile!.importers[importerId][depType]![alias] + if (ignoreDependenciesMatcher?.(alias)) return const currentRef = (currentLockfile.importers[importerId] as ProjectSnapshot)?.[depType]?.[alias] - const currentRelative = currentRef && dp.refToRelative(currentRef, alias) - const current = (currentRelative && dp.parse(currentRelative).version) ?? currentRef - const wanted = dp.parse(relativeDepPath).version ?? ref - const { name: packageName } = nameVerFromPkgSnapshot(relativeDepPath, pkgSnapshot) - const name = dp.parse(relativeDepPath).name ?? packageName + const wantedRelative = dp.refToRelative(wantedRef, alias) + const currentRelative = currentRef ? dp.refToRelative(currentRef, alias) : null + const wantedSnapshot = wantedRelative != null ? opts.wantedLockfile!.packages?.[wantedRelative] : undefined + const currentSnapshot = currentRelative != null ? currentLockfile.packages?.[currentRelative] : undefined + // Aliased npm deps lock under their real name (e.g. `positive: is-positive@3.1.0`); + // pull the name off the depPath so the report shows the real package. + const packageName = (wantedRelative != null ? dp.parse(wantedRelative).name : undefined) ?? alias const bareSpecifier = _replaceCatalogProtocolIfNecessary({ alias, bareSpecifier: allDeps[alias] }) - // If the npm resolve parser cannot parse the spec of the dependency, - // it means that the package is not from a npm-compatible registry. - // In that case, we can't check whether the package is up-to-date - if ( - parseBareSpecifier(bareSpecifier, alias, 'latest', pickRegistryForPackage(opts.registries, name)) == null - ) { - if (current !== wanted) { + + const info = await opts.resolveLatest( + { wantedDependency: { alias, bareSpecifier }, compatible: opts.compatible }, + resolveOpts + ) + if (info == null) return // resolver doesn't claim this dep — skip silently + + const wanted = displayVersion(wantedRef, wantedRelative, wantedSnapshot?.version) + const current = currentRef ? displayVersion(currentRef, currentRelative, currentSnapshot?.version) : undefined + const { latestManifest } = info + + // Compare the parsed `wanted` / `current` rather than raw refs. + // For npm-style deps that means peer-graph-only changes (same + // semver, different `(peer-hash)`) don't surface as fake + // "outdated" entries; for URL/git refs the display values *are* + // the refs, so a commit/path change still fires correctly. + if (latestManifest == null) { + if (wanted !== current) { outdated.push({ alias, belongsTo: depType, @@ -154,14 +156,6 @@ export async function outdated ( } return } - - const latestManifest = await opts.getLatestManifest( - name, - opts.compatible ? (bareSpecifier ?? 'latest') : 'latest' - ) - - if (latestManifest == null) return - if (!current) { outdated.push({ alias, @@ -170,12 +164,10 @@ export async function outdated ( packageName, wanted, workspace: opts.manifest.name, - }) return } - - if (current !== wanted || semver.lt(current, latestManifest.version) || latestManifest.deprecated) { + if (wanted !== current || isLowerVersion(wanted, latestManifest.version) || latestManifest.deprecated) { outdated.push({ alias, belongsTo: depType, @@ -184,7 +176,6 @@ export async function outdated ( packageName, wanted, workspace: opts.manifest.name, - }) } }) @@ -205,6 +196,33 @@ function isEmpty (obj: object): boolean { return Object.keys(obj).length === 0 } +// Pick a clean display string for a lockfile ref. +// +// - If the dep-path parses to a semver, that's the value (handles +// `pkg@1.0.0(peer-hash)` and aliased `positive: is-positive@3.1.0`). +// - If the dep-path's non-semver version contains a `/`, it's a +// URL/git-shape (`https://`, `git+ssh://`, scheme-less `github.com/…/sha`, +// `link:../foo`, etc.) — return the raw ref so a commit/path change is +// visible to the user. +// - Otherwise prefer `snapshot.version` (clean semver for `runtime:`-style +// refs); fall back to the raw ref when the snapshot didn't record one. +function displayVersion (ref: string, relativeDepPath: DepPath | null, snapshotVersion: string | undefined): string { + if (relativeDepPath != null) { + const parsed = dp.parse(relativeDepPath) + if (parsed.version != null) return parsed.version + if (parsed.nonSemverVersion?.includes('/')) return ref + } + return snapshotVersion ?? ref +} + +// semver.lt throws on non-semver strings (e.g. URL refs from git/tarball). +// Treat those as "not lower" so a ref change still gets surfaced via the +// `wantedRef !== currentRef` check above. +function isLowerVersion (current: string, latest: string): boolean { + if (!semver.valid(current) || !semver.valid(latest)) return false + return semver.lt(current, latest) +} + function replaceCatalogProtocolIfNecessary (catalogs: Catalogs, wantedDependency: WantedDependency) { return matchCatalogResolveResult(resolveFromCatalog(catalogs, wantedDependency), { unused: () => wantedDependency.bareSpecifier, diff --git a/deps/inspection/outdated/src/outdatedDepsOfProjects.ts b/deps/inspection/outdated/src/outdatedDepsOfProjects.ts index 383389d49c..f7cfa1b16a 100644 --- a/deps/inspection/outdated/src/outdatedDepsOfProjects.ts +++ b/deps/inspection/outdated/src/outdatedDepsOfProjects.ts @@ -2,6 +2,8 @@ import path from 'node:path' import type { Catalogs } from '@pnpm/catalogs.types' import { createMatcher } from '@pnpm/config.matcher' +import { getPublishedByPolicy } from '@pnpm/config.version-policy' +import { type ClientOptions, createResolver } from '@pnpm/installing.client' import { readCurrentLockfile, readWantedLockfile, @@ -10,23 +12,33 @@ import type { IncludedDependencies, ProjectManifest, ProjectRootDir, + RegistryConfig, } from '@pnpm/types' import { unnest } from 'ramda' -import { createManifestGetter, type ManifestGetterOptions } from './createManifestGetter.js' import { outdated, type OutdatedPackage } from './outdated.js' +export type OutdatedDepsOfProjectsOptions = Omit +& { + dir: string + lockfileDir?: string + configByUri: Record + fullMetadata?: boolean + minimumReleaseAge?: number + minimumReleaseAgeExclude?: string[] + minimumReleaseAgeIgnoreMissingTime?: boolean + minimumReleaseAgeStrict?: boolean +} + export async function outdatedDepsOfProjects ( pkgs: Array<{ rootDir: ProjectRootDir, manifest: ProjectManifest }>, args: string[], - opts: Omit & { + opts: OutdatedDepsOfProjectsOptions & { catalogs?: Catalogs compatible?: boolean ignoreDependencies?: string[] include: IncludedDependencies - minimumReleaseAge?: number - minimumReleaseAgeExclude?: string[] - } & Partial> + } ): Promise { if (!opts.lockfileDir) { return unnest(await Promise.all( @@ -39,20 +51,23 @@ export async function outdatedDepsOfProjects ( const internalPnpmDir = path.join(path.join(lockfileDir, 'node_modules/.pnpm')) const currentLockfile = await readCurrentLockfile(internalPnpmDir, { ignoreIncompatible: false }) const wantedLockfile = await readWantedLockfile(lockfileDir, { ignoreIncompatible: false }) ?? currentLockfile - const getLatestManifest = createManifestGetter({ + const { publishedBy, publishedByExclude } = getPublishedByPolicy(opts) + + const { resolveLatest } = createResolver({ ...opts, + configByUri: opts.configByUri, + filterMetadata: false, fullMetadata: opts.fullMetadata === true || Boolean(opts.minimumReleaseAge), - lockfileDir, - minimumReleaseAge: opts.minimumReleaseAge, - minimumReleaseAgeExclude: opts.minimumReleaseAgeExclude, + ignoreMissingTimeField: opts.minimumReleaseAgeIgnoreMissingTime, }) + return Promise.all(pkgs.map(async ({ rootDir, manifest }): Promise => { const match = (args.length > 0) && createMatcher(args) || undefined return outdated({ catalogs: opts.catalogs, compatible: opts.compatible, currentLockfile, - getLatestManifest, + resolveLatest, ignoreDependencies: opts.ignoreDependencies, include: opts.include, lockfileDir, @@ -61,7 +76,8 @@ export async function outdatedDepsOfProjects ( minimumReleaseAge: opts.minimumReleaseAge, minimumReleaseAgeExclude: opts.minimumReleaseAgeExclude, prefix: rootDir, - registries: opts.registries, + publishedBy, + publishedByExclude, wantedLockfile, }) })) diff --git a/deps/inspection/outdated/test/getManifest.spec.ts b/deps/inspection/outdated/test/getManifest.spec.ts index 6f58c54b87..89a694cf8b 100644 --- a/deps/inspection/outdated/test/getManifest.spec.ts +++ b/deps/inspection/outdated/test/getManifest.spec.ts @@ -58,14 +58,30 @@ test('getManifest() with minimumReleaseAge filters latest when too new', async ( const publishedBy = new Date(Date.now() - 10080 * 60 * 1000) + // The resolver no longer throws on immature picks — it falls back to + // the lowest matching version and flags the result with `policyViolation`. + // outdated treats that as "no version available within the policy" and + // returns null, same as the pre-refactor throw path. const resolve = jest.fn(async (wantedPackage, resolveOpts) => { expect(wantedPackage.bareSpecifier).toBe('latest') expect(resolveOpts.publishedBy).toBeInstanceOf(Date) - - // Simulate latest version being too new - const error = new Error('No matching version found') as Error & { code?: string } - error.code = 'ERR_PNPM_NO_MATURE_MATCHING_VERSION' - throw error + return { + id: 'foo/2.0.0' as PkgResolutionId, + latest: '2.0.0', + manifest: { + name: 'foo', + version: '2.0.0', + }, + resolution: {} as TarballResolution, + resolvedVia: 'npm-registry', + policyViolation: { + name: 'foo', + version: '2.0.0', + resolution: {} as TarballResolution, + code: 'MINIMUM_RELEASE_AGE_VIOLATION', + reason: 'was published within the minimumReleaseAge cutoff', + }, + } }) const result = await getManifest({ ...opts, resolve, publishedBy }, 'foo', 'latest') diff --git a/deps/inspection/outdated/test/outdated.spec.ts b/deps/inspection/outdated/test/outdated.spec.ts index 1553df451b..eefdc2ffd8 100644 --- a/deps/inspection/outdated/test/outdated.spec.ts +++ b/deps/inspection/outdated/test/outdated.spec.ts @@ -1,10 +1,44 @@ import { expect, test } from '@jest/globals' import { LOCKFILE_VERSION } from '@pnpm/constants' -import type { DepPath, ProjectId } from '@pnpm/types' +import type { ResolveLatestDispatcher } from '@pnpm/installing.client' +import type { DepPath, PackageManifest, ProjectId } from '@pnpm/types' import { outdated } from '../lib/outdated.js' -async function getLatestManifest (packageName: string) { +type ManifestGetter = (packageName: string) => Promise + +// Test stand-in for the real dispatcher: route by protocol shape, look up +// "latest" through the per-test mock. Resolvers for non-latest protocols +// (git, tarball) return `{}` to claim the dep with no latest available; +// local refs return undefined so the dispatcher falls through and skips. +function makeResolveLatest (getLatest: ManifestGetter): ResolveLatestDispatcher { + return async (query) => { + const { alias, bareSpecifier } = query.wantedDependency + if (bareSpecifier?.startsWith('file:') || bareSpecifier?.startsWith('link:') || bareSpecifier?.startsWith('workspace:')) { + return undefined + } + if (bareSpecifier?.startsWith('runtime:') && (alias === 'node' || alias === 'bun' || alias === 'deno')) { + const latestManifest = await getLatest(alias) + return { latestManifest: latestManifest ?? undefined } + } + if ( + bareSpecifier?.startsWith('http://') || bareSpecifier?.startsWith('https://') || + bareSpecifier?.startsWith('github:') || bareSpecifier?.startsWith('git+') || bareSpecifier?.startsWith('git:') + ) { + return {} + } + let pkgName = alias ?? '' + if (bareSpecifier?.startsWith('npm:')) { + const inner = bareSpecifier.slice(4) + const atIdx = inner.lastIndexOf('@') + pkgName = (atIdx > 0 ? inner.slice(0, atIdx) : inner) || pkgName + } + const latestManifest = await getLatest(pkgName) + return { latestManifest: latestManifest ?? undefined } + } +} + +async function getLatestManifest (packageName: string): Promise { return ({ 'deprecated-pkg': { deprecated: 'This package is deprecated', @@ -23,9 +57,11 @@ async function getLatestManifest (packageName: string) { name: 'pkg-with-1-dep', version: '1.0.0', }, - })[packageName] ?? null + } as Record)[packageName] ?? null } +const resolveLatest = makeResolveLatest(getLatestManifest) + test('outdated()', async () => { const outdatedPkgs = await outdated({ currentLockfile: { @@ -70,7 +106,7 @@ test('outdated()', async () => { }, }, }, - getLatestManifest, + resolveLatest, lockfileDir: 'project', manifest: { name: 'wanted-shrinkwrap', @@ -136,9 +172,6 @@ test('outdated()', async () => { }, }, }, - registries: { - default: 'https://registry.npmjs.org/', - }, }) expect(outdatedPkgs).toStrictEqual([ { @@ -209,7 +242,7 @@ test('outdated() should return deprecated package even if its current version is } const outdatedPkgs = await outdated({ currentLockfile: lockfile, - getLatestManifest, + resolveLatest, lockfileDir: 'project', manifest: { name: 'wanted-shrinkwrap', @@ -221,9 +254,6 @@ test('outdated() should return deprecated package even if its current version is }, prefix: 'project', wantedLockfile: lockfile, - registries: { - default: 'https://registry.npmjs.org/', - }, }) expect(outdatedPkgs).toStrictEqual([ { @@ -286,7 +316,7 @@ test('outdated() with minimumReleaseAge', async () => { }, }, }, - getLatestManifest: getLatestManifestForMinimumAge, + resolveLatest: makeResolveLatest(getLatestManifestForMinimumAge), lockfileDir: 'project', manifest: { name: 'with-min-age', @@ -324,9 +354,6 @@ test('outdated() with minimumReleaseAge', async () => { }, }, }, - registries: { - default: 'https://registry.npmjs.org/', - }, minimumReleaseAge: 10080, }) @@ -402,7 +429,7 @@ test('outdated() with minimumReleaseAgeExclude', async () => { }, }, }, - getLatestManifest: getLatestManifestWithExclude, + resolveLatest: makeResolveLatest(getLatestManifestWithExclude), lockfileDir: 'project', manifest: { name: 'with-exclude', @@ -440,9 +467,6 @@ test('outdated() with minimumReleaseAgeExclude', async () => { }, }, }, - registries: { - default: 'https://registry.npmjs.org/', - }, minimumReleaseAge: 10080, minimumReleaseAgeExclude: ['is-negative'], }) @@ -515,7 +539,7 @@ test('using a matcher', async () => { }, }, }, - getLatestManifest, + resolveLatest, lockfileDir: 'wanted-shrinkwrap', manifest: { name: 'wanted-shrinkwrap', @@ -575,9 +599,6 @@ test('using a matcher', async () => { }, }, }, - registries: { - default: 'https://registry.npmjs.org/', - }, }) expect(outdatedPkgs).toStrictEqual([ { @@ -617,7 +638,7 @@ test('outdated() aliased dependency', async () => { }, }, }, - getLatestManifest, + resolveLatest, lockfileDir: 'project', manifest: { name: 'wanted-shrinkwrap', @@ -648,9 +669,6 @@ test('outdated() aliased dependency', async () => { }, }, }, - registries: { - default: 'https://registry.npmjs.org/', - }, }) expect(outdatedPkgs).toStrictEqual([ { @@ -705,7 +723,7 @@ test('a dependency is not outdated if it is newer than the latest version', asyn } const outdatedPkgs = await outdated({ currentLockfile: lockfile, - getLatestManifest: async (packageName) => { + resolveLatest: makeResolveLatest( async (packageName) => { switch (packageName) { case 'foo': return { @@ -724,7 +742,7 @@ test('a dependency is not outdated if it is newer than the latest version', asyn } } return null - }, + }), lockfileDir: 'project', manifest: { name: 'pkg', @@ -738,9 +756,6 @@ test('a dependency is not outdated if it is newer than the latest version', asyn }, prefix: 'project', wantedLockfile: lockfile, - registries: { - default: 'https://registry.npmjs.org/', - }, }) expect(outdatedPkgs).toStrictEqual([]) }) @@ -748,9 +763,9 @@ test('a dependency is not outdated if it is newer than the latest version', asyn test('outdated() should [] when there is no dependency', async () => { const outdatedPkgs = await outdated({ currentLockfile: null, - getLatestManifest: async () => { + resolveLatest: makeResolveLatest( async () => { return null - }, + }), lockfileDir: 'project', manifest: { name: 'pkg', @@ -758,9 +773,6 @@ test('outdated() should [] when there is no dependency', async () => { }, prefix: 'project', wantedLockfile: null, - registries: { - default: 'https://registry.npmjs.org/', - }, }) expect(outdatedPkgs).toStrictEqual([]) }) @@ -810,7 +822,7 @@ test('should ignore dependencies as expected', async () => { }, }, }, - getLatestManifest, + resolveLatest, lockfileDir: 'project', manifest: { name: 'wanted-shrinkwrap', @@ -876,9 +888,6 @@ test('should ignore dependencies as expected', async () => { }, }, }, - registries: { - default: 'https://registry.npmjs.org/', - }, ignoreDependencies: [ 'from-*', 'is-negative', @@ -899,3 +908,182 @@ test('should ignore dependencies as expected', async () => { }, ]) }) + +test('outdated() lists outdated runtimes (node, deno, bun)', async () => { + const runtimeLatestManifest = async (packageName: string) => { + return ({ + node: { name: 'node', version: '23.0.0' }, + deno: { name: 'deno', version: '2.5.0' }, + bun: { name: 'bun', version: '1.1.42' }, + } as Record)[packageName] ?? null + } + + const lockfile = { + importers: { + ['.' as ProjectId]: { + dependencies: { + node: 'runtime:22.0.0', + deno: 'runtime:2.4.2', + }, + devDependencies: { + bun: 'runtime:1.1.40', + }, + specifiers: { + node: 'runtime:^22.0.0', + deno: 'runtime:2.4.2', + bun: 'runtime:^1.1.0', + }, + }, + }, + lockfileVersion: LOCKFILE_VERSION, + packages: { + ['node@runtime:22.0.0' as DepPath]: { + version: '22.0.0', + resolution: { type: 'variations', variants: [] } as any, // eslint-disable-line @typescript-eslint/no-explicit-any + }, + ['deno@runtime:2.4.2' as DepPath]: { + version: '2.4.2', + resolution: { type: 'variations', variants: [] } as any, // eslint-disable-line @typescript-eslint/no-explicit-any + }, + ['bun@runtime:1.1.40' as DepPath]: { + version: '1.1.40', + resolution: { type: 'variations', variants: [] } as any, // eslint-disable-line @typescript-eslint/no-explicit-any + }, + }, + } + + const outdatedPkgs = await outdated({ + currentLockfile: lockfile, + resolveLatest: makeResolveLatest(runtimeLatestManifest), + lockfileDir: 'project', + manifest: { + name: 'has-runtimes', + version: '1.0.0', + dependencies: { + node: 'runtime:^22.0.0', + deno: 'runtime:2.4.2', + }, + devDependencies: { + bun: 'runtime:^1.1.0', + }, + }, + prefix: 'project', + wantedLockfile: lockfile, + }) + + expect(outdatedPkgs).toStrictEqual([ + { + alias: 'bun', + belongsTo: 'devDependencies', + current: '1.1.40', + latestManifest: { name: 'bun', version: '1.1.42' }, + packageName: 'bun', + wanted: '1.1.40', + workspace: 'has-runtimes', + }, + { + alias: 'deno', + belongsTo: 'dependencies', + current: '2.4.2', + latestManifest: { name: 'deno', version: '2.5.0' }, + packageName: 'deno', + wanted: '2.4.2', + workspace: 'has-runtimes', + }, + { + alias: 'node', + belongsTo: 'dependencies', + current: '22.0.0', + latestManifest: { name: 'node', version: '23.0.0' }, + packageName: 'node', + wanted: '22.0.0', + workspace: 'has-runtimes', + }, + ]) +}) + +test('outdated() runtime in --compatible mode resolves within the declared range', async () => { + const getLatestForCompat = async (packageName: string): Promise => { + expect(packageName).toBe('node') + return { name: 'node', version: '22.5.0' } + } + + const lockfile = { + importers: { + ['.' as ProjectId]: { + dependencies: { node: 'runtime:22.0.0' }, + specifiers: { node: 'runtime:^22.0.0' }, + }, + }, + lockfileVersion: LOCKFILE_VERSION, + packages: { + ['node@runtime:22.0.0' as DepPath]: { + version: '22.0.0', + resolution: { type: 'variations', variants: [] } as any, // eslint-disable-line @typescript-eslint/no-explicit-any + }, + }, + } + + const outdatedPkgs = await outdated({ + compatible: true, + currentLockfile: lockfile, + resolveLatest: makeResolveLatest(getLatestForCompat), + lockfileDir: 'project', + manifest: { + name: 'with-runtime-range', + version: '1.0.0', + dependencies: { node: 'runtime:^22.0.0' }, + }, + prefix: 'project', + wantedLockfile: lockfile, + }) + + expect(outdatedPkgs).toStrictEqual([ + { + alias: 'node', + belongsTo: 'dependencies', + current: '22.0.0', + latestManifest: { name: 'node', version: '22.5.0' }, + packageName: 'node', + wanted: '22.0.0', + workspace: 'with-runtime-range', + }, + ]) +}) + +test('outdated() does not list runtime that is already up to date', async () => { + const getLatestManifestUpToDate = async (packageName: string) => { + return packageName === 'node' ? { name: 'node', version: '22.0.0' } : null + } + + const lockfile = { + importers: { + ['.' as ProjectId]: { + dependencies: { node: 'runtime:22.0.0' }, + specifiers: { node: 'runtime:22.0.0' }, + }, + }, + lockfileVersion: LOCKFILE_VERSION, + packages: { + ['node@runtime:22.0.0' as DepPath]: { + version: '22.0.0', + resolution: { type: 'variations', variants: [] } as any, // eslint-disable-line @typescript-eslint/no-explicit-any + }, + }, + } + + const outdatedPkgs = await outdated({ + currentLockfile: lockfile, + resolveLatest: makeResolveLatest(getLatestManifestUpToDate), + lockfileDir: 'project', + manifest: { + name: 'up-to-date', + version: '1.0.0', + dependencies: { node: 'runtime:22.0.0' }, + }, + prefix: 'project', + wantedLockfile: lockfile, + }) + + expect(outdatedPkgs).toStrictEqual([]) +}) diff --git a/deps/inspection/outdated/tsconfig.json b/deps/inspection/outdated/tsconfig.json index 62327557cc..5e5a1cb455 100644 --- a/deps/inspection/outdated/tsconfig.json +++ b/deps/inspection/outdated/tsconfig.json @@ -21,9 +21,6 @@ { "path": "../../../config/parse-overrides" }, - { - "path": "../../../config/pick-registry-for-package" - }, { "path": "../../../config/version-policy" }, diff --git a/deps/inspection/peers-checker/CHANGELOG.md b/deps/inspection/peers-checker/CHANGELOG.md index eb3d470fd2..787e4e475c 100644 --- a/deps/inspection/peers-checker/CHANGELOG.md +++ b/deps/inspection/peers-checker/CHANGELOG.md @@ -1,5 +1,35 @@ # @pnpm/deps.inspection.peers-checker +## 1100.0.11 + +### Patch Changes + +- Updated dependencies [35d2355] + - @pnpm/types@1101.2.0 + - @pnpm/lockfile.fs@1100.1.2 + - @pnpm/deps.path@1100.0.5 + - @pnpm/lockfile.walker@1100.0.8 + +## 1100.0.10 + +### Patch Changes + +- Updated dependencies [9cb48bb] +- Updated dependencies [64afc92] + - @pnpm/lockfile.fs@1100.1.1 + - @pnpm/types@1101.1.1 + - @pnpm/deps.path@1100.0.4 + - @pnpm/lockfile.walker@1100.0.7 + +## 1100.0.9 + +### Patch Changes + +- Updated dependencies [6e93f35] +- Updated dependencies [2a9bd89] + - @pnpm/lockfile.fs@1100.1.0 + - @pnpm/lockfile.walker@1100.0.6 + ## 1100.0.8 ### Patch Changes diff --git a/deps/inspection/peers-checker/package.json b/deps/inspection/peers-checker/package.json index be368c7b36..a47bd63c9e 100644 --- a/deps/inspection/peers-checker/package.json +++ b/deps/inspection/peers-checker/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/deps.inspection.peers-checker", - "version": "1100.0.8", + "version": "1100.0.11", "description": "Check for unmet and missing peer dependency issues from the lockfile", "keywords": [ "pnpm", diff --git a/deps/inspection/peers-issues-renderer/CHANGELOG.md b/deps/inspection/peers-issues-renderer/CHANGELOG.md index a3ab3dda54..81dc3febba 100644 --- a/deps/inspection/peers-issues-renderer/CHANGELOG.md +++ b/deps/inspection/peers-issues-renderer/CHANGELOG.md @@ -1,5 +1,19 @@ # @pnpm/deps.inspection.peers-issues-renderer +## 1100.0.3 + +### Patch Changes + +- Updated dependencies [35d2355] + - @pnpm/types@1101.2.0 + +## 1100.0.2 + +### Patch Changes + +- Updated dependencies [64afc92] + - @pnpm/types@1101.1.1 + ## 1100.0.1 ### Patch Changes diff --git a/deps/inspection/peers-issues-renderer/package.json b/deps/inspection/peers-issues-renderer/package.json index eb87dd6aa3..0624b7020c 100644 --- a/deps/inspection/peers-issues-renderer/package.json +++ b/deps/inspection/peers-issues-renderer/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/deps.inspection.peers-issues-renderer", - "version": "1100.0.1", + "version": "1100.0.3", "description": "Visualizes peer dependency issues", "keywords": [ "pnpm", diff --git a/deps/inspection/tree-builder/CHANGELOG.md b/deps/inspection/tree-builder/CHANGELOG.md index 2c5300edb2..9e1d174754 100644 --- a/deps/inspection/tree-builder/CHANGELOG.md +++ b/deps/inspection/tree-builder/CHANGELOG.md @@ -1,5 +1,48 @@ # @pnpm/reviewing.dependencies-hierarchy +## 1100.0.12 + +### Patch Changes + +- Updated dependencies [e55f4b5] +- Updated dependencies [35d2355] + - @pnpm/lockfile.utils@1100.0.10 + - @pnpm/types@1101.2.0 + - @pnpm/lockfile.fs@1100.1.2 + - @pnpm/config.normalize-registries@1100.0.5 + - @pnpm/deps.path@1100.0.5 + - @pnpm/installing.modules-yaml@1100.0.6 + - @pnpm/lockfile.detect-dep-types@1100.0.8 + - @pnpm/pkg-manifest.reader@1100.0.5 + - @pnpm/store.cafs@1100.1.7 + +## 1100.0.11 + +### Patch Changes + +- Updated dependencies [9cb48bb] +- Updated dependencies [64afc92] + - @pnpm/lockfile.fs@1100.1.1 + - @pnpm/types@1101.1.1 + - @pnpm/lockfile.utils@1100.0.9 + - @pnpm/config.normalize-registries@1100.0.4 + - @pnpm/deps.path@1100.0.4 + - @pnpm/installing.modules-yaml@1100.0.5 + - @pnpm/lockfile.detect-dep-types@1100.0.7 + - @pnpm/pkg-manifest.reader@1100.0.4 + - @pnpm/store.cafs@1100.1.6 + +## 1100.0.10 + +### Patch Changes + +- Updated dependencies [6e93f35] +- Updated dependencies [2a9bd89] + - @pnpm/lockfile.fs@1100.1.0 + - @pnpm/lockfile.utils@1100.0.8 + - @pnpm/store.cafs@1100.1.5 + - @pnpm/lockfile.detect-dep-types@1100.0.6 + ## 1100.0.9 ### Patch Changes diff --git a/deps/inspection/tree-builder/package.json b/deps/inspection/tree-builder/package.json index 3a327cde37..cef3a75ec9 100644 --- a/deps/inspection/tree-builder/package.json +++ b/deps/inspection/tree-builder/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/deps.inspection.tree-builder", - "version": "1100.0.9", + "version": "1100.0.12", "description": "Creates a dependencies hierarchy for a symlinked `node_modules`", "keywords": [ "pnpm", diff --git a/deps/path/CHANGELOG.md b/deps/path/CHANGELOG.md index f1c8d3d79a..60004990c7 100644 --- a/deps/path/CHANGELOG.md +++ b/deps/path/CHANGELOG.md @@ -1,5 +1,21 @@ # @pnpm/dependency-path +## 1100.0.5 + +### Patch Changes + +- Updated dependencies [35d2355] + - @pnpm/types@1101.2.0 + - @pnpm/crypto.hash@1100.0.1 + +## 1100.0.4 + +### Patch Changes + +- Updated dependencies [64afc92] + - @pnpm/types@1101.1.1 + - @pnpm/crypto.hash@1100.0.1 + ## 1100.0.3 ### Patch Changes diff --git a/deps/path/package.json b/deps/path/package.json index 791807c5ca..240ab36fe1 100644 --- a/deps/path/package.json +++ b/deps/path/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/deps.path", - "version": "1100.0.3", + "version": "1100.0.5", "description": "Utilities for working with symlinked node_modules", "keywords": [ "pnpm", diff --git a/deps/security/signatures/CHANGELOG.md b/deps/security/signatures/CHANGELOG.md index 28bdce64e2..bbececb62f 100644 --- a/deps/security/signatures/CHANGELOG.md +++ b/deps/security/signatures/CHANGELOG.md @@ -1,5 +1,23 @@ # @pnpm/deps.security.signatures +## 1101.1.4 + +### Patch Changes + +- @pnpm/network.fetch@1100.0.7 + +## 1101.1.3 + +### Patch Changes + +- @pnpm/network.fetch@1100.0.6 + +## 1101.1.2 + +### Patch Changes + +- @pnpm/network.fetch@1100.0.5 + ## 1101.1.1 ### Patch Changes diff --git a/deps/security/signatures/package.json b/deps/security/signatures/package.json index dd0aacadf0..8c56b2dc0e 100644 --- a/deps/security/signatures/package.json +++ b/deps/security/signatures/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/deps.security.signatures", - "version": "1101.1.1", + "version": "1101.1.4", "description": "Verify package signatures from npm registries", "keywords": [ "pnpm", diff --git a/deps/status/CHANGELOG.md b/deps/status/CHANGELOG.md index 2e66148c13..07ec74c6e8 100644 --- a/deps/status/CHANGELOG.md +++ b/deps/status/CHANGELOG.md @@ -1,5 +1,76 @@ # @pnpm/deps.status +## 1100.0.19 + +### Patch Changes + +- Updated dependencies [e8b3ae1] +- Updated dependencies [a23956e] +- Updated dependencies [35d2355] + - @pnpm/workspace.projects-reader@1101.0.8 + - @pnpm/config.reader@1101.4.1 + - @pnpm/types@1101.2.0 + - @pnpm/workspace.state@1100.0.16 + - @pnpm/lockfile.fs@1100.1.2 + - @pnpm/lockfile.verification@1100.0.13 + - @pnpm/installing.context@1100.0.13 + - @pnpm/resolving.resolver-base@1100.3.1 + - @pnpm/workspace.workspace-manifest-reader@1100.0.5 + - @pnpm/lockfile.settings-checker@1100.0.13 + +## 1100.0.18 + +### Patch Changes + +- Updated dependencies [3b62f9d] +- Updated dependencies [212315d] + - @pnpm/config.reader@1101.4.0 + - @pnpm/workspace.projects-reader@1101.0.7 + - @pnpm/workspace.state@1100.0.15 + +## 1100.0.17 + +### Patch Changes + +- Updated dependencies [3687b0e] +- Updated dependencies [ced20cb] +- Updated dependencies [9cb48bb] +- Updated dependencies [d1b340f] +- Updated dependencies [1627943] +- Updated dependencies [64afc92] + - @pnpm/config.reader@1101.3.3 + - @pnpm/lockfile.fs@1100.1.1 + - @pnpm/resolving.resolver-base@1100.3.0 + - @pnpm/types@1101.1.1 + - @pnpm/workspace.state@1100.0.14 + - @pnpm/installing.context@1100.0.12 + - @pnpm/lockfile.verification@1100.0.12 + - @pnpm/workspace.projects-reader@1101.0.6 + - @pnpm/workspace.workspace-manifest-reader@1100.0.4 + - @pnpm/lockfile.settings-checker@1100.0.12 + +## 1100.0.16 + +### Patch Changes + +- Updated dependencies [4195766] +- Updated dependencies [31538bf] +- Updated dependencies [020ac45] +- Updated dependencies [d3f8408] +- Updated dependencies [6e93f35] +- Updated dependencies [a62f959] +- Updated dependencies [ba2c884] +- Updated dependencies [2a9bd89] +- Updated dependencies [8df408c] + - @pnpm/resolving.resolver-base@1100.2.0 + - @pnpm/config.reader@1101.3.2 + - @pnpm/lockfile.fs@1100.1.0 + - @pnpm/installing.context@1100.0.11 + - @pnpm/lockfile.verification@1100.0.11 + - @pnpm/workspace.state@1100.0.13 + - @pnpm/lockfile.settings-checker@1100.0.11 + - @pnpm/workspace.projects-reader@1101.0.5 + ## 1100.0.15 ### Patch Changes diff --git a/deps/status/package.json b/deps/status/package.json index 11e18e6211..a8712deb23 100644 --- a/deps/status/package.json +++ b/deps/status/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/deps.status", - "version": "1100.0.15", + "version": "1100.0.19", "description": "Check dependencies status", "keywords": [ "pnpm", diff --git a/dylint.toml b/dylint.toml index 269c736ff8..282f1edc3c 100644 --- a/dylint.toml +++ b/dylint.toml @@ -8,5 +8,59 @@ # or via the `just dylint` recipe. [workspace.metadata.dylint] libraries = [ - { git = "https://github.com/KSXGitHub/perfectionist", tag = "0.0.0-rc.7" }, + { git = "https://github.com/KSXGitHub/perfectionist", tag = "0.0.0-rc.15" }, +] + +# Project-local DSL / test macros that perfectionist's exactly-once +# heuristic cannot peer through. The built-in allow list in rc.14 already +# covers `concat!`, `json!`, and the `assert_*_snapshot!` family, so they +# do not need to be repeated here. +["perfectionist::macro_argument_binding"] +ignore = [ + # `pacquet_testing_utils::allow_known_failure!` binds its argument + # to a `let` internally before the match, so the expression is + # evaluated exactly once. The rule cannot peer through the macro + # body to see that. + "allow_known_failure", + # Test-local `static_env!` (in `npmrc_auth/tests.rs`) generates an + # `EnvVar` impl. The `&[(...)]` slice expression it receives binds + # to a local inside the impl, evaluated once on each `var(name)` + # call. + "static_env", + # Test-local `case!` (in `pkg_*_*/tests.rs`) takes + # `$input:expr, $message:expr, $variant:pat`. The lint sees the + # `$variant` slot's token stream as a non-trivial expression even + # though it's parsed as a pattern. The macro binds `$input` to a + # local on entry, so the exactly-once contract holds. + "case", + # `rusqlite::params!` is a DSL macro that wraps each arg as a + # `&dyn ToSql` reference. Args carry no side effects (they are + # already references / borrows that the macro forwards once); + # hoisting to a `let` is purely noise. + "params", +] + +[perfectionist] +enable = ["derive_ordering"] +disable = [ + { name = "arc_rc_clone", reason = "`arc_rc_clone` is enforced by `clippy::clone_on_ref_ptr` instead" }, +] + +["perfectionist::derive_ordering"] +style = "prefix_then_alphabetical" +prefix = [ + "Debug", + "Default", + "Display", + "Error", + "Diagnostic", + "Clone", + "Copy", + "PartialEq", + "Eq", + "PartialOrd", + "Ord", + "Hash", + "Serialize", + "Deserialize", ] diff --git a/engine/pm/commands/CHANGELOG.md b/engine/pm/commands/CHANGELOG.md index 97828265ca..8a244591cf 100644 --- a/engine/pm/commands/CHANGELOG.md +++ b/engine/pm/commands/CHANGELOG.md @@ -1,5 +1,152 @@ # @pnpm/engine.pm.commands +## 1101.1.17 + +### Patch Changes + +- Updated dependencies [a23956e] +- Updated dependencies [a456dc7] +- Updated dependencies [35d2355] +- Updated dependencies [440e155] +- Updated dependencies [0721d64] + - @pnpm/config.reader@1101.4.1 + - @pnpm/workspace.project-manifest-reader@1100.0.9 + - @pnpm/types@1101.2.0 + - @pnpm/global.commands@1100.0.22 + - @pnpm/resolving.npm-resolver@1101.3.3 + - @pnpm/store.connection-manager@1100.2.4 + - @pnpm/installing.client@1100.2.3 + - @pnpm/installing.env-installer@1101.1.3 + - @pnpm/bins.linker@1100.0.10 + - @pnpm/cli.utils@1101.0.8 + - @pnpm/installing.deps-restorer@1101.1.6 + - @pnpm/deps.graph-hasher@1100.2.2 + - @pnpm/lockfile.fs@1100.1.2 + - @pnpm/building.policy@1100.0.7 + - @pnpm/cli.meta@1100.0.5 + - @pnpm/config.version-policy@1100.1.2 + - @pnpm/global.packages@1100.0.5 + - @pnpm/lockfile.types@1100.0.8 + - @pnpm/store.controller@1101.0.9 + +## 1101.1.16 + +### Patch Changes + +- Updated dependencies [d7da112] +- Updated dependencies [155af87] +- Updated dependencies [3b62f9d] +- Updated dependencies [212315d] + - @pnpm/workspace.project-manifest-reader@1100.0.8 + - @pnpm/installing.env-installer@1101.1.2 + - @pnpm/config.reader@1101.4.0 + - @pnpm/resolving.npm-resolver@1101.3.2 + - @pnpm/bins.linker@1100.0.9 + - @pnpm/cli.utils@1101.0.7 + - @pnpm/installing.deps-restorer@1101.1.5 + - @pnpm/global.commands@1100.0.21 + - @pnpm/store.connection-manager@1100.2.3 + - @pnpm/installing.client@1100.2.2 + - @pnpm/store.controller@1101.0.8 + +## 1101.1.15 + +### Patch Changes + +- Updated dependencies [2061c55] +- Updated dependencies [e5e7b72] + - @pnpm/installing.env-installer@1101.1.1 + - @pnpm/resolving.npm-resolver@1101.3.1 + - @pnpm/installing.client@1100.2.1 + - @pnpm/store.connection-manager@1100.2.2 + - @pnpm/store.controller@1101.0.8 + - @pnpm/global.commands@1100.0.20 + - @pnpm/installing.deps-restorer@1101.1.4 + +## 1101.1.14 + +### Patch Changes + +- Updated dependencies [3687b0e] +- Updated dependencies [c8d8fde] +- Updated dependencies [ced20cb] +- Updated dependencies [a620557] +- Updated dependencies [9cb48bb] +- Updated dependencies [d1b340f] +- Updated dependencies [3a54205] +- Updated dependencies [1627943] +- Updated dependencies [64afc92] + - @pnpm/config.reader@1101.3.3 + - @pnpm/installing.env-installer@1101.1.0 + - @pnpm/global.commands@1100.0.19 + - @pnpm/lockfile.fs@1100.1.1 + - @pnpm/resolving.npm-resolver@1101.3.0 + - @pnpm/installing.client@1100.2.0 + - @pnpm/types@1101.1.1 + - @pnpm/store.connection-manager@1100.2.1 + - @pnpm/installing.deps-restorer@1101.1.4 + - @pnpm/store.controller@1101.0.8 + - @pnpm/deps.graph-hasher@1100.2.1 + - @pnpm/lockfile.types@1100.0.7 + - @pnpm/bins.linker@1100.0.8 + - @pnpm/cli.utils@1101.0.6 + - @pnpm/workspace.project-manifest-reader@1100.0.7 + - @pnpm/building.policy@1100.0.6 + - @pnpm/cli.meta@1100.0.4 + - @pnpm/config.version-policy@1100.1.1 + - @pnpm/global.packages@1100.0.4 + +## 1101.1.13 + +### Patch Changes + +- 4195766: Tightened the `minimumReleaseAge` story so the bypass becomes explicit on disk instead of silent, and removed the discover-by-loop dance for strict-mode users: + + 1. Fresh resolutions in loose mode (`minimumReleaseAgeStrict: false`) that fall back to a version newer than the cutoff auto-collect the picked `name@version` into the workspace manifest's `minimumReleaseAgeExclude`. A single info message lists the additions; entries already on the list are left alone. + 2. The post-resolution lockfile verifier introduced in #11583 now runs in loose mode too — every accepted-immature pin must be on `minimumReleaseAgeExclude`, just like strict mode requires. A lockfile produced under a weaker (or absent) policy that still has immature entries is rejected the same way strict mode would reject it. + 3. **Strict mode (interactive)** no longer aborts on the first immature pick. The resolver gathers every immature direct _and_ transitive in one pass; before peer-dependency resolution runs, pnpm prompts the user with the full list and asks whether to add them all to `minimumReleaseAgeExclude` and proceed. Approve → install continues and the workspace manifest is written at the end. Decline → resolution aborts before the lockfile or package.json is touched (tarballs already in the store stay, since the store is idempotent). This closes the [#10488](https://github.com/pnpm/pnpm/issues/10488) loop where security bumps to packages with platform-specific transitives (e.g. `next` + the `@next/swc-*` shims) made users re-run `pnpm add` once per transitive. + 4. **Strict mode (non-interactive / CI)** now aborts with the full immature set in the error message instead of the first pick. The resolver always collects every immature direct + transitive; the install command then throws `ERR_PNPM_NO_MATURE_MATCHING_VERSION` listing each entry's `name@version` and publish time. Deterministic CI behavior is preserved (same exit code, same error code), but the error pinpoints every offending entry instead of forcing the discover-by-loop dance. The expected workflow is interactive approval locally → the lockfile + workspace manifest get committed → CI runs cleanly against the populated exclude list. + + 5. **The lockfile verifier now also covers `trustPolicy: 'no-downgrade'`.** The same post-resolution gate that re-checks `minimumReleaseAge` on lockfile entries now re-runs `failIfTrustDowngraded` for every npm-registry entry whose name isn't on `trustPolicyExclude`. The two checks share a single full-metadata fetch per package, so the extra coverage doesn't cost an extra round trip when both policies are active. Resolver-time trust checks still run as before — this just closes the gap when an entry bypasses resolution (peek path, `--frozen-lockfile`, restored CI cache). + + Pacquet parity: not ported — pacquet's `minimumReleaseAge` policy is itself only stubbed today (see `pacquet/crates/package-manager/src/version_policy.rs`). The auto-exclude, loose-mode verifier, prompt, and the new trust-policy verifier check will travel with the broader policy port whenever that happens. + +- b6e2c8c: Make `pnpm self-update` respect `minimumReleaseAge` (and `minimumReleaseAgeExclude`) when resolving which pnpm version to install. + + When the `latest` dist-tag points to a version newer than the configured age threshold, `self-update` now selects the newest mature version instead unless excluded by `minimumReleaseAgeExclude`. + + Also makes `dlx` and `outdated` surface invalid `minimumReleaseAgeExclude` patterns under the same `ERR_PNPM_INVALID_MINIMUM_RELEASE_AGE_EXCLUDE` error code already used by `install`, instead of leaking the internal `ERR_PNPM_INVALID_VERSION_UNION` / `ERR_PNPM_NAME_PATTERN_IN_VERSION_UNION` codes. + +- Updated dependencies [963861c] +- Updated dependencies [4195766] +- Updated dependencies [31538bf] +- Updated dependencies [020ac45] +- Updated dependencies [b6e2c8c] +- Updated dependencies [d3f8408] +- Updated dependencies [6e93f35] +- Updated dependencies [3ddde2b] +- Updated dependencies [5dc8be8] +- Updated dependencies [a62f959] +- Updated dependencies [ba2c884] +- Updated dependencies [2a9bd89] +- Updated dependencies [8df408c] + - @pnpm/resolving.npm-resolver@1101.2.0 + - @pnpm/installing.client@1100.1.0 + - @pnpm/store.connection-manager@1100.2.0 + - @pnpm/config.reader@1101.3.2 + - @pnpm/config.version-policy@1100.1.0 + - @pnpm/lockfile.fs@1100.1.0 + - @pnpm/deps.graph-hasher@1100.2.0 + - @pnpm/installing.deps-restorer@1101.1.3 + - @pnpm/installing.env-installer@1101.0.10 + - @pnpm/lockfile.types@1100.0.6 + - @pnpm/store.controller@1101.0.7 + - @pnpm/global.commands@1100.0.18 + - @pnpm/building.policy@1100.0.5 + - @pnpm/cli.utils@1101.0.5 + - @pnpm/bins.linker@1100.0.7 + - @pnpm/workspace.project-manifest-reader@1100.0.6 + ## 1101.1.12 ### Patch Changes diff --git a/engine/pm/commands/package.json b/engine/pm/commands/package.json index bc85558202..e5a7990be0 100644 --- a/engine/pm/commands/package.json +++ b/engine/pm/commands/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/engine.pm.commands", - "version": "1101.1.12", + "version": "1101.1.17", "description": "pnpm commands for self-updating and setting up pnpm", "keywords": [ "pnpm", diff --git a/engine/pm/commands/src/self-updater/selfUpdate.ts b/engine/pm/commands/src/self-updater/selfUpdate.ts index d2130b5d52..bc0c435afb 100644 --- a/engine/pm/commands/src/self-updater/selfUpdate.ts +++ b/engine/pm/commands/src/self-updater/selfUpdate.ts @@ -7,7 +7,7 @@ import { docsUrl } from '@pnpm/cli.utils' import { type Config, type ConfigContext, parsePackageManager, types as allTypes } from '@pnpm/config.reader' import { getPublishedByPolicy } from '@pnpm/config.version-policy' import { PnpmError } from '@pnpm/error' -import { createResolver } from '@pnpm/installing.client' +import { createResolver, makeResolutionStrict } from '@pnpm/installing.client' import { resolvePackageManagerIntegrities } from '@pnpm/installing.env-installer' import { readEnvLockfile } from '@pnpm/lockfile.fs' import { globalInfo, globalWarn } from '@pnpm/logger' @@ -80,12 +80,21 @@ export async function handler ( throw new PnpmError('CANT_SELF_UPDATE_IN_COREPACK', 'You should update pnpm with corepack') } globalInfo('Checking for updates...') - const { resolve } = createResolver({ + const { resolve: baseResolve } = createResolver({ ...opts, configByUri: opts.configByUri, - strictPublishedByCheck: Boolean(opts.minimumReleaseAge) && opts.minimumReleaseAgeStrict === true, ignoreMissingTimeField: opts.minimumReleaseAgeIgnoreMissingTime, }) + // self-update has nowhere to "defer to" either — wrap the resolver + // under any policy that wants to reject violations up-front. Strict + // minimumReleaseAge keeps self-update from switching to an immature + // pnpm; `trustPolicy: 'no-downgrade'` keeps it from switching to a + // pnpm whose trust evidence weakened relative to the installed + // version. + const strictResolution = + (Boolean(opts.minimumReleaseAge) && opts.minimumReleaseAgeStrict === true) || + opts.trustPolicy === 'no-downgrade' + const resolve = strictResolution ? makeResolutionStrict(baseResolve) : baseResolve const pkgName = 'pnpm' const { publishedBy, publishedByExclude } = getPublishedByPolicy(opts) // `pnpm self-update` (no args) defaults to the `latest` dist-tag, but we diff --git a/engine/runtime/bun-resolver/CHANGELOG.md b/engine/runtime/bun-resolver/CHANGELOG.md index 431d320bb6..030800a4f6 100644 --- a/engine/runtime/bun-resolver/CHANGELOG.md +++ b/engine/runtime/bun-resolver/CHANGELOG.md @@ -1,5 +1,67 @@ # @pnpm/resolving.bun-resolver +## 1101.1.3 + +### Patch Changes + +- Updated dependencies [aa6149d] +- Updated dependencies [35d2355] +- Updated dependencies [0721d64] + - @pnpm/worker@1100.1.8 + - @pnpm/types@1101.2.0 + - @pnpm/resolving.npm-resolver@1101.3.3 + - @pnpm/fetching.fetcher-base@1100.1.6 + - @pnpm/resolving.resolver-base@1100.3.1 + - @pnpm/fetching.binary-fetcher@1101.0.8 + +## 1101.1.2 + +### Patch Changes + +- Updated dependencies [212315d] + - @pnpm/resolving.npm-resolver@1101.3.2 + +## 1101.1.1 + +### Patch Changes + +- @pnpm/resolving.npm-resolver@1101.3.1 + +## 1101.1.0 + +### Minor Changes + +- 1627943: `pnpm outdated` and `pnpm update --interactive` now report Node.js, Deno, and Bun runtimes installed as project dependencies (`runtime:` specifiers). Previously these were silently skipped because the npm specifier parser did not understand the `runtime:` protocol, so runtime versions never appeared in the outdated table or the interactive update picker. + + Internally, the outdated check is now resolver-driven: `@pnpm/resolving.resolver-base` defines a `ResolveLatestFunction` shape (with `LatestQuery` input — `{ wantedDependency, compatible? }` — and `LatestInfo` result — `{ latestManifest? }`), and every protocol resolver (npm, jsr, named-registry, git, tarball, local, node/bun/deno runtimes) exports its own `resolveLatest*` function alongside its `resolve*`. `@pnpm/resolving.default-resolver` composes them into a single dispatcher, exposed through `@pnpm/installing.client` as `createResolver(...).resolveLatest`. + + Each resolver decides whether it owns the dep and what "latest" means for its protocol; the outdated command derives `current` / `wanted` display values from the lockfile snapshot (`pkgSnapshot.version` for semver protocols, raw ref for URL-shaped ones) and uses raw ref equality for the "lockfile changed" check, so protocol knowledge stays inside each resolver instead of the command. + +### Patch Changes + +- Updated dependencies [3a54205] +- Updated dependencies [1627943] +- Updated dependencies [64afc92] + - @pnpm/resolving.npm-resolver@1101.3.0 + - @pnpm/resolving.resolver-base@1100.3.0 + - @pnpm/types@1101.1.1 + - @pnpm/fetching.fetcher-base@1100.1.5 + - @pnpm/worker@1100.1.7 + - @pnpm/fetching.binary-fetcher@1101.0.7 + +## 1101.0.7 + +### Patch Changes + +- Updated dependencies [963861c] +- Updated dependencies [4195766] +- Updated dependencies [31538bf] + - @pnpm/resolving.npm-resolver@1101.2.0 + - @pnpm/resolving.resolver-base@1100.2.0 + - @pnpm/fetching.fetcher-base@1100.1.4 + - @pnpm/fetching.binary-fetcher@1101.0.6 + - @pnpm/worker@1100.1.6 + ## 1101.0.6 ### Patch Changes diff --git a/engine/runtime/bun-resolver/package.json b/engine/runtime/bun-resolver/package.json index a81894ba16..c363c82e27 100644 --- a/engine/runtime/bun-resolver/package.json +++ b/engine/runtime/bun-resolver/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/engine.runtime.bun-resolver", - "version": "1101.0.6", + "version": "1101.1.3", "description": "Resolves the Bun runtime", "keywords": [ "pnpm", diff --git a/engine/runtime/bun-resolver/src/index.ts b/engine/runtime/bun-resolver/src/index.ts index f60d1b4738..fb5ad76124 100644 --- a/engine/runtime/bun-resolver/src/index.ts +++ b/engine/runtime/bun-resolver/src/index.ts @@ -1,9 +1,11 @@ import { fetchShasumsFile } from '@pnpm/crypto.shasums-file' import { PnpmError } from '@pnpm/error' import type { FetchFromRegistry } from '@pnpm/fetching.types' -import type { NpmResolver } from '@pnpm/resolving.npm-resolver' +import { MINIMUM_RELEASE_AGE_VIOLATION_CODE, type NpmResolver } from '@pnpm/resolving.npm-resolver' import type { BinaryResolution, + LatestInfo, + LatestQuery, PlatformAssetResolution, PlatformAssetTarget, ResolveOptions, @@ -65,6 +67,30 @@ export async function resolveBunRuntime ( } } +export async function resolveLatestBunRuntime ( + ctx: { resolveFromNpm: NpmResolver }, + query: LatestQuery, + opts: ResolveOptions +): Promise { + const manifestSpec = query.wantedDependency.bareSpecifier + if (query.wantedDependency.alias !== 'bun' || !manifestSpec?.startsWith('runtime:')) return undefined + const versionSpec = query.compatible ? manifestSpec.substring('runtime:'.length) : 'latest' + try { + const npmResolution = await ctx.resolveFromNpm( + { alias: 'bun', bareSpecifier: versionSpec }, + query.compatible ? opts : { ...opts, update: 'latest' } + ) + if (npmResolution?.policyViolation?.code === MINIMUM_RELEASE_AGE_VIOLATION_CODE) return {} + if (!npmResolution?.manifest) return {} + return { latestManifest: { name: 'bun', version: npmResolution.manifest.version } } + } catch (err) { + if (opts.publishedBy && (err as { code?: string }).code === 'ERR_PNPM_NO_MATCHING_VERSION') { + return {} + } + throw err + } +} + async function readBunAssets (fetch: FetchFromRegistry, version: string): Promise { const integritiesFileUrl = `https://github.com/oven-sh/bun/releases/download/bun-v${version}/SHASUMS256.txt` const shasumsFileItems = await fetchShasumsFile(fetch, integritiesFileUrl) diff --git a/engine/runtime/commands/CHANGELOG.md b/engine/runtime/commands/CHANGELOG.md index e67a649ae6..04d05638c6 100644 --- a/engine/runtime/commands/CHANGELOG.md +++ b/engine/runtime/commands/CHANGELOG.md @@ -1,5 +1,59 @@ # @pnpm/engine.runtime.commands +## 1100.1.0 + +### Minor Changes + +- a662de4: `pnpm runtime set ` now saves the runtime to `devEngines.runtime` by default instead of `engines.runtime`. Pass `--save-prod` (or `-P`) to save it to `engines.runtime` instead [#11948](https://github.com/pnpm/pnpm/issues/11948). + +### Patch Changes + +- Updated dependencies [a23956e] +- Updated dependencies [35d2355] + - @pnpm/config.reader@1101.4.1 + - @pnpm/engine.runtime.node-resolver@1101.1.2 + - @pnpm/cli.utils@1101.0.8 + - @pnpm/network.fetch@1100.0.7 + +## 1100.0.17 + +### Patch Changes + +- Updated dependencies [3b62f9d] +- Updated dependencies [212315d] + - @pnpm/config.reader@1101.4.0 + - @pnpm/cli.utils@1101.0.7 + - @pnpm/engine.runtime.node-resolver@1101.1.1 + +## 1100.0.16 + +### Patch Changes + +- Updated dependencies [3687b0e] +- Updated dependencies [ced20cb] +- Updated dependencies [d1b340f] +- Updated dependencies [1627943] + - @pnpm/config.reader@1101.3.3 + - @pnpm/engine.runtime.node-resolver@1101.1.0 + - @pnpm/cli.utils@1101.0.6 + - @pnpm/network.fetch@1100.0.6 + +## 1100.0.15 + +### Patch Changes + +- Updated dependencies [020ac45] +- Updated dependencies [d3f8408] +- Updated dependencies [247d70b] +- Updated dependencies [a62f959] +- Updated dependencies [ba2c884] +- Updated dependencies [8df408c] + - @pnpm/config.reader@1101.3.2 + - @pnpm/exec.pnpm-cli-runner@1100.0.1 + - @pnpm/engine.runtime.node-resolver@1101.0.9 + - @pnpm/network.fetch@1100.0.5 + - @pnpm/cli.utils@1101.0.5 + ## 1100.0.14 ### Patch Changes diff --git a/engine/runtime/commands/package.json b/engine/runtime/commands/package.json index b0aeeb54ab..93cb3cf7ac 100644 --- a/engine/runtime/commands/package.json +++ b/engine/runtime/commands/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/engine.runtime.commands", - "version": "1100.0.14", + "version": "1100.1.0", "description": "pnpm commands for managing runtimes", "keywords": [ "pnpm", diff --git a/engine/runtime/commands/src/runtime/runtime.ts b/engine/runtime/commands/src/runtime/runtime.ts index 49ac563434..9f296fe3f7 100644 --- a/engine/runtime/commands/src/runtime/runtime.ts +++ b/engine/runtime/commands/src/runtime/runtime.ts @@ -12,6 +12,8 @@ export type RuntimeCommandOptions = Pick & Partial> export const skipPackageManagerCheck = true @@ -23,6 +25,8 @@ export function rcOptionsTypes (): Record { export function cliOptionsTypes (): Record { return { global: Boolean, + 'save-dev': Boolean, + 'save-prod': Boolean, } } @@ -49,11 +53,22 @@ export function help (): string { name: '--global', shortAlias: '-g', }, + { + description: 'Save the runtime to `devEngines.runtime`. This is the default', + name: '--save-dev', + shortAlias: '-D', + }, + { + description: 'Save the runtime to `engines.runtime`', + name: '--save-prod', + shortAlias: '-P', + }, ], }, ], url: docsUrl('runtime'), usages: [ + 'pnpm runtime set node 22', 'pnpm runtime set node 22 -g', 'pnpm runtime set node lts -g', 'pnpm runtime set node rc/22 -g', @@ -91,6 +106,14 @@ function runtimeSet (opts: RuntimeCommandOptions, params: string[]): void { const versionSpec = params[1]?.trim() const args = ['add', `${runtimeName}@runtime:${versionSpec ?? ''}`] + // Default to `devEngines.runtime`; the manifest writer maps a + // `devDependencies.: runtime:` entry to it. + // `saveDev` wins over `saveProd` to match `getSaveType` precedence. + if (opts.saveDev || !opts.saveProd) { + args.push('--save-dev') + } else { + args.push('--save-prod') + } if (opts.global) { args.push('--global') if (opts.bin) args.push('--global-bin-dir', opts.bin) diff --git a/engine/runtime/commands/test/runtime.test.ts b/engine/runtime/commands/test/runtime.test.ts index 685e47ab61..ae653da21f 100644 --- a/engine/runtime/commands/test/runtime.test.ts +++ b/engine/runtime/commands/test/runtime.test.ts @@ -23,12 +23,12 @@ test('runtime set calls pnpm add with the correct arguments globally', async () }, ['set', 'node', '22']) expect(mockRunPnpmCli).toHaveBeenCalledWith( - ['add', 'node@runtime:22', '--global', '--global-bin-dir', '/usr/local/bin', '--store-dir', '/tmp/store', '--cache-dir', '/tmp/cache'], + ['add', 'node@runtime:22', '--save-dev', '--global', '--global-bin-dir', '/usr/local/bin', '--store-dir', '/tmp/store', '--cache-dir', '/tmp/cache'], { cwd: '/tmp/pnpm-home' } ) }) -test('runtime set uses project dir when not global', async () => { +test('runtime set defaults to --save-dev so the runtime lands in devEngines', async () => { await runtime.handler({ bin: '/usr/local/bin', dir: '/tmp/project', @@ -37,7 +37,53 @@ test('runtime set uses project dir when not global', async () => { }, ['set', 'node', '22']) expect(mockRunPnpmCli).toHaveBeenCalledWith( - ['add', 'node@runtime:22', '--ignore-workspace-root-check'], + ['add', 'node@runtime:22', '--save-dev', '--ignore-workspace-root-check'], + { cwd: '/tmp/project' } + ) +}) + +test('runtime set with --save-prod saves the runtime under engines', async () => { + await runtime.handler({ + bin: '/usr/local/bin', + dir: '/tmp/project', + global: false, + pnpmHomeDir: '/tmp/pnpm-home', + saveProd: true, + }, ['set', 'node', '22']) + + expect(mockRunPnpmCli).toHaveBeenCalledWith( + ['add', 'node@runtime:22', '--save-prod', '--ignore-workspace-root-check'], + { cwd: '/tmp/project' } + ) +}) + +test('runtime set with --save-dev keeps the runtime under devEngines (matches the default)', async () => { + await runtime.handler({ + bin: '/usr/local/bin', + dir: '/tmp/project', + global: false, + pnpmHomeDir: '/tmp/pnpm-home', + saveDev: true, + }, ['set', 'node', '22']) + + expect(mockRunPnpmCli).toHaveBeenCalledWith( + ['add', 'node@runtime:22', '--save-dev', '--ignore-workspace-root-check'], + { cwd: '/tmp/project' } + ) +}) + +test('runtime set with both --save-dev and --save-prod prefers --save-dev (matches getSaveType precedence)', async () => { + await runtime.handler({ + bin: '/usr/local/bin', + dir: '/tmp/project', + global: false, + pnpmHomeDir: '/tmp/pnpm-home', + saveDev: true, + saveProd: true, + }, ['set', 'node', '22']) + + expect(mockRunPnpmCli).toHaveBeenCalledWith( + ['add', 'node@runtime:22', '--save-dev', '--ignore-workspace-root-check'], { cwd: '/tmp/project' } ) }) @@ -51,7 +97,7 @@ test('runtime set without version spec', async () => { }, ['set', 'node']) expect(mockRunPnpmCli).toHaveBeenCalledWith( - ['add', 'node@runtime:', '--global', '--global-bin-dir', '/usr/local/bin'], + ['add', 'node@runtime:', '--save-dev', '--global', '--global-bin-dir', '/usr/local/bin'], { cwd: '/tmp/pnpm-home' } ) }) @@ -65,7 +111,7 @@ test('runtime set works with deno', async () => { }, ['set', 'deno', '2']) expect(mockRunPnpmCli).toHaveBeenCalledWith( - ['add', 'deno@runtime:2', '--global', '--global-bin-dir', '/usr/local/bin'], + ['add', 'deno@runtime:2', '--save-dev', '--global', '--global-bin-dir', '/usr/local/bin'], { cwd: '/tmp/pnpm-home' } ) }) diff --git a/engine/runtime/deno-resolver/CHANGELOG.md b/engine/runtime/deno-resolver/CHANGELOG.md index f398bde312..c5e556d37b 100644 --- a/engine/runtime/deno-resolver/CHANGELOG.md +++ b/engine/runtime/deno-resolver/CHANGELOG.md @@ -1,5 +1,67 @@ # @pnpm/resolving.deno-resolver +## 1101.1.3 + +### Patch Changes + +- Updated dependencies [aa6149d] +- Updated dependencies [35d2355] +- Updated dependencies [0721d64] + - @pnpm/worker@1100.1.8 + - @pnpm/types@1101.2.0 + - @pnpm/resolving.npm-resolver@1101.3.3 + - @pnpm/fetching.fetcher-base@1100.1.6 + - @pnpm/resolving.resolver-base@1100.3.1 + - @pnpm/fetching.binary-fetcher@1101.0.8 + +## 1101.1.2 + +### Patch Changes + +- Updated dependencies [212315d] + - @pnpm/resolving.npm-resolver@1101.3.2 + +## 1101.1.1 + +### Patch Changes + +- @pnpm/resolving.npm-resolver@1101.3.1 + +## 1101.1.0 + +### Minor Changes + +- 1627943: `pnpm outdated` and `pnpm update --interactive` now report Node.js, Deno, and Bun runtimes installed as project dependencies (`runtime:` specifiers). Previously these were silently skipped because the npm specifier parser did not understand the `runtime:` protocol, so runtime versions never appeared in the outdated table or the interactive update picker. + + Internally, the outdated check is now resolver-driven: `@pnpm/resolving.resolver-base` defines a `ResolveLatestFunction` shape (with `LatestQuery` input — `{ wantedDependency, compatible? }` — and `LatestInfo` result — `{ latestManifest? }`), and every protocol resolver (npm, jsr, named-registry, git, tarball, local, node/bun/deno runtimes) exports its own `resolveLatest*` function alongside its `resolve*`. `@pnpm/resolving.default-resolver` composes them into a single dispatcher, exposed through `@pnpm/installing.client` as `createResolver(...).resolveLatest`. + + Each resolver decides whether it owns the dep and what "latest" means for its protocol; the outdated command derives `current` / `wanted` display values from the lockfile snapshot (`pkgSnapshot.version` for semver protocols, raw ref for URL-shaped ones) and uses raw ref equality for the "lockfile changed" check, so protocol knowledge stays inside each resolver instead of the command. + +### Patch Changes + +- Updated dependencies [3a54205] +- Updated dependencies [1627943] +- Updated dependencies [64afc92] + - @pnpm/resolving.npm-resolver@1101.3.0 + - @pnpm/resolving.resolver-base@1100.3.0 + - @pnpm/types@1101.1.1 + - @pnpm/fetching.fetcher-base@1100.1.5 + - @pnpm/worker@1100.1.7 + - @pnpm/fetching.binary-fetcher@1101.0.7 + +## 1101.0.7 + +### Patch Changes + +- Updated dependencies [963861c] +- Updated dependencies [4195766] +- Updated dependencies [31538bf] + - @pnpm/resolving.npm-resolver@1101.2.0 + - @pnpm/resolving.resolver-base@1100.2.0 + - @pnpm/fetching.fetcher-base@1100.1.4 + - @pnpm/fetching.binary-fetcher@1101.0.6 + - @pnpm/worker@1100.1.6 + ## 1101.0.6 ### Patch Changes diff --git a/engine/runtime/deno-resolver/package.json b/engine/runtime/deno-resolver/package.json index 91c7fea836..1dc3d350d8 100644 --- a/engine/runtime/deno-resolver/package.json +++ b/engine/runtime/deno-resolver/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/engine.runtime.deno-resolver", - "version": "1101.0.6", + "version": "1101.1.3", "description": "Resolves the Deno runtime", "keywords": [ "pnpm", diff --git a/engine/runtime/deno-resolver/src/index.ts b/engine/runtime/deno-resolver/src/index.ts index c9ef4c51c9..3d628214a7 100644 --- a/engine/runtime/deno-resolver/src/index.ts +++ b/engine/runtime/deno-resolver/src/index.ts @@ -1,8 +1,10 @@ import { PnpmError } from '@pnpm/error' import type { FetchFromRegistry } from '@pnpm/fetching.types' -import type { NpmResolver } from '@pnpm/resolving.npm-resolver' +import { MINIMUM_RELEASE_AGE_VIOLATION_CODE, type NpmResolver } from '@pnpm/resolving.npm-resolver' import type { BinaryResolution, + LatestInfo, + LatestQuery, PlatformAssetResolution, PlatformAssetTarget, ResolveOptions, @@ -96,6 +98,30 @@ export async function resolveDenoRuntime ( } } +export async function resolveLatestDenoRuntime ( + ctx: { resolveFromNpm: NpmResolver }, + query: LatestQuery, + opts: ResolveOptions +): Promise { + const manifestSpec = query.wantedDependency.bareSpecifier + if (query.wantedDependency.alias !== 'deno' || !manifestSpec?.startsWith('runtime:')) return undefined + const versionSpec = query.compatible ? manifestSpec.substring('runtime:'.length) : 'latest' + try { + const npmResolution = await ctx.resolveFromNpm( + { alias: 'deno', bareSpecifier: versionSpec }, + query.compatible ? opts : { ...opts, update: 'latest' } + ) + if (npmResolution?.policyViolation?.code === MINIMUM_RELEASE_AGE_VIOLATION_CODE) return {} + if (!npmResolution?.manifest) return {} + return { latestManifest: { name: 'deno', version: npmResolution.manifest.version } } + } catch (err) { + if (opts.publishedBy && (err as { code?: string }).code === 'ERR_PNPM_NO_MATCHING_VERSION') { + return {} + } + throw err + } +} + function parseAssetName (name: string): PlatformAssetTarget[] | null { const m = ASSET_REGEX.exec(name) if (!m?.groups) return null diff --git a/engine/runtime/node-resolver/CHANGELOG.md b/engine/runtime/node-resolver/CHANGELOG.md index 4e1b912c77..2b2be38a0f 100644 --- a/engine/runtime/node-resolver/CHANGELOG.md +++ b/engine/runtime/node-resolver/CHANGELOG.md @@ -1,5 +1,58 @@ # @pnpm/node.resolver +## 1101.1.2 + +### Patch Changes + +- Updated dependencies [a23956e] +- Updated dependencies [35d2355] + - @pnpm/config.reader@1101.4.1 + - @pnpm/types@1101.2.0 + - @pnpm/resolving.resolver-base@1100.3.1 + +## 1101.1.1 + +### Patch Changes + +- Updated dependencies [3b62f9d] +- Updated dependencies [212315d] + - @pnpm/config.reader@1101.4.0 + +## 1101.1.0 + +### Minor Changes + +- 1627943: `pnpm outdated` and `pnpm update --interactive` now report Node.js, Deno, and Bun runtimes installed as project dependencies (`runtime:` specifiers). Previously these were silently skipped because the npm specifier parser did not understand the `runtime:` protocol, so runtime versions never appeared in the outdated table or the interactive update picker. + + Internally, the outdated check is now resolver-driven: `@pnpm/resolving.resolver-base` defines a `ResolveLatestFunction` shape (with `LatestQuery` input — `{ wantedDependency, compatible? }` — and `LatestInfo` result — `{ latestManifest? }`), and every protocol resolver (npm, jsr, named-registry, git, tarball, local, node/bun/deno runtimes) exports its own `resolveLatest*` function alongside its `resolve*`. `@pnpm/resolving.default-resolver` composes them into a single dispatcher, exposed through `@pnpm/installing.client` as `createResolver(...).resolveLatest`. + + Each resolver decides whether it owns the dep and what "latest" means for its protocol; the outdated command derives `current` / `wanted` display values from the lockfile snapshot (`pkgSnapshot.version` for semver protocols, raw ref for URL-shaped ones) and uses raw ref equality for the "lockfile changed" check, so protocol knowledge stays inside each resolver instead of the command. + +### Patch Changes + +- Updated dependencies [3687b0e] +- Updated dependencies [ced20cb] +- Updated dependencies [d1b340f] +- Updated dependencies [1627943] +- Updated dependencies [64afc92] + - @pnpm/config.reader@1101.3.3 + - @pnpm/resolving.resolver-base@1100.3.0 + - @pnpm/types@1101.1.1 + +## 1101.0.9 + +### Patch Changes + +- Updated dependencies [4195766] +- Updated dependencies [31538bf] +- Updated dependencies [020ac45] +- Updated dependencies [d3f8408] +- Updated dependencies [a62f959] +- Updated dependencies [ba2c884] +- Updated dependencies [8df408c] + - @pnpm/resolving.resolver-base@1100.2.0 + - @pnpm/config.reader@1101.3.2 + ## 1101.0.8 ### Patch Changes diff --git a/engine/runtime/node-resolver/package.json b/engine/runtime/node-resolver/package.json index 28424b24d9..a6041e6431 100644 --- a/engine/runtime/node-resolver/package.json +++ b/engine/runtime/node-resolver/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/engine.runtime.node-resolver", - "version": "1101.0.8", + "version": "1101.1.2", "description": "Resolves a Node.js version specifier to an exact Node.js version", "keywords": [ "pnpm", diff --git a/engine/runtime/node-resolver/src/index.ts b/engine/runtime/node-resolver/src/index.ts index 39c7b0d2a9..3e45f02744 100644 --- a/engine/runtime/node-resolver/src/index.ts +++ b/engine/runtime/node-resolver/src/index.ts @@ -3,6 +3,8 @@ import { PnpmError } from '@pnpm/error' import type { FetchFromRegistry } from '@pnpm/fetching.types' import type { BinaryResolution, + LatestInfo, + LatestQuery, PlatformAssetResolution, PlatformAssetTarget, ResolveOptions, @@ -79,6 +81,21 @@ export async function resolveNodeRuntime ( } } +export async function resolveLatestNodeRuntime ( + ctx: { fetchFromRegistry: FetchFromRegistry, nodeDownloadMirrors?: Record }, + query: LatestQuery, + _opts: ResolveOptions +): Promise { + const manifestSpec = query.wantedDependency.bareSpecifier + if (query.wantedDependency.alias !== 'node' || !manifestSpec?.startsWith('runtime:')) return undefined + const versionSpec = query.compatible ? manifestSpec.substring('runtime:'.length) : 'latest' + const { releaseChannel, versionSpecifier } = parseNodeSpecifier(versionSpec) + const nodeMirrorBaseUrl = getNodeMirror(ctx.nodeDownloadMirrors, releaseChannel) + const version = await resolveNodeVersion(ctx.fetchFromRegistry, versionSpecifier, nodeMirrorBaseUrl) + if (!version) return {} + return { latestManifest: { name: 'node', version } } +} + async function readNodeAssets (fetch: FetchFromRegistry, nodeMirrorBaseUrl: string, version: string): Promise { const assets = await readNodeAssetsFromMirror(fetch, { nodeMirrorBaseUrl, version, muslOnly: false }) diff --git a/engine/runtime/system-node-version/CHANGELOG.md b/engine/runtime/system-node-version/CHANGELOG.md deleted file mode 100644 index 171097aec5..0000000000 --- a/engine/runtime/system-node-version/CHANGELOG.md +++ /dev/null @@ -1,112 +0,0 @@ -# @pnpm/env.system-node-version - -## 1100.0.3 - -### Patch Changes - -- @pnpm/cli.meta@1100.0.3 - -## 1100.0.2 - -### Patch Changes - -- 184ce26: Fix the package name in README.md. -- Updated dependencies [184ce26] - - @pnpm/cli.meta@1100.0.2 - -## 1100.0.1 - -### Patch Changes - -- @pnpm/cli.meta@1100.0.1 - -## 1001.0.0 - -### Major Changes - -- 491a84f: This package is now pure ESM. -- 7d2fd48: Node.js v18, 19, 20, and 21 support discontinued. - -### Patch Changes - -- Updated dependencies [491a84f] -- Updated dependencies [7d2fd48] - - @pnpm/cli.meta@1001.0.0 - -## 1000.0.11 - -### Patch Changes - -- @pnpm/cli-meta@1000.0.11 - -## 1000.0.10 - -### Patch Changes - -- @pnpm/cli-meta@1000.0.10 - -## 1000.0.9 - -### Patch Changes - -- @pnpm/cli-meta@1000.0.9 - -## 1000.0.8 - -### Patch Changes - -- @pnpm/cli-meta@1000.0.8 - -## 1000.0.7 - -### Patch Changes - -- @pnpm/cli-meta@1000.0.7 - -## 1000.0.6 - -### Patch Changes - -- @pnpm/cli-meta@1000.0.6 - -## 1000.0.5 - -### Patch Changes - -- @pnpm/cli-meta@1000.0.5 - -## 1000.0.4 - -### Patch Changes - -- @pnpm/cli-meta@1000.0.4 - -## 1000.0.3 - -### Patch Changes - -- @pnpm/cli-meta@1000.0.3 - -## 1000.0.2 - -### Patch Changes - -- @pnpm/cli-meta@1000.0.2 - -## 1000.0.1 - -### Patch Changes - -- @pnpm/cli-meta@1000.0.1 - -## 1.0.1 - -### Patch Changes - -- e476b07: Don't crash if the `use-node-version` setting is used and the system has no Node.js installed [#8769](https://github.com/pnpm/pnpm/issues/8769). - -## 1.0.0 - -### Major Changes - -- d04f7f2: Initial release. diff --git a/engine/runtime/system-node-version/README.md b/engine/runtime/system-node-version/README.md deleted file mode 100644 index 07aba3d15e..0000000000 --- a/engine/runtime/system-node-version/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# @pnpm/engine.runtime.system-node-version - -> Detects the current system node version - -[![npm version](https://img.shields.io/npm/v/@pnpm/engine.runtime.system-node-version.svg)](https://npmx.dev/package/@pnpm/engine.runtime.system-node-version) - -## Installation - -```sh -pnpm add @pnpm/engine.runtime.system-node-version -``` - -## License - -MIT diff --git a/engine/runtime/system-version/CHANGELOG.md b/engine/runtime/system-version/CHANGELOG.md new file mode 100644 index 0000000000..4139be22bc --- /dev/null +++ b/engine/runtime/system-version/CHANGELOG.md @@ -0,0 +1,13 @@ +# @pnpm/engine.runtime.system-version + +## 1100.0.0 + +### Minor Changes + +- 35d2355: Validate `devEngines.runtime` and `engines.runtime` version ranges for `node`, `deno`, and `bun` when `onFail` is set to `error` or `warn`. Previously these settings only had an effect with `onFail: 'download'` — the `error` and `warn` modes silently did nothing [#11818](https://github.com/pnpm/pnpm/issues/11818). Violations now throw `ERR_PNPM_BAD_RUNTIME_VERSION`. + +### Patch Changes + +- Updated dependencies [35d2355] + - @pnpm/types@1101.2.0 + - @pnpm/cli.meta@1100.0.5 diff --git a/engine/runtime/system-version/README.md b/engine/runtime/system-version/README.md new file mode 100644 index 0000000000..f236959d6f --- /dev/null +++ b/engine/runtime/system-version/README.md @@ -0,0 +1,15 @@ +# @pnpm/engine.runtime.system-version + +> Detects the current system version of supported runtimes (Node.js, Deno, Bun) + +[![npm version](https://img.shields.io/npm/v/@pnpm/engine.runtime.system-version.svg)](https://npmx.dev/package/@pnpm/engine.runtime.system-version) + +## Installation + +```sh +pnpm add @pnpm/engine.runtime.system-version +``` + +## License + +MIT diff --git a/engine/runtime/system-node-version/package.json b/engine/runtime/system-version/package.json similarity index 77% rename from engine/runtime/system-node-version/package.json rename to engine/runtime/system-version/package.json index e62f28d6de..7494bb7667 100644 --- a/engine/runtime/system-node-version/package.json +++ b/engine/runtime/system-version/package.json @@ -1,7 +1,7 @@ { - "name": "@pnpm/engine.runtime.system-node-version", - "version": "1100.0.3", - "description": "Detects the current system node version", + "name": "@pnpm/engine.runtime.system-version", + "version": "1100.0.0", + "description": "Detects the current system version of supported runtimes (Node.js, Deno, Bun)", "keywords": [ "pnpm", "pnpm11", @@ -9,8 +9,8 @@ ], "license": "MIT", "funding": "https://opencollective.com/pnpm", - "repository": "https://github.com/pnpm/pnpm/tree/main/engine/runtime/system-node-version", - "homepage": "https://github.com/pnpm/pnpm/tree/main/engine/runtime/system-node-version#readme", + "repository": "https://github.com/pnpm/pnpm/tree/main/engine/runtime/system-version", + "homepage": "https://github.com/pnpm/pnpm/tree/main/engine/runtime/system-version#readme", "bugs": { "url": "https://github.com/pnpm/pnpm/issues" }, @@ -33,12 +33,13 @@ }, "dependencies": { "@pnpm/cli.meta": "workspace:*", + "@pnpm/types": "workspace:*", "execa": "catalog:", "memoize": "catalog:" }, "devDependencies": { "@jest/globals": "catalog:", - "@pnpm/engine.runtime.system-node-version": "workspace:*" + "@pnpm/engine.runtime.system-version": "workspace:*" }, "engines": { "node": ">=22.13" diff --git a/engine/runtime/system-node-version/src/index.ts b/engine/runtime/system-version/src/index.ts similarity index 57% rename from engine/runtime/system-node-version/src/index.ts rename to engine/runtime/system-version/src/index.ts index d779812747..ab98076d22 100644 --- a/engine/runtime/system-node-version/src/index.ts +++ b/engine/runtime/system-version/src/index.ts @@ -1,4 +1,5 @@ import { detectIfCurrentPkgIsExecutable } from '@pnpm/cli.meta' +import type { RuntimeName } from '@pnpm/types' import * as execa from 'execa' import mem from 'memoize' @@ -14,7 +15,38 @@ export function getSystemNodeVersionNonCached (): string | undefined { return process.version } +export function getSystemDenoVersionNonCached (): string | undefined { + try { + // `deno --version` prints e.g. "deno 1.40.0 (release, ...)\nv8 ..." + const output = execa.sync('deno', ['--version']).stdout?.toString() ?? '' + const match = /^deno\s+(\d+\.\d+\.\d\S*)/m.exec(output) + return match?.[1] ? `v${match[1]}` : undefined + } catch { + return undefined + } +} + +export function getSystemBunVersionNonCached (): string | undefined { + try { + // `bun --version` prints just the bare version, e.g. "1.1.0". + const output = execa.sync('bun', ['--version']).stdout?.toString().trim() ?? '' + return /^\d+\.\d+\.\d+/.test(output) ? `v${output}` : undefined + } catch { + return undefined + } +} + export const getSystemNodeVersion = mem(getSystemNodeVersionNonCached) +export const getSystemDenoVersion = mem(getSystemDenoVersionNonCached) +export const getSystemBunVersion = mem(getSystemBunVersionNonCached) + +export function getSystemRuntimeVersion (name: RuntimeName): string | undefined { + switch (name) { + case 'node': return getSystemNodeVersion() + case 'deno': return getSystemDenoVersion() + case 'bun': return getSystemBunVersion() + } +} /** * The `;;node` string used as the side-effects @@ -27,8 +59,9 @@ export const getSystemNodeVersion = mem(getSystemNodeVersionNonCached) * * 1. `nodeVersion` argument when provided. Callers use this to thread * a project-pinned runtime (`engines.runtime` / `devEngines.runtime`) - * through to the hash — see {@link findRuntimeNodeVersion} for the - * helper that extracts the value from a lockfile. + * through to the hash — see `findRuntimeNodeVersion` / + * `readSnapshotRuntimePin` in `@pnpm/deps.path` for the helpers + * that extract the value from a lockfile or graph node. * 2. {@link getSystemNodeVersion} — the `node` on the user's `PATH`, * or `process.version` when not SEA-bundled. * 3. `process.version` as a last-resort fallback when the host has @@ -52,35 +85,3 @@ export function engineName (nodeVersion?: string): string { const major = stripped.split('.')[0] return `${process.platform};${process.arch};node${major}` } - -/** - * Scan an iterable of lockfile snapshot keys for the resolved - * `engines.runtime` / `devEngines.runtime` Node version and return - * its bare version string (e.g. `"22.11.0"`), or `undefined` when - * the project doesn't pin a runtime. - * - * Pnpm's runtime resolver writes the pinned Node into the lockfile as - * a snapshot with key `node@runtime:[()]` - * (see [`engine/runtime/node-resolver/src/index.ts`](https://github.com/pnpm/pnpm/blob/29a42efc3b/engine/runtime/node-resolver/src/index.ts)). - * The first such key found is treated as authoritative — workspaces - * with conflicting pins across importers are pathological and the - * resolver rejects them before they reach the lockfile. - * - * Callers typically pass `Object.keys(lockfile.packages ?? {})` — the - * in-memory `LockfileObject` merges the on-disk `packages:` and - * `snapshots:` sections under a single `packages` field, so its keys - * include every snapshot key the install will hash. - */ -export function findRuntimeNodeVersion (snapshotKeys: Iterable): string | undefined { - const prefix = 'node@runtime:' - for (const key of snapshotKeys) { - if (!key.startsWith(prefix)) continue - // Strip peer-context suffix `(...)` — `node@runtime:22.11.0(node@22.11.0)` - // resolves to the same Node version as `node@runtime:22.11.0`, - // so peer-stripped and peer-bearing keys yield the same answer. - const versionWithPeers = key.slice(prefix.length) - const parenAt = versionWithPeers.indexOf('(') - return parenAt === -1 ? versionWithPeers : versionWithPeers.slice(0, parenAt) - } - return undefined -} diff --git a/engine/runtime/system-node-version/test/getSystemNodeVersion.test.ts b/engine/runtime/system-version/test/getSystemNodeVersion.test.ts similarity index 50% rename from engine/runtime/system-node-version/test/getSystemNodeVersion.test.ts rename to engine/runtime/system-version/test/getSystemNodeVersion.test.ts index f6bf8e72ac..d5bc08c574 100644 --- a/engine/runtime/system-node-version/test/getSystemNodeVersion.test.ts +++ b/engine/runtime/system-version/test/getSystemNodeVersion.test.ts @@ -11,7 +11,13 @@ jest.unstable_mockModule('execa', () => ({ })), })) -const { getSystemNodeVersionNonCached, engineName, findRuntimeNodeVersion } = await import('../lib/index.js') +const { + getSystemNodeVersionNonCached, + getSystemDenoVersionNonCached, + getSystemBunVersionNonCached, + getSystemRuntimeVersion, + engineName, +} = await import('../lib/index.js') const execa = await import('execa') test('getSystemNodeVersion() executed from an executable pnpm CLI', () => { @@ -36,6 +42,54 @@ test('getSystemNodeVersion() returns undefined if execa.sync throws an error', ( expect(execa.sync).toHaveBeenCalledWith('node', ['--version']) }) +test('getSystemDenoVersion() parses the first line of `deno --version`', () => { + jest.mocked(execa.sync).mockReturnValueOnce({ + stdout: 'deno 1.40.0 (release, aarch64-apple-darwin)\nv8 12.1.285.27\ntypescript 5.3.3', + } as ReturnType) + expect(getSystemDenoVersionNonCached()).toBe('v1.40.0') + expect(execa.sync).toHaveBeenCalledWith('deno', ['--version']) +}) + +test('getSystemDenoVersion() returns undefined when deno is missing or output is unexpected', () => { + jest.mocked(execa.sync).mockImplementationOnce(() => { + throw new Error('not found: deno') + }) + expect(getSystemDenoVersionNonCached()).toBeUndefined() + + jest.mocked(execa.sync).mockReturnValueOnce({ stdout: 'unexpected output' } as ReturnType) + expect(getSystemDenoVersionNonCached()).toBeUndefined() +}) + +test('getSystemBunVersion() parses the bare version printed by `bun --version`', () => { + jest.mocked(execa.sync).mockReturnValueOnce({ stdout: '1.1.0\n' } as ReturnType) + expect(getSystemBunVersionNonCached()).toBe('v1.1.0') + expect(execa.sync).toHaveBeenCalledWith('bun', ['--version']) +}) + +test('getSystemBunVersion() returns undefined when bun is missing', () => { + jest.mocked(execa.sync).mockImplementationOnce(() => { + throw new Error('not found: bun') + }) + expect(getSystemBunVersionNonCached()).toBeUndefined() +}) + +test('getSystemRuntimeVersion() dispatches to the per-runtime helpers', () => { + isSea = false + expect(getSystemRuntimeVersion('node')).toBe(process.version) + + jest.mocked(execa.sync).mockReturnValueOnce({ + stdout: 'deno 9.9.9 (release)', + } as ReturnType) + expect(getSystemRuntimeVersion('deno')).toBe('v9.9.9') + expect(execa.sync).toHaveBeenLastCalledWith('deno', ['--version']) + + jest.mocked(execa.sync).mockReturnValueOnce({ + stdout: '9.9.9\n', + } as ReturnType) + expect(getSystemRuntimeVersion('bun')).toBe('v9.9.9') + expect(execa.sync).toHaveBeenLastCalledWith('bun', ['--version']) +}) + test('engineName() honours an explicit nodeVersion over the host probe', () => { // The pinned-runtime override path: when a project's // `engines.runtime` / `devEngines.runtime` resolves to a specific @@ -58,20 +112,3 @@ test('engineName() falls back to the host Node when no override is provided', () const major = process.version.replace(/^v/, '').split('.')[0] expect(engineName()).toBe(`${process.platform};${process.arch};node${major}`) }) - -test('findRuntimeNodeVersion() pulls the pinned major from a node@runtime: snapshot key', () => { - // Mirrors pacquet's `find_runtime_node_major` helper; both must - // agree on the version-extraction rule or the two tools would - // hash GVS slots under different engine majors for the same - // project. The peer-suffixed form must reduce to the same bare - // version as the form without a peer suffix. - expect( - findRuntimeNodeVersion(['leftpad@1.3.0', 'node@runtime:22.11.0']) - ).toBe('22.11.0') - expect( - findRuntimeNodeVersion(['node@runtime:22.11.0(node@22.11.0)']) - ).toBe('22.11.0') - expect( - findRuntimeNodeVersion(['leftpad@1.3.0', 'is-positive@3.1.0']) - ).toBeUndefined() -}) diff --git a/engine/runtime/system-node-version/test/tsconfig.json b/engine/runtime/system-version/test/tsconfig.json similarity index 100% rename from engine/runtime/system-node-version/test/tsconfig.json rename to engine/runtime/system-version/test/tsconfig.json diff --git a/engine/runtime/system-node-version/tsconfig.json b/engine/runtime/system-version/tsconfig.json similarity index 83% rename from engine/runtime/system-node-version/tsconfig.json rename to engine/runtime/system-version/tsconfig.json index 794facae00..ae10b8dac0 100644 --- a/engine/runtime/system-node-version/tsconfig.json +++ b/engine/runtime/system-version/tsconfig.json @@ -11,6 +11,9 @@ "references": [ { "path": "../../../cli/meta" + }, + { + "path": "../../../core/types" } ] } diff --git a/engine/runtime/system-node-version/tsconfig.lint.json b/engine/runtime/system-version/tsconfig.lint.json similarity index 100% rename from engine/runtime/system-node-version/tsconfig.lint.json rename to engine/runtime/system-version/tsconfig.lint.json diff --git a/exec/commands/CHANGELOG.md b/exec/commands/CHANGELOG.md index d66882b764..0c2695d937 100644 --- a/exec/commands/CHANGELOG.md +++ b/exec/commands/CHANGELOG.md @@ -1,5 +1,145 @@ # @pnpm/plugin-commands-script-runners +## 1100.1.13 + +### Patch Changes + +- Updated dependencies [a23956e] +- Updated dependencies [aa6149d] +- Updated dependencies [a456dc7] +- Updated dependencies [572842a] +- Updated dependencies [35d2355] +- Updated dependencies [a662de4] + - @pnpm/config.reader@1101.4.1 + - @pnpm/installing.commands@1100.6.0 + - @pnpm/workspace.project-manifest-reader@1100.0.9 + - @pnpm/types@1101.2.0 + - @pnpm/engine.runtime.commands@1100.1.0 + - @pnpm/deps.status@1100.0.19 + - @pnpm/workspace.injected-deps-syncer@1100.0.15 + - @pnpm/building.commands@1100.0.23 + - @pnpm/installing.client@1100.2.3 + - @pnpm/cli.utils@1101.0.8 + - @pnpm/bins.resolver@1100.0.5 + - @pnpm/config.version-policy@1100.1.2 + - @pnpm/core-loggers@1100.1.2 + - @pnpm/exec.lifecycle@1100.0.14 + - @pnpm/pkg-manifest.reader@1100.0.5 + - @pnpm/workspace.projects-sorter@1100.0.4 + - @pnpm/crypto.hash@1100.0.1 + +## 1100.1.12 + +### Patch Changes + +- 3d14385: Fixed `pnpm dlx` failing with `ERR_PNPM_NO_IMPORTER_MANIFEST_FOUND` when the installed package's CAS slot is missing its `package.json`. Observed in the wild for `pnpm dlx node@runtime:` when the GVS slot was populated without the synthesized manifest runtime archives need (they don't ship a `package.json` of their own, so the synthesized one is the only way it gets there; an existing slot from an earlier code path that skipped the synthesis stays incomplete). The bin link itself is wired up from the resolution and remains valid, so `dlx` now falls back to the scopeless package name when the slot's manifest is unreadable — for single-bin packages (the dlx common case, including every `runtime:` spec) this matches what `manifest.bin` would have named. Multi-bin packages already require `--package= ` to disambiguate and don't enter this code path. +- Updated dependencies [d7da112] +- Updated dependencies [3b62f9d] +- Updated dependencies [212315d] + - @pnpm/workspace.project-manifest-reader@1100.0.8 + - @pnpm/config.reader@1101.4.0 + - @pnpm/installing.commands@1100.5.0 + - @pnpm/cli.utils@1101.0.7 + - @pnpm/building.commands@1100.0.22 + - @pnpm/deps.status@1100.0.18 + - @pnpm/engine.runtime.commands@1100.0.17 + - @pnpm/installing.client@1100.2.2 + - @pnpm/exec.lifecycle@1100.0.13 + - @pnpm/workspace.injected-deps-syncer@1100.0.14 + +## 1100.1.11 + +### Patch Changes + +- Updated dependencies [881a865] + - @pnpm/installing.commands@1100.4.2 + - @pnpm/building.commands@1100.0.21 + +## 1100.1.10 + +### Patch Changes + +- @pnpm/installing.commands@1100.4.1 +- @pnpm/building.commands@1100.0.20 +- @pnpm/installing.client@1100.2.1 + +## 1100.1.9 + +### Patch Changes + +- Updated dependencies [3687b0e] +- Updated dependencies [ced20cb] +- Updated dependencies [a620557] +- Updated dependencies [9cb48bb] +- Updated dependencies [d1b340f] +- Updated dependencies [1627943] +- Updated dependencies [b206a15] +- Updated dependencies [64afc92] + - @pnpm/config.reader@1101.3.3 + - @pnpm/installing.commands@1100.4.0 + - @pnpm/exec.lifecycle@1100.0.12 + - @pnpm/installing.client@1100.2.0 + - @pnpm/types@1101.1.1 + - @pnpm/building.commands@1100.0.19 + - @pnpm/deps.status@1100.0.17 + - @pnpm/engine.runtime.commands@1100.0.16 + - @pnpm/cli.utils@1101.0.6 + - @pnpm/workspace.project-manifest-reader@1100.0.7 + - @pnpm/bins.resolver@1100.0.4 + - @pnpm/config.version-policy@1100.1.1 + - @pnpm/core-loggers@1100.1.1 + - @pnpm/pkg-manifest.reader@1100.0.4 + - @pnpm/workspace.injected-deps-syncer@1100.0.13 + - @pnpm/workspace.projects-sorter@1100.0.3 + - @pnpm/crypto.hash@1100.0.1 + +## 1100.1.8 + +### Patch Changes + +- 4195766: Tightened the `minimumReleaseAge` story so the bypass becomes explicit on disk instead of silent, and removed the discover-by-loop dance for strict-mode users: + + 1. Fresh resolutions in loose mode (`minimumReleaseAgeStrict: false`) that fall back to a version newer than the cutoff auto-collect the picked `name@version` into the workspace manifest's `minimumReleaseAgeExclude`. A single info message lists the additions; entries already on the list are left alone. + 2. The post-resolution lockfile verifier introduced in #11583 now runs in loose mode too — every accepted-immature pin must be on `minimumReleaseAgeExclude`, just like strict mode requires. A lockfile produced under a weaker (or absent) policy that still has immature entries is rejected the same way strict mode would reject it. + 3. **Strict mode (interactive)** no longer aborts on the first immature pick. The resolver gathers every immature direct _and_ transitive in one pass; before peer-dependency resolution runs, pnpm prompts the user with the full list and asks whether to add them all to `minimumReleaseAgeExclude` and proceed. Approve → install continues and the workspace manifest is written at the end. Decline → resolution aborts before the lockfile or package.json is touched (tarballs already in the store stay, since the store is idempotent). This closes the [#10488](https://github.com/pnpm/pnpm/issues/10488) loop where security bumps to packages with platform-specific transitives (e.g. `next` + the `@next/swc-*` shims) made users re-run `pnpm add` once per transitive. + 4. **Strict mode (non-interactive / CI)** now aborts with the full immature set in the error message instead of the first pick. The resolver always collects every immature direct + transitive; the install command then throws `ERR_PNPM_NO_MATURE_MATCHING_VERSION` listing each entry's `name@version` and publish time. Deterministic CI behavior is preserved (same exit code, same error code), but the error pinpoints every offending entry instead of forcing the discover-by-loop dance. The expected workflow is interactive approval locally → the lockfile + workspace manifest get committed → CI runs cleanly against the populated exclude list. + + 5. **The lockfile verifier now also covers `trustPolicy: 'no-downgrade'`.** The same post-resolution gate that re-checks `minimumReleaseAge` on lockfile entries now re-runs `failIfTrustDowngraded` for every npm-registry entry whose name isn't on `trustPolicyExclude`. The two checks share a single full-metadata fetch per package, so the extra coverage doesn't cost an extra round trip when both policies are active. Resolver-time trust checks still run as before — this just closes the gap when an entry bypasses resolution (peek path, `--frozen-lockfile`, restored CI cache). + + Pacquet parity: not ported — pacquet's `minimumReleaseAge` policy is itself only stubbed today (see `pacquet/crates/package-manager/src/version_policy.rs`). The auto-exclude, loose-mode verifier, prompt, and the new trust-policy verifier check will travel with the broader policy port whenever that happens. + +- b6e2c8c: Make `pnpm self-update` respect `minimumReleaseAge` (and `minimumReleaseAgeExclude`) when resolving which pnpm version to install. + + When the `latest` dist-tag points to a version newer than the configured age threshold, `self-update` now selects the newest mature version instead unless excluded by `minimumReleaseAgeExclude`. + + Also makes `dlx` and `outdated` surface invalid `minimumReleaseAgeExclude` patterns under the same `ERR_PNPM_INVALID_MINIMUM_RELEASE_AGE_EXCLUDE` error code already used by `install`, instead of leaking the internal `ERR_PNPM_INVALID_VERSION_UNION` / `ERR_PNPM_NAME_PATTERN_IN_VERSION_UNION` codes. + +- 247d70b: Honor `--silent` when `verifyDepsBeforeRun: install` auto-installs dependencies before `pnpm run` or `pnpm exec`, preventing install output from being written to stdout [#11636](https://github.com/pnpm/pnpm/issues/11636). +- Updated dependencies [4195766] +- Updated dependencies [31538bf] +- Updated dependencies [020ac45] +- Updated dependencies [b6e2c8c] +- Updated dependencies [d3f8408] +- Updated dependencies [247d70b] +- Updated dependencies [4a79336] +- Updated dependencies [a62f959] +- Updated dependencies [ba2c884] +- Updated dependencies [8df408c] + - @pnpm/installing.client@1100.1.0 + - @pnpm/installing.commands@1100.3.0 + - @pnpm/config.reader@1101.3.2 + - @pnpm/config.version-policy@1100.1.0 + - @pnpm/exec.pnpm-cli-runner@1100.0.1 + - @pnpm/core-loggers@1100.1.0 + - @pnpm/deps.status@1100.0.16 + - @pnpm/exec.lifecycle@1100.0.11 + - @pnpm/building.commands@1100.0.18 + - @pnpm/engine.runtime.commands@1100.0.15 + - @pnpm/workspace.injected-deps-syncer@1100.0.12 + - @pnpm/cli.utils@1101.0.5 + - @pnpm/workspace.project-manifest-reader@1100.0.6 + - @pnpm/crypto.hash@1100.0.1 + ## 1100.1.7 ### Patch Changes diff --git a/exec/commands/package.json b/exec/commands/package.json index 7e72d75c99..cc971a952c 100644 --- a/exec/commands/package.json +++ b/exec/commands/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/exec.commands", - "version": "1100.1.7", + "version": "1100.1.13", "description": "Commands for running scripts", "keywords": [ "pnpm", @@ -77,7 +77,7 @@ }, "devDependencies": { "@jest/globals": "catalog:", - "@pnpm/engine.runtime.system-node-version": "workspace:*", + "@pnpm/engine.runtime.system-version": "workspace:*", "@pnpm/exec.commands": "workspace:*", "@pnpm/logger": "workspace:*", "@pnpm/prepare": "workspace:*", diff --git a/exec/commands/src/dlx.ts b/exec/commands/src/dlx.ts index 5fac706b52..481755deec 100644 --- a/exec/commands/src/dlx.ts +++ b/exec/commands/src/dlx.ts @@ -15,7 +15,7 @@ import { type Config, types } from '@pnpm/config.reader' import { getPublishedByPolicy } from '@pnpm/config.version-policy' import { createHexHash } from '@pnpm/crypto.hash' import { PnpmError } from '@pnpm/error' -import { createResolver } from '@pnpm/installing.client' +import { createResolver, makeResolutionStrict } from '@pnpm/installing.client' import { add } from '@pnpm/installing.commands' import { readPackageJsonFromDir } from '@pnpm/pkg-manifest.reader' import { parseWantedDependency } from '@pnpm/resolving.parse-wanted-dependency' @@ -113,12 +113,11 @@ export async function handler ( ) && !opts.registrySupportsTimeField ) const catalogResolver = resolveFromCatalog.bind(null, opts.catalogs ?? {}) - const { resolve } = createResolver({ + const { resolve: baseResolve } = createResolver({ ...opts, configByUri: opts.configByUri, fullMetadata, filterMetadata: fullMetadata, - strictPublishedByCheck: Boolean(opts.minimumReleaseAge) && opts.minimumReleaseAgeStrict === true, ignoreMissingTimeField: opts.minimumReleaseAgeIgnoreMissingTime, retry: { factor: opts.fetchRetryFactor, @@ -128,6 +127,17 @@ export async function handler ( }, timeout: opts.fetchTimeout, }) + // dlx has nowhere to "defer to" — it runs the resolved package directly. + // Wrap the resolver under any policy that wants to reject violations + // up-front: strict minimumReleaseAge (refuse immature picks) and + // `trustPolicy: 'no-downgrade'` (refuse versions whose trust evidence + // weakened). Without the trust-policy arm, a downgraded version would + // resolve to a `policyViolation` that dlx silently ignored and then + // executed. + const strictResolution = + (Boolean(opts.minimumReleaseAge) && opts.minimumReleaseAgeStrict === true) || + opts.trustPolicy === 'no-downgrade' + const resolve = strictResolution ? makeResolutionStrict(baseResolve) : baseResolve const resolvedPkgAliases: string[] = [] const { publishedBy, publishedByExclude } = getPublishedByPolicy(opts) const resolvedPkgs = await Promise.all(pkgs.map(async (pkg) => { @@ -246,7 +256,26 @@ async function getPkgName (pkgDir: string): Promise { async function getBinName (cachedDir: string, opts: Pick): Promise { const pkgName = await getPkgName(cachedDir) const pkgDir = path.join(cachedDir, 'node_modules', pkgName) - const manifest = await readProjectManifestOnly(pkgDir, opts) as PackageManifest + let manifest: PackageManifest + try { + manifest = await readProjectManifestOnly(pkgDir, opts) as PackageManifest + } catch (err: unknown) { + // The installed package's `package.json` is unreadable. Observed in the + // wild for `node@runtime:` whose CAS slot was materialized by + // a code path that didn't run pnpm's `appendManifest` (or pacquet's + // equivalent runtime-manifest synthesis), leaving the slot without + // the `package.json` runtime archives don't ship themselves. Fall back + // to the scopeless package name — for single-bin packages (the dlx + // common case) it matches what `manifest.bin` would have named, and + // the `node_modules/.bin/` symlink the install already wired up + // from the resolution's bin info is what `execa` resolves against. + // Multi-bin packages require `--package= ` to disambiguate, + // which short-circuits `getBinName` upstream and never enters this path. + if (util.types.isNativeError(err) && 'code' in err && err.code === 'ERR_PNPM_NO_IMPORTER_MANIFEST_FOUND') { + return scopeless(pkgName) + } + throw err + } const bins = await getBinsFromPackageManifest(manifest, pkgDir) if (bins.length === 0) { throw new PnpmError('DLX_NO_BIN', `No binaries found in ${pkgName}`) diff --git a/exec/commands/src/exec.ts b/exec/commands/src/exec.ts index 330f0ad89e..b11736395d 100644 --- a/exec/commands/src/exec.ts +++ b/exec/commands/src/exec.ts @@ -156,6 +156,7 @@ export type ExecOpts = Required> & | 'nodeOptions' | 'pnpmHomeDir' | 'recursive' +| 'reporter' | 'reporterHidePrefix' | 'userAgent' | 'verifyDepsBeforeRun' diff --git a/exec/commands/src/runDepsStatusCheck.ts b/exec/commands/src/runDepsStatusCheck.ts index 900fcc7189..136db411ba 100644 --- a/exec/commands/src/runDepsStatusCheck.ts +++ b/exec/commands/src/runDepsStatusCheck.ts @@ -1,4 +1,4 @@ -import type { VerifyDepsBeforeRun } from '@pnpm/config.reader' +import type { Config, VerifyDepsBeforeRun } from '@pnpm/config.reader' import { checkDepsStatus, type CheckDepsStatusOptions, type WorkspaceStateSettings } from '@pnpm/deps.status' import { PnpmError } from '@pnpm/error' import { runPnpmCli } from '@pnpm/exec.pnpm-cli-runner' @@ -7,6 +7,7 @@ import enquirer from 'enquirer' export interface RunDepsStatusCheckOptions extends CheckDepsStatusOptions { dir: string + reporter?: Config['reporter'] verifyDepsBeforeRun?: VerifyDepsBeforeRun } @@ -20,7 +21,7 @@ export async function runDepsStatusCheck (opts: RunDepsStatusCheckOptions): Prom if (upToDate) return const command = ['install', ...createInstallArgs(workspaceState?.settings)] - const install = runPnpmCli.bind(null, command, { cwd: opts.dir }) + const install = runPnpmCli.bind(null, command, { cwd: opts.dir, reporter: opts.reporter }) switch (opts.verifyDepsBeforeRun) { case 'install': diff --git a/exec/commands/test/dlx.e2e.ts b/exec/commands/test/dlx.e2e.ts index 39289a3109..6dd8a044b1 100644 --- a/exec/commands/test/dlx.e2e.ts +++ b/exec/commands/test/dlx.e2e.ts @@ -9,17 +9,15 @@ import { DLX_DEFAULT_OPTS as DEFAULT_OPTS } from './utils/index.js' const { getSystemNodeVersion: originalGetSystemNodeVersion, engineName: originalEngineName, - findRuntimeNodeVersion: originalFindRuntimeNodeVersion, -} = await import('@pnpm/engine.runtime.system-node-version') +} = await import('@pnpm/engine.runtime.system-version') // Re-export every public symbol the package surfaces so downstream // dynamic imports (e.g. `@pnpm/deps.graph-hasher`'s use of // `engineName` for the GVS hash) keep working under the mock. Only // `getSystemNodeVersion` is wrapped with `jest.fn` for spy-ability; -// the other two delegate straight back to the originals. -jest.unstable_mockModule('@pnpm/engine.runtime.system-node-version', () => ({ +// `engineName` delegates straight back to the original. +jest.unstable_mockModule('@pnpm/engine.runtime.system-version', () => ({ getSystemNodeVersion: jest.fn(originalGetSystemNodeVersion), engineName: originalEngineName, - findRuntimeNodeVersion: originalFindRuntimeNodeVersion, })) const installingCommands = await import('@pnpm/installing.commands') const { add: originalAdd } = installingCommands @@ -31,7 +29,7 @@ jest.unstable_mockModule('@pnpm/installing.commands', () => ({ }, })) -const systemNodeVersion = await import('@pnpm/engine.runtime.system-node-version') +const systemNodeVersion = await import('@pnpm/engine.runtime.system-version') const { add } = await import('@pnpm/installing.commands') const { dlx } = await import('@pnpm/exec.commands') const { approveBuilds } = await import('@pnpm/building.commands') @@ -484,7 +482,7 @@ test('dlx should fail when the requested package does not meet the minimum age r default: 'https://registry.npmjs.org/', }, }, ['shx@0.3.4']) - ).rejects.toThrow(/Version 0\.3\.4 \(released .+\) of shx does not meet the minimumReleaseAge constraint/) + ).rejects.toThrow(/shx@0\.3\.4 was published.+minimumReleaseAge cutoff/) }) test('dlx should respect minimumReleaseAgeExclude', async () => { diff --git a/exec/commands/tsconfig.json b/exec/commands/tsconfig.json index cfd305a983..4cf7c3da27 100644 --- a/exec/commands/tsconfig.json +++ b/exec/commands/tsconfig.json @@ -61,7 +61,7 @@ "path": "../../engine/runtime/commands" }, { - "path": "../../engine/runtime/system-node-version" + "path": "../../engine/runtime/system-version" }, { "path": "../../installing/client" diff --git a/exec/lifecycle/CHANGELOG.md b/exec/lifecycle/CHANGELOG.md index fc10b96db9..58eae8b283 100644 --- a/exec/lifecycle/CHANGELOG.md +++ b/exec/lifecycle/CHANGELOG.md @@ -1,5 +1,54 @@ # @pnpm/lifecycle +## 1100.0.14 + +### Patch Changes + +- Updated dependencies [35d2355] + - @pnpm/types@1101.2.0 + - @pnpm/bins.linker@1100.0.10 + - @pnpm/fetching.directory-fetcher@1100.0.13 + - @pnpm/core-loggers@1100.1.2 + - @pnpm/pkg-manifest.reader@1100.0.5 + - @pnpm/store.cafs-types@1100.0.1 + - @pnpm/store.controller-types@1100.1.2 + +## 1100.0.13 + +### Patch Changes + +- @pnpm/bins.linker@1100.0.9 +- @pnpm/fetching.directory-fetcher@1100.0.12 + +## 1100.0.12 + +### Patch Changes + +- 9cb48bb: Fix two crashes with `injectWorkspacePackages: true` when the lockfile has been pruned (e.g. by `turbo prune --docker`): + + - `Cannot use 'in' operator to search for 'directory' in undefined`: a peer-dependency-variant injected snapshot inherits its `resolution` from the base `packages:` entry; when a pruner drops that base entry the readers crash. `convertToLockfileObject` now reconstructs the directory resolution from the `file:` depPath at load time — a single normalization point, so every reader sees a fully-formed snapshot. + - `ERR_PNPM_ENOENT` on `node_modules/.bin/`: after `prepare`/`postinstall`, `runLifecycleHooksConcurrently` re-imported each injected workspace package; the `scanDir`-into-`filesMap` workaround fed target-internal paths to the importer, which the `makeEmptyDir` fast path (#11088) then wiped. Drop the workaround and pass `keepModulesDir: true` so the importer preserves the target's existing `node_modules` (bin links + transitive deps) and source files keep their hardlinks. + +- Updated dependencies [64afc92] + - @pnpm/types@1101.1.1 + - @pnpm/fetching.directory-fetcher@1100.0.11 + - @pnpm/store.controller-types@1100.1.1 + - @pnpm/bins.linker@1100.0.8 + - @pnpm/core-loggers@1100.1.1 + - @pnpm/pkg-manifest.reader@1100.0.4 + - @pnpm/store.cafs-types@1100.0.1 + +## 1100.0.11 + +### Patch Changes + +- Updated dependencies [4195766] +- Updated dependencies [4a79336] + - @pnpm/store.controller-types@1100.1.0 + - @pnpm/core-loggers@1100.1.0 + - @pnpm/fetching.directory-fetcher@1100.0.10 + - @pnpm/bins.linker@1100.0.7 + ## 1100.0.10 ### Patch Changes diff --git a/exec/lifecycle/package.json b/exec/lifecycle/package.json index 073b46707a..2f9876b02e 100644 --- a/exec/lifecycle/package.json +++ b/exec/lifecycle/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/exec.lifecycle", - "version": "1100.0.10", + "version": "1100.0.14", "description": "Package lifecycle hook runner", "keywords": [ "pnpm", diff --git a/exec/lifecycle/src/runLifecycleHooksConcurrently.ts b/exec/lifecycle/src/runLifecycleHooksConcurrently.ts index 7ab30ec083..1d58871cd2 100644 --- a/exec/lifecycle/src/runLifecycleHooksConcurrently.ts +++ b/exec/lifecycle/src/runLifecycleHooksConcurrently.ts @@ -1,10 +1,8 @@ -import fs from 'node:fs' import path from 'node:path' import { linkBins } from '@pnpm/bins.linker' import { fetchFromDir } from '@pnpm/fetching.directory-fetcher' import { logger } from '@pnpm/logger' -import type { FilesMap } from '@pnpm/store.cafs-types' import type { StoreController } from '@pnpm/store.controller-types' import type { ProjectManifest, ProjectRootDir } from '@pnpm/types' import { runGroups } from 'run-groups' @@ -73,45 +71,30 @@ export async function runLifecycleHooksConcurrently ( } } if (targetDirs == null || targetDirs.length === 0 || !isBuilt) return + // Re-import only the freshly-built source — fetchFromDir already + // excludes the source's node_modules/. `keepModulesDir: true` makes + // importIndexedDir skip the destructive makeEmptyDir fast path + // (#11088) and preserve the target's existing node_modules (bin + // symlinks + transitive deps from the initial install) via its + // staging/move path. Replaces the old scanDir-into-filesMap + // workaround (#4299) that the fast path then wiped, causing ENOENT + // on .bin/. Stays on storeController.importPackage so source + // files keep their hardlinks (no copy-loop). const filesResponse = await fetchFromDir(rootDir, { resolveSymlinks: opts.resolveSymlinksInInjectedDirs }) await Promise.all( - targetDirs.map(async (targetDir) => { - const targetModulesDir = path.join(targetDir, 'node_modules') - const newFilesMap: FilesMap = new Map(filesResponse.filesMap) - if (fs.existsSync(targetModulesDir)) { - // If the target directory contains a node_modules directory - // (it may happen when the hoisted node linker is used) - // then we need to preserve this node_modules. - // So we scan this node_modules directory and pass it as part of the new package. - await scanDir('node_modules', targetModulesDir, targetModulesDir, newFilesMap) - } - return opts.storeController.importPackage(targetDir, { + targetDirs.map(async (targetDir) => + opts.storeController.importPackage(targetDir, { filesResponse: { resolvedFrom: 'local-dir', ...filesResponse, - filesMap: newFilesMap, }, force: false, + keepModulesDir: true, }) - }) + ) ) } ) }) await runGroups(childConcurrency, groups) } - -async function scanDir (prefix: string, rootDir: string, currentDir: string, index: FilesMap): Promise { - const files = await fs.promises.readdir(currentDir) - await Promise.all(files.map(async (file) => { - const fullPath = path.join(currentDir, file) - const stat = await fs.promises.stat(fullPath) - if (stat.isDirectory()) { - return scanDir(prefix, rootDir, fullPath, index) - } - if (stat.isFile()) { - const relativePath = path.relative(rootDir, fullPath) - index.set(path.join(prefix, relativePath), fullPath) - } - })) -} diff --git a/exec/pnpm-cli-runner/CHANGELOG.md b/exec/pnpm-cli-runner/CHANGELOG.md index b7718c90a9..5eedfd5b14 100644 --- a/exec/pnpm-cli-runner/CHANGELOG.md +++ b/exec/pnpm-cli-runner/CHANGELOG.md @@ -1,5 +1,11 @@ # @pnpm/exec.pnpm-cli-runner +## 1100.0.1 + +### Patch Changes + +- 247d70b: Honor `--silent` when `verifyDepsBeforeRun: install` auto-installs dependencies before `pnpm run` or `pnpm exec`, preventing install output from being written to stdout [#11636](https://github.com/pnpm/pnpm/issues/11636). + ## 1001.0.0 ### Major Changes diff --git a/exec/pnpm-cli-runner/package.json b/exec/pnpm-cli-runner/package.json index d4d57052fd..b3500c842d 100644 --- a/exec/pnpm-cli-runner/package.json +++ b/exec/pnpm-cli-runner/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/exec.pnpm-cli-runner", - "version": "1100.0.0", + "version": "1100.0.1", "description": "Runs pnpm CLI", "keywords": [ "pnpm", diff --git a/exec/pnpm-cli-runner/src/index.ts b/exec/pnpm-cli-runner/src/index.ts index 9161af10bd..9df02756f4 100644 --- a/exec/pnpm-cli-runner/src/index.ts +++ b/exec/pnpm-cli-runner/src/index.ts @@ -2,17 +2,23 @@ import path from 'node:path' import { sync as execSync } from 'execa' -export function runPnpmCli (command: string[], { cwd }: { cwd: string }): void { +export interface RunPnpmCliOptions { + cwd: string + reporter?: string +} + +export function runPnpmCli (command: string[], { cwd, reporter }: RunPnpmCliOptions): void { const execOpts = { cwd, stdio: 'inherit' as const, } + const cliCommand = reporter ? [...command, `--reporter=${reporter}`] : command const execFileName = path.basename(process.execPath).toLowerCase() if (execFileName === 'pnpm' || execFileName === 'pnpm.exe') { - execSync(process.execPath, command, execOpts) + execSync(process.execPath, cliCommand, execOpts) } else if (path.basename(process.argv[1]) === 'pnpm.mjs') { - execSync(process.execPath, [process.argv[1], ...command], execOpts) + execSync(process.execPath, [process.argv[1], ...cliCommand], execOpts) } else { - execSync('pnpm', command, execOpts) + execSync('pnpm', cliCommand, execOpts) } } diff --git a/exec/prepare-package/CHANGELOG.md b/exec/prepare-package/CHANGELOG.md index ec8d0c33f9..5f4306ff8a 100644 --- a/exec/prepare-package/CHANGELOG.md +++ b/exec/prepare-package/CHANGELOG.md @@ -1,5 +1,36 @@ # @pnpm/prepare-package +## 1100.0.14 + +### Patch Changes + +- Updated dependencies [35d2355] + - @pnpm/types@1101.2.0 + - @pnpm/exec.lifecycle@1100.0.14 + - @pnpm/pkg-manifest.reader@1100.0.5 + +## 1100.0.13 + +### Patch Changes + +- @pnpm/exec.lifecycle@1100.0.13 + +## 1100.0.12 + +### Patch Changes + +- Updated dependencies [9cb48bb] +- Updated dependencies [64afc92] + - @pnpm/exec.lifecycle@1100.0.12 + - @pnpm/types@1101.1.1 + - @pnpm/pkg-manifest.reader@1100.0.4 + +## 1100.0.11 + +### Patch Changes + +- @pnpm/exec.lifecycle@1100.0.11 + ## 1100.0.10 ### Patch Changes diff --git a/exec/prepare-package/package.json b/exec/prepare-package/package.json index 7751530b85..c11eb1ad9a 100644 --- a/exec/prepare-package/package.json +++ b/exec/prepare-package/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/exec.prepare-package", - "version": "1100.0.10", + "version": "1100.0.14", "description": "Prepares a Git-hosted package", "keywords": [ "pnpm", diff --git a/fetching/binary-fetcher/CHANGELOG.md b/fetching/binary-fetcher/CHANGELOG.md index e9e7e0f9af..ae027c75f7 100644 --- a/fetching/binary-fetcher/CHANGELOG.md +++ b/fetching/binary-fetcher/CHANGELOG.md @@ -1,5 +1,27 @@ # @pnpm/fetching.binary-fetcher +## 1101.0.8 + +### Patch Changes + +- Updated dependencies [aa6149d] + - @pnpm/worker@1100.1.8 + - @pnpm/fetching.fetcher-base@1100.1.6 + +## 1101.0.7 + +### Patch Changes + +- @pnpm/fetching.fetcher-base@1100.1.5 +- @pnpm/worker@1100.1.7 + +## 1101.0.6 + +### Patch Changes + +- @pnpm/fetching.fetcher-base@1100.1.4 +- @pnpm/worker@1100.1.6 + ## 1101.0.5 ### Patch Changes diff --git a/fetching/binary-fetcher/package.json b/fetching/binary-fetcher/package.json index 1b382992cc..0b8b81cf66 100644 --- a/fetching/binary-fetcher/package.json +++ b/fetching/binary-fetcher/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/fetching.binary-fetcher", - "version": "1101.0.5", + "version": "1101.0.8", "description": "A fetcher for binary archives", "keywords": [ "pnpm", diff --git a/fetching/directory-fetcher/CHANGELOG.md b/fetching/directory-fetcher/CHANGELOG.md index f28b3b07aa..4d3f155d24 100644 --- a/fetching/directory-fetcher/CHANGELOG.md +++ b/fetching/directory-fetcher/CHANGELOG.md @@ -1,5 +1,48 @@ # @pnpm/directory-fetcher +## 1100.0.13 + +### Patch Changes + +- Updated dependencies [a456dc7] +- Updated dependencies [35d2355] + - @pnpm/workspace.project-manifest-reader@1100.0.9 + - @pnpm/types@1101.2.0 + - @pnpm/building.pkg-requires-build@1100.0.5 + - @pnpm/fetching.fetcher-base@1100.1.6 + - @pnpm/resolving.resolver-base@1100.3.1 + - @pnpm/store.cafs-types@1100.0.1 + +## 1100.0.12 + +### Patch Changes + +- Updated dependencies [d7da112] + - @pnpm/workspace.project-manifest-reader@1100.0.8 + +## 1100.0.11 + +### Patch Changes + +- Updated dependencies [1627943] +- Updated dependencies [64afc92] + - @pnpm/resolving.resolver-base@1100.3.0 + - @pnpm/types@1101.1.1 + - @pnpm/fetching.fetcher-base@1100.1.5 + - @pnpm/workspace.project-manifest-reader@1100.0.7 + - @pnpm/building.pkg-requires-build@1100.0.4 + - @pnpm/store.cafs-types@1100.0.1 + +## 1100.0.10 + +### Patch Changes + +- Updated dependencies [4195766] +- Updated dependencies [31538bf] + - @pnpm/resolving.resolver-base@1100.2.0 + - @pnpm/fetching.fetcher-base@1100.1.4 + - @pnpm/workspace.project-manifest-reader@1100.0.6 + ## 1100.0.9 ### Patch Changes diff --git a/fetching/directory-fetcher/package.json b/fetching/directory-fetcher/package.json index 7c6a824b28..8cf3c86369 100644 --- a/fetching/directory-fetcher/package.json +++ b/fetching/directory-fetcher/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/fetching.directory-fetcher", - "version": "1100.0.9", + "version": "1100.0.13", "description": "A fetcher for local directory packages", "keywords": [ "pnpm", diff --git a/fetching/fetcher-base/CHANGELOG.md b/fetching/fetcher-base/CHANGELOG.md index 48a14d420e..ad15107226 100644 --- a/fetching/fetcher-base/CHANGELOG.md +++ b/fetching/fetcher-base/CHANGELOG.md @@ -1,5 +1,30 @@ # @pnpm/fetcher-base +## 1100.1.6 + +### Patch Changes + +- Updated dependencies [35d2355] + - @pnpm/types@1101.2.0 + - @pnpm/resolving.resolver-base@1100.3.1 + +## 1100.1.5 + +### Patch Changes + +- Updated dependencies [1627943] +- Updated dependencies [64afc92] + - @pnpm/resolving.resolver-base@1100.3.0 + - @pnpm/types@1101.1.1 + +## 1100.1.4 + +### Patch Changes + +- Updated dependencies [4195766] +- Updated dependencies [31538bf] + - @pnpm/resolving.resolver-base@1100.2.0 + ## 1100.1.3 ### Patch Changes diff --git a/fetching/fetcher-base/package.json b/fetching/fetcher-base/package.json index 354fb8f395..eaf3294a6d 100644 --- a/fetching/fetcher-base/package.json +++ b/fetching/fetcher-base/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/fetching.fetcher-base", - "version": "1100.1.3", + "version": "1100.1.6", "description": "Types for pnpm-compatible fetchers", "keywords": [ "pnpm", diff --git a/fetching/git-fetcher/CHANGELOG.md b/fetching/git-fetcher/CHANGELOG.md index 16e9feb106..44e2ca3fba 100644 --- a/fetching/git-fetcher/CHANGELOG.md +++ b/fetching/git-fetcher/CHANGELOG.md @@ -1,5 +1,37 @@ # @pnpm/git-fetcher +## 1101.0.10 + +### Patch Changes + +- 90d1ce6: Reject git resolutions whose `commit` field is not a 40-character hexadecimal SHA before invoking `git`. A malicious lockfile could otherwise smuggle a value such as `--upload-pack=` through `git fetch` / `git checkout`, which on SSH or local-file transports executes the supplied command. +- Updated dependencies [aa6149d] + - @pnpm/worker@1100.1.8 + - @pnpm/exec.prepare-package@1100.0.14 + - @pnpm/fetching.fetcher-base@1100.1.6 + +## 1101.0.9 + +### Patch Changes + +- @pnpm/exec.prepare-package@1100.0.13 + +## 1101.0.8 + +### Patch Changes + +- @pnpm/exec.prepare-package@1100.0.12 +- @pnpm/fetching.fetcher-base@1100.1.5 +- @pnpm/worker@1100.1.7 + +## 1101.0.7 + +### Patch Changes + +- @pnpm/fetching.fetcher-base@1100.1.4 +- @pnpm/exec.prepare-package@1100.0.11 +- @pnpm/worker@1100.1.6 + ## 1101.0.6 ### Patch Changes diff --git a/fetching/git-fetcher/package.json b/fetching/git-fetcher/package.json index 655b10f1e3..31339078e3 100644 --- a/fetching/git-fetcher/package.json +++ b/fetching/git-fetcher/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/fetching.git-fetcher", - "version": "1101.0.6", + "version": "1101.0.10", "description": "A fetcher for git-hosted packages", "keywords": [ "pnpm", diff --git a/fetching/git-fetcher/src/index.ts b/fetching/git-fetcher/src/index.ts index 2a402e07bf..1d35259886 100644 --- a/fetching/git-fetcher/src/index.ts +++ b/fetching/git-fetcher/src/index.ts @@ -26,6 +26,9 @@ export function createGitFetcher (createOpts: CreateGitFetcherOptions): { git: G const ignoreScripts = createOpts.ignoreScripts ?? false const gitFetcher: GitFetcher = async (cafs, resolution, opts) => { + if (!isValidCommitHash(resolution.commit)) { + throw new PnpmError('INVALID_GIT_COMMIT', `Invalid git commit hash "${resolution.commit}" for repository "${resolution.repo}". Expected a 40-character hexadecimal SHA.`) + } const tempLocation = await cafs.tempDir() if (allowedHosts.size > 0 && shouldUseShallow(resolution.repo, allowedHosts)) { await execGit(['init'], { cwd: tempLocation }) @@ -78,6 +81,10 @@ export function createGitFetcher (createOpts: CreateGitFetcherOptions): { git: G } } +function isValidCommitHash (commit: string): boolean { + return /^[0-9a-f]{40}$/i.test(commit) +} + function shouldUseShallow (repoUrl: string, allowedHosts: Set): boolean { try { const { host } = new URL(repoUrl) diff --git a/fetching/git-fetcher/test/index.ts b/fetching/git-fetcher/test/index.ts index 1abd567538..fdc11afe28 100644 --- a/fetching/git-fetcher/test/index.ts +++ b/fetching/git-fetcher/test/index.ts @@ -227,7 +227,7 @@ test('fail when preparing a git-hosted package', async () => { ).rejects.toThrow('Failed to prepare git-hosted package fetched from "https://github.com/pnpm-e2e/prepare-script-fails.git": @pnpm.e2e/prepare-script-fails@1.0.0 npm-install: `npm install`') }) -test('fail when preparing a git-hosted package with a partial commit', async () => { +test('reject a partial commit before invoking git', async () => { const storeDir = temporaryDirectory() const fetch = createGitFetcher({ storeIndex: createStoreIndex(storeDir), @@ -241,7 +241,24 @@ test('fail when preparing a git-hosted package with a partial commit', async () }, { filesIndexFile: path.join(storeDir, 'index.json'), }) - ).rejects.toThrow(/received commit [0-9a-f]{40} does not match expected value deadbeef/) + ).rejects.toThrow('Invalid git commit hash "deadbeef"') + expect(jest.mocked(execa)).not.toHaveBeenCalled() +}) + +test('reject a commit value that looks like a git option', async () => { + const storeDir = temporaryDirectory() + const fetch = createGitFetcher({ storeIndex: createStoreIndex(storeDir) }).git + await expect( + fetch(createCafsStore(storeDir), + { + commit: '--upload-pack=touch /tmp/pwned', + repo: 'file:///tmp/repo.git', + type: 'git', + }, { + filesIndexFile: path.join(storeDir, 'index.json'), + }) + ).rejects.toThrow('Invalid git commit hash "--upload-pack=touch /tmp/pwned"') + expect(jest.mocked(execa)).not.toHaveBeenCalled() }) test('do not build the package when scripts are ignored', async () => { diff --git a/fetching/pick-fetcher/CHANGELOG.md b/fetching/pick-fetcher/CHANGELOG.md index 922f4f454a..1698978c35 100644 --- a/fetching/pick-fetcher/CHANGELOG.md +++ b/fetching/pick-fetcher/CHANGELOG.md @@ -1,5 +1,34 @@ # @pnpm/pick-fetcher +## 1100.0.9 + +### Patch Changes + +- @pnpm/fetching.fetcher-base@1100.1.6 +- @pnpm/hooks.types@1100.0.9 +- @pnpm/resolving.resolver-base@1100.3.1 +- @pnpm/store.cafs-types@1100.0.1 + +## 1100.0.8 + +### Patch Changes + +- Updated dependencies [1627943] + - @pnpm/resolving.resolver-base@1100.3.0 + - @pnpm/fetching.fetcher-base@1100.1.5 + - @pnpm/hooks.types@1100.0.8 + - @pnpm/store.cafs-types@1100.0.1 + +## 1100.0.7 + +### Patch Changes + +- Updated dependencies [4195766] +- Updated dependencies [31538bf] + - @pnpm/resolving.resolver-base@1100.2.0 + - @pnpm/fetching.fetcher-base@1100.1.4 + - @pnpm/hooks.types@1100.0.7 + ## 1100.0.6 ### Patch Changes diff --git a/fetching/pick-fetcher/package.json b/fetching/pick-fetcher/package.json index c733665674..b3eecea303 100644 --- a/fetching/pick-fetcher/package.json +++ b/fetching/pick-fetcher/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/fetching.pick-fetcher", - "version": "1100.0.6", + "version": "1100.0.9", "description": "Pick a package fetcher by type", "keywords": [ "pnpm", diff --git a/fetching/tarball-fetcher/CHANGELOG.md b/fetching/tarball-fetcher/CHANGELOG.md index cb8837eed5..8e2b7d489e 100644 --- a/fetching/tarball-fetcher/CHANGELOG.md +++ b/fetching/tarball-fetcher/CHANGELOG.md @@ -1,5 +1,44 @@ # @pnpm/tarball-fetcher +## 1101.0.11 + +### Patch Changes + +- Updated dependencies [aa6149d] +- Updated dependencies [35d2355] + - @pnpm/worker@1100.1.8 + - @pnpm/types@1101.2.0 + - @pnpm/core-loggers@1100.1.2 + - @pnpm/exec.prepare-package@1100.0.14 + - @pnpm/fetching.fetcher-base@1100.1.6 + +## 1101.0.10 + +### Patch Changes + +- @pnpm/exec.prepare-package@1100.0.13 + +## 1101.0.9 + +### Patch Changes + +- Updated dependencies [64afc92] + - @pnpm/types@1101.1.1 + - @pnpm/exec.prepare-package@1100.0.12 + - @pnpm/fetching.fetcher-base@1100.1.5 + - @pnpm/core-loggers@1100.1.1 + - @pnpm/worker@1100.1.7 + +## 1101.0.8 + +### Patch Changes + +- Updated dependencies [4a79336] + - @pnpm/core-loggers@1100.1.0 + - @pnpm/fetching.fetcher-base@1100.1.4 + - @pnpm/exec.prepare-package@1100.0.11 + - @pnpm/worker@1100.1.6 + ## 1101.0.7 ### Patch Changes diff --git a/fetching/tarball-fetcher/package.json b/fetching/tarball-fetcher/package.json index f119ac7ec1..0d24efed1a 100644 --- a/fetching/tarball-fetcher/package.json +++ b/fetching/tarball-fetcher/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/fetching.tarball-fetcher", - "version": "1101.0.7", + "version": "1101.0.11", "description": "Fetcher for packages hosted as tarballs", "keywords": [ "pnpm", diff --git a/fs/indexed-pkg-importer/CHANGELOG.md b/fs/indexed-pkg-importer/CHANGELOG.md index f4ff80fc8f..434a070bc6 100644 --- a/fs/indexed-pkg-importer/CHANGELOG.md +++ b/fs/indexed-pkg-importer/CHANGELOG.md @@ -1,5 +1,28 @@ # @pnpm/fs.indexed-pkg-importer +## 1100.0.10 + +### Patch Changes + +- @pnpm/core-loggers@1100.1.2 +- @pnpm/store.controller-types@1100.1.2 + +## 1100.0.9 + +### Patch Changes + +- @pnpm/store.controller-types@1100.1.1 +- @pnpm/core-loggers@1100.1.1 + +## 1100.0.8 + +### Patch Changes + +- Updated dependencies [4195766] +- Updated dependencies [4a79336] + - @pnpm/store.controller-types@1100.1.0 + - @pnpm/core-loggers@1100.1.0 + ## 1100.0.7 ### Patch Changes diff --git a/fs/indexed-pkg-importer/package.json b/fs/indexed-pkg-importer/package.json index 9419fb48b8..c8c6b93fc1 100644 --- a/fs/indexed-pkg-importer/package.json +++ b/fs/indexed-pkg-importer/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/fs.indexed-pkg-importer", - "version": "1100.0.7", + "version": "1100.0.10", "description": "Replicates indexed directories using hard links, copies, or cloning", "keywords": [ "pnpm", diff --git a/fs/symlink-dependency/CHANGELOG.md b/fs/symlink-dependency/CHANGELOG.md index 42041ba616..e4b2db1459 100644 --- a/fs/symlink-dependency/CHANGELOG.md +++ b/fs/symlink-dependency/CHANGELOG.md @@ -1,5 +1,29 @@ # @pnpm/symlink-dependency +## 1100.0.6 + +### Patch Changes + +- ad84fff: Reject dependency aliases that contain path-traversal segments (such as `@x/../../../../../.git/hooks`) when reading them from a package manifest or symlinking them into `node_modules`. A malicious registry package could otherwise use a transitive dependency key to make `pnpm install` create symlinks at attacker-chosen paths outside the intended `node_modules` directory. +- Updated dependencies [35d2355] + - @pnpm/types@1101.2.0 + - @pnpm/core-loggers@1100.1.2 + +## 1100.0.5 + +### Patch Changes + +- Updated dependencies [64afc92] + - @pnpm/types@1101.1.1 + - @pnpm/core-loggers@1100.1.1 + +## 1100.0.4 + +### Patch Changes + +- Updated dependencies [4a79336] + - @pnpm/core-loggers@1100.1.0 + ## 1100.0.3 ### Patch Changes diff --git a/fs/symlink-dependency/package.json b/fs/symlink-dependency/package.json index 59f907e294..962ba58d27 100644 --- a/fs/symlink-dependency/package.json +++ b/fs/symlink-dependency/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/fs.symlink-dependency", - "version": "1100.0.3", + "version": "1100.0.6", "description": "Symlink a dependency to node_modules", "keywords": [ "pnpm", diff --git a/fs/symlink-dependency/src/index.ts b/fs/symlink-dependency/src/index.ts index 7862a04be0..5f80c69a58 100644 --- a/fs/symlink-dependency/src/index.ts +++ b/fs/symlink-dependency/src/index.ts @@ -1,8 +1,8 @@ -import path from 'node:path' - import { linkLogger } from '@pnpm/core-loggers' import { symlinkDir, symlinkDirSync } from 'symlink-dir' +import { safeJoinModulesDir } from './safeJoinModulesDir.js' + export { symlinkDirectRootDependency } from './symlinkDirectRootDependency.js' export async function symlinkDependency ( @@ -10,7 +10,7 @@ export async function symlinkDependency ( destModulesDir: string, importAs: string ): Promise<{ reused: boolean, warn?: string }> { - const link = path.join(destModulesDir, importAs) + const link = safeJoinModulesDir(destModulesDir, importAs) linkLogger.debug({ target: dependencyRealLocation, link }) return symlinkDir(dependencyRealLocation, link) } @@ -20,7 +20,7 @@ export function symlinkDependencySync ( destModulesDir: string, importAs: string ): { reused: boolean, warn?: string } { - const link = path.join(destModulesDir, importAs) + const link = safeJoinModulesDir(destModulesDir, importAs) linkLogger.debug({ target: dependencyRealLocation, link }) return symlinkDirSync(dependencyRealLocation, link) } diff --git a/fs/symlink-dependency/src/safeJoinModulesDir.ts b/fs/symlink-dependency/src/safeJoinModulesDir.ts new file mode 100644 index 0000000000..5017b363bc --- /dev/null +++ b/fs/symlink-dependency/src/safeJoinModulesDir.ts @@ -0,0 +1,19 @@ +import path from 'node:path' + +// `path.join(modulesDir, alias)` paired with a containment check, so a +// caller can't accidentally use the joined path without verifying that +// it lives inside `modulesDir`. Earlier passes reject path-traversal +// aliases at manifest-read time, but this layer also runs for paths +// reconstructed from lockfiles and snapshots, so the check stays here +// as a final guarantee. +export function safeJoinModulesDir (modulesDir: string, alias: string): string { + const link = path.join(modulesDir, alias) + const resolvedDir = path.resolve(modulesDir) + const resolvedLink = path.resolve(link) + if (resolvedLink === resolvedDir || !resolvedLink.startsWith(resolvedDir + path.sep)) { + const error = new Error(`Refusing to symlink dependency outside ${modulesDir}: alias ${JSON.stringify(alias)} resolves to ${resolvedLink}`) as Error & { code: string } + error.code = 'ERR_PNPM_INVALID_DEPENDENCY_NAME' + throw error + } + return link +} diff --git a/fs/symlink-dependency/src/symlinkDirectRootDependency.ts b/fs/symlink-dependency/src/symlinkDirectRootDependency.ts index 57703a9818..808259ec27 100644 --- a/fs/symlink-dependency/src/symlinkDirectRootDependency.ts +++ b/fs/symlink-dependency/src/symlinkDirectRootDependency.ts @@ -1,5 +1,4 @@ import { promises as fs } from 'node:fs' -import path from 'node:path' import util from 'node:util' import { @@ -9,6 +8,8 @@ import { import type { DependenciesField } from '@pnpm/types' import { symlinkDir } from 'symlink-dir' +import { safeJoinModulesDir } from './safeJoinModulesDir.js' + const DEP_TYPE_BY_DEPS_FIELD_NAME = { dependencies: 'prod', devDependencies: 'dev', @@ -45,7 +46,7 @@ export async function symlinkDirectRootDependency ( } } - const dest = path.join(destModulesDirReal, importAs) + const dest = safeJoinModulesDir(destModulesDirReal, importAs) const { reused } = await symlinkDir(dependencyLocation, dest) if (reused) return // if the link was already present, don't log rootLogger.debug({ diff --git a/fs/symlink-dependency/test/safeJoinModulesDir.test.ts b/fs/symlink-dependency/test/safeJoinModulesDir.test.ts new file mode 100644 index 0000000000..edbda76131 --- /dev/null +++ b/fs/symlink-dependency/test/safeJoinModulesDir.test.ts @@ -0,0 +1,36 @@ +import fs from 'node:fs' +import path from 'node:path' + +import { expect, test } from '@jest/globals' +import { symlinkDependency, symlinkDependencySync, symlinkDirectRootDependency } from '@pnpm/fs.symlink-dependency' +import { tempDir } from '@pnpm/prepare' + +const escapeAliases = ['@x/../../../etc', '../sibling', '', '.'] + +test.each(escapeAliases)('symlinkDependency refuses alias %p', async (alias) => { + const tmp = tempDir(false) + const destModulesDir = path.join(tmp, 'node_modules') + fs.mkdirSync(destModulesDir) + await expect( + symlinkDependency(path.join(tmp, 'dep'), destModulesDir, alias) + ).rejects.toThrow(expect.objectContaining({ code: 'ERR_PNPM_INVALID_DEPENDENCY_NAME' })) +}) + +test.each(escapeAliases)('symlinkDependencySync refuses alias %p', (alias) => { + const tmp = tempDir(false) + const destModulesDir = path.join(tmp, 'node_modules') + fs.mkdirSync(destModulesDir) + expect(() => { + symlinkDependencySync(path.join(tmp, 'dep'), destModulesDir, alias) + }).toThrow(expect.objectContaining({ code: 'ERR_PNPM_INVALID_DEPENDENCY_NAME' })) +}) + +test.each(escapeAliases)('symlinkDirectRootDependency refuses alias %p', async (alias) => { + const tmp = tempDir(false) + const destModulesDir = path.join(tmp, 'node_modules') + fs.mkdirSync(destModulesDir) + await expect(symlinkDirectRootDependency(path.join(tmp, 'dep'), destModulesDir, alias, { + linkedPackage: { name: 'dep', version: '1.0.0' }, + prefix: '', + })).rejects.toThrow(expect.objectContaining({ code: 'ERR_PNPM_INVALID_DEPENDENCY_NAME' })) +}) diff --git a/global/commands/CHANGELOG.md b/global/commands/CHANGELOG.md index 35ac0d93eb..3e4ee329f5 100644 --- a/global/commands/CHANGELOG.md +++ b/global/commands/CHANGELOG.md @@ -1,5 +1,94 @@ # @pnpm/global.commands +## 1100.0.22 + +### Patch Changes + +- 440e155: Print the global update summary after all isolated global package groups are processed. +- Updated dependencies [a23956e] +- Updated dependencies [aa6149d] +- Updated dependencies [a456dc7] +- Updated dependencies [35d2355] + - @pnpm/config.reader@1101.4.1 + - @pnpm/installing.deps-installer@1101.5.0 + - @pnpm/deps.inspection.list@1100.0.14 + - @pnpm/types@1101.2.0 + - @pnpm/store.connection-manager@1100.2.4 + - @pnpm/bins.linker@1100.0.10 + - @pnpm/cli.utils@1101.0.8 + - @pnpm/bins.remover@1100.0.6 + - @pnpm/bins.resolver@1100.0.5 + - @pnpm/core-loggers@1100.1.2 + - @pnpm/global.packages@1100.0.5 + - @pnpm/pkg-manifest.reader@1100.0.5 + +## 1100.0.21 + +### Patch Changes + +- Updated dependencies [3b62f9d] +- Updated dependencies [212315d] + - @pnpm/config.reader@1101.4.0 + - @pnpm/installing.deps-installer@1101.4.0 + - @pnpm/bins.linker@1100.0.9 + - @pnpm/cli.utils@1101.0.7 + - @pnpm/deps.inspection.list@1100.0.13 + - @pnpm/store.connection-manager@1100.2.3 + +## 1100.0.20 + +### Patch Changes + +- @pnpm/store.connection-manager@1100.2.2 +- @pnpm/installing.deps-installer@1101.3.1 + +## 1100.0.19 + +### Patch Changes + +- a620557: Fix global add/update to handle minimumReleaseAge policy violations instead of surfacing an internal resolver guardrail error. +- Updated dependencies [3687b0e] +- Updated dependencies [ced20cb] +- Updated dependencies [d1b340f] +- Updated dependencies [b206a15] +- Updated dependencies [64afc92] + - @pnpm/config.reader@1101.3.3 + - @pnpm/installing.deps-installer@1101.3.0 + - @pnpm/types@1101.1.1 + - @pnpm/store.connection-manager@1100.2.1 + - @pnpm/deps.inspection.list@1100.0.12 + - @pnpm/bins.linker@1100.0.8 + - @pnpm/cli.utils@1101.0.6 + - @pnpm/bins.remover@1100.0.5 + - @pnpm/bins.resolver@1100.0.4 + - @pnpm/core-loggers@1100.1.1 + - @pnpm/global.packages@1100.0.4 + - @pnpm/pkg-manifest.reader@1100.0.4 + +## 1100.0.18 + +### Patch Changes + +- Updated dependencies [4195766] +- Updated dependencies [31538bf] +- Updated dependencies [020ac45] +- Updated dependencies [d3f8408] +- Updated dependencies [3ddde2b] +- Updated dependencies [4a79336] +- Updated dependencies [a62f959] +- Updated dependencies [ba2c884] +- Updated dependencies [2a9bd89] +- Updated dependencies [31538bf] +- Updated dependencies [8df408c] + - @pnpm/installing.deps-installer@1101.2.0 + - @pnpm/store.connection-manager@1100.2.0 + - @pnpm/config.reader@1101.3.2 + - @pnpm/core-loggers@1100.1.0 + - @pnpm/deps.inspection.list@1100.0.11 + - @pnpm/bins.remover@1100.0.4 + - @pnpm/cli.utils@1101.0.5 + - @pnpm/bins.linker@1100.0.7 + ## 1100.0.17 ### Patch Changes diff --git a/global/commands/package.json b/global/commands/package.json index ca2ae9d31e..0b7c194b24 100644 --- a/global/commands/package.json +++ b/global/commands/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/global.commands", - "version": "1100.0.17", + "version": "1100.0.22", "description": "Global package command handlers for pnpm", "keywords": [ "pnpm", diff --git a/global/commands/src/globalAdd.ts b/global/commands/src/globalAdd.ts index c21eac4439..2ce56fbeac 100644 --- a/global/commands/src/globalAdd.ts +++ b/global/commands/src/globalAdd.ts @@ -19,7 +19,7 @@ import { isSubdir } from 'is-subdir' import { symlinkDir } from 'symlink-dir' import { checkGlobalBinConflicts } from './checkGlobalBinConflicts.js' -import { installGlobalPackages } from './installGlobalPackages.js' +import { installGlobalPackages, type ResolutionPolicyViolation } from './installGlobalPackages.js' import { promptApproveGlobalBuilds } from './promptApproveGlobalBuilds.js' import { readInstalledPackages } from './readInstalledPackages.js' @@ -33,6 +33,8 @@ export type GlobalAddOptions = CreateStoreControllerOptions & { savePrefix?: string supportedArchitectures?: { libc?: string[] } rootProjectManifest?: unknown + handleResolutionPolicyViolations?: (violations: readonly ResolutionPolicyViolation[]) => Promise + updateResolutionPolicyManifest?: (violations: readonly ResolutionPolicyViolation[], dir: string) => Promise } export async function handleGlobalAdd ( @@ -124,7 +126,7 @@ async function installGroup ( omitSummaryLog: true, } - const ignoredBuilds = await installGlobalPackages(installOpts, params) + const { ignoredBuilds, resolutionPolicyViolations } = await installGlobalPackages(installOpts, params) await promptApproveGlobalBuilds({ globalPkgDir: globalDir, @@ -167,6 +169,7 @@ async function installGroup ( // Link bins from installed packages into global bin dir await linkBinsOfPackages(pkgs, globalBinDir, { excludeBins: binsToSkip }) + await opts.updateResolutionPolicyManifest?.(resolutionPolicyViolations, globalDir) } function splitCommaSeparated (param: string, baseDir: string): string[] { diff --git a/global/commands/src/globalUpdate.ts b/global/commands/src/globalUpdate.ts index f46b6ed962..31b6c31406 100644 --- a/global/commands/src/globalUpdate.ts +++ b/global/commands/src/globalUpdate.ts @@ -4,6 +4,7 @@ import path from 'node:path' import { linkBinsOfPackages } from '@pnpm/bins.linker' import { removeBin } from '@pnpm/bins.remover' import type { CommandHandlerMap } from '@pnpm/cli.command' +import { summaryLogger } from '@pnpm/core-loggers' import { cleanOrphanedInstallDirs, createInstallDir, @@ -17,7 +18,7 @@ import { isSubdir } from 'is-subdir' import { symlinkDir } from 'symlink-dir' import { checkGlobalBinConflicts } from './checkGlobalBinConflicts.js' -import { installGlobalPackages } from './installGlobalPackages.js' +import { installGlobalPackages, type ResolutionPolicyViolation } from './installGlobalPackages.js' import { promptApproveGlobalBuilds } from './promptApproveGlobalBuilds.js' import { readInstalledPackages } from './readInstalledPackages.js' @@ -30,6 +31,8 @@ export type GlobalUpdateOptions = CreateStoreControllerOptions & { savePrefix?: string supportedArchitectures?: { libc?: string[] } rootProjectManifest?: unknown + handleResolutionPolicyViolations?: (violations: readonly ResolutionPolicyViolation[]) => Promise + updateResolutionPolicyManifest?: (violations: readonly ResolutionPolicyViolation[], dir: string) => Promise } export async function handleGlobalUpdate ( @@ -64,6 +67,7 @@ export async function handleGlobalUpdate ( for (const pkg of packagesToUpdate) { await updateGlobalPackageGroup(opts, globalDir, globalBinDir, pkg, commands) // eslint-disable-line no-await-in-loop } + summaryLogger.debug({ prefix: globalDir }) return undefined } @@ -90,7 +94,7 @@ async function updateGlobalPackageGroup ( const fetchFullMetadata = opts.supportedArchitectures?.libc != null && true const allowBuilds = opts.allowBuilds ?? {} - const ignoredBuilds = await installGlobalPackages({ + const { ignoredBuilds, resolutionPolicyViolations } = await installGlobalPackages({ ...opts, global: false, bin: path.join(installDir, 'node_modules/.bin'), @@ -109,6 +113,7 @@ async function updateGlobalPackageGroup ( include, includeDirect: include, allowBuilds, + omitSummaryLog: true, }, depSpecs) await promptApproveGlobalBuilds({ @@ -148,4 +153,5 @@ async function updateGlobalPackageGroup ( // Link bins from new installation await linkBinsOfPackages(pkgs, globalBinDir, { excludeBins: binsToSkip }) + await opts.updateResolutionPolicyManifest?.(resolutionPolicyViolations, globalDir) } diff --git a/global/commands/src/installGlobalPackages.ts b/global/commands/src/installGlobalPackages.ts index 3636a9b1ca..f3adadba04 100644 --- a/global/commands/src/installGlobalPackages.ts +++ b/global/commands/src/installGlobalPackages.ts @@ -3,6 +3,18 @@ import { mutateModulesInSingleProject } from '@pnpm/installing.deps-installer' import { createStoreController, type CreateStoreControllerOptions } from '@pnpm/store.connection-manager' import type { IgnoredBuilds, IncludedDependencies, ProjectRootDir } from '@pnpm/types' +export interface ResolutionPolicyViolation { + name: string + version: string + code: string + reason: string +} + +export interface InstallGlobalPackagesResult { + ignoredBuilds: IgnoredBuilds | undefined + resolutionPolicyViolations: ResolutionPolicyViolation[] +} + export interface InstallGlobalPackagesOptions extends CreateStoreControllerOptions { bin: string dir: string @@ -24,12 +36,13 @@ export interface InstallGlobalPackagesOptions extends CreateStoreControllerOptio saveProd?: boolean sharedWorkspaceLockfile?: boolean workspaceDir?: string + handleResolutionPolicyViolations?: (violations: readonly ResolutionPolicyViolation[]) => Promise } export async function installGlobalPackages ( opts: InstallGlobalPackagesOptions, params: string[] -): Promise { +): Promise { const store = await createStoreController(opts) let { manifest, writeProjectManifest } = await tryReadProjectManifest(opts.dir, opts) if (manifest == null) { @@ -42,7 +55,7 @@ export async function installGlobalPackages ( storeDir: store.dir, } const pinnedVersion = opts.saveExact ? 'patch' : (opts.savePrefix === '~' ? 'minor' : 'major') - const { updatedProject, ignoredBuilds } = await mutateModulesInSingleProject( + const { updatedProject, ignoredBuilds, resolutionPolicyViolations } = await mutateModulesInSingleProject( { allowNew: true, binsDir: opts.bin, @@ -57,5 +70,5 @@ export async function installGlobalPackages ( installOpts ) await writeProjectManifest(updatedProject.manifest) - return ignoredBuilds + return { ignoredBuilds, resolutionPolicyViolations } } diff --git a/global/commands/test/globalUpdate.test.ts b/global/commands/test/globalUpdate.test.ts new file mode 100644 index 0000000000..e0b15afe6c --- /dev/null +++ b/global/commands/test/globalUpdate.test.ts @@ -0,0 +1,83 @@ +import { expect, jest, test } from '@jest/globals' + +const linkBinsOfPackages = jest.fn<() => Promise>().mockResolvedValue(undefined) +const removeBin = jest.fn<() => Promise>().mockResolvedValue(undefined) +const cleanOrphanedInstallDirs = jest.fn() +const createInstallDir = jest.fn() +const getHashLink = jest.fn() +const getInstalledBinNames = jest.fn<() => Promise>().mockResolvedValue([]) +const scanGlobalPackages = jest.fn() +const checkGlobalBinConflicts = jest.fn<() => Promise>>().mockResolvedValue(new Set()) +const installGlobalPackages = jest.fn<(...args: unknown[]) => Promise<{ ignoredBuilds: undefined, resolutionPolicyViolations: [] }>>() + .mockResolvedValue({ ignoredBuilds: undefined, resolutionPolicyViolations: [] }) +const promptApproveGlobalBuilds = jest.fn<() => Promise>().mockResolvedValue(undefined) +const readInstalledPackages = jest.fn<() => Promise<[]>>().mockResolvedValue([]) +const summaryDebug = jest.fn() +const symlinkDir = jest.fn<() => Promise>().mockResolvedValue(undefined) + +jest.unstable_mockModule('@pnpm/bins.linker', () => ({ linkBinsOfPackages })) +jest.unstable_mockModule('@pnpm/bins.remover', () => ({ removeBin })) +jest.unstable_mockModule('@pnpm/core-loggers', () => ({ summaryLogger: { debug: summaryDebug } })) +jest.unstable_mockModule('@pnpm/global.packages', () => ({ + cleanOrphanedInstallDirs, + createInstallDir, + getHashLink, + getInstalledBinNames, + scanGlobalPackages, +})) +jest.unstable_mockModule('is-subdir', () => ({ isSubdir: () => false })) +jest.unstable_mockModule('symlink-dir', () => ({ symlinkDir })) +jest.unstable_mockModule('../src/checkGlobalBinConflicts.js', () => ({ checkGlobalBinConflicts })) +jest.unstable_mockModule('../src/installGlobalPackages.js', () => ({ installGlobalPackages })) +jest.unstable_mockModule('../src/promptApproveGlobalBuilds.js', () => ({ promptApproveGlobalBuilds })) +jest.unstable_mockModule('../src/readInstalledPackages.js', () => ({ readInstalledPackages })) + +const { handleGlobalUpdate } = await import('../src/globalUpdate.js') + +test('global update emits a single summary after updating all isolated groups', async () => { + createInstallDir + .mockReturnValueOnce('/global/v11/install-1') + .mockReturnValueOnce('/global/v11/install-2') + getHashLink + .mockReturnValueOnce('/global/v11/hash-foo') + .mockReturnValueOnce('/global/v11/hash-bar') + scanGlobalPackages.mockReturnValue([ + { + dependencies: { foo: '^1.0.0' }, + hash: 'hash-foo', + installDir: '/global/v11/old-foo', + }, + { + dependencies: { bar: '^2.0.0' }, + hash: 'hash-bar', + installDir: '/global/v11/old-bar', + }, + ]) + + await handleGlobalUpdate({ + bin: '/global/bin', + globalPkgDir: '/global/v11', + } as any, [], {}) // eslint-disable-line @typescript-eslint/no-explicit-any + + expect(installGlobalPackages).toHaveBeenCalledTimes(2) + expect(installGlobalPackages).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + dir: '/global/v11/install-1', + global: false, + omitSummaryLog: true, + }), + ['foo@^1.0.0'] + ) + expect(installGlobalPackages).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + dir: '/global/v11/install-2', + global: false, + omitSummaryLog: true, + }), + ['bar@^2.0.0'] + ) + expect(summaryDebug).toHaveBeenCalledTimes(1) + expect(summaryDebug).toHaveBeenCalledWith({ prefix: '/global/v11' }) +}) diff --git a/global/packages/CHANGELOG.md b/global/packages/CHANGELOG.md index 3d9bd3c13d..699a5dca03 100644 --- a/global/packages/CHANGELOG.md +++ b/global/packages/CHANGELOG.md @@ -1,5 +1,25 @@ # @pnpm/global.packages +## 1100.0.5 + +### Patch Changes + +- Updated dependencies [35d2355] + - @pnpm/types@1101.2.0 + - @pnpm/bins.resolver@1100.0.5 + - @pnpm/pkg-manifest.reader@1100.0.5 + - @pnpm/crypto.hash@1100.0.1 + +## 1100.0.4 + +### Patch Changes + +- Updated dependencies [64afc92] + - @pnpm/types@1101.1.1 + - @pnpm/bins.resolver@1100.0.4 + - @pnpm/pkg-manifest.reader@1100.0.4 + - @pnpm/crypto.hash@1100.0.1 + ## 1100.0.3 ### Patch Changes diff --git a/global/packages/package.json b/global/packages/package.json index 5423860866..3854a958e5 100644 --- a/global/packages/package.json +++ b/global/packages/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/global.packages", - "version": "1100.0.3", + "version": "1100.0.5", "description": "Utilities for managing isolated global packages", "keywords": [ "pnpm", diff --git a/hooks/pnpmfile/CHANGELOG.md b/hooks/pnpmfile/CHANGELOG.md index 5c6f93a6e6..65a704b14f 100644 --- a/hooks/pnpmfile/CHANGELOG.md +++ b/hooks/pnpmfile/CHANGELOG.md @@ -1,5 +1,41 @@ # @pnpm/pnpmfile +## 1100.0.11 + +### Patch Changes + +- Updated dependencies [35d2355] + - @pnpm/types@1101.2.0 + - @pnpm/core-loggers@1100.1.2 + - @pnpm/hooks.types@1100.0.9 + - @pnpm/lockfile.types@1100.0.8 + - @pnpm/store.controller-types@1100.1.2 + - @pnpm/crypto.hash@1100.0.1 + +## 1100.0.10 + +### Patch Changes + +- Updated dependencies [64afc92] + - @pnpm/types@1101.1.1 + - @pnpm/hooks.types@1100.0.8 + - @pnpm/lockfile.types@1100.0.7 + - @pnpm/store.controller-types@1100.1.1 + - @pnpm/core-loggers@1100.1.1 + - @pnpm/crypto.hash@1100.0.1 + +## 1100.0.9 + +### Patch Changes + +- Updated dependencies [4195766] +- Updated dependencies [4a79336] + - @pnpm/store.controller-types@1100.1.0 + - @pnpm/core-loggers@1100.1.0 + - @pnpm/hooks.types@1100.0.7 + - @pnpm/lockfile.types@1100.0.6 + - @pnpm/crypto.hash@1100.0.1 + ## 1100.0.8 ### Patch Changes diff --git a/hooks/pnpmfile/package.json b/hooks/pnpmfile/package.json index cfef2d0bb9..b98d44307b 100644 --- a/hooks/pnpmfile/package.json +++ b/hooks/pnpmfile/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/hooks.pnpmfile", - "version": "1100.0.8", + "version": "1100.0.11", "description": "Reading a .pnpmfile.cjs", "keywords": [ "pnpm", diff --git a/hooks/read-package-hook/CHANGELOG.md b/hooks/read-package-hook/CHANGELOG.md index 8d88bb7c3c..762946cc55 100644 --- a/hooks/read-package-hook/CHANGELOG.md +++ b/hooks/read-package-hook/CHANGELOG.md @@ -1,5 +1,19 @@ # @pnpm/hooks.read-package-hook +## 1100.0.5 + +### Patch Changes + +- Updated dependencies [35d2355] + - @pnpm/types@1101.2.0 + +## 1100.0.4 + +### Patch Changes + +- Updated dependencies [64afc92] + - @pnpm/types@1101.1.1 + ## 1100.0.3 ### Patch Changes diff --git a/hooks/read-package-hook/package.json b/hooks/read-package-hook/package.json index 440147c857..d1f0962ff2 100644 --- a/hooks/read-package-hook/package.json +++ b/hooks/read-package-hook/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/hooks.read-package-hook", - "version": "1100.0.3", + "version": "1100.0.5", "description": "Creates the default package reader hook used by pnpm", "keywords": [ "pnpm", diff --git a/hooks/types/CHANGELOG.md b/hooks/types/CHANGELOG.md index d92415d709..e5b3949c29 100644 --- a/hooks/types/CHANGELOG.md +++ b/hooks/types/CHANGELOG.md @@ -1,5 +1,38 @@ # @pnpm/hooks.types +## 1100.0.9 + +### Patch Changes + +- Updated dependencies [35d2355] + - @pnpm/types@1101.2.0 + - @pnpm/fetching.fetcher-base@1100.1.6 + - @pnpm/lockfile.types@1100.0.8 + - @pnpm/resolving.resolver-base@1100.3.1 + - @pnpm/store.cafs-types@1100.0.1 + +## 1100.0.8 + +### Patch Changes + +- Updated dependencies [1627943] +- Updated dependencies [64afc92] + - @pnpm/resolving.resolver-base@1100.3.0 + - @pnpm/types@1101.1.1 + - @pnpm/fetching.fetcher-base@1100.1.5 + - @pnpm/lockfile.types@1100.0.7 + - @pnpm/store.cafs-types@1100.0.1 + +## 1100.0.7 + +### Patch Changes + +- Updated dependencies [4195766] +- Updated dependencies [31538bf] + - @pnpm/resolving.resolver-base@1100.2.0 + - @pnpm/fetching.fetcher-base@1100.1.4 + - @pnpm/lockfile.types@1100.0.6 + ## 1100.0.6 ### Patch Changes diff --git a/hooks/types/package.json b/hooks/types/package.json index fa27396b42..59eb1f430c 100644 --- a/hooks/types/package.json +++ b/hooks/types/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/hooks.types", - "version": "1100.0.6", + "version": "1100.0.9", "description": "Types for hooks", "keywords": [ "pnpm", diff --git a/installing/client/CHANGELOG.md b/installing/client/CHANGELOG.md index 966c91dd21..a35d56ca90 100644 --- a/installing/client/CHANGELOG.md +++ b/installing/client/CHANGELOG.md @@ -1,5 +1,108 @@ # @pnpm/client +## 1100.2.3 + +### Patch Changes + +- Updated dependencies [a23956e] +- Updated dependencies [90d1ce6] +- Updated dependencies [35d2355] +- Updated dependencies [0721d64] + - @pnpm/network.auth-header@1101.0.0 + - @pnpm/fetching.git-fetcher@1101.0.10 + - @pnpm/types@1101.2.0 + - @pnpm/resolving.npm-resolver@1101.3.3 + - @pnpm/engine.runtime.node-resolver@1101.1.2 + - @pnpm/resolving.default-resolver@1100.3.3 + - @pnpm/fetching.directory-fetcher@1100.0.13 + - @pnpm/fetching.tarball-fetcher@1101.0.11 + - @pnpm/hooks.types@1100.0.9 + - @pnpm/network.fetch@1100.0.7 + - @pnpm/resolving.resolver-base@1100.3.1 + - @pnpm/fetching.binary-fetcher@1101.0.8 + +## 1100.2.2 + +### Patch Changes + +- Updated dependencies [212315d] + - @pnpm/resolving.npm-resolver@1101.3.2 + - @pnpm/fetching.directory-fetcher@1100.0.12 + - @pnpm/engine.runtime.node-resolver@1101.1.1 + - @pnpm/resolving.default-resolver@1100.3.2 + - @pnpm/fetching.git-fetcher@1101.0.9 + - @pnpm/fetching.tarball-fetcher@1101.0.10 + +## 1100.2.1 + +### Patch Changes + +- @pnpm/resolving.npm-resolver@1101.3.1 +- @pnpm/resolving.default-resolver@1100.3.1 + +## 1100.2.0 + +### Minor Changes + +- 1627943: `pnpm outdated` and `pnpm update --interactive` now report Node.js, Deno, and Bun runtimes installed as project dependencies (`runtime:` specifiers). Previously these were silently skipped because the npm specifier parser did not understand the `runtime:` protocol, so runtime versions never appeared in the outdated table or the interactive update picker. + + Internally, the outdated check is now resolver-driven: `@pnpm/resolving.resolver-base` defines a `ResolveLatestFunction` shape (with `LatestQuery` input — `{ wantedDependency, compatible? }` — and `LatestInfo` result — `{ latestManifest? }`), and every protocol resolver (npm, jsr, named-registry, git, tarball, local, node/bun/deno runtimes) exports its own `resolveLatest*` function alongside its `resolve*`. `@pnpm/resolving.default-resolver` composes them into a single dispatcher, exposed through `@pnpm/installing.client` as `createResolver(...).resolveLatest`. + + Each resolver decides whether it owns the dep and what "latest" means for its protocol; the outdated command derives `current` / `wanted` display values from the lockfile snapshot (`pkgSnapshot.version` for semver protocols, raw ref for URL-shaped ones) and uses raw ref equality for the "lockfile changed" check, so protocol knowledge stays inside each resolver instead of the command. + +### Patch Changes + +- Updated dependencies [3a54205] +- Updated dependencies [1627943] +- Updated dependencies [64afc92] + - @pnpm/resolving.npm-resolver@1101.3.0 + - @pnpm/resolving.default-resolver@1100.3.0 + - @pnpm/resolving.resolver-base@1100.3.0 + - @pnpm/engine.runtime.node-resolver@1101.1.0 + - @pnpm/types@1101.1.1 + - @pnpm/fetching.directory-fetcher@1100.0.11 + - @pnpm/hooks.types@1100.0.8 + - @pnpm/fetching.git-fetcher@1101.0.8 + - @pnpm/fetching.tarball-fetcher@1101.0.9 + - @pnpm/network.auth-header@1100.0.3 + - @pnpm/network.fetch@1100.0.6 + - @pnpm/fetching.binary-fetcher@1101.0.7 + +## 1100.1.0 + +### Minor Changes + +- 4195766: Tightened the `minimumReleaseAge` story so the bypass becomes explicit on disk instead of silent, and removed the discover-by-loop dance for strict-mode users: + + 1. Fresh resolutions in loose mode (`minimumReleaseAgeStrict: false`) that fall back to a version newer than the cutoff auto-collect the picked `name@version` into the workspace manifest's `minimumReleaseAgeExclude`. A single info message lists the additions; entries already on the list are left alone. + 2. The post-resolution lockfile verifier introduced in #11583 now runs in loose mode too — every accepted-immature pin must be on `minimumReleaseAgeExclude`, just like strict mode requires. A lockfile produced under a weaker (or absent) policy that still has immature entries is rejected the same way strict mode would reject it. + 3. **Strict mode (interactive)** no longer aborts on the first immature pick. The resolver gathers every immature direct _and_ transitive in one pass; before peer-dependency resolution runs, pnpm prompts the user with the full list and asks whether to add them all to `minimumReleaseAgeExclude` and proceed. Approve → install continues and the workspace manifest is written at the end. Decline → resolution aborts before the lockfile or package.json is touched (tarballs already in the store stay, since the store is idempotent). This closes the [#10488](https://github.com/pnpm/pnpm/issues/10488) loop where security bumps to packages with platform-specific transitives (e.g. `next` + the `@next/swc-*` shims) made users re-run `pnpm add` once per transitive. + 4. **Strict mode (non-interactive / CI)** now aborts with the full immature set in the error message instead of the first pick. The resolver always collects every immature direct + transitive; the install command then throws `ERR_PNPM_NO_MATURE_MATCHING_VERSION` listing each entry's `name@version` and publish time. Deterministic CI behavior is preserved (same exit code, same error code), but the error pinpoints every offending entry instead of forcing the discover-by-loop dance. The expected workflow is interactive approval locally → the lockfile + workspace manifest get committed → CI runs cleanly against the populated exclude list. + + 5. **The lockfile verifier now also covers `trustPolicy: 'no-downgrade'`.** The same post-resolution gate that re-checks `minimumReleaseAge` on lockfile entries now re-runs `failIfTrustDowngraded` for every npm-registry entry whose name isn't on `trustPolicyExclude`. The two checks share a single full-metadata fetch per package, so the extra coverage doesn't cost an extra round trip when both policies are active. Resolver-time trust checks still run as before — this just closes the gap when an entry bypasses resolution (peek path, `--frozen-lockfile`, restored CI cache). + + Pacquet parity: not ported — pacquet's `minimumReleaseAge` policy is itself only stubbed today (see `pacquet/crates/package-manager/src/version_policy.rs`). The auto-exclude, loose-mode verifier, prompt, and the new trust-policy verifier check will travel with the broader policy port whenever that happens. + +- 31538bf: Restructured the `minimumReleaseAge` lockfile revalidation gate around a generic `ResolutionVerifier` interface. Each resolver may now export a sibling verifier factory (today: `createNpmResolutionVerifier`) that re-checks an already-resolved lockfile entry against its policies; the resolver chain returns the verifier list as `resolutionVerifiers` and the install side fans out across it. A `ResolutionVerifier` carries `verify` plus `policy` and `canTrustPastCheck` — the cache contract that lets repeat installs against an unchanged lockfile skip the per-package registry round trip entirely. + + Verification results are memoized in JSON Lines at `/lockfile-verified.jsonl`: a stat-only fast path matches on lockfile size, mtime, and inode, falling back to a content hash when those drift (typical after a CI checkout). Every active verifier's policy contribution is merged into a single `policy` bag on the record; the gate runs in full whenever the lockfile changes, any verifier rejects the cached policy, or no record exists [#11687](https://github.com/pnpm/pnpm/issues/11687). + +### Patch Changes + +- Updated dependencies [963861c] +- Updated dependencies [4195766] +- Updated dependencies [31538bf] + - @pnpm/resolving.npm-resolver@1101.2.0 + - @pnpm/resolving.resolver-base@1100.2.0 + - @pnpm/resolving.default-resolver@1100.2.0 + - @pnpm/engine.runtime.node-resolver@1101.0.9 + - @pnpm/fetching.directory-fetcher@1100.0.10 + - @pnpm/hooks.types@1100.0.7 + - @pnpm/fetching.tarball-fetcher@1101.0.8 + - @pnpm/network.fetch@1100.0.5 + - @pnpm/fetching.binary-fetcher@1101.0.6 + - @pnpm/fetching.git-fetcher@1101.0.7 + ## 1100.0.15 ### Patch Changes diff --git a/installing/client/package.json b/installing/client/package.json index 96b16bec8f..3cf1846220 100644 --- a/installing/client/package.json +++ b/installing/client/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/installing.client", - "version": "1100.0.15", + "version": "1100.2.3", "description": "Creates the package resolve and fetch functions", "keywords": [ "pnpm", @@ -34,6 +34,7 @@ }, "dependencies": { "@pnpm/engine.runtime.node-resolver": "workspace:*", + "@pnpm/error": "workspace:*", "@pnpm/fetching.binary-fetcher": "workspace:*", "@pnpm/fetching.directory-fetcher": "workspace:*", "@pnpm/fetching.git-fetcher": "workspace:*", @@ -43,6 +44,7 @@ "@pnpm/network.auth-header": "workspace:*", "@pnpm/network.fetch": "workspace:*", "@pnpm/resolving.default-resolver": "workspace:*", + "@pnpm/resolving.npm-resolver": "workspace:*", "@pnpm/resolving.resolver-base": "workspace:*", "@pnpm/store.index": "workspace:*", "@pnpm/types": "workspace:*", diff --git a/installing/client/src/index.ts b/installing/client/src/index.ts index cfd62c93f1..c62a6885cd 100644 --- a/installing/client/src/index.ts +++ b/installing/client/src/index.ts @@ -1,4 +1,5 @@ import { NODE_EXTRAS_IGNORE_PATTERN } from '@pnpm/engine.runtime.node-resolver' +import { PnpmError } from '@pnpm/error' import { createBinaryFetcher } from '@pnpm/fetching.binary-fetcher' import { createDirectoryFetcher } from '@pnpm/fetching.directory-fetcher' import type { BinaryFetcher, DirectoryFetcher, GitFetcher } from '@pnpm/fetching.fetcher-base' @@ -9,17 +10,20 @@ import type { CustomFetcher, CustomResolver } from '@pnpm/hooks.types' import { createGetAuthHeaderByURI } from '@pnpm/network.auth-header' import { createFetchFromRegistry, type DispatcherOptions } from '@pnpm/network.fetch' import { - createResolutionVerifier, + createDefaultPackageMetaCache, + createResolutionVerifiers, createResolver as _createResolver, type ResolutionVerifierFactoryOptions, type ResolveFunction, + type ResolveLatestDispatcher, type ResolverFactoryOptions, } from '@pnpm/resolving.default-resolver' -import type { ResolutionVerifier } from '@pnpm/resolving.resolver-base' +import { MINIMUM_RELEASE_AGE_VIOLATION_CODE } from '@pnpm/resolving.npm-resolver' +import type { LatestInfo, LatestQuery, ResolutionPolicyViolation, ResolutionVerifier } from '@pnpm/resolving.resolver-base' import type { StoreIndex } from '@pnpm/store.index' import type { RegistryConfig } from '@pnpm/types' -export type { ResolutionVerifier, ResolveFunction } +export type { LatestInfo, LatestQuery, ResolutionVerifier, ResolveFunction, ResolveLatestDispatcher } export type ClientOptions = { configByUri: Record @@ -38,42 +42,90 @@ export type ClientOptions = { preserveAbsolutePaths?: boolean fetchMinSpeedKiBps?: number } & ResolverFactoryOptions & DispatcherOptions - & Pick + & Pick export interface Client { fetchers: Fetchers resolve: ResolveFunction clearResolutionCache: () => void /** - * Combined verifier across the resolver chain. `undefined` when no - * resolver-level policy is active (today: minimumReleaseAge strict mode). - * Used by the install layer to re-validate an already-resolved lockfile - * entry without re-doing resolution. + * List of resolver-side verifiers — one entry per active policy + * (today: at most one, `npm.minimumReleaseAge`). Empty when no policy + * is active. The install layer fans out across the list to re-validate + * each lockfile entry; each verifier handles its own protocol + * short-circuit inside `verify`. */ - verifyResolution?: ResolutionVerifier + resolutionVerifiers: ResolutionVerifier[] } export function createClient (opts: ClientOptions): Client { const fetchFromRegistry = createFetchFromRegistry(opts) - const getAuthHeader = createGetAuthHeaderByURI(opts.configByUri, opts.registries?.default) + const getAuthHeader = createGetAuthHeaderByURI(opts.configByUri) - const { resolve, clearCache: clearResolutionCache } = _createResolver(fetchFromRegistry, getAuthHeader, { ...opts, customResolvers: opts.customResolvers }) - const verifyResolution = createResolutionVerifier(fetchFromRegistry, opts) + // One per-install LRU shared with both the resolver's pickPackage + // pass and the verifier's lookup chain. When the resolver populates + // an entry for a given `name`, a later verify of the same name + // (e.g. the post-resolution gate, or a second `mutateModules` call + // in the same long-lived process) reuses it instead of re-fetching. + const metaCache = createDefaultPackageMetaCache() + const { resolve, clearCache: clearResolutionCache } = _createResolver(fetchFromRegistry, getAuthHeader, { ...opts, metaCache, customResolvers: opts.customResolvers }) return { fetchers: createFetchers(fetchFromRegistry, getAuthHeader, opts), resolve, clearResolutionCache, - verifyResolution, + resolutionVerifiers: createResolutionVerifiers(fetchFromRegistry, { ...opts, metaCache }), } } -export function createResolver (opts: Omit): { resolve: ResolveFunction, clearCache: () => void } { +export function createResolver (opts: Omit): { resolve: ResolveFunction, resolveLatest: ResolveLatestDispatcher, clearCache: () => void } { const fetchFromRegistry = createFetchFromRegistry(opts) - const getAuthHeader = createGetAuthHeaderByURI(opts.configByUri, opts.registries?.default) + const getAuthHeader = createGetAuthHeaderByURI(opts.configByUri) return _createResolver(fetchFromRegistry, getAuthHeader, { ...opts, customResolvers: opts.customResolvers }) } +/** + * Wraps a `ResolveFunction` so any inline policy violation surfaced by + * the resolver is rethrown as a `PnpmError` instead of being returned on + * the result. Use this from one-shot callers (dlx, self-update) that + * have nowhere to defer a violation to — the install command leaves + * resolution unwrapped because it aggregates violations across the + * whole tree before deciding what to do. + * + * The error mapping is centralized here so future violation codes + * (today: `MINIMUM_RELEASE_AGE_VIOLATION`) get a consistent error code + * across every strict-mode caller without each call site re-translating. + */ +export function makeResolutionStrict (resolve: ResolveFunction): ResolveFunction { + return (async (wantedDependency, opts) => { + const result = await resolve(wantedDependency, opts) + if (result?.policyViolation) { + throw policyViolationToError(result.policyViolation) + } + return result + }) as ResolveFunction +} + +function policyViolationToError (violation: ResolutionPolicyViolation): PnpmError { + const message = `${violation.name}@${violation.version} ${violation.reason}` + // Map the per-violation `code` to the user-facing PnpmError code that + // pre-refactor callers (and `default-reporter`) already recognize. + // Future violation codes get their mapping added here so call sites + // don't have to re-translate. + const errorCode = violation.code === MINIMUM_RELEASE_AGE_VIOLATION_CODE + ? 'NO_MATURE_MATCHING_VERSION' + : violation.code + return new PnpmError(errorCode, message) +} + type Fetchers = { git: GitFetcher directory: DirectoryFetcher diff --git a/installing/client/tsconfig.json b/installing/client/tsconfig.json index 1f246c1ebe..b09f94f5d1 100644 --- a/installing/client/tsconfig.json +++ b/installing/client/tsconfig.json @@ -9,6 +9,9 @@ "../../__typings__/**/*.d.ts" ], "references": [ + { + "path": "../../core/error" + }, { "path": "../../core/types" }, @@ -45,6 +48,9 @@ { "path": "../../resolving/default-resolver" }, + { + "path": "../../resolving/npm-resolver" + }, { "path": "../../resolving/resolver-base" }, diff --git a/installing/commands/CHANGELOG.md b/installing/commands/CHANGELOG.md index c87559231a..86b42dab13 100644 --- a/installing/commands/CHANGELOG.md +++ b/installing/commands/CHANGELOG.md @@ -1,5 +1,227 @@ # @pnpm/plugin-commands-installation +## 1100.6.0 + +### Minor Changes + +- aa6149d: Treat tarball-integrity mismatches against the lockfile as a hard failure by default. Previously, `pnpm install` (non-frozen) would log `ERR_PNPM_TARBALL_INTEGRITY`, silently re-resolve from the registry, and overwrite the locked integrity — which meant a compromised registry, proxy, or republished version could substitute attacker-controlled content on a clean machine even though the project shipped a committed lockfile. + + `pnpm install` now exits with `ERR_PNPM_TARBALL_INTEGRITY` and a hint pointing at the new opt-in flag. + + The only opt-in is **`pnpm install --update-checksums`** — narrowly scoped to refreshing the locked integrity values from what the registry currently serves. Mirrors yarn's flag of the same name. A warning still prints when the bypass takes effect so the operation is auditable. + + `--force` and `pnpm update` deliberately do **not** bypass the integrity check. They are routine refresh operations; silently overwriting a locked integrity in those flows would erase the protection a committed lockfile is supposed to provide. `--frozen-lockfile` behavior is unchanged. `--fix-lockfile` keeps its documented purpose (filling in missing lockfile entries) and is also not a bypass. + +### Patch Changes + +- 572842a: Improve the log message that pnpm prints after auto-adding entries to `minimumReleaseAgeExclude` when `minimumReleaseAge` is set without `minimumReleaseAgeStrict`. The message previously referred to the internal "loose mode" terminology, which wasn't searchable in the docs; it now tells the user to set `minimumReleaseAgeStrict` to `true` if they want these updates gated behind a prompt instead [#11747](https://github.com/pnpm/pnpm/issues/11747). +- Updated dependencies [e8b3ae1] +- Updated dependencies [a23956e] +- Updated dependencies [aa6149d] +- Updated dependencies [a456dc7] +- Updated dependencies [35d2355] +- Updated dependencies [440e155] +- Updated dependencies [0721d64] + - @pnpm/workspace.projects-reader@1101.0.8 + - @pnpm/config.reader@1101.4.1 + - @pnpm/installing.deps-installer@1101.5.0 + - @pnpm/workspace.project-manifest-reader@1100.0.9 + - @pnpm/types@1101.2.0 + - @pnpm/global.commands@1100.0.22 + - @pnpm/resolving.npm-resolver@1101.3.3 + - @pnpm/deps.status@1100.0.19 + - @pnpm/workspace.projects-filter@1100.0.16 + - @pnpm/workspace.workspace-manifest-writer@1100.0.10 + - @pnpm/building.after-install@1101.0.17 + - @pnpm/store.connection-manager@1100.2.4 + - @pnpm/workspace.state@1100.0.16 + - @pnpm/installing.env-installer@1101.1.3 + - @pnpm/cli.utils@1101.0.8 + - @pnpm/deps.inspection.outdated@1100.1.3 + - @pnpm/config.pick-registry-for-package@1100.0.6 + - @pnpm/config.writer@1100.0.10 + - @pnpm/deps.path@1100.0.5 + - @pnpm/hooks.pnpmfile@1100.0.11 + - @pnpm/installing.context@1100.0.13 + - @pnpm/installing.dedupe.check@1100.0.8 + - @pnpm/lockfile.types@1100.0.8 + - @pnpm/pkg-manifest.reader@1100.0.5 + - @pnpm/pkg-manifest.utils@1100.2.1 + - @pnpm/resolving.resolver-base@1100.3.1 + - @pnpm/store.controller@1101.0.9 + - @pnpm/workspace.project-manifest-writer@1100.0.5 + - @pnpm/workspace.projects-graph@1100.0.13 + - @pnpm/workspace.projects-sorter@1100.0.4 + +## 1100.5.0 + +### Minor Changes + +- 212315d: Added a new setting `trustLockfile`. When `true`, `pnpm install` skips the supply-chain verification pass that re-applies `minimumReleaseAge` / `trustPolicy='no-downgrade'` to every entry in the loaded lockfile. The install treats the lockfile as already-trusted — useful for closed-source projects where every commit comes from a trusted author, or for CI runs against an already-verified lockfile. Defaults to `false`; verification stays on by default. Set in `pnpm-workspace.yaml`. + + Also cut the memory footprint of the verification pass itself: the per-(registry, name) trust-meta cache previously retained the full packument — dependency graphs, scripts, README, and per-version manifests — for the entire install. On large workspaces (`~4k` lockfile entries with `minimumReleaseAge` + `trustPolicy: no-downgrade` enabled) this could OOM CI runners with a 2GB heap cap. The cache now stores only the fields the trust check actually reads (`time`, per-version `_npmUser.trustedPublisher`, `dist.attestations.provenance`). The abbreviated-metadata cache is similarly projected to just the package-level `modified` field and the set of currently-listed version names. Fixes [#11860](https://github.com/pnpm/pnpm/issues/11860). + +### Patch Changes + +- Updated dependencies [d7da112] +- Updated dependencies [155af87] +- Updated dependencies [3b62f9d] +- Updated dependencies [212315d] + - @pnpm/workspace.project-manifest-reader@1100.0.8 + - @pnpm/installing.env-installer@1101.1.2 + - @pnpm/config.reader@1101.4.0 + - @pnpm/installing.deps-installer@1101.4.0 + - @pnpm/resolving.npm-resolver@1101.3.2 + - @pnpm/cli.utils@1101.0.7 + - @pnpm/workspace.projects-reader@1101.0.7 + - @pnpm/building.after-install@1101.0.16 + - @pnpm/deps.status@1100.0.18 + - @pnpm/global.commands@1100.0.21 + - @pnpm/store.connection-manager@1100.2.3 + - @pnpm/workspace.state@1100.0.15 + - @pnpm/deps.inspection.outdated@1100.1.2 + - @pnpm/workspace.projects-graph@1100.0.12 + - @pnpm/workspace.projects-filter@1100.0.15 + - @pnpm/workspace.workspace-manifest-writer@1100.0.9 + - @pnpm/store.controller@1101.0.8 + +## 1100.4.2 + +### Patch Changes + +- 881a865: When the install engine is delegated to pacquet via `configDependencies`, the user's CLI flags passed to `pnpm install` (e.g. `--no-runtime`, `--prod`, `--dev`, `--no-optional`, `--node-linker`, `--cpu`/`--os`/`--libc`, `--offline`, `--prefer-offline`) are now forwarded to pacquet's `install` subcommand verbatim. Previously pacquet was invoked with a fixed argument list, so flags like `--no-runtime` were silently dropped. Flag forwarding is gated on the command being `install`/`i`; `add`, `update`, and `dedupe` still don't forward (their flag surface doesn't line up with pacquet's `install`). + +## 1100.4.1 + +### Patch Changes + +- Updated dependencies [2061c55] +- Updated dependencies [097983f] +- Updated dependencies [e5e7b72] + - @pnpm/installing.env-installer@1101.1.1 + - @pnpm/config.pick-registry-for-package@1100.0.5 + - @pnpm/resolving.npm-resolver@1101.3.1 + - @pnpm/deps.inspection.outdated@1100.1.1 + - @pnpm/workspace.projects-graph@1100.0.11 + - @pnpm/store.connection-manager@1100.2.2 + - @pnpm/store.controller@1101.0.8 + - @pnpm/installing.deps-installer@1101.3.1 + - @pnpm/workspace.projects-filter@1100.0.14 + - @pnpm/building.after-install@1101.0.15 + - @pnpm/global.commands@1100.0.20 + +## 1100.4.0 + +### Minor Changes + +- b206a15: Adding [`pacquet`](https://github.com/pnpm/pnpm/tree/main/pacquet) (the Rust port of pnpm) to `configDependencies` in `pnpm-workspace.yaml` now delegates the materialization phase of `pnpm install` to the pacquet binary instead of running the JS installer's headless path. Pacquet emits the same `pnpm:*` NDJSON log events that `@pnpm/cli.default-reporter` already parses, so the install renders identically. Absent the `pacquet` entry, behavior is unchanged. + + ```yaml + # pnpm-workspace.yaml + configDependencies: + pacquet: "^0.1.0" + ``` + + Pacquet takes over every place pnpm would otherwise call `headlessInstall`: the frozen-install path, the hoisted-`nodeLinker` install, the workspace partial-install (where pnpm runs a `lockfileOnly` resolve pass first), and the agent-server install. In all cases pnpm still owns dependency resolution; pacquet only fetches and imports from the freshly-written lockfile. This is an opt-in preview of the Rust install engine [#11723](https://github.com/pnpm/pnpm/issues/11723). + +### Patch Changes + +- a620557: Fix global add/update to handle minimumReleaseAge policy violations instead of surfacing an internal resolver guardrail error. +- Updated dependencies [3687b0e] +- Updated dependencies [c8d8fde] +- Updated dependencies [ced20cb] +- Updated dependencies [a620557] +- Updated dependencies [d1b340f] +- Updated dependencies [3a54205] +- Updated dependencies [1627943] +- Updated dependencies [b206a15] +- Updated dependencies [64afc92] + - @pnpm/config.reader@1101.3.3 + - @pnpm/installing.env-installer@1101.1.0 + - @pnpm/global.commands@1100.0.19 + - @pnpm/resolving.npm-resolver@1101.3.0 + - @pnpm/deps.inspection.outdated@1100.1.0 + - @pnpm/resolving.resolver-base@1100.3.0 + - @pnpm/pkg-manifest.utils@1100.2.0 + - @pnpm/installing.deps-installer@1101.3.0 + - @pnpm/types@1101.1.1 + - @pnpm/building.after-install@1101.0.14 + - @pnpm/deps.status@1100.0.17 + - @pnpm/store.connection-manager@1100.2.1 + - @pnpm/workspace.state@1100.0.14 + - @pnpm/installing.context@1100.0.12 + - @pnpm/workspace.projects-graph@1100.0.10 + - @pnpm/store.controller@1101.0.8 + - @pnpm/lockfile.types@1100.0.7 + - @pnpm/cli.utils@1101.0.6 + - @pnpm/workspace.project-manifest-reader@1100.0.7 + - @pnpm/config.pick-registry-for-package@1100.0.4 + - @pnpm/config.writer@1100.0.9 + - @pnpm/deps.path@1100.0.4 + - @pnpm/hooks.pnpmfile@1100.0.10 + - @pnpm/installing.dedupe.check@1100.0.7 + - @pnpm/pkg-manifest.reader@1100.0.4 + - @pnpm/workspace.project-manifest-writer@1100.0.4 + - @pnpm/workspace.projects-filter@1100.0.13 + - @pnpm/workspace.projects-reader@1101.0.6 + - @pnpm/workspace.projects-sorter@1100.0.3 + - @pnpm/workspace.workspace-manifest-writer@1100.0.9 + +## 1100.3.0 + +### Minor Changes + +- 4195766: Tightened the `minimumReleaseAge` story so the bypass becomes explicit on disk instead of silent, and removed the discover-by-loop dance for strict-mode users: + + 1. Fresh resolutions in loose mode (`minimumReleaseAgeStrict: false`) that fall back to a version newer than the cutoff auto-collect the picked `name@version` into the workspace manifest's `minimumReleaseAgeExclude`. A single info message lists the additions; entries already on the list are left alone. + 2. The post-resolution lockfile verifier introduced in #11583 now runs in loose mode too — every accepted-immature pin must be on `minimumReleaseAgeExclude`, just like strict mode requires. A lockfile produced under a weaker (or absent) policy that still has immature entries is rejected the same way strict mode would reject it. + 3. **Strict mode (interactive)** no longer aborts on the first immature pick. The resolver gathers every immature direct _and_ transitive in one pass; before peer-dependency resolution runs, pnpm prompts the user with the full list and asks whether to add them all to `minimumReleaseAgeExclude` and proceed. Approve → install continues and the workspace manifest is written at the end. Decline → resolution aborts before the lockfile or package.json is touched (tarballs already in the store stay, since the store is idempotent). This closes the [#10488](https://github.com/pnpm/pnpm/issues/10488) loop where security bumps to packages with platform-specific transitives (e.g. `next` + the `@next/swc-*` shims) made users re-run `pnpm add` once per transitive. + 4. **Strict mode (non-interactive / CI)** now aborts with the full immature set in the error message instead of the first pick. The resolver always collects every immature direct + transitive; the install command then throws `ERR_PNPM_NO_MATURE_MATCHING_VERSION` listing each entry's `name@version` and publish time. Deterministic CI behavior is preserved (same exit code, same error code), but the error pinpoints every offending entry instead of forcing the discover-by-loop dance. The expected workflow is interactive approval locally → the lockfile + workspace manifest get committed → CI runs cleanly against the populated exclude list. + + 5. **The lockfile verifier now also covers `trustPolicy: 'no-downgrade'`.** The same post-resolution gate that re-checks `minimumReleaseAge` on lockfile entries now re-runs `failIfTrustDowngraded` for every npm-registry entry whose name isn't on `trustPolicyExclude`. The two checks share a single full-metadata fetch per package, so the extra coverage doesn't cost an extra round trip when both policies are active. Resolver-time trust checks still run as before — this just closes the gap when an entry bypasses resolution (peek path, `--frozen-lockfile`, restored CI cache). + + Pacquet parity: not ported — pacquet's `minimumReleaseAge` policy is itself only stubbed today (see `pacquet/crates/package-manager/src/version_policy.rs`). The auto-exclude, loose-mode verifier, prompt, and the new trust-policy verifier check will travel with the broader policy port whenever that happens. + +### Patch Changes + +- Updated dependencies [963861c] +- Updated dependencies [4195766] +- Updated dependencies [31538bf] +- Updated dependencies [020ac45] +- Updated dependencies [b6e2c8c] +- Updated dependencies [d3f8408] +- Updated dependencies [3ddde2b] +- Updated dependencies [4a79336] +- Updated dependencies [a62f959] +- Updated dependencies [ba2c884] +- Updated dependencies [2a9bd89] +- Updated dependencies [31538bf] +- Updated dependencies [8df408c] + - @pnpm/resolving.npm-resolver@1101.2.0 + - @pnpm/resolving.resolver-base@1100.2.0 + - @pnpm/installing.deps-installer@1101.2.0 + - @pnpm/store.connection-manager@1100.2.0 + - @pnpm/deps.inspection.outdated@1100.0.16 + - @pnpm/config.reader@1101.3.2 + - @pnpm/building.after-install@1101.0.13 + - @pnpm/installing.env-installer@1101.0.10 + - @pnpm/workspace.projects-graph@1100.0.9 + - @pnpm/deps.status@1100.0.16 + - @pnpm/installing.context@1100.0.11 + - @pnpm/lockfile.types@1100.0.6 + - @pnpm/store.controller@1101.0.7 + - @pnpm/hooks.pnpmfile@1100.0.9 + - @pnpm/global.commands@1100.0.18 + - @pnpm/workspace.state@1100.0.13 + - @pnpm/pkg-manifest.utils@1100.1.4 + - @pnpm/workspace.projects-filter@1100.0.12 + - @pnpm/installing.dedupe.check@1100.0.6 + - @pnpm/workspace.workspace-manifest-writer@1100.0.8 + - @pnpm/cli.utils@1101.0.5 + - @pnpm/workspace.project-manifest-reader@1100.0.6 + - @pnpm/config.writer@1100.0.8 + - @pnpm/workspace.projects-reader@1101.0.5 + ## 1100.2.2 ### Patch Changes diff --git a/installing/commands/package.json b/installing/commands/package.json index f9d418f746..71e32f58da 100644 --- a/installing/commands/package.json +++ b/installing/commands/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/installing.commands", - "version": "1100.2.2", + "version": "1100.6.0", "description": "Commands for installation", "keywords": [ "pnpm", @@ -58,6 +58,7 @@ "@pnpm/lockfile.types": "workspace:*", "@pnpm/pkg-manifest.reader": "workspace:*", "@pnpm/pkg-manifest.utils": "workspace:*", + "@pnpm/resolving.npm-resolver": "workspace:*", "@pnpm/resolving.parse-wanted-dependency": "workspace:*", "@pnpm/resolving.resolver-base": "workspace:*", "@pnpm/semver-diff": "catalog:", @@ -80,6 +81,7 @@ "@zkochan/rimraf": "catalog:", "@zkochan/table": "catalog:", "chalk": "catalog:", + "ci-info": "catalog:", "enquirer": "catalog:", "get-npm-tarball-url": "catalog:", "is-subdir": "catalog:", @@ -107,6 +109,7 @@ "@pnpm/test-ipc-server": "workspace:*", "@pnpm/testing.command-defaults": "workspace:*", "@pnpm/testing.mock-agent": "workspace:*", + "@pnpm/testing.registry-mock": "workspace:*", "@pnpm/worker": "workspace:*", "@pnpm/workspace.projects-filter": "workspace:*", "@types/normalize-path": "catalog:", @@ -114,7 +117,6 @@ "@types/ramda": "catalog:", "@types/yarnpkg__lockfile": "catalog:", "@types/zkochan__table": "catalog:", - "ci-info": "catalog:", "delay": "catalog:", "jest-diff": "catalog:", "path-name": "catalog:", diff --git a/installing/commands/src/add.ts b/installing/commands/src/add.ts index 46e1364bfe..afbaffa033 100644 --- a/installing/commands/src/add.ts +++ b/installing/commands/src/add.ts @@ -13,6 +13,7 @@ import { renderHelp } from 'render-help' import { getFetchFullMetadata } from './getFetchFullMetadata.js' import type { InstallCommandOptions } from './install.js' import { installDeps } from './installDeps.js' +import { createGlobalPolicyCallbacks } from './resolutionPolicyManifest.js' export const shorthands: Record = { 'save-catalog': '--save-catalog-name=default', @@ -78,6 +79,7 @@ export function rcOptionsTypes (): Record { 'side-effects-cache', 'store-dir', 'strict-peer-dependencies', + 'trust-lockfile', 'trust-policy', 'trust-policy-exclude', 'trust-policy-ignore-after', @@ -257,7 +259,10 @@ export async function handler ( if (params.includes('pnpm') || params.includes('@pnpm/exe')) { throw new PnpmError('GLOBAL_PNPM_INSTALL', 'Use the "pnpm self-update" command to install or update pnpm') } - return handleGlobalAdd(opts, params, commands ?? {}) + return handleGlobalAdd({ + ...opts, + ...createGlobalPolicyCallbacks(opts), + }, params, commands ?? {}) } const include = { diff --git a/installing/commands/src/fetch.ts b/installing/commands/src/fetch.ts index b6209e3141..fd86cc1d9d 100644 --- a/installing/commands/src/fetch.ts +++ b/installing/commands/src/fetch.ts @@ -70,7 +70,7 @@ export async function handler (opts: FetchCommandOptions): Promise { pruneStore: true, storeController: store.ctrl, storeDir: store.dir, - verifyResolution: store.verifyResolution, + resolutionVerifiers: store.resolutionVerifiers, // Hoisting is skipped anyway, // so we store these empty patterns in node_modules/.modules.yaml // to let the subsequent install know that hoisting should be performed. diff --git a/installing/commands/src/import/index.ts b/installing/commands/src/import/index.ts index d249d2b738..27cb169b9d 100644 --- a/installing/commands/src/import/index.ts +++ b/installing/commands/src/import/index.ts @@ -186,7 +186,7 @@ export async function handler ( preferredVersions, storeController: store.ctrl, storeDir: store.dir, - verifyResolution: store.verifyResolution, + resolutionVerifiers: store.resolutionVerifiers, } await install(manifest, installOpts) } diff --git a/installing/commands/src/install.ts b/installing/commands/src/install.ts index 7e7ceea71d..05a4650bb6 100644 --- a/installing/commands/src/install.ts +++ b/installing/commands/src/install.ts @@ -65,6 +65,7 @@ export function rcOptionsTypes (): Record { 'side-effects-cache', 'store-dir', 'strict-peer-dependencies', + 'trust-lockfile', 'trust-policy', 'trust-policy-exclude', 'trust-policy-ignore-after', @@ -82,8 +83,14 @@ export const cliOptionsTypes = (): Record => ({ ...rcOptionsTypes(), ...pick(['force'], allTypes), 'fix-lockfile': Boolean, + 'update-checksums': Boolean, 'resolution-only': Boolean, recursive: Boolean, + // `--no-save` lets `pnpm install` skip writing to package.json / + // pnpm-workspace.yaml. Without registering it here, nopt drops the + // flag, `opts.save` stays undefined, and the auto-add path treats + // it as "save enabled". + save: Boolean, }) export const shorthands: Record = { @@ -161,6 +168,10 @@ For options that may be used with `-r`, see "pnpm help recursive"', description: 'Fix broken lockfile entries automatically', name: '--fix-lockfile', }, + { + description: 'Refresh integrity checksums recorded in the lockfile from the registry', + name: '--update-checksums', + }, { description: 'Merge lockfiles were generated on git branch', name: '--merge-git-branch-lockfiles', @@ -225,6 +236,10 @@ by any dependencies, so it is an emulation of a flat node_modules', description: 'Ignore trust downgrades for packages published more than specified minutes ago', name: '--trust-policy-ignore-after ', }, + { + description: 'Trust the lockfile and skip the supply-chain verification step that re-applies minimumReleaseAge / trustPolicy to each lockfile entry. Use only when the lockfile is part of the trusted base (closed-source projects, CI runs against an already-verified lockfile)', + name: '--trust-lockfile', + }, { description: 'Clones/hardlinks or copies packages. The selected method depends from the file system', name: '--package-import-method auto', @@ -319,6 +334,7 @@ export type InstallCommandOptions = Pick & CreateStoreControllerOptions & Partial> & { argv: { + cooked?: string[] original: string[] + remain?: string[] } fixLockfile?: boolean + updateChecksums?: boolean frozenLockfileIfExists?: boolean useBetaCli?: boolean pruneDirectDependencies?: boolean @@ -383,6 +402,7 @@ export async function handler (opts: InstallCommandOptions & { _calledFromLink?: include, includeDirect: include, fetchFullMetadata: getFetchFullMetadata(opts), + isInstallCommand: true, } if (opts.resolutionOnly) { installDepsOptions.lockfileOnly = true diff --git a/installing/commands/src/installDeps.ts b/installing/commands/src/installDeps.ts index fa109f5321..6aae361d39 100644 --- a/installing/commands/src/installDeps.ts +++ b/installing/commands/src/installDeps.ts @@ -40,6 +40,7 @@ import { updateWorkspaceManifest } from '@pnpm/workspace.workspace-manifest-writ import { getPinnedVersion } from './getPinnedVersion.js' import { getSaveType } from './getSaveType.js' import { handleIgnoredBuilds } from './handleIgnoredBuilds.js' +import { setupPolicyHandlers } from './policyHandlers.js' import { type CommandFullName, createMatcher, @@ -49,6 +50,7 @@ import { type RecursiveOptions, type UpdateDepsMatcher, } from './recursive.js' +import { makeRunPacquet } from './runPacquet.js' import { createWorkspaceSpecs, updateToWorkspacePackagesFromManifest } from './updateWorkspaceDependencies.js' const OVERWRITE_UPDATE_OPTIONS = { @@ -103,6 +105,7 @@ export type InstallDepsOptions = Pick & CreateStoreControllerOptions & { +> & Partial> +& CreateStoreControllerOptions & { argv: { + cooked?: string[] original: string[] + remain?: string[] } allowNew?: boolean forceFullResolution?: boolean @@ -157,6 +163,13 @@ export type InstallDepsOptions = Pick> export async function installDeps ( @@ -193,6 +206,30 @@ export async function installDeps ( opts['preserveWorkspaceProtocol'] = !opts.linkWorkspacePackages } const store = await createStoreController(opts) + // When `configDependencies` declares pacquet, build the alternative + // install engine the deps-installer delegates to. The CLI layer owns + // the construction so the installer doesn't need to know about + // pacquet's binary path, CLI surface, or any settings that only + // pacquet consumes. Threaded through both the workspace recursive + // path and the single-project path below. Two declaration names are + // accepted: the original unscoped `pacquet` and the official scoped + // `@pnpm/pacquet` mirror. Both packages ship the same JS shim and + // optional `@pacquet/-` binary sub-packages, so the + // resolved \`node_modules/.pnpm-config/\` layout pacquet's + // wrapper expects is identical either way. + const pacquetConfigDepName = opts.configDependencies?.['@pnpm/pacquet'] != null + ? '@pnpm/pacquet' + : opts.configDependencies?.pacquet != null + ? 'pacquet' + : undefined + const runPacquet = pacquetConfigDepName != null + ? makeRunPacquet({ + lockfileDir: opts.lockfileDir ?? opts.dir, + packageName: pacquetConfigDepName, + argv: { original: opts.argv.original, remain: opts.argv.remain ?? [] }, + isInstallCommand: opts.isInstallCommand === true, + }) + : undefined const includeDirect = opts.includeDirect ?? { dependencies: true, devDependencies: true, @@ -241,6 +278,7 @@ export async function installDeps ( selectedProjectsGraph, storeControllerAndDir: store, workspaceDir: opts.workspaceDir, + runPacquet, }, opts.update ? 'update' : (params.length === 0 ? 'install' : 'add') ) @@ -267,6 +305,13 @@ export async function installDeps ( applyRuntimeOnFailOverride(manifest, opts.runtimeOnFail) } + // `setupPolicyHandlers` composes the per-policy handlers the install + // needs for the current opts (today: minimumReleaseAge; future: + // trustPolicy UX, license policy, etc.). Returns `undefined` when no + // handler is active so the install skips the empty no-op call at + // every checkpoint when no policies are configured. + const policyHandlers = setupPolicyHandlers(opts) + const installOpts: Omit = { ...opts, // In case installation is done in a multi-package repository @@ -279,9 +324,11 @@ export async function installDeps ( skipRuntimes: opts.runtime === false, storeController: store.ctrl, storeDir: store.dir, - verifyResolution: store.verifyResolution, + resolutionVerifiers: store.resolutionVerifiers, workspacePackages, preferredVersions: opts.packageVulnerabilityAudit ? preferNonvulnerablePackageVersions(opts.packageVulnerabilityAudit) : undefined, + handleResolutionPolicyViolations: policyHandlers?.handleResolutionPolicyViolations, + runPacquet, } let updateMatch: UpdateDepsMatcher | null @@ -340,14 +387,20 @@ export async function installDeps ( rootDir: opts.dir as ProjectRootDir, targetDependenciesField: getSaveType(opts), } - const { updatedCatalogs, updatedProject, ignoredBuilds } = await mutateModulesInSingleProject(mutatedProject, installOpts) + const { updatedCatalogs, updatedProject, ignoredBuilds, resolutionPolicyViolations } = await mutateModulesInSingleProject(mutatedProject, installOpts) if (opts.save !== false) { + // Only pick entries when we'll actually persist. Otherwise the + // info log would claim we added entries the workspace manifest + // never saw, and the next install would re-prompt or fail + // verification. + const policyUpdates = policyHandlers?.pickManifestUpdates(resolutionPolicyViolations) await Promise.all([ writeProjectManifest(updatedProject.manifest), updateWorkspaceManifest(opts.workspaceDir ?? opts.dir, { updatedCatalogs, cleanupUnusedCatalogs: opts.cleanupUnusedCatalogs, allProjects: opts.allProjects, + ...policyUpdates, }), ]) } @@ -365,20 +418,34 @@ export async function installDeps ( return } - const { updatedCatalogs, updatedManifest, ignoredBuilds } = await install(manifest, { + const { updatedCatalogs, updatedManifest, ignoredBuilds, resolutionPolicyViolations } = await install(manifest, { ...installOpts, updatePackageManifest, updateMatching, }) - if (opts.update === true && opts.save !== false) { - await Promise.all([ - writeProjectManifest(updatedManifest), - updateWorkspaceManifest(opts.workspaceDir ?? opts.dir, { - updatedCatalogs, - cleanupUnusedCatalogs: opts.cleanupUnusedCatalogs, - allProjects, - }), - ]) + // `opts.save === false` (e.g. `--no-save`) means "don't persist anything + // from this install" — both package.json and the workspace manifest. + // Skip the pick so the info log doesn't claim entries were added that + // were never written; the next install will resurface them. + if (opts.save !== false) { + const policyUpdates = policyHandlers?.pickManifestUpdates(resolutionPolicyViolations) + if (opts.update === true) { + await Promise.all([ + writeProjectManifest(updatedManifest), + updateWorkspaceManifest(opts.workspaceDir ?? opts.dir, { + updatedCatalogs, + cleanupUnusedCatalogs: opts.cleanupUnusedCatalogs, + allProjects, + ...policyUpdates, + }), + ]) + } else if (policyUpdates != null) { + // Plain `pnpm install` (no --update, no params) wouldn't otherwise touch + // the workspace manifest. Persist the auto-policy patches anyway so any + // loose bypass (today: minimumReleaseAgeExclude) remains explicit on + // subsequent installs. + await updateWorkspaceManifest(opts.workspaceDir ?? opts.dir, policyUpdates) + } } await handleIgnoredBuilds(opts, ignoredBuilds) @@ -398,6 +465,7 @@ export async function installDeps ( allProjectsGraph: opts.allProjectsGraph!, selectedProjectsGraph, workspaceDir: opts.workspaceDir, // Otherwise TypeScript doesn't understand that is not undefined + runPacquet, }, 'install') if (opts.ignoreScripts) return diff --git a/installing/commands/src/policyHandlers.ts b/installing/commands/src/policyHandlers.ts new file mode 100644 index 0000000000..e2aed18bfa --- /dev/null +++ b/installing/commands/src/policyHandlers.ts @@ -0,0 +1,260 @@ +import { PnpmError } from '@pnpm/error' +import { globalInfo } from '@pnpm/logger' +import { MINIMUM_RELEASE_AGE_VIOLATION_CODE } from '@pnpm/resolving.npm-resolver' +import { isCI } from 'ci-info' +import enquirer from 'enquirer' + +/** + * Shape returned by `installing/deps-installer`'s + * `collectResolutionPolicyViolations` and the inline accumulator on + * the resolveDependencies result. Re-declared locally so the commands + * layer can react without depending on the deps-installer's private + * install types. + * + * Verifier codes (today: `MINIMUM_RELEASE_AGE_VIOLATION` and + * `TRUST_DOWNGRADE`) are the contract surface for downstream UX. + * Each `PolicyHandler` below filters violations by code to decide + * what to do with them (prompt, persist to an exclude list, log, + * abort). + */ +export interface PolicyViolation { + name: string + version: string + code: string + reason: string +} + +/** + * Workspace-manifest patch a per-policy handler can request. Each + * field maps to a `pnpm-workspace.yaml` exclude-list array; the + * install command forwards these to `updateWorkspaceManifest` so the + * workspace writer dedupes and appends them in one pass. + * + * New policies that want auto-persistence add their field here AND + * teach `updateWorkspaceManifest` how to honor it. + */ +export interface WorkspaceManifestPolicyUpdates { + addedMinimumReleaseAgeExcludes?: string[] +} + +/** + * What the install command asks of each registered policy handler. + * Both hooks are optional — a handler that only wants to abort can + * skip `pickManifestUpdates`; a handler that only wants to persist + * can skip `handleResolutionPolicyViolations`. + */ +interface PolicyHandler { + /** + * Runs between `resolveDependencyTree` and `resolvePeers`. Throw to + * abort the install before any lockfile / package.json / + * modules-dir mutation. Receives the full violations list across + * every policy — handlers filter by `code` for their own. + */ + handleResolutionPolicyViolations?: (violations: readonly PolicyViolation[]) => Promise + /** + * Called at the install's tail to assemble the workspace-manifest + * patch. Returns `undefined` (or an empty object) when this + * handler has nothing to persist for the current batch. + */ + pickManifestUpdates?: (violations: readonly PolicyViolation[]) => WorkspaceManifestPolicyUpdates | undefined +} + +/** + * Aggregated plan the install command consumes. The `handleResolutionPolicyViolations` + * call fans out across every registered handler in registration order; + * any handler can throw to abort. `pickManifestUpdates` merges the + * per-handler patches into one bag so the workspace writer runs once. + */ +export interface PolicyHandlersPlan { + handleResolutionPolicyViolations: (violations: readonly PolicyViolation[]) => Promise + pickManifestUpdates: (violations: readonly PolicyViolation[]) => WorkspaceManifestPolicyUpdates | undefined +} + +export interface PolicyHandlersOptions { + minimumReleaseAge?: number + minimumReleaseAgeStrict?: boolean + /** + * Pass `false` for `--no-save` installs. Handlers that would + * persist to the workspace manifest refuse to enter modes where + * approval is durably required (today: strict minimumReleaseAge) + * so the prompt never offers an action it can't honor. + */ + save?: boolean + /** + * Override for CI detection. Defaults to `ci-info`'s `isCI` flag. + */ + ci?: boolean +} + +/** + * Composes the per-policy handlers the install command needs for the + * current opts. Returns `undefined` only when no handler reports + * activity — saves the install command an empty no-op call at every + * checkpoint when no policies are configured. + * + * Today only the minimumReleaseAge handler is registered. Future + * policies (trustPolicy UX, license policy, etc.) plug in by + * exporting a sibling `createPolicyHandler(opts)` and getting + * pushed into the `handlers` list below. + */ +export function setupPolicyHandlers (opts: PolicyHandlersOptions): PolicyHandlersPlan | undefined { + const handlers: PolicyHandler[] = [] + const minimumReleaseAge = createMinimumReleaseAgeHandler(opts) + if (minimumReleaseAge) handlers.push(minimumReleaseAge) + + if (handlers.length === 0) return undefined + + return { + handleResolutionPolicyViolations: async (violations) => { + // Sequential, not parallel: a TTY prompt from handler N would + // race with a different prompt from N+1, and we want a clean + // throw to short-circuit before later handlers ask for input. + for (const handler of handlers) { + if (handler.handleResolutionPolicyViolations) { + // eslint-disable-next-line no-await-in-loop + await handler.handleResolutionPolicyViolations(violations) + } + } + }, + pickManifestUpdates: (violations) => { + const merged: WorkspaceManifestPolicyUpdates = {} + let any = false + for (const handler of handlers) { + if (!handler.pickManifestUpdates) continue + const patch = handler.pickManifestUpdates(violations) + if (patch == null) continue + // Shallow merge — handlers own disjoint fields by convention, + // so there's no collision policy to encode here yet. + for (const [key, value] of Object.entries(patch)) { + if (value == null) continue + ;(merged as Record)[key] = value + any = true + } + } + return any ? merged : undefined + }, + } +} + +/** + * minimumReleaseAge policy handler. + * + * Loose mode (`minimumReleaseAgeStrict: false`) lets the resolver + * install versions newer than the cutoff and auto-persists them to + * `minimumReleaseAgeExclude`. Strict mode + an interactive TTY + * surfaces the full set of immature picks (direct AND transitive) at + * once via a confirm prompt — the install proceeds if the user + * approves, otherwise it aborts before touching the lockfile or + * package.json (#10488). Strict mode in CI or any other non-TTY + * context aborts hard with the same violation list so the failure + * pinpoints every offending entry, not just the first one the + * resolver picked. + * + * Strict mode combined with `--no-save` is rejected up-front — the + * approval prompt promises persistence the install command's + * `opts.save !== false` gate would block, leaving the lockfile + * holding approved-but-unlisted immature picks that the next install + * would reject. + * + * Returns `undefined` when minimumReleaseAge is not active. + */ +function createMinimumReleaseAgeHandler (opts: PolicyHandlersOptions): PolicyHandler | undefined { + if (!opts.minimumReleaseAge) return undefined + const strictMode = opts.minimumReleaseAgeStrict === true + const persistenceEnabled = opts.save !== false + const inCi = opts.ci ?? isCI + const canPrompt = !inCi && Boolean(process.stdin.isTTY) + + return { + handleResolutionPolicyViolations: async (violations) => { + if (!strictMode) return + const immature = filterImmatureViolations(violations) + if (immature.length === 0) return + if (!persistenceEnabled) { + throw new PnpmError( + 'STRICT_MIN_RELEASE_AGE_REQUIRES_SAVE', + 'minimumReleaseAgeStrict cannot be combined with --no-save: ' + + 'approval would require writing to minimumReleaseAgeExclude in pnpm-workspace.yaml, ' + + 'which --no-save prevents.', + { + hint: 'Drop --no-save so the exclude list can be persisted, or set ' + + 'minimumReleaseAgeStrict: false to let the install proceed without prompting ' + + '(the lockfile would still trigger the auto-collect on the next normal install).', + } + ) + } + if (canPrompt) { + await promptForApproval(immature) + } else { + throw failOnImmature(immature) + } + }, + pickManifestUpdates: (violations) => { + const entries = pickImmatureEntries(violations, strictMode) + return entries ? { addedMinimumReleaseAgeExcludes: entries } : undefined + }, + } +} + +function filterImmatureViolations (violations: readonly PolicyViolation[]): PolicyViolation[] { + return violations.filter((v) => v.code === MINIMUM_RELEASE_AGE_VIOLATION_CODE) +} + +function pickImmatureEntries ( + violations: readonly PolicyViolation[], + promptRequired: boolean +): string[] | undefined { + const immature = filterImmatureViolations(violations) + if (immature.length === 0) return undefined + const sorted = [...new Set(immature.map((v) => `${v.name}@${v.version}`))].sort() + // Strict-mode picks already passed through the approval prompt, so + // the log here only confirms what was persisted. Loose-mode picks + // haven't been announced anywhere else, so the same log doubles as + // the discovery notice. + const reason = promptRequired + ? '(approved at the prompt)' + : '(set minimumReleaseAgeStrict to true to gate these updates with a prompt)' + globalInfo( + `Added ${sorted.length} ${sorted.length === 1 ? 'entry' : 'entries'} to minimumReleaseAgeExclude in pnpm-workspace.yaml ` + + `${reason}:\n ${sorted.join('\n ')}` + ) + return sorted +} + +function failOnImmature (immature: readonly PolicyViolation[]): PnpmError { + const sorted = [...immature].sort((a, b) => `${a.name}@${a.version}`.localeCompare(`${b.name}@${b.version}`)) + const list = sorted.map((v) => ` ${v.name}@${v.version} ${v.reason}`).join('\n') + return new PnpmError( + 'NO_MATURE_MATCHING_VERSION', + `${sorted.length} ${sorted.length === 1 ? 'version does' : 'versions do'} not meet the minimumReleaseAge constraint:\n${list}`, + { + hint: 'Run the install interactively to approve these picks, or add them to ' + + 'minimumReleaseAgeExclude in pnpm-workspace.yaml, or wait for the packages ' + + 'to mature past the configured cutoff.', + } + ) +} + +async function promptForApproval (immature: readonly PolicyViolation[]): Promise { + const sorted = [...immature].sort((a, b) => `${a.name}@${a.version}`.localeCompare(`${b.name}@${b.version}`)) + const message = + `${sorted.length} ${sorted.length === 1 ? 'version does' : 'versions do'} not meet the minimumReleaseAge constraint:\n` + + sorted.map((v) => ` ${v.name}@${v.version}`).join('\n') + '\n' + + 'Add to minimumReleaseAgeExclude in pnpm-workspace.yaml and proceed with the install?' + const answer = await enquirer.prompt<{ confirmed: boolean }>({ + type: 'confirm', + name: 'confirmed', + message, + initial: false, + }) + if (!answer.confirmed) { + throw new PnpmError( + 'MINIMUM_RELEASE_AGE_DENIED', + 'Aborted: the immature versions were not approved.', + { + hint: 'Re-run the install without `minimumReleaseAgeStrict: true` to allow these versions, ' + + 'or wait for the packages to mature past the configured cutoff.', + } + ) + } +} diff --git a/installing/commands/src/recursive.ts b/installing/commands/src/recursive.ts index afeb074ecf..dbff849d45 100755 --- a/installing/commands/src/recursive.ts +++ b/installing/commands/src/recursive.ts @@ -55,6 +55,7 @@ import pLimit from 'p-limit' import { getPinnedVersion } from './getPinnedVersion.js' import { getSaveType } from './getSaveType.js' import { handleIgnoredBuilds } from './handleIgnoredBuilds.js' +import { type PolicyViolation, setupPolicyHandlers } from './policyHandlers.js' import { createWorkspaceSpecs, updateToWorkspacePackagesFromManifest } from './updateWorkspaceDependencies.js' export type RecursiveOptions = CreateStoreControllerOptions & Pick Promise } & Partial< Pick> = mutatedPkgs.map(async ({ originalManifest, manifest, rootDir }) => { return manifestsByPath[rootDir].writeProjectManifest(originalManifest ?? manifest) }) @@ -309,6 +332,7 @@ export async function recursive ( updatedCatalogs, cleanupUnusedCatalogs: opts.cleanupUnusedCatalogs, allProjects, + ...policyUpdates, })) await Promise.all(promises) } @@ -321,6 +345,10 @@ export async function recursive ( let updatedCatalogs: Catalogs | undefined const allIgnoredBuilds = new Set() + // Each per-project install returns its own slice of lockfile-resolution + // violations; accumulate them here so the post-loop persist step can + // dedup and write a single batch to the workspace manifest. + const allResolutionPolicyViolations: PolicyViolation[] = [] const limitInstallation = pLimit(getWorkspaceConcurrency(opts.workspaceConcurrency)) await Promise.all(pkgPaths.map(async (rootDir) => limitInstallation(async () => { @@ -368,6 +396,7 @@ export async function recursive ( updatedCatalogs?: Catalogs updatedManifest: ProjectManifest ignoredBuilds: IgnoredBuilds | undefined + resolutionPolicyViolations?: PolicyViolation[] } type ActionFunction = (manifest: PackageManifest | ProjectManifest, opts: ActionOpts) => Promise @@ -387,6 +416,7 @@ export async function recursive ( updatedCatalogs: undefined, // there's no reason to add new or update catalogs on `pnpm remove` updatedManifest: mutationResult.updatedProjects[0].manifest, ignoredBuilds: mutationResult.ignoredBuilds, + resolutionPolicyViolations: mutationResult.resolutionPolicyViolations, } } break @@ -402,6 +432,7 @@ export async function recursive ( updatedCatalogs: newCatalogsAddition, updatedManifest: newManifest, ignoredBuilds, + resolutionPolicyViolations, } = await action( manifest, { @@ -418,7 +449,7 @@ export async function recursive ( }), configByUri: installOpts.configByUri, storeController: store.ctrl, - verifyResolution: store.verifyResolution, + resolutionVerifiers: store.resolutionVerifiers, } ) if (opts.save !== false) { @@ -433,6 +464,11 @@ export async function recursive ( allIgnoredBuilds.add(depPath) } } + if (resolutionPolicyViolations?.length) { + for (const violation of resolutionPolicyViolations) { + allResolutionPolicyViolations.push(violation) + } + } result[rootDir].status = 'passed' } catch (err: any) { // eslint-disable-line logger.info(err) @@ -453,11 +489,18 @@ export async function recursive ( }) )) await handleIgnoredBuilds(opts, allIgnoredBuilds.size ? allIgnoredBuilds : undefined) - await updateWorkspaceManifest(opts.workspaceDir, { - updatedCatalogs, - cleanupUnusedCatalogs: opts.cleanupUnusedCatalogs, - allProjects, - }) + if (opts.save !== false) { + // Only pick entries when we'll actually persist. Otherwise the + // info log would claim entries were added that the workspace + // manifest never saw, mirroring the gate the shared-lockfile + // branch + installDeps already apply. + await updateWorkspaceManifest(opts.workspaceDir, { + updatedCatalogs, + cleanupUnusedCatalogs: opts.cleanupUnusedCatalogs, + allProjects, + ...policyHandlers?.pickManifestUpdates(allResolutionPolicyViolations), + }) + } if ( !opts.lockfileOnly && !opts.ignoreScripts && ( diff --git a/installing/commands/src/remove.ts b/installing/commands/src/remove.ts index ac7367f9fe..907126d395 100644 --- a/installing/commands/src/remove.ts +++ b/installing/commands/src/remove.ts @@ -145,6 +145,7 @@ export async function handler ( | 'workspacePackagePatterns' | 'sharedWorkspaceLockfile' | 'cleanupUnusedCatalogs' + | 'trustLockfile' > & Pick Promise + updateResolutionPolicyManifest?: (violations: readonly PolicyViolation[], dir: string) => Promise +} + +export function createGlobalPolicyCallbacks (opts: PolicyHandlersOptions): GlobalPolicyCallbacks { + const policyHandlers = setupPolicyHandlers(opts) + if (policyHandlers == null) return {} + return { + handleResolutionPolicyViolations: policyHandlers.handleResolutionPolicyViolations, + updateResolutionPolicyManifest: async (violations, dir) => { + const policyUpdates = policyHandlers.pickManifestUpdates(violations) + if (policyUpdates != null) { + await updateWorkspaceManifest(dir, policyUpdates) + } + }, + } +} diff --git a/installing/commands/src/runPacquet.ts b/installing/commands/src/runPacquet.ts new file mode 100644 index 0000000000..8ab582fe00 --- /dev/null +++ b/installing/commands/src/runPacquet.ts @@ -0,0 +1,278 @@ +import { spawn } from 'node:child_process' +import fs from 'node:fs' +import { createRequire } from 'node:module' +import path from 'node:path' +import readline from 'node:readline' +import type { Writable } from 'node:stream' + +import { PnpmError } from '@pnpm/error' +import { logger, streamParser } from '@pnpm/logger' +import chalk from 'chalk' + +// The runtime `streamParser` is a `Transform` stream (split2 + JSON.parse). +// Its public typing only exposes `on`/`removeListener`, so we narrow to the +// writable side here to feed pacquet's NDJSON lines back through the same +// parser that `@pnpm/cli.default-reporter` listens on. +const streamParserWritable = streamParser as unknown as Writable + +export interface MakeRunPacquetOpts { + lockfileDir: string + /** + * Which `configDependencies` entry installed pacquet: either the + * original unscoped `pacquet` or the official scoped + * `@pnpm/pacquet` mirror. Drives the directory we look in under + * `node_modules/.pnpm-config//`. Both packages ship + * the same shim and the same `@pacquet/-` binary + * sub-packages, so the rest of the lookup is identical. + */ + packageName: 'pacquet' | '@pnpm/pacquet' + /** + * The parsed pnpm argv from `@pnpm/cli.parse-cli-args` — `original` + * preserves the user's exact tokens (so `--key=value` stays joined, + * which pacquet's `--config.=` parser requires), and + * `remain` lists the positionals (the `install`/`i` command token + * among them). When `isInstallCommand` is true we forward + * `original` minus positionals to pacquet's own `install` + * subcommand; otherwise we only inspect it to warn about flags + * pacquet won't see. + */ + argv: { + original: string[] + remain: string[] + } + /** + * `true` when the user invoked `pnpm install` (or `pnpm i`). Gates + * flag forwarding: pacquet's `install` subcommand mirrors pnpm's + * surface closely enough that the user's flags are safe to pass + * along on that command, but not from `add`/`update`/`dedupe` (whose + * own flag surface doesn't line up with pacquet's `install`). + */ + isInstallCommand: boolean +} + +/** + * Build the install-engine callback `mutateModules` invokes when + * `configDependencies` declares pacquet. + * + * The callback spawns the pacquet binary installed under + * `node_modules/.pnpm-config/pacquet`. From `pnpm install`/`pnpm i` it + * forwards the user's own pnpm CLI flags to pacquet's `install` + * subcommand; from `add`/`update`/`dedupe` it doesn't forward (warning + * instead). Pacquet's NDJSON stderr is parsed line-by-line and the + * valid JSON records are re-emitted on pnpm's global `streamParser` so + * `@pnpm/cli.default-reporter` renders pacquet's events the same way it + * renders pnpm's own. Non-JSON stderr lines (panic backtraces, + * unexpected diagnostics) are forwarded to the real stderr verbatim so + * they reach the user. + */ +/** Args the deps-installer passes per pacquet invocation. */ +export interface RunPacquetCallOpts { + /** + * `true` when pnpm has already run a lockfileOnly resolve pass and + * the reporter has already accumulated one `pnpm:progress + * status:resolved` per package. Pacquet's own `resolved` events + * (emitted for wire-format parity as it walks the lockfile) are + * dropped on the way back through the reader so the reporter + * doesn't double-count. The frozen-install path passes `false`: + * pnpm did no resolution there, so pacquet's events are the only + * source. + */ + filterResolvedProgress?: boolean +} + +export function makeRunPacquet (opts: MakeRunPacquetOpts): (callOpts?: RunPacquetCallOpts) => Promise { + return async (callOpts) => { + const pacquetBin = resolvePacquetBin(opts.lockfileDir, opts.packageName) + // From `pnpm install`/`pnpm i` we forward the user's flags through to + // pacquet's own `install` subcommand verbatim — pacquet mirrors pnpm's + // surface closely enough on that command that they're safe to pass + // along. From `add`/`update`/`dedupe` we don't forward anything: those + // commands carry flags pacquet's `install` doesn't recognize + // (`--save-dev`, `--save-peer`, etc.) which clap would reject. Either + // way pacquet picks up the settings users care about from + // `pnpm-workspace.yaml` / `.npmrc` on its own, so a non-install + // delegation isn't broken by the omission. + const forwardedFlags = opts.isInstallCommand ? collectForwardedFlags(opts.argv) : [] + // `--ignore-manifest-check` tells pacquet to skip its per-importer + // `package.json` ↔ `pnpm-lock.yaml` freshness gate. pnpm just + // resolved and wrote the lockfile itself; on `pnpm up` / `add` / + // `remove` the manifest on disk is still the pre-mutation copy + // (pnpm writes it after `mutateModules` returns), so pacquet's own + // check would always fire here. See + // https://github.com/pnpm/pnpm/issues/11797. The flag is narrow + // (only the manifest check); settings drift like `overrides` is + // still enforced and was already re-validated by pnpm. + const args = ['--reporter=ndjson', 'install', '--frozen-lockfile', '--ignore-manifest-check', ...forwardedFlags] + const droppedFlags = opts.isInstallCommand ? [] : collectDroppedFlags(opts.argv) + if (droppedFlags.length > 0) { + logger.warn({ + message: `The following CLI flags are not forwarded to pacquet and may not be honored: ${droppedFlags.join(' ')}. Move the equivalent settings into pnpm-workspace.yaml (or .npmrc for auth/registry) if pacquet needs them.`, + prefix: opts.lockfileDir, + }) + } + // Banner so users can tell at a glance their install is going + // through the Rust engine rather than the JS path. Chalk is the + // same dependency the default reporter uses for the "+ pkg + // version" summary, so colorization respects the user's TTY + // settings consistently. + const banner = [ + chalk.magentaBright('▶ Using pacquet for this install'), + chalk.gray(' pacquet is pnpm\'s Rust install engine (preview); declared in configDependencies.'), + ].join('\n') + logger.info({ message: banner, prefix: opts.lockfileDir }) + const child = spawn(pacquetBin, args, { + cwd: opts.lockfileDir, + stdio: ['ignore', 'inherit', 'pipe'], + }) + const filterResolved = callOpts?.filterResolvedProgress === true + const rl = readline.createInterface({ input: child.stderr!, crlfDelay: Infinity }) + rl.on('line', (line) => { + if (!line) return + let parsed: unknown + try { + parsed = JSON.parse(line) + } catch { + process.stderr.write(`${line}\n`) + return + } + if ( + filterResolved && + typeof parsed === 'object' && parsed !== null && + (parsed as { name?: string }).name === 'pnpm:progress' && + (parsed as { status?: string }).status === 'resolved' + ) { + return + } + streamParserWritable.write(`${line}\n`) + }) + await new Promise((resolve, reject) => { + child.once('error', reject) + child.once('close', (code) => { + rl.close() + if (code === 0) { + resolve() + return + } + reject(new PnpmError('PACQUET_INSTALL_FAILED', `pacquet exited with code ${code ?? 'null'}`)) + }) + }) + } +} + +/** + * Path of the platform-specific native pacquet binary for the host. The + * pacquet npm package ships a Node wrapper at `bin/pacquet` that uses + * `require.resolve('@pacquet/-/pacquet[.exe]')` to find + * the binary — so the platform package lands as a *sibling* of pacquet, + * not inside its own `node_modules` (pacquet's own `node_modules` is + * empty after configDependencies install). Use Node's resolver rooted + * at pacquet's own `package.json` so we follow the same path the + * wrapper would have. + * + * The `realpathSync` is required: `.pnpm-config/pacquet` is a symlink + * into the global virtual store, and Node's `createRequire` builds its + * search paths from the *literal* ancestors of the path it's given — + * it won't follow the symlink up into the store dir where the + * `@pacquet/-` sibling actually lives. + */ +function resolvePacquetBin (lockfileDir: string, packageName: 'pacquet' | '@pnpm/pacquet'): string { + const ext = process.platform === 'win32' ? '.exe' : '' + const pacquetPkg = fs.realpathSync(path.join(lockfileDir, 'node_modules/.pnpm-config', packageName, 'package.json')) + return createRequire(pacquetPkg).resolve(`@pacquet/${process.platform}-${process.arch}/pacquet${ext}`) +} + +/** + * From `pnpm install`/`pnpm i`, return everything in argv that should + * ride along to pacquet's own `install` subcommand. Drops the + * positionals nopt classified (`install` / `i`, plus anything users + * typed positionally) since pacquet's `install` doesn't accept any — + * leaving them in produces `error: unexpected argument 'install' + * found`. Pacquet's clap parser walks the same `--prod`, `--dev`, + * `--no-optional`, `--no-runtime`, `--node-linker`, `--offline`, + * `--prefer-offline`, `--cpu`, `--os`, `--libc`, `--frozen-lockfile` + * surface pnpm itself accepts on `install`, so the flags don't need + * reshaping. + * + * Flags we always inject ourselves (`--frozen-lockfile`, + * `--ignore-manifest-check`) are dropped in every form the user can + * type them — positive (`--frozen-lockfile`), negated + * (`--no-frozen-lockfile`), and any `=value` form. Pacquet's clap + * defines these as plain `#[clap(long)] bool` flags, so a duplicate + * `--frozen-lockfile` or a conflicting `--no-frozen-lockfile` + * crashes the parser with "used multiple times" / "unexpected + * argument". The user's `--no-frozen-lockfile` intent is already + * honored upstream (pnpm did a fresh resolve before delegating); + * pacquet's role here is just lockfile-driven materialization. + * + * `--reporter` is stripped in any form (`--reporter=foo`, + * `--reporter foo`): pacquet's `reporter` is a clap value option + * with last-value-wins semantics, so a user-supplied value would + * override our `--reporter=ndjson` and break the + * NDJSON-to-streamParser plumbing the default reporter relies on. + */ +function collectForwardedFlags (argv: { original: string[], remain: string[] }): string[] { + const result: string[] = [] + // `argv.remain` is the ordered subsequence of positionals nopt + // extracted from `original`. Match by index rather than by value so + // an option's value that happens to equal a positional (e.g. + // `--node-linker install`) isn't mistaken for the positional itself. + let positionalIdx = 0 + for (let i = 0; i < argv.original.length; i++) { + const arg = argv.original[i] + if (positionalIdx < argv.remain.length && arg === argv.remain[positionalIdx]) { + positionalIdx++ + continue + } + if (isAlwaysInjected(arg)) continue + if (arg.startsWith('--reporter=')) continue + if (arg === '--reporter') { + // Consume the next token as the reporter value (`--reporter foo`). + i++ + continue + } + result.push(arg) + } + return result +} + +const ALWAYS_INJECTED_FLAGS = ['frozen-lockfile', 'ignore-manifest-check'] as const + +function isAlwaysInjected (arg: string): boolean { + for (const name of ALWAYS_INJECTED_FLAGS) { + if (arg === `--${name}` || arg === `--no-${name}`) return true + if (arg.startsWith(`--${name}=`) || arg.startsWith(`--no-${name}=`)) return true + } + return false +} + +/** + * From a non-install command (`add`, `update`, `dedupe`, ...), pull the + * CLI flags out of pnpm's argv so we can warn that pacquet won't see + * them. They're still handled by pnpm itself before delegation + * (`--save-dev` rewrites `package.json`, `--filter` selects projects, + * etc.) so listing them to the user makes the "not forwarded" surface + * concrete. + * + * Flags pnpm itself honors before delegation are filtered out — + * warning about them would be misleading: `--frozen-lockfile` and + * `--ignore-manifest-check` in every shape (positive / negated / + * `=value`); `--reporter` in every shape (`--reporter=foo`, + * `--reporter foo`); and `--config.*` (configures pnpm's runtime, + * not the install engine). + */ +function collectDroppedFlags (argv: { original: string[] }): string[] { + const result: string[] = [] + for (let i = 0; i < argv.original.length; i++) { + const arg = argv.original[i] + if (!arg.startsWith('-')) continue + if (isAlwaysInjected(arg)) continue + if (arg.startsWith('--config.')) continue + if (arg.startsWith('--reporter=')) continue + if (arg === '--reporter') { + i++ + continue + } + result.push(arg) + } + return result +} diff --git a/installing/commands/src/update/index.ts b/installing/commands/src/update/index.ts index ebb71599c1..ffdadc63e2 100644 --- a/installing/commands/src/update/index.ts +++ b/installing/commands/src/update/index.ts @@ -21,6 +21,7 @@ import { renderHelp } from 'render-help' import type { InstallCommandOptions } from '../install.js' import { installDeps } from '../installDeps.js' import { parseUpdateParam } from '../recursive.js' +import { createGlobalPolicyCallbacks } from '../resolutionPolicyManifest.js' import { type ChoiceRow, getUpdateChoices } from './getUpdateChoices.js' export function rcOptionsTypes (): Record { return pick([ @@ -178,7 +179,10 @@ export async function handler ( hint: 'Run "pnpm setup" to create it automatically, or set the global-bin-dir setting, or the PNPM_HOME env variable. The global bin directory should be in the PATH.', }) } - return handleGlobalUpdate(opts, params, commands ?? {}) + return handleGlobalUpdate({ + ...opts, + ...createGlobalPolicyCallbacks(opts), + }, params, commands ?? {}) } const rebuildHandler = commands?.rebuild if (opts.interactive) { diff --git a/installing/commands/test/add.ts b/installing/commands/test/add.ts index 7ee435c4fc..b4a9eb634d 100644 --- a/installing/commands/test/add.ts +++ b/installing/commands/test/add.ts @@ -381,7 +381,7 @@ test('minimumReleaseAge with minimumReleaseAgeStrict enabled makes install fail minimumReleaseAge, minimumReleaseAgeStrict: true, linkWorkspacePackages: false, - }, ['is-odd@0.1.1'])).rejects.toThrow(/Version 0\.1\.1 \(released .+\) of is-odd does not meet the minimumReleaseAge constraint/) + }, ['is-odd@0.1.1'])).rejects.toThrow(/is-odd@0\.1\.1 was published.+minimumReleaseAge cutoff/) }) describeOnLinuxOnly('filters optional dependencies based on pnpm.supportedArchitectures.libc', () => { diff --git a/installing/commands/test/import.ts b/installing/commands/test/import.ts index c380ff15bb..00d9409f0c 100644 --- a/installing/commands/test/import.ts +++ b/installing/commands/test/import.ts @@ -6,8 +6,9 @@ import { assertProject } from '@pnpm/assert-project' import { PnpmError } from '@pnpm/error' import { importCommand } from '@pnpm/installing.commands' import { prepare } from '@pnpm/prepare' -import { addDistTag, REGISTRY_MOCK_PORT } from '@pnpm/registry-mock' +import { REGISTRY_MOCK_PORT } from '@pnpm/registry-mock' import { fixtures } from '@pnpm/test-fixtures' +import { addDistTag } from '@pnpm/testing.registry-mock' import { temporaryDirectory } from 'tempy' const f = fixtures(import.meta.dirname) diff --git a/installing/commands/test/miscRecursive.ts b/installing/commands/test/miscRecursive.ts index f5619060fa..98ecd05aae 100644 --- a/installing/commands/test/miscRecursive.ts +++ b/installing/commands/test/miscRecursive.ts @@ -6,7 +6,7 @@ import type { PnpmError } from '@pnpm/error' import { add, install, remove, update } from '@pnpm/installing.commands' import type { LockfileFile } from '@pnpm/lockfile.types' import { preparePackages } from '@pnpm/prepare' -import { addDistTag } from '@pnpm/registry-mock' +import { addDistTag } from '@pnpm/testing.registry-mock' import type { ProjectManifest } from '@pnpm/types' import { filterProjectsBySelectorObjectsFromDir } from '@pnpm/workspace.projects-filter' import { loadJsonFile } from 'load-json-file' diff --git a/installing/commands/test/policyHandlers.ts b/installing/commands/test/policyHandlers.ts new file mode 100644 index 0000000000..72edd8720b --- /dev/null +++ b/installing/commands/test/policyHandlers.ts @@ -0,0 +1,160 @@ +import { expect, jest, test } from '@jest/globals' + +import { type PolicyViolation, setupPolicyHandlers } from '../lib/policyHandlers.js' + +function violation ( + name: string, + version: string, + code = 'MINIMUM_RELEASE_AGE_VIOLATION' +): PolicyViolation { + return { name, version, code, reason: 'stub reason' } +} + +// Swap `process.stdin.isTTY` for the duration of a test, restoring the +// original descriptor — not just the value — so the property's +// configurability/enumerability shape doesn't leak between tests when +// the host process didn't define an own `isTTY` at all. +function withStdinTTY (value: boolean | undefined, fn: () => void | Promise): void | Promise { + const originalDescriptor = Object.getOwnPropertyDescriptor(process.stdin, 'isTTY') + Object.defineProperty(process.stdin, 'isTTY', { value, configurable: true, writable: true }) + const restore = (): void => { + if (originalDescriptor) { + Object.defineProperty(process.stdin, 'isTTY', originalDescriptor) + } else { + delete (process.stdin as { isTTY?: boolean }).isTTY + } + } + let result: void | Promise + try { + result = fn() + } catch (err) { + restore() + throw err + } + if (result && typeof (result as Promise).then === 'function') { + return (result as Promise).then( + (v) => { + restore(); return v + }, + (err) => { + restore(); throw err + } + ) + } + restore() + return result +} + +test('setupPolicyHandlers returns undefined when no policy is active', () => { + expect(setupPolicyHandlers({})).toBeUndefined() +}) + +test('setupPolicyHandlers returns a plan even when strict mode is on without a TTY', () => { + // Pre-refactor this returned undefined and the resolver did the fail-fast + // throw. Now the plan is always returned: the strict-no-TTY case throws + // from the handler with the full violation list, not just the first + // immature pick the resolver happened to hit. + withStdinTTY(false, () => { + expect(setupPolicyHandlers({ + minimumReleaseAge: 60, + minimumReleaseAgeStrict: true, + ci: false, + })).toBeDefined() + }) +}) + +test('strict no-TTY plan throws from the hook with the full violation list', async () => { + await withStdinTTY(false, async () => { + const plan = setupPolicyHandlers({ + minimumReleaseAge: 60, + minimumReleaseAgeStrict: true, + ci: false, + })! + await expect(plan.handleResolutionPolicyViolations([ + violation('foo', '1.0.0'), + violation('bar', '2.3.4'), + ])).rejects.toMatchObject({ + code: 'ERR_PNPM_NO_MATURE_MATCHING_VERSION', + }) + }) +}) + +test('setupPolicyHandlers returns a plan when ci=false and stdin is a TTY', () => { + withStdinTTY(true, () => { + const plan = setupPolicyHandlers({ + minimumReleaseAge: 60, + minimumReleaseAgeStrict: true, + ci: false, + }) + expect(plan).toBeDefined() + }) +}) + +test('strict + --no-save refuses up-front instead of prompting for approval it cannot persist', async () => { + // The prompt promises to write to minimumReleaseAgeExclude, but the + // install command's `opts.save !== false` gate blocks that under + // --no-save — accepting the prompt would leave the lockfile holding + // approved-but-unlisted picks that the next install rejects. + await withStdinTTY(true, async () => { + const plan = setupPolicyHandlers({ + minimumReleaseAge: 60, + minimumReleaseAgeStrict: true, + save: false, + ci: false, + })! + await expect(plan.handleResolutionPolicyViolations([violation('foo', '1.0.0')])) + .rejects.toMatchObject({ code: 'ERR_PNPM_STRICT_MIN_RELEASE_AGE_REQUIRES_SAVE' }) + }) +}) + +test('loose + --no-save runs the hook as a no-op (lockfile re-triggers auto-collect later)', async () => { + // Loose mode never persists from the hook anyway — `pickManifestUpdates` + // is what writes the exclude list at the install's tail, and the + // installDeps / recursive `opts.save !== false` gates already skip that + // when --no-save is set. + const plan = setupPolicyHandlers({ + minimumReleaseAge: 60, + save: false, + })! + await expect(plan.handleResolutionPolicyViolations([violation('foo', '1.0.0')])) + .resolves.toBeUndefined() +}) + +test('loose-mode plan emits a workspace patch with sorted unique entries and logs once', () => { + const plan = setupPolicyHandlers({ minimumReleaseAge: 60 })! + const violations = [ + violation('foo', '1.0.0'), + violation('foo', '1.0.0'), + violation('bar', '2.3.4'), + // Non-minimumReleaseAge code: the minimumReleaseAge handler ignores it. + // (When more handlers register, each filters its own codes.) + violation('quux', '0.0.1', 'TRUST_DOWNGRADE'), + ] + + // Avoid leaking console output in test runs. + const infoSpy = jest.spyOn(console, 'log').mockImplementation(() => {}) + try { + const updates = plan.pickManifestUpdates(violations) + expect(updates).toEqual({ addedMinimumReleaseAgeExcludes: ['bar@2.3.4', 'foo@1.0.0'] }) + } finally { + infoSpy.mockRestore() + } +}) + +test('pickManifestUpdates returns undefined when no handler contributes anything', () => { + const plan = setupPolicyHandlers({ minimumReleaseAge: 60 })! + expect(plan.pickManifestUpdates([])).toBeUndefined() + // Codes the minimumReleaseAge handler doesn't recognize don't produce a + // patch — and with no other handler registered yet, the merged result + // collapses to undefined so the install command skips the workspace + // writer entirely. + expect(plan.pickManifestUpdates([violation('foo', '1.0.0', 'TRUST_DOWNGRADE')])).toBeUndefined() +}) + +test('the hook is a no-op in loose mode regardless of violations', async () => { + const plan = setupPolicyHandlers({ minimumReleaseAge: 60 })! + // Loose mode never prompts — picks are persisted from + // `pickManifestUpdates` at the end of the install. + await expect(plan.handleResolutionPolicyViolations([violation('foo', '1.0.0')])) + .resolves.toBeUndefined() +}) diff --git a/installing/commands/test/saveCatalog.ts b/installing/commands/test/saveCatalog.ts index f2301896eb..8c3b7e9dce 100644 --- a/installing/commands/test/saveCatalog.ts +++ b/installing/commands/test/saveCatalog.ts @@ -4,8 +4,8 @@ import path from 'node:path' import { afterEach, expect, test } from '@jest/globals' import { add } from '@pnpm/installing.commands' import { prepare, preparePackages } from '@pnpm/prepare' -import { addDistTag } from '@pnpm/registry-mock' import { getMockAgent, setupMockAgent, teardownMockAgent } from '@pnpm/testing.mock-agent' +import { addDistTag } from '@pnpm/testing.registry-mock' import { loadJsonFileSync } from 'load-json-file' import { readYamlFileSync } from 'read-yaml-file' diff --git a/installing/commands/test/update/interactive.ts b/installing/commands/test/update/interactive.ts index 4c8a1d3ce0..17f59d2142 100644 --- a/installing/commands/test/update/interactive.ts +++ b/installing/commands/test/update/interactive.ts @@ -3,7 +3,8 @@ import path from 'node:path' import { expect, jest, test } from '@jest/globals' import type { LockfileObject } from '@pnpm/lockfile.types' import { prepare, preparePackages } from '@pnpm/prepare' -import { addDistTag, REGISTRY_MOCK_PORT } from '@pnpm/registry-mock' +import { REGISTRY_MOCK_PORT } from '@pnpm/registry-mock' +import { addDistTag } from '@pnpm/testing.registry-mock' import { filterProjectsBySelectorObjectsFromDir } from '@pnpm/workspace.projects-filter' import chalk from 'chalk' import { readYamlFileSync } from 'read-yaml-file' diff --git a/installing/commands/test/update/jsr.ts b/installing/commands/test/update/jsr.ts index 27c7957920..dce54f0fa2 100644 --- a/installing/commands/test/update/jsr.ts +++ b/installing/commands/test/update/jsr.ts @@ -3,7 +3,7 @@ import path from 'node:path' import { expect, test } from '@jest/globals' import { install, update } from '@pnpm/installing.commands' import { prepare } from '@pnpm/prepare' -import { addDistTag } from '@pnpm/registry-mock' +import { addDistTag } from '@pnpm/testing.registry-mock' import type { ProjectManifest } from '@pnpm/types' import { loadJsonFileSync } from 'load-json-file' diff --git a/installing/commands/test/update/recursive.ts b/installing/commands/test/update/recursive.ts index ec4b006555..95906bcc3a 100644 --- a/installing/commands/test/update/recursive.ts +++ b/installing/commands/test/update/recursive.ts @@ -4,7 +4,7 @@ import { install, update } from '@pnpm/installing.commands' import { readModulesManifest } from '@pnpm/installing.modules-yaml' import type { LockfileObject } from '@pnpm/lockfile.types' import { preparePackages } from '@pnpm/prepare' -import { addDistTag } from '@pnpm/registry-mock' +import { addDistTag } from '@pnpm/testing.registry-mock' import { readProjectManifestOnly } from '@pnpm/workspace.project-manifest-reader' import { filterProjectsBySelectorObjectsFromDir } from '@pnpm/workspace.projects-filter' import { readYamlFileSync } from 'read-yaml-file' diff --git a/installing/commands/test/update/update.ts b/installing/commands/test/update/update.ts index a777792711..2fde6b8f76 100644 --- a/installing/commands/test/update/update.ts +++ b/installing/commands/test/update/update.ts @@ -4,7 +4,7 @@ import { beforeAll, describe, expect, it, test } from '@jest/globals' import type { PnpmError } from '@pnpm/error' import { install, update } from '@pnpm/installing.commands' import { prepare, preparePackages } from '@pnpm/prepare' -import { addDistTag } from '@pnpm/registry-mock' +import { addDistTag } from '@pnpm/testing.registry-mock' import type { ProjectManifest } from '@pnpm/types' import { loadJsonFileSync } from 'load-json-file' diff --git a/installing/commands/tsconfig.json b/installing/commands/tsconfig.json index d627195020..258e3cdf56 100644 --- a/installing/commands/tsconfig.json +++ b/installing/commands/tsconfig.json @@ -90,6 +90,9 @@ { "path": "../../pkg-manifest/utils" }, + { + "path": "../../resolving/npm-resolver" + }, { "path": "../../resolving/parse-wanted-dependency" }, @@ -111,6 +114,9 @@ { "path": "../../testing/mock-agent" }, + { + "path": "../../testing/registry-mock" + }, { "path": "../../worker" }, diff --git a/installing/context/CHANGELOG.md b/installing/context/CHANGELOG.md index 914f757b03..e07cde686c 100644 --- a/installing/context/CHANGELOG.md +++ b/installing/context/CHANGELOG.md @@ -1,5 +1,48 @@ # @pnpm/get-context +## 1100.0.13 + +### Patch Changes + +- Updated dependencies [35d2355] + - @pnpm/types@1101.2.0 + - @pnpm/lockfile.fs@1100.1.2 + - @pnpm/core-loggers@1100.1.2 + - @pnpm/installing.modules-yaml@1100.0.6 + - @pnpm/installing.read-projects-context@1100.0.12 + - @pnpm/resolving.resolver-base@1100.3.1 + - @pnpm/store.controller@1101.0.9 + +## 1100.0.12 + +### Patch Changes + +- Updated dependencies [9cb48bb] +- Updated dependencies [1627943] +- Updated dependencies [64afc92] + - @pnpm/lockfile.fs@1100.1.1 + - @pnpm/resolving.resolver-base@1100.3.0 + - @pnpm/types@1101.1.1 + - @pnpm/installing.read-projects-context@1100.0.11 + - @pnpm/store.controller@1101.0.8 + - @pnpm/core-loggers@1100.1.1 + - @pnpm/installing.modules-yaml@1100.0.5 + +## 1100.0.11 + +### Patch Changes + +- Updated dependencies [4195766] +- Updated dependencies [31538bf] +- Updated dependencies [6e93f35] +- Updated dependencies [4a79336] +- Updated dependencies [2a9bd89] + - @pnpm/resolving.resolver-base@1100.2.0 + - @pnpm/lockfile.fs@1100.1.0 + - @pnpm/core-loggers@1100.1.0 + - @pnpm/store.controller@1101.0.7 + - @pnpm/installing.read-projects-context@1100.0.10 + ## 1100.0.10 ### Patch Changes diff --git a/installing/context/package.json b/installing/context/package.json index 34f584d104..5a0fdac586 100644 --- a/installing/context/package.json +++ b/installing/context/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/installing.context", - "version": "1100.0.10", + "version": "1100.0.13", "description": "Gets context information about a project", "keywords": [ "pnpm", diff --git a/installing/context/src/readLockfiles.ts b/installing/context/src/readLockfiles.ts index f298f2103d..d491e02091 100644 --- a/installing/context/src/readLockfiles.ts +++ b/installing/context/src/readLockfiles.ts @@ -122,10 +122,14 @@ export async function readLockfiles ( } } } + const existsWantedLockfile = files[0] != null + const existsCurrentLockfile = files[1] != null const wantedLockfile = files[0] ?? (currentLockfile && clone(currentLockfile)) ?? createLockfileObject(importerIds, sopts) - let wantedLockfileIsModified = false + // Cloning the current lockfile means the disk copy of the wanted lockfile is + // stale, so flag it for rewriting after the install completes. + let wantedLockfileIsModified = !existsWantedLockfile && existsCurrentLockfile for (const importerId of importerIds) { if (!wantedLockfile.importers[importerId]) { wantedLockfileIsModified = true @@ -134,11 +138,10 @@ export async function readLockfiles ( } } } - const existsWantedLockfile = files[0] != null return { currentLockfile, currentLockfileIsUpToDate: equals(currentLockfile, wantedLockfile), - existsCurrentLockfile: files[1] != null, + existsCurrentLockfile, existsWantedLockfile, existsNonEmptyWantedLockfile: existsWantedLockfile && !isEmptyLockfile(wantedLockfile), wantedLockfile, diff --git a/installing/dedupe/check/CHANGELOG.md b/installing/dedupe/check/CHANGELOG.md index 05cebf55a3..84c0c7c618 100644 --- a/installing/dedupe/check/CHANGELOG.md +++ b/installing/dedupe/check/CHANGELOG.md @@ -1,5 +1,27 @@ # @pnpm/dedupe.check +## 1100.0.8 + +### Patch Changes + +- Updated dependencies [35d2355] + - @pnpm/types@1101.2.0 + - @pnpm/lockfile.types@1100.0.8 + +## 1100.0.7 + +### Patch Changes + +- Updated dependencies [64afc92] + - @pnpm/types@1101.1.1 + - @pnpm/lockfile.types@1100.0.7 + +## 1100.0.6 + +### Patch Changes + +- @pnpm/lockfile.types@1100.0.6 + ## 1100.0.5 ### Patch Changes diff --git a/installing/dedupe/check/package.json b/installing/dedupe/check/package.json index 68bfbb2de1..41a7473940 100644 --- a/installing/dedupe/check/package.json +++ b/installing/dedupe/check/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/installing.dedupe.check", - "version": "1100.0.5", + "version": "1100.0.8", "description": "Visualize pnpm dedupe --check issues.", "keywords": [ "pnpm", diff --git a/installing/deps-installer/CHANGELOG.md b/installing/deps-installer/CHANGELOG.md index 8eef3f5639..bec1539131 100644 --- a/installing/deps-installer/CHANGELOG.md +++ b/installing/deps-installer/CHANGELOG.md @@ -1,5 +1,245 @@ # @pnpm/core +## 1101.5.0 + +### Minor Changes + +- aa6149d: Treat tarball-integrity mismatches against the lockfile as a hard failure by default. Previously, `pnpm install` (non-frozen) would log `ERR_PNPM_TARBALL_INTEGRITY`, silently re-resolve from the registry, and overwrite the locked integrity — which meant a compromised registry, proxy, or republished version could substitute attacker-controlled content on a clean machine even though the project shipped a committed lockfile. + + `pnpm install` now exits with `ERR_PNPM_TARBALL_INTEGRITY` and a hint pointing at the new opt-in flag. + + The only opt-in is **`pnpm install --update-checksums`** — narrowly scoped to refreshing the locked integrity values from what the registry currently serves. Mirrors yarn's flag of the same name. A warning still prints when the bypass takes effect so the operation is auditable. + + `--force` and `pnpm update` deliberately do **not** bypass the integrity check. They are routine refresh operations; silently overwriting a locked integrity in those flows would erase the protection a committed lockfile is supposed to provide. `--frozen-lockfile` behavior is unchanged. `--fix-lockfile` keeps its documented purpose (filling in missing lockfile entries) and is also not a bypass. + +### Patch Changes + +- Updated dependencies [aa6149d] +- Updated dependencies [a456dc7] +- Updated dependencies [ad84fff] +- Updated dependencies [e55f4b5] +- Updated dependencies [35d2355] + - @pnpm/worker@1100.1.8 + - @pnpm/workspace.project-manifest-reader@1100.0.9 + - @pnpm/installing.deps-resolver@1100.1.4 + - @pnpm/fs.symlink-dependency@1100.0.6 + - @pnpm/lockfile.utils@1100.0.10 + - @pnpm/types@1101.2.0 + - @pnpm/building.after-install@1101.0.17 + - @pnpm/building.during-install@1101.0.14 + - @pnpm/bins.linker@1100.0.10 + - @pnpm/installing.deps-restorer@1101.1.6 + - @pnpm/installing.linking.direct-dep-linker@1100.0.6 + - @pnpm/deps.graph-hasher@1100.2.2 + - @pnpm/installing.linking.modules-cleaner@1100.1.4 + - @pnpm/lockfile.filtering@1100.1.3 + - @pnpm/lockfile.fs@1100.1.2 + - @pnpm/lockfile.preferred-versions@1100.0.12 + - @pnpm/lockfile.to-pnp@1100.0.11 + - @pnpm/lockfile.verification@1100.0.13 + - @pnpm/agent.client@1.0.8 + - @pnpm/bins.remover@1100.0.6 + - @pnpm/building.policy@1100.0.7 + - @pnpm/config.normalize-registries@1100.0.5 + - @pnpm/core-loggers@1100.1.2 + - @pnpm/deps.path@1100.0.5 + - @pnpm/exec.lifecycle@1100.0.14 + - @pnpm/hooks.read-package-hook@1100.0.5 + - @pnpm/hooks.types@1100.0.9 + - @pnpm/installing.context@1100.0.13 + - @pnpm/installing.linking.hoist@1100.0.10 + - @pnpm/installing.modules-yaml@1100.0.6 + - @pnpm/installing.package-requester@1101.0.9 + - @pnpm/lockfile.pruner@1100.0.8 + - @pnpm/lockfile.walker@1100.0.8 + - @pnpm/pkg-manifest.utils@1100.2.1 + - @pnpm/resolving.resolver-base@1100.3.1 + - @pnpm/store.controller-types@1100.1.2 + - @pnpm/lockfile.settings-checker@1100.0.13 + - @pnpm/crypto.hash@1100.0.1 + - @pnpm/patching.config@1100.0.5 + +## 1101.4.0 + +### Minor Changes + +- 212315d: Added a new setting `trustLockfile`. When `true`, `pnpm install` skips the supply-chain verification pass that re-applies `minimumReleaseAge` / `trustPolicy='no-downgrade'` to every entry in the loaded lockfile. The install treats the lockfile as already-trusted — useful for closed-source projects where every commit comes from a trusted author, or for CI runs against an already-verified lockfile. Defaults to `false`; verification stays on by default. Set in `pnpm-workspace.yaml`. + + Also cut the memory footprint of the verification pass itself: the per-(registry, name) trust-meta cache previously retained the full packument — dependency graphs, scripts, README, and per-version manifests — for the entire install. On large workspaces (`~4k` lockfile entries with `minimumReleaseAge` + `trustPolicy: no-downgrade` enabled) this could OOM CI runners with a 2GB heap cap. The cache now stores only the fields the trust check actually reads (`time`, per-version `_npmUser.trustedPublisher`, `dist.attestations.provenance`). The abbreviated-metadata cache is similarly projected to just the package-level `modified` field and the set of currently-listed version names. Fixes [#11860](https://github.com/pnpm/pnpm/issues/11860). + +### Patch Changes + +- Updated dependencies [3422cec] +- Updated dependencies [e0bd879] +- Updated dependencies [d7da112] + - @pnpm/installing.deps-resolver@1100.1.3 + - @pnpm/workspace.project-manifest-reader@1100.0.8 + - @pnpm/bins.linker@1100.0.9 + - @pnpm/installing.deps-restorer@1101.1.5 + - @pnpm/building.after-install@1101.0.16 + - @pnpm/building.during-install@1101.0.13 + - @pnpm/exec.lifecycle@1100.0.13 + - @pnpm/installing.linking.hoist@1100.0.9 + - @pnpm/installing.package-requester@1101.0.8 + +## 1101.3.1 + +### Patch Changes + +- @pnpm/installing.deps-resolver@1100.1.2 +- @pnpm/installing.package-requester@1101.0.8 +- @pnpm/building.after-install@1101.0.15 +- @pnpm/installing.deps-restorer@1101.1.4 + +## 1101.3.0 + +### Minor Changes + +- b206a15: Adding [`pacquet`](https://github.com/pnpm/pnpm/tree/main/pacquet) (the Rust port of pnpm) to `configDependencies` in `pnpm-workspace.yaml` now delegates the materialization phase of `pnpm install` to the pacquet binary instead of running the JS installer's headless path. Pacquet emits the same `pnpm:*` NDJSON log events that `@pnpm/cli.default-reporter` already parses, so the install renders identically. Absent the `pacquet` entry, behavior is unchanged. + + ```yaml + # pnpm-workspace.yaml + configDependencies: + pacquet: "^0.1.0" + ``` + + Pacquet takes over every place pnpm would otherwise call `headlessInstall`: the frozen-install path, the hoisted-`nodeLinker` install, the workspace partial-install (where pnpm runs a `lockfileOnly` resolve pass first), and the agent-server install. In all cases pnpm still owns dependency resolution; pacquet only fetches and imports from the freshly-written lockfile. This is an opt-in preview of the Rust install engine [#11723](https://github.com/pnpm/pnpm/issues/11723). + +### Patch Changes + +- Updated dependencies [9cb48bb] +- Updated dependencies [1627943] +- Updated dependencies [64afc92] + - @pnpm/lockfile.fs@1100.1.1 + - @pnpm/exec.lifecycle@1100.0.12 + - @pnpm/resolving.resolver-base@1100.3.0 + - @pnpm/pkg-manifest.utils@1100.2.0 + - @pnpm/types@1101.1.1 + - @pnpm/building.after-install@1101.0.14 + - @pnpm/building.during-install@1101.0.12 + - @pnpm/installing.context@1100.0.12 + - @pnpm/installing.deps-restorer@1101.1.4 + - @pnpm/lockfile.to-pnp@1100.0.10 + - @pnpm/installing.deps-resolver@1100.1.1 + - @pnpm/installing.package-requester@1101.0.8 + - @pnpm/deps.graph-hasher@1100.2.1 + - @pnpm/hooks.types@1100.0.8 + - @pnpm/lockfile.preferred-versions@1100.0.11 + - @pnpm/lockfile.utils@1100.0.9 + - @pnpm/lockfile.verification@1100.0.12 + - @pnpm/store.controller-types@1100.1.1 + - @pnpm/bins.linker@1100.0.8 + - @pnpm/workspace.project-manifest-reader@1100.0.7 + - @pnpm/agent.client@1.0.7 + - @pnpm/bins.remover@1100.0.5 + - @pnpm/building.policy@1100.0.6 + - @pnpm/config.normalize-registries@1100.0.4 + - @pnpm/core-loggers@1100.1.1 + - @pnpm/deps.path@1100.0.4 + - @pnpm/fs.symlink-dependency@1100.0.5 + - @pnpm/hooks.read-package-hook@1100.0.4 + - @pnpm/installing.linking.hoist@1100.0.8 + - @pnpm/installing.linking.modules-cleaner@1100.1.3 + - @pnpm/installing.modules-yaml@1100.0.5 + - @pnpm/lockfile.filtering@1100.1.2 + - @pnpm/lockfile.pruner@1100.0.7 + - @pnpm/lockfile.walker@1100.0.7 + - @pnpm/worker@1100.1.7 + - @pnpm/lockfile.settings-checker@1100.0.12 + - @pnpm/crypto.hash@1100.0.1 + - @pnpm/installing.linking.direct-dep-linker@1100.0.5 + - @pnpm/patching.config@1100.0.4 + +## 1101.2.0 + +### Minor Changes + +- 4195766: Tightened the `minimumReleaseAge` story so the bypass becomes explicit on disk instead of silent, and removed the discover-by-loop dance for strict-mode users: + + 1. Fresh resolutions in loose mode (`minimumReleaseAgeStrict: false`) that fall back to a version newer than the cutoff auto-collect the picked `name@version` into the workspace manifest's `minimumReleaseAgeExclude`. A single info message lists the additions; entries already on the list are left alone. + 2. The post-resolution lockfile verifier introduced in #11583 now runs in loose mode too — every accepted-immature pin must be on `minimumReleaseAgeExclude`, just like strict mode requires. A lockfile produced under a weaker (or absent) policy that still has immature entries is rejected the same way strict mode would reject it. + 3. **Strict mode (interactive)** no longer aborts on the first immature pick. The resolver gathers every immature direct _and_ transitive in one pass; before peer-dependency resolution runs, pnpm prompts the user with the full list and asks whether to add them all to `minimumReleaseAgeExclude` and proceed. Approve → install continues and the workspace manifest is written at the end. Decline → resolution aborts before the lockfile or package.json is touched (tarballs already in the store stay, since the store is idempotent). This closes the [#10488](https://github.com/pnpm/pnpm/issues/10488) loop where security bumps to packages with platform-specific transitives (e.g. `next` + the `@next/swc-*` shims) made users re-run `pnpm add` once per transitive. + 4. **Strict mode (non-interactive / CI)** now aborts with the full immature set in the error message instead of the first pick. The resolver always collects every immature direct + transitive; the install command then throws `ERR_PNPM_NO_MATURE_MATCHING_VERSION` listing each entry's `name@version` and publish time. Deterministic CI behavior is preserved (same exit code, same error code), but the error pinpoints every offending entry instead of forcing the discover-by-loop dance. The expected workflow is interactive approval locally → the lockfile + workspace manifest get committed → CI runs cleanly against the populated exclude list. + + 5. **The lockfile verifier now also covers `trustPolicy: 'no-downgrade'`.** The same post-resolution gate that re-checks `minimumReleaseAge` on lockfile entries now re-runs `failIfTrustDowngraded` for every npm-registry entry whose name isn't on `trustPolicyExclude`. The two checks share a single full-metadata fetch per package, so the extra coverage doesn't cost an extra round trip when both policies are active. Resolver-time trust checks still run as before — this just closes the gap when an entry bypasses resolution (peek path, `--frozen-lockfile`, restored CI cache). + + Pacquet parity: not ported — pacquet's `minimumReleaseAge` policy is itself only stubbed today (see `pacquet/crates/package-manager/src/version_policy.rs`). The auto-exclude, loose-mode verifier, prompt, and the new trust-policy verifier check will travel with the broader policy port whenever that happens. + +- 31538bf: Restructured the `minimumReleaseAge` lockfile revalidation gate around a generic `ResolutionVerifier` interface. Each resolver may now export a sibling verifier factory (today: `createNpmResolutionVerifier`) that re-checks an already-resolved lockfile entry against its policies; the resolver chain returns the verifier list as `resolutionVerifiers` and the install side fans out across it. A `ResolutionVerifier` carries `verify` plus `policy` and `canTrustPastCheck` — the cache contract that lets repeat installs against an unchanged lockfile skip the per-package registry round trip entirely. + + Verification results are memoized in JSON Lines at `/lockfile-verified.jsonl`: a stat-only fast path matches on lockfile size, mtime, and inode, falling back to a content hash when those drift (typical after a CI checkout). Every active verifier's policy contribution is merged into a single `policy` bag on the record; the gate runs in full whenever the lockfile changes, any verifier rejects the cached policy, or no record exists [#11687](https://github.com/pnpm/pnpm/issues/11687). + +- 31538bf: `minimumReleaseAge` is now re-checked against `pnpm-lock.yaml` before any tarball is installed, so a freshly-published version pinned in the lockfile (e.g. by a developer who bypassed the policy locally) is no longer installed silently by other consumers or CI. Violating entries abort the install with `ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION`; `minimumReleaseAgeExclude` is honored. [#10438](https://github.com/pnpm/pnpm/issues/10438). + +### Patch Changes + +- 3ddde2b: **fix**: anchor the side-effects-cache key and global-virtual-store hash to the project's script-runner Node — `engines.runtime` pin when present, shell `node` otherwise — instead of pnpm's own runtime. + + `ENGINE_NAME` (the `;;node` prefix used as the side-effects-cache key and the engine portion of the GVS hash) was computed from `process.version` — the Node that runs pnpm itself. That was wrong in two situations: + + 1. **`@pnpm/exe` SEA bundle.** The bundle has its own embedded Node, not the `node` on the user's `PATH` that actually spawns lifecycle scripts. Two pnpm installations on the same machine (one SEA, one npm-package) therefore disagreed on the cache key, partitioning the side-effects cache and the global virtual store across two Node majors even though both installs would run scripts on the same shell `node`. + 2. **`engines.runtime` / `devEngines.runtime` pin.** When a project pins a Node version via `devEngines.runtime` (pnpm v11+), pnpm downloads that Node into `node_modules/node/` and uses it to run lifecycle scripts. But the hash still anchored to whichever Node ran pnpm itself, not to the pinned Node — so two installs of the same project with two different runner Nodes would still disagree on the GVS slot path even though scripts run on the same pinned Node. + + Three changes: + + - `@pnpm/engine.runtime.system-node-version` now exports `engineName(nodeVersion?)`. Resolves the version in this order: explicit override → `getSystemNodeVersion()` (which already prefers `node --version` over `process.version` in SEA contexts) → `process.version`. + - `@pnpm/deps.graph-hasher` now exports `findRuntimeNodeVersion(snapshotKeys)` — scans an iterable of lockfile snapshot keys for a `node@runtime:` entry and returns its bare version string. `calcDepState` and `calcGraphNodeHash`/`iterateHashedGraphNodes` accept a `nodeVersion?` (in the options bag for the first, as a trailing parameter / ctx field for the others), forwarded to `engineName()`. The default (no override) preserves the pre-change behaviour. The legacy `ENGINE_NAME` constant in `@pnpm/constants` is unchanged so external consumers and existing tests keep working; in non-SEA, non-pinned contexts every value lines up. + - Every install-side caller of the graph-hasher (`@pnpm/installing.deps-resolver`, `@pnpm/installing.deps-restorer`, `@pnpm/installing.deps-installer`, `@pnpm/building.during-install`, `@pnpm/building.after-install`, `@pnpm/deps.graph-builder`) now derives the project's pinned runtime via `findRuntimeNodeVersion(Object.keys(graph))` once per invocation and threads it through. + + On upgrade, two one-time GVS slot churns are possible: + + - **SEA-pnpm users** without a runtime pin: slots that previously hashed under the embedded-Node major (e.g. `node26`) now hash under the shell-Node major (e.g. `node24`), matching what pacquet, the npm-published `pnpm` package, and any other pnpm-compatible tool already produce. + - **Projects with a `devEngines.runtime` pin**: slots that previously hashed under the runner's Node major now hash under the pinned Node major, matching what the lifecycle scripts will actually run on. + + In both cases the old slots become prune-eligible. + +- 4a79336: The lockfile verifier added in #11705 now emits `pnpm:lockfile-verification` log events (`status: 'started' | 'done'`) around the registry round-trip pass, and the default reporter renders them as a transient progress line so users can see that pnpm is doing work — on a cold registry cache the round-trip can take a noticeable beat, and the previous behavior was complete silence followed by either a long pause or an error. The cached short-circuit stays silent (no logs when no work happens), and the `done` line carries the number of distinct entries that were checked plus the elapsed time. + + Pacquet parity: not ported — pacquet doesn't carry the lockfile verifier yet (see the parity note on #11705). + +- 2a9bd89: Record the post-resolution lockfile in the verification cache. Previously the cache only captured the lockfile that was loaded at the start of an install, so a flow like `pnpm install ` followed by `rm -rf node_modules && pnpm install` re-ran the per-package registry round-trip against the newly written lockfile even though the local resolver had already enforced the policy when picking those versions. The fresh lockfile is now recorded immediately after each install-time write, so the second install takes the cache fast path. +- Updated dependencies [4195766] +- Updated dependencies [31538bf] +- Updated dependencies [b6e2c8c] +- Updated dependencies [6e93f35] +- Updated dependencies [3ddde2b] +- Updated dependencies [5dc8be8] +- Updated dependencies [4a79336] +- Updated dependencies [2a9bd89] + - @pnpm/resolving.resolver-base@1100.2.0 + - @pnpm/store.controller-types@1100.1.0 + - @pnpm/installing.deps-resolver@1100.1.0 + - @pnpm/lockfile.fs@1100.1.0 + - @pnpm/building.after-install@1101.0.13 + - @pnpm/building.during-install@1101.0.11 + - @pnpm/deps.graph-hasher@1100.2.0 + - @pnpm/installing.deps-restorer@1101.1.3 + - @pnpm/core-loggers@1100.1.0 + - @pnpm/hooks.types@1100.0.7 + - @pnpm/installing.context@1100.0.11 + - @pnpm/installing.package-requester@1101.0.7 + - @pnpm/lockfile.preferred-versions@1100.0.10 + - @pnpm/lockfile.utils@1100.0.8 + - @pnpm/lockfile.verification@1100.0.11 + - @pnpm/exec.lifecycle@1100.0.11 + - @pnpm/installing.linking.modules-cleaner@1100.1.2 + - @pnpm/building.policy@1100.0.5 + - @pnpm/lockfile.to-pnp@1100.0.9 + - @pnpm/bins.remover@1100.0.4 + - @pnpm/fs.symlink-dependency@1100.0.4 + - @pnpm/installing.linking.direct-dep-linker@1100.0.4 + - @pnpm/installing.linking.hoist@1100.0.7 + - @pnpm/pkg-manifest.utils@1100.1.4 + - @pnpm/agent.client@1.0.6 + - @pnpm/lockfile.filtering@1100.1.1 + - @pnpm/lockfile.pruner@1100.0.6 + - @pnpm/lockfile.settings-checker@1100.0.11 + - @pnpm/lockfile.walker@1100.0.6 + - @pnpm/worker@1100.1.6 + - @pnpm/bins.linker@1100.0.7 + - @pnpm/workspace.project-manifest-reader@1100.0.6 + - @pnpm/crypto.hash@1100.0.1 + ## 1101.1.2 ### Patch Changes diff --git a/installing/deps-installer/package.json b/installing/deps-installer/package.json index ae26ae5ace..28c003e6ec 100644 --- a/installing/deps-installer/package.json +++ b/installing/deps-installer/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/installing.deps-installer", - "version": "1101.1.2", + "version": "1101.5.0", "description": "Fast, disk space efficient installation engine", "keywords": [ "pnpm", @@ -75,7 +75,6 @@ "@pnpm/deps.graph-hasher": "workspace:*", "@pnpm/deps.graph-sequencer": "workspace:*", "@pnpm/deps.path": "workspace:*", - "@pnpm/engine.runtime.system-node-version": "workspace:*", "@pnpm/error": "workspace:*", "@pnpm/exec.lifecycle": "workspace:*", "@pnpm/fs.read-modules-dir": "workspace:*", @@ -144,6 +143,7 @@ "@pnpm/test-fixtures": "workspace:*", "@pnpm/test-ipc-server": "workspace:*", "@pnpm/testing.mock-agent": "workspace:*", + "@pnpm/testing.registry-mock": "workspace:*", "@pnpm/testing.temp-store": "workspace:*", "@types/fs-extra": "catalog:", "@types/is-windows": "catalog:", diff --git a/installing/deps-installer/src/install/extendInstallOptions.ts b/installing/deps-installer/src/install/extendInstallOptions.ts index 584c4a829f..4b3a189025 100644 --- a/installing/deps-installer/src/install/extendInstallOptions.ts +++ b/installing/deps-installer/src/install/extendInstallOptions.ts @@ -11,7 +11,7 @@ import type { ProjectOptions } from '@pnpm/installing.context' import type { HoistingLimits } from '@pnpm/installing.deps-restorer' import type { IncludedDependencies } from '@pnpm/installing.modules-yaml' import type { LockfileObject } from '@pnpm/lockfile.fs' -import type { ResolutionVerifier, WorkspacePackages } from '@pnpm/resolving.resolver-base' +import type { ResolutionPolicyViolation, ResolutionVerifier, WorkspacePackages } from '@pnpm/resolving.resolver-base' import type { StoreController } from '@pnpm/store.controller-types' import type { AllowedDeprecatedVersions, @@ -51,6 +51,7 @@ export interface StrictInstallOptions { lockfileOnly: boolean forceFullResolution: boolean fixLockfile: boolean + updateChecksums: boolean dedupe: boolean ignoreCompatibilityDb: boolean ignorePackageManifest: boolean @@ -176,19 +177,78 @@ export interface StrictInstallOptions { minimumReleaseAge?: number minimumReleaseAgeExclude?: string[] /** - * Optional verifier that re-checks each lockfile-pinned resolution - * against policies configured upstream (today: minimumReleaseAge strict - * mode). Constructed by `createClient` and surfaced via the - * `createStoreController` return; mutateModules invokes it once, right - * after the lockfile is loaded from disk. When omitted, no revalidation - * runs. + * Resolver-agnostic post-tree gate, invoked between + * `resolveDependencyTree` and `resolvePeers` inside + * `resolveDependencies`. Receives the violations the verifier + * fan-out collected from the freshly-resolved tree. Throwing here + * unwinds the install before peer-dep resolution runs — nothing on + * disk has changed, and the (potentially expensive) peer pass is + * skipped on abort. + * + * Intentionally policy-neutral. Each verifier owns its violation + * codes (`MINIMUM_RELEASE_AGE_VIOLATION`, `TRUST_DOWNGRADE`, …); the + * install command filters by code to decide what to do. Future + * resolvers can plug verifiers in without touching this signature. */ - verifyResolution?: ResolutionVerifier + handleResolutionPolicyViolations?: ( + violations: readonly ResolutionPolicyViolation[] + ) => Promise + /** + * Resolver-side verifiers that re-check each lockfile-pinned resolution + * against policies configured upstream (today: at most one, + * `npm.minimumReleaseAge` in strict mode). Constructed by `createClient` + * and surfaced via the `createStoreController` return; mutateModules + * fans out across the list once, right after the lockfile is loaded + * from disk. Empty when no policy is active. + */ + resolutionVerifiers: ResolutionVerifier[] + /** + * pnpm's on-disk cache directory. When set together with non-empty + * `resolutionVerifiers`, the lockfile verification result is memoized + * in `/lockfile-verified.jsonl` so repeat installs against an + * unchanged lockfile skip the per-package registry round trip. The + * record is policy-neutral; each active resolver-side verifier writes + * its own slot under `verifiers[]`. + */ + cacheDir?: string trustPolicy?: TrustPolicy trustPolicyExclude?: string[] trustPolicyIgnoreAfter?: number + /** + * Skip the lockfile supply-chain verification pass entirely. When + * true, `verifyLockfileResolutions` is not called even if + * `resolutionVerifiers` is non-empty — the install trusts the + * lockfile as-is. Trade-off: a poisoned lockfile (e.g. one a + * contributor authored under a weaker policy than CI enforces) can + * slip through. Use only in environments where the lockfile is + * effectively part of the trusted base — closed-source projects + * where every commit comes from a trusted author, fully reproducible + * CI runs against an already-verified lockfile, etc. + * + * Added for #11860: on workspaces with thousands of locked entries, + * the verification pass holds the per-package registry metadata + * needed for the trust check resident in memory and can OOM CI + * runners with a 2GB heap cap. + */ + trustLockfile?: boolean packageVulnerabilityAudit?: PackageVulnerabilityAudit blockExoticSubdeps?: boolean + /** + * Optional alternative install engine. When set, the frozen-install + * path invokes this callback instead of `headlessInstall`. The CLI + * layer constructs it (today: spawning the pacquet binary installed + * via `configDependencies` and forwarding pnpm's own CLI argv); the + * installer treats it as an opaque "do the install" hook so it + * doesn't need to know about pacquet's binary path, CLI surface, or + * any settings that only pacquet consumes. + * + * `filterResolvedProgress` tells the helper to drop the engine's + * own `pnpm:progress status:resolved` events because pnpm already + * emitted one per package during a preceding lockfileOnly resolve + * pass. The frozen-install path passes `false` (or nothing): no + * resolve pass ran, so the engine's events are the only source. + */ + runPacquet?: (opts?: { filterResolvedProgress?: boolean }) => Promise /** * If true, `mutateModules` does not emit the per-install `summary` log * event. Used by `pnpm add -g` when it runs multiple isolated installs @@ -243,6 +303,7 @@ const defaults = (opts: InstallOptions): StrictInstallOptions => { }, lockfileDir: opts.lockfileDir ?? opts.dir ?? process.cwd(), lockfileOnly: false, + updateChecksums: false, nodeVersion: opts.nodeVersion, nodeLinker: 'isolated', overrides: {}, @@ -301,6 +362,7 @@ const defaults = (opts: InstallOptions): StrictInstallOptions => { peersSuffixMaxLength: 1000, blockExoticSubdeps: false, omitSummaryLog: false, + resolutionVerifiers: [] as ResolutionVerifier[], } as StrictInstallOptions } diff --git a/installing/deps-installer/src/install/index.ts b/installing/deps-installer/src/install/index.ts index 5b4ecd7564..be63a2576b 100644 --- a/installing/deps-installer/src/install/index.ts +++ b/installing/deps-installer/src/install/index.ts @@ -42,6 +42,8 @@ import { writeModulesManifest } from '@pnpm/installing.modules-yaml' import { type CatalogSnapshots, cleanGitBranchLockfiles, + getWantedLockfileName, + isEmptyLockfile, type LockfileObject, type ProjectSnapshot, readWantedLockfile, @@ -63,6 +65,7 @@ import { createVersionSpecFromResolvedVersion, getAllDependenciesFromManifest, g import { parseWantedDependency } from '@pnpm/resolving.parse-wanted-dependency' import type { PreferredVersions, + ResolutionPolicyViolation, } from '@pnpm/resolving.resolver-base' import type { AllowBuild, @@ -96,6 +99,8 @@ import { linkPackages } from './link.js' import { reportPeerDependencyIssues } from './reportPeerDependencyIssues.js' import { validateModules } from './validateModules.js' import { verifyLockfileResolutions } from './verifyLockfileResolutions.js' +import { writeLockfilesAndRecordVerified } from './writeLockfilesAndRecordVerified.js' +import { writeWantedLockfileAndRecordVerified } from './writeWantedLockfileAndRecordVerified.js' class LockfileConfigMismatchError extends PnpmError { constructor (outdatedLockfileSettingName: string) { @@ -159,6 +164,8 @@ export interface InstallResult { updatedCatalogs: Catalogs | undefined updatedManifest: ProjectManifest ignoredBuilds: IgnoredBuilds | undefined + /** Forwarded from {@link MutateModulesResult.resolutionPolicyViolations}. */ + resolutionPolicyViolations: ResolutionPolicyViolation[] } export async function install ( @@ -173,7 +180,7 @@ export async function install ( return installFromPnpmRegistry(manifest, rootDir, opts) } - const { updatedCatalogs, updatedProjects: projects, ignoredBuilds } = await mutateModules( + const { updatedCatalogs, updatedProjects: projects, ignoredBuilds, resolutionPolicyViolations } = await mutateModules( [ { mutation: 'install', @@ -195,7 +202,7 @@ export async function install ( }], } ) - return { updatedCatalogs, updatedManifest: projects[0].manifest, ignoredBuilds } + return { updatedCatalogs, updatedManifest: projects[0].manifest, ignoredBuilds, resolutionPolicyViolations } } interface ProjectToBeInstalled { @@ -219,6 +226,8 @@ export interface MutateModulesInSingleProjectResult { updatedCatalogs: Catalogs | undefined updatedProject: UpdatedProject ignoredBuilds: IgnoredBuilds | undefined + /** Forwarded from {@link MutateModulesResult.resolutionPolicyViolations}. */ + resolutionPolicyViolations: ResolutionPolicyViolation[] } export async function mutateModulesInSingleProject ( @@ -252,6 +261,7 @@ export async function mutateModulesInSingleProject ( updatedCatalogs: result.updatedCatalogs, updatedProject: result.updatedProjects[0], ignoredBuilds: result.ignoredBuilds, + resolutionPolicyViolations: result.resolutionPolicyViolations, } } @@ -261,6 +271,15 @@ export interface MutateModulesResult { stats: InstallationResultStats depsRequiringBuild?: DepPath[] ignoredBuilds: IgnoredBuilds | undefined + /** + * Resolver-policy violations the post-resolution scan found in the + * freshly-resolved lockfile. Each violation carries a verifier code + * (e.g. `MINIMUM_RELEASE_AGE_VIOLATION`, `TRUST_DOWNGRADE`); the + * install command filters by code to decide what to do (persist to + * `minimumReleaseAgeExclude`, log, etc.). Empty array when no + * verifier reported a violation or no policy was active. + */ + resolutionPolicyViolations: ResolutionPolicyViolation[] } const pickCatalogSpecifier: CatalogResultMatcher = { @@ -343,15 +362,49 @@ export async function mutateModules ( // resolver's own filters already cover fresh resolution. We run this // exactly once, right after the lockfile is loaded from disk, before any // path branches. - try { - await verifyLockfileResolutions(ctx.wantedLockfile, opts.verifyResolution) - } catch (err) { - // verifyLockfileResolutions is the one throw site in this function - // that's part of normal user-facing operation (a rejected lockfile); - // other throws here are unexpected. Detach the reporter listener so - // long-lived processes don't leak it on every rejected install. - detachReporter() - throw err + // + // Skipped when we already know pacquet will run the install: pacquet's + // frozen-install path applies the same resolver-policy gate (port of + // this function), so re-running here would duplicate the work — and + // for `minimumReleaseAge` in strict mode each lockfile entry is an + // HTTP probe. + // + // The predicate mirrors every short-circuit `tryFrozenInstall` checks + // before reaching the pacquet branch: anything that would make it + // return null, throw, or fall through to the JS path must keep + // verification on. The optimistic `preferFrozenLockfile` path decides + // whether to delegate later (based on `allProjectsAreUpToDate`), which + // isn't known here — so verification still runs in that window, the + // duplicate is bounded to it. + const willDelegateToPacquet = opts.runPacquet != null && + installsOnly && + !opts.lockfileOnly && + !opts.fixLockfile && + !opts.dedupe && + !ctx.lockfileHadConflicts && + ctx.existsNonEmptyWantedLockfile && + (opts.frozenLockfile === true || opts.frozenLockfileIfExists === true) + if (!willDelegateToPacquet && !opts.trustLockfile) { + const cacheActive = opts.cacheDir != null && opts.resolutionVerifiers.length > 0 + const wantedLockfilePath = cacheActive + ? path.resolve(ctx.lockfileDir, await getWantedLockfileName({ + useGitBranchLockfile: opts.useGitBranchLockfile, + mergeGitBranchLockfiles: opts.mergeGitBranchLockfiles, + })) + : undefined + try { + await verifyLockfileResolutions(ctx.wantedLockfile, opts.resolutionVerifiers, { + cacheDir: opts.cacheDir, + lockfilePath: wantedLockfilePath, + }) + } catch (err) { + // verifyLockfileResolutions is the one throw site in this function + // that's part of normal user-facing operation (a rejected lockfile); + // other throws here are unexpected. Detach the reporter listener so + // long-lived processes don't leak it on every rejected install. + detachReporter() + throw err + } } if (opts.hooks.preResolution) { @@ -449,6 +502,7 @@ export async function mutateModules ( stats: result.stats ?? { added: 0, removed: 0, linkedToRoot: 0 }, depsRequiringBuild: result.depsRequiringBuild, ignoredBuilds, + resolutionPolicyViolations: result.resolutionPolicyViolations ?? [], } interface InnerInstallResult { @@ -457,6 +511,7 @@ export async function mutateModules ( readonly stats?: InstallationResultStats readonly depsRequiringBuild?: DepPath[] readonly ignoredBuilds: IgnoredBuilds | undefined + readonly resolutionPolicyViolations?: ResolutionPolicyViolation[] } async function _install (): Promise { @@ -537,6 +592,7 @@ export async function mutateModules ( const upToDateLockfileMajorVersion = ctx.wantedLockfile.lockfileVersion.toString().startsWith(`${LOCKFILE_MAJOR_VERSION}.`) let needsFullResolution = outdatedLockfileSettings || opts.fixLockfile || + opts.updateChecksums || !upToDateLockfileMajorVersion || opts.forceFullResolution || forceResolutionFromHook @@ -796,6 +852,7 @@ export async function mutateModules ( stats: result.stats, depsRequiringBuild: result.depsRequiringBuild, ignoredBuilds: result.ignoredBuilds, + resolutionPolicyViolations: result.resolutionPolicyViolations, } } @@ -853,7 +910,7 @@ export async function mutateModules ( !needsFullResolution && opts.preferFrozenLockfile && (!opts.pruneLockfileImporters || Object.keys(ctx.wantedLockfile.importers).length === Object.keys(ctx.projects).length) && - ctx.existsNonEmptyWantedLockfile && + !isEmptyLockfile(ctx.wantedLockfile) && ctx.wantedLockfile.lockfileVersion === LOCKFILE_VERSION && await allProjectsAreUpToDate(Object.values(ctx.projects), { catalogs: opts.catalogs, @@ -883,6 +940,17 @@ Note that in CI environments, this setting is enabled by default.`, ) } if (!opts.ignorePackageManifest) { + // `--frozen-lockfile` (the CI default) means "fail if pnpm-lock.yaml is + // out of sync." Treat its absence as a sync failure even when the + // synthesized snapshot from node_modules/.pnpm/lock.yaml would satisfy + // the manifest — the developer needs to commit the regenerated file. + if (frozenLockfile && !ctx.existsWantedLockfile && + Object.values(ctx.projects).some((project) => pkgHasDependencies(project.manifest))) { + throw new PnpmError('NO_LOCKFILE', + `Cannot install with "frozen-lockfile" because ${WANTED_LOCKFILE} is absent`, { + hint: 'Note that in CI environments this setting is true by default. If you still need to run install in such cases, use "pnpm install --no-frozen-lockfile"', + }) + } const _satisfiesPackageManifest = satisfiesPackageManifest.bind(null, { autoInstallPeers: opts.autoInstallPeers, excludeLinksFromLockfile: opts.excludeLinksFromLockfile, @@ -916,7 +984,7 @@ Note that in CI environments, this setting is enabled by default.`, ignoredBuilds: undefined, } } - if (!ctx.existsNonEmptyWantedLockfile) { + if (isEmptyLockfile(ctx.wantedLockfile)) { if (Object.values(ctx.projects).some((project) => pkgHasDependencies(project.manifest))) { throw new Error(`Headless installation requires a ${WANTED_LOCKFILE} file`) } @@ -928,6 +996,27 @@ Note that in CI environments, this setting is enabled by default.`, } else { logger.info({ message: 'Lockfile is up to date, resolution step is skipped', prefix: opts.lockfileDir }) } + if (opts.runPacquet != null) { + try { + await opts.runPacquet() + } catch (err) { + // Same reasoning as the verifyLockfileResolutions catch above: this + // is the user-facing failure path, so detach the reporter listener + // before rethrowing so long-lived processes don't leak it. + detachReporter() + throw err + } + return { + updatedProjects: projects.map((mutatedProject) => { + const project = ctx.projects[mutatedProject.rootDir] + return { + ...project, + manifest: project.originalManifest ?? project.manifest, + } + }), + ignoredBuilds: undefined, + } + } try { const { stats, ignoredBuilds } = await headlessInstall({ ...ctx, @@ -971,21 +1060,16 @@ Note that in CI environments, this setting is enabled by default.`, ignoredBuilds, } } catch (error: any) { // eslint-disable-line + const isIntegrityError = BROKEN_LOCKFILE_INTEGRITY_ERRORS.has(error.code) if ( frozenLockfile || ( error.code !== 'ERR_PNPM_LOCKFILE_MISSING_DEPENDENCY' && - !BROKEN_LOCKFILE_INTEGRITY_ERRORS.has(error.code) + !isIntegrityError ) || - (!ctx.existsNonEmptyWantedLockfile && !ctx.existsCurrentLockfile) + (!ctx.existsNonEmptyWantedLockfile && !ctx.existsCurrentLockfile) || + (isIntegrityError && !opts.updateChecksums) ) throw error - if (BROKEN_LOCKFILE_INTEGRITY_ERRORS.has(error.code)) { - needsFullResolution = true - // Ideally, we would not update but currently there is no other way to redownload the integrity of the package - for (const project of projects) { - (project as InstallMutationOptions).update = true - } - } // A broken lockfile may be caused by a badly resolved Git conflict logger.warn({ error, @@ -1155,7 +1239,7 @@ export async function addDependenciesToPackage ( } & InstallMutationOptions ): Promise { const rootDir = (opts.dir ?? process.cwd()) as ProjectRootDir - const { updatedCatalogs, updatedProjects: projects, ignoredBuilds } = await mutateModules( + const { updatedCatalogs, updatedProjects: projects, ignoredBuilds, resolutionPolicyViolations } = await mutateModules( [ { allowNew: opts.allowNew, @@ -1183,7 +1267,7 @@ export async function addDependenciesToPackage ( }, ], }) - return { updatedCatalogs, updatedManifest: projects[0].manifest, ignoredBuilds } + return { updatedCatalogs, updatedManifest: projects[0].manifest, ignoredBuilds, resolutionPolicyViolations } } export type ImporterToUpdate = { @@ -1214,6 +1298,7 @@ interface InstallFunctionResult { stats?: InstallationResultStats depsRequiringBuild: DepPath[] ignoredBuilds?: IgnoredBuilds + resolutionPolicyViolations: ResolutionPolicyViolation[] } type InstallFunction = ( @@ -1328,6 +1413,7 @@ const _installInContext: InstallFunction = async (projects, ctx, opts) => { peerDependencyIssuesByProjects, wantedToBeSkippedPackageIds, waitTillAllFetchingsFinish, + resolutionPolicyViolations, } = await resolveDependencies( projects, { @@ -1349,6 +1435,7 @@ const _installInContext: InstallFunction = async (projects, ctx, opts) => { excludeLinksFromLockfile: opts.excludeLinksFromLockfile, force: opts.force, forceFullResolution, + updateChecksums: opts.updateChecksums, ignoreScripts: opts.ignoreScripts, hooks: { readPackage: opts.readPackageHook, @@ -1384,6 +1471,7 @@ const _installInContext: InstallFunction = async (projects, ctx, opts) => { trustPolicyIgnoreAfter: opts.trustPolicyIgnoreAfter, blockExoticSubdeps: opts.blockExoticSubdeps, allProjectIds: Object.values(ctx.projects).map((p) => p.id), + handleResolutionPolicyViolations: opts.handleResolutionPolicyViolations, } ) if (!opts.include.optionalDependencies || !opts.include.devDependencies || !opts.include.dependencies) { @@ -1633,11 +1721,13 @@ const _installInContext: InstallFunction = async (projects, ctx, opts) => { const currentLockfileDir = path.join(ctx.rootModulesDir, '.pnpm') await Promise.all([ opts.useLockfile && opts.saveLockfile - ? writeLockfiles({ + ? writeLockfilesAndRecordVerified({ currentLockfile: result.currentLockfile, currentLockfileDir, wantedLockfile: newLockfile, wantedLockfileDir: ctx.lockfileDir, + cacheDir: opts.cacheDir, + resolutionVerifiers: opts.resolutionVerifiers, ...lockfileOpts, }) : writeCurrentLockfile(ctx.virtualStoreDir, result.currentLockfile), @@ -1692,11 +1782,25 @@ const _installInContext: InstallFunction = async (projects, ctx, opts) => { } } else { if (opts.useLockfile && opts.saveLockfile && !isInstallationOnlyForLockfileCheck) { - await writeWantedLockfile(ctx.lockfileDir, newLockfile, lockfileOpts) + await writeWantedLockfileAndRecordVerified({ + lockfileDir: ctx.lockfileDir, + lockfile: newLockfile, + cacheDir: opts.cacheDir, + resolutionVerifiers: opts.resolutionVerifiers, + ...lockfileOpts, + }) } - if (opts.nodeLinker !== 'hoisted') { - // This is only needed because otherwise the reporter will hang + if (opts.nodeLinker !== 'hoisted' && opts.runPacquet == null) { + // This is only needed because otherwise the reporter will hang. + // Skipped when pacquet is about to take over the materialization + // phase: the default reporter completes the progress stream for + // this prefix on `importing_done`, so emitting it from the + // lockfileOnly resolve pass would prematurely close the stream + // and pacquet's own `importing_started` / progress events would + // render to a stale stream. Pacquet emits its own + // `importing_done` after the install, which closes the stream + // normally. stageLogger.debug({ prefix: opts.lockfileDir, stage: 'importing_done', @@ -1722,7 +1826,15 @@ const _installInContext: InstallFunction = async (projects, ctx, opts) => { rules: opts.peerDependencyRules, }) - if (!opts.omitSummaryLog) { + // Skipped when pacquet will take over the materialization. The + // default reporter's `reportSummary` `take(1)`s the first summary + // event and combines it with whatever `pkgsDiff` it has at that + // moment — which is empty here, since pacquet hasn't emitted its + // per-direct-dep `pnpm:root` events yet. Letting pnpm fire summary + // now would lock in an empty diff. Pacquet emits its own + // `pnpm:summary` after the install completes, by which point its + // root events have populated the diff. + if (!opts.omitSummaryLog && opts.runPacquet == null) { summaryLogger.debug({ prefix: opts.lockfileDir }) } @@ -1745,6 +1857,7 @@ const _installInContext: InstallFunction = async (projects, ctx, opts) => { stats, depsRequiringBuild, ignoredBuilds, + resolutionPolicyViolations, } } @@ -1752,6 +1865,37 @@ function allMutationsAreInstalls (projects: MutatedProject[]): boolean { return projects.every((project) => project.mutation === 'install' && !project.update && !project.updateMatching) } +/** + * Run the pacquet binary if it's configured, otherwise run the JS + * `headlessInstall`. Callers can hand off any code path that materializes + * an already-resolved lockfile (workspace partial install, hoisted + * linker, agent-server install, frozen install) without restating the + * delegation choice. + * + * Pacquet reads the wanted lockfile from disk and produces its own + * `pnpm:stats` / `pnpm:ignored-scripts` log events that drive the + * reporter. The structured stats / ignoredBuilds return values that + * `headlessInstall` produces aren't recovered here — pacquet doesn't + * surface them through any return path — so callers get `undefined` for + * both. `mutateModules` already tolerates that (it falls back to a zero + * stats record and a no-op ignoredBuilds iteration). + */ +async function materializeOrDelegate ( + opts: { runPacquet?: (opts?: { filterResolvedProgress?: boolean }) => Promise }, + runHeadlessInstall: () => Promise<{ stats: InstallationResultStats, ignoredBuilds: IgnoredBuilds | undefined }> +): Promise<{ stats?: InstallationResultStats, ignoredBuilds?: IgnoredBuilds }> { + if (opts.runPacquet != null) { + // Reached only from the resolve-then-materialize call sites + // (workspace-partial, hoisted-linker, agent install). Each ran a + // lockfileOnly resolve pass that emitted one + // `pnpm:progress status:resolved` per package, so pacquet's + // duplicate `resolved` events would double the reporter's count. + await opts.runPacquet({ filterResolvedProgress: true }) + return {} + } + return runHeadlessInstall() +} + const installInContext: InstallFunction = async (projects, ctx, opts) => { try { const isPathInsideWorkspace = isSubdir.bind(null, opts.lockfileDir) @@ -1788,7 +1932,7 @@ const installInContext: InstallFunction = async (projects, ctx, opts) => { ...opts, lockfileOnly: true, }) - const { stats, ignoredBuilds } = await headlessInstall({ + const { stats, ignoredBuilds } = await materializeOrDelegate(opts, () => headlessInstall({ ...ctx, ...opts, currentEngine: { @@ -1802,7 +1946,7 @@ const installInContext: InstallFunction = async (projects, ctx, opts) => { wantedLockfile: result.newLockfile, useLockfile: opts.useLockfile && ctx.wantedLockfileIsModified, hoistWorkspacePackages: opts.hoistWorkspacePackages, - }) + })) return { ...result, stats, @@ -1815,7 +1959,7 @@ const installInContext: InstallFunction = async (projects, ctx, opts) => { ...opts, lockfileOnly: true, }) - const { stats, ignoredBuilds } = await headlessInstall({ + const { stats, ignoredBuilds } = await materializeOrDelegate(opts, () => headlessInstall({ ...ctx, ...opts, currentEngine: { @@ -1829,30 +1973,43 @@ const installInContext: InstallFunction = async (projects, ctx, opts) => { wantedLockfile: result.newLockfile, useLockfile: opts.useLockfile && ctx.wantedLockfileIsModified, hoistWorkspacePackages: opts.hoistWorkspacePackages, - }) + })) return { ...result, stats, ignoredBuilds, } } + // Isolated `nodeLinker` (the default) with a non-frozen install: + // pacquet doesn't ship a resolver yet, so split the install in two — + // ask `_installInContext` for a `lockfileOnly` resolve pass (writes + // `pnpm-lock.yaml`), then hand the freshly-written lockfile to + // pacquet for the fetch / import / link / build phases. The frozen + // branch is handled earlier in `tryFrozenInstall`; the hoisted + // branch above already runs the same resolve-then-materialize + // sequence (it had to even before pacquet existed). When no pacquet + // is configured this falls through to the full single-pass install. + if (opts.runPacquet != null && !opts.lockfileOnly) { + const result = await _installInContext(projects, ctx, { ...opts, lockfileOnly: true }) + // The resolve pass above emitted a `pnpm:progress status:resolved` + // per package; ask pacquet to drop its own duplicates. + await opts.runPacquet({ filterResolvedProgress: true }) + return result + } return await _installInContext(projects, ctx, opts) } catch (error: any) { // eslint-disable-line if ( !BROKEN_LOCKFILE_INTEGRITY_ERRORS.has(error.code) || - (!ctx.existsNonEmptyWantedLockfile && !ctx.existsCurrentLockfile) + (!ctx.existsNonEmptyWantedLockfile && !ctx.existsCurrentLockfile) || + !opts.updateChecksums ) throw error opts.needsFullResolution = true - // Ideally, we would not update but currently there is no other way to redownload the integrity of the package - for (const project of projects) { - (project as InstallMutationOptions).update = true - } logger.warn({ error, message: error.message, prefix: ctx.lockfileDir, }) - logger.error(new PnpmError(error.code, 'The lockfile is broken! A full installation will be performed in an attempt to fix it.')) + logger.error(new PnpmError(error.code, 'Refreshing the locked integrity from the registry as requested by --update-checksums. A full installation will be performed.')) return _installInContext(projects, ctx, opts) } finally { await opts.storeController.close() @@ -2170,6 +2327,18 @@ async function installFromPnpmRegistry ( opts: Opts, allInstallProjects?: Array<{ rootDir: ProjectRootDir, manifest: ProjectManifest }> ): Promise { + // The agent path skips client-side resolution, so resolver-side policies + // can't be enforced locally. `minimumReleaseAge` is forwarded to the + // agent and enforced server-side. `trustPolicy` has no server-side + // counterpart yet, so refuse to run under it instead of silently + // letting through a lockfile the local verifier would reject. + if (opts.trustPolicy === 'no-downgrade') { + throw new PnpmError( + 'TRUST_POLICY_INCOMPATIBLE_WITH_AGENT', + 'The pnpm agent does not yet enforce `trustPolicy: no-downgrade`, so running an install through the agent under this policy would produce a lockfile that the local verifier rejects.', + { hint: 'Unset `trustPolicy` for this install, or disable the agent (unset `--agent` / `agent` in pnpm-workspace.yaml) so resolution runs locally and the trust check applies.' } + ) + } const { fetchFromPnpmRegistry } = await import('@pnpm/agent.client') const { StoreIndex } = await import('@pnpm/store.index') const { setImportConcurrency } = await import('@pnpm/worker') @@ -2227,7 +2396,14 @@ async function installFromPnpmRegistry ( storeIndex.close() } - await writeWantedLockfile(lockfileDir, lockfile) + await writeWantedLockfileAndRecordVerified({ + lockfileDir, + lockfile, + cacheDir: opts.cacheDir, + resolutionVerifiers: opts.resolutionVerifiers, + useGitBranchLockfile: opts.useGitBranchLockfile, + mergeGitBranchLockfiles: opts.mergeGitBranchLockfiles, + }) logger.info({ message: `Resolved ${agentStats.totalPackages} packages: ${agentStats.alreadyInStore} cached, ${agentStats.filesToDownload} files to download`, @@ -2308,15 +2484,28 @@ async function installFromPnpmRegistry ( skipped: new Set(), wantedLockfile: lockfile, } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const { ignoredBuilds, stats } = await headlessInstall(headlessOpts as any) + const { ignoredBuilds, stats } = await materializeOrDelegate( + opts, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + () => headlessInstall(headlessOpts as any) + ) return { updatedCatalogs: undefined, updatedManifest: manifest, ignoredBuilds, - stats, + // Pacquet doesn't surface a structured stats return; default to + // zeros so the agent-path's non-optional `stats` slot is filled. + // The reporter still renders accurate counts from pacquet's + // `pnpm:stats` log events. + stats: stats ?? { added: 0, removed: 0, linkedToRoot: 0 }, lockfile, + // Server-side resolution (pnpm agent) enforces `minimumReleaseAge` + // itself — the agent picks only mature versions and the lockfile + // can't contain immature entries to auto-collect. `trustPolicy` is + // guarded above (we refuse to enter this path when it's set), so + // there's nothing for the install command to react to here. + resolutionPolicyViolations: [], } } finally { // Close the storeController to flush queued StoreIndex writes — the diff --git a/installing/deps-installer/src/install/link.ts b/installing/deps-installer/src/install/link.ts index 6d66f8dc0e..e331733b84 100644 --- a/installing/deps-installer/src/install/link.ts +++ b/installing/deps-installer/src/install/link.ts @@ -6,8 +6,7 @@ import { stageLogger, statsLogger, } from '@pnpm/core-loggers' -import { calcDepState, type DepsStateCache } from '@pnpm/deps.graph-hasher' -import { findRuntimeNodeVersion } from '@pnpm/engine.runtime.system-node-version' +import { calcDepState, type DepsStateCache, findRuntimeNodeVersion } from '@pnpm/deps.graph-hasher' import { symlinkDependency } from '@pnpm/fs.symlink-dependency' import type { DependenciesGraph, diff --git a/installing/deps-installer/src/install/recordLockfileVerified.ts b/installing/deps-installer/src/install/recordLockfileVerified.ts new file mode 100644 index 0000000000..f837537b01 --- /dev/null +++ b/installing/deps-installer/src/install/recordLockfileVerified.ts @@ -0,0 +1,36 @@ +import { hashObject } from '@pnpm/crypto.object-hasher' +import type { LockfileObject } from '@pnpm/lockfile.fs' +import type { ResolutionVerifier } from '@pnpm/resolving.resolver-base' + +import { recordVerification } from './verifyLockfileResolutionsCache.js' + +export interface RecordLockfileVerifiedOptions { + cacheDir?: string + /** Absolute path of the lockfile the next install will read. + * Under `useGitBranchLockfile` this is the branch-suffixed name. */ + lockfilePath: string + /** The writer's canonical return value — see {@link writeWantedLockfile}. + * Passing the raw in-memory write object would record a hash the + * next install can't match (YAML drops undefined fields). */ + lockfile: LockfileObject + resolutionVerifiers: readonly ResolutionVerifier[] | undefined +} + +/** + * Records the post-resolution lockfile as verified so the next install + * skips the registry round-trip. Skipping is safe: fresh local picks + * are filtered by the resolver (see + * `resolving/npm-resolver/src/pickPackage.ts`) and carried-over entries + * already passed the gate at the top of `mutateModules`, so the + * recorded lockfile is policy-clean by construction. + */ +export function recordLockfileVerified (opts: RecordLockfileVerifiedOptions): void { + if (!opts.cacheDir) return + if (!opts.resolutionVerifiers?.length) return + if (!opts.lockfile.packages) return + recordVerification(opts.cacheDir, { + lockfilePath: opts.lockfilePath, + verifiers: opts.resolutionVerifiers, + hashLockfile: () => hashObject(opts.lockfile), + }) +} diff --git a/installing/deps-installer/src/install/verifyLockfileResolutions.ts b/installing/deps-installer/src/install/verifyLockfileResolutions.ts index 1f1ad89773..c2ac9a2c2e 100644 --- a/installing/deps-installer/src/install/verifyLockfileResolutions.ts +++ b/installing/deps-installer/src/install/verifyLockfileResolutions.ts @@ -1,15 +1,25 @@ +import { lockfileVerificationLogger } from '@pnpm/core-loggers' +import { hashObject } from '@pnpm/crypto.object-hasher' import { PnpmError } from '@pnpm/error' import type { LockfileObject } from '@pnpm/lockfile.fs' import { nameVerFromPkgSnapshot } from '@pnpm/lockfile.utils' -import type { ResolutionVerifier } from '@pnpm/resolving.resolver-base' +import type { + Resolution, + ResolutionPolicyViolation, + ResolutionVerifier, +} from '@pnpm/resolving.resolver-base' import type { DepPath } from '@pnpm/types' import pLimit from 'p-limit' -interface Violation { - pkgId: string - code: string - reason: string -} +import { + recordVerification, + tryLockfileVerificationCache, +} from './verifyLockfileResolutionsCache.js' + +// Re-exported for back-compat with the existing import surface. +// The interface itself lives in resolver-base so deps-resolver can +// participate in the same shape; see the doc there. +export type { ResolutionPolicyViolation } // Cap the per-entry breakdown so a verifier rejecting hundreds of entries // (e.g. a poisoned lockfile) doesn't flood the terminal / CI log; the full @@ -21,69 +31,167 @@ const MAX_VIOLATIONS_TO_PRINT = 20 // verification pass doesn't push past what the rest of the install respects. const DEFAULT_CONCURRENCY = 16 +export interface VerifyLockfileResolutionsOptions { + concurrency?: number + /** + * pnpm's on-disk cache directory. When set together with + * `lockfilePath`, verification results are memoized in + * `/lockfile-verified.jsonl` and the gate short-circuits on + * a repeat run against an unchanged lockfile + same-or-stricter + * policy. Omit to disable the cache entirely (every call rehashes + * the lockfile and re-verifies). + */ + cacheDir?: string + /** Absolute path of the lockfile being verified. Used by the cache's stat shortcut. */ + lockfilePath?: string +} + /** - * Policy-neutral pass that asks each resolver-supplied {@link ResolutionVerifier} - * to check every entry in a lockfile loaded from disk. Iteration runs - * before resolution decisions are touched and before any tarball is - * fetched, so a lockfile whose entries were resolved elsewhere (committed - * to the repo, restored from a cache, etc.) under a weaker or absent - * policy cannot reach the filesystem. Fresh local resolution is covered - * by the resolver's own per-version filter. + * Policy-neutral pass that asks every resolver-supplied + * {@link ResolutionVerifier} to check every entry in a lockfile loaded + * from disk. Iteration runs before resolution decisions are touched and + * before any tarball is fetched, so a lockfile whose entries were + * resolved elsewhere (committed to the repo, restored from a cache, + * etc.) under a weaker or absent policy cannot reach the filesystem. + * Fresh local resolution is covered by the resolver's own per-version + * filter. * - * Designed for fail-closed semantics at the verifier level: a verifier that - * can't confirm a resolution is expected to return `{ ok: false }` rather - * than passing silently — otherwise a registry hiccup or an unpublished - * version would re-open the bypass. + * Each verifier handles its own protocol short-circuit inside `verify` + * (returning `{ ok: true }` for resolutions outside its scope), so the + * fan-out is policy-neutral and dispatch-free at this layer. * - * No-op when `verifyResolution` is undefined (no active policies). + * Designed for fail-closed semantics at the verifier level: a verifier + * that can't confirm a resolution is expected to return `{ ok: false }` + * rather than passing silently — otherwise a registry hiccup or an + * unpublished version would re-open the bypass. + * + * No-op when `verifiers` is empty. + * + * When `options.cacheDir` and `options.lockfilePath` are both + * provided, an unchanged lockfile that has already been verified + * under the same (or stricter) policy short-circuits the registry + * round-trip entirely — see {@link tryLockfileVerificationCache} for + * the lookup logic. */ export async function verifyLockfileResolutions ( lockfile: LockfileObject, - verifyResolution: ResolutionVerifier | undefined, - options?: { concurrency?: number } + verifiers: ResolutionVerifier[], + options?: VerifyLockfileResolutionsOptions ): Promise { - if (verifyResolution == null) return + if (verifiers.length === 0) return if (!lockfile.packages) return - // depPath can include peer-dependency and patch_hash suffixes (e.g. - // `react@18.0.0(peer)(patch_hash=…)`); the same (name, version) pair may - // therefore appear multiple times. Dedupe so we issue at most one - // verification per package version. - const candidates = new Map() - for (const [depPath, snapshot] of Object.entries(lockfile.packages)) { - const { name, version } = nameVerFromPkgSnapshot(depPath as DepPath, snapshot) - if (!name || !version) continue - candidates.set(`${name}@${version}`, { name, version, resolution: snapshot.resolution }) + // Caching kicks in only when the caller surfaced both a writable + // cache directory and the lockfile's absolute path — that's the + // production wiring; unit tests that skip them get the gate without + // memoization and still exercise the same code path. + const cache = options?.cacheDir && options?.lockfilePath + ? { cacheDir: options.cacheDir, lockfilePath: options.lockfilePath } + : undefined + + // Cache lookup runs before any registry I/O — the fast path is a + // single stat() of the lockfile when the previous install already + // verified it under a policy that's at least as strict as today's. + // The content key is hashed lazily from the in-memory lockfile (not + // the file bytes) so we never read the file a second time. On a + // miss the precomputed stat+hash flow to recordVerification. + type Precomputed = ReturnType['precomputed'] + let cachePrecomputed: Precomputed | undefined + // hashObject streams and is key-order-stable, unlike JSON.stringify. + let cachedHash: string | undefined + const hashLockfile = (): string => { + if (cachedHash == null) cachedHash = hashObject(lockfile) + return cachedHash + } + if (cache) { + const result = tryLockfileVerificationCache(cache.cacheDir, { + lockfilePath: cache.lockfilePath, + verifiers, + hashLockfile, + }) + if (result.hit) return + cachePrecomputed = result.precomputed } - const violations: Violation[] = [] - const limit = pLimit(options?.concurrency ?? DEFAULT_CONCURRENCY) - await Promise.all( - Array.from(candidates.values(), ({ name, version, resolution }) => limit(async () => { - const pkgId = `${name}@${version}` - const result = await verifyResolution(resolution as Parameters[0], { name, version }) - if (!result.ok) { - violations.push({ pkgId, code: result.code, reason: result.reason }) + // Emit started/done around the actual verification pass — the + // round-trip can be slow on a cold registry cache, and the cached + // short-circuit above doesn't reach this branch, so a user only + // sees these messages on installs that are doing real work. + // A degenerate lockfile where every snapshot fails the + // name/version extraction (so candidates is empty) skips emission + // entirely — no work, no noise. + const candidates = collectCandidates(lockfile) + if (candidates.size === 0) { + if (cache) { + recordVerification(cache.cacheDir, { + lockfilePath: cache.lockfilePath, + verifiers, + hashLockfile, + }, cachePrecomputed) + } + return + } + const startedAt = Date.now() + lockfileVerificationLogger.debug({ + status: 'started', + entries: candidates.size, + lockfilePath: options?.lockfilePath, + }) + // Guarantee a terminal `done` or `failed` event on every exit path + // that emitted `started`. Without this, an unexpected throw from the + // registry fan-out (or the policy-violation throw below) would leave + // the transient "Verifying lockfile…" line as the last frame the + // reporter rendered for this block, hanging spinner-style above the + // failure output. + let terminalStatus: 'done' | 'failed' = 'failed' + try { + const violations = await iterateLockfileViolations(candidates, verifiers, options?.concurrency) + if (violations.length === 0) { + terminalStatus = 'done' + // Persist the success so the next install can stat-only the lockfile. + if (cache) { + recordVerification(cache.cacheDir, { + lockfilePath: cache.lockfilePath, + verifiers, + hashLockfile, + }, cachePrecomputed) } - })) - ) - - if (violations.length === 0) return + return + } + throw buildVerificationError(violations) + } finally { + lockfileVerificationLogger.debug({ + status: terminalStatus, + entries: candidates.size, + elapsedMs: Date.now() - startedAt, + lockfilePath: options?.lockfilePath, + }) + } +} +function buildVerificationError (violations: ResolutionPolicyViolation[]): PnpmError { // Stable order so the error output is deterministic. - violations.sort((a, b) => a.pkgId.localeCompare(b.pkgId)) + violations.sort((a, b) => `${a.name}@${a.version}`.localeCompare(`${b.name}@${b.version}`)) + // Pick the throw code: a single-code batch keeps the per-policy code + // (so existing handlers / docs / search keywords still route correctly); + // a mixed batch (e.g. minimumReleaseAge + trust-downgrade on the same + // lockfile) escalates to the generic `LOCKFILE_RESOLUTION_VERIFICATION` + // and the per-entry code goes into the breakdown so the user can see + // which policy each entry tripped. + const distinctCodes = new Set(violations.map((v) => v.code)) + const isMixed = distinctCodes.size > 1 + const errorCode = isMixed ? 'LOCKFILE_RESOLUTION_VERIFICATION' : violations[0].code const visible = violations.slice(0, MAX_VIOLATIONS_TO_PRINT) const omitted = violations.length - visible.length - const breakdown = visible.map((v) => ` ${v.pkgId} ${v.reason}`).join('\n') + const formatEntry = isMixed + ? (v: ResolutionPolicyViolation): string => ` ${v.name}@${v.version} [${v.code}] ${v.reason}` + : (v: ResolutionPolicyViolation): string => ` ${v.name}@${v.version} ${v.reason}` + const breakdown = visible.map(formatEntry).join('\n') const details = omitted > 0 ? `${breakdown}\n …and ${omitted} more` : breakdown - // Use the code of the first violation — all of today's violations are the - // same shape (one verifier, one code). If multiple verifiers fire later - // with mixed codes, switch to a generic LOCKFILE_RESOLUTION_VERIFICATION - // code and list per-entry codes in the breakdown. - throw new PnpmError( - violations[0].code, + return new PnpmError( + errorCode, `${violations.length} lockfile entries failed verification:\n${details}`, { hint: 'The lockfile contains entries that the active policies reject. ' + @@ -96,3 +204,85 @@ export async function verifyLockfileResolutions ( } ) } + +/** + * Collect-mode sibling of {@link verifyLockfileResolutions}: runs the + * same fan-out over every verifier and every lockfile entry, but + * returns the violations as data instead of throwing on the first batch. + * No cache lookup or write — the throw-mode `verifyLockfileResolutions` + * is what populates / honors the cache; this is for callers that need + * to inspect violations (auto-collect into `minimumReleaseAgeExclude`, + * the strict-mode interactive prompt, future resolver-specific + * policies). + * + * Returns an empty array when `verifiers` is empty or the lockfile has + * no packages, so callers don't need a separate emptiness check. + */ +export async function collectResolutionPolicyViolations ( + lockfile: LockfileObject, + verifiers: ResolutionVerifier[], + options?: Pick +): Promise { + if (verifiers.length === 0) return [] + if (!lockfile.packages) return [] + return iterateLockfileViolations(collectCandidates(lockfile), verifiers, options?.concurrency) +} + +interface Candidate { + name: string + version: string + resolution: Resolution +} + +// depPath can include peer-dependency and patch_hash suffixes (e.g. +// `react@18.0.0(peer)(patch_hash=…)`); the same (name, version) pair may +// therefore appear multiple times. Dedupe so we issue at most one +// verification per package version. +// +// Include a serialization of `resolution` in the key so two entries that +// share a (name, version) but differ in *what* was resolved (e.g. one +// pinned via npm, another via a git URL under the same alias) don't +// collapse: if the wrong shape wins the dedup, a protocol-scoped +// verifier short-circuits on the surviving entry and the real one is +// never checked. +function collectCandidates (lockfile: LockfileObject): Map { + const candidates = new Map() + for (const [depPath, snapshot] of Object.entries(lockfile.packages ?? {})) { + const { name, version } = nameVerFromPkgSnapshot(depPath as DepPath, snapshot) + if (!name || !version) continue + const key = `${name}@${version}@${JSON.stringify(snapshot.resolution)}` + candidates.set(key, { + name, + version, + resolution: snapshot.resolution as Resolution, + }) + } + return candidates +} + +async function iterateLockfileViolations ( + candidates: Map, + verifiers: readonly ResolutionVerifier[], + concurrency: number | undefined +): Promise { + const violations: ResolutionPolicyViolation[] = [] + const limit = pLimit(concurrency ?? DEFAULT_CONCURRENCY) + await Promise.all( + Array.from(candidates.values(), ({ name, version, resolution }) => limit(async () => { + // Fan out across every active verifier; each handles its own + // protocol short-circuit (e.g. the npm verifier returns ok:true for + // git resolutions). We stop at the first failure per entry so a + // multi-verifier setup doesn't produce duplicate violations for the + // same (name, version). + for (const verifier of verifiers) { + // eslint-disable-next-line no-await-in-loop + const result = await verifier.verify(resolution, { name, version }) + if (!result.ok) { + violations.push({ name, version, resolution, code: result.code, reason: result.reason }) + break + } + } + })) + ) + return violations +} diff --git a/installing/deps-installer/src/install/verifyLockfileResolutionsCache.ts b/installing/deps-installer/src/install/verifyLockfileResolutionsCache.ts new file mode 100644 index 0000000000..b1315577fb --- /dev/null +++ b/installing/deps-installer/src/install/verifyLockfileResolutionsCache.ts @@ -0,0 +1,428 @@ +import fs from 'node:fs' +import path from 'node:path' +import util from 'node:util' + +import { logger } from '@pnpm/logger' +import type { ResolutionVerifier } from '@pnpm/resolving.resolver-base' + +/** + * Subset of {@link ResolutionVerifier} the cache layer needs: the + * verifier's `policy` contribution plus the `canTrustPastCheck` + * comparator. `verify` is intentionally absent — the cache never runs + * verifiers, it just decides whether a previous run is still + * trustworthy. + */ +export type VerifierCacheIdentity = Pick + +/** + * On-disk cache of verifyLockfileResolutions results, keyed by lockfile + * content hash. Lets repeat installs against an unchanged lockfile skip + * the per-package registry round trips entirely — including across git + * worktrees, where the same lockfile content lives at different paths. + * + * Two indexes share the same JSONL records: + * + * - **by content hash** — the primary index. Recognizing the same + * lockfile content regardless of where it sits on disk is what makes + * worktrees and lockfile copies hit. + * - **by absolute path** — a same-machine stat shortcut. When we've + * seen this exact path before with these exact stat values, we + * trust the cached hash and skip reading the lockfile entirely + * (microseconds vs. ms-per-MB). Worktrees that get reinstalled in + * pay the hash cost once, then hit the stat fast path. + * + * All filesystem operations are synchronous: the cache is consulted + * once before verification fan-out and recorded once after — there's + * no concurrent install work to overlap with, so blocking the event + * loop for the brief read/stat/hash is fine and keeps the call sites + * straight-line. + * + * Persisted as JSON Lines: each verification appends one record; + * later records overwrite earlier ones on key collision when read. + * Appends of a single line are atomic on POSIX and NTFS, so parallel + * pnpm processes (monorepo installs, CI matrices sharing a cache) can + * write without coordination. + * + * Policy-neutral. Every active verifier's `policy` contribution merges + * into a single `policy` bag on the record; verifiers sharing a + * logical policy (same field name) share the slot — no resolver-level + * namespacing. + */ + +const CACHE_FILE_NAME = 'lockfile-verified.jsonl' + +// Cap the file before it grows large enough to slow down reads. When the +// cap is exceeded we rewrite the file keeping the N most recently +// verified entries. The number is generous — a developer machine that +// touches a thousand distinct (path, content) tuples is far past steady +// state. +const MAX_CACHE_ENTRIES = 1000 + +// Records cluster around 250–400 bytes; budget 1 KiB per entry as a +// conservative upper bound. The compaction check uses `stat().size` to +// decide whether to read+rewrite, so we never parse the file unless it +// has actually grown past the cap. +const COMPACT_TRIGGER_BYTES = MAX_CACHE_ENTRIES * 1024 * 3 / 2 + +interface CacheRecord { + lockfile: { + /** + * sha256 hex of the lockfile content — primary cache key. Computed + * from the parsed in-memory lockfile object (not the raw file + * bytes); two YAML layouts that parse to the same object share a + * hash. Same content on disk → same parsed object → same hash, so + * worktrees and CI checkouts collide here. + */ + hash: string + /** Absolute path the cache last saw this content at — secondary index for the stat fast path. */ + path: string + /** Lockfile size in bytes. */ + size: number + /** + * Lockfile mtime in nanoseconds (stringified — JSON numbers lose + * ns precision). Cross-machine values are meaningless; on a CI + * runner the fresh checkout resets mtime, so we fall back to + * hashing. + */ + mtimeNs: string + /** + * Stringified — some filesystems (e.g. large network drives) use + * inodes that exceed Number.MAX_SAFE_INTEGER, so a plain number + * would lose precision and silently invalidate the fast path. + */ + inode: string + } + /** ISO-8601 timestamp of when the verification ran. */ + verifiedAt: string + /** + * Merged policy snapshot that passed when the verification ran. Each + * active {@link VerifierCacheIdentity} contributes its fields here; + * verifiers checking the same logical policy (same field name) share + * the slot. On read, each verifier's `canTrustPastCheck` decides + * whether today's policy can still trust this snapshot. + */ + policy: Record +} + +export interface CacheLookupResult { + hit: boolean + /** + * stat + hash already computed during the lookup. When the caller + * follows up with {@link recordVerification} after running the gate, + * passing these back avoids re-stat'ing and (especially) re-hashing + * the lockfile a second time. Fields are undefined when the lookup + * couldn't (or didn't need to) compute them — `recordVerification` + * falls back to computing what's missing. + */ + precomputed: { stat?: LockfileStat, hash?: string } +} + +interface LockfileStat { + size: number + mtimeNs: string + inode: string +} + +export interface LockfileVerificationCacheKey { + lockfilePath: string + verifiers: readonly VerifierCacheIdentity[] + /** + * Lazy: returns a stable hex hash of the in-memory lockfile. The + * cache invokes this only when the stat shortcut doesn't apply (the + * lockfile is at a new path, or its stat has drifted from the + * cached record). When the stat shortcut hits, the in-memory hash is + * never computed. + */ + hashLockfile: () => string +} + +interface CacheIndexes { + /** Latest record per content hash — primary lookup. */ + byHash: Map + /** Latest record per absolute path — same-machine stat fast path. */ + byPath: Map +} + +/** + * Build two indexes over the JSONL records in one pass: by content + * hash (primary) and by absolute path (stat shortcut). Records are + * walked in file order so the last record for any key wins. + */ +function readCache (cacheDir: string): CacheIndexes { + const cacheFilePath = path.join(cacheDir, CACHE_FILE_NAME) + let contents: string + try { + contents = fs.readFileSync(cacheFilePath, 'utf8') + } catch (err: unknown) { + if (isNodeError(err) && err.code === 'ENOENT') return { byHash: new Map(), byPath: new Map() } + throw err + } + const byHash = new Map() + const byPath = new Map() + for (const line of contents.split('\n')) { + if (!line) continue + try { + const parsed = JSON.parse(line) as Partial + const hash = parsed?.lockfile?.hash + const lockfilePath = parsed?.lockfile?.path + if (typeof hash !== 'string' || typeof lockfilePath !== 'string') continue + const record = normalizeRecord(parsed) + byHash.set(hash, record) + byPath.set(lockfilePath, record) + } catch { + // Skip malformed lines; the next clean append will still work. + } + } + return { byHash, byPath } +} + +function normalizeRecord (parsed: Partial): CacheRecord { + const lockfile: Partial = parsed.lockfile ?? {} + return { + lockfile: { + hash: lockfile.hash ?? '', + path: lockfile.path ?? '', + size: lockfile.size ?? -1, + mtimeNs: lockfile.mtimeNs ?? '', + inode: lockfile.inode ?? '', + }, + verifiedAt: parsed.verifiedAt ?? '', + policy: parsed.policy && typeof parsed.policy === 'object' ? parsed.policy : {}, + } +} + +function statLockfile (lockfilePath: string): LockfileStat | null { + try { + const stat = fs.statSync(lockfilePath, { bigint: true }) + return { + size: Number(stat.size), + mtimeNs: stat.mtimeNs.toString(), + inode: stat.ino.toString(), + } + } catch (err: unknown) { + if (isNodeError(err) && err.code === 'ENOENT') return null + throw err + } +} + +function statMatches (stat: LockfileStat, lockfile: CacheRecord['lockfile']): boolean { + return stat.size === lockfile.size && + stat.mtimeNs === lockfile.mtimeNs && + stat.inode === lockfile.inode +} + +/** + * Try to confirm a cached verification covers the lockfile as it + * currently sits on disk and the policies currently in effect. Returns + * `{ hit: true }` to skip the gate; `{ hit: false }` means the caller + * should run the verifier and persist the result with + * {@link recordVerification}. + * + * Lookup order: + * + * 1. **Stat shortcut** — if we've previously verified this exact path + * with these exact stat values, trust the cached hash and skip + * reading the lockfile. + * 2. **Content lookup** — hash the lockfile and look up by hash. + * Catches the worktree case (same content, different path) and + * CI checkouts where stat fields got reset. Refreshes the + * stat-shortcut entry on hit so the next install at this path + * skips the hash. + * + * Every active verifier must agree the cached policy snapshot is still + * trustworthy under what it currently demands; if any rejects, the + * full gate runs. + */ +export function tryLockfileVerificationCache ( + cacheDir: string, + key: LockfileVerificationCacheKey +): CacheLookupResult { + let indexes: CacheIndexes + try { + indexes = readCache(cacheDir) + } catch (err: unknown) { + // A corrupt cache file should never block the install; fall + // through to verification so the gate still runs. + logger.debug({ msg: 'lockfile-verified cache: read failed', err }) + return { hit: false, precomputed: {} } + } + + const stat = statLockfile(key.lockfilePath) + if (!stat) return { hit: false, precomputed: {} } + + // Stat shortcut: same path + same stat means we trust the cached + // hash without reading the file. Microseconds. + const byPathRecord = indexes.byPath.get(key.lockfilePath) + if (byPathRecord && statMatches(stat, byPathRecord.lockfile)) { + return { + hit: everyVerifierTrustsCachedRun(byPathRecord, key.verifiers), + // The stat-match implies the file content is unchanged since the + // cached record was written, so its hash is still correct. Pass + // it through to skip hashing on the miss-then-record path. + precomputed: { stat, hash: byPathRecord.lockfile.hash }, + } + } + + // Content lookup: hash the in-memory lockfile, look up by content + // hash. Catches worktrees (same content, different path) and CI + // checkouts (same content, reset stat). On hit, refresh the + // path/stat entry so the next install at this path takes the stat + // shortcut above. + let hash: string + try { + hash = key.hashLockfile() + } catch (err: unknown) { + logger.debug({ msg: 'lockfile-verified cache: lockfile hash failed', err }) + return { hit: false, precomputed: { stat } } + } + const byHashRecord = indexes.byHash.get(hash) + if (!byHashRecord) return { hit: false, precomputed: { stat, hash } } + if (!everyVerifierTrustsCachedRun(byHashRecord, key.verifiers)) { + return { hit: false, precomputed: { stat, hash } } + } + + appendRecord(cacheDir, { + ...byHashRecord, + lockfile: { ...byHashRecord.lockfile, path: key.lockfilePath, size: stat.size, mtimeNs: stat.mtimeNs, inode: stat.inode }, + }) + return { hit: true, precomputed: { stat, hash } } +} + +function everyVerifierTrustsCachedRun (record: CacheRecord, verifiers: readonly VerifierCacheIdentity[]): boolean { + for (const verifier of verifiers) { + if (!verifier.canTrustPastCheck(record.policy)) return false + } + return true +} + +function mergePolicies (verifiers: readonly VerifierCacheIdentity[]): Record { + // Later verifiers overwrite earlier ones on conflict — a shared field + // should carry the same value across verifiers by convention; mismatch + // is a config bug and we don't try to reconcile it here. + const merged: Record = {} + for (const verifier of verifiers) { + Object.assign(merged, verifier.policy) + } + return merged +} + +/** + * Persist a successful verification. Called after the gate passes; the + * lockfile is hashed once and the resulting record is appended to the + * cache file. If the file is past {@link MAX_CACHE_ENTRIES}, it is + * rewritten keeping the most recent entries. + * + * Reuses `precomputed` values from a prior + * {@link tryLockfileVerificationCache} lookup so we don't re-stat or + * (especially) re-hash the lockfile a second time on the miss-then- + * record path. + */ +export function recordVerification ( + cacheDir: string, + key: LockfileVerificationCacheKey, + precomputed?: { stat?: LockfileStat, hash?: string } +): void { + let stat: LockfileStat | null + let hash: string + try { + stat = precomputed?.stat ?? statLockfile(key.lockfilePath) + if (!stat) return + hash = precomputed?.hash ?? key.hashLockfile() + } catch (err: unknown) { + // The gate has already passed; if we can't record the cache entry we + // just won't get the speedup next time. Not a reason to fail install. + logger.debug({ msg: 'lockfile-verified cache: could not record verification', err }) + return + } + const record: CacheRecord = { + lockfile: { + hash, + path: key.lockfilePath, + size: stat.size, + mtimeNs: stat.mtimeNs, + inode: stat.inode, + }, + verifiedAt: new Date().toISOString(), + policy: mergePolicies(key.verifiers), + } + appendRecord(cacheDir, record) +} + +function appendRecord (cacheDir: string, record: CacheRecord): void { + const cacheFilePath = path.join(cacheDir, CACHE_FILE_NAME) + const line = `${JSON.stringify(record)}\n` + try { + fs.mkdirSync(cacheDir, { recursive: true }) + fs.appendFileSync(cacheFilePath, line) + } catch (err: unknown) { + logger.debug({ msg: 'lockfile-verified cache: append failed', err }) + return + } + maybeCompactCache(cacheDir) +} + +function maybeCompactCache (cacheDir: string): void { + const cacheFilePath = path.join(cacheDir, CACHE_FILE_NAME) + // Decide whether to compact from the file size alone — avoids reading + // and parsing the file on every successful install. Records cluster + // around a few hundred bytes; the byte budget translates directly to + // the entry cap with generous slack so we don't trigger a rewrite on + // every append once we cross the line. + let size: number + try { + size = fs.statSync(cacheFilePath).size + } catch (err: unknown) { + if (isNodeError(err) && err.code === 'ENOENT') return + logger.debug({ msg: 'lockfile-verified cache: stat for compaction failed', err }) + return + } + if (size <= COMPACT_TRIGGER_BYTES) return + + let contents: string + try { + contents = fs.readFileSync(cacheFilePath, 'utf8') + } catch (err: unknown) { + if (isNodeError(err) && err.code === 'ENOENT') return + logger.debug({ msg: 'lockfile-verified cache: read for compaction failed', err }) + return + } + const lines = contents.split('\n').filter(Boolean) + + // Dedup by (path, hash) — that's the unit both indexes care about. + // Walking reverse keeps the newest record per tuple; we then trim to + // MAX_CACHE_ENTRIES and write back in original order. + const seen = new Set() + const reversed: string[] = [] + for (let i = lines.length - 1; i >= 0; i--) { + const line = lines[i] + try { + const parsed = JSON.parse(line) as Partial + const lockfilePath = parsed?.lockfile?.path + const hash = parsed?.lockfile?.hash + if (typeof lockfilePath !== 'string' || typeof hash !== 'string') continue + const tupleKey = `${lockfilePath}${hash}` + if (seen.has(tupleKey)) continue + seen.add(tupleKey) + reversed.push(line) + } catch { + // Skip malformed lines. + } + } + reversed.reverse() + const kept = reversed.slice(-MAX_CACHE_ENTRIES) + try { + // Write to a sibling tempfile + rename so a concurrent pnpm process + // can't observe a half-written file. + const tmpPath = `${cacheFilePath}.${process.pid}.tmp` + fs.writeFileSync(tmpPath, kept.map((line) => `${line}\n`).join('')) + fs.renameSync(tmpPath, cacheFilePath) + } catch (err: unknown) { + logger.debug({ msg: 'lockfile-verified cache: compaction failed', err }) + } +} + +function isNodeError (err: unknown): err is NodeJS.ErrnoException { + // `instanceof Error` is unreliable across realms (Jest's VM context), so + // route through util.types.isNativeError per the repo guideline. + return util.types.isNativeError(err) && 'code' in err +} diff --git a/installing/deps-installer/src/install/writeLockfilesAndRecordVerified.ts b/installing/deps-installer/src/install/writeLockfilesAndRecordVerified.ts new file mode 100644 index 0000000000..576e833725 --- /dev/null +++ b/installing/deps-installer/src/install/writeLockfilesAndRecordVerified.ts @@ -0,0 +1,48 @@ +import path from 'node:path' + +import { getWantedLockfileName, type LockfileObject, writeLockfiles, type WriteLockfilesResult } from '@pnpm/lockfile.fs' +import type { ResolutionVerifier } from '@pnpm/resolving.resolver-base' + +import { recordLockfileVerified } from './recordLockfileVerified.js' + +export interface WriteLockfilesAndRecordVerifiedOptions { + wantedLockfile: LockfileObject + wantedLockfileDir: string + currentLockfile: LockfileObject + currentLockfileDir: string + useGitBranchLockfile?: boolean + mergeGitBranchLockfiles?: boolean + cacheDir?: string + resolutionVerifiers: readonly ResolutionVerifier[] | undefined +} + +/** Plural counterpart of {@link writeWantedLockfileAndRecordVerified}. */ +export async function writeLockfilesAndRecordVerified ( + opts: WriteLockfilesAndRecordVerifiedOptions +): Promise { + const cacheActive = opts.cacheDir != null && (opts.resolutionVerifiers?.length ?? 0) > 0 + const wantedLockfileName = cacheActive + ? await getWantedLockfileName({ + useGitBranchLockfile: opts.useGitBranchLockfile, + mergeGitBranchLockfiles: opts.mergeGitBranchLockfiles, + }) + : undefined + const written = await writeLockfiles({ + wantedLockfile: opts.wantedLockfile, + wantedLockfileDir: opts.wantedLockfileDir, + currentLockfile: opts.currentLockfile, + currentLockfileDir: opts.currentLockfileDir, + useGitBranchLockfile: opts.useGitBranchLockfile, + mergeGitBranchLockfiles: opts.mergeGitBranchLockfiles, + wantedLockfileName, + }) + if (cacheActive) { + recordLockfileVerified({ + cacheDir: opts.cacheDir, + lockfilePath: path.resolve(opts.wantedLockfileDir, wantedLockfileName!), + lockfile: written.wantedLockfile, + resolutionVerifiers: opts.resolutionVerifiers, + }) + } + return written +} diff --git a/installing/deps-installer/src/install/writeWantedLockfileAndRecordVerified.ts b/installing/deps-installer/src/install/writeWantedLockfileAndRecordVerified.ts new file mode 100644 index 0000000000..0c3838b26b --- /dev/null +++ b/installing/deps-installer/src/install/writeWantedLockfileAndRecordVerified.ts @@ -0,0 +1,42 @@ +import path from 'node:path' + +import { getWantedLockfileName, type LockfileObject, writeWantedLockfile } from '@pnpm/lockfile.fs' +import type { ResolutionVerifier } from '@pnpm/resolving.resolver-base' + +import { recordLockfileVerified } from './recordLockfileVerified.js' + +export interface WriteWantedLockfileAndRecordVerifiedOptions { + lockfileDir: string + lockfile: LockfileObject + cacheDir?: string + resolutionVerifiers: readonly ResolutionVerifier[] | undefined + useGitBranchLockfile?: boolean + mergeGitBranchLockfiles?: boolean +} + +/** Combines {@link writeWantedLockfile} and {@link recordLockfileVerified} — see each for semantics. */ +export async function writeWantedLockfileAndRecordVerified ( + opts: WriteWantedLockfileAndRecordVerifiedOptions +): Promise { + const cacheActive = opts.cacheDir != null && (opts.resolutionVerifiers?.length ?? 0) > 0 + const lockfileName = cacheActive + ? await getWantedLockfileName({ + useGitBranchLockfile: opts.useGitBranchLockfile, + mergeGitBranchLockfiles: opts.mergeGitBranchLockfiles, + }) + : undefined + const written = await writeWantedLockfile(opts.lockfileDir, opts.lockfile, { + useGitBranchLockfile: opts.useGitBranchLockfile, + mergeGitBranchLockfiles: opts.mergeGitBranchLockfiles, + lockfileName, + }) + if (cacheActive) { + recordLockfileVerified({ + cacheDir: opts.cacheDir, + lockfilePath: path.resolve(opts.lockfileDir, lockfileName!), + lockfile: written, + resolutionVerifiers: opts.resolutionVerifiers, + }) + } + return written +} diff --git a/installing/deps-installer/test/brokenLockfileIntegrity.ts b/installing/deps-installer/test/brokenLockfileIntegrity.ts index 61becab3cf..c5962f838a 100644 --- a/installing/deps-installer/test/brokenLockfileIntegrity.ts +++ b/installing/deps-installer/test/brokenLockfileIntegrity.ts @@ -1,4 +1,4 @@ -import { expect, jest, test } from '@jest/globals' +import { expect, test } from '@jest/globals' import { WANTED_LOCKFILE } from '@pnpm/constants' import { addDependenciesToPackage, @@ -6,7 +6,7 @@ import { } from '@pnpm/installing.deps-installer' import type { TarballResolution } from '@pnpm/lockfile.fs' import { prepareEmpty } from '@pnpm/prepare' -import { addDistTag } from '@pnpm/registry-mock' +import { addDistTag } from '@pnpm/testing.registry-mock' import type { ProjectRootDir } from '@pnpm/types' import { rimrafSync } from '@zkochan/rimraf' import { clone } from 'ramda' @@ -14,7 +14,7 @@ import { writeYamlFileSync } from 'write-yaml-file' import { testDefaults } from './utils/index.js' -test('installation breaks if the lockfile contains the wrong checksum', async () => { +test('installation fails by default if the lockfile contains a wrong checksum, but --update-checksums recovers', async () => { await addDistTag({ package: '@pnpm.e2e/dep-of-pkg-with-1-dep', version: '100.0.0', distTag: 'latest' }) const project = prepareEmpty() @@ -38,11 +38,17 @@ test('installation breaks if the lockfile contains the wrong checksum', async () rootDir: process.cwd() as ProjectRootDir, }, testDefaults({ frozenLockfile: true }, { retry: { retries: 0 } }))).rejects.toThrow(/Got unexpected checksum for/) + await expect(mutateModulesInSingleProject({ + manifest, + mutation: 'install', + rootDir: process.cwd() as ProjectRootDir, + }, testDefaults({}, { retry: { retries: 0 } }))).rejects.toThrow(/Got unexpected checksum for/) + await mutateModulesInSingleProject({ manifest, mutation: 'install', rootDir: process.cwd() as ProjectRootDir, - }, testDefaults({}, { retry: { retries: 0 } })) + }, testDefaults({ updateChecksums: true }, { retry: { retries: 0 } })) expect(project.readLockfile()).toStrictEqual(correctLockfile) @@ -51,16 +57,15 @@ test('installation breaks if the lockfile contains the wrong checksum', async () rimrafSync('node_modules') - await mutateModulesInSingleProject({ + // --force is NOT an opt-in: it should still fail. + await expect(mutateModulesInSingleProject({ manifest, mutation: 'install', rootDir: process.cwd() as ProjectRootDir, - }, testDefaults({ preferFrozenLockfile: false }, { retry: { retries: 0 } })) - - expect(project.readLockfile()).toStrictEqual(correctLockfile) + }, testDefaults({ force: true }, { retry: { retries: 0 } }))).rejects.toThrow(/Got unexpected checksum for/) }) -test('installation breaks if the lockfile contains the wrong checksum and the store is clean', async () => { +test('installation fails by default if the lockfile contains the wrong checksum and the store is clean', async () => { await addDistTag({ package: '@pnpm.e2e/dep-of-pkg-with-1-dep', version: '100.0.0', distTag: 'latest' }) const project = prepareEmpty() @@ -85,37 +90,20 @@ test('installation breaks if the lockfile contains the wrong checksum and the st }, testDefaults({ frozenLockfile: true }, { retry: { retries: 0 } })) ).rejects.toThrow(/Got unexpected checksum/) + await expect(mutateModulesInSingleProject({ + manifest, + mutation: 'install', + rootDir: process.cwd() as ProjectRootDir, + }, testDefaults({}, { retry: { retries: 0 } }))).rejects.toThrow(/Got unexpected checksum/) + await mutateModulesInSingleProject({ manifest, mutation: 'install', rootDir: process.cwd() as ProjectRootDir, - }, testDefaults({}, { retry: { retries: 0 } })) + }, testDefaults({ updateChecksums: true }, { retry: { retries: 0 } })) { const lockfile = project.readLockfile() expect((lockfile.packages['@pnpm.e2e/pkg-with-1-dep@100.0.0'].resolution as TarballResolution).integrity).toBe(correctIntegrity) } - - // Breaking the lockfile again - writeYamlFileSync(WANTED_LOCKFILE, corruptedLockfile, { lineWidth: 1000 }) - - rimrafSync('node_modules') - - const reporter = jest.fn() - await mutateModulesInSingleProject({ - manifest, - mutation: 'install', - rootDir: process.cwd() as ProjectRootDir, - }, testDefaults({ preferFrozenLockfile: false, reporter }, { retry: { retries: 0 } })) - - expect(reporter).toHaveBeenCalledWith(expect.objectContaining({ - level: 'warn', - name: 'pnpm', - prefix: process.cwd(), - message: expect.stringMatching(/Got unexpected checksum/), - })) - { - const lockfile = project.readLockfile() - expect((lockfile.packages['@pnpm.e2e/pkg-with-1-dep@100.0.0'].resolution as TarballResolution).integrity).toBe(correctIntegrity) - } }) diff --git a/installing/deps-installer/test/cache.ts b/installing/deps-installer/test/cache.ts index 437f96ae75..a4635fa571 100644 --- a/installing/deps-installer/test/cache.ts +++ b/installing/deps-installer/test/cache.ts @@ -1,7 +1,7 @@ import { test } from '@jest/globals' import { addDependenciesToPackage, install } from '@pnpm/installing.deps-installer' import { prepareEmpty } from '@pnpm/prepare' -import { addDistTag } from '@pnpm/registry-mock' +import { addDistTag } from '@pnpm/testing.registry-mock' import { testDefaults } from './utils/index.js' diff --git a/installing/deps-installer/test/catalogs.ts b/installing/deps-installer/test/catalogs.ts index f6feea0b6d..5bd6a5eacd 100644 --- a/installing/deps-installer/test/catalogs.ts +++ b/installing/deps-installer/test/catalogs.ts @@ -5,7 +5,7 @@ import { createPeerDepGraphHash } from '@pnpm/deps.path' import type { MutatedProject, MutateModulesOptions, ProjectOptions } from '@pnpm/installing.deps-installer' import type { CatalogSnapshots } from '@pnpm/lockfile.types' import { prepareEmpty } from '@pnpm/prepare' -import { addDistTag } from '@pnpm/registry-mock' +import { addDistTag } from '@pnpm/testing.registry-mock' import type { ProjectId, ProjectManifest, ProjectRootDir } from '@pnpm/types' import { loadJsonFileSync } from 'load-json-file' @@ -1959,6 +1959,553 @@ describe('update', () => { '@pnpm.e2e/foo@1.0.0', ]) }) + + // Regression test for https://github.com/pnpm/pnpm/issues/11658 + // When running `pnpm upgrade -r` (install mutation with update=true and no specific package names), + // catalog: references should be preserved in package.json and only the catalog entry + // in pnpm-workspace.yaml should be updated. + test('update via install mutation preserves catalog: in manifest (issue #11658)', async () => { + const { options, projects, readLockfile } = preparePackagesAndReturnObjects([{ + name: 'project1', + dependencies: { + '@pnpm.e2e/foo': 'catalog:', + }, + }]) + + const mutateOpts = { + ...options, + lockfileOnly: true, + catalogs: { + default: { '@pnpm.e2e/foo': '1.0.0' }, + }, + } + + await mutateModules(installProjects(projects), mutateOpts) + + // Change the catalog to ^1.0.0 so that update can find a newer version. + mutateOpts.catalogs.default['@pnpm.e2e/foo'] = '^1.0.0' + await mutateModules(installProjects(projects), mutateOpts) + + // Sanity check that the @pnpm.e2e/foo dependency is installed on the older + // requested version. + expect(readLockfile().catalogs.default).toEqual({ + '@pnpm.e2e/foo': { specifier: '^1.0.0', version: '1.0.0' }, + }) + + // Simulate `pnpm upgrade -r` by using the "install" mutation with update=true + // and updatePackageManifest=true, without specifying any dependencySelectors. + const { updatedCatalogs, updatedProjects } = await mutateModules( + installProjects(projects).map((project) => ({ + ...project, + mutation: 'install' as const, + update: true, + updatePackageManifest: true, + })), + mutateOpts + ) + + // The manifest should still have "catalog:" — NOT a resolved version like "^1.3.0". + const updatedManifest = updatedProjects[0]?.manifest + expect(updatedManifest?.dependencies?.['@pnpm.e2e/foo']).toBe('catalog:') + + // The catalog should be updated to the newer version. + expect(updatedCatalogs).toEqual({ + default: { + '@pnpm.e2e/foo': '^1.3.0', + }, + }) + }) + + // Similar to above but with updateToLatest (simulating `pnpm upgrade -r --latest`) + test('update via install mutation with updateToLatest preserves catalog: in manifest (issue #11658)', async () => { + await addDistTag({ package: '@pnpm.e2e/foo', version: '100.1.0', distTag: 'latest' }) + + const { options, projects, readLockfile } = preparePackagesAndReturnObjects([{ + name: 'project1', + dependencies: { + '@pnpm.e2e/foo': 'catalog:', + }, + }]) + + const mutateOpts = { + ...options, + lockfileOnly: true, + catalogs: { + default: { '@pnpm.e2e/foo': '1.0.0' }, + }, + } + + await mutateModules(installProjects(projects), mutateOpts) + + // Change the catalog to ^1.0.0 so that updateToLatest can find a newer version. + mutateOpts.catalogs.default['@pnpm.e2e/foo'] = '^1.0.0' + await mutateModules(installProjects(projects), mutateOpts) + + // Sanity check that the @pnpm.e2e/foo dependency is installed on the older + // requested version. + expect(readLockfile().catalogs.default).toEqual({ + '@pnpm.e2e/foo': { specifier: '^1.0.0', version: '1.0.0' }, + }) + + // Simulate `pnpm upgrade -r --latest` by using the "install" mutation with + // update=true, updateToLatest=true, and updatePackageManifest=true. + const { updatedCatalogs, updatedProjects } = await mutateModules( + installProjects(projects).map((project) => ({ + ...project, + mutation: 'install' as const, + update: true, + updateToLatest: true, + updatePackageManifest: true, + })), + mutateOpts + ) + + // The manifest should still have "catalog:" — NOT a resolved version like "^100.1.0" or "100.1.0". + const updatedManifest = updatedProjects[0]?.manifest + expect(updatedManifest?.dependencies?.['@pnpm.e2e/foo']).toBe('catalog:') + + // The catalog should be updated to the latest version (with range prefix from resolution). + expect(updatedCatalogs).toBeTruthy() + expect(updatedCatalogs!.default?.['@pnpm.e2e/foo']).toMatch(/^[\^~]?100\.1\.0$/) + }) + + // Test with multiple catalog dependencies: ensures that the index alignment in + // updateProjectManifest is correct when some deps are catalog and some are not. + test('update via install mutation preserves catalog: with mixed deps (issue #11658)', async () => { + await addDistTag({ package: '@pnpm.e2e/bar', version: '100.1.0', distTag: 'latest' }) + + const { options, projects, readLockfile } = preparePackagesAndReturnObjects([{ + name: 'project1', + dependencies: { + '@pnpm.e2e/foo': 'catalog:', + '@pnpm.e2e/bar': '^100.0.0', + }, + }]) + + const mutateOpts = { + ...options, + lockfileOnly: true, + catalogs: { + default: { '@pnpm.e2e/foo': '1.0.0' }, + }, + } + + await mutateModules(installProjects(projects), mutateOpts) + + // Change the catalog to ^1.0.0 so that update can find a newer version. + mutateOpts.catalogs.default['@pnpm.e2e/foo'] = '^1.0.0' + await mutateModules(installProjects(projects), mutateOpts) + + // Sanity check + expect(readLockfile().catalogs.default).toEqual({ + '@pnpm.e2e/foo': { specifier: '^1.0.0', version: '1.0.0' }, + }) + + // Simulate `pnpm upgrade -r` with mixed deps (catalog and non-catalog). + const { updatedCatalogs, updatedProjects } = await mutateModules( + installProjects(projects).map((project) => ({ + ...project, + mutation: 'install' as const, + update: true, + updatePackageManifest: true, + })), + mutateOpts + ) + + // The manifest should still have "catalog:" for @pnpm.e2e/foo + const updatedManifest = updatedProjects[0]?.manifest + expect(updatedManifest?.dependencies?.['@pnpm.e2e/foo']).toBe('catalog:') + + // @pnpm.e2e/bar is not a catalog dep, its version range should remain or be updated + expect(updatedManifest?.dependencies?.['@pnpm.e2e/bar']).toBeTruthy() + + // The catalog should be updated to the newer version. + expect(updatedCatalogs).toEqual({ + default: { + '@pnpm.e2e/foo': '^1.3.0', + }, + }) + }) + + // Simulates `pnpm upgrade -r --latest` which uses installSome mutation with + // all dependency names as dependencySelectors. + test('installSome mutation with all deps preserves catalog: in manifest (issue #11658)', async () => { + await addDistTag({ package: '@pnpm.e2e/foo', version: '100.1.0', distTag: 'latest' }) + + const { options, projects, readLockfile } = preparePackagesAndReturnObjects([{ + name: 'project1', + dependencies: { + '@pnpm.e2e/foo': 'catalog:', + }, + }]) + + const catalogs = { + default: { '@pnpm.e2e/foo': '1.0.0' }, + } + + const mutateOpts = { + ...options, + lockfileOnly: true, + catalogs, + } + + await mutateModules(installProjects(projects), mutateOpts) + + // Change the catalog to ^1.0.0 so that update can find a newer version. + catalogs.default['@pnpm.e2e/foo'] = '^1.0.0' + await mutateModules(installProjects(projects), mutateOpts) + + // Sanity check + expect(readLockfile().catalogs.default).toEqual({ + '@pnpm.e2e/foo': { specifier: '^1.0.0', version: '1.0.0' }, + }) + + // Simulate `pnpm upgrade -r --latest`: installSome mutation with + // all dependency names as dependencySelectors, updateToLatest=true + const { updatedCatalogs, updatedManifest } = await addDependenciesToPackage( + projects['project1' as ProjectId], + ['@pnpm.e2e/foo'], + { + ...mutateOpts, + dir: path.join(options.lockfileDir, 'project1'), + allowNew: false, + update: true, + updateToLatest: true, + updatePackageManifest: true, + } + ) + + // The manifest should still have "catalog:" — NOT a resolved version. + expect(updatedManifest?.dependencies?.['@pnpm.e2e/foo']).toBe('catalog:') + + // The catalog should be updated to the latest version. + expect(updatedCatalogs).toBeTruthy() + expect(updatedCatalogs!.default?.['@pnpm.e2e/foo']).toMatch(/^[\^~]?100\.1\.0$/) + }) + + // Simulates `pnpm upgrade -r --latest` with multiple deps (catalog + non-catalog) + // This tests the index alignment bug in updateProjectManifest.ts where + // .filter().map() causes misaligned indices when some deps have updateSpec=false + test('installSome mutation with mixed catalog/non-catalog deps preserves catalog: (issue #11658)', async () => { + await addDistTag({ package: '@pnpm.e2e/foo', version: '100.1.0', distTag: 'latest' }) + await addDistTag({ package: '@pnpm.e2e/bar', version: '100.1.0', distTag: 'latest' }) + + const { options, projects, readLockfile } = preparePackagesAndReturnObjects([{ + name: 'project1', + dependencies: { + '@pnpm.e2e/foo': 'catalog:', + '@pnpm.e2e/bar': '^100.0.0', + }, + }]) + + const catalogs = { + default: { '@pnpm.e2e/foo': '1.0.0' }, + } + + const mutateOpts = { + ...options, + lockfileOnly: true, + catalogs, + } + + await mutateModules(installProjects(projects), mutateOpts) + + // Change the catalog to ^1.0.0 so that update can find a newer version. + catalogs.default['@pnpm.e2e/foo'] = '^1.0.0' + await mutateModules(installProjects(projects), mutateOpts) + + // Sanity check + expect(readLockfile().catalogs.default).toEqual({ + '@pnpm.e2e/foo': { specifier: '^1.0.0', version: '1.0.0' }, + }) + + // Simulate `pnpm upgrade -r --latest`: installSome mutation with + // both deps listed as dependencySelectors (like recursive.ts does) + const { updatedCatalogs, updatedManifest } = await addDependenciesToPackage( + projects['project1' as ProjectId], + ['@pnpm.e2e/foo', '@pnpm.e2e/bar'], + { + ...mutateOpts, + dir: path.join(options.lockfileDir, 'project1'), + allowNew: false, + update: true, + updateToLatest: true, + updatePackageManifest: true, + } + ) + + // The manifest should still have "catalog:" for @pnpm.e2e/foo + expect(updatedManifest?.dependencies?.['@pnpm.e2e/foo']).toBe('catalog:') + + // @pnpm.e2e/bar is not a catalog dep, its version should be updated + expect(updatedManifest?.dependencies?.['@pnpm.e2e/bar']).toBeTruthy() + + // The catalog should be updated + expect(updatedCatalogs).toBeTruthy() + }) + + // KEY REPRODUCTION TEST: When a project has both workspace:* and catalog: deps, + // the workspace dep is excluded from directDependencies (it becomes a linked dep), + // causing index misalignment between directDependencies and wantedDependencies + // in updateProjectManifest.ts's .filter().map() which then reads the wrong + // wantedDependency for each directDependency, resulting in catalog: being + // replaced by the resolved version. + test('install mutation with workspace + catalog deps preserves catalog: (issue #11658)', async () => { + const { options, projects, readLockfile } = preparePackagesAndReturnObjects([ + { + name: 'project1', + dependencies: { + project2: 'workspace:*', + '@pnpm.e2e/foo': 'catalog:', + }, + }, + { + name: 'project2', + }, + ]) + + const catalogs = { + default: { '@pnpm.e2e/foo': '1.0.0' }, + } + + const mutateOpts = { + ...options, + lockfileOnly: true, + catalogs, + } + + await mutateModules(installProjects(projects), mutateOpts) + + // Sanity check that the workspace dep and catalog dep both work + expect(readLockfile().importers['project1' as ProjectId].dependencies?.['@pnpm.e2e/foo']).toEqual({ + specifier: 'catalog:', + version: '1.0.0', + }) + + // Change the catalog to ^1.0.0 so that update can find a newer version. + catalogs.default['@pnpm.e2e/foo'] = '^1.0.0' + await mutateModules(installProjects(projects), mutateOpts) + + // Sanity check that the old version was installed + expect(readLockfile().catalogs.default).toEqual({ + '@pnpm.e2e/foo': { specifier: '^1.0.0', version: '1.0.0' }, + }) + + // Simulate `pnpm upgrade -r`: install mutation with update=true + const { updatedProjects } = await mutateModules( + installProjects(projects).map((project) => ({ + ...project, + update: true, + updatePackageManifest: true, + })), + mutateOpts + ) + + // project1 manifest should still have "catalog:" — NOT a resolved version + const project1Manifest = updatedProjects.find(p => p.rootDir.includes('project1'))?.manifest + expect(project1Manifest?.dependencies?.['@pnpm.e2e/foo']).toBe('catalog:') + }) + + // Same as above but using installSome (simulates pnpm upgrade in a workspace) + test('installSome mutation with workspace + catalog deps preserves catalog: (issue #11658)', async () => { + const { options, projects, readLockfile } = preparePackagesAndReturnObjects([ + { + name: 'project1', + dependencies: { + project2: 'workspace:*', + '@pnpm.e2e/foo': 'catalog:', + }, + }, + { + name: 'project2', + }, + ]) + + const catalogs = { + default: { '@pnpm.e2e/foo': '1.0.0' }, + } + + const mutateOpts = { + ...options, + lockfileOnly: true, + catalogs, + } + + await mutateModules(installProjects(projects), mutateOpts) + + // Change the catalog to ^1.0.0 so that updateToLatest can find a newer version. + catalogs.default['@pnpm.e2e/foo'] = '^1.0.0' + await mutateModules(installProjects(projects), mutateOpts) + + // Sanity check + expect(readLockfile().catalogs.default).toEqual({ + '@pnpm.e2e/foo': { specifier: '^1.0.0', version: '1.0.0' }, + }) + + // Simulate pnpm upgrade : installSome mutation on @pnpm.e2e/foo + // Using mutateModules directly to avoid addDependenciesToPackage which + // doesn't provide workspace context + const { updatedProjects } = await mutateModules( + [ + { + ...projects['project1' as ProjectId], + rootDir: path.resolve('project1') as ProjectRootDir, + mutation: 'installSome' as const, + dependencySelectors: ['@pnpm.e2e/foo'], + allowNew: false, + update: true, + updateToLatest: true, + updatePackageManifest: true, + }, + ], + mutateOpts + ) + + // The manifest should still have "catalog:" — NOT a resolved version. + const project1Manifest = updatedProjects.find(p => p.rootDir.includes('project1'))?.manifest + expect(project1Manifest?.dependencies?.['@pnpm.e2e/foo']).toBe('catalog:') + }) + + // Simulates the exact recursive.ts code path for `pnpm upgrade -r --latest` + // in a monorepo with multiple projects. This uses installSome mutation with + // multiple importers, which is what recursive.ts creates. + test('multi-project installSome mutation with updateToLatest preserves catalog: (issue #11658)', async () => { + await addDistTag({ package: '@pnpm.e2e/foo', version: '100.1.0', distTag: 'latest' }) + + const { options, projects, readLockfile } = preparePackagesAndReturnObjects([ + { + name: 'project1', + dependencies: { + '@pnpm.e2e/foo': 'catalog:', + }, + }, + { + name: 'project2', + dependencies: { + '@pnpm.e2e/foo': 'catalog:', + }, + }, + ]) + + const catalogs = { + default: { '@pnpm.e2e/foo': '1.0.0' }, + } + + const mutateOpts = { + ...options, + lockfileOnly: true, + catalogs, + } + + await mutateModules(installProjects(projects), mutateOpts) + + // Change the catalog to ^1.0.0 so that updateToLatest can find a newer version. + catalogs.default['@pnpm.e2e/foo'] = '^1.0.0' + await mutateModules(installProjects(projects), mutateOpts) + + // Sanity check + expect(readLockfile().catalogs.default).toEqual({ + '@pnpm.e2e/foo': { specifier: '^1.0.0', version: '1.0.0' }, + }) + + // Simulate `pnpm upgrade -r --latest`: installSome mutation for ALL projects, + // with all dependency names as dependencySelectors + const { updatedCatalogs, updatedProjects } = await mutateModules( + [ + { + ...projects['project1' as ProjectId], + rootDir: path.resolve('project1') as ProjectRootDir, + mutation: 'installSome' as const, + dependencySelectors: ['@pnpm.e2e/foo'], + allowNew: false, + update: true, + updateToLatest: true, + updatePackageManifest: true, + }, + { + ...projects['project2' as ProjectId], + rootDir: path.resolve('project2') as ProjectRootDir, + mutation: 'installSome' as const, + dependencySelectors: ['@pnpm.e2e/foo'], + allowNew: false, + update: true, + updateToLatest: true, + updatePackageManifest: true, + }, + ], + mutateOpts + ) + + // Both manifests should still have "catalog:" — NOT a resolved version + expect(updatedProjects[0]?.manifest?.dependencies?.['@pnpm.e2e/foo']).toBe('catalog:') + expect(updatedProjects[1]?.manifest?.dependencies?.['@pnpm.e2e/foo']).toBe('catalog:') + + // The catalog should be updated to the latest version. + expect(updatedCatalogs).toBeTruthy() + expect(updatedCatalogs!.default?.['@pnpm.e2e/foo']).toMatch(/^[\^~]?100\.1\.0$/) + }) + + // Simulates `pnpm upgrade -r` (no --latest, no package names) in a monorepo. + // This uses the `install` mutation with update=true, which is what recursive.ts + // creates when there are no dependencySelectors and !updateToLatest. + test('multi-project install mutation with update preserves catalog: (issue #11658)', async () => { + await addDistTag({ package: '@pnpm.e2e/foo', version: '100.1.0', distTag: 'latest' }) + + const { options, projects, readLockfile } = preparePackagesAndReturnObjects([ + { + name: 'project1', + dependencies: { + '@pnpm.e2e/foo': 'catalog:', + }, + }, + { + name: 'project2', + dependencies: { + '@pnpm.e2e/foo': 'catalog:', + }, + }, + ]) + + const catalogs = { + default: { '@pnpm.e2e/foo': '1.0.0' }, + } + + const mutateOpts = { + ...options, + lockfileOnly: true, + catalogs, + } + + await mutateModules(installProjects(projects), mutateOpts) + + // Change the catalog to ^1.0.0 so that update can find a newer version. + catalogs.default['@pnpm.e2e/foo'] = '^1.0.0' + await mutateModules(installProjects(projects), mutateOpts) + + // Sanity check + expect(readLockfile().catalogs.default).toEqual({ + '@pnpm.e2e/foo': { specifier: '^1.0.0', version: '1.0.0' }, + }) + + // Simulate `pnpm upgrade -r`: install mutation with update=true for ALL projects + const { updatedCatalogs, updatedProjects } = await mutateModules( + installProjects(projects).map((project) => ({ + ...project, + update: true, + updatePackageManifest: true, + })), + mutateOpts + ) + + // Both manifests should still have "catalog:" — NOT a resolved version + expect(updatedProjects[0]?.manifest?.dependencies?.['@pnpm.e2e/foo']).toBe('catalog:') + expect(updatedProjects[1]?.manifest?.dependencies?.['@pnpm.e2e/foo']).toBe('catalog:') + + // The catalog should be updated to the newer version (within the ^1.0.0 range). + expect(updatedCatalogs).toEqual({ + default: { '@pnpm.e2e/foo': '^1.3.0' }, + }) + }) }) test('catalogs work in overrides', async () => { diff --git a/installing/deps-installer/test/hoistedNodeLinker/install.ts b/installing/deps-installer/test/hoistedNodeLinker/install.ts index 77595f7bed..5321b0944e 100644 --- a/installing/deps-installer/test/hoistedNodeLinker/install.ts +++ b/installing/deps-installer/test/hoistedNodeLinker/install.ts @@ -4,7 +4,7 @@ import path from 'node:path' import { expect, test } from '@jest/globals' import { addDependenciesToPackage, install, mutateModules, mutateModulesInSingleProject } from '@pnpm/installing.deps-installer' import { prepareEmpty, preparePackages } from '@pnpm/prepare' -import { addDistTag } from '@pnpm/registry-mock' +import { addDistTag } from '@pnpm/testing.registry-mock' import type { ProjectRootDir } from '@pnpm/types' import { rimrafSync } from '@zkochan/rimraf' import { loadJsonFileSync } from 'load-json-file' diff --git a/installing/deps-installer/test/install/aliases.ts b/installing/deps-installer/test/install/aliases.ts index 7a38384afa..e0736509da 100644 --- a/installing/deps-installer/test/install/aliases.ts +++ b/installing/deps-installer/test/install/aliases.ts @@ -2,7 +2,8 @@ import { expect, test } from '@jest/globals' import { LOCKFILE_VERSION } from '@pnpm/constants' import { addDependenciesToPackage } from '@pnpm/installing.deps-installer' import { prepareEmpty } from '@pnpm/prepare' -import { addDistTag, getIntegrity } from '@pnpm/registry-mock' +import { getIntegrity } from '@pnpm/registry-mock' +import { addDistTag } from '@pnpm/testing.registry-mock' import { testDefaults } from '../utils/index.js' diff --git a/installing/deps-installer/test/install/auth.ts b/installing/deps-installer/test/install/auth.ts index 9411171550..a6aa074949 100644 --- a/installing/deps-installer/test/install/auth.ts +++ b/installing/deps-installer/test/install/auth.ts @@ -3,7 +3,8 @@ import path from 'node:path' import { test } from '@jest/globals' import { addDependenciesToPackage, install } from '@pnpm/installing.deps-installer' import { prepareEmpty } from '@pnpm/prepare' -import { addUser, REGISTRY_MOCK_PORT } from '@pnpm/registry-mock' +import { REGISTRY_MOCK_PORT } from '@pnpm/registry-mock' +import { addUser } from '@pnpm/testing.registry-mock' import type { RegistryConfig } from '@pnpm/types' import { rimrafSync } from '@zkochan/rimraf' @@ -70,27 +71,6 @@ test('installing a package that need authentication, using password', async () = project.has('@pnpm.e2e/needs-auth') }) -test('a package that need authentication, legacy way', async () => { - const project = prepareEmpty() - - await addUser({ - email: 'foo@bar.com', - password: 'bar', - username: 'foo', - }) - - const configByUri: Record = { - '': { creds: { basicAuth: { username: 'foo', password: 'bar' } } }, - } - await addDependenciesToPackage({}, ['@pnpm.e2e/needs-auth'], testDefaults({}, { - configByUri, - }, { - configByUri, - })) - - project.has('@pnpm.e2e/needs-auth') -}) - test('a scoped package that need authentication specific to scope', async () => { const project = prepareEmpty() diff --git a/installing/deps-installer/test/install/autoInstallPeers.ts b/installing/deps-installer/test/install/autoInstallPeers.ts index 138106a223..f907d5e5a5 100644 --- a/installing/deps-installer/test/install/autoInstallPeers.ts +++ b/installing/deps-installer/test/install/autoInstallPeers.ts @@ -5,7 +5,8 @@ import { assertProject } from '@pnpm/assert-project' import { createPeerDepGraphHash } from '@pnpm/deps.path' import { addDependenciesToPackage, install, mutateModules, mutateModulesInSingleProject, type PackageManifest } from '@pnpm/installing.deps-installer' import { prepareEmpty, preparePackages } from '@pnpm/prepare' -import { addDistTag, REGISTRY_MOCK_PORT } from '@pnpm/registry-mock' +import { REGISTRY_MOCK_PORT } from '@pnpm/registry-mock' +import { addDistTag } from '@pnpm/testing.registry-mock' import type { ProjectRootDir } from '@pnpm/types' import { rimrafSync } from '@zkochan/rimraf' diff --git a/installing/deps-installer/test/install/cyclicPeerDeterminism.ts b/installing/deps-installer/test/install/cyclicPeerDeterminism.ts new file mode 100644 index 0000000000..100ae7cbc2 --- /dev/null +++ b/installing/deps-installer/test/install/cyclicPeerDeterminism.ts @@ -0,0 +1,118 @@ +import { afterEach, expect, test } from '@jest/globals' +import { type MutatedProject, mutateModules, type MutateModulesOptions, type ProjectOptions } from '@pnpm/installing.deps-installer' +import { prepareEmpty } from '@pnpm/prepare' +import type { PackageMeta } from '@pnpm/resolving.registry.types' +import { getMockAgent, setupMockAgent, teardownMockAgent } from '@pnpm/testing.mock-agent' +import type { ProjectManifest, ProjectRootDir } from '@pnpm/types' + +import { testDefaults } from '../utils/index.js' + +afterEach(async () => { + await teardownMockAgent() +}) + +// Regression test for https://github.com/pnpm/pnpm/issues/8155. +// +// Reproduces the @aws-sdk/client-sts ↔ @aws-sdk/client-sso-oidc situation: a +// "wrapper" package pulls in two transitive deps that declare each other as +// peer dependencies, and `auto-install-peers` hoists those two peers up to the +// workspace root. Before the fix, `resolveDependencies` pushed onto its +// pkgAddresses / postponedResolutionsQueue arrays from inside +// `Promise.all`-spawned callbacks, so completion order leaked into the array +// order and the cyclic-peer suffix flipped between two equally valid forms +// across consecutive installs. +test('cyclic transitive peer dependencies resolve deterministically across installs', async () => { + const rootProject = prepareEmpty() + const lockfileDir = rootProject.dir() + + const wrapperName = '@pnpm.e2e/cyclic-wrapper' + const aName = '@pnpm.e2e/cyclic-a' + const bName = '@pnpm.e2e/cyclic-b' + + const manifest: ProjectManifest = { + name: 'root', + dependencies: { + [wrapperName]: '1.0.0', + }, + } + const allProjects: ProjectOptions[] = [{ + buildIndex: 0, + manifest, + rootDir: lockfileDir as ProjectRootDir, + }] + const options = { + ...testDefaults( + { allProjects, autoInstallPeers: true, forceFullResolution: true }, + { retry: { retries: 0 } } + ), + lockfileDir, + lockfileOnly: true, + } satisfies MutateModulesOptions + + const installProjects: MutatedProject[] = [{ + mutation: 'install', + rootDir: lockfileDir as ProjectRootDir, + }] + + const registryUrl = options.registries.default.replace(/\/$/, '') + + function makeMeta (name: string, deps: Record, peerDeps: Record): PackageMeta { + return { + name, + versions: { + '1.0.0': { + name, + version: '1.0.0', + dependencies: deps, + peerDependencies: peerDeps, + dist: { + // Resolver only reads metadata when lockfileOnly is true, so the + // shasum value is never checked against a tarball. + shasum: '0000000000000000000000000000000000000000', + tarball: `${options.registries.default}/${encodeURIComponent(name)}-1.0.0.tgz`, + }, + }, + }, + 'dist-tags': { latest: '1.0.0' }, + } + } + + const metaByName = { + [wrapperName]: makeMeta(wrapperName, { [aName]: '1.0.0', [bName]: '1.0.0' }, {}), + [aName]: makeMeta(aName, {}, { [bName]: '1.0.0' }), + [bName]: makeMeta(bName, {}, { [aName]: '1.0.0' }), + } + + function metadataPath (name: string): string { + return `/${name.replaceAll('/', '%2F')}` + } + + function arm (): void { + const agent = getMockAgent().get(registryUrl) + for (const [name, meta] of Object.entries(metaByName)) { + agent.intercept({ path: metadataPath(name), method: 'GET' }).reply(200, meta).persist() + } + } + + // ~30 iterations gives a ≥ (1 − 2⁻²⁹) chance of catching a 50/50 flip if + // the bug returns. The fix makes the walk order canonical so a single run + // would suffice, but iterating cheaply hedges against scheduling drift. + const iterations = 30 + + async function runOnce (): Promise { + await setupMockAgent() + arm() + options.storeController.clearResolutionCache() + await mutateModules(installProjects, options) + const lockfile = rootProject.readLockfile() + const snapshotKeys = Object.keys(lockfile.snapshots ?? {}) + await teardownMockAgent() + return snapshotKeys + } + + const first = JSON.stringify(await runOnce()) + for (let i = 1; i < iterations; i++) { + const subsequent = JSON.stringify(await runOnce()) // eslint-disable-line no-await-in-loop + expect(subsequent).toEqual(first) + } +}) diff --git a/installing/deps-installer/test/install/dedupe.ts b/installing/deps-installer/test/install/dedupe.ts index 148d043e42..dcb089e9db 100644 --- a/installing/deps-installer/test/install/dedupe.ts +++ b/installing/deps-installer/test/install/dedupe.ts @@ -1,7 +1,7 @@ import { expect, test } from '@jest/globals' import { addDependenciesToPackage, install } from '@pnpm/installing.deps-installer' import { prepareEmpty } from '@pnpm/prepare' -import { addDistTag } from '@pnpm/registry-mock' +import { addDistTag } from '@pnpm/testing.registry-mock' import { testDefaults } from '../utils/index.js' diff --git a/installing/deps-installer/test/install/dedupeInWorkspace.ts b/installing/deps-installer/test/install/dedupeInWorkspace.ts index 31e24cff78..de91b597c6 100644 --- a/installing/deps-installer/test/install/dedupeInWorkspace.ts +++ b/installing/deps-installer/test/install/dedupeInWorkspace.ts @@ -4,7 +4,7 @@ import { expect, test } from '@jest/globals' import { assertProject } from '@pnpm/assert-project' import { type MutatedProject, mutateModules } from '@pnpm/installing.deps-installer' import { preparePackages } from '@pnpm/prepare' -import { addDistTag } from '@pnpm/registry-mock' +import { addDistTag } from '@pnpm/testing.registry-mock' import type { ProjectRootDir } from '@pnpm/types' import { testDefaults } from '../utils/index.js' diff --git a/installing/deps-installer/test/install/defaultPeerDependencies.ts b/installing/deps-installer/test/install/defaultPeerDependencies.ts index 329c3538d1..1abe9aeb48 100644 --- a/installing/deps-installer/test/install/defaultPeerDependencies.ts +++ b/installing/deps-installer/test/install/defaultPeerDependencies.ts @@ -5,7 +5,7 @@ import { expect, test } from '@jest/globals' import { createPeerDepGraphHash } from '@pnpm/deps.path' import { addDependenciesToPackage } from '@pnpm/installing.deps-installer' import { prepareEmpty } from '@pnpm/prepare' -import { addDistTag } from '@pnpm/registry-mock' +import { addDistTag } from '@pnpm/testing.registry-mock' import deepRequireCwd from 'deep-require-cwd' import { testDefaults } from '../utils/index.js' diff --git a/installing/deps-installer/test/install/excludeLinksFromLockfile.ts b/installing/deps-installer/test/install/excludeLinksFromLockfile.ts index 8b809752d8..2a9a58b174 100644 --- a/installing/deps-installer/test/install/excludeLinksFromLockfile.ts +++ b/installing/deps-installer/test/install/excludeLinksFromLockfile.ts @@ -12,8 +12,8 @@ import { } from '@pnpm/installing.deps-installer' import type { LockfileFile, LockfileObject } from '@pnpm/lockfile.types' import { prepareEmpty, preparePackages, tempDir } from '@pnpm/prepare' -import { addDistTag } from '@pnpm/registry-mock' import { fixtures } from '@pnpm/test-fixtures' +import { addDistTag } from '@pnpm/testing.registry-mock' import type { ProjectId, ProjectRootDir } from '@pnpm/types' import { rimrafSync } from '@zkochan/rimraf' import normalizePath from 'normalize-path' diff --git a/installing/deps-installer/test/install/frozenLockfile.ts b/installing/deps-installer/test/install/frozenLockfile.ts index 718dcedd73..c6315630ee 100644 --- a/installing/deps-installer/test/install/frozenLockfile.ts +++ b/installing/deps-installer/test/install/frozenLockfile.ts @@ -1,3 +1,4 @@ +import fs from 'node:fs' import path from 'node:path' import { expect, jest, test } from '@jest/globals' @@ -220,6 +221,78 @@ test(`prefer-frozen-lockfile+hoistPattern: should prefer headless installation w project.has('.pnpm/node_modules/@pnpm.e2e/dep-of-pkg-with-1-dep') }) +test(`prefer-frozen-lockfile: should reuse node_modules/.pnpm/lock.yaml when ${WANTED_LOCKFILE} is missing and the snapshot satisfies package.json`, async () => { + const project = prepareEmpty() + + const { updatedManifest: manifest } = await install({ + dependencies: { + 'is-positive': '^3.0.0', + }, + }, testDefaults()) + + project.has('is-positive') + + const wantedLockfilePath = path.resolve(WANTED_LOCKFILE) + const lockfileBefore = fs.readFileSync(wantedLockfilePath, 'utf8') + fs.rmSync(wantedLockfilePath) + + const reporter = jest.fn() + await install(manifest, testDefaults({ reporter, preferFrozenLockfile: true })) + + expect(reporter).toHaveBeenCalledWith(expect.objectContaining({ + level: 'info', + message: 'Lockfile is up to date, resolution step is skipped', + name: 'pnpm', + })) + + expect(fs.existsSync(wantedLockfilePath)).toBe(true) + expect(fs.readFileSync(wantedLockfilePath, 'utf8')).toBe(lockfileBefore) + project.has('is-positive') +}) + +test(`prefer-frozen-lockfile: should re-resolve when ${WANTED_LOCKFILE} is missing and node_modules/.pnpm/lock.yaml does not satisfy package.json`, async () => { + const project = prepareEmpty() + + await install({ + dependencies: { + 'is-positive': '^3.0.0', + }, + }, testDefaults()) + + fs.rmSync(path.resolve(WANTED_LOCKFILE)) + + const reporter = jest.fn() + await install({ + dependencies: { + 'is-negative': '1.0.0', + }, + }, testDefaults({ reporter, preferFrozenLockfile: true })) + + expect(reporter).not.toHaveBeenCalledWith(expect.objectContaining({ + level: 'info', + message: 'Lockfile is up to date, resolution step is skipped', + name: 'pnpm', + })) + + project.has('is-negative') +}) + +test(`frozen-lockfile: should fail if ${WANTED_LOCKFILE} is missing even when node_modules/.pnpm/lock.yaml satisfies package.json`, async () => { + prepareEmpty() + + const { updatedManifest: manifest } = await install({ + dependencies: { + 'is-positive': '^3.0.0', + }, + }, testDefaults()) + + fs.rmSync(path.resolve(WANTED_LOCKFILE)) + + await expect( + install(manifest, testDefaults({ frozenLockfile: true })) + ).rejects.toThrow(`Cannot install with "frozen-lockfile" because ${WANTED_LOCKFILE} is absent`) +}) + test('prefer-frozen-lockfile: should prefer frozen-lockfile when package has linked dependency', async () => { const projects = preparePackages([ { diff --git a/installing/deps-installer/test/install/globalVirtualStore.ts b/installing/deps-installer/test/install/globalVirtualStore.ts index d8e5841418..2a7fbf2abc 100644 --- a/installing/deps-installer/test/install/globalVirtualStore.ts +++ b/installing/deps-installer/test/install/globalVirtualStore.ts @@ -5,9 +5,10 @@ import { afterAll, expect, test } from '@jest/globals' import { assertProject } from '@pnpm/assert-project' import { install, type MutatedProject, mutateModules, type ProjectOptions } from '@pnpm/installing.deps-installer' import { prepareEmpty, preparePackages } from '@pnpm/prepare' -import { addDistTag, getIntegrity } from '@pnpm/registry-mock' +import { getIntegrity } from '@pnpm/registry-mock' import type { PackageFilesIndex } from '@pnpm/store.cafs' import { StoreIndex, storeIndexKey } from '@pnpm/store.index' +import { addDistTag } from '@pnpm/testing.registry-mock' import type { ProjectRootDir } from '@pnpm/types' import { rimrafSync } from '@zkochan/rimraf' diff --git a/installing/deps-installer/test/install/hoist.ts b/installing/deps-installer/test/install/hoist.ts index db262fe624..c96755b357 100644 --- a/installing/deps-installer/test/install/hoist.ts +++ b/installing/deps-installer/test/install/hoist.ts @@ -12,7 +12,7 @@ import { mutateModulesInSingleProject, } from '@pnpm/installing.deps-installer' import { prepareEmpty, preparePackages } from '@pnpm/prepare' -import { addDistTag } from '@pnpm/registry-mock' +import { addDistTag } from '@pnpm/testing.registry-mock' import type { DepPath, ProjectRootDir } from '@pnpm/types' import { rimrafSync } from '@zkochan/rimraf' import { resolveLinkTarget } from 'resolve-link-target' diff --git a/installing/deps-installer/test/install/hooks.ts b/installing/deps-installer/test/install/hooks.ts index 954a5d636e..054b89fe79 100644 --- a/installing/deps-installer/test/install/hooks.ts +++ b/installing/deps-installer/test/install/hooks.ts @@ -6,7 +6,7 @@ import { } from '@pnpm/installing.deps-installer' import type { LockfileObject } from '@pnpm/lockfile.fs' import { prepareEmpty } from '@pnpm/prepare' -import { addDistTag } from '@pnpm/registry-mock' +import { addDistTag } from '@pnpm/testing.registry-mock' import { testDefaults } from '../utils/index.js' diff --git a/installing/deps-installer/test/install/injectLocalPackages.ts b/installing/deps-installer/test/install/injectLocalPackages.ts index c4599ab0a9..6153b58e62 100644 --- a/installing/deps-installer/test/install/injectLocalPackages.ts +++ b/installing/deps-installer/test/install/injectLocalPackages.ts @@ -2013,3 +2013,91 @@ test('injected local packages are deduped', async () => { expect(modulesState?.injectedDeps?.['project-1'][0]).toContain(`node_modules${path.sep}.pnpm`) } }) + +// Regression test: when an injected workspace package has a prepare script +// AND any dep with a bin, the re-import after the script crashed with +// `ENOENT: copyfile '...node_modules/.bin/'`. `runLifecycleHooksConcurrently` +// scanned the existing injected node_modules and added absolute paths under +// it to the filesMap; the importer's fast path then wiped the target before +// reading from those paths. Fixed by keeping storeController.importPackage +// but passing keepModulesDir: true, so importIndexedDir skips the +// destructive makeEmptyDir fast path (#11088) and preserves the target's +// existing node_modules (bin links + transitive deps) instead. +test('inject local package with prepare script + bin-having dep does not crash on re-import', async () => { + const project1Manifest = { + name: 'project-1', + version: '1.0.0', + dependencies: { + // hello-world-js-bin declares "bin": "./index.js", so pnpm creates + // a node_modules/.bin/hello-world-js-bin symlink in the injected + // copy. Before the fix that symlink triggered the crash. + '@pnpm.e2e/hello-world-js-bin': '1.0.0', + }, + scripts: { + // `prepare` (not `prepublishOnly`) is the script that pnpm runs during + // install — it's the lifecycle stage that triggered the original + // re-import crash on bin-having injected deps. + prepare: 'node -e "require(\'node:fs\').writeFileSync(\'built.txt\',\'ok\')"', + }, + } + const project2Manifest = { + name: 'project-2', + version: '1.0.0', + dependencies: { + 'project-1': 'workspace:1.0.0', + }, + dependenciesMeta: { + 'project-1': { + injected: true, + }, + }, + } + const projects = preparePackages([ + { + location: 'project-1', + package: project1Manifest, + }, + { + location: 'project-2', + package: project2Manifest, + }, + ]) + + const importers: MutatedProject[] = [ + { + mutation: 'install', + rootDir: path.resolve('project-1') as ProjectRootDir, + }, + { + mutation: 'install', + rootDir: path.resolve('project-2') as ProjectRootDir, + }, + ] + const allProjects = [ + { + buildIndex: 0, + manifest: project1Manifest, + rootDir: path.resolve('project-1') as ProjectRootDir, + }, + { + buildIndex: 0, + manifest: project2Manifest, + rootDir: path.resolve('project-2') as ProjectRootDir, + }, + ] + + // The install would throw with ERR_PNPM_ENOENT on `.bin/hello-world-js-bin` + // before the fix. + await mutateModules(importers, testDefaults({ + autoInstallPeers: false, + allProjects, + })) + + // prepare ran in project-1. + expect(fs.existsSync(path.resolve('project-1/built.txt'))).toBeTruthy() + // The re-import succeeded — project-2's injected copy has both the bin + // symlink from the original injection AND the built.txt from prepare. + expect(() => projects['project-2'].has('project-1')).not.toThrow() + expect(fs.existsSync(path.resolve('project-2/node_modules/project-1/built.txt'))).toBeTruthy() + expect(fs.existsSync(path.resolve('project-2/node_modules/project-1/node_modules/.bin/hello-world-js-bin'))).toBeTruthy() +}) diff --git a/installing/deps-installer/test/install/local.ts b/installing/deps-installer/test/install/local.ts index 905ce07e66..895c02521b 100644 --- a/installing/deps-installer/test/install/local.ts +++ b/installing/deps-installer/test/install/local.ts @@ -13,8 +13,8 @@ import { } from '@pnpm/installing.deps-installer' import type { LockfileFile } from '@pnpm/lockfile.fs' import { prepareEmpty, preparePackages } from '@pnpm/prepare' -import { addDistTag } from '@pnpm/registry-mock' import { fixtures } from '@pnpm/test-fixtures' +import { addDistTag } from '@pnpm/testing.registry-mock' import type { ProjectRootDir } from '@pnpm/types' import { rimrafSync } from '@zkochan/rimraf' import normalizePath from 'normalize-path' diff --git a/installing/deps-installer/test/install/lockfileOnly.ts b/installing/deps-installer/test/install/lockfileOnly.ts index 6ccb12164c..708f117284 100644 --- a/installing/deps-installer/test/install/lockfileOnly.ts +++ b/installing/deps-installer/test/install/lockfileOnly.ts @@ -9,7 +9,8 @@ import { install, } from '@pnpm/installing.deps-installer' import { prepareEmpty } from '@pnpm/prepare' -import { addDistTag, REGISTRY_MOCK_PORT } from '@pnpm/registry-mock' +import { REGISTRY_MOCK_PORT } from '@pnpm/registry-mock' +import { addDistTag } from '@pnpm/testing.registry-mock' import { testDefaults } from '../utils/index.js' diff --git a/installing/deps-installer/test/install/minimumReleaseAge.ts b/installing/deps-installer/test/install/minimumReleaseAge.ts index 14c8842a6d..63310eaf5f 100644 --- a/installing/deps-installer/test/install/minimumReleaseAge.ts +++ b/installing/deps-installer/test/install/minimumReleaseAge.ts @@ -72,21 +72,33 @@ test('minimumReleaseAge falls back to immature version when no mature version sa // The fallback picks the lowest matching version (0.1.0), which differs from // normal resolution without minimumReleaseAge that would pick the highest (0.1.2). const opts = testDefaults({ minimumReleaseAge: allImmatureMinimumReleaseAge }) - const { updatedManifest: manifest } = await addDependenciesToPackage({}, ['is-odd@0.1'], opts) + const { updatedManifest: manifest } = await addDependenciesToPackage({}, ['is-odd@0.1'], { + ...opts, + // Acknowledge the policy violations without aborting — this test + // only inspects the resolved manifest. resolveDependencies refuses + // to proceed if violations fire and no handler is wired. + handleResolutionPolicyViolations: async () => {}, + }) expect(manifest.dependencies!['is-odd']).toBe('~0.1.0') }) -test('minimumReleaseAge throws when no mature version satisfies the range and strict mode is enabled', async () => { +test('strict minimumReleaseAge surfaces every immature pick via handleResolutionPolicyViolations, then aborts', async () => { + // Pre-refactor strict mode threw at the resolver on the first immature + // pick (forcing a discover-by-loop dance, #10488). With always-defer the + // resolver records every immature pick inline; the install command (here + // simulated via the hook) decides what to do once it has the full set. prepareEmpty() - - await expect(async () => { - const opts = testDefaults( - { minimumReleaseAge: allImmatureMinimumReleaseAge }, - { strictPublishedByCheck: true } - ) - await addDependenciesToPackage({}, ['is-odd@0.1'], opts) - }).rejects.toThrow(/does not meet the minimumReleaseAge constraint/) + const opts = testDefaults({ minimumReleaseAge: allImmatureMinimumReleaseAge }) + const seen: string[] = [] + await expect(addDependenciesToPackage({}, ['is-odd@0.1'], { + ...opts, + handleResolutionPolicyViolations: async (violations) => { + for (const v of violations) seen.push(`${v.name}@${v.version}`) + throw new Error('immature picks rejected') + }, + })).rejects.toThrow(/immature picks rejected/) + expect(seen).toContain('is-odd@0.1.0') }) test('time-based resolution repopulates missing lockfile time entries on re-install', async () => { @@ -180,17 +192,131 @@ test('minimumReleaseAge is enforced on pre-existing lockfile entries during pnpm ).rejects.toThrow(/minimumReleaseAge/) }) -test('the lockfile minimumReleaseAge gate is inert when strict mode is off (default-value semantics)', async () => { +test('the lockfile minimumReleaseAge gate runs in loose mode too', async () => { prepareEmpty() const { updatedManifest: manifest } = await addDependenciesToPackage({}, ['is-odd@0.1.2'], testDefaults()) expect(manifest.dependencies!['is-odd']).toBe('0.1.2') - // Without explicit strict mode — the same shape as the CLI built-in default - // (1-day release-age window applied without `minimumReleaseAge` being set in - // .npmrc) — the revalidation pass stays inert and the locked version - // installs cleanly. + // Loose mode no longer skips the verifier — once auto-collect makes every + // accepted-immature pin explicit in `minimumReleaseAgeExclude`, running + // the verifier in loose mode is what keeps the manifest in sync with the + // lockfile. A pre-existing immature lockfile entry that isn't yet on the + // exclude list is rejected here, same as strict mode. await expect( install(manifest, testDefaults({ minimumReleaseAge })) + ).rejects.toThrow(/minimumReleaseAge/) +}) + +test('the lockfile minimumReleaseAge gate accepts loose-mode entries already on the exclude list', async () => { + prepareEmpty() + + const { updatedManifest: manifest } = await addDependenciesToPackage({}, ['is-odd@0.1.2'], testDefaults()) + + // is-odd@0.1.2 pulls in is-buffer and kind-of transitively. With the exclude + // list pre-populated (as the auto-collect would have produced on a previous + // install), the loose-mode verifier accepts all three and the install + // completes — the steady-state shape this feature is built around. + await expect( + install(manifest, testDefaults({ + minimumReleaseAge, + minimumReleaseAgeStrict: false, + minimumReleaseAgeExclude: ['is-odd@0.1.2', 'is-buffer', 'kind-of'], + })) ).resolves.toBeDefined() }) + +test('loose mode surfaces immature fresh picks in the install result', async () => { + prepareEmpty() + + // Every version is younger than the cutoff. With strict mode off the + // resolver's lowest-version fallback installs the immature version, + // and the post-resolution scan in `mutateModules` reports it back via + // `resolutionPolicyViolations`. The CLI command filters by code to + // persist the entries to `minimumReleaseAgeExclude`. + const opts = testDefaults({ minimumReleaseAge: allImmatureMinimumReleaseAge }) + const result = await addDependenciesToPackage({}, ['is-odd@0.1'], { + ...opts, + // Acknowledge the violations without aborting so the install + // proceeds and the result can be inspected. + handleResolutionPolicyViolations: async () => {}, + }) + + expect(result.resolutionPolicyViolations).toContainEqual( + expect.objectContaining({ + name: 'is-odd', + version: '0.1.0', + code: 'MINIMUM_RELEASE_AGE_VIOLATION', + }) + ) +}) + +test('versions excluded via minimumReleaseAgeExclude are not surfaced as violations', async () => { + prepareEmpty() + + const opts = testDefaults({ + minimumReleaseAge: allImmatureMinimumReleaseAge, + minimumReleaseAgeExclude: ['is-odd'], + }) + // is-odd is excluded, but `is-odd@0.1.2` pulls in is-buffer / is-number / + // kind-of transitively — those still produce policy violations. Wire a + // no-op handler to acknowledge them. + const result = await addDependenciesToPackage({}, ['is-odd@0.1'], { + ...opts, + handleResolutionPolicyViolations: async () => {}, + }) + + // is-odd is excluded by policy — the install installed 0.1.2 (the highest in + // range) treating it as fully trusted. The verifier short-circuits on the + // excluded entry, so it doesn't end up in the violations array — otherwise + // every install would re-add the same exclude entry the user just dismissed. + expect(result.resolutionPolicyViolations.find((v) => v.name === 'is-odd')).toBeUndefined() +}) + +test('handleResolutionPolicyViolations throwing aborts the install before the lockfile is written', async () => { + // Simulates the strict-mode interactive prompt rejecting the immature + // picks. The hook runs after the new lockfile is built but before it's + // written to disk; throwing unwinds the install in its pre-install state. + prepareEmpty() + const opts = testDefaults({ minimumReleaseAge: allImmatureMinimumReleaseAge }) + await expect(addDependenciesToPackage({}, ['is-odd@0.1'], { + ...opts, + handleResolutionPolicyViolations: async () => { + throw new Error('user denied') + }, + })).rejects.toThrow(/user denied/) + + // The lockfile must NOT have been written — the throw fires before the + // resolver finishes, so no on-disk side effects. + await expect(readWantedLockfile('.', { ignoreIncompatible: false })).resolves.toBeNull() +}) + +test('resolveDependencies throws if violations fire but no handleResolutionPolicyViolations is wired', async () => { + // Safety net: the policy contract is "every pick that trips a check + // produces a violation that gets handled". A caller that opted into a + // policy but forgot to wire the handler would otherwise silently drop + // the violations and land policy-rejected versions in the lockfile. + prepareEmpty() + const opts = testDefaults({ minimumReleaseAge: allImmatureMinimumReleaseAge }) + await expect(addDependenciesToPackage({}, ['is-odd@0.1'], { + ...opts, + // Explicitly omit handleResolutionPolicyViolations. + handleResolutionPolicyViolations: undefined, + })).rejects.toMatchObject({ code: 'ERR_PNPM_RESOLUTION_POLICY_VIOLATIONS_UNHANDLED' }) +}) + +test('handleResolutionPolicyViolations approval lets the install proceed cleanly', async () => { + prepareEmpty() + const opts = testDefaults({ minimumReleaseAge: allImmatureMinimumReleaseAge }) + const result = await addDependenciesToPackage({}, ['is-odd@0.1'], { + ...opts, + handleResolutionPolicyViolations: async (violations) => { + // The real install command would inspect the violations and run + // an enquirer prompt here. The test just confirms the hook gets a + // full set and returns to approve. + expect(violations.some((v) => v.name === 'is-odd' && v.version === '0.1.0')).toBe(true) + }, + }) + + expect(result.updatedManifest.dependencies!['is-odd']).toBe('~0.1.0') +}) diff --git a/installing/deps-installer/test/install/misc.ts b/installing/deps-installer/test/install/misc.ts index 99ef58605d..81abd1ff0a 100644 --- a/installing/deps-installer/test/install/misc.ts +++ b/installing/deps-installer/test/install/misc.ts @@ -11,8 +11,9 @@ import { UnexpectedVirtualStoreDirError, } from '@pnpm/installing.deps-installer' import { prepare, prepareEmpty, preparePackages } from '@pnpm/prepare' -import { addDistTag, getIntegrity, REGISTRY_MOCK_PORT } from '@pnpm/registry-mock' +import { getIntegrity, REGISTRY_MOCK_PORT } from '@pnpm/registry-mock' import { fixtures } from '@pnpm/test-fixtures' +import { addDistTag } from '@pnpm/testing.registry-mock' import type { ProjectManifest, ProjectRootDir } from '@pnpm/types' import { rimrafSync } from '@zkochan/rimraf' import { isCI } from 'ci-info' diff --git a/installing/deps-installer/test/install/multipleImporters.ts b/installing/deps-installer/test/install/multipleImporters.ts index 43e7877e28..2d5c34d291 100644 --- a/installing/deps-installer/test/install/multipleImporters.ts +++ b/installing/deps-installer/test/install/multipleImporters.ts @@ -13,7 +13,7 @@ import { } from '@pnpm/installing.deps-installer' import { readCurrentLockfile } from '@pnpm/lockfile.fs' import { prepareEmpty, preparePackages } from '@pnpm/prepare' -import { addDistTag } from '@pnpm/registry-mock' +import { addDistTag } from '@pnpm/testing.registry-mock' import type { ProjectId, ProjectManifest, ProjectRootDir } from '@pnpm/types' import { rimrafSync } from '@zkochan/rimraf' import { loadJsonFileSync } from 'load-json-file' diff --git a/installing/deps-installer/test/install/overrides.ts b/installing/deps-installer/test/install/overrides.ts index 81ecc4a83e..ae3facfbf7 100644 --- a/installing/deps-installer/test/install/overrides.ts +++ b/installing/deps-installer/test/install/overrides.ts @@ -7,7 +7,7 @@ import { PnpmError } from '@pnpm/error' import { addDependenciesToPackage, type MutatedProject, mutateModules, mutateModulesInSingleProject, type ProjectOptions } from '@pnpm/installing.deps-installer' import type { LockfileFile } from '@pnpm/lockfile.types' import { prepare, prepareEmpty, preparePackages } from '@pnpm/prepare' -import { addDistTag } from '@pnpm/registry-mock' +import { addDistTag } from '@pnpm/testing.registry-mock' import type { ProjectManifest, ProjectRootDir } from '@pnpm/types' import { readYamlFileSync } from 'read-yaml-file' diff --git a/installing/deps-installer/test/install/peerDependencies.ts b/installing/deps-installer/test/install/peerDependencies.ts index d0398ba067..54295d7260 100644 --- a/installing/deps-installer/test/install/peerDependencies.ts +++ b/installing/deps-installer/test/install/peerDependencies.ts @@ -16,8 +16,9 @@ import { } from '@pnpm/installing.deps-installer' import type { LockfileFile } from '@pnpm/lockfile.fs' import { prepareEmpty, preparePackages } from '@pnpm/prepare' -import { addDistTag, REGISTRY_MOCK_PORT } from '@pnpm/registry-mock' +import { REGISTRY_MOCK_PORT } from '@pnpm/registry-mock' import { fixtures } from '@pnpm/test-fixtures' +import { addDistTag } from '@pnpm/testing.registry-mock' import type { ProjectRootDir } from '@pnpm/types' import { rimrafSync } from '@zkochan/rimraf' import deepRequireCwd from 'deep-require-cwd' diff --git a/installing/deps-installer/test/install/recordLockfileVerified.ts b/installing/deps-installer/test/install/recordLockfileVerified.ts new file mode 100644 index 0000000000..32b975c8a4 --- /dev/null +++ b/installing/deps-installer/test/install/recordLockfileVerified.ts @@ -0,0 +1,214 @@ +import fs from 'node:fs' +import os from 'node:os' +import path from 'node:path' + +import { afterEach, beforeEach, expect, test } from '@jest/globals' +import { WANTED_LOCKFILE } from '@pnpm/constants' +import { hashObject } from '@pnpm/crypto.object-hasher' +import { type LockfileObject, readWantedLockfile, writeWantedLockfile } from '@pnpm/lockfile.fs' +import type { ResolutionVerifier } from '@pnpm/resolving.resolver-base' + +import { recordLockfileVerified } from '../../src/install/recordLockfileVerified.js' +import { tryLockfileVerificationCache } from '../../src/install/verifyLockfileResolutionsCache.js' + +let tmpDir!: string +let cacheDir!: string +let lockfileDir!: string +let lockfilePath!: string + +beforeEach(async () => { + tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'pnpm-record-verified-')) + cacheDir = path.join(tmpDir, 'cache') + lockfileDir = path.join(tmpDir, 'project') + lockfilePath = path.resolve(lockfileDir, WANTED_LOCKFILE) + await fs.promises.mkdir(lockfileDir, { recursive: true }) +}) + +afterEach(async () => { + await fs.promises.rm(tmpDir, { recursive: true, force: true }) +}) + +function mraVerifier (current: number): ResolutionVerifier { + return { + policy: { minimumReleaseAge: current }, + canTrustPastCheck: (cached) => { + const past = cached.minimumReleaseAge + return typeof past === 'number' && past >= current + }, + verify: async () => ({ ok: true }), + } +} + +function makeLockfile (): LockfileObject { + return { + lockfileVersion: '9.0', + importers: { + '.': { + specifiers: { 'is-positive': '^1.0.0' }, + dependencies: { 'is-positive': '1.0.0' }, + }, + }, + packages: { + 'is-positive@1.0.0': { + resolution: { + integrity: 'sha1-ChbBDewTLAqLCzb793Fo5VDvg/g=', + tarball: '', + }, + }, + }, + } as unknown as LockfileObject +} + +test('no-op when cacheDir is undefined', () => { + recordLockfileVerified({ + cacheDir: undefined, + lockfilePath, + lockfile: makeLockfile(), + resolutionVerifiers: [mraVerifier(60)], + }) + expect(fs.existsSync(cacheDir)).toBe(false) +}) + +test('no-op when resolutionVerifiers is empty', () => { + recordLockfileVerified({ + cacheDir, + lockfilePath, + lockfile: makeLockfile(), + resolutionVerifiers: [], + }) + expect(fs.existsSync(cacheDir)).toBe(false) +}) + +test('no-op when resolutionVerifiers is undefined', () => { + recordLockfileVerified({ + cacheDir, + lockfilePath, + lockfile: makeLockfile(), + resolutionVerifiers: undefined, + }) + expect(fs.existsSync(cacheDir)).toBe(false) +}) + +test('records nothing when the in-memory lockfile has no packages', async () => { + await writeWantedLockfile(lockfileDir, makeLockfile()) + recordLockfileVerified({ + cacheDir, + lockfilePath, + lockfile: { lockfileVersion: '9.0', importers: {} } as unknown as LockfileObject, + resolutionVerifiers: [mraVerifier(60)], + }) + expect(fs.existsSync(path.join(cacheDir, 'lockfile-verified.jsonl'))).toBe(false) +}) + +test('records the load-equivalent hash — matches what the next install computes off-disk', async () => { + // Use a fixture that carries an explicit `undefined` optional field + // (the real divergence case install-time code produces), then pass + // the *writer's return value* to recordLockfileVerified — same flow + // as the production call sites. Passing the in-memory input here + // instead would silently regress the moment the writer's + // canonicalization stops matching the reader's output. + const inMemoryLockfile = { + ...makeLockfile(), + settings: { + autoInstallPeers: true, + excludeLinksFromLockfile: false, + dedupePeers: undefined, + }, + } as unknown as LockfileObject + const written = await writeWantedLockfile(lockfileDir, inMemoryLockfile) + recordLockfileVerified({ + cacheDir, + lockfilePath, + lockfile: written, + resolutionVerifiers: [mraVerifier(60)], + }) + + // The cache contract: the next install hashes its loaded + // `LockfileObject` and looks the hash up. The recorded hash must + // match what that lookup computes. + const loaded = await readWantedLockfile(lockfileDir, { ignoreIncompatible: false }) + expect(loaded).not.toBeNull() + const expectedHash = hashObject(loaded!) + + const cacheFile = path.join(cacheDir, 'lockfile-verified.jsonl') + const record = JSON.parse(fs.readFileSync(cacheFile, 'utf8').trim()) as { + lockfile: { hash: string, path: string } + } + expect(record.lockfile.hash).toBe(expectedHash) + expect(record.lockfile.path).toBe(lockfilePath) +}) + +test('respects the caller-supplied lockfilePath — git-branch lockfiles record under their branch-suffixed filename', async () => { + // Simulates `useGitBranchLockfile`: the actual on-disk lockfile is + // pnpm-lock..yaml, not pnpm-lock.yaml. The helper has no + // git logic of its own — it records whatever path the caller hands + // it, so cache lookups on the same path will hit. + const branchLockfilePath = path.resolve(lockfileDir, 'pnpm-lock.feature-x.yaml') + await fs.promises.writeFile(branchLockfilePath, 'lockfileVersion: \'9.0\'\n') + const lockfile = makeLockfile() + recordLockfileVerified({ + cacheDir, + lockfilePath: branchLockfilePath, + lockfile, + resolutionVerifiers: [mraVerifier(60)], + }) + const cacheFile = path.join(cacheDir, 'lockfile-verified.jsonl') + const record = JSON.parse(fs.readFileSync(cacheFile, 'utf8').trim()) as { + lockfile: { path: string } + } + expect(record.lockfile.path).toBe(branchLockfilePath) +}) + +test('records a cache entry that the next install hits on both the stat shortcut and hash fallback paths', async () => { + // Mirror real call sites: hand `recordLockfileVerified` the + // writer's return value rather than the in-memory input. With an + // explicit `undefined` optional field in the fixture, those two + // diverge structurally — the in-memory variant would record a hash + // the next install can't match, and this test would silently miss + // that regression. + const inMemoryLockfile = { + ...makeLockfile(), + settings: { + autoInstallPeers: true, + excludeLinksFromLockfile: false, + dedupePeers: undefined, + }, + } as unknown as LockfileObject + const written = await writeWantedLockfile(lockfileDir, inMemoryLockfile) + recordLockfileVerified({ + cacheDir, + lockfilePath, + lockfile: written, + resolutionVerifiers: [mraVerifier(60)], + }) + const loaded = (await readWantedLockfile(lockfileDir, { ignoreIncompatible: false }))! + + // Stat shortcut: file untouched since record. + const statResult = tryLockfileVerificationCache(cacheDir, { + lockfilePath, + verifiers: [mraVerifier(60)], + hashLockfile: () => hashObject(loaded), + }) + expect(statResult.hit).toBe(true) + + // Hash fallback: invalidate stat fields the cache compares against so the + // shortcut bails. This is the CI-checkout / new-worktree path; the hash + // has to match for the fallback to hit, which is the whole point of + // hashing the canonical load-equivalent form. Use `size = -1` (impossible + // for a real file) rather than zeroing `inode`/`mtimeNs` alone — on + // Windows `stat.ino` is often 0, which would let the cached record + // accidentally match and skip the fallback path we want to exercise. + const cacheFile = path.join(cacheDir, 'lockfile-verified.jsonl') + const cached = JSON.parse(fs.readFileSync(cacheFile, 'utf8').trim()) as { + lockfile: { size: number } + } + cached.lockfile.size = -1 + fs.writeFileSync(cacheFile, `${JSON.stringify(cached)}\n`) + + const hashResult = tryLockfileVerificationCache(cacheDir, { + lockfilePath, + verifiers: [mraVerifier(60)], + hashLockfile: () => hashObject(loaded), + }) + expect(hashResult.hit).toBe(true) +}) diff --git a/installing/deps-installer/test/install/resolutionMode.ts b/installing/deps-installer/test/install/resolutionMode.ts index 312d9a67b7..43f1a2ba14 100644 --- a/installing/deps-installer/test/install/resolutionMode.ts +++ b/installing/deps-installer/test/install/resolutionMode.ts @@ -1,7 +1,7 @@ import { expect, test } from '@jest/globals' import { addDependenciesToPackage, install } from '@pnpm/installing.deps-installer' import { prepareEmpty } from '@pnpm/prepare' -import { addDistTag } from '@pnpm/registry-mock' +import { addDistTag } from '@pnpm/testing.registry-mock' import { testDefaults } from '../utils/index.js' diff --git a/installing/deps-installer/test/install/trustLockfile.ts b/installing/deps-installer/test/install/trustLockfile.ts new file mode 100644 index 0000000000..8b50bd0042 --- /dev/null +++ b/installing/deps-installer/test/install/trustLockfile.ts @@ -0,0 +1,55 @@ +import { expect, test } from '@jest/globals' +import { install } from '@pnpm/installing.deps-installer' +import { prepareEmpty } from '@pnpm/prepare' +import type { ResolutionVerifier } from '@pnpm/resolving.resolver-base' + +import { testDefaults } from '../utils/index.js' + +const rejectingVerifier: ResolutionVerifier = { + policy: {}, + canTrustPastCheck: () => false, + verify: async (_resolution, { name, version }) => ({ + ok: false, + code: 'TEST_REJECT', + reason: `${name}@${version} rejected by test verifier`, + }), +} + +test('install rejects the lockfile when a verifier fails and trustLockfile is unset', async () => { + prepareEmpty() + + await install( + { dependencies: { 'is-positive': '1.0.0' } }, + testDefaults() + ) + + await expect( + install( + { dependencies: { 'is-positive': '1.0.0' } }, + testDefaults({ + frozenLockfile: true, + resolutionVerifiers: [rejectingVerifier], + }) + ) + ).rejects.toMatchObject({ code: 'ERR_PNPM_TEST_REJECT' }) +}) + +test('install skips lockfile verification when trustLockfile is true even if a verifier rejects', async () => { + prepareEmpty() + + await install( + { dependencies: { 'is-positive': '1.0.0' } }, + testDefaults() + ) + + await expect( + install( + { dependencies: { 'is-positive': '1.0.0' } }, + testDefaults({ + frozenLockfile: true, + trustLockfile: true, + resolutionVerifiers: [rejectingVerifier], + }) + ) + ).resolves.toBeDefined() +}) diff --git a/installing/deps-installer/test/install/update.ts b/installing/deps-installer/test/install/update.ts index e47008cb7e..a1342add0b 100644 --- a/installing/deps-installer/test/install/update.ts +++ b/installing/deps-installer/test/install/update.ts @@ -5,7 +5,7 @@ import { WANTED_LOCKFILE } from '@pnpm/constants' import { addDependenciesToPackage, install } from '@pnpm/installing.deps-installer' import type { LockfileFile } from '@pnpm/lockfile.fs' import { prepareEmpty } from '@pnpm/prepare' -import { addDistTag } from '@pnpm/registry-mock' +import { addDistTag } from '@pnpm/testing.registry-mock' import { readYamlFileSync } from 'read-yaml-file' import { testDefaults } from '../utils/index.js' diff --git a/installing/deps-installer/test/install/updatingPkgJson.ts b/installing/deps-installer/test/install/updatingPkgJson.ts index 5f159fd9bb..4462f12859 100644 --- a/installing/deps-installer/test/install/updatingPkgJson.ts +++ b/installing/deps-installer/test/install/updatingPkgJson.ts @@ -5,7 +5,7 @@ import { mutateModulesInSingleProject, } from '@pnpm/installing.deps-installer' import { prepareEmpty } from '@pnpm/prepare' -import { addDistTag } from '@pnpm/registry-mock' +import { addDistTag } from '@pnpm/testing.registry-mock' import type { ProjectRootDir } from '@pnpm/types' import { testDefaults } from '../utils/index.js' diff --git a/installing/deps-installer/test/install/verifyLockfileResolutions.ts b/installing/deps-installer/test/install/verifyLockfileResolutions.ts index 67474c906c..7b1bf0adbb 100644 --- a/installing/deps-installer/test/install/verifyLockfileResolutions.ts +++ b/installing/deps-installer/test/install/verifyLockfileResolutions.ts @@ -1,3 +1,7 @@ +import fs from 'node:fs' +import os from 'node:os' +import path from 'node:path' + import { expect, test } from '@jest/globals' import type { LockfileObject } from '@pnpm/lockfile.fs' import type { ResolutionVerifier } from '@pnpm/resolving.resolver-base' @@ -14,18 +18,30 @@ function makeLockfile (packages: Record ({ integrity, tarball: '' }) -const okVerifier: ResolutionVerifier = async () => ({ ok: true }) +const NOOP_SLOT = { + policy: {} as Record, + canTrustPastCheck: () => true, +} -test('no-op when verifyResolution is undefined', async () => { +function wrap ( + verify: ResolutionVerifier['verify'], + slot: Omit = NOOP_SLOT +): ResolutionVerifier { + return { ...slot, verify } +} + +const okVerifier = wrap(async () => ({ ok: true })) + +test('no-op when the verifier list is empty', async () => { const lockfile = makeLockfile({ 'fresh@1.0.0': { resolution: tarballResolution() }, }) - await expect(verifyLockfileResolutions(lockfile, undefined)).resolves.toBeUndefined() + await expect(verifyLockfileResolutions(lockfile, [])).resolves.toBeUndefined() }) test('no-op when lockfile has no packages', async () => { const lockfile = makeLockfile({}) - await expect(verifyLockfileResolutions(lockfile, okVerifier)).resolves.toBeUndefined() + await expect(verifyLockfileResolutions(lockfile, [okVerifier])).resolves.toBeUndefined() }) test('passes when every entry is verified ok', async () => { @@ -33,37 +49,60 @@ test('passes when every entry is verified ok', async () => { 'lodash@4.17.21': { resolution: tarballResolution() }, 'is-odd@0.1.0': { resolution: tarballResolution() }, }) - await expect(verifyLockfileResolutions(lockfile, okVerifier)).resolves.toBeUndefined() + await expect(verifyLockfileResolutions(lockfile, [okVerifier])).resolves.toBeUndefined() }) test('throws with the verifier-supplied code and reason on a single failure', async () => { const lockfile = makeLockfile({ 'is-odd@0.1.2': { resolution: tarballResolution() }, }) - const verifier: ResolutionVerifier = async () => ({ + const verifier = wrap(async () => ({ ok: false, code: 'MINIMUM_RELEASE_AGE_VIOLATION', reason: 'was published yesterday', - }) + })) - await expect(verifyLockfileResolutions(lockfile, verifier)).rejects.toMatchObject({ + await expect(verifyLockfileResolutions(lockfile, [verifier])).rejects.toMatchObject({ code: 'ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION', message: expect.stringMatching(/is-odd@0\.1\.2 was published yesterday/), }) }) +test('throws a generic code with per-entry codes in the breakdown when violations span policies', async () => { + const lockfile = makeLockfile({ + 'is-odd@0.1.2': { resolution: tarballResolution('sha512-a') }, + 'untrusted@1.0.0': { resolution: tarballResolution('sha512-b') }, + }) + const verifier = wrap(async (_, { name }) => { + if (name === 'is-odd') { + return { ok: false, code: 'MINIMUM_RELEASE_AGE_VIOLATION', reason: 'too fresh' } + } + return { ok: false, code: 'TRUST_DOWNGRADE', reason: 'trust weakened' } + }) + + await expect(verifyLockfileResolutions(lockfile, [verifier])).rejects.toMatchObject({ + // Mixed-code batch escalates to the generic LOCKFILE_RESOLUTION_VERIFICATION + // code so downstream handlers don't mis-route on whichever entry happened + // to land first. + code: 'ERR_PNPM_LOCKFILE_RESOLUTION_VERIFICATION', + // Per-entry code is included in the breakdown so the user can see + // which policy each line tripped. + message: expect.stringMatching(/is-odd@0\.1\.2 \[MINIMUM_RELEASE_AGE_VIOLATION\][\s\S]*untrusted@1\.0\.0 \[TRUST_DOWNGRADE\]/), + }) +}) + test('lists violations in stable order across multiple failures', async () => { const lockfile = makeLockfile({ 'fresh-b@2.0.0': { resolution: tarballResolution('sha512-b') }, 'fresh-a@1.0.0': { resolution: tarballResolution('sha512-a') }, }) - const verifier: ResolutionVerifier = async (_, { name, version }) => ({ + const verifier = wrap(async (_, { name, version }) => ({ ok: false, code: 'POLICY_X', reason: `${name}@${version} failed`, - }) + })) - await expect(verifyLockfileResolutions(lockfile, verifier)) + await expect(verifyLockfileResolutions(lockfile, [verifier])) .rejects.toThrow(/fresh-a@1\.0\.0[\s\S]*fresh-b@2\.0\.0/) }) @@ -75,13 +114,13 @@ test('caps printed violations at 20 with an "…and N more" summary', async () = } } const lockfile = makeLockfile(packages) - const verifier: ResolutionVerifier = async (_, { name, version }) => ({ + const verifier = wrap(async (_, { name, version }) => ({ ok: false, code: 'POLICY_X', reason: `${name}@${version}`, - }) + })) - await expect(verifyLockfileResolutions(lockfile, verifier)) + await expect(verifyLockfileResolutions(lockfile, [verifier])) .rejects.toThrow(/25 lockfile entries failed verification[\s\S]*…and 5 more/) }) @@ -92,15 +131,38 @@ test('dedupes peer/patch-suffix variants and invokes the verifier once per (name 'react@18.0.0(patch_hash=abc)(peer-x)': { resolution: tarballResolution('sha512-a') }, }) const seen: Array<{ name: string, version: string }> = [] - const verifier: ResolutionVerifier = async (_, { name, version }) => { + const verifier = wrap(async (_, { name, version }) => { seen.push({ name, version }) return { ok: true } - } + }) - await verifyLockfileResolutions(lockfile, verifier) + await verifyLockfileResolutions(lockfile, [verifier]) expect(seen).toEqual([{ name: 'react', version: '18.0.0' }]) }) +test('does not collapse same (name, version) with different resolutions', async () => { + // Two entries sharing a name@version but pinned via different protocols + // (npm registry vs. git). If the dedup key were just `name@version` one + // would silently overwrite the other and a protocol-scoped verifier + // would short-circuit on the survivor — letting the real entry skip + // the gate. + const npmResolution = tarballResolution('sha512-a') + const gitResolution = { type: 'git', repo: 'x', commit: 'abc' } + const lockfile = makeLockfile({ + 'foo@1.0.0': { resolution: npmResolution }, + 'foo@1.0.0(peer-x)': { resolution: gitResolution }, + }) + const seenResolutions: unknown[] = [] + const verifier = wrap(async (resolution) => { + seenResolutions.push(resolution) + return { ok: true } + }) + + await verifyLockfileResolutions(lockfile, [verifier]) + expect(seenResolutions).toEqual(expect.arrayContaining([npmResolution, gitResolution])) + expect(seenResolutions).toHaveLength(2) +}) + test('the verifier sees the resolution shape verbatim', async () => { const npmResolution = tarballResolution() const gitResolution = { type: 'git', repo: 'x', commit: 'abc' } @@ -109,27 +171,122 @@ test('the verifier sees the resolution shape verbatim', async () => { 'git-pkg@1.0.0': { resolution: gitResolution }, }) const received: unknown[] = [] - const verifier: ResolutionVerifier = async (resolution) => { + const verifier = wrap(async (resolution) => { received.push(resolution) return { ok: true } - } + }) - await verifyLockfileResolutions(lockfile, verifier) + await verifyLockfileResolutions(lockfile, [verifier]) expect(received).toEqual(expect.arrayContaining([npmResolution, gitResolution])) }) -test('uses the first violation\'s code when multiple verifiers fire', async () => { +test('keeps the per-policy code when every violation in the batch shares it', async () => { + // Same code across all violations → throw with that code so existing + // handlers / docs / search routes still match. Mixed-code coverage is + // in the dedicated "throws a generic code …" test above. const lockfile = makeLockfile({ 'a@1.0.0': { resolution: tarballResolution('sha512-a') }, 'b@1.0.0': { resolution: tarballResolution('sha512-b') }, }) - const verifier: ResolutionVerifier = async (_, { name }) => ({ + const verifier = wrap(async () => ({ ok: false, - code: name === 'a' ? 'POLICY_A' : 'POLICY_B', + code: 'POLICY_A', reason: 'failed', - }) + })) - await expect(verifyLockfileResolutions(lockfile, verifier)).rejects.toMatchObject({ + await expect(verifyLockfileResolutions(lockfile, [verifier])).rejects.toMatchObject({ code: 'ERR_PNPM_POLICY_A', }) }) + +test('runs every active verifier per entry and stops at the first failure', async () => { + const lockfile = makeLockfile({ + 'a@1.0.0': { resolution: tarballResolution('sha512-a') }, + }) + const calls: string[] = [] + const firstOk = wrap(async () => { + calls.push('first') + return { ok: true } + }, NOOP_SLOT) + const secondFail = wrap(async () => { + calls.push('second') + return { ok: false, code: 'SECOND_POLICY', reason: 'nope' } + }, NOOP_SLOT) + + await expect(verifyLockfileResolutions(lockfile, [firstOk, secondFail])) + .rejects.toMatchObject({ code: 'ERR_PNPM_SECOND_POLICY' }) + // Both verifiers ran on the entry; ordering follows the list. + expect(calls).toEqual(['first', 'second']) +}) + +function exampleSlot (current: number): Omit { + return { + policy: { minimumReleaseAge: current }, + canTrustPastCheck: (cached) => { + const past = cached.minimumReleaseAge + return typeof past === 'number' && past >= current + }, + } +} + +test('skips the verifier when the cache holds an unchanged lockfile + matching policy', async () => { + const tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'pnpm-vlr-')) + try { + const cacheDir = path.join(tmpDir, 'cache') + const lockfilePath = path.join(tmpDir, 'pnpm-lock.yaml') + await fs.promises.writeFile(lockfilePath, 'lockfileVersion: \'9.0\'\n') + const lockfile = makeLockfile({ + 'a@1.0.0': { resolution: tarballResolution('sha512-a') }, + }) + + let calls = 0 + const counting = wrap(async () => { + calls++ + return { ok: true } + }, exampleSlot(60)) + + // First call has no cache record yet — verifier runs. + await verifyLockfileResolutions(lockfile, [counting], { + cacheDir, lockfilePath, + }) + expect(calls).toBe(1) + + // Second call against the same lockfile + policy — cache short-circuit. + await verifyLockfileResolutions(lockfile, [counting], { + cacheDir, lockfilePath, + }) + expect(calls).toBe(1) + } finally { + await fs.promises.rm(tmpDir, { recursive: true, force: true }) + } +}) + +test('does not write a cache record when verification rejects', async () => { + const tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'pnpm-vlr-')) + try { + const cacheDir = path.join(tmpDir, 'cache') + const lockfilePath = path.join(tmpDir, 'pnpm-lock.yaml') + await fs.promises.writeFile(lockfilePath, 'lockfileVersion: \'9.0\'\n') + const lockfile = makeLockfile({ + 'a@1.0.0': { resolution: tarballResolution('sha512-a') }, + }) + + const rejecting = wrap(async () => ({ + ok: false, + code: 'POLICY_X', + reason: 'failed', + }), exampleSlot(60)) + + await expect( + verifyLockfileResolutions(lockfile, [rejecting], { + cacheDir, lockfilePath, + }) + ).rejects.toThrow() + + // No record was written — a rejecting verification must rerun next install. + const cacheFile = path.join(cacheDir, 'lockfile-verified.jsonl') + await expect(fs.promises.access(cacheFile)).rejects.toThrow() + } finally { + await fs.promises.rm(tmpDir, { recursive: true, force: true }) + } +}) diff --git a/installing/deps-installer/test/install/verifyLockfileResolutionsCache.ts b/installing/deps-installer/test/install/verifyLockfileResolutionsCache.ts new file mode 100644 index 0000000000..2c83cb36c7 --- /dev/null +++ b/installing/deps-installer/test/install/verifyLockfileResolutionsCache.ts @@ -0,0 +1,359 @@ +import fs from 'node:fs' +import os from 'node:os' +import path from 'node:path' + +import { afterEach, beforeEach, describe, expect, test } from '@jest/globals' + +import { + recordVerification, + tryLockfileVerificationCache, + type VerifierCacheIdentity, +} from '../../src/install/verifyLockfileResolutionsCache.js' + +let tmpDir!: string +let cacheDir!: string +let lockfilePath!: string + +beforeEach(async () => { + tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'pnpm-verify-cache-')) + cacheDir = path.join(tmpDir, 'cache') + lockfilePath = path.join(tmpDir, 'pnpm-lock.yaml') +}) + +afterEach(async () => { + await fs.promises.rm(tmpDir, { recursive: true, force: true }) +}) + +// Helpers — most tests use a stand-in for the npm minimumReleaseAge +// verifier. The cache layer is policy-neutral, so this could be any +// verifier shape. +function mraVerifier (current: number): VerifierCacheIdentity { + return { + policy: { minimumReleaseAge: current }, + canTrustPastCheck: (cached) => { + const past = cached.minimumReleaseAge + return typeof past === 'number' && past >= current + }, + } +} + +// The cache hashes the in-memory lockfile, not the file bytes — but for +// unit tests we don't need a real lockfile object. A stable thunk is +// enough; tests that simulate "different content" pass a different tag. +const hashLockfileFor = (tag: string) => () => `hash:${tag}` + +describe('tryLockfileVerificationCache', () => { + test('miss when the cache file does not exist', async () => { + await fs.promises.writeFile(lockfilePath, 'lockfileVersion: \'9.0\'\n') + const result = tryLockfileVerificationCache(cacheDir, { + lockfilePath, + verifiers: [mraVerifier(60)], hashLockfile: hashLockfileFor('lockfilePath'), + }) + expect(result.hit).toBe(false) + }) + + test('miss when the lockfile path is not in the cache', async () => { + await fs.promises.writeFile(lockfilePath, 'lockfileVersion: \'9.0\'\n') + // Seed an unrelated record. + recordVerification(cacheDir, { + lockfilePath: path.join(tmpDir, 'other-lockfile.yaml'), + verifiers: [mraVerifier(60)], hashLockfile: hashLockfileFor('path.join(tmpDir'), + }) + const result = tryLockfileVerificationCache(cacheDir, { + lockfilePath, + verifiers: [mraVerifier(60)], hashLockfile: hashLockfileFor('lockfilePath'), + }) + expect(result.hit).toBe(false) + }) + + test('stat-only hit when size, mtime, and inode all match', async () => { + await fs.promises.writeFile(lockfilePath, 'lockfileVersion: \'9.0\'\n') + recordVerification(cacheDir, { lockfilePath, verifiers: [mraVerifier(60)], hashLockfile: hashLockfileFor('lockfilePath') }) + + const result = tryLockfileVerificationCache(cacheDir, { + lockfilePath, + verifiers: [mraVerifier(60)], hashLockfile: hashLockfileFor('lockfilePath'), + }) + expect(result.hit).toBe(true) + }) + + test('stat shortcut bails on size mismatch and falls through to hash lookup', async () => { + await fs.promises.writeFile(lockfilePath, 'lockfileVersion: \'9.0\'\n') + recordVerification(cacheDir, { lockfilePath, verifiers: [mraVerifier(60)], hashLockfile: hashLockfileFor('content-a') }) + + // Append bytes — file size changes, so byPath stat-match bails. + // The lookup falls through to the hash lookup. Today's content + // produces a different hash, so byHash misses → overall miss. + await fs.promises.appendFile(lockfilePath, 'extra: bytes\n') + + const result = tryLockfileVerificationCache(cacheDir, { + lockfilePath, + verifiers: [mraVerifier(60)], + hashLockfile: hashLockfileFor('content-b'), + }) + expect(result.hit).toBe(false) + }) + + test('hash-fallback hit when size matches but mtime/inode were reset', async () => { + // Simulate a CI checkout: same content, fresh inode + mtime. + await fs.promises.writeFile(lockfilePath, 'lockfileVersion: \'9.0\'\n') + recordVerification(cacheDir, { lockfilePath, verifiers: [mraVerifier(60)], hashLockfile: hashLockfileFor('lockfilePath') }) + + // Write to a different path, unlink the original, rename in. This + // guarantees a different inode while keeping the same content. + const sibling = path.join(tmpDir, 'pnpm-lock-2.yaml') + await fs.promises.writeFile(sibling, 'lockfileVersion: \'9.0\'\n') + await fs.promises.rm(lockfilePath) + await fs.promises.rename(sibling, lockfilePath) + + const result = tryLockfileVerificationCache(cacheDir, { + lockfilePath, + verifiers: [mraVerifier(60)], hashLockfile: hashLockfileFor('lockfilePath'), + }) + expect(result.hit).toBe(true) + }) + + test('miss when content changed even if size happens to match', async () => { + await fs.promises.writeFile(lockfilePath, 'aaaaaaaaaaaa') + recordVerification(cacheDir, { lockfilePath, verifiers: [mraVerifier(60)], hashLockfile: hashLockfileFor('content-a') }) + + // Same byte length, different content — hash check is what rejects. + await fs.promises.rm(lockfilePath) + await fs.promises.writeFile(lockfilePath, 'bbbbbbbbbbbb') + + const result = tryLockfileVerificationCache(cacheDir, { + lockfilePath, + verifiers: [mraVerifier(60)], + hashLockfile: hashLockfileFor('content-b'), + }) + expect(result.hit).toBe(false) + }) + + test('miss when a verifier rejects the cached policy', async () => { + await fs.promises.writeFile(lockfilePath, 'lockfileVersion: \'9.0\'\n') + recordVerification(cacheDir, { lockfilePath, verifiers: [mraVerifier(60)], hashLockfile: hashLockfileFor('lockfilePath') }) + + // Today's policy is stricter than the cached one. + const result = tryLockfileVerificationCache(cacheDir, { + lockfilePath, + verifiers: [mraVerifier(120)], hashLockfile: hashLockfileFor('lockfilePath'), + }) + expect(result.hit).toBe(false) + }) + + test('hit when a verifier accepts the cached policy', async () => { + await fs.promises.writeFile(lockfilePath, 'lockfileVersion: \'9.0\'\n') + recordVerification(cacheDir, { lockfilePath, verifiers: [mraVerifier(1440)], hashLockfile: hashLockfileFor('lockfilePath') }) + + // Today's policy is weaker — the stricter cached run still covers it. + const result = tryLockfileVerificationCache(cacheDir, { + lockfilePath, + verifiers: [mraVerifier(60)], hashLockfile: hashLockfileFor('lockfilePath'), + }) + expect(result.hit).toBe(true) + }) + + test('miss when the cached policy lacks a field the current verifier reads', async () => { + await fs.promises.writeFile(lockfilePath, 'lockfileVersion: \'9.0\'\n') + // Seed a record whose policy doesn't have minimumReleaseAge. + recordVerification(cacheDir, { + lockfilePath, + verifiers: [{ + policy: { someOther: 'value' }, + canTrustPastCheck: () => true, + }], + hashLockfile: hashLockfileFor('lockfilePath'), + }) + + const result = tryLockfileVerificationCache(cacheDir, { + lockfilePath, + verifiers: [mraVerifier(60)], + hashLockfile: hashLockfileFor('lockfilePath'), + }) + expect(result.hit).toBe(false) + }) + + test('hit when every verifier trusts its share of the merged cached policy', async () => { + await fs.promises.writeFile(lockfilePath, 'lockfileVersion: \'9.0\'\n') + const verifiers: VerifierCacheIdentity[] = [ + mraVerifier(60), + { + policy: { trustedPublishers: ['foo-org'] }, + canTrustPastCheck: (cached) => Array.isArray(cached.trustedPublishers) && + cached.trustedPublishers.includes('foo-org'), + }, + ] + const hashLockfile = hashLockfileFor('lockfilePath') + recordVerification(cacheDir, { lockfilePath, verifiers, hashLockfile }) + + const result = tryLockfileVerificationCache(cacheDir, { lockfilePath, verifiers, hashLockfile }) + expect(result.hit).toBe(true) + }) + + test('miss when the lockfile no longer exists', async () => { + await fs.promises.writeFile(lockfilePath, 'lockfileVersion: \'9.0\'\n') + recordVerification(cacheDir, { lockfilePath, verifiers: [mraVerifier(60)], hashLockfile: hashLockfileFor('lockfilePath') }) + await fs.promises.rm(lockfilePath) + + const result = tryLockfileVerificationCache(cacheDir, { + lockfilePath, + verifiers: [mraVerifier(60)], hashLockfile: hashLockfileFor('lockfilePath'), + }) + expect(result.hit).toBe(false) + }) + + test('hit at a new path when the content matches a cached hash (worktree case)', async () => { + // Both paths represent the same lockfile content, so they share a + // hash. This is the whole point: the cache should recognize the + // content regardless of path. + const sharedHash = hashLockfileFor('shared-content') + + // First install: lockfile at `lockfilePath` gets verified and cached. + await fs.promises.writeFile(lockfilePath, 'lockfileVersion: \'9.0\'\n') + recordVerification(cacheDir, { lockfilePath, verifiers: [mraVerifier(60)], hashLockfile: sharedHash }) + + // Second install: a different worktree with the same lockfile content, + // so a different absolute path but identical bytes. The stat shortcut + // misses (different path), but the content lookup hits via hash. + const worktreeLockfile = path.join(tmpDir, 'worktree', 'pnpm-lock.yaml') + await fs.promises.mkdir(path.dirname(worktreeLockfile), { recursive: true }) + await fs.promises.writeFile(worktreeLockfile, 'lockfileVersion: \'9.0\'\n') + + const result = tryLockfileVerificationCache(cacheDir, { + lockfilePath: worktreeLockfile, + verifiers: [mraVerifier(60)], + hashLockfile: sharedHash, + }) + expect(result.hit).toBe(true) + + // The hit appended a refreshed record for the new path, so the next + // install on the worktree path takes the stat shortcut (no rehash). + // We can't directly observe whether the shortcut fires, but we can + // confirm the cache file now has at least one record naming the + // worktree path. + const cacheFile = path.join(cacheDir, 'lockfile-verified.jsonl') + const raw = await fs.promises.readFile(cacheFile, 'utf8') + const paths = raw.split('\n').filter(Boolean) + .map((line) => (JSON.parse(line) as { lockfile: { path: string } }).lockfile.path) + expect(paths).toEqual(expect.arrayContaining([worktreeLockfile])) + }) + + test('latest record per path wins when the cache has multiple appends', async () => { + await fs.promises.writeFile(lockfilePath, 'lockfileVersion: \'9.0\'\n') + + // Earlier record under a stricter cutoff. + recordVerification(cacheDir, { lockfilePath, verifiers: [mraVerifier(60)], hashLockfile: hashLockfileFor('lockfilePath') }) + // Later record under a weaker cutoff that does satisfy 120. + recordVerification(cacheDir, { lockfilePath, verifiers: [mraVerifier(120)], hashLockfile: hashLockfileFor('lockfilePath') }) + + const result = tryLockfileVerificationCache(cacheDir, { + lockfilePath, + verifiers: [mraVerifier(120)], hashLockfile: hashLockfileFor('lockfilePath'), + }) + expect(result.hit).toBe(true) + }) + + test('malformed lines are ignored, not propagated', async () => { + await fs.promises.writeFile(lockfilePath, 'lockfileVersion: \'9.0\'\n') + await fs.promises.mkdir(cacheDir, { recursive: true }) + const cacheFile = path.join(cacheDir, 'lockfile-verified.jsonl') + await fs.promises.writeFile(cacheFile, '{not json\n\n') + + recordVerification(cacheDir, { lockfilePath, verifiers: [mraVerifier(60)], hashLockfile: hashLockfileFor('lockfilePath') }) + + const result = tryLockfileVerificationCache(cacheDir, { + lockfilePath, + verifiers: [mraVerifier(60)], hashLockfile: hashLockfileFor('lockfilePath'), + }) + expect(result.hit).toBe(true) + }) +}) + +describe('recordVerification', () => { + test('writes a JSONL record with a merged policy bag', async () => { + await fs.promises.writeFile(lockfilePath, 'lockfileVersion: \'9.0\'\n') + recordVerification(cacheDir, { lockfilePath, verifiers: [mraVerifier(60)], hashLockfile: hashLockfileFor('lockfilePath') }) + + const cacheFile = path.join(cacheDir, 'lockfile-verified.jsonl') + const raw = await fs.promises.readFile(cacheFile, 'utf8') + const lines = raw.split('\n').filter(Boolean) + expect(lines).toHaveLength(1) + const record = JSON.parse(lines[0]) as Record & { + lockfile: Record + } + expect(record).toMatchObject({ + lockfile: { path: lockfilePath }, + policy: { minimumReleaseAge: 60 }, + }) + expect(typeof record.lockfile.hash).toBe('string') + expect(typeof record.verifiedAt).toBe('string') + expect(typeof record.lockfile.size).toBe('number') + expect(typeof record.lockfile.mtimeNs).toBe('string') + expect(typeof record.lockfile.inode).toBe('string') + }) + + test('merges policy fields across verifiers into a single bag', async () => { + await fs.promises.writeFile(lockfilePath, 'lockfileVersion: \'9.0\'\n') + recordVerification(cacheDir, { + lockfilePath, + verifiers: [ + mraVerifier(60), + { + policy: { trustedPublishers: ['foo-org', 'pnpm'] }, + canTrustPastCheck: () => true, + }, + ], + hashLockfile: hashLockfileFor('lockfilePath'), + }) + + const cacheFile = path.join(cacheDir, 'lockfile-verified.jsonl') + const raw = await fs.promises.readFile(cacheFile, 'utf8') + const record = JSON.parse(raw.trim()) as { policy: Record } + expect(record.policy).toEqual({ + minimumReleaseAge: 60, + trustedPublishers: ['foo-org', 'pnpm'], + }) + }) + + test('shared policy field is stored once, not duplicated', async () => { + await fs.promises.writeFile(lockfilePath, 'lockfileVersion: \'9.0\'\n') + // Two verifiers both contribute minimumReleaseAge with the same value + // — the merged bag stores it once. + recordVerification(cacheDir, { + lockfilePath, + verifiers: [mraVerifier(60), mraVerifier(60)], hashLockfile: hashLockfileFor('lockfilePath'), + }) + + const cacheFile = path.join(cacheDir, 'lockfile-verified.jsonl') + const raw = await fs.promises.readFile(cacheFile, 'utf8') + const record = JSON.parse(raw.trim()) as { policy: Record } + expect(record.policy).toEqual({ minimumReleaseAge: 60 }) + }) + + test('silently skips when the lockfile is missing', async () => { + expect( + recordVerification(cacheDir, { lockfilePath, verifiers: [mraVerifier(60)], hashLockfile: hashLockfileFor('lockfilePath') }) + ).toBeUndefined() + const cacheFile = path.join(cacheDir, 'lockfile-verified.jsonl') + await expect(fs.promises.access(cacheFile)).rejects.toThrow() + }) + + test('appends without rewriting previous lines', async () => { + await fs.promises.writeFile(lockfilePath, 'lockfileVersion: \'9.0\'\n') + recordVerification(cacheDir, { lockfilePath, verifiers: [mraVerifier(60)], hashLockfile: hashLockfileFor('lockfilePath') }) + + const otherLockfile = path.join(tmpDir, 'other-lockfile.yaml') + await fs.promises.writeFile(otherLockfile, 'lockfileVersion: \'9.0\'\n') + recordVerification(cacheDir, { + lockfilePath: otherLockfile, + verifiers: [mraVerifier(60)], hashLockfile: hashLockfileFor('otherLockfile'), + }) + + const cacheFile = path.join(cacheDir, 'lockfile-verified.jsonl') + const raw = await fs.promises.readFile(cacheFile, 'utf8') + const lines = raw.split('\n').filter(Boolean) + expect(lines).toHaveLength(2) + const paths = lines.map((line) => (JSON.parse(line) as { lockfile: { path: string } }).lockfile.path) + expect(paths).toEqual(expect.arrayContaining([lockfilePath, otherLockfile])) + }) +}) diff --git a/installing/deps-installer/test/link.ts b/installing/deps-installer/test/link.ts index 8a85d646f1..63e38d65dd 100644 --- a/installing/deps-installer/test/link.ts +++ b/installing/deps-installer/test/link.ts @@ -3,8 +3,8 @@ import path from 'node:path' import { expect, test } from '@jest/globals' import { addDependenciesToPackage, install } from '@pnpm/installing.deps-installer' import { prepareEmpty } from '@pnpm/prepare' -import { addDistTag } from '@pnpm/registry-mock' import { fixtures } from '@pnpm/test-fixtures' +import { addDistTag } from '@pnpm/testing.registry-mock' import { symlinkDirSync } from 'symlink-dir' import { testDefaults } from './utils/index.js' diff --git a/installing/deps-installer/test/lockfile.ts b/installing/deps-installer/test/lockfile.ts index 85eb860c5b..e4f90d21c9 100644 --- a/installing/deps-installer/test/lockfile.ts +++ b/installing/deps-installer/test/lockfile.ts @@ -16,9 +16,10 @@ import type { LockfileObject, TarballResolution } from '@pnpm/lockfile.fs' import type { LockfileFile } from '@pnpm/lockfile.types' import { readPackageJsonFromDir } from '@pnpm/pkg-manifest.reader' import { prepareEmpty, preparePackages, tempDir } from '@pnpm/prepare' -import { addDistTag, getIntegrity, REGISTRY_MOCK_PORT } from '@pnpm/registry-mock' +import { getIntegrity, REGISTRY_MOCK_PORT } from '@pnpm/registry-mock' import { fixtures } from '@pnpm/test-fixtures' import { getMockAgent, setupMockAgent, teardownMockAgent } from '@pnpm/testing.mock-agent' +import { addDistTag } from '@pnpm/testing.registry-mock' import type { DepPath, ProjectManifest, ProjectRootDir } from '@pnpm/types' import { rimrafSync } from '@zkochan/rimraf' import { loadJsonFileSync } from 'load-json-file' diff --git a/installing/deps-installer/test/utils/testDefaults.ts b/installing/deps-installer/test/utils/testDefaults.ts index 1df64f7a66..a40d1667ea 100644 --- a/installing/deps-installer/test/utils/testDefaults.ts +++ b/installing/deps-installer/test/utils/testDefaults.ts @@ -28,7 +28,7 @@ export function testDefaults ( registries: Registries storeController: StoreController storeDir: string - verifyResolution?: ResolutionVerifier + resolutionVerifiers: ResolutionVerifier[] } & T { // Forward minimumReleaseAge policy into the Client so it builds the @@ -39,7 +39,7 @@ export function testDefaults ( ...(opts?.minimumReleaseAgeStrict != null ? { minimumReleaseAgeStrict: opts.minimumReleaseAgeStrict } : {}), ...(opts?.minimumReleaseAgeExclude != null ? { minimumReleaseAgeExclude: opts.minimumReleaseAgeExclude } : {}), } - const { storeController, storeDir, cacheDir, verifyResolution } = createTempStore({ + const { storeController, storeDir, cacheDir, resolutionVerifiers } = createTempStore({ ...opts, clientOptions: { ...(opts?.registries != null ? { registries: opts.registries } : {}), @@ -57,7 +57,7 @@ export function testDefaults ( }, storeController, storeDir, - verifyResolution, + resolutionVerifiers, ...opts, } as ( InstallOptions & @@ -66,7 +66,7 @@ export function testDefaults ( registries: Registries storeController: StoreController storeDir: string - verifyResolution?: ResolutionVerifier + resolutionVerifiers: ResolutionVerifier[] } & T ) diff --git a/installing/deps-installer/tsconfig.json b/installing/deps-installer/tsconfig.json index a838b53526..754a58fc00 100644 --- a/installing/deps-installer/tsconfig.json +++ b/installing/deps-installer/tsconfig.json @@ -90,9 +90,6 @@ { "path": "../../deps/path" }, - { - "path": "../../engine/runtime/system-node-version" - }, { "path": "../../exec/lifecycle" }, @@ -174,6 +171,9 @@ { "path": "../../testing/mock-agent" }, + { + "path": "../../testing/registry-mock" + }, { "path": "../../testing/temp-store" }, diff --git a/installing/deps-resolver/CHANGELOG.md b/installing/deps-resolver/CHANGELOG.md index 90c67d86f3..e26fb7e96d 100644 --- a/installing/deps-resolver/CHANGELOG.md +++ b/installing/deps-resolver/CHANGELOG.md @@ -1,5 +1,136 @@ # @pnpm/resolve-dependencies +## 1100.1.4 + +### Patch Changes + +- ad84fff: Reject dependency aliases that contain path-traversal segments (such as `@x/../../../../../.git/hooks`) when reading them from a package manifest or symlinking them into `node_modules`. A malicious registry package could otherwise use a transitive dependency key to make `pnpm install` create symlinks at attacker-chosen paths outside the intended `node_modules` directory. +- Updated dependencies [e55f4b5] +- Updated dependencies [35d2355] +- Updated dependencies [0721d64] + - @pnpm/lockfile.utils@1100.0.10 + - @pnpm/types@1101.2.0 + - @pnpm/resolving.npm-resolver@1101.3.3 + - @pnpm/deps.graph-hasher@1100.2.2 + - @pnpm/lockfile.preferred-versions@1100.0.12 + - @pnpm/config.version-policy@1100.1.2 + - @pnpm/core-loggers@1100.1.2 + - @pnpm/deps.path@1100.0.5 + - @pnpm/hooks.types@1100.0.9 + - @pnpm/lockfile.pruner@1100.0.8 + - @pnpm/lockfile.types@1100.0.8 + - @pnpm/pkg-manifest.reader@1100.0.5 + - @pnpm/pkg-manifest.utils@1100.2.1 + - @pnpm/resolving.resolver-base@1100.3.1 + - @pnpm/store.controller-types@1100.1.2 + - @pnpm/patching.config@1100.0.5 + - @pnpm/fetching.pick-fetcher@1100.0.9 + +## 1100.1.3 + +### Patch Changes + +- 3422cec: Fixed non-determinism in `pnpm dedupe` and `pnpm install` when a dependency graph contains packages with transitive peer dependencies on each other (e.g. `@aws-sdk/client-sts` and `@aws-sdk/client-sso-oidc`) and `auto-install-peers` is enabled. The lockfile no longer flips between two equally-valid forms across consecutive runs. The root cause was that `resolveDependencies` pushed onto its `pkgAddresses` / `postponedResolutionsQueue` arrays from inside `Promise.all`-spawned callbacks, so completion-order timing leaked into the array order and downstream cyclic-peer suffix assignment. Fixes [#8155](https://github.com/pnpm/pnpm/issues/8155). +- e0bd879: Fixed a regression introduced by [#11711](https://github.com/pnpm/pnpm/pull/11711) where `pnpm add ` (and any other wanted-dependency whose alias can't be parsed from the user-supplied spec, e.g. tarball URLs or `pnpm/test-git-fetch#sha`) was silently dropped from the manifest update and from `pendingBuilds`. The alias-keyed lookup added in that PR couldn't find a `wantedDependency` whose `alias` was `undefined` at parse time but resolved to a package name only after fetching, so the entry never made it into `specsToUpsert`. Restored the original index-based pairing between `directDependencies` and `wantedDependencies`; the catalog-protocol preservation that PR was originally fixing is unaffected because it's driven by `rdd.catalogLookup.userSpecifiedBareSpecifier`, not by the lookup. Fixes the three `rebuilds dependencies` / `rebuilds specific dependencies` / `rebuild with pending option` failures in `building/commands/test/build/index.ts`. +- Updated dependencies [212315d] + - @pnpm/resolving.npm-resolver@1101.3.2 + - @pnpm/fetching.pick-fetcher@1100.0.8 + +## 1100.1.2 + +### Patch Changes + +- @pnpm/resolving.npm-resolver@1101.3.1 + +## 1100.1.1 + +### Patch Changes + +- Updated dependencies [3a54205] +- Updated dependencies [1627943] +- Updated dependencies [64afc92] + - @pnpm/resolving.npm-resolver@1101.3.0 + - @pnpm/resolving.resolver-base@1100.3.0 + - @pnpm/pkg-manifest.utils@1100.2.0 + - @pnpm/types@1101.1.1 + - @pnpm/deps.graph-hasher@1100.2.1 + - @pnpm/fetching.pick-fetcher@1100.0.8 + - @pnpm/hooks.types@1100.0.8 + - @pnpm/lockfile.preferred-versions@1100.0.11 + - @pnpm/lockfile.types@1100.0.7 + - @pnpm/lockfile.utils@1100.0.9 + - @pnpm/store.controller-types@1100.1.1 + - @pnpm/config.version-policy@1100.1.1 + - @pnpm/core-loggers@1100.1.1 + - @pnpm/deps.path@1100.0.4 + - @pnpm/lockfile.pruner@1100.0.7 + - @pnpm/pkg-manifest.reader@1100.0.4 + - @pnpm/patching.config@1100.0.4 + +## 1100.1.0 + +### Minor Changes + +- 4195766: Tightened the `minimumReleaseAge` story so the bypass becomes explicit on disk instead of silent, and removed the discover-by-loop dance for strict-mode users: + + 1. Fresh resolutions in loose mode (`minimumReleaseAgeStrict: false`) that fall back to a version newer than the cutoff auto-collect the picked `name@version` into the workspace manifest's `minimumReleaseAgeExclude`. A single info message lists the additions; entries already on the list are left alone. + 2. The post-resolution lockfile verifier introduced in #11583 now runs in loose mode too — every accepted-immature pin must be on `minimumReleaseAgeExclude`, just like strict mode requires. A lockfile produced under a weaker (or absent) policy that still has immature entries is rejected the same way strict mode would reject it. + 3. **Strict mode (interactive)** no longer aborts on the first immature pick. The resolver gathers every immature direct _and_ transitive in one pass; before peer-dependency resolution runs, pnpm prompts the user with the full list and asks whether to add them all to `minimumReleaseAgeExclude` and proceed. Approve → install continues and the workspace manifest is written at the end. Decline → resolution aborts before the lockfile or package.json is touched (tarballs already in the store stay, since the store is idempotent). This closes the [#10488](https://github.com/pnpm/pnpm/issues/10488) loop where security bumps to packages with platform-specific transitives (e.g. `next` + the `@next/swc-*` shims) made users re-run `pnpm add` once per transitive. + 4. **Strict mode (non-interactive / CI)** now aborts with the full immature set in the error message instead of the first pick. The resolver always collects every immature direct + transitive; the install command then throws `ERR_PNPM_NO_MATURE_MATCHING_VERSION` listing each entry's `name@version` and publish time. Deterministic CI behavior is preserved (same exit code, same error code), but the error pinpoints every offending entry instead of forcing the discover-by-loop dance. The expected workflow is interactive approval locally → the lockfile + workspace manifest get committed → CI runs cleanly against the populated exclude list. + + 5. **The lockfile verifier now also covers `trustPolicy: 'no-downgrade'`.** The same post-resolution gate that re-checks `minimumReleaseAge` on lockfile entries now re-runs `failIfTrustDowngraded` for every npm-registry entry whose name isn't on `trustPolicyExclude`. The two checks share a single full-metadata fetch per package, so the extra coverage doesn't cost an extra round trip when both policies are active. Resolver-time trust checks still run as before — this just closes the gap when an entry bypasses resolution (peek path, `--frozen-lockfile`, restored CI cache). + + Pacquet parity: not ported — pacquet's `minimumReleaseAge` policy is itself only stubbed today (see `pacquet/crates/package-manager/src/version_policy.rs`). The auto-exclude, loose-mode verifier, prompt, and the new trust-policy verifier check will travel with the broader policy port whenever that happens. + +### Patch Changes + +- b6e2c8c: Make `pnpm self-update` respect `minimumReleaseAge` (and `minimumReleaseAgeExclude`) when resolving which pnpm version to install. + + When the `latest` dist-tag points to a version newer than the configured age threshold, `self-update` now selects the newest mature version instead unless excluded by `minimumReleaseAgeExclude`. + + Also makes `dlx` and `outdated` surface invalid `minimumReleaseAgeExclude` patterns under the same `ERR_PNPM_INVALID_MINIMUM_RELEASE_AGE_EXCLUDE` error code already used by `install`, instead of leaking the internal `ERR_PNPM_INVALID_VERSION_UNION` / `ERR_PNPM_NAME_PATTERN_IN_VERSION_UNION` codes. + +- 3ddde2b: **fix**: anchor the side-effects-cache key and global-virtual-store hash to the project's script-runner Node — `engines.runtime` pin when present, shell `node` otherwise — instead of pnpm's own runtime. + + `ENGINE_NAME` (the `;;node` prefix used as the side-effects-cache key and the engine portion of the GVS hash) was computed from `process.version` — the Node that runs pnpm itself. That was wrong in two situations: + + 1. **`@pnpm/exe` SEA bundle.** The bundle has its own embedded Node, not the `node` on the user's `PATH` that actually spawns lifecycle scripts. Two pnpm installations on the same machine (one SEA, one npm-package) therefore disagreed on the cache key, partitioning the side-effects cache and the global virtual store across two Node majors even though both installs would run scripts on the same shell `node`. + 2. **`engines.runtime` / `devEngines.runtime` pin.** When a project pins a Node version via `devEngines.runtime` (pnpm v11+), pnpm downloads that Node into `node_modules/node/` and uses it to run lifecycle scripts. But the hash still anchored to whichever Node ran pnpm itself, not to the pinned Node — so two installs of the same project with two different runner Nodes would still disagree on the GVS slot path even though scripts run on the same pinned Node. + + Three changes: + + - `@pnpm/engine.runtime.system-node-version` now exports `engineName(nodeVersion?)`. Resolves the version in this order: explicit override → `getSystemNodeVersion()` (which already prefers `node --version` over `process.version` in SEA contexts) → `process.version`. + - `@pnpm/deps.graph-hasher` now exports `findRuntimeNodeVersion(snapshotKeys)` — scans an iterable of lockfile snapshot keys for a `node@runtime:` entry and returns its bare version string. `calcDepState` and `calcGraphNodeHash`/`iterateHashedGraphNodes` accept a `nodeVersion?` (in the options bag for the first, as a trailing parameter / ctx field for the others), forwarded to `engineName()`. The default (no override) preserves the pre-change behaviour. The legacy `ENGINE_NAME` constant in `@pnpm/constants` is unchanged so external consumers and existing tests keep working; in non-SEA, non-pinned contexts every value lines up. + - Every install-side caller of the graph-hasher (`@pnpm/installing.deps-resolver`, `@pnpm/installing.deps-restorer`, `@pnpm/installing.deps-installer`, `@pnpm/building.during-install`, `@pnpm/building.after-install`, `@pnpm/deps.graph-builder`) now derives the project's pinned runtime via `findRuntimeNodeVersion(Object.keys(graph))` once per invocation and threads it through. + + On upgrade, two one-time GVS slot churns are possible: + + - **SEA-pnpm users** without a runtime pin: slots that previously hashed under the embedded-Node major (e.g. `node26`) now hash under the shell-Node major (e.g. `node24`), matching what pacquet, the npm-published `pnpm` package, and any other pnpm-compatible tool already produce. + - **Projects with a `devEngines.runtime` pin**: slots that previously hashed under the runner's Node major now hash under the pinned Node major, matching what the lifecycle scripts will actually run on. + + In both cases the old slots become prune-eligible. + +- Updated dependencies [963861c] +- Updated dependencies [4195766] +- Updated dependencies [31538bf] +- Updated dependencies [b6e2c8c] +- Updated dependencies [3ddde2b] +- Updated dependencies [5dc8be8] +- Updated dependencies [4a79336] + - @pnpm/resolving.npm-resolver@1101.2.0 + - @pnpm/resolving.resolver-base@1100.2.0 + - @pnpm/store.controller-types@1100.1.0 + - @pnpm/config.version-policy@1100.1.0 + - @pnpm/deps.graph-hasher@1100.2.0 + - @pnpm/core-loggers@1100.1.0 + - @pnpm/fetching.pick-fetcher@1100.0.7 + - @pnpm/hooks.types@1100.0.7 + - @pnpm/lockfile.preferred-versions@1100.0.10 + - @pnpm/lockfile.types@1100.0.6 + - @pnpm/lockfile.utils@1100.0.8 + - @pnpm/pkg-manifest.utils@1100.1.4 + - @pnpm/lockfile.pruner@1100.0.6 + ## 1100.0.10 ### Patch Changes diff --git a/installing/deps-resolver/package.json b/installing/deps-resolver/package.json index ba64f1fd4f..c77eae76f5 100644 --- a/installing/deps-resolver/package.json +++ b/installing/deps-resolver/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/installing.deps-resolver", - "version": "1100.0.10", + "version": "1100.1.4", "description": "Resolves dependency graph of a package", "keywords": [ "pnpm", @@ -40,7 +40,6 @@ "@pnpm/deps.graph-hasher": "workspace:*", "@pnpm/deps.path": "workspace:*", "@pnpm/deps.peer-range": "workspace:*", - "@pnpm/engine.runtime.system-node-version": "workspace:*", "@pnpm/error": "workspace:*", "@pnpm/fetching.pick-fetcher": "workspace:*", "@pnpm/hooks.types": "workspace:*", @@ -71,6 +70,7 @@ "safe-promise-defer": "catalog:", "semver": "catalog:", "semver-range-intersect": "catalog:", + "validate-npm-package-name": "catalog:", "version-selector-type": "catalog:" }, "peerDependencies": { @@ -82,7 +82,8 @@ "@pnpm/logger": "workspace:*", "@types/normalize-path": "catalog:", "@types/ramda": "catalog:", - "@types/semver": "catalog:" + "@types/semver": "catalog:", + "@types/validate-npm-package-name": "catalog:" }, "engines": { "node": ">=22.13" diff --git a/installing/deps-resolver/src/getNonDevWantedDependencies.ts b/installing/deps-resolver/src/getNonDevWantedDependencies.ts index d8ac98f539..611aae2d25 100644 --- a/installing/deps-resolver/src/getNonDevWantedDependencies.ts +++ b/installing/deps-resolver/src/getNonDevWantedDependencies.ts @@ -1,6 +1,8 @@ import type { Dependencies, DependenciesMeta, DependencyManifest } from '@pnpm/types' import { pickBy } from 'ramda' +import { assertValidDependencyAliases } from './validateDependencyAlias.js' + export interface WantedDependency { alias: string bareSpecifier: string // package reference @@ -10,9 +12,17 @@ export interface WantedDependency { saveCatalogName?: string } -type GetNonDevWantedDependenciesManifest = Pick +type GetNonDevWantedDependenciesManifest = Pick & { + name?: string + version?: string +} export function getNonDevWantedDependencies (pkg: GetNonDevWantedDependenciesManifest): WantedDependency[] { + const pkgDescription = pkg.name != null + ? `Package "${pkg.name}${pkg.version != null ? `@${pkg.version}` : ''}"` + : 'Package' + assertValidDependencyAliases(pkg.dependencies, pkgDescription) + assertValidDependencyAliases(pkg.optionalDependencies, pkgDescription) let bd = pkg.bundledDependencies ?? pkg.bundleDependencies if (bd === true) { bd = pkg.dependencies != null ? Object.keys(pkg.dependencies) : [] diff --git a/installing/deps-resolver/src/getWantedDependencies.ts b/installing/deps-resolver/src/getWantedDependencies.ts index 56f7aa9b2f..12832cb59a 100644 --- a/installing/deps-resolver/src/getWantedDependencies.ts +++ b/installing/deps-resolver/src/getWantedDependencies.ts @@ -6,6 +6,8 @@ import type { ProjectManifest, } from '@pnpm/types' +import { assertValidDependencyAliases } from './validateDependencyAlias.js' + export interface WantedDependency { alias: string bareSpecifier: string // package reference @@ -23,6 +25,10 @@ export function getWantedDependencies ( includeDirect?: IncludedDependencies } ): WantedDependency[] { + assertValidDependencyAliases(pkg.dependencies, 'The current package') + assertValidDependencyAliases(pkg.devDependencies, 'The current package') + assertValidDependencyAliases(pkg.optionalDependencies, 'The current package') + assertValidDependencyAliases(pkg.peerDependencies, 'The current package') let depsToInstall = filterDependenciesByType(pkg, opts?.includeDirect ?? { dependencies: true, diff --git a/installing/deps-resolver/src/index.ts b/installing/deps-resolver/src/index.ts index 3558b322ee..fc6756a65e 100644 --- a/installing/deps-resolver/src/index.ts +++ b/installing/deps-resolver/src/index.ts @@ -4,9 +4,9 @@ import type { Catalogs } from '@pnpm/catalogs.types' import { packageManifestLogger, } from '@pnpm/core-loggers' -import { iterateHashedGraphNodes } from '@pnpm/deps.graph-hasher' +import { findRuntimeNodeVersion, iterateHashedGraphNodes } from '@pnpm/deps.graph-hasher' import { isRuntimeDepPath } from '@pnpm/deps.path' -import { findRuntimeNodeVersion } from '@pnpm/engine.runtime.system-node-version' +import { PnpmError } from '@pnpm/error' import type { LockfileObject, ProjectSnapshot, @@ -17,6 +17,7 @@ import { getAllDependenciesFromManifest, getSpecFromPackageManifest, } from '@pnpm/pkg-manifest.utils' +import type { ResolutionPolicyViolation } from '@pnpm/resolving.resolver-base' import { type AllowBuild, DEPENDENCIES_FIELDS, @@ -111,6 +112,17 @@ export interface ResolveDependenciesResult { peerDependencyIssuesByProjects: PeerDependencyIssuesByProjects waitTillAllFetchingsFinish: () => Promise wantedToBeSkippedPackageIds: Set + /** + * Policy violations collected inline during resolution — each + * resolver pushes to the list whenever it picks a version that + * trips one of its own checks (today: `minimumReleaseAge`). The + * install command reacts via `handleResolutionPolicyViolations` + * (prompt / abort) and `mutateModules` forwards the array out so + * the auto-persist path at the install's tail can drain it into + * the workspace manifest. Empty when no policy is active or no + * pick violates. + */ + resolutionPolicyViolations: ResolutionPolicyViolation[] } export async function resolveDependencies ( @@ -128,6 +140,17 @@ export async function resolveDependencies ( allowUnusedPatches?: boolean enableGlobalVirtualStore?: boolean allProjectIds: string[] + /** + * Generic checkpoint invoked between `resolveDependencyTree` and + * `resolvePeers` once any inline-collected policy violations have + * been gathered. Callers can prompt, persist, or throw based on + * the violations. Throwing unwinds before any peer-dep work, + * lockfile write, package.json update, or modules-dir change. + * Intentionally policy-neutral: each resolver owns its violation + * codes and the hook implementer (install command) decides what + * to do with them. + */ + handleResolutionPolicyViolations?: (violations: readonly ResolutionPolicyViolation[]) => Promise } ): Promise { const _toResolveImporter = toResolveImporter.bind(null, { @@ -149,8 +172,36 @@ export async function resolveDependencies ( appliedPatches, time, allPeerDepNames, + resolutionPolicyViolations, } = await resolveDependencyTree(projectsToResolve, opts) + // Resolver-policy gate between main resolution and peer-dep + // resolution: every resolver records its own policy violations + // inline as it picks each version, and we hand the accumulated + // list to the install command's hook. The hook throws to abort + // cleanly — nothing on disk has changed yet, and we haven't paid + // the cost of peer resolution. Dispatch stays policy-neutral: each + // resolver owns its violation codes, and the hook implementer + // decides what to do with them. + // + // If violations fired but no hook was wired, throw rather than + // silently dropping them — the resolver-policy contract is "every + // pick that trips a check produces a violation that gets handled"; + // a missing handler means the caller forgot to opt in and would + // otherwise see policy-rejected versions land in the lockfile. + if (resolutionPolicyViolations.length > 0) { + if (!opts.handleResolutionPolicyViolations) { + throw new PnpmError( + 'RESOLUTION_POLICY_VIOLATIONS_UNHANDLED', + `${resolutionPolicyViolations.length} resolution-policy ${resolutionPolicyViolations.length === 1 ? 'violation was' : 'violations were'} produced but no handleResolutionPolicyViolations callback was wired to react to them.`, + { + hint: 'Internal: resolveDependencies needs a handleResolutionPolicyViolations callback whenever a policy that can produce violations (today: minimumReleaseAge) is active. Wire setupPolicyHandlers (in @pnpm/installing.commands) or supply a callback directly.', + } + ) + } + await opts.handleResolutionPolicyViolations(resolutionPolicyViolations) + } + opts.storeController.clearResolutionCache() // We only check whether patches were applied in cases when the whole lockfile was reanalyzed. @@ -359,6 +410,7 @@ export async function resolveDependencies ( peerDependencyIssuesByProjects, waitTillAllFetchingsFinish, wantedToBeSkippedPackageIds, + resolutionPolicyViolations, } } diff --git a/installing/deps-resolver/src/resolveDependencies.ts b/installing/deps-resolver/src/resolveDependencies.ts index 1142afe759..7692eb5c91 100644 --- a/installing/deps-resolver/src/resolveDependencies.ts +++ b/installing/deps-resolver/src/resolveDependencies.ts @@ -28,6 +28,7 @@ import { type PkgResolutionId, type PreferredVersions, type Resolution, + type ResolutionPolicyViolation, type WorkspacePackages, } from '@pnpm/resolving.resolver-base' import type { @@ -155,6 +156,7 @@ export interface ResolutionContext { defaultTag: string dryRun: boolean forceFullResolution: boolean + updateChecksums?: boolean ignoreScripts?: boolean resolvedPkgsById: ResolvedPkgsById resolvePeersFromWorkspaceRoot?: boolean @@ -187,6 +189,16 @@ export interface ResolutionContext { hoistPeers?: boolean maximumPublishedBy?: Date publishedByExclude?: PackageVersionPolicy + /** + * Shared accumulator the resolver pushes into when an inline policy + * check (today: minimumReleaseAge in `npm-resolver`) flags a pick. + * resolveDependencyTree hands the populated array back to the install + * command via its return so the post-tree gate can prompt / abort / + * persist without re-walking the resolved tree. Each verifier code + * (`MINIMUM_RELEASE_AGE_VIOLATION`, `TRUST_DOWNGRADE`, …) is the + * contract surface for downstream UX. + */ + resolutionPolicyViolations: ResolutionPolicyViolation[] trustPolicy?: TrustPolicy trustPolicyExclude?: PackageVersionPolicy trustPolicyIgnoreAfter?: number @@ -672,29 +684,29 @@ export async function resolveDependencies ( const postponedResolutionsQueue: PostponedResolutionFunction[] = [] const postponedPeersResolutionQueue: PostponedPeersResolutionFunction[] = [] const pkgAddresses: PkgAddress[] = [] - await Promise.all( - extendedWantedDeps.map(async (extendedWantedDep) => { - const { - resolveDependencyResult, - postponedResolution, - postponedPeersResolution, - } = await resolveDependenciesOfDependency( - ctx, - preferredVersions, - options, - extendedWantedDep - ) - if (resolveDependencyResult) { - pkgAddresses.push(resolveDependencyResult as PkgAddress) - } - if (postponedResolution) { - postponedResolutionsQueue.push(postponedResolution) - } - if (postponedPeersResolution) { - postponedPeersResolutionQueue.push(postponedPeersResolution) - } - }) + // Resolve in parallel, then drain the results in input order. Pushing + // from inside the Promise.all callbacks would leak completion-order timing + // into pkgAddresses / postponedResolutionsQueue, which downstream + // determines how cyclic peer suffixes are assigned. See pnpm/pnpm#8155. + const resolvedDependencies = await Promise.all( + extendedWantedDeps.map((extendedWantedDep) => resolveDependenciesOfDependency( + ctx, + preferredVersions, + options, + extendedWantedDep + )) ) + for (const { resolveDependencyResult, postponedResolution, postponedPeersResolution } of resolvedDependencies) { + if (resolveDependencyResult) { + pkgAddresses.push(resolveDependencyResult as PkgAddress) + } + if (postponedResolution) { + postponedResolutionsQueue.push(postponedResolution) + } + if (postponedPeersResolution) { + postponedPeersResolutionQueue.push(postponedPeersResolution) + } + } const newPreferredVersions = Object.create(preferredVersions) as PreferredVersions const currentParentPkgAliases: Record = {} for (const pkgAddress of pkgAddresses) { @@ -867,6 +879,7 @@ async function resolveDependenciesOfDependency ( proceed: extendedWantedDep.proceed || updateShouldContinue || ctx.updatedSet.size > 0, publishedBy: options.publishedBy, update: update ? options.updateToLatest ? 'latest' : 'compatible' : false, + updateChecksums: ctx.updateChecksums, updateDepth, updateRequested, supportedArchitectures: options.supportedArchitectures, @@ -1255,6 +1268,7 @@ interface ResolveDependencyOptions { publishedBy?: Date pickLowestVersion?: boolean update: false | 'compatible' | 'latest' + updateChecksums?: boolean updateDepth: number /** * Whether or not an update is requested based on filter conditions (such as @@ -1356,6 +1370,7 @@ async function resolveDependency ( trustPolicyExclude: ctx.trustPolicyExclude, trustPolicyIgnoreAfter: ctx.trustPolicyIgnoreAfter, update: options.update, + updateChecksums: options.updateChecksums, workspacePackages: ctx.workspacePackages, supportedArchitectures: options.supportedArchitectures, onFetchError: (err: any) => { // eslint-disable-line @@ -1373,7 +1388,7 @@ async function resolveDependency ( bareSpecifier: wantedDependency.bareSpecifier, version: wantedDependency.alias ? wantedDependency.bareSpecifier : undefined, } - if (wantedDependency.optional && err.code !== 'ERR_PNPM_TRUST_DOWNGRADE' && err.code !== 'ERR_PNPM_NO_MATURE_MATCHING_VERSION') { + if (wantedDependency.optional && err.code !== 'ERR_PNPM_TRUST_DOWNGRADE') { skippedOptionalDependencyLogger.debug({ details: err.toString(), package: wantedDependencyDetails, @@ -1398,6 +1413,14 @@ async function resolveDependency ( }, }) + // Resolver-inline policy violations (e.g. minimumReleaseAge) flow up + // here; collect them onto the shared context so resolveDependencyTree + // can hand the full set to the install command between + // resolveDependencyTree and resolvePeers. + if (pkgResponse.body.policyViolation) { + ctx.resolutionPolicyViolations.push(pkgResponse.body.policyViolation) + } + // Check if exotic dependencies are disallowed in subdependencies if ( ctx.blockExoticSubdeps && diff --git a/installing/deps-resolver/src/resolveDependencyTree.ts b/installing/deps-resolver/src/resolveDependencyTree.ts index 404284882d..cdbcf5b45a 100644 --- a/installing/deps-resolver/src/resolveDependencyTree.ts +++ b/installing/deps-resolver/src/resolveDependencyTree.ts @@ -5,7 +5,7 @@ import type { LockfileObject } from '@pnpm/lockfile.types' import { globalWarn } from '@pnpm/logger' import type { PatchGroupRecord } from '@pnpm/patching.config' import { BUILTIN_NAMED_REGISTRIES } from '@pnpm/resolving.npm-resolver' -import type { PreferredVersions, Resolution, WorkspacePackages } from '@pnpm/resolving.resolver-base' +import type { PreferredVersions, Resolution, ResolutionPolicyViolation, WorkspacePackages } from '@pnpm/resolving.resolver-base' import type { StoreController } from '@pnpm/store.controller-types' import type { AllowBuild, @@ -115,6 +115,7 @@ export interface ResolveDependenciesOptions { engineStrict: boolean force: boolean forceFullResolution: boolean + updateChecksums?: boolean ignoreScripts?: boolean hooks: { readPackage?: ReadPackageHook @@ -159,6 +160,14 @@ export interface ResolveDependencyTreeResult { wantedToBeSkippedPackageIds: Set appliedPatches: Set time?: Record + /** + * Policy violations collected inline during resolution — the + * resolver pushes to this list whenever it picks a package that + * trips one of its own checks (today: `minimumReleaseAge`). The + * shape mirrors `ResolutionPolicyViolation`; downstream callers + * filter by `code` to decide what to do. + */ + resolutionPolicyViolations: ResolutionPolicyViolation[] } export async function resolveDependencyTree ( @@ -182,6 +191,7 @@ export async function resolveDependencyTree ( engineStrict: opts.engineStrict, force: opts.force, forceFullResolution: opts.forceFullResolution, + updateChecksums: opts.updateChecksums, ignoreScripts: opts.ignoreScripts, injectWorkspacePackages: opts.injectWorkspacePackages, linkWorkspacePackagesDepth: opts.linkWorkspacePackagesDepth ?? -1, @@ -220,6 +230,7 @@ export async function resolveDependencyTree ( trustPolicyExclude: opts.trustPolicyExclude ? createPackageVersionPolicyOrThrow(opts.trustPolicyExclude, 'trustPolicyExclude') : undefined, trustPolicyIgnoreAfter: opts.trustPolicyIgnoreAfter, blockExoticSubdeps: opts.blockExoticSubdeps, + resolutionPolicyViolations: [], } const resolveArgs: ImporterToResolve[] = importers.map((importer) => { @@ -343,6 +354,7 @@ export async function resolveDependencyTree ( appliedPatches: ctx.appliedPatches, time, allPeerDepNames: ctx.allPeerDepNames, + resolutionPolicyViolations: ctx.resolutionPolicyViolations, } } diff --git a/installing/deps-resolver/src/validateDependencyAlias.ts b/installing/deps-resolver/src/validateDependencyAlias.ts new file mode 100644 index 0000000000..e23cbfcde9 --- /dev/null +++ b/installing/deps-resolver/src/validateDependencyAlias.ts @@ -0,0 +1,31 @@ +import { PnpmError } from '@pnpm/error' +import validateNpmPackageName from 'validate-npm-package-name' + +// An alias is the directory name pnpm creates inside `node_modules`, so +// it must be a valid npm package name. Anything else (path-traversal +// shapes such as `@x/../../../../../.git/hooks`, control characters, +// names that collide with pnpm's own `node_modules` layout such as +// `.bin` / `.pnpm` / `node_modules`) is rejected. Matches the +// `validForOldPackages` check `parseWantedDependency` applies to +// CLI-given names. +export function isValidDependencyAlias (alias: string): boolean { + return typeof alias === 'string' && validateNpmPackageName(alias).validForOldPackages +} + +export function assertValidDependencyAliases ( + deps: Record | undefined, + parentPkgDescription: string +): void { + if (deps == null) return + for (const alias of Object.keys(deps)) { + if (!isValidDependencyAlias(alias)) { + throw new PnpmError( + 'INVALID_DEPENDENCY_NAME', + `${parentPkgDescription} contains a dependency with an invalid name: ${JSON.stringify(alias)}`, + { + hint: 'A dependency name must be a valid npm package name — a single `name` or `@scope/name` consisting of URL-friendly characters, with no leading `.` or `_`, and not equal to reserved names such as `node_modules`.', + } + ) + } + } +} diff --git a/installing/deps-resolver/test/validateDependencyAlias.test.ts b/installing/deps-resolver/test/validateDependencyAlias.test.ts new file mode 100644 index 0000000000..c161db58e4 --- /dev/null +++ b/installing/deps-resolver/test/validateDependencyAlias.test.ts @@ -0,0 +1,67 @@ +import { expect, test } from '@jest/globals' + +import { assertValidDependencyAliases, isValidDependencyAlias } from '../lib/validateDependencyAlias.js' + +test.each([ + ['foo'], + ['Foo'], + ['@scope/name'], + ['@s/x'], + ['lodash.merge'], + ['a_b'], + ['a-b'], + ['x'], + ['underscore'], +])('accepts %p', (alias) => { + expect(isValidDependencyAlias(alias)).toBe(true) +}) + +test.each([ + ['', 'empty string'], + ['..', 'parent traversal'], + ['.', 'current dir'], + ['/foo', 'absolute posix'], + ['foo/bar', 'unscoped slash'], + ['@scope/name/extra', 'scoped with extra segment'], + ['@scope/../etc', 'scope with parent traversal'], + ['@x/../../../../../.git/hooks', 'PoC payload'], + ['foo\\bar', 'backslash'], + ['C:\\Windows\\System32', 'windows absolute'], + ['foo\0bar', 'null byte'], + ['scope/name', 'two segments without @'], + ['./foo', 'current dir prefix'], + ['.bin', 'leading dot (collides with pnpm .bin)'], + ['.pnpm', 'leading dot (collides with pnpm .pnpm)'], + ['_foo', 'leading underscore'], + ['node_modules', 'reserved name'], + ['favicon.ico', 'reserved name'], + [' foo ', 'leading/trailing whitespace'], + ['foo bar', 'embedded whitespace'], + ['foo?bar', 'non-url-friendly character'], +])('rejects %s (%s)', (alias) => { + expect(isValidDependencyAlias(alias)).toBe(false) +}) + +test('assertValidDependencyAliases throws ERR_PNPM_INVALID_DEPENDENCY_NAME for malicious aliases', () => { + expect(() => { + assertValidDependencyAliases({ '@x/../../../../../.git/hooks': '1.0.0' }, 'Package "bad@1.0.0"') + }).toThrow(expect.objectContaining({ + code: 'ERR_PNPM_INVALID_DEPENDENCY_NAME', + message: expect.stringContaining('Package "bad@1.0.0" contains a dependency with an invalid name'), + })) +}) + +test('assertValidDependencyAliases is a no-op for undefined and empty input', () => { + expect(() => { + assertValidDependencyAliases(undefined, 'pkg') + }).not.toThrow() + expect(() => { + assertValidDependencyAliases({}, 'pkg') + }).not.toThrow() +}) + +test('assertValidDependencyAliases is a no-op for valid aliases', () => { + expect(() => { + assertValidDependencyAliases({ foo: '1.0.0', '@scope/bar': '2.0.0' }, 'pkg') + }).not.toThrow() +}) diff --git a/installing/deps-resolver/tsconfig.json b/installing/deps-resolver/tsconfig.json index 6330cfcb2e..0d5b77c4b1 100644 --- a/installing/deps-resolver/tsconfig.json +++ b/installing/deps-resolver/tsconfig.json @@ -42,9 +42,6 @@ { "path": "../../deps/peer-range" }, - { - "path": "../../engine/runtime/system-node-version" - }, { "path": "../../fetching/pick-fetcher" }, diff --git a/installing/deps-restorer/CHANGELOG.md b/installing/deps-restorer/CHANGELOG.md index dad8f97b71..dc0bd22e16 100644 --- a/installing/deps-restorer/CHANGELOG.md +++ b/installing/deps-restorer/CHANGELOG.md @@ -1,5 +1,138 @@ # @pnpm/headless +## 1101.1.6 + +### Patch Changes + +- Updated dependencies [aa6149d] +- Updated dependencies [a456dc7] +- Updated dependencies [ad84fff] +- Updated dependencies [e55f4b5] +- Updated dependencies [35d2355] + - @pnpm/worker@1100.1.8 + - @pnpm/workspace.project-manifest-reader@1100.0.9 + - @pnpm/fs.symlink-dependency@1100.0.6 + - @pnpm/lockfile.utils@1100.0.10 + - @pnpm/types@1101.2.0 + - @pnpm/building.during-install@1101.0.14 + - @pnpm/bins.linker@1100.0.10 + - @pnpm/installing.linking.direct-dep-linker@1100.0.6 + - @pnpm/deps.graph-builder@1100.0.12 + - @pnpm/deps.graph-hasher@1100.2.2 + - @pnpm/installing.linking.modules-cleaner@1100.1.4 + - @pnpm/installing.linking.real-hoist@1100.0.10 + - @pnpm/lockfile.filtering@1100.1.3 + - @pnpm/lockfile.fs@1100.1.2 + - @pnpm/lockfile.to-pnp@1100.0.11 + - @pnpm/config.package-is-installable@1100.0.7 + - @pnpm/building.policy@1100.0.7 + - @pnpm/core-loggers@1100.1.2 + - @pnpm/deps.path@1100.0.5 + - @pnpm/exec.lifecycle@1100.0.14 + - @pnpm/installing.linking.hoist@1100.0.10 + - @pnpm/installing.modules-yaml@1100.0.6 + - @pnpm/installing.package-requester@1101.0.9 + - @pnpm/pkg-manifest.reader@1100.0.5 + - @pnpm/store.controller-types@1100.1.2 + - @pnpm/patching.config@1100.0.5 + +## 1101.1.5 + +### Patch Changes + +- Updated dependencies [d7da112] + - @pnpm/workspace.project-manifest-reader@1100.0.8 + - @pnpm/bins.linker@1100.0.9 + - @pnpm/building.during-install@1101.0.13 + - @pnpm/exec.lifecycle@1100.0.13 + - @pnpm/installing.linking.hoist@1100.0.9 + - @pnpm/installing.package-requester@1101.0.8 + +## 1101.1.4 + +### Patch Changes + +- Updated dependencies [9cb48bb] +- Updated dependencies [64afc92] + - @pnpm/lockfile.fs@1100.1.1 + - @pnpm/exec.lifecycle@1100.0.12 + - @pnpm/types@1101.1.1 + - @pnpm/building.during-install@1101.0.12 + - @pnpm/deps.graph-builder@1100.0.11 + - @pnpm/installing.linking.real-hoist@1100.0.9 + - @pnpm/lockfile.to-pnp@1100.0.10 + - @pnpm/installing.package-requester@1101.0.8 + - @pnpm/deps.graph-hasher@1100.2.1 + - @pnpm/lockfile.utils@1100.0.9 + - @pnpm/store.controller-types@1100.1.1 + - @pnpm/bins.linker@1100.0.8 + - @pnpm/workspace.project-manifest-reader@1100.0.7 + - @pnpm/building.policy@1100.0.6 + - @pnpm/config.package-is-installable@1100.0.6 + - @pnpm/core-loggers@1100.1.1 + - @pnpm/deps.path@1100.0.4 + - @pnpm/fs.symlink-dependency@1100.0.5 + - @pnpm/installing.linking.hoist@1100.0.8 + - @pnpm/installing.linking.modules-cleaner@1100.1.3 + - @pnpm/installing.modules-yaml@1100.0.5 + - @pnpm/lockfile.filtering@1100.1.2 + - @pnpm/pkg-manifest.reader@1100.0.4 + - @pnpm/worker@1100.1.7 + - @pnpm/installing.linking.direct-dep-linker@1100.0.5 + - @pnpm/patching.config@1100.0.4 + +## 1101.1.3 + +### Patch Changes + +- 3ddde2b: **fix**: anchor the side-effects-cache key and global-virtual-store hash to the project's script-runner Node — `engines.runtime` pin when present, shell `node` otherwise — instead of pnpm's own runtime. + + `ENGINE_NAME` (the `;;node` prefix used as the side-effects-cache key and the engine portion of the GVS hash) was computed from `process.version` — the Node that runs pnpm itself. That was wrong in two situations: + + 1. **`@pnpm/exe` SEA bundle.** The bundle has its own embedded Node, not the `node` on the user's `PATH` that actually spawns lifecycle scripts. Two pnpm installations on the same machine (one SEA, one npm-package) therefore disagreed on the cache key, partitioning the side-effects cache and the global virtual store across two Node majors even though both installs would run scripts on the same shell `node`. + 2. **`engines.runtime` / `devEngines.runtime` pin.** When a project pins a Node version via `devEngines.runtime` (pnpm v11+), pnpm downloads that Node into `node_modules/node/` and uses it to run lifecycle scripts. But the hash still anchored to whichever Node ran pnpm itself, not to the pinned Node — so two installs of the same project with two different runner Nodes would still disagree on the GVS slot path even though scripts run on the same pinned Node. + + Three changes: + + - `@pnpm/engine.runtime.system-node-version` now exports `engineName(nodeVersion?)`. Resolves the version in this order: explicit override → `getSystemNodeVersion()` (which already prefers `node --version` over `process.version` in SEA contexts) → `process.version`. + - `@pnpm/deps.graph-hasher` now exports `findRuntimeNodeVersion(snapshotKeys)` — scans an iterable of lockfile snapshot keys for a `node@runtime:` entry and returns its bare version string. `calcDepState` and `calcGraphNodeHash`/`iterateHashedGraphNodes` accept a `nodeVersion?` (in the options bag for the first, as a trailing parameter / ctx field for the others), forwarded to `engineName()`. The default (no override) preserves the pre-change behaviour. The legacy `ENGINE_NAME` constant in `@pnpm/constants` is unchanged so external consumers and existing tests keep working; in non-SEA, non-pinned contexts every value lines up. + - Every install-side caller of the graph-hasher (`@pnpm/installing.deps-resolver`, `@pnpm/installing.deps-restorer`, `@pnpm/installing.deps-installer`, `@pnpm/building.during-install`, `@pnpm/building.after-install`, `@pnpm/deps.graph-builder`) now derives the project's pinned runtime via `findRuntimeNodeVersion(Object.keys(graph))` once per invocation and threads it through. + + On upgrade, two one-time GVS slot churns are possible: + + - **SEA-pnpm users** without a runtime pin: slots that previously hashed under the embedded-Node major (e.g. `node26`) now hash under the shell-Node major (e.g. `node24`), matching what pacquet, the npm-published `pnpm` package, and any other pnpm-compatible tool already produce. + - **Projects with a `devEngines.runtime` pin**: slots that previously hashed under the runner's Node major now hash under the pinned Node major, matching what the lifecycle scripts will actually run on. + + In both cases the old slots become prune-eligible. + +- Updated dependencies [4195766] +- Updated dependencies [6e93f35] +- Updated dependencies [3ddde2b] +- Updated dependencies [5dc8be8] +- Updated dependencies [4a79336] +- Updated dependencies [2a9bd89] + - @pnpm/store.controller-types@1100.1.0 + - @pnpm/lockfile.fs@1100.1.0 + - @pnpm/building.during-install@1101.0.11 + - @pnpm/deps.graph-builder@1100.0.10 + - @pnpm/deps.graph-hasher@1100.2.0 + - @pnpm/core-loggers@1100.1.0 + - @pnpm/installing.package-requester@1101.0.7 + - @pnpm/lockfile.utils@1100.0.8 + - @pnpm/exec.lifecycle@1100.0.11 + - @pnpm/installing.linking.modules-cleaner@1100.1.2 + - @pnpm/building.policy@1100.0.5 + - @pnpm/installing.linking.real-hoist@1100.0.8 + - @pnpm/lockfile.to-pnp@1100.0.9 + - @pnpm/config.package-is-installable@1100.0.5 + - @pnpm/fs.symlink-dependency@1100.0.4 + - @pnpm/installing.linking.direct-dep-linker@1100.0.4 + - @pnpm/installing.linking.hoist@1100.0.7 + - @pnpm/lockfile.filtering@1100.1.1 + - @pnpm/worker@1100.1.6 + - @pnpm/bins.linker@1100.0.7 + - @pnpm/workspace.project-manifest-reader@1100.0.6 + ## 1101.1.2 ### Patch Changes diff --git a/installing/deps-restorer/package.json b/installing/deps-restorer/package.json index be1d47db77..93dc79d9e7 100644 --- a/installing/deps-restorer/package.json +++ b/installing/deps-restorer/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/installing.deps-restorer", - "version": "1101.1.2", + "version": "1101.1.6", "description": "Fast installation using only pnpm-lock.yaml", "keywords": [ "pnpm", @@ -48,7 +48,6 @@ "@pnpm/deps.graph-builder": "workspace:*", "@pnpm/deps.graph-hasher": "workspace:*", "@pnpm/deps.path": "workspace:*", - "@pnpm/engine.runtime.system-node-version": "workspace:*", "@pnpm/error": "workspace:*", "@pnpm/exec.lifecycle": "workspace:*", "@pnpm/fs.symlink-dependency": "workspace:*", diff --git a/installing/deps-restorer/src/index.ts b/installing/deps-restorer/src/index.ts index 29a6e4b4dc..f1bad4e844 100644 --- a/installing/deps-restorer/src/index.ts +++ b/installing/deps-restorer/src/index.ts @@ -22,9 +22,8 @@ import { lockfileToDepGraph, type LockfileToDepGraphOptions, } from '@pnpm/deps.graph-builder' -import { calcDepState, type DepsStateCache } from '@pnpm/deps.graph-hasher' +import { calcDepState, type DepsStateCache, findRuntimeNodeVersion } from '@pnpm/deps.graph-hasher' import * as dp from '@pnpm/deps.path' -import { findRuntimeNodeVersion } from '@pnpm/engine.runtime.system-node-version' import { PnpmError } from '@pnpm/error' import { makeNodeRequireOption, diff --git a/installing/deps-restorer/src/linkHoistedModules.ts b/installing/deps-restorer/src/linkHoistedModules.ts index 7cd9913003..169ddb7ed1 100644 --- a/installing/deps-restorer/src/linkHoistedModules.ts +++ b/installing/deps-restorer/src/linkHoistedModules.ts @@ -10,8 +10,7 @@ import type { DependenciesGraph, DepHierarchy, } from '@pnpm/deps.graph-builder' -import { calcDepState, type DepsStateCache } from '@pnpm/deps.graph-hasher' -import { findRuntimeNodeVersion } from '@pnpm/engine.runtime.system-node-version' +import { calcDepState, type DepsStateCache, findRuntimeNodeVersion } from '@pnpm/deps.graph-hasher' import { logger } from '@pnpm/logger' import type { PackageFilesResponse, diff --git a/installing/deps-restorer/test/fixtures/peer-variant-missing-resolution/package.json b/installing/deps-restorer/test/fixtures/peer-variant-missing-resolution/package.json new file mode 100644 index 0000000000..83e000174f --- /dev/null +++ b/installing/deps-restorer/test/fixtures/peer-variant-missing-resolution/package.json @@ -0,0 +1,8 @@ +{ + "name": "peer-variant-missing-resolution", + "version": "1.0.0", + "private": true, + "dependencies": { + "pkg-a": "workspace:*" + } +} diff --git a/installing/deps-restorer/test/fixtures/peer-variant-missing-resolution/packages/peer/package.json b/installing/deps-restorer/test/fixtures/peer-variant-missing-resolution/packages/peer/package.json new file mode 100644 index 0000000000..2b8b6606e6 --- /dev/null +++ b/installing/deps-restorer/test/fixtures/peer-variant-missing-resolution/packages/peer/package.json @@ -0,0 +1,5 @@ +{ + "name": "peer", + "version": "1.0.0", + "private": true +} diff --git a/installing/deps-restorer/test/fixtures/peer-variant-missing-resolution/packages/pkg-a/package.json b/installing/deps-restorer/test/fixtures/peer-variant-missing-resolution/packages/pkg-a/package.json new file mode 100644 index 0000000000..d0a16e5de2 --- /dev/null +++ b/installing/deps-restorer/test/fixtures/peer-variant-missing-resolution/packages/pkg-a/package.json @@ -0,0 +1,8 @@ +{ + "name": "pkg-a", + "version": "1.0.0", + "private": true, + "peerDependencies": { + "peer": "1.0.0" + } +} diff --git a/installing/deps-restorer/test/fixtures/peer-variant-missing-resolution/pnpm-lock.yaml b/installing/deps-restorer/test/fixtures/peer-variant-missing-resolution/pnpm-lock.yaml new file mode 100644 index 0000000000..b904def648 --- /dev/null +++ b/installing/deps-restorer/test/fixtures/peer-variant-missing-resolution/pnpm-lock.yaml @@ -0,0 +1,51 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + injectWorkspacePackages: true + +importers: + + .: + dependencies: + pkg-a: + specifier: workspace:* + version: 'file:packages/pkg-a(peer@1.0.0)' + + packages/peer: {} + + packages/pkg-a: + peerDependencies: + peer: + specifier: 1.0.0 + version: 1.0.0 + +packages: + + # Peer-variant entry whose base `pkg-a@file:packages/pkg-a` packages entry + # was dropped by a pruner (e.g. `turbo prune --docker`). With the base gone + # `convertToLockfileObject` cannot inherit `resolution`; pre-fix this + # crashed `lockfileToDepGraph` with + # `TypeError: Cannot use 'in' operator to search for 'directory' in undefined`. + 'pkg-a@file:packages/pkg-a(peer@1.0.0)': + dependencies: + peer: 1.0.0 + + # Peer dep resolved to the workspace `packages/peer/` directory. This + # entry is required for `filterLockfileByImportersAndEngine` to walk + # the graph past the variant entry — otherwise it throws + # `LockfileMissingDependencyError` once the graph-builder no longer + # crashes early on the missing `resolution`. + 'peer@1.0.0': + resolution: { directory: packages/peer, type: directory } + +snapshots: + + 'pkg-a@file:packages/pkg-a': {} + + 'pkg-a@file:packages/pkg-a(peer@1.0.0)': + dependencies: + peer: 1.0.0 + + 'peer@1.0.0': {} diff --git a/installing/deps-restorer/test/fixtures/peer-variant-missing-resolution/pnpm-workspace.yaml b/installing/deps-restorer/test/fixtures/peer-variant-missing-resolution/pnpm-workspace.yaml new file mode 100644 index 0000000000..9af4682d1d --- /dev/null +++ b/installing/deps-restorer/test/fixtures/peer-variant-missing-resolution/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +packages: + - "packages/*" +injectWorkspacePackages: true diff --git a/installing/deps-restorer/test/index.ts b/installing/deps-restorer/test/index.ts index 518d40a1e1..e725e3a752 100644 --- a/installing/deps-restorer/test/index.ts +++ b/installing/deps-restorer/test/index.ts @@ -919,3 +919,25 @@ test('installing a package deeply installs all required dependencies', async () expect(projectAssertion.requireModule('is-positive')).toBeTruthy() } }) + +// Regression test: `lockfileToDepGraph` crashed with +// `TypeError: Cannot use 'in' operator to search for 'directory' in undefined` +// when a peer-dep variant snapshot in the lockfile omitted its `resolution` +// field. The variant intentionally inherits resolution from the base entry, +// so this shape is valid pnpm output but the graph builder accessed +// `pkgSnapshot.resolution` without guarding for undefined. +test('headlessInstall: peer-variant snapshot without `resolution` does not crash', async () => { + const workspaceFixture = f.prepare('peer-variant-missing-resolution') + const projects = [ + workspaceFixture, + path.join(workspaceFixture, 'packages', 'pkg-a'), + path.join(workspaceFixture, 'packages', 'peer'), + ] + + // The bug surfaces as a rejection from `headlessInstall`, so awaiting it + // is enough — jest fails the test on an unhandled rejection. + await headlessInstall(await testDefaults({ + lockfileDir: workspaceFixture, + projects, + })) +}) diff --git a/installing/deps-restorer/tsconfig.json b/installing/deps-restorer/tsconfig.json index 196d57dbcb..b9d2042f22 100644 --- a/installing/deps-restorer/tsconfig.json +++ b/installing/deps-restorer/tsconfig.json @@ -60,9 +60,6 @@ { "path": "../../deps/path" }, - { - "path": "../../engine/runtime/system-node-version" - }, { "path": "../../exec/lifecycle" }, diff --git a/installing/env-installer/CHANGELOG.md b/installing/env-installer/CHANGELOG.md index ef62fc9d26..57b18081ac 100644 --- a/installing/env-installer/CHANGELOG.md +++ b/installing/env-installer/CHANGELOG.md @@ -1,5 +1,118 @@ # @pnpm/config.deps-installer +## 1101.1.3 + +### Patch Changes + +- Updated dependencies [a23956e] +- Updated dependencies [aa6149d] +- Updated dependencies [ad84fff] +- Updated dependencies [e55f4b5] +- Updated dependencies [35d2355] +- Updated dependencies [0721d64] + - @pnpm/network.auth-header@1101.0.0 + - @pnpm/worker@1100.1.8 + - @pnpm/installing.deps-resolver@1100.1.4 + - @pnpm/lockfile.utils@1100.0.10 + - @pnpm/types@1101.2.0 + - @pnpm/resolving.npm-resolver@1101.3.3 + - @pnpm/deps.graph-hasher@1100.2.2 + - @pnpm/lockfile.fs@1100.1.2 + - @pnpm/config.package-is-installable@1100.0.7 + - @pnpm/config.pick-registry-for-package@1100.0.6 + - @pnpm/config.writer@1100.0.10 + - @pnpm/core-loggers@1100.1.2 + - @pnpm/lockfile.pruner@1100.0.8 + - @pnpm/lockfile.types@1100.0.8 + - @pnpm/network.fetch@1100.0.7 + - @pnpm/pkg-manifest.reader@1100.0.5 + - @pnpm/store.controller@1101.0.9 + - @pnpm/store.controller-types@1100.1.2 + +## 1101.1.2 + +### Patch Changes + +- 155af87: Fixed `pnpm add --config` leaving orphan entries in `pnpm-lock.env.yaml` (the optional subdependencies of the previously resolved version of the updated config dependency). +- Updated dependencies [3422cec] +- Updated dependencies [e0bd879] +- Updated dependencies [212315d] + - @pnpm/installing.deps-resolver@1100.1.3 + - @pnpm/resolving.npm-resolver@1101.3.2 + - @pnpm/store.controller@1101.0.8 + +## 1101.1.1 + +### Patch Changes + +- 2061c55: Mark optional subdependency snapshots of config dependencies with `optional: true` in the env lockfile, matching how optional dependencies are recorded elsewhere in `pnpm-lock.yaml`. Previously, snapshots for the platform-specific subdeps pulled in via a config dep's `optionalDependencies` were written as empty objects, which was inconsistent with the rest of the lockfile and made it look like those non-host platform variants were required. +- e5e7b72: Don't print "Installing config dependencies..." when config dependencies are already installed and nothing needs to be fetched, re-linked, or removed. +- Updated dependencies [097983f] + - @pnpm/config.pick-registry-for-package@1100.0.5 + - @pnpm/resolving.npm-resolver@1101.3.1 + - @pnpm/installing.deps-resolver@1100.1.2 + - @pnpm/store.controller@1101.0.8 + +## 1101.1.0 + +### Minor Changes + +- c8d8fde: `configDependencies` now resolve and install one level of `optionalDependencies` declared by the config dependency, with `os`/`cpu`/`libc` platform filtering applied at install time. This unlocks the esbuild/swc-style pattern where a package ships platform-specific binaries via `optionalDependencies` — a config dependency can now do the same and have the matching binary symlinked next to it in the global virtual store, so `require('pkg-platform-arch')` from inside the config dependency resolves correctly. + + The env lockfile records all platform variants regardless of host platform, so it remains portable across machines. Each entry in a config dependency's `optionalDependencies` must declare an exact version — ranges and tags are rejected to keep installs reproducible. + +### Patch Changes + +- Updated dependencies [9cb48bb] +- Updated dependencies [3a54205] +- Updated dependencies [1627943] +- Updated dependencies [64afc92] + - @pnpm/lockfile.fs@1100.1.1 + - @pnpm/resolving.npm-resolver@1101.3.0 + - @pnpm/types@1101.1.1 + - @pnpm/installing.deps-resolver@1100.1.1 + - @pnpm/store.controller@1101.0.8 + - @pnpm/deps.graph-hasher@1100.2.1 + - @pnpm/lockfile.types@1100.0.7 + - @pnpm/lockfile.utils@1100.0.9 + - @pnpm/store.controller-types@1100.1.1 + - @pnpm/config.package-is-installable@1100.0.6 + - @pnpm/config.pick-registry-for-package@1100.0.4 + - @pnpm/config.writer@1100.0.9 + - @pnpm/core-loggers@1100.1.1 + - @pnpm/lockfile.pruner@1100.0.7 + - @pnpm/network.auth-header@1100.0.3 + - @pnpm/network.fetch@1100.0.6 + - @pnpm/pkg-manifest.reader@1100.0.4 + - @pnpm/worker@1100.1.7 + +## 1101.0.10 + +### Patch Changes + +- Updated dependencies [963861c] +- Updated dependencies [4195766] +- Updated dependencies [31538bf] +- Updated dependencies [b6e2c8c] +- Updated dependencies [6e93f35] +- Updated dependencies [3ddde2b] +- Updated dependencies [5dc8be8] +- Updated dependencies [4a79336] +- Updated dependencies [2a9bd89] + - @pnpm/resolving.npm-resolver@1101.2.0 + - @pnpm/store.controller-types@1100.1.0 + - @pnpm/installing.deps-resolver@1100.1.0 + - @pnpm/lockfile.fs@1100.1.0 + - @pnpm/deps.graph-hasher@1100.2.0 + - @pnpm/core-loggers@1100.1.0 + - @pnpm/lockfile.types@1100.0.6 + - @pnpm/lockfile.utils@1100.0.8 + - @pnpm/store.controller@1101.0.7 + - @pnpm/network.fetch@1100.0.5 + - @pnpm/lockfile.pruner@1100.0.6 + - @pnpm/worker@1100.1.6 + - @pnpm/config.writer@1100.0.8 + ## 1101.0.9 ### Patch Changes diff --git a/installing/env-installer/package.json b/installing/env-installer/package.json index f7a29f6c29..4cb62ea564 100644 --- a/installing/env-installer/package.json +++ b/installing/env-installer/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/installing.env-installer", - "version": "1101.0.9", + "version": "1101.1.3", "description": "Installer for configurational dependencies", "keywords": [ "pnpm", @@ -33,6 +33,7 @@ ".test": "cross-env NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules --disable-warning=ExperimentalWarning --disable-warning=DEP0169\" jest" }, "dependencies": { + "@pnpm/config.package-is-installable": "workspace:*", "@pnpm/config.pick-registry-for-package": "workspace:*", "@pnpm/config.writer": "workspace:*", "@pnpm/constants": "workspace:*", @@ -55,6 +56,7 @@ "@pnpm/types": "workspace:*", "@zkochan/rimraf": "catalog:", "get-npm-tarball-url": "catalog:", + "semver": "catalog:", "symlink-dir": "catalog:" }, "peerDependencies": { @@ -67,6 +69,7 @@ "@pnpm/prepare": "workspace:*", "@pnpm/registry-mock": "catalog:", "@pnpm/testing.temp-store": "workspace:*", + "@types/semver": "catalog:", "load-json-file": "catalog:", "read-yaml-file": "catalog:" }, diff --git a/installing/env-installer/src/installConfigDeps.ts b/installing/env-installer/src/installConfigDeps.ts index a83de1f7ac..5d7540e92a 100644 --- a/installing/env-installer/src/installConfigDeps.ts +++ b/installing/env-installer/src/installConfigDeps.ts @@ -1,13 +1,13 @@ import fs from 'node:fs' import path from 'node:path' +import { checkPackage } from '@pnpm/config.package-is-installable' import { pickRegistryForPackage } from '@pnpm/config.pick-registry-for-package' -import { installingConfigDepsLogger } from '@pnpm/core-loggers' -import { calcLeafGlobalVirtualStorePath } from '@pnpm/deps.graph-hasher' +import { installingConfigDepsLogger, skippedOptionalDependencyLogger } from '@pnpm/core-loggers' +import { calcGlobalVirtualStorePathWithSubdeps, calcLeafGlobalVirtualStorePath } from '@pnpm/deps.graph-hasher' import { PnpmError } from '@pnpm/error' import { readModulesDir } from '@pnpm/fs.read-modules-dir' import { type EnvLockfile, readEnvLockfile } from '@pnpm/lockfile.fs' -import { safeReadPackageJsonFromDir } from '@pnpm/pkg-manifest.reader' import type { StoreController } from '@pnpm/store.controller' import type { ConfigDependencies, Registries } from '@pnpm/types' import { rimraf } from '@zkochan/rimraf' @@ -15,7 +15,7 @@ import getNpmTarballUrl from 'get-npm-tarball-url' import { symlinkDir } from 'symlink-dir' import { migrateConfigDepsToLockfile } from './migrateConfigDeps.js' -import type { NormalizedConfigDep } from './parseIntegrity.js' +import type { NormalizedConfigDep, NormalizedSubdep } from './parseIntegrity.js' export interface InstallConfigDepsOpts { frozenLockfile?: boolean @@ -39,8 +39,17 @@ export async function installConfigDeps ( const configModulesDir = path.join(opts.rootDir, 'node_modules/.pnpm-config') const existingConfigDeps: string[] = await readModulesDir(configModulesDir) ?? [] + + let startedEmitted = false + const reportStarted = (): void => { + if (startedEmitted) return + startedEmitted = true + installingConfigDepsLogger.debug({ status: 'started' }) + } + await Promise.all(existingConfigDeps.map(async (existingConfigDep) => { if (!normalizedDeps[existingConfigDep]) { + reportStarted() await rimraf(path.join(configModulesDir, existingConfigDep)) } })) @@ -48,17 +57,28 @@ export async function installConfigDeps ( const installedConfigDeps: Array<{ name: string, version: string }> = [] await Promise.all(Object.entries(normalizedDeps).map(async ([pkgName, pkg]) => { const configDepPath = path.join(configModulesDir, pkgName) - const existingPkgJson = existingConfigDeps.includes(pkgName) - ? await safeReadPackageJsonFromDir(configDepPath) - : null - if (existingPkgJson != null && existingPkgJson.name === pkgName && existingPkgJson.version === pkg.version) { - return - } - installingConfigDepsLogger.debug({ status: 'started' }) const fullPkgId = `${pkgName}@${pkg.version}:${pkg.resolution.integrity}` - const relPath = calcLeafGlobalVirtualStorePath(fullPkgId, pkgName, pkg.version) + // The parent's GVS hash must incorporate its optional subdeps; otherwise + // changing a subdep version while keeping the parent pinned would collide + // on the same leaf and silently overwrite the previous sibling symlinks. + const optionalSubdepIds: Record = {} + for (const subdep of pkg.optionalSubdeps ?? []) { + optionalSubdepIds[subdep.name] = `${subdep.name}@${subdep.version}:${subdep.resolution.integrity}` + } + const relPath = calcGlobalVirtualStorePathWithSubdeps(fullPkgId, pkgName, pkg.version, optionalSubdepIds) const pkgDirInGlobalVirtualStore = path.join(globalVirtualStoreDir, relPath, 'node_modules', pkgName) + // The leaf hash captures parent+subdep identities from the lockfile but + // not the host's `process.arch`/`process.platform` selection. So even if + // the symlink target is already the expected leaf, the sibling links + // inside that leaf may target the wrong platform binary if the host's + // effective arch changed between runs (e.g. Rosetta x64 vs arm64 on + // macOS). Short-circuit only the parent's re-import/re-symlink in that + // case; always run installOptionalSubdeps so platform-specific siblings + // get pruned and relinked. + const parentSymlinkAlreadyCorrect = existingConfigDeps.includes(pkgName) && + await symlinkPointsTo(configDepPath, pkgDirInGlobalVirtualStore) if (!fs.existsSync(path.join(pkgDirInGlobalVirtualStore, 'package.json'))) { + reportStarted() const { fetching } = await opts.store.fetchPackage({ force: true, lockfileDir: opts.rootDir, @@ -74,6 +94,24 @@ export async function installConfigDeps ( filesResponse, }) } + if (pkg.optionalSubdeps?.length) { + await installOptionalSubdeps({ + parentName: pkgName, + parentVersion: pkg.version, + subdeps: pkg.optionalSubdeps, + // path.dirname would land in the scope subdir for scoped parents; use + // the leaf's node_modules root so sibling symlinks resolve correctly. + parentNodeModulesDir: path.join(globalVirtualStoreDir, relPath, 'node_modules'), + globalVirtualStoreDir, + rootDir: opts.rootDir, + store: opts.store, + reportStarted, + }) + } + if (parentSymlinkAlreadyCorrect) { + return + } + reportStarted() if (existingConfigDeps.includes(pkgName)) { await rimraf(configDepPath) } @@ -149,13 +187,148 @@ function normalizeFromLockfile ( ) } const registry = pickRegistryForPackage(registries, pkgName) + const snapshot = lockfile.snapshots[pkgKey] + const optionalSubdeps = snapshot?.optionalDependencies + ? readOptionalSubdepsFromLockfile(pkgName, snapshot.optionalDependencies, lockfile, registries) + : undefined deps[pkgName] = { version, resolution: { integrity: resolution.integrity, tarball: resolution.tarball ?? getNpmTarballUrl(pkgName, version, { registry }), }, + optionalSubdeps, } } return deps } + +function readOptionalSubdepsFromLockfile ( + parentName: string, + optionalDeps: Record, + lockfile: EnvLockfile, + registries: Registries +): NormalizedSubdep[] { + const subdeps: NormalizedSubdep[] = [] + for (const [subdepName, subdepVersion] of Object.entries(optionalDeps)) { + const subdepKey = `${subdepName}@${subdepVersion}` + const subdepInfo = lockfile.packages[subdepKey] + if (!subdepInfo) { + throw new PnpmError( + 'ENV_LOCKFILE_CORRUPTED', + `pnpm-lock.yaml is corrupted or incomplete: missing packages entry for "${subdepKey}" ` + + `referenced from optionalDependencies of config dependency "${parentName}"` + ) + } + const subdepResolution = subdepInfo.resolution as { integrity?: string; tarball?: string } + if (!subdepResolution.integrity) { + throw new PnpmError( + 'ENV_LOCKFILE_CORRUPTED', + `pnpm-lock.yaml is corrupted or incomplete: missing integrity for "${subdepKey}"` + ) + } + const registry = pickRegistryForPackage(registries, subdepName) + subdeps.push({ + name: subdepName, + version: subdepVersion, + resolution: { + integrity: subdepResolution.integrity, + tarball: subdepResolution.tarball ?? getNpmTarballUrl(subdepName, subdepVersion, { registry }), + }, + os: subdepInfo.os, + cpu: subdepInfo.cpu, + libc: subdepInfo.libc, + }) + } + return subdeps +} + +interface InstallOptionalSubdepsOpts { + parentName: string + parentVersion: string + subdeps: NormalizedSubdep[] + parentNodeModulesDir: string + globalVirtualStoreDir: string + rootDir: string + store: StoreController + reportStarted: () => void +} + +async function installOptionalSubdeps (opts: InstallOptionalSubdepsOpts): Promise { + const parentLogInfo = { id: `${opts.parentName}@${opts.parentVersion}`, name: opts.parentName, version: opts.parentVersion } + const compatibleSubdeps = opts.subdeps.filter((subdep) => { + if (!subdep.os && !subdep.cpu && !subdep.libc) return true + // Use checkPackage rather than packageIsInstallable: the latter emits a + // user-visible warn for every incompatible variant, which would fire on + // every install since the env lockfile records all platform variants for + // portability. We log skipped subdeps at debug instead. + const error = checkPackage( + `${subdep.name}@${subdep.version}`, + { os: subdep.os, cpu: subdep.cpu, libc: subdep.libc }, + {} + ) + if (error == null) return true + skippedOptionalDependencyLogger.debug({ + details: error.toString(), + package: { id: `${subdep.name}@${subdep.version}`, name: subdep.name, version: subdep.version }, + parents: [parentLogInfo], + prefix: opts.rootDir, + reason: error.code === 'ERR_PNPM_UNSUPPORTED_ENGINE' ? 'unsupported_engine' : 'unsupported_platform', + }) + return false + }) + + const expectedSiblings = new Set([opts.parentName, ...compatibleSubdeps.map((s) => s.name)]) + const existingSiblings = await readModulesDir(opts.parentNodeModulesDir) ?? [] + const orphanSiblings = existingSiblings.filter((name) => !expectedSiblings.has(name)) + if (orphanSiblings.length > 0) { + opts.reportStarted() + } + await Promise.all(orphanSiblings.map((name) => rimraf(path.join(opts.parentNodeModulesDir, name)))) + + await Promise.all(compatibleSubdeps.map(async (subdep) => { + const subdepFullPkgId = `${subdep.name}@${subdep.version}:${subdep.resolution.integrity}` + const subdepRelPath = calcLeafGlobalVirtualStorePath(subdepFullPkgId, subdep.name, subdep.version) + const subdepDirInGlobalVirtualStore = path.join(opts.globalVirtualStoreDir, subdepRelPath, 'node_modules', subdep.name) + if (!fs.existsSync(path.join(subdepDirInGlobalVirtualStore, 'package.json'))) { + opts.reportStarted() + const { fetching } = await opts.store.fetchPackage({ + force: true, + lockfileDir: opts.rootDir, + pkg: { + id: `${subdep.name}@${subdep.version}`, + resolution: subdep.resolution, + }, + }) + const { files: filesResponse } = await fetching() + await opts.store.importPackage(subdepDirInGlobalVirtualStore, { + force: true, + requiresBuild: false, + filesResponse, + }) + } + const linkPath = path.join(opts.parentNodeModulesDir, subdep.name) + if (await symlinkPointsTo(linkPath, subdepDirInGlobalVirtualStore)) { + return + } + opts.reportStarted() + await fs.promises.mkdir(path.dirname(linkPath), { recursive: true }) + await symlinkDir(subdepDirInGlobalVirtualStore, linkPath) + })) +} + +async function symlinkPointsTo (linkPath: string, expectedTarget: string): Promise { + try { + // Realpath both sides: the expected target itself may live under a + // symlinked storeDir, and on case-insensitive filesystems the literal + // string forms can disagree about casing even when they refer to the + // same inode. + const [linkReal, targetReal] = await Promise.all([ + fs.promises.realpath(linkPath), + fs.promises.realpath(expectedTarget), + ]) + return linkReal === targetReal + } catch { + return false + } +} diff --git a/installing/env-installer/src/parseIntegrity.ts b/installing/env-installer/src/parseIntegrity.ts index 7ffa970938..78e0be64b2 100644 --- a/installing/env-installer/src/parseIntegrity.ts +++ b/installing/env-installer/src/parseIntegrity.ts @@ -6,6 +6,19 @@ export interface NormalizedConfigDep { integrity: string tarball: string } + optionalSubdeps?: NormalizedSubdep[] +} + +export interface NormalizedSubdep { + name: string + version: string + resolution: { + integrity: string + tarball: string + } + os?: string[] + cpu?: string[] + libc?: string[] } export function parseIntegrity (pkgName: string, pkgSpec: string): { version: string, integrity: string } { diff --git a/installing/env-installer/src/resolveAndInstallConfigDeps.ts b/installing/env-installer/src/resolveAndInstallConfigDeps.ts index db5dd7a950..a8601b9da1 100644 --- a/installing/env-installer/src/resolveAndInstallConfigDeps.ts +++ b/installing/env-installer/src/resolveAndInstallConfigDeps.ts @@ -16,6 +16,7 @@ import getNpmTarballUrl from 'get-npm-tarball-url' import { installConfigDeps, type InstallConfigDepsOpts } from './installConfigDeps.js' import { parseIntegrity } from './parseIntegrity.js' import { pruneEnvLockfile } from './pruneEnvLockfile.js' +import { resolveOptionalSubdeps } from './resolveOptionalSubdeps.js' export type ResolveAndInstallConfigDepsOpts = CreateFetchFromRegistryOptions & ResolverFactoryOptions & InstallConfigDepsOpts & { rootDir: string @@ -99,7 +100,7 @@ export async function resolveAndInstallConfigDeps ( // Resolve missing deps const fetch = createFetchFromRegistry(opts) - const getAuthHeader = createGetAuthHeaderByURI(opts.configByUri ?? {}, opts.registries?.default) + const getAuthHeader = createGetAuthHeaderByURI(opts.configByUri ?? {}) const { resolveFromNpm } = createNpmResolver(fetch, getAuthHeader, opts) await Promise.all(depsToResolve.map(async ({ name, specifier }) => { @@ -131,7 +132,13 @@ export async function resolveAndInstallConfigDeps ( registry ), } - envLockfile.snapshots[pkgKey] = {} + const optionalSubdeps = await resolveOptionalSubdeps(name, resolution.manifest, { + envLockfile, + lockfileDir: opts.rootDir, + registries: opts.registries, + resolveFromNpm, + }) + envLockfile.snapshots[pkgKey] = optionalSubdeps ? { optionalDependencies: optionalSubdeps } : {} })) pruneEnvLockfile(envLockfile) diff --git a/installing/env-installer/src/resolveConfigDeps.ts b/installing/env-installer/src/resolveConfigDeps.ts index cb174c0ada..a800fd5658 100644 --- a/installing/env-installer/src/resolveConfigDeps.ts +++ b/installing/env-installer/src/resolveConfigDeps.ts @@ -15,6 +15,8 @@ import { parseWantedDependency } from '@pnpm/resolving.parse-wanted-dependency' import type { ConfigDependencies, ConfigDependencySpecifiers, RegistryConfig } from '@pnpm/types' import { installConfigDeps, type InstallConfigDepsOpts } from './installConfigDeps.js' +import { pruneEnvLockfile } from './pruneEnvLockfile.js' +import { resolveOptionalSubdeps } from './resolveOptionalSubdeps.js' export type ResolveConfigDepsOpts = CreateFetchFromRegistryOptions & ResolverFactoryOptions & InstallConfigDepsOpts & { configDependencies?: ConfigDependencies @@ -28,7 +30,7 @@ export async function resolveConfigDeps (configDeps: string[], opts: ResolveConf } const fetch = createFetchFromRegistry(opts) - const getAuthHeader = createGetAuthHeaderByURI(opts.configByUri ?? {}, opts.registries?.default) + const getAuthHeader = createGetAuthHeaderByURI(opts.configByUri ?? {}) const { resolveFromNpm } = createNpmResolver(fetch, getAuthHeader, opts) // Extract existing specifiers from configDependencies (handles both old and new formats) @@ -68,9 +70,17 @@ export async function resolveConfigDeps (configDeps: string[], opts: ResolveConf registry ), } - envLockfile.snapshots[pkgKey] = {} + const optionalSubdeps = await resolveOptionalSubdeps(pkgName, resolution.manifest, { + envLockfile, + lockfileDir: opts.rootDir, + registries: opts.registries, + resolveFromNpm, + }) + envLockfile.snapshots[pkgKey] = optionalSubdeps ? { optionalDependencies: optionalSubdeps } : {} })) + pruneEnvLockfile(envLockfile) + await Promise.all([ writeSettings({ ...opts, diff --git a/installing/env-installer/src/resolveOptionalSubdeps.ts b/installing/env-installer/src/resolveOptionalSubdeps.ts new file mode 100644 index 0000000000..d1dacdef03 --- /dev/null +++ b/installing/env-installer/src/resolveOptionalSubdeps.ts @@ -0,0 +1,114 @@ +import util from 'node:util' + +import { pickRegistryForPackage } from '@pnpm/config.pick-registry-for-package' +import { skippedOptionalDependencyLogger } from '@pnpm/core-loggers' +import { PnpmError } from '@pnpm/error' +import type { EnvLockfile } from '@pnpm/lockfile.fs' +import type { ResolvedDependencies } from '@pnpm/lockfile.types' +import { toLockfileResolution } from '@pnpm/lockfile.utils' +import type { createNpmResolver } from '@pnpm/resolving.npm-resolver' +import type { DependencyManifest, Registries } from '@pnpm/types' +import semver from 'semver' + +type ResolveFromNpm = ReturnType['resolveFromNpm'] + +export interface ResolveOptionalSubdepsOpts { + envLockfile: EnvLockfile + lockfileDir: string + registries: Registries + resolveFromNpm: ResolveFromNpm +} + +export async function resolveOptionalSubdeps ( + parentName: string, + parentManifest: DependencyManifest, + opts: ResolveOptionalSubdepsOpts +): Promise { + const optionalDeps = parentManifest.optionalDependencies + if (!optionalDeps || Object.keys(optionalDeps).length === 0) { + return undefined + } + + const resolved: ResolvedDependencies = {} + await Promise.all(Object.entries(optionalDeps).map(async ([subdepName, subdepSpec]) => { + if (semver.valid(subdepSpec) == null) { + // Ranges and tags would let the resolved version drift between machines + // even with a stable parent integrity, breaking the lockfile's promise + // of reproducible config-dep installs. + throw new PnpmError( + 'CONFIG_DEP_OPTIONAL_NOT_EXACT', + `Cannot install "${subdepName}@${subdepSpec}" as an optionalDependency of config dependency "${parentName}": only exact versions are supported (got "${subdepSpec}")` + ) + } + let resolution + try { + // `optional: true` opts into full registry metadata so the resolver + // returns `libc` (and any other fields the abbreviated metadata strips). + // See pnpm/pnpm#9950. + resolution = await opts.resolveFromNpm({ alias: subdepName, bareSpecifier: subdepSpec, optional: true }, { + lockfileDir: opts.lockfileDir, + preferredVersions: {}, + projectDir: opts.lockfileDir, + }) + } catch (err: unknown) { + // Trust-downgrade is a security signal that must fail the install even + // for optional deps; everything else mirrors npm's optionalDependencies + // semantics — log and skip. + if (util.types.isNativeError(err) && 'code' in err && err.code === 'ERR_PNPM_TRUST_DOWNGRADE') { + throw err + } + skippedOptionalDependencyLogger.debug({ + details: util.types.isNativeError(err) ? err.toString() : String(err), + package: { + name: subdepName, + // No resolved version yet; surface the requested specifier so log + // consumers that format `${name}@${version}` don't render `@undefined`. + version: subdepSpec, + bareSpecifier: subdepSpec, + }, + parents: [{ id: `${parentName}@${parentManifest.version}`, name: parentName, version: parentManifest.version }], + prefix: opts.lockfileDir, + reason: 'resolution_failure', + }) + return + } + if ( + resolution?.resolution == null || + !('integrity' in resolution.resolution) || + typeof resolution.resolution.integrity !== 'string' || + !resolution.resolution.integrity || + resolution.manifest == null + ) { + throw new PnpmError( + 'BAD_CONFIG_DEP', + `Cannot resolve optionalDependency "${subdepName}" of config dependency "${parentName}" because it has no integrity` + ) + } + const subdepVersion = resolution.manifest.version + const registry = pickRegistryForPackage(opts.registries, subdepName) + const subdepKey = `${subdepName}@${subdepVersion}` + + opts.envLockfile.packages[subdepKey] = { + resolution: toLockfileResolution( + { name: subdepName, version: subdepVersion }, + resolution.resolution, + registry + ), + ...pickPlatformFields(resolution.manifest), + } + if (opts.envLockfile.snapshots[subdepKey] == null) { + opts.envLockfile.snapshots[subdepKey] = { optional: true } + } + resolved[subdepName] = subdepVersion + })) + + return Object.keys(resolved).length > 0 ? resolved : undefined +} + +function pickPlatformFields (manifest: DependencyManifest): { os?: string[], cpu?: string[], libc?: string[] } { + const out: { os?: string[], cpu?: string[], libc?: string[] } = {} + if (manifest.os?.length) out.os = manifest.os + if (manifest.cpu?.length) out.cpu = manifest.cpu + if (manifest.libc?.length) out.libc = manifest.libc + return out +} diff --git a/installing/env-installer/test/installConfigDeps.ts b/installing/env-installer/test/installConfigDeps.ts index 28c0bf6c83..ee9708e41e 100644 --- a/installing/env-installer/test/installConfigDeps.ts +++ b/installing/env-installer/test/installConfigDeps.ts @@ -1,4 +1,6 @@ import fs from 'node:fs' +import { createRequire } from 'node:module' +import path from 'node:path' import { expect, test } from '@jest/globals' import { installConfigDeps } from '@pnpm/installing.env-installer' @@ -6,6 +8,7 @@ import { createEnvLockfile, type EnvLockfile, readEnvLockfile } from '@pnpm/lock import { prepareEmpty } from '@pnpm/prepare' import { getIntegrity, REGISTRY_MOCK_PORT } from '@pnpm/registry-mock' import { createTempStore } from '@pnpm/testing.temp-store' +import { rimraf } from '@zkochan/rimraf' import { loadJsonFileSync } from 'load-json-file' const registry = `http://localhost:${REGISTRY_MOCK_PORT}/` @@ -80,6 +83,200 @@ test('configuration dependency is installed from env lockfile', async () => { expect(fs.existsSync('node_modules/.pnpm-config/@pnpm.e2e/foo/package.json')).toBeFalsy() }) +test('optional subdep matching the current platform is installed and symlinked next to parent', async () => { + prepareEmpty() + const { storeController, storeDir } = createTempStore() + + const parentName = '@pnpm.e2e/foo' + const parentVersion = '100.0.0' + const subdepName = '@pnpm.e2e/bar' + const subdepVersion = '100.0.0' + + const lockfile = createEnvLockfile() + const parentKey = `${parentName}@${parentVersion}` + lockfile.importers['.'].configDependencies[parentName] = { specifier: parentVersion, version: parentVersion } + lockfile.packages[parentKey] = { resolution: { integrity: getIntegrity(parentName, parentVersion) } } + lockfile.snapshots[parentKey] = { + optionalDependencies: { [subdepName]: subdepVersion }, + } + lockfile.packages[`${subdepName}@${subdepVersion}`] = { + resolution: { integrity: getIntegrity(subdepName, subdepVersion) }, + os: [process.platform], + cpu: [process.arch], + } + + await installConfigDeps(lockfile, { + registries: { + default: registry, + }, + rootDir: process.cwd(), + store: storeController, + storeDir, + }) + + expect(fs.existsSync(`node_modules/.pnpm-config/${parentName}/package.json`)).toBe(true) + + // Node-style resolution from inside the parent must find the sibling subdep. + const parentRealPath = fs.realpathSync(`node_modules/.pnpm-config/${parentName}`) + const requireFromParent = createRequire(path.join(parentRealPath, 'package.json')) + const siblingPkgJsonPath = requireFromParent.resolve(`${subdepName}/package.json`) + const siblingManifest = loadJsonFileSync<{ name: string, version: string }>(siblingPkgJsonPath) + expect(siblingManifest.name).toBe(subdepName) + expect(siblingManifest.version).toBe(subdepVersion) +}) + +test('changing only an optional subdep version re-installs and re-symlinks the parent', async () => { + prepareEmpty() + const { storeController, storeDir } = createTempStore() + + const parentName = '@pnpm.e2e/foo' + const parentVersion = '100.0.0' + const subdepName = '@pnpm.e2e/bar' + + function buildLockfile (subdepVersion: string): EnvLockfile { + const lockfile = createEnvLockfile() + const parentKey = `${parentName}@${parentVersion}` + lockfile.importers['.'].configDependencies[parentName] = { specifier: parentVersion, version: parentVersion } + lockfile.packages[parentKey] = { resolution: { integrity: getIntegrity(parentName, parentVersion) } } + lockfile.snapshots[parentKey] = { optionalDependencies: { [subdepName]: subdepVersion } } + lockfile.packages[`${subdepName}@${subdepVersion}`] = { + resolution: { integrity: getIntegrity(subdepName, subdepVersion) }, + os: [process.platform], + cpu: [process.arch], + } + return lockfile + } + + const installOpts = { + registries: { default: registry }, + rootDir: process.cwd(), + store: storeController, + storeDir, + } + + await installConfigDeps(buildLockfile('100.0.0'), installOpts) + const requireBefore = createRequire(path.join(fs.realpathSync(`node_modules/.pnpm-config/${parentName}`), 'package.json')) + expect(loadJsonFileSync<{ version: string }>(requireBefore.resolve(`${subdepName}/package.json`)).version).toBe('100.0.0') + + await installConfigDeps(buildLockfile('100.1.0'), installOpts) + const requireAfter = createRequire(path.join(fs.realpathSync(`node_modules/.pnpm-config/${parentName}`), 'package.json')) + expect(loadJsonFileSync<{ version: string }>(requireAfter.resolve(`${subdepName}/package.json`)).version).toBe('100.1.0') +}) + +test('optional subdep that does not match the current platform is skipped', async () => { + prepareEmpty() + const { storeController, storeDir } = createTempStore() + + const parentName = '@pnpm.e2e/foo' + const parentVersion = '100.0.0' + const subdepName = '@pnpm.e2e/bar' + const subdepVersion = '100.0.0' + + const lockfile = createEnvLockfile() + const parentKey = `${parentName}@${parentVersion}` + lockfile.importers['.'].configDependencies[parentName] = { specifier: parentVersion, version: parentVersion } + lockfile.packages[parentKey] = { resolution: { integrity: getIntegrity(parentName, parentVersion) } } + lockfile.snapshots[parentKey] = { + optionalDependencies: { [subdepName]: subdepVersion }, + } + lockfile.packages[`${subdepName}@${subdepVersion}`] = { + resolution: { integrity: getIntegrity(subdepName, subdepVersion) }, + os: ['this-os-does-not-exist'], + } + + await installConfigDeps(lockfile, { + registries: { + default: registry, + }, + rootDir: process.cwd(), + store: storeController, + storeDir, + }) + + const parentRealPath = fs.realpathSync(`node_modules/.pnpm-config/${parentName}`) + const requireFromParent = createRequire(path.join(parentRealPath, 'package.json')) + expect(() => requireFromParent.resolve(`${subdepName}/package.json`)).toThrow(/Cannot find/) +}) + +test('re-installs sibling symlinks even when the parent symlink is already correct', async () => { + prepareEmpty() + const { storeController, storeDir } = createTempStore() + + const parentName = '@pnpm.e2e/foo' + const parentVersion = '100.0.0' + const subdepName = '@pnpm.e2e/bar' + const subdepVersion = '100.0.0' + + const lockfile = createEnvLockfile() + const parentKey = `${parentName}@${parentVersion}` + lockfile.importers['.'].configDependencies[parentName] = { specifier: parentVersion, version: parentVersion } + lockfile.packages[parentKey] = { resolution: { integrity: getIntegrity(parentName, parentVersion) } } + lockfile.snapshots[parentKey] = { optionalDependencies: { [subdepName]: subdepVersion } } + lockfile.packages[`${subdepName}@${subdepVersion}`] = { + resolution: { integrity: getIntegrity(subdepName, subdepVersion) }, + os: [process.platform], + cpu: [process.arch], + } + + const installOpts = { + registries: { default: registry }, + rootDir: process.cwd(), + store: storeController, + storeDir, + } + + // First install — parent + subdep symlink land in the GVS leaf. + await installConfigDeps(lockfile, installOpts) + const parentRealPath = fs.realpathSync(`node_modules/.pnpm-config/${parentName}`) + const subdepSiblingPath = path.join(path.dirname(path.dirname(parentRealPath)), subdepName) + expect(fs.existsSync(`${subdepSiblingPath}/package.json`)).toBe(true) + + // Simulate stale state: remove the subdep sibling symlink. The parent's + // .pnpm-config symlink still points at the expected leaf, so the realpath + // skip-check passes. installOptionalSubdeps must still run to repair. + await rimraf(subdepSiblingPath) + expect(fs.existsSync(subdepSiblingPath)).toBe(false) + + // Second install with the same lockfile. + await installConfigDeps(lockfile, installOpts) + expect(fs.existsSync(`${subdepSiblingPath}/package.json`)).toBe(true) +}) + +test('optional subdep that does not match the current cpu is skipped', async () => { + prepareEmpty() + const { storeController, storeDir } = createTempStore() + + const parentName = '@pnpm.e2e/foo' + const parentVersion = '100.0.0' + const subdepName = '@pnpm.e2e/bar' + const subdepVersion = '100.0.0' + + const lockfile = createEnvLockfile() + const parentKey = `${parentName}@${parentVersion}` + lockfile.importers['.'].configDependencies[parentName] = { specifier: parentVersion, version: parentVersion } + lockfile.packages[parentKey] = { resolution: { integrity: getIntegrity(parentName, parentVersion) } } + lockfile.snapshots[parentKey] = { + optionalDependencies: { [subdepName]: subdepVersion }, + } + lockfile.packages[`${subdepName}@${subdepVersion}`] = { + resolution: { integrity: getIntegrity(subdepName, subdepVersion) }, + cpu: ['this-cpu-does-not-exist'], + } + + await installConfigDeps(lockfile, { + registries: { + default: registry, + }, + rootDir: process.cwd(), + store: storeController, + storeDir, + }) + + const parentRealPath = fs.realpathSync(`node_modules/.pnpm-config/${parentName}`) + const requireFromParent = createRequire(path.join(parentRealPath, 'package.json')) + expect(() => requireFromParent.resolve(`${subdepName}/package.json`)).toThrow(/Cannot find/) +}) + test('installation fails if the checksum of the config dependency is invalid', async () => { prepareEmpty() const { storeController, storeDir } = createTempStore({ diff --git a/installing/env-installer/test/resolveAndInstallConfigDeps.test.ts b/installing/env-installer/test/resolveAndInstallConfigDeps.test.ts index c8c56920d4..019744abff 100644 --- a/installing/env-installer/test/resolveAndInstallConfigDeps.test.ts +++ b/installing/env-installer/test/resolveAndInstallConfigDeps.test.ts @@ -1,8 +1,9 @@ import path from 'node:path' -import { expect, test } from '@jest/globals' +import { afterAll, expect, test } from '@jest/globals' import { resolveAndInstallConfigDeps } from '@pnpm/installing.env-installer' import { createEnvLockfile, readEnvLockfile, writeEnvLockfile } from '@pnpm/lockfile.fs' +import { type LogBase, streamParser } from '@pnpm/logger' import { prepareEmpty } from '@pnpm/prepare' import { getIntegrity, REGISTRY_MOCK_PORT } from '@pnpm/registry-mock' import { createTempStore } from '@pnpm/testing.temp-store' @@ -22,6 +23,28 @@ function createOpts () { } } +interface InstallingConfigDepsEvent { status: string, deps?: Array<{ name: string, version: string }> } + +// `streamParser` is a `split2` Transform stream that buffers writes until the +// first 'data' listener attaches, then drains the whole buffer into it. +// Subscribing per-test would therefore replay events from earlier tests into +// the current test's listener. Subscribe once at module load and let each test +// take only the events accumulated since its last drain. +const accumulatedConfigDepEvents: InstallingConfigDepsEvent[] = [] +const configDepsListener = (msg: LogBase): void => { + const log = msg as { name?: string, status?: string, deps?: Array<{ name: string, version: string }> } + if (log.name !== 'pnpm:installing-config-deps' || log.status == null) return + accumulatedConfigDepEvents.push({ status: log.status, deps: log.deps }) +} +streamParser.on('data', configDepsListener) +afterAll(() => { + streamParser.removeListener('data', configDepsListener) +}) + +function takeConfigDepEvents (): InstallingConfigDepsEvent[] { + return accumulatedConfigDepEvents.splice(0, accumulatedConfigDepEvents.length) +} + test('resolves and installs config dep when no env lockfile exists', async () => { prepareEmpty() const opts = createOpts() @@ -222,6 +245,29 @@ test('fails with frozenLockfile when new-format deps need resolution', async () }, { ...opts, frozenLockfile: true })).rejects.toThrow('Cannot update configDependencies with "frozen-lockfile"') }) +test('emits installing-config-deps events only when work is needed', async () => { + prepareEmpty() + const opts = createOpts() + + takeConfigDepEvents() + await resolveAndInstallConfigDeps({ + '@pnpm.e2e/foo': '100.0.0', + }, opts) + const firstRunEvents = takeConfigDepEvents() + + expect(firstRunEvents.map(e => e.status)).toEqual(['started', 'done']) + expect(firstRunEvents.find(e => e.status === 'done')?.deps).toEqual([ + { name: '@pnpm.e2e/foo', version: '100.0.0' }, + ]) + + await resolveAndInstallConfigDeps({ + '@pnpm.e2e/foo': '100.0.0', + }, opts) + const secondRunEvents = takeConfigDepEvents() + + expect(secondRunEvents).toStrictEqual([]) +}) + test('succeeds with frozenLockfile when env lockfile is up-to-date', async () => { prepareEmpty() const opts = createOpts() diff --git a/installing/env-installer/test/resolveConfigDeps.test.ts b/installing/env-installer/test/resolveConfigDeps.test.ts index 7be11b8509..9165398e62 100644 --- a/installing/env-installer/test/resolveConfigDeps.test.ts +++ b/installing/env-installer/test/resolveConfigDeps.test.ts @@ -2,7 +2,7 @@ import path from 'node:path' import { expect, test } from '@jest/globals' import { resolveConfigDeps } from '@pnpm/installing.env-installer' -import { readEnvLockfile } from '@pnpm/lockfile.fs' +import { readEnvLockfile, writeEnvLockfile } from '@pnpm/lockfile.fs' import { prepareEmpty } from '@pnpm/prepare' import { getIntegrity, REGISTRY_MOCK_PORT } from '@pnpm/registry-mock' import { createTempStore } from '@pnpm/testing.temp-store' @@ -45,6 +45,141 @@ test('configuration dependency is resolved', async () => { expect(envLockfile!.snapshots['@pnpm.e2e/foo@100.0.0']).toStrictEqual({}) }) +test('one level of optionalDependencies is recorded in the env lockfile with platform fields', async () => { + prepareEmpty() + const { storeController, storeDir } = createTempStore() + + await resolveConfigDeps(['@pnpm.e2e/support-different-architectures@1.0.0'], { + registries: { + default: registry, + }, + rootDir: process.cwd(), + cacheDir: path.resolve('cache'), + store: storeController, + storeDir, + }) + + const envLockfile = await readEnvLockfile(process.cwd()) + expect(envLockfile).not.toBeNull() + + const parentKey = '@pnpm.e2e/support-different-architectures@1.0.0' + expect(envLockfile!.snapshots[parentKey]).toStrictEqual({ + optionalDependencies: { + '@pnpm.e2e/only-darwin-arm64': '1.0.0', + '@pnpm.e2e/only-darwin-x64': '1.0.0', + '@pnpm.e2e/only-linux-arm64-glibc': '1.0.0', + '@pnpm.e2e/only-linux-arm64-musl': '1.0.0', + '@pnpm.e2e/only-linux-x64-glibc': '1.0.0', + '@pnpm.e2e/only-linux-x64-musl': '1.0.0', + '@pnpm.e2e/only-win32-arm64': '1.0.0', + '@pnpm.e2e/only-win32-x64': '1.0.0', + }, + }) + + // Each optional subdep is in `packages` with its os/cpu fields preserved for + // install-time platform filtering, and gets an `optional: true` snapshot + // to match how optional packages are recorded elsewhere in the lockfile. + expect(envLockfile!.packages['@pnpm.e2e/only-darwin-arm64@1.0.0']).toStrictEqual({ + resolution: { + integrity: getIntegrity('@pnpm.e2e/only-darwin-arm64', '1.0.0'), + }, + os: ['darwin'], + cpu: ['arm64'], + }) + expect(envLockfile!.snapshots['@pnpm.e2e/only-darwin-arm64@1.0.0']).toStrictEqual({ optional: true }) + // libc is preserved alongside os/cpu for musl/glibc variants. + expect(envLockfile!.packages['@pnpm.e2e/only-linux-x64-musl@1.0.0']).toStrictEqual({ + resolution: { + integrity: getIntegrity('@pnpm.e2e/only-linux-x64-musl', '1.0.0'), + }, + os: ['linux'], + cpu: ['x64'], + libc: ['musl'], + }) + + // The parent config dep itself is still registered as the only top-level config dep. + expect(envLockfile!.importers['.'].configDependencies).toStrictEqual({ + '@pnpm.e2e/support-different-architectures': { + specifier: '1.0.0', + version: '1.0.0', + }, + }) +}) + +test('config dep with no optionalDependencies keeps an empty snapshot', async () => { + prepareEmpty() + const { storeController, storeDir } = createTempStore() + + await resolveConfigDeps(['@pnpm.e2e/foo@100.0.0'], { + registries: { + default: registry, + }, + rootDir: process.cwd(), + cacheDir: path.resolve('cache'), + store: storeController, + storeDir, + }) + + const envLockfile = await readEnvLockfile(process.cwd()) + expect(envLockfile!.snapshots['@pnpm.e2e/foo@100.0.0']).toStrictEqual({}) +}) + +test('rejects an optionalDependency declared with a non-exact version', async () => { + prepareEmpty() + const { storeController, storeDir } = createTempStore() + + // @pnpm.e2e/foobar declares `@pnpm.e2e/bar: "^100.0.0"` — a range, not an exact version. + await expect(resolveConfigDeps(['@pnpm.e2e/foobar@100.0.0'], { + registries: { + default: registry, + }, + rootDir: process.cwd(), + cacheDir: path.resolve('cache'), + store: storeController, + storeDir, + })).rejects.toThrow(/only exact versions are supported/) +}) + +test('orphan optional subdeps from a previous resolution are pruned', async () => { + prepareEmpty() + const { storeController, storeDir } = createTempStore() + + // Simulate a prior resolution that left optional subdeps for a now-removed + // version of a config dependency. The stale `foo@99.0.0` and its optional + // subdep `bar@1.0.0` are not referenced from any current configDependency. + await writeEnvLockfile(process.cwd(), { + lockfileVersion: '9.0', + importers: { + '.': { configDependencies: {} }, + }, + packages: { + '@pnpm.e2e/foo@99.0.0': { resolution: { integrity: 'sha512-stale==' } }, + '@pnpm.e2e/bar@1.0.0': { resolution: { integrity: 'sha512-stale==' } }, + }, + snapshots: { + '@pnpm.e2e/foo@99.0.0': { optionalDependencies: { '@pnpm.e2e/bar': '1.0.0' } }, + '@pnpm.e2e/bar@1.0.0': { optional: true }, + }, + }) + + await resolveConfigDeps(['@pnpm.e2e/foo@100.0.0'], { + registries: { + default: registry, + }, + rootDir: process.cwd(), + cacheDir: path.resolve('cache'), + store: storeController, + storeDir, + }) + + const envLockfile = await readEnvLockfile(process.cwd()) + expect(envLockfile!.packages['@pnpm.e2e/foo@99.0.0']).toBeUndefined() + expect(envLockfile!.packages['@pnpm.e2e/bar@1.0.0']).toBeUndefined() + expect(envLockfile!.snapshots['@pnpm.e2e/foo@99.0.0']).toBeUndefined() + expect(envLockfile!.snapshots['@pnpm.e2e/bar@1.0.0']).toBeUndefined() + expect(envLockfile!.packages['@pnpm.e2e/foo@100.0.0']).toBeDefined() +}) + test('fails with frozenLockfile', async () => { prepareEmpty() const { storeController, storeDir } = createTempStore() diff --git a/installing/env-installer/tsconfig.json b/installing/env-installer/tsconfig.json index 3c641d4bf5..e8d280f50e 100644 --- a/installing/env-installer/tsconfig.json +++ b/installing/env-installer/tsconfig.json @@ -12,6 +12,9 @@ { "path": "../../__utils__/prepare" }, + { + "path": "../../config/package-is-installable" + }, { "path": "../../config/pick-registry-for-package" }, diff --git a/installing/linking/direct-dep-linker/CHANGELOG.md b/installing/linking/direct-dep-linker/CHANGELOG.md index c9c9dcbbb7..c8fe227245 100644 --- a/installing/linking/direct-dep-linker/CHANGELOG.md +++ b/installing/linking/direct-dep-linker/CHANGELOG.md @@ -1,5 +1,28 @@ # @pnpm/pkg-manager.direct-dep-linker +## 1100.0.6 + +### Patch Changes + +- Updated dependencies [ad84fff] + - @pnpm/fs.symlink-dependency@1100.0.6 + - @pnpm/core-loggers@1100.1.2 + +## 1100.0.5 + +### Patch Changes + +- @pnpm/core-loggers@1100.1.1 +- @pnpm/fs.symlink-dependency@1100.0.5 + +## 1100.0.4 + +### Patch Changes + +- Updated dependencies [4a79336] + - @pnpm/core-loggers@1100.1.0 + - @pnpm/fs.symlink-dependency@1100.0.4 + ## 1100.0.3 ### Patch Changes diff --git a/installing/linking/direct-dep-linker/package.json b/installing/linking/direct-dep-linker/package.json index 9f26529645..9bc54af431 100644 --- a/installing/linking/direct-dep-linker/package.json +++ b/installing/linking/direct-dep-linker/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/installing.linking.direct-dep-linker", - "version": "1100.0.3", + "version": "1100.0.6", "description": "Fast installation using only pnpm-lock.yaml", "keywords": [ "pnpm", diff --git a/installing/linking/hoist/CHANGELOG.md b/installing/linking/hoist/CHANGELOG.md index b50dc090ce..e405525d11 100644 --- a/installing/linking/hoist/CHANGELOG.md +++ b/installing/linking/hoist/CHANGELOG.md @@ -1,5 +1,37 @@ # @pnpm/hoist +## 1100.0.10 + +### Patch Changes + +- Updated dependencies [35d2355] + - @pnpm/types@1101.2.0 + - @pnpm/bins.linker@1100.0.10 + - @pnpm/core-loggers@1100.1.2 + +## 1100.0.9 + +### Patch Changes + +- @pnpm/bins.linker@1100.0.9 + +## 1100.0.8 + +### Patch Changes + +- Updated dependencies [64afc92] + - @pnpm/types@1101.1.1 + - @pnpm/bins.linker@1100.0.8 + - @pnpm/core-loggers@1100.1.1 + +## 1100.0.7 + +### Patch Changes + +- Updated dependencies [4a79336] + - @pnpm/core-loggers@1100.1.0 + - @pnpm/bins.linker@1100.0.7 + ## 1100.0.6 ### Patch Changes diff --git a/installing/linking/hoist/package.json b/installing/linking/hoist/package.json index 8722223413..68d3165acb 100644 --- a/installing/linking/hoist/package.json +++ b/installing/linking/hoist/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/installing.linking.hoist", - "version": "1100.0.6", + "version": "1100.0.10", "description": "Hoists dependencies in a node_modules created by pnpm", "keywords": [ "pnpm", diff --git a/installing/linking/modules-cleaner/CHANGELOG.md b/installing/linking/modules-cleaner/CHANGELOG.md index 29286916d5..54be62f86c 100644 --- a/installing/linking/modules-cleaner/CHANGELOG.md +++ b/installing/linking/modules-cleaner/CHANGELOG.md @@ -1,5 +1,47 @@ # @pnpm/modules-cleaner +## 1100.1.4 + +### Patch Changes + +- Updated dependencies [e55f4b5] +- Updated dependencies [35d2355] + - @pnpm/lockfile.utils@1100.0.10 + - @pnpm/types@1101.2.0 + - @pnpm/lockfile.filtering@1100.1.3 + - @pnpm/bins.remover@1100.0.6 + - @pnpm/core-loggers@1100.1.2 + - @pnpm/deps.path@1100.0.5 + - @pnpm/lockfile.types@1100.0.8 + - @pnpm/store.controller-types@1100.1.2 + +## 1100.1.3 + +### Patch Changes + +- Updated dependencies [64afc92] + - @pnpm/types@1101.1.1 + - @pnpm/lockfile.types@1100.0.7 + - @pnpm/lockfile.utils@1100.0.9 + - @pnpm/store.controller-types@1100.1.1 + - @pnpm/bins.remover@1100.0.5 + - @pnpm/core-loggers@1100.1.1 + - @pnpm/deps.path@1100.0.4 + - @pnpm/lockfile.filtering@1100.1.2 + +## 1100.1.2 + +### Patch Changes + +- Updated dependencies [4195766] +- Updated dependencies [4a79336] + - @pnpm/store.controller-types@1100.1.0 + - @pnpm/core-loggers@1100.1.0 + - @pnpm/lockfile.types@1100.0.6 + - @pnpm/lockfile.utils@1100.0.8 + - @pnpm/bins.remover@1100.0.4 + - @pnpm/lockfile.filtering@1100.1.1 + ## 1100.1.1 ### Patch Changes diff --git a/installing/linking/modules-cleaner/package.json b/installing/linking/modules-cleaner/package.json index b77ae55012..c1a5d257aa 100644 --- a/installing/linking/modules-cleaner/package.json +++ b/installing/linking/modules-cleaner/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/installing.linking.modules-cleaner", - "version": "1100.1.1", + "version": "1100.1.4", "description": "Exports util functions to clean up node_modules", "keywords": [ "pnpm", diff --git a/installing/linking/real-hoist/CHANGELOG.md b/installing/linking/real-hoist/CHANGELOG.md index 6f60df2f9f..10c7694c5d 100644 --- a/installing/linking/real-hoist/CHANGELOG.md +++ b/installing/linking/real-hoist/CHANGELOG.md @@ -1,5 +1,26 @@ # @pnpm/real-hoist +## 1100.0.10 + +### Patch Changes + +- Updated dependencies [e55f4b5] + - @pnpm/lockfile.utils@1100.0.10 + - @pnpm/deps.path@1100.0.5 + +## 1100.0.9 + +### Patch Changes + +- @pnpm/lockfile.utils@1100.0.9 +- @pnpm/deps.path@1100.0.4 + +## 1100.0.8 + +### Patch Changes + +- @pnpm/lockfile.utils@1100.0.8 + ## 1100.0.7 ### Patch Changes diff --git a/installing/linking/real-hoist/package.json b/installing/linking/real-hoist/package.json index 639f04f3f3..fd30bc8c2e 100644 --- a/installing/linking/real-hoist/package.json +++ b/installing/linking/real-hoist/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/installing.linking.real-hoist", - "version": "1100.0.7", + "version": "1100.0.10", "description": "Hoists dependencies in a node_modules created by pnpm", "keywords": [ "pnpm", diff --git a/installing/modules-yaml/CHANGELOG.md b/installing/modules-yaml/CHANGELOG.md index 1796b828c7..6550edb6db 100644 --- a/installing/modules-yaml/CHANGELOG.md +++ b/installing/modules-yaml/CHANGELOG.md @@ -1,5 +1,19 @@ # @pnpm/modules-yaml +## 1100.0.6 + +### Patch Changes + +- Updated dependencies [35d2355] + - @pnpm/types@1101.2.0 + +## 1100.0.5 + +### Patch Changes + +- Updated dependencies [64afc92] + - @pnpm/types@1101.1.1 + ## 1100.0.4 ### Patch Changes diff --git a/installing/modules-yaml/package.json b/installing/modules-yaml/package.json index 7bb1d60211..bbc7e93c25 100644 --- a/installing/modules-yaml/package.json +++ b/installing/modules-yaml/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/installing.modules-yaml", - "version": "1100.0.4", + "version": "1100.0.6", "description": "Reads/writes `node_modules/.modules.yaml`", "keywords": [ "pnpm", diff --git a/installing/package-requester/CHANGELOG.md b/installing/package-requester/CHANGELOG.md index 68ad065fe6..52387529de 100644 --- a/installing/package-requester/CHANGELOG.md +++ b/installing/package-requester/CHANGELOG.md @@ -1,5 +1,58 @@ # @pnpm/package-requester +## 1101.0.9 + +### Patch Changes + +- Updated dependencies [aa6149d] +- Updated dependencies [35d2355] + - @pnpm/worker@1100.1.8 + - @pnpm/types@1101.2.0 + - @pnpm/config.package-is-installable@1100.0.7 + - @pnpm/core-loggers@1100.1.2 + - @pnpm/deps.path@1100.0.5 + - @pnpm/fetching.fetcher-base@1100.1.6 + - @pnpm/hooks.types@1100.0.9 + - @pnpm/resolving.resolver-base@1100.3.1 + - @pnpm/store.cafs@1100.1.7 + - @pnpm/store.controller-types@1100.1.2 + - @pnpm/fetching.pick-fetcher@1100.0.9 + +## 1101.0.8 + +### Patch Changes + +- Updated dependencies [1627943] +- Updated dependencies [64afc92] + - @pnpm/resolving.resolver-base@1100.3.0 + - @pnpm/types@1101.1.1 + - @pnpm/fetching.fetcher-base@1100.1.5 + - @pnpm/fetching.pick-fetcher@1100.0.8 + - @pnpm/hooks.types@1100.0.8 + - @pnpm/store.controller-types@1100.1.1 + - @pnpm/config.package-is-installable@1100.0.6 + - @pnpm/core-loggers@1100.1.1 + - @pnpm/deps.path@1100.0.4 + - @pnpm/store.cafs@1100.1.6 + - @pnpm/worker@1100.1.7 + +## 1101.0.7 + +### Patch Changes + +- Updated dependencies [4195766] +- Updated dependencies [31538bf] +- Updated dependencies [4a79336] + - @pnpm/resolving.resolver-base@1100.2.0 + - @pnpm/store.controller-types@1100.1.0 + - @pnpm/core-loggers@1100.1.0 + - @pnpm/fetching.fetcher-base@1100.1.4 + - @pnpm/fetching.pick-fetcher@1100.0.7 + - @pnpm/hooks.types@1100.0.7 + - @pnpm/store.cafs@1100.1.5 + - @pnpm/config.package-is-installable@1100.0.5 + - @pnpm/worker@1100.1.6 + ## 1101.0.6 ### Patch Changes diff --git a/installing/package-requester/package.json b/installing/package-requester/package.json index d12a953906..3a26a37b7f 100644 --- a/installing/package-requester/package.json +++ b/installing/package-requester/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/installing.package-requester", - "version": "1101.0.6", + "version": "1101.0.9", "description": "Concurrent downloader of npm-compatible packages", "keywords": [ "pnpm", diff --git a/installing/package-requester/src/packageRequester.ts b/installing/package-requester/src/packageRequester.ts index 97b0e118d2..7547a0ec56 100644 --- a/installing/package-requester/src/packageRequester.ts +++ b/installing/package-requester/src/packageRequester.ts @@ -193,6 +193,7 @@ async function resolveAndFetch ( publishedAt, normalizedBareSpecifier, alias, + policyViolation, } = resolveResult // Check if the integrity has changed between the current and newly resolved package @@ -256,6 +257,7 @@ async function resolveAndFetch ( updated, publishedAt, alias, + policyViolation, }, } } @@ -319,6 +321,7 @@ async function resolveAndFetch ( updated, publishedAt, alias, + policyViolation, }, fetching: fetchResult.fetching, filesIndexFile: fetchResult.filesIndexFile, diff --git a/installing/read-projects-context/CHANGELOG.md b/installing/read-projects-context/CHANGELOG.md index ddc868d0d6..15d7e68d6d 100644 --- a/installing/read-projects-context/CHANGELOG.md +++ b/installing/read-projects-context/CHANGELOG.md @@ -1,5 +1,34 @@ # @pnpm/read-projects-context +## 1100.0.12 + +### Patch Changes + +- Updated dependencies [35d2355] + - @pnpm/types@1101.2.0 + - @pnpm/lockfile.fs@1100.1.2 + - @pnpm/config.normalize-registries@1100.0.5 + - @pnpm/installing.modules-yaml@1100.0.6 + +## 1100.0.11 + +### Patch Changes + +- Updated dependencies [9cb48bb] +- Updated dependencies [64afc92] + - @pnpm/lockfile.fs@1100.1.1 + - @pnpm/types@1101.1.1 + - @pnpm/config.normalize-registries@1100.0.4 + - @pnpm/installing.modules-yaml@1100.0.5 + +## 1100.0.10 + +### Patch Changes + +- Updated dependencies [6e93f35] +- Updated dependencies [2a9bd89] + - @pnpm/lockfile.fs@1100.1.0 + ## 1100.0.9 ### Patch Changes diff --git a/installing/read-projects-context/package.json b/installing/read-projects-context/package.json index 2c8f5a6835..4a78c07475 100644 --- a/installing/read-projects-context/package.json +++ b/installing/read-projects-context/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/installing.read-projects-context", - "version": "1100.0.9", + "version": "1100.0.12", "description": "Reads the current state of projects from modules manifest", "keywords": [ "pnpm", diff --git a/justfile b/justfile index 4fc245bf28..8767399814 100644 --- a/justfile +++ b/justfile @@ -52,9 +52,9 @@ fmt: # Run cargo check check: - cargo check --locked + cargo check --locked --workspace --all-targets -# Run all the tests +# Run all the tests. test: cargo nextest run @@ -68,7 +68,7 @@ known-failures: @cargo test --workspace known_failures -- --list 2>nul | rg '^known_failures::' # Lint the whole project lint: - cargo clippy --locked -- --deny warnings + cargo clippy --locked --workspace --all-targets -- --deny warnings # Run perfectionist dylint rules. Requires `cargo-dylint` and `dylint-link` # (install with `cargo binstall cargo-dylint dylint-link`). The lint library @@ -84,11 +84,26 @@ codecov: micro-benchmark: cargo run --bin=micro-benchmark --release -# Manage registry-mock +# Manage registry-mock. The launcher spawns `pnpm-registry`; on +# Windows you can't overwrite a running .exe, so we pre-build all +# the test artifacts a subsequent `just test` will need with the +# exact same invocation. A `-p pnpm-registry`-scoped pre-build is +# not enough — workspace-wide feature unification gives a +# different fingerprint and nextest would still try to re-link the +# running binary, failing with `os error 5` on Windows MSVC. registry-mock +args: + cargo nextest run --no-run cargo run --bin=pacquet-registry-mock -- {{args}} +# The benchmark may auto-spawn the registry mock (via +# `AutoMockInstance::load_or_init()`), so make sure `pnpm-registry` +# is built before the executor runs — otherwise the spawn step +# aborts with "binary not found". Built with `--release` so the +# mock serves at optimized perf; a debug build would put the +# Rust mock at a multi-second handicap vs verdaccio, which V8 +# always JITs, polluting the install-perf signal. integrated-benchmark +args: + cargo build --release --bin=pnpm-registry cargo run --bin=integrated-benchmark -- {{args}} cli +args: diff --git a/lockfile/detect-dep-types/CHANGELOG.md b/lockfile/detect-dep-types/CHANGELOG.md index 09b62aeafc..c83c94db72 100644 --- a/lockfile/detect-dep-types/CHANGELOG.md +++ b/lockfile/detect-dep-types/CHANGELOG.md @@ -1,5 +1,29 @@ # @pnpm/lockfile.detect-dep-types +## 1100.0.8 + +### Patch Changes + +- Updated dependencies [35d2355] + - @pnpm/types@1101.2.0 + - @pnpm/deps.path@1100.0.5 + - @pnpm/lockfile.types@1100.0.8 + +## 1100.0.7 + +### Patch Changes + +- Updated dependencies [64afc92] + - @pnpm/types@1101.1.1 + - @pnpm/lockfile.types@1100.0.7 + - @pnpm/deps.path@1100.0.4 + +## 1100.0.6 + +### Patch Changes + +- @pnpm/lockfile.types@1100.0.6 + ## 1100.0.5 ### Patch Changes diff --git a/lockfile/detect-dep-types/package.json b/lockfile/detect-dep-types/package.json index 1f8105cc7c..bc722e166f 100644 --- a/lockfile/detect-dep-types/package.json +++ b/lockfile/detect-dep-types/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/lockfile.detect-dep-types", - "version": "1100.0.5", + "version": "1100.0.8", "description": "Detect the types of dependencies", "keywords": [ "pnpm", diff --git a/lockfile/filtering/CHANGELOG.md b/lockfile/filtering/CHANGELOG.md index d3afb0fc42..6d3952bc60 100644 --- a/lockfile/filtering/CHANGELOG.md +++ b/lockfile/filtering/CHANGELOG.md @@ -1,5 +1,39 @@ # @pnpm/filter-lockfile +## 1100.1.3 + +### Patch Changes + +- Updated dependencies [e55f4b5] +- Updated dependencies [35d2355] + - @pnpm/lockfile.utils@1100.0.10 + - @pnpm/types@1101.2.0 + - @pnpm/config.package-is-installable@1100.0.7 + - @pnpm/deps.path@1100.0.5 + - @pnpm/lockfile.types@1100.0.8 + - @pnpm/lockfile.walker@1100.0.8 + +## 1100.1.2 + +### Patch Changes + +- Updated dependencies [64afc92] + - @pnpm/types@1101.1.1 + - @pnpm/lockfile.types@1100.0.7 + - @pnpm/lockfile.utils@1100.0.9 + - @pnpm/config.package-is-installable@1100.0.6 + - @pnpm/deps.path@1100.0.4 + - @pnpm/lockfile.walker@1100.0.7 + +## 1100.1.1 + +### Patch Changes + +- @pnpm/lockfile.types@1100.0.6 +- @pnpm/lockfile.utils@1100.0.8 +- @pnpm/config.package-is-installable@1100.0.5 +- @pnpm/lockfile.walker@1100.0.6 + ## 1100.1.0 ### Minor Changes diff --git a/lockfile/filtering/package.json b/lockfile/filtering/package.json index 892447fa8b..af88c62ad8 100644 --- a/lockfile/filtering/package.json +++ b/lockfile/filtering/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/lockfile.filtering", - "version": "1100.1.0", + "version": "1100.1.3", "description": "Filters a lockfile", "keywords": [ "pnpm", diff --git a/lockfile/fs/CHANGELOG.md b/lockfile/fs/CHANGELOG.md index 18a3c7642b..9e2c7f9e57 100644 --- a/lockfile/fs/CHANGELOG.md +++ b/lockfile/fs/CHANGELOG.md @@ -1,5 +1,46 @@ # @pnpm/lockfile-file +## 1100.1.2 + +### Patch Changes + +- Updated dependencies [e55f4b5] +- Updated dependencies [35d2355] + - @pnpm/lockfile.utils@1100.0.10 + - @pnpm/types@1101.2.0 + - @pnpm/deps.path@1100.0.5 + - @pnpm/lockfile.merger@1100.0.8 + - @pnpm/lockfile.types@1100.0.8 + +## 1100.1.1 + +### Patch Changes + +- 9cb48bb: Fix two crashes with `injectWorkspacePackages: true` when the lockfile has been pruned (e.g. by `turbo prune --docker`): + + - `Cannot use 'in' operator to search for 'directory' in undefined`: a peer-dependency-variant injected snapshot inherits its `resolution` from the base `packages:` entry; when a pruner drops that base entry the readers crash. `convertToLockfileObject` now reconstructs the directory resolution from the `file:` depPath at load time — a single normalization point, so every reader sees a fully-formed snapshot. + - `ERR_PNPM_ENOENT` on `node_modules/.bin/`: after `prepare`/`postinstall`, `runLifecycleHooksConcurrently` re-imported each injected workspace package; the `scanDir`-into-`filesMap` workaround fed target-internal paths to the importer, which the `makeEmptyDir` fast path (#11088) then wiped. Drop the workaround and pass `keepModulesDir: true` so the importer preserves the target's existing `node_modules` (bin links + transitive deps) and source files keep their hardlinks. + +- Updated dependencies [64afc92] + - @pnpm/types@1101.1.1 + - @pnpm/lockfile.types@1100.0.7 + - @pnpm/lockfile.utils@1100.0.9 + - @pnpm/deps.path@1100.0.4 + - @pnpm/lockfile.merger@1100.0.7 + +## 1100.1.0 + +### Minor Changes + +- 2a9bd89: Record the post-resolution lockfile in the verification cache. Previously the cache only captured the lockfile that was loaded at the start of an install, so a flow like `pnpm install ` followed by `rm -rf node_modules && pnpm install` re-ran the per-package registry round-trip against the newly written lockfile even though the local resolver had already enforced the policy when picking those versions. The fresh lockfile is now recorded immediately after each install-time write, so the second install takes the cache fast path. + +### Patch Changes + +- 6e93f35: Fix lockfile parsing failures when `pnpm-lock.yaml` contains CRLF line endings and multiple YAML documents [#11612](https://github.com/pnpm/pnpm/issues/11612). + - @pnpm/lockfile.types@1100.0.6 + - @pnpm/lockfile.utils@1100.0.8 + - @pnpm/lockfile.merger@1100.0.6 + ## 1100.0.8 ### Patch Changes diff --git a/lockfile/fs/package.json b/lockfile/fs/package.json index 92a60f40ad..93bad086ac 100644 --- a/lockfile/fs/package.json +++ b/lockfile/fs/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/lockfile.fs", - "version": "1100.0.8", + "version": "1100.1.2", "description": "Read/write pnpm-lock.yaml files", "keywords": [ "pnpm", diff --git a/lockfile/fs/src/index.ts b/lockfile/fs/src/index.ts index 7693bce771..910f216103 100644 --- a/lockfile/fs/src/index.ts +++ b/lockfile/fs/src/index.ts @@ -3,12 +3,14 @@ export { existsNonEmptyWantedLockfile } from './existsWantedLockfile.js' export { getLockfileImporterId } from './getLockfileImporterId.js' export { cleanGitBranchLockfiles } from './gitBranchLockfile.js' export { convertToLockfileFile, convertToLockfileObject } from './lockfileFormatConverters.js' +export { getWantedLockfileName } from './lockfileName.js' export * from './read.js' export { isEmptyLockfile, writeCurrentLockfile, writeLockfileFile, writeLockfiles, + type WriteLockfilesResult, writeWantedLockfile, } from './write.js' export { extractMainDocument } from './yamlDocuments.js' diff --git a/lockfile/fs/src/lockfileFormatConverters.ts b/lockfile/fs/src/lockfileFormatConverters.ts index d8415db4ed..6f1e81aa64 100644 --- a/lockfile/fs/src/lockfileFormatConverters.ts +++ b/lockfile/fs/src/lockfileFormatConverters.ts @@ -1,5 +1,5 @@ import { LOCKFILE_VERSION } from '@pnpm/constants' -import { refToRelative, removeSuffix } from '@pnpm/deps.path' +import { parse, refToRelative, removeSuffix } from '@pnpm/deps.path' import type { LockfileFile, LockfileFileProjectResolvedDependencies, @@ -138,13 +138,31 @@ function pruneTimeInLockfile (time: Record, importers: Record rootDepPaths.has(depPath), time) } +// Mirrors `isFilename` in `resolving/local-resolver/src/parseBareSpecifier.ts` +// so the directory-vs-tarball boundary applied at lockfile load time +// matches the resolver's at resolve time. +const LOCAL_TARBALL_RE = /\.(?:tgz|tar\.gz|tar)$/i + export function convertToLockfileObject (lockfile: LockfileFile): LockfileObject { const { importers, ...rest } = lockfile const packages: PackageSnapshots = {} for (const [depPath, pkg] of Object.entries(lockfile.snapshots ?? {})) { const pkgId = removeSuffix(depPath) - packages[depPath as DepPath] = Object.assign(pkg, lockfile.packages?.[pkgId]) + const snapshot = Object.assign(pkg, lockfile.packages?.[pkgId]) + // Defense-in-depth for pruned lockfiles (older `turbo prune --docker`, + // pre vercel/turborepo#12825): a peer-variant injected workspace + // snapshot whose base `packages:` entry was dropped now has a null + // `resolution`. Reconstruct it from the `file:` depPath — same value + // pnpm's writer emits — so every downstream reader sees a complete + // snapshot without per-reader guards. + if (snapshot.resolution == null) { + const ref = parse(depPath).nonSemverVersion + if (ref != null && ref.startsWith('file:') && !LOCAL_TARBALL_RE.test(ref)) { + snapshot.resolution = { directory: ref.slice('file:'.length), type: 'directory' } + } + } + packages[depPath as DepPath] = snapshot enrichGitHostedFlag(packages[depPath as DepPath]?.resolution as TarballResolution | undefined) } return { diff --git a/lockfile/fs/src/write.ts b/lockfile/fs/src/write.ts index 46de136232..dd6a1fc575 100644 --- a/lockfile/fs/src/write.ts +++ b/lockfile/fs/src/write.ts @@ -8,7 +8,7 @@ import yaml from 'js-yaml' import { isEmpty } from 'ramda' import writeFileAtomic from 'write-file-atomic' -import { convertToLockfileFile } from './lockfileFormatConverters.js' +import { convertToLockfileFile, convertToLockfileObject } from './lockfileFormatConverters.js' import { getWantedLockfileName } from './lockfileName.js' import { lockfileLogger as logger } from './logger.js' import { sortLockfileKeys } from './sortLockfileKeys.js' @@ -26,26 +26,34 @@ export function lockfileYamlDump (obj: object): string { return yaml.dump(obj, LOCKFILE_YAML_FORMAT) } +/** + * Returns the canonical post-write lockfile — structurally identical + * to what `readWantedLockfile` would parse back. Lets callers like + * the verification cache hash the as-saved form without re-reading. + */ export async function writeWantedLockfile ( pkgPath: string, wantedLockfile: LockfileObject, opts?: { useGitBranchLockfile?: boolean mergeGitBranchLockfiles?: boolean + /** Pre-resolved filename; skips the `getWantedLockfileName` (and + * its `getCurrentBranch`) call when supplied. */ + lockfileName?: string } -): Promise { - const wantedLockfileName: string = await getWantedLockfileName(opts) +): Promise { + const wantedLockfileName: string = opts?.lockfileName ?? await getWantedLockfileName(opts) return writeLockfile(wantedLockfileName, pkgPath, wantedLockfile) } export async function writeCurrentLockfile ( virtualStoreDir: string, currentLockfile: LockfileObject -): Promise { +): Promise { // empty lockfile is not saved if (isEmptyLockfile(currentLockfile)) { await rimraf(path.join(virtualStoreDir, 'lock.yaml')) - return + return undefined } await fs.mkdir(virtualStoreDir, { recursive: true }) return writeLockfile('lock.yaml', virtualStoreDir, currentLockfile) @@ -55,7 +63,7 @@ async function writeLockfile ( lockfileFilename: string, pkgPath: string, wantedLockfile: LockfileObject -): Promise { +): Promise { const lockfilePath = path.join(pkgPath, lockfileFilename) const lockfileToStringify = convertToLockfileFile(wantedLockfile) @@ -69,9 +77,27 @@ async function writeLockfile ( // in the OS page cache and streaming stops at the first separator. const envDoc = await streamReadFirstYamlDocument(lockfilePath) const envPrefix = envDoc != null ? `${YAML_DOCUMENT_START}${envDoc}${YAML_DOCUMENT_SEPARATOR}` : '' - return writeFileAtomic(lockfilePath, `${envPrefix}${yamlDoc}`) + await writeFileAtomic(lockfilePath, `${envPrefix}${yamlDoc}`) + } else { + await writeFileAtomic(lockfilePath, yamlDoc) } - return writeFileAtomic(lockfilePath, yamlDoc) + + // YAML drops undefined on serialize, so the in-memory LockfileFile + // can carry fields (like an unset settings.dedupePeers) that won't + // survive a round-trip; strip them to mirror what the next reader + // will parse back. + return convertToLockfileObject(stripUndefinedDeep(lockfileToStringify) as LockfileFile) +} + +function stripUndefinedDeep (value: T): T { + if (value === null || typeof value !== 'object') return value + if (Array.isArray(value)) return value.map(stripUndefinedDeep) as unknown as T + const out: Record = {} + for (const [k, v] of Object.entries(value as Record)) { + if (v === undefined) continue + out[k] = stripUndefinedDeep(v) + } + return out as T } export function writeLockfileFile ( @@ -91,6 +117,19 @@ export function isEmptyLockfile (lockfile: LockfileObject): boolean { return Object.values(lockfile.importers).every((importer) => isEmpty(importer.specifiers ?? {}) && isEmpty(importer.dependencies ?? {})) } +export interface WriteLockfilesResult { + /** + * The canonical "as-saved" wanted lockfile — the inverse converter + * applied to the same object that was serialized to YAML. Hashing + * this is equivalent to hashing the lockfile the next install will + * load from disk (modulo undefined values that YAML drops, which any + * sensible canonicalization-then-hash routine should strip). + */ + wantedLockfile: LockfileObject + /** Same as above for the current lockfile, or undefined when it was skipped because empty. */ + currentLockfile: LockfileObject | undefined +} + export async function writeLockfiles ( opts: { wantedLockfile: LockfileObject @@ -99,9 +138,11 @@ export async function writeLockfiles ( currentLockfileDir: string useGitBranchLockfile?: boolean mergeGitBranchLockfiles?: boolean + /** See {@link writeWantedLockfile}'s `lockfileName` option. */ + wantedLockfileName?: string } -): Promise { - const wantedLockfileName: string = await getWantedLockfileName(opts) +): Promise { + const wantedLockfileName: string = opts.wantedLockfileName ?? await getWantedLockfileName(opts) const wantedLockfilePath = path.join(opts.wantedLockfileDir, wantedLockfileName) const currentLockfilePath = path.join(opts.currentLockfileDir, 'lock.yaml') @@ -134,7 +175,12 @@ export async function writeLockfiles ( } })(), ]) - return + // Both files share the same source object; strip once and reuse. + const normalized = convertToLockfileObject(stripUndefinedDeep(wantedLockfileToStringify) as LockfileFile) + return { + wantedLockfile: normalized, + currentLockfile: isEmptyLockfile(opts.wantedLockfile) ? undefined : normalized, + } } logger.debug({ @@ -145,10 +191,13 @@ export async function writeLockfiles ( const currentLockfileToStringify = convertToLockfileFile(opts.currentLockfile) const currentYamlDoc = yamlStringify(currentLockfileToStringify) + // Filtered-current callers (deps-restorer) can pass an empty + // current against a non-empty wanted; key off the current. + const currentIsEmpty = isEmptyLockfile(opts.currentLockfile) await Promise.all([ writeFileAtomic(wantedLockfilePath, wantedYamlDoc), (async () => { - if (isEmptyLockfile(opts.wantedLockfile)) { + if (currentIsEmpty) { await rimraf(currentLockfilePath) } else { await fs.mkdir(path.dirname(currentLockfilePath), { recursive: true }) @@ -156,4 +205,10 @@ export async function writeLockfiles ( } })(), ]) + return { + wantedLockfile: convertToLockfileObject(stripUndefinedDeep(wantedLockfileToStringify) as LockfileFile), + currentLockfile: currentIsEmpty + ? undefined + : convertToLockfileObject(stripUndefinedDeep(currentLockfileToStringify) as LockfileFile), + } } diff --git a/lockfile/fs/test/lockfileV6Converters.test.ts b/lockfile/fs/test/lockfileV6Converters.test.ts index 25f5e94fcd..52cf65c292 100644 --- a/lockfile/fs/test/lockfileV6Converters.test.ts +++ b/lockfile/fs/test/lockfileV6Converters.test.ts @@ -1,4 +1,5 @@ import { expect, test } from '@jest/globals' +import type { DepPath } from '@pnpm/types' import { convertToLockfileFile, convertToLockfileObject } from '../lib/lockfileFormatConverters.js' @@ -93,6 +94,36 @@ test('convertToLockfileFile()', () => { expect(convertToLockfileObject(lockfileV6)).toEqual(lockfileV5) }) +test('convertToLockfileObject() reconstructs a dropped directory resolution for a pruned file: peer-variant, but never for a file: tarball', () => { + // Simulates a pruned lockfile (e.g. after `turbo prune --docker`): the + // base `pkg@file:...` packages entry that carried `resolution` is gone, + // only the peer-variant snapshot remains. + const prunedLockfileV6 = { + lockfileVersion: '9.0', + importers: {}, + snapshots: { + 'dir@file:packages/dir(peer@1.0.0)': {}, + 'tar@file:vendor/tar-1.0.0.tgz(peer@1.0.0)': {}, + // Uppercase tarball extensions must be treated as tarballs too — the + // resolver in resolving/local-resolver/src/parseBareSpecifier.ts + // matches /\.(?:tgz|tar.gz|tar)$/i, so the boundary applied here at + // load time has to be case-insensitive in lockstep. + 'upper@file:vendor/upper-1.0.0.TGZ(peer@1.0.0)': {}, + 'mixed@file:vendor/mixed-1.0.0.Tar.Gz(peer@1.0.0)': {}, + }, + } + const lockfile = convertToLockfileObject(prunedLockfileV6) + // Local-directory `file:` ref → directory resolution losslessly reconstructed. + expect(lockfile.packages?.['dir@file:packages/dir(peer@1.0.0)' as DepPath]?.resolution).toEqual({ + directory: 'packages/dir', + type: 'directory', + }) + // `file:` tarball ref → must NOT be turned into a directory resolution. + expect(lockfile.packages?.['tar@file:vendor/tar-1.0.0.tgz(peer@1.0.0)' as DepPath]?.resolution).toBeUndefined() + expect(lockfile.packages?.['upper@file:vendor/upper-1.0.0.TGZ(peer@1.0.0)' as DepPath]?.resolution).toBeUndefined() + expect(lockfile.packages?.['mixed@file:vendor/mixed-1.0.0.Tar.Gz(peer@1.0.0)' as DepPath]?.resolution).toBeUndefined() +}) + test('convertToLockfileFile() with lockfile v6', () => { const lockfileV5 = { lockfileVersion: '9.0', diff --git a/lockfile/fs/test/write.test.ts b/lockfile/fs/test/write.test.ts index a6ed6183f3..13a39ccbe9 100644 --- a/lockfile/fs/test/write.test.ts +++ b/lockfile/fs/test/write.test.ts @@ -98,6 +98,76 @@ test('writeLockfiles() when no specifiers but dependencies present', async () => expect(await readWantedLockfile(projectPath, { ignoreIncompatible: false })).toEqual(wantedLockfile) }) +test('writeWantedLockfile() returns the canonical lockfile — matches what readWantedLockfile produces, even when the input carries undefined optional fields', async () => { + // Cache-key contract: callers (today, the verification cache) need a + // hash of the *as-saved* lockfile, not the in-memory write object. + // Those two diverge specifically because YAML drops `undefined` on + // serialize. To exercise that drop, the fixture has to actually + // carry an explicit `undefined` — `settings.dedupePeers` here, the + // same field install-time code produces (see + // installing/deps-installer/src/install/index.ts where it's set to + // `opts.dedupePeers || undefined`). Without this, the test would + // happily pass against a writer that returned a near-canonical-but- + // still-divergent object. + const projectPath = temporaryDirectory() + const wantedLockfile = { + importers: { + '.': { + specifiers: { 'is-positive': '^1.0.0' }, + dependencies: { 'is-positive': '1.0.0' }, + }, + }, + lockfileVersion: LOCKFILE_VERSION, + settings: { + autoInstallPeers: true, + excludeLinksFromLockfile: false, + dedupePeers: undefined, + }, + packages: { + '/is-positive@1.0.0': { + resolution: { + integrity: 'sha1-ChbBDewTLAqLCzb793Fo5VDvg/g=', + }, + }, + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any + const written = await writeWantedLockfile(projectPath, wantedLockfile) + const loaded = await readWantedLockfile(projectPath, { ignoreIncompatible: false }) + expect(written).toEqual(loaded) + // Verify the canonicalization actually dropped the undefined field — + // toEqual is lenient about undefined-vs-missing, so check explicitly. + expect('dedupePeers' in (written.settings ?? {})).toBe(false) +}) + +test('writeLockfiles() return matches readWantedLockfile/readCurrentLockfile output', async () => { + const projectPath = temporaryDirectory() + const wantedLockfile = { + importers: { + '.': { + specifiers: { 'is-positive': '^1.0.0' }, + dependencies: { 'is-positive': '1.0.0' }, + }, + }, + lockfileVersion: LOCKFILE_VERSION, + packages: { + '/is-positive@1.0.0': { + resolution: { integrity: 'sha1-ChbBDewTLAqLCzb793Fo5VDvg/g=' }, + }, + }, + } + const written = await writeLockfiles({ + currentLockfile: wantedLockfile, + currentLockfileDir: projectPath, + wantedLockfile, + wantedLockfileDir: projectPath, + }) + const loadedWanted = await readWantedLockfile(projectPath, { ignoreIncompatible: false }) + const loadedCurrent = await readCurrentLockfile(projectPath, { ignoreIncompatible: false }) + expect(written.wantedLockfile).toEqual(loadedWanted) + expect(written.currentLockfile).toEqual(loadedCurrent) +}) + test('write does not use yaml anchors/aliases', async () => { const projectPath = temporaryDirectory() const wantedLockfile = { diff --git a/lockfile/make-dedicated-lockfile/CHANGELOG.md b/lockfile/make-dedicated-lockfile/CHANGELOG.md index 404176797e..686af2a287 100644 --- a/lockfile/make-dedicated-lockfile/CHANGELOG.md +++ b/lockfile/make-dedicated-lockfile/CHANGELOG.md @@ -1,5 +1,49 @@ # @pnpm/make-dedicated-lockfile +## 1100.0.14 + +### Patch Changes + +- Updated dependencies [a456dc7] +- Updated dependencies [35d2355] + - @pnpm/workspace.project-manifest-reader@1100.0.9 + - @pnpm/types@1101.2.0 + - @pnpm/releasing.exportable-manifest@1100.1.1 + - @pnpm/lockfile.fs@1100.1.2 + - @pnpm/lockfile.pruner@1100.0.8 + +## 1100.0.13 + +### Patch Changes + +- Updated dependencies [d7da112] +- Updated dependencies [3b62f9d] + - @pnpm/workspace.project-manifest-reader@1100.0.8 + - @pnpm/releasing.exportable-manifest@1100.1.0 + +## 1100.0.12 + +### Patch Changes + +- Updated dependencies [9cb48bb] +- Updated dependencies [64afc92] + - @pnpm/lockfile.fs@1100.1.1 + - @pnpm/types@1101.1.1 + - @pnpm/workspace.project-manifest-reader@1100.0.7 + - @pnpm/lockfile.pruner@1100.0.7 + - @pnpm/releasing.exportable-manifest@1100.0.7 + +## 1100.0.11 + +### Patch Changes + +- Updated dependencies [6e93f35] +- Updated dependencies [2a9bd89] + - @pnpm/lockfile.fs@1100.1.0 + - @pnpm/lockfile.pruner@1100.0.6 + - @pnpm/releasing.exportable-manifest@1100.0.6 + - @pnpm/workspace.project-manifest-reader@1100.0.6 + ## 1100.0.10 ### Patch Changes diff --git a/lockfile/make-dedicated-lockfile/package.json b/lockfile/make-dedicated-lockfile/package.json index 6945cdbd75..9c3188207a 100644 --- a/lockfile/make-dedicated-lockfile/package.json +++ b/lockfile/make-dedicated-lockfile/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/lockfile.make-dedicated-lockfile", - "version": "1100.0.10", + "version": "1100.0.14", "description": "Creates a dedicated lockfile for a subset of workspace projects", "keywords": [ "pnpm", diff --git a/lockfile/merger/CHANGELOG.md b/lockfile/merger/CHANGELOG.md index 3915ec9efc..4db370edbc 100644 --- a/lockfile/merger/CHANGELOG.md +++ b/lockfile/merger/CHANGELOG.md @@ -1,5 +1,27 @@ # @pnpm/merge-lockfile-changes +## 1100.0.8 + +### Patch Changes + +- Updated dependencies [35d2355] + - @pnpm/types@1101.2.0 + - @pnpm/lockfile.types@1100.0.8 + +## 1100.0.7 + +### Patch Changes + +- Updated dependencies [64afc92] + - @pnpm/types@1101.1.1 + - @pnpm/lockfile.types@1100.0.7 + +## 1100.0.6 + +### Patch Changes + +- @pnpm/lockfile.types@1100.0.6 + ## 1100.0.5 ### Patch Changes diff --git a/lockfile/merger/package.json b/lockfile/merger/package.json index 0d4f1c5c5e..c40f28c9e4 100644 --- a/lockfile/merger/package.json +++ b/lockfile/merger/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/lockfile.merger", - "version": "1100.0.5", + "version": "1100.0.8", "description": "Merges lockfiles. Can automatically fix merge conflicts", "keywords": [ "pnpm", diff --git a/lockfile/preferred-versions/CHANGELOG.md b/lockfile/preferred-versions/CHANGELOG.md index 211d9679aa..ed062c0320 100644 --- a/lockfile/preferred-versions/CHANGELOG.md +++ b/lockfile/preferred-versions/CHANGELOG.md @@ -1,5 +1,37 @@ # @pnpm/lockfile.preferred-versions +## 1100.0.12 + +### Patch Changes + +- Updated dependencies [e55f4b5] +- Updated dependencies [35d2355] + - @pnpm/lockfile.utils@1100.0.10 + - @pnpm/types@1101.2.0 + - @pnpm/pkg-manifest.utils@1100.2.1 + - @pnpm/resolving.resolver-base@1100.3.1 + +## 1100.0.11 + +### Patch Changes + +- Updated dependencies [1627943] +- Updated dependencies [64afc92] + - @pnpm/resolving.resolver-base@1100.3.0 + - @pnpm/pkg-manifest.utils@1100.2.0 + - @pnpm/types@1101.1.1 + - @pnpm/lockfile.utils@1100.0.9 + +## 1100.0.10 + +### Patch Changes + +- Updated dependencies [4195766] +- Updated dependencies [31538bf] + - @pnpm/resolving.resolver-base@1100.2.0 + - @pnpm/lockfile.utils@1100.0.8 + - @pnpm/pkg-manifest.utils@1100.1.4 + ## 1100.0.9 ### Patch Changes diff --git a/lockfile/preferred-versions/package.json b/lockfile/preferred-versions/package.json index 82dc16fea8..94d112daab 100644 --- a/lockfile/preferred-versions/package.json +++ b/lockfile/preferred-versions/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/lockfile.preferred-versions", - "version": "1100.0.9", + "version": "1100.0.12", "description": "Get preferred version from lockfile", "keywords": [ "pnpm", diff --git a/lockfile/pruner/CHANGELOG.md b/lockfile/pruner/CHANGELOG.md index 24acd9e682..9aa78e93a4 100644 --- a/lockfile/pruner/CHANGELOG.md +++ b/lockfile/pruner/CHANGELOG.md @@ -1,5 +1,29 @@ # @pnpm/prune-lockfile +## 1100.0.8 + +### Patch Changes + +- Updated dependencies [35d2355] + - @pnpm/types@1101.2.0 + - @pnpm/deps.path@1100.0.5 + - @pnpm/lockfile.types@1100.0.8 + +## 1100.0.7 + +### Patch Changes + +- Updated dependencies [64afc92] + - @pnpm/types@1101.1.1 + - @pnpm/lockfile.types@1100.0.7 + - @pnpm/deps.path@1100.0.4 + +## 1100.0.6 + +### Patch Changes + +- @pnpm/lockfile.types@1100.0.6 + ## 1100.0.5 ### Patch Changes diff --git a/lockfile/pruner/package.json b/lockfile/pruner/package.json index e005df55b8..8ae003895e 100644 --- a/lockfile/pruner/package.json +++ b/lockfile/pruner/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/lockfile.pruner", - "version": "1100.0.5", + "version": "1100.0.8", "description": "Prune a pnpm-lock.yaml", "keywords": [ "pnpm", diff --git a/lockfile/settings-checker/CHANGELOG.md b/lockfile/settings-checker/CHANGELOG.md index f486928df1..95205a24e9 100644 --- a/lockfile/settings-checker/CHANGELOG.md +++ b/lockfile/settings-checker/CHANGELOG.md @@ -1,5 +1,29 @@ # @pnpm/lockfile.settings-checker +## 1100.0.13 + +### Patch Changes + +- @pnpm/lockfile.verification@1100.0.13 +- @pnpm/lockfile.types@1100.0.8 +- @pnpm/crypto.hash@1100.0.1 + +## 1100.0.12 + +### Patch Changes + +- @pnpm/lockfile.types@1100.0.7 +- @pnpm/lockfile.verification@1100.0.12 +- @pnpm/crypto.hash@1100.0.1 + +## 1100.0.11 + +### Patch Changes + +- @pnpm/lockfile.types@1100.0.6 +- @pnpm/lockfile.verification@1100.0.11 +- @pnpm/crypto.hash@1100.0.1 + ## 1100.0.10 ### Patch Changes diff --git a/lockfile/settings-checker/package.json b/lockfile/settings-checker/package.json index 75ab4e87c7..1f44bebb32 100644 --- a/lockfile/settings-checker/package.json +++ b/lockfile/settings-checker/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/lockfile.settings-checker", - "version": "1100.0.10", + "version": "1100.0.13", "description": "Utilities to check if lockfile settings are out-of-date", "keywords": [ "pnpm", diff --git a/lockfile/to-pnp/CHANGELOG.md b/lockfile/to-pnp/CHANGELOG.md index 9400a6d77b..57322f1655 100644 --- a/lockfile/to-pnp/CHANGELOG.md +++ b/lockfile/to-pnp/CHANGELOG.md @@ -1,5 +1,36 @@ # @pnpm/lockfile-to-pnp +## 1100.0.11 + +### Patch Changes + +- Updated dependencies [e55f4b5] +- Updated dependencies [35d2355] + - @pnpm/lockfile.utils@1100.0.10 + - @pnpm/types@1101.2.0 + - @pnpm/lockfile.fs@1100.1.2 + - @pnpm/deps.path@1100.0.5 + +## 1100.0.10 + +### Patch Changes + +- Updated dependencies [9cb48bb] +- Updated dependencies [64afc92] + - @pnpm/lockfile.fs@1100.1.1 + - @pnpm/types@1101.1.1 + - @pnpm/lockfile.utils@1100.0.9 + - @pnpm/deps.path@1100.0.4 + +## 1100.0.9 + +### Patch Changes + +- Updated dependencies [6e93f35] +- Updated dependencies [2a9bd89] + - @pnpm/lockfile.fs@1100.1.0 + - @pnpm/lockfile.utils@1100.0.8 + ## 1100.0.8 ### Patch Changes diff --git a/lockfile/to-pnp/package.json b/lockfile/to-pnp/package.json index eb5f278887..d6d5420f2e 100644 --- a/lockfile/to-pnp/package.json +++ b/lockfile/to-pnp/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/lockfile.to-pnp", - "version": "1100.0.8", + "version": "1100.0.11", "description": "Creates a Plug'n'Play file from a pnpm-lock.yaml", "keywords": [ "pnpm", diff --git a/lockfile/types/CHANGELOG.md b/lockfile/types/CHANGELOG.md index f915c0b20f..d19cf62f44 100644 --- a/lockfile/types/CHANGELOG.md +++ b/lockfile/types/CHANGELOG.md @@ -1,5 +1,30 @@ # @pnpm/lockfile-types +## 1100.0.8 + +### Patch Changes + +- Updated dependencies [35d2355] + - @pnpm/types@1101.2.0 + - @pnpm/resolving.resolver-base@1100.3.1 + +## 1100.0.7 + +### Patch Changes + +- Updated dependencies [1627943] +- Updated dependencies [64afc92] + - @pnpm/resolving.resolver-base@1100.3.0 + - @pnpm/types@1101.1.1 + +## 1100.0.6 + +### Patch Changes + +- Updated dependencies [4195766] +- Updated dependencies [31538bf] + - @pnpm/resolving.resolver-base@1100.2.0 + ## 1100.0.5 ### Patch Changes diff --git a/lockfile/types/package.json b/lockfile/types/package.json index d97006ad74..8ee44d1141 100644 --- a/lockfile/types/package.json +++ b/lockfile/types/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/lockfile.types", - "version": "1100.0.5", + "version": "1100.0.8", "description": "Types for the pnpm-lock.yaml lockfile", "keywords": [ "pnpm", diff --git a/lockfile/utils/CHANGELOG.md b/lockfile/utils/CHANGELOG.md index ecfc936f65..f7d13059e1 100644 --- a/lockfile/utils/CHANGELOG.md +++ b/lockfile/utils/CHANGELOG.md @@ -1,5 +1,39 @@ # @pnpm/lockfile-utils +## 1100.0.10 + +### Patch Changes + +- e55f4b5: Reject `pnpm-lock.yaml` entries whose remote tarball `resolution:` block is missing the `integrity` field. Previously the worker that extracts a downloaded tarball skipped hash verification when no integrity was supplied and minted a fresh one from the unverified bytes, so an attacker who could both alter the lockfile (e.g. via a pull request that strips `integrity:`) and serve modified content at the referenced tarball URL could install a tampered package without any error — including under `--frozen-lockfile`. pnpm now fails closed at lockfile-read time with `ERR_PNPM_MISSING_TARBALL_INTEGRITY`. Git-hosted tarballs (`gitHosted: true` or a URL on codeload.github.com / bitbucket.org / gitlab.com) and `file:` tarballs are exempt — the commit SHA in a git-host URL and the user-controlled local path already anchor the bytes. +- Updated dependencies [35d2355] + - @pnpm/types@1101.2.0 + - @pnpm/deps.path@1100.0.5 + - @pnpm/hooks.types@1100.0.9 + - @pnpm/lockfile.types@1100.0.8 + - @pnpm/resolving.resolver-base@1100.3.1 + +## 1100.0.9 + +### Patch Changes + +- Updated dependencies [1627943] +- Updated dependencies [64afc92] + - @pnpm/resolving.resolver-base@1100.3.0 + - @pnpm/types@1101.1.1 + - @pnpm/hooks.types@1100.0.8 + - @pnpm/lockfile.types@1100.0.7 + - @pnpm/deps.path@1100.0.4 + +## 1100.0.8 + +### Patch Changes + +- Updated dependencies [4195766] +- Updated dependencies [31538bf] + - @pnpm/resolving.resolver-base@1100.2.0 + - @pnpm/hooks.types@1100.0.7 + - @pnpm/lockfile.types@1100.0.6 + ## 1100.0.7 ### Patch Changes diff --git a/lockfile/utils/package.json b/lockfile/utils/package.json index c4d2020158..b034e3ee44 100644 --- a/lockfile/utils/package.json +++ b/lockfile/utils/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/lockfile.utils", - "version": "1100.0.7", + "version": "1100.0.10", "description": "Utils for dealing with pnpm-lock.yaml", "keywords": [ "pnpm", diff --git a/lockfile/utils/src/pkgSnapshotToResolution.ts b/lockfile/utils/src/pkgSnapshotToResolution.ts index 021b1369bb..5b210f43f6 100644 --- a/lockfile/utils/src/pkgSnapshotToResolution.ts +++ b/lockfile/utils/src/pkgSnapshotToResolution.ts @@ -1,22 +1,49 @@ import url from 'node:url' import * as dp from '@pnpm/deps.path' +import { PnpmError } from '@pnpm/error' import type { PackageSnapshot, TarballResolution } from '@pnpm/lockfile.types' import type { Resolution } from '@pnpm/resolving.resolver-base' import type { Registries } from '@pnpm/types' import getNpmTarballUrl from 'get-npm-tarball-url' import { nameVerFromPkgSnapshot } from './nameVerFromPkgSnapshot.js' +import { isGitHostedTarballUrl } from './toLockfileResolution.js' export function pkgSnapshotToResolution ( depPath: string, pkgSnapshot: PackageSnapshot, registries: Registries ): Resolution { + const resolution = pkgSnapshot.resolution as TarballResolution + // Tarball-shaped resolutions (no `type` field) must carry `integrity`, + // except where the URL itself anchors the bytes: + // - `file:` tarballs (local file on the user's machine; integrity + // adds nothing the user doesn't already control). + // - Git-hosted tarballs (URL contains the commit SHA; git's content- + // addressed model binds the bytes to the commit). The `gitHosted` + // flag may be absent on legacy lockfiles, so fall back to a URL + // match — same logic as `toLockfileResolution`. + // For any other tarball entry a missing integrity is what a tampered + // lockfile looks like: the worker would mint a fresh integrity from + // whatever bytes the URL returned, so we fail closed here. Pacquet + // enforces the same invariant via + // `pacquet_package_manager::missing_tarball_integrity`. if ( - Boolean((pkgSnapshot.resolution as TarballResolution).type) || - (pkgSnapshot.resolution as TarballResolution).tarball?.startsWith('file:') || - (pkgSnapshot.resolution as TarballResolution).gitHosted === true + resolution.type == null && + resolution.integrity == null && + !resolution.tarball?.startsWith('file:') && + !(resolution.gitHosted === true || (resolution.tarball != null && isGitHostedTarballUrl(resolution.tarball))) + ) { + throw new PnpmError('MISSING_TARBALL_INTEGRITY', + `Cannot install package "${depPath}": its lockfile entry has no "integrity" field, so pnpm cannot verify the downloaded tarball.`, + { hint: 'The lockfile may be corrupted or have been tampered with. Restore it from a trusted source, or delete it and re-run installation without --frozen-lockfile to regenerate.' } + ) + } + if ( + Boolean(resolution.type) || + resolution.tarball?.startsWith('file:') || + resolution.gitHosted === true ) { return pkgSnapshot.resolution as Resolution } diff --git a/lockfile/utils/src/toLockfileResolution.ts b/lockfile/utils/src/toLockfileResolution.ts index 324ae2ee9e..0a1348861d 100644 --- a/lockfile/utils/src/toLockfileResolution.ts +++ b/lockfile/utils/src/toLockfileResolution.ts @@ -74,7 +74,7 @@ function preservingGitHosted // Inlined to avoid pulling @pnpm/fetching.pick-fetcher into the lockfile-utils // dep graph. Used as a fallback when callers haven't pre-set the // `gitHosted` field on TarballResolution. -function isGitHostedTarballUrl (url: string): boolean { +export function isGitHostedTarballUrl (url: string): boolean { return ( url.startsWith('https://codeload.github.com/') || url.startsWith('https://bitbucket.org/') || diff --git a/lockfile/utils/test/pkgSnapshotToResolution.ts b/lockfile/utils/test/pkgSnapshotToResolution.ts index ca816e657f..2e0c9ebc8d 100644 --- a/lockfile/utils/test/pkgSnapshotToResolution.ts +++ b/lockfile/utils/test/pkgSnapshotToResolution.ts @@ -33,9 +33,11 @@ test('pkgSnapshotToResolution()', () => { expect(pkgSnapshotToResolution('@cdn.sheetjs.com/xlsx-0.18.9/xlsx-0.18.9.tgz', { resolution: { + integrity: 'sha512-CCCC', tarball: 'https://cdn.sheetjs.com/xlsx-0.18.9/xlsx-0.18.9.tgz', }, }, { default: 'https://registry.npmjs.org/' })).toEqual({ + integrity: 'sha512-CCCC', tarball: 'https://cdn.sheetjs.com/xlsx-0.18.9/xlsx-0.18.9.tgz', }) @@ -51,3 +53,60 @@ test('pkgSnapshotToResolution()', () => { tarball: 'file:test-package-1.0.0.tgz', }) }) + +test('pkgSnapshotToResolution() rejects a remote tarball resolution that has no integrity', () => { + // A tampered or malformed lockfile that strips the `integrity` field + // would otherwise let pnpm download the URL contents unchecked. The + // helper must fail closed so neither install path nor any read-only + // consumer (sbom, list, etc.) silently trusts the lockfile entry. + expect(() => pkgSnapshotToResolution('foo@1.0.0', { + resolution: { + tarball: 'https://registry.npmjs.org/foo/-/foo-1.0.0.tgz', + }, + }, { default: 'https://registry.npmjs.org/' })).toThrow(expect.objectContaining({ code: 'ERR_PNPM_MISSING_TARBALL_INTEGRITY' })) + + // A tarball URL on an arbitrary CDN (no `gitHosted` flag, no known git + // host pattern) is still a regular remote tarball — integrity required. + expect(() => pkgSnapshotToResolution('xlsx@https+++cdn.sheetjs.com+xlsx-0.18.9+xlsx-0.18.9.tgz', { + resolution: { + tarball: 'https://cdn.sheetjs.com/xlsx-0.18.9/xlsx-0.18.9.tgz', + }, + }, { default: 'https://registry.npmjs.org/' })).toThrow(expect.objectContaining({ code: 'ERR_PNPM_MISSING_TARBALL_INTEGRITY' })) +}) + +test('pkgSnapshotToResolution() allows git-hosted and file: tarballs to lack integrity', () => { + // Git-hosted tarballs are anchored by the commit SHA in their URL — + // pnpm's own install pipeline writes them without `integrity:` (see + // the `with-git-protocol-dep` fixture). Both the explicit + // `gitHosted: true` flag and a URL on a known git host must bypass + // the integrity check, matching the URL-fallback logic in + // `toLockfileResolution`. + expect(pkgSnapshotToResolution('foo@https+++github.com+foo+bar', { + resolution: { + tarball: 'https://codeload.github.com/foo/bar/tar.gz/abc1234', + gitHosted: true, + }, + }, { default: 'https://registry.npmjs.org/' })).toEqual({ + tarball: 'https://codeload.github.com/foo/bar/tar.gz/abc1234', + gitHosted: true, + }) + + expect(pkgSnapshotToResolution('is-negative@https+++codeload.github.com+kevva+is-negative+tar.gz+abc', { + resolution: { + tarball: 'https://codeload.github.com/kevva/is-negative/tar.gz/abc1234', + }, + }, { default: 'https://registry.npmjs.org/' })).toEqual({ + tarball: 'https://codeload.github.com/kevva/is-negative/tar.gz/abc1234', + }) + + // `file:` tarballs are local files; the user already controls the + // bytes, and the install pipeline may write them without integrity. + expect(pkgSnapshotToResolution('local-pkg@file:local-pkg-1.0.0.tgz', { + resolution: { + tarball: 'file:local-pkg-1.0.0.tgz', + }, + version: '1.0.0', + }, { default: 'https://registry.npmjs.org/' })).toEqual({ + tarball: 'file:local-pkg-1.0.0.tgz', + }) +}) diff --git a/lockfile/verification/CHANGELOG.md b/lockfile/verification/CHANGELOG.md index 92ca527364..69ac58f1ab 100644 --- a/lockfile/verification/CHANGELOG.md +++ b/lockfile/verification/CHANGELOG.md @@ -1,5 +1,47 @@ # @pnpm/lockfile.verification +## 1100.0.13 + +### Patch Changes + +- Updated dependencies [e55f4b5] +- Updated dependencies [35d2355] + - @pnpm/lockfile.utils@1100.0.10 + - @pnpm/types@1101.2.0 + - @pnpm/deps.path@1100.0.5 + - @pnpm/installing.context@1100.0.13 + - @pnpm/lockfile.types@1100.0.8 + - @pnpm/pkg-manifest.reader@1100.0.5 + - @pnpm/resolving.resolver-base@1100.3.1 + - @pnpm/crypto.hash@1100.0.1 + +## 1100.0.12 + +### Patch Changes + +- Updated dependencies [1627943] +- Updated dependencies [64afc92] + - @pnpm/resolving.resolver-base@1100.3.0 + - @pnpm/types@1101.1.1 + - @pnpm/installing.context@1100.0.12 + - @pnpm/lockfile.types@1100.0.7 + - @pnpm/lockfile.utils@1100.0.9 + - @pnpm/deps.path@1100.0.4 + - @pnpm/pkg-manifest.reader@1100.0.4 + - @pnpm/crypto.hash@1100.0.1 + +## 1100.0.11 + +### Patch Changes + +- Updated dependencies [4195766] +- Updated dependencies [31538bf] + - @pnpm/resolving.resolver-base@1100.2.0 + - @pnpm/installing.context@1100.0.11 + - @pnpm/lockfile.types@1100.0.6 + - @pnpm/lockfile.utils@1100.0.8 + - @pnpm/crypto.hash@1100.0.1 + ## 1100.0.10 ### Patch Changes diff --git a/lockfile/verification/package.json b/lockfile/verification/package.json index 3de39ec402..ecc8c3969a 100644 --- a/lockfile/verification/package.json +++ b/lockfile/verification/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/lockfile.verification", - "version": "1100.0.10", + "version": "1100.0.13", "description": "Checks a lockfile", "keywords": [ "pnpm", diff --git a/lockfile/walker/CHANGELOG.md b/lockfile/walker/CHANGELOG.md index b36fd198c5..be7e357ec4 100644 --- a/lockfile/walker/CHANGELOG.md +++ b/lockfile/walker/CHANGELOG.md @@ -1,5 +1,29 @@ # @pnpm/lockfile-walker +## 1100.0.8 + +### Patch Changes + +- Updated dependencies [35d2355] + - @pnpm/types@1101.2.0 + - @pnpm/deps.path@1100.0.5 + - @pnpm/lockfile.types@1100.0.8 + +## 1100.0.7 + +### Patch Changes + +- Updated dependencies [64afc92] + - @pnpm/types@1101.1.1 + - @pnpm/lockfile.types@1100.0.7 + - @pnpm/deps.path@1100.0.4 + +## 1100.0.6 + +### Patch Changes + +- @pnpm/lockfile.types@1100.0.6 + ## 1100.0.5 ### Patch Changes diff --git a/lockfile/walker/package.json b/lockfile/walker/package.json index dc5fa459e6..75f26db786 100644 --- a/lockfile/walker/package.json +++ b/lockfile/walker/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/lockfile.walker", - "version": "1100.0.5", + "version": "1100.0.8", "description": "Walk over all the dependencies in a lockfile", "keywords": [ "pnpm", diff --git a/modules-mounter/daemon/CHANGELOG.md b/modules-mounter/daemon/CHANGELOG.md index 04a684b67b..002ab78784 100644 --- a/modules-mounter/daemon/CHANGELOG.md +++ b/modules-mounter/daemon/CHANGELOG.md @@ -1,5 +1,59 @@ # @pnpm/mount-modules +## 1100.0.17 + +### Patch Changes + +- Updated dependencies [a23956e] +- Updated dependencies [e55f4b5] +- Updated dependencies [35d2355] + - @pnpm/config.reader@1101.4.1 + - @pnpm/lockfile.utils@1100.0.10 + - @pnpm/types@1101.2.0 + - @pnpm/lockfile.fs@1100.1.2 + - @pnpm/deps.path@1100.0.5 + - @pnpm/store.cafs@1100.1.7 + +## 1100.0.16 + +### Patch Changes + +- Updated dependencies [3b62f9d] +- Updated dependencies [212315d] + - @pnpm/config.reader@1101.4.0 + +## 1100.0.15 + +### Patch Changes + +- Updated dependencies [3687b0e] +- Updated dependencies [ced20cb] +- Updated dependencies [9cb48bb] +- Updated dependencies [d1b340f] +- Updated dependencies [64afc92] + - @pnpm/config.reader@1101.3.3 + - @pnpm/lockfile.fs@1100.1.1 + - @pnpm/types@1101.1.1 + - @pnpm/lockfile.utils@1100.0.9 + - @pnpm/deps.path@1100.0.4 + - @pnpm/store.cafs@1100.1.6 + +## 1100.0.14 + +### Patch Changes + +- Updated dependencies [020ac45] +- Updated dependencies [d3f8408] +- Updated dependencies [6e93f35] +- Updated dependencies [a62f959] +- Updated dependencies [ba2c884] +- Updated dependencies [2a9bd89] +- Updated dependencies [8df408c] + - @pnpm/config.reader@1101.3.2 + - @pnpm/lockfile.fs@1100.1.0 + - @pnpm/lockfile.utils@1100.0.8 + - @pnpm/store.cafs@1100.1.5 + ## 1100.0.13 ### Patch Changes diff --git a/modules-mounter/daemon/package.json b/modules-mounter/daemon/package.json index daf6cb56a6..9ed18665c7 100644 --- a/modules-mounter/daemon/package.json +++ b/modules-mounter/daemon/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/modules-mounter.daemon", - "version": "1100.0.13", + "version": "1100.0.17", "description": "Mounts a node_modules directory with FUSE", "keywords": [ "pnpm", diff --git a/network/auth-header/CHANGELOG.md b/network/auth-header/CHANGELOG.md index d0e9772bc1..d34d82642e 100644 --- a/network/auth-header/CHANGELOG.md +++ b/network/auth-header/CHANGELOG.md @@ -1,5 +1,29 @@ # @pnpm/network.auth-header +## 1101.0.0 + +### Major Changes + +- a23956e: Fix a credential disclosure issue where an unscoped `_authToken` (or `_auth`, or `username` + `_password`, or `tokenHelper`) defined in one source — `~/.npmrc`, `~/.config/pnpm/auth.ini`, a workspace `.npmrc`, CLI flags, etc. — would be sent as an `Authorization` header to whichever registry a different (potentially untrusted) source named. The same fix extends to client TLS credentials (`cert`, `key`) so they aren't presented to a registry their author didn't choose. + + pnpm now rewrites each unscoped per-registry setting (`_authToken`, `_auth`, `username`, `_password`, `tokenHelper`, `cert`, `key`) to its URL-scoped form at load time, using the `registry=` value declared in the same source (or the npmjs default registry if the source declares none). A later layer overriding `registry=` therefore cannot pull an unscoped credential along, because it is already pinned to the URL its author intended. `ca`/`cafile` are intentionally not rescoped — they're trust anchors, not credentials, and corporate MITM-proxy setups rely on them applying globally. + + Every rescope emits a deprecation warning telling the user where the setting was pinned and how to write it directly. npm has rejected unscoped credentials outright since `npm@9`, and pnpm intends to remove support in a future major release. To target a specific registry, write the setting URL-scoped (e.g. `//registry.example.com/:_authToken=...` or `//registry.example.com/:cert=...`). + + `@pnpm/network.auth-header`: removed the `defaultRegistry` parameter from `createGetAuthHeaderByURI` and `getAuthHeadersFromCreds`. Now that credentials are URL-scoped at load time, the merged `configByUri` never contains the empty-string "default registry" placeholder slot, so re-keying it onto the merged default registry is no longer needed. + +### Patch Changes + +- Updated dependencies [35d2355] + - @pnpm/types@1101.2.0 + +## 1100.0.3 + +### Patch Changes + +- Updated dependencies [64afc92] + - @pnpm/types@1101.1.1 + ## 1100.0.2 ### Patch Changes diff --git a/network/auth-header/package.json b/network/auth-header/package.json index 8e8f3a27e4..2ea8ef0a87 100644 --- a/network/auth-header/package.json +++ b/network/auth-header/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/network.auth-header", - "version": "1100.0.2", + "version": "1101.0.0", "description": "Gets the authorization header for the given URI", "keywords": [ "pnpm", diff --git a/network/auth-header/src/getAuthHeadersFromConfig.ts b/network/auth-header/src/getAuthHeadersFromConfig.ts index ec4aef9fa1..01390ddb76 100644 --- a/network/auth-header/src/getAuthHeadersFromConfig.ts +++ b/network/auth-header/src/getAuthHeadersFromConfig.ts @@ -4,24 +4,15 @@ import { PnpmError } from '@pnpm/error' import type { Creds, RegistryConfig, TokenHelper } from '@pnpm/types' export function getAuthHeadersFromCreds ( - configByUri: Record, - defaultRegistry: string + configByUri: Record ): Record { const authHeaderValueByURI: Record = {} for (const [uri, registryConfig] of Object.entries(configByUri)) { - if (uri === '') continue // default auth handled below const header = credsToHeader(registryConfig.creds) if (header) { authHeaderValueByURI[uri] = header } } - const defaultConfig = configByUri[''] - if (defaultConfig?.creds) { - const header = credsToHeader(defaultConfig.creds) - if (header) { - authHeaderValueByURI[defaultRegistry] = header - } - } return authHeaderValueByURI } diff --git a/network/auth-header/src/index.ts b/network/auth-header/src/index.ts index bdaf21ac85..8b2d6a80d5 100644 --- a/network/auth-header/src/index.ts +++ b/network/auth-header/src/index.ts @@ -5,11 +5,9 @@ import { getAuthHeadersFromCreds } from './getAuthHeadersFromConfig.js' import { removePort } from './helpers/removePort.js' export function createGetAuthHeaderByURI ( - configByUri: Record, - defaultRegistry?: string + configByUri: Record ): (uri: string) => string | undefined { - const registry = defaultRegistry ? nerfDart(defaultRegistry) : '//registry.npmjs.org/' - const authHeaders = getAuthHeadersFromCreds(configByUri, registry) + const authHeaders = getAuthHeadersFromCreds(configByUri) if (Object.keys(authHeaders).length === 0) return (uri: string) => basicAuth(new URL(uri)) return getAuthHeaderByURI.bind(null, authHeaders, getMaxParts(Object.keys(authHeaders))) } diff --git a/network/auth-header/test/getAuthHeaderByURI.ts b/network/auth-header/test/getAuthHeaderByURI.ts index 8f504b249a..08d99e66df 100644 --- a/network/auth-header/test/getAuthHeaderByURI.ts +++ b/network/auth-header/test/getAuthHeaderByURI.ts @@ -69,15 +69,6 @@ test('getAuthHeaderByURI() when the registry has pathnames', () => { expect(getAuthHeaderByURI('https://npm.pkg.github.com/pnpm/foo/-/foo-1.0.0.tgz')).toBe('Bearer abc123') }) -test('getAuthHeaderByURI() with default registry auth', () => { - const getAuthHeaderByURI = createGetAuthHeaderByURI( - { '': { creds: { authToken: 'default-token' } } }, - 'https://registry.npmjs.org/' - ) - expect(getAuthHeaderByURI('https://registry.npmjs.org/')).toBe('Bearer default-token') - expect(getAuthHeaderByURI('https://registry.npmjs.org/foo/-/foo-1.0.0.tgz')).toBe('Bearer default-token') -}) - test('getAuthHeaderByURI() with basic auth via basicAuth', () => { const getAuthHeaderByURI = createGetAuthHeaderByURI({ '//reg.com/': { creds: { basicAuth: { username: 'user', password: 'pass' } } }, diff --git a/network/auth-header/test/getAuthHeadersFromConfig.test.ts b/network/auth-header/test/getAuthHeadersFromConfig.test.ts index 67c5a6b253..87b74881b7 100644 --- a/network/auth-header/test/getAuthHeadersFromConfig.test.ts +++ b/network/auth-header/test/getAuthHeadersFromConfig.test.ts @@ -33,7 +33,7 @@ describe('getAuthHeadersFromCreds()', () => { const result = getAuthHeadersFromCreds({ '//registry.npmjs.org/': { creds: { authToken: 'abc123' } }, '//registry.hu/': { creds: { authToken: 'def456' } }, - }, '//registry.npmjs.org/') + }) expect(result).toStrictEqual({ '//registry.npmjs.org/': 'Bearer abc123', '//registry.hu/': 'Bearer def456', @@ -42,23 +42,15 @@ describe('getAuthHeadersFromCreds()', () => { it('should convert basicAuth to Basic header', () => { const result = getAuthHeadersFromCreds({ '//registry.foobar.eu/': { creds: { basicAuth: { username: 'foobar', password: 'foobar' } } }, - }, '//registry.npmjs.org/') + }) expect(result).toStrictEqual({ '//registry.foobar.eu/': 'Basic Zm9vYmFyOmZvb2Jhcg==', }) }) - it('should handle default registry auth (empty key)', () => { - const result = getAuthHeadersFromCreds({ - '': { creds: { authToken: 'default-token' } }, - }, '//reg.com/') - expect(result).toStrictEqual({ - '//reg.com/': 'Bearer default-token', - }) - }) it('should execute tokenHelper', () => { const result = getAuthHeadersFromCreds({ '//registry.foobar.eu/': { creds: { tokenHelper: [osTokenHelper[osFamily]] } }, - }, '//registry.npmjs.org/') + }) expect(result).toStrictEqual({ '//registry.foobar.eu/': 'Bearer token-from-spawn', }) @@ -66,7 +58,7 @@ describe('getAuthHeadersFromCreds()', () => { it('should prepend Bearer to raw token from tokenHelper', () => { const result = getAuthHeadersFromCreds({ '//registry.foobar.eu/': { creds: { tokenHelper: [osRawTokenHelper[osFamily]] } }, - }, '//registry.npmjs.org/') + }) expect(result).toStrictEqual({ '//registry.foobar.eu/': 'Bearer raw-token-no-scheme', }) @@ -74,15 +66,15 @@ describe('getAuthHeadersFromCreds()', () => { it('should throw an error if the token helper fails', () => { expect(() => getAuthHeadersFromCreds({ '//reg.com/': { creds: { tokenHelper: [osErrorTokenHelper[osFamily]] } }, - }, '//registry.npmjs.org/')).toThrow('Exit code') + })).toThrow('Exit code') }) it('should throw an error if the token helper returns an empty token', () => { expect(() => getAuthHeadersFromCreds({ '//reg.com/': { creds: { tokenHelper: [osEmptyTokenHelper[osFamily]] } }, - }, '//registry.npmjs.org/')).toThrow('returned an empty token') + })).toThrow('returned an empty token') }) it('should return empty object when no auth infos', () => { - const result = getAuthHeadersFromCreds({}, '//registry.npmjs.org/') + const result = getAuthHeadersFromCreds({}) expect(result).toStrictEqual({}) }) }) diff --git a/network/fetch/CHANGELOG.md b/network/fetch/CHANGELOG.md index bb65f304fe..cacc77f2c4 100644 --- a/network/fetch/CHANGELOG.md +++ b/network/fetch/CHANGELOG.md @@ -1,5 +1,28 @@ # @pnpm/fetch +## 1100.0.7 + +### Patch Changes + +- Updated dependencies [35d2355] + - @pnpm/types@1101.2.0 + - @pnpm/core-loggers@1100.1.2 + +## 1100.0.6 + +### Patch Changes + +- Updated dependencies [64afc92] + - @pnpm/types@1101.1.1 + - @pnpm/core-loggers@1100.1.1 + +## 1100.0.5 + +### Patch Changes + +- Updated dependencies [4a79336] + - @pnpm/core-loggers@1100.1.0 + ## 1100.0.4 ### Patch Changes diff --git a/network/fetch/package.json b/network/fetch/package.json index 10f498fbbd..4c99b5e01f 100644 --- a/network/fetch/package.json +++ b/network/fetch/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/network.fetch", - "version": "1100.0.4", + "version": "1100.0.7", "description": "Native fetch with retries", "keywords": [ "pnpm", diff --git a/object/property-path/CHANGELOG.md b/object/property-path/CHANGELOG.md index 436d22f357..43f19b2a83 100644 --- a/object/property-path/CHANGELOG.md +++ b/object/property-path/CHANGELOG.md @@ -1,5 +1,11 @@ # @pnpm/object.property-path +## 1100.1.0 + +### Minor Changes + +- d7da112: Implement `pnpm pkg` command natively, following `npm pkg` standards. + ## 1001.0.0 ### Major Changes diff --git a/object/property-path/package.json b/object/property-path/package.json index 003dd14671..65ce5d0a30 100644 --- a/object/property-path/package.json +++ b/object/property-path/package.json @@ -1,6 +1,6 @@ { "name": "@pnpm/object.property-path", - "version": "1100.0.0", + "version": "1100.1.0", "description": "Basic library to manipulate object property path which includes dots and subscriptions", "keywords": [ "pnpm", diff --git a/object/property-path/src/delete.ts b/object/property-path/src/delete.ts new file mode 100644 index 0000000000..c2224000e2 --- /dev/null +++ b/object/property-path/src/delete.ts @@ -0,0 +1,51 @@ +import { parsePropertyPath } from './parse.js' +import { rejectUnsafeKeys } from './unsafeKeys.js' + +type ObjectOrArray = Record | unknown[] + +/** + * Remove the value at a nested property path on {@link object}. + * + * No-op if the path does not resolve to an existing value. Array elements are + * removed via `splice` so no `null` hole is left behind. + * + * Throws on unsafe keys (`__proto__`, `constructor`, `prototype`) to prevent + * prototype pollution. + */ +export function deleteObjectValueByPropertyPath (object: ObjectOrArray, propertyPath: Iterable): void { + const path = Array.from(propertyPath) + if (path.length === 0) return + rejectUnsafeKeys(path) + + let obj: ObjectOrArray = object + for (let i = 0; i < path.length - 1; i++) { + const key = path[i] + if ( + typeof obj !== 'object' || + obj === null || + !Object.hasOwn(obj, key) || + (Array.isArray(obj) && typeof key !== 'number') + ) { + return + } + obj = (obj as Record)[key] as ObjectOrArray + } + + if (typeof obj !== 'object' || obj === null) return + + const lastKey = path[path.length - 1] + if (Array.isArray(obj) && isArrayIndex(lastKey)) { + obj.splice(Number(lastKey), 1) + return + } + delete (obj as Record)[lastKey] +} + +export const deleteObjectValueByPropertyPathString = (object: ObjectOrArray, propertyPath: string): void => + deleteObjectValueByPropertyPath(object, parsePropertyPath(propertyPath)) + +function isArrayIndex (key: string | number): boolean { + if (typeof key === 'number') return Number.isInteger(key) && key >= 0 + if (!/^(?:0|[1-9]\d*)$/.test(key)) return false + return Number.isSafeInteger(Number(key)) +} diff --git a/object/property-path/src/index.ts b/object/property-path/src/index.ts index b2ca238e12..b86a274a7c 100644 --- a/object/property-path/src/index.ts +++ b/object/property-path/src/index.ts @@ -1,3 +1,6 @@ +export * from './delete.js' export * from './get.js' export * from './parse.js' +export * from './set.js' export * from './token/index.js' +export * from './unsafeKeys.js' diff --git a/object/property-path/src/set.ts b/object/property-path/src/set.ts new file mode 100644 index 0000000000..8048c486e9 --- /dev/null +++ b/object/property-path/src/set.ts @@ -0,0 +1,67 @@ +import { PnpmError } from '@pnpm/error' + +import { parsePropertyPath } from './parse.js' +import { rejectUnsafeKeys } from './unsafeKeys.js' + +type ObjectOrArray = Record | unknown[] + +export class EmptyPropertyPathError extends PnpmError { + constructor () { + super('EMPTY_PROPERTY_PATH', 'Cannot set a value with an empty property path') + } +} + +/** + * Set the value at a nested property path on {@link object}. + * + * Creates intermediate objects or arrays as needed. If an intermediate node + * exists but its shape disagrees with the next path segment (a scalar where a + * container is needed, an array where an object is needed, or vice versa), it + * is replaced with a fresh container so the write is persisted in a shape that + * round-trips through `JSON.stringify`. + * + * Throws on unsafe keys (`__proto__`, `constructor`, `prototype`) to prevent + * prototype pollution. + */ +export function setObjectValueByPropertyPath (object: ObjectOrArray, propertyPath: Iterable, value: unknown): void { + const path = Array.from(propertyPath) + if (path.length === 0) throw new EmptyPropertyPathError() + rejectUnsafeKeys(path) + + let obj: ObjectOrArray = object + for (let i = 0; i < path.length - 1; i++) { + const key = path[i] + const current = (obj as Record)[key] + const needsArray = typeof path[i + 1] === 'number' + const isContainer = typeof current === 'object' && current !== null + if (!isContainer || Array.isArray(current) !== needsArray) { + const replacement: ObjectOrArray = needsArray ? [] : {} + defineOwnProperty(obj, key, replacement) + obj = replacement + } else { + obj = current as ObjectOrArray + } + } + + defineOwnProperty(obj, path[path.length - 1], value) +} + +/** + * Set a value as an own enumerable, writable, configurable property. + * + * Using `Object.defineProperty` rather than bracket assignment ensures that + * even if a `__proto__`-like key slipped past {@link rejectUnsafeKeys}, the + * write would create an own property instead of invoking the prototype + * setter, so this assignment site cannot be a prototype-pollution sink. + */ +function defineOwnProperty (obj: ObjectOrArray, key: string | number, value: unknown): void { + Object.defineProperty(obj, key, { + value, + writable: true, + enumerable: true, + configurable: true, + }) +} + +export const setObjectValueByPropertyPathString = (object: ObjectOrArray, propertyPath: string, value: unknown): void => + setObjectValueByPropertyPath(object, parsePropertyPath(propertyPath), value) diff --git a/object/property-path/src/unsafeKeys.ts b/object/property-path/src/unsafeKeys.ts new file mode 100644 index 0000000000..6ea11b35b2 --- /dev/null +++ b/object/property-path/src/unsafeKeys.ts @@ -0,0 +1,23 @@ +import { PnpmError } from '@pnpm/error' + +const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype']) + +export class UnsafePropertyPathKeyError extends PnpmError { + readonly key: string + constructor (key: string) { + super('UNSAFE_PROPERTY_PATH_KEY', `Key "${key}" is not allowed in a property path`) + this.key = key + } +} + +/** + * Throw if the property path contains a key that could trigger prototype + * pollution when used to mutate an object (e.g. via {@link setObjectValueByPropertyPath}). + */ +export function rejectUnsafeKeys (propertyPath: Iterable): void { + for (const segment of propertyPath) { + if (typeof segment === 'string' && UNSAFE_KEYS.has(segment)) { + throw new UnsafePropertyPathKeyError(segment) + } + } +} diff --git a/object/property-path/test/delete.test.ts b/object/property-path/test/delete.test.ts new file mode 100644 index 0000000000..08d5cde43e --- /dev/null +++ b/object/property-path/test/delete.test.ts @@ -0,0 +1,64 @@ +import { expect, test } from '@jest/globals' + +import { + deleteObjectValueByPropertyPathString, + type UnsafePropertyPathKeyError, +} from '../src/index.js' + +test('deletes a top-level key', () => { + const obj: Record = { name: 'foo', version: '1.0.0' } + deleteObjectValueByPropertyPathString(obj, 'version') + expect(obj).toEqual({ name: 'foo' }) +}) + +test('deletes a nested key', () => { + const obj: Record = { scripts: { build: 'tsc', test: 'jest' } } + deleteObjectValueByPropertyPathString(obj, 'scripts.test') + expect(obj).toEqual({ scripts: { build: 'tsc' } }) +}) + +test('removes an array element without leaving a hole', () => { + const obj: Record = { contributors: [{ name: 'Alice' }, { name: 'Bob' }] } + deleteObjectValueByPropertyPathString(obj, 'contributors[0]') + expect(obj).toEqual({ contributors: [{ name: 'Bob' }] }) +}) + +test('removes an array element by string index without leaving a hole', () => { + const obj: Record = { contributors: [{ name: 'Alice' }, { name: 'Bob' }] } + deleteObjectValueByPropertyPathString(obj, 'contributors["0"]') + expect(obj).toEqual({ contributors: [{ name: 'Bob' }] }) +}) + +test('no-op on a missing path', () => { + const obj: Record = { name: 'foo' } + deleteObjectValueByPropertyPathString(obj, 'scripts.test') + expect(obj).toEqual({ name: 'foo' }) +}) + +test('no-op when an intermediate value is null', () => { + const obj: Record = { a: null } + deleteObjectValueByPropertyPathString(obj, 'a.b') + expect(obj).toEqual({ a: null }) +}) + +test('no-op when an intermediate value is a scalar', () => { + const obj: Record = { a: 'scalar' } + deleteObjectValueByPropertyPathString(obj, 'a.b') + expect(obj).toEqual({ a: 'scalar' }) +}) + +test('no-op on an empty property path', () => { + const obj: Record = { name: 'foo' } + deleteObjectValueByPropertyPathString(obj, '') + expect(obj).toEqual({ name: 'foo' }) +}) + +test('rejects __proto__, constructor and prototype keys', () => { + for (const unsafe of ['__proto__', 'constructor', 'prototype']) { + expect(() => deleteObjectValueByPropertyPathString({}, `${unsafe}.foo`)) + .toThrow(expect.objectContaining({ + code: 'ERR_PNPM_UNSAFE_PROPERTY_PATH_KEY', + key: unsafe, + } as Partial)) + } +}) diff --git a/object/property-path/test/set.test.ts b/object/property-path/test/set.test.ts new file mode 100644 index 0000000000..a453f0486f --- /dev/null +++ b/object/property-path/test/set.test.ts @@ -0,0 +1,71 @@ +import { expect, test } from '@jest/globals' + +import { + EmptyPropertyPathError, + setObjectValueByPropertyPathString, + UnsafePropertyPathKeyError, +} from '../src/index.js' + +test('sets a top-level key', () => { + const obj: Record = { name: 'foo' } + setObjectValueByPropertyPathString(obj, 'version', '1.0.0') + expect(obj).toEqual({ name: 'foo', version: '1.0.0' }) +}) + +test('creates missing intermediate objects', () => { + const obj: Record = {} + setObjectValueByPropertyPathString(obj, 'scripts.build', 'tsc') + expect(obj).toEqual({ scripts: { build: 'tsc' } }) +}) + +test('creates an array when the next segment is numeric', () => { + const obj: Record = {} + setObjectValueByPropertyPathString(obj, 'contributors[0].name', 'Alice') + expect(obj).toEqual({ contributors: [{ name: 'Alice' }] }) +}) + +test('replaces a scalar intermediate with the right container', () => { + const obj: Record = { scripts: 'echo hi' } + setObjectValueByPropertyPathString(obj, 'scripts.test', 'vitest') + expect(obj).toEqual({ scripts: { test: 'vitest' } }) +}) + +test('replaces a scalar intermediate with an array when the next segment is numeric', () => { + const obj: Record = { keywords: 'oops' } + setObjectValueByPropertyPathString(obj, 'keywords[0]', 'pnpm') + expect(obj).toEqual({ keywords: ['pnpm'] }) +}) + +test('replaces an array intermediate with an object when the next segment is a string', () => { + const obj: Record = { contributors: [] } + setObjectValueByPropertyPathString(obj, 'contributors.name', 'Alice') + expect(obj).toEqual({ contributors: { name: 'Alice' } }) +}) + +test('replaces an object intermediate with an array when the next segment is numeric', () => { + const obj: Record = { contributors: { x: 1 } } + setObjectValueByPropertyPathString(obj, 'contributors[0]', 'Alice') + expect(obj).toEqual({ contributors: ['Alice'] }) +}) + +test('overwrites an existing value', () => { + const obj: Record = { scripts: { build: 'old' } } + setObjectValueByPropertyPathString(obj, 'scripts.build', 'new') + expect(obj).toEqual({ scripts: { build: 'new' } }) +}) + +test('rejects __proto__, constructor and prototype keys', () => { + for (const unsafe of ['__proto__', 'constructor', 'prototype']) { + expect(() => setObjectValueByPropertyPathString({}, `${unsafe}.polluted`, true)) + .toThrow(expect.objectContaining({ + code: 'ERR_PNPM_UNSAFE_PROPERTY_PATH_KEY', + key: unsafe, + } as Partial)) + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(({} as any).polluted).toBeUndefined() +}) + +test('throws on empty property path', () => { + expect(() => setObjectValueByPropertyPathString({}, '', 'value')).toThrow(EmptyPropertyPathError) +}) diff --git a/package.json b/package.json index 1361924829..f8008083b0 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "test-branch": "pn pretest && pn lint && git remote set-branches --add origin main && git fetch origin main && pn test-pkgs-branch", "ci:test-branch": "pn prepare-fixtures && pn test-pkgs-branch", "test-pkgs-branch": "pn remove-temp-dir && pn --workspace-concurrency=1 --filter=...[origin/main] --no-sort --if-present .test", - "compile-only": "tsgo --build workspace/workspace-manifest-reader workspace/projects-reader && pnx node@runtime:24.6.0 __utils__/scripts/src/typecheck-only.ts && pn -F=pnpm compile", + "compile-only": "tsgo --build workspace/workspace-manifest-reader workspace/projects-reader && pnx node@runtime:26.0.0 __utils__/scripts/src/typecheck-only.ts && pn -F=pnpm compile", "compile": "pn compile-only && pn update-manifests", "build:pacquet": "cargo build --release --bin pacquet", "make-lcov": "shx mkdir -p coverage && lcov-result-merger './packages/*/coverage/lcov.info' 'coverage/lcov.info'", @@ -55,19 +55,18 @@ "lcov-result-merger": "catalog:", "rimraf": "catalog:", "shx": "catalog:", - "typescript": "catalog:", - "verdaccio": "catalog:" + "typescript": "catalog:" }, - "packageManager": "pnpm@11.1.1", + "packageManager": "pnpm@11.3.0", "devEngines": { "packageManager": { "name": "pnpm", - "version": "11.1.1", + "version": "11.3.0", "onFail": "download" }, "runtime": { "name": "node", - "version": "24.6.0", + "version": "26.0.0", "onFail": "download" } } diff --git a/pacquet/AGENTS.md b/pacquet/AGENTS.md index c285e2d8ec..126fccc48d 100644 --- a/pacquet/AGENTS.md +++ b/pacquet/AGENTS.md @@ -42,6 +42,22 @@ Before writing code for a feature, bug fix, or behavior change: [Reporter / log events](./CODE_STYLE_GUIDE.md#reporter--log-events) in the style guide for the convention (channel mapping, threading `R: Reporter`, emit-site placement, recording-fake tests). +7. **Prefer real fixtures; reach for the dependency-injection seam + only when they can't cover the branch.** Most happy paths and + error paths should be tested with a `tempfile::TempDir`, the + mocked registry, or an integration test that spawns the actual + binary. Use the DI seam — a capability trait on the `Host` + provider, threaded as `Sys: ` — only for branches a real + fixture can't reach portably: filesystem error kinds + (`PermissionDenied`, `ENOSPC`, …), deterministic time, shared + process-global state a test would otherwise mutate + (`env::set_var`, `set_current_dir`, the umask, …), or the + external-service happy paths in features like `pnpm login` (2FA) + and `pnpm publish` (OIDC / provenance) when those land. See + [Dependency injection for tests](./CODE_STYLE_GUIDE.md#dependency-injection-for-tests) + in the style guide for the gating rule, the names (`Sys`, `Host`, + `Fs*`, `Clock`, `EnvVar`, …), the eight principles, and the + `modules-yaml` worked example. If the pnpm behavior is unclear or looks wrong, stop and ask the user rather than guessing. @@ -156,8 +172,8 @@ by crate or name — see below). - `just ready` — run the same checks CI runs (typos, fmt, check, test, lint). Run this before declaring a task complete. - `just test` — `cargo nextest run`. -- `just lint` — `cargo clippy --locked -- --deny warnings`. -- `just check` — `cargo check --locked`. +- `just lint` — `cargo clippy --locked --workspace --all-targets -- --deny warnings`. +- `just check` — `cargo check --locked --workspace --all-targets`. - `just fmt` — `cargo fmt` + `taplo format`. - `just cli -- ` — run the pacquet binary. - `just registry-mock ` — manage the mock registry used by tests. @@ -190,6 +206,48 @@ Warnings are errors (`--deny warnings` in lint). Do not silence them with regression. Consult it before adding ported tests, and update its checkboxes as items land. +### No "tolerant" tests for missing tools + +Tests must not be tolerant of a missing build / runtime environment by +silently `return`-ing early when a tool isn't found. Patterns like: + +```rust +fn skip_if_no_git() -> bool { + if std::process::Command::new("git").arg("--version").output().is_err() { + eprintln!("skipping: `git` not on PATH"); + return true; + } + false +} + +#[test] +fn my_test() { + if skip_if_no_git() { + return; + } + // ... +} +``` + +are forbidden. If the test needs a tool, just call into it and let the +existing `.unwrap()` / `.expect(...)` panic when the tool is absent — a +failing test in an under-provisioned environment is the correct signal. +Tolerance defeats the purpose of testing: if the environment really +doesn't have the required tools, that's the *environment's* fault and it +needs to be fixed. + +This applies in particular to `git`, `node`, and `npm` — git is ubiquitous +on developer machines, and Node.js is a documented prerequisite for +building pnpm. There is no realistic environment in which pacquet's tests +should run *and* these tools should be absent. + +The only marginally acceptable exception is platform-locked tools — APIs +or binaries that exist on one OS but not another. Even then, prefer +`#[cfg_attr(target_os = "windows", ignore = "...")]` (or the matching +`#[cfg(unix)]` gate already used in this crate for `/bin/sh` shims) over a +runtime probe-and-skip helper. The gate is visible to `cargo test` and +shows up in the test report; a silent `return` does not. + ### Running tests narrowly Running the full suite is slow. While iterating, target what you're working @@ -229,6 +287,19 @@ Run `just ready` (full suite) before handing the PR off. re-exports such as `pub use submodule::*;` in a `lib.rs`. See the "No star imports" section in `CODE_STYLE_GUIDE.md`. +### Comments + +Same baseline as [`../AGENTS.md`](../AGENTS.md#comments): write code that explains itself; comments are for the non-obvious *why*, not a translation of the *what*. + +Rust-specific defaults: + +- **Doc comments (`///`, `//!`) document the contract.** Preconditions, postconditions, panics, the reason the function exists. They are not a re-narration of the body. +- **Do not restate at call sites what the callee's doc comment already says.** If `///` on the function says "no-op when …", the caller should not repeat that. Update the doc once; let every call site benefit. +- **Tests are documentation. Do not duplicate them in prose.** If a behavioral scenario, edge case, failure mode, or worked example is already captured by a test (its name, its setup, its assertions), do not also narrate it in the doc comment on the implementation. The doc comment should state the contract once; the test demonstrates the behavior. The same applies in reverse: a test's own doc comment should not re-explain what the asserts already say, only the *why* if it is not obvious. +- **`// SAFETY:`, `// TODO:`, and similar prefixes are the exception.** They signal hidden invariants or known follow-ups that a reader cannot recover from the code alone. + +Prefer renaming, restructuring, or extracting a helper over leaving a comment. Reach for prose only when the right names and types genuinely cannot carry the information. + ### Preserve existing method chains When editing existing code, do not break a method chain (including `pipe-trait` diff --git a/pacquet/CODE_STYLE_GUIDE.md b/pacquet/CODE_STYLE_GUIDE.md index 9d20b8f02f..83ce6d2752 100644 --- a/pacquet/CODE_STYLE_GUIDE.md +++ b/pacquet/CODE_STYLE_GUIDE.md @@ -46,7 +46,7 @@ Follow [the Rust API guidelines](https://rust-lang.github.io/api-guidelines/nami ### Module Organization -- Use the flat file pattern (`module.rs`) rather than `module/mod.rs` for submodules. Enforced by [`perfectionist::flat_module_pattern`](https://github.com/KSXGitHub/perfectionist). +- Use the flat file pattern (`module.rs`) rather than `module/mod.rs` for submodules. Enforced by [`perfectionist::flat_module_pattern`](https://github.com/KSXGitHub/perfectionist/blob/0.0.0-rc.15/rules/flat_module_pattern.md). - List `pub mod` declarations first, then `pub use` re-exports, then private imports and items. - Use `pub use` to re-export key types at the module level for convenience. @@ -114,102 +114,15 @@ pub use load_lockfile::*; ### Generic Parameter Naming -Use **descriptive names** for type parameters, not single letters: - -- `Size`, `Name`, `Manifest`, `Store`, `Reporter` - -Single-letter generics are acceptable only in very short, self-contained trait impls. +Use descriptive names for type parameters (`Size`, `Name`, `Manifest`, `Store`, `Reporter`) instead of single letters. Enforced by [`perfectionist::single_letter_generic`](https://github.com/KSXGitHub/perfectionist/blob/0.0.0-rc.15/rules/single_letter_generic.md), which exempts short self-contained trait impls. ### Variable and Closure Parameter Naming -Use **descriptive names** for variables and closure parameters by default. Single-letter names are permitted only in the specific cases listed below. +Use descriptive names for variables and closure parameters. Single letters are accepted only where the rules' default allowlists permit them: `n`/`f`/`i`/`j`/`k` for their conventional roles, the `sort_by` / `sort_by_key` / `min_by` / `max_by` / `fold` callback shape, single-expression closure bodies, and `let` bindings in `#[cfg(test)]` code. Multi-line closure bodies and non-test `let` bindings are flagged. Enforced by: -#### When single-letter names are allowed - -- **Comparison closures:** `|a, b|` in `sort_by`, `cmp`, or similar two-argument comparison callbacks. This is idiomatic Rust. - - ```rust - packages.sort_by(|a, b| a.name.cmp(&b.name)); - ``` - -- **Conventional single-letter names:** `n` for a natural number such as an unsigned integer or count, `f` for a `fmt::Formatter`, and similar well-established conventions from math or the Rust standard library. Note: for indices, use `index`, or `*_index` such as `row_index`, not `n`. For `i`/`j`/`k`, see the dedicated rule below. - - ```rust - fn with_capacity(n: usize) -> Self { todo!() } - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { todo!() } - ``` - -- **Index variables (`i`, `j`, `k`):** These may only be used in two contexts: short closures, and index-based loops or iterations. The latter is rare in Rust. In all other cases, use `index` or `*_index`. - - ```rust - // OK: short closure - rows.zip(cols).map(|(i, j)| matrix[i][j]) - - // OK: index-based loop - for i in 0..len { /* ... */ } - - // Bad: use a descriptive name instead - let i = items.iter().position(|item| item.is_active()).unwrap(); - ``` - -- **Trivial single-expression closures:** A closure whose body is a single field access, method call, or wrapper may use a single letter when the type and purpose are obvious from context. - - ```rust - .pipe(|x| vec![x]) - ``` - -- **Fold accumulators:** `acc` for the accumulator and a single letter for the element in trivial folds. - - ```rust - .fold(PathBuf::new(), |acc, x| acc.join(x)) - ``` - -- **Test fixtures:** `let a`, `let b`, `let c` for interchangeable specimens with identical roles in equality or comparison tests. Do not use single letters when the variables have distinct roles; use `actual`/`expected` or similar descriptive names instead. - - ```rust - let a = vec![3, 1, 2].into_iter().collect::>(); - let b = vec![2, 3, 1].into_iter().collect::>(); - assert_eq!(a, b); - ``` - -#### When single-letter names are NOT allowed - -- **Multi-line functions and closures:** Use a descriptive name when a function or closure body spans multiple lines. Examples include a body that contains a `let` binding followed by another expression, or a body with multiple chained operations. - - ```rust - // Good - .map(|package| { - let manifest = package.manifest()?; - install(&manifest) - }) - - // Bad - .map(|p| { - let manifest = p.manifest()?; - install(&manifest) - }) - ``` - -- **`let` bindings in non-test code:** Always use descriptive names. - - ```rust - // Good - let manifest = package.manifest()?; - // Bad - let m = package.manifest()?; - ``` - -- **Function and method parameters:** Always use descriptive names, except for the conventional single-letter names listed above, such as `n` and `f`. - -- **Closures with non-obvious context:** When the type or purpose is not immediately clear from the surrounding method chain, use a descriptive name. - - ```rust - // Good: descriptive name makes the closure self-documenting - .filter(|entry| entry.is_published()) - - // Bad: reader must look up what .filter receives - .filter(|x| x.is_published()) - ``` +- [`perfectionist::single_letter_function_param`](https://github.com/KSXGitHub/perfectionist/blob/0.0.0-rc.15/rules/single_letter_function_param.md) +- [`perfectionist::single_letter_closure_param`](https://github.com/KSXGitHub/perfectionist/blob/0.0.0-rc.15/rules/single_letter_closure_param.md) +- [`perfectionist::single_letter_let_binding`](https://github.com/KSXGitHub/perfectionist/blob/0.0.0-rc.15/rules/single_letter_let_binding.md) ### When to use [owned] parameter? When to use [borrowed] parameter? @@ -589,57 +502,199 @@ Do not flatten the tests into a sibling file such as `src/foo_tests.rs`, and do ### Cloning `Arc` and `Rc` -Prefer using `Arc::clone` or `Rc::clone` to vague `.clone()` or `Clone::clone`. +Prefer `Arc::clone(&x)` / `Rc::clone(&x)` over `x.clone()` for reference-counted types. The qualified form makes the O(1) refcount bump visible at the call site and fails to compile if a refactor changes the binding's type to something whose `Clone` is an arbitrarily expensive deep copy. Enforced by [`clippy::clone_on_ref_ptr`](https://rust-lang.github.io/rust-clippy/master/index.html#clone_on_ref_ptr), wired into `[workspace.lints.clippy]` in the root `Cargo.toml`. -**Error resistance:** Explicitly specifying the cloned type would avoid accidentally cloning the wrong type. As seen below: +### Reading process state + +Library code should rarely call `std::env::var`, `std::env::var_os`, or `std::env::current_dir` directly. Process state belongs to the running process, not to the operation a function performs, so reaching for it couples the logic to whichever shell invoked pacquet and hides the dependency from the caller. Prefer one of: + +- **Take the value as a parameter.** A function that needs a path declares `path: &Path`; one that needs an env-var value takes it as a `&str` argument or an `Option`. +- **Thread the read through the DI seam.** Declare `Sys: EnvVar` (or `EnvVarOs`, `GetHomeDir`, `GetCurrentDir`, …) and call `Sys::var(name)` / `Sys::var_os(name)` / `Sys::home_dir()` / `Sys::current_dir()`. See [Dependency injection for tests](#dependency-injection-for-tests) for the convention. This is how `Config::current` resolves the home dir and the `NPM_CONFIG_WORKSPACE_DIR` lookup, and how `crates/workspace`'s `find_workspace_dir_from_env_with` resolves its env-var lookup — both let tests drive the read without touching the process environment. + +The narrow case where a direct call is acceptable is computing the default of a `Config` field that has no caller to take the value from — `default_store_dir`, `default_modules_dir`, and `default_virtual_store_dir` are the canonical examples. Even there, route the lookup through the DI seam: write the helper as `default_store_dir()` and inline the production composition at the `SmartDefault` site (`#[default(_code = "default_store_dir::()")]`), so the fake-`Sys` test path and the production `Host` path resolve through the same generic. The other accepted boundary reads are program-entry knobs (`RAYON_NUM_THREADS` in `crates/cli`, `TRACE` in `crates/diagnostics`) and the lifecycle-script env snapshot in `crates/executor` and `crates/git-fetcher`, which has to forward the parent environment to spawned children verbatim. If new library code seems to need `env::var` / `env::current_dir` for any other reason, the answer is almost always a `&Path` (or equivalent) parameter, not a process-state read. + +### Dependency injection for tests + +Side-effecting code — filesystem access, environment variables, network calls, time, process state — has two testing routes. **The default route is a real fixture:** a `tempfile::TempDir` for filesystem work, the mocked registry (`just registry-mock`) for HTTP, an integration test that spawns the actual pacquet binary in a scratch directory for end-to-end flows. Real fixtures keep tests close to what users see and scale with the codebase without per-call-site plumbing; they are the right tool everywhere except the cases enumerated below. + +The dependency-injection seam described below is the **narrow second route**. Reach for it only when one of the following applies: + +- **Filesystem error branches the host OS won't reproduce portably.** `PermissionDenied`, `ENOSPC`, a directory that disappears mid-walk, a chmod that fails after the file exists — provoking these on real disks is platform-specific, racy, or both. A fake that returns the exact `io::ErrorKind` is the only portable way to drive the branch. +- **Deterministic time.** Asserting that `prunedAt` equals a specific HTTP-date (RFC 7231 IMF-fixdate, what `httpdate::fmt_http_date` emits), or that a throttled emitter fires on the second sample, needs the clock to be a known value. The real `SystemTime::now` makes those assertions flaky. +- **Shared process-global state that tests would otherwise mutate.** When a branch depends on a single per-process slot — environment variables, the current working directory, the umask, signal handlers, the global allocator — the only way to exercise it without DI is to write to that slot, and the write is observed by every other test in the same process. A serialisation lock (the `EnvGuard` pattern that pnpm/pacquet#343 + pnpm/pnpm#11718 retired from `default_store_dir`) restores correctness only by forcing the affected tests to run single-threaded, and leaves an `unsafe { env::set_var(...) }` block at the call site. A capability-trait fake keeps the read deterministic and the mutation contained to the test that needs it, so nextest's in-process parallelism stays useful and `unsafe` stays out of test code. The reverse follows too: a real-fixture happy path should not be promoted to DI just because it crosses a process-global slot — only the *mutation* side of the slot is the problem; reading whatever the shell already has is fine. +- **External-service happy paths that can't be staged in CI.** Upstream pnpm has features whose *normal* flow depends on real external systems — `pnpm login`'s 2FA prompt round-trip, the OIDC token exchange and provenance attestation in `pnpm publish`, and similar — where the happy path itself is what needs faking, not just the error path. When pacquet ports those features, DI is the right tool for their tests too. (As of writing, these are not yet ported; this exception is documented so the convention is in place when they land.) +- **Unreachable-by-design preconditions.** When a function declares a capability bound but a specific test exercises a branch that never reaches that capability, the fake satisfies the bound with `unreachable!` and documents the precondition. See the worked example below. + +A function that takes a `` generic but is only ever exercised via real fixtures is a smell — either the DI branches are missing coverage, or the generic is over-design. Either add the tests that justify the seam, or drop the generic and let the real fixture cover everything. + +The rest of this section is the convention that applies *when DI is the right tool*. + +#### Names + +- The generic type parameter is named **`Sys`** — short for "system seam," the slot in the function signature that selects between the real OS and the test fake. A single short name makes a generic call site instantly recognisable as the DI seam. +- The production provider struct is named **`Host`** — unqualified, because the production implementation is the default. Fakes carry behaviour-based names that describe what they do (`FailingRead`, `EmptyRead`, `PermissionDenied`, `FakeHostName`), not what category of thing they are. +- Capability traits use the form ``: filesystem capabilities are `Fs*` (`FsReadToString`, `FsCreateDirAll`, `FsWrite`, `FsReadDir`, `FsWalkFiles`, `FsSetExecutable`, `FsEnsureExecutableBits`); environment-variable lookup is `EnvVar`; clock reads are `Clock`; hostname lookup is `GetHostName`. The domain prefix lets a reader of a generic bound see which side effect the function reaches for without chasing definitions. Method names mirror their `std` equivalents so the trait is a thin seam over `std::fs::*` / `std::env::var` / `SystemTime::now`, not a re-imagining. + +#### Eight principles + +1. **Single-purpose traits.** Each capability gets its own trait — one method per side effect, no umbrella trait that bundles `read`, `write`, `create_dir_all` into one bag. A function then binds only the capabilities it actually consumes, and a test fake implements only the methods the function under test exercises. + +2. **One generic parameter with multiple bounds.** Compose bounds on a single `Sys`, never introduce a second type parameter per capability: + + ```rust + // Good: one parameter, composed bounds + pub fn read_modules_manifest(modules_dir: &Path) -> Result<...> + where + Sys: FsReadToString + Clock, + { /* ... */ } + + // Bad: a parameter per capability — every call site has to satisfy two slots + pub fn read_modules_manifest(modules_dir: &Path) -> Result<...> + where Fs: FsReadToString, C: Clock { /* ... */ } + ``` + + One parameter keeps turbofish call sites short (`read_modules_manifest::(dir)`) and makes the fake's job obvious: implement every trait in the bound list, no more. + +3. **Static methods, not `&self`.** Capability methods are associated functions (no `&self` receiver). The provider is a unit struct that carries no data: + + ```rust + pub trait FsReadToString { + fn read_to_string(path: &Path) -> io::Result; + } + + pub struct Host; + impl FsReadToString for Host { + fn read_to_string(path: &Path) -> io::Result { fs::read_to_string(path) } + } + ``` + + Stateful fakes (a fake clock that returns a fixed `SystemTime`, a recording fake that captures every call) store their state in an interior-mutable `static` declared inside the `#[test]` body, so the trait shape doesn't have to change to accommodate state. This keeps the production impl free of `&self` plumbing the test-only fake would otherwise force on it. + +4. **Associated types for data operations.** When a capability operates over a domain data type, expose the data type as an associated type rather than threading an instance through every call. The provider chooses the concrete type, and fakes can pick a stub-friendly stand-in. + +5. **Capability traits on the implementor.** `impl FsReadToString for Host` lives on the provider, not on the data type. The data types stay free of test-shim conditional impls; the seam is the provider. + +6. **Domain-neutral provider, domain-scoped traits.** The generic is `Sys`, the production type is `Host` (or whatever your crate exports as its provider), and the trait names carry the domain prefix (`Fs*`, `Env*`, `Clock`, `GetHostName`, …). A reader of `Sys: FsReadToString + Clock + EnvVar` knows immediately which side effects the function reaches for. + +7. **Explicit turbofish in production.** Production call sites name the provider: + + ```rust + read_modules_manifest::(modules_dir) + write_modules_manifest::(modules_dir, manifest) + Config::current::(env::current_dir, home::home_dir, Default::default) + ``` + + The turbofish makes the production choice visible at the call site instead of relying on type inference; if a future caller wants to swap in a different provider (a test driver, a dry-run shim), the spot to change is obvious. + +8. **Capabilities are primitives, not algorithms.** Each trait names a leaf-level effect that maps to a single `std` function (`read_to_string`, `create_dir_all`, `write`, `var`, …). Higher-level guarantees — atomic write, retry loops, walk-with-options — become free functions composed on top of those primitives, not new trait methods. That keeps the fake surface dead-simple: a fake declares one method per capability, never a knob-laden builder. + +#### Worked example: `modules-yaml` + +`crates/modules-yaml` reads and writes `node_modules/.modules.yaml`. The read path is generic over `FsReadToString + Clock` because it needs to read the file and stamp `prunedAt` from the wall clock; the write path is generic over `FsCreateDirAll + FsWrite` because it needs to ensure the parent directory exists before writing the serialized manifest: ```rust -fn my_function(value: Arc>) { - // ... do many things here - let value_clone = value.clone(); // inexpensive clone - tokio::task::spawn(async move { - // ... do stuff with value_clone - }); +pub trait FsReadToString { + fn read_to_string(path: &Path) -> io::Result; +} + +pub trait FsCreateDirAll { + fn create_dir_all(path: &Path) -> io::Result<()>; +} + +pub trait FsWrite { + fn write(path: &Path, contents: &[u8]) -> io::Result<()>; +} + +pub trait Clock { + fn now() -> SystemTime; +} + +pub struct Host; + +impl FsReadToString for Host { + fn read_to_string(path: &Path) -> io::Result { fs::read_to_string(path) } +} +impl FsCreateDirAll for Host { + fn create_dir_all(path: &Path) -> io::Result<()> { fs::create_dir_all(path) } +} +impl FsWrite for Host { + fn write(path: &Path, contents: &[u8]) -> io::Result<()> { fs::write(path, contents) } +} +impl Clock for Host { + fn now() -> SystemTime { SystemTime::now() } +} + +pub fn read_modules_manifest(modules_dir: &Path) -> Result, ReadModulesError> +where + Sys: FsReadToString + Clock, +{ + let content = match Sys::read_to_string(&manifest_path) { /* ... */ }; + // ... + manifest.pruned_at = httpdate::fmt_http_date(Sys::now()); + Ok(Some(manifest)) } ``` -The above function could easily be refactored into the following code: +A test that wants to drive the `PermissionDenied` branch declares a unit-struct fake inside the `#[test]` body, implementing only the capability the function touches: ```rust -fn my_function(value: &Vec) { - // ... do many things here - let value_clone = value.clone(); // expensive clone, oops - tokio::task::spawn(async move { - // ... do stuff with value_clone - }); +#[test] +fn read_propagates_non_not_found_io_error() { + struct FailingRead; + impl FsReadToString for FailingRead { + fn read_to_string(_: &Path) -> io::Result { + Err(io::Error::new(io::ErrorKind::PermissionDenied, "mocked")) + } + } + // `read_modules_manifest`'s bound list is `FsReadToString + Clock`, + // so every fake must satisfy both bounds at the type level — Rust + // doesn't know that the `prunedAt` branch is unreachable for this + // input. The convention for capabilities the test won't exercise + // is a trivial impl whose body is `unreachable!`: the bound is + // satisfied, and the panic message documents the precondition the + // test relies on. + impl Clock for FailingRead { + fn now() -> SystemTime { + unreachable!("clock must not be called when read_to_string fails"); + } + } + let err = read_modules_manifest::(Path::new("/")).unwrap_err(); + assert!(matches!(err, ReadModulesError::ReadFile { .. })); } ``` -With an explicit `Arc::clone`, however, the performance characteristic will never be missed: +Stateful fakes (deterministic clock, recording reads) hold their state in a `static` inside the test fn: ```rust -fn my_function(value: Arc>) { - // ... do many things here - let value_clone = Arc::clone(&value); // no compile error - tokio::task::spawn(async move { - // ... do stuff with value_clone - }); +#[test] +fn read_fills_in_pruned_at_when_missing() { + static FAKE_NOW: SystemTime = SystemTime::UNIX_EPOCH; + struct FakeClock; + impl Clock for FakeClock { + fn now() -> SystemTime { FAKE_NOW } + } + // The `static` lives in this fn's scope, so other tests get + // independent storage and never race on it. The provider type + // stays an empty unit struct; the state lives next to the test + // that needs it. Use `SystemTime::UNIX_EPOCH` (a `const`) for a + // const-initialised static, or `LazyLock` when the desired value + // needs a runtime constructor. + // ... } ``` +#### Cross-domain composition + +When a function needs capabilities from more than one domain, list them inline on `Sys`: + ```rust -fn my_function(value: &Vec) { - // ... do many things here - let value_clone = Arc::clone(&value); // compile error - tokio::task::spawn(async move { - // ... do stuff with value_clone - }); -} +fn write_shim(target_path: &Path, shim_path: &Path) -> Result<(), LinkBinsError> +where + Sys: FsReadToString + FsReadHead + FsWrite + FsSetExecutable + FsEnsureExecutableBits, +{ /* ... */ } ``` -The above code is still valid code, and the Rust compiler doesn't error, but it has a different performance characteristic now. - -**Readability:** The generic `.clone()` or `Clone::clone` often implies an expensive operation (for example: cloning a `Vec`), but `Arc` and `Rc` are not as expensive as the generic `.clone()`. Explicitly marking the cloned type aids future refactoring. +The provider implements each trait independently, so adding a domain to an existing `Sys` is one more `impl X for Host` block — no churn on the production type beyond the new line, and no churn on existing tests beyond the ones whose fakes now need the new method. ### Reporter / log events diff --git a/pacquet/CONTRIBUTING.md b/pacquet/CONTRIBUTING.md index 80e44986fc..afd76d574d 100644 --- a/pacquet/CONTRIBUTING.md +++ b/pacquet/CONTRIBUTING.md @@ -96,7 +96,7 @@ Before submitting, run: just ready ``` -This runs `typos`, `cargo fmt`, `just check` (which is `cargo check --locked`), `just test` (which is `cargo nextest run`), and `just lint` (which is `cargo clippy --locked -- --deny warnings`), then prints `git status`. CI runs the same commands on Linux, macOS, and Windows. +This runs `typos`, `cargo fmt`, `just check` (which is `cargo check --locked --workspace --all-targets`), `just test` (which is `cargo nextest run`), and `just lint` (which is `cargo clippy --locked --workspace --all-targets -- --deny warnings`), then prints `git status`. CI runs the same commands on Linux, macOS, and Windows. > [!IMPORTANT] > Run `just ready` before every commit. This rule applies to all changes, including documentation edits, comment changes, and config updates. Any change can break formatting, linting, building, or tests across the supported platforms. @@ -140,22 +140,22 @@ Then use the `integrated-benchmark` task to run benchmarks. For example: ```sh # Compare the branch you are working on against main -just integrated-benchmark --scenario=frozen-lockfile my-branch main +just integrated-benchmark --scenario=isolated-linker.fresh-restore.cold-cache.cold-store pacquet@my-branch pacquet@main ``` ```sh # Compare the current commit against the previous commit -just integrated-benchmark --scenario=frozen-lockfile HEAD HEAD~ +just integrated-benchmark --scenario=isolated-linker.fresh-restore.cold-cache.cold-store pacquet@HEAD pacquet@HEAD~ ``` ```sh # Compare pacquet of the current commit against pnpm -just integrated-benchmark --scenario=frozen-lockfile --with-pnpm HEAD +just integrated-benchmark --scenario=isolated-linker.fresh-restore.cold-cache.cold-store --with-pnpm pacquet@HEAD ``` ```sh # Compare pacquet of the current commit, pacquet of main, and pnpm against each other -just integrated-benchmark --scenario=frozen-lockfile --with-pnpm HEAD main +just integrated-benchmark --scenario=isolated-linker.fresh-restore.cold-cache.cold-store --with-pnpm pacquet@HEAD pacquet@main ``` ```sh diff --git a/pacquet/benchmark/.npmrc b/pacquet/benchmark/.npmrc deleted file mode 100644 index 61c7ca9c23..0000000000 --- a/pacquet/benchmark/.npmrc +++ /dev/null @@ -1,3 +0,0 @@ -registry=http://localhost:4873 -auto-install-peers=false -lockfile=false diff --git a/pacquet/benchmark/.yarnrc.yml b/pacquet/benchmark/.yarnrc.yml deleted file mode 100644 index 105eb477a8..0000000000 --- a/pacquet/benchmark/.yarnrc.yml +++ /dev/null @@ -1,3 +0,0 @@ -npmRegistryServer: "http://localhost:4873" -unsafeHttpWhitelist: ["localhost"] -enableGlobalCache: false diff --git a/pacquet/benchmark/add.sh b/pacquet/benchmark/add.sh deleted file mode 100755 index 06b6870800..0000000000 --- a/pacquet/benchmark/add.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env bash - -# Make sure to create a .npmrc file -# registry=http://localhost:4873 - -PACQUET="../target/release/pacquet add fastify" -PNPM="pnpm add fastify --silent" -YARN="yarn add fastify --silent" -BUN="bun add fastify --no-cache --silent --backend=symlink" - -FILE_CLEAN="rm -rf package.json node_modules .yarn yarn.lock .pnp* && echo {} > package.json || true" -PACQUET_CLEAN="../target/release/pacquet store prune" -PNPM_CLEAN="pnpm store prune" -YARN_CLEAN="yarn cache clean --all" -CLEANUP="${PACQUET_CLEAN} && ${PNPM_CLEAN} && ${YARN_CLEAN} && ${FILE_CLEAN}" - -$FILE_CLEAN - -hyperfine -w 5 -i \ - --prepare "${CLEANUP}" \ - -n pacquet "${PACQUET}" \ - -n pnpm "${PNPM}" \ - -n yarn "${YARN}" \ - -n bun "${BUN}" - -$FILE_CLEAN diff --git a/pacquet/benchmark/bunfig.toml b/pacquet/benchmark/bunfig.toml deleted file mode 100644 index fc15acdd13..0000000000 --- a/pacquet/benchmark/bunfig.toml +++ /dev/null @@ -1,12 +0,0 @@ -[install] -optional = false -dev = false -peer = false -registry = "http://localhost:4873/" - -[install.cache] -disable = true -disableManifest = true - -[install.lockfile] -save = false diff --git a/pacquet/crates/catalogs-config/Cargo.toml b/pacquet/crates/catalogs-config/Cargo.toml new file mode 100644 index 0000000000..e38e971507 --- /dev/null +++ b/pacquet/crates/catalogs-config/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "pacquet-catalogs-config" +version = "0.0.1" +publish = false +authors.workspace = true +description.workspace = true +edition.workspace = true +homepage.workspace = true +keywords.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +pacquet-catalogs-types = { workspace = true } +pacquet-workspace = { workspace = true } + +derive_more = { workspace = true } +miette = { workspace = true } + +[lints] +workspace = true diff --git a/pacquet/crates/catalogs-config/src/lib.rs b/pacquet/crates/catalogs-config/src/lib.rs new file mode 100644 index 0000000000..2c562bfddf --- /dev/null +++ b/pacquet/crates/catalogs-config/src/lib.rs @@ -0,0 +1,81 @@ +//! Pacquet port of pnpm's +//! [`@pnpm/catalogs.config`](https://github.com/pnpm/pnpm/blob/a8a8cbce6d/catalogs/config/src/getCatalogsFromWorkspaceManifest.ts). +//! +//! Normalizes the two surface forms `pnpm-workspace.yaml` supports for +//! defining the default catalog — `catalog:` at the top level vs. +//! `catalogs.default` nested under the named catalogs — into the +//! single flat [`Catalogs`] map every resolver consumer expects. + +use derive_more::{Display, Error}; +use miette::Diagnostic; +use pacquet_catalogs_types::{Catalogs, DEFAULT_CATALOG_NAME}; +use pacquet_workspace::WorkspaceManifest; + +/// Raised when the workspace manifest defines the default catalog +/// twice — once via the top-level `catalog:` shorthand and once via +/// the explicit `catalogs.default` key. +/// +/// Mirrors upstream's `INVALID_CATALOGS_CONFIGURATION` `PnpmError` +/// ([source](https://github.com/pnpm/pnpm/blob/a8a8cbce6d/catalogs/config/src/getCatalogsFromWorkspaceManifest.ts#L32-L37)). +#[derive(Debug, Display, Error, Diagnostic, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub enum InvalidCatalogsConfigurationError { + #[display( + "The 'default' catalog was defined multiple times. Use the 'catalog' field or 'catalogs.default', but not both." + )] + #[diagnostic(code(ERR_PNPM_INVALID_CATALOGS_CONFIGURATION))] + DefaultDefinedMultipleTimes, +} + +/// Project the catalog-shaped fields from a parsed workspace manifest +/// into a single flat [`Catalogs`] map. +/// +/// Returns an empty map when `workspace_manifest` is `None`, matching +/// the upstream "no workspace manifest → no catalogs" branch. +/// +/// Mirrors upstream's `getCatalogsFromWorkspaceManifest` +/// ([source](https://github.com/pnpm/pnpm/blob/a8a8cbce6d/catalogs/config/src/getCatalogsFromWorkspaceManifest.ts#L5-L30)). +pub fn get_catalogs_from_workspace_manifest( + workspace_manifest: Option<&WorkspaceManifest>, +) -> Result { + let Some(manifest) = workspace_manifest else { + return Ok(Catalogs::new()); + }; + + check_default_catalog_is_defined_once(manifest)?; + + // Upstream spreads `workspace.catalogs` after writing `default`, so + // an explicit `catalogs.default` overrides the (already-validated + // to be absent) `catalog` field. With `catalog`/`catalogs.default` + // mutually exclusive only one branch ever populates the key. + let mut catalogs = Catalogs::new(); + if let Some(default) = &manifest.catalog { + catalogs.insert(DEFAULT_CATALOG_NAME.to_string(), default.clone()); + } + if let Some(named) = &manifest.catalogs { + for (name, catalog) in named { + catalogs.insert(name.clone(), catalog.clone()); + } + } + + Ok(catalogs) +} + +/// Validate that the default catalog is defined through at most one of +/// the two surface forms. +/// +/// Mirrors upstream's `checkDefaultCatalogIsDefinedOnce` +/// ([source](https://github.com/pnpm/pnpm/blob/a8a8cbce6d/catalogs/config/src/getCatalogsFromWorkspaceManifest.ts#L32-L40)). +pub fn check_default_catalog_is_defined_once( + manifest: &WorkspaceManifest, +) -> Result<(), InvalidCatalogsConfigurationError> { + if manifest.catalog.is_some() + && manifest.catalogs.as_ref().is_some_and(|c| c.contains_key(DEFAULT_CATALOG_NAME)) + { + return Err(InvalidCatalogsConfigurationError::DefaultDefinedMultipleTimes); + } + Ok(()) +} + +#[cfg(test)] +mod tests; diff --git a/pacquet/crates/catalogs-config/src/tests.rs b/pacquet/crates/catalogs-config/src/tests.rs new file mode 100644 index 0000000000..958c1e8d84 --- /dev/null +++ b/pacquet/crates/catalogs-config/src/tests.rs @@ -0,0 +1,68 @@ +//! Ports of `catalogs/config/test/getCatalogsFromWorkspaceManifest.test.ts` +//! ([source](https://github.com/pnpm/pnpm/blob/a8a8cbce6d/catalogs/config/test/getCatalogsFromWorkspaceManifest.test.ts)). + +use super::{InvalidCatalogsConfigurationError, get_catalogs_from_workspace_manifest}; +use pacquet_catalogs_types::{Catalog, Catalogs}; +use pacquet_workspace::WorkspaceManifest; + +fn catalog_from(entries: &[(&str, &str)]) -> Catalog { + entries.iter().map(|(k, v)| ((*k).to_string(), (*v).to_string())).collect() +} + +#[test] +fn combines_implicit_default_and_named_catalogs() { + let manifest = WorkspaceManifest { + catalog: Some(catalog_from(&[("foo", "^1.0.0")])), + catalogs: Some(Catalogs::from([("bar".to_string(), catalog_from(&[("baz", "^2.0.0")]))])), + ..WorkspaceManifest::default() + }; + + let expected = Catalogs::from([ + ("default".to_string(), catalog_from(&[("foo", "^1.0.0")])), + ("bar".to_string(), catalog_from(&[("baz", "^2.0.0")])), + ]); + assert_eq!(get_catalogs_from_workspace_manifest(Some(&manifest)).unwrap(), expected); +} + +#[test] +fn combines_explicit_default_and_named_catalogs() { + let manifest = WorkspaceManifest { + catalog: None, + catalogs: Some(Catalogs::from([ + ("default".to_string(), catalog_from(&[("foo", "^1.0.0")])), + ("bar".to_string(), catalog_from(&[("baz", "^2.0.0")])), + ])), + ..WorkspaceManifest::default() + }; + + let expected = Catalogs::from([ + ("default".to_string(), catalog_from(&[("foo", "^1.0.0")])), + ("bar".to_string(), catalog_from(&[("baz", "^2.0.0")])), + ]); + assert_eq!(get_catalogs_from_workspace_manifest(Some(&manifest)).unwrap(), expected); +} + +#[test] +fn throws_if_default_catalog_is_defined_multiple_times() { + let manifest = WorkspaceManifest { + catalog: Some(catalog_from(&[("bar", "^2.0.0")])), + catalogs: Some(Catalogs::from([( + "default".to_string(), + catalog_from(&[("foo", "^1.0.0")]), + )])), + ..WorkspaceManifest::default() + }; + + let err = get_catalogs_from_workspace_manifest(Some(&manifest)).unwrap_err(); + assert_eq!(err, InvalidCatalogsConfigurationError::DefaultDefinedMultipleTimes); + assert_eq!( + err.to_string(), + "The 'default' catalog was defined multiple times. \ + Use the 'catalog' field or 'catalogs.default', but not both.", + ); +} + +#[test] +fn returns_empty_map_for_missing_workspace_manifest() { + assert_eq!(get_catalogs_from_workspace_manifest(None).unwrap(), Catalogs::new()); +} diff --git a/pacquet/crates/catalogs-protocol-parser/Cargo.toml b/pacquet/crates/catalogs-protocol-parser/Cargo.toml new file mode 100644 index 0000000000..a519fdb248 --- /dev/null +++ b/pacquet/crates/catalogs-protocol-parser/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "pacquet-catalogs-protocol-parser" +version = "0.0.1" +publish = false +authors.workspace = true +description.workspace = true +edition.workspace = true +homepage.workspace = true +keywords.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +pacquet-catalogs-types = { workspace = true } + +[lints] +workspace = true diff --git a/pacquet/crates/catalogs-protocol-parser/src/lib.rs b/pacquet/crates/catalogs-protocol-parser/src/lib.rs new file mode 100644 index 0000000000..84ccc690ba --- /dev/null +++ b/pacquet/crates/catalogs-protocol-parser/src/lib.rs @@ -0,0 +1,27 @@ +//! Pacquet port of pnpm's +//! [`@pnpm/catalogs.protocol-parser`](https://github.com/pnpm/pnpm/blob/a8a8cbce6d/catalogs/protocol-parser/src/parseCatalogProtocol.ts). +//! +//! Splits the `catalog:` protocol prefix off a manifest bare specifier +//! and returns the requested catalog name. Used by the resolver chain +//! to decide whether a wanted dependency should be looked up in a +//! catalog before falling through to the npm / git / tarball resolvers. + +use pacquet_catalogs_types::DEFAULT_CATALOG_NAME; + +const CATALOG_PROTOCOL: &str = "catalog:"; + +/// Parse a package.json dependency specifier using the `catalog:` +/// protocol. +/// +/// Returns `None` if the specifier does not start with `catalog:`. +/// An empty `catalog:` is shorthand for [`DEFAULT_CATALOG_NAME`]. +/// +/// Mirrors upstream's `parseCatalogProtocol` +/// ([source](https://github.com/pnpm/pnpm/blob/a8a8cbce6d/catalogs/protocol-parser/src/parseCatalogProtocol.ts#L3-L16)). +pub fn parse_catalog_protocol(bare_specifier: &str) -> Option<&str> { + let raw = bare_specifier.strip_prefix(CATALOG_PROTOCOL)?.trim(); + Some(if raw.is_empty() { DEFAULT_CATALOG_NAME } else { raw }) +} + +#[cfg(test)] +mod tests; diff --git a/pacquet/crates/catalogs-protocol-parser/src/tests.rs b/pacquet/crates/catalogs-protocol-parser/src/tests.rs new file mode 100644 index 0000000000..2121f25912 --- /dev/null +++ b/pacquet/crates/catalogs-protocol-parser/src/tests.rs @@ -0,0 +1,25 @@ +//! Ports of `catalogs/protocol-parser/test/parseCatalogProtocol.test.ts` +//! ([source](https://github.com/pnpm/pnpm/blob/a8a8cbce6d/catalogs/protocol-parser/test/parseCatalogProtocol.test.ts)). + +use super::parse_catalog_protocol; + +#[test] +fn parses_named_catalog() { + assert_eq!(parse_catalog_protocol("catalog:foo"), Some("foo")); + assert_eq!(parse_catalog_protocol("catalog:bar"), Some("bar")); +} + +#[test] +fn returns_none_for_specifier_not_using_catalog_protocol() { + assert_eq!(parse_catalog_protocol("^1.0.0"), None); +} + +#[test] +fn parses_explicit_default_catalog() { + assert_eq!(parse_catalog_protocol("catalog:default"), Some("default")); +} + +#[test] +fn parses_implicit_default_catalog() { + assert_eq!(parse_catalog_protocol("catalog:"), Some("default")); +} diff --git a/pacquet/crates/catalogs-resolver/Cargo.toml b/pacquet/crates/catalogs-resolver/Cargo.toml new file mode 100644 index 0000000000..b7466c473d --- /dev/null +++ b/pacquet/crates/catalogs-resolver/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "pacquet-catalogs-resolver" +version = "0.0.1" +publish = false +authors.workspace = true +description.workspace = true +edition.workspace = true +homepage.workspace = true +keywords.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +pacquet-catalogs-protocol-parser = { workspace = true } +pacquet-catalogs-types = { workspace = true } + +derive_more = { workspace = true } +miette = { workspace = true } + +[lints] +workspace = true diff --git a/pacquet/crates/catalogs-resolver/src/lib.rs b/pacquet/crates/catalogs-resolver/src/lib.rs new file mode 100644 index 0000000000..e34e5462d2 --- /dev/null +++ b/pacquet/crates/catalogs-resolver/src/lib.rs @@ -0,0 +1,184 @@ +//! Pacquet port of pnpm's +//! [`@pnpm/catalogs.resolver`](https://github.com/pnpm/pnpm/blob/a8a8cbce6d/catalogs/resolver/src/resolveFromCatalog.ts). +//! +//! Dereferences a `catalog:` bare specifier against a parsed +//! [`Catalogs`] map and returns either the configured version or one of +//! the upstream misconfiguration errors. The npm-resolver chain calls +//! [`resolve_from_catalog`] before its own protocol dispatch so a +//! resolved [`CatalogResolutionFound::resolution`] feeds back in as a +//! plain bare specifier. + +use derive_more::{Display, Error}; +use miette::Diagnostic; +use pacquet_catalogs_protocol_parser::parse_catalog_protocol; +use pacquet_catalogs_types::Catalogs; + +/// Subset of `pacquet-resolving-resolver-base`'s `WantedDependency` +/// that catalog resolution needs. Modeled as its own type so this +/// crate doesn't depend on the resolver-base crate; the conversion +/// is a trivial field copy at the call site. +/// +/// Mirrors upstream's `WantedDependency` interface +/// ([source](https://github.com/pnpm/pnpm/blob/a8a8cbce6d/catalogs/resolver/src/resolveFromCatalog.ts#L5-L8)). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct WantedDependency { + pub alias: String, + pub bare_specifier: String, +} + +/// Outcome of [`resolve_from_catalog`]. +/// +/// Mirrors upstream's `CatalogResolutionResult` discriminated union +/// ([source](https://github.com/pnpm/pnpm/blob/a8a8cbce6d/catalogs/resolver/src/resolveFromCatalog.ts#L19)). +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CatalogResolutionResult { + /// The catalog protocol resolved to a usable specifier. + Found(CatalogResolutionFound), + /// The catalog entry was missing or used a forbidden protocol. + Misconfiguration(CatalogResolutionMisconfiguration), + /// The wanted dependency does not use the catalog protocol. + Unused, +} + +/// Successful catalog dereference. +/// +/// Mirrors upstream's `CatalogResolutionFound` +/// ([source](https://github.com/pnpm/pnpm/blob/a8a8cbce6d/catalogs/resolver/src/resolveFromCatalog.ts#L21-L24)). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CatalogResolutionFound { + pub resolution: CatalogResolution, +} + +/// Resolved (catalog name, specifier) pair. +/// +/// Mirrors upstream's `CatalogResolution` +/// ([source](https://github.com/pnpm/pnpm/blob/a8a8cbce6d/catalogs/resolver/src/resolveFromCatalog.ts#L26-L38)). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CatalogResolution { + /// Catalog the entry was found in. + pub catalog_name: String, + /// Version specifier the catalog entry resolved to. + pub specifier: String, +} + +/// A user-misconfigured catalog entry. Carries the error so the call +/// site can rethrow or render it. +/// +/// Mirrors upstream's `CatalogResolutionMisconfiguration` +/// ([source](https://github.com/pnpm/pnpm/blob/a8a8cbce6d/catalogs/resolver/src/resolveFromCatalog.ts#L40-L52)). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CatalogResolutionMisconfiguration { + pub catalog_name: String, + pub error: CatalogResolutionError, +} + +/// The four ways a `catalog:` lookup can fail. Each variant maps +/// byte-for-byte to the upstream `PnpmError` code. +/// +/// Mirrors the four errors raised by upstream's `resolveFromCatalog` +/// ([source](https://github.com/pnpm/pnpm/blob/a8a8cbce6d/catalogs/resolver/src/resolveFromCatalog.ts#L60-L120)). +#[derive(Debug, Display, Error, Diagnostic, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub enum CatalogResolutionError { + #[display("No catalog entry '{alias}' was found for catalog '{catalog_name}'.")] + #[diagnostic(code(ERR_PNPM_CATALOG_ENTRY_NOT_FOUND_FOR_SPEC))] + EntryNotFoundForSpec { alias: String, catalog_name: String }, + + #[display( + "Found invalid catalog entry using the catalog protocol recursively. The entry for '{alias}' in catalog '{catalog_name}' is invalid." + )] + #[diagnostic(code(ERR_PNPM_CATALOG_ENTRY_INVALID_RECURSIVE_DEFINITION))] + EntryInvalidRecursiveDefinition { alias: String, catalog_name: String }, + + #[display( + "The workspace protocol cannot be used as a catalog value. The entry for '{alias}' in catalog '{catalog_name}' is invalid." + )] + #[diagnostic(code(ERR_PNPM_CATALOG_ENTRY_INVALID_WORKSPACE_SPEC))] + EntryInvalidWorkspaceSpec { alias: String, catalog_name: String }, + + #[display( + "The entry for '{alias}' in catalog '{catalog_name}' declares a dependency using the '{protocol}' protocol. This is not yet supported, but may be in a future version of pnpm." + )] + #[diagnostic(code(ERR_PNPM_CATALOG_ENTRY_INVALID_SPEC))] + EntryInvalidSpec { alias: String, catalog_name: String, protocol: String }, +} + +/// Resolve a wanted dependency through the catalogs map. +/// +/// - Non-`catalog:` specifiers return [`CatalogResolutionResult::Unused`] +/// so the caller falls through to the next resolver. +/// - Missing entries, recursive entries, and forbidden protocols +/// (`workspace:`, `link:`, `file:`) return +/// [`CatalogResolutionResult::Misconfiguration`]. +/// - A valid entry returns [`CatalogResolutionResult::Found`]. +/// +/// Mirrors upstream's `resolveFromCatalog` +/// ([source](https://github.com/pnpm/pnpm/blob/a8a8cbce6d/catalogs/resolver/src/resolveFromCatalog.ts#L60-L130)). +pub fn resolve_from_catalog( + catalogs: &Catalogs, + wanted_dependency: &WantedDependency, +) -> CatalogResolutionResult { + let Some(catalog_name) = parse_catalog_protocol(&wanted_dependency.bare_specifier) else { + return CatalogResolutionResult::Unused; + }; + + let catalog_lookup = + catalogs.get(catalog_name).and_then(|catalog| catalog.get(&wanted_dependency.alias)); + let Some(catalog_lookup) = catalog_lookup else { + return CatalogResolutionResult::Misconfiguration(CatalogResolutionMisconfiguration { + catalog_name: catalog_name.to_string(), + error: CatalogResolutionError::EntryNotFoundForSpec { + alias: wanted_dependency.alias.clone(), + catalog_name: catalog_name.to_string(), + }, + }); + }; + + if parse_catalog_protocol(catalog_lookup).is_some() { + return CatalogResolutionResult::Misconfiguration(CatalogResolutionMisconfiguration { + catalog_name: catalog_name.to_string(), + error: CatalogResolutionError::EntryInvalidRecursiveDefinition { + alias: wanted_dependency.alias.clone(), + catalog_name: catalog_name.to_string(), + }, + }); + } + + // Banning `workspace:` matches upstream's three-part justification + // ([source](https://github.com/pnpm/pnpm/blob/a8a8cbce6d/catalogs/resolver/src/resolveFromCatalog.ts#L84-L100)): + // it's silly to indirect through a catalog when the workspace + // protocol resolves directly, and `link:` resolutions cannot be + // cached in `pnpm-lock.yaml` across importers the way semver + // selectors can. + let protocol_of_lookup = catalog_lookup.split(':').next().unwrap_or(""); + if protocol_of_lookup == "workspace" { + return CatalogResolutionResult::Misconfiguration(CatalogResolutionMisconfiguration { + catalog_name: catalog_name.to_string(), + error: CatalogResolutionError::EntryInvalidWorkspaceSpec { + alias: wanted_dependency.alias.clone(), + catalog_name: catalog_name.to_string(), + }, + }); + } + + if matches!(protocol_of_lookup, "link" | "file") { + return CatalogResolutionResult::Misconfiguration(CatalogResolutionMisconfiguration { + catalog_name: catalog_name.to_string(), + error: CatalogResolutionError::EntryInvalidSpec { + alias: wanted_dependency.alias.clone(), + catalog_name: catalog_name.to_string(), + protocol: protocol_of_lookup.to_string(), + }, + }); + } + + CatalogResolutionResult::Found(CatalogResolutionFound { + resolution: CatalogResolution { + catalog_name: catalog_name.to_string(), + specifier: catalog_lookup.clone(), + }, + }) +} + +#[cfg(test)] +mod tests; diff --git a/pacquet/crates/catalogs-resolver/src/tests.rs b/pacquet/crates/catalogs-resolver/src/tests.rs new file mode 100644 index 0000000000..f480f9c681 --- /dev/null +++ b/pacquet/crates/catalogs-resolver/src/tests.rs @@ -0,0 +1,187 @@ +//! Ports of `catalogs/resolver/test/resolveFromCatalog.test.ts` +//! ([source](https://github.com/pnpm/pnpm/blob/a8a8cbce6d/catalogs/resolver/test/resolveFromCatalog.test.ts)). + +use super::{ + CatalogResolution, CatalogResolutionError, CatalogResolutionFound, CatalogResolutionResult, + WantedDependency, resolve_from_catalog, +}; +use pacquet_catalogs_types::{Catalog, Catalogs}; + +fn catalogs_from(entries: &[(&str, &[(&str, &str)])]) -> Catalogs { + entries + .iter() + .map(|(name, items)| { + let catalog: Catalog = + items.iter().map(|(k, v)| ((*k).to_string(), (*v).to_string())).collect(); + ((*name).to_string(), catalog) + }) + .collect() +} + +fn wanted(alias: &str, bare_specifier: &str) -> WantedDependency { + WantedDependency { alias: alias.to_string(), bare_specifier: bare_specifier.to_string() } +} + +#[test] +fn default_catalog_resolves_using_implicit_name() { + let catalogs = catalogs_from(&[("default", &[("foo", "1.0.0")])]); + assert_eq!( + resolve_from_catalog(&catalogs, &wanted("foo", "catalog:")), + CatalogResolutionResult::Found(CatalogResolutionFound { + resolution: CatalogResolution { + catalog_name: "default".to_string(), + specifier: "1.0.0".to_string(), + }, + }), + ); +} + +#[test] +fn default_catalog_resolves_using_explicit_name() { + let catalogs = catalogs_from(&[("default", &[("foo", "1.0.0")])]); + assert_eq!( + resolve_from_catalog(&catalogs, &wanted("foo", "catalog:default")), + CatalogResolutionResult::Found(CatalogResolutionFound { + resolution: CatalogResolution { + catalog_name: "default".to_string(), + specifier: "1.0.0".to_string(), + }, + }), + ); +} + +#[test] +fn resolves_named_catalog() { + let catalogs = catalogs_from(&[("foo", &[("bar", "1.0.0")])]); + assert_eq!( + resolve_from_catalog(&catalogs, &wanted("bar", "catalog:foo")), + CatalogResolutionResult::Found(CatalogResolutionFound { + resolution: CatalogResolution { + catalog_name: "foo".to_string(), + specifier: "1.0.0".to_string(), + }, + }), + ); +} + +#[test] +fn returns_unused_for_specifier_not_using_catalog_protocol() { + let catalogs = catalogs_from(&[("foo", &[("bar", "1.0.0")])]); + assert_eq!( + resolve_from_catalog(&catalogs, &wanted("bar", "^2.0.0")), + CatalogResolutionResult::Unused, + ); +} + +#[test] +fn returns_error_for_missing_unresolved_catalog() { + let catalogs = catalogs_from(&[("foo", &[("bar", "1.0.0")])]); + for (alias, bare, expected_catalog) in [ + ("bar", "catalog:", "default"), + ("bar", "catalog:baz", "baz"), + ("foo", "catalog:foo", "foo"), + ] { + let result = resolve_from_catalog(&catalogs, &wanted(alias, bare)); + let CatalogResolutionResult::Misconfiguration(misconfig) = &result else { + panic!("expected misconfiguration for ({alias}, {bare}), got {result:?}"); + }; + assert_eq!(misconfig.catalog_name, expected_catalog); + assert_eq!( + misconfig.error, + CatalogResolutionError::EntryNotFoundForSpec { + alias: alias.to_string(), + catalog_name: expected_catalog.to_string(), + }, + ); + assert_eq!( + misconfig.error.to_string(), + format!("No catalog entry '{alias}' was found for catalog '{expected_catalog}'."), + ); + } +} + +#[test] +fn returns_error_for_recursive_catalog() { + let catalogs = catalogs_from(&[("foo", &[("bar", "catalog:foo")])]); + let result = resolve_from_catalog(&catalogs, &wanted("bar", "catalog:foo")); + let CatalogResolutionResult::Misconfiguration(misconfig) = &result else { + panic!("expected misconfiguration, got {result:?}"); + }; + assert_eq!( + misconfig.error, + CatalogResolutionError::EntryInvalidRecursiveDefinition { + alias: "bar".to_string(), + catalog_name: "foo".to_string(), + }, + ); + assert_eq!( + misconfig.error.to_string(), + "Found invalid catalog entry using the catalog protocol recursively. \ + The entry for 'bar' in catalog 'foo' is invalid.", + ); +} + +#[test] +fn returns_error_for_workspace_protocol_in_catalog() { + let catalogs = catalogs_from(&[("foo", &[("bar", "workspace:*")])]); + let result = resolve_from_catalog(&catalogs, &wanted("bar", "catalog:foo")); + let CatalogResolutionResult::Misconfiguration(misconfig) = &result else { + panic!("expected misconfiguration, got {result:?}"); + }; + assert_eq!( + misconfig.error, + CatalogResolutionError::EntryInvalidWorkspaceSpec { + alias: "bar".to_string(), + catalog_name: "foo".to_string(), + }, + ); + assert_eq!( + misconfig.error.to_string(), + "The workspace protocol cannot be used as a catalog value. \ + The entry for 'bar' in catalog 'foo' is invalid.", + ); +} + +#[test] +fn returns_error_for_file_protocol_in_catalog() { + let catalogs = catalogs_from(&[("foo", &[("bar", "file:./bar.tgz")])]); + let result = resolve_from_catalog(&catalogs, &wanted("bar", "catalog:foo")); + let CatalogResolutionResult::Misconfiguration(misconfig) = &result else { + panic!("expected misconfiguration, got {result:?}"); + }; + assert_eq!( + misconfig.error, + CatalogResolutionError::EntryInvalidSpec { + alias: "bar".to_string(), + catalog_name: "foo".to_string(), + protocol: "file".to_string(), + }, + ); + assert_eq!( + misconfig.error.to_string(), + "The entry for 'bar' in catalog 'foo' declares a dependency using the 'file' protocol. \ + This is not yet supported, but may be in a future version of pnpm.", + ); +} + +#[test] +fn returns_error_for_link_protocol_in_catalog() { + let catalogs = catalogs_from(&[("foo", &[("bar", "link:./bar")])]); + let result = resolve_from_catalog(&catalogs, &wanted("bar", "catalog:foo")); + let CatalogResolutionResult::Misconfiguration(misconfig) = &result else { + panic!("expected misconfiguration, got {result:?}"); + }; + assert_eq!( + misconfig.error, + CatalogResolutionError::EntryInvalidSpec { + alias: "bar".to_string(), + catalog_name: "foo".to_string(), + protocol: "link".to_string(), + }, + ); + assert_eq!( + misconfig.error.to_string(), + "The entry for 'bar' in catalog 'foo' declares a dependency using the 'link' protocol. \ + This is not yet supported, but may be in a future version of pnpm.", + ); +} diff --git a/pacquet/crates/catalogs-types/Cargo.toml b/pacquet/crates/catalogs-types/Cargo.toml new file mode 100644 index 0000000000..e15f1a229f --- /dev/null +++ b/pacquet/crates/catalogs-types/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "pacquet-catalogs-types" +version = "0.0.1" +publish = false +authors.workspace = true +description.workspace = true +edition.workspace = true +homepage.workspace = true +keywords.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] + +[lints] +workspace = true diff --git a/pacquet/crates/catalogs-types/src/lib.rs b/pacquet/crates/catalogs-types/src/lib.rs new file mode 100644 index 0000000000..95216d7772 --- /dev/null +++ b/pacquet/crates/catalogs-types/src/lib.rs @@ -0,0 +1,30 @@ +//! Pacquet port of pnpm's +//! [`@pnpm/catalogs.types`](https://github.com/pnpm/pnpm/blob/a8a8cbce6d/catalogs/types/src/index.ts). +//! +//! Defines [`Catalog`] and [`Catalogs`], the normalized in-memory shape +//! every other catalogs crate consumes. Both are plain typed maps in +//! upstream — `interface Catalog { [name: string]: string | undefined }` +//! and `interface Catalogs { default?: Catalog; [name: string]: Catalog +//! | undefined }` — so pacquet exposes them as `BTreeMap` aliases. The +//! `"default"` catalog is just the well-known key inside [`Catalogs`]; +//! no separate field is needed. + +use std::collections::BTreeMap; + +/// The well-known name of the default catalog. Matches the literal pnpm +/// uses everywhere a catalog name is implicit. +pub const DEFAULT_CATALOG_NAME: &str = "default"; + +/// One catalog: a map of dependency name to version specifier. +/// +/// Mirrors upstream's `Catalog` interface +/// ([source](https://github.com/pnpm/pnpm/blob/a8a8cbce6d/catalogs/types/src/index.ts#L27-L29)). +pub type Catalog = BTreeMap; + +/// The full set of catalogs parsed from `pnpm-workspace.yaml`. The +/// default catalog lives at the [`DEFAULT_CATALOG_NAME`] key; any other +/// key is a user-defined named catalog. +/// +/// Mirrors upstream's `Catalogs` interface +/// ([source](https://github.com/pnpm/pnpm/blob/a8a8cbce6d/catalogs/types/src/index.ts#L1-L25)). +pub type Catalogs = BTreeMap; diff --git a/pacquet/crates/cli/Cargo.toml b/pacquet/crates/cli/Cargo.toml index bcbe92a91a..2d42a8611b 100644 --- a/pacquet/crates/cli/Cargo.toml +++ b/pacquet/crates/cli/Cargo.toml @@ -48,3 +48,6 @@ pretty_assertions = { workspace = true } serde_json = { workspace = true } tempfile = { workspace = true } walkdir = { workspace = true } + +[lints] +workspace = true diff --git a/pacquet/crates/cli/src/cli_args.rs b/pacquet/crates/cli/src/cli_args.rs index 21305fc3fe..737673591b 100644 --- a/pacquet/crates/cli/src/cli_args.rs +++ b/pacquet/crates/cli/src/cli_args.rs @@ -4,12 +4,12 @@ pub mod run; pub mod store; pub mod supported_architectures; -use crate::State; +use crate::{State, config_overrides::ConfigOverrides}; use add::AddArgs; use clap::{Parser, Subcommand, ValueEnum}; use install::InstallArgs; use miette::{Context, IntoDiagnostic}; -use pacquet_config::{Config, RealApi}; +use pacquet_config::{Config, Host}; use pacquet_executor::execute_shell; use pacquet_package_manifest::PackageManifest; use pacquet_reporter::{NdjsonReporter, SilentReporter}; @@ -21,7 +21,7 @@ use store::StoreCommand; #[derive(Debug, Parser)] #[clap(name = "pacquet")] #[clap(bin_name = "pacquet")] -#[clap(version = "0.2.1")] +#[clap(version = "0.2.2")] #[clap(about = "Experimental package manager for node.js")] pub struct CliArgs { #[clap(subcommand)] @@ -31,6 +31,15 @@ pub struct CliArgs { #[clap(short = 'C', long, default_value = ".")] pub dir: PathBuf, + /// Run the command for every project in the workspace instead of + /// only the project in `--dir`. Mirrors pnpm's global `-r` / + /// `--recursive` flag and sets + /// [`pacquet_config::Config::recursive`]. pacquet's `install` + /// already spans the whole workspace, so the flag is a surface + /// no-op there today; see the field docs. + #[clap(short = 'r', long, global = true)] + pub recursive: bool, + /// Reporter output format. #[clap(long, value_enum, default_value_t = ReporterType::Silent, global = true)] pub reporter: ReporterType, @@ -52,7 +61,7 @@ pub enum ReporterType { Silent, } -#[derive(Subcommand, Debug)] +#[derive(Debug, Subcommand)] pub enum CliCommand { /// Initialize a package.json Init, @@ -72,9 +81,13 @@ pub enum CliCommand { } impl CliArgs { - /// Execute the command - pub async fn run(self) -> miette::Result<()> { - let CliArgs { command, dir, reporter } = self; + /// Execute the command. `config_overrides` carries `--config.=` + /// tokens already stripped from argv by [`ConfigOverrides::extract`]; + /// they're layered on top of `.npmrc` / `pnpm-workspace.yaml` whenever + /// `Config` is loaded, mirroring pnpm 11's + /// "CLI > yaml > .npmrc > defaults" precedence. + pub async fn run(self, config_overrides: &ConfigOverrides) -> miette::Result<()> { + let CliArgs { command, dir, recursive, reporter } = self; // Canonicalize `--dir` so the bunyan-envelope `prefix` emitted by // the reporter is the same absolute, symlink-resolved path that // `@pnpm/cli.default-reporter` derives via `process.cwd()`. Without @@ -94,19 +107,24 @@ impl CliArgs { // builds its `localPrefix` from `cliOptions.dir`, not `cwd`) — // see [`loadNpmrcConfig`](https://github.com/pnpm/pnpm/blob/1819226b51/config/reader/src/loadNpmrcFiles.ts#L48-L50). // - // Production callers turbofish `RealApi` explicitly so the + // Production callers turbofish `Host` explicitly so the // dependency-injection plumbing is visible at the call site. // See [pnpm/pacquet#339](https://github.com/pnpm/pacquet/issues/339) // for the pattern and rationale. let config = || -> miette::Result<&'static mut Config> { - Config::current::( - || Ok::<_, std::convert::Infallible>(dir.clone()), - home::home_dir, - Default::default, - ) - .map(Config::leak) - .map_err(miette::Report::new) - .wrap_err("load configuration") + Config::default() + .current::(&dir) + .map(|mut cfg| { + config_overrides.apply(&mut cfg); + // `--recursive` / `-r` is CLI-only upstream (not a + // `.npmrc` / yaml key), so it is set here from the + // global flag rather than through the yaml / env + // overlay. Mirrors pnpm's `Config.recursive`. + cfg.recursive = recursive; + Config::leak(cfg) + }) + .map_err(miette::Report::new) + .wrap_err("load configuration") }; // `require_lockfile` is the "this subcommand cannot run without a // lockfile loaded" signal, used by `State::init` to override @@ -141,6 +159,8 @@ impl CliArgs { let cfg = config()?; cfg.offline = cfg.offline || args.offline; cfg.prefer_offline = cfg.prefer_offline || args.prefer_offline; + cfg.workspace_concurrency = + args.resolve_workspace_concurrency(cfg.workspace_concurrency); let require_lockfile = args.frozen_lockfile; let state = State::init(manifest_path(), cfg, require_lockfile) .wrap_err("initialize the state")?; @@ -174,3 +194,6 @@ impl CliArgs { Ok(()) } } + +#[cfg(test)] +mod tests; diff --git a/pacquet/crates/cli/src/cli_args/add.rs b/pacquet/crates/cli/src/cli_args/add.rs index a6ee2d2e9f..da599d4df6 100644 --- a/pacquet/crates/cli/src/cli_args/add.rs +++ b/pacquet/crates/cli/src/cli_args/add.rs @@ -90,7 +90,10 @@ pub struct AddArgs { impl AddArgs { /// Execute the subcommand. - pub async fn run(self, mut state: State) -> miette::Result<()> { + pub async fn run( + self, + mut state: State, + ) -> miette::Result<()> { // TODO: if a package already exists in another dependency group, don't remove the existing entry. let State { tarball_mem_cache, http_client, config, manifest, lockfile, resolved_packages } = @@ -104,19 +107,25 @@ impl AddArgs { let supported_architectures = self.supported_architectures.apply_to(config.supported_architectures.clone()); + let lockfile_path = manifest + .path() + .parent() + .map(|parent| parent.join(pacquet_lockfile::Lockfile::FILE_NAME)); Add { - tarball_mem_cache, + tarball_mem_cache: std::sync::Arc::clone(tarball_mem_cache), http_client, + http_client_arc: std::sync::Arc::clone(http_client), config, manifest, lockfile: lockfile.as_ref(), + lockfile_path: lockfile_path.as_deref(), list_dependency_groups: || self.dependency_options.dependency_groups(), package_name: &self.package_name, save_exact: self.save_exact, resolved_packages, supported_architectures, } - .run::() + .run::() .await .wrap_err("adding a new package") } diff --git a/pacquet/crates/cli/src/cli_args/install.rs b/pacquet/crates/cli/src/cli_args/install.rs index 4a3e0d5f13..c7e56d93f0 100644 --- a/pacquet/crates/cli/src/cli_args/install.rs +++ b/pacquet/crates/cli/src/cli_args/install.rs @@ -80,6 +80,42 @@ pub struct InstallArgs { #[clap(long)] pub frozen_lockfile: bool, + /// Force-enable `preferFrozenLockfile` for this invocation. + /// Overrides `pnpm-workspace.yaml` / `PNPM_CONFIG_PREFER_FROZEN_LOCKFILE`. + /// Mirrors pnpm's `--prefer-frozen-lockfile`. Conflicts with + /// [`Self::no_prefer_frozen_lockfile`] so a single invocation + /// can't both force-on and force-off. + #[clap(long = "prefer-frozen-lockfile")] + pub prefer_frozen_lockfile: bool, + + /// Force-disable `preferFrozenLockfile` for this invocation. + /// Overrides `pnpm-workspace.yaml` / `PNPM_CONFIG_PREFER_FROZEN_LOCKFILE`. + /// Mirrors pnpm's `--no-prefer-frozen-lockfile`. Useful for CI + /// runs that want to force a re-resolve against the registry + /// without setting the flag globally. + #[clap(long = "no-prefer-frozen-lockfile", conflicts_with = "prefer_frozen_lockfile")] + pub no_prefer_frozen_lockfile: bool, + + /// Skip the per-importer `package.json` ↔ `pnpm-lock.yaml` + /// freshness check that normally guards `--frozen-lockfile`. + /// Intended for callers that just resolved and wrote the + /// lockfile themselves (today: the pnpm CLI delegating + /// materialization to pacquet via `configDependencies`), where + /// the manifest may still be the pre-mutation copy while the + /// lockfile is already post-mutation — the upstream resolver + /// will rewrite the manifest right after pacquet returns. See + /// . + /// + /// Narrow on purpose: only gates + /// [`pacquet_lockfile::satisfies_package_manifest`]. Settings + /// drift (`overrides`, `ignoredOptionalDependencies`, + /// `pnpmfileChecksum`, …) still aborts. A future broader flag + /// matching pnpm's internal `ignorePackageManifest` (used by + /// `pnpm fetch`) would skip linking / hoisting / pruning too; + /// that's deliberately a separate name. + #[clap(long)] + pub ignore_manifest_check: bool, + /// Skip the install of any runtime dependencies /// (`node@runtime:`, `deno@runtime:`, `bun@runtime:`). /// Their archives aren't fetched, their slots aren't @@ -130,22 +166,69 @@ pub struct InstallArgs { /// path the way upstream does. #[clap(long)] pub prefer_offline: bool, + + /// Skip the lockfile supply-chain verification pass entirely. + /// Overrides `pnpm-workspace.yaml#trustLockfile`. Mirrors pnpm's + /// `--trust-lockfile`. See [`pacquet_config::Config::trust_lockfile`]. + /// Added for [pnpm/pnpm#11860](https://github.com/pnpm/pnpm/issues/11860). + #[clap(long = "trust-lockfile")] + pub trust_lockfile: bool, + + /// Refresh the integrity checksums recorded in `pnpm-lock.yaml` + /// from the registry. Mirrors pnpm's `--update-checksums`. Skips + /// the frozen-lockfile fast path; conflicts with `--frozen-lockfile`. + #[clap(long = "update-checksums")] + pub update_checksums: bool, + + /// Maximum number of workspace projects to process in parallel. + /// Mirrors pnpm's `--workspace-concurrency`. Overrides the + /// `workspaceConcurrency` value resolved from `pnpm-workspace.yaml` / + /// global `config.yaml` / `PNPM_CONFIG_WORKSPACE_CONCURRENCY` for + /// this invocation. A non-positive value is read as + /// `parallelism - |value|` (floored at 1), matching upstream's + /// [`getWorkspaceConcurrency`](https://github.com/pnpm/pnpm/blob/b4f8f47ac2/config/reader/src/concurrency.ts#L25-L34). + /// `None` (flag absent) leaves the config-resolved value in place. + /// + /// Applied to [`pacquet_config::Config::workspace_concurrency`] at + /// the CLI dispatch in [`crate::cli_args::CliArgs::run`]; see that + /// field for why it has no consumption point on `install` yet. + #[clap(long = "workspace-concurrency")] + pub workspace_concurrency: Option, } impl InstallArgs { - pub async fn run(self, state: State) -> miette::Result<()> { + pub async fn run(self, state: State) -> miette::Result<()> { let State { tarball_mem_cache, http_client, config, manifest, lockfile, resolved_packages } = &state; let InstallArgs { dependency_options, supported_architectures, frozen_lockfile, + prefer_frozen_lockfile, + no_prefer_frozen_lockfile, + ignore_manifest_check, no_runtime, node_linker, offline: _, prefer_offline: _, + trust_lockfile, + update_checksums, + workspace_concurrency: _, } = self; + // `--prefer-frozen-lockfile` / `--no-prefer-frozen-lockfile` + // map to `Option`: `Some(true)` / `Some(false)` when + // either flag is set, `None` otherwise (use config). Clap's + // `conflicts_with` on the off-flag ensures the two aren't + // both set, so the precedence here is straightforward. + let prefer_frozen_lockfile = if prefer_frozen_lockfile { + Some(true) + } else if no_prefer_frozen_lockfile { + Some(false) + } else { + None + }; + // Merge CLI overrides with the yaml-derived value before // handing off to the install pipeline. `state.config` is a // shared `&'static Config`, so we compute the effective @@ -161,29 +244,66 @@ impl InstallArgs { // matching pnpm's stance on the same flag. let skip_runtimes = config.skip_runtimes || no_runtime; + // Same shape as `skip_runtimes`: yaml `trustLockfile: true` + // or the CLI flag turns the verification skip on. There's no + // CLI inverse — relax the yaml value if you need to flip it + // back off for a single invocation. + let trust_lockfile = config.trust_lockfile || trust_lockfile; + // `--node-linker` flag (if passed) overrides the // yaml/npmrc value for this invocation. Mirrors pnpm's // override-on-explicit-flag semantics. let node_linker = node_linker.map(NodeLinkerArg::into_config).unwrap_or(config.node_linker); + // The lockfile-verification gate keys its on-disk cache off + // `/pnpm-lock.yaml`. Once workspace support + // lands (pacquet#431), this becomes `workspace_root` to + // match where the lockfile actually lives. + let lockfile_path = manifest + .path() + .parent() + .map(|parent| parent.join(pacquet_lockfile::Lockfile::FILE_NAME)); Install { - tarball_mem_cache, + tarball_mem_cache: std::sync::Arc::clone(tarball_mem_cache), http_client, + http_client_arc: std::sync::Arc::clone(http_client), config, manifest, lockfile: lockfile.as_ref(), + lockfile_path: lockfile_path.as_deref(), dependency_groups: dependency_options.dependency_groups(), frozen_lockfile, + prefer_frozen_lockfile, + ignore_manifest_check, skip_runtimes, + trust_lockfile, + update_checksums, resolved_packages, supported_architectures, node_linker, } - .run::() + .run::() .await .wrap_err("installing dependencies")?; Ok(()) } + + /// Effective `workspaceConcurrency` for this invocation: the + /// `--workspace-concurrency` flag when passed (resolved through + /// [`pacquet_config::resolve_child_concurrency`], so a non-positive + /// value means `parallelism - |value|`, floored at 1), otherwise + /// the already-resolved `config_value` from `pnpm-workspace.yaml` / + /// global `config.yaml` / `PNPM_CONFIG_WORKSPACE_CONCURRENCY`. + /// + /// Mirrors upstream's final `workspaceConcurrency = + /// getWorkspaceConcurrency(...)` pass at + /// . + pub(crate) fn resolve_workspace_concurrency(&self, config_value: u32) -> u32 { + match self.workspace_concurrency { + Some(value) => pacquet_config::resolve_child_concurrency(Some(value)), + None => config_value, + } + } } #[cfg(test)] diff --git a/pacquet/crates/cli/src/cli_args/install/tests.rs b/pacquet/crates/cli/src/cli_args/install/tests.rs index 58370a7e91..eb03d4bd1e 100644 --- a/pacquet/crates/cli/src/cli_args/install/tests.rs +++ b/pacquet/crates/cli/src/cli_args/install/tests.rs @@ -2,6 +2,7 @@ use super::{InstallArgs, InstallDependencyOptions, NodeLinkerArg}; use clap::Parser; use pacquet_config::NodeLinker; use pacquet_package_manifest::DependencyGroup; +use pipe_trait::Pipe; use pretty_assertions::assert_eq; #[test] @@ -109,6 +110,81 @@ fn node_linker_invalid_value_rejected() { assert!(msg.contains("bogus"), "error mentions bad value: {msg}"); } +/// `--ignore-manifest-check` parses to `true`. Absent → `false`. +/// Surfaced for the pnpm CLI `configDependencies` delegation path +/// (issue #11797); see the field doc on `InstallArgs::ignore_manifest_check`. +#[test] +fn ignore_manifest_check_flag_parses() { + let parsed = InstallArgsHarness::try_parse_from(["pacquet-test"]).expect("parses"); + assert!(!parsed.args.ignore_manifest_check, "flag absent → false"); + + let parsed = InstallArgsHarness::try_parse_from(["pacquet-test", "--ignore-manifest-check"]) + .expect("parses --ignore-manifest-check"); + assert!(parsed.args.ignore_manifest_check, "flag present → true"); +} + +/// `--workspace-concurrency` is absent by default, so the override +/// is `None` and the config-resolved value stays in effect. +#[test] +fn workspace_concurrency_default_is_none() { + let parsed = ["pacquet-test"].pipe(InstallArgsHarness::try_parse_from).expect("parses"); + assert_eq!(parsed.args.workspace_concurrency, None, "flag absent → None"); +} + +/// A positive `--workspace-concurrency` parses to its value verbatim. +#[test] +fn workspace_concurrency_parses_positive() { + let parsed = ["pacquet-test", "--workspace-concurrency", "3"] + .pipe(InstallArgsHarness::try_parse_from) + .expect("parses --workspace-concurrency 3"); + assert_eq!(parsed.args.workspace_concurrency, Some(3)); +} + +/// A negative `--workspace-concurrency` parses to the signed value; +/// the `parallelism - |value|` interpretation happens later at the +/// CLI dispatch via `resolve_child_concurrency`. Mirrors pnpm +/// accepting `--workspace-concurrency=-1`. +#[test] +fn workspace_concurrency_parses_negative() { + let parsed = ["pacquet-test", "--workspace-concurrency=-1"] + .pipe(InstallArgsHarness::try_parse_from) + .expect("parses --workspace-concurrency=-1"); + assert_eq!(parsed.args.workspace_concurrency, Some(-1)); +} + +/// No `--workspace-concurrency` flag → the already-resolved config +/// value passes through untouched. +#[test] +fn resolve_workspace_concurrency_keeps_config_value_when_flag_absent() { + let args = ["pacquet-test"].pipe(InstallArgsHarness::try_parse_from).expect("parses").args; + assert_eq!(args.resolve_workspace_concurrency(7), 7); +} + +/// A positive `--workspace-concurrency` replaces the config value +/// verbatim (it does not fall through to `config_value`). +#[test] +fn resolve_workspace_concurrency_positive_flag_overrides_config() { + let args = ["pacquet-test", "--workspace-concurrency", "3"] + .pipe(InstallArgsHarness::try_parse_from) + .expect("parses") + .args; + assert_eq!(args.resolve_workspace_concurrency(7), 3); +} + +/// A non-positive `--workspace-concurrency` resolves to +/// `max(1, parallelism - |value|)` via `getWorkspaceConcurrency`, +/// independent of the config value. Pinned exactly against the host's +/// reported parallelism. +#[test] +fn resolve_workspace_concurrency_negative_flag_resolves_to_offset() { + let args = ["pacquet-test", "--workspace-concurrency=-1"] + .pipe(InstallArgsHarness::try_parse_from) + .expect("parses") + .args; + let expected = pacquet_config::available_parallelism().saturating_sub(1).max(1); + assert_eq!(args.resolve_workspace_concurrency(7), expected); +} + /// `NodeLinkerArg::into_config` maps every variant 1:1 to the /// canonical `pacquet_config::NodeLinker` enum. Tied to the /// `ValueEnum` derive's kebab-case rename — if a future variant diff --git a/pacquet/crates/cli/src/cli_args/supported_architectures.rs b/pacquet/crates/cli/src/cli_args/supported_architectures.rs index f1cfd3ca6e..ddf6005bcc 100644 --- a/pacquet/crates/cli/src/cli_args/supported_architectures.rs +++ b/pacquet/crates/cli/src/cli_args/supported_architectures.rs @@ -25,7 +25,7 @@ use pacquet_package_is_installable::SupportedArchitectures; /// Flattened into the `InstallArgs` / `AddArgs` clap derives so the /// three flags appear under the regular `--help` output. Shared /// between the two so the wire shape is identical. -#[derive(Debug, Args, Default, Clone)] +#[derive(Debug, Default, Clone, Args)] pub struct SupportedArchitecturesArgs { /// CPU architectures whose platform-tagged optional dependencies /// should be kept. Repeat or comma-separate for multiple values. diff --git a/pacquet/crates/cli/src/cli_args/tests.rs b/pacquet/crates/cli/src/cli_args/tests.rs new file mode 100644 index 0000000000..7cac762ed7 --- /dev/null +++ b/pacquet/crates/cli/src/cli_args/tests.rs @@ -0,0 +1,25 @@ +use super::{CliArgs, CliCommand}; +use clap::Parser; + +/// `--recursive` / `-r` defaults to `false` when absent. Mirrors +/// pnpm, where `recursive` is unset unless the flag (or a recursive +/// command form) is used. +#[test] +fn recursive_default_is_false() { + let parsed = CliArgs::try_parse_from(["pacquet", "install"]).expect("parses"); + assert!(!parsed.recursive, "flag absent → false"); +} + +/// `-r` is a global flag, so it parses both before and after the +/// subcommand. Mirrors pnpm's global `-r` / `--recursive`. +#[test] +fn recursive_flag_is_global_and_parses_either_side_of_subcommand() { + let before = CliArgs::try_parse_from(["pacquet", "-r", "install"]).expect("parses -r install"); + assert!(before.recursive, "`-r install` → recursive"); + assert!(matches!(before.command, CliCommand::Install(_))); + + let after = CliArgs::try_parse_from(["pacquet", "install", "--recursive"]) + .expect("parses install --recursive"); + assert!(after.recursive, "`install --recursive` → recursive"); + assert!(matches!(after.command, CliCommand::Install(_))); +} diff --git a/pacquet/crates/cli/src/config_overrides.rs b/pacquet/crates/cli/src/config_overrides.rs new file mode 100644 index 0000000000..8fbb498dfe --- /dev/null +++ b/pacquet/crates/cli/src/config_overrides.rs @@ -0,0 +1,151 @@ +use pacquet_config::Config; +use std::ffi::{OsStr, OsString}; + +/// CLI overrides parsed from pnpm's `--config.=` dotted-key +/// syntax. Upstream pnpm uses [`npm-conf`](https://github.com/npm/npm-conf) +/// to translate each `--config.=` token into a runtime config +/// assignment that wins over `.npmrc` and `pnpm-workspace.yaml`; pacquet +/// mirrors that by stripping the same tokens out of argv before clap sees +/// them and re-applying them onto [`Config`] after the file-based layers +/// have run. +/// +/// Unknown keys are accepted silently: pnpm exposes a long tail of config +/// keys, and erroring on an unrecognized one would break the moment pnpm +/// adds a new key that pacquet hasn't ported yet. The token has already +/// been honored by pnpm itself before delegation, so dropping it on +/// pacquet's side just means the pacquet leg falls back to the yaml/npmrc +/// value — never an incorrect override. +#[derive(Debug, Default)] +pub struct ConfigOverrides { + registry: Option, +} + +impl ConfigOverrides { + /// Pull `--config.=` tokens out of `argv` and collect + /// them. Returns the parsed overrides together with the remaining + /// argv tokens (in their original order) for clap to parse. + /// + /// Malformed tokens — `--config.foo` with no `=`, or `--config.=value` + /// with an empty key — are dropped: clap would reject `--config.*` as + /// unknown anyway, and the dropped tokens carry no usable signal. + pub fn extract(argv: Argv) -> (Self, Vec) + where + Argv: IntoIterator, + { + let mut overrides = Self::default(); + let mut remaining = Vec::new(); + for arg in argv { + match classify(&arg) { + ConfigToken::WellFormed { key, value } => overrides.set(key, value), + ConfigToken::Malformed => {} + ConfigToken::NotOurs => remaining.push(arg), + } + } + (overrides, remaining) + } + + fn set(&mut self, key: &str, value: &str) { + if key == "registry" { + self.registry = Some(value.to_owned()); + } + } + + /// Layer the CLI overrides on top of a [`Config`] that has already + /// been built from defaults, `.npmrc`, and `pnpm-workspace.yaml`. + /// Mirrors pnpm 11's "CLI > yaml > .npmrc > defaults" precedence. + pub fn apply(&self, config: &mut Config) { + if let Some(registry) = &self.registry { + config.registry = registry.clone(); + } + } +} + +enum ConfigToken<'a> { + WellFormed { key: &'a str, value: &'a str }, + Malformed, + NotOurs, +} + +/// Decide whether an argv token belongs to the `--config.=` +/// family. Everything with a `--config.` prefix is claimed, so a typo +/// like `--config.foo` never escapes into clap's "unexpected argument" +/// path; non-prefixed tokens are returned untouched. +fn classify(arg: &OsStr) -> ConfigToken<'_> { + let Some(rest) = arg.to_str().and_then(|arg| arg.strip_prefix("--config.")) else { + return ConfigToken::NotOurs; + }; + let Some((key, value)) = rest.split_once('=') else { + return ConfigToken::Malformed; + }; + if key.is_empty() { + return ConfigToken::Malformed; + } + ConfigToken::WellFormed { key, value } +} + +#[cfg(test)] +mod tests { + use super::ConfigOverrides; + use pacquet_config::Config; + use pretty_assertions::assert_eq; + use std::ffi::OsString; + + fn argv>(items: Items) -> Vec { + items.into_iter().map(OsString::from).collect() + } + + #[test] + fn extract_separates_config_tokens_from_argv() { + let (overrides, remaining) = ConfigOverrides::extract(argv([ + "pacquet", + "--config.registry=https://example.test/", + "install", + "--frozen-lockfile", + ])); + assert_eq!(remaining, argv(["pacquet", "install", "--frozen-lockfile"])); + let mut config = Config::default(); + overrides.apply(&mut config); + assert_eq!(config.registry, "https://example.test/"); + } + + #[test] + fn unknown_keys_are_dropped_silently() { + let (overrides, remaining) = + ConfigOverrides::extract(argv(["pacquet", "--config.unknown-key=whatever", "install"])); + assert_eq!(remaining, argv(["pacquet", "install"])); + let default_registry = Config::default().registry; + let mut config = Config::default(); + overrides.apply(&mut config); + assert_eq!(config.registry, default_registry, "no known key set ⇒ registry untouched"); + } + + #[test] + fn malformed_tokens_are_dropped() { + let (_, remaining) = ConfigOverrides::extract(argv([ + "--config.registry", + "--config.=missing-key", + "install", + ])); + assert_eq!(remaining, argv(["install"])); + } + + #[test] + fn last_value_wins_for_repeated_keys() { + let (overrides, _) = ConfigOverrides::extract(argv([ + "--config.registry=https://first.test/", + "--config.registry=https://second.test/", + ])); + let mut config = Config::default(); + overrides.apply(&mut config); + assert_eq!(config.registry, "https://second.test/"); + } + + #[test] + fn apply_is_a_noop_when_no_overrides_set() { + let (overrides, _) = ConfigOverrides::extract(argv(["pacquet", "install"])); + let default_registry = Config::default().registry; + let mut config = Config::default(); + overrides.apply(&mut config); + assert_eq!(config.registry, default_registry); + } +} diff --git a/pacquet/crates/cli/src/lib.rs b/pacquet/crates/cli/src/lib.rs index 6491b981e9..7358d5d115 100644 --- a/pacquet/crates/cli/src/lib.rs +++ b/pacquet/crates/cli/src/lib.rs @@ -1,8 +1,10 @@ mod cli_args; +mod config_overrides; mod state; use clap::Parser; use cli_args::CliArgs; +use config_overrides::ConfigOverrides; use miette::set_panic_hook; use pacquet_diagnostics::enable_tracing_by_env; use state::State; @@ -10,14 +12,20 @@ use state::State; pub async fn main() -> miette::Result<()> { enable_tracing_by_env(); set_panic_hook(); + // Extract pnpm's `--config.=` tokens before clap sees + // argv. Clap can't parse a dotted-key flag whose right-hand name is + // arbitrary, so a `--config.registry=...` from pnpm's forwarded flags + // would otherwise error out as "unexpected argument". Each extracted + // token is layered onto `Config` after `.npmrc` / yaml run. + let (config_overrides, argv) = ConfigOverrides::extract(std::env::args_os()); // Run argument parsing *before* sizing the rayon pool so // `pacquet --help` / `--version` (and any clap parse error) exit // without spinning up worker threads. `clap::Parser::parse` calls // `std::process::exit` on those paths, so we never reach // `configure_rayon_pool` for them (Copilot review on #292). - let args = CliArgs::parse(); + let args = CliArgs::parse_from(argv); configure_rayon_pool(); - args.run().await + args.run(&config_overrides).await } /// Size rayon's global pool at `2 × available_parallelism`. The link diff --git a/pacquet/crates/cli/src/state.rs b/pacquet/crates/cli/src/state.rs index 6c7b57d49c..47df94f086 100644 --- a/pacquet/crates/cli/src/state.rs +++ b/pacquet/crates/cli/src/state.rs @@ -7,14 +7,22 @@ use pacquet_package_manager::ResolvedPackages; use pacquet_package_manifest::{PackageManifest, PackageManifestError}; use pacquet_tarball::MemCache; use pipe_trait::Pipe; -use std::path::PathBuf; +use std::{path::PathBuf, sync::Arc}; /// Application state when running `pacquet run` or `pacquet install`. pub struct State { - /// Shared cache that store downloaded tarballs. - pub tarball_mem_cache: MemCache, - /// HTTP client to make HTTP requests. - pub http_client: ThrottledClient, + /// Shared cache that stores downloaded tarballs. Held behind + /// [`Arc`] so the resolve-time prefetch + /// ([`pacquet_package_manager::PrefetchingResolver`]) can capture + /// an owned clone into the `tokio::spawn`ed background download + /// while every install sub-pipeline still takes a borrowed + /// `&MemCache` via deref. + pub tarball_mem_cache: Arc, + /// HTTP client to make HTTP requests. Held behind [`std::sync::Arc`] so + /// the lockfile-verification gate can own a clone for the + /// `NpmResolutionVerifier`'s lifetime while every install + /// sub-pipeline takes a borrowed `&ThrottledClient` via deref. + pub http_client: std::sync::Arc, /// Merged runtime configuration: built-in defaults, with overlays from /// the auth subset of `.npmrc` and from `pnpm-workspace.yaml`. pub config: &'static Config, @@ -62,13 +70,11 @@ impl State { .map_err(InitStateError::Manifest)?, lockfile: call_load_lockfile(should_load, Lockfile::load_from_current_dir) .map_err(InitStateError::Lockfile)?, - http_client: ThrottledClient::for_installs( - &config.proxy, - &config.tls, - &config.tls_by_uri, - ) - .map_err(InitStateError::Network)?, - tarball_mem_cache: MemCache::new(), + http_client: std::sync::Arc::new( + ThrottledClient::for_installs(&config.proxy, &config.tls, &config.tls_by_uri) + .map_err(InitStateError::Network)?, + ), + tarball_mem_cache: Arc::new(MemCache::new()), resolved_packages: ResolvedPackages::new(), }) } diff --git a/pacquet/crates/cli/tests/_utils.rs b/pacquet/crates/cli/tests/_utils.rs index 0c28ea667c..fe69dba5e9 100644 --- a/pacquet/crates/cli/tests/_utils.rs +++ b/pacquet/crates/cli/tests/_utils.rs @@ -1,5 +1,27 @@ use pacquet_store_dir::{CafsFileInfo, StoreDir, StoreIndex}; -use std::{collections::BTreeMap, path::Path}; +use std::{collections::BTreeMap, fs, path::Path}; + +/// Flip the `enableGlobalVirtualStore` key in the `pnpm-workspace.yaml` +/// that [`pacquet_testing_utils::bin::CommandTempCwd::add_mocked_registry`] +/// populated with `storeDir` / `cacheDir` / `enableGlobalVirtualStore: false`. +/// The replacement is in-place rather than appended so the file stays +/// valid YAML (pnpm rejects duplicate top-level mapping keys). +pub fn enable_gvs_in_workspace_yaml(workspace: &Path, extra_yaml: &str) { + let yaml_path = workspace.join("pnpm-workspace.yaml"); + let yaml = fs::read_to_string(&yaml_path).expect("read pnpm-workspace.yaml"); + let flipped = yaml.replace("enableGlobalVirtualStore: false", "enableGlobalVirtualStore: true"); + assert_ne!( + flipped, yaml, + "expected the default `enableGlobalVirtualStore: false` line written by \ + `CommandTempCwd::add_mocked_registry` — has the helper changed?", + ); + let mut yaml = flipped; + if !yaml.ends_with('\n') { + yaml.push('\n'); + } + yaml.push_str(extra_yaml); + fs::write(&yaml_path, yaml).expect("write pnpm-workspace.yaml"); +} /// Snapshot-friendly view of every row in `/v11/index.db`. /// diff --git a/pacquet/crates/cli/tests/add.rs b/pacquet/crates/cli/tests/add.rs index 6105a69e80..4c67a4c0d9 100644 --- a/pacquet/crates/cli/tests/add.rs +++ b/pacquet/crates/cli/tests/add.rs @@ -6,7 +6,9 @@ use pacquet_testing_utils::{ fs::{get_all_folders, get_filenames_in_folder}, }; use pretty_assertions::assert_eq; -use std::{ffi::OsStr, fs, path::PathBuf}; +#[cfg(unix)] +use std::fs; +use std::{ffi::OsStr, path::PathBuf}; use tempfile::TempDir; fn exec_pacquet_in_temp_cwd(args: Args) -> (TempDir, PathBuf, AddMockedRegistry) diff --git a/pacquet/crates/cli/tests/hoist.rs b/pacquet/crates/cli/tests/hoist.rs index a001939110..43947ba2ea 100644 --- a/pacquet/crates/cli/tests/hoist.rs +++ b/pacquet/crates/cli/tests/hoist.rs @@ -98,9 +98,8 @@ fn private_hoist_default_pattern_hoists_transitives() { is_symlink_or_junction(&private_hoist).unwrap(), "transitive `@pnpm.e2e/hello-world-js-bin` should be hoisted to {private_hoist:?}", ); - // Public-hoist patterns default to `["*eslint*", "*prettier*"]`, - // neither of which match this transitive — so it should NOT be at - // the root. + // Public-hoist patterns default to `[]` (matching pnpm v11), so + // no transitive can match — it should NOT be at the root. assert!( !workspace.join("node_modules/@pnpm.e2e/hello-world-js-bin").exists(), "transitive should not be publicly hoisted under default patterns", @@ -235,7 +234,7 @@ fn modules_yaml_records_hoisted_dependencies() { // doesn't drag in a YAML parser. Assert the presence rather than // exact serialization to keep the test resilient to ordering. assert!( - modules_yaml_text.contains("\"@pnpm.e2e/hello-world-js-bin@1.0.0\""), + modules_yaml_text.contains(r#""@pnpm.e2e/hello-world-js-bin@1.0.0""#), "hoistedDependencies should record the transitive dep path; got:\n{modules_yaml_text}", ); // Alias-as-stored is the full scoped name, since that's how the @@ -243,13 +242,45 @@ fn modules_yaml_records_hoisted_dependencies() { // `dependencies` map. Mirrors upstream's // `hoistedDependencies[depPath][alias] = kind`. assert!( - modules_yaml_text.contains("\"@pnpm.e2e/hello-world-js-bin\": \"private\""), + modules_yaml_text.contains(r#""@pnpm.e2e/hello-world-js-bin": "private""#), "transitive should be marked as `private` hoist; got:\n{modules_yaml_text}", ); drop((root, mock_instance)); } +/// Regression for [pnpm/pnpm#11750](https://github.com/pnpm/pnpm/issues/11750): +/// pacquet's default `publicHoistPattern` must match pnpm v11's +/// (empty list) so a follow-up `pnpm` invocation in the same project +/// doesn't reject the `.modules.yaml` with +/// `ERR_PNPM_PUBLIC_HOIST_PATTERN_DIFF`. See pnpm's default at +/// +/// and the comparison site at +/// . +#[test] +fn modules_yaml_public_hoist_pattern_matches_pnpm_default() { + let CommandTempCwd { pacquet, pnpm, root, workspace, npmrc_info, .. } = + CommandTempCwd::init().add_mocked_registry(); + let AddMockedRegistry { mock_instance, .. } = npmrc_info; + + write_manifest( + &workspace, + serde_json::json!({ "@pnpm.e2e/hello-world-js-bin-parent": "1.0.0" }), + ); + generate_lockfile(pnpm); + + pacquet.with_args(["install", "--frozen-lockfile"]).assert().success(); + + let modules_yaml_text = fs::read_to_string(workspace.join("node_modules/.modules.yaml")) + .expect("read .modules.yaml"); + assert!( + modules_yaml_text.contains(r#""publicHoistPattern": []"#), + "publicHoistPattern should serialize as an empty list (pnpm default); got:\n{modules_yaml_text}", + ); + + drop((root, mock_instance)); +} + /// `hoistPattern: ["@pnpm.e2e/*"]` — only aliases under the /// `@pnpm.e2e` scope hoist privately. Mirrors upstream's /// [`hoist.ts:107` "should hoist dependencies by pattern"](https://github.com/pnpm/pnpm/blob/94240bc046/installing/deps-installer/test/install/hoist.ts#L107) diff --git a/pacquet/crates/cli/tests/install.rs b/pacquet/crates/cli/tests/install.rs index aa3d992bc8..95be1be8d0 100644 --- a/pacquet/crates/cli/tests/install.rs +++ b/pacquet/crates/cli/tests/install.rs @@ -7,6 +7,7 @@ use pacquet_testing_utils::{ bin::{AddMockedRegistry, CommandTempCwd}, fs::{get_all_files, get_all_folders, is_symlink_or_junction}, }; +#[cfg(unix)] use pipe_trait::Pipe; use std::fs; @@ -253,7 +254,7 @@ fn should_install_circular_dependencies() { /// End-to-end coverage for `${VAR}` substitution in `.npmrc`. /// -/// `::var` (the `std::env::var` bridge in +/// `::var` (the `std::env::var` bridge in /// `crates/config/src/api.rs`) is unreachable by every other test /// because `add_mocked_registry` writes literal values, so /// `env_replace` short-circuits at the no-`$` branch. @@ -264,7 +265,7 @@ fn should_install_circular_dependencies() { /// upstream's [`installing/deps-installer/test/install/auth.ts`](https://github.com/pnpm/pnpm/blob/601317e7a3/installing/deps-installer/test/install/auth.ts) /// is not exercised here. The mock registry doesn't gate on auth, so /// substituting the registry URL is the smallest scenario that drives -/// `::var` end-to-end. Token-substitution coverage +/// `::var` end-to-end. Token-substitution coverage /// belongs in a test against a registry that actually validates the /// header. #[test] @@ -309,3 +310,334 @@ fn install_resolves_env_var_in_npmrc_registry() { drop((root, mock_instance)); // cleanup } + +/// `@pnpm.e2e/abc-parent-with-missing-peers@1.0.0` depends on +/// `@pnpm.e2e/abc@1.0.0`, which declares `peer-a`, `peer-b`, and +/// `peer-c` as peer dependencies. The parent provides none of them. +/// With `auto-install-peers` enabled (pacquet's default, matching +/// pnpm), all three peers should appear in `node_modules/.pnpm/`. +/// Without the orchestrator's hoist loop they'd be missing, and the +/// peer-resolution issue list would carry three entries. +/// +/// Transitive auto-installed peers are NOT also linked at +/// `node_modules/` — pnpm's `addDirectDependenciesToLockfile` +/// iterates only `getAllDependenciesFromManifest(manifest)`, so +/// transitive peers live in `snapshots:` / `packages:` only and +/// consumers reach them through their parent's slot's `node_modules`. +/// Hoisting them at the importer would require listing them in +/// `importer.dependencies`, which breaks `satisfiesPackageManifest` +/// and pushes every later install onto the fresh-resolve path. +#[test] +fn auto_install_peers_hoists_missing_peers_at_importer() { + let CommandTempCwd { pacquet, root, workspace, npmrc_info, .. } = + CommandTempCwd::init().add_mocked_registry(); + let AddMockedRegistry { mock_instance, .. } = npmrc_info; + + let manifest_path = workspace.join("package.json"); + let package_json_content = serde_json::json!({ + "dependencies": { + "@pnpm.e2e/abc-parent-with-missing-peers": "1.0.0", + }, + }); + fs::write(&manifest_path, package_json_content.to_string()).expect("write to package.json"); + + pacquet.with_arg("install").assert().success(); + + let pnpm_dir = workspace.join("node_modules/.pnpm"); + let entries: Vec = fs::read_dir(&pnpm_dir) + .map(|dir| { + dir.filter_map(Result::ok) + .map(|entry| entry.file_name().to_string_lossy().into_owned()) + .collect() + }) + .unwrap_or_default(); + for peer in ["peer-a", "peer-b", "peer-c"] { + // The registry's `^1.0.0` resolves to the latest 1.x; assert on + // the slot prefix rather than a specific version so a registry + // bump doesn't churn this test. + let prefix = format!("@pnpm.e2e+{peer}@1."); + assert!( + entries.iter().any(|name| name.starts_with(&prefix) && !name.contains('_')), + "expected {peer} to be auto-installed; .pnpm/ entries: {entries:?}", + ); + } + + drop((root, mock_instance)); // cleanup +} + +/// `catalog:` on a direct dep should be dereferenced through +/// `pnpm-workspace.yaml`'s `catalog` section before the npm resolver +/// sees it. The fetched virtual-store entry is the catalog's resolved +/// version, not the literal `catalog:` string. +/// +/// Mirrors the upstream end-to-end coverage in +/// [`installing/deps-installer/test/catalogs.ts`](https://github.com/pnpm/pnpm/blob/a8a8cbce6d/installing/deps-installer/test/catalogs.ts). +#[test] +fn install_resolves_catalog_protocol() { + let CommandTempCwd { pacquet, root, workspace, npmrc_info, .. } = + CommandTempCwd::init().add_mocked_registry(); + let AddMockedRegistry { mock_instance, .. } = npmrc_info; + + eprintln!("Appending catalog to pnpm-workspace.yaml..."); + let workspace_yaml = workspace.join("pnpm-workspace.yaml"); + let mut existing = fs::read_to_string(&workspace_yaml).expect("read pnpm-workspace.yaml"); + existing.push_str("catalog:\n '@pnpm.e2e/hello-world-js-bin-parent': '1.0.0'\n"); + fs::write(&workspace_yaml, existing).expect("write pnpm-workspace.yaml"); + + eprintln!("Creating package.json that uses the catalog protocol..."); + let manifest_path = workspace.join("package.json"); + let package_json_content = serde_json::json!({ + "dependencies": { + "@pnpm.e2e/hello-world-js-bin-parent": "catalog:", + }, + }); + fs::write(&manifest_path, package_json_content.to_string()).expect("write to package.json"); + + eprintln!("Executing command..."); + pacquet.with_arg("install").assert().success(); + + eprintln!("Make sure the package is installed at the catalog's version"); + let symlink_path = workspace.join("node_modules/@pnpm.e2e/hello-world-js-bin-parent"); + assert!(is_symlink_or_junction(&symlink_path).unwrap()); + let virtual_path = + workspace.join("node_modules/.pnpm/@pnpm.e2e+hello-world-js-bin-parent@1.0.0"); + assert!(virtual_path.exists(), "expected virtual store entry at {virtual_path:?}"); + + drop((root, mock_instance)); // cleanup +} + +/// A misconfigured catalog (specifier points at a missing entry) must +/// fail the install with the upstream `ERR_PNPM_CATALOG_ENTRY_NOT_FOUND_FOR_SPEC` +/// rather than the chain's `SPEC_NOT_SUPPORTED_BY_ANY_RESOLVER`. +#[test] +fn install_surfaces_catalog_misconfiguration() { + let CommandTempCwd { pacquet, root, workspace, npmrc_info, .. } = + CommandTempCwd::init().add_mocked_registry(); + let AddMockedRegistry { mock_instance, .. } = npmrc_info; + + eprintln!("Creating package.json with a catalog: dep but no matching catalog entry..."); + let manifest_path = workspace.join("package.json"); + let package_json_content = serde_json::json!({ + "dependencies": { + "@pnpm.e2e/hello-world-js-bin-parent": "catalog:", + }, + }); + fs::write(&manifest_path, package_json_content.to_string()).expect("write to package.json"); + + eprintln!("Executing command..."); + let output = pacquet.with_arg("install").assert().failure(); + let stderr = String::from_utf8_lossy(&output.get_output().stderr); + eprintln!("stderr={stderr}"); + // The miette report hard-wraps the message and inserts a leading + // `│` on the wrapped line. Strip all whitespace and box-drawing + // characters before substring-matching so wrap position can't + // make the assertion brittle. + let flattened: String = stderr + .chars() + .filter(|ch| !ch.is_whitespace() && !matches!(ch, '│' | '├' | '╰' | '─' | '▶' | '×')) + .collect(); + assert!( + flattened.contains( + "Nocatalogentry'@pnpm.e2e/hello-world-js-bin-parent'wasfoundforcatalog'default'.", + ), + "stderr did not mention the missing-catalog-entry error: {stderr}", + ); + + drop((root, mock_instance)); // cleanup +} + +/// Fresh-install GVS regression: `pacquet install` (no flag, no +/// lockfile) on a clean project with `enableGlobalVirtualStore: true` +/// must materialize packages under the shared +/// `/v11/links////` tree, not +/// the project-local `node_modules/.pnpm/` legacy layout. Pins the +/// fix for pnpm/pnpm#11814: before that fix the without-lockfile +/// path hardcoded `VirtualStoreLayout::legacy`, so the fresh-resolve +/// install silently fell through to project-local slots even with +/// GVS opted in. +/// +/// Also asserts that the project gets registered under +/// `/v11/projects/`, mirroring the frozen-lockfile branch +/// — the prune sweep walks that directory to learn which projects +/// still reference the shared store. +#[cfg(unix)] +#[test] +fn fresh_install_honors_enable_global_virtual_store() { + let CommandTempCwd { pacquet, root, workspace, npmrc_info, .. } = + CommandTempCwd::init().add_mocked_registry(); + let AddMockedRegistry { store_dir, mock_instance, .. } = npmrc_info; + + enable_gvs_in_workspace_yaml(&workspace, ""); + + eprintln!("Creating package.json..."); + let manifest_path = workspace.join("package.json"); + let package_json_content = serde_json::json!({ + "dependencies": { + "@pnpm.e2e/hello-world-js-bin-parent": "1.0.0", + }, + }); + fs::write(&manifest_path, package_json_content.to_string()).expect("write to package.json"); + + eprintln!("Running pacquet install (no flag, no lockfile, GVS opted in)..."); + pacquet.with_arg("install").assert().success(); + + eprintln!("Direct-dep symlink must resolve under /v11/links/..."); + let symlink_path = workspace.join("node_modules/@pnpm.e2e/hello-world-js-bin-parent"); + assert!(is_symlink_or_junction(&symlink_path).unwrap()); + let canonical = symlink_path.pipe(fs::canonicalize).expect("canonicalize symlink"); + let canonical_store = store_dir.pipe(fs::canonicalize).expect("canonicalize store_dir"); + let gvs_root = canonical_store.join("v11").join("links"); + assert!( + canonical.starts_with(&gvs_root), + "expected the package directory to live under {gvs_root:?}, got {canonical:?}", + ); + + eprintln!("Project must be registered under /v11/projects/..."); + let projects_dir = canonical_store.join("v11").join("projects"); + let projects_entries = + fs::read_dir(&projects_dir).expect("v11/projects must exist after a GVS install"); + let project_count = projects_entries.count(); + assert!( + project_count >= 1, + "expected at least one project-registry entry under {projects_dir:?}; got {project_count}", + ); + + drop((root, mock_instance)); // cleanup +} + +/// End-to-end coverage for the `cache+node_modules` shortcut. After a +/// successful install, deleting `pnpm-lock.yaml` but keeping `node_modules` +/// (and the materialized `node_modules/.pnpm/lock.yaml`) should let the +/// next `pacquet install` skip resolution and regenerate the lockfile +/// from the on-disk snapshot. Mirrors the pnpm-side fix at +/// . +#[test] +fn install_regenerates_lockfile_from_node_modules_when_wanted_is_missing() { + use std::process::Command; + let CommandTempCwd { pacquet, root, workspace, npmrc_info, .. } = + CommandTempCwd::init().add_mocked_registry(); + let AddMockedRegistry { mock_instance, .. } = npmrc_info; + + eprintln!("Creating package.json..."); + let manifest_path = workspace.join("package.json"); + let package_json = serde_json::json!({ + "dependencies": { + "@pnpm.e2e/hello-world-js-bin-parent": "1.0.0", + }, + }); + fs::write(&manifest_path, package_json.to_string()).expect("write to package.json"); + + eprintln!("Priming with the first install..."); + pacquet.with_arg("install").assert().success(); + + let lockfile_path = workspace.join("pnpm-lock.yaml"); + assert!(lockfile_path.exists(), "first install must produce pnpm-lock.yaml"); + + eprintln!("Removing pnpm-lock.yaml; node_modules/.pnpm/lock.yaml stays intact..."); + fs::remove_file(&lockfile_path).expect("remove pnpm-lock.yaml"); + // The test helper writes a `pnpm-workspace.yaml` for storeDir/cacheDir + // config, which makes `optimistic_repeat_install` treat this as a + // workspace install and skip the missing-wanted-lockfile invalidator. + // Drop the workspace state file so the freshness fast path falls + // through to the regular install dispatch where the synthesis logic + // lives. Real-world single-project installs (no pnpm-workspace.yaml) + // hit the `wanted lockfile missing` gate at + // `optimistic_repeat_install.rs:149` directly. + fs::remove_file(workspace.join("node_modules/.pnpm-workspace-state-v1.json")) + .expect("remove .pnpm-workspace-state-v1.json"); + + eprintln!("Re-running install with --reporter=ndjson..."); + let pacquet_rerun = Command::cargo_bin("pacquet") + .expect("find the pacquet binary") + .with_current_dir(&workspace); + let output = pacquet_rerun + .with_args(["--reporter=ndjson", "install"]) + .output() + .expect("run pacquet install"); + assert!( + output.status.success(), + "second install must succeed: stderr={}", + String::from_utf8_lossy(&output.stderr), + ); + + let stderr = String::from_utf8(output.stderr).expect("stderr is utf-8"); + let up_to_date = stderr + .lines() + .filter_map(|line| serde_json::from_str::(line).ok()) + .find(|record| { + record.get("name").and_then(|v| v.as_str()) == Some("pnpm") + && record.get("level").and_then(|v| v.as_str()) == Some("info") + && record.get("message").and_then(|v| v.as_str()) + == Some("Lockfile is up to date, resolution step is skipped") + }); + assert!( + up_to_date.is_some(), + "expected `name: \"pnpm\" / level: \"info\"` up-to-date log in NDJSON stderr; got:\n{stderr}", + ); + + let regenerated = fs::read_to_string(&lockfile_path).expect("pnpm-lock.yaml was regenerated"); + assert!( + regenerated.contains("@pnpm.e2e/hello-world-js-bin-parent") + && regenerated.contains("@pnpm.e2e/hello-world-js-bin"), + "regenerated pnpm-lock.yaml must list the installed packages:\n{regenerated}", + ); + + drop((root, mock_instance)); // cleanup +} + +/// End-to-end coverage for the no-op short-circuit. After a successful +/// install, a second `pacquet install --frozen-lockfile` against an +/// untouched workspace must skip materialization and emit pnpm's +/// `name: "pnpm" / level: "info"` "Lockfile is up to date, resolution +/// step is skipped" log. Mirrors upstream pnpm's behavior at +/// . +#[test] +fn frozen_install_short_circuits_when_node_modules_is_up_to_date() { + use std::process::Command; + let CommandTempCwd { pacquet, root, workspace, npmrc_info, .. } = + CommandTempCwd::init().add_mocked_registry(); + let AddMockedRegistry { mock_instance, .. } = npmrc_info; + + eprintln!("Creating package.json..."); + let manifest_path = workspace.join("package.json"); + let package_json = serde_json::json!({ + "dependencies": { + "@pnpm.e2e/hello-world-js-bin-parent": "1.0.0", + }, + }); + fs::write(&manifest_path, package_json.to_string()).expect("write to package.json"); + + eprintln!("Priming with the first install..."); + pacquet.with_arg("install").assert().success(); + + eprintln!("Re-running with --frozen-lockfile + --reporter=ndjson..."); + let pacquet_rerun = Command::cargo_bin("pacquet") + .expect("find the pacquet binary") + .with_current_dir(&workspace); + let output = pacquet_rerun + .with_args(["--reporter=ndjson", "install", "--frozen-lockfile"]) + .output() + .expect("run pacquet install --frozen-lockfile"); + assert!( + output.status.success(), + "second install must succeed: stderr={}", + String::from_utf8_lossy(&output.stderr), + ); + + let stderr = String::from_utf8(output.stderr).expect("stderr is utf-8"); + let up_to_date = stderr + .lines() + .filter_map(|line| serde_json::from_str::(line).ok()) + .find(|record| { + record.get("name").and_then(|v| v.as_str()) == Some("pnpm") + && record.get("level").and_then(|v| v.as_str()) == Some("info") + && record.get("message").and_then(|v| v.as_str()) + == Some("Lockfile is up to date, resolution step is skipped") + }); + assert!( + up_to_date.is_some(), + "expected `name: \"pnpm\" / level: \"info\"` up-to-date log in NDJSON stderr; got:\n{stderr}", + ); + + drop((root, mock_instance)); // cleanup +} diff --git a/pacquet/crates/cli/tests/lockfile_verification.rs b/pacquet/crates/cli/tests/lockfile_verification.rs new file mode 100644 index 0000000000..f3f7e9b017 --- /dev/null +++ b/pacquet/crates/cli/tests/lockfile_verification.rs @@ -0,0 +1,228 @@ +//! End-to-end CLI integration test for the lockfile-verification +//! gate ported in Phase 7. Spawns the `pacquet` binary against a +//! pnpm-workspace.yaml that activates the verifier and confirms the +//! gate fires through the real install path — non-zero exit, the +//! upstream-canonical diagnostic code in stderr. +//! +//! Doesn't try to exercise every branch — the unit tests in +//! `pacquet-lockfile-verification` and `pacquet-resolving-npm-resolver` +//! already do that. This file pins the user-visible contract: the +//! gate runs from the CLI, the install fails when policy is +//! tripped, and the error envelope carries the upstream code so +//! `pnpm errors` documentation routes to the right entry. + +pub mod _utils; +pub use _utils::*; + +use command_extra::CommandExtra; +use pacquet_testing_utils::bin::{AddMockedRegistry, CommandTempCwd}; +use std::fs; + +/// `minimumReleaseAge` set to 100 years rejects every version the +/// mocked registry has ever served. The install fails before any +/// tarball is fetched; stderr names the +/// `ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION` code so log consumers +/// and `pnpm errors` URL routing both work. +#[test] +fn install_fails_under_huge_minimum_release_age() { + let CommandTempCwd { pacquet, root, workspace, npmrc_info, .. } = + CommandTempCwd::init().add_mocked_registry(); + let AddMockedRegistry { mock_instance, .. } = npmrc_info; + + let manifest_path = workspace.join("package.json"); + let package_json = serde_json::json!({ + "dependencies": { + "@pnpm.e2e/hello-world-js-bin": "1.0.0", + }, + }); + fs::write(&manifest_path, package_json.to_string()).expect("write package.json"); + + // The mocked registry's packument times are real-world (years + // old), so a `minimumReleaseAge` set in the millions of minutes + // catches every version regardless of when the mock was + // populated. The yaml entry shape matches upstream's + // pnpm-workspace.yaml settings keys byte-for-byte. + let workspace_yaml_path = workspace.join("pnpm-workspace.yaml"); + let workspace_yaml = format!( + "{}\nminimumReleaseAge: {}\n", + fs::read_to_string(&workspace_yaml_path).expect("read workspace yaml seed"), + 60 * 24 * 365 * 100, + ); + fs::write(&workspace_yaml_path, workspace_yaml).expect("write pnpm-workspace.yaml"); + + // Hand-rolled minimal v9 lockfile pinning the same package the + // manifest above declares. The placeholder integrity is fine: + // the gate rejects the entry before the tarball is verified. + let lockfile = "lockfileVersion: '9.0'\n\ + importers:\n \ + .:\n \ + dependencies:\n \ + '@pnpm.e2e/hello-world-js-bin':\n \ + specifier: 1.0.0\n \ + version: 1.0.0\n\ + packages:\n \ + '@pnpm.e2e/hello-world-js-bin@1.0.0':\n \ + resolution: {integrity: sha512-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==}\n\ + snapshots:\n \ + '@pnpm.e2e/hello-world-js-bin@1.0.0': {}\n"; + fs::write(workspace.join("pnpm-lock.yaml"), lockfile).expect("write lockfile"); + + let output = pacquet + .with_args(["install", "--frozen-lockfile"]) + .output() + .expect("spawn pacquet install"); + + assert!( + !output.status.success(), + "the gate must reject the install (stderr: {})", + String::from_utf8_lossy(&output.stderr), + ); + let stderr = String::from_utf8_lossy(&output.stderr).into_owned(); + assert!( + stderr.contains("ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION"), + "stderr must name the upstream-canonical diagnostic code; got:\n{stderr}", + ); + + // No `node_modules/.pnpm/...` slot must materialize for the + // gated package — proves the failure short-circuits before + // tarball fetch. + assert!( + !workspace.join("node_modules/.pnpm/@pnpm.e2e+hello-world-js-bin@1.0.0").exists(), + "the gate must fail before any virtual-store materialization", + ); + + drop((root, mock_instance)); +} + +/// `trustLockfile: true` short-circuits the verification gate so a +/// lockfile that would otherwise trip the policy +/// (`minimumReleaseAge: 100 years` rejects every published version) +/// bypasses the verification step. Confirms the opt-out path runs +/// end-to-end through the CLI and that no +/// `MINIMUM_RELEASE_AGE_VIOLATION` error leaks into stderr — the test +/// stops short of asserting full install success, see the inline +/// comment above the assertion below. +#[test] +fn trust_lockfile_skips_verification() { + let CommandTempCwd { pacquet, root, workspace, npmrc_info, .. } = + CommandTempCwd::init().add_mocked_registry(); + let AddMockedRegistry { mock_instance, .. } = npmrc_info; + + let manifest_path = workspace.join("package.json"); + let package_json = serde_json::json!({ + "dependencies": { + "@pnpm.e2e/hello-world-js-bin": "1.0.0", + }, + }); + fs::write(&manifest_path, package_json.to_string()).expect("write package.json"); + + // Same provocation as the gated test above: 100 years of + // minimumReleaseAge rejects every version the mocked registry + // serves. `trustLockfile: true` is the opt-out that makes the + // install ignore the gate entirely. + let workspace_yaml_path = workspace.join("pnpm-workspace.yaml"); + let workspace_yaml = format!( + "{}\nminimumReleaseAge: {}\ntrustLockfile: true\n", + fs::read_to_string(&workspace_yaml_path).expect("read workspace yaml seed"), + 60 * 24 * 365 * 100, + ); + fs::write(&workspace_yaml_path, workspace_yaml).expect("write pnpm-workspace.yaml"); + + let lockfile = "lockfileVersion: '9.0'\n\ + importers:\n \ + .:\n \ + dependencies:\n \ + '@pnpm.e2e/hello-world-js-bin':\n \ + specifier: 1.0.0\n \ + version: 1.0.0\n\ + packages:\n \ + '@pnpm.e2e/hello-world-js-bin@1.0.0':\n \ + resolution: {integrity: sha512-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==}\n\ + snapshots:\n \ + '@pnpm.e2e/hello-world-js-bin@1.0.0': {}\n"; + fs::write(workspace.join("pnpm-lock.yaml"), lockfile).expect("write lockfile"); + + let output = pacquet + .with_args(["install", "--frozen-lockfile"]) + .output() + .expect("spawn pacquet install"); + + // Asserting only on the absence of the verifier error code, not + // `output.status.success()`: the test fixture's `pnpm-lock.yaml` + // is hand-rolled with a placeholder integrity hash, so the + // install fails the tarball integrity check downstream of the + // verification pass. That's irrelevant to what's being tested — + // the contract here is "the supply-chain gate doesn't fire", + // not "the install completes". Asserting success would require a + // real lockfile generated against the mocked registry first + // (see hoist.rs's `generate_lockfile` pattern); not worth the + // extra wiring for a smoke test of the opt-out switch. + let stderr = String::from_utf8_lossy(&output.stderr).into_owned(); + assert!( + !stderr.contains("ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION"), + "trustLockfile must skip the verification gate; got:\n{stderr}", + ); + + drop((root, mock_instance)); +} + +/// `--trust-lockfile` CLI flag short-circuits the verification gate +/// the same way `trustLockfile: true` in `pnpm-workspace.yaml` does. +/// Same provocation as the yaml-based test above, with the yaml +/// override removed so the gate would normally fire — the flag is +/// what makes the verification gate skip. Like that test, this only +/// asserts the absence of `MINIMUM_RELEASE_AGE_VIOLATION` (not full +/// install success); see the inline comment above the assertion. +#[test] +fn trust_lockfile_cli_flag_skips_verification() { + let CommandTempCwd { pacquet, root, workspace, npmrc_info, .. } = + CommandTempCwd::init().add_mocked_registry(); + let AddMockedRegistry { mock_instance, .. } = npmrc_info; + + let manifest_path = workspace.join("package.json"); + let package_json = serde_json::json!({ + "dependencies": { + "@pnpm.e2e/hello-world-js-bin": "1.0.0", + }, + }); + fs::write(&manifest_path, package_json.to_string()).expect("write package.json"); + + let workspace_yaml_path = workspace.join("pnpm-workspace.yaml"); + let workspace_yaml = format!( + "{}\nminimumReleaseAge: {}\n", + fs::read_to_string(&workspace_yaml_path).expect("read workspace yaml seed"), + 60 * 24 * 365 * 100, + ); + fs::write(&workspace_yaml_path, workspace_yaml).expect("write pnpm-workspace.yaml"); + + let lockfile = "lockfileVersion: '9.0'\n\ + importers:\n \ + .:\n \ + dependencies:\n \ + '@pnpm.e2e/hello-world-js-bin':\n \ + specifier: 1.0.0\n \ + version: 1.0.0\n\ + packages:\n \ + '@pnpm.e2e/hello-world-js-bin@1.0.0':\n \ + resolution: {integrity: sha512-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==}\n\ + snapshots:\n \ + '@pnpm.e2e/hello-world-js-bin@1.0.0': {}\n"; + fs::write(workspace.join("pnpm-lock.yaml"), lockfile).expect("write lockfile"); + + let output = pacquet + .with_args(["install", "--frozen-lockfile", "--trust-lockfile"]) + .output() + .expect("spawn pacquet install"); + + // Same reasoning as the yaml-opt-out test above: not asserting + // `output.status.success()` because the hand-rolled lockfile's + // placeholder integrity trips the downstream tarball check. The + // contract being tested is gate-skipped, not install-succeeded. + let stderr = String::from_utf8_lossy(&output.stderr).into_owned(); + assert!( + !stderr.contains("ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION"), + "--trust-lockfile must skip the verification gate; got:\n{stderr}", + ); + + drop((root, mock_instance)); +} diff --git a/pacquet/crates/cli/tests/pnpm_compatibility.rs b/pacquet/crates/cli/tests/pnpm_compatibility.rs index 67a0c068ec..5b1b8f554e 100644 --- a/pacquet/crates/cli/tests/pnpm_compatibility.rs +++ b/pacquet/crates/cli/tests/pnpm_compatibility.rs @@ -45,10 +45,18 @@ fn same_file_structure() { let AddMockedRegistry { store_dir, mock_instance, .. } = npmrc_info; let modules_dir = workspace.join("node_modules"); + // Cleanup also drops `pnpm-lock.yaml` because the fresh-lockfile + // install path writes one, and leaving it would let pnpm's second + // install pick a different code path (frozen-with-existing-lockfile) + // than pacquet's first install (fresh), which the + // `.pnpm-needs-build-marker` artifact in the GVS store difference + // would surface as a spurious diff here. + let lockfile_path = workspace.join("pnpm-lock.yaml"); let cleanup = || { eprintln!("Cleaning up..."); fs::remove_dir_all(&store_dir).expect("delete store dir"); fs::remove_dir_all(&modules_dir).expect("delete node_modules"); + let _ = fs::remove_file(&lockfile_path); }; eprintln!("Creating package.json..."); @@ -73,16 +81,16 @@ fn same_file_structure() { .into_iter() // Per-project metadata that pnpm 11 populates and pacquet doesn't. // Doesn't affect the shared-cafs story. - .filter(|p| !p.starts_with("v11/projects/")) + .filter(|path| !path.starts_with("v11/projects/")) // Hoisted-symlinks layout introduced in pnpm 11 — pnpm stores // one `node_modules` tree per `///` under // `v11/links/` and links the project's `node_modules/X` into there. // Pacquet still uses the older per-project `.pnpm/` virtual store, // so these paths exist only on the pnpm side. - .filter(|p| !p.starts_with("v11/links/")) + .filter(|path| !path.starts_with("v11/links/")) // SQLite WAL sidecars exist only while a connection holds the // journal open. Their presence at compare-time depends on timing. - .filter(|p| p != "v11/index.db-wal" && p != "v11/index.db-shm") + .filter(|path| path != "v11/index.db-wal" && path != "v11/index.db-shm") .collect() }; @@ -118,10 +126,15 @@ fn same_index_file_contents() { let AddMockedRegistry { store_dir, mock_instance, .. } = npmrc_info; let modules_dir = workspace.join("node_modules"); + // Cleanup also drops `pnpm-lock.yaml` so pnpm doesn't pick a + // different install code path than pacquet on the second leg — + // see the matching note in `same_file_structure`. + let lockfile_path = workspace.join("pnpm-lock.yaml"); let cleanup = || { eprintln!("Cleaning up..."); fs::remove_dir_all(&store_dir).expect("delete store dir"); fs::remove_dir_all(&modules_dir).expect("delete node_modules"); + let _ = fs::remove_file(&lockfile_path); }; eprintln!("Creating package.json..."); @@ -218,27 +231,10 @@ fn pnpm_reads_pacquet_written_rows() { /// alongside any inner-shape disagreement instead of being silently /// normalized away. fn gvs_paths_only(files: Vec) -> Vec { - files.into_iter().filter(|p| p.starts_with("links/") || p.starts_with("v11/links/")).collect() -} - -/// Append GVS opt-in (and any extra fields) to the `pnpm-workspace.yaml` -/// that [`CommandTempCwd::add_mocked_registry`] already populated with -/// `storeDir` / `cacheDir`. `enableGlobalVirtualStore: true` is the -/// switch that flips both pnpm and pacquet to the shared-store layout. -fn enable_gvs_in_workspace_yaml(workspace: &std::path::Path, extra_yaml: &str) { - let yaml_path = workspace.join("pnpm-workspace.yaml"); - let mut yaml = fs::read_to_string(&yaml_path).expect("read pnpm-workspace.yaml"); - // Guarantee a newline before the appended keys. If the helper - // that wrote the file ever drops the trailing newline, naive - // concatenation would merge its last key with - // `enableGlobalVirtualStore` and produce invalid YAML — flagged - // by CodeRabbit on PR #11689. - if !yaml.ends_with('\n') { - yaml.push('\n'); - } - yaml.push_str("enableGlobalVirtualStore: true\n"); - yaml.push_str(extra_yaml); - fs::write(&yaml_path, yaml).expect("write pnpm-workspace.yaml"); + files + .into_iter() + .filter(|path| path.starts_with("links/") || path.starts_with("v11/links/")) + .collect() } /// Run pnpm-then-pacquet against a shared workspace and compare the diff --git a/pacquet/crates/cli/tests/run.rs b/pacquet/crates/cli/tests/run.rs new file mode 100644 index 0000000000..fad051ec20 --- /dev/null +++ b/pacquet/crates/cli/tests/run.rs @@ -0,0 +1,117 @@ +use assert_cmd::prelude::*; +use command_extra::CommandExtra; +use pacquet_testing_utils::bin::CommandTempCwd; +use serde_json::json; +use std::fs; + +/// `pacquet run