diff --git a/docs/content/using-npm/config.md b/docs/content/using-npm/config.md index e3e1bd6c73bb..4689d340b587 100644 --- a/docs/content/using-npm/config.md +++ b/docs/content/using-npm/config.md @@ -1393,6 +1393,24 @@ The base URL of the npm registry. +#### `replace-registry-host` + +* Default: "npmjs" +* Type: "npmjs", "never", "always", or String + +Defines behavior for replacing the registry host in a lockfile with the +configured registry. + +The default behavior is to replace package dist URLs from the default +registry (https://registry.npmjs.org) to the configured registry. If set to +"never", then use the registry value. If set to "always", then replace the +registry host with the configured host every time. + +You may also specify a bare hostname (e.g., "registry.npmjs.org"). + + + + #### `save` * Default: `true` unless when using `npm update` where it defaults to `false` diff --git a/lib/utils/config/definitions.js b/lib/utils/config/definitions.js index 7d6af2473f2b..04576df0abf8 100644 --- a/lib/utils/config/definitions.js +++ b/lib/utils/config/definitions.js @@ -1649,6 +1649,24 @@ define('registry', { flatten, }) +define('replace-registry-host', { + default: 'npmjs', + hint: ' | hostname', + type: ['npmjs', 'never', 'always', String], + description: ` + Defines behavior for replacing the registry host in a lockfile with the + configured registry. + + The default behavior is to replace package dist URLs from the default + registry (https://registry.npmjs.org) to the configured registry. If set to + "never", then use the registry value. If set to "always", then replace the + registry host with the configured host every time. + + You may also specify a bare hostname (e.g., "registry.npmjs.org"). + `, + flatten, +}) + define('save', { default: true, defaultDescription: `\`true\` unless when using \`npm update\` where it diff --git a/tap-snapshots/test/lib/commands/config.js.test.cjs b/tap-snapshots/test/lib/commands/config.js.test.cjs index fe392344ed35..5c3f86415dff 100644 --- a/tap-snapshots/test/lib/commands/config.js.test.cjs +++ b/tap-snapshots/test/lib/commands/config.js.test.cjs @@ -121,6 +121,7 @@ exports[`test/lib/commands/config.js TAP config list --json > output matches sna "read-only": false, "rebuild-bundle": true, "registry": "https://registry.npmjs.org/", + "replace-registry-host": "npmjs", "save": true, "save-bundle": false, "save-dev": false, @@ -277,6 +278,7 @@ proxy = null read-only = false rebuild-bundle = true registry = "https://registry.npmjs.org/" +replace-registry-host = "npmjs" save = true save-bundle = false save-dev = false diff --git a/tap-snapshots/test/lib/utils/config/definitions.js.test.cjs b/tap-snapshots/test/lib/utils/config/definitions.js.test.cjs index 89c9969d6942..324118f1de1b 100644 --- a/tap-snapshots/test/lib/utils/config/definitions.js.test.cjs +++ b/tap-snapshots/test/lib/utils/config/definitions.js.test.cjs @@ -116,6 +116,7 @@ Array [ "read-only", "rebuild-bundle", "registry", + "replace-registry-host", "save", "save-bundle", "save-dev", @@ -1460,6 +1461,23 @@ exports[`test/lib/utils/config/definitions.js TAP > config description for regis The base URL of the npm registry. ` +exports[`test/lib/utils/config/definitions.js TAP > config description for replace-registry-host 1`] = ` +#### \`replace-registry-host\` + +* Default: "npmjs" +* Type: "npmjs", "never", "always", or String + +Defines behavior for replacing the registry host in a lockfile with the +configured registry. + +The default behavior is to replace package dist URLs from the default +registry (https://registry.npmjs.org) to the configured registry. If set to +"never", then use the registry value. If set to "always", then replace the +registry host with the configured host every time. + +You may also specify a bare hostname (e.g., "registry.npmjs.org"). +` + exports[`test/lib/utils/config/definitions.js TAP > config description for save 1`] = ` #### \`save\` diff --git a/tap-snapshots/test/lib/utils/config/describe-all.js.test.cjs b/tap-snapshots/test/lib/utils/config/describe-all.js.test.cjs index a9247f49c041..032caefbda18 100644 --- a/tap-snapshots/test/lib/utils/config/describe-all.js.test.cjs +++ b/tap-snapshots/test/lib/utils/config/describe-all.js.test.cjs @@ -1266,6 +1266,24 @@ The base URL of the npm registry. +#### \`replace-registry-host\` + +* Default: "npmjs" +* Type: "npmjs", "never", "always", or String + +Defines behavior for replacing the registry host in a lockfile with the +configured registry. + +The default behavior is to replace package dist URLs from the default +registry (https://registry.npmjs.org) to the configured registry. If set to +"never", then use the registry value. If set to "always", then replace the +registry host with the configured host every time. + +You may also specify a bare hostname (e.g., "registry.npmjs.org"). + + + + #### \`save\` * Default: \`true\` unless when using \`npm update\` where it defaults to \`false\` diff --git a/workspaces/arborist/lib/arborist/index.js b/workspaces/arborist/lib/arborist/index.js index cb6ef1e0c2cc..9564f7648f92 100644 --- a/workspaces/arborist/lib/arborist/index.js +++ b/workspaces/arborist/lib/arborist/index.js @@ -74,8 +74,12 @@ class Arborist extends Base { cache: options.cache || `${homedir()}/.npm/_cacache`, packumentCache: options.packumentCache || new Map(), workspacesEnabled: options.workspacesEnabled !== false, + replaceRegistryHost: options.replaceRegistryHost, lockfileVersion: lockfileVersion(options.lockfileVersion), } + this.replaceRegistryHost = this.options.replaceRegistryHost = + (!this.options.replaceRegistryHost || this.options.replaceRegistryHost === 'npmjs') ? + 'registry.npmjs.org' : this.options.replaceRegistryHost this[_workspacesEnabled] = this.options.workspacesEnabled diff --git a/workspaces/arborist/lib/arborist/reify.js b/workspaces/arborist/lib/arborist/reify.js index 4f1061e4abe5..7663a3a342cc 100644 --- a/workspaces/arborist/lib/arborist/reify.js +++ b/workspaces/arborist/lib/arborist/reify.js @@ -712,13 +712,19 @@ module.exports = cls => class Reifier extends cls { [_registryResolved] (resolved) { // the default registry url is a magic value meaning "the currently // configured registry". + // `resolved` must never be falsey. // // XXX: use a magic string that isn't also a valid value, like // ${REGISTRY} or something. This has to be threaded through the // Shrinkwrap and Node classes carefully, so for now, just treat // the default reg as the magical animal that it has been. - return resolved && resolved - .replace(/^https?:\/\/registry\.npmjs\.org\//, this.registry) + const resolvedURL = new URL(resolved) + if ((this.options.replaceRegistryHost === resolvedURL.hostname) + || this.options.replaceRegistryHost === 'always') { + // this.registry always has a trailing slash + resolved = `${this.registry.slice(0, -1)}${resolvedURL.pathname}${resolvedURL.searchParams}` + } + return resolved } // bundles are *sort of* like shrinkwraps, in that the branch is defined diff --git a/workspaces/arborist/test/arborist/index.js b/workspaces/arborist/test/arborist/index.js index 3469c5c73591..7f69eb36ef74 100644 --- a/workspaces/arborist/test/arborist/index.js +++ b/workspaces/arborist/test/arborist/index.js @@ -236,3 +236,12 @@ t.test('lockfileVersion config validation', async t => { message: 'Invalid lockfileVersion config: banana', }) }) + +t.test('valid replaceRegistryHost values', t => { + t.equal(new Arborist({ replaceRegistryHost: 'registry.garbage.com' }).options.replaceRegistryHost, 'registry.garbage.com') + t.equal(new Arborist({ replaceRegistryHost: 'npmjs' }).options.replaceRegistryHost, 'registry.npmjs.org') + t.equal(new Arborist({ replaceRegistryHost: undefined }).options.replaceRegistryHost, 'registry.npmjs.org') + t.equal(new Arborist({ replaceRegistryHost: 'always' }).options.replaceRegistryHost, 'always') + t.equal(new Arborist({ replaceRegistryHost: 'never' }).options.replaceRegistryHost, 'never') + t.end() +}) diff --git a/workspaces/arborist/test/arborist/reify.js b/workspaces/arborist/test/arborist/reify.js index 406b4281dc5b..db5a9c1fe1af 100644 --- a/workspaces/arborist/test/arborist/reify.js +++ b/workspaces/arborist/test/arborist/reify.js @@ -2,6 +2,7 @@ const { join, resolve, basename } = require('path') const t = require('tap') const runScript = require('@npmcli/run-script') const localeCompare = require('@isaacs/string-locale-compare')('en') +const tnock = require('../fixtures/tnock') // mock rimraf so we can make it fail in rollback tests const realRimraf = require('rimraf') @@ -2923,3 +2924,132 @@ t.test('installLinks', (t) => { t.end() }) + +t.only('should preserve exact ranges, missing actual tree', async (t) => { + const Arborist = require('../../lib/index.js') + const abbrev = resolve(__dirname, + '../fixtures/registry-mocks/content/abbrev/-/abbrev-1.1.1.tgz') + const abbrevTGZ = fs.readFileSync(abbrev) + + const abbrevPackument = JSON.stringify({ + _id: 'abbrev', + _rev: 'lkjadflkjasdf', + name: 'abbrev', + 'dist-tags': { latest: '1.1.1' }, + versions: { + '1.1.1': { + name: 'abbrev', + version: '1.1.1', + dist: { + tarball: 'https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz', + }, + }, + }, + }) + + const abbrevPackument2 = JSON.stringify({ + _id: 'abbrev', + _rev: 'lkjadflkjasdf', + name: 'abbrev', + 'dist-tags': { latest: '1.1.1' }, + versions: { + '1.1.1': { + name: 'abbrev', + version: '1.1.1', + dist: { + tarball: 'https://registry.garbage.org/abbrev/-/abbrev-1.1.1.tgz', + }, + }, + }, + }) + + t.only('host should not be replaced replaceRegistryHost=never', async (t) => { + const testdir = t.testdir({ + project: { + 'package.json': JSON.stringify({ + name: 'myproject', + version: '1.0.0', + dependencies: { + abbrev: '1.1.1', + }, + }), + }, + }) + + tnock(t, 'https://registry.github.com') + .get('/abbrev') + .reply(200, abbrevPackument) + + tnock(t, 'https://registry.npmjs.org') + .get('/abbrev/-/abbrev-1.1.1.tgz') + .reply(200, abbrevTGZ) + + const arb = new Arborist({ + path: resolve(testdir, 'project'), + registry: 'https://registry.github.com', + cache: resolve(testdir, 'cache'), + replaceRegistryHost: 'never', + }) + await arb.reify() + }) + + t.only('host should be replaced replaceRegistryHost=npmjs', async (t) => { + const testdir = t.testdir({ + project: { + 'package.json': JSON.stringify({ + name: 'myproject', + version: '1.0.0', + dependencies: { + abbrev: '1.1.1', + }, + }), + }, + }) + + tnock(t, 'https://registry.github.com') + .get('/abbrev') + .reply(200, abbrevPackument) + + tnock(t, 'https://registry.github.com') + .get('/abbrev/-/abbrev-1.1.1.tgz') + .reply(200, abbrevTGZ) + + const arb = new Arborist({ + path: resolve(testdir, 'project'), + registry: 'https://registry.github.com', + cache: resolve(testdir, 'cache'), + replaceRegistryHost: 'npmjs', + }) + await arb.reify() + }) + + t.only('host should be always replaceRegistryHost=always', async (t) => { + const testdir = t.testdir({ + project: { + 'package.json': JSON.stringify({ + name: 'myproject', + version: '1.0.0', + dependencies: { + abbrev: '1.1.1', + }, + }), + }, + }) + + tnock(t, 'https://registry.github.com') + .get('/abbrev') + .reply(200, abbrevPackument2) + + tnock(t, 'https://registry.github.com') + .get('/abbrev/-/abbrev-1.1.1.tgz') + .reply(200, abbrevTGZ) + + const arb = new Arborist({ + path: resolve(testdir, 'project'), + registry: 'https://registry.github.com', + cache: resolve(testdir, 'cache'), + replaceRegistryHost: 'always', + }) + await arb.reify() + }) +}) diff --git a/workspaces/arborist/test/fixtures/tnock.js b/workspaces/arborist/test/fixtures/tnock.js new file mode 100644 index 000000000000..2e07f7364789 --- /dev/null +++ b/workspaces/arborist/test/fixtures/tnock.js @@ -0,0 +1,14 @@ +'use strict' + +const nock = require('nock') + +module.exports = tnock +function tnock (t, host) { + const server = nock(host) + nock.disableNetConnect() + t.teardown(function () { + nock.enableNetConnect() + server.done() + }) + return server +}