diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index bedb08acf6676..6f77fc08184bb 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -150,13 +150,12 @@ pub const SavedSourceMap = struct { Output.errorWriter(), logger.Kind.warn, true, - false, ); } else { try fail.toData(path).writeFormat( Output.errorWriter(), logger.Kind.warn, - false, + false, ); } @@ -2022,6 +2021,10 @@ pub const VirtualMachine = struct { if (!result.isEmptyOrUndefinedOrNull()) this.last_reported_error_for_dedupe = result; + var prev_had_errors = this.had_errors; + this.had_errors = false; + defer this.had_errors = prev_had_errors; + if (result.isException(this.global.vm())) { var exception = @as(*Exception, @ptrCast(result.asVoid())); @@ -2429,9 +2432,12 @@ pub const VirtualMachine = struct { if (value.as(JSC.BuildMessage)) |build_error| { defer Output.flush(); if (!build_error.logged) { + if (this.had_errors) { + writer.writeAll("\n") catch {}; + } build_error.msg.writeFormat(writer, allow_ansi_color) catch {}; - writer.writeAll("\n") catch {}; build_error.logged = true; + writer.writeAll("\n") catch {}; } this.had_errors = this.had_errors or build_error.msg.kind == .err; if (exception_list != null) { @@ -2443,8 +2449,12 @@ pub const VirtualMachine = struct { } else if (value.as(JSC.ResolveMessage)) |resolve_error| { defer Output.flush(); if (!resolve_error.logged) { + if (this.had_errors) { + writer.writeAll("\n") catch {}; + } resolve_error.msg.writeFormat(writer, allow_ansi_color) catch {}; resolve_error.logged = true; + writer.writeAll("\n") catch {}; } this.had_errors = this.had_errors or resolve_error.msg.kind == .err; @@ -2683,12 +2693,14 @@ pub const VirtualMachine = struct { } } - pub fn printErrorInstance(this: *VirtualMachine, error_instance: JSValue, exception_list: ?*ExceptionList, comptime Writer: type, writer: Writer, comptime allow_ansi_color: bool, comptime allow_side_effects: bool) !void { + pub fn printErrorInstance(this: *VirtualMachine, error_instance: JSValue, exception_list: ?*ExceptionList, comptime Writer: type, writer: Writer, comptime allow_ansi_color: bool, comptime allow_side_effects: bool) anyerror!void { var exception_holder = ZigException.Holder.init(); var exception = exception_holder.zigException(); defer exception_holder.deinit(); this.remapZigException(exception, error_instance, exception_list); + var prev_had_errors = this.had_errors; this.had_errors = true; + defer this.had_errors = prev_had_errors; if (allow_side_effects) { defer if (this.on_exception) |cb| { @@ -2807,27 +2819,41 @@ pub const VirtualMachine = struct { "info", "pkg", "errors", + "cause", }; + // This is usually unsafe to do, but we are protecting them each time first + var errors_to_append = std.ArrayList(JSC.JSValue).init(this.allocator); + defer { + for (errors_to_append.items) |err| { + err.unprotect(); + } + errors_to_append.deinit(); + } + if (error_instance != .zero and error_instance.isCell() and error_instance.jsType().canGet()) { inline for (extra_fields) |field| { - if (error_instance.get(this.global, field)) |value| { - if (!value.isEmptyOrUndefinedOrNull()) { - const kind = value.jsType(); - if (kind.isStringLike()) { - if (value.toStringOrNull(this.global)) |str| { - var zig_str = str.toSlice(this.global, bun.default_allocator); - defer zig_str.deinit(); - try writer.print(comptime Output.prettyFmt(" {s}: \"{s}\"\n", allow_ansi_color), .{ field, zig_str.slice() }); - add_extra_line = true; - } - } else if (kind.isObject() or kind.isArray()) { - var bun_str = bun.String.empty; - defer bun_str.deref(); - value.jsonStringify(this.global, 2, &bun_str); //2 - try writer.print(comptime Output.prettyFmt(" {s}: {any}\n", allow_ansi_color), .{ field, bun_str }); + if (error_instance.getTruthy(this.global, field)) |value| { + const kind = value.jsType(); + if (kind.isStringLike()) { + if (value.toStringOrNull(this.global)) |str| { + var zig_str = str.toSlice(this.global, bun.default_allocator); + defer zig_str.deinit(); + try writer.print(comptime Output.prettyFmt(" {s}: \"{s}\"\n", allow_ansi_color), .{ field, zig_str.slice() }); add_extra_line = true; } + } else if (kind == .ErrorInstance and + // avoid infinite recursion + !prev_had_errors) + { + value.protect(); + try errors_to_append.append(value); + } else if (kind.isObject() or kind.isArray()) { + var bun_str = bun.String.empty; + defer bun_str.deref(); + value.jsonStringify(this.global, 2, &bun_str); //2 + try writer.print(comptime Output.prettyFmt(" {s}: {any}\n", allow_ansi_color), .{ field, bun_str }); + add_extra_line = true; } } } @@ -2878,6 +2904,11 @@ pub const VirtualMachine = struct { if (add_extra_line) try writer.writeAll("\n"); try printStackTrace(@TypeOf(writer), writer, exception.stack, allow_ansi_color); + + for (errors_to_append.items) |err| { + try writer.writeAll("\n"); + try this.printErrorInstance(err, exception_list, Writer, writer, allow_ansi_color, allow_side_effects); + } } fn printErrorNameAndMessage(_: *VirtualMachine, name: String, message: String, comptime Writer: type, writer: Writer, comptime allow_ansi_color: bool) !void { diff --git a/src/js_ast.zig b/src/js_ast.zig index 8a2fa3912a902..b9ddedbab3c3a 100644 --- a/src/js_ast.zig +++ b/src/js_ast.zig @@ -7265,7 +7265,7 @@ pub const Macro = struct { this.source, this.caller.loc, this.allocator, - "cannot coerce {s} to Bun's AST. Please return a valid macro using the JSX syntax", + "cannot coerce {s} to Bun's AST. Please return a simpler type", .{@tagName(value.jsType())}, ) catch unreachable; return error.MacroFailed; diff --git a/src/js_lexer.zig b/src/js_lexer.zig index 24484a02bbfcd..17c70026a1ac6 100644 --- a/src/js_lexer.zig +++ b/src/js_lexer.zig @@ -3126,7 +3126,6 @@ pub fn rangeOfIdentifier(source: *const Source, loc: logger.Loc) logger.Range { } if (isIdentifierStart(cursor.c) or cursor.c == '\\') { - defer r.len = @as(i32, @intCast(cursor.i)); while (iter.next(&cursor)) { if (cursor.c == '\\') { @@ -3144,9 +3143,12 @@ pub fn rangeOfIdentifier(source: *const Source, loc: logger.Loc) logger.Range { } } } else if (!isIdentifierContinue(cursor.c)) { + r.len = @as(i32, @intCast(cursor.i)); return r; } } + + r.len = @as(i32, @intCast(cursor.i)); } // const offset = @intCast(usize, loc.start); diff --git a/src/js_parser.zig b/src/js_parser.zig index a4fdbe25c7bd3..9a5185a1a3582 100644 --- a/src/js_parser.zig +++ b/src/js_parser.zig @@ -3635,6 +3635,52 @@ pub const Parser = struct { wrapper_expr = .{ .bun_js = {}, }; + + const import_record: ?*const ImportRecord = brk: { + for (p.import_records.items) |*import_record| { + if (import_record.is_internal or import_record.is_unused) continue; + if (import_record.kind == .stmt) break :brk import_record; + } + + break :brk null; + }; + + // make it an error to use an import statement with a commonjs exports usage + if (import_record) |record| { + // find the usage of the export symbol + + var notes = ListManaged(logger.Data).init(p.allocator); + + try notes.append(logger.Data{ + .text = try std.fmt.allocPrint(p.allocator, "Try require({}) instead", .{strings.QuotedFormatter{ .text = record.path.text }}), + }); + + if (uses_module_ref) { + try notes.append(logger.Data{ + .text = "This file is CommonJS because 'module' was used", + }); + } + + if (uses_exports_ref) { + try notes.append(logger.Data{ + .text = "This file is CommonJS because 'exports' was used", + }); + } + + if (p.has_top_level_return) { + try notes.append(logger.Data{ + .text = "This file is CommonJS because top-level return was used", + }); + } + + if (p.has_with_scope) { + try notes.append(logger.Data{ + .text = "This file is CommonJS because a \"with\" statement is used", + }); + } + + try p.log.addRangeErrorWithNotes(p.source, record.range, "Cannot use import statement with CommonJS-only features", notes.items); + } } else if (!p.options.bundle and !p.options.features.commonjs_at_runtime and (!p.options.transform_only or p.options.features.dynamic_require)) { if (p.options.legacy_transform_require_to_import or p.options.features.dynamic_require) { var args = p.allocator.alloc(Expr, 2) catch unreachable; diff --git a/src/logger.zig b/src/logger.zig index 3ef18043b5dff..c78d07fd21ed3 100644 --- a/src/logger.zig +++ b/src/logger.zig @@ -192,8 +192,8 @@ pub const Location = struct { .namespace = source.path.namespace, .line = usize2Loc(data.line_count).start, .column = usize2Loc(data.column_count).start, - .length = full_line.len, - .line_text = full_line, + .length = if (r.len > -1) @as(u32, @intCast(r.len)) else 1, + .line_text = std.mem.trimLeft(u8, full_line, "\n\r"), .offset = @as(usize, @intCast(@max(r.loc.start, 0))), }; } else { @@ -257,95 +257,106 @@ pub const Data = struct { to: anytype, kind: Kind, comptime enable_ansi_colors: bool, - comptime is_note: bool, ) !void { if (this.text.len == 0) return; const message_color = switch (kind) { .err => comptime Output.color_map.get("b").?, - .note => comptime Output.color_map.get("cyan").? ++ Output.color_map.get("d").?, + .note => comptime Output.color_map.get("blue").?, else => comptime Output.color_map.get("d").? ++ Output.color_map.get("b").?, }; const color_name: string = switch (kind) { .err => comptime Output.color_map.get("red").?, - .note => comptime Output.color_map.get("cyan").?, + .note => comptime Output.color_map.get("blue").?, else => comptime Output.color_map.get("d").?, }; - try to.writeAll("\n\n"); - - if (comptime enable_ansi_colors) { - try to.writeAll(color_name); - } - - try to.writeAll(kind.string()); - - try std.fmt.format(to, comptime Output.prettyFmt(": ", enable_ansi_colors), .{}); - - if (comptime enable_ansi_colors) { - try to.writeAll(message_color); - } - - try std.fmt.format(to, comptime Output.prettyFmt("{s}\n", enable_ansi_colors), .{this.text}); - - if (this.location) |location| { + if (this.location) |*location| { if (location.line_text) |line_text_| { - const line_text = std.mem.trimRight(u8, line_text_, "\r\n\t"); + const location_in_line_text_original = @as(usize, @intCast(@max(location.column, 1) - 1)); + + const line_text_right_trimmed = std.mem.trimRight(u8, line_text_, " \r\n\t"); + const line_text = std.mem.trimLeft(u8, line_text_right_trimmed, "\n\r"); + const line_text_left_trimmed_offset = line_text_right_trimmed.len -| line_text.len; + const location_in_line_text: usize = line_text_left_trimmed_offset + (location_in_line_text_original -| @as(usize, @intCast(line_text_.len -| line_text.len))); - const location_in_line_text = @as(u32, @intCast(@max(location.column, 1) - 1)); const has_position = location.column > -1 and line_text.len > 0 and location_in_line_text < line_text.len; + var line_offset_for_second_line: usize = location_in_line_text; + if (has_position) { if (comptime enable_ansi_colors) { const is_colored = message_color.len > 0; - const before_segment = line_text[0..location_in_line_text]; + var before_segment = line_text[0..location_in_line_text]; + if (before_segment.len > 40) { + before_segment = before_segment[before_segment.len - 40 ..]; + } - try to.writeAll(before_segment); - if (is_colored) { - try to.writeAll(color_name); + if (location.line > -1) { + try std.fmt.format(to, comptime Output.prettyFmt("{d} | ", true), .{ + location.line, + }); } + try to.writeAll(before_segment); + const rest_of_line = line_text[location_in_line_text..]; + line_offset_for_second_line = before_segment.len + " | ".len + bun.fmt.fastDigitCount(@intCast(location.line)); - if (rest_of_line.len > 0) { - var end_of_segment: usize = 1; - var iter = strings.CodepointIterator.initOffset(rest_of_line, 1); - // extremely naive: we should really use IsIdentifierContinue || isIdentifierStart here + const end_of_segment: usize = brk: { + if (rest_of_line.len == 0) break :brk 0; + if (location.length > 0 and location.length < rest_of_line.len) { + break :brk location.length; + } - // highlight until we reach the next matching + var iter = strings.CodepointIterator.initOffset(rest_of_line, 1); switch (line_text[location_in_line_text]) { '\'' => { - end_of_segment = iter.scanUntilQuotedValueOrEOF('\''); + break :brk iter.scanUntilQuotedValueOrEOF('\''); }, '"' => { - end_of_segment = iter.scanUntilQuotedValueOrEOF('"'); + break :brk iter.scanUntilQuotedValueOrEOF('"'); }, '<' => { - end_of_segment = iter.scanUntilQuotedValueOrEOF('>'); + break :brk iter.scanUntilQuotedValueOrEOF('>'); }, '`' => { - end_of_segment = iter.scanUntilQuotedValueOrEOF('`'); + break :brk iter.scanUntilQuotedValueOrEOF('`'); }, else => {}, } - try to.writeAll(rest_of_line[0..end_of_segment]); + + break :brk 1; + }; + + var middle_segment: []const u8 = rest_of_line[0..end_of_segment]; + + if (middle_segment.len > 0) { + try to.writeAll(Output.color_map.get("b").?); + try to.writeAll(middle_segment); + if (is_colored) { try to.writeAll("\x1b[0m"); } - try to.writeAll(rest_of_line[end_of_segment..]); + var after = rest_of_line[middle_segment.len..]; + if (after.len > 40) { + after = after[0..40]; + } + try to.writeAll(after); } else if (is_colored) { try to.writeAll("\x1b[0m"); } } else { + line_offset_for_second_line = location_in_line_text; try to.writeAll(line_text); } try to.writeAll("\n"); - try to.writeByteNTimes(' ', location_in_line_text); + try to.writeByteNTimes(' ', line_offset_for_second_line); if (comptime enable_ansi_colors) { const is_colored = message_color.len > 0; if (is_colored) { @@ -365,15 +376,28 @@ pub const Data = struct { } } } + } + + if (comptime enable_ansi_colors) { + try to.writeAll(color_name); + } + try to.writeAll(kind.string()); + + try std.fmt.format(to, comptime Output.prettyFmt(": ", enable_ansi_colors), .{}); + + if (comptime enable_ansi_colors) { + try to.writeAll(message_color); + } + + try std.fmt.format(to, comptime Output.prettyFmt("{s}", enable_ansi_colors), .{this.text}); + + if (this.location) |*location| { if (location.file.len > 0) { - if (comptime enable_ansi_colors) { - if (!is_note and kind == .err) { - try to.writeAll(comptime Output.color_map.get("b").?); - } - } + try to.writeAll("\n"); + try to.writeByteNTimes(' ', (kind.string().len + ": ".len) - "at ".len); - try std.fmt.format(to, comptime Output.prettyFmt("{s}", enable_ansi_colors), .{ + try std.fmt.format(to, comptime Output.prettyFmt("at {s}", enable_ansi_colors), .{ location.file, }); @@ -542,11 +566,17 @@ pub const Msg = struct { to: anytype, comptime enable_ansi_colors: bool, ) !void { - try msg.data.writeFormat(to, msg.kind, enable_ansi_colors, false); + try msg.data.writeFormat(to, msg.kind, enable_ansi_colors); if (msg.notes) |notes| { + if (notes.len > 0) { + try to.writeAll("\n"); + } + for (notes) |note| { - try note.writeFormat(to, .note, enable_ansi_colors, true); + try to.writeAll("\n"); + + try note.writeFormat(to, .note, enable_ansi_colors); } } } @@ -1245,42 +1275,43 @@ pub const Log = struct { } pub fn printForLogLevelWithEnableAnsiColors(self: *Log, to: anytype, comptime enable_ansi_colors: bool) !void { - var printed = false; - + var needs_newline = false; if (self.warnings > 0 and self.errors > 0) { // Print warnings at the top // errors at the bottom // This is so if you're reading from a terminal // and there are a bunch of warnings // You can more easily see where the errors are - - for (self.msgs.items) |msg| { + for (self.msgs.items) |*msg| { if (msg.kind != .err) { if (msg.kind.shouldPrint(self.level)) { + if (needs_newline) try to.writeAll("\n\n"); try msg.writeFormat(to, enable_ansi_colors); - printed = true; + needs_newline = true; } } } - for (self.msgs.items) |msg| { + for (self.msgs.items) |*msg| { if (msg.kind == .err) { if (msg.kind.shouldPrint(self.level)) { + if (needs_newline) try to.writeAll("\n\n"); try msg.writeFormat(to, enable_ansi_colors); - printed = true; + needs_newline = true; } } } } else { - for (self.msgs.items) |msg| { + for (self.msgs.items) |*msg| { if (msg.kind.shouldPrint(self.level)) { + if (needs_newline) try to.writeAll("\n\n"); try msg.writeFormat(to, enable_ansi_colors); - printed = true; + needs_newline = true; } } } - if (printed) _ = try to.write("\n"); + if (needs_newline) _ = try to.write("\n"); } pub fn toZigException(this: *const Log, allocator: std.mem.Allocator) *js.ZigException.Holder { diff --git a/test/bundler/expectBundled.ts b/test/bundler/expectBundled.ts index 3ec68ac4e12b7..7486deb2880dd 100644 --- a/test/bundler/expectBundled.ts +++ b/test/bundler/expectBundled.ts @@ -14,6 +14,39 @@ import * as esbuild from "esbuild"; let currentFile: string | undefined; +function errorOrWarnParser(isError = true) { + const prefix = isError ? "error: " : "warn: "; + return function (text: string) { + var i = 0; + var list = []; + while (i < text.length) { + let errorLineI = text.indexOf(prefix, i); + if (errorLineI === -1) { + return list; + } + const message = text.slice(errorLineI + prefix.length, text.indexOf("\n", errorLineI + 1)); + i = errorLineI + 1; + const fileLineI = text.indexOf(" at ", errorLineI + message.length); + + let fileLine = ""; + if (fileLineI !== -1) { + const fileLineEnd = text.indexOf("\n", fileLineI + 1); + fileLine = text.slice(fileLineI + "\n at ".length, fileLineEnd); + i = fileLineEnd; + } + list.push([message, fileLine]); + + if (i === -1) { + break; + } + } + return list; + }; +} + +const errorParser = errorOrWarnParser(true); +const warnParser = errorOrWarnParser(false); + type BunTestExports = typeof import("bun:test"); export function testForFile(file: string): BunTestExports { if (file.startsWith("file://")) { @@ -653,15 +686,16 @@ function expectBundled( if (!success) { if (!ESBUILD) { const errorText = stderr.toString("utf-8"); - const errorRegex = /^error: (.*?)\n(?:.*?\n\s*\^\s*\n(.*?)\n)?/gms; + var skip = false; if (errorText.includes("----- bun meta -----")) { skip = true; } + const allErrors = skip ? [] - : ([...errorText.matchAll(errorRegex)] - .map(([_str1, error, source]) => { + : (errorParser(errorText) + .map(([error, source]) => { if (!source) { if (error === "FileNotFound") { return null; @@ -674,7 +708,6 @@ function expectBundled( return { error, file, line, col }; }) .filter(Boolean) as any[]); - if (allErrors.length === 0) { console.log(errorText); } @@ -746,8 +779,8 @@ function expectBundled( // Check for warnings if (!ESBUILD) { - const warningRegex = /^warn: (.*?)\n.*?\n\s*\^\s*\n(.*?)\n/gms; - const allWarnings = [...stderr!.toString("utf-8").matchAll(warningRegex)].map(([_str1, error, source]) => { + const warningText = stderr!.toString("utf-8"); + const allWarnings = warnParser(warningText).map(([error, source]) => { const [_str2, fullFilename, line, col] = source.match(/bun-build-tests\/(.*):(\d+):(\d+)/)!; const file = fullFilename.slice(id.length + path.basename(outBase).length + 1); return { error, file, line, col }; diff --git a/test/cli/install/__snapshots__/bun-install.test.ts.snap b/test/cli/install/__snapshots__/bun-install.test.ts.snap new file mode 100644 index 0000000000000..d0204559ac8ab --- /dev/null +++ b/test/cli/install/__snapshots__/bun-install.test.ts.snap @@ -0,0 +1,56 @@ +// Bun Snapshot v1, https://goo.gl/fbAQLP + +exports[`should report error on invalid format for package.json 1`] = ` +"bun install +foo +^ +error: Unexpected foo + at [dir]/package.json:1:1 0 +ParserError parsing package.json in "[dir]/" +" +`; + +exports[`should report error on invalid format for dependencies 1`] = ` +"bun install +{"name":"foo","version":"0.0.1","dependencies":[]} + ^ +error: dependencies expects a map of specifiers, e.g. +"dependencies": { + "bun": "latest" +} + at [dir]/package.json:1:33 32 +" +`; + +exports[`should report error on invalid format for optionalDependencies 1`] = ` +"bun install +{"name":"foo","version":"0.0.1","optionalDependencies":"bar"} + ^ +error: optionalDependencies expects a map of specifiers, e.g. +"optionalDependencies": { + "bun": "latest" +} + at [dir]/package.json:1:33 32 +" +`; + +exports[`should report error on invalid format for workspaces 1`] = ` +"bun install +{"name":"foo","version":"0.0.1","workspaces":{"packages":{"bar":true}}} + ^ +error: Workspaces expects an array of strings, e.g. +"workspaces": [ + "path/to/package" +] + at [dir]/package.json:1:33 32 +" +`; + +exports[`should report error on duplicated workspace packages 1`] = ` +"bun install +{"name":"foo","version":"0.0.1","workspaces":["bar","baz"]} +^ +error: Workspace name "moo" already exists + at [dir]/package.json:1:1 0 +" +`; diff --git a/test/cli/install/bun-install.test.ts b/test/cli/install/bun-install.test.ts index d32edfa5db25b..a531683b789b9 100644 --- a/test/cli/install/bun-install.test.ts +++ b/test/cli/install/bun-install.test.ts @@ -1,6 +1,6 @@ import { file, listen, Socket, spawn } from "bun"; import { afterAll, afterEach, beforeAll, beforeEach, expect, it, describe, test } from "bun:test"; -import { bunExe, bunEnv as env } from "harness"; +import { bunExe, bunEnv as env, ignoreMimallocWarning, withoutMimalloc } from "harness"; import { access, mkdir, readlink, realpath, rm, writeFile } from "fs/promises"; import { join } from "path"; import { @@ -21,6 +21,8 @@ afterAll(dummyAfterAll); beforeEach(dummyBeforeEach); afterEach(dummyAfterEach); +ignoreMimallocWarning({ beforeAll, afterAll }); + describe("chooses", () => { async function runTest(latest: string, range: string, chosen = "0.0.5") { const exeName: string = { @@ -4180,17 +4182,7 @@ it("should report error on invalid format for package.json", async () => { }); expect(stderr).toBeDefined(); const err = await new Response(stderr).text(); - expect(err.replace(/^bun install v.+\n/, "bun install\n").split(/\r?\n/)).toEqual([ - "bun install", - "", - "", - "error: Unexpected foo", - "foo", - "^", - `${package_dir}/package.json:1:1 0`, - `ParserError parsing package.json in "${package_dir}/"`, - "", - ]); + expect(err.replace(/^bun install v.+\n/, "bun install\n").replaceAll(package_dir, "[dir]")).toMatchSnapshot(); expect(stdout).toBeDefined(); const out = await new Response(stdout).text(); expect(out).toEqual(""); @@ -4216,19 +4208,7 @@ it("should report error on invalid format for dependencies", async () => { }); expect(stderr).toBeDefined(); const err = await new Response(stderr).text(); - expect(err.replace(/^bun install v.+\n/, "bun install\n").split(/\r?\n/)).toEqual([ - "bun install", - "", - "", - "error: dependencies expects a map of specifiers, e.g.", - '"dependencies": {', - ' "bun": "latest"', - "}", - '{"name":"foo","version":"0.0.1","dependencies":[]}', - " ^", - `${package_dir}/package.json:1:33 32`, - "", - ]); + expect(err.replace(/^bun install v.+\n/, "bun install\n").replaceAll(package_dir, "[dir]")).toMatchSnapshot(); expect(stdout).toBeDefined(); const out = await new Response(stdout).text(); expect(out).toEqual(""); @@ -4254,19 +4234,7 @@ it("should report error on invalid format for optionalDependencies", async () => }); expect(stderr).toBeDefined(); const err = await new Response(stderr).text(); - expect(err.replace(/^bun install v.+\n/, "bun install\n").split(/\r?\n/)).toEqual([ - "bun install", - "", - "", - "error: optionalDependencies expects a map of specifiers, e.g.", - '"optionalDependencies": {', - ' "bun": "latest"', - "}", - '{"name":"foo","version":"0.0.1","optionalDependencies":"bar"}', - " ^", - `${package_dir}/package.json:1:33 32`, - "", - ]); + expect(err.replace(/^bun install v.+\n/, "bun install\n").replaceAll(package_dir, "[dir]")).toMatchSnapshot(); expect(stdout).toBeDefined(); const out = await new Response(stdout).text(); expect(out).toEqual(""); @@ -4294,19 +4262,7 @@ it("should report error on invalid format for workspaces", async () => { }); expect(stderr).toBeDefined(); const err = await new Response(stderr).text(); - expect(err.replace(/^bun install v.+\n/, "bun install\n").split(/\r?\n/)).toEqual([ - "bun install", - "", - "", - "error: Workspaces expects an array of strings, e.g.", - '"workspaces": [', - ' "path/to/package"', - "]", - '{"name":"foo","version":"0.0.1","workspaces":{"packages":{"bar":true}}}', - " ^", - `${package_dir}/package.json:1:33 32`, - "", - ]); + expect(err.replace(/^bun install v.+\n/, "bun install\n").replaceAll(package_dir, "[dir]")).toMatchSnapshot(); expect(stdout).toBeDefined(); const out = await new Response(stdout).text(); expect(out).toEqual(""); @@ -4348,17 +4304,7 @@ it("should report error on duplicated workspace packages", async () => { }); expect(stderr).toBeDefined(); const err = await new Response(stderr).text(); - expect(err.replace(/^bun install v.+\n/, "bun install\n").split(/\r?\n/)).toEqual([ - "bun install", - "", - "", - 'error: Workspace name "moo" already exists', - '{"name":"foo","version":"0.0.1","workspaces":["bar","baz"]}', - // we don't have a name location anymore - "^", - `${package_dir}/package.json:1:1 0`, - "", - ]); + expect(err.replace(/^bun install v.+\n/, "bun install\n").replaceAll(package_dir, "[dir]")).toMatchSnapshot(); expect(stdout).toBeDefined(); const out = await new Response(stdout).text(); expect(out).toEqual(""); diff --git a/test/cli/install/registry/bun-install-registry.test.ts b/test/cli/install/registry/bun-install-registry.test.ts index 89b0264027642..540deec697da8 100644 --- a/test/cli/install/registry/bun-install-registry.test.ts +++ b/test/cli/install/registry/bun-install-registry.test.ts @@ -1,5 +1,5 @@ import { file, spawn } from "bun"; -import { bunExe, bunEnv as env } from "harness"; +import { bunExe, bunEnv as env, ignoreMimallocWarning } from "harness"; import { join } from "path"; import { mkdtempSync, realpathSync } from "fs"; import { rm, writeFile, mkdir, exists, cp } from "fs/promises"; @@ -13,6 +13,8 @@ var testCounter: number = 0; var port: number = 4784; var packageDir: string; +ignoreMimallocWarning({ beforeAll, afterAll }); + beforeAll(async done => { verdaccioServer = fork( await import.meta.resolve("verdaccio/bin/verdaccio"), @@ -382,9 +384,7 @@ test("it should install with missing bun.lockb, node_modules, and/or cache", asy var out = await new Response(stdout).text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); - if (!err.includes("mimalloc: warning")) { - expect(err).not.toContain("error:"); - } + expect(err).not.toContain("error:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ " + dep-loop-entry@1.0.0", " + dep-with-tags@3.0.0", @@ -404,7 +404,7 @@ test("it should install with missing bun.lockb, node_modules, and/or cache", asy expect(await exited).toBe(0); // delete node_modules - await await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], @@ -415,13 +415,11 @@ test("it should install with missing bun.lockb, node_modules, and/or cache", asy env, })); - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); + [err, out] = await Promise.all([new Response(stderr).text(), new Response(stdout).text()]); + expect(err).not.toContain("Saved lockfile"); expect(err).not.toContain("not found"); - if (!err.includes("mimalloc: warning")) { - expect(err).not.toContain("error:"); - } + expect(err).not.toContain("error:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ " + dep-loop-entry@1.0.0", " + dep-with-tags@3.0.0", @@ -453,8 +451,8 @@ test("it should install with missing bun.lockb, node_modules, and/or cache", asy env, })); - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); + [err, out] = await Promise.all([new Response(stderr).text(), new Response(stdout).text()]); + expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); if (!err.includes("mimalloc: warning")) { @@ -464,6 +462,7 @@ test("it should install with missing bun.lockb, node_modules, and/or cache", asy "", expect.stringContaining("Checked 19 installs across 23 packages (no changes)"), ]); + expect(await exited).toBe(0); } @@ -479,13 +478,11 @@ test("it should install with missing bun.lockb, node_modules, and/or cache", asy env, })); - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); + [err, out] = await Promise.all([new Response(stderr).text(), new Response(stdout).text()]); + expect(err).not.toContain("Saved lockfile"); expect(err).not.toContain("not found"); - if (!err.includes("mimalloc: warning")) { - expect(err).not.toContain("error:"); - } + expect(err).not.toContain("error:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", expect.stringContaining("Checked 19 installs across 23 packages (no changes)"), @@ -505,18 +502,18 @@ test("it should install with missing bun.lockb, node_modules, and/or cache", asy env, })); - err = await new Response(stderr).text(); - out = await new Response(stdout).text(); + expect(await exited).toBe(0); + + [err, out] = await Promise.all([new Response(stderr).text(), new Response(stdout).text()]); + expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); - if (!err.includes("mimalloc: warning")) { - expect(err).not.toContain("error:"); - } + expect(err).not.toContain("error:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ "", expect.stringContaining("Checked 19 installs across 23 packages (no changes)"), ]); -}); +}, 30_000); test("it should re-populate .bin folder if package is reinstalled", async () => { await writeFile( @@ -542,9 +539,7 @@ test("it should re-populate .bin folder if package is reinstalled", async () => var out = await new Response(stdout).text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); - if (!err.includes("mimalloc: warning")) { - expect(err).not.toContain("error:"); - } + expect(err).not.toContain("error:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ " + what-bin@1.5.0", "", @@ -572,9 +567,7 @@ test("it should re-populate .bin folder if package is reinstalled", async () => out = await new Response(stdout).text(); expect(err).not.toContain("Saved lockfile"); expect(err).not.toContain("not found"); - if (!err.includes("mimalloc: warning")) { - expect(err).not.toContain("error:"); - } + expect(err).not.toContain("error:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ " + what-bin@1.5.0", "", @@ -622,9 +615,7 @@ test("missing package on reinstall, some with binaries", async () => { var out = await new Response(stdout).text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); - if (!err.includes("mimalloc: warning")) { - expect(err).not.toContain("error:"); - } + expect(err).not.toContain("error:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ " + dep-loop-entry@1.0.0", " + dep-with-tags@3.0.0", @@ -672,9 +663,7 @@ test("missing package on reinstall, some with binaries", async () => { out = await new Response(stdout).text(); expect(err).not.toContain("Saved lockfile"); expect(err).not.toContain("not found"); - if (!err.includes("mimalloc: warning")) { - expect(err).not.toContain("error:"); - } + expect(err).not.toContain("error:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ " + dep-loop-entry@1.0.0", " + left-pad@1.0.0", diff --git a/test/harness.ts b/test/harness.ts index f6f3fb375be89..4f93af0b81f0f 100644 --- a/test/harness.ts +++ b/test/harness.ts @@ -14,6 +14,10 @@ export function bunExe() { return process.execPath; } +export function withoutMimalloc(input: string) { + return input.replaceAll(/^mimalloc warning:.*$/gm, ""); +} + export function nodeExe(): string | null { return which("node") || null; } @@ -155,3 +159,24 @@ export function bunRunAsScript(dir: string, script: string, env?: Record & Pick) { + const origResponseText = Response.prototype.text; + beforeAll(() => { + // @ts-expect-error + Response.prototype.text = async function () { + return withoutMimalloc(await origResponseText.call(this)); + }; + }); + + afterAll(() => { + // @ts-expect-error + Response.prototype.text = origResponseText; + }); +} diff --git a/test/js/bun/util/__snapshots__/inspect-error.test.js.snap b/test/js/bun/util/__snapshots__/inspect-error.test.js.snap new file mode 100644 index 0000000000000..b97eba5839e8f --- /dev/null +++ b/test/js/bun/util/__snapshots__/inspect-error.test.js.snap @@ -0,0 +1,46 @@ +// Bun Snapshot v1, https://goo.gl/fbAQLP + +exports[`error.cause 1`] = ` +"1 | import { test, expect } from "bun:test"; +2 | +3 | test("error.cause", () => { +4 | const err = new Error("error 1"); +5 | const err2 = new Error("error 2", { cause: err }); + ^ +error: error 2 + at [dir]/inspect-error.test.js:5:15 + +1 | import { test, expect } from "bun:test"; +2 | +3 | test("error.cause", () => { +4 | const err = new Error("error 1"); + ^ +error: error 1 + at [dir]/inspect-error.test.js:4:14 +" +`; + +exports[`Error 1`] = ` +" 5 | const err2 = new Error("error 2", { cause: err }); + 6 | expect(Bun.inspect(err2).replaceAll(import.meta.dir, "[dir]")).toMatchSnapshot(); + 7 | }); + 8 | + 9 | test("Error", () => { +10 | const err = new Error("my message"); + ^ +error: my message + at [dir]/inspect-error.test.js:10:14 +" +`; + +exports[`BuildMessage 1`] = ` +"const duplicateConstDecl = 456; + ^ +error: "duplicateConstDecl" has already been declared + at [dir]/inspect-error-fixture-bad.js:2:7 38 + +const duplicateConstDecl = 123; + ^ +note: "duplicateConstDecl" was originally declared here + at [dir]/inspect-error-fixture-bad.js:1:7 6" +`; diff --git a/test/js/bun/util/inspect-error-fixture-bad.js b/test/js/bun/util/inspect-error-fixture-bad.js new file mode 100644 index 0000000000000..9b02d71626e4d --- /dev/null +++ b/test/js/bun/util/inspect-error-fixture-bad.js @@ -0,0 +1,2 @@ +const duplicateConstDecl = 123; +const duplicateConstDecl = 456; diff --git a/test/js/bun/util/inspect-error.test.js b/test/js/bun/util/inspect-error.test.js new file mode 100644 index 0000000000000..4a67cd14a6275 --- /dev/null +++ b/test/js/bun/util/inspect-error.test.js @@ -0,0 +1,21 @@ +import { test, expect } from "bun:test"; + +test("error.cause", () => { + const err = new Error("error 1"); + const err2 = new Error("error 2", { cause: err }); + expect(Bun.inspect(err2).replaceAll(import.meta.dir, "[dir]")).toMatchSnapshot(); +}); + +test("Error", () => { + const err = new Error("my message"); + expect(Bun.inspect(err).replaceAll(import.meta.dir, "[dir]")).toMatchSnapshot(); +}); + +test("BuildMessage", async () => { + try { + await import("./inspect-error-fixture-bad.js"); + expect.unreachable(); + } catch (e) { + expect(Bun.inspect(e).replaceAll(import.meta.dir, "[dir]")).toMatchSnapshot(); + } +}); diff --git a/test/js/node/fs/cp.test.ts b/test/js/node/fs/cp.test.ts index 41f5dd1e7b463..2baeaf8f56acb 100644 --- a/test/js/node/fs/cp.test.ts +++ b/test/js/node/fs/cp.test.ts @@ -291,5 +291,5 @@ test("cp with missing callback throws", () => { expect(() => { // @ts-expect-error fs.cp("a", "b" as any); - }).toThrow(/callback/); + }).toThrow(/Callback/); }); diff --git a/test/regression/issue/03830.test.ts b/test/regression/issue/03830.test.ts index caf2d9490c0e4..24be68313d8e2 100644 --- a/test/regression/issue/03830.test.ts +++ b/test/regression/issue/03830.test.ts @@ -2,7 +2,7 @@ import { it, expect } from "bun:test"; import { bunEnv, bunExe } from "harness"; -import { mkdirSync, rmSync, writeFileSync, readFileSync, mkdtempSync } from "fs"; +import { mkdirSync, rmSync, writeFileSync, readFileSync, mkdtempSync, realpathSync } from "fs"; import { tmpdir } from "os"; import { join } from "path"; @@ -10,7 +10,7 @@ it("macros should not lead to seg faults under any given input", async () => { // this test code follows the same structure as and // is based on the code for testing issue 4893 - const testDir = mkdtempSync(join(tmpdir(), "issue3830-")); + let testDir = mkdtempSync(join(tmpdir(), "issue3830-")); // Clean up from prior runs if necessary rmSync(testDir, { recursive: true, force: true }); @@ -19,6 +19,7 @@ it("macros should not lead to seg faults under any given input", async () => { mkdirSync(testDir, { recursive: true }); writeFileSync(join(testDir, "macro.ts"), "export function fn(str) { return str; }"); writeFileSync(join(testDir, "index.ts"), "import { fn } from './macro' assert { type: 'macro' };\nfn(`©${''}`);"); + testDir = realpathSync(testDir); const { stderr, exitCode } = Bun.spawnSync({ cmd: [bunExe(), "build", "--minify", join(testDir, "index.ts")], @@ -26,7 +27,6 @@ it("macros should not lead to seg faults under any given input", async () => { stderr: "pipe", }); - expect(stderr.toString().trim()).toStartWith('error: "Cannot convert argument type to JS'); - expect(exitCode).not.toBe(0); + expect(stderr.toString().trim().replaceAll(testDir, "[dir]")).toMatchSnapshot(); expect(exitCode).toBe(1); });