• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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