From 0a4dc69bb1fef3cd880b63e58d6eae6b08b5facf Mon Sep 17 00:00:00 2001 From: Ruy Adorno Date: Wed, 26 Jan 2022 18:32:34 -0500 Subject: [PATCH] fix(arborist): update save exact When updating dependencies we need an extra check when filtering nodes to be updated that ensures we do not override semver ranges that are pointing to an exact version. e.g: =1.0.0, 1.0.0 Fixes: https://github.com/npm/cli/issues/4329 --- workspaces/arborist/lib/arborist/reify.js | 18 +++++++ workspaces/arborist/test/arborist/reify.js | 29 ++++++++++ .../reify-cases/update-exact-version.js | 54 +++++++++++++++++++ .../node_modules/abbrev/package.json | 15 ++++++ .../update-exact-version/package-lock.json | 24 +++++++++ .../update-exact-version/package.json | 5 ++ 6 files changed, 145 insertions(+) create mode 100644 workspaces/arborist/test/fixtures/reify-cases/update-exact-version.js create mode 100644 workspaces/arborist/test/fixtures/update-exact-version/node_modules/abbrev/package.json create mode 100644 workspaces/arborist/test/fixtures/update-exact-version/package-lock.json create mode 100644 workspaces/arborist/test/fixtures/update-exact-version/package.json diff --git a/workspaces/arborist/lib/arborist/reify.js b/workspaces/arborist/lib/arborist/reify.js index d5e70323830b6..45ef93985358b 100644 --- a/workspaces/arborist/lib/arborist/reify.js +++ b/workspaces/arborist/lib/arborist/reify.js @@ -5,6 +5,7 @@ const pacote = require('pacote') const AuditReport = require('../audit-report.js') const { subset, intersects } = require('semver') const npa = require('npm-package-arg') +const semver = require('semver') const debug = require('../debug.js') const walkUp = require('walk-up-path') @@ -1273,6 +1274,21 @@ module.exports = cls => class Reifier extends cls { } } + // Returns true if any of the edges from this node has a semver + // range definition that is an exact match to the version installed + // e.g: should return true if for a given an installed version 1.0.0, + // range is either =1.0.0 or 1.0.0 + const exactVersion = node => { + for (const edge of node.edgesIn) { + try { + if (semver.subset(edge.spec, node.version)) { + return false + } + } catch {} + } + return true + } + // helper that retrieves an array of nodes that were // potentially updated during the reify process, in order // to limit the number of nodes to check and update, only @@ -1284,6 +1300,8 @@ module.exports = cls => class Reifier extends cls { const filterDirectDependencies = node => !node.isRoot && node.resolveParent.isRoot && (!names || names.includes(node.name)) + && exactVersion(node) // skip update for exact ranges + const directDeps = this.idealTree.inventory .filter(filterDirectDependencies) diff --git a/workspaces/arborist/test/arborist/reify.js b/workspaces/arborist/test/arborist/reify.js index d5fc166a5636d..caa15f59f2476 100644 --- a/workspaces/arborist/test/arborist/reify.js +++ b/workspaces/arborist/test/arborist/reify.js @@ -2572,5 +2572,34 @@ t.test('save package.json on update', t => { ) }) + t.test('should preserve exact ranges', async t => { + const path = fixture(t, 'update-exact-version') + + await reify(path, { update: true, save: true }) + + t.equal( + require(resolve(path, 'package.json')).dependencies.abbrev, + '1.0.4', + 'should save no top level dep update to root package.json' + ) + }) + + t.test('should preserve exact ranges, missing actual tree', async t => { + const path = t.testdir({ + 'package.json': JSON.stringify({ + dependencies: { + abbrev: '1.0.4', + }, + }), + }) + + await reify(path, { update: true, save: true }) + + t.equal( + require(resolve(path, 'package.json')).dependencies.abbrev, + '1.0.4', + 'should save no top level dep update to root package.json' + ) + }) t.end() }) diff --git a/workspaces/arborist/test/fixtures/reify-cases/update-exact-version.js b/workspaces/arborist/test/fixtures/reify-cases/update-exact-version.js new file mode 100644 index 0000000000000..d766d3bc915ff --- /dev/null +++ b/workspaces/arborist/test/fixtures/reify-cases/update-exact-version.js @@ -0,0 +1,54 @@ +// generated from test/fixtures/update-exact-version +module.exports = t => { + const path = t.testdir({ + "node_modules": { + "abbrev": { + "package.json": JSON.stringify({ + "name": "abbrev", + "version": "1.0.4", + "description": "Like ruby's abbrev module, but in js", + "author": "Isaac Z. Schlueter ", + "main": "./lib/abbrev.js", + "scripts": { + "test": "node lib/abbrev.js" + }, + "repository": "http://github.com/isaacs/abbrev-js", + "license": { + "type": "MIT", + "url": "https://github.com/isaacs/abbrev-js/raw/master/LICENSE" + } + }) + } + }, + "package-lock.json": JSON.stringify({ + "name": "update-exact-version", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "dependencies": { + "abbrev": "1.0.4" + } + }, + "node_modules/abbrev": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.4.tgz", + "integrity": "sha1-vVWuXkE7oXIu5Mq6H26hBBSlns0=" + } + }, + "dependencies": { + "abbrev": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.4.tgz", + "integrity": "sha1-vVWuXkE7oXIu5Mq6H26hBBSlns0=" + } + } + }), + "package.json": JSON.stringify({ + "dependencies": { + "abbrev": "1.0.4" + } + }) +}) + return path +} diff --git a/workspaces/arborist/test/fixtures/update-exact-version/node_modules/abbrev/package.json b/workspaces/arborist/test/fixtures/update-exact-version/node_modules/abbrev/package.json new file mode 100644 index 0000000000000..72042a5b907f4 --- /dev/null +++ b/workspaces/arborist/test/fixtures/update-exact-version/node_modules/abbrev/package.json @@ -0,0 +1,15 @@ +{ + "name": "abbrev", + "version": "1.0.4", + "description": "Like ruby's abbrev module, but in js", + "author": "Isaac Z. Schlueter ", + "main": "./lib/abbrev.js", + "scripts": { + "test": "node lib/abbrev.js" + }, + "repository": "http://github.com/isaacs/abbrev-js", + "license": { + "type": "MIT", + "url": "https://github.com/isaacs/abbrev-js/raw/master/LICENSE" + } +} diff --git a/workspaces/arborist/test/fixtures/update-exact-version/package-lock.json b/workspaces/arborist/test/fixtures/update-exact-version/package-lock.json new file mode 100644 index 0000000000000..0d7b5f6472ebf --- /dev/null +++ b/workspaces/arborist/test/fixtures/update-exact-version/package-lock.json @@ -0,0 +1,24 @@ +{ + "name": "update-exact-version", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "dependencies": { + "abbrev": "1.0.4" + } + }, + "node_modules/abbrev": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.4.tgz", + "integrity": "sha1-vVWuXkE7oXIu5Mq6H26hBBSlns0=" + } + }, + "dependencies": { + "abbrev": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.4.tgz", + "integrity": "sha1-vVWuXkE7oXIu5Mq6H26hBBSlns0=" + } + } +} diff --git a/workspaces/arborist/test/fixtures/update-exact-version/package.json b/workspaces/arborist/test/fixtures/update-exact-version/package.json new file mode 100644 index 0000000000000..4fa41479389d6 --- /dev/null +++ b/workspaces/arborist/test/fixtures/update-exact-version/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "abbrev": "1.0.4" + } +}