mirror of
https://github.com/pnpm/pnpm.git
synced 2026-05-12 10:11:42 -04:00
fix(config): honor NPM_CONFIG_USERCONFIG as a low-priority fallback (#11545)
* fix(config): honor NPM_CONFIG_USERCONFIG as a low-priority fallback
Restores compatibility with environments that point npm at a custom
.npmrc via NPM_CONFIG_USERCONFIG (e.g. actions/setup-node writing to
${runner.temp}/.npmrc), which silently broke after the v11 env var
prefix change. PNPM-prefixed env vars and npmrcAuthFile from the
global config.yaml continue to take precedence.
Closes #11539
* fix(config): treat empty NPM_CONFIG_USERCONFIG as unset
`??` accepts an empty string as a defined value, so an exported but
unset NPM_CONFIG_USERCONFIG would short-circuit the fallback chain and
make normalizePath('') resolve to process.cwd(). Mirror readEnvVar's
empty-string-to-undefined coercion via a readNpmEnvVar helper so the
fallback to ~/.npmrc works as expected.
This commit is contained in:
6
.changeset/honor-npm-config-userconfig.md
Normal file
6
.changeset/honor-npm-config-userconfig.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@pnpm/config.reader": patch
|
||||
"pnpm": patch
|
||||
---
|
||||
|
||||
Honor `NPM_CONFIG_USERCONFIG` (and its lowercase `npm_config_userconfig` form) as a low-priority fallback when locating the user-level `.npmrc`. This restores compatibility with environments that point npm at a custom auth file via that env var — most notably `actions/setup-node`, which writes registry credentials to `${runner.temp}/.npmrc` and exports `NPM_CONFIG_USERCONFIG` to reference it. Without this, GitHub Actions workflows using `actions/setup-node` to authenticate to private registries broke after upgrading to pnpm v11. PNPM-prefixed env vars and `npmrcAuthFile` from the global `config.yaml` continue to take precedence [#11539](https://github.com/pnpm/pnpm/issues/11539).
|
||||
@@ -221,12 +221,17 @@ export async function getConfig (opts: {
|
||||
// also have to peek at the relevant env vars here in order for
|
||||
// PNPM_CONFIG_NPMRC_AUTH_FILE / PNPM_CONFIG_USERCONFIG (and their lowercase
|
||||
// equivalents) to actually decide which user-level .npmrc gets read.
|
||||
// npm_config_userconfig is honored as a low-priority compatibility fallback
|
||||
// so that environments that point npm at a custom .npmrc (e.g. actions/setup-node
|
||||
// writing to ${runner.temp}/.npmrc) keep working without requiring users to
|
||||
// rename the env var to its PNPM_CONFIG_* equivalent.
|
||||
const globalYamlConfigForNpmrcAuthFile = await readWorkspaceManifest(configDir, GLOBAL_CONFIG_YAML_FILENAME)
|
||||
const npmrcAuthFile = cliOptions['npmrc-auth-file'] as string | undefined
|
||||
?? cliOptions.userconfig as string | undefined
|
||||
?? readEnvVar(env, 'npmrc_auth_file')
|
||||
?? readEnvVar(env, 'userconfig')
|
||||
?? globalYamlConfigForNpmrcAuthFile?.npmrcAuthFile
|
||||
?? readNpmEnvVar(env, 'userconfig')
|
||||
|
||||
const npmrcResult = loadNpmrcConfig({
|
||||
cliOptions,
|
||||
@@ -696,6 +701,14 @@ function readEnvVar (env: NodeJS.ProcessEnv, key: string): string | undefined {
|
||||
return value !== '' ? value : undefined
|
||||
}
|
||||
|
||||
// Same shape as readEnvVar but for the `npm_config_<key>` family. Used as a
|
||||
// low-priority compatibility shim so that npm-style env vars (e.g.
|
||||
// NPM_CONFIG_USERCONFIG written by actions/setup-node) keep working.
|
||||
function readNpmEnvVar (env: NodeJS.ProcessEnv, key: string): string | undefined {
|
||||
const value = env[`npm_config_${key}`] ?? env[`NPM_CONFIG_${key.toUpperCase()}`]
|
||||
return value !== '' ? value : undefined
|
||||
}
|
||||
|
||||
function getWantedPackageManager (manifest: ProjectManifest): { pm?: WantedPackageManager, warnings: string[] } {
|
||||
const warnings: string[] = []
|
||||
const pmFromDevEngines = parseDevEnginesPackageManager(manifest.devEngines)
|
||||
|
||||
@@ -1217,6 +1217,91 @@ test('getConfig() prefers pnpm_config_userconfig over PNPM_CONFIG_USERCONFIG whe
|
||||
expect(config.userConfig).toEqual({ registry: 'https://lower.example.test' })
|
||||
})
|
||||
|
||||
// actions/setup-node writes auth to ${runner.temp}/.npmrc and sets NPM_CONFIG_USERCONFIG;
|
||||
// pnpm honors it as a low-priority compatibility fallback for that flow.
|
||||
test('getConfig() reads userconfig from NPM_CONFIG_USERCONFIG env var', async () => {
|
||||
prepareEmpty()
|
||||
fs.mkdirSync('user-home')
|
||||
fs.writeFileSync(path.resolve('user-home', '.npmrc'), 'registry = https://registry.example.test', 'utf-8')
|
||||
const { config } = await getConfig({
|
||||
cliOptions: {},
|
||||
env: {
|
||||
...env,
|
||||
NPM_CONFIG_USERCONFIG: path.resolve('user-home', '.npmrc'),
|
||||
},
|
||||
packageManager: {
|
||||
name: 'pnpm',
|
||||
version: '1.0.0',
|
||||
},
|
||||
})
|
||||
expect(config.userConfig).toEqual({ registry: 'https://registry.example.test' })
|
||||
})
|
||||
|
||||
test('getConfig() reads userconfig from npm_config_userconfig env var', async () => {
|
||||
prepareEmpty()
|
||||
fs.mkdirSync('user-home')
|
||||
fs.writeFileSync(path.resolve('user-home', '.npmrc'), 'registry = https://registry.example.test', 'utf-8')
|
||||
const { config } = await getConfig({
|
||||
cliOptions: {},
|
||||
env: {
|
||||
...env,
|
||||
npm_config_userconfig: path.resolve('user-home', '.npmrc'),
|
||||
},
|
||||
packageManager: {
|
||||
name: 'pnpm',
|
||||
version: '1.0.0',
|
||||
},
|
||||
})
|
||||
expect(config.userConfig).toEqual({ registry: 'https://registry.example.test' })
|
||||
})
|
||||
|
||||
test('getConfig() prefers PNPM_CONFIG_USERCONFIG over NPM_CONFIG_USERCONFIG when both are set', async () => {
|
||||
prepareEmpty()
|
||||
fs.mkdirSync('user-home')
|
||||
fs.writeFileSync(path.resolve('user-home', 'pnpm.npmrc'), 'registry = https://pnpm.example.test', 'utf-8')
|
||||
fs.writeFileSync(path.resolve('user-home', 'npm.npmrc'), 'registry = https://npm.example.test', 'utf-8')
|
||||
const { config } = await getConfig({
|
||||
cliOptions: {},
|
||||
env: {
|
||||
...env,
|
||||
PNPM_CONFIG_USERCONFIG: path.resolve('user-home', 'pnpm.npmrc'),
|
||||
NPM_CONFIG_USERCONFIG: path.resolve('user-home', 'npm.npmrc'),
|
||||
},
|
||||
packageManager: {
|
||||
name: 'pnpm',
|
||||
version: '1.0.0',
|
||||
},
|
||||
})
|
||||
expect(config.userConfig).toEqual({ registry: 'https://pnpm.example.test' })
|
||||
})
|
||||
|
||||
// An empty NPM_CONFIG_USERCONFIG (e.g. `export NPM_CONFIG_USERCONFIG=`) must be
|
||||
// treated as unset. Otherwise it short-circuits the fallback chain and resolves
|
||||
// to the cwd, returning an empty/invalid auth config instead of ~/.npmrc.
|
||||
test('getConfig() ignores an empty NPM_CONFIG_USERCONFIG and falls back to ~/.npmrc', async () => {
|
||||
prepareEmpty()
|
||||
const homedirSpy = jest.spyOn(os, 'homedir').mockReturnValue(path.resolve('user-home'))
|
||||
try {
|
||||
fs.mkdirSync('user-home')
|
||||
fs.writeFileSync(path.resolve('user-home', '.npmrc'), 'registry = https://home.example.test', 'utf-8')
|
||||
const { config } = await getConfig({
|
||||
cliOptions: {},
|
||||
env: {
|
||||
...env,
|
||||
NPM_CONFIG_USERCONFIG: '',
|
||||
npm_config_userconfig: '',
|
||||
},
|
||||
packageManager: {
|
||||
name: 'pnpm',
|
||||
version: '1.0.0',
|
||||
},
|
||||
})
|
||||
expect(config.userConfig).toEqual({ registry: 'https://home.example.test' })
|
||||
} finally {
|
||||
homedirSpy.mockRestore()
|
||||
}
|
||||
})
|
||||
|
||||
test('getConfig() sets sideEffectsCacheRead and sideEffectsCacheWrite when side-effects-cache is set', async () => {
|
||||
const { config } = await getConfig({
|
||||
cliOptions: {
|
||||
@@ -1869,6 +1954,37 @@ describe('global config.yaml', () => {
|
||||
|
||||
expect(config.httpsProxy).toBe('http://cli-proxy.example.com:7070')
|
||||
})
|
||||
|
||||
// npmrcAuthFile in global config.yaml is a deliberate pnpm-native setting and should
|
||||
// not be silently overridden by an ambient NPM_CONFIG_USERCONFIG (e.g. from a CI runner).
|
||||
test('npmrcAuthFile from global config.yaml takes precedence over NPM_CONFIG_USERCONFIG', async () => {
|
||||
prepareEmpty()
|
||||
fs.mkdirSync('user-home')
|
||||
fs.writeFileSync(path.resolve('user-home', 'yaml.npmrc'), 'registry = https://yaml.example.test', 'utf-8')
|
||||
fs.writeFileSync(path.resolve('user-home', 'npm.npmrc'), 'registry = https://npm.example.test', 'utf-8')
|
||||
|
||||
fs.mkdirSync('.config/pnpm', { recursive: true })
|
||||
writeYamlFileSync('.config/pnpm/config.yaml', {
|
||||
npmrcAuthFile: path.resolve('user-home', 'yaml.npmrc'),
|
||||
})
|
||||
|
||||
process.env.XDG_CONFIG_HOME = path.resolve('.config')
|
||||
|
||||
const { config } = await getConfig({
|
||||
cliOptions: {},
|
||||
env: {
|
||||
...env,
|
||||
NPM_CONFIG_USERCONFIG: path.resolve('user-home', 'npm.npmrc'),
|
||||
},
|
||||
packageManager: {
|
||||
name: 'pnpm',
|
||||
version: '1.0.0',
|
||||
},
|
||||
workspaceDir: process.cwd(),
|
||||
})
|
||||
|
||||
expect(config.userConfig).toEqual({ registry: 'https://yaml.example.test' })
|
||||
})
|
||||
})
|
||||
|
||||
test('proxy settings are still read from .npmrc', async () => {
|
||||
|
||||
Reference in New Issue
Block a user