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