@npmcli/arborist 4.1.2 → 4.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -41,7 +41,7 @@ const _complete = Symbol('complete')
41
41
  const _depsSeen = Symbol('depsSeen')
42
42
  const _depsQueue = Symbol('depsQueue')
43
43
  const _currentDep = Symbol('currentDep')
44
- const _updateAll = Symbol('updateAll')
44
+ const _updateAll = Symbol.for('updateAll')
45
45
  const _mutateTree = Symbol('mutateTree')
46
46
  const _flagsSuspect = Symbol.for('flagsSuspect')
47
47
  const _workspaces = Symbol.for('workspaces')
@@ -176,7 +176,7 @@ module.exports = cls => class IdealTreeBuilder extends cls {
176
176
  // public method
177
177
  async buildIdealTree (options = {}) {
178
178
  if (this.idealTree) {
179
- return Promise.resolve(this.idealTree)
179
+ return this.idealTree
180
180
  }
181
181
 
182
182
  // allow the user to set reify options on the ctor as well.
@@ -194,8 +194,7 @@ module.exports = cls => class IdealTreeBuilder extends cls {
194
194
  process.emit('time', 'idealTree')
195
195
 
196
196
  if (!options.add && !options.rm && !options.update && this[_global]) {
197
- const er = new Error('global requires add, rm, or update option')
198
- return Promise.reject(er)
197
+ throw new Error('global requires add, rm, or update option')
199
198
  }
200
199
 
201
200
  // first get the virtual tree, if possible. If there's a lockfile, then
@@ -270,6 +269,22 @@ module.exports = cls => class IdealTreeBuilder extends cls {
270
269
  this[_complete] = !!options.complete
271
270
  this[_preferDedupe] = !!options.preferDedupe
272
271
  this[_legacyBundling] = !!options.legacyBundling
272
+
273
+ // validates list of update names, they must
274
+ // be dep names only, no semver ranges are supported
275
+ for (const name of update.names) {
276
+ const spec = npa(name)
277
+ const validationError =
278
+ new TypeError(`Update arguments must not contain package version specifiers
279
+
280
+ Try using the package name instead, e.g:
281
+ npm update ${spec.name}`)
282
+ validationError.code = 'EUPDATEARGS'
283
+
284
+ if (spec.fetchSpec !== 'latest') {
285
+ throw validationError
286
+ }
287
+ }
273
288
  this[_updateNames] = update.names
274
289
 
275
290
  this[_updateAll] = update.all
@@ -321,7 +336,7 @@ module.exports = cls => class IdealTreeBuilder extends cls {
321
336
  // Load on a new Arborist object, so the Nodes aren't the same,
322
337
  // or else it'll get super confusing when we change them!
323
338
  .then(async root => {
324
- if (!this[_updateAll] && !this[_global] && !root.meta.loadedFromDisk) {
339
+ if ((!this[_updateAll] && !this[_global] && !root.meta.loadedFromDisk) || (this[_global] && this[_updateNames].length)) {
325
340
  await new this.constructor(this.options).loadActual({ root })
326
341
  const tree = root.target
327
342
  // even though we didn't load it from a package-lock.json FILE,
@@ -334,6 +349,7 @@ module.exports = cls => class IdealTreeBuilder extends cls {
334
349
  root.meta.lockfileVersion = defaultLockfileVersion
335
350
  }
336
351
  }
352
+ root.meta.inferFormattingOptions(root.package)
337
353
  return root
338
354
  })
339
355
 
@@ -1180,6 +1196,11 @@ This is a one-time fix-up, please be patient...
1180
1196
  return true
1181
1197
  }
1182
1198
 
1199
+ // If the edge is a workspace, and it's valid, leave it alone
1200
+ if (edge.to.isWorkspace) {
1201
+ return false
1202
+ }
1203
+
1183
1204
  // user explicitly asked to update this package by name, problem
1184
1205
  if (this[_updateNames].includes(edge.name)) {
1185
1206
  return true
@@ -1229,24 +1250,40 @@ This is a one-time fix-up, please be patient...
1229
1250
  // Don't bother to load the manifest for link deps, because the target
1230
1251
  // might be within another package that doesn't exist yet.
1231
1252
  const { legacyPeerDeps } = this
1232
- return spec.type === 'directory'
1233
- ? this[_linkFromSpec](name, spec, parent, edge)
1234
- : this[_fetchManifest](spec)
1235
- .then(pkg => new Node({ name, pkg, parent, legacyPeerDeps }), error => {
1236
- error.requiredBy = edge.from.location || '.'
1237
-
1238
- // failed to load the spec, either because of enotarget or
1239
- // fetch failure of some other sort. save it so we can verify
1240
- // later that it's optional, otherwise the error is fatal.
1241
- const n = new Node({
1242
- name,
1243
- parent,
1244
- error,
1245
- legacyPeerDeps,
1246
- })
1247
- this[_loadFailures].add(n)
1248
- return n
1253
+
1254
+ // spec is a directory, link it
1255
+ if (spec.type === 'directory') {
1256
+ return this[_linkFromSpec](name, spec, parent, edge)
1257
+ }
1258
+
1259
+ // if the spec matches a workspace name, then see if the workspace node will
1260
+ // satisfy the edge. if it does, we return the workspace node to make sure it
1261
+ // takes priority.
1262
+ if (this.idealTree.workspaces && this.idealTree.workspaces.has(spec.name)) {
1263
+ const existingNode = this.idealTree.edgesOut.get(spec.name).to
1264
+ if (existingNode && existingNode.isWorkspace && existingNode.satisfies(edge)) {
1265
+ return edge.to
1266
+ }
1267
+ }
1268
+
1269
+ // spec isn't a directory, and either isn't a workspace or the workspace we have
1270
+ // doesn't satisfy the edge. try to fetch a manifest and build a node from that.
1271
+ return this[_fetchManifest](spec)
1272
+ .then(pkg => new Node({ name, pkg, parent, legacyPeerDeps }), error => {
1273
+ error.requiredBy = edge.from.location || '.'
1274
+
1275
+ // failed to load the spec, either because of enotarget or
1276
+ // fetch failure of some other sort. save it so we can verify
1277
+ // later that it's optional, otherwise the error is fatal.
1278
+ const n = new Node({
1279
+ name,
1280
+ parent,
1281
+ error,
1282
+ legacyPeerDeps,
1249
1283
  })
1284
+ this[_loadFailures].add(n)
1285
+ return n
1286
+ })
1250
1287
  }
1251
1288
 
1252
1289
  [_linkFromSpec] (name, spec, parent, edge) {
@@ -212,7 +212,8 @@ module.exports = cls => class ActualLoader extends cls {
212
212
  const promises = []
213
213
  for (const path of tree.workspaces.values()) {
214
214
  if (!this[_cache].has(path)) {
215
- const p = this[_loadFSNode]({ path, root: this[_actualTree] })
215
+ // workspace overrides use the root overrides
216
+ const p = this[_loadFSNode]({ path, root: this[_actualTree], useRootOverrides: true })
216
217
  .then(node => this[_loadFSTree](node))
217
218
  promises.push(p)
218
219
  }
@@ -240,7 +241,7 @@ module.exports = cls => class ActualLoader extends cls {
240
241
  this[_actualTree] = root
241
242
  }
242
243
 
243
- [_loadFSNode] ({ path, parent, real, root, loadOverrides }) {
244
+ [_loadFSNode] ({ path, parent, real, root, loadOverrides, useRootOverrides }) {
244
245
  if (!real) {
245
246
  return realpath(path, this[_rpcache], this[_stcache])
246
247
  .then(
@@ -250,6 +251,7 @@ module.exports = cls => class ActualLoader extends cls {
250
251
  real,
251
252
  root,
252
253
  loadOverrides,
254
+ useRootOverrides,
253
255
  }),
254
256
  // if realpath fails, just provide a dummy error node
255
257
  error => new Node({
@@ -289,6 +291,9 @@ module.exports = cls => class ActualLoader extends cls {
289
291
  parent,
290
292
  root,
291
293
  loadOverrides,
294
+ ...(useRootOverrides && root.overrides
295
+ ? { overrides: root.overrides.getNodeRule({ name: pkg.name, version: pkg.version }) }
296
+ : {}),
292
297
  })
293
298
  })
294
299
  .then(node => {
@@ -5,6 +5,7 @@ const pacote = require('pacote')
5
5
  const AuditReport = require('../audit-report.js')
6
6
  const { subset, intersects } = require('semver')
7
7
  const npa = require('npm-package-arg')
8
+ const semver = require('semver')
8
9
  const debug = require('../debug.js')
9
10
  const walkUp = require('walk-up-path')
10
11
 
@@ -58,6 +59,8 @@ const _bundleUnpacked = Symbol('bundleUnpacked')
58
59
  const _bundleMissing = Symbol('bundleMissing')
59
60
  const _reifyNode = Symbol.for('reifyNode')
60
61
  const _extractOrLink = Symbol('extractOrLink')
62
+ const _updateAll = Symbol.for('updateAll')
63
+ const _updateNames = Symbol.for('updateNames')
61
64
  // defined by rebuild mixin
62
65
  const _checkBins = Symbol.for('checkBins')
63
66
  const _symlink = Symbol('symlink')
@@ -1140,21 +1143,33 @@ module.exports = cls => class Reifier extends cls {
1140
1143
  // for install failures. Those still end up in the shrinkwrap, so we
1141
1144
  // save it first, then prune out the optional trash, and then return it.
1142
1145
 
1143
- // support save=false option
1144
- if (options.save === false || this[_global] || this[_dryRun]) {
1146
+ const save = !(options.save === false)
1147
+
1148
+ // we check for updates in order to make sure we run save ideal tree
1149
+ // even though save=false since we want `npm update` to be able to
1150
+ // write to package-lock files by default
1151
+ const hasUpdates = this[_updateAll] || this[_updateNames].length
1152
+
1153
+ // we're going to completely skip save ideal tree in case of a global or
1154
+ // dry-run install and also if the save option is set to false, EXCEPT for
1155
+ // update since the expected behavior for npm7+ is for update to
1156
+ // NOT save to package.json, we make that exception since we still want
1157
+ // saveIdealTree to be able to write the lockfile by default.
1158
+ const saveIdealTree = !(
1159
+ (!save && !hasUpdates)
1160
+ || this[_global]
1161
+ || this[_dryRun]
1162
+ )
1163
+
1164
+ if (!saveIdealTree) {
1145
1165
  return false
1146
1166
  }
1147
1167
 
1148
1168
  process.emit('time', 'reify:save')
1149
1169
 
1150
1170
  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]) {
1171
+ const updateNodes = nodes => {
1172
+ for (const { name, tree: addTree } of nodes) {
1158
1173
  // addTree either the root, or a workspace
1159
1174
  const edge = addTree.edgesOut.get(name)
1160
1175
  const pkg = addTree.package
@@ -1168,7 +1183,7 @@ module.exports = cls => class Reifier extends cls {
1168
1183
  // that we couldn't resolve, this MAY be missing. if we haven't
1169
1184
  // blown up by now, it's because it was not a problem, though, so
1170
1185
  // just move on.
1171
- if (!child) {
1186
+ if (!child || !addTree.isTop) {
1172
1187
  continue
1173
1188
  }
1174
1189
 
@@ -1259,6 +1274,80 @@ module.exports = cls => class Reifier extends cls {
1259
1274
  }
1260
1275
  }
1261
1276
 
1277
+ // Returns true if any of the edges from this node has a semver
1278
+ // range definition that is an exact match to the version installed
1279
+ // e.g: should return true if for a given an installed version 1.0.0,
1280
+ // range is either =1.0.0 or 1.0.0
1281
+ const exactVersion = node => {
1282
+ for (const edge of node.edgesIn) {
1283
+ try {
1284
+ if (semver.subset(edge.spec, node.version)) {
1285
+ return false
1286
+ }
1287
+ } catch {}
1288
+ }
1289
+ return true
1290
+ }
1291
+
1292
+ // helper that retrieves an array of nodes that were
1293
+ // potentially updated during the reify process, in order
1294
+ // to limit the number of nodes to check and update, only
1295
+ // select nodes from the inventory that are direct deps
1296
+ // of a given package.json (project root or a workspace)
1297
+ // and in ase of using a list of `names`, restrict nodes
1298
+ // to only names that are found in this list
1299
+ const retrieveUpdatedNodes = names => {
1300
+ const filterDirectDependencies = node =>
1301
+ !node.isRoot && node.resolveParent.isRoot
1302
+ && (!names || names.includes(node.name))
1303
+ && exactVersion(node) // skip update for exact ranges
1304
+
1305
+ const directDeps = this.idealTree.inventory
1306
+ .filter(filterDirectDependencies)
1307
+
1308
+ // traverses the list of direct dependencies and collect all nodes
1309
+ // to be updated, since any of them might have changed during reify
1310
+ const nodes = []
1311
+ for (const node of directDeps) {
1312
+ for (const edgeIn of node.edgesIn) {
1313
+ nodes.push({
1314
+ name: node.name,
1315
+ tree: edgeIn.from.target,
1316
+ })
1317
+ }
1318
+ }
1319
+ return nodes
1320
+ }
1321
+
1322
+ if (save) {
1323
+ // when using update all alongside with save, we'll make
1324
+ // sure to refresh every dependency of the root idealTree
1325
+ if (this[_updateAll]) {
1326
+ const nodes = retrieveUpdatedNodes()
1327
+ updateNodes(nodes)
1328
+ } else {
1329
+ // resolvedAdd is the list of user add requests, but with names added
1330
+ // to things like git repos and tarball file/urls. However, if the
1331
+ // user requested 'foo@', and we have a foo@file:../foo, then we should
1332
+ // end up saving the spec we actually used, not whatever they gave us.
1333
+ if (this[_resolvedAdd].length) {
1334
+ updateNodes(this[_resolvedAdd])
1335
+ }
1336
+
1337
+ // if updating given dependencies by name, restrict the list of
1338
+ // nodes to check to only those currently in _updateNames
1339
+ if (this[_updateNames].length) {
1340
+ const nodes = retrieveUpdatedNodes(this[_updateNames])
1341
+ updateNodes(nodes)
1342
+ }
1343
+
1344
+ // grab any from explicitRequests that had deps removed
1345
+ for (const { from: tree } of this.explicitRequests) {
1346
+ updatedTrees.add(tree)
1347
+ }
1348
+ }
1349
+ }
1350
+
1262
1351
  // preserve indentation, if possible
1263
1352
  const {
1264
1353
  [Symbol.for('indent')]: indent,
@@ -1291,15 +1380,12 @@ module.exports = cls => class Reifier extends cls {
1291
1380
  await pkgJson.save()
1292
1381
  }
1293
1382
 
1294
- // grab any from explicitRequests that had deps removed
1295
- for (const { from: tree } of this.explicitRequests) {
1296
- updatedTrees.add(tree)
1297
- }
1298
-
1299
- for (const tree of updatedTrees) {
1300
- // refresh the edges so they have the correct specs
1301
- tree.package = tree.package
1302
- promises.push(updatePackageJson(tree))
1383
+ if (save) {
1384
+ for (const tree of updatedTrees) {
1385
+ // refresh the edges so they have the correct specs
1386
+ tree.package = tree.package
1387
+ promises.push(updatePackageJson(tree))
1388
+ }
1303
1389
  }
1304
1390
 
1305
1391
  await Promise.all(promises)
package/lib/shrinkwrap.js CHANGED
@@ -424,6 +424,18 @@ class Shrinkwrap {
424
424
  .map(fn => fn && maybeStatFile(fn)))
425
425
  }
426
426
 
427
+ inferFormattingOptions (packageJSONData) {
428
+ // don't use detect-indent, just pick the first line.
429
+ // if the file starts with {" then we have an indent of '', ie, none
430
+ // which will default to 2 at save time.
431
+ const {
432
+ [Symbol.for('indent')]: indent,
433
+ [Symbol.for('newline')]: newline,
434
+ } = packageJSONData
435
+ this.indent = indent !== undefined ? indent : this.indent
436
+ this.newline = newline !== undefined ? newline : this.newline
437
+ }
438
+
427
439
  load () {
428
440
  // we don't need to load package-lock.json except for top of tree nodes,
429
441
  // only npm-shrinkwrap.json.
@@ -451,15 +463,7 @@ class Shrinkwrap {
451
463
 
452
464
  return data ? parseJSON(data) : {}
453
465
  }).then(async data => {
454
- // don't use detect-indent, just pick the first line.
455
- // if the file starts with {" then we have an indent of '', ie, none
456
- // which will default to 2 at save time.
457
- const {
458
- [Symbol.for('indent')]: indent,
459
- [Symbol.for('newline')]: newline,
460
- } = data
461
- this.indent = indent !== undefined ? indent : this.indent
462
- this.newline = newline !== undefined ? newline : this.newline
466
+ this.inferFormattingOptions(data)
463
467
 
464
468
  if (!this.hiddenLockfile || !data.packages) {
465
469
  return data
@@ -472,8 +476,13 @@ class Shrinkwrap {
472
476
  // all good! hidden lockfile is the newest thing in here.
473
477
  return data
474
478
  }).catch(er => {
475
- const rel = relpath(this.path, this.filename)
476
- this.log.verbose('shrinkwrap', `failed to load ${rel}`, er)
479
+ /* istanbul ignore else */
480
+ if (typeof this.filename === 'string') {
481
+ const rel = relpath(this.path, this.filename)
482
+ this.log.verbose('shrinkwrap', `failed to load ${rel}`, er)
483
+ } else {
484
+ this.log.verbose('shrinkwrap', `failed to load ${this.path}`, er)
485
+ }
477
486
  this.loadingError = er
478
487
  this.loadedFromDisk = false
479
488
  this.ancientLockfile = false
@@ -1085,18 +1094,29 @@ class Shrinkwrap {
1085
1094
  return lock
1086
1095
  }
1087
1096
 
1088
- save (options = {}) {
1097
+ toJSON () {
1089
1098
  if (!this.data) {
1090
- throw new Error('run load() before saving data')
1099
+ throw new Error('run load() before getting or setting data')
1091
1100
  }
1092
1101
 
1102
+ return this.commit()
1103
+ }
1104
+
1105
+ toString (options = {}) {
1106
+ const data = this.toJSON()
1093
1107
  const { format = true } = options
1094
1108
  const defaultIndent = this.indent || 2
1095
1109
  const indent = format === true ? defaultIndent
1096
1110
  : format || 0
1097
1111
  const eol = format ? this.newline || '\n' : ''
1098
- const data = this.commit()
1099
- const json = stringify(data, swKeyOrder, indent).replace(/\n/g, eol)
1112
+ return stringify(data, swKeyOrder, indent).replace(/\n/g, eol)
1113
+ }
1114
+
1115
+ save (options = {}) {
1116
+ if (!this.data) {
1117
+ throw new Error('run load() before saving data')
1118
+ }
1119
+ const json = this.toString(options)
1100
1120
  return Promise.all([
1101
1121
  writeFile(this.filename, json).catch(er => {
1102
1122
  if (this.hiddenLockfile) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@npmcli/arborist",
3
- "version": "4.1.2",
3
+ "version": "4.3.1",
4
4
  "description": "Manage node_modules trees",
5
5
  "dependencies": {
6
6
  "@isaacs/string-locale-compare": "^1.1.0",
@@ -12,7 +12,7 @@
12
12
  "@npmcli/node-gyp": "^1.0.3",
13
13
  "@npmcli/package-json": "^1.0.1",
14
14
  "@npmcli/run-script": "^2.0.0",
15
- "bin-links": "^2.3.0",
15
+ "bin-links": "^3.0.0",
16
16
  "cacache": "^15.0.3",
17
17
  "common-ancestor-path": "^1.0.1",
18
18
  "json-parse-even-better-errors": "^2.3.1",
@@ -22,7 +22,7 @@
22
22
  "npm-install-checks": "^4.0.0",
23
23
  "npm-package-arg": "^8.1.5",
24
24
  "npm-pick-manifest": "^6.1.0",
25
- "npm-registry-fetch": "^11.0.0",
25
+ "npm-registry-fetch": "^12.0.1",
26
26
  "pacote": "^12.0.2",
27
27
  "parse-conflict-json": "^2.0.1",
28
28
  "proc-log": "^1.0.0",
@@ -37,7 +37,7 @@
37
37
  "walk-up-path": "^1.0.0"
38
38
  },
39
39
  "devDependencies": {
40
- "@npmcli/template-oss": "^2.3.1",
40
+ "@npmcli/template-oss": "^2.4.2",
41
41
  "benchmark": "^2.1.4",
42
42
  "chalk": "^4.1.0",
43
43
  "minify-registry-metadata": "^2.1.0",
@@ -94,9 +94,11 @@
94
94
  "engines": {
95
95
  "node": "^12.13.0 || ^14.15.0 || >=16"
96
96
  },
97
- "templateVersion": "2.3.1",
98
97
  "eslintIgnore": [
99
98
  "test/fixtures/",
100
99
  "!test/fixtures/*.js"
101
- ]
100
+ ],
101
+ "templateOSS": {
102
+ "version": "2.4.3"
103
+ }
102
104
  }