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