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