diff --git a/src/reporters/fancy.ts b/src/reporters/fancy.ts index 3c58b277..df93a954 100644 --- a/src/reporters/fancy.ts +++ b/src/reporters/fancy.ts @@ -113,8 +113,24 @@ export class FancyReporter extends BasicReporter { let line; const left = this.filterAndJoin([type, characterFormat(message)]); const right = this.filterAndJoin(opts.columns ? [tag, coloredDate] : [tag]); + + // string-width reports some consola icons (ℹ ✔ ✖ ⚠) as width 2 + // (East Asian Ambiguous) but they render as width 1 in most terminals. + // Other icons (◐ →) are correctly reported as width 1. This inconsistency + // causes the right-aligned date/tag to be misaligned between log types. + // Correct the discrepancy by subtracting the overestimate. + // https://github.com/unjs/consola/issues/394 + const typeStripped = stripAnsi(type); + const iconWidthDiff = typeStripped + ? stringWidth(typeStripped) - typeStripped.length + : 0; + const space = - (opts.columns || 0) - stringWidth(left) - stringWidth(right) - 2; + (opts.columns || 0) - + stringWidth(left) - + stringWidth(right) - + 2 + + iconWidthDiff; line = space > 0 && (opts.columns || 0) >= 80 diff --git a/test/consola.test.ts b/test/consola.test.ts index ae9e177f..68b48d8a 100644 --- a/test/consola.test.ts +++ b/test/consola.test.ts @@ -1,5 +1,7 @@ import { describe, test, expect } from "vitest"; import { ConsolaReporter, LogLevels, LogObject, createConsola } from "../src"; +import { FancyReporter } from "../src/reporters/fancy"; +import { stripAnsi } from "../src/utils/string"; describe("consola", () => { test("can set level", () => { @@ -56,6 +58,31 @@ describe("consola", () => { expect(logs.at(-1)!.args).toEqual(["SPAM", "(repeated 4 times)"]); }); + + test("fancy reporter aligns right-side content across log types (#394)", () => { + const reporter = new FancyReporter(); + const opts = { columns: 120, date: true }; + + const lines = ["info", "success", "start"].map((type) => { + const logObj: LogObject = { + type, + level: 3, + tag: "", + args: ["test message"], + date: new Date("2025-01-01T00:00:00.000Z"), + } as any; + return reporter.formatLogObj(logObj, opts as any); + }); + + // Extract the position of the date string in each line + const positions = lines.map((line) => { + return stripAnsi(line).lastIndexOf("12:00:00 AM"); + }); + + // All positions must be equal (right-aligned) + expect(positions[0]).toBe(positions[1]); + expect(positions[1]).toBe(positions[2]); + }); }); function wait(delay) {