'use strict' const chunk = require('lodash/chunk') const dedent = require('dedent') const isWindows = require('is-windows') const execa = require('execa') const chalk = require('chalk') const symbols = require('log-symbols') const pMap = require('p-map') const calcChunkSize = require('./calcChunkSize') const findBin = require('./findBin') const debug = require('debug')('lint-staged:task') /** * Execute the given linter binary with arguments and file paths using execa and * return the promise. * * @param {string} bin * @param {Array} args * @param {Object} execaOptions * @param {Array} pathsToLint * @return {Promise} child_process */ function execLinter(bin, args, execaOptions, pathsToLint) { const binArgs = args.concat(pathsToLint) debug('bin:', bin) debug('args: %O', binArgs) debug('opts: %o', execaOptions) return execa(bin, binArgs, { ...execaOptions }) } const successMsg = linter => `${symbols.success} ${linter} passed!` /** * Create and returns an error instance with a given message. * If we set the message on the error instance, it gets logged multiple times(see #142). * So we set the actual error message in a private field and extract it later, * log only once. * * @param {string} message * @returns {Error} */ function throwError(message) { const err = new Error() err.privateMsg = `\n\n\n${message}` return err } /** * Create a failure message dependding on process result. * * @param {string} linter * @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} context (see https://github.com/SamVerschueren/listr#context) * @returns {Error} */ function makeErr(linter, result, context = {}) { // Indicate that some linter will fail so we don't update the index with formatting changes context.hasErrors = true // eslint-disable-line no-param-reassign const { stdout, stderr, killed, signal } = result if (killed || (signal && signal !== '')) { return throwError( `${symbols.warning} ${chalk.yellow(`${linter} was terminated with ${signal}`)}` ) } return throwError(dedent`${symbols.error} ${chalk.redBright( `${linter} found some errors. Please fix them and try committing again.` )} ${stdout} ${stderr} `) } /** * Returns the task function for the linter. It handles chunking for file paths * if the OS is Windows. * * @param {Object} options * @param {string} options.linter * @param {string} options.gitDir * @param {Array} options.pathsToLint * @param {number} options.chunkSize * @param {number} options.subTaskConcurrency * @returns {function(): Promise} */ module.exports = function resolveTaskFn(options) { const { linter, gitDir, pathsToLint } = options const { bin, args } = findBin(linter) const execaOptions = { reject: false } // Only use gitDir as CWD if we are using the git binary // e.g `npm` should run tasks in the actual CWD if (/git(\.exe)?$/i.test(bin) && gitDir !== process.cwd()) { execaOptions.cwd = gitDir } if (!isWindows()) { debug('%s OS: %s; File path chunking unnecessary', symbols.success, process.platform) return ctx => execLinter(bin, args, execaOptions, pathsToLint).then(result => { if (result.failed || result.killed || result.signal != null) { throw makeErr(linter, result, ctx) } return successMsg(linter) }) } const { chunkSize, subTaskConcurrency: concurrency } = options const filePathChunks = chunk(pathsToLint, calcChunkSize(pathsToLint, chunkSize)) const mapper = execLinter.bind(null, bin, args, execaOptions) debug( 'OS: %s; Creating linter task with %d chunked file paths', process.platform, filePathChunks.length ) return ctx => pMap(filePathChunks, mapper, { concurrency }) .catch(err => { /* This will probably never be called. But just in case.. */ throw new Error(dedent` ${symbols.error} ${linter} got an unexpected error. ${err.message} `) }) .then(results => { const errors = results.filter(res => res.failed || res.killed) const failed = results.some(res => res.failed) const killed = results.some(res => res.killed) const signals = results.map(res => res.signal).filter(Boolean) if (failed || killed || signals.length > 0) { const finalResult = { stdout: errors.map(err => err.stdout).join(''), stderr: errors.map(err => err.stderr).join(''), failed, killed, signal: signals.join(', ') } throw makeErr(linter, finalResult, ctx) } return successMsg(linter) }) }