File Explorer

/proc/2/root/var/lang/lib/node_modules/npm/node_modules/@npmcli/arborist/lib/arborist

This explorer reads the filesystem of the server it runs on, so /workspace/user isn't present here. Browsing and the terminal still work against this server's own disk from /.

rebuild.js13.4 KB · 427 lines
// Arborist.rebuild({path = this.path}) will do all the binlinks and// bundle building needed.  Called by reify, and by `npm rebuild`. const PackageJson = require('@npmcli/package-json')const binLinks = require('bin-links')const localeCompare = require('@isaacs/string-locale-compare')('en')const promiseAllRejectLate = require('promise-all-reject-late')const runScript = require('@npmcli/run-script')const { callLimit: promiseCallLimit } = require('promise-call-limit')const { depth: dfwalk } = require('treeverse')const { isNodeGypPackage, defaultGypInstallScript } = require('@npmcli/node-gyp')const { promiseRetry } = require('@gar/promise-retry')const { log, time } = require('proc-log')const { resolve, delimiter } = require('node:path')const { isScriptAllowed } = require('../script-allowed.js') 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 mixinconst _handleOptionalFailure = Symbol.for('handleOptionalFailure')const _trashList = Symbol.for('trashList') module.exports = cls => class Builder extends cls {  #doHandleOptionalFailure  #oldMeta = null  #queues = {    preinstall: [],    install: [],    postinstall: [],    prepare: [],    bin: [],  }   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.#queues = {        preinstall: [],        install: [],        postinstall: [],        prepare: [],        bin: [],      }      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,    }  }   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') {      if (!this.options.ignoreScripts) {        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 }      // A denied package (allowScripts resolves to `false`) still gets its      // bins linked; only its lifecycle scripts are skipped, matching      // --ignore-scripts (npm/cli#9681). --dangerously-allow-all-scripts      // bypasses the gate.      const scriptsDenied =        !this.options.dangerouslyAllowAllScripts &&        isScriptAllowed(node, this.options.allowScripts) === false      for (const [key, has] of Object.entries(tests)) {        if (!has) {          continue        }        if (key !== 'bin' && scriptsDenied) {          continue        }        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 { content: pkg } = await PackageJson.normalize(node.path).catch(() => {        return { content: {} }      })      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,        name,        integrity,        resolved,        optional,        peer,        dev,        devOptional,        package: pkg,        location,        isInStore,      } = node.target       // skip any that we know we'll be deleting      // or links to store entries (their scripts run on the store      // entry itself, not through the link)      if (this[_trashList].has(path) || (node.isLink && node.target?.isInStore)) {        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),      }      // In the linked strategy a store package's dependencies are symlinked siblings in its store node_modules.      // A separate bin invoked by the script (e.g. napi-postinstall) resolves modules from its own realpath in the store and cannot see those deps, so expose them via NODE_PATH.      if (isInStore) {        const storeNodeModules = resolve(path, ...name.split('/').map(() => '..'))        env.NODE_PATH = [storeNodeModules, process.env.NODE_PATH].filter(Boolean).join(delimiter)      }      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}`)     // On Windows, antivirus/indexer can transiently lock files, causing EPERM/EACCES/EBUSY on the rename inside write-file-atomic (used by bin-links/fix-bin.js), so, retry with backoff.    const p = promiseRetry((retry) => binLinks({      pkg: node.package,      path: node.path,      top: !!(node.isTop || node.globalTop),      force: this.options.force,      global: !!node.globalTop,    }).catch(/* istanbul ignore next - Windows-only transient antivirus locks */ err => {      if (process.platform === 'win32' &&          (err.code === 'EPERM' || err.code === 'EACCES' || err.code === 'EBUSY')) {        return retry(err)      }      throw err    }), { retries: 5, minTimeout: 500 })     await (this.#doHandleOptionalFailure      ? this[_handleOptionalFailure](node, p)      : p)     timeEnd()  }}