lint-staged 13.0.0 → 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 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
- The fastest way to start using lint-staged is to run the following command in your terminal:
51
+ To install _lint-staged_ in the recommended way, you need to:
48
52
 
49
- ```bash
50
- npx mrm@2 lint-staged
51
- ```
52
-
53
- This command will install and configure [husky](https://github.com/typicode/husky) and lint-staged depending on the code quality tools from your project's `package.json` dependencies, so please make sure you install (`npm install --save-dev`) and configure all code quality tools like [Prettier](https://prettier.io) and [ESLint](https://eslint.org) prior to that.
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
 
@@ -110,7 +118,7 @@ Options:
110
118
  ```
111
119
 
112
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.
113
- - **`--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:
114
122
  - `false`: Run all tasks serially
115
123
  - `true` (default) : _Infinite_ concurrency. Runs as many tasks in parallel as possible.
116
124
  - `{number}`: Run the specified number of tasks in parallel, where `1` is equivalent to `false`.
@@ -130,7 +138,7 @@ Options:
130
138
 
131
139
  ## Configuration
132
140
 
133
- Starting with v3.1 you can now use different ways of configuring lint-staged:
141
+ _Lint-staged_ can be configured in many ways:
134
142
 
135
143
  - `lint-staged` object in your `package.json`
136
144
  - `.lintstagedrc` file in JSON or YML format, or you can be explicit with the file extension:
@@ -173,6 +181,37 @@ So, considering you did `git add file1.ext file2.ext`, lint-staged will run the
173
181
 
174
182
  `your-cmd file1.ext file2.ext`
175
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
+
176
215
  ## Filtering files
177
216
 
178
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:
@@ -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 ERROR_CHECK_INTERVAL = 200
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 {function(): void} Function that clears the interval that
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 loopIntervalId
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
- // The execa process is killed separately in order
71
- // to get the `KILLED` status.
72
- execaChildProcess.kill()
73
- }
85
+ const errorListener = async () => {
86
+ killPromise = killExecaProcess(execaChildProcess)
87
+ await killPromise
74
88
  }
75
89
 
76
- loopIntervalId = setInterval(loop, ERROR_CHECK_INTERVAL)
90
+ ctx.events.on(TASK_ERROR, errorListener, { once: true })
77
91
 
78
- return () => {
79
- clearInterval(loopIntervalId)
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 {function(): Promise<Array<string>>}
133
+ * @returns {() => Promise<Array<string>>}
115
134
  */
116
135
  export const resolveTaskFn = ({
117
136
  command,
@@ -144,7 +163,7 @@ export const resolveTaskFn = ({
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
@@ -203,15 +203,15 @@ export const runAll = async (
203
203
  const fileCount = task.fileList.length
204
204
 
205
205
  return {
206
- title: `${task.pattern}${dim(` — ${fileCount} ${fileCount > 1 ? 'files' : 'file'}`)}`,
207
- task: async () =>
208
- new Listr(subTasks, {
209
- // In sub-tasks we don't want to run concurrently
210
- // and we want to abort on errors
211
- ...listrOptions,
212
- concurrent: false,
213
- exitOnError: true,
214
- }),
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
+ ),
215
215
  skip: () => {
216
216
  // Skip task when no files matched
217
217
  if (fileCount === 0) {
@@ -228,7 +228,7 @@ export const runAll = async (
228
228
  title:
229
229
  `${configName}${dim(` — ${files.length} ${files.length > 1 ? 'files' : 'file'}`)}` +
230
230
  (chunkCount > 1 ? dim(` (chunk ${index + 1}/${chunkCount})...`) : ''),
231
- task: () => new Listr(chunkListrTasks, { ...listrOptions, concurrent, exitOnError: true }),
231
+ task: (ctx, task) => task.newListr(chunkListrTasks, { concurrent, exitOnError: true }),
232
232
  skip: () => {
233
233
  // Skip if the first step (backup) failed
234
234
  if (ctx.errors.has(GitError)) return SKIPPED_GIT_ERROR
@@ -277,7 +277,7 @@ export const runAll = async (
277
277
  },
278
278
  {
279
279
  title: `Running tasks for staged files...`,
280
- task: () => new Listr(listrTasks, { ...listrOptions, concurrent }),
280
+ task: (ctx, task) => task.newListr(listrTasks, { concurrent }),
281
281
  skip: () => listrTasks.every((task) => task.skip()),
282
282
  },
283
283
  {
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lint-staged",
3
- "version": "13.0.0",
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",
@@ -33,7 +33,7 @@
33
33
  },
34
34
  "dependencies": {
35
35
  "cli-truncate": "^3.1.0",
36
- "colorette": "^2.0.16",
36
+ "colorette": "^2.0.17",
37
37
  "commander": "^9.3.0",
38
38
  "debug": "^4.3.4",
39
39
  "execa": "^6.1.0",
@@ -42,7 +42,7 @@
42
42
  "micromatch": "^4.0.5",
43
43
  "normalize-path": "^3.0.0",
44
44
  "object-inspect": "^1.12.2",
45
- "pidtree": "^0.5.0",
45
+ "pidtree": "^0.6.0",
46
46
  "string-argv": "^0.3.1",
47
47
  "yaml": "^2.1.1"
48
48
  },
@@ -50,17 +50,17 @@
50
50
  "@babel/core": "^7.18.2",
51
51
  "@babel/eslint-parser": "^7.18.2",
52
52
  "@babel/preset-env": "^7.18.2",
53
- "babel-jest": "^28.1.0",
53
+ "babel-jest": "^28.1.1",
54
54
  "babel-plugin-transform-imports": "2.0.0",
55
55
  "consolemock": "^1.1.0",
56
- "eslint": "^8.16.0",
56
+ "eslint": "^8.17.0",
57
57
  "eslint-config-prettier": "^8.5.0",
58
58
  "eslint-plugin-import": "^2.26.0",
59
59
  "eslint-plugin-node": "^11.1.0",
60
60
  "eslint-plugin-prettier": "^4.0.0",
61
61
  "fs-extra": "^10.1.0",
62
62
  "husky": "^8.0.1",
63
- "jest": "^28.1.0",
63
+ "jest": "^28.1.1",
64
64
  "jest-snapshot-serializer-ansi": "^1.0.0",
65
65
  "prettier": "^2.6.2"
66
66
  },