lint-staged 12.1.5 → 12.2.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 +48 -4
- package/lib/dynamicImport.js +3 -0
- package/lib/generateTasks.js +2 -3
- package/lib/getConfigGroups.js +80 -0
- package/lib/getStagedFiles.js +19 -7
- package/lib/index.js +3 -29
- package/lib/loadConfig.js +10 -18
- package/lib/makeCmdTasks.js +3 -2
- package/lib/resolveConfig.js +15 -0
- package/lib/resolveTaskFn.js +8 -7
- package/lib/runAll.js +82 -58
- package/lib/validateConfig.js +12 -4
- 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.
|
|
@@ -116,6 +138,8 @@ Starting with v3.1 you can now use different ways of configuring lint-staged:
|
|
|
116
138
|
|
|
117
139
|
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
140
|
|
|
141
|
+
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.
|
|
142
|
+
|
|
119
143
|
#### `package.json` example:
|
|
120
144
|
|
|
121
145
|
```json
|
|
@@ -644,12 +668,32 @@ _Thanks to [this comment](https://youtrack.jetbrains.com/issue/IDEA-135454#comme
|
|
|
644
668
|
<details>
|
|
645
669
|
<summary>Click to expand</summary>
|
|
646
670
|
|
|
647
|
-
|
|
671
|
+
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.
|
|
672
|
+
|
|
673
|
+
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/`.
|
|
674
|
+
|
|
675
|
+
**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
676
|
|
|
649
|
-
|
|
650
|
-
|
|
677
|
+
```js
|
|
678
|
+
// ./.lintstagedrc.json
|
|
679
|
+
{ "*.md": "prettier --write" }
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
```js
|
|
683
|
+
// ./packages/frontend/.lintstagedrc.json
|
|
684
|
+
{ "*.js": "eslint --fix" }
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
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:
|
|
651
688
|
|
|
652
|
-
|
|
689
|
+
```js
|
|
690
|
+
import baseConfig from '../.lintstagedrc.js'
|
|
691
|
+
|
|
692
|
+
export default {
|
|
693
|
+
...baseConfig,
|
|
694
|
+
'*.js': 'eslint --fix',
|
|
695
|
+
}
|
|
696
|
+
```
|
|
653
697
|
|
|
654
698
|
</details>
|
|
655
699
|
|
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,80 @@
|
|
|
1
|
+
/** @typedef {import('./index').Logger} Logger */
|
|
2
|
+
|
|
3
|
+
import path from 'path'
|
|
4
|
+
|
|
5
|
+
import { loadConfig } from './loadConfig.js'
|
|
6
|
+
import { ConfigNotFoundError } from './symbols.js'
|
|
7
|
+
import { validateConfig } from './validateConfig.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Return matched files grouped by their configuration.
|
|
11
|
+
*
|
|
12
|
+
* @param {object} options
|
|
13
|
+
* @param {Object} [options.configObject] - Explicit config object from the js API
|
|
14
|
+
* @param {string} [options.configPath] - Explicit path to a config file
|
|
15
|
+
* @param {string} [options.cwd] - Current working directory
|
|
16
|
+
* @param {Logger} logger
|
|
17
|
+
*/
|
|
18
|
+
export const getConfigGroups = async ({ configObject, configPath, files }, logger = console) => {
|
|
19
|
+
// Return explicit config object from js API
|
|
20
|
+
if (configObject) {
|
|
21
|
+
const config = validateConfig(configObject, 'config object', logger)
|
|
22
|
+
return { '': { config, files } }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Use only explicit config path instead of discovering multiple
|
|
26
|
+
if (configPath) {
|
|
27
|
+
const { config, filepath } = await loadConfig({ configPath }, logger)
|
|
28
|
+
|
|
29
|
+
if (!config) {
|
|
30
|
+
logger.error(`${ConfigNotFoundError.message}.`)
|
|
31
|
+
throw ConfigNotFoundError
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const validatedConfig = validateConfig(config, filepath, logger)
|
|
35
|
+
return { [configPath]: { config: validatedConfig, files } }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Group files by their base directory
|
|
39
|
+
const filesByDir = files.reduce((acc, file) => {
|
|
40
|
+
const dir = path.normalize(path.dirname(file))
|
|
41
|
+
|
|
42
|
+
if (dir in acc) {
|
|
43
|
+
acc[dir].push(file)
|
|
44
|
+
} else {
|
|
45
|
+
acc[dir] = [file]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return acc
|
|
49
|
+
}, {})
|
|
50
|
+
|
|
51
|
+
// Group files by their discovered config
|
|
52
|
+
// { '.lintstagedrc.json': { config: {...}, files: [...] } }
|
|
53
|
+
const configGroups = {}
|
|
54
|
+
|
|
55
|
+
await Promise.all(
|
|
56
|
+
Object.entries(filesByDir).map(([dir, files]) => {
|
|
57
|
+
// Discover config from the base directory of the file
|
|
58
|
+
return loadConfig({ cwd: dir }, logger).then(({ config, filepath }) => {
|
|
59
|
+
if (!config) return
|
|
60
|
+
|
|
61
|
+
if (filepath in configGroups) {
|
|
62
|
+
// Re-use cached config and skip validation
|
|
63
|
+
configGroups[filepath].files.push(...files)
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const validatedConfig = validateConfig(config, filepath, logger)
|
|
68
|
+
configGroups[filepath] = { config: validatedConfig, files }
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
// Throw if no configurations were found
|
|
74
|
+
if (Object.keys(configGroups).length === 0) {
|
|
75
|
+
logger.error(`${ConfigNotFoundError.message}.`)
|
|
76
|
+
throw ConfigNotFoundError
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return configGroups
|
|
80
|
+
}
|
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')
|
|
@@ -58,25 +50,6 @@ const lintStaged = async (
|
|
|
58
50
|
) => {
|
|
59
51
|
await validateOptions({ shell }, logger)
|
|
60
52
|
|
|
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
|
-
}
|
|
79
|
-
|
|
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)
|
|
82
55
|
delete 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,12 @@
|
|
|
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'
|
|
8
|
+
import { resolveConfig } from './resolveConfig.js'
|
|
9
|
+
|
|
9
10
|
const debugLog = debug('lint-staged:loadConfig')
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -26,9 +27,6 @@ const searchPlaces = [
|
|
|
26
27
|
'lint-staged.config.cjs',
|
|
27
28
|
]
|
|
28
29
|
|
|
29
|
-
/** exported for tests */
|
|
30
|
-
export const dynamicImport = (path) => import(pathToFileURL(path)).then((module) => module.default)
|
|
31
|
-
|
|
32
30
|
const jsonParse = (path, content) => JSON.parse(content)
|
|
33
31
|
|
|
34
32
|
const yamlParse = (path, content) => YAML.parse(content)
|
|
@@ -49,13 +47,7 @@ const loaders = {
|
|
|
49
47
|
noExt: yamlParse,
|
|
50
48
|
}
|
|
51
49
|
|
|
52
|
-
const
|
|
53
|
-
try {
|
|
54
|
-
return require.resolve(configPath)
|
|
55
|
-
} catch {
|
|
56
|
-
return configPath
|
|
57
|
-
}
|
|
58
|
-
}
|
|
50
|
+
const explorer = lilconfig('lint-staged', { searchPlaces, loaders })
|
|
59
51
|
|
|
60
52
|
/**
|
|
61
53
|
* @param {object} options
|
|
@@ -70,22 +62,22 @@ export const loadConfig = async ({ configPath, cwd }, logger) => {
|
|
|
70
62
|
debugLog('Searching for configuration from `%s`...', cwd)
|
|
71
63
|
}
|
|
72
64
|
|
|
73
|
-
const explorer = lilconfig('lint-staged', { searchPlaces, loaders })
|
|
74
|
-
|
|
75
65
|
const result = await (configPath
|
|
76
66
|
? explorer.load(resolveConfig(configPath))
|
|
77
67
|
: explorer.search(cwd))
|
|
78
|
-
|
|
68
|
+
|
|
69
|
+
if (!result) return {}
|
|
79
70
|
|
|
80
71
|
// config is a promise when using the `dynamicImport` loader
|
|
81
72
|
const config = await result.config
|
|
73
|
+
const filepath = result.filepath
|
|
82
74
|
|
|
83
|
-
debugLog('Successfully loaded config from `%s`:\n%O',
|
|
75
|
+
debugLog('Successfully loaded config from `%s`:\n%O', filepath, config)
|
|
84
76
|
|
|
85
|
-
return config
|
|
77
|
+
return { config, filepath }
|
|
86
78
|
} catch (error) {
|
|
87
79
|
debugLog('Failed to load configuration!')
|
|
88
80
|
logger.error(error)
|
|
89
|
-
return
|
|
81
|
+
return {}
|
|
90
82
|
}
|
|
91
83
|
}
|
package/lib/makeCmdTasks.js
CHANGED
|
@@ -28,13 +28,14 @@ const getTitleLength = (renderer, columns = process.stdout.columns) => {
|
|
|
28
28
|
*
|
|
29
29
|
* @param {object} options
|
|
30
30
|
* @param {Array<string|Function>|string|Function} options.commands
|
|
31
|
+
* @param {string} options.cwd
|
|
31
32
|
* @param {Array<string>} options.files
|
|
32
33
|
* @param {string} options.gitDir
|
|
33
34
|
* @param {string} options.renderer
|
|
34
35
|
* @param {Boolean} shell
|
|
35
36
|
* @param {Boolean} verbose
|
|
36
37
|
*/
|
|
37
|
-
export const makeCmdTasks = async ({ commands, files, gitDir, renderer, shell, verbose }) => {
|
|
38
|
+
export const makeCmdTasks = async ({ commands, cwd, files, gitDir, renderer, shell, verbose }) => {
|
|
38
39
|
debugLog('Creating listr tasks for commands %o', commands)
|
|
39
40
|
const commandArray = Array.isArray(commands) ? commands : [commands]
|
|
40
41
|
const cmdTasks = []
|
|
@@ -61,7 +62,7 @@ export const makeCmdTasks = async ({ commands, files, gitDir, renderer, shell, v
|
|
|
61
62
|
|
|
62
63
|
// Truncate title to single line based on renderer
|
|
63
64
|
const title = cliTruncate(command, getTitleLength(renderer))
|
|
64
|
-
const task = resolveTaskFn({ command, files, gitDir, isFn, shell, verbose })
|
|
65
|
+
const task = resolveTaskFn({ command, cwd, files, gitDir, isFn, shell, verbose })
|
|
65
66
|
cmdTasks.push({ title, command, task })
|
|
66
67
|
}
|
|
67
68
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createRequire } from 'module'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* require() does not exist for ESM, so we must create it to use require.resolve().
|
|
5
|
+
* @see https://nodejs.org/api/module.html#modulecreaterequirefilename
|
|
6
|
+
*/
|
|
7
|
+
const require = createRequire(import.meta.url)
|
|
8
|
+
|
|
9
|
+
export function resolveConfig(configPath) {
|
|
10
|
+
try {
|
|
11
|
+
return require.resolve(configPath)
|
|
12
|
+
} catch {
|
|
13
|
+
return configPath
|
|
14
|
+
}
|
|
15
|
+
}
|
package/lib/resolveTaskFn.js
CHANGED
|
@@ -68,20 +68,20 @@ const makeErr = (command, result, ctx) => {
|
|
|
68
68
|
*
|
|
69
69
|
* @param {Object} options
|
|
70
70
|
* @param {string} options.command — Linter task
|
|
71
|
+
* @param {string} [options.cwd]
|
|
71
72
|
* @param {String} options.gitDir - Current git repo path
|
|
72
73
|
* @param {Boolean} options.isFn - Whether the linter task is a function
|
|
73
74
|
* @param {Array<string>} options.files — Filepaths to run the linter task against
|
|
74
|
-
* @param {Boolean} [options.relative] — Whether the filepaths should be relative
|
|
75
75
|
* @param {Boolean} [options.shell] — Whether to skip parsing linter task for better shell support
|
|
76
76
|
* @param {Boolean} [options.verbose] — Always show task verbose
|
|
77
77
|
* @returns {function(): Promise<Array<string>>}
|
|
78
78
|
*/
|
|
79
79
|
export const resolveTaskFn = ({
|
|
80
80
|
command,
|
|
81
|
+
cwd = process.cwd(),
|
|
81
82
|
files,
|
|
82
83
|
gitDir,
|
|
83
84
|
isFn,
|
|
84
|
-
relative,
|
|
85
85
|
shell = false,
|
|
86
86
|
verbose = false,
|
|
87
87
|
}) => {
|
|
@@ -89,14 +89,15 @@ export const resolveTaskFn = ({
|
|
|
89
89
|
debugLog('cmd:', cmd)
|
|
90
90
|
debugLog('args:', args)
|
|
91
91
|
|
|
92
|
-
const execaOptions = {
|
|
93
|
-
if (relative) {
|
|
94
|
-
execaOptions.cwd = process.cwd()
|
|
95
|
-
} else if (/^git(\.exe)?/i.test(cmd) && gitDir !== process.cwd()) {
|
|
92
|
+
const execaOptions = {
|
|
96
93
|
// Only use gitDir as CWD if we are using the git binary
|
|
97
94
|
// e.g `npm` should run tasks in the actual CWD
|
|
98
|
-
|
|
95
|
+
cwd: /^git(\.exe)?/i.test(cmd) ? gitDir : cwd,
|
|
96
|
+
preferLocal: true,
|
|
97
|
+
reject: false,
|
|
98
|
+
shell,
|
|
99
99
|
}
|
|
100
|
+
|
|
100
101
|
debugLog('execaOptions:', execaOptions)
|
|
101
102
|
|
|
102
103
|
return async (ctx = getInitialState()) => {
|
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,7 +64,8 @@ export const runAll = async (
|
|
|
58
64
|
{
|
|
59
65
|
allowEmpty = false,
|
|
60
66
|
concurrent = true,
|
|
61
|
-
|
|
67
|
+
configObject,
|
|
68
|
+
configPath,
|
|
62
69
|
cwd = process.cwd(),
|
|
63
70
|
debug = false,
|
|
64
71
|
maxArgLength,
|
|
@@ -107,9 +114,7 @@ export const runAll = async (
|
|
|
107
114
|
return ctx
|
|
108
115
|
}
|
|
109
116
|
|
|
110
|
-
const
|
|
111
|
-
const chunkCount = stagedFileChunks.length
|
|
112
|
-
if (chunkCount > 1) debugLog(`Chunked staged files into ${chunkCount} part`, chunkCount)
|
|
117
|
+
const configGroups = await getConfigGroups({ configObject, configPath, files }, logger)
|
|
113
118
|
|
|
114
119
|
// lint-staged 10 will automatically add modifications to index
|
|
115
120
|
// Warn user when their command includes `git add`
|
|
@@ -128,61 +133,76 @@ export const runAll = async (
|
|
|
128
133
|
// Set of all staged files that matched a task glob. Values in a set are unique.
|
|
129
134
|
const matchedFiles = new Set()
|
|
130
135
|
|
|
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
|
-
files: task.fileList,
|
|
139
|
-
gitDir,
|
|
140
|
-
renderer: listrOptions.renderer,
|
|
141
|
-
shell,
|
|
142
|
-
verbose,
|
|
143
|
-
})
|
|
136
|
+
for (const [configPath, { config, files }] of Object.entries(configGroups)) {
|
|
137
|
+
const stagedFileChunks = chunkFiles({ baseDir: gitDir, files, maxArgLength, relative })
|
|
144
138
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
139
|
+
const chunkCount = stagedFileChunks.length
|
|
140
|
+
if (chunkCount > 1) {
|
|
141
|
+
debugLog('Chunked staged files from `%s` into %d part', configPath, chunkCount)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
for (const [index, files] of stagedFileChunks.entries()) {
|
|
145
|
+
const relativeConfig = normalize(path.relative(cwd, configPath))
|
|
146
|
+
|
|
147
|
+
const chunkListrTasks = await Promise.all(
|
|
148
|
+
generateTasks({ config, cwd, files, relative }).map((task) =>
|
|
149
|
+
makeCmdTasks({
|
|
150
|
+
commands: task.commands,
|
|
151
|
+
cwd,
|
|
152
|
+
files: task.fileList,
|
|
153
|
+
gitDir,
|
|
154
|
+
renderer: listrOptions.renderer,
|
|
155
|
+
shell,
|
|
156
|
+
verbose,
|
|
157
|
+
}).then((subTasks) => {
|
|
158
|
+
// Add files from task to match set
|
|
159
|
+
task.fileList.forEach((file) => {
|
|
160
|
+
matchedFiles.add(file)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
hasDeprecatedGitAdd =
|
|
164
|
+
hasDeprecatedGitAdd || subTasks.some((subTask) => subTask.command === 'git add')
|
|
149
165
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
166
|
+
const fileCount = task.fileList.length
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
title: `${task.pattern}${dim(` — ${fileCount} ${fileCount > 1 ? 'files' : 'file'}`)}`,
|
|
170
|
+
task: async () =>
|
|
171
|
+
new Listr(subTasks, {
|
|
172
|
+
// In sub-tasks we don't want to run concurrently
|
|
173
|
+
// and we want to abort on errors
|
|
174
|
+
...listrOptions,
|
|
175
|
+
concurrent: false,
|
|
176
|
+
exitOnError: true,
|
|
177
|
+
}),
|
|
178
|
+
skip: () => {
|
|
179
|
+
// Skip task when no files matched
|
|
180
|
+
if (fileCount === 0) {
|
|
181
|
+
return `${task.pattern}${dim(' — no files')}`
|
|
182
|
+
}
|
|
183
|
+
return false
|
|
184
|
+
},
|
|
185
|
+
}
|
|
186
|
+
})
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
listrTasks.push({
|
|
191
|
+
title:
|
|
192
|
+
`${relativeConfig}${dim(` — ${files.length} ${files.length > 1 ? 'files' : 'file'}`)}` +
|
|
193
|
+
(chunkCount > 1 ? dim(` (chunk ${index + 1}/${chunkCount})...`) : ''),
|
|
194
|
+
task: () => new Listr(chunkListrTasks, { ...listrOptions, concurrent, exitOnError: true }),
|
|
163
195
|
skip: () => {
|
|
164
|
-
// Skip
|
|
165
|
-
if (
|
|
166
|
-
|
|
196
|
+
// Skip if the first step (backup) failed
|
|
197
|
+
if (ctx.errors.has(GitError)) return SKIPPED_GIT_ERROR
|
|
198
|
+
// Skip chunk when no every task is skipped (due to no matches)
|
|
199
|
+
if (chunkListrTasks.every((task) => task.skip())) {
|
|
200
|
+
return `${relativeConfig}${dim(' — no tasks to run')}`
|
|
167
201
|
}
|
|
168
202
|
return false
|
|
169
203
|
},
|
|
170
204
|
})
|
|
171
205
|
}
|
|
172
|
-
|
|
173
|
-
listrTasks.push({
|
|
174
|
-
// No need to show number of task chunks when there's only one
|
|
175
|
-
title:
|
|
176
|
-
chunkCount > 1 ? `Running tasks (chunk ${index + 1}/${chunkCount})...` : 'Running tasks...',
|
|
177
|
-
task: () => new Listr(chunkListrTasks, { ...listrOptions, concurrent }),
|
|
178
|
-
skip: () => {
|
|
179
|
-
// Skip if the first step (backup) failed
|
|
180
|
-
if (ctx.errors.has(GitError)) return SKIPPED_GIT_ERROR
|
|
181
|
-
// Skip chunk when no every task is skipped (due to no matches)
|
|
182
|
-
if (chunkListrTasks.every((task) => task.skip())) return 'No tasks to run.'
|
|
183
|
-
return false
|
|
184
|
-
},
|
|
185
|
-
})
|
|
186
206
|
}
|
|
187
207
|
|
|
188
208
|
if (hasDeprecatedGitAdd) {
|
|
@@ -210,7 +230,7 @@ export const runAll = async (
|
|
|
210
230
|
const runner = new Listr(
|
|
211
231
|
[
|
|
212
232
|
{
|
|
213
|
-
title: 'Preparing...',
|
|
233
|
+
title: 'Preparing lint-staged...',
|
|
214
234
|
task: (ctx) => git.prepare(ctx),
|
|
215
235
|
},
|
|
216
236
|
{
|
|
@@ -218,9 +238,13 @@ export const runAll = async (
|
|
|
218
238
|
task: (ctx) => git.hideUnstagedChanges(ctx),
|
|
219
239
|
enabled: hasPartiallyStagedFiles,
|
|
220
240
|
},
|
|
221
|
-
...listrTasks,
|
|
222
241
|
{
|
|
223
|
-
title:
|
|
242
|
+
title: `Running tasks for staged files...`,
|
|
243
|
+
task: () => new Listr(listrTasks, { ...listrOptions, concurrent }),
|
|
244
|
+
skip: () => listrTasks.every((task) => task.skip()),
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
title: 'Applying modifications from tasks...',
|
|
224
248
|
task: (ctx) => git.applyModifications(ctx),
|
|
225
249
|
skip: applyModificationsSkipped,
|
|
226
250
|
},
|
|
@@ -237,7 +261,7 @@ export const runAll = async (
|
|
|
237
261
|
skip: restoreOriginalStateSkipped,
|
|
238
262
|
},
|
|
239
263
|
{
|
|
240
|
-
title: 'Cleaning up...',
|
|
264
|
+
title: 'Cleaning up temporary files...',
|
|
241
265
|
task: (ctx) => git.cleanup(ctx),
|
|
242
266
|
enabled: cleanupEnabled,
|
|
243
267
|
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
|
}
|