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