1// a module that manages a shrinkwrap file (npm-shrinkwrap.json or 2// package-lock.json). 3 4// Increment whenever the lockfile version updates 5// v1 - npm <=6 6// v2 - arborist v1, npm v7, backwards compatible with v1, add 'packages' 7// v3 will drop the 'dependencies' field, backwards comp with v2, not v1 8// 9// We cannot bump to v3 until npm v6 is out of common usage, and 10// definitely not before npm v8. 11 12const localeCompare = require('@isaacs/string-locale-compare')('en') 13const defaultLockfileVersion = 3 14 15// for comparing nodes to yarn.lock entries 16const mismatch = (a, b) => a && b && a !== b 17 18// this.tree => the root node for the tree (ie, same path as this) 19// - Set the first time we do `this.add(node)` for a path matching this.path 20// 21// this.add(node) => 22// - decorate the node with the metadata we have, if we have it, and it matches 23// - add to the map of nodes needing to be committed, so that subsequent 24// changes are captured when we commit that location's metadata. 25// 26// this.commit() => 27// - commit all nodes awaiting update to their metadata entries 28// - re-generate this.data and this.yarnLock based on this.tree 29// 30// Note that between this.add() and this.commit(), `this.data` will be out of 31// date! Always call `commit()` before relying on it. 32// 33// After calling this.commit(), any nodes not present in the tree will have 34// been removed from the shrinkwrap data as well. 35 36const log = require('proc-log') 37const YarnLock = require('./yarn-lock.js') 38const { 39 readFile, 40 readdir, 41 readlink, 42 rm, 43 stat, 44 writeFile, 45} = require('fs/promises') 46 47const { resolve, basename, relative } = require('path') 48const specFromLock = require('./spec-from-lock.js') 49const versionFromTgz = require('./version-from-tgz.js') 50const npa = require('npm-package-arg') 51const pkgJson = require('@npmcli/package-json') 52const parseJSON = require('parse-conflict-json') 53 54const stringify = require('json-stringify-nice') 55const swKeyOrder = [ 56 'name', 57 'version', 58 'lockfileVersion', 59 'resolved', 60 'integrity', 61 'requires', 62 'packages', 63 'dependencies', 64] 65 66// used to rewrite from yarn registry to npm registry 67const yarnRegRe = /^https?:\/\/registry\.yarnpkg\.com\// 68const npmRegRe = /^https?:\/\/registry\.npmjs\.org\// 69 70// sometimes resolved: is weird or broken, or something npa can't handle 71const specFromResolved = resolved => { 72 try { 73 return npa(resolved) 74 } catch (er) { 75 return {} 76 } 77} 78 79const relpath = require('./relpath.js') 80 81const consistentResolve = require('./consistent-resolve.js') 82const { overrideResolves } = require('./override-resolves.js') 83 84const pkgMetaKeys = [ 85 // note: name is included if necessary, for alias packages 86 'version', 87 'dependencies', 88 'peerDependencies', 89 'peerDependenciesMeta', 90 'optionalDependencies', 91 'bundleDependencies', 92 'acceptDependencies', 93 'funding', 94 'engines', 95 'os', 96 'cpu', 97 '_integrity', 98 'license', 99 '_hasShrinkwrap', 100 'hasInstallScript', 101 'bin', 102 'deprecated', 103 'workspaces', 104] 105 106const nodeMetaKeys = [ 107 'integrity', 108 'inBundle', 109 'hasShrinkwrap', 110 'hasInstallScript', 111] 112 113const metaFieldFromPkg = (pkg, key) => { 114 const val = pkg[key] 115 if (val) { 116 // get only the license type, not the full object 117 if (key === 'license' && typeof val === 'object' && val.type) { 118 return val.type 119 } 120 // skip empty objects and falsey values 121 if (typeof val !== 'object' || Object.keys(val).length) { 122 return val 123 } 124 } 125 return null 126} 127 128// check to make sure that there are no packages newer than or missing from the hidden lockfile 129const assertNoNewer = async (path, data, lockTime, dir, seen) => { 130 const base = basename(dir) 131 const isNM = dir !== path && base === 'node_modules' 132 const isScope = dir !== path && base.startsWith('@') 133 const isParent = (dir === path) || isNM || isScope 134 135 const parent = isParent ? dir : resolve(dir, 'node_modules') 136 const rel = relpath(path, dir) 137 seen.add(rel) 138 let entries 139 if (dir === path) { 140 entries = [{ name: 'node_modules', isDirectory: () => true }] 141 } else { 142 const { mtime: dirTime } = await stat(dir) 143 if (dirTime > lockTime) { 144 throw new Error(`out of date, updated: ${rel}`) 145 } 146 if (!isScope && !isNM && !data.packages[rel]) { 147 throw new Error(`missing from lockfile: ${rel}`) 148 } 149 entries = await readdir(parent, { withFileTypes: true }).catch(() => []) 150 } 151 152 // TODO limit concurrency here, this is recursive 153 await Promise.all(entries.map(async dirent => { 154 const child = resolve(parent, dirent.name) 155 if (dirent.isDirectory() && !dirent.name.startsWith('.')) { 156 await assertNoNewer(path, data, lockTime, child, seen) 157 } else if (dirent.isSymbolicLink()) { 158 const target = resolve(parent, await readlink(child)) 159 const tstat = await stat(target).catch( 160 /* istanbul ignore next - windows */ () => null) 161 seen.add(relpath(path, child)) 162 /* istanbul ignore next - windows cannot do this */ 163 if (tstat?.isDirectory() && !seen.has(relpath(path, target))) { 164 await assertNoNewer(path, data, lockTime, target, seen) 165 } 166 } 167 })) 168 169 if (dir !== path) { 170 return 171 } 172 173 // assert that all the entries in the lockfile were seen 174 for (const loc in data.packages) { 175 if (!seen.has(loc)) { 176 throw new Error(`missing from node_modules: ${loc}`) 177 } 178 } 179} 180 181class Shrinkwrap { 182 static get defaultLockfileVersion () { 183 return defaultLockfileVersion 184 } 185 186 static load (options) { 187 return new Shrinkwrap(options).load() 188 } 189 190 static get keyOrder () { 191 return swKeyOrder 192 } 193 194 static async reset (options) { 195 // still need to know if it was loaded from the disk, but don't 196 // bother reading it if we're gonna just throw it away. 197 const s = new Shrinkwrap(options) 198 s.reset() 199 200 const [sw, lock] = await s.resetFiles 201 202 // XXX this is duplicated in this.load(), but using loadFiles instead of resetFiles 203 if (s.hiddenLockfile) { 204 s.filename = resolve(s.path, 'node_modules/.package-lock.json') 205 } else if (s.shrinkwrapOnly || sw) { 206 s.filename = resolve(s.path, 'npm-shrinkwrap.json') 207 } else { 208 s.filename = resolve(s.path, 'package-lock.json') 209 } 210 s.loadedFromDisk = !!(sw || lock) 211 // TODO what uses this? 212 s.type = basename(s.filename) 213 214 return s 215 } 216 217 static metaFromNode (node, path, options = {}) { 218 if (node.isLink) { 219 return { 220 resolved: relpath(path, node.realpath), 221 link: true, 222 } 223 } 224 225 const meta = {} 226 for (const key of pkgMetaKeys) { 227 const val = metaFieldFromPkg(node.package, key) 228 if (val) { 229 meta[key.replace(/^_/, '')] = val 230 } 231 } 232 // we only include name if different from the node path name, and for the 233 // root to help prevent churn based on the name of the directory the 234 // project is in 235 const pname = node.packageName 236 if (pname && (node === node.root || pname !== node.name)) { 237 meta.name = pname 238 } 239 240 if (node.isTop && node.package.devDependencies) { 241 meta.devDependencies = node.package.devDependencies 242 } 243 244 for (const key of nodeMetaKeys) { 245 if (node[key]) { 246 meta[key] = node[key] 247 } 248 } 249 250 const resolved = consistentResolve(node.resolved, node.path, path, true) 251 // hide resolved from registry dependencies. 252 if (!resolved) { 253 // no-op 254 } else if (node.isRegistryDependency) { 255 meta.resolved = overrideResolves(resolved, options) 256 } else { 257 meta.resolved = resolved 258 } 259 260 if (node.extraneous) { 261 meta.extraneous = true 262 } else { 263 if (node.peer) { 264 meta.peer = true 265 } 266 if (node.dev) { 267 meta.dev = true 268 } 269 if (node.optional) { 270 meta.optional = true 271 } 272 if (node.devOptional && !node.dev && !node.optional) { 273 meta.devOptional = true 274 } 275 } 276 return meta 277 } 278 279 #awaitingUpdate = new Map() 280 281 constructor (options = {}) { 282 const { 283 path, 284 indent = 2, 285 newline = '\n', 286 shrinkwrapOnly = false, 287 hiddenLockfile = false, 288 lockfileVersion, 289 resolveOptions = {}, 290 } = options 291 292 if (hiddenLockfile) { 293 this.lockfileVersion = 3 294 } else if (lockfileVersion) { 295 this.lockfileVersion = parseInt(lockfileVersion, 10) 296 } else { 297 this.lockfileVersion = null 298 } 299 300 this.tree = null 301 this.path = resolve(path || '.') 302 this.filename = null 303 this.data = null 304 this.indent = indent 305 this.newline = newline 306 this.loadedFromDisk = false 307 this.type = null 308 this.yarnLock = null 309 this.hiddenLockfile = hiddenLockfile 310 this.loadingError = null 311 this.resolveOptions = resolveOptions 312 // only load npm-shrinkwrap.json in dep trees, not package-lock 313 this.shrinkwrapOnly = shrinkwrapOnly 314 } 315 316 // check to see if a spec is present in the yarn.lock file, and if so, 317 // if we should use it, and what it should resolve to. This is only 318 // done when we did not load a shrinkwrap from disk. Also, decorate 319 // the options object if provided with the resolved and integrity that 320 // we expect. 321 checkYarnLock (spec, options = {}) { 322 spec = npa(spec) 323 const { yarnLock, loadedFromDisk } = this 324 const useYarnLock = yarnLock && !loadedFromDisk 325 const fromYarn = useYarnLock && yarnLock.entries.get(spec.raw) 326 if (fromYarn && fromYarn.version) { 327 // if it's the yarn or npm default registry, use the version as 328 // our effective spec. if it's any other kind of thing, use that. 329 const { resolved, version, integrity } = fromYarn 330 const isYarnReg = spec.registry && yarnRegRe.test(resolved) 331 const isnpmReg = spec.registry && !isYarnReg && npmRegRe.test(resolved) 332 const isReg = isnpmReg || isYarnReg 333 // don't use the simple version if the "registry" url is 334 // something else entirely! 335 const tgz = isReg && versionFromTgz(spec.name, resolved) || {} 336 let yspec = resolved 337 if (tgz.name === spec.name && tgz.version === version) { 338 yspec = version 339 } else if (isReg && tgz.name && tgz.version) { 340 yspec = `npm:${tgz.name}@${tgz.version}` 341 } 342 if (yspec) { 343 options.resolved = resolved.replace(yarnRegRe, 'https://registry.npmjs.org/') 344 options.integrity = integrity 345 return npa(`${spec.name}@${yspec}`) 346 } 347 } 348 return spec 349 } 350 351 // throw away the shrinkwrap data so we can start fresh 352 // still worth doing a load() first so we know which files to write. 353 reset () { 354 this.tree = null 355 this.#awaitingUpdate = new Map() 356 const lockfileVersion = this.lockfileVersion || defaultLockfileVersion 357 this.originalLockfileVersion = lockfileVersion 358 359 this.data = { 360 lockfileVersion, 361 requires: true, 362 packages: {}, 363 dependencies: {}, 364 } 365 } 366 367 // files to potentially read from and write to, in order of priority 368 get #filenameSet () { 369 if (this.shrinkwrapOnly) { 370 return [`${this.path}/npm-shrinkwrap.json`] 371 } 372 if (this.hiddenLockfile) { 373 return [`${this.path}/node_modules/.package-lock.json`] 374 } 375 return [ 376 `${this.path}/npm-shrinkwrap.json`, 377 `${this.path}/package-lock.json`, 378 `${this.path}/yarn.lock`, 379 ] 380 } 381 382 get loadFiles () { 383 return Promise.all( 384 this.#filenameSet.map(file => file && readFile(file, 'utf8').then(d => d, er => { 385 /* istanbul ignore else - can't test without breaking module itself */ 386 if (er.code === 'ENOENT') { 387 return '' 388 } else { 389 throw er 390 } 391 })) 392 ) 393 } 394 395 get resetFiles () { 396 // slice out yarn, we only care about lock or shrinkwrap when checking 397 // this way, since we're not actually loading the full lock metadata 398 return Promise.all(this.#filenameSet.slice(0, 2) 399 .map(file => file && stat(file).then(st => st.isFile(), er => { 400 /* istanbul ignore else - can't test without breaking module itself */ 401 if (er.code === 'ENOENT') { 402 return null 403 } else { 404 throw er 405 } 406 }) 407 ) 408 ) 409 } 410 411 inferFormattingOptions (packageJSONData) { 412 const { 413 [Symbol.for('indent')]: indent, 414 [Symbol.for('newline')]: newline, 415 } = packageJSONData 416 if (indent !== undefined) { 417 this.indent = indent 418 } 419 if (newline !== undefined) { 420 this.newline = newline 421 } 422 } 423 424 async load () { 425 // we don't need to load package-lock.json except for top of tree nodes, 426 // only npm-shrinkwrap.json. 427 let data 428 try { 429 const [sw, lock, yarn] = await this.loadFiles 430 data = sw || lock || '{}' 431 432 // use shrinkwrap only for deps, otherwise prefer package-lock 433 // and ignore npm-shrinkwrap if both are present. 434 // TODO: emit a warning here or something if both are present. 435 if (this.hiddenLockfile) { 436 this.filename = resolve(this.path, 'node_modules/.package-lock.json') 437 } else if (this.shrinkwrapOnly || sw) { 438 this.filename = resolve(this.path, 'npm-shrinkwrap.json') 439 } else { 440 this.filename = resolve(this.path, 'package-lock.json') 441 } 442 this.type = basename(this.filename) 443 this.loadedFromDisk = Boolean(sw || lock) 444 445 if (yarn) { 446 this.yarnLock = new YarnLock() 447 // ignore invalid yarn data. we'll likely clobber it later anyway. 448 try { 449 this.yarnLock.parse(yarn) 450 } catch { 451 // ignore errors 452 } 453 } 454 455 data = parseJSON(data) 456 this.inferFormattingOptions(data) 457 458 if (this.hiddenLockfile && data.packages) { 459 // add a few ms just to account for jitter 460 const lockTime = +(await stat(this.filename)).mtime + 10 461 await assertNoNewer(this.path, data, lockTime, this.path, new Set()) 462 } 463 464 // all good! hidden lockfile is the newest thing in here. 465 } catch (er) { 466 /* istanbul ignore else */ 467 if (typeof this.filename === 'string') { 468 const rel = relpath(this.path, this.filename) 469 log.verbose('shrinkwrap', `failed to load ${rel}`, er.message) 470 } else { 471 log.verbose('shrinkwrap', `failed to load ${this.path}`, er.message) 472 } 473 this.loadingError = er 474 this.loadedFromDisk = false 475 this.ancientLockfile = false 476 data = {} 477 } 478 // auto convert v1 lockfiles to v3 479 // leave v2 in place unless configured 480 // v3 by default 481 let lockfileVersion = defaultLockfileVersion 482 if (this.lockfileVersion) { 483 lockfileVersion = this.lockfileVersion 484 } else if (data.lockfileVersion && data.lockfileVersion !== 1) { 485 lockfileVersion = data.lockfileVersion 486 } 487 488 this.data = { 489 ...data, 490 lockfileVersion, 491 requires: true, 492 packages: data.packages || {}, 493 dependencies: data.dependencies || {}, 494 } 495 496 this.originalLockfileVersion = data.lockfileVersion 497 498 // use default if it wasn't explicitly set, and the current file is 499 // less than our default. otherwise, keep whatever is in the file, 500 // unless we had an explicit setting already. 501 if (!this.lockfileVersion) { 502 this.lockfileVersion = this.data.lockfileVersion = lockfileVersion 503 } 504 this.ancientLockfile = this.loadedFromDisk && 505 !(data.lockfileVersion >= 2) && !data.requires 506 507 // load old lockfile deps into the packages listing 508 if (data.dependencies && !data.packages) { 509 let pkg 510 try { 511 pkg = await pkgJson.normalize(this.path) 512 pkg = pkg.content 513 } catch { 514 pkg = {} 515 } 516 this.#loadAll('', null, this.data) 517 this.#fixDependencies(pkg) 518 } 519 return this 520 } 521 522 #loadAll (location, name, lock) { 523 // migrate a v1 package lock to the new format. 524 const meta = this.#metaFromLock(location, name, lock) 525 // dependencies nested under a link are actually under the link target 526 if (meta.link) { 527 location = meta.resolved 528 } 529 if (lock.dependencies) { 530 for (const name in lock.dependencies) { 531 const loc = location + (location ? '/' : '') + 'node_modules/' + name 532 this.#loadAll(loc, name, lock.dependencies[name]) 533 } 534 } 535 } 536 537 // v1 lockfiles track the optional/dev flags, but they don't tell us 538 // which thing had what kind of dep on what other thing, so we need 539 // to correct that now, or every link will be considered prod 540 #fixDependencies (pkg) { 541 // we need the root package.json because legacy shrinkwraps just 542 // have requires:true at the root level, which is even less useful 543 // than merging all dep types into one object. 544 const root = this.data.packages[''] 545 for (const key of pkgMetaKeys) { 546 const val = metaFieldFromPkg(pkg, key) 547 if (val) { 548 root[key.replace(/^_/, '')] = val 549 } 550 } 551 552 for (const loc in this.data.packages) { 553 const meta = this.data.packages[loc] 554 if (!meta.requires || !loc) { 555 continue 556 } 557 558 // resolve each require to a meta entry 559 // if this node isn't optional, but the dep is, then it's an optionalDep 560 // likewise for dev deps. 561 // This isn't perfect, but it's a pretty good approximation, and at 562 // least gets us out of having all 'prod' edges, which throws off the 563 // buildIdealTree process 564 for (const name in meta.requires) { 565 const dep = this.#resolveMetaNode(loc, name) 566 // this overwrites the false value set above 567 // default to dependencies if the dep just isn't in the tree, which 568 // maybe should be an error, since it means that the shrinkwrap is 569 // invalid, but we can't do much better without any info. 570 let depType = 'dependencies' 571 /* istanbul ignore else - dev deps are only for the root level */ 572 if (dep?.optional && !meta.optional) { 573 depType = 'optionalDependencies' 574 } else if (dep?.dev && !meta.dev) { 575 // XXX is this even reachable? 576 depType = 'devDependencies' 577 } 578 if (!meta[depType]) { 579 meta[depType] = {} 580 } 581 meta[depType][name] = meta.requires[name] 582 } 583 delete meta.requires 584 } 585 } 586 587 #resolveMetaNode (loc, name) { 588 for (let path = loc; true; path = path.replace(/(^|\/)[^/]*$/, '')) { 589 const check = `${path}${path ? '/' : ''}node_modules/${name}` 590 if (this.data.packages[check]) { 591 return this.data.packages[check] 592 } 593 594 if (!path) { 595 break 596 } 597 } 598 return null 599 } 600 601 #lockFromLoc (lock, path, i = 0) { 602 if (!lock) { 603 return null 604 } 605 606 if (path[i] === '') { 607 i++ 608 } 609 610 if (i >= path.length) { 611 return lock 612 } 613 614 if (!lock.dependencies) { 615 return null 616 } 617 618 return this.#lockFromLoc(lock.dependencies[path[i]], path, i + 1) 619 } 620 621 // pass in a path relative to the root path, or an absolute path, 622 // get back a /-normalized location based on root path. 623 #pathToLoc (path) { 624 return relpath(this.path, resolve(this.path, path)) 625 } 626 627 delete (nodePath) { 628 if (!this.data) { 629 throw new Error('run load() before getting or setting data') 630 } 631 const location = this.#pathToLoc(nodePath) 632 this.#awaitingUpdate.delete(location) 633 634 delete this.data.packages[location] 635 const path = location.split(/(?:^|\/)node_modules\//) 636 const name = path.pop() 637 const pLock = this.#lockFromLoc(this.data, path) 638 if (pLock && pLock.dependencies) { 639 delete pLock.dependencies[name] 640 } 641 } 642 643 get (nodePath) { 644 if (!this.data) { 645 throw new Error('run load() before getting or setting data') 646 } 647 648 const location = this.#pathToLoc(nodePath) 649 if (this.#awaitingUpdate.has(location)) { 650 this.#updateWaitingNode(location) 651 } 652 653 // first try to get from the newer spot, which we know has 654 // all the things we need. 655 if (this.data.packages[location]) { 656 return this.data.packages[location] 657 } 658 659 // otherwise, fall back to the legacy metadata, and hope for the best 660 // get the node in the shrinkwrap corresponding to this spot 661 const path = location.split(/(?:^|\/)node_modules\//) 662 const name = path[path.length - 1] 663 const lock = this.#lockFromLoc(this.data, path) 664 665 return this.#metaFromLock(location, name, lock) 666 } 667 668 #metaFromLock (location, name, lock) { 669 // This function tries as hard as it can to figure out the metadata 670 // from a lockfile which may be outdated or incomplete. Since v1 671 // lockfiles used the "version" field to contain a variety of 672 // different possible types of data, this gets a little complicated. 673 if (!lock) { 674 return {} 675 } 676 677 // try to figure out a npm-package-arg spec from the lockfile entry 678 // This will return null if we could not get anything valid out of it. 679 const spec = specFromLock(name, lock, this.path) 680 681 if (spec.type === 'directory') { 682 // the "version" was a file: url to a non-tarball path 683 // this is a symlink dep. We don't store much metadata 684 // about symlinks, just the target. 685 const target = relpath(this.path, spec.fetchSpec) 686 this.data.packages[location] = { 687 link: true, 688 resolved: target, 689 } 690 // also save the link target, omitting version since we don't know 691 // what it is, but we know it isn't a link to itself! 692 if (!this.data.packages[target]) { 693 this.#metaFromLock(target, name, { ...lock, version: null }) 694 } 695 return this.data.packages[location] 696 } 697 698 const meta = {} 699 // when calling loadAll we'll change these into proper dep objects 700 if (lock.requires && typeof lock.requires === 'object') { 701 meta.requires = lock.requires 702 } 703 704 if (lock.optional) { 705 meta.optional = true 706 } 707 if (lock.dev) { 708 meta.dev = true 709 } 710 711 // the root will typically have a name from the root project's 712 // package.json file. 713 if (location === '') { 714 meta.name = lock.name 715 } 716 717 // if we have integrity, save it now. 718 if (lock.integrity) { 719 meta.integrity = lock.integrity 720 } 721 722 if (lock.version && !lock.integrity) { 723 // this is usually going to be a git url or symlink, but it could 724 // also be a registry dependency that did not have integrity at 725 // the time it was saved. 726 // Symlinks were already handled above, so that leaves git. 727 // 728 // For git, always save the full SSH url. we'll actually fetch the 729 // tgz most of the time, since it's faster, but it won't work for 730 // private repos, and we can't get back to the ssh from the tgz, 731 // so we store the ssh instead. 732 // For unknown git hosts, just resolve to the raw spec in lock.version 733 if (spec.type === 'git') { 734 meta.resolved = consistentResolve(spec, this.path, this.path) 735 736 // return early because there is nothing else we can do with this 737 return this.data.packages[location] = meta 738 } else if (spec.registry) { 739 // registry dep that didn't save integrity. grab the version, and 740 // fall through to pick up the resolved and potentially name. 741 meta.version = lock.version 742 } 743 // only other possible case is a tarball without integrity. 744 // fall through to do what we can with the filename later. 745 } 746 747 // at this point, we know that the spec is either a registry dep 748 // (ie, version, because locking, which means a resolved url), 749 // or a remote dep, or file: url. Remote deps and file urls 750 // have a fetchSpec equal to the fully resolved thing. 751 // Registry deps, we take what's in the lockfile. 752 if (lock.resolved || (spec.type && !spec.registry)) { 753 if (spec.registry) { 754 meta.resolved = lock.resolved 755 } else if (spec.type === 'file') { 756 meta.resolved = consistentResolve(spec, this.path, this.path, true) 757 } else if (spec.fetchSpec) { 758 meta.resolved = spec.fetchSpec 759 } 760 } 761 762 // at this point, if still we don't have a version, do our best to 763 // infer it from the tarball url/file. This works a surprising 764 // amount of the time, even though it's not guaranteed. 765 if (!meta.version) { 766 if (spec.type === 'file' || spec.type === 'remote') { 767 const fromTgz = versionFromTgz(spec.name, spec.fetchSpec) || 768 versionFromTgz(spec.name, meta.resolved) 769 if (fromTgz) { 770 meta.version = fromTgz.version 771 if (fromTgz.name !== name) { 772 meta.name = fromTgz.name 773 } 774 } 775 } else if (spec.type === 'alias') { 776 meta.name = spec.subSpec.name 777 meta.version = spec.subSpec.fetchSpec 778 } else if (spec.type === 'version') { 779 meta.version = spec.fetchSpec 780 } 781 // ok, I did my best! good luck! 782 } 783 784 if (lock.bundled) { 785 meta.inBundle = true 786 } 787 788 // save it for next time 789 return this.data.packages[location] = meta 790 } 791 792 add (node) { 793 if (!this.data) { 794 throw new Error('run load() before getting or setting data') 795 } 796 797 // will be actually updated on read 798 const loc = relpath(this.path, node.path) 799 if (node.path === this.path) { 800 this.tree = node 801 } 802 803 // if we have metadata about this node, and it's a match, then 804 // try to decorate it. 805 if (node.resolved === null || node.integrity === null) { 806 const { 807 resolved, 808 integrity, 809 hasShrinkwrap, 810 version, 811 } = this.get(node.path) 812 813 let pathFixed = null 814 if (resolved) { 815 if (!/^file:/.test(resolved)) { 816 pathFixed = resolved 817 } else { 818 pathFixed = `file:${resolve(this.path, resolved.slice(5)).replace(/#/g, '%23')}` 819 } 820 } 821 822 // if we have one, only set the other if it matches 823 // otherwise it could be for a completely different thing. 824 const resolvedOk = !resolved || !node.resolved || 825 node.resolved === pathFixed 826 const integrityOk = !integrity || !node.integrity || 827 node.integrity === integrity 828 const versionOk = !version || !node.version || version === node.version 829 830 const allOk = (resolved || integrity || version) && 831 resolvedOk && integrityOk && versionOk 832 833 if (allOk) { 834 node.resolved = node.resolved || pathFixed || null 835 node.integrity = node.integrity || integrity || null 836 node.hasShrinkwrap = node.hasShrinkwrap || hasShrinkwrap || false 837 } else { 838 // try to read off the package or node itself 839 const { 840 resolved, 841 integrity, 842 hasShrinkwrap, 843 } = Shrinkwrap.metaFromNode(node, this.path, this.resolveOptions) 844 node.resolved = node.resolved || resolved || null 845 node.integrity = node.integrity || integrity || null 846 node.hasShrinkwrap = node.hasShrinkwrap || hasShrinkwrap || false 847 } 848 } 849 this.#awaitingUpdate.set(loc, node) 850 } 851 852 addEdge (edge) { 853 if (!this.yarnLock || !edge.valid) { 854 return 855 } 856 857 const { to: node } = edge 858 859 // if it's already set up, nothing to do 860 if (node.resolved !== null && node.integrity !== null) { 861 return 862 } 863 864 // if the yarn lock is empty, nothing to do 865 if (!this.yarnLock.entries || !this.yarnLock.entries.size) { 866 return 867 } 868 869 // we relativize the path here because that's how it shows up in the lock 870 // XXX why is this different from pathFixed in this.add?? 871 let pathFixed = null 872 if (node.resolved) { 873 if (!/file:/.test(node.resolved)) { 874 pathFixed = node.resolved 875 } else { 876 pathFixed = consistentResolve(node.resolved, node.path, this.path, true) 877 } 878 } 879 880 const spec = npa(`${node.name}@${edge.spec}`) 881 const entry = this.yarnLock.entries.get(`${node.name}@${edge.spec}`) 882 883 if (!entry || 884 mismatch(node.version, entry.version) || 885 mismatch(node.integrity, entry.integrity) || 886 mismatch(pathFixed, entry.resolved)) { 887 return 888 } 889 890 if (entry.resolved && yarnRegRe.test(entry.resolved) && spec.registry) { 891 entry.resolved = entry.resolved.replace(yarnRegRe, 'https://registry.npmjs.org/') 892 } 893 894 node.integrity = node.integrity || entry.integrity || null 895 node.resolved = node.resolved || 896 consistentResolve(entry.resolved, this.path, node.path) || null 897 898 this.#awaitingUpdate.set(relpath(this.path, node.path), node) 899 } 900 901 #updateWaitingNode (loc) { 902 const node = this.#awaitingUpdate.get(loc) 903 this.#awaitingUpdate.delete(loc) 904 this.data.packages[loc] = Shrinkwrap.metaFromNode( 905 node, 906 this.path, 907 this.resolveOptions) 908 } 909 910 commit () { 911 if (this.tree) { 912 if (this.yarnLock) { 913 this.yarnLock.fromTree(this.tree) 914 } 915 const root = Shrinkwrap.metaFromNode( 916 this.tree.target, 917 this.path, 918 this.resolveOptions) 919 this.data.packages = {} 920 if (Object.keys(root).length) { 921 this.data.packages[''] = root 922 } 923 for (const node of this.tree.root.inventory.values()) { 924 // only way this.tree is not root is if the root is a link to it 925 if (node === this.tree || node.isRoot || node.location === '') { 926 continue 927 } 928 const loc = relpath(this.path, node.path) 929 this.data.packages[loc] = Shrinkwrap.metaFromNode( 930 node, 931 this.path, 932 this.resolveOptions) 933 } 934 } else if (this.#awaitingUpdate.size > 0) { 935 for (const loc of this.#awaitingUpdate.keys()) { 936 this.#updateWaitingNode(loc) 937 } 938 } 939 940 // if we haven't set it by now, use the default 941 if (!this.lockfileVersion) { 942 this.lockfileVersion = defaultLockfileVersion 943 } 944 this.data.lockfileVersion = this.lockfileVersion 945 946 // hidden lockfiles don't include legacy metadata or a root entry 947 if (this.hiddenLockfile) { 948 delete this.data.packages[''] 949 delete this.data.dependencies 950 } else if (this.tree && this.lockfileVersion <= 3) { 951 this.#buildLegacyLockfile(this.tree, this.data) 952 } 953 954 // lf version 1 = dependencies only 955 // lf version 2 = dependencies and packages 956 // lf version 3 = packages only 957 if (this.lockfileVersion >= 3) { 958 const { dependencies, ...data } = this.data 959 return data 960 } else if (this.lockfileVersion < 2) { 961 const { packages, ...data } = this.data 962 return data 963 } else { 964 return { ...this.data } 965 } 966 } 967 968 #buildLegacyLockfile (node, lock, path = []) { 969 if (node === this.tree) { 970 // the root node 971 lock.name = node.packageName || node.name 972 if (node.version) { 973 lock.version = node.version 974 } 975 } 976 977 // npm v6 and before tracked 'from', meaning "the request that led 978 // to this package being installed". However, that's inherently 979 // racey and non-deterministic in a world where deps are deduped 980 // ahead of fetch time. In order to maintain backwards compatibility 981 // with v6 in the lockfile, we do this trick where we pick a valid 982 // dep link out of the edgesIn set. Choose the edge with the fewest 983 // number of `node_modules` sections in the requestor path, and then 984 // lexically sort afterwards. 985 const edge = [...node.edgesIn].filter(e => e.valid).sort((a, b) => { 986 const aloc = a.from.location.split('node_modules') 987 const bloc = b.from.location.split('node_modules') 988 /* istanbul ignore next - sort calling order is indeterminate */ 989 if (aloc.length > bloc.length) { 990 return 1 991 } 992 if (bloc.length > aloc.length) { 993 return -1 994 } 995 return localeCompare(aloc[aloc.length - 1], bloc[bloc.length - 1]) 996 })[0] 997 998 const res = consistentResolve(node.resolved, this.path, this.path, true) 999 const rSpec = specFromResolved(res) 1000 1001 // if we don't have anything (ie, it's extraneous) then use the resolved 1002 // value as if that was where we got it from, since at least it's true. 1003 // if we don't have either, just an empty object so nothing matches below. 1004 // This will effectively just save the version and resolved, as if it's 1005 // a standard version/range dep, which is a reasonable default. 1006 let spec = rSpec 1007 if (edge) { 1008 spec = npa.resolve(node.name, edge.spec, edge.from.realpath) 1009 } 1010 1011 if (node.isLink) { 1012 lock.version = `file:${relpath(this.path, node.realpath).replace(/#/g, '%23')}` 1013 } else if (spec && (spec.type === 'file' || spec.type === 'remote')) { 1014 lock.version = spec.saveSpec 1015 } else if (spec && spec.type === 'git' || rSpec.type === 'git') { 1016 lock.version = node.resolved 1017 /* istanbul ignore else - don't think there are any cases where a git 1018 * spec (or indeed, ANY npa spec) doesn't have a .raw member */ 1019 if (spec.raw) { 1020 lock.from = spec.raw 1021 } 1022 } else if (!node.isRoot && 1023 node.package && 1024 node.packageName && 1025 node.packageName !== node.name) { 1026 lock.version = `npm:${node.packageName}@${node.version}` 1027 } else if (node.package && node.version) { 1028 lock.version = node.version 1029 } 1030 1031 if (node.inDepBundle) { 1032 lock.bundled = true 1033 } 1034 1035 // when we didn't resolve to git, file, or dir, and didn't request 1036 // git, file, dir, or remote, then the resolved value is necessary. 1037 if (node.resolved && 1038 !node.isLink && 1039 rSpec.type !== 'git' && 1040 rSpec.type !== 'file' && 1041 rSpec.type !== 'directory' && 1042 spec.type !== 'directory' && 1043 spec.type !== 'git' && 1044 spec.type !== 'file' && 1045 spec.type !== 'remote') { 1046 lock.resolved = overrideResolves(node.resolved, this.resolveOptions) 1047 } 1048 1049 if (node.integrity) { 1050 lock.integrity = node.integrity 1051 } 1052 1053 if (node.extraneous) { 1054 lock.extraneous = true 1055 } else if (!node.isLink) { 1056 if (node.peer) { 1057 lock.peer = true 1058 } 1059 1060 if (node.devOptional && !node.dev && !node.optional) { 1061 lock.devOptional = true 1062 } 1063 1064 if (node.dev) { 1065 lock.dev = true 1066 } 1067 1068 if (node.optional) { 1069 lock.optional = true 1070 } 1071 } 1072 1073 const depender = node.target 1074 if (depender.edgesOut.size > 0) { 1075 if (node !== this.tree) { 1076 const entries = [...depender.edgesOut.entries()] 1077 lock.requires = entries.reduce((set, [k, v]) => { 1078 // omit peer deps from legacy lockfile requires field, because 1079 // npm v6 doesn't handle peer deps, and this triggers some bad 1080 // behavior if the dep can't be found in the dependencies list. 1081 const { spec, peer } = v 1082 if (peer) { 1083 return set 1084 } 1085 if (spec.startsWith('file:')) { 1086 // turn absolute file: paths into relative paths from the node 1087 // this especially shows up with workspace edges when the root 1088 // node is also a workspace in the set. 1089 const p = resolve(node.realpath, spec.slice('file:'.length)) 1090 set[k] = `file:${relpath(node.realpath, p).replace(/#/g, '%23')}` 1091 } else { 1092 set[k] = spec 1093 } 1094 return set 1095 }, {}) 1096 } else { 1097 lock.requires = true 1098 } 1099 } 1100 1101 // now we walk the children, putting them in the 'dependencies' object 1102 const { children } = node.target 1103 if (!children.size) { 1104 delete lock.dependencies 1105 } else { 1106 const kidPath = [...path, node.realpath] 1107 const dependencies = {} 1108 // skip any that are already in the descent path, so cyclical link 1109 // dependencies don't blow up with ELOOP. 1110 let found = false 1111 for (const [name, kid] of children.entries()) { 1112 if (path.includes(kid.realpath)) { 1113 continue 1114 } 1115 dependencies[name] = this.#buildLegacyLockfile(kid, {}, kidPath) 1116 found = true 1117 } 1118 if (found) { 1119 lock.dependencies = dependencies 1120 } 1121 } 1122 return lock 1123 } 1124 1125 toJSON () { 1126 if (!this.data) { 1127 throw new Error('run load() before getting or setting data') 1128 } 1129 1130 return this.commit() 1131 } 1132 1133 toString (options = {}) { 1134 const data = this.toJSON() 1135 const { format = true } = options 1136 const defaultIndent = this.indent || 2 1137 const indent = format === true ? defaultIndent 1138 : format || 0 1139 const eol = format ? this.newline || '\n' : '' 1140 return stringify(data, swKeyOrder, indent).replace(/\n/g, eol) 1141 } 1142 1143 save (options = {}) { 1144 if (!this.data) { 1145 throw new Error('run load() before saving data') 1146 } 1147 1148 const json = this.toString(options) 1149 if ( 1150 !this.hiddenLockfile 1151 && this.originalLockfileVersion !== undefined 1152 && this.originalLockfileVersion !== this.lockfileVersion 1153 ) { 1154 log.warn( 1155 `Converting lock file (${relative(process.cwd(), this.filename)}) from v${this.originalLockfileVersion} -> v${this.lockfileVersion}` 1156 ) 1157 } 1158 return Promise.all([ 1159 writeFile(this.filename, json).catch(er => { 1160 if (this.hiddenLockfile) { 1161 // well, we did our best. 1162 // if we reify, and there's nothing there, then it might be lacking 1163 // a node_modules folder, but then the lockfile is not important. 1164 // Remove the file, so that in case there WERE deps, but we just 1165 // failed to update the file for some reason, it's not out of sync. 1166 return rm(this.filename, { recursive: true, force: true }) 1167 } 1168 throw er 1169 }), 1170 this.yarnLock && this.yarnLock.entries.size && 1171 writeFile(this.path + '/yarn.lock', this.yarnLock.toString()), 1172 ]) 1173 } 1174} 1175 1176module.exports = Shrinkwrap 1177