관리-도구
편집 파일: load-virtual.js
// mixin providing the loadVirtual method const localeCompare = require('@isaacs/string-locale-compare')('en') const mapWorkspaces = require('@npmcli/map-workspaces') const { resolve } = require('path') const nameFromFolder = require('@npmcli/name-from-folder') const consistentResolve = require('../consistent-resolve.js') const Shrinkwrap = require('../shrinkwrap.js') const Node = require('../node.js') const Link = require('../link.js') const relpath = require('../relpath.js') const calcDepFlags = require('../calc-dep-flags.js') const rpj = require('read-package-json-fast') const treeCheck = require('../tree-check.js') const loadFromShrinkwrap = Symbol('loadFromShrinkwrap') const resolveNodes = Symbol('resolveNodes') const resolveLinks = Symbol('resolveLinks') const assignBundles = Symbol('assignBundles') const loadRoot = Symbol('loadRoot') const loadNode = Symbol('loadVirtualNode') const loadLink = Symbol('loadVirtualLink') const loadWorkspaces = Symbol.for('loadWorkspaces') const flagsSuspect = Symbol.for('flagsSuspect') const reCalcDepFlags = Symbol('reCalcDepFlags') const checkRootEdges = Symbol('checkRootEdges') const rootOptionProvided = Symbol('rootOptionProvided') const depsToEdges = (type, deps) => Object.entries(deps).map(d => [type, ...d]) module.exports = cls => class VirtualLoader extends cls { constructor (options) { super(options) // the virtual tree we load from a shrinkwrap this.virtualTree = options.virtualTree this[flagsSuspect] = false } // public method async loadVirtual (options = {}) { if (this.virtualTree) { return this.virtualTree } // allow the user to set reify options on the ctor as well. // XXX: deprecate separate reify() options object. options = { ...this.options, ...options } if (options.root && options.root.meta) { await this[loadFromShrinkwrap](options.root.meta, options.root) return treeCheck(this.virtualTree) } const s = await Shrinkwrap.load({ path: this.path, lockfileVersion: this.options.lockfileVersion, resolveOptions: this.options, }) if (!s.loadedFromDisk && !options.root) { const er = new Error('loadVirtual requires existing shrinkwrap file') throw Object.assign(er, { code: 'ENOLOCK' }) } // when building the ideal tree, we pass in a root node to this function // otherwise, load it from the root package json or the lockfile const { root = await this[loadRoot](s), } = options this[rootOptionProvided] = options.root await this[loadFromShrinkwrap](s, root) root.assertRootOverrides() return treeCheck(this.virtualTree) } async [loadRoot] (s) { const pj = this.path + '/package.json' const pkg = await rpj(pj).catch(() => s.data.packages['']) || {} return this[loadWorkspaces](this[loadNode]('', pkg, true)) } async [loadFromShrinkwrap] (s, root) { if (!this[rootOptionProvided]) { // root is never any of these things, but might be a brand new // baby Node object that never had its dep flags calculated. root.extraneous = false root.dev = false root.optional = false root.devOptional = false root.peer = false } else { this[flagsSuspect] = true } this[checkRootEdges](s, root) root.meta = s this.virtualTree = root const { links, nodes } = this[resolveNodes](s, root) await this[resolveLinks](links, nodes) if (!(s.originalLockfileVersion >= 2)) { this[assignBundles](nodes) } if (this[flagsSuspect]) { this[reCalcDepFlags](nodes.values()) } return root } [reCalcDepFlags] (nodes) { // reset all dep flags // can't use inventory here, because virtualTree might not be root for (const node of nodes) { if (node.isRoot || node === this[rootOptionProvided]) { continue } node.extraneous = true node.dev = true node.optional = true node.devOptional = true node.peer = true } calcDepFlags(this.virtualTree, !this[rootOptionProvided]) } // check the lockfile deps, and see if they match. if they do not // then we have to reset dep flags at the end. for example, if the // user manually edits their package.json file, then we need to know // that the idealTree is no longer entirely trustworthy. [checkRootEdges] (s, root) { // loaded virtually from tree, no chance of being out of sync // ancient lockfiles are critically damaged by this process, // so we need to just hope for the best in those cases. if (!s.loadedFromDisk || s.ancientLockfile) { return } const lock = s.get('') const prod = lock.dependencies || {} const dev = lock.devDependencies || {} const optional = lock.optionalDependencies || {} const peer = lock.peerDependencies || {} const peerOptional = {} if (lock.peerDependenciesMeta) { for (const [name, meta] of Object.entries(lock.peerDependenciesMeta)) { if (meta.optional && peer[name] !== undefined) { peerOptional[name] = peer[name] delete peer[name] } } } for (const name of Object.keys(optional)) { delete prod[name] } const lockWS = [] const workspaces = mapWorkspaces.virtual({ cwd: this.path, lockfile: s.data, }) for (const [name, path] of workspaces.entries()) { lockWS.push(['workspace', name, `file:${path.replace(/#/g, '%23')}`]) } const lockEdges = [ ...depsToEdges('prod', prod), ...depsToEdges('dev', dev), ...depsToEdges('optional', optional), ...depsToEdges('peer', peer), ...depsToEdges('peerOptional', peerOptional), ...lockWS, ].sort(([atype, aname], [btype, bname]) => localeCompare(atype, btype) || localeCompare(aname, bname)) const rootEdges = [...root.edgesOut.values()] .map(e => [e.type, e.name, e.spec]) .sort(([atype, aname], [btype, bname]) => localeCompare(atype, btype) || localeCompare(aname, bname)) if (rootEdges.length !== lockEdges.length) { // something added or removed return this[flagsSuspect] = true } for (let i = 0; i < lockEdges.length; i++) { if (rootEdges[i][0] !== lockEdges[i][0] || rootEdges[i][1] !== lockEdges[i][1] || rootEdges[i][2] !== lockEdges[i][2]) { return this[flagsSuspect] = true } } } // separate out link metadatas, and create Node objects for nodes [resolveNodes] (s, root) { const links = new Map() const nodes = new Map([['', root]]) for (const [location, meta] of Object.entries(s.data.packages)) { // skip the root because we already got it if (!location) { continue } if (meta.link) { links.set(location, meta) } else { nodes.set(location, this[loadNode](location, meta)) } } return { links, nodes } } // links is the set of metadata, and nodes is the map of non-Link nodes // Set the targets to nodes in the set, if we have them (we might not) async [resolveLinks] (links, nodes) { for (const [location, meta] of links.entries()) { const targetPath = resolve(this.path, meta.resolved) const targetLoc = relpath(this.path, targetPath) const target = nodes.get(targetLoc) const link = this[loadLink](location, targetLoc, target, meta) nodes.set(location, link) nodes.set(targetLoc, link.target) // we always need to read the package.json for link targets // outside node_modules because they can be changed by the local user if (!link.target.parent) { const pj = link.realpath + '/package.json' const pkg = await rpj(pj).catch(() => null) if (pkg) { link.target.package = pkg } } } } [assignBundles] (nodes) { for (const [location, node] of nodes) { // Skip assignment of parentage for the root package if (!location || node.isLink && !node.target.location) { continue } const { name, parent, package: { inBundle } } = node if (!parent) { continue } // read inBundle from package because 'package' here is // actually a v2 lockfile metadata entry. // If the *parent* is also bundled, though, or if the parent has // no dependency on it, then we assume that it's being pulled in // just by virtue of its parent or a transitive dep being bundled. const { package: ppkg } = parent const { inBundle: parentBundled } = ppkg if (inBundle && !parentBundled && parent.edgesOut.has(node.name)) { if (!ppkg.bundleDependencies) { ppkg.bundleDependencies = [name] } else { ppkg.bundleDependencies.push(name) } } } } [loadNode] (location, sw, loadOverrides) { const p = this.virtualTree ? this.virtualTree.realpath : this.path const path = resolve(p, location) // shrinkwrap doesn't include package name unless necessary if (!sw.name) { sw.name = nameFromFolder(path) } const dev = sw.dev const optional = sw.optional const devOptional = dev || optional || sw.devOptional const peer = sw.peer const node = new Node({ installLinks: this.installLinks, legacyPeerDeps: this.legacyPeerDeps, root: this.virtualTree, path, realpath: path, integrity: sw.integrity, resolved: consistentResolve(sw.resolved, this.path, path), pkg: sw, hasShrinkwrap: sw.hasShrinkwrap, dev, optional, devOptional, peer, loadOverrides, }) // cast to boolean because they're undefined in the lock file when false node.extraneous = !!sw.extraneous node.devOptional = !!(sw.devOptional || sw.dev || sw.optional) node.peer = !!sw.peer node.optional = !!sw.optional node.dev = !!sw.dev return node } [loadLink] (location, targetLoc, target, meta) { const path = resolve(this.path, location) const link = new Link({ installLinks: this.installLinks, legacyPeerDeps: this.legacyPeerDeps, path, realpath: resolve(this.path, targetLoc), target, pkg: target && target.package, }) link.extraneous = target.extraneous link.devOptional = target.devOptional link.peer = target.peer link.optional = target.optional link.dev = target.dev return link } }