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
parent 17c4f6ad29
commit 52ca5a4f5d
16 changed files with 150 additions and 104 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

@@ -62,11 +62,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(),
@@ -318,7 +320,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']) {
@@ -480,25 +484,27 @@ export async function getConfig (opts: {
pnpmConfig.workspaceConcurrency = getWorkspaceConcurrency(pnpmConfig.workspaceConcurrency)
if (!pnpmConfig.ignorePnpmfile) {
pnpmConfig.hooks = requireHooks(pnpmConfig.lockfileDir ?? pnpmConfig.dir, pnpmConfig)
}
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) {
if (!pnpmConfig.ignorePnpmfile) {
pnpmConfig.hooks = requireHooks(pnpmConfig.lockfileDir ?? pnpmConfig.dir, pnpmConfig)
}
if (pnpmConfig.rootProjectManifest.packageManager) {
pnpmConfig.wantedPackageManager = parsePackageManager(pnpmConfig.rootProjectManifest.packageManager)
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.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)
pnpmConfig.workspacePackagePatterns = cliOptions['workspace-packages'] as string[] ?? workspaceManifest?.packages
pnpmConfig.catalogs = getCatalogsFromWorkspaceManifest(workspaceManifest)
}
}
pnpmConfig.failedToLoadBuiltInConfig = failedToLoadBuiltInConfig

View File

@@ -10,7 +10,6 @@ import { add } from '@pnpm/plugin-commands-installation'
import { readPackageJsonFromDir } from '@pnpm/read-package-json'
import { getBinsFromPackageManifest } from '@pnpm/package-bins'
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'
@@ -81,9 +80,7 @@ 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'], opts),
...opts,
bin: path.join(cachedDir, 'node_modules/.bin'),
dir: cachedDir,
lockfileDir: cachedDir,
@@ -92,6 +89,8 @@ export async function handler (
saveDev: false,
saveOptional: false,
savePeer: false,
symlink: true,
workspaceDir: undefined,
}, pkgs)
try {
await symlinkDir(cachedDir, cacheLink, { overwrite: true })

View File

@@ -295,6 +295,7 @@ export type InstallCommandOptions = Pick<Config,
| 'sort'
| 'sharedWorkspaceLockfile'
| 'tag'
| 'onlyBuiltDependencies'
| 'optional'
| 'virtualStoreDir'
| 'workspaceConcurrency'
@@ -318,7 +319,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 = {

17
pnpm-lock.yaml generated
View File

@@ -24,6 +24,9 @@ catalogs:
'@pnpm/exec':
specifier: ^2.0.0
version: 2.0.0
'@pnpm/exec.pnpm-cli-runner':
specifier: 1000.0.0
version: 1000.0.0
'@pnpm/fs.packlist':
specifier: 2.0.0
version: 2.0.0
@@ -7555,6 +7558,9 @@ importers:
'@pnpm/error':
specifier: workspace:*
version: link:../../packages/error
'@pnpm/exec.pnpm-cli-runner':
specifier: 'catalog:'
version: 1000.0.0
'@pnpm/link-bins':
specifier: workspace:*
version: link:../../pkg-manager/link-bins
@@ -7564,9 +7570,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
@@ -8870,6 +8873,10 @@ packages:
resolution: {integrity: sha512-OIYhG7HQh4zUFh2s8/6bp7glVRjNxms7bpzXVOLV7pyRa+rSYFmqJ8zDsBC64k58nuaxS85Ip+SCDjFxsFGeOg==}
engines: {node: '>=18.12'}
'@pnpm/exec.pnpm-cli-runner@1000.0.0':
resolution: {integrity: sha512-5oRH9X7S1UKozhkOxMfn7i7H7fZSWRFlX6R/xXoTnLCIe7ab2tU3EN98D739areH53thhjk8bq9osklcOZGM7g==}
engines: {node: '>=18.12'}
'@pnpm/exec@2.0.0':
resolution: {integrity: sha512-b5ALfWEOFQprWKntN7MF8XWCyslBk2c8u20GEDcDDQOs6c0HyHlWxX5lig8riQKdS000U6YyS4L4b32NOleXAQ==}
engines: {node: '>=10'}
@@ -15881,6 +15888,10 @@ snapshots:
dependencies:
'@pnpm/constants': 10.0.0
'@pnpm/exec.pnpm-cli-runner@1000.0.0':
dependencies:
execa: safe-execa@0.1.2
'@pnpm/exec@2.0.0':
dependencies:
'@pnpm/self-installer': 2.2.1

View File

@@ -45,6 +45,7 @@ catalog:
"@pnpm/colorize-semver-diff": ^1.0.1
"@pnpm/config.env-replace": 3.0.0
"@pnpm/exec": ^2.0.0
"@pnpm/exec.pnpm-cli-runner": 1000.0.0
"@pnpm/fs.packlist": 2.0.0
"@pnpm/log.group": 3.0.0
"@pnpm/meta-updater": 2.0.3

View File

@@ -104,7 +104,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": "catalog:",
"@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

@@ -149,7 +149,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

@@ -36,9 +36,6 @@
{
"path": "../../pkg-manager/link-bins"
},
{
"path": "../../pkg-manager/plugin-commands-installation"
},
{
"path": "../../pkg-manifest/read-project-manifest"
},