mirror of
https://github.com/pnpm/pnpm.git
synced 2026-04-27 18:46:18 -04:00
fix: prevent frozen-lockfile error when approving builds in global install (#11294)
In --global mode, globalAdd passes workspaceDir to approve-builds so it can update the global pnpm-workspace.yaml. approve-builds then forwarded that workspaceDir into install.handler, which (with workspacePackagePatterns undefined) recursively discovered sibling install dirs as workspace projects and failed the frozen-lockfile check on stale @pnpm/exe install dirs.
This commit is contained in:
6
.changeset/fix-global-approve-builds-frozen-lockfile.md
Normal file
6
.changeset/fix-global-approve-builds-frozen-lockfile.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@pnpm/building.commands": patch
|
||||
"pnpm": patch
|
||||
---
|
||||
|
||||
Fix `ERR_PNPM_OUTDATED_LOCKFILE` when approving builds during a global install. The `approve-builds` flow called by `pnpm add -g` passed the global packages directory to the subsequent install as `workspaceDir`, which caused sibling install directories (such as those left behind by `pnpm self-update`) to be picked up as workspace projects and fail the frozen-lockfile check.
|
||||
@@ -14,7 +14,17 @@ import { renderHelp } from 'render-help'
|
||||
import { rebuild, type RebuildCommandOpts } from '../build/index.js'
|
||||
import { getAutomaticallyIgnoredBuilds } from './getAutomaticallyIgnoredBuilds.js'
|
||||
|
||||
export type ApproveBuildsCommandOpts = Pick<Config, 'modulesDir' | 'dir' | 'allowBuilds' | 'enableGlobalVirtualStore'> & Pick<ConfigContext, 'rootProjectManifest' | 'rootProjectManifestDir'> & { all?: boolean, global?: boolean }
|
||||
export type ApproveBuildsCommandOpts = Pick<Config, 'modulesDir' | 'dir' | 'allowBuilds' | 'enableGlobalVirtualStore'> & Pick<ConfigContext, 'rootProjectManifest' | 'rootProjectManifestDir'> & {
|
||||
all?: boolean
|
||||
global?: boolean
|
||||
/**
|
||||
* When set, overrides the target directory for writeSettings.
|
||||
* Used by the global-install flow to point allowBuilds updates at the
|
||||
* global pnpm-workspace.yaml while keeping workspaceDir unset so the
|
||||
* install itself targets only the single install directory.
|
||||
*/
|
||||
settingsDir?: string
|
||||
}
|
||||
|
||||
export const commandNames = ['approve-builds']
|
||||
|
||||
@@ -184,7 +194,7 @@ Do you approve?`,
|
||||
}
|
||||
await writeSettings({
|
||||
...opts,
|
||||
workspaceDir: opts.workspaceDir ?? opts.rootProjectManifestDir,
|
||||
workspaceDir: opts.settingsDir ?? opts.workspaceDir ?? opts.rootProjectManifestDir,
|
||||
updatedSettings: { allowBuilds },
|
||||
})
|
||||
if (modulesManifest?.ignoredBuilds) {
|
||||
|
||||
@@ -462,3 +462,54 @@ test('should retain existing allowBuilds entries when approving builds', async (
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
// Regression test for the global-install path: globalAdd invokes
|
||||
// approve-builds with globalPkgDir set (so writeSettings updates the global
|
||||
// pnpm-workspace.yaml) but without workspaceDir. If approve-builds were to
|
||||
// treat globalPkgDir as a workspace, install.handler would recursively
|
||||
// discover sibling install dirs as workspace projects and fail the
|
||||
// frozen-lockfile check on those that don't have a matching pnpm-lock.yaml.
|
||||
test('GVS approve-builds writes settings to globalPkgDir without scanning siblings', async () => {
|
||||
const temp = tempDir()
|
||||
|
||||
prepare({
|
||||
dependencies: {
|
||||
'@pnpm.e2e/pre-and-postinstall-scripts-example': '1.0.0',
|
||||
},
|
||||
}, {
|
||||
tempDir: path.join(temp, 'project'),
|
||||
})
|
||||
|
||||
// Sibling install dir with a package.json that has no matching
|
||||
// pnpm-lock.yaml — mimics a stale `@pnpm/exe` install dir left behind in
|
||||
// the global packages directory.
|
||||
fs.mkdirSync(path.join(temp, 'stale-install'))
|
||||
fs.writeFileSync(
|
||||
path.join(temp, 'stale-install/package.json'),
|
||||
JSON.stringify({ dependencies: { '@pnpm/exe': '11.0.0-rc.2' } })
|
||||
)
|
||||
|
||||
await execPnpmInstall({ enableGlobalVirtualStore: true })
|
||||
|
||||
const config = await getApproveBuildsConfig()
|
||||
prompt.mockResolvedValueOnce({
|
||||
result: [{ value: '@pnpm.e2e/pre-and-postinstall-scripts-example' }],
|
||||
})
|
||||
prompt.mockResolvedValueOnce({ build: true })
|
||||
|
||||
// Match the global-install call site: settingsDir points at the global
|
||||
// packages dir (for writeSettings) but workspaceDir is not set, so install
|
||||
// doesn't scan globalPkgDir as a workspace.
|
||||
await approveBuilds.handler({
|
||||
...omit(['workspaceDir', 'workspacePackagePatterns'], config),
|
||||
enableGlobalVirtualStore: true,
|
||||
settingsDir: temp,
|
||||
rootProjectManifestDir: process.cwd(),
|
||||
} as ApproveBuildsCommandOpts & RebuildCommandOpts, [], {})
|
||||
|
||||
// writeSettings should have written allowBuilds to globalPkgDir's
|
||||
// pnpm-workspace.yaml, not to the project dir.
|
||||
const globalManifest = readYamlFileSync<any>(path.join(temp, 'pnpm-workspace.yaml')) // eslint-disable-line
|
||||
expect(globalManifest.allowBuilds?.['@pnpm.e2e/pre-and-postinstall-scripts-example']).toBe(true)
|
||||
expect(fs.existsSync('node_modules/@pnpm.e2e/pre-and-postinstall-scripts-example/generated-by-postinstall.js')).toBeTruthy()
|
||||
})
|
||||
|
||||
@@ -19,6 +19,7 @@ import { symlinkDir } from 'symlink-dir'
|
||||
|
||||
import { checkGlobalBinConflicts } from './checkGlobalBinConflicts.js'
|
||||
import { installGlobalPackages } from './installGlobalPackages.js'
|
||||
import { promptApproveGlobalBuilds } from './promptApproveGlobalBuilds.js'
|
||||
import { readInstalledPackages } from './readInstalledPackages.js'
|
||||
|
||||
export type GlobalAddOptions = CreateStoreControllerOptions & {
|
||||
@@ -90,22 +91,13 @@ export async function handleGlobalAdd (
|
||||
|
||||
const ignoredBuilds = await installGlobalPackages(makeInstallOpts(installDir, allowBuilds), params)
|
||||
|
||||
// If any packages had their builds skipped, prompt the user to approve them
|
||||
// (reuses the same interactive flow as `pnpm approve-builds`)
|
||||
if (ignoredBuilds?.size && process.stdin.isTTY) {
|
||||
await commands['approve-builds']({
|
||||
...opts,
|
||||
modulesDir: path.join(installDir, 'node_modules'),
|
||||
dir: installDir,
|
||||
lockfileDir: installDir,
|
||||
rootProjectManifest: undefined,
|
||||
rootProjectManifestDir: installDir,
|
||||
workspaceDir: opts.globalPkgDir!,
|
||||
global: false,
|
||||
pending: false,
|
||||
allowBuilds,
|
||||
}, [], commands)
|
||||
}
|
||||
await promptApproveGlobalBuilds({
|
||||
globalPkgDir: globalDir,
|
||||
installDir,
|
||||
ignoredBuilds,
|
||||
allowBuilds,
|
||||
inheritedOpts: opts,
|
||||
}, commands)
|
||||
|
||||
// Read resolved aliases from the installed package.json
|
||||
const pkgJson = readPackageJsonFromDirRawSync(installDir)
|
||||
|
||||
@@ -18,6 +18,7 @@ import { symlinkDir } from 'symlink-dir'
|
||||
|
||||
import { checkGlobalBinConflicts } from './checkGlobalBinConflicts.js'
|
||||
import { installGlobalPackages } from './installGlobalPackages.js'
|
||||
import { promptApproveGlobalBuilds } from './promptApproveGlobalBuilds.js'
|
||||
import { readInstalledPackages } from './readInstalledPackages.js'
|
||||
|
||||
export type GlobalUpdateOptions = CreateStoreControllerOptions & {
|
||||
@@ -110,22 +111,13 @@ async function updateGlobalPackageGroup (
|
||||
allowBuilds,
|
||||
}, depSpecs)
|
||||
|
||||
// If any packages had their builds skipped, prompt the user to approve them
|
||||
// (reuses the same interactive flow as `pnpm approve-builds`)
|
||||
if (ignoredBuilds?.size && process.stdin.isTTY) {
|
||||
await commands['approve-builds']({
|
||||
...opts,
|
||||
modulesDir: path.join(installDir, 'node_modules'),
|
||||
dir: installDir,
|
||||
lockfileDir: installDir,
|
||||
rootProjectManifest: undefined,
|
||||
rootProjectManifestDir: installDir,
|
||||
workspaceDir: opts.globalPkgDir!,
|
||||
global: false,
|
||||
pending: false,
|
||||
allowBuilds,
|
||||
}, [], commands)
|
||||
}
|
||||
await promptApproveGlobalBuilds({
|
||||
globalPkgDir: globalDir,
|
||||
installDir,
|
||||
ignoredBuilds,
|
||||
allowBuilds,
|
||||
inheritedOpts: opts,
|
||||
}, commands)
|
||||
|
||||
// Check for bin name conflicts with other global packages
|
||||
const pkgs = await readInstalledPackages(installDir)
|
||||
|
||||
49
global/commands/src/promptApproveGlobalBuilds.ts
Normal file
49
global/commands/src/promptApproveGlobalBuilds.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import path from 'node:path'
|
||||
|
||||
import type { CommandHandlerMap } from '@pnpm/cli.command'
|
||||
import type { IgnoredBuilds } from '@pnpm/types'
|
||||
|
||||
export interface PromptApproveGlobalBuildsOptions {
|
||||
globalPkgDir: string
|
||||
installDir: string
|
||||
ignoredBuilds: IgnoredBuilds | undefined
|
||||
allowBuilds: Record<string, string | boolean>
|
||||
/** Inherited config opts from the global add/update handler. */
|
||||
inheritedOpts: object
|
||||
}
|
||||
|
||||
/**
|
||||
* If the previous global install left builds awaiting approval, run the
|
||||
* interactive `approve-builds` flow against the install directory.
|
||||
*
|
||||
* `settingsDir` points at the global packages directory so the resulting
|
||||
* allowBuilds update lands in its pnpm-workspace.yaml. The
|
||||
* workspace-context fields (`workspaceDir`, `allProjects`,
|
||||
* `selectedProjectsGraph`, `workspacePackagePatterns`) are explicitly
|
||||
* cleared so that the install run by approve-builds in GVS mode operates
|
||||
* only on the install directory — otherwise it would treat the global
|
||||
* packages dir as a workspace and discover sibling install directories as
|
||||
* workspace projects.
|
||||
*/
|
||||
export async function promptApproveGlobalBuilds (
|
||||
opts: PromptApproveGlobalBuildsOptions,
|
||||
commands: CommandHandlerMap
|
||||
): Promise<void> {
|
||||
if (!opts.ignoredBuilds?.size || !process.stdin.isTTY) return
|
||||
await commands['approve-builds']({
|
||||
...opts.inheritedOpts,
|
||||
workspaceDir: undefined,
|
||||
allProjects: undefined,
|
||||
selectedProjectsGraph: undefined,
|
||||
workspacePackagePatterns: undefined,
|
||||
modulesDir: path.join(opts.installDir, 'node_modules'),
|
||||
dir: opts.installDir,
|
||||
lockfileDir: opts.installDir,
|
||||
rootProjectManifest: undefined,
|
||||
rootProjectManifestDir: opts.installDir,
|
||||
settingsDir: opts.globalPkgDir,
|
||||
global: false,
|
||||
pending: false,
|
||||
allowBuilds: opts.allowBuilds,
|
||||
}, [], commands)
|
||||
}
|
||||
Reference in New Issue
Block a user