Minimal reproduction of a bug where, in a workspace using enableGlobalVirtualStore: true
together with per-package lockfiles (sharedWorkspaceLockfile: false + a nested
pnpm-workspace.yaml in each package), running pnpm install inside a workspace
package — after a successful install at the workspace root — prompts:
? The modules directory at ".../libs/common/node_modules" will be removed and
reinstalled from scratch. Proceed? (Y/n)
(In a non-TTY/CI shell the same condition surfaces as
ERR_PNPM_ABORTED_REMOVE_MODULES_DIR_NO_TTY.)
.
├── package.json # private workspace root (no deps)
├── pnpm-workspace.yaml # packages: [libs/*]; enableGlobalVirtualStore; sharedWorkspaceLockfile: false
└── libs/common
├── package.json # one dep: is-odd
└── pnpm-workspace.yaml # makes libs/common ALSO its own workspace root; enableGlobalVirtualStore
libs/common is therefore both a member of the root workspace and its own
standalone workspace root (it has its own pnpm-workspace.yaml and gets its own
pnpm-lock.yaml). This dual role is what exposes the bug.
./reset.sh # remove node_modules + per-project lockfiles
pnpm install # at the root — succeeds
cd libs/common && pnpm install
# -> prompts to remove & recreate node_modules (or ABORTED_..._NO_TTY in CI)The purge is triggered by an ERR_PNPM_UNEXPECTED_VIRTUAL_STORE mismatch in
libs/common/node_modules/.modules.yaml:
| Install | virtualStoreDir it expects / records |
|---|---|
| root workspace install | records local node_modules/.pnpm |
standalone libs/common install |
wants global <store>/v11/links |
Both have GVS enabled, so they should agree. The divergence comes from the post-install build pass:
- The install/link step writes
.modules.yamlwith the correct GVS valuevirtualStoreDir = <store>/links(set byextendInstallOptions). - A per-project rebuild pass (
buildProjectsinbuilding/after-install/src/index.ts, run via the recursive rebuild during a workspace install) callsgetContext()then rewrites each project's.modules.yaml. Its options come fromextendBuildOptions, which — unlikeextendInstallOptions— never setsvirtualStoreDir = <store>/linksfor GVS. SogetContextfalls back to the localnode_modules/.pnpmdefault and the rewrite clobbers the correct value.
The next install inside the package recomputes <store>/links, sees the recorded
local .pnpm, and concludes the modules dir is incompatible → purge prompt.
(buildSelectedPkgs in the same file already preserves
ctx.modulesFile?.virtualStoreDir ?? ctx.virtualStoreDir; buildProjects does not.)
Make the build options GVS-aware, symmetric with extendInstallOptions, in
building/after-install/src/extendBuildOptions.ts:
if (extendedOpts.enableGlobalVirtualStore && extendedOpts.virtualStoreDir == null) {
extendedOpts.virtualStoreDir =
extendedOpts.globalVirtualStoreDir ?? path.join(extendedOpts.storeDir, 'links')
}With this, the build pass records the same <store>/links the install wrote, both
the root and the package record it consistently, and the per-package install no
longer prompts to purge.