Skip to content

fix(pacquet): dedupeDirectDeps default + per-importer workspace link resolution, with defaults contract test#12128

Merged
zkochan merged 3 commits into
mainfrom
fix/pacquet-dedupe-direct-deps-default
Jun 2, 2026
Merged

fix(pacquet): dedupeDirectDeps default + per-importer workspace link resolution, with defaults contract test#12128
zkochan merged 3 commits into
mainfrom
fix/pacquet-dedupe-direct-deps-default

Conversation

@zkochan

@zkochan zkochan commented Jun 2, 2026

Copy link
Copy Markdown
Member

What

Two pacquet fixes for workspace link: handling, both surfaced by the v11.5.1 release failure, plus a contract test that prevents the class of bug from recurring.

1. dedupeDirectDeps default truefalse (the release cause)

pnpm's config-reader default is false (config/reader/src/index.ts:139, 'dedupe-direct-deps': false); pacquet defaulted to true.

The v11.5.1 release run failed at Publish @pnpm/exe with ERR_PNPM_CANNOT_RESOLVE_WORKSPACE_PROTOCOL for @pnpm/jest-config:

  • v11.5.1 bumped the @pnpm/pacquet configDependency (0.2.80.2.2-15, a newer build despite the lower semver), engaging pacquet as the install engine for the first time (v11.5.0's release log mentions pacquet zero times).
  • The release installs via pacquet (frozen), then pnpm publish @pnpm/exe rewrites its workspace:* devDependency on @pnpm/jest-config by reading exe/node_modules/@pnpm/jest-config/package.json.
  • The workspace root also depends on @pnpm/jest-config, so the erroneous dedupe removed the per-importer symlink and publish aborted.

Verified on the repo: a pacquet 0.2.2-15 frozen install leaves pnpm/artifacts/exe/node_modules/@pnpm/jest-config missing; the JS engine keeps it.

2. Per-importer workspace link: resolution

A non-injected workspace dep resolves to a link:<path> computed relative to the consuming importer's directory. pacquet's workspace-wide resolved_by_wanted cache omitted project_dir from its key, so the first importer to resolve a shared workspace dep seeded the cache with its own relative path and every other importer reused it verbatim — a root resolving link:packages/lib handed packages/app the same string (dangling packages/app/packages/lib; pnpm writes link:../lib). The fix adds project_dir to the key only for project-relative specifiers (link:/file:/workspace:); registry specifiers stay importer-independent and keep sharing one cache slot.

3. Contract test: pacquet defaults == pnpm CLI defaults

crates/config/src/pnpm_default_parity.rs reads pnpm's defaultOptions literal from config/reader/src/index.ts at test time and asserts every pacquet default that maps to a pnpm setting equals pnpm's value — the dedupeDirectDeps divergence would have failed here instead of in the release pipeline. A second test partitions all 80 pnpm settings into mapped / non-literal (env·platform·CPU) / not-ported, so a new pnpm setting that's neither mapped nor skipped fails the test until someone classifies it.

Tests

  • The dedupe-on integration tests now set dedupeDirectDeps: true explicitly (mirroring upstream's testDefaults({ ..., dedupeDirectDeps: true })).
  • dedupe_off_by_default_keeps_shared_workspace_link pins the default-off behavior against a pnpm-written frozen lockfile (reproduces the release scenario).
  • shared_workspace_dep_link_is_relative_to_each_importer pins per-importer link: paths on a fresh resolve.
  • pacquet_defaults_match_pnpm_cli_defaults + every_pnpm_default_is_classified — the contract tests above.

Each new behavioral test was confirmed to fail before its fix. just-level checks run: fmt, clippy --deny warnings, cargo doc -D warnings, dylint (pre-push), typos, and the resolver + config + workspace/dedupe/lockfile-reuse suites.

Note for the release

The configDependency @pnpm/pacquet@0.2.2-15 is the published binary; these source fixes only take effect once a new pacquet build is published and configDependencies['@pnpm/pacquet'] in pnpm-workspace.yaml is bumped to it.


Written by an agent (Claude Code, claude-opus-4-8).

Summary by CodeRabbit

  • Bug Fixes

    • Non-root workspace packages now retain their own workspace symlink when direct-dep deduplication is off, preventing release/publish failures.
  • Documentation

    • Workspace config docs updated: direct dependency deduplication now defaults to off.
  • Tests

    • Added and expanded tests covering dedupe-direct-deps behavior, workspace linking, frozen-lockfile scenarios, and parity of default config values with upstream.

pacquet defaulted `dedupe_direct_deps` to `true`, but pnpm's
config-reader default is `false` (config/reader/src/index.ts:139,
`'dedupe-direct-deps': false`). With the wrong default, a direct
dependency that both the workspace root and a non-root importer
declare gets dropped from the non-root importer's `node_modules/`.

This broke the v11.5.1 release (.github#214). The release installs
the monorepo through pacquet's frozen-lockfile path, then runs
`pnpm publish` on `@pnpm/exe`, which rewrites its `workspace:*`
devDependency on `@pnpm/jest-config` by reading
`exe/node_modules/@pnpm/jest-config/package.json`. The workspace
root also depends on `@pnpm/jest-config`, so pacquet's erroneous
dedupe removed the per-importer symlink and publish failed with
`ERR_PNPM_CANNOT_RESOLVE_WORKSPACE_PROTOCOL`.

The dedupe-on integration tests already in this file relied on the
old default; they now set `dedupeDirectDeps: true` explicitly,
mirroring upstream's `testDefaults({ ..., dedupeDirectDeps: true })`.
A new test pins the default-off behavior against a pnpm-written
frozen lockfile, reproducing the release scenario.
Copilot AI review requested due to automatic review settings June 2, 2026 06:57
@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5e43a85c-f5c8-4488-b6ab-d29490b140de

📥 Commits

Reviewing files that changed from the base of the PR and between e018f24 and d3c5f33.

📒 Files selected for processing (2)
  • pacquet/crates/config/src/lib.rs
  • pacquet/crates/config/src/pnpm_default_parity.rs
📜 Recent review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: Agent
  • GitHub Check: Analyze (javascript)
  • GitHub Check: Doc
  • GitHub Check: Lint and Test (ubuntu-latest)
  • GitHub Check: Lint and Test (macos-latest)
  • GitHub Check: Lint and Test (windows-latest)
  • GitHub Check: Code Coverage
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Compile & Lint
🧰 Additional context used
📓 Path-based instructions (1)
pacquet/**/*.rs

📄 CodeRabbit inference engine (pacquet/AGENTS.md)

pacquet/**/*.rs: Log emissions are part of matching pnpm — when porting a function that fires pnpm:<channel> events through globalLogger, logger.debug(...), or streamParser.write(...), mirror the call site, payload, and ordering so @pnpm/cli.default-reporter parses pacquet's NDJSON the same way
Declare a newtype wrapper for branded string types instead of collapsing the brand into a plain String or &str in Rust
If upstream TypeScript always validates before construction of a branded string, validate in the Rust wrapper too via TryFrom<String> and/or FromStr and do not provide an infallible public constructor
If upstream TypeScript never validates a branded string, just brand for type-safety in Rust by exposing an infallible From<String> constructor
If upstream TypeScript occasionally constructs a branded string without validation, expose from_str_unchecked in Rust as an escape hatch alongside the validating constructor
Match upstream serde behavior for branded strings crossing JSON, YAML, or INI boundaries by using #[serde(try_from = "String")] for deserialization and #[serde(into = "String")] for serialization
Derive simple conversions for branded strings using #[derive(derive_more::From)] and #[derive(derive_more::Into)] instead of handwriting impl blocks; use manual impl only when conversion needs custom logic
Model TypeScript string literal unions (like 'auto' | 'always' | 'never') as Rust enums instead of newtype wrappers, since the set of valid values is closed
Treat TypeScript string template literal types (like `${string}@${string}`) the same as branded string types in Rust, using a newtype wrapper with validation
Follow the code style guide in CODE_STYLE_GUIDE.md — imports, modules, naming, ownership and borrowing, parameter type selection, trait bounds, pattern matching, pipe-trait, error handling, test layout, and cloning of Arc and Rc
Choose owned vs. borrowed parameters to minimize copies; widen to t...

Files:

  • pacquet/crates/config/src/lib.rs
  • pacquet/crates/config/src/pnpm_default_parity.rs
🧠 Learnings (3)
📚 Learning: 2026-05-20T19:40:55.051Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11774
File: pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs:0-0
Timestamp: 2026-05-20T19:40:55.051Z
Learning: In the pacquet Rust code, ensure the semver implementation uses the `node-semver` crate (not `nodejs-semver`). `node-semver`’s public API does not include a `satisfies_with_prerelease`-style method; prerelease-tolerant matching should be implemented inline by first calling `Range::satisfies`, and when it rejects a prerelease version, retry matching against a stripped `MAJOR.MINOR.PATCH` base of the prerelease version.

Applied to files:

  • pacquet/crates/config/src/lib.rs
  • pacquet/crates/config/src/pnpm_default_parity.rs
📚 Learning: 2026-05-22T00:08:44.646Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11837
File: pacquet/crates/resolving-npm-resolver/src/pick_package.rs:33-51
Timestamp: 2026-05-22T00:08:44.646Z
Learning: In the pnpm/pnpm repo’s pacquet Rust crates, do not flag Unicode ellipsis characters (U+2026, `…`) in Rust doc comments (`///` / `/** */`) as a lint violation. The pacquet crate’s `dylint.toml` only enables `perfectionist::derive_ordering`, and the Dylint `unicode-ellipsis` rule is not enabled for this project—so `…` in doc comments is an intentional, repo-consistent style.

Applied to files:

  • pacquet/crates/config/src/lib.rs
  • pacquet/crates/config/src/pnpm_default_parity.rs
📚 Learning: 2026-05-20T23:07:58.444Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11784
File: pacquet/crates/resolving-deps-resolver/src/hoist_peers.rs:120-133
Timestamp: 2026-05-20T23:07:58.444Z
Learning: When reviewing code in this pacquet Rust port, follow the upstream pnpm compatibility rule: only match pnpm’s behavior exactly. Do not propose review changes that intentionally deviate from pnpm’s documented/observed behavior, even if pnpm appears buggy. If you identify a real bug in pnpm behavior, the review should prioritize fixing it upstream in pnpm first, and avoid implementing a pnpm-behavior workaround here unless the same fix has already landed upstream.

Applied to files:

  • pacquet/crates/config/src/lib.rs
  • pacquet/crates/config/src/pnpm_default_parity.rs
🔇 Additional comments (8)
pacquet/crates/config/src/lib.rs (2)

721-733: LGTM!


1830-1831: LGTM!

pacquet/crates/config/src/pnpm_default_parity.rs (6)

1-44: LGTM!


46-99: LGTM!


101-195: LGTM!


197-240: LGTM!


242-309: LGTM!


311-371: LGTM!


📝 Walkthrough

Walkthrough

Change default for dedupe_direct_deps to false, partition resolver cache entries by importer for project-relative specs, and update/add workspace tests to assert importer-local link behavior and a frozen-lockfile regression.

Changes

Dedupe Direct Deps Default & Workspace Resolution

Layer / File(s) Summary
Configuration Default Update
pacquet/crates/config/src/lib.rs
The dedupe_direct_deps field's #[default] attribute and documentation are updated to false.
Scope per-importer project-relative resolutions
pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
Add project_dir: Option<PathBuf> to the per-wanted cache key, introduce is_project_relative_specifier, and include importer-specific project scope in both fresh and reused resolution cache logic.
Test Suite Documentation and Existing Test Updates
pacquet/crates/cli/tests/dedupe_direct_deps.rs
Module docs rewritten to state dedupe is off by default; one test renamed to reflect explicit dedupeDirectDeps: true; multiple tests updated to explicitly set dedupeDirectDeps: true in workspace YAML for dedupe scenarios.
Regression Test for Default Off-by-Default Behavior
pacquet/crates/cli/tests/dedupe_direct_deps.rs
New test omits dedupeDirectDeps, uses a supplied pnpm-lock.yaml, runs pacquet install --frozen-lockfile, and asserts a non-root importer keeps a workspace symlink resolving to package.json.
Importer-relative workspace link integration test
pacquet/crates/cli/tests/workspace_install.rs
New test sets up two importers depending on the same workspace package, runs install, verifies pnpm-lock.yaml records importer-relative link: targets and asserts on-disk importer-relative symlink resolves to the shared package.
pnpm defaults parity contract test
pacquet/crates/config/src/pnpm_default_parity.rs, pacquet/crates/config/src/lib.rs
Add contract-test module that parses pnpm's defaultOptions and verifies Config::default() matches mapped pnpm defaults, plus compile-time test module wiring under #[cfg(test)].

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • pnpm/pnpm#11944: Related workspace/link resolution and linked-node short-circuiting work touching resolver behavior.
  • pnpm/pnpm#12024: Overlapping changes to dedupeDirectDeps behavior and test coverage adjustments.
  • pnpm/pnpm#11905: Related adjustments ensuring project-relative/workspace resolutions are handled per-importer.

Suggested reviewers

  • anonrig

Poem

🐰 A config flip, a gentle hop,
Each importer keeps its little prop,
Tests read locks and follow the link,
Symlinks settle where they should sink,
Hop, install—workspace all set to shop.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically summarizes the main changes: fixing dedupeDirectDeps default behavior and implementing per-importer workspace link resolution caching, with an added contract test to maintain parity with pnpm defaults.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/pacquet-dedupe-direct-deps-default

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Review Summary by Qodo

Fix dedupe_direct_deps default to false, matching pnpm config

🐞 Bug fix

Grey Divider

Walkthroughs

Description
• Changed dedupe_direct_deps default from true to false to match pnpm
• Fixes v11.5.1 release failure where shared workspace deps were incorrectly deduplicated
• Updated existing tests to explicitly set dedupeDirectDeps: true
• Added regression test for default-off behavior with frozen lockfile
Diagram
flowchart LR
  A["pacquet config default"] -->|changed from true| B["dedupe_direct_deps = false"]
  B -->|matches| C["pnpm config-reader default"]
  B -->|prevents| D["incorrect dedup of shared workspace deps"]
  D -->|fixes| E["v11.5.1 release failure"]

Loading

Grey Divider

File Changes

1. pacquet/crates/config/src/lib.rs 🐞 Bug fix +5/-5

Change dedupe_direct_deps default to false

• Changed dedupe_direct_deps default from true to false
• Updated documentation to reference pnpm's config-reader default
• Updated linker call site reference to correct pnpm source location

pacquet/crates/config/src/lib.rs


2. pacquet/crates/cli/tests/dedupe_direct_deps.rs 🧪 Tests +142/-12

Update tests for dedupe_direct_deps default change

• Updated module documentation to clarify dedupeDirectDeps is off by default
• Modified all existing dedupe-on tests to explicitly set dedupeDirectDeps: true in workspace yaml
• Renamed test dedupes_direct_deps_against_workspace_root_by_default to remove "by_default"
• Added new regression test dedupe_off_by_default_keeps_shared_workspace_link that reproduces
 v11.5.1 release scenario with pnpm-written frozen lockfile

pacquet/crates/cli/tests/dedupe_direct_deps.rs


Grey Divider

Qodo Logo

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Aligns pacquet’s dedupeDirectDeps default with pnpm’s config-reader default (false) to prevent missing per-importer workspace symlinks during frozen installs (notably impacting the release workflow that runs pnpm publish after a pacquet frozen install).

Changes:

  • Change pacquet Config::dedupe_direct_deps default from true to false and update the inline documentation to reference pnpm’s default.
  • Update existing dedupe-on integration tests to explicitly set dedupeDirectDeps: true in pnpm-workspace.yaml.
  • Add a regression test asserting that the default-off behavior preserves a non-root importer’s workspace symlink when the root shares the same workspace dependency (frozen-lockfile replay).

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
pacquet/crates/config/src/lib.rs Changes dedupe_direct_deps default to false and updates doc links to pnpm’s default/call site.
pacquet/crates/cli/tests/dedupe_direct_deps.rs Makes dedupe-on tests set dedupeDirectDeps: true explicitly and adds a regression test for default-off behavior under frozen install.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@github-actions

github-actions Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Micro-Benchmark Results

Linux

group                          main                                   pr
-----                          ----                                   --
tarball/download_dependency    1.00      7.6±0.11ms   573.4 KB/sec    1.02      7.7±0.44ms   561.3 KB/sec

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
pacquet/crates/cli/tests/dedupe_direct_deps.rs (1)

231-350: 💤 Low value

Consider adding a log before the second assertion.

The test correctly validates the default-off behavior and reproduces the release scenario. However, the assertion at line 343 lacks a preceding log statement. While the variable is derived from the already-logged app_link, adding an explicit log would improve debuggability.

📋 Suggested logging addition
     );
+    eprintln!("app_manifest={app_manifest:?}");
     assert!(
         app_manifest.exists(),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pacquet/crates/cli/tests/dedupe_direct_deps.rs` around lines 231 - 350, The
second assertion that checks app_manifest.exists() lacks a preceding log; add a
short diagnostic print (e.g. eprintln!) just before the assertion to show the
app_manifest path and whether it exists (use the existing app_manifest and
app_link variables) so test failures include that context; update the test
function dedupe_off_by_default_keeps_shared_workspace_link to emit this log
immediately before the assert!(app_manifest.exists(), ...) line.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@pacquet/crates/cli/tests/dedupe_direct_deps.rs`:
- Around line 231-350: The second assertion that checks app_manifest.exists()
lacks a preceding log; add a short diagnostic print (e.g. eprintln!) just before
the assertion to show the app_manifest path and whether it exists (use the
existing app_manifest and app_link variables) so test failures include that
context; update the test function
dedupe_off_by_default_keeps_shared_workspace_link to emit this log immediately
before the assert!(app_manifest.exists(), ...) line.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 11994c51-ba3f-4bf2-964d-d70553a546aa

📥 Commits

Reviewing files that changed from the base of the PR and between 0f509d0 and f17e0de.

📒 Files selected for processing (2)
  • pacquet/crates/cli/tests/dedupe_direct_deps.rs
  • pacquet/crates/config/src/lib.rs
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: Agent
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Code Coverage
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Lint and Test (windows-latest)
  • GitHub Check: Doc
  • GitHub Check: Dylint
  • GitHub Check: Lint and Test (ubuntu-latest)
  • GitHub Check: Lint and Test (macos-latest)
  • GitHub Check: Analyze (javascript)
  • GitHub Check: Compile & Lint
🧰 Additional context used
📓 Path-based instructions (2)
pacquet/**/*.rs

📄 CodeRabbit inference engine (pacquet/AGENTS.md)

pacquet/**/*.rs: Log emissions are part of matching pnpm — when porting a function that fires pnpm:<channel> events through globalLogger, logger.debug(...), or streamParser.write(...), mirror the call site, payload, and ordering so @pnpm/cli.default-reporter parses pacquet's NDJSON the same way
Declare a newtype wrapper for branded string types instead of collapsing the brand into a plain String or &str in Rust
If upstream TypeScript always validates before construction of a branded string, validate in the Rust wrapper too via TryFrom<String> and/or FromStr and do not provide an infallible public constructor
If upstream TypeScript never validates a branded string, just brand for type-safety in Rust by exposing an infallible From<String> constructor
If upstream TypeScript occasionally constructs a branded string without validation, expose from_str_unchecked in Rust as an escape hatch alongside the validating constructor
Match upstream serde behavior for branded strings crossing JSON, YAML, or INI boundaries by using #[serde(try_from = "String")] for deserialization and #[serde(into = "String")] for serialization
Derive simple conversions for branded strings using #[derive(derive_more::From)] and #[derive(derive_more::Into)] instead of handwriting impl blocks; use manual impl only when conversion needs custom logic
Model TypeScript string literal unions (like 'auto' | 'always' | 'never') as Rust enums instead of newtype wrappers, since the set of valid values is closed
Treat TypeScript string template literal types (like `${string}@${string}`) the same as branded string types in Rust, using a newtype wrapper with validation
Follow the code style guide in CODE_STYLE_GUIDE.md — imports, modules, naming, ownership and borrowing, parameter type selection, trait bounds, pattern matching, pipe-trait, error handling, test layout, and cloning of Arc and Rc
Choose owned vs. borrowed parameters to minimize copies; widen to t...

Files:

  • pacquet/crates/config/src/lib.rs
  • pacquet/crates/cli/tests/dedupe_direct_deps.rs
pacquet/**/tests/**/*.rs

📄 CodeRabbit inference engine (pacquet/AGENTS.md)

pacquet/**/tests/**/*.rs: Prefer real fixtures over dependency-injection seams — use tempfile::TempDir, the mocked registry, or integration tests spawning the actual binary for happy paths and most error paths; use the DI seam only for filesystem error kinds, deterministic time, shared process-global state, or external-service happy paths
Follow test-logging guidance — log before non-assert_eq! assertions, dbg! complex structures, skip logging for simple scalar assert_eq!
Tests must not be tolerant of missing build/runtime environment by silently returning early when a tool isn't found — let the test panic when a required tool is absent
Prefer #[cfg_attr(target_os = "windows", ignore = "...")] (or matching #[cfg(unix)] gates) over runtime probe-and-skip helpers for platform-locked tools
Port relevant pnpm tests to Rust tests whenever they translate when porting behavior from pnpm
Use snapshot tests with insta and carefully review diffs when intentional changes alter snapshots; accept with cargo insta review only after careful review
Tests that need the mocked registry should start pnpr through pacquet-testing-utils; cargo test / cargo nextest run should not require a separate just registry-mock launch step

Files:

  • pacquet/crates/cli/tests/dedupe_direct_deps.rs
🧠 Learnings (3)
📚 Learning: 2026-05-20T19:40:55.051Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11774
File: pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs:0-0
Timestamp: 2026-05-20T19:40:55.051Z
Learning: In the pacquet Rust code, ensure the semver implementation uses the `node-semver` crate (not `nodejs-semver`). `node-semver`’s public API does not include a `satisfies_with_prerelease`-style method; prerelease-tolerant matching should be implemented inline by first calling `Range::satisfies`, and when it rejects a prerelease version, retry matching against a stripped `MAJOR.MINOR.PATCH` base of the prerelease version.

Applied to files:

  • pacquet/crates/config/src/lib.rs
  • pacquet/crates/cli/tests/dedupe_direct_deps.rs
📚 Learning: 2026-05-22T00:08:44.646Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11837
File: pacquet/crates/resolving-npm-resolver/src/pick_package.rs:33-51
Timestamp: 2026-05-22T00:08:44.646Z
Learning: In the pnpm/pnpm repo’s pacquet Rust crates, do not flag Unicode ellipsis characters (U+2026, `…`) in Rust doc comments (`///` / `/** */`) as a lint violation. The pacquet crate’s `dylint.toml` only enables `perfectionist::derive_ordering`, and the Dylint `unicode-ellipsis` rule is not enabled for this project—so `…` in doc comments is an intentional, repo-consistent style.

Applied to files:

  • pacquet/crates/config/src/lib.rs
  • pacquet/crates/cli/tests/dedupe_direct_deps.rs
📚 Learning: 2026-05-20T23:07:58.444Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11784
File: pacquet/crates/resolving-deps-resolver/src/hoist_peers.rs:120-133
Timestamp: 2026-05-20T23:07:58.444Z
Learning: When reviewing code in this pacquet Rust port, follow the upstream pnpm compatibility rule: only match pnpm’s behavior exactly. Do not propose review changes that intentionally deviate from pnpm’s documented/observed behavior, even if pnpm appears buggy. If you identify a real bug in pnpm behavior, the review should prioritize fixing it upstream in pnpm first, and avoid implementing a pnpm-behavior workaround here unless the same fix has already landed upstream.

Applied to files:

  • pacquet/crates/config/src/lib.rs
  • pacquet/crates/cli/tests/dedupe_direct_deps.rs
🔇 Additional comments (5)
pacquet/crates/config/src/lib.rs (1)

728-733: LGTM!

pacquet/crates/cli/tests/dedupe_direct_deps.rs (4)

1-18: LGTM!


30-34: LGTM!

Also applies to: 57-57


184-184: LGTM!


394-394: LGTM!

Also applies to: 462-462, 543-543, 567-567, 638-638

@codecov-commenter

codecov-commenter commented Jun 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.14563% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.50%. Comparing base (eec417b) to head (d3c5f33).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
pacquet/crates/config/src/pnpm_default_parity.rs 94.87% 10 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #12128      +/-   ##
==========================================
+ Coverage   87.45%   87.50%   +0.05%     
==========================================
  Files         260      261       +1     
  Lines       29376    29582     +206     
==========================================
+ Hits        25690    25886     +196     
- Misses       3686     3696      +10     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

A non-injected workspace dependency (`workspace:*`, `link:<path>`)
resolves to a `link:<path>` whose path is computed relative to the
*consuming importer's* directory. pacquet's workspace-wide
`resolved_by_wanted` cache keyed only `(alias, specifier, optional,
injected, pick_lowest, published_by)`, so the first importer to
resolve a shared workspace dep seeded the cache with its own relative
path and every other importer reused it verbatim.

With a root and a `packages/app` both depending on `@scope/lib`, the
root resolved `link:packages/lib` and `packages/app` got that same
string back — a dangling `packages/app/packages/lib`. pnpm writes
`link:../lib` for `packages/app`.

Add the consuming importer's `project_dir` to the cache key for
project-relative specifiers (`link:` / `file:` / `workspace:`) only;
registry specifiers stay importer-independent and keep sharing one
cache slot across importers, preserving the cross-importer dedup.
@zkochan zkochan changed the title fix(pacquet): default dedupeDirectDeps to false to match pnpm fix(pacquet): dedupeDirectDeps default + per-importer workspace link resolution Jun 2, 2026
@github-actions

github-actions Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Integrated-Benchmark Report (Linux)

Scenario: Isolated linker: fresh restore, cold cache + cold store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.056 ± 0.060 1.966 2.140 1.00
pacquet@main 2.064 ± 0.111 1.970 2.319 1.00 ± 0.06
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.05597792568,
      "stddev": 0.05966570853653516,
      "median": 2.04471446928,
      "user": 2.5805680399999997,
      "system": 3.2996187399999997,
      "min": 1.96576046128,
      "max": 2.13956809528,
      "times": [
        2.0103994482800003,
        2.13956809528,
        2.06192212028,
        1.96576046128,
        2.02750681828,
        2.01174244628,
        2.11933733728,
        2.13664733428,
        2.06791557928,
        2.0189796162800002
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 2.06366964378,
      "stddev": 0.11101341751138684,
      "median": 2.0316759062800003,
      "user": 2.59176954,
      "system": 3.3007187399999998,
      "min": 1.97002900628,
      "max": 2.3193753362800003,
      "times": [
        1.97666972128,
        2.13483024628,
        1.9719016992799998,
        2.07003251928,
        2.0823270002800003,
        1.9872439472799999,
        2.1309676682800003,
        1.9933192932800001,
        1.97002900628,
        2.3193753362800003
      ]
    }
  ]
}

Scenario: Isolated linker: fresh restore, hot cache + hot store

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 638.9 ± 23.4 626.0 704.2 1.00
pacquet@main 640.4 ± 20.0 618.2 693.0 1.00 ± 0.05
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.63893473266,
      "stddev": 0.023438081067033347,
      "median": 0.6314090986600001,
      "user": 0.35042303999999996,
      "system": 1.2992657199999997,
      "min": 0.62601674716,
      "max": 0.70420367216,
      "times": [
        0.70420367216,
        0.63065367616,
        0.6290457431600001,
        0.64357521716,
        0.62601674716,
        0.63385914216,
        0.62681646216,
        0.63168314516,
        0.63113505216,
        0.63235846916
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.6404123232600001,
      "stddev": 0.020000701237503235,
      "median": 0.63657513516,
      "user": 0.33946174,
      "system": 1.3147367199999997,
      "min": 0.61823322716,
      "max": 0.6929929031600001,
      "times": [
        0.6929929031600001,
        0.63298222016,
        0.64069221816,
        0.6273808481600001,
        0.63845698616,
        0.63469328416,
        0.63264679516,
        0.64502982716,
        0.61823322716,
        0.64101492316
      ]
    }
  ]
}

