@plugjs/plug
Version:
PlugJS Build System ===================
346 lines • 14.3 kB
JavaScript
import { BuildFailure } from "../asserts.js";
import { readFile } from "../fs.js";
import { $blu, $cyn, $gry, $plur, $red, $und, $wht, $ylw } from "./colors.js";
import { githubAnnotation } from "./github.js";
import { ERROR, NOTICE, WARN, logLevels } from "./levels.js";
import { logOptions } from "./options.js";
let _showSources = logOptions.showSources;
let _githubAnnotations = logOptions.githubAnnotations;
logOptions.on('changed', (options) => {
_showSources = options.showSources;
_githubAnnotations = options.githubAnnotations;
});
/* ========================================================================== *
* REPORT IMPLEMENTATION *
* ========================================================================== */
const nul = '\u2400'; // null, yep, as a character, always gets sorted last
export class ReportImpl {
_title;
_task;
_emitter;
_sources = new Map();
_annotations = new Map();
_records = new Map();
_noticeRecords = 0;
_warningRecords = 0;
_errorRecords = 0;
_noticeAnnotations = 0;
_warningAnnotations = 0;
_errorAnnotations = 0;
constructor(_title, _task, _emitter) {
this._title = _title;
this._task = _task;
this._emitter = _emitter;
}
get notices() {
return this._noticeRecords + this._noticeAnnotations;
}
get warnings() {
return this._warningRecords + this._warningAnnotations;
}
get errors() {
return this._errorRecords + this._errorAnnotations;
}
get noticeRecords() {
return this._noticeRecords;
}
get warningRecords() {
return this._warningRecords;
}
get errorRecords() {
return this._errorRecords;
}
get noticeAnnotations() {
return this._noticeAnnotations;
}
get warningAnnotations() {
return this._warningAnnotations;
}
get errorAnnotations() {
return this._errorAnnotations;
}
get records() {
return this._noticeRecords + this._warningRecords + this._errorRecords;
}
get annotations() {
return this._noticeAnnotations + this._warningAnnotations + this._errorAnnotations;
}
get empty() {
return !(this.records + this.annotations);
}
annotate(annotationLevel, file, note) {
if (note) {
const level = annotationLevel;
this._annotations.set(file, { level, note });
switch (level) {
case NOTICE:
this._noticeRecords++;
break;
case WARN:
this._warningRecords++;
break;
case ERROR:
this._errorRecords++;
break;
}
}
return this;
}
add(...records) {
for (const record of records) {
/* Normalize the basic entries in this message */
let messages = Array.isArray(record.message) ?
[...record.message.map((msg) => msg.split('\n')).flat(1)] :
record.message.split('\n');
messages = messages.filter((message) => !!message);
if (!messages.length) {
const options = { taskName: this._task, level: ERROR };
this._emitter(options, ['No message for report record']);
throw BuildFailure.fail();
}
const level = record.level;
const file = record.file;
const source = record.source || undefined;
const tags = record.tags ?
Array.isArray(record.tags) ?
[...record.tags] :
[record.tags] :
[];
switch (level) {
case NOTICE:
this._noticeRecords++;
break;
case WARN:
this._warningRecords++;
break;
case ERROR:
this._errorRecords++;
break;
}
/* Line, column and characters are a bit more complicated */
let line = 0;
let column = 0;
let length = 1;
if (file && record.line) {
line = record.line;
if (record.column) {
column = record.column;
if (record.length) {
length = record.length;
if (length < 0) {
length = Number.MAX_SAFE_INTEGER;
}
}
}
}
/* Remember our source code, line by line */
if ((file && source) && (!this._sources.has(file))) {
this._sources.set(file, source.split('\n'));
}
/* Remember this normalized report */
let reports = this._records.get(file || nul);
if (!reports)
this._records.set(file || nul, reports = new Set());
reports.add({ level, messages, tags, line, column, length: length });
}
/* All done */
return this;
}
async loadSources() {
// Read files in parallel
const promises = [];
// Iterate through all the files having records
for (const file of this._records.keys()) {
if (!file)
continue;
if (file === nul)
continue;
if (this._sources.has(file))
continue;
promises.push(readFile(file, 'utf-8')
.then((source) => source.split('\n'))
.then((lines) => this._sources.set(file, lines)));
}
// Await _all_ promise, ignore errors
await Promise.allSettled(promises);
}
done(showSources) {
if (showSources == null)
showSources = _showSources;
if (!this.empty)
this._emit(showSources);
if (this.errors)
throw BuildFailure.fail();
}
_emit(showSources) {
/* Counters for all we need to print nicely */
let fPad = 0;
let aPad = 0;
let mPad = 0;
let lPad = 0;
let cPad = 0;
/* Skip report all together if empty! */
if ((this._annotations.size === 0) && (this._records.size === 0))
return;
/* This is GIANT: sort and convert our data for easy reporting */
const entries = [...this._annotations.keys(), ...this._records.keys()]
// dedupe
.filter((file, i, a) => a.indexOf(file) === i) // dedupe
// sort ("null" files first - remember, "undefined" never gets sorted)
.sort((a, b) => {
return ((a || '') < (b || '')) ? -1 : ((a || '') > (b || '')) ? 1 : 0;
})
// map to a [ file, record[], annotation? ]
.map((file) => {
// Get our annotation for the file
const ann = file && file !== nul && this._annotations.get(file);
// Get the records (or an empty record array)
const records = [...(this._records.get(file) || [])]
// Sort records by line / column
.sort(({ line: al, column: ac }, { line: bl, column: bc }) => ((al || Number.MAX_SAFE_INTEGER) - (bl || Number.MAX_SAFE_INTEGER)) ||
((ac || Number.MAX_SAFE_INTEGER) - (bc || Number.MAX_SAFE_INTEGER)))
// Update our record padding length
.map((record) => {
if (record.line && (record.line > lPad))
lPad = record.line;
if (record.column && (record.column > cPad))
cPad = record.column;
for (const message of record.messages) {
if (message.length > mPad)
mPad = message.length;
}
return record;
});
// Update our file and annotation padding lengths
if (file && (file.length > fPad))
fPad = file.length;
if (ann && (ann.note.length > aPad))
aPad = ann.note.length;
// Return our entry
return { file, records, annotation: ann };
});
/* Adjust paddings... */
mPad = mPad <= 100 ? mPad : 0; // limit length of padding for breakaway lines
lPad = lPad.toString().length;
cPad = cPad.toString().length;
/* Basic emit options */
const options = { taskName: this._task, level: NOTICE };
this._emitter(options, ['']);
this._emitter(options, [$und($wht(this._title))]);
/* Iterate through all our [file,reports] tuple */
for (let f = 0; f < entries.length; f++) {
const { file, records, annotation } = entries[f];
const source = file && file != nul && this._sources.get(file);
if ((f === 0) || entries[f - 1]?.records.length) {
this._emitter(options, ['']);
}
if (file && file !== nul && annotation) {
const { level, note } = annotation;
const $col = level === NOTICE ? $blu : level === WARN ? $ylw : $red;
const ann = `${$gry('[')}${$col(note)}${$gry(']')}`;
const pad = ''.padStart((fPad + aPad) - (file.length + note.length));
this._emitter({ ...options, level }, [$wht($und(file)), pad, ann]);
}
else if (file !== nul) {
this._emitter(options, [$wht($und(file))]);
}
else if (f > 0) {
this._emitter(options, ['']); // white line for the last
}
/* Now get each message and do our magic */
for (let r = 0; r < records.length; r++) {
const { level, messages, tags, line, column, length = 1 } = records[r];
/* Prefix includes line and column */
let pfx;
if (file && line) {
if (column) {
pfx = ` ${line.toString().padStart(lPad)}:${column.toString().padEnd(cPad)} `;
}
else {
pfx = ` ${line.toString().padStart(lPad)}:${'-'.padEnd(cPad)} `;
}
}
else if (file != nul) {
pfx = ` ${'-'.padStart(lPad)}:${'-'.padEnd(cPad)} `;
}
else {
pfx = ' ~ ';
}
const prefix = ''.padStart(pfx.length + 1);
/* Nice tags */
const tag = tags.length == 0 ? '' :
`${$gry('[')}${tags.map((tag) => $cyn(tag)).join($gry('|'))}${$gry(']')}`;
/* Print out our messages, one by one */
if (messages.length === 1) {
this._emitter({ ...options, level }, [$gry(pfx), messages[0].padEnd(mPad), tag]);
}
else {
for (let m = 0; m < messages.length; m++) {
if (!m) { // first line
this._emitter({ ...options, level }, [$gry(pfx), messages[m]]);
}
else if (m === messages.length - 1) { // last line
this._emitter({ ...options, level, prefix }, [messages[m].padEnd(mPad), tag]);
}
else { // in between lines
this._emitter({ ...options, level, prefix }, [messages[m]]);
}
}
}
/* See if we have to / can print out the source */
if (showSources && source && source[line - 1]) {
if (column) {
const $col = level === NOTICE ? $blu : level === WARN ? $ylw : $red;
const offset = column - 1;
const text = source[line - 1] || '';
const head = $gry(text.substring(0, offset));
const body = $und($col(text.substring(offset, offset + length)));
const tail = $gry(text.substring(offset + length));
this._emitter({ ...options, level, prefix }, [$gry(`| ${head}${body}${tail}`)]);
}
else {
this._emitter({ ...options, level, prefix }, [$gry(`| ${source[line - 1]}`)]);
}
}
}
}
/* Our totals (if any) */
this._emitter(options, ['']);
const status = ['Found'];
if (this.errors) {
status.push($plur(this.errors, 'error', 'errors'));
}
if (this.warnings) {
if (this.errors)
status.push('and');
status.push($plur(this.warnings, 'warning', 'warnings'));
}
if (this.errors || this.warnings) {
this._emitter(options, status);
this._emitter(options, ['']);
}
/* Annotate in GitHub */
if (_githubAnnotations) {
for (const entry of entries) {
const file = entry.file === nul ? undefined : entry.file;
for (const report of entry.records) {
const type = report.level === logLevels.ERROR ? 'error' :
report.level === logLevels.WARN ? 'warning' :
null;
if (!type)
continue;
const title = `${this._title} (task "${this._task}")`;
const col = report.column || undefined;
const line = report.line || undefined;
const endColumn = report.column ?
report.length >= Number.MAX_SAFE_INTEGER ? undefined :
((report.column + report.length) || undefined) :
undefined;
const message = report.messages.join('\n');
githubAnnotation({ type, title, file, col, line, endColumn }, message);
}
}
}
}
}
//# sourceMappingURL=report.js.map