18d307f52SEvan Bacon// This file represents an abstraction on the metro TerminalReporter.
28d307f52SEvan Bacon// We use this abstraction to safely extend the TerminalReporter for our own custom logging.
38d307f52SEvan Baconimport chalk from 'chalk';
48d307f52SEvan Baconimport UpstreamTerminalReporter from 'metro/src/lib/TerminalReporter';
5*8a424bebSJames Ideimport { Terminal } from 'metro-core';
68d307f52SEvan Baconimport util from 'util';
78d307f52SEvan Bacon
88d307f52SEvan Baconimport {
98d307f52SEvan Bacon  BundleDetails,
108d307f52SEvan Bacon  TerminalReportableEvent,
118d307f52SEvan Bacon  TerminalReporterInterface,
128d307f52SEvan Bacon} from './TerminalReporter.types';
13*8a424bebSJames Ideimport { stripAnsi } from '../../../utils/ansi';
148d307f52SEvan Bacon
158d307f52SEvan Bacon/**
168d307f52SEvan Bacon * A standard way to log a warning to the terminal. This should not be called
178d307f52SEvan Bacon * from some arbitrary Metro logic, only from the reporters. Instead of
188d307f52SEvan Bacon * calling this, add a new type of ReportableEvent instead, and implement a
198d307f52SEvan Bacon * proper handler in the reporter(s).
208d307f52SEvan Bacon */
218d307f52SEvan Baconexport function logWarning(terminal: Terminal, format: string, ...args: any[]): void {
228d307f52SEvan Bacon  const str = util.format(format, ...args);
238d307f52SEvan Bacon  terminal.log('%s: %s', chalk.yellow('warning'), str);
248d307f52SEvan Bacon}
258d307f52SEvan Bacon
268d307f52SEvan Bacon/**
278d307f52SEvan Bacon * Similar to `logWarning`, but for messages that require the user to act.
288d307f52SEvan Bacon */
298d307f52SEvan Baconexport function logError(terminal: Terminal, format: string, ...args: any[]): void {
308d307f52SEvan Bacon  terminal.log(
318d307f52SEvan Bacon    '%s: %s',
328d307f52SEvan Bacon    chalk.red('error'),
338d307f52SEvan Bacon    // Syntax errors may have colors applied for displaying code frames
348d307f52SEvan Bacon    // in various places outside of where Metro is currently running.
358d307f52SEvan Bacon    // If the current terminal does not support color, we'll strip the colors
368d307f52SEvan Bacon    // here.
378d307f52SEvan Bacon    util.format(chalk.supportsColor ? format : stripAnsi(format), ...args)
388d307f52SEvan Bacon  );
398d307f52SEvan Bacon}
408d307f52SEvan Bacon
418d307f52SEvan Baconconst XTerminalReporter = UpstreamTerminalReporter as unknown as TerminalReporterInterface;
428d307f52SEvan Bacon
438d307f52SEvan Bacon/** Extended TerminalReporter class but with proper types and extra functionality to avoid using the `_log` method directly in subclasses. */
448d307f52SEvan Baconexport class TerminalReporter extends XTerminalReporter implements TerminalReporterInterface {
458d307f52SEvan Bacon  /**
468d307f52SEvan Bacon   * A cache of { [buildID]: BundleDetails } which can be used to
478d307f52SEvan Bacon   * add more contextual logs. BundleDetails is currently only sent with `bundle_build_started`
488d307f52SEvan Bacon   * so we need to cache the details in order to print the platform info with other event types.
498d307f52SEvan Bacon   */
508d307f52SEvan Bacon  _bundleDetails: Map<string, BundleDetails> = new Map();
518d307f52SEvan Bacon
528d307f52SEvan Bacon  /** Keep track of how long a bundle takes to complete */
538d307f52SEvan Bacon  _bundleTimers: Map<string, number> = new Map();
548d307f52SEvan Bacon
558d307f52SEvan Bacon  _log(event: TerminalReportableEvent): void {
568d307f52SEvan Bacon    switch (event.type) {
578d307f52SEvan Bacon      case 'transform_cache_reset':
588d307f52SEvan Bacon        return this.transformCacheReset();
598d307f52SEvan Bacon      case 'dep_graph_loading':
608d307f52SEvan Bacon        return this.dependencyGraphLoading(event.hasReducedPerformance);
618d307f52SEvan Bacon      case 'client_log':
628d307f52SEvan Bacon        if (this.shouldFilterClientLog(event)) {
638d307f52SEvan Bacon          return;
648d307f52SEvan Bacon        }
658d307f52SEvan Bacon        break;
668d307f52SEvan Bacon    }
678d307f52SEvan Bacon    return super._log(event);
688d307f52SEvan Bacon  }
698d307f52SEvan Bacon
708d307f52SEvan Bacon  /** Gives subclasses an easy interface for filtering out logs. Return `true` to skip. */
718d307f52SEvan Bacon  shouldFilterClientLog(event: {
728d307f52SEvan Bacon    type: 'client_log';
738d307f52SEvan Bacon    level: 'trace' | 'info' | 'warn' | 'log' | 'group' | 'groupCollapsed' | 'groupEnd' | 'debug';
748d307f52SEvan Bacon    data: unknown[];
758d307f52SEvan Bacon  }): boolean {
768d307f52SEvan Bacon    return false;
778d307f52SEvan Bacon  }
788d307f52SEvan Bacon
798d307f52SEvan Bacon  /** Cache has been reset. */
808d307f52SEvan Bacon  transformCacheReset(): void {}
818d307f52SEvan Bacon
828d307f52SEvan Bacon  /** One of the first logs that will be printed. */
838d307f52SEvan Bacon  dependencyGraphLoading(hasReducedPerformance: boolean): void {}
848d307f52SEvan Bacon
858d307f52SEvan Bacon  /**
868d307f52SEvan Bacon   * Custom log event representing the end of the bundling.
878d307f52SEvan Bacon   *
888d307f52SEvan Bacon   * @param event event object.
898d307f52SEvan Bacon   * @param duration duration of the build in milliseconds.
908d307f52SEvan Bacon   */
918d307f52SEvan Bacon  bundleBuildEnded(event: TerminalReportableEvent, duration: number): void {}
928d307f52SEvan Bacon
938d307f52SEvan Bacon  /**
948d307f52SEvan Bacon   * This function is exclusively concerned with updating the internal state.
958d307f52SEvan Bacon   * No logging or status updates should be done at this point.
968d307f52SEvan Bacon   */
978d307f52SEvan Bacon  _updateState(
988d307f52SEvan Bacon    event: TerminalReportableEvent & { bundleDetails?: BundleDetails; buildID?: string }
998d307f52SEvan Bacon  ) {
1008d307f52SEvan Bacon    // Append the buildID to the bundleDetails.
1018d307f52SEvan Bacon    if (event.bundleDetails) {
1028d307f52SEvan Bacon      event.bundleDetails.buildID = event.buildID;
1038d307f52SEvan Bacon    }
1048d307f52SEvan Bacon
1058d307f52SEvan Bacon    super._updateState(event);
1068d307f52SEvan Bacon    switch (event.type) {
1078d307f52SEvan Bacon      case 'bundle_build_done':
10829975bfdSEvan Bacon      case 'bundle_build_failed': {
10929975bfdSEvan Bacon        const startTime = this._bundleTimers.get(event.buildID);
11037d1352bSEvan Bacon        // Observed a bug in Metro where the `bundle_build_done` is invoked twice during a static bundle
11137d1352bSEvan Bacon        // i.e. `expo export`.
11237d1352bSEvan Bacon        if (startTime == null) {
11337d1352bSEvan Bacon          break;
11437d1352bSEvan Bacon        }
11537d1352bSEvan Bacon
11629975bfdSEvan Bacon        this.bundleBuildEnded(event, startTime ? Date.now() - startTime : 0);
1178d307f52SEvan Bacon        this._bundleTimers.delete(event.buildID);
1188d307f52SEvan Bacon        break;
11929975bfdSEvan Bacon      }
1208d307f52SEvan Bacon      case 'bundle_build_started':
1218d307f52SEvan Bacon        this._bundleDetails.set(event.buildID, event.bundleDetails);
1228d307f52SEvan Bacon        this._bundleTimers.set(event.buildID, Date.now());
1238d307f52SEvan Bacon        break;
1248d307f52SEvan Bacon    }
1258d307f52SEvan Bacon  }
1268d307f52SEvan Bacon}
127