• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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