UNPKG

4.61 kBJavaScriptView Raw
1import { redBright, dim } from 'colorette'
2import { execa, execaCommand } from 'execa'
3import debug from 'debug'
4import { parseArgsStringToArgv } from 'string-argv'
5import pidTree from 'pidtree'
6
7import { error, info } from './figures.js'
8import { getInitialState } from './state.js'
9import { TaskError } from './symbols.js'
10
11const ERROR_CHECK_INTERVAL = 200
12
13const debugLog = debug('lint-staged:resolveTaskFn')
14
15const getTag = ({ code, killed, signal }) => (killed && 'KILLED') || signal || code || 'FAILED'
16
17/**
18 * Handle task console output.
19 *
20 * @param {string} command
21 * @param {Object} result
22 * @param {string} result.stdout
23 * @param {string} result.stderr
24 * @param {boolean} result.failed
25 * @param {boolean} result.killed
26 * @param {string} result.signal
27 * @param {Object} ctx
28 * @returns {Error}
29 */
30const handleOutput = (command, result, ctx, isError = false) => {
31 const { stderr, stdout } = result
32 const hasOutput = !!stderr || !!stdout
33
34 if (hasOutput) {
35 const outputTitle = isError ? redBright(`${error} ${command}:`) : `${info} ${command}:`
36 const output = []
37 .concat(ctx.quiet ? [] : ['', outputTitle])
38 .concat(stderr ? stderr : [])
39 .concat(stdout ? stdout : [])
40 ctx.output.push(output.join('\n'))
41 } else if (isError) {
42 // Show generic error when task had no output
43 const tag = getTag(result)
44 const message = redBright(`\n${error} ${command} failed without output (${tag}).`)
45 if (!ctx.quiet) ctx.output.push(message)
46 }
47}
48
49/**
50 * Interrupts the execution of the execa process that we spawned if
51 * another task adds an error to the context.
52 *
53 * @param {Object} ctx
54 * @param {execa.ExecaChildProcess<string>} execaChildProcess
55 * @returns {function(): void} Function that clears the interval that
56 * checks the context.
57 */
58const interruptExecutionOnError = (ctx, execaChildProcess) => {
59 let loopIntervalId
60
61 async function loop() {
62 if (ctx.errors.size > 0) {
63 clearInterval(loopIntervalId)
64
65 const childPids = await pidTree(execaChildProcess.pid)
66 for (const pid of childPids) {
67 process.kill(pid)
68 }
69
70 // The execa process is killed separately in order
71 // to get the `KILLED` status.
72 execaChildProcess.kill()
73 }
74 }
75
76 loopIntervalId = setInterval(loop, ERROR_CHECK_INTERVAL)
77
78 return () => {
79 clearInterval(loopIntervalId)
80 }
81}
82
83/**
84 * Create a error output dependding on process result.
85 *
86 * @param {string} command
87 * @param {Object} result
88 * @param {string} result.stdout
89 * @param {string} result.stderr
90 * @param {boolean} result.failed
91 * @param {boolean} result.killed
92 * @param {string} result.signal
93 * @param {Object} ctx
94 * @returns {Error}
95 */
96const makeErr = (command, result, ctx) => {
97 ctx.errors.add(TaskError)
98 handleOutput(command, result, ctx, true)
99 const tag = getTag(result)
100 return new Error(`${redBright(command)} ${dim(`[${tag}]`)}`)
101}
102
103/**
104 * Returns the task function for the linter.
105 *
106 * @param {Object} options
107 * @param {string} options.command — Linter task
108 * @param {string} [options.cwd]
109 * @param {String} options.gitDir - Current git repo path
110 * @param {Boolean} options.isFn - Whether the linter task is a function
111 * @param {Array<string>} options.files — Filepaths to run the linter task against
112 * @param {Boolean} [options.shell] — Whether to skip parsing linter task for better shell support
113 * @param {Boolean} [options.verbose] — Always show task verbose
114 * @returns {function(): Promise<Array<string>>}
115 */
116export const resolveTaskFn = ({
117 command,
118 cwd = process.cwd(),
119 files,
120 gitDir,
121 isFn,
122 shell = false,
123 verbose = false,
124}) => {
125 const [cmd, ...args] = parseArgsStringToArgv(command)
126 debugLog('cmd:', cmd)
127 debugLog('args:', args)
128
129 const execaOptions = {
130 // Only use gitDir as CWD if we are using the git binary
131 // e.g `npm` should run tasks in the actual CWD
132 cwd: /^git(\.exe)?/i.test(cmd) ? gitDir : cwd,
133 preferLocal: true,
134 reject: false,
135 shell,
136 }
137
138 debugLog('execaOptions:', execaOptions)
139
140 return async (ctx = getInitialState()) => {
141 const execaChildProcess = shell
142 ? execaCommand(isFn ? command : `${command} ${files.join(' ')}`, execaOptions)
143 : execa(cmd, isFn ? args : args.concat(files), execaOptions)
144
145 const quitInterruptCheck = interruptExecutionOnError(ctx, execaChildProcess)
146 const result = await execaChildProcess
147 quitInterruptCheck()
148
149 if (result.failed || result.killed || result.signal != null) {
150 throw makeErr(command, result, ctx)
151 }
152
153 if (verbose) {
154 handleOutput(command, result, ctx)
155 }
156 }
157}