lint-staged 12.1.7 → 12.3.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 +60 -18
- package/bin/lint-staged.js +8 -6
- package/lib/dynamicImport.js +3 -0
- package/lib/generateTasks.js +2 -3
- package/lib/getConfigGroups.js +112 -0
- package/lib/getStagedFiles.js +19 -7
- package/lib/index.js +5 -31
- package/lib/loadConfig.js +9 -11
- package/lib/runAll.js +88 -61
- package/lib/validateConfig.js +12 -4
- package/lib/validateOptions.js +13 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,8 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
Run linters against staged git files and don't let :poop: slip into your code base!
|
|
4
4
|
|
|
5
|
+
```
|
|
6
|
+
$ git commit
|
|
7
|
+
|
|
8
|
+
✔ Preparing lint-staged...
|
|
9
|
+
❯ Running tasks for staged files...
|
|
10
|
+
❯ packages/frontend/.lintstagedrc.json — 1 file
|
|
11
|
+
↓ *.js — no files [SKIPPED]
|
|
12
|
+
❯ *.{json,md} — 1 file
|
|
13
|
+
⠹ prettier --write
|
|
14
|
+
↓ packages/backend/.lintstagedrc.json — 2 files
|
|
15
|
+
❯ *.js — 2 files
|
|
16
|
+
⠼ eslint --fix
|
|
17
|
+
↓ *.{json,md} — no files [SKIPPED]
|
|
18
|
+
◼ Applying modifications from tasks...
|
|
19
|
+
◼ Cleaning up temporary files...
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
<details>
|
|
23
|
+
<summary>See asciinema video</summary>
|
|
24
|
+
|
|
5
25
|
[](https://asciinema.org/a/199934)
|
|
6
26
|
|
|
27
|
+
</details>
|
|
28
|
+
|
|
7
29
|
## Why
|
|
8
30
|
|
|
9
31
|
Linting makes more sense when run before committing your code. By doing so you can ensure no errors go into the repository and enforce code style. But running a lint process on a whole project is slow, and linting results can be irrelevant. Ultimately you only want to lint files that will be committed.
|
|
@@ -59,38 +81,36 @@ See [Releases](https://github.com/okonet/lint-staged/releases).
|
|
|
59
81
|
|
|
60
82
|
## Command line flags
|
|
61
83
|
|
|
62
|
-
```
|
|
84
|
+
```
|
|
63
85
|
❯ npx lint-staged --help
|
|
64
86
|
Usage: lint-staged [options]
|
|
65
87
|
|
|
66
88
|
Options:
|
|
67
89
|
-V, --version output the version number
|
|
68
|
-
--allow-empty allow empty commits when tasks revert all staged changes
|
|
69
|
-
|
|
90
|
+
--allow-empty allow empty commits when tasks revert all staged changes (default: false)
|
|
91
|
+
-p, --concurrent <number|boolean> the number of tasks to run concurrently, or false for serial (default: true)
|
|
70
92
|
-c, --config [path] path to configuration file, or - to read from stdin
|
|
93
|
+
--cwd [path] run all tasks in specific directory, instead of the current
|
|
71
94
|
-d, --debug print additional debug information (default: false)
|
|
72
|
-
--no-stash disable the backup stash, and do not revert in case of
|
|
73
|
-
errors
|
|
74
|
-
-p, --concurrent <parallel tasks> the number of tasks to run concurrently, or false to run
|
|
75
|
-
tasks serially (default: true)
|
|
95
|
+
--no-stash disable the backup stash, and do not revert in case of errors
|
|
76
96
|
-q, --quiet disable lint-staged’s own console output (default: false)
|
|
77
97
|
-r, --relative pass relative filepaths to tasks (default: false)
|
|
78
|
-
-x, --shell [path] skip parsing of tasks for better shell support (default:
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
failed output is shown (default: false)
|
|
98
|
+
-x, --shell [path] skip parsing of tasks for better shell support (default: false)
|
|
99
|
+
-v, --verbose show task output even when tasks succeed; by default only failed output is shown
|
|
100
|
+
(default: false)
|
|
82
101
|
-h, --help display help for command
|
|
83
102
|
```
|
|
84
103
|
|
|
85
104
|
- **`--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.
|
|
86
|
-
- **`--
|
|
87
|
-
- **`--debug`**: Run in debug mode. When set, it does the following:
|
|
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:
|
|
88
106
|
- 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*`.
|
|
89
107
|
- 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.
|
|
90
|
-
- **`--concurrent [number | (true/false)]`**: 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:
|
|
91
108
|
- `false`: Run all tasks serially
|
|
92
109
|
- `true` (default) : _Infinite_ concurrency. Runs as many tasks in parallel as possible.
|
|
93
110
|
- `{number}`: Run the specified number of tasks in parallel, where `1` is equivalent to `false`.
|
|
111
|
+
- **`--config [path]`**: Manually specify a path to a config file or npm package name. Note: when used, lint-staged won't perform the config file search and will print an error if the specified file cannot be found. If '-' is provided as the filename then the config will be read from stdin, allowing piping in the config like `cat my-config.json | npx lint-staged --config -`.
|
|
112
|
+
- **`--cwd [path]`**: By default tasks run in the current working directory. Use the `--cwd some/directory` to override this. The path can be absolute or relative to the current working directory.
|
|
113
|
+
- **`--debug`**: Run in debug mode. When set, it does the following:
|
|
94
114
|
- **`--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.
|
|
95
115
|
- **`--quiet`**: Supress all CLI output, except from tasks.
|
|
96
116
|
- **`--relative`**: Pass filepaths relative to `process.cwd()` (where `lint-staged` runs) to tasks. Default is `false`.
|
|
@@ -116,6 +136,8 @@ Starting with v3.1 you can now use different ways of configuring lint-staged:
|
|
|
116
136
|
|
|
117
137
|
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.
|
|
118
138
|
|
|
139
|
+
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.
|
|
140
|
+
|
|
119
141
|
#### `package.json` example:
|
|
120
142
|
|
|
121
143
|
```json
|
|
@@ -644,12 +666,32 @@ _Thanks to [this comment](https://youtrack.jetbrains.com/issue/IDEA-135454#comme
|
|
|
644
666
|
<details>
|
|
645
667
|
<summary>Click to expand</summary>
|
|
646
668
|
|
|
647
|
-
|
|
669
|
+
Install _lint-staged_ on the monorepo root level, and add separate configuration files in each package. When running, _lint-staged_ will always use the configuration closest to a staged file, so having separate configuration files makes sure linters do not "leak" into other packages.
|
|
670
|
+
|
|
671
|
+
For example, in a monorepo with `packages/frontend/.lintstagedrc.json` and `packages/backend/.lintstagedrc.json`, a staged file inside `packages/frontend/` will only match that configuration, and not the one in `packages/backend/`.
|
|
672
|
+
|
|
673
|
+
**Note**: _lint-staged_ discovers the closest configuration to each staged file, even if that configuration doesn't include any matching globs. Given these example configurations:
|
|
648
674
|
|
|
649
|
-
|
|
650
|
-
|
|
675
|
+
```js
|
|
676
|
+
// ./.lintstagedrc.json
|
|
677
|
+
{ "*.md": "prettier --write" }
|
|
678
|
+
```
|
|
651
679
|
|
|
652
|
-
|
|
680
|
+
```js
|
|
681
|
+
// ./packages/frontend/.lintstagedrc.json
|
|
682
|
+
{ "*.js": "eslint --fix" }
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
When committing `./packages/frontend/README.md`, it **will not run** _prettier_, because the configuration in the `frontend/` directory is closer to the file and doesn't include it. You should treat all _lint-staged_ configuration files as isolated and separated from each other. You can always use JS files to "extend" configurations, for example:
|
|
686
|
+
|
|
687
|
+
```js
|
|
688
|
+
import baseConfig from '../.lintstagedrc.js'
|
|
689
|
+
|
|
690
|
+
export default {
|
|
691
|
+
...baseConfig,
|
|
692
|
+
'*.js': 'eslint --fix',
|
|
693
|
+
}
|
|
694
|
+
```
|
|
653
695
|
|
|
654
696
|
</details>
|
|
655
697
|
|
package/bin/lint-staged.js
CHANGED
|
@@ -26,14 +26,15 @@ const version = packageJson.version
|
|
|
26
26
|
cmdline
|
|
27
27
|
.version(version)
|
|
28
28
|
.option('--allow-empty', 'allow empty commits when tasks revert all staged changes', false)
|
|
29
|
-
.option('-c, --config [path]', 'path to configuration file, or - to read from stdin')
|
|
30
|
-
.option('-d, --debug', 'print additional debug information', false)
|
|
31
|
-
.option('--no-stash', 'disable the backup stash, and do not revert in case of errors', false)
|
|
32
29
|
.option(
|
|
33
|
-
'-p, --concurrent <
|
|
34
|
-
'the number of tasks to run concurrently, or false
|
|
30
|
+
'-p, --concurrent <number|boolean>',
|
|
31
|
+
'the number of tasks to run concurrently, or false for serial',
|
|
35
32
|
true
|
|
36
33
|
)
|
|
34
|
+
.option('-c, --config [path]', 'path to configuration file, or - to read from stdin')
|
|
35
|
+
.option('--cwd [path]', 'run all tasks in specific directory, instead of the current')
|
|
36
|
+
.option('-d, --debug', 'print additional debug information', false)
|
|
37
|
+
.option('--no-stash', 'disable the backup stash, and do not revert in case of errors', false)
|
|
37
38
|
.option('-q, --quiet', 'disable lint-staged’s own console output', false)
|
|
38
39
|
.option('-r, --relative', 'pass relative filepaths to tasks', false)
|
|
39
40
|
.option('-x, --shell [path]', 'skip parsing of tasks for better shell support', false)
|
|
@@ -75,12 +76,13 @@ const options = {
|
|
|
75
76
|
allowEmpty: !!cmdlineOptions.allowEmpty,
|
|
76
77
|
concurrent: JSON.parse(cmdlineOptions.concurrent),
|
|
77
78
|
configPath: cmdlineOptions.config,
|
|
79
|
+
cwd: cmdlineOptions.cwd,
|
|
78
80
|
debug: !!cmdlineOptions.debug,
|
|
79
81
|
maxArgLength: getMaxArgLength() / 2,
|
|
80
|
-
stash: !!cmdlineOptions.stash, // commander inverts `no-<x>` flags to `!x`
|
|
81
82
|
quiet: !!cmdlineOptions.quiet,
|
|
82
83
|
relative: !!cmdlineOptions.relative,
|
|
83
84
|
shell: cmdlineOptions.shell /* Either a boolean or a string pointing to the shell */,
|
|
85
|
+
stash: !!cmdlineOptions.stash, // commander inverts `no-<x>` flags to `!x`
|
|
84
86
|
verbose: !!cmdlineOptions.verbose,
|
|
85
87
|
}
|
|
86
88
|
|
package/lib/generateTasks.js
CHANGED
|
@@ -16,11 +16,10 @@ const debugLog = debug('lint-staged:generateTasks')
|
|
|
16
16
|
* @param {boolean} [options.files] - Staged filepaths
|
|
17
17
|
* @param {boolean} [options.relative] - Whether filepaths to should be relative to gitDir
|
|
18
18
|
*/
|
|
19
|
-
export const generateTasks = ({ config, cwd = process.cwd(),
|
|
19
|
+
export const generateTasks = ({ config, cwd = process.cwd(), files, relative = false }) => {
|
|
20
20
|
debugLog('Generating linter tasks')
|
|
21
21
|
|
|
22
|
-
const
|
|
23
|
-
const relativeFiles = absoluteFiles.map((file) => normalize(path.relative(cwd, file)))
|
|
22
|
+
const relativeFiles = files.map((file) => normalize(path.relative(cwd, file)))
|
|
24
23
|
|
|
25
24
|
return Object.entries(config).map(([rawPattern, commands]) => {
|
|
26
25
|
let pattern = rawPattern
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/** @typedef {import('./index').Logger} Logger */
|
|
2
|
+
|
|
3
|
+
import path from 'path'
|
|
4
|
+
|
|
5
|
+
import debug from 'debug'
|
|
6
|
+
import objectInspect from 'object-inspect'
|
|
7
|
+
|
|
8
|
+
import { loadConfig } from './loadConfig.js'
|
|
9
|
+
import { ConfigNotFoundError } from './symbols.js'
|
|
10
|
+
import { validateConfig } from './validateConfig.js'
|
|
11
|
+
|
|
12
|
+
const debugLog = debug('lint-staged:getConfigGroups')
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Return matched files grouped by their configuration.
|
|
16
|
+
*
|
|
17
|
+
* @param {object} options
|
|
18
|
+
* @param {Object} [options.configObject] - Explicit config object from the js API
|
|
19
|
+
* @param {string} [options.configPath] - Explicit path to a config file
|
|
20
|
+
* @param {string} [options.cwd] - Current working directory
|
|
21
|
+
* @param {string} [options.files] - List of staged files
|
|
22
|
+
* @param {Logger} logger
|
|
23
|
+
*/
|
|
24
|
+
export const getConfigGroups = async (
|
|
25
|
+
{ configObject, configPath, cwd, files },
|
|
26
|
+
logger = console
|
|
27
|
+
) => {
|
|
28
|
+
debugLog('Grouping configuration files...')
|
|
29
|
+
|
|
30
|
+
// Return explicit config object from js API
|
|
31
|
+
if (configObject) {
|
|
32
|
+
debugLog('Using single direct configuration object...')
|
|
33
|
+
|
|
34
|
+
const config = validateConfig(configObject, 'config object', logger)
|
|
35
|
+
return { '': { config, files } }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Use only explicit config path instead of discovering multiple
|
|
39
|
+
if (configPath) {
|
|
40
|
+
debugLog('Using single configuration path...')
|
|
41
|
+
|
|
42
|
+
const { config, filepath } = await loadConfig({ configPath }, logger)
|
|
43
|
+
|
|
44
|
+
if (!config) {
|
|
45
|
+
logger.error(`${ConfigNotFoundError.message}.`)
|
|
46
|
+
throw ConfigNotFoundError
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const validatedConfig = validateConfig(config, filepath, logger)
|
|
50
|
+
return { [configPath]: { config: validatedConfig, files } }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
debugLog('Grouping staged files by their directories...')
|
|
54
|
+
|
|
55
|
+
// Group files by their base directory
|
|
56
|
+
const filesByDir = files.reduce((acc, file) => {
|
|
57
|
+
const dir = path.normalize(path.dirname(file))
|
|
58
|
+
|
|
59
|
+
if (dir in acc) {
|
|
60
|
+
acc[dir].push(file)
|
|
61
|
+
} else {
|
|
62
|
+
acc[dir] = [file]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return acc
|
|
66
|
+
}, {})
|
|
67
|
+
|
|
68
|
+
debugLog('Grouped staged files into %d directories:', Object.keys(filesByDir).length)
|
|
69
|
+
debugLog(objectInspect(filesByDir, { indent: 2 }))
|
|
70
|
+
|
|
71
|
+
// Group files by their discovered config
|
|
72
|
+
// { '.lintstagedrc.json': { config: {...}, files: [...] } }
|
|
73
|
+
const configGroups = {}
|
|
74
|
+
|
|
75
|
+
debugLog('Searching config files...')
|
|
76
|
+
|
|
77
|
+
const searchConfig = async (cwd, files = []) => {
|
|
78
|
+
const { config, filepath } = await loadConfig({ cwd }, logger)
|
|
79
|
+
if (!config) {
|
|
80
|
+
debugLog('Found no config from "%s"!', cwd)
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (filepath in configGroups) {
|
|
85
|
+
debugLog('Found existing config "%s" from "%s"!', filepath, cwd)
|
|
86
|
+
// Re-use cached config and skip validation
|
|
87
|
+
configGroups[filepath].files.push(...files)
|
|
88
|
+
} else {
|
|
89
|
+
debugLog('Found new config "%s" from "%s"!', filepath, cwd)
|
|
90
|
+
|
|
91
|
+
const validatedConfig = validateConfig(config, filepath, logger)
|
|
92
|
+
configGroups[filepath] = { config: validatedConfig, files }
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Start by searching from cwd
|
|
97
|
+
await searchConfig(cwd)
|
|
98
|
+
|
|
99
|
+
// Discover configs from the base directory of each file
|
|
100
|
+
await Promise.all(Object.entries(filesByDir).map(([dir, files]) => searchConfig(dir, files)))
|
|
101
|
+
|
|
102
|
+
// Throw if no configurations were found
|
|
103
|
+
if (Object.keys(configGroups).length === 0) {
|
|
104
|
+
debugLog('Found no config groups!')
|
|
105
|
+
logger.error(`${ConfigNotFoundError.message}.`)
|
|
106
|
+
throw ConfigNotFoundError
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
debugLog('Grouped staged files into %d groups!', Object.keys(configGroups).length)
|
|
110
|
+
|
|
111
|
+
return configGroups
|
|
112
|
+
}
|
package/lib/getStagedFiles.js
CHANGED
|
@@ -1,16 +1,28 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
|
|
3
|
+
import normalize from 'normalize-path'
|
|
4
|
+
|
|
1
5
|
import { execGit } from './execGit.js'
|
|
2
6
|
|
|
3
|
-
export const getStagedFiles = async (
|
|
7
|
+
export const getStagedFiles = async ({ cwd = process.cwd() } = {}) => {
|
|
4
8
|
try {
|
|
5
9
|
// Docs for --diff-filter option: https://git-scm.com/docs/git-diff#Documentation/git-diff.txt---diff-filterACDMRTUXB82308203
|
|
6
10
|
// Docs for -z option: https://git-scm.com/docs/git-diff#Documentation/git-diff.txt--z
|
|
7
|
-
const lines = await execGit(
|
|
8
|
-
|
|
9
|
-
|
|
11
|
+
const lines = await execGit(['diff', '--staged', '--diff-filter=ACMR', '--name-only', '-z'], {
|
|
12
|
+
cwd,
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
if (!lines) return []
|
|
16
|
+
|
|
17
|
+
// With `-z`, git prints `fileA\u0000fileB\u0000fileC\u0000` so we need to
|
|
18
|
+
// remove the last occurrence of `\u0000` before splitting
|
|
19
|
+
return (
|
|
20
|
+
lines
|
|
21
|
+
// eslint-disable-next-line no-control-regex
|
|
22
|
+
.replace(/\u0000$/, '')
|
|
23
|
+
.split('\u0000')
|
|
24
|
+
.map((file) => normalize(path.resolve(cwd, file)))
|
|
10
25
|
)
|
|
11
|
-
// With `-z`, git prints `fileA\u0000fileB\u0000fileC\u0000` so we need to remove the last occurrence of `\u0000` before splitting
|
|
12
|
-
// eslint-disable-next-line no-control-regex
|
|
13
|
-
return lines ? lines.replace(/\u0000$/, '').split('\u0000') : []
|
|
14
26
|
} catch {
|
|
15
27
|
return null
|
|
16
28
|
}
|
package/lib/index.js
CHANGED
|
@@ -1,17 +1,9 @@
|
|
|
1
1
|
import debug from 'debug'
|
|
2
|
-
import inspect from 'object-inspect'
|
|
3
2
|
|
|
4
|
-
import { loadConfig } from './loadConfig.js'
|
|
5
3
|
import { PREVENTED_EMPTY_COMMIT, GIT_ERROR, RESTORE_STASH_EXAMPLE } from './messages.js'
|
|
6
4
|
import { printTaskOutput } from './printTaskOutput.js'
|
|
7
5
|
import { runAll } from './runAll.js'
|
|
8
|
-
import {
|
|
9
|
-
ApplyEmptyCommitError,
|
|
10
|
-
ConfigNotFoundError,
|
|
11
|
-
GetBackupStashError,
|
|
12
|
-
GitError,
|
|
13
|
-
} from './symbols.js'
|
|
14
|
-
import { validateConfig } from './validateConfig.js'
|
|
6
|
+
import { ApplyEmptyCommitError, GetBackupStashError, GitError } from './symbols.js'
|
|
15
7
|
import { validateOptions } from './validateOptions.js'
|
|
16
8
|
|
|
17
9
|
const debugLog = debug('lint-staged')
|
|
@@ -45,7 +37,7 @@ const lintStaged = async (
|
|
|
45
37
|
concurrent = true,
|
|
46
38
|
config: configObject,
|
|
47
39
|
configPath,
|
|
48
|
-
cwd
|
|
40
|
+
cwd,
|
|
49
41
|
debug = false,
|
|
50
42
|
maxArgLength,
|
|
51
43
|
quiet = false,
|
|
@@ -56,26 +48,7 @@ const lintStaged = async (
|
|
|
56
48
|
} = {},
|
|
57
49
|
logger = console
|
|
58
50
|
) => {
|
|
59
|
-
await validateOptions({ shell }, logger)
|
|
60
|
-
|
|
61
|
-
const inputConfig = configObject || (await loadConfig({ configPath, cwd }, logger))
|
|
62
|
-
|
|
63
|
-
if (!inputConfig) {
|
|
64
|
-
logger.error(`${ConfigNotFoundError.message}.`)
|
|
65
|
-
throw ConfigNotFoundError
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const config = validateConfig(inputConfig, logger)
|
|
69
|
-
|
|
70
|
-
if (debug) {
|
|
71
|
-
// Log using logger to be able to test through `consolemock`.
|
|
72
|
-
logger.log('Running lint-staged with the following config:')
|
|
73
|
-
logger.log(inspect(config, { indent: 2 }))
|
|
74
|
-
} else {
|
|
75
|
-
// We might not be in debug mode but `DEBUG=lint-staged*` could have
|
|
76
|
-
// been set.
|
|
77
|
-
debugLog('lint-staged config:\n%O', config)
|
|
78
|
-
}
|
|
51
|
+
await validateOptions({ cwd, shell }, logger)
|
|
79
52
|
|
|
80
53
|
// Unset GIT_LITERAL_PATHSPECS to not mess with path interpretation
|
|
81
54
|
debugLog('Unset GIT_LITERAL_PATHSPECS (was `%s`)', process.env.GIT_LITERAL_PATHSPECS)
|
|
@@ -86,7 +59,8 @@ const lintStaged = async (
|
|
|
86
59
|
{
|
|
87
60
|
allowEmpty,
|
|
88
61
|
concurrent,
|
|
89
|
-
|
|
62
|
+
configObject,
|
|
63
|
+
configPath,
|
|
90
64
|
cwd,
|
|
91
65
|
debug,
|
|
92
66
|
maxArgLength,
|
package/lib/loadConfig.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/** @typedef {import('./index').Logger} Logger */
|
|
2
2
|
|
|
3
|
-
import { pathToFileURL } from 'url'
|
|
4
|
-
|
|
5
3
|
import debug from 'debug'
|
|
6
4
|
import { lilconfig } from 'lilconfig'
|
|
7
5
|
import YAML from 'yaml'
|
|
8
6
|
|
|
7
|
+
import { dynamicImport } from './dynamicImport.js'
|
|
9
8
|
import { resolveConfig } from './resolveConfig.js'
|
|
10
9
|
|
|
11
10
|
const debugLog = debug('lint-staged:loadConfig')
|
|
@@ -28,9 +27,6 @@ const searchPlaces = [
|
|
|
28
27
|
'lint-staged.config.cjs',
|
|
29
28
|
]
|
|
30
29
|
|
|
31
|
-
/** exported for tests */
|
|
32
|
-
export const dynamicImport = (path) => import(pathToFileURL(path)).then((module) => module.default)
|
|
33
|
-
|
|
34
30
|
const jsonParse = (path, content) => JSON.parse(content)
|
|
35
31
|
|
|
36
32
|
const yamlParse = (path, content) => YAML.parse(content)
|
|
@@ -51,6 +47,8 @@ const loaders = {
|
|
|
51
47
|
noExt: yamlParse,
|
|
52
48
|
}
|
|
53
49
|
|
|
50
|
+
const explorer = lilconfig('lint-staged', { searchPlaces, loaders })
|
|
51
|
+
|
|
54
52
|
/**
|
|
55
53
|
* @param {object} options
|
|
56
54
|
* @param {string} [options.configPath] - Explicit path to a config file
|
|
@@ -64,22 +62,22 @@ export const loadConfig = async ({ configPath, cwd }, logger) => {
|
|
|
64
62
|
debugLog('Searching for configuration from `%s`...', cwd)
|
|
65
63
|
}
|
|
66
64
|
|
|
67
|
-
const explorer = lilconfig('lint-staged', { searchPlaces, loaders })
|
|
68
|
-
|
|
69
65
|
const result = await (configPath
|
|
70
66
|
? explorer.load(resolveConfig(configPath))
|
|
71
67
|
: explorer.search(cwd))
|
|
72
|
-
|
|
68
|
+
|
|
69
|
+
if (!result) return {}
|
|
73
70
|
|
|
74
71
|
// config is a promise when using the `dynamicImport` loader
|
|
75
72
|
const config = await result.config
|
|
73
|
+
const filepath = result.filepath
|
|
76
74
|
|
|
77
|
-
debugLog('Successfully loaded config from `%s`:\n%O',
|
|
75
|
+
debugLog('Successfully loaded config from `%s`:\n%O', filepath, config)
|
|
78
76
|
|
|
79
|
-
return config
|
|
77
|
+
return { config, filepath }
|
|
80
78
|
} catch (error) {
|
|
81
79
|
debugLog('Failed to load configuration!')
|
|
82
80
|
logger.error(error)
|
|
83
|
-
return
|
|
81
|
+
return {}
|
|
84
82
|
}
|
|
85
83
|
}
|
package/lib/runAll.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
/** @typedef {import('./index').Logger} Logger */
|
|
2
2
|
|
|
3
|
+
import path from 'path'
|
|
4
|
+
|
|
5
|
+
import { dim } from 'colorette'
|
|
3
6
|
import debug from 'debug'
|
|
4
7
|
import { Listr } from 'listr2'
|
|
8
|
+
import normalize from 'normalize-path'
|
|
5
9
|
|
|
6
10
|
import { chunkFiles } from './chunkFiles.js'
|
|
7
11
|
import { execGit } from './execGit.js'
|
|
8
12
|
import { generateTasks } from './generateTasks.js'
|
|
13
|
+
import { getConfigGroups } from './getConfigGroups.js'
|
|
9
14
|
import { getRenderer } from './getRenderer.js'
|
|
10
15
|
import { getStagedFiles } from './getStagedFiles.js'
|
|
11
16
|
import { GitWorkflow } from './gitWorkflow.js'
|
|
@@ -40,10 +45,11 @@ const createError = (ctx) => Object.assign(new Error('lint-staged failed'), { ct
|
|
|
40
45
|
* Executes all tasks and either resolves or rejects the promise
|
|
41
46
|
*
|
|
42
47
|
* @param {object} options
|
|
43
|
-
* @param {
|
|
48
|
+
* @param {boolean} [options.allowEmpty] - Allow empty commits when tasks revert all staged changes
|
|
44
49
|
* @param {boolean | number} [options.concurrent] - The number of tasks to run concurrently, or false to run tasks serially
|
|
45
|
-
* @param {Object} [options.
|
|
46
|
-
* @param {
|
|
50
|
+
* @param {Object} [options.configObject] - Explicit config object from the js API
|
|
51
|
+
* @param {string} [options.configPath] - Explicit path to a config file
|
|
52
|
+
* @param {string} [options.cwd] - Current working directory
|
|
47
53
|
* @param {boolean} [options.debug] - Enable debug mode
|
|
48
54
|
* @param {number} [options.maxArgLength] - Maximum argument string length
|
|
49
55
|
* @param {boolean} [options.quiet] - Disable lint-staged’s own console output
|
|
@@ -58,8 +64,9 @@ export const runAll = async (
|
|
|
58
64
|
{
|
|
59
65
|
allowEmpty = false,
|
|
60
66
|
concurrent = true,
|
|
61
|
-
|
|
62
|
-
|
|
67
|
+
configObject,
|
|
68
|
+
configPath,
|
|
69
|
+
cwd,
|
|
63
70
|
debug = false,
|
|
64
71
|
maxArgLength,
|
|
65
72
|
quiet = false,
|
|
@@ -70,7 +77,11 @@ export const runAll = async (
|
|
|
70
77
|
},
|
|
71
78
|
logger = console
|
|
72
79
|
) => {
|
|
73
|
-
debugLog('Running all linter scripts')
|
|
80
|
+
debugLog('Running all linter scripts...')
|
|
81
|
+
|
|
82
|
+
// Resolve relative CWD option
|
|
83
|
+
cwd = cwd ? path.resolve(cwd) : process.cwd()
|
|
84
|
+
debugLog('Using working directory `%s`', cwd)
|
|
74
85
|
|
|
75
86
|
const ctx = getInitialState({ quiet })
|
|
76
87
|
|
|
@@ -107,9 +118,7 @@ export const runAll = async (
|
|
|
107
118
|
return ctx
|
|
108
119
|
}
|
|
109
120
|
|
|
110
|
-
const
|
|
111
|
-
const chunkCount = stagedFileChunks.length
|
|
112
|
-
if (chunkCount > 1) debugLog(`Chunked staged files into ${chunkCount} part`, chunkCount)
|
|
121
|
+
const configGroups = await getConfigGroups({ configObject, configPath, cwd, files }, logger)
|
|
113
122
|
|
|
114
123
|
// lint-staged 10 will automatically add modifications to index
|
|
115
124
|
// Warn user when their command includes `git add`
|
|
@@ -128,62 +137,76 @@ export const runAll = async (
|
|
|
128
137
|
// Set of all staged files that matched a task glob. Values in a set are unique.
|
|
129
138
|
const matchedFiles = new Set()
|
|
130
139
|
|
|
131
|
-
for (const [
|
|
132
|
-
const
|
|
133
|
-
const chunkListrTasks = []
|
|
134
|
-
|
|
135
|
-
for (const task of chunkTasks) {
|
|
136
|
-
const subTasks = await makeCmdTasks({
|
|
137
|
-
commands: task.commands,
|
|
138
|
-
cwd,
|
|
139
|
-
files: task.fileList,
|
|
140
|
-
gitDir,
|
|
141
|
-
renderer: listrOptions.renderer,
|
|
142
|
-
shell,
|
|
143
|
-
verbose,
|
|
144
|
-
})
|
|
140
|
+
for (const [configPath, { config, files }] of Object.entries(configGroups)) {
|
|
141
|
+
const stagedFileChunks = chunkFiles({ baseDir: gitDir, files, maxArgLength, relative })
|
|
145
142
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
143
|
+
const chunkCount = stagedFileChunks.length
|
|
144
|
+
if (chunkCount > 1) {
|
|
145
|
+
debugLog('Chunked staged files from `%s` into %d part', configPath, chunkCount)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
for (const [index, files] of stagedFileChunks.entries()) {
|
|
149
|
+
const relativeConfig = normalize(path.relative(cwd, configPath))
|
|
150
|
+
|
|
151
|
+
const chunkListrTasks = await Promise.all(
|
|
152
|
+
generateTasks({ config, cwd, files, relative }).map((task) =>
|
|
153
|
+
makeCmdTasks({
|
|
154
|
+
commands: task.commands,
|
|
155
|
+
cwd,
|
|
156
|
+
files: task.fileList,
|
|
157
|
+
gitDir,
|
|
158
|
+
renderer: listrOptions.renderer,
|
|
159
|
+
shell,
|
|
160
|
+
verbose,
|
|
161
|
+
}).then((subTasks) => {
|
|
162
|
+
// Add files from task to match set
|
|
163
|
+
task.fileList.forEach((file) => {
|
|
164
|
+
matchedFiles.add(file)
|
|
165
|
+
})
|
|
150
166
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
167
|
+
hasDeprecatedGitAdd =
|
|
168
|
+
hasDeprecatedGitAdd || subTasks.some((subTask) => subTask.command === 'git add')
|
|
169
|
+
|
|
170
|
+
const fileCount = task.fileList.length
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
title: `${task.pattern}${dim(` — ${fileCount} ${fileCount > 1 ? 'files' : 'file'}`)}`,
|
|
174
|
+
task: async () =>
|
|
175
|
+
new Listr(subTasks, {
|
|
176
|
+
// In sub-tasks we don't want to run concurrently
|
|
177
|
+
// and we want to abort on errors
|
|
178
|
+
...listrOptions,
|
|
179
|
+
concurrent: false,
|
|
180
|
+
exitOnError: true,
|
|
181
|
+
}),
|
|
182
|
+
skip: () => {
|
|
183
|
+
// Skip task when no files matched
|
|
184
|
+
if (fileCount === 0) {
|
|
185
|
+
return `${task.pattern}${dim(' — no files')}`
|
|
186
|
+
}
|
|
187
|
+
return false
|
|
188
|
+
},
|
|
189
|
+
}
|
|
190
|
+
})
|
|
191
|
+
)
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
listrTasks.push({
|
|
195
|
+
title:
|
|
196
|
+
`${relativeConfig}${dim(` — ${files.length} ${files.length > 1 ? 'files' : 'file'}`)}` +
|
|
197
|
+
(chunkCount > 1 ? dim(` (chunk ${index + 1}/${chunkCount})...`) : ''),
|
|
198
|
+
task: () => new Listr(chunkListrTasks, { ...listrOptions, concurrent, exitOnError: true }),
|
|
164
199
|
skip: () => {
|
|
165
|
-
// Skip
|
|
166
|
-
if (
|
|
167
|
-
|
|
200
|
+
// Skip if the first step (backup) failed
|
|
201
|
+
if (ctx.errors.has(GitError)) return SKIPPED_GIT_ERROR
|
|
202
|
+
// Skip chunk when no every task is skipped (due to no matches)
|
|
203
|
+
if (chunkListrTasks.every((task) => task.skip())) {
|
|
204
|
+
return `${relativeConfig}${dim(' — no tasks to run')}`
|
|
168
205
|
}
|
|
169
206
|
return false
|
|
170
207
|
},
|
|
171
208
|
})
|
|
172
209
|
}
|
|
173
|
-
|
|
174
|
-
listrTasks.push({
|
|
175
|
-
// No need to show number of task chunks when there's only one
|
|
176
|
-
title:
|
|
177
|
-
chunkCount > 1 ? `Running tasks (chunk ${index + 1}/${chunkCount})...` : 'Running tasks...',
|
|
178
|
-
task: () => new Listr(chunkListrTasks, { ...listrOptions, concurrent }),
|
|
179
|
-
skip: () => {
|
|
180
|
-
// Skip if the first step (backup) failed
|
|
181
|
-
if (ctx.errors.has(GitError)) return SKIPPED_GIT_ERROR
|
|
182
|
-
// Skip chunk when no every task is skipped (due to no matches)
|
|
183
|
-
if (chunkListrTasks.every((task) => task.skip())) return 'No tasks to run.'
|
|
184
|
-
return false
|
|
185
|
-
},
|
|
186
|
-
})
|
|
187
210
|
}
|
|
188
211
|
|
|
189
212
|
if (hasDeprecatedGitAdd) {
|
|
@@ -211,7 +234,7 @@ export const runAll = async (
|
|
|
211
234
|
const runner = new Listr(
|
|
212
235
|
[
|
|
213
236
|
{
|
|
214
|
-
title: 'Preparing...',
|
|
237
|
+
title: 'Preparing lint-staged...',
|
|
215
238
|
task: (ctx) => git.prepare(ctx),
|
|
216
239
|
},
|
|
217
240
|
{
|
|
@@ -219,9 +242,13 @@ export const runAll = async (
|
|
|
219
242
|
task: (ctx) => git.hideUnstagedChanges(ctx),
|
|
220
243
|
enabled: hasPartiallyStagedFiles,
|
|
221
244
|
},
|
|
222
|
-
...listrTasks,
|
|
223
245
|
{
|
|
224
|
-
title:
|
|
246
|
+
title: `Running tasks for staged files...`,
|
|
247
|
+
task: () => new Listr(listrTasks, { ...listrOptions, concurrent }),
|
|
248
|
+
skip: () => listrTasks.every((task) => task.skip()),
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
title: 'Applying modifications from tasks...',
|
|
225
252
|
task: (ctx) => git.applyModifications(ctx),
|
|
226
253
|
skip: applyModificationsSkipped,
|
|
227
254
|
},
|
|
@@ -238,7 +265,7 @@ export const runAll = async (
|
|
|
238
265
|
skip: restoreOriginalStateSkipped,
|
|
239
266
|
},
|
|
240
267
|
{
|
|
241
|
-
title: 'Cleaning up...',
|
|
268
|
+
title: 'Cleaning up temporary files...',
|
|
242
269
|
task: (ctx) => git.cleanup(ctx),
|
|
243
270
|
enabled: cleanupEnabled,
|
|
244
271
|
skip: cleanupSkipped,
|
package/lib/validateConfig.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
/** @typedef {import('./index').Logger} Logger */
|
|
2
|
+
|
|
1
3
|
import debug from 'debug'
|
|
4
|
+
import inspect from 'object-inspect'
|
|
2
5
|
|
|
3
6
|
import { configurationError } from './messages.js'
|
|
4
7
|
import { ConfigEmptyError, ConfigFormatError } from './symbols.js'
|
|
@@ -21,11 +24,13 @@ const TEST_DEPRECATED_KEYS = new Map([
|
|
|
21
24
|
|
|
22
25
|
/**
|
|
23
26
|
* Runs config validation. Throws error if the config is not valid.
|
|
24
|
-
* @param
|
|
25
|
-
* @
|
|
27
|
+
* @param {Object} config
|
|
28
|
+
* @param {string} configPath
|
|
29
|
+
* @param {Logger} logger
|
|
30
|
+
* @returns {Object} config
|
|
26
31
|
*/
|
|
27
|
-
export const validateConfig = (config, logger) => {
|
|
28
|
-
debugLog('Validating config')
|
|
32
|
+
export const validateConfig = (config, configPath, logger) => {
|
|
33
|
+
debugLog('Validating config from `%s`...', configPath)
|
|
29
34
|
|
|
30
35
|
if (!config || (typeof config !== 'object' && typeof config !== 'function')) {
|
|
31
36
|
throw ConfigFormatError
|
|
@@ -103,5 +108,8 @@ See https://github.com/okonet/lint-staged#configuration.`)
|
|
|
103
108
|
throw new Error(message)
|
|
104
109
|
}
|
|
105
110
|
|
|
111
|
+
debugLog('Validated config from `%s`:', configPath)
|
|
112
|
+
debugLog(inspect(config, { indent: 2 }))
|
|
113
|
+
|
|
106
114
|
return validatedConfig
|
|
107
115
|
}
|
package/lib/validateOptions.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { constants, promises as fs } from 'fs'
|
|
2
|
+
import path from 'path'
|
|
2
3
|
|
|
3
4
|
import debug from 'debug'
|
|
4
5
|
|
|
@@ -10,6 +11,7 @@ const debugLog = debug('lint-staged:validateOptions')
|
|
|
10
11
|
/**
|
|
11
12
|
* Validate lint-staged options, either from the Node.js API or the command line flags.
|
|
12
13
|
* @param {*} options
|
|
14
|
+
* @param {boolean|string} [options.cwd] - Current working directory
|
|
13
15
|
* @param {boolean|string} [options.shell] - Skip parsing of tasks for better shell support
|
|
14
16
|
*
|
|
15
17
|
* @throws {InvalidOptionsError}
|
|
@@ -17,6 +19,17 @@ const debugLog = debug('lint-staged:validateOptions')
|
|
|
17
19
|
export const validateOptions = async (options = {}, logger) => {
|
|
18
20
|
debugLog('Validating options...')
|
|
19
21
|
|
|
22
|
+
/** Ensure the passed cwd option exists; it might also be relative */
|
|
23
|
+
if (typeof options.cwd === 'string') {
|
|
24
|
+
try {
|
|
25
|
+
const resolved = path.resolve(options.cwd)
|
|
26
|
+
await fs.access(resolved, constants.F_OK)
|
|
27
|
+
} catch (error) {
|
|
28
|
+
logger.error(invalidOption('cwd', options.cwd, error.message))
|
|
29
|
+
throw InvalidOptionsError
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
20
33
|
/** Ensure the passed shell option is executable */
|
|
21
34
|
if (typeof options.shell === 'string') {
|
|
22
35
|
try {
|