run.js 6.3 KB

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