/* eslint-disable camelcase */ const fs = require('fs') const util = require('util') const readdir = util.promisify(fs.readdir) const reifyFinish = require('../utils/reify-finish.js') const log = require('../utils/log-shim.js') const { resolve, join } = require('path') const Arborist = require('@npmcli/arborist') const runScript = require('@npmcli/run-script') const pacote = require('pacote') const checks = require('npm-install-checks') const ArboristWorkspaceCmd = require('../arborist-cmd.js') class Install extends ArboristWorkspaceCmd { static description = 'Install a package' static name = 'install' // These are in the order they will show up in when running "-h" static params = [ 'save', 'save-exact', 'global', 'install-strategy', 'legacy-bundling', 'global-style', 'omit', 'strict-peer-deps', 'package-lock', 'foreground-scripts', 'ignore-scripts', 'audit', 'bin-links', 'fund', 'dry-run', ...super.params, ] static usage = ['[ ...]'] async completion (opts) { const { partialWord } = opts // install can complete to a folder with a package.json, or any package. // if it has a slash, then it's gotta be a folder // if it starts with https?://, then just give up, because it's a url if (/^https?:\/\//.test(partialWord)) { // do not complete to URLs return [] } if (/\//.test(partialWord)) { // Complete fully to folder if there is exactly one match and it // is a folder containing a package.json file. If that is not the // case we return 0 matches, which will trigger the default bash // complete. const lastSlashIdx = partialWord.lastIndexOf('/') const partialName = partialWord.slice(lastSlashIdx + 1) const partialPath = partialWord.slice(0, lastSlashIdx) || '/' const isDirMatch = async sibling => { if (sibling.slice(0, partialName.length) !== partialName) { return false } try { const contents = await readdir(join(partialPath, sibling)) const result = (contents.indexOf('package.json') !== -1) return result } catch (er) { return false } } try { const siblings = await readdir(partialPath) const matches = [] for (const sibling of siblings) { if (await isDirMatch(sibling)) { matches.push(sibling) } } if (matches.length === 1) { return [join(partialPath, matches[0])] } // no matches return [] } catch (er) { return [] // invalid dir: no matching } } // Note: there used to be registry completion here, // but it stopped making sense somewhere around // 50,000 packages on the registry } async exec (args) { // the /path/to/node_modules/.. const globalTop = resolve(this.npm.globalDir, '..') const ignoreScripts = this.npm.config.get('ignore-scripts') const isGlobalInstall = this.npm.global const where = isGlobalInstall ? globalTop : this.npm.prefix const forced = this.npm.config.get('force') const scriptShell = this.npm.config.get('script-shell') || undefined // be very strict about engines when trying to update npm itself const npmInstall = args.find(arg => arg.startsWith('npm@') || arg === 'npm') if (isGlobalInstall && npmInstall) { const npmOptions = this.npm.flatOptions const npmManifest = await pacote.manifest(npmInstall, npmOptions) try { checks.checkEngine(npmManifest, npmManifest.version, process.version) } catch (e) { if (forced) { log.warn( 'install', /* eslint-disable-next-line max-len */ `Forcing global npm install with incompatible version ${npmManifest.version} into node ${process.version}` ) } else { throw e } } } // don't try to install the prefix into itself args = args.filter(a => resolve(a) !== this.npm.prefix) // `npm i -g` => "install this package globally" if (where === globalTop && !args.length) { args = ['.'] } // throw usage error if trying to install empty package // name to global space, e.g: `npm i -g ""` if (where === globalTop && !args.every(Boolean)) { throw this.usageError() } const opts = { ...this.npm.flatOptions, auditLevel: null, path: where, add: args, workspaces: this.workspaceNames, } const arb = new Arborist(opts) await arb.reify(opts) if (!args.length && !isGlobalInstall && !ignoreScripts) { const scripts = [ 'preinstall', 'install', 'postinstall', 'prepublish', // XXX(npm9) should we remove this finally?? 'preprepare', 'prepare', 'postprepare', ] for (const event of scripts) { await runScript({ path: where, args: [], scriptShell, stdio: 'inherit', banner: !this.npm.silent, event, }) } } await reifyFinish(this.npm, arb) } } module.exports = Install