From 6af83070c44003477c00d4c088806af23333ec59 Mon Sep 17 00:00:00 2001 From: jeffreyffs Date: Tue, 26 Nov 2019 21:09:17 -0800 Subject: [PATCH] feat: add support for concurrent CLI option --- README.md | 19 +++++---- bin/lint-staged | 8 +++- src/index.js | 13 ++++++- src/runAll.js | 14 +++++-- test/runAll.unmocked.spec.js | 75 ++++++++++++++++++++++++++++++++++++ 5 files changed, 116 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index b883bae43..786fbf3dc 100644 --- a/README.md +++ b/README.md @@ -70,13 +70,14 @@ $ npx lint-staged --help Usage: lint-staged [options] Options: - -V, --version output the version number - -c, --config [path] Path to configuration file - -r, --relative Pass relative filepaths to tasks - -x, --shell Skip parsing of tasks for better shell support - -q, --quiet Disable lint-staged’s own console output - -d, --debug Enable debug mode - -h, --help output usage information + -V, --version output the version number + -c, --config [path] Path to configuration file + -r, --relative Pass relative filepaths to tasks + -x, --shell Skip parsing of tasks for better shell support + -q, --quiet Disable lint-staged’s own console output + -d, --debug Enable debug mode + -p, --concurrent [parallel tasks] The number of tasks to run concurrently, or false to run tasks sequentially + -h, --help output usage information ``` - **`--config [path]`**: This can be used to manually specify the `lint-staged` config file location. However, if the specified file cannot be found, it will error out instead of performing the usual search. You may pass a npm package name for configuration also. @@ -86,6 +87,10 @@ Options: - **`--debug`**: Enabling the debug mode does the following: - `lint-staged` uses the [debug](https://github.com/visionmedia/debug) module internally to log information about staged files, commands being executed, location of binaries, etc. Debug logs, which are automatically enabled by passing the flag, can also be enabled by setting the environment variable `$DEBUG` to `lint-staged*`. - Use the [`verbose` renderer](https://github.com/SamVerschueren/listr-verbose-renderer) for `listr`. +- **`--concurrent [number | (true/false)]`**: Controls the concurrency of tasks being run by lint-staged. **NOTE**: This does NOT affect the concurrency of subtasks (they will always be run sequentially). Possible values are: + - `false`: Run all tasks serially + - `true` (default) : _Infinite_ concurrency. Runs as many tasks in parallel as possible. + - `{number}`: Run the specified number of tasks in parallel, where `1` is equivalent to `false`. ## Configuration diff --git a/bin/lint-staged b/bin/lint-staged index 2f26eee17..7bb885846 100755 --- a/bin/lint-staged +++ b/bin/lint-staged @@ -34,6 +34,11 @@ cmdline .option('-x, --shell', 'Skip parsing of tasks for better shell support') .option('-q, --quiet', 'Disable lint-staged’s own console output') .option('-d, --debug', 'Enable debug mode') + .option( + '-p, --concurrent ', + 'The number of tasks to run concurrently, or false to run tasks serially', + true + ) .parse(process.argv) if (cmdline.debug) { @@ -47,7 +52,8 @@ lintStaged({ relative: !!cmdline.relative, shell: !!cmdline.shell, quiet: !!cmdline.quiet, - debug: !!cmdline.debug + debug: !!cmdline.debug, + concurrent: cmdline.concurrent }) .then(passed => { process.exitCode = passed ? 0 : 1 diff --git a/src/index.js b/src/index.js index 15334dc23..eb83af5eb 100644 --- a/src/index.js +++ b/src/index.js @@ -48,12 +48,21 @@ function loadConfig(configPath) { * @param {boolean} [options.shell] - Skip parsing of tasks for better shell support * @param {boolean} [options.quiet] - Disable lint-staged’s own console output * @param {boolean} [options.debug] - Enable debug mode + * @param {boolean | number} [options.concurrent] - The number of tasks to run concurrently, or false to run tasks serially * @param {Logger} [logger] * * @returns {Promise} Promise of whether the linting passed or failed */ module.exports = function lintStaged( - { configPath, config, relative = false, shell = false, quiet = false, debug = false } = {}, + { + configPath, + config, + relative = false, + shell = false, + quiet = false, + debug = false, + concurrent = true + } = {}, logger = console ) { debugLog('Loading config using `cosmiconfig`') @@ -76,7 +85,7 @@ module.exports = function lintStaged( debugLog('lint-staged config:\n%O', config) } - return runAll({ config, relative, shell, quiet, debug }, logger) + return runAll({ config, relative, shell, quiet, debug, concurrent }, logger) .then(() => { debugLog('tasks were executed successfully!') return Promise.resolve(true) diff --git a/src/runAll.js b/src/runAll.js index 5f4d652f0..3e6607b7d 100644 --- a/src/runAll.js +++ b/src/runAll.js @@ -33,15 +33,23 @@ const MAX_ARG_LENGTH = * @param {boolean} [options.shell] - Skip parsing of tasks for better shell support * @param {boolean} [options.quiet] - Disable lint-staged’s own console output * @param {boolean} [options.debug] - Enable debug mode + * @param {boolean | number} [options.concurrent] - The number of tasks to run concurrently, or false to run tasks serially * @param {Logger} logger * @returns {Promise} */ module.exports = async function runAll( - { config, cwd = process.cwd(), debug = false, quiet = false, relative = false, shell = false }, + { + config, + cwd = process.cwd(), + debug = false, + quiet = false, + relative = false, + shell = false, + concurrent = true + }, logger = console ) { debugLog('Running all linter scripts') - const gitDir = await resolveGitDir({ cwd }) if (!gitDir) { @@ -120,7 +128,7 @@ https://github.com/okonet/lint-staged#using-js-functions-to-customize-linter-com }, { title: 'Running tasks...', - task: () => new Listr(tasks, { ...listrOptions, concurrent: true, exitOnError: false }) + task: () => new Listr(tasks, { ...listrOptions, concurrent, exitOnError: false }) }, { title: 'Updating stash...', diff --git a/test/runAll.unmocked.spec.js b/test/runAll.unmocked.spec.js index cacbd7dbd..baafe8c43 100644 --- a/test/runAll.unmocked.spec.js +++ b/test/runAll.unmocked.spec.js @@ -89,6 +89,81 @@ describe('runAll', () => { expect(await readFile('test.js')).toEqual(testJsFilePretty) }) + it('Should succeed when conflicting tasks sequentially edit a file', async () => { + await appendFile('test.js', testJsFileUgly) + + await fs.mkdir(cwd + '/files') + await appendFile('file.js', testJsFileUgly, cwd + '/files') + + await execGit(['add', 'test.js']) + await execGit(['add', 'files']) + + const success = await gitCommit({ + config: { + 'file.js': ['prettier --write', 'git add'], + 'test.js': files => { + // concurrent: false, means this should still work + fs.removeSync(`${cwd}/files`) + return [`prettier --write ${files.join(' ')}`, `git add ${files.join(' ')}`] + } + }, + concurrent: false + }) + + expect(success).toEqual(true) + }) + + it('Should fail when conflicting tasks concurrently edit a file', async () => { + await appendFile('test.js', testJsFileUgly) + await appendFile('test2.js', testJsFileUgly) + + await fs.mkdir(cwd + '/files') + await appendFile('file.js', testJsFileUgly, cwd + '/files') + + await execGit(['add', 'test.js']) + await execGit(['add', 'test2.js']) + await execGit(['add', 'files']) + + const success = await gitCommit({ + config: { + 'file.js': ['prettier --write', 'git add'], + 'test.js': ['prettier --write', 'git add'], + 'test2.js': files => { + // remove `files` so the 1st command should fail + fs.removeSync(`${cwd}/files`) + return [`prettier --write ${files.join(' ')}`, `git add ${files.join(' ')}`] + } + }, + concurrent: true + }) + + expect(success).toEqual(false) + }) + + it('Should succeed when conflicting tasks concurrently (max concurrency 1) edit a file', async () => { + await appendFile('test.js', testJsFileUgly) + + await fs.mkdir(cwd + '/files') + await appendFile('file.js', testJsFileUgly, cwd + '/files') + + await execGit(['add', 'test.js']) + await execGit(['add', 'files']) + + const success = await gitCommit({ + config: { + 'file.js': ['prettier --write', 'git add'], + 'test2.js': files => { + // concurrency of one should prevent save this operation + fs.removeSync(`${cwd}/files`) + return [`prettier --write ${files.join(' ')}`, `git add ${files.join(' ')}`] + } + }, + concurrent: 1 + }) + + expect(success).toEqual(true) + }) + it('Should commit entire staged file when no errors and linter modifies file', async () => { // Stage ugly file await appendFile('test.js', testJsFileUgly)