From e5a3254b7033db87da422a1e03e323a999672f1d Mon Sep 17 00:00:00 2001 From: tin Date: Thu, 13 Aug 2026 21:37:54 +0700 Subject: [PATCH] fix(fancy): align right-side content across log types (#394) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit string-width reports East Asian Ambiguous icons inconsistently: ℹ (info) and ✔ (success) as width 2, but ◐ (start) as width 1. All render as width 1 in most terminals. This inconsistency caused the right-aligned date/tag to be misaligned by 1 character between log types using different icons. Fix: calculate the icon width discrepancy (stringWidth vs actual character length) and compensate in the space calculation. This normalizes all icons to their actual terminal display width without changing the icons themselves. - 1 regression test: verify date position is equal across info/success/start - All 4 tests pass, lint clean --- src/reporters/fancy.ts | 18 +++++++++++++++++- test/consola.test.ts | 27 +++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) 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) {