codekingpro/portable-devtools
114k
1const { resolve } = require('node:path')
2const libexec = require('libnpmexec')
3const BaseCommand = require('../base-cmd.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 let localBin = this.npm.localBin
43 let pkgPath = this.npm.localPrefix
44
45 // This is where libnpmexec will actually run the scripts from
46 if (!runPath) {
47 runPath = process.cwd()
48 } else {
49 // We have to consider if the workspace has its own separate versions libnpmexec will walk up to localDir after looking here
50 localBin = resolve(this.npm.localDir, name, 'node_modules', '.bin')
51 // We also need to look for `bin` entries in the workspace package.json
52 // libnpmexec will NOT look in the project root for the bin entry
53 pkgPath = runPath
54 }
55
56 const call = this.npm.config.get('call')
57 let globalPath
58 const {
59 flatOptions,
60 globalBin,
61 globalDir,
62 chalk,
63 } = this.npm
64 const scriptShell = this.npm.config.get('script-shell') || undefined
65 const packages = this.npm.config.get('package')
66 const yes = this.npm.config.get('yes')
67 // --prefix sets both of these to the same thing, meaning the global prefix is invalid (i.e. no lib/node_modules).
68 // This is not a trivial thing to untangle and fix so we work around it here.
69 if (this.npm.localPrefix !== this.npm.globalPrefix) {
70 globalPath = resolve(globalDir, '..')
71 }
72
73 if (call && args.length) {
74 throw this.usageError()
75 }
76
77 return libexec({
78 ...flatOptions,
79 // we explicitly set packageLockOnly to false because if it's true when we try to install a missing package, we won't actually install it
80 packageLockOnly: false,
81 // what the user asked to run args[0] is run by default
82 args: [...args], // copy args so they don't get mutated
83 // specify a custom command to be run instead of args[0]
84 call,
85 chalk,
86 // where to look for bins globally, if a file matches call or args[0] it is called
87 globalBin,
88 // where to look for packages globally, if a package matches call or args[0] it is called
89 globalPath,
90 // where to look for bins locally, if a file matches call or args[0] it is called
91 localBin,
92 locationMsg,
93 // packages that need to be installed
94 packages,
95 // path where node_modules is
96 path: this.npm.localPrefix,
97 // where to look for package.json#bin entries first
98 pkgPath,
99 // cwd to run from
100 runPath,
101 scriptShell,
102 yes,
103 })
104 }
105}
106
107module.exports = Exec
108 