1const runScript = require('@npmcli/run-script') 2const { isServerPackage } = runScript 3const pkgJson = require('@npmcli/package-json') 4const log = require('../utils/log-shim.js') 5const didYouMean = require('../utils/did-you-mean.js') 6const { isWindowsShell } = require('../utils/is-windows.js') 7 8const cmdList = [ 9 'publish', 10 'install', 11 'uninstall', 12 'test', 13 'stop', 14 'start', 15 'restart', 16 'version', 17].reduce((l, p) => l.concat(['pre' + p, p, 'post' + p]), []) 18 19const BaseCommand = require('../base-command.js') 20class RunScript extends BaseCommand { 21 static description = 'Run arbitrary package scripts' 22 static params = [ 23 'workspace', 24 'workspaces', 25 'include-workspace-root', 26 'if-present', 27 'ignore-scripts', 28 'foreground-scripts', 29 'script-shell', 30 ] 31 32 static name = 'run-script' 33 static usage = ['<command> [-- <args>]'] 34 static workspaces = true 35 static ignoreImplicitWorkspace = false 36 static isShellout = true 37 38 static async completion (opts, npm) { 39 const argv = opts.conf.argv.remain 40 if (argv.length === 2) { 41 const { content: { scripts = {} } } = await pkgJson.normalize(npm.localPrefix) 42 .catch(er => ({ content: {} })) 43 if (opts.isFish) { 44 return Object.keys(scripts).map(s => `${s}\t${scripts[s].slice(0, 30)}`) 45 } 46 return Object.keys(scripts) 47 } 48 } 49 50 async exec (args) { 51 if (args.length) { 52 return this.run(args) 53 } else { 54 return this.list(args) 55 } 56 } 57 58 async execWorkspaces (args) { 59 if (args.length) { 60 return this.runWorkspaces(args) 61 } else { 62 return this.listWorkspaces(args) 63 } 64 } 65 66 async run ([event, ...args], { path = this.npm.localPrefix, pkg } = {}) { 67 // this || undefined is because runScript will be unhappy with the default 68 // null value 69 const scriptShell = this.npm.config.get('script-shell') || undefined 70 71 if (!pkg) { 72 const { content } = await pkgJson.normalize(path) 73 pkg = content 74 } 75 const { scripts = {} } = pkg 76 77 if (event === 'restart' && !scripts.restart) { 78 scripts.restart = 'npm stop --if-present && npm start' 79 } else if (event === 'env' && !scripts.env) { 80 scripts.env = isWindowsShell ? 'SET' : 'env' 81 } 82 83 pkg.scripts = scripts 84 85 if ( 86 !Object.prototype.hasOwnProperty.call(scripts, event) && 87 !(event === 'start' && (await isServerPackage(path))) 88 ) { 89 if (this.npm.config.get('if-present')) { 90 return 91 } 92 93 const suggestions = await didYouMean(path, event) 94 throw new Error( 95 `Missing script: "${event}"${suggestions}\n\nTo see a list of scripts, run:\n npm run` 96 ) 97 } 98 99 // positional args only added to the main event, not pre/post 100 const events = [[event, args]] 101 if (!this.npm.config.get('ignore-scripts')) { 102 if (scripts[`pre${event}`]) { 103 events.unshift([`pre${event}`, []]) 104 } 105 106 if (scripts[`post${event}`]) { 107 events.push([`post${event}`, []]) 108 } 109 } 110 111 const opts = { 112 path, 113 args, 114 scriptShell, 115 stdio: 'inherit', 116 pkg, 117 banner: !this.npm.silent, 118 } 119 120 for (const [ev, evArgs] of events) { 121 await runScript({ 122 ...opts, 123 event: ev, 124 args: evArgs, 125 }) 126 } 127 } 128 129 async list (args, path) { 130 /* eslint-disable-next-line max-len */ 131 const { content: { scripts, name, _id } } = await pkgJson.normalize(path || this.npm.localPrefix) 132 const pkgid = _id || name 133 134 if (!scripts) { 135 return [] 136 } 137 138 const allScripts = Object.keys(scripts) 139 if (this.npm.silent) { 140 return allScripts 141 } 142 143 if (this.npm.config.get('json')) { 144 this.npm.output(JSON.stringify(scripts, null, 2)) 145 return allScripts 146 } 147 148 if (this.npm.config.get('parseable')) { 149 for (const [script, cmd] of Object.entries(scripts)) { 150 this.npm.output(`${script}:${cmd}`) 151 } 152 153 return allScripts 154 } 155 156 const indent = '\n ' 157 const prefix = ' ' 158 const cmds = [] 159 const runScripts = [] 160 for (const script of allScripts) { 161 const list = cmdList.includes(script) ? cmds : runScripts 162 list.push(script) 163 } 164 const colorize = this.npm.chalk 165 166 if (cmds.length) { 167 this.npm.output( 168 `${colorize.reset(colorize.bold('Lifecycle scripts'))} included in ${colorize.green( 169 pkgid 170 )}:` 171 ) 172 } 173 174 for (const script of cmds) { 175 this.npm.output(prefix + script + indent + colorize.dim(scripts[script])) 176 } 177 178 if (!cmds.length && runScripts.length) { 179 this.npm.output( 180 `${colorize.bold('Scripts')} available in ${colorize.green(pkgid)} via \`${colorize.blue( 181 'npm run-script' 182 )}\`:` 183 ) 184 } else if (runScripts.length) { 185 this.npm.output(`\navailable via \`${colorize.blue('npm run-script')}\`:`) 186 } 187 188 for (const script of runScripts) { 189 this.npm.output(prefix + script + indent + colorize.dim(scripts[script])) 190 } 191 192 this.npm.output('') 193 return allScripts 194 } 195 196 async runWorkspaces (args, filters) { 197 const res = [] 198 await this.setWorkspaces() 199 200 for (const workspacePath of this.workspacePaths) { 201 const { content: pkg } = await pkgJson.normalize(workspacePath) 202 const runResult = await this.run(args, { 203 path: workspacePath, 204 pkg, 205 }).catch(err => { 206 log.error(`Lifecycle script \`${args[0]}\` failed with error:`) 207 log.error(err) 208 log.error(` in workspace: ${pkg._id || pkg.name}`) 209 log.error(` at location: ${workspacePath}`) 210 211 const scriptMissing = err.message.startsWith('Missing script') 212 213 // avoids exiting with error code in case there's scripts missing 214 // in some workspaces since other scripts might have succeeded 215 if (!scriptMissing) { 216 process.exitCode = 1 217 } 218 219 return scriptMissing 220 }) 221 res.push(runResult) 222 } 223 224 // in case **all** tests are missing, then it should exit with error code 225 if (res.every(Boolean)) { 226 throw new Error(`Missing script: ${args[0]}`) 227 } 228 } 229 230 async listWorkspaces (args, filters) { 231 await this.setWorkspaces() 232 233 if (this.npm.silent) { 234 return 235 } 236 237 if (this.npm.config.get('json')) { 238 const res = {} 239 for (const workspacePath of this.workspacePaths) { 240 const { content: { scripts, name } } = await pkgJson.normalize(workspacePath) 241 res[name] = { ...scripts } 242 } 243 this.npm.output(JSON.stringify(res, null, 2)) 244 return 245 } 246 247 if (this.npm.config.get('parseable')) { 248 for (const workspacePath of this.workspacePaths) { 249 const { content: { scripts, name } } = await pkgJson.normalize(workspacePath) 250 for (const [script, cmd] of Object.entries(scripts || {})) { 251 this.npm.output(`${name}:${script}:${cmd}`) 252 } 253 } 254 return 255 } 256 257 for (const workspacePath of this.workspacePaths) { 258 await this.list(args, workspacePath) 259 } 260 } 261} 262 263module.exports = RunScript 264