-
-
Notifications
You must be signed in to change notification settings - Fork 422
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Run linters with configurable concurrency (#149)
If the amount of staged files is big, passing them as arguments can exceed a max string limit (on Windows it's `8191`). This PR addresses it by splitting work into chunks based on this limit and running linters in parallel. Closes #147
- Loading branch information
1 parent
c7283b7
commit 79ad8b3
Showing
14 changed files
with
341 additions
and
75 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,16 @@ | ||
{ | ||
"presets": ["es2015", "stage-0"] | ||
"presets": [ | ||
["env", { | ||
"targets": { | ||
"node": "current" | ||
} | ||
}] | ||
], | ||
"plugins": [ | ||
["transform-runtime", { | ||
"helpers": false, | ||
"polyfill": false, | ||
"regenerator": true | ||
}] | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
'use strict' | ||
|
||
/** | ||
* Calculates and returns the chunk size for given file paths and `chunkSize` | ||
* option. | ||
* | ||
* It returns the minimum of the following: | ||
* | ||
* - Total number of files | ||
* - Max allowed chunk size so that command length does not exceed the system | ||
* limitation on windows | ||
* - User specified chunk size or the default | ||
* | ||
* Worked example: | ||
* **Assumption** - Our max file path length is 100, Hence max allowed chunk | ||
* size is 80 | ||
* | ||
* - Case 1: Only 10 files are there, chunk size should be 10 only | ||
* - Case 2: There are 100 files and user has overridden the option with | ||
* chunk size 40. So chunk size should be 40 | ||
* - Case 3: There are 100 files and user has overridden the option with | ||
* chunk size 100. So chunk size should be 80 | ||
* | ||
* @param {Array<string>} paths The array of file paths | ||
* @param {number} idealChunkSize User specified / default chunk size | ||
* @returns {number} The chunk size | ||
*/ | ||
module.exports = function calcChunkSize(paths, idealChunkSize) { | ||
/* What is the longest file path? */ | ||
const maxPathLen = paths.reduce( | ||
(maxLen, filePath) => Math.max(maxLen, filePath.length), | ||
20 // safe initial value | ||
) | ||
|
||
/* In the worst case scenario, */ | ||
/* how many files can we process in a single command? */ | ||
/* For windows systems, command length is limited to 8192 */ | ||
const maxAllowedChunkSize = Math.floor(8000 / maxPathLen) | ||
|
||
/* Configured chunk size / default - idealChunkSize */ | ||
return Math.min(paths.length, maxAllowedChunkSize, idealChunkSize) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
'use strict' | ||
|
||
/** | ||
* Helper function for reading config option. | ||
* Returns the `config` option for given `key` or the given `defaultValue` | ||
* if `config` does not have the given `key`. | ||
* | ||
* @param {Object} config | ||
* @param {string} key | ||
* @param {*} defaultValue | ||
* @returns {*} | ||
*/ | ||
module.exports = function readConfigOption(config, key, defaultValue) { | ||
if (typeof config !== 'undefined' && typeof config[key] !== 'undefined') { | ||
return config[key] | ||
} | ||
|
||
return defaultValue | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,33 +1,70 @@ | ||
'use strict' | ||
|
||
const findBin = require('./findBin') | ||
const chunk = require('lodash.chunk') | ||
const execa = require('execa') | ||
const pMap = require('p-map') | ||
|
||
const calcChunkSize = require('./calcChunkSize') | ||
const findBin = require('./findBin') | ||
const readConfigOption = require('./readConfigOption') | ||
|
||
module.exports = function runScript(commands, pathsToLint, packageJson, options) { | ||
const config = readConfigOption(options, 'config', {}) | ||
|
||
const concurrency = readConfigOption(config, 'subTaskConcurrency', 2) | ||
const chunkSize = calcChunkSize( | ||
pathsToLint, | ||
readConfigOption(config, 'chunkSize', Number.MAX_SAFE_INTEGER) | ||
) | ||
|
||
const filePathChunks = chunk(pathsToLint, chunkSize) | ||
|
||
const lintersArray = Array.isArray(commands) ? commands : [commands] | ||
|
||
return lintersArray.map(linter => ({ | ||
title: linter, | ||
task: () => { | ||
try { | ||
const res = findBin(linter, pathsToLint, packageJson, options) | ||
const res = findBin(linter, packageJson, options) | ||
|
||
// Only use gitDir as CWD if we are using the git binary | ||
// e.g `npm` should run tasks in the actual CWD | ||
const execaOptions = | ||
res.bin.endsWith('git') && options && options.gitDir ? { cwd: options.gitDir } : {} | ||
return new Promise((resolve, reject) => { | ||
execa(res.bin, res.args, execaOptions) | ||
.then(() => { | ||
resolve(`✅ ${ linter } passed!`) | ||
}) | ||
res.bin.endsWith('git') && options && options.gitDir | ||
? { cwd: options.gitDir } : {} | ||
|
||
const errors = [] | ||
const mapper = (pathsChunk) => { | ||
const args = res.args.concat(['--'], pathsChunk) | ||
|
||
return execa(res.bin, args, Object.assign({}, execaOptions)) | ||
/* If we don't catch, pMap will terminate on first rejection */ | ||
/* We want error information of all chunks */ | ||
.catch((err) => { | ||
reject(new Error(`🚫 ${ linter } found some errors. Please fix them and try committing again. | ||
${ err.stderr } | ||
${ err.stdout }`)) | ||
errors.push(err) | ||
}) | ||
}) | ||
} | ||
|
||
return pMap(filePathChunks, mapper, { concurrency }) | ||
.catch((err) => { | ||
/* This will probably never be called. But just in case.. */ | ||
throw new Error(`🚫 ${ linter } got an unexpected error. | ||
${ err.message }`) | ||
}) | ||
.then(() => { | ||
if (errors.length === 0) return `✅ ${ linter } passed!` | ||
|
||
const errStdout = errors.map(err => err.stdout).join('') | ||
const errStderr = errors.map(err => err.stderr).join('') | ||
|
||
throw new Error(`🚫 ${ linter } found some errors. Please fix them and try committing again. | ||
${ errStdout } | ||
${ errStderr }`) | ||
}) | ||
} catch (err) { | ||
throw err | ||
} | ||
} | ||
})) | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import calcChunkSize from '../src/calcChunkSize' | ||
|
||
// This is only ever used for length so the contents do not matter much | ||
const testFilePath = | ||
'This-is-only-ever-used-for-length-so-the-contents-do-not-matter-much.length-is-100-for-simplicity.js' | ||
|
||
describe('calcChunkSize', () => { | ||
it('should not return high chunk size for less files', () => { | ||
let chunkSize = calcChunkSize([testFilePath], 50) | ||
expect(chunkSize).toEqual(1) | ||
|
||
chunkSize = calcChunkSize([testFilePath, testFilePath], 50) | ||
expect(chunkSize).toEqual(2) | ||
}) | ||
|
||
it('should not return chunk size which will fail max command length', () => { | ||
const fakeFilePaths = Array(200).fill(testFilePath) | ||
const chunkSize = calcChunkSize(fakeFilePaths, Number.MAX_SAFE_INTEGER) | ||
expect(chunkSize).toEqual(80) | ||
}) | ||
|
||
it('should respect option chunkSize where ever possible', () => { | ||
const fakeFilePaths = Array(200).fill(testFilePath) | ||
const chunkSize = calcChunkSize(fakeFilePaths, 50) | ||
expect(chunkSize).toEqual(50) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import readConfigOption from '../src/readConfigOption' | ||
|
||
describe('readConfigOption', () => { | ||
|
||
it('should return default value if config is undefined', () => { | ||
const configOption = readConfigOption(undefined, 'my_key', 'default_value') | ||
expect(configOption).toEqual('default_value') | ||
}) | ||
|
||
it('should return default value if config option is undefined', () => { | ||
const configOption = readConfigOption({}, 'my_key', 'default_value') | ||
expect(configOption).toEqual('default_value') | ||
}) | ||
|
||
it('should return config option if not undefined', () => { | ||
const configOption = readConfigOption({ my_key: 'my_value' }, 'my_key', 'default_value') | ||
expect(configOption).toEqual('my_value') | ||
}) | ||
|
||
}) |
Oops, something went wrong.