fix: self-update should not read pnpm settings from current package.json (#9196)

close #9188
close #9183
This commit is contained in:
Zoltan Kochan
2025-03-01 13:49:56 +01:00
committed by GitHub
parent a5e4965c59
commit 6a59366248
16 changed files with 150 additions and 135 deletions

View File

@@ -0,0 +1,7 @@
---
"@pnpm/tools.plugin-commands-self-updater": patch
"@pnpm/plugin-commands-installation": patch
"pnpm": patch
---
`pnpm self-update` should not read the pnpm settings from the `package.json` file in the current working directory.

View File

@@ -0,0 +1,5 @@
---
"@pnpm/tools.plugin-commands-self-updater": minor
---
Export `installPnpmToTools`.

View File

@@ -0,0 +1,5 @@
---
"@pnpm/plugin-commands-script-runners": patch
---
Pass onlyBuiltDependencies as a direct option to add.handler.

View File

@@ -63,11 +63,13 @@ export async function getConfig (opts: {
checkUnknownSetting?: boolean
env?: Record<string, string | undefined>
ignoreNonAuthSettingsFromLocal?: boolean
ignoreLocalSettings?: boolean
}): Promise<{ config: Config, warnings: string[] }> {
if (opts.ignoreNonAuthSettingsFromLocal) {
const { ignoreNonAuthSettingsFromLocal: _, ...authOpts } = opts
const globalCfgOpts: typeof authOpts = {
...authOpts,
ignoreLocalSettings: true,
cliOptions: {
...authOpts.cliOptions,
dir: os.homedir(),
@@ -322,7 +324,9 @@ export async function getConfig (opts: {
pnpmConfig.virtualStoreDir = '.pnpm'
} else {
pnpmConfig.dir = cwd
pnpmConfig.bin = path.join(pnpmConfig.dir, 'node_modules', '.bin')
if (!pnpmConfig.bin) {
pnpmConfig.bin = path.join(pnpmConfig.dir, 'node_modules', '.bin')
}
}
if (opts.cliOptions['save-peer']) {
if (opts.cliOptions['save-prod']) {
@@ -476,27 +480,29 @@ export async function getConfig (opts: {
pnpmConfig.workspaceConcurrency = getWorkspaceConcurrency(pnpmConfig.workspaceConcurrency)
pnpmConfig.rootProjectManifestDir = pnpmConfig.lockfileDir ?? pnpmConfig.workspaceDir ?? pnpmConfig.dir
pnpmConfig.rootProjectManifest = await safeReadProjectManifestOnly(pnpmConfig.rootProjectManifestDir) ?? undefined
if (pnpmConfig.rootProjectManifest != null) {
if (pnpmConfig.rootProjectManifest.workspaces?.length && !pnpmConfig.workspaceDir) {
warnings.push('The "workspaces" field in package.json is not supported by pnpm. Create a "pnpm-workspace.yaml" file instead.')
if (!opts.ignoreLocalSettings) {
pnpmConfig.rootProjectManifestDir = pnpmConfig.lockfileDir ?? pnpmConfig.workspaceDir ?? pnpmConfig.dir
pnpmConfig.rootProjectManifest = await safeReadProjectManifestOnly(pnpmConfig.rootProjectManifestDir) ?? undefined
if (pnpmConfig.rootProjectManifest != null) {
if (pnpmConfig.rootProjectManifest.workspaces?.length && !pnpmConfig.workspaceDir) {
warnings.push('The "workspaces" field in package.json is not supported by pnpm. Create a "pnpm-workspace.yaml" file instead.')
}
if (pnpmConfig.rootProjectManifest.packageManager) {
pnpmConfig.wantedPackageManager = parsePackageManager(pnpmConfig.rootProjectManifest.packageManager)
}
if (pnpmConfig.rootProjectManifest) {
Object.assign(pnpmConfig, getOptionsFromRootManifest(pnpmConfig.rootProjectManifestDir, pnpmConfig.rootProjectManifest))
}
}
if (pnpmConfig.rootProjectManifest.packageManager) {
pnpmConfig.wantedPackageManager = parsePackageManager(pnpmConfig.rootProjectManifest.packageManager)
}
if (pnpmConfig.rootProjectManifest) {
Object.assign(pnpmConfig, getOptionsFromRootManifest(pnpmConfig.rootProjectManifestDir, pnpmConfig.rootProjectManifest))
}
}
if (pnpmConfig.workspaceDir != null) {
const workspaceManifest = await readWorkspaceManifest(pnpmConfig.workspaceDir)
if (pnpmConfig.workspaceDir != null) {
const workspaceManifest = await readWorkspaceManifest(pnpmConfig.workspaceDir)
pnpmConfig.workspacePackagePatterns = cliOptions['workspace-packages'] as string[] ?? workspaceManifest?.packages ?? ['.']
pnpmConfig.catalogs = getCatalogsFromWorkspaceManifest(workspaceManifest)
if (workspaceManifest) {
Object.assign(pnpmConfig, getOptionsFromPnpmSettings(pnpmConfig.workspaceDir, workspaceManifest, pnpmConfig.rootProjectManifest))
pnpmConfig.workspacePackagePatterns = cliOptions['workspace-packages'] as string[] ?? workspaceManifest?.packages ?? ['.']
pnpmConfig.catalogs = getCatalogsFromWorkspaceManifest(workspaceManifest)
if (workspaceManifest) {
Object.assign(pnpmConfig, getOptionsFromPnpmSettings(pnpmConfig.workspaceDir, workspaceManifest, pnpmConfig.rootProjectManifest))
}
}
}

View File

@@ -14,7 +14,6 @@ import { getBinsFromPackageManifest } from '@pnpm/package-bins'
import { pickRegistryForPackage } from '@pnpm/pick-registry-for-package'
import { type PnpmSettings } from '@pnpm/types'
import execa from 'execa'
import omit from 'ramda/src/omit'
import pick from 'ramda/src/pick'
import renderHelp from 'render-help'
import symlinkDir from 'symlink-dir'
@@ -109,40 +108,17 @@ export async function handler (
if (!cacheExists) {
fs.mkdirSync(cachedDir, { recursive: true })
await add.handler({
// Ideally the config reader should ignore these settings when the dlx command is executed.
// This is a temporary solution until "@pnpm/config" is refactored.
...omit([
'workspaceDir',
'rootProjectManifest',
'symlink',
// Options from root manifest
'allowNonAppliedPatches',
'allowedDeprecatedVersions',
'configDependencies',
'ignoredBuiltDependencies',
'ignoredOptionalDependencies',
'neverBuiltDependencies',
'onlyBuiltDependencies',
'onlyBuiltDependenciesFile',
'overrides',
'packageExtensions',
'patchedDependencies',
'peerDependencyRules',
'supportedArchitectures',
], opts),
...opts,
bin: path.join(cachedDir, 'node_modules/.bin'),
dir: cachedDir,
lockfileDir: cachedDir,
rootProjectManifestDir: cachedDir, // This property won't be used as rootProjectManifest will be undefined
rootProjectManifest: {
pnpm: {
onlyBuiltDependencies: [...resolvedPkgAliases, ...(opts.allowBuild ?? [])],
},
},
onlyBuiltDependencies: [...resolvedPkgAliases, ...(opts.allowBuild ?? [])],
saveProd: true, // dlx will be looking for the package in the "dependencies" field!
saveDev: false,
saveOptional: false,
savePeer: false,
symlink: true,
workspaceDir: undefined,
}, resolvedPkgs)
try {
await symlinkDir(cachedDir, cacheLink, { overwrite: true })

View File

@@ -227,12 +227,13 @@ export async function handler (
})
}
}
opts.rootProjectManifest = opts.rootProjectManifest ?? {}
opts.rootProjectManifest.pnpm = opts.rootProjectManifest.pnpm ?? {}
opts.rootProjectManifest.pnpm.onlyBuiltDependencies = Array.from(new Set([
...(opts.rootProjectManifest.pnpm.onlyBuiltDependencies ?? []),
opts.onlyBuiltDependencies = Array.from(new Set([
...(opts.onlyBuiltDependencies ?? []),
...opts.allowBuild,
])).sort((a, b) => a.localeCompare(b))
opts.rootProjectManifest = opts.rootProjectManifest ?? {}
opts.rootProjectManifest.pnpm = opts.rootProjectManifest.pnpm ?? {}
opts.rootProjectManifest.pnpm.onlyBuiltDependencies = opts.onlyBuiltDependencies
const writeProjectManifest = await createProjectManifestWriter(opts.rootProjectManifestDir)
await writeProjectManifest(opts.rootProjectManifest)
}

View File

@@ -304,6 +304,7 @@ export type InstallCommandOptions = Pick<Config,
| 'sort'
| 'sharedWorkspaceLockfile'
| 'tag'
| 'onlyBuiltDependencies'
| 'optional'
| 'virtualStoreDir'
| 'workspaceConcurrency'
@@ -328,7 +329,7 @@ export type InstallCommandOptions = Pick<Config,
workspace?: boolean
includeOnlyPackageFiles?: boolean
confirmModulesPurge?: boolean
} & Partial<Pick<Config, 'modulesCacheMaxAge' | 'pnpmHomeDir' | 'preferWorkspacePackages' | 'useLockfile'>>
} & Partial<Pick<Config, 'modulesCacheMaxAge' | 'pnpmHomeDir' | 'preferWorkspacePackages' | 'useLockfile' | 'symlink'>>
export async function handler (opts: InstallCommandOptions): Promise<void> {
const include = {

6
pnpm-lock.yaml generated
View File

@@ -7792,6 +7792,9 @@ importers:
'@pnpm/error':
specifier: workspace:*
version: link:../../packages/error
'@pnpm/exec.pnpm-cli-runner':
specifier: workspace:*
version: link:../../exec/pnpm-cli-runner
'@pnpm/link-bins':
specifier: workspace:*
version: link:../../pkg-manager/link-bins
@@ -7801,9 +7804,6 @@ importers:
'@pnpm/pick-registry-for-package':
specifier: workspace:*
version: link:../../config/pick-registry-for-package
'@pnpm/plugin-commands-installation':
specifier: workspace:*
version: link:../../pkg-manager/plugin-commands-installation
'@pnpm/read-project-manifest':
specifier: workspace:*
version: link:../../pkg-manifest/read-project-manifest

View File

@@ -106,7 +106,7 @@ export async function main (inputArgv: string[]): Promise<void> {
rcOptionsTypes,
workspaceDir,
checkUnknownSetting: false,
ignoreNonAuthSettingsFromLocal: isDlxCommand,
ignoreNonAuthSettingsFromLocal: isDlxCommand || cmd === 'self-update',
}) as typeof config
if (!isExecutedByCorepack() && cmd !== 'setup' && config.wantedPackageManager != null) {
if (config.managePackageManagerVersions && config.wantedPackageManager?.name === 'pnpm') {

View File

@@ -1,14 +1,12 @@
import fs from 'fs'
import path from 'path'
import { type Config } from '@pnpm/config'
import { PnpmError } from '@pnpm/error'
import { globalWarn } from '@pnpm/logger'
import { getCurrentPackageName, packageManager } from '@pnpm/cli-meta'
import { packageManager } from '@pnpm/cli-meta'
import { prependDirsToPath } from '@pnpm/env.path'
import { getToolDirPath } from '@pnpm/tools.path'
import { installPnpmToTools } from '@pnpm/tools.plugin-commands-self-updater'
import spawn from 'cross-spawn'
import semver from 'semver'
import { pnpmCmds } from './cmd'
export async function switchCliVersion (config: Config): Promise<void> {
const pm = config.wantedPackageManager
@@ -22,28 +20,7 @@ export async function switchCliVersion (config: Config): Promise<void> {
globalWarn(`Cannot switch to pnpm@${pm.version}: you need to specify the version as "${pmVersion}"`)
return
}
const pkgName = getCurrentPackageName()
const dir = getToolDirPath({
pnpmHomeDir: config.pnpmHomeDir,
tool: {
name: pkgName,
version: pmVersion,
},
})
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true })
fs.writeFileSync(path.join(dir, 'package.json'), '{}')
await pnpmCmds.add(
{
...config,
dir,
lockfileDir: dir,
bin: path.join(dir, 'bin'),
},
[`${pkgName}@${pmVersion}`]
)
}
const wantedPnpmBinDir = path.join(dir, 'bin')
const { binDir: wantedPnpmBinDir } = await installPnpmToTools(pmVersion, config)
const pnpmEnv = prependDirsToPath([wantedPnpmBinDir])
if (!pnpmEnv.updated) {
// We throw this error to prevent an infinite recursive call of the same pnpm version.

View File

@@ -34,9 +34,9 @@
"@pnpm/client": "workspace:*",
"@pnpm/config": "workspace:*",
"@pnpm/error": "workspace:*",
"@pnpm/exec.pnpm-cli-runner": "workspace:*",
"@pnpm/link-bins": "workspace:*",
"@pnpm/pick-registry-for-package": "workspace:*",
"@pnpm/plugin-commands-installation": "workspace:*",
"@pnpm/read-project-manifest": "workspace:*",
"@pnpm/tools.path": "workspace:*",
"@zkochan/rimraf": "catalog:",

View File

@@ -1,3 +1,4 @@
import * as selfUpdate from './selfUpdate'
export { installPnpmToTools } from './installPnpmToTools'
export { selfUpdate }

View File

@@ -0,0 +1,64 @@
import fs from 'fs'
import path from 'path'
import { getCurrentPackageName } from '@pnpm/cli-meta'
import { runPnpmCli } from '@pnpm/exec.pnpm-cli-runner'
import { getToolDirPath } from '@pnpm/tools.path'
import { sync as rimraf } from '@zkochan/rimraf'
import { fastPathTemp as pathTemp } from 'path-temp'
import renameOverwrite from 'rename-overwrite'
import { type SelfUpdateCommandOptions } from './selfUpdate'
export interface InstallPnpmToToolsResult {
binDir: string
baseDir: string
alreadyExisted: boolean
}
export async function installPnpmToTools (pnpmVersion: string, opts: SelfUpdateCommandOptions): Promise<InstallPnpmToToolsResult> {
const currentPkgName = getCurrentPackageName()
const dir = getToolDirPath({
pnpmHomeDir: opts.pnpmHomeDir,
tool: {
name: currentPkgName,
version: pnpmVersion,
},
})
const binDir = path.join(dir, 'bin')
const alreadyExisted = fs.existsSync(binDir)
if (alreadyExisted) {
return {
alreadyExisted,
baseDir: dir,
binDir,
}
}
const stage = pathTemp(dir)
fs.mkdirSync(stage, { recursive: true })
fs.writeFileSync(path.join(stage, 'package.json'), '{}')
try {
// The reason we don't just run add.handler is that at this point we might have settings from local config files
// that we don't want to use while installing the pnpm CLI.
runPnpmCli([
'add',
`${currentPkgName}@${pnpmVersion}`,
'--loglevel=error',
'--allow-build=@pnpm/exe',
// We want to avoid symlinks because of the rename step,
// which breaks the junctions on Windows.
'--config.node-linker=hoisted',
`--config.bin=${path.join(stage, 'bin')}`,
], { cwd: stage })
renameOverwrite.sync(stage, dir)
} catch (err: unknown) {
try {
rimraf(stage)
} catch {} // eslint-disable-line:no-empty
throw err
}
return {
alreadyExisted,
baseDir: dir,
binDir,
}
}

View File

@@ -1,21 +1,16 @@
import fs from 'fs'
import path from 'path'
import { docsUrl } from '@pnpm/cli-utils'
import { getCurrentPackageName, packageManager, isExecutedByCorepack } from '@pnpm/cli-meta'
import { packageManager, isExecutedByCorepack } from '@pnpm/cli-meta'
import { createResolver } from '@pnpm/client'
import { pickRegistryForPackage } from '@pnpm/pick-registry-for-package'
import { type Config, types as allTypes } from '@pnpm/config'
import { PnpmError } from '@pnpm/error'
import { globalWarn } from '@pnpm/logger'
import { add, type InstallCommandOptions } from '@pnpm/plugin-commands-installation'
import { readProjectManifest } from '@pnpm/read-project-manifest'
import { getToolDirPath } from '@pnpm/tools.path'
import { linkBins } from '@pnpm/link-bins'
import { sync as rimraf } from '@zkochan/rimraf'
import { fastPathTemp as pathTemp } from 'path-temp'
import pick from 'ramda/src/pick'
import renameOverwrite from 'rename-overwrite'
import renderHelp from 'render-help'
import { installPnpmToTools } from './installPnpmToTools'
export function rcOptionsTypes (): Record<string, unknown> {
return pick([], allTypes)
@@ -43,7 +38,18 @@ export function help (): string {
})
}
export type SelfUpdateCommandOptions = InstallCommandOptions & Pick<Config, 'wantedPackageManager' | 'managePackageManagerVersions'>
export type SelfUpdateCommandOptions = Pick<Config,
| 'cacheDir'
| 'dir'
| 'lockfileDir'
| 'managePackageManagerVersions'
| 'modulesDir'
| 'pnpmHomeDir'
| 'rawConfig'
| 'registries'
| 'rootProjectManifestDir'
| 'wantedPackageManager'
>
export async function handler (
opts: SelfUpdateCommandOptions,
@@ -75,49 +81,13 @@ export async function handler (
return `The current project has been updated to use pnpm v${resolution.manifest.version}`
}
const currentPkgName = getCurrentPackageName()
const dir = getToolDirPath({
pnpmHomeDir: opts.pnpmHomeDir,
tool: {
name: currentPkgName,
version: resolution.manifest.version,
},
})
const alreadyExists = fs.existsSync(dir)
if (!alreadyExists) {
const stage = pathTemp(dir)
fs.mkdirSync(stage, { recursive: true })
fs.writeFileSync(path.join(stage, 'package.json'), '{}')
try {
await add.handler(
{
...opts,
dir: stage,
lockfileDir: stage,
// We want to avoid symlinks because of the rename step,
// which breaks the junctions on Windows.
nodeLinker: 'hoisted',
// This won't be used but there is currently no way to skip the bin creation
// and we can't create the bin shims in the pnpm home directory
// because the stage directory will be renamed.
bin: path.join(stage, 'node_modules/.bin'),
},
[`${currentPkgName}@${resolution.manifest.version}`]
)
renameOverwrite.sync(stage, dir)
} catch (err: unknown) {
try {
rimraf(stage)
} catch {} // eslint-disable-line:no-empty
throw err
}
}
await linkBins(path.join(dir, opts.modulesDir ?? 'node_modules'), opts.pnpmHomeDir,
const { baseDir, alreadyExisted } = await installPnpmToTools(resolution.manifest.version, opts)
await linkBins(path.join(baseDir, opts.modulesDir ?? 'node_modules'), opts.pnpmHomeDir,
{
warn: globalWarn,
}
)
return alreadyExists
? `The ${pref} version, v${resolution.manifest.version}, is already present on the system. It was activated by linking it from ${dir}.`
return alreadyExisted
? `The ${pref} version, v${resolution.manifest.version}, is already present on the system. It was activated by linking it from ${baseDir}.`
: undefined
}

View File

@@ -151,7 +151,9 @@ test('self-update links pnpm that is already present on the disk', async () => {
.get('/pnpm')
.reply(200, createMetadata('9.2.0', opts.registries.default))
const latestPnpmDir = path.join(opts.pnpmHomeDir, '.tools/pnpm/9.2.0/node_modules/pnpm')
const baseDir = path.join(opts.pnpmHomeDir, '.tools/pnpm/9.2.0')
fs.mkdirSync(path.join(baseDir, 'bin'), { recursive: true })
const latestPnpmDir = path.join(baseDir, 'node_modules/pnpm')
fs.mkdirSync(latestPnpmDir, { recursive: true })
fs.writeFileSync(path.join(latestPnpmDir, 'package.json'), JSON.stringify({ name: 'pnpm', bin: 'bin.js' }), 'utf8')
fs.writeFileSync(path.join(latestPnpmDir, 'bin.js'), `#!/usr/bin/env node

View File

@@ -27,6 +27,9 @@
{
"path": "../../env/path"
},
{
"path": "../../exec/pnpm-cli-runner"
},
{
"path": "../../packages/error"
},
@@ -36,9 +39,6 @@
{
"path": "../../pkg-manager/link-bins"
},
{
"path": "../../pkg-manager/plugin-commands-installation"
},
{
"path": "../../pkg-manifest/read-project-manifest"
},