fix(config.reader): fall back to legacy proxy for pinned pnpm (#17363)
createPackageManagerNetworkConfig now reads `https-proxy`, `http-proxy`, and `proxy` as unset unless they are a non-empty string. `false`, `null`, and an empty value fall through to the next setting and then the environment, matching resolveProxySettings for installs and pnpm v12. An explicit `proxy=false` remains the way to turn the proxy off, and it now also stops the download's `httpProxy` from falling back to `HTTP_PROXY`, as installs do. The engine identity check of the pinned pnpm now takes its network settings from getPackageManagerBootstrapConfig, like the store controller that downloads it, instead of the project's proxy, TLS, and auth settings. Closes pnpm/tasks#193 --------- Co-authored-by: Zoltan Kochan <z@kochan.io> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
5 files changed
+147
-22
No files matched your search
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@pnpm/config.reader": patch
|
||||
"pnpm": patch
|
||||
---
|
||||
|
||||
When `https-proxy` is `false`, pnpm now downloads the pnpm version pinned by `packageManager` through the legacy `proxy` setting, as installs already do. Setting `proxy` to `false` now also stops that download from using the `HTTP_PROXY` environment variable. The signature check of the pinned pnpm now uses the same proxy and TLS settings as its download [pnpm/tasks#193](https://github.com/pnpm/tasks/issues/193).
|
||||
@@ -189,13 +189,14 @@ function createPackageManagerNetworkConfig (
|
||||
// trusted `.npmrc` files, as they do for the project's requests. The
|
||||
// command line's `.npmrc`-spelled tokens sit inside `trustedConfig` too.
|
||||
const cliNetworkSettings = configFromCliOpts as BootstrapNetworkSettings
|
||||
const httpsProxy = getProxyValue(
|
||||
trustedConfig['https-proxy'] ?? trustedConfig.proxy,
|
||||
getEnvValue(env, 'https_proxy')
|
||||
)
|
||||
const httpsProxySetting = getProxyUrl(trustedConfig['https-proxy']) ??
|
||||
(trustedConfig.proxy === false ? false : getProxyUrl(trustedConfig.proxy))
|
||||
const httpsProxy = getProxyValue(httpsProxySetting, getEnvValue(env, 'https_proxy'))
|
||||
const httpProxy = getProxyValue(
|
||||
trustedConfig['http-proxy'],
|
||||
httpsProxy ?? getEnvValue(env, 'http_proxy') ?? getEnvValue(env, 'proxy')
|
||||
getProxyUrl(trustedConfig['http-proxy']),
|
||||
httpsProxySetting === false
|
||||
? undefined
|
||||
: httpsProxy ?? getEnvValue(env, 'http_proxy') ?? getEnvValue(env, 'proxy')
|
||||
)
|
||||
return {
|
||||
ca: trustedConfig.ca as string | string[] | undefined,
|
||||
@@ -214,10 +215,19 @@ function getEnvValue (env: Record<string, string | undefined>, key: string): str
|
||||
return env[key] ?? env[key.toUpperCase()] ?? env[key.toLowerCase()]
|
||||
}
|
||||
|
||||
function getProxyValue (value: unknown, fallback: string | undefined): string | undefined {
|
||||
if (value === false || value === null) return undefined
|
||||
if (typeof value === 'string' && value.length > 0) return value
|
||||
return fallback
|
||||
/**
|
||||
* A proxy setting of `false`, `null`, or an empty string reads as unset, so
|
||||
* the next setting and then the environment apply, as they do for installs.
|
||||
* Only `proxy=false` turns the proxy off, including the environment fallback
|
||||
* for `http-proxy`.
|
||||
*/
|
||||
function getProxyUrl (value: unknown): string | undefined {
|
||||
return typeof value === 'string' && value.length > 0 ? value : undefined
|
||||
}
|
||||
|
||||
function getProxyValue (value: string | false | undefined, fallback: string | undefined): string | undefined {
|
||||
if (value === false) return undefined
|
||||
return value ?? fallback
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6065,6 +6065,102 @@ test('proxy settings are still read from .npmrc', async () => {
|
||||
expect(config.noProxy).toBe('internal.example.com')
|
||||
})
|
||||
|
||||
test('package manager downloads use the legacy proxy when https-proxy is false', async () => {
|
||||
prepareEmpty()
|
||||
|
||||
fs.writeFileSync('user.npmrc', 'https-proxy=false\nproxy=http://legacy-proxy.example.com:8080', 'utf8')
|
||||
|
||||
const { config } = await getConfig({
|
||||
cliOptions: { userconfig: path.resolve('user.npmrc') },
|
||||
env: {},
|
||||
packageManager: {
|
||||
name: 'pnpm',
|
||||
version: '1.0.0',
|
||||
},
|
||||
workspaceDir: process.cwd(),
|
||||
})
|
||||
|
||||
expect(config.packageManagerNetworkConfig?.httpsProxy).toBe('http://legacy-proxy.example.com:8080')
|
||||
})
|
||||
|
||||
test('package manager downloads use the legacy proxy when https-proxy is empty', async () => {
|
||||
prepareEmpty()
|
||||
|
||||
fs.writeFileSync('user.npmrc', 'https-proxy=\nproxy=http://legacy-proxy.example.com:8080', 'utf8')
|
||||
|
||||
const { config } = await getConfig({
|
||||
cliOptions: { userconfig: path.resolve('user.npmrc') },
|
||||
env: {},
|
||||
packageManager: {
|
||||
name: 'pnpm',
|
||||
version: '1.0.0',
|
||||
},
|
||||
workspaceDir: process.cwd(),
|
||||
})
|
||||
|
||||
expect(config.packageManagerNetworkConfig?.httpsProxy).toBe('http://legacy-proxy.example.com:8080')
|
||||
})
|
||||
|
||||
test('package manager downloads ignore the environment proxy when proxy is false', async () => {
|
||||
prepareEmpty()
|
||||
|
||||
fs.writeFileSync('user.npmrc', 'https-proxy=false\nproxy=false', 'utf8')
|
||||
|
||||
const { config } = await getConfig({
|
||||
cliOptions: { userconfig: path.resolve('user.npmrc') },
|
||||
env: {
|
||||
HTTP_PROXY: 'http://env-proxy.example.com:8080',
|
||||
HTTPS_PROXY: 'http://env-proxy.example.com:8080',
|
||||
},
|
||||
packageManager: {
|
||||
name: 'pnpm',
|
||||
version: '1.0.0',
|
||||
},
|
||||
workspaceDir: process.cwd(),
|
||||
})
|
||||
|
||||
expect(config.packageManagerNetworkConfig?.httpsProxy).toBeUndefined()
|
||||
expect(config.packageManagerNetworkConfig?.httpProxy).toBeUndefined()
|
||||
})
|
||||
|
||||
test('package manager downloads use the environment proxy when proxy is null', async () => {
|
||||
prepareEmpty()
|
||||
|
||||
fs.writeFileSync('user.npmrc', 'proxy=null', 'utf8')
|
||||
|
||||
const { config } = await getConfig({
|
||||
cliOptions: { userconfig: path.resolve('user.npmrc') },
|
||||
env: {
|
||||
HTTPS_PROXY: 'http://env-proxy.example.com:8080',
|
||||
},
|
||||
packageManager: {
|
||||
name: 'pnpm',
|
||||
version: '1.0.0',
|
||||
},
|
||||
workspaceDir: process.cwd(),
|
||||
})
|
||||
|
||||
expect(config.packageManagerNetworkConfig?.httpsProxy).toBe('http://env-proxy.example.com:8080')
|
||||
})
|
||||
|
||||
test('package manager downloads use the https proxy when http-proxy is false', async () => {
|
||||
prepareEmpty()
|
||||
|
||||
fs.writeFileSync('user.npmrc', 'https-proxy=http://https-proxy.example.com:8080\nhttp-proxy=false', 'utf8')
|
||||
|
||||
const { config } = await getConfig({
|
||||
cliOptions: { userconfig: path.resolve('user.npmrc') },
|
||||
env: {},
|
||||
packageManager: {
|
||||
name: 'pnpm',
|
||||
version: '1.0.0',
|
||||
},
|
||||
workspaceDir: process.cwd(),
|
||||
})
|
||||
|
||||
expect(config.packageManagerNetworkConfig?.httpProxy).toBe('http://https-proxy.example.com:8080')
|
||||
})
|
||||
|
||||
test('an empty legacy proxy setting in .npmrc falls through to the environment', async () => {
|
||||
prepareEmpty()
|
||||
|
||||
|
||||
@@ -698,6 +698,28 @@ test('ends with the exit status of the pnpm it switched to', async () => {
|
||||
}
|
||||
})
|
||||
|
||||
test('switchCliVersion verifies the pinned pnpm through the trusted proxy settings', async () => {
|
||||
const config = {
|
||||
httpsProxy: 'http://project-proxy.example.com:8080',
|
||||
packageManagerNetworkConfig: { configByUri: {} },
|
||||
rawConfig: {},
|
||||
} as unknown as Config
|
||||
const context = {
|
||||
rootProjectManifestDir: '/project',
|
||||
wantedPackageManager: { name: 'pnpm', version: '11.13.1', fromDevEngines: true, onFail: 'download' },
|
||||
} as unknown as ConfigContext
|
||||
readEnvLockfile.mockResolvedValue(envLockfileFor('11.13.1'))
|
||||
|
||||
const exit = jest.spyOn(process, 'exit').mockImplementation((() => undefined) as never)
|
||||
try {
|
||||
await switchCliVersion(config, context)
|
||||
} finally {
|
||||
exit.mockRestore()
|
||||
}
|
||||
|
||||
expect(installPnpmToStore).toHaveBeenCalledWith('11.13.1', expect.objectContaining({ httpsProxy: undefined }))
|
||||
})
|
||||
|
||||
test('reports a pnpm that cannot be started as a failed version switch', async () => {
|
||||
const config = { rawConfig: {} } as unknown as Config
|
||||
const context = {
|
||||
|
||||
@@ -346,21 +346,12 @@ function installPnpmToStoreOptions (
|
||||
envLockfile,
|
||||
storeController: store.ctrl,
|
||||
storeDir: store.dir,
|
||||
registriesByScope: getPackageManagerBootstrapConfig(config).registriesByScope,
|
||||
virtualStoreDirMaxLength: config.virtualStoreDirMaxLength,
|
||||
packageManager: { name: packageManager.name, version: packageManager.version },
|
||||
// Network settings so the engine identity check can reach the canonical
|
||||
// npm registry through the user's proxy / TLS configuration.
|
||||
ca: config.ca,
|
||||
cert: config.cert,
|
||||
key: config.key,
|
||||
httpProxy: config.httpProxy,
|
||||
httpsProxy: config.httpsProxy,
|
||||
noProxy: config.noProxy,
|
||||
strictSsl: config.strictSsl,
|
||||
localAddress: config.localAddress,
|
||||
// The engine identity check reaches the registry with the same trusted
|
||||
// network settings as the store controller that downloads pnpm.
|
||||
...getPackageManagerBootstrapConfig(config),
|
||||
maxSockets: config.maxSockets,
|
||||
configByUri: config.configByUri,
|
||||
timeout: config.fetchTimeout,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user