codekingpro/portable-devtools
114k
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 