UNPKG

@bluecadet/launchpad-cli

Version:
204 lines 7.21 kB
import { sep } from "node:path"; import { formatWithOptions } from "node:util"; import ansiEscapes from "ansi-escapes"; import chalk, {} from "chalk"; import stringWidth from "string-width"; import { forwardLog } from "./detached-messaging.js"; const LEVEL_COLORS = { info: chalk.green, debug: chalk.cyan, warn: chalk.yellow, error: chalk.red, verbose: chalk.magenta, }; const LEVEL_BG_COLORS = { info: chalk.bgGreen.black, debug: chalk.bgCyan.black, warn: chalk.bgYellow.black, error: chalk.bgRed.black, verbose: chalk.bgMagenta.black, }; /** * Parses a stack trace string and normalises its paths by removing the current working directory and the "file://" protocol. * @param {string} stack - The stack trace string. * @returns {string[]} An array of stack trace lines with normalised paths. */ function parseStack(stack, message) { const cwd = process.cwd() + sep; const lines = stack .split("\n") .splice(message.split("\n").length) .map((l) => l.trim().replace("file://", "").replace(cwd, "")); return lines; } function formatStack(stack, message, errorLevel = 0) { const indent = " ".repeat(errorLevel + 1); return (`\n${indent}` + parseStack(stack, message) .map((line) => " " + line .replace(/^at +/, (m) => chalk.gray(m)) .replace(/\((.+)\)/, (_, m) => `(${chalk.cyan(m)})`)) .join(`\n${indent}`)); } function formatErr(err, errorLevel = 0) { const message = err.message; const stack = err.stack ? formatStack(err.stack, message, errorLevel) : ""; const level = errorLevel || 0; const causedPrefix = level > 0 ? `${" ".repeat(level)}[cause]: ` : ""; const causedError = err.cause instanceof Error ? `\n\n${formatErr(err.cause, level + 1)}` : ""; return `${causedPrefix + message}\n${stack}${causedError}`; } function formatLevelPrefix(level, isBadge = false) { if (isBadge) { return LEVEL_BG_COLORS[level](` ${level.toUpperCase()} `); } return LEVEL_COLORS[level](`${level}:`); } function formatDate(date) { return date.toLocaleTimeString(); } /** * Splits a message into multiple lines based on the given width. * Preserves word boundaries where possible. * Respect newline characters. */ function splitLines(message, widthFirstLine, widthAdditionalLines) { if (!message) { return [""]; } // First split by existing newlines to respect them const paragraphs = message.split("\n"); const result = []; for (let i = 0; i < paragraphs.length; i++) { const paragraph = paragraphs[i]; const isFirstParagraph = i === 0 && result.length === 0; const maxWidth = isFirstParagraph ? widthFirstLine : widthAdditionalLines; if (stringWidth(paragraph) <= maxWidth) { result.push(paragraph); } else { // Need to wrap this paragraph const words = paragraph.split(" "); let currentLine = ""; for (const word of words) { const testLine = currentLine ? `${currentLine} ${word}` : word; const currentMaxWidth = result.length === 0 && isFirstParagraph ? widthFirstLine : widthAdditionalLines; if (stringWidth(testLine) <= currentMaxWidth) { currentLine = testLine; } else { // Current line is full, start a new one if (currentLine) { result.push(currentLine); } currentLine = word; } } // Add the last line if there's content if (currentLine) { result.push(currentLine); } } } return result.length > 0 ? result : [""]; } function formatLogObj(level, payload) { const date = chalk.gray(formatDate(new Date())); const module = payload.module ? `${chalk.gray(payload.module)} ` : ""; const isBadge = level === "error" || level === "warn"; const left = formatLevelPrefix(level, isBadge); const right = `${module}${date}`; const availableSpace = process.stdout.columns - stringWidth(left) - stringWidth(right) - 2; // 2 for spaces const [message, ...additionalLines] = splitLines(formatArgs(payload.args), availableSpace, process.stdout.columns - 4); // justify right side all the way to the right const spaceRight = availableSpace - stringWidth(message) + 1; let formatted = `${left} ${message}${" ".repeat(spaceRight)}${right}`; for (const line of additionalLines) { formatted += `\n ${line}`; } // Add extra padding for badge style return isBadge ? `\n${formatted}\n` : formatted; } function formatArgs(args) { const _args = args.map((arg) => { if (arg instanceof Error) { return formatErr(arg); } return arg; }); return formatWithOptions({ colors: false, compact: true }, ..._args); } const LOG_LEVEL_TO_NUM = { error: 0, warn: 1, info: 2, verbose: 3, debug: 4, }; let configuredLogLevel = LOG_LEVEL_TO_NUM.info; function setLevel(level) { configuredLogLevel = LOG_LEVEL_TO_NUM[level]; } let lastFixedMessage = null; function logFixedMessage(message) { if (lastFixedMessage === null) { // hide cursor when displaying fixed message process.stdout.write(ansiEscapes.cursorHide); } if (message === null) { process.stdout.write(ansiEscapes.cursorShow); } else { const lastLineCount = lastFixedMessage?.split("\n").length || 0; process.stdout.write(ansiEscapes.eraseLines(lastLineCount) + message); // erase last message and show new one } lastFixedMessage = message; } process.on("beforeExit", () => { if (lastFixedMessage !== null) { // ensure cursor is shown again process.stdout.write(ansiEscapes.cursorShow); } }); function logFromPayload(level, payload) { if (LOG_LEVEL_TO_NUM[level] > configuredLogLevel) { return; } const formatted = formatLogObj(level, payload); // Forward log to parent process if detached forwardLog(level, payload); if (lastFixedMessage !== null) { // erase fixed message before logging const lastLineCount = lastFixedMessage.split("\n").length; process.stdout.write(ansiEscapes.eraseLines(lastLineCount)); } const formattedWithFixed = lastFixedMessage ? `${formatted}\n${lastFixedMessage}` : `${formatted}\n`; if (level === "error" || level === "warn") { process.stderr.write(formattedWithFixed); } else { process.stdout.write(formattedWithFixed); } } /** * for a console-like API */ function log(level, ...args) { logFromPayload(level, { args, }); } export const cliLogger = { debug: log.bind(null, "debug"), info: log.bind(null, "info"), warn: log.bind(null, "warn"), error: log.bind(null, "error"), verbose: log.bind(null, "verbose"), fromPayload: logFromPayload, setLevel, fixed: logFixedMessage, }; //# sourceMappingURL=cli-logger.js.map