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