mirror of
https://github.com/oven-sh/bun
synced 2026-02-15 05:12:29 +00:00
471 lines
17 KiB
Zig
471 lines
17 KiB
Zig
const SavedSourceMap = @This();
|
|
|
|
/// This is a pointer to the map located on the VirtualMachine struct
|
|
map: *HashTable,
|
|
mutex: bun.Mutex = .{},
|
|
|
|
pub const vlq_offset = 24;
|
|
|
|
pub fn init(this: *SavedSourceMap, map: *HashTable) void {
|
|
this.* = .{
|
|
.map = map,
|
|
.mutex = .{},
|
|
};
|
|
|
|
this.map.lockPointers();
|
|
}
|
|
|
|
pub inline fn lock(map: *SavedSourceMap) void {
|
|
map.mutex.lock();
|
|
map.map.unlockPointers();
|
|
}
|
|
|
|
pub inline fn unlock(map: *SavedSourceMap) void {
|
|
map.map.lockPointers();
|
|
map.mutex.unlock();
|
|
}
|
|
|
|
// For the runtime, we store the number of mappings and how many bytes the final list is at the beginning of the array
|
|
// The first 8 bytes are the length of the array
|
|
// The second 8 bytes are the number of mappings
|
|
pub const SavedMappings = struct {
|
|
data: [*]u8,
|
|
|
|
pub fn vlq(this: SavedMappings) []u8 {
|
|
return this.data[vlq_offset..this.len()];
|
|
}
|
|
|
|
pub inline fn len(this: SavedMappings) usize {
|
|
return @as(u64, @bitCast(this.data[0..8].*));
|
|
}
|
|
|
|
pub fn deinit(this: SavedMappings) void {
|
|
bun.default_allocator.free(this.data[0..this.len()]);
|
|
}
|
|
|
|
pub fn toMapping(this: SavedMappings, allocator: Allocator, path: string) anyerror!ParsedSourceMap {
|
|
const result = SourceMap.Mapping.parse(
|
|
allocator,
|
|
this.data[vlq_offset..this.len()],
|
|
@as(usize, @bitCast(this.data[8..16].*)),
|
|
1,
|
|
@as(usize, @bitCast(this.data[16..24].*)),
|
|
.{},
|
|
);
|
|
switch (result) {
|
|
.fail => |fail| {
|
|
if (Output.enable_ansi_colors_stderr) {
|
|
try fail.toData(path).writeFormat(
|
|
Output.errorWriter(),
|
|
logger.Kind.warn,
|
|
false,
|
|
true,
|
|
);
|
|
} else {
|
|
try fail.toData(path).writeFormat(
|
|
Output.errorWriter(),
|
|
logger.Kind.warn,
|
|
false,
|
|
false,
|
|
);
|
|
}
|
|
|
|
return fail.err;
|
|
},
|
|
.success => |success| {
|
|
return success;
|
|
},
|
|
}
|
|
}
|
|
};
|
|
|
|
/// Compact variant that uses LineOffsetTable.Compact for reduced memory usage
|
|
pub const SavedMappingsCompact = struct {
|
|
compact_table: *SourceMap.LineOffsetTable.Compact,
|
|
sources_count: usize,
|
|
|
|
pub fn init(allocator: Allocator, vlq_mappings: []const u8) !*SavedMappingsCompact {
|
|
return bun.new(SavedMappingsCompact, .{
|
|
.compact_table = try SourceMap.LineOffsetTable.Compact.init(allocator, vlq_mappings),
|
|
.sources_count = 1, // Default to 1 source
|
|
});
|
|
}
|
|
|
|
pub fn initWithSourcesCount(allocator: Allocator, vlq_mappings: []const u8, sources_count: usize) !SavedMappingsCompact {
|
|
return bun.new(SavedMappingsCompact, .{
|
|
.compact_table = try SourceMap.LineOffsetTable.Compact.init(allocator, vlq_mappings),
|
|
.sources_count = sources_count,
|
|
});
|
|
}
|
|
|
|
pub fn deinit(this: *SavedMappingsCompact) void {
|
|
this.compact_table.deref();
|
|
bun.destroy(this);
|
|
}
|
|
|
|
fn toCompactMappings(this: *SavedMappingsCompact) anyerror!ParsedSourceMap {
|
|
// For error reporting and other uses, stay in compact format
|
|
// Increment ref count and return compact data
|
|
this.compact_table.ref();
|
|
|
|
// Calculate proper input line count - use the last line index as the total
|
|
const input_line_count = if (this.compact_table.line_offsets().len() > 1)
|
|
this.compact_table.line_offsets().len() - 1
|
|
else
|
|
1;
|
|
|
|
return ParsedSourceMap{
|
|
.ref_count = .init(),
|
|
.input_line_count = input_line_count,
|
|
.mappings = .{ .compact = this.compact_table },
|
|
.external_source_names = &.{},
|
|
.underlying_provider = .none,
|
|
.is_standalone_module_graph = false,
|
|
};
|
|
}
|
|
|
|
fn toFullMappings(this: *SavedMappingsCompact, allocator: Allocator, path: string) anyerror!ParsedSourceMap {
|
|
const line_offsets = this.compact_table.line_offsets();
|
|
// Parse the VLQ mappings using the existing parser for coverage analysis
|
|
const input_line_count = if (line_offsets.len() > 1)
|
|
line_offsets.len() - 1
|
|
else
|
|
1;
|
|
|
|
const result = SourceMap.Mapping.parse(
|
|
allocator,
|
|
this.compact_table.vlq_mappings,
|
|
null, // estimated mapping count
|
|
@intCast(this.sources_count), // use stored sources count
|
|
input_line_count, // input line count - use proper calculation
|
|
.{ .allow_names = true, .sort = true }, // Enable names support for coverage
|
|
);
|
|
switch (result) {
|
|
.fail => |fail| {
|
|
if (Output.enable_ansi_colors_stderr) {
|
|
try fail.toData(path).writeFormat(
|
|
Output.errorWriter(),
|
|
logger.Kind.warn,
|
|
false,
|
|
true,
|
|
);
|
|
} else {
|
|
try fail.toData(path).writeFormat(
|
|
Output.errorWriter(),
|
|
logger.Kind.warn,
|
|
false,
|
|
false,
|
|
);
|
|
}
|
|
return fail.err;
|
|
},
|
|
.success => |success| {
|
|
return success;
|
|
},
|
|
}
|
|
}
|
|
|
|
pub fn toMapping(this: *SavedMappingsCompact, allocator: Allocator, path: string) anyerror!ParsedSourceMap {
|
|
// Check if coverage is enabled - only then convert to full format
|
|
if (bun.cli.Command.get().test_options.coverage.enabled) {
|
|
return this.toFullMappings(allocator, path);
|
|
} else {
|
|
return this.toCompactMappings();
|
|
}
|
|
}
|
|
};
|
|
|
|
/// ParsedSourceMap is the canonical form for sourcemaps,
|
|
///
|
|
/// but `SavedMappings` and `SourceProviderMap` are much cheaper to construct.
|
|
/// In `fn get`, this value gets converted to ParsedSourceMap always
|
|
pub const Value = bun.TaggedPointerUnion(.{
|
|
ParsedSourceMap,
|
|
SavedMappings,
|
|
SavedMappingsCompact,
|
|
SourceProviderMap,
|
|
BakeSourceProvider,
|
|
});
|
|
|
|
pub const MissingSourceMapNoteInfo = struct {
|
|
pub var storage: bun.PathBuffer = undefined;
|
|
pub var path: ?[]const u8 = null;
|
|
pub var seen_invalid = false;
|
|
|
|
pub fn print() void {
|
|
if (seen_invalid) return;
|
|
if (path) |note| {
|
|
Output.note("missing sourcemaps for {s}", .{note});
|
|
Output.note("consider bundling with '--sourcemap' to get unminified traces", .{});
|
|
}
|
|
}
|
|
};
|
|
|
|
pub fn putBakeSourceProvider(this: *SavedSourceMap, opaque_source_provider: *BakeSourceProvider, path: []const u8) void {
|
|
this.putValue(path, Value.init(opaque_source_provider)) catch bun.outOfMemory();
|
|
}
|
|
|
|
pub fn putZigSourceProvider(this: *SavedSourceMap, opaque_source_provider: *anyopaque, path: []const u8) void {
|
|
const source_provider: *SourceProviderMap = @ptrCast(opaque_source_provider);
|
|
this.putValue(path, Value.init(source_provider)) catch bun.outOfMemory();
|
|
}
|
|
|
|
pub fn removeZigSourceProvider(this: *SavedSourceMap, opaque_source_provider: *anyopaque, path: []const u8) void {
|
|
this.lock();
|
|
defer this.unlock();
|
|
|
|
const entry = this.map.getEntry(bun.hash(path)) orelse return;
|
|
const old_value = Value.from(entry.value_ptr.*);
|
|
if (old_value.get(SourceProviderMap)) |prov| {
|
|
if (@intFromPtr(prov) == @intFromPtr(opaque_source_provider)) {
|
|
// there is nothing to unref or deinit
|
|
this.map.removeByPtr(entry.key_ptr);
|
|
}
|
|
} else if (old_value.get(ParsedSourceMap)) |map| {
|
|
if (map.underlying_provider.provider()) |prov| {
|
|
if (@intFromPtr(prov.ptr()) == @intFromPtr(opaque_source_provider)) {
|
|
this.map.removeByPtr(entry.key_ptr);
|
|
map.deref();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub const HashTable = std.HashMap(u64, *anyopaque, bun.IdentityContext(u64), 80);
|
|
|
|
pub fn onSourceMapChunk(this: *SavedSourceMap, chunk: SourceMap.Chunk, source: *const logger.Source) anyerror!void {
|
|
try this.putMappings(source, chunk.buffer);
|
|
}
|
|
|
|
pub const SourceMapHandler = js_printer.SourceMapHandler.For(SavedSourceMap, onSourceMapChunk);
|
|
|
|
pub fn deinit(this: *SavedSourceMap) void {
|
|
{
|
|
this.lock();
|
|
defer this.unlock();
|
|
|
|
var iter = this.map.valueIterator();
|
|
while (iter.next()) |val| {
|
|
var value = Value.from(val.*);
|
|
if (value.get(ParsedSourceMap)) |source_map| {
|
|
source_map.deref();
|
|
} else if (value.get(SavedMappings)) |saved_mappings| {
|
|
var saved = SavedMappings{ .data = @as([*]u8, @ptrCast(saved_mappings)) };
|
|
saved.deinit();
|
|
} else if (value.get(SavedMappingsCompact)) |saved_compact| {
|
|
var compact: *SavedMappingsCompact = saved_compact;
|
|
compact.deinit();
|
|
bun.default_allocator.destroy(compact);
|
|
} else if (value.get(SourceProviderMap)) |provider| {
|
|
_ = provider; // do nothing, we did not hold a ref to ZigSourceProvider
|
|
}
|
|
}
|
|
}
|
|
|
|
this.map.unlockPointers();
|
|
this.map.deinit();
|
|
}
|
|
|
|
pub fn putMappings(this: *SavedSourceMap, source: *const logger.Source, mappings: MutableString) !void {
|
|
// Check if coverage is enabled - if so, use full format for easier unpacking
|
|
// Otherwise use compact format for memory savings
|
|
if (bun.cli.Command.get().test_options.coverage.enabled) {
|
|
const data = try bun.default_allocator.dupe(u8, mappings.list.items);
|
|
try this.putValue(source.path.text, Value.init(bun.cast(*SavedMappings, data.ptr)));
|
|
} else {
|
|
// The mappings buffer has a 24-byte header when prepend_count is true
|
|
// We need to skip this header for the compact format
|
|
const vlq_data = if (mappings.list.items.len > vlq_offset)
|
|
mappings.list.items[vlq_offset..]
|
|
else
|
|
mappings.list.items;
|
|
|
|
const compact = try SavedMappingsCompact.init(bun.default_allocator, vlq_data);
|
|
try this.putValue(source.path.text, Value.init(compact));
|
|
}
|
|
}
|
|
|
|
pub fn putValue(this: *SavedSourceMap, path: []const u8, value: Value) !void {
|
|
this.lock();
|
|
defer this.unlock();
|
|
|
|
const entry = try this.map.getOrPut(bun.hash(path));
|
|
if (entry.found_existing) {
|
|
var old_value = Value.from(entry.value_ptr.*);
|
|
if (old_value.get(ParsedSourceMap)) |parsed_source_map| {
|
|
var source_map: *ParsedSourceMap = parsed_source_map;
|
|
source_map.deref();
|
|
} else if (old_value.get(SavedMappings)) |saved_mappings| {
|
|
var saved = SavedMappings{ .data = @as([*]u8, @ptrCast(saved_mappings)) };
|
|
saved.deinit();
|
|
} else if (old_value.get(SavedMappingsCompact)) |saved_compact| {
|
|
var compact: *SavedMappingsCompact = saved_compact;
|
|
compact.deinit();
|
|
} else if (old_value.get(SourceProviderMap)) |provider| {
|
|
_ = provider; // do nothing, we did not hold a ref to ZigSourceProvider
|
|
}
|
|
}
|
|
entry.value_ptr.* = value.ptr();
|
|
}
|
|
|
|
/// You must call `sourcemap.map.deref()` or you will leak memory
|
|
fn getWithContent(
|
|
this: *SavedSourceMap,
|
|
path: string,
|
|
hint: SourceMap.ParseUrlResultHint,
|
|
) SourceMap.ParseUrl {
|
|
const hash = bun.hash(path);
|
|
|
|
// This lock is for the hash table
|
|
this.lock();
|
|
|
|
// This mapping entry is only valid while the mutex is locked
|
|
const mapping = this.map.getEntry(hash) orelse {
|
|
this.unlock();
|
|
return .{};
|
|
};
|
|
|
|
switch (Value.from(mapping.value_ptr.*).tag()) {
|
|
@field(Value.Tag, @typeName(ParsedSourceMap)) => {
|
|
defer this.unlock();
|
|
const map = Value.from(mapping.value_ptr.*).as(ParsedSourceMap);
|
|
map.ref();
|
|
return .{ .map = map };
|
|
},
|
|
@field(Value.Tag, @typeName(SavedMappings)) => {
|
|
defer this.unlock();
|
|
var saved = SavedMappings{ .data = @as([*]u8, @ptrCast(Value.from(mapping.value_ptr.*).as(ParsedSourceMap))) };
|
|
defer saved.deinit();
|
|
const result = bun.new(ParsedSourceMap, saved.toMapping(bun.default_allocator, path) catch {
|
|
_ = this.map.remove(mapping.key_ptr.*);
|
|
return .{};
|
|
});
|
|
mapping.value_ptr.* = Value.init(result).ptr();
|
|
result.ref();
|
|
|
|
return .{ .map = result };
|
|
},
|
|
@field(Value.Tag, @typeName(SavedMappingsCompact)) => {
|
|
defer this.unlock();
|
|
var saved_compact = Value.from(mapping.value_ptr.*).as(SavedMappingsCompact);
|
|
const parsed_map = saved_compact.toMapping(bun.default_allocator, path) catch {
|
|
// On failure, remove the entry and return null to indicate no sourcemap
|
|
_ = this.map.remove(mapping.key_ptr.*);
|
|
return .{};
|
|
};
|
|
const result = bun.new(ParsedSourceMap, parsed_map);
|
|
mapping.value_ptr.* = Value.init(result).ptr();
|
|
result.ref();
|
|
|
|
return .{ .map = result };
|
|
},
|
|
@field(Value.Tag, @typeName(SourceProviderMap)) => {
|
|
const ptr: *SourceProviderMap = Value.from(mapping.value_ptr.*).as(SourceProviderMap);
|
|
this.unlock();
|
|
|
|
// Do not lock the mutex while we're parsing JSON!
|
|
if (ptr.getSourceMap(path, .none, hint)) |parse| {
|
|
if (parse.map) |map| {
|
|
map.ref();
|
|
// The mutex is not locked. We have to check the hash table again.
|
|
this.putValue(path, Value.init(map)) catch bun.outOfMemory();
|
|
|
|
return parse;
|
|
}
|
|
}
|
|
|
|
this.lock();
|
|
defer this.unlock();
|
|
// does not have a valid source map. let's not try again
|
|
_ = this.map.remove(hash);
|
|
|
|
// Store path for a user note.
|
|
const storage = MissingSourceMapNoteInfo.storage[0..path.len];
|
|
@memcpy(storage, path);
|
|
MissingSourceMapNoteInfo.path = storage;
|
|
return .{};
|
|
},
|
|
@field(Value.Tag, @typeName(BakeSourceProvider)) => {
|
|
// TODO: This is a copy-paste of above branch
|
|
const ptr: *BakeSourceProvider = Value.from(mapping.value_ptr.*).as(BakeSourceProvider);
|
|
this.unlock();
|
|
|
|
// Do not lock the mutex while we're parsing JSON!
|
|
if (ptr.getSourceMap(path, .none, hint)) |parse| {
|
|
if (parse.map) |map| {
|
|
map.ref();
|
|
// The mutex is not locked. We have to check the hash table again.
|
|
this.putValue(path, Value.init(map)) catch bun.outOfMemory();
|
|
|
|
return parse;
|
|
}
|
|
}
|
|
|
|
this.lock();
|
|
defer this.unlock();
|
|
// does not have a valid source map. let's not try again
|
|
_ = this.map.remove(hash);
|
|
|
|
// Store path for a user note.
|
|
const storage = MissingSourceMapNoteInfo.storage[0..path.len];
|
|
@memcpy(storage, path);
|
|
MissingSourceMapNoteInfo.path = storage;
|
|
return .{};
|
|
},
|
|
else => {
|
|
if (Environment.allow_assert) {
|
|
@panic("Corrupt pointer tag");
|
|
}
|
|
this.unlock();
|
|
|
|
return .{};
|
|
},
|
|
}
|
|
}
|
|
|
|
/// You must `deref()` the returned value or you will leak memory
|
|
pub fn get(this: *SavedSourceMap, path: string) ?*ParsedSourceMap {
|
|
return this.getWithContent(path, .mappings_only).map;
|
|
}
|
|
|
|
pub fn resolveMapping(
|
|
this: *SavedSourceMap,
|
|
path: []const u8,
|
|
line: i32,
|
|
column: i32,
|
|
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 } },
|
|
});
|
|
const map = parse.map orelse return null;
|
|
|
|
const mapping = parse.mapping orelse
|
|
map.mappings.find(line, column) orelse
|
|
return null;
|
|
|
|
return .{
|
|
.mapping = mapping,
|
|
.source_map = map,
|
|
.prefetched_source_code = parse.source_contents,
|
|
};
|
|
}
|
|
|
|
const string = []const u8;
|
|
|
|
const std = @import("std");
|
|
const Allocator = std.mem.Allocator;
|
|
|
|
const bun = @import("bun");
|
|
const Environment = bun.Environment;
|
|
const MutableString = bun.MutableString;
|
|
const Output = bun.Output;
|
|
const js_printer = bun.js_printer;
|
|
const logger = bun.logger;
|
|
|
|
const SourceMap = bun.sourcemap;
|
|
const BakeSourceProvider = bun.sourcemap.BakeSourceProvider;
|
|
const ParsedSourceMap = SourceMap.ParsedSourceMap;
|
|
const SourceProviderMap = SourceMap.SourceProviderMap;
|