Skip to content

Commit

Permalink
feat: support async function tasks
Browse files Browse the repository at this point in the history
  • Loading branch information
iiroj committed Dec 18, 2019
1 parent f2a2702 commit 20d5c5d
Show file tree
Hide file tree
Showing 4 changed files with 39 additions and 29 deletions.
10 changes: 5 additions & 5 deletions README.md
Expand Up @@ -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<string | string[]>
```
### Example: Wrap filenames in single quotes and run once per file
Expand Down Expand Up @@ -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(' ')}`
}
}
```
Expand All @@ -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(' ')}`
}
}
```
Expand Down
32 changes: 17 additions & 15 deletions lib/makeCmdTasks.js
Expand Up @@ -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
}
12 changes: 7 additions & 5 deletions lib/runAll.js
Expand Up @@ -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,
Expand All @@ -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, {
Expand All @@ -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
Expand Down
14 changes: 10 additions & 4 deletions test/makeCmdTasks.spec.js
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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')
})
})

0 comments on commit 20d5c5d

Please sign in to comment.