Skip to content

fix: preserve workspace specs on update#12140

Merged
zkochan merged 2 commits into
pnpm:mainfrom
morning-verlu:codex/preserve-workspace-update-spec-fork
Jun 16, 2026
Merged

fix: preserve workspace specs on update#12140
zkochan merged 2 commits into
pnpm:mainfrom
morning-verlu:codex/preserve-workspace-update-spec-fork

Conversation

@morning-verlu

@morning-verlu morning-verlu commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

What

  • preserve existing workspace: dependency specifiers when updateProjectManifest saves updated direct dependencies and preserveWorkspaceProtocol is enabled
  • keep catalog specifiers taking precedence over resolver-normalized specs
  • add focused coverage for preserved and normalized local spec behavior
  • add a changeset for the published @pnpm/installing.deps-resolver change

pacquet parity

Ported the same fix to pacquet's update command. Previously pacquet update --latest routed every direct dependency through a registry latest lookup, so a workspace: local-path dependency (e.g. workspace:../packages/foo/dist) was rewritten into a registry version — corrupting the manifest (in the regression test it became 0.0.1-security). Both --latest rewrite sites now skip registry resolution for such specs via is_workspace_local_path_specifier, a faithful port of pnpm's isWorkspaceLocalPathSpecifier. The gate is unconditional in the --latest path because preserveWorkspaceProtocol is always on there (its only override derives from linkWorkspacePackages under --workspace, which cannot be combined with --latest).

Fixes #3902

Testing

pnpm side:

  • pnpm --dir installing/deps-resolver exec tsgo --build
  • env NODE_OPTIONS="--experimental-vm-modules --disable-warning=ExperimentalWarning --disable-warning=DEP0169" pnpm --dir installing/deps-resolver exec jest updateProjectManifest.test.ts
  • pnpm --dir installing/deps-resolver exec eslint src/updateProjectManifest.ts test/updateProjectManifest.test.ts

pacquet side:

  • cargo nextest run -p pacquet-package-manager workspace — helper unit tests
  • cargo nextest run -E 'test(update_latest_preserves_workspace_local_path_specifier)' — integration regression test (verified it fails without the guard: the manifest spec was rewritten to 0.0.1-security)
  • cargo clippy -p pacquet-package-manager -p pacquet-cli --all-targets -- --deny warnings
  • cargo fmt --check

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

Summary by CodeRabbit

  • Refactor

    • Improved dependency specifier selection by prioritizing catalog-provided specifiers and centralizing workspace: preservation vs normalization logic.
  • Bug Fixes

    • Fixed pnpm update / pacquet update --latest rewriting workspace: specifiers that point to local paths, keeping them as-is when appropriate.
  • Tests

    • Added Jest and Rust regression/unit tests covering workspace protocol edge cases (local-path detection, range forms, catalog precedence) and shared test fixtures.

@morning-verlu morning-verlu requested a review from zkochan as a code owner June 2, 2026 14:15
@welcome

welcome Bot commented Jun 2, 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 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: aec1234c-c9bb-4ebb-8e66-0f9f196f593c

📥 Commits

Reviewing files that changed from the base of the PR and between 79f83e8 and 1f58673.

📒 Files selected for processing (6)
  • .changeset/preserve-workspace-update-spec.md
  • installing/deps-resolver/src/updateProjectManifest.ts
  • installing/deps-resolver/test/updateProjectManifest.test.ts
  • pacquet/crates/cli/tests/update.rs
  • pacquet/crates/package-manager/src/update.rs
  • pacquet/crates/package-manager/src/update/tests.rs
✅ Files skipped from review due to trivial changes (1)
  • .changeset/preserve-workspace-update-spec.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • installing/deps-resolver/test/updateProjectManifest.test.ts
  • installing/deps-resolver/src/updateProjectManifest.ts

📝 Walkthrough

Walkthrough

Fixes workspace: local-path dependency specifiers being incorrectly rewritten during pnpm update and pacquet update --latest by extracting helpers that detect local paths and conditionally preserve the workspace protocol. Implements the fix in both pnpm TypeScript (installing-deps-resolver) and pacquet Rust (package-manager) with comprehensive test coverage and release documentation.

Changes

Workspace Protocol Preservation

