codekingpro/portable-devtools
114k
1const { output } = require('proc-log')
2const pkgJson = require('@npmcli/package-json')
3const BaseCommand = require('../base-cmd.js')
4const { getError } = require('../utils/error-message.js')
5const { outputError } = require('../utils/output-error.js')
6
7class RunScript extends BaseCommand {
8 static description = 'Run arbitrary package scripts'
9 static params = [
10 'workspace',
11 'workspaces',
12 'include-workspace-root',
13 'if-present',
14 'ignore-scripts',
15 'foreground-scripts',
16 'script-shell',
17 ]
18
19 static name = 'run'
20 static usage = ['<command> [-- <args>]']
21 static workspaces = true
22 static ignoreImplicitWorkspace = false
23 static isShellout = true
24 static checkDevEngines = true
25
26 static async completion (opts, npm) {
27 const argv = opts.conf.argv.remain
28 if (argv.length === 2) {
29 const workspacePrefixes = npm.config.get('workspace', 'default')
30 const localPrefix = workspacePrefixes.length
31 ? workspacePrefixes[0]
32 : npm.localPrefix
33 const { content: { scripts = {} } } = await pkgJson.normalize(localPrefix)
34 .catch(() => ({ content: {} }))
35 if (opts.isFish) {
36 return Object.keys(scripts).map(s => `${s}\t${scripts[s].slice(0, 30)}`)
37 }
38 return Object.keys(scripts)
39 }
40 }
41
42 async exec (args) {
43 if (args.length) {
44 await this.#run(args, { path: this.npm.localPrefix })
45 } else {
46 await this.#list(this.npm.localPrefix)
47 }
48 }
49
50 async execWorkspaces (args) {
51 await this.setWorkspaces()
52
53 const ws = [...this.workspaces.entries()]
54 for (const [workspace, path] of ws) {
55 const last = path === ws.at(-1)[1]
56
57 if (!args.length) {
58 const newline = await this.#list(path, { workspace })
59 if (newline && !last) {
60 output.standard()
61 }
62 continue
63 }
64
65 const pkg = await pkgJson.normalize(path).then(p => p.content)
66 try {
67 await this.#run(args, { path, pkg, workspace })
68 } catch (e) {
69 const err = getError(e, { npm: this.npm, command: null })
70 outputError({
71 ...err,
72 error: [
73 ['', `Lifecycle script \`${args[0]}\` failed with error:`],
74 ...err.error,
75 ['workspace', pkg._id || pkg.name],
76 ['location', path],
77 ],
78 })
79 process.exitCode = err.exitCode
80 if (!last) {
81 output.error('')
82 }
83 }
84 }
85 }
86
87 async #run ([event, ...args], { path, pkg, workspace }) {
88 const runScript = require('@npmcli/run-script')
89
90 pkg ??= await pkgJson.normalize(path).then(p => p.content)
91
92 const { scripts = {} } = pkg
93
94 if (event === 'restart' && !scripts.restart) {
95 scripts.restart = 'npm stop --if-present && npm start'
96 } else if (event === 'env' && !scripts.env) {
97 const { isWindowsShell } = require('../utils/is-windows.js')
98 scripts.env = isWindowsShell ? 'SET' : 'env'
99 }
100
101 pkg.scripts = scripts
102
103 if (
104 !Object.prototype.hasOwnProperty.call(scripts, event) &&
105 !(event === 'start' && (await runScript.isServerPackage(path)))
106 ) {
107 if (this.npm.config.get('if-present')) {
108 return
109 }
110
111 const suggestions = require('../utils/did-you-mean.js')(pkg, event)
112 const wsArg = workspace && path !== this.npm.localPrefix
113 ? ` --workspace=${pkg._id || pkg.name}`
114 : ''
115 throw new Error([
116 `Missing script: "${event}"${suggestions}`,
117 '',
118 'To see a list of scripts, run:',
119 ` npm run${wsArg}`,
120 ].join('\n'))
121 }
122
123 // positional args only added to the main event, not pre/post
124 const events = [[event, args]]
125 if (!this.npm.config.get('ignore-scripts')) {
126 if (scripts[`pre${event}`]) {
127 events.unshift([`pre${event}`, []])
128 }
129
130 if (scripts[`post${event}`]) {
131 events.push([`post${event}`, []])
132 }
133 }
134
135 for (const [ev, evArgs] of events) {
136 await runScript({
137 args: evArgs,
138 event: ev,
139 nodeGyp: this.npm.config.get('node-gyp'),
140 path,
141 pkg,
142 // || undefined is because runScript will be unhappy with the default null value
143 scriptShell: this.npm.config.get('script-shell') || undefined,
144 stdio: 'inherit',
145 })
146 }
147 }
148
149 async #list (path, { workspace } = {}) {
150 const { scripts = {}, name, _id } = await pkgJson.normalize(path).then(p => p.content)
151 const scriptEntries = Object.entries(scripts)
152
153 if (this.npm.silent) {
154 return
155 }
156
157 if (this.npm.config.get('json')) {
158 output.buffer(workspace ? { [workspace]: scripts } : scripts)
159 return
160 }
161
162 if (!scriptEntries.length) {
163 return
164 }
165
166 if (this.npm.config.get('parseable')) {
167 output.standard(scriptEntries
168 .map((s) => (workspace ? [workspace, ...s] : s).join(':'))
169 .join('\n')
170 .trim())
171 return
172 }
173
174 const cmdList = [
175 'prepare', 'prepublishOnly',
176 'prepack', 'postpack',
177 'dependencies',
178 'preinstall', 'install', 'postinstall',
179 'prepublish', 'publish', 'postpublish',
180 'prerestart', 'restart', 'postrestart',
181 'prestart', 'start', 'poststart',
182 'prestop', 'stop', 'poststop',
183 'pretest', 'test', 'posttest',
184 'preuninstall', 'uninstall', 'postuninstall',
185 'preversion', 'version', 'postversion',
186 ]
187 const [cmds, runScripts] = scriptEntries.reduce((acc, s) => {
188 acc[cmdList.includes(s[0]) ? 0 : 1].push(s)
189 return acc
190 }, [[], []])
191
192 const { reset, bold, cyan, dim, blue } = this.npm.chalk
193 const pkgId = `in ${cyan(_id || name)}`
194 const title = (t) => reset(bold(t))
195
196 if (cmds.length) {
197 output.standard(`${title('Lifecycle scripts')} included ${pkgId}:`)
198 for (const [k, v] of cmds) {
199 output.standard(` ${k}`)
200 output.standard(` ${dim(v)}`)
201 }
202 }
203
204 if (runScripts.length) {
205 const via = `via \`${blue('npm run')}\`:`
206 if (!cmds.length) {
207 output.standard(`${title('Scripts')} available ${pkgId} ${via}`)
208 } else {
209 output.standard(`available ${via}`)
210 }
211 for (const [k, v] of runScripts) {
212 output.standard(` ${k}`)
213 output.standard(` ${dim(v)}`)
214 }
215 }
216
217 // Return true to indicate that something was output for this path that should be separated from others
218 return true
219 }
220}
221
222module.exports = RunScript
223 