1// mixin implementing the reify method 2const onExit = require('../signal-handling.js') 3const pacote = require('pacote') 4const AuditReport = require('../audit-report.js') 5const { subset, intersects } = require('semver') 6const npa = require('npm-package-arg') 7const semver = require('semver') 8const debug = require('../debug.js') 9const { walkUp } = require('walk-up-path') 10const log = require('proc-log') 11const hgi = require('hosted-git-info') 12const rpj = require('read-package-json-fast') 13 14const { dirname, resolve, relative, join } = require('path') 15const { depth: dfwalk } = require('treeverse') 16const { 17 lstat, 18 mkdir, 19 rm, 20 symlink, 21} = require('fs/promises') 22const { moveFile } = require('@npmcli/fs') 23const PackageJson = require('@npmcli/package-json') 24const packageContents = require('@npmcli/installed-package-contents') 25const runScript = require('@npmcli/run-script') 26const { checkEngine, checkPlatform } = require('npm-install-checks') 27 28const treeCheck = require('../tree-check.js') 29const relpath = require('../relpath.js') 30const Diff = require('../diff.js') 31const retirePath = require('../retire-path.js') 32const promiseAllRejectLate = require('promise-all-reject-late') 33const { callLimit: promiseCallLimit } = require('promise-call-limit') 34const optionalSet = require('../optional-set.js') 35const calcDepFlags = require('../calc-dep-flags.js') 36const { saveTypeMap, hasSubKey } = require('../add-rm-pkg-deps.js') 37 38const Shrinkwrap = require('../shrinkwrap.js') 39const { defaultLockfileVersion } = Shrinkwrap 40 41const _retiredPaths = Symbol('retiredPaths') 42const _retiredUnchanged = Symbol('retiredUnchanged') 43const _sparseTreeDirs = Symbol('sparseTreeDirs') 44const _sparseTreeRoots = Symbol('sparseTreeRoots') 45const _savePrefix = Symbol('savePrefix') 46const _retireShallowNodes = Symbol.for('retireShallowNodes') 47const _getBundlesByDepth = Symbol('getBundlesByDepth') 48const _registryResolved = Symbol('registryResolved') 49const _addNodeToTrashList = Symbol.for('addNodeToTrashList') 50 51// shared by rebuild mixin 52const _trashList = Symbol.for('trashList') 53const _handleOptionalFailure = Symbol.for('handleOptionalFailure') 54const _loadTrees = Symbol.for('loadTrees') 55 56// shared symbols for swapping out when testing 57const _diffTrees = Symbol.for('diffTrees') 58const _createSparseTree = Symbol.for('createSparseTree') 59const _loadShrinkwrapsAndUpdateTrees = Symbol.for('loadShrinkwrapsAndUpdateTrees') 60const _shrinkwrapInflated = Symbol('shrinkwrapInflated') 61const _bundleUnpacked = Symbol('bundleUnpacked') 62const _bundleMissing = Symbol('bundleMissing') 63const _reifyNode = Symbol.for('reifyNode') 64const _extractOrLink = Symbol('extractOrLink') 65const _updateAll = Symbol.for('updateAll') 66const _updateNames = Symbol.for('updateNames') 67// defined by rebuild mixin 68const _checkBins = Symbol.for('checkBins') 69const _symlink = Symbol('symlink') 70const _warnDeprecated = Symbol('warnDeprecated') 71const _loadBundlesAndUpdateTrees = Symbol.for('loadBundlesAndUpdateTrees') 72const _submitQuickAudit = Symbol('submitQuickAudit') 73const _unpackNewModules = Symbol.for('unpackNewModules') 74const _moveContents = Symbol.for('moveContents') 75const _moveBackRetiredUnchanged = Symbol.for('moveBackRetiredUnchanged') 76const _build = Symbol.for('build') 77const _removeTrash = Symbol.for('removeTrash') 78const _renamePath = Symbol.for('renamePath') 79const _rollbackRetireShallowNodes = Symbol.for('rollbackRetireShallowNodes') 80const _rollbackCreateSparseTree = Symbol.for('rollbackCreateSparseTree') 81const _rollbackMoveBackRetiredUnchanged = Symbol.for('rollbackMoveBackRetiredUnchanged') 82const _saveIdealTree = Symbol.for('saveIdealTree') 83const _copyIdealToActual = Symbol('copyIdealToActual') 84const _addOmitsToTrashList = Symbol('addOmitsToTrashList') 85const _packageLockOnly = Symbol('packageLockOnly') 86const _dryRun = Symbol('dryRun') 87const _validateNodeModules = Symbol('validateNodeModules') 88const _nmValidated = Symbol('nmValidated') 89const _validatePath = Symbol('validatePath') 90const _reifyPackages = Symbol.for('reifyPackages') 91 92const _omitDev = Symbol('omitDev') 93const _omitOptional = Symbol('omitOptional') 94const _omitPeer = Symbol('omitPeer') 95 96const _pruneBundledMetadeps = Symbol('pruneBundledMetadeps') 97 98// defined by Ideal mixin 99const _resolvedAdd = Symbol.for('resolvedAdd') 100const _usePackageLock = Symbol.for('usePackageLock') 101const _formatPackageLock = Symbol.for('formatPackageLock') 102 103const _createIsolatedTree = Symbol.for('createIsolatedTree') 104 105module.exports = cls => class Reifier extends cls { 106 constructor (options) { 107 super(options) 108 109 const { 110 savePrefix = '^', 111 packageLockOnly = false, 112 dryRun = false, 113 formatPackageLock = true, 114 } = options 115 116 this[_dryRun] = !!dryRun 117 this[_packageLockOnly] = !!packageLockOnly 118 this[_savePrefix] = savePrefix 119 this[_formatPackageLock] = !!formatPackageLock 120 121 this.diff = null 122 this[_retiredPaths] = {} 123 this[_shrinkwrapInflated] = new Set() 124 this[_retiredUnchanged] = {} 125 this[_sparseTreeDirs] = new Set() 126 this[_sparseTreeRoots] = new Set() 127 this[_trashList] = new Set() 128 // the nodes we unpack to read their bundles 129 this[_bundleUnpacked] = new Set() 130 // child nodes we'd EXPECT to be included in a bundle, but aren't 131 this[_bundleMissing] = new Set() 132 this[_nmValidated] = new Set() 133 } 134 135 // public method 136 async reify (options = {}) { 137 const linked = (options.installStrategy || this.options.installStrategy) === 'linked' 138 139 if (this[_packageLockOnly] && this.options.global) { 140 const er = new Error('cannot generate lockfile for global packages') 141 er.code = 'ESHRINKWRAPGLOBAL' 142 throw er 143 } 144 145 const omit = new Set(options.omit || []) 146 this[_omitDev] = omit.has('dev') 147 this[_omitOptional] = omit.has('optional') 148 this[_omitPeer] = omit.has('peer') 149 150 // start tracker block 151 this.addTracker('reify') 152 process.emit('time', 'reify') 153 await this[_validatePath]() 154 await this[_loadTrees](options) 155 156 const oldTree = this.idealTree 157 if (linked) { 158 // swap out the tree with the isolated tree 159 // this is currently technical debt which will be resolved in a refactor 160 // of Node/Link trees 161 log.warn('reify', 'The "linked" install strategy is EXPERIMENTAL and may contain bugs.') 162 this.idealTree = await this[_createIsolatedTree](this.idealTree) 163 } 164 await this[_diffTrees]() 165 await this[_reifyPackages]() 166 if (linked) { 167 // swap back in the idealTree 168 // so that the lockfile is preserved 169 this.idealTree = oldTree 170 } 171 await this[_saveIdealTree](options) 172 await this[_copyIdealToActual]() 173 // This is a very bad pattern and I can't wait to stop doing it 174 this.auditReport = await this.auditReport 175 176 this.finishTracker('reify') 177 process.emit('timeEnd', 'reify') 178 return treeCheck(this.actualTree) 179 } 180 181 async [_validatePath] () { 182 // don't create missing dirs on dry runs 183 if (this[_packageLockOnly] || this[_dryRun]) { 184 return 185 } 186 187 // we do NOT want to set ownership on this folder, especially 188 // recursively, because it can have other side effects to do that 189 // in a project directory. We just want to make it if it's missing. 190 await mkdir(resolve(this.path), { recursive: true }) 191 192 // do not allow the top-level node_modules to be a symlink 193 await this[_validateNodeModules](resolve(this.path, 'node_modules')) 194 } 195 196 async [_reifyPackages] () { 197 // we don't submit the audit report or write to disk on dry runs 198 if (this[_dryRun]) { 199 return 200 } 201 202 if (this[_packageLockOnly]) { 203 // we already have the complete tree, so just audit it now, 204 // and that's all we have to do here. 205 return this[_submitQuickAudit]() 206 } 207 208 // ok, we're about to start touching the fs. need to roll back 209 // if we get an early termination. 210 let reifyTerminated = null 211 const removeHandler = onExit(({ signal }) => { 212 // only call once. if signal hits twice, we just terminate 213 removeHandler() 214 reifyTerminated = Object.assign(new Error('process terminated'), { 215 signal, 216 }) 217 return false 218 }) 219 220 // [rollbackfn, [...actions]] 221 // after each step, if the process was terminated, execute the rollback 222 // note that each rollback *also* calls the previous one when it's 223 // finished, and then the first one throws the error, so we only need 224 // a new rollback step when we have a new thing that must be done to 225 // revert the install. 226 const steps = [ 227 [_rollbackRetireShallowNodes, [ 228 _retireShallowNodes, 229 ]], 230 [_rollbackCreateSparseTree, [ 231 _createSparseTree, 232 _addOmitsToTrashList, 233 _loadShrinkwrapsAndUpdateTrees, 234 _loadBundlesAndUpdateTrees, 235 _submitQuickAudit, 236 _unpackNewModules, 237 ]], 238 [_rollbackMoveBackRetiredUnchanged, [ 239 _moveBackRetiredUnchanged, 240 _build, 241 ]], 242 ] 243 for (const [rollback, actions] of steps) { 244 for (const action of actions) { 245 try { 246 await this[action]() 247 if (reifyTerminated) { 248 throw reifyTerminated 249 } 250 } catch (er) { 251 await this[rollback](er) 252 /* istanbul ignore next - rollback throws, should never hit this */ 253 throw er 254 } 255 } 256 } 257 258 // no rollback for this one, just exit with the error, since the 259 // install completed and can't be safely recovered at this point. 260 await this[_removeTrash]() 261 if (reifyTerminated) { 262 throw reifyTerminated 263 } 264 265 // done modifying the file system, no need to keep listening for sigs 266 removeHandler() 267 } 268 269 // when doing a local install, we load everything and figure it all out. 270 // when doing a global install, we *only* care about the explicit requests. 271 [_loadTrees] (options) { 272 process.emit('time', 'reify:loadTrees') 273 const bitOpt = { 274 ...options, 275 complete: this[_packageLockOnly] || this[_dryRun], 276 } 277 278 // if we're only writing a package lock, then it doesn't matter what's here 279 if (this[_packageLockOnly]) { 280 return this.buildIdealTree(bitOpt) 281 .then(() => process.emit('timeEnd', 'reify:loadTrees')) 282 } 283 284 const actualOpt = this.options.global ? { 285 ignoreMissing: true, 286 global: true, 287 filter: (node, kid) => { 288 // if it's not the project root, and we have no explicit requests, 289 // then we're already into a nested dep, so we keep it 290 if (this.explicitRequests.size === 0 || !node.isProjectRoot) { 291 return true 292 } 293 294 // if we added it as an edgeOut, then we want it 295 if (this.idealTree.edgesOut.has(kid)) { 296 return true 297 } 298 299 // if it's an explicit request, then we want it 300 const hasExplicit = [...this.explicitRequests] 301 .some(edge => edge.name === kid) 302 if (hasExplicit) { 303 return true 304 } 305 306 // ignore the rest of the global install folder 307 return false 308 }, 309 } : { ignoreMissing: true } 310 311 if (!this.options.global) { 312 return Promise.all([ 313 this.loadActual(actualOpt), 314 this.buildIdealTree(bitOpt), 315 ]).then(() => process.emit('timeEnd', 'reify:loadTrees')) 316 } 317 318 // the global install space tends to have a lot of stuff in it. don't 319 // load all of it, just what we care about. we won't be saving a 320 // hidden lockfile in there anyway. Note that we have to load ideal 321 // BEFORE loading actual, so that the actualOpt can use the 322 // explicitRequests which is set during buildIdealTree 323 return this.buildIdealTree(bitOpt) 324 .then(() => this.loadActual(actualOpt)) 325 .then(() => process.emit('timeEnd', 'reify:loadTrees')) 326 } 327 328 [_diffTrees] () { 329 if (this[_packageLockOnly]) { 330 return 331 } 332 333 process.emit('time', 'reify:diffTrees') 334 // XXX if we have an existing diff already, there should be a way 335 // to just invalidate the parts that changed, but avoid walking the 336 // whole tree again. 337 338 const includeWorkspaces = this.options.workspacesEnabled 339 const includeRootDeps = !includeWorkspaces 340 || this.options.includeWorkspaceRoot && this.options.workspaces.length > 0 341 342 const filterNodes = [] 343 if (this.options.global && this.explicitRequests.size) { 344 const idealTree = this.idealTree.target 345 const actualTree = this.actualTree.target 346 // we ONLY are allowed to make changes in the global top-level 347 // children where there's an explicit request. 348 for (const { name } of this.explicitRequests) { 349 const ideal = idealTree.children.get(name) 350 if (ideal) { 351 filterNodes.push(ideal) 352 } 353 const actual = actualTree.children.get(name) 354 if (actual) { 355 filterNodes.push(actual) 356 } 357 } 358 } else { 359 if (includeWorkspaces) { 360 // add all ws nodes to filterNodes 361 for (const ws of this.options.workspaces) { 362 const ideal = this.idealTree.children.get(ws) 363 if (ideal) { 364 filterNodes.push(ideal) 365 } 366 const actual = this.actualTree.children.get(ws) 367 if (actual) { 368 filterNodes.push(actual) 369 } 370 } 371 } 372 if (includeRootDeps) { 373 // add all non-workspace nodes to filterNodes 374 for (const tree of [this.idealTree, this.actualTree]) { 375 for (const { type, to } of tree.edgesOut.values()) { 376 if (type !== 'workspace' && to) { 377 filterNodes.push(to) 378 } 379 } 380 } 381 } 382 } 383 384 // find all the nodes that need to change between the actual 385 // and ideal trees. 386 this.diff = Diff.calculate({ 387 shrinkwrapInflated: this[_shrinkwrapInflated], 388 filterNodes, 389 actual: this.actualTree, 390 ideal: this.idealTree, 391 }) 392 393 // we don't have to add 'removed' folders to the trashlist, because 394 // they'll be moved aside to a retirement folder, and then the retired 395 // folder will be deleted at the end. This is important when we have 396 // a folder like FOO being "removed" in favor of a folder like "foo", 397 // because if we remove node_modules/FOO on case-insensitive systems, 398 // it will remove the dep that we *want* at node_modules/foo. 399 400 process.emit('timeEnd', 'reify:diffTrees') 401 } 402 403 // add the node and all its bins to the list of things to be 404 // removed later on in the process. optionally, also mark them 405 // as a retired paths, so that we move them out of the way and 406 // replace them when rolling back on failure. 407 [_addNodeToTrashList] (node, retire = false) { 408 const paths = [node.path, ...node.binPaths] 409 const moves = this[_retiredPaths] 410 log.silly('reify', 'mark', retire ? 'retired' : 'deleted', paths) 411 for (const path of paths) { 412 if (retire) { 413 const retired = retirePath(path) 414 moves[path] = retired 415 this[_trashList].add(retired) 416 } else { 417 this[_trashList].add(path) 418 } 419 } 420 } 421 422 // move aside the shallowest nodes in the tree that have to be 423 // changed or removed, so that we can rollback if necessary. 424 [_retireShallowNodes] () { 425 process.emit('time', 'reify:retireShallow') 426 const moves = this[_retiredPaths] = {} 427 for (const diff of this.diff.children) { 428 if (diff.action === 'CHANGE' || diff.action === 'REMOVE') { 429 // we'll have to clean these up at the end, so add them to the list 430 this[_addNodeToTrashList](diff.actual, true) 431 } 432 } 433 log.silly('reify', 'moves', moves) 434 const movePromises = Object.entries(moves) 435 .map(([from, to]) => this[_renamePath](from, to)) 436 return promiseAllRejectLate(movePromises) 437 .then(() => process.emit('timeEnd', 'reify:retireShallow')) 438 } 439 440 [_renamePath] (from, to, didMkdirp = false) { 441 return moveFile(from, to) 442 .catch(er => { 443 // Occasionally an expected bin file might not exist in the package, 444 // or a shim/symlink might have been moved aside. If we've already 445 // handled the most common cause of ENOENT (dir doesn't exist yet), 446 // then just ignore any ENOENT. 447 if (er.code === 'ENOENT') { 448 return didMkdirp ? null : mkdir(dirname(to), { recursive: true }).then(() => 449 this[_renamePath](from, to, true)) 450 } else if (er.code === 'EEXIST') { 451 return rm(to, { recursive: true, force: true }).then(() => moveFile(from, to)) 452 } else { 453 throw er 454 } 455 }) 456 } 457 458 [_rollbackRetireShallowNodes] (er) { 459 process.emit('time', 'reify:rollback:retireShallow') 460 const moves = this[_retiredPaths] 461 const movePromises = Object.entries(moves) 462 .map(([from, to]) => this[_renamePath](to, from)) 463 return promiseAllRejectLate(movePromises) 464 // ignore subsequent rollback errors 465 .catch(er => {}) 466 .then(() => process.emit('timeEnd', 'reify:rollback:retireShallow')) 467 .then(() => { 468 throw er 469 }) 470 } 471 472 // adding to the trash list will skip reifying, and delete them 473 // if they are currently in the tree and otherwise untouched. 474 [_addOmitsToTrashList] () { 475 if (!this[_omitDev] && !this[_omitOptional] && !this[_omitPeer]) { 476 return 477 } 478 479 process.emit('time', 'reify:trashOmits') 480 481 for (const node of this.idealTree.inventory.values()) { 482 const { top } = node 483 484 // if the top is not the root or workspace then we do not want to omit it 485 if (!top.isProjectRoot && !top.isWorkspace) { 486 continue 487 } 488 489 // if a diff filter has been created, then we do not omit the node if the 490 // top node is not in that set 491 if (this.diff?.filterSet?.size && !this.diff.filterSet.has(top)) { 492 continue 493 } 494 495 // omit node if the dep type matches any omit flags that were set 496 if ( 497 node.peer && this[_omitPeer] || 498 node.dev && this[_omitDev] || 499 node.optional && this[_omitOptional] || 500 node.devOptional && this[_omitOptional] && this[_omitDev] 501 ) { 502 this[_addNodeToTrashList](node) 503 } 504 } 505 506 process.emit('timeEnd', 'reify:trashOmits') 507 } 508 509 [_createSparseTree] () { 510 process.emit('time', 'reify:createSparse') 511 // if we call this fn again, we look for the previous list 512 // so that we can avoid making the same directory multiple times 513 const leaves = this.diff.leaves 514 .filter(diff => { 515 return (diff.action === 'ADD' || diff.action === 'CHANGE') && 516 !this[_sparseTreeDirs].has(diff.ideal.path) && 517 !diff.ideal.isLink 518 }) 519 .map(diff => diff.ideal) 520 521 // we check this in parallel, so guard against multiple attempts to 522 // retire the same path at the same time. 523 const dirsChecked = new Set() 524 return promiseAllRejectLate(leaves.map(async node => { 525 for (const d of walkUp(node.path)) { 526 if (d === node.top.path) { 527 break 528 } 529 if (dirsChecked.has(d)) { 530 continue 531 } 532 dirsChecked.add(d) 533 const st = await lstat(d).catch(er => null) 534 // this can happen if we have a link to a package with a name 535 // that the filesystem treats as if it is the same thing. 536 // would be nice to have conditional istanbul ignores here... 537 /* istanbul ignore next - defense in depth */ 538 if (st && !st.isDirectory()) { 539 const retired = retirePath(d) 540 this[_retiredPaths][d] = retired 541 this[_trashList].add(retired) 542 await this[_renamePath](d, retired) 543 } 544 } 545 this[_sparseTreeDirs].add(node.path) 546 const made = await mkdir(node.path, { recursive: true }) 547 // if the directory already exists, made will be undefined. if that's the case 548 // we don't want to remove it because we aren't the ones who created it so we 549 // omit it from the _sparseTreeRoots 550 if (made) { 551 this[_sparseTreeRoots].add(made) 552 } 553 })) 554 .then(() => process.emit('timeEnd', 'reify:createSparse')) 555 } 556 557 [_rollbackCreateSparseTree] (er) { 558 process.emit('time', 'reify:rollback:createSparse') 559 // cut the roots of the sparse tree that were created, not the leaves 560 const roots = this[_sparseTreeRoots] 561 // also delete the moves that we retired, so that we can move them back 562 const failures = [] 563 const targets = [...roots, ...Object.keys(this[_retiredPaths])] 564 const unlinks = targets 565 .map(path => rm(path, { recursive: true, force: true }).catch(er => failures.push([path, er]))) 566 return promiseAllRejectLate(unlinks).then(() => { 567 // eslint-disable-next-line promise/always-return 568 if (failures.length) { 569 log.warn('cleanup', 'Failed to remove some directories', failures) 570 } 571 }) 572 .then(() => process.emit('timeEnd', 'reify:rollback:createSparse')) 573 .then(() => this[_rollbackRetireShallowNodes](er)) 574 } 575 576 // shrinkwrap nodes define their dependency branches with a file, so 577 // we need to unpack them, read that shrinkwrap file, and then update 578 // the tree by calling loadVirtual with the node as the root. 579 [_loadShrinkwrapsAndUpdateTrees] () { 580 const seen = this[_shrinkwrapInflated] 581 const shrinkwraps = this.diff.leaves 582 .filter(d => (d.action === 'CHANGE' || d.action === 'ADD' || !d.action) && 583 d.ideal.hasShrinkwrap && !seen.has(d.ideal) && 584 !this[_trashList].has(d.ideal.path)) 585 586 if (!shrinkwraps.length) { 587 return 588 } 589 590 process.emit('time', 'reify:loadShrinkwraps') 591 592 const Arborist = this.constructor 593 return promiseAllRejectLate(shrinkwraps.map(diff => { 594 const node = diff.ideal 595 seen.add(node) 596 return diff.action ? this[_reifyNode](node) : node 597 })) 598 .then(nodes => promiseAllRejectLate(nodes.map(node => new Arborist({ 599 ...this.options, 600 path: node.path, 601 }).loadVirtual({ root: node })))) 602 // reload the diff and sparse tree because the ideal tree changed 603 .then(() => this[_diffTrees]()) 604 .then(() => this[_createSparseTree]()) 605 .then(() => this[_addOmitsToTrashList]()) 606 .then(() => this[_loadShrinkwrapsAndUpdateTrees]()) 607 .then(() => process.emit('timeEnd', 'reify:loadShrinkwraps')) 608 } 609 610 // create a symlink for Links, extract for Nodes 611 // return the node object, since we usually want that 612 // handle optional dep failures here 613 // If node is in trash list, skip it 614 // If reifying fails, and the node is optional, add it and its optionalSet 615 // to the trash list 616 // Always return the node. 617 [_reifyNode] (node) { 618 if (this[_trashList].has(node.path)) { 619 return node 620 } 621 622 const timer = `reifyNode:${node.location}` 623 process.emit('time', timer) 624 this.addTracker('reify', node.name, node.location) 625 626 const { npmVersion, nodeVersion, cpu, os, libc } = this.options 627 const p = Promise.resolve().then(async () => { 628 // when we reify an optional node, check the engine and platform 629 // first. be sure to ignore the --force and --engine-strict flags, 630 // since we always want to skip any optional packages we can't install. 631 // these checks throwing will result in a rollback and removal 632 // of the mismatches 633 // eslint-disable-next-line promise/always-return 634 if (node.optional) { 635 checkEngine(node.package, npmVersion, nodeVersion, false) 636 checkPlatform(node.package, false, { cpu, os, libc }) 637 } 638 await this[_checkBins](node) 639 await this[_extractOrLink](node) 640 await this[_warnDeprecated](node) 641 }) 642 643 return this[_handleOptionalFailure](node, p) 644 .then(() => { 645 this.finishTracker('reify', node.name, node.location) 646 process.emit('timeEnd', timer) 647 return node 648 }) 649 } 650 651 // do not allow node_modules to be a symlink 652 async [_validateNodeModules] (nm) { 653 if (this.options.force || this[_nmValidated].has(nm)) { 654 return 655 } 656 const st = await lstat(nm).catch(() => null) 657 if (!st || st.isDirectory()) { 658 this[_nmValidated].add(nm) 659 return 660 } 661 log.warn('reify', 'Removing non-directory', nm) 662 await rm(nm, { recursive: true, force: true }) 663 } 664 665 async [_extractOrLink] (node) { 666 const nm = resolve(node.parent.path, 'node_modules') 667 await this[_validateNodeModules](nm) 668 669 if (!node.isLink) { 670 // in normal cases, node.resolved should *always* be set by now. 671 // however, it is possible when a lockfile is damaged, or very old, 672 // or in some other race condition bugs in npm v6, that a previously 673 // bundled dependency will have just a version, but no resolved value, 674 // and no 'bundled: true' setting. 675 // Do the best with what we have, or else remove it from the tree 676 // entirely, since we can't possibly reify it. 677 let res = null 678 if (node.resolved) { 679 const registryResolved = this[_registryResolved](node.resolved) 680 if (registryResolved) { 681 res = `${node.name}@${registryResolved}` 682 } 683 } else if (node.package.name && node.version) { 684 res = `${node.package.name}@${node.version}` 685 } 686 687 // no idea what this thing is. remove it from the tree. 688 if (!res) { 689 const warning = 'invalid or damaged lockfile detected\n' + 690 'please re-try this operation once it completes\n' + 691 'so that the damage can be corrected, or perform\n' + 692 'a fresh install with no lockfile if the problem persists.' 693 log.warn('reify', warning) 694 log.verbose('reify', 'unrecognized node in tree', node.path) 695 node.parent = null 696 node.fsParent = null 697 this[_addNodeToTrashList](node) 698 return 699 } 700 await debug(async () => { 701 const st = await lstat(node.path).catch(e => null) 702 if (st && !st.isDirectory()) { 703 debug.log('unpacking into a non-directory', node) 704 throw Object.assign(new Error('ENOTDIR: not a directory'), { 705 code: 'ENOTDIR', 706 path: node.path, 707 }) 708 } 709 }) 710 await pacote.extract(res, node.path, { 711 ...this.options, 712 resolved: node.resolved, 713 integrity: node.integrity, 714 }) 715 // store nodes don't use Node class so node.package doesn't get updated 716 if (node.isInStore) { 717 const pkg = await rpj(join(node.path, 'package.json')) 718 node.package.scripts = pkg.scripts 719 } 720 return 721 } 722 723 // node.isLink 724 await rm(node.path, { recursive: true, force: true }) 725 await this[_symlink](node) 726 } 727 728 async [_symlink] (node) { 729 const dir = dirname(node.path) 730 const target = node.realpath 731 const rel = relative(dir, target) 732 await mkdir(dir, { recursive: true }) 733 return symlink(rel, node.path, 'junction') 734 } 735 736 [_warnDeprecated] (node) { 737 const { _id, deprecated } = node.package 738 if (deprecated) { 739 log.warn('deprecated', `${_id}: ${deprecated}`) 740 } 741 } 742 743 // if the node is optional, then the failure of the promise is nonfatal 744 // just add it and its optional set to the trash list. 745 [_handleOptionalFailure] (node, p) { 746 return (node.optional ? p.catch(er => { 747 const set = optionalSet(node) 748 for (node of set) { 749 log.verbose('reify', 'failed optional dependency', node.path) 750 this[_addNodeToTrashList](node) 751 } 752 }) : p).then(() => node) 753 } 754 755 [_registryResolved] (resolved) { 756 // the default registry url is a magic value meaning "the currently 757 // configured registry". 758 // `resolved` must never be falsey. 759 // 760 // XXX: use a magic string that isn't also a valid value, like 761 // ${REGISTRY} or something. This has to be threaded through the 762 // Shrinkwrap and Node classes carefully, so for now, just treat 763 // the default reg as the magical animal that it has been. 764 const resolvedURL = hgi.parseUrl(resolved) 765 766 if (!resolvedURL) { 767 // if we could not parse the url at all then returning nothing 768 // here means it will get removed from the tree in the next step 769 return 770 } 771 772 if ((this.options.replaceRegistryHost === resolvedURL.hostname) 773 || this.options.replaceRegistryHost === 'always') { 774 // this.registry always has a trailing slash 775 return `${this.registry.slice(0, -1)}${resolvedURL.pathname}${resolvedURL.searchParams}` 776 } 777 778 return resolved 779 } 780 781 // bundles are *sort of* like shrinkwraps, in that the branch is defined 782 // by the contents of the package. however, in their case, rather than 783 // shipping a virtual tree that must be reified, they ship an entire 784 // reified actual tree that must be unpacked and not modified. 785 [_loadBundlesAndUpdateTrees] ( 786 depth = 0, bundlesByDepth = this[_getBundlesByDepth]() 787 ) { 788 if (depth === 0) { 789 process.emit('time', 'reify:loadBundles') 790 } 791 792 const maxBundleDepth = bundlesByDepth.get('maxBundleDepth') 793 if (depth > maxBundleDepth) { 794 // if we did something, then prune the tree and update the diffs 795 if (maxBundleDepth !== -1) { 796 this[_pruneBundledMetadeps](bundlesByDepth) 797 this[_diffTrees]() 798 } 799 process.emit('timeEnd', 'reify:loadBundles') 800 return 801 } 802 803 // skip any that have since been removed from the tree, eg by a 804 // shallower bundle overwriting them with a bundled meta-dep. 805 const set = (bundlesByDepth.get(depth) || []) 806 .filter(node => node.root === this.idealTree && 807 node.target !== node.root && 808 !this[_trashList].has(node.path)) 809 810 if (!set.length) { 811 return this[_loadBundlesAndUpdateTrees](depth + 1, bundlesByDepth) 812 } 813 814 // extract all the nodes with bundles 815 return promiseCallLimit(set.map(node => { 816 return () => { 817 this[_bundleUnpacked].add(node) 818 return this[_reifyNode](node) 819 } 820 }), { rejectLate: true }) 821 // then load their unpacked children and move into the ideal tree 822 .then(nodes => 823 promiseAllRejectLate(nodes.map(async node => { 824 const arb = new this.constructor({ 825 ...this.options, 826 path: node.path, 827 }) 828 const notTransplanted = new Set(node.children.keys()) 829 await arb.loadActual({ 830 root: node, 831 // don't transplant any sparse folders we created 832 // loadActual will set node.package to {} for empty directories 833 // if by chance there are some empty folders in the node_modules 834 // tree for some other reason, then ok, ignore those too. 835 transplantFilter: node => { 836 if (node.package._id) { 837 // it's actually in the bundle if it gets transplanted 838 notTransplanted.delete(node.name) 839 return true 840 } else { 841 return false 842 } 843 }, 844 }) 845 for (const name of notTransplanted) { 846 this[_bundleMissing].add(node.children.get(name)) 847 } 848 }))) 849 // move onto the next level of bundled items 850 .then(() => this[_loadBundlesAndUpdateTrees](depth + 1, bundlesByDepth)) 851 } 852 853 [_getBundlesByDepth] () { 854 const bundlesByDepth = new Map() 855 let maxBundleDepth = -1 856 dfwalk({ 857 tree: this.diff, 858 visit: diff => { 859 const node = diff.ideal 860 if (!node) { 861 return 862 } 863 if (node.isProjectRoot) { 864 return 865 } 866 867 const { bundleDependencies } = node.package 868 if (bundleDependencies && bundleDependencies.length) { 869 maxBundleDepth = Math.max(maxBundleDepth, node.depth) 870 if (!bundlesByDepth.has(node.depth)) { 871 bundlesByDepth.set(node.depth, [node]) 872 } else { 873 bundlesByDepth.get(node.depth).push(node) 874 } 875 } 876 }, 877 getChildren: diff => diff.children, 878 }) 879 880 bundlesByDepth.set('maxBundleDepth', maxBundleDepth) 881 return bundlesByDepth 882 } 883 884 // https://github.com/npm/cli/issues/1597#issuecomment-667639545 885 [_pruneBundledMetadeps] (bundlesByDepth) { 886 const bundleShadowed = new Set() 887 888 // Example dep graph: 889 // root -> (a, c) 890 // a -> BUNDLE(b) 891 // b -> c 892 // c -> b 893 // 894 // package tree: 895 // root 896 // +-- a 897 // | +-- b(1) 898 // | +-- c(1) 899 // +-- b(2) 900 // +-- c(2) 901 // 1. mark everything that's shadowed by anything in the bundle. This 902 // marks b(2) and c(2). 903 // 2. anything with edgesIn from outside the set, mark not-extraneous, 904 // remove from set. This unmarks c(2). 905 // 3. continue until no change 906 // 4. remove everything in the set from the tree. b(2) is pruned 907 908 // create the list of nodes shadowed by children of bundlers 909 for (const bundles of bundlesByDepth.values()) { 910 // skip the 'maxBundleDepth' item 911 if (!Array.isArray(bundles)) { 912 continue 913 } 914 for (const node of bundles) { 915 for (const name of node.children.keys()) { 916 const shadow = node.parent.resolve(name) 917 if (!shadow) { 918 continue 919 } 920 bundleShadowed.add(shadow) 921 shadow.extraneous = true 922 } 923 } 924 } 925 926 // lib -> (a@1.x) BUNDLE(a@1.2.3 (b@1.2.3)) 927 // a@1.2.3 -> (b@1.2.3) 928 // a@1.3.0 -> (b@2) 929 // b@1.2.3 -> () 930 // b@2 -> (c@2) 931 // 932 // root 933 // +-- lib 934 // | +-- a@1.2.3 935 // | +-- b@1.2.3 936 // +-- b@2 <-- shadowed, now extraneous 937 // +-- c@2 <-- also shadowed, because only dependent is shadowed 938 for (const shadow of bundleShadowed) { 939 for (const shadDep of shadow.edgesOut.values()) { 940 /* istanbul ignore else - pretty unusual situation, just being 941 * defensive here. Would mean that a bundled dep has a dependency 942 * that is unmet. which, weird, but if you bundle it, we take 943 * whatever you put there and assume the publisher knows best. */ 944 if (shadDep.to) { 945 bundleShadowed.add(shadDep.to) 946 shadDep.to.extraneous = true 947 } 948 } 949 } 950 951 let changed 952 do { 953 changed = false 954 for (const shadow of bundleShadowed) { 955 for (const edge of shadow.edgesIn) { 956 if (!bundleShadowed.has(edge.from)) { 957 shadow.extraneous = false 958 bundleShadowed.delete(shadow) 959 changed = true 960 break 961 } 962 } 963 } 964 } while (changed) 965 966 for (const shadow of bundleShadowed) { 967 this[_addNodeToTrashList](shadow) 968 shadow.root = null 969 } 970 } 971 972 async [_submitQuickAudit] () { 973 if (this.options.audit === false) { 974 this.auditReport = null 975 return 976 } 977 978 // we submit the quick audit at this point in the process, as soon as 979 // we have all the deps resolved, so that it can overlap with the other 980 // actions as much as possible. Stash the promise, which we resolve 981 // before finishing the reify() and returning the tree. Thus, we do 982 // NOT return the promise, as the intent is for this to run in parallel 983 // with the reification, and be resolved at a later time. 984 process.emit('time', 'reify:audit') 985 const options = { ...this.options } 986 const tree = this.idealTree 987 988 // if we're operating on a workspace, only audit the workspace deps 989 if (this.options.workspaces.length) { 990 options.filterSet = this.workspaceDependencySet( 991 tree, 992 this.options.workspaces, 993 this.options.includeWorkspaceRoot 994 ) 995 } 996 997 this.auditReport = AuditReport.load(tree, options).then(res => { 998 process.emit('timeEnd', 'reify:audit') 999 return res 1000 }) 1001 } 1002 1003 // ok! actually unpack stuff into their target locations! 1004 // The sparse tree has already been created, so we walk the diff 1005 // kicking off each unpack job. If any fail, we rm the sparse 1006 // tree entirely and try to put everything back where it was. 1007 [_unpackNewModules] () { 1008 process.emit('time', 'reify:unpack') 1009 const unpacks = [] 1010 dfwalk({ 1011 tree: this.diff, 1012 visit: diff => { 1013 // no unpacking if we don't want to change this thing 1014 if (diff.action !== 'CHANGE' && diff.action !== 'ADD') { 1015 return 1016 } 1017 1018 const node = diff.ideal 1019 const bd = this[_bundleUnpacked].has(node) 1020 const sw = this[_shrinkwrapInflated].has(node) 1021 const bundleMissing = this[_bundleMissing].has(node) 1022 1023 // check whether we still need to unpack this one. 1024 // test the inDepBundle last, since that's potentially a tree walk. 1025 const doUnpack = node && // can't unpack if removed! 1026 // root node already exists 1027 !node.isRoot && 1028 // already unpacked to read bundle 1029 !bd && 1030 // already unpacked to read sw 1031 !sw && 1032 // already unpacked by another dep's bundle 1033 (bundleMissing || !node.inDepBundle) 1034 1035 if (doUnpack) { 1036 unpacks.push(this[_reifyNode](node)) 1037 } 1038 }, 1039 getChildren: diff => diff.children, 1040 }) 1041 return promiseAllRejectLate(unpacks) 1042 .then(() => process.emit('timeEnd', 'reify:unpack')) 1043 } 1044 1045 // This is the part where we move back the unchanging nodes that were 1046 // the children of a node that did change. If this fails, the rollback 1047 // is a three-step process. First, we try to move the retired unchanged 1048 // nodes BACK to their retirement folders, then delete the sparse tree, 1049 // then move everything out of retirement. 1050 [_moveBackRetiredUnchanged] () { 1051 // get a list of all unchanging children of any shallow retired nodes 1052 // if they are not the ancestor of any node in the diff set, then the 1053 // directory won't already exist, so just rename it over. 1054 // This is sort of an inverse diff tree, of all the nodes where 1055 // the actualTree and idealTree _don't_ differ, starting from the 1056 // shallowest nodes that we moved aside in the first place. 1057 process.emit('time', 'reify:unretire') 1058 const moves = this[_retiredPaths] 1059 this[_retiredUnchanged] = {} 1060 return promiseAllRejectLate(this.diff.children.map(diff => { 1061 // skip if nothing was retired 1062 if (diff.action !== 'CHANGE' && diff.action !== 'REMOVE') { 1063 return 1064 } 1065 1066 const { path: realFolder } = diff.actual 1067 const retireFolder = moves[realFolder] 1068 /* istanbul ignore next - should be impossible */ 1069 debug(() => { 1070 if (!retireFolder) { 1071 const er = new Error('trying to un-retire but not retired') 1072 throw Object.assign(er, { 1073 realFolder, 1074 retireFolder, 1075 actual: diff.actual, 1076 ideal: diff.ideal, 1077 action: diff.action, 1078 }) 1079 } 1080 }) 1081 1082 this[_retiredUnchanged][retireFolder] = [] 1083 return promiseAllRejectLate(diff.unchanged.map(node => { 1084 // no need to roll back links, since we'll just delete them anyway 1085 if (node.isLink) { 1086 return mkdir(dirname(node.path), { recursive: true, force: true }) 1087 .then(() => this[_reifyNode](node)) 1088 } 1089 1090 // will have been moved/unpacked along with bundler 1091 if (node.inDepBundle && !this[_bundleMissing].has(node)) { 1092 return 1093 } 1094 1095 this[_retiredUnchanged][retireFolder].push(node) 1096 1097 const rel = relative(realFolder, node.path) 1098 const fromPath = resolve(retireFolder, rel) 1099 // if it has bundleDependencies, then make node_modules. otherwise 1100 // skip it. 1101 const bd = node.package.bundleDependencies 1102 const dir = bd && bd.length ? node.path + '/node_modules' : node.path 1103 return mkdir(dir, { recursive: true }).then(() => this[_moveContents](node, fromPath)) 1104 })) 1105 })) 1106 .then(() => process.emit('timeEnd', 'reify:unretire')) 1107 } 1108 1109 // move the contents from the fromPath to the node.path 1110 [_moveContents] (node, fromPath) { 1111 return packageContents({ 1112 path: fromPath, 1113 depth: 1, 1114 packageJsonCache: new Map([[fromPath + '/package.json', node.package]]), 1115 }).then(res => promiseAllRejectLate(res.map(path => { 1116 const rel = relative(fromPath, path) 1117 const to = resolve(node.path, rel) 1118 return this[_renamePath](path, to) 1119 }))) 1120 } 1121 1122 [_rollbackMoveBackRetiredUnchanged] (er) { 1123 const moves = this[_retiredPaths] 1124 // flip the mapping around to go back 1125 const realFolders = new Map(Object.entries(moves).map(([k, v]) => [v, k])) 1126 const promises = Object.entries(this[_retiredUnchanged]) 1127 .map(([retireFolder, nodes]) => promiseAllRejectLate(nodes.map(node => { 1128 const realFolder = realFolders.get(retireFolder) 1129 const rel = relative(realFolder, node.path) 1130 const fromPath = resolve(retireFolder, rel) 1131 return this[_moveContents]({ ...node, path: fromPath }, node.path) 1132 }))) 1133 return promiseAllRejectLate(promises) 1134 .then(() => this[_rollbackCreateSparseTree](er)) 1135 } 1136 1137 [_build] () { 1138 process.emit('time', 'reify:build') 1139 1140 // for all the things being installed, run their appropriate scripts 1141 // run in tip->root order, so as to be more likely to build a node's 1142 // deps before attempting to build it itself 1143 const nodes = [] 1144 dfwalk({ 1145 tree: this.diff, 1146 leave: diff => { 1147 if (!diff.ideal.isProjectRoot) { 1148 nodes.push(diff.ideal) 1149 } 1150 }, 1151 // process adds before changes, ignore removals 1152 getChildren: diff => diff && diff.children, 1153 filter: diff => diff.action === 'ADD' || diff.action === 'CHANGE', 1154 }) 1155 1156 // pick up link nodes from the unchanged list as we want to run their 1157 // scripts in every install despite of having a diff status change 1158 for (const node of this.diff.unchanged) { 1159 const tree = node.root.target 1160 1161 // skip links that only live within node_modules as they are most 1162 // likely managed by packages we installed, we only want to rebuild 1163 // unchanged links we directly manage 1164 const linkedFromRoot = node.parent === tree || node.target.fsTop === tree 1165 if (node.isLink && linkedFromRoot) { 1166 nodes.push(node) 1167 } 1168 } 1169 1170 return this.rebuild({ nodes, handleOptionalFailure: true }) 1171 .then(() => process.emit('timeEnd', 'reify:build')) 1172 } 1173 1174 // the tree is pretty much built now, so it's cleanup time. 1175 // remove the retired folders, and any deleted nodes 1176 // If this fails, there isn't much we can do but tell the user about it. 1177 // Thankfully, it's pretty unlikely that it'll fail, since rm is a node builtin. 1178 async [_removeTrash] () { 1179 process.emit('time', 'reify:trash') 1180 const promises = [] 1181 const failures = [] 1182 const _rm = path => rm(path, { recursive: true, force: true }).catch(er => failures.push([path, er])) 1183 1184 for (const path of this[_trashList]) { 1185 promises.push(_rm(path)) 1186 } 1187 1188 await promiseAllRejectLate(promises) 1189 if (failures.length) { 1190 log.warn('cleanup', 'Failed to remove some directories', failures) 1191 } 1192 process.emit('timeEnd', 'reify:trash') 1193 } 1194 1195 // last but not least, we save the ideal tree metadata to the package-lock 1196 // or shrinkwrap file, and any additions or removals to package.json 1197 async [_saveIdealTree] (options) { 1198 // the ideal tree is actualized now, hooray! 1199 // it still contains all the references to optional nodes that were removed 1200 // for install failures. Those still end up in the shrinkwrap, so we 1201 // save it first, then prune out the optional trash, and then return it. 1202 1203 const save = !(options.save === false) 1204 1205 // we check for updates in order to make sure we run save ideal tree 1206 // even though save=false since we want `npm update` to be able to 1207 // write to package-lock files by default 1208 const hasUpdates = this[_updateAll] || this[_updateNames].length 1209 1210 // we're going to completely skip save ideal tree in case of a global or 1211 // dry-run install and also if the save option is set to false, EXCEPT for 1212 // update since the expected behavior for npm7+ is for update to 1213 // NOT save to package.json, we make that exception since we still want 1214 // saveIdealTree to be able to write the lockfile by default. 1215 const saveIdealTree = !( 1216 (!save && !hasUpdates) 1217 || this.options.global 1218 || this[_dryRun] 1219 ) 1220 1221 if (!saveIdealTree) { 1222 return false 1223 } 1224 1225 process.emit('time', 'reify:save') 1226 1227 const updatedTrees = new Set() 1228 const updateNodes = nodes => { 1229 for (const { name, tree: addTree } of nodes) { 1230 // addTree either the root, or a workspace 1231 const edge = addTree.edgesOut.get(name) 1232 const pkg = addTree.package 1233 const req = npa.resolve(name, edge.spec, addTree.realpath) 1234 const { rawSpec, subSpec } = req 1235 1236 const spec = subSpec ? subSpec.rawSpec : rawSpec 1237 const child = edge.to 1238 1239 // if we tried to install an optional dep, but it was a version 1240 // that we couldn't resolve, this MAY be missing. if we haven't 1241 // blown up by now, it's because it was not a problem, though, so 1242 // just move on. 1243 if (!child || !addTree.isTop) { 1244 continue 1245 } 1246 1247 let newSpec 1248 // True if the dependency is getting installed from a local file path 1249 // In this case it is not possible to do the normal version comparisons 1250 // as the new version will be a file path 1251 const isLocalDep = req.type === 'directory' || req.type === 'file' 1252 if (req.registry) { 1253 const version = child.version 1254 const prefixRange = version ? this[_savePrefix] + version : '*' 1255 // if we installed a range, then we save the range specified 1256 // if it is not a subset of the ^x.y.z. eg, installing a range 1257 // of `1.x <1.2.3` will not be saved as `^1.2.0`, because that 1258 // would allow versions outside the requested range. Tags and 1259 // specific versions save with the save-prefix. 1260 const isRange = (subSpec || req).type === 'range' 1261 1262 let range = spec 1263 if ( 1264 !isRange || 1265 spec === '*' || 1266 subset(prefixRange, spec, { loose: true }) 1267 ) { 1268 range = prefixRange 1269 } 1270 1271 const pname = child.packageName 1272 const alias = name !== pname 1273 newSpec = alias ? `npm:${pname}@${range}` : range 1274 } else if (req.hosted) { 1275 // save the git+https url if it has auth, otherwise shortcut 1276 const h = req.hosted 1277 const opt = { noCommittish: false } 1278 if (h.https && h.auth) { 1279 newSpec = `git+${h.https(opt)}` 1280 } else { 1281 newSpec = h.shortcut(opt) 1282 } 1283 } else if (isLocalDep) { 1284 // when finding workspace nodes, make sure that 1285 // we save them using their version instead of 1286 // using their relative path 1287 if (edge.type === 'workspace') { 1288 const { version } = edge.to.target 1289 const prefixRange = version ? this[_savePrefix] + version : '*' 1290 newSpec = prefixRange 1291 } else { 1292 // save the relative path in package.json 1293 // Normally saveSpec is updated with the proper relative 1294 // path already, but it's possible to specify a full absolute 1295 // path initially, in which case we can end up with the wrong 1296 // thing, so just get the ultimate fetchSpec and relativize it. 1297 const p = req.fetchSpec.replace(/^file:/, '') 1298 const rel = relpath(addTree.realpath, p).replace(/#/g, '%23') 1299 newSpec = `file:${rel}` 1300 } 1301 } else { 1302 newSpec = req.saveSpec 1303 } 1304 1305 if (options.saveType) { 1306 const depType = saveTypeMap.get(options.saveType) 1307 pkg[depType][name] = newSpec 1308 // rpj will have moved it here if it was in both 1309 // if it is empty it will be deleted later 1310 if (options.saveType === 'prod' && pkg.optionalDependencies) { 1311 delete pkg.optionalDependencies[name] 1312 } 1313 } else { 1314 if (hasSubKey(pkg, 'dependencies', name)) { 1315 pkg.dependencies[name] = newSpec 1316 } 1317 1318 if (hasSubKey(pkg, 'devDependencies', name)) { 1319 pkg.devDependencies[name] = newSpec 1320 // don't update peer or optional if we don't have to 1321 if (hasSubKey(pkg, 'peerDependencies', name) && (isLocalDep || !intersects(newSpec, pkg.peerDependencies[name]))) { 1322 pkg.peerDependencies[name] = newSpec 1323 } 1324 1325 if (hasSubKey(pkg, 'optionalDependencies', name) && (isLocalDep || !intersects(newSpec, pkg.optionalDependencies[name]))) { 1326 pkg.optionalDependencies[name] = newSpec 1327 } 1328 } else { 1329 if (hasSubKey(pkg, 'peerDependencies', name)) { 1330 pkg.peerDependencies[name] = newSpec 1331 } 1332 1333 if (hasSubKey(pkg, 'optionalDependencies', name)) { 1334 pkg.optionalDependencies[name] = newSpec 1335 } 1336 } 1337 } 1338 1339 updatedTrees.add(addTree) 1340 } 1341 } 1342 1343 // Returns true if any of the edges from this node has a semver 1344 // range definition that is an exact match to the version installed 1345 // e.g: should return true if for a given an installed version 1.0.0, 1346 // range is either =1.0.0 or 1.0.0 1347 const exactVersion = node => { 1348 for (const edge of node.edgesIn) { 1349 try { 1350 if (semver.subset(edge.spec, node.version)) { 1351 return false 1352 } 1353 } catch { 1354 // ignore errors 1355 } 1356 } 1357 return true 1358 } 1359 1360 // helper that retrieves an array of nodes that were 1361 // potentially updated during the reify process, in order 1362 // to limit the number of nodes to check and update, only 1363 // select nodes from the inventory that are direct deps 1364 // of a given package.json (project root or a workspace) 1365 // and in ase of using a list of `names`, restrict nodes 1366 // to only names that are found in this list 1367 const retrieveUpdatedNodes = names => { 1368 const filterDirectDependencies = node => 1369 !node.isRoot && node.resolveParent && node.resolveParent.isRoot 1370 && (!names || names.includes(node.name)) 1371 && exactVersion(node) // skip update for exact ranges 1372 1373 const directDeps = this.idealTree.inventory 1374 .filter(filterDirectDependencies) 1375 1376 // traverses the list of direct dependencies and collect all nodes 1377 // to be updated, since any of them might have changed during reify 1378 const nodes = [] 1379 for (const node of directDeps) { 1380 for (const edgeIn of node.edgesIn) { 1381 nodes.push({ 1382 name: node.name, 1383 tree: edgeIn.from.target, 1384 }) 1385 } 1386 } 1387 return nodes 1388 } 1389 1390 if (save) { 1391 // when using update all alongside with save, we'll make 1392 // sure to refresh every dependency of the root idealTree 1393 if (this[_updateAll]) { 1394 const nodes = retrieveUpdatedNodes() 1395 updateNodes(nodes) 1396 } else { 1397 // resolvedAdd is the list of user add requests, but with names added 1398 // to things like git repos and tarball file/urls. However, if the 1399 // user requested 'foo@', and we have a foo@file:../foo, then we should 1400 // end up saving the spec we actually used, not whatever they gave us. 1401 if (this[_resolvedAdd].length) { 1402 updateNodes(this[_resolvedAdd]) 1403 } 1404 1405 // if updating given dependencies by name, restrict the list of 1406 // nodes to check to only those currently in _updateNames 1407 if (this[_updateNames].length) { 1408 const nodes = retrieveUpdatedNodes(this[_updateNames]) 1409 updateNodes(nodes) 1410 } 1411 1412 // grab any from explicitRequests that had deps removed 1413 for (const { from: tree } of this.explicitRequests) { 1414 updatedTrees.add(tree) 1415 } 1416 } 1417 } 1418 1419 if (save) { 1420 for (const tree of updatedTrees) { 1421 // refresh the edges so they have the correct specs 1422 tree.package = tree.package 1423 const pkgJson = await PackageJson.load(tree.path, { create: true }) 1424 const { 1425 dependencies = {}, 1426 devDependencies = {}, 1427 optionalDependencies = {}, 1428 peerDependencies = {}, 1429 // bundleDependencies is not required by PackageJson like the other 1430 // fields here PackageJson also doesn't omit an empty array for this 1431 // field so defaulting this to an empty array would add that field to 1432 // every package.json file. 1433 bundleDependencies, 1434 } = tree.package 1435 1436 pkgJson.update({ 1437 dependencies, 1438 devDependencies, 1439 optionalDependencies, 1440 peerDependencies, 1441 bundleDependencies, 1442 }) 1443 await pkgJson.save() 1444 } 1445 } 1446 1447 // before now edge specs could be changing, affecting the `requires` field 1448 // in the package lock, so we hold off saving to the very last action 1449 if (this[_usePackageLock]) { 1450 // preserve indentation, if possible 1451 let format = this.idealTree.package[Symbol.for('indent')] 1452 if (format === undefined) { 1453 format = ' ' 1454 } 1455 1456 // TODO this ignores options.save 1457 await this.idealTree.meta.save({ 1458 format: (this[_formatPackageLock] && format) ? format 1459 : this[_formatPackageLock], 1460 }) 1461 } 1462 1463 process.emit('timeEnd', 'reify:save') 1464 return true 1465 } 1466 1467 async [_copyIdealToActual] () { 1468 // clean up any trash that is still in the tree 1469 for (const path of this[_trashList]) { 1470 const loc = relpath(this.idealTree.realpath, path) 1471 const node = this.idealTree.inventory.get(loc) 1472 if (node && node.root === this.idealTree) { 1473 node.parent = null 1474 } 1475 } 1476 1477 // if we filtered to only certain nodes, then anything ELSE needs 1478 // to be untouched in the resulting actual tree, even if it differs 1479 // in the idealTree. Copy over anything that was in the actual and 1480 // was not changed, delete anything in the ideal and not actual. 1481 // Then we move the entire idealTree over to this.actualTree, and 1482 // save the hidden lockfile. 1483 if (this.diff && this.diff.filterSet.size) { 1484 const reroot = new Set() 1485 1486 const { filterSet } = this.diff 1487 const seen = new Set() 1488 for (const [loc, ideal] of this.idealTree.inventory.entries()) { 1489 seen.add(loc) 1490 1491 // if it's an ideal node from the filter set, then skip it 1492 // because we already made whatever changes were necessary 1493 if (filterSet.has(ideal)) { 1494 continue 1495 } 1496 1497 // otherwise, if it's not in the actualTree, then it's not a thing 1498 // that we actually added. And if it IS in the actualTree, then 1499 // it's something that we left untouched, so we need to record 1500 // that. 1501 const actual = this.actualTree.inventory.get(loc) 1502 if (!actual) { 1503 ideal.root = null 1504 } else { 1505 if ([...actual.linksIn].some(link => filterSet.has(link))) { 1506 seen.add(actual.location) 1507 continue 1508 } 1509 const { realpath, isLink } = actual 1510 if (isLink && ideal.isLink && ideal.realpath === realpath) { 1511 continue 1512 } else { 1513 reroot.add(actual) 1514 } 1515 } 1516 } 1517 1518 // now find any actual nodes that may not be present in the ideal 1519 // tree, but were left behind by virtue of not being in the filter 1520 for (const [loc, actual] of this.actualTree.inventory.entries()) { 1521 if (seen.has(loc)) { 1522 continue 1523 } 1524 seen.add(loc) 1525 1526 // we know that this is something that ISN'T in the idealTree, 1527 // or else we will have addressed it in the previous loop. 1528 // If it's in the filterSet, that means we intentionally removed 1529 // it, so nothing to do here. 1530 if (filterSet.has(actual)) { 1531 continue 1532 } 1533 1534 reroot.add(actual) 1535 } 1536 1537 // go through the rerooted actual nodes, and move them over. 1538 for (const actual of reroot) { 1539 actual.root = this.idealTree 1540 } 1541 1542 // prune out any tops that lack a linkIn, they are no longer relevant. 1543 for (const top of this.idealTree.tops) { 1544 if (top.linksIn.size === 0) { 1545 top.root = null 1546 } 1547 } 1548 1549 // need to calculate dep flags, since nodes may have been marked 1550 // as extraneous or otherwise incorrect during transit. 1551 calcDepFlags(this.idealTree) 1552 } 1553 1554 // save the ideal's meta as a hidden lockfile after we actualize it 1555 this.idealTree.meta.filename = 1556 this.idealTree.realpath + '/node_modules/.package-lock.json' 1557 this.idealTree.meta.hiddenLockfile = true 1558 this.idealTree.meta.lockfileVersion = defaultLockfileVersion 1559 1560 this.actualTree = this.idealTree 1561 this.idealTree = null 1562 1563 if (!this.options.global) { 1564 await this.actualTree.meta.save() 1565 const ignoreScripts = !!this.options.ignoreScripts 1566 // if we aren't doing a dry run or ignoring scripts and we actually made changes to the dep 1567 // tree, then run the dependencies scripts 1568 if (!this[_dryRun] && !ignoreScripts && this.diff && this.diff.children.length) { 1569 const { path, package: pkg } = this.actualTree.target 1570 const stdio = this.options.foregroundScripts ? 'inherit' : 'pipe' 1571 const { scripts = {} } = pkg 1572 for (const event of ['predependencies', 'dependencies', 'postdependencies']) { 1573 if (Object.prototype.hasOwnProperty.call(scripts, event)) { 1574 const timer = `reify:run:${event}` 1575 process.emit('time', timer) 1576 log.info('run', pkg._id, event, scripts[event]) 1577 await runScript({ 1578 event, 1579 path, 1580 pkg, 1581 stdio, 1582 scriptShell: this.options.scriptShell, 1583 }) 1584 process.emit('timeEnd', timer) 1585 } 1586 } 1587 } 1588 } 1589 } 1590 1591 async dedupe (options = {}) { 1592 // allow the user to set options on the ctor as well. 1593 // XXX: deprecate separate method options objects. 1594 options = { ...this.options, ...options } 1595 const tree = await this.loadVirtual().catch(() => this.loadActual()) 1596 const names = [] 1597 for (const name of tree.inventory.query('name')) { 1598 if (tree.inventory.query('name', name).size > 1) { 1599 names.push(name) 1600 } 1601 } 1602 return this.reify({ 1603 ...options, 1604 preferDedupe: true, 1605 update: { names }, 1606 }) 1607 } 1608} 1609