mirror of
https://github.com/pnpm/pnpm.git
synced 2026-04-27 18:46:18 -04:00
fix: prevent store prune from breaking globally installed pnpm (#11253)
When pnpm self-updates via the headless install path, the install directory was not registered in the store's project registry. This caused `pnpm store prune` to treat its global virtual store packages as unreachable and remove them, breaking the global pnpm binary. Register the install dir after headless install in installPnpmToGlobalDir
This commit is contained in:
7
.changeset/fix-store-prune-global-pnpm.md
Normal file
7
.changeset/fix-store-prune-global-pnpm.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@pnpm/store.commands": patch
|
||||
"@pnpm/engine.pm.commands": patch
|
||||
"pnpm": patch
|
||||
---
|
||||
|
||||
Fixed `pnpm store prune` removing packages used by the globally installed pnpm, breaking it.
|
||||
@@ -61,6 +61,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/globals": "catalog:",
|
||||
"@pnpm/constants": "workspace:*",
|
||||
"@pnpm/engine.pm.commands": "workspace:*",
|
||||
"@pnpm/error": "workspace:*",
|
||||
"@pnpm/logger": "workspace:*",
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
} from '@pnpm/global.packages'
|
||||
import { headlessInstall } from '@pnpm/installing.deps-restorer'
|
||||
import type { EnvLockfile, LockfileObject, PackageSnapshot } from '@pnpm/lockfile.types'
|
||||
import type { StoreController } from '@pnpm/store.controller'
|
||||
import { registerProject, type StoreController } from '@pnpm/store.controller'
|
||||
import type { DepPath, ProjectId, ProjectRootDir, Registries } from '@pnpm/types'
|
||||
import { symlinkDir } from 'symlink-dir'
|
||||
|
||||
@@ -196,6 +196,11 @@ async function installPnpmToGlobalDir (
|
||||
virtualStoreDirMaxLength: opts.virtualStoreDirMaxLength,
|
||||
packageManager: opts.packageManager,
|
||||
})
|
||||
// headlessInstall does not register the project, so we must do it
|
||||
// explicitly. Without this, `pnpm store prune` would not know about
|
||||
// this install directory and would remove its packages from the
|
||||
// global virtual store.
|
||||
await registerProject(opts.storeDir, installDir)
|
||||
} else {
|
||||
await installFromResolution(installDir, opts, [`${pkgName}@${version}`])
|
||||
}
|
||||
|
||||
@@ -3,8 +3,10 @@ import { createRequire } from 'node:module'
|
||||
import path from 'node:path'
|
||||
|
||||
import { jest } from '@jest/globals'
|
||||
import { STORE_VERSION } from '@pnpm/constants'
|
||||
import { prepare as prepareWithPkg, tempDir } from '@pnpm/prepare'
|
||||
import { prependDirsToPath } from '@pnpm/shell.path'
|
||||
import { getRegisteredProjects } from '@pnpm/store.controller'
|
||||
import { getMockAgent, setupMockAgent, teardownMockAgent } from '@pnpm/testing.mock-agent'
|
||||
import spawn from 'cross-spawn'
|
||||
|
||||
@@ -138,14 +140,24 @@ test('self-update', async () => {
|
||||
|
||||
await selfUpdate.handler(opts, [])
|
||||
|
||||
// Verify the package was installed in the global dir
|
||||
// Verify the package was installed in the global dir.
|
||||
// The globalDir contains both the real install dir (a directory) and a
|
||||
// hash symlink pointing to it. Use lstatSync to pick the real dir.
|
||||
const globalDir = path.join(opts.pnpmHomeDir, 'global', 'v11')
|
||||
const entries = fs.readdirSync(globalDir)
|
||||
const installDirName = entries.find((e) => fs.statSync(path.join(globalDir, e)).isDirectory())
|
||||
const installDirName = entries.find((e) => fs.lstatSync(path.join(globalDir, e)).isDirectory())
|
||||
expect(installDirName).toBeDefined()
|
||||
const pnpmPkgJson = JSON.parse(fs.readFileSync(path.join(globalDir, installDirName!, 'node_modules/pnpm/package.json'), 'utf8'))
|
||||
const installDir = path.join(globalDir, installDirName!)
|
||||
const pnpmPkgJson = JSON.parse(fs.readFileSync(path.join(installDir, 'node_modules/pnpm/package.json'), 'utf8'))
|
||||
expect(pnpmPkgJson.version).toBe('9.1.0')
|
||||
|
||||
// Verify the install dir was registered in the store's project registry.
|
||||
// Without this, `pnpm store prune` would remove the install's packages
|
||||
// from the global virtual store.
|
||||
const storeDir = path.join(opts.pnpmHomeDir, 'store', STORE_VERSION)
|
||||
const registeredProjects = await getRegisteredProjects(storeDir)
|
||||
expect(registeredProjects).toContain(installDir)
|
||||
|
||||
const pnpmEnv = prependDirsToPath([path.join(opts.pnpmHomeDir, 'bin')])
|
||||
const { status, stdout } = spawn.sync('pnpm', ['-v'], {
|
||||
env: {
|
||||
|
||||
@@ -28,6 +28,9 @@
|
||||
{
|
||||
"path": "../../../config/reader"
|
||||
},
|
||||
{
|
||||
"path": "../../../core/constants"
|
||||
},
|
||||
{
|
||||
"path": "../../../core/error"
|
||||
},
|
||||
|
||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -3646,6 +3646,9 @@ importers:
|
||||
'@jest/globals':
|
||||
specifier: 'catalog:'
|
||||
version: 30.3.0
|
||||
'@pnpm/constants':
|
||||
specifier: workspace:*
|
||||
version: link:../../../core/constants
|
||||
'@pnpm/engine.pm.commands':
|
||||
specifier: workspace:*
|
||||
version: 'link:'
|
||||
|
||||
@@ -19,20 +19,23 @@ export async function storePrune (
|
||||
if ((reporter != null) && typeof reporter === 'function') {
|
||||
streamParser.on('data', reporter)
|
||||
}
|
||||
await opts.storeController.prune(opts.removeAlienFiles)
|
||||
await opts.storeController.close()
|
||||
|
||||
await cleanExpiredDlxCache({
|
||||
cacheDir: opts.cacheDir,
|
||||
dlxCacheMaxAge: opts.dlxCacheMaxAge,
|
||||
now: new Date(),
|
||||
})
|
||||
try {
|
||||
await opts.storeController.prune(opts.removeAlienFiles)
|
||||
|
||||
if (opts.globalPkgDir) {
|
||||
cleanOrphanedInstallDirs(opts.globalPkgDir)
|
||||
}
|
||||
await cleanExpiredDlxCache({
|
||||
cacheDir: opts.cacheDir,
|
||||
dlxCacheMaxAge: opts.dlxCacheMaxAge,
|
||||
now: new Date(),
|
||||
})
|
||||
|
||||
if ((reporter != null) && typeof reporter === 'function') {
|
||||
streamParser.removeListener('data', reporter)
|
||||
if (opts.globalPkgDir) {
|
||||
cleanOrphanedInstallDirs(opts.globalPkgDir)
|
||||
}
|
||||
} finally {
|
||||
await opts.storeController.close()
|
||||
if ((reporter != null) && typeof reporter === 'function') {
|
||||
streamParser.removeListener('data', reporter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user