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 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
  [![asciicast](https://asciinema.org/a/199934.svg)](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
- Starting with v5.0, `lint-staged` automatically resolves the git root **without any** additional configuration. You configure `lint-staged` as you normally would if your project root and git root were the same directory.
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
- If you wish to use `lint-staged` in a multi package monorepo, it is recommended to install [`husky`](https://github.com/typicode/husky) in the root package.json.
650
- [`lerna`](https://github.com/lerna/lerna) can be used to execute the `precommit` script in all sub-packages.
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
- Example repo: [sudo-suhas/lint-staged-multi-pkg](https://github.com/sudo-suhas/lint-staged-multi-pkg).
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
 
@@ -0,0 +1,3 @@
1
+ import { pathToFileURL } from 'url'
2
+
3
+ export const dynamicImport = (path) => import(pathToFileURL(path)).then((module) => module.default)
@@ -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(), gitDir, files, relative = false }) => {
19
+ export const generateTasks = ({ config, cwd = process.cwd(), files, relative = false }) => {
20
20
  debugLog('Generating linter tasks')
21
21
 
22
- const absoluteFiles = files.map((file) => normalize(path.resolve(gitDir, file)))
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
+ }
@@ -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 (options) => {
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
- ['diff', '--staged', '--diff-filter=ACMR', '--name-only', '-z'],
9
- options
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
- config,
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 resolveConfig = (configPath) => {
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
- if (!result) return null
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', result.filepath, config)
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 null
81
+ return {}
90
82
  }
91
83
  }
@@ -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
+ }
@@ -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 = { preferLocal: true, reject: false, shell }
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
- execaOptions.cwd = gitDir
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 {Object} [options.allowEmpty] - Allow empty commits when tasks revert all staged changes
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.config] - Task configuration
46
- * @param {Object} [options.cwd] - Current working directory
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
- config,
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 stagedFileChunks = chunkFiles({ baseDir: gitDir, files, maxArgLength, relative })
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 [index, files] of stagedFileChunks.entries()) {
132
- const chunkTasks = generateTasks({ config, cwd, gitDir, files, relative })
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
- // Add files from task to match set
146
- task.fileList.forEach((file) => {
147
- matchedFiles.add(file)
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
- hasDeprecatedGitAdd =
151
- hasDeprecatedGitAdd || subTasks.some((subTask) => subTask.command === 'git add')
152
-
153
- chunkListrTasks.push({
154
- title: `Running tasks for ${task.pattern}`,
155
- task: async () =>
156
- new Listr(subTasks, {
157
- // In sub-tasks we don't want to run concurrently
158
- // and we want to abort on errors
159
- ...listrOptions,
160
- concurrent: false,
161
- exitOnError: true,
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 task when no files matched
165
- if (task.fileList.length === 0) {
166
- return `No staged files match ${task.pattern}`
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: 'Applying modifications...',
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,
@@ -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 config {Object}
25
- * @returns config {Object}
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lint-staged",
3
- "version": "12.1.5",
3
+ "version": "12.2.1",
4
4
  "description": "Lint files staged by git",
5
5
  "license": "MIT",
6
6
  "repository": "https://github.com/okonet/lint-staged",