1'use strict' 2 3const Buffer = require('safe-buffer').Buffer 4const promisify = require('./util.js').promisify 5 6const child = require('./child') 7const fs = require('fs') 8const parseArgs = require('./parse-args.js') 9const path = require('path') 10const which = promisify(require('which')) 11 12module.exports = npx 13module.exports.parseArgs = parseArgs 14function npx (argv) { 15 const shell = argv['shell-auto-fallback'] 16 if (shell || shell === '') { 17 const fallback = require('./auto-fallback.js')( 18 shell, process.env.SHELL, argv 19 ) 20 if (fallback) { 21 return console.log(fallback) 22 } else { 23 process.exitCode = 1 24 return 25 } 26 } 27 28 if (!argv.call && (!argv.command || !argv.package)) { 29 !argv.q && console.error(Y()`\nERROR: You must supply a command.\n`) 30 !argv.q && parseArgs.showHelp() 31 process.exitCode = 1 32 return 33 } 34 35 const startTime = Date.now() 36 37 // First, we look to see if we're inside an npm project, and grab its 38 // bin path. This is exactly the same as running `$ npm bin`. 39 return localBinPath(process.cwd()).then(local => { 40 if (local) { 41 // Local project paths take priority. Go ahead and prepend it. 42 process.env.PATH = `${local}${path.delimiter}${process.env.PATH}` 43 } 44 return Promise.all([ 45 // Figuring out if a command exists, early on, lets us maybe 46 // short-circuit a few things later. This bit here primarily benefits 47 // calls like `$ npx foo`, where we might just be trying to invoke 48 // a single command and use whatever is already in the path. 49 argv.command && getExistingPath(argv.command, argv), 50 // The `-c` flag involves special behavior when used: in this case, 51 // we take a bit of extra time to pick up npm's full lifecycle script 52 // environment (so you can use `$npm_package_xxxxx` and company). 53 // Without that flag, we just use the current env. 54 argv.call && local && getEnv(argv) 55 ]).then(args => { 56 const existing = args[0] 57 const newEnv = args[1] 58 if (newEnv) { 59 // NOTE - we don't need to manipulate PATH further here, because 60 // npm has already done so. And even added the node-gyp path! 61 Object.assign(process.env, newEnv) 62 } 63 if ((!existing && !argv.call) || argv.packageRequested) { 64 // We only fire off the updateNotifier if we're installing things 65 if (argv.npxPkg) { 66 try { 67 require('update-notifier')({ 68 pkg: require(argv.npxPkg) 69 }).notify() 70 } catch (e) {} 71 } 72 // Some npm packages need to be installed. Let's install them! 73 return ensurePackages(argv.package, argv).then(results => { 74 if (results && results.added && results.updated && !argv.q) { 75 console.error(Y()`npx: installed ${ 76 results.added.length + results.updated.length 77 } in ${(Date.now() - startTime) / 1000}s`) 78 } 79 if ( 80 argv.command && 81 !existing && 82 !argv.packageRequested && 83 argv.package.length === 1 84 ) { 85 return promisify(fs.readdir)(results.bin).then(bins => { 86 if (process.platform === 'win32') { 87 bins = bins.filter(b => b !== 'etc' && b !== 'node_modules') 88 } 89 if (bins.length < 1) { 90 throw new Error(Y()`command not found: ${argv.command}`) 91 } 92 const cmd = new RegExp(`^${argv.command}(?:\\.cmd)?$`, 'i') 93 const matching = bins.find(b => b.match(cmd)) 94 return path.resolve(results.bin, bins[matching] || bins[0]) 95 }, err => { 96 if (err.code === 'ENOENT') { 97 throw new Error(Y()`command not found: ${argv.command}`) 98 } else { 99 throw err 100 } 101 }) 102 } else { 103 return existing 104 } 105 }) 106 } else { 107 // We can skip any extra installation, 'cause everything exists. 108 return existing 109 } 110 }).then(existing => { 111 return execCommand(existing, argv) 112 }).catch(err => { 113 !argv.q && console.error(err.message) 114 process.exitCode = err.exitCode || 1 115 }) 116 }) 117} 118 119module.exports._localBinPath = localBinPath 120function localBinPath (cwd) { 121 return require('./get-prefix.js')(cwd).then(prefix => { 122 return prefix && path.join(prefix, 'node_modules', '.bin') 123 }) 124} 125 126module.exports._getEnv = getEnv 127function getEnv (opts) { 128 const args = ['run', 'env', '--parseable'] 129 return findNodeScript(opts.npm, {isLocal: true}).then(npmPath => { 130 if (npmPath) { 131 args.unshift(child.escapeArg(opts.npm)) 132 return process.argv[0] 133 } else { 134 return opts.npm 135 } 136 }).then(npmPath => { 137 return child.exec(npmPath, args) 138 }).then(require('dotenv').parse) 139} 140 141module.exports._ensurePackages = ensurePackages 142function ensurePackages (specs, opts) { 143 return ( 144 opts.cache ? Promise.resolve(opts.cache) : getNpmCache(opts) 145 ).then(cache => { 146 const prefix = path.join(cache, '_npx', process.pid.toString()) 147 const bins = process.platform === 'win32' 148 ? prefix 149 : path.join(prefix, 'bin') 150 const rimraf = require('rimraf') 151 process.on('exit', () => rimraf.sync(prefix)) 152 return promisify(rimraf)(bins).then(() => { 153 return installPackages(specs, prefix, opts) 154 }).then(info => { 155 // This will make temp bins _higher priority_ than even local bins. 156 // This is intentional, since npx assumes that if you went through 157 // the trouble of doing `-p`, you're rather have that one. Right? ;) 158 process.env.PATH = `${bins}${path.delimiter}${process.env.PATH}` 159 if (!info) { info = {} } 160 info.prefix = prefix 161 info.bin = bins 162 return info 163 }) 164 }) 165} 166 167module.exports._getExistingPath = getExistingPath 168function getExistingPath (command, opts) { 169 if (opts.isLocal) { 170 return Promise.resolve(command) 171 } else if ( 172 opts.cmdHadVersion || opts.packageRequested || opts.ignoreExisting 173 ) { 174 return Promise.resolve(false) 175 } else { 176 return which(command).catch(err => { 177 if (err.code === 'ENOENT') { 178 if (opts.install === false) { 179 err.exitCode = 127 180 throw err 181 } 182 } else { 183 throw err 184 } 185 }) 186 } 187} 188 189module.exports._getNpmCache = getNpmCache 190function getNpmCache (opts) { 191 const args = ['config', 'get', 'cache', '--parseable'] 192 if (opts.userconfig) { 193 args.push('--userconfig', child.escapeArg(opts.userconfig, true)) 194 } 195 return findNodeScript(opts.npm, {isLocal: true}).then(npmPath => { 196 if (npmPath) { 197 // This one is NOT escaped as a path because it's handed to Node. 198 args.unshift(child.escapeArg(opts.npm)) 199 return process.argv[0] 200 } else { 201 return opts.npm 202 } 203 }).then(npmPath => { 204 return child.exec(npmPath, args) 205 }).then(cache => cache.trim()) 206} 207 208module.exports._buildArgs = buildArgs 209function buildArgs (specs, prefix, opts) { 210 const args = ['install'].concat(specs) 211 args.push('--global', '--prefix', prefix) 212 if (opts.cache) args.push('--cache', opts.cache) 213 if (opts.userconfig) args.push('--userconfig', opts.userconfig) 214 args.push('--loglevel', 'error', '--json') 215 216 return args 217} 218 219module.exports._installPackages = installPackages 220function installPackages (specs, prefix, opts) { 221 const args = buildArgs(specs, prefix, opts) 222 return findNodeScript(opts.npm, {isLocal: true}).then(npmPath => { 223 if (npmPath) { 224 args.unshift( 225 process.platform === 'win32' 226 ? child.escapeArg(opts.npm) 227 : opts.npm 228 ) 229 return process.argv[0] 230 } else { 231 return opts.npm 232 } 233 }).then(npmPath => { 234 return process.platform === 'win32' ? child.escapeArg(npmPath, true) : npmPath 235 }).then(npmPath => { 236 return child.spawn(npmPath, args, { 237 stdio: opts.installerStdio 238 ? opts.installerStdio 239 : [0, 'pipe', opts.q ? 'ignore' : 2] 240 }).then(deets => { 241 try { 242 return deets.stdout ? JSON.parse(deets.stdout) : null 243 } catch (e) { } 244 }, err => { 245 if (err.exitCode) { 246 err.message = Y()`Install for ${specs} failed with code ${err.exitCode}` 247 } 248 throw err 249 }) 250 }) 251} 252 253module.exports._execCommand = execCommand 254function execCommand (_existing, argv) { 255 return findNodeScript(_existing, argv).then(existing => { 256 const argvCmdOpts = argv.cmdOpts || [] 257 if (existing && !argv.alwaysSpawn && !argv.nodeArg && !argv.shell && existing !== process.argv[1]) { 258 const Module = require('module') 259 // let it take over the process. This means we can skip node startup! 260 if (!argv.noYargs) { 261 // blow away built-up yargs crud 262 require('yargs').reset() 263 } 264 process.argv = [ 265 process.argv[0], // Current node binary 266 existing // node script path. `runMain()` will set this as the new main 267 ].concat(argvCmdOpts) // options for the cmd itself 268 Module.runMain() // ✨MAGIC✨. Sorry-not-sorry 269 } else if (!existing && argv.nodeArg && argv.nodeArg.length) { 270 throw new Error(Y()`ERROR: --node-arg/-n can only be used on packages with node scripts.`) 271 } else { 272 let cmd = existing 273 let cmdOpts = argvCmdOpts 274 if (existing) { 275 cmd = process.argv[0] 276 if (process.platform === 'win32') { 277 cmd = child.escapeArg(cmd, true) 278 } 279 // If we know we're running a run script and we got a --node-arg, 280 // we need to fudge things a bit to get them working right. 281 cmdOpts = argv.nodeArg 282 if (cmdOpts) { 283 cmdOpts = Array.isArray(cmdOpts) ? cmdOpts : [cmdOpts] 284 } else { 285 cmdOpts = [] 286 } 287 // It's valid for a single arg to be a string of multiple 288 // space-separated node args. 289 // Example: `$ npx -n '--inspect --harmony --debug' ...` 290 cmdOpts = cmdOpts.reduce((acc, arg) => { 291 return acc.concat(arg.split(/\s+/)) 292 }, []) 293 cmdOpts = cmdOpts.concat(existing, argvCmdOpts) 294 } 295 const opts = Object.assign({}, argv, { cmdOpts }) 296 return child.runCommand(cmd, opts).catch(err => { 297 if (err.isOperational && err.exitCode) { 298 // At this point, we want to treat errors from the child as if 299 // we were just running the command. That means no extra msg logging 300 process.exitCode = err.exitCode 301 } else { 302 // But if it's not just a regular child-level error, blow up normally 303 throw err 304 } 305 }) 306 } 307 }) 308} 309 310module.exports._findNodeScript = findNodeScript 311function findNodeScript (existing, opts) { 312 if (!existing) { 313 return Promise.resolve(false) 314 } else { 315 return promisify(fs.stat)(existing).then(stat => { 316 if (opts && opts.isLocal && path.extname(existing) === '.js') { 317 return existing 318 } else if (opts && opts.isLocal && stat.isDirectory()) { 319 // npx will execute the directory itself 320 try { 321 const pkg = require(path.resolve(existing, 'package.json')) 322 const target = path.resolve(existing, pkg.bin || pkg.main || 'index.js') 323 return findNodeScript(target, opts).then(script => { 324 if (script) { 325 return script 326 } else { 327 throw new Error(Y()`command not found: ${target}`) 328 } 329 }) 330 } catch (e) { 331 throw new Error(Y()`command not found: ${existing}`) 332 } 333 } else if (process.platform !== 'win32') { 334 const bytecount = 400 335 const buf = Buffer.alloc(bytecount) 336 return promisify(fs.open)(existing, 'r').then(fd => { 337 return promisify(fs.read)(fd, buf, 0, bytecount, 0).then(() => { 338 return promisify(fs.close)(fd) 339 }, err => { 340 return promisify(fs.close)(fd).then(() => { throw err }) 341 }) 342 }).then(() => { 343 const re = /#!\s*(?:\/usr\/bin\/env\s*node|\/usr\/local\/bin\/node|\/usr\/bin\/node)\s*\r?\n/i 344 return buf.toString('utf8').match(re) && existing 345 }) 346 } else if (process.platform === 'win32') { 347 const buf = Buffer.alloc(1000) 348 return promisify(fs.open)(existing, 'r').then(fd => { 349 return promisify(fs.read)(fd, buf, 0, 1000, 0).then(() => { 350 return promisify(fs.close)(fd) 351 }, err => { 352 return promisify(fs.close)(fd).then(() => { throw err }) 353 }) 354 }).then(() => { 355 return buf.toString('utf8').trim() 356 }).then(str => { 357 const cmd = /"%~dp0\\node\.exe"\s+"%~dp0\\(.*)"\s+%\*/ 358 const mingw = /"\$basedir\/node"\s+"\$basedir\/(.*)"\s+"\$@"/i 359 return str.match(cmd) || str.match(mingw) 360 }).then(match => { 361 return match && path.join(path.dirname(existing), match[1]) 362 }) 363 } 364 }) 365 } 366} 367 368function Y () { 369 return require('./y.js') 370} 371