diff --git a/src/bun.js/bindings/BunProcess.cpp b/src/bun.js/bindings/BunProcess.cpp index d6cb4cc993..964098140c 100644 --- a/src/bun.js/bindings/BunProcess.cpp +++ b/src/bun.js/bindings/BunProcess.cpp @@ -1617,8 +1617,12 @@ static JSValue constructStdioWriteStream(JSC::JSGlobalObject* globalObject, int auto result = JSC::call(globalObject, getStdioWriteStream, callData, globalObject->globalThis(), args, returnedException); RETURN_IF_EXCEPTION(scope, {}); - if (returnedException) { - throwException(globalObject, scope, returnedException.get()); + if (auto* exception = returnedException.get()) { +#if BUN_DEBUG + Zig::GlobalObject::reportUncaughtExceptionAtEventLoop(globalObject, exception); +#endif + scope.throwException(globalObject, exception->value()); + returnedException.clear(); return {}; } @@ -1657,8 +1661,12 @@ static JSValue constructStdin(VM& vm, JSObject* processObject) auto result = JSC::call(globalObject, getStdioWriteStream, callData, globalObject, args, returnedException); RETURN_IF_EXCEPTION(scope, {}); - if (UNLIKELY(returnedException)) { - throwException(globalObject, scope, returnedException.get()); + if (auto* exception = returnedException.get()) { +#if BUN_DEBUG + Zig::GlobalObject::reportUncaughtExceptionAtEventLoop(globalObject, exception); +#endif + scope.throwException(globalObject, exception->value()); + returnedException.clear(); return {}; } diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index f64698d1dc..8ff02ef847 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -1619,6 +1619,8 @@ enum ReadableStreamTag : int32_t { Bytes = 4, }; +extern "C" JSC_DECLARE_HOST_FUNCTION(BunString__getStringWidth); + JSC_DEFINE_HOST_FUNCTION(jsReceiveMessageOnPort, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { auto& vm = lexicalGlobalObject->vm(); @@ -1751,9 +1753,14 @@ JSC_DEFINE_HOST_FUNCTION(functionLazyLoad, obj->putDirect( vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "parseArgs"_s)), JSC::JSFunction::create(vm, globalObject, 1, "parseArgs"_s, Bun__NodeUtil__jsParseArgs, ImplementationVisibility::Public), NoIntrinsic); + return JSValue::encode(obj); } + if (string == "getStringWidth"_s) { + return JSValue::encode(JSC::JSFunction::create(vm, globalObject, 1, "getStringWidth"_s, BunString__getStringWidth, ImplementationVisibility::Public)); + } + if (string == "pathToFileURL"_s) { return JSValue::encode( JSFunction::create(vm, globalObject, 1, pathToFileURLString, functionPathToFileURL, ImplementationVisibility::Public, NoIntrinsic)); diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index 0f7f8474b5..66ecdd10b3 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -261,7 +261,7 @@ pub const ZigString = extern struct { pub fn substringWithLen(this: ZigString, start_index: usize, end_index: usize) ZigString { if (this.is16Bit()) { - return ZigString.from16Slice(this.utf16SliceAligned()[start_index..end_index]); + return ZigString.from16SliceMaybeGlobal(this.utf16SliceAligned()[start_index..end_index], this.isGloballyAllocated()); } var out = ZigString.init(this.slice()[start_index..end_index]); @@ -642,6 +642,15 @@ pub const ZigString = extern struct { return from16(slice_.ptr, slice_.len); } + fn from16SliceMaybeGlobal(slice_: []const u16, global: bool) ZigString { + var str = init(@as([*]const u8, @alignCast(@ptrCast(slice_.ptr)))[0..slice_.len]); + str.markUTF16(); + if (global) { + str.mark(); + } + return str; + } + /// Globally-allocated memory only pub fn from16(slice_: [*]const u16, len: usize) ZigString { var str = init(@as([*]const u8, @ptrCast(slice_))[0..len]); @@ -5941,9 +5950,8 @@ pub fn JSPropertyIterator(comptime options: JSPropertyIteratorOptions) type { var property_name_ref = JSC.C.JSPropertyNameArrayGetNameAtIndex(self.array_ref, self.iter_i); self.iter_i += 1; - const len = JSC.C.JSStringGetLength(property_name_ref); - if (comptime options.skip_empty_name) { + const len = JSC.C.JSStringGetLength(property_name_ref); if (len == 0) return self.next(); } @@ -6015,3 +6023,7 @@ pub const ScriptExecutionStatus = enum(i32) { suspended = 1, stopped = 2, }; + +comptime { + _ = bun.String.BunString__getStringWidth; +} diff --git a/src/bun.js/bindings/exports.zig b/src/bun.js/bindings/exports.zig index 44ef1ce2ef..d8e2843406 100644 --- a/src/bun.js/bindings/exports.zig +++ b/src/bun.js/bindings/exports.zig @@ -978,9 +978,9 @@ pub const ZigConsoleClient = struct { Output.enable_ansi_colors_stdout; var buffered_writer = if (level == .Warning or level == .Error) - console.error_writer + &console.error_writer else - console.writer; + &console.writer; var writer = buffered_writer.writer(); const Writer = @TypeOf(writer); @@ -1002,7 +1002,10 @@ pub const ZigConsoleClient = struct { tabular_data, properties, ); - table_printer.printTable(writer) catch return; + + switch (enable_colors) { + inline else => |colors| table_printer.printTable(Writer, writer, colors) catch return, + } buffered_writer.flush() catch {}; return; } @@ -1096,34 +1099,63 @@ pub const ZigConsoleClient = struct { .remaining_values = &[_]JSValue{}, .globalThis = globalObject, .ordered_properties = false, - .quote_strings = true, + .quote_strings = false, .single_line = true, .max_depth = 5, }, }; } + const VisibleCharacterCounter = struct { + width: *usize = undefined, + + pub const WriteError = error{}; + + pub const Writer = std.io.Writer( + VisibleCharacterCounter, + VisibleCharacterCounter.WriteError, + VisibleCharacterCounter.write, + ); + + pub fn write(this: VisibleCharacterCounter, bytes: []const u8) WriteError!usize { + this.width.* += strings.visibleUTF8Width(bytes); + return bytes.len; + } + + pub fn writeAll(this: VisibleCharacterCounter, bytes: []const u8) WriteError!void { + this.width.* += strings.visibleUTF8Width(bytes); + } + }; + /// Compute how much horizontal space will take a JSValue when printed fn getWidthForValue(this: *TablePrinter, value: JSValue) u32 { - var counting_writer = std.io.countingWriter(std.io.null_writer); + var width: usize = 0; var value_formatter = this.value_formatter; + + const tag = ZigConsoleClient.Formatter.Tag.get(value, this.globalObject); + value_formatter.quote_strings = !(tag.tag == .String or tag.tag == .StringPossiblyFormatted); value_formatter.format( - ZigConsoleClient.Formatter.Tag.get(value, this.globalObject), - @TypeOf(counting_writer).Writer, - counting_writer.writer(), + tag, + VisibleCharacterCounter.Writer, + VisibleCharacterCounter.Writer{ + .context = .{ + .width = &width, + }, + }, value, this.globalObject, false, ); - return @as(u32, @intCast(counting_writer.bytes_written)); + + return @truncate(width); } /// Update the sizes of the columns for the values of a given row, and create any additional columns as needed fn updateColumnsForRow(this: *TablePrinter, columns: *std.ArrayList(Column), row_key: RowKey, row_value: JSValue) !void { // update size of "(index)" column const row_key_len: u32 = switch (row_key) { - .str => |value| @intCast(value.length()), - .num => |value| @intCast(std.fmt.count("{d}", .{value})), + .str => |value| @intCast(value.visibleWidth()), + .num => |value| @truncate(bun.fmt.fastDigitCount(value)), }; columns.items[0].width = @max(columns.items[0].width, row_key_len); @@ -1141,9 +1173,9 @@ pub const ZigConsoleClient = struct { // - if "properties" arg was provided: iterate the already-created columns (except for the 0-th which is the index) // - otherwise: iterate the object properties, and create the columns on-demand if (!this.properties.isUndefined()) { - for (1..columns.items.len) |i| { - if (row_value.getWithString(this.globalObject, columns.items[i].name)) |value| { - columns.items[i].width = @max(columns.items[i].width, this.getWidthForValue(value)); + for (columns.items[1..]) |*column| { + if (row_value.getWithString(this.globalObject, column.name)) |value| { + column.width = @max(column.width, this.getWidthForValue(value)); } } } else { @@ -1157,15 +1189,20 @@ pub const ZigConsoleClient = struct { const value = cols_iter.value; // find or create the column for the property - var col_idx: usize = 0; - while (col_idx < columns.items.len) : (col_idx += 1) { - if (columns.items[col_idx].name.eql(String.init(col_key))) break; - } - if (col_idx == columns.items.len) { - try columns.append(.{ .name = String.init(col_key) }); - } + const column: *Column = brk: { + const col_str = String.init(col_key); + for (columns.items[1..]) |*col| { + if (col.name.eql(col_str)) { + break :brk col; + } + } - columns.items[col_idx].width = @max(columns.items[col_idx].width, this.getWidthForValue(value)); + try columns.append(.{ .name = col_str }); + + break :brk &columns.items[columns.items.len - 1]; + }; + + column.width = @max(column.width, this.getWidthForValue(value)); } } } else if (this.properties.isUndefined()) { @@ -1174,7 +1211,12 @@ pub const ZigConsoleClient = struct { } } - inline fn writeStringNTimes(writer: anytype, str: []const u8, n: usize) !void { + fn writeStringNTimes(comptime Writer: type, writer: Writer, comptime str: []const u8, n: usize) !void { + if (comptime str.len == 1) { + try writer.writeByteNTimes(str[0], n); + return; + } + for (0..n) |_| { try writer.writeAll(str); } @@ -1182,7 +1224,8 @@ pub const ZigConsoleClient = struct { fn printRow( this: *TablePrinter, - writer: anytype, + comptime Writer: type, + writer: Writer, comptime enable_ansi_colors: bool, columns: *std.ArrayList(Column), row_key: RowKey, @@ -1191,17 +1234,18 @@ pub const ZigConsoleClient = struct { try writer.writeAll("│"); { const len: u32 = switch (row_key) { - .str => |value| @intCast(value.length()), - .num => |value| @intCast(std.fmt.count("{d}", .{value})), + .str => |value| @truncate(value.visibleWidth()), + .num => |value| @truncate(bun.fmt.fastDigitCount(value)), }; - const pad_l = (columns.items[0].width - len) >> 1; - const pad_r = columns.items[0].width - len - pad_l; - try writer.writeByteNTimes(' ', pad_l + PADDING); + const needed = columns.items[0].width -| len; + + // Right-align the number column + try writer.writeByteNTimes(' ', needed + PADDING); switch (row_key) { .str => |value| try writer.print("{}", .{value}), .num => |value| try writer.print("{d}", .{value}), } - try writer.writeByteNTimes(' ', pad_r + PADDING); + try writer.writeByteNTimes(' ', PADDING); } for (1..columns.items.len) |col_idx| { @@ -1223,24 +1267,33 @@ pub const ZigConsoleClient = struct { } if (value.isEmpty()) { - try writer.writeByteNTimes(' ', col.width + 2 * PADDING); + try writer.writeByteNTimes(' ', col.width + 2 + PADDING); } else { const len: u32 = this.getWidthForValue(value); - - const pad_l = (col.width - len) >> 1; - const pad_r = col.width - len - pad_l; - try writer.writeByteNTimes(' ', pad_l + PADDING); + const needed = col.width -| len; + try writer.writeByteNTimes(' ', PADDING); const tag = ZigConsoleClient.Formatter.Tag.get(value, this.globalObject); var value_formatter = this.value_formatter; + + value_formatter.quote_strings = !(tag.tag == .String or tag.tag == .StringPossiblyFormatted); + + defer { + if (value_formatter.map_node) |node| { + node.data = value_formatter.map; + node.data.clearRetainingCapacity(); + node.release(); + } + } value_formatter.format( tag, - @TypeOf(writer), + Writer, writer, value, this.globalObject, enable_ansi_colors, ); - try writer.writeByteNTimes(' ', pad_r + PADDING); + + try writer.writeByteNTimes(' ', needed + PADDING); } } try writer.writeAll("│\n"); @@ -1248,7 +1301,9 @@ pub const ZigConsoleClient = struct { pub fn printTable( this: *TablePrinter, - writer: anytype, + comptime Writer: type, + writer: Writer, + comptime enable_ansi_colors: bool, ) !void { const globalObject = this.globalObject; @@ -1256,9 +1311,10 @@ pub const ZigConsoleClient = struct { var columns = try std.ArrayList(Column).initCapacity(stack_fallback.get(), 16); defer columns.deinit(); - // create the first column "(index)", which is always present + // create the first column " " which is always present columns.appendAssumeCapacity(.{ - .name = if (this.is_iterable and !this.tabular_data.jsType().isArray()) String.static("(iteration index)") else String.static("(index)"), + .name = String.static(" "), + .width = 1, }); // special case for Map: create the special "Key" column at index 1 @@ -1315,27 +1371,38 @@ pub const ZigConsoleClient = struct { // print the table header (border line + column names line + border line) { + for (columns.items) |*col| { + // also update the col width with the length of the column name itself + col.width = @max(col.width, @as(u32, @intCast(col.name.visibleWidth()))); + } + try writer.writeAll("┌"); for (columns.items, 0..) |*col, i| { - // also update the col width with the length of the column name itself - col.width = @max(col.width, @as(u32, @intCast(col.name.length()))); if (i > 0) try writer.writeAll("┬"); - try writeStringNTimes(writer, "─", col.width + 2 * PADDING); + try writeStringNTimes(Writer, writer, "─", col.width + (PADDING * 2)); } + try writer.writeAll("┐\n│"); + for (columns.items, 0..) |col, i| { if (i > 0) try writer.writeAll("│"); - const len = col.name.length(); - const pad_l = (col.width - len) >> 1; - const pad_r = col.width - len - pad_l; - try writer.writeByteNTimes(' ', pad_l + PADDING); + const len = col.name.visibleWidth(); + const needed = col.width -| len; + try writer.writeByteNTimes(' ', 1); + if (comptime enable_ansi_colors) { + try writer.writeAll(Output.prettyFmt("", true)); + } try writer.print("{}", .{col.name}); - try writer.writeByteNTimes(' ', pad_r + PADDING); + if (comptime enable_ansi_colors) { + try writer.writeAll(Output.prettyFmt("", true)); + } + try writer.writeByteNTimes(' ', needed + PADDING); } + try writer.writeAll("│\n├"); for (columns.items, 0..) |col, i| { if (i > 0) try writer.writeAll("┼"); - try writeStringNTimes(writer, "─", col.width + 2 * PADDING); + try writeStringNTimes(Writer, writer, "─", col.width + (PADDING * 2)); } try writer.writeAll("┤\n"); } @@ -1343,16 +1410,12 @@ pub const ZigConsoleClient = struct { // rows second pass - print the actual table rows { if (this.is_iterable) { - var ctx_: struct { this: *TablePrinter, columns: *@TypeOf(columns), writer: *const @TypeOf(writer), idx: u32 = 0, err: bool = false } = .{ .this = this, .columns = &columns, .writer = &writer }; + var ctx_: struct { this: *TablePrinter, columns: *@TypeOf(columns), writer: Writer, idx: u32 = 0, err: bool = false } = .{ .this = this, .columns = &columns, .writer = writer }; this.tabular_data.forEachWithContext(globalObject, &ctx_, struct { fn callback(_: *JSC.VM, _: *JSGlobalObject, ctx: *@TypeOf(ctx_), value: JSValue) callconv(.C) void { - switch (Output.enable_ansi_colors) { - inline else => |enable_ansi_colors| { - printRow(ctx.this, ctx.writer.*, enable_ansi_colors, ctx.columns, .{ .num = ctx.idx }, value) catch { - ctx.err = true; - }; - }, - } + printRow(ctx.this, Writer, ctx.writer, enable_ansi_colors, ctx.columns, .{ .num = ctx.idx }, value) catch { + ctx.err = true; + }; ctx.idx += 1; } }.callback); @@ -1364,12 +1427,8 @@ pub const ZigConsoleClient = struct { }).init(globalObject, this.tabular_data.asObjectRef()); defer rows_iter.deinit(); - switch (Output.enable_ansi_colors) { - inline else => |enable_ansi_colors| { - while (rows_iter.next()) |row_key| { - try this.printRow(writer, enable_ansi_colors, &columns, .{ .str = String.init(row_key) }, rows_iter.value); - } - }, + while (rows_iter.next()) |row_key| { + try this.printRow(Writer, writer, enable_ansi_colors, &columns, .{ .str = String.init(row_key) }, rows_iter.value); } } } @@ -1377,10 +1436,10 @@ pub const ZigConsoleClient = struct { // print the table bottom border { try writer.writeAll("└"); - try writeStringNTimes(writer, "─", columns.items[0].width + 2 * PADDING); - for (1..columns.items.len) |i| { + try writeStringNTimes(Writer, writer, "─", columns.items[0].width + (PADDING * 2)); + for (columns.items[1..]) |*column| { try writer.writeAll("┴"); - try writeStringNTimes(writer, "─", columns.items[i].width + 2 * PADDING); + try writeStringNTimes(Writer, writer, "─", column.width + (PADDING * 2)); } try writer.writeAll("┘\n"); } @@ -2475,12 +2534,14 @@ pub const ZigConsoleClient = struct { this.writeWithFormatting(Writer, writer_, @TypeOf(slice), slice, this.globalThis, enable_ansi_colors); }, .String => { - var str = ZigString.init(""); - value.toZigString(&str, this.globalThis); - this.addForNewLine(str.len); + var str: bun.String = bun.String.tryFromJS(value, this.globalThis) orelse { + writer.failed = true; + return; + }; + this.addForNewLine(str.length()); if (this.quote_strings and jsType != .RegExpObject) { - if (str.len == 0) { + if (str.isEmpty()) { writer.writeAll("\"\""); return; } @@ -2492,12 +2553,12 @@ pub const ZigConsoleClient = struct { defer if (comptime enable_ansi_colors) writer.writeAll(Output.prettyFmt("", true)); - if (str.is16Bit()) { + if (str.isUTF16()) { this.printAs(.JSON, Writer, writer_, value, .StringObject, enable_ansi_colors); return; } - JSPrinter.writeJSONString(str.slice(), Writer, writer_, .latin1) catch unreachable; + JSPrinter.writeJSONString(str.latin1(), Writer, writer_, .latin1) catch unreachable; return; } @@ -2506,15 +2567,15 @@ pub const ZigConsoleClient = struct { writer.print(comptime Output.prettyFmt("", enable_ansi_colors), .{}); } - if (str.is16Bit()) { + if (str.isUTF16()) { // streaming print writer.print("{}", .{str}); - } else if (strings.isAllASCII(str.slice())) { + } else if (str.asUTF8()) |slice| { // fast path - writer.writeAll(str.slice()); - } else if (str.len > 0) { + writer.writeAll(slice); + } else if (!str.isEmpty()) { // slow path - const buf = strings.allocateLatin1IntoUTF8(bun.default_allocator, []const u8, str.slice()) catch &[_]u8{}; + const buf = strings.allocateLatin1IntoUTF8(bun.default_allocator, []const u8, str.latin1()) catch &[_]u8{}; if (buf.len > 0) { defer bun.default_allocator.free(buf); writer.writeAll(buf); diff --git a/src/bun.js/test/expect.zig b/src/bun.js/test/expect.zig index 546be8a144..ab87208acd 100644 --- a/src/bun.js/test/expect.zig +++ b/src/bun.js/test/expect.zig @@ -524,12 +524,12 @@ pub const Expect = struct { .globalObject = globalObject, .not = not, }; - const fmt = signature ++ "\n\n{any}\n"; + const fmt = comptime signature ++ "\n\n{any}\n"; if (Output.enable_ansi_colors) { - globalObject.throw(Output.prettyFmt(fmt, true), .{diff_format}); + globalObject.throw(comptime Output.prettyFmt(fmt, true), .{diff_format}); return .zero; } - globalObject.throw(Output.prettyFmt(fmt, false), .{diff_format}); + globalObject.throw(comptime Output.prettyFmt(fmt, false), .{diff_format}); return .zero; } diff --git a/src/bun.zig b/src/bun.zig index d1d0f60990..8f3705d3e1 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -605,6 +605,10 @@ pub const fmt = struct { // https://lemire.me/blog/2021/06/03/computing-the-number-of-digits-of-an-integer-even-faster/ pub fn fastDigitCount(x: u64) u64 { + if (x == 0) { + return 1; + } + const table = [_]u64{ 4294967296, 8589934582, diff --git a/src/js/builtins/ConsoleObject.ts b/src/js/builtins/ConsoleObject.ts index b48593154b..e0812e10e1 100644 --- a/src/js/builtins/ConsoleObject.ts +++ b/src/js/builtins/ConsoleObject.ts @@ -164,85 +164,20 @@ export function createConsoleConstructor(console: typeof globalThis.console) { "|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))"; var ansi = new RegExp(ansiPattern, "g"); - /** - * Returns true if the character represented by a given - * Unicode code point is full-width. Otherwise returns false. - */ - var isFullWidthCodePoint = code => { - // Code points are partially derived from: - // https://www.unicode.org/Public/UNIDATA/EastAsianWidth.txt - return ( - code >= 0x1100 && - (code <= 0x115f || // Hangul Jamo - code === 0x2329 || // LEFT-POINTING ANGLE BRACKET - code === 0x232a || // RIGHT-POINTING ANGLE BRACKET - // CJK Radicals Supplement .. Enclosed CJK Letters and Months - (code >= 0x2e80 && code <= 0x3247 && code !== 0x303f) || - // Enclosed CJK Letters and Months .. CJK Unified Ideographs Extension A - (code >= 0x3250 && code <= 0x4dbf) || - // CJK Unified Ideographs .. Yi Radicals - (code >= 0x4e00 && code <= 0xa4c6) || - // Hangul Jamo Extended-A - (code >= 0xa960 && code <= 0xa97c) || - // Hangul Syllables - (code >= 0xac00 && code <= 0xd7a3) || - // CJK Compatibility Ideographs - (code >= 0xf900 && code <= 0xfaff) || - // Vertical Forms - (code >= 0xfe10 && code <= 0xfe19) || - // CJK Compatibility Forms .. Small Form Variants - (code >= 0xfe30 && code <= 0xfe6b) || - // Halfwidth and Fullwidth Forms - (code >= 0xff01 && code <= 0xff60) || - (code >= 0xffe0 && code <= 0xffe6) || - // Kana Supplement - (code >= 0x1b000 && code <= 0x1b001) || - // Enclosed Ideographic Supplement - (code >= 0x1f200 && code <= 0x1f251) || - // Miscellaneous Symbols and Pictographs 0x1f300 - 0x1f5ff - // Emoticons 0x1f600 - 0x1f64f - (code >= 0x1f300 && code <= 0x1f64f) || - // CJK Unified Ideographs Extension B .. Tertiary Ideographic Plane - (code >= 0x20000 && code <= 0x3fffd)) - ); - }; - - var isZeroWidthCodePoint = code => { - return ( - code <= 0x1f || // C0 control codes - (code >= 0x7f && code <= 0x9f) || // C1 control codes - (code >= 0x300 && code <= 0x36f) || // Combining Diacritical Marks - (code >= 0x200b && code <= 0x200f) || // Modifying Invisible Characters - // Combining Diacritical Marks for Symbols - (code >= 0x20d0 && code <= 0x20ff) || - (code >= 0xfe00 && code <= 0xfe0f) || // Variation Selectors - (code >= 0xfe20 && code <= 0xfe2f) || // Combining Half Marks - (code >= 0xe0100 && code <= 0xe01ef) - ); // Variation Selectors - }; - function stripVTControlCharacters(str) { return (RegExpPrototypeSymbolReplace as any).$call(ansi, str, ""); } + var internalGetStringWidth = $lazy("getStringWidth"); + /** * Returns the number of columns required to display the given string. */ var getStringWidth = function getStringWidth(str, removeControlChars = true) { - var width = 0; - if (removeControlChars) str = stripVTControlCharacters(str); str = StringPrototypeNormalize.$call(str, "NFC"); - for (var char of str) { - var code = StringPrototypeCodePointAt.$call(char, 0); - if (isFullWidthCodePoint(code)) { - width += 2; - } else if (!isZeroWidthCodePoint(code)) { - width++; - } - } - return width; + return internalGetStringWidth(str); }; const tableChars = { diff --git a/src/js/internal/util/inspect.js b/src/js/internal/util/inspect.js index 6c2c0be253..05d57d8e96 100644 --- a/src/js/internal/util/inspect.js +++ b/src/js/internal/util/inspect.js @@ -102,7 +102,6 @@ const { RegExpPrototypeSymbolSplit, RegExpPrototypeTest, RegExpPrototypeToString, - SafeStringIterator, SafeMap, SafeSet, SetPrototypeEntries, @@ -619,8 +618,6 @@ const meta = [ "\\x9F", // x9F ]; -let getStringWidth; - function getUserOptions(ctx, isCrossContext) { const ret = { stylize: ctx.stylize, @@ -2572,83 +2569,14 @@ function formatWithOptionsInternal(inspectOptions, args) { return str; } -function isZeroWidthCodePoint(code) { - return ( - code <= 0x1f || // C0 control codes - (code >= 0x7f && code <= 0x9f) || // C1 control codes - (code >= 0x300 && code <= 0x36f) || // Combining Diacritical Marks - (code >= 0x200b && code <= 0x200f) || // Modifying Invisible Characters - // Combining Diacritical Marks for Symbols - (code >= 0x20d0 && code <= 0x20ff) || - (code >= 0xfe00 && code <= 0xfe0f) || // Variation Selectors - (code >= 0xfe20 && code <= 0xfe2f) || // Combining Half Marks - (code >= 0xe0100 && code <= 0xe01ef) - ); // Variation Selectors -} - -{ - /** - * Returns the number of columns required to display the given string. - */ - getStringWidth = function getStringWidth(str, removeControlChars = true) { - let width = 0; - - if (removeControlChars) str = stripVTControlCharacters(str); - str = StringPrototypeNormalize(str, "NFC"); - for (const char of new SafeStringIterator(str)) { - const code = StringPrototypeCodePointAt(char, 0); - if (isFullWidthCodePoint(code)) { - width += 2; - } else if (!isZeroWidthCodePoint(code)) { - width++; - } - } - - return width; - }; - - /** - * Returns true if the character represented by a given - * Unicode code point is full-width. Otherwise returns false. - */ - const isFullWidthCodePoint = code => { - // Code points are partially derived from: - // https://www.unicode.org/Public/UNIDATA/EastAsianWidth.txt - return ( - code >= 0x1100 && - (code <= 0x115f || // Hangul Jamo - code === 0x2329 || // LEFT-POINTING ANGLE BRACKET - code === 0x232a || // RIGHT-POINTING ANGLE BRACKET - // CJK Radicals Supplement .. Enclosed CJK Letters and Months - (code >= 0x2e80 && code <= 0x3247 && code !== 0x303f) || - // Enclosed CJK Letters and Months .. CJK Unified Ideographs Extension A - (code >= 0x3250 && code <= 0x4dbf) || - // CJK Unified Ideographs .. Yi Radicals - (code >= 0x4e00 && code <= 0xa4c6) || - // Hangul Jamo Extended-A - (code >= 0xa960 && code <= 0xa97c) || - // Hangul Syllables - (code >= 0xac00 && code <= 0xd7a3) || - // CJK Compatibility Ideographs - (code >= 0xf900 && code <= 0xfaff) || - // Vertical Forms - (code >= 0xfe10 && code <= 0xfe19) || - // CJK Compatibility Forms .. Small Form Variants - (code >= 0xfe30 && code <= 0xfe6b) || - // Halfwidth and Fullwidth Forms - (code >= 0xff01 && code <= 0xff60) || - (code >= 0xffe0 && code <= 0xffe6) || - // Kana Supplement - (code >= 0x1b000 && code <= 0x1b001) || - // Enclosed Ideographic Supplement - (code >= 0x1f200 && code <= 0x1f251) || - // Miscellaneous Symbols and Pictographs 0x1f300 - 0x1f5ff - // Emoticons 0x1f600 - 0x1f64f - (code >= 0x1f300 && code <= 0x1f64f) || - // CJK Unified Ideographs Extension B .. Tertiary Ideographic Plane - (code >= 0x20000 && code <= 0x3fffd)) - ); - }; +var internalGetStringWidth = $lazy("getStringWidth"); +/** + * Returns the number of columns required to display the given string. + */ +function getStringWidth(str, removeControlChars = true) { + if (removeControlChars) str = stripVTControlCharacters(str); + str = StringPrototypeNormalize(str, "NFC"); + return internalGetStringWidth(str); } // Regex used for ansi escape code splitting diff --git a/src/js/node/readline.js b/src/js/node/readline.js index 16f4e19957..b439dea8df 100644 --- a/src/js/node/readline.js +++ b/src/js/node/readline.js @@ -113,81 +113,14 @@ var SafeStringIterator = createSafeIterator(StringPrototypeSymbolIterator, Strin // Section: "Internal" modules // ---------------------------------------------------------------------------- -/** - * Returns true if the character represented by a given - * Unicode code point is full-width. Otherwise returns false. - */ -var isFullWidthCodePoint = code => { - // Code points are partially derived from: - // https://www.unicode.org/Public/UNIDATA/EastAsianWidth.txt - return ( - code >= 0x1100 && - (code <= 0x115f || // Hangul Jamo - code === 0x2329 || // LEFT-POINTING ANGLE BRACKET - code === 0x232a || // RIGHT-POINTING ANGLE BRACKET - // CJK Radicals Supplement .. Enclosed CJK Letters and Months - (code >= 0x2e80 && code <= 0x3247 && code !== 0x303f) || - // Enclosed CJK Letters and Months .. CJK Unified Ideographs Extension A - (code >= 0x3250 && code <= 0x4dbf) || - // CJK Unified Ideographs .. Yi Radicals - (code >= 0x4e00 && code <= 0xa4c6) || - // Hangul Jamo Extended-A - (code >= 0xa960 && code <= 0xa97c) || - // Hangul Syllables - (code >= 0xac00 && code <= 0xd7a3) || - // CJK Compatibility Ideographs - (code >= 0xf900 && code <= 0xfaff) || - // Vertical Forms - (code >= 0xfe10 && code <= 0xfe19) || - // CJK Compatibility Forms .. Small Form Variants - (code >= 0xfe30 && code <= 0xfe6b) || - // Halfwidth and Fullwidth Forms - (code >= 0xff01 && code <= 0xff60) || - (code >= 0xffe0 && code <= 0xffe6) || - // Kana Supplement - (code >= 0x1b000 && code <= 0x1b001) || - // Enclosed Ideographic Supplement - (code >= 0x1f200 && code <= 0x1f251) || - // Miscellaneous Symbols and Pictographs 0x1f300 - 0x1f5ff - // Emoticons 0x1f600 - 0x1f64f - (code >= 0x1f300 && code <= 0x1f64f) || - // CJK Unified Ideographs Extension B .. Tertiary Ideographic Plane - (code >= 0x20000 && code <= 0x3fffd)) - ); -}; - -var isZeroWidthCodePoint = code => { - return ( - code <= 0x1f || // C0 control codes - (code >= 0x7f && code <= 0x9f) || // C1 control codes - (code >= 0x300 && code <= 0x36f) || // Combining Diacritical Marks - (code >= 0x200b && code <= 0x200f) || // Modifying Invisible Characters - // Combining Diacritical Marks for Symbols - (code >= 0x20d0 && code <= 0x20ff) || - (code >= 0xfe00 && code <= 0xfe0f) || // Variation Selectors - (code >= 0xfe20 && code <= 0xfe2f) || // Combining Half Marks - (code >= 0xe0100 && code <= 0xe01ef) - ); // Variation Selectors -}; - +var internalGetStringWidth = $lazy("getStringWidth"); /** * Returns the number of columns required to display the given string. */ var getStringWidth = function getStringWidth(str, removeControlChars = true) { - var width = 0; - if (removeControlChars) str = stripVTControlCharacters(str); str = StringPrototypeNormalize.$call(str, "NFC"); - for (var char of new SafeStringIterator(str)) { - var code = StringPrototypeCodePointAt.$call(char, 0); - if (isFullWidthCodePoint(code)) { - width += 2; - } else if (!isZeroWidthCodePoint(code)) { - width++; - } - } - - return width; + return internalGetStringWidth(str); }; // Regex used for ansi escape code splitting diff --git a/src/js/private.d.ts b/src/js/private.d.ts index 762449333c..5f3c5bab99 100644 --- a/src/js/private.d.ts +++ b/src/js/private.d.ts @@ -199,6 +199,7 @@ interface BunLazyModules { isatty: (fd: number) => boolean; getWindowSize: (fd: number, out: number[2]) => boolean; }; + "getStringWidth": (str: string) => number; // ReadableStream related [1]: any; diff --git a/src/string.zig b/src/string.zig index 6e28580399..3ff9e2497a 100644 --- a/src/string.zig +++ b/src/string.zig @@ -618,16 +618,22 @@ pub const String = extern struct { pub inline fn utf16(self: String) []const u16 { if (self.tag == .Empty) return &[_]u16{}; - std.debug.assert(self.tag == .WTFStringImpl); - return self.value.WTFStringImpl.utf16Slice(); + if (self.tag == .WTFStringImpl) { + return self.value.WTFStringImpl.utf16Slice(); + } + + return self.toZigString().utf16SliceAligned(); } pub inline fn latin1(self: String) []const u8 { if (self.tag == .Empty) return &[_]u8{}; - std.debug.assert(self.tag == .WTFStringImpl); - return self.value.WTFStringImpl.latin1Slice(); + if (self.tag == .WTFStringImpl) { + return self.value.WTFStringImpl.latin1Slice(); + } + + return self.toZigString().slice(); } pub fn isUTF8(self: String) bool { @@ -637,6 +643,30 @@ pub const String = extern struct { return self.value.ZigString.isUTF8(); } + pub inline fn asUTF8(self: String) ?[]const u8 { + if (self.tag == .WTFStringImpl) { + if (self.value.WTFStringImpl.is8Bit() and bun.strings.isAllASCII(self.value.WTFStringImpl.latin1Slice())) { + return self.value.WTFStringImpl.latin1Slice(); + } + + return null; + } + + if (self.tag == .ZigString or self.tag == .StaticZigString) { + if (self.value.ZigString.isUTF8()) { + return self.value.ZigString.slice(); + } + + if (bun.strings.isAllASCII(self.toZigString().slice())) { + return self.value.ZigString.slice(); + } + + return null; + } + + return ""; + } + pub fn encoding(self: String) bun.strings.Encoding { if (self.isUTF16()) { return .utf16; @@ -707,8 +737,13 @@ pub const String = extern struct { if (self.tag == .WTFStringImpl) return self.value.WTFStringImpl.is8Bit() and bun.strings.isAllASCII(self.value.WTFStringImpl.latin1Slice()); - if (self.tag == .ZigString or self.tag == .StaticZigString) - return self.value.ZigString.isUTF8(); + if (self.tag == .ZigString or self.tag == .StaticZigString) { + if (self.value.ZigString.isUTF8()) { + return true; + } + + return bun.strings.isAllASCII(self.toZigString().slice()); + } return self.tag == .Empty; } @@ -898,6 +933,16 @@ pub const String = extern struct { }; } + pub fn visibleWidth(this: *const String) usize { + if (this.isUTF8()) { + return bun.strings.visibleUTF8Width(this.utf8()); + } else if (this.isUTF16()) { + return bun.strings.visibleUTF16Width(this.utf16()); + } else { + return bun.strings.visibleLatin1Width(this.latin1()); + } + } + pub fn indexOfComptimeWithCheckLen(this: String, comptime values: []const []const u8, comptime check_len: usize) ?usize { if (this.is8Bit()) { const bytes = this.byteSlice(); @@ -1091,6 +1136,25 @@ pub const String = extern struct { pub inline fn createFromConcat(allocator: std.mem.Allocator, strings: anytype) !String { return try concat(strings.len, allocator, strings); } + + pub export fn BunString__getStringWidth(globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { + var str: String = String.dead; + + const args = callFrame.arguments(1).slice(); + + if (args.len == 0 or !args.ptr[0].isString()) { + return JSC.jsNumber(@as(i32, 0)); + } + + str = args[0].toBunString(globalObject); + + if (str.isEmpty()) { + return JSC.jsNumber(@as(i32, 0)); + } + + const width = str.visibleWidth(); + return JSC.jsNumber(width); + } }; pub const SliceWithUnderlyingString = struct { diff --git a/src/string_immutable.zig b/src/string_immutable.zig index 331e7e33f2..5271bcd44f 100644 --- a/src/string_immutable.zig +++ b/src/string_immutable.zig @@ -4414,16 +4414,30 @@ test "firstNonASCII16" { } } +const SharedTempBuffer = [32 * 1024]u8; fn getSharedBuffer() []u8 { return std.mem.asBytes(shared_temp_buffer_ptr orelse brk: { - shared_temp_buffer_ptr = bun.default_allocator.create([32 * 1024]u8) catch unreachable; + shared_temp_buffer_ptr = bun.default_allocator.create(SharedTempBuffer) catch unreachable; break :brk shared_temp_buffer_ptr.?; }); } -threadlocal var shared_temp_buffer_ptr: ?*[32 * 1024]u8 = null; +threadlocal var shared_temp_buffer_ptr: ?*SharedTempBuffer = null; pub fn formatUTF16Type(comptime Slice: type, slice_: Slice, writer: anytype) !void { var chunk = getSharedBuffer(); + + // Defensively ensure recursion doesn't cause the buffer to be overwritten in-place + shared_temp_buffer_ptr = null; + defer { + if (shared_temp_buffer_ptr) |existing| { + if (existing != chunk.ptr) { + bun.default_allocator.destroy(@as(*SharedTempBuffer, @ptrCast(chunk.ptr))); + } + } else { + shared_temp_buffer_ptr = @ptrCast(chunk.ptr); + } + } + var slice = slice_; while (slice.len > 0) { @@ -4468,6 +4482,18 @@ pub fn formatLatin1(slice_: []const u8, writer: anytype) !void { var chunk = getSharedBuffer(); var slice = slice_; + // Defensively ensure recursion doesn't cause the buffer to be overwritten in-place + shared_temp_buffer_ptr = null; + defer { + if (shared_temp_buffer_ptr) |existing| { + if (existing != chunk.ptr) { + bun.default_allocator.destroy(@as(*SharedTempBuffer, @ptrCast(chunk.ptr))); + } + } else { + shared_temp_buffer_ptr = @ptrCast(chunk.ptr); + } + } + while (strings.firstNonASCII(slice)) |i| { if (i > 0) { try writer.writeAll(slice[0..i]); @@ -5452,3 +5478,191 @@ pub fn mustEscapeYAMLString(contents: []const u8) bool { pub fn pathContainsNodeModulesFolder(path: []const u8) bool { return strings.contains(path, comptime std.fs.path.sep_str ++ "node_modules" ++ std.fs.path.sep_str); } + +pub fn isZeroWidthCodepointType(comptime T: type, cp: T) bool { + if (cp <= 0x1f) { + return true; + } + + if (cp >= 0x7f and cp <= 0x9f) { + // C1 control characters + return true; + } + + if (comptime @sizeOf(T) == 1) { + return false; + } + + if (cp >= 0x300 and cp <= 0x36f) { + // Combining Diacritical Marks + return true; + } + if (cp >= 0x300 and cp <= 0x36f) + // Combining Diacritical Marks + return true; + + if (cp >= 0x200b and cp <= 0x200f) { + // Modifying Invisible Characters + return true; + } + + if (cp >= 0x20d0 and cp <= 0x20ff) + // Combining Diacritical Marks for Symbols + return true; + + if (cp >= 0xfe00 and cp <= 0xfe0f) + // Variation Selectors + return true; + if (cp >= 0xfe20 and cp <= 0xfe2f) + // Combining Half Marks + return true; + + if (cp >= 0xe0100 and cp <= 0xe01ef) + // Variation Selectors + return true; + + return false; +} + +pub fn isFullWidthCodepointType(comptime T: type, cp: T) bool { + if (!(cp >= 0x1100)) { + return false; + } + + if (cp <= 0x115f) { + return true; + } + + if (cp == 0x2329 or // LEFT-POINTING ANGLE BRACKET + cp == 0x232a) // RIGHT-POINTING ANGLE BRACKET + return true; + + // CJK Radicals Supplement .. Enclosed CJK Letters and Months + return (cp >= 0x2e80 and cp <= 0x3247 and cp != 0x303f) or + // Enclosed CJK Letters and Months .. CJK Unified Ideographs Extension A + (cp >= 0x3250 and cp <= 0x4dbf) or + // CJK Unified Ideographs .. Yi Radicals + (cp >= 0x4e00 and cp <= 0xa4c6) or + // Hangul Jamo Extended-A + (cp >= 0xa960 and cp <= 0xa97c) or + // Hangul Syllables + (cp >= 0xac00 and cp <= 0xd7a3) or + // CJK Compatibility Ideographs + (cp >= 0xf900 and cp <= 0xfaff) or + // Vertical Forms + (cp >= 0xfe10 and cp <= 0xfe19) or + // CJK Compatibility Forms .. Small Form Variants + (cp >= 0xfe30 and cp <= 0xfe6b) or + // Halfwidth and Fullwidth Forms + (cp >= 0xff01 and cp <= 0xff60) or + (cp >= 0xffe0 and cp <= 0xffe6) or + // Kana Supplement + (cp >= 0x1b000 and cp <= 0x1b001) or + // Enclosed Ideographic Supplement + (cp >= 0x1f200 and cp <= 0x1f251) or + // Miscellaneous Symbols and Pictographs 0x1f300 - 0x1f5ff + // Emoticons 0x1f600 - 0x1f64f + (cp >= 0x1f300 and cp <= 0x1f64f) or + // CJK Unified Ideographs Extension B .. Tertiary Ideographic Plane + (cp >= 0x20000 and cp <= 0x3fffd); +} + +pub fn visibleCodepointWidth(cp: anytype) u3 { + return visibleCodepointWidthType(@TypeOf(cp), cp); +} + +pub fn visibleCodepointWidthType(comptime T: type, cp: T) usize { + if (isZeroWidthCodepointType(T, cp)) { + return 0; + } + + if (isFullWidthCodepointType(T, cp)) { + return 2; + } + + return 1; +} + +pub fn visibleASCIIWidth(input_: anytype) usize { + var length: usize = 0; + var input = input_; + + if (comptime Environment.enableSIMD) { + // https://zig.godbolt.org/z/hxhjncvq7 + const ElementType = std.meta.Child(@TypeOf(input_)); + const simd = 16 / @sizeOf(ElementType); + if (input.len >= simd) { + const input_end = input.ptr + input.len - (input.len % simd); + while (input.ptr != input_end) { + const chunk: @Vector(simd, ElementType) = input[0..simd].*; + input = input[simd..]; + + const cmp: @Vector(simd, ElementType) = @splat(0x1f); + const match1: @Vector(simd, u1) = @bitCast(chunk >= cmp); + const match: @Vector(simd, ElementType) = match1; + + length += @reduce(.Add, match); + } + } + + // this is a deliberate compiler optimization + // it disables auto-vectorizing the "input" for loop. + if (!(input.len < simd)) unreachable; + } + + for (input) |c| { + length += if (c > 0x1f) 1 else 0; + } + + return length; +} + +pub fn visibleUTF8Width(input: []const u8) usize { + var bytes = input; + var len: usize = 0; + while (bun.strings.firstNonASCII(bytes)) |i| { + len += visibleASCIIWidth(bytes[0..i]); + + const byte = bytes[i]; + const skip = bun.strings.wtf8ByteSequenceLengthWithInvalid(byte); + const cp_bytes: [4]u8 = switch (skip) { + inline 1, 2, 3, 4 => |cp_len| .{ + byte, + if (comptime cp_len > 1) bytes[1] else 0, + if (comptime cp_len > 2) bytes[2] else 0, + if (comptime cp_len > 3) bytes[3] else 0, + }, + else => unreachable, + }; + + const cp = decodeWTF8RuneTMultibyte(&cp_bytes, skip, u32, unicode_replacement); + len += visibleCodepointWidthType(u32, cp); + + bytes = bytes[@min(i + skip, bytes.len)..]; + } + + len += visibleASCIIWidth(bytes); + + return len; +} + +pub fn visibleUTF16Width(input: []const u16) usize { + var bytes = input; + var len: usize = 0; + while (bun.strings.firstNonASCII16CheckMin([]const u16, bytes, false)) |i| { + len += visibleASCIIWidth(bytes[0..i]); + bytes = bytes[i..]; + + const utf8 = utf16CodepointWithFFFD([]const u16, bytes); + len += visibleCodepointWidthType(u32, utf8.code_point); + bytes = bytes[@min(@as(usize, utf8.len), bytes.len)..]; + } + + len += visibleASCIIWidth(bytes); + + return len; +} + +pub fn visibleLatin1Width(input: []const u8) usize { + return visibleASCIIWidth(input); +} diff --git a/test/js/bun/console/__snapshots__/console-table.test.ts.snap b/test/js/bun/console/__snapshots__/console-table.test.ts.snap new file mode 100644 index 0000000000..42d8652174 --- /dev/null +++ b/test/js/bun/console/__snapshots__/console-table.test.ts.snap @@ -0,0 +1,176 @@ +// Bun Snapshot v1, https://goo.gl/fbAQLP + +exports[`console.table expected output for: not object (number) 1`] = ` +"42 +" +`; + +exports[`console.table expected output for: not object (string) 1`] = ` +"bun +" +`; + +exports[`console.table expected output for: object - empty 1`] = ` +"┌───┐ +│ │ +├───┤ +└───┘ +" +`; + +exports[`console.table expected output for: object 1`] = ` +"┌───┬────────┐ +│ │ Values │ +├───┼────────┤ +│ a │ 42 │ +│ b │ bun │ +└───┴────────┘ +" +`; + +exports[`console.table expected output for: array - empty 1`] = ` +"┌───┐ +│ │ +├───┤ +└───┘ +" +`; + +exports[`console.table expected output for: array - plain 1`] = ` +"┌───┬────────┐ +│ │ Values │ +├───┼────────┤ +│ 0 │ 42 │ +│ 1 │ bun │ +└───┴────────┘ +" +`; + +exports[`console.table expected output for: array - object 1`] = ` +"┌───┬────┬─────┐ +│ │ a │ b │ +├───┼────┼─────┤ +│ 0 │ 42 │ bun │ +└───┴────┴─────┘ +" +`; + +exports[`console.table expected output for: array - objects with diff props 1`] = ` +"┌───┬─────┬────┐ +│ │ b │ a │ +├───┼─────┼────┤ +│ 0 │ bun │ │ +│ 1 │ │ 42 │ +└───┴─────┴────┘ +" +`; + +exports[`console.table expected output for: array - mixed 1`] = ` +"┌───┬────┬─────┬────────┐ +│ │ a │ b │ Values │ +├───┼────┼─────┼────────┤ +│ 0 │ 42 │ bun │ │ +│ 1 │ │ │ 42 │ +└───┴────┴─────┴────────┘ +" +`; + +exports[`console.table expected output for: set 1`] = ` +"┌───┬────────┐ +│ │ Values │ +├───┼────────┤ +│ 0 │ 42 │ +│ 1 │ bun │ +└───┴────────┘ +" +`; + +exports[`console.table expected output for: map 1`] = ` +"┌───┬─────┬────────┐ +│ │ Key │ Values │ +├───┼─────┼────────┤ +│ 0 │ a │ 42 │ +│ 1 │ b │ bun │ +│ 2 │ 42 │ c │ +└───┴─────┴────────┘ +" +`; + +exports[`console.table expected output for: properties 1`] = ` +"┌───┬─────┬───┬────┐ +│ │ b │ c │ a │ +├───┼─────┼───┼────┤ +│ 0 │ bun │ │ 42 │ +└───┴─────┴───┴────┘ +" +`; + +exports[`console.table expected output for: properties - empty 1`] = ` +"┌───┐ +│ │ +├───┤ +│ 0 │ +└───┘ +" +`; + +exports[`console.table expected output for: values - array 1`] = ` +"┌───┬─────────────────────────────────┐ +│ │ value │ +├───┼─────────────────────────────────┤ +│ 0 │ { a: 42, b: "bun" } │ +│ 1 │ [ 42, "bun" ] │ +│ 2 │ Set(2) { 42, "bun" } │ +│ 3 │ Map(2) { 42: "bun", "bun": 42 } │ +└───┴─────────────────────────────────┘ +" +`; + + +exports[`console.table json fixture 1`] = ` +"┌────┬──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┬───────┬──────────────────────┬────────────┐ +│ │ title │ state │ created_at │ id │ +├────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────┼──────────────────────┼────────────┤ +│ 0 │ Respond handling issue for Most of Google Cloud API (vision, document, etc.) │ open │ 2023-12-30T04:43:36Z │ 2060630898 │ +│ 1 │ @elastic/elasticsearch@8.11.0 not working due to missing undici.Pool.request │ open │ 2023-12-29T20:26:07Z │ 2060382675 │ +│ 2 │ fs.watch skips save events │ open │ 2023-12-29T14:00:48Z │ 2060059045 │ +│ 3 │ Remix running with node, not bun │ open │ 2023-12-29T13:52:26Z │ 2060043035 │ +│ 4 │ 'fetch' upload with formdata is different than node │ open │ 2023-12-29T13:04:03Z │ 2059869225 │ +│ 5 │ Error throw with an object causes an empty error log line │ open │ 2023-12-29T12:35:33Z │ 2059655921 │ +│ 6 │ setTimeout is 4x slower than node │ open │ 2023-12-29T12:28:59Z │ 2059611949 │ +│ 7 │ Failing when adding new "workspace:*" dependencies │ open │ 2023-12-29T12:04:12Z │ 2059483332 │ +│ 8 │ feat(build): adjust arch linux auto detect to include manjaro │ open │ 2023-12-29T11:18:32Z │ 2059335554 │ +│ 9 │ Make next-build test less flaky │ open │ 2023-12-29T10:06:08Z │ 2059272416 │ +│ 10 │ Try to make fs.watch tests less flaky │ open │ 2023-12-29T08:14:46Z │ 2059184911 │ +│ 11 │ Compile code that relies on Workers │ open │ 2023-12-29T00:33:49Z │ 2058961498 │ +│ 12 │ Add --ignore-scripts option on bun install command shown in the benchmark │ open │ 2023-12-28T23:09:55Z │ 2058931278 │ +│ 13 │ fix: add ws properties to BunWebSocketMocked prototype │ open │ 2023-12-28T21:24:26Z │ 2058831269 │ +│ 14 │ WebSocket "close" event not firing │ open │ 2023-12-28T19:25:01Z │ 2058755917 │ +│ 15 │ Bun's 'ws' mock is missing readonly instance members │ open │ 2023-12-28T17:48:58Z │ 2058684399 │ +│ 16 │ Bun.Glob onlyDirectories Support │ open │ 2023-12-28T15:13:38Z │ 2058550162 │ +│ 17 │ React@canary ssr with suspense got 'Welcome to Bun! To get started, return a Response object.' in middle of response │ open │ 2023-12-28T12:19:38Z │ 2058371954 │ +│ 18 │ Bun.build with sourcemap: 'inline' changes assets imports URLs │ open │ 2023-12-28T11:10:37Z │ 2058308543 │ +│ 19 │ Can't build with knex module │ open │ 2023-12-28T10:24:59Z │ 2058264026 │ +│ 20 │ OutgoingResponse.writeHead should work │ open │ 2023-12-28T08:00:59Z │ 2058121229 │ +│ 21 │ Add Scoop installation reference for Windows │ open │ 2023-12-28T07:21:55Z │ 2058087851 │ +│ 22 │ Multer-s3 breaks on bun but works fine with node when contentType: multerS3.AUTO_CONTENT_TYPE │ open │ 2023-12-28T03:32:57Z │ 2057948132 │ +│ 23 │ HuggingFace failure │ open │ 2023-12-28T02:08:48Z │ 2057908659 │ +│ 24 │ Unable to handle test generation helper correctly │ open │ 2023-12-28T01:06:19Z │ 2057880878 │ +│ 25 │ peerDependencies not honored over dependencies when both are specified │ open │ 2023-12-27T20:09:48Z │ 2057739037 │ +│ 26 │ Cannot find module 'backend/node_modules/prisma/build/index.js' │ open │ 2023-12-27T14:31:15Z │ 2057441216 │ +│ 27 │ Segmentation Fault Crash when using Prisma │ open │ 2023-12-27T10:28:07Z │ 2057197057 │ +│ 28 │ Ensure duplicated 'devDependencies' are chosen before 'dependencies' │ open │ 2023-12-27T09:32:40Z │ 2057137439 │ +│ 29 │ Enhancing trusted dependency list with checksums and blacklists │ open │ 2023-12-27T09:17:21Z │ 2057121878 │ +└────┴──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┴───────┴──────────────────────┴────────────┘ +" +`; + +exports[`console.table expected output for: headers object 1`] = ` +"┌───┬────────┬────────┐ +│ │ 0 │ 1 │ +├───┼────────┼────────┤ +│ 0 │ abc │ bun │ +│ 1 │ potato │ tomato │ +└───┴────────┴────────┘ +" +`; diff --git a/test/js/bun/console/console-table-json-fixture.json b/test/js/bun/console/console-table-json-fixture.json new file mode 100644 index 0000000000..c189c946f7 --- /dev/null +++ b/test/js/bun/console/console-table-json-fixture.json @@ -0,0 +1,167 @@ +[ + { + "title": "Respond handling issue for Most of Google Cloud API (vision, document, etc.)", + "state": "open", + "created_at": "2023-12-30T04:43:36Z", + "id": 2060630898 + }, + { + "title": "@elastic/elasticsearch@8.11.0 not working due to missing undici.Pool.request", + "state": "open", + "created_at": "2023-12-29T20:26:07Z", + "id": 2060382675 + }, + { "title": "fs.watch skips save events", "state": "open", "created_at": "2023-12-29T14:00:48Z", "id": 2060059045 }, + { + "title": "Remix running with node, not bun", + "state": "open", + "created_at": "2023-12-29T13:52:26Z", + "id": 2060043035 + }, + { + "title": "`fetch` upload with formdata is different than node", + "state": "open", + "created_at": "2023-12-29T13:04:03Z", + "id": 2059869225 + }, + { + "title": "Error throw with an object causes an empty error log line", + "state": "open", + "created_at": "2023-12-29T12:35:33Z", + "id": 2059655921 + }, + { + "title": "setTimeout is 4x slower than node", + "state": "open", + "created_at": "2023-12-29T12:28:59Z", + "id": 2059611949 + }, + { + "title": "Failing when adding new \"workspace:*\" dependencies", + "state": "open", + "created_at": "2023-12-29T12:04:12Z", + "id": 2059483332 + }, + { + "title": "feat(build): adjust arch linux auto detect to include manjaro", + "state": "open", + "created_at": "2023-12-29T11:18:32Z", + "id": 2059335554 + }, + { + "title": "Make next-build test less flaky", + "state": "open", + "created_at": "2023-12-29T10:06:08Z", + "id": 2059272416 + }, + { + "title": "Try to make fs.watch tests less flaky", + "state": "open", + "created_at": "2023-12-29T08:14:46Z", + "id": 2059184911 + }, + { + "title": "Compile code that relies on Workers", + "state": "open", + "created_at": "2023-12-29T00:33:49Z", + "id": 2058961498 + }, + { + "title": "Add --ignore-scripts option on bun install command shown in the benchmark", + "state": "open", + "created_at": "2023-12-28T23:09:55Z", + "id": 2058931278 + }, + { + "title": "fix: add ws properties to BunWebSocketMocked prototype", + "state": "open", + "created_at": "2023-12-28T21:24:26Z", + "id": 2058831269 + }, + { + "title": "WebSocket \"close\" event not firing", + "state": "open", + "created_at": "2023-12-28T19:25:01Z", + "id": 2058755917 + }, + { + "title": "Bun's `ws` mock is missing readonly instance members", + "state": "open", + "created_at": "2023-12-28T17:48:58Z", + "id": 2058684399 + }, + { + "title": "Bun.Glob onlyDirectories Support", + "state": "open", + "created_at": "2023-12-28T15:13:38Z", + "id": 2058550162 + }, + { + "title": "React@canary ssr with suspense got 'Welcome to Bun! To get started, return a Response object.' in middle of response", + "state": "open", + "created_at": "2023-12-28T12:19:38Z", + "id": 2058371954 + }, + { + "title": "Bun.build with sourcemap: 'inline' changes assets imports URLs", + "state": "open", + "created_at": "2023-12-28T11:10:37Z", + "id": 2058308543 + }, + { "title": "Can't build with knex module", "state": "open", "created_at": "2023-12-28T10:24:59Z", "id": 2058264026 }, + { + "title": "OutgoingResponse.writeHead should work", + "state": "open", + "created_at": "2023-12-28T08:00:59Z", + "id": 2058121229 + }, + { + "title": "Add Scoop installation reference for Windows", + "state": "open", + "created_at": "2023-12-28T07:21:55Z", + "id": 2058087851 + }, + { + "title": "Multer-s3 breaks on bun but works fine with node when contentType: multerS3.AUTO_CONTENT_TYPE", + "state": "open", + "created_at": "2023-12-28T03:32:57Z", + "id": 2057948132 + }, + { "title": "HuggingFace failure", "state": "open", "created_at": "2023-12-28T02:08:48Z", "id": 2057908659 }, + { + "title": "Unable to handle test generation helper correctly", + "state": "open", + "created_at": "2023-12-28T01:06:19Z", + "id": 2057880878 + }, + { + "title": "peerDependencies not honored over dependencies when both are specified", + "state": "open", + "created_at": "2023-12-27T20:09:48Z", + "id": 2057739037 + }, + { + "title": "Cannot find module 'backend/node_modules/prisma/build/index.js'", + "state": "open", + "created_at": "2023-12-27T14:31:15Z", + "id": 2057441216 + }, + { + "title": "Segmentation Fault Crash when using Prisma", + "state": "open", + "created_at": "2023-12-27T10:28:07Z", + "id": 2057197057 + }, + { + "title": "Ensure duplicated `devDependencies` are chosen before `dependencies`", + "state": "open", + "created_at": "2023-12-27T09:32:40Z", + "id": 2057137439 + }, + { + "title": "Enhancing trusted dependency list with checksums and blacklists", + "state": "open", + "created_at": "2023-12-27T09:17:21Z", + "id": 2057121878 + } +] diff --git a/test/js/bun/console/console-table.test.ts b/test/js/bun/console/console-table.test.ts index de16ffbc7f..d2f7212451 100644 --- a/test/js/bun/console/console-table.test.ts +++ b/test/js/bun/console/console-table.test.ts @@ -15,113 +15,60 @@ describe("console.table", () => { "not object (number)", { args: () => [42], - output: `42\n`, }, ], [ "not object (string)", { args: () => ["bun"], - output: `bun\n`, }, ], [ "object - empty", { args: () => [{}], - output: `┌─────────┐ -│ (index) │ -├─────────┤ -└─────────┘ -`, }, ], [ "object", { args: () => [{ a: 42, b: "bun" }], - output: `┌─────────┬────────┐ -│ (index) │ Values │ -├─────────┼────────┤ -│ a │ 42 │ -│ b │ "bun" │ -└─────────┴────────┘ -`, }, ], [ "array - empty", { args: () => [[]], - output: `┌─────────┐ -│ (index) │ -├─────────┤ -└─────────┘ -`, }, ], [ "array - plain", { args: () => [[42, "bun"]], - output: `┌─────────┬────────┐ -│ (index) │ Values │ -├─────────┼────────┤ -│ 0 │ 42 │ -│ 1 │ "bun" │ -└─────────┴────────┘ -`, }, ], [ "array - object", { args: () => [[{ a: 42, b: "bun" }]], - output: `┌─────────┬────┬───────┐ -│ (index) │ a │ b │ -├─────────┼────┼───────┤ -│ 0 │ 42 │ "bun" │ -└─────────┴────┴───────┘ -`, }, ], [ "array - objects with diff props", { args: () => [[{ b: "bun" }, { a: 42 }]], - output: `┌─────────┬───────┬────┐ -│ (index) │ b │ a │ -├─────────┼───────┼────┤ -│ 0 │ "bun" │ │ -│ 1 │ │ 42 │ -└─────────┴───────┴────┘ -`, }, ], [ "array - mixed", { args: () => [[{ a: 42, b: "bun" }, 42]], - output: `┌─────────┬────┬───────┬────────┐ -│ (index) │ a │ b │ Values │ -├─────────┼────┼───────┼────────┤ -│ 0 │ 42 │ "bun" │ │ -│ 1 │ │ │ 42 │ -└─────────┴────┴───────┴────────┘ -`, }, ], [ "set", { args: () => [new Set([42, "bun"])], - output: `┌───────────────────┬────────┐ -│ (iteration index) │ Values │ -├───────────────────┼────────┤ -│ 0 │ 42 │ -│ 1 │ "bun" │ -└───────────────────┴────────┘ -`, }, ], [ @@ -134,38 +81,18 @@ describe("console.table", () => { [42, "c"], ]), ], - output: `┌───────────────────┬─────┬────────┐ -│ (iteration index) │ Key │ Values │ -├───────────────────┼─────┼────────┤ -│ 0 │ "a" │ 42 │ -│ 1 │ "b" │ "bun" │ -│ 2 │ 42 │ "c" │ -└───────────────────┴─────┴────────┘ -`, }, ], [ "properties", { args: () => [[{ a: 42, b: "bun" }], ["b", "c", "a"]], - output: `┌─────────┬───────┬───┬────┐ -│ (index) │ b │ c │ a │ -├─────────┼───────┼───┼────┤ -│ 0 │ "bun" │ │ 42 │ -└─────────┴───────┴───┴────┘ -`, }, ], [ "properties - empty", { args: () => [[{ a: 42, b: "bun" }], []], - output: `┌─────────┐ -│ (index) │ -├─────────┤ -│ 0 │ -└─────────┘ -`, }, ], [ @@ -184,18 +111,20 @@ describe("console.table", () => { }, ], ], - output: `┌─────────┬─────────────────────────────────┐ -│ (index) │ value │ -├─────────┼─────────────────────────────────┤ -│ 0 │ { a: 42, b: "bun" } │ -│ 1 │ [ 42, "bun" ] │ -│ 2 │ Set(2) { 42, "bun" } │ -│ 3 │ Map(2) { 42: "bun", "bun": 42 } │ -└─────────┴─────────────────────────────────┘ -`, }, ], - ])("expected output for: %s", (label, { args, output }) => { + [ + "headers object", + { + args: () => [ + new Headers([ + ["abc", "bun"], + ["potato", "tomato"], + ]), + ], + }, + ], + ])("expected output for: %s", (label, { args }) => { const { stdout } = spawnSync({ cmd: [bunExe(), `${import.meta.dir}/console-table-run.ts`, args.toString()], stdout: "pipe", @@ -204,6 +133,27 @@ describe("console.table", () => { }); const actualOutput = stdout.toString(); - expect(actualOutput).toBe(output); + expect(actualOutput).toMatchSnapshot(); + console.log(actualOutput); }); }); + +test("console.table json fixture", () => { + const { stdout } = spawnSync({ + cmd: [ + bunExe(), + `${import.meta.dir}/console-table-run.ts`, + `(() => [${JSON.stringify(require("./console-table-json-fixture.json"), null, 2)}])`, + ], + stdout: "pipe", + stderr: "inherit", + env: bunEnv, + }); + + const actualOutput = stdout + .toString() + // todo: fix bug causing this to be necessary: + .replaceAll("`", "'"); + expect(actualOutput).toMatchSnapshot(); + console.log(actualOutput); +}); diff --git a/test/js/node/readline/getStringWidth.test.ts b/test/js/node/readline/getStringWidth.test.ts new file mode 100644 index 0000000000..2dd201472d --- /dev/null +++ b/test/js/node/readline/getStringWidth.test.ts @@ -0,0 +1,37 @@ +import readline from "node:readline"; + +var { + utils: { getStringWidth }, + // @ts-ignore +} = readline[Symbol.for("__BUN_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED__")]; + +it("handles invisible ASCII character at any position", () => { + const visible = "a"; + const invisible = String.fromCharCode(3); + for (let i = 0; i < 48; i++) { + const str = visible.repeat(i) + invisible + visible.repeat(48 - i); + + expect(getStringWidth(str)).toBe(48); + } +}); + +it("handles visible ASCII character at any position", () => { + const visible = "a"; + const invisible = String.fromCharCode(3); + for (let i = 0; i < 48; i++) { + const str = invisible.repeat(i) + visible + invisible.repeat(48 - i); + + expect(getStringWidth(str)).toBe(1); + } +}); + +it("handles alternating characters", () => { + // In node, this is `process.binding("icu").getStringWidth` + expect(getStringWidth("あ")).toBe(2); + expect(getStringWidth("'あ")).toBe(3); + expect(getStringWidth("ああ")).toBe(4); + expect(getStringWidth("あああ")).toBe(6); + expect(getStringWidth("'あああ")).toBe(7); + expect(getStringWidth('"あああ')).toBe(7); + expect(getStringWidth('"あああ"')).toBe(8); +});