lint-staged 12.3.7 → 13.0.2

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.
@@ -5,5 +5,7 @@
5
5
  */
6
6
  export const parseGitZOutput = (input) =>
7
7
  input
8
- .replace(/\u0000$/, '') // eslint-disable-line no-control-regex
9
- .split('\u0000')
8
+ ? input
9
+ .replace(/\u0000$/, '') // eslint-disable-line no-control-regex
10
+ .split('\u0000')
11
+ : []
@@ -5,7 +5,7 @@
5
5
  */
6
6
  export const printTaskOutput = (ctx = {}, logger) => {
7
7
  if (!Array.isArray(ctx.output)) return
8
- const log = ctx.errors && ctx.errors.size > 0 ? logger.error : logger.log
8
+ const log = ctx.errors?.size > 0 ? logger.error : logger.log
9
9
  for (const line of ctx.output) {
10
10
  log(line)
11
11
  }
@@ -1,4 +1,4 @@
1
- import { createRequire } from 'module'
1
+ import { createRequire } from 'node:module'
2
2
 
3
3
  /**
4
4
  * require() does not exist for ESM, so we must create it to use require.resolve().
@@ -1,5 +1,5 @@
1
- import { promises as fs } from 'fs'
2
- import path from 'path'
1
+ import fs from 'node:fs/promises'
2
+ import path from 'node:path'
3
3
 
4
4
  import debug from 'debug'
5
5
  import normalize from 'normalize-path'
@@ -1,5 +1,5 @@
1
1
  import { redBright, dim } from 'colorette'
2
- import execa from 'execa'
2
+ import { execa, execaCommand } from 'execa'
3
3
  import debug from 'debug'
4
4
  import { parseArgsStringToArgv } from 'string-argv'
5
5
  import pidTree from 'pidtree'
@@ -8,7 +8,7 @@ import { error, info } from './figures.js'
8
8
  import { getInitialState } from './state.js'
9
9
  import { TaskError } from './symbols.js'
10
10
 
11
- const ERROR_CHECK_INTERVAL = 200
11
+ const TASK_ERROR = 'lint-staged:taskError'
12
12
 
13
13
  const debugLog = debug('lint-staged:resolveTaskFn')
14
14
 
@@ -46,31 +46,52 @@ const handleOutput = (command, result, ctx, isError = false) => {
46
46
  }
47
47
  }
48
48
 
49
+ /**
50
+ * Kill an execa process along with all its child processes.
51
+ * @param {execa.ExecaChildProcess<string>} execaProcess
52
+ */
53
+ const killExecaProcess = async (execaProcess) => {
54
+ try {
55
+ const childPids = await pidTree(execaProcess.pid)
56
+ for (const childPid of childPids) {
57
+ try {
58
+ process.kill(childPid)
59
+ } catch (error) {
60
+ debugLog(`Failed to kill process with pid "%d": %o`, childPid, error)
61
+ }
62
+ }
63
+ } catch (error) {
64
+ // Suppress "No matching pid found" error. This probably means
65
+ // the process already died before executing.
66
+ debugLog(`Failed to kill process with pid "%d": %o`, execaProcess.pid, error)
67
+ }
68
+
69
+ // The execa process is killed separately in order to get the `KILLED` status.
70
+ execaProcess.kill()
71
+ }
72
+
49
73
  /**
50
74
  * Interrupts the execution of the execa process that we spawned if
51
75
  * another task adds an error to the context.
52
76
  *
53
77
  * @param {Object} ctx
54
78
  * @param {execa.ExecaChildProcess<string>} execaChildProcess
55
- * @returns {function(): void} Function that clears the interval that
79
+ * @returns {() => Promise<void>} Function that clears the interval that
56
80
  * checks the context.
57
81
  */
58
82
  const interruptExecutionOnError = (ctx, execaChildProcess) => {
59
- async function loop() {
60
- if (ctx.errors.size > 0) {
61
- const ids = await pidTree(execaChildProcess.pid)
62
- ids.forEach(process.kill)
63
-
64
- // The execa process is killed separately in order
65
- // to get the `KILLED` status.
66
- execaChildProcess.kill()
67
- }
83
+ let killPromise
84
+
85
+ const errorListener = async () => {
86
+ killPromise = killExecaProcess(execaChildProcess)
87
+ await killPromise
68
88
  }
69
89
 
70
- const loopIntervalId = setInterval(loop, ERROR_CHECK_INTERVAL)
90
+ ctx.events.on(TASK_ERROR, errorListener, { once: true })
71
91
 
72
- return () => {
73
- clearInterval(loopIntervalId)
92
+ return async () => {
93
+ ctx.events.off(TASK_ERROR, errorListener)
94
+ await killPromise
74
95
  }
75
96
  }
76
97
 
@@ -89,6 +110,10 @@ const interruptExecutionOnError = (ctx, execaChildProcess) => {
89
110
  */
90
111
  const makeErr = (command, result, ctx) => {
91
112
  ctx.errors.add(TaskError)
113
+
114
+ // https://nodejs.org/api/events.html#error-events
115
+ ctx.events.emit(TASK_ERROR, TaskError)
116
+
92
117
  handleOutput(command, result, ctx, true)
93
118
  const tag = getTag(result)
94
119
  return new Error(`${redBright(command)} ${dim(`[${tag}]`)}`)
@@ -105,7 +130,7 @@ const makeErr = (command, result, ctx) => {
105
130
  * @param {Array<string>} options.files — Filepaths to run the linter task against
106
131
  * @param {Boolean} [options.shell] — Whether to skip parsing linter task for better shell support
107
132
  * @param {Boolean} [options.verbose] — Always show task verbose
108
- * @returns {function(): Promise<Array<string>>}
133
+ * @returns {() => Promise<Array<string>>}
109
134
  */
110
135
  export const resolveTaskFn = ({
111
136
  command,
@@ -133,12 +158,12 @@ export const resolveTaskFn = ({
133
158
 
134
159
  return async (ctx = getInitialState()) => {
135
160
  const execaChildProcess = shell
136
- ? execa.command(isFn ? command : `${command} ${files.join(' ')}`, execaOptions)
161
+ ? execaCommand(isFn ? command : `${command} ${files.join(' ')}`, execaOptions)
137
162
  : execa(cmd, isFn ? args : args.concat(files), execaOptions)
138
163
 
139
164
  const quitInterruptCheck = interruptExecutionOnError(ctx, execaChildProcess)
140
165
  const result = await execaChildProcess
141
- quitInterruptCheck()
166
+ await quitInterruptCheck()
142
167
 
143
168
  if (result.failed || result.killed || result.signal != null) {
144
169
  throw makeErr(command, result, ctx)
package/lib/runAll.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /** @typedef {import('./index').Logger} Logger */
2
2
 
3
- import path from 'path'
3
+ import path from 'node:path'
4
4
 
5
5
  import { dim } from 'colorette'
6
6
  import debug from 'debug'
@@ -10,10 +10,10 @@ import normalize from 'normalize-path'
10
10
  import { chunkFiles } from './chunkFiles.js'
11
11
  import { execGit } from './execGit.js'
12
12
  import { generateTasks } from './generateTasks.js'
13
- import { getConfigGroups } from './getConfigGroups.js'
14
13
  import { getRenderer } from './getRenderer.js'
15
14
  import { getStagedFiles } from './getStagedFiles.js'
16
15
  import { GitWorkflow } from './gitWorkflow.js'
16
+ import { groupFilesByConfig } from './groupFilesByConfig.js'
17
17
  import { makeCmdTasks } from './makeCmdTasks.js'
18
18
  import {
19
19
  DEPRECATED_GIT_ADD,
@@ -52,6 +52,8 @@ const createError = (ctx) => Object.assign(new Error('lint-staged failed'), { ct
52
52
  * @param {string} [options.configPath] - Explicit path to a config file
53
53
  * @param {string} [options.cwd] - Current working directory
54
54
  * @param {boolean} [options.debug] - Enable debug mode
55
+ * @param {string} [options.diff] - Override the default "--staged" flag of "git diff" to get list of files
56
+ * @param {string} [options.diffFilter] - Override the default "--diff-filter=ACMR" flag of "git diff" to get list of files
55
57
  * @param {number} [options.maxArgLength] - Maximum argument string length
56
58
  * @param {boolean} [options.quiet] - Disable lint-staged’s own console output
57
59
  * @param {boolean} [options.relative] - Pass relative filepaths to tasks
@@ -69,6 +71,8 @@ export const runAll = async (
69
71
  configPath,
70
72
  cwd,
71
73
  debug = false,
74
+ diff,
75
+ diffFilter,
72
76
  maxArgLength,
73
77
  quiet = false,
74
78
  relative = false,
@@ -100,13 +104,14 @@ export const runAll = async (
100
104
  .then(() => true)
101
105
  .catch(() => false)
102
106
 
103
- // Lint-staged should create a backup stash only when there's an initial commit
104
- ctx.shouldBackup = hasInitialCommit && stash
107
+ // Lint-staged should create a backup stash only when there's an initial commit,
108
+ // and when using the default list of staged files
109
+ ctx.shouldBackup = hasInitialCommit && stash && diff === undefined
105
110
  if (!ctx.shouldBackup) {
106
- logger.warn(skippingBackup(hasInitialCommit))
111
+ logger.warn(skippingBackup(hasInitialCommit, diff))
107
112
  }
108
113
 
109
- const files = await getStagedFiles({ cwd: gitDir })
114
+ const files = await getStagedFiles({ cwd: gitDir, diff, diffFilter })
110
115
  if (!files) {
111
116
  if (!quiet) ctx.output.push(FAILED_GET_STAGED_FILES)
112
117
  ctx.errors.add(GetStagedFilesError)
@@ -120,11 +125,8 @@ export const runAll = async (
120
125
  return ctx
121
126
  }
122
127
 
123
- const configGroups = await getConfigGroups({ configObject, configPath, cwd, files }, logger)
124
-
125
- const hasExplicitConfig = configObject || configPath
126
- const foundConfigs = hasExplicitConfig ? null : await searchConfigs(gitDir, logger)
127
- const numberOfConfigs = hasExplicitConfig ? 1 : Object.keys(foundConfigs).length
128
+ const foundConfigs = await searchConfigs({ configObject, configPath, cwd, gitDir }, logger)
129
+ const numberOfConfigs = Object.keys(foundConfigs).length
128
130
 
129
131
  // Throw if no configurations were found
130
132
  if (numberOfConfigs === 0) {
@@ -132,7 +134,11 @@ export const runAll = async (
132
134
  throw createError(ctx, ConfigNotFoundError)
133
135
  }
134
136
 
135
- debugLog('Found %d configs:\n%O', numberOfConfigs, foundConfigs)
137
+ const filesByConfig = await groupFilesByConfig({
138
+ configs: foundConfigs,
139
+ files,
140
+ singleConfigMode: configObject || configPath !== undefined,
141
+ })
136
142
 
137
143
  const hasMultipleConfigs = numberOfConfigs > 1
138
144
 
@@ -152,8 +158,9 @@ export const runAll = async (
152
158
  // Set of all staged files that matched a task glob. Values in a set are unique.
153
159
  const matchedFiles = new Set()
154
160
 
155
- for (const [configPath, { config, files }] of Object.entries(configGroups)) {
156
- const relativeConfig = normalize(path.relative(cwd, configPath))
161
+ for (const [configPath, { config, files }] of Object.entries(filesByConfig)) {
162
+ const configName = configPath ? normalize(path.relative(cwd, configPath)) : 'Config object'
163
+
157
164
  const stagedFileChunks = chunkFiles({ baseDir: gitDir, files, maxArgLength, relative })
158
165
 
159
166
  // Use actual cwd if it's specified, or there's only a single config file.
@@ -196,15 +203,15 @@ export const runAll = async (
196
203
  const fileCount = task.fileList.length
197
204
 
198
205
  return {
199
- title: `${task.pattern}${dim(` — ${fileCount} ${fileCount > 1 ? 'files' : 'file'}`)}`,
200
- task: async () =>
201
- new Listr(subTasks, {
202
- // In sub-tasks we don't want to run concurrently
203
- // and we want to abort on errors
204
- ...listrOptions,
205
- concurrent: false,
206
- exitOnError: true,
207
- }),
206
+ title: `${task.pattern}${dim(
207
+ ` ${fileCount} ${fileCount === 1 ? 'file' : 'files'}`
208
+ )}`,
209
+ task: async (ctx, task) =>
210
+ task.newListr(
211
+ subTasks,
212
+ // Subtasks should not run in parallel, and should exit on error
213
+ { concurrent: false, exitOnError: true }
214
+ ),
208
215
  skip: () => {
209
216
  // Skip task when no files matched
210
217
  if (fileCount === 0) {
@@ -219,15 +226,15 @@ export const runAll = async (
219
226
 
220
227
  listrTasks.push({
221
228
  title:
222
- `${relativeConfig}${dim(` — ${files.length} ${files.length > 1 ? 'files' : 'file'}`)}` +
229
+ `${configName}${dim(` — ${files.length} ${files.length > 1 ? 'files' : 'file'}`)}` +
223
230
  (chunkCount > 1 ? dim(` (chunk ${index + 1}/${chunkCount})...`) : ''),
224
- task: () => new Listr(chunkListrTasks, { ...listrOptions, concurrent, exitOnError: true }),
231
+ task: (ctx, task) => task.newListr(chunkListrTasks, { concurrent, exitOnError: true }),
225
232
  skip: () => {
226
233
  // Skip if the first step (backup) failed
227
234
  if (ctx.errors.has(GitError)) return SKIPPED_GIT_ERROR
228
235
  // Skip chunk when no every task is skipped (due to no matches)
229
236
  if (chunkListrTasks.every((task) => task.skip())) {
230
- return `${relativeConfig}${dim(' — no tasks to run')}`
237
+ return `${configName}${dim(' — no tasks to run')}`
231
238
  }
232
239
  return false
233
240
  },
@@ -255,7 +262,14 @@ export const runAll = async (
255
262
  relative: false,
256
263
  })
257
264
 
258
- const git = new GitWorkflow({ allowEmpty, gitConfigDir, gitDir, matchedFileChunks })
265
+ const git = new GitWorkflow({
266
+ allowEmpty,
267
+ gitConfigDir,
268
+ gitDir,
269
+ matchedFileChunks,
270
+ diff,
271
+ diffFilter,
272
+ })
259
273
 
260
274
  const runner = new Listr(
261
275
  [
@@ -270,7 +284,7 @@ export const runAll = async (
270
284
  },
271
285
  {
272
286
  title: `Running tasks for staged files...`,
273
- task: () => new Listr(listrTasks, { ...listrOptions, concurrent }),
287
+ task: (ctx, task) => task.newListr(listrTasks, { concurrent }),
274
288
  skip: () => listrTasks.every((task) => task.skip()),
275
289
  },
276
290
  {
@@ -1,7 +1,8 @@
1
1
  /** @typedef {import('./index').Logger} Logger */
2
2
 
3
- import { basename, join } from 'path'
3
+ import path from 'node:path'
4
4
 
5
+ import debug from 'debug'
5
6
  import normalize from 'normalize-path'
6
7
 
7
8
  import { execGit } from './execGit.js'
@@ -9,38 +10,70 @@ import { loadConfig, searchPlaces } from './loadConfig.js'
9
10
  import { parseGitZOutput } from './parseGitZOutput.js'
10
11
  import { validateConfig } from './validateConfig.js'
11
12
 
13
+ const debugLog = debug('lint-staged:searchConfigs')
14
+
12
15
  const EXEC_GIT = ['ls-files', '-z', '--full-name']
13
16
 
14
- const filterPossibleConfigFiles = (file) => searchPlaces.includes(basename(file))
17
+ const filterPossibleConfigFiles = (files) =>
18
+ files.filter((file) => searchPlaces.includes(path.basename(file)))
15
19
 
16
20
  const numberOfLevels = (file) => file.split('/').length
17
21
 
18
22
  const sortDeepestParth = (a, b) => (numberOfLevels(a) > numberOfLevels(b) ? -1 : 1)
19
23
 
24
+ const isInsideDirectory = (dir) => (file) => file.startsWith(normalize(dir))
25
+
20
26
  /**
21
- * Search all config files from the git repository
27
+ * Search all config files from the git repository, preferring those inside `cwd`.
22
28
  *
23
- * @param {string} gitDir
29
+ * @param {object} options
30
+ * @param {Object} [options.configObject] - Explicit config object from the js API
31
+ * @param {string} [options.configPath] - Explicit path to a config file
32
+ * @param {string} [options.cwd] - Current working directory
24
33
  * @param {Logger} logger
25
- * @returns {Promise<{ [key: string]: * }>} found configs with filepath as key, and config as value
34
+ *
35
+ * @returns {Promise<{ [key: string]: { config: *, files: string[] } }>} found configs with filepath as key, and config as value
26
36
  */
27
- export const searchConfigs = async (gitDir = process.cwd(), logger) => {
28
- /** Get all possible config files known to git */
29
- const cachedFiles = parseGitZOutput(await execGit(EXEC_GIT, { cwd: gitDir })).filter(
30
- filterPossibleConfigFiles
31
- )
37
+ export const searchConfigs = async (
38
+ { configObject, configPath, cwd = process.cwd(), gitDir = cwd },
39
+ logger
40
+ ) => {
41
+ debugLog('Searching for configuration files...')
42
+
43
+ // Return explicit config object from js API
44
+ if (configObject) {
45
+ debugLog('Using single direct configuration object...')
46
+
47
+ return { '': validateConfig(configObject, 'config object', logger) }
48
+ }
49
+
50
+ // Use only explicit config path instead of discovering multiple
51
+ if (configPath) {
52
+ debugLog('Using single configuration path...')
53
+
54
+ const { config, filepath } = await loadConfig({ configPath }, logger)
32
55
 
33
- /** Get all possible config files from uncommitted files */
34
- const otherFiles = parseGitZOutput(
35
- await execGit([...EXEC_GIT, '--others', '--exclude-standard'], { cwd: gitDir })
36
- ).filter(filterPossibleConfigFiles)
56
+ if (!config) return {}
57
+ return { [configPath]: validateConfig(config, filepath, logger) }
58
+ }
59
+
60
+ const [cachedFiles, otherFiles] = await Promise.all([
61
+ /** Get all possible config files known to git */
62
+ execGit(EXEC_GIT, { cwd: gitDir }).then(parseGitZOutput).then(filterPossibleConfigFiles),
63
+ /** Get all possible config files from uncommitted files */
64
+ execGit([...EXEC_GIT, '--others', '--exclude-standard'], { cwd: gitDir })
65
+ .then(parseGitZOutput)
66
+ .then(filterPossibleConfigFiles),
67
+ ])
37
68
 
38
69
  /** Sort possible config files so that deepest is first */
39
70
  const possibleConfigFiles = [...cachedFiles, ...otherFiles]
40
- .map((file) => join(gitDir, file))
41
- .map((file) => normalize(file))
71
+ .map((file) => normalize(path.join(gitDir, file)))
72
+ .filter(isInsideDirectory(cwd))
42
73
  .sort(sortDeepestParth)
43
74
 
75
+ debugLog('Found possible config files:', possibleConfigFiles)
76
+
44
77
  /** Create object with key as config file, and value as null */
45
78
  const configs = possibleConfigFiles.reduce(
46
79
  (acc, configPath) => Object.assign(acc, { [configPath]: null }),
@@ -49,15 +82,17 @@ export const searchConfigs = async (gitDir = process.cwd(), logger) => {
49
82
 
50
83
  /** Load and validate all configs to the above object */
51
84
  await Promise.all(
52
- possibleConfigFiles
53
- .map((configPath) => loadConfig({ configPath }, logger))
54
- .map((promise) =>
55
- promise.then(({ config, filepath }) => {
56
- if (config) {
57
- configs[filepath] = validateConfig(config, filepath, logger)
85
+ Object.keys(configs).map((configPath) =>
86
+ loadConfig({ configPath }, logger).then(({ config, filepath }) => {
87
+ if (config) {
88
+ if (configPath !== filepath) {
89
+ debugLog('Config file "%s" resolved to "%s"', configPath, filepath)
58
90
  }
59
- })
60
- )
91
+
92
+ configs[configPath] = validateConfig(config, filepath, logger)
93
+ }
94
+ })
95
+ )
61
96
  )
62
97
 
63
98
  /** Get validated configs from the above object, without any `null` values (not found) */
@@ -65,5 +100,24 @@ export const searchConfigs = async (gitDir = process.cwd(), logger) => {
65
100
  .filter(([, value]) => !!value)
66
101
  .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {})
67
102
 
103
+ /**
104
+ * Try to find a single config from parent directories
105
+ * to match old behavior before monorepo support
106
+ */
107
+ if (!Object.keys(foundConfigs).length) {
108
+ debugLog('Could not find config files inside "%s"', cwd)
109
+
110
+ const { config, filepath } = await loadConfig({ cwd }, logger)
111
+ if (config) {
112
+ debugLog('Found parent configuration file from "%s"', filepath)
113
+
114
+ foundConfigs[filepath] = validateConfig(config, filepath, logger)
115
+ } else {
116
+ debugLog('Could not find parent configuration files from "%s"', cwd)
117
+ }
118
+ }
119
+
120
+ debugLog('Found %d config files', Object.keys(foundConfigs).length)
121
+
68
122
  return foundConfigs
69
123
  }
package/lib/state.js CHANGED
@@ -1,3 +1,5 @@
1
+ import EventEmitter from 'events'
2
+
1
3
  import { GIT_ERROR, TASK_ERROR } from './messages.js'
2
4
  import {
3
5
  ApplyEmptyCommitError,
@@ -11,6 +13,7 @@ export const getInitialState = ({ quiet = false } = {}) => ({
11
13
  hasPartiallyStagedFiles: null,
12
14
  shouldBackup: null,
13
15
  errors: new Set([]),
16
+ events: new EventEmitter(),
14
17
  output: [],
15
18
  quiet,
16
19
  })
@@ -1,5 +1,6 @@
1
- import { constants, promises as fs } from 'fs'
2
- import path from 'path'
1
+ import { constants } from 'node:fs'
2
+ import fs from 'node:fs/promises'
3
+ import path from 'node:path'
3
4
 
4
5
  import debug from 'debug'
5
6
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lint-staged",
3
- "version": "12.3.7",
3
+ "version": "13.0.2",
4
4
  "description": "Lint files staged by git",
5
5
  "license": "MIT",
6
6
  "repository": "https://github.com/okonet/lint-staged",
@@ -14,7 +14,7 @@
14
14
  "url": "https://opencollective.com/lint-staged"
15
15
  },
16
16
  "engines": {
17
- "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
17
+ "node": "^14.13.1 || >=16.0.0"
18
18
  },
19
19
  "type": "module",
20
20
  "bin": "./bin/lint-staged.js",
@@ -33,36 +33,36 @@
33
33
  },
34
34
  "dependencies": {
35
35
  "cli-truncate": "^3.1.0",
36
- "colorette": "^2.0.16",
37
- "commander": "^8.3.0",
38
- "debug": "^4.3.3",
39
- "execa": "^5.1.1",
40
- "lilconfig": "2.0.4",
41
- "listr2": "^4.0.1",
42
- "micromatch": "^4.0.4",
36
+ "colorette": "^2.0.17",
37
+ "commander": "^9.3.0",
38
+ "debug": "^4.3.4",
39
+ "execa": "^6.1.0",
40
+ "lilconfig": "2.0.5",
41
+ "listr2": "^4.0.5",
42
+ "micromatch": "^4.0.5",
43
43
  "normalize-path": "^3.0.0",
44
- "object-inspect": "^1.12.0",
45
- "pidtree": "^0.5.0",
44
+ "object-inspect": "^1.12.2",
45
+ "pidtree": "^0.6.0",
46
46
  "string-argv": "^0.3.1",
47
- "supports-color": "^9.2.1",
48
- "yaml": "^1.10.2"
47
+ "yaml": "^2.1.1"
49
48
  },
50
49
  "devDependencies": {
51
- "@babel/core": "^7.16.12",
52
- "@babel/eslint-parser": "^7.16.5",
53
- "@babel/preset-env": "^7.16.11",
54
- "babel-jest": "^27.4.6",
50
+ "@babel/core": "^7.18.2",
51
+ "@babel/eslint-parser": "^7.18.2",
52
+ "@babel/preset-env": "^7.18.2",
53
+ "babel-jest": "^28.1.1",
54
+ "babel-plugin-transform-imports": "2.0.0",
55
55
  "consolemock": "^1.1.0",
56
- "eslint": "^8.7.0",
57
- "eslint-config-prettier": "^8.3.0",
58
- "eslint-plugin-import": "^2.25.4",
56
+ "eslint": "^8.17.0",
57
+ "eslint-config-prettier": "^8.5.0",
58
+ "eslint-plugin-import": "^2.26.0",
59
59
  "eslint-plugin-node": "^11.1.0",
60
60
  "eslint-plugin-prettier": "^4.0.0",
61
- "fs-extra": "^10.0.0",
62
- "husky": "^7.0.4",
63
- "jest": "^27.4.7",
61
+ "fs-extra": "^10.1.0",
62
+ "husky": "^8.0.1",
63
+ "jest": "^28.1.1",
64
64
  "jest-snapshot-serializer-ansi": "^1.0.0",
65
- "prettier": "^2.5.1"
65
+ "prettier": "^2.6.2"
66
66
  },
67
67
  "keywords": [
68
68
  "lint",
@@ -1,105 +0,0 @@
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
- debugLog('Grouped staged files into %d groups!', Object.keys(configGroups).length)
103
-
104
- return configGroups
105
- }