diff --git a/bench/snippets/source-map.js b/bench/snippets/source-map.js new file mode 100644 index 0000000000..0d41bebd41 --- /dev/null +++ b/bench/snippets/source-map.js @@ -0,0 +1,28 @@ +import { SourceMap } from "node:module"; +import { readFileSync } from "node:fs"; +import { bench, run } from "../runner.mjs"; +const json = JSON.parse(readFileSync(process.argv.at(-1), "utf-8")); + +bench("new SourceMap(json)", () => { + return new SourceMap(json); +}); + +const map = new SourceMap(json); + +const toRotate = []; +for (let j = 0; j < 10000; j++) { + if (map.findEntry(0, j).generatedColumn) { + toRotate.push(j); + if (toRotate.length > 5) break; + } +} +let i = 0; +bench("findEntry (match)", () => { + return map.findEntry(0, toRotate[i++ % 3]).generatedColumn; +}); + +bench("findEntry (no match)", () => { + return map.findEntry(0, 9999).generatedColumn; +}); + +await run(); diff --git a/cmake/sources/ZigGeneratedClassesSources.txt b/cmake/sources/ZigGeneratedClassesSources.txt index cc657aebeb..116f1cc26d 100644 --- a/cmake/sources/ZigGeneratedClassesSources.txt +++ b/cmake/sources/ZigGeneratedClassesSources.txt @@ -14,6 +14,7 @@ src/bun.js/api/server.classes.ts src/bun.js/api/Shell.classes.ts src/bun.js/api/ShellArgs.classes.ts src/bun.js/api/sockets.classes.ts +src/bun.js/api/sourcemap.classes.ts src/bun.js/api/streams.classes.ts src/bun.js/api/valkey.classes.ts src/bun.js/api/zlib.classes.ts diff --git a/cmake/sources/ZigSources.txt b/cmake/sources/ZigSources.txt index 08d07b7249..1060e7fac6 100644 --- a/cmake/sources/ZigSources.txt +++ b/cmake/sources/ZigSources.txt @@ -729,6 +729,7 @@ src/shell/subproc.zig src/shell/util.zig src/shell/Yield.zig src/sourcemap/CodeCoverage.zig +src/sourcemap/JSSourceMap.zig src/sourcemap/LineOffsetTable.zig src/sourcemap/sourcemap.zig src/sourcemap/VLQ.zig diff --git a/src/StandaloneModuleGraph.zig b/src/StandaloneModuleGraph.zig index 478a6b563e..fc5530aabc 100644 --- a/src/StandaloneModuleGraph.zig +++ b/src/StandaloneModuleGraph.zig @@ -237,6 +237,7 @@ pub const StandaloneModuleGraph = struct { null, std.math.maxInt(i32), std.math.maxInt(i32), + .{}, )) { .success => |x| x, .fail => { diff --git a/src/baby_list.zig b/src/baby_list.zig index f7a5272e5a..539c548a47 100644 --- a/src/baby_list.zig +++ b/src/baby_list.zig @@ -417,6 +417,10 @@ pub fn BabyList(comptime Type: type) type { @as([*]align(1) Int, @ptrCast(this.ptr[this.len .. this.len + @sizeOf(Int)]))[0] = int; this.len += @sizeOf(Int); } + + pub fn memoryCost(self: *const @This()) usize { + return self.cap; + } }; } diff --git a/src/bake/DevServer.zig b/src/bake/DevServer.zig index 15a39c7b8a..9bdc249913 100644 --- a/src/bake/DevServer.zig +++ b/src/bake/DevServer.zig @@ -8040,6 +8040,7 @@ pub const SourceMapStore = struct { null, @intCast(entry.paths.len), 0, // unused + .{}, )) { .fail => |fail| { Output.debugWarn("Failed to re-parse source map: {s}", .{fail.msg}); @@ -8199,7 +8200,7 @@ const ErrorReportRequest = struct { const result: *const SourceMapStore.GetResult = &(gop.value_ptr.* orelse continue); // When before the first generated line, remap to the HMR runtime - const generated_mappings = result.mappings.items(.generated); + const generated_mappings = result.mappings.generated(); if (frame.position.line.oneBased() < generated_mappings[1].lines) { frame.source_url = .init(runtime_name); // matches value in source map frame.position = .invalid; @@ -8207,8 +8208,7 @@ const ErrorReportRequest = struct { } // Remap the frame - const remapped = SourceMap.Mapping.find( - result.mappings, + const remapped = result.mappings.find( frame.position.line.oneBased(), frame.position.column.zeroBased(), ); diff --git a/src/bun.js/SavedSourceMap.zig b/src/bun.js/SavedSourceMap.zig index 2784c08369..01e6286785 100644 --- a/src/bun.js/SavedSourceMap.zig +++ b/src/bun.js/SavedSourceMap.zig @@ -50,6 +50,7 @@ pub const SavedMappings = struct { @as(usize, @bitCast(this.data[8..16].*)), 1, @as(usize, @bitCast(this.data[16..24].*)), + .{}, ); switch (result) { .fail => |fail| { @@ -310,7 +311,7 @@ pub fn resolveMapping( const map = parse.map orelse return null; const mapping = parse.mapping orelse - SourceMap.Mapping.find(map.mappings, line, column) orelse + map.mappings.find(line, column) orelse return null; return .{ diff --git a/src/bun.js/VirtualMachine.zig b/src/bun.js/VirtualMachine.zig index a28633e557..adad143990 100644 --- a/src/bun.js/VirtualMachine.zig +++ b/src/bun.js/VirtualMachine.zig @@ -3408,7 +3408,7 @@ pub fn resolveSourceMapping( this.source_mappings.putValue(path, SavedSourceMap.Value.init(map)) catch bun.outOfMemory(); - const mapping = SourceMap.Mapping.find(map.mappings, line, column) orelse + const mapping = map.mappings.find(line, column) orelse return null; return .{ diff --git a/src/bun.js/api/sourcemap.classes.ts b/src/bun.js/api/sourcemap.classes.ts new file mode 100644 index 0000000000..9a4ebd5201 --- /dev/null +++ b/src/bun.js/api/sourcemap.classes.ts @@ -0,0 +1,32 @@ +import { define } from "../../codegen/class-definitions"; + +export default [ + define({ + name: "SourceMap", + JSType: "0b11101110", + proto: { + findOrigin: { + fn: "findOrigin", + length: 2, + }, + findEntry: { + fn: "findEntry", + length: 2, + }, + payload: { + getter: "getPayload", + cache: true, + }, + lineLengths: { + getter: "getLineLengths", + cache: true, + }, + }, + finalize: true, + construct: true, + constructNeedsThis: true, + memoryCost: true, + estimatedSize: true, + structuredClone: false, + }), +]; diff --git a/src/bun.js/bindings/ZigGlobalObject.h b/src/bun.js/bindings/ZigGlobalObject.h index 9420108724..b80fefee49 100644 --- a/src/bun.js/bindings/ZigGlobalObject.h +++ b/src/bun.js/bindings/ZigGlobalObject.h @@ -462,6 +462,8 @@ public: V(public, LazyPropertyOfGlobalObject, m_modulePrototypeUnderscoreCompileFunction) \ V(public, LazyPropertyOfGlobalObject, m_commonJSRequireESMFromHijackedExtensionFunction) \ V(public, LazyPropertyOfGlobalObject, m_nodeModuleConstructor) \ + V(public, LazyPropertyOfGlobalObject, m_nodeModuleSourceMapEntryStructure) \ + V(public, LazyPropertyOfGlobalObject, m_nodeModuleSourceMapOriginStructure) \ \ V(public, WriteBarrier, m_nextTickQueue) \ \ diff --git a/src/bun.js/bindings/generated_classes_list.zig b/src/bun.js/bindings/generated_classes_list.zig index 904da43346..bfaf43245b 100644 --- a/src/bun.js/bindings/generated_classes_list.zig +++ b/src/bun.js/bindings/generated_classes_list.zig @@ -89,4 +89,5 @@ pub const Classes = struct { pub const RedisClient = api.Valkey; pub const BlockList = api.BlockList; pub const NativeZstd = api.NativeZstd; + pub const SourceMap = bun.sourcemap.JSSourceMap; }; diff --git a/src/bun.js/modules/NodeModuleModule.cpp b/src/bun.js/modules/NodeModuleModule.cpp index e26286c65e..ffafec3306 100644 --- a/src/bun.js/modules/NodeModuleModule.cpp +++ b/src/bun.js/modules/NodeModuleModule.cpp @@ -20,21 +20,22 @@ #include "ErrorCode.h" #include "GeneratedNodeModuleModule.h" +#include "ZigGeneratedClasses.h" namespace Bun { using namespace JSC; +BUN_DECLARE_HOST_FUNCTION(Bun__JSSourceMap__find); + BUN_DECLARE_HOST_FUNCTION(Resolver__nodeModulePathsForJS); JSC_DECLARE_HOST_FUNCTION(jsFunctionDebugNoop); JSC_DECLARE_HOST_FUNCTION(jsFunctionFindPath); -JSC_DECLARE_HOST_FUNCTION(jsFunctionFindSourceMap); JSC_DECLARE_HOST_FUNCTION(jsFunctionIsBuiltinModule); JSC_DECLARE_HOST_FUNCTION(jsFunctionNodeModuleCreateRequire); JSC_DECLARE_HOST_FUNCTION(jsFunctionNodeModuleModuleConstructor); JSC_DECLARE_HOST_FUNCTION(jsFunctionResolveFileName); JSC_DECLARE_HOST_FUNCTION(jsFunctionResolveLookupPaths); -JSC_DECLARE_HOST_FUNCTION(jsFunctionSourceMap); JSC_DECLARE_HOST_FUNCTION(jsFunctionSyncBuiltinExports); JSC_DECLARE_HOST_FUNCTION(jsFunctionWrap); @@ -287,13 +288,6 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionNodeModuleCreateRequire, scope, JSValue::encode(Bun::JSCommonJSModule::createBoundRequireFunction(vm, globalObject, val))); } -JSC_DEFINE_HOST_FUNCTION(jsFunctionFindSourceMap, - (JSGlobalObject * globalObject, - CallFrame* callFrame)) -{ - return JSValue::encode(jsUndefined()); -} - JSC_DEFINE_HOST_FUNCTION(jsFunctionSyncBuiltinExports, (JSGlobalObject * globalObject, CallFrame* callFrame)) @@ -301,15 +295,6 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionSyncBuiltinExports, return JSValue::encode(jsUndefined()); } -JSC_DEFINE_HOST_FUNCTION(jsFunctionSourceMap, (JSGlobalObject * globalObject, CallFrame* callFrame)) -{ - auto& vm = JSC::getVM(globalObject); - auto scope = DECLARE_THROW_SCOPE(vm); - throwException(globalObject, scope, - createError(globalObject, "Not implemented"_s)); - return {}; -} - JSC_DEFINE_HOST_FUNCTION(jsFunctionResolveFileName, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) @@ -585,10 +570,10 @@ static JSValue getPathCacheObject(VM& vm, JSObject* moduleObject) static JSValue getSourceMapFunction(VM& vm, JSObject* moduleObject) { auto* globalObject = defaultGlobalObject(moduleObject->globalObject()); - JSFunction* sourceMapFunction = JSFunction::create( - vm, globalObject, 1, "SourceMap"_s, jsFunctionSourceMap, - ImplementationVisibility::Public, NoIntrinsic, jsFunctionSourceMap); - return sourceMapFunction; + auto* zigGlobalObject = jsCast(globalObject); + + // Return the actual SourceMap constructor from code generation + return zigGlobalObject->JSSourceMapConstructor(); } static JSValue getBuiltinModulesObject(VM& vm, JSObject* moduleObject) @@ -847,7 +832,7 @@ builtinModules getBuiltinModulesObject PropertyCallback constants getConstantsObject PropertyCallback createRequire jsFunctionNodeModuleCreateRequire Function 1 enableCompileCache jsFunctionEnableCompileCache Function 0 -findSourceMap jsFunctionFindSourceMap Function 0 +findSourceMap Bun__JSSourceMap__find Function 1 getCompileCacheDir jsFunctionGetCompileCacheDir Function 0 globalPaths getGlobalPathsObject PropertyCallback isBuiltin jsFunctionIsBuiltinModule Function 1 @@ -918,6 +903,82 @@ const JSC::ClassInfo JSModuleConstructor::s_info = { CREATE_METHOD_TABLE(JSModuleConstructor) }; +static JSC::Structure* createNodeModuleSourceMapEntryStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject) +{ + Structure* structure = globalObject->structureCache().emptyObjectStructureForPrototype(globalObject, globalObject->objectPrototype(), 6); + PropertyOffset offset; + + structure = Structure::addPropertyTransition(vm, structure, Identifier::fromString(vm, "generatedLine"), 0, offset); + RELEASE_ASSERT(offset == 0); + structure = Structure::addPropertyTransition(vm, structure, Identifier::fromString(vm, "generatedColumn"), 0, offset); + RELEASE_ASSERT(offset == 1); + structure = Structure::addPropertyTransition(vm, structure, Identifier::fromString(vm, "originalLine"), 0, offset); + RELEASE_ASSERT(offset == 2); + structure = Structure::addPropertyTransition(vm, structure, Identifier::fromString(vm, "originalColumn"), 0, offset); + RELEASE_ASSERT(offset == 3); + structure = Structure::addPropertyTransition(vm, structure, Identifier::fromString(vm, "originalSource"), 0, offset); + RELEASE_ASSERT(offset == 4); + structure = Structure::addPropertyTransition(vm, structure, vm.propertyNames->name, 0, offset); + RELEASE_ASSERT(offset == 5); + + return structure; +} + +extern "C" JSC::EncodedJSValue Bun__createNodeModuleSourceMapEntryObject( + JSC::JSGlobalObject* globalObject, + JSC::EncodedJSValue encodedGeneratedLine, + JSC::EncodedJSValue encodedGeneratedColumn, + JSC::EncodedJSValue encodedOriginalLine, + JSC::EncodedJSValue encodedOriginalColumn, + JSC::EncodedJSValue encodedOriginalSource, + JSC::EncodedJSValue encodedName) +{ + auto& vm = globalObject->vm(); + auto* zigGlobalObject = defaultGlobalObject(globalObject); + JSObject* object = JSC::constructEmptyObject(vm, zigGlobalObject->m_nodeModuleSourceMapEntryStructure.getInitializedOnMainThread(zigGlobalObject)); + object->putDirectOffset(vm, 0, JSC::JSValue::decode(encodedGeneratedLine)); + object->putDirectOffset(vm, 1, JSC::JSValue::decode(encodedGeneratedColumn)); + object->putDirectOffset(vm, 2, JSC::JSValue::decode(encodedOriginalLine)); + object->putDirectOffset(vm, 3, JSC::JSValue::decode(encodedOriginalColumn)); + object->putDirectOffset(vm, 4, JSC::JSValue::decode(encodedOriginalSource)); + object->putDirectOffset(vm, 5, JSC::JSValue::decode(encodedName)); + return JSValue::encode(object); +} + +static JSC::Structure* createNodeModuleSourceMapOriginStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject) +{ + Structure* structure = globalObject->structureCache().emptyObjectStructureForPrototype(globalObject, globalObject->objectPrototype(), 4); + PropertyOffset offset; + + structure = Structure::addPropertyTransition(vm, structure, vm.propertyNames->name, 0, offset); + RELEASE_ASSERT(offset == 0); + structure = Structure::addPropertyTransition(vm, structure, Identifier::fromString(vm, "line"), 0, offset); + RELEASE_ASSERT(offset == 1); + structure = Structure::addPropertyTransition(vm, structure, Identifier::fromString(vm, "column"), 0, offset); + RELEASE_ASSERT(offset == 2); + structure = Structure::addPropertyTransition(vm, structure, Identifier::fromString(vm, "fileName"), 0, offset); + RELEASE_ASSERT(offset == 3); + + return structure; +} + +extern "C" JSC::EncodedJSValue Bun__createNodeModuleSourceMapOriginObject( + JSC::JSGlobalObject* globalObject, + JSC::EncodedJSValue encodedName, + JSC::EncodedJSValue encodedLine, + JSC::EncodedJSValue encodedColumn, + JSC::EncodedJSValue encodedSource) +{ + auto& vm = globalObject->vm(); + auto* zigGlobalObject = defaultGlobalObject(globalObject); + JSObject* object = JSC::constructEmptyObject(vm, zigGlobalObject->m_nodeModuleSourceMapOriginStructure.getInitializedOnMainThread(zigGlobalObject)); + object->putDirectOffset(vm, 0, JSC::JSValue::decode(encodedName)); + object->putDirectOffset(vm, 1, JSC::JSValue::decode(encodedLine)); + object->putDirectOffset(vm, 2, JSC::JSValue::decode(encodedColumn)); + object->putDirectOffset(vm, 3, JSC::JSValue::decode(encodedSource)); + return JSValue::encode(object); +} + void addNodeModuleConstructorProperties(JSC::VM& vm, Zig::GlobalObject* globalObject) { @@ -928,6 +989,15 @@ void addNodeModuleConstructorProperties(JSC::VM& vm, init.set(moduleConstructor); }); + globalObject->m_nodeModuleSourceMapEntryStructure.initLater( + [](const Zig::GlobalObject::Initializer& init) { + init.set(createNodeModuleSourceMapEntryStructure(init.vm, init.owner)); + }); + globalObject->m_nodeModuleSourceMapOriginStructure.initLater( + [](const Zig::GlobalObject::Initializer& init) { + init.set(createNodeModuleSourceMapOriginStructure(init.vm, init.owner)); + }); + globalObject->m_moduleRunMainFunction.initLater( [](const Zig::GlobalObject::Initializer& init) { JSFunction* runMainFunction = JSFunction::create( diff --git a/src/codegen/class-definitions.ts b/src/codegen/class-definitions.ts index a4736ee949..ab123f9d5f 100644 --- a/src/codegen/class-definitions.ts +++ b/src/codegen/class-definitions.ts @@ -92,9 +92,21 @@ export class ClassDefinition { */ name: string; /** - * Class constructor is newable. + * Class constructor is newable. Called before the JSValue corresponding to + * the object is created. Throwing an exception prevents the object from being + * created. */ construct?: boolean; + + /** + * Class constructor needs `this` value. + * + * Makes the code generator call the Zig constructor function **after** the + * JSValue is instantiated. Only use this if you must, as it probably isn't + * good for GC since it means if the constructor throws the GC will have to + * clean up the object that never reached JS. + */ + constructNeedsThis?: boolean; /** * Class constructor is callable. In JS, ES6 class constructors are not * callable. @@ -168,10 +180,6 @@ export class ClassDefinition { final?: boolean; - // Do not try to track the `this` value in the constructor automatically. - // That is a memory leak. - wantsThis?: never; - /** * Class has an `estimatedSize` function that reports external allocations to GC. * Called from any thread. diff --git a/src/codegen/generate-classes.ts b/src/codegen/generate-classes.ts index 1399a5cd46..08b824b674 100644 --- a/src/codegen/generate-classes.ts +++ b/src/codegen/generate-classes.ts @@ -406,10 +406,17 @@ function generatePrototype(typeName, obj) { var staticPrototypeValues = ""; if (obj.construct) { - externs += ` + if (obj.constructNeedsThis) { + externs += ` +extern JSC_CALLCONV void* JSC_HOST_CALL_ATTRIBUTES ${classSymbolName(typeName, "construct")}(JSC::JSGlobalObject*, JSC::CallFrame*, JSC::EncodedJSValue); +JSC_DECLARE_CUSTOM_GETTER(js${typeName}Constructor); +`; + } else { + externs += ` extern JSC_CALLCONV void* JSC_HOST_CALL_ATTRIBUTES ${classSymbolName(typeName, "construct")}(JSC::JSGlobalObject*, JSC::CallFrame*); JSC_DECLARE_CUSTOM_GETTER(js${typeName}Constructor); `; + } } if (obj.structuredClone) { @@ -622,7 +629,8 @@ function generateConstructorImpl(typeName, obj: ClassDefinition) { externs += `extern JSC_CALLCONV size_t ${symbolName(typeName, "estimatedSize")}(void* ptr);` + "\n"; } - return ` + return ( + ` ${renderStaticDecls(classSymbolName, typeName, fields, obj.supportsObjectCreate || false)} ${hashTable} @@ -635,14 +643,14 @@ void ${name}::finishCreation(VM& vm, JSC::JSGlobalObject* globalObject, ${protot } ${name}::${name}(JSC::VM& vm, JSC::Structure* structure) : Base(vm, structure, ${ - obj.call ? classSymbolName(typeName, "call") : "call" - }, construct) { + obj.call ? classSymbolName(typeName, "call") : "call" + }, construct) { } ${name}* ${name}::create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, ${prototypeName( - typeName, - )}* prototype) { + typeName, + )}* prototype) { ${name}* ptr = new (NotNull, JSC::allocateCell<${name}>(vm)) ${name}(vm, structure); ptr->finishCreation(vm, globalObject, prototype); return ptr; @@ -653,6 +661,10 @@ JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES ${name}::call(JSC::JSGlobalObject* Zig::GlobalObject *globalObject = reinterpret_cast(lexicalGlobalObject); JSC::VM &vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); + +${ + !obj.constructNeedsThis + ? ` void* ptr = ${classSymbolName(typeName, "construct")}(globalObject, callFrame); if (!ptr || scope.exception()) [[unlikely]] { @@ -661,6 +673,21 @@ JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES ${name}::call(JSC::JSGlobalObject* Structure* structure = globalObject->${className(typeName)}Structure(); ${className(typeName)}* instance = ${className(typeName)}::create(vm, globalObject, structure, ptr); +` + : ` + Structure* structure = globalObject->${className(typeName)}Structure(); + ${className(typeName)}* instance = ${className(typeName)}::create(vm, globalObject, structure, nullptr); + + void* ptr = ${classSymbolName(typeName, "construct")}(globalObject, callFrame, JSValue::encode(instance)); + if (scope.exception()) [[unlikely]] { + ASSERT_WITH_MESSAGE(!ptr, "Memory leak detected: new ${typeName}() allocated memory without checking for exceptions."); + return JSValue::encode(JSC::jsUndefined()); + } + + instance->m_ctx = ptr; +` +} + RETURN_IF_EXCEPTION(scope, {}); ${ obj.estimatedSize @@ -694,7 +721,10 @@ JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES ${name}::construct(JSC::JSGlobalObj functionGlobalObject->${className(typeName)}Structure() ); } - + +` + + (!obj.constructNeedsThis + ? ` void* ptr = ${classSymbolName(typeName, "construct")}(globalObject, callFrame); if (scope.exception()) [[unlikely]] { @@ -704,6 +734,19 @@ JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES ${name}::construct(JSC::JSGlobalObj ASSERT_WITH_MESSAGE(ptr, "Incorrect exception handling: new ${typeName} returned a null pointer, indicating an exception - but did not throw an exception."); ${className(typeName)}* instance = ${className(typeName)}::create(vm, globalObject, structure, ptr); +` + : ` + ${className(typeName)}* instance = ${className(typeName)}::create(vm, globalObject, structure, nullptr); + + void* ptr = ${classSymbolName(typeName, "construct")}(globalObject, callFrame, JSValue::encode(instance)); + if (scope.exception()) [[unlikely]] { + ASSERT_WITH_MESSAGE(!ptr, "Memory leak detected: new ${typeName}() allocated memory without checking for exceptions."); + return JSValue::encode(JSC::jsUndefined()); + } + + instance->m_ctx = ptr; + `) + + ` ${ obj.estimatedSize ? ` @@ -728,7 +771,8 @@ ${ } - `; + ` + ); } function renderCachedFieldsHeader(typeName, klass, proto, values) { @@ -1788,6 +1832,7 @@ function generateZig( proto = {}, own = {}, construct, + constructNeedsThis = false, finalize, noConstructor = false, overridesToJS = false, @@ -1913,7 +1958,21 @@ const JavaScriptCoreBindings = struct { if (construct && !noConstructor) { exports.set("construct", classSymbolName(typeName, "construct")); - output += ` + if (constructNeedsThis) { + output += ` + pub fn ${classSymbolName(typeName, "construct")}(globalObject: *jsc.JSGlobalObject, callFrame: *jsc.CallFrame, thisValue: jsc.JSValue) callconv(jsc.conv) ?*anyopaque { + if (comptime Environment.enable_logs) log_zig_constructor("${typeName}", callFrame); + return @as(*${typeName}, ${typeName}.constructor(globalObject, callFrame, thisValue) catch |err| switch (err) { + error.JSError => return null, + error.OutOfMemory => { + globalObject.throwOutOfMemory() catch {}; + return null; + }, + }); + } + `; + } else { + output += ` pub fn ${classSymbolName(typeName, "construct")}(globalObject: *jsc.JSGlobalObject, callFrame: *jsc.CallFrame) callconv(jsc.conv) ?*anyopaque { if (comptime Environment.enable_logs) log_zig_constructor("${typeName}", callFrame); return @as(*${typeName}, ${typeName}.constructor(globalObject, callFrame) catch |err| switch (err) { @@ -1925,6 +1984,7 @@ const JavaScriptCoreBindings = struct { }); } `; + } } if (call) { diff --git a/src/logger.zig b/src/logger.zig index 6f03fc5a7e..8fdea0d6ca 100644 --- a/src/logger.zig +++ b/src/logger.zig @@ -64,8 +64,8 @@ pub const Kind = enum(u8) { pub const Loc = struct { start: i32 = -1, - pub inline fn toNullable(loc: *Loc) ?Loc { - return if (loc.start == -1) null else loc.*; + pub inline fn toNullable(loc: Loc) ?Loc { + return if (loc.start == -1) null else loc; } pub const toUsize = i; diff --git a/src/sourcemap/CodeCoverage.zig b/src/sourcemap/CodeCoverage.zig index 167f7d2ad7..d920ba42ac 100644 --- a/src/sourcemap/CodeCoverage.zig +++ b/src/sourcemap/CodeCoverage.zig @@ -1,7 +1,6 @@ const bun = @import("bun"); const std = @import("std"); const LineOffsetTable = bun.sourcemap.LineOffsetTable; -const SourceMap = bun.sourcemap; const Bitset = bun.bit_set.DynamicBitSetUnmanaged; const LinesHits = @import("../baby_list.zig").BabyList(u32); const Output = bun.Output; @@ -561,7 +560,7 @@ pub const ByteRangeMapping = struct { } const column_position = byte_offset -| line_start_byte_offset; - if (SourceMap.Mapping.find(parsed_mapping.mappings, @intCast(new_line_index), @intCast(column_position))) |point| { + if (parsed_mapping.mappings.find(@intCast(new_line_index), @intCast(column_position))) |*point| { if (point.original.lines < 0) continue; const line: u32 = @as(u32, @intCast(point.original.lines)); @@ -605,7 +604,7 @@ pub const ByteRangeMapping = struct { const column_position = byte_offset -| line_start_byte_offset; - if (SourceMap.Mapping.find(parsed_mapping.mappings, @intCast(new_line_index), @intCast(column_position))) |point| { + if (parsed_mapping.mappings.find(@intCast(new_line_index), @intCast(column_position))) |point| { if (point.original.lines < 0) continue; const line: u32 = @as(u32, @intCast(point.original.lines)); diff --git a/src/sourcemap/JSSourceMap.zig b/src/sourcemap/JSSourceMap.zig new file mode 100644 index 0000000000..99afae53e4 --- /dev/null +++ b/src/sourcemap/JSSourceMap.zig @@ -0,0 +1,306 @@ +/// 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" }); +} + +// @sortImports + +const std = @import("std"); + +const bun = @import("bun"); +const string = bun.string; + +const JSC = bun.JSC; +const CallFrame = JSC.CallFrame; +const JSGlobalObject = JSC.JSGlobalObject; +const JSValue = JSC.JSValue; + +pub const js = JSC.Codegen.JSSourceMap; +pub const fromJS = js.fromJS; +pub const fromJSDirect = js.fromJSDirect; +pub const toJS = js.toJS; diff --git a/src/sourcemap/sourcemap.zig b/src/sourcemap/sourcemap.zig index bb4d969fe0..e664edf77d 100644 --- a/src/sourcemap/sourcemap.zig +++ b/src/sourcemap/sourcemap.zig @@ -10,7 +10,7 @@ const JSPrinter = bun.js_printer; const URL = bun.URL; const FileSystem = bun.fs.FileSystem; -const SourceMap = @This(); +pub const SourceMap = @This(); const debug = bun.Output.scoped(.SourceMap, false); /// Coordinates in source maps are stored using relative offsets for size @@ -42,7 +42,11 @@ pub const ParseUrlResultHint = union(enum) { /// In order to fetch source contents, you need to know the /// index, but you cant know the index until the mappings /// are loaded. So pass in line+col. - all: struct { line: i32, column: i32 }, + all: struct { + line: i32, + column: i32, + include_names: bool = false, + }, }; pub const ParseUrl = struct { @@ -179,19 +183,46 @@ pub fn parseJSON( }; const map = if (hint != .source_only) map: { - const map_data = switch (Mapping.parse( + var map_data = switch (Mapping.parse( alloc, mappings_str.data.e_string.slice(arena), null, std.math.maxInt(i32), std.math.maxInt(i32), + .{ .allow_names = hint == .all and hint.all.include_names, .sort = true }, )) { .success => |x| x, .fail => |fail| return fail.err, }; + if (hint == .all and hint.all.include_names and map_data.mappings.impl == .with_names) { + if (json.get("names")) |names| { + if (names.data == .e_array) { + var names_list = try std.ArrayListUnmanaged(bun.Semver.String).initCapacity(alloc, names.data.e_array.items.len); + errdefer names_list.deinit(alloc); + + var names_buffer = std.ArrayListUnmanaged(u8){}; + errdefer names_buffer.deinit(alloc); + + for (names.data.e_array.items.slice()) |*item| { + if (item.data != .e_string) { + return error.InvalidSourceMap; + } + + const str = try item.data.e_string.string(arena); + + names_list.appendAssumeCapacity(try bun.Semver.String.initAppendIfNeeded(alloc, &names_buffer, str)); + } + + map_data.mappings.names = names_list.items; + map_data.mappings.names_buffer = .fromList(names_buffer); + } + } + } + const ptr = bun.new(ParsedSourceMap, map_data); ptr.external_source_names = source_paths_slice.?; + break :map ptr; } else null; errdefer if (map) |m| m.deref(); @@ -199,7 +230,7 @@ pub fn parseJSON( const mapping, const source_index = switch (hint) { .source_only => |index| .{ null, index }, .all => |loc| brk: { - const mapping = Mapping.find(map.?.mappings, loc.line, loc.column) orelse + const mapping = map.?.mappings.find(loc.line, loc.column) orelse break :brk .{ null, null }; break :brk .{ mapping, std.math.cast(u32, mapping.source_index) }; }, @@ -234,8 +265,206 @@ pub const Mapping = struct { generated: LineColumnOffset, original: LineColumnOffset, source_index: i32, + name_index: i32 = -1, - pub const List = bun.MultiArrayList(Mapping); + /// Optimization: if we don't care about the "names" column, then don't store the names. + pub const MappingWithoutName = struct { + generated: LineColumnOffset, + original: LineColumnOffset, + source_index: i32, + + pub fn toNamed(this: *const MappingWithoutName) Mapping { + return .{ + .generated = this.generated, + .original = this.original, + .source_index = this.source_index, + .name_index = -1, + }; + } + }; + + pub const List = struct { + impl: Value = .{ .without_names = .{} }, + names: []const bun.Semver.String = &[_]bun.Semver.String{}, + names_buffer: bun.ByteList = .{}, + + pub const Value = union(enum) { + without_names: bun.MultiArrayList(MappingWithoutName), + with_names: bun.MultiArrayList(Mapping), + + pub fn memoryCost(this: *const Value) usize { + return switch (this.*) { + .without_names => |*list| list.memoryCost(), + .with_names => |*list| list.memoryCost(), + }; + } + + pub fn ensureTotalCapacity(this: *Value, allocator: std.mem.Allocator, count: usize) !void { + switch (this.*) { + inline else => |*list| try list.ensureTotalCapacity(allocator, count), + } + } + }; + + fn ensureWithNames(this: *List, allocator: std.mem.Allocator) !void { + if (this.impl == .with_names) return; + + var without_names = this.impl.without_names; + var with_names = bun.MultiArrayList(Mapping){}; + try with_names.ensureTotalCapacity(allocator, without_names.len); + defer without_names.deinit(allocator); + + with_names.len = without_names.len; + var old_slices = without_names.slice(); + var new_slices = with_names.slice(); + + @memcpy(new_slices.items(.generated), old_slices.items(.generated)); + @memcpy(new_slices.items(.original), old_slices.items(.original)); + @memcpy(new_slices.items(.source_index), old_slices.items(.source_index)); + @memset(new_slices.items(.name_index), -1); + + this.impl = .{ .with_names = with_names }; + } + + fn findIndexFromGenerated(line_column_offsets: []const LineColumnOffset, line: i32, column: i32) ?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 < line or (mapping.lines == line and mapping.columns <= column)) { + index = i + 1; + count -|= step + 1; + } else { + count = step; + } + } + + if (index > 0) { + if (line_column_offsets[index - 1].lines == line) { + return index - 1; + } + } + + return null; + } + + pub fn findIndex(this: *const List, line: i32, column: i32) ?usize { + switch (this.impl) { + inline else => |*list| { + if (findIndexFromGenerated(list.items(.generated), line, column)) |i| { + return i; + } + }, + } + + return null; + } + + const SortContext = struct { + generated: []const LineColumnOffset, + pub fn lessThan(ctx: SortContext, a_index: usize, b_index: usize) bool { + const a = ctx.generated[a_index]; + const b = ctx.generated[b_index]; + + return a.lines < b.lines or (a.lines == b.lines and a.columns <= b.columns); + } + }; + + pub fn sort(this: *List) void { + switch (this.impl) { + .without_names => |*list| list.sort(SortContext{ .generated = list.items(.generated) }), + .with_names => |*list| list.sort(SortContext{ .generated = list.items(.generated) }), + } + } + + pub fn append(this: *List, allocator: std.mem.Allocator, mapping: *const Mapping) !void { + switch (this.impl) { + .without_names => |*list| { + try list.append(allocator, .{ + .generated = mapping.generated, + .original = mapping.original, + .source_index = mapping.source_index, + }); + }, + .with_names => |*list| { + try list.append(allocator, mapping.*); + }, + } + } + + pub fn find(this: *const List, line: i32, column: i32) ?Mapping { + switch (this.impl) { + inline else => |*list, tag| { + if (findIndexFromGenerated(list.items(.generated), line, column)) |i| { + if (tag == .without_names) { + return list.get(i).toNamed(); + } else { + return list.get(i); + } + } + }, + } + + return null; + } + pub fn generated(self: *const List) []const LineColumnOffset { + return switch (self.impl) { + inline else => |*list| list.items(.generated), + }; + } + + pub fn original(self: *const List) []const LineColumnOffset { + return switch (self.impl) { + inline else => |*list| list.items(.original), + }; + } + + pub fn sourceIndex(self: *const List) []const i32 { + return switch (self.impl) { + inline else => |*list| list.items(.source_index), + }; + } + + pub fn nameIndex(self: *const List) []const i32 { + return switch (self.impl) { + inline else => |*list| list.items(.name_index), + }; + } + + pub fn deinit(self: *List, allocator: std.mem.Allocator) void { + switch (self.impl) { + inline else => |*list| list.deinit(allocator), + } + + self.names_buffer.deinitWithAllocator(allocator); + allocator.free(self.names); + } + + pub fn getName(this: *List, index: i32) ?[]const u8 { + if (index < 0) return null; + const i: usize = @intCast(index); + + if (i >= this.names.len) return null; + + if (this.impl == .with_names) { + const str: *const bun.Semver.String = &this.names[i]; + return str.slice(this.names_buffer.slice()); + } + + return null; + } + + pub fn memoryCost(this: *const List) usize { + return this.impl.memoryCost() + this.names_buffer.memoryCost() + + (this.names.len * @sizeOf(bun.Semver.String)); + } + + pub fn ensureTotalCapacity(this: *List, allocator: std.mem.Allocator, count: usize) !void { + try this.impl.ensureTotalCapacity(allocator, count); + } + }; pub const Lookup = struct { mapping: Mapping, @@ -244,6 +473,8 @@ pub const Mapping = struct { /// use `getSourceCode` to access this as a Slice prefetched_source_code: ?[]const u8, + name: ?[]const u8 = null, + /// This creates a bun.String if the source remap *changes* the source url, /// which is only possible if the executed file differs from the source file: /// @@ -336,58 +567,28 @@ pub const Mapping = struct { } }; - pub inline fn generatedLine(mapping: Mapping) i32 { + pub inline fn generatedLine(mapping: *const Mapping) i32 { return mapping.generated.lines; } - pub inline fn generatedColumn(mapping: Mapping) i32 { + pub inline fn generatedColumn(mapping: *const Mapping) i32 { return mapping.generated.columns; } - pub inline fn sourceIndex(mapping: Mapping) i32 { + pub inline fn sourceIndex(mapping: *const Mapping) i32 { return mapping.source_index; } - pub inline fn originalLine(mapping: Mapping) i32 { + pub inline fn originalLine(mapping: *const Mapping) i32 { return mapping.original.lines; } - pub inline fn originalColumn(mapping: Mapping) i32 { + pub inline fn originalColumn(mapping: *const Mapping) i32 { return mapping.original.columns; } - pub fn find(mappings: Mapping.List, line: i32, column: i32) ?Mapping { - if (findIndex(mappings, line, column)) |i| { - return mappings.get(i); - } - - return null; - } - - pub fn findIndex(mappings: Mapping.List, line: i32, column: i32) ?usize { - const generated = mappings.items(.generated); - - var count = generated.len; - var index: usize = 0; - while (count > 0) { - const step = count / 2; - const i: usize = index + step; - const mapping = generated[i]; - if (mapping.lines < line or (mapping.lines == line and mapping.columns <= column)) { - index = i + 1; - count -|= step + 1; - } else { - count = step; - } - } - - if (index > 0) { - if (generated[index - 1].lines == line) { - return index - 1; - } - } - - return null; + pub inline fn nameIndex(mapping: *const Mapping) i32 { + return mapping.name_index; } pub fn parse( @@ -396,19 +597,35 @@ pub const Mapping = struct { estimated_mapping_count: ?usize, sources_count: i32, input_line_count: usize, + options: struct { + allow_names: bool = false, + sort: bool = false, + }, ) ParseResult { debug("parse mappings ({d} bytes)", .{bytes.len}); var mapping = Mapping.List{}; + errdefer mapping.deinit(allocator); + if (estimated_mapping_count) |count| { - mapping.ensureTotalCapacity(allocator, count) catch unreachable; + mapping.ensureTotalCapacity(allocator, count) catch { + return .{ + .fail = .{ + .msg = "Out of memory", + .err = error.OutOfMemory, + .loc = .{}, + }, + }; + }; } var generated = LineColumnOffset{ .lines = 0, .columns = 0 }; var original = LineColumnOffset{ .lines = 0, .columns = 0 }; + var name_index: i32 = 0; var source_index: i32 = 0; var needs_sort = false; var remain = bytes; + var has_names = false; while (remain.len > 0) { if (remain[0] == ';') { generated.columns = 0; @@ -558,28 +775,70 @@ pub const Mapping = struct { if (remain.len > 0) { switch (remain[0]) { ',' => { + // 4 column, but there's more on this line. remain = remain[1..]; }, + // 4 column, and there's no more on this line. ';' => {}, + + // 5th column: the name else => |c| { - return .{ - .fail = .{ - .msg = "Invalid character after mapping", - .err = error.InvalidSourceMap, - .value = @as(i32, @intCast(c)), - .loc = .{ .start = @as(i32, @intCast(bytes.len - remain.len)) }, - }, - }; + // Read the name index + const name_index_delta = decodeVLQ(remain, 0); + if (name_index_delta.start == 0) { + return .{ + .fail = .{ + .msg = "Invalid name index delta", + .err = error.InvalidNameIndexDelta, + .value = @intCast(c), + .loc = .{ .start = @as(i32, @intCast(bytes.len - remain.len)) }, + }, + }; + } + remain = remain[name_index_delta.start..]; + + if (options.allow_names) { + name_index += name_index_delta.value; + if (!has_names) { + mapping.ensureWithNames(allocator) catch { + return .{ + .fail = .{ + .msg = "Out of memory", + .err = error.OutOfMemory, + .loc = .{ .start = @as(i32, @intCast(bytes.len - remain.len)) }, + }, + }; + }; + } + has_names = true; + } + + if (remain.len > 0) { + switch (remain[0]) { + // There's more on this line. + ',' => { + remain = remain[1..]; + }, + // That's the end of the line. + ';' => {}, + else => {}, + } + } }, } } - mapping.append(allocator, .{ + mapping.append(allocator, &.{ .generated = generated, .original = original, .source_index = source_index, + .name_index = name_index, }) catch bun.outOfMemory(); } + if (needs_sort and options.sort) { + mapping.sort(); + } + return .{ .success = .{ .ref_count = .init(), .mappings = mapping, @@ -622,6 +881,7 @@ pub const ParsedSourceMap = struct { input_line_count: usize = 0, mappings: Mapping.List = .{}, + /// If this is empty, this implies that the source code is a single file /// transpiled on-demand. If there are items, then it means this is a file /// loaded without transpilation but with external sources. This array @@ -710,16 +970,20 @@ pub const ParsedSourceMap = struct { return @ptrFromInt(this.underlying_provider.data); } - pub fn writeVLQs(map: ParsedSourceMap, writer: anytype) !void { + pub fn memoryCost(this: *const ParsedSourceMap) usize { + return @sizeOf(ParsedSourceMap) + this.mappings.memoryCost() + this.external_source_names.len * @sizeOf([]const u8); + } + + pub fn writeVLQs(map: *const ParsedSourceMap, writer: anytype) !void { var last_col: i32 = 0; var last_src: i32 = 0; var last_ol: i32 = 0; var last_oc: i32 = 0; var current_line: i32 = 0; for ( - map.mappings.items(.generated), - map.mappings.items(.original), - map.mappings.items(.source_index), + map.mappings.generated(), + map.mappings.original(), + map.mappings.sourceIndex(), 0.., ) |gen, orig, source_index, i| { if (current_line != gen.lines) { @@ -1056,7 +1320,7 @@ pub fn find( line: i32, column: i32, ) ?Mapping { - return Mapping.find(this.mapping, line, column); + return this.mapping.find(line, column); } pub const SourceMapShifts = struct { @@ -1671,6 +1935,7 @@ const assert = bun.assert; pub const coverage = @import("./CodeCoverage.zig"); pub const VLQ = @import("./VLQ.zig"); pub const LineOffsetTable = @import("./LineOffsetTable.zig"); +pub const JSSourceMap = @import("./JSSourceMap.zig"); const decodeVLQAssumeValid = VLQ.decodeAssumeValid; const decodeVLQ = VLQ.decode; diff --git a/test/internal/ban-words.test.ts b/test/internal/ban-words.test.ts index fccdb519b9..1a78466ae7 100644 --- a/test/internal/ban-words.test.ts +++ b/test/internal/ban-words.test.ts @@ -34,7 +34,7 @@ const words: Record [String.raw`: [a-zA-Z0-9_\.\*\?\[\]\(\)]+ = undefined,`]: { reason: "Do not default a struct field to undefined", limit: 243, regex: true }, "usingnamespace": { reason: "Zig 0.15 will remove `usingnamespace`" }, - "catch unreachable": { reason: "For out-of-memory, prefer 'catch bun.outOfMemory()'", limit: 1867 }, + "catch unreachable": { reason: "For out-of-memory, prefer 'catch bun.outOfMemory()'", limit: 1866 }, "std.fs.Dir": { reason: "Prefer bun.sys + bun.FD instead of std.fs", limit: 179 }, "std.fs.cwd": { reason: "Prefer bun.FD.cwd()", limit: 103 }, diff --git a/test/js/node/module/module-sourcemap.test.js b/test/js/node/module/module-sourcemap.test.js new file mode 100644 index 0000000000..dc847c01b2 --- /dev/null +++ b/test/js/node/module/module-sourcemap.test.js @@ -0,0 +1,27 @@ +const { test, expect } = require("bun:test"); + +test("SourceMap is available from node:module", () => { + const module = require("node:module"); + expect(module.SourceMap).toBeDefined(); + expect(typeof module.SourceMap).toBe("function"); +}); + +test("SourceMap from require('module') works", () => { + const module = require("module"); + expect(module.SourceMap).toBeDefined(); + expect(typeof module.SourceMap).toBe("function"); +}); + +test("Can create SourceMap instance from node:module", () => { + const { SourceMap } = require("node:module"); + const payload = { + version: 3, + sources: ["test.js"], + names: [], + mappings: "AAAA", + }; + + const sourceMap = new SourceMap(payload); + expect(sourceMap).toBeInstanceOf(SourceMap); + expect(sourceMap.payload).toBe(payload); +}); diff --git a/test/js/node/module/sourcemap.test.js b/test/js/node/module/sourcemap.test.js new file mode 100644 index 0000000000..d9ff675cdf --- /dev/null +++ b/test/js/node/module/sourcemap.test.js @@ -0,0 +1,177 @@ +const { test, expect } = require("bun:test"); +const { SourceMap } = require("node:module"); + +test("SourceMap class exists", () => { + expect(SourceMap).toBeDefined(); + expect(typeof SourceMap).toBe("function"); + expect(SourceMap.name).toBe("SourceMap"); +}); + +test("SourceMap constructor requires payload", () => { + expect(() => { + new SourceMap(); + }).toThrowErrorMatchingInlineSnapshot(`"The "payload" argument must be of type object. Received undefined"`); +}); + +test("SourceMap payload must be an object", () => { + expect(() => { + new SourceMap("not an object"); + }).toThrowErrorMatchingInlineSnapshot( + `"The "payload" argument must be of type object. Received type string ('not an object')"`, + ); +}); + +test("SourceMap instance has expected methods", () => { + const sourceMap = new SourceMap({ + version: 3, + sources: ["test.js"], + mappings: "AAAA", + }); + + expect(typeof sourceMap.findOrigin).toBe("function"); + expect(typeof sourceMap.findEntry).toBe("function"); + expect(sourceMap.findOrigin.length).toBe(2); + expect(sourceMap.findEntry.length).toBe(2); +}); + +test("SourceMap payload getter", () => { + const payload = { + version: 3, + sources: ["test.js"], + mappings: "AAAA", + }; + const sourceMap = new SourceMap(payload); + + expect(sourceMap.payload).toBe(payload); +}); + +test("SourceMap lineLengths getter", () => { + const payload = { + version: 3, + sources: ["test.js"], + mappings: "AAAA", + }; + const lineLengths = [10, 20, 30]; + const sourceMap = new SourceMap(payload, { lineLengths }); + + expect(sourceMap.lineLengths).toBe(lineLengths); +}); + +test("SourceMap lineLengths undefined when not provided", () => { + const sourceMap = new SourceMap({ + version: 3, + sources: ["test.js"], + mappings: "AAAA", + }); + + expect(sourceMap.lineLengths).toBeUndefined(); +}); +test("SourceMap findEntry returns mapping data", () => { + const sourceMap = new SourceMap({ + version: 3, + sources: ["test.js"], + mappings: "AAAA", + }); + const result = sourceMap.findEntry(0, 0); + + expect(result).toMatchInlineSnapshot(` + { + "generatedColumn": 0, + "generatedLine": 0, + "name": undefined, + "originalColumn": 0, + "originalLine": 0, + "originalSource": "test.js", + } + `); +}); + +test("SourceMap findOrigin returns origin data", () => { + const sourceMap = new SourceMap({ + version: 3, + sources: ["test.js"], + mappings: "AAAA", + }); + const result = sourceMap.findOrigin(0, 0); + + expect(result).toMatchInlineSnapshot(` + { + "column": 0, + "fileName": "test.js", + "line": 0, + "name": undefined, + } + `); +}); + +test("SourceMap with names returns name property correctly", () => { + const sourceMap = new SourceMap({ + version: 3, + sources: ["test.js"], + names: ["myFunction", "myVariable"], + mappings: "AAAAA,CAACC", // Both segments reference names + }); + + const result = sourceMap.findEntry(0, 0); + const resultWithName = sourceMap.findEntry(0, 6); + expect(result).toMatchInlineSnapshot(` + { + "generatedColumn": 0, + "generatedLine": 0, + "name": "myFunction", + "originalColumn": 0, + "originalLine": 0, + "originalSource": "test.js", + } + `); + expect(resultWithName).toMatchInlineSnapshot(` + { + "generatedColumn": 1, + "generatedLine": 0, + "name": "myVariable", + "originalColumn": 1, + "originalLine": 0, + "originalSource": "test.js", + } + `); +}); + +test("SourceMap without names has undefined name property", () => { + const sourceMap = new SourceMap({ + version: 3, + sources: ["test.js"], + mappings: "AAAA", + }); + + const result = sourceMap.findEntry(0, 0); + expect(result).toMatchInlineSnapshot(` + { + "generatedColumn": 0, + "generatedLine": 0, + "name": undefined, + "originalColumn": 0, + "originalLine": 0, + "originalSource": "test.js", + } + `); +}); + +test("SourceMap with invalid name index has undefined name property", () => { + const sourceMap = new SourceMap({ + version: 3, + sources: ["test.js"], + mappings: "AAAAA,CAACC", // Both segments reference names + }); + + const result = sourceMap.findEntry(0, 0); + expect(result).toMatchInlineSnapshot(` + { + "generatedColumn": 0, + "generatedLine": 0, + "name": undefined, + "originalColumn": 0, + "originalLine": 0, + "originalSource": "test.js", + } + `); +}); diff --git a/test/no-validate-exceptions.txt b/test/no-validate-exceptions.txt index 3823c677bc..e1a5bffacb 100644 --- a/test/no-validate-exceptions.txt +++ b/test/no-validate-exceptions.txt @@ -195,6 +195,7 @@ test/js/bun/spawn/spawn-stdin-readable-stream-sync.test.ts test/js/bun/spawn/spawn-stdin-readable-stream.test.ts test/js/bun/spawn/spawn-stream-serve.test.ts test/js/bun/spawn/spawn-streaming-stdout.test.ts +test/js/node/module/sourcemap.test.js test/js/bun/spawn/spawn-stress.test.ts test/js/bun/spawn/spawn.ipc.bun-node.test.ts test/js/bun/spawn/spawn.ipc.node-bun.test.ts