lint-staged 16.1.6 → 16.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 +91 -25
- package/bin/lint-staged.js +84 -67
- package/lib/chunkFiles.js +2 -3
- package/lib/colors.js +106 -0
- package/lib/debug.js +27 -0
- package/lib/execGit.js +3 -2
- package/lib/figures.js +5 -4
- package/lib/file.js +2 -2
- package/lib/generateTasks.js +2 -2
- package/lib/getFunctionTask.js +2 -3
- package/lib/getRenderer.js +7 -8
- package/lib/getSpawnedTask.js +9 -6
- package/lib/getSpawnedTasks.js +14 -5
- package/lib/gitWorkflow.js +109 -60
- package/lib/groupFilesByConfig.js +2 -2
- package/lib/index.d.ts +30 -9
- package/lib/index.js +25 -8
- package/lib/loadConfig.js +30 -53
- package/lib/messages.js +21 -22
- package/lib/resolveGitRepo.js +2 -3
- package/lib/runAll.js +45 -24
- package/lib/searchConfigs.js +107 -60
- package/lib/state.js +22 -6
- package/lib/symbols.js +2 -0
- package/lib/validateConfig.js +2 -3
- package/lib/validateOptions.js +2 -3
- package/package.json +15 -21
- package/lib/dynamicImport.js +0 -3
package/lib/loadConfig.js
CHANGED
|
@@ -2,30 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
import fs from 'node:fs/promises'
|
|
4
4
|
import path from 'node:path'
|
|
5
|
+
import { pathToFileURL } from 'node:url'
|
|
5
6
|
|
|
6
|
-
import debug from 'debug'
|
|
7
7
|
import YAML from 'yaml'
|
|
8
8
|
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
CONFIG_NAME,
|
|
12
|
-
PACKAGE_JSON_FILE,
|
|
13
|
-
PACKAGE_YAML_FILES,
|
|
14
|
-
} from './configFiles.js'
|
|
15
|
-
import { dynamicImport } from './dynamicImport.js'
|
|
9
|
+
import { CONFIG_NAME, PACKAGE_JSON_FILE, PACKAGE_YAML_FILES } from './configFiles.js'
|
|
10
|
+
import { createDebug } from './debug.js'
|
|
16
11
|
import { failedToLoadConfig } from './messages.js'
|
|
17
12
|
import { resolveConfig } from './resolveConfig.js'
|
|
18
13
|
|
|
19
|
-
const debugLog =
|
|
14
|
+
const debugLog = createDebug('lint-staged:loadConfig')
|
|
20
15
|
|
|
21
|
-
const
|
|
22
|
-
|
|
16
|
+
const readFile = async (filename) => fs.readFile(path.resolve(filename), 'utf-8')
|
|
17
|
+
|
|
18
|
+
const jsonParse = async (filename) => {
|
|
19
|
+
const isPackageFile = PACKAGE_JSON_FILE.includes(path.basename(filename))
|
|
23
20
|
try {
|
|
21
|
+
const content = await readFile(filename)
|
|
24
22
|
const json = JSON.parse(content)
|
|
25
23
|
return isPackageFile ? json[CONFIG_NAME] : json
|
|
26
24
|
} catch (error) {
|
|
27
|
-
if (path.basename(
|
|
28
|
-
debugLog('Ignoring invalid
|
|
25
|
+
if (path.basename(filename) === PACKAGE_JSON_FILE) {
|
|
26
|
+
debugLog('Ignoring invalid JSON file %s', filename)
|
|
29
27
|
return undefined
|
|
30
28
|
}
|
|
31
29
|
|
|
@@ -33,14 +31,15 @@ const jsonParse = (filePath, content) => {
|
|
|
33
31
|
}
|
|
34
32
|
}
|
|
35
33
|
|
|
36
|
-
const yamlParse = (
|
|
37
|
-
const isPackageFile = PACKAGE_YAML_FILES.includes(path.basename(
|
|
34
|
+
const yamlParse = async (filename) => {
|
|
35
|
+
const isPackageFile = PACKAGE_YAML_FILES.includes(path.basename(filename))
|
|
38
36
|
try {
|
|
37
|
+
const content = await readFile(filename)
|
|
39
38
|
const yaml = YAML.parse(content)
|
|
40
39
|
return isPackageFile ? yaml[CONFIG_NAME] : yaml
|
|
41
40
|
} catch (error) {
|
|
42
41
|
if (isPackageFile) {
|
|
43
|
-
debugLog('Ignoring invalid
|
|
42
|
+
debugLog('Ignoring invalid YAML file %s', filename)
|
|
44
43
|
return undefined
|
|
45
44
|
}
|
|
46
45
|
|
|
@@ -48,6 +47,8 @@ const yamlParse = (filePath, content) => {
|
|
|
48
47
|
}
|
|
49
48
|
}
|
|
50
49
|
|
|
50
|
+
export const dynamicImport = (path) => import(pathToFileURL(path)).then((module) => module.default)
|
|
51
|
+
|
|
51
52
|
const NO_EXT = 'noExt'
|
|
52
53
|
|
|
53
54
|
/**
|
|
@@ -69,54 +70,30 @@ const loaders = {
|
|
|
69
70
|
'.yml': yamlParse,
|
|
70
71
|
}
|
|
71
72
|
|
|
72
|
-
const
|
|
73
|
-
const
|
|
74
|
-
return fs.readFile(absolutePath, 'utf-8')
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const loadConfigByExt = async (filepath) => {
|
|
78
|
-
filepath = path.resolve(filepath)
|
|
73
|
+
const loadConfigByExt = async (filename) => {
|
|
74
|
+
const filepath = path.resolve(filename)
|
|
79
75
|
const ext = path.extname(filepath) || NO_EXT
|
|
80
76
|
const loader = loaders[ext]
|
|
77
|
+
const config = await loader(filepath)
|
|
81
78
|
|
|
82
|
-
|
|
83
|
-
* No need to read file contents when loader only takes in the filepath argument
|
|
84
|
-
* and reads itself; this is for `lilconfig` compatibility
|
|
85
|
-
*/
|
|
86
|
-
const content = loader.length > 1 ? await readFile(filepath) : undefined
|
|
87
|
-
|
|
88
|
-
return {
|
|
89
|
-
config: await loader(filepath, content),
|
|
90
|
-
filepath,
|
|
91
|
-
}
|
|
79
|
+
return { config, filepath }
|
|
92
80
|
}
|
|
93
81
|
|
|
94
|
-
/**
|
|
95
|
-
|
|
96
|
-
* @param {string} [options.configPath] - Explicit path to a config file
|
|
97
|
-
* @param {string} [options.cwd] - Current working directory
|
|
98
|
-
*/
|
|
99
|
-
export const loadConfig = async ({ configPath, cwd }, logger) => {
|
|
82
|
+
/** @param {string} configPath */
|
|
83
|
+
export const loadConfig = async (configPath, logger) => {
|
|
100
84
|
try {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (configPath) {
|
|
104
|
-
debugLog('Loading configuration from `%s`...', configPath)
|
|
105
|
-
result = await loadConfigByExt(resolveConfig(configPath))
|
|
106
|
-
} else {
|
|
107
|
-
debugLog('Searching for configuration from `%s`...', cwd)
|
|
108
|
-
const { lilconfig } = await import('lilconfig')
|
|
109
|
-
const explorer = lilconfig(CONFIG_NAME, { searchPlaces: CONFIG_FILE_NAMES, loaders })
|
|
110
|
-
result = await explorer.search(cwd)
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (!result) return {}
|
|
85
|
+
debugLog('Loading configuration from `%s`...', configPath)
|
|
86
|
+
const result = await loadConfigByExt(resolveConfig(configPath))
|
|
114
87
|
|
|
115
88
|
// config is a promise when using the `dynamicImport` loader
|
|
116
89
|
const config = (await result.config) ?? null
|
|
117
90
|
const filepath = result.filepath
|
|
118
91
|
|
|
119
|
-
|
|
92
|
+
if (config) {
|
|
93
|
+
debugLog('Successfully loaded config from `%s`:\n%O', filepath, config)
|
|
94
|
+
} else {
|
|
95
|
+
debugLog('Found no config in %s', filepath)
|
|
96
|
+
}
|
|
120
97
|
|
|
121
98
|
return { config, filepath }
|
|
122
99
|
} catch (error) {
|
package/lib/messages.js
CHANGED
|
@@ -1,22 +1,21 @@
|
|
|
1
1
|
import { inspect } from 'node:util'
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
|
|
3
|
+
import { bold, red, yellow } from './colors.js'
|
|
5
4
|
import { error, info, warning } from './figures.js'
|
|
6
5
|
|
|
7
6
|
export const configurationError = (opt, helpMsg, value) =>
|
|
8
|
-
`${
|
|
7
|
+
`${red(`${error} Validation Error:`)}
|
|
9
8
|
|
|
10
|
-
Invalid value for '${
|
|
9
|
+
Invalid value for '${bold(opt)}': ${bold(inspect(value))}
|
|
11
10
|
|
|
12
11
|
${helpMsg}`
|
|
13
12
|
|
|
14
|
-
export const NOT_GIT_REPO =
|
|
13
|
+
export const NOT_GIT_REPO = red(`${error} Current directory is not a git directory!`)
|
|
15
14
|
|
|
16
|
-
export const FAILED_GET_STAGED_FILES =
|
|
15
|
+
export const FAILED_GET_STAGED_FILES = red(`${error} Failed to get staged files!`)
|
|
17
16
|
|
|
18
17
|
export const incorrectBraces = (before, after) =>
|
|
19
|
-
|
|
18
|
+
yellow(
|
|
20
19
|
`${warning} Detected incorrect braces with only single value: \`${before}\`. Reformatted as: \`${after}\`
|
|
21
20
|
`
|
|
22
21
|
)
|
|
@@ -34,54 +33,54 @@ export const skippingBackup = (hasInitialCommit, diff) => {
|
|
|
34
33
|
: (hasInitialCommit ? '`--no-stash` was used' : 'there’s no initial commit yet') +
|
|
35
34
|
'. This might result in data loss'
|
|
36
35
|
|
|
37
|
-
return
|
|
36
|
+
return yellow(`${warning} Skipping backup because ${reason}.\n`)
|
|
38
37
|
}
|
|
39
38
|
|
|
40
|
-
export const SKIPPING_HIDE_PARTIALLY_CHANGED =
|
|
39
|
+
export const SKIPPING_HIDE_PARTIALLY_CHANGED = yellow(
|
|
41
40
|
`${warning} Skipping hiding unstaged changes from partially staged files because \`--no-hide-partially-staged\` was used.\n`
|
|
42
41
|
)
|
|
43
42
|
|
|
44
|
-
export const DEPRECATED_GIT_ADD =
|
|
43
|
+
export const DEPRECATED_GIT_ADD = yellow(
|
|
45
44
|
`${warning} Some of your tasks use \`git add\` command. Please remove it from the config since all modifications made by tasks will be automatically added to the git commit index.
|
|
46
45
|
`
|
|
47
46
|
)
|
|
48
47
|
|
|
49
48
|
export const TASK_ERROR = 'Skipped because of errors from tasks.'
|
|
50
49
|
|
|
50
|
+
export const PREVENTED_TASK_MODIFICATIONS = `\n${error} lint-staged failed because \`--fail-on-changes\` was used.`
|
|
51
|
+
|
|
51
52
|
export const SKIPPED_GIT_ERROR = 'Skipped because of previous git error.'
|
|
52
53
|
|
|
53
|
-
export const GIT_ERROR = `\n ${
|
|
54
|
+
export const GIT_ERROR = `\n ${red(`${error} lint-staged failed due to a git error.`)}`
|
|
54
55
|
|
|
55
|
-
export const invalidOption = (name, value, message) => `${
|
|
56
|
-
`${error} Validation Error:`
|
|
57
|
-
)}
|
|
56
|
+
export const invalidOption = (name, value, message) => `${red(`${error} Validation Error:`)}
|
|
58
57
|
|
|
59
|
-
Invalid value for option '${
|
|
58
|
+
Invalid value for option '${bold(name)}': ${bold(value)}
|
|
60
59
|
|
|
61
60
|
${message}
|
|
62
61
|
|
|
63
62
|
See https://github.com/okonet/lint-staged#command-line-flags`
|
|
64
63
|
|
|
65
64
|
export const PREVENTED_EMPTY_COMMIT = `
|
|
66
|
-
${
|
|
65
|
+
${yellow(`${warning} lint-staged prevented an empty git commit.
|
|
67
66
|
Use the --allow-empty option to continue, or check your task configuration`)}
|
|
68
67
|
`
|
|
69
68
|
|
|
70
69
|
export const RESTORE_STASH_EXAMPLE = `Any lost modifications can be restored from a git stash:
|
|
71
70
|
|
|
72
|
-
> git stash list
|
|
73
|
-
|
|
74
|
-
> git
|
|
71
|
+
> git stash list --format="%h %s"
|
|
72
|
+
h0a0s0h0 On main: lint-staged automatic backup
|
|
73
|
+
> git apply --index h0a0s0h0`
|
|
75
74
|
|
|
76
|
-
export const CONFIG_STDIN_ERROR =
|
|
75
|
+
export const CONFIG_STDIN_ERROR = red(`${error} Failed to read config from stdin.`)
|
|
77
76
|
|
|
78
77
|
export const failedToLoadConfig = (filepath) =>
|
|
79
|
-
|
|
78
|
+
red(`${error} Failed to read config from file "${filepath}".`)
|
|
80
79
|
|
|
81
80
|
export const failedToParseConfig = (
|
|
82
81
|
filepath,
|
|
83
82
|
error
|
|
84
|
-
) => `${
|
|
83
|
+
) => `${red(`${error} Failed to parse config from file "${filepath}".`)}
|
|
85
84
|
|
|
86
85
|
${error}
|
|
87
86
|
|
package/lib/resolveGitRepo.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
|
|
3
|
+
import { createDebug } from './debug.js'
|
|
5
4
|
import { execGit } from './execGit.js'
|
|
6
5
|
import { normalizePath } from './normalizePath.js'
|
|
7
6
|
|
|
8
|
-
const debugLog =
|
|
7
|
+
const debugLog = createDebug('lint-staged:resolveGitRepo')
|
|
9
8
|
|
|
10
9
|
/**
|
|
11
10
|
* Relative path up to the repo top-level directory
|
package/lib/runAll.js
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
|
|
5
|
-
import chalk from 'chalk'
|
|
6
|
-
import debug from 'debug'
|
|
7
5
|
import { Listr } from 'listr2'
|
|
8
6
|
|
|
9
7
|
import { chunkFiles } from './chunkFiles.js'
|
|
8
|
+
import { blackBright } from './colors.js'
|
|
9
|
+
import { createDebug } from './debug.js'
|
|
10
10
|
import { execGit } from './execGit.js'
|
|
11
11
|
import { generateTasks } from './generateTasks.js'
|
|
12
12
|
import { getFunctionTask, isFunctionTask } from './getFunctionTask.js'
|
|
@@ -37,10 +37,11 @@ import {
|
|
|
37
37
|
restoreOriginalStateSkipped,
|
|
38
38
|
restoreUnstagedChangesSkipped,
|
|
39
39
|
shouldHidePartiallyStagedFiles,
|
|
40
|
+
shouldRestoreUnstagedChanges,
|
|
40
41
|
} from './state.js'
|
|
41
42
|
import { ConfigNotFoundError, GetStagedFilesError, GitError, GitRepoError } from './symbols.js'
|
|
42
43
|
|
|
43
|
-
const debugLog =
|
|
44
|
+
const debugLog = createDebug('lint-staged:runAll')
|
|
44
45
|
|
|
45
46
|
/**
|
|
46
47
|
* @param {ReturnType<typeof getInitialState>} ctx context
|
|
@@ -54,15 +55,20 @@ const createError = (ctx, cause) =>
|
|
|
54
55
|
*
|
|
55
56
|
* @param {object} options
|
|
56
57
|
* @param {boolean} [options.allowEmpty] - Allow empty commits when tasks revert all staged changes
|
|
58
|
+
* @param {boolean} [options.color] - Enable or disable ANSI color codes in output.
|
|
57
59
|
* @param {boolean | number} [options.concurrent] - The number of tasks to run concurrently, or false to run tasks serially
|
|
58
60
|
* @param {Object} [options.configObject] - Explicit config object from the js API
|
|
59
61
|
* @param {string} [options.configPath] - Explicit path to a config file
|
|
62
|
+
* @param {boolean} [options.continueOnError] - Run all tasks to completion even if one fails
|
|
60
63
|
* @param {string} [options.cwd] - Current working directory
|
|
61
64
|
* @param {boolean} [options.debug] - Enable debug mode
|
|
62
65
|
* @param {string} [options.diff] - Override the default "--staged" flag of "git diff" to get list of files
|
|
63
66
|
* @param {string} [options.diffFilter] - Override the default "--diff-filter=ACMR" flag of "git diff" to get list of files
|
|
67
|
+
* @param {boolean} [options.failOnChanges] - Fail with exit code 1 when tasks modify tracked files
|
|
68
|
+
* @param {boolean} [options.hidePartiallyStaged] - Whether to hide unstaged changes from partially staged files before running tasks
|
|
69
|
+
* @param {boolean} [options.hideUnstaged] - Whether to hide all unstaged changes before running tasks
|
|
64
70
|
* @param {number} [options.maxArgLength] - Maximum argument string length
|
|
65
|
-
* @param {boolean} [options.quiet] - Disable lint-staged
|
|
71
|
+
* @param {boolean} [options.quiet] - Disable lint-staged's own console output
|
|
66
72
|
* @param {boolean} [options.relative] - Pass relative filepaths to tasks
|
|
67
73
|
* @param {boolean} [options.revert] - revert to original state in case of errors
|
|
68
74
|
* @param {boolean} [options.stash] - Enable the backup stash, and revert in case of errors
|
|
@@ -73,13 +79,18 @@ const createError = (ctx, cause) =>
|
|
|
73
79
|
export const runAll = async (
|
|
74
80
|
{
|
|
75
81
|
allowEmpty = false,
|
|
82
|
+
color = false,
|
|
76
83
|
concurrent = true,
|
|
77
84
|
configObject,
|
|
78
85
|
configPath,
|
|
86
|
+
continueOnError = false,
|
|
79
87
|
cwd,
|
|
80
88
|
debug = false,
|
|
81
89
|
diff,
|
|
82
90
|
diffFilter,
|
|
91
|
+
failOnChanges = false,
|
|
92
|
+
hideUnstaged = false,
|
|
93
|
+
hidePartiallyStaged = !hideUnstaged,
|
|
83
94
|
maxArgLength,
|
|
84
95
|
quiet = false,
|
|
85
96
|
relative = false,
|
|
@@ -87,7 +98,6 @@ export const runAll = async (
|
|
|
87
98
|
stash = diff === undefined,
|
|
88
99
|
// Cannot revert to original state without stash
|
|
89
100
|
revert = stash,
|
|
90
|
-
hidePartiallyStaged = true,
|
|
91
101
|
verbose = false,
|
|
92
102
|
},
|
|
93
103
|
logger = console
|
|
@@ -99,7 +109,12 @@ export const runAll = async (
|
|
|
99
109
|
cwd = hasExplicitCwd ? path.resolve(cwd) : process.cwd()
|
|
100
110
|
debugLog('Using working directory `%s`', cwd)
|
|
101
111
|
|
|
102
|
-
const ctx = getInitialState({
|
|
112
|
+
const ctx = getInitialState({
|
|
113
|
+
hidePartiallyStaged,
|
|
114
|
+
hideUnstaged,
|
|
115
|
+
quiet,
|
|
116
|
+
revert,
|
|
117
|
+
})
|
|
103
118
|
|
|
104
119
|
const { topLevelDir, gitConfigDir } = await resolveGitRepo(cwd)
|
|
105
120
|
if (!topLevelDir) {
|
|
@@ -121,12 +136,16 @@ export const runAll = async (
|
|
|
121
136
|
logger.warn(skippingBackup(hasInitialCommit, diff))
|
|
122
137
|
}
|
|
123
138
|
|
|
124
|
-
ctx.shouldHidePartiallyStaged
|
|
125
|
-
if (!ctx.shouldHidePartiallyStaged && !quiet) {
|
|
139
|
+
if (!ctx.shouldHidePartiallyStaged && !ctx.shouldHideUnstaged && !quiet) {
|
|
126
140
|
logger.warn(SKIPPING_HIDE_PARTIALLY_CHANGED)
|
|
127
141
|
}
|
|
128
142
|
|
|
129
|
-
|
|
143
|
+
// Run staged files retrieval and config search in parallel since they're independent
|
|
144
|
+
const [stagedFiles, foundConfigs] = await Promise.all([
|
|
145
|
+
getStagedFiles({ cwd: topLevelDir, diff, diffFilter }),
|
|
146
|
+
searchConfigs({ configObject, configPath, cwd, topLevelDir }, logger),
|
|
147
|
+
])
|
|
148
|
+
|
|
130
149
|
if (!stagedFiles) {
|
|
131
150
|
if (!quiet) ctx.output.push(FAILED_GET_STAGED_FILES)
|
|
132
151
|
ctx.errors.add(GetStagedFilesError)
|
|
@@ -140,7 +159,6 @@ export const runAll = async (
|
|
|
140
159
|
return ctx
|
|
141
160
|
}
|
|
142
161
|
|
|
143
|
-
const foundConfigs = await searchConfigs({ configObject, configPath, cwd, topLevelDir }, logger)
|
|
144
162
|
const numberOfConfigs = Object.keys(foundConfigs).length
|
|
145
163
|
|
|
146
164
|
// Throw if no configurations were found
|
|
@@ -165,7 +183,7 @@ export const runAll = async (
|
|
|
165
183
|
ctx,
|
|
166
184
|
exitOnError: false,
|
|
167
185
|
registerSignalListeners: false,
|
|
168
|
-
...getRenderer({ debug, quiet }, logger),
|
|
186
|
+
...getRenderer({ color, debug, quiet }, logger),
|
|
169
187
|
}
|
|
170
188
|
|
|
171
189
|
/**
|
|
@@ -203,6 +221,7 @@ export const runAll = async (
|
|
|
203
221
|
(isFunctionTask(task.commands)
|
|
204
222
|
? getFunctionTask(task.commands, task.fileList)
|
|
205
223
|
: getSpawnedTasks({
|
|
224
|
+
color,
|
|
206
225
|
commands: task.commands,
|
|
207
226
|
cwd: groupCwd,
|
|
208
227
|
files: task.fileList,
|
|
@@ -231,19 +250,19 @@ export const runAll = async (
|
|
|
231
250
|
const fileCount = task.fileList.length
|
|
232
251
|
|
|
233
252
|
return {
|
|
234
|
-
title: `${task.pattern}${
|
|
253
|
+
title: `${task.pattern}${blackBright(
|
|
235
254
|
` — ${fileCount} ${fileCount === 1 ? 'file' : 'files'}`
|
|
236
255
|
)}`,
|
|
237
256
|
task: async (ctx, task) =>
|
|
238
257
|
task.newListr(
|
|
239
258
|
subTasks,
|
|
240
259
|
// Subtasks should not run in parallel, and should exit on error
|
|
241
|
-
{ concurrent: false, exitOnError:
|
|
260
|
+
{ concurrent: false, exitOnError: !continueOnError }
|
|
242
261
|
),
|
|
243
262
|
skip: () => {
|
|
244
263
|
// Skip task when no files matched
|
|
245
264
|
if (fileCount === 0) {
|
|
246
|
-
return `${task.pattern}${
|
|
265
|
+
return `${task.pattern}${blackBright(' — no files')}`
|
|
247
266
|
}
|
|
248
267
|
return false
|
|
249
268
|
},
|
|
@@ -256,15 +275,16 @@ export const runAll = async (
|
|
|
256
275
|
|
|
257
276
|
listrTasks.push({
|
|
258
277
|
title:
|
|
259
|
-
`${configName}${
|
|
260
|
-
(chunkCount > 1 ?
|
|
261
|
-
task: (ctx, task) =>
|
|
278
|
+
`${configName}${blackBright(` — ${files.length} ${files.length > 1 ? 'files' : 'file'}`)}` +
|
|
279
|
+
(chunkCount > 1 ? blackBright(` (chunk ${index + 1}/${chunkCount})...`) : ''),
|
|
280
|
+
task: (ctx, task) =>
|
|
281
|
+
task.newListr(chunkListrTasks, { concurrent, exitOnError: !continueOnError }),
|
|
262
282
|
skip: () => {
|
|
263
283
|
// Skip if the first step (backup) failed
|
|
264
284
|
if (ctx.errors.has(GitError)) return SKIPPED_GIT_ERROR
|
|
265
285
|
// Skip chunk when no every task is skipped (due to no matches)
|
|
266
286
|
if (chunkListrTasks.every((task) => task.skip())) {
|
|
267
|
-
return `${configName}${
|
|
287
|
+
return `${configName}${blackBright(' — no tasks to run')}`
|
|
268
288
|
}
|
|
269
289
|
return false
|
|
270
290
|
},
|
|
@@ -295,11 +315,12 @@ export const runAll = async (
|
|
|
295
315
|
|
|
296
316
|
const git = new GitWorkflow({
|
|
297
317
|
allowEmpty,
|
|
298
|
-
gitConfigDir,
|
|
299
|
-
topLevelDir,
|
|
300
|
-
matchedFileChunks,
|
|
301
318
|
diff,
|
|
302
319
|
diffFilter,
|
|
320
|
+
failOnChanges,
|
|
321
|
+
gitConfigDir,
|
|
322
|
+
matchedFileChunks,
|
|
323
|
+
topLevelDir,
|
|
303
324
|
})
|
|
304
325
|
|
|
305
326
|
const runner = new Listr(
|
|
@@ -310,7 +331,7 @@ export const runAll = async (
|
|
|
310
331
|
},
|
|
311
332
|
{
|
|
312
333
|
title: 'Hiding unstaged changes to partially staged files...',
|
|
313
|
-
task: (ctx) => git.
|
|
334
|
+
task: (ctx) => git.hidePartiallyStagedChanges(ctx),
|
|
314
335
|
enabled: shouldHidePartiallyStagedFiles,
|
|
315
336
|
},
|
|
316
337
|
{
|
|
@@ -324,9 +345,9 @@ export const runAll = async (
|
|
|
324
345
|
skip: applyModificationsSkipped,
|
|
325
346
|
},
|
|
326
347
|
{
|
|
327
|
-
title: 'Restoring unstaged changes
|
|
348
|
+
title: 'Restoring unstaged changes...',
|
|
328
349
|
task: (ctx) => git.restoreUnstagedChanges(ctx),
|
|
329
|
-
enabled:
|
|
350
|
+
enabled: shouldRestoreUnstagedChanges,
|
|
330
351
|
skip: restoreUnstagedChangesSkipped,
|
|
331
352
|
},
|
|
332
353
|
{
|
package/lib/searchConfigs.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
/** @typedef {import('./index').Logger} Logger */
|
|
2
2
|
|
|
3
|
+
import fs, { constants } from 'node:fs/promises'
|
|
3
4
|
import path from 'node:path'
|
|
4
5
|
|
|
5
|
-
import debug from 'debug'
|
|
6
|
-
|
|
7
6
|
import { CONFIG_FILE_NAMES } from './configFiles.js'
|
|
7
|
+
import { createDebug } from './debug.js'
|
|
8
8
|
import { execGit } from './execGit.js'
|
|
9
9
|
import { loadConfig } from './loadConfig.js'
|
|
10
10
|
import { normalizePath } from './normalizePath.js'
|
|
11
11
|
import { parseGitZOutput } from './parseGitZOutput.js'
|
|
12
12
|
import { validateConfig } from './validateConfig.js'
|
|
13
13
|
|
|
14
|
-
const debugLog =
|
|
14
|
+
const debugLog = createDebug('lint-staged:searchConfigs')
|
|
15
15
|
|
|
16
16
|
const EXEC_GIT = ['ls-files', '-z', '--full-name', '-t']
|
|
17
17
|
|
|
@@ -19,8 +19,89 @@ const CONFIG_PATHSPEC = CONFIG_FILE_NAMES.map((f) => `:(glob)**/${f}`)
|
|
|
19
19
|
|
|
20
20
|
const numberOfLevels = (file) => file.split('/').length
|
|
21
21
|
|
|
22
|
+
const sortAlphabetically = (a, b) => a.localeCompare(b)
|
|
23
|
+
|
|
22
24
|
const sortDeepestParth = (a, b) => (numberOfLevels(a) > numberOfLevels(b) ? -1 : 1)
|
|
23
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Get all possible config files from git
|
|
28
|
+
*
|
|
29
|
+
* @param {object} options
|
|
30
|
+
* @param {string} options.cwd
|
|
31
|
+
* @param {string} options.topLevelDir
|
|
32
|
+
* @returns {Promise<string[]>}
|
|
33
|
+
*/
|
|
34
|
+
const listConfigFilesFromGit = async ({ cwd, topLevelDir }) =>
|
|
35
|
+
execGit(
|
|
36
|
+
[
|
|
37
|
+
...EXEC_GIT,
|
|
38
|
+
'--cached', // show all tracked files
|
|
39
|
+
'--others', // show untracked files
|
|
40
|
+
'--exclude-standard', // apply standard git exclusions (.gitignore, etc.)
|
|
41
|
+
'--',
|
|
42
|
+
...CONFIG_PATHSPEC,
|
|
43
|
+
],
|
|
44
|
+
{ cwd }
|
|
45
|
+
)
|
|
46
|
+
.then(parseGitZOutput)
|
|
47
|
+
.then((lines) => {
|
|
48
|
+
const possibleConfigFiles = lines.flatMap((line) => {
|
|
49
|
+
/**
|
|
50
|
+
* Leave out lines starting with "S " to ignore not-checked-out files in a sparse repo.
|
|
51
|
+
* The "S" status means a tracked file that is "skip-worktree"
|
|
52
|
+
* @see https://git-scm.com/docs/git-ls-files#Documentation/git-ls-files.txt--t
|
|
53
|
+
*/
|
|
54
|
+
if (line.startsWith('S ')) {
|
|
55
|
+
return []
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const relativePath = line.replace(/^[HSMRCK?U] /, '')
|
|
59
|
+
const absolutePath = normalizePath(path.join(topLevelDir, relativePath))
|
|
60
|
+
return [absolutePath]
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
debugLog('Found possible config files from git:', possibleConfigFiles)
|
|
64
|
+
|
|
65
|
+
return possibleConfigFiles
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get all possible config files from filesystem, starting from `cwd` and
|
|
70
|
+
* moving upwards if nothing is found.
|
|
71
|
+
*
|
|
72
|
+
* @param {object} options
|
|
73
|
+
* @param {string} options.cwd
|
|
74
|
+
* @param {string[]} [possibleConfigFiles]
|
|
75
|
+
* @returns {Promise<string[]>}
|
|
76
|
+
*/
|
|
77
|
+
export const listConfigFilesFromFs = async ({ cwd }) => {
|
|
78
|
+
debugLog('Listing possible configs from filesystem starting from "%s"...', cwd)
|
|
79
|
+
|
|
80
|
+
const results = await Promise.allSettled(
|
|
81
|
+
CONFIG_FILE_NAMES.map(async (f) => {
|
|
82
|
+
const filepath = path.join(cwd, f)
|
|
83
|
+
await fs.access(filepath, constants.F_OK)
|
|
84
|
+
return filepath
|
|
85
|
+
})
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
const possibleConfigFiles = results.flatMap((r) =>
|
|
89
|
+
r.status === 'fulfilled' ? [normalizePath(r.value)] : []
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if (possibleConfigFiles.length > 0) {
|
|
93
|
+
debugLog('Found possible config files from filesystem:', possibleConfigFiles)
|
|
94
|
+
return possibleConfigFiles
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const parentDir = path.dirname(cwd)
|
|
98
|
+
if (parentDir === cwd) {
|
|
99
|
+
return [] /** Root-level / */
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return listConfigFilesFromFs({ cwd: parentDir })
|
|
103
|
+
}
|
|
104
|
+
|
|
24
105
|
/**
|
|
25
106
|
* Search all config files from the git repository, preferring those inside `cwd`.
|
|
26
107
|
*
|
|
@@ -28,6 +109,7 @@ const sortDeepestParth = (a, b) => (numberOfLevels(a) > numberOfLevels(b) ? -1 :
|
|
|
28
109
|
* @param {Object} [options.configObject] - Explicit config object from the js API
|
|
29
110
|
* @param {string} [options.configPath] - Explicit path to a config file
|
|
30
111
|
* @param {string} [options.cwd] - Current working directory
|
|
112
|
+
* @param {string} [options.topLevelDir] - Top-level directory of the git repo
|
|
31
113
|
* @param {Logger} logger
|
|
32
114
|
*
|
|
33
115
|
* @returns {Promise<{ [key: string]: { config: *, files: string[] } }>} found configs with filepath as key, and config as value
|
|
@@ -49,57 +131,35 @@ export const searchConfigs = async (
|
|
|
49
131
|
if (configPath) {
|
|
50
132
|
debugLog('Using single configuration path...')
|
|
51
133
|
|
|
52
|
-
const { config, filepath } = await loadConfig(
|
|
134
|
+
const { config, filepath } = await loadConfig(configPath, logger)
|
|
53
135
|
|
|
54
136
|
if (!config) return {}
|
|
55
137
|
return { [configPath]: validateConfig(config, filepath, logger) }
|
|
56
138
|
}
|
|
57
139
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
'--others', // show untracked files
|
|
64
|
-
'--exclude-standard', // apply standard git exclusions (.gitignore, etc.)
|
|
65
|
-
'--',
|
|
66
|
-
...CONFIG_PATHSPEC,
|
|
67
|
-
],
|
|
68
|
-
{ cwd }
|
|
69
|
-
).then(parseGitZOutput)
|
|
70
|
-
|
|
71
|
-
debugLog('Git listed files matching config files:', gitListedFiles)
|
|
72
|
-
|
|
73
|
-
/** Sort possible config files so that deepest is first */
|
|
74
|
-
const possibleConfigFiles = gitListedFiles
|
|
75
|
-
.flatMap((line) => {
|
|
76
|
-
/**
|
|
77
|
-
* Leave out lines starting with "S " to ignore not-checked-out files in a sparse repo.
|
|
78
|
-
* The "S" status means a tracked file that is "skip-worktree"
|
|
79
|
-
* @see https://git-scm.com/docs/git-ls-files#Documentation/git-ls-files.txt--t
|
|
80
|
-
*/
|
|
81
|
-
if (line.startsWith('S ')) {
|
|
82
|
-
return []
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const relativePath = line.replace(/^[HSMRCK?U] /, '')
|
|
86
|
-
const absolutePath = normalizePath(path.join(topLevelDir, relativePath))
|
|
87
|
-
return [absolutePath]
|
|
140
|
+
const possibleConfigFiles = new Set()
|
|
141
|
+
|
|
142
|
+
const addToSet = (files) => {
|
|
143
|
+
files.forEach((f) => {
|
|
144
|
+
possibleConfigFiles.add(f)
|
|
88
145
|
})
|
|
89
|
-
|
|
146
|
+
}
|
|
90
147
|
|
|
91
|
-
|
|
148
|
+
await Promise.all([
|
|
149
|
+
listConfigFilesFromGit({ cwd, topLevelDir }).then(addToSet),
|
|
150
|
+
listConfigFilesFromFs({ cwd }).then(addToSet),
|
|
151
|
+
])
|
|
92
152
|
|
|
93
153
|
/** Create object with key as config file, and value as null */
|
|
94
|
-
const configs =
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
154
|
+
const configs = Array.from(possibleConfigFiles)
|
|
155
|
+
.sort(sortAlphabetically)
|
|
156
|
+
.sort(sortDeepestParth)
|
|
157
|
+
.reduce((acc, configPath) => Object.assign(acc, { [configPath]: null }), {})
|
|
98
158
|
|
|
99
159
|
/** Load and validate all configs to the above object */
|
|
100
160
|
await Promise.all(
|
|
101
161
|
Object.keys(configs).map((configPath) =>
|
|
102
|
-
loadConfig(
|
|
162
|
+
loadConfig(configPath, logger).then(({ config, filepath }) => {
|
|
103
163
|
if (config) {
|
|
104
164
|
if (configPath !== filepath) {
|
|
105
165
|
debugLog('Config file "%s" resolved to "%s"', configPath, filepath)
|
|
@@ -112,26 +172,13 @@ export const searchConfigs = async (
|
|
|
112
172
|
)
|
|
113
173
|
|
|
114
174
|
/** Get validated configs from the above object, without any `null` values (not found) */
|
|
115
|
-
const foundConfigs = Object.entries(configs)
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Try to find a single config from parent directories
|
|
121
|
-
* to match old behavior before monorepo support
|
|
122
|
-
*/
|
|
123
|
-
if (!Object.keys(foundConfigs).length) {
|
|
124
|
-
debugLog('Could not find config files inside "%s"', cwd)
|
|
125
|
-
|
|
126
|
-
const { config, filepath } = await loadConfig({ cwd }, logger)
|
|
127
|
-
if (config) {
|
|
128
|
-
debugLog('Found parent configuration file from "%s"', filepath)
|
|
129
|
-
|
|
130
|
-
foundConfigs[filepath] = validateConfig(config, filepath, logger)
|
|
131
|
-
} else {
|
|
132
|
-
debugLog('Could not find parent configuration files from "%s"', cwd)
|
|
175
|
+
const foundConfigs = Object.entries(configs).reduce((acc, [key, value]) => {
|
|
176
|
+
if (value) {
|
|
177
|
+
Object.assign(acc, { [key]: value })
|
|
133
178
|
}
|
|
134
|
-
|
|
179
|
+
|
|
180
|
+
return acc
|
|
181
|
+
}, {})
|
|
135
182
|
|
|
136
183
|
debugLog('Found %d config files', Object.keys(foundConfigs).length)
|
|
137
184
|
|