/
opt
/
alt
/
alt-nodejs18
/
root
/
lib
/
node_modules
/
npm
/
node_modules
/
@npmcli
/
arborist
/
lib
/
arborist
/
Upload Filee
HOME
// Arborist.rebuild({path = this.path}) will do all the binlinks and // bundle building needed. Called by reify, and by `npm rebuild`. const localeCompare = require('@isaacs/string-locale-compare')('en') const { depth: dfwalk } = require('treeverse') const promiseAllRejectLate = require('promise-all-reject-late') const rpj = require('read-package-json-fast') const binLinks = require('bin-links') const runScript = require('@npmcli/run-script') const { callLimit: promiseCallLimit } = require('promise-call-limit') const { resolve } = require('path') const { isNodeGypPackage, defaultGypInstallScript } = require('@npmcli/node-gyp') const { log, time } = require('proc-log') const boolEnv = b => b ? '1' : '' const sortNodes = (a, b) => (a.depth - b.depth) || localeCompare(a.path, b.path) const _checkBins = Symbol.for('checkBins') // defined by reify mixin const _handleOptionalFailure = Symbol.for('handleOptionalFailure') const _trashList = Symbol.for('trashList') module.exports = cls => class Builder extends cls { #doHandleOptionalFailure #oldMeta = null #queues constructor (options) { super(options) this.scriptsRun = new Set() this.#resetQueues() } async rebuild ({ nodes, handleOptionalFailure = false } = {}) { // nothing to do if we're not building anything! if (this.options.ignoreScripts && !this.options.binLinks) { return } // when building for the first time, as part of reify, we ignore // failures in optional nodes, and just delete them. however, when // running JUST a rebuild, we treat optional failures as real fails this.#doHandleOptionalFailure = handleOptionalFailure if (!nodes) { nodes = await this.#loadDefaultNodes() } // separates links nodes so that it can run // prepare scripts and link bins in the expected order const timeEnd = time.start('build') const { depNodes, linkNodes, } = this.#retrieveNodesByType(nodes) // build regular deps await this.#build(depNodes, {}) // build link deps if (linkNodes.size) { this.#resetQueues() await this.#build(linkNodes, { type: 'links' }) } timeEnd() } // if we don't have a set of nodes, then just rebuild // the actual tree on disk. async #loadDefaultNodes () { let nodes const tree = await this.loadActual() let filterSet if (!this.options.workspacesEnabled) { filterSet = this.excludeWorkspacesDependencySet(tree) nodes = tree.inventory.filter(node => filterSet.has(node) || node.isProjectRoot ) } else if (this.options.workspaces.length) { filterSet = this.workspaceDependencySet( tree, this.options.workspaces, this.options.includeWorkspaceRoot ) nodes = tree.inventory.filter(node => filterSet.has(node)) } else { nodes = tree.inventory.values() } return nodes } #retrieveNodesByType (nodes) { const depNodes = new Set() const linkNodes = new Set() const storeNodes = new Set() for (const node of nodes) { if (node.isStoreLink) { storeNodes.add(node) } else if (node.isLink) { linkNodes.add(node) } else { depNodes.add(node) } } // Make sure that store linked nodes are processed last. // We can't process store links separately or else lifecycle scripts on // standard nodes might not have bin links yet. for (const node of storeNodes) { depNodes.add(node) } // deduplicates link nodes and their targets, avoids // calling lifecycle scripts twice when running `npm rebuild` // ref: https://github.com/npm/cli/issues/2905 // // we avoid doing so if global=true since `bin-links` relies // on having the target nodes available in global mode. if (!this.options.global) { for (const node of linkNodes) { depNodes.delete(node.target) } } return { depNodes, linkNodes, } } #resetQueues () { this.#queues = { preinstall: [], install: [], postinstall: [], prepare: [], bin: [], } } async #build (nodes, { type = 'deps' }) { const timeEnd = time.start(`build:${type}`) await this.#buildQueues(nodes) if (!this.options.ignoreScripts) { await this.#runScripts('preinstall') } // links should run prepare scripts and only link bins after that if (type === 'links') { await this.#runScripts('prepare') } if (this.options.binLinks) { await this.#linkAllBins() } if (!this.options.ignoreScripts) { await this.#runScripts('install') await this.#runScripts('postinstall') } timeEnd() } async #buildQueues (nodes) { const timeEnd = time.start('build:queue') const set = new Set() const promises = [] for (const node of nodes) { promises.push(this.#addToBuildSet(node, set)) // if it has bundle deps, add those too, if rebuildBundle if (this.options.rebuildBundle !== false) { const bd = node.package.bundleDependencies if (bd && bd.length) { dfwalk({ tree: node, leave: node => promises.push(this.#addToBuildSet(node, set)), getChildren: node => [...node.children.values()], filter: node => node.inBundle, }) } } } await promiseAllRejectLate(promises) // now sort into the queues for the 4 things we have to do // run in the same predictable order that buildIdealTree uses // there's no particular reason for doing it in this order rather // than another, but sorting *somehow* makes it consistent. const queue = [...set].sort(sortNodes) for (const node of queue) { const { package: { bin, scripts = {} } } = node.target const { preinstall, install, postinstall, prepare } = scripts const tests = { bin, preinstall, install, postinstall, prepare } for (const [key, has] of Object.entries(tests)) { if (has) { this.#queues[key].push(node) } } } timeEnd() } async [_checkBins] (node) { // if the node is a global top, and we're not in force mode, then // any existing bins need to either be missing, or a symlink into // the node path. Otherwise a package can have a preinstall script // that unlinks something, to allow them to silently overwrite system // binaries, which is unsafe and insecure. if (!node.globalTop || this.options.force) { return } const { path, package: pkg } = node await binLinks.checkBins({ pkg, path, top: true, global: true }) } async #addToBuildSet (node, set, refreshed = false) { if (set.has(node)) { return } if (this.#oldMeta === null) { const { root: { meta } } = node this.#oldMeta = meta && meta.loadedFromDisk && !(meta.originalLockfileVersion >= 2) } const { package: pkg, hasInstallScript } = node.target const { gypfile, bin, scripts = {} } = pkg const { preinstall, install, postinstall, prepare } = scripts const anyScript = preinstall || install || postinstall || prepare if (!refreshed && !anyScript && (hasInstallScript || this.#oldMeta)) { // we either have an old metadata (and thus might have scripts) // or we have an indication that there's install scripts (but // don't yet know what they are) so we have to load the package.json // from disk to see what the deal is. Failure here just means // no scripts to add, probably borked package.json. // add to the set then remove while we're reading the pj, so we // don't accidentally hit it multiple times. set.add(node) const pkg = await rpj(node.path + '/package.json').catch(() => ({})) set.delete(node) const { scripts = {} } = pkg node.package.scripts = scripts return this.#addToBuildSet(node, set, true) } // Rebuild node-gyp dependencies lacking an install or preinstall script // note that 'scripts' might be missing entirely, and the package may // set gypfile:false to avoid this automatic detection. const isGyp = gypfile !== false && !install && !preinstall && await isNodeGypPackage(node.path) if (bin || preinstall || install || postinstall || prepare || isGyp) { if (bin) { await this[_checkBins](node) } if (isGyp) { scripts.install = defaultGypInstallScript node.package.scripts = scripts } set.add(node) } } async #runScripts (event) { const queue = this.#queues[event] if (!queue.length) { return } const timeEnd = time.start(`build:run:${event}`) const stdio = this.options.foregroundScripts ? 'inherit' : 'pipe' const limit = this.options.foregroundScripts ? 1 : undefined await promiseCallLimit(queue.map(node => async () => { const { path, integrity, resolved, optional, peer, dev, devOptional, package: pkg, location, isStoreLink, } = node.target // skip any that we know we'll be deleting // or storeLinks if (this[_trashList].has(path) || isStoreLink) { return } const timeEndLocation = time.start(`build:run:${event}:${location}`) log.info('run', pkg._id, event, location, pkg.scripts[event]) const env = { npm_package_resolved: resolved, npm_package_integrity: integrity, npm_package_json: resolve(path, 'package.json'), npm_package_optional: boolEnv(optional), npm_package_dev: boolEnv(dev), npm_package_peer: boolEnv(peer), npm_package_dev_optional: boolEnv(devOptional && !dev && !optional), } const runOpts = { event, path, pkg, stdio, env, scriptShell: this.options.scriptShell, } const p = runScript(runOpts).catch(er => { const { code, signal } = er log.info('run', pkg._id, event, { code, signal }) throw er }).then(({ args, code, signal, stdout, stderr }) => { this.scriptsRun.add({ pkg, path, event, // I do not know why this needs to be on THIS line but refactoring // this function would be quite a process // eslint-disable-next-line promise/always-return cmd: args && args[args.length - 1], env, code, signal, stdout, stderr, }) log.info('run', pkg._id, event, { code, signal }) }) await (this.#doHandleOptionalFailure ? this[_handleOptionalFailure](node, p) : p) timeEndLocation() }), { limit }) timeEnd() } async #linkAllBins () { const queue = this.#queues.bin if (!queue.length) { return } const timeEnd = time.start('build:link') const promises = [] // sort the queue by node path, so that the module-local collision // detector in bin-links will always resolve the same way. for (const node of queue.sort(sortNodes)) { // TODO these run before they're awaited promises.push(this.#createBinLinks(node)) } await promiseAllRejectLate(promises) timeEnd() } async #createBinLinks (node) { if (this[_trashList].has(node.path)) { return } const timeEnd = time.start(`build:link:${node.location}`) const p = binLinks({ pkg: node.package, path: node.path, top: !!(node.isTop || node.globalTop), force: this.options.force, global: !!node.globalTop, }) await (this.#doHandleOptionalFailure ? this[_handleOptionalFailure](node, p) : p) timeEnd() } }