lint-staged 15.5.1 → 16.0.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/MIGRATION.md ADDED
@@ -0,0 +1,70 @@
1
+ ## v16
2
+
3
+ #### Updated Node.js version requirement
4
+
5
+ The lowest supported Node.js version is `18.19.0` or `20.5.0`, following requirements of `execa@9`. Please upgrade your Node.js version.
6
+
7
+ #### Removed validation for removed advanced configuration file options
8
+
9
+ Advanced configuration options (removed in v9) are no longer validated separately, and might be treated as valid globs for tasks. Please do not try to use advanced config options anymore, they haven't been supported since v8.
10
+
11
+ #### Removed the `--shell` option
12
+
13
+ The `--shell` flag has been removed and _lint-staged_ no longer supports evaluating commands directly via a shell. To migrate existing commands, you can create a shell script and invoke it instead. Lint-staged will pass matched staged files as a list of arguments, accessible via `"$@"`:
14
+
15
+ ```shell
16
+ # my-script.sh
17
+ #!/bin/bash
18
+
19
+ echo "Staged files: $@"
20
+ ```
21
+
22
+ and
23
+
24
+ ```json
25
+ { "*.js": "my-script.sh" }
26
+ ```
27
+
28
+ If you were using the shell option to avoid passing filenames to tasks, for example `bash -c 'tsc --noEmit'`, use the function syntax instead:
29
+
30
+ ```js
31
+ export default { '*.ts': () => 'tsc --noEmit' }
32
+ ```
33
+
34
+ #### Processes are spawned using `nano-spawn`
35
+
36
+ Processes are spawned using [nano-spawn](https://github.com/sindresorhus/nano-spawn) instead of [execa](https://github.com/sindresorhus/execa). If you are using Node.js scripts as tasks, you might need to explicitly run them with `node`, especially when using Windows:
37
+
38
+ ```json
39
+ {
40
+ "*.js": "node my-js-linter.js"
41
+ }
42
+ ```
43
+
44
+ ## v15
45
+
46
+ - Since `v15.0.0` _lint-staged_ no longer supports Node.js 16. Please upgrade your Node.js version to at least `18.12.0`.
47
+
48
+ ## v14
49
+
50
+ - Since `v14.0.0` _lint-staged_ no longer supports Node.js 14. Please upgrade your Node.js version to at least `16.14.0`.
51
+
52
+ ## v13
53
+
54
+ - 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.
55
+ - Version `v13.3.0` was incorrectly released including code of version `v14.0.0`. This means the breaking changes of `v14` are also included in `v13.3.0`, the last `v13` version released
56
+
57
+ ## v12
58
+
59
+ - 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).
60
+
61
+ ## v10
62
+
63
+ - From `v10.0.0` onwards any new modifications to originally staged files will be automatically added to the commit.
64
+ If your task previously contained a `git add` step, please remove this.
65
+ The automatic behaviour ensures there are less race-conditions,
66
+ since trying to run multiple git operations at the same time usually results in an error.
67
+ - From `v10.0.0` onwards, lint-staged uses git stashes to improve speed and provide backups while running.
68
+ Since git stashes require at least an initial commit, you shouldn't run lint-staged in an empty repo.
69
+ - From `v10.0.0` onwards, lint-staged requires Node.js version 10.13.0 or later.
70
+ - From `v10.0.0` onwards, lint-staged will abort the commit if linter tasks undo all staged changes. To allow creating an empty commit, please use the `--allow-empty` option.
package/README.md CHANGED
@@ -79,33 +79,7 @@ See [Releases](https://github.com/okonet/lint-staged/releases).
79
79
 
80
80
  ### Migration
81
81
 
82
- #### v15
83
-
84
- - Since `v15.0.0` _lint-staged_ no longer supports Node.js 16. Please upgrade your Node.js version to at least `18.12.0`.
85
-
86
- #### v14
87
-
88
- - Since `v14.0.0` _lint-staged_ no longer supports Node.js 14. Please upgrade your Node.js version to at least `16.14.0`.
89
-
90
- #### v13
91
-
92
- - 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.
93
- - Version `v13.3.0` was incorrectly released including code of version `v14.0.0`. This means the breaking changes of `v14` are also included in `v13.3.0`, the last `v13` version released
94
-
95
- #### v12
96
-
97
- - 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).
98
-
99
- #### v10
100
-
101
- - From `v10.0.0` onwards any new modifications to originally staged files will be automatically added to the commit.
102
- If your task previously contained a `git add` step, please remove this.
103
- The automatic behaviour ensures there are less race-conditions,
104
- since trying to run multiple git operations at the same time usually results in an error.
105
- - From `v10.0.0` onwards, lint-staged uses git stashes to improve speed and provide backups while running.
106
- Since git stashes require at least an initial commit, you shouldn't run lint-staged in an empty repo.
107
- - From `v10.0.0` onwards, lint-staged requires Node.js version 10.13.0 or later.
108
- - From `v10.0.0` onwards, lint-staged will abort the commit if linter tasks undo all staged changes. To allow creating an empty commit, please use the `--allow-empty` option.
82
+ For breaking changes, see [MIGRATION.md](./MIGRATION.md).
109
83
 
110
84
  ## Command line flags
111
85
 
@@ -130,7 +104,6 @@ Options:
130
104
  --no-hide-partially-staged disable hiding unstaged changes from partially staged files
131
105
  -q, --quiet disable lint-staged’s own console output (default: false)
132
106
  -r, --relative pass relative filepaths to tasks (default: false)
