Skip to content

fix: dedupe workspace deps with children in single-project mode#11448

Merged
zkochan merged 6 commits into
pnpm:mainfrom
DASPRiD:fix/dedupe-injected-deps-with-children
Jun 17, 2026
Merged

fix: dedupe workspace deps with children in single-project mode#11448
zkochan merged 6 commits into
pnpm:mainfrom
DASPRiD:fix/dedupe-injected-deps-with-children

Conversation

@DASPRiD

@DASPRiD DASPRiD commented May 4, 2026

Copy link
Copy Markdown
Contributor

Issue

When injectWorkspacePackages: true is set and a workspace package depends on another workspace package that has its own dependencies, running pnpm rm from inside the dependent package's directory switches the lockfile protocol from link: to file:.

Reproduction (workspace where a depends on workspace b, and b has any dependency of its own):

cd packages/a
pnpm add redis
pnpm rm redis
# pnpm-lock.yaml: a's "b" entry switched from link:../b to file:packages/b

Root Cause

The fix in #10575 added a defensive guard in dedupeInjectedDeps that skipped deduplication whenever the target workspace project's children weren't in dependenciesByProjectId:

if (!targetProjectDeps) {
  if (children.length > 0) continue
}

In single-project operations (mutateModulesInSingleProject, used by pnpm rm from inside a package directory) only the operated-on project is resolved. dependenciesByProjectId then only has that one project, so the guard fires for any workspace dependency whose target has children, and the protocol stays file:.

Solution

