mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 18:38:55 +00:00
305 lines
9.9 KiB
Zig
305 lines
9.9 KiB
Zig
/// This implements the JavaScript SourceMap class from Node.js.
|
|
///
|
|
const JSSourceMap = @This();
|
|
|
|
sourcemap: *bun.sourcemap.ParsedSourceMap,
|
|
sources: []bun.String = &.{},
|
|
names: []bun.String = &.{},
|
|
|
|
fn findSourceMap(
|
|
globalObject: *JSGlobalObject,
|
|
callFrame: *CallFrame,
|
|
) bun.JSError!JSValue {
|
|
const source_url_value = callFrame.argument(0);
|
|
if (!source_url_value.isString()) {
|
|
return .js_undefined;
|
|
}
|
|
|
|
var source_url_string = try bun.String.fromJS(source_url_value, globalObject);
|
|
defer source_url_string.deref();
|
|
|
|
var source_url_slice = source_url_string.toUTF8(bun.default_allocator);
|
|
defer source_url_slice.deinit();
|
|
|
|
var source_url = source_url_slice.slice();
|
|
if (bun.strings.hasPrefix(source_url, "node:") or bun.strings.hasPrefix(source_url, "bun:") or bun.strings.hasPrefix(source_url, "data:")) {
|
|
return .js_undefined;
|
|
}
|
|
|
|
if (bun.strings.indexOf(source_url, "://")) |source_url_index| {
|
|
if (bun.strings.eqlComptime(source_url[0..source_url_index], "file")) {
|
|
const path = bun.jsc.URL.pathFromFileURL(source_url_string);
|
|
|
|
if (path.tag == .Dead) {
|
|
return globalObject.ERR(.INVALID_URL, "Invalid URL: {s}", .{source_url}).throw();
|
|
}
|
|
|
|
// Replace the file:// URL with the absolute path.
|
|
source_url_string.deref();
|
|
source_url_slice.deinit();
|
|
source_url_string = path;
|
|
source_url_slice = path.toUTF8(bun.default_allocator);
|
|
source_url = source_url_slice.slice();
|
|
}
|
|
}
|
|
|
|
const vm = globalObject.bunVM();
|
|
const source_map = vm.source_mappings.get(source_url) orelse return .js_undefined;
|
|
const fake_sources_array = bun.default_allocator.alloc(bun.String, 1) catch return globalObject.throwOutOfMemory();
|
|
fake_sources_array[0] = source_url_string.dupeRef();
|
|
|
|
const this = bun.new(JSSourceMap, .{
|
|
.sourcemap = source_map,
|
|
.sources = fake_sources_array,
|
|
.names = &.{},
|
|
});
|
|
|
|
return this.toJS(globalObject);
|
|
}
|
|
|
|
pub fn constructor(
|
|
globalObject: *JSGlobalObject,
|
|
callFrame: *CallFrame,
|
|
thisValue: JSValue,
|
|
) bun.JSError!*JSSourceMap {
|
|
const payload_arg = callFrame.argument(0);
|
|
const options_arg = callFrame.argument(1);
|
|
|
|
try globalObject.validateObject("payload", payload_arg, .{});
|
|
|
|
var line_lengths: JSValue = .zero;
|
|
if (options_arg.isObject()) {
|
|
// Node doesn't check it further than this.
|
|
if (try options_arg.getIfPropertyExists(globalObject, "lineLengths")) |lengths| {
|
|
if (lengths.jsType().isArray()) {
|
|
line_lengths = lengths;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Parse the payload to create a proper sourcemap
|
|
var arena = bun.ArenaAllocator.init(bun.default_allocator);
|
|
defer arena.deinit();
|
|
const arena_allocator = arena.allocator();
|
|
|
|
// Extract mappings string from payload
|
|
const mappings_value = try payload_arg.getStringish(globalObject, "mappings") orelse {
|
|
return globalObject.throwInvalidArguments("payload 'mappings' must be a string", .{});
|
|
};
|
|
defer mappings_value.deref();
|
|
|
|
const mappings_str = mappings_value.toUTF8(arena_allocator);
|
|
defer mappings_str.deinit();
|
|
|
|
var names = std.ArrayList(bun.String).init(bun.default_allocator);
|
|
errdefer {
|
|
for (names.items) |*str| {
|
|
str.deref();
|
|
}
|
|
names.deinit();
|
|
}
|
|
|
|
var sources = std.ArrayList(bun.String).init(bun.default_allocator);
|
|
errdefer {
|
|
for (sources.items) |*str| {
|
|
str.deref();
|
|
}
|
|
sources.deinit();
|
|
}
|
|
|
|
if (try payload_arg.getArray(globalObject, "sources")) |sources_value| {
|
|
var iter = try sources_value.arrayIterator(globalObject);
|
|
while (try iter.next()) |source| {
|
|
const source_str = try source.toBunString(globalObject);
|
|
try sources.append(source_str);
|
|
}
|
|
}
|
|
|
|
if (try payload_arg.getArray(globalObject, "names")) |names_value| {
|
|
var iter = try names_value.arrayIterator(globalObject);
|
|
while (try iter.next()) |name| {
|
|
const name_str = try name.toBunString(globalObject);
|
|
try names.append(name_str);
|
|
}
|
|
}
|
|
|
|
// Parse the VLQ mappings
|
|
const parse_result = bun.sourcemap.Mapping.parse(
|
|
bun.default_allocator,
|
|
mappings_str.slice(),
|
|
null, // estimated_mapping_count
|
|
@intCast(sources.items.len), // sources_count
|
|
std.math.maxInt(i32),
|
|
.{ .allow_names = true, .sort = true },
|
|
);
|
|
|
|
const mapping_list = switch (parse_result) {
|
|
.success => |parsed| parsed,
|
|
.fail => |fail| {
|
|
if (fail.loc.toNullable()) |loc| {
|
|
return globalObject.throwValue(globalObject.createSyntaxErrorInstance("{s} at {d}", .{ fail.msg, loc.start }));
|
|
}
|
|
return globalObject.throwValue(globalObject.createSyntaxErrorInstance("{s}", .{fail.msg}));
|
|
},
|
|
};
|
|
|
|
const source_map = bun.new(JSSourceMap, .{
|
|
.sourcemap = bun.new(bun.sourcemap.ParsedSourceMap, mapping_list),
|
|
.sources = sources.items,
|
|
.names = names.items,
|
|
});
|
|
|
|
if (payload_arg != .zero) {
|
|
js.payloadSetCached(thisValue, globalObject, payload_arg);
|
|
}
|
|
if (line_lengths != .zero) {
|
|
js.lineLengthsSetCached(thisValue, globalObject, line_lengths);
|
|
}
|
|
|
|
return source_map;
|
|
}
|
|
|
|
pub fn memoryCost(this: *const JSSourceMap) usize {
|
|
return @sizeOf(JSSourceMap) + this.sources.len * @sizeOf(bun.String) + this.sourcemap.memoryCost();
|
|
}
|
|
|
|
pub fn estimatedSize(this: *JSSourceMap) usize {
|
|
return this.memoryCost();
|
|
}
|
|
|
|
// The cached value should handle this.
|
|
pub fn getPayload(_: *JSSourceMap, _: *JSGlobalObject) JSValue {
|
|
return .js_undefined;
|
|
}
|
|
|
|
// The cached value should handle this.
|
|
pub fn getLineLengths(_: *JSSourceMap, _: *JSGlobalObject) JSValue {
|
|
return .js_undefined;
|
|
}
|
|
|
|
fn getLineColumn(globalObject: *JSGlobalObject, callFrame: *CallFrame) bun.JSError![2]i32 {
|
|
const line_number_value = callFrame.argument(0);
|
|
const column_number_value = callFrame.argument(1);
|
|
|
|
return .{
|
|
// Node.js does no validations.
|
|
try line_number_value.coerce(i32, globalObject),
|
|
try column_number_value.coerce(i32, globalObject),
|
|
};
|
|
}
|
|
|
|
fn mappingNameToJS(this: *const JSSourceMap, globalObject: *JSGlobalObject, mapping: *const bun.sourcemap.Mapping) bun.JSError!JSValue {
|
|
const name_index = mapping.nameIndex();
|
|
if (name_index >= 0) {
|
|
if (this.sourcemap.mappings.getName(name_index)) |name| {
|
|
return bun.String.createUTF8ForJS(globalObject, name);
|
|
} else {
|
|
const index: usize = @intCast(name_index);
|
|
if (index < this.names.len) {
|
|
return this.names[index].toJS(globalObject);
|
|
}
|
|
}
|
|
}
|
|
return .js_undefined;
|
|
}
|
|
|
|
fn sourceNameToJS(this: *const JSSourceMap, globalObject: *JSGlobalObject, mapping: *const bun.sourcemap.Mapping) bun.JSError!JSValue {
|
|
const source_index = mapping.sourceIndex();
|
|
if (source_index >= 0 and source_index < @as(i32, @intCast(this.sources.len))) {
|
|
return this.sources[@intCast(source_index)].toJS(globalObject);
|
|
}
|
|
|
|
return .js_undefined;
|
|
}
|
|
|
|
extern fn Bun__createNodeModuleSourceMapOriginObject(
|
|
globalObject: *JSGlobalObject,
|
|
name: JSValue,
|
|
line: JSValue,
|
|
column: JSValue,
|
|
source: JSValue,
|
|
) JSValue;
|
|
|
|
extern fn Bun__createNodeModuleSourceMapEntryObject(
|
|
globalObject: *JSGlobalObject,
|
|
generatedLine: JSValue,
|
|
generatedColumn: JSValue,
|
|
originalLine: JSValue,
|
|
originalColumn: JSValue,
|
|
source: JSValue,
|
|
name: JSValue,
|
|
) JSValue;
|
|
|
|
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 name = try mappingNameToJS(this, globalObject, &mapping);
|
|
const source = try sourceNameToJS(this, globalObject, &mapping);
|
|
return Bun__createNodeModuleSourceMapOriginObject(
|
|
globalObject,
|
|
name,
|
|
jsc.JSValue.jsNumber(mapping.originalLine()),
|
|
jsc.JSValue.jsNumber(mapping.originalColumn()),
|
|
source,
|
|
);
|
|
}
|
|
|
|
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 name = try mappingNameToJS(this, globalObject, &mapping);
|
|
const source = try sourceNameToJS(this, globalObject, &mapping);
|
|
return Bun__createNodeModuleSourceMapEntryObject(
|
|
globalObject,
|
|
jsc.JSValue.jsNumber(mapping.generatedLine()),
|
|
jsc.JSValue.jsNumber(mapping.generatedColumn()),
|
|
jsc.JSValue.jsNumber(mapping.originalLine()),
|
|
jsc.JSValue.jsNumber(mapping.originalColumn()),
|
|
source,
|
|
name,
|
|
);
|
|
}
|
|
|
|
pub fn deinit(this: *JSSourceMap) void {
|
|
for (this.sources) |*str| {
|
|
str.deref();
|
|
}
|
|
bun.default_allocator.free(this.sources);
|
|
|
|
for (this.names) |*name| {
|
|
name.deref();
|
|
}
|
|
|
|
bun.default_allocator.free(this.names);
|
|
|
|
this.sourcemap.deref();
|
|
bun.destroy(this);
|
|
}
|
|
|
|
pub fn finalize(this: *JSSourceMap) void {
|
|
this.deinit();
|
|
}
|
|
|
|
comptime {
|
|
const jsFunctionFindSourceMap = jsc.toJSHostFn(findSourceMap);
|
|
@export(&jsFunctionFindSourceMap, .{ .name = "Bun__JSSourceMap__find" });
|
|
}
|
|
|
|
pub const js = jsc.Codegen.JSSourceMap;
|
|
pub const fromJS = js.fromJS;
|
|
pub const fromJSDirect = js.fromJSDirect;
|
|
pub const toJS = js.toJS;
|
|
|
|
const string = []const u8;
|
|
|
|
const bun = @import("bun");
|
|
const std = @import("std");
|
|
|
|
const jsc = bun.jsc;
|
|
const CallFrame = jsc.CallFrame;
|
|
const JSGlobalObject = jsc.JSGlobalObject;
|
|
const JSValue = jsc.JSValue;
|