Layer / File(s) Summary
pnpm TypeScript: helper extraction and integration
installing/deps-resolver/src/updateProjectManifest.ts
Replaces inline bareSpecifier fallback with getBareSpecifierToSave(...) that centralizes logic to prefer catalog lookups, conditionally preserve workspace: local-path specifiers when enabled, or fall back to normalized specifiers via new isWorkspaceLocalPathSpecifier(...) predicate.
pnpm TypeScript: test coverage and fixtures
installing/deps-resolver/test/updateProjectManifest.test.ts
Jest tests covering workspace protocol preservation vs. normalization, workspace:* normalization to versioned ranges, and catalog specifier precedence; includes helper factories (createImporter, createDirectDependency) for consistent test fixtures.
pacquet Rust: helper definition and unit tests
pacquet/crates/package-manager/src/update.rs, pacquet/crates/package-manager/src/update/tests.rs
Adds is_workspace_local_path_specifier(...) helper to detect workspace: specs pointing to local paths (relative, absolute, ~/, Windows drive letters); unit tests verify detection of local vs. range/alias patterns and non-workspace specs.
pacquet Rust: --latest update logic integration and regression test
pacquet/crates/package-manager/src/update.rs, pacquet/crates/cli/tests/update.rs
Conditionally skips registry latest-version fetching and manifest rewrites for workspace: local-path dependencies in both no-selector and selector-matched update paths; CLI regression test verifies workspace:./local-dep specifiers remain unchanged after update --latest.
Release documentation
.changeset/preserve-workspace-update-spec.md
Changeset entry marking patch releases for @pnpm/installing.deps-resolver and pnpm, documenting the fix for pnpm update incorrectly rewriting workspace: local-path dependencies instead of preserving them verbatim.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • pnpm/pnpm#12104: Both PRs modify pacquet/crates/package-manager/src/update.rs's --latest manifest rewrite logic; retrieved PR omits ignored deps from rewrites while main PR skips workspace local-path specs.

Suggested labels

product: pnpm, product: pacquet

Suggested reviewers

  • zkochan

Poem

🐇 I hop through specs to find what stays,
Workspace paths preserved through pnpm's ways.
Both TypeScript and Rust now dance the same,
Local-path protocols keep their name.
Manifest truth, at last, is mine! 🍃

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.44% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: preserving workspace protocol specifiers during dependency updates across both pnpm and pacquet, addressing issue #3902.
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 unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.


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

Preserve workspace protocol specs on manifest update

🐞 Bug fix

Grey Divider

Walkthroughs

Description
• Preserve workspace protocol specs when updating project manifest
• Extract bare specifier logic into dedicated function
• Prioritize catalog specifiers over resolver-normalized specs
• Add comprehensive test coverage for workspace spec preservation
Diagram
flowchart LR
  A["updateProjectManifest"] -- "calls" --> B["getBareSpecifierToSave"]
  B -- "checks catalog lookup" --> C["Return user specified spec"]
  B -- "checks workspace protocol" --> D["Return workspace spec"]
  B -- "fallback" --> E["Return normalized spec"]
  F["Test: preserveWorkspaceProtocol=true"] -- "expects" --> G["workspace: spec saved"]
  H["Test: preserveWorkspaceProtocol=false"] -- "expects" --> I["link: spec saved"]

Loading

Grey Divider

File Changes

1. installing/deps-resolver/src/updateProjectManifest.ts 🐞 Bug fix +15/-1

Extract and enhance bare specifier selection logic

• Extracted bare specifier selection logic into new getBareSpecifierToSave function
• Function prioritizes catalog lookup specs, then workspace protocol preservation, then normalized
 specs
• Replaced inline ternary operator with function call for better maintainability

installing/deps-resolver/src/updateProjectManifest.ts


2. installing/deps-resolver/test/updateProjectManifest.test.ts 🧪 Tests +67/-0

Add tests for workspace spec preservation behavior

• Added new test file with comprehensive coverage for workspace spec preservation
• Test verifies workspace protocol specs are preserved when preserveWorkspaceProtocol is true
• Test verifies normalized local specs are saved when preserveWorkspaceProtocol is false
• Includes helper functions to create test fixtures for importer and direct dependency

installing/deps-resolver/test/updateProjectManifest.test.ts


Grey Divider

Qodo Logo

@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)
installing/deps-resolver/test/updateProjectManifest.test.ts (1)

8-26: ⚡ Quick win

Add a catalog-precedence test case.

The new suite only covers the workspace-vs-link branches. getBareSpecifierToSave() also gives resolvedDep.catalogLookup.userSpecifiedBareSpecifier highest priority, and that behavior is part of this PR’s contract. A focused fixture with catalogLookup set would keep that branch from regressing if the precedence order changes later.

Also applies to: 53-66

🤖 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 `@installing/deps-resolver/test/updateProjectManifest.test.ts` around lines 8 -
26, Add a test to cover the catalog-precedence branch by exercising
getBareSpecifierToSave via updateProjectManifest: create a fixture/direct
dependency where the resolved dependency includes
resolvedDep.catalogLookup.userSpecifiedBareSpecifier (e.g., set on the object
returned by createDirectDependency or the resolvedDep mock used by
updateProjectManifest), call updateProjectManifest with
preserveWorkspaceProtocol true/false as appropriate, and assert that
manifest.dependencies.foo equals the userSpecifiedBareSpecifier value; this
ensures the catalogLookup.userSpecifiedBareSpecifier branch in
getBareSpecifierToSave is covered and won't regress.
🤖 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 `@installing/deps-resolver/test/updateProjectManifest.test.ts`:
- Around line 8-26: Add a test to cover the catalog-precedence branch by
exercising getBareSpecifierToSave via updateProjectManifest: create a
fixture/direct dependency where the resolved dependency includes
resolvedDep.catalogLookup.userSpecifiedBareSpecifier (e.g., set on the object
returned by createDirectDependency or the resolvedDep mock used by
updateProjectManifest), call updateProjectManifest with
preserveWorkspaceProtocol true/false as appropriate, and assert that
manifest.dependencies.foo equals the userSpecifiedBareSpecifier value; this
ensures the catalogLookup.userSpecifiedBareSpecifier branch in
getBareSpecifierToSave is covered and won't regress.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 30781d69-23ca-41e0-84ac-adec924e1430

📥 Commits

Reviewing files that changed from the base of the PR and between 6d17b66 and 198a987.

📒 Files selected for processing (2)
  • installing/deps-resolver/src/updateProjectManifest.ts
  • installing/deps-resolver/test/updateProjectManifest.test.ts
📜 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). (2)
  • GitHub Check: Compile & Lint
  • GitHub Check: Analyze (javascript)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Follow Standard Style with trailing commas, preferring functions over classes, and declaring functions after they are used (relying on hoisting)
Use a single options object instead of multiple parameters when a function needs more than two or three arguments
Follow Import Order: Standard libraries first, then external dependencies (alphabetically), then relative imports
Write self-documenting code where function names, parameters, and types explain what a function does without requiring prose comments
Do not write comments that restate what the code already says; refactor via renaming, splitting helpers, or restructuring instead
Do not repeat documentation at call sites that already exists in JSDoc on the callee; update JSDoc once for all call sites to benefit
Use JSDoc only for a function's contract (preconditions, postconditions, edge cases, why the function exists), not for re-narrating the body
Do not record past implementation shape, refactor history, or 'the previous code did X' framing in code; use git log and git blame instead
Write comments only when: the reason for code is non-obvious (hidden invariant, workaround for known bug, deliberate exception), or the right name doesn't fit (temporary technical constraint)

Files:

  • installing/deps-resolver/test/updateProjectManifest.test.ts
  • installing/deps-resolver/src/updateProjectManifest.ts
**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use util.types.isNativeError() instead of instanceof Error for error type checking in Jest tests

Files:

  • installing/deps-resolver/test/updateProjectManifest.test.ts
🧠 Learnings (1)
📚 Learning: 2026-05-14T09:04:00.133Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11622
File: resolving/npm-resolver/test/publishedBy.test.ts:350-354
Timestamp: 2026-05-14T09:04:00.133Z
Learning: In the pnpm/pnpm repository, ESLint is the authoritative style linter. Do not raise review findings for missing trailing commas in multiline function calls (e.g., `fs.writeFileSync(...)`) when this repo’s ESLint configuration does not report them and lint passes. Prefer deferring to the ESLint results for this specific trailing-comma rule rather than enforcing it manually in code review.

Applied to files:

  • installing/deps-resolver/test/updateProjectManifest.test.ts
  • installing/deps-resolver/src/updateProjectManifest.ts

@morning-verlu morning-verlu force-pushed the codex/preserve-workspace-update-spec-fork branch from 198a987 to 5e581e2 Compare June 2, 2026 14:52
@zkochan zkochan force-pushed the codex/preserve-workspace-update-spec-fork branch from 5e581e2 to fddb8a4 Compare June 16, 2026 19:28
@qodo-free-for-open-source-projects

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

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📎 Requirement gaps (0)

Context used

Grey Divider


Remediation recommended

1. Backslash workspace path missed 🐞 Bug ≡ Correctness ⭐ New
Description
isWorkspaceLocalPathSpecifier()/is_workspace_local_path_specifier() fails to recognize Windows
backslash-root workspace specs like workspace:\foo\bar as local-path dependencies, so update flows
fall back to registry/normalized spec handling and can rewrite manifests even when
preserveWorkspaceProtocol is enabled. This violates the intended guarantee that workspace
local-path specifiers— including Windows-authored backslash paths—are preserved verbatim during
updates.
Code

installing/deps-resolver/src/updateProjectManifest.ts[R72-76]

+function isWorkspaceLocalPathSpecifier (bareSpecifier: string): boolean {
+  if (!bareSpecifier.startsWith('workspace:')) return false
+  const pref = bareSpecifier.slice('workspace:'.length)
+  return pref.startsWith('.') || pref.startsWith('/') || pref.startsWith('~/') || /^[A-Z]:/i.test(pref)
+}
Evidence
Both code paths make the “preserve vs rewrite” decision based on the workspace-local-path predicate:
if isWorkspaceLocalPathSpecifier() (or is_workspace_local_path_specifier(prev)) returns false,
the implementation saves a normalized/resolved bare specifier (via
resolvedDep.normalizedBareSpecifier / the --latest registry fetch_latest() route), which
rewrites the manifest entry. The evidence notes that pnpm’s local resolver normalizes backslashes in
workspace: specs (showing backslash-authored forms are valid local-path inputs), and pacquet’s
local bare-spec parsing explicitly accepts \\ as a path prefix, so failing to detect
workspace:\foo\bar incorrectly routes these dependencies through registry/normalization logic
rather than treating them as local workspace paths to preserve.

installing/deps-resolver/src/updateProjectManifest.ts[58-70]
installing/deps-resolver/src/updateProjectManifest.ts[72-76]
resolving/local-resolver/src/parseBareSpecifier.ts[44-46]
resolving/local-resolver/src/parseBareSpecifier.ts[82-85]
pacquet/crates/package-manager/src/update.rs[287-296]
pacquet/crates/package-manager/src/update.rs[716-725]
pacquet/crates/resolving-local-resolver/src/parse_bare_specifier.rs[312-329]

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

## Issue description
The workspace-local-path detection helper (`isWorkspaceLocalPathSpecifier()` / `is_workspace_local_path_specifier()`) does not recognize Windows backslash-root paths after `workspace:` (e.g. `workspace:\\foo\\bar`) as local workspace paths. Because update/save logic is gated on this predicate, a false negative causes the updater to take the registry/normalized-spec path (e.g. `fetch_latest()` / saving `resolvedDep.normalizedBareSpecifier`) and rewrite the manifest even when `preserveWorkspaceProtocol` is enabled; these Windows-authored backslash workspace paths should instead be treated as local and preserved.

## Issue Context
- The decision of whether to preserve the existing dependency spec or rewrite it during updates depends on the workspace-local-path predicate.
- pnpm’s local scheme parsing normalizes backslashes to `/`, indicating that backslash-authored `workspace:` paths are valid local-path inputs and resolve as local dependencies.
- pacquet’s local bare-spec parsing explicitly treats `\\` as a valid filespec/path prefix on Windows-host inputs, so `workspace:\\...` forms are plausible and should be excluded from registry resolution/rewriting.

## Fix Focus Areas
- installing/deps-resolver/src/updateProjectManifest.ts[58-76]
- pacquet/crates/package-manager/src/update.rs[287-296]
- pacquet/crates/package-manager/src/update.rs[716-725]

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


2. Brittle YAML append in test 🐞 Bug ☼ Reliability
Description
The new pacquet regression test appends a top-level packages: block to pnpm-workspace.yaml via
string concatenation, which will create duplicate keys (and potentially invalid/ambiguous YAML) if
the harness/fixture later starts emitting its own packages: key. This is test-only but can cause
future CI failures unrelated to the behavior under test.
Code

pacquet/crates/cli/tests/update.rs[R207-213]

+    let workspace_yaml = workspace.join("pnpm-workspace.yaml");
+    let mut yaml = fs::read_to_string(&workspace_yaml).expect("read pnpm-workspace.yaml");
+    if !yaml.ends_with('\n') {
+        yaml.push('\n');
+    }
+    yaml.push_str("packages:\n  - 'local-dep'\n");
+    fs::write(&workspace_yaml, yaml).expect("write pnpm-workspace.yaml");
Evidence
The test unconditionally appends a packages: key, while the harness currently writes
pnpm-workspace.yaml without packages: (so this passes today). A nearby helper demonstrates the
safer pattern: it asserts the key doesn’t already exist before appending to avoid invalid YAML when
the fixture changes.

pacquet/crates/cli/tests/update.rs[191-223]
pacquet/crates/testing-utils/src/bin.rs[52-89]
pacquet/crates/cli/tests/update.rs[40-59]

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

## Issue description
`update_latest_preserves_workspace_local_path_specifier` mutates `pnpm-workspace.yaml` by blindly appending a new `packages:` block. If `pnpm-workspace.yaml` already contains `packages:`, this produces duplicate top-level keys, which is brittle and may fail parsing depending on YAML loader behavior.
### Issue Context
Today, the harness (`CommandTempCwd::add_mocked_registry`) writes a minimal `pnpm-workspace.yaml` containing only `storeDir`, `cacheDir`, and `enableGlobalVirtualStore`, so the append happens to work. But other helpers in the same test file already defensively assert a key is absent before appending (e.g. `set_ignore_dependencies` checks for `updateConfig:`), indicating the intended pattern for safe mutation.
### Fix Focus Areas
- pacquet/crates/cli/tests/update.rs[207-213]
- pacquet/crates/testing-utils/src/bin.rs[52-85]
- pacquet/crates/cli/tests/update.rs[40-59]
### Suggested fix
Either:
1) Assert `!yaml.contains("packages:")` before appending (similar to `set_ignore_dependencies`), and fail loudly with guidance if it already exists; or
2) Properly parse YAML and merge `local-dep` into the existing `packages` array if present, otherwise create it.

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


Grey Divider

Previous review results

Review updated until commit 1f58673

Results up to commit 79f83e8


🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (0)

Context used

Remediation recommended
1. Brittle YAML append in test 🐞 Bug ☼ Reliability
Description
The new pacquet regression test appends a top-level packages: block to pnpm-workspace.yaml via
string concatenation, which will create duplicate keys (and potentially invalid/ambiguous YAML) if
the harness/fixture later starts emitting its own packages: key. This is test-only but can cause
future CI failures unrelated to the behavior under test.
Code

pacquet/crates/cli/tests/update.rs[R207-213]

+    let workspace_yaml = workspace.join("pnpm-workspace.yaml");
+    let mut yaml = fs::read_to_string(&workspace_yaml).expect("read pnpm-workspace.yaml");
+    if !yaml.ends_with('\n') {
+        yaml.push('\n');
+    }
+    yaml.push_str("packages:\n  - 'local-dep'\n");
+    fs::write(&workspace_yaml, yaml).expect("write pnpm-workspace.yaml");
Evidence
The test unconditionally appends a packages: key, while the harness currently writes
pnpm-workspace.yaml without packages: (so this passes today). A nearby helper demonstrates the
safer pattern: it asserts the key doesn’t already exist before appending to avoid invalid YAML when
the fixture changes.

pacquet/crates/cli/tests/update.rs[191-223]
pacquet/crates/testing-utils/src/bin.rs[52-89]
pacquet/crates/cli/tests/update.rs[40-59]

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

### Issue description
`update_latest_preserves_workspace_local_path_specifier` mutates `pnpm-workspace.yaml` by blindly appending a new `packages:` block. If `pnpm-workspace.yaml` already contains `packages:`, this produces duplicate top-level keys, which is brittle and may fail parsing depending on YAML loader behavior.

### Issue Context
Today, the harness (`CommandTempCwd::add_mocked_registry`) writes a minimal `pnpm-workspace.yaml` containing only `storeDir`, `cacheDir`, and `enableGlobalVirtualStore`, so the append happens to work. But other helpers in the same test file already defensively assert a key is absent before appending (e.g. `set_ignore_dependencies` checks for `updateConfig:`), indicating the intended pattern for safe mutation.

### Fix Focus Areas
- pacquet/crates/cli/tests/update.rs[207-213]
- pacquet/crates/testing-utils/src/bin.rs[52-85]
- pacquet/crates/cli/tests/update.rs[40-59]

### Suggested fix
Either:
1) Assert `!yaml.contains("packages:")` before appending (similar to `set_ignore_dependencies`), and fail loudly with guidance if it already exists; or
2) Properly parse YAML and merge `local-dep` into the existing `packages` array if present, otherwise create it.

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


Results up to commit fddb8a4


🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)

Great, no issues found!

Qodo reviewed your code and found no material issues that require review
Qodo Logo

@zkochan zkochan force-pushed the codex/preserve-workspace-update-spec-fork branch from fddb8a4 to 79f83e8 Compare June 16, 2026 21:39
@qodo-free-for-open-source-projects

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

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 79f83e8

Comment thread pacquet/crates/cli/tests/update.rs
@codecov-commenter

codecov-commenter commented Jun 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 88.08%. Comparing base (8081aac) to head (1f58673).
⚠️ Report is 3 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main   #12140   +/-   ##
=======================================
  Coverage   88.08%   88.08%           
=======================================
  Files         310      310           
  Lines       41847    41855    +8     
=======================================
+ Hits        36862    36870    +8     
  Misses       4985     4985           

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

morning-verlu and others added 2 commits June 16, 2026 23:56
…hangeset

Mirror the pnpm-side `getBareSpecifierToSave` fix in pacquet's `update`
command so `pacquet update --latest` no longer rewrites a `workspace:`
local-path dependency (e.g. `workspace:../packages/foo/dist`) into a
registry version. Both `--latest` rewrite sites now skip registry
resolution for such specs via `is_workspace_local_path_specifier`, a
faithful port of pnpm's `isWorkspaceLocalPathSpecifier`. Also adds the
missing changeset for the published `@pnpm/installing.deps-resolver`
change.

---
Written by an agent (Claude Code, claude-opus-4-8).
@zkochan zkochan force-pushed the codex/preserve-workspace-update-spec-fork branch from 79f83e8 to 1f58673 Compare June 16, 2026 21:58
@qodo-free-for-open-source-projects

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

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 1f58673

Comment on lines +72 to +76
function isWorkspaceLocalPathSpecifier (bareSpecifier: string): boolean {
if (!bareSpecifier.startsWith('workspace:')) return false
const pref = bareSpecifier.slice('workspace:'.length)
return pref.startsWith('.') || pref.startsWith('/') || pref.startsWith('~/') || /^[A-Z]:/i.test(pref)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

1. Backslash workspace path missed 🐞 Bug ≡ Correctness

isWorkspaceLocalPathSpecifier()/is_workspace_local_path_specifier() fails to recognize Windows
backslash-root workspace specs like workspace:\foo\bar as local-path dependencies, so update flows
fall back to registry/normalized spec handling and can rewrite manifests even when
preserveWorkspaceProtocol is enabled. This violates the intended guarantee that workspace
local-path specifiers— including Windows-authored backslash paths—are preserved verbatim during
updates.
Agent Prompt
## Issue description
The workspace-local-path detection helper (`isWorkspaceLocalPathSpecifier()` / `is_workspace_local_path_specifier()`) does not recognize Windows backslash-root paths after `workspace:` (e.g. `workspace:\\foo\\bar`) as local workspace paths. Because update/save logic is gated on this predicate, a false negative causes the updater to take the registry/normalized-spec path (e.g. `fetch_latest()` / saving `resolvedDep.normalizedBareSpecifier`) and rewrite the manifest even when `preserveWorkspaceProtocol` is enabled; these Windows-authored backslash workspace paths should instead be treated as local and preserved.

## Issue Context
- The decision of whether to preserve the existing dependency spec or rewrite it during updates depends on the workspace-local-path predicate.
- pnpm’s local scheme parsing normalizes backslashes to `/`, indicating that backslash-authored `workspace:` paths are valid local-path inputs and resolve as local dependencies.
- pacquet’s local bare-spec parsing explicitly treats `\\` as a valid filespec/path prefix on Windows-host inputs, so `workspace:\\...` forms are plausible and should be excluded from registry resolution/rewriting.

## Fix Focus Areas
- installing/deps-resolver/src/updateProjectManifest.ts[58-76]
- pacquet/crates/package-manager/src/update.rs[287-296]
- pacquet/crates/package-manager/src/update.rs[716-725]

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

@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.231 ± 0.101 4.109 4.408 2.04 ± 0.14
pacquet@main 4.232 ± 0.193 3.981 4.589 2.05 ± 0.16
pnpr@HEAD 2.182 ± 0.111 2.027 2.339 1.05 ± 0.08
pnpr@main 2.069 ± 0.128 1.889 2.292 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 4.23089331874,
      "stddev": 0.10116526122339824,
      "median": 4.19723895544,
      "user": 4.113932559999999,
      "system": 3.4701284599999993,
      "min": 4.10907628544,
      "max": 4.40833499644,
      "times": [
        4.25095880444,
        4.10907628544,
        4.1863405364399995,
        4.26022831544,
        4.39675254244,
        4.15055693244,
        4.40833499644,
        4.20487532644,
        4.15220686344,
        4.189602584439999
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 4.23197203824,
      "stddev": 0.19287780409771055,
      "median": 4.20946907644,
      "user": 4.054297859999999,
      "system": 3.4205866599999992,
      "min": 3.98052352344,
      "max": 4.5889700724399995,
      "times": [
        4.5889700724399995,
        4.117222970439999,
        4.30062261644,
        4.0461278714399995,
        4.19801830344,
        4.43992869344,
        4.06115609344,
        4.2209198494399995,
        4.36623038844,
        3.98052352344
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 2.18178215874,
      "stddev": 0.11053646472005757,
      "median": 2.16010811644,
      "user": 2.7009714599999994,
      "system": 2.9373769599999995,
      "min": 2.0272342944400004,
      "max": 2.3388523674400004,
      "times": [
        2.3169527644400003,
        2.12256470244,
        2.07395999144,
        2.2970809244400003,
        2.0272342944400004,
        2.0838405684400003,
        2.18374221444,
        2.3388523674400004,
        2.13647401844,
        2.2371197414400004
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 2.06906477364,
      "stddev": 0.12844746938281967,
      "median": 2.05440726094,
      "user": 2.72936556,
      "system": 2.8820641599999997,
      "min": 1.8888966764400001,
      "max": 2.29234637544,
      "times": [
        2.10018358144,
        2.29234637544,
        2.20167826244,
        1.93352433544,
        1.98800392844,
        1.98698488144,
        1.8888966764400001,
        2.0086309404400002,
        2.1265653594400002,
        2.16383339544
      ]
    }
  ]
}

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

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 636.7 ± 11.2 613.1 651.3 1.00
pacquet@main 638.2 ± 12.9 619.8 657.3 1.00 ± 0.03
pnpr@HEAD 681.4 ± 15.8 656.5 706.8 1.07 ± 0.03
pnpr@main 685.8 ± 17.9 655.8 722.4 1.08 ± 0.03
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.63667739288,
      "stddev": 0.01117062980186702,
      "median": 0.63810889648,
      "user": 0.37254692,
      "system": 1.3255773200000003,
      "min": 0.61313586498,
      "max": 0.65129120098,
      "times": [
        0.63789825398,
        0.62367819798,
        0.63789922598,
        0.6414469939799999,
        0.64812129098,
        0.65129120098,
        0.63831856698,
        0.63391555498,
        0.64106877798,
        0.61313586498
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.6381763151800001,
      "stddev": 0.01288915645949533,
      "median": 0.63911952648,
      "user": 0.39098862,
      "system": 1.3282018199999999,
      "min": 0.61977959498,
      "max": 0.65730383998,
      "times": [
        0.65730383998,
        0.63069935698,
        0.61977959498,
        0.62207976298,
        0.62895593598,
        0.63801911698,
        0.64648301798,
        0.64021993598,
        0.65537637298,
        0.64284621698
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.6814439734800001,
      "stddev": 0.015835730168625443,
      "median": 0.68372370248,
      "user": 0.38799771999999993,
      "system": 1.35771002,
      "min": 0.65654944398,
      "max": 0.70684523098,
      "times": [
        0.68415877198,
        0.69667626898,
        0.66622332698,
        0.65654944398,
        0.68006073698,
        0.69210432998,
        0.68328863298,
        0.70684523098,
        0.66204113398,
        0.68649185798
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.68578244968,
      "stddev": 0.017949889189942045,
      "median": 0.68292605098,
      "user": 0.39389031999999996,
      "system": 1.3567570199999996,
      "min": 0.65577611098,
      "max": 0.72244110898,
      "times": [
        0.70065772798,
        0.69417953698,
        0.72244110898,
        0.65577611098,
        0.6807686039799999,
        0.68065460998,
        0.68338634598,
        0.66883262198,
        0.68246575598,
        0.68866207398
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 4.278 ± 0.070 4.231 4.464 2.05 ± 0.10
pacquet@main 4.252 ± 0.076 4.117 4.372 2.04 ± 0.10
pnpr@HEAD 2.159 ± 0.127 1.979 2.349 1.04 ± 0.08
pnpr@main 2.085 ± 0.094 1.975 2.298 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 4.277545605039999,
      "stddev": 0.07003039808260346,
      "median": 4.25320628044,
      "user": 3.84938104,
      "system": 3.318024,
      "min": 4.23051554244,
      "max": 4.46384908644,
      "times": [
        4.46384908644,
        4.26141773144,
        4.23108579944,
        4.24180289444,
        4.23051554244,
        4.25453987344,
        4.24085884044,
        4.29932233844,
        4.25187268744,
        4.30019125644
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 4.25222874824,
      "stddev": 0.07619050543042295,
      "median": 4.23895752994,
      "user": 3.87456064,
      "system": 3.3002009,
      "min": 4.11660505044,
      "max": 4.37207131944,
      "times": [
        4.303391245439999,
        4.24471225644,
        4.11660505044,
        4.23320280344,
        4.20114379944,
        4.2504093554399995,
        4.208970852439999,
        4.2320315084399995,
        4.35974929144,
        4.37207131944
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 2.1589483194399994,
      "stddev": 0.1271747984605788,
      "median": 2.1465098439399997,
      "user": 2.58513934,
      "system": 2.8408594,
      "min": 1.97892903944,
      "max": 2.3485919684399996,
      "times": [
        2.3485919684399996,
        2.0586522494399997,
        2.1815665324399998,
        1.97892903944,
        2.3194669904399996,
        2.13818593144,
        2.04104815944,
        2.0717700024399996,
        2.29643856444,
        2.15483375644
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 2.0850699161399997,
      "stddev": 0.09400826935105654,
      "median": 2.07966266494,
      "user": 2.5505356399999997,
      "system": 2.8352712999999996,
      "min": 1.97498896644,
      "max": 2.29820830344,
      "times": [
        2.1178899424399997,
        1.99028882344,
        2.01903185744,
        2.14882555644,
        2.1096670774399997,
        1.97498896644,
        2.03247330444,
        2.0735028124399997,
        2.29820830344,
        2.0858225174399996
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 1.345 ± 0.014 1.319 1.364 2.07 ± 0.08
pacquet@main 1.374 ± 0.027 1.353 1.438 2.11 ± 0.09
pnpr@HEAD 0.653 ± 0.027 0.626 0.722 1.00 ± 0.05
pnpr@main 0.651 ± 0.023 0.634 0.714 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 1.3445011324399998,
      "stddev": 0.01402422903908496,
      "median": 1.3457932065399998,
      "user": 1.3255103,
      "system": 1.7028575400000001,
      "min": 1.31883450004,
      "max": 1.36363421704,
      "times": [
        1.35177648904,
        1.34969206204,
        1.33420841204,
        1.3383403940399998,
        1.36363421704,
        1.3418943510399999,
        1.36234409304,
        1.33252040004,
        1.31883450004,
        1.3517664060399999
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 1.3736260468400001,
      "stddev": 0.02700693088015784,
      "median": 1.36161706304,
      "user": 1.3690604,
      "system": 1.72454114,
      "min": 1.35302879904,
      "max": 1.43765567804,
      "times": [
        1.36139747804,
        1.43765567804,
        1.36839804504,
        1.36063193104,
        1.3578778490399999,
        1.40635050904,
        1.35302879904,
        1.37052830004,
        1.36183664804,
        1.35855523104
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.65320104264,
      "stddev": 0.027155867442142052,
      "median": 0.64649483304,
      "user": 0.3288849,
      "system": 1.2776648400000001,
      "min": 0.62626235604,
      "max": 0.7218971320400001,
      "times": [
        0.63422920104,
        0.62626235604,
        0.6404374610400001,
        0.64532215104,
        0.7218971320400001,
        0.64840214504,
        0.6710426740400001,
        0.65808703804,
        0.6476675150400001,
        0.6386627530400001
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.6508371794400001,
      "stddev": 0.022974661275112118,
      "median": 0.64336086504,
      "user": 0.3363094,
      "system": 1.2843951399999998,
      "min": 0.6335056490400001,
      "max": 0.7135281960400001,
      "times": [
        0.64063036104,
        0.64825417904,
        0.6401445440400001,
        0.64280954704,
        0.6439121830400001,
        0.65880661104,
        0.6453856440400001,
        0.6335056490400001,
        0.7135281960400001,
        0.64139488004
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 3.033 ± 0.062 2.978 3.192 4.49 ± 0.14
pacquet@main 3.104 ± 0.078 3.029 3.265 4.59 ± 0.16
pnpr@HEAD 0.676 ± 0.016 0.654 0.702 1.00
pnpr@main 0.694 ± 0.086 0.647 0.937 1.03 ± 0.13
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 3.03286467224,
      "stddev": 0.06199991427814319,
      "median": 3.01277683064,
      "user": 1.76653616,
      "system": 2.00169972,
      "min": 2.97778838864,
      "max": 3.19169278864,
      "times": [
        3.0656085606400003,
        3.04315698464,
        3.00058608364,
        3.01281792864,
        3.01273573264,
        3.03719814264,
        2.9836304566400003,
        3.19169278864,
        2.97778838864,
        3.00343165564
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 3.1037916231400002,
      "stddev": 0.07765484860962729,
      "median": 3.08108702614,
      "user": 1.8602231599999999,
      "system": 2.0231171199999998,
      "min": 3.02886147164,
      "max": 3.26531164764,
      "times": [
        3.03064639264,
        3.06000944264,
        3.0357489276400003,
        3.02886147164,
        3.18874543564,
        3.14665261264,
        3.09396958064,
        3.26531164764,
        3.11976624864,
        3.06820447164
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.6756841827400001,
      "stddev": 0.016209705350704016,
      "median": 0.67336116814,
      "user": 0.33082196,
      "system": 1.33899992,
      "min": 0.65396622064,
      "max": 0.70173369864,
      "times": [
        0.68721488564,
        0.6637727426400001,
        0.6714406806400001,
        0.66897054164,
        0.67528165564,
        0.69960116064,
        0.67660020864,
        0.65826003264,
        0.65396622064,
        0.70173369864
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.6940260319400001,
      "stddev": 0.08631268460466239,
      "median": 0.66698110564,
      "user": 0.34727036,
      "system": 1.2985485200000002,
      "min": 0.64678313564,
      "max": 0.93664970264,
      "times": [
        0.70024740364,
        0.66289661864,
        0.64678313564,
        0.66747313064,
        0.65773203664,
        0.66652633364,
        0.66376505464,
        0.67075102564,
        0.93664970264,
        0.66743587764
      ]
    }
  ]
}

@github-actions

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12140
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,277.55 ms
(+2.42%)Baseline: 4,176.66 ms
5,011.99 ms
(85.35%)
isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
🚷 view threshold
3,032.86 ms
(+1.44%)Baseline: 2,989.69 ms
3,587.63 ms
(84.54%)
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
🚷 view threshold
1,344.50 ms
(+2.54%)Baseline: 1,311.17 ms
1,573.41 ms
(85.45%)
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
🚷 view threshold
4,230.89 ms
(+4.55%)Baseline: 4,046.70 ms
4,856.04 ms
(87.13%)
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
🚷 view threshold
636.68 ms
(+3.81%)Baseline: 613.33 ms
736.00 ms
(86.51%)
🐰 View full continuous benchmarking report in Bencher

@github-actions

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12140
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,158.95 ms
isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
675.68 ms
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
653.20 ms
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
⚠️ NO THRESHOLD
2,181.78 ms
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
681.44 ms
🐰 View full continuous benchmarking report in Bencher

@zkochan zkochan enabled auto-merge (squash) June 16, 2026 22:37
@zkochan zkochan merged commit 531f2a3 into pnpm:main Jun 16, 2026
26 of 27 checks passed
KSXGitHub pushed a commit that referenced this pull request Jun 17, 2026
Brings in 2 new commits from main (through 531f2a3): #12140
(preserve workspace specs on update) and #12462 (PR review
automation CI). Conflict-free — neither touches the Arc-cleanup files
(compat_package_extensions.rs, install_with_fresh_lockfile.rs, upload.rs).
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.

pnpm up replaces workspace: protocol relative paths with version range

3 participants