• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// mixin providing the loadVirtual method
2const mapWorkspaces = require('@npmcli/map-workspaces')
3
4const { resolve } = require('path')
5
6const nameFromFolder = require('@npmcli/name-from-folder')
7const consistentResolve = require('../consistent-resolve.js')
8const Shrinkwrap = require('../shrinkwrap.js')
9const Node = require('../node.js')
10const Link = require('../link.js')
11const relpath = require('../relpath.js')
12const calcDepFlags = require('../calc-dep-flags.js')
13const rpj = require('read-package-json-fast')
14const treeCheck = require('../tree-check.js')
15
16const flagsSuspect = Symbol.for('flagsSuspect')
17const setWorkspaces = Symbol.for('setWorkspaces')
18
19module.exports = cls => class VirtualLoader extends cls {
20  #rootOptionProvided
21
22  constructor (options) {
23    super(options)
24
25    // the virtual tree we load from a shrinkwrap
26    this.virtualTree = options.virtualTree
27    this[flagsSuspect] = false
28  }
29
30  // public method
31  async loadVirtual (options = {}) {
32    if (this.virtualTree) {
33      return this.virtualTree
34    }
35
36    // allow the user to set reify options on the ctor as well.
37    // XXX: deprecate separate reify() options object.
38    options = { ...this.options, ...options }
39
40    if (options.root && options.root.meta) {
41      await this.#loadFromShrinkwrap(options.root.meta, options.root)
42      return treeCheck(this.virtualTree)
43    }
44
45    const s = await Shrinkwrap.load({
46      path: this.path,
47      lockfileVersion: this.options.lockfileVersion,
48      resolveOptions: this.options,
49    })
50    if (!s.loadedFromDisk && !options.root) {
51      const er = new Error('loadVirtual requires existing shrinkwrap file')
52      throw Object.assign(er, { code: 'ENOLOCK' })
53    }
54
55    // when building the ideal tree, we pass in a root node to this function
56    // otherwise, load it from the root package json or the lockfile
57    const {
58      root = await this.#loadRoot(s),
59    } = options
60
61    this.#rootOptionProvided = options.root
62
63    await this.#loadFromShrinkwrap(s, root)
64    root.assertRootOverrides()
65    return treeCheck(this.virtualTree)
66  }
67
68  async #loadRoot (s) {
69    const pj = this.path + '/package.json'
70    const pkg = await rpj(pj).catch(() => s.data.packages['']) || {}
71    return this[setWorkspaces](this.#loadNode('', pkg, true))
72  }
73
74  async #loadFromShrinkwrap (s, root) {
75    if (!this.#rootOptionProvided) {
76      // root is never any of these things, but might be a brand new
77      // baby Node object that never had its dep flags calculated.
78      root.extraneous = false
79      root.dev = false
80      root.optional = false
81      root.devOptional = false
82      root.peer = false
83    } else {
84      this[flagsSuspect] = true
85    }
86
87    this.#checkRootEdges(s, root)
88    root.meta = s
89    this.virtualTree = root
90    const { links, nodes } = this.#resolveNodes(s, root)
91    await this.#resolveLinks(links, nodes)
92    if (!(s.originalLockfileVersion >= 2)) {
93      this.#assignBundles(nodes)
94    }
95    if (this[flagsSuspect]) {
96      // reset all dep flags
97      // can't use inventory here, because virtualTree might not be root
98      for (const node of nodes.values()) {
99        if (node.isRoot || node === this.#rootOptionProvided) {
100          continue
101        }
102        node.extraneous = true
103        node.dev = true
104        node.optional = true
105        node.devOptional = true
106        node.peer = true
107      }
108      calcDepFlags(this.virtualTree, !this.#rootOptionProvided)
109    }
110    return root
111  }
112
113  // check the lockfile deps, and see if they match.  if they do not
114  // then we have to reset dep flags at the end.  for example, if the
115  // user manually edits their package.json file, then we need to know
116  // that the idealTree is no longer entirely trustworthy.
117  #checkRootEdges (s, root) {
118    // loaded virtually from tree, no chance of being out of sync
119    // ancient lockfiles are critically damaged by this process,
120    // so we need to just hope for the best in those cases.
121    if (!s.loadedFromDisk || s.ancientLockfile) {
122      return
123    }
124
125    const lock = s.get('')
126    const prod = lock.dependencies || {}
127    const dev = lock.devDependencies || {}
128    const optional = lock.optionalDependencies || {}
129    const peer = lock.peerDependencies || {}
130    const peerOptional = {}
131
132    if (lock.peerDependenciesMeta) {
133      for (const [name, meta] of Object.entries(lock.peerDependenciesMeta)) {
134        if (meta.optional && peer[name] !== undefined) {
135          peerOptional[name] = peer[name]
136          delete peer[name]
137        }
138      }
139    }
140
141    for (const name of Object.keys(optional)) {
142      delete prod[name]
143    }
144
145    const lockWS = {}
146    const workspaces = mapWorkspaces.virtual({
147      cwd: this.path,
148      lockfile: s.data,
149    })
150
151    for (const [name, path] of workspaces.entries()) {
152      lockWS[name] = `file:${path.replace(/#/g, '%23')}`
153    }
154
155    // Should rootNames exclude optional?
156    const rootNames = new Set(root.edgesOut.keys())
157
158    const lockByType = ({ dev, optional, peer, peerOptional, prod, workspace: lockWS })
159
160    // Find anything in shrinkwrap deps that doesn't match root's type or spec
161    for (const type in lockByType) {
162      const deps = lockByType[type]
163      for (const name in deps) {
164        const edge = root.edgesOut.get(name)
165        if (!edge || edge.type !== type || edge.spec !== deps[name]) {
166          return this[flagsSuspect] = true
167        }
168        rootNames.delete(name)
169      }
170    }
171    // Something was in root that's not accounted for in shrinkwrap
172    if (rootNames.size) {
173      return this[flagsSuspect] = true
174    }
175  }
176
177  // separate out link metadatas, and create Node objects for nodes
178  #resolveNodes (s, root) {
179    const links = new Map()
180    const nodes = new Map([['', root]])
181    for (const [location, meta] of Object.entries(s.data.packages)) {
182      // skip the root because we already got it
183      if (!location) {
184        continue
185      }
186
187      if (meta.link) {
188        links.set(location, meta)
189      } else {
190        nodes.set(location, this.#loadNode(location, meta))
191      }
192    }
193    return { links, nodes }
194  }
195
196  // links is the set of metadata, and nodes is the map of non-Link nodes
197  // Set the targets to nodes in the set, if we have them (we might not)
198  async #resolveLinks (links, nodes) {
199    for (const [location, meta] of links.entries()) {
200      const targetPath = resolve(this.path, meta.resolved)
201      const targetLoc = relpath(this.path, targetPath)
202      const target = nodes.get(targetLoc)
203      const link = this.#loadLink(location, targetLoc, target, meta)
204      nodes.set(location, link)
205      nodes.set(targetLoc, link.target)
206
207      // we always need to read the package.json for link targets
208      // outside node_modules because they can be changed by the local user
209      if (!link.target.parent) {
210        const pj = link.realpath + '/package.json'
211        const pkg = await rpj(pj).catch(() => null)
212        if (pkg) {
213          link.target.package = pkg
214        }
215      }
216    }
217  }
218
219  #assignBundles (nodes) {
220    for (const [location, node] of nodes) {
221      // Skip assignment of parentage for the root package
222      if (!location || node.isLink && !node.target.location) {
223        continue
224      }
225      const { name, parent, package: { inBundle } } = node
226
227      if (!parent) {
228        continue
229      }
230
231      // read inBundle from package because 'package' here is
232      // actually a v2 lockfile metadata entry.
233      // If the *parent* is also bundled, though, or if the parent has
234      // no dependency on it, then we assume that it's being pulled in
235      // just by virtue of its parent or a transitive dep being bundled.
236      const { package: ppkg } = parent
237      const { inBundle: parentBundled } = ppkg
238      if (inBundle && !parentBundled && parent.edgesOut.has(node.name)) {
239        if (!ppkg.bundleDependencies) {
240          ppkg.bundleDependencies = [name]
241        } else {
242          ppkg.bundleDependencies.push(name)
243        }
244      }
245    }
246  }
247
248  #loadNode (location, sw, loadOverrides) {
249    const p = this.virtualTree ? this.virtualTree.realpath : this.path
250    const path = resolve(p, location)
251    // shrinkwrap doesn't include package name unless necessary
252    if (!sw.name) {
253      sw.name = nameFromFolder(path)
254    }
255
256    const dev = sw.dev
257    const optional = sw.optional
258    const devOptional = dev || optional || sw.devOptional
259    const peer = sw.peer
260
261    const node = new Node({
262      installLinks: this.installLinks,
263      legacyPeerDeps: this.legacyPeerDeps,
264      root: this.virtualTree,
265      path,
266      realpath: path,
267      integrity: sw.integrity,
268      resolved: consistentResolve(sw.resolved, this.path, path),
269      pkg: sw,
270      hasShrinkwrap: sw.hasShrinkwrap,
271      dev,
272      optional,
273      devOptional,
274      peer,
275      loadOverrides,
276    })
277    // cast to boolean because they're undefined in the lock file when false
278    node.extraneous = !!sw.extraneous
279    node.devOptional = !!(sw.devOptional || sw.dev || sw.optional)
280    node.peer = !!sw.peer
281    node.optional = !!sw.optional
282    node.dev = !!sw.dev
283    return node
284  }
285
286  #loadLink (location, targetLoc, target, meta) {
287    const path = resolve(this.path, location)
288    const link = new Link({
289      installLinks: this.installLinks,
290      legacyPeerDeps: this.legacyPeerDeps,
291      path,
292      realpath: resolve(this.path, targetLoc),
293      target,
294      pkg: target && target.package,
295    })
296    link.extraneous = target.extraneous
297    link.devOptional = target.devOptional
298    link.peer = target.peer
299    link.optional = target.optional
300    link.dev = target.dev
301    return link
302  }
303}
304