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" + } +}