UNPKG

3.51 kBJavaScriptView Raw
1import { redBright, dim } from 'colorette'
2import execa from 'execa'
3import debug from 'debug'
4import { parseArgsStringToArgv } from 'string-argv'
5
6import { error, info } from './figures.js'
7import { getInitialState } from './state.js'
8import { TaskError } from './symbols.js'
9
10const debugLog = debug('lint-staged:resolveTaskFn')
11
12const getTag = ({ code, killed, signal }) => signal || (killed && 'KILLED') || code || 'FAILED'
13
14/**
15 * Handle task console output.
16 *
17 * @param {string} command
18 * @param {Object} result
19 * @param {string} result.stdout
20 * @param {string} result.stderr
21 * @param {boolean} result.failed
22 * @param {boolean} result.killed
23 * @param {string} result.signal
24 * @param {Object} ctx
25 * @returns {Error}
26 */
27const handleOutput = (command, result, ctx, isError = false) => {
28 const { stderr, stdout } = result
29 const hasOutput = !!stderr || !!stdout
30
31 if (hasOutput) {
32 const outputTitle = isError ? redBright(`${error} ${command}:`) : `${info} ${command}:`
33 const output = []
34 .concat(ctx.quiet ? [] : ['', outputTitle])
35 .concat(stderr ? stderr : [])
36 .concat(stdout ? stdout : [])
37 ctx.output.push(output.join('\n'))
38 } else if (isError) {
39 // Show generic error when task had no output
40 const tag = getTag(result)
41 const message = redBright(`\n${error} ${command} failed without output (${tag}).`)
42 if (!ctx.quiet) ctx.output.push(message)
43 }
44}
45
46/**
47 * Create a error output dependding on process result.
48 *
49 * @param {string} command
50 * @param {Object} result
51 * @param {string} result.stdout
52 * @param {string} result.stderr
53 * @param {boolean} result.failed
54 * @param {boolean} result.killed
55 * @param {string} result.signal
56 * @param {Object} ctx
57 * @returns {Error}
58 */
59const makeErr = (command, result, ctx) => {
60 ctx.errors.add(TaskError)
61 handleOutput(command, result, ctx, true)
62 const tag = getTag(result)
63 return new Error(`${redBright(command)} ${dim(`[${tag}]`)}`)
64}
65
66/**
67 * Returns the task function for the linter.
68 *
69 * @param {Object} options
70 * @param {string} options.command — Linter task
71 * @param {string} [options.cwd]
72 * @param {String} options.gitDir - Current git repo path
73 * @param {Boolean} options.isFn - Whether the linter task is a function
74 * @param {Array<string>} options.files — Filepaths to run the linter task against
75 * @param {Boolean} [options.shell] — Whether to skip parsing linter task for better shell support
76 * @param {Boolean} [options.verbose] — Always show task verbose
77 * @returns {function(): Promise<Array<string>>}
78 */
79export const resolveTaskFn = ({
80 command,
81 cwd = process.cwd(),
82 files,
83 gitDir,
84 isFn,
85 shell = false,
86 verbose = false,
87}) => {
88 const [cmd, ...args] = parseArgsStringToArgv(command)
89 debugLog('cmd:', cmd)
90 debugLog('args:', args)
91
92 const execaOptions = {
93 // Only use gitDir as CWD if we are using the git binary
94 // e.g `npm` should run tasks in the actual CWD
95 cwd: /^git(\.exe)?/i.test(cmd) ? gitDir : cwd,
96 preferLocal: true,
97 reject: false,
98 shell,
99 }
100
101 debugLog('execaOptions:', execaOptions)
102
103 return async (ctx = getInitialState()) => {
104 const result = await (shell
105 ? execa.command(isFn ? command : `${command} ${files.join(' ')}`, execaOptions)
106 : execa(cmd, isFn ? args : args.concat(files), execaOptions))
107
108 if (result.failed || result.killed || result.signal != null) {
109 throw makeErr(command, result, ctx)
110 }
111
112 if (verbose) {
113 handleOutput(command, result, ctx)
114 }
115 }
116}