lint-staged 17.1.0 → 17.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -5
- package/lib/cli.js +17 -12
- package/lib/generateTasks.js +1 -1
- package/lib/getSpawnedTask.js +4 -4
- package/lib/getSpawnedTasks.js +125 -37
- package/lib/gitWorkflow.js +25 -20
- package/lib/index.js +2 -19
- package/lib/runAll.js +64 -86
- package/lib/runParallelTasks.js +5 -5
- package/lib/validateOptions.js +25 -7
- package/package.json +3 -3
- package/lib/chunkFiles.js +0 -65
package/README.md
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
# 🚫💩 lint-staged
|
|
2
2
|
|
|
3
|
-
[](https://badge.fury.io/js/lint-staged)
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
3
|
Run tasks like formatters and linters against staged git files and don't let :poop: slip into your code base!
|
|
8
4
|
|
|
9
5
|
```bash
|
|
@@ -168,7 +164,7 @@ By default changes made by tasks are automatically staged and added to the commi
|
|
|
168
164
|
|
|
169
165
|
#### `--max-arg-length [number]`
|
|
170
166
|
|
|
171
|
-
|
|
167
|
+
The list of staged files are appended to configured tasks as arguments, and the resulting string might be too long for the current shell, especially on Windows. _Lint-staged_ tries to avoid this by splitting the list of staged files into chunks resulting in the commands being run multiple times. This behavior affects all tasks, including functions. Use the `--max-arg-length` to override the platform-specific default value if you want to avoid this. Setting `--max-arg-length=Infinity` will disable chunking completely.
|
|
172
168
|
|
|
173
169
|
#### `--no-stash`
|
|
174
170
|
|
package/lib/cli.js
CHANGED
|
@@ -91,7 +91,7 @@ const CLI_OPTIONS = [
|
|
|
91
91
|
},
|
|
92
92
|
{
|
|
93
93
|
flag: 'max-arg-length',
|
|
94
|
-
type: 'string', // Parsed with `
|
|
94
|
+
type: 'string', // Parsed with `Number()` below
|
|
95
95
|
positional: '[number]',
|
|
96
96
|
description: 'maximum length of the command-line argument string (default: 0)',
|
|
97
97
|
},
|
|
@@ -128,6 +128,20 @@ const CLI_OPTIONS = [
|
|
|
128
128
|
},
|
|
129
129
|
]
|
|
130
130
|
|
|
131
|
+
/** @type {(value: string |undefined) => number | boolean | null | undefined} */
|
|
132
|
+
const parseBoolOrNumber = (value) => {
|
|
133
|
+
switch (value) {
|
|
134
|
+
case undefined:
|
|
135
|
+
return undefined
|
|
136
|
+
case 'true':
|
|
137
|
+
return true
|
|
138
|
+
case 'false':
|
|
139
|
+
return false
|
|
140
|
+
default:
|
|
141
|
+
return Number(value)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
131
145
|
/** @param {string[]} argv */
|
|
132
146
|
export const parseCliOptions = (argv) => {
|
|
133
147
|
const options = CLI_OPTIONS.reduce((acc, current) => {
|
|
@@ -167,18 +181,9 @@ export const parseCliOptions = (argv) => {
|
|
|
167
181
|
values['hide-unstaged'] = false // becomes redundant
|
|
168
182
|
}
|
|
169
183
|
|
|
170
|
-
const maxArgLength =
|
|
171
|
-
values['max-arg-length'] !== undefined ? parseInt(values['max-arg-length'], 10) : undefined
|
|
172
|
-
|
|
173
|
-
if (Number.isNaN(maxArgLength)) {
|
|
174
|
-
throw new TypeError(`Option '--mar-arg-length' takes a numeric argument`, {
|
|
175
|
-
cause: { '--max-arg-length': values['max-arg-length'] },
|
|
176
|
-
})
|
|
177
|
-
}
|
|
178
|
-
|
|
179
184
|
return {
|
|
180
185
|
allowEmpty: values['allow-empty'] ?? false,
|
|
181
|
-
concurrent: values.concurrent
|
|
186
|
+
concurrent: parseBoolOrNumber(values.concurrent) ?? true,
|
|
182
187
|
configPath: values.config,
|
|
183
188
|
continueOnError: !!values['continue-on-error'],
|
|
184
189
|
cwd: values.cwd,
|
|
@@ -190,7 +195,7 @@ export const parseCliOptions = (argv) => {
|
|
|
190
195
|
hidePartiallyStaged: values['hide-partially-staged'] ?? true,
|
|
191
196
|
hideUnstaged: !!values['hide-unstaged'],
|
|
192
197
|
hideAll: !!values['hide-all'],
|
|
193
|
-
maxArgLength,
|
|
198
|
+
maxArgLength: parseBoolOrNumber(values['max-arg-length']),
|
|
194
199
|
quiet: !!values.quiet,
|
|
195
200
|
relative: !!values.relative,
|
|
196
201
|
revert: values.revert ?? true,
|
package/lib/generateTasks.js
CHANGED
|
@@ -16,7 +16,7 @@ const debugLog = createDebug('lint-staged:generateTasks')
|
|
|
16
16
|
* @param {boolean} [options.relative] - Whether filepaths to should be relative to cwd
|
|
17
17
|
*/
|
|
18
18
|
export const generateTasks = ({ config, cwd = process.cwd(), files, relative = false }) => {
|
|
19
|
-
debugLog('Generating
|
|
19
|
+
debugLog('Generating tasks')
|
|
20
20
|
|
|
21
21
|
/** @type {StagedFile[]} */
|
|
22
22
|
const relativeFiles = files.map((file) => ({
|
package/lib/getSpawnedTask.js
CHANGED
|
@@ -57,16 +57,16 @@ export const createTaskError = (command, result, ctx, signal = 'FAILED') => {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
/**
|
|
60
|
-
* Returns the task
|
|
60
|
+
* Returns the spawnable task.
|
|
61
61
|
*
|
|
62
62
|
* @param {Object} options
|
|
63
63
|
* @param {AbortController} options.abortController
|
|
64
|
-
* @param {string} options.command —
|
|
64
|
+
* @param {string} options.command — the command to spawn
|
|
65
65
|
* @param {string} [options.continueOnError]
|
|
66
66
|
* @param {string} [options.cwd]
|
|
67
67
|
* @param {String} options.topLevelDir - Current git repo top-level path
|
|
68
|
-
* @param {Boolean} options.isFn - Whether the
|
|
69
|
-
* @param {string[]} options.files — Filepaths to run the
|
|
68
|
+
* @param {Boolean} options.isFn - Whether the task is a function instead of a string
|
|
69
|
+
* @param {string[]} options.files — Filepaths to run the task against
|
|
70
70
|
* @param {Boolean} [options.verbose] — Always show task verbose
|
|
71
71
|
* @returns {() => Promise<Array<string>>}
|
|
72
72
|
*/
|
package/lib/getSpawnedTasks.js
CHANGED
|
@@ -4,6 +4,104 @@ import { configurationError } from './messages.js'
|
|
|
4
4
|
|
|
5
5
|
const debugLog = createDebug('lint-staged:getSpawnedTasks')
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Get the maximum length of a command-line argument string based on current platform
|
|
9
|
+
*
|
|
10
|
+
* https://serverfault.com/questions/69430/what-is-the-maximum-length-of-a-command-line-in-mac-os-x
|
|
11
|
+
* https://support.microsoft.com/en-us/help/830473/command-prompt-cmd-exe-command-line-string-limitation
|
|
12
|
+
* https://unix.stackexchange.com/a/120652
|
|
13
|
+
*/
|
|
14
|
+
export const getMaxArgLength = (platform = process.platform) => {
|
|
15
|
+
switch (platform) {
|
|
16
|
+
case 'darwin':
|
|
17
|
+
return 262_144
|
|
18
|
+
case 'win32':
|
|
19
|
+
return 8_191
|
|
20
|
+
default:
|
|
21
|
+
return 131_072
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Chunk `files` so that when joined with `command`, the resulting string fits into `maxArgLength`.
|
|
27
|
+
* For function commands, they are evaluated with the list of files and measured afterwards.
|
|
28
|
+
*
|
|
29
|
+
* @param {string | Function} command
|
|
30
|
+
* @param {string[]} files
|
|
31
|
+
* @param {number} [maxArgLength]
|
|
32
|
+
* @returns {Promise<Array<{ command: string, files: string[] }>>}
|
|
33
|
+
*/
|
|
34
|
+
export const chunkFilesForCommand = async (command, files, maxArgLength) => {
|
|
35
|
+
const isFn = typeof command === 'function'
|
|
36
|
+
const resolved = isFn ? await command([...files]) : command
|
|
37
|
+
const resolvedCommands = Array.isArray(resolved) ? resolved : [resolved]
|
|
38
|
+
|
|
39
|
+
if (isFn && resolvedCommands.some((command) => typeof command !== 'string')) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
configurationError(
|
|
42
|
+
'[Function]',
|
|
43
|
+
'Function task should return a string or an array of strings',
|
|
44
|
+
resolved
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!maxArgLength || maxArgLength === Infinity || files.length <= 1) {
|
|
50
|
+
debugLog(
|
|
51
|
+
'Skipping chunking because maxArgLength is %s and there are %s files',
|
|
52
|
+
maxArgLength,
|
|
53
|
+
files.length
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
return resolvedCommands.map((resolvedCommand) => ({
|
|
57
|
+
command: resolvedCommand,
|
|
58
|
+
files,
|
|
59
|
+
}))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const fits = resolvedCommands.every((resolvedCommand) => {
|
|
63
|
+
/**
|
|
64
|
+
* Really the command is spawned using `tinyexec` and the files passed as an array of
|
|
65
|
+
* arguments; but this approximates the length without quoting filenames.
|
|
66
|
+
*/
|
|
67
|
+
const finalCommandString = isFn ? resolvedCommand : resolvedCommand + ' ' + files.join(' ')
|
|
68
|
+
|
|
69
|
+
debugLog(
|
|
70
|
+
'Resolved an argument string length of %d characters from %d files',
|
|
71
|
+
finalCommandString.length,
|
|
72
|
+
files.length
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
return finalCommandString.length <= maxArgLength
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
if (fits) {
|
|
79
|
+
return resolvedCommands.map((resolvedCommand) => ({
|
|
80
|
+
command: resolvedCommand,
|
|
81
|
+
files,
|
|
82
|
+
}))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const middle = Math.ceil(files.length / 2)
|
|
86
|
+
const left = files.slice(0, middle)
|
|
87
|
+
const right = files.slice(middle)
|
|
88
|
+
|
|
89
|
+
debugLog(
|
|
90
|
+
'Splitting %d files into chunks of %d and %d for maxArgLength of %d',
|
|
91
|
+
files.length,
|
|
92
|
+
left.length,
|
|
93
|
+
right.length,
|
|
94
|
+
maxArgLength
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
const commands = await Promise.all([
|
|
98
|
+
chunkFilesForCommand(command, left, maxArgLength),
|
|
99
|
+
chunkFilesForCommand(command, right, maxArgLength),
|
|
100
|
+
])
|
|
101
|
+
|
|
102
|
+
return commands.flat()
|
|
103
|
+
}
|
|
104
|
+
|
|
7
105
|
/**
|
|
8
106
|
* Creates and returns an array of tasks which map to the given commands.
|
|
9
107
|
*
|
|
@@ -13,6 +111,7 @@ const debugLog = createDebug('lint-staged:getSpawnedTasks')
|
|
|
13
111
|
* @param {string} options.continueOnError
|
|
14
112
|
* @param {string} options.cwd
|
|
15
113
|
* @param {import('./getStagedFiles.js').StagedFile[]} options.files
|
|
114
|
+
* @param {number} [options.maxArgLength]
|
|
16
115
|
* @param {string} options.topLevelDir
|
|
17
116
|
* @param {Boolean} verbose
|
|
18
117
|
*/
|
|
@@ -22,52 +121,41 @@ export const getSpawnedTasks = async ({
|
|
|
22
121
|
continueOnError,
|
|
23
122
|
cwd,
|
|
24
123
|
files,
|
|
124
|
+
maxArgLength,
|
|
25
125
|
topLevelDir,
|
|
26
126
|
verbose,
|
|
27
127
|
}) => {
|
|
28
128
|
debugLog('Creating tasks for commands %o', commands)
|
|
29
|
-
const cmdTasks = []
|
|
30
129
|
|
|
31
130
|
const commandArray = Array.isArray(commands) ? commands : [commands]
|
|
32
131
|
|
|
33
132
|
const filepaths = files.map((f) => f.filepath)
|
|
34
133
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
/** Pass copy of file list to prevent mutation by function from config file. */
|
|
40
|
-
const resolved = isFn ? await cmd([...filepaths]) : cmd
|
|
41
|
-
|
|
42
|
-
const resolvedArray = Array.isArray(resolved) ? resolved : [resolved] // Wrap non-array command as array
|
|
43
|
-
|
|
44
|
-
for (const command of resolvedArray) {
|
|
45
|
-
// If the function linter didn't return string | string[] it won't work
|
|
46
|
-
// Do the validation here instead of `validateConfig` to skip evaluating the function multiple times
|
|
47
|
-
if (isFn && typeof command !== 'string') {
|
|
48
|
-
throw new Error(
|
|
49
|
-
configurationError(
|
|
50
|
-
'[Function]',
|
|
51
|
-
'Function task should return a string or an array of strings',
|
|
52
|
-
resolved
|
|
53
|
-
)
|
|
54
|
-
)
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const task = getSpawnedTask({
|
|
58
|
-
abortController,
|
|
59
|
-
command,
|
|
60
|
-
continueOnError,
|
|
61
|
-
cwd,
|
|
62
|
-
files: filepaths,
|
|
63
|
-
topLevelDir,
|
|
64
|
-
isFn,
|
|
65
|
-
verbose,
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
cmdTasks.push({ title: command, task })
|
|
69
|
-
}
|
|
134
|
+
if (filepaths.length === 0) {
|
|
135
|
+
debugLog('Skipping task generation because no files matched', { commands })
|
|
136
|
+
return []
|
|
70
137
|
}
|
|
71
138
|
|
|
72
|
-
|
|
139
|
+
const spawnedTasks = await Promise.all(
|
|
140
|
+
commandArray.map(async (cmd) => {
|
|
141
|
+
const isFn = typeof cmd === 'function'
|
|
142
|
+
const chunkedCommands = await chunkFilesForCommand(cmd, filepaths, maxArgLength)
|
|
143
|
+
|
|
144
|
+
return chunkedCommands.map(({ command, files: chunkedFiles }) => ({
|
|
145
|
+
title: command,
|
|
146
|
+
task: getSpawnedTask({
|
|
147
|
+
abortController,
|
|
148
|
+
command,
|
|
149
|
+
continueOnError,
|
|
150
|
+
cwd,
|
|
151
|
+
files: chunkedFiles,
|
|
152
|
+
topLevelDir,
|
|
153
|
+
isFn,
|
|
154
|
+
verbose,
|
|
155
|
+
}),
|
|
156
|
+
}))
|
|
157
|
+
})
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
return spawnedTasks.flat()
|
|
73
161
|
}
|
package/lib/gitWorkflow.js
CHANGED
|
@@ -8,6 +8,7 @@ import { execGit } from './execGit.js'
|
|
|
8
8
|
import * as figures from './figures.js'
|
|
9
9
|
import { readFile, unlink, writeFile } from './file.js'
|
|
10
10
|
import { getDiffCommand } from './getDiffCommand.js'
|
|
11
|
+
import { chunkFilesForCommand } from './getSpawnedTasks.js'
|
|
11
12
|
import { normalizePath } from './normalizePath.js'
|
|
12
13
|
import { parseGitZOutput } from './parseGitZOutput.js'
|
|
13
14
|
import { runParallelTasks } from './runParallelTasks.js'
|
|
@@ -86,7 +87,8 @@ export class GitWorkflow {
|
|
|
86
87
|
failOnChanges,
|
|
87
88
|
gitConfigDir,
|
|
88
89
|
logger,
|
|
89
|
-
|
|
90
|
+
matchedFiles,
|
|
91
|
+
maxArgLength,
|
|
90
92
|
topLevelDir,
|
|
91
93
|
}) {
|
|
92
94
|
this.execGit = (args, options = {}) => execGit(args, { ...options, cwd: topLevelDir })
|
|
@@ -96,8 +98,9 @@ export class GitWorkflow {
|
|
|
96
98
|
this.gitConfigDir = gitConfigDir
|
|
97
99
|
this.failOnChanges = !!failOnChanges
|
|
98
100
|
this.logger = logger
|
|
99
|
-
/** @type {import('./getStagedFiles.js').StagedFile
|
|
100
|
-
this.
|
|
101
|
+
/** @type {Set<import('./getStagedFiles.js').StagedFile>} */
|
|
102
|
+
this.matchedFiles = matchedFiles
|
|
103
|
+
this.maxArgLength = maxArgLength
|
|
101
104
|
this.topLevelDir = topLevelDir
|
|
102
105
|
|
|
103
106
|
/**
|
|
@@ -369,25 +372,27 @@ export class GitWorkflow {
|
|
|
369
372
|
? normalizePath(process.env.GIT_INDEX_FILE)
|
|
370
373
|
: process.env.GIT_INDEX_FILE
|
|
371
374
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
})
|
|
383
|
-
)
|
|
375
|
+
const accessCheckedFiles = await Promise.allSettled(
|
|
376
|
+
Array.from(this.matchedFiles).map(async (f) => {
|
|
377
|
+
if (f.status === 'D') {
|
|
378
|
+
await fs.access(f.filepath)
|
|
379
|
+
return f.filepath // File is no longer deleted and can be added
|
|
380
|
+
} else {
|
|
381
|
+
return f.filepath
|
|
382
|
+
}
|
|
383
|
+
})
|
|
384
|
+
)
|
|
384
385
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
386
|
+
const addableFiles = accessCheckedFiles.flatMap((r) =>
|
|
387
|
+
r.status === 'fulfilled' ? [r.value] : []
|
|
388
|
+
)
|
|
388
389
|
|
|
390
|
+
const addCommands = await chunkFilesForCommand('git add --', addableFiles, this.maxArgLength)
|
|
391
|
+
|
|
392
|
+
/** Needs to be run serially because of locking Git operation */
|
|
393
|
+
for (const { files } of addCommands) {
|
|
389
394
|
debugLog('Updating active Git index: %s', activeIndexFile)
|
|
390
|
-
await this.execGit(['add', '--', ...
|
|
395
|
+
await this.execGit(['add', '--', ...files])
|
|
391
396
|
debugLog('Done updating Git index: %s', activeIndexFile)
|
|
392
397
|
|
|
393
398
|
if (activeIndexFile?.endsWith('.lock')) {
|
|
@@ -402,7 +407,7 @@ export class GitWorkflow {
|
|
|
402
407
|
*/
|
|
403
408
|
if (activeIndexFile !== defaultIndexLock) {
|
|
404
409
|
debugLog('Updating default Git index again: %s', defaultIndexLock)
|
|
405
|
-
await this.execGit(['add', '--', ...
|
|
410
|
+
await this.execGit(['add', '--', ...files], {
|
|
406
411
|
env: {
|
|
407
412
|
GIT_INDEX_FILE: defaultIndexLock,
|
|
408
413
|
},
|
package/lib/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import { assertGitVersion, MIN_GIT_VERSION } from './assertGitVersion.js'
|
|
|
2
2
|
import { enableColors } from './colors.js'
|
|
3
3
|
import { createDebug, enableDebug } from './debug.js'
|
|
4
4
|
import { execGit } from './execGit.js'
|
|
5
|
+
import { getMaxArgLength } from './getSpawnedTasks.js'
|
|
5
6
|
import {
|
|
6
7
|
gitError,
|
|
7
8
|
minGitVersionRequired,
|
|
@@ -27,24 +28,6 @@ import { getVersion } from './version.js'
|
|
|
27
28
|
|
|
28
29
|
const debugLog = createDebug('lint-staged')
|
|
29
30
|
|
|
30
|
-
/**
|
|
31
|
-
* Get the maximum length of a command-line argument string based on current platform
|
|
32
|
-
*
|
|
33
|
-
* https://serverfault.com/questions/69430/what-is-the-maximum-length-of-a-command-line-in-mac-os-x
|
|
34
|
-
* https://support.microsoft.com/en-us/help/830473/command-prompt-cmd-exe-command-line-string-limitation
|
|
35
|
-
* https://unix.stackexchange.com/a/120652
|
|
36
|
-
*/
|
|
37
|
-
const getMaxArgLength = () => {
|
|
38
|
-
switch (process.platform) {
|
|
39
|
-
case 'darwin':
|
|
40
|
-
return 262144
|
|
41
|
-
case 'win32':
|
|
42
|
-
return 8191
|
|
43
|
-
default:
|
|
44
|
-
return 131072
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
31
|
/**
|
|
49
32
|
* @typedef {(...any) => void} LogFunction
|
|
50
33
|
* @typedef {{ error: LogFunction, log: LogFunction, warn: LogFunction }} Logger
|
|
@@ -91,7 +74,7 @@ const lintStaged = async (
|
|
|
91
74
|
hideAll = false,
|
|
92
75
|
hideUnstaged = false,
|
|
93
76
|
hidePartiallyStaged = !(hideAll || hideUnstaged),
|
|
94
|
-
maxArgLength = getMaxArgLength()
|
|
77
|
+
maxArgLength = getMaxArgLength(),
|
|
95
78
|
quiet = false,
|
|
96
79
|
relative = false,
|
|
97
80
|
// Stashing should be disabled by default when the `diff` option is used
|
package/lib/runAll.js
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
|
|
5
|
-
import { chunkFiles } from './chunkFiles.js'
|
|
6
5
|
import { dim } from './colors.js'
|
|
7
6
|
import { createDebug } from './debug.js'
|
|
8
7
|
import { execGit } from './execGit.js'
|
|
@@ -114,7 +113,7 @@ export const runAll = async (
|
|
|
114
113
|
) => {
|
|
115
114
|
const thisLogger = configureLogger(logger, quiet)
|
|
116
115
|
|
|
117
|
-
debugLog('Running all
|
|
116
|
+
debugLog('Running all tasks...')
|
|
118
117
|
|
|
119
118
|
// Resolve relative CWD option
|
|
120
119
|
const hasExplicitCwd = !!cwd
|
|
@@ -204,91 +203,79 @@ export const runAll = async (
|
|
|
204
203
|
for (const [configPath, { config, files }] of Object.entries(filesByConfig)) {
|
|
205
204
|
const configName = configPath ? normalizePath(path.relative(cwd, configPath)) : 'Config object'
|
|
206
205
|
|
|
207
|
-
const stagedFileChunks = chunkFiles({ baseDir: topLevelDir, files, maxArgLength, relative })
|
|
208
|
-
|
|
209
206
|
// Use actual cwd if it's specified, or there's only a single config file.
|
|
210
207
|
// Otherwise use the directory of the config file for each config group,
|
|
211
208
|
// to make sure tasks are separated from each other.
|
|
212
209
|
const groupCwd = hasExplicitCwd || !hasMultipleConfigs ? cwd : path.dirname(configPath)
|
|
213
210
|
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
continueOnError,
|
|
233
|
-
cwd: groupCwd,
|
|
234
|
-
files: task.fileList,
|
|
235
|
-
topLevelDir,
|
|
236
|
-
verbose,
|
|
237
|
-
})
|
|
238
|
-
).then((subTasks) => {
|
|
239
|
-
// Add files from task to match set
|
|
240
|
-
task.fileList.forEach((file) => {
|
|
241
|
-
// Make sure relative files are normalized to the
|
|
242
|
-
// group cwd, because other there might be identical
|
|
243
|
-
// relative filenames in the entire set.
|
|
244
|
-
const normalizedFile = path.isAbsolute(file.filepath)
|
|
245
|
-
? file
|
|
246
|
-
: {
|
|
247
|
-
filepath: normalizePath(path.join(groupCwd, file.filepath)),
|
|
248
|
-
status: file.status,
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
matchedFiles.add(normalizedFile)
|
|
211
|
+
const configTasks = await Promise.all(
|
|
212
|
+
generateTasks({ config, cwd: groupCwd, files, relative }).map((task) =>
|
|
213
|
+
(isFunctionTask(task.commands)
|
|
214
|
+
? getFunctionTask({
|
|
215
|
+
abortController,
|
|
216
|
+
command: task.commands,
|
|
217
|
+
continueOnError,
|
|
218
|
+
files: task.fileList,
|
|
219
|
+
})
|
|
220
|
+
: getSpawnedTasks({
|
|
221
|
+
abortController,
|
|
222
|
+
commands: task.commands,
|
|
223
|
+
continueOnError,
|
|
224
|
+
cwd: groupCwd,
|
|
225
|
+
files: task.fileList,
|
|
226
|
+
maxArgLength,
|
|
227
|
+
topLevelDir,
|
|
228
|
+
verbose,
|
|
252
229
|
})
|
|
230
|
+
).then((subTasks) => {
|
|
231
|
+
// Add files from task to match set
|
|
232
|
+
task.fileList.forEach((file) => {
|
|
233
|
+
// Make sure relative files are normalized to the
|
|
234
|
+
// group cwd, because other there might be identical
|
|
235
|
+
// relative filenames in the entire set.
|
|
236
|
+
const normalizedFile = path.isAbsolute(file.filepath)
|
|
237
|
+
? file
|
|
238
|
+
: {
|
|
239
|
+
filepath: normalizePath(path.join(groupCwd, file.filepath)),
|
|
240
|
+
status: file.status,
|
|
241
|
+
}
|
|
253
242
|
|
|
254
|
-
|
|
255
|
-
|
|
243
|
+
matchedFiles.add(normalizedFile)
|
|
244
|
+
})
|
|
256
245
|
|
|
257
|
-
|
|
246
|
+
hasDeprecatedGitAdd =
|
|
247
|
+
hasDeprecatedGitAdd || subTasks.some((subTask) => subTask.title.includes('git add'))
|
|
258
248
|
|
|
259
|
-
|
|
260
|
-
title: `${task.pattern}${dim(
|
|
261
|
-
` — ${fileCount} ${fileCount === 1 ? 'file' : 'files'}`
|
|
262
|
-
)}`,
|
|
263
|
-
task: subTasks,
|
|
264
|
-
skip: () => {
|
|
265
|
-
// Skip task when no files matched
|
|
266
|
-
if (fileCount === 0) {
|
|
267
|
-
return `${task.pattern}${dim(' — no files')}`
|
|
268
|
-
}
|
|
249
|
+
const fileCount = task.fileList.length
|
|
269
250
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
251
|
+
return {
|
|
252
|
+
title: `${task.pattern}${dim(` — ${fileCount} ${fileCount === 1 ? 'file' : 'files'}`)}`,
|
|
253
|
+
task: subTasks,
|
|
254
|
+
skip: () => {
|
|
255
|
+
// Skip task when no files matched
|
|
256
|
+
if (fileCount === 0) {
|
|
257
|
+
return `${task.pattern}${dim(' — no files')}`
|
|
258
|
+
}
|
|
276
259
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
`${configName}${dim(` — ${files.length} ${files.length > 1 ? 'files' : 'file'}`)}` +
|
|
280
|
-
(chunkCount > 1 ? dim(` (chunk ${index + 1}/${chunkCount})...`) : ''),
|
|
281
|
-
task: taskChunk,
|
|
282
|
-
skip: () => {
|
|
283
|
-
// Skip chunk when no every task is skipped (due to no matches)
|
|
284
|
-
if (taskChunk.every((task) => task.skip())) {
|
|
285
|
-
return `${configName}${dim(' — no tasks to run')}`
|
|
260
|
+
return false
|
|
261
|
+
},
|
|
286
262
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
263
|
+
})
|
|
264
|
+
)
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
tasks.push({
|
|
268
|
+
title: `${configName}${dim(` — ${files.length} ${files.length > 1 ? 'files' : 'file'}`)}`,
|
|
269
|
+
task: configTasks,
|
|
270
|
+
skip: () => {
|
|
271
|
+
// Skip chunk when no every task is skipped (due to no matches)
|
|
272
|
+
if (configTasks.every((task) => task.skip())) {
|
|
273
|
+
return `${configName}${dim(' — no tasks to run')}`
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return false
|
|
277
|
+
},
|
|
278
|
+
})
|
|
292
279
|
}
|
|
293
280
|
|
|
294
281
|
if (hasDeprecatedGitAdd) {
|
|
@@ -300,16 +287,6 @@ export const runAll = async (
|
|
|
300
287
|
return ctx
|
|
301
288
|
}
|
|
302
289
|
|
|
303
|
-
// Chunk matched files for better Windows compatibility
|
|
304
|
-
/** @type {import('./getStagedFiles.js').StagedFile[][]} */
|
|
305
|
-
const matchedFileChunks = chunkFiles({
|
|
306
|
-
// matched files are relative to `cwd`, not `topLevelDir`, when `relative` is used
|
|
307
|
-
baseDir: cwd,
|
|
308
|
-
files: Array.from(matchedFiles),
|
|
309
|
-
maxArgLength,
|
|
310
|
-
relative: false,
|
|
311
|
-
})
|
|
312
|
-
|
|
313
290
|
const git = new GitWorkflow({
|
|
314
291
|
allowEmpty,
|
|
315
292
|
diff,
|
|
@@ -317,7 +294,8 @@ export const runAll = async (
|
|
|
317
294
|
failOnChanges,
|
|
318
295
|
gitConfigDir,
|
|
319
296
|
logger: thisLogger,
|
|
320
|
-
|
|
297
|
+
matchedFiles,
|
|
298
|
+
maxArgLength,
|
|
321
299
|
topLevelDir,
|
|
322
300
|
})
|
|
323
301
|
|
package/lib/runParallelTasks.js
CHANGED
|
@@ -6,15 +6,15 @@ const debugLog = createDebug('lint-staged:runParallelTasks')
|
|
|
6
6
|
|
|
7
7
|
/** @param {Number | boolean} options.concurrent Boolean value for whether to run concurrently */
|
|
8
8
|
export const parseConcurrency = (concurrent) => {
|
|
9
|
-
if (concurrent ===
|
|
10
|
-
return
|
|
9
|
+
if (concurrent === true || concurrent === Infinity) {
|
|
10
|
+
return Infinity
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
if (concurrent ===
|
|
14
|
-
return
|
|
13
|
+
if (concurrent === false || concurrent < 1 || !Number.isInteger(concurrent)) {
|
|
14
|
+
return 1
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
return
|
|
17
|
+
return concurrent
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
/**
|
package/lib/validateOptions.js
CHANGED
|
@@ -8,6 +8,9 @@ import { InvalidOptionsError } from './symbols.js'
|
|
|
8
8
|
|
|
9
9
|
const debugLog = createDebug('lint-staged:validateOptions')
|
|
10
10
|
|
|
11
|
+
/** @type {(value: number) => boolean} */
|
|
12
|
+
const isValidIntegerValue = (value) => Number.isInteger(value) || value === Infinity
|
|
13
|
+
|
|
11
14
|
/**
|
|
12
15
|
* Validate lint-staged options, either from the Node.js API or the command line flags.
|
|
13
16
|
* @param {*} options
|
|
@@ -17,26 +20,41 @@ const debugLog = createDebug('lint-staged:validateOptions')
|
|
|
17
20
|
export const validateOptions = async (options = {}, logger) => {
|
|
18
21
|
debugLog('Validating options...')
|
|
19
22
|
|
|
23
|
+
const { concurrent, cwd, maxArgLength } = options
|
|
24
|
+
|
|
20
25
|
if (
|
|
21
|
-
|
|
22
|
-
typeof
|
|
23
|
-
(!
|
|
26
|
+
concurrent !== undefined &&
|
|
27
|
+
typeof concurrent !== 'boolean' &&
|
|
28
|
+
(!isValidIntegerValue(concurrent) || concurrent < 1)
|
|
24
29
|
) {
|
|
25
|
-
logger.error(
|
|
30
|
+
logger.error(
|
|
31
|
+
invalidOption('concurrent', `${concurrent}`, 'Must be boolean, positive integer or Infinite')
|
|
32
|
+
)
|
|
26
33
|
throw InvalidOptionsError
|
|
27
34
|
}
|
|
28
35
|
|
|
29
36
|
/** Ensure the passed cwd option exists; it might also be relative */
|
|
30
|
-
if (typeof
|
|
37
|
+
if (typeof cwd === 'string') {
|
|
31
38
|
try {
|
|
32
|
-
const resolved = path.resolve(
|
|
39
|
+
const resolved = path.resolve(cwd)
|
|
33
40
|
await fs.access(resolved, constants.F_OK)
|
|
34
41
|
} catch (error) {
|
|
35
42
|
debugLog('Failed to validate options: %o', options)
|
|
36
|
-
logger.error(invalidOption('cwd',
|
|
43
|
+
logger.error(invalidOption('cwd', cwd, error.message))
|
|
37
44
|
throw InvalidOptionsError
|
|
38
45
|
}
|
|
39
46
|
}
|
|
40
47
|
|
|
48
|
+
if (
|
|
49
|
+
maxArgLength !== undefined &&
|
|
50
|
+
maxArgLength !== null &&
|
|
51
|
+
(!isValidIntegerValue(maxArgLength) || maxArgLength < 1)
|
|
52
|
+
) {
|
|
53
|
+
logger.error(
|
|
54
|
+
invalidOption('maxArgLength', `${maxArgLength}`, 'Must be positive integer, or Infinite')
|
|
55
|
+
)
|
|
56
|
+
throw InvalidOptionsError
|
|
57
|
+
}
|
|
58
|
+
|
|
41
59
|
debugLog('Validated options: %o', options)
|
|
42
60
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lint-staged",
|
|
3
|
-
"version": "17.
|
|
3
|
+
"version": "17.2.0",
|
|
4
4
|
"description": "Lint files staged by git",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -69,8 +69,8 @@
|
|
|
69
69
|
"cross-env": "10.1.0",
|
|
70
70
|
"husky": "9.1.7",
|
|
71
71
|
"mock-stdin": "1.0.0",
|
|
72
|
-
"oxfmt": "0.
|
|
73
|
-
"oxlint": "1.
|
|
72
|
+
"oxfmt": "0.60.0",
|
|
73
|
+
"oxlint": "1.75.0",
|
|
74
74
|
"semver": "7.8.5",
|
|
75
75
|
"vitest": "4.1.10"
|
|
76
76
|
},
|
package/lib/chunkFiles.js
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import path from 'node:path'
|
|
2
|
-
|
|
3
|
-
import { createDebug } from './debug.js'
|
|
4
|
-
import { normalizePath } from './normalizePath.js'
|
|
5
|
-
|
|
6
|
-
const debugLog = createDebug('lint-staged:chunkFiles')
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Chunk array into sub-arrays
|
|
10
|
-
* @param {Array} arr
|
|
11
|
-
* @param {Number} chunkCount
|
|
12
|
-
* @returns {Array<Array>}
|
|
13
|
-
*/
|
|
14
|
-
const chunkArray = (arr, chunkCount) => {
|
|
15
|
-
if (chunkCount === 1) return [arr]
|
|
16
|
-
const chunked = []
|
|
17
|
-
let position = 0
|
|
18
|
-
for (let i = 0; i < chunkCount; i++) {
|
|
19
|
-
const chunkLength = Math.ceil((arr.length - position) / (chunkCount - i))
|
|
20
|
-
chunked.push([])
|
|
21
|
-
chunked[i] = arr.slice(position, chunkLength + position)
|
|
22
|
-
position += chunkLength
|
|
23
|
-
}
|
|
24
|
-
return chunked
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Chunk files into sub-arrays based on the length of the resulting argument string
|
|
29
|
-
*
|
|
30
|
-
* @typedef {import('./getStagedFiles.js').StagedFile[]} StagedFile
|
|
31
|
-
*
|
|
32
|
-
* @param {Object} opts
|
|
33
|
-
* @param {Array<StagedFile>} opts.files
|
|
34
|
-
* @param {String} [opts.baseDir] The optional base directory to resolve relative paths.
|
|
35
|
-
* @param {number} [opts.maxArgLength] the maximum argument string length
|
|
36
|
-
* @param {Boolean} [opts.relative] whether files are relative to `topLevelDir` or should be resolved as absolute
|
|
37
|
-
* @returns {Array<Array<StagedFile>>}
|
|
38
|
-
*/
|
|
39
|
-
export const chunkFiles = ({ files, baseDir, maxArgLength = null, relative = false }) => {
|
|
40
|
-
const normalizedFiles = files.map((file) => {
|
|
41
|
-
return {
|
|
42
|
-
filepath: normalizePath(
|
|
43
|
-
relative || !baseDir ? file.filepath : path.resolve(baseDir, file.filepath)
|
|
44
|
-
),
|
|
45
|
-
status: file.status,
|
|
46
|
-
}
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
if (!maxArgLength) {
|
|
50
|
-
debugLog('Skip chunking files because of undefined maxArgLength')
|
|
51
|
-
return [normalizedFiles] // wrap in an array to return a single chunk
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** Calculate total character length of all filepaths, with added spaces in between */
|
|
55
|
-
const fileListLength =
|
|
56
|
-
normalizedFiles.reduce((sum, file) => sum + file.filepath.length, 0) +
|
|
57
|
-
Math.max(normalizedFiles.length - 1, 0)
|
|
58
|
-
|
|
59
|
-
debugLog(
|
|
60
|
-
`Resolved an argument string length of ${fileListLength} characters from ${normalizedFiles.length} files`
|
|
61
|
-
)
|
|
62
|
-
const chunkCount = Math.min(Math.ceil(fileListLength / maxArgLength), normalizedFiles.length)
|
|
63
|
-
debugLog(`Creating ${chunkCount} chunks for maxArgLength of ${maxArgLength}`)
|
|
64
|
-
return chunkArray(normalizedFiles, chunkCount)
|
|
65
|
-
}
|