Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 11 additions & 12 deletions src/utils/box.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import _stringWidth from "string-width";
import { getColor } from "./color";
import { stripAnsi } from "./string";

function stringWidth(str: string) {
return _stringWidth(stripAnsi(str));
}
Comment on lines +1 to +7

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

# Verify all string-width call sites and whether they consistently guard Intl.Segmenter.
rg -n -C3 --type=ts '(_stringWidth|stringWidth\s*\(|Intl\.Segmenter)'

echo
# Verify declared string-width version and Node engine constraints.
fd -i '^(package\.json|pnpm-lock\.yaml)$' -d 3 -x sh -c '
  echo "--- $1"
  rg -n '\''(string-width|"node"|engines)'\'' "$1" || true
' sh {}

Repository: unjs/consola

Length of output: 16465


Add Intl.Segmenter guard to the stringWidth helper in src/utils/box.ts.

The new stringWidth helper calls _stringWidth unconditionally, while src/reporters/fancy.ts protects the same dependency with a runtime guard for missing ICU support. The inconsistency risks runtime failures in limited-ICU runtimes that the project already mitigates elsewhere.

🛡️ Proposed fix
 import _stringWidth from "string-width";
 import { getColor } from "./color";
 import { stripAnsi } from "./string";
 
 function stringWidth(str: string) {
+  // https://github.com/unjs/consola/issues/204
+  if (typeof Intl !== "object" || !Intl.Segmenter) {
+    return stripAnsi(str).length;
+  }
   return _stringWidth(stripAnsi(str));
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import _stringWidth from "string-width";
import { getColor } from "./color";
import { stripAnsi } from "./string";
function stringWidth(str: string) {
return _stringWidth(stripAnsi(str));
}
import _stringWidth from "string-width";
import { getColor } from "./color";
import { stripAnsi } from "./string";
function stringWidth(str: string) {
// https://github.com/unjs/consola/issues/204
if (typeof Intl !== "object" || !Intl.Segmenter) {
return stripAnsi(str).length;
}
return _stringWidth(stripAnsi(str));
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/box.ts` around lines 1 - 7, The stringWidth helper currently calls
_stringWidth unconditionally which will fail on limited-ICU runtimes; update the
stringWidth function to guard with a runtime check (e.g., typeof Intl !==
"undefined" && typeof Intl.Segmenter !== "undefined") and only call
_stringWidth(stripAnsi(str)) when the guard passes; otherwise return a safe
fallback such as stripAnsi(str).length (or an equivalent simple character-count)
so callers of stringWidth (referencing the stringWidth function and _stringWidth
import) remain robust in environments without Intl.Segmenter.


export type BoxBorderStyle = {
/**
* Top left corner
Expand Down Expand Up @@ -255,11 +260,10 @@ export function box(text: string, _opts: BoxOpts = {}) {
const paddingOffset =
opts.style.padding % 2 === 0 ? opts.style.padding : opts.style.padding + 1;
const height = textLines.length + paddingOffset;
const titleWidth = opts.title ? stringWidth(opts.title) : 0;
const width =
Math.max(
...textLines.map((line) => stripAnsi(line).length),
opts.title ? stripAnsi(opts.title).length : 0,
) + paddingOffset;
Math.max(...textLines.map((line) => stringWidth(line)), titleWidth) +
paddingOffset;
Comment on lines +263 to +266
const widthOffset = width + paddingOffset;

const leftSpace =
Expand All @@ -272,14 +276,9 @@ export function box(text: string, _opts: BoxOpts = {}) {
// Include the title if it exists with borders
if (opts.title) {
const title = _color ? _color(opts.title) : opts.title;
const left = borderStyle.h.repeat(
Math.floor((width - stripAnsi(opts.title).length) / 2),
);
const left = borderStyle.h.repeat(Math.floor((width - titleWidth) / 2));
const right = borderStyle.h.repeat(
width -
stripAnsi(opts.title).length -
stripAnsi(left).length +
paddingOffset,
width - titleWidth - stringWidth(left) + paddingOffset,
);
boxLines.push(
`${leftSpace}${borderStyle.tl}${left}${title}${right}${borderStyle.tr}`,
Expand Down Expand Up @@ -312,7 +311,7 @@ export function box(text: string, _opts: BoxOpts = {}) {
// Text line
const line = textLines[i - valignOffset];
const left = " ".repeat(paddingOffset);
const right = " ".repeat(width - stripAnsi(line).length);
const right = " ".repeat(width - stringWidth(line));
boxLines.push(
`${leftSpace}${borderStyle.v}${left}${line}${right}${borderStyle.v}`,
);
Expand Down
21 changes: 21 additions & 0 deletions test/box.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import stringWidth from "string-width";
import { describe, expect, test } from "vitest";
import { box } from "../src/utils/box";

describe("box", () => {
test.each(["hello 🚀️ world", "漢字 test"])(
"keeps all rendered lines aligned for %s",
(text) => {
const lines = box(text, {
style: {
marginTop: 0,
marginBottom: 0,
},
}).split("\n");

const widths = lines.map((line) => stringWidth(line));

expect(widths.every((width) => width === widths[0])).toBe(true);
},
);
});
Loading