diff --git a/src/OutputFile.zig b/src/OutputFile.zig index 1eb0fb90d2..941037f2a0 100644 --- a/src/OutputFile.zig +++ b/src/OutputFile.zig @@ -15,6 +15,7 @@ hash: u64 = 0, is_executable: bool = false, source_map_index: u32 = std.math.maxInt(u32), bytecode_index: u32 = std.math.maxInt(u32), +module_info_index: u32 = std.math.maxInt(u32), output_kind: jsc.API.BuildArtifact.OutputKind, /// Relative dest_path: []const u8 = "", @@ -210,6 +211,7 @@ pub const Options = struct { hash: ?u64 = null, source_map_index: ?u32 = null, bytecode_index: ?u32 = null, + module_info_index: ?u32 = null, output_path: string, source_index: Index.Optional = .none, size: ?usize = null, @@ -251,6 +253,7 @@ pub fn init(options: Options) OutputFile { .hash = options.hash orelse 0, .output_kind = options.output_kind, .bytecode_index = options.bytecode_index orelse std.math.maxInt(u32), + .module_info_index = options.module_info_index orelse std.math.maxInt(u32), .source_map_index = options.source_map_index orelse std.math.maxInt(u32), .is_executable = options.is_executable, .value = switch (options.data) { diff --git a/src/StandaloneModuleGraph.zig b/src/StandaloneModuleGraph.zig index b4123564cd..3985a6aee6 100644 --- a/src/StandaloneModuleGraph.zig +++ b/src/StandaloneModuleGraph.zig @@ -92,6 +92,10 @@ pub const StandaloneModuleGraph = struct { contents: Schema.StringPointer = .{}, sourcemap: Schema.StringPointer = .{}, bytecode: Schema.StringPointer = .{}, + module_info: Schema.StringPointer = .{}, + /// The file path used when generating bytecode (e.g., "B:/~BUN/root/app.js"). + /// Must match exactly at runtime for bytecode cache hits. + bytecode_origin_path: Schema.StringPointer = .{}, encoding: Encoding = .latin1, loader: bun.options.Loader = .file, module_format: ModuleFormat = .none, @@ -159,6 +163,10 @@ pub const StandaloneModuleGraph = struct { encoding: Encoding = .binary, wtf_string: bun.String = bun.String.empty, bytecode: []u8 = "", + module_info: []u8 = "", + /// The file path used when generating bytecode (e.g., "B:/~BUN/root/app.js"). + /// Must match exactly at runtime for bytecode cache hits. + bytecode_origin_path: []const u8 = "", module_format: ModuleFormat = .none, side: FileSide = .server, @@ -333,6 +341,8 @@ pub const StandaloneModuleGraph = struct { else .none, .bytecode = if (module.bytecode.length > 0) @constCast(sliceTo(raw_bytes, module.bytecode)) else &.{}, + .module_info = if (module.module_info.length > 0) @constCast(sliceTo(raw_bytes, module.module_info)) else &.{}, + .bytecode_origin_path = if (module.bytecode_origin_path.length > 0) sliceToZ(raw_bytes, module.bytecode_origin_path) else "", .module_format = module.module_format, .side = module.side, }, @@ -382,6 +392,8 @@ pub const StandaloneModuleGraph = struct { } else if (output_file.output_kind == .bytecode) { // Allocate up to 256 byte alignment for bytecode string_builder.cap += (output_file.value.buffer.bytes.len + 255) / 256 * 256 + 256; + } else if (output_file.output_kind == .module_info) { + string_builder.cap += output_file.value.buffer.bytes.len; } else { if (entry_point_id == null) { if (output_file.side == null or output_file.side.? == .server) { @@ -477,6 +489,19 @@ pub const StandaloneModuleGraph = struct { } }; + // Embed module_info for ESM bytecode + const module_info: StringPointer = brk: { + if (output_file.module_info_index != std.math.maxInt(u32)) { + const mi_bytes = output_files[output_file.module_info_index].value.buffer.bytes; + const offset = string_builder.len; + const writable = string_builder.writable(); + @memcpy(writable[0..mi_bytes.len], mi_bytes[0..mi_bytes.len]); + string_builder.len += mi_bytes.len; + break :brk StringPointer{ .offset = @truncate(offset), .length = @truncate(mi_bytes.len) }; + } + break :brk .{}; + }; + if (comptime bun.Environment.is_canary or bun.Environment.isDebug) { if (bun.env_var.BUN_FEATURE_FLAG_DUMP_CODE.get()) |dump_code_dir| { const buf = bun.path_buffer_pool.get(); @@ -498,6 +523,13 @@ pub const StandaloneModuleGraph = struct { } } + // When there's bytecode, store the bytecode output file's path as bytecode_origin_path. + // This path was used to generate the bytecode cache and must match at runtime. + const bytecode_origin_path: StringPointer = if (output_file.bytecode_index != std.math.maxInt(u32)) + string_builder.appendCountZ(output_files[output_file.bytecode_index].dest_path) + else + .{}; + var module = CompiledModuleGraphFile{ .name = string_builder.fmtAppendCountZ("{s}{s}", .{ prefix, @@ -515,6 +547,8 @@ pub const StandaloneModuleGraph = struct { else => .none, } else .none, .bytecode = bytecode, + .module_info = module_info, + .bytecode_origin_path = bytecode_origin_path, .side = switch (output_file.side orelse .server) { .server => .server, .client => .client, diff --git a/src/analyze_transpiled_module.zig b/src/analyze_transpiled_module.zig new file mode 100644 index 0000000000..3780592edb --- /dev/null +++ b/src/analyze_transpiled_module.zig @@ -0,0 +1,513 @@ +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_single_type_script, + /// 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_single_type_script => 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 Flags = packed struct(u8) { + contains_import_meta: bool = false, + is_typescript: bool = false, + _padding: u6 = 0, +}; + +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, + flags: Flags, + owner: union(enum) { + module_info, + allocated_slice: struct { + slice: []const u8, + allocator: std.mem.Allocator, + }, + }, + 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 { + const duped = try gpa.dupe(u8, source); + errdefer gpa.free(duped); + var rem: []const u8 = duped; + const res = try gpa.create(ModuleInfoDeserialized); + errdefer gpa.destroy(res); + + 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))); + _ = try eat(&rem, (4 - (record_kinds_len % 4)) % 4); // alignment padding + + 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 flags: Flags = @bitCast((try eatC(&rem, 1))[0]); + _ = try eat(&rem, 3); // alignment padding + + 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, + .flags = flags, + .owner = .{ .allocated_slice = .{ + .slice = duped, + .allocator = gpa, + } }, + }; + return res; + } + + /// Wrapper around `create` for use when loading from a cache (transpiler cache or standalone module graph). + /// Returns `null` instead of panicking on corrupt/truncated data. + pub fn createFromCachedRecord(source: []const u8, gpa: std.mem.Allocator) ?*ModuleInfoDeserialized { + return create(source, gpa) catch |e| switch (e) { + error.OutOfMemory => bun.outOfMemory(), + error.BadModuleInfo => null, + }; + } + + 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.writeByteNTimes(0, (4 - (self.record_kinds.len % 4)) % 4); // alignment padding + + 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.writeByte(@bitCast(self.flags)); + try writer.writeByteNTimes(0, 3); // alignment padding + + 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) { + _, +}; +pub const StringContext = struct { + strings_buf: []const u8, + strings_lens: []const u32, + + pub fn hash(_: @This(), s: []const u8) u32 { + return @as(u32, @truncate(std.hash.Wyhash.hash(0, s))); + } + pub fn eql(self: @This(), fetch_key: []const u8, item_key: StringMapKey, item_i: usize) bool { + return bun.strings.eqlLong(fetch_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, void, true), + strings_buf: std.ArrayListUnmanaged(u8), + strings_lens: std.ArrayListUnmanaged(u32), + requested_modules: std.AutoArrayHashMap(StringID, FetchParameters), + buffer: std.ArrayListUnmanaged(StringID), + record_kinds: std.ArrayListUnmanaged(RecordKind), + flags: Flags, + exported_names: std.AutoArrayHashMapUnmanaged(StringID, void), + finalized: bool = false, + + /// only initialized after .finalize() is called + _deserialized: ModuleInfoDeserialized, + + 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: StringID, 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(self.gpa, kind); + try self.buffer.appendSlice(self.gpa, data); + } + pub fn addDeclaredVariable(self: *ModuleInfo, id: StringID) !void { + try self._addRecord(.declared_variable, &.{id}); + } + pub fn addLexicalVariable(self: *ModuleInfo, id: StringID) !void { + try self._addRecord(.lexical_variable, &.{id}); + } + pub fn addImportInfoSingle(self: *ModuleInfo, module_name: StringID, import_name: StringID, local_name: StringID, only_used_as_type: bool) !void { + try self._addRecord(if (only_used_as_type) .import_info_single_type_script else .import_info_single, &.{ module_name, import_name, local_name }); + } + pub fn addImportInfoNamespace(self: *ModuleInfo, module_name: StringID, local_name: StringID) !void { + try self._addRecord(.import_info_namespace, &.{ module_name, try self.str("*"), local_name }); + } + pub fn addExportInfoIndirect(self: *ModuleInfo, export_name: StringID, import_name: StringID, module_name: StringID) !void { + if (try self._hasOrAddExportedName(export_name)) return; // a syntax error will be emitted later in this case + try self._addRecord(.export_info_indirect, &.{ export_name, import_name, module_name }); + } + pub fn addExportInfoLocal(self: *ModuleInfo, export_name: StringID, local_name: StringID) !void { + if (try self._hasOrAddExportedName(export_name)) return; // a syntax error will be emitted later in this case + try self._addRecord(.export_info_local, &.{ export_name, local_name, @enumFromInt(std.math.maxInt(u32)) }); + } + pub fn addExportInfoNamespace(self: *ModuleInfo, export_name: StringID, module_name: StringID) !void { + if (try self._hasOrAddExportedName(export_name)) return; // a syntax error will be emitted later in this case + try self._addRecord(.export_info_namespace, &.{ export_name, module_name }); + } + pub fn addExportInfoStar(self: *ModuleInfo, module_name: StringID) !void { + try self._addRecord(.export_info_star, &.{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, is_typescript: bool) !*ModuleInfo { + const res = try gpa.create(ModuleInfo); + res.* = ModuleInfo.init(gpa, is_typescript); + return res; + } + fn init(allocator: std.mem.Allocator, is_typescript: bool) ModuleInfo { + return .{ + .gpa = allocator, + .strings_map = .{}, + .strings_buf = .{}, + .strings_lens = .{}, + .exported_names = .{}, + .requested_modules = std.AutoArrayHashMap(StringID, FetchParameters).init(allocator), + .buffer = .empty, + .record_kinds = .empty, + .flags = .{ .contains_import_meta = false, .is_typescript = is_typescript }, + ._deserialized = undefined, + }; + } + 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.gpa); + self.record_kinds.deinit(self.gpa); + } + pub fn destroy(self: *ModuleInfo) void { + const alloc = self.gpa; + self.deinit(); + alloc.destroy(self); + } + pub fn str(self: *ModuleInfo, value: []const u8) !StringID { + try self.strings_buf.ensureUnusedCapacity(self.gpa, value.len); + try self.strings_lens.ensureUnusedCapacity(self.gpa, 1); + const gpres = try self.strings_map.getOrPutAdapted(self.gpa, value, StringContext{ + .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.* = {}; + self.strings_buf.appendSliceAssumeCapacity(value); + self.strings_lens.appendAssumeCapacity(@as(u32, @truncate(value.len))); + return @enumFromInt(@as(u32, @intCast(gpres.index))); + } + pub fn requestModule(self: *ModuleInfo, import_record_path: StringID, 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(import_record_path); + if (!gpres.found_existing) gpres.value_ptr.* = fetch_parameters; + } + + /// Replace all occurrences of old_id with new_id in records and requested_modules. + /// Used to fix up cross-chunk import specifiers after final paths are computed. + pub fn replaceStringID(self: *ModuleInfo, old_id: StringID, new_id: StringID) void { + bun.assert(!self.finalized); + // Replace in record buffer + for (self.buffer.items) |*item| { + if (item.* == old_id) item.* = new_id; + } + // Replace in requested_modules keys (preserving insertion order) + if (self.requested_modules.getIndex(old_id)) |idx| { + self.requested_modules.keys()[idx] = new_id; + self.requested_modules.reIndex() catch {}; + } + } + + /// 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, record_kinds_idx: usize }).init(bun.default_allocator); + defer local_name_to_module_name.deinit(); + { + var i: usize = 0; + for (self.record_kinds.items, 0..) |k, idx| { + if (k == .import_info_single or k == .import_info_single_type_script) { + 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], .record_kinds_idx = idx }); + } + 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; + // In TypeScript, the re-exported import may target a type-only + // export that was elided. Convert the import to SingleTypeScript + // so JSC tolerates it being NotFound during linking. + if (self.flags.is_typescript) { + self.record_kinds.items[ip.record_kinds_idx] = .import_info_single_type_script; + } + } + } + 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, + .flags = self.flags, + .owner = .module_info, + }; + + self.finalized = true; + } +}; +pub const StringID = enum(u32) { + star_default = std.math.maxInt(u32), + star_namespace = std.math.maxInt(u32) - 1, + _, +}; + +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 formatter = DiffFormatter{ + .received_string = received_ptr[0..received_len], + .expected_string = expected_ptr[0..expected_len], + .globalThis = globalThis, + }; + bun.Output.errorWriter().print("DIFF:\n{any}\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 { + 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]; + 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(vm, identifiers, res.buffer[i]), + .lexical_variable => lexical_variables.add(vm, identifiers, res.buffer[i]), + .import_info_single, .import_info_single_type_script, .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.flags.contains_import_meta, res.flags.is_typescript); + + 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_single_type_script => module_record.addImportEntrySingleTypeScript(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.destroy(); +} + +const VariableEnvironment = opaque { + extern fn JSC__VariableEnvironment__add(environment: *VariableEnvironment, vm: *bun.jsc.VM, 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); + } +}; +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, is_typescript: 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__addImportEntrySingleTypeScript(module_record: *JSModuleRecord, identifier_array: *IdentifierArray, import_name: StringID, local_name: StringID, module_name: StringID) void; + pub const addImportEntrySingleTypeScript = JSC_JSModuleRecord__addImportEntrySingleTypeScript; + 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; +}; + +export fn zig_log(msg: [*:0]const u8) void { + bun.Output.errorWriter().print("{s}\n", .{std.mem.span(msg)}) catch {}; +} + +const bun = @import("bun"); +const std = @import("std"); +const DiffFormatter = @import("./bun.js/test/diff_format.zig").DiffFormatter; diff --git a/src/ast/Ast.zig b/src/ast/Ast.zig index 9aa1386f1a..a7d5101716 100644 --- a/src/ast/Ast.zig +++ b/src/ast/Ast.zig @@ -68,6 +68,7 @@ ts_enums: TsEnumsMap = .{}, /// This is a list of named exports that may exist in a CommonJS module /// We use this with `commonjs_at_runtime` to re-export CommonJS has_commonjs_export_names: bool = false, +has_import_meta: bool = false, import_meta_ref: Ref = Ref.None, pub const CommonJSNamedExport = struct { diff --git a/src/ast/BundledAst.zig b/src/ast/BundledAst.zig index 0398b3ab59..63c63d28cb 100644 --- a/src/ast/BundledAst.zig +++ b/src/ast/BundledAst.zig @@ -52,7 +52,7 @@ ts_enums: Ast.TsEnumsMap = .{}, flags: BundledAst.Flags = .{}, -pub const Flags = packed struct(u8) { +pub const Flags = packed struct(u16) { // This is a list of CommonJS features. When a file uses CommonJS features, // it's not a candidate for "flat bundling" and must be wrapped in its own // closure. @@ -65,6 +65,8 @@ pub const Flags = packed struct(u8) { has_lazy_export: bool = false, commonjs_module_exports_assigned_deoptimized: bool = false, has_explicit_use_strict_directive: bool = false, + has_import_meta: bool = false, + _padding: u7 = 0, }; pub const empty = BundledAst.init(Ast.empty); @@ -116,6 +118,7 @@ pub fn toAST(this: *const BundledAst) Ast { .has_lazy_export = this.flags.has_lazy_export, .commonjs_module_exports_assigned_deoptimized = this.flags.commonjs_module_exports_assigned_deoptimized, .directive = if (this.flags.has_explicit_use_strict_directive) "use strict" else null, + .has_import_meta = this.flags.has_import_meta, }; } @@ -168,6 +171,7 @@ pub fn init(ast: Ast) BundledAst { .has_lazy_export = ast.has_lazy_export, .commonjs_module_exports_assigned_deoptimized = ast.commonjs_module_exports_assigned_deoptimized, .has_explicit_use_strict_directive = strings.eqlComptime(ast.directive orelse "", "use strict"), + .has_import_meta = ast.has_import_meta, }, }; } diff --git a/src/ast/P.zig b/src/ast/P.zig index 5dbcb1b03b..9d2b4e126e 100644 --- a/src/ast/P.zig +++ b/src/ast/P.zig @@ -6591,6 +6591,7 @@ pub fn NewParser_( .top_level_await_keyword = p.top_level_await_keyword, .commonjs_named_exports = p.commonjs_named_exports, .has_commonjs_export_names = p.has_commonjs_export_names, + .has_import_meta = p.has_import_meta, .hashbang = hashbang, // TODO: cross-module constant inlining diff --git a/src/bake/production.zig b/src/bake/production.zig index bacdb61dd1..f38ad9d49f 100644 --- a/src/bake/production.zig +++ b/src/bake/production.zig @@ -433,6 +433,7 @@ pub fn buildWithVm(ctx: bun.cli.Command.Context, cwd: []const u8, vm: *VirtualMa .asset => {}, .bytecode => {}, .sourcemap => {}, + .module_info => {}, .@"metafile-json", .@"metafile-markdown" => {}, } }, diff --git a/src/bun.js/AsyncModule.zig b/src/bun.js/AsyncModule.zig index bce85737f6..055567d184 100644 --- a/src/bun.js/AsyncModule.zig +++ b/src/bun.js/AsyncModule.zig @@ -694,6 +694,7 @@ pub const AsyncModule = struct { &printer, .esm_ascii, mapper.get(), + null, ); } diff --git a/src/bun.js/ModuleLoader.zig b/src/bun.js/ModuleLoader.zig index a1858839d4..57e8497ad9 100644 --- a/src/bun.js/ModuleLoader.zig +++ b/src/bun.js/ModuleLoader.zig @@ -178,6 +178,7 @@ pub fn transpileSourceCode( 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; @@ -422,6 +423,10 @@ pub fn transpileSourceCode( dumpSourceString(jsc_vm, specifier, entry.output_code.byteSlice()); } + // TODO: module_info is only needed for standalone ESM bytecode. + // For now, skip it entirely in the runtime transpiler. + const module_info: ?*analyze_transpiled_module.ModuleInfoDeserialized = null; + return ResolvedSource{ .allocator = null, .source_code = switch (entry.output_code) { @@ -436,6 +441,7 @@ pub fn transpileSourceCode( .specifier = input_specifier, .source_url = input_specifier.createIfDifferent(path.text), .is_commonjs_module = entry.metadata.module_type == .cjs, + .module_info = module_info, .tag = brk: { if (entry.metadata.module_type == .cjs and source.path.isFile()) { const actual_package_json: *PackageJSON = package_json orelse brk2: { @@ -504,6 +510,11 @@ pub fn transpileSourceCode( jsc_vm.resolved_count += jsc_vm.transpiler.linker.import_counter - start_count; jsc_vm.transpiler.linker.import_counter = 0; + const is_commonjs_module = parse_result.ast.has_commonjs_export_names or parse_result.ast.exports_kind == .cjs; + // TODO: module_info is only needed for standalone ESM bytecode. + // For now, skip it entirely in the runtime transpiler. + const module_info: ?*analyze_transpiled_module.ModuleInfo = null; + var printer = source_code_printer.*; printer.ctx.reset(); defer source_code_printer.* = printer; @@ -516,6 +527,7 @@ pub fn transpileSourceCode( &printer, .esm_ascii, mapper.get(), + module_info, ); }; @@ -529,9 +541,12 @@ pub fn transpileSourceCode( } } + const module_info_deserialized: ?*anyopaque = if (module_info) |mi| @ptrCast(mi.asDeserialized()) else null; + if (jsc_vm.isWatcherEnabled()) { var resolved_source = jsc_vm.refCountedResolvedSource(printer.ctx.written, input_specifier, path.text, null, false); - 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; + resolved_source.module_info = module_info_deserialized; return resolved_source; } @@ -564,7 +579,8 @@ pub fn transpileSourceCode( }, .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, + .module_info = module_info_deserialized, .tag = tag, }; }, @@ -1192,9 +1208,15 @@ pub fn fetchBuiltinModule(jsc_vm: *VirtualMachine, specifier: bun.String) !?Reso .source_code = file.toWTFString(), .specifier = specifier, .source_url = specifier.dupeRef(), + // bytecode_origin_path is the path used when generating bytecode; must match for cache hits + .bytecode_origin_path = if (file.bytecode_origin_path.len > 0) bun.String.fromBytes(file.bytecode_origin_path) else bun.String.empty, .source_code_needs_deref = false, .bytecode_cache = if (file.bytecode.len > 0) file.bytecode.ptr else null, .bytecode_cache_size = file.bytecode.len, + .module_info = if (file.module_info.len > 0) + analyze_transpiled_module.ModuleInfoDeserialized.createFromCachedRecord(file.module_info, bun.default_allocator) + else + null, .is_commonjs_module = file.module_format == .cjs, }; } @@ -1324,6 +1346,7 @@ const string = []const u8; const Fs = @import("../fs.zig"); const Runtime = @import("../runtime.zig"); +const analyze_transpiled_module = @import("../analyze_transpiled_module.zig"); const ast = @import("../import_record.zig"); const node_module_module = @import("./bindings/NodeModuleModule.zig"); const std = @import("std"); diff --git a/src/bun.js/RuntimeTranspilerCache.zig b/src/bun.js/RuntimeTranspilerCache.zig index 3f8125de1a..31a1a46267 100644 --- a/src/bun.js/RuntimeTranspilerCache.zig +++ b/src/bun.js/RuntimeTranspilerCache.zig @@ -14,7 +14,8 @@ /// Version 15: Updated global defines table list. /// Version 16: Added typeof undefined minification optimization. /// Version 17: Removed transpiler import rewrite for bun:test. Not bumping it causes test/js/bun/http/req-url-leak.test.ts to fail with SyntaxError: Export named 'expect' not found in module 'bun:test'. -const expected_version = 17; +/// Version 18: Include ESM record (module info) with an ES Module, see #15758 +const expected_version = 18; const debug = Output.scoped(.cache, .visible); const MINIMUM_CACHE_SIZE = 50 * 1024; @@ -32,6 +33,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 { @@ -52,6 +54,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 = .{}; @@ -78,6 +84,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 { @@ -102,6 +112,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 @@ -120,6 +134,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, @@ -142,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( @@ -156,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.ast.ExportsKind, ) !void { @@ -201,10 +220,16 @@ 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); metadata.sourcemap_hash = hash(sourcemap); + if (esm_record.len > 0) { + metadata.esm_record_hash = hash(esm_record); + } + var metadata_stream = std.io.fixedBufferStream(&metadata_buf); try metadata.encode(metadata_stream.writer()); @@ -219,20 +244,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; @@ -242,7 +273,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.sys.preallocate_file(tmpfile.fd.cast(), 0, @intCast(end_position)) catch {}; while (position < end_position) { @@ -263,6 +294,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) { @@ -338,6 +370,23 @@ 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; + } + + if (this.metadata.esm_record_hash != 0) { + if (hash(esm_record) != this.metadata.esm_record_hash) { + return error.InvalidHash; + } + } + + this.esm_record = esm_record; + } } }; @@ -455,6 +504,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.perf.trace("RuntimeTranspilerCache.fromFile"); defer tracer.end(); @@ -469,6 +519,7 @@ pub const RuntimeTranspilerCache = struct { input_stat_size, sourcemap_allocator, output_code_allocator, + esm_record_allocator, ); } @@ -479,6 +530,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(); @@ -510,7 +562,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; } @@ -527,6 +579,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.ast.ExportsKind, ) !void { @@ -566,6 +619,7 @@ pub const RuntimeTranspilerCache = struct { input_hash, features_hash, sourcemap, + esm_record, output_code, exports_kind, ); @@ -599,7 +653,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; }; @@ -615,7 +669,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; } } @@ -624,7 +678,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"); @@ -635,7 +689,7 @@ pub const RuntimeTranspilerCache = struct { const output_code = bun.String.cloneLatin1(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/RuntimeTranspilerStore.zig b/src/bun.js/RuntimeTranspilerStore.zig index b43de4516a..8374819bf1 100644 --- a/src/bun.js/RuntimeTranspilerStore.zig +++ b/src/bun.js/RuntimeTranspilerStore.zig @@ -315,6 +315,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 log = logger.Log.init(allocator); defer { @@ -471,6 +472,10 @@ pub const RuntimeTranspilerStore = struct { dumpSourceString(vm, specifier, entry.output_code.byteSlice()); } + // TODO: module_info is only needed for standalone ESM bytecode. + // For now, skip it entirely in the runtime transpiler. + const module_info: ?*analyze_transpiled_module.ModuleInfoDeserialized = null; + this.resolved_source = ResolvedSource{ .allocator = null, .source_code = switch (entry.output_code) { @@ -483,6 +488,7 @@ pub const RuntimeTranspilerStore = struct { }, }, .is_commonjs_module = entry.metadata.module_type == .cjs, + .module_info = module_info, .tag = this.resolved_source.tag, }; @@ -541,6 +547,11 @@ pub const RuntimeTranspilerStore = struct { printer = source_code_printer.?.*; } + const is_commonjs_module = parse_result.ast.has_commonjs_export_names or parse_result.ast.exports_kind == .cjs; + // TODO: module_info is only needed for standalone ESM bytecode. + // For now, skip it entirely in the runtime transpiler. + const module_info: ?*analyze_transpiled_module.ModuleInfo = null; + { var mapper = vm.sourceMapHandler(&printer); defer source_code_printer.?.* = printer; @@ -550,7 +561,9 @@ pub const RuntimeTranspilerStore = struct { &printer, .esm_ascii, mapper.get(), + module_info, ) catch |err| { + if (module_info) |mi| mi.destroy(); this.parse_error = err; return; }; @@ -589,7 +602,8 @@ pub const RuntimeTranspilerStore = struct { this.resolved_source = ResolvedSource{ .allocator = null, .source_code = source_code, - .is_commonjs_module = parse_result.ast.has_commonjs_export_names or parse_result.ast.exports_kind == .cjs, + .is_commonjs_module = is_commonjs_module, + .module_info = if (module_info) |mi| @ptrCast(mi.asDeserialized()) else null, .tag = this.resolved_source.tag, }; } @@ -597,6 +611,7 @@ pub const RuntimeTranspilerStore = struct { }; const Fs = @import("../fs.zig"); +const analyze_transpiled_module = @import("../analyze_transpiled_module.zig"); const node_fallbacks = @import("../node_fallbacks.zig"); const std = @import("std"); const AsyncModule = @import("./AsyncModule.zig").AsyncModule; diff --git a/src/bun.js/api/JSBundler.zig b/src/bun.js/api/JSBundler.zig index af8ba45e79..9de645a85c 100644 --- a/src/bun.js/api/JSBundler.zig +++ b/src/bun.js/api/JSBundler.zig @@ -675,8 +675,8 @@ pub const JSBundler = struct { if (try config.getOptionalEnum(globalThis, "format", options.Format)) |format| { this.format = format; - if (this.bytecode and format != .cjs) { - return globalThis.throwInvalidArguments("format must be 'cjs' when bytecode is true. Eventually we'll add esm support as well.", .{}); + if (this.bytecode and format != .cjs and format != .esm) { + return globalThis.throwInvalidArguments("format must be 'cjs' or 'esm' when bytecode is true.", .{}); } } @@ -1717,11 +1717,12 @@ pub const BuildArtifact = struct { @"entry-point", sourcemap, bytecode, + module_info, @"metafile-json", @"metafile-markdown", pub fn isFileInStandaloneMode(this: OutputKind) bool { - return this != .sourcemap and this != .bytecode and this != .@"metafile-json" and this != .@"metafile-markdown"; + return this != .sourcemap and this != .bytecode and this != .module_info and this != .@"metafile-json" and this != .@"metafile-markdown"; } }; diff --git a/src/bun.js/bindings/BunAnalyzeTranspiledModule.cpp b/src/bun.js/bindings/BunAnalyzeTranspiledModule.cpp new file mode 100644 index 0000000000..dd8684948a --- /dev/null +++ b/src/bun.js/bindings/BunAnalyzeTranspiledModule.cpp @@ -0,0 +1,337 @@ +#include "root.h" + +#include "JavaScriptCore/JSInternalPromise.h" +#include "JavaScriptCore/JSModuleRecord.h" +#include "JavaScriptCore/GlobalObjectMethodTable.h" +#include "JavaScriptCore/Nodes.h" +#include "JavaScriptCore/Parser.h" +#include "JavaScriptCore/ParserError.h" +#include "JavaScriptCore/SyntheticModuleRecord.h" +#include +#include "JavaScriptCore/JSGlobalObject.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 + +#include "JavaScriptCore/ModuleAnalyzer.h" +#include "JavaScriptCore/ErrorType.h" + +namespace JSC { + +String dumpRecordInfo(JSModuleRecord* moduleRecord); + +Identifier getFromIdentifierArray(VM& vm, Identifier* identifierArray, uint32_t n) +{ + if (n == std::numeric_limits::max()) { + return vm.propertyNames->starDefaultPrivateName; + } + return identifierArray[n]; +} + +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__VariableEnvironment__add(VariableEnvironment& environment, VM& vm, Identifier* identifierArray, uint32_t index) +{ + environment.add(getFromIdentifierArray(vm, identifierArray, index)); +} + +extern "C" VariableEnvironment* JSC_JSModuleRecord__declaredVariables(JSModuleRecord* moduleRecord) +{ + return const_cast(&moduleRecord->declaredVariables()); +} +extern "C" VariableEnvironment* JSC_JSModuleRecord__lexicalVariables(JSModuleRecord* moduleRecord) +{ + return const_cast(&moduleRecord->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, bool isTypescript) +{ + JSModuleRecord* result = JSModuleRecord::create(globalObject, vm, globalObject->moduleRecordStructure(), *moduleKey, sourceCode, declaredVariables, lexicalVariables, hasImportMeta ? ImportMetaFeature : 0); + result->m_isTypeScript = isTypescript; + return result; +} + +extern "C" void JSC_JSModuleRecord__addIndirectExport(JSModuleRecord* moduleRecord, Identifier* identifierArray, uint32_t exportName, uint32_t importName, uint32_t moduleName) +{ + moduleRecord->addExportEntry(JSModuleRecord::ExportEntry::createIndirect(getFromIdentifierArray(moduleRecord->vm(), identifierArray, exportName), getFromIdentifierArray(moduleRecord->vm(), identifierArray, importName), getFromIdentifierArray(moduleRecord->vm(), identifierArray, moduleName))); +} +extern "C" void JSC_JSModuleRecord__addLocalExport(JSModuleRecord* moduleRecord, Identifier* identifierArray, uint32_t exportName, uint32_t localName) +{ + moduleRecord->addExportEntry(JSModuleRecord::ExportEntry::createLocal(getFromIdentifierArray(moduleRecord->vm(), identifierArray, exportName), getFromIdentifierArray(moduleRecord->vm(), identifierArray, localName))); +} +extern "C" void JSC_JSModuleRecord__addNamespaceExport(JSModuleRecord* moduleRecord, Identifier* identifierArray, uint32_t exportName, uint32_t moduleName) +{ + moduleRecord->addExportEntry(JSModuleRecord::ExportEntry::createNamespace(getFromIdentifierArray(moduleRecord->vm(), identifierArray, exportName), getFromIdentifierArray(moduleRecord->vm(), identifierArray, moduleName))); +} +extern "C" void JSC_JSModuleRecord__addStarExport(JSModuleRecord* moduleRecord, Identifier* identifierArray, uint32_t moduleName) +{ + moduleRecord->addStarExportEntry(getFromIdentifierArray(moduleRecord->vm(), identifierArray, moduleName)); +} +extern "C" void JSC_JSModuleRecord__addRequestedModuleNullAttributesPtr(JSModuleRecord* moduleRecord, Identifier* identifierArray, uint32_t moduleName) +{ + RefPtr attributes = RefPtr {}; + moduleRecord->appendRequestedModule(getFromIdentifierArray(moduleRecord->vm(), identifierArray, moduleName), std::move(attributes)); +} +extern "C" void JSC_JSModuleRecord__addRequestedModuleJavaScript(JSModuleRecord* moduleRecord, Identifier* identifierArray, uint32_t moduleName) +{ + Ref attributes = ScriptFetchParameters::create(ScriptFetchParameters::Type::JavaScript); + moduleRecord->appendRequestedModule(getFromIdentifierArray(moduleRecord->vm(), identifierArray, moduleName), std::move(attributes)); +} +extern "C" void JSC_JSModuleRecord__addRequestedModuleWebAssembly(JSModuleRecord* moduleRecord, Identifier* identifierArray, uint32_t moduleName) +{ + Ref attributes = ScriptFetchParameters::create(ScriptFetchParameters::Type::WebAssembly); + moduleRecord->appendRequestedModule(getFromIdentifierArray(moduleRecord->vm(), identifierArray, moduleName), std::move(attributes)); +} +extern "C" void JSC_JSModuleRecord__addRequestedModuleJSON(JSModuleRecord* moduleRecord, Identifier* identifierArray, uint32_t moduleName) +{ + Ref attributes = ScriptFetchParameters::create(ScriptFetchParameters::Type::JSON); + moduleRecord->appendRequestedModule(getFromIdentifierArray(moduleRecord->vm(), identifierArray, moduleName), std::move(attributes)); +} +extern "C" void JSC_JSModuleRecord__addRequestedModuleHostDefined(JSModuleRecord* moduleRecord, Identifier* identifierArray, uint32_t moduleName, uint32_t hostDefinedImportType) +{ + Ref attributes = ScriptFetchParameters::create(getFromIdentifierArray(moduleRecord->vm(), identifierArray, hostDefinedImportType).string()); + moduleRecord->appendRequestedModule(getFromIdentifierArray(moduleRecord->vm(), identifierArray, moduleName), std::move(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 = getFromIdentifierArray(moduleRecord->vm(), identifierArray, moduleName), + .importName = getFromIdentifierArray(moduleRecord->vm(), identifierArray, importName), + .localName = getFromIdentifierArray(moduleRecord->vm(), identifierArray, localName), + }); +} +extern "C" void JSC_JSModuleRecord__addImportEntrySingleTypeScript(JSModuleRecord* moduleRecord, Identifier* identifierArray, uint32_t importName, uint32_t localName, uint32_t moduleName) +{ + moduleRecord->addImportEntry(JSModuleRecord::ImportEntry { + .type = JSModuleRecord::ImportEntryType::SingleTypeScript, + .moduleRequest = getFromIdentifierArray(moduleRecord->vm(), identifierArray, moduleName), + .importName = getFromIdentifierArray(moduleRecord->vm(), identifierArray, importName), + .localName = getFromIdentifierArray(moduleRecord->vm(), 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 = getFromIdentifierArray(moduleRecord->vm(), identifierArray, moduleName), + .importName = getFromIdentifierArray(moduleRecord->vm(), identifierArray, importName), + .localName = getFromIdentifierArray(moduleRecord->vm(), 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(vm, 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, static_cast(provider->m_resolvedSource.module_info)); + // zig__ModuleInfoDeserialized__toJSModuleRecord consumes and frees the module_info. + // Null it out to prevent use-after-free via the dangling pointer. + provider->m_resolvedSource.module_info = nullptr; + if (moduleRecord == nullptr) { + RELEASE_AND_RETURN(scope, JSValue::encode(rejectWithError(createError(globalObject, WTF::String::fromLatin1("parseFromSourceCode failed"))))); + } + +#if BUN_DEBUG + RELEASE_AND_RETURN(scope, fallbackParse(globalObject, moduleKey, sourceCode, promise, moduleRecord)); +#else + promise->resolve(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(vm, 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] = std::move(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 --- module key ---\n", moduleKey.utf8().data(), "\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->resolve(globalObject, resultValue == nullptr ? moduleRecord : resultValue); + return JSValue::encode(promise); +} + +String dumpRecordInfo(JSModuleRecord* moduleRecord) +{ + WTF::StringPrintStream stream; + + { + Vector sortedVars; + for (const auto& pair : moduleRecord->declaredVariables()) + sortedVars.append(String(pair.key.get())); + std::sort(sortedVars.begin(), sortedVars.end(), [](const String& a, const String& b) { + return codePointCompare(a, b) < 0; + }); + stream.print(" varDeclarations:\n"); + for (const auto& name : sortedVars) + stream.print(" - ", name, "\n"); + } + + { + Vector sortedVars; + for (const auto& pair : moduleRecord->lexicalVariables()) + sortedVars.append(String(pair.key.get())); + std::sort(sortedVars.begin(), sortedVars.end(), [](const String& a, const String& b) { + return codePointCompare(a, b) < 0; + }); + stream.print(" lexicalVariables:\n"); + for (const auto& name : sortedVars) + stream.print(" - ", name, "\n"); + } + + stream.print(" features: (not accessible)\n"); + + stream.print("\nAnalyzing ModuleRecord key(", moduleRecord->moduleKey().impl(), ")\n"); + + stream.print(" Dependencies: ", moduleRecord->requestedModules().size(), " modules\n"); + { + Vector sortedDeps; + for (const auto& request : moduleRecord->requestedModules()) { + WTF::StringPrintStream line; + if (request.m_attributes == nullptr) + line.print(" module(", request.m_specifier, ")\n"); + else + line.print(" module(", request.m_specifier, "),attributes(", (uint8_t)request.m_attributes->type(), ", ", request.m_attributes->hostDefinedImportType(), ")\n"); + sortedDeps.append(line.toString()); + } + std::sort(sortedDeps.begin(), sortedDeps.end(), [](const String& a, const String& b) { + return codePointCompare(a, b) < 0; + }); + for (const auto& dep : sortedDeps) + stream.print(dep); + } + + stream.print(" Import: ", moduleRecord->importEntries().size(), " entries\n"); + { + Vector sortedImports; + for (const auto& pair : moduleRecord->importEntries()) { + WTF::StringPrintStream line; + auto& importEntry = pair.value; + line.print(" import(", importEntry.importName, "), local(", importEntry.localName, "), module(", importEntry.moduleRequest, ")\n"); + sortedImports.append(line.toString()); + } + std::sort(sortedImports.begin(), sortedImports.end(), [](const String& a, const String& b) { + return codePointCompare(a, b) < 0; + }); + for (const auto& imp : sortedImports) + stream.print(imp); + } + + 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 codePointCompare(a, b) < 0; + }); + for (const auto& entry : sortedEntries) + stream.print(entry); + + { + Vector sortedStarExports; + for (const auto& moduleName : moduleRecord->starExportEntries()) { + WTF::StringPrintStream line; + line.print(" [Star] module(", moduleName.get(), ")\n"); + sortedStarExports.append(line.toString()); + } + std::sort(sortedStarExports.begin(), sortedStarExports.end(), [](const String& a, const String& b) { + return codePointCompare(a, b) < 0; + }); + for (const auto& entry : sortedStarExports) + stream.print(entry); + } + + 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/ResolvedSource.zig b/src/bun.js/bindings/ResolvedSource.zig index a105592b36..d11394a89b 100644 --- a/src/bun.js/bindings/ResolvedSource.zig +++ b/src/bun.js/bindings/ResolvedSource.zig @@ -24,8 +24,16 @@ pub const ResolvedSource = extern struct { /// This is for source_code source_code_needs_deref: bool = true, already_bundled: bool = false, + + // -- Bytecode cache fields -- bytecode_cache: ?[*]u8 = null, bytecode_cache_size: usize = 0, + module_info: ?*anyopaque = null, + /// The file path used as the source origin for bytecode cache validation. + /// JSC validates bytecode by checking if the origin URL matches exactly what + /// was used at build time. If empty, the origin is derived from source_url. + /// This is converted to a file:// URL on the C++ side. + bytecode_origin_path: bun.String = bun.String.empty, pub const Tag = @import("ResolvedSourceTag").ResolvedSourceTag; }; diff --git a/src/bun.js/bindings/ZigSourceProvider.cpp b/src/bun.js/bindings/ZigSourceProvider.cpp index ff17060d39..dcb4b32297 100644 --- a/src/bun.js/bindings/ZigSourceProvider.cpp +++ b/src/bun.js/bindings/ZigSourceProvider.cpp @@ -75,6 +75,14 @@ Ref SourceProvider::create( JSC::SourceProviderSourceType sourceType, bool isBuiltin) { + // Use BunTranspiledModule when module_info is present. + // This allows JSC to skip parsing during the analyze phase (uses pre-computed imports/exports). + // Bytecode cache (if present) is used separately during the evaluate phase. + if (resolvedSource.module_info != nullptr) { + ASSERT(!resolvedSource.isCommonJSModule); + sourceType = JSC::SourceProviderSourceType::BunTranspiledModule; + } + auto string = resolvedSource.source_code.toWTFString(BunString::ZeroCopy); auto sourceURLString = resolvedSource.source_url.toWTFString(BunString::ZeroCopy); @@ -91,6 +99,18 @@ Ref SourceProvider::create( // https://github.com/oven-sh/bun/issues/9521 } + // Compute source origin: use explicit bytecode_origin_path if provided, otherwise derive from source_url. + // bytecode_origin_path is used for bytecode cache validation where the origin must match + // exactly what was used at build time. + const auto getSourceOrigin = [&]() -> SourceOrigin { + auto bytecodeOriginPath = resolvedSource.bytecode_origin_path.toWTFString(BunString::ZeroCopy); + if (!bytecodeOriginPath.isNull() && !bytecodeOriginPath.isEmpty()) { + // Convert file path to file:// URL (same as build time) + return SourceOrigin(WTF::URL::fileURLWithFileSystemPath(bytecodeOriginPath)); + } + return toSourceOrigin(sourceURLString, isBuiltin); + }; + const auto getProvider = [&]() -> Ref { if (resolvedSource.bytecode_cache != nullptr) { const auto destructorPtr = [](const void* ptr) { @@ -101,13 +121,15 @@ Ref SourceProvider::create( }; const auto destructor = resolvedSource.needsDeref ? destructorPtr : destructorNoOp; + auto origin = getSourceOrigin(); + Ref bytecode = JSC::CachedBytecode::create(std::span(resolvedSource.bytecode_cache, resolvedSource.bytecode_cache_size), destructor, {}); auto provider = adoptRef(*new SourceProvider( globalObject->isThreadLocalDefaultGlobalObject ? globalObject : nullptr, resolvedSource, string.isNull() ? *StringImpl::empty() : *string.impl(), JSC::SourceTaintedOrigin::Untainted, - toSourceOrigin(sourceURLString, isBuiltin), + origin, sourceURLString.impl(), TextPosition(), sourceType)); provider->m_cachedBytecode = WTF::move(bytecode); @@ -119,7 +141,7 @@ Ref SourceProvider::create( resolvedSource, string.isNull() ? *StringImpl::empty() : *string.impl(), JSC::SourceTaintedOrigin::Untainted, - toSourceOrigin(sourceURLString, isBuiltin), + getSourceOrigin(), sourceURLString.impl(), TextPosition(), sourceType)); }; @@ -189,6 +211,8 @@ extern "C" bool generateCachedModuleByteCodeFromSourceCode(BunString* sourceProv auto key = JSC::sourceCodeKeyForSerializedModule(vm, sourceCode); + dataLogLnIf(JSC::Options::verboseDiskCache(), "[Bytecode Build] generateModule url=", sourceProviderURL->toWTFString(), " origin=", sourceCode.provider()->sourceOrigin().url().string(), " sourceSize=", inputSourceCodeSize, " keyHash=", key.hash()); + RefPtr cachedBytecode = JSC::encodeCodeBlock(vm, key, unlinkedCodeBlock); if (!cachedBytecode) return false; @@ -222,6 +246,8 @@ extern "C" bool generateCachedCommonJSProgramByteCodeFromSourceCode(BunString* s auto key = JSC::sourceCodeKeyForSerializedProgram(vm, sourceCode); + dataLogLnIf(JSC::Options::verboseDiskCache(), "[Bytecode Build] generateCJS url=", sourceProviderURL->toWTFString(), " origin=", sourceCode.provider()->sourceOrigin().url().string(), " sourceSize=", inputSourceCodeSize, " keyHash=", key.hash()); + RefPtr cachedBytecode = JSC::encodeCodeBlock(vm, key, unlinkedCodeBlock); if (!cachedBytecode) return false; diff --git a/src/bun.js/bindings/headers-handwritten.h b/src/bun.js/bindings/headers-handwritten.h index 43ea88864a..785e538062 100644 --- a/src/bun.js/bindings/headers-handwritten.h +++ b/src/bun.js/bindings/headers-handwritten.h @@ -116,8 +116,13 @@ typedef struct ResolvedSource { uint32_t tag; bool needsDeref; bool already_bundled; + // -- Bytecode cache fields -- uint8_t* bytecode_cache; size_t bytecode_cache_size; + void* module_info; + // File path used as source origin for bytecode cache validation. + // Converted to file:// URL. If empty, origin is derived from source_url. + BunString bytecode_origin_path; } ResolvedSource; static const uint32_t ResolvedSourceTagPackageJSONTypeModule = 1; typedef union ErrorableResolvedSourceResult { diff --git a/src/bundler/Chunk.zig b/src/bundler/Chunk.zig index ed3ae6c45a..d4eece7c71 100644 --- a/src/bundler/Chunk.zig +++ b/src/bundler/Chunk.zig @@ -502,6 +502,11 @@ pub const Chunk = struct { /// /// Mutated while sorting chunks in `computeChunks` css_chunks: []u32 = &.{}, + + /// Serialized ModuleInfo for ESM bytecode (--compile --bytecode --format=esm) + module_info_bytes: ?[]const u8 = null, + /// Unserialized ModuleInfo for deferred serialization (after chunk paths are resolved) + module_info: ?*analyze_transpiled_module.ModuleInfo = null, }; pub const CssChunk = struct { @@ -654,6 +659,7 @@ pub const ParseTask = bun.bundle_v2.ParseTask; const string = []const u8; const HTMLImportManifest = @import("./HTMLImportManifest.zig"); +const analyze_transpiled_module = @import("../analyze_transpiled_module.zig"); const std = @import("std"); const options = @import("../options.zig"); diff --git a/src/bundler/LinkerContext.zig b/src/bundler/LinkerContext.zig index 0a388937a0..edfd1e62d5 100644 --- a/src/bundler/LinkerContext.zig +++ b/src/bundler/LinkerContext.zig @@ -70,6 +70,7 @@ pub const LinkerContext = struct { css_chunking: bool = false, source_maps: options.SourceMapOption = .none, target: options.Target = .browser, + compile: bool = false, metafile: bool = false, /// Path to write JSON metafile (for Bun.build API) metafile_json_path: []const u8 = "", diff --git a/src/bundler/bundle_v2.zig b/src/bundler/bundle_v2.zig index 78e3fd1189..335ec3cc43 100644 --- a/src/bundler/bundle_v2.zig +++ b/src/bundler/bundle_v2.zig @@ -971,6 +971,7 @@ pub const BundleV2 = struct { this.linker.options.target = transpiler.options.target; this.linker.options.output_format = transpiler.options.output_format; this.linker.options.generate_bytecode_cache = transpiler.options.bytecode; + this.linker.options.compile = transpiler.options.compile; this.linker.options.metafile = transpiler.options.metafile; this.linker.options.metafile_json_path = transpiler.options.metafile_json_path; this.linker.options.metafile_markdown_path = transpiler.options.metafile_markdown_path; @@ -4508,9 +4509,19 @@ pub const CrossChunkImport = struct { }; pub const CompileResult = union(enum) { + pub const DeclInfo = struct { + pub const Kind = enum(u1) { declared, lexical }; + name: []const u8, + kind: Kind, + }; + javascript: struct { source_index: Index.Int, result: js_printer.PrintResult, + /// Top-level declarations collected from converted statements during + /// parallel printing. Used by postProcessJSChunk to populate ModuleInfo + /// without re-scanning the original (unconverted) AST. + decls: []const DeclInfo = &.{}, pub fn code(this: @This()) []const u8 { return switch (this.result) { diff --git a/src/bundler/linker_context/OutputFileListBuilder.zig b/src/bundler/linker_context/OutputFileListBuilder.zig index 47d1b6f1b9..6d49f03203 100644 --- a/src/bundler/linker_context/OutputFileListBuilder.zig +++ b/src/bundler/linker_context/OutputFileListBuilder.zig @@ -3,7 +3,7 @@ //! chunk indexing remains the same: //! //! 1. chunks -//! 2. sourcemaps and bytecode +//! 2. sourcemaps, bytecode, and module_info //! 3. additional output files //! //! We can calculate the space ahead of time and avoid having to do something @@ -41,7 +41,7 @@ pub fn init( chunks: []const bun.bundle_v2.Chunk, _: usize, ) !@This() { - const length, const source_map_and_bytecode_count = OutputFileList.calculateOutputFileListCapacity(c, chunks); + const length, const supplementary_file_count = OutputFileList.calculateOutputFileListCapacity(c, chunks); var output_files = try std.array_list.Managed(options.OutputFile).initCapacity( allocator, length, @@ -51,8 +51,8 @@ pub fn init( return .{ .output_files = output_files, .index_for_chunk = 0, - .index_for_sourcemaps_and_bytecode = if (source_map_and_bytecode_count == 0) null else @as(u32, @truncate(chunks.len)), - .additional_output_files_start = @as(u32, @intCast(chunks.len)) + source_map_and_bytecode_count, + .index_for_sourcemaps_and_bytecode = if (supplementary_file_count == 0) null else @as(u32, @truncate(chunks.len)), + .additional_output_files_start = @as(u32, @intCast(chunks.len)) + supplementary_file_count, .total_insertions = 0, }; } @@ -94,7 +94,10 @@ pub fn calculateOutputFileListCapacity(c: *const bun.bundle_v2.LinkerContext, ch break :bytecode_count bytecode_count; } else 0; - return .{ @intCast(chunks.len + source_map_count + bytecode_count + c.parse_graph.additional_output_files.items.len), @intCast(source_map_count + bytecode_count) }; + // module_info is generated for ESM bytecode in --compile builds + const module_info_count = if (c.options.generate_bytecode_cache and c.options.output_format == .esm and c.options.compile) bytecode_count else 0; + + return .{ @intCast(chunks.len + source_map_count + bytecode_count + module_info_count + c.parse_graph.additional_output_files.items.len), @intCast(source_map_count + bytecode_count + module_info_count) }; } pub fn insertForChunk(this: *OutputFileList, output_file: options.OutputFile) u32 { diff --git a/src/bundler/linker_context/generateChunksInParallel.zig b/src/bundler/linker_context/generateChunksInParallel.zig index acdc062e0d..d40ebcc3f8 100644 --- a/src/bundler/linker_context/generateChunksInParallel.zig +++ b/src/bundler/linker_context/generateChunksInParallel.zig @@ -304,6 +304,69 @@ pub fn generateChunksInParallel( } } + // After final_rel_path is computed for all chunks, fix up module_info + // cross-chunk import specifiers. During printing, cross-chunk imports use + // unique_key placeholders as paths. Now that final paths are known, replace + // those placeholders with the resolved paths and serialize. + if (c.options.generate_bytecode_cache and c.options.output_format == .esm and c.options.compile) { + // Build map from unique_key -> final resolved path + const b = @as(*bun.bundle_v2.BundleV2, @fieldParentPtr("linker", c)); + var unique_key_to_path = bun.StringHashMap([]const u8).init(c.allocator()); + defer unique_key_to_path.deinit(); + for (chunks) |*ch| { + if (ch.unique_key.len > 0 and ch.final_rel_path.len > 0) { + // Use the per-chunk public_path to match what IntermediateOutput.code() + // uses during emission (browser chunks from server builds use the + // browser transpiler's public_path). + const public_path = if (ch.flags.is_browser_chunk_from_server_build) + b.transpilerForTarget(.browser).options.public_path + else + c.options.public_path; + const normalizer = bun.bundle_v2.cheapPrefixNormalizer(public_path, ch.final_rel_path); + const resolved = std.fmt.allocPrint(c.allocator(), "{s}{s}", .{ normalizer[0], normalizer[1] }) catch |err| bun.handleOom(err); + unique_key_to_path.put(ch.unique_key, resolved) catch |err| bun.handleOom(err); + } + } + + // Fix up each chunk's module_info + for (chunks) |*chunk| { + if (chunk.content != .javascript) continue; + const mi = chunk.content.javascript.module_info orelse continue; + + // Collect replacements first (can't modify string table while iterating) + const Replacement = struct { old_id: analyze_transpiled_module.StringID, resolved_path: []const u8 }; + var replacements: std.ArrayListUnmanaged(Replacement) = .{}; + defer replacements.deinit(c.allocator()); + + var offset: usize = 0; + for (mi.strings_lens.items, 0..) |slen, string_index| { + const len: usize = @intCast(slen); + const s = mi.strings_buf.items[offset..][0..len]; + if (unique_key_to_path.get(s)) |resolved_path| { + replacements.append(c.allocator(), .{ + .old_id = @enumFromInt(@as(u32, @intCast(string_index))), + .resolved_path = resolved_path, + }) catch |err| bun.handleOom(err); + } + offset += len; + } + + for (replacements.items) |rep| { + const new_id = mi.str(rep.resolved_path) catch |err| bun.handleOom(err); + mi.replaceStringID(rep.old_id, new_id); + } + + // Serialize the fixed-up module_info + chunk.content.javascript.module_info_bytes = bun.js_printer.serializeModuleInfo(mi); + + // Free the ModuleInfo now that it's been serialized to bytes. + // It was allocated with bun.default_allocator (not the arena), + // so it must be explicitly destroyed. + mi.destroy(); + chunk.content.javascript.module_info = null; + } + } + // Generate metafile JSON fragments for each chunk (after paths are resolved) if (c.options.metafile) { for (chunks) |*chunk| { @@ -431,6 +494,14 @@ pub fn generateChunksInParallel( .none => {}, } + // Compute side early so it can be used for bytecode, module_info, and main chunk output files + const side: bun.bake.Side = if (chunk.content == .css or chunk.flags.is_browser_chunk_from_server_build) + .client + else switch (c.graph.ast.items(.target)[chunk.entry_point.source_index]) { + .browser => .client, + else => .server, + }; + const bytecode_output_file: ?options.OutputFile = brk: { if (c.options.generate_bytecode_cache) { const loader: Loader = if (chunk.entry_point.is_entry_point) @@ -444,7 +515,18 @@ pub fn generateChunksInParallel( jsc.VirtualMachine.is_bundler_thread_for_bytecode_cache = true; jsc.initialize(false); var fdpath: bun.PathBuffer = undefined; - var source_provider_url = try bun.String.createFormat("{s}" ++ bun.bytecode_extension, .{chunk.final_rel_path}); + // For --compile builds, the bytecode URL must match the module name + // that will be used at runtime. The module name is: + // public_path + final_rel_path (e.g., "/$bunfs/root/app.js") + // Without this prefix, the JSC bytecode cache key won't match at runtime. + // Use the per-chunk public_path (already computed above) for browser chunks + // from server builds, and normalize with cheapPrefixNormalizer for consistency + // with module_info path fixup. + // For non-compile builds, use the normal .jsc extension. + var source_provider_url = if (c.options.compile) url_blk: { + const normalizer = bun.bundle_v2.cheapPrefixNormalizer(public_path, chunk.final_rel_path); + break :url_blk try bun.String.createFormat("{s}{s}", .{ normalizer[0], normalizer[1] }); + } else try bun.String.createFormat("{s}" ++ bun.bytecode_extension, .{chunk.final_rel_path}); source_provider_url.ref(); defer source_provider_url.deref(); @@ -469,7 +551,7 @@ pub fn generateChunksInParallel( .data = .{ .buffer = .{ .data = bytecode, .allocator = cached_bytecode.allocator() }, }, - .side = .server, + .side = side, .entry_point_index = null, .is_executable = false, }); @@ -485,6 +567,40 @@ pub fn generateChunksInParallel( break :brk null; }; + // Create module_info output file for ESM bytecode in --compile builds + const module_info_output_file: ?options.OutputFile = brk: { + if (c.options.generate_bytecode_cache and c.options.output_format == .esm and c.options.compile) { + const loader: Loader = if (chunk.entry_point.is_entry_point) + c.parse_graph.input_files.items(.loader)[ + chunk.entry_point.source_index + ] + else + .js; + + if (chunk.content == .javascript and loader.isJavaScriptLike()) { + if (chunk.content.javascript.module_info_bytes) |module_info_bytes| { + break :brk options.OutputFile.init(.{ + .output_path = bun.handleOom(std.fmt.allocPrint(bun.default_allocator, "{s}.module-info", .{chunk.final_rel_path})), + .input_path = bun.handleOom(std.fmt.allocPrint(bun.default_allocator, "{s}.module-info", .{chunk.final_rel_path})), + .input_loader = .js, + .hash = if (chunk.template.placeholder.hash != null) bun.hash(module_info_bytes) else null, + .output_kind = .module_info, + .loader = .file, + .size = @as(u32, @truncate(module_info_bytes.len)), + .display_size = @as(u32, @truncate(module_info_bytes.len)), + .data = .{ + .buffer = .{ .data = module_info_bytes, .allocator = bun.default_allocator }, + }, + .side = side, + .entry_point_index = null, + .is_executable = false, + }); + } + } + } + break :brk null; + }; + const source_map_index: ?u32 = if (sourcemap_output_file != null) try output_files.insertForSourcemapOrBytecode(sourcemap_output_file.?) else @@ -495,6 +611,11 @@ pub fn generateChunksInParallel( else null; + const module_info_index: ?u32 = if (module_info_output_file != null) + try output_files.insertForSourcemapOrBytecode(module_info_output_file.?) + else + null; + const output_kind = if (chunk.content == .css) .asset else if (chunk.entry_point.is_entry_point) @@ -502,12 +623,6 @@ pub fn generateChunksInParallel( else .chunk; - const side: bun.bake.Side = if (chunk.content == .css or chunk.flags.is_browser_chunk_from_server_build) - .client - else switch (c.graph.ast.items(.target)[chunk.entry_point.source_index]) { - .browser => .client, - else => .server, - }; const chunk_index = output_files.insertForChunk(options.OutputFile.init(.{ .data = .{ .buffer = .{ @@ -525,6 +640,7 @@ pub fn generateChunksInParallel( .is_executable = chunk.flags.is_executable, .source_map_index = source_map_index, .bytecode_index = bytecode_index, + .module_info_index = module_info_index, .side = side, .entry_point_index = if (output_kind == .@"entry-point") chunk.entry_point.source_index - @as(u32, (if (c.framework) |fw| if (fw.server_components != null) 3 else 1 else 1)) @@ -564,6 +680,7 @@ pub const ThreadPool = bun.bundle_v2.ThreadPool; const debugPartRanges = Output.scoped(.PartRanges, .hidden); +const analyze_transpiled_module = @import("../../analyze_transpiled_module.zig"); const std = @import("std"); const bun = @import("bun"); diff --git a/src/bundler/linker_context/generateCodeForFileInChunkJS.zig b/src/bundler/linker_context/generateCodeForFileInChunkJS.zig index 7e780fee74..d9fee73ef8 100644 --- a/src/bundler/linker_context/generateCodeForFileInChunkJS.zig +++ b/src/bundler/linker_context/generateCodeForFileInChunkJS.zig @@ -10,6 +10,7 @@ pub fn generateCodeForFileInChunkJS( stmts: *StmtList, allocator: std.mem.Allocator, temp_allocator: std.mem.Allocator, + decl_collector: ?*DeclCollector, ) js_printer.PrintResult { const parts: []Part = c.graph.ast.items(.parts)[part_range.source_index.get()].slice()[part_range.part_index_begin..part_range.part_index_end]; const all_flags: []const JSMeta.Flags = c.graph.meta.items(.flags); @@ -613,6 +614,15 @@ pub fn generateCodeForFileInChunkJS( }; } + // Collect top-level declarations from the converted statements. + // This is done here (after convertStmtsForChunk) rather than in + // postProcessJSChunk, because convertStmtsForChunk transforms the AST + // (e.g. export default expr → var, export stripping) and the converted + // statements reflect what actually gets printed. + if (decl_collector) |dc| { + dc.collectFromStmts(out_stmts, r, c); + } + return c.printCodeForFileInChunkJS( r, allocator, @@ -628,6 +638,77 @@ pub fn generateCodeForFileInChunkJS( ); } +pub const DeclCollector = struct { + decls: std.ArrayListUnmanaged(CompileResult.DeclInfo) = .{}, + allocator: std.mem.Allocator, + + const CompileResult = bun.bundle_v2.CompileResult; + + /// Collect top-level declarations from **converted** statements (after + /// `convertStmtsForChunk`). At that point, export statements have already + /// been transformed: + /// - `s_export_default` → `s_local` / `s_function` / `s_class` + /// - `s_export_clause` → removed entirely + /// - `s_export_from` / `s_export_star` → removed or converted to `s_import` + /// + /// Remaining `s_import` statements (external, non-bundled) don't need + /// handling here; their bindings are recorded separately in + /// `postProcessJSChunk` by scanning the original AST import records. + pub fn collectFromStmts(self: *DeclCollector, stmts: []const Stmt, r: renamer.Renamer, c: *LinkerContext) void { + for (stmts) |stmt| { + switch (stmt.data) { + .s_local => |s| { + const kind: CompileResult.DeclInfo.Kind = if (s.kind == .k_var) .declared else .lexical; + for (s.decls.slice()) |decl| { + self.collectFromBinding(decl.binding, kind, r, c); + } + }, + .s_function => |s| { + if (s.func.name) |name_loc_ref| { + if (name_loc_ref.ref) |name_ref| { + self.addRef(name_ref, .lexical, r, c); + } + } + }, + .s_class => |s| { + if (s.class.class_name) |class_name| { + if (class_name.ref) |name_ref| { + self.addRef(name_ref, .lexical, r, c); + } + } + }, + else => {}, + } + } + } + + fn collectFromBinding(self: *DeclCollector, binding: Binding, kind: CompileResult.DeclInfo.Kind, r: renamer.Renamer, c: *LinkerContext) void { + switch (binding.data) { + .b_identifier => |b| { + self.addRef(b.ref, kind, r, c); + }, + .b_array => |b| { + for (b.items) |item| { + self.collectFromBinding(item.binding, kind, r, c); + } + }, + .b_object => |b| { + for (b.properties) |prop| { + self.collectFromBinding(prop.value, kind, r, c); + } + }, + .b_missing => {}, + } + } + + fn addRef(self: *DeclCollector, ref: Ref, kind: CompileResult.DeclInfo.Kind, r: renamer.Renamer, c: *LinkerContext) void { + const followed = c.graph.symbols.follow(ref); + const name = r.nameForSymbol(followed); + if (name.len == 0) return; + self.decls.append(self.allocator, .{ .name = name, .kind = kind }) catch return; + } +}; + fn mergeAdjacentLocalStmts(stmts: *std.ArrayListUnmanaged(Stmt), allocator: std.mem.Allocator) void { if (stmts.items.len == 0) return; diff --git a/src/bundler/linker_context/generateCompileResultForJSChunk.zig b/src/bundler/linker_context/generateCompileResultForJSChunk.zig index fac17cb33f..58b16bf2d6 100644 --- a/src/bundler/linker_context/generateCompileResultForJSChunk.zig +++ b/src/bundler/linker_context/generateCompileResultForJSChunk.zig @@ -46,6 +46,9 @@ fn generateCompileResultForJSChunkImpl(worker: *ThreadPool.Worker, c: *LinkerCon const toESMRef = c.graph.symbols.follow(runtime_members.get("__toESM").?.ref); const runtimeRequireRef = if (c.options.output_format == .cjs) null else c.graph.symbols.follow(runtime_members.get("__require").?.ref); + const collect_decls = c.options.generate_bytecode_cache and c.options.output_format == .esm and c.options.compile; + var dc = DeclCollector{ .allocator = allocator }; + const result = c.generateCodeForFileInChunkJS( &buffer_writer, chunk.renamer, @@ -57,6 +60,7 @@ fn generateCompileResultForJSChunkImpl(worker: *ThreadPool.Worker, c: *LinkerCon &worker.stmt_list, worker.allocator, arena.allocator(), + if (collect_decls) &dc else null, ); // Update bytesInOutput for this source in the chunk (for metafile) @@ -75,6 +79,7 @@ fn generateCompileResultForJSChunkImpl(worker: *ThreadPool.Worker, c: *LinkerCon .javascript = .{ .source_index = part_range.source_index.get(), .result = result, + .decls = if (collect_decls) dc.decls.items else &.{}, }, }; } @@ -82,6 +87,8 @@ fn generateCompileResultForJSChunkImpl(worker: *ThreadPool.Worker, c: *LinkerCon pub const DeferredBatchTask = bun.bundle_v2.DeferredBatchTask; pub const ParseTask = bun.bundle_v2.ParseTask; +const DeclCollector = @import("./generateCodeForFileInChunkJS.zig").DeclCollector; + const bun = @import("bun"); const Environment = bun.Environment; const ThreadPoolLib = bun.ThreadPool; diff --git a/src/bundler/linker_context/postProcessJSChunk.zig b/src/bundler/linker_context/postProcessJSChunk.zig index bfe0035a44..afa1f0fc84 100644 --- a/src/bundler/linker_context/postProcessJSChunk.zig +++ b/src/bundler/linker_context/postProcessJSChunk.zig @@ -25,6 +25,15 @@ pub fn postProcessJSChunk(ctx: GenerateChunkCtx, worker: *ThreadPool.Worker, chu const toESMRef = c.graph.symbols.follow(runtime_members.get("__toESM").?.ref); const runtimeRequireRef = if (c.options.output_format == .cjs) null else c.graph.symbols.follow(runtime_members.get("__require").?.ref); + // Create ModuleInfo for ESM bytecode in --compile builds + const generate_module_info = c.options.generate_bytecode_cache and c.options.output_format == .esm and c.options.compile; + const loader = c.parse_graph.input_files.items(.loader)[chunk.entry_point.source_index]; + const is_typescript = loader.isTypeScript(); + const module_info: ?*analyze_transpiled_module.ModuleInfo = if (generate_module_info) + analyze_transpiled_module.ModuleInfo.create(bun.default_allocator, is_typescript) catch null + else + null; + { const print_options = js_printer.Options{ .bundling = true, @@ -39,6 +48,7 @@ pub fn postProcessJSChunk(ctx: GenerateChunkCtx, worker: *ThreadPool.Worker, chu .target = c.options.target, .print_dce_annotations = c.options.emit_dce_annotations, .mangled_props = &c.mangled_props, + .module_info = module_info, // .const_values = c.graph.const_values, }; @@ -84,7 +94,124 @@ pub fn postProcessJSChunk(ctx: GenerateChunkCtx, worker: *ThreadPool.Worker, chu ); } - // Generate the exports for the entry point, if there are any + // Populate ModuleInfo with declarations collected during parallel printing, + // external import records from the original AST, and wrapper refs. + if (module_info) |mi| { + // 1. Add declarations collected by DeclCollector during parallel part printing. + // These come from the CONVERTED statements (after convertStmtsForChunk transforms + // export default → var, strips exports, etc.), so they match what's actually printed. + for (chunk.compile_results_for_chunk) |cr| { + const decls = switch (cr) { + .javascript => |js| js.decls, + else => continue, + }; + for (decls) |decl| { + const var_kind: analyze_transpiled_module.ModuleInfo.VarKind = switch (decl.kind) { + .declared => .declared, + .lexical => .lexical, + }; + const string_id = mi.str(decl.name) catch continue; + mi.addVar(string_id, var_kind) catch continue; + } + } + + // 1b. Check if any source in this chunk uses import.meta. The per-part + // parallel printer does not have module_info, so the printer cannot set + // this flag during per-part printing. We derive it from the AST instead. + // Note: the runtime source (index 0) also uses import.meta (e.g. + // `import.meta.require`), so we must not skip it. + { + const all_ast_flags = c.graph.ast.items(.flags); + for (chunk.content.javascript.parts_in_chunk_in_order) |part_range| { + if (all_ast_flags[part_range.source_index.get()].has_import_meta) { + mi.flags.contains_import_meta = true; + break; + } + } + } + + // 2. Collect truly-external imports from the original AST. Bundled imports + // (where source_index is valid) are removed by convertStmtsForChunk and + // re-created as cross-chunk imports — those are already captured by the + // printer when it prints cross_chunk_prefix_stmts above. Only truly-external + // imports (node built-ins, etc.) survive as s_import in per-file parts and + // need recording here. + const all_parts = c.graph.ast.items(.parts); + const all_flags = c.graph.meta.items(.flags); + const all_import_records = c.graph.ast.items(.import_records); + for (chunk.content.javascript.parts_in_chunk_in_order) |part_range| { + if (all_flags[part_range.source_index.get()].wrap == .cjs) continue; + const source_parts = all_parts[part_range.source_index.get()].slice(); + const source_import_records = all_import_records[part_range.source_index.get()].slice(); + var part_i = part_range.part_index_begin; + while (part_i < part_range.part_index_end) : (part_i += 1) { + for (source_parts[part_i].stmts) |stmt| { + switch (stmt.data) { + .s_import => |s| { + const record = &source_import_records[s.import_record_index]; + if (record.path.is_disabled) continue; + if (record.tag == .bun) continue; + // Skip bundled imports — these are converted to cross-chunk + // imports by the linker. The printer already recorded them + // when printing cross_chunk_prefix_stmts. + if (record.source_index.isValid()) continue; + + const import_path = record.path.text; + const irp_id = mi.str(import_path) catch continue; + mi.requestModule(irp_id, .none) catch continue; + + if (s.default_name) |name| { + if (name.ref) |name_ref| { + const local_name = chunk.renamer.nameForSymbol(name_ref); + const local_name_id = mi.str(local_name) catch continue; + mi.addVar(local_name_id, .lexical) catch continue; + mi.addImportInfoSingle(irp_id, mi.str("default") catch continue, local_name_id, false) catch continue; + } + } + + for (s.items) |item| { + if (item.name.ref) |name_ref| { + const local_name = chunk.renamer.nameForSymbol(name_ref); + const local_name_id = mi.str(local_name) catch continue; + mi.addVar(local_name_id, .lexical) catch continue; + mi.addImportInfoSingle(irp_id, mi.str(item.alias) catch continue, local_name_id, false) catch continue; + } + } + + if (record.flags.contains_import_star) { + const local_name = chunk.renamer.nameForSymbol(s.namespace_ref); + const local_name_id = mi.str(local_name) catch continue; + mi.addVar(local_name_id, .lexical) catch continue; + mi.addImportInfoNamespace(irp_id, local_name_id) catch continue; + } + }, + else => {}, + } + } + } + } + + // 3. Add wrapper-generated declarations (init_xxx, require_xxx) that are + // not in any part statement. + const all_wrapper_refs = c.graph.ast.items(.wrapper_ref); + for (chunk.content.javascript.parts_in_chunk_in_order) |part_range| { + const source_index = part_range.source_index.get(); + if (all_flags[source_index].wrap != .none) { + const wrapper_ref = all_wrapper_refs[source_index]; + if (!wrapper_ref.isEmpty()) { + const name = chunk.renamer.nameForSymbol(wrapper_ref); + if (name.len > 0) { + const string_id = mi.str(name) catch continue; + mi.addVar(string_id, .declared) catch continue; + } + } + } + } + } + + // Generate the exports for the entry point, if there are any. + // This must happen before module_info serialization so the printer + // can populate export entries in module_info. const entry_point_tail = brk: { if (chunk.isEntryPoint()) { break :brk generateEntryPointTailJS( @@ -95,12 +222,21 @@ pub fn postProcessJSChunk(ctx: GenerateChunkCtx, worker: *ThreadPool.Worker, chu worker.allocator, arena.allocator(), chunk.renamer, + module_info, ); } break :brk CompileResult.empty; }; + // Store unserialized ModuleInfo on the chunk. Serialization is deferred to + // generateChunksInParallel after final chunk paths are computed, so that + // cross-chunk import specifiers (which use unique_key placeholders during + // printing) can be resolved to actual paths. + if (module_info) |mi| { + chunk.content.javascript.module_info = mi; + } + var j = StringJoiner{ .allocator = worker.allocator, .watcher = .{ @@ -435,6 +571,37 @@ pub fn postProcessJSChunk(ctx: GenerateChunkCtx, worker: *ThreadPool.Worker, chu } } +/// Recursively walk a binding and add all declared names to `ModuleInfo`. +/// Handles `b_identifier`, `b_array`, `b_object`, and `b_missing`. +fn addBindingVarsToModuleInfo( + mi: *analyze_transpiled_module.ModuleInfo, + binding: Binding, + var_kind: analyze_transpiled_module.ModuleInfo.VarKind, + r: renamer.Renamer, + symbols: *const js_ast.Symbol.Map, +) void { + switch (binding.data) { + .b_identifier => |b| { + const name = r.nameForSymbol(symbols.follow(b.ref)); + if (name.len > 0) { + const str_id = mi.str(name) catch return; + mi.addVar(str_id, var_kind) catch {}; + } + }, + .b_array => |b| { + for (b.items) |item| { + addBindingVarsToModuleInfo(mi, item.binding, var_kind, r, symbols); + } + }, + .b_object => |b| { + for (b.properties) |prop| { + addBindingVarsToModuleInfo(mi, prop.value, var_kind, r, symbols); + } + }, + .b_missing => {}, + } +} + pub fn generateEntryPointTailJS( c: *LinkerContext, toCommonJSRef: Ref, @@ -443,6 +610,7 @@ pub fn generateEntryPointTailJS( allocator: std.mem.Allocator, temp_allocator: std.mem.Allocator, r: renamer.Renamer, + module_info: ?*analyze_transpiled_module.ModuleInfo, ) CompileResult { const flags: JSMeta.Flags = c.graph.meta.items(.flags)[source_index]; var stmts = std.array_list.Managed(Stmt).init(temp_allocator); @@ -825,6 +993,22 @@ pub fn generateEntryPointTailJS( }, } + // Add generated local declarations from entry point tail to module_info. + // This captures vars like `var export_foo = cjs.foo` for CJS export copies. + if (module_info) |mi| { + for (stmts.items) |stmt| { + switch (stmt.data) { + .s_local => |s| { + const var_kind: analyze_transpiled_module.ModuleInfo.VarKind = if (s.kind == .k_var) .declared else .lexical; + for (s.decls.slice()) |decl| { + addBindingVarsToModuleInfo(mi, decl.binding, var_kind, r, &c.graph.symbols); + } + }, + else => {}, + } + } + } + if (stmts.items.len == 0) { return .{ .javascript = .{ @@ -850,6 +1034,7 @@ pub fn generateEntryPointTailJS( .print_dce_annotations = c.options.emit_dce_annotations, .minify_syntax = c.options.minify_syntax, .mangled_props = &c.mangled_props, + .module_info = module_info, // .const_values = c.graph.const_values, }; @@ -875,6 +1060,7 @@ pub fn generateEntryPointTailJS( }; } +const analyze_transpiled_module = @import("../../analyze_transpiled_module.zig"); const std = @import("std"); const bun = @import("bun"); diff --git a/src/cli/Arguments.zig b/src/cli/Arguments.zig index 27cea7b5b5..ee24944abc 100644 --- a/src/cli/Arguments.zig +++ b/src/cli/Arguments.zig @@ -974,7 +974,6 @@ pub fn parse(allocator: std.mem.Allocator, ctx: Command.Context, comptime cmd: C args.flag("--debug-no-minify"); } - // TODO: support --format=esm if (ctx.bundler_options.bytecode) { ctx.bundler_options.output_format = .cjs; ctx.args.target = .bun; @@ -1187,6 +1186,10 @@ pub fn parse(allocator: std.mem.Allocator, ctx: Command.Context, comptime cmd: C Output.errGeneric("Using --windows-hide-console is only available when compiling on Windows", .{}); Global.crash(); } + if (ctx.bundler_options.compile_target.os != .windows) { + Output.errGeneric("--windows-hide-console requires a Windows compile target", .{}); + Global.crash(); + } if (!ctx.bundler_options.compile) { Output.errGeneric("--windows-hide-console requires --compile", .{}); Global.crash(); @@ -1198,6 +1201,10 @@ pub fn parse(allocator: std.mem.Allocator, ctx: Command.Context, comptime cmd: C Output.errGeneric("Using --windows-icon is only available when compiling on Windows", .{}); Global.crash(); } + if (ctx.bundler_options.compile_target.os != .windows) { + Output.errGeneric("--windows-icon requires a Windows compile target", .{}); + Global.crash(); + } if (!ctx.bundler_options.compile) { Output.errGeneric("--windows-icon requires --compile", .{}); Global.crash(); @@ -1209,6 +1216,10 @@ pub fn parse(allocator: std.mem.Allocator, ctx: Command.Context, comptime cmd: C Output.errGeneric("Using --windows-title is only available when compiling on Windows", .{}); Global.crash(); } + if (ctx.bundler_options.compile_target.os != .windows) { + Output.errGeneric("--windows-title requires a Windows compile target", .{}); + Global.crash(); + } if (!ctx.bundler_options.compile) { Output.errGeneric("--windows-title requires --compile", .{}); Global.crash(); @@ -1220,6 +1231,10 @@ pub fn parse(allocator: std.mem.Allocator, ctx: Command.Context, comptime cmd: C Output.errGeneric("Using --windows-publisher is only available when compiling on Windows", .{}); Global.crash(); } + if (ctx.bundler_options.compile_target.os != .windows) { + Output.errGeneric("--windows-publisher requires a Windows compile target", .{}); + Global.crash(); + } if (!ctx.bundler_options.compile) { Output.errGeneric("--windows-publisher requires --compile", .{}); Global.crash(); @@ -1231,6 +1246,10 @@ pub fn parse(allocator: std.mem.Allocator, ctx: Command.Context, comptime cmd: C Output.errGeneric("Using --windows-version is only available when compiling on Windows", .{}); Global.crash(); } + if (ctx.bundler_options.compile_target.os != .windows) { + Output.errGeneric("--windows-version requires a Windows compile target", .{}); + Global.crash(); + } if (!ctx.bundler_options.compile) { Output.errGeneric("--windows-version requires --compile", .{}); Global.crash(); @@ -1242,6 +1261,10 @@ pub fn parse(allocator: std.mem.Allocator, ctx: Command.Context, comptime cmd: C Output.errGeneric("Using --windows-description is only available when compiling on Windows", .{}); Global.crash(); } + if (ctx.bundler_options.compile_target.os != .windows) { + Output.errGeneric("--windows-description requires a Windows compile target", .{}); + Global.crash(); + } if (!ctx.bundler_options.compile) { Output.errGeneric("--windows-description requires --compile", .{}); Global.crash(); @@ -1253,6 +1276,10 @@ pub fn parse(allocator: std.mem.Allocator, ctx: Command.Context, comptime cmd: C Output.errGeneric("Using --windows-copyright is only available when compiling on Windows", .{}); Global.crash(); } + if (ctx.bundler_options.compile_target.os != .windows) { + Output.errGeneric("--windows-copyright requires a Windows compile target", .{}); + Global.crash(); + } if (!ctx.bundler_options.compile) { Output.errGeneric("--windows-copyright requires --compile", .{}); Global.crash(); @@ -1312,8 +1339,9 @@ pub fn parse(allocator: std.mem.Allocator, ctx: Command.Context, comptime cmd: C } ctx.bundler_options.output_format = format; - if (format != .cjs and ctx.bundler_options.bytecode) { - Output.errGeneric("format must be 'cjs' when bytecode is true. Eventually we'll add esm support as well.", .{}); + // ESM bytecode is supported for --compile builds (module_info is embedded in binary) + if (format != .cjs and format != .esm and ctx.bundler_options.bytecode) { + Output.errGeneric("format must be 'cjs' or 'esm' when bytecode is true.", .{}); Global.exit(1); } } diff --git a/src/cli/build_command.zig b/src/cli/build_command.zig index a3ad272784..ac426f0796 100644 --- a/src/cli/build_command.zig +++ b/src/cli/build_command.zig @@ -583,6 +583,7 @@ pub const BuildCommand = struct { .asset => Output.prettyFmt("", true), .sourcemap => Output.prettyFmt("", true), .bytecode => Output.prettyFmt("", true), + .module_info => Output.prettyFmt("", true), .@"metafile-json", .@"metafile-markdown" => Output.prettyFmt("", true), }); @@ -614,6 +615,7 @@ pub const BuildCommand = struct { .asset => "asset", .sourcemap => "source map", .bytecode => "bytecode", + .module_info => "module info", .@"metafile-json" => "metafile json", .@"metafile-markdown" => "metafile markdown", }}); diff --git a/src/js_printer.zig b/src/js_printer.zig index 455cb01d1c..77f839e2f3 100644 --- a/src/js_printer.zig +++ b/src/js_printer.zig @@ -395,6 +395,7 @@ pub const Options = struct { target: options.Target = .browser, runtime_transpiler_cache: ?*bun.jsc.RuntimeTranspilerCache = null, + module_info: ?*analyze_transpiled_module.ModuleInfo = null, input_files_for_dev_server: ?[]logger.Source = null, commonjs_named_exports: js_ast.Ast.CommonJSNamedExports = .{}, @@ -632,9 +633,44 @@ fn NewPrinter( binary_expression_stack: std.array_list.Managed(BinaryExpressionVisitor) = undefined, was_lazy_export: bool = false, + module_info: if (!may_have_module_info) void else ?*analyze_transpiled_module.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: ?analyze_transpiled_module.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 .{}; + } + pub inline fn isTopLevel(_: @This()) bool { + return false; + } + } 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); + } + pub inline fn isTopLevel(self: @This()) bool { + return self.is_top_level != .no; + } + }; + const IsTopLevel = enum { yes, var_only, no }; + inline fn moduleInfo(self: *const Printer) ?*analyze_transpiled_module.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{}; @@ -1031,6 +1067,25 @@ fn NewPrinter( p.printSemicolonAfterStatement(); } + + // Record var declarations for module_info. printGlobalBunImportStatement + // bypasses printDeclStmt/printBinding, so we must record vars explicitly. + if (may_have_module_info) { + if (p.moduleInfo()) |mi| { + if (import.star_name_loc != null) { + const name = p.renamer.nameForSymbol(import.namespace_ref); + bun.handleOom(mi.addVar(bun.handleOom(mi.str(name)), .declared)); + } + if (import.default_name) |default| { + const name = p.renamer.nameForSymbol(default.ref.?); + bun.handleOom(mi.addVar(bun.handleOom(mi.str(name)), .declared)); + } + for (import.items) |item| { + const name = p.renamer.nameForSymbol(item.name.ref.?); + bun.handleOom(mi.addVar(bun.handleOom(mi.str(name)), .declared)); + } + } + } } pub inline fn printSpaceBeforeIdentifier( @@ -1073,30 +1128,30 @@ fn NewPrinter( } } - pub fn printBody(p: *Printer, stmt: Stmt) void { + pub fn printBody(p: *Printer, stmt: Stmt, tlmtlo: TopLevel) void { 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, tlmtlo); p.printNewline(); }, else => { p.printNewline(); p.indent(); - p.printStmt(stmt) catch unreachable; + p.printStmt(stmt, tlmtlo) catch unreachable; 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 unreachable; } } - 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("{"); if (stmts.len > 0) { @@ -1104,7 +1159,7 @@ fn NewPrinter( p.printNewline(); p.indent(); - p.printBlockBody(stmts); + p.printBlockBody(stmts, tlmtlo); p.unindent(); p.printIndent(); @@ -1123,8 +1178,8 @@ fn NewPrinter( p.printNewline(); p.indent(); - p.printBlockBody(prepend); - p.printBlockBody(stmts); + p.printBlockBody(prepend, TopLevel.init(.no)); + p.printBlockBody(stmts, TopLevel.init(.no)); p.unindent(); p.needs_semicolon = false; @@ -1132,7 +1187,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_; @@ -1240,7 +1295,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(" = ")); @@ -1256,7 +1311,7 @@ fn NewPrinter( } { - p.printBinding(decls[0].binding); + p.printBinding(decls[0].binding, tlm); if (decls[0].value) |value| { p.printWhitespacer(ws(" = ")); @@ -1268,7 +1323,7 @@ fn NewPrinter( p.print(","); p.printSpace(); - p.printBinding(decl.binding); + p.printBinding(decl.binding, tlm); if (decl.value) |value| { p.printWhitespacer(ws(" = ")); @@ -1342,7 +1397,7 @@ fn NewPrinter( p.print("..."); } - p.printBinding(arg.binding); + p.printBinding(arg.binding, .{}); if (arg.default) |default| { p.printWhitespacer(ws(" = ")); @@ -1358,7 +1413,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, TopLevel.init(.no)); } pub fn printClass(p: *Printer, class: G.Class) void { @@ -1382,7 +1437,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, TopLevel.init(.no)); p.printNewline(); continue; } @@ -2009,6 +2064,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.flags.contains_import_meta = true; p.print("import.meta"); } else { // Note: The bundler will not hit this code path. The bundler will replace @@ -2034,6 +2090,7 @@ fn NewPrinter( p.printSpaceBeforeIdentifier(); p.addSourceMapping(expr.loc); } + if (p.moduleInfo()) |mi| mi.flags.contains_import_meta = true; p.print("import.meta.main"); } else { bun.debugAssert(p.options.module_type != .internal_bake_dev); @@ -2539,7 +2596,7 @@ fn NewPrinter( } if (!wasPrinted) { - p.printBlock(e.body.loc, e.body.stmts, null); + p.printBlock(e.body.loc, e.body.stmts, null, TopLevel.init(.no)); } if (wrap) { @@ -3525,13 +3582,21 @@ 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); + if (may_have_module_info) { + if (p.moduleInfo()) |mi| { + const local_name = p.renamer.nameForSymbol(b.ref); + const name_id = bun.handleOom(mi.str(local_name)); + if (tlm.is_top_level) |vk| bun.handleOom(mi.addVar(name_id, vk)); + if (tlm.is_export) bun.handleOom(mi.addExportInfoLocal(name_id, name_id)); + } + } }, .b_array => |b| { p.print("["); @@ -3558,7 +3623,7 @@ fn NewPrinter( p.print("..."); } - p.printBinding(item.binding); + p.printBinding(item.binding, tlm); p.maybePrintDefaultBindingValue(item); @@ -3605,7 +3670,7 @@ fn NewPrinter( p.print("]:"); p.printSpace(); - p.printBinding(property.value); + p.printBinding(property.value, tlm); p.maybePrintDefaultBindingValue(property); continue; } @@ -3630,6 +3695,13 @@ fn NewPrinter( switch (property.value.data) { .b_identifier => |id| { if (str.eql(string, p.renamer.nameForSymbol(id.ref))) { + if (may_have_module_info) { + if (p.moduleInfo()) |mi| { + const name_id = bun.handleOom(mi.str(str.data)); + if (tlm.is_top_level) |vk| bun.handleOom(mi.addVar(name_id, vk)); + if (tlm.is_export) bun.handleOom(mi.addExportInfoLocal(name_id, name_id)); + } + } p.maybePrintDefaultBindingValue(property); continue; } @@ -3647,6 +3719,14 @@ fn NewPrinter( switch (property.value.data) { .b_identifier => |id| { if (strings.utf16EqlString(str.slice16(), p.renamer.nameForSymbol(id.ref))) { + if (may_have_module_info) { + if (p.moduleInfo()) |mi| { + const str8 = str.slice(p.options.allocator); + const name_id = bun.handleOom(mi.str(str8)); + if (tlm.is_top_level) |vk| bun.handleOom(mi.addVar(name_id, vk)); + if (tlm.is_export) bun.handleOom(mi.addExportInfoLocal(name_id, name_id)); + } + } p.maybePrintDefaultBindingValue(property); continue; } @@ -3666,7 +3746,7 @@ fn NewPrinter( p.printSpace(); } - p.printBinding(property.value); + p.printBinding(property.value, tlm); p.maybePrintDefaultBindingValue(property); } @@ -3692,7 +3772,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 { @@ -3729,23 +3809,25 @@ fn NewPrinter( } p.addSourceMapping(name.loc); - p.printSymbol(nameRef); + const local_name = p.renamer.nameForSymbol(nameRef); + p.printIdentifier(local_name); p.printFunc(s.func); - // if (rewrite_esm_to_cjs and s.func.flags.contains(.is_export)) { - // p.printSemicolonAfterStatement(); - // p.print("var "); - // p.printSymbol(nameRef); - // p.@"print = "(); - // p.printSymbol(nameRef); - // p.printSemicolonAfterStatement(); - // } else { + if (may_have_module_info) { + if (p.moduleInfo()) |mi| { + const name_id = bun.handleOom(mi.str(local_name)); + // function declarations are lexical (block-scoped in modules); + // only record at true top-level, not inside blocks. + if (tlmtlo.is_top_level == .yes) bun.handleOom(mi.addVar(name_id, .lexical)); + if (s.func.flags.contains(.is_export)) bun.handleOom(mi.addExportInfoLocal(name_id, name_id)); + } + } + p.printNewline(); - // } if (rewrite_esm_to_cjs and s.func.flags.contains(.is_export)) { p.printIndent(); - p.printBundledExport(p.renamer.nameForSymbol(nameRef), p.renamer.nameForSymbol(nameRef)); + p.printBundledExport(local_name, local_name); p.printSemicolonAfterStatement(); } }, @@ -3767,9 +3849,20 @@ fn NewPrinter( p.print("class "); p.addSourceMapping(s.class.class_name.?.loc); - p.printSymbol(nameRef); + const nameStr = p.renamer.nameForSymbol(nameRef); + p.printIdentifier(nameStr); p.printClass(s.class); + if (may_have_module_info) { + if (p.moduleInfo()) |mi| { + const name_id = bun.handleOom(mi.str(nameStr)); + // class declarations are lexical (block-scoped in modules); + // only record at true top-level, not inside blocks. + if (tlmtlo.is_top_level == .yes) bun.handleOom(mi.addVar(name_id, .lexical)); + if (s.is_export) bun.handleOom(mi.addExportInfoLocal(name_id, name_id)); + } + } + if (rewrite_esm_to_cjs and s.is_export) { p.printSemicolonAfterStatement(); } else { @@ -3805,6 +3898,13 @@ fn NewPrinter( p.export_default_start = p.writer.written; p.printExpr(expr, .comma, ExprFlag.None()); p.printSemicolonAfterStatement(); + + if (may_have_module_info) { + if (p.moduleInfo()) |mi| { + bun.handleOom(mi.addExportInfoLocal(bun.handleOom(mi.str("default")), .star_default)); + bun.handleOom(mi.addVar(.star_default, .lexical)); + } + } return; }, @@ -3825,26 +3925,44 @@ fn NewPrinter( p.maybePrintSpace(); } - if (func.func.name) |name| { - p.printSymbol(name.ref.?); + const func_name: ?[]const u8 = if (func.func.name) |name| p.renamer.nameForSymbol(name.ref.?) else null; + if (func_name) |fn_name| { + p.printIdentifier(fn_name); } p.printFunc(func.func); + if (may_have_module_info) { + if (p.moduleInfo()) |mi| { + const local_name: analyze_transpiled_module.StringID = if (func_name) |f| bun.handleOom(mi.str(f)) else .star_default; + bun.handleOom(mi.addExportInfoLocal(bun.handleOom(mi.str("default")), local_name)); + bun.handleOom(mi.addVar(local_name, .lexical)); + } + } + p.printNewline(); }, .s_class => |class| { p.printSpaceBeforeIdentifier(); + const class_name: ?[]const u8 = if (class.class.class_name) |name| p.renamer.nameForSymbol(name.ref orelse Output.panic("Internal error: Expected class to have a name ref", .{})) else null; if (class.class.class_name) |name| { p.print("class "); - p.printSymbol(name.ref orelse Output.panic("Internal error: Expected class to have a name ref", .{})); + p.printIdentifier(p.renamer.nameForSymbol(name.ref.?)); } else { p.print("class"); } p.printClass(class.class); + if (may_have_module_info) { + if (p.moduleInfo()) |mi| { + const local_name: analyze_transpiled_module.StringID = if (class_name) |f| bun.handleOom(mi.str(f)) else .star_default; + bun.handleOom(mi.addExportInfoLocal(bun.handleOom(mi.str("default")), local_name)); + bun.handleOom(mi.addVar(local_name, .lexical)); + } + } + p.printNewline(); }, else => { @@ -3875,8 +3993,21 @@ fn NewPrinter( p.printWhitespacer(ws("from ")); } + const irp = p.importRecord(s.import_record_index).path.text; p.printImportRecordPath(p.importRecord(s.import_record_index)); p.printSemicolonAfterStatement(); + + if (may_have_module_info) { + if (p.moduleInfo()) |mi| { + const irp_id = bun.handleOom(mi.str(irp)); + bun.handleOom(mi.requestModule(irp_id, .none)); + if (s.alias) |alias| { + bun.handleOom(mi.addExportInfoNamespace(bun.handleOom(mi.str(alias.original_name)), irp_id)); + } else { + bun.handleOom(mi.addExportInfoStar(irp_id)); + } + } + } }, .s_export_clause => |s| { if (rewrite_esm_to_cjs) { @@ -4026,7 +4157,14 @@ fn NewPrinter( p.printIndent(); } + const name = p.renamer.nameForSymbol(item.name.ref.?); p.printExportClauseItem(item); + + if (may_have_module_info) { + if (p.moduleInfo()) |mi| { + bun.handleOom(mi.addExportInfoLocal(bun.handleOom(mi.str(item.alias)), bun.handleOom(mi.str(name)))); + } + } } if (!s.is_single_line) { @@ -4079,8 +4217,20 @@ fn NewPrinter( } p.printWhitespacer(ws("} from ")); + const irp = import_record.path.text; p.printImportRecordPath(import_record); p.printSemicolonAfterStatement(); + + if (may_have_module_info) { + if (p.moduleInfo()) |mi| { + const irp_id = bun.handleOom(mi.str(irp)); + bun.handleOom(mi.requestModule(irp_id, .none)); + for (s.items) |item| { + const name = p.renamer.nameForSymbol(item.name.ref.?); + bun.handleOom(mi.addExportInfoIndirect(bun.handleOom(mi.str(item.alias)), bun.handleOom(mi.str(name)), irp_id)); + } + } + } }, .s_local => |s| { p.printIndent(); @@ -4088,41 +4238,42 @@ fn NewPrinter( p.addSourceMapping(stmt.loc); switch (s.kind) { .k_const => { - p.printDeclStmt(s.is_export, "const", s.decls.slice()); + p.printDeclStmt(s.is_export, "const", s.decls.slice(), tlmtlo); }, .k_let => { - p.printDeclStmt(s.is_export, "let", s.decls.slice()); + p.printDeclStmt(s.is_export, "let", s.decls.slice(), tlmtlo); }, .k_var => { - p.printDeclStmt(s.is_export, "var", s.decls.slice()); + p.printDeclStmt(s.is_export, "var", s.decls.slice(), tlmtlo); }, .k_using => { - p.printDeclStmt(s.is_export, "using", s.decls.slice()); + p.printDeclStmt(s.is_export, "using", s.decls.slice(), tlmtlo); }, .k_await_using => { - p.printDeclStmt(s.is_export, "await using", s.decls.slice()); + p.printDeclStmt(s.is_export, "await using", s.decls.slice(), tlmtlo); }, } }, .s_if => |s| { p.printIndent(); - p.printIf(s, stmt.loc); + p.printIf(s, stmt.loc, tlmtlo.subVar()); }, .s_do_while => |s| { p.printIndent(); p.printSpaceBeforeIdentifier(); p.addSourceMapping(stmt.loc); p.print("do"); + const sub_var = tlmtlo.subVar(); 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, sub_var); p.printSpace(); }, else => { p.printNewline(); p.indent(); - p.printStmt(s.body) catch unreachable; + p.printStmt(s.body, sub_var) catch unreachable; p.printSemicolonIfNeeded(); p.unindent(); p.printIndent(); @@ -4150,7 +4301,7 @@ fn NewPrinter( p.printSpace(); p.printExpr(s.value, .lowest, ExprFlag.None()); p.print(")"); - p.printBody(s.body); + p.printBody(s.body, tlmtlo.subVar()); }, .s_for_of => |s| { p.printIndent(); @@ -4170,7 +4321,7 @@ fn NewPrinter( p.printSpace(); p.printExpr(s.value, .comma, ExprFlag.None()); p.print(")"); - p.printBody(s.body); + p.printBody(s.body, tlmtlo.subVar()); }, .s_while => |s| { p.printIndent(); @@ -4181,7 +4332,7 @@ fn NewPrinter( p.print("("); p.printExpr(s.test_, .lowest, ExprFlag.None()); p.print(")"); - p.printBody(s.body); + p.printBody(s.body, tlmtlo.subVar()); }, .s_with => |s| { p.printIndent(); @@ -4192,7 +4343,7 @@ fn NewPrinter( p.print("("); p.printExpr(s.value, .lowest, ExprFlag.None()); p.print(")"); - p.printBody(s.body); + p.printBody(s.body, tlmtlo.subVar()); }, .s_label => |s| { if (!p.options.minify_whitespace and p.options.indent.count > 0) { @@ -4202,7 +4353,7 @@ fn NewPrinter( p.addSourceMapping(stmt.loc); p.printSymbol(s.name.ref orelse Output.panic("Internal error: expected label to have a name", .{})); p.print(":"); - p.printBody(s.stmt); + p.printBody(s.stmt, tlmtlo.subVar()); }, .s_try => |s| { p.printIndent(); @@ -4210,7 +4361,8 @@ fn NewPrinter( p.addSourceMapping(stmt.loc); p.print("try"); p.printSpace(); - p.printBlock(s.body_loc, s.body, null); + const sub_var_try = tlmtlo.subVar(); + p.printBlock(s.body_loc, s.body, null, sub_var_try); if (s.catch_) |catch_| { p.printSpace(); @@ -4219,18 +4371,18 @@ fn NewPrinter( if (catch_.binding) |binding| { p.printSpace(); p.print("("); - p.printBinding(binding); + p.printBinding(binding, .{}); p.print(")"); } p.printSpace(); - p.printBlock(catch_.body_loc, catch_.body, null); + p.printBlock(catch_.body_loc, catch_.body, null, sub_var_try); } 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, sub_var_try); } p.printNewline(); @@ -4261,7 +4413,7 @@ fn NewPrinter( } p.print(")"); - p.printBody(s.body); + p.printBody(s.body, tlmtlo.subVar()); }, .s_switch => |s| { p.printIndent(); @@ -4293,11 +4445,12 @@ fn NewPrinter( p.print(":"); + const sub_var_case = tlmtlo.subVar(); if (c.body.len == 1) { 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, sub_var_case); p.printNewline(); continue; }, @@ -4309,7 +4462,7 @@ fn NewPrinter( p.indent(); for (c.body) |st| { p.printSemicolonIfNeeded(); - p.printStmt(st) catch unreachable; + p.printStmt(st, sub_var_case) catch unreachable; } p.unindent(); } @@ -4494,16 +4647,68 @@ fn NewPrinter( .dataurl => p.printWhitespacer(ws(" with { type: \"dataurl\" }")), .text => p.printWhitespacer(ws(" with { type: \"text\" }")), .bunsh => p.printWhitespacer(ws(" with { type: \"sh\" }")), - // sqlite_embedded only relevant when bundling .sqlite, .sqlite_embedded => p.printWhitespacer(ws(" with { type: \"sqlite\" }")), .html => p.printWhitespacer(ws(" with { type: \"html\" }")), .md => p.printWhitespacer(ws(" with { type: \"md\" }")), }; p.printSemicolonAfterStatement(); + + if (may_have_module_info) { + if (p.moduleInfo()) |mi| { + const import_record_path = record.path.text; + const irp_id = bun.handleOom(mi.str(import_record_path)); + const fetch_parameters: analyze_transpiled_module.ModuleInfo.FetchParameters = if (comptime is_bun_platform) (if (record.loader) |loader| switch (loader) { + .json => .json, + .jsx => analyze_transpiled_module.ModuleInfo.FetchParameters.hostDefined(bun.handleOom(mi.str("jsx"))), + .js => analyze_transpiled_module.ModuleInfo.FetchParameters.hostDefined(bun.handleOom(mi.str("js"))), + .ts => analyze_transpiled_module.ModuleInfo.FetchParameters.hostDefined(bun.handleOom(mi.str("ts"))), + .tsx => analyze_transpiled_module.ModuleInfo.FetchParameters.hostDefined(bun.handleOom(mi.str("tsx"))), + .css => analyze_transpiled_module.ModuleInfo.FetchParameters.hostDefined(bun.handleOom(mi.str("css"))), + .file => analyze_transpiled_module.ModuleInfo.FetchParameters.hostDefined(bun.handleOom(mi.str("file"))), + .jsonc => analyze_transpiled_module.ModuleInfo.FetchParameters.hostDefined(bun.handleOom(mi.str("jsonc"))), + .toml => analyze_transpiled_module.ModuleInfo.FetchParameters.hostDefined(bun.handleOom(mi.str("toml"))), + .yaml => analyze_transpiled_module.ModuleInfo.FetchParameters.hostDefined(bun.handleOom(mi.str("yaml"))), + .wasm => analyze_transpiled_module.ModuleInfo.FetchParameters.hostDefined(bun.handleOom(mi.str("wasm"))), + .napi => analyze_transpiled_module.ModuleInfo.FetchParameters.hostDefined(bun.handleOom(mi.str("napi"))), + .base64 => analyze_transpiled_module.ModuleInfo.FetchParameters.hostDefined(bun.handleOom(mi.str("base64"))), + .dataurl => analyze_transpiled_module.ModuleInfo.FetchParameters.hostDefined(bun.handleOom(mi.str("dataurl"))), + .text => analyze_transpiled_module.ModuleInfo.FetchParameters.hostDefined(bun.handleOom(mi.str("text"))), + .bunsh => analyze_transpiled_module.ModuleInfo.FetchParameters.hostDefined(bun.handleOom(mi.str("sh"))), + .sqlite, .sqlite_embedded => analyze_transpiled_module.ModuleInfo.FetchParameters.hostDefined(bun.handleOom(mi.str("sqlite"))), + .html => analyze_transpiled_module.ModuleInfo.FetchParameters.hostDefined(bun.handleOom(mi.str("html"))), + .json5 => analyze_transpiled_module.ModuleInfo.FetchParameters.hostDefined(bun.handleOom(mi.str("json5"))), + .md => analyze_transpiled_module.ModuleInfo.FetchParameters.hostDefined(bun.handleOom(mi.str("md"))), + } else .none) else .none; + bun.handleOom(mi.requestModule(irp_id, fetch_parameters)); + + if (s.default_name) |name| { + const local_name = p.renamer.nameForSymbol(name.ref.?); + const local_name_id = bun.handleOom(mi.str(local_name)); + bun.handleOom(mi.addVar(local_name_id, .lexical)); + bun.handleOom(mi.addImportInfoSingle(irp_id, bun.handleOom(mi.str("default")), local_name_id, false)); + } + + for (s.items) |item| { + const local_name = p.renamer.nameForSymbol(item.name.ref.?); + const local_name_id = bun.handleOom(mi.str(local_name)); + bun.handleOom(mi.addVar(local_name_id, .lexical)); + // In bundled output, all surviving imports are value imports + // (tree-shaking already removed type-only ones). The finalize() + // step handles re-export type-script conversion separately. + bun.handleOom(mi.addImportInfoSingle(irp_id, bun.handleOom(mi.str(item.alias)), local_name_id, false)); + } + + if (record.flags.contains_import_star) { + const local_name = p.renamer.nameForSymbol(s.namespace_ref); + bun.handleOom(mi.addVar(bun.handleOom(mi.str(local_name)), .lexical)); + bun.handleOom(mi.addImportInfoNamespace(irp_id, bun.handleOom(mi.str(local_name)))); + } + } + } }, .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, tlmtlo.subVar()); p.printNewline(); }, .s_debugger => { @@ -4779,19 +4984,19 @@ fn NewPrinter( .s_local => |s| { switch (s.kind) { .k_var => { - p.printDecls("var", s.decls.slice(), ExprFlag.Set.init(.{ .forbid_in = true })); + 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 })); + 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 })); + 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 })); + 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 })); + p.printDecls("await using", s.decls.slice(), ExprFlag.Set.init(.{ .forbid_in = true }), .{}); }, } }, @@ -4802,7 +5007,7 @@ fn NewPrinter( }, } } - pub fn printIf(p: *Printer, s: *const S.If, loc: logger.Loc) void { + pub fn printIf(p: *Printer, s: *const S.If, loc: logger.Loc, tlmtlo: TopLevel) void { p.printSpaceBeforeIdentifier(); p.addSourceMapping(loc); p.print("if"); @@ -4814,7 +5019,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, tlmtlo); if (s.no != null) { p.printSpace(); @@ -4829,7 +5034,7 @@ fn NewPrinter( p.printNewline(); p.indent(); - p.printStmt(s.yes) catch unreachable; + p.printStmt(s.yes, tlmtlo) catch unreachable; p.unindent(); p.needs_semicolon = false; @@ -4844,7 +5049,7 @@ fn NewPrinter( } else { p.printNewline(); p.indent(); - p.printStmt(s.yes) catch unreachable; + p.printStmt(s.yes, tlmtlo) catch unreachable; p.unindent(); if (s.no != null) { @@ -4863,16 +5068,16 @@ 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, tlmtlo); p.printNewline(); }, .s_if => { - p.printIf(no_block.data.s_if, no_block.loc); + p.printIf(no_block.data.s_if, no_block.loc, tlmtlo); }, else => { p.printNewline(); p.indent(); - p.printStmt(no_block) catch unreachable; + p.printStmt(no_block, tlmtlo) catch unreachable; p.unindent(); }, } @@ -4953,11 +5158,20 @@ 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 { if (!rewrite_esm_to_cjs and is_export) { p.print("export "); } - p.printDecls(keyword, decls, ExprFlag.None()); + const tlm: TopLevelAndIsExport = if (may_have_module_info) .{ + .is_export = is_export, + .is_top_level = if (comptime strings.eqlComptime(keyword, "var")) + (if (tlmtlo.isTopLevel()) .declared else null) + else + // let/const are block-scoped: only record at true top-level, + // not inside blocks where subVar() downgrades to .var_only. + (if (tlmtlo.is_top_level == .yes) .lexical else null), + } else .{}; + p.printDecls(keyword, decls, ExprFlag.None(), tlm); p.printSemicolonAfterStatement(); if (rewrite_esm_to_cjs and is_export and decls.len > 0) { for (decls) |decl| { @@ -5002,7 +5216,7 @@ fn NewPrinter( p.print("}"); }, else => { - p.printBinding(decl.binding); + p.printBinding(decl.binding, .{}); }, } p.print(")"); @@ -5335,7 +5549,7 @@ fn NewPrinter( p.printFnArgs(func.open_parens_loc, func.args, func.flags.contains(.has_rest_arg), false); p.print(" => {\n"); p.indent(); - p.printBlockBody(func.body.stmts); + p.printBlockBody(func.body.stmts, TopLevel.init(.no)); p.unindent(); p.printIndent(); p.print("}, "); @@ -5792,6 +6006,9 @@ pub fn printAst( } } printer.was_lazy_export = tree.has_lazy_export; + if (PrinterType.may_have_module_info) { + printer.module_info = opts.module_info; + } var bin_stack_heap = std.heap.stackFallback(1024, bun.default_allocator); printer.binary_expression_stack = std.array_list.Managed(PrinterType.BinaryExpressionVisitor).init(bin_stack_heap.get()); defer printer.binary_expression_stack.clearAndFree(); @@ -5813,11 +6030,18 @@ pub fn printAst( // This is never a symbol collision because `uses_require_ref` means // `require` must be an unbound variable. printer.print("var {require}=import.meta;"); + + if (PrinterType.may_have_module_info) { + if (printer.moduleInfo()) |mi| { + mi.flags.contains_import_meta = true; + bun.handleOom(mi.addVar(bun.handleOom(mi.str("require")), .declared)); + } + } } 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; } @@ -5825,26 +6049,30 @@ pub fn printAst( } } - if (comptime FeatureFlags.runtime_transpiler_cache and generate_source_map) { - if (opts.source_map_handler) |handler| { - var 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); - } + const have_module_info = PrinterType.may_have_module_info and opts.module_info != null; + if (have_module_info) { + try opts.module_info.?.finalize(); + } - defer source_maps_chunk.deinit(); + var source_maps_chunk: ?SourceMap.Chunk = if (comptime generate_source_map) + if (opts.source_map_handler != null) + printer.source_map_builder.generateChunk(printer.writer.ctx.getWritten()) + else + null + else + null; + defer if (source_maps_chunk) |*chunk| chunk.deinit(); - 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.runtime_transpiler_cache) |cache| { + var srlz_res = std.array_list.Managed(u8).init(bun.default_allocator); + defer srlz_res.deinit(); + if (have_module_info) try opts.module_info.?.asDeserialized().serialize(srlz_res.writer()); + cache.put(printer.writer.ctx.getWritten(), if (source_maps_chunk) |chunk| chunk.buffer.list.items else "", srlz_res.items); + } + + if (comptime generate_source_map) { if (opts.source_map_handler) |handler| { - var chunk = printer.source_map_builder.generateChunk(printer.writer.ctx.getWritten()); - defer chunk.deinit(); - try handler.onSourceMapChunk(chunk, source); + try handler.onSourceMapChunk(source_maps_chunk.?, source); } } @@ -5986,6 +6214,9 @@ pub fn printWithWriterAndPlatform( getSourceMapBuilder(if (generate_source_maps) .eager else .disable, is_bun_platform, opts, source, &ast), ); printer.was_lazy_export = ast.has_lazy_export; + if (PrinterType.may_have_module_info) { + printer.module_info = opts.module_info; + } var bin_stack_heap = std.heap.stackFallback(1024, bun.default_allocator); printer.binary_expression_stack = std.array_list.Managed(PrinterType.BinaryExpressionVisitor).init(bin_stack_heap.get()); defer printer.binary_expression_stack.clearAndFree(); @@ -6004,7 +6235,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| { @@ -6074,7 +6305,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; } @@ -6098,9 +6329,24 @@ pub fn printCommonJS( return @as(usize, @intCast(@max(printer.writer.written, 0))); } +/// Serializes ModuleInfo to an owned byte slice. Returns null on failure. +/// The caller is responsible for freeing the returned slice with bun.default_allocator. +pub fn serializeModuleInfo(module_info: ?*analyze_transpiled_module.ModuleInfo) ?[]const u8 { + const mi = module_info orelse return null; + if (!mi.finalized) { + mi.finalize() catch return null; + } + const deserialized = mi.asDeserialized(); + var buf: std.ArrayList(u8) = .empty; + defer buf.deinit(bun.default_allocator); + deserialized.serialize(buf.writer(bun.default_allocator)) catch return null; + return buf.toOwnedSlice(bun.default_allocator) catch null; +} + const string = []const u8; const SourceMap = @import("./sourcemap/sourcemap.zig"); +const analyze_transpiled_module = @import("./analyze_transpiled_module.zig"); const fs = @import("./fs.zig"); const importRecord = @import("./import_record.zig"); const options = @import("./options.zig"); diff --git a/src/transpiler.zig b/src/transpiler.zig index 37a063355e..688c780038 100644 --- a/src/transpiler.zig +++ b/src/transpiler.zig @@ -783,6 +783,7 @@ pub const Transpiler = struct { comptime enable_source_map: bool, source_map_context: ?js_printer.SourceMapHandler, runtime_transpiler_cache: ?*bun.jsc.RuntimeTranspilerCache, + module_info: ?*analyze_transpiled_module.ModuleInfo, ) !usize { const tracer = if (enable_source_map) bun.perf.trace("JSPrinter.printWithSourceMap") @@ -872,6 +873,7 @@ pub const Transpiler = struct { .inline_require_and_import_errors = false, .import_meta_ref = ast.import_meta_ref, .runtime_transpiler_cache = runtime_transpiler_cache, + .module_info = module_info, .target = transpiler.options.target, .print_dce_annotations = transpiler.options.emit_dce_annotations, .hmr_ref = ast.wrapper_ref, @@ -900,6 +902,7 @@ pub const Transpiler = struct { false, null, null, + null, ); } @@ -910,6 +913,7 @@ pub const Transpiler = struct { writer: Writer, comptime format: js_printer.Format, handler: js_printer.SourceMapHandler, + module_info: ?*analyze_transpiled_module.ModuleInfo, ) !usize { if (bun.feature_flag.BUN_FEATURE_FLAG_DISABLE_SOURCE_MAPS.get()) { return transpiler.printWithSourceMapMaybe( @@ -921,6 +925,7 @@ pub const Transpiler = struct { false, handler, result.runtime_transpiler_cache, + module_info, ); } return transpiler.printWithSourceMapMaybe( @@ -932,6 +937,7 @@ pub const Transpiler = struct { true, handler, result.runtime_transpiler_cache, + module_info, ); } @@ -1621,6 +1627,7 @@ const Fs = @import("./fs.zig"); const MimeType = @import("./http/MimeType.zig"); const NodeFallbackModules = @import("./node_fallbacks.zig"); const Router = @import("./router.zig"); +const analyze_transpiled_module = @import("./analyze_transpiled_module.zig"); const runtime = @import("./runtime.zig"); const std = @import("std"); const DataURL = @import("./resolver/data_url.zig").DataURL; diff --git a/test/bundler/bundler_compile.test.ts b/test/bundler/bundler_compile.test.ts index 6be7076f73..28cea068e0 100644 --- a/test/bundler/bundler_compile.test.ts +++ b/test/bundler/bundler_compile.test.ts @@ -1,7 +1,8 @@ import { Database } from "bun:sqlite"; import { describe, expect, test } from "bun:test"; import { rmSync } from "fs"; -import { bunEnv, bunExe, isWindows, tempDirWithFiles } from "harness"; +import { bunEnv, bunExe, isWindows, tempDir, tempDirWithFiles } from "harness"; +import { join } from "path"; import { itBundled } from "./expectBundled"; describe("bundler", () => { @@ -89,6 +90,135 @@ describe("bundler", () => { }, }, }); + // ESM bytecode test matrix: each scenario × {default, minified} = 2 tests per scenario. + // With --compile, static imports are inlined into one chunk, but dynamic imports + // create separate modules in the standalone graph — each with its own bytecode + ModuleInfo. + const esmBytecodeScenarios: Array<{ + name: string; + files: Record; + stdout: string; + }> = [ + { + name: "HelloWorld", + files: { + "/entry.ts": `console.log("Hello, world!");`, + }, + stdout: "Hello, world!", + }, + { + // top-level await is ESM-only; if ModuleInfo or bytecode generation + // mishandles async modules, this breaks. + name: "TopLevelAwait", + files: { + "/entry.ts": ` + const result = await Promise.resolve("tla works"); + console.log(result); + `, + }, + stdout: "tla works", + }, + { + // import.meta is ESM-only. + name: "ImportMeta", + files: { + "/entry.ts": ` + console.log(typeof import.meta.url === "string" ? "ok" : "fail"); + console.log(typeof import.meta.dir === "string" ? "ok" : "fail"); + `, + }, + stdout: "ok\nok", + }, + { + // Dynamic import creates a separate module in the standalone graph, + // exercising per-module bytecode + ModuleInfo. + name: "DynamicImport", + files: { + "/entry.ts": ` + const { value } = await import("./lazy.ts"); + console.log("lazy:", value); + `, + "/lazy.ts": `export const value = 42;`, + }, + stdout: "lazy: 42", + }, + { + // Dynamic import of a module that itself uses top-level await. + // The dynamically imported module is a separate chunk with async + // evaluation — stresses both ModuleInfo and async bytecode loading. + name: "DynamicImportTLA", + files: { + "/entry.ts": ` + const mod = await import("./async-mod.ts"); + console.log("value:", mod.value); + `, + "/async-mod.ts": `export const value = await Promise.resolve(99);`, + }, + stdout: "value: 99", + }, + { + // Multiple dynamic imports: several separate modules in the graph, + // each with its own bytecode + ModuleInfo. + name: "MultipleDynamicImports", + files: { + "/entry.ts": ` + const [a, b] = await Promise.all([ + import("./mod-a.ts"), + import("./mod-b.ts"), + ]); + console.log(a.value, b.value); + `, + "/mod-a.ts": `export const value = "a";`, + "/mod-b.ts": `export const value = "b";`, + }, + stdout: "a b", + }, + ]; + + for (const scenario of esmBytecodeScenarios) { + for (const minify of [false, true]) { + itBundled(`compile/ESMBytecode+${scenario.name}${minify ? "+minify" : ""}`, { + compile: true, + bytecode: true, + format: "esm", + ...(minify && { + minifySyntax: true, + minifyIdentifiers: true, + minifyWhitespace: true, + }), + files: scenario.files, + run: { stdout: scenario.stdout }, + }); + } + } + + // Multi-entry ESM bytecode with Worker (can't be in the matrix — needs + // entryPointsRaw, outfile, setCwd). Each entry becomes a separate module + // in the standalone graph with its own bytecode + ModuleInfo. + itBundled("compile/WorkerBytecodeESM", { + backend: "cli", + compile: true, + bytecode: true, + format: "esm", + files: { + "/entry.ts": /* js */ ` + import {rmSync} from 'fs'; + // Verify we're not just importing from the filesystem + rmSync("./worker.ts", {force: true}); + console.log("Hello, world!"); + new Worker("./worker.ts"); + `, + "/worker.ts": /* js */ ` + console.log("Worker loaded!"); + `.trim(), + }, + entryPointsRaw: ["./entry.ts", "./worker.ts"], + outfile: "dist/out", + run: { + stdout: "Hello, world!\nWorker loaded!\n", + file: "dist/out", + setCwd: true, + }, + }); // https://github.com/oven-sh/bun/issues/8697 itBundled("compile/EmbeddedFileOutfile", { compile: true, @@ -311,6 +441,8 @@ describe("bundler", () => { format: "cjs" | "esm"; }> = [ { bytecode: true, minify: true, format: "cjs" }, + { bytecode: true, format: "esm" }, + { bytecode: true, minify: true, format: "esm" }, { format: "cjs" }, { format: "cjs", minify: true }, { format: "esm" }, @@ -736,6 +868,54 @@ const server = serve({ .throws(true); }); + // Verify ESM bytecode is actually loaded from the cache at runtime, not just generated. + // Uses regex matching on stderr (not itBundled) since we don't know the exact + // number of cache hit/miss lines for ESM standalone. + test("ESM bytecode cache is used at runtime", async () => { + const ext = isWindows ? ".exe" : ""; + using dir = tempDir("esm-bytecode-cache", { + "entry.js": `console.log("esm bytecode loaded");`, + }); + + const outfile = join(String(dir), `app${ext}`); + + // Build with ESM + bytecode + await using build = Bun.spawn({ + cmd: [ + bunExe(), + "build", + "--compile", + "--bytecode", + "--format=esm", + join(String(dir), "entry.js"), + "--outfile", + outfile, + ], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + + const [, buildStderr, buildExitCode] = await Promise.all([build.stdout.text(), build.stderr.text(), build.exited]); + + expect(buildStderr).toBe(""); + expect(buildExitCode).toBe(0); + + // Run with verbose disk cache to verify bytecode is loaded + await using exe = Bun.spawn({ + cmd: [outfile], + env: { ...bunEnv, BUN_JSC_verboseDiskCache: "1" }, + stdout: "pipe", + stderr: "pipe", + }); + + const [exeStdout, exeStderr, exeExitCode] = await Promise.all([exe.stdout.text(), exe.stderr.text(), exe.exited]); + + expect(exeStdout).toContain("esm bytecode loaded"); + expect(exeStderr).toMatch(/\[Disk Cache\].*Cache hit/i); + expect(exeExitCode).toBe(0); + }); + // When compiling with 8+ entry points, the main entry point should still run correctly. test("compile with 8+ entry points runs main entry correctly", async () => { const dir = tempDirWithFiles("compile-many-entries", { diff --git a/test/bundler/bundler_compile_splitting.test.ts b/test/bundler/bundler_compile_splitting.test.ts index f80d0bfdc9..c2bdd9784b 100644 --- a/test/bundler/bundler_compile_splitting.test.ts +++ b/test/bundler/bundler_compile_splitting.test.ts @@ -36,5 +36,30 @@ describe("bundler", () => { stdout: "app entry\nheader rendering\nmenu showing\nitems: home,about,contact", }, }); + + for (const minify of [false, true]) { + itBundled(`compile/splitting/ImportMetaInSplitChunk${minify ? "+minify" : ""}`, { + compile: true, + splitting: true, + bytecode: true, + format: "esm", + ...(minify ? { minifySyntax: true, minifyIdentifiers: true, minifyWhitespace: true } : {}), + files: { + "/entry.ts": /* js */ ` + const mod = await import("./worker.ts"); + mod.run(); + `, + "/worker.ts": /* js */ ` + export function run() { + console.log(typeof import.meta.url === "string" ? "ok" : "fail"); + console.log(typeof import.meta.dir === "string" ? "ok" : "fail"); + } + `, + }, + run: { + stdout: "ok\nok", + }, + }); + } }); }); diff --git a/test/bundler/compile-windows-metadata.test.ts b/test/bundler/compile-windows-metadata.test.ts index 465d417094..a513dea104 100644 --- a/test/bundler/compile-windows-metadata.test.ts +++ b/test/bundler/compile-windows-metadata.test.ts @@ -163,8 +163,8 @@ describe.skipIf(!isWindows).concurrent("Windows compile metadata", () => { const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]); expect(exitCode).not.toBe(0); - // When cross-compiling to non-Windows, it tries to download the target but fails - expect(stderr.toLowerCase()).toContain("target platform"); + // Windows flags require a Windows compile target + expect(stderr.toLowerCase()).toContain("windows compile target"); }); }); diff --git a/test/integration/typegraphql/src/unsolvable.test.ts b/test/integration/typegraphql/src/unsolvable.test.ts index c5e7a533fe..783a57fada 100644 --- a/test/integration/typegraphql/src/unsolvable.test.ts +++ b/test/integration/typegraphql/src/unsolvable.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from "bun:test" with { todo: "true" }; +import { expect, test } from "bun:test"; import "reflect-metadata"; function Abc() { return (target: any, field: string) => {}; 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..8a80c0af7c --- /dev/null +++ b/test/js/bun/typescript/type-export.test.ts @@ -0,0 +1,499 @@ +import { describe, expect, test } from "bun:test"; +import { bunEnv, bunExe, isWindows, tempDirWithFiles } from "harness"; + +const ext = isWindows ? ".exe" : ""; + +function compileAndRun(dir: string, entrypoint: string) { + const outfile = dir + `/compiled${ext}`; + const buildResult = Bun.spawnSync({ + cmd: [bunExe(), "build", "--compile", "--bytecode", "--format=esm", entrypoint, "--outfile", outfile], + env: bunEnv, + cwd: dir, + stdio: ["inherit", "pipe", "pipe"], + }); + expect(buildResult.stderr.toString()).toBe(""); + expect(buildResult.exitCode).toBe(0); + + return Bun.spawnSync({ + cmd: [outfile], + env: bunEnv, + cwd: dir, + stdio: ["inherit", "pipe", "pipe"], + }); +} + +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, + }); + + describe.each(["run", "compile", "build"])("%s", mode => { + // TODO: "run" is skipped until ESM module_info is enabled in the runtime transpiler. + // Currently module_info is only generated for standalone ESM bytecode (--compile). + // Once enabled, flip this to include "run". + test.skipIf(mode === "run")("works", async () => { + let result: Bun.SyncSubprocess<"pipe", "inherit"> | Bun.SyncSubprocess<"pipe", "pipe">; + if (mode === "compile") { + result = compileAndRun(dir, dir + "/c.ts"); + } else if (mode === "build") { + const build_result = await Bun.build({ + entrypoints: [dir + "/c.ts"], + outdir: dir + "/dist", + }); + expect(build_result.success).toBe(true); + result = Bun.spawnSync({ + cmd: [bunExe(), "run", dir + "/dist/c.js"], + cwd: dir, + env: bunEnv, + stdio: ["inherit", "pipe", "inherit"], + }); + } else { + result = Bun.spawnSync({ + cmd: [bunExe(), "run", "c.ts"], + cwd: dir, + env: bunEnv, + stdio: ["inherit", "pipe", "inherit"], + }); + } + + const parsedOutput = JSON.parse(result.stdout.toString().trim()); + expect(parsedOutput).toEqual({ my_value: "2", my_only: "3" }); + expect(result.exitCode).toBe(0); + }); + }); + }); + } + }); +} + +describe("import not found", () => { + for (const [ccase, target_value, name] of [ + [``, /SyntaxError: Export named 'not_found' not found in module '[^']+?'\./, "none"], + [ + `export default function not_found() {};`, + /SyntaxError: Export named 'not_found' not found in module '[^']+?'\. Did you mean to import default\?/, + "default with same name", + ], + [ + `export type not_found = "not_found";`, + /SyntaxError: Export named 'not_found' not found in module '[^']+?'\./, + "type", + ], + ] as const) + test(`${name}`, () => { + const dir = tempDirWithFiles("type-export", { + "a.ts": ccase, + "b.ts": /*js*/ ` + import { not_found } from "./a"; + console.log(not_found); + `, + "nf.ts": "", + }); + + const result = Bun.spawnSync({ + cmd: [bunExe(), "run", "b.ts"], + cwd: dir, + env: bunEnv, + stdio: ["inherit", "pipe", "pipe"], + }); + + expect(result.stderr?.toString().trim()).toMatch(target_value); + expect({ + exitCode: result.exitCode, + stdout: result.stdout?.toString().trim(), + }).toEqual({ + exitCode: 1, + stdout: "", + }); + }); +}); + +test("js file type export", () => { + const dir = tempDirWithFiles("type-export", { + "a.js": "export {not_found};", + }); + + const result = Bun.spawnSync({ + cmd: [bunExe(), "a.js"], + cwd: dir, + env: bunEnv, + stdio: ["inherit", "pipe", "pipe"], + }); + + expect(result.stderr?.toString().trim()).toInclude('error: "not_found" is not declared in this file'); + expect(result.exitCode).toBe(1); +}); + +test("js file type import", () => { + const dir = tempDirWithFiles("type-import", { + "b.js": "import {type_only} from './ts.ts';", + "ts.ts": "export type type_only = 'type_only';", + }); + + const result = Bun.spawnSync({ + cmd: [bunExe(), "b.js"], + cwd: dir, + env: bunEnv, + stdio: ["inherit", "pipe", "pipe"], + }); + + expect(result.stderr?.toString().trim()).toInclude("Export named 'type_only' not found in module '"); + expect(result.stderr?.toString().trim()).not.toInclude("Did you mean to import default?"); + expect(result.exitCode).toBe(1); +}); + +test("js file type import with default export", () => { + const dir = tempDirWithFiles("type-import", { + "b.js": "import {type_only} from './ts.ts';", + "ts.ts": "export type type_only = 'type_only'; export default function type_only() {};", + }); + + const result = Bun.spawnSync({ + cmd: [bunExe(), "b.js"], + cwd: dir, + env: bunEnv, + stdio: ["inherit", "pipe", "pipe"], + }); + + expect(result.stderr?.toString().trim()).toInclude("Export named 'type_only' not found in module '"); + expect(result.stderr?.toString().trim()).toInclude("Did you mean to import default?"); + expect(result.exitCode).toBe(1); +}); + +test("js file with through export", () => { + const dir = tempDirWithFiles("type-import", { + "b.js": "export {type_only} from './ts.ts';", + "ts.ts": "export type type_only = 'type_only'; export default function type_only() {};", + }); + + const result = Bun.spawnSync({ + cmd: [bunExe(), "b.js"], + cwd: dir, + env: bunEnv, + stdio: ["inherit", "pipe", "pipe"], + }); + + expect(result.stderr?.toString().trim()).toInclude("SyntaxError: export 'type_only' not found in './ts.ts'"); + expect(result.exitCode).toBe(1); +}); + +test("js file with through export 2", () => { + const dir = tempDirWithFiles("type-import", { + "b.js": "import {type_only} from './ts.ts'; export {type_only};", + "ts.ts": "export type type_only = 'type_only'; export default function type_only() {};", + }); + + const result = Bun.spawnSync({ + cmd: [bunExe(), "b.js"], + cwd: dir, + env: bunEnv, + stdio: ["inherit", "pipe", "pipe"], + }); + + expect(result.stderr?.toString().trim()).toInclude("SyntaxError: export 'type_only' not found in './ts.ts'"); + expect(result.exitCode).toBe(1); +}); + +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); + }); + } + }); + } + }); + } +}); + +describe("check ownkeys from a star import", () => { + const dir = tempDirWithFiles("ownkeys-star-import", { + ["main.ts"]: ` + import * as ns from './a'; + console.log(JSON.stringify({ + keys: Object.keys(ns).sort(), + ns, + has_sometype: Object.hasOwn(ns, 'sometype'), + })); + `, + ["a.ts"]: "export * from './b'; export {sometype} from './b';", + ["b.ts"]: "export const value = 'b'; export const anotherValue = 'another'; export type sometype = 'sometype';", + }); + + const expected = { + keys: ["anotherValue", "value"], + ns: { + anotherValue: "another", + value: "b", + }, + has_sometype: false, + }; + + describe.each(["run", "compile"] as const)("%s", mode => { + const testFn = mode === "run" ? test.skip : test; + + testFn("works", () => { + const result = + mode === "compile" + ? compileAndRun(dir, dir + "/main.ts") + : Bun.spawnSync({ + cmd: [bunExe(), "main.ts"], + cwd: dir, + env: bunEnv, + stdio: ["inherit", "pipe", "pipe"], + }); + + expect(result.stderr?.toString().trim()).toBe(""); + expect(JSON.parse(result.stdout?.toString().trim())).toEqual(expected); + expect(result.exitCode).toBe(0); + }); + }); +}); + +test("check commonjs", () => { + const dir = tempDirWithFiles("commonjs", { + ["main.ts"]: "const {my_value, my_type} = require('./a'); console.log(my_value, my_type);", + ["a.ts"]: "module.exports = require('./b');", + ["b.ts"]: "export const my_value = 'my_value'; export type my_type = 'my_type';", + }); + const result = Bun.spawnSync({ + cmd: [bunExe(), "main.ts"], + cwd: dir, + env: bunEnv, + stdio: ["inherit", "pipe", "pipe"], + }); + expect(result.stderr?.toString().trim()).toBe(""); + expect(result.stdout?.toString().trim()).toBe("my_value undefined"); + expect(result.exitCode).toBe(0); +}); + +test("check merge", () => { + const dir = tempDirWithFiles("merge", { + ["main.ts"]: "import {value} from './a'; console.log(value);", + ["a.ts"]: "export * from './b'; export * from './c';", + ["b.ts"]: "export const value = 'b';", + ["c.ts"]: "export const value = 'c';", + }); + const result = Bun.spawnSync({ + cmd: [bunExe(), "main.ts"], + cwd: dir, + env: bunEnv, + stdio: ["inherit", "pipe", "pipe"], + }); + expect(result.stderr?.toString().trim()).toInclude( + "SyntaxError: Export named 'value' cannot be resolved due to ambiguous multiple bindings in module", + ); + expect(result.exitCode).toBe(1); +}); + +describe("export * from './module'", () => { + for (const fmt of ["js", "ts"]) { + describe(fmt, () => { + const dir = tempDirWithFiles("export-star", { + ["main." + fmt]: "import {value} from './a'; console.log(value);", + ["a." + fmt]: "export * from './b';", + ["b." + fmt]: "export const value = 'b';", + }); + 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()).toBe(""); + expect(result.exitCode).toBe(0); + }); + } + }); + } +}); + +describe("export * as ns from './module'", () => { + for (const fmt of ["js", "ts"]) { + describe(fmt, () => { + const dir = tempDirWithFiles("export-star-as", { + ["main." + fmt]: "import {ns} from './a'; console.log(ns.value);", + ["a." + fmt]: "export * as ns from './b';", + ["b." + fmt]: "export const value = 'b';", + }); + 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()).toBe(""); + expect(result.exitCode).toBe(0); + }); + } + }); + } +}); + +describe("export type {Type} from './module'", () => { + for (const fmt of ["ts"]) { + describe(fmt, () => { + const dir = tempDirWithFiles("export-type", { + ["main." + fmt]: "import {Type} from './a'; const x: Type = 'test'; console.log(x);", + ["a." + fmt]: "export type {Type} from './b';", + ["b." + fmt]: "export type Type = string;", + }); + 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()).toBe(""); + expect(result.exitCode).toBe(0); + }); + } + }); + } +}); + +describe("import only used in decorator (#8439)", () => { + const dir = tempDirWithFiles("import-only-used-in-decorator", { + ["index.ts"]: /*js*/ ` + import { TestInterface } from "./interface.ts"; + + function Decorator(): PropertyDecorator { + return () => {}; + } + + class TestClass { + @Decorator() + test?: TestInterface; + } + class OtherClass { + other?: TestInterface; + } + + export {TestInterface}; + `, + ["interface.ts"]: "export interface TestInterface {};", + "tsconfig.json": JSON.stringify({ + compilerOptions: { + experimentalDecorators: true, + emitDecoratorMetadata: true, + }, + }), + }); + + describe.each(["run", "compile"] as const)("%s", mode => { + const testFn = mode === "run" ? test.skip : test; + + testFn("works", () => { + const result = + mode === "compile" + ? compileAndRun(dir, dir + "/index.ts") + : Bun.spawnSync({ + cmd: [bunExe(), "index.ts"], + cwd: dir, + env: bunEnv, + stdio: ["inherit", "pipe", "pipe"], + }); + + expect(result.stderr?.toString().trim()).toBe(""); + expect(result.exitCode).toBe(0); + }); + }); +});