@plugjs/plug
Version:
PlugJS Build System ===================
239 lines (194 loc) • 7.36 kB
text/typescript
import { formatWithOptions } from 'node:util'
import { BuildFailure } from '../asserts'
import { currentContext } from '../async'
import { stripAnsi } from '../utils/ansi'
import { $gry } from './colors'
import { emit } from './emit'
import { DEBUG, ERROR, INFO, NOTICE, TRACE, WARN } from './levels'
import { logOptions } from './options'
import { ReportImpl } from './report'
import type { LogEmitter, LogEmitterOptions } from './emit'
import type { LogLevel } from './levels'
import type { Report } from './report'
/* ========================================================================== */
/* Initial value of log colors, and subscribe to changes */
let _level = logOptions.level
logOptions.on('changed', ({ level }) => {
_level = level
})
/* ========================================================================== *
* LOGGER *
* ========================================================================== */
/** The basic interface giving access to log facilities. */
export interface Log {
/** Log a `TRACE` message */
trace(...args: [ any, ...any ]): void
/** Log a `DEBUG` message */
debug(...args: [ any, ...any ]): void
/** Log an `INFO` message */
info(...args: [ any, ...any ]): void
/** Log a `NOTICE` message */
notice(...args: [ any, ...any ]): void
/** Log a `WARNING` message */
warn(...args: [ any, ...any ]): void
/** Log an `ERROR` message */
error(...args: [ any, ...any ]): void
/** Log an `ERROR` message and fail the build */
fail(...args: [ any, ...any ]): never
}
/** A {@link Logger} extends the basic {@link Log} adding some state. */
export interface Logger extends Log {
/** The current level for logging. */
level: LogLevel,
/** The current indent level for logging. */
indent: number,
/** Enter a sub-level of logging, increasing indent */
enter(): void
/** Enter a sub-level of logging, increasing indent */
enter(evel: LogLevel, message: string): void
/** Leave a sub-level of logging, decreasing indent */
leave(): void
/** Leave a sub-level of logging, decreasing indent */
leave(level: LogLevel, message: string): void
/** Create a {@link Report} associated with this instance */
report(title: string): Report
}
/** Return a {@link Logger} associated with the specified task name. */
export function getLogger(task?: string, indent?: number): Logger {
const context = currentContext()
const taskName = task === undefined ? (context?.taskName || '') : task
const indentLevel = indent === undefined ? (context?.log.indent || 0) : 0
return new LoggerImpl(taskName, emit, indentLevel)
}
/* ========================================================================== */
/** Weak set of already logged build failures */
const _loggedFailures = new WeakSet<BuildFailure>()
/** Default implementation of the {@link Logger} interface. */
class LoggerImpl implements Logger {
private readonly _stack: { level: LogLevel, message: string, indent: number }[] = []
public level = _level
constructor(
private readonly _task: string,
private readonly _emitter: LogEmitter,
public indent: number,
) {}
private _emit(level: LogLevel, args: [ any, ...any ], taskName = this._task): void {
if (this.level > level) return
// The `BuildFailure` is a bit special case
const params = args.filter((arg) => {
if (arg instanceof BuildFailure) {
// Filter out any previously logged build failure and mark
if (_loggedFailures.has(arg)) return false
_loggedFailures.add(arg)
// If the build failure has any root cause, log those
arg.errors?.forEach((error) => this._emit(level, [ error ]))
// Log this only if it has a message
if (! arg.message) return false
// Log the full error (with stack) if the _default_ level is DEBUG
if (_level < INFO) return true
// Log only the message in other cases
this._emit(level, [ arg.message ])
return false
} else {
return true
}
})
// If there's nothing left to log, then we're done
if (params.length === 0) return
// Prepare our options for logging
const options = { level, taskName, indent: this.indent }
// Dump any existing stack entry
if (this._stack.length) {
for (const { message, ...extras } of this._stack) {
this._emitter({ ...options, ...extras }, [ message ])
}
this._stack.splice(0)
}
// Emit our log lines and return
this._emitter(options, params)
}
trace(...args: [ any, ...any ]): void {
this._emit(TRACE, args)
}
debug(...args: [ any, ...any ]): void {
this._emit(DEBUG, args)
}
info(...args: [ any, ...any ]): void {
this._emit(INFO, args)
}
notice(...args: [ any, ...any ]): void {
this._emit(NOTICE, args)
}
warn(...args: [ any, ...any ]): void {
this._emit(WARN, args)
}
error(...args: [ any, ...any ]): void {
this._emit(ERROR, args)
}
fail(...args: [ any, ...any ]): never {
this._emit(ERROR, args)
throw BuildFailure.fail()
}
enter(): void
enter(level: LogLevel, message: string): void
enter(...args: [] | [ level: LogLevel, message: string ]): void {
if (args.length) {
const [ level, message ] = args
this._stack.push({ level, message, indent: this.indent })
}
this.indent ++
}
leave(): void
leave(level: LogLevel, message: string): void
leave(...args: [] | [ level: LogLevel, message: string ]): void {
this._stack.pop()
this.indent --
if (this.indent < 0) this.indent = 0
if (args.length) {
const [ level, message ] = args
this._emit(level, [ message ])
}
}
report(title: string): Report {
const emitter: LogEmitter = (options: LogEmitterOptions, args: any) => {
if (this._stack.length) {
for (const { message, ...extras } of this._stack) {
this._emitter({ ...options, ...extras }, [ message ])
}
this._stack.splice(0)
}
let { indent = 0, prefix = '' } = options
prefix = this.indent ? $gry('| ') + prefix : prefix
indent += this.indent
this._emitter({ ...options, indent, prefix }, args)
}
return new ReportImpl(title, this._task, emitter)
}
}
/* ========================================================================== */
/** A test logger, writing to a buffer always _without_ colors/indent */
export class TestLogger extends LoggerImpl {
private _lines: string[] = []
constructor() {
super('', (options: LogEmitterOptions, args: any[]): void => {
const { prefix = '', indent = 0 } = options
const linePrefix = ''.padStart(indent * 2) + prefix
/* Now for the normal logging of all our parameters */
formatWithOptions({ colors: false, breakLength: 120 }, ...args)
.split('\n').forEach((line) => {
const stripped = stripAnsi(line)
this._lines.push(`${linePrefix}${stripped}`)
})
}, 0)
}
/** Return the _current_ buffer for this instance */
get buffer(): string {
return this._lines.join('\n')
}
/** Reset the buffer and return any previously buffered text */
reset(): string {
const buffer = this.buffer
this._lines = []
return buffer
}
}