@socketsecurity/lib
Version:
Core utilities and infrastructure for Socket.dev security tools
1,455 lines (1,454 loc) • 43.8 kB
JavaScript
"use strict";
/* Socket Lib - Built with esbuild */
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var logger_exports = {};
__export(logger_exports, {
LOG_SYMBOLS: () => LOG_SYMBOLS,
Logger: () => Logger,
getDefaultLogger: () => getDefaultLogger,
incLogCallCountSymbol: () => incLogCallCountSymbol,
lastWasBlankSymbol: () => lastWasBlankSymbol
});
module.exports = __toCommonJS(logger_exports);
var import_is_unicode_supported = __toESM(require("./external/@socketregistry/is-unicode-supported"));
var import_yoctocolors_cjs = __toESM(require("./external/yoctocolors-cjs"));
var import_strings = require("./strings");
var import_context = require("./themes/context");
var import_themes = require("./themes/themes");
const globalConsole = console;
const ReflectApply = Reflect.apply;
const ReflectConstruct = Reflect.construct;
let _Console;
// @__NO_SIDE_EFFECTS__
function constructConsole(...args) {
if (_Console === void 0) {
const nodeConsole = require("node:console");
_Console = nodeConsole.Console;
}
return ReflectConstruct(
_Console,
// eslint-disable-line no-undef
args
);
}
// @__NO_SIDE_EFFECTS__
function getYoctocolors() {
return import_yoctocolors_cjs.default;
}
// @__NO_SIDE_EFFECTS__
function applyColor(text, color, colors) {
if (typeof color === "string") {
return colors[color](text);
}
return colors.rgb(color[0], color[1], color[2])(text);
}
const LOG_SYMBOLS = /* @__PURE__ */ (() => {
const target = {
__proto__: null
};
let initialized = false;
const handler = {
__proto__: null
};
const updateSymbols = () => {
const supported = (0, import_is_unicode_supported.default)();
const colors = /* @__PURE__ */ getYoctocolors();
const theme = (0, import_context.getTheme)();
const successColor = theme.colors.success;
const errorColor = theme.colors.error;
const warningColor = theme.colors.warning;
const infoColor = theme.colors.info;
const stepColor = theme.colors.step;
target.fail = /* @__PURE__ */ applyColor(supported ? "\u2716" : "\xD7", errorColor, colors);
target.info = /* @__PURE__ */ applyColor(supported ? "\u2139" : "i", infoColor, colors);
target.step = /* @__PURE__ */ applyColor(supported ? "\u2192" : ">", stepColor, colors);
target.success = /* @__PURE__ */ applyColor(supported ? "\u2714" : "\u221A", successColor, colors);
target.warn = /* @__PURE__ */ applyColor(supported ? "\u26A0" : "\u203C", warningColor, colors);
};
const init = () => {
if (initialized) {
return;
}
updateSymbols();
initialized = true;
for (const trapName in handler) {
delete handler[trapName];
}
};
const reset = () => {
if (!initialized) {
return;
}
updateSymbols();
};
for (const trapName of Reflect.ownKeys(Reflect)) {
const fn = Reflect[trapName];
if (typeof fn === "function") {
;
handler[trapName] = (...args) => {
init();
return fn(...args);
};
}
}
(0, import_context.onThemeChange)(() => {
reset();
});
return new Proxy(target, handler);
})();
const boundConsoleEntries = [
// Add bound properties from console[kBindProperties](ignoreErrors, colorMode, groupIndentation).
// https://github.com/nodejs/node/blob/v24.0.1/lib/internal/console/constructor.js#L230-L265
"_stderrErrorHandler",
"_stdoutErrorHandler",
// Add methods that need to be bound to function properly.
"assert",
"clear",
"count",
"countReset",
"createTask",
"debug",
"dir",
"dirxml",
"error",
// Skip group methods because in at least Node 20 with the Node --frozen-intrinsics
// flag it triggers a readonly property for Symbol(kGroupIndent). Instead, we
// implement these methods ourselves.
//'group',
//'groupCollapsed',
//'groupEnd',
"info",
"log",
"table",
"time",
"timeEnd",
"timeLog",
"trace",
"warn"
].filter((n) => typeof globalConsole[n] === "function").map((n) => [n, globalConsole[n].bind(globalConsole)]);
const consolePropAttributes = {
__proto__: null,
writable: true,
enumerable: false,
configurable: true
};
const maxIndentation = 1e3;
const privateConsole = /* @__PURE__ */ new WeakMap();
const privateConstructorArgs = /* @__PURE__ */ new WeakMap();
let _consoleSymbols;
function getConsoleSymbols() {
if (_consoleSymbols === void 0) {
_consoleSymbols = Object.getOwnPropertySymbols(globalConsole);
}
return _consoleSymbols;
}
const incLogCallCountSymbol = Symbol.for("logger.logCallCount++");
let _kGroupIndentationWidthSymbol;
function getKGroupIndentationWidthSymbol() {
if (_kGroupIndentationWidthSymbol === void 0) {
_kGroupIndentationWidthSymbol = getConsoleSymbols().find((s) => s.label === "kGroupIndentWidth") ?? Symbol("kGroupIndentWidth");
}
return _kGroupIndentationWidthSymbol;
}
const lastWasBlankSymbol = Symbol.for("logger.lastWasBlank");
class Logger {
/**
* Static reference to log symbols for convenience.
*
* @example
* ```typescript
* console.log(`${Logger.LOG_SYMBOLS.success} Done`)
* ```
*/
static LOG_SYMBOLS = LOG_SYMBOLS;
#parent;
#boundStream;
#stderrLogger;
#stdoutLogger;
#stderrIndention = "";
#stdoutIndention = "";
#stderrLastWasBlank = false;
#stdoutLastWasBlank = false;
#logCallCount = 0;
#options;
#originalStdout;
#theme;
/**
* Creates a new Logger instance.
*
* When called without arguments, creates a logger using the default
* `process.stdout` and `process.stderr` streams. Can accept custom
* console constructor arguments for advanced use cases.
*
* @param args - Optional console constructor arguments
*
* @example
* ```typescript
* // Default logger
* const logger = new Logger()
*
* // Custom streams (advanced)
* const customLogger = new Logger({
* stdout: customWritableStream,
* stderr: customErrorStream
* })
* ```
*/
constructor(...args) {
privateConstructorArgs.set(this, args);
const options = args["0"];
if (typeof options === "object" && options !== null) {
this.#options = { __proto__: null, ...options };
this.#originalStdout = options.stdout;
const themeOption = options.theme;
if (themeOption) {
if (typeof themeOption === "string") {
this.#theme = import_themes.THEMES[themeOption];
} else {
this.#theme = themeOption;
}
}
} else {
this.#options = { __proto__: null };
}
}
/**
* Get the Console instance for this logger, creating it lazily on first access.
*
* This lazy initialization allows the logger to be imported during early
* Node.js bootstrap before stdout is ready, avoiding Console initialization
* errors (ERR_CONSOLE_WRITABLE_STREAM).
*
* @private
*/
#getConsole() {
ensurePrototypeInitialized();
let con = privateConsole.get(this);
if (!con) {
const ctorArgs = privateConstructorArgs.get(this) ?? [];
if (ctorArgs.length) {
con = /* @__PURE__ */ constructConsole(...ctorArgs);
} else {
con = /* @__PURE__ */ constructConsole({
stdout: process.stdout,
stderr: process.stderr
});
for (const { 0: key, 1: method } of boundConsoleEntries) {
con[key] = method;
}
}
privateConsole.set(this, con);
privateConstructorArgs.delete(this);
}
return con;
}
/**
* Gets a logger instance bound exclusively to stderr.
*
* All logging operations on this instance will write to stderr only.
* Indentation is tracked separately from stdout. The instance is
* cached and reused on subsequent accesses.
*
* @returns A logger instance bound to stderr
*
* @example
* ```typescript
* // Write errors to stderr
* logger.stderr.error('Configuration invalid')
* logger.stderr.warn('Using fallback settings')
*
* // Indent only affects stderr
* logger.stderr.indent()
* logger.stderr.error('Nested error details')
* logger.stderr.dedent()
* ```
*/
get stderr() {
if (!this.#stderrLogger) {
const ctorArgs = privateConstructorArgs.get(this) ?? [];
const instance = new Logger(...ctorArgs);
instance.#parent = this;
instance.#boundStream = "stderr";
instance.#options = { __proto__: null, ...this.#options };
instance.#theme = this.#theme;
this.#stderrLogger = instance;
}
return this.#stderrLogger;
}
/**
* Gets a logger instance bound exclusively to stdout.
*
* All logging operations on this instance will write to stdout only.
* Indentation is tracked separately from stderr. The instance is
* cached and reused on subsequent accesses.
*
* @returns A logger instance bound to stdout
*
* @example
* ```typescript
* // Write normal output to stdout
* logger.stdout.log('Processing started')
* logger.stdout.log('Items processed: 42')
*
* // Indent only affects stdout
* logger.stdout.indent()
* logger.stdout.log('Detailed output')
* logger.stdout.dedent()
* ```
*/
get stdout() {
if (!this.#stdoutLogger) {
const ctorArgs = privateConstructorArgs.get(this) ?? [];
const instance = new Logger(...ctorArgs);
instance.#parent = this;
instance.#boundStream = "stdout";
instance.#options = { __proto__: null, ...this.#options };
instance.#theme = this.#theme;
this.#stdoutLogger = instance;
}
return this.#stdoutLogger;
}
/**
* Get the root logger (for accessing shared indentation state).
* @private
*/
#getRoot() {
return this.#parent || this;
}
/**
* Get the resolved theme for this logger instance.
* Returns instance theme if set, otherwise falls back to context theme.
* @private
*/
#getTheme() {
return this.#theme ?? (0, import_context.getTheme)();
}
/**
* Get logger-specific symbols using the resolved theme.
* @private
*/
#getSymbols() {
const theme = this.#getTheme();
const supported = (0, import_is_unicode_supported.default)();
const colors = /* @__PURE__ */ getYoctocolors();
return {
__proto__: null,
fail: /* @__PURE__ */ applyColor(supported ? "\u2716" : "\xD7", theme.colors.error, colors),
info: /* @__PURE__ */ applyColor(supported ? "\u2139" : "i", theme.colors.info, colors),
step: /* @__PURE__ */ applyColor(supported ? "\u2192" : ">", theme.colors.step, colors),
success: /* @__PURE__ */ applyColor(supported ? "\u2714" : "\u221A", theme.colors.success, colors),
warn: /* @__PURE__ */ applyColor(supported ? "\u26A0" : "\u203C", theme.colors.warning, colors)
};
}
/**
* Get indentation for a specific stream.
* @private
*/
#getIndent(stream) {
const root = this.#getRoot();
return stream === "stderr" ? root.#stderrIndention : root.#stdoutIndention;
}
/**
* Set indentation for a specific stream.
* @private
*/
#setIndent(stream, value) {
const root = this.#getRoot();
if (stream === "stderr") {
root.#stderrIndention = value;
} else {
root.#stdoutIndention = value;
}
}
/**
* Get lastWasBlank state for a specific stream.
* @private
*/
#getLastWasBlank(stream) {
const root = this.#getRoot();
return stream === "stderr" ? root.#stderrLastWasBlank : root.#stdoutLastWasBlank;
}
/**
* Set lastWasBlank state for a specific stream.
* @private
*/
#setLastWasBlank(stream, value) {
const root = this.#getRoot();
if (stream === "stderr") {
root.#stderrLastWasBlank = value;
} else {
root.#stdoutLastWasBlank = value;
}
}
/**
* Get the target stream for this logger instance.
* @private
*/
#getTargetStream() {
return this.#boundStream || "stderr";
}
/**
* Apply a console method with indentation.
* @private
*/
#apply(methodName, args, stream) {
const con = this.#getConsole();
const text = args.at(0);
const hasText = typeof text === "string";
const targetStream = stream || (methodName === "log" ? "stdout" : "stderr");
const indent = this.#getIndent(targetStream);
const logArgs = hasText ? [(0, import_strings.applyLinePrefix)(text, { prefix: indent }), ...args.slice(1)] : args;
ReflectApply(
con[methodName],
con,
logArgs
);
this[lastWasBlankSymbol](hasText && (0, import_strings.isBlankString)(logArgs[0]), targetStream);
this[incLogCallCountSymbol]();
return this;
}
/**
* Strip log symbols from the start of text.
* @private
*/
#stripSymbols(text) {
return text.replace(/^[✖✗×⚠‼✔✓√ℹ→]\uFE0F?\s*/u, "");
}
/**
* Apply a method with a symbol prefix.
* @private
*/
#symbolApply(symbolType, args) {
const con = this.#getConsole();
let text = args.at(0);
let extras;
if (typeof text === "string") {
text = this.#stripSymbols(text);
extras = args.slice(1);
} else {
extras = args;
text = "";
}
const indent = this.#getIndent("stderr");
const symbols = this.#getSymbols();
con.error(
(0, import_strings.applyLinePrefix)(`${symbols[symbolType]} ${text}`, {
prefix: indent
}),
...extras
);
this[lastWasBlankSymbol](false, "stderr");
this[incLogCallCountSymbol]();
return this;
}
/**
* Gets the total number of log calls made on this logger instance.
*
* Tracks all logging method calls including `log()`, `error()`, `warn()`,
* `success()`, `fail()`, etc. Useful for testing and monitoring logging activity.
*
* @returns The number of times logging methods have been called
*
* @example
* ```typescript
* logger.log('Message 1')
* logger.error('Message 2')
* console.log(logger.logCallCount) // 2
* ```
*/
get logCallCount() {
const root = this.#getRoot();
return root.#logCallCount;
}
/**
* Increments the internal log call counter.
*
* This is called automatically by logging methods and should not
* be called directly in normal usage.
*
* @returns The logger instance for chaining
*/
[incLogCallCountSymbol]() {
const root = this.#getRoot();
root.#logCallCount += 1;
return this;
}
/**
* Sets whether the last logged line was blank.
*
* Used internally to track blank lines and prevent duplicate spacing.
* This is called automatically by logging methods.
*
* @param value - Whether the last line was blank
* @param stream - Optional stream to update (defaults to both streams if not bound, or target stream if bound)
* @returns The logger instance for chaining
*/
[lastWasBlankSymbol](value, stream) {
if (stream) {
this.#setLastWasBlank(stream, !!value);
} else if (this.#boundStream) {
this.#setLastWasBlank(this.#boundStream, !!value);
} else {
this.#setLastWasBlank("stderr", !!value);
this.#setLastWasBlank("stdout", !!value);
}
return this;
}
/**
* Logs an assertion failure message if the value is falsy.
*
* Works like `console.assert()` but returns the logger for chaining.
* If the value is truthy, nothing is logged. If falsy, logs an error
* message with an assertion failure.
*
* @param value - The value to test
* @param message - Optional message and additional arguments to log
* @returns The logger instance for chaining
*
* @example
* ```typescript
* logger.assert(true, 'This will not log')
* logger.assert(false, 'Assertion failed: value is false')
* logger.assert(items.length > 0, 'No items found')
* ```
*/
assert(value, ...message) {
const con = this.#getConsole();
con.assert(value, message[0], ...message.slice(1));
this[lastWasBlankSymbol](false);
return value ? this : this[incLogCallCountSymbol]();
}
/**
* Clears the visible terminal screen.
*
* Only available on the main logger instance, not on stream-bound instances
* (`.stderr` or `.stdout`). Resets the log call count and blank line tracking
* if the output is a TTY.
*
* @returns The logger instance for chaining
* @throws {Error} If called on a stream-bound logger instance
*
* @example
* ```typescript
* logger.log('Some output')
* logger.clearVisible() // Screen is now clear
*
* // Error: Can't call on stream-bound instance
* logger.stderr.clearVisible() // throws
* ```
*/
clearVisible() {
if (this.#boundStream) {
throw new Error(
"clearVisible() is only available on the main logger instance, not on stream-bound instances"
);
}
const con = this.#getConsole();
con.clear();
if (con._stdout.isTTY) {
;
this[lastWasBlankSymbol](true);
this.#logCallCount = 0;
}
return this;
}
/**
* Increments and logs a counter for the given label.
*
* Each unique label maintains its own counter. Works like `console.count()`.
*
* @param label - Optional label for the counter
* @default 'default'
* @returns The logger instance for chaining
*
* @example
* ```typescript
* logger.count('requests') // requests: 1
* logger.count('requests') // requests: 2
* logger.count('errors') // errors: 1
* logger.count() // default: 1
* ```
*/
count(label) {
const con = this.#getConsole();
con.count(label);
this[lastWasBlankSymbol](false);
return this[incLogCallCountSymbol]();
}
/**
* Creates a task that logs start and completion messages automatically.
*
* Returns a task object with a `run()` method that executes the provided
* function and logs "Starting task: {name}" before execution and
* "Completed task: {name}" after completion.
*
* @param name - The name of the task
* @returns A task object with a `run()` method
*
* @example
* ```typescript
* const task = logger.createTask('Database Migration')
* const result = task.run(() => {
* // Logs: "Starting task: Database Migration"
* migrateDatabase()
* return 'success'
* // Logs: "Completed task: Database Migration"
* })
* console.log(result) // 'success'
* ```
*/
createTask(name) {
return {
run: (f) => {
this.log(`Starting task: ${name}`);
const result = f();
this.log(`Completed task: ${name}`);
return result;
}
};
}
/**
* Decreases the indentation level by removing spaces from the prefix.
*
* When called on the main logger, affects both stderr and stdout indentation.
* When called on a stream-bound logger (`.stderr` or `.stdout`), affects
* only that stream's indentation.
*
* @param spaces - Number of spaces to remove from indentation
* @default 2
* @returns The logger instance for chaining
*
* @example
* ```typescript
* logger.indent()
* logger.log('Indented')
* logger.dedent()
* logger.log('Back to normal')
*
* // Remove custom amount
* logger.indent(4)
* logger.log('Four spaces')
* logger.dedent(4)
*
* // Stream-specific dedent
* logger.stdout.indent()
* logger.stdout.log('Indented stdout')
* logger.stdout.dedent()
* ```
*/
dedent(spaces = 2) {
if (this.#boundStream) {
const current = this.#getIndent(this.#boundStream);
this.#setIndent(this.#boundStream, current.slice(0, -spaces));
} else {
const stderrCurrent = this.#getIndent("stderr");
const stdoutCurrent = this.#getIndent("stdout");
this.#setIndent("stderr", stderrCurrent.slice(0, -spaces));
this.#setIndent("stdout", stdoutCurrent.slice(0, -spaces));
}
return this;
}
/**
* Displays an object's properties in a formatted way.
*
* Works like `console.dir()` with customizable options for depth,
* colors, etc. Useful for inspecting complex objects.
*
* @param obj - The object to display
* @param options - Optional formatting options (Node.js inspect options)
* @returns The logger instance for chaining
*
* @example
* ```typescript
* const obj = { a: 1, b: { c: 2, d: { e: 3 } } }
* logger.dir(obj)
* logger.dir(obj, { depth: 1 }) // Limit nesting depth
* logger.dir(obj, { colors: true }) // Enable colors
* ```
*/
dir(obj, options) {
const con = this.#getConsole();
con.dir(obj, options);
this[lastWasBlankSymbol](false);
return this[incLogCallCountSymbol]();
}
/**
* Displays data as XML/HTML in a formatted way.
*
* Works like `console.dirxml()`. In Node.js, behaves the same as `dir()`.
*
* @param data - The data to display
* @returns The logger instance for chaining
*
* @example
* ```typescript
* logger.dirxml(document.body) // In browser environments
* logger.dirxml(xmlObject) // In Node.js
* ```
*/
dirxml(...data) {
const con = this.#getConsole();
con.dirxml(data);
this[lastWasBlankSymbol](false);
return this[incLogCallCountSymbol]();
}
/**
* Logs an error message to stderr.
*
* Automatically applies current indentation. All arguments are formatted
* and logged like `console.error()`.
*
* @param args - Message and additional arguments to log
* @returns The logger instance for chaining
*
* @example
* ```typescript
* logger.error('Build failed')
* logger.error('Error code:', 500)
* logger.error('Details:', { message: 'Not found' })
* ```
*/
error(...args) {
return this.#apply("error", args);
}
/**
* Logs a newline to stderr only if the last line wasn't already blank.
*
* Prevents multiple consecutive blank lines. Useful for adding spacing
* between sections without creating excessive whitespace.
*
* @returns The logger instance for chaining
*
* @example
* ```typescript
* logger.error('Error message')
* logger.errorNewline() // Adds blank line
* logger.errorNewline() // Does nothing (already blank)
* logger.error('Next section')
* ```
*/
errorNewline() {
return this.#getLastWasBlank("stderr") ? this : this.error("");
}
/**
* Logs a failure message with a red colored fail symbol.
*
* Automatically prefixes the message with `LOG_SYMBOLS.fail` (red ✖).
* Always outputs to stderr. If the message starts with an existing
* symbol, it will be stripped and replaced.
*
* @param args - Message and additional arguments to log
* @returns The logger instance for chaining
*
* @example
* ```typescript
* logger.fail('Build failed')
* logger.fail('Test suite failed:', { passed: 5, failed: 3 })
* ```
*/
fail(...args) {
return this.#symbolApply("fail", args);
}
/**
* Starts a new indented log group.
*
* If a label is provided, it's logged before increasing indentation.
* Groups can be nested. Each group increases indentation by the
* `kGroupIndentWidth` (default 2 spaces). Call `groupEnd()` to close.
*
* @param label - Optional label to display before the group
* @returns The logger instance for chaining
*
* @example
* ```typescript
* logger.group('Processing files:')
* logger.log('file1.js')
* logger.log('file2.js')
* logger.groupEnd()
*
* // Nested groups
* logger.group('Outer')
* logger.log('Outer content')
* logger.group('Inner')
* logger.log('Inner content')
* logger.groupEnd()
* logger.groupEnd()
* ```
*/
group(...label) {
const { length } = label;
if (length) {
ReflectApply(this.log, this, label);
}
this.indent(this[getKGroupIndentationWidthSymbol()]);
if (length) {
;
this[lastWasBlankSymbol](false);
this[incLogCallCountSymbol]();
}
return this;
}
/**
* Starts a new collapsed log group (alias for `group()`).
*
* In browser consoles, this creates a collapsed group. In Node.js,
* it behaves identically to `group()`.
*
* @param label - Optional label to display before the group
* @returns The logger instance for chaining
*
* @example
* ```typescript
* logger.groupCollapsed('Details')
* logger.log('Hidden by default in browsers')
* logger.groupEnd()
* ```
*/
// groupCollapsed is an alias of group.
// https://nodejs.org/api/console.html#consolegroupcollapsed
groupCollapsed(...label) {
return ReflectApply(this.group, this, label);
}
/**
* Ends the current log group and decreases indentation.
*
* Must be called once for each `group()` or `groupCollapsed()` call
* to properly close the group and restore indentation.
*
* @returns The logger instance for chaining
*
* @example
* ```typescript
* logger.group('Group 1')
* logger.log('Content')
* logger.groupEnd() // Closes 'Group 1'
* ```
*/
groupEnd() {
this.dedent(this[getKGroupIndentationWidthSymbol()]);
return this;
}
/**
* Increases the indentation level by adding spaces to the prefix.
*
* When called on the main logger, affects both stderr and stdout indentation.
* When called on a stream-bound logger (`.stderr` or `.stdout`), affects
* only that stream's indentation. Maximum indentation is 1000 spaces.
*
* @param spaces - Number of spaces to add to indentation
* @default 2
* @returns The logger instance for chaining
*
* @example
* ```typescript
* logger.log('Level 0')
* logger.indent()
* logger.log('Level 1')
* logger.indent()
* logger.log('Level 2')
* logger.dedent()
* logger.dedent()
*
* // Custom indent amount
* logger.indent(4)
* logger.log('Indented 4 spaces')
* logger.dedent(4)
*
* // Stream-specific indent
* logger.stdout.indent()
* logger.stdout.log('Only stdout is indented')
* ```
*/
indent(spaces = 2) {
const spacesToAdd = " ".repeat(Math.min(spaces, maxIndentation));
if (this.#boundStream) {
const current = this.#getIndent(this.#boundStream);
this.#setIndent(this.#boundStream, current + spacesToAdd);
} else {
const stderrCurrent = this.#getIndent("stderr");
const stdoutCurrent = this.#getIndent("stdout");
this.#setIndent("stderr", stderrCurrent + spacesToAdd);
this.#setIndent("stdout", stdoutCurrent + spacesToAdd);
}
return this;
}
/**
* Logs an informational message with a blue colored info symbol.
*
* Automatically prefixes the message with `LOG_SYMBOLS.info` (blue ℹ).
* Always outputs to stderr. If the message starts with an existing
* symbol, it will be stripped and replaced.
*
* @param args - Message and additional arguments to log
* @returns The logger instance for chaining
*
* @example
* ```typescript
* logger.info('Starting build process')
* logger.info('Configuration loaded:', config)
* logger.info('Using cache directory:', cacheDir)
* ```
*/
info(...args) {
return this.#symbolApply("info", args);
}
/**
* Logs a message to stdout.
*
* Automatically applies current indentation. All arguments are formatted
* and logged like `console.log()`. This is the primary method for
* standard output.
*
* @param args - Message and additional arguments to log
* @returns The logger instance for chaining
*
* @example
* ```typescript
* logger.log('Processing complete')
* logger.log('Items processed:', 42)
* logger.log('Results:', { success: true, count: 10 })
*
* // Method chaining
* logger.log('Step 1').log('Step 2').log('Step 3')
* ```
*/
log(...args) {
return this.#apply("log", args);
}
/**
* Logs a newline to stdout only if the last line wasn't already blank.
*
* Prevents multiple consecutive blank lines. Useful for adding spacing
* between sections without creating excessive whitespace.
*
* @returns The logger instance for chaining
*
* @example
* ```typescript
* logger.log('Section 1')
* logger.logNewline() // Adds blank line
* logger.logNewline() // Does nothing (already blank)
* logger.log('Section 2')
* ```
*/
logNewline() {
return this.#getLastWasBlank("stdout") ? this : this.log("");
}
/**
* Resets all indentation to zero.
*
* When called on the main logger, resets both stderr and stdout indentation.
* When called on a stream-bound logger (`.stderr` or `.stdout`), resets
* only that stream's indentation.
*
* @returns The logger instance for chaining
*
* @example
* ```typescript
* logger.indent().indent().indent()
* logger.log('Very indented')
* logger.resetIndent()
* logger.log('Back to zero indentation')
*
* // Reset only stdout
* logger.stdout.resetIndent()
* ```
*/
resetIndent() {
if (this.#boundStream) {
this.#setIndent(this.#boundStream, "");
} else {
this.#setIndent("stderr", "");
this.#setIndent("stdout", "");
}
return this;
}
/**
* Logs a main step message with a cyan arrow symbol and blank line before it.
*
* Automatically prefixes the message with `LOG_SYMBOLS.step` (cyan →) and
* adds a blank line before the message unless the last line was already blank.
* Useful for marking major steps in a process with clear visual separation.
* Always outputs to stdout. If the message starts with an existing symbol,
* it will be stripped and replaced.
*
* @param msg - The step message to log
* @param extras - Additional arguments to log
* @returns The logger instance for chaining
*
* @example
* ```typescript
* logger.step('Building project')
* logger.log('Compiling TypeScript...')
* logger.step('Running tests')
* logger.log('Running test suite...')
* // Output:
* // [blank line]
* // → Building project
* // Compiling TypeScript...
* // [blank line]
* // → Running tests
* // Running test suite...
* ```
*/
step(msg, ...extras) {
if (!this.#getLastWasBlank("stdout")) {
this.log("");
}
const text = this.#stripSymbols(msg);
const indent = this.#getIndent("stdout");
const symbols = this.#getSymbols();
const con = this.#getConsole();
con.log(
(0, import_strings.applyLinePrefix)(`${symbols.step} ${text}`, {
prefix: indent
}),
...extras
);
this[lastWasBlankSymbol](false, "stdout");
this[incLogCallCountSymbol]();
return this;
}
/**
* Logs an indented substep message (stateless).
*
* Adds a 2-space indent to the message without affecting the logger's
* indentation state. Useful for showing sub-items under a main step.
*
* @param msg - The substep message to log
* @param extras - Additional arguments to log
* @returns The logger instance for chaining
*
* @example
* ```typescript
* logger.log('Installing dependencies:')
* logger.substep('Installing react')
* logger.substep('Installing typescript')
* logger.substep('Installing eslint')
* // Output:
* // Installing dependencies:
* // Installing react
* // Installing typescript
* // Installing eslint
* ```
*/
substep(msg, ...extras) {
const indentedMsg = ` ${msg}`;
return this.log(indentedMsg, ...extras);
}
/**
* Logs a success message with a green colored success symbol.
*
* Automatically prefixes the message with `LOG_SYMBOLS.success` (green ✔).
* Always outputs to stderr. If the message starts with an existing
* symbol, it will be stripped and replaced.
*
* @param args - Message and additional arguments to log
* @returns The logger instance for chaining
*
* @example
* ```typescript
* logger.success('Build completed')
* logger.success('Tests passed:', { total: 42, passed: 42 })
* logger.success('Deployment successful')
* ```
*/
success(...args) {
return this.#symbolApply("success", args);
}
/**
* Logs a completion message with a success symbol (alias for `success()`).
*
* Provides semantic clarity when marking something as "done". Does NOT
* automatically clear the current line - call `clearLine()` first if
* needed after using `progress()`.
*
* @param args - Message and additional arguments to log
* @returns The logger instance for chaining
*
* @example
* ```typescript
* logger.done('Task completed')
*
* // After progress indicator
* logger.progress('Processing...')
* // ... do work ...
* logger.clearLine()
* logger.done('Processing complete')
* ```
*/
done(...args) {
return this.#symbolApply("success", args);
}
/**
* Displays data in a table format.
*
* Works like `console.table()`. Accepts arrays of objects or
* objects with nested objects. Optionally specify which properties
* to include in the table.
*
* @param tabularData - The data to display as a table
* @param properties - Optional array of property names to include
* @returns The logger instance for chaining
*
* @example
* ```typescript
* // Array of objects
* logger.table([
* { name: 'Alice', age: 30 },
* { name: 'Bob', age: 25 }
* ])
*
* // Specify properties to show
* logger.table(users, ['name', 'email'])
*
* // Object with nested objects
* logger.table({
* user1: { name: 'Alice', age: 30 },
* user2: { name: 'Bob', age: 25 }
* })
* ```
*/
table(tabularData, properties) {
const con = this.#getConsole();
con.table(tabularData, properties);
this[lastWasBlankSymbol](false);
return this[incLogCallCountSymbol]();
}
/**
* Starts a timer for measuring elapsed time.
*
* Creates a timer with the given label. Use `timeEnd()` with the same
* label to stop the timer and log the elapsed time, or use `timeLog()`
* to check the time without stopping the timer.
*
* @param label - Optional label for the timer
* @default 'default'
* @returns The logger instance for chaining
*
* @example
* ```typescript
* logger.time('operation')
* // ... do work ...
* logger.timeEnd('operation')
* // Logs: "operation: 123.456ms"
*
* logger.time()
* // ... do work ...
* logger.timeEnd()
* // Logs: "default: 123.456ms"
* ```
*/
time(label) {
const con = this.#getConsole();
con.time(label);
return this;
}
/**
* Ends a timer and logs the elapsed time.
*
* Logs the duration since `console.time()` or `logger.time()` was called
* with the same label. The timer is stopped and removed.
*
* @param label - Optional label for the timer
* @default 'default'
* @returns The logger instance for chaining
*
* @example
* ```typescript
* logger.time('operation')
* // ... do work ...
* logger.timeEnd('operation')
* // Logs: "operation: 123.456ms"
*
* logger.time()
* // ... do work ...
* logger.timeEnd()
* // Logs: "default: 123.456ms"
* ```
*/
timeEnd(label) {
const con = this.#getConsole();
con.timeEnd(label);
this[lastWasBlankSymbol](false);
return this[incLogCallCountSymbol]();
}
/**
* Logs the current value of a timer without stopping it.
*
* Logs the duration since `console.time()` was called with the same
* label, but keeps the timer running. Can include additional data
* to log alongside the time.
*
* @param label - Optional label for the timer
* @param data - Additional data to log with the time
* @default 'default'
* @returns The logger instance for chaining
*
* @example
* ```typescript
* console.time('process')
* // ... partial work ...
* logger.timeLog('process', 'Checkpoint 1')
* // Logs: "process: 123.456ms Checkpoint 1"
* // ... more work ...
* logger.timeLog('process', 'Checkpoint 2')
* // Logs: "process: 234.567ms Checkpoint 2"
* console.timeEnd('process')
* ```
*/
timeLog(label, ...data) {
const con = this.#getConsole();
con.timeLog(label, ...data);
this[lastWasBlankSymbol](false);
return this[incLogCallCountSymbol]();
}
/**
* Logs a stack trace to the console.
*
* Works like `console.trace()`. Shows the call stack leading to
* where this method was called. Useful for debugging.
*
* @param message - Optional message to display with the trace
* @param args - Additional arguments to log
* @returns The logger instance for chaining
*
* @example
* ```typescript
* function debugFunction() {
* logger.trace('Debug point reached')
* }
*
* logger.trace('Trace from here')
* logger.trace('Error context:', { userId: 123 })
* ```
*/
trace(message, ...args) {
const con = this.#getConsole();
con.trace(message, ...args);
this[lastWasBlankSymbol](false);
return this[incLogCallCountSymbol]();
}
/**
* Logs a warning message with a yellow colored warning symbol.
*
* Automatically prefixes the message with `LOG_SYMBOLS.warn` (yellow ⚠).
* Always outputs to stderr. If the message starts with an existing
* symbol, it will be stripped and replaced.
*
* @param args - Message and additional arguments to log
* @returns The logger instance for chaining
*
* @example
* ```typescript
* logger.warn('Deprecated API used')
* logger.warn('Low memory:', { available: '100MB' })
* logger.warn('Missing optional configuration')
* ```
*/
warn(...args) {
return this.#symbolApply("warn", args);
}
/**
* Writes text directly to stdout without a newline or indentation.
*
* Useful for progress indicators or custom formatting where you need
* low-level control. Does not apply any indentation or formatting.
*
* @param text - The text to write
* @returns The logger instance for chaining
*
* @example
* ```typescript
* logger.write('Processing... ')
* // ... do work ...
* logger.write('done\n')
*
* // Build a line incrementally
* logger.write('Step 1')
* logger.write('... Step 2')
* logger.write('... Step 3\n')
* ```
*/
write(text) {
const con = this.#getConsole();
const ctorArgs = privateConstructorArgs.get(this) ?? [];
const stdout = this.#originalStdout || ctorArgs[0]?.stdout || con._stdout;
stdout.write(text);
this[lastWasBlankSymbol](false);
return this;
}
/**
* Shows a progress indicator that can be cleared with `clearLine()`.
*
* Displays a simple status message with a '∴' prefix. Does not include
* animation or spinner. Intended to be cleared once the operation completes.
* The output stream (stderr or stdout) depends on whether the logger is
* stream-bound.
*
* @param text - The progress message to display
* @returns The logger instance for chaining
*
* @example
* ```typescript
* logger.progress('Processing files...')
* // ... do work ...
* logger.clearLine()
* logger.success('Files processed')
*
* // Stream-specific progress
* logger.stdout.progress('Loading...')
* // ... do work ...
* logger.stdout.clearLine()
* logger.stdout.log('Done')
* ```
*/
progress(text) {
const con = this.#getConsole();
const stream = this.#getTargetStream();
const streamObj = stream === "stderr" ? con._stderr : con._stdout;
streamObj.write(`\u2234 ${text}`);
this[lastWasBlankSymbol](false);
return this;
}
/**
* Clears the current line in the terminal.
*
* Moves the cursor to the beginning of the line and clears all content.
* Works in both TTY and non-TTY environments. Useful for clearing
* progress indicators created with `progress()`.
*
* The stream to clear (stderr or stdout) depends on whether the logger
* is stream-bound.
*
* @returns The logger instance for chaining
*
* @example
* ```typescript
* logger.progress('Loading...')
* // ... do work ...
* logger.clearLine()
* logger.success('Loaded')
*
* // Clear multiple progress updates
* for (const file of files) {
* logger.progress(`Processing ${file}`)
* processFile(file)
* logger.clearLine()
* }
* logger.success('All files processed')
* ```
*/
clearLine() {
const con = this.#getConsole();
const stream = this.#getTargetStream();
const streamObj = stream === "stderr" ? con._stderr : con._stdout;
if (streamObj.isTTY) {
streamObj.cursorTo(0);
streamObj.clearLine(0);
} else {
streamObj.write("\r\x1B[K");
}
return this;
}
}
let _prototypeInitialized = false;
function ensurePrototypeInitialized() {
if (_prototypeInitialized) {
return;
}
_prototypeInitialized = true;
const entries = [
[
getKGroupIndentationWidthSymbol(),
{
...consolePropAttributes,
value: 2
}
],
[
Symbol.toStringTag,
{
__proto__: null,
configurable: true,
value: "logger"
}
]
];
for (const { 0: key, 1: value } of Object.entries(globalConsole)) {
if (!Logger.prototype[key] && typeof value === "function") {
const { [key]: func } = {
[key](...args) {
let con = privateConsole.get(this);
if (con === void 0) {
const ctorArgs = privateConstructorArgs.get(this) ?? [];
privateConstructorArgs.delete(this);
if (ctorArgs.length) {
con = /* @__PURE__ */ constructConsole(...ctorArgs);
} else {
con = /* @__PURE__ */ constructConsole({
stdout: process.stdout,
stderr: process.stderr
});
for (const { 0: k, 1: method } of boundConsoleEntries) {
con[k] = method;
}
}
privateConsole.set(this, con);
}
const result = con[key](...args);
return result === void 0 || result === con ? this : result;
}
};
entries.push([
key,
{
...consolePropAttributes,
value: func
}
]);
}
}
Object.defineProperties(Logger.prototype, Object.fromEntries(entries));
}
let _logger;
function getDefaultLogger() {
if (_logger === void 0) {
_logger = new Logger();
}
return _logger;
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
LOG_SYMBOLS,
Logger,
getDefaultLogger,
incLogCallCountSymbol,
lastWasBlankSymbol
});