UNPKG

@socketsecurity/lib

Version:

Core utilities and infrastructure for Socket.dev security tools

1,455 lines (1,454 loc) 43.8 kB
"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 });