fix(config): conflicts between global and local in whether to run scripts (#9714)

close #9628
This commit is contained in:
Khải
2025-07-07 21:17:02 +07:00
committed by GitHub
parent b656f8aea1
commit 623da6fd27
4 changed files with 157 additions and 3 deletions

View File

@@ -0,0 +1,6 @@
---
"@pnpm/config": minor
"pnpm": patch
---
Prevent conflicts between local projects' config and the global config in `dangerously-allow-all-builds`, `only-built-dependencies`, `only-built-dependencies-file`, and `never-built-dependencies` [#9628](https://github.com/pnpm/pnpm/issues/9628).

View File

@@ -0,0 +1,28 @@
import { type Config } from './Config'
export const DEPS_BUILD_CONFIG_KEYS = [
'dangerouslyAllowAllBuilds',
'onlyBuiltDependencies',
'onlyBuiltDependenciesFile',
'neverBuiltDependencies',
] as const satisfies Array<keyof Config>
export type DepsBuildConfigKey = typeof DEPS_BUILD_CONFIG_KEYS[number]
export type DepsBuildConfig = Partial<Pick<Config, DepsBuildConfigKey>>
export const hasDependencyBuildOptions = (config: Config): boolean => DEPS_BUILD_CONFIG_KEYS.some(key => config[key] != null)
/**
* Remove deps build settings from a config.
* @param targetConfig - Target config object whose deps build settings need to be removed.
* @returns Record of removed settings.
*/
export function extractAndRemoveDependencyBuildOptions (targetConfig: Config): DepsBuildConfig {
const depsBuildConfig: DepsBuildConfig = {}
for (const key of DEPS_BUILD_CONFIG_KEYS) {
depsBuildConfig[key] = targetConfig[key] as any // eslint-disable-line
delete targetConfig[key]
}
return depsBuildConfig
}

View File

@@ -20,6 +20,7 @@ import pathAbsolute from 'path-absolute'
import which from 'which'
import { inheritAuthConfig } from './auth'
import { checkGlobalBinDir } from './checkGlobalBinDir'
import { hasDependencyBuildOptions, extractAndRemoveDependencyBuildOptions } from './dependencyBuildOptions'
import { getNetworkConfigs } from './getNetworkConfigs'
import { transformPathKeys } from './transformPath'
import { getCacheDir, getConfigDir, getDataDir, getStateDir } from './dirs'
@@ -225,9 +226,13 @@ export async function getConfig (opts: {
.filter(([_, value]) => typeof value !== 'undefined')
.map(([name, value]) => [camelcase(name, { locale: 'en-US' }), value])
)
const pnpmConfig: ConfigWithDeprecatedSettings = Object.assign(Object.fromEntries(
rcOptions.map((configKey) => [camelcase(configKey, { locale: 'en-US' }), npmConfig.get(configKey)]) as any, // eslint-disable-line
), configFromCliOpts) as unknown as ConfigWithDeprecatedSettings
const pnpmConfig: ConfigWithDeprecatedSettings = Object.fromEntries(
rcOptions.map((configKey) => [camelcase(configKey, { locale: 'en-US' }), npmConfig.get(configKey)])
) as ConfigWithDeprecatedSettings
const globalDepsBuildConfig = extractAndRemoveDependencyBuildOptions(pnpmConfig)
Object.assign(pnpmConfig, configFromCliOpts)
// Resolving the current working directory to its actual location is crucial.
// This prevents potential inconsistencies in the future, especially when processing or mapping subdirectories.
const cwd = fs.realpathSync(betterPathResolve(cliOptions.dir ?? npmConfig.localPrefix))
@@ -374,6 +379,14 @@ export async function getConfig (opts: {
}
}
}
if (opts.cliOptions['global']) {
extractAndRemoveDependencyBuildOptions(pnpmConfig)
Object.assign(pnpmConfig, globalDepsBuildConfig)
} else {
if (!hasDependencyBuildOptions(pnpmConfig)) {
Object.assign(pnpmConfig, globalDepsBuildConfig)
}
}
if (opts.cliOptions['save-peer']) {
if (opts.cliOptions['save-prod']) {
throw new PnpmError('CONFIG_CONFLICT_PEER_CANNOT_BE_PROD_DEP', 'A package cannot be a peer dependency and a prod dependency at the same time')

View File

@@ -3,6 +3,7 @@ import PATH_NAME from 'path-name'
import fs from 'fs'
import { LAYOUT_VERSION } from '@pnpm/constants'
import { prepare } from '@pnpm/prepare'
import { type ProjectManifest } from '@pnpm/types'
import isWindows from 'is-windows'
import {
addDistTag,
@@ -89,6 +90,112 @@ test('run lifecycle events of global packages in correct working directory', asy
expect(fs.existsSync(path.join(globalPkgDir, 'node_modules/@pnpm.e2e/postinstall-calls-pnpm/created-by-postinstall'))).toBeTruthy()
})
test('dangerously-allow-all-builds=true in global config', async () => {
// the directory structure below applies only to Linux
if (process.platform !== 'linux') return
const manifest: ProjectManifest = {
name: 'local',
version: '0.0.0',
private: true,
pnpm: {
onlyBuiltDependencies: [], // don't allow any dependencies to be built
},
}
const project = prepare(manifest)
const home = path.resolve('..', 'home/username')
const cfgHome = path.resolve(home, '.config')
const pnpmCfgDir = path.resolve(cfgHome, 'pnpm')
const pnpmRcFile = path.join(pnpmCfgDir, 'rc')
const global = path.resolve('..', 'global')
const pnpmHome = path.join(global, 'pnpm')
const globalPkgDir = path.join(pnpmHome, 'global', String(LAYOUT_VERSION))
fs.mkdirSync(pnpmCfgDir, { recursive: true })
fs.writeFileSync(pnpmRcFile, [
'reporter=append-only',
'dangerously-allow-all-builds=true',
].join('\n'))
const env = {
[PATH_NAME]: `${pnpmHome}${path.delimiter}${process.env[PATH_NAME]!}`,
HOME: home,
XDG_CONFIG_HOME: cfgHome,
PNPM_HOME: pnpmHome,
XDG_DATA_HOME: global,
}
// global install should run scripts
await execPnpm(['install', '-g', '@pnpm.e2e/postinstall-calls-pnpm@1.0.0'], { env })
expect(fs.readdirSync(path.join(globalPkgDir, 'node_modules/@pnpm.e2e/postinstall-calls-pnpm'))).toContain('created-by-postinstall')
// local config should override global config
await execPnpm(['add', '@pnpm.e2e/postinstall-calls-pnpm@1.0.0'], { env })
expect(fs.readdirSync(path.resolve('node_modules/@pnpm.e2e/postinstall-calls-pnpm'))).not.toContain('created-by-postinstall')
// global config should be used if local config did not specify
delete manifest.pnpm!.onlyBuiltDependencies
project.writePackageJson(manifest)
fs.rmSync('node_modules', { recursive: true })
fs.rmSync('pnpm-lock.yaml')
await execPnpm(['add', '@pnpm.e2e/postinstall-calls-pnpm@1.0.0'], { env })
expect(fs.readdirSync(path.resolve('node_modules/@pnpm.e2e/postinstall-calls-pnpm'))).toContain('created-by-postinstall')
})
test('dangerously-allow-all-builds=false in global config', async () => {
// the directory structure below applies only to Linux
if (process.platform !== 'linux') return
const manifest: ProjectManifest = {
name: 'local',
version: '0.0.0',
private: true,
pnpm: {
onlyBuiltDependencies: ['@pnpm.e2e/postinstall-calls-pnpm'],
},
}
const project = prepare(manifest)
const home = path.resolve('..', 'home/username')
const cfgHome = path.resolve(home, '.config')
const pnpmCfgDir = path.resolve(cfgHome, 'pnpm')
const pnpmRcFile = path.join(pnpmCfgDir, 'rc')
const global = path.resolve('..', 'global')
const pnpmHome = path.join(global, 'pnpm')
const globalPkgDir = path.join(pnpmHome, 'global', String(LAYOUT_VERSION))
fs.mkdirSync(pnpmCfgDir, { recursive: true })
fs.writeFileSync(pnpmRcFile, [
'reporter=append-only',
'dangerously-allow-all-builds=false',
].join('\n'))
const env = {
[PATH_NAME]: `${pnpmHome}${path.delimiter}${process.env[PATH_NAME]!}`,
HOME: home,
XDG_CONFIG_HOME: cfgHome,
PNPM_HOME: pnpmHome,
XDG_DATA_HOME: global,
}
// global install should run scripts
await execPnpm(['install', '-g', '@pnpm.e2e/postinstall-calls-pnpm@1.0.0'], { env })
expect(fs.readdirSync(path.join(globalPkgDir, 'node_modules/@pnpm.e2e/postinstall-calls-pnpm'))).not.toContain('created-by-postinstall')
// local config should override global config
await execPnpm(['add', '@pnpm.e2e/postinstall-calls-pnpm@1.0.0'], { env })
expect(fs.readdirSync(path.resolve('node_modules/@pnpm.e2e/postinstall-calls-pnpm'))).toContain('created-by-postinstall')
// global config should be used if local config did not specify
delete manifest.pnpm!.onlyBuiltDependencies
project.writePackageJson(manifest)
fs.rmSync('node_modules', { recursive: true })
fs.rmSync('pnpm-lock.yaml')
await execPnpm(['add', '@pnpm.e2e/postinstall-calls-pnpm@1.0.0'], { env })
expect(fs.readdirSync(path.resolve('node_modules/@pnpm.e2e/postinstall-calls-pnpm'))).not.toContain('created-by-postinstall')
})
test('global update to latest', async () => {
prepare()
const global = path.resolve('..', 'global')