Skip to content

Commit 9e0c375

Browse files
rubnogueirazkochan
andauthored
fix: record the global virtual store dir in .modules.yaml during the post-install build step (#12308)
* fix: invalid gvs workspace * test: cover GVS workspace virtual-store-dir + add changeset 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 (#12307). --------- Co-authored-by: Zoltan Kochan <z@kochan.io>
1 parent c112b61 commit 9e0c375

3 files changed

Lines changed: 60 additions & 1 deletion

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@pnpm/building.after-install": patch
3+
"pnpm": patch
4+
---
5+
6+
Fixed `pnpm install` repeatedly prompting to remove and reinstall `node_modules` in a workspace package when `enableGlobalVirtualStore` is enabled. The post-install build step recorded a per-project `node_modules/.pnpm` virtual store directory in `node_modules/.modules.yaml`, overwriting the global `<storeDir>/links` value the install step had written. The next install then detected a virtual-store mismatch (`ERR_PNPM_UNEXPECTED_VIRTUAL_STORE`). The build step now derives the same global virtual store directory as the install step [#12307](https://github.com/pnpm/pnpm/issues/12307).

building/after-install/src/extendBuildOptions.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export type StrictBuildOptions = {
5050
allowBuilds?: Record<string, boolean | string>
5151
enableGlobalVirtualStore?: boolean
5252
globalVirtualStoreDir?: string
53+
virtualStoreDir?: string
5354
virtualStoreDirMaxLength: number
5455
peersSuffixMaxLength: number
5556
strictStorePkgContentCheck: boolean
@@ -109,5 +110,15 @@ export async function extendBuildOptions (
109110
storeDir: defaultOpts.storeDir,
110111
}
111112
extendedOpts.registries = normalizeRegistries(extendedOpts.registries)
113+
// Mirror extendInstallOptions: under a global virtual store, the virtual
114+
// store directory is `<storeDir>/links`, not the per-project
115+
// `node_modules/.pnpm`. Without this, getContext() in the build step
116+
// defaults virtualStoreDir to the local `.pnpm` and writeModulesManifest
117+
// overwrites the correct value the install step recorded — which makes the
118+
// next install in that project detect a virtual-store mismatch and prompt
119+
// to purge node_modules.
120+
if (extendedOpts.enableGlobalVirtualStore && extendedOpts.virtualStoreDir == null) {
121+
extendedOpts.virtualStoreDir = extendedOpts.globalVirtualStoreDir ?? path.join(extendedOpts.storeDir, 'links')
122+
}
112123
return extendedOpts
113124
}

pnpm/test/install/globalVirtualStore.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import fs from 'node:fs'
22
import path from 'node:path'
33

44
import { expect, test } from '@jest/globals'
5-
import { prepare } from '@pnpm/prepare'
5+
import { prepare, preparePackages } from '@pnpm/prepare'
66
import { readYamlFileSync } from 'read-yaml-file'
77
import { writeYamlFileSync } from 'write-yaml-file'
88

@@ -107,3 +107,45 @@ test('warm GVS reinstall skips internal linking', async () => {
107107
expect(fs.existsSync(path.resolve('node_modules/.bin/hello-world-js-bin'))).toBeTruthy()
108108
expect(fs.existsSync(path.resolve('node_modules/.pnpm/lock.yaml'))).toBeTruthy()
109109
})
110+
111+
test('the post-install build step preserves the global virtual store directory of a workspace package', async () => {
112+
// A workspace package that is also its own workspace root (its own
113+
// pnpm-workspace.yaml and lockfile). The root install runs a per-project
114+
// build pass that must not overwrite the package's recorded global virtual
115+
// store directory with the local node_modules/.pnpm — otherwise the next
116+
// install in that package detects a virtual-store mismatch and prompts to
117+
// purge node_modules.
118+
const storeDir = path.resolve('store')
119+
const globalVirtualStoreDir = path.join(storeDir, 'v11/links')
120+
preparePackages([
121+
{
122+
location: 'libs/common',
123+
package: {
124+
name: '@repro/common',
125+
dependencies: {
126+
'@pnpm.e2e/pkg-with-1-dep': '100.0.0',
127+
},
128+
},
129+
},
130+
])
131+
writeYamlFileSync(path.resolve('pnpm-workspace.yaml'), {
132+
packages: ['libs/*'],
133+
enableGlobalVirtualStore: true,
134+
sharedWorkspaceLockfile: false,
135+
storeDir,
136+
})
137+
writeYamlFileSync(path.resolve('libs/common/pnpm-workspace.yaml'), {
138+
enableGlobalVirtualStore: true,
139+
storeDir,
140+
})
141+
142+
await execPnpm(['install'])
143+
144+
const modulesManifestPath = path.resolve('libs/common/node_modules/.modules.yaml')
145+
const modules = readYamlFileSync<{ virtualStoreDir: string }>(modulesManifestPath)
146+
expect(path.resolve('libs/common/node_modules', modules.virtualStoreDir)).toBe(globalVirtualStoreDir)
147+
148+
// A subsequent install in the package must be a no-op, not a virtual-store
149+
// mismatch that aborts (in a non-TTY shell) or prompts to purge node_modules.
150+
await execPnpm(['install', '--dir', path.resolve('libs/common')])
151+
})

0 commit comments

Comments
 (0)