// This file represents an abstraction on the metro TerminalReporter. // We use this abstraction to safely extend the TerminalReporter for our own custom logging. import chalk from 'chalk'; import UpstreamTerminalReporter from 'metro/src/lib/TerminalReporter'; import { Terminal } from 'metro-core'; import util from 'util'; import { BundleDetails, TerminalReportableEvent, TerminalReporterInterface, } from './TerminalReporter.types'; import { stripAnsi } from '../../../utils/ansi'; /** * A standard way to log a warning to the terminal. This should not be called * from some arbitrary Metro logic, only from the reporters. Instead of * calling this, add a new type of ReportableEvent instead, and implement a * proper handler in the reporter(s). */ export function logWarning(terminal: Terminal, format: string, ...args: any[]): void { const str = util.format(format, ...args); terminal.log('%s: %s', chalk.yellow('warning'), str); } /** * Similar to `logWarning`, but for messages that require the user to act. */ export function logError(terminal: Terminal, format: string, ...args: any[]): void { terminal.log( '%s: %s', chalk.red('error'), // Syntax errors may have colors applied for displaying code frames // in various places outside of where Metro is currently running. // If the current terminal does not support color, we'll strip the colors // here. util.format(chalk.supportsColor ? format : stripAnsi(format), ...args) ); } const XTerminalReporter = UpstreamTerminalReporter as unknown as TerminalReporterInterface; /** Extended TerminalReporter class but with proper types and extra functionality to avoid using the `_log` method directly in subclasses. */ export class TerminalReporter extends XTerminalReporter implements TerminalReporterInterface { /** * A cache of { [buildID]: BundleDetails } which can be used to * add more contextual logs. BundleDetails is currently only sent with `bundle_build_started` * so we need to cache the details in order to print the platform info with other event types. */ _bundleDetails: Map = new Map(); /** Keep track of how long a bundle takes to complete */ _bundleTimers: Map = new Map(); _log(event: TerminalReportableEvent): void { switch (event.type) { case 'transform_cache_reset': return this.transformCacheReset(); case 'dep_graph_loading': return this.dependencyGraphLoading(event.hasReducedPerformance); case 'client_log': if (this.shouldFilterClientLog(event)) { return; } break; } return super._log(event); } /** Gives subclasses an easy interface for filtering out logs. Return `true` to skip. */ shouldFilterClientLog(event: { type: 'client_log'; level: 'trace' | 'info' | 'warn' | 'log' | 'group' | 'groupCollapsed' | 'groupEnd' | 'debug'; data: unknown[]; }): boolean { return false; } /** Cache has been reset. */ transformCacheReset(): void {} /** One of the first logs that will be printed. */ dependencyGraphLoading(hasReducedPerformance: boolean): void {} /** * Custom log event representing the end of the bundling. * * @param event event object. * @param duration duration of the build in milliseconds. */ bundleBuildEnded(event: TerminalReportableEvent, duration: number): void {} /** * This function is exclusively concerned with updating the internal state. * No logging or status updates should be done at this point. */ _updateState( event: TerminalReportableEvent & { bundleDetails?: BundleDetails; buildID?: string } ) { // Append the buildID to the bundleDetails. if (event.bundleDetails) { event.bundleDetails.buildID = event.buildID; } super._updateState(event); switch (event.type) { case 'bundle_build_done': case 'bundle_build_failed': { const startTime = this._bundleTimers.get(event.buildID); // Observed a bug in Metro where the `bundle_build_done` is invoked twice during a static bundle // i.e. `expo export`. if (startTime == null) { break; } this.bundleBuildEnded(event, startTime ? Date.now() - startTime : 0); this._bundleTimers.delete(event.buildID); break; } case 'bundle_build_started': this._bundleDetails.set(event.buildID, event.bundleDetails); this._bundleTimers.set(event.buildID, Date.now()); break; } } }