UNPKG

10.5 kBJavaScriptView Raw
1/** @typedef {import('./index').Logger} Logger */
2
3import path from 'path'
4
5import { dim } from 'colorette'
6import debug from 'debug'
7import { Listr } from 'listr2'
8import normalize from 'normalize-path'
9
10import { chunkFiles } from './chunkFiles.js'
11import { execGit } from './execGit.js'
12import { generateTasks } from './generateTasks.js'
13import { getConfigGroups } from './getConfigGroups.js'
14import { getRenderer } from './getRenderer.js'
15import { getStagedFiles } from './getStagedFiles.js'
16import { GitWorkflow } from './gitWorkflow.js'
17import { makeCmdTasks } from './makeCmdTasks.js'
18import {
19 DEPRECATED_GIT_ADD,
20 FAILED_GET_STAGED_FILES,
21 NOT_GIT_REPO,
22 NO_STAGED_FILES,
23 NO_TASKS,
24 SKIPPED_GIT_ERROR,
25 skippingBackup,
26} from './messages.js'
27import { resolveGitRepo } from './resolveGitRepo.js'
28import {
29 applyModificationsSkipped,
30 cleanupEnabled,
31 cleanupSkipped,
32 getInitialState,
33 hasPartiallyStagedFiles,
34 restoreOriginalStateEnabled,
35 restoreOriginalStateSkipped,
36 restoreUnstagedChangesSkipped,
37} from './state.js'
38import { GitRepoError, GetStagedFilesError, GitError, ConfigNotFoundError } from './symbols.js'
39import { searchConfigs } from './searchConfigs.js'
40
41const debugLog = debug('lint-staged:runAll')
42
43const createError = (ctx) => Object.assign(new Error('lint-staged failed'), { ctx })
44
45/**
46 * Executes all tasks and either resolves or rejects the promise
47 *
48 * @param {object} options
49 * @param {boolean} [options.allowEmpty] - Allow empty commits when tasks revert all staged changes
50 * @param {boolean | number} [options.concurrent] - The number of tasks to run concurrently, or false to run tasks serially
51 * @param {Object} [options.configObject] - Explicit config object from the js API
52 * @param {string} [options.configPath] - Explicit path to a config file
53 * @param {string} [options.cwd] - Current working directory
54 * @param {boolean} [options.debug] - Enable debug mode
55 * @param {number} [options.maxArgLength] - Maximum argument string length
56 * @param {boolean} [options.quiet] - Disable lint-staged’s own console output
57 * @param {boolean} [options.relative] - Pass relative filepaths to tasks
58 * @param {boolean} [options.shell] - Skip parsing of tasks for better shell support
59 * @param {boolean} [options.stash] - Enable the backup stash, and revert in case of errors
60 * @param {boolean} [options.verbose] - Show task output even when tasks succeed; by default only failed output is shown
61 * @param {Logger} logger
62 * @returns {Promise}
63 */
64export const runAll = async (
65 {
66 allowEmpty = false,
67 concurrent = true,
68 configObject,
69 configPath,
70 cwd,
71 debug = false,
72 maxArgLength,
73 quiet = false,
74 relative = false,
75 shell = false,
76 stash = true,
77 verbose = false,
78 },
79 logger = console
80) => {
81 debugLog('Running all linter scripts...')
82
83 // Resolve relative CWD option
84 const hasExplicitCwd = !!cwd
85 cwd = hasExplicitCwd ? path.resolve(cwd) : process.cwd()
86 debugLog('Using working directory `%s`', cwd)
87
88 const ctx = getInitialState({ quiet })
89
90 const { gitDir, gitConfigDir } = await resolveGitRepo(cwd)
91 if (!gitDir) {
92 if (!quiet) ctx.output.push(NOT_GIT_REPO)
93 ctx.errors.add(GitRepoError)
94 throw createError(ctx)
95 }
96
97 // Test whether we have any commits or not.
98 // Stashing must be disabled with no initial commit.
99 const hasInitialCommit = await execGit(['log', '-1'], { cwd: gitDir })
100 .then(() => true)
101 .catch(() => false)
102
103 // Lint-staged should create a backup stash only when there's an initial commit
104 ctx.shouldBackup = hasInitialCommit && stash
105 if (!ctx.shouldBackup) {
106 logger.warn(skippingBackup(hasInitialCommit))
107 }
108
109 const files = await getStagedFiles({ cwd: gitDir })
110 if (!files) {
111 if (!quiet) ctx.output.push(FAILED_GET_STAGED_FILES)
112 ctx.errors.add(GetStagedFilesError)
113 throw createError(ctx, GetStagedFilesError)
114 }
115 debugLog('Loaded list of staged files in git:\n%O', files)
116
117 // If there are no files avoid executing any lint-staged logic
118 if (files.length === 0) {
119 if (!quiet) ctx.output.push(NO_STAGED_FILES)
120 return ctx
121 }
122
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
129 // Throw if no configurations were found
130 if (numberOfConfigs === 0) {
131 ctx.errors.add(ConfigNotFoundError)
132 throw createError(ctx, ConfigNotFoundError)
133 }
134
135 debugLog('Found %d configs:\n%O', numberOfConfigs, foundConfigs)
136
137 const hasMultipleConfigs = numberOfConfigs > 1
138
139 // lint-staged 10 will automatically add modifications to index
140 // Warn user when their command includes `git add`
141 let hasDeprecatedGitAdd = false
142
143 const listrOptions = {
144 ctx,
145 exitOnError: false,
146 registerSignalListeners: false,
147 ...getRenderer({ debug, quiet }),
148 }
149
150 const listrTasks = []
151
152 // Set of all staged files that matched a task glob. Values in a set are unique.
153 const matchedFiles = new Set()
154
155 for (const [configPath, { config, files }] of Object.entries(configGroups)) {
156 const relativeConfig = normalize(path.relative(cwd, configPath))
157 const stagedFileChunks = chunkFiles({ baseDir: gitDir, files, maxArgLength, relative })
158
159 // Use actual cwd if it's specified, or there's only a single config file.
160 // Otherwise use the directory of the config file for each config group,
161 // to make sure tasks are separated from each other.
162 const groupCwd = hasMultipleConfigs && !hasExplicitCwd ? path.dirname(configPath) : cwd
163
164 const chunkCount = stagedFileChunks.length
165 if (chunkCount > 1) {
166 debugLog('Chunked staged files from `%s` into %d part', configPath, chunkCount)
167 }
168
169 for (const [index, files] of stagedFileChunks.entries()) {
170 const chunkListrTasks = await Promise.all(
171 generateTasks({ config, cwd: groupCwd, files, relative }).map((task) =>
172 makeCmdTasks({
173 commands: task.commands,
174 cwd: groupCwd,
175 files: task.fileList,
176 gitDir,
177 renderer: listrOptions.renderer,
178 shell,
179 verbose,
180 }).then((subTasks) => {
181 // Add files from task to match set
182 task.fileList.forEach((file) => {
183 // Make sure relative files are normalized to the
184 // group cwd, because other there might be identical
185 // relative filenames in the entire set.
186 const normalizedFile = path.isAbsolute(file)
187 ? file
188 : normalize(path.join(groupCwd, file))
189
190 matchedFiles.add(normalizedFile)
191 })
192
193 hasDeprecatedGitAdd =
194 hasDeprecatedGitAdd || subTasks.some((subTask) => subTask.command === 'git add')
195
196 const fileCount = task.fileList.length
197
198 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 }),
208 skip: () => {
209 // Skip task when no files matched
210 if (fileCount === 0) {
211 return `${task.pattern}${dim(' — no files')}`
212 }
213 return false
214 },
215 }
216 })
217 )
218 )
219
220 listrTasks.push({
221 title:
222 `${relativeConfig}${dim(` — ${files.length} ${files.length > 1 ? 'files' : 'file'}`)}` +
223 (chunkCount > 1 ? dim(` (chunk ${index + 1}/${chunkCount})...`) : ''),
224 task: () => new Listr(chunkListrTasks, { ...listrOptions, concurrent, exitOnError: true }),
225 skip: () => {
226 // Skip if the first step (backup) failed
227 if (ctx.errors.has(GitError)) return SKIPPED_GIT_ERROR
228 // Skip chunk when no every task is skipped (due to no matches)
229 if (chunkListrTasks.every((task) => task.skip())) {
230 return `${relativeConfig}${dim(' — no tasks to run')}`
231 }
232 return false
233 },
234 })
235 }
236 }
237
238 if (hasDeprecatedGitAdd) {
239 logger.warn(DEPRECATED_GIT_ADD)
240 }
241
242 // If all of the configured tasks should be skipped
243 // avoid executing any lint-staged logic
244 if (listrTasks.every((task) => task.skip())) {
245 if (!quiet) ctx.output.push(NO_TASKS)
246 return ctx
247 }
248
249 // Chunk matched files for better Windows compatibility
250 const matchedFileChunks = chunkFiles({
251 // matched files are relative to `cwd`, not `gitDir`, when `relative` is used
252 baseDir: cwd,
253 files: Array.from(matchedFiles),
254 maxArgLength,
255 relative: false,
256 })
257
258 const git = new GitWorkflow({ allowEmpty, gitConfigDir, gitDir, matchedFileChunks })
259
260 const runner = new Listr(
261 [
262 {
263 title: 'Preparing lint-staged...',
264 task: (ctx) => git.prepare(ctx),
265 },
266 {
267 title: 'Hiding unstaged changes to partially staged files...',
268 task: (ctx) => git.hideUnstagedChanges(ctx),
269 enabled: hasPartiallyStagedFiles,
270 },
271 {
272 title: `Running tasks for staged files...`,
273 task: () => new Listr(listrTasks, { ...listrOptions, concurrent }),
274 skip: () => listrTasks.every((task) => task.skip()),
275 },
276 {
277 title: 'Applying modifications from tasks...',
278 task: (ctx) => git.applyModifications(ctx),
279 skip: applyModificationsSkipped,
280 },
281 {
282 title: 'Restoring unstaged changes to partially staged files...',
283 task: (ctx) => git.restoreUnstagedChanges(ctx),
284 enabled: hasPartiallyStagedFiles,
285 skip: restoreUnstagedChangesSkipped,
286 },
287 {
288 title: 'Reverting to original state because of errors...',
289 task: (ctx) => git.restoreOriginalState(ctx),
290 enabled: restoreOriginalStateEnabled,
291 skip: restoreOriginalStateSkipped,
292 },
293 {
294 title: 'Cleaning up temporary files...',
295 task: (ctx) => git.cleanup(ctx),
296 enabled: cleanupEnabled,
297 skip: cleanupSkipped,
298 },
299 ],
300 listrOptions
301 )
302
303 await runner.run()
304
305 if (ctx.errors.size > 0) {
306 throw createError(ctx)
307 }
308
309 return ctx
310}