133
- -x, --shell [path] skip parsing of tasks for better shell support (default: false)
134
107
  -v, --verbose show task output even when tasks succeed; by default only failed output is
135
108
  shown (default: false)
136
109
  -h, --help display help for command
@@ -160,7 +133,6 @@ Any lost modifications can be restored from a git stash:
160
133
  - **`--no-hide-partially-staged`**: By default, unstaged changes from partially staged files will be hidden. This option will disable this behavior and include all unstaged changes in partially staged files. Can be re-enabled with `--hide-partially-staged`
161
134
  - **`--quiet`**: Supress all CLI output, except from tasks.
162
135
  - **`--relative`**: Pass filepaths relative to `process.cwd()` (where `lint-staged` runs) to tasks. Default is `false`.
163
- - **`--shell`**: By default task commands will be parsed for speed and security. This has the side-effect that regular shell scripts might not work as expected. You can skip parsing of commands with this option. To use a specific shell, use a path like `--shell "/bin/bash"`.
164
136
  - **`--verbose`**: Show task output even when tasks succeed. By default only failed output is shown.
165
137
 
166
138
  ## Configuration
@@ -180,7 +152,7 @@ _Lint-staged_ can be configured in many ways:
180
152
  whether your project's _package.json_ contains the `"type": "module"` option or not.
181
153
  - Pass a configuration file using the `--config` or `-c` flag
182
154
 
