UNPKG

4.5 kBJavaScriptView Raw
1import { redBright, dim } from 'colorette'
2import execa 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 async function loop() {
60 if (ctx.errors.size > 0) {
61 const ids = await pidTree(execaChildProcess.pid)
62 ids.forEach(process.kill)
63
64 // The execa process is killed separately in order
65 // to get the `KILLED` status.
66 execaChildProcess.kill()
67 }
68 }
69
70 const loopIntervalId = setInterval(loop, ERROR_CHECK_INTERVAL)
71
72 return () => {
73 clearInterval(loopIntervalId)
74 }
75}
76
77/**
78 * Create a error output dependding on process result.
79 *
80 * @param {string} command
81 * @param {Object} result
82 * @param {string} result.stdout
83 * @param {string} result.stderr
84 * @param {boolean} result.failed
85 * @param {boolean} result.killed
86 * @param {string} result.signal
87 * @param {Object} ctx
88 * @returns {Error}
89 */
90const makeErr = (command, result, ctx) => {
91 ctx.errors.add(TaskError)
92 handleOutput(command, result, ctx, true)
93 const tag = getTag(result)
94 return new Error(`${redBright(command)} ${dim(`[${tag}]`)}`)
95}
96
97/**
98 * Returns the task function for the linter.
99 *
100 * @param {Object} options
101 * @param {string} options.command — Linter task
102 * @param {string} [options.cwd]
103 * @param {String} options.gitDir - Current git repo path
104 * @param {Boolean} options.isFn - Whether the linter task is a function
105 * @param {Array<string>} options.files — Filepaths to run the linter task against
106 * @param {Boolean} [options.shell] — Whether to skip parsing linter task for better shell support
107 * @param {Boolean} [options.verbose] — Always show task verbose
108 * @returns {function(): Promise<Array<string>>}
109 */
110export const resolveTaskFn = ({
111 command,
112 cwd = process.cwd(),
113 files,
114 gitDir,
115 isFn,
116 shell = false,
117 verbose = false,
118}) => {
119 const [cmd, ...args] = parseArgsStringToArgv(command)
120 debugLog('cmd:', cmd)
121 debugLog('args:', args)
122
123 const execaOptions = {
124 // Only use gitDir as CWD if we are using the git binary
125 // e.g `npm` should run tasks in the actual CWD
126 cwd: /^git(\.exe)?/i.test(cmd) ? gitDir : cwd,
127 preferLocal: true,
128 reject: false,
129 shell,
130 }
131
132 debugLog('execaOptions:', execaOptions)
133
134 return async (ctx = getInitialState()) => {
135 const execaChildProcess = shell
136 ? execa.command(isFn ? command : `${command} ${files.join(' ')}`, execaOptions)
137 : execa(cmd, isFn ? args : args.concat(files), execaOptions)
138
139 const quitInterruptCheck = interruptExecutionOnError(ctx, execaChildProcess)
140 const result = await execaChildProcess
141 quitInterruptCheck()
142
143 if (result.failed || result.killed || result.signal != null) {
144 throw makeErr(command, result, ctx)
145 }
146
147 if (verbose) {
148 handleOutput(command, result, ctx)
149 }
150 }
151}