Currently if formatting for a file fails due a formatter error, the `ng-dev` tool reports that formatting failed, but no actual error (or involved file) is printed out. This commit prints out the failed files with their error message. PR Close #42202
		
			
				
	
	
		
			125 lines
		
	
	
		
			4.8 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			125 lines
		
	
	
		
			4.8 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
/**
 | 
						|
 * @license
 | 
						|
 * Copyright Google LLC All Rights Reserved.
 | 
						|
 *
 | 
						|
 * Use of this source code is governed by an MIT-style license that can be
 | 
						|
 * found in the LICENSE file at https://angular.io/license
 | 
						|
 */
 | 
						|
 | 
						|
import {Bar} from 'cli-progress';
 | 
						|
import * as multimatch from 'multimatch';
 | 
						|
import {cpus} from 'os';
 | 
						|
import {exec} from 'shelljs';
 | 
						|
 | 
						|
import {info} from '../utils/console';
 | 
						|
 | 
						|
import {Formatter, FormatterAction, getActiveFormatters} from './formatters/index';
 | 
						|
 | 
						|
const AVAILABLE_THREADS = Math.max(cpus().length - 1, 1);
 | 
						|
 | 
						|
/** Interface describing a failure occurred during formatting of a file. */
 | 
						|
export interface FormatFailure {
 | 
						|
  /** Path to the file that failed. */
 | 
						|
  filePath: string;
 | 
						|
  /** Error message reported by the formatter. */
 | 
						|
  message: string;
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Run the provided commands in parallel for each provided file.
 | 
						|
 *
 | 
						|
 * Running the formatter is split across (number of available cpu threads - 1) processess.
 | 
						|
 * The task is done in multiple processess to speed up the overall time of the task, as running
 | 
						|
 * across entire repositories takes a large amount of time.
 | 
						|
 * As a data point for illustration, using 8 process rather than 1 cut the execution
 | 
						|
 * time from 276 seconds to 39 seconds for the same 2700 files.
 | 
						|
 *
 | 
						|
 * A promise is returned, completed when the command has completed running for each file.
 | 
						|
 * The promise resolves with a list of failures, or `false` if no formatters have matched.
 | 
						|
 */
 | 
						|
export function runFormatterInParallel(allFiles: string[], action: FormatterAction) {
 | 
						|
  return new Promise<false|FormatFailure[]>((resolve) => {
 | 
						|
    const formatters = getActiveFormatters();
 | 
						|
    const failures: FormatFailure[] = [];
 | 
						|
    const pendingCommands: {formatter: Formatter, file: string}[] = [];
 | 
						|
 | 
						|
    for (const formatter of formatters) {
 | 
						|
      pendingCommands.push(
 | 
						|
          ...multimatch.call(undefined, allFiles, formatter.getFileMatcher(), {dot: true})
 | 
						|
              .map(file => ({formatter, file})));
 | 
						|
    }
 | 
						|
 | 
						|
    // If no commands are generated, resolve the promise as `false` as no files
 | 
						|
    // were run against the any formatters.
 | 
						|
    if (pendingCommands.length === 0) {
 | 
						|
      return resolve(false);
 | 
						|
    }
 | 
						|
 | 
						|
    switch (action) {
 | 
						|
      case 'format':
 | 
						|
        info(`Formatting ${pendingCommands.length} file(s)`);
 | 
						|
        break;
 | 
						|
      case 'check':
 | 
						|
        info(`Checking format of ${pendingCommands.length} file(s)`);
 | 
						|
        break;
 | 
						|
      default:
 | 
						|
        throw Error(`Invalid format action "${action}": allowed actions are "format" and "check"`);
 | 
						|
    }
 | 
						|
 | 
						|
    // The progress bar instance to use for progress tracking.
 | 
						|
    const progressBar =
 | 
						|
        new Bar({format: `[{bar}] ETA: {eta}s | {value}/{total} files`, clearOnComplete: true});
 | 
						|
    // A local copy of the files to run the command on.
 | 
						|
    // An array to represent the current usage state of each of the threads for parallelization.
 | 
						|
    const threads = new Array<boolean>(AVAILABLE_THREADS).fill(false);
 | 
						|
 | 
						|
    // Recursively run the command on the next available file from the list using the provided
 | 
						|
    // thread.
 | 
						|
    function runCommandInThread(thread: number) {
 | 
						|
      const nextCommand = pendingCommands.pop();
 | 
						|
      // If no file was pulled from the array, return as there are no more files to run against.
 | 
						|
      if (nextCommand === undefined) {
 | 
						|
        threads[thread] = false;
 | 
						|
        return;
 | 
						|
      }
 | 
						|
 | 
						|
      // Get the file and formatter for the next command.
 | 
						|
      const {file, formatter} = nextCommand;
 | 
						|
 | 
						|
      exec(
 | 
						|
          `${formatter.commandFor(action)} ${file}`,
 | 
						|
          {async: true, silent: true},
 | 
						|
          (code, stdout, stderr) => {
 | 
						|
            // Run the provided callback function.
 | 
						|
            const failed = formatter.callbackFor(action)(file, code, stdout, stderr);
 | 
						|
            if (failed) {
 | 
						|
              failures.push({filePath: file, message: stderr});
 | 
						|
            }
 | 
						|
            // Note in the progress bar another file being completed.
 | 
						|
            progressBar.increment(1);
 | 
						|
            // If more files exist in the list, run again to work on the next file,
 | 
						|
            // using the same slot.
 | 
						|
            if (pendingCommands.length) {
 | 
						|
              return runCommandInThread(thread);
 | 
						|
            }
 | 
						|
            // If not more files are available, mark the thread as unused.
 | 
						|
            threads[thread] = false;
 | 
						|
            // If all of the threads are false, as they are unused, mark the progress bar
 | 
						|
            // completed and resolve the promise.
 | 
						|
            if (threads.every(active => !active)) {
 | 
						|
              progressBar.stop();
 | 
						|
              resolve(failures);
 | 
						|
            }
 | 
						|
          },
 | 
						|
      );
 | 
						|
      // Mark the thread as in use as the command execution has been started.
 | 
						|
      threads[thread] = true;
 | 
						|
    }
 | 
						|
 | 
						|
    // Start the progress bar
 | 
						|
    progressBar.start(pendingCommands.length, 0);
 | 
						|
    // Start running the command on files from the least in each available thread.
 | 
						|
    threads.forEach((_, idx) => runCommandInThread(idx));
 | 
						|
  });
 | 
						|
}
 |