forked from lint-staged/lint-staged
/
resolveTaskFn.js
139 lines (123 loc) Β· 4.08 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
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'
export const FAIL_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)
}
}
/**
* 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()) => {
let isExecutionDone = false
const execaChildProcess = shell
? execa.command(isFn ? command : `${command} ${files.join(' ')}`, execaOptions)
: execa(cmd, isFn ? args : args.concat(files), execaOptions)
const interruptExecutionOnError = async () => {
if (!isExecutionDone) {
if (ctx.errors.size > 0) {
const ids = await pidTree(execaChildProcess.pid)
ids.forEach(process.kill)
// The execa process is killed separately in order
// to get the `KILLED` status.
execaChildProcess.kill()
} else {
setTimeout(interruptExecutionOnError, FAIL_CHECK_INTERVAL)
}
}
}
interruptExecutionOnError()
const result = await execaChildProcess
isExecutionDone = true
if (result.failed || result.killed || result.signal != null) {
throw makeErr(command, result, ctx)
}
if (verbose) {
handleOutput(command, result, ctx)
}
}
}