/
resolveTaskFn.js
155 lines (135 loc) Β· 4.46 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
import { redBright, dim } from 'colorette'
import execa from 'execa'
import debug from 'debug'
import { parseArgsStringToArgv } from 'string-argv'
import pidTree from 'pidtree'
import { error, info } from './figures.js'
import { getInitialState } from './state.js'
import { TaskError } from './symbols.js'
const ERROR_CHECK_INTERVAL = 200
const debugLog = debug('lint-staged:resolveTaskFn')
const getTag = ({ code, killed, signal }) => (killed && 'KILLED') || signal || code || 'FAILED'
/**
* Handle task console output.
*
* @param {string} command
* @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} ctx
* @returns {Error}
*/
const handleOutput = (command, result, ctx, isError = false) => {
const { stderr, stdout } = result
const hasOutput = !!stderr || !!stdout
if (hasOutput) {
const outputTitle = isError ? redBright(`${error} ${command}:`) : `${info} ${command}:`
const output = []
.concat(ctx.quiet ? [] : ['', outputTitle])
.concat(stderr ? stderr : [])
.concat(stdout ? stdout : [])
ctx.output.push(output.join('\n'))
} else if (isError) {
// Show generic error when task had no output
const tag = getTag(result)
const message = redBright(`\n${error} ${command} failed without output (${tag}).`)
if (!ctx.quiet) ctx.output.push(message)
}
}
/**
* Interrupts the execution of the execa process that we spawned if
* another task adds an error to the context.
*
* @param {Object} ctx
* @param {execa.ExecaChildProcess<string>} execaChildProcess
* @returns {function(): void} Function that clears the interval that
* checks the context.
*/
const interruptExecutionOnError = (ctx, execaChildProcess) => {
let loopIntervalId
async function loop() {
if (ctx.errors.size > 0) {
clearInterval(loopIntervalId)
const ids = await pidTree(execaChildProcess.pid)
ids.forEach((id) => process.kill(id))
// The execa process is killed separately in order
// to get the `KILLED` status.
execaChildProcess.kill()
}
}
loopIntervalId = setInterval(loop, ERROR_CHECK_INTERVAL)
return () => {
clearInterval(loopIntervalId)
}
}
/**
* Create a error output dependding on process result.
*
* @param {string} command
* @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} ctx
* @returns {Error}
*/
const makeErr = (command, result, ctx) => {
ctx.errors.add(TaskError)
handleOutput(command, result, ctx, true)
const tag = getTag(result)
return new Error(`${redBright(command)} ${dim(`[${tag}]`)}`)
}
/**
* Returns the task function for the linter.
*
* @param {Object} options
* @param {string} options.command β Linter task
* @param {string} [options.cwd]
* @param {String} options.gitDir - Current git repo path
* @param {Boolean} options.isFn - Whether the linter task is a function
* @param {Array<string>} options.files β Filepaths to run the linter task against
* @param {Boolean} [options.shell] β Whether to skip parsing linter task for better shell support
* @param {Boolean} [options.verbose] β Always show task verbose
* @returns {function(): Promise<Array<string>>}
*/
export const resolveTaskFn = ({
command,
cwd = process.cwd(),
files,
gitDir,
isFn,
shell = false,
verbose = false,
}) => {
const [cmd, ...args] = parseArgsStringToArgv(command)
debugLog('cmd:', cmd)
debugLog('args:', args)
const execaOptions = {
// Only use gitDir as CWD if we are using the git binary
// e.g `npm` should run tasks in the actual CWD
cwd: /^git(\.exe)?/i.test(cmd) ? gitDir : cwd,
preferLocal: true,
reject: false,
shell,
}
debugLog('execaOptions:', execaOptions)
return async (ctx = getInitialState()) => {
const execaChildProcess = shell
? execa.command(isFn ? command : `${command} ${files.join(' ')}`, execaOptions)
: execa(cmd, isFn ? args : args.concat(files), execaOptions)
const quitInterruptCheck = interruptExecutionOnError(ctx, execaChildProcess)
const result = await execaChildProcess
quitInterruptCheck()
if (result.failed || result.killed || result.signal != null) {
throw makeErr(command, result, ctx)
}
if (verbose) {
handleOutput(command, result, ctx)
}
}
}