2021-10-30 17:26:41 +00:00
|
|
|
import * as util from 'util';
|
|
|
|
import * as path from 'path';
|
2021-11-02 04:29:24 +00:00
|
|
|
import * as fs from 'fs/promises';
|
|
|
|
|
|
|
|
import * as moment from 'moment';
|
|
|
|
import * as colors from 'colors/safe';
|
|
|
|
import { SourceMapConsumer } from 'source-map';
|
2021-10-30 17:26:41 +00:00
|
|
|
|
|
|
|
import * as StackTrace from '../stack-trace/stack-trace';
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Pads a string toward the center
|
|
|
|
* @param x The value to pad
|
|
|
|
* @param p The padding to use
|
|
|
|
* @param n The end-length of the string
|
|
|
|
* @returns The padded string
|
|
|
|
*/
|
|
|
|
function padCenter(x: string, p: string, n: number) {
|
|
|
|
let z = false;
|
|
|
|
while (x.length < n) {
|
|
|
|
if (z) {
|
|
|
|
z = false;
|
|
|
|
x = p + x;
|
|
|
|
} else {
|
|
|
|
z = true;
|
|
|
|
x = x + p;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return x;
|
|
|
|
}
|
|
|
|
|
2021-11-02 04:29:24 +00:00
|
|
|
/**
|
|
|
|
* @param jsFile The compiled javascript file
|
|
|
|
* @param tsRelativeFile The source typescript file relative to jsFile's directory
|
|
|
|
* @returns The path to tsRelativeFile based on the root directory
|
|
|
|
*/
|
|
|
|
function getPrintedPath(jsFile: string, tsRelativeFile: string) {
|
|
|
|
let rootFile = path.join(__dirname, '..', '..');
|
|
|
|
let jsDir = path.dirname(jsFile);
|
|
|
|
let tsPath = path.resolve(jsDir, tsRelativeFile);
|
|
|
|
return path.relative(rootFile, tsPath);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param dir The directory to search
|
|
|
|
* @returns an async iterator to all files in a directory (recursively searched)
|
|
|
|
*/
|
|
|
|
async function* getAllFiles(dir: string): AsyncIterable<string> {
|
|
|
|
let filesAndDirs = await fs.readdir(dir, { withFileTypes: true });
|
|
|
|
for (let next of filesAndDirs) {
|
|
|
|
if (next.isFile()) {
|
|
|
|
yield path.join(dir, next.name);
|
|
|
|
} else if (next.isDirectory()) {
|
|
|
|
let nextFiles = getAllFiles(path.join(dir, next.name));
|
|
|
|
for await (let nextFile of nextFiles) {
|
|
|
|
yield nextFile;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-05 00:30:30 +00:00
|
|
|
/**
|
|
|
|
* @param sourceMaps A mapping from .js file to SourceMapConsumer created from a corresponding .js.map file
|
|
|
|
* @param frame The StackTrace.Frame to try to convert into typescript format
|
|
|
|
* @returns A StackTrace.Frame that is in terms of the corresponding typescript file or the same StackTrace.Frame
|
|
|
|
* if the conversion couldn't be done (i.e. it's an internal node code)
|
|
|
|
*/
|
2021-11-02 04:29:24 +00:00
|
|
|
function tryCreateTSFrame(sourceMaps: Map<string, SourceMapConsumer>, frame: StackTrace.Frame): StackTrace.Frame {
|
|
|
|
if (!frame.fileName) return frame;
|
|
|
|
let mapping = sourceMaps.get(frame.fileName);
|
|
|
|
if (!mapping) return frame;
|
|
|
|
if (!frame.line || !frame.column) return frame;
|
|
|
|
let pos = mapping.originalPositionFor({ line: frame.line, column: frame.column });
|
|
|
|
if (!pos.source) return frame;
|
|
|
|
return {
|
|
|
|
functionName: frame.functionName,
|
2021-11-05 00:30:30 +00:00
|
|
|
fileName: path.resolve(path.dirname(frame.fileName), pos.source),
|
2021-11-02 04:29:24 +00:00
|
|
|
line: pos.line ?? undefined,
|
|
|
|
column: pos.column ?? undefined
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// I know, I know. But it takes all this disgusting code to make sure we only have to load the source maps once per module instance
|
2021-11-05 00:30:30 +00:00
|
|
|
// instead of spamming the hard drive. To be honest, why do you care about this file anyway. It's just the logger :)
|
2021-11-02 04:29:24 +00:00
|
|
|
let STATIC_SOURCE_MAPS_LOADING = false;
|
|
|
|
let STATIC_SOURCE_MAPS_CALLBACKS: ((sourceMaps: Map<string, SourceMapConsumer>) => void)[] = [];
|
|
|
|
let STATIC_SOURCE_MAPS: Map<string, SourceMapConsumer> | null = null;
|
|
|
|
async function getStaticSourceMaps(): Promise<Map<string, SourceMapConsumer>> {
|
|
|
|
if (STATIC_SOURCE_MAPS !== null) {
|
|
|
|
return STATIC_SOURCE_MAPS;
|
|
|
|
} else if (STATIC_SOURCE_MAPS_LOADING) {
|
|
|
|
return new Promise<Map<string, SourceMapConsumer>>((resolve) => {
|
|
|
|
STATIC_SOURCE_MAPS_CALLBACKS.push(resolve);
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
STATIC_SOURCE_MAPS_LOADING = true;
|
|
|
|
let sourceMaps = new Map<string, SourceMapConsumer>();
|
|
|
|
for await (let file of getAllFiles(path.join(__dirname, '..'))) {
|
|
|
|
if (!file.endsWith('.js')) continue;
|
|
|
|
if (!await fs.stat(file + '.map').then(() => true).catch(() => false)) continue; // make sure .map file exists
|
|
|
|
let fileBuff = await fs.readFile(file + '.map');
|
|
|
|
if (typeof window !== 'undefined' && URL && URL.createObjectURL) {
|
|
|
|
// Only run this stuff if we are running in a browser window
|
|
|
|
let mappingsWasmBuffer = await fs.readFile(path.join(__dirname, '../../node_modules/source-map/lib/mappings.wasm'));
|
|
|
|
let mappingsWasmBlob = new Blob([ mappingsWasmBuffer ], { type: 'application/wasm' });
|
|
|
|
//@ts-ignore Typescript is missing this function... Probably because it is static
|
|
|
|
SourceMapConsumer.initialize({ "lib/mappings.wasm": URL.createObjectURL(mappingsWasmBlob) });
|
|
|
|
}
|
|
|
|
let sourceMapConsumer = await new SourceMapConsumer(fileBuff.toString());
|
|
|
|
sourceMaps.set(file, sourceMapConsumer);
|
|
|
|
}
|
|
|
|
STATIC_SOURCE_MAPS = sourceMaps;
|
|
|
|
STATIC_SOURCE_MAPS_LOADING = false;
|
|
|
|
for (let callback of STATIC_SOURCE_MAPS_CALLBACKS) {
|
|
|
|
callback(sourceMaps);
|
|
|
|
}
|
|
|
|
STATIC_SOURCE_MAPS_CALLBACKS = [];
|
|
|
|
return STATIC_SOURCE_MAPS;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-05 00:30:30 +00:00
|
|
|
enum LoggerLevel {
|
|
|
|
Fatal, Error, Warn,
|
|
|
|
Info, Debug, Silly
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Colorizes text
|
|
|
|
* @param level The color level
|
|
|
|
* @param text The text to colorize
|
|
|
|
* @returns The colored text
|
|
|
|
*/
|
|
|
|
function colorize(level: LoggerLevel, text: string): string {
|
|
|
|
switch (level) {
|
|
|
|
case LoggerLevel.Fatal: return colors.bgRed(colors.white(text));
|
|
|
|
case LoggerLevel.Error: return colors.red(text);
|
|
|
|
case LoggerLevel.Warn: return colors.yellow(text);
|
|
|
|
case LoggerLevel.Info: return colors.green(text);
|
|
|
|
case LoggerLevel.Debug: return colors.blue(text);
|
|
|
|
case LoggerLevel.Silly: return colors.magenta(text);
|
|
|
|
default: return colors.bgWhite(text);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-30 17:26:41 +00:00
|
|
|
export default class Logger {
|
|
|
|
private name: string;
|
|
|
|
private console: Console;
|
2021-11-02 04:29:24 +00:00
|
|
|
private sourceMaps = new Map<string, SourceMapConsumer>();
|
2021-11-07 20:13:59 +00:00
|
|
|
private hasSourceMaps = false;
|
|
|
|
private sourceMapsWaiters: (() => void)[] = [];
|
2021-10-30 17:26:41 +00:00
|
|
|
|
2021-11-02 04:29:24 +00:00
|
|
|
private constructor(name: string, processConsole?: Console) {
|
2021-10-30 17:26:41 +00:00
|
|
|
this.name = name;
|
|
|
|
this.console = processConsole ?? console;
|
|
|
|
}
|
|
|
|
|
2021-11-05 00:30:30 +00:00
|
|
|
/** Creates a new logger. The logger will print in terms of the dist .js files until it loads the TypeScript mappings */
|
2021-11-02 04:29:24 +00:00
|
|
|
static create(name: string, processConsole?: Console) {
|
|
|
|
let log = new Logger(name, processConsole);
|
|
|
|
(async () => {
|
|
|
|
let sourceMaps = await getStaticSourceMaps();
|
|
|
|
log.sourceMaps = sourceMaps;
|
2021-11-07 20:13:59 +00:00
|
|
|
log.onGetSourceMaps();
|
2021-11-02 04:29:24 +00:00
|
|
|
})();
|
|
|
|
return log;
|
|
|
|
}
|
|
|
|
|
2021-11-07 20:13:59 +00:00
|
|
|
private onGetSourceMaps() {
|
|
|
|
this.hasSourceMaps = true;
|
|
|
|
for (let sourceMapWaiter of this.sourceMapsWaiters) {
|
|
|
|
sourceMapWaiter();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public async ensureSourceMaps(): Promise<void> {
|
|
|
|
if (this.hasSourceMaps) return;
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
this.sourceMapsWaiters.push(resolve);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-11-05 00:30:30 +00:00
|
|
|
/** Logs a message and potentially corresponding data */
|
2021-10-30 17:26:41 +00:00
|
|
|
private log(level: LoggerLevel, message: string | null, data?: Error | any): void {
|
|
|
|
let frames: StackTrace.Frame[] = StackTrace.parse(new Error());
|
2021-11-02 04:29:24 +00:00
|
|
|
let jsFrame = frames[2];
|
|
|
|
let tsFrame = tryCreateTSFrame(this.sourceMaps, jsFrame);
|
2021-10-30 17:26:41 +00:00
|
|
|
|
2021-11-05 00:30:30 +00:00
|
|
|
let nameDisplay = tsFrame.fileName ? `${path.basename(getPrintedPath(jsFrame.fileName, tsFrame.fileName))}:${tsFrame.line}:${tsFrame.column}` : this.name;
|
2021-10-30 17:26:41 +00:00
|
|
|
|
2021-10-31 19:02:26 +00:00
|
|
|
let prefix: string = `[ ${colorize(level, padCenter(LoggerLevel[level].toLowerCase(), ' ', 5))} | ${nameDisplay} | ${moment().format('HH:mm:ss.SSS')} ]`;
|
2021-10-30 17:26:41 +00:00
|
|
|
|
|
|
|
let out: string = '';
|
|
|
|
if (message !== null) {
|
|
|
|
out += message.split('\n').map(o => `${prefix}: ${o}`).join('\n');
|
|
|
|
}
|
2021-11-02 04:29:24 +00:00
|
|
|
let handleData = (data: Error | any) => {
|
2021-10-30 17:26:41 +00:00
|
|
|
if (data) {
|
|
|
|
if (message !== null) {
|
|
|
|
out += '\n';
|
|
|
|
}
|
|
|
|
if (data instanceof Error) {
|
|
|
|
if (data.stack) {
|
2021-11-05 00:30:30 +00:00
|
|
|
const baseDir = path.join(__dirname, '..', '..');
|
2021-11-02 04:29:24 +00:00
|
|
|
let errorFrames = StackTrace.parse(data);
|
|
|
|
let tsStackLines = errorFrames
|
2021-11-05 00:30:30 +00:00
|
|
|
.map((frame: StackTrace.Frame) => tryCreateTSFrame(this.sourceMaps, frame))
|
|
|
|
.map((frame: StackTrace.Frame) => StackTrace.relativizeFilePath(frame, baseDir))
|
|
|
|
.map((frame: StackTrace.Frame) => StackTrace.getFrameLine(frame));
|
2021-11-02 04:29:24 +00:00
|
|
|
out += `${prefix}# ${data.name}: ${data.message}`;
|
|
|
|
out += `\n${prefix}# ${tsStackLines.join(`\n${prefix}# `)}`;
|
2021-11-05 00:30:30 +00:00
|
|
|
//out += `${prefix}# ${data.stack.split('\n').join(`\n${prefix}# `)}`;
|
2021-10-30 17:26:41 +00:00
|
|
|
} else {
|
|
|
|
out += `${prefix}# ${data.name}: ${data.message}`;
|
|
|
|
}
|
2021-11-02 04:29:24 +00:00
|
|
|
// Use the source map to create a typescript version of the stack
|
|
|
|
// This will be printed asynchronously because SourceMapConsumer
|
2021-10-30 17:26:41 +00:00
|
|
|
} else {
|
|
|
|
let s = util.inspect(data, { colors: true });
|
|
|
|
s = s.split('\n').map(o => `${prefix}$ ${o}`).join('\n');
|
|
|
|
out += s;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (Array.isArray(data)) {
|
|
|
|
data.forEach(handleData);
|
|
|
|
} else {
|
|
|
|
handleData(data);
|
|
|
|
}
|
|
|
|
this.console.log(out);
|
|
|
|
}
|
|
|
|
|
|
|
|
public fatal(message: string | null, data?: Error | any): void { this.log(LoggerLevel.Fatal, message, data); }
|
|
|
|
public error(message: string | null, data?: Error | any): void { this.log(LoggerLevel.Error, message, data); }
|
|
|
|
public warn( message: string | null, data?: Error | any): void { this.log(LoggerLevel.Warn, message, data); }
|
|
|
|
public info( message: string | null, data?: Error | any): void { this.log(LoggerLevel.Info, message, data); }
|
|
|
|
public debug(message: string | null, data?: Error | any): void { this.log(LoggerLevel.Debug, message, data); }
|
|
|
|
public silly(message: string | null, data?: Error | any): void { this.log(LoggerLevel.Silly, message, data); }
|
|
|
|
|
2021-11-05 00:30:30 +00:00
|
|
|
// Wrapper for util.inspect in case we want to do anything special in the future
|
2021-10-30 17:26:41 +00:00
|
|
|
public inspect(object: any, showHidden?: boolean, depth?: number, color?: boolean): string {
|
|
|
|
return util.inspect(object, showHidden, depth, color);
|
|
|
|
}
|
|
|
|
}
|