Skip to content

Commit cd99dfd

Browse files
ljharbclaude
andcommitted
feat: add publish-registry config option
Adds a new `publish-registry` config option that allows setting a separate registry URL for `npm publish` and `npm unpublish`, while leaving the `registry` config in effect for all other operations like install and view. This enables workflows like using a local caching proxy (e.g. VSR, Verdaccio) for reads while publishing directly to the public npm registry, without needing per-package publishConfig or shell aliases. When set in .npmrc: registry=http://localhost:1337/npm publish-registry=https://registry.npmjs.org/ All installs/views go through the local proxy, while publishes go directly to npmjs.org. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a774fb7 commit cd99dfd

6 files changed

Lines changed: 115 additions & 1 deletion

File tree

lib/commands/publish.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ class Publish extends BaseCommand {
2626
'access',
2727
'dry-run',
2828
'otp',
29+
'publish-registry',
2930
'workspace',
3031
'workspaces',
3132
'include-workspace-root',
@@ -82,6 +83,10 @@ class Publish extends BaseCommand {
8283
}
8384

8485
const opts = { ...this.npm.flatOptions, progress: false }
86+
const publishRegistry = this.npm.config.get('publish-registry')
87+
if (publishRegistry) {
88+
opts.registry = publishRegistry
89+
}
8590

8691
// you can publish name@version, ./foo.tgz, etc.
8792
// even though the default is the 'file:.' cwd.

lib/commands/unpublish.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const LAST_REMAINING_VERSION_ERROR = 'Refusing to delete the last version of the
1616
class Unpublish extends BaseCommand {
1717
static description = 'Remove a package from the registry'
1818
static name = 'unpublish'
19-
static params = ['dry-run', 'force', 'workspace', 'workspaces']
19+
static params = ['dry-run', 'force', 'publish-registry', 'workspace', 'workspaces']
2020
static usage = ['[<package-spec>]']
2121
static workspaces = true
2222
static ignoreImplicitWorkspace = false
@@ -103,6 +103,10 @@ class Unpublish extends BaseCommand {
103103
}
104104

105105
const opts = { ...this.npm.flatOptions }
106+
const publishRegistry = this.npm.config.get('publish-registry')
107+
if (publishRegistry) {
108+
opts.registry = publishRegistry
109+
}
106110

107111
let manifest
108112
try {

tap-snapshots/test/lib/commands/publish.js.test.cjs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,10 +288,18 @@ exports[`test/lib/commands/publish.js TAP public access > new package version 1`
288288
+ @npm/test-package@1.0.0
289289
`
290290

291+
exports[`test/lib/commands/publish.js TAP publish-registry config overridden by publishConfig.registry > new package version 1`] = `
292+
+ @npmcli/test-package@1.0.0
293+
`
294+
291295
exports[`test/lib/commands/publish.js TAP re-loads publishConfig.registry if added during script process > new package version 1`] = `
292296
+ @npmcli/test-package@1.0.0
293297
`
294298

299+
exports[`test/lib/commands/publish.js TAP respects publish-registry config > new package version 1`] = `
300+
+ @npmcli/test-package@1.0.0
301+
`
302+
295303
exports[`test/lib/commands/publish.js TAP respects publishConfig.registry, runs appropriate scripts > new package version 1`] = `
296304
297305
> @npmcli/test-package@1.0.0 prepublishOnly

test/lib/commands/publish.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,64 @@ t.test('respects publishConfig.registry, runs appropriate scripts', async t => {
5858
t.same(logs.warn, ['Unknown publishConfig config "other". This will stop working in the next major version of npm. See `npm help npmrc` for supported config options.'])
5959
})
6060

61+
t.test('respects publish-registry config', async t => {
62+
const publishRegistry = alternateRegistry
63+
const { joinedOutput, npm, registry } = await loadNpmWithRegistry(t, {
64+
config: {
65+
'publish-registry': publishRegistry,
66+
[`${publishRegistry.slice(6)}/:_authToken`]: 'test-other-token',
67+
},
68+
prefixDir: {
69+
'package.json': JSON.stringify(pkgJson, null, 2),
70+
},
71+
registry: publishRegistry,
72+
authorization: 'test-other-token',
73+
})
74+
registry.getPackage(pkg, { times: 2, code: 404 })
75+
registry.putPackage(pkg, { packageJson: pkgJson, registry: publishRegistry })
76+
await npm.exec('publish', [])
77+
t.matchSnapshot(joinedOutput(), 'new package version')
78+
})
79+
80+
t.test('publish-registry config overridden by publishConfig.registry', async t => {
81+
const publishRegistry = alternateRegistry
82+
const thirdRegistry = 'https://third.registry.npmjs.org'
83+
const packageJson = {
84+
...pkgJson,
85+
publishConfig: { registry: thirdRegistry },
86+
}
87+
const { joinedOutput, npm, registry } = await loadNpmWithRegistry(t, {
88+
config: {
89+
'publish-registry': publishRegistry,
90+
[`${thirdRegistry.slice(6)}/:_authToken`]: 'test-third-token',
91+
},
92+
prefixDir: {
93+
'package.json': JSON.stringify(packageJson, null, 2),
94+
},
95+
registry: thirdRegistry,
96+
authorization: 'test-third-token',
97+
})
98+
registry.publish(pkg, { packageJson })
99+
await npm.exec('publish', [])
100+
t.matchSnapshot(joinedOutput(), 'new package version')
101+
})
102+
103+
t.test('publish-registry config does not affect install registry', async t => {
104+
const publishRegistry = alternateRegistry
105+
const { npm } = await loadNpmWithRegistry(t, {
106+
config: {
107+
'publish-registry': publishRegistry,
108+
...auth,
109+
},
110+
prefixDir: {
111+
'package.json': JSON.stringify(pkgJson, null, 2),
112+
},
113+
authorization: token,
114+
})
115+
t.equal(npm.config.get('registry'), 'https://registry.npmjs.org/')
116+
t.ok(npm.config.get('publish-registry').startsWith(publishRegistry))
117+
})
118+
61119
t.test('re-loads publishConfig.registry if added during script process', async t => {
62120
const initPackageJson = {
63121
...pkgJson,

test/lib/commands/unpublish.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,34 @@ t.test('dryRun with no args', async t => {
378378
t.equal(joinedOutput(), '- test-package@1.0.0')
379379
})
380380

381+
t.test('publish-registry config', async t => {
382+
const alternateRegistry = 'https://other.registry.npmjs.org'
383+
const { joinedOutput, npm } = await loadMockNpm(t, {
384+
config: {
385+
force: true,
386+
'publish-registry': alternateRegistry,
387+
'//other.registry.npmjs.org/:_authToken': 'test-other-token',
388+
},
389+
prefixDir: {
390+
'package.json': JSON.stringify({
391+
name: pkg,
392+
version: '1.0.0',
393+
}, null, 2),
394+
},
395+
})
396+
397+
const registry = new MockRegistry({
398+
tap: t,
399+
registry: alternateRegistry,
400+
authorization: 'test-other-token',
401+
})
402+
const manifest = registry.manifest({ name: pkg })
403+
await registry.package({ manifest, query: { write: true }, times: 2 })
404+
registry.unpublish({ manifest })
405+
await npm.exec('unpublish', [])
406+
t.equal(joinedOutput(), '- test-package')
407+
})
408+
381409
t.test('publishConfig no spec', async t => {
382410
const alternateRegistry = 'https://other.registry.npmjs.org'
383411
const { logs, joinedOutput, npm } = await loadMockNpm(t, {

workspaces/config/lib/definitions/definitions.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1726,6 +1726,17 @@ const definitions = {
17261726
`,
17271727
flatten,
17281728
}),
1729+
'publish-registry': new Definition('publish-registry', {
1730+
default: null,
1731+
type: [null, url],
1732+
description: `
1733+
The base URL of the npm registry to use for \`npm publish\` and
1734+
\`npm unpublish\`. When set, overrides \`registry\` for these
1735+
commands while leaving \`registry\` in effect for all other
1736+
operations like install and view.
1737+
`,
1738+
flatten,
1739+
}),
17291740
registry: new Definition('registry', {
17301741
default: 'https://registry.npmjs.org/',
17311742
type: url,

0 commit comments

Comments
 (0)