Skip to content

Commit 7c6a91c

Browse files
committed
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.
1 parent a774fb7 commit 7c6a91c

8 files changed

Lines changed: 141 additions & 2 deletions

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/config.js.test.cjs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ exports[`test/lib/commands/config.js TAP config list --json > output matches sna
135135
"proxy": null,
136136
"read-only": false,
137137
"rebuild-bundle": true,
138+
"publish-registry": null,
138139
"registry": "https://registry.npmjs.org/",
139140
"replace-registry-host": "npmjs",
140141
"save": true,
@@ -315,6 +316,7 @@ progress = {PROGRESS}
315316
provenance = false
316317
provenance-file = null
317318
proxy = null
319+
publish-registry = null
318320
read-only = false
319321
rebuild-bundle = true
320322
registry = "https://registry.npmjs.org/"

‎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

‎tap-snapshots/test/lib/docs.js.test.cjs‎

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1459,6 +1459,17 @@ by the underlying \`request\` library.
14591459
14601460
14611461
1462+
#### \`publish-registry\`
1463+
1464+
* Default: null
1465+
* Type: null or URL
1466+
1467+
The base URL of the npm registry to use for \`npm publish\` and \`npm
1468+
unpublish\`. When set, overrides \`registry\` for these commands while leaving
1469+
\`registry\` in effect for all other operations like install and view.
1470+
1471+
1472+
14621473
#### \`read-only\`
14631474
14641475
* Default: false
@@ -2364,6 +2375,7 @@ Array [
23642375
"proxy",
23652376
"read-only",
23662377
"rebuild-bundle",
2378+
"publish-registry",
23672379
"registry",
23682380
"replace-registry-host",
23692381
"save",
@@ -2519,6 +2531,7 @@ Array [
25192531
"proxy",
25202532
"read-only",
25212533
"rebuild-bundle",
2534+
"publish-registry",
25222535
"registry",
25232536
"replace-registry-host",
25242537
"save",
@@ -2692,6 +2705,7 @@ Object {
26922705
"provenance": false,
26932706
"provenanceFile": null,
26942707
"proxy": null,
2708+
"publishRegistry": null,
26952709
"readOnly": false,
26962710
"rebuildBundle": true,
26972711
"registry": "https://registry.npmjs.org/",
@@ -5055,6 +5069,7 @@ npm publish <package-spec>
50555069
50565070
Options:
50575071
[--tag <tag>] [--access <restricted|public>] [--dry-run] [--otp <otp>]
5072+
[--publish-registry <publish-registry>]
50585073
[-w|--workspace <workspace-name> [-w|--workspace <workspace-name> ...]]
50595074
[--workspaces] [--include-workspace-root] [--provenance|--provenance-file <file>]
50605075
@@ -5070,6 +5085,9 @@ Options:
50705085
--otp
50715086
This is a one-time password from a two-factor authenticator. It's needed
50725087
5088+
--publish-registry
5089+
The base URL of the npm registry to use for \`npm publish\` and
5090+
50735091
-w|--workspace
50745092
Enable running a command in the context of the configured workspaces of the
50755093
@@ -5093,6 +5111,7 @@ npm publish <package-spec>
50935111
#### \`access\`
50945112
#### \`dry-run\`
50955113
#### \`otp\`
5114+
#### \`publish-registry\`
50965115
#### \`workspace\`
50975116
#### \`workspaces\`
50985117
#### \`include-workspace-root\`
@@ -5910,7 +5929,7 @@ Usage:
59105929
npm unpublish [<package-spec>]
59115930
59125931
Options:
5913-
[--dry-run] [-f|--force]
5932+
[--dry-run] [-f|--force] [--publish-registry <publish-registry>]
59145933
[-w|--workspace <workspace-name> [-w|--workspace <workspace-name> ...]]
59155934
[--workspaces]
59165935
@@ -5920,6 +5939,9 @@ Options:
59205939
-f|--force
59215940
Removes various protections against unfortunate side effects, common
59225941
5942+
--publish-registry
5943+
The base URL of the npm registry to use for \`npm publish\` and
5944+
59235945
-w|--workspace
59245946
Enable running a command in the context of the configured workspaces of the
59255947
@@ -5935,6 +5957,7 @@ npm unpublish [<package-spec>]
59355957
59365958
#### \`dry-run\`
59375959
#### \`force\`
5960+
#### \`publish-registry\`
59385961
#### \`workspace\`
59395962
#### \`workspaces\`
59405963
`

‎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.equal(npm.config.get('publish-registry'), alternateRegistry + '/')
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)