UNPKG

3.57 kBJavaScriptView Raw
1'use strict'
2
3const chalk = require('chalk')
4const dedent = require('dedent')
5const execa = require('execa')
6const symbols = require('log-symbols')
7const stringArgv = require('string-argv')
8
9const debug = require('debug')('lint-staged:task')
10
11/**
12 * Execute the given linter cmd using execa and
13 * return the promise.
14 *
15 * @param {string} cmd
16 * @param {Array<string>} args
17 * @param {Object} execaOptions
18 * @return {Promise} child_process
19 */
20const execLinter = (cmd, args, execaOptions) => {
21 debug('cmd:', cmd)
22 if (args) debug('args:', args)
23 debug('execaOptions:', execaOptions)
24
25 return args ? execa(cmd, args, execaOptions) : execa(cmd, execaOptions)
26}
27
28const successMsg = linter => `${symbols.success} ${linter} passed!`
29
30/**
31 * Create and returns an error instance with a given message.
32 * If we set the message on the error instance, it gets logged multiple times(see #142).
33 * So we set the actual error message in a private field and extract it later,
34 * log only once.
35 *
36 * @param {string} message
37 * @returns {Error}
38 */
39function throwError(message) {
40 const err = new Error()
41 err.privateMsg = `\n\n\n${message}`
42 return err
43}
44
45/**
46 * Create a failure message dependding on process result.
47 *
48 * @param {string} linter
49 * @param {Object} result
50 * @param {string} result.stdout
51 * @param {string} result.stderr
52 * @param {boolean} result.failed
53 * @param {boolean} result.killed
54 * @param {string} result.signal
55 * @param {Object} context (see https://github.com/SamVerschueren/listr#context)
56 * @returns {Error}
57 */
58function makeErr(linter, result, context = {}) {
59 // Indicate that some linter will fail so we don't update the index with formatting changes
60 context.hasErrors = true // eslint-disable-line no-param-reassign
61 const { stdout, stderr, killed, signal } = result
62 if (killed || (signal && signal !== '')) {
63 return throwError(
64 `${symbols.warning} ${chalk.yellow(`${linter} was terminated with ${signal}`)}`
65 )
66 }
67 return throwError(dedent`${symbols.error} ${chalk.redBright(
68 `${linter} found some errors. Please fix them and try committing again.`
69 )}
70 ${stdout}
71 ${stderr}
72 `)
73}
74
75/**
76 * Returns the task function for the linter. It handles chunking for file paths
77 * if the OS is Windows.
78 *
79 * @param {Object} options
80 * @param {string} options.gitDir
81 * @param {Boolean} options.isFn
82 * @param {string} options.linter
83 * @param {Array<string>} options.pathsToLint
84 * @param {Boolean} [options.shell]
85 * @returns {function(): Promise<Array<string>>}
86 */
87module.exports = function resolveTaskFn({ gitDir, isFn, linter, pathsToLint, shell = false }) {
88 const execaOptions = { preferLocal: true, reject: false, shell }
89
90 let cmd
91 let args
92
93 if (shell) {
94 execaOptions.shell = true
95 // If `shell`, passed command shouldn't be parsed
96 // If `linter` is a function, command already includes `pathsToLint`.
97 cmd = isFn ? linter : `${linter} ${pathsToLint.join(' ')}`
98 } else {
99 const [parsedCmd, ...parsedArgs] = stringArgv.parseArgsStringToArgv(linter)
100 cmd = parsedCmd
101 args = isFn ? parsedArgs : parsedArgs.concat(pathsToLint)
102 }
103
104 // Only use gitDir as CWD if we are using the git binary
105 // e.g `npm` should run tasks in the actual CWD
106 if (/^git(\.exe)?/i.test(linter) && gitDir !== process.cwd()) {
107 execaOptions.cwd = gitDir
108 }
109
110 return ctx =>
111 execLinter(cmd, args, execaOptions).then(result => {
112 if (result.failed || result.killed || result.signal != null) {
113 throw makeErr(linter, result, ctx)
114 }
115
116 return successMsg(linter)
117 })
118}