diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 03a2b4d663..2aa1a1b113 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -207,7 +207,7 @@ $ git clone https://github.com/oven-sh/WebKit vendor/WebKit # Make a debug build of JSC. This will output build artifacts in ./vendor/WebKit/WebKitBuild/Debug # Optionally, you can use `make jsc` for a release build -$ make jsc-debug +$ make jsc-debug && rm vendor/WebKit/WebKitBuild/Debug/JavaScriptCore/DerivedSources/inspector/InspectorProtocolObjects.h # Build bun with the local JSC build $ bun run build:local diff --git a/build.zig b/build.zig index 0fc377326f..e7dfcb2e1f 100644 --- a/build.zig +++ b/build.zig @@ -470,6 +470,11 @@ pub fn addInstallObjectFile( name: []const u8, out_mode: ObjectFormat, ) *Step { + if (@import("builtin").os.tag != .windows and std.posix.getenvZ("COMPILE_ERRORS_ONLY") != null) { + const failstep = b.addSystemCommand(&.{"COMPILE_ERRORS_ONLY set but there were no compile errors"}); + failstep.step.dependOn(&compile.step); + return &failstep.step; + } // bin always needed to be computed or else the compilation will do nothing. zig build system bug? const bin = compile.getEmittedBin(); return &b.addInstallFile(switch (out_mode) { diff --git a/cmake/tools/SetupWebKit.cmake b/cmake/tools/SetupWebKit.cmake index 22e95e7742..af3aa504df 100644 --- a/cmake/tools/SetupWebKit.cmake +++ b/cmake/tools/SetupWebKit.cmake @@ -28,6 +28,8 @@ if(WEBKIT_LOCAL) ${WEBKIT_PATH} ${WEBKIT_PATH}/JavaScriptCore/Headers/JavaScriptCore ${WEBKIT_PATH}/JavaScriptCore/PrivateHeaders + ${WEBKIT_PATH}/JavaScriptCore/DerivedSources/inspector + ${WEBKIT_PATH}/JavaScriptCore/PrivateHeaders/JavaScriptCore ${WEBKIT_PATH}/bmalloc/Headers ${WEBKIT_PATH}/WTF/Headers ) diff --git a/src/analyze_transpiled_module.zig b/src/analyze_transpiled_module.zig new file mode 100644 index 0000000000..72e241bd9f --- /dev/null +++ b/src/analyze_transpiled_module.zig @@ -0,0 +1,477 @@ +const std = @import("std"); +const bun = @import("bun.zig"); +const js_ast = bun.JSAst; +const Ast = js_ast.Ast; + +pub const RecordKind = enum(u8) { + /// var_name + declared_variable, + /// let_name + lexical_variable, + /// module_name, import_name, local_name + import_info_single, + /// module_name, import_name = '*', local_name + import_info_namespace, + /// export_name, import_name, module_name + export_info_indirect, + /// export_name, local_name, padding (for local => indirect conversion) + export_info_local, + /// export_name, module_name + export_info_namespace, + /// module_name + export_info_star, + _, + + pub fn len(record: RecordKind) !usize { + return switch (record) { + .declared_variable, .lexical_variable => 1, + .import_info_single => 3, + .import_info_namespace => 3, + .export_info_indirect => 3, + .export_info_local => 3, + .export_info_namespace => 2, + .export_info_star => 1, + else => return error.InvalidRecordKind, + }; + } +}; + +pub const ModuleInfoDeserialized = struct { + strings_buf: []const u8, + strings_lens: []align(1) const u32, + requested_modules_keys: []align(1) const StringID, + requested_modules_values: []align(1) const ModuleInfo.FetchParameters, + buffer: []align(1) const StringID, + record_kinds: []align(1) const RecordKind, + contains_import_meta: bool, + owner: union(enum) { + module_info, + allocated_slice: struct { + slice: []const u8, + allocator: std.mem.Allocator, + }, + }, + dead: bool = false, + + pub fn deinit(self: *ModuleInfoDeserialized) void { + switch (self.owner) { + .module_info => { + const mi: *ModuleInfo = @fieldParentPtr("_deserialized", self); + mi.destroy(); + }, + .allocated_slice => |as| { + as.allocator.free(as.slice); + as.allocator.destroy(self); + }, + } + } + + inline fn eat(rem: *[]const u8, len: usize) ![]const u8 { + if (rem.*.len < len) return error.BadModuleInfo; + const res = rem.*[0..len]; + rem.* = rem.*[len..]; + return res; + } + inline fn eatC(rem: *[]const u8, comptime len: usize) !*const [len]u8 { + if (rem.*.len < len) return error.BadModuleInfo; + const res = rem.*[0..len]; + rem.* = rem.*[len..]; + return res; + } + pub fn create(source: []const u8, gpa: std.mem.Allocator) !*ModuleInfoDeserialized { + std.log.info("ModuleInfoDeserialized.create", .{}); + var rem = try gpa.dupe(u8, source); + errdefer gpa.free(rem); + var res = try gpa.create(ModuleInfoDeserialized); + errdefer res.deinit(); + + const record_kinds_len = std.mem.readInt(u32, try eatC(&rem, 4), .little); + const record_kinds = std.mem.bytesAsSlice(RecordKind, try eat(&rem, record_kinds_len * @sizeOf(RecordKind))); + const buffer_len = std.mem.readInt(u32, try eatC(&rem, 4), .little); + const buffer = std.mem.bytesAsSlice(StringID, try eat(&rem, buffer_len * @sizeOf(StringID))); + const requested_modules_len = std.mem.readInt(u32, try eatC(&rem, 4), .little); + const requested_modules_keys = std.mem.bytesAsSlice(StringID, try eat(&rem, requested_modules_len * @sizeOf(StringID))); + const requested_modules_values = std.mem.bytesAsSlice(ModuleInfo.FetchParameters, try eat(&rem, requested_modules_len * @sizeOf(ModuleInfo.FetchParameters))); + const contains_import_meta = (try eatC(&rem, 1))[0] != 0; + const strings_len = std.mem.readInt(u32, try eatC(&rem, 4), .little); + const strings_lens = std.mem.bytesAsSlice(u32, try eat(&rem, strings_len * @sizeOf(u32))); + const strings_buf = rem; + + res.* = .{ + .strings_buf = strings_buf, + .strings_lens = strings_lens, + .requested_modules_keys = requested_modules_keys, + .requested_modules_values = requested_modules_values, + .buffer = buffer, + .record_kinds = record_kinds, + .contains_import_meta = contains_import_meta, + .owner = .{ .allocated_slice = .{ + .slice = source, + .allocator = gpa, + } }, + }; + return res; + } + pub fn serialize(self: *const ModuleInfoDeserialized, writer: anytype) !void { + try writer.writeInt(u32, @truncate(self.record_kinds.len), .little); + try writer.writeAll(std.mem.sliceAsBytes(self.record_kinds)); + try writer.writeInt(u32, @truncate(self.buffer.len), .little); + try writer.writeAll(std.mem.sliceAsBytes(self.buffer)); + + try writer.writeInt(u32, @truncate(self.requested_modules_keys.len), .little); + try writer.writeAll(std.mem.sliceAsBytes(self.requested_modules_keys)); + try writer.writeAll(std.mem.sliceAsBytes(self.requested_modules_values)); + + try writer.writeInt(u8, @intFromBool(self.contains_import_meta), .little); + + try writer.writeInt(u32, @truncate(self.strings_lens.len), .little); + try writer.writeAll(std.mem.sliceAsBytes(self.strings_lens)); + try writer.writeAll(self.strings_buf); + } +}; + +const StringMapKey = enum(u32) { + get_or_put = std.math.maxInt(u32), + _, +}; +pub const StringContext = struct { + get_or_put_key: []const u8, + strings_buf: []const u8, + strings_lens: []const u32, + + pub fn hash(self: @This(), s: StringMapKey) u32 { + bun.assert(s == .get_or_put); + return @as(u32, @truncate(std.hash.Wyhash.hash(0, self.get_or_put_key))); + } + pub fn eql(self: @This(), fetch_key: StringMapKey, item_key: StringMapKey, item_i: usize) bool { + bun.assert(item_key != .get_or_put); + bun.assert(fetch_key == .get_or_put); + return bun.strings.eqlLong(self.get_or_put_key, self.strings_buf[@intFromEnum(item_key)..][0..self.strings_lens[item_i]], true); + } +}; + +pub const ModuleInfo = struct { + /// all strings in wtf-8. index in hashmap = StringID + gpa: std.mem.Allocator, + strings_map: std.ArrayHashMapUnmanaged(StringMapKey, void, StringContext, true), + strings_buf: std.ArrayListUnmanaged(u8), + strings_lens: std.ArrayListUnmanaged(u32), + requested_modules: std.AutoArrayHashMap(StringID, FetchParameters), + buffer: std.ArrayList(StringID), + record_kinds: std.ArrayList(RecordKind), + exported_names: std.AutoArrayHashMapUnmanaged(StringID, void), + contains_import_meta: bool, + finalized: bool = false, + + _deserialized: ModuleInfoDeserialized = undefined, + + pub fn asDeserialized(self: *ModuleInfo) *ModuleInfoDeserialized { + bun.assert(self.finalized); + return &self._deserialized; + } + + pub const FetchParameters = enum(u32) { + none = std.math.maxInt(u32), + javascript = std.math.maxInt(u32) - 1, + webassembly = std.math.maxInt(u32) - 2, + json = std.math.maxInt(u32) - 3, + _, // host_defined: cast to StringID + pub fn hostDefined(value: StringID) FetchParameters { + return @enumFromInt(@intFromEnum(value)); + } + }; + + pub const VarKind = enum { declared, lexical }; + pub fn addVar(self: *ModuleInfo, name: []const u8, kind: VarKind) !void { + switch (kind) { + .declared => try self.addDeclaredVariable(name), + .lexical => try self.addLexicalVariable(name), + } + } + + fn _addRecord(self: *ModuleInfo, kind: RecordKind, data: []const StringID) !void { + bun.assert(!self.finalized); + bun.assert(data.len == kind.len() catch unreachable); + try self.record_kinds.append(kind); + try self.buffer.appendSlice(data); + } + pub fn addDeclaredVariable(self: *ModuleInfo, id: []const u8) !void { + try self._addRecord(.declared_variable, &.{try self.str(id)}); + } + pub fn addLexicalVariable(self: *ModuleInfo, id: []const u8) !void { + try self._addRecord(.lexical_variable, &.{try self.str(id)}); + } + pub fn addImportInfoSingle(self: *ModuleInfo, module_name: []const u8, import_name: []const u8, local_name: []const u8) !void { + try self._addRecord(.import_info_single, &.{ try self.str(module_name), try self.str(import_name), try self.str(local_name) }); + } + pub fn addImportInfoNamespace(self: *ModuleInfo, module_name: []const u8, local_name: []const u8) !void { + try self._addRecord(.import_info_namespace, &.{ try self.str(module_name), try self.str("*"), try self.str(local_name) }); + } + pub fn addExportInfoIndirect(self: *ModuleInfo, export_name: []const u8, import_name: []const u8, module_name: []const u8) !void { + const export_name_id = try self.str(export_name); + if (try self._hasOrAddExportedName(export_name_id)) return; // a syntax error will be emitted later in this case + try self._addRecord(.export_info_indirect, &.{ export_name_id, try self.str(import_name), try self.str(module_name) }); + } + pub fn addExportInfoLocal(self: *ModuleInfo, export_name: []const u8, local_name: []const u8) !void { + const export_name_id = try self.str(export_name); + if (try self._hasOrAddExportedName(export_name_id)) return; // a syntax error will be emitted later in this case + try self._addRecord(.export_info_local, &.{ export_name_id, try self.str(local_name), @enumFromInt(std.math.maxInt(u32)) }); + } + pub fn addExportInfoNamespace(self: *ModuleInfo, export_name: []const u8, module_name: []const u8) !void { + const export_name_id = try self.str(export_name); + if (try self._hasOrAddExportedName(export_name_id)) return; // a syntax error will be emitted later in this case + try self._addRecord(.export_info_namespace, &.{ export_name_id, try self.str(module_name) }); + } + pub fn addExportInfoStar(self: *ModuleInfo, module_name: []const u8) !void { + try self._addRecord(.export_info_star, &.{try self.str(module_name)}); + } + + pub fn _hasOrAddExportedName(self: *ModuleInfo, name: StringID) !bool { + if (try self.exported_names.fetchPut(self.gpa, name, {}) != null) return true; + return false; + } + + pub fn create(gpa: std.mem.Allocator) !*ModuleInfo { + const res = try gpa.create(ModuleInfo); + res.* = ModuleInfo.init(gpa); + return res; + } + fn init(allocator: std.mem.Allocator) ModuleInfo { + return .{ + .gpa = allocator, + .strings_map = .{}, + .strings_buf = .{}, + .strings_lens = .{}, + .exported_names = .{}, + .requested_modules = std.AutoArrayHashMap(StringID, FetchParameters).init(allocator), + .buffer = std.ArrayList(StringID).init(allocator), + .record_kinds = std.ArrayList(RecordKind).init(allocator), + .contains_import_meta = false, + }; + } + fn deinit(self: *ModuleInfo) void { + self.strings_map.deinit(self.gpa); + self.strings_buf.deinit(self.gpa); + self.strings_lens.deinit(self.gpa); + self.exported_names.deinit(self.gpa); + self.requested_modules.deinit(); + self.buffer.deinit(); + self.record_kinds.deinit(); + } + pub fn destroy(self: *ModuleInfo) void { + const alloc = self.gpa; + self.deinit(); + alloc.destroy(self); + } + pub fn str(self: *ModuleInfo, value: []const u8) !StringID { + const gpres = try self.strings_map.getOrPutContext(self.gpa, .get_or_put, .{ + .get_or_put_key = value, + .strings_buf = self.strings_buf.items, + .strings_lens = self.strings_lens.items, + }); + if (gpres.found_existing) return @enumFromInt(@as(u32, @intCast(gpres.index))); + + gpres.key_ptr.* = @enumFromInt(@as(u32, @truncate(self.strings_buf.items.len))); + gpres.value_ptr.* = {}; + try self.strings_buf.ensureUnusedCapacity(self.gpa, value.len); + try self.strings_lens.ensureUnusedCapacity(self.gpa, 1); + self.strings_buf.appendSliceAssumeCapacity(value); + self.strings_lens.appendAssumeCapacity(@as(u32, @truncate(value.len))); + return @enumFromInt(@as(u32, @intCast(gpres.index))); + } + pub const star_default = "*default*"; + pub fn requestModule(self: *ModuleInfo, import_record_path: []const u8, fetch_parameters: FetchParameters) !void { + // jsc only records the attributes of the first import with the given import_record_path. so only put if not exists. + const gpres = try self.requested_modules.getOrPut(try self.str(import_record_path)); + if (!gpres.found_existing) gpres.value_ptr.* = fetch_parameters; + } + + /// find any exports marked as 'local' that are actually 'indirect' and fix them + pub fn finalize(self: *ModuleInfo) !void { + bun.assert(!self.finalized); + var local_name_to_module_name = std.AutoArrayHashMap(StringID, struct { module_name: StringID, import_name: StringID }).init(bun.default_allocator); + defer local_name_to_module_name.deinit(); + { + var i: usize = 0; + for (self.record_kinds.items) |k| { + if (k == .import_info_single) { + try local_name_to_module_name.put(self.buffer.items[i + 2], .{ .module_name = self.buffer.items[i], .import_name = self.buffer.items[i + 1] }); + } + i += k.len() catch unreachable; + } + } + + { + var i: usize = 0; + for (self.record_kinds.items) |*k| { + if (k.* == .export_info_local) { + if (local_name_to_module_name.get(self.buffer.items[i + 1])) |ip| { + k.* = .export_info_indirect; + self.buffer.items[i + 1] = ip.import_name; + self.buffer.items[i + 2] = ip.module_name; + } + } + i += k.len() catch unreachable; + } + } + + self._deserialized = .{ + .strings_buf = self.strings_buf.items, + .strings_lens = self.strings_lens.items, + .requested_modules_keys = self.requested_modules.keys(), + .requested_modules_values = self.requested_modules.values(), + .buffer = self.buffer.items, + .record_kinds = self.record_kinds.items, + .contains_import_meta = self.contains_import_meta, + .owner = .module_info, + }; + + self.finalized = true; + } +}; +pub const StringID = enum(u32) { + _, +}; + +export fn zig__renderDiff(expected_ptr: [*:0]const u8, expected_len: usize, received_ptr: [*:0]const u8, received_len: usize, globalThis: *bun.JSC.JSGlobalObject) void { + const DiffFormatter = @import("bun.js/test/diff_format.zig").DiffFormatter; + const formatter = DiffFormatter{ + .received_string = received_ptr[0..received_len], + .expected_string = expected_ptr[0..expected_len], + .globalThis = globalThis, + }; + const stderr = std.io.getStdErr().writer(); + stderr.print("DIFF:\n{}\n", .{formatter}) catch {}; +} + +export fn zig__ModuleInfoDeserialized__toJSModuleRecord( + globalObject: *bun.JSC.JSGlobalObject, + vm: *bun.JSC.VM, + module_key: *const IdentifierArray, + source_code: *const SourceCode, + declared_variables: *VariableEnvironment, + lexical_variables: *VariableEnvironment, + res: *ModuleInfoDeserialized, +) ?*JSModuleRecord { + if (res.dead) @panic("ModuleInfoDeserialized already deinit()ed"); + defer res.deinit(); + + var identifiers = IdentifierArray.create(res.strings_lens.len); + defer identifiers.destroy(); + var offset: usize = 0; + for (0.., res.strings_lens) |index, len| { + if (res.strings_buf.len < offset + len) return null; // error! + const sub = res.strings_buf[offset..][0..len]; + if (bun.strings.eqlComptime(sub, ModuleInfo.star_default)) { + identifiers.setFromStarDefault(index, vm); + } else { + identifiers.setFromUtf8(index, vm, sub); + } + offset += len; + } + + { + var i: usize = 0; + for (res.record_kinds) |k| { + if (i + (k.len() catch 0) > res.buffer.len) return null; + switch (k) { + .declared_variable => declared_variables.add(identifiers, res.buffer[i]), + .lexical_variable => lexical_variables.add(identifiers, res.buffer[i]), + .import_info_single, .import_info_namespace, .export_info_indirect, .export_info_local, .export_info_namespace, .export_info_star => {}, + else => return null, + } + i += k.len() catch unreachable; // handled above + } + } + + const module_record = JSModuleRecord.create(globalObject, vm, module_key, source_code, declared_variables, lexical_variables, res.contains_import_meta); + + for (res.requested_modules_keys, res.requested_modules_values) |reqk, reqv| { + switch (reqv) { + .none => module_record.addRequestedModuleNullAttributesPtr(identifiers, reqk), + .javascript => module_record.addRequestedModuleJavaScript(identifiers, reqk), + .webassembly => module_record.addRequestedModuleWebAssembly(identifiers, reqk), + .json => module_record.addRequestedModuleJSON(identifiers, reqk), + else => |uv| module_record.addRequestedModuleHostDefined(identifiers, reqk, @enumFromInt(@intFromEnum(uv))), + } + } + + { + var i: usize = 0; + for (res.record_kinds) |k| { + if (i + (k.len() catch unreachable) > res.buffer.len) unreachable; // handled above + switch (k) { + .declared_variable, .lexical_variable => {}, + .import_info_single => module_record.addImportEntrySingle(identifiers, res.buffer[i + 1], res.buffer[i + 2], res.buffer[i]), + .import_info_namespace => module_record.addImportEntryNamespace(identifiers, res.buffer[i + 1], res.buffer[i + 2], res.buffer[i]), + .export_info_indirect => module_record.addIndirectExport(identifiers, res.buffer[i + 0], res.buffer[i + 1], res.buffer[i + 2]), + .export_info_local => module_record.addLocalExport(identifiers, res.buffer[i], res.buffer[i + 1]), + .export_info_namespace => module_record.addNamespaceExport(identifiers, res.buffer[i], res.buffer[i + 1]), + .export_info_star => module_record.addStarExport(identifiers, res.buffer[i]), + else => unreachable, // handled above + } + i += k.len() catch unreachable; // handled above + } + } + + return module_record; +} +export fn zig__ModuleInfo__destroy(info: *ModuleInfo) void { + info.deinit(); + bun.default_allocator.destroy(info); +} + +const VariableEnvironment = opaque { + extern fn JSC__VariableEnvironment__add(environment: *VariableEnvironment, identifier_array: *IdentifierArray, identifier_index: StringID) void; + pub const add = JSC__VariableEnvironment__add; +}; +const IdentifierArray = opaque { + extern fn JSC__IdentifierArray__create(len: usize) *IdentifierArray; + pub const create = JSC__IdentifierArray__create; + + extern fn JSC__IdentifierArray__destroy(identifier_array: *IdentifierArray) void; + pub const destroy = JSC__IdentifierArray__destroy; + + extern fn JSC__IdentifierArray__setFromUtf8(identifier_array: *IdentifierArray, n: usize, vm: *bun.JSC.VM, str: [*]const u8, len: usize) void; + pub fn setFromUtf8(self: *IdentifierArray, n: usize, vm: *bun.JSC.VM, str: []const u8) void { + JSC__IdentifierArray__setFromUtf8(self, n, vm, str.ptr, str.len); + } + + extern fn JSC__IdentifierArray__setFromStarDefault(identifier_array: *IdentifierArray, n: usize, vm: *bun.JSC.VM) void; + pub const setFromStarDefault = JSC__IdentifierArray__setFromStarDefault; +}; +const SourceCode = opaque {}; +const JSModuleRecord = opaque { + extern fn JSC_JSModuleRecord__create(global_object: *bun.JSC.JSGlobalObject, vm: *bun.JSC.VM, module_key: *const IdentifierArray, source_code: *const SourceCode, declared_variables: *VariableEnvironment, lexical_variables: *VariableEnvironment, has_import_meta: bool) *JSModuleRecord; + pub const create = JSC_JSModuleRecord__create; + + extern fn JSC_JSModuleRecord__declaredVariables(module_record: *JSModuleRecord) *VariableEnvironment; + pub const declaredVariables = JSC_JSModuleRecord__declaredVariables; + extern fn JSC_JSModuleRecord__lexicalVariables(module_record: *JSModuleRecord) *VariableEnvironment; + pub const lexicalVariables = JSC_JSModuleRecord__lexicalVariables; + + extern fn JSC_JSModuleRecord__addIndirectExport(module_record: *JSModuleRecord, identifier_array: *IdentifierArray, export_name: StringID, import_name: StringID, module_name: StringID) void; + pub const addIndirectExport = JSC_JSModuleRecord__addIndirectExport; + extern fn JSC_JSModuleRecord__addLocalExport(module_record: *JSModuleRecord, identifier_array: *IdentifierArray, export_name: StringID, local_name: StringID) void; + pub const addLocalExport = JSC_JSModuleRecord__addLocalExport; + extern fn JSC_JSModuleRecord__addNamespaceExport(module_record: *JSModuleRecord, identifier_array: *IdentifierArray, export_name: StringID, module_name: StringID) void; + pub const addNamespaceExport = JSC_JSModuleRecord__addNamespaceExport; + extern fn JSC_JSModuleRecord__addStarExport(module_record: *JSModuleRecord, identifier_array: *IdentifierArray, module_name: StringID) void; + pub const addStarExport = JSC_JSModuleRecord__addStarExport; + + extern fn JSC_JSModuleRecord__addRequestedModuleNullAttributesPtr(module_record: *JSModuleRecord, identifier_array: *IdentifierArray, module_name: StringID) void; + pub const addRequestedModuleNullAttributesPtr = JSC_JSModuleRecord__addRequestedModuleNullAttributesPtr; + extern fn JSC_JSModuleRecord__addRequestedModuleJavaScript(module_record: *JSModuleRecord, identifier_array: *IdentifierArray, module_name: StringID) void; + pub const addRequestedModuleJavaScript = JSC_JSModuleRecord__addRequestedModuleJavaScript; + extern fn JSC_JSModuleRecord__addRequestedModuleWebAssembly(module_record: *JSModuleRecord, identifier_array: *IdentifierArray, module_name: StringID) void; + pub const addRequestedModuleWebAssembly = JSC_JSModuleRecord__addRequestedModuleWebAssembly; + extern fn JSC_JSModuleRecord__addRequestedModuleJSON(module_record: *JSModuleRecord, identifier_array: *IdentifierArray, module_name: StringID) void; + pub const addRequestedModuleJSON = JSC_JSModuleRecord__addRequestedModuleJSON; + extern fn JSC_JSModuleRecord__addRequestedModuleHostDefined(module_record: *JSModuleRecord, identifier_array: *IdentifierArray, module_name: StringID, host_defined_import_type: StringID) void; + pub const addRequestedModuleHostDefined = JSC_JSModuleRecord__addRequestedModuleHostDefined; + + extern fn JSC_JSModuleRecord__addImportEntrySingle(module_record: *JSModuleRecord, identifier_array: *IdentifierArray, import_name: StringID, local_name: StringID, module_name: StringID) void; + pub const addImportEntrySingle = JSC_JSModuleRecord__addImportEntrySingle; + extern fn JSC_JSModuleRecord__addImportEntryNamespace(module_record: *JSModuleRecord, identifier_array: *IdentifierArray, import_name: StringID, local_name: StringID, module_name: StringID) void; + pub const addImportEntryNamespace = JSC_JSModuleRecord__addImportEntryNamespace; +}; diff --git a/src/bun.js/RuntimeTranspilerCache.zig b/src/bun.js/RuntimeTranspilerCache.zig index 3eac90cbf6..0d8235d417 100644 --- a/src/bun.js/RuntimeTranspilerCache.zig +++ b/src/bun.js/RuntimeTranspilerCache.zig @@ -10,7 +10,8 @@ /// Version 11: Fix \uFFFF printing regression /// Version 12: "use strict"; makes it CommonJS if we otherwise don't know which one to pick. /// Version 13: Hoist `import.meta.require` definition, see #15738 -const expected_version = 13; +/// Version 14: Include module info with an ES Module, see #15758 +const expected_version = 14; const bun = @import("root").bun; const std = @import("std"); @@ -33,6 +34,7 @@ pub const RuntimeTranspilerCache = struct { sourcemap_allocator: std.mem.Allocator, output_code_allocator: std.mem.Allocator, + esm_record_allocator: std.mem.Allocator, const seed = 42; pub const Metadata = struct { @@ -53,6 +55,10 @@ pub const RuntimeTranspilerCache = struct { sourcemap_byte_length: u64 = 0, sourcemap_hash: u64 = 0, + esm_record_byte_offset: u64 = 0, + esm_record_byte_length: u64 = 0, + esm_record_hash: u64 = 0, + pub const size = brk: { var count: usize = 0; const meta: Metadata = .{}; @@ -79,6 +85,10 @@ pub const RuntimeTranspilerCache = struct { try writer.writeInt(u64, this.sourcemap_byte_offset, .little); try writer.writeInt(u64, this.sourcemap_byte_length, .little); try writer.writeInt(u64, this.sourcemap_hash, .little); + + try writer.writeInt(u64, this.esm_record_byte_offset, .little); + try writer.writeInt(u64, this.esm_record_byte_length, .little); + try writer.writeInt(u64, this.esm_record_hash, .little); } pub fn decode(this: *Metadata, reader: anytype) !void { @@ -103,6 +113,10 @@ pub const RuntimeTranspilerCache = struct { this.sourcemap_byte_length = try reader.readInt(u64, .little); this.sourcemap_hash = try reader.readInt(u64, .little); + this.esm_record_byte_offset = try reader.readInt(u64, .little); + this.esm_record_byte_length = try reader.readInt(u64, .little); + this.esm_record_hash = try reader.readInt(u64, .little); + switch (this.module_type) { .esm, .cjs => {}, // Invalid module type @@ -121,7 +135,7 @@ pub const RuntimeTranspilerCache = struct { metadata: Metadata, output_code: OutputCode = .{ .utf8 = "" }, sourcemap: []const u8 = "", - + esm_record: []const u8 = "", pub const OutputCode = union(enum) { utf8: []const u8, string: bun.String, @@ -143,11 +157,14 @@ pub const RuntimeTranspilerCache = struct { } }; - pub fn deinit(this: *Entry, sourcemap_allocator: std.mem.Allocator, output_code_allocator: std.mem.Allocator) void { + pub fn deinit(this: *Entry, sourcemap_allocator: std.mem.Allocator, output_code_allocator: std.mem.Allocator, esm_record_allocator: std.mem.Allocator) void { this.output_code.deinit(output_code_allocator); if (this.sourcemap.len > 0) { sourcemap_allocator.free(this.sourcemap); } + if (this.esm_record.len > 0) { + esm_record_allocator.free(this.esm_record); + } } pub fn save( @@ -157,6 +174,7 @@ pub const RuntimeTranspilerCache = struct { input_hash: u64, features_hash: u64, sourcemap: []const u8, + esm_record: []const u8, output_code: OutputCode, exports_kind: bun.JSAst.ExportsKind, ) !void { @@ -202,6 +220,8 @@ pub const RuntimeTranspilerCache = struct { .output_byte_offset = Metadata.size, .output_byte_length = output_bytes.len, .sourcemap_byte_offset = Metadata.size + output_bytes.len, + .esm_record_byte_offset = Metadata.size + output_bytes.len + sourcemap.len, + .esm_record_byte_length = esm_record.len, }; metadata.output_hash = hash(output_bytes); @@ -220,20 +240,26 @@ pub const RuntimeTranspilerCache = struct { break :brk metadata_buf[0..metadata_stream.pos]; }; - const vecs: []const bun.PlatformIOVecConst = if (output_bytes.len > 0) - &.{ - bun.platformIOVecConstCreate(metadata_bytes), - bun.platformIOVecConstCreate(output_bytes), - bun.platformIOVecConstCreate(sourcemap), - } - else - &.{ - bun.platformIOVecConstCreate(metadata_bytes), - bun.platformIOVecConstCreate(sourcemap), - }; + var vecs_buf: [4]bun.PlatformIOVecConst = undefined; + var vecs_i: usize = 0; + vecs_buf[vecs_i] = bun.platformIOVecConstCreate(metadata_bytes); + vecs_i += 1; + if (output_bytes.len > 0) { + vecs_buf[vecs_i] = bun.platformIOVecConstCreate(output_bytes); + vecs_i += 1; + } + if (sourcemap.len > 0) { + vecs_buf[vecs_i] = bun.platformIOVecConstCreate(sourcemap); + vecs_i += 1; + } + if (esm_record.len > 0) { + vecs_buf[vecs_i] = bun.platformIOVecConstCreate(esm_record); + vecs_i += 1; + } + const vecs: []const bun.PlatformIOVecConst = vecs_buf[0..vecs_i]; var position: isize = 0; - const end_position = Metadata.size + output_bytes.len + sourcemap.len; + const end_position = Metadata.size + output_bytes.len + sourcemap.len + esm_record.len; if (bun.Environment.allow_assert) { var total: usize = 0; @@ -243,7 +269,7 @@ pub const RuntimeTranspilerCache = struct { } bun.assert(end_position == total); } - bun.assert(end_position == @as(i64, @intCast(sourcemap.len + output_bytes.len + Metadata.size))); + bun.assert(end_position == @as(i64, @intCast(sourcemap.len + output_bytes.len + Metadata.size + esm_record.len))); bun.C.preallocate_file(tmpfile.fd.cast(), 0, @intCast(end_position)) catch {}; while (position < end_position) { @@ -264,6 +290,7 @@ pub const RuntimeTranspilerCache = struct { file: std.fs.File, sourcemap_allocator: std.mem.Allocator, output_code_allocator: std.mem.Allocator, + esm_record_allocator: std.mem.Allocator, ) !void { const stat_size = try file.getEndPos(); if (stat_size < Metadata.size + this.metadata.output_byte_length + this.metadata.sourcemap_byte_length) { @@ -339,6 +366,17 @@ pub const RuntimeTranspilerCache = struct { this.sourcemap = sourcemap; } + + if (this.metadata.esm_record_byte_length > 0) { + const esm_record = try esm_record_allocator.alloc(u8, this.metadata.esm_record_byte_length); + errdefer esm_record_allocator.free(esm_record); + const read_bytes = try file.preadAll(esm_record, this.metadata.esm_record_byte_offset); + if (read_bytes != this.metadata.esm_record_byte_length) { + return error.MissingData; + } + + this.esm_record = esm_record; + } } }; @@ -456,6 +494,7 @@ pub const RuntimeTranspilerCache = struct { input_stat_size: u64, sourcemap_allocator: std.mem.Allocator, output_code_allocator: std.mem.Allocator, + esm_record_allocator: std.mem.Allocator, ) !Entry { var tracer = bun.tracy.traceNamed(@src(), "RuntimeTranspilerCache.fromFile"); defer tracer.end(); @@ -470,6 +509,7 @@ pub const RuntimeTranspilerCache = struct { input_stat_size, sourcemap_allocator, output_code_allocator, + esm_record_allocator, ); } @@ -480,6 +520,7 @@ pub const RuntimeTranspilerCache = struct { input_stat_size: u64, sourcemap_allocator: std.mem.Allocator, output_code_allocator: std.mem.Allocator, + esm_record_allocator: std.mem.Allocator, ) !Entry { var metadata_bytes_buf: [Metadata.size * 2]u8 = undefined; const cache_fd = try bun.sys.open(cache_file_path.sliceAssumeZ(), bun.O.RDONLY, 0).unwrap(); @@ -511,7 +552,7 @@ pub const RuntimeTranspilerCache = struct { return error.MismatchedFeatureHash; } - try entry.load(file, sourcemap_allocator, output_code_allocator); + try entry.load(file, sourcemap_allocator, output_code_allocator, esm_record_allocator); return entry; } @@ -528,6 +569,7 @@ pub const RuntimeTranspilerCache = struct { input_hash: u64, features_hash: u64, sourcemap: []const u8, + esm_record: []const u8, source_code: bun.String, exports_kind: bun.JSAst.ExportsKind, ) !void { @@ -567,6 +609,7 @@ pub const RuntimeTranspilerCache = struct { input_hash, features_hash, sourcemap, + esm_record, output_code, exports_kind, ); @@ -600,7 +643,7 @@ pub const RuntimeTranspilerCache = struct { parser_options.hashForRuntimeTranspiler(&features_hasher, used_jsx); this.features_hash = features_hasher.final(); - this.entry = fromFile(input_hash, this.features_hash.?, source.contents.len, this.sourcemap_allocator, this.output_code_allocator) catch |err| { + this.entry = fromFile(input_hash, this.features_hash.?, source.contents.len, this.sourcemap_allocator, this.output_code_allocator, this.esm_record_allocator) catch |err| { debug("get(\"{s}\") = {s}", .{ source.path.text, @errorName(err) }); return false; }; @@ -616,7 +659,7 @@ pub const RuntimeTranspilerCache = struct { if (comptime bun.Environment.isDebug) { if (!bun_debug_restore_from_cache) { if (this.entry) |*entry| { - entry.deinit(this.sourcemap_allocator, this.output_code_allocator); + entry.deinit(this.sourcemap_allocator, this.output_code_allocator, this.esm_record_allocator); this.entry = null; } } @@ -625,7 +668,7 @@ pub const RuntimeTranspilerCache = struct { return this.entry != null; } - pub fn put(this: *RuntimeTranspilerCache, output_code_bytes: []const u8, sourcemap: []const u8) void { + pub fn put(this: *RuntimeTranspilerCache, output_code_bytes: []const u8, sourcemap: []const u8, esm_record: []const u8) void { if (comptime !bun.FeatureFlags.runtime_transpiler_cache) @compileError("RuntimeTranspilerCache is disabled"); @@ -636,7 +679,7 @@ pub const RuntimeTranspilerCache = struct { const output_code = bun.String.createLatin1(output_code_bytes); this.output_code = output_code; - toFile(this.input_byte_length.?, this.input_hash.?, this.features_hash.?, sourcemap, output_code, this.exports_kind) catch |err| { + toFile(this.input_byte_length.?, this.input_hash.?, this.features_hash.?, sourcemap, esm_record, output_code, this.exports_kind) catch |err| { debug("put() = {s}", .{@errorName(err)}); return; }; diff --git a/src/bun.js/bindings/BunAnalyzeTranspiledModule.cpp b/src/bun.js/bindings/BunAnalyzeTranspiledModule.cpp new file mode 100644 index 0000000000..4268358a65 --- /dev/null +++ b/src/bun.js/bindings/BunAnalyzeTranspiledModule.cpp @@ -0,0 +1,313 @@ +#include "root.h" + +#include "JavaScriptCore/JSInternalPromise.h" +#include "JavaScriptCore/JSModuleRecord.h" +#include "JavaScriptCore/GlobalObjectMethodTable.h" +#include "JavaScriptCore/JSModuleRecord.h" +#include "JavaScriptCore/Nodes.h" +#include "JavaScriptCore/Parser.h" +#include "JavaScriptCore/ParserError.h" +#include "JavaScriptCore/SyntheticModuleRecord.h" +#include +#include "JavaScriptCore/JSGlobalObject.h" +#include "JavaScriptCore/JSModuleRecord.h" +#include "JavaScriptCore/ExceptionScope.h" +#include "ZigSourceProvider.h" +#include "BunAnalyzeTranspiledModule.h" + +// ref: JSModuleLoader.cpp +// ref: ModuleAnalyzer.cpp +// ref: JSModuleRecord.cpp +// ref: NodesAnalyzeModule.cpp, search ::analyzeModule + +// TODO: #include "JavaScriptCore/parser/ModuleAnalyzer.h" +#include "JavaScriptCore/ErrorType.h" +#include "JavaScriptCore/Nodes.h" + +namespace JSC { + +class JSModuleRecord; +class SourceCode; +class ScriptFetchParameters; + +class ModuleAnalyzer { + WTF_MAKE_NONCOPYABLE(ModuleAnalyzer); + WTF_FORBID_HEAP_ALLOCATION; + +public: + ModuleAnalyzer(JSGlobalObject*, const Identifier& moduleKey, const SourceCode&, const VariableEnvironment& declaredVariables, const VariableEnvironment& lexicalVariables, CodeFeatures); + + Expected> analyze(ModuleProgramNode&); + + VM& vm() { return m_vm; } + + JSModuleRecord* moduleRecord() { return m_moduleRecord; } + + void appendRequestedModule(const Identifier&, RefPtr&&); + + void fail(std::tuple&& errorMessage) { m_errorMessage = errorMessage; } + +private: + void exportVariable(ModuleProgramNode&, const RefPtr&, const VariableEnvironmentEntry&); + + VM& m_vm; + JSModuleRecord* m_moduleRecord; + IdentifierSet m_requestedModules; + std::tuple m_errorMessage; +}; + +} + +namespace JSC { + +String dumpRecordInfo(JSModuleRecord* moduleRecord); + +extern "C" JSModuleRecord* zig__ModuleInfoDeserialized__toJSModuleRecord(JSGlobalObject* globalObject, VM& vm, const Identifier& module_key, const SourceCode& source_code, VariableEnvironment& declared_variables, VariableEnvironment& lexical_variables, bun_ModuleInfoDeserialized* module_info); +extern "C" void zig__renderDiff(const char* expected_ptr, size_t expected_len, const char* received_ptr, size_t received_len, JSGlobalObject* globalObject); + +extern "C" Identifier* JSC__IdentifierArray__create(size_t len) +{ + return new Identifier[len]; +} +extern "C" void JSC__IdentifierArray__destroy(Identifier* identifier) +{ + delete[] identifier; +} +extern "C" void JSC__IdentifierArray__setFromUtf8(Identifier* identifierArray, size_t n, VM& vm, char* str, size_t len) +{ + identifierArray[n] = Identifier::fromString(vm, AtomString::fromUTF8(std::span(str, len))); +} +extern "C" void JSC__IdentifierArray__setFromStarDefault(Identifier* identifierArray, size_t n, VM& vm) +{ + identifierArray[n] = vm.propertyNames->starDefaultPrivateName; +} + +extern "C" void JSC__VariableEnvironment__add(VariableEnvironment& environment, Identifier* identifierArray, uint32_t index) +{ + environment.add(identifierArray[index]); +} + +extern "C" VariableEnvironment* JSC_JSModuleRecord__declaredVariables(JSModuleRecord* moduleRecord) +{ + return &moduleRecord->m_declaredVariables; +} +extern "C" VariableEnvironment* JSC_JSModuleRecord__lexicalVariables(JSModuleRecord* moduleRecord) +{ + return &moduleRecord->m_lexicalVariables; +} + +extern "C" JSModuleRecord* JSC_JSModuleRecord__create(JSGlobalObject* globalObject, VM& vm, const Identifier* moduleKey, const SourceCode& sourceCode, const VariableEnvironment& declaredVariables, const VariableEnvironment& lexicalVariables, bool hasImportMeta) +{ + return JSModuleRecord::create(globalObject, vm, globalObject->moduleRecordStructure(), moduleKey[0], sourceCode, declaredVariables, lexicalVariables, hasImportMeta ? ImportMetaFeature : 0); +} + +extern "C" void JSC_JSModuleRecord__addIndirectExport(JSModuleRecord* moduleRecord, Identifier* identifierArray, uint32_t exportName, uint32_t importName, uint32_t moduleName) +{ + moduleRecord->addExportEntry(JSModuleRecord::ExportEntry::createIndirect(identifierArray[exportName], identifierArray[importName], identifierArray[moduleName])); +} +extern "C" void JSC_JSModuleRecord__addLocalExport(JSModuleRecord* moduleRecord, Identifier* identifierArray, uint32_t exportName, uint32_t localName) +{ + moduleRecord->addExportEntry(JSModuleRecord::ExportEntry::createLocal(identifierArray[exportName], identifierArray[localName])); +} +extern "C" void JSC_JSModuleRecord__addNamespaceExport(JSModuleRecord* moduleRecord, Identifier* identifierArray, uint32_t exportName, uint32_t moduleName) +{ + moduleRecord->addExportEntry(JSModuleRecord::ExportEntry::createNamespace(identifierArray[exportName], identifierArray[moduleName])); +} +extern "C" void JSC_JSModuleRecord__addStarExport(JSModuleRecord* moduleRecord, Identifier* identifierArray, uint32_t moduleName) +{ + moduleRecord->addStarExportEntry(identifierArray[moduleName]); +} +extern "C" void JSC_JSModuleRecord__addRequestedModuleNullAttributesPtr(JSModuleRecord* moduleRecord, Identifier* identifierArray, uint32_t moduleName) +{ + RefPtr attributes = RefPtr {}; + moduleRecord->appendRequestedModule(identifierArray[moduleName], WTFMove(attributes)); +} +extern "C" void JSC_JSModuleRecord__addRequestedModuleJavaScript(JSModuleRecord* moduleRecord, Identifier* identifierArray, uint32_t moduleName) +{ + Ref attributes = ScriptFetchParameters::create(ScriptFetchParameters::Type::JavaScript); + moduleRecord->appendRequestedModule(identifierArray[moduleName], WTFMove(attributes)); +} +extern "C" void JSC_JSModuleRecord__addRequestedModuleWebAssembly(JSModuleRecord* moduleRecord, Identifier* identifierArray, uint32_t moduleName) +{ + Ref attributes = ScriptFetchParameters::create(ScriptFetchParameters::Type::WebAssembly); + moduleRecord->appendRequestedModule(identifierArray[moduleName], WTFMove(attributes)); +} +extern "C" void JSC_JSModuleRecord__addRequestedModuleJSON(JSModuleRecord* moduleRecord, Identifier* identifierArray, uint32_t moduleName) +{ + Ref attributes = ScriptFetchParameters::create(ScriptFetchParameters::Type::JSON); + moduleRecord->appendRequestedModule(identifierArray[moduleName], WTFMove(attributes)); +} +extern "C" void JSC_JSModuleRecord__addRequestedModuleHostDefined(JSModuleRecord* moduleRecord, Identifier* identifierArray, uint32_t moduleName, uint32_t hostDefinedImportType) +{ + Ref attributes = ScriptFetchParameters::create(identifierArray[hostDefinedImportType].string()); + moduleRecord->appendRequestedModule(identifierArray[moduleName], WTFMove(attributes)); +} + +extern "C" void JSC_JSModuleRecord__addImportEntrySingle(JSModuleRecord* moduleRecord, Identifier* identifierArray, uint32_t importName, uint32_t localName, uint32_t moduleName) +{ + moduleRecord->addImportEntry(JSModuleRecord::ImportEntry { + .type = JSModuleRecord::ImportEntryType::Single, + .moduleRequest = identifierArray[moduleName], + .importName = identifierArray[importName], + .localName = identifierArray[localName], + }); +} +extern "C" void JSC_JSModuleRecord__addImportEntryNamespace(JSModuleRecord* moduleRecord, Identifier* identifierArray, uint32_t importName, uint32_t localName, uint32_t moduleName) +{ + moduleRecord->addImportEntry(JSModuleRecord::ImportEntry { + .type = JSModuleRecord::ImportEntryType::Namespace, + .moduleRequest = identifierArray[moduleName], + .importName = identifierArray[importName], + .localName = identifierArray[localName], + }); +} + +static EncodedJSValue fallbackParse(JSGlobalObject* globalObject, const Identifier& moduleKey, const SourceCode& sourceCode, JSInternalPromise* promise, JSModuleRecord* resultValue = nullptr); +extern "C" EncodedJSValue Bun__analyzeTranspiledModule(JSGlobalObject* globalObject, const Identifier& moduleKey, const SourceCode& sourceCode, JSInternalPromise* promise) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto rejectWithError = [&](JSValue error) { + promise->reject(globalObject, error); + return promise; + }; + + VariableEnvironment declaredVariables = VariableEnvironment(); + VariableEnvironment lexicalVariables = VariableEnvironment(); + + auto provider = static_cast(sourceCode.provider()); + + if (provider->m_resolvedSource.module_info == nullptr) { + dataLog("[note] module_info is null for module: ", moduleKey.utf8(), "\n"); + RELEASE_AND_RETURN(scope, JSValue::encode(rejectWithError(createError(globalObject, WTF::String::fromLatin1("module_info is null"))))); + } + + auto moduleRecord = zig__ModuleInfoDeserialized__toJSModuleRecord(globalObject, vm, moduleKey, sourceCode, declaredVariables, lexicalVariables, provider->m_resolvedSource.module_info); + if (moduleRecord == nullptr) { + RELEASE_AND_RETURN(scope, JSValue::encode(rejectWithError(createError(globalObject, WTF::String::fromLatin1("parseFromSourceCode failed"))))); + } + +#ifdef DEBUG + RELEASE_AND_RETURN(scope, fallbackParse(globalObject, moduleKey, sourceCode, promise, moduleRecord)); +#else + promise->fulfillWithNonPromise(globalObject, moduleRecord); + RELEASE_AND_RETURN(scope, JSValue::encode(promise)); +#endif +} +static EncodedJSValue fallbackParse(JSGlobalObject* globalObject, const Identifier& moduleKey, const SourceCode& sourceCode, JSInternalPromise* promise, JSModuleRecord* resultValue) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + auto rejectWithError = [&](JSValue error) { + promise->reject(globalObject, error); + return promise; + }; + + ParserError error; + std::unique_ptr moduleProgramNode = parseRootNode( + vm, sourceCode, ImplementationVisibility::Public, JSParserBuiltinMode::NotBuiltin, + StrictModeLexicallyScopedFeature, JSParserScriptMode::Module, SourceParseMode::ModuleAnalyzeMode, error); + if (error.isValid()) + RELEASE_AND_RETURN(scope, JSValue::encode(rejectWithError(error.toErrorObject(globalObject, sourceCode)))); + ASSERT(moduleProgramNode); + + ModuleAnalyzer ModuleAnalyzer(globalObject, moduleKey, sourceCode, moduleProgramNode->varDeclarations(), moduleProgramNode->lexicalVariables(), moduleProgramNode->features()); + RETURN_IF_EXCEPTION(scope, JSValue::encode(promise->rejectWithCaughtException(globalObject, scope))); + + auto result = ModuleAnalyzer.analyze(*moduleProgramNode); + if (!result) { + auto [errorType, message] = WTFMove(result.error()); + RELEASE_AND_RETURN(scope, JSValue::encode(rejectWithError(createError(globalObject, errorType, message)))); + } + + JSModuleRecord* moduleRecord = result.value(); + + if (resultValue != nullptr) { + auto actual = dumpRecordInfo(resultValue); + auto expected = dumpRecordInfo(moduleRecord); + if (actual != expected) { + dataLog("\n\n\n\n\n\n\x1b[95mBEGIN analyzeTranspiledModule\x1b(B\x1b[m\n ---code---\n\n", sourceCode.toUTF8().data(), "\n"); + dataLog(" ------", "\n"); + dataLog(" BunAnalyzeTranspiledModule:", "\n"); + + zig__renderDiff(expected.utf8().data(), expected.utf8().length(), actual.utf8().data(), actual.utf8().length(), globalObject); + + RELEASE_AND_RETURN(scope, JSValue::encode(rejectWithError(createError(globalObject, WTF::String::fromLatin1("Imports different between parseFromSourceCode and fallbackParse"))))); + } + } + + scope.release(); + promise->fulfillWithNonPromise(globalObject, resultValue == nullptr ? moduleRecord : resultValue); + return JSValue::encode(promise); +} + +String dumpRecordInfo(JSModuleRecord* moduleRecord) +{ + WTF::StringPrintStream stream; + + stream.print(" varDeclarations:\n"); + for (const auto& pair : moduleRecord->m_declaredVariables) { + stream.print(" - ", pair.key, "\n"); + } + + stream.print(" lexicalVariables:\n"); + for (const auto& pair : moduleRecord->m_lexicalVariables) { + stream.print(" - ", pair.key, "\n"); + } + + stream.print(" features: "); + stream.print(moduleRecord->m_features & ImportMetaFeature); + stream.print("\n"); + + stream.print("\nAnalyzing ModuleRecord key(", moduleRecord->moduleKey().impl(), ")\n"); + + stream.print(" Dependencies: ", moduleRecord->requestedModules().size(), " modules\n"); + for (const auto& request : moduleRecord->requestedModules()) + if (request.m_attributes == nullptr) { + stream.print(" module(", request.m_specifier, ")\n"); + } else { + stream.print(" module(", request.m_specifier, "),attributes(", (uint8_t)request.m_attributes->type(), ", ", request.m_attributes->hostDefinedImportType(), ")\n"); + } + + stream.print(" Import: ", moduleRecord->importEntries().size(), " entries\n"); + for (const auto& pair : moduleRecord->importEntries()) { + auto& importEntry = pair.value; + stream.print(" import(", importEntry.importName, "), local(", importEntry.localName, "), module(", importEntry.moduleRequest, ")\n"); + } + + stream.print(" Export: ", moduleRecord->exportEntries().size(), " entries\n"); + Vector sortedEntries; + for (const auto& pair : moduleRecord->exportEntries()) { + WTF::StringPrintStream line; + auto& exportEntry = pair.value; + switch (exportEntry.type) { + case AbstractModuleRecord::ExportEntry::Type::Local: + line.print(" [Local] ", "export(", exportEntry.exportName, "), local(", exportEntry.localName, ")\n"); + break; + + case AbstractModuleRecord::ExportEntry::Type::Indirect: + line.print(" [Indirect] ", "export(", exportEntry.exportName, "), import(", exportEntry.importName, "), module(", exportEntry.moduleName, ")\n"); + break; + + case AbstractModuleRecord::ExportEntry::Type::Namespace: + line.print(" [Namespace] ", "export(", exportEntry.exportName, "), module(", exportEntry.moduleName, ")\n"); + break; + } + sortedEntries.append(line.toString()); + } + std::sort(sortedEntries.begin(), sortedEntries.end(), [](const String& a, const String& b) { + return a.utf8().toStdString() < b.utf8().toStdString(); + }); + for (const auto& entry : sortedEntries) + stream.print(entry); + + for (const auto& moduleName : moduleRecord->starExportEntries()) + stream.print(" [Star] module(", moduleName.get(), ")\n"); + + stream.print(" -> done\n"); + + return stream.toString(); +} + +} diff --git a/src/bun.js/bindings/BunAnalyzeTranspiledModule.h b/src/bun.js/bindings/BunAnalyzeTranspiledModule.h new file mode 100644 index 0000000000..9a4e055d0d --- /dev/null +++ b/src/bun.js/bindings/BunAnalyzeTranspiledModule.h @@ -0,0 +1 @@ +struct bun_ModuleInfoDeserialized; diff --git a/src/bun.js/bindings/CommonJSModuleRecord.cpp b/src/bun.js/bindings/CommonJSModuleRecord.cpp index 5fb4cdb9d2..bac55fc1dc 100644 --- a/src/bun.js/bindings/CommonJSModuleRecord.cpp +++ b/src/bun.js/bindings/CommonJSModuleRecord.cpp @@ -1145,7 +1145,7 @@ bool JSCommonJSModule::evaluate( bool isBuiltIn) { auto& vm = globalObject->vm(); - auto sourceProvider = Zig::SourceProvider::create(jsCast(globalObject), source, JSC::SourceProviderSourceType::Program, isBuiltIn); + auto sourceProvider = Zig::SourceProvider::create(jsCast(globalObject), source, isBuiltIn); this->ignoreESModuleAnnotation = source.tag == ResolvedSourceTagPackageJSONTypeModule; if (this->hasEvaluated) return true; @@ -1199,7 +1199,7 @@ std::optional createCommonJSModule( dirname = jsEmptyString(vm); } - auto sourceProvider = Zig::SourceProvider::create(jsCast(globalObject), source, JSC::SourceProviderSourceType::Program, isBuiltIn); + auto sourceProvider = Zig::SourceProvider::create(jsCast(globalObject), source, isBuiltIn); sourceOrigin = sourceProvider->sourceOrigin(); moduleObject = JSCommonJSModule::create( vm, diff --git a/src/bun.js/bindings/ModuleLoader.cpp b/src/bun.js/bindings/ModuleLoader.cpp index bb0653d1e8..bb116b0662 100644 --- a/src/bun.js/bindings/ModuleLoader.cpp +++ b/src/bun.js/bindings/ModuleLoader.cpp @@ -745,7 +745,7 @@ static JSValue fetchESMSourceCode( auto tag = res->result.value.tag; switch (tag) { case SyntheticModuleType::ESM: { - auto&& provider = Zig::SourceProvider::create(globalObject, res->result.value, JSC::SourceProviderSourceType::Module, true); + auto&& provider = Zig::SourceProvider::create(globalObject, res->result.value, true); return rejectOrResolve(JSSourceCode::create(vm, JSC::SourceCode(provider))); } @@ -764,7 +764,7 @@ static JSValue fetchESMSourceCode( auto source = JSC::SourceCode(JSC::SyntheticSourceProvider::create(generateInternalModuleSourceCode(globalObject, static_cast(tag & mask)), JSC::SourceOrigin(URL(makeString("builtins://"_s, moduleKey))), moduleKey)); return rejectOrResolve(JSSourceCode::create(vm, WTFMove(source))); } else { - auto&& provider = Zig::SourceProvider::create(globalObject, res->result.value, JSC::SourceProviderSourceType::Module, true); + auto&& provider = Zig::SourceProvider::create(globalObject, res->result.value, true); return rejectOrResolve(JSC::JSSourceCode::create(vm, JSC::SourceCode(provider))); } } diff --git a/src/bun.js/bindings/ZigSourceProvider.cpp b/src/bun.js/bindings/ZigSourceProvider.cpp index acb4f530f1..e6914bcd88 100644 --- a/src/bun.js/bindings/ZigSourceProvider.cpp +++ b/src/bun.js/bindings/ZigSourceProvider.cpp @@ -75,9 +75,17 @@ extern "C" void Bun__removeSourceProviderSourceMap(void* bun_vm, SourceProvider* Ref SourceProvider::create( Zig::GlobalObject* globalObject, ResolvedSource& resolvedSource, - JSC::SourceProviderSourceType sourceType, bool isBuiltin) { + + JSC::SourceProviderSourceType sourceType = JSC::SourceProviderSourceType::BunTranspiledModule; + if (resolvedSource.isCommonJSModule) { + ASSERT(resolvedSource.module_info == nullptr, "isCommonJSModule should not have module_info"); + sourceType = JSC::SourceProviderSourceType::Program; + } else if (resolvedSource.module_info == nullptr) { + sourceType = JSC::SourceProviderSourceType::Module; + } + auto string = resolvedSource.source_code.toWTFString(BunString::ZeroCopy); auto sourceURLString = resolvedSource.source_url.toWTFString(BunString::ZeroCopy); diff --git a/src/bun.js/bindings/ZigSourceProvider.h b/src/bun.js/bindings/ZigSourceProvider.h index eddb638665..73e0fbb88c 100644 --- a/src/bun.js/bindings/ZigSourceProvider.h +++ b/src/bun.js/bindings/ZigSourceProvider.h @@ -39,7 +39,6 @@ public: static Ref create( Zig::GlobalObject*, ResolvedSource& resolvedSource, - JSC::SourceProviderSourceType sourceType = JSC::SourceProviderSourceType::Module, bool isBuiltIn = false); ~SourceProvider(); unsigned hash() const override; diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index 1d88a6d235..e804c0c8d3 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -7062,3 +7062,7 @@ pub const DeferredError = struct { return err; } }; + +comptime { + _ = @import("../../analyze_transpiled_module.zig"); +} diff --git a/src/bun.js/bindings/exports.zig b/src/bun.js/bindings/exports.zig index a6b716c523..5bcb682cb3 100644 --- a/src/bun.js/bindings/exports.zig +++ b/src/bun.js/bindings/exports.zig @@ -205,6 +205,7 @@ pub fn Errorable(comptime Type: type) type { }; } +/// must be kept in sync with `ResolvedSource` in `headers-handwritten.h` pub const ResolvedSource = extern struct { pub const shim = Shimmer("Zig", "ResolvedSource", @This()); pub const name = "ResolvedSource"; @@ -234,6 +235,14 @@ pub const ResolvedSource = extern struct { bytecode_cache: ?[*]u8 = null, bytecode_cache_size: usize = 0, + /// - for esm: null means to use jsc's regular parsing step. more info: https://github.com/oven-sh/bun/pull/15758 + /// - for cjs: must be null + module_info: ?*@import("../../analyze_transpiled_module.zig").ModuleInfoDeserialized, + + pub const unfilled = ResolvedSource{ + .module_info = null, + }; + pub const Tag = @import("ResolvedSourceTag").ResolvedSourceTag; }; diff --git a/src/bun.js/bindings/headers-handwritten.h b/src/bun.js/bindings/headers-handwritten.h index 01c5c1a978..72c96c46aa 100644 --- a/src/bun.js/bindings/headers-handwritten.h +++ b/src/bun.js/bindings/headers-handwritten.h @@ -3,6 +3,7 @@ #include "wtf/text/OrdinalNumber.h" #include "JavaScriptCore/JSCJSValue.h" #include "JavaScriptCore/ArgList.h" +#include "BunAnalyzeTranspiledModule.h" #include #ifndef HEADERS_HANDWRITTEN @@ -107,6 +108,7 @@ typedef struct ResolvedSource { bool already_bundled; uint8_t* bytecode_cache; size_t bytecode_cache_size; + bun_ModuleInfoDeserialized* module_info; } ResolvedSource; static const uint32_t ResolvedSourceTagPackageJSONTypeModule = 1; typedef union ErrorableResolvedSourceResult { diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index 41d5c1ba2f..bb03ba2415 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -2336,6 +2336,7 @@ pub const VirtualMachine = struct { .hash = 0, .allocator = null, .source_code_needs_deref = false, + .module_info = null, }; } var source = this.refCountedString(code, hash_, !add_double_ref); @@ -2351,6 +2352,7 @@ pub const VirtualMachine = struct { .hash = source.hash, .allocator = source, .source_code_needs_deref = false, + .module_info = null, }; } diff --git a/src/bun.js/module_loader.zig b/src/bun.js/module_loader.zig index fe982165b3..dcb8327de4 100644 --- a/src/bun.js/module_loader.zig +++ b/src/bun.js/module_loader.zig @@ -13,6 +13,8 @@ const default_allocator = bun.default_allocator; const StoredFileDescriptorType = bun.StoredFileDescriptorType; const Arena = @import("../allocators/mimalloc_arena.zig").Arena; const C = bun.C; +const analyze_transpiled_module = @import("../analyze_transpiled_module.zig"); +const ModuleInfo = analyze_transpiled_module.ModuleInfo; const Allocator = std.mem.Allocator; const IdentityContext = @import("../identity_context.zig").IdentityContext; @@ -97,6 +99,7 @@ inline fn jsSyntheticModule(comptime name: ResolvedSource.Tag, specifier: String .hash = 0, .tag = name, .source_code_needs_deref = false, + .module_info = null, }; } @@ -286,7 +289,7 @@ pub const RuntimeTranspilerStore = struct { generation_number: u32 = 0, log: logger.Log, parse_error: ?anyerror = null, - resolved_source: ResolvedSource = ResolvedSource{}, + resolved_source: ResolvedSource = ResolvedSource.unfilled, work_task: JSC.WorkPoolTask = .{ .callback = runFromWorkerThread }, next: ?*TranspilerJob = null, @@ -409,6 +412,7 @@ pub const RuntimeTranspilerStore = struct { var cache = JSC.RuntimeTranspilerCache{ .output_code_allocator = allocator, .sourcemap_allocator = bun.default_allocator, + .esm_record_allocator = bun.default_allocator, }; var vm = this.vm; @@ -554,6 +558,19 @@ pub const RuntimeTranspilerStore = struct { dumpSourceString(vm, specifier, entry.output_code.byteSlice()); } + var module_info: ?*analyze_transpiled_module.ModuleInfoDeserialized = null; + if (entry.esm_record.len > 0) { + if (entry.metadata.module_type == .cjs) { + @panic("TranspilerCache contained cjs module with module info"); + } + module_info = analyze_transpiled_module.ModuleInfoDeserialized.create(entry.esm_record, bun.default_allocator) catch |e| switch (e) { + error.OutOfMemory => bun.outOfMemory(), + // uh oh! invalid module info in cache + // (not sure what to do here) + error.BadModuleInfo => @panic("TranspilerCache contained invalid module info"), + }; + } + this.resolved_source = ResolvedSource{ .allocator = null, .source_code = switch (entry.output_code) { @@ -569,6 +586,7 @@ pub const RuntimeTranspilerStore = struct { .source_url = duped.createIfDifferent(path.text), .hash = 0, .is_commonjs_module = entry.metadata.module_type == .cjs, + .module_info = module_info, }; return; @@ -587,6 +605,7 @@ pub const RuntimeTranspilerStore = struct { .bytecode_cache = if (bytecode_slice.len > 0) bytecode_slice.ptr else null, .bytecode_cache_size = bytecode_slice.len, .is_commonjs_module = parse_result.already_bundled.isCommonJS(), + .module_info = null, }; this.resolved_source.source_code.ensureHash(); return; @@ -639,6 +658,10 @@ pub const RuntimeTranspilerStore = struct { var printer = source_code_printer.?.*; printer.ctx.reset(); + const is_commonjs_module = parse_result.ast.has_commonjs_export_names or parse_result.ast.exports_kind == .cjs; + const module_info: ?*ModuleInfo = if (is_commonjs_module) null else ModuleInfo.create(bun.default_allocator) catch bun.outOfMemory(); + // defer module_info.destroy(); // TODO: do not leak module_info + { var mapper = vm.sourceMapHandler(&printer); defer source_code_printer.?.* = printer; @@ -648,6 +671,7 @@ pub const RuntimeTranspilerStore = struct { &printer, .esm_ascii, mapper.get(), + module_info, ) catch |err| { this.parse_error = err; return; @@ -681,13 +705,15 @@ pub const RuntimeTranspilerStore = struct { break :brk result; }; + this.resolved_source = ResolvedSource{ .allocator = null, .source_code = source_code, .specifier = duped, .source_url = duped.createIfDifferent(path.text), - .is_commonjs_module = parse_result.ast.has_commonjs_export_names or parse_result.ast.exports_kind == .cjs, + .is_commonjs_module = is_commonjs_module, .hash = 0, + .module_info = if (module_info) |mi| mi.asDeserialized() else null, }; } }; @@ -1406,6 +1432,10 @@ pub const ModuleLoader = struct { var printer = VirtualMachine.source_code_printer.?.*; printer.ctx.reset(); + const is_commonjs_module = parse_result.ast.has_commonjs_export_names or parse_result.ast.exports_kind == .cjs; + const module_info: ?*ModuleInfo = if (is_commonjs_module) null else ModuleInfo.create(bun.default_allocator) catch bun.outOfMemory(); + // defer module_info.destroy(); // TODO: do not leak module_info + { var mapper = jsc_vm.sourceMapHandler(&printer); defer VirtualMachine.source_code_printer.?.* = printer; @@ -1415,6 +1445,7 @@ pub const ModuleLoader = struct { &printer, .esm_ascii, mapper.get(), + module_info, ); } @@ -1439,7 +1470,7 @@ pub const ModuleLoader = struct { } } - resolved_source.is_commonjs_module = parse_result.ast.has_commonjs_export_names or parse_result.ast.exports_kind == .cjs; + resolved_source.is_commonjs_module = is_commonjs_module; return resolved_source; } @@ -1449,9 +1480,10 @@ pub const ModuleLoader = struct { .source_code = bun.String.createLatin1(printer.ctx.getWritten()), .specifier = String.init(specifier), .source_url = String.init(path.text), - .is_commonjs_module = parse_result.ast.has_commonjs_export_names or parse_result.ast.exports_kind == .cjs, + .is_commonjs_module = is_commonjs_module, .hash = 0, + .module_info = if (module_info) |mi| mi.asDeserialized() else null, }; } @@ -1513,6 +1545,7 @@ pub const ModuleLoader = struct { .specifier = input_specifier, .source_url = input_specifier.createIfDifferent(path.text), .hash = 0, + .module_info = null, }; } } @@ -1580,6 +1613,7 @@ pub const ModuleLoader = struct { var cache = JSC.RuntimeTranspilerCache{ .output_code_allocator = allocator, .sourcemap_allocator = bun.default_allocator, + .esm_record_allocator = bun.default_allocator, }; const old = jsc_vm.transpiler.log; @@ -1746,6 +1780,7 @@ pub const ModuleLoader = struct { .hash = 0, .tag = ResolvedSource.Tag.json_for_object_loader, + .module_info = null, }; } @@ -1760,6 +1795,7 @@ pub const ModuleLoader = struct { .specifier = input_specifier, .source_url = input_specifier.createIfDifferent(path.text), .hash = 0, + .module_info = null, }; } @@ -1772,6 +1808,7 @@ pub const ModuleLoader = struct { .hash = 0, .jsvalue_for_export = JSC.JSValue.createEmptyObject(jsc_vm.global, 0), .tag = .exports_object, + .module_info = null, }; } @@ -1782,6 +1819,7 @@ pub const ModuleLoader = struct { .hash = 0, .jsvalue_for_export = parse_result.ast.parts.@"[0]"().stmts[0].data.s_expr.value.toJS(allocator, globalObject orelse jsc_vm.global) catch |e| panic("Unexpected JS error: {s}", .{@errorName(e)}), .tag = .exports_object, + .module_info = null, }; } @@ -1797,6 +1835,7 @@ pub const ModuleLoader = struct { .bytecode_cache = if (bytecode_slice.len > 0) bytecode_slice.ptr else null, .bytecode_cache_size = if (bytecode_slice.len > 0) bytecode_slice.len else 0, .is_commonjs_module = parse_result.already_bundled.isCommonJS(), + .module_info = null, }; } @@ -1810,6 +1849,19 @@ pub const ModuleLoader = struct { dumpSourceString(jsc_vm, specifier, entry.output_code.byteSlice()); } + var module_info: ?*analyze_transpiled_module.ModuleInfoDeserialized = null; + if (entry.esm_record.len > 0) { + if (entry.metadata.module_type == .cjs) { + @panic("TranspilerCache contained cjs module with module info"); + } + module_info = analyze_transpiled_module.ModuleInfoDeserialized.create(entry.esm_record, bun.default_allocator) catch |e| switch (e) { + error.OutOfMemory => bun.outOfMemory(), + // uh oh! invalid module info in cache + // (not sure what to do here) + error.BadModuleInfo => @panic("TranspilerCache contained invalid module info"), + }; + } + return ResolvedSource{ .allocator = null, .source_code = switch (entry.output_code) { @@ -1842,6 +1894,7 @@ pub const ModuleLoader = struct { break :brk ResolvedSource.Tag.javascript; }, + .module_info = module_info, }; } @@ -1896,6 +1949,11 @@ pub const ModuleLoader = struct { var printer = source_code_printer.*; printer.ctx.reset(); defer source_code_printer.* = printer; + + const is_commonjs_module = parse_result.ast.has_commonjs_export_names or parse_result.ast.exports_kind == .cjs; + const module_info: ?*ModuleInfo = if (is_commonjs_module) null else ModuleInfo.create(bun.default_allocator) catch bun.outOfMemory(); + // defer module_info.destroy(); // TODO: do not leak module_info + _ = brk: { var mapper = jsc_vm.sourceMapHandler(&printer); @@ -1905,6 +1963,7 @@ pub const ModuleLoader = struct { &printer, .esm_ascii, mapper.get(), + module_info, ); }; @@ -1957,9 +2016,10 @@ pub const ModuleLoader = struct { }, .specifier = input_specifier, .source_url = input_specifier.createIfDifferent(path.text), - .is_commonjs_module = parse_result.ast.has_commonjs_export_names or parse_result.ast.exports_kind == .cjs, + .is_commonjs_module = is_commonjs_module, .hash = 0, .tag = tag, + .module_info = if (module_info) |mi| mi.asDeserialized() else null, }; }, // provideFetch() should be called @@ -2028,6 +2088,7 @@ pub const ModuleLoader = struct { .source_url = input_specifier.createIfDifferent(path.text), .tag = .esm, .hash = 0, + .module_info = null, }; } @@ -2088,6 +2149,7 @@ pub const ModuleLoader = struct { .source_url = input_specifier.createIfDifferent(path.text), .tag = .esm, .hash = 0, + .module_info = null, }; }, @@ -2169,6 +2231,7 @@ pub const ModuleLoader = struct { .specifier = input_specifier, .source_url = input_specifier.createIfDifferent(path.text), .hash = 0, + .module_info = null, }; }, } @@ -2437,6 +2500,7 @@ pub const ModuleLoader = struct { .specifier = specifier, .source_url = specifier, .hash = Runtime.Runtime.versionHash(), + .module_info = null, }; } else if (HardcodedModule.Map.getWithEql(specifier, bun.String.eqlComptime)) |hardcoded| { Analytics.Features.builtin_modules.insert(hardcoded); @@ -2451,6 +2515,7 @@ pub const ModuleLoader = struct { .hash = 0, .tag = .esm, .source_code_needs_deref = true, + .module_info = null, }; }, @@ -2560,6 +2625,7 @@ pub const ModuleLoader = struct { .specifier = specifier, .source_url = specifier.dupeRef(), .hash = 0, + .module_info = null, }; } } else if (jsc_vm.standalone_module_graph) |graph| { @@ -2583,6 +2649,7 @@ pub const ModuleLoader = struct { .source_url = specifier.dupeRef(), .hash = 0, .source_code_needs_deref = false, + .module_info = null, }; } @@ -2596,6 +2663,7 @@ pub const ModuleLoader = struct { .bytecode_cache = if (file.bytecode.len > 0) file.bytecode.ptr else null, .bytecode_cache_size = file.bytecode.len, .is_commonjs_module = file.module_format == .cjs, + .module_info = null, }; } } diff --git a/src/js_ast.zig b/src/js_ast.zig index 6b4fd31d32..f78d7dbe8c 100644 --- a/src/js_ast.zig +++ b/src/js_ast.zig @@ -322,10 +322,6 @@ pub const Binding = struct { loc: logger.Loc, }; - pub fn jsonStringify(self: *const @This(), writer: anytype) !void { - return try writer.write(Serializable{ .type = std.meta.activeTag(self.data), .object = "binding", .value = self.data, .loc = self.loc }); - } - pub fn ToExpr(comptime expr_type: type, comptime func_type: anytype) type { const ExprType = expr_type; return struct { @@ -2854,6 +2850,89 @@ pub const Stmt = struct { loc: logger.Loc, }; + pub fn print(self: *const Stmt, tree: Ast, writer: std.io.AnyWriter) !void { + _ = tree; + switch (self.data) { + .s_import => |simport| { + // const record = &tree.import_records.slice()[simport.import_record_index]; + try writer.print(".s_import{{\n", .{}); + try writer.print(" import_records[import_record_index = {d}] = ,\n", .{simport.import_record_index}); + // simport.default_name + // simport.is_single_line + // simport.items + // simport.namespace_ref + + // === record: === + // range: logger.Range, + // path: fs.Path, + // kind: ImportKind, + // tag: Tag = .none, + // source_index: Index = Index.invalid, + // print_mode: PrintMode = .normal, + // handles_import_errors: bool = false, + // is_internal: bool = false, + // is_unused: bool = false, + // contains_import_star: bool = false, + // contains_default_alias: bool = false, + // contains_es_module_alias: bool = false, + // calls_runtime_re_export_fn: bool = false, + // is_inside_try_body: bool = false, + // was_originally_bare_import: bool = false, + // was_originally_require: bool = false, + // was_injected_by_macro: bool = false, + // is_external_without_side_effects: bool = false, + // print_namespace_in_path: bool = false, + // wrap_with_to_esm: bool = false, + // wrap_with_to_commonjs: bool = false, + + try writer.print(" ", .{}); + try writer.print("}}", .{}); + }, + .s_expr => |expr| { + try writer.print(".s_expr{{ .does_not_affect_tree_shaking = {}, .value = ", .{expr.does_not_affect_tree_shaking}); + try expr.value.print(writer, 0); + try writer.print("}}", .{}); + }, + .s_local => |local| { + try writer.print(".s_local{{ .kind = .{s}, .is_export = {}, .was_ts_import_equals = {}, .was_commonjs_export = {}, .decls = .{{\n", .{ @tagName(local.kind), local.is_export, local.was_ts_import_equals, local.was_commonjs_export }); + for (local.decls.slice()) |m| { + try writer.print(" .{{\n .binding = ", .{}); + switch (m.binding.data) { + .b_array => |v| { + try writer.print(".b_array{{ .has_spread = {}, .is_single_line = {}, .items = .{{", .{ v.has_spread, v.is_single_line }); + for (v.items, 0..) |item, i| { + if (i != 0) try writer.print(", ", .{}); + try writer.print("(TODO)", .{}); + _ = item; + } + try writer.print("}}}}", .{}); + }, + .b_identifier => |v| { + try writer.print(".b_identifier{{ .ref = {} }}", .{v.ref}); + }, + .b_object => { + try writer.print(".b_object", .{}); + }, + .b_missing => { + try writer.print(".b_missing", .{}); + }, + } + try writer.print(",\n .value = ", .{}); + if (m.value == null) { + try writer.print("null", .{}); + } else { + try m.value.?.print(writer, 2); + } + try writer.print(",\n }},\n", .{}); + } + try writer.print("}} }}", .{}); + }, + else => { + try writer.print(".{s}._todo_print_stmt", .{@tagName(self.data)}); + }, + } + } + pub fn jsonStringify(self: *const Stmt, writer: anytype) !void { return try writer.write(Serializable{ .type = std.meta.activeTag(self.data), .object = "stmt", .value = self.data, .loc = self.loc }); } @@ -3233,6 +3312,18 @@ pub const Expr = struct { loc: logger.Loc, data: Data, + pub fn print(self: *const Expr, writer: std.io.AnyWriter, depth: u32) !void { + _ = depth; + switch (self.data) { + .e_string => |str| { + try writer.print("(string: \"{s}\")", .{bun.strings.formatEscapes(str.data, .{ .str_encoding = .utf8, .quote_char = '"' })}); + }, + else => { + try writer.print("(expr: {s})", .{@tagName(self.data)}); + }, + } + } + pub const empty = Expr{ .data = .{ .e_missing = E.Missing{} }, .loc = logger.Loc.Empty }; pub fn isAnonymousNamed(expr: Expr) bool { diff --git a/src/js_printer.zig b/src/js_printer.zig index 8cb2b6327d..be9dd807f1 100644 --- a/src/js_printer.zig +++ b/src/js_printer.zig @@ -23,6 +23,8 @@ const Ref = @import("ast/base.zig").Ref; const StoredFileDescriptorType = bun.StoredFileDescriptorType; const FeatureFlags = bun.FeatureFlags; const FileDescriptorType = bun.FileDescriptor; +const analyze_transpiled_module = @import("analyze_transpiled_module.zig"); +const ModuleInfo = analyze_transpiled_module.ModuleInfo; const expect = std.testing.expect; const ImportKind = importRecord.ImportKind; @@ -690,8 +692,38 @@ fn NewPrinter( binary_expression_stack: std.ArrayList(BinaryExpressionVisitor) = undefined, + module_info: if (!may_have_module_info) void else ?*ModuleInfo = if (!may_have_module_info) {} else null, + const Printer = @This(); + const may_have_module_info = is_bun_platform and !rewrite_esm_to_cjs; + const TopLevelAndIsExport = if (!may_have_module_info) struct {} else struct { + is_export: bool = false, + is_top_level: ?ModuleInfo.VarKind = null, + }; + const TopLevel = if (!may_have_module_info) struct { + pub inline fn init(_: IsTopLevel) @This() { + return .{}; + } + pub inline fn subVar(_: @This()) @This() { + return .{}; + } + } else struct { + is_top_level: IsTopLevel = .no, + pub inline fn init(is_top_level: IsTopLevel) @This() { + return .{ .is_top_level = is_top_level }; + } + pub fn subVar(self: @This()) @This() { + if (self.is_top_level == .no) return @This().init(.no); + return @This().init(.var_only); + } + }; + const IsTopLevel = enum { yes, var_only, no }; + inline fn moduleInfo(self: *const @This()) ?*ModuleInfo { + if (!may_have_module_info) return null; + return self.module_info; + } + /// When Printer is used as a io.Writer, this represents it's error type, aka nothing. pub const Error = error{}; @@ -988,6 +1020,7 @@ fn NewPrinter( printInternalBunImport(p, import, @TypeOf("globalThis.Bun.jest(__filename)"), "globalThis.Bun.jest(__filename)"); }, else => { + if (p.moduleInfo()) |mi| mi.contains_import_meta = true; printInternalBunImport(p, import, @TypeOf("globalThis.Bun.jest(import.meta.path)"), "globalThis.Bun.jest(import.meta.path)"); }, } @@ -1003,7 +1036,9 @@ fn NewPrinter( if (import.star_name_loc != null) { p.print("var "); - p.printSymbol(import.namespace_ref); + const name = p.renamer.nameForSymbol(import.namespace_ref); + p.printIdentifier(name); + if (p.moduleInfo()) |mi| mi.addVar(name, .declared) catch bun.outOfMemory(); p.printSpace(); p.print("="); p.printSpaceBeforeIdentifier(); @@ -1026,7 +1061,9 @@ fn NewPrinter( if (import.default_name) |default| { p.print("var "); - p.printSymbol(default.ref.?); + const default_name = p.renamer.nameForSymbol(default.ref.?); + p.printIdentifier(default_name); + if (p.moduleInfo()) |mi| mi.addVar(default_name, .declared) catch bun.outOfMemory(); if (comptime Statement == void) { p.@"print = "(); p.printRequireOrImportExpr( @@ -1064,7 +1101,11 @@ fn NewPrinter( } } - p.printClauseItemAs(item, .@"var"); + p.printVarClauseItem(item); + if (p.moduleInfo()) |mi| { + const varname = p.renamer.nameForSymbol(item.name.ref.?); + mi.addVar(varname, .declared) catch bun.outOfMemory(); + } } if (!import.is_single_line) { @@ -1136,32 +1177,32 @@ fn NewPrinter( switch (stmt.data) { .s_block => |block| { p.printSpace(); - p.printBlock(stmt.loc, block.stmts, block.close_brace_loc); + p.printBlock(stmt.loc, block.stmts, block.close_brace_loc, .{}); p.printNewline(); }, else => { p.printNewline(); p.indent(); - p.printStmt(stmt) catch unreachable; + p.printStmt(stmt, .{}) catch bun.outOfMemory(); p.unindent(); }, } } - pub fn printBlockBody(p: *Printer, stmts: []const Stmt) void { + pub fn printBlockBody(p: *Printer, stmts: []const Stmt, tlmtlo: TopLevel) void { for (stmts) |stmt| { p.printSemicolonIfNeeded(); - p.printStmt(stmt) catch unreachable; + p.printStmt(stmt, tlmtlo) catch bun.outOfMemory(); } } - pub fn printBlock(p: *Printer, loc: logger.Loc, stmts: []const Stmt, close_brace_loc: ?logger.Loc) void { + pub fn printBlock(p: *Printer, loc: logger.Loc, stmts: []const Stmt, close_brace_loc: ?logger.Loc, tlmtlo: TopLevel) void { p.addSourceMapping(loc); p.print("{"); p.printNewline(); p.indent(); - p.printBlockBody(stmts); + p.printBlockBody(stmts, tlmtlo); p.unindent(); p.needs_semicolon = false; @@ -1178,8 +1219,8 @@ fn NewPrinter( p.printNewline(); p.indent(); - p.printBlockBody(prepend); - p.printBlockBody(stmts); + p.printBlockBody(prepend, .{}); + p.printBlockBody(stmts, .{}); p.unindent(); p.needs_semicolon = false; @@ -1187,7 +1228,7 @@ fn NewPrinter( p.print("}"); } - pub fn printDecls(p: *Printer, comptime keyword: string, decls_: []G.Decl, flags: ExprFlag.Set) void { + pub fn printDecls(p: *Printer, comptime keyword: string, decls_: []G.Decl, flags: ExprFlag.Set, tlm: TopLevelAndIsExport) void { p.print(keyword); p.printSpace(); var decls = decls_; @@ -1295,7 +1336,7 @@ fn NewPrinter( .is_single_line = true, }; const binding = Binding.init(&b_object, target_e_dot.target.loc); - p.printBinding(binding); + p.printBinding(binding, tlm); } p.printWhitespacer(ws(" = ")); @@ -1311,7 +1352,7 @@ fn NewPrinter( } { - p.printBinding(decls[0].binding); + p.printBinding(decls[0].binding, tlm); if (decls[0].value) |value| { p.printWhitespacer(ws(" = ")); @@ -1323,7 +1364,7 @@ fn NewPrinter( p.print(","); p.printSpace(); - p.printBinding(decl.binding); + p.printBinding(decl.binding, tlm); if (decl.value) |value| { p.printWhitespacer(ws(" = ")); @@ -1390,7 +1431,7 @@ fn NewPrinter( p.print("..."); } - p.printBinding(arg.binding); + p.printBinding(arg.binding, .{}); if (arg.default) |default| { p.printWhitespacer(ws(" = ")); @@ -1406,7 +1447,7 @@ fn NewPrinter( pub fn printFunc(p: *Printer, func: G.Fn) void { p.printFnArgs(func.open_parens_loc, func.args, func.flags.contains(.has_rest_arg), false); p.printSpace(); - p.printBlock(func.body.loc, func.body.stmts, null); + p.printBlock(func.body.loc, func.body.stmts, null, .{}); } pub fn printClass(p: *Printer, class: G.Class) void { if (class.extends) |extends| { @@ -1429,7 +1470,7 @@ fn NewPrinter( if (item.kind == .class_static_block) { p.print("static"); p.printSpace(); - p.printBlock(item.class_static_block.?.loc, item.class_static_block.?.stmts.slice(), null); + p.printBlock(item.class_static_block.?.loc, item.class_static_block.?.stmts.slice(), null, .{}); p.printNewline(); continue; } @@ -1670,12 +1711,14 @@ fn NewPrinter( if (module_type == .cjs) { p.print("Promise.resolve(globalThis.Bun.jest(__filename))"); } else { + if (p.moduleInfo()) |mi| mi.contains_import_meta = true; p.print("Promise.resolve(globalThis.Bun.jest(import.meta.path))"); } } else if (record.kind == .require) { if (module_type == .cjs) { p.print("globalThis.Bun.jest(__filename)"); } else { + if (p.moduleInfo()) |mi| mi.contains_import_meta = true; p.print("globalThis.Bun.jest(import.meta.path)"); } } @@ -1898,45 +1941,28 @@ fn NewPrinter( p.print(quote); } - fn printClauseItem(p: *Printer, item: js_ast.ClauseItem) void { - return printClauseItemAs(p, item, .import); - } - fn printExportClauseItem(p: *Printer, item: js_ast.ClauseItem) void { - return printClauseItemAs(p, item, .@"export"); - } - - fn printClauseItemAs(p: *Printer, item: js_ast.ClauseItem, comptime as: @Type(.EnumLiteral)) void { const name = p.renamer.nameForSymbol(item.name.ref.?); - if (comptime as == .import) { - if (strings.eql(name, item.alias)) { - p.printIdentifier(name); - } else { - p.printClauseAlias(item.alias); - p.print(" as "); - p.addSourceMapping(item.alias_loc); - p.printIdentifier(name); - } - } else if (comptime as == .@"var") { + p.printIdentifier(name); + + if (!strings.eql(name, item.alias)) { + p.print(" as "); + p.addSourceMapping(item.alias_loc); p.printClauseAlias(item.alias); + } + } - if (!strings.eql(name, item.alias)) { - p.print(":"); - p.printSpace(); + fn printVarClauseItem(p: *Printer, item: js_ast.ClauseItem) void { + const name = p.renamer.nameForSymbol(item.name.ref.?); + + p.printClauseAlias(item.alias); + + if (!strings.eql(name, item.alias)) { + p.print(":"); + p.printSpace(); - p.printIdentifier(name); - } - } else if (comptime as == .@"export") { p.printIdentifier(name); - - if (!strings.eql(name, item.alias)) { - p.print(" as "); - p.addSourceMapping(item.alias_loc); - p.printClauseAlias(item.alias); - } - } else { - @compileError("Unknown as"); } } @@ -2048,6 +2074,7 @@ fn NewPrinter( p.print(".importMeta()"); } else if (!p.options.import_meta_ref.isValid()) { // Most of the time, leave it in there + if (p.moduleInfo()) |mi| mi.contains_import_meta = true; p.print("import.meta"); } else { // Note: The bundler will not hit this code path. The bundler will replace @@ -2074,6 +2101,7 @@ fn NewPrinter( p.printSpaceBeforeIdentifier(); p.addSourceMapping(expr.loc); } + if (p.moduleInfo()) |mi| mi.contains_import_meta = true; p.print("import.meta.main"); } else { bun.assert(p.options.module_type != .internal_bake_dev); @@ -2541,7 +2569,7 @@ fn NewPrinter( } if (!wasPrinted) { - p.printBlock(e.body.loc, e.body.stmts, null); + p.printBlock(e.body.loc, e.body.stmts, null, .{}); } if (wrap) { @@ -3449,13 +3477,19 @@ fn NewPrinter( p.printExpr(initial, .comma, ExprFlag.None()); } - pub fn printBinding(p: *Printer, binding: Binding) void { + pub fn printBinding(p: *Printer, binding: Binding, tlm: TopLevelAndIsExport) void { switch (binding.data) { .b_missing => {}, .b_identifier => |b| { p.printSpaceBeforeIdentifier(); p.addSourceMapping(binding.loc); - p.printSymbol(b.ref); + const local_name = p.renamer.nameForSymbol(b.ref); + p.printIdentifier(local_name); + + if (p.moduleInfo()) |mi| { + if (tlm.is_top_level) |vk| mi.addVar(local_name, vk) catch bun.outOfMemory(); + if (tlm.is_export) mi.addExportInfoLocal(local_name, local_name) catch bun.outOfMemory(); + } }, .b_array => |b| { p.print("["); @@ -3482,7 +3516,7 @@ fn NewPrinter( p.print("..."); } - p.printBinding(item.binding); + p.printBinding(item.binding, tlm); p.maybePrintDefaultBindingValue(item); @@ -3529,7 +3563,7 @@ fn NewPrinter( p.print("]:"); p.printSpace(); - p.printBinding(property.value); + p.printBinding(property.value, tlm); p.maybePrintDefaultBindingValue(property); continue; } @@ -3554,6 +3588,10 @@ fn NewPrinter( switch (property.value.data) { .b_identifier => |id| { if (str.eql(string, p.renamer.nameForSymbol(id.ref))) { + if (p.moduleInfo()) |mi| { + if (tlm.is_top_level) |vk| mi.addVar(str.data, vk) catch bun.outOfMemory(); + if (tlm.is_export) mi.addExportInfoLocal(str.data, str.data) catch bun.outOfMemory(); + } p.maybePrintDefaultBindingValue(property); continue; } @@ -3571,6 +3609,11 @@ fn NewPrinter( switch (property.value.data) { .b_identifier => |id| { if (strings.utf16EqlString(str.slice16(), p.renamer.nameForSymbol(id.ref))) { + if (p.moduleInfo()) |mi| { + const str8 = str.slice(p.options.allocator); + if (tlm.is_top_level) |vk| mi.addVar(str8, vk) catch bun.outOfMemory(); + if (tlm.is_export) mi.addExportInfoLocal(str8, str8) catch bun.outOfMemory(); + } p.maybePrintDefaultBindingValue(property); continue; } @@ -3590,7 +3633,7 @@ fn NewPrinter( p.printSpace(); } - p.printBinding(property.value); + p.printBinding(property.value, tlm); p.maybePrintDefaultBindingValue(property); } @@ -3616,7 +3659,7 @@ fn NewPrinter( } } - pub fn printStmt(p: *Printer, stmt: Stmt) !void { + pub fn printStmt(p: *Printer, stmt: Stmt, tlmtlo: TopLevel) !void { const prev_stmt_tag = p.prev_stmt_tag; defer { @@ -3650,9 +3693,15 @@ fn NewPrinter( p.printSpaceBeforeIdentifier(); p.addSourceMapping(name.loc); - p.printSymbol(nameRef); + const local_name = p.renamer.nameForSymbol(nameRef); + p.printIdentifier(local_name); p.printFunc(s.func); + if (p.moduleInfo()) |mi| { + if (tlmtlo.is_top_level == .yes) try mi.addVar(local_name, .lexical); + if (s.func.flags.contains(.is_export)) try mi.addExportInfoLocal(local_name, local_name); + } + // if (rewrite_esm_to_cjs and s.func.flags.contains(.is_export)) { // p.printSemicolonAfterStatement(); // p.print("var "); @@ -3679,6 +3728,7 @@ fn NewPrinter( p.printIndent(); p.printSpaceBeforeIdentifier(); const nameRef = s.class.class_name.?.ref.?; + const nameStr = p.renamer.nameForSymbol(nameRef); if (s.is_export) { if (!rewrite_esm_to_cjs) { p.print("export "); @@ -3687,9 +3737,14 @@ fn NewPrinter( p.print("class "); p.addSourceMapping(s.class.class_name.?.loc); - p.printSymbol(nameRef); + p.printIdentifier(nameStr); p.printClass(s.class); + if (p.moduleInfo()) |mi| { + if (s.is_export) try mi.addExportInfoLocal(nameStr, nameStr); + if (tlmtlo.is_top_level == .yes) try mi.addVar(nameStr, .lexical); + } + if (rewrite_esm_to_cjs and s.is_export) { p.printSemicolonAfterStatement(); } else { @@ -3723,6 +3778,10 @@ fn NewPrinter( p.export_default_start = p.writer.written; p.printExpr(expr, .comma, ExprFlag.None()); p.printSemicolonAfterStatement(); + if (p.moduleInfo()) |mi| { + try mi.addExportInfoLocal("default", ModuleInfo.star_default); + try mi.addVar(ModuleInfo.star_default, .lexical); + } return; }, @@ -3743,20 +3802,26 @@ fn NewPrinter( p.maybePrintSpace(); } - if (func.func.name) |name| { - p.printSymbol(name.ref.?); - } + const func_name: ?[]const u8 = if (func.func.name) |f| p.renamer.nameForSymbol(f.ref.?) else null; + if (func_name) |f| p.printIdentifier(f); p.printFunc(func.func); p.printNewline(); + + if (p.moduleInfo()) |mi| { + const local_name = func_name orelse ModuleInfo.star_default; + try mi.addExportInfoLocal("default", local_name); + try mi.addVar(local_name, .lexical); + } }, .s_class => |class| { p.printSpaceBeforeIdentifier(); - if (class.class.class_name) |name| { + const class_name: ?[]const u8 = if (class.class.class_name) |f| p.renamer.nameForSymbol(f.ref.?) else null; + if (class_name) |name| { p.print("class "); - p.printSymbol(name.ref orelse Output.panic("Internal error: Expected class to have a name ref\n{any}", .{class})); + p.printIdentifier(name); } else { p.print("class"); } @@ -3764,6 +3829,12 @@ fn NewPrinter( p.printClass(class.class); p.printNewline(); + + if (p.moduleInfo()) |mi| { + const local_name = class_name orelse ModuleInfo.star_default; + try mi.addExportInfoLocal("default", local_name); + try mi.addVar(local_name, .lexical); + } }, else => { Output.panic("Internal error: unexpected export default stmt data {any}", .{s}); @@ -3781,19 +3852,27 @@ fn NewPrinter( p.printIndent(); p.printSpaceBeforeIdentifier(); - if (s.alias != null) - p.printWhitespacer(comptime ws("export *").append(" as ")) - else - p.printWhitespacer(comptime ws("export * from ")); - if (s.alias) |alias| { + p.printWhitespacer(comptime ws("export *").append(" as ")); p.printClauseAlias(alias.original_name); p.print(" "); p.printWhitespacer(ws("from ")); + } else { + p.printWhitespacer(comptime ws("export * from ")); } - p.printImportRecordPath(p.importRecord(s.import_record_index)); + const irp = try p.fmtImportRecordPath(p.importRecord(s.import_record_index)); + p.printStringLiteralUTF8(irp, false); p.printSemicolonAfterStatement(); + + if (p.moduleInfo()) |mi| { + try mi.requestModule(irp, .none); + if (s.alias) |alias| { + try mi.addExportInfoNamespace(alias.original_name, irp); + } else { + try mi.addExportInfoStar(irp); + } + } }, .s_export_clause => |s| { if (rewrite_esm_to_cjs) { @@ -3941,7 +4020,12 @@ fn NewPrinter( p.printIndent(); } + const name = p.renamer.nameForSymbol(item.name.ref.?); p.printExportClauseItem(item); + + if (p.moduleInfo()) |mi| { + try mi.addExportInfoLocal(item.alias, name); + } } if (!s.is_single_line) { @@ -3993,26 +4077,26 @@ fn NewPrinter( } p.printWhitespacer(ws("} from ")); - p.printImportRecordPath(import_record); + const irp = try p.fmtImportRecordPath(import_record); + p.printStringLiteralUTF8(irp, false); p.printSemicolonAfterStatement(); + + if (p.moduleInfo()) |mi| { + try mi.requestModule(irp, .none); + for (s.items) |item| { + // how could this be renamed, it's in `export from`? + const name = p.renamer.nameForSymbol(item.name.ref.?); + try mi.addExportInfoIndirect(item.alias, name, irp); + } + } }, .s_local => |s| { switch (s.kind) { - .k_const => { - p.printDeclStmt(s.is_export, "const", s.decls.slice()); - }, - .k_let => { - p.printDeclStmt(s.is_export, "let", s.decls.slice()); - }, - .k_var => { - p.printDeclStmt(s.is_export, "var", s.decls.slice()); - }, - .k_using => { - p.printDeclStmt(s.is_export, "using", s.decls.slice()); - }, - .k_await_using => { - p.printDeclStmt(s.is_export, "await using", s.decls.slice()); - }, + .k_const => p.printDeclStmt(s.is_export, "const", s.decls.slice(), tlmtlo), + .k_let => p.printDeclStmt(s.is_export, "let", s.decls.slice(), tlmtlo), + .k_var => p.printDeclStmt(s.is_export, "var", s.decls.slice(), tlmtlo), + .k_using => p.printDeclStmt(s.is_export, "using", s.decls.slice(), tlmtlo), + .k_await_using => p.printDeclStmt(s.is_export, "await using", s.decls.slice(), tlmtlo), } }, .s_if => |s| { @@ -4026,13 +4110,13 @@ fn NewPrinter( switch (s.body.data) { .s_block => { p.printSpace(); - p.printBlock(s.body.loc, s.body.data.s_block.stmts, s.body.data.s_block.close_brace_loc); + p.printBlock(s.body.loc, s.body.data.s_block.stmts, s.body.data.s_block.close_brace_loc, .{}); p.printSpace(); }, else => { p.printNewline(); p.indent(); - p.printStmt(s.body) catch unreachable; + p.printStmt(s.body, .{}) catch unreachable; p.printSemicolonIfNeeded(); p.unindent(); p.printIndent(); @@ -4115,7 +4199,7 @@ fn NewPrinter( p.printSpaceBeforeIdentifier(); p.print("try"); p.printSpace(); - p.printBlock(s.body_loc, s.body, null); + p.printBlock(s.body_loc, s.body, null, tlmtlo.subVar()); if (s.catch_) |catch_| { p.printSpace(); @@ -4123,18 +4207,18 @@ fn NewPrinter( if (catch_.binding) |binding| { p.printSpace(); p.print("("); - p.printBinding(binding); + p.printBinding(binding, .{}); p.print(")"); } p.printSpace(); - p.printBlock(catch_.loc, catch_.body, null); + p.printBlock(catch_.loc, catch_.body, null, tlmtlo.subVar()); } if (s.finally) |finally| { p.printSpace(); p.print("finally"); p.printSpace(); - p.printBlock(finally.loc, finally.stmts, null); + p.printBlock(finally.loc, finally.stmts, null, tlmtlo.subVar()); } p.printNewline(); @@ -4199,7 +4283,7 @@ fn NewPrinter( switch (c.body[0].data) { .s_block => { p.printSpace(); - p.printBlock(c.body[0].loc, c.body[0].data.s_block.stmts, c.body[0].data.s_block.close_brace_loc); + p.printBlock(c.body[0].loc, c.body[0].data.s_block.stmts, c.body[0].data.s_block.close_brace_loc, .{}); p.printNewline(); continue; }, @@ -4211,7 +4295,7 @@ fn NewPrinter( p.indent(); for (c.body) |st| { p.printSemicolonIfNeeded(); - p.printStmt(st) catch unreachable; + p.printStmt(st, .{}) catch unreachable; } p.unindent(); } @@ -4292,6 +4376,7 @@ fn NewPrinter( p.printSymbol(s.namespace_ref); p.@"print = "(); + if (p.moduleInfo()) |mi| mi.contains_import_meta = true; p.print("import.meta.require("); p.printImportRecordPath(record); p.print(")"); @@ -4344,7 +4429,7 @@ fn NewPrinter( p.print(","); p.printSpace(); for (s.items, 0..) |item, i| { - p.printClauseItemAs(item, .@"var"); + p.printVarClauseItem(item); if (i < s.items.len - 1) { p.print(","); @@ -4354,7 +4439,7 @@ fn NewPrinter( } } else { for (s.items, 0..) |item, i| { - p.printClauseItemAs(item, .@"var"); + p.printVarClauseItem(item); if (i < s.items.len - 1) { p.print(","); @@ -4386,10 +4471,18 @@ fn NewPrinter( var item_count: usize = 0; + const import_record_path = try p.fmtImportRecordPath(record); + if (s.default_name) |name| { p.print(" "); - p.printSymbol(name.ref.?); + const local_name = p.renamer.nameForSymbol(name.ref.?); + p.printIdentifier(local_name); item_count += 1; + + if (p.moduleInfo()) |mi| { + try mi.addVar(local_name, .lexical); + try mi.addImportInfoSingle(import_record_path, "default", local_name); + } } if (s.items.len > 0) { @@ -4418,7 +4511,20 @@ fn NewPrinter( p.printIndent(); } - p.printClauseItem(item); + const local_name = p.renamer.nameForSymbol(item.name.ref.?); + if (strings.eql(local_name, item.alias)) { + p.printIdentifier(local_name); + } else { + p.printClauseAlias(item.alias); + p.print(" as "); + p.addSourceMapping(item.alias_loc); + p.printIdentifier(local_name); + } + + if (p.moduleInfo()) |mi| { + try mi.addVar(local_name, .lexical); + try mi.addImportInfoSingle(import_record_path, item.alias, local_name); + } } if (!s.is_single_line) { @@ -4438,10 +4544,17 @@ fn NewPrinter( } p.printSpace(); + const local_name = p.renamer.nameForSymbol(s.namespace_ref); + p.printWhitespacer(ws("* as")); p.print(" "); - p.printSymbol(s.namespace_ref); + p.printIdentifier(local_name); item_count += 1; + + if (p.moduleInfo()) |mi| { + try mi.addVar(local_name, .lexical); + try mi.addImportInfoNamespace(import_record_path, local_name); + } } if (item_count > 0) { @@ -4453,43 +4566,54 @@ fn NewPrinter( p.printWhitespacer(ws("from ")); } - p.printImportRecordPath(record); + p.printStringLiteralUTF8(import_record_path, false); + var fetch_parameters: ModuleInfo.FetchParameters = if (p.moduleInfo() != null) .none else undefined; switch (record.tag) { .with_type_sqlite, .with_type_sqlite_embedded => { // we do not preserve "embed": "true" since it is not necessary p.printWhitespacer(ws(" with { type: \"sqlite\" }")); + if (p.moduleInfo()) |mi| fetch_parameters = ModuleInfo.FetchParameters.hostDefined(try mi.str("sqlite")); }, .with_type_text => { if (comptime is_bun_platform) { p.printWhitespacer(ws(" with { type: \"text\" }")); + if (p.moduleInfo()) |mi| fetch_parameters = ModuleInfo.FetchParameters.hostDefined(try mi.str("text")); } }, .with_type_json => { // backwards compatibility: previously, we always stripped type json if (comptime is_bun_platform) { p.printWhitespacer(ws(" with { type: \"json\" }")); + if (p.moduleInfo() != null) fetch_parameters = .json; } }, .with_type_toml => { // backwards compatibility: previously, we always stripped type if (comptime is_bun_platform) { p.printWhitespacer(ws(" with { type: \"toml\" }")); + if (p.moduleInfo()) |mi| fetch_parameters = ModuleInfo.FetchParameters.hostDefined(try mi.str("toml")); } }, .with_type_file => { // backwards compatibility: previously, we always stripped type if (comptime is_bun_platform) { p.printWhitespacer(ws(" with { type: \"file\" }")); + if (p.moduleInfo()) |mi| fetch_parameters = ModuleInfo.FetchParameters.hostDefined(try mi.str("file")); } }, else => {}, } + + if (p.moduleInfo()) |mi| { + try mi.requestModule(import_record_path, fetch_parameters); + } + p.printSemicolonAfterStatement(); }, .s_block => |s| { p.printIndent(); - p.printBlock(stmt.loc, s.stmts, s.close_brace_loc); + p.printBlock(stmt.loc, s.stmts, s.close_brace_loc, .{}); p.printNewline(); }, .s_debugger => { @@ -4568,22 +4692,16 @@ fn NewPrinter( p.print("module.exports"); } - pub fn printImportRecordPath(p: *Printer, import_record: *const ImportRecord) void { - if (comptime is_json) - unreachable; + pub fn fmtImportRecordPath(p: *Printer, import_record: *const ImportRecord) ![]const u8 { + if (comptime is_json) unreachable; - const quote = bestQuoteCharForString(u8, import_record.path.text, false); if (import_record.print_namespace_in_path and !import_record.path.isFile()) { - p.print(quote); - p.printStringCharactersUTF8(import_record.path.namespace, quote); - p.print(":"); - p.printStringCharactersUTF8(import_record.path.text, quote); - p.print(quote); - } else { - p.print(quote); - p.printStringCharactersUTF8(import_record.path.text, quote); - p.print(quote); + return try std.fmt.allocPrint(p.options.allocator, "{s}:{s}", .{ import_record.path.namespace, import_record.path.text }); } + return import_record.path.text; + } + pub fn printImportRecordPath(p: *Printer, import_record: *const ImportRecord) void { + p.printStringLiteralUTF8(p.fmtImportRecordPath(import_record) catch bun.outOfMemory(), false); } pub fn printBundledImport(p: *Printer, record: ImportRecord, s: *S.Import) void { @@ -4759,21 +4877,11 @@ fn NewPrinter( }, .s_local => |s| { switch (s.kind) { - .k_var => { - p.printDecls("var", s.decls.slice(), ExprFlag.Set.init(.{ .forbid_in = true })); - }, - .k_let => { - p.printDecls("let", s.decls.slice(), ExprFlag.Set.init(.{ .forbid_in = true })); - }, - .k_const => { - p.printDecls("const", s.decls.slice(), ExprFlag.Set.init(.{ .forbid_in = true })); - }, - .k_using => { - p.printDecls("using", s.decls.slice(), ExprFlag.Set.init(.{ .forbid_in = true })); - }, - .k_await_using => { - p.printDecls("await using", s.decls.slice(), ExprFlag.Set.init(.{ .forbid_in = true })); - }, + .k_var => p.printDecls("var", s.decls.slice(), ExprFlag.Set.init(.{ .forbid_in = true }), .{}), + .k_let => p.printDecls("let", s.decls.slice(), ExprFlag.Set.init(.{ .forbid_in = true }), .{}), + .k_const => p.printDecls("const", s.decls.slice(), ExprFlag.Set.init(.{ .forbid_in = true }), .{}), + .k_using => p.printDecls("using", s.decls.slice(), ExprFlag.Set.init(.{ .forbid_in = true }), .{}), + .k_await_using => p.printDecls("await using", s.decls.slice(), ExprFlag.Set.init(.{ .forbid_in = true }), .{}), } }, // for(;) @@ -4794,7 +4902,7 @@ fn NewPrinter( switch (s.yes.data) { .s_block => |block| { p.printSpace(); - p.printBlock(s.yes.loc, block.stmts, block.close_brace_loc); + p.printBlock(s.yes.loc, block.stmts, block.close_brace_loc, .{}); if (s.no != null) { p.printSpace(); @@ -4809,7 +4917,7 @@ fn NewPrinter( p.printNewline(); p.indent(); - p.printStmt(s.yes) catch unreachable; + p.printStmt(s.yes, .{}) catch bun.outOfMemory(); p.unindent(); p.needs_semicolon = false; @@ -4824,7 +4932,7 @@ fn NewPrinter( } else { p.printNewline(); p.indent(); - p.printStmt(s.yes) catch unreachable; + p.printStmt(s.yes, .{}) catch bun.outOfMemory(); p.unindent(); if (s.no != null) { @@ -4842,7 +4950,7 @@ fn NewPrinter( switch (no_block.data) { .s_block => { p.printSpace(); - p.printBlock(no_block.loc, no_block.data.s_block.stmts, null); + p.printBlock(no_block.loc, no_block.data.s_block.stmts, null, .{}); p.printNewline(); }, .s_if => { @@ -4851,7 +4959,7 @@ fn NewPrinter( else => { p.printNewline(); p.indent(); - p.printStmt(no_block) catch unreachable; + p.printStmt(no_block, .{}) catch bun.outOfMemory(); p.unindent(); }, } @@ -4932,14 +5040,17 @@ fn NewPrinter( } } - pub fn printDeclStmt(p: *Printer, is_export: bool, comptime keyword: string, decls: []G.Decl) void { + pub fn printDeclStmt(p: *Printer, is_export: bool, comptime keyword: string, decls: []G.Decl, tlmtlo: TopLevel) void { p.printIndent(); p.printSpaceBeforeIdentifier(); if (!rewrite_esm_to_cjs and is_export) { p.print("export "); } - p.printDecls(keyword, decls, ExprFlag.None()); + p.printDecls(keyword, decls, ExprFlag.None(), if (may_have_module_info) .{ + .is_export = is_export and !rewrite_esm_to_cjs, + .is_top_level = if (comptime std.mem.eql(u8, keyword, "var")) if (tlmtlo.is_top_level == .no) null else .declared else if (tlmtlo.is_top_level == .yes) .lexical else null, + } else .{}); p.printSemicolonAfterStatement(); if (rewrite_esm_to_cjs and is_export and decls.len > 0) { for (decls) |decl| { @@ -4984,7 +5095,7 @@ fn NewPrinter( p.print("}"); }, else => { - p.printBinding(decl.binding); + p.printBinding(decl.binding, .{}); }, } p.print(")"); @@ -5667,6 +5778,7 @@ pub fn printAst( comptime ascii_only: bool, opts: Options, comptime generate_source_map: bool, + module_info: ?*@import("analyze_transpiled_module.zig").ModuleInfo, ) !usize { var renamer: rename.Renamer = undefined; var no_op_renamer: rename.NoOpRenamer = undefined; @@ -5765,6 +5877,11 @@ pub fn printAst( printer.source_map_builder.line_offset_tables.deinit(opts.allocator); } } + if (module_info != null) bun.assert(PrinterType.may_have_module_info); + const have_module_info = PrinterType.may_have_module_info and module_info != null; + + if (have_module_info) printer.module_info = module_info; + var bin_stack_heap = std.heap.stackFallback(1024, bun.default_allocator); printer.binary_expression_stack = std.ArrayList(PrinterType.BinaryExpressionVisitor).init(bin_stack_heap.get()); defer printer.binary_expression_stack.clearAndFree(); @@ -5788,12 +5905,13 @@ pub fn printAst( // // This is never a symbol collision because `uses_require_ref` means // `require` must be an unbound variable. + if (printer.moduleInfo()) |mi| mi.contains_import_meta = true; printer.print("var {require}=import.meta;"); } for (tree.parts.slice()) |part| { for (part.stmts) |stmt| { - try printer.printStmt(stmt); + try printer.printStmt(stmt, PrinterType.TopLevel.init(.yes)); if (printer.writer.getError()) {} else |err| { return err; } @@ -5801,24 +5919,25 @@ pub fn printAst( } } - if (comptime FeatureFlags.runtime_transpiler_cache and generate_source_map) { + if (have_module_info) { + try module_info.?.finalize(); + } + + var sourcemap: []const u8 = ""; + if (comptime generate_source_map) { if (opts.source_map_handler) |handler| { const source_maps_chunk = printer.source_map_builder.generateChunk(printer.writer.ctx.getWritten()); - if (opts.runtime_transpiler_cache) |cache| { - cache.put(printer.writer.ctx.getWritten(), source_maps_chunk.buffer.list.items); - } + sourcemap = source_maps_chunk.buffer.list.items; try handler.onSourceMapChunk(source_maps_chunk, source.*); - } else { - if (opts.runtime_transpiler_cache) |cache| { - cache.put(printer.writer.ctx.getWritten(), ""); - } - } - } else if (comptime generate_source_map) { - if (opts.source_map_handler) |handler| { - try handler.onSourceMapChunk(printer.source_map_builder.generateChunk(printer.writer.ctx.getWritten()), source.*); } } + if (opts.runtime_transpiler_cache) |cache| { + var srlz_res = std.ArrayList(u8).init(bun.default_allocator); + defer srlz_res.deinit(); + if (have_module_info) try module_info.?.asDeserialized().serialize(srlz_res.writer()); + cache.put(printer.writer.ctx.getWritten(), sourcemap, srlz_res.items); + } try printer.writer.done(); @@ -6006,7 +6125,7 @@ pub fn printWithWriterAndPlatform( for (parts) |part| { for (part.stmts) |stmt| { - printer.printStmt(stmt) catch |err| { + printer.printStmt(stmt, PrinterType.TopLevel.init(.yes)) catch |err| { return .{ .err = err }; }; if (printer.writer.getError()) {} else |err| { @@ -6069,7 +6188,7 @@ pub fn printCommonJS( for (tree.parts.slice()) |part| { for (part.stmts) |stmt| { - try printer.printStmt(stmt); + try printer.printStmt(stmt, PrinterType.TopLevel.init(.yes)); if (printer.writer.getError()) {} else |err| { return err; } diff --git a/src/string_immutable.zig b/src/string_immutable.zig index ab1d796490..db9db240b6 100644 --- a/src/string_immutable.zig +++ b/src/string_immutable.zig @@ -6473,7 +6473,7 @@ fn QuoteEscapeFormat(comptime flags: QuoteEscapeFormatFlags) type { data: []const u8, pub fn format(self: @This(), comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { - try bun.js_printer.writePreQuotedString(self.data, @TypeOf(writer), writer, flags.quote_char, false, flags.json, flags.str_encoding); + try bun.js_printer.writePreQuotedString(self.data, @TypeOf(writer), writer, flags.quote_char, flags.ascii_only, flags.json, flags.str_encoding); } }; } diff --git a/src/transpiler.zig b/src/transpiler.zig index 5836e0e20c..54ec22dff3 100644 --- a/src/transpiler.zig +++ b/src/transpiler.zig @@ -1068,12 +1068,15 @@ pub const Transpiler = struct { comptime enable_source_map: bool, source_map_context: ?js_printer.SourceMapHandler, runtime_transpiler_cache: ?*bun.JSC.RuntimeTranspilerCache, + module_info: ?*@import("analyze_transpiled_module.zig").ModuleInfo, ) !usize { const tracer = bun.tracy.traceNamed(@src(), if (enable_source_map) "JSPrinter.printWithSourceMap" else "JSPrinter.print"); defer tracer.end(); const symbols = js_ast.Symbol.NestedList.init(&[_]js_ast.Symbol.List{ast.symbols}); + if (module_info != null) bun.assert(format == .esm or format == .esm_ascii); + return switch (format) { .cjs => try js_printer.printCommonJS( Writer, @@ -1120,6 +1123,7 @@ pub const Transpiler = struct { .print_dce_annotations = transpiler.options.emit_dce_annotations, }, enable_source_map, + module_info, ), .esm_ascii => switch (transpiler.options.target.isBun()) { inline else => |is_bun| try js_printer.printAst( @@ -1154,6 +1158,7 @@ pub const Transpiler = struct { .print_dce_annotations = transpiler.options.emit_dce_annotations, }, enable_source_map, + module_info, ), }, else => unreachable, @@ -1176,6 +1181,7 @@ pub const Transpiler = struct { false, null, null, + null, ); } @@ -1186,6 +1192,7 @@ pub const Transpiler = struct { writer: Writer, comptime format: js_printer.Format, handler: js_printer.SourceMapHandler, + module_info: ?*@import("analyze_transpiled_module.zig").ModuleInfo, ) !usize { if (bun.getRuntimeFeatureFlag("BUN_FEATURE_FLAG_DISABLE_SOURCE_MAPS")) { return transpiler.printWithSourceMapMaybe( @@ -1197,6 +1204,7 @@ pub const Transpiler = struct { false, handler, result.runtime_transpiler_cache, + module_info, ); } return transpiler.printWithSourceMapMaybe( @@ -1208,6 +1216,7 @@ pub const Transpiler = struct { true, handler, result.runtime_transpiler_cache, + module_info, ); } diff --git a/test/js/bun/typescript/type-export.test.ts b/test/js/bun/typescript/type-export.test.ts new file mode 100644 index 0000000000..2f44ac4577 --- /dev/null +++ b/test/js/bun/typescript/type-export.test.ts @@ -0,0 +1,174 @@ +import { describe, test, expect } from "bun:test" with { todo: "true" }; +import { bunEnv, bunExe, tempDirWithFiles } from "harness"; + +/* +Potential solutions: +- Option 1: Make a fake export `export const my_string = undefined;` and make sure it is not enumerable +- Option 2: In b.ts, make javascriptcore skip re-exporting something if it is not found rather than SyntaxErroring + - this won't work because in the import {} export {} case, the error will be on the import +*/ + +const a_file = ` + export type my_string = "1"; + + export type my_value = "2"; + export const my_value = "2"; + + export const my_only = "3"; +`; +const a_no_value = ` + export type my_string = "1"; + export type my_value = "2"; + export const my_only = "3"; +`; +const a_with_value = ` + export type my_string = "1"; + export const my_value = "2"; +`; +const b_files = [ + { + name: "export from", + value: `export { my_string, my_value, my_only } from "./a.ts";`, + }, + { + name: "import then export", + value: ` + import { my_string, my_value, my_only } from "./a.ts"; + export { my_string, my_value, my_only }; + `, + }, + { + name: "export star", + value: `export * from "./a.ts";`, + }, + { + name: "export merge", + value: `export * from "./a_no_value.ts"; export * from "./a_with_value.ts"`, + }, +]; +const c_files = [ + { name: "require", value: `console.log(JSON.stringify(require("./b")));` }, + { name: "import star", value: `import * as b from "./b"; console.log(JSON.stringify(b));` }, + { name: "await import", value: `console.log(JSON.stringify(await import("./b")));` }, + { + name: "import individual", + value: ` + import { my_string, my_value, my_only } from "./b"; + console.log(JSON.stringify({ my_only, my_value })); + `, + }, +]; +for (const b_file of b_files) { + describe(`re-export with ${b_file.name}`, () => { + for (const c_file of c_files) { + describe(`import with ${c_file.name}`, () => { + const dir = tempDirWithFiles("type-export", { + "a.ts": a_file, + "b.ts": b_file.value, + "c.ts": c_file.value, + + "a_no_value.ts": a_no_value, + "a_with_value.ts": a_with_value, + }); + + const runAndVerify = (filename: string) => { + const result = Bun.spawnSync({ + cmd: [bunExe(), "run", filename], + cwd: dir, + env: bunEnv, + stdio: ["inherit", "pipe", "inherit"], + }); + + expect(result.exitCode).toBe(0); + expect(JSON.parse(result.stdout.toString().trim())).toEqual({ my_value: "2", my_only: "3" }); + }; + + test.todoIf(b_file.name !== "export star" && b_file.name !== "export merge")("run", () => { + runAndVerify("c.ts"); + }); + + test("build", async () => { + const result = Bun.spawnSync({ + cmd: [bunExe(), "build", "--target=bun", "--outfile", "bundle.js", "c.ts"], + cwd: dir, + env: bunEnv, + stdio: ["inherit", "inherit", "inherit"], + }); + + expect(result.exitCode).toBe(0); + runAndVerify("bundle.js"); + }); + }); + } + }); +} + +test("import not found", () => { + const dir = tempDirWithFiles("type-export", { + "a.ts": `export const a = 25; export const c = "hello";`, + "b.ts": /*js*/ ` + import { a, b, c } from "./a"; + console.log(a, b, c); + `, + }); + + const result = Bun.spawnSync({ + cmd: [bunExe(), "run", "b.ts"], + cwd: dir, + env: bunEnv, + stdio: ["inherit", "pipe", "pipe"], + }); + + expect(result.stderr?.toString().trim()).toContain("SyntaxError: Export named 'b' not found in module"); + expect({ + exitCode: result.exitCode, + stdout: result.stdout?.toString().trim(), + }).toEqual({ + exitCode: 1, + stdout: "", + }); +}); + +describe("through export merge", () => { + // this isn't allowed, even in typescript (tsc emits "Duplicate identifier 'value'.") + for (const fmt of ["js", "ts"]) { + describe(fmt, () => { + for (const [name, mode] of [ + ["through", "export {value} from './b'; export {value} from './c';"], + ["direct", "export {value} from './b'; export const value = 'abc';"], + ["direct2", "export const value = 'abc'; export {value};"], + ["ns", "export * as value from './c'; export * as value from './c';"], + ]) { + describe(name, () => { + const dir = tempDirWithFiles("type-import", { + ["main." + fmt]: "import {value} from './a'; console.log(value);", + ["a." + fmt]: mode, + ["b." + fmt]: fmt === "ts" ? "export type value = 'b';" : "", + ["c." + fmt]: "export const value = 'c';", + }); + for (const file of ["main." + fmt, "a." + fmt]) { + test(file, () => { + const result = Bun.spawnSync({ + cmd: [bunExe(), file], + cwd: dir, + env: bunEnv, + stdio: ["inherit", "pipe", "pipe"], + }); + expect(result.stderr?.toString().trim()).toInclude( + file === "a." + fmt + ? 'error: Multiple exports with the same name "value"\n' // bun's syntax error + : "SyntaxError: Cannot export a duplicate name 'value'.\n", // jsc's syntax error + ); + expect(result.exitCode).toBe(1); + }); + } + }); + } + }); + } +}); + +// TODO: +// - check ownkeys from a star import +// - check commonjs cases +// - what happens with `export * from ./a; export * from ./b` where a and b have different definitions of the same name?