1// This file represents an abstraction on the metro TerminalReporter.
2// We use this abstraction to safely extend the TerminalReporter for our own custom logging.
3import chalk from 'chalk';
4import { Terminal } from 'metro-core';
5import UpstreamTerminalReporter from 'metro/src/lib/TerminalReporter';
6import util from 'util';
7
8import { stripAnsi } from '../../../utils/ansi';
9import {
10  BundleDetails,
11  TerminalReportableEvent,
12  TerminalReporterInterface,
13} from './TerminalReporter.types';
14
15/**
16 * A standard way to log a warning to the terminal. This should not be called
17 * from some arbitrary Metro logic, only from the reporters. Instead of
18 * calling this, add a new type of ReportableEvent instead, and implement a
19 * proper handler in the reporter(s).
20 */
21export function logWarning(terminal: Terminal, format: string, ...args: any[]): void {
22  const str = util.format(format, ...args);
23  terminal.log('%s: %s', chalk.yellow('warning'), str);
24}
25
26/**
27 * Similar to `logWarning`, but for messages that require the user to act.
28 */
29export function logError(terminal: Terminal, format: string, ...args: any[]): void {
30  terminal.log(
31    '%s: %s',
32    chalk.red('error'),
33    // Syntax errors may have colors applied for displaying code frames
34    // in various places outside of where Metro is currently running.
35    // If the current terminal does not support color, we'll strip the colors
36    // here.
37    util.format(chalk.supportsColor ? format : stripAnsi(format), ...args)
38  );
39}
40
41const XTerminalReporter = UpstreamTerminalReporter as unknown as TerminalReporterInterface;
42
43/** Extended TerminalReporter class but with proper types and extra functionality to avoid using the `_log` method directly in subclasses. */
44export class TerminalReporter extends XTerminalReporter implements TerminalReporterInterface {
45  /**
46   * A cache of { [buildID]: BundleDetails } which can be used to
47   * add more contextual logs. BundleDetails is currently only sent with `bundle_build_started`
48   * so we need to cache the details in order to print the platform info with other event types.
49   */
50  _bundleDetails: Map<string, BundleDetails> = new Map();
51
52  /** Keep track of how long a bundle takes to complete */
53  _bundleTimers: Map<string, number> = new Map();
54
55  _log(event: TerminalReportableEvent): void {
56    switch (event.type) {
57      case 'transform_cache_reset':
58        return this.transformCacheReset();
59      case 'dep_graph_loading':
60        return this.dependencyGraphLoading(event.hasReducedPerformance);
61      case 'client_log':
62        if (this.shouldFilterClientLog(event)) {
63          return;
64        }
65        break;
66    }
67    return super._log(event);
68  }
69
70  /** Gives subclasses an easy interface for filtering out logs. Return `true` to skip. */
71  shouldFilterClientLog(event: {
72    type: 'client_log';
73    level: 'trace' | 'info' | 'warn' | 'log' | 'group' | 'groupCollapsed' | 'groupEnd' | 'debug';
74    data: unknown[];
75  }): boolean {
76    return false;
77  }
78
79  /** Cache has been reset. */
80  transformCacheReset(): void {}
81
82  /** One of the first logs that will be printed. */
83  dependencyGraphLoading(hasReducedPerformance: boolean): void {}
84
85  /**
86   * Custom log event representing the end of the bundling.
87   *
88   * @param event event object.
89   * @param duration duration of the build in milliseconds.
90   */
91  bundleBuildEnded(event: TerminalReportableEvent, duration: number): void {}
92
93  /**
94   * This function is exclusively concerned with updating the internal state.
95   * No logging or status updates should be done at this point.
96   */
97  _updateState(
98    event: TerminalReportableEvent & { bundleDetails?: BundleDetails; buildID?: string }
99  ) {
100    // Append the buildID to the bundleDetails.
101    if (event.bundleDetails) {
102      event.bundleDetails.buildID = event.buildID;
103    }
104
105    super._updateState(event);
106    switch (event.type) {
107      case 'bundle_build_done':
108      case 'bundle_build_failed': {
109        const startTime = this._bundleTimers.get(event.buildID);
110        // Observed a bug in Metro where the `bundle_build_done` is invoked twice during a static bundle
111        // i.e. `expo export`.
112        if (startTime == null) {
113          break;
114        }
115
116        this.bundleBuildEnded(event, startTime ? Date.now() - startTime : 0);
117        this._bundleTimers.delete(event.buildID);
118        break;
119      }
120      case 'bundle_build_started':
121        this._bundleDetails.set(event.buildID, event.bundleDetails);
122        this._bundleTimers.set(event.buildID, Date.now());
123        break;
124    }
125  }
126}
127