diff --git a/src/bake/BakeSourceProvider.cpp b/src/bake/BakeSourceProvider.cpp index d2111891ab..8efa3d8d34 100644 --- a/src/bake/BakeSourceProvider.cpp +++ b/src/bake/BakeSourceProvider.cpp @@ -79,7 +79,7 @@ extern "C" JSC::EncodedJSValue BakeLoadServerHmrPatch(GlobalObject* global, BunS return JSC::JSValue::encode(result); } -extern "C" JSC::EncodedJSValue BakeLoadServerHmrPatchWithSourceMap(GlobalObject* global, BunString source, BunString sourceMapJSON) { +extern "C" JSC::EncodedJSValue BakeLoadServerHmrPatchWithSourceMap(GlobalObject* global, BunString source, const char* sourceMapJSONPtr, size_t sourceMapJSONLength) { JSC::VM&vm = global->vm(); auto scope = DECLARE_THROW_SCOPE(vm); @@ -90,7 +90,8 @@ extern "C" JSC::EncodedJSValue BakeLoadServerHmrPatchWithSourceMap(GlobalObject* auto provider = DevServerSourceProvider::create( global, source.toWTFString(), - sourceMapJSON.toWTFString(), + sourceMapJSONPtr, + sourceMapJSONLength, origin, WTFMove(string), WTF::TextPosition(), diff --git a/src/bake/DevServer.zig b/src/bake/DevServer.zig index c5b3794a22..6d713edabe 100644 --- a/src/bake/DevServer.zig +++ b/src/bake/DevServer.zig @@ -2243,7 +2243,7 @@ pub fn finalizeBundle( const server_script_id = SourceMapStore.Key.init((1 << 63) | @as(u64, dev.generation)); // 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: { + var 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, @@ -2270,7 +2270,7 @@ pub fn finalizeBundle( ); break :json json_data; } else null; - defer if (source_map_json) |json| dev.allocator.free(json); + defer if (source_map_json) |json| bun.default_allocator.free(json); const server_bundle = try dev.server_graph.takeJSBundle(&.{ .kind = .hmr_chunk, @@ -2278,10 +2278,13 @@ pub fn finalizeBundle( }); defer dev.allocator.free(server_bundle); - 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| { + const server_modules = if (bun.take(&source_map_json)) |json| blk: { + break :blk c.BakeLoadServerHmrPatchWithSourceMap( + @ptrCast(dev.vm.global), + bun.String.cloneLatin1(server_bundle), + json.ptr, + json.len, + ) 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 @@ -3599,9 +3602,9 @@ 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 BakeLoadServerHmrPatchWithSourceMap(global: *jsc.JSGlobalObject, code: bun.String, source_map_json_ptr: [*]const u8, source_map_json_len: usize) bun.JSError!JSValue { + const f = @extern(*const fn (*jsc.JSGlobalObject, bun.String, [*]const u8, usize) callconv(.c) JSValue, .{ .name = "BakeLoadServerHmrPatchWithSourceMap" }).*; + return bun.jsc.fromJSHostCall(global, @src(), f, .{ global, code, source_map_json_ptr, source_map_json_len }); } fn BakeLoadInitialServerCode(global: *jsc.JSGlobalObject, code: bun.String, separate_ssr_graph: bool) bun.JSError!JSValue { diff --git a/src/bake/DevServer/SourceMapStore.zig b/src/bake/DevServer/SourceMapStore.zig index 8cbac3c127..32229688dd 100644 --- a/src/bake/DevServer/SourceMapStore.zig +++ b/src/bake/DevServer/SourceMapStore.zig @@ -108,13 +108,21 @@ pub const Entry = struct { if (std.fs.path.isAbsolute(path)) { const is_windows_drive_path = Environment.isWindows and path[0] != '/'; - try source_map_strings.appendSlice(if (is_windows_drive_path) - "\"file:///" - else - "\"file://"); + + // On the client we prefix the sourcemap path with "file://" and + // percent encode it + if (side == .client) { + try source_map_strings.appendSlice(if (is_windows_drive_path) + "\"file:///" + else + "\"file://"); + } else { + try source_map_strings.append('"'); + } + if (Environment.isWindows and !is_windows_drive_path) { // UNC namespace -> file://server/share/path.ext - bun.strings.percentEncodeWrite( + encodeSourceMapPath( if (path.len > 2 and path[0] == '/' and path[1] == '/') path[2..] else @@ -129,7 +137,7 @@ pub const Entry = struct { // -> file:///path/to/file.js // windows drive letter paths have the extra slash added // -> file:///C:/path/to/file.js - bun.strings.percentEncodeWrite(path, &source_map_strings) catch |err| switch (err) { + encodeSourceMapPath(side, path, &source_map_strings) catch |err| switch (err) { error.IncompleteUTF8 => @panic("Unexpected: asset with incomplete UTF-8 as file path"), error.OutOfMemory => |e| return e, }; @@ -193,6 +201,20 @@ pub const Entry = struct { return json_bytes; } + fn encodeSourceMapPath( + side: bake.Side, + utf8_input: []const u8, + writer: *std.ArrayList(u8), + ) error{ OutOfMemory, IncompleteUTF8 }!void { + // On the client, percent encode everything so it works in the browser + if (side == .client) { + return bun.strings.percentEncodeWrite(utf8_input, writer); + } + + // On the server, we don't need to do anything + try writer.appendSlice(utf8_input); + } + fn joinVLQ(map: *const Entry, kind: ChunkKind, j: *StringJoiner, arena: Allocator, side: bake.Side) !void { const map_files = map.files.slice(); diff --git a/src/bake/DevServerSourceProvider.cpp b/src/bake/DevServerSourceProvider.cpp index 2f1c61f7e8..3667664384 100644 --- a/src/bake/DevServerSourceProvider.cpp +++ b/src/bake/DevServerSourceProvider.cpp @@ -6,10 +6,12 @@ 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) { +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 +extern "C" Bake::SourceMapData DevServerSourceProvider__getSourceMapJSON(Bake::DevServerSourceProvider* provider) +{ + return provider->sourceMapJSON(); +} diff --git a/src/bake/DevServerSourceProvider.h b/src/bake/DevServerSourceProvider.h index 187010d7fc..6d1bd47f1d 100644 --- a/src/bake/DevServerSourceProvider.h +++ b/src/bake/DevServerSourceProvider.h @@ -3,11 +3,68 @@ #include "headers-handwritten.h" #include "JavaScriptCore/SourceOrigin.h" #include "ZigGlobalObject.h" +#include namespace Bake { class DevServerSourceProvider; +class SourceMapJSONString { +public: + SourceMapJSONString(const char* ptr, size_t length) + : m_ptr(ptr) + , m_length(length) + { + } + + ~SourceMapJSONString() + { + if (m_ptr) { + mi_free(const_cast(m_ptr)); + } + } + + // Delete copy constructor and assignment operator to prevent double free + SourceMapJSONString(const SourceMapJSONString&) = delete; + SourceMapJSONString& operator=(const SourceMapJSONString&) = delete; + + // Move constructor and assignment + SourceMapJSONString(SourceMapJSONString&& other) noexcept + : m_ptr(other.m_ptr) + , m_length(other.m_length) + { + other.m_ptr = nullptr; + other.m_length = 0; + } + + SourceMapJSONString& operator=(SourceMapJSONString&& other) noexcept + { + if (this != &other) { + if (m_ptr) { + mi_free(const_cast(m_ptr)); + } + m_ptr = other.m_ptr; + m_length = other.m_length; + other.m_ptr = nullptr; + other.m_length = 0; + } + return *this; + } + + const char* ptr() const { return m_ptr; } + size_t length() const { return m_length; } + +private: + const char* m_ptr; + size_t m_length; +}; + +// Struct to return source map data to Zig +struct SourceMapData { + const char* ptr; + size_t length; +}; + // 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); @@ -16,27 +73,30 @@ public: static Ref create( JSC::JSGlobalObject* globalObject, const String& source, - const String& sourceMapJSON, + const char* sourceMapJSONPtr, + size_t sourceMapJSONLength, 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 provider = adoptRef(*new DevServerSourceProvider(source, sourceMapJSONPtr, sourceMapJSONLength, 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; } + SourceMapData sourceMapJSON() const + { + return SourceMapData { m_sourceMapJSON.ptr(), m_sourceMapJSON.length() }; + } private: DevServerSourceProvider( const String& source, - const String& sourceMapJSON, + const char* sourceMapJSONPtr, + size_t sourceMapJSONLength, const JSC::SourceOrigin& sourceOrigin, String&& sourceURL, const TextPosition& startPosition, @@ -48,11 +108,11 @@ private: WTFMove(sourceURL), startPosition, sourceType) - , m_sourceMapJSON(sourceMapJSON) + , m_sourceMapJSON(sourceMapJSONPtr, sourceMapJSONLength) { } - String m_sourceMapJSON; + SourceMapJSONString m_sourceMapJSON; }; } // namespace Bake diff --git a/src/sourcemap/sourcemap.zig b/src/sourcemap/sourcemap.zig index 34685524bc..a26481b63b 100644 --- a/src/sourcemap/sourcemap.zig +++ b/src/sourcemap/sourcemap.zig @@ -1292,25 +1292,26 @@ pub fn getSourceMapImpl( 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(); - + const source_map_data = provider.getSourceMapJSON(); + + if (source_map_data.length == 0) { + break :try_external; + } + + const json_slice = source_map_data.ptr[0..source_map_data.length]; + // Parse the JSON source map break :parsed .{ .is_external_map, parseJSON( bun.default_allocator, allocator, - json_data.slice(), + json_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 @@ -1465,12 +1466,17 @@ pub const BakeSourceProvider = opaque { }; pub const DevServerSourceProvider = opaque { + pub const SourceMapData = extern struct { + ptr: [*]const u8, + length: usize, + }; + extern fn DevServerSourceProvider__getSourceSlice(*DevServerSourceProvider) bun.String; - extern fn DevServerSourceProvider__getSourceMapJSON(*DevServerSourceProvider) bun.String; - + extern fn DevServerSourceProvider__getSourceMapJSON(*DevServerSourceProvider) SourceMapData; + pub const getSourceSlice = DevServerSourceProvider__getSourceSlice; pub const getSourceMapJSON = DevServerSourceProvider__getSourceMapJSON; - + pub fn toSourceContentPtr(this: *DevServerSourceProvider) ParsedSourceMap.SourceContentPtr { return ParsedSourceMap.SourceContentPtr.fromDevServerProvider(this); }