/
resolveTaskFn.js
162 lines (142 loc) Β· 4.99 KB
/
resolveTaskFn.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
'use strict'
const chunk = require('lodash/chunk')
const dedent = require('dedent')
const isWindows = require('is-windows')
const execa = require('execa')
const chalk = require('chalk')
const symbols = require('log-symbols')
const pMap = require('p-map')
const calcChunkSize = require('./calcChunkSize')
const findBin = require('./findBin')
const debug = require('debug')('lint-staged:task')
/**
* Execute the given linter binary with arguments using execa and
* return the promise.
*
* @param {string} bin
* @param {Array<string>} args
* @param {Object} execaOptions
* @return {Promise} child_process
*/
function execLinter(bin, args, execaOptions) {
debug('bin:', bin)
debug('args: %O', args)
debug('opts: %o', execaOptions)
return execa(bin, args, { ...execaOptions })
}
const successMsg = linter => `${symbols.success} ${linter} passed!`
/**
* Create and returns an error instance with a given message.
* If we set the message on the error instance, it gets logged multiple times(see #142).
* So we set the actual error message in a private field and extract it later,
* log only once.
*
* @param {string} message
* @returns {Error}
*/
function throwError(message) {
const err = new Error()
err.privateMsg = `\n\n\n${message}`
return err
}
/**
* Create a failure message dependding on process result.
*
* @param {string} linter
* @param {Object} result
* @param {string} result.stdout
* @param {string} result.stderr
* @param {boolean} result.failed
* @param {boolean} result.killed
* @param {string} result.signal
* @param {Object} context (see https://github.com/SamVerschueren/listr#context)
* @returns {Error}
*/
function makeErr(linter, result, context = {}) {
// Indicate that some linter will fail so we don't update the index with formatting changes
context.hasErrors = true // eslint-disable-line no-param-reassign
const { stdout, stderr, killed, signal } = result
if (killed || (signal && signal !== '')) {
return throwError(
`${symbols.warning} ${chalk.yellow(`${linter} was terminated with ${signal}`)}`
)
}
return throwError(dedent`${symbols.error} ${chalk.redBright(
`${linter} found some errors. Please fix them and try committing again.`
)}
${stdout}
${stderr}
`)
}
/**
* Returns the task function for the linter. It handles chunking for file paths
* if the OS is Windows.
*
* @param {Object} options
* @param {string} options.linter
* @param {string} options.gitDir
* @param {Array<string>} options.pathsToLint
* @param {number} options.chunkSize
* @param {number} options.subTaskConcurrency
* @returns {function(): Promise<string>}
*/
module.exports = function resolveTaskFn(options) {
const { linter, gitDir, pathsToLint } = options
// If cmd` is a function, it should return a string when evaluated with `pathsToLint`.
// Else, it's a already a string
const cmdIsFn = typeof cmd === 'function'
const linterString = cmdIsFn ? linter(pathsToLint) : linter
const { bin, args } = findBin(linterString)
// If `cmd` is a function, args already include `pathsToLint`.
const argsWithPaths = cmdIsFn ? args : args.concat(pathsToLint)
const execaOptions = { reject: false }
// Only use gitDir as CWD if we are using the git binary
// e.g `npm` should run tasks in the actual CWD
if (/git(\.exe)?$/i.test(bin) && gitDir !== process.cwd()) {
execaOptions.cwd = gitDir
}
if (!isWindows()) {
debug('%s OS: %s; File path chunking unnecessary', symbols.success, process.platform)
return ctx =>
execLinter(bin, argsWithPaths, execaOptions).then(result => {
if (result.failed || result.killed || result.signal != null) {
throw makeErr(linter, result, ctx)
}
return successMsg(linter)
})
}
const { chunkSize, subTaskConcurrency: concurrency } = options
const filePathChunks = chunk(pathsToLint, calcChunkSize(pathsToLint, chunkSize))
const mapper = execLinter.bind(null, bin, argsWithPaths, execaOptions)
debug(
'OS: %s; Creating linter task with %d chunked file paths',
process.platform,
filePathChunks.length
)
return ctx =>
pMap(filePathChunks, mapper, { concurrency })
.catch(err => {
/* This will probably never be called. But just in case.. */
throw new Error(dedent`
${symbols.error} ${linter} got an unexpected error.
${err.message}
`)
})
.then(results => {
const errors = results.filter(res => res.failed || res.killed)
const failed = results.some(res => res.failed)
const killed = results.some(res => res.killed)
const signals = results.map(res => res.signal).filter(Boolean)
if (failed || killed || signals.length > 0) {
const finalResult = {
stdout: errors.map(err => err.stdout).join(''),
stderr: errors.map(err => err.stderr).join(''),
failed,
killed,
signal: signals.join(', ')
}
throw makeErr(linter, finalResult, ctx)
}
return successMsg(linter)
})
}