Skip to content

extforge/logger

extforge/logger is the structured logger used internally by the CLI, dev server, and built-in plugins. It is exported so plugin authors and CI scripts can produce log output that matches ExtForge’s banner-and-summary style — and so external tooling can pipe ExtForge into JSON consumers.

import { createLogger, LogLevel } from 'extforge/logger';
const log = createLogger({ scope: 'my-plugin', level: LogLevel.Info });
log.info('Reading manifest from %s', './src/manifest.ts');
log.success('Built %d outputs in %s', 3, log.formatDuration(412));
log.warn('Permission %o looks unused', 'identity.email');
enum LogLevel {
Silent = 0,
Error = 1,
Warn = 2,
Info = 3,
Success = 3, // alias for Info
Debug = 4,
Trace = 5,
}

The level is the threshold: everything ≤ the configured level prints, everything above is dropped. LogLevel.Silent disables all output.

Level resolution at runtime (highest priority wins):

  1. EXTFORGE_LOG_LEVEL=debug (or trace, info, warn, error, silent) — env override.
  2. --log-level <name> on the CLI.
  3. createLogger({ level }) in code.
  4. Default: LogLevel.Info.
log.error(msg, ...args)
log.warn(msg, ...args)
log.info(msg, ...args)
log.success(msg, ...args)
log.debug(msg, ...args)
log.trace(msg, ...args)
log.time(label) // start a timer
log.timeEnd(label, msg?) // stop + log duration
log.child('child-scope', overrides?) // returns a Logger with a nested [parent:child] scope
log.raw(line?) // write a line straight to the transport, bypassing formatting

Format placeholders match Node’s util.format: %s, %d, %o, %j.

child(scope, overrides?) inherits the parent’s level and transports unless you pass overrides (a Partial<LoggerOptions>). The new scope is appended to the parent’s, so a child of an extforge-scoped logger named manifest logs under extforge:manifest.

The CLI also uses a handful of presentation helpers on the same logger — banner(), group(), step(n, total, msg), summary(title, rows), file(), and hmr(). They format ExtForge’s own console output; plugin authors rarely need them, but they’re available on every Logger instance.

A transport is (entry: LogEntry) => void. The default transport is a human-formatted writer with ANSI colour. To capture structured output (for CI, log aggregators, or tests):

import { createLogger, jsonTransport } from 'extforge/logger';
const log = createLogger({
scope: 'extforge',
transports: [jsonTransport()], // one JSON line per entry to stdout
silentHumanOutput: true, // suppress the colour banner
});

Each JSON entry has shape:

{
"level": "info",
"scope": "extforge → manifest",
"message": "Wrote manifest for chrome",
"args": [],
"timestamp": 1715500000000,
"duration": 412 // present on timeEnd entries
}

You can plug in multiple transports — for example, a JSON file writer alongside the default colour transport. Transports can also be managed after construction with log.addTransport(fn) and log.clearTransports().

ANSI colour is auto-detected via:

  • FORCE_COLOR=1 → force on.
  • NO_COLOR=1 (or true) → force off. Respects no-color.org.
  • TERM=dumb → off.
  • Otherwise: enabled if process.stdout.isTTY.

The colour palette is re-exported as colors for plugins that want to match ExtForge’s look-and-feel:

import { colors } from 'extforge/logger';
console.log(colors.cyan('hello'));

The package also exports the formatters the CLI uses for banners and summaries. Number formatting is delegated to @arshad-shah/clif (formatDuration / formatBytes); formatPath is ExtForge’s cwd-relative path helper:

import { formatDuration, formatFileSize, formatPath } from 'extforge/logger';
formatDuration(412); // "412ms"
formatDuration(2_540); // "2.5s"
formatFileSize(2_580_000); // "2.5 MB"
formatPath('/abs/path/to/foo.ts'); // "./to/foo.ts" (relative to cwd)

ExtForge ships a singleton root logger so plugins emit under a consistent scope tree. Use it when you want to write into the ExtForge banner stream rather than your own:

import { getLogger } from 'extforge/logger';
const log = getLogger().child('my-plugin');
log.info('hook fired');

extforge/logger adds the build-tool presentation layer — badges, scopes, banners, summaries, timers, and the printf-style API the CLI relies on — on top of @arshad-shah/log-kit, a tiny zero-dependency structured logger that handles record fan-out and per-transport failure isolation. That keeps it lightweight enough to use from the CLI, plugins, build hooks, and CI scripts without dragging in pino, winston, or similar. If you need log rotation, remote shipping, or sampling, pipe jsonTransport() into a process that handles that.