Files
pnpm/config/reader/src/localConfig.ts
Maikel van Dort b14496af4e fix: dlx catalogs not found (#11308)
Fixes #10594, catalogs not being read from the workspace when using the `catalog:` protocol with the `pnpm dlx` / `pnpx` command, resulting in a catalog entry not found error.

Added e2e tests to check if the workspace config is actually loaded. Also added that pnpm dlx reads the retry options from the workspace (Could potentially put that in a separate PR)

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **Bug Fixes**
  * Fixed catalog resolution when using the `catalog:` protocol with `pnpm dlx` / `pnpx` so catalogs are correctly read from the workspace.

* **New Features**
  * `dlx` now inherits workspace catalog and fetch retry/timeout settings so CLI runs respect those local configs.

* **Tests**
  * Added tests validating catalog inheritance and failure cases for `dlx` catalog resolution.

* **Chores**
  * Updated changeset metadata to mark related packages for patch releases.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-05-05 11:35:05 +00:00

214 lines
7.1 KiB
TypeScript

import type { Config } from './Config.js'
import { type InheritableConfigPair, inheritPickedConfig } from './inheritPickedConfig.js'
import type { types } from './types.js'
const RAW_AUTH_CFG_KEYS = [
'ca',
'cafile',
'cert',
'key',
'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',
'http-proxy',
'local-address',
'strict-ssl',
]
const RAW_AUTH_CFG_KEY_SUFFIXES = [
':ca',
':cafile',
':cert',
':certfile',
':key',
':keyfile',
':registry',
':tokenHelper',
':_auth',
':_authToken',
]
const AUTH_CFG_KEYS = [
'ca',
'cert',
'configByUri',
'key',
'registry',
'registries',
] satisfies Array<keyof Config>
/**
* Config key categories inherited by `pnpm dlx`.
*
* ## Principle
*
* `pnpm dlx` runs packages in isolation from the current project. It must not
* read project-structural settings (hoisting, linking, workspace layout, etc.)
* from local config. However, several categories of local settings DO apply:
*
* 1. **Registry & auth:** needed to reach the same package sources
* (registries, tokens, certificates).
* 2. **Security & trust policy:** these reflect the user's or organization's
* security posture and must apply regardless of how a package is installed.
* A setting that answers "what am I allowed to download?" belongs here.
* 3. **Catalogs:** the `catalog:` protocol resolves package versions through
* workspace catalog entries; without them, `pnpm dlx pkg@catalog:...` cannot
* look up the requested version.
* 4. **Fetch retry/timeout:** governs how the client talks to the registry.
* These reflect the same network environment as a regular install.
*
* Other settings are intentionally excluded. These are the ones that control
* how downloaded packages are arranged in `node_modules` (hoisting, linking,
* workspace layout, etc.).
*
* ## Rules
*
* | Category | Inherited by dlx? | Examples |
* |--------------------------------|--------------------|--------------------------------------------------|
* | Registry & auth | Yes | registry, _authToken, ca |
* | Security & trust policy | Yes | minimumReleaseAge, trustPolicy |
* | Catalogs | Yes | catalogs |
* | Fetch retry/timeout | Yes | fetchRetries, fetchTimeout |
* | Installation structure | No | shamefully-hoist, node-linker, hoist-pattern |
* | Workspace settings | No | link-workspace-packages, shared-workspace-lockfile|
* | Resolution strategy | No | resolution-mode, dedupe-peers |
*/
const SECURITY_POLICY_CFG_KEYS = [
'minimumReleaseAge',
'minimumReleaseAgeExclude',
'minimumReleaseAgeIgnoreMissingTime',
'minimumReleaseAgeStrict',
'trustPolicy',
'trustPolicyExclude',
'trustPolicyIgnoreAfter',
] satisfies Array<keyof Config>
const CATALOGS_CFG_KEYS = [
'catalogs',
] satisfies Array<keyof Config>
const FETCH_CFG_KEYS = [
'fetchRetryFactor',
'fetchRetryMaxtimeout',
'fetchRetryMintimeout',
'fetchRetries',
'fetchTimeout',
] satisfies Array<keyof Config>
const NPM_AUTH_SETTINGS = [
...RAW_AUTH_CFG_KEYS,
'_auth',
'_authToken',
'_password',
'email',
'keyfile',
'username',
]
function isRawAuthCfgKey (rawCfgKey: string): boolean {
if ((RAW_AUTH_CFG_KEYS as string[]).includes(rawCfgKey)) return true
if (RAW_AUTH_CFG_KEY_SUFFIXES.some(suffix => rawCfgKey.endsWith(suffix))) return true
return false
}
function isAuthCfgKey (cfgKey: keyof Config): cfgKey is typeof AUTH_CFG_KEYS[number] {
return (AUTH_CFG_KEYS as Array<keyof Config>).includes(cfgKey)
}
function isSecurityPolicyCfgKey (cfgKey: keyof Config): cfgKey is typeof SECURITY_POLICY_CFG_KEYS[number] {
return (SECURITY_POLICY_CFG_KEYS as Array<keyof Config>).includes(cfgKey)
}
function isCatalogsCfgKey (cfgKey: keyof Config): cfgKey is typeof CATALOGS_CFG_KEYS[number] {
return (CATALOGS_CFG_KEYS as Array<keyof Config>).includes(cfgKey)
}
function isFetchCfgKey (cfgKey: keyof Config): cfgKey is typeof FETCH_CFG_KEYS[number] {
return (FETCH_CFG_KEYS as Array<keyof Config>).includes(cfgKey)
}
function pickRawAuthConfig<RawLocalCfg extends Record<string, unknown>> (rawLocalCfg: RawLocalCfg): Partial<RawLocalCfg> {
const result: Partial<RawLocalCfg> = {}
for (const key in rawLocalCfg) {
if (isRawAuthCfgKey(key)) {
result[key] = rawLocalCfg[key]
}
}
return result
}
function pickAuthConfig (localCfg: Partial<Config>): Partial<Config> {
const result: Record<string, unknown> = {}
for (const key in localCfg) {
if (isAuthCfgKey(key as keyof Config)) {
result[key] = localCfg[key as keyof Config]
}
}
return result as Partial<Config>
}
function pickDlxConfig (localCfg: Partial<Config>): Partial<Config> {
const result: Record<string, unknown> = {}
for (const key in localCfg) {
if (isAuthCfgKey(key as keyof Config) || isSecurityPolicyCfgKey(key as keyof Config) || isCatalogsCfgKey(key as keyof Config) || isFetchCfgKey(key as keyof Config)) {
result[key] = localCfg[key as keyof Config]
}
}
return result as Partial<Config>
}
export function inheritAuthConfig (target: InheritableConfigPair, src: InheritableConfigPair): void {
inheritPickedConfig(target, src, pickAuthConfig, pickRawAuthConfig)
}
/**
* Inherits the categories listed above (auth/registry, security & trust
* policy, catalogs, and fetch retry/timeout) from a local config source
* into the target config.
*
* Used by `pnpm dlx` and `pnpm create` so that these commands respect
* the local project's registry authentication, security policies,
* catalog definitions, and fetch behavior, while ignoring
* project-structural settings.
*/
export function inheritDlxConfig (target: InheritableConfigPair, src: InheritableConfigPair): void {
inheritPickedConfig(target, src, pickDlxConfig, pickRawAuthConfig)
}
/**
* Whether the config key would be read from an INI config file.
*/
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.
*/
export function pickIniConfig<RawConfig extends Record<string, unknown>> (rawConfig: RawConfig): Partial<RawConfig> {
const result: Partial<RawConfig> = {}
for (const key in rawConfig) {
if (isIniConfigKey(key)) {
result[key] = rawConfig[key]
}
}
return result
}