Files
pnpm/config/reader/src/configFileKey.ts
Zoltan Kochan 707a879e53 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.
2026-05-06 17:41:07 +02:00

188 lines
5.2 KiB
TypeScript

import { npmConfigTypes } from './npmConfigTypes.js'
import type { pnpmTypes } from './types.js'
type NpmKey = keyof typeof npmConfigTypes
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',
'cache-dir',
'child-concurrency',
'dangerously-allow-all-builds',
'enable-modules-dir',
'enable-global-virtual-store',
'exclude-links-from-lockfile',
'extend-node-path',
'fetch-timeout',
'fetch-warn-timeout-ms',
'fetch-min-speed-ki-bps',
'fetching-concurrency',
'git-checks',
'git-shallow-hosts',
'global-bin-dir',
'global-dir',
'global-path',
'global-pnpmfile',
'global-virtual-store-dir',
'http-proxy',
'init-package-manager',
'init-type',
'optimistic-repeat-install',
'loglevel',
'maxsockets',
'modules-cache-max-age',
'dlx-cache-max-age',
'minimum-release-age',
'minimum-release-age-exclude',
'minimum-release-age-ignore-missing-time',
'minimum-release-age-strict',
'network-concurrency',
'noproxy',
'npm-path',
'npmrc-auth-file',
'package-import-method',
'prefer-frozen-lockfile',
'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]
/**
* Keys that present in {@link pnpmTypes} but are excluded from {@link ConfigFileKey}.
* They are usually CLI flags or workspace-only settings.
*/
export const excludedPnpmKeys = [
'auto-install-peers',
'catalog-mode',
'config-dir',
'merge-git-branch-lockfiles',
'merge-git-branch-lockfiles-branch-pattern',
'deploy-all-files',
'dedupe-peer-dependents',
'dedupe-peers',
'dedupe-direct-deps',
'dedupe-injected-deps',
'dev',
'dir',
'disallow-workspace-cycles',
'enable-pre-post-scripts',
'filter',
'filter-prod',
'force-legacy-deploy',
'frozen-lockfile',
'git-branch-lockfile',
'hoist',
'hoist-pattern',
'hoist-workspace-packages',
'ignore-compatibility-db',
'ignore-pnpmfile',
'ignore-workspace',
'ignore-workspace-cycles',
'ignore-workspace-root-check',
'include-workspace-root',
'inject-workspace-packages',
'legacy-dir-filtering',
'link-workspace-packages',
'lockfile',
'lockfile-dir',
'lockfile-include-tarball-url',
'lockfile-only',
'modules-dir',
'node-linker',
'offline',
'pack-destination',
'pack-gzip-level',
'patches-dir',
'pnpmfile',
'pm-on-fail',
'prefer-workspace-packages',
'preserve-absolute-paths',
'production',
'public-hoist-pattern',
'publish-branch',
'recursive-install',
'resolve-peers-from-workspace-root',
'runtime-on-fail',
'aggregate-output',
'reporter-hide-prefix',
'save-catalog-name',
'save-peer',
'save-workspace-protocol',
'shamefully-hoist',
'shared-workspace-lockfile',
'symlink',
'sort',
'stream',
'strict-store-pkg-content-check',
'strict-peer-dependencies',
'virtual-store-only',
'peers-suffix-max-length',
'workspace-concurrency',
'workspace-packages',
'workspace-root',
'test-pattern',
'changed-files-ignore-pattern',
'embed-readme',
'fail-if-no-match',
'sync-injected-deps-after-scripts',
'cpu',
'libc',
'os',
'audit-level',
'yes',
] as const satisfies ReadonlyArray<Exclude<PnpmKey, PnpmConfigFileKey>>
export type ExcludedPnpmKey = typeof excludedPnpmKeys[number]
/**
* Proof that {@link excludedPnpmKeys} is complete and exhaustive, i.e. All keys that appear in {@link pnpmTypes} but not in
* {@link pnpmConfigFileKeys} should be included in {@link excludedPnpmKeys}.
*/
export const _proofExcludedPnpmKeysIsExhaustive = (carrier: Exclude<PnpmKey, PnpmConfigFileKey>): ExcludedPnpmKey => carrier
/**
* Proof that there are no keys that are both included and excluded, i.e. {@link pnpmConfigFileKeys} and {@link excludedPnpmKeys}
* have no overlap.
*/
export const _proofNoContradiction = (carrier: PnpmConfigFileKey & ExcludedPnpmKey): never => carrier
// even npmTypes still have keys that don't make sense in global config, but the list is quite long, let's do it another day.
// TODO: compile a list of npm keys that are valid or invalid in a global config file.
export type NpmConfigFileKey = Exclude<NpmKey, ExcludedPnpmKey>
/** Key that is valid in a global config file. */
export type ConfigFileKey = NpmConfigFileKey | PnpmConfigFileKey
const setOfPnpmConfigFilesKeys: ReadonlySet<string> = new Set(pnpmConfigFileKeys)
const setOfExcludedPnpmKeys: ReadonlySet<string> = new Set(excludedPnpmKeys)
/** Whether the key (in kebab-case) is a valid key in a global config file. */
export const isConfigFileKey = (kebabKey: string): kebabKey is ConfigFileKey =>
setOfPnpmConfigFilesKeys.has(kebabKey) || (kebabKey in npmConfigTypes && !setOfExcludedPnpmKeys.has(kebabKey))