diff --git a/src/bake/BakeSourceProvider.cpp b/src/bake/BakeSourceProvider.cpp index 4534ddd3e8..d2111891ab 100644 --- a/src/bake/BakeSourceProvider.cpp +++ b/src/bake/BakeSourceProvider.cpp @@ -1,5 +1,6 @@ // clang-format off #include "BakeSourceProvider.h" +#include "DevServerSourceProvider.h" #include "BakeGlobalObject.h" #include "JavaScriptCore/CallData.h" #include "JavaScriptCore/Completion.h" @@ -78,6 +79,33 @@ extern "C" JSC::EncodedJSValue BakeLoadServerHmrPatch(GlobalObject* global, BunS return JSC::JSValue::encode(result); } +extern "C" JSC::EncodedJSValue BakeLoadServerHmrPatchWithSourceMap(GlobalObject* global, BunString source, BunString sourceMapJSON) { + JSC::VM&vm = global->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + String string = "bake://server.patch.js"_s; + JSC::SourceOrigin origin = JSC::SourceOrigin(WTF::URL(string)); + + // Use DevServerSourceProvider with the source map JSON + auto provider = DevServerSourceProvider::create( + global, + source.toWTFString(), + sourceMapJSON.toWTFString(), + origin, + WTFMove(string), + WTF::TextPosition(), + JSC::SourceProviderSourceType::Program + ); + + JSC::SourceCode sourceCode = JSC::SourceCode(provider); + + JSC::JSValue result = vm.interpreter.executeProgram(sourceCode, global, global); + RETURN_IF_EXCEPTION(scope, {}); + + RELEASE_ASSERT(result); + return JSC::JSValue::encode(result); +} + extern "C" JSC::EncodedJSValue BakeGetModuleNamespace( JSC::JSGlobalObject* global, JSC::JSValue keyValue diff --git a/src/bake/DevServer.zig b/src/bake/DevServer.zig index f71ab7b0fe..c5b3794a22 100644 --- a/src/bake/DevServer.zig +++ b/src/bake/DevServer.zig @@ -844,6 +844,7 @@ fn onJsRequest(dev: *DevServer, req: *Request, resp: AnyResponse) void { arena.allocator(), source_id.kind, dev.allocator, + .client, ) catch bun.outOfMemory(); const response = StaticRoute.initFromAnyBlob(&.fromOwnedSlice(dev.allocator, json_bytes), .{ .server = dev.server, @@ -2239,19 +2240,37 @@ pub fn finalizeBundle( if (!dev.frontend_only and dev.server_graph.current_chunk_len > 0) { // Generate a script_id for server bundles // Use high bit set to distinguish from client bundles, and include generation - // FIXME: Claude, this is highly sus const server_script_id = SourceMapStore.Key.init((1 << 63) | @as(u64, dev.generation)); - // Register the source map for server bundle - switch (try dev.source_maps.putOrIncrementRefCount(server_script_id, 1)) { - .uninitialized => |entry| { - errdefer dev.source_maps.unref(server_script_id); - var arena = std.heap.ArenaAllocator.init(dev.allocator); - defer arena.deinit(); - try dev.server_graph.takeSourceMap(arena.allocator(), dev.allocator, entry); - }, - .shared => {}, - } + // Get the source map if available and render to JSON + const source_map_json = if (dev.server_graph.current_chunk_source_maps.items.len > 0) json: { + // Create a temporary source map entry to render + var source_map_entry = SourceMapStore.Entry{ + .ref_count = 1, + .paths = &.{}, + .files = .empty, + .overlapping_memory_cost = 0, + }; + + // Fill the source map entry + var arena = std.heap.ArenaAllocator.init(dev.allocator); + defer arena.deinit(); + try dev.server_graph.takeSourceMap(arena.allocator(), dev.allocator, &source_map_entry); + defer { + source_map_entry.ref_count = 0; + source_map_entry.deinit(dev); + } + + const json_data = try source_map_entry.renderJSON( + dev, + arena.allocator(), + .hmr_chunk, + dev.allocator, + .server, + ); + break :json json_data; + } else null; + defer if (source_map_json) |json| dev.allocator.free(json); const server_bundle = try dev.server_graph.takeJSBundle(&.{ .kind = .hmr_chunk, @@ -2259,11 +2278,18 @@ pub fn finalizeBundle( }); defer dev.allocator.free(server_bundle); - const server_modules = c.BakeLoadServerHmrPatch(@ptrCast(dev.vm.global), bun.String.cloneLatin1(server_bundle)) catch |err| { - // No user code has been evaluated yet, since everything is to - // be wrapped in a function clousure. This means that the likely - // error is going to be a syntax error, or other mistake in the - // bundler. + const server_modules = if (source_map_json) |json| blk: { + const json_string = bun.String.cloneUTF8(json); + defer json_string.deref(); + break :blk c.BakeLoadServerHmrPatchWithSourceMap(@ptrCast(dev.vm.global), bun.String.cloneLatin1(server_bundle), json_string) catch |err| { + // No user code has been evaluated yet, since everything is to + // be wrapped in a function clousure. This means that the likely + // error is going to be a syntax error, or other mistake in the + // bundler. + dev.vm.printErrorLikeObjectToConsole(dev.vm.global.takeException(err)); + @panic("Error thrown while evaluating server code. This is always a bug in the bundler."); + }; + } else c.BakeLoadServerHmrPatch(@ptrCast(dev.vm.global), bun.String.cloneLatin1(server_bundle)) catch |err| { dev.vm.printErrorLikeObjectToConsole(dev.vm.global.takeException(err)); @panic("Error thrown while evaluating server code. This is always a bug in the bundler."); }; @@ -3573,6 +3599,11 @@ const c = struct { return bun.jsc.fromJSHostCall(global, @src(), f, .{ global, code }); } + fn BakeLoadServerHmrPatchWithSourceMap(global: *jsc.JSGlobalObject, code: bun.String, source_map_json: bun.String) bun.JSError!JSValue { + const f = @extern(*const fn (*jsc.JSGlobalObject, bun.String, bun.String) callconv(.c) JSValue, .{ .name = "BakeLoadServerHmrPatchWithSourceMap" }).*; + return bun.jsc.fromJSHostCall(global, @src(), f, .{ global, code, source_map_json }); + } + fn BakeLoadInitialServerCode(global: *jsc.JSGlobalObject, code: bun.String, separate_ssr_graph: bool) bun.JSError!JSValue { const f = @extern(*const fn (*jsc.JSGlobalObject, bun.String, bool) callconv(.c) JSValue, .{ .name = "BakeLoadInitialServerCode" }).*; return bun.jsc.fromJSHostCall(global, @src(), f, .{ global, code, separate_ssr_graph }); diff --git a/src/bake/DevServer/IncrementalGraph.zig b/src/bake/DevServer/IncrementalGraph.zig index b4b7ed8317..95d4bff854 100644 --- a/src/bake/DevServer/IncrementalGraph.zig +++ b/src/bake/DevServer/IncrementalGraph.zig @@ -1687,14 +1687,11 @@ pub fn IncrementalGraph(side: bake.Side) type { .server => try w.writeAll("})"), }, } - // Append source map URL for both client and server bundles - try w.writeAll("\n//# sourceMappingURL="); - switch (side) { - .client => try w.writeAll(DevServer.client_prefix ++ "/"), - .server => try w.writeAll("bake://server.map/"), + if (side == .client) { + try w.writeAll("\n//# sourceMappingURL=" ++ DevServer.client_prefix ++ "/"); + try w.writeAll(&std.fmt.bytesToHex(std.mem.asBytes(&options.script_id), .lower)); + try w.writeAll(".js.map\n"); } - try w.writeAll(&std.fmt.bytesToHex(std.mem.asBytes(&options.script_id), .lower)); - try w.writeAll(".js.map\n"); break :end end_list.items; }; diff --git a/src/bake/DevServer/SourceMapStore.zig b/src/bake/DevServer/SourceMapStore.zig index 0a8a1cd992..8cbac3c127 100644 --- a/src/bake/DevServer/SourceMapStore.zig +++ b/src/bake/DevServer/SourceMapStore.zig @@ -75,11 +75,11 @@ pub const Entry = struct { pub fn renderMappings(map: Entry, kind: ChunkKind, arena: Allocator, gpa: Allocator) ![]u8 { var j: StringJoiner = .{ .allocator = arena }; j.pushStatic("AAAA"); - try joinVLQ(&map, kind, &j, arena); + try joinVLQ(&map, kind, &j, arena, .client); return j.done(gpa); } - pub fn renderJSON(map: *const Entry, dev: *DevServer, arena: Allocator, kind: ChunkKind, gpa: Allocator) ![]u8 { + pub fn renderJSON(map: *const Entry, dev: *DevServer, arena: Allocator, kind: ChunkKind, gpa: Allocator, side: bake.Side) ![]u8 { const map_files = map.files.slice(); const paths = map.paths; @@ -177,14 +177,14 @@ pub const Entry = struct { j.pushStatic( \\],"names":[],"mappings":"AAAA ); - try joinVLQ(map, kind, &j, arena); + try joinVLQ(map, kind, &j, arena, side); const json_bytes = try j.doneWithEnd(gpa, "\"}"); errdefer @compileError("last try should be the final alloc"); if (bun.FeatureFlags.bake_debugging_features) if (dev.dump_dir) |dump_dir| { - const rel_path_escaped = "latest_chunk.js.map"; - dumpBundle(dump_dir, .client, rel_path_escaped, json_bytes, false) catch |err| { + const rel_path_escaped = if (side == .client) "latest_chunk.js.map" else "latest_hmr.js.map"; + dumpBundle(dump_dir, if (side == .client) .client else .server, rel_path_escaped, json_bytes, false) catch |err| { bun.handleErrorReturnTrace(err, @errorReturnTrace()); Output.warn("Could not dump bundle: {}", .{err}); }; @@ -193,14 +193,9 @@ pub const Entry = struct { return json_bytes; } - fn joinVLQ(map: *const Entry, kind: ChunkKind, j: *StringJoiner, arena: Allocator) !void { + fn joinVLQ(map: *const Entry, kind: ChunkKind, j: *StringJoiner, arena: Allocator, side: bake.Side) !void { const map_files = map.files.slice(); - const runtime: bake.HmrRuntime = switch (kind) { - .initial_response => bun.bake.getHmrRuntime(.client), - .hmr_chunk => comptime .init("self[Symbol.for(\"bun:hmr\")]({\n"), - }; - var prev_end_state: SourceMap.SourceMapState = .{ .generated_line = 0, .generated_column = 0, @@ -209,8 +204,20 @@ 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; + var lines_between: u32 = lines_between: { + if (side == .client) { + const runtime: bake.HmrRuntime = switch (kind) { + .initial_response => bun.bake.getHmrRuntime(.client), + .hmr_chunk => comptime .init("self[Symbol.for(\"bun:hmr\")]({\n"), + }; + // +2 because the magic fairy in my dreams said it would align the source maps. + // TODO: why the fuck is this 2? + const lines_between: u32 = runtime.line_count + 2; + break :lines_between lines_between; + } + + break :lines_between 0; + }; // Join all of the mappings together. for (map_files.items(.tags), map_files.items(.data), 1..) |tag, chunk, source_index| switch (tag) { @@ -226,7 +233,7 @@ pub const Entry = struct { continue; }, .ref => { - const content = chunk.ref.data; + const content: *PackedMap = chunk.ref.data; const start_state: SourceMap.SourceMapState = .{ .source_index = @intCast(source_index), .generated_line = @intCast(lines_between), diff --git a/src/bake/DevServerSourceProvider.cpp b/src/bake/DevServerSourceProvider.cpp new file mode 100644 index 0000000000..2f1c61f7e8 --- /dev/null +++ b/src/bake/DevServerSourceProvider.cpp @@ -0,0 +1,15 @@ +#include "DevServerSourceProvider.h" +#include "BunBuiltinNames.h" +#include "BunString.h" + +// The Zig implementation will be provided to handle registration +extern "C" void Bun__addDevServerSourceProvider(void* bun_vm, Bake::DevServerSourceProvider* opaque_source_provider, BunString* specifier); + +// Export functions for Zig to access DevServerSourceProvider +extern "C" BunString DevServerSourceProvider__getSourceSlice(Bake::DevServerSourceProvider* provider) { + return Bun::toStringView(provider->source()); +} + +extern "C" BunString DevServerSourceProvider__getSourceMapJSON(Bake::DevServerSourceProvider* provider) { + return Bun::toStringView(provider->sourceMapJSON()); +} \ No newline at end of file diff --git a/src/bake/DevServerSourceProvider.h b/src/bake/DevServerSourceProvider.h new file mode 100644 index 0000000000..187010d7fc --- /dev/null +++ b/src/bake/DevServerSourceProvider.h @@ -0,0 +1,58 @@ +#pragma once +#include "root.h" +#include "headers-handwritten.h" +#include "JavaScriptCore/SourceOrigin.h" +#include "ZigGlobalObject.h" + +namespace Bake { + +class DevServerSourceProvider; + +// Function to be implemented in Zig to register the source provider +extern "C" void Bun__addDevServerSourceProvider(void* bun_vm, DevServerSourceProvider* opaque_source_provider, BunString* specifier); + +class DevServerSourceProvider final : public JSC::StringSourceProvider { +public: + static Ref create( + JSC::JSGlobalObject* globalObject, + const String& source, + const String& sourceMapJSON, + const JSC::SourceOrigin& sourceOrigin, + String&& sourceURL, + const TextPosition& startPosition, + JSC::SourceProviderSourceType sourceType) + { + auto provider = adoptRef(*new DevServerSourceProvider(source, sourceMapJSON, sourceOrigin, WTFMove(sourceURL), startPosition, sourceType)); + auto* zigGlobalObject = jsCast<::Zig::GlobalObject*>(globalObject); + auto specifier = Bun::toString(provider->sourceURL()); + Bun__addDevServerSourceProvider(zigGlobalObject->bunVM(), provider.ptr(), &specifier); + return provider; + } + + // TODO: This should be ZigString so we can have a UTF-8 string and not need + // to do conversions + const String& sourceMapJSON() const { return m_sourceMapJSON; } + +private: + DevServerSourceProvider( + const String& source, + const String& sourceMapJSON, + const JSC::SourceOrigin& sourceOrigin, + String&& sourceURL, + const TextPosition& startPosition, + JSC::SourceProviderSourceType sourceType) + : StringSourceProvider( + source, + sourceOrigin, + JSC::SourceTaintedOrigin::Untainted, + WTFMove(sourceURL), + startPosition, + sourceType) + , m_sourceMapJSON(sourceMapJSON) + { + } + + String m_sourceMapJSON; +}; + +} // namespace Bake diff --git a/src/bun.js/SavedSourceMap.zig b/src/bun.js/SavedSourceMap.zig index 68fcdb75e9..86bd34fe9c 100644 --- a/src/bun.js/SavedSourceMap.zig +++ b/src/bun.js/SavedSourceMap.zig @@ -88,6 +88,7 @@ pub const Value = bun.TaggedPointerUnion(.{ SavedMappings, SourceProviderMap, BakeSourceProvider, + DevServerSourceProvider, }); pub const MissingSourceMapNoteInfo = struct { @@ -108,6 +109,10 @@ pub fn putBakeSourceProvider(this: *SavedSourceMap, opaque_source_provider: *Bak this.putValue(path, Value.init(opaque_source_provider)) catch bun.outOfMemory(); } +pub fn putDevServerSourceProvider(this: *SavedSourceMap, opaque_source_provider: *DevServerSourceProvider, 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(); @@ -279,6 +284,33 @@ fn getWithContent( MissingSourceMapNoteInfo.path = storage; return .{}; }, + @field(Value.Tag, @typeName(DevServerSourceProvider)) => { + // TODO: This is a copy-paste of above branch + const ptr: *DevServerSourceProvider = Value.from(mapping.value_ptr.*).as(DevServerSourceProvider); + 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"); @@ -333,5 +365,6 @@ const logger = bun.logger; const SourceMap = bun.sourcemap; const BakeSourceProvider = bun.sourcemap.BakeSourceProvider; +const DevServerSourceProvider = bun.sourcemap.DevServerSourceProvider; const ParsedSourceMap = SourceMap.ParsedSourceMap; const SourceProviderMap = SourceMap.SourceProviderMap; diff --git a/src/bun.js/virtual_machine_exports.zig b/src/bun.js/virtual_machine_exports.zig index 41fa44cafc..9873b029ff 100644 --- a/src/bun.js/virtual_machine_exports.zig +++ b/src/bun.js/virtual_machine_exports.zig @@ -179,6 +179,13 @@ export fn Bun__addBakeSourceProviderSourceMap(vm: *VirtualMachine, opaque_source vm.source_mappings.putBakeSourceProvider(@as(*BakeSourceProvider, @ptrCast(opaque_source_provider)), slice.slice()); } +export fn Bun__addDevServerSourceProvider(vm: *VirtualMachine, opaque_source_provider: *anyopaque, specifier: *bun.String) void { + var sfb = std.heap.stackFallback(4096, bun.default_allocator); + const slice = specifier.toUTF8(sfb.get()); + defer slice.deinit(); + vm.source_mappings.putDevServerSourceProvider(@as(*DevServerSourceProvider, @ptrCast(opaque_source_provider)), slice.slice()); +} + export fn Bun__addSourceProviderSourceMap(vm: *VirtualMachine, opaque_source_provider: *anyopaque, specifier: *bun.String) void { var sfb = std.heap.stackFallback(4096, bun.default_allocator); const slice = specifier.toUTF8(sfb.get()); @@ -215,6 +222,7 @@ const std = @import("std"); const bun = @import("bun"); const BakeSourceProvider = bun.sourcemap.BakeSourceProvider; +const DevServerSourceProvider = bun.sourcemap.DevServerSourceProvider; const PluginRunner = bun.transpiler.PluginRunner; const jsc = bun.jsc; diff --git a/src/sourcemap/sourcemap.zig b/src/sourcemap/sourcemap.zig index 15cb72feb5..34685524bc 100644 --- a/src/sourcemap/sourcemap.zig +++ b/src/sourcemap/sourcemap.zig @@ -887,15 +887,17 @@ pub const ParsedSourceMap = struct { is_standalone_module_graph: bool = false, - const SourceProviderKind = enum(u1) { zig, bake }; + const SourceProviderKind = enum(u2) { zig, bake, dev_server }; const AnySourceProvider = union(enum) { zig: *SourceProviderMap, bake: *BakeSourceProvider, + dev_server: *DevServerSourceProvider, pub fn ptr(this: AnySourceProvider) *anyopaque { return switch (this) { .zig => @ptrCast(this.zig), .bake => @ptrCast(this.bake), + .dev_server => @ptrCast(this.dev_server), }; } @@ -908,6 +910,7 @@ pub const ParsedSourceMap = struct { return switch (this) { .zig => this.zig.getSourceMap(source_filename, load_hint, result), .bake => this.bake.getSourceMap(source_filename, load_hint, result), + .dev_server => this.dev_server.getSourceMap(source_filename, load_hint, result), }; } }; @@ -915,7 +918,7 @@ pub const ParsedSourceMap = struct { const SourceContentPtr = packed struct(u64) { load_hint: SourceMapLoadHint, kind: SourceProviderKind, - data: u61, + data: u60, pub const none: SourceContentPtr = .{ .load_hint = .none, .kind = .zig, .data = 0 }; @@ -927,10 +930,15 @@ pub const ParsedSourceMap = struct { return .{ .load_hint = .none, .data = @intCast(@intFromPtr(p)), .kind = .bake }; } + fn fromDevServerProvider(p: *DevServerSourceProvider) SourceContentPtr { + return .{ .load_hint = .none, .data = @intCast(@intFromPtr(p)), .kind = .dev_server }; + } + pub fn provider(sc: SourceContentPtr) ?AnySourceProvider { switch (sc.kind) { .zig => return .{ .zig = @ptrFromInt(sc.data) }, .bake => return .{ .bake = @ptrFromInt(sc.data) }, + .dev_server => return .{ .dev_server = @ptrFromInt(sc.data) }, } } }; @@ -1282,6 +1290,27 @@ pub fn getSourceMapImpl( // try to load a .map file if (load_hint != .is_inline_map) try_external: { + if (comptime SourceProviderKind == DevServerSourceProvider) { + // For DevServerSourceProvider, get the source map JSON directly + const json_string = provider.getSourceMapJSON(); + defer json_string.deref(); + + // Convert to UTF-8 slice + const json_data = json_string.toUTF8(allocator); + defer json_data.deinit(); + + // Parse the JSON source map + break :parsed .{ + .is_external_map, + parseJSON( + bun.default_allocator, + allocator, + json_data.slice(), + result, + ) catch return null, + }; + } + if (comptime SourceProviderKind == BakeSourceProvider) fallback_to_normal: { const global = bun.jsc.VirtualMachine.get().global; // If we're using bake's production build the global object will @@ -1435,6 +1464,34 @@ pub const BakeSourceProvider = opaque { } }; +pub const DevServerSourceProvider = opaque { + extern fn DevServerSourceProvider__getSourceSlice(*DevServerSourceProvider) bun.String; + extern fn DevServerSourceProvider__getSourceMapJSON(*DevServerSourceProvider) bun.String; + + pub const getSourceSlice = DevServerSourceProvider__getSourceSlice; + pub const getSourceMapJSON = DevServerSourceProvider__getSourceMapJSON; + + pub fn toSourceContentPtr(this: *DevServerSourceProvider) ParsedSourceMap.SourceContentPtr { + return ParsedSourceMap.SourceContentPtr.fromDevServerProvider(this); + } + + /// The last two arguments to this specify loading hints + pub fn getSourceMap( + provider: *DevServerSourceProvider, + source_filename: []const u8, + load_hint: SourceMap.SourceMapLoadHint, + result: SourceMap.ParseUrlResultHint, + ) ?SourceMap.ParseUrl { + return getSourceMapImpl( + DevServerSourceProvider, + provider, + source_filename, + load_hint, + result, + ); + } +}; + pub const LineColumnOffset = struct { lines: i32 = 0, columns: i32 = 0,