Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: pnpm/pnpm
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v11.2.0
Choose a base ref
...
head repository: pnpm/pnpm
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v11.2.1
Choose a head ref
  • 6 commits
  • 119 files changed
  • 1 contributor

Commits on May 20, 2026

  1. feat(pacquet): wire NpmResolver into install; fix(pick-registry) unsc…

    …oped npm-alias routing (#11760)
    
    Two changes ship together: the bulk is the pacquet refactor described in #11756, plus a TypeScript-side fix to `@pnpm/config.pick-registry-for-package` that surfaced during review.
    
    ### Pacquet — wire NpmResolver into install (Phases A/B/C of #11756)
    
    - **Phase A.** New `parse_bare_specifier.rs` and `npm_resolver.rs` in `pacquet-resolving-npm-resolver`. `NpmResolver` implements the `Resolver` trait: parses the bare specifier (including npm-alias `npm:@scope/name@<spec>` and tarball-URL forms — with prefix-anchored name validation), picks a version via `pick_package`, surfaces `minimumReleaseAge` violations inline via `detect_min_release_age_violation`. `workspace:` specs decline so the chain falls through. `published_by` / `published_by_exclude` / `dry_run` added to `ResolveOptions`.
    - **Phase B.** `install_without_lockfile.rs` constructs an `NpmResolver` at install entry from the config-derived registries map and an `InMemoryPackageMetaCache` that's shared across the resolve pass and dropped before the install pass.
    - **Phase C.** New `pacquet-resolving-deps-resolver` crate exposes `resolve_dependency_tree`: a flat `name@version`-keyed package map with parent-child edges, concurrent sibling resolution via `try_join_all`, per-id dedup gate. `install_package_from_registry.rs` no longer calls `Package::fetch_from_registry` / `Package::pinned_version`; it takes a pre-resolved `ResolveResult` and reads tarball URL + integrity off `LockfileResolution::Tarball`.
    
    Additional behaviors landed during review:
    
    - **`minimumReleaseAge` policy in the resolve pass.** Previously only enforced by the lockfile-verification gate; the no-lockfile resolve pass now derives `published_by` and the exclude policy from `Config` so resolver-time picks match the configured policy.
    - **`SPEC_NOT_SUPPORTED_BY_ANY_RESOLVER` surfaces correctly.** `resolve_dependency_tree` now returns a typed error when the chain returns `Ok(None)` — silently dropping the edge would leave installs missing transitive deps. Mirrors upstream's `default-resolver` error shape.
    - **Per-package progress events.** `InstallPackageFromRegistry` takes a `first_visit: bool`; `pnpm:progress resolved` / `pnpm:progress imported` plus the tarball download fire once per `(name, version)`, while the per-parent `symlink_package` runs on every edge. Matches upstream's per-package (not per-edge) reporter contract.
    - **Windows symlink race fix.** `ResolvedPackages` is now `DashMap<String, watch::Sender<bool>>`; the first writer signals completion after `import_indexed_dir`, so a second visitor's `symlink_package` (which may fall back to a Windows junction requiring an existing target) doesn't race ahead of the materialization. A dropped first-writer task surfaces as a typed `FirstWriterAborted` error.
    - **Scope routing.** `pick_registry_for_package` is now bareSpecifier-aware so an entry like `"foo": "npm:@acme/bar@^1"` routes through `registries[@acme]`.
    
    ### TS — `@pnpm/config.pick-registry-for-package` unscoped-target fix
    
    A separate bug surfaced during the scope-routing port: `pickRegistryForPackage('@private/foo', 'npm:lodash@^1')` was routing through `registries['@Private']`, even though `lodash` is unscoped and doesn't live on the `@private` registry. `getScope` now returns `null` in the npm-alias branch when the alias target is unscoped (instead of falling through to the local pkgName's scope). Changeset is in `.changeset/pick-registry-unscoped-npm-alias.md` (patch bump for `@pnpm/config.pick-registry-for-package` and `pnpm`). Added matching tests on both the TS and pacquet sides.
    
    ### Out of scope (left as #11756 follow-ups)
    
    - Preferred-versions harvesting from the lockfile (Phase D).
    - Install-side aggregation of `policy_violation` from the tree (Phase E) — the resolver attaches them per-pick already, but the install layer doesn't yet collect or fail on them.
    - Other-protocol resolvers (git, tarball, workspace, jsr, named-registry, …) — `NpmResolver` is the only chain entry today; once a second resolver lands, `DefaultResolver` will get wired in too.
    - Full `parseBareSpecifier.test.ts` corpus port — the parser tests pacquet ships cover the cases the install path exercises; remaining corpus items land alongside Phase F.
    
    Closes part of #11756.
    zkochan authored May 20, 2026
    Configuration menu
    Copy the full SHA
    097983f View commit details
    Browse the repository at this point in the history
  2. test(pnpm): group release-brittle tests under a shared describe block (

    …#11767)
    
    Three tests resolve the running pnpm version's integrity from registry-mock,
    which proxies pnpm to npmjs. They fail every release between the version
    bump commit and the matching npm publish ("No matching version found for
    pnpm@<version>"), then pass again once the version lands on npmjs. Group
    them under a 'release-brittle' describe in each file so the failure mode
    is obvious from the test name and only stated once.
    zkochan authored May 20, 2026
    Configuration menu
    Copy the full SHA
    ef87f3c View commit details
    Browse the repository at this point in the history
  3. fix(env-installer): suppress 'Installing config dependencies...' on n…

    …o-op installs (#11766)
    
    * fix(env-installer): only print "Installing config dependencies..." when work is actually being done
    
    Previously the message was emitted unconditionally for every config
    dependency, before any of the "do we need to fetch / re-symlink?"
    checks. As a result the banner printed on every install even when
    everything was already cached and correctly linked.
    
    Emit the started event lazily — at most once per install, and only
    when an orphan is being removed, a parent or subdep needs fetching,
    a parent symlink needs (re)creating, or orphan subdep siblings are
    being pruned.
    
    ---
    Written by an agent (Claude Code, claude-opus-4-7).
    
    * test(env-installer): assert installing-config-deps events fire only when work happens
    
    Captures `streamParser` events around `resolveAndInstallConfigDeps`
    to verify the lazy emission introduced in the previous commit:
    - fresh install emits both `started` and `done`,
    - a follow-up no-op install emits neither,
    - removing a config dep still emits `started` (orphan cleanup work).
    
    ---
    Written by an agent (Claude Code, claude-opus-4-7).
    
    * test(env-installer): subscribe to streamParser once at module load
    
    `streamParser` is a `split2` Transform stream that buffers writes until
    the first 'data' listener attaches and then drains the whole buffer into
    it. Subscribing per-test made the new install-config-deps test capture
    events from every earlier test in the file. Move the subscription to
    module load and have each test drain the accumulated events around its
    own call.
    
    Also drop the "removal" assertion: `resolveAndInstallConfigDeps` does
    not prune entries that disappear from the configDeps argument (lockfile
    pruning happens at a higher layer), so the scenario it claimed to test
    never actually fired the orphan-cleanup path.
    
    ---
    Written by an agent (Claude Code, claude-opus-4-7).
    
    * fix(env-installer): emit started when only the sibling symlink needs relinking
    
    If a config dep's optional subdep is already cached in the global
    virtual store but the sibling symlink under the parent's node_modules
    is missing or points at a stale target, symlinkDir() does real work
    without reportStarted ever firing. Check whether the link already
    points at the expected target and only fire reportStarted + symlinkDir
    when it doesn't, mirroring the parentSymlinkAlreadyCorrect path.
    
    Also clean up the test-level streamParser listener in afterAll so the
    subscription doesn't outlive the test file.
    
    ---
    Written by an agent (Claude Code, claude-opus-4-7).
    zkochan authored May 20, 2026
    Configuration menu
    Copy the full SHA
    e5e7b72 View commit details
    Browse the repository at this point in the history
  4. fix(pacquet): shorten long virtual store dirnames to avoid ENAMETOOLO…

    …NG (#11768)
    
    * fix(pacquet): shorten long virtual store dirnames to avoid ENAMETOOLONG
    
    Peer-heavy snapshot keys (e.g. vitest with a dozen browser / coverage /
    DOM peers) produced flat-name directories that overflowed macOS's 255-
    byte filename limit, so `install` aborted with errno 63 before unpacking
    any tarballs. Port the trailing length / case-shortening branch of
    upstream's `depPathToFilename` (deps/path/src/index.ts:169) so the name
    becomes `<prefix>_<32-hex-sha256>` capped at `virtualStoreDirMaxLength`
    bytes (default 120).
    
    Extract `create_short_hash` and `shorten_virtual_store_name` into a new
    `pacquet-crypto-hash` crate mirroring upstream `@pnpm/crypto.hash`;
    `pacquet-lockfile`, `pacquet-registry`, and `pacquet-store-dir` all
    consume it instead of duplicating the sha2 + truncate logic.
    
    Reported via pnpm/pacquet issue triage (vitest@4.1.6 peer suffix).
    
    * fix(pacquet): taplo format and remove broken intra-doc link
    
    Format `pacquet/crates/crypto-hash/Cargo.toml` per the workspace
    `.taplo.toml` (aligns the `[package]` keys) and downgrade the
    `pacquet_modules_yaml::DEFAULT_VIRTUAL_STORE_DIR_MAX_LENGTH` reference
    in `PkgNameVerPeer::to_virtual_store_name` to plain text, since
    `pacquet-lockfile` deliberately does not depend on
    `pacquet-modules-yaml` and `RUSTDOCFLAGS=-D warnings` rejected the
    unresolved intra-doc link.
    
    * feat(pacquet/config): expose virtualStoreDirMaxLength
    
    Add `virtual_store_dir_max_length: u64` to `Config` with default 120
    (matching `pacquet_modules_yaml::DEFAULT_VIRTUAL_STORE_DIR_MAX_LENGTH`).
    Wire it through `WorkspaceSettings.virtualStoreDirMaxLength` and the
    `PNPM_CONFIG_VIRTUAL_STORE_DIR_MAX_LENGTH` env-overlay so users can
    override the threshold via `pnpm-workspace.yaml`, global `config.yaml`,
    or environment variables — mirroring upstream
    `Config.virtualStoreDirMaxLength`.
    
    The three flat-name call sites (`install_without_lockfile.rs`,
    `install_package_from_registry.rs`, `virtual_store_layout.rs`) and the
    `.modules.yaml` writer now read the configured value instead of the
    hardcoded constant. `VirtualStoreLayout::legacy` takes the value as an
    explicit second arg so test fixtures don't silently inherit a default.
    zkochan authored May 20, 2026
    Configuration menu
    Copy the full SHA
    c068720 View commit details
    Browse the repository at this point in the history
  5. fix(env-installer): mark optional config subdep snapshots with option…

    …al: true (#11770)
    
    Match how optional packages are recorded elsewhere in pnpm-lock.yaml so
    non-host platform variants pulled in via a config dep's optionalDependencies
    aren't treated as required.
    zkochan authored May 20, 2026
    Configuration menu
    Copy the full SHA
    2061c55 View commit details
    Browse the repository at this point in the history
  6. Configuration menu
    Copy the full SHA
    11a43b1 View commit details
    Browse the repository at this point in the history
Loading