Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
b366755
util: port node v26.3.0 styleText, getCallSites, and regexp highlighting
cirospaciari Jul 17, 2026
05fc2be
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 17, 2026
4fdad1f
util: address review — fix boxed-string deepEquals perf and getCallSi…
cirospaciari Jul 17, 2026
e4914bf
util: fix CI failures — lint, stale styleText assertions, styletext T…
cirospaciari Jul 17, 2026
521eb17
util: vm module namespaces get a null prototype, port v26 inspect fixes
cirospaciari Jul 17, 2026
b63bf90
util: address review — keep loose deepEquals on node's behavior, lazy…
cirospaciari Jul 17, 2026
6dd7fa7
util: fix lint, sync the vendored inspect test to v26's bracketed get…
cirospaciari Jul 17, 2026
8072986
util: give the styletext tests a deterministic color environment
cirospaciari Jul 17, 2026
7491ac7
util: keep hex coverage bun-side, gate the wrapper check on ObjectType
cirospaciari Jul 17, 2026
e6dfacd
fs: don't throw from WriteStream when the fd has no writable sink
cirospaciari Jul 17, 2026
7ff738e
Merge branch 'main' into claude/node-util-v26-compat
cirospaciari Jul 17, 2026
be5cfc8
util: address review findings from the merge round
robobun Jul 17, 2026
f626428
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 17, 2026
9151219
util: drop upstream's speculative groupType note from highlightRegExp
robobun Jul 17, 2026
c46c84b
Merge branch 'main' into claude/node-util-v26-compat
cirospaciari Jul 19, 2026
2a1ad47
deepEquals: run byte checks before routing typed-arrays-with-extras t…
robobun Jul 19, 2026
3afe0ab
Merge branch 'main' into claude/node-util-v26-compat
dylan-conway Jul 21, 2026
55ab9dc
Merge remote-tracking branch 'origin/main' into claude/node-util-v26-…
robobun Jul 22, 2026
7b1d0c7
Merge branch 'main' into claude/node-util-v26-compat
cirospaciari Jul 24, 2026
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
6 changes: 6 additions & 0 deletions scripts/runner.node.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -758,6 +758,12 @@ async function runTests() {
NO_COLOR: "1",
BUN_DEBUG_QUIET_LOGS: "1",
};
if (title.includes("test-util-styletext")) {
// These assert styleText's own color decisions against a TTY, so they
// have to see the real environment instead of the forced no-color one.
delete env.FORCE_COLOR;
delete env.NO_COLOR;
}
if (!isWindows && title.includes("/sequential/")) {
// Sequential node tests share common.PORT (12346); a cluster worker
// or child_process subprocess that outlives its test can keep that
Expand Down
2 changes: 2 additions & 0 deletions src/js/builtins/UtilInspect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ export function getStylizeWithColor(inspect: Inspect) {
return function stylizeWithColor(str: string, styleType: string) {
const style = inspect.styles[styleType];
if (style !== undefined) {
// inspect.styles.regexp is a function (highlightRegExp), not a color name.
if (typeof style === "function") return style(str);
const color = inspect.colors[style];
if (color !== undefined) return `\u001b[${color[0]}m${str}\u001b[${color[1]}m`;
}
Expand Down
243 changes: 241 additions & 2 deletions src/js/internal/util/inspect.js
Original file line number Diff line number Diff line change
Expand Up @@ -870,11 +870,15 @@ inspect.styles = {
symbol: "green",
date: "magenta",
// "name": intentionally not styling
// TODO(BridgeAR): Highlight regular expressions properly.
regexp: "red",
regexp: highlightRegExp,
Comment thread
robobun marked this conversation as resolved.
module: "underline",
};

// Define the palette for RegExp group depth highlighting. Can be changed by users.
inspect.styles.regexp.colors = ["green", "red", "yellow", "cyan", "magenta"];
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const highlightRegExpColors = inspect.styles.regexp.colors.slice();
Comment thread
coderabbitai[bot] marked this conversation as resolved.

function addQuotes(str, quotes) {
if (quotes === -1) {
return `"${str}"`;
Expand Down Expand Up @@ -952,9 +956,244 @@ function strEscape(str) {
return addQuotes(result, singleQuote);
}

function highlightRegExp(regexpString) {
const length = regexpString.length;
let out = "";
let i = 0;
let depth = 0;
let inClass = false;
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// TODO(BridgeAR): Add group type tracking. That allows to increase the depth
// in case the same type is next to each other.
// let groupType = 0;

// Verify palette and update cache if user changed colors
const paletteNames = highlightRegExp.colors?.length > 0 ? highlightRegExp.colors : highlightRegExpColors;

const palette = [];
for (const name of paletteNames) {
const color = inspect.colors[name];
if (color) palette.push([`\u001b[${color[0]}m`, `\u001b[${color[1]}m`]);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
robobun marked this conversation as resolved.

function writeGroup(start, end, decreaseDepth = 1) {
let seq = "";
i++;
// Only checking for the closing delimiter is a fast heuristic for regular
// expressions without the u or v flag. A safer check would verify that the
// read characters are all alphanumeric.
while (i < length && regexpString[i] !== end) {
seq += regexpString[i++];
}
if (i < length) {
depth -= decreaseDepth;
write(start);
writeDepth(seq, 1, 1);
write(end);
depth += decreaseDepth;
} else {
// The group is not closed which would lead to mistakes in the output.
// This is a workaround to prevent output from being corrupted.
writeDepth(start, 1, -seq.length);
}
}

function write(str) {
const idx = depth % palette.length;
// Safeguard against bugs in the implementation.
const color = palette[idx] ?? palette[0];
out += color[0] + str + color[1];
}

function writeDepth(str, incDepth, incI) {
depth += incDepth;
write(str);
depth -= incDepth;
i += incI;
}

// Opening '/'
write("/");
depth++;
i = 1;

// Parse pattern until next unescaped '/'
while (i < length) {
const ch = regexpString[i];

if (inClass) {
if (ch === "\\") {
let seq = "\\";
i++;
if (i < length) {
seq += regexpString[i++];
const next = seq[1];
if (next === "u" && regexpString[i] === "{") {
writeGroup(`${seq}{`, "}", 0);
continue;
} else if ((next === "p" || next === "P") && regexpString[i] === "{") {
writeGroup(`${seq}{`, "}", 0);
continue;
} else if (seq[1] === "x") {
seq += regexpString.slice(i, i + 2);
i += 2;
}
}
write(seq);
} else if (ch === "]") {
depth--;
write("]");
i++;
inClass = false;
} else if (ch === "-" && regexpString[i - 1] !== "[" && i + 1 < length && regexpString[i + 1] !== "]") {
writeDepth("-", 1, 1);
} else {
write(ch);
i++;
}
} else if (ch === "[") {
// Enter class
write("[");
depth++;
i++;
inClass = true;
} else if (ch === "(") {
write("(");
depth++;
i++;
if (i < length && regexpString[i] === "?") {
// Assertions and named groups
i++;
const a = i < length ? regexpString[i] : "";
if (a === ":" || a === "=" || a === "!") {
writeDepth(`?${a}`, -1, 1);
} else {
const b = i + 1 < length ? regexpString[i + 1] : "";
if (a === "<" && (b === "=" || b === "!")) {
writeDepth(`?<${b}`, -1, 2);
} else if (a === "<") {
// Named capture: write '?<name>' as a single colored token
i++; // consume '<'
const start = i;
while (i < length && regexpString[i] !== ">") {
i++;
}
const name = regexpString.slice(start, i);
if (i < length && regexpString[i] === ">") {
depth--;
write("?<");
writeDepth(name, 1, 0);
write(">");
depth++;
i++;
} else {
writeDepth("?<", -1, 0);
write(name);
}
} else {
write("?");
}
}
}
} else if (ch === ")") {
depth--;
write(")");
i++;
} else if (ch === "\\") {
let seq = "\\";
i++;
if (i < length) {
seq += regexpString[i++];
const next = seq[1];
if (i < length) {
if (next === "u" && regexpString[i] === "{") {
writeGroup(`${seq}{`, "}", 0);
continue;
} else if (next === "x") {
seq += regexpString.slice(i, i + 2);
i += 2;
} else if (next >= "0" && next <= "9") {
while (i < length && regexpString[i] >= "0" && regexpString[i] <= "9") {
seq += regexpString[i++];
}
} else if (next === "k" && regexpString[i] === "<") {
writeGroup(`${seq}<`, ">");
continue;
} else if ((next === "p" || next === "P") && regexpString[i] === "{") {
// Unicode properties
writeGroup(`${seq}{`, "}", 0);
continue;
}
}
}
writeDepth(seq, 1, 0);
} else if (ch === "|" || ch === "+" || ch === "*" || ch === "?" || ch === "," || ch === "^" || ch === "$") {
writeDepth(ch, 3, 1);
} else if (ch === "{") {
i++;
let digits = "";
while (i < length && regexpString[i] >= "0" && regexpString[i] <= "9") {
digits += regexpString[i++];
}
if (digits) {
write("{");
depth++;
writeDepth(digits, 1, 0);
}
if (i < length) {
if (regexpString[i] === ",") {
if (!digits) {
write("{");
depth++;
}
write(",");
i++;
} else if (!digits) {
depth += 1;
write("{");
depth -= 1;
continue;
}
}
let digits2 = "";
while (i < length && regexpString[i] >= "0" && regexpString[i] <= "9") {
digits2 += regexpString[i++];
}
if (digits2) {
writeDepth(digits2, 1, 0);
}
if (i < length && regexpString[i] === "}") {
depth--;
write("}");
i++;
}
if (i < length && regexpString[i] === "?") {
writeDepth("?", 3, 1);
}
} else if (ch === ".") {
writeDepth(ch, 2, 1);
} else if (ch === "/") {
// Stop at closing delimiter (unescaped, outside of character class)
break;
} else {
writeDepth(ch, 1, 1);
}
}

// Closing delimiter and flags
writeDepth("/", -1, 1);
if (i < length) {
write(regexpString.slice(i));
}
return out;
}

function stylizeWithColor(str, styleType) {
const style = inspect.styles[styleType];
if (style !== undefined) {
// Checked first: a function style (regexp) would otherwise be stringified
// into a property key on every lookup.
if (typeof style === "function") return style(str);
const color = inspect.colors[style];
if (color !== undefined) return `\u001b[${color[0]}m${str}\u001b[${color[1]}m`;
}
Expand Down
Loading
Loading