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.
Quick start
Section titled “Quick start”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');Log levels
Section titled “Log levels”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):
EXTFORGE_LOG_LEVEL=debug(ortrace,info,warn,error,silent) — env override.--log-level <name>on the CLI.createLogger({ level })in code.- Default:
LogLevel.Info.
Methods
Section titled “Methods”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 timerlog.timeEnd(label, msg?) // stop + log duration
log.child('child-scope', overrides?) // returns a Logger with a nested [parent:child] scopelog.raw(line?) // write a line straight to the transport, bypassing formattingFormat 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.
Transports
Section titled “Transports”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().
Colour handling
Section titled “Colour handling”ANSI colour is auto-detected via:
FORCE_COLOR=1→ force on.NO_COLOR=1(ortrue) → 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'));Formatters
Section titled “Formatters”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)Root logger
Section titled “Root logger”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');Why a custom logger?
Section titled “Why a custom logger?”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.