Skip to content

Commit f7456da

Browse files
committed
fix: npm update --save
Previously `npm update` was not respecting the `save` option, it would be impossible for users to use `npm update` and automatically update their `package.json` files. This fixes it by adding extra steps on `Arborist.reify._saveIdealTree` to read direct dependencies of any `package.json` and update them as needed when reifying using the `update` and `save` options. Fixes: #708 Fixes: #2704 Relates to: npm/feedback#270
1 parent 0d3cf8b commit f7456da

4 files changed

Lines changed: 226 additions & 8 deletions

File tree

workspaces/arborist/lib/arborist/build-ideal-tree.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ const _complete = Symbol('complete')
4141
const _depsSeen = Symbol('depsSeen')
4242
const _depsQueue = Symbol('depsQueue')
4343
const _currentDep = Symbol('currentDep')
44-
const _updateAll = Symbol('updateAll')
44+
const _updateAll = Symbol.for('updateAll')
4545
const _mutateTree = Symbol('mutateTree')
4646
const _flagsSuspect = Symbol.for('flagsSuspect')
4747
const _workspaces = Symbol.for('workspaces')

workspaces/arborist/lib/arborist/reify.js

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ const _bundleUnpacked = Symbol('bundleUnpacked')
5858
const _bundleMissing = Symbol('bundleMissing')
5959
const _reifyNode = Symbol.for('reifyNode')
6060
const _extractOrLink = Symbol('extractOrLink')
61+
const _updateAll = Symbol.for('updateAll')
62+
const _updateNames = Symbol.for('updateNames')
6163
// defined by rebuild mixin
6264
const _checkBins = Symbol.for('checkBins')
6365
const _symlink = Symbol('symlink')
@@ -1148,13 +1150,8 @@ module.exports = cls => class Reifier extends cls {
11481150
process.emit('time', 'reify:save')
11491151

11501152
const updatedTrees = new Set()
1151-
1152-
// resolvedAdd is the list of user add requests, but with names added
1153-
// to things like git repos and tarball file/urls. However, if the
1154-
// user requested 'foo@', and we have a foo@file:../foo, then we should
1155-
// end up saving the spec we actually used, not whatever they gave us.
1156-
if (this[_resolvedAdd].length) {
1157-
for (const { name, tree: addTree } of this[_resolvedAdd]) {
1153+
const updateNodes = nodes => {
1154+
for (const { name, tree: addTree } of nodes) {
11581155
// addTree either the root, or a workspace
11591156
const edge = addTree.edgesOut.get(name)
11601157
const pkg = addTree.package
@@ -1259,6 +1256,56 @@ module.exports = cls => class Reifier extends cls {
12591256
}
12601257
}
12611258

1259+
// helper that retrieves an array of nodes that were
1260+
// potentially updated during the reify process, in order
1261+
// to limit the number of nodes to check and update, only
1262+
// select nodes from the inventory that are direct deps
1263+
// of a given package.json (project root or a workspace)
1264+
// and in ase of using a list of `names`, restrict nodes
1265+
// to only names that are found in this list
1266+
const retrieveUpdatedNodes = names => {
1267+
const filterDirectDependencies = node =>
1268+
!node.isRoot && node.resolveParent.isRoot
1269+
&& (!names || names.includes(node.name))
1270+
const directDeps = this.idealTree.inventory
1271+
.filter(filterDirectDependencies)
1272+
1273+
// traverses the list of direct dependencies and collect all nodes
1274+
// to be updated, since any of them might have changed during reify
1275+
const nodes = []
1276+
for (const node of directDeps) {
1277+
for (const edgeIn of node.edgesIn) {
1278+
nodes.push({
1279+
name: node.name,
1280+
tree: edgeIn.from.target,
1281+
})
1282+
}
1283+
}
1284+
return nodes
1285+
}
1286+
1287+
// when using update all alongside with save, we'll make
1288+
// sure to refresh every dependency of the root idealTree
1289+
if (this[_updateAll] && options.save) {
1290+
const nodes = retrieveUpdatedNodes()
1291+
updateNodes(nodes)
1292+
} else {
1293+
// resolvedAdd is the list of user add requests, but with names added
1294+
// to things like git repos and tarball file/urls. However, if the
1295+
// user requested 'foo@', and we have a foo@file:../foo, then we should
1296+
// end up saving the spec we actually used, not whatever they gave us.
1297+
if (this[_resolvedAdd].length) {
1298+
updateNodes(this[_resolvedAdd])
1299+
}
1300+
1301+
// if updating given dependencies by name, restrict the list of
1302+
// nodes to check to only those currently in _updateNames
1303+
if (this[_updateNames].length && options.save) {
1304+
const nodes = retrieveUpdatedNodes(this[_updateNames])
1305+
updateNodes(nodes)
1306+
}
1307+
}
1308+
12621309
// preserve indentation, if possible
12631310
const {
12641311
[Symbol.for('indent')]: indent,

workspaces/arborist/tap-snapshots/test/arborist/reify.js.test.cjs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32468,6 +32468,34 @@ exports[`test/arborist/reify.js TAP save complete lockfile on update-all > shoul
3246832468

3246932469
`
3247032470

32471+
exports[`test/arborist/reify.js TAP save package.json on update should update both package.json and package-lock.json using save=true > should update lockfile with same dep range from now updated package.json 1`] = `
32472+
{
32473+
"name": "tap-testdir-reify-save-package.json-on-update-should-update-both-package.json-and-package-lock.json-using-save-true",
32474+
"lockfileVersion": 2,
32475+
"requires": true,
32476+
"packages": {
32477+
"": {
32478+
"dependencies": {
32479+
"abbrev": "^1.1.1"
32480+
}
32481+
},
32482+
"node_modules/abbrev": {
32483+
"version": "1.1.1",
32484+
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
32485+
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
32486+
}
32487+
},
32488+
"dependencies": {
32489+
"abbrev": {
32490+
"version": "1.1.1",
32491+
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
32492+
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
32493+
}
32494+
}
32495+
}
32496+
32497+
`
32498+
3247132499
exports[`test/arborist/reify.js TAP save proper lockfile with bins when upgrading lockfile complete=false > should upgrade, with bins in place 1`] = `
3247232500
{
3247332501
"name": "tap-testdir-reify-save-proper-lockfile-with-bins-when-upgrading-lockfile-complete-false",

workspaces/arborist/test/arborist/reify.js

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2432,3 +2432,146 @@ t.test('add local dep with existing dev + peer/optional', async t => {
24322432
t.equal(tree.children.get('abbrev').resolved, 'file:../../dep', 'resolved')
24332433
t.equal(tree.children.size, 1, 'children')
24342434
})
2435+
2436+
t.test('save package.json on update', t => {
2437+
const pjson = {
2438+
dependencies: {
2439+
abbrev: '^1.0.9',
2440+
},
2441+
}
2442+
const expectedUsingSaveOption = {
2443+
dependencies: {
2444+
abbrev: '^1.1.1',
2445+
},
2446+
}
2447+
const plock = {
2448+
name: 'tap-testdir-reify-save-package.json-on-update-should-update-package-lock.json',
2449+
version: '1.0.0',
2450+
lockfileVersion: 2,
2451+
requires: true,
2452+
packages: {
2453+
'': {
2454+
name: 'a',
2455+
version: '1.0.0',
2456+
license: 'MIT',
2457+
dependencies: {
2458+
abbrev: '^1.0.9',
2459+
},
2460+
},
2461+
'node_modules/abbrev': {
2462+
version: '1.0.9',
2463+
resolved: 'https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz',
2464+
integrity: 'sha1-kbR5JYinc4wl813W9jdSovh3YTU=',
2465+
},
2466+
},
2467+
dependencies: {
2468+
abbrev: {
2469+
version: '1.0.9',
2470+
resolved: 'https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz',
2471+
integrity: 'sha1-kbR5JYinc4wl813W9jdSovh3YTU=',
2472+
},
2473+
},
2474+
}
2475+
2476+
t.test('should not touch package.json on update-all', async t => {
2477+
const path = t.testdir({
2478+
'package.json': JSON.stringify(pjson),
2479+
})
2480+
2481+
await reify(path, { update: true })
2482+
2483+
t.same(require(resolve(path, 'package.json')), pjson,
2484+
'should not have changed package.json file on update all')
2485+
})
2486+
2487+
t.test('should update package-lock.json', async t => {
2488+
const path = t.testdir({
2489+
'package.json': JSON.stringify(pjson),
2490+
'package-lock.json': JSON.stringify(plock),
2491+
})
2492+
2493+
await reify(path, { update: true })
2494+
2495+
t.same(require(resolve(path, 'package.json')), pjson,
2496+
'should not have changed package.json file on update all')
2497+
2498+
// in a package-lock the version gets updated even though the
2499+
// package.json dependency info hasn't been updated
2500+
const lock = require(resolve(path, 'package-lock.json'))
2501+
const lockedVersion = lock.packages['node_modules/abbrev'].version
2502+
t.equal(lockedVersion, '1.1.1',
2503+
'should update locked versions on packages entries')
2504+
const depRange = lock.packages[''].dependencies.abbrev
2505+
t.equal(depRange, '^1.0.9',
2506+
'should have same version range described in package.json')
2507+
})
2508+
2509+
t.test('should not touch package.json on named update', async t => {
2510+
const path = t.testdir({
2511+
'package.json': JSON.stringify(pjson),
2512+
})
2513+
2514+
await reify(path, { update: { names: ['abbrev'] } })
2515+
2516+
t.same(require(resolve(path, 'package.json')), pjson,
2517+
'should not have changed package.json file on named update')
2518+
})
2519+
2520+
t.test('should save to package.json when using save=true', async t => {
2521+
const path = t.testdir({
2522+
'package.json': JSON.stringify(pjson),
2523+
})
2524+
2525+
await reify(path, { update: true, save: true })
2526+
2527+
t.same(require(resolve(path, 'package.json')), expectedUsingSaveOption,
2528+
'should save pkg change to package.json file on update all')
2529+
})
2530+
2531+
t.test('should save to package.json on named update using save=true', async t => {
2532+
const path = t.testdir({
2533+
'package.json': JSON.stringify(pjson),
2534+
})
2535+
2536+
await reify(path, { update: { names: ['abbrev'] }, save: true })
2537+
2538+
t.same(require(resolve(path, 'package.json')), expectedUsingSaveOption,
2539+
'should save pkg change to package.json file on named update')
2540+
})
2541+
2542+
t.test('should update both package.json and package-lock.json using save=true', async t => {
2543+
const path = t.testdir({
2544+
'package.json': JSON.stringify(pjson),
2545+
'package-lock.json': JSON.stringify(plock),
2546+
})
2547+
2548+
await reify(path, { update: true, save: true })
2549+
2550+
t.same(require(resolve(path, 'package.json')), expectedUsingSaveOption,
2551+
'should change package.json file on update all along with lockfile')
2552+
2553+
t.matchSnapshot(fs.readFileSync(resolve(path, 'package-lock.json'), 'utf8'),
2554+
'should update lockfile with same dep range from now updated package.json')
2555+
})
2556+
2557+
t.test('should save to multiple package.json when using save=true', async t => {
2558+
const path = t.testdir({
2559+
'package.json': JSON.stringify({
2560+
...pjson,
2561+
workspaces: ['a'],
2562+
}),
2563+
a: {
2564+
'package.json': JSON.stringify(pjson),
2565+
},
2566+
})
2567+
2568+
await reify(path, { update: true, save: true })
2569+
2570+
t.match(require(resolve(path, 'package.json')), expectedUsingSaveOption,
2571+
'should save pkg change to all package.json files on update all')
2572+
t.same(require(resolve(path, 'a', 'package.json')), expectedUsingSaveOption,
2573+
'should save workspace pkg change to all package.json files on update all')
2574+
})
2575+
2576+
t.end()
2577+
})

0 commit comments

Comments
 (0)