mirror of
https://github.com/pnpm/pnpm.git
synced 2025-12-24 07:38:12 -05:00
fix(config): conflicts between global and local in whether to run scripts (#9714)
close #9628
This commit is contained in:
6
.changeset/cuddly-plants-sleep.md
Normal file
6
.changeset/cuddly-plants-sleep.md
Normal 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).
|
||||
28
config/config/src/dependencyBuildOptions.ts
Normal file
28
config/config/src/dependencyBuildOptions.ts
Normal 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
|
||||
}
|
||||
@@ -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')
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user