diff --git a/doc/misc/npm-config.md b/doc/misc/npm-config.md index 8f04a76010c1a..88d30b62ca252 100644 --- a/doc/misc/npm-config.md +++ b/doc/misc/npm-config.md @@ -179,6 +179,22 @@ a non-zero exit code. What authentication strategy to use with `adduser`/`login`. +### before + +* Alias: enjoy-by +* Default: null +* Type: Date + +If passed to `npm install`, will rebuild the npm tree such that only versions +that were available **on or before** the `--before` time get installed. +If there's no versions available for the current set of direct dependencies, the +command will error. + +If the requested version is a `dist-tag` and the given tag does not pass the +`--before` filter, the most recent version less than or equal to that tag will +be used. For example, `foo@latest` might install `foo@1.2` even though `latest` +is `2.0`. + ### bin-links * Default: `true` diff --git a/lib/config/defaults.js b/lib/config/defaults.js index 2592659539120..f563357d4cba3 100644 --- a/lib/config/defaults.js +++ b/lib/config/defaults.js @@ -113,6 +113,7 @@ Object.defineProperty(exports, 'defaults', {get: function () { 'audit-level': 'low', 'auth-type': 'legacy', + 'before': null, 'bin-links': true, browser: null, @@ -260,6 +261,7 @@ exports.types = { audit: Boolean, 'audit-level': ['low', 'moderate', 'high', 'critical'], 'auth-type': ['legacy', 'sso', 'saml', 'oauth'], + 'before': [null, Date], 'bin-links': Boolean, browser: [null, String], ca: [null, String, Array], @@ -394,6 +396,7 @@ function getLocalAddresses () { } exports.shorthands = { + before: ['--enjoy-by'], s: ['--loglevel', 'silent'], d: ['--loglevel', 'info'], dd: ['--loglevel', 'verbose'], diff --git a/lib/install.js b/lib/install.js index 34a02962c8a8e..d2f705e1d1abd 100644 --- a/lib/install.js +++ b/lib/install.js @@ -703,8 +703,25 @@ Installer.prototype.cloneCurrentTreeToIdealTree = function (cb) { validate('F', arguments) log.silly('install', 'cloneCurrentTreeToIdealTree') - this.idealTree = copyTree(this.currentTree) - this.idealTree.warnings = [] + if (npm.config.get('before')) { + this.idealTree = { + package: this.currentTree.package, + path: this.currentTree.path, + realpath: this.currentTree.realpath, + children: [], + requires: [], + missingDeps: {}, + missingDevDeps: {}, + requiredBy: [], + error: this.currentTree.error, + warnings: [], + isTop: true + } + } else { + this.idealTree = copyTree(this.currentTree) + this.idealTree.warnings = [] + } + cb() } diff --git a/test/tap/install-before.js b/test/tap/install-before.js new file mode 100644 index 0000000000000..c99b996c43029 --- /dev/null +++ b/test/tap/install-before.js @@ -0,0 +1,89 @@ +'use strict' + +const BB = require('bluebird') + +const common = require('../common-tap.js') +const mockTar = require('../util/mock-tarball.js') +const mr = common.fakeRegistry.compat +const path = require('path') +const rimraf = BB.promisify(require('rimraf')) +const Tacks = require('tacks') +const { test } = require('tap') + +const { Dir, File } = Tacks + +const testDir = path.join(__dirname, path.basename(__filename, '.js')) + +let server +test('setup', t => { + mr({}, (err, s) => { + t.ifError(err, 'registry mocked successfully') + server = s + t.end() + }) +}) + +test('installs an npm package before a certain date', t => { + const fixture = new Tacks(Dir({ + 'package.json': File({}) + })) + fixture.create(testDir) + const packument = { + name: 'foo', + 'dist-tags': { latest: '1.2.4' }, + versions: { + '1.2.3': { + name: 'foo', + version: '1.2.3', + dist: { + tarball: `${server.registry}/foo/-/foo-1.2.3.tgz` + } + }, + '1.2.4': { + name: 'foo', + version: '1.2.4', + dist: { + tarball: `${server.registry}/foo/-/foo-1.2.4.tgz` + } + } + }, + time: { + created: '2017-01-01T00:00:01.000Z', + modified: '2018-01-01T00:00:01.000Z', + '1.2.3': '2017-01-01T00:00:01.000Z', + '1.2.4': '2018-01-01T00:00:01.000Z' + } + } + server.get('/foo').reply(200, packument) + return mockTar({ + 'package.json': JSON.stringify({ + name: 'foo', + version: '1.2.3' + }) + }).then(tarball => { + server.get('/foo/-/foo-1.2.3.tgz').reply(200, tarball) + server.get('/foo/-/foo-1.2.4.tgz').reply(500) + return common.npm([ + 'install', 'foo', + '--before', '2018', + '--json', + '--cache', path.join(testDir, 'npmcache'), + '--registry', server.registry + ], { cwd: testDir }) + }).then(([code, stdout, stderr]) => { + t.comment(stdout) + t.comment(stderr) + t.like(JSON.parse(stdout), { + added: [{ + action: 'add', + name: 'foo', + version: '1.2.3' + }] + }, 'installed the 2017 version of the package') + }) +}) + +test('cleanup', t => { + server.close() + return rimraf(testDir) +})