1/* eslint-disable camelcase */ 2const fs = require('fs') 3const util = require('util') 4const readdir = util.promisify(fs.readdir) 5const reifyFinish = require('../utils/reify-finish.js') 6const log = require('../utils/log-shim.js') 7const { resolve, join } = require('path') 8const runScript = require('@npmcli/run-script') 9const pacote = require('pacote') 10const checks = require('npm-install-checks') 11 12const ArboristWorkspaceCmd = require('../arborist-cmd.js') 13class Install extends ArboristWorkspaceCmd { 14 static description = 'Install a package' 15 static name = 'install' 16 17 // These are in the order they will show up in when running "-h" 18 // If adding to this list, consider adding also to ci.js 19 static params = [ 20 'save', 21 'save-exact', 22 'global', 23 'install-strategy', 24 'legacy-bundling', 25 'global-style', 26 'omit', 27 'include', 28 'strict-peer-deps', 29 'prefer-dedupe', 30 'package-lock', 31 'package-lock-only', 32 'foreground-scripts', 33 'ignore-scripts', 34 'audit', 35 'bin-links', 36 'fund', 37 'dry-run', 38 'cpu', 39 'os', 40 'libc', 41 ...super.params, 42 ] 43 44 static usage = ['[<package-spec> ...]'] 45 46 static async completion (opts) { 47 const { partialWord } = opts 48 // install can complete to a folder with a package.json, or any package. 49 // if it has a slash, then it's gotta be a folder 50 // if it starts with https?://, then just give up, because it's a url 51 if (/^https?:\/\//.test(partialWord)) { 52 // do not complete to URLs 53 return [] 54 } 55 56 if (/\//.test(partialWord)) { 57 // Complete fully to folder if there is exactly one match and it 58 // is a folder containing a package.json file. If that is not the 59 // case we return 0 matches, which will trigger the default bash 60 // complete. 61 const lastSlashIdx = partialWord.lastIndexOf('/') 62 const partialName = partialWord.slice(lastSlashIdx + 1) 63 const partialPath = partialWord.slice(0, lastSlashIdx) || '/' 64 65 const isDirMatch = async sibling => { 66 if (sibling.slice(0, partialName.length) !== partialName) { 67 return false 68 } 69 70 try { 71 const contents = await readdir(join(partialPath, sibling)) 72 const result = (contents.indexOf('package.json') !== -1) 73 return result 74 } catch (er) { 75 return false 76 } 77 } 78 79 try { 80 const siblings = await readdir(partialPath) 81 const matches = [] 82 for (const sibling of siblings) { 83 if (await isDirMatch(sibling)) { 84 matches.push(sibling) 85 } 86 } 87 if (matches.length === 1) { 88 return [join(partialPath, matches[0])] 89 } 90 // no matches 91 return [] 92 } catch (er) { 93 return [] // invalid dir: no matching 94 } 95 } 96 // Note: there used to be registry completion here, 97 // but it stopped making sense somewhere around 98 // 50,000 packages on the registry 99 } 100 101 async exec (args) { 102 // the /path/to/node_modules/.. 103 const globalTop = resolve(this.npm.globalDir, '..') 104 const ignoreScripts = this.npm.config.get('ignore-scripts') 105 const isGlobalInstall = this.npm.global 106 const where = isGlobalInstall ? globalTop : this.npm.prefix 107 const forced = this.npm.config.get('force') 108 const scriptShell = this.npm.config.get('script-shell') || undefined 109 110 // be very strict about engines when trying to update npm itself 111 const npmInstall = args.find(arg => arg.startsWith('npm@') || arg === 'npm') 112 if (isGlobalInstall && npmInstall) { 113 const npmOptions = this.npm.flatOptions 114 const npmManifest = await pacote.manifest(npmInstall, npmOptions) 115 try { 116 checks.checkEngine(npmManifest, npmManifest.version, process.version) 117 } catch (e) { 118 if (forced) { 119 log.warn( 120 'install', 121 /* eslint-disable-next-line max-len */ 122 `Forcing global npm install with incompatible version ${npmManifest.version} into node ${process.version}` 123 ) 124 } else { 125 throw e 126 } 127 } 128 } 129 130 // don't try to install the prefix into itself 131 args = args.filter(a => resolve(a) !== this.npm.prefix) 132 133 // `npm i -g` => "install this package globally" 134 if (where === globalTop && !args.length) { 135 args = ['.'] 136 } 137 138 // throw usage error if trying to install empty package 139 // name to global space, e.g: `npm i -g ""` 140 if (where === globalTop && !args.every(Boolean)) { 141 throw this.usageError() 142 } 143 144 const Arborist = require('@npmcli/arborist') 145 const opts = { 146 ...this.npm.flatOptions, 147 auditLevel: null, 148 path: where, 149 add: args, 150 workspaces: this.workspaceNames, 151 } 152 const arb = new Arborist(opts) 153 await arb.reify(opts) 154 155 if (!args.length && !isGlobalInstall && !ignoreScripts) { 156 const scripts = [ 157 'preinstall', 158 'install', 159 'postinstall', 160 'prepublish', // XXX(npm9) should we remove this finally?? 161 'preprepare', 162 'prepare', 163 'postprepare', 164 ] 165 for (const event of scripts) { 166 await runScript({ 167 path: where, 168 args: [], 169 scriptShell, 170 stdio: 'inherit', 171 banner: !this.npm.silent, 172 event, 173 }) 174 } 175 } 176 await reifyFinish(this.npm, arb) 177 } 178} 179module.exports = Install 180