xref: /third_party/node/deps/npm/lib/commands/exec.js (revision 1cb0ef41)
1const { resolve } = require('path')
2const libexec = require('libnpmexec')
3const BaseCommand = require('../base-command.js')
4
5class Exec extends BaseCommand {
6  static description = 'Run a command from a local or remote npm package'
7  static params = [
8    'package',
9    'call',
10    'workspace',
11    'workspaces',
12    'include-workspace-root',
13  ]
14
15  static name = 'exec'
16  static usage = [
17    '-- <pkg>[@<version>] [args...]',
18    '--package=<pkg>[@<version>] -- <cmd> [args...]',
19    '-c \'<cmd> [args...]\'',
20    '--package=foo -c \'<cmd> [args...]\'',
21  ]
22
23  static workspaces = true
24  static ignoreImplicitWorkspace = false
25  static isShellout = true
26
27  async exec (args) {
28    return this.callExec(args)
29  }
30
31  async execWorkspaces (args) {
32    await this.setWorkspaces()
33
34    for (const [name, path] of this.workspaces) {
35      const locationMsg =
36        `in workspace ${this.npm.chalk.green(name)} at location:\n${this.npm.chalk.dim(path)}`
37      await this.callExec(args, { name, locationMsg, runPath: path })
38    }
39  }
40
41  async callExec (args, { name, locationMsg, runPath } = {}) {
42    // This is where libnpmexec will look for locally installed packages at the project level
43    const localPrefix = this.npm.localPrefix
44    // This is where libnpmexec will look for locally installed packages at the workspace level
45    let localBin = this.npm.localBin
46    let path = localPrefix
47
48    // This is where libnpmexec will actually run the scripts from
49    if (!runPath) {
50      runPath = process.cwd()
51    } else {
52      // We have to consider if the workspace has its own separate versions
53      // libnpmexec will walk up to localDir after looking here
54      localBin = resolve(this.npm.localDir, name, 'node_modules', '.bin')
55      // We also need to look for `bin` entries in the workspace package.json
56      // libnpmexec will NOT look in the project root for the bin entry
57      path = runPath
58    }
59
60    const call = this.npm.config.get('call')
61    let globalPath
62    const {
63      flatOptions,
64      globalBin,
65      globalDir,
66      chalk,
67    } = this.npm
68    const output = this.npm.output.bind(this.npm)
69    const scriptShell = this.npm.config.get('script-shell') || undefined
70    const packages = this.npm.config.get('package')
71    const yes = this.npm.config.get('yes')
72    // --prefix sets both of these to the same thing, meaning the global prefix
73    // is invalid (i.e. no lib/node_modules).  This is not a trivial thing to
74    // untangle and fix so we work around it here.
75    if (this.npm.localPrefix !== this.npm.globalPrefix) {
76      globalPath = resolve(globalDir, '..')
77    }
78
79    if (call && args.length) {
80      throw this.usageError()
81    }
82
83    return libexec({
84      ...flatOptions,
85      // we explicitly set packageLockOnly to false because if it's true
86      // when we try to install a missing package, we won't actually install it
87      packageLockOnly: false,
88      // copy args so they dont get mutated
89      args: [...args],
90      call,
91      chalk,
92      globalBin,
93      globalPath,
94      localBin,
95      locationMsg,
96      output,
97      packages,
98      path,
99      runPath,
100      scriptShell,
101      yes,
102    })
103  }
104}
105
106module.exports = Exec
107