In single-project mode the injected dep is resolved against the same workspace package source, so dedupe is safe — except for peer-suffixed depPaths, whose resolution depends on the importer's peer context (a plain link: would lose it). The new code dedupes whenever targetProjectDeps is missing for a known workspace project and the depPath has no peer suffix. The peer-suffix check compares the depPath against its peer-free pkgIdWithPatchHash (depPaths are built as ${pkgIdWithPatchHash}${peerDepGraphHash}), so it's exact rather than a (-substring heuristic.

Test

Two tests were added alongside the existing one in injectWorkspacePackagesPersistence.test.ts: one where workspace package b depends on is-negative, run through mutateModulesInSingleProject with mutation: 'uninstallSome' to reproduce the pnpm rm path (fails on main, passes here); and one where b has a peer dependency, so the injected entry resolves to file:b(is-positive@1.0.0) and must stay in that peer-suffixed form rather than collapsing to link:.

On the pacquet side, a behavioral CLI test (pacquet/crates/cli/tests/dedupe_injected_deps.rs) covers an injected workspace dep that has its own dependency being deduped to link: and surviving a remove.

Summary by CodeRabbit

  • Bug Fixes

    • Preserve link: workspace protocol when removing workspace packages with injectWorkspacePackages enabled, preventing link:file: switching—also when the affected workspace dependency has its own dependencies.
  • Tests

    • Added/extended Jest coverage for single-project removal to confirm the lockfile keeps link: resolutions.
    • Added an end-to-end CLI test validating the same behavior after installing and removing in a multi-package workspace.

🤖 Description updated by Claude (Opus 4.8) via Claude Code.

@DASPRiD DASPRiD requested a review from zkochan as a code owner May 4, 2026 15:02
Copilot AI review requested due to automatic review settings May 4, 2026 15:02
@welcome

welcome Bot commented May 4, 2026

Copy link
Copy Markdown

💖 Thanks for opening this pull request! 💖
Please be patient and we will get back to you as soon as we can.

@coderabbitai

coderabbitai Bot commented May 4, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR extends a prior deduplication fix for injected workspace packages: when a workspace dependency has its own dependencies and a single-project pnpm rm runs with injectWorkspacePackages: true, the dependency now correctly preserves the link: protocol instead of incorrectly switching to file:. The change updates core deduplication logic, adds Node.js and Pacquet integration tests, and documents the fix in a changeset.

Changes

Workspace Dependency Deduplication with Children

Layer / File(s) Summary
Deduplication logic fix
installing/deps-resolver/src/dedupeInjectedDeps.ts
getDedupeMap now reads targetProjectDeps up-front before subset checks; when the target project entry is missing, it dedupes based on node.pkgIdWithPatchHash comparison and skips subset evaluation; when present, child depPaths are compared directly against targetProjectDeps.get(alias) without optional chaining.
Node.js behavioral test
installing/deps-installer/test/install/injectWorkspacePackagesPersistence.test.ts
New Jest test simulates workspace package aworkspace:* b where b also has its own dependency, performs initial install with injectWorkspacePackages: true, executes single-project pnpm rm-like mutation via mutateModulesInSingleProject, and asserts a's dependency on b remains link:../b instead of switching to file:.
Pacquet integration test
pacquet/crates/cli/tests/dedupe_injected_deps.rs
New test sets up two-package workspace with injected dependency b having its own child, validates link:../b deduping after install (not file:packages/b), invokes pacquet remove for an unrelated root dependency to trigger workspace re-resolution, and asserts b remains link:../b and does not revert to file:. Expands std::fs import to include Command for process invocation.
Release note
.changeset/dedupe-injected-deps-with-children.md
Patch-level changeset for @pnpm/installing.deps-resolver and pnpm documents the fix for correct link: protocol behavior after pnpm rm with injectWorkspacePackages: true when the target workspace dependency has its own dependencies.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

product: pacquet

Suggested reviewers

  • zkochan
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title follows the Conventional Commits specification with the 'fix:' prefix and clearly describes the main change: deduplicating workspace dependencies with children in single-project mode.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

Review ran into problems

🔥 Problems

Stopped waiting for pipeline failures after 30000ms. One of your pipelines takes longer than our 30000ms fetch window to run, so review may not consider pipeline-failure results for inline comments if any failures occurred after the fetch window. Increase the timeout if you want to wait longer or run a @coderabbit review after the pipeline has finished.


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.

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

Fixes a regression where, with injectWorkspacePackages: true, single-project operations (notably pnpm rm run from inside a workspace package) can cause workspace dependency entries to flip from link: to file: in pnpm-lock.yaml when the target workspace package has its own dependencies.

Changes:

  • Adjust dedupeInjectedDeps to dedupe injected workspace deps even when the target workspace project wasn’t part of the current resolution (dependenciesByProjectId missing).
  • Add a regression test that reproduces the single-project uninstall path when the target workspace package has its own dependencies.
  • Add a changeset for patch releases.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
installing/deps-resolver/src/dedupeInjectedDeps.ts Changes dedupe behavior when the target workspace project isn’t in dependenciesByProjectId, aiming to preserve link: protocol in single-project operations.
installing/deps-installer/test/install/injectWorkspacePackagesPersistence.test.ts Adds a regression test for pnpm rm-equivalent single-project mutation when the target workspace dep has children.
.changeset/dedupe-injected-deps-with-children.md Declares patch releases and documents the behavioral fix.

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

Comment thread installing/deps-resolver/src/dedupeInjectedDeps.ts Outdated
DASPRiD added a commit to DASPRiD/pnpm that referenced this pull request May 4, 2026
The previous fast-path unconditionally deduped any injected workspace
dep when the target project wasn't part of the resolution. For
peer-resolved injected deps (depPaths containing `(`), the resolution
depends on the importer's peer context, and a plain `link:` would lose
that. Skip dedupe whenever the depPath has a peer suffix.

Adds a third test case that exercises the peer-resolved single-project
rm path. Spotted by Copilot review on pnpm#11448.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@DASPRiD

DASPRiD commented May 4, 2026

Copy link
Copy Markdown
Contributor Author

Addressing @copilot's review feedback (#11448 (comment)):

The fast-path now skips dedupe when dep.depPath contains ( (peer-suffixed). For peer-resolved injected workspace deps the resolution depends on the importer's peer context, and a plain link: would lose that.

Added a third test in injectWorkspacePackagesPersistence.test.ts covering the peer-resolved single-project rm path: b declares peerDependencies: { 'is-positive': '>=1.0.0' }, a provides is-positive@1.0.0, the initial install gives file:b(is-positive@1.0.0), and mutateModulesInSingleProject with mutation: 'uninstallSome' must preserve that exact form. The new test fails without the peer-suffix check (gets link:../b) and passes with it.

Copilot AI review requested due to automatic review settings May 4, 2026 15:46
@zkochan zkochan force-pushed the fix/dedupe-injected-deps-with-children branch from bd8600c to e91f3c3 Compare June 17, 2026 07:59
zkochan pushed a commit to DASPRiD/pnpm that referenced this pull request Jun 17, 2026
The previous fast-path unconditionally deduped any injected workspace
dep when the target project wasn't part of the resolution. For
peer-resolved injected deps (depPaths containing `(`), the resolution
depends on the importer's peer context, and a plain `link:` would lose
that. Skip dedupe whenever the depPath has a peer suffix.

Adds a third test case that exercises the peer-resolved single-project
rm path. Spotted by Copilot review on pnpm#11448.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 17, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Peer suffix heuristic wrong ✓ Resolved 🐞 Bug ≡ Correctness
Description
dedupeInjectedDeps treats any depPath containing '(' as peer-suffixed and skips dedupe, but non-peer
file:/workspace depPaths can contain parentheses when the underlying directory path contains them.
This can incorrectly skip deduplication and reintroduce link↔file protocol churn for workspaces
located under paths with parentheses.
Code

installing/deps-resolver/src/dedupeInjectedDeps.ts[R64-75]

const targetProjectDeps = opts.dependenciesByProjectId[dep.id]
-      // When the target project wasn't part of the current resolution (e.g. single-project
-      // operation), its dependencies aren't available. We can only deduplicate safely when the
-      // injected dep has no children (the empty set is always a subset).
+      // In single-project operations (e.g. `pnpm rm` from inside a workspace package) the target
+      // workspace project isn't being resolved, so its children aren't in
+      // `dependenciesByProjectId`. The injected dep was resolved against the same workspace
+      // package source, so dedupe is safe. The exception is peer-suffixed depPaths
+      // (containing `(`), whose resolution depends on the importer's peer context. A plain
+      // `link:` would lose that, so we skip dedupe for those.
if (!targetProjectDeps) {
-        if (children.length > 0) continue
+        if (!dep.depPath.includes('(')) {
+          dedupedInjectedDeps.set(alias, dep.id)
+        }
+        continue
Evidence
The PR’s new logic skips dedupe based on a raw substring check for (. But pnpm’s own depPath
suffix parser only treats parentheses as a peer/patch suffix when the depPath ends with ) and the
suffix is at the end; additionally, local file: ids are derived from normalized filesystem paths
(which may contain parentheses), so non-peer depPaths can legitimately include (.

installing/deps-resolver/src/dedupeInjectedDeps.ts[64-75]
deps/path/src/index.ts[9-30]
resolving/local-resolver/src/parseBareSpecifier.ts[75-127]
installing/deps-resolver/src/resolveDependencies.ts[1855-1860]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`dedupeInjectedDeps` currently uses `dep.depPath.includes('(')` to detect peer-suffixed depPaths and skip dedupe. This is over-broad because `(` may appear in *non-peer* local depPaths due to the actual directory name (e.g. `packages/b(foo)`), whereas pnpm’s peer suffix is a structured `(...)` suffix at the end of the depPath.
### Issue Context
Peer suffix detection already exists in `@pnpm/deps.path` via `indexOfDepPathSuffix` / `parseDepPath`, which only treat parentheses as a peer suffix when they form the trailing suffix (depPath ends with `)`). Local `file:` ids preserve the normalized path, so parentheses in folder names are legal and should not be treated as peer context.
### Fix Focus Areas
- installing/deps-resolver/src/dedupeInjectedDeps.ts[64-75]
### Suggested change
Replace the heuristic `dep.depPath.includes('(')` with a real peer-suffix check, e.g.:
- `parseDepPath(dep.depPath).peerDepGraphHash !== ''` (skip dedupe only when peerDepGraphHash exists), or
- `indexOfDepPathSuffix(dep.depPath).peersIndex !== -1`
This keeps the intended behavior for real peer-suffixed depPaths while allowing dedupe for non-peer depPaths that merely contain parentheses in their path.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread installing/deps-resolver/src/dedupeInjectedDeps.ts
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 94234ff

@zkochan zkochan force-pushed the fix/dedupe-injected-deps-with-children branch from 94234ff to 924ccc3 Compare June 17, 2026 09:07
@zkochan zkochan enabled auto-merge (squash) June 17, 2026 09:15
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 924ccc3

@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.

Actionable comments posted: 1

🤖 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.

Inline comments:
In `@pacquet/crates/cli/tests/dedupe_injected_deps.rs`:
- Around line 89-188: The test
injected_workspace_dep_with_children_stays_link_after_remove currently verifies
that injected workspace dependencies stay as link:../b after remove, but it
doesn't cover the case where peer-suffixed depPaths (like file:b(peer@...))
should be preserved with their peer context intact. Extend the test to create a
scenario with peer-suffixed dependencies, then add assertions after the remove
operation to verify that peer-suffix data in dependency paths is maintained in
the pnpm-lock.yaml and hasn't been stripped or simplified, ensuring lockfile
identity integrity is preserved through the remove operation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 8a0bc522-837b-4620-993d-2cd202a3922b

📥 Commits

Reviewing files that changed from the base of the PR and between e91f3c3 and 924ccc3.

📒 Files selected for processing (2)
  • installing/deps-resolver/src/dedupeInjectedDeps.ts
  • pacquet/crates/cli/tests/dedupe_injected_deps.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • installing/deps-resolver/src/dedupeInjectedDeps.ts

Comment thread pacquet/crates/cli/tests/dedupe_injected_deps.rs
DASPRiD and others added 5 commits June 17, 2026 11:33
Removes the defensive guard added in pnpm#10575 that skipped deduplication
whenever the target workspace project's children weren't available in
the current resolution. In single-project operations the injected dep
is resolved against the same workspace package source, so dedupe is
safe and the link: protocol is preserved.

Adds a test case where the workspace dep target has its own children,
reproducing the bug the original PR was meant to fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous fast-path unconditionally deduped any injected workspace
dep when the target project wasn't part of the resolution. For
peer-resolved injected deps (depPaths containing `(`), the resolution
depends on the importer's peer context, and a plain `link:` would lose
that. Skip dedupe whenever the depPath has a peer suffix.

Adds a third test case that exercises the peer-resolved single-project
rm path. Spotted by Copilot review on pnpm#11448.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Without the explicit type, TypeScript inferred the workspacePackages
Map's value type from project a's manifest (which has dependencies),
making project b's manifest (which has peerDependencies but no
dependencies) fail to unify.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…jectedDeps

The single-project fast-path must skip dedupe only for peer-suffixed
depPaths. A raw `includes('(')` check misfires when a workspace
project's directory path itself contains a parenthesis. A depPath is
built as `${pkgIdWithPatchHash}${peerDepGraphHash}`, so it carries a
peer suffix exactly when it differs from its peer-free
`pkgIdWithPatchHash` — an exact comparison of already-resolved data,
with no string heuristic.
…emove

Pacquet's dedupeInjectedDeps had no end-to-end coverage for an injected
workspace dep that has its own dependencies (the children-subset path);
the existing test only covered the childless case. Add a CLI test with
injectWorkspacePackages and a workspace dep that depends on a registry
package, asserting it is deduped to link: on install and stays link:
after a remove re-resolves the workspace.
@codecov-commenter

codecov-commenter commented Jun 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 88.13%. Comparing base (3f0fb21) to head (24cb56e).
⚠️ Report is 7 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main   #11448   +/-   ##
=======================================
  Coverage   88.13%   88.13%           
=======================================
  Files         310      310           
  Lines       41864    41861    -3     
=======================================
- Hits        36896    36894    -2     
+ Misses       4968     4967    -1     

☔ View full report in Codecov by Harness.
📢 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.

@zkochan zkochan force-pushed the fix/dedupe-injected-deps-with-children branch from 924ccc3 to 81fda9d Compare June 17, 2026 09:44
…emove

The existing pacquet dedupe coverage asserted that a childless and a
with-children injected workspace dep collapse to link: and stay link:
after a remove, but nothing pinned the peer-suffixed case. A
peer-resolved injected dep's identity depends on the importer's peer
context, so it must stay in its `file:packages/b(peer@ver)` form rather
than collapse to a plain `link:../b` that would strip the peer suffix.

Add a CLI test where workspace dep `b` declares a peer dependency that
`a` provides, asserting the peer-suffixed file: entry is produced on
install and preserved byte-for-byte through a remove that re-resolves
the whole workspace. Mirrors the TS peer-suffixed single-project rm
test on the pacquet side.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code Review by Qodo

Grey Divider

New Review Started

This review has been superseded by a new analysis

Grey Divider

Qodo Logo

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

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 24cb56e

@github-actions

Copy link
Copy Markdown
Contributor

Integrated-Benchmark Report (Linux)

Each scenario reports direct installs and pnpr installs. Bencher consumes pacquet@HEAD and pnpr@HEAD.

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 4.169 ± 0.159 4.011 4.412 2.03 ± 0.13
pacquet@main 4.121 ± 0.130 3.944 4.340 2.01 ± 0.12
pnpr@HEAD 2.053 ± 0.088 1.955 2.269 1.00 ± 0.07
pnpr@main 2.050 ± 0.104 1.955 2.305 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 4.16922529872,
      "stddev": 0.1585788386673922,
      "median": 4.08543503532,
      "user": 3.8952464399999998,
      "system": 3.4723049399999995,
      "min": 4.01063320232,
      "max": 4.4123248333200005,
      "times": [
        4.4123248333200005,
        4.10455205532,
        4.06631801532,
        4.38151114332,
        4.26574094732,
        4.0569983373200005,
        4.01063320232,
        4.05874686932,
        4.32282401832,
        4.01260356532
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 4.12118126052,
      "stddev": 0.12976790165426558,
      "median": 4.0755062653200005,
      "user": 3.8949780399999994,
      "system": 3.4674872399999996,
      "min": 3.94426177332,
      "max": 4.34006984432,
      "times": [
        3.94426177332,
        4.0661534253200005,
        4.34006984432,
        4.0848591053200005,
        4.2704918883200005,
        4.030954293320001,
        4.277190147320001,
        4.0355153473200005,
        4.04454114632,
        4.11777563432
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 2.05283350242,
      "stddev": 0.08813716442379484,
      "median": 2.02651524682,
      "user": 2.59722874,
      "system": 2.9938379399999997,
      "min": 1.95539420232,
      "max": 2.26865762832,
      "times": [
        2.04698003132,
        2.00329477832,
        2.03464321232,
        2.01838728132,
        2.26865762832,
        2.00475273932,
        2.00144684932,
        1.95539420232,
        2.11872492632,
        2.07605337532
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 2.0495164252199998,
      "stddev": 0.10375375113172854,
      "median": 2.01103503132,
      "user": 2.6006448399999997,
      "system": 2.9865364399999996,
      "min": 1.9553005083200001,
      "max": 2.30531309032,
      "times": [
        2.00732526432,
        2.00297510732,
        2.01987858832,
        2.05364503532,
        2.14998370232,
        2.30531309032,
        1.99759250732,
        1.98840565032,
        1.9553005083200001,
        2.01474479832
      ]
    }
  ]
}

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

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 648.8 ± 8.5 637.9 660.7 1.04 ± 0.02
pacquet@main 623.8 ± 5.6 617.1 633.0 1.00
pnpr@HEAD 774.3 ± 128.1 707.8 1136.0 1.24 ± 0.21
pnpr@main 703.1 ± 80.2 659.1 925.6 1.13 ± 0.13
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.6487752078,
      "stddev": 0.008463196998539431,
      "median": 0.6467481648,
      "user": 0.3829406799999999,
      "system": 1.3388325999999997,
      "min": 0.6378976748,
      "max": 0.6606972778,
      "times": [
        0.6488702298,
        0.6606972778,
        0.6378976748,
        0.6441537848000001,
        0.6438432628,
        0.6526780898,
        0.6446260998000001,
        0.6579102548,
        0.6379907898,
        0.6590846138
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.6237826773000001,
      "stddev": 0.00558478476149773,
      "median": 0.6218904043,
      "user": 0.38594098,
      "system": 1.3055861,
      "min": 0.6171006548,
      "max": 0.6329533088,
      "times": [
        0.6310264398000001,
        0.6212874208,
        0.6199556378000001,
        0.6171006548,
        0.6224933878000001,
        0.6180031278,
        0.6211597558,
        0.6240132888000001,
        0.6329533088,
        0.6298337508
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.7743046479,
      "stddev": 0.12809796955559816,
      "median": 0.7375015738,
      "user": 0.40837428,
      "system": 1.4058275,
      "min": 0.7078489868000001,
      "max": 1.1360276548,
      "times": [
        0.7578343868,
        0.7342737498,
        0.7374780158,
        0.7084254508000001,
        0.7375251318,
        0.7327143268,
        0.7527161848,
        0.7382025908000001,
        0.7078489868000001,
        1.1360276548
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.7031111992000001,
      "stddev": 0.08015111687005719,
      "median": 0.6766265078,
      "user": 0.39660828,
      "system": 1.3444207,
      "min": 0.6590566648,
      "max": 0.9255818148,
      "times": [
        0.6824575268,
        0.6590566648,
        0.7001152258000001,
        0.6836794318,
        0.6662554888000001,
        0.6707954888000001,
        0.6605641458,
        0.6680767078000001,
        0.9255818148,
        0.7145294968
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 4.202 ± 0.051 4.139 4.284 1.94 ± 0.11
pacquet@main 4.162 ± 0.020 4.133 4.190 1.92 ± 0.11
pnpr@HEAD 2.164 ± 0.120 2.009 2.402 1.00
pnpr@main 2.177 ± 0.136 2.019 2.435 1.01 ± 0.08
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 4.201751134660001,
      "stddev": 0.051003698701321314,
      "median": 4.19555348266,
      "user": 3.7192203400000006,
      "system": 3.3394490400000003,
      "min": 4.13947034566,
      "max": 4.28396231266,
      "times": [
        4.18242093266,
        4.28396231266,
        4.15446685066,
        4.198347296660001,
        4.25690924266,
        4.19799376966,
        4.26455673566,
        4.1931131956600005,
        4.14627066466,
        4.13947034566
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 4.162003422760001,
      "stddev": 0.020195745954781325,
      "median": 4.165145256660001,
      "user": 3.6977772399999993,
      "system": 3.3388716400000007,
      "min": 4.132862304660001,
      "max": 4.18959035666,
      "times": [
        4.1894916676600005,
        4.18959035666,
        4.164787915660001,
        4.166084151660001,
        4.14345908566,
        4.179106919660001,
        4.1480651256600005,
        4.132862304660001,
        4.165502597660001,
        4.141084102660001
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 2.16427511866,
      "stddev": 0.12034283881067602,
      "median": 2.16216224316,
      "user": 2.4428411399999996,
      "system": 2.88978134,
      "min": 2.00871753766,
      "max": 2.40156427366,
      "times": [
        2.01013626666,
        2.10751152266,
        2.20737855266,
        2.00871753766,
        2.16115252966,
        2.29780925066,
        2.17598081366,
        2.40156427366,
        2.16317195666,
        2.10932848266
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 2.1769394569600005,
      "stddev": 0.13607841267897341,
      "median": 2.15867335616,
      "user": 2.42456344,
      "system": 2.8954117399999992,
      "min": 2.01942481066,
      "max": 2.4349982466599998,
      "times": [
        2.16038896966,
        2.15695774266,
        2.20831308766,
        2.34511336866,
        2.12872992066,
        2.01942481066,
        2.0355832176599997,
        2.24060930466,
        2.03927590066,
        2.4349982466599998
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 1.333 ± 0.012 1.316 1.348 1.99 ± 0.04
pacquet@main 1.356 ± 0.084 1.310 1.592 2.03 ± 0.13
pnpr@HEAD 0.690 ± 0.076 0.657 0.906 1.03 ± 0.12
pnpr@main 0.668 ± 0.011 0.658 0.695 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 1.33281732632,
      "stddev": 0.011911888686680888,
      "median": 1.3376011600200002,
      "user": 1.35126634,
      "system": 1.7098449600000003,
      "min": 1.31613531402,
      "max": 1.3479376820200002,
      "times": [
        1.33807671702,
        1.3176415650200002,
        1.31613531402,
        1.3449487910200002,
        1.3230877840200002,
        1.3431606630200001,
        1.32198242702,
        1.3479376820200002,
        1.33730790202,
        1.33789441802
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 1.3561373836200001,
      "stddev": 0.08373907621769579,
      "median": 1.3304655855200003,
      "user": 1.36765554,
      "system": 1.6940052599999997,
      "min": 1.3101795100200002,
      "max": 1.5915342150200003,
      "times": [
        1.33822435602,
        1.3333040200200001,
        1.3273670400200002,
        1.5915342150200003,
        1.3417980790200001,
        1.3238615690200002,
        1.35438807402,
        1.3276271510200002,
        1.3130898220200002,
        1.3101795100200002
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.69023597932,
      "stddev": 0.07627880179080472,
      "median": 0.66638292702,
      "user": 0.34436624,
      "system": 1.3009123599999999,
      "min": 0.65724964202,
      "max": 0.90629389802,
      "times": [
        0.65724964202,
        0.66454248102,
        0.66822337302,
        0.90629389802,
        0.67880861602,
        0.65872195602,
        0.67586941402,
        0.67098589702,
        0.6574835840200001,
        0.6641809320200001
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.66829295362,
      "stddev": 0.011308884961379138,
      "median": 0.66528196552,
      "user": 0.34326133999999997,
      "system": 1.2893644599999998,
      "min": 0.65768845602,
      "max": 0.69464989202,
      "times": [
        0.66221055302,
        0.65768845602,
        0.6590763550200001,
        0.66835337802,
        0.69464989202,
        0.65922992302,
        0.67548969202,
        0.66883491502,
        0.67530786402,
        0.6620885080200001
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.972 ± 0.036 2.937 3.059 4.62 ± 0.07
pacquet@main 3.003 ± 0.060 2.961 3.168 4.67 ± 0.10
pnpr@HEAD 0.670 ± 0.009 0.654 0.681 1.04 ± 0.02
pnpr@main 0.643 ± 0.006 0.634 0.651 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.9723380720200003,
      "stddev": 0.03557807625757142,
      "median": 2.96081738672,
      "user": 1.80323778,
      "system": 1.93088406,
      "min": 2.93698002072,
      "max": 3.05896933572,
      "times": [
        2.97304185472,
        2.98695638972,
        2.94795190172,
        2.94918658772,
        2.9519726637200003,
        2.95772877972,
        2.99668719272,
        3.05896933572,
        2.96390599372,
        2.93698002072
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 3.00253723432,
      "stddev": 0.059992916939952366,
      "median": 2.98770363472,
      "user": 1.8091009799999997,
      "system": 1.9525032599999996,
      "min": 2.9609553337200003,
      "max": 3.16789950672,
      "times": [
        2.97506612472,
        2.9896739507200003,
        2.99582563572,
        2.9609553337200003,
        2.98573331872,
        2.97757353872,
        3.01239618172,
        2.96697114972,
        3.16789950672,
        2.99327760272
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.6702866164200001,
      "stddev": 0.00874681829646051,
      "median": 0.67330525722,
      "user": 0.34298928,
      "system": 1.30718116,
      "min": 0.65354951472,
      "max": 0.68127265272,
      "times": [
        0.68127265272,
        0.6750473757200001,
        0.6737447977200001,
        0.65354951472,
        0.6728657167200001,
        0.6743740907200001,
        0.6611898447200001,
        0.67959409372,
        0.66272366672,
        0.66850441072
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.64347197862,
      "stddev": 0.0055268283199200385,
      "median": 0.64352287972,
      "user": 0.33866107999999995,
      "system": 1.27763046,
      "min": 0.63374759772,
      "max": 0.6506434507200001,
      "times": [
        0.64867341972,
        0.64132057472,
        0.65053284272,
        0.64366699872,
        0.63374759772,
        0.6451665667200001,
        0.6381994027200001,
        0.6506434507200001,
        0.64337876072,
        0.63939017172
      ]
    }
  ]
}

@github-actions

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/11448
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
4,201.75 ms
(-0.12%)Baseline: 4,206.88 ms
5,048.25 ms
(83.23%)
isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
🚷 view threshold
2,972.34 ms
(-1.36%)Baseline: 3,013.43 ms
3,616.12 ms
(82.20%)
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
🚷 view threshold
1,332.82 ms
(+0.35%)Baseline: 1,328.19 ms
1,593.83 ms
(83.62%)
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
🚷 view threshold
4,169.23 ms
(+0.01%)Baseline: 4,168.86 ms
5,002.63 ms
(83.34%)
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
🚷 view threshold
648.78 ms
(+5.67%)Baseline: 613.94 ms
736.72 ms
(88.06%)
🐰 View full continuous benchmarking report in Bencher

@github-actions

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/11448
Testbedpnpr

⚠️ WARNING: No Threshold found!

Without a Threshold, no Alerts will ever be generated.

Click here to create a new Threshold
For more information, see the Threshold documentation.
To only post results if a Threshold exists, set the --ci-only-thresholds flag.

Click to view all benchmark results
BenchmarkLatencymilliseconds (ms)
isolated-linker.fresh-install.cold-cache.cold-store📈 view plot
⚠️ NO THRESHOLD
2,164.28 ms
isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
670.29 ms
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
690.24 ms
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
⚠️ NO THRESHOLD
2,052.83 ms
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
774.30 ms
🐰 View full continuous benchmarking report in Bencher

@zkochan zkochan disabled auto-merge June 17, 2026 11:36
@zkochan zkochan merged commit 96bdd57 into pnpm:main Jun 17, 2026
28 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants