diff --git a/config/commands/test/configSet.test.ts b/config/commands/test/configSet.test.ts index 9d76d0d6da..7b1df8b985 100644 --- a/config/commands/test/configSet.test.ts +++ b/config/commands/test/configSet.test.ts @@ -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() +}) diff --git a/config/reader/src/auth.ts b/config/reader/src/auth.ts index dda7be19b1..79410f65e1 100644 --- a/config/reader/src/auth.ts +++ b/config/reader/src/auth.ts @@ -7,14 +7,21 @@ const RAW_AUTH_CFG_KEYS = [ 'cafile', 'cert', 'key', - 'local-address', - 'git-shallow-hosts', + 'registry', +] satisfies Array + +/** + * 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 +] 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 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. */ diff --git a/config/reader/src/configFileKey.ts b/config/reader/src/configFileKey.ts index 9e49805d17..2925e426fd 100644 --- a/config/reader/src/configFileKey.ts +++ b/config/reader/src/configFileKey.ts @@ -28,6 +28,7 @@ export const pnpmConfigFileKeys = [ 'global-dir', 'global-path', 'global-pnpmfile', + 'http-proxy', 'optimistic-repeat-install', 'loglevel', 'maxsockets', diff --git a/config/reader/src/index.ts b/config/reader/src/index.ts index 3a7255363b..60f2b3fb56 100644 --- a/config/reader/src/index.ts +++ b/config/reader/src/index.ts @@ -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 `${infer T}${infer U}` diff --git a/config/reader/src/loadNpmrcFiles.ts b/config/reader/src/loadNpmrcFiles.ts index 6f0a34bc28..8c818b05d2 100644 --- a/config/reader/src/loadNpmrcFiles.ts +++ b/config/reader/src/loadNpmrcFiles.ts @@ -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 = {} 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 } } diff --git a/config/reader/src/types.ts b/config/reader/src/types.ts index 8cff63f3a6..2d9eac37d5 100644 --- a/config/reader/src/types.ts +++ b/config/reader/src/types.ts @@ -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, diff --git a/config/reader/test/index.ts b/config/reader/test/index.ts index a8fa31ed60..6ce973e58f 100644 --- a/config/reader/test/index.ts +++ b/config/reader/test/index.ts @@ -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 () => { diff --git a/core/types/src/package.ts b/core/types/src/package.ts index 4247d4cdc9..32238bc405 100644 --- a/core/types/src/package.ts +++ b/core/types/src/package.ts @@ -188,6 +188,9 @@ export interface PnpmSettings { requiredScripts?: string[] supportedArchitectures?: SupportedArchitectures nodeDownloadMirrors?: Record + httpProxy?: string + httpsProxy?: string + noProxy?: string | boolean } export interface ProjectManifest extends BaseManifest {