UNPKG

10.6 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 nonTTYRenderer: 'verbose',
147 registerSignalListeners: false,
148 ...getRenderer({ debug, quiet }),
149 }
150
151 const listrTasks = []
152
153 // Set of all staged files that matched a task glob. Values in a set are unique.
154 const matchedFiles = new Set()
155
156 for (const [configPath, { config, files }] of Object.entries(configGroups)) {
157 const relativeConfig = normalize(path.relative(cwd, configPath))
158 const stagedFileChunks = chunkFiles({ baseDir: gitDir, files, maxArgLength, relative })
159
160 // Use actual cwd if it's specified, or there's only a single config file.
161 // Otherwise use the directory of the config file for each config group,
162 // to make sure tasks are separated from each other.
163 const groupCwd = hasMultipleConfigs && !hasExplicitCwd ? path.dirname(configPath) : cwd
164
165 const chunkCount = stagedFileChunks.length
166 if (chunkCount > 1) {
167 debugLog('Chunked staged files from `%s` into %d part', configPath, chunkCount)
168 }
169
170 for (const [index, files] of stagedFileChunks.entries()) {
171 const chunkListrTasks = await Promise.all(
172 generateTasks({ config, cwd: groupCwd, files, relative }).map((task) =>
173 makeCmdTasks({
174 commands: task.commands,
175 cwd: groupCwd,
176 files: task.fileList,
177 gitDir,
178 renderer: listrOptions.renderer,
179 shell,
180 verbose,
181 }).then((subTasks) => {
182 // Add files from task to match set
183 task.fileList.forEach((file) => {
184 // Make sure relative files are normalized to the
185 // group cwd, because other there might be identical
186 // relative filenames in the entire set.
187 const normalizedFile = path.isAbsolute(file)
188 ? file
189 : normalize(path.join(groupCwd, file))
190
191 matchedFiles.add(normalizedFile)
192 })
193
194 hasDeprecatedGitAdd =
195 hasDeprecatedGitAdd || subTasks.some((subTask) => subTask.command === 'git add')
196
197 const fileCount = task.fileList.length
198
199 return {
200 title: `${task.pattern}${dim(` — ${fileCount} ${fileCount > 1 ? 'files' : 'file'}`)}`,
201 task: async () =>
202 new Listr(subTasks, {
203 // In sub-tasks we don't want to run concurrently
204 // and we want to abort on errors
205 ...listrOptions,
206 concurrent: false,
207 exitOnError: true,
208 }),
209 skip: () => {
210 // Skip task when no files matched
211 if (fileCount === 0) {
212 return `${task.pattern}${dim(' — no files')}`
213 }
214 return false
215 },
216 }
217 })
218 )
219 )
220
221 listrTasks.push({
222 title:
223 `${relativeConfig}${dim(` — ${files.length} ${files.length > 1 ? 'files' : 'file'}`)}` +
224 (chunkCount > 1 ? dim(` (chunk ${index + 1}/${chunkCount})...`) : ''),
225 task: () => new Listr(chunkListrTasks, { ...listrOptions, concurrent, exitOnError: true }),
226 skip: () => {
227 // Skip if the first step (backup) failed
228 if (ctx.errors.has(GitError)) return SKIPPED_GIT_ERROR
229 // Skip chunk when no every task is skipped (due to no matches)
230 if (chunkListrTasks.every((task) => task.skip())) {
231 return `${relativeConfig}${dim(' — no tasks to run')}`
232 }
233 return false
234 },
235 })
236 }
237 }
238
239 if (hasDeprecatedGitAdd) {
240 logger.warn(DEPRECATED_GIT_ADD)
241 }
242
243 // If all of the configured tasks should be skipped
244 // avoid executing any lint-staged logic
245 if (listrTasks.every((task) => task.skip())) {
246 if (!quiet) ctx.output.push(NO_TASKS)
247 return ctx
248 }
249
250 // Chunk matched files for better Windows compatibility
251 const matchedFileChunks = chunkFiles({
252 // matched files are relative to `cwd`, not `gitDir`, when `relative` is used
253 baseDir: cwd,
254 files: Array.from(matchedFiles),
255 maxArgLength,
256 relative: false,
257 })
258
259 const git = new GitWorkflow({ allowEmpty, gitConfigDir, gitDir, matchedFileChunks })
260
261 const runner = new Listr(
262 [
263 {
264 title: 'Preparing lint-staged...',
265 task: (ctx) => git.prepare(ctx),
266 },
267 {
268 title: 'Hiding unstaged changes to partially staged files...',
269 task: (ctx) => git.hideUnstagedChanges(ctx),
270 enabled: hasPartiallyStagedFiles,
271 },
272 {
273 title: `Running tasks for staged files...`,
274 task: () => new Listr(listrTasks, { ...listrOptions, concurrent }),
275 skip: () => listrTasks.every((task) => task.skip()),
276 },
277 {
278 title: 'Applying modifications from tasks...',
279 task: (ctx) => git.applyModifications(ctx),
280 skip: applyModificationsSkipped,
281 },
282 {
283 title: 'Restoring unstaged changes to partially staged files...',
284 task: (ctx) => git.restoreUnstagedChanges(ctx),
285 enabled: hasPartiallyStagedFiles,
286 skip: restoreUnstagedChangesSkipped,
287 },
288 {
289 title: 'Reverting to original state because of errors...',
290 task: (ctx) => git.restoreOriginalState(ctx),
291 enabled: restoreOriginalStateEnabled,
292 skip: restoreOriginalStateSkipped,
293 },
294 {
295 title: 'Cleaning up temporary files...',
296 task: (ctx) => git.cleanup(ctx),
297 enabled: cleanupEnabled,
298 skip: cleanupSkipped,
299 },
300 ],
301 listrOptions
302 )
303
304 await runner.run()
305
306 if (ctx.errors.size > 0) {
307 throw createError(ctx)
308 }
309
310 return ctx
311}