refactor(config): move network settings from .npmrc to YAML config (#11209)

Proxy settings (httpProxy, httpsProxy, noProxy), local-address,
strict-ssl, and git-shallow-hosts are now written to config.yaml
(global) or pnpm-workspace.yaml (local) instead of auth.ini/.npmrc.

They are still readable from .npmrc for easier migration from npm CLI.
The canonical YAML key names (httpProxy, httpsProxy, noProxy) match
Yarn Berry's naming convention.

- Add httpProxy, httpsProxy, noProxy to PnpmSettings type
- Add http-proxy to pnpmTypes and pnpmConfigFileKeys
- Separate network keys from auth keys in config routing
- Add isNpmrcReadableKey for backward-compatible .npmrc reading
This commit is contained in:
Zoltan Kochan authored and GitHub committed 2026-04-06 11:36:08 +02:00
1 parent 705e3fe3cc
commit b65204762a
8 files changed
+203 -14

No files matched your search

+79
View File
@@ -870,3 +870,82 @@ test('config set when only pnpm-workspace.yaml exists, writes to it', async () =
})
expect(fs.existsSync(path.join(tmp, '.npmrc'))).toBeFalsy()
})
test('config set --global https-proxy writes to config.yaml, not auth.ini', async () => {
const tmp = tempDir()
const configDir = path.join(tmp, 'global-config')
const initConfig = {
globalRc: undefined,
globalYaml: undefined,
localRc: undefined,
localYaml: undefined,
} satisfies ConfigFilesData
writeConfigFiles(configDir, tmp, initConfig)
await config.handler(createConfigCommandOpts({
dir: process.cwd(),
cliOptions: {},
configDir,
global: true,
authConfig: {},
}), ['set', 'https-proxy', 'http://proxy.example.com:8443'])
const result = readConfigFiles(configDir, tmp)
expect(result.globalYaml).toEqual({
httpsProxy: 'http://proxy.example.com:8443',
})
// Must NOT write to auth.ini
expect(result.globalRc).toBeUndefined()
})
test('config set --global httpProxy writes to config.yaml', async () => {
const tmp = tempDir()
const configDir = path.join(tmp, 'global-config')
const initConfig = {
globalRc: undefined,
globalYaml: undefined,
localRc: undefined,
localYaml: undefined,
} satisfies ConfigFilesData
writeConfigFiles(configDir, tmp, initConfig)
await config.handler(createConfigCommandOpts({
dir: process.cwd(),
cliOptions: {},
configDir,
global: true,
authConfig: {},
}), ['set', 'httpProxy', 'http://proxy.example.com:8080'])
const result = readConfigFiles(configDir, tmp)
expect(result.globalYaml).toEqual({
httpProxy: 'http://proxy.example.com:8080',
})
expect(result.globalRc).toBeUndefined()
})
test('config set --global no-proxy writes to config.yaml', async () => {
const tmp = tempDir()
const configDir = path.join(tmp, 'global-config')
const initConfig = {
globalRc: undefined,
globalYaml: undefined,
localRc: undefined,
localYaml: undefined,
} satisfies ConfigFilesData
writeConfigFiles(configDir, tmp, initConfig)
await config.handler(createConfigCommandOpts({
dir: process.cwd(),
cliOptions: {},
configDir,
global: true,
authConfig: {},
}), ['set', 'no-proxy', 'localhost,127.0.0.1'])
const result = readConfigFiles(configDir, tmp)
expect(result.globalYaml).toEqual({
noProxy: 'localhost,127.0.0.1',
})
expect(result.globalRc).toBeUndefined()
})
+19 -10
View File
@@ -7,14 +7,21 @@ const RAW_AUTH_CFG_KEYS = [
'cafile',
'cert',
'key',
'local-address',
'git-shallow-hosts',
'registry',
] satisfies Array<keyof typeof types>
/**
* Network-related keys that should be readable from .npmrc (for migration from npm)
* but written to YAML config files (config.yaml / pnpm-workspace.yaml).
*/
const NETWORK_INI_KEYS = [
'https-proxy',
'proxy',
'no-proxy',
'registry',
'http-proxy',
'local-address',
'strict-ssl',
] satisfies Array<keyof typeof types>
]
const RAW_AUTH_CFG_KEY_SUFFIXES = [
':ca',
@@ -34,14 +41,8 @@ const AUTH_CFG_KEYS = [
'cert',
'configByUri',
'key',
'localAddress',
'gitShallowHosts',
'httpsProxy',
'httpProxy',
'noProxy',
'registry',
'registries',
'strictSsl',
] satisfies Array<keyof Config>
const NPM_AUTH_SETTINGS = [
@@ -94,6 +95,14 @@ export function inheritAuthConfig (target: InheritableConfigPair, src: Inheritab
export const isIniConfigKey = (key: string): boolean =>
key.startsWith('@') || key.startsWith('//') || NPM_AUTH_SETTINGS.includes(key)
/**
* Whether the config key should be read from .npmrc files.
* This includes auth keys and proxy keys (proxy keys are readable from .npmrc
* for easier migration from npm, but are written to YAML config files).
*/
export const isNpmrcReadableKey = (key: string): boolean =>
isIniConfigKey(key) || NETWORK_INI_KEYS.includes(key)
/**
* Filter keys that are allowed to be read from an INI config file.
*/
+1
View File
@@ -28,6 +28,7 @@ export const pnpmConfigFileKeys = [
'global-dir',
'global-path',
'global-pnpmfile',
'http-proxy',
'optimistic-repeat-install',
'loglevel',
'maxsockets',
+1 -1
View File
@@ -66,7 +66,7 @@ export {
} from './projectConfig.js'
export type { Config, ConfigContext, ProjectConfig, UniversalOptions, VerifyDepsBeforeRun }
export { isIniConfigKey } from './auth.js'
export { isIniConfigKey, isNpmrcReadableKey } from './auth.js'
export { type ConfigFileKey, isConfigFileKey } from './configFileKey.js'
type CamelToKebabCase<S extends string> = S extends `${infer T}${infer U}`
+3 -3
View File
@@ -5,7 +5,7 @@ import path from 'node:path'
import { envReplace } from '@pnpm/config.env-replace'
import { readIniFileSync } from 'read-ini-file'
import { isIniConfigKey } from './auth.js'
import { isNpmrcReadableKey } from './auth.js'
export interface NpmrcConfigResult {
/**
@@ -95,7 +95,7 @@ export function loadNpmrcConfig (opts: LoadNpmrcConfigOpts): NpmrcConfigResult {
const mergedConfig: Record<string, unknown> = {}
for (const source of [pnpmBuiltinConfig, opts.defaultOptions, userConfig, pnpmAuthConfig, workspaceNpmrc, opts.cliOptions]) {
for (const [key, value] of Object.entries(source)) {
if (isIniConfigKey(key)) {
if (isNpmrcReadableKey(key)) {
mergedConfig[key] = value
}
}
@@ -146,7 +146,7 @@ function readAndFilterNpmrc (
: rawValue
// Only keep auth/registry related keys
if (isIniConfigKey(key)) {
if (isNpmrcReadableKey(key)) {
result[key] = value
}
}
+1
View File
@@ -45,6 +45,7 @@ export const pnpmTypes = {
'global-pnpmfile': String,
'git-branch-lockfile': Boolean,
hoist: Boolean,
'http-proxy': [null, String],
'hoist-pattern': Array,
'hoist-workspace-packages': Boolean,
'ignore-compatibility-db': Boolean,
+96
View File
@@ -1451,6 +1451,102 @@ describe('global config.yaml', () => {
// NOTE: the field may appear kebab-case here, but only internally,
expect(config.dangerouslyAllowAllBuilds).toBeDefined()
})
test('reads proxy settings from global config.yaml', async () => {
prepareEmpty()
fs.mkdirSync('.config/pnpm', { recursive: true })
writeYamlFileSync('.config/pnpm/config.yaml', {
httpProxy: 'http://proxy.example.com:8080',
httpsProxy: 'http://proxy.example.com:8443',
noProxy: 'localhost,127.0.0.1',
})
process.env.XDG_CONFIG_HOME = path.resolve('.config')
const { config } = await getConfig({
cliOptions: {},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
workspaceDir: process.cwd(),
})
expect(config.httpProxy).toBe('http://proxy.example.com:8080')
expect(config.httpsProxy).toBe('http://proxy.example.com:8443')
expect(config.noProxy).toBe('localhost,127.0.0.1')
})
test('proxy settings from global config.yaml override .npmrc', async () => {
prepareEmpty()
// Set proxy in .npmrc (npm-style keys)
fs.writeFileSync('.npmrc', 'https-proxy=http://npmrc-proxy.example.com:8080', 'utf8')
// Set different proxy in global config.yaml
fs.mkdirSync('.config/pnpm', { recursive: true })
writeYamlFileSync('.config/pnpm/config.yaml', {
httpsProxy: 'http://yaml-proxy.example.com:9090',
})
process.env.XDG_CONFIG_HOME = path.resolve('.config')
const { config } = await getConfig({
cliOptions: {},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
workspaceDir: process.cwd(),
})
// Global YAML should override .npmrc
expect(config.httpsProxy).toBe('http://yaml-proxy.example.com:9090')
})
test('CLI flags override proxy settings from global config.yaml', async () => {
prepareEmpty()
fs.mkdirSync('.config/pnpm', { recursive: true })
writeYamlFileSync('.config/pnpm/config.yaml', {
httpsProxy: 'http://yaml-proxy.example.com:9090',
})
process.env.XDG_CONFIG_HOME = path.resolve('.config')
const { config } = await getConfig({
cliOptions: {
'https-proxy': 'http://cli-proxy.example.com:7070',
},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
workspaceDir: process.cwd(),
})
expect(config.httpsProxy).toBe('http://cli-proxy.example.com:7070')
})
})
test('proxy settings are still read from .npmrc', async () => {
prepareEmpty()
fs.writeFileSync('.npmrc', 'https-proxy=http://npmrc-proxy.example.com:8080\nproxy=http://npmrc-http-proxy.example.com:3128\nno-proxy=internal.example.com', 'utf8')
const { config } = await getConfig({
cliOptions: {},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
workspaceDir: process.cwd(),
})
expect(config.httpsProxy).toBe('http://npmrc-proxy.example.com:8080')
expect(config.httpProxy).toBe('http://npmrc-proxy.example.com:8080')
expect(config.noProxy).toBe('internal.example.com')
})
test('lockfile: false in pnpm-workspace.yaml sets useLockfile to false', async () => {
+3
View File
@@ -188,6 +188,9 @@ export interface PnpmSettings {
requiredScripts?: string[]
supportedArchitectures?: SupportedArchitectures
nodeDownloadMirrors?: Record<string, string>
httpProxy?: string
httpsProxy?: string
noProxy?: string | boolean
}
export interface ProjectManifest extends BaseManifest {