lint-staged 12.4.3 → 13.0.1
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 +83 -9
- package/bin/lint-staged.js +18 -6
- package/lib/chunkFiles.js +1 -1
- package/lib/dynamicImport.js +1 -1
- package/lib/execGit.js +1 -1
- package/lib/file.js +1 -1
- package/lib/generateTasks.js +1 -1
- package/lib/getStagedFiles.js +18 -7
- package/lib/gitWorkflow.js +1 -1
- package/lib/groupFilesByConfig.js +5 -9
- package/lib/index.js +23 -18
- package/lib/messages.js +8 -2
- package/lib/printTaskOutput.js +1 -1
- package/lib/resolveConfig.js +1 -1
- package/lib/resolveGitRepo.js +2 -2
- package/lib/resolveTaskFn.js +42 -23
- package/lib/runAll.js +30 -26
- package/lib/searchConfigs.js +4 -6
- package/lib/state.js +3 -0
- package/lib/validateOptions.js +3 -2
- package/package.json +9 -10
package/README.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
Run linters against staged git files and don't let :poop: slip into your code base!
|
|
4
4
|
|
|
5
|
+
```bash
|
|
6
|
+
npm install --save-dev lint-staged # requires further setup
|
|
7
|
+
```
|
|
8
|
+
|
|
5
9
|
```
|
|
6
10
|
$ git commit
|
|
7
11
|
|
|
@@ -44,13 +48,17 @@ This project contains a script that will run arbitrary shell tasks with a list o
|
|
|
44
48
|
|
|
45
49
|
## Installation and setup
|
|
46
50
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
```bash
|
|
50
|
-
npx mrm@2 lint-staged
|
|
51
|
-
```
|
|
51
|
+
To install _lint-staged_ in the recommended way, you need to:
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
1. Install _lint-staged_ itself:
|
|
54
|
+
- `npm install --save-dev lint-staged`
|
|
55
|
+
1. Set up the `pre-commit` git hook to run _lint-staged_
|
|
56
|
+
- [Husky](https://github.com/typicode/husky) is a popular choice for configuring git hooks
|
|
57
|
+
- Read more about git hooks [here](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks)
|
|
58
|
+
1. Install some linters, like [ESLint](https://eslint.org) or [Prettier](https://prettier.io)
|
|
59
|
+
1. Configure _lint-staged_ to run linters and other tasks:
|
|
60
|
+
- for example: `{ "*.js": "eslint" }` to run ESLint for all staged JS files
|
|
61
|
+
- See [Configuration](#Configuration) for more info
|
|
54
62
|
|
|
55
63
|
Don't forget to commit changes to `package.json` and `.husky` to share this setup with your team!
|
|
56
64
|
|
|
@@ -64,6 +72,10 @@ See [Releases](https://github.com/okonet/lint-staged/releases).
|
|
|
64
72
|
|
|
65
73
|
### Migration
|
|
66
74
|
|
|
75
|
+
#### v13
|
|
76
|
+
|
|
77
|
+
- Since `v13.0.0` _lint-staged_ no longer supports Node.js 12. Please upgrade your Node.js version to at least `14.13.1`, or `16.0.0` onward.
|
|
78
|
+
|
|
67
79
|
#### v12
|
|
68
80
|
|
|
69
81
|
- Since `v12.0.0` _lint-staged_ is a pure ESM module, so make sure your Node.js version is at least `12.20.0`, `14.13.1`, or `16.0.0`. Read more about ESM modules from the official [Node.js Documentation site here](https://nodejs.org/api/esm.html#introduction).
|
|
@@ -92,6 +104,10 @@ Options:
|
|
|
92
104
|
-c, --config [path] path to configuration file, or - to read from stdin
|
|
93
105
|
--cwd [path] run all tasks in specific directory, instead of the current
|
|
94
106
|
-d, --debug print additional debug information (default: false)
|
|
107
|
+
--diff [string] override the default "--staged" flag of "git diff" to get list of files. Implies
|
|
108
|
+
"--no-stash".
|
|
109
|
+
--diff-filter [string] override the default "--diff-filter=ACMR" flag of "git diff" to get list of files
|
|
110
|
+
--max-arg-length [number] maximum length of the command-line argument string (default: 0)
|
|
95
111
|
--no-stash disable the backup stash, and do not revert in case of errors
|
|
96
112
|
-q, --quiet disable lint-staged’s own console output (default: false)
|
|
97
113
|
-r, --relative pass relative filepaths to tasks (default: false)
|
|
@@ -102,7 +118,7 @@ Options:
|
|
|
102
118
|
```
|
|
103
119
|
|
|
104
120
|
- **`--allow-empty`**: By default, when linter tasks undo all staged changes, lint-staged will exit with an error and abort the commit. Use this flag to allow creating empty git commits.
|
|
105
|
-
- **`--concurrent [number|boolean]`**: Controls the concurrency of tasks being run by lint-staged. **NOTE**: This does NOT affect the concurrency of subtasks (they will always be run sequentially). Possible values are:
|
|
121
|
+
- **`--concurrent [number|boolean]`**: Controls the [concurrency of tasks](#task-concurrency) being run by lint-staged. **NOTE**: This does NOT affect the concurrency of subtasks (they will always be run sequentially). Possible values are:
|
|
106
122
|
- `false`: Run all tasks serially
|
|
107
123
|
- `true` (default) : _Infinite_ concurrency. Runs as many tasks in parallel as possible.
|
|
108
124
|
- `{number}`: Run the specified number of tasks in parallel, where `1` is equivalent to `false`.
|
|
@@ -111,6 +127,9 @@ Options:
|
|
|
111
127
|
- **`--debug`**: Run in debug mode. When set, it does the following:
|
|
112
128
|
- uses [debug](https://github.com/visionmedia/debug) internally to log additional information about staged files, commands being executed, location of binaries, etc. Debug logs, which are automatically enabled by passing the flag, can also be enabled by setting the environment variable `$DEBUG` to `lint-staged*`.
|
|
113
129
|
- uses [`verbose` renderer](https://github.com/SamVerschueren/listr-verbose-renderer) for `listr`; this causes serial, uncoloured output to the terminal, instead of the default (beautified, dynamic) output.
|
|
130
|
+
- **`--diff`**: By default linters are filtered against all files staged in git, generated from `git diff --staged`. This option allows you to override the `--staged` flag with arbitrary revisions. For example to get a list of changed files between two branches, use `--diff="branch1...branch2"`. You can also read more from about [git diff](https://git-scm.com/docs/git-diff) and [gitrevisions](https://git-scm.com/docs/gitrevisions).
|
|
131
|
+
- **`--diff-filter`**: By default only files that are _added_, _copied_, _modified_, or _renamed_ are included. Use this flag to override the default `ACMR` value with something else: _added_ (`A`), _copied_ (`C`), _deleted_ (`D`), _modified_ (`M`), _renamed_ (`R`), _type changed_ (`T`), _unmerged_ (`U`), _unknown_ (`X`), or _pairing broken_ (`B`). See also the `git diff` docs for [--diff-filter](https://git-scm.com/docs/git-diff#Documentation/git-diff.txt---diff-filterACDMRTUXB82308203).
|
|
132
|
+
- **`--max-arg-length`**: long commands (a lot of files) are automatically split into multiple chunks when it detects the current shell cannot handle them. Use this flag to override the maximum length of the generated command string.
|
|
114
133
|
- **`--no-stash`**: By default a backup stash will be created before running the tasks, and all task modifications will be reverted in case of an error. This option will disable creating the stash, and instead leave all modifications in the index when aborting the commit.
|
|
115
134
|
- **`--quiet`**: Supress all CLI output, except from tasks.
|
|
116
135
|
- **`--relative`**: Pass filepaths relative to `process.cwd()` (where `lint-staged` runs) to tasks. Default is `false`.
|
|
@@ -119,7 +138,7 @@ Options:
|
|
|
119
138
|
|
|
120
139
|
## Configuration
|
|
121
140
|
|
|
122
|
-
|
|
141
|
+
_Lint-staged_ can be configured in many ways:
|
|
123
142
|
|
|
124
143
|
- `lint-staged` object in your `package.json`
|
|
125
144
|
- `.lintstagedrc` file in JSON or YML format, or you can be explicit with the file extension:
|
|
@@ -162,6 +181,37 @@ So, considering you did `git add file1.ext file2.ext`, lint-staged will run the
|
|
|
162
181
|
|
|
163
182
|
`your-cmd file1.ext file2.ext`
|
|
164
183
|
|
|
184
|
+
### Task concurrency
|
|
185
|
+
|
|
186
|
+
By default _lint-staged_ will run configured tasks concurrently. This means that for every glob, all the commands will be started at the same time. With the following config, both `eslint` and `prettier` will run at the same time:
|
|
187
|
+
|
|
188
|
+
```json
|
|
189
|
+
{
|
|
190
|
+
"*.ts": "eslint",
|
|
191
|
+
"*.md": "prettier --list-different"
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
This is typically not a problem since the globs do not overlap, and the commands do not make changes to the files, but only report possible errors (aborting the git commit). If you want to run multiple commands for the same set of files, you can use the array syntax to make sure commands are run in order. In the following example, `prettier` will run for both globs, and in addition `eslint` will run for `*.ts` files _after_ it. Both sets of commands (for each glob) are still started at the same time (but do not overlap).
|
|
196
|
+
|
|
197
|
+
```json
|
|
198
|
+
{
|
|
199
|
+
"*.ts": ["prettier --list-different", "eslint"],
|
|
200
|
+
"*.md": "prettier --list-different"
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Pay extra attention when the configured globs overlap, and tasks make edits to files. For example, in this configuration `prettier` and `eslint` might try to make changes to the same `*.ts` file at the same time, causing a _race condition_:
|
|
205
|
+
|
|
206
|
+
```json
|
|
207
|
+
{
|
|
208
|
+
"*": "prettier --write",
|
|
209
|
+
"*.ts": "eslint --fix"
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
If necessary, you can limit the concurrency using `--concurrent <number>` or disable it entirely with `--concurrent false`.
|
|
214
|
+
|
|
165
215
|
## Filtering files
|
|
166
216
|
|
|
167
217
|
Linter commands work on a subset of all staged files, defined by a _glob pattern_. lint-staged uses [micromatch](https://github.com/micromatch/micromatch) for matching files with the following rules:
|
|
@@ -596,7 +646,7 @@ const success = await lintStaged({
|
|
|
596
646
|
relative: false,
|
|
597
647
|
shell: false,
|
|
598
648
|
stash: true,
|
|
599
|
-
verbose: false
|
|
649
|
+
verbose: false,
|
|
600
650
|
})
|
|
601
651
|
```
|
|
602
652
|
|
|
@@ -713,6 +763,30 @@ Example repo: [sudo-suhas/lint-staged-django-react-demo](https://github.com/sudo
|
|
|
713
763
|
|
|
714
764
|
</details>
|
|
715
765
|
|
|
766
|
+
### Can I run `lint-staged` in CI, or when there are no staged files?
|
|
767
|
+
|
|
768
|
+
<details>
|
|
769
|
+
<summary>Click to expand</summary>
|
|
770
|
+
|
|
771
|
+
Lint-staged will by default run against files staged in git, and should be run during the git pre-commit hook, for example. It's also possible to override this default behaviour and run against files in a specific diff, for example
|
|
772
|
+
all changed files between two different branches. If you want to run _lint-staged_ in the CI, maybe you can set it up to compare the branch in a _Pull Request_/_Merge Request_ to the target branch.
|
|
773
|
+
|
|
774
|
+
Try out the `git diff` command until you are satisfied with the result, for example:
|
|
775
|
+
|
|
776
|
+
```
|
|
777
|
+
git diff --diff-filter=ACMR --name-only master...my-branch
|
|
778
|
+
```
|
|
779
|
+
|
|
780
|
+
This will print a list of _added_, _changed_, _modified_, and _renamed_ files between `master` and `my-branch`.
|
|
781
|
+
|
|
782
|
+
You can then run lint-staged against the same files with:
|
|
783
|
+
|
|
784
|
+
```
|
|
785
|
+
npx lint-staged --diff="master...my-branch"
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
</details>
|
|
789
|
+
|
|
716
790
|
### Can I use `lint-staged` with `ng lint`
|
|
717
791
|
|
|
718
792
|
<details>
|
package/bin/lint-staged.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import fs from 'fs'
|
|
4
|
-
import path from 'path'
|
|
5
|
-
import { fileURLToPath } from 'url'
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
6
|
|
|
7
|
+
import { isColorSupported } from 'colorette'
|
|
7
8
|
import { Option, program } from 'commander'
|
|
8
9
|
import debug from 'debug'
|
|
9
|
-
import supportsColor from 'supports-color'
|
|
10
10
|
|
|
11
11
|
import lintStaged from '../lib/index.js'
|
|
12
12
|
import { CONFIG_STDIN_ERROR } from '../lib/messages.js'
|
|
13
13
|
|
|
14
14
|
// Force colors for packages that depend on https://www.npmjs.com/package/supports-color
|
|
15
|
-
if (
|
|
16
|
-
process.env.FORCE_COLOR =
|
|
15
|
+
if (isColorSupported) {
|
|
16
|
+
process.env.FORCE_COLOR = '1'
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
// Do not terminate main Listr process on SIGINT
|
|
@@ -42,6 +42,16 @@ cli.option('--cwd [path]', 'run all tasks in specific directory, instead of the
|
|
|
42
42
|
|
|
43
43
|
cli.option('-d, --debug', 'print additional debug information', false)
|
|
44
44
|
|
|
45
|
+
cli.option(
|
|
46
|
+
'--diff [string]',
|
|
47
|
+
'override the default "--staged" flag of "git diff" to get list of files. Implies "--no-stash".'
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
cli.option(
|
|
51
|
+
'--diff-filter [string]',
|
|
52
|
+
'override the default "--diff-filter=ACMR" flag of "git diff" to get list of files'
|
|
53
|
+
)
|
|
54
|
+
|
|
45
55
|
cli.option('--max-arg-length [number]', 'maximum length of the command-line argument string', 0)
|
|
46
56
|
|
|
47
57
|
/**
|
|
@@ -86,6 +96,8 @@ const options = {
|
|
|
86
96
|
configPath: cliOptions.config,
|
|
87
97
|
cwd: cliOptions.cwd,
|
|
88
98
|
debug: !!cliOptions.debug,
|
|
99
|
+
diff: cliOptions.diff,
|
|
100
|
+
diffFilter: cliOptions.diffFilter,
|
|
89
101
|
maxArgLength: cliOptions.maxArgLength || undefined,
|
|
90
102
|
quiet: !!cliOptions.quiet,
|
|
91
103
|
relative: !!cliOptions.relative,
|
package/lib/chunkFiles.js
CHANGED
package/lib/dynamicImport.js
CHANGED
package/lib/execGit.js
CHANGED
package/lib/file.js
CHANGED
package/lib/generateTasks.js
CHANGED
package/lib/getStagedFiles.js
CHANGED
|
@@ -1,18 +1,29 @@
|
|
|
1
|
-
import path from 'path'
|
|
1
|
+
import path from 'node:path'
|
|
2
2
|
|
|
3
3
|
import normalize from 'normalize-path'
|
|
4
4
|
|
|
5
5
|
import { execGit } from './execGit.js'
|
|
6
6
|
import { parseGitZOutput } from './parseGitZOutput.js'
|
|
7
7
|
|
|
8
|
-
export const getStagedFiles = async ({ cwd = process.cwd() } = {}) => {
|
|
8
|
+
export const getStagedFiles = async ({ cwd = process.cwd(), diff, diffFilter } = {}) => {
|
|
9
9
|
try {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Docs for --diff-filter option:
|
|
12
|
+
* @see https://git-scm.com/docs/git-diff#Documentation/git-diff.txt---diff-filterACDMRTUXB82308203
|
|
13
|
+
*/
|
|
14
|
+
const diffFilterArg = diffFilter !== undefined ? diffFilter.trim() : 'ACMR'
|
|
15
15
|
|
|
16
|
+
/** Use `--diff branch1...branch2` or `--diff="branch1 branch2", or fall back to default staged files */
|
|
17
|
+
const diffArgs = diff !== undefined ? diff.trim().split(' ') : ['--staged']
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Docs for -z option:
|
|
21
|
+
* @see https://git-scm.com/docs/git-diff#Documentation/git-diff.txt--z
|
|
22
|
+
*/
|
|
23
|
+
const lines = await execGit(
|
|
24
|
+
['diff', '--name-only', '-z', `--diff-filter=${diffFilterArg}`, ...diffArgs],
|
|
25
|
+
{ cwd }
|
|
26
|
+
)
|
|
16
27
|
if (!lines) return []
|
|
17
28
|
|
|
18
29
|
return parseGitZOutput(lines).map((file) => normalize(path.resolve(cwd, file)))
|
package/lib/gitWorkflow.js
CHANGED
|
@@ -1,23 +1,19 @@
|
|
|
1
|
-
import path from 'path'
|
|
1
|
+
import path from 'node:path'
|
|
2
2
|
|
|
3
3
|
import debug from 'debug'
|
|
4
4
|
|
|
5
|
-
import { ConfigObjectSymbol } from './searchConfigs.js'
|
|
6
|
-
|
|
7
5
|
const debugLog = debug('lint-staged:groupFilesByConfig')
|
|
8
6
|
|
|
9
|
-
export const groupFilesByConfig = async ({ configs, files }) => {
|
|
7
|
+
export const groupFilesByConfig = async ({ configs, files, singleConfigMode }) => {
|
|
10
8
|
debugLog('Grouping %d files by %d configurations', files.length, Object.keys(configs).length)
|
|
11
9
|
|
|
12
10
|
const filesSet = new Set(files)
|
|
13
11
|
const filesByConfig = {}
|
|
14
12
|
|
|
15
13
|
/** Configs are sorted deepest first by `searchConfigs` */
|
|
16
|
-
for (const filepath of
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
/** When passed an explicit config object via the Node.js API, skip logic */
|
|
20
|
-
if (filepath === ConfigObjectSymbol) {
|
|
14
|
+
for (const [filepath, config] of Object.entries(configs)) {
|
|
15
|
+
/** When passed an explicit config object via the Node.js API‚ or an explicit path, skip logic */
|
|
16
|
+
if (singleConfigMode) {
|
|
21
17
|
filesByConfig[filepath] = { config, files }
|
|
22
18
|
break
|
|
23
19
|
}
|
package/lib/index.js
CHANGED
|
@@ -49,6 +49,8 @@ const getMaxArgLength = () => {
|
|
|
49
49
|
* @param {string} [options.configPath] - Path to configuration file
|
|
50
50
|
* @param {Object} [options.cwd] - Current working directory
|
|
51
51
|
* @param {boolean} [options.debug] - Enable debug mode
|
|
52
|
+
* @param {string} [options.diff] - Override the default "--staged" flag of "git diff" to get list of files
|
|
53
|
+
* @param {string} [options.diffFilter] - Override the default "--diff-filter=ACMR" flag of "git diff" to get list of files
|
|
52
54
|
* @param {number} [options.maxArgLength] - Maximum argument string length
|
|
53
55
|
* @param {boolean} [options.quiet] - Disable lint-staged’s own console output
|
|
54
56
|
* @param {boolean} [options.relative] - Pass relative filepaths to tasks
|
|
@@ -67,6 +69,8 @@ const lintStaged = async (
|
|
|
67
69
|
configPath,
|
|
68
70
|
cwd,
|
|
69
71
|
debug = false,
|
|
72
|
+
diff,
|
|
73
|
+
diffFilter,
|
|
70
74
|
maxArgLength = getMaxArgLength() / 2,
|
|
71
75
|
quiet = false,
|
|
72
76
|
relative = false,
|
|
@@ -82,29 +86,30 @@ const lintStaged = async (
|
|
|
82
86
|
debugLog('Unset GIT_LITERAL_PATHSPECS (was `%s`)', process.env.GIT_LITERAL_PATHSPECS)
|
|
83
87
|
delete process.env.GIT_LITERAL_PATHSPECS
|
|
84
88
|
|
|
89
|
+
const options = {
|
|
90
|
+
allowEmpty,
|
|
91
|
+
concurrent,
|
|
92
|
+
configObject,
|
|
93
|
+
configPath,
|
|
94
|
+
cwd,
|
|
95
|
+
debug,
|
|
96
|
+
diff,
|
|
97
|
+
diffFilter,
|
|
98
|
+
maxArgLength,
|
|
99
|
+
quiet,
|
|
100
|
+
relative,
|
|
101
|
+
shell,
|
|
102
|
+
stash,
|
|
103
|
+
verbose,
|
|
104
|
+
}
|
|
105
|
+
|
|
85
106
|
try {
|
|
86
|
-
const ctx = await runAll(
|
|
87
|
-
{
|
|
88
|
-
allowEmpty,
|
|
89
|
-
concurrent,
|
|
90
|
-
configObject,
|
|
91
|
-
configPath,
|
|
92
|
-
cwd,
|
|
93
|
-
debug,
|
|
94
|
-
maxArgLength,
|
|
95
|
-
quiet,
|
|
96
|
-
relative,
|
|
97
|
-
shell,
|
|
98
|
-
stash,
|
|
99
|
-
verbose,
|
|
100
|
-
},
|
|
101
|
-
logger
|
|
102
|
-
)
|
|
107
|
+
const ctx = await runAll(options, logger)
|
|
103
108
|
debugLog('Tasks were executed successfully!')
|
|
104
109
|
printTaskOutput(ctx, logger)
|
|
105
110
|
return true
|
|
106
111
|
} catch (runAllError) {
|
|
107
|
-
if (runAllError
|
|
112
|
+
if (runAllError?.ctx?.errors) {
|
|
108
113
|
const { ctx } = runAllError
|
|
109
114
|
|
|
110
115
|
if (ctx.errors.has(ConfigNotFoundError)) {
|
package/lib/messages.js
CHANGED
|
@@ -28,8 +28,14 @@ export const NO_STAGED_FILES = `${info} No staged files found.`
|
|
|
28
28
|
|
|
29
29
|
export const NO_TASKS = `${info} No staged files match any configured task.`
|
|
30
30
|
|
|
31
|
-
export const skippingBackup = (hasInitialCommit) => {
|
|
32
|
-
const reason =
|
|
31
|
+
export const skippingBackup = (hasInitialCommit, diff) => {
|
|
32
|
+
const reason =
|
|
33
|
+
diff !== undefined
|
|
34
|
+
? '`--diff` was used'
|
|
35
|
+
: hasInitialCommit
|
|
36
|
+
? '`--no-stash` was used'
|
|
37
|
+
: 'there’s no initial commit yet'
|
|
38
|
+
|
|
33
39
|
return yellow(`${warning} Skipping backup because ${reason}.\n`)
|
|
34
40
|
}
|
|
35
41
|
|
package/lib/printTaskOutput.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export const printTaskOutput = (ctx = {}, logger) => {
|
|
7
7
|
if (!Array.isArray(ctx.output)) return
|
|
8
|
-
const log = ctx.errors
|
|
8
|
+
const log = ctx.errors?.size > 0 ? logger.error : logger.log
|
|
9
9
|
for (const line of ctx.output) {
|
|
10
10
|
log(line)
|
|
11
11
|
}
|
package/lib/resolveConfig.js
CHANGED
package/lib/resolveGitRepo.js
CHANGED
package/lib/resolveTaskFn.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { redBright, dim } from 'colorette'
|
|
2
|
-
import execa from 'execa'
|
|
2
|
+
import { execa, execaCommand } from 'execa'
|
|
3
3
|
import debug from 'debug'
|
|
4
4
|
import { parseArgsStringToArgv } from 'string-argv'
|
|
5
5
|
import pidTree from 'pidtree'
|
|
@@ -8,7 +8,7 @@ import { error, info } from './figures.js'
|
|
|
8
8
|
import { getInitialState } from './state.js'
|
|
9
9
|
import { TaskError } from './symbols.js'
|
|
10
10
|
|
|
11
|
-
const
|
|
11
|
+
const TASK_ERROR = 'lint-staged:taskError'
|
|
12
12
|
|
|
13
13
|
const debugLog = debug('lint-staged:resolveTaskFn')
|
|
14
14
|
|
|
@@ -46,37 +46,52 @@ const handleOutput = (command, result, ctx, isError = false) => {
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Kill an execa process along with all its child processes.
|
|
51
|
+
* @param {execa.ExecaChildProcess<string>} execaProcess
|
|
52
|
+
*/
|
|
53
|
+
const killExecaProcess = async (execaProcess) => {
|
|
54
|
+
try {
|
|
55
|
+
const childPids = await pidTree(execaProcess.pid)
|
|
56
|
+
for (const childPid of childPids) {
|
|
57
|
+
try {
|
|
58
|
+
process.kill(childPid)
|
|
59
|
+
} catch (error) {
|
|
60
|
+
debugLog(`Failed to kill process with pid "%d": %o`, childPid, error)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
} catch (error) {
|
|
64
|
+
// Suppress "No matching pid found" error. This probably means
|
|
65
|
+
// the process already died before executing.
|
|
66
|
+
debugLog(`Failed to kill process with pid "%d": %o`, execaProcess.pid, error)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// The execa process is killed separately in order to get the `KILLED` status.
|
|
70
|
+
execaProcess.kill()
|
|
71
|
+
}
|
|
72
|
+
|
|
49
73
|
/**
|
|
50
74
|
* Interrupts the execution of the execa process that we spawned if
|
|
51
75
|
* another task adds an error to the context.
|
|
52
76
|
*
|
|
53
77
|
* @param {Object} ctx
|
|
54
78
|
* @param {execa.ExecaChildProcess<string>} execaChildProcess
|
|
55
|
-
* @returns {
|
|
79
|
+
* @returns {() => Promise<void>} Function that clears the interval that
|
|
56
80
|
* checks the context.
|
|
57
81
|
*/
|
|
58
82
|
const interruptExecutionOnError = (ctx, execaChildProcess) => {
|
|
59
|
-
let
|
|
60
|
-
|
|
61
|
-
async function loop() {
|
|
62
|
-
if (ctx.errors.size > 0) {
|
|
63
|
-
clearInterval(loopIntervalId)
|
|
64
|
-
|
|
65
|
-
const childPids = await pidTree(execaChildProcess.pid)
|
|
66
|
-
for (const pid of childPids) {
|
|
67
|
-
process.kill(pid)
|
|
68
|
-
}
|
|
83
|
+
let killPromise
|
|
69
84
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
85
|
+
const errorListener = async () => {
|
|
86
|
+
killPromise = killExecaProcess(execaChildProcess)
|
|
87
|
+
await killPromise
|
|
74
88
|
}
|
|
75
89
|
|
|
76
|
-
|
|
90
|
+
ctx.events.on(TASK_ERROR, errorListener, { once: true })
|
|
77
91
|
|
|
78
|
-
return () => {
|
|
79
|
-
|
|
92
|
+
return async () => {
|
|
93
|
+
ctx.events.off(TASK_ERROR, errorListener)
|
|
94
|
+
await killPromise
|
|
80
95
|
}
|
|
81
96
|
}
|
|
82
97
|
|
|
@@ -95,6 +110,10 @@ const interruptExecutionOnError = (ctx, execaChildProcess) => {
|
|
|
95
110
|
*/
|
|
96
111
|
const makeErr = (command, result, ctx) => {
|
|
97
112
|
ctx.errors.add(TaskError)
|
|
113
|
+
|
|
114
|
+
// https://nodejs.org/api/events.html#error-events
|
|
115
|
+
ctx.events.emit(TASK_ERROR, TaskError)
|
|
116
|
+
|
|
98
117
|
handleOutput(command, result, ctx, true)
|
|
99
118
|
const tag = getTag(result)
|
|
100
119
|
return new Error(`${redBright(command)} ${dim(`[${tag}]`)}`)
|
|
@@ -111,7 +130,7 @@ const makeErr = (command, result, ctx) => {
|
|
|
111
130
|
* @param {Array<string>} options.files — Filepaths to run the linter task against
|
|
112
131
|
* @param {Boolean} [options.shell] — Whether to skip parsing linter task for better shell support
|
|
113
132
|
* @param {Boolean} [options.verbose] — Always show task verbose
|
|
114
|
-
* @returns {
|
|
133
|
+
* @returns {() => Promise<Array<string>>}
|
|
115
134
|
*/
|
|
116
135
|
export const resolveTaskFn = ({
|
|
117
136
|
command,
|
|
@@ -139,12 +158,12 @@ export const resolveTaskFn = ({
|
|
|
139
158
|
|
|
140
159
|
return async (ctx = getInitialState()) => {
|
|
141
160
|
const execaChildProcess = shell
|
|
142
|
-
?
|
|
161
|
+
? execaCommand(isFn ? command : `${command} ${files.join(' ')}`, execaOptions)
|
|
143
162
|
: execa(cmd, isFn ? args : args.concat(files), execaOptions)
|
|
144
163
|
|
|
145
164
|
const quitInterruptCheck = interruptExecutionOnError(ctx, execaChildProcess)
|
|
146
165
|
const result = await execaChildProcess
|
|
147
|
-
quitInterruptCheck()
|
|
166
|
+
await quitInterruptCheck()
|
|
148
167
|
|
|
149
168
|
if (result.failed || result.killed || result.signal != null) {
|
|
150
169
|
throw makeErr(command, result, ctx)
|
package/lib/runAll.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** @typedef {import('./index').Logger} Logger */
|
|
2
2
|
|
|
3
|
-
import path from 'path'
|
|
3
|
+
import path from 'node:path'
|
|
4
4
|
|
|
5
5
|
import { dim } from 'colorette'
|
|
6
6
|
import debug from 'debug'
|
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
restoreUnstagedChangesSkipped,
|
|
37
37
|
} from './state.js'
|
|
38
38
|
import { GitRepoError, GetStagedFilesError, GitError, ConfigNotFoundError } from './symbols.js'
|
|
39
|
-
import {
|
|
39
|
+
import { searchConfigs } from './searchConfigs.js'
|
|
40
40
|
|
|
41
41
|
const debugLog = debug('lint-staged:runAll')
|
|
42
42
|
|
|
@@ -52,6 +52,8 @@ const createError = (ctx) => Object.assign(new Error('lint-staged failed'), { ct
|
|
|
52
52
|
* @param {string} [options.configPath] - Explicit path to a config file
|
|
53
53
|
* @param {string} [options.cwd] - Current working directory
|
|
54
54
|
* @param {boolean} [options.debug] - Enable debug mode
|
|
55
|
+
* @param {string} [options.diff] - Override the default "--staged" flag of "git diff" to get list of files
|
|
56
|
+
* @param {string} [options.diffFilter] - Override the default "--diff-filter=ACMR" flag of "git diff" to get list of files
|
|
55
57
|
* @param {number} [options.maxArgLength] - Maximum argument string length
|
|
56
58
|
* @param {boolean} [options.quiet] - Disable lint-staged’s own console output
|
|
57
59
|
* @param {boolean} [options.relative] - Pass relative filepaths to tasks
|
|
@@ -69,6 +71,8 @@ export const runAll = async (
|
|
|
69
71
|
configPath,
|
|
70
72
|
cwd,
|
|
71
73
|
debug = false,
|
|
74
|
+
diff,
|
|
75
|
+
diffFilter,
|
|
72
76
|
maxArgLength,
|
|
73
77
|
quiet = false,
|
|
74
78
|
relative = false,
|
|
@@ -100,13 +104,14 @@ export const runAll = async (
|
|
|
100
104
|
.then(() => true)
|
|
101
105
|
.catch(() => false)
|
|
102
106
|
|
|
103
|
-
// Lint-staged should create a backup stash only when there's an initial commit
|
|
104
|
-
|
|
107
|
+
// Lint-staged should create a backup stash only when there's an initial commit,
|
|
108
|
+
// and when using the default list of staged files
|
|
109
|
+
ctx.shouldBackup = hasInitialCommit && stash && diff === undefined
|
|
105
110
|
if (!ctx.shouldBackup) {
|
|
106
|
-
logger.warn(skippingBackup(hasInitialCommit))
|
|
111
|
+
logger.warn(skippingBackup(hasInitialCommit, diff))
|
|
107
112
|
}
|
|
108
113
|
|
|
109
|
-
const files = await getStagedFiles({ cwd: gitDir })
|
|
114
|
+
const files = await getStagedFiles({ cwd: gitDir, diff, diffFilter })
|
|
110
115
|
if (!files) {
|
|
111
116
|
if (!quiet) ctx.output.push(FAILED_GET_STAGED_FILES)
|
|
112
117
|
ctx.errors.add(GetStagedFilesError)
|
|
@@ -121,7 +126,7 @@ export const runAll = async (
|
|
|
121
126
|
}
|
|
122
127
|
|
|
123
128
|
const foundConfigs = await searchConfigs({ configObject, configPath, cwd, gitDir }, logger)
|
|
124
|
-
const numberOfConfigs =
|
|
129
|
+
const numberOfConfigs = Object.keys(foundConfigs).length
|
|
125
130
|
|
|
126
131
|
// Throw if no configurations were found
|
|
127
132
|
if (numberOfConfigs === 0) {
|
|
@@ -129,7 +134,11 @@ export const runAll = async (
|
|
|
129
134
|
throw createError(ctx, ConfigNotFoundError)
|
|
130
135
|
}
|
|
131
136
|
|
|
132
|
-
const filesByConfig = await groupFilesByConfig({
|
|
137
|
+
const filesByConfig = await groupFilesByConfig({
|
|
138
|
+
configs: foundConfigs,
|
|
139
|
+
files,
|
|
140
|
+
singleConfigMode: configObject || configPath !== undefined,
|
|
141
|
+
})
|
|
133
142
|
|
|
134
143
|
const hasMultipleConfigs = numberOfConfigs > 1
|
|
135
144
|
|
|
@@ -149,13 +158,8 @@ export const runAll = async (
|
|
|
149
158
|
// Set of all staged files that matched a task glob. Values in a set are unique.
|
|
150
159
|
const matchedFiles = new Set()
|
|
151
160
|
|
|
152
|
-
for (const configPath of
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
const configName =
|
|
156
|
-
configPath === ConfigObjectSymbol
|
|
157
|
-
? 'Config object'
|
|
158
|
-
: normalize(path.relative(cwd, configPath))
|
|
161
|
+
for (const [configPath, { config, files }] of Object.entries(filesByConfig)) {
|
|
162
|
+
const configName = configPath ? normalize(path.relative(cwd, configPath)) : 'Config object'
|
|
159
163
|
|
|
160
164
|
const stagedFileChunks = chunkFiles({ baseDir: gitDir, files, maxArgLength, relative })
|
|
161
165
|
|
|
@@ -199,15 +203,15 @@ export const runAll = async (
|
|
|
199
203
|
const fileCount = task.fileList.length
|
|
200
204
|
|
|
201
205
|
return {
|
|
202
|
-
title: `${task.pattern}${dim(
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
exitOnError: true
|
|
210
|
-
|
|
206
|
+
title: `${task.pattern}${dim(
|
|
207
|
+
` — ${fileCount} ${fileCount === 1 ? 'file' : 'files'}`
|
|
208
|
+
)}`,
|
|
209
|
+
task: async (ctx, task) =>
|
|
210
|
+
task.newListr(
|
|
211
|
+
subTasks,
|
|
212
|
+
// Subtasks should not run in parallel, and should exit on error
|
|
213
|
+
{ concurrent: false, exitOnError: true }
|
|
214
|
+
),
|
|
211
215
|
skip: () => {
|
|
212
216
|
// Skip task when no files matched
|
|
213
217
|
if (fileCount === 0) {
|
|
@@ -224,7 +228,7 @@ export const runAll = async (
|
|
|
224
228
|
title:
|
|
225
229
|
`${configName}${dim(` — ${files.length} ${files.length > 1 ? 'files' : 'file'}`)}` +
|
|
226
230
|
(chunkCount > 1 ? dim(` (chunk ${index + 1}/${chunkCount})...`) : ''),
|
|
227
|
-
task: () =>
|
|
231
|
+
task: (ctx, task) => task.newListr(chunkListrTasks, { concurrent, exitOnError: true }),
|
|
228
232
|
skip: () => {
|
|
229
233
|
// Skip if the first step (backup) failed
|
|
230
234
|
if (ctx.errors.has(GitError)) return SKIPPED_GIT_ERROR
|
|
@@ -273,7 +277,7 @@ export const runAll = async (
|
|
|
273
277
|
},
|
|
274
278
|
{
|
|
275
279
|
title: `Running tasks for staged files...`,
|
|
276
|
-
task: () =>
|
|
280
|
+
task: (ctx, task) => task.newListr(listrTasks, { concurrent }),
|
|
277
281
|
skip: () => listrTasks.every((task) => task.skip()),
|
|
278
282
|
},
|
|
279
283
|
{
|
package/lib/searchConfigs.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** @typedef {import('./index').Logger} Logger */
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import path from 'node:path'
|
|
4
4
|
|
|
5
5
|
import debug from 'debug'
|
|
6
6
|
import normalize from 'normalize-path'
|
|
@@ -15,7 +15,7 @@ const debugLog = debug('lint-staged:searchConfigs')
|
|
|
15
15
|
const EXEC_GIT = ['ls-files', '-z', '--full-name']
|
|
16
16
|
|
|
17
17
|
const filterPossibleConfigFiles = (files) =>
|
|
18
|
-
files.filter((file) => searchPlaces.includes(basename(file)))
|
|
18
|
+
files.filter((file) => searchPlaces.includes(path.basename(file)))
|
|
19
19
|
|
|
20
20
|
const numberOfLevels = (file) => file.split('/').length
|
|
21
21
|
|
|
@@ -23,8 +23,6 @@ const sortDeepestParth = (a, b) => (numberOfLevels(a) > numberOfLevels(b) ? -1 :
|
|
|
23
23
|
|
|
24
24
|
const isInsideDirectory = (dir) => (file) => file.startsWith(normalize(dir))
|
|
25
25
|
|
|
26
|
-
export const ConfigObjectSymbol = Symbol()
|
|
27
|
-
|
|
28
26
|
/**
|
|
29
27
|
* Search all config files from the git repository, preferring those inside `cwd`.
|
|
30
28
|
*
|
|
@@ -46,7 +44,7 @@ export const searchConfigs = async (
|
|
|
46
44
|
if (configObject) {
|
|
47
45
|
debugLog('Using single direct configuration object...')
|
|
48
46
|
|
|
49
|
-
return {
|
|
47
|
+
return { '': validateConfig(configObject, 'config object', logger) }
|
|
50
48
|
}
|
|
51
49
|
|
|
52
50
|
// Use only explicit config path instead of discovering multiple
|
|
@@ -70,7 +68,7 @@ export const searchConfigs = async (
|
|
|
70
68
|
|
|
71
69
|
/** Sort possible config files so that deepest is first */
|
|
72
70
|
const possibleConfigFiles = [...cachedFiles, ...otherFiles]
|
|
73
|
-
.map((file) => normalize(join(gitDir, file)))
|
|
71
|
+
.map((file) => normalize(path.join(gitDir, file)))
|
|
74
72
|
.filter(isInsideDirectory(cwd))
|
|
75
73
|
.sort(sortDeepestParth)
|
|
76
74
|
|
package/lib/state.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import EventEmitter from 'events'
|
|
2
|
+
|
|
1
3
|
import { GIT_ERROR, TASK_ERROR } from './messages.js'
|
|
2
4
|
import {
|
|
3
5
|
ApplyEmptyCommitError,
|
|
@@ -11,6 +13,7 @@ export const getInitialState = ({ quiet = false } = {}) => ({
|
|
|
11
13
|
hasPartiallyStagedFiles: null,
|
|
12
14
|
shouldBackup: null,
|
|
13
15
|
errors: new Set([]),
|
|
16
|
+
events: new EventEmitter(),
|
|
14
17
|
output: [],
|
|
15
18
|
quiet,
|
|
16
19
|
})
|
package/lib/validateOptions.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lint-staged",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "13.0.1",
|
|
4
4
|
"description": "Lint files staged by git",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": "https://github.com/okonet/lint-staged",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"url": "https://opencollective.com/lint-staged"
|
|
15
15
|
},
|
|
16
16
|
"engines": {
|
|
17
|
-
"node": "^
|
|
17
|
+
"node": "^14.13.1 || >=16.0.0"
|
|
18
18
|
},
|
|
19
19
|
"type": "module",
|
|
20
20
|
"bin": "./bin/lint-staged.js",
|
|
@@ -33,35 +33,34 @@
|
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"cli-truncate": "^3.1.0",
|
|
36
|
-
"colorette": "^2.0.
|
|
36
|
+
"colorette": "^2.0.17",
|
|
37
37
|
"commander": "^9.3.0",
|
|
38
38
|
"debug": "^4.3.4",
|
|
39
|
-
"execa": "^
|
|
39
|
+
"execa": "^6.1.0",
|
|
40
40
|
"lilconfig": "2.0.5",
|
|
41
41
|
"listr2": "^4.0.5",
|
|
42
42
|
"micromatch": "^4.0.5",
|
|
43
43
|
"normalize-path": "^3.0.0",
|
|
44
44
|
"object-inspect": "^1.12.2",
|
|
45
|
-
"pidtree": "^0.
|
|
45
|
+
"pidtree": "^0.6.0",
|
|
46
46
|
"string-argv": "^0.3.1",
|
|
47
|
-
"
|
|
48
|
-
"yaml": "^1.10.2"
|
|
47
|
+
"yaml": "^2.1.1"
|
|
49
48
|
},
|
|
50
49
|
"devDependencies": {
|
|
51
50
|
"@babel/core": "^7.18.2",
|
|
52
51
|
"@babel/eslint-parser": "^7.18.2",
|
|
53
52
|
"@babel/preset-env": "^7.18.2",
|
|
54
|
-
"babel-jest": "^28.1.
|
|
53
|
+
"babel-jest": "^28.1.1",
|
|
55
54
|
"babel-plugin-transform-imports": "2.0.0",
|
|
56
55
|
"consolemock": "^1.1.0",
|
|
57
|
-
"eslint": "^8.
|
|
56
|
+
"eslint": "^8.17.0",
|
|
58
57
|
"eslint-config-prettier": "^8.5.0",
|
|
59
58
|
"eslint-plugin-import": "^2.26.0",
|
|
60
59
|
"eslint-plugin-node": "^11.1.0",
|
|
61
60
|
"eslint-plugin-prettier": "^4.0.0",
|
|
62
61
|
"fs-extra": "^10.1.0",
|
|
63
62
|
"husky": "^8.0.1",
|
|
64
|
-
"jest": "^28.1.
|
|
63
|
+
"jest": "^28.1.1",
|
|
65
64
|
"jest-snapshot-serializer-ansi": "^1.0.0",
|
|
66
65
|
"prettier": "^2.6.2"
|
|
67
66
|
},
|