diff --git a/README.md b/README.md index 5f0bb1fde..82f1d653e 100644 --- a/README.md +++ b/README.md @@ -153,12 +153,12 @@ Pass arguments to your commands separated by space as you would do in the shell. Starting from [v2.0.0](https://github.com/okonet/lint-staged/releases/tag/2.0.0) sequences of commands are supported. Pass an array of commands instead of a single one and they will run sequentially. This is useful for running autoformatting tools like `eslint --fix` or `stylefmt` but can be used for any arbitrary sequences. -## Using JS functions to customize linter commands +## Using JS functions to customize tasks -When supplying configuration in JS format it is possible to define the linter command as a function which receives an array of staged filenames/paths and returns the complete linter command as a string. It is also possible to return an array of complete command strings, for example when the linter command supports only a single file input. +When supplying configuration in JS format it is possible to define the task as a function, which will receive an array of staged filenames/paths and should return the complete command as a string. It is also possible to return an array of complete command strings, for example when the task supports only a single file input. The function can be either sync or async. ```ts -type LinterFn = (filenames: string[]) => string | string[] +type TaskFn = (filenames: string[]) => string | string[] | Promise ``` ### Example: Wrap filenames in single quotes and run once per file @@ -196,7 +196,7 @@ const micromatch = require('micromatch') module.exports = { '*': allFiles => { const match = micromatch(allFiles, ['*.js', '*.ts']) - return `eslint ${match.join(" ")}` + return `eslint ${match.join(' ')}` } } ``` @@ -212,7 +212,7 @@ module.exports = { '*.js': files => { // from `files` filter those _NOT_ matching `*test.js` const match = micromatch.not(files, '*test.js') - return `eslint ${match.join(" ")}` + return `eslint ${match.join(' ')}` } } ``` diff --git a/lib/makeCmdTasks.js b/lib/makeCmdTasks.js index 6d1482906..703611973 100644 --- a/lib/makeCmdTasks.js +++ b/lib/makeCmdTasks.js @@ -13,41 +13,43 @@ const debug = require('debug')('lint-staged:make-cmd-tasks') * @param {string} options.gitDir * @param {Boolean} shell */ -module.exports = function makeCmdTasks({ commands, files, gitDir, shell }) { +module.exports = async function makeCmdTasks({ commands, files, gitDir, shell }) { debug('Creating listr tasks for commands %o', commands) const commandsArray = Array.isArray(commands) ? commands : [commands] + const cmdTasks = [] - return commandsArray.reduce((tasks, command) => { + for (const cmd of commandsArray) { // command function may return array of commands that already include `stagedFiles` - const isFn = typeof command === 'function' - const resolved = isFn ? command(files) : command - const commands = Array.isArray(resolved) ? resolved : [resolved] // Wrap non-array command as array + const isFn = typeof cmd === 'function' + const resolved = isFn ? await cmd(files) : cmd + + const resolvedArray = Array.isArray(resolved) ? resolved : [resolved] // Wrap non-array command as array // Function command should not be used as the task title as-is // because the resolved string it might be very long // Create a matching command array with [file] in place of file names - let mockCommands + let mockCmdTasks if (isFn) { const mockFileList = Array(files.length).fill('[file]') - const resolved = command(mockFileList) - mockCommands = Array.isArray(resolved) ? resolved : [resolved] + const resolved = await cmd(mockFileList) + mockCmdTasks = Array.isArray(resolved) ? resolved : [resolved] } - commands.forEach((command, i) => { + for (const [i, command] of resolvedArray.entries()) { let title = isFn ? '[Function]' : command - if (isFn && mockCommands[i]) { + if (isFn && mockCmdTasks[i]) { // If command is a function, use the matching mock command as title, // but since might include multiple [file] arguments, shorten to one - title = mockCommands[i].replace(/\[file\].*\[file\]/, '[file]') + title = mockCmdTasks[i].replace(/\[file\].*\[file\]/, '[file]') } - tasks.push({ + cmdTasks.push({ title, command, task: resolveTaskFn({ command, files, gitDir, isFn, shell }) }) - }) + } + } - return tasks - }, []) + return cmdTasks } diff --git a/lib/runAll.js b/lib/runAll.js index 8b930174f..4edf39324 100644 --- a/lib/runAll.js +++ b/lib/runAll.js @@ -81,8 +81,10 @@ module.exports = async function runAll( for (const [index, files] of stagedFileChunks.entries()) { const chunkTasks = generateTasks({ config, cwd, gitDir, files, relative }) - const chunkListrTasks = chunkTasks.map(task => { - const subTasks = makeCmdTasks({ + const chunkListrTasks = [] + + for (const task of chunkTasks) { + const subTasks = await makeCmdTasks({ commands: task.commands, files: task.fileList, gitDir, @@ -93,7 +95,7 @@ module.exports = async function runAll( hasDeprecatedGitAdd = true } - return { + chunkListrTasks.push({ title: `Running tasks for ${task.pattern}`, task: async () => new Listr(subTasks, { @@ -109,8 +111,8 @@ module.exports = async function runAll( } return false } - } - }) + }) + } listrTasks.push({ // No need to show number of task chunks when there's only one diff --git a/test/makeCmdTasks.spec.js b/test/makeCmdTasks.spec.js index 0c94e1d65..393f820b2 100644 --- a/test/makeCmdTasks.spec.js +++ b/test/makeCmdTasks.spec.js @@ -58,13 +58,13 @@ describe('makeCmdTasks', () => { }) }) - it('should work with function linter returning a string', async () => { + it('should work with function task returning a string', async () => { const res = await makeCmdTasks({ commands: () => 'test', gitDir, files: ['test.js'] }) expect(res.length).toBe(1) expect(res[0].title).toEqual('test') }) - it('should work with function linter returning array of string', async () => { + it('should work with function task returning array of string', async () => { const res = await makeCmdTasks({ commands: () => ['test', 'test2'], gitDir, @@ -75,7 +75,7 @@ describe('makeCmdTasks', () => { expect(res[1].title).toEqual('test2') }) - it('should work with function linter accepting arguments', async () => { + it('should work with function task accepting arguments', async () => { const res = await makeCmdTasks({ commands: filenames => filenames.map(file => `test ${file}`), gitDir, @@ -86,7 +86,7 @@ describe('makeCmdTasks', () => { expect(res[1].title).toEqual('test [file]') }) - it('should work with array of mixed string and function linters', async () => { + it('should work with array of mixed string and function tasks', async () => { const res = await makeCmdTasks({ commands: [() => 'test', 'test2', files => files.map(file => `test ${file}`)], gitDir, @@ -109,4 +109,10 @@ describe('makeCmdTasks', () => { expect(res.length).toBe(1) expect(res[0].title).toEqual('test --file [file]') }) + + it('should work with async function tasks', async () => { + const res = await makeCmdTasks({ commands: async () => 'test', gitDir, files: ['test.js'] }) + expect(res.length).toBe(1) + expect(res[0].title).toEqual('test') + }) })