fix: match read/write priority when both pnpm-workspace.yaml and .npmrc exist (#10073)

* fix: match read/write priority when both pnpm-workspace.yaml and .npmrc exist

* chore: update help message

close #10072
This commit is contained in:
Ryo Matsukawa
2025-10-12 22:43:57 +09:00
committed by GitHub
parent 986516756c
commit eaaf8cb965
5 changed files with 106 additions and 12 deletions

View File

@@ -0,0 +1,6 @@
---
"@pnpm/plugin-commands-config": patch
pnpm: patch
---
When both `pnpm-workspace.yaml` and `.npmrc` exist, `pnpm config set --location=project` now writes to `pnpm-workspace.yaml` (matching read priority) [#10072](https://github.com/pnpm/pnpm/issues/10072).

View File

@@ -54,7 +54,7 @@ export function help (): string {
shortAlias: '-g', shortAlias: '-g',
}, },
{ {
description: 'When set to "project", the .npmrc file at the nearest package.json will be used. If no .npmrc file is present in the directory, the setting will be written to a pnpm-workspace.yaml file.', description: 'When set to "project", the pnpm-workspace.yaml file will be used if it exists. If only .npmrc exists, it will be used. If neither exists, a pnpm-workspace.yaml file will be created.',
name: '--location <project|global>', name: '--location <project|global>',
}, },
{ {

View File

@@ -1,5 +1,3 @@
import fs from 'fs'
import path from 'path'
import util from 'util' import util from 'util'
import { types } from '@pnpm/config' import { types } from '@pnpm/config'
import { PnpmError } from '@pnpm/error' import { PnpmError } from '@pnpm/error'
@@ -11,6 +9,7 @@ import kebabCase from 'lodash.kebabcase'
import { readIniFile } from 'read-ini-file' import { readIniFile } from 'read-ini-file'
import { writeIniFile } from 'write-ini-file' import { writeIniFile } from 'write-ini-file'
import { type ConfigCommandOptions } from './ConfigCommandOptions.js' import { type ConfigCommandOptions } from './ConfigCommandOptions.js'
import { getConfigFilePath } from './getConfigFilePath.js'
import { isStrictlyKebabCase } from './isStrictlyKebabCase.js' import { isStrictlyKebabCase } from './isStrictlyKebabCase.js'
import { settingShouldFallBackToNpm } from './settingShouldFallBackToNpm.js' import { settingShouldFallBackToNpm } from './settingShouldFallBackToNpm.js'
@@ -36,8 +35,17 @@ export async function configSet (opts: ConfigCommandOptions, key: string, valueP
} }
throw new PnpmError('CONFIG_SET_AUTH_NON_STRING', `Cannot set ${key} to a non-string value (${JSON.stringify(value)})`) throw new PnpmError('CONFIG_SET_AUTH_NON_STRING', `Cannot set ${key} to a non-string value (${JSON.stringify(value)})`)
} }
if (opts.global === true || fs.existsSync(path.join(opts.dir, '.npmrc'))) {
const configPath = opts.global ? path.join(opts.configDir, 'rc') : path.join(opts.dir, '.npmrc') const { configPath, isWorkspaceYaml } = getConfigFilePath(opts)
if (isWorkspaceYaml) {
key = camelCase(key)
await updateWorkspaceManifest(opts.workspaceDir ?? opts.dir, {
updatedFields: ({
[key]: castField(value, kebabCase(key)),
}),
})
} else {
const settings = await safeReadIniFile(configPath) const settings = await safeReadIniFile(configPath)
key = kebabCase(key) key = kebabCase(key)
if (value == null) { if (value == null) {
@@ -47,14 +55,7 @@ export async function configSet (opts: ConfigCommandOptions, key: string, valueP
settings[key] = value settings[key] = value
} }
await writeIniFile(configPath, settings) await writeIniFile(configPath, settings)
return
} }
key = camelCase(key)
await updateWorkspaceManifest(opts.workspaceDir ?? opts.dir, {
updatedFields: ({
[key]: castField(value, kebabCase(key)),
}),
})
} }
function castField (value: unknown, key: string) { function castField (value: unknown, key: string) {

View File

@@ -0,0 +1,42 @@
import fs from 'fs'
import path from 'path'
import { type ConfigCommandOptions } from './ConfigCommandOptions.js'
interface ConfigFilePathInfo {
configPath: string
isWorkspaceYaml: boolean
}
/**
* Priority: pnpm-workspace.yaml > .npmrc > default to pnpm-workspace.yaml
*/
export function getConfigFilePath (opts: Pick<ConfigCommandOptions, 'global' | 'configDir' | 'dir'>): ConfigFilePathInfo {
if (opts.global) {
return {
configPath: path.join(opts.configDir, 'rc'),
isWorkspaceYaml: false,
}
}
const workspaceYamlPath = path.join(opts.dir, 'pnpm-workspace.yaml')
if (fs.existsSync(workspaceYamlPath)) {
return {
configPath: workspaceYamlPath,
isWorkspaceYaml: true,
}
}
const npmrcPath = path.join(opts.dir, '.npmrc')
if (fs.existsSync(npmrcPath)) {
return {
configPath: npmrcPath,
isWorkspaceYaml: false,
}
}
// If neither exists, return pnpm-workspace.yaml
return {
configPath: workspaceYamlPath,
isWorkspaceYaml: true,
}
}

View File

@@ -290,3 +290,48 @@ test('config set with location=project and json=true', async () => {
}, },
}) })
}) })
test('config set when both pnpm-workspace.yaml and .npmrc exist, pnpm-workspace.yaml has priority', async () => {
const tmp = tempDir()
const configDir = path.join(tmp, 'global-config')
fs.mkdirSync(configDir, { recursive: true })
fs.writeFileSync(path.join(tmp, '.npmrc'), 'store-dir=~/store')
fs.writeFileSync(path.join(tmp, 'pnpm-workspace.yaml'), 'fetchRetries: 5')
await config.handler({
dir: process.cwd(),
cliOptions: {},
configDir,
location: 'project',
rawConfig: {},
}, ['set', 'fetch-timeout', '2000'])
expect(readYamlFile(path.join(tmp, 'pnpm-workspace.yaml'))).toEqual({
fetchRetries: 5,
fetchTimeout: 2000,
})
expect(readIniFileSync(path.join(tmp, '.npmrc'))).toEqual({
'store-dir': '~/store',
})
})
test('config set when only pnpm-workspace.yaml exists, writes to it', async () => {
const tmp = tempDir()
const configDir = path.join(tmp, 'global-config')
fs.mkdirSync(configDir, { recursive: true })
fs.writeFileSync(path.join(tmp, 'pnpm-workspace.yaml'), 'fetchRetries: 5')
await config.handler({
dir: process.cwd(),
cliOptions: {},
configDir,
location: 'project',
rawConfig: {},
}, ['set', 'fetch-timeout', '3000'])
expect(readYamlFile(path.join(tmp, 'pnpm-workspace.yaml'))).toEqual({
fetchRetries: 5,
fetchTimeout: 3000,
})
expect(fs.existsSync(path.join(tmp, '.npmrc'))).toBeFalsy()
})