fix(config): allow user-level preferences in global config.yaml (#11477)

Moves 20 user-level preference settings from the workspace-only exclusion list into the global config allowlist (`config/reader/src/configFileKey.ts`):

- Shell / scripts: `scriptShell`, `shellEmulator`
- Notifications & UI: `updateNotifier`, `useStderr`
- Trust policy (already DLX-inherited as user-level posture): `trustPolicy`, `trustPolicyExclude`, `trustPolicyIgnoreAfter`
- Store / virtual store: `globalVirtualStoreDir`, `virtualStoreDir`, `virtualStoreDirMaxLength`, `verifyStoreIntegrity`, `sideEffectsCache`, `sideEffectsCacheReadonly`
- Build / dep verification: `strictDepBuilds`, `verifyDepsBeforeRun`
- Misc personal/system prefs: `stateDir`, `registrySupportsTimeField`, `initPackageManager`, `initType`, `agent`

These are personal/system preferences rather than workspace structure. In v10 they could be set in `~/.npmrc`. v11 silently dropped them from both `~/.npmrc` and the new global `config.yaml`, leaving `pnpm-workspace.yaml` as the only working location — which the issue author rightly points out is impractical for system-level defaults like `scriptShell`.

After this change:
- Settings in `~/.config/pnpm/config.yaml` are applied instead of being filtered out by `isConfigFileKey` (`config/reader/src/index.ts:296`).
- `pnpm config set --location global scriptShell <path>` succeeds instead of throwing `ConfigSetUnsupportedYamlConfigKeyError` (same predicate used in `config/commands/src/configSet.ts:237`).

`pmOnFail` and `runtimeOnFail` are intentionally left workspace-only because they would cause lockfile divergence between contributors when set globally. `~/.npmrc` support for non-auth/non-network keys is also intentionally not restored — the team has moved those settings to YAML config.

Closes #11474.
This commit is contained in:
Zoltan Kochan
2026-05-06 01:44:46 +02:00
committed by GitHub
parent 76083acd54
commit fcec623c00
3 changed files with 75 additions and 20 deletions

View File

@@ -0,0 +1,6 @@
---
"@pnpm/config.reader": patch
"pnpm": patch
---
Allow user-level preferences in the global `config.yaml`. The following settings can now be set in `~/.config/pnpm/config.yaml` (or via `pnpm config set --location global`) instead of being restricted to `pnpm-workspace.yaml`: `agent`, `globalVirtualStoreDir`, `initPackageManager`, `initType`, `registrySupportsTimeField`, `scriptShell`, `shellEmulator`, `sideEffectsCache`, `sideEffectsCacheReadonly`, `stateDir`, `strictDepBuilds`, `trustPolicy`, `trustPolicyExclude`, `trustPolicyIgnoreAfter`, `updateNotifier`, `useStderr`, `verifyDepsBeforeRun`, `verifyStoreIntegrity`, `virtualStoreDir`, `virtualStoreDirMaxLength` [#11474](https://github.com/pnpm/pnpm/issues/11474).

View File

@@ -8,6 +8,7 @@ type PnpmKey = keyof typeof pnpmTypes
* Keys from {@link pnpmTypes} that are valid fields in a global config file.
*/
export const pnpmConfigFileKeys = [
'agent',
'bail',
'ci',
'color',
@@ -28,7 +29,10 @@ export const pnpmConfigFileKeys = [
'global-dir',
'global-path',
'global-pnpmfile',
'global-virtual-store-dir',
'http-proxy',
'init-package-manager',
'init-type',
'optimistic-repeat-install',
'loglevel',
'maxsockets',
@@ -47,10 +51,26 @@ export const pnpmConfigFileKeys = [
'prefer-offline',
'prefer-symlinked-executables',
'block-exotic-subdeps',
'registry-supports-time-field',
'reporter',
'resolution-mode',
'script-shell',
'shell-emulator',
'side-effects-cache',
'side-effects-cache-readonly',
'state-dir',
'store-dir',
'strict-dep-builds',
'trust-policy',
'trust-policy-exclude',
'trust-policy-ignore-after',
'update-notifier',
'use-beta-cli',
'use-stderr',
'verify-deps-before-run',
'verify-store-integrity',
'virtual-store-dir',
'virtual-store-dir-max-length',
] as const satisfies readonly PnpmKey[]
export type PnpmConfigFileKey = typeof pnpmConfigFileKeys[number]
@@ -87,8 +107,6 @@ export const excludedPnpmKeys = [
'ignore-workspace-cycles',
'ignore-workspace-root-check',
'include-workspace-root',
'init-package-manager',
'init-type',
'inject-workspace-packages',
'legacy-dir-filtering',
'link-workspace-packages',
@@ -104,7 +122,6 @@ export const excludedPnpmKeys = [
'patches-dir',
'pnpmfile',
'pm-on-fail',
'agent',
'prefer-workspace-packages',
'preserve-absolute-paths',
'production',
@@ -118,28 +135,13 @@ export const excludedPnpmKeys = [
'save-catalog-name',
'save-peer',
'save-workspace-protocol',
'script-shell',
'shamefully-hoist',
'shared-workspace-lockfile',
'shell-emulator',
'side-effects-cache',
'side-effects-cache-readonly',
'symlink',
'sort',
'state-dir',
'stream',
'strict-dep-builds',
'strict-store-pkg-content-check',
'strict-peer-dependencies',
'trust-policy',
'trust-policy-exclude',
'trust-policy-ignore-after',
'use-stderr',
'verify-deps-before-run',
'verify-store-integrity',
'global-virtual-store-dir',
'virtual-store-dir',
'virtual-store-dir-max-length',
'virtual-store-only',
'peers-suffix-max-length',
'workspace-concurrency',
@@ -148,8 +150,6 @@ export const excludedPnpmKeys = [
'test-pattern',
'changed-files-ignore-pattern',
'embed-readme',
'update-notifier',
'registry-supports-time-field',
'fail-if-no-match',
'sync-injected-deps-after-scripts',
'cpu',

View File

@@ -1687,6 +1687,55 @@ describe('global config.yaml', () => {
expect(config.dangerouslyAllowAllBuilds).toBeDefined()
})
test('reads user-level preference settings from global config.yaml', async () => {
prepareEmpty()
fs.mkdirSync('.config/pnpm', { recursive: true })
writeYamlFileSync('.config/pnpm/config.yaml', {
scriptShell: '/usr/local/bin/bash',
shellEmulator: true,
updateNotifier: false,
stateDir: '/custom/state',
trustPolicy: 'no-downgrade',
trustPolicyExclude: ['legacy-pkg'],
registrySupportsTimeField: true,
sideEffectsCache: false,
strictDepBuilds: true,
useStderr: true,
verifyDepsBeforeRun: 'error',
verifyStoreIntegrity: false,
virtualStoreDir: '/custom/.pnpm',
virtualStoreDirMaxLength: 80,
})
process.env.XDG_CONFIG_HOME = path.resolve('.config')
const { config, warnings } = await getConfig({
cliOptions: {},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
workspaceDir: process.cwd(),
})
expect(config.scriptShell).toBe('/usr/local/bin/bash')
expect(config.shellEmulator).toBe(true)
expect(config.updateNotifier).toBe(false)
expect(config.stateDir).toBe('/custom/state')
expect(config.trustPolicy).toBe('no-downgrade')
expect(config.trustPolicyExclude).toEqual(['legacy-pkg'])
expect(config.registrySupportsTimeField).toBe(true)
expect(config.sideEffectsCache).toBe(false)
expect(config.strictDepBuilds).toBe(true)
expect(config.useStderr).toBe(true)
expect(config.verifyDepsBeforeRun).toBe('error')
expect(config.verifyStoreIntegrity).toBe(false)
expect(config.virtualStoreDir).toBe('/custom/.pnpm')
expect(config.virtualStoreDirMaxLength).toBe(80)
expect(warnings.find((w) => w.includes('global config file'))).toBeUndefined()
})
test('warns when global config.yaml contains settings that are not allowed in the global config', async () => {
prepareEmpty()