183
- Configuration should be an object where each value is a command to run and its key is a glob pattern to use for this command. This package uses [micromatch](https://github.com/micromatch/micromatch) for glob patterns. JavaScript files can also export advanced configuration as a function. See [Using JS configuration files](#using-js-configuration-files) for more info.
155
+ Configuration should be an object where each value is a **command** to run and its key is a glob pattern to use for this command. This package uses [micromatch](https://github.com/micromatch/micromatch) for glob patterns. JavaScript files can also export advanced configuration as a function. See [Using JS configuration files](#using-js-configuration-files) for more info.
184
156
 
185
157
  You can also place multiple configuration files in different directories inside a project. For a given staged file, the closest configuration file will always be used. See ["How to use `lint-staged` in a multi-package monorepo?"](#how-to-use-lint-staged-in-a-multi-package-monorepo) for more info and an example.
186
158
 
@@ -363,12 +335,19 @@ export default {
363
335
 
364
336
  This will result in _lint-staged_ first running `eslint .` (matching _all_ files), and if it passes, `prettier --write file-1.js file-2.js`, when you have staged files `file-1.js`, `file-2.js` and `README.md`.
365
337
 
366
- ### Function signature
338
+ ### JavaScript Functions
367
339
 
368
- The function can also be async:
340
+ You can also configure _lint-staged_ to run a JavaScript/Node.js script directly, passing the list of staged files as an argument:
369
341
 
370
- ```ts
371
- (filenames: string[]) => string | string[] | Promise<string | string[]>
342
+ ```js
343
+ export default {
344
+ '*.js': {
345
+ title: 'Log staged JS files to console',
346
+ task: async (files) => {
347
+ console.log('Staged JS files:', files)
348
+ },
349
+ },
350
+ }
372
351
  ```
373
352
 
374
353
  ### Example: Export a function to build your own matchers
@@ -777,7 +756,6 @@ const success = await lintStaged({
777
756
  maxArgLength: null,
778
757
  quiet: false,
779
758
  relative: false,
780
- shell: false,
781
759
  stash: true,
782
760
  verbose: false,
783
761
  })
@@ -795,7 +773,6 @@ const success = await lintStaged({
795
773
  maxArgLength: null,
796
774
  quiet: false,
797
775
  relative: false,
798
- shell: false,
799
776
  stash: true,
800
777
  verbose: false,
801
778
  })
@@ -1022,26 +999,18 @@ ESLint v8.51.0 introduced [`--no-warn-ignored` CLI flag](https://eslint.org/docs
1022
999
  When running `lint-staged` via Husky hooks, TypeScript may ignore `tsconfig.json`, leading to errors like:
1023
1000
 
1024
1001
  > **TS17004:** Cannot use JSX unless the '--jsx' flag is provided.
1025
- > **TS1056:** Accessors are only available when targeting ECMAScript 5 and higher.
1026
-
1027
- See issue [#825](https://github.com/okonet/lint-staged/issues/825) for more details.
1028
-
1029
- #### Root Cause
1030
-
1031
- <details>
1032
- <summary>Click to expand</summary>
1002
+ > **TS1056:** Accessors are only available when targeting ECMAScript 5 and higher.
1033
1003
 
1034
- 1. `lint-staged` automatically passes matched staged files as arguments to commands.
1035
- 2. Certain input files can cause TypeScript to ignore `tsconfig.json`. For more details, see this TypeScript issue: [Allow tsconfig.json when input files are specified](https://github.com/microsoft/TypeScript/issues/27379).
1004
+ See issue [#825](https://github.com/okonet/lint-staged/issues/825) for more details.
1036
1005
 
1037
- </details>
1006
+ #### Root Cause
1038
1007
 
1039
- #### Workaround 1: Use a [function signature](https://github.com/lint-staged/lint-staged?tab=readme-ov-file#example-run-tsc-on-changes-to-typescript-files-but-do-not-pass-any-filename-arguments) for the `tsc` command
1008
+ 1. `lint-staged` automatically passes matched staged files as arguments to commands.
1009
+ 2. Certain input files can cause TypeScript to ignore `tsconfig.json`. For more details, see this TypeScript issue: [Allow tsconfig.json when input files are specified](https://github.com/microsoft/TypeScript/issues/27379).
1040
1010
 
1041
- <details>
1042
- <summary>Click to expand</summary>
1011
+ #### Workaround: Use a [function signature](https://github.com/lint-staged/lint-staged?tab=readme-ov-file#example-run-tsc-on-changes-to-typescript-files-but-do-not-pass-any-filename-arguments) for the `tsc` command
1043
1012
 
1044
- As suggested by @antoinerousseau in [#825 (comment)](https://github.com/lint-staged/lint-staged/issues/825#issuecomment-620018284), using a function prevents `lint-staged` from appending file arguments:
1013
+ As suggested by @antoinerousseau in [#825 (comment)](https://github.com/lint-staged/lint-staged/issues/825#issuecomment-620018284), using a function prevents `lint-staged` from appending file arguments:
1045
1014
 
1046
1015
  **Before:**
1047
1016
 
@@ -1061,50 +1030,8 @@ As suggested by @antoinerousseau in [#825 (comment)](https://github.com/lint-sta
1061
1030
  ```js
1062
1031
  // lint-staged.config.js
1063
1032
  module.exports = {
1064
- "*.{ts,tsx}": [
1065
- () => "tsc --noEmit",
1066
- "prettier --write"
1067
- ],
1068
- }
1069
- ```
1070
-
1071
- </details>
1072
-
1073
- #### Workaround 2: Take the `sh` or `bash` to wrap the `tsc` command
1074
-
1075
- <details>
1076
- <summary>Click to expand</summary>
1077
-
1078
- As suggested by @sombreroEnPuntas in [#825 (comment)](https://github.com/lint-staged/lint-staged/issues/825#issuecomment-674575655), wrapping `tsc` in a shell command prevents `lint-staged` from modifying its arguments:
1079
-
1080
- **Before:**
1081
-
1082
- ```js
1083
- // package.json
1084
-
1085
- "lint-staged": {
1086
- "*.{ts,tsx}":[
1087
- "tsc --noEmit",
1088
- "prettier --write"
1089
- ]
1033
+ '*.{ts,tsx}': [() => 'tsc --noEmit', 'prettier --write'],
1090
1034
  }
1091
1035
  ```
1092
1036
 
1093
- **After:**
1094
-
1095
- ```js
1096
- // package.json
1097
-
1098
- "lint-staged": {
1099
- "*.{ts,tsx}":[
1100
- "bash -c 'tsc --noEmit'"
1101
- "prettier --write"
1102
- ]
1103
- }
1104
- ```
1105
-
1106
- **Note:** This approach may have cross-platform compatibility issues.
1107
-
1108
- </details>
1109
-
1110
1037
  </details>
@@ -102,8 +102,6 @@ program.option('-q, --quiet', 'disable lint-staged’s own console output', fals
102
102
 
103
103
  program.option('-r, --relative', 'pass relative filepaths to tasks', false)
104
104
 
105
- program.option('-x, --shell [path]', 'skip parsing of tasks for better shell support', false)
106
-
107
105
  program.option(
108
106
  '-v, --verbose',
109
107
  'show task output even when tasks succeed; by default only failed output is shown',
@@ -129,7 +127,6 @@ const options = {
129
127
  maxArgLength: cliOptions.maxArgLength || undefined,
130
128
  quiet: !!cliOptions.quiet,
131
129
  relative: !!cliOptions.relative,
132
- shell: cliOptions.shell /* Either a boolean or a string pointing to the shell */,
133
130
  stash: !!cliOptions.stash, // commander inverts `no-<x>` flags to `!x`
134
131
  hidePartiallyStaged: !!cliOptions.hidePartiallyStaged, // commander inverts `no-<x>` flags to `!x`
135
132
  verbose: !!cliOptions.verbose,
package/lib/execGit.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import debug from 'debug'
2
- import { execa } from 'execa'
2
+ import spawn, { SubprocessError } from 'nano-spawn'
3
3
 
4
4
  const debugLog = debug('lint-staged:execGit')
5
5
 
@@ -12,17 +12,22 @@ const NO_SUBMODULE_RECURSE = ['-c', 'submodule.recurse=false']
12
12
  // exported for tests
13
13
  export const GIT_GLOBAL_OPTIONS = [...NO_SUBMODULE_RECURSE]
14
14
 
15
- export const execGit = async (cmd, options = {}) => {
15
+ /** @type {(cmd: string[], options?: import('nano-spawn').Options) => Promise<string>} */
16
+ export const execGit = async (cmd, options) => {
16
17
  debugLog('Running git command', cmd)
17
18
  try {
18
- const { stdout } = await execa('git', GIT_GLOBAL_OPTIONS.concat(cmd), {
19
+ const result = await spawn('git', [...NO_SUBMODULE_RECURSE, ...cmd], {
19
20
  ...options,
20
- all: true,
21
- cwd: options.cwd || process.cwd(),
21
+ cwd: options?.cwd ?? process.cwd(),
22
22
  stdin: 'ignore',
23
23
  })
24
- return stdout
25
- } catch ({ all }) {
26
- throw new Error(all)
24
+
25
+ return result.stdout
26
+ } catch (error) {
27
+ if (error instanceof SubprocessError) {
28
+ throw new Error(error.output, { cause: error })
29
+ }
30
+
31
+ throw error
27
32
  }
28
33
  }
@@ -0,0 +1,38 @@
1
+ import debug from 'debug'
2
+
3
+ import { makeErr } from './getSpawnedTask.js'
4
+
5
+ const debugLog = debug('lint-staged:getFunctionTasks')
6
+
7
+ /**
8
+ * @typedef {{ title: string; task: Function }} FunctionTask
9
+ * @type {(commands: FunctionTask|Array<string|Function>|string|Function) => boolean}
10
+ * @returns `true` if command is a function task
11
+ */
12
+ export const isFunctionTask = (commands) => typeof commands === 'object' && !Array.isArray(commands)
13
+
14
+ /**
15
+ * Handles function configuration and pushes the tasks into the task array
16
+ *
17
+ * @param {object} command
18
+ * @param {Array<string>} files
19
+ * @throws {Error} If the function configuration is not valid
20
+ */
21
+ export const getFunctionTask = async (command, files) => {
22
+ debugLog('Creating Listr tasks for function %o', command)
23
+
24
+ const task = async (ctx) => {
25
+ try {
26
+ await command.task(files)
27
+ } catch (e) {
28
+ throw makeErr(command.title, e, ctx)
29
+ }
30
+ }
31
+
32
+ return [
33
+ {
34
+ title: command.title,
35
+ task,
36
+ },
37
+ ]
38
+ }
@@ -1,6 +1,6 @@
1
1
  import chalk from 'chalk'
2
2
  import debug from 'debug'
3
- import { execa, execaCommand } from 'execa'
3
+ import spawn from 'nano-spawn'
4
4
  import pidTree from 'pidtree'
5
5
  import { parseArgsStringToArgv } from 'string-argv'
6
6
 
@@ -8,40 +8,27 @@ import { error, info } from './figures.js'
8
8
  import { getInitialState } from './state.js'
9
9
  import { TaskError } from './symbols.js'
10
10
 
11
- /**
12
- * @see https://github.com/sindresorhus/execa/blob/f4b8b3ab601c94d1503f1010822952758dcc6350/lib/command.js#L32-L37
13
- */
14
- const escapeSpaces = (input) => input.replaceAll(' ', '\\ ')
15
-
16
11
  const TASK_ERROR = 'lint-staged:taskError'
17
12
 
18
- const debugLog = debug('lint-staged:resolveTaskFn')
13
+ const debugLog = debug('lint-staged:getSpawnedTask')
19
14
 
20
- const getTag = ({ code, killed, signal }) => (killed && 'KILLED') || signal || code || 'FAILED'
15
+ /** @type {(error: import('nano-spawn').SubprocessError) => string} */
16
+ const getTag = (error) => {
17
+ return error.signalName ?? 'FAILED'
18
+ }
21
19
 
22
20
  /**
23
21
  * Handle task console output.
24
22
  *
25
23
  * @param {string} command
26
- * @param {Object} result
27
- * @param {string} result.stdout
28
- * @param {string} result.stderr
29
- * @param {boolean} result.failed
30
- * @param {boolean} result.killed
31
- * @param {string} result.signal
24
+ * @param {import('nano-spawn').Result | import('nano-spawn').SubprocessError} result
32
25
  * @param {Object} ctx
33
26
  * @returns {Error}
34
27
  */
35
28
  const handleOutput = (command, result, ctx, isError = false) => {
36
- const { stderr, stdout } = result
37
- const hasOutput = !!stderr || !!stdout
38
-
39
- if (hasOutput) {
29
+ if (result.output) {
40
30
  const outputTitle = isError ? chalk.redBright(`${error} ${command}:`) : `${info} ${command}:`
41
- const output = []
42
- .concat(ctx.quiet ? [] : ['', outputTitle])
43
- .concat(stderr ? stderr : [])
44
- .concat(stdout ? stdout : [])
31
+ const output = [...(ctx.quiet ? [] : ['', outputTitle]), result.output]
45
32
  ctx.output.push(output.join('\n'))
46
33
  } else if (isError) {
47
34
  // Show generic error when task had no output
@@ -52,12 +39,14 @@ const handleOutput = (command, result, ctx, isError = false) => {
52
39
  }
53
40
 
54
41
  /**
55
- * Kill an execa process along with all its child processes.
56
- * @param {execa.ExecaChildProcess<string>} execaProcess
42
+ * Kill subprocess along with all its child processes.
43
+ * @param {import('nano-spawn').Subprocess} subprocess
57
44
  */
58
- const killExecaProcess = async (execaProcess) => {
45
+ const killSubprocess = async (subprocess) => {
46
+ const childProcess = await subprocess.nodeChildProcess
47
+
59
48
  try {
60
- const childPids = await pidTree(execaProcess.pid)
49
+ const childPids = await pidTree(childProcess.pid)
61
50
  for (const childPid of childPids) {
62
51
  try {
63
52
  process.kill(childPid)
@@ -68,27 +57,27 @@ const killExecaProcess = async (execaProcess) => {
68
57
  } catch (error) {
69
58
  // Suppress "No matching pid found" error. This probably means
70
59
  // the process already died before executing.
71
- debugLog(`Failed to kill process with pid "%d": %o`, execaProcess.pid, error)
60
+ debugLog(`Failed to kill process with pid "%d": %o`, childProcess.pid, error)
72
61
  }
73
62
 
74
- // The execa process is killed separately in order to get the `KILLED` status.
75
- execaProcess.kill()
63
+ // The child process is terminated separately in order to get the `KILLED` status.
64
+ childProcess.kill('SIGKILL')
76
65
  }
77
66
 
78
67
  /**
79
- * Interrupts the execution of the execa process that we spawned if
68
+ * Interrupts the execution of the subprocess that we spawned if
80
69
  * another task adds an error to the context.
81
70
  *
82
71
  * @param {Object} ctx
83
- * @param {execa.ExecaChildProcess<string>} execaChildProcess
72
+ * @param {import('nano-spawn').Subprocess} subprocess
84
73
  * @returns {() => Promise<void>} Function that clears the interval that
85
74
  * checks the context.
86
75
  */
87
- const interruptExecutionOnError = (ctx, execaChildProcess) => {
76
+ const interruptExecutionOnError = (ctx, subprocess) => {
88
77
  let killPromise
89
78
 
90
79
  const errorListener = async () => {
91
- killPromise = killExecaProcess(execaChildProcess)
80
+ killPromise = killSubprocess(subprocess)
92
81
  await killPromise
93
82
  }
94
83
 
@@ -104,23 +93,18 @@ const interruptExecutionOnError = (ctx, execaChildProcess) => {
104
93
  * Create a error output depending on process result.
105
94
  *
106
95
  * @param {string} command
107
- * @param {Object} result
108
- * @param {string} result.stdout
109
- * @param {string} result.stderr
110
- * @param {boolean} result.failed
111
- * @param {boolean} result.killed
112
- * @param {string} result.signal
96
+ * @param {import('nano-spawn').SubprocessError} error
113
97
  * @param {Object} ctx
114
98
  * @returns {Error}
115
99
  */
116
- const makeErr = (command, result, ctx) => {
100
+ export const makeErr = (command, error, ctx) => {
117
101
  ctx.errors.add(TaskError)
118
102
 
119
103
  // https://nodejs.org/api/events.html#error-events
120
104
  ctx.events.emit(TASK_ERROR, TaskError)
121
105
 
122
- handleOutput(command, result, ctx, true)
123
- const tag = getTag(result)
106
+ handleOutput(command, error, ctx, true)
107
+ const tag = getTag(error)
124
108
  return new Error(`${chalk.redBright(command)} ${chalk.dim(`[${tag}]`)}`)
125
109
  }
126
110
 
@@ -133,53 +117,45 @@ const makeErr = (command, result, ctx) => {
133
117
  * @param {String} options.topLevelDir - Current git repo top-level path
134
118
  * @param {Boolean} options.isFn - Whether the linter task is a function
135
119
  * @param {Array<string>} options.files — Filepaths to run the linter task against
136
- * @param {Boolean} [options.shell] — Whether to skip parsing linter task for better shell support
137
120
  * @param {Boolean} [options.verbose] — Always show task verbose
138
121
  * @returns {() => Promise<Array<string>>}
139
122
  */
140
- export const resolveTaskFn = ({
123
+ export const getSpawnedTask = ({
141
124
  command,
142
125
  cwd = process.cwd(),
143
126
  files,
144
127
  topLevelDir,
145
128
  isFn,
146
- shell = false,
147
129
  verbose = false,
148
130
  }) => {
149
131
  const [cmd, ...args] = parseArgsStringToArgv(command)
150
132
  debugLog('cmd:', cmd)
151
133
  debugLog('args:', args)
152
134
 
153
- const execaOptions = {
135
+ const spawnOptions = {
154
136
  // Only use topLevelDir as CWD if we are using the git binary
155
137
  // e.g `npm` should run tasks in the actual CWD
156
138
  cwd: /^git(\.exe)?/i.test(cmd) ? topLevelDir : cwd,
157
139
  preferLocal: true,
158
- reject: false,
159
- shell,
160
140
  stdin: 'ignore',
161
141
  }
162
142
 
163
- debugLog('execaOptions:', execaOptions)
143
+ debugLog('Spawn options:', spawnOptions)
164
144
 
165
145
  return async (ctx = getInitialState()) => {
166
- const execaChildProcess = shell
167
- ? execaCommand(
168
- isFn ? command : `${command} ${files.map(escapeSpaces).join(' ')}`,
169
- execaOptions
170
- )
171
- : execa(cmd, isFn ? args : args.concat(files), execaOptions)
172
-
173
- const quitInterruptCheck = interruptExecutionOnError(ctx, execaChildProcess)
174
- const result = await execaChildProcess
175
- await quitInterruptCheck()
176
-
177
- if (result.failed || result.killed || result.signal != null) {
178
- throw makeErr(command, result, ctx)
179
- }
146
+ const subprocess = spawn(cmd, isFn ? args : args.concat(files), spawnOptions)
180
147
 
181
- if (verbose) {
182
- handleOutput(command, result, ctx)
148
+ const quitInterruptCheck = interruptExecutionOnError(ctx, subprocess)
149
+
150
+ try {
151
+ const result = await subprocess
152
+ if (verbose) {
153
+ handleOutput(command, result, ctx)
154
+ }
155
+ } catch (error) {
156
+ throw makeErr(command, error, ctx)
157
+ } finally {
158
+ await quitInterruptCheck()
183
159
  }
184
160
  }
185
161
  }
@@ -1,9 +1,9 @@
1
1
  import debug from 'debug'
2
2
 
3
+ import { getSpawnedTask } from './getSpawnedTask.js'
3
4
  import { configurationError } from './messages.js'
4
- import { resolveTaskFn } from './resolveTaskFn.js'
5
5
 
6
- const debugLog = debug('lint-staged:makeCmdTasks')
6
+ const debugLog = debug('lint-staged:getSpawnedTasks')
7
7
 
8
8
  /**
9
9
  * Creates and returns an array of listr tasks which map to the given commands.
@@ -13,14 +13,14 @@ const debugLog = debug('lint-staged:makeCmdTasks')
13
13
  * @param {string} options.cwd
14
14
  * @param {Array<string>} options.files
15
15
  * @param {string} options.topLevelDir
16
- * @param {Boolean} shell
17
16
  * @param {Boolean} verbose
18
17
  */
19
- export const makeCmdTasks = async ({ commands, cwd, files, topLevelDir, shell, verbose }) => {
20
- debugLog('Creating listr tasks for commands %o', commands)
21
- const commandArray = Array.isArray(commands) ? commands : [commands]
18
+ export const getSpawnedTasks = async ({ commands, cwd, files, topLevelDir, verbose }) => {
19
+ debugLog('Creating Listr tasks for commands %o', commands)
22
20
  const cmdTasks = []
23
21
 
22
+ const commandArray = Array.isArray(commands) ? commands : [commands]
23
+
24
24
  for (const cmd of commandArray) {
25
25
  // command function may return array of commands that already include `stagedFiles`
26
26
  const isFn = typeof cmd === 'function'
@@ -43,7 +43,7 @@ export const makeCmdTasks = async ({ commands, cwd, files, topLevelDir, shell, v
43
43
  )
44
44
  }
45
45
 
46
- const task = resolveTaskFn({ command, cwd, files, topLevelDir, isFn, shell, verbose })
46
+ const task = getSpawnedTask({ command, cwd, files, topLevelDir, isFn, verbose })
47
47
  cmdTasks.push({ title: command, command, task })
48
48
  }
49
49
  }
@@ -31,8 +31,8 @@ export const getStagedFiles = async ({ cwd = process.cwd(), diff, diffFilter } =
31
31
  * roots and get the filename.
32
32
  */
33
33
  return output
34
- .split(':')
35
34
  .slice(1)
35
+ .split('\u0000:')
36
36
  .map(parseGitZOutput)
37
37
  .flatMap(([info, src, dst]) => {
38
38
  const [, dstMode, , , ,] = info.split(' ')
package/lib/index.d.ts CHANGED
@@ -1,12 +1,17 @@
1
- type SyncFunctionTask = (stagedFileNames: string[]) => string | string[]
1
+ type SyncGenerateTask = (stagedFileNames: string[]) => string | string[]
2
2
 
3
- type AsyncFunctionTask = (stagedFileNames: string[]) => Promise<string | string[]>
3
+ type AsyncGenerateTask = (stagedFileNames: string[]) => Promise<string | string[]>
4
4
 
5
- type FunctionTask = SyncFunctionTask | AsyncFunctionTask
5
+ type GenerateTask = SyncGenerateTask | AsyncGenerateTask
6
+
7
+ type TaskFunction = {
8
+ title: string
9
+ task: (stagedFileNames: string[]) => void | Promise<void>
10
+ }
6
11
 
7
12
  export type Configuration =
8
- | Record<string, string | FunctionTask | (string | FunctionTask)[]>
9
- | FunctionTask
13
+ | Record<string, string | TaskFunction | GenerateTask | (string | GenerateTask)[]>
14
+ | GenerateTask
10
15
 
11
16
  export type Options = {
12
17
  /**
@@ -61,11 +66,6 @@ export type Options = {
61
66
  * @default false
62
67
  */
63
68
  relative?: boolean
64
- /**
65
- * Skip parsing of tasks for better shell support
66
- * @default false
67
- */
68
- shell?: boolean
69
69
  /**
70
70
  * Enable the backup stash, and revert in case of errors.
71
71
  * @warn Disabling this also implies `hidePartiallyStaged: false`.
package/lib/index.js CHANGED
@@ -57,7 +57,6 @@ const getMaxArgLength = () => {
57
57
  * @param {number} [options.maxArgLength] - Maximum argument string length
58
58
  * @param {boolean} [options.quiet] - Disable lint-staged’s own console output
59
59
  * @param {boolean} [options.relative] - Pass relative filepaths to tasks
60
- * @param {boolean|string} [options.shell] - Skip parsing of tasks for better shell support
61
60
  * @param {boolean} [options.stash] - Enable the backup stash, and revert in case of errors
62
61
  * @param {boolean} [options.verbose] - Show task output even when tasks succeed; by default only failed output is shown
63
62
  * @param {Logger} [logger]
@@ -77,7 +76,6 @@ const lintStaged = async (
77
76
  maxArgLength = getMaxArgLength() / 2,
78
77
  quiet = false,
79
78
  relative = false,
80
- shell = false,
81
79
  // Stashing should be disabled by default when the `diff` option is used
82
80
  stash = diff === undefined,
83
81
  hidePartiallyStaged = stash,
@@ -112,7 +110,6 @@ const lintStaged = async (
112
110
  maxArgLength,
113
111
  quiet,
114
112
  relative,
115
- shell,
116
113
  stash,
117
114
  hidePartiallyStaged,
118
115
  verbose,
package/lib/runAll.js CHANGED
@@ -9,11 +9,12 @@ import { Listr } from 'listr2'
9
9
  import { chunkFiles } from './chunkFiles.js'
10
10
  import { execGit } from './execGit.js'
11
11
  import { generateTasks } from './generateTasks.js'
12
+ import { getFunctionTask, isFunctionTask } from './getFunctionTask.js'
12
13
  import { getRenderer } from './getRenderer.js'
14
+ import { getSpawnedTasks } from './getSpawnedTasks.js'
13
15
  import { getStagedFiles } from './getStagedFiles.js'
14
16
  import { GitWorkflow } from './gitWorkflow.js'
15
17
  import { groupFilesByConfig } from './groupFilesByConfig.js'
16
- import { makeCmdTasks } from './makeCmdTasks.js'
17
18
  import {
18
19
  DEPRECATED_GIT_ADD,
19
20
  FAILED_GET_STAGED_FILES,
@@ -58,7 +59,6 @@ const createError = (ctx) => Object.assign(new Error('lint-staged failed'), { ct
58
59
  * @param {number} [options.maxArgLength] - Maximum argument string length
59
60
  * @param {boolean} [options.quiet] - Disable lint-staged’s own console output
60
61
  * @param {boolean} [options.relative] - Pass relative filepaths to tasks
61
- * @param {boolean} [options.shell] - Skip parsing of tasks for better shell support
62
62
  * @param {boolean} [options.stash] - Enable the backup stash, and revert in case of errors
63
63
  * @param {boolean} [options.verbose] - Show task output even when tasks succeed; by default only failed output is shown
64
64
  * @param {Logger} logger
@@ -77,7 +77,6 @@ export const runAll = async (
77
77
  maxArgLength,
78
78
  quiet = false,
79
79
  relative = false,
80
- shell = false,
81
80
  // Stashing should be disabled by default when the `diff` option is used
82
81
  stash = diff === undefined,
83
82
  hidePartiallyStaged = stash,
@@ -165,7 +164,7 @@ export const runAll = async (
165
164
  * This is used to set max event listener count to the total number
166
165
  * of generated tasks. The event listener is used to keep track of
167
166
  * the interrupt signal and kill all tasks when it happens. See the
168
- * `interruptExecutionOnError` in `resolveTaskFn`.
167
+ * `interruptExecutionOnError` in `getSpawnedTask`.
169
168
  */
170
169
  let listrTaskCount = 0
171
170
 
@@ -192,14 +191,16 @@ export const runAll = async (
192
191
  for (const [index, files] of stagedFileChunks.entries()) {
193
192
  const chunkListrTasks = await Promise.all(
194
193
  generateTasks({ config, cwd: groupCwd, files, relative }).map((task) =>
195
- makeCmdTasks({
196
- commands: task.commands,
197
- cwd: groupCwd,
198
- files: task.fileList,
199
- topLevelDir,
200
- shell,
201
- verbose,
202
- }).then((subTasks) => {
194
+ (isFunctionTask(task.commands)
195
+ ? getFunctionTask(task.commands, files)
196
+ : getSpawnedTasks({
197
+ commands: task.commands,
198
+ cwd: groupCwd,
199
+ files: task.fileList,
200
+ topLevelDir,
201
+ verbose,
202
+ })
203
+ ).then((subTasks) => {
203
204
  // Add files from task to match set
204
205
  task.fileList.forEach((file) => {
205
206
  // Make sure relative files are normalized to the
@@ -106,7 +106,7 @@ export const searchConfigs = async (
106
106
  /** Get validated configs from the above object, without any `null` values (not found) */
107
107
  const foundConfigs = Object.entries(configs)
108
108
  .filter(([, value]) => !!value)
109
- .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {})
109
+ .reduce((acc, [key, value]) => Object.assign(acc, { [key]: value }), {})
110
110
 
111
111
  /**
112
112
  * Try to find a single config from parent directories
@@ -10,19 +10,6 @@ import { validateBraces } from './validateBraces.js'
10
10
 
11
11
  const debugLog = debug('lint-staged:validateConfig')
12
12
 
13
- const isObject = (test) => test && typeof test === 'object' && !Array.isArray(test)
14
-
15
- const TEST_DEPRECATED_KEYS = new Map([
16
- ['concurrent', (key) => typeof key === 'boolean'],
17
- ['chunkSize', (key) => typeof key === 'number'],
18
- ['globOptions', isObject],
19
- ['linters', isObject],
20
- ['ignore', (key) => Array.isArray(key)],
21
- ['subTaskConcurrency', (key) => typeof key === 'number'],
22
- ['renderer', (key) => typeof key === 'string'],
23
- ['relative', (key) => typeof key === 'boolean'],
24
- ])
25
-
26
13
  export const validateConfigLogic = (config, configPath, logger) => {
27
14
  debugLog('Validating config from `%s`...', configPath)
28
15
 
@@ -35,7 +22,7 @@ export const validateConfigLogic = (config, configPath, logger) => {
35
22
  * They are not further validated here to make sure the function gets
36
23
  * evaluated only once.
37
24
  *
38
- * @see makeCmdTasks
25
+ * @see getSpawnedTasks
39
26
  */
40
27
  if (typeof config === 'function') {
41
28
  return { '*': config }
@@ -53,29 +40,30 @@ export const validateConfigLogic = (config, configPath, logger) => {
53
40
  * it can be used for validating the values at the same time.
54
41
  */
55
42
  const validatedConfig = Object.entries(config).reduce((collection, [pattern, task]) => {
56
- /** Versions < 9 had more complex configuration options that are no longer supported. */
57
- if (TEST_DEPRECATED_KEYS.has(pattern)) {
58
- const testFn = TEST_DEPRECATED_KEYS.get(pattern)
59
- if (testFn(task)) {
43
+ if (Array.isArray(task)) {
44
+ /** Array with invalid values */
45
+ if (task.some((item) => typeof item !== 'string' && typeof item !== 'function')) {
60
46
  errors.push(
61
- configurationError(pattern, 'Advanced configuration has been deprecated.', task)
47
+ configurationError(pattern, 'Should be an array of strings or functions.', task)
62
48
  )
63
49
  }
64
-
65
- /** Return early for deprecated keys to skip validating their (deprecated) values */
66
- return collection
67
- }
68
-
69
- if (
70
- (!Array.isArray(task) ||
71
- task.some((item) => typeof item !== 'string' && typeof item !== 'function')) &&
72
- typeof task !== 'string' &&
73
- typeof task !== 'function'
74
- ) {
50
+ } else if (typeof task === 'object') {
51
+ /** Invalid function task */
52
+ if (typeof task.title !== 'string' || typeof task.task !== 'function') {
53
+ errors.push(
54
+ configurationError(
55
+ pattern,
56
+ 'Function task should contain `title` and `task` fields, where `title` should be a string and `task` should be a function.',
57
+ task
58
+ )
59
+ )
60
+ }
61
+ } else if (typeof task !== 'string' && typeof task !== 'function') {
62
+ /** Singular invalid value */
75
63
  errors.push(
76
64
  configurationError(
77
65
  pattern,
78
- 'Should be a string, a function, or an array of strings and functions.',
66
+ 'Should be a string, a function, an object or an array of strings and functions.',
79
67
  task
80
68
  )
81
69
  )
@@ -87,7 +75,7 @@ export const validateConfigLogic = (config, configPath, logger) => {
87
75
  */
88
76
  const fixedPattern = validateBraces(pattern, logger)
89
77
 
90
- return { ...collection, [fixedPattern]: task }
78
+ return Object.assign(collection, { [fixedPattern]: task })
91
79
  }, {})
92
80
 
93
81
  if (errors.length) {
@@ -13,8 +13,6 @@ const debugLog = debug('lint-staged:validateOptions')
13
13
  * Validate lint-staged options, either from the Node.js API or the command line flags.
14
14
  * @param {*} options
15
15
  * @param {boolean|string} [options.cwd] - Current working directory
16
- * @param {boolean|string} [options.shell] - Skip parsing of tasks for better shell support
17
- *
18
16
  * @throws {InvalidOptionsError}
19
17
  */
20
18
  export const validateOptions = async (options = {}, logger) => {
@@ -32,16 +30,5 @@ export const validateOptions = async (options = {}, logger) => {
32
30
  }
33
31
  }
34
32
 
35
- /** Ensure the passed shell option is executable */
36
- if (typeof options.shell === 'string') {
37
- try {
38
- await fs.access(options.shell, constants.X_OK)
39
- } catch (error) {
40
- debugLog('Failed to validate options: %o', options)
41
- logger.error(invalidOption('shell', options.shell, error.message))
42
- throw InvalidOptionsError
43
- }
44
- }
45
-
46
33
  debugLog('Validated options: %o', options)
47
34
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lint-staged",
3
- "version": "15.5.1",
3
+ "version": "16.0.0",
4
4
  "description": "Lint files staged by git",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -21,7 +21,7 @@
21
21
  "url": "https://opencollective.com/lint-staged"
22
22
  },
23
23
  "engines": {
24
- "node": ">=18.12.0"
24
+ "node": ">=20.18"
25
25
  },
26
26
  "type": "module",
27
27
  "bin": {
@@ -33,8 +33,9 @@
33
33
  "./package.json": "./package.json"
34
34
  },
35
35
  "files": [
36
- "bin",
37
- "lib"
36
+ "bin/",
37
+ "lib/",
38
+ "MIGRATION.md"
38
39
  ],
39
40
  "scripts": {
40
41
  "lint": "eslint .",
@@ -49,27 +50,27 @@
49
50
  "chalk": "^5.4.1",
50
51
  "commander": "^13.1.0",
51
52
  "debug": "^4.4.0",
52
- "execa": "^8.0.1",
53
53
  "lilconfig": "^3.1.3",
54
- "listr2": "^8.2.5",
54
+ "listr2": "^8.3.3",
55
55
  "micromatch": "^4.0.8",
56
+ "nano-spawn": "^1.0.0",
56
57
  "pidtree": "^0.6.0",
57
58
  "string-argv": "^0.3.2",
58
- "yaml": "^2.7.0"
59
+ "yaml": "^2.7.1"
59
60
  },
60
61
  "devDependencies": {
61
62
  "@changesets/changelog-github": "0.5.1",
62
- "@changesets/cli": "2.28.1",
63
- "@commitlint/cli": "19.8.0",
64
- "@commitlint/config-conventional": "19.8.0",
65
- "@eslint/js": "9.22.0",
63
+ "@changesets/cli": "2.29.3",
64
+ "@commitlint/cli": "19.8.1",
65
+ "@commitlint/config-conventional": "19.8.1",
66
+ "@eslint/js": "9.26.0",
66
67
  "consolemock": "1.1.0",
67
68
  "cross-env": "7.0.3",
68
- "eslint": "9.22.0",
69
- "eslint-config-prettier": "10.1.1",
69
+ "eslint": "9.26.0",
70
+ "eslint-config-prettier": "10.1.5",
70
71
  "eslint-plugin-jest": "28.11.0",
71
- "eslint-plugin-n": "17.16.2",
72
- "eslint-plugin-prettier": "5.2.3",
72
+ "eslint-plugin-n": "17.18.0",
73
+ "eslint-plugin-prettier": "5.4.0",
73
74
  "eslint-plugin-simple-import-sort": "12.1.1",
74
75
  "husky": "9.1.7",
75
76
  "jest": "29.7.0",
@@ -77,7 +78,7 @@
77
78
  "mock-stdin": "1.0.0",
78
79
  "prettier": "3.5.3",
79
80
  "semver": "7.7.1",
80
- "typescript": "5.8.2"
81
+ "typescript": "5.8.3"
81
82
  },
82
83
  "keywords": [
83
84
  "lint",