diff --git a/workspaces/arborist/lib/arborist/reify.js b/workspaces/arborist/lib/arborist/reify.js index b099d4d72c6a4..037488509c28d 100644 --- a/workspaces/arborist/lib/arborist/reify.js +++ b/workspaces/arborist/lib/arborist/reify.js @@ -32,6 +32,7 @@ const Shrinkwrap = require('../shrinkwrap.js') const { defaultLockfileVersion } = Shrinkwrap const { saveTypeMap, hasSubKey } = require('../add-rm-pkg-deps.js') const { IsolatedNode, IsolatedLink } = require('../isolated-classes.js') +const { isReleaseAgeExcluded } = require('../release-age-exclude.js') // Part of steps (steps need refactoring before we can do anything about these) const _retireShallowNodes = Symbol.for('retireShallowNodes') @@ -737,8 +738,20 @@ module.exports = cls => class Reifier extends cls { }) } }) + // When node.resolved is missing (e.g. a lockfile written with + // omit-lockfile-registry-resolved) `res` above is a bare `name@version` + // spec, so pacote has to hit the registry and re-run version-picking + // (including the `min-release-age`/`before` cutoff) to find the tarball. + // That re-resolution must honor `min-release-age-exclude` the same way + // build-ideal-tree's #releaseAgeBefore does, or a package that was + // correctly exempted while building the ideal tree can still get + // rejected here with ETARGET. + const before = isReleaseAgeExcluded(node.name, this.options.minReleaseAgeExclude) + ? null + : this.options.before await pacote.extract(res, node.path, { ...this.options, + before, resolved: node.resolved, integrity: node.integrity, // A node counts as "root" for allow-* enforcement if it satisfies at least one valid dependency edge declared by the project root or a workspace. diff --git a/workspaces/arborist/test/arborist/reify.js b/workspaces/arborist/test/arborist/reify.js index d453c79baa325..f3b74981f333e 100644 --- a/workspaces/arborist/test/arborist/reify.js +++ b/workspaces/arborist/test/arborist/reify.js @@ -167,6 +167,56 @@ t.test('weirdly broken lockfile without resolved value', async t => { await t.resolveMatchSnapshot(printReified(fixture(t, 'dep-missing-resolved'))) }) +t.test('min-release-age-exclude applies when reify re-resolves a node with no resolved value', async t => { + // minimist@1.2.5 was published 2020-03-12T22:16:19.463Z (see + // ../fixtures/registry-mocks/content/minimist.json). A lockfile entry + // missing `resolved` (e.g. written with omit-lockfile-registry-resolved) + // forces reify's #extractOrLink to re-resolve `minimist@1.2.5` against the + // registry instead of extracting directly from a known tarball URL. That + // re-resolution must still honor min-release-age-exclude the same way + // build-ideal-tree's manifest fetches do. + const before = new Date('2020-01-01T00:00:00.000Z') + + const mkPath = t => t.testdir({ + 'package.json': JSON.stringify({ + name: 'root', + version: '1.0.0', + dependencies: { minimist: '1.2.5' }, + }), + 'package-lock.json': JSON.stringify({ + name: 'root', + version: '1.0.0', + lockfileVersion: 2, + requires: true, + packages: { + '': { + name: 'root', + version: '1.0.0', + dependencies: { minimist: '1.2.5' }, + }, + 'node_modules/minimist': { + version: '1.2.5', + }, + }, + }, null, 2), + }) + + await t.test('without exclude, before still applies and fails', async t => { + createRegistry(t, true) + await t.rejects( + reify(mkPath(t), { before }), + { code: 'ETARGET' }, + 'the already-locked version is rejected by the release-age filter' + ) + }) + + await t.test('with exclude, the already-locked version installs', async t => { + createRegistry(t, true) + const tree = await reify(mkPath(t), { before, minReleaseAgeExclude: ['minimist'] }) + t.equal(tree.children.get('minimist').version, '1.2.5', 'excluded package still installs') + }) +}) + t.test('testing-peer-deps package', async t => { createRegistry(t, true) await t.resolveMatchSnapshot(printReified(fixture(t, 'testing-peer-deps')))