mirror of
https://github.com/pnpm/pnpm.git
synced 2026-07-02 11:55:17 -04:00
c3ca5cda13985ef4a21035cb4dcdfe6d1f3e27fa
11488 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
c3ca5cda13 |
style(package-manifest): drop trailing comma in single-line assert!
Dylint's perfectionist::macro-trailing-comma rule rejected the single- line `assert!` formatting I'd accidentally inherited from the multi- line shape — remove the trailing comma so the lint clears. https://claude.ai/code/session_01D8WBTfQzTpsZsRknrzwNKL |
||
|
|
5ce101062f |
test: fill coverage holes across lockfile, package-manifest, fs, and friends
Adds ~44 unit and integration tests to close the easier targets in the coverage analysis at #339-style boundaries. Touches: - `lockfile::resolved_dependency`: cover `as_alias` / `ver_peer` for non-matching variants, alias / parse error variants, `TryFrom<Cow>`, serialize-alias / serialize-link, `From<PkgVerPeer> / From<PkgNameVerPeer>`, and a Display vs. `String` round-trip. - `lockfile::freshness`: cover the plural arms of `SpecDiff::Display` and the comma separator inside the removed/modified loops. - `lockfile::pkg_name`: cover `TryFrom<String>` and `TryFrom<Cow>` happy and empty-input paths. - `lockfile::snapshot_dep_ref`: cover `ver_peer` and `From<PkgVerPeer>`. - `lockfile::save_lockfile`: cover the `CreateDir` / `RemoveFile` / `RenameFile` error classifications by planting a regular file or directory where the writer expects a file or a writable path. - `registry::package`: cover `PartialEq`, `latest`, and `pinned_version` happy and no-match paths. - `graph-hasher::engine_name`: cover `detect_node_version` / `detect_node_major` when `node` is on PATH; skip cleanly otherwise. - `graph-hasher::object_hasher`: cover the null / bool / array arms of the bytestream serializer. - `patching::apply`: cover non-NotFound read-patch error, partial-delete non-empty result, unsupported rename/copy operation, and `Create` with an unwritable parent. - `workspace-state`: cover the `CreateDir` / `ReadFile` / `ParseJson` error variants. - `package-manifest`: cover `from_path` ENOENT, `add_dependency` on a non-object field, `safe_read_package_json_from_dir` non-NotFound IO error, and `convert_engines_runtime_to_dependencies` for unsupported shapes. - `fs::file_mode`: cover `EXEC_MASK` / `EXEC_MODE` constants, `is_executable` for all positions, and `make_file_executable` on Unix. - `executor`: cover `execute_shell` happy and non-zero-exit paths. - `cli/tests/run.rs`: new integration tests for `pacquet run`, including argument forwarding and `--if-present`. Also bumps `hex_decode` in `graph-hasher::tests` to use `is_multiple_of` so test-time clippy stays clean under Rust 1.95. https://claude.ai/code/session_01D8WBTfQzTpsZsRknrzwNKL |
||
|
|
f46757d732 |
fix(cmd-shim): symlink node runtime binary instead of cmd-shimming it (#11707)
* fix(cmd-shim): symlink node runtime binary instead of cmd-shimming it
Ports pnpm v11's `cmd.name === 'node'` short-circuit from
`bins/linker/src/index.ts:281-308`: on Unix, symlink `.bin/node`
directly at the runtime binary; on Windows, hardlink (or copy on
hardlink failure) the source to `<bin>.exe` when the source ends in
`.exe`, otherwise fall through to the cmd-shim path. Adds two
`LinkBinsError` variants (`RemoveStaleBin`, `LinkNodeBin`).
Without the short-circuit, the node binary was wrapped in a
`/bin/sh "$basedir/../node/bin/node"` shim. On any subsequent run that
read it back as a shim source, `search_script_runtime` parsed the
`#!/bin/sh` shebang and emitted a wrapper around the wrapper, with a
self-referencing relative target (`node/bin/../node/bin/node` — the
`node` segment appears twice and resolves to a non-existent path).
`remove_file`-before-link, rather than `fs::write` truncation, also
prevents the corrupt-the-source-binary failure mode that arises when
the existing dirent is hardlinked into the GVS slot from the store.
Tests mirror `bins/linker/test/index.ts:643-735` and add a regression
test (`link_node_bin_does_not_corrupt_hardlinked_target`) that
pre-hardlinks the bin slot to the source and verifies the binary
content survives.
---
Written by an agent (Claude Code, claude-opus-4-7).
* chore(cmd-shim): rewrite "mis-handle" to "mishandle" for typos check
The hyphenated form trips `typos` ("mis" flagged as "miss"/"mist").
No behavior change.
---
Written by an agent (Claude Code, claude-opus-4-7).
|
||
|
|
8df408c901 |
fix(config): warn when package.json has a legacy "pnpm" field with migrated settings (#11680)
* fix(config): warn when package.json has a legacy "pnpm" field In v11, pnpm stopped reading settings from the `pnpm` field of package.json (#10086). Most former pnpm-field settings now live in `pnpm-workspace.yaml`; a few (e.g. `onlyBuiltDependencies`, `executionEnv`) were removed entirely. Until now the old field was silently ignored, so users upgrading from v10 had no signal that their overrides or patched dependencies had stopped taking effect. Emit a warning whenever the `pnpm` field contains any key that pnpm no longer reads from package.json. The check is an allowlist (only `pnpm.app`, consumed by `pnpm pack-app`, is still active), so the warning won't go stale as new settings are added or removed in future versions. The message points users at https://pnpm.io/settings rather than prescribing a single fix, since the new home depends on the key. Closes #11677. * fix(config): only warn for migrated pnpm-field keys, not unrelated ones Previously the warning fired for every key under `pnpm` except `app`, which would surface false positives for third-party tooling that piggybacks on the `pnpm` namespace. Switch to an explicit denylist of the v10 settings that moved to pnpm-workspace.yaml, matching the PR's stated contract. --------- Co-authored-by: Damon <damon@deeplearning.ai> Co-authored-by: Zoltan Kochan <z@kochan.io> |
||
|
|
06d2d3deb2 |
fix: write packageManagerDependencies to lockfile when devEngines.packageManager is set (#11681)
When `devEngines.packageManager.pnpm` is set without `onFail: "download"`,
`pnpm install` ran `syncEnvLockfile` instead of `switchCliVersion`. That sync
returned early whenever the env lockfile did not already record a
`packageManagerDependencies.pnpm` entry, so the resolved pnpm version was
never recorded on first install — contradicting the documented behavior
("The resolved version is stored in pnpm-lock.yaml") and forcing users to
add `onFail: "download"` purely to trigger the lockfile write.
Drop the two early-returns that only fired when the env lockfile was
missing or empty. The resolution proceeds whenever (a) the project pins a
pnpm version via `devEngines.packageManager` (or a v12+ `packageManager`
field) and (b) the running pnpm satisfies that pin. The existing
"already-resolved" no-op path still skips work when the lockfile already
records a satisfying version, so steady-state installs don't churn.
Closes #11674 (part 1). Part 3 (pruning `@pnpm/exe` platform entries when
`onFail: "download"` is removed) is left for a follow-up — it needs a
state-transition signal the codebase doesn't yet track.
Co-authored-by: Damon <damon@deeplearning.ai>
|
||
|
|
963861cac1 |
perf(npm-resolver): layer abbreviated meta + attestation before full metadata in the minimumReleaseAge gate (#11704)
Follow-up to #11691 — item 2 from #11687, plus a related shortcut. ## What When the `minimumReleaseAge` lockfile verification gate needs to know when a version was published, it used to fetch a multi-MB full metadata document per package just to read one timestamp. This PR replaces that single-step path with a four-layer lookup that pays the cheapest viable source first: 1. **Abbreviated metadata's `modified` field** — the resolver already fetches this for resolution. If the package as a whole hasn't been modified within the policy cutoff, every version it contains is at least that old; return `modified` as a conservative upper-bound and skip the rest of the chain. 2. **Local `FULL_META_DIR` mirror** — exact per-version times if a previous verification populated it. 3. **npm attestation endpoint** (`/-/npm/v1/attestations/<name>@<version>`) — a tens-of-KB Sigstore bundle whose Rekor inclusion time stands in for publish time. Wins on cold cache when the package was published with provenance. 4. **Full metadata fetch** — last resort. ## Why The verification cache from #11691 made repeat installs against an unchanged lockfile effectively free. The remaining cost is the *first* verification on a fresh CI runner with no restored cache — particularly `pnpm install --frozen-lockfile`, where every locked package's publish timestamp has to be confirmed. Fetching the full metadata document for each package is wasteful when: - The resolver has usually already cached abbreviated metadata, whose `modified` field alone is enough to clear stable packages (the common case). - For recently-modified packages, the per-version attestation endpoint is orders of magnitude smaller than full metadata. ## How ### Abbreviated `modified` shortcut `fetchFullMetadataCached` is refactored to share an internal helper with a new `fetchAbbreviatedMetadataCached`. Both do conditional GETs against their respective on-disk mirrors. On a non-frozen install the abbreviated mirror is already populated by the resolver, so the shortcut hits the local cache at headers-only cost. On `--frozen-lockfile` the fetch is still cheaper than full metadata. If `Date.parse(modified) < cutoff`, return `modified` — it's an upper bound on every version's publish time in this package, and the verifier's `published < cutoff` check passes trivially. ### Attestation endpoint `fetchAttestationPublishedAt` (new module) hits `/-/npm/v1/attestations/<name>@<version>`, parses the response, and reads the earliest `bundle.verificationMaterial.tlogEntries[].integratedTime` across the attestation bundles. That's the Rekor inclusion time — a couple of seconds after publish, well within tolerance for a policy that operates in minutes/hours/days. Returns `undefined` on 404 / network error / malformed body so the caller falls back. ### Per-install dedup The lookup carries a `PublishedAtLookupContext` with four memo maps: abbreviated meta per (registry, name), local full meta per (registry, name), full meta network fetch per (registry, name), final published-at per (registry, name, version). Verifying many versions of the same package only pays the disk/network costs once. ## Trust model **No Sigstore signature verification on the attestation path.** The trust model is identical to reading the registry's `time` field on the full metadata document — we already trust the registry to serve correct publish timestamps for the gate's purpose. The win is purely bandwidth. Full Sigstore verification (Fulcio cert chain + Rekor inclusion proof) would harden the timestamp against a compromised registry. It pulls in the `sigstore` npm package and the TUF root — a separate dependency-surface discussion, parked as future work. ## Tests - **13 unit tests** in `resolving/npm-resolver/test/fetchAttestationPublishedAt.test.ts`: ISO timestamp extraction, URL construction (scoped + unscoped), 404 / 5xx / network error / malformed JSON / missing fields → undefined, earliest-of-multiple-attestations, defensive number-as-integratedTime, auth header forwarding, trailing-slash normalization. - Existing `minimumReleaseAge` + `verifyLockfileResolutions` integration suites (45 tests) still pass — the fallback chain preserves end-to-end behavior when the new shortcuts don't apply. |
||
|
|
ba2c8844c9 |
fix(config): apply pmOnFail default to devEngines.packageManager (singular) (#11682)
* fix(config): apply pmOnFail default to devEngines.packageManager (singular)
The pnpm v11 release notes document the `pmOnFail` default as `download`
(via the migration table that maps `managePackageManagerVersions: true` →
`pmOnFail: download (default)`). The legacy `packageManager` field already
gets that default applied at the central onFail-resolution site, but the
singular form of `devEngines.packageManager` short-circuited it by setting
`onFail = 'error'` inside `parseDevEnginesPackageManager`, so projects that
pinned a different pnpm via `devEngines.packageManager` saw a hard version
mismatch instead of an auto-download.
Drop that local `?? 'error'` and let the central default apply. The array
form of `devEngines.packageManager` keeps its own per-element defaults
('error' for the last entry, 'ignore' for the rest) — those reflect
explicit prioritisation by the user, not a system-wide fallback. Explicit
`onFail` values are still honored everywhere.
Closes #11676.
* chore: fix spelling (prioritisation → prioritization)
cspell flagged the British spelling at pre-push.
---------
Co-authored-by: Damon <damon@deeplearning.ai>
|
||
|
|
5dc8be8a42 |
fix(graph-hasher): resolve GVS engine per-snapshot for runtime-pinned deps (#11693)
Closes #11690. A dependency that declares `engines.runtime` in its manifest carries the desugared `dependencies.node: 'runtime:<version>'` pin in the lockfile, and pnpm's bin linker spawns that dep's lifecycle scripts through the pinned Node downloaded into `<pkgDir>/node_modules/node/`. The GVS hash and the side-effects-cache key prefix were still anchored to the install-wide runtime — so the 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 `<platform>;<arch>;node<major>` 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: a snapshot's own pin wins; the install-wide value (from #11689's `findRuntimeNodeVersion`) is the fallback. ### TypeScript - `deps/graph-hasher/src/index.ts:72-77` — adds `readSnapshotRuntimePin(children)`: pulls the bare Node version from a graph node's `children.node` entry when that points at a `node@runtime:<version>` snapshot. Factors out a small `extractRuntimeNodeVersion(snapshotKey)` parser shared with `findRuntimeNodeVersion`. - `deps/graph-hasher/src/index.ts:115-116,245-246` — `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. No caller changes required — install-wide fallback continues to be computed via `findRuntimeNodeVersion(Object.keys(graph))` at each call site. - **Refactor (separate commit):** `findRuntimeNodeVersion` moved from `@pnpm/engine.runtime.system-node-version` to `@pnpm/deps.graph-hasher` (along with the new `readSnapshotRuntimePin`). `system-node-version` is about probing the *host* Node — `getSystemNodeVersion`, `engineName`. The lockfile-shape parsers fit better next to the package that actually composes the engine string. Every caller already depended on graph-hasher, so no new deps; six packages drop the now-unused dependency on `system-node-version`. ### Pacquet - `pacquet/crates/package-manager/src/install_frozen_lockfile.rs:1309-1345` — new `find_own_runtime_node_major(snapshot)` reads a snapshot's `dependencies` for a `node` entry with `Prefix::Runtime`, returning the bare major. - `pacquet/crates/package-manager/src/virtual_store_layout.rs:178-205` — `VirtualStoreLayout::new` resolves engine per-snapshot inside the hash loop via `engine_name(own_major, None, None)` when the snapshot pins, otherwise inherits the install-wide `engine` argument. ### Migration Snapshots of dependencies that declare their own `engines.runtime` re-hash under that dep's pinned Node instead of the install-wide value. Old slots become prune-eligible on next install. |
||
|
|
020ac45d3d |
fix: tolerate padded auth base64 (#11694)
* fix: tolerate padded auth base64 * fix: avoid regex in auth padding normalization |
||
|
|
fcf95c7faa |
perf: cache the post-resolution lockfile verification gate (#11691)
Closes #11687. ## What Cache the result of the post-resolution lockfile verification gate (#11583) so repeat installs against an unchanged lockfile skip the per-package registry round trips entirely. Persisted as JSON Lines at `<cacheDir>/lockfile-verified.jsonl`. The cache layer is policy-neutral. Today there's one verifier (`minimumReleaseAge`); future resolver-side verifiers (jsr trust, attestation, …) plug in by declaring their own `policy` slot and `canTrustPastCheck` comparator — no install-side changes. ## Why #11583 re-hits the registry on every install for every locked (name, version) pair. On warm/repeat installs where the lockfile hasn't moved, that's a stack of per-package round trips with nothing to show for them. This change makes the steady-state case effectively free without weakening the protection — the gate still runs in full whenever the lockfile changes, any verifier's policy tightens, or no record exists. ## How ### Cache lookup, in order The cache is **indexed by content hash** so git worktrees with identical lockfile bytes share a cache entry. A secondary path-keyed index drives the same-machine stat shortcut. 1. **`stat()` shortcut** — when a previous record for this exact `lockfilePath` matches today's `size + mtime + inode`, trust the cached hash without reading anything. Zero I/O beyond the stat. Microseconds. 2. **Content lookup** — hash the in-memory lockfile (not the file bytes — we already have the parsed object) and look up by content hash. Catches worktrees (same content, different path) and CI checkouts (same content, reset stat). On hit, append a refreshed path/stat entry so the next install at this path takes the stat shortcut. 3. **Any active verifier rejects the cached `policy`** — run the full gate. 4. **No record** — run the full gate. The in-memory object is hashed with `hashObject` from `@pnpm/crypto.object-hasher` (streaming, key-order-stable). ### Record shape ```json { "lockfile": { "hash": "<sha256 base64>", "path": "/abs/path/to/pnpm-lock.yaml", "size": 154, "mtimeNs": "1736245123000000000", "inode": "12345" }, "verifiedAt": "2026-05-17T...", "policy": { "minimumReleaseAge": 1440 } } ``` `policy` is the union of every active verifier's `policy` contribution. Verifiers checking the same logical policy (e.g. `minimumReleaseAge` honored by multiple registries) name it the same and share the slot — no resolver namespacing. ### File semantics - **Sync fs throughout** — the cache is consulted once before verification fan-out and recorded once after. No concurrent install work to overlap with; keeping the call sites straight-line. - **JSONL appends are atomic** on POSIX/NTFS, so parallel pnpm processes (monorepo installs, CI matrices sharing a cache) write without coordination. Latest record per `(path, hash)` tuple wins on read. - **Bounded file** — capped at ~1000 entries; compaction is triggered by a single `stat()` of the cache file (1.5 MiB byte budget) so we never parse the file on the steady-state path. When triggered, the tail is rewritten via tempfile + rename. - **No record on rejection** — a failing verification deliberately doesn't write a record; the next install must rerun the gate. - **Single hash per install** — the in-memory hash is computed lazily and reused: `tryLockfileVerificationCache` returns the precomputed stat+hash to `recordVerification` on a miss, and the stat-shortcut hit forwards the cached record's hash unchanged. ## Plumbing The verifier contract changed alongside the cache to make this composable without install-side knowledge of each policy: - **`@pnpm/resolving.resolver-base`** — `ResolutionVerifier` is now `{ verify, policy, canTrustPastCheck }` (was a bare function in #11583). Each resolver-side verifier owns its policy snapshot and the comparator that decides whether a cached policy is still trustworthy. - **`@pnpm/resolving.npm-resolver`** — `createNpmResolutionVerifier` returns the new shape: `policy: { minimumReleaseAge }`, `canTrustPastCheck` reads `minimumReleaseAge` from the merged cached bag. - **`@pnpm/resolving.default-resolver`** — `createResolutionVerifier` (singular, returning a combined function) → `createResolutionVerifiers` (plural, returning a `ResolutionVerifier[]`). No combinator; each verifier handles its own protocol short-circuit inside `verify`, so dispatch happens naturally at the install side. - **`@pnpm/installing.client`** — `Client.verifyResolution?` → `Client.resolutionVerifiers: ResolutionVerifier[]`. Same rename propagates through `@pnpm/store.connection-manager`, `@pnpm/testing.temp-store`, and `StrictInstallOptions`. - **`@pnpm/installing.deps-installer`** — new `verifyLockfileResolutionsCache.ts` (`tryLockfileVerificationCache` + `recordVerification`). `verifyLockfileResolutions` takes the verifier list plus `cacheDir` + `lockfilePath` as flat options; the cache fires when both are present, otherwise the gate runs without memoization. The dedup key for in-flight candidates includes a serialization of `resolution` so two entries sharing a (name, version) but pinned via different protocols don't collapse. Breaking but safe — `@pnpm/resolving.npm-resolver` hasn't been released since #11583 introduced the verifier abstraction, so no downstream consumer is on the old shape. ## Tests - **17 unit tests** in `verifyLockfileResolutionsCache.ts`: cache miss/hit, stat shortcut, size mismatch falling through to hash lookup, hash-fallback on reset stat, content change with matching size, stricter/weaker policy, missing-field policy rejection, multi-verifier policy merge (shared field stored once), worktree case (same content, different path), JSONL append semantics, malformed-line tolerance. - **12 integration tests** in `verifyLockfileResolutions.ts`: dedup of peer/patch-suffix variants, distinct-resolution dedup at the same (name, version), stable violation ordering, the 20-entry cap, multi-verifier fan-out (first failure wins), cache short-circuit on a passing run, no cache write on a rejecting run, empty-verifier-list passthrough. - **1 e2e test** in `pnpm/test/install/minimumReleaseAge.ts`: bundled CLI plumbing — install once to seed the lockfile, enable `minimumReleaseAge` + `cacheDir`, install again, assert the cache file lands at `<cacheDir>/lockfile-verified.jsonl` with the documented record shape. - Existing `minimumReleaseAge` (13) and `frozenLockfile` (12) suites still pass. |
||
|
|
3ddde2b975 |
fix(pacquet): align GVS slot layout with pnpm (#11689)
## Summary Adds three end-to-end **GVS parity tests** under `pacquet/crates/cli/tests/pnpm_compatibility.rs` that run `pnpm install` and `pacquet install --frozen-lockfile` against the same workspace + lockfile with `enableGlobalVirtualStore: true`, then diff the resulting `<store>/v11/links/` slot trees. The tests surfaced three independent divergences, each fixed in its own commit set: 1. **`<store>/v11/links` prefix.** `getStorePath` appends `STORE_VERSION` (`v11`) to the configured `storeDir` before `extendInstallOptions.ts:352` joins `'links'` onto it, so pnpm's GVS lives at `<store>/v11/links/` — pacquet's `StoreDir::links()` was one level shallower, joining onto `self.root`. Same gap on `projects()`. Anchored both under `self.v11()` so the on-disk paths agree. 2. **GVS engine-name resolution.** `ENGINE_NAME` was computed from `process.version`, which is wrong in two cases: - **`@pnpm/exe` SEA bundle.** The bundle has its own embedded Node, not the `node` on PATH that runs lifecycle scripts. Two pnpm installs 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. - **`engines.runtime` / `devEngines.runtime` pin.** When a project pins a Node version, 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. `@pnpm/engine.runtime.system-node-version` now exports `engineName(nodeVersion?)` and `findRuntimeNodeVersion(snapshotKeys)`. The override has priority; otherwise the helper falls through to `getSystemNodeVersion()` — which already prefers shell `node --version` over `process.version` in SEA contexts — and finally to `process.version` as a last resort. `@pnpm/deps.graph-hasher`'s `calcDepState`, `calcGraphNodeHash`, and `iterateHashedGraphNodes` accept an optional `nodeVersion`. Every install-side caller (`deps.graph-builder`, `installing.deps-resolver`, `installing.deps-restorer`, `installing.deps-installer/install/link`, `building.during-install`, `building.after-install`) derives the project's pinned runtime via `findRuntimeNodeVersion` once per invocation and forwards it. The legacy `ENGINE_NAME` constant in `@pnpm/constants` is unchanged so external consumers and existing tests keep working. Pacquet mirrors this with `find_runtime_node_major` in `install_frozen_lockfile.rs` — it scans the lockfile's `snapshots:` map for a `node@runtime:<version>` entry and uses that major outright, only falling back to the host probe when no pin is present. 3. **Slot bin-shim layout.** Pacquet was emitting `.cmd` / `.ps1` shims on every host platform, even though pnpm only writes them on Windows ([`@zkochan/cmd-shim` `createCmdFile: isWindows`](https://github.com/pnpm/cmd-shim/blob/0d79ca9534/src/index.ts#L32) + `bins/linker`'s [`POWER_SHELL_IS_SUPPORTED = IS_WINDOWS`](https://github.com/pnpm/pnpm/blob/29a42efc3b/bins/linker/src/index.ts#L28) gate). Pacquet also excluded the slot's own package from the slot-local `node_modules/.bin/` based on a stale assumption ("which pnpm doesn't"), but pnpm's [`linkBinsOfDependencies`](https://github.com/pnpm/pnpm/blob/29a42efc3b/building/during-install/src/index.ts#L272-L298) appends `depNode` to the bin-source list unconditionally, so a leaf package like `hello-world-js-bin` writes a self-shim at `<slot>/node_modules/<pkg>/node_modules/.bin/<pkg>`. Both behaviors now match pnpm. ## Test plan - [x] `cargo nextest run -p pacquet-cli --test pnpm_compatibility` — 5 active tests pass, 1 ignored (see below) - [x] `cargo nextest run -p pacquet-store-dir -p pacquet-config -p pacquet-cmd-shim -p pacquet-package-manager` — 600+ tests pass after the prefix / bin-shim updates - [x] `same_global_virtual_store_layout_pure_js` — pacquet & pnpm produce byte-identical `<store>/v11/links/` trees for `@pnpm.e2e/hello-world-js-bin-parent` - [x] `same_global_virtual_store_layout_diamond` — same for `pkg-with-1-dep` + `parent-of-pkg-with-1-dep`, verifying `calc_dep_graph_hash` memoization parity - [x] Three new TS unit tests in `engine/runtime/system-node-version/test/` cover the `engineName(version)` override branch and `findRuntimeNodeVersion`'s extraction rule (with and without peer suffix) - [ ] `same_global_virtual_store_layout_with_approved_postinstall` is currently `#[ignore]`d. It requires pnpm and pacquet to agree on the `<platform>;<arch>;node<major>` triple in the engine-included hash branch. The `pnpm/setup` action on CI installs an `@pnpm/exe` SEA bundle whose embedded Node (node26) differs from the runner's PATH `node` (node24), so the digests don't line up. The pnpm-side fix in this PR resolves `engineName()` via `getSystemNodeVersion()` which prefers the shell `node`, so once a published pnpm version with the fix reaches `pnpm/setup` the test will pass without modification — re-enable it then. The other two GVS parity tests are unaffected since they exercise the engine-agnostic branch. ## Notes - Two pacquet integration tests in `package-manager/src/install/tests.rs` had hard-coded `<store_dir>/projects/` assertions; updated to `<store_dir>/v11/projects/` to follow the prefix fix. - The `link_bins_rewrites_when_only_sh_flavor_exists` cmd-shim test is now `#[cfg(windows)]` — the upgrade-recovery scenario it exercises is meaningless on Unix where `.cmd`/`.ps1` are no longer written in the first place. - Review feedback addressed: (a) test YAML helper now guarantees a trailing newline before appending GVS keys; (b) `findRuntimeNodeVersion` calls in `installing/deps-restorer/` switched from `Object.keys(graph)` (install-dir-keyed in that module) to extracting `depPath` per node, with the computation lifted out of the recursion; (c) `dlx.e2e.ts`'s `jest.unstable_mockModule` against `@pnpm/engine.runtime.system-node-version` now forwards every exported symbol so transitive importers of `engineName` don't break. - Known caveat: pacquet's non-lockfile install path (`run_with_readdir`) still excludes the slot's own bin via `link_bins_excluding`. That path runs only for the legacy flat layout where GVS parity isn't a constraint, so it's deliberately out of scope here. - Known caveat tracked in #11690: when a dependency's own manifest declares `engines.runtime`, the resolver desugars it into a regular `dependencies.node: 'runtime:<v>'` entry on that package, so the **deps** portion of the hash captures it on both sides. The **engine** portion is still install-wide rather than per-snapshot, so cached side-effects for dep-pinned runtimes can be reused under the wrong host Node. pnpm has this same gap today; closing it on both sides requires per-snapshot engine resolution and is outside this PR's scope. |
||
|
|
31538bf8d2 |
fix: enforce minimumReleaseAge on existing lockfile entries (#11583)
Closes #10438. ## What Re-verify every entry in `pnpm-lock.yaml` against the policies the resolver chain was configured with — today: `minimumReleaseAge` in strict mode — right after the lockfile is loaded from disk and before any tarball is fetched. A locked version that fails the policy aborts the install with `ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION`; `minimumReleaseAgeExclude` is honored. ## Why The policy only fires while pnpm is *choosing* a version. Once a version is pinned in the lockfile — e.g. a developer disabled the policy locally and committed a fresh dependency, or a CI cache restored a stale lockfile — every later `pnpm install` (including `--frozen-lockfile` and `pnpm fetch`) installs it without re-checking, which defeats the supply-chain protection the setting is supposed to provide. The threat model is **a lockfile someone else resolved**, not local resolution: local resolution is already covered by the resolver's own per-version filter. bun fixed the same shape of bug in [oven-sh/bun#30526](https://github.com/oven-sh/bun/pull/30526); this PR is the pnpm side. ## How The fix introduces a generic `ResolutionVerifier` abstraction in the resolver chain — each resolver factory can ship a sibling verifier factory, exactly the way each resolver ships a `resolve` function. Today there's one verifier (npm); the shape leaves room for future ones (jsr, attestation-based, etc.) without changing the install-side interface. - **`@pnpm/resolving.resolver-base`** exports the `ResolutionVerifier` / `ResolutionVerification` types — the shared contract. - **`@pnpm/resolving.npm-resolver`** exports `createNpmResolutionVerifier`. Returns `undefined` when no policy is active, so callers can cheaply decide whether to iterate at all. When active, it inspects each lockfile entry, handles `minimumReleaseAgeExclude`, routes through named-registry prefixes (built-ins like `gh:` merged in), and uses `fetchFullMetadataCached` to fetch full registry metadata — decoupled from the resolver pipeline so neither `peekManifestFromStore` nor abbreviated metadata can hide the publish timestamp. - **`@pnpm/resolving.default-resolver`** exports `createResolutionVerifier`, a combinator that asks each underlying verifier (today: npm) if it has work and returns `undefined` when none does. Designed so that adding more verifiers later doesn't change the install side. - **`@pnpm/installing.client`** exposes `verifyResolution` on `Client`, built from the same `fetchFromRegistry` / `getAuthHeader` the resolver chain already uses — **no second fetcher is constructed**. - **`@pnpm/store.connection-manager`** and **`@pnpm/testing.temp-store`** surface `verifyResolution` alongside the store controller they hand back, so it reaches `mutateModules` through the existing plumbing. - **`@pnpm/installing.deps-installer`** gains one option on `StrictInstallOptions`: `verifyResolution?: ResolutionVerifier`. `mutateModules` invokes `verifyLockfileResolutions(ctx.wantedLockfile, opts.verifyResolution)` **once**, right after `getContext` returns the on-disk lockfile and before any path branches. When the verifier is `undefined`, the call is a no-op. The iteration is policy-neutral: dedupes by `(name, version)`, applies `pLimit(16)`, sorts violations stably, caps the printed list at 20 with an `…and N more` summary, throws a `PnpmError` carrying the verifier-supplied error code. The error includes a recovery hint that points at `pnpm clean --lockfile` followed by `pnpm install` — the safe way to throw away a poisoned lockfile and rebuild from fresh resolution. ## Tests - **9 unit tests** for `verifyLockfileResolutions` against a mock `ResolutionVerifier` — dedup, aggregation, stable ordering, the 20-entry cap, no-op behavior, the verifier-supplied error code surfacing in `PnpmError`. - **13 integration tests** in `installing/deps-installer/test/install/minimumReleaseAge.ts` via the real `install()` entry — `testDefaults()` wires `verifyResolution` from `createTempStore` → `createClient`, so the npm verifier runs end-to-end at the install boundary. Covers the rejection scenario, `minimumReleaseAgeExclude`, the strict-mode toggle, the existing `minimumReleaseAge` resolver-side suite, and a `pnpm add` scenario where a pre-existing entry would otherwise survive resolution. - **3 e2e tests** in `pnpm/test/install/minimumReleaseAge.ts` against the bundled CLI: rejection path with the right `ERR_PNPM_*` code and `pnpm clean --lockfile` hint in output, `minimumReleaseAgeExclude` honored, and the strict-off path (which now requires an explicit `minimumReleaseAgeStrict: false` since the config reader auto-enables strict mode when `minimumReleaseAge` is set). - Existing `frozenLockfile` suite (12 tests) and npm-resolver suite (179 tests) still pass. --------- Co-authored-by: Zoltan Kochan <z@kochan.io> |
||
|
|
c178d1396f |
fix(fs,package-manager): match upstream symlink-dir semantics in pacquet (#11684)
* fix(fs,package-manager): match upstream symlink-dir semantics `pacquet_fs::symlink_dir` was a thin wrapper over the platform syscall and diverged from pnpm's [`symlink-dir`](https://github.com/pnpm/symlink-dir) npm package (v10.0.1) in three user-visible ways: 1. Unix symlinks stored the absolute target. Upstream writes the target as a path relative to the link's parent dir (`path.relative(dirname(link), src)`), so installs survive a project move and on-disk symlink contents match pnpm's byte-for-byte. 2. Windows always created junctions. Upstream tries a true directory symlink first and only falls back to a junction on `ERROR_PRIVILEGE_NOT_HELD`. Dev-Mode / Administrator users get a true symlink with pacquet now too. The choice is cached process-wide via `AtomicU8` so subsequent calls skip the EPERM probe, mirroring upstream's binding-rebind. 3. The library default `{ overwrite: true }` retargets stale links and moves non-symlink occupants to `.ignored_<basename>`. Pacquet only exposed the bare primitive, so `symlink_package` (the direct-dep linker, equivalent to pnpm's `symlinkDependency`) silently swallowed `AlreadyExists` — re-installs that retargeted a dependency left the stale link in place. Adds `force_symlink_dir` and `ForceSymlinkOutcome` to `pacquet_fs` porting upstream's `forceSymlink` flow: mkdir-on-`NotFound`, reuse on matching target, unlink-and-retry on stale link, rename-to-`.ignored_*` on non-symlink occupant (with the macOS unlink fallback on the second attempt, per pnpm/pnpm#5909). `symlink_package` calls it. Existing call sites that intentionally use `{ overwrite: false }` semantics (`hoist::symlink_hoisted_dependencies`, the `register_project` heal path) stay on the bare `symlink_dir` primitive. Tests: - New tests in `pacquet/crates/fs/src/symlink_dir/tests.rs` cover the relative-target encoding, idempotent re-symlink, stale-link retarget, occupant rename, and missing-parent mkdir. - Updated `create_symlink_layout/tests.rs`, `install_package_from_registry/tests.rs`, and `install/tests.rs` to compare against the relative encoding (via `pathdiff` / `canonicalize`) instead of the prior absolute string. --- Written by an agent (Claude Code, claude-opus-4-7). * test(cli,store-dir): adapt remaining symlink assertions to relative encoding CI surfaced two more test sites that compared `fs::read_link` / `dunce::canonicalize(read_link_output)` against the absolute target path. Those resolved fine when pacquet wrote absolute symlinks but fail now that the symlink contents are stored as paths relative to the link's parent (matching upstream `symlink-dir`). Canonicalize via the symlink path itself so the kernel resolves the relative target, the same fix already applied to the `package-manager` tests in the previous commit. - `pacquet/crates/cli/tests/add.rs::should_symlink_correctly` - `pacquet/crates/store-dir/src/project_registry.rs::register_creates_symlink_to_project_dir` --- Written by an agent (Claude Code, claude-opus-4-7). * fix(fs): satisfy dylint perfectionist lints in symlink_dir CI's `cargo dylint --all -- --all-targets --workspace` flagged two multi-line macro invocations missing a trailing comma: - `format!` in the mkdir-error wrapping branch (line 163). - `matches!` over the rename-overwrite occupied-kind set (line 271). Both surfaced under `-D warnings` via `perfectionist::macro-trailing-comma`. Added the trailing commas; local `cargo check -p pacquet-fs` is clean. --- Written by an agent (Claude Code, claude-opus-4-7). * fix(fs): drop trailing commas on collapsed single-line asserts `cargo fmt` collapsed five multi-line `assert!` calls in `symlink_dir/tests.rs` to single lines but left the trailing comma intact. Dylint's `perfectionist::macro-trailing-comma` rejects the single-line-with-trailing-comma shape under `RUSTFLAGS=-D warnings`. Removed the commas. `cargo test -p pacquet-fs` still passes. --- Written by an agent (Claude Code, claude-opus-4-7). * test(testing-utils): recognize true symlinks in is_symlink_or_junction The helper has always claimed to detect "symlink or junction" but only called `junction::exists` on Windows, which checks the `IO_REPARSE_TAG_MOUNT_POINT` reparse tag. With pacquet's symlink writer now trying a true directory symlink first on Windows (and only falling back to a junction on `PermissionDenied`), Windows runners that have `SeCreateSymbolicLinkPrivilege` enabled — including GitHub Actions `windows-latest`, which runs as Administrator with Developer Mode on — get a true symlink at `IO_REPARSE_TAG_SYMLINK`. The old helper returned `false` for those, which fooled `should_install_dependencies` and `install_resolves_env_var_in_npmrc_registry` into thinking the install hadn't produced any symlink at all. Combine `junction::exists` with `Path::is_symlink` so the helper matches its name. No change on Unix. --- Written by an agent (Claude Code, claude-opus-4-7). * fix(testing-utils): drop needless return in is_symlink_or_junction Clippy's `needless_return` (denied under `-D warnings` on the Windows `Clippy` step) flagged the trailing `return Ok(path.is_symlink())` inside the `#[cfg(windows)]` block I added in the previous commit. Fold the junction-or-symlink check into a single boolean expression so each cfg branch ends with the result directly. --- Written by an agent (Claude Code, claude-opus-4-7). * fix(fs): write relative target for Windows true symlinks too Upstream `symlink-dir`'s `resolveSrcOnTrueSymlink` (which feeds the `fs.symlink(target, path, 'dir')` call) runs on both Unix and Windows and always passes the path-relative-to-dirname target. The Windows junction path takes the absolute target with a trailing backslash instead, but the `junction` crate handles that wrapping internally. Pacquet's Windows branch was calling `std::os::windows::fs::symlink_dir(original, link)` with the absolute target, which diverged from pnpm on hosts that have `SeCreateSymbolicLinkPrivilege` (GitHub Actions Windows runners do — they run as Administrator with Developer Mode on) and tripped the two `create_symlink_layout` parity tests on Windows CI: Diff < left / right > : <"C:\\...\\.tmpdluV0r\\string-width@4.2.3\\node_modules\\string-width" >"..\\..\\string-width@4.2.3\\node_modules\\string-width" Lift the existing Unix helper `relative_target_for` out of its `cfg(any(unix, test))` gate so the Windows symlink branch can call it too, and route both the cached `USE_SYMLINK` path and the probe-and-cache path through a new `create_true_symlink` helper that computes the relative form before the `CreateSymbolicLinkW` syscall. The junction branch keeps the absolute `original`. --- Written by an agent (Claude Code, claude-opus-4-7). |
||
|
|
29a42efc3b |
feat(pacquet): port directory fetcher for injected workspace deps (#11678)
* feat(pacquet/directory-fetcher,pacquet/package-manager): port directory fetcher for injected workspace deps
Port pnpm's `fetching/directory-fetcher` package as a new
`pacquet-directory-fetcher` crate and wire `LockfileResolution::Directory`
into `InstallPackageBySnapshot::run` so injected workspace deps
(`file:./local-pkg` + `dependenciesMeta[*].injected = true`) actually
install instead of returning `UnsupportedResolution`.
Mirrors upstream's
[`fetching/directory-fetcher/src/index.ts`](https://github.com/pnpm/pnpm/blob/85ceff2383/fetching/directory-fetcher/src/index.ts):
- `walk_all_files` ports `fetchAllFilesFromDir` — recursive walk,
`node_modules` exclusion at any depth, broken-symlink ENOENT skip,
`resolve_symlinks` toggle between `fileStat` (`fs::metadata`) and
`realFileStat` (`symlink_metadata` + `canonicalize`).
- `walk_package_files` ports `fetchPackageFilesFromDir` — delegates
to `pacquet_git_fetcher::packlist` for the npm-packlist filter,
tolerates a missing manifest for the Bit-workspace shape upstream
documents at L63-L66.
- `DirectoryFetcher::run` returns `files_map` / `manifest` /
`requires_build`, matching upstream's `FetchResult` minus
`local: true` (implicit at the call site) and `packageImportMethod`
(encoded by which slot the install dispatcher writes to).
Install dispatch:
- `install_package_by_snapshot.rs` resolves `dir_resolution.directory`
against the newly threaded `workspace_root` (upstream's
`lockfileDir`), calls `DirectoryFetcher::run`, and hands the
source-path `files_map` straight through as `cas_paths`. The
existing `import_indexed_dir` / `link_file` path accepts arbitrary
source paths so no CAFS write is needed — directory deps don't go
through the store at all, matching upstream's
`local: true, packageImportMethod: 'hardlink'` semantics.
- `create_virtual_store.rs`'s `snapshot_cache_key` now returns
`Ok(None)` for directory resolutions: there's no warm-cache key
to recover from (the source dir may have changed since last
install), so every install re-walks. Matches upstream.
Deferred follow-ups (out of scope for this PR):
- `resolveSymlinksInInjectedDirs` / `includeOnlyPackageFiles`
config-knob plumbing — the dispatch hard-codes the upstream
defaults (`false` / `false`) from
[`extendInstallOptions.ts:41`](https://github.com/pnpm/pnpm/blob/85ceff2383/installing/deps-installer/src/install/extendInstallOptions.ts#L41)
for now.
- Injected-dep re-mirror pass — `hoisted_dep_graph.rs` already
records `injection_targets_by_dep_path` for every directory-typed
snapshot location, but the post-install mirror that rebuilds those
copies is not implemented yet.
- `package.json5` / `package.yaml` manifest read — pacquet's
`safe_read_package_json_from_dir` only handles `package.json`; the
other two variants upstream's `safeReadProjectManifestOnly`
supports are a parity gap that affects directory deps the same way
it affects every other manifest read in pacquet.
---
Written by an agent (Claude Code, claude-opus-4-7).
* fix(pacquet/directory-fetcher,pacquet/package-manager): address CodeRabbit review on #11678
Three parity / hardening fixes flagged in the CodeRabbit review on PR
#11678:
1. **Carve out directory snapshots from the current-lockfile-skip
gate** (`create_virtual_store.rs`). Directory-typed snapshots have
`integrity() == None`; without the carve-out `integrity_equal`
collapsed `None == None == true` and the skip filter dropped the
snapshot whenever a slot for it existed on disk, so a second
install never re-walked the (mutable) source dir. Mirrors pnpm's
`!isDirectoryDep` clause in `depIsPresent` at
<https://github.com/pnpm/pnpm/blob/94240bc046/deps/graph-builder/src/lockfileToDepGraph.ts#L226-L228>.
2. **Add `DirectoryFetch` to `is_fetch_side_failure`**
(`create_virtual_store.rs`). Upstream's catch at
[`lockfileToDepGraph.ts:286-298`](https://github.com/pnpm/pnpm/blob/94240bc046/deps/graph-builder/src/lockfileToDepGraph.ts#L286-L298)
wraps the whole `fetchPackage` dispatch, so directory-fetcher
errors on optional snapshots are swallowed uniformly with tarball
/ git fetch errors. Without this, an optional injected-directory
dep whose source was missing would hard-fail the install instead
of being dropped.
3. **Symlink-cycle guard in `walk_all_inner`** (`directory-fetcher/
src/walker.rs`). A `loop -> .` (or any ancestor-pointing) symlink
previously sank the walker into infinite recursion until either
ENAMETOOLONG or stack overflow fired. Skip-on-revisit keyed off
`fs::canonicalize`, matching the pattern
`pacquet_git_fetcher::packlist` already uses for
`bundleDependencies` cycles. Pnpm's directory-fetcher has the same
vulnerability; the guard is a defensible divergence because the
positive-case behavior is identical to pnpm and the cycle case
degrades from "crash" to "skip with a `tracing::warn`".
Added a regression test
(`walk_all_files_terminates_on_symlink_cycle`) that points a
`loop -> root` symlink at the walk root and asserts the cycle guard
short-circuits before any `loop/` descendant is recorded.
---
Written by an agent (Claude Code, claude-opus-4-7).
* style(pacquet/package-manager): trailing comma on multi-line matches! macro
Dylint's `perfectionist::macro-trailing-comma` lint flagged the
`matches!` invocation added in
|
||
|
|
496e655092 | refactor: prematurely interrupting the link command without passing parameters (#11424) | ||
|
|
d3f8408def |
fix: global installs respect build policy from global config.yaml when GVS is enabled (#11363)
* fix(config.reader): move GVS allowBuilds default after globalDepsBuildConfig re-application
The GVS default allowBuilds = {} was applied too early — before
workspace manifest settings were read and before .npmrc values
(dangerouslyAllowAllBuilds) were re-applied via globalDepsBuildConfig.
This caused hasDependencyBuildOptions() to return true (because {}} is
not null), blocking restoration of .npmrc values. Global installs
with GVS enabled would silently skip all build scripts even when
the config explicitly allowed them.
This fix moves the GVS default to after both workspace manifest
reading and globalDepsBuildConfig re-application, so that:
1. Workspace manifest allowBuilds takes precedence (if present)
2. .npmrc dangerously-allow-all-builds is properly restored
3. Empty {}} is only applied as a last resort
Closes #9249
* fix(config.reader): apply Copilot suggestion for GVS allowBuilds guard
From PR review discussion_r3141002317:
- Replace hasDependencyBuildOptions() == null with hasDependencyBuildOptions()
so the GVS default only applies when no build policy at all is
configured (not even dangerouslyAllowAllBuilds). This is cleaner because
the condition now matches the re-application guard on the line
immediately before it.
- Add regression test verifying that dangerouslyAllowAllBuilds with GVS
preserves allowBuilds when no global workspace manifest exists.
* docs: update changeset
* fix(config.reader): address PR review feedback
- Fix unreachable GVS allowBuilds default: hasDependencyBuildOptions()
always returns true after globalDepsBuildConfig re-applies defaults
(dangerouslyAllowAllBuilds: false is != null). Replace with explicit
allowBuilds == null && dangerouslyAllowAllBuilds !== true check.
- Rename .npmrc references to global config.yaml in changeset, comments,
and test names (zkochan: v11 reads from global rc file, not .npmrc).
- Add try/finally env cleanup for XDG_CONFIG_HOME and PNPM_HOME in tests.
- Add test for workspace manifest allowBuilds precedence over config.yaml.
* fix(config.reader): fix GVS workspace manifest test
- Use import.meta.dirname/global/v11 for globalPkgDir (matches env.PNPM_HOME)
- Fix assertion: dangerouslyAllowAllBuilds coexists with allowBuilds
- Clean up global/v11 directory in finally block to prevent test leakage
* fix(config.reader): use object form for workspace manifest allowBuilds; clean up parent global/ dir
Fixes two PR #11363 review threads:
1. allowBuilds in workspace manifest must be Record<string, boolean>,
not array — createAllowBuildFunction uses Object.entries()
2. Remove empty config/reader/test/global/ directory after test
* fix(config.reader): address production review nits
- Update changeset: use camelCase dangerouslyAllowAllBuilds (YAML key, not .npmrc)
- Add enableGlobalVirtualStore assertion to first GVS test
- Add comment explaining dangerouslyAllowAllBuilds coexistence on config object
* fix(config.reader): address Copilot review — env safety, GLOBAL_LAYOUT_VERSION, try/finally
- Move XDG_CONFIG_HOME mutation and file setup inside try blocks
so env is always restored even if setup throws
- Replace hard-coded v11 with GLOBAL_LAYOUT_VERSION import
- Fix corrupted try/finally in workspace manifest precedence test
(missing finally block and mangled expect line from prior bad edit)
- Reword comment: enableGlobalVirtualStore defaults to true for
global installs, not \"when not in CI\"
* fix(config.reader): address last 3 Copilot review threads — comment wording, cleanup placement, test rename
* fix(config.reader): fix block-scoped globalDir leak in GVS test
* fix: address Copilot review #4194783789 — restore auth test, fix naming, remove artifacts
* Remove local dev tooling — not part of this PR
* Remove PR.md — issue context is in the PR description
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Tom Hale <tom@hale.net>
|
||
|
|
85ceff2383 |
chore(deps): bump the github-actions group across 1 directory with 7 updates (#11642)
Bumps the github-actions group with 7 updates in the / directory: | Package | From | To | | --- | --- | --- | | [actions/upload-artifact](https://github.com/actions/upload-artifact) | `7.0.0` | `7.0.1` | | [github/codeql-action](https://github.com/github/codeql-action) | `4.32.5` | `4.35.4` | | [actions/download-artifact](https://github.com/actions/download-artifact) | `8.0.0` | `8.0.1` | | [softprops/action-gh-release](https://github.com/softprops/action-gh-release) | `2.5.0` | `3.0.0` | | [actions/setup-node](https://github.com/actions/setup-node) | `6.2.0` | `6.4.0` | | [vedantmgoyal9/winget-releaser](https://github.com/vedantmgoyal9/winget-releaser) | `19e706d4c9121098010096f9c495a70a7518b30f` | `7bd472be23763def6e16bd06cc8b1cdfab0e2fd5` | | [cbrgm/mastodon-github-action](https://github.com/cbrgm/mastodon-github-action) | `2.1.26` | `2.2.0` | Updates `actions/upload-artifact` from 7.0.0 to 7.0.1 - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits]( |
||
|
|
b1e8d7aa03 |
chore(cargo): bump libc from 0.2.185 to 0.2.186 (#11638)
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.185 to 0.2.186. - [Release notes](https://github.com/rust-lang/libc/releases) - [Changelog](https://github.com/rust-lang/libc/blob/0.2.186/CHANGELOG.md) - [Commits](https://github.com/rust-lang/libc/compare/0.2.185...0.2.186) --- updated-dependencies: - dependency-name: libc dependency-version: 0.2.186 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
7d4422f67a |
chore(deps): bump postcss (#11646)
Bumps [postcss](https://github.com/postcss/postcss) from 5.2.18 to 8.5.10. - [Release notes](https://github.com/postcss/postcss/releases) - [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/postcss/postcss/commits/8.5.10) --- updated-dependencies: - dependency-name: postcss dependency-version: 8.5.10 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
2dee606f43 |
chore(cargo): bump zip from 5.1.1 to 8.6.0 (#11637)
Bumps [zip](https://github.com/zip-rs/zip2) from 5.1.1 to 8.6.0. - [Release notes](https://github.com/zip-rs/zip2/releases) - [Changelog](https://github.com/zip-rs/zip2/blob/master/CHANGELOG.md) - [Commits](https://github.com/zip-rs/zip2/compare/v5.1.1...v8.6.0) --- updated-dependencies: - dependency-name: zip dependency-version: 8.6.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
a551f9dc01 |
chore(deps): bump immutable (#11647)
Bumps [immutable](https://github.com/immutable-js/immutable-js) from 3.7.6 to 3.8.3. - [Release notes](https://github.com/immutable-js/immutable-js/releases) - [Changelog](https://github.com/immutable-js/immutable-js/blob/main/CHANGELOG.md) - [Commits](https://github.com/immutable-js/immutable-js/compare/3.7.6...v3.8.3) --- updated-dependencies: - dependency-name: immutable dependency-version: 3.8.3 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
0c48613189 |
chore(deps): bump micromatch (#11648)
Bumps [micromatch](https://github.com/micromatch/micromatch) from 3.1.10 to 4.0.8. - [Release notes](https://github.com/micromatch/micromatch/releases) - [Changelog](https://github.com/micromatch/micromatch/blob/master/CHANGELOG.md) - [Commits](https://github.com/micromatch/micromatch/compare/3.1.10...4.0.8) --- updated-dependencies: - dependency-name: micromatch dependency-version: 4.0.8 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
f98a925a9b |
chore(deps): bump json5 (#11649)
Bumps [json5](https://github.com/json5/json5) from 0.5.1 to 2.2.3. - [Release notes](https://github.com/json5/json5/releases) - [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md) - [Commits](https://github.com/json5/json5/compare/v0.5.1...v2.2.3) --- updated-dependencies: - dependency-name: json5 dependency-version: 2.2.3 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
397e94301d |
chore(deps): bump js-yaml (#11650)
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 3.7.0 to 4.1.1. - [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodeca/js-yaml/compare/3.7.0...4.1.1) --- updated-dependencies: - dependency-name: js-yaml dependency-version: 4.1.1 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
dda8153e6e |
chore(deps): bump braces (#11651)
Bumps [braces](https://github.com/micromatch/braces) from 2.3.2 to 3.0.3. - [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md) - [Commits](https://github.com/micromatch/braces/commits/3.0.3) --- updated-dependencies: - dependency-name: braces dependency-version: 3.0.3 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
f05845a02f |
fix(pacquet/config): satisfy dylint perfectionist lints (#11672)
Two pre-existing perfectionist findings landed via #11526 and broke the Dylint CI job on main: - `perfectionist::macro-trailing-comma` flagged a trailing comma in a single-line `assert_eq!(...)` that `cargo fmt` had collapsed from a multi-line form without dropping the comma. Removed the comma. - `perfectionist::unicode_ellipsis_in_comments` warned about a U+2026 `…` character in a test comment. CI runs with `RUSTFLAGS=-D warnings`, so the warning fails the build. Replaced with ASCII `...`. Verified locally with `cargo dylint --all -- --all-targets --workspace` under `RUSTFLAGS=-D warnings`; passes clean. --- Written by an agent (Claude Code, claude-opus-4-7). |
||
|
|
a62f959242 |
fix(config): drop unresolved ${VAR} placeholders from .npmrc auth values (#11526)
Closes #11513. `actions/setup-node` writes `_authToken=${NODE_AUTH_TOKEN}` to `.npmrc`. When the user relies on OIDC trusted publishing without setting `NODE_AUTH_TOKEN`, pnpm previously passed the literal placeholder through verbatim — so any time OIDC fallback failed, pnpm sent `Authorization: Bearer ${NODE_AUTH_TOKEN}` to the registry and the publish came back as a 404. This worked in v10 because `pnpm publish` shelled out to `npm publish`, whose own OIDC flow handled the case. The fix lives in `@pnpm/config.env-replace@4.1.0`, which adds an `envReplaceLossy` variant that returns `{ value, unresolved }` instead of throwing. Unresolved `${VAR}` placeholders become `''` and are reported back as a list — leaving OIDC trusted publishing as the sole auth source. Resolvable placeholders and `${VAR-default}` / `${VAR:-default}` fallbacks elsewhere in the same string still expand normally, so a value like `pre-${SET}-mid-${UNSET}-${OTHER-default}-post` now produces `pre-AAA-mid--default-post` rather than dropping every placeholder. Also treats `{ KEY: undefined }` in the env object the same as a missing key (the `Record<string, string | undefined>` contract), so a `${KEY-default}` reaches the fallback in that case. ### Changes - `@pnpm/config.env-replace` catalog bumped from `^3.0.2` → `^4.1.0` (`pnpm-workspace.yaml`, `pnpm-lock.yaml`) - `config/reader/src/loadNpmrcFiles.ts` — `substituteEnv` now calls `envReplaceLossy` and pushes one warning per unresolved placeholder - `config/reader/test/index.ts` + `parseCreds.test.ts` — regression tests covering the OIDC case, mixed resolvable/unresolved placeholders, explicit-undefined env values, and `parseCreds({ authToken: '' })` - `.changeset/oidc-unresolved-env-placeholder.md` — patch bump for `@pnpm/config.reader` and `pnpm` - `pacquet/crates/config/{env_replace.rs, npmrc_auth.rs, npmrc_auth/tests.rs}` — mirrors the lossy semantics in pacquet's local `env_replace_lossy`, with matching test coverage |
||
|
|
4ababc0fc6 |
feat(package-manager): write .pnpm-workspace-state-v1.json after install (#11665)
* feat(package-manager): write .pnpm-workspace-state-v1.json after install
pnpm's verifyDepsBeforeRun gate bails out with "Cannot check whether
dependencies are outdated" as soon as the workspace state file is
missing, so a node_modules tree materialized by pacquet always tripped
the check and forced a reinstall. Port @pnpm/workspace.state to a new
pacquet-workspace-state crate and write the file at the end of
Install::run so pnpm can fast-path the freshness check after pacquet
has done the install.
Closes the gap behind the pnpm_config_verify_deps_before_run: false
workaround in
|
||
|
|
6e93f350a9 |
fix(lockfile): support CRLF line endings in env lockfiles (#11654)
* fix(lockfile): support CRLF line endings in env lockfiles Normalize CRLF line endings before parsing YAML document separators in streamed env lockfile reads. Previously the parser assumed LF-only separators (`\n---\n`), which caused pnpm to report ERR_PNPM_BROKEN_LOCKFILE or outdated lockfile errors when configDependencies lockfiles were checked out with CRLF line endings on Windows. Fixes #11612 * test(lockfile): cover CRLF normalization and clean up yamlDocuments Add CRLF-handling tests for streamReadFirstYamlDocument (CRLF and BOM+CRLF) and extractMainDocument (CRLF in combined file and CRLF in content without separator). Hoist the duplicated CRLF replace in Phase 1 out of the if/else, drop two stray semicolons and a couple of blank lines. * chore: include pnpm in changeset --------- Co-authored-by: Zoltan Kochan <z@kochan.io> |
||
|
|
b6e2c8c5ac |
fix(engine.pm.commands): honor minimumReleaseAgeExclude in self-update (#11664)
* fix(engine.pm.commands): honor minimumReleaseAgeExclude in self-update * refactor(config.version-policy): centralize publishedBy policy derivation Extract the publishedBy / publishedByExclude derivation duplicated across selfUpdate, dlx, outdated, and deps-resolver into a new `getPublishedByPolicy()` helper, and the version-policy error rewrap into `createPackageVersionPolicyOrThrow()`. Also adds the global self-update test branch (no wantedPackageManager) requested in PR review, and harmonizes the dlx/outdated error code for invalid minimumReleaseAgeExclude patterns with install/self-update. * style(config.version-policy): rename 'callsite' to 'call site' to satisfy cspell --------- Co-authored-by: Zoltan Kochan <z@kochan.io> |
||
|
|
7ff112bac6 | ci: run install with pacquet (#11657) | ||
|
|
ae703b1bcd |
fix(test-ipc-server): allow tilde in shell-arg paths for Windows 8.3 short names (#11661)
`os.tmpdir()` on GitHub's Windows runners returns the 8.3 short-name form
of the user-profile directory (e.g. `C:\Users\RUNNER~1\AppData\Local\Temp`)
because `runneradmin` is longer than 8 characters. The `~` then trips the
`quoteShellArg` allowlist regex and every test that calls `sendLineScript`
or `generateSendStdinScript` throws "Unsupported character in shell argument".
The tilde is safe to allow:
- cmd.exe performs no tilde expansion at all.
- POSIX shells only expand `~` when it is unquoted at the start of a word;
inside the double-quoted `"${arg}"` wrapper produced here it is literal.
The matching CodeQL shell-injection sanitization argument is unchanged —
the allowlist is still anchored and still rejects every metacharacter.
The bug was masked until #11659 because the Windows test legs had been
silently no-op'ing since #11608.
---
Written by an agent (Claude Code, claude-opus-4-7).
v11.1.2-canora.10
v11.1.2-canora.8
v11.1.2-canora.7
|
||
|
|
e8fc34389a |
ci: pin Run tests step to bash so $TEST_SCRIPT expands on Windows (#11659)
Without an explicit shell, the step ran under PowerShell on windows-latest, where `$TEST_SCRIPT` is not a variable (PowerShell exposes env vars as `$env:TEST_SCRIPT`). `pn run ""` then exited 0 and just listed available scripts — the Windows test legs have been silently no-op'ing since the env-var move in #11608. The sibling `Verify Node version` and `Determine test scope` steps already pin `shell: bash`; this brings `Run tests` in line. --- Written by an agent (Claude Code, claude-opus-4-7). |
||
|
|
a4f3c6d3b7 |
ci: enable manual releases for pacquet and pnpm (#11652)
Two related workflow changes: ### `pacquet-release-to-npm.yml`: switch to `workflow_dispatch` The trigger was "push to main touching `pacquet/npm/pacquet/package.json`" — the version came from a committed bump and the workflow auto-fired on every such commit. Switch to `workflow_dispatch` only, with a `version` input (validated as semver). The workflow patches `pacquet/npm/pacquet/package.json` before `generate-packages.mjs` runs, so the version is single-sourced from the manual trigger rather than needing a separate commit to bump the manifest first. The committed manifest now omits the `version` field entirely — it only exists at release time inside the runner. Dropped along the way: - The `check` job (EndBug/version-check against unpkg) — no longer needed when the operator types the version. - The `Create GitHub Release` step — no draft release, no `v*.*.*` git tag. The pacquet `v0.x.x` tag scheme collided with pnpm's `v11.x.x`; npm is the authoritative artifact store and provenance attestations stay attached via `--provenance` on `pnpm publish`. - `contents: write` on the publish job (no longer needs to create a tag). ### `release.yml`: add `workflow_dispatch` as a lib-only republish path Add a `workflow_dispatch:` trigger alongside the existing tag-push trigger. Tag-push behaves exactly as before. Manual dispatch becomes a fast **lib-only republish** path — useful after a version bump to one or more lib packages that doesn't warrant a full CLI release. On `workflow_dispatch` from any ref, the following are skipped (guarded with `if: startsWith(github.ref, 'refs/tags/')`): - `Publish @pnpm/exe` step — also contains the multi-minute `build-artifacts` call. - `Publish pnpm CLI` step. - `Copy Artifacts`, `Attest build provenance` (the `dist/*` attestation), `Generate release description`, `Release` (`softprops/action-gh-release`) — these are the GitHub-Release-side ceremony. Without an explicit `tag_name`, `softprops/action-gh-release@v2.5.0` defaults to `github.ref_name`, which on a manual dispatch from main would create a junk release tagged literally `main`. What still runs on `workflow_dispatch`: - `actions/checkout`, garnet scan, `pnpm/setup` - `Publish internal workspace packages (static token)` — i.e. `pn publish --filter=!pnpm --filter=!@pnpm/exe --access=public --provenance` Compilation is handled by each lib package's own `prepublishOnly: tsgo --build` hook (which `pnpm publish` runs automatically), same as the existing tag-push flow. The npm registry rejects any version already on it, so re-running on an already-released tree is a no-op — that's the safety net for accidental clicks. ## How to use **pacquet release**: Actions → Release Pacquet → Run workflow → fill in `version` (e.g. `0.2.3` or `0.2.3-rc.1`) → Run. No tag, no GitHub release. **pnpm full release**: still triggered by a `v*.*.*` tag push. Publishes @pnpm/exe + libs + CLI, attests, copies artifacts, creates a draft GitHub release. **pnpm lib-only republish**: Actions → Release → Run workflow → choose `main` → Run. Publishes just the internal workspace packages from whatever versions are currently in each `package.json`. Skips CLI, @pnpm/exe, build-artifacts, GitHub release. |
||
|
|
1575076d08 |
chore(pacquet): fold registry-mock into root workspace, fix npm metadata (#11643)
* chore(pacquet): fold registry-mock into root workspace, fix npm metadata
Two unrelated cleanups bundled because they touch the same publishing/
workspace plumbing:
1. **`pacquet/npm/pacquet/package.json` metadata** — the imported file
still pointed at the standalone repo: `repository.url` was
`pnpm/pacquet`, `repository.directory` was `npm/pacquet`, `homepage`
and `bugs` likewise. Repoint at `pnpm/pnpm`. Update
`generate-packages.mjs` so the per-platform packages it emits at
release time also point at `pnpm/pnpm`.
2. **Fold `pacquet/tasks/registry-mock` into the root pnpm workspace**.
Pacquet's standalone-repo nested-workspace setup pinned
`nodeLinker: hoisted` "for verdaccio CJS resolution," but pnpm's
own jest globalSetup (`__utils__/jest-config/with-registry/`) calls
the same `@pnpm/registry-mock.start()` API under the default
isolated linker without issue, and verified locally that
`node launch.mjs prepare` works after consolidation. The hoisted
constraint was scoped to standalone-pacquet's install pattern; in
the monorepo it's unnecessary.
Changes for (2):
- Add `pacquet/tasks/registry-mock` to `pnpm-workspace.yaml`.
- Rename the package `@pnpm-private/pacquet-registry-mock-launcher`
(private, matches the `@pnpm-private/*` convention used by other
internal workspace members) and switch `@pnpm/registry-mock` to
`catalog:` (the root catalog already pins it at 6.0.0).
- Delete `pacquet/tasks/registry-mock/pnpm-lock.yaml` and
`pnpm-workspace.yaml` — root install handles both now.
- Delete `pacquet/package.json` and `pacquet/pnpm-lock.yaml` — the
file only had a `cargo build` script + `devEngines: pnpm`, both
already covered by root, and nothing referenced it.
- `justfile install` is now just `pnpm install` (was
`cd pacquet/tasks/registry-mock && pnpm install --frozen-lockfile`).
- `pacquet-integrated-benchmark.yml` path filter and cache key
swap the deleted nested lockfile for the root `pnpm-lock.yaml` /
`pnpm-workspace.yaml`.
Verified: `pnpm install` resolves the workspace member, the lockfile
gains a `pacquet/tasks/registry-mock` importer entry, and
`pacquet/tasks/registry-mock/node_modules/@pnpm/registry-mock` is
linked correctly under the isolated layout.
* fix(pacquet): match meta-updater conventions in registry-mock launcher
The previous version of `pacquet/tasks/registry-mock/package.json`
omitted `version`, which crashed `pn lint:meta` (meta-updater hits
`manifest.version!.split('.')[0]` for every workspace package).
Backfill all the fields meta-updater would emit for an internal
`@pnpm-private/*` private package, matching `__typecheck__/package.json`
and friends:
- `version: 1100.0.0` (the pnpm 11.x convention for non-experimental
internal packages)
- self-devDep entry (`workspace:*`) that meta-updater would otherwise
inject
- `keywords: [pnpm, pnpm11]`
- `repository` pointing at this directory inside pnpm/pnpm
This is the same shape every other `@pnpm-private/*` private workspace
member uses; it lets `pn lint:meta --test` pass without modifying the
file.
* fix: update lockfile
* chore(pacquet): add build:pacquet script at root
Restore the `cargo build --release --bin pacquet` shortcut that lived in
the deleted `pacquet/package.json`. Naming it `build:pacquet` (rather
than `build`) matches the existing namespacing convention in this
file (`lint:ts`, `lint:meta`) and leaves room for a general `build`
script later. Invoke with `pnpm build:pacquet` or `pn build:pacquet`
from the repo root.
|
||
|
|
2e33bb1955 |
docs: split AGENTS.md into shared + pacquet-specific (#11640)
* docs: split AGENTS.md into shared + pacquet-specific
Before: root AGENTS.md and pacquet/AGENTS.md each maintained their own
copy of the GitHub PR workflow, agent-footer rule, "never ignore test
failures," Conventional Commits list, and code-reuse philosophy.
Drift waiting to happen.
After:
- Root AGENTS.md owns the shared conventions (PR workflow, agent
footer, conventional commits, code reuse, never-ignore-tests,
PR-conflict script) and marks TS-only sections explicitly
(setup/build, testing, linting, changesets, Standard Style, Jest
gotchas).
- pacquet/AGENTS.md opens with "Read ../AGENTS.md first" and keeps
only pacquet-specific rules (cardinal rule, branded types, just
recipes, insta snapshots, miette diagnostics, Rust style notes,
the `bench:` commit type, things-not-to-do that are Rust-flavored).
- Root adds a one-line entry for `pacquet/` in the repo structure
list so first-time readers find the cross-link.
CLAUDE.md and pacquet/{CLAUDE,GEMINI}.md are unchanged — they're
symlinks to AGENTS.md and follow automatically.
* docs(agents): require parity between pnpm and pacquet
Add a "Keep pnpm and pacquet in sync" section to root AGENTS.md spelling
out the bidirectional obligation: any user-visible change (CLI surface,
lockfile/manifest format, error codes, defaults, env-var handling, log
emissions, store layout) must land in both stacks in the same PR, or
the originating PR must spawn a tracking issue. Pure refactors / perf
wins / TS-only test cleanups don't need mirroring.
Cross-link from pacquet/AGENTS.md's "cardinal rule" so a pacquet-side
reader knows the obligation goes both ways and where the pnpm-side
version lives.
* docs(agents): restore Rust-specific dependency-level guidance
The root "Keep the dependency on the right level" bullet uses npm
vocabulary ("package," "shared package"). For a Rust reader that
required mentally translating "package" → "crate" and made the
workspace-vs-crate distinction less obvious. Restore the pacquet
phrasing alongside the existing pacquet-specific notes.
* docs(agents): hand off cross-stack porting via the same PR
Drop the "open a tracking issue" fallback — it lets one side drift
behind while the issue sits in the backlog. Instead, the PR author
opens the PR with their side and flags in the description what still
needs porting; someone else pushes the matching commits to the same
PR before it lands. Both sides land together or not at all.
* docs(agents): drop external-repo framing from the cardinal rule
pacquet now lives in the same repo as pnpm, so the cardinal rule no
longer needs the "fetch pnpm/pnpm main, compare ls-remote SHAs, watch
your local clone for drift" mechanics. The reference TypeScript code
is just a few directories over (`pnpm/`, `pkg-manager/`, `resolving/`,
`lockfile/`, `store/`, etc.), and pnpm is the source of truth by
position in the repo, not by branch tracking.
Updates:
- Root `AGENTS.md`: rephrase the cross-link line to drop the "follow
pnpm's main" framing.
- `pacquet/AGENTS.md` cardinal rule: redirect "find the equivalent
code" from `https://github.com/pnpm/pnpm` to the in-repo
TypeScript workspaces, drop the "confirm you're on the freshest
main" paragraph, and reword the source-of-truth wording.
- Permalink citation rule: generalize from "upstream pnpm" to "any
GitHub repository, including this one" — citation SHAs now usually
point at this repo's history.
* docs(agents): note pacquet's current scope is install-only
Without this caveat the parity rule reads as if every command needs
porting today. pacquet only implements `install` right now; resolution
and other commands (`update`, `add`, `remove`, `publish`, `exec`,
`run`, `dlx`, `audit`, etc.) live only in TypeScript, so changes there
don't need a pacquet-side port. The caveat also flags that the parity
rule's scope will widen as pacquet ports more commands.
|
||
|
|
d2b64b6689 |
ci(pacquet): fix all zizmor code-scanning findings (#11641)
* ci(pacquet): fix all zizmor code-scanning findings Resolves the 90 alerts opened by zizmor against the imported pacquet-* workflows and shared composite actions: - unpinned-uses: pin every third-party action to a SHA + version comment (matching SHAs already used elsewhere in the repo where applicable; taiki-e/install-action collapsed onto v2.78.0 with explicit `tool:` input). - artipacked: add `persist-credentials: false` to every actions/checkout. - template-injection: pass `inputs.*` and `steps.*.outputs.*` through `env:` in binstall/rustup composite actions and pacquet-release-to-npm.yml. - excessive-permissions: add top-level `permissions: contents: read` to pacquet-release-to-npm.yml; move issues/pull-requests writes from the workflow level to the benchmark-compare job in pacquet-micro-benchmark.yml. - dangerous-triggers: keep workflow_run in pacquet-integrated-benchmark- comment.yml but suppress with a documented zizmor: ignore — the trigger is the recommended pattern for posting comments back to fork PRs. - superfluous-actions: keep softprops/action-gh-release with a zizmor: ignore (matches release.yml). Verified by running `zizmor .github` locally with no remaining findings. * ci(pacquet): point SHA pins at the patch-version tag Swatinem/rust-cache and montudor/action-zip were pinned to the SHA the major-version alias (`v2`, `v1`) resolves to, but the version comments claimed `v2.9.1` / `v1.0.0`. zizmor's online `ref-version-mismatch` audit flagged the inconsistency. Repoint at the SHAs the patch-version tags actually annotate so the pin and the comment agree. |
||
|
|
763ddf1c99 |
chore(pacquet): wire pacquet workflows into monorepo (#11635)
* chore(pacquet): wire pacquet workflows into monorepo
Move Cargo workspace, Rust toolchain configs, justfile, composite actions,
and 7 workflow files out of `pacquet/` and up to the repo root so:
- cargo / just / taplo run from repo root, the way the rest of the
monorepo's tooling does
- GitHub Actions actually discovers the workflows (it only reads
`.github/workflows/` at the repo root)
Workflows are prefixed with `pacquet-` and renamed to "Pacquet ..." so
they don't collide with the existing pnpm CI. Path filters are scoped
to `pacquet/**` so they don't trigger on every commit. The cargo entry
from pacquet's standalone `dependabot.yml` is folded into the root one;
pacquet's `CODEOWNERS` and `pull_request_template.md` are dropped because
the root copies supersede them.
Path rewrites:
- `Cargo.toml` workspace members → `pacquet/crates/*`, `pacquet/tasks/*`
- all path-deps in `[workspace.dependencies]` → `pacquet/...`
- `justfile` recipes (`install`, `install-hooks`) point at `pacquet/...`
- `.taplo.toml` include globs → `pacquet/crates/*/*.toml`, `pacquet/tasks/*/*.toml`
- `pacquet/npm/pacquet/scripts/generate-packages.mjs` REPO_ROOT walks one
more level up
- workflow `paths:` filters, `hashFiles(...)`, and shell paths all updated
Verified: `cargo metadata` resolves the workspace, `cargo fmt --check`
clean, `taplo format --check` picks up all 26 Cargo.tomls, `actionlint`
reports no new issues (the `type:`-on-input warnings on the rustup action
predate this move).
* chore(pacquet): drop pnpm version pin from pacquet CI workflows
The monorepo's root `package.json` declares `pnpm@11.1.1` under
`packageManager`, which conflicts with the workflows' explicit
`version: 11.0.0-rc.5` and trips `pnpm/action-setup` ERR_PNPM_BAD_PM_VERSION.
The pin was a pacquet-era workaround for the v9 lockfile while pnpm 11
was still pre-release. Stable 11.x writes v9 too, so let action-setup
read the version from `packageManager` like every other workflow in
this repo does.
* chore(pacquet): use pnpm/setup matching the rest of the monorepo
Replaces `pnpm/action-setup@v6` with the same `pnpm/setup@b1cac3...`
SHA the rest of pnpm/pnpm uses (release.yml, test.yml, ci.yml,
benchmark.yml, audit.yml). Reads pnpm version from `packageManager`
in root package.json, and skips the implicit `pnpm install` since
pacquet does its own scoped install via `just install` (which only
touches `pacquet/tasks/registry-mock/`).
The release workflow now also installs Node via the same action
(`runtime: node@22`) instead of via `pnpm runtime -g set node 22`,
since pnpm/setup handles runtimes in one step.
* chore(pacquet): tighten permissions and Dependabot cooldown
Address zizmor warnings on the pacquet CI changes:
- `dependabot.yml`: the cargo entry I added in the previous commit
inherited from pacquet's standalone repo and is missing the
`cooldown: default-days: 7` the github-actions entry uses. Add it
so cargo and github-actions debounce updates consistently.
- `pacquet-ci.yml`, `pacquet-codecov.yml`, `pacquet-cargo-unused.yml`
lacked a top-level `permissions:` block, so GITHUB_TOKEN inherited
the repo default. Declare `contents: read` — every job in these
workflows only reads the repo and runs local checks.
The other four pacquet workflows already declare permissions
explicitly (integrated-benchmark/comment, micro-benchmark, release).
* chore(pacquet): add "reimagining" to cspell dictionary
cspell at the repo root scans all `**/README.md` and was rejecting
`pacquet/README.md` and `pacquet/npm/pacquet/README.md`, which describe
pacquet as "not a reimagining of pnpm." Add the word to the existing
allow-list rather than rewording two READMEs imported from a separate
repo.
* fix(pacquet): prefix workspace-relative paths with pacquet/
Two Rust source files looked up paths off the cargo workspace root
(\`cargo locate-project --workspace\`), which now resolves to the
monorepo root rather than the pacquet directory. Add the \`pacquet/\`
prefix:
- \`tasks/registry-mock/src/dirs.rs\` — \`registry_mock()\` was
pointing the node launcher at \`<repo>/tasks/registry-mock/launch.mjs\`
instead of \`<repo>/pacquet/tasks/registry-mock/launch.mjs\`, which
failed every Pacquet CI test job ("Cannot find module ...launch.mjs").
- \`tasks/micro-benchmark/src/main.rs\` — same idea for the
fixtures folder.
|
||
|
|
4a89b06b44 |
chore: import pacquet repository into monorepo
Imports https://github.com/pnpm/pacquet at the pacquet/ subdirectory, preserving full commit history (rewritten with git filter-repo so blame and log work for files under pacquet/). |
||
|
|
ceec335e85 |
fix(package-manager): link optionalDependencies siblings into slot (#526)
`create_symlink_layout` only iterated `snapshot.dependencies`, so a
package whose CPU/OS-specific siblings live entirely under
`optionalDependencies` (e.g. `@typescript/native-preview`,
`@reflink/reflink`, every `*-darwin-arm64` / `*-linux-x64` family)
ended up with a slot `node_modules/<scope>/` containing only the
parent package — no platform binary sibling. Consumers that do
`require.resolve('@typescript/native-preview-darwin-arm64')` from
inside `getExePath.js` walked parent directories and found nothing,
so `tsgo --version` (and every other tool that delegates to a
platform variant) crashed with `Unable to resolve … missing the
package on disk`.
Port upstream's `dependencies ∪ optionalDependencies` merge — the
graph builder at
https://github.com/pnpm/pnpm/blob/da65e6262/deps/graph-builder/src/lockfileToDepGraph.ts#L150-L156
unifies both maps into one `allDeps` for each node's children, and
`linkAllModules` then symlinks every child with two short-circuits:
`alias === depNode.name` (a snapshot referencing itself) and
`!pkg.installable && pkg.optional` (a non-materialized optional).
See https://github.com/pnpm/pnpm/blob/da65e6262/installing/deps-installer/src/install/link.ts#L521-L549.
`create_symlink_layout` now takes both maps and a `SkippedSnapshots`
reference, merges them, and applies both short-circuits. The skip
set is threaded through `InstallPackageBySnapshot` (cold batch) and
the warm-batch `CreateVirtualDirBySnapshot` call site in
`CreateVirtualStore::run`, so a target dropped by the installability
pass, by `--no-optional`, or by a swallowed optional fetch failure
gets no dangling symlink.
Five new unit tests in `create_symlink_layout/tests.rs` cover the
matching-optional happy path, the skipped-optional dangling-link
guard, the self-name guard for entries listed in either bucket, the
both-buckets-absent no-op, and the alias-resolve path (aliased deps
still link the alias filename while resolving the slot via the
target's name). End-to-end verification: `pacquet install
--frozen-lockfile` followed by `tsgo --version` in the pnpm v11
repo now succeeds; the matching `native-preview-darwin-arm64`
sibling shows up in the slot's `node_modules/@typescript/`.
---
Written by an agent (Claude Code, claude-opus-4-7).
|
||
|
|
da65e62625 |
fix: pacquet install --frozen-lockfile works against pnpm v11 repos (#525)
* fix(lockfile): skip env document in pnpm v11 combined lockfiles Pnpm v11 writes `pnpm-lock.yaml` as a stream of up to two YAML documents: an optional first document carries the package-manager bootstrap (`packageManagerDependencies` and the snapshots that back it) and the second document is the regular project lockfile. Pacquet hands the file straight to `serde_saphyr::from_str`, which rejects a multi-document stream with "multiple YAML documents detected" — so every install against a v11 repo that has a `packageManager` / `devEngines.packageManager` declaration fails before staleness checking even runs. Port upstream's `extractMainDocument` to a new `yaml_documents` module: if the file starts with `---\n`, return the slice after the next `\n---\n` separator; if there is no second separator, the file is env-only and the loader returns `Ok(None)`. `load_from_path` threads the lockfile content through the filter before deserializing, matching pnpm's `_read` call site at https://github.com/pnpm/pnpm/blob/31858c544b/lockfile/fs/src/read.ts#L103-L110. Three unit tests in `yaml_documents/tests` mirror upstream's `extractMainDocument` test cases, and two integration tests in `load_lockfile/tests.rs` cover the combined-document and env-only paths end-to-end. --- Written by an agent (Claude Code, claude-opus-4-7). * fix(package-manifest): reify devEngines.runtime into devDependencies With #511 / #512 pacquet recognises `runtime:` specifiers in the lockfile, so a frozen install against a v11 repo gets past the YAML parse but then trips the staleness check: the lockfile lists `node@runtime:24.6.0` under the root importer's `devDependencies`, while the on-disk `package.json` only declares the runtime through `devEngines.runtime`. The flat-record diff then surfaces a spurious "node@runtime:24.6.0 was removed" mismatch. Port upstream's `convertEnginesRuntimeToDependencies` to a new free function in `crates/package-manifest`. For each of `node`, `deno`, `bun`, if `devEngines.runtime` (or `engines.runtime`) declares the runtime with `onFail: "download"` and an explicit version, and the target dependencies bucket has no explicit entry yet, insert `<name>: "runtime:<version>"`. Array and single-object runtime shapes are both accepted. Skip when `onFail` is anything other than `"download"` or when the version is absent — upstream warns on the missing-version path; pacquet skips silently and the staleness check still surfaces the gap if it matters downstream. WebContainer's "no runtime download" branch is intentionally omitted since pacquet does not run there. `PackageManifest::read_from_file` calls the function for both `(devEngines, devDependencies)` and `(engines, dependencies)`, mirroring upstream's `convertManifestAfterRead` at https://github.com/pnpm/pnpm/blob/9cad8274fd/workspace/project-manifest-reader/src/index.ts#L227-L231. Six unit tests in `tests.rs` cover the happy path, the no-version skip, the non-`download` `onFail` skip, preservation of an explicit user-declared entry, the array-of-runtimes form, and the `engines` → `dependencies` variant. A seventh test exercises the hook through `PackageManifest::from_path` end-to-end. Upstream reference: https://github.com/pnpm/pnpm/blob/9cad8274fd/pkg-manifest/utils/src/convertEnginesRuntimeToDependencies.ts#L10-L45. --- Written by an agent (Claude Code, claude-opus-4-7). |
||
|
|
b07b152c19 |
feat(lockfile,package-manager,real-hoist): npm-alias importer dep versions (#524)
pnpm writes importer dependency `version:` fields in three shapes:
bare semver-with-peer (`4.0.0`), `link:<path>`, or — when a specifier
(typically `catalog:`) resolves to a different package name — the full
npm-alias `<name>@<version>`. The third shape is what
`refToRelative` recognises with the same leading-`@` / `@` before
`(`/`:` test that `SnapshotDepRef` already uses, and which pnpm v11
emits for entries like:
js-yaml:
specifier: 'catalog:'
version: '@zkochan/js-yaml@0.0.11'
Pacquet's `ImporterDepVersion` only modelled `Regular` and `Link`, so
deserialising a lockfile with an aliased catalog dep failed with
"Failed to parse importer dependency version".
Add an `Alias(PkgNameVerPeer)` variant and a `resolved_key` helper
that returns the correct snapshot-map key for each shape — the
importer-map key paired with the version for `Regular`, the alias's
own `(name, suffix)` for `Alias`, and `None` for `Link`. Every site
that previously built a key from `as_regular().map(|v| PkgNameVerPeer::new(name, v))`
now goes through `resolved_key`, so aliased deps reach the snapshot,
the skipped-set, the reachability BFS, the build-sequence root walk,
the runtime exclusion check, and both hoist passes correctly.
For symlink targets, an aliased dep links the importer-key name to
`<slot>/node_modules/<alias-real-name>` (the resolved package's true
name inside its slot), matching pnpm's `linkDirectDeps`. The
`pnpm:root added` event now reports `realName` as the resolved
package name for aliases, where before it always echoed the
importer-map key.
Upstream reference: `refToRelative` in
`pnpm/pnpm@8a80235c7b/deps/path/src/index.ts:96-110`.
|
||
|
|
8a80235c7b | chore(release): 11.1.2 v11.1.2 | ||
|
|
18a464f5b4 |
fix(network): strip sec-fetch-* headers to fix Azure DevOps Artifacts 400 errors (#11602)
* fix(network): strip sec-fetch-* headers to fix Azure DevOps Artifacts 400 errors undici's fetch() automatically adds sec-fetch-* headers (e.g. sec-fetch-mode: cors) per the Fetch spec. Azure DevOps Artifacts interprets these as browser requests and returns HTTP 400 for uncached upstream packages. Since pnpm is a CLI tool, these headers serve no purpose. Adds a stripSecFetchHeaders interceptor applied to all dispatchers (global, proxy, and non-proxy) via undici's compose() API. Fixes #11572 * refactor: fix header types and function placement in stripSecFetchHeaders - Widen header type from Record<string, string> to Record<string, string | string[] | undefined> to match Dispatcher.DispatchOptions - Move stripSecFetchHeaders below its first use, relying on function hoisting per codebase conventions * refactor(network.fetch): handle iterable header form and tidy test `Dispatcher.dispatch` accepts headers as a Map/web-Headers iterable in addition to the flat string[] and plain object forms. The previous object branch routed iterables through Object.entries, which would silently drop every header for Map-like inputs. Detect Symbol.iterator and consume the iterator directly when present. Also drop the underscore prefix on the test's `req` parameter since it is used. --------- Co-authored-by: Zoltan Kochan <z@kochan.io> |
||
|
|
8c06d1a2f9 |
fix: preserve named catalog group during interactive upgrade --latest (#11567)
When upgrading a dependency that uses a named catalog (e.g. "catalog:foo"), the previous specifier's catalog name now takes priority over the global saveCatalogName option. This prevents the package.json from being rewritten to "catalog:" and the updated version from landing in the default catalog instead of the named one. Closes #10115 Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
e526f89650 |
fix(npm-resolver): minimumReleaseAge handling for cached abbreviated metadata (#11622)
* fix(npm-resolver): dont rethrow ERR_PNPM_MISSING_TIME from version-spec cache * fix(npm-resolver): upgrade cached abbreviated metadata on 304 for minimumReleaseAge * fix(npm-resolver): expand abbreviated-meta upgrade to in-memory cache and preferOffline paths * fix(npm-resolver): address Copilot review feedback on pickPackage - Extract `persistUpgradedMeta` helper and call it from all three sites (in-memory cache hit, preferOffline disk-cache hit, 304 path) so a fresh process doesn't repeat the upgrade fetch. - Forward `etag`/`modified` to the upgrade fetch in `maybeUpgradeAbbreviatedMetaForReleaseAge` so the registry can answer 304. - Extract `shouldRethrowFromFastPathCache` so the two fast-path catch sites can't drift on the MISSING_TIME-vs-strict invariant. - Document the deliberate choice to upgrade-fetch when `meta.modified` is absent or unparseable (correctness over saving a network call). - Add a companion test that exercises the catch fix with the default `ignoreMissingTimeField` so the invariant holds regardless of that flag. - Fix the existing `bareSpecifier: '3.1.0'` test setup: 3.1.0 was published 2016-01-11, after the test's `publishedBy` of 2015-08-17, so strict mode correctly rejected it. Switch to 3.0.0 (released 2015-07-10). * chore(npm-resolver): replace 'unparseable' with 'malformed' for cspell * style(npm-resolver): declare pickPackage helpers after their caller --------- Co-authored-by: Zoltan Kochan <z@kochan.io> |
||
|
|
fb221c626c |
feat(cmd-shim,package-manager): hoisted-bin precedence + post-build top-level bin re-link (#342) (#523)
Two related bin-linking behaviors deferred from #333. Behavior 1 — hoisted-bin precedence: - Add `BinOrigin { Direct, Hoisted }` discriminator to `PackageBinSource`. - New top tier in `pick_winner`: Direct wins outright over Hoisted regardless of ownership / lexical order. Mirrors upstream's `preferDirectCmds` partition at https://github.com/pnpm/pnpm/blob/4750fd370c/bins/linker/src/index.ts#L92. - New `link_top_level_bins(modules_dir, direct, hoisted)` helper in `pacquet-package-manager` mixes both candidate lists into one `link_bins_of_packages` call so the new tier resolves conflicts in a single pass — previously the two passes (SymlinkDirectDependencies for direct + hoist pass for publicly-hoisted) wrote shims independently and a hoisted bin could shadow a direct one when its package name was lexically smaller. Behavior 2 — lifecycle-script-created bins: - Add a post-`BuildModules` per-importer top-level bin link pass. Re-reading each direct dep's `package.json` after lifecycle scripts run picks up bins generated by `postinstall` (the `@pnpm.e2e/generated-bins` upstream fixture). Idempotent for unchanged shims via `is_shim_pointing_at`. - Mirrors upstream's `linkBinsOfImporter` pass that runs after `buildModules` at https://github.com/pnpm/pnpm/blob/4750fd370c/installing/deps-installer/src/install/index.ts#L1539. Supporting changes: - `PackageBinSource::new(location, manifest)` constructor + `with_origin` builder so existing call sites don't have to spell out the new field. - Public `direct_dep_names_for_importer` helper extracted from `SymlinkDirectDependencies` so the post-build pass uses the same filter (skipped / first-wins / link_only) as the symlink phase. - `InstallFrozenLockfileError::TopLevelBinLink` for the new failure surface. Tests: - `direct_origin_wins_over_hoisted_regardless_of_lexical` — pins the new tier overrides lexical ordering. - `hoisted_origin_loses_to_existing_direct` — pins both arms of the new tier (Direct incumbent vs Hoisted candidate). |
||
|
|
180aee9ba5 |
fix: handle lockfile conflicts in optimistic install (#11605)
* fix: handle lockfile conflicts in optimistic install * test: move sharedWorkspaceLockfile out of WorkspaceState.settings `sharedWorkspaceLockfile` is not in `WORKSPACE_STATE_SETTING_KEYS`, so placing it inside `WorkspaceState.settings` broke type checking. Pass it directly on the `CheckDepsStatusOptions` instead. * chore: add lockfile/fs project reference to installing/commands tsconfig `@pnpm/lockfile.fs` was added as a runtime dependency but the tsconfig project references were not updated. meta-updater enforces this in CI. * perf: restore optimistic-repeat-install fast-path for conflict-free state The first iteration of the conflict-detection fix unconditionally read pnpm-lock.yaml on every install - once in installDeps and again inside checkDepsStatus - defeating the point of optimisticRepeatInstall, which was to skip reading the lockfile entirely when nothing changed. Restore the fast path by: - Dropping the redundant lockfile read from installDeps. checkDepsStatus already returns upToDate: false when the lockfile is conflicted, so the pre-check was dead weight. - Gating the conflict check inside checkDepsStatus on the lockfile's mtime: if it hasn't been touched since the last successful install, it cannot have grown conflict markers, so the read is skipped. Conflict markers introduced after a successful install (e.g. via git pull/merge) still update the lockfile mtime, so the correctness fix is preserved. * perf: make lockfile-conflict check synchronous findConflictedLockfileDir awaits its work serially with no concurrent operations to interleave, so the async overhead (Promise.all microtasks, event-loop hops) buys nothing. Convert to a plain for-loop with fs.statSync and a new wantedLockfileHasMergeConflictsSync export. Also resolves a test-mock issue: the previous version called safeStat from deps/status, which is jest-mocked across the test file. The mocked safeStat returned undefined (or stale stats from earlier tests), causing the conflict check to silently no-op. Switching to fs.statSync bypasses the mock and gets the real mtime of the temp lockfile the regression tests write. --------- Co-authored-by: Zoltan Kochan <z@kochan.io> |
||
|
|
c2c289094f | fix: time-based resolution loses publishedAt on fast path (#11618) | ||
|
|
1ad6ffd152 |
feat(config,package-manager): hoistingLimits + externalDependencies knobs (#438 slice 10) (#522)
Plumbs the two programmatic-only hoister knobs from
`pnpm-workspace.yaml` through to the slice 4 walker and the slice 3
hoister. Both fields already existed on `HoistOpts`; this slice wires
them end-to-end.
- `Config::hoisting_limits: BTreeMap<String, BTreeSet<String>>` —
per-importer block-list, locator-keyed (`'.@'` for the root). Reads
`hoistingLimits: { ".@": [foo, bar] }` from yaml. Mirrors upstream's
https://github.com/pnpm/pnpm/blob/94240bc046/installing/linking/real-hoist/src/index.ts#L10
programmatic-only knob, exposed as yaml for parity since the
ergonomics of the locator-keyed map don't translate to a CLI flag.
- `Config::external_dependencies: BTreeSet<String>` — name slots
reserved at the root for an external linker (the Bit CLI is the
only known consumer upstream). Reads `externalDependencies: [...]`
from yaml.
- `LockfileToHoistedDepGraphOptions` gains both fields and forwards
them to `HoistOpts` in `build_dep_graph`.
- `InstallFrozenLockfile::run` clones the two `Config` fields into the
walker opts.
Both knobs default to empty (no limits, no externals), matching
upstream's default. Neither has any effect under `nodeLinker:
isolated` — the isolated linker keeps per-importer subtrees by
construction and doesn't consult the hoister.
Tests:
- `parses_hoisting_limits_from_yaml_and_applies` — yaml round-trip +
apply_to.
- `parses_external_dependencies_from_yaml_and_applies` — same.
- `omitting_hoisting_limits_and_external_dependencies_keeps_defaults`
— pins the apply_to skip-on-None branch so a yaml without these
keys doesn't accidentally overwrite Config defaults.
- `walker_forwards_external_dependencies_to_hoister` — end-to-end:
the walker observes an empty graph for an externalised alias
because the hoister stripped it. Pins the slice 10 plumbing.
|