mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 10:28:47 +00:00
Fix DevServer HMR sourcemap offset issues (#22739)
## Summary Fixes sourcemap offset issues in DevServer HMR mode that were causing incorrect line number mappings when debugging. ## Problem When using DevServer with HMR enabled, sourcemap line numbers were consistently off by one or more lines when shown in Chrome DevTools. In some cases, they were off when shown in the terminal as well. ## Solution ### 1. Remove magic +2 offset Removed an arbitrary "+2" that was added to `runtime.line_count` in SourceMapStore.zig. The comment said "magic fairy in my dreams said it would align the source maps" - this was causing positions to be incorrectly offset. ### 2. Fix double-increment bug ErrorReportRequest.zig was incorrectly adding 1 to line numbers that were already 1-based from the browser, causing an off-by-one error. ### 3. Improve type safety Converted all line/column handling to use `bun.Ordinal` type instead of raw `i32`, ensuring consistent 0-based vs 1-based conversions throughout the codebase. ## Test plan - [x] Added comprehensive sourcemap tests for complex error scenarios - [x] Tested with React applications in dev mode - [x] Verified line numbers match correctly in browser dev tools - [x] Existing sourcemap tests continue to pass 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -69,8 +69,8 @@ pub fn runWithBody(ctx: *ErrorReportRequest, body: []const u8, r: AnyResponse) !
|
||||
.function_name = .init(function_name),
|
||||
.source_url = .init(file_name),
|
||||
.position = if (line > 0) .{
|
||||
.line = .fromOneBased(line + 1),
|
||||
.column = .fromOneBased(@max(1, column)),
|
||||
.line = .fromOneBased(line),
|
||||
.column = if (column < 1) .invalid else .fromOneBased(column),
|
||||
.line_start_byte = 0,
|
||||
} else .{
|
||||
.line = .invalid,
|
||||
@@ -147,10 +147,10 @@ pub fn runWithBody(ctx: *ErrorReportRequest, body: []const u8, r: AnyResponse) !
|
||||
|
||||
// Remap the frame
|
||||
const remapped = result.mappings.find(
|
||||
frame.position.line.oneBased(),
|
||||
frame.position.column.zeroBased(),
|
||||
frame.position.line,
|
||||
frame.position.column,
|
||||
);
|
||||
if (remapped) |remapped_position| {
|
||||
if (remapped) |*remapped_position| {
|
||||
frame.position = .{
|
||||
.line = .fromZeroBased(remapped_position.originalLine()),
|
||||
.column = .fromZeroBased(remapped_position.originalColumn()),
|
||||
|
||||
@@ -207,8 +207,9 @@ pub const Entry = struct {
|
||||
.original_column = 0,
|
||||
};
|
||||
|
||||
// +2 because the magic fairy in my dreams said it would align the source maps.
|
||||
var lines_between: u32 = runtime.line_count + 2;
|
||||
// The runtime.line_count counts newlines (e.g., 2941 for a 2942-line file).
|
||||
// The runtime ends at line 2942 with })({ so modules start after that.
|
||||
var lines_between: u32 = runtime.line_count;
|
||||
|
||||
// Join all of the mappings together.
|
||||
for (0..map_files.len) |i| switch (map_files.get(i)) {
|
||||
|
||||
@@ -251,8 +251,8 @@ export async function onRuntimeError(err: any, fatal = false, async = false) {
|
||||
writer.stringWithLength(browserUrl);
|
||||
writer.u32(parsed.length);
|
||||
for (const frame of parsed) {
|
||||
writer.u32(frame.line ?? 0);
|
||||
writer.u32(frame.col ?? 0);
|
||||
writer.i32(frame.line ?? 0);
|
||||
writer.i32(frame.col ?? 0);
|
||||
writer.stringWithLength(frame.fn ?? "");
|
||||
const fileName = frame.file;
|
||||
if (fileName) {
|
||||
|
||||
@@ -298,13 +298,13 @@ pub fn get(this: *SavedSourceMap, path: string) ?*ParsedSourceMap {
|
||||
pub fn resolveMapping(
|
||||
this: *SavedSourceMap,
|
||||
path: []const u8,
|
||||
line: i32,
|
||||
column: i32,
|
||||
line: bun.Ordinal,
|
||||
column: bun.Ordinal,
|
||||
source_handling: SourceMap.SourceContentHandling,
|
||||
) ?SourceMap.Mapping.Lookup {
|
||||
const parse = this.getWithContent(path, switch (source_handling) {
|
||||
.no_source_contents => .mappings_only,
|
||||
.source_contents => .{ .all = .{ .line = line, .column = column } },
|
||||
.source_contents => .{ .all = .{ .line = @max(line.zeroBased(), 0), .column = @max(column.zeroBased(), 0) } },
|
||||
});
|
||||
const map = parse.map orelse return null;
|
||||
|
||||
|
||||
@@ -2606,8 +2606,8 @@ pub fn remapStackFramePositions(this: *VirtualMachine, frames: [*]jsc.ZigStackFr
|
||||
|
||||
if (this.resolveSourceMapping(
|
||||
sourceURL.slice(),
|
||||
@max(frame.position.line.zeroBased(), 0),
|
||||
@max(frame.position.column.zeroBased(), 0),
|
||||
frame.position.line,
|
||||
frame.position.column,
|
||||
.no_source_contents,
|
||||
)) |lookup| {
|
||||
const source_map = lookup.source_map;
|
||||
@@ -2745,8 +2745,8 @@ pub fn remapZigException(
|
||||
else
|
||||
this.resolveSourceMapping(
|
||||
top_source_url.slice(),
|
||||
@max(top.position.line.zeroBased(), 0),
|
||||
@max(top.position.column.zeroBased(), 0),
|
||||
top.position.line,
|
||||
top.position.column,
|
||||
.source_contents,
|
||||
);
|
||||
|
||||
@@ -2834,8 +2834,8 @@ pub fn remapZigException(
|
||||
defer source_url.deinit();
|
||||
if (this.resolveSourceMapping(
|
||||
source_url.slice(),
|
||||
@max(frame.position.line.zeroBased(), 0),
|
||||
@max(frame.position.column.zeroBased(), 0),
|
||||
frame.position.line,
|
||||
frame.position.column,
|
||||
.no_source_contents,
|
||||
)) |lookup| {
|
||||
defer if (lookup.source_map) |map| map.deref();
|
||||
@@ -3440,8 +3440,8 @@ pub noinline fn printGithubAnnotation(exception: *ZigException) void {
|
||||
pub fn resolveSourceMapping(
|
||||
this: *VirtualMachine,
|
||||
path: []const u8,
|
||||
line: i32,
|
||||
column: i32,
|
||||
line: Ordinal,
|
||||
column: Ordinal,
|
||||
source_handling: SourceMap.SourceContentHandling,
|
||||
) ?SourceMap.Mapping.Lookup {
|
||||
return this.source_mappings.resolveMapping(path, line, column, source_handling) orelse {
|
||||
|
||||
14
src/bun.zig
14
src/bun.zig
@@ -3391,36 +3391,36 @@ pub fn OrdinalT(comptime Int: type) type {
|
||||
start = 0,
|
||||
_,
|
||||
|
||||
pub fn fromZeroBased(int: Int) @This() {
|
||||
pub inline fn fromZeroBased(int: Int) @This() {
|
||||
assert(int >= 0);
|
||||
assert(int != std.math.maxInt(Int));
|
||||
return @enumFromInt(int);
|
||||
}
|
||||
|
||||
pub fn fromOneBased(int: Int) @This() {
|
||||
pub inline fn fromOneBased(int: Int) @This() {
|
||||
assert(int > 0);
|
||||
return @enumFromInt(int - 1);
|
||||
}
|
||||
|
||||
pub fn zeroBased(ord: @This()) Int {
|
||||
pub inline fn zeroBased(ord: @This()) Int {
|
||||
return @intFromEnum(ord);
|
||||
}
|
||||
|
||||
pub fn oneBased(ord: @This()) Int {
|
||||
pub inline fn oneBased(ord: @This()) Int {
|
||||
return @intFromEnum(ord) + 1;
|
||||
}
|
||||
|
||||
/// Add two ordinal numbers together. Both are converted to zero-based before addition.
|
||||
pub fn add(ord: @This(), b: @This()) @This() {
|
||||
pub inline fn add(ord: @This(), b: @This()) @This() {
|
||||
return fromZeroBased(ord.zeroBased() + b.zeroBased());
|
||||
}
|
||||
|
||||
/// Add a scalar value to an ordinal number
|
||||
pub fn addScalar(ord: @This(), inc: Int) @This() {
|
||||
pub inline fn addScalar(ord: @This(), inc: Int) @This() {
|
||||
return fromZeroBased(ord.zeroBased() + inc);
|
||||
}
|
||||
|
||||
pub fn isValid(ord: @This()) bool {
|
||||
pub inline fn isValid(ord: @This()) bool {
|
||||
return ord.zeroBased() >= 0;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -554,7 +554,7 @@ pub const ByteRangeMapping = struct {
|
||||
}
|
||||
const column_position = byte_offset -| line_start_byte_offset;
|
||||
|
||||
if (parsed_mapping.mappings.find(@intCast(new_line_index), @intCast(column_position))) |*point| {
|
||||
if (parsed_mapping.mappings.find(.fromZeroBased(@intCast(new_line_index)), .fromZeroBased(@intCast(column_position)))) |*point| {
|
||||
if (point.original.lines.zeroBased() < 0) continue;
|
||||
|
||||
const line: u32 = @as(u32, @intCast(point.original.lines.zeroBased()));
|
||||
@@ -598,7 +598,7 @@ pub const ByteRangeMapping = struct {
|
||||
|
||||
const column_position = byte_offset -| line_start_byte_offset;
|
||||
|
||||
if (parsed_mapping.mappings.find(@intCast(new_line_index), @intCast(column_position))) |point| {
|
||||
if (parsed_mapping.mappings.find(.fromZeroBased(@intCast(new_line_index)), .fromZeroBased(@intCast(column_position)))) |point| {
|
||||
if (point.original.lines.zeroBased() < 0) continue;
|
||||
|
||||
const line: u32 = @as(u32, @intCast(point.original.lines.zeroBased()));
|
||||
|
||||
@@ -245,7 +245,7 @@ extern fn Bun__createNodeModuleSourceMapEntryObject(
|
||||
pub fn findOrigin(this: *JSSourceMap, globalObject: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue {
|
||||
const line_number, const column_number = try getLineColumn(globalObject, callFrame);
|
||||
|
||||
const mapping = this.sourcemap.mappings.find(line_number, column_number) orelse return jsc.JSValue.createEmptyObject(globalObject, 0);
|
||||
const mapping = this.sourcemap.mappings.find(.fromZeroBased(line_number), .fromZeroBased(column_number)) orelse return jsc.JSValue.createEmptyObject(globalObject, 0);
|
||||
const name = try mappingNameToJS(this, globalObject, &mapping);
|
||||
const source = try sourceNameToJS(this, globalObject, &mapping);
|
||||
return Bun__createNodeModuleSourceMapOriginObject(
|
||||
@@ -260,7 +260,7 @@ pub fn findOrigin(this: *JSSourceMap, globalObject: *JSGlobalObject, callFrame:
|
||||
pub fn findEntry(this: *JSSourceMap, globalObject: *JSGlobalObject, callFrame: *CallFrame) bun.JSError!JSValue {
|
||||
const line_number, const column_number = try getLineColumn(globalObject, callFrame);
|
||||
|
||||
const mapping = this.sourcemap.mappings.find(line_number, column_number) orelse return jsc.JSValue.createEmptyObject(globalObject, 0);
|
||||
const mapping = this.sourcemap.mappings.find(.fromZeroBased(line_number), .fromZeroBased(column_number)) orelse return jsc.JSValue.createEmptyObject(globalObject, 0);
|
||||
|
||||
const name = try mappingNameToJS(this, globalObject, &mapping);
|
||||
const source = try sourceNameToJS(this, globalObject, &mapping);
|
||||
|
||||
@@ -218,7 +218,7 @@ pub fn parseJSON(
|
||||
const mapping, const source_index = switch (hint) {
|
||||
.source_only => |index| .{ null, index },
|
||||
.all => |loc| brk: {
|
||||
const mapping = map.?.mappings.find(loc.line, loc.column) orelse
|
||||
const mapping = map.?.mappings.find(.fromZeroBased(loc.line), .fromZeroBased(loc.column)) orelse
|
||||
break :brk .{ null, null };
|
||||
break :brk .{ mapping, std.math.cast(u32, mapping.source_index) };
|
||||
},
|
||||
@@ -315,14 +315,14 @@ pub const Mapping = struct {
|
||||
this.impl = .{ .with_names = with_names };
|
||||
}
|
||||
|
||||
fn findIndexFromGenerated(line_column_offsets: []const LineColumnOffset, line: i32, column: i32) ?usize {
|
||||
fn findIndexFromGenerated(line_column_offsets: []const LineColumnOffset, line: bun.Ordinal, column: bun.Ordinal) ?usize {
|
||||
var count = line_column_offsets.len;
|
||||
var index: usize = 0;
|
||||
while (count > 0) {
|
||||
const step = count / 2;
|
||||
const i: usize = index + step;
|
||||
const mapping = line_column_offsets[i];
|
||||
if (mapping.lines.zeroBased() < line or (mapping.lines.zeroBased() == line and mapping.columns.zeroBased() <= column)) {
|
||||
if (mapping.lines.zeroBased() < line.zeroBased() or (mapping.lines.zeroBased() == line.zeroBased() and mapping.columns.zeroBased() <= column.zeroBased())) {
|
||||
index = i + 1;
|
||||
count -|= step + 1;
|
||||
} else {
|
||||
@@ -331,7 +331,7 @@ pub const Mapping = struct {
|
||||
}
|
||||
|
||||
if (index > 0) {
|
||||
if (line_column_offsets[index - 1].lines.zeroBased() == line) {
|
||||
if (line_column_offsets[index - 1].lines.zeroBased() == line.zeroBased()) {
|
||||
return index - 1;
|
||||
}
|
||||
}
|
||||
@@ -339,7 +339,7 @@ pub const Mapping = struct {
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn findIndex(this: *const List, line: i32, column: i32) ?usize {
|
||||
pub fn findIndex(this: *const List, line: bun.Ordinal, column: bun.Ordinal) ?usize {
|
||||
switch (this.impl) {
|
||||
inline else => |*list| {
|
||||
if (findIndexFromGenerated(list.items(.generated), line, column)) |i| {
|
||||
@@ -383,7 +383,7 @@ pub const Mapping = struct {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find(this: *const List, line: i32, column: i32) ?Mapping {
|
||||
pub fn find(this: *const List, line: bun.Ordinal, column: bun.Ordinal) ?Mapping {
|
||||
switch (this.impl) {
|
||||
inline else => |*list, tag| {
|
||||
if (findIndexFromGenerated(list.items(.generated), line, column)) |i| {
|
||||
@@ -1356,8 +1356,8 @@ pub const SourceContent = struct {
|
||||
|
||||
pub fn find(
|
||||
this: *const SourceMap,
|
||||
line: i32,
|
||||
column: i32,
|
||||
line: bun.Ordinal,
|
||||
column: bun.Ordinal,
|
||||
) ?Mapping {
|
||||
return this.mapping.find(line, column);
|
||||
}
|
||||
|
||||
@@ -121,7 +121,12 @@ function indexOfLineColumn(text: string, search: string) {
|
||||
}
|
||||
|
||||
function charOffsetToLineColumn(text: string, offset: number) {
|
||||
let line = 1;
|
||||
// sourcemap lines are 0-based.
|
||||
// > If present, the **zero-based** starting line in the original source. This
|
||||
// > field contains a base64 VLQ relative to the previous occurrence of this
|
||||
// > field, unless it is the first occurrence of this field, in which case the
|
||||
// > whole value is represented. Shall be present if there is a source field.
|
||||
let line = 0;
|
||||
let i = 0;
|
||||
let prevI = 0;
|
||||
while (i < offset) {
|
||||
@@ -133,5 +138,5 @@ function charOffsetToLineColumn(text: string, offset: number) {
|
||||
i = nextIndex + 1;
|
||||
line++;
|
||||
}
|
||||
return { line: 1 + line, column: offset - prevI };
|
||||
return { line: line, column: offset - prevI };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user