Skip to content

Commit 0dd0c94

Browse files
iirojokonet
authored andcommittedJul 3, 2019
fix: run all commands returned by function linters
Function linters have to be evaluated at makeCmdTasks, and resolveTaskFn has to run for each of the returned commands.
1 parent 146e6ce commit 0dd0c94

5 files changed

+92
-85
lines changed
 

‎src/makeCmdTasks.js

+16-22
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,6 @@ const resolveTaskFn = require('./resolveTaskFn')
44

55
const debug = require('debug')('lint-staged:make-cmd-tasks')
66

7-
/**
8-
* Get title for linter task. For a function, evaluate by passing single [file].
9-
* For strings, return as-is
10-
* @param {string|Function} linter
11-
*/
12-
const getTitle = linter => {
13-
if (typeof linter === 'function') {
14-
const resolved = linter(['[file]'])
15-
return Array.isArray(resolved) ? resolved[0] : resolved
16-
}
17-
return linter
18-
}
19-
207
/**
218
* Creates and returns an array of listr tasks which map to the given commands.
229
*
@@ -27,16 +14,23 @@ const getTitle = linter => {
2714
*/
2815
module.exports = async function makeCmdTasks(commands, shell, gitDir, pathsToLint) {
2916
debug('Creating listr tasks for commands %o', commands)
17+
const commandsArray = Array.isArray(commands) ? commands : [commands]
18+
19+
return commandsArray.reduce((tasks, command) => {
20+
// linter function may return array of commands that already include `pathsToLit`
21+
const isFn = typeof command === 'function'
22+
const resolved = isFn ? command(pathsToLint) : command
23+
const linters = Array.isArray(resolved) ? resolved : [resolved] // Wrap non-array linter as array
3024

31-
const lintersArray = Array.isArray(commands) ? commands : [commands]
25+
linters.forEach(linter => {
26+
const task = {
27+
title: linter,
28+
task: resolveTaskFn({ gitDir, isFn, linter, pathsToLint, shell })
29+
}
3230

33-
return lintersArray.map(linter => ({
34-
title: getTitle(linter),
35-
task: resolveTaskFn({
36-
linter,
37-
shell,
38-
gitDir,
39-
pathsToLint
31+
tasks.push(task)
4032
})
41-
}))
33+
34+
return tasks
35+
}, [])
4236
}

‎src/resolveTaskFn.js

+30-38
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@ const debug = require('debug')('lint-staged:task')
1313
* return the promise.
1414
*
1515
* @param {string} cmd
16+
* @param {Array<string>} args
17+
* @param {Object} execaOptions
1618
* @return {Promise} child_process
1719
*/
18-
const execLinter = (cmd, args, execaOptions = {}) => {
20+
const execLinter = (cmd, args, execaOptions) => {
1921
debug('cmd:', cmd)
2022
if (args) debug('args:', args)
2123
debug('execaOptions:', execaOptions)
@@ -75,52 +77,42 @@ function makeErr(linter, result, context = {}) {
7577
* if the OS is Windows.
7678
*
7779
* @param {Object} options
80+
* @param {string} options.gitDir
81+
* @param {Boolean} options.isFn
7882
* @param {string} options.linter
7983
* @param {Boolean} options.shellMode
80-
* @param {string} options.gitDir
8184
* @param {Array<string>} options.pathsToLint
8285
* @returns {function(): Promise<Array<string>>}
8386
*/
84-
module.exports = function resolveTaskFn({ gitDir, linter, pathsToLint, shell = false }) {
85-
// If `linter` is a function, it should return a string when evaluated with `pathsToLint`.
86-
// Else, it's a already a string
87-
const fnLinter = typeof linter === 'function'
88-
const linterString = fnLinter ? linter(pathsToLint) : linter
89-
// Support arrays of strings/functions by treating everything as arrays
90-
const linters = Array.isArray(linterString) ? linterString : [linterString]
91-
87+
module.exports = function resolveTaskFn({ gitDir, isFn, linter, pathsToLint, shell = false }) {
9288
const execaOptions = { preferLocal: true, reject: false, shell }
9389

94-
const tasks = linters.map(command => {
95-
let cmd
96-
let args
90+
let cmd
91+
let args
9792

98-
if (shell) {
99-
execaOptions.shell = true
100-
// If `shell`, passed command shouldn't be parsed
101-
// If `linter` is a function, command already includes `pathsToLint`.
102-
cmd = fnLinter ? command : `${command} ${pathsToLint.join(' ')}`
103-
} else {
104-
const [parsedCmd, ...parsedArgs] = stringArgv.parseArgsStringToArgv(command)
105-
cmd = parsedCmd
106-
args = fnLinter ? parsedArgs : parsedArgs.concat(pathsToLint)
107-
}
108-
109-
// Only use gitDir as CWD if we are using the git binary
110-
// e.g `npm` should run tasks in the actual CWD
111-
if (/^git(\.exe)?/i.test(command) && gitDir !== process.cwd()) {
112-
execaOptions.cwd = gitDir
113-
}
93+
if (shell) {
94+
execaOptions.shell = true
95+
// If `shell`, passed command shouldn't be parsed
96+
// If `linter` is a function, command already includes `pathsToLint`.
97+
cmd = isFn ? linter : `${linter} ${pathsToLint.join(' ')}`
98+
} else {
99+
const [parsedCmd, ...parsedArgs] = stringArgv.parseArgsStringToArgv(linter)
100+
cmd = parsedCmd
101+
args = isFn ? parsedArgs : parsedArgs.concat(pathsToLint)
102+
}
114103

115-
return ctx =>
116-
execLinter(cmd, args, execaOptions).then(result => {
117-
if (result.failed || result.killed || result.signal != null) {
118-
throw makeErr(linter, result, ctx)
119-
}
104+
// Only use gitDir as CWD if we are using the git binary
105+
// e.g `npm` should run tasks in the actual CWD
106+
if (/^git(\.exe)?/i.test(linter) && gitDir !== process.cwd()) {
107+
execaOptions.cwd = gitDir
108+
}
120109

121-
return successMsg(linter)
122-
})
123-
})
110+
return ctx =>
111+
execLinter(cmd, args, execaOptions).then(result => {
112+
if (result.failed || result.killed || result.signal != null) {
113+
throw makeErr(linter, result, ctx)
114+
}
124115

125-
return ctx => Promise.all(tasks.map(task => task(ctx)))
116+
return successMsg(linter)
117+
})
126118
}

‎test/makeCmdTasks.spec.js

+16-6
Original file line numberDiff line numberDiff line change
@@ -61,25 +61,35 @@ describe('makeCmdTasks', () => {
6161

6262
it('should work with function linter returning array of string', async () => {
6363
const res = await makeCmdTasks(() => ['test', 'test2'], false, gitDir, ['test.js'])
64-
expect(res.length).toBe(1)
64+
expect(res.length).toBe(2)
6565
expect(res[0].title).toEqual('test')
66+
expect(res[1].title).toEqual('test2')
6667
})
6768

6869
it('should work with function linter accepting arguments', async () => {
6970
const res = await makeCmdTasks(
7071
filenames => filenames.map(file => `test ${file}`),
7172
false,
7273
gitDir,
73-
['test.js']
74+
['test.js', 'test2.js']
7475
)
75-
expect(res.length).toBe(1)
76-
expect(res[0].title).toEqual('test [file]')
76+
expect(res.length).toBe(2)
77+
expect(res[0].title).toEqual('test test.js')
78+
expect(res[1].title).toEqual('test test2.js')
7779
})
7880

7981
it('should work with array of mixed string and function linters', async () => {
80-
const res = await makeCmdTasks([() => 'test', 'test2'], false, gitDir, ['test.js'])
81-
expect(res.length).toBe(2)
82+
const res = await makeCmdTasks(
83+
[() => 'test', 'test2', files => files.map(file => `test ${file}`)],
84+
false,
85+
gitDir,
86+
['test.js', 'test2.js', 'test3.js']
87+
)
88+
expect(res.length).toBe(5)
8289
expect(res[0].title).toEqual('test')
8390
expect(res[1].title).toEqual('test2')
91+
expect(res[2].title).toEqual('test test.js')
92+
expect(res[3].title).toEqual('test test2.js')
93+
expect(res[4].title).toEqual('test test3.js')
8494
})
8595
})

‎test/resolveTaskFn.spec.js

+25-11
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,12 @@ describe('resolveTaskFn', () => {
2424
})
2525
})
2626

27-
it('should support function linters that return string', async () => {
27+
it('should not append pathsToLint when isFn', async () => {
2828
expect.assertions(2)
2929
const taskFn = resolveTaskFn({
3030
...defaultOpts,
31-
linter: filenames => `node --arg=true ./myscript.js ${filenames}`
31+
isFn: true,
32+
linter: 'node --arg=true ./myscript.js test.js'
3233
})
3334

3435
await taskFn()
@@ -40,25 +41,38 @@ describe('resolveTaskFn', () => {
4041
})
4142
})
4243

43-
it('should support function linters that return array of strings', async () => {
44-
expect.assertions(3)
44+
it('should not append pathsToLint when isFn and shell', async () => {
45+
expect.assertions(2)
4546
const taskFn = resolveTaskFn({
4647
...defaultOpts,
47-
pathsToLint: ['foo.js', 'bar.js'],
48-
linter: filenames => filenames.map(filename => `node --arg=true ./myscript.js ${filename}`)
48+
isFn: true,
49+
shell: true,
50+
linter: 'node --arg=true ./myscript.js test.js'
4951
})
5052

5153
await taskFn()
52-
expect(execa).toHaveBeenCalledTimes(2)
53-
expect(execa).nthCalledWith(1, 'node', ['--arg=true', './myscript.js', 'foo.js'], {
54+
expect(execa).toHaveBeenCalledTimes(1)
55+
expect(execa).lastCalledWith('node --arg=true ./myscript.js test.js', {
5456
preferLocal: true,
5557
reject: false,
56-
shell: false
58+
shell: true
59+
})
60+
})
61+
62+
it('should work with shell', async () => {
63+
expect.assertions(2)
64+
const taskFn = resolveTaskFn({
65+
...defaultOpts,
66+
shell: true,
67+
linter: 'node --arg=true ./myscript.js'
5768
})
58-
expect(execa).nthCalledWith(2, 'node', ['--arg=true', './myscript.js', 'bar.js'], {
69+
70+
await taskFn()
71+
expect(execa).toHaveBeenCalledTimes(1)
72+
expect(execa).lastCalledWith('node --arg=true ./myscript.js test.js', {
5973
preferLocal: true,
6074
reject: false,
61-
shell: false
75+
shell: true
6276
})
6377
})
6478

‎test/resolveTaskFn.unmocked.spec.js

+5-8
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,13 @@ describe('resolveTaskFn', () => {
66
it('should call execa with shell when configured so', async () => {
77
const taskFn = resolveTaskFn({
88
pathsToLint: ['package.json'],
9-
linter: () => 'node -e "process.exit(1)" || echo $?',
9+
isFn: true,
10+
linter: 'node -e "process.exit(1)" || echo $?',
1011
shell: true
1112
})
1213

13-
await expect(taskFn()).resolves.toMatchInlineSnapshot(`
14-
Array [
15-
"√ function linter() {
16-
return 'node -e \\"process.exit(1)\\" || echo $?';
17-
} passed!",
18-
]
19-
`)
14+
await expect(taskFn()).resolves.toMatchInlineSnapshot(
15+
`"√ node -e \\"process.exit(1)\\" || echo $? passed!"`
16+
)
2017
})
2118
})

0 commit comments

Comments
 (0)
Please sign in to comment.