Scenario: Isolated linker: fresh install, cold cache + cold store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.294 ± 0.059 2.235 2.452 1.00 ± 0.03
pacquet@main 2.282 ± 0.024 2.233 2.328 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.2936714719599998,
      "stddev": 0.058672964949918145,
      "median": 2.28108554856,
      "user": 3.7257128,
      "system": 3.07950932,
      "min": 2.2345697965599998,
      "max": 2.45240063656,
      "times": [
        2.30062643156,
        2.45240063656,
        2.27400651656,
        2.28154528856,
        2.29076333056,
        2.28062580856,
        2.26486780256,
        2.2345697965599998,
        2.26740164956,
        2.28990745856
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 2.2824490902600005,
      "stddev": 0.023521177978152574,
      "median": 2.28341052306,
      "user": 3.7434642000000005,
      "system": 3.0843857200000007,
      "min": 2.23305065656,
      "max": 2.32767273956,
      "times": [
        2.29026134656,
        2.28255028856,
        2.29097568756,
        2.23305065656,
        2.26402820056,
        2.28415969556,
        2.32767273956,
        2.2824647965600002,
        2.28266135056,
        2.28666614056
      ]
    }
  ]
}

Scenario: Isolated linker: fresh install, hot cache + hot store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 1.459 ± 0.018 1.432 1.477 1.00
pacquet@main 1.466 ± 0.017 1.440 1.494 1.00 ± 0.02
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 1.4594998276200002,
      "stddev": 0.017689562748462864,
      "median": 1.4672491924200002,
      "user": 1.65091514,
      "system": 1.8192971400000002,
      "min": 1.43221257242,
      "max": 1.4765537804200002,
      "times": [
        1.46720033842,
        1.46729804642,
        1.43672809242,
        1.46977090542,
        1.4765537804200002,
        1.4764674474200001,
        1.4564840404200001,
        1.4750775444200002,
        1.43221257242,
        1.4372055084200002
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 1.4664844893200002,
      "stddev": 0.017031018165125325,
      "median": 1.4705056329200001,
      "user": 1.6594252399999998,
      "system": 1.80653854,
      "min": 1.4398853254200001,
      "max": 1.49383030942,
      "times": [
        1.4499624934200002,
        1.4705447964200002,
        1.4475658904200002,
        1.46136526842,
        1.49383030942,
        1.48635534842,
        1.47265382142,
        1.4398853254200001,
        1.47046646942,
        1.4722151704200002
      ]
    }
  ]
}

