Skip to content

Commit 1dcdb89

Browse files
iirojokonet
authored andcommitted
feat: Use shorter title for function tasks with many staged files (#706)
* refactor: remove usage of `linter` and prefer `command` * test: remove full snapshot since new git version changed the text * improvement: create shorter titles for function tasks with many staged files Closes #674
1 parent 88d9d4f commit 1dcdb89

File tree

7 files changed

+76
-78
lines changed

7 files changed

+76
-78
lines changed

src/makeCmdTasks.js

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,27 +8,36 @@ const debug = require('debug')('lint-staged:make-cmd-tasks')
88
* Creates and returns an array of listr tasks which map to the given commands.
99
*
1010
* @param {object} options
11-
* @param {Array<string|Function>|string|Function} [options.commands]
12-
* @param {string} [options.gitDir]
13-
* @param {Array<string>} [options.pathsToLint]
11+
* @param {Array<string|Function>|string|Function} options.commands
12+
* @param {Array<string>} options.files
13+
* @param {string} options.gitDir
1414
* @param {Boolean} shell
1515
*/
16-
module.exports = async function makeCmdTasks({ commands, gitDir, pathsToLint, shell }) {
16+
module.exports = async function makeCmdTasks({ commands, files, gitDir, shell }) {
1717
debug('Creating listr tasks for commands %o', commands)
1818
const commandsArray = Array.isArray(commands) ? commands : [commands]
1919

2020
return commandsArray.reduce((tasks, command) => {
21-
// linter function may return array of commands that already include `pathsToLit`
21+
// command function may return array of commands that already include `stagedFiles`
2222
const isFn = typeof command === 'function'
23-
const resolved = isFn ? command(pathsToLint) : command
24-
const linters = Array.isArray(resolved) ? resolved : [resolved] // Wrap non-array linter as array
23+
const resolved = isFn ? command(files) : command
24+
const commands = Array.isArray(resolved) ? resolved : [resolved] // Wrap non-array command as array
2525

26-
linters.forEach(linter => {
27-
const task = {
28-
title: linter,
29-
task: resolveTaskFn({ gitDir, isFn, linter, pathsToLint, shell })
30-
}
26+
// Function command should not be used as the task title as-is
27+
// because the resolved string it might be very long
28+
// Create a matching command array with [file] in place of file names
29+
let mockCommands
30+
if (isFn) {
31+
const mockFileList = Array(commands.length).fill('[file]')
32+
const resolved = command(mockFileList)
33+
mockCommands = Array.isArray(resolved) ? resolved : [resolved]
34+
}
3135

36+
commands.forEach((command, i) => {
37+
// If command is a function, use the matching mock command as title,
38+
// but since might include multiple [file] arguments, shorten to one
39+
const title = isFn ? mockCommands[i].replace(/\[file\].*\[file\]/, '[file]') : command
40+
const task = { title, task: resolveTaskFn({ gitDir, isFn, command, files, shell }) }
3241
tasks.push(task)
3342
})
3443

src/resolveTaskFn.js

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -77,27 +77,20 @@ function makeErr(linter, result, context = {}) {
7777
* if the OS is Windows.
7878
*
7979
* @param {Object} options
80-
* @param {String} [options.gitDir] - Current git repo path
81-
* @param {Boolean} [options.isFn] - Whether the linter task is a function
82-
* @param {string} [options.linter] — Linter task
83-
* @param {Array<string>} [options.pathsToLint] — Filepaths to run the linter task against
80+
* @param {string} options.command — Linter task
81+
* @param {String} options.gitDir - Current git repo path
82+
* @param {Boolean} options.isFn - Whether the linter task is a function
83+
* @param {Array<string>} options.pathsToLint — Filepaths to run the linter task against
8484
* @param {Boolean} [options.relative] — Whether the filepaths should be relative
8585
* @param {Boolean} [options.shell] — Whether to skip parsing linter task for better shell support
8686
* @returns {function(): Promise<Array<string>>}
8787
*/
88-
module.exports = function resolveTaskFn({
89-
gitDir,
90-
isFn,
91-
linter,
92-
pathsToLint,
93-
relative,
94-
shell = false
95-
}) {
88+
module.exports = function resolveTaskFn({ command, files, gitDir, isFn, relative, shell = false }) {
9689
const execaOptions = { preferLocal: true, reject: false, shell }
9790

9891
if (relative) {
9992
execaOptions.cwd = process.cwd()
100-
} else if (/^git(\.exe)?/i.test(linter) && gitDir !== process.cwd()) {
93+
} else if (/^git(\.exe)?/i.test(command) && gitDir !== process.cwd()) {
10194
// Only use gitDir as CWD if we are using the git binary
10295
// e.g `npm` should run tasks in the actual CWD
10396
execaOptions.cwd = gitDir
@@ -109,20 +102,20 @@ module.exports = function resolveTaskFn({
109102
if (shell) {
110103
execaOptions.shell = true
111104
// If `shell`, passed command shouldn't be parsed
112-
// If `linter` is a function, command already includes `pathsToLint`.
113-
cmd = isFn ? linter : `${linter} ${pathsToLint.join(' ')}`
105+
// If `linter` is a function, command already includes `files`.
106+
cmd = isFn ? command : `${command} ${files.join(' ')}`
114107
} else {
115-
const [parsedCmd, ...parsedArgs] = stringArgv.parseArgsStringToArgv(linter)
108+
const [parsedCmd, ...parsedArgs] = stringArgv.parseArgsStringToArgv(command)
116109
cmd = parsedCmd
117-
args = isFn ? parsedArgs : parsedArgs.concat(pathsToLint)
110+
args = isFn ? parsedArgs : parsedArgs.concat(files)
118111
}
119112

120113
return ctx =>
121114
execLinter(cmd, args, execaOptions).then(result => {
122115
if (result.failed || result.killed || result.signal != null) {
123-
throw makeErr(linter, result, ctx)
116+
throw makeErr(command, result, ctx)
124117
}
125118

126-
return successMsg(linter)
119+
return successMsg(command)
127120
})
128121
}

src/runAll.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ https://github.com/okonet/lint-staged#using-js-functions-to-customize-linter-com
7373
title: `Running tasks for ${task.pattern}`,
7474
task: async () =>
7575
new Listr(
76-
await makeCmdTasks({ commands: task.commands, gitDir, shell, pathsToLint: task.fileList }),
76+
await makeCmdTasks({ commands: task.commands, files: task.fileList, gitDir, shell }),
7777
{
7878
// In sub-tasks we don't want to run concurrently
7979
// and we want to abort on errors

test/makeCmdTasks.spec.js

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@ describe('makeCmdTasks', () => {
99
})
1010

1111
it('should return an array', async () => {
12-
const array = await makeCmdTasks({ commands: 'test', gitDir, pathsToLint: ['test.js'] })
12+
const array = await makeCmdTasks({ commands: 'test', gitDir, files: ['test.js'] })
1313
expect(array).toBeInstanceOf(Array)
1414
})
1515

1616
it('should work with a single command', async () => {
1717
expect.assertions(4)
18-
const res = await makeCmdTasks({ commands: 'test', gitDir, pathsToLint: ['test.js'] })
18+
const res = await makeCmdTasks({ commands: 'test', gitDir, files: ['test.js'] })
1919
expect(res.length).toBe(1)
2020
const [linter] = res
2121
expect(linter.title).toBe('test')
@@ -30,7 +30,7 @@ describe('makeCmdTasks', () => {
3030
const res = await makeCmdTasks({
3131
commands: ['test', 'test2'],
3232
gitDir,
33-
pathsToLint: ['test.js']
33+
files: ['test.js']
3434
})
3535
expect(res.length).toBe(2)
3636
const [linter1, linter2] = res
@@ -58,7 +58,7 @@ describe('makeCmdTasks', () => {
5858
})
5959

6060
it('should work with function linter returning a string', async () => {
61-
const res = await makeCmdTasks({ commands: () => 'test', gitDir, pathsToLint: ['test.js'] })
61+
const res = await makeCmdTasks({ commands: () => 'test', gitDir, files: ['test.js'] })
6262
expect(res.length).toBe(1)
6363
expect(res[0].title).toEqual('test')
6464
})
@@ -67,7 +67,7 @@ describe('makeCmdTasks', () => {
6767
const res = await makeCmdTasks({
6868
commands: () => ['test', 'test2'],
6969
gitDir,
70-
pathsToLint: ['test.js']
70+
files: ['test.js']
7171
})
7272
expect(res.length).toBe(2)
7373
expect(res[0].title).toEqual('test')
@@ -78,24 +78,34 @@ describe('makeCmdTasks', () => {
7878
const res = await makeCmdTasks({
7979
commands: filenames => filenames.map(file => `test ${file}`),
8080
gitDir,
81-
pathsToLint: ['test.js', 'test2.js']
81+
files: ['test.js', 'test2.js']
8282
})
8383
expect(res.length).toBe(2)
84-
expect(res[0].title).toEqual('test test.js')
85-
expect(res[1].title).toEqual('test test2.js')
84+
expect(res[0].title).toEqual('test [file]')
85+
expect(res[1].title).toEqual('test [file]')
8686
})
8787

8888
it('should work with array of mixed string and function linters', async () => {
8989
const res = await makeCmdTasks({
9090
commands: [() => 'test', 'test2', files => files.map(file => `test ${file}`)],
9191
gitDir,
92-
pathsToLint: ['test.js', 'test2.js', 'test3.js']
92+
files: ['test.js', 'test2.js', 'test3.js']
9393
})
9494
expect(res.length).toBe(5)
9595
expect(res[0].title).toEqual('test')
9696
expect(res[1].title).toEqual('test2')
97-
expect(res[2].title).toEqual('test test.js')
98-
expect(res[3].title).toEqual('test test2.js')
99-
expect(res[4].title).toEqual('test test3.js')
97+
expect(res[2].title).toEqual('test [file]')
98+
expect(res[3].title).toEqual('test [file]')
99+
expect(res[4].title).toEqual('test [file]')
100+
})
101+
102+
it('should generate short names for function tasks with long file list', async () => {
103+
const res = await makeCmdTasks({
104+
commands: filenames => `test ${filenames.map(file => `--file ${file}`).join(' ')}`,
105+
gitDir,
106+
files: Array(100).fill('file.js') // 100 times `file.js`
107+
})
108+
expect(res.length).toBe(1)
109+
expect(res[0].title).toEqual('test --file [file]')
100110
})
101111
})

test/resolveTaskFn.spec.js

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import execa from 'execa'
22
import resolveTaskFn from '../src/resolveTaskFn'
33

4-
const defaultOpts = { pathsToLint: ['test.js'] }
4+
const defaultOpts = { files: ['test.js'] }
55

66
describe('resolveTaskFn', () => {
77
beforeEach(() => {
@@ -12,7 +12,7 @@ describe('resolveTaskFn', () => {
1212
expect.assertions(2)
1313
const taskFn = resolveTaskFn({
1414
...defaultOpts,
15-
linter: 'node --arg=true ./myscript.js'
15+
command: 'node --arg=true ./myscript.js'
1616
})
1717

1818
await taskFn()
@@ -29,7 +29,7 @@ describe('resolveTaskFn', () => {
2929
const taskFn = resolveTaskFn({
3030
...defaultOpts,
3131
isFn: true,
32-
linter: 'node --arg=true ./myscript.js test.js'
32+
command: 'node --arg=true ./myscript.js test.js'
3333
})
3434

3535
await taskFn()
@@ -47,7 +47,7 @@ describe('resolveTaskFn', () => {
4747
...defaultOpts,
4848
isFn: true,
4949
shell: true,
50-
linter: 'node --arg=true ./myscript.js test.js'
50+
command: 'node --arg=true ./myscript.js test.js'
5151
})
5252

5353
await taskFn()
@@ -64,7 +64,7 @@ describe('resolveTaskFn', () => {
6464
const taskFn = resolveTaskFn({
6565
...defaultOpts,
6666
shell: true,
67-
linter: 'node --arg=true ./myscript.js'
67+
command: 'node --arg=true ./myscript.js'
6868
})
6969

7070
await taskFn()
@@ -80,7 +80,7 @@ describe('resolveTaskFn', () => {
8080
expect.assertions(2)
8181
const taskFn = resolveTaskFn({
8282
...defaultOpts,
83-
linter: 'git add',
83+
command: 'git add',
8484
gitDir: '../'
8585
})
8686

@@ -96,7 +96,7 @@ describe('resolveTaskFn', () => {
9696

9797
it('should not pass `gitDir` as `cwd` to `execa()` if a non-git binary is called', async () => {
9898
expect.assertions(2)
99-
const taskFn = resolveTaskFn({ ...defaultOpts, linter: 'jest', gitDir: '../' })
99+
const taskFn = resolveTaskFn({ ...defaultOpts, command: 'jest', gitDir: '../' })
100100

101101
await taskFn()
102102
expect(execa).toHaveBeenCalledTimes(1)
@@ -111,7 +111,7 @@ describe('resolveTaskFn', () => {
111111
expect.assertions(2)
112112
const taskFn = resolveTaskFn({
113113
...defaultOpts,
114-
linter: 'git add',
114+
command: 'git add',
115115
relative: true
116116
})
117117

@@ -135,7 +135,7 @@ describe('resolveTaskFn', () => {
135135
cmd: 'mock cmd'
136136
})
137137

138-
const taskFn = resolveTaskFn({ ...defaultOpts, linter: 'mock-fail-linter' })
138+
const taskFn = resolveTaskFn({ ...defaultOpts, command: 'mock-fail-linter' })
139139
try {
140140
await taskFn()
141141
} catch (err) {
@@ -161,7 +161,7 @@ Mock error"
161161
cmd: 'mock cmd'
162162
})
163163

164-
const taskFn = resolveTaskFn({ ...defaultOpts, linter: 'mock-killed-linter' })
164+
const taskFn = resolveTaskFn({ ...defaultOpts, command: 'mock-killed-linter' })
165165
try {
166166
await taskFn()
167167
} catch (err) {
@@ -177,7 +177,7 @@ Mock error"
177177
it('should not set hasErrors on context if no error occur', async () => {
178178
expect.assertions(1)
179179
const context = {}
180-
const taskFn = resolveTaskFn({ ...defaultOpts, linter: 'jest', gitDir: '../' })
180+
const taskFn = resolveTaskFn({ ...defaultOpts, command: 'jest', gitDir: '../' })
181181
await taskFn(context)
182182
expect(context.hasErrors).toBeUndefined()
183183
})
@@ -191,7 +191,7 @@ Mock error"
191191
cmd: 'mock cmd'
192192
})
193193
const context = {}
194-
const taskFn = resolveTaskFn({ ...defaultOpts, linter: 'mock-fail-linter' })
194+
const taskFn = resolveTaskFn({ ...defaultOpts, command: 'mock-fail-linter' })
195195
expect.assertions(1)
196196
try {
197197
await taskFn(context)

test/resolveTaskFn.unmocked.spec.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ jest.unmock('execa')
55
describe('resolveTaskFn', () => {
66
it('should call execa with shell when configured so', async () => {
77
const taskFn = resolveTaskFn({
8-
pathsToLint: ['package.json'],
8+
command: 'node -e "process.exit(1)" || echo $?',
9+
files: ['package.json'],
910
isFn: true,
10-
linter: 'node -e "process.exit(1)" || echo $?',
1111
shell: true
1212
})
1313

test/runAll.unmocked.spec.js

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -172,16 +172,9 @@ describe('runAll', () => {
172172
expect(await execGit(['show', 'HEAD:test.js'])).toEqual(testJsFilePretty.replace(/\n$/, ''))
173173

174174
// Since edit was not staged, the file is still modified
175-
expect(await execGit(['status'])).toMatchInlineSnapshot(`
176-
"On branch master
177-
Changes not staged for commit:
178-
(use \\"git add <file>...\\" to update what will be committed)
179-
(use \\"git checkout -- <file>...\\" to discard changes in working directory)
180-
181-
modified: test.js
182-
183-
no changes added to commit (use \\"git add\\" and/or \\"git commit -a\\")"
184-
`)
175+
const status = await execGit(['status'])
176+
expect(status).toMatch('modified: test.js')
177+
expect(status).toMatch('no changes added to commit')
185178
expect(await readFile('test.js')).toEqual(testJsFilePretty + appended)
186179
})
187180

@@ -210,16 +203,9 @@ no changes added to commit (use \\"git add\\" and/or \\"git commit -a\\")"
210203
expect(await execGit(['show', 'HEAD:test.js'])).toEqual(testJsFilePretty.replace(/\n$/, ''))
211204

212205
// Nothing is staged
213-
expect(await execGit(['status'])).toMatchInlineSnapshot(`
214-
"On branch master
215-
Changes not staged for commit:
216-
(use \\"git add <file>...\\" to update what will be committed)
217-
(use \\"git checkout -- <file>...\\" to discard changes in working directory)
218-
219-
modified: test.js
220-
221-
no changes added to commit (use \\"git add\\" and/or \\"git commit -a\\")"
222-
`)
206+
const status = await execGit(['status'])
207+
expect(status).toMatch('modified: test.js')
208+
expect(status).toMatch('no changes added to commit')
223209

224210
// File is pretty, and has been edited
225211
expect(await readFile('test.js')).toEqual(testJsFilePretty + appended)

0 commit comments

Comments
 (0)