UNPKG

4.85 kBJavaScriptView Raw
1'use strict'
2
3const chunk = require('lodash/chunk')
4const dedent = require('dedent')
5const isWindows = require('is-windows')
6const execa = require('execa')
7const chalk = require('chalk')
8const symbols = require('log-symbols')
9const pMap = require('p-map')
10const calcChunkSize = require('./calcChunkSize')
11const findBin = require('./findBin')
12
13const debug = require('debug')('lint-staged:task')
14
15/**
16 * Execute the given linter binary with arguments and file paths using execa and
17 * return the promise.
18 *
19 * @param {string} bin
20 * @param {Array<string>} args
21 * @param {Object} execaOptions
22 * @param {Array<string>} pathsToLint
23 * @return {Promise} child_process
24 */
25function execLinter(bin, args, execaOptions, pathsToLint) {
26 const binArgs = args.concat(pathsToLint)
27
28 debug('bin:', bin)
29 debug('args: %O', binArgs)
30 debug('opts: %o', execaOptions)
31
32 return execa(bin, binArgs, { ...execaOptions })
33}
34
35const successMsg = linter => `${symbols.success} ${linter} passed!`
36
37/**
38 * Create and returns an error instance with a given message.
39 * If we set the message on the error instance, it gets logged multiple times(see #142).
40 * So we set the actual error message in a private field and extract it later,
41 * log only once.
42 *
43 * @param {string} message
44 * @returns {Error}
45 */
46function throwError(message) {
47 const err = new Error()
48 err.privateMsg = `\n\n\n${message}`
49 return err
50}
51
52/**
53 * Create a failure message dependding on process result.
54 *
55 * @param {string} linter
56 * @param {Object} result
57 * @param {string} result.stdout
58 * @param {string} result.stderr
59 * @param {boolean} result.failed
60 * @param {boolean} result.killed
61 * @param {string} result.signal
62 * @param {Object} context (see https://github.com/SamVerschueren/listr#context)
63 * @returns {Error}
64 */
65function makeErr(linter, result, context = {}) {
66 // Indicate that some linter will fail so we don't update the index with formatting changes
67 context.hasErrors = true // eslint-disable-line no-param-reassign
68 const { stdout, stderr, killed, signal } = result
69 if (killed || (signal && signal !== '')) {
70 return throwError(
71 `${symbols.warning} ${chalk.yellow(`${linter} was terminated with ${signal}`)}`
72 )
73 }
74 return throwError(dedent`${symbols.error} ${chalk.redBright(
75 `${linter} found some errors. Please fix them and try committing again.`
76 )}
77 ${stdout}
78 ${stderr}
79 `)
80}
81
82/**
83 * Returns the task function for the linter. It handles chunking for file paths
84 * if the OS is Windows.
85 *
86 * @param {Object} options
87 * @param {string} options.linter
88 * @param {string} options.gitDir
89 * @param {Array<string>} options.pathsToLint
90 * @param {number} options.chunkSize
91 * @param {number} options.subTaskConcurrency
92 * @returns {function(): Promise<string>}
93 */
94module.exports = function resolveTaskFn(options) {
95 const { linter, gitDir, pathsToLint } = options
96 const { bin, args } = findBin(linter)
97
98 const execaOptions = { reject: false }
99 // Only use gitDir as CWD if we are using the git binary
100 // e.g `npm` should run tasks in the actual CWD
101 if (/git(\.exe)?$/i.test(bin) && gitDir !== process.cwd()) {
102 execaOptions.cwd = gitDir
103 }
104
105 if (!isWindows()) {
106 debug('%s OS: %s; File path chunking unnecessary', symbols.success, process.platform)
107 return ctx =>
108 execLinter(bin, args, execaOptions, pathsToLint).then(result => {
109 if (result.failed || result.killed || result.signal != null) {
110 throw makeErr(linter, result, ctx)
111 }
112
113 return successMsg(linter)
114 })
115 }
116
117 const { chunkSize, subTaskConcurrency: concurrency } = options
118
119 const filePathChunks = chunk(pathsToLint, calcChunkSize(pathsToLint, chunkSize))
120 const mapper = execLinter.bind(null, bin, args, execaOptions)
121
122 debug(
123 'OS: %s; Creating linter task with %d chunked file paths',
124 process.platform,
125 filePathChunks.length
126 )
127 return ctx =>
128 pMap(filePathChunks, mapper, { concurrency })
129 .catch(err => {
130 /* This will probably never be called. But just in case.. */
131 throw new Error(dedent`
132 ${symbols.error} ${linter} got an unexpected error.
133 ${err.message}
134 `)
135 })
136 .then(results => {
137 const errors = results.filter(res => res.failed || res.killed)
138 const failed = results.some(res => res.failed)
139 const killed = results.some(res => res.killed)
140 const signals = results.map(res => res.signal).filter(Boolean)
141
142 if (failed || killed || signals.length > 0) {
143 const finalResult = {
144 stdout: errors.map(err => err.stdout).join(''),
145 stderr: errors.map(err => err.stderr).join(''),
146 failed,
147 killed,
148 signal: signals.join(', ')
149 }
150
151 throw makeErr(linter, finalResult, ctx)
152 }
153
154 return successMsg(linter)
155 })
156}