@github-actions

github-actions Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12128
Testbedpacquet
Click to view all benchmark results
BenchmarkLatencyBenchmark Result
milliseconds (ms)
(Result Δ%)
Upper Boundary
milliseconds (ms)
(Limit %)
isolated-linker.fresh-install.cold-cache.cold-store📈 view plot
🚷 view threshold
2,293.67 ms
(-1.94%)Baseline: 2,339.10 ms
2,806.92 ms
(81.71%)
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
🚷 view threshold
1,459.50 ms
(-3.34%)Baseline: 1,509.91 ms
1,811.89 ms
(80.55%)
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
🚷 view threshold
2,055.98 ms
(-0.57%)Baseline: 2,067.80 ms
2,481.35 ms
(82.86%)
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
🚷 view threshold
638.93 ms
(-2.88%)Baseline: 657.90 ms
789.48 ms
(80.93%)
🐰 View full continuous benchmarking report in Bencher

A contract test reads pnpm's `defaultOptions` literal from
config/reader/src/index.ts at test time and asserts every pacquet
default that maps to a pnpm setting equals pnpm's value. Drift on
either side fails here — the `dedupeDirectDeps` default that broke the
v11.5.1 release would have been caught.

`every_pnpm_default_is_classified` partitions all 80 pnpm settings into
mapped / non-literal (env, platform, CPU) / not-ported, so a new pnpm
setting that's neither mapped nor skipped fails the test until someone
classifies it.
Copilot AI review requested due to automatic review settings June 2, 2026 07:35
@zkochan zkochan changed the title fix(pacquet): dedupeDirectDeps default + per-importer workspace link resolution fix(pacquet): dedupeDirectDeps default + per-importer workspace link resolution, with defaults contract test Jun 2, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.

@zkochan zkochan merged commit ba2bacd into main Jun 2, 2026
28 of 29 checks passed
@zkochan zkochan deleted the fix/pacquet-dedupe-direct-deps-default branch June 2, 2026 07:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants