storebound/cli/lib/log.ts

48 lines
3.1 KiB
TypeScript

// Plain, line-based progress output: easy to follow live and easy to tail when a long upload runs in the background
// (`storebound play images > .storebound/images.log 2>&1 &`). Every line carries a timestamp and a tag.
const t0 = Date.now();
const stamp = (): string => new Date().toISOString().slice(11, 19);
const since = (): string => `${((Date.now() - t0) / 1000).toFixed(0)}s`;
export const log = (tag: string, msg: string): void => console.log(`${stamp()} [${tag}] ${msg}`);
export const warn = (tag: string, msg: string): void => console.warn(`${stamp()} [${tag}] WARN ${msg}`);
export const done = (tag: string, msg: string): void => console.log(`${stamp()} [${tag}] ${msg} (${since()})`);
// The count lives on globalThis: the Node bundle (tools/build-node.ts) can carry this module twice (once for the
// static imports, once inside a lazily initialised chunk), and both copies must see the same --json run.
const JSON_RUNS = Symbol.for('storebound.jsonRuns');
const runs = globalThis as unknown as Record<symbol, number>;
/** Whether a `--json` run is going on: stdout carries only the JSON (printJson), everything else goes to stderr. */
export const jsonOutput = (): boolean => (runs[JSON_RUNS] ?? 0) > 0;
/** Where a question / prompt is written: stderr during a --json run, so stdout stays one JSON document. */
export const promptOut = (): NodeJS.WriteStream => (jsonOutput() ? process.stderr : process.stdout);
/**
* Runs a command with --json: stdout is only the JSON. console.log / console.info (log, done, step lines, tables and
* every other line a command prints) go to stderr for the run; printJson writes the result to stdout itself. index.ts
* wraps every command run with --json in this, so no command has to remember it.
*/
export async function withJsonOutput<T>(fn: () => Promise<T>): Promise<T> {
const log = console.log, info = console.info;
console.log = (...a: unknown[]) => console.error(...a);
console.info = (...a: unknown[]) => console.error(...a);
runs[JSON_RUNS] = (runs[JSON_RUNS] ?? 0) + 1;
try { return await fn(); } finally { runs[JSON_RUNS] = runs[JSON_RUNS]! - 1; console.log = log; console.info = info; }
}
/** "3/14 de-DE" style counters. */
export const step = (i: number, n: number, what: string): string => `${String(i).padStart(String(n).length)}/${n} ${what}`;
/** Prefix of the one machine-readable result line that `storebound serve --stdio` reads from a child command. */
export const RESULT_MARK = '\u001estorebound-result ';
/**
* Structured result of a `--json` run. In a terminal (or a pipe) it is pretty JSON on stdout as before; under
* `storebound serve` (STOREBOUND_MACHINE=1) it is one marked line, so log lines printed before it never break parsing.
*/
export function printJson(data: unknown): void {
if (process.env.STOREBOUND_MACHINE === '1') process.stdout.write(RESULT_MARK + JSON.stringify(data) + '\n');
// during a --json run console.log goes to stderr, so the JSON goes to stdout itself
else if (jsonOutput()) process.stdout.write(JSON.stringify(data, null, 2) + '\n');
else console.log(JSON.stringify(data, null, 2));
}