fuse-box
Version:
Fuse-Box a bundler that does it right
383 lines (337 loc) • 12 kB
text/typescript
import { ModuleCollection } from "./core/ModuleCollection";
import { WorkFlowContext } from "./core/WorkflowContext";
import * as log from "fliplog";
import * as prettysize from "prettysize";
import * as prettyTime from "pretty-time";
import * as zlib from "zlib";
// @TODO: I've moved this into fliplog in v1, migrate to that
export class Indenter {
public store: Map<string, any> = new Map()
constructor() {
this.set('indent', 0);
}
// easy set/set
public set(key: string, val: any): Indenter {
this.store.set(key, val)
return this;
}
public get(key: string) {
return this.store.get(key);
}
// back to 0
public reset(): Indenter {
return this.set('indent', 0);
}
// tap value
public tap(key: string, cb: Function): Indenter {
const updated = cb(this.store.get(key));
return this.set(key, updated);
}
// increment
public indent(level: number): Indenter {
return this.tap('indent', indent => indent + level);
}
// specific number
public level(level: number): Indenter {
return this.set('indent', level);
}
// string repeat indent
public toString(): string {
return ' '.repeat(this.get('indent'))
}
public toNumber(): number {
return this.get('indent');
}
public [Symbol.toPrimitive](hint: string) {
if (hint === 'number') { return this.toNumber(); }
return this.toString();;
}
}
/**
* @TODO:
* - [ ] should add filters for outputing fs
* - [ ] should .tree the files
* - [ ] fix the →→→→→→→
*/
export class Log {
public timeStart = process.hrtime();
public printLog: any = true;
public debugMode: any = false;
public spinner: any;
public indent: Indenter = new Indenter();
private totalSize = 0;
constructor(public context: WorkFlowContext) {
this.printLog = context.doLog;
this.debugMode = context.debugMode;
log.filter((arg) => {
// conditions for filtering specific tags
const debug = this.debugMode
const level = this.printLog
const hasTag = tag =>
arg.tags.includes(tag)
const levelHas = tag =>
debug || (level && level.includes && level.includes(tag) && !level.includes('!' + tag));
// when off, silent
if (level === false) return false;
// counting this as verbose for now
if (level === true && debug === true) { return null; }
if (level == 'error') {
if (!hasTag('error')) { return false; }
}
// could be verbose, reasoning, etc
if (hasTag('magic')) {
if (!levelHas('magic')) { return false; }
}
if (hasTag('filelist')) {
if (!levelHas('filelist')) { return false; }
}
// if not false and conditions pass, log it
return null;
});;
}
// --- config ---
public reset(): Log {
this.timeStart = process.hrtime();
this.totalSize = 0;
this.indent.reset();
return this;
}
public printOptions(title: string, obj: any) {
let indent = this.indent.level(2) + '';;
let indent2 = this.indent.level(4) + '';;
// @TODO: moved this into fliplog v1, migrate
log.addPreset('min', instance => {
instance.formatter(data => {
return log.inspector()(data).split('\n')
.map(data => indent2 + data)
.map(data => data.replace(/[{},]/, ''))
.join('\n');;
});
});
log.bold().yellow(`${indent}→ ${title}\n`).preset('min').data(obj).echo();
// for (let i in obj) {
// indent = this.indent.level(6) + ''
// log.green(`${indent}${i} : ${obj[i]}`).echo();
// }
// this.indent.indent(-2)
return this;
}
// --- start end ---
// @TODO add spinners here
// @TODO combine logs here, output when needed
// @TODO combine this and subBundleStart
public bundleStart(name: string) {
log.gray(``).echo();
log.gray(`--------------------------`).echo();
log.magenta(`Bundle "${name}" `).echo();
log.gray(``).echo();
return this;
}
public subBundleStart(name: string, parent: string) {
log.bold(`${name} (child of ${parent}) ->`).echo();
return this;
}
public bundleEnd(name: string, collection: ModuleCollection) {
let took = process.hrtime(this.timeStart) as [number, number];
log
.ansi()
.write(`-> Finished`)
.green(collection.cachedName || collection.name)
.yellow(`took: ${prettyTime(took, "ms")}`)
.echo();
}
// --- spinner ---
public startSpinner(text: string) {
if (!this.printLog) { return this; }
// spinner opts
const indentStr = this.indent.toString();
const indent = +this.indent;
const interval = 20;
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'].map(frame => indentStr + frame);
const spinner = { frames, interval };
// @TODO @FIXME the spinner needs to be scoped inside of fliplog, has todo to update
// instantiate
this.spinner = log.requirePkg('ora')({ text, indent, spinner });
this.spinner.start();
this.spinner.indent = +this.indent;
this.spinner.succeeded = false;
// safety for if errors happen so it does not keep spinning
setTimeout(() => {
if (this.spinner.succeeded === false && this.spinner.fail) {
this.spinner.fail();;
}
}, 1000);
return this;
}
public stopSpinner(text?: string) {
if (!this.printLog) { return this; }
// safety, mark as success
if (this.spinner && this.spinner.succeeded === false) {
this.spinner.succeeded = true;
const reference = this.spinner;
const indent = this.indent.level(this.spinner.indent).toString();
// @override success to indent
// reference.succeed()
const success = log.chalk().green(`${indent}✔ `);
text = text || reference.text;
reference.stopAndPersist({ symbol: success, text });
// it's too fast!
// setTimeout(() => reference.succeed(), 1)
}
return this;
}
// --- collection stats ---
// @TODO: list vendor files as filter
public echoDefaultCollection(collection: ModuleCollection, contents: string) {
if (this.printLog === false) return this;
let bytes = Buffer.byteLength(contents, "utf8");
let size = prettysize(bytes);
this.totalSize += bytes;
const indent = this.indent.reset().indent(+1).toString();
// @example └── (5 files, 7.6 kB) default
// @TODO auto indent as with ansi
collection.dependencies.forEach(file => {
if (file.info.isRemoteFile) { return; }
const indent = this.indent.level(4).toString()
log
// .tags('filelist')
.white(`${indent}${file.info.fuseBoxPath}`)
.echo()
});
log
.ansi()
.write(`└──`)
.yellow(`${indent}(${collection.dependencies.size} files, ${size})`)
.green(collection.cachedName || collection.name)
.echo();
this.indent.level(0);
return this;
}
// @example
// └── fuse-box-css 1.5 kB (1 files)
// └── lodash 14.2 kB (12 files)
public echoCollection(collection: ModuleCollection, contents: string) {
if (this.printLog === false) { return this; }
let bytes = Buffer.byteLength(contents, "utf8");
let size = prettysize(bytes);
this.totalSize += bytes;
const indent = this.indent.toString(); // reset
const name = (collection.cachedName || collection.name).trim();
log
.ansi()
.write(`${indent}└──`)
.green(name)
.yellow(size)
.write(`(${collection.dependencies.size} files)`)
.echo();
return this;
}
public end(header?: string) {
let took = process.hrtime(this.timeStart) as [number, number];
this.echoBundleStats(header || "Bundle", this.totalSize, took);
return this;
}
/**
* @TODO
* - [ ] ensure header will not conflict if it is used in echoBundleStats
*
* string | number | Buffer
*/
public echoGzip(size: any, msg: string | any = '') {
if (!size) return this;
const yellow = log.chalk().yellow;
const gzipped = zlib.gzipSync(size, { level: 9 }).length;
const gzippedSize = prettysize(gzipped) + ' (gzipped)';
const compressedSize = prettysize(size.length);
const prettyGzip = yellow(`${compressedSize}, ${gzippedSize}`);
log
.title(this.indent + '')
.when(msg,
() => log.text(msg),
() => log.bold('size: '))
.data(prettyGzip)
.echo();
return this;
}
/**
* @TODO @FIXME
* - [ ] bundle stats are wrong because they use accumulated size,
* not the uglified end result size
* use uglified and QuantumPlugin output
*/
public echoBundleStats(header: string, size: number, took: [number, number]) {
this.indent.reset();
const yellow = log.chalk().yellow;
const sized = yellow(`${prettysize(size)}`);
log.text(`size: ${sized} in ${prettyTime(took, "ms")}`).echo();
return this;
}
// --- bundle specifics ---
public echoHeader(str: string) {
this.indent.level(1);
log.yellow(`${this.indent}${str}`).echo();
return this;
}
public echoStatus(str: string) {
log.title(`→`).cyan(`${str}`).echo();
return this;
}
// --- generalized ---
public groupHeader(str: string) {
log.color('bold.underline').text(`${str}`).echo();
return this;
}
public echoInfo(str: string) {
const indent = this.indent.level(2);
log.preset('info').green(`${indent}→ ${str}`).echo();
return this;
}
public error(error: Error) {
// @TODO: finish forking notifier & dep chain
// if (this.printLog.includes('notify')) {
// log.factory().notify({title: error.message, message: error.stack}).echo()
// }
log.tags('error').data(error).echo();
return this;
}
// @NOTE: later this will be used with preset tags
public magicReason(str: string, metadata: any = false) {
if (metadata) {
log.data(metadata);
}
log.tags('magic').magenta(str).echo();
return this;
}
// -----------
// simplified shorthands for external formatting
// @TODO: anything using these should be fomatted inside of the logger
// -----------
public echo(str: string) {
log.time(true).green(str).echo();
return this;
}
public echoBoldRed(msg) {
log.red().bold(msg).echo();
return this;
}
public echoRed(msg) {
log.red(msg).echo();
return this;
}
public echoBreak() {
log.green(`\n -------------- \n`).echo();
return this;
}
public echoWarning(str: string) {
log.yellow(` → WARNING ${str}`).echo();
return this;
}
public echoYellow(str: string) {
log.yellow(str).echo();
return this;
}
public echoGray(str: string) {
log.gray(str).echo();
return this;
}
}