Skip to content

fix: record the global virtual store dir in .modules.yaml during the post-install build step#12308

Merged
zkochan merged 2 commits into
pnpm:mainfrom
rubnogueira:fix/gvs-workspace-invalid-link
Jun 16, 2026
Merged

fix: record the global virtual store dir in .modules.yaml during the post-install build step#12308
zkochan merged 2 commits into
pnpm:mainfrom
rubnogueira:fix/gvs-workspace-invalid-link

Conversation

@rubnogueira

@rubnogueira rubnogueira commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Fixes #12307.

Under enableGlobalVirtualStore, running pnpm install in a workspace package kept prompting to remove and reinstall node_modules (or, in a non-TTY shell, failing with ERR_PNPM_ABORTED_REMOVE_MODULES_DIR_NO_TTY), even when nothing changed.

Root cause

During an install, each project's node_modules/.modules.yaml is written twice:

  1. The install/link step records the correct global virtual store directory — extendInstallOptions sets virtualStoreDir to <storeDir>/links when GVS is enabled.
  2. The post-install build step (buildProjects / buildSelectedPkgs) calls getContext() and rewrites .modules.yaml. Its options come from extendBuildOptions, which — unlike extendInstallOptions — never set virtualStoreDir for GVS, so getContext() fell back to the per-project node_modules/.pnpm default and overwrote the value the install step had recorded.

The next install then recomputes <storeDir>/links, compares it to the recorded node_modules/.pnpm, fails the virtual-store check with ERR_PNPM_UNEXPECTED_VIRTUAL_STORE, and — because a plain install runs with forceNewModules — prompts to purge node_modules.

Fix

Make extendBuildOptions GVS-aware, mirroring extendInstallOptions: when enableGlobalVirtualStore is set and virtualStoreDir is unset, derive it from globalVirtualStoreDir (falling back to <storeDir>/links). Both build consumers flow their options through extendBuildOptions into getContext, so this single change makes the build step record the same virtual-store path the install step wrote.

Tests

Added an e2e regression test (pnpm/test/install/globalVirtualStore.ts) covering a workspace package that is also its own workspace root: after the install it asserts the package's .modules.yaml records the global virtual store directory, and that a subsequent install is a clean no-op rather than a purge prompt.

pacquet

Not affected. pacquet resolves the virtual store directory through a single Config::effective_virtual_store_dir() helper at its one .modules.yaml write site, so it has no equivalent install-vs-build asymmetry (already covered by existing config and modules-yaml tests).


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

@rubnogueira rubnogueira requested a review from zkochan as a code owner June 10, 2026 11:59
@welcome

welcome Bot commented Jun 10, 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.

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

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

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0)

Grey Divider


Informational

1. Hardcoded store version 🐞 Bug ⚙ Maintainability
Description
The new GVS build-step regression test hardcodes the store layout version as "v11", so it will
become brittle and fail on the next STORE_VERSION bump even if behavior remains correct. This
increases maintenance overhead because the store version is already centrally derived by
getStorePath().
Code

pnpm/test/install/globalVirtualStore.ts[R118-119]

+  const storeDir = path.resolve('store')
+  const globalVirtualStoreDir = path.join(storeDir, 'v11/links')
Evidence
The new test hardcodes v11/links, while store path versioning is centrally controlled by
STORE_VERSION and appended automatically by store-path resolution logic; using the constant avoids
future version-bump breakage.

pnpm/test/install/globalVirtualStore.ts[111-120]
store/path/src/index.ts[5-43]

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 new test computes `globalVirtualStoreDir` using a hardcoded `v11/links` segment, which will break when pnpm’s `STORE_VERSION` changes.

### Issue Context
The actual store directory path is versioned via `getStorePath()` (it appends `STORE_VERSION` to the configured base store dir). Tests should derive the versioned path from `STORE_VERSION` to avoid churn.

### Fix Focus Areas
- pnpm/test/install/globalVirtualStore.ts[118-120]

### Suggested change
Import `STORE_VERSION` from `@pnpm/constants` and build the expected path as:

```ts
import { STORE_VERSION } from '@pnpm/constants'
// ...
const globalVirtualStoreDir = path.join(storeDir, STORE_VERSION, 'links')
```

ⓘ 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 6069d24

Results up to commit 701a6be


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

Great, no issues found!

Qodo reviewed your code and found no material issues that require review
Results up to commit 671244f


🐞 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

@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

extendBuildOptions now conditionally sets virtualStoreDir to align with the global virtual store configuration, mirroring the install-step behavior: when enableGlobalVirtualStore is enabled and virtualStoreDir is unset, it derives the value from globalVirtualStoreDir or defaults to <storeDir>/links, preventing mismatches where the build step would otherwise overwrite the install step's recorded location. An integration test validates that nested workspace packages preserve the correct global virtual store directory across successive installs.

Changes

Global virtual store behavior

Layer / File(s) Summary
virtualStoreDir type property and derivation logic
building/after-install/src/extendBuildOptions.ts, .changeset/gvs-build-step-virtual-store-dir.md
StrictBuildOptions adds optional virtualStoreDir?: string property. extendBuildOptions conditionally sets virtualStoreDir to globalVirtualStoreDir or defaults to <storeDir>/links when enableGlobalVirtualStore is true and virtualStoreDir is unset. Changeset documents the fix for repeated reinstall prompts when the build step overwrites the install step's recorded global virtual store location.
Integration test for global virtual store preservation
pnpm/test/install/globalVirtualStore.ts
Test imports preparePackages to enable multi-package workspace setup. New test verifies a nested workspace package with its own workspace root preserves the correct virtualStoreDir in node_modules/.modules.yaml pointing to the global virtual store, and confirms subsequent installs in that package scope are no-ops without virtual-store mismatch behavior.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

  • pnpm/pnpm#11987: Both PRs modify building/after-install/src/extendBuildOptions.ts to ensure virtualStoreDir defaults align with the global virtual store layout used by the GVS-aware after-install rebuild flow.
  • pnpm/pnpm#11896: Both PRs update how virtualStoreDir is derived under enableGlobalVirtualStore to keep it aligned with the global virtual store value across install and build steps.

Suggested labels

area: global virtual store

Suggested reviewers

  • zkochan

Poem

🐰 A virtual store with a wandering name,
Now gets the same treatment—always the same!
Build and install sing in perfect tune,
No more surprises beneath the moon.
The links all point to the right place now—
Hop hop hop! 🥕

🚥 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 title accurately describes the main change: fixing the post-install build step to record the global virtual store directory in .modules.yaml, which directly addresses issue #12307.
Linked Issues check ✅ Passed All code changes successfully implement the fix for issue #12307: StrictBuildOptions now includes virtualStoreDir, extendBuildOptions mirrors GVS logic to set the correct virtual store path, and test verifies the fix works as intended.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing issue #12307: type updates, build option logic, test infrastructure, and changeset documentation are all focused on resolving the GVS-related virtual store mismatch problem.
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

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

PR Summary by Qodo

Fix GVS build step to preserve global virtualStoreDir in .modules.yaml
🐞 Bug fix 🕐 10-20 Minutes

Grey Divider

Walkthroughs

Description
• Make post-install build options GVS-aware to avoid virtual-store mismatches.
• Ensure build step records /links in .modules.yaml under GVS.
• Prevent subsequent workspace installs from prompting to purge node_modules.
Diagram
graph TD
  A["Post-install build"] --> B["extendBuildOptions"] --> C["getContext()"] --> D["writeModulesManifest"] --> E[".modules.yaml"]
  B --> F{GVS enabled?}
  F -->|Yes| G["virtualStoreDir = <storeDir>/links"] --> C
  F -->|No| H["default: node_modules/.pnpm"] --> C

  subgraph Legend
    direction LR
    _proc[Process] ~~~ _fn[Function] ~~~ _dec{Decision} ~~~ _file[File]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Patch callers to pass virtualStoreDir explicitly
  • ➕ Avoids changing shared option-extension logic
  • ➕ Call sites can be explicit about intended behavior
  • ➖ Duplicated logic across buildProjects/buildSelectedPkgs and future consumers
  • ➖ Easier to miss new call sites, reintroducing the bug
2. Rely on existing manifest value when rewriting .modules.yaml
  • ➕ Minimizes dependence on option defaults
  • ➕ Works even if options are incomplete
  • ➖ Doesn’t help first-time manifest writes
  • ➖ Still leaves getContext() inconsistent with install semantics under GVS
3. Move GVS virtualStoreDir derivation into getContext()
  • ➕ Single canonical place to compute virtualStoreDir
  • ➕ Protects any context consumer, not just build step
  • ➖ Higher risk / broader behavioral change (affects all context users)
  • ➖ Harder to reason about when options should override defaults

Recommendation: The chosen approach—making extendBuildOptions mirror extendInstallOptions under enableGlobalVirtualStore—is the best tradeoff. It fixes the root cause (incorrect defaulting before getContext()) with minimal scope, and automatically covers both buildProjects and buildSelectedPkgs without duplicating logic at call sites.

Grey Divider

File Changes

Bug fix (1)
extendBuildOptions.ts Default virtualStoreDir to '<storeDir>/links' when GVS is enabled +11/-0

Default virtualStoreDir to '<storeDir>/links' when GVS is enabled

• Adds 'virtualStoreDir?: string' to build options and sets it during option extension when 'enableGlobalVirtualStore' is on and no override is provided. This prevents the build step from falling back to per-project 'node_modules/.pnpm' and overwriting the install-recorded global virtual store path in '.modules.yaml'.

building/after-install/src/extendBuildOptions.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)
building/after-install/src/extendBuildOptions.ts (1)

112-118: ⚡ Quick win

Trim the inline comment to invariant-focused intent.

This note reads like incident history. Keep only the non-obvious invariant and move the narrative to PR/issue context.

Suggested edit
-  // Mirror extendInstallOptions: under a global virtual store, the virtual
-  // store directory is `<storeDir>/links`, not the per-project
-  // `node_modules/.pnpm`. Without this, getContext() in the build step
-  // defaults virtualStoreDir to the local `.pnpm` and writeModulesManifest
-  // overwrites the correct value the install step recorded — which makes the
-  // next install in that project detect a virtual-store mismatch and prompt
-  // to purge node_modules.
+  // Keep build-step virtualStoreDir aligned with install-step GVS resolution.
+  // Under GVS, default to `<storeDir>/links` (or explicit globalVirtualStoreDir).

As per coding guidelines, “Do not record past implementation shape, refactor history, or removed code in comments; use git log and git blame for that information instead.”

🤖 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 `@building/after-install/src/extendBuildOptions.ts` around lines 112 - 118, The
existing inline comment in extendBuildOptions.ts contains historical narrative;
replace it with a concise invariant-only comment that states: under a global
virtual store the virtual store directory is "<storeDir>/links" (not
"node_modules/.pnpm") and therefore getContext()/writeModulesManifest must
preserve the install-step virtualStoreDir to avoid virtual-store mismatches.
Remove the story/history text and keep only this minimal, non-obvious invariant
and its direct consequence so future readers understand why the value must be
preserved.

Source: Coding guidelines

🤖 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 `@building/after-install/src/extendBuildOptions.ts`:
- Around line 112-118: The existing inline comment in extendBuildOptions.ts
contains historical narrative; replace it with a concise invariant-only comment
that states: under a global virtual store the virtual store directory is
"<storeDir>/links" (not "node_modules/.pnpm") and therefore
getContext()/writeModulesManifest must preserve the install-step virtualStoreDir
to avoid virtual-store mismatches. Remove the story/history text and keep only
this minimal, non-obvious invariant and its direct consequence so future readers
understand why the value must be preserved.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 321fea68-fea6-41ca-aa3a-725a59b789f5

📥 Commits

Reviewing files that changed from the base of the PR and between b7195db and 671244f.

📒 Files selected for processing (1)
  • building/after-install/src/extendBuildOptions.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 (1)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Use Standard Style with trailing commas, prefer functions over classes, declare functions after they are used (relying on hoisting), limit functions to no more than two or three arguments, and use a single options object for functions needing more parameters
Follow import order: standard libraries first, then external dependencies (sorted alphabetically), then relative imports
Do not write comments that restate what the code already says; rename variables, split helpers, or move checks to more obvious places instead
Do not repeat documentation at call sites that already lives on the callee; update the JSDoc once and let every call site benefit
Use JSDoc for the function's contract (preconditions, postconditions, edge cases, why the function exists), not for re-narrating the function body
Do not record past implementation shape, refactor history, or removed code in comments; use git log and git blame for that information instead
Write comments only when the reason for code is non-obvious, a hidden invariant exists, a workaround for a known bug is needed, or an exception to surrounding pattern is deliberate

Files:

  • building/after-install/src/extendBuildOptions.ts
🧠 Learnings (2)
📚 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:

  • building/after-install/src/extendBuildOptions.ts
📚 Learning: 2026-06-05T13:47:26.046Z
Learnt from: vsumner
Repo: pnpm/pnpm PR: 12190
File: installing/deps-installer/src/install/index.ts:2337-2343
Timestamp: 2026-06-05T13:47:26.046Z
Learning: In the pnpm/pnpm codebase, `PnpmError` automatically prefixes `err.code` with `ERR_PNPM_` when you pass a code that does not already start with `ERR_PNPM_` (it normalizes `this.code` via `code.startsWith('ERR_PNPM_') ? code : `ERR_PNPM_${code}``). Therefore, during code review you should NOT flag `new PnpmError(...)` call sites for passing a bare error code (e.g., `new PnpmError('FROZEN_STORE_INCOMPATIBLE_WITH_PNPR', ...)`); the resulting `err.code` will still be `ERR_PNPM_FROZEN_STORE_INCOMPATIBLE_WITH_PNPR`.

Applied to files:

  • building/after-install/src/extendBuildOptions.ts
🔇 Additional comments (1)
building/after-install/src/extendBuildOptions.ts (1)

52-52: LGTM!

Also applies to: 119-121

@zkochan zkochan force-pushed the fix/gvs-workspace-invalid-link branch from 671244f to 701a6be Compare June 16, 2026 13:26
@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 701a6be

Add an e2e regression test for the post-install build step preserving the
global virtual store directory in a workspace package's .modules.yaml, and
the missing changeset for the fix (pnpm#12307).
@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 6069d24

@zkochan zkochan merged commit 9e0c375 into pnpm:main Jun 16, 2026
18 checks passed
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.

pnpm install in a workspace package keeps prompting to remove node_modules when enableGlobalVirtualStore is enabled

2 participants