Compare commits

...

8 Commits

Author SHA1 Message Date
jarred-sumner-bot
b9b3fed4bb bun run prettier 2025-07-15 01:34:14 +00:00
jarred-sumner-bot
4acc7ed1ba bun run zig-format 2025-07-15 01:32:49 +00:00
Claude Bot
b83f5e5b81 Fix dependency resolution failure location to point to specific dependency
The error location now points to the specific dependency name in the lockfile
rather than the containing package/workspace. This makes it much easier for
users to locate the problematic dependency.

- Search through the AST to find the exact dependency location
- Add helper functions findDependencyInAST and findDependencyInWorkspace
- Update dependencyResolutionFailure to use AST-based location finding
- Add regression test to verify the fix works correctly

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-15 01:29:07 +00:00
jarred-sumner-bot
44a7e6279e Make --console-depth=0 enable infinite depth instead of zero depth (#21038)
Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
2025-07-14 07:45:20 -07:00
Jarred Sumner
7bb9a94d68 Implement test.coveragePathIgnorePatterns (#21013)
Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Jarred-Sumner <709451+Jarred-Sumner@users.noreply.github.com>
2025-07-14 05:08:32 -07:00
jarred-sumner-bot
3bba4e1446 Add --console-depth CLI flag and console.depth bunfig option (#21016)
Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: jarred-sumner-bot <220441119+jarred-sumner-bot@users.noreply.github.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
2025-07-14 04:58:41 -07:00
Jarred Sumner
8cf4df296d Delete the merge conflict factory
There are many situations where using `catch unreachable` is a reasonable or sometimes necessary decision. This rule causes many, many merge conflicts.
2025-07-14 03:53:50 -07:00
Michael H
3ba9b5710e fix Bun.build with { sourcemap: true } (#21029) 2025-07-14 03:52:26 -07:00
19 changed files with 1270 additions and 31 deletions

View File

@@ -2,6 +2,25 @@
**Note** — Bun provides a browser- and Node.js-compatible [console](https://developer.mozilla.org/en-US/docs/Web/API/console) global. This page only documents Bun-native APIs.
{% /callout %}
## Object inspection depth
Bun allows you to configure how deeply nested objects are displayed in `console.log()` output:
- **CLI flag**: Use `--console-depth <number>` to set the depth for a single run
- **Configuration**: Set `console.depth` in your `bunfig.toml` for persistent configuration
- **Default**: Objects are inspected to a depth of `2` levels
```js
const nested = { a: { b: { c: { d: "deep" } } } };
console.log(nested);
// Default (depth 2): { a: { b: [Object] } }
// With depth 4: { a: { b: { c: { d: 'deep' } } } }
```
The CLI flag takes precedence over the configuration file setting.
## Reading from stdin
In Bun, the `console` object can be used as an `AsyncIterable` to sequentially read lines from `process.stdin`.
```ts

View File

@@ -185,6 +185,23 @@ This is TypeScript!
For convenience, all code is treated as TypeScript with JSX support when using `bun run -`.
## `bun run --console-depth`
Control the depth of object inspection in console output with the `--console-depth` flag.
```bash
$ bun --console-depth 5 run index.tsx
```
This sets how deeply nested objects are displayed in `console.log()` output. The default depth is `2`. Higher values show more nested properties but may produce verbose output for complex objects.
```js
const nested = { a: { b: { c: { d: "deep" } } } };
console.log(nested);
// With --console-depth 2 (default): { a: { b: [Object] } }
// With --console-depth 4: { a: { b: { c: { d: 'deep' } } } }
```
## `bun run --smol`
In memory-constrained environments, use the `--smol` flag to reduce memory usage at a cost to performance.

View File

@@ -108,6 +108,21 @@ The `telemetry` field permit to enable/disable the analytics records. Bun record
telemetry = false
```
### `console`
Configure console output behavior.
#### `console.depth`
Set the default depth for `console.log()` object inspection. Default `2`.
```toml
[console]
depth = 3
```
This controls how deeply nested objects are displayed in console output. Higher values show more nested properties but may produce verbose output for complex objects. This setting can be overridden by the `--console-depth` CLI flag.
## Test runner
The test runner is configured under the `[test]` section of your bunfig.toml.

View File

@@ -18,8 +18,13 @@ const Environment = bun.Environment;
const default_allocator = bun.default_allocator;
const JestPrettyFormat = @import("./test/pretty_format.zig").JestPrettyFormat;
const JSPromise = JSC.JSPromise;
const CLI = @import("../cli.zig").Command;
const EventType = JSC.EventType;
/// Default depth for console.log object inspection
/// Only --console-depth CLI flag and console.depth bunfig option should modify this
const DEFAULT_CONSOLE_LOG_DEPTH: u16 = 2;
const Counter = std.AutoHashMapUnmanaged(u64, u32);
const BufferedWriter = std.io.BufferedWriter(4096, Output.WriterType);
@@ -168,11 +173,16 @@ fn messageWithTypeAndLevel_(
const Writer = @TypeOf(writer);
var print_length = len;
// Get console depth from CLI options or bunfig, fallback to default
const cli_context = CLI.get();
const console_depth = cli_context.runtime_options.console_depth orelse DEFAULT_CONSOLE_LOG_DEPTH;
var print_options: FormatOptions = .{
.enable_colors = enable_colors,
.add_newline = true,
.flush = true,
.default_indent = console.default_indent,
.max_depth = console_depth,
.error_display_level = switch (level) {
.Error => .full,
else => .normal,
@@ -912,6 +922,7 @@ pub fn format2(
.globalThis = global,
.ordered_properties = options.ordered_properties,
.quote_strings = options.quote_strings,
.max_depth = options.max_depth,
.single_line = options.single_line,
.indent = options.default_indent,
.stack_check = bun.StackCheck.init(),
@@ -3632,6 +3643,10 @@ pub fn timeLog(
.globalThis = global,
.ordered_properties = false,
.quote_strings = false,
.max_depth = blk: {
const cli_context = CLI.get();
break :blk cli_context.runtime_options.console_depth orelse DEFAULT_CONSOLE_LOG_DEPTH;
},
.stack_check = bun.StackCheck.init(),
.can_throw_stack_overflow = true,
};

View File

@@ -184,7 +184,7 @@ pub const JSBundler = struct {
}
if (try config.getTruthy(globalThis, "sourcemap")) |source_map_js| {
if (config.isBoolean()) {
if (source_map_js.isBoolean()) {
if (source_map_js == .true) {
this.source_map = if (has_out_dir)
.linked

View File

@@ -341,6 +341,34 @@ pub const Bunfig = struct {
try this.expect(expr, .e_boolean);
this.ctx.test_options.coverage.skip_test_files = expr.data.e_boolean.value;
}
if (test_.get("coveragePathIgnorePatterns")) |expr| brk: {
switch (expr.data) {
.e_string => |str| {
const pattern = try str.string(allocator);
const patterns = try allocator.alloc(string, 1);
patterns[0] = pattern;
this.ctx.test_options.coverage.ignore_patterns = patterns;
},
.e_array => |arr| {
if (arr.items.len == 0) break :brk;
const patterns = try allocator.alloc(string, arr.items.len);
for (arr.items.slice(), 0..) |item, i| {
if (item.data != .e_string) {
try this.addError(item.loc, "coveragePathIgnorePatterns array must contain only strings");
return;
}
patterns[i] = try item.data.e_string.string(allocator);
}
this.ctx.test_options.coverage.ignore_patterns = patterns;
},
else => {
try this.addError(expr.loc, "coveragePathIgnorePatterns must be a string or array of strings");
return;
},
}
}
}
}
@@ -629,6 +657,18 @@ pub const Bunfig = struct {
}
}
}
if (json.get("console")) |console_expr| {
if (console_expr.get("depth")) |depth| {
if (depth.data == .e_number) {
const depth_value = @as(u16, @intFromFloat(depth.data.e_number.value));
// Treat depth=0 as maxInt(u16) for infinite depth
this.ctx.runtime_options.console_depth = if (depth_value == 0) std.math.maxInt(u16) else depth_value;
} else {
try this.addError(depth.loc, "Expected number");
}
}
}
}
if (json.getObject("serve")) |serve_obj2| {

View File

@@ -388,6 +388,7 @@ pub const Command = struct {
/// compatibility.
expose_gc: bool = false,
preserve_symlinks_main: bool = false,
console_depth: ?u16 = null,
};
var global_cli_ctx: Context = undefined;

View File

@@ -108,6 +108,7 @@ pub const runtime_params_ = [_]ParamType{
clap.parseParam("--redis-preconnect Preconnect to $REDIS_URL at startup") catch unreachable,
clap.parseParam("--no-addons Throw an error if process.dlopen is called, and disable export condition \"node-addons\"") catch unreachable,
clap.parseParam("--unhandled-rejections <STR> One of \"strict\", \"throw\", \"warn\", \"none\", or \"warn-with-error-code\"") catch unreachable,
clap.parseParam("--console-depth <NUMBER> Set the default depth for console.log object inspection (default: 2)") catch unreachable,
};
pub const auto_or_run_params = [_]ParamType{
@@ -668,6 +669,15 @@ pub fn parse(allocator: std.mem.Allocator, ctx: Command.Context, comptime cmd: C
ctx.runtime_options.preconnect = args.options("--fetch-preconnect");
ctx.runtime_options.expose_gc = args.flag("--expose-gc");
if (args.option("--console-depth")) |depth_str| {
const depth = std.fmt.parseInt(u16, depth_str, 10) catch {
Output.errGeneric("Invalid value for --console-depth: \"{s}\". Must be a positive integer\n", .{depth_str});
Global.exit(1);
};
// Treat depth=0 as maxInt(u16) for infinite depth
ctx.runtime_options.console_depth = if (depth == 0) std.math.maxInt(u16) else depth;
}
if (args.option("--dns-result-order")) |order| {
ctx.runtime_options.dns_result_order = order;
}

View File

@@ -1015,7 +1015,24 @@ pub const CommandLineReporter = struct {
var len = "All files".len;
for (byte_ranges) |*entry| {
const utf8 = entry.source_url.slice();
len = @max(bun.path.relative(relative_dir, utf8).len, len);
const relative_path = bun.path.relative(relative_dir, utf8);
// Check if this file should be ignored based on coveragePathIgnorePatterns
if (opts.ignore_patterns.len > 0) {
var should_ignore = false;
for (opts.ignore_patterns) |pattern| {
if (bun.glob.match(bun.default_allocator, pattern, relative_path).matches()) {
should_ignore = true;
break;
}
}
if (should_ignore) {
continue;
}
}
len = @max(relative_path.len, len);
}
break :brk len;
@@ -1117,6 +1134,24 @@ pub const CommandLineReporter = struct {
// --- LCOV ---
for (byte_ranges) |*entry| {
// Check if this file should be ignored based on coveragePathIgnorePatterns
if (opts.ignore_patterns.len > 0) {
const utf8 = entry.source_url.slice();
const relative_path = bun.path.relative(relative_dir, utf8);
var should_ignore = false;
for (opts.ignore_patterns) |pattern| {
if (bun.glob.match(bun.default_allocator, pattern, relative_path).matches()) {
should_ignore = true;
break;
}
}
if (should_ignore) {
continue;
}
}
var report = CodeCoverageReport.generate(vm.global, bun.default_allocator, entry, opts.ignore_sourcemap) orelse continue;
defer report.deinit(bun.default_allocator);
@@ -1145,15 +1180,27 @@ pub const CommandLineReporter = struct {
if (comptime reporters.text) {
{
avg.functions /= avg_count;
avg.lines /= avg_count;
avg.stmts /= avg_count;
if (avg_count == 0) {
avg.functions = 0;
avg.lines = 0;
avg.stmts = 0;
} else {
avg.functions /= avg_count;
avg.lines /= avg_count;
avg.stmts /= avg_count;
}
const failed = if (avg_count > 0) base_fraction else bun.sourcemap.coverage.Fraction{
.functions = 0,
.lines = 0,
.stmts = 0,
};
try CodeCoverageReport.Text.writeFormatWithValues(
"All files",
max_filepath_length,
avg,
base_fraction,
failed,
failing,
console,
false,
@@ -1242,6 +1289,7 @@ pub const TestCommand = struct {
ignore_sourcemap: bool = false,
enabled: bool = false,
fail_on_low_coverage: bool = false,
ignore_patterns: []const string = &.{},
};
pub const Reporter = enum {
text,

View File

@@ -1923,7 +1923,7 @@ pub fn parseIntoBinaryLockfile(
if (dep.behavior.optional) {
continue;
}
try dependencyResolutionFailure(dep, null, allocator, lockfile.buffers.string_bytes.items, source, log, root_pkg_exr.loc);
try dependencyResolutionFailure(dep, null, allocator, lockfile.buffers.string_bytes.items, source, log, root_pkg_exr.loc, root);
return error.InvalidPackageInfo;
};
@@ -1954,7 +1954,7 @@ pub fn parseIntoBinaryLockfile(
if (dep.behavior.optional) {
continue;
}
try dependencyResolutionFailure(dep, workspace_name, allocator, lockfile.buffers.string_bytes.items, source, log, root_pkg_exr.loc);
try dependencyResolutionFailure(dep, workspace_name, allocator, lockfile.buffers.string_bytes.items, source, log, root_pkg_exr.loc, root);
return error.InvalidPackageInfo;
};
@@ -1988,7 +1988,7 @@ pub fn parseIntoBinaryLockfile(
if (dep.behavior.optional) {
continue :deps;
}
try dependencyResolutionFailure(dep, pkg_path, allocator, lockfile.buffers.string_bytes.items, source, log, key.loc);
try dependencyResolutionFailure(dep, pkg_path, allocator, lockfile.buffers.string_bytes.items, source, log, key.loc, root);
return error.InvalidPackageInfo;
},
};
@@ -2023,7 +2023,46 @@ fn mapDepToPkg(dep: *Dependency, dep_id: DependencyID, pkg_id: PackageID, lockfi
}
}
fn dependencyResolutionFailure(dep: *const Dependency, pkg_path: ?string, allocator: std.mem.Allocator, buf: string, source: *const logger.Source, log: *logger.Log, loc: logger.Loc) OOM!void {
fn findDependencyInAST(ast_root: Expr, dep_name: string, pkg_path: ?string) logger.Loc {
// For root dependencies, look in workspaces[""]
if (pkg_path == null) {
if (ast_root.get("workspaces")) |workspaces| {
if (workspaces.get("")) |root_workspace| {
return findDependencyInWorkspace(root_workspace, dep_name);
}
}
} else {
// For package dependencies, look in packages[pkg_path]
if (ast_root.get("packages")) |packages| {
if (packages.get(pkg_path.?)) |package| {
return findDependencyInWorkspace(package, dep_name);
}
}
}
return logger.Loc.Empty;
}
fn findDependencyInWorkspace(workspace: Expr, dep_name: string) logger.Loc {
// Check all dependency groups
const dep_groups = [_][]const u8{ "dependencies", "devDependencies", "optionalDependencies", "peerDependencies" };
for (dep_groups) |group_name| {
if (workspace.get(group_name)) |deps| {
if (deps.isObject()) {
for (deps.data.e_object.properties.slice()) |prop| {
const key = prop.key.?;
if (key.asString(bun.default_allocator)) |key_name| {
if (strings.eqlLong(key_name, dep_name, true)) {
return key.loc;
}
}
}
}
}
}
return logger.Loc.Empty;
}
fn dependencyResolutionFailure(dep: *const Dependency, pkg_path: ?string, allocator: std.mem.Allocator, buf: string, source: *const logger.Source, log: *logger.Log, fallback_loc: logger.Loc, ast_root: Expr) OOM!void {
const behavior_str = if (dep.behavior.dev)
"dev"
else if (dep.behavior.optional)
@@ -2035,16 +2074,22 @@ fn dependencyResolutionFailure(dep: *const Dependency, pkg_path: ?string, alloca
else
"prod";
const dep_name = dep.name.slice(buf);
// Search for the dependency location in the AST
const loc = findDependencyInAST(ast_root, dep_name, pkg_path);
const final_loc = if (loc.start != -1) loc else fallback_loc;
if (pkg_path) |path| {
try log.addErrorFmt(source, loc, allocator, "Failed to resolve {s} dependency '{s}' for package '{s}'", .{
try log.addErrorFmt(source, final_loc, allocator, "Failed to resolve {s} dependency '{s}' for package '{s}'", .{
behavior_str,
dep.name.slice(buf),
dep_name,
path,
});
} else {
try log.addErrorFmt(source, loc, allocator, "Failed to resolve root {s} dependency '{s}'", .{
try log.addErrorFmt(source, final_loc, allocator, "Failed to resolve root {s} dependency '{s}'", .{
behavior_str,
dep.name.slice(buf),
dep_name,
});
}
}

View File

@@ -792,3 +792,66 @@ identity(mod23);
expect(text).not.toContain(" global.");
expect(text).toContain(" globalThis.");
});
describe("sourcemap boolean values", () => {
test("sourcemap: true should work (boolean)", async () => {
const dir = tempDirWithFiles("sourcemap-true-boolean", {
"index.js": `console.log("hello");`,
});
const build = await Bun.build({
entrypoints: [join(dir, "index.js")],
sourcemap: true,
});
expect(build.success).toBe(true);
expect(build.outputs).toHaveLength(1);
expect(build.outputs[0].kind).toBe("entry-point");
const output = await build.outputs[0].text();
expect(output).toContain("//# sourceMappingURL=data:application/json;base64,");
});
test("sourcemap: false should work (boolean)", async () => {
const dir = tempDirWithFiles("sourcemap-false-boolean", {
"index.js": `console.log("hello");`,
});
const build = await Bun.build({
entrypoints: [join(dir, "index.js")],
sourcemap: false,
});
expect(build.success).toBe(true);
expect(build.outputs).toHaveLength(1);
expect(build.outputs[0].kind).toBe("entry-point");
const output = await build.outputs[0].text();
expect(output).not.toContain("//# sourceMappingURL=");
});
test("sourcemap: true with outdir should create linked sourcemap", async () => {
const dir = tempDirWithFiles("sourcemap-true-outdir", {
"index.js": `console.log("hello");`,
});
const build = await Bun.build({
entrypoints: [join(dir, "index.js")],
outdir: join(dir, "out"),
sourcemap: true,
});
expect(build.success).toBe(true);
expect(build.outputs).toHaveLength(2);
const jsOutput = build.outputs.find(o => o.kind === "entry-point");
const mapOutput = build.outputs.find(o => o.kind === "sourcemap");
expect(jsOutput).toBeTruthy();
expect(mapOutput).toBeTruthy();
expect(jsOutput!.sourcemap).toBe(mapOutput);
const jsText = await jsOutput!.text();
expect(jsText).toContain("//# sourceMappingURL=index.js.map");
});
});

View File

@@ -0,0 +1,372 @@
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe, tempDirWithFiles } from "harness";
describe("console depth", () => {
const deepObject = {
level1: {
level2: {
level3: {
level4: {
level5: {
level6: {
level7: {
level8: {
level9: {
level10: "deep value",
},
},
},
},
},
},
},
},
},
};
const testScript = `console.log(${JSON.stringify(deepObject)});`;
function normalizeOutput(output: string): string {
// Normalize line endings and trim whitespace
return output.replace(/\r\n/g, "\n").replace(/\r/g, "\n").trim();
}
test("default console depth should be 2", async () => {
const dir = tempDirWithFiles("console-depth-default", {
"test.js": testScript,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "test.js"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
expect(normalizeOutput(stdout)).toMatchInlineSnapshot(`
"{
level1: {
level2: {
level3: [Object ...],
},
},
}"
`);
});
test("--console-depth flag sets custom depth", async () => {
const dir = tempDirWithFiles("console-depth-cli", {
"test.js": testScript,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "--console-depth", "3", "test.js"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
expect(normalizeOutput(stdout)).toMatchInlineSnapshot(`
"{
level1: {
level2: {
level3: {
level4: [Object ...],
},
},
},
}"
`);
});
test("--console-depth with higher value shows deeper nesting", async () => {
const dir = tempDirWithFiles("console-depth-high", {
"test.js": testScript,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "--console-depth", "10", "test.js"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
expect(normalizeOutput(stdout)).toMatchInlineSnapshot(`
"{
level1: {
level2: {
level3: {
level4: {
level5: {
level6: {
level7: {
level8: {
level9: {
level10: \"deep value\",
},
},
},
},
},
},
},
},
},
}"
`);
});
test("bunfig.toml console.depth configuration", async () => {
const dir = tempDirWithFiles("console-depth-bunfig", {
"test.js": testScript,
"bunfig.toml": `[console]\ndepth = 4`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "test.js"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
expect(normalizeOutput(stdout)).toMatchInlineSnapshot(`
"{
level1: {
level2: {
level3: {
level4: {
level5: [Object ...],
},
},
},
},
}"
`);
});
test("CLI flag overrides bunfig.toml", async () => {
const dir = tempDirWithFiles("console-depth-override", {
"test.js": testScript,
"bunfig.toml": `[console]\ndepth = 6`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "--console-depth", "2", "test.js"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
expect(normalizeOutput(stdout)).toMatchInlineSnapshot(`
"{
level1: {
level2: {
level3: [Object ...],
},
},
}"
`);
});
test("invalid --console-depth value shows error", async () => {
const dir = tempDirWithFiles("console-depth-invalid", {
"test.js": testScript,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "--console-depth", "invalid", "test.js"],
env: bunEnv,
cwd: dir,
stderr: "pipe",
stdout: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(exitCode).toBe(1);
const allOutput = normalizeOutput(stdout + stderr);
expect(allOutput).toMatchInlineSnapshot(
`"error: Invalid value for --console-depth: \"invalid\". Must be a positive integer"`,
);
});
test("edge case: depth 0 should show infinite depth", async () => {
const dir = tempDirWithFiles("console-depth-zero", {
"test.js": testScript,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "--console-depth", "0", "test.js"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
expect(normalizeOutput(stdout)).toMatchInlineSnapshot(`
"{
level1: {
level2: {
level3: {
level4: {
level5: {
level6: {
level7: {
level8: {
level9: {
level10: \"deep value\",
},
},
},
},
},
},
},
},
},
}"
`);
});
test("bunfig.toml depth=0 should show infinite depth", async () => {
const dir = tempDirWithFiles("console-depth-bunfig-zero", {
"test.js": testScript,
"bunfig.toml": `[console]\ndepth = 0`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "test.js"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
expect(normalizeOutput(stdout)).toMatchInlineSnapshot(`
"{
level1: {
level2: {
level3: {
level4: {
level5: {
level6: {
level7: {
level8: {
level9: {
level10: \"deep value\",
},
},
},
},
},
},
},
},
},
}"
`);
});
test("console depth affects console.log, console.error, and console.warn", async () => {
const testScriptMultiple = `
const obj = ${JSON.stringify(deepObject)};
console.log("LOG:", obj);
console.error("ERROR:", obj);
console.warn("WARN:", obj);
`;
const dir = tempDirWithFiles("console-depth-multiple", {
"test.js": testScriptMultiple,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "--console-depth", "2", "test.js"],
env: bunEnv,
cwd: dir,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(exitCode).toBe(0);
expect(normalizeOutput(stdout + stderr)).toMatchInlineSnapshot(`
"LOG: {
level1: {
level2: {
level3: [Object ...],
},
},
}
ERROR: {
level1: {
level2: {
level3: [Object ...],
},
},
}
WARN: {
level1: {
level2: {
level3: [Object ...],
},
},
}"
`);
});
});

View File

@@ -1,6 +1,6 @@
// Bun Snapshot v1, https://bun.sh/docs/test/snapshots
exports[`lcov coverage reporter 1`] = `
exports[`lcov coverage reporter: lcov-coverage-reporter-output 1`] = `
"TN:
SF:demo1.ts
FNF:1
@@ -24,6 +24,5 @@ DA:11,1
DA:14,9
LF:15
LH:5
end_of_record
"
end_of_record"
`;

View File

@@ -1,5 +1,5 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDirWithFiles } from "harness";
import { bunEnv, bunExe, normalizeBunSnapshot, tempDirWithFiles } from "harness";
import { readFileSync } from "node:fs";
import path from "path";
@@ -52,7 +52,9 @@ export class Y {
});
expect(result.exitCode).toBe(0);
expect(result.signalCode).toBeUndefined();
expect(readFileSync(path.join(dir, "coverage", "lcov.info"), "utf-8")).toMatchSnapshot();
expect(normalizeBunSnapshot(readFileSync(path.join(dir, "coverage", "lcov.info"), "utf-8"), dir)).toMatchSnapshot(
"lcov-coverage-reporter-output",
);
});
test("coverage excludes node_modules directory", () => {
@@ -77,3 +79,513 @@ test("coverage excludes node_modules directory", () => {
expect(result.exitCode).toBe(0);
expect(result.signalCode).toBeUndefined();
});
test("coveragePathIgnorePatterns - single pattern string", () => {
const dir = tempDirWithFiles("cov", {
"bunfig.toml": `
[test]
coveragePathIgnorePatterns = "ignore-me.ts"
coverageSkipTestFiles = false
`,
"include-me.ts": `
export function includeMe() {
return "included";
}
`,
"ignore-me.ts": `
export function ignoreMe() {
return "ignored";
}
`,
"test.test.ts": `
import { test, expect } from "bun:test";
import { includeMe } from "./include-me";
import { ignoreMe } from "./ignore-me";
test("should call both functions", () => {
expect(includeMe()).toBe("included");
expect(ignoreMe()).toBe("ignored");
});
`,
});
const result = Bun.spawnSync([bunExe(), "test", "--coverage"], {
cwd: dir,
env: {
...bunEnv,
},
stdio: [null, null, "pipe"],
});
let stderr = result.stderr.toString("utf-8");
// Normalize output for cross-platform consistency
stderr = normalizeBunSnapshot(stderr, dir);
expect(stderr).toMatchInlineSnapshot(`
"test.test.ts:
(pass) should call both functions
---------------|---------|---------|-------------------
File | % Funcs | % Lines | Uncovered Line #s
---------------|---------|---------|-------------------
All files | 100.00 | 100.00 |
include-me.ts | 100.00 | 100.00 |
test.test.ts | 100.00 | 100.00 |
---------------|---------|---------|-------------------
1 pass
0 fail
2 expect() calls
Ran 1 test across 1 file."
`);
expect(result.exitCode).toBe(0);
});
test("coveragePathIgnorePatterns - partial coverage without nan", () => {
const dir = tempDirWithFiles("cov", {
"bunfig.toml": `
[test]
coveragePathIgnorePatterns = "ignore-me.ts"
coverageSkipTestFiles = false
`,
"include-me.ts": `
export function includeMe() {
return "included";
}
export function neverCalled() {
return "never called";
}
`,
"ignore-me.ts": `
export function ignoreMe() {
return "ignored";
}
`,
"test.test.ts": `
import { test, expect } from "bun:test";
import { includeMe } from "./include-me";
import { ignoreMe } from "./ignore-me";
test("should call only some functions", () => {
expect(includeMe()).toBe("included");
expect(ignoreMe()).toBe("ignored");
// Note: neverCalled() is not called, so coverage should be partial
});
`,
});
const result = Bun.spawnSync([bunExe(), "test", "--coverage"], {
cwd: dir,
env: {
...bunEnv,
},
stdio: [null, null, "pipe"],
});
let stderr = result.stderr.toString("utf-8");
// Normalize output for cross-platform consistency
stderr = normalizeBunSnapshot(stderr, dir);
expect(stderr).toMatchInlineSnapshot(`
"test.test.ts:
(pass) should call only some functions
---------------|---------|---------|-------------------
File | % Funcs | % Lines | Uncovered Line #s
---------------|---------|---------|-------------------
All files | 75.00 | 83.33 |
include-me.ts | 50.00 | 66.67 |
test.test.ts | 100.00 | 100.00 |
---------------|---------|---------|-------------------
1 pass
0 fail
2 expect() calls
Ran 1 test across 1 file."
`);
expect(result.exitCode).toBe(0);
});
test("coveragePathIgnorePatterns - array of patterns", () => {
const dir = tempDirWithFiles("cov", {
"bunfig.toml": `
[test]
coveragePathIgnorePatterns = ["utils/**", "*.config.ts"]
coverageSkipTestFiles = false
`,
"src/main.ts": `
export function main() {
return "main";
}
`,
"utils/helper.ts": `
export function helper() {
return "helper";
}
`,
"build.config.ts": `
export const config = { build: true };
`,
"test.test.ts": `
import { test, expect } from "bun:test";
import { main } from "./src/main";
import { helper } from "./utils/helper";
import { config } from "./build.config";
test("should call all functions", () => {
expect(main()).toBe("main");
expect(helper()).toBe("helper");
expect(config.build).toBe(true);
});
`,
});
const result = Bun.spawnSync([bunExe(), "test", "--coverage"], {
cwd: dir,
env: {
...bunEnv,
},
stdio: [null, null, "pipe"],
});
let stderr = result.stderr.toString("utf-8");
// Normalize output for cross-platform consistency
stderr = normalizeBunSnapshot(stderr, dir);
expect(stderr).toMatchInlineSnapshot(`
"test.test.ts:
(pass) should call all functions
--------------|---------|---------|-------------------
File | % Funcs | % Lines | Uncovered Line #s
--------------|---------|---------|-------------------
All files | 100.00 | 100.00 |
src/main.ts | 100.00 | 100.00 |
test.test.ts | 100.00 | 100.00 |
--------------|---------|---------|-------------------
1 pass
0 fail
3 expect() calls
Ran 1 test across 1 file."
`);
expect(result.exitCode).toBe(0);
});
test("coveragePathIgnorePatterns - glob patterns", () => {
const dir = tempDirWithFiles("cov", {
"bunfig.toml": `
[test]
coveragePathIgnorePatterns = ["**/*.spec.ts", "test-utils/**"]
coverageSkipTestFiles = false
`,
"src/feature.ts": `
export function feature() {
return "feature";
}
`,
"src/feature.spec.ts": `
export function featureSpec() {
return "spec";
}
`,
"test-utils/index.ts": `
export function testUtils() {
return "utils";
}
`,
"main.test.ts": `
import { test, expect } from "bun:test";
import { feature } from "./src/feature";
import { featureSpec } from "./src/feature.spec";
import { testUtils } from "./test-utils";
test("should call all functions", () => {
expect(feature()).toBe("feature");
expect(featureSpec()).toBe("spec");
expect(testUtils()).toBe("utils");
});
`,
});
const result = Bun.spawnSync([bunExe(), "test", "--coverage"], {
cwd: dir,
env: {
...bunEnv,
},
stdio: [null, null, "pipe"],
});
let stderr = result.stderr.toString("utf-8");
// Normalize output for cross-platform consistency
stderr = normalizeBunSnapshot(stderr, dir);
expect(stderr).toMatchInlineSnapshot(`
"main.test.ts:
(pass) should call all functions
src/feature.spec.ts:
----------------|---------|---------|-------------------
File | % Funcs | % Lines | Uncovered Line #s
----------------|---------|---------|-------------------
All files | 100.00 | 100.00 |
main.test.ts | 100.00 | 100.00 |
src/feature.ts | 100.00 | 100.00 |
----------------|---------|---------|-------------------
1 pass
0 fail
3 expect() calls
Ran 1 test across 2 files."
`);
expect(result.exitCode).toBe(0);
});
test("coveragePathIgnorePatterns - lcov reporter", () => {
const dir = tempDirWithFiles("cov", {
"bunfig.toml": `
[test]
coveragePathIgnorePatterns = "ignore-me.ts"
coverageSkipTestFiles = false
`,
"include-me.ts": `
export function includeMe() {
return "included";
}
`,
"ignore-me.ts": `
export function ignoreMe() {
return "ignored";
}
`,
"test.test.ts": `
import { test, expect } from "bun:test";
import { includeMe } from "./include-me";
import { ignoreMe } from "./ignore-me";
test("should call both functions", () => {
expect(includeMe()).toBe("included");
expect(ignoreMe()).toBe("ignored");
});
`,
});
const result = Bun.spawnSync([bunExe(), "test", "--coverage", "--coverage-reporter", "lcov"], {
cwd: dir,
env: {
...bunEnv,
},
stdio: [null, null, "pipe"],
});
let lcovContent = readFileSync(path.join(dir, "coverage", "lcov.info"), "utf-8");
// Normalize LCOV content for cross-platform consistency
lcovContent = normalizeBunSnapshot(lcovContent, dir);
expect(lcovContent).toMatchInlineSnapshot(`
"TN:
SF:include-me.ts
FNF:1
FNH:1
DA:2,11
DA:3,17
LF:5
LH:2
end_of_record
TN:
SF:test.test.ts
FNF:1
FNH:1
DA:2,60
DA:3,41
DA:4,39
DA:6,42
DA:7,39
DA:8,36
DA:9,2
LF:10
LH:7
end_of_record"
`);
expect(result.exitCode).toBe(0);
});
test("coveragePathIgnorePatterns - invalid config type", () => {
const dir = tempDirWithFiles("cov", {
"bunfig.toml": `
[test]
coveragePathIgnorePatterns = 123
coverageSkipTestFiles = false
`,
"test.test.ts": `
import { test, expect } from "bun:test";
test("should pass", () => {
expect(true).toBe(true);
});
`,
});
const result = Bun.spawnSync([bunExe(), "test", "--coverage"], {
cwd: dir,
env: {
...bunEnv,
},
stdio: [null, null, "pipe"],
});
let stderr = result.stderr.toString("utf-8");
// Normalize error output for cross-platform consistency
stderr = normalizeBunSnapshot(stderr, dir);
expect(stderr).toMatchInlineSnapshot(`
"3 | coveragePathIgnorePatterns = 123
^
error: coveragePathIgnorePatterns must be a string or array of strings
at <dir>/bunfig.toml:3:30
Invalid Bunfig: failed to load bunfig"
`);
expect(result.exitCode).toBe(1);
});
test("coveragePathIgnorePatterns - invalid array item", () => {
const dir = tempDirWithFiles("cov", {
"bunfig.toml": `
[test]
coveragePathIgnorePatterns = ["valid-pattern", 123]
coverageSkipTestFiles = false
`,
"test.test.ts": `
import { test, expect } from "bun:test";
test("should pass", () => {
expect(true).toBe(true);
});
`,
});
const result = Bun.spawnSync([bunExe(), "test", "--coverage"], {
cwd: dir,
env: {
...bunEnv,
},
stdio: [null, null, "pipe"],
});
let stderr = result.stderr.toString("utf-8");
// Normalize error output for cross-platform consistency
stderr = normalizeBunSnapshot(stderr, dir);
expect(stderr).toMatchInlineSnapshot(`
"3 | coveragePathIgnorePatterns = ["valid-pattern", 123]
^
error: coveragePathIgnorePatterns array must contain only strings
at <dir>/bunfig.toml:3:48
Invalid Bunfig: failed to load bunfig"
`);
expect(result.exitCode).toBe(1);
});
test("coveragePathIgnorePatterns - empty array", () => {
const dir = tempDirWithFiles("cov", {
"bunfig.toml": `
[test]
coveragePathIgnorePatterns = []
coverageSkipTestFiles = false
`,
"include-me.ts": `
export function includeMe() {
return "included";
}
`,
"test.test.ts": `
import { test, expect } from "bun:test";
import { includeMe } from "./include-me";
test("should call function", () => {
expect(includeMe()).toBe("included");
});
`,
});
const result = Bun.spawnSync([bunExe(), "test", "--coverage"], {
cwd: dir,
env: {
...bunEnv,
},
stdio: [null, null, "pipe"],
});
let stderr = result.stderr.toString("utf-8");
// Normalize output for cross-platform consistency
stderr = normalizeBunSnapshot(stderr, dir);
expect(stderr).toMatchInlineSnapshot(`
"test.test.ts:
(pass) should call function
---------------|---------|---------|-------------------
File | % Funcs | % Lines | Uncovered Line #s
---------------|---------|---------|-------------------
All files | 100.00 | 100.00 |
include-me.ts | 100.00 | 100.00 |
test.test.ts | 100.00 | 100.00 |
---------------|---------|---------|-------------------
1 pass
0 fail
1 expect() calls
Ran 1 test across 1 file."
`);
expect(result.exitCode).toBe(0);
});
test("coveragePathIgnorePatterns - ignore all files", () => {
const dir = tempDirWithFiles("cov", {
"bunfig.toml": `
[test]
coveragePathIgnorePatterns = "**"
coverageSkipTestFiles = false
`,
"include-me.ts": `
export function includeMe() {
return "included";
}
`,
"test.test.ts": `
import { test, expect } from "bun:test";
import { includeMe } from "./include-me";
test("should call function", () => {
expect(includeMe()).toBe("included");
});
`,
});
const result = Bun.spawnSync([bunExe(), "test", "--coverage"], {
cwd: dir,
env: {
...bunEnv,
},
stdio: [null, null, "pipe"],
});
let stderr = result.stderr.toString("utf-8");
// Normalize output for cross-platform consistency
stderr = normalizeBunSnapshot(stderr, dir);
expect(stderr).toMatchInlineSnapshot(`
"test.test.ts:
(pass) should call function
-----------|---------|---------|-------------------
File | % Funcs | % Lines | Uncovered Line #s
-----------|---------|---------|-------------------
All files | 0.00 | 0.00 |
-----------|---------|---------|-------------------
1 pass
0 fail
1 expect() calls
Ran 1 test across 1 file."
`);
expect(result.exitCode).toBe(0);
});

View File

@@ -1713,3 +1713,33 @@ export async function gunzipJsonRequest(req: Request) {
const body = JSON.parse(Buffer.from(inflated).toString("utf-8"));
return body;
}
export function normalizeBunSnapshot(snapshot: string, optionalDir?: string) {
if (optionalDir) {
snapshot = snapshot
.replaceAll(fs.realpathSync.native(optionalDir).replaceAll("\\", "/"), "<dir>")
.replaceAll(optionalDir, "<dir>");
}
// Remove timestamps from test result lines that start with (pass), (fail), (skip), or (todo)
snapshot = snapshot.replace(/^((?:pass|fail|skip|todo)\) .+) \[[\d.]+\s?m?s\]$/gm, "$1");
return (
snapshot
.replaceAll("\r\n", "\n")
.replaceAll("\\", "/")
.replaceAll(fs.realpathSync.native(process.cwd()).replaceAll("\\", "/"), "<cwd>")
.replaceAll(fs.realpathSync.native(os.tmpdir()).replaceAll("\\", "/"), "<tmp>")
.replaceAll(fs.realpathSync.native(os.homedir()).replaceAll("\\", "/"), "<home>")
// look for [\d\d ms] or [\d\d s] with optional periods
.replace(/\s\[[\d.]+\s?m?s\]/gm, "")
.replace(/^\[[\d.]+\s?m?s\]/gm, "")
// line numbers in stack traces like at FunctionName (NN:NN)
// it must specifically look at the stacktrace format
.replace(/^\s+at (.*?)\(.*?:\d+(?::\d+)?\)/gm, " at $1(file:NN:NN)")
.replaceAll(Bun.version_with_sha, "<version> (<revision>)")
.replaceAll(Bun.version, "<bun-version>")
.replaceAll(Bun.revision, "<revision>")
.trim()
);
}

View File

@@ -34,7 +34,6 @@ const words: Record<string, { reason: string; limit?: number; regex?: boolean }>
[String.raw`: [a-zA-Z0-9_\.\*\?\[\]\(\)]+ = undefined,`]: { reason: "Do not default a struct field to undefined", limit: 242, regex: true },
"usingnamespace": { reason: "Zig 0.15 will remove `usingnamespace`" },
"catch unreachable": { reason: "For out-of-memory, prefer 'catch bun.outOfMemory()'", limit: 1857 },
"std.fs.Dir": { reason: "Prefer bun.sys + bun.FD instead of std.fs", limit: 170 },
"std.fs.cwd": { reason: "Prefer bun.FD.cwd()", limit: 102 },

View File

@@ -76,7 +76,10 @@ it("console.group", async () => {
.replaceAll("\r\n", "\n")
.replaceAll("\\", "/")
.trim()
.replaceAll(filepath, "<file>");
.replaceAll(filepath, "<file>")
// Normalize line numbers for consistency between debug and release builds
.replace(/\(\d+:\d+\)/g, "(N:NN)")
.replace(/<file>:\d+:\d+/g, "<file>:NN:NN");
expect(stdout).toMatchInlineSnapshot(`
"Basic group
Inside basic group
@@ -118,8 +121,8 @@ Quote"Backslash
expect(stderr).toMatchInlineSnapshot(`
"Warning log
warn: console.warn an error
at <file>:56:14
at loadAndEvaluateModule (2:1)
at <file>:NN:NN
at loadAndEvaluateModule (N:NN)
52 | console.group("Different logs");
53 | console.log("Regular log");
@@ -129,8 +132,8 @@ Quote"Backslash
57 | console.error(new Error("console.error an error"));
^
error: console.error an error
at <file>:57:15
at loadAndEvaluateModule (2:1)
at <file>:NN:NN
at loadAndEvaluateModule (N:NN)
41 | console.groupEnd(); // Extra
42 | console.groupEnd(); // Extra
@@ -140,14 +143,14 @@ error: console.error an error
46 | super(message);
^
NamedError: console.error a named error
at new NamedError (<file>:46:5)
at <file>:58:15
at loadAndEvaluateModule (2:1)
at new NamedError (<file>:NN:NN)
at <file>:NN:NN
at loadAndEvaluateModule (N:NN)
NamedError: console.warn a named error
at new NamedError (<file>:46:5)
at <file>:59:14
at loadAndEvaluateModule (2:1)
at new NamedError (<file>:NN:NN)
at <file>:NN:NN
at loadAndEvaluateModule (N:NN)
Error log"
`);

View File

@@ -398,6 +398,7 @@ test/js/third_party/comlink/comlink.test.ts
test/js/third_party/duckdb/duckdb-basic-usage.test.ts
test/js/third_party/esbuild/esbuild-child_process.test.ts
test/js/third_party/express/app.router.test.ts
test/cli/test/coverage.test.ts
test/js/third_party/express/express.json.test.ts
test/js/third_party/express/express.test.ts
test/js/third_party/express/express.text.test.ts

View File

@@ -0,0 +1,50 @@
import { spawn } from "bun";
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDirWithFiles } from "harness";
test("dependency resolution failure should point to dependency location, not package location", async () => {
const dir = tempDirWithFiles("dependency-location-test", {
"package.json": JSON.stringify({
dependencies: {
"non-existent-package": "1.0.0",
},
}),
"bun.lock": JSON.stringify(
{
lockfileVersion: 0,
workspaces: {
"": {
dependencies: {
"non-existent-package": "1.0.0",
},
},
},
packages: {},
},
null,
2,
),
});
await using proc = spawn({
cmd: [bunExe(), "install", "--production"],
cwd: dir,
env: bunEnv,
stderr: "pipe",
stdout: "pipe",
});
const [stderr, stdout, exitCode] = await Promise.all([
new Response(proc.stderr).text(),
new Response(proc.stdout).text(),
proc.exited,
]);
expect(exitCode).toBe(1);
expect(stderr).toContain("Failed to resolve root prod dependency 'non-existent-package'");
// The error should reference the dependency line, not the root package
// We expect to see line 4 (where "non-existent-package" is defined in the bun.lock)
// not line 3 (where the "" workspace is defined)
expect(stderr).toMatch(/bun\.lock:\d+:\d+/);
});