Team Ai
Datasetpublic

codekingpro/portable-devtools

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