import type {Bud} from '@roots/bud-framework' import type { MultiStats, StatsCompilation, StatsError, } from '@roots/bud-framework/config' import type {Dashboard as BudDashboard} from '@roots/bud-framework/services' import {stdin} from 'node:process' import {makeErrorFormatter} from '@roots/bud-dashboard/helpers/formatErrors' import {Service} from '@roots/bud-framework/service' import {bind} from '@roots/bud-support/decorators/bind' import {Box, Text} from '@roots/bud-support/ink' import {render} from '@roots/bud-support/ink/instance' import isUndefined from '@roots/bud-support/isUndefined' import {Application, TeletypeApplication} from './application.js' type Compilations = Array> /** * {@link BudDashboard} */ export class Dashboard extends Service implements BudDashboard { /** * {@link BudDashboard.formatStatsErrors} * * @param errors - {@link StatsError[]} */ public declare formatStatsErrors: ( errors?: StatsError[] | undefined, ) => StatsError[] | undefined /** * Class constructor * * @param app - {@link Bud} instance */ public constructor(app: () => Bud) { super(app) this.formatStatsErrors = makeErrorFormatter(this.app) } /** * {@link BudDashboard.render} * * @param stats - {@link MultiStats} * @param error - {@link Error} */ @bind public render( stats?: MultiStats | StatsCompilation, error?: Error, ): unknown { /** * Do not render if silent mode is enabled */ if (this.app.context.silent) return /** * Do not render if no stats received */ if (!stats) return /** * Render basic output if `--dashboard` flag is false */ if (this.app.context.dashboard === false) { const stringStats = stats.toString({ preset: `minimal`, }) return render({stringStats}) } const data: StatsCompilation = this.app.compiler.stats ?? stats.toJson({ all: false, children: { all: false, assets: true, cached: true, cachedAssets: true, cachedModules: true, entrypoints: true, errorDetails: false, errors: true, errorsCount: true, hash: true, modules: true, name: true, outputPath: true, timings: true, warnings: true, warningsCount: true, }, }) /** * Get compilations * * @remarks * If the stats object has children, we can assume that * we are dealing with a multi-compiler setup. In this * case, we want to flatten the children array. */ const getCompilations = (): Compilations => { if (!data) return [] if (!data.children?.length) return [data] return data.children.flat() } /** * Cleanly format errors and warnings for each compilation */ const compilations = getCompilations().map(compilation => ({ ...compilation, errors: this.formatStatsErrors(compilation?.errors), warnings: this.formatStatsErrors(compilation?.warnings), })) /** * `--dashboard.assets` flag value */ const assets = !isUndefined(this.app.context.dashboard) ? this.app.context.dashboard.assets : true /** * `--dashboard.compact` flag value */ const compact = !isUndefined(this.app.context.dashboard) ? this.app.context.dashboard.compact : compilations.length > 2 /** * `--dashboard.entrypoints flag value` */ const entrypoints = !isUndefined( this.app.context.dashboard?.entrypoints, ) ? this.app.context.dashboard?.entrypoints : true /** * `--dashboard.server` flag value */ const server = !isUndefined(this.app.context.dashboard?.server) ? this.app.context.dashboard?.server : true /** * If the application is running in development mode, and * the terminal is a TTY, we want to render the teletype * application. Otherwise, we want to render the normal * application. */ const App = stdin.isTTY && this.app.isDevelopment ? TeletypeApplication : Application render( this.app.compiler?.instance?.compilers?.map(c => c.close(cb)) } compact={compact} compilations={compilations} debug={this.app.context.debug} devUrl={this.app.server?.url} displayAssets={assets} displayEntrypoints={entrypoints} displayServerInfo={this.app.mode === `development` && server} error={error} mode={this.app.mode} notifier={this.app.notifier} proxy={this.app.server?.enabledMiddleware?.[`proxy`]} proxyUrl={this.app.server?.proxyUrl} publicDevUrl={this.app.server?.publicUrl} publicProxyUrl={this.app.server?.publicProxyUrl} /> , ) } }