From 3e2df55ed45d659a31d40dbc047a3cf230164f40 Mon Sep 17 00:00:00 2001 From: Zoltan Kochan Date: Wed, 6 May 2026 17:06:18 +0200 Subject: [PATCH] fix(config): align scoped registry resolution between config get and publish (#11494) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes [#11492](https://github.com/pnpm/pnpm/issues/11492). In pnpm v11 a scoped registry resolved to different URLs depending on which command read it: - `pnpm config get @:registry` returned the value from `.npmrc` - `pnpm publish` used the value from `pnpm-workspace.yaml`'s `registries` block When the two sources disagreed, `pnpm publish` silently targeted the URL from `pnpm-workspace.yaml`, even though `pnpm config get` reported a different (and seemingly authoritative) URL — so users could publish to the wrong registry without any indication. This PR makes `pnpm config get @:registry` read from the merged `Config.registries` map (the same map `publish` and the resolvers use) before falling back to `authConfig`. Both commands now report and use the same URL. --- .../scoped-registry-config-get-publish.md | 6 ++++++ config/commands/src/configGet.ts | 12 +++++++++++ config/commands/test/configGet.test.ts | 21 +++++++++++++++++++ config/reader/test/index.ts | 19 +++++++++++++++++ pnpm/test/config/get.ts | 6 ++++-- 5 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 .changeset/scoped-registry-config-get-publish.md diff --git a/.changeset/scoped-registry-config-get-publish.md b/.changeset/scoped-registry-config-get-publish.md new file mode 100644 index 0000000000..dbde8e7ba7 --- /dev/null +++ b/.changeset/scoped-registry-config-get-publish.md @@ -0,0 +1,6 @@ +--- +"@pnpm/config.commands": patch +"pnpm": patch +--- + +`pnpm config get @:registry` now reports the same URL that `pnpm publish` and the resolvers actually use. Previously, `config get` only consulted `.npmrc`, while `publish`/install used the merged map that includes `pnpm-workspace.yaml`'s `registries` block — so the two could diverge silently and a publish could go to the wrong registry [#11492](https://github.com/pnpm/pnpm/issues/11492). diff --git a/config/commands/src/configGet.ts b/config/commands/src/configGet.ts index 60b2d862ee..a0e927b303 100644 --- a/config/commands/src/configGet.ts +++ b/config/commands/src/configGet.ts @@ -21,6 +21,18 @@ interface Found { function lookupConfig (opts: ConfigCommandOptions, key: string, isScopedKey: boolean): Found | undefined { if (isScopedKey) { + // Scoped registry keys (e.g. `@scope:registry`) can be set in two places: + // `.npmrc` (which lands in authConfig) or pnpm-workspace.yaml's + // `registries` block (which lands in the merged Config.registries map). + // Prefer the merged map so this command reports the same value that + // `pnpm publish` and the resolvers actually use. + if (key.endsWith(':registry')) { + const scope = key.slice(0, key.length - ':registry'.length) + const merged = opts._config.registries?.[scope] + if (merged !== undefined) { + return { value: merged } + } + } return { value: opts.authConfig[key] } } const kebabKey = isCamelCase(key) ? kebabCase(key) : key diff --git a/config/commands/test/configGet.test.ts b/config/commands/test/configGet.test.ts index 6a9c36298c..edebf26c37 100644 --- a/config/commands/test/configGet.test.ts +++ b/config/commands/test/configGet.test.ts @@ -234,6 +234,27 @@ test('config get with scoped registry key (global: true)', async () => { expect(getOutputString(getResult)).toBe('https://custom-registry.example.com/') }) +test('config get with scoped registry returns the merged value from pnpm-workspace.yaml (#11492)', async () => { + // .npmrc set the scope to one URL, but pnpm-workspace.yaml's `registries` + // block overrides it. `pnpm config get` must report the URL that + // resolvers/publish actually use, not the raw .npmrc value. + const getResult = await config.handler(createConfigCommandOpts({ + dir: process.cwd(), + cliOptions: {}, + configDir: process.cwd(), + global: false, + authConfig: { + '@scope:registry': 'https://from-npmrc.example.com/', + }, + registries: { + default: 'https://registry.npmjs.org/', + '@scope': 'https://from-workspace-yaml.example.com/', + }, + }), ['get', '@scope:registry']) + + expect(getOutputString(getResult)).toBe('https://from-workspace-yaml.example.com/') +}) + test('config get with scoped registry key that does not exist', async () => { const getResult = await config.handler(createConfigCommandOpts({ dir: process.cwd(), diff --git a/config/reader/test/index.ts b/config/reader/test/index.ts index a4f3ebd6d5..8671b8f7e1 100644 --- a/config/reader/test/index.ts +++ b/config/reader/test/index.ts @@ -561,6 +561,25 @@ test('registries in current directory\'s .npmrc have bigger priority then global }) }) +test('pnpm-workspace.yaml registries override the same scope from .npmrc (#11492)', async () => { + prepareEmpty() + + fs.writeFileSync('.npmrc', '@my-org:registry=https://from-npmrc.example.com/', 'utf8') + writeYamlFileSync('pnpm-workspace.yaml', { + registries: { + '@my-org': 'https://from-workspace-yaml.example.com/', + }, + }) + + const { config } = await getConfig({ + cliOptions: {}, + packageManager: { name: 'pnpm', version: '1.0.0' }, + workspaceDir: process.cwd(), + }) + + expect(config.registries['@my-org']).toBe('https://from-workspace-yaml.example.com/') +}) + test('auth tokens from pnpm auth file override ~/.npmrc', async () => { prepareEmpty() diff --git a/pnpm/test/config/get.ts b/pnpm/test/config/get.ts index 227549f093..56a47f5fd6 100644 --- a/pnpm/test/config/get.ts +++ b/pnpm/test/config/get.ts @@ -27,14 +27,16 @@ test('pnpm config get reads npm options but ignores other settings from .npmrc', 'packages[]=qux', ].join('\n')) + // `config get @:registry` reports the merged (normalized) URL — + // the same one `pnpm publish` and the resolvers use — see #11492. { const { stdout } = execPnpmSync(['config', 'get', '@my-org:registry'], { expectSuccess: true }) - expect(stdout.toString().trim()).toBe('https://my-org.registry.example.com') + expect(stdout.toString().trim()).toBe('https://my-org.registry.example.com/') } { const { stdout } = execPnpmSync(['config', 'get', '@jsr:registry'], { expectSuccess: true }) - expect(stdout.toString().trim()).toBe('https://not-actually-jsr.example.com') + expect(stdout.toString().trim()).toBe('https://not-actually-jsr.example.com/') } {