From fa1d37b4e3fdffaff95e149387f99fcb8912e9fb Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Fri, 6 Jun 2025 18:34:18 -0700 Subject: [PATCH] Split bundler up into multiple files (#20192) --- build.zig | 5 + cmake/sources/ZigSources.txt | 31 + package.json | 2 +- src/bun.zig | 2 + src/bundler/AstBuilder.zig | 371 + src/bundler/BundleThread.zig | 190 + src/bundler/Chunk.zig | 620 + src/bundler/DeferredBatchTask.zig | 52 + src/bundler/Graph.zig | 128 + src/bundler/LinkerContext.zig | 2479 +++ src/bundler/LinkerGraph.zig | 467 + src/bundler/ParseTask.zig | 1446 ++ src/bundler/ServerComponentParseTask.zig | 236 + src/bundler/ThreadPool.zig | 293 + src/bundler/bundle_v2.zig | 14231 +--------------- src/bundler/entry_points.zig | 18 +- src/bundler/linker_context/README.md | 1094 ++ src/bundler/linker_context/computeChunks.zig | 382 + .../computeCrossChunkDependencies.zig | 455 + .../linker_context/convertStmtsForChunk.zig | 552 + .../convertStmtsForChunkForDevServer.zig | 175 + src/bundler/linker_context/doStep5.zig | 497 + .../findAllImportedPartsInJSOrder.zig | 218 + .../findImportedCSSFilesInJSOrder.zig | 100 + .../findImportedFilesInCSSOrder.zig | 680 + .../generateChunksInParallel.zig | 595 + .../generateCodeForFileInChunkJS.zig | 709 + .../generateCodeForLazyExport.zig | 419 + .../generateCompileResultForCssChunk.zig | 168 + .../generateCompileResultForHtmlChunk.zig | 278 + .../generateCompileResultForJSChunk.zig | 95 + .../linker_context/postProcessCSSChunk.zig | 128 + .../linker_context/postProcessHTMLChunk.zig | 36 + .../linker_context/postProcessJSChunk.zig | 901 + .../linker_context/prepareCssAstsForChunk.zig | 288 + .../linker_context/renameSymbolsInChunk.zig | 274 + .../linker_context/scanImportsAndExports.zig | 1240 ++ .../linker_context/writeOutputFilesToDisk.zig | 438 + src/env.zig | 2 +- 39 files changed, 16151 insertions(+), 14144 deletions(-) create mode 100644 src/bundler/AstBuilder.zig create mode 100644 src/bundler/BundleThread.zig create mode 100644 src/bundler/Chunk.zig create mode 100644 src/bundler/DeferredBatchTask.zig create mode 100644 src/bundler/Graph.zig create mode 100644 src/bundler/LinkerContext.zig create mode 100644 src/bundler/LinkerGraph.zig create mode 100644 src/bundler/ParseTask.zig create mode 100644 src/bundler/ServerComponentParseTask.zig create mode 100644 src/bundler/ThreadPool.zig create mode 100644 src/bundler/linker_context/README.md create mode 100644 src/bundler/linker_context/computeChunks.zig create mode 100644 src/bundler/linker_context/computeCrossChunkDependencies.zig create mode 100644 src/bundler/linker_context/convertStmtsForChunk.zig create mode 100644 src/bundler/linker_context/convertStmtsForChunkForDevServer.zig create mode 100644 src/bundler/linker_context/doStep5.zig create mode 100644 src/bundler/linker_context/findAllImportedPartsInJSOrder.zig create mode 100644 src/bundler/linker_context/findImportedCSSFilesInJSOrder.zig create mode 100644 src/bundler/linker_context/findImportedFilesInCSSOrder.zig create mode 100644 src/bundler/linker_context/generateChunksInParallel.zig create mode 100644 src/bundler/linker_context/generateCodeForFileInChunkJS.zig create mode 100644 src/bundler/linker_context/generateCodeForLazyExport.zig create mode 100644 src/bundler/linker_context/generateCompileResultForCssChunk.zig create mode 100644 src/bundler/linker_context/generateCompileResultForHtmlChunk.zig create mode 100644 src/bundler/linker_context/generateCompileResultForJSChunk.zig create mode 100644 src/bundler/linker_context/postProcessCSSChunk.zig create mode 100644 src/bundler/linker_context/postProcessHTMLChunk.zig create mode 100644 src/bundler/linker_context/postProcessJSChunk.zig create mode 100644 src/bundler/linker_context/prepareCssAstsForChunk.zig create mode 100644 src/bundler/linker_context/renameSymbolsInChunk.zig create mode 100644 src/bundler/linker_context/scanImportsAndExports.zig create mode 100644 src/bundler/linker_context/writeOutputFilesToDisk.zig diff --git a/build.zig b/build.zig index 04afe96fe8..d3b78178e5 100644 --- a/build.zig +++ b/build.zig @@ -63,6 +63,7 @@ const BunBuildOptions = struct { /// `./build/codegen` or equivalent codegen_path: []const u8, no_llvm: bool, + override_no_export_cpp_apis: bool, cached_options_module: ?*Module = null, windows_shim: ?WindowsShim = null, @@ -95,6 +96,7 @@ const BunBuildOptions = struct { opts.addOption(bool, "enable_asan", this.enable_asan); opts.addOption([]const u8, "reported_nodejs_version", b.fmt("{}", .{this.reported_nodejs_version})); opts.addOption(bool, "zig_self_hosted_backend", this.no_llvm); + opts.addOption(bool, "override_no_export_cpp_apis", this.override_no_export_cpp_apis); const mod = opts.createModule(); this.cached_options_module = mod; @@ -206,6 +208,7 @@ pub fn build(b: *Build) !void { const obj_format = b.option(ObjectFormat, "obj_format", "Output file for object files") orelse .obj; const no_llvm = b.option(bool, "no_llvm", "Experiment with Zig self hosted backends. No stability guaranteed") orelse false; + const override_no_export_cpp_apis = b.option(bool, "override-no-export-cpp-apis", "Override the default export_cpp_apis logic to disable exports") orelse false; var build_options = BunBuildOptions{ .target = target, @@ -217,6 +220,7 @@ pub fn build(b: *Build) !void { .codegen_path = codegen_path, .codegen_embed = codegen_embed, .no_llvm = no_llvm, + .override_no_export_cpp_apis = override_no_export_cpp_apis, .version = try Version.parse(bun_version), .canary_revision = canary: { @@ -476,6 +480,7 @@ fn addMultiCheck( .codegen_path = root_build_options.codegen_path, .no_llvm = root_build_options.no_llvm, .enable_asan = root_build_options.enable_asan, + .override_no_export_cpp_apis = root_build_options.override_no_export_cpp_apis, }; var obj = addBunObject(b, &options); diff --git a/cmake/sources/ZigSources.txt b/cmake/sources/ZigSources.txt index 3f3540bf7e..a5b7749608 100644 --- a/cmake/sources/ZigSources.txt +++ b/cmake/sources/ZigSources.txt @@ -250,8 +250,39 @@ src/bun.js/webcore/TextEncoder.zig src/bun.js/webcore/TextEncoderStreamEncoder.zig src/bun.js/WTFTimer.zig src/bun.zig +src/bundler/AstBuilder.zig src/bundler/bundle_v2.zig +src/bundler/BundleThread.zig +src/bundler/Chunk.zig +src/bundler/DeferredBatchTask.zig src/bundler/entry_points.zig +src/bundler/Graph.zig +src/bundler/linker_context/computeChunks.zig +src/bundler/linker_context/computeCrossChunkDependencies.zig +src/bundler/linker_context/convertStmtsForChunk.zig +src/bundler/linker_context/convertStmtsForChunkForDevServer.zig +src/bundler/linker_context/doStep5.zig +src/bundler/linker_context/findAllImportedPartsInJSOrder.zig +src/bundler/linker_context/findImportedCSSFilesInJSOrder.zig +src/bundler/linker_context/findImportedFilesInCSSOrder.zig +src/bundler/linker_context/generateChunksInParallel.zig +src/bundler/linker_context/generateCodeForFileInChunkJS.zig +src/bundler/linker_context/generateCodeForLazyExport.zig +src/bundler/linker_context/generateCompileResultForCssChunk.zig +src/bundler/linker_context/generateCompileResultForHtmlChunk.zig +src/bundler/linker_context/generateCompileResultForJSChunk.zig +src/bundler/linker_context/postProcessCSSChunk.zig +src/bundler/linker_context/postProcessHTMLChunk.zig +src/bundler/linker_context/postProcessJSChunk.zig +src/bundler/linker_context/prepareCssAstsForChunk.zig +src/bundler/linker_context/renameSymbolsInChunk.zig +src/bundler/linker_context/scanImportsAndExports.zig +src/bundler/linker_context/writeOutputFilesToDisk.zig +src/bundler/LinkerContext.zig +src/bundler/LinkerGraph.zig +src/bundler/ParseTask.zig +src/bundler/ServerComponentParseTask.zig +src/bundler/ThreadPool.zig src/bunfig.zig src/cache.zig src/ci_info.zig diff --git a/package.json b/package.json index b6e7d28a7b..6eac407617 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ }, "scripts": { "build": "bun run build:debug", - "watch": "zig build check --watch -fincremental --prominent-compile-errors --global-cache-dir build/debug/zig-check-cache --zig-lib-dir vendor/zig/lib", + "watch": "zig build check --watch -fincremental --prominent-compile-errors --global-cache-dir build/debug/zig-check-cache --zig-lib-dir vendor/zig/lib -Doverride-no-export-cpp-apis=true", "watch-windows": "zig build check-windows --watch -fincremental --prominent-compile-errors --global-cache-dir build/debug/zig-check-cache --zig-lib-dir vendor/zig/lib", "bd:v": "(bun run --silent build:debug &> /tmp/bun.debug.build.log || (cat /tmp/bun.debug.build.log && rm -rf /tmp/bun.debug.build.log && exit 1)) && rm -f /tmp/bun.debug.build.log && ./build/debug/bun-debug", "bd": "BUN_DEBUG_QUIET_LOGS=1 bun bd:v", diff --git a/src/bun.zig b/src/bun.zig index cc31819545..8e070de409 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -1820,6 +1820,8 @@ pub const StringMap = struct { pub const DotEnv = @import("./env_loader.zig"); pub const bundle_v2 = @import("./bundler/bundle_v2.zig"); +pub const js_ast = bun.bundle_v2.js_ast; +pub const Loader = bundle_v2.Loader; pub const BundleV2 = bundle_v2.BundleV2; pub const ParseTask = bundle_v2.ParseTask; diff --git a/src/bundler/AstBuilder.zig b/src/bundler/AstBuilder.zig new file mode 100644 index 0000000000..508673b56c --- /dev/null +++ b/src/bundler/AstBuilder.zig @@ -0,0 +1,371 @@ +/// Utility to construct `Ast`s intended for generated code, such as the +/// boundary modules when dealing with server components. This is a saner +/// alternative to building a string, then sending it through `js_parser` +/// +/// For in-depth details on the fields, most of these are documented +/// inside of `js_parser` +pub const AstBuilder = struct { + allocator: std.mem.Allocator, + source: *const Logger.Source, + source_index: u31, + stmts: std.ArrayListUnmanaged(Stmt), + scopes: std.ArrayListUnmanaged(*Scope), + symbols: std.ArrayListUnmanaged(Symbol), + import_records: std.ArrayListUnmanaged(ImportRecord), + named_imports: js_ast.Ast.NamedImports, + named_exports: js_ast.Ast.NamedExports, + import_records_for_current_part: std.ArrayListUnmanaged(u32), + export_star_import_records: std.ArrayListUnmanaged(u32), + current_scope: *Scope, + log: Logger.Log, + module_ref: Ref, + declared_symbols: js_ast.DeclaredSymbol.List, + /// When set, codegen is altered + hot_reloading: bool, + hmr_api_ref: Ref, + + // stub fields for ImportScanner duck typing + comptime options: js_parser.Parser.Options = .{ + .jsx = .{}, + .bundle = true, + }, + comptime import_items_for_namespace: struct { + pub fn get(_: @This(), _: Ref) ?js_parser.ImportItemForNamespaceMap { + return null; + } + } = .{}, + pub const parser_features = struct { + pub const typescript = false; + }; + + pub fn init(allocator: std.mem.Allocator, source: *const Logger.Source, hot_reloading: bool) !AstBuilder { + const scope = try allocator.create(Scope); + scope.* = .{ + .kind = .entry, + .label_ref = null, + .parent = null, + .generated = .{}, + }; + var ab: AstBuilder = .{ + .allocator = allocator, + .current_scope = scope, + .source = source, + .source_index = @intCast(source.index.get()), + .stmts = .{}, + .scopes = .{}, + .symbols = .{}, + .import_records = .{}, + .import_records_for_current_part = .{}, + .named_imports = .{}, + .named_exports = .{}, + .log = Logger.Log.init(allocator), + .export_star_import_records = .{}, + .declared_symbols = .{}, + .hot_reloading = hot_reloading, + .module_ref = undefined, + .hmr_api_ref = undefined, + }; + ab.module_ref = try ab.newSymbol(.other, "module"); + ab.hmr_api_ref = try ab.newSymbol(.other, "hmr"); + return ab; + } + + pub fn pushScope(p: *AstBuilder, kind: Scope.Kind) *js_ast.Scope { + try p.scopes.ensureUnusedCapacity(p.allocator, 1); + try p.current_scope.children.ensureUnusedCapacity(p.allocator, 1); + const scope = try p.allocator.create(Scope); + scope.* = .{ + .kind = kind, + .label_ref = null, + .parent = p.current_scope, + .generated = .{}, + }; + p.current_scope.children.appendAssumeCapacity(scope); + p.scopes.appendAssumeCapacity(p.current_scope); + p.current_scope = scope; + return scope; + } + + pub fn popScope(p: *AstBuilder) void { + p.current_scope = p.scopes.pop(); + } + + pub fn newSymbol(p: *AstBuilder, kind: Symbol.Kind, identifier: []const u8) !Ref { + const inner_index: Ref.Int = @intCast(p.symbols.items.len); + try p.symbols.append(p.allocator, .{ + .kind = kind, + .original_name = identifier, + }); + const ref: Ref = .{ + .inner_index = inner_index, + .source_index = p.source_index, + .tag = .symbol, + }; + try p.current_scope.generated.push(p.allocator, ref); + try p.declared_symbols.append(p.allocator, .{ + .ref = ref, + .is_top_level = p.scopes.items.len == 0 or p.current_scope == p.scopes.items[0], + }); + return ref; + } + + pub fn getSymbol(p: *AstBuilder, ref: Ref) *Symbol { + bun.assert(ref.source_index == p.source.index.get()); + return &p.symbols.items[ref.inner_index]; + } + + pub fn addImportRecord(p: *AstBuilder, path: []const u8, kind: ImportKind) !u32 { + const index = p.import_records.items.len; + try p.import_records.append(p.allocator, .{ + .path = bun.fs.Path.init(path), + .kind = kind, + .range = .{}, + }); + return @intCast(index); + } + + pub fn addImportStmt( + p: *AstBuilder, + path: []const u8, + identifiers_to_import: anytype, + ) ![identifiers_to_import.len]Expr { + var out: [identifiers_to_import.len]Expr = undefined; + + const record = try p.addImportRecord(path, .stmt); + + var path_name = bun.fs.PathName.init(path); + const name = try strings.append(p.allocator, "import_", try path_name.nonUniqueNameString(p.allocator)); + const namespace_ref = try p.newSymbol(.other, name); + + const clauses = try p.allocator.alloc(js_ast.ClauseItem, identifiers_to_import.len); + + inline for (identifiers_to_import, &out, clauses) |import_id_untyped, *out_ref, *clause| { + const import_id: []const u8 = import_id_untyped; // must be given '[N][]const u8' + const ref = try p.newSymbol(.import, import_id); + if (p.hot_reloading) { + p.getSymbol(ref).namespace_alias = .{ + .namespace_ref = namespace_ref, + .alias = import_id, + .import_record_index = record, + }; + } + out_ref.* = p.newExpr(E.ImportIdentifier{ .ref = ref }); + clause.* = .{ + .name = .{ .loc = Logger.Loc.Empty, .ref = ref }, + .original_name = import_id, + .alias = import_id, + }; + } + + try p.appendStmt(S.Import{ + .namespace_ref = namespace_ref, + .import_record_index = record, + .items = clauses, + .is_single_line = identifiers_to_import.len < 1, + }); + + return out; + } + + pub fn appendStmt(p: *AstBuilder, data: anytype) !void { + try p.stmts.ensureUnusedCapacity(p.allocator, 1); + p.stmts.appendAssumeCapacity(p.newStmt(data)); + } + + pub fn newStmt(p: *AstBuilder, data: anytype) Stmt { + _ = p; + return Stmt.alloc(@TypeOf(data), data, Logger.Loc.Empty); + } + + pub fn newExpr(p: *AstBuilder, data: anytype) Expr { + _ = p; + return Expr.init(@TypeOf(data), data, Logger.Loc.Empty); + } + + pub fn newExternalSymbol(p: *AstBuilder, name: []const u8) !Ref { + const ref = try p.newSymbol(.other, name); + const sym = p.getSymbol(ref); + sym.must_not_be_renamed = true; + return ref; + } + + pub fn toBundledAst(p: *AstBuilder, target: options.Target) !js_ast.BundledAst { + // TODO: missing import scanner + bun.assert(p.scopes.items.len == 0); + const module_scope = p.current_scope; + + var parts = try Part.List.initCapacity(p.allocator, 2); + parts.len = 2; + parts.mut(0).* = .{}; + parts.mut(1).* = .{ + .stmts = p.stmts.items, + .can_be_removed_if_unused = false, + + // pretend that every symbol was used + .symbol_uses = uses: { + var map: Part.SymbolUseMap = .{}; + try map.ensureTotalCapacity(p.allocator, p.symbols.items.len); + for (0..p.symbols.items.len) |i| { + map.putAssumeCapacity(Ref{ + .tag = .symbol, + .source_index = p.source_index, + .inner_index = @intCast(i), + }, .{ .count_estimate = 1 }); + } + break :uses map; + }, + }; + + const single_u32 = try BabyList(u32).fromSlice(p.allocator, &.{1}); + + var top_level_symbols_to_parts = js_ast.Ast.TopLevelSymbolToParts{}; + try top_level_symbols_to_parts.entries.setCapacity(p.allocator, module_scope.generated.len); + top_level_symbols_to_parts.entries.len = module_scope.generated.len; + const slice = top_level_symbols_to_parts.entries.slice(); + for ( + slice.items(.key), + slice.items(.value), + module_scope.generated.slice(), + ) |*k, *v, ref| { + k.* = ref; + v.* = single_u32; + } + try top_level_symbols_to_parts.reIndex(p.allocator); + + // For more details on this section, look at js_parser.toAST + // This is mimicking how it calls ImportScanner + if (p.hot_reloading) { + var hmr_transform_ctx = js_parser.ConvertESMExportsForHmr{ + .last_part = parts.last() orelse + unreachable, // was definitely allocated + .is_in_node_modules = p.source.path.isNodeModule(), + }; + try hmr_transform_ctx.stmts.ensureTotalCapacity(p.allocator, prealloc_count: { + // get a estimate on how many statements there are going to be + const count = p.stmts.items.len; + break :prealloc_count count + 2; + }); + + _ = try js_parser.ImportScanner.scan(AstBuilder, p, p.stmts.items, false, true, &hmr_transform_ctx); + + try hmr_transform_ctx.finalize(p, parts.slice()); + const new_parts = parts.slice(); + // preserve original capacity + parts.len = @intCast(new_parts.len); + bun.assert(new_parts.ptr == parts.ptr); + } else { + const result = try js_parser.ImportScanner.scan(AstBuilder, p, p.stmts.items, false, false, {}); + parts.mut(1).stmts = result.stmts; + } + + parts.mut(1).declared_symbols = p.declared_symbols; + parts.mut(1).scopes = p.scopes.items; + parts.mut(1).import_record_indices = BabyList(u32).fromList(p.import_records_for_current_part); + + return .{ + .parts = parts, + .module_scope = module_scope.*, + .symbols = js_ast.Symbol.List.fromList(p.symbols), + .exports_ref = Ref.None, + .wrapper_ref = Ref.None, + .module_ref = p.module_ref, + .import_records = ImportRecord.List.fromList(p.import_records), + .export_star_import_records = &.{}, + .approximate_newline_count = 1, + .exports_kind = .esm, + .named_imports = p.named_imports, + .named_exports = p.named_exports, + .top_level_symbols_to_parts = top_level_symbols_to_parts, + .char_freq = .{}, + .flags = .{}, + .target = target, + .top_level_await_keyword = Logger.Range.None, + // .nested_scope_slot_counts = if (p.options.features.minify_identifiers) + // renamer.assignNestedScopeSlots(p.allocator, p.scopes.items[0], p.symbols.items) + // else + // js_ast.SlotCounts{}, + }; + } + + // stub methods for ImportScanner duck typing + + pub fn generateTempRef(ab: *AstBuilder, name: ?[]const u8) Ref { + return ab.newSymbol(.other, name orelse "temp") catch bun.outOfMemory(); + } + + pub fn recordExport(p: *AstBuilder, _: Logger.Loc, alias: []const u8, ref: Ref) !void { + if (p.named_exports.get(alias)) |_| { + // Duplicate exports are an error + Output.panic( + "In generated file, duplicate export \"{s}\"", + .{alias}, + ); + } else { + try p.named_exports.put(p.allocator, alias, .{ .alias_loc = Logger.Loc.Empty, .ref = ref }); + } + } + + pub fn recordExportedBinding(p: *AstBuilder, binding: Binding) void { + switch (binding.data) { + .b_missing => {}, + .b_identifier => |ident| { + p.recordExport(binding.loc, p.symbols.items[ident.ref.innerIndex()].original_name, ident.ref) catch unreachable; + }, + .b_array => |array| { + for (array.items) |prop| { + p.recordExportedBinding(prop.binding); + } + }, + .b_object => |obj| { + for (obj.properties) |prop| { + p.recordExportedBinding(prop.value); + } + }, + } + } + + pub fn ignoreUsage(p: *AstBuilder, ref: Ref) void { + _ = p; + _ = ref; + } + + pub fn panic(p: *AstBuilder, comptime fmt: []const u8, args: anytype) noreturn { + _ = p; + Output.panic(fmt, args); + } + + pub fn @"module.exports"(p: *AstBuilder, loc: Logger.Loc) Expr { + return p.newExpr(E.Dot{ .name = "exports", .name_loc = loc, .target = p.newExpr(E.Identifier{ .ref = p.module_ref }) }); + } +}; + +const bun = @import("bun"); +const string = bun.string; +const Output = bun.Output; +const strings = bun.strings; + +const std = @import("std"); +const Logger = @import("../logger.zig"); +const options = @import("../options.zig"); +const js_parser = bun.js_parser; +const Part = js_ast.Part; +const js_ast = @import("../js_ast.zig"); +pub const Ref = @import("../ast/base.zig").Ref; +const BabyList = @import("../baby_list.zig").BabyList; +const ImportRecord = bun.ImportRecord; +const ImportKind = bun.ImportKind; + +pub const Index = @import("../ast/base.zig").Index; +const Symbol = js_ast.Symbol; +const Stmt = js_ast.Stmt; +const Expr = js_ast.Expr; +const E = js_ast.E; +const S = js_ast.S; +const Binding = js_ast.Binding; +const renamer = bun.renamer; +const Scope = js_ast.Scope; +const Loc = Logger.Loc; + +pub const DeferredBatchTask = bun.bundle_v2.DeferredBatchTask; +pub const ThreadPool = bun.bundle_v2.ThreadPool; +pub const ParseTask = bun.bundle_v2.ParseTask; diff --git a/src/bundler/BundleThread.zig b/src/bundler/BundleThread.zig new file mode 100644 index 0000000000..49ae6044b9 --- /dev/null +++ b/src/bundler/BundleThread.zig @@ -0,0 +1,190 @@ +/// Used to keep the bundle thread from spinning on Windows +pub fn timerCallback(_: *bun.windows.libuv.Timer) callconv(.C) void {} + +/// Originally, bake.DevServer required a separate bundling thread, but that was +/// later removed. The bundling thread's scheduling logic is generalized over +/// the completion structure. +/// +/// CompletionStruct's interface: +/// +/// - `configureBundler` is used to configure `Bundler`. +/// - `completeOnBundleThread` is used to tell the task that it is done. +pub fn BundleThread(CompletionStruct: type) type { + return struct { + const Self = @This(); + + waker: bun.Async.Waker, + ready_event: std.Thread.ResetEvent, + queue: bun.UnboundedQueue(CompletionStruct, .next), + generation: bun.Generation = 0, + + /// To initialize, put this somewhere in memory, and then call `spawn()` + pub const uninitialized: Self = .{ + .waker = undefined, + .queue = .{}, + .generation = 0, + .ready_event = .{}, + }; + + pub fn spawn(instance: *Self) !std.Thread { + const thread = try std.Thread.spawn(.{}, threadMain, .{instance}); + instance.ready_event.wait(); + return thread; + } + + /// Lazily-initialized singleton. This is used for `Bun.build` since the + /// bundle thread may not be needed. + pub const singleton = struct { + var once = std.once(loadOnceImpl); + var instance: ?*Self = null; + + // Blocks the calling thread until the bun build thread is created. + // std.once also blocks other callers of this function until the first caller is done. + fn loadOnceImpl() void { + const bundle_thread = bun.default_allocator.create(Self) catch bun.outOfMemory(); + bundle_thread.* = uninitialized; + instance = bundle_thread; + + // 2. Spawn the bun build thread. + const os_thread = bundle_thread.spawn() catch + Output.panic("Failed to spawn bun build thread", .{}); + os_thread.detach(); + } + + pub fn get() *Self { + once.call(); + return instance.?; + } + + pub fn enqueue(completion: *CompletionStruct) void { + get().enqueue(completion); + } + }; + + pub fn enqueue(instance: *Self, completion: *CompletionStruct) void { + instance.queue.push(completion); + instance.waker.wake(); + } + + fn threadMain(instance: *Self) void { + Output.Source.configureNamedThread("Bundler"); + + instance.waker = bun.Async.Waker.init() catch @panic("Failed to create waker"); + + // Unblock the calling thread so it can continue. + instance.ready_event.set(); + + var timer: bun.windows.libuv.Timer = undefined; + if (bun.Environment.isWindows) { + timer.init(instance.waker.loop.uv_loop); + timer.start(std.math.maxInt(u64), std.math.maxInt(u64), &timerCallback); + } + + var has_bundled = false; + while (true) { + while (instance.queue.pop()) |completion| { + generateInNewThread(completion, instance.generation) catch |err| { + completion.result = .{ .err = err }; + completion.completeOnBundleThread(); + }; + has_bundled = true; + } + instance.generation +|= 1; + + if (has_bundled) { + bun.Mimalloc.mi_collect(false); + has_bundled = false; + } + + _ = instance.waker.wait(); + } + } + + /// This is called from `Bun.build` in JavaScript. + fn generateInNewThread(completion: *CompletionStruct, generation: bun.Generation) !void { + var heap = try ThreadlocalArena.init(); + defer heap.deinit(); + + const allocator = heap.allocator(); + var ast_memory_allocator = try allocator.create(js_ast.ASTMemoryAllocator); + ast_memory_allocator.* = .{ .allocator = allocator }; + ast_memory_allocator.reset(); + ast_memory_allocator.push(); + + const transpiler = try allocator.create(bun.Transpiler); + + try completion.configureBundler(transpiler, allocator); + + transpiler.resolver.generation = generation; + + const this = try BundleV2.init( + transpiler, + null, // TODO: Kit + allocator, + JSC.AnyEventLoop.init(allocator), + false, + JSC.WorkPool.get(), + heap, + ); + + this.plugins = completion.plugins; + this.completion = switch (CompletionStruct) { + BundleV2.JSBundleCompletionTask => completion, + else => @compileError("Unknown completion struct: " ++ CompletionStruct), + }; + completion.transpiler = this; + + defer { + this.graph.pool.reset(); + ast_memory_allocator.pop(); + this.deinitWithoutFreeingArena(); + } + + errdefer { + // Wait for wait groups to finish. There still may be ongoing work. + this.linker.source_maps.line_offset_wait_group.wait(); + this.linker.source_maps.quoted_contents_wait_group.wait(); + + var out_log = Logger.Log.init(bun.default_allocator); + this.transpiler.log.appendToWithRecycled(&out_log, true) catch bun.outOfMemory(); + completion.log = out_log; + } + + completion.result = .{ .value = .{ + .output_files = try this.runFromJSInNewThread(transpiler.options.entry_points), + } }; + + var out_log = Logger.Log.init(bun.default_allocator); + this.transpiler.log.appendToWithRecycled(&out_log, true) catch bun.outOfMemory(); + completion.log = out_log; + completion.completeOnBundleThread(); + } + }; +} + +const Transpiler = bun.Transpiler; +const bun = @import("bun"); +const Output = bun.Output; +const Environment = bun.Environment; +const default_allocator = bun.default_allocator; + +const std = @import("std"); +const Logger = @import("../logger.zig"); +const options = @import("../options.zig"); +const js_ast = @import("../js_ast.zig"); +const linker = @import("../linker.zig"); +pub const Ref = @import("../ast/base.zig").Ref; +const ThreadlocalArena = @import("../allocators/mimalloc_arena.zig").Arena; +const allocators = @import("../allocators.zig"); +const Timer = @import("../system_timer.zig"); + +pub const Index = @import("../ast/base.zig").Index; +const JSC = bun.JSC; +const Async = bun.Async; +const bake = bun.bake; +const bundler = bun.bundle_v2; +const BundleV2 = bundler.BundleV2; + +pub const DeferredBatchTask = bun.bundle_v2.DeferredBatchTask; +pub const ThreadPool = bun.bundle_v2.ThreadPool; +pub const ParseTask = bun.bundle_v2.ParseTask; diff --git a/src/bundler/Chunk.zig b/src/bundler/Chunk.zig new file mode 100644 index 0000000000..97388a8f5c --- /dev/null +++ b/src/bundler/Chunk.zig @@ -0,0 +1,620 @@ +pub const ChunkImport = struct { + chunk_index: u32, + import_kind: ImportKind, +}; + +pub const Chunk = struct { + /// This is a random string and is used to represent the output path of this + /// chunk before the final output path has been computed. See OutputPiece + /// for more info on this technique. + unique_key: string = "", + + files_with_parts_in_chunk: std.AutoArrayHashMapUnmanaged(Index.Int, void) = .{}, + + /// We must not keep pointers to this type until all chunks have been allocated. + entry_bits: AutoBitSet = undefined, + + final_rel_path: string = "", + /// The path template used to generate `final_rel_path` + template: PathTemplate = .{}, + + /// For code splitting + cross_chunk_imports: BabyList(ChunkImport) = .{}, + + content: Content, + + entry_point: Chunk.EntryPoint = .{}, + + is_executable: bool = false, + has_html_chunk: bool = false, + + output_source_map: sourcemap.SourceMapPieces, + + intermediate_output: IntermediateOutput = .{ .empty = {} }, + isolated_hash: u64 = std.math.maxInt(u64), + + renamer: renamer.Renamer = undefined, + + compile_results_for_chunk: []CompileResult = &.{}, + + pub inline fn isEntryPoint(this: *const Chunk) bool { + return this.entry_point.is_entry_point; + } + + pub fn getJSChunkForHTML(this: *const Chunk, chunks: []Chunk) ?*Chunk { + const entry_point_id = this.entry_point.entry_point_id; + for (chunks) |*other| { + if (other.content == .javascript) { + if (other.entry_point.entry_point_id == entry_point_id) { + return other; + } + } + } + return null; + } + + pub fn getCSSChunkForHTML(this: *const Chunk, chunks: []Chunk) ?*Chunk { + const entry_point_id = this.entry_point.entry_point_id; + for (chunks) |*other| { + if (other.content == .css) { + if (other.entry_point.entry_point_id == entry_point_id) { + return other; + } + } + } + return null; + } + + pub inline fn entryBits(this: *const Chunk) *const AutoBitSet { + return &this.entry_bits; + } + + pub const Order = struct { + source_index: Index.Int = 0, + distance: u32 = 0, + tie_breaker: u32 = 0, + + pub fn lessThan(_: @This(), a: Order, b: Order) bool { + return (a.distance < b.distance) or + (a.distance == b.distance and a.tie_breaker < b.tie_breaker); + } + + /// Sort so files closest to an entry point come first. If two files are + /// equidistant to an entry point, then break the tie by sorting on the + /// stable source index derived from the DFS over all entry points. + pub fn sort(a: []Order) void { + std.sort.pdq(Order, a, Order{}, lessThan); + } + }; + + /// TODO: rewrite this + /// This implementation is just slow. + /// Can we make the JSPrinter itself track this without increasing + /// complexity a lot? + pub const IntermediateOutput = union(enum) { + /// If the chunk has references to other chunks, then "pieces" contains + /// the contents of the chunk. Another joiner will have to be + /// constructed later when merging the pieces together. + /// + /// See OutputPiece's documentation comment for more details. + pieces: bun.BabyList(OutputPiece), + + /// If the chunk doesn't have any references to other chunks, then + /// `joiner` contains the contents of the chunk. This is more efficient + /// because it avoids doing a join operation twice. + joiner: StringJoiner, + + empty: void, + + pub fn allocatorForSize(size: usize) std.mem.Allocator { + if (size >= 512 * 1024) + return std.heap.page_allocator + else + return bun.default_allocator; + } + + pub const CodeResult = struct { + buffer: []u8, + shifts: []sourcemap.SourceMapShifts, + }; + + pub fn code( + this: *IntermediateOutput, + allocator_to_use: ?std.mem.Allocator, + parse_graph: *const Graph, + linker_graph: *const LinkerGraph, + import_prefix: []const u8, + chunk: *Chunk, + chunks: []Chunk, + display_size: ?*usize, + enable_source_map_shifts: bool, + ) !CodeResult { + return switch (enable_source_map_shifts) { + inline else => |source_map_shifts| this.codeWithSourceMapShifts( + allocator_to_use, + parse_graph, + linker_graph, + import_prefix, + chunk, + chunks, + display_size, + source_map_shifts, + ), + }; + } + + pub fn codeWithSourceMapShifts( + this: *IntermediateOutput, + allocator_to_use: ?std.mem.Allocator, + graph: *const Graph, + linker_graph: *const LinkerGraph, + import_prefix: []const u8, + chunk: *Chunk, + chunks: []Chunk, + display_size: ?*usize, + comptime enable_source_map_shifts: bool, + ) !CodeResult { + const additional_files = graph.input_files.items(.additional_files); + const unique_key_for_additional_files = graph.input_files.items(.unique_key_for_additional_file); + switch (this.*) { + .pieces => |*pieces| { + const entry_point_chunks_for_scb = linker_graph.files.items(.entry_point_chunk_index); + + var shift = if (enable_source_map_shifts) + sourcemap.SourceMapShifts{ + .after = .{}, + .before = .{}, + }; + var shifts = if (enable_source_map_shifts) + try std.ArrayList(sourcemap.SourceMapShifts).initCapacity(bun.default_allocator, pieces.len + 1); + + if (enable_source_map_shifts) + shifts.appendAssumeCapacity(shift); + + var count: usize = 0; + var from_chunk_dir = std.fs.path.dirnamePosix(chunk.final_rel_path) orelse ""; + if (strings.eqlComptime(from_chunk_dir, ".")) + from_chunk_dir = ""; + + for (pieces.slice()) |piece| { + count += piece.data_len; + + switch (piece.query.kind) { + .chunk, .asset, .scb => { + const index = piece.query.index; + const file_path = switch (piece.query.kind) { + .asset => brk: { + const files = additional_files[index]; + if (!(files.len > 0)) { + Output.panic("Internal error: missing asset file", .{}); + } + + const output_file = files.last().?.output_file; + + break :brk graph.additional_output_files.items[output_file].dest_path; + }, + .chunk => chunks[index].final_rel_path, + .scb => chunks[entry_point_chunks_for_scb[index]].final_rel_path, + .none => unreachable, + }; + + const cheap_normalizer = cheapPrefixNormalizer( + import_prefix, + if (from_chunk_dir.len == 0) + file_path + else + bun.path.relativePlatform(from_chunk_dir, file_path, .posix, false), + ); + count += cheap_normalizer[0].len + cheap_normalizer[1].len; + }, + .none => {}, + } + } + + if (display_size) |amt| { + amt.* = count; + } + + const debug_id_len = if (enable_source_map_shifts and FeatureFlags.source_map_debug_id) + std.fmt.count("\n//# debugId={}\n", .{bun.sourcemap.DebugIDFormatter{ .id = chunk.isolated_hash }}) + else + 0; + + const total_buf = try (allocator_to_use orelse allocatorForSize(count)).alloc(u8, count + debug_id_len); + var remain = total_buf; + + for (pieces.slice()) |piece| { + const data = piece.data(); + + if (enable_source_map_shifts) { + var data_offset = sourcemap.LineColumnOffset{}; + data_offset.advance(data); + shift.before.add(data_offset); + shift.after.add(data_offset); + } + + if (data.len > 0) + @memcpy(remain[0..data.len], data); + + remain = remain[data.len..]; + + switch (piece.query.kind) { + .asset, .chunk, .scb => { + const index = piece.query.index; + const file_path = switch (piece.query.kind) { + .asset => brk: { + const files = additional_files[index]; + bun.assert(files.len > 0); + + const output_file = files.last().?.output_file; + + if (enable_source_map_shifts) { + shift.before.advance(unique_key_for_additional_files[index]); + } + + break :brk graph.additional_output_files.items[output_file].dest_path; + }, + .chunk => brk: { + const piece_chunk = chunks[index]; + + if (enable_source_map_shifts) { + shift.before.advance(piece_chunk.unique_key); + } + + break :brk piece_chunk.final_rel_path; + }, + .scb => brk: { + const piece_chunk = chunks[entry_point_chunks_for_scb[index]]; + + if (enable_source_map_shifts) { + shift.before.advance(piece_chunk.unique_key); + } + + break :brk piece_chunk.final_rel_path; + }, + else => unreachable, + }; + + // normalize windows paths to '/' + bun.path.platformToPosixInPlace(u8, @constCast(file_path)); + const cheap_normalizer = cheapPrefixNormalizer( + import_prefix, + if (from_chunk_dir.len == 0) + file_path + else + bun.path.relativePlatform(from_chunk_dir, file_path, .posix, false), + ); + + if (cheap_normalizer[0].len > 0) { + @memcpy(remain[0..cheap_normalizer[0].len], cheap_normalizer[0]); + remain = remain[cheap_normalizer[0].len..]; + if (enable_source_map_shifts) + shift.after.advance(cheap_normalizer[0]); + } + + if (cheap_normalizer[1].len > 0) { + @memcpy(remain[0..cheap_normalizer[1].len], cheap_normalizer[1]); + remain = remain[cheap_normalizer[1].len..]; + if (enable_source_map_shifts) + shift.after.advance(cheap_normalizer[1]); + } + + if (enable_source_map_shifts) + shifts.appendAssumeCapacity(shift); + }, + .none => {}, + } + } + + if (enable_source_map_shifts and FeatureFlags.source_map_debug_id) { + // This comment must go before the //# sourceMappingURL comment + remain = remain[(std.fmt.bufPrint( + remain, + "\n//# debugId={}\n", + .{bun.sourcemap.DebugIDFormatter{ .id = chunk.isolated_hash }}, + ) catch bun.outOfMemory()).len..]; + } + + bun.assert(remain.len == 0); + bun.assert(total_buf.len == count + debug_id_len); + + return .{ + .buffer = total_buf, + .shifts = if (enable_source_map_shifts) + shifts.items + else + &[_]sourcemap.SourceMapShifts{}, + }; + }, + .joiner => |*joiner| { + const allocator = allocator_to_use orelse allocatorForSize(joiner.len); + + if (display_size) |amt| { + amt.* = joiner.len; + } + + const buffer = brk: { + if (enable_source_map_shifts and FeatureFlags.source_map_debug_id) { + // This comment must go before the //# sourceMappingURL comment + const debug_id_fmt = std.fmt.allocPrint( + graph.allocator, + "\n//# debugId={}\n", + .{bun.sourcemap.DebugIDFormatter{ .id = chunk.isolated_hash }}, + ) catch bun.outOfMemory(); + + break :brk try joiner.doneWithEnd(allocator, debug_id_fmt); + } + + break :brk try joiner.done(allocator); + }; + + return .{ + .buffer = buffer, + .shifts = &[_]sourcemap.SourceMapShifts{}, + }; + }, + .empty => return .{ + .buffer = "", + .shifts = &[_]sourcemap.SourceMapShifts{}, + }, + } + } + }; + + /// An issue with asset files and server component boundaries is they + /// contain references to output paths, but those paths are not known until + /// very late in the bundle. The solution is to have a magic word in the + /// bundle text (BundleV2.unique_key, a random u64; impossible to guess). + /// When a file wants a path to an emitted chunk, it emits the unique key + /// in hex followed by the kind of path it wants: + /// + /// `74f92237f4a85a6aA00000009` --> `./some-asset.png` + /// ^--------------^|^------- .query.index + /// unique_key .query.kind + /// + /// An output piece is the concatenation of source code text and an output + /// path, in that order. An array of pieces makes up an entire file. + pub const OutputPiece = struct { + /// Pointer and length split to reduce struct size + data_ptr: [*]const u8, + data_len: u32, + query: Query, + + pub fn data(this: OutputPiece) []const u8 { + return this.data_ptr[0..this.data_len]; + } + + pub const Query = packed struct(u32) { + index: u30, + kind: Kind, + + pub const Kind = enum(u2) { + /// The last piece in an array uses this to indicate it is just data + none, + /// Given a source index, print the asset's output + asset, + /// Given a chunk index, print the chunk's output path + chunk, + /// Given a server component boundary index, print the chunk's output path + scb, + }; + + pub const none: Query = .{ .index = 0, .kind = .none }; + }; + + pub fn init(data_slice: []const u8, query: Query) OutputPiece { + return .{ + .data_ptr = data_slice.ptr, + .data_len = @intCast(data_slice.len), + .query = query, + }; + } + }; + + pub const OutputPieceIndex = OutputPiece.Query; + + pub const EntryPoint = packed struct(u64) { + /// Index into `Graph.input_files` + source_index: u32 = 0, + entry_point_id: ID = 0, + is_entry_point: bool = false, + is_html: bool = false, + + /// so `EntryPoint` can be a u64 + pub const ID = u30; + }; + + pub const JavaScriptChunk = struct { + files_in_chunk_order: []const Index.Int = &.{}, + parts_in_chunk_in_order: []const PartRange = &.{}, + + // for code splitting + exports_to_other_chunks: std.ArrayHashMapUnmanaged(Ref, string, Ref.ArrayHashCtx, false) = .{}, + imports_from_other_chunks: ImportsFromOtherChunks = .{}, + cross_chunk_prefix_stmts: BabyList(Stmt) = .{}, + cross_chunk_suffix_stmts: BabyList(Stmt) = .{}, + + /// Indexes to CSS chunks. Currently this will only ever be zero or one + /// items long, but smarter css chunking will allow multiple js entry points + /// share a css file, or have an entry point contain multiple css files. + /// + /// Mutated while sorting chunks in `computeChunks` + css_chunks: []u32 = &.{}, + }; + + pub const CssChunk = struct { + imports_in_chunk_in_order: BabyList(CssImportOrder), + /// When creating a chunk, this is to be an uninitialized slice with + /// length of `imports_in_chunk_in_order` + /// + /// Multiple imports may refer to the same file/stylesheet, but may need to + /// wrap them in conditions (e.g. a layer). + /// + /// When we go through the `prepareCssAstsForChunk()` step, each import will + /// create a shallow copy of the file's AST (just dereferencing the pointer). + asts: []bun.css.BundlerStyleSheet, + }; + + const CssImportKind = enum { + source_index, + external_path, + import_layers, + }; + + pub const CssImportOrder = struct { + conditions: BabyList(bun.css.ImportConditions) = .{}, + condition_import_records: BabyList(ImportRecord) = .{}, + + kind: union(enum) { + /// Represents earlier imports that have been made redundant by later ones (see `isConditionalImportRedundant`) + /// We don't want to redundantly print the rules of these redundant imports + /// BUT, the imports may include layers. + /// We'll just print layer name declarations so that the original ordering is preserved. + layers: Layers, + external_path: bun.fs.Path, + source_index: Index, + }, + + pub const Layers = bun.ptr.Cow(bun.BabyList(bun.css.LayerName), struct { + const Self = bun.BabyList(bun.css.LayerName); + pub fn copy(self: *const Self, allocator: std.mem.Allocator) Self { + return self.deepClone2(allocator); + } + + pub fn deinit(self: *Self, a: std.mem.Allocator) void { + // do shallow deinit since `LayerName` has + // allocations in arena + self.deinitWithAllocator(a); + } + }); + + pub fn hash(this: *const CssImportOrder, hasher: anytype) void { + // TODO: conditions, condition_import_records + + bun.writeAnyToHasher(hasher, std.meta.activeTag(this.kind)); + switch (this.kind) { + .layers => |layers| { + for (layers.inner().sliceConst()) |layer| { + for (layer.v.slice(), 0..) |layer_name, i| { + const is_last = i == layers.inner().len - 1; + if (is_last) { + hasher.update(layer_name); + } else { + hasher.update(layer_name); + hasher.update("."); + } + } + } + hasher.update("\x00"); + }, + .external_path => |path| hasher.update(path.text), + .source_index => |idx| bun.writeAnyToHasher(hasher, idx), + } + } + + pub fn fmt(this: *const CssImportOrder, ctx: *LinkerContext) CssImportOrderDebug { + return .{ + .inner = this, + .ctx = ctx, + }; + } + + pub const CssImportOrderDebug = struct { + inner: *const CssImportOrder, + ctx: *LinkerContext, + + pub fn format(this: *const CssImportOrderDebug, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + try writer.print("{s} = ", .{@tagName(this.inner.kind)}); + switch (this.inner.kind) { + .layers => |layers| { + try writer.print("[", .{}); + const l = layers.inner(); + for (l.sliceConst(), 0..) |*layer, i| { + if (i > 0) try writer.print(", ", .{}); + try writer.print("\"{}\"", .{layer}); + } + + try writer.print("]", .{}); + }, + .external_path => |path| { + try writer.print("\"{s}\"", .{path.pretty}); + }, + .source_index => |source_index| { + const source = this.ctx.parse_graph.input_files.items(.source)[source_index.get()]; + try writer.print("{d} ({s})", .{ source_index.get(), source.path.text }); + }, + } + } + }; + }; + + pub const ImportsFromOtherChunks = std.AutoArrayHashMapUnmanaged(Index.Int, CrossChunkImport.Item.List); + + pub const Content = union(enum) { + javascript: JavaScriptChunk, + css: CssChunk, + html, + + pub fn sourcemap(this: *const Content, default: options.SourceMapOption) options.SourceMapOption { + return switch (this.*) { + .javascript => default, + .css => .none, // TODO: css source maps + .html => .none, + }; + } + + pub fn loader(this: *const Content) Loader { + return switch (this.*) { + .javascript => .js, + .css => .css, + .html => .html, + }; + } + + pub fn ext(this: *const Content) string { + return switch (this.*) { + .javascript => "js", + .css => "css", + .html => "html", + }; + } + }; +}; + +const bun = @import("bun"); +const string = bun.string; +const Output = bun.Output; +const strings = bun.strings; +const default_allocator = bun.default_allocator; +const FeatureFlags = bun.FeatureFlags; + +const std = @import("std"); +const options = @import("../options.zig"); +const js_ast = @import("../js_ast.zig"); +const sourcemap = bun.sourcemap; +const StringJoiner = bun.StringJoiner; +pub const Ref = @import("../ast/base.zig").Ref; +const BabyList = @import("../baby_list.zig").BabyList; +const ImportRecord = bun.ImportRecord; +const ImportKind = bun.ImportKind; + +const Loader = options.Loader; +pub const Index = @import("../ast/base.zig").Index; +const Stmt = js_ast.Stmt; +const AutoBitSet = bun.bit_set.AutoBitSet; +const renamer = bun.renamer; +const bundler = bun.bundle_v2; +const BundleV2 = bundler.BundleV2; +const Graph = bundler.Graph; +const LinkerGraph = bundler.LinkerGraph; + +pub const DeferredBatchTask = bun.bundle_v2.DeferredBatchTask; +pub const ThreadPool = bun.bundle_v2.ThreadPool; +pub const ParseTask = bun.bundle_v2.ParseTask; +const PathTemplate = bundler.PathTemplate; +const PartRange = bundler.PartRange; +const EntryPoint = bundler.EntryPoint; +const CrossChunkImport = bundler.CrossChunkImport; +const CompileResult = bundler.CompileResult; +const cheapPrefixNormalizer = bundler.cheapPrefixNormalizer; +const LinkerContext = bundler.LinkerContext; diff --git a/src/bundler/DeferredBatchTask.zig b/src/bundler/DeferredBatchTask.zig new file mode 100644 index 0000000000..165292262c --- /dev/null +++ b/src/bundler/DeferredBatchTask.zig @@ -0,0 +1,52 @@ +/// This task is run once all parse and resolve tasks have been complete +/// and we have deferred onLoad plugins that we need to resume +/// +/// It enqueues a task to be run on the JS thread which resolves the promise +/// for every onLoad callback which called `.defer()`. +pub const DeferredBatchTask = @This(); + +running: if (Environment.isDebug) bool else u0 = if (Environment.isDebug) false else 0, + +pub fn init(this: *DeferredBatchTask) void { + if (comptime Environment.isDebug) bun.debugAssert(!this.running); + this.* = .{ + .running = if (comptime Environment.isDebug) false else 0, + }; +} + +pub fn getBundleV2(this: *DeferredBatchTask) *bun.BundleV2 { + return @alignCast(@fieldParentPtr("drain_defer_task", this)); +} + +pub fn schedule(this: *DeferredBatchTask) void { + if (comptime Environment.isDebug) { + bun.assert(!this.running); + this.running = false; + } + this.getBundleV2().jsLoopForPlugins().enqueueTaskConcurrent(JSC.ConcurrentTask.create(JSC.Task.init(this))); +} + +pub fn deinit(this: *DeferredBatchTask) void { + if (comptime Environment.isDebug) { + this.running = false; + } +} + +pub fn runOnJSThread(this: *DeferredBatchTask) void { + defer this.deinit(); + var bv2 = this.getBundleV2(); + bv2.plugins.?.drainDeferred( + if (bv2.completion) |completion| + completion.result == .err + else + false, + ); +} + +const bun = @import("bun"); +const Environment = bun.Environment; + +pub const Ref = @import("../ast/base.zig").Ref; + +pub const Index = @import("../ast/base.zig").Index; +const JSC = bun.JSC; diff --git a/src/bundler/Graph.zig b/src/bundler/Graph.zig new file mode 100644 index 0000000000..cb4e9cebe1 --- /dev/null +++ b/src/bundler/Graph.zig @@ -0,0 +1,128 @@ +pub const Graph = @This(); + +pool: *ThreadPool, +heap: ThreadlocalArena = .{}, +/// This allocator is thread-local to the Bundler thread +/// .allocator == .heap.allocator() +allocator: std.mem.Allocator = undefined, + +/// Mapping user-specified entry points to their Source Index +entry_points: std.ArrayListUnmanaged(Index) = .{}, +/// Every source index has an associated InputFile +input_files: MultiArrayList(InputFile) = .{}, +/// Every source index has an associated Ast +/// When a parse is in progress / queued, it is `Ast.empty` +ast: MultiArrayList(JSAst) = .{}, + +/// During the scan + parse phase, this value keeps a count of the remaining +/// tasks. Once it hits zero, the scan phase ends and linking begins. Note +/// that if `deferred_pending > 0`, it means there are plugin callbacks +/// to invoke before linking, which can initiate another scan phase. +/// +/// Increment and decrement this via `incrementScanCounter` and +/// `decrementScanCounter`, as asynchronous bundles check for `0` in the +/// decrement function, instead of at the top of the event loop. +/// +/// - Parsing a file (ParseTask and ServerComponentParseTask) +/// - onResolve and onLoad functions +/// - Resolving an onDefer promise +pending_items: u32 = 0, +/// When an `onLoad` plugin calls `.defer()`, the count from `pending_items` +/// is "moved" into this counter (pending_items -= 1; deferred_pending += 1) +/// +/// When `pending_items` hits zero and there are deferred pending tasks, those +/// tasks will be run, and the count is "moved" back to `pending_items` +deferred_pending: u32 = 0, + +/// Maps a hashed path string to a source index, if it exists in the compilation. +/// Instead of accessing this directly, consider using BundleV2.pathToSourceIndexMap +path_to_source_index_map: PathToSourceIndexMap = .{}, +/// When using server components, a completely separate file listing is +/// required to avoid incorrect inlining of defines and dependencies on +/// other files. This is relevant for files shared between server and client +/// and have no "use " directive, and must be duplicated. +/// +/// To make linking easier, this second graph contains indices into the +/// same `.ast` and `.input_files` arrays. +client_path_to_source_index_map: PathToSourceIndexMap = .{}, +/// When using server components with React, there is an additional module +/// graph which is used to contain SSR-versions of all client components; +/// the SSR graph. The difference between the SSR graph and the server +/// graph is that this one does not apply '--conditions react-server' +/// +/// In Bun's React Framework, it includes SSR versions of 'react' and +/// 'react-dom' (an export condition is used to provide a different +/// implementation for RSC, which is potentially how they implement +/// server-only features such as async components). +ssr_path_to_source_index_map: PathToSourceIndexMap = .{}, + +/// When Server Components is enabled, this holds a list of all boundary +/// files. This happens for all files with a "use " directive. +server_component_boundaries: ServerComponentBoundary.List = .{}, + +estimated_file_loader_count: usize = 0, + +/// For Bake, a count of the CSS asts is used to make precise +/// pre-allocations without re-iterating the file listing. +css_file_count: usize = 0, + +additional_output_files: std.ArrayListUnmanaged(options.OutputFile) = .{}, + +kit_referenced_server_data: bool, +kit_referenced_client_data: bool, + +pub const InputFile = struct { + source: Logger.Source, + loader: options.Loader = options.Loader.file, + side_effects: _resolver.SideEffects, + allocator: std.mem.Allocator = bun.default_allocator, + additional_files: BabyList(AdditionalFile) = .{}, + unique_key_for_additional_file: string = "", + content_hash_for_additional_file: u64 = 0, + is_plugin_file: bool = false, +}; + +/// Schedule a task to be run on the JS thread which resolves the promise of +/// each `.defer()` called in an onLoad plugin. +/// +/// Returns true if there were more tasks queued. +pub fn drainDeferredTasks(this: *@This(), transpiler: *BundleV2) bool { + transpiler.thread_lock.assertLocked(); + + if (this.deferred_pending > 0) { + this.pending_items += this.deferred_pending; + this.deferred_pending = 0; + + transpiler.drain_defer_task.init(); + transpiler.drain_defer_task.schedule(); + + return true; + } + + return false; +} + +const bun = @import("bun"); +const string = bun.string; +const default_allocator = bun.default_allocator; + +const std = @import("std"); +const Logger = @import("../logger.zig"); +const options = @import("../options.zig"); +const js_ast = @import("../js_ast.zig"); +pub const Ref = @import("../ast/base.zig").Ref; +const ThreadlocalArena = @import("../allocators/mimalloc_arena.zig").Arena; +const BabyList = @import("../baby_list.zig").BabyList; +const _resolver = @import("../resolver/resolver.zig"); +const allocators = @import("../allocators.zig"); + +const JSAst = js_ast.BundledAst; +const Loader = options.Loader; +pub const Index = @import("../ast/base.zig").Index; +const MultiArrayList = bun.MultiArrayList; +const ThreadPool = bun.bundle_v2.ThreadPool; +const ParseTask = bun.bundle_v2.ParseTask; +const PathToSourceIndexMap = bun.bundle_v2.PathToSourceIndexMap; +const ServerComponentBoundary = js_ast.ServerComponentBoundary; +const BundleV2 = bun.bundle_v2.BundleV2; +const AdditionalFile = bun.bundle_v2.AdditionalFile; diff --git a/src/bundler/LinkerContext.zig b/src/bundler/LinkerContext.zig new file mode 100644 index 0000000000..8f0ec14cb5 --- /dev/null +++ b/src/bundler/LinkerContext.zig @@ -0,0 +1,2479 @@ +pub const LinkerContext = struct { + pub const debug = Output.scoped(.LinkerCtx, false); + pub const CompileResult = bundler.CompileResult; + + parse_graph: *Graph = undefined, + graph: LinkerGraph = undefined, + allocator: std.mem.Allocator = undefined, + log: *Logger.Log = undefined, + + resolver: *Resolver = undefined, + cycle_detector: std.ArrayList(ImportTracker) = undefined, + + /// We may need to refer to the "__esm" and/or "__commonJS" runtime symbols + cjs_runtime_ref: Ref = Ref.None, + esm_runtime_ref: Ref = Ref.None, + + /// We may need to refer to the CommonJS "module" symbol for exports + unbound_module_ref: Ref = Ref.None, + + options: LinkerOptions = .{}, + + wait_group: ThreadPoolLib.WaitGroup = .{}, + + ambiguous_result_pool: std.ArrayList(MatchImport) = undefined, + + loop: EventLoop, + + /// string buffer containing pre-formatted unique keys + unique_key_buf: []u8 = "", + + /// string buffer containing prefix for each unique keys + unique_key_prefix: string = "", + + source_maps: SourceMapData = .{}, + + /// This will eventually be used for reference-counting LinkerContext + /// to know whether or not we can free it safely. + pending_task_count: std.atomic.Value(u32) = std.atomic.Value(u32).init(0), + + /// + has_any_css_locals: std.atomic.Value(u32) = std.atomic.Value(u32).init(0), + + /// Used by Bake to extract []CompileResult before it is joined + dev_server: ?*bun.bake.DevServer = null, + framework: ?*const bake.Framework = null, + + mangled_props: MangledProps = .{}, + + pub fn pathWithPrettyInitialized(this: *LinkerContext, path: Fs.Path) !Fs.Path { + return bundler.genericPathWithPrettyInitialized(path, this.options.target, this.resolver.fs.top_level_dir, this.graph.allocator); + } + + pub const LinkerOptions = struct { + generate_bytecode_cache: bool = false, + output_format: options.Format = .esm, + ignore_dce_annotations: bool = false, + emit_dce_annotations: bool = true, + tree_shaking: bool = true, + minify_whitespace: bool = false, + minify_syntax: bool = false, + minify_identifiers: bool = false, + banner: []const u8 = "", + footer: []const u8 = "", + css_chunking: bool = false, + source_maps: options.SourceMapOption = .none, + target: options.Target = .browser, + + mode: Mode = .bundle, + + public_path: []const u8 = "", + + pub const Mode = enum { + passthrough, + bundle, + }; + }; + + pub const SourceMapData = struct { + line_offset_wait_group: sync.WaitGroup = .{}, + line_offset_tasks: []Task = &.{}, + + quoted_contents_wait_group: sync.WaitGroup = .{}, + quoted_contents_tasks: []Task = &.{}, + + pub const Task = struct { + ctx: *LinkerContext, + source_index: Index.Int, + thread_task: ThreadPoolLib.Task = .{ .callback = &runLineOffset }, + + pub fn runLineOffset(thread_task: *ThreadPoolLib.Task) void { + var task: *Task = @fieldParentPtr("thread_task", thread_task); + defer { + task.ctx.markPendingTaskDone(); + task.ctx.source_maps.line_offset_wait_group.finish(); + } + + const worker = ThreadPool.Worker.get(@fieldParentPtr("linker", task.ctx)); + defer worker.unget(); + SourceMapData.computeLineOffsets(task.ctx, worker.allocator, task.source_index); + } + + pub fn runQuotedSourceContents(thread_task: *ThreadPoolLib.Task) void { + var task: *Task = @fieldParentPtr("thread_task", thread_task); + defer { + task.ctx.markPendingTaskDone(); + task.ctx.source_maps.quoted_contents_wait_group.finish(); + } + + const worker = ThreadPool.Worker.get(@fieldParentPtr("linker", task.ctx)); + defer worker.unget(); + + // Use the default allocator when using DevServer and the file + // was generated. This will be preserved so that remapping + // stack traces can show the source code, even after incremental + // rebuilds occur. + const allocator = if (worker.ctx.transpiler.options.dev_server) |dev| + dev.allocator + else + worker.allocator; + + SourceMapData.computeQuotedSourceContents(task.ctx, allocator, task.source_index); + } + }; + + pub fn computeLineOffsets(this: *LinkerContext, allocator: std.mem.Allocator, source_index: Index.Int) void { + debug("Computing LineOffsetTable: {d}", .{source_index}); + const line_offset_table: *bun.sourcemap.LineOffsetTable.List = &this.graph.files.items(.line_offset_table)[source_index]; + + const source: *const Logger.Source = &this.parse_graph.input_files.items(.source)[source_index]; + const loader: options.Loader = this.parse_graph.input_files.items(.loader)[source_index]; + + if (!loader.canHaveSourceMap()) { + // This is not a file which we support generating source maps for + line_offset_table.* = .{}; + return; + } + + const approximate_line_count = this.graph.ast.items(.approximate_newline_count)[source_index]; + + line_offset_table.* = bun.sourcemap.LineOffsetTable.generate( + allocator, + source.contents, + + // We don't support sourcemaps for source files with more than 2^31 lines + @as(i32, @intCast(@as(u31, @truncate(approximate_line_count)))), + ); + } + + pub fn computeQuotedSourceContents(this: *LinkerContext, allocator: std.mem.Allocator, source_index: Index.Int) void { + debug("Computing Quoted Source Contents: {d}", .{source_index}); + const loader: options.Loader = this.parse_graph.input_files.items(.loader)[source_index]; + const quoted_source_contents: *string = &this.graph.files.items(.quoted_source_contents)[source_index]; + if (!loader.canHaveSourceMap()) { + quoted_source_contents.* = ""; + return; + } + + const source: *const Logger.Source = &this.parse_graph.input_files.items(.source)[source_index]; + const mutable = MutableString.initEmpty(allocator); + quoted_source_contents.* = (js_printer.quoteForJSON(source.contents, mutable, false) catch bun.outOfMemory()).list.items; + } + }; + + pub fn isExternalDynamicImport(this: *LinkerContext, record: *const ImportRecord, source_index: u32) bool { + return this.graph.code_splitting and + record.kind == .dynamic and + this.graph.files.items(.entry_point_kind)[record.source_index.get()].isEntryPoint() and + record.source_index.get() != source_index; + } + + pub fn shouldIncludePart(c: *LinkerContext, source_index: Index.Int, part: Part) bool { + // As an optimization, ignore parts containing a single import statement to + // an internal non-wrapped file. These will be ignored anyway and it's a + // performance hit to spin up a goroutine only to discover this later. + if (part.stmts.len == 1) { + if (part.stmts[0].data == .s_import) { + const record = c.graph.ast.items(.import_records)[source_index].at(part.stmts[0].data.s_import.import_record_index); + if (record.source_index.isValid() and c.graph.meta.items(.flags)[record.source_index.get()].wrap == .none) { + return false; + } + } + } + + return true; + } + + pub fn load( + this: *LinkerContext, + bundle: *BundleV2, + entry_points: []Index, + server_component_boundaries: ServerComponentBoundary.List, + reachable: []Index, + ) !void { + const trace = bun.perf.trace("Bundler.CloneLinkerGraph"); + defer trace.end(); + this.parse_graph = &bundle.graph; + + this.graph.code_splitting = bundle.transpiler.options.code_splitting; + this.log = bundle.transpiler.log; + + this.resolver = &bundle.transpiler.resolver; + this.cycle_detector = std.ArrayList(ImportTracker).init(this.allocator); + + this.graph.reachable_files = reachable; + + const sources: []const Logger.Source = this.parse_graph.input_files.items(.source); + + try this.graph.load(entry_points, sources, server_component_boundaries, bundle.dynamic_import_entry_points.keys()); + bundle.dynamic_import_entry_points.deinit(); + this.wait_group.init(); + this.ambiguous_result_pool = std.ArrayList(MatchImport).init(this.allocator); + + var runtime_named_exports = &this.graph.ast.items(.named_exports)[Index.runtime.get()]; + + this.esm_runtime_ref = runtime_named_exports.get("__esm").?.ref; + this.cjs_runtime_ref = runtime_named_exports.get("__commonJS").?.ref; + + if (this.options.output_format == .cjs) { + this.unbound_module_ref = this.graph.generateNewSymbol(Index.runtime.get(), .unbound, "module"); + } + + if (this.options.output_format == .cjs or this.options.output_format == .iife) { + const exports_kind = this.graph.ast.items(.exports_kind); + const ast_flags_list = this.graph.ast.items(.flags); + const meta_flags_list = this.graph.meta.items(.flags); + + for (entry_points) |entry_point| { + var ast_flags: js_ast.BundledAst.Flags = ast_flags_list[entry_point.get()]; + + // Loaders default to CommonJS when they are the entry point and the output + // format is not ESM-compatible since that avoids generating the ESM-to-CJS + // machinery. + if (ast_flags.has_lazy_export) { + exports_kind[entry_point.get()] = .cjs; + } + + // Entry points with ES6 exports must generate an exports object when + // targeting non-ES6 formats. Note that the IIFE format only needs this + // when the global name is present, since that's the only way the exports + // can actually be observed externally. + if (ast_flags.uses_export_keyword) { + ast_flags.uses_exports_ref = true; + ast_flags_list[entry_point.get()] = ast_flags; + meta_flags_list[entry_point.get()].force_include_exports_for_entry_point = true; + } + } + } + } + + pub fn computeDataForSourceMap( + this: *LinkerContext, + reachable: []const Index.Int, + ) void { + bun.assert(this.options.source_maps != .none); + this.source_maps.line_offset_wait_group.init(); + this.source_maps.quoted_contents_wait_group.init(); + this.source_maps.line_offset_wait_group.counter = @as(u32, @truncate(reachable.len)); + this.source_maps.quoted_contents_wait_group.counter = @as(u32, @truncate(reachable.len)); + this.source_maps.line_offset_tasks = this.allocator.alloc(SourceMapData.Task, reachable.len) catch unreachable; + this.source_maps.quoted_contents_tasks = this.allocator.alloc(SourceMapData.Task, reachable.len) catch unreachable; + + var batch = ThreadPoolLib.Batch{}; + var second_batch = ThreadPoolLib.Batch{}; + for (reachable, this.source_maps.line_offset_tasks, this.source_maps.quoted_contents_tasks) |source_index, *line_offset, *quoted| { + line_offset.* = .{ + .ctx = this, + .source_index = source_index, + .thread_task = .{ .callback = &SourceMapData.Task.runLineOffset }, + }; + quoted.* = .{ + .ctx = this, + .source_index = source_index, + .thread_task = .{ .callback = &SourceMapData.Task.runQuotedSourceContents }, + }; + batch.push(.from(&line_offset.thread_task)); + second_batch.push(.from("ed.thread_task)); + } + + // line offsets block sooner and are faster to compute, so we should schedule those first + batch.push(second_batch); + + this.scheduleTasks(batch); + } + + pub fn scheduleTasks(this: *LinkerContext, batch: ThreadPoolLib.Batch) void { + _ = this.pending_task_count.fetchAdd(@as(u32, @truncate(batch.len)), .monotonic); + this.parse_graph.pool.worker_pool.schedule(batch); + } + + pub fn markPendingTaskDone(this: *LinkerContext) void { + _ = this.pending_task_count.fetchSub(1, .monotonic); + } + + pub noinline fn link( + this: *LinkerContext, + bundle: *BundleV2, + entry_points: []Index, + server_component_boundaries: ServerComponentBoundary.List, + reachable: []Index, + ) ![]Chunk { + try this.load( + bundle, + entry_points, + server_component_boundaries, + reachable, + ); + + if (this.options.source_maps != .none) { + this.computeDataForSourceMap(@as([]Index.Int, @ptrCast(reachable))); + } + + if (comptime FeatureFlags.help_catch_memory_issues) { + this.checkForMemoryCorruption(); + } + + try this.scanImportsAndExports(); + + // Stop now if there were errors + if (this.log.hasErrors()) { + return error.BuildFailed; + } + + if (comptime FeatureFlags.help_catch_memory_issues) { + this.checkForMemoryCorruption(); + } + + try this.treeShakingAndCodeSplitting(); + + if (comptime FeatureFlags.help_catch_memory_issues) { + this.checkForMemoryCorruption(); + } + + const chunks = try this.computeChunks(bundle.unique_key); + + if (comptime FeatureFlags.help_catch_memory_issues) { + this.checkForMemoryCorruption(); + } + + try this.computeCrossChunkDependencies(chunks); + + if (comptime FeatureFlags.help_catch_memory_issues) { + this.checkForMemoryCorruption(); + } + + this.graph.symbols.followAll(); + + return chunks; + } + + pub fn checkForMemoryCorruption(this: *LinkerContext) void { + // For this to work, you need mimalloc's debug build enabled. + // make mimalloc-debug + this.parse_graph.heap.helpCatchMemoryIssues(); + } + + pub const computeChunks = @import("linker_context/computeChunks.zig").computeChunks; + + pub const findAllImportedPartsInJSOrder = @import("linker_context/findAllImportedPartsInJSOrder.zig").findAllImportedPartsInJSOrder; + pub const findImportedPartsInJSOrder = @import("linker_context/findAllImportedPartsInJSOrder.zig").findImportedPartsInJSOrder; + pub const findImportedFilesInCSSOrder = @import("linker_context/findImportedFilesInCSSOrder.zig").findImportedFilesInCSSOrder; + pub const findImportedCSSFilesInJSOrder = @import("linker_context/findImportedCSSFilesInJSOrder.zig").findImportedCSSFilesInJSOrder; + + pub fn generateNamedExportInFile(this: *LinkerContext, source_index: Index.Int, module_ref: Ref, name: []const u8, alias: []const u8) !struct { Ref, u32 } { + const ref = this.graph.generateNewSymbol(source_index, .other, name); + const part_index = this.graph.addPartToFile(source_index, .{ + .declared_symbols = js_ast.DeclaredSymbol.List.fromSlice( + this.allocator, + &[_]js_ast.DeclaredSymbol{ + .{ .ref = ref, .is_top_level = true }, + }, + ) catch unreachable, + .can_be_removed_if_unused = true, + }) catch unreachable; + + try this.graph.generateSymbolImportAndUse(source_index, part_index, module_ref, 1, Index.init(source_index)); + var top_level = &this.graph.meta.items(.top_level_symbol_to_parts_overlay)[source_index]; + var parts_list = this.allocator.alloc(u32, 1) catch unreachable; + parts_list[0] = part_index; + + top_level.put(this.allocator, ref, BabyList(u32).init(parts_list)) catch unreachable; + + var resolved_exports = &this.graph.meta.items(.resolved_exports)[source_index]; + resolved_exports.put(this.allocator, alias, ExportData{ + .data = ImportTracker{ + .source_index = Index.init(source_index), + .import_ref = ref, + }, + }) catch unreachable; + return .{ ref, part_index }; + } + + pub const generateCodeForLazyExport = @import("linker_context/generateCodeForLazyExport.zig").generateCodeForLazyExport; + pub const scanImportsAndExports = @import("linker_context/scanImportsAndExports.zig").scanImportsAndExports; + pub const doStep5 = @import("linker_context/doStep5.zig").doStep5; + pub const createExportsForFile = @import("linker_context/doStep5.zig").createExportsForFile; + + pub fn scanCSSImports( + this: *LinkerContext, + file_source_index: u32, + file_import_records: []ImportRecord, + // slices from Graph + css_asts: []const ?*bun.css.BundlerStyleSheet, + sources: []const Logger.Source, + loaders: []const Loader, + log: *Logger.Log, + ) enum { ok, errors } { + for (file_import_records) |*record| { + if (record.source_index.isValid()) { + // Other file is not CSS + if (css_asts[record.source_index.get()] == null) { + const source = &sources[file_source_index]; + const loader = loaders[record.source_index.get()]; + + switch (loader) { + .jsx, .js, .ts, .tsx, .napi, .sqlite, .json, .jsonc, .html, .sqlite_embedded => { + log.addErrorFmt( + source, + record.range.loc, + this.allocator, + "Cannot import a \".{s}\" file into a CSS file", + .{@tagName(loader)}, + ) catch bun.outOfMemory(); + }, + .css, .file, .toml, .wasm, .base64, .dataurl, .text, .bunsh => {}, + } + } + } + } + return if (log.errors > 0) .errors else .ok; + } + + const MatchImport = struct { + alias: string = "", + kind: MatchImport.Kind = MatchImport.Kind.ignore, + namespace_ref: Ref = Ref.None, + source_index: u32 = 0, + name_loc: Logger.Loc = Logger.Loc.Empty, // Optional, goes with sourceIndex, ignore if zero, + other_source_index: u32 = 0, + other_name_loc: Logger.Loc = Logger.Loc.Empty, // Optional, goes with otherSourceIndex, ignore if zero, + ref: Ref = Ref.None, + + pub const Kind = enum { + /// The import is either external or undefined + ignore, + + /// "sourceIndex" and "ref" are in use + normal, + + /// "namespaceRef" and "alias" are in use + namespace, + + /// Both "normal" and "namespace" + normal_and_namespace, + + /// The import could not be evaluated due to a cycle + cycle, + + /// The import is missing but came from a TypeScript file + probably_typescript_type, + + /// The import resolved to multiple symbols via "export * from" + ambiguous, + }; + }; + + pub fn getSource(c: *LinkerContext, index: usize) *const Logger.Source { + return &c.parse_graph.input_files.items(.source)[index]; + } + + pub fn treeShakingAndCodeSplitting(c: *LinkerContext) !void { + const trace = bun.perf.trace("Bundler.treeShakingAndCodeSplitting"); + defer trace.end(); + + const parts = c.graph.ast.items(.parts); + const import_records = c.graph.ast.items(.import_records); + const css_reprs = c.graph.ast.items(.css); + const side_effects = c.parse_graph.input_files.items(.side_effects); + const entry_point_kinds = c.graph.files.items(.entry_point_kind); + const entry_points = c.graph.entry_points.items(.source_index); + const distances = c.graph.files.items(.distance_from_entry_point); + + { + const trace2 = bun.perf.trace("Bundler.markFileLiveForTreeShaking"); + defer trace2.end(); + + // Tree shaking: Each entry point marks all files reachable from itself + for (entry_points) |entry_point| { + c.markFileLiveForTreeShaking( + entry_point, + side_effects, + parts, + import_records, + entry_point_kinds, + css_reprs, + ); + } + } + + { + const trace2 = bun.perf.trace("Bundler.markFileReachableForCodeSplitting"); + defer trace2.end(); + + const file_entry_bits: []AutoBitSet = c.graph.files.items(.entry_bits); + // AutoBitSet needs to be initialized if it is dynamic + if (AutoBitSet.needsDynamic(entry_points.len)) { + for (file_entry_bits) |*bits| { + bits.* = try AutoBitSet.initEmpty(c.allocator, entry_points.len); + } + } else if (file_entry_bits.len > 0) { + // assert that the tag is correct + bun.assert(file_entry_bits[0] == .static); + } + + // Code splitting: Determine which entry points can reach which files. This + // has to happen after tree shaking because there is an implicit dependency + // between live parts within the same file. All liveness has to be computed + // first before determining which entry points can reach which files. + for (entry_points, 0..) |entry_point, i| { + c.markFileReachableForCodeSplitting( + entry_point, + i, + distances, + 0, + parts, + import_records, + file_entry_bits, + css_reprs, + ); + } + } + } + + pub const ChunkMeta = struct { + imports: Map, + exports: Map, + dynamic_imports: std.AutoArrayHashMap(Index.Int, void), + + pub const Map = std.AutoArrayHashMap(Ref, void); + }; + + pub const computeCrossChunkDependencies = @import("linker_context/computeCrossChunkDependencies.zig").computeCrossChunkDependencies; + + pub const GenerateChunkCtx = struct { + wg: *sync.WaitGroup, + c: *LinkerContext, + chunks: []Chunk, + chunk: *Chunk, + }; + + pub const postProcessJSChunk = @import("linker_context/postProcessJSChunk.zig").postProcessJSChunk; + pub const postProcessCSSChunk = @import("linker_context/postProcessCSSChunk.zig").postProcessCSSChunk; + pub const postProcessHTMLChunk = @import("linker_context/postProcessHTMLChunk.zig").postProcessHTMLChunk; + pub fn generateChunk(ctx: GenerateChunkCtx, chunk: *Chunk, chunk_index: usize) void { + defer ctx.wg.finish(); + const worker = ThreadPool.Worker.get(@fieldParentPtr("linker", ctx.c)); + defer worker.unget(); + switch (chunk.content) { + .javascript => postProcessJSChunk(ctx, worker, chunk, chunk_index) catch |err| Output.panic("TODO: handle error: {s}", .{@errorName(err)}), + .css => postProcessCSSChunk(ctx, worker, chunk) catch |err| Output.panic("TODO: handle error: {s}", .{@errorName(err)}), + .html => postProcessHTMLChunk(ctx, worker, chunk) catch |err| Output.panic("TODO: handle error: {s}", .{@errorName(err)}), + } + } + + pub const renameSymbolsInChunk = @import("linker_context/renameSymbolsInChunk.zig").renameSymbolsInChunk; + + pub fn generateJSRenamer(ctx: GenerateChunkCtx, chunk: *Chunk, chunk_index: usize) void { + defer ctx.wg.finish(); + var worker = ThreadPool.Worker.get(@fieldParentPtr("linker", ctx.c)); + defer worker.unget(); + switch (chunk.content) { + .javascript => generateJSRenamer_(ctx, worker, chunk, chunk_index), + .css => {}, + .html => {}, + } + } + + fn generateJSRenamer_(ctx: GenerateChunkCtx, worker: *ThreadPool.Worker, chunk: *Chunk, chunk_index: usize) void { + _ = chunk_index; + chunk.renamer = ctx.c.renameSymbolsInChunk( + worker.allocator, + chunk, + chunk.content.javascript.files_in_chunk_order, + ) catch @panic("TODO: handle error"); + } + + pub const generateChunksInParallel = @import("linker_context/generateChunksInParallel.zig").generateChunksInParallel; + pub const generateCompileResultForJSChunk = @import("linker_context/generateCompileResultForJSChunk.zig").generateCompileResultForJSChunk; + pub const generateCompileResultForCssChunk = @import("linker_context/generateCompileResultForCssChunk.zig").generateCompileResultForCssChunk; + pub const generateCompileResultForHtmlChunk = @import("linker_context/generateCompileResultForHtmlChunk.zig").generateCompileResultForHtmlChunk; + + pub const prepareCssAstsForChunk = @import("linker_context/prepareCssAstsForChunk.zig").prepareCssAstsForChunk; + pub const PrepareCssAstTask = @import("linker_context/prepareCssAstsForChunk.zig").PrepareCssAstTask; + + pub fn generateSourceMapForChunk( + c: *LinkerContext, + isolated_hash: u64, + worker: *ThreadPool.Worker, + results: std.MultiArrayList(CompileResultForSourceMap), + chunk_abs_dir: string, + can_have_shifts: bool, + ) !sourcemap.SourceMapPieces { + const trace = bun.perf.trace("Bundler.generateSourceMapForChunk"); + defer trace.end(); + + var j = StringJoiner{ .allocator = worker.allocator }; + + const sources = c.parse_graph.input_files.items(.source); + const quoted_source_map_contents = c.graph.files.items(.quoted_source_contents); + + // Entries in `results` do not 1:1 map to source files, the mapping + // is actually many to one, where a source file can have multiple chunks + // in the sourcemap. + // + // This hashmap is going to map: + // `source_index` (per compilation) in a chunk + // --> + // Which source index in the generated sourcemap, referred to + // as the "mapping source index" within this function to be distinct. + var source_id_map = std.AutoArrayHashMap(u32, i32).init(worker.allocator); + defer source_id_map.deinit(); + + const source_indices = results.items(.source_index); + + j.pushStatic( + \\{ + \\ "version": 3, + \\ "sources": [ + ); + if (source_indices.len > 0) { + { + const index = source_indices[0]; + var path = sources[index].path; + try source_id_map.putNoClobber(index, 0); + + if (path.isFile()) { + const rel_path = try std.fs.path.relative(worker.allocator, chunk_abs_dir, path.text); + path.pretty = rel_path; + } + + var quote_buf = try MutableString.init(worker.allocator, path.pretty.len + 2); + quote_buf = try js_printer.quoteForJSON(path.pretty, quote_buf, false); + j.pushStatic(quote_buf.list.items); // freed by arena + } + + var next_mapping_source_index: i32 = 1; + for (source_indices[1..]) |index| { + const gop = try source_id_map.getOrPut(index); + if (gop.found_existing) continue; + + gop.value_ptr.* = next_mapping_source_index; + next_mapping_source_index += 1; + + var path = sources[index].path; + + if (path.isFile()) { + const rel_path = try std.fs.path.relative(worker.allocator, chunk_abs_dir, path.text); + path.pretty = rel_path; + } + + var quote_buf = try MutableString.init(worker.allocator, path.pretty.len + ", ".len + 2); + quote_buf.appendAssumeCapacity(", "); + quote_buf = try js_printer.quoteForJSON(path.pretty, quote_buf, false); + j.pushStatic(quote_buf.list.items); // freed by arena + } + } + + j.pushStatic( + \\], + \\ "sourcesContent": [ + ); + + const source_indices_for_contents = source_id_map.keys(); + if (source_indices_for_contents.len > 0) { + j.pushStatic("\n "); + j.pushStatic(quoted_source_map_contents[source_indices_for_contents[0]]); + + for (source_indices_for_contents[1..]) |index| { + j.pushStatic(",\n "); + j.pushStatic(quoted_source_map_contents[index]); + } + } + j.pushStatic( + \\ + \\ ], + \\ "mappings": " + ); + + const mapping_start = j.len; + var prev_end_state = sourcemap.SourceMapState{}; + var prev_column_offset: i32 = 0; + const source_map_chunks = results.items(.source_map_chunk); + const offsets = results.items(.generated_offset); + for (source_map_chunks, offsets, source_indices) |chunk, offset, current_source_index| { + const mapping_source_index = source_id_map.get(current_source_index) orelse + unreachable; // the pass above during printing of "sources" must add the index + + var start_state = sourcemap.SourceMapState{ + .source_index = mapping_source_index, + .generated_line = offset.lines, + .generated_column = offset.columns, + }; + + if (offset.lines == 0) { + start_state.generated_column += prev_column_offset; + } + + try sourcemap.appendSourceMapChunk(&j, worker.allocator, prev_end_state, start_state, chunk.buffer.list.items); + + prev_end_state = chunk.end_state; + prev_end_state.source_index = mapping_source_index; + prev_column_offset = chunk.final_generated_column; + + if (prev_end_state.generated_line == 0) { + prev_end_state.generated_column += start_state.generated_column; + prev_column_offset += start_state.generated_column; + } + } + const mapping_end = j.len; + + if (comptime FeatureFlags.source_map_debug_id) { + j.pushStatic("\",\n \"debugId\": \""); + j.push( + try std.fmt.allocPrint(worker.allocator, "{}", .{bun.sourcemap.DebugIDFormatter{ .id = isolated_hash }}), + worker.allocator, + ); + j.pushStatic("\",\n \"names\": []\n}"); + } else { + j.pushStatic("\",\n \"names\": []\n}"); + } + + const done = try j.done(worker.allocator); + bun.assert(done[0] == '{'); + + var pieces = sourcemap.SourceMapPieces.init(worker.allocator); + if (can_have_shifts) { + try pieces.prefix.appendSlice(done[0..mapping_start]); + try pieces.mappings.appendSlice(done[mapping_start..mapping_end]); + try pieces.suffix.appendSlice(done[mapping_end..]); + } else { + try pieces.prefix.appendSlice(done); + } + + return pieces; + } + + pub fn generateIsolatedHash(c: *LinkerContext, chunk: *const Chunk) u64 { + const trace = bun.perf.trace("Bundler.generateIsolatedHash"); + defer trace.end(); + + var hasher = ContentHasher{}; + + // Mix the file names and part ranges of all of the files in this chunk into + // the hash. Objects that appear identical but that live in separate files or + // that live in separate parts in the same file must not be merged. This only + // needs to be done for JavaScript files, not CSS files. + if (chunk.content == .javascript) { + const sources = c.parse_graph.input_files.items(.source); + for (chunk.content.javascript.parts_in_chunk_in_order) |part_range| { + const source: *Logger.Source = &sources[part_range.source_index.get()]; + + const file_path = brk: { + if (source.path.isFile()) { + // Use the pretty path as the file name since it should be platform- + // independent (relative paths and the "/" path separator) + if (source.path.text.ptr == source.path.pretty.ptr) { + source.path = c.pathWithPrettyInitialized(source.path) catch bun.outOfMemory(); + } + source.path.assertPrettyIsValid(); + + break :brk source.path.pretty; + } else { + // If this isn't in the "file" namespace, just use the full path text + // verbatim. This could be a source of cross-platform differences if + // plugins are storing platform-specific information in here, but then + // that problem isn't caused by esbuild itself. + break :brk source.path.text; + } + }; + + // Include the path namespace in the hash + hasher.write(source.path.namespace); + + // Then include the file path + hasher.write(file_path); + + // Then include the part range + hasher.writeInts(&[_]u32{ + part_range.part_index_begin, + part_range.part_index_end, + }); + } + } + + // Hash the output path template as part of the content hash because we want + // any import to be considered different if the import's output path has changed. + hasher.write(chunk.template.data); + + // Also hash the public path. If provided, this is used whenever files + // reference each other such as cross-chunk imports, asset file references, + // and source map comments. We always include the hash in all chunks instead + // of trying to figure out which chunks will include the public path for + // simplicity and for robustness to code changes in the future. + if (c.options.public_path.len > 0) { + hasher.write(c.options.public_path); + } + + // Include the generated output content in the hash. This excludes the + // randomly-generated import paths (the unique keys) and only includes the + // data in the spans between them. + if (chunk.intermediate_output == .pieces) { + for (chunk.intermediate_output.pieces.slice()) |piece| { + hasher.write(piece.data()); + } + } else { + var el = chunk.intermediate_output.joiner.head; + while (el) |e| : (el = e.next) { + hasher.write(e.slice); + } + } + + // Also include the source map data in the hash. The source map is named the + // same name as the chunk name for ease of discovery. So we want the hash to + // change if the source map data changes even if the chunk data doesn't change. + // Otherwise the output path for the source map wouldn't change and the source + // map wouldn't end up being updated. + // + // Note that this means the contents of all input files are included in the + // hash because of "sourcesContent", so changing a comment in an input file + // can now change the hash of the output file. This only happens when you + // have source maps enabled (and "sourcesContent", which is on by default). + // + // The generated positions in the mappings here are in the output content + // *before* the final paths have been substituted. This may seem weird. + // However, I think this shouldn't cause issues because a) the unique key + // values are all always the same length so the offsets are deterministic + // and b) the final paths will be folded into the final hash later. + hasher.write(chunk.output_source_map.prefix.items); + hasher.write(chunk.output_source_map.mappings.items); + hasher.write(chunk.output_source_map.suffix.items); + + return hasher.digest(); + } + + pub fn validateTLA( + c: *LinkerContext, + source_index: Index.Int, + tla_keywords: []Logger.Range, + tla_checks: []js_ast.TlaCheck, + input_files: []Logger.Source, + import_records: []ImportRecord, + meta_flags: []JSMeta.Flags, + ast_import_records: []bun.BabyList(ImportRecord), + ) js_ast.TlaCheck { + var result_tla_check: *js_ast.TlaCheck = &tla_checks[source_index]; + + if (result_tla_check.depth == 0) { + result_tla_check.depth = 1; + if (tla_keywords[source_index].len > 0) { + result_tla_check.parent = source_index; + } + + for (import_records, 0..) |record, import_record_index| { + if (Index.isValid(record.source_index) and (record.kind == .require or record.kind == .stmt)) { + const parent = c.validateTLA(record.source_index.get(), tla_keywords, tla_checks, input_files, import_records, meta_flags, ast_import_records); + if (Index.isInvalid(Index.init(parent.parent))) { + continue; + } + + // Follow any import chains + if (record.kind == .stmt and (Index.isInvalid(Index.init(result_tla_check.parent)) or parent.depth < result_tla_check.depth)) { + result_tla_check.depth = parent.depth + 1; + result_tla_check.parent = record.source_index.get(); + result_tla_check.import_record_index = @intCast(import_record_index); + continue; + } + + // Require of a top-level await chain is forbidden + if (record.kind == .require) { + var notes = std.ArrayList(Logger.Data).init(c.allocator); + + var tla_pretty_path: string = ""; + var other_source_index = record.source_index.get(); + + // Build up a chain of notes for all of the imports + while (true) { + const parent_result_tla_keyword = tla_keywords[other_source_index]; + const parent_tla_check = tla_checks[other_source_index]; + const parent_source_index = other_source_index; + + if (parent_result_tla_keyword.len > 0) { + const source = input_files[other_source_index]; + tla_pretty_path = source.path.pretty; + notes.append(Logger.Data{ + .text = std.fmt.allocPrint(c.allocator, "The top-level await in {s} is here:", .{tla_pretty_path}) catch bun.outOfMemory(), + .location = .initOrNull(&source, parent_result_tla_keyword), + }) catch bun.outOfMemory(); + break; + } + + if (!Index.isValid(Index.init(parent_tla_check.parent))) { + notes.append(Logger.Data{ + .text = "unexpected invalid index", + }) catch bun.outOfMemory(); + break; + } + + other_source_index = parent_tla_check.parent; + + notes.append(Logger.Data{ + .text = std.fmt.allocPrint(c.allocator, "The file {s} imports the file {s} here:", .{ + input_files[parent_source_index].path.pretty, + input_files[other_source_index].path.pretty, + }) catch bun.outOfMemory(), + .location = .initOrNull(&input_files[parent_source_index], ast_import_records[parent_source_index].slice()[tla_checks[parent_source_index].import_record_index].range), + }) catch bun.outOfMemory(); + } + + const source: *const Logger.Source = &input_files[source_index]; + const imported_pretty_path = source.path.pretty; + const text: string = if (strings.eql(imported_pretty_path, tla_pretty_path)) + std.fmt.allocPrint(c.allocator, "This require call is not allowed because the imported file \"{s}\" contains a top-level await", .{imported_pretty_path}) catch bun.outOfMemory() + else + std.fmt.allocPrint(c.allocator, "This require call is not allowed because the transitive dependency \"{s}\" contains a top-level await", .{tla_pretty_path}) catch bun.outOfMemory(); + + c.log.addRangeErrorWithNotes(source, record.range, text, notes.items) catch bun.outOfMemory(); + } + } + } + + // Make sure that if we wrap this module in a closure, the closure is also + // async. This happens when you call "import()" on this module and code + // splitting is off. + if (Index.isValid(Index.init(result_tla_check.parent))) { + meta_flags[source_index].is_async_or_has_async_dependency = true; + } + } + + return result_tla_check.*; + } + + pub const StmtList = struct { + inside_wrapper_prefix: std.ArrayList(Stmt), + outside_wrapper_prefix: std.ArrayList(Stmt), + inside_wrapper_suffix: std.ArrayList(Stmt), + + all_stmts: std.ArrayList(Stmt), + + pub fn reset(this: *StmtList) void { + this.inside_wrapper_prefix.clearRetainingCapacity(); + this.outside_wrapper_prefix.clearRetainingCapacity(); + this.inside_wrapper_suffix.clearRetainingCapacity(); + this.all_stmts.clearRetainingCapacity(); + } + + pub fn deinit(this: *StmtList) void { + this.inside_wrapper_prefix.deinit(); + this.outside_wrapper_prefix.deinit(); + this.inside_wrapper_suffix.deinit(); + this.all_stmts.deinit(); + } + + pub fn init(allocator: std.mem.Allocator) StmtList { + return .{ + .inside_wrapper_prefix = std.ArrayList(Stmt).init(allocator), + .outside_wrapper_prefix = std.ArrayList(Stmt).init(allocator), + .inside_wrapper_suffix = std.ArrayList(Stmt).init(allocator), + .all_stmts = std.ArrayList(Stmt).init(allocator), + }; + } + }; + + pub fn shouldRemoveImportExportStmt( + c: *LinkerContext, + stmts: *StmtList, + loc: Logger.Loc, + namespace_ref: Ref, + import_record_index: u32, + allocator: std.mem.Allocator, + ast: *const JSAst, + ) !bool { + const record = ast.import_records.at(import_record_index); + // Is this an external import? + if (!record.source_index.isValid()) { + // Keep the "import" statement if import statements are supported + if (c.options.output_format.keepES6ImportExportSyntax()) { + return false; + } + + // Otherwise, replace this statement with a call to "require()" + stmts.inside_wrapper_prefix.append( + Stmt.alloc( + S.Local, + S.Local{ + .decls = G.Decl.List.fromSlice( + allocator, + &.{ + .{ + .binding = Binding.alloc( + allocator, + B.Identifier{ + .ref = namespace_ref, + }, + loc, + ), + .value = Expr.init( + E.RequireString, + E.RequireString{ + .import_record_index = import_record_index, + }, + loc, + ), + }, + }, + ) catch unreachable, + }, + record.range.loc, + ), + ) catch unreachable; + return true; + } + + // We don't need a call to "require()" if this is a self-import inside a + // CommonJS-style module, since we can just reference the exports directly. + if (ast.exports_kind == .cjs and c.graph.symbols.follow(namespace_ref).eql(ast.exports_ref)) { + return true; + } + + const other_flags = c.graph.meta.items(.flags)[record.source_index.get()]; + switch (other_flags.wrap) { + .none => {}, + .cjs => { + // Replace the statement with a call to "require()" if this module is not wrapped + try stmts.inside_wrapper_prefix.append( + Stmt.alloc(S.Local, .{ + .decls = try G.Decl.List.fromSlice( + allocator, + &.{ + .{ + .binding = Binding.alloc(allocator, B.Identifier{ + .ref = namespace_ref, + }, loc), + .value = Expr.init(E.RequireString, .{ + .import_record_index = import_record_index, + }, loc), + }, + }, + ), + }, loc), + ); + }, + .esm => { + // Ignore this file if it's not included in the bundle. This can happen for + // wrapped ESM files but not for wrapped CommonJS files because we allow + // tree shaking inside wrapped ESM files. + if (!c.graph.files_live.isSet(record.source_index.get())) { + return true; + } + + const wrapper_ref = c.graph.ast.items(.wrapper_ref)[record.source_index.get()]; + if (wrapper_ref.isEmpty()) { + return true; + } + + // Replace the statement with a call to "init()" + const value: Expr = brk: { + const default = Expr.init(E.Call, .{ + .target = Expr.initIdentifier( + wrapper_ref, + loc, + ), + }, loc); + + if (other_flags.is_async_or_has_async_dependency) { + // This currently evaluates sibling dependencies in serial instead of in + // parallel, which is incorrect. This should be changed to store a promise + // and await all stored promises after all imports but before any code. + break :brk Expr.init(E.Await, .{ + .value = default, + }, loc); + } + + break :brk default; + }; + + try stmts.inside_wrapper_prefix.append( + Stmt.alloc(S.SExpr, .{ + .value = value, + }, loc), + ); + }, + } + + return true; + } + + pub const convertStmtsForChunk = @import("linker_context/convertStmtsForChunk.zig").convertStmtsForChunk; + pub const convertStmtsForChunkForDevServer = @import("linker_context/convertStmtsForChunkForDevServer.zig").convertStmtsForChunkForDevServer; + + pub fn runtimeFunction(c: *LinkerContext, name: []const u8) Ref { + return c.graph.runtimeFunction(name); + } + + pub const generateCodeForFileInChunkJS = @import("linker_context/generateCodeForFileInChunkJS.zig").generateCodeForFileInChunkJS; + + pub fn printCodeForFileInChunkJS( + c: *LinkerContext, + r: renamer.Renamer, + allocator: std.mem.Allocator, + writer: *js_printer.BufferWriter, + out_stmts: []Stmt, + ast: *const js_ast.BundledAst, + flags: JSMeta.Flags, + to_esm_ref: Ref, + to_commonjs_ref: Ref, + runtime_require_ref: ?Ref, + source_index: Index, + source: *const bun.logger.Source, + ) js_printer.PrintResult { + const parts_to_print = &[_]Part{ + .{ .stmts = out_stmts }, + }; + + const print_options = js_printer.Options{ + .bundling = true, + // TODO: IIFE + .indent = .{}, + .commonjs_named_exports = ast.commonjs_named_exports, + .commonjs_named_exports_ref = ast.exports_ref, + .commonjs_module_ref = if (ast.flags.uses_module_ref) + ast.module_ref + else + Ref.None, + .commonjs_named_exports_deoptimized = flags.wrap == .cjs, + .commonjs_module_exports_assigned_deoptimized = ast.flags.commonjs_module_exports_assigned_deoptimized, + // .const_values = c.graph.const_values, + .ts_enums = c.graph.ts_enums, + + .minify_whitespace = c.options.minify_whitespace, + .minify_syntax = c.options.minify_syntax, + .module_type = c.options.output_format, + .print_dce_annotations = c.options.emit_dce_annotations, + .has_run_symbol_renamer = true, + + .allocator = allocator, + .source_map_allocator = if (c.dev_server != null and + c.parse_graph.input_files.items(.loader)[source_index.get()].isJavaScriptLike()) + // The loader check avoids globally allocating asset source maps + writer.buffer.allocator + else + allocator, + .to_esm_ref = to_esm_ref, + .to_commonjs_ref = to_commonjs_ref, + .require_ref = switch (c.options.output_format) { + .cjs => null, // use unbounded global + else => runtime_require_ref, + }, + .require_or_import_meta_for_source_callback = .init( + LinkerContext, + requireOrImportMetaForSource, + c, + ), + .line_offset_tables = c.graph.files.items(.line_offset_table)[source_index.get()], + .target = c.options.target, + + .hmr_ref = if (c.options.output_format == .internal_bake_dev) + ast.wrapper_ref + else + .None, + + .input_files_for_dev_server = if (c.options.output_format == .internal_bake_dev) + c.parse_graph.input_files.items(.source) + else + null, + .mangled_props = &c.mangled_props, + }; + + writer.buffer.reset(); + var printer = js_printer.BufferPrinter.init(writer.*); + defer writer.* = printer.ctx; + + switch (c.options.source_maps != .none and !source_index.isRuntime()) { + inline else => |enable_source_maps| { + return js_printer.printWithWriter( + *js_printer.BufferPrinter, + &printer, + ast.target, + ast.toAST(), + source, + print_options, + ast.import_records.slice(), + parts_to_print, + r, + enable_source_maps, + ); + }, + } + } + + pub const PendingPartRange = struct { + part_range: PartRange, + task: ThreadPoolLib.Task, + ctx: *GenerateChunkCtx, + i: u32 = 0, + }; + + pub fn requireOrImportMetaForSource( + c: *LinkerContext, + source_index: Index.Int, + was_unwrapped_require: bool, + ) js_printer.RequireOrImportMeta { + const flags = c.graph.meta.items(.flags)[source_index]; + return .{ + .exports_ref = if (flags.wrap == .esm or (was_unwrapped_require and c.graph.ast.items(.flags)[source_index].force_cjs_to_esm)) + c.graph.ast.items(.exports_ref)[source_index] + else + Ref.None, + .is_wrapper_async = flags.is_async_or_has_async_dependency, + .wrapper_ref = c.graph.ast.items(.wrapper_ref)[source_index], + + .was_unwrapped_require = was_unwrapped_require and c.graph.ast.items(.flags)[source_index].force_cjs_to_esm, + }; + } + + const SubstituteChunkFinalPathResult = struct { + j: StringJoiner, + shifts: []sourcemap.SourceMapShifts, + }; + + pub fn mangleLocalCss(c: *LinkerContext) void { + if (c.has_any_css_locals.load(.monotonic) == 0) return; + + const all_css_asts: []?*bun.css.BundlerStyleSheet = c.graph.ast.items(.css); + const all_symbols: []Symbol.List = c.graph.ast.items(.symbols); + const all_sources: []Logger.Source = c.parse_graph.input_files.items(.source); + + // Collect all local css names + var sfb = std.heap.stackFallback(512, c.allocator); + const allocator = sfb.get(); + var local_css_names = std.AutoHashMap(bun.bundle_v2.Ref, void).init(allocator); + defer local_css_names.deinit(); + + for (all_css_asts, 0..) |maybe_css_ast, source_index| { + if (maybe_css_ast) |css_ast| { + if (css_ast.local_scope.count() == 0) continue; + const symbols = all_symbols[source_index]; + for (symbols.sliceConst(), 0..) |*symbol_, inner_index| { + var symbol = symbol_; + if (symbol.kind == .local_css) { + const ref = ref: { + var ref = Ref.init(@intCast(inner_index), @intCast(source_index), false); + ref.tag = .symbol; + while (symbol.hasLink()) { + ref = symbol.link; + symbol = all_symbols[ref.source_index].at(ref.inner_index); + } + break :ref ref; + }; + + const entry = local_css_names.getOrPut(ref) catch bun.outOfMemory(); + if (entry.found_existing) continue; + + const source = all_sources[ref.source_index]; + + const original_name = symbol.original_name; + const path_hash = bun.css.css_modules.hash( + allocator, + "{s}", + // use path relative to cwd for determinism + .{source.path.pretty}, + false, + ); + + const final_generated_name = std.fmt.allocPrint(c.graph.allocator, "{s}_{s}", .{ original_name, path_hash }) catch bun.outOfMemory(); + c.mangled_props.put(c.allocator, ref, final_generated_name) catch bun.outOfMemory(); + } + } + } + } + } + + pub fn appendIsolatedHashesForImportedChunks( + c: *LinkerContext, + hash: *ContentHasher, + chunks: []Chunk, + index: u32, + chunk_visit_map: *AutoBitSet, + ) void { + // Only visit each chunk at most once. This is important because there may be + // cycles in the chunk import graph. If there's a cycle, we want to include + // the hash of every chunk involved in the cycle (along with all of their + // dependencies). This depth-first traversal will naturally do that. + if (chunk_visit_map.isSet(index)) { + return; + } + chunk_visit_map.set(index); + + // Visit the other chunks that this chunk imports before visiting this chunk + const chunk = &chunks[index]; + for (chunk.cross_chunk_imports.slice()) |import| { + c.appendIsolatedHashesForImportedChunks( + hash, + chunks, + import.chunk_index, + chunk_visit_map, + ); + } + + // Mix in hashes for referenced asset paths (i.e. the "file" loader) + switch (chunk.intermediate_output) { + .pieces => |pieces| for (pieces.slice()) |piece| { + if (piece.query.kind == .asset) { + var from_chunk_dir = std.fs.path.dirnamePosix(chunk.final_rel_path) orelse ""; + if (strings.eqlComptime(from_chunk_dir, ".")) + from_chunk_dir = ""; + + const source_index = piece.query.index; + const additional_files: []AdditionalFile = c.parse_graph.input_files.items(.additional_files)[source_index].slice(); + bun.assert(additional_files.len > 0); + switch (additional_files[0]) { + .output_file => |output_file_id| { + const path = c.parse_graph.additional_output_files.items[output_file_id].dest_path; + hash.write(bun.path.relativePlatform(from_chunk_dir, path, .posix, false)); + }, + .source_index => {}, + } + } + }, + else => {}, + } + + // Mix in the hash for this chunk + hash.write(std.mem.asBytes(&chunk.isolated_hash)); + } + + pub const writeOutputFilesToDisk = @import("linker_context/writeOutputFilesToDisk.zig").writeOutputFilesToDisk; + + // Sort cross-chunk exports by chunk name for determinism + pub fn sortedCrossChunkExportItems( + c: *LinkerContext, + export_refs: ChunkMeta.Map, + list: *std.ArrayList(StableRef), + ) void { + var result = list.*; + defer list.* = result; + result.clearRetainingCapacity(); + result.ensureTotalCapacity(export_refs.count()) catch unreachable; + result.items.len = export_refs.count(); + for (export_refs.keys(), result.items) |export_ref, *item| { + if (comptime Environment.allow_assert) + debugTreeShake("Export name: {s} (in {s})", .{ + c.graph.symbols.get(export_ref).?.original_name, + c.parse_graph.input_files.get(export_ref.sourceIndex()).source.path.text, + }); + item.* = .{ + .stable_source_index = c.graph.stable_source_indices[export_ref.sourceIndex()], + .ref = export_ref, + }; + } + std.sort.pdq(StableRef, result.items, {}, StableRef.isLessThan); + } + + pub fn markFileReachableForCodeSplitting( + c: *LinkerContext, + source_index: Index.Int, + entry_points_count: usize, + distances: []u32, + distance: u32, + parts: []bun.BabyList(Part), + import_records: []bun.BabyList(bun.ImportRecord), + file_entry_bits: []AutoBitSet, + css_reprs: []?*bun.css.BundlerStyleSheet, + ) void { + if (!c.graph.files_live.isSet(source_index)) + return; + + const cur_dist = distances[source_index]; + const traverse_again = distance < cur_dist; + if (traverse_again) { + distances[source_index] = distance; + } + const out_dist = distance + 1; + + var bits = &file_entry_bits[source_index]; + + // Don't mark this file more than once + if (bits.isSet(entry_points_count) and !traverse_again) + return; + + bits.set(entry_points_count); + + if (comptime bun.Environment.enable_logs) + debugTreeShake( + "markFileReachableForCodeSplitting(entry: {d}): {s} {s} ({d})", + .{ + entry_points_count, + c.parse_graph.input_files.items(.source)[source_index].path.pretty, + @tagName(c.parse_graph.ast.items(.target)[source_index].bakeGraph()), + out_dist, + }, + ); + + if (css_reprs[source_index] != null) { + for (import_records[source_index].slice()) |*record| { + if (record.source_index.isValid() and !c.isExternalDynamicImport(record, source_index)) { + c.markFileReachableForCodeSplitting( + record.source_index.get(), + entry_points_count, + distances, + out_dist, + parts, + import_records, + file_entry_bits, + css_reprs, + ); + } + } + return; + } + + for (import_records[source_index].slice()) |*record| { + if (record.source_index.isValid() and !c.isExternalDynamicImport(record, source_index)) { + c.markFileReachableForCodeSplitting( + record.source_index.get(), + entry_points_count, + distances, + out_dist, + parts, + import_records, + file_entry_bits, + css_reprs, + ); + } + } + + const parts_in_file = parts[source_index].slice(); + for (parts_in_file) |part| { + for (part.dependencies.slice()) |dependency| { + if (dependency.source_index.get() != source_index) { + c.markFileReachableForCodeSplitting( + dependency.source_index.get(), + entry_points_count, + distances, + out_dist, + parts, + import_records, + file_entry_bits, + css_reprs, + ); + } + } + } + } + + pub fn markFileLiveForTreeShaking( + c: *LinkerContext, + source_index: Index.Int, + side_effects: []_resolver.SideEffects, + parts: []bun.BabyList(Part), + import_records: []bun.BabyList(bun.ImportRecord), + entry_point_kinds: []EntryPoint.Kind, + css_reprs: []?*bun.css.BundlerStyleSheet, + ) void { + if (comptime bun.Environment.allow_assert) { + debugTreeShake("markFileLiveForTreeShaking({d}, {s} {s}) = {s}", .{ + source_index, + c.parse_graph.input_files.get(source_index).source.path.pretty, + @tagName(c.parse_graph.ast.items(.target)[source_index].bakeGraph()), + if (c.graph.files_live.isSet(source_index)) "already seen" else "first seen", + }); + } + + defer if (Environment.allow_assert) { + debugTreeShake("end()", .{}); + }; + + if (c.graph.files_live.isSet(source_index)) return; + c.graph.files_live.set(source_index); + + if (source_index >= c.graph.ast.len) { + bun.assert(false); + return; + } + + if (css_reprs[source_index] != null) { + for (import_records[source_index].slice()) |*record| { + const other_source_index = record.source_index.get(); + if (record.source_index.isValid()) { + c.markFileLiveForTreeShaking( + other_source_index, + side_effects, + parts, + import_records, + entry_point_kinds, + css_reprs, + ); + } + } + return; + } + + for (parts[source_index].slice(), 0..) |part, part_index| { + var can_be_removed_if_unused = part.can_be_removed_if_unused; + + if (can_be_removed_if_unused and part.tag == .commonjs_named_export) { + if (c.graph.meta.items(.flags)[source_index].wrap == .cjs) { + can_be_removed_if_unused = false; + } + } + + // Also include any statement-level imports + for (part.import_record_indices.slice()) |import_index| { + const record = import_records[source_index].at(import_index); + if (record.kind != .stmt) + continue; + + if (record.source_index.isValid()) { + const other_source_index = record.source_index.get(); + + // Don't include this module for its side effects if it can be + // considered to have no side effects + const se = side_effects[other_source_index]; + + if (se != .has_side_effects and + !c.options.ignore_dce_annotations) + { + continue; + } + + // Otherwise, include this module for its side effects + c.markFileLiveForTreeShaking( + other_source_index, + side_effects, + parts, + import_records, + entry_point_kinds, + css_reprs, + ); + } else if (record.is_external_without_side_effects) { + // This can be removed if it's unused + continue; + } + + // If we get here then the import was included for its side effects, so + // we must also keep this part + can_be_removed_if_unused = false; + } + + // Include all parts in this file with side effects, or just include + // everything if tree-shaking is disabled. Note that we still want to + // perform tree-shaking on the runtime even if tree-shaking is disabled. + if (!can_be_removed_if_unused or + (!part.force_tree_shaking and + !c.options.tree_shaking and + entry_point_kinds[source_index].isEntryPoint())) + { + c.markPartLiveForTreeShaking( + @intCast(part_index), + source_index, + side_effects, + parts, + import_records, + entry_point_kinds, + css_reprs, + ); + } + } + } + + pub fn markPartLiveForTreeShaking( + c: *LinkerContext, + part_index: Index.Int, + source_index: Index.Int, + side_effects: []_resolver.SideEffects, + parts: []bun.BabyList(Part), + import_records: []bun.BabyList(bun.ImportRecord), + entry_point_kinds: []EntryPoint.Kind, + css_reprs: []?*bun.css.BundlerStyleSheet, + ) void { + const part: *Part = &parts[source_index].slice()[part_index]; + + // only once + if (part.is_live) { + return; + } + part.is_live = true; + + if (comptime bun.Environment.isDebug) { + debugTreeShake("markPartLiveForTreeShaking({d}): {s}:{d} = {d}, {s}", .{ + source_index, + c.parse_graph.input_files.get(source_index).source.path.pretty, + part_index, + if (part.stmts.len > 0) part.stmts[0].loc.start else Logger.Loc.Empty.start, + if (part.stmts.len > 0) @tagName(part.stmts[0].data) else @tagName(Stmt.empty().data), + }); + } + + defer if (Environment.allow_assert) { + debugTreeShake("end()", .{}); + }; + + // Include the file containing this part + c.markFileLiveForTreeShaking( + source_index, + side_effects, + parts, + import_records, + entry_point_kinds, + css_reprs, + ); + + if (Environment.enable_logs and part.dependencies.slice().len == 0) { + logPartDependencyTree("markPartLiveForTreeShaking {d}:{d} | EMPTY", .{ + source_index, part_index, + }); + } + + for (part.dependencies.slice()) |dependency| { + if (Environment.enable_logs and source_index != 0 and dependency.source_index.get() != 0) { + logPartDependencyTree("markPartLiveForTreeShaking: {d}:{d} --> {d}:{d}\n", .{ + source_index, part_index, dependency.source_index.get(), dependency.part_index, + }); + } + + c.markPartLiveForTreeShaking( + dependency.part_index, + dependency.source_index.get(), + side_effects, + parts, + import_records, + entry_point_kinds, + css_reprs, + ); + } + } + + pub fn matchImportWithExport( + c: *LinkerContext, + init_tracker: ImportTracker, + re_exports: *std.ArrayList(js_ast.Dependency), + ) MatchImport { + const cycle_detector_top = c.cycle_detector.items.len; + defer c.cycle_detector.shrinkRetainingCapacity(cycle_detector_top); + + var tracker = init_tracker; + var ambiguous_results = std.ArrayList(MatchImport).init(c.allocator); + defer ambiguous_results.clearAndFree(); + + var result: MatchImport = MatchImport{}; + const named_imports = c.graph.ast.items(.named_imports); + + loop: while (true) { + // Make sure we avoid infinite loops trying to resolve cycles: + // + // // foo.js + // export {a as b} from './foo.js' + // export {b as c} from './foo.js' + // export {c as a} from './foo.js' + // + // This uses a O(n^2) array scan instead of a O(n) map because the vast + // majority of cases have one or two elements + for (c.cycle_detector.items[cycle_detector_top..]) |prev_tracker| { + if (std.meta.eql(tracker, prev_tracker)) { + result = .{ .kind = .cycle }; + break :loop; + } + } + + if (tracker.source_index.isInvalid()) { + // External + break; + } + + const prev_source_index = tracker.source_index.get(); + c.cycle_detector.append(tracker) catch bun.outOfMemory(); + + // Resolve the import by one step + const advanced = c.advanceImportTracker(&tracker); + const next_tracker = advanced.value; + const status = advanced.status; + const potentially_ambiguous_export_star_refs = advanced.import_data; + + switch (status) { + .cjs, .cjs_without_exports, .disabled, .external => { + if (status == .external and c.options.output_format.keepES6ImportExportSyntax()) { + // Imports from external modules should not be converted to CommonJS + // if the output format preserves the original ES6 import statements + break; + } + + // If it's a CommonJS or external file, rewrite the import to a + // property access. Don't do this if the namespace reference is invalid + // though. This is the case for star imports, where the import is the + // namespace. + const named_import: js_ast.NamedImport = named_imports[prev_source_index].get(tracker.import_ref).?; + + if (named_import.namespace_ref != null and named_import.namespace_ref.?.isValid()) { + if (result.kind == .normal) { + result.kind = .normal_and_namespace; + result.namespace_ref = named_import.namespace_ref.?; + result.alias = named_import.alias.?; + } else { + result = .{ + .kind = .namespace, + .namespace_ref = named_import.namespace_ref.?, + .alias = named_import.alias.?, + }; + } + } + + // Warn about importing from a file that is known to not have any exports + if (status == .cjs_without_exports) { + const source = c.getSource(tracker.source_index.get()); + c.log.addRangeWarningFmt( + source, + source.rangeOfIdentifier(named_import.alias_loc.?), + c.allocator, + "Import \"{s}\" will always be undefined because the file \"{s}\" has no exports", + .{ + named_import.alias.?, + source.path.pretty, + }, + ) catch unreachable; + } + }, + + .dynamic_fallback_interop_default => { + // if the file was rewritten from CommonJS into ESM + // and the developer imported an export that doesn't exist + // We don't do a runtime error since that CJS would have returned undefined. + const named_import: js_ast.NamedImport = named_imports[prev_source_index].get(tracker.import_ref).?; + + if (named_import.namespace_ref != null and named_import.namespace_ref.?.isValid()) { + const symbol = c.graph.symbols.get(tracker.import_ref).?; + symbol.import_item_status = .missing; + result.kind = .normal_and_namespace; + result.namespace_ref = tracker.import_ref; + result.alias = named_import.alias.?; + result.name_loc = named_import.alias_loc orelse Logger.Loc.Empty; + } + }, + + .dynamic_fallback => { + // If it's a file with dynamic export fallback, rewrite the import to a property access + const named_import: js_ast.NamedImport = named_imports[prev_source_index].get(tracker.import_ref).?; + if (named_import.namespace_ref != null and named_import.namespace_ref.?.isValid()) { + if (result.kind == .normal) { + result.kind = .normal_and_namespace; + result.namespace_ref = next_tracker.import_ref; + result.alias = named_import.alias.?; + } else { + result = .{ + .kind = .namespace, + .namespace_ref = next_tracker.import_ref, + .alias = named_import.alias.?, + }; + } + } + }, + .no_match => { + // Report mismatched imports and exports + const symbol = c.graph.symbols.get(tracker.import_ref).?; + const named_import: js_ast.NamedImport = named_imports[prev_source_index].get(tracker.import_ref).?; + const source = c.getSource(prev_source_index); + + const next_source = c.getSource(next_tracker.source_index.get()); + const r = source.rangeOfIdentifier(named_import.alias_loc.?); + + // Report mismatched imports and exports + if (symbol.import_item_status == .generated) { + // This is a debug message instead of an error because although it + // appears to be a named import, it's actually an automatically- + // generated named import that was originally a property access on an + // import star namespace object. Normally this property access would + // just resolve to undefined at run-time instead of failing at binding- + // time, so we emit a debug message and rewrite the value to the literal + // "undefined" instead of emitting an error. + symbol.import_item_status = .missing; + + if (c.resolver.opts.target == .browser and JSC.ModuleLoader.HardcodedModule.Alias.has(next_source.path.pretty, .bun)) { + c.log.addRangeWarningFmtWithNote( + source, + r, + c.allocator, + "Browser polyfill for module \"{s}\" doesn't have a matching export named \"{s}\"", + .{ + next_source.path.pretty, + named_import.alias.?, + }, + "Bun's bundler defaults to browser builds instead of node or bun builds. If you want to use node or bun builds, you can set the target to \"node\" or \"bun\" in the transpiler options.", + .{}, + r, + ) catch unreachable; + } else { + c.log.addRangeWarningFmt( + source, + r, + c.allocator, + "Import \"{s}\" will always be undefined because there is no matching export in \"{s}\"", + .{ + named_import.alias.?, + next_source.path.pretty, + }, + ) catch unreachable; + } + } else if (c.resolver.opts.target == .browser and bun.strings.hasPrefixComptime(next_source.path.text, NodeFallbackModules.import_path)) { + c.log.addRangeErrorFmtWithNote( + source, + r, + c.allocator, + "Browser polyfill for module \"{s}\" doesn't have a matching export named \"{s}\"", + .{ + next_source.path.pretty, + named_import.alias.?, + }, + "Bun's bundler defaults to browser builds instead of node or bun builds. If you want to use node or bun builds, you can set the target to \"node\" or \"bun\" in the transpiler options.", + .{}, + r, + ) catch unreachable; + } else { + c.log.addRangeErrorFmt( + source, + r, + c.allocator, + "No matching export in \"{s}\" for import \"{s}\"", + .{ + next_source.path.pretty, + named_import.alias.?, + }, + ) catch unreachable; + } + }, + .probably_typescript_type => { + // Omit this import from any namespace export code we generate for + // import star statements (i.e. "import * as ns from 'path'") + result = .{ .kind = .probably_typescript_type }; + }, + .found => { + + // If there are multiple ambiguous results due to use of "export * from" + // statements, trace them all to see if they point to different things. + for (potentially_ambiguous_export_star_refs) |*ambiguous_tracker| { + // If this is a re-export of another import, follow the import + if (named_imports[ambiguous_tracker.data.source_index.get()].contains(ambiguous_tracker.data.import_ref)) { + const ambig = c.matchImportWithExport(ambiguous_tracker.data, re_exports); + ambiguous_results.append(ambig) catch unreachable; + } else { + ambiguous_results.append(.{ + .kind = .normal, + .source_index = ambiguous_tracker.data.source_index.get(), + .ref = ambiguous_tracker.data.import_ref, + .name_loc = ambiguous_tracker.data.name_loc, + }) catch unreachable; + } + } + + // Defer the actual binding of this import until after we generate + // namespace export code for all files. This has to be done for all + // import-to-export matches, not just the initial import to the final + // export, since all imports and re-exports must be merged together + // for correctness. + result = .{ + .kind = .normal, + .source_index = next_tracker.source_index.get(), + .ref = next_tracker.import_ref, + .name_loc = next_tracker.name_loc, + }; + + // Depend on the statement(s) that declared this import symbol in the + // original file + { + const deps = c.topLevelSymbolsToParts(prev_source_index, tracker.import_ref); + re_exports.ensureUnusedCapacity(deps.len) catch unreachable; + for (deps) |dep| { + re_exports.appendAssumeCapacity( + .{ + .part_index = dep, + .source_index = tracker.source_index, + }, + ); + } + } + + // If this is a re-export of another import, continue for another + // iteration of the loop to resolve that import as well + const next_id = next_tracker.source_index.get(); + if (named_imports[next_id].contains(next_tracker.import_ref)) { + tracker = next_tracker; + continue :loop; + } + }, + } + + break :loop; + } + + // If there is a potential ambiguity, all results must be the same + for (ambiguous_results.items) |ambig| { + if (!std.meta.eql(ambig, result)) { + if (result.kind == ambig.kind and + ambig.kind == .normal and + ambig.name_loc.start != 0 and + result.name_loc.start != 0) + { + return .{ + .kind = .ambiguous, + .source_index = result.source_index, + .name_loc = result.name_loc, + .other_source_index = ambig.source_index, + .other_name_loc = ambig.name_loc, + }; + } + + return .{ .kind = .ambiguous }; + } + } + + return result; + } + + pub fn topLevelSymbolsToParts(c: *LinkerContext, id: u32, ref: Ref) []u32 { + return c.graph.topLevelSymbolToParts(id, ref); + } + + pub fn topLevelSymbolsToPartsForRuntime(c: *LinkerContext, ref: Ref) []u32 { + return topLevelSymbolsToParts(c, Index.runtime.get(), ref); + } + + pub fn createWrapperForFile( + c: *LinkerContext, + wrap: WrapKind, + wrapper_ref: Ref, + wrapper_part_index: *Index, + source_index: Index.Int, + ) void { + switch (wrap) { + // If this is a CommonJS file, we're going to need to generate a wrapper + // for the CommonJS closure. That will end up looking something like this: + // + // var require_foo = __commonJS((exports, module) => { + // ... + // }); + // + // However, that generation is special-cased for various reasons and is + // done later on. Still, we're going to need to ensure that this file + // both depends on the "__commonJS" symbol and declares the "require_foo" + // symbol. Instead of special-casing this during the reachability analysis + // below, we just append a dummy part to the end of the file with these + // dependencies and let the general-purpose reachability analysis take care + // of it. + .cjs => { + const common_js_parts = c.topLevelSymbolsToPartsForRuntime(c.cjs_runtime_ref); + + for (common_js_parts) |part_id| { + const runtime_parts = c.graph.ast.items(.parts)[Index.runtime.get()].slice(); + const part: *Part = &runtime_parts[part_id]; + const symbol_refs = part.symbol_uses.keys(); + for (symbol_refs) |ref| { + if (ref.eql(c.cjs_runtime_ref)) continue; + } + } + + // Generate a dummy part that depends on the "__commonJS" symbol. + const dependencies: []js_ast.Dependency = if (c.options.output_format != .internal_bake_dev) brk: { + const dependencies = c.allocator.alloc(js_ast.Dependency, common_js_parts.len) catch bun.outOfMemory(); + for (common_js_parts, dependencies) |part, *cjs| { + cjs.* = .{ + .part_index = part, + .source_index = Index.runtime, + }; + } + break :brk dependencies; + } else &.{}; + var symbol_uses: Part.SymbolUseMap = .empty; + symbol_uses.put(c.allocator, wrapper_ref, .{ .count_estimate = 1 }) catch bun.outOfMemory(); + const part_index = c.graph.addPartToFile( + source_index, + .{ + .stmts = &.{}, + .symbol_uses = symbol_uses, + .declared_symbols = js_ast.DeclaredSymbol.List.fromSlice( + c.allocator, + &[_]js_ast.DeclaredSymbol{ + .{ .ref = c.graph.ast.items(.exports_ref)[source_index], .is_top_level = true }, + .{ .ref = c.graph.ast.items(.module_ref)[source_index], .is_top_level = true }, + .{ .ref = c.graph.ast.items(.wrapper_ref)[source_index], .is_top_level = true }, + }, + ) catch unreachable, + .dependencies = Dependency.List.init(dependencies), + }, + ) catch unreachable; + bun.assert(part_index != js_ast.namespace_export_part_index); + wrapper_part_index.* = Index.part(part_index); + + // Bake uses a wrapping approach that does not use __commonJS + if (c.options.output_format != .internal_bake_dev) { + c.graph.generateSymbolImportAndUse( + source_index, + part_index, + c.cjs_runtime_ref, + 1, + Index.runtime, + ) catch unreachable; + } + }, + + .esm => { + // If this is a lazily-initialized ESM file, we're going to need to + // generate a wrapper for the ESM closure. That will end up looking + // something like this: + // + // var init_foo = __esm(() => { + // ... + // }); + // + // This depends on the "__esm" symbol and declares the "init_foo" symbol + // for similar reasons to the CommonJS closure above. + const esm_parts = if (wrapper_ref.isValid() and c.options.output_format != .internal_bake_dev) + c.topLevelSymbolsToPartsForRuntime(c.esm_runtime_ref) + else + &.{}; + + // generate a dummy part that depends on the "__esm" symbol + const dependencies = c.allocator.alloc(js_ast.Dependency, esm_parts.len) catch unreachable; + for (esm_parts, dependencies) |part, *esm| { + esm.* = .{ + .part_index = part, + .source_index = Index.runtime, + }; + } + + var symbol_uses: Part.SymbolUseMap = .empty; + symbol_uses.put(c.allocator, wrapper_ref, .{ .count_estimate = 1 }) catch bun.outOfMemory(); + const part_index = c.graph.addPartToFile( + source_index, + .{ + .symbol_uses = symbol_uses, + .declared_symbols = js_ast.DeclaredSymbol.List.fromSlice(c.allocator, &[_]js_ast.DeclaredSymbol{ + .{ .ref = wrapper_ref, .is_top_level = true }, + }) catch unreachable, + .dependencies = Dependency.List.init(dependencies), + }, + ) catch unreachable; + bun.assert(part_index != js_ast.namespace_export_part_index); + wrapper_part_index.* = Index.part(part_index); + if (wrapper_ref.isValid() and c.options.output_format != .internal_bake_dev) { + c.graph.generateSymbolImportAndUse( + source_index, + part_index, + c.esm_runtime_ref, + 1, + Index.runtime, + ) catch bun.outOfMemory(); + } + }, + else => {}, + } + } + + pub fn advanceImportTracker(c: *LinkerContext, tracker: *const ImportTracker) ImportTracker.Iterator { + const id = tracker.source_index.get(); + var named_imports: *JSAst.NamedImports = &c.graph.ast.items(.named_imports)[id]; + var import_records = c.graph.ast.items(.import_records)[id]; + const exports_kind: []const js_ast.ExportsKind = c.graph.ast.items(.exports_kind); + const ast_flags = c.graph.ast.items(.flags); + + const named_import: js_ast.NamedImport = named_imports.get(tracker.import_ref) orelse + // TODO: investigate if this is a bug + // It implies there are imports being added without being resolved + return .{ + .value = .{}, + .status = .external, + }; + + // Is this an external file? + const record: *const ImportRecord = import_records.at(named_import.import_record_index); + if (!record.source_index.isValid()) { + return .{ + .value = .{}, + .status = .external, + }; + } + + // Is this a disabled file? + const other_source_index = record.source_index.get(); + const other_id = other_source_index; + + if (other_id > c.graph.ast.len or c.parse_graph.input_files.items(.source)[other_source_index].path.is_disabled) { + return .{ + .value = .{ + .source_index = record.source_index, + }, + .status = .disabled, + }; + } + + const flags = ast_flags[other_id]; + + // Is this a named import of a file without any exports? + if (!named_import.alias_is_star and + flags.has_lazy_export and + + // CommonJS exports + !flags.uses_export_keyword and !strings.eqlComptime(named_import.alias orelse "", "default") and + // ESM exports + !flags.uses_exports_ref and !flags.uses_module_ref) + { + // Just warn about it and replace the import with "undefined" + return .{ + .value = .{ + .source_index = Index.source(other_source_index), + .import_ref = Ref.None, + }, + .status = .cjs_without_exports, + }; + } + const other_kind = exports_kind[other_id]; + // Is this a CommonJS file? + if (other_kind == .cjs) { + return .{ + .value = .{ + .source_index = Index.source(other_source_index), + .import_ref = Ref.None, + }, + .status = .cjs, + }; + } + + // Match this import star with an export star from the imported file + if (named_import.alias_is_star) { + const matching_export = c.graph.meta.items(.resolved_export_star)[other_id]; + if (matching_export.data.import_ref.isValid()) { + // Check to see if this is a re-export of another import + return .{ + .value = matching_export.data, + .status = .found, + .import_data = matching_export.potentially_ambiguous_export_star_refs.slice(), + }; + } + } + + // Match this import up with an export from the imported file + if (c.graph.meta.items(.resolved_exports)[other_id].get(named_import.alias.?)) |matching_export| { + // Check to see if this is a re-export of another import + return .{ + .value = .{ + .source_index = matching_export.data.source_index, + .import_ref = matching_export.data.import_ref, + .name_loc = matching_export.data.name_loc, + }, + .status = .found, + .import_data = matching_export.potentially_ambiguous_export_star_refs.slice(), + }; + } + + // Is this a file with dynamic exports? + const is_commonjs_to_esm = flags.force_cjs_to_esm; + if (other_kind.isESMWithDynamicFallback() or is_commonjs_to_esm) { + return .{ + .value = .{ + .source_index = Index.source(other_source_index), + .import_ref = c.graph.ast.items(.exports_ref)[other_id], + }, + .status = if (is_commonjs_to_esm) + .dynamic_fallback_interop_default + else + .dynamic_fallback, + }; + } + + // Missing re-exports in TypeScript files are indistinguishable from types + const other_loader = c.parse_graph.input_files.items(.loader)[other_id]; + if (named_import.is_exported and other_loader.isTypeScript()) { + return .{ + .value = .{}, + .status = .probably_typescript_type, + }; + } + + return .{ + .value = .{ + .source_index = Index.source(other_source_index), + }, + .status = .no_match, + }; + } + + pub fn matchImportsWithExportsForFile( + c: *LinkerContext, + named_imports_ptr: *JSAst.NamedImports, + imports_to_bind: *RefImportData, + source_index: Index.Int, + ) void { + var named_imports = named_imports_ptr.clone(c.allocator) catch bun.outOfMemory(); + defer named_imports_ptr.* = named_imports; + + const Sorter = struct { + imports: *JSAst.NamedImports, + + pub fn lessThan(self: @This(), a_index: usize, b_index: usize) bool { + const a_ref = self.imports.keys()[a_index]; + const b_ref = self.imports.keys()[b_index]; + + return std.math.order(a_ref.innerIndex(), b_ref.innerIndex()) == .lt; + } + }; + const sorter = Sorter{ + .imports = &named_imports, + }; + named_imports.sort(sorter); + + for (named_imports.keys(), named_imports.values()) |ref, named_import| { + // Re-use memory for the cycle detector + c.cycle_detector.clearRetainingCapacity(); + + const import_ref = ref; + + var re_exports = std.ArrayList(js_ast.Dependency).init(c.allocator); + const result = c.matchImportWithExport(.{ + .source_index = Index.source(source_index), + .import_ref = import_ref, + }, &re_exports); + + switch (result.kind) { + .normal => { + imports_to_bind.put( + c.allocator, + import_ref, + .{ + .re_exports = bun.BabyList(js_ast.Dependency).init(re_exports.items), + .data = .{ + .source_index = Index.source(result.source_index), + .import_ref = result.ref, + }, + }, + ) catch unreachable; + }, + .namespace => { + c.graph.symbols.get(import_ref).?.namespace_alias = js_ast.G.NamespaceAlias{ + .namespace_ref = result.namespace_ref, + .alias = result.alias, + }; + }, + .normal_and_namespace => { + imports_to_bind.put( + c.allocator, + import_ref, + .{ + .re_exports = bun.BabyList(js_ast.Dependency).init(re_exports.items), + .data = .{ + .source_index = Index.source(result.source_index), + .import_ref = result.ref, + }, + }, + ) catch unreachable; + + c.graph.symbols.get(import_ref).?.namespace_alias = js_ast.G.NamespaceAlias{ + .namespace_ref = result.namespace_ref, + .alias = result.alias, + }; + }, + .cycle => { + const source = &c.parse_graph.input_files.items(.source)[source_index]; + const r = lex.rangeOfIdentifier(source, named_import.alias_loc orelse Logger.Loc{}); + c.log.addRangeErrorFmt( + source, + r, + c.allocator, + "Detected cycle while resolving import \"{s}\"", + .{ + named_import.alias.?, + }, + ) catch unreachable; + }, + .probably_typescript_type => { + c.graph.meta.items(.probably_typescript_type)[source_index].put( + c.allocator, + import_ref, + {}, + ) catch unreachable; + }, + .ambiguous => { + const source = &c.parse_graph.input_files.items(.source)[source_index]; + + const r = lex.rangeOfIdentifier(source, named_import.alias_loc orelse Logger.Loc{}); + + // TODO: log locations of the ambiguous exports + + const symbol: *Symbol = c.graph.symbols.get(import_ref).?; + if (symbol.import_item_status == .generated) { + symbol.import_item_status = .missing; + c.log.addRangeWarningFmt( + source, + r, + c.allocator, + "Import \"{s}\" will always be undefined because there are multiple matching exports", + .{ + named_import.alias.?, + }, + ) catch unreachable; + } else { + c.log.addRangeErrorFmt( + source, + r, + c.allocator, + "Ambiguous import \"{s}\" has multiple matching exports", + .{ + named_import.alias.?, + }, + ) catch unreachable; + } + }, + .ignore => {}, + } + } + } + + pub fn breakOutputIntoPieces( + c: *LinkerContext, + allocator: std.mem.Allocator, + j: *StringJoiner, + count: u32, + ) !Chunk.IntermediateOutput { + const trace = bun.perf.trace("Bundler.breakOutputIntoPieces"); + defer trace.end(); + + const OutputPiece = Chunk.OutputPiece; + + if (!j.contains(c.unique_key_prefix)) + // There are like several cases that prohibit this from being checked more trivially, example: + // 1. dynamic imports + // 2. require() + // 3. require.resolve() + // 4. externals + return .{ .joiner = j.* }; + + var pieces = try std.ArrayList(OutputPiece).initCapacity(allocator, count); + const complete_output = try j.done(allocator); + var output = complete_output; + + const prefix = c.unique_key_prefix; + + outer: while (true) { + // Scan for the next piece boundary + const boundary = strings.indexOf(output, prefix) orelse + break; + + // Try to parse the piece boundary + const start = boundary + prefix.len; + if (start + 9 > output.len) { + // Not enough bytes to parse the piece index + break; + } + + const kind: OutputPiece.Query.Kind = switch (output[start]) { + 'A' => .asset, + 'C' => .chunk, + 'S' => .scb, + else => { + if (bun.Environment.isDebug) + bun.Output.debugWarn("Invalid output piece boundary", .{}); + break; + }, + }; + + var index: usize = 0; + for (output[start..][1..9].*) |char| { + if (char < '0' or char > '9') { + if (bun.Environment.isDebug) + bun.Output.debugWarn("Invalid output piece boundary", .{}); + break :outer; + } + + index = (index * 10) + (@as(usize, char) - '0'); + } + + // Validate the boundary + switch (kind) { + .asset, .scb => if (index >= c.graph.files.len) { + if (bun.Environment.isDebug) + bun.Output.debugWarn("Invalid output piece boundary", .{}); + break; + }, + .chunk => if (index >= count) { + if (bun.Environment.isDebug) + bun.Output.debugWarn("Invalid output piece boundary", .{}); + break; + }, + else => unreachable, + } + + try pieces.append(OutputPiece.init(output[0..boundary], .{ + .kind = kind, + .index = @intCast(index), + })); + output = output[boundary + prefix.len + 9 ..]; + } + + try pieces.append(OutputPiece.init(output, OutputPiece.Query.none)); + + return .{ + .pieces = bun.BabyList(Chunk.OutputPiece).init(pieces.items), + }; + } +}; + +const bun = @import("bun"); +const string = bun.string; +const Output = bun.Output; +const Environment = bun.Environment; +const strings = bun.strings; +const MutableString = bun.MutableString; +const FeatureFlags = bun.FeatureFlags; + +const std = @import("std"); +const lex = @import("../js_lexer.zig"); +const Logger = @import("../logger.zig"); +const options = @import("../options.zig"); +const Part = js_ast.Part; +const js_printer = @import("../js_printer.zig"); +const js_ast = @import("../js_ast.zig"); +const linker = @import("../linker.zig"); +const sourcemap = bun.sourcemap; +const StringJoiner = bun.StringJoiner; +const base64 = bun.base64; +pub const Ref = @import("../ast/base.zig").Ref; +pub const ThreadPoolLib = @import("../thread_pool.zig"); +const BabyList = @import("../baby_list.zig").BabyList; +pub const Fs = @import("../fs.zig"); +const _resolver = @import("../resolver/resolver.zig"); +const sync = bun.ThreadPool; +const ImportRecord = bun.ImportRecord; +const runtime = @import("../runtime.zig"); + +const NodeFallbackModules = @import("../node_fallbacks.zig"); +const Resolver = _resolver.Resolver; +const Dependency = js_ast.Dependency; +const JSAst = js_ast.BundledAst; +const Loader = options.Loader; +pub const Index = @import("../ast/base.zig").Index; +const Symbol = js_ast.Symbol; +const EventLoop = bun.JSC.AnyEventLoop; +const MultiArrayList = bun.MultiArrayList; +const Stmt = js_ast.Stmt; +const Expr = js_ast.Expr; +const E = js_ast.E; +const S = js_ast.S; +const G = js_ast.G; +const B = js_ast.B; +const Binding = js_ast.Binding; +const AutoBitSet = bun.bit_set.AutoBitSet; +const renamer = bun.renamer; +const JSC = bun.JSC; +const debugTreeShake = Output.scoped(.TreeShake, true); +const Loc = Logger.Loc; +const bake = bun.bake; +const bundler = bun.bundle_v2; +const BundleV2 = bundler.BundleV2; +const Graph = bundler.Graph; +const LinkerGraph = bundler.LinkerGraph; + +pub const DeferredBatchTask = bun.bundle_v2.DeferredBatchTask; +pub const ThreadPool = bun.bundle_v2.ThreadPool; +pub const ParseTask = bun.bundle_v2.ParseTask; +const ImportTracker = bundler.ImportTracker; +const MangledProps = bundler.MangledProps; +const Chunk = bundler.Chunk; +const ServerComponentBoundary = bundler.ServerComponentBoundary; +const PartRange = bundler.PartRange; +const JSMeta = bundler.JSMeta; +const ExportData = bundler.ExportData; +const EntryPoint = bundler.EntryPoint; +const RefImportData = bundler.RefImportData; +const StableRef = bundler.StableRef; +const CompileResultForSourceMap = bundler.CompileResultForSourceMap; +const ContentHasher = bundler.ContentHasher; +const WrapKind = bundler.WrapKind; +const genericPathWithPrettyInitialized = bundler.genericPathWithPrettyInitialized; +const AdditionalFile = bundler.AdditionalFile; +const logPartDependencyTree = bundler.logPartDependencyTree; diff --git a/src/bundler/LinkerGraph.zig b/src/bundler/LinkerGraph.zig new file mode 100644 index 0000000000..a9af90388b --- /dev/null +++ b/src/bundler/LinkerGraph.zig @@ -0,0 +1,467 @@ +pub const LinkerGraph = @This(); + +const debug = Output.scoped(.LinkerGraph, false); + +files: File.List = .{}, +files_live: BitSet = undefined, +entry_points: EntryPoint.List = .{}, +symbols: js_ast.Symbol.Map = .{}, + +allocator: std.mem.Allocator, + +code_splitting: bool = false, + +// This is an alias from Graph +// it is not a clone! +ast: MultiArrayList(JSAst) = .{}, +meta: MultiArrayList(JSMeta) = .{}, + +/// We should avoid traversing all files in the bundle, because the linker +/// should be able to run a linking operation on a large bundle where only +/// a few files are needed (e.g. an incremental compilation scenario). This +/// holds all files that could possibly be reached through the entry points. +/// If you need to iterate over all files in the linking operation, iterate +/// over this array. This array is also sorted in a deterministic ordering +/// to help ensure deterministic builds (source indices are random). +reachable_files: []Index = &[_]Index{}, + +/// Index from `.parse_graph.input_files` to index in `.files` +stable_source_indices: []const u32 = &[_]u32{}, + +is_scb_bitset: BitSet = .{}, +has_client_components: bool = false, +has_server_components: bool = false, + +/// This is for cross-module inlining of detected inlinable constants +// const_values: js_ast.Ast.ConstValuesMap = .{}, +/// This is for cross-module inlining of TypeScript enum constants +ts_enums: js_ast.Ast.TsEnumsMap = .{}, + +pub fn init(allocator: std.mem.Allocator, file_count: usize) !LinkerGraph { + return LinkerGraph{ + .allocator = allocator, + .files_live = try BitSet.initEmpty(allocator, file_count), + }; +} + +pub fn runtimeFunction(this: *const LinkerGraph, name: string) Ref { + return this.ast.items(.named_exports)[Index.runtime.value].get(name).?.ref; +} + +pub fn generateNewSymbol(this: *LinkerGraph, source_index: u32, kind: Symbol.Kind, original_name: string) Ref { + const source_symbols = &this.symbols.symbols_for_source.slice()[source_index]; + + var ref = Ref.init( + @truncate(source_symbols.len), + @truncate(source_index), + false, + ); + ref.tag = .symbol; + + // TODO: will this crash on resize due to using threadlocal mimalloc heap? + source_symbols.push( + this.allocator, + .{ + .kind = kind, + .original_name = original_name, + }, + ) catch unreachable; + + this.ast.items(.module_scope)[source_index].generated.push(this.allocator, ref) catch unreachable; + return ref; +} + +pub fn generateRuntimeSymbolImportAndUse( + graph: *LinkerGraph, + source_index: Index.Int, + entry_point_part_index: Index, + name: []const u8, + count: u32, +) !void { + if (count == 0) return; + debug("generateRuntimeSymbolImportAndUse({s}) for {d}", .{ name, source_index }); + + const ref = graph.runtimeFunction(name); + try graph.generateSymbolImportAndUse( + source_index, + entry_point_part_index.get(), + ref, + count, + Index.runtime, + ); +} + +pub fn addPartToFile( + graph: *LinkerGraph, + id: u32, + part: Part, +) !u32 { + var parts: *Part.List = &graph.ast.items(.parts)[id]; + const part_id = @as(u32, @truncate(parts.len)); + try parts.push(graph.allocator, part); + var top_level_symbol_to_parts_overlay: ?*TopLevelSymbolToParts = null; + + const Iterator = struct { + graph: *LinkerGraph, + id: u32, + top_level_symbol_to_parts_overlay: *?*TopLevelSymbolToParts, + part_id: u32, + + pub fn next(self: *@This(), ref: Ref) void { + var overlay = brk: { + if (self.top_level_symbol_to_parts_overlay.*) |out| { + break :brk out; + } + + const out = &self.graph.meta.items(.top_level_symbol_to_parts_overlay)[self.id]; + + self.top_level_symbol_to_parts_overlay.* = out; + break :brk out; + }; + + var entry = overlay.getOrPut(self.graph.allocator, ref) catch unreachable; + if (!entry.found_existing) { + if (self.graph.ast.items(.top_level_symbols_to_parts)[self.id].get(ref)) |original_parts| { + var list = std.ArrayList(u32).init(self.graph.allocator); + list.ensureTotalCapacityPrecise(original_parts.len + 1) catch unreachable; + list.appendSliceAssumeCapacity(original_parts.slice()); + list.appendAssumeCapacity(self.part_id); + + entry.value_ptr.* = .init(list.items); + } else { + entry.value_ptr.* = BabyList(u32).fromSlice(self.graph.allocator, &.{self.part_id}) catch bun.outOfMemory(); + } + } else { + entry.value_ptr.push(self.graph.allocator, self.part_id) catch unreachable; + } + } + }; + + var ctx = Iterator{ + .graph = graph, + .id = id, + .part_id = part_id, + .top_level_symbol_to_parts_overlay = &top_level_symbol_to_parts_overlay, + }; + + js_ast.DeclaredSymbol.forEachTopLevelSymbol(&parts.ptr[part_id].declared_symbols, &ctx, Iterator.next); + + return part_id; +} + +pub fn generateSymbolImportAndUse( + g: *LinkerGraph, + source_index: u32, + part_index: u32, + ref: Ref, + use_count: u32, + source_index_to_import_from: Index, +) !void { + if (use_count == 0) return; + + var parts_list = g.ast.items(.parts)[source_index].slice(); + var part: *Part = &parts_list[part_index]; + + // Mark this symbol as used by this part + + var uses = &part.symbol_uses; + var uses_entry = uses.getOrPut(g.allocator, ref) catch unreachable; + + if (!uses_entry.found_existing) { + uses_entry.value_ptr.* = .{ .count_estimate = use_count }; + } else { + uses_entry.value_ptr.count_estimate += use_count; + } + + const exports_ref = g.ast.items(.exports_ref)[source_index]; + const module_ref = g.ast.items(.module_ref)[source_index]; + if (!exports_ref.isNull() and ref.eql(exports_ref)) { + g.ast.items(.flags)[source_index].uses_exports_ref = true; + } + + if (!module_ref.isNull() and ref.eql(module_ref)) { + g.ast.items(.flags)[source_index].uses_module_ref = true; + } + + // null ref shouldn't be there. + bun.assert(!ref.isEmpty()); + + // Track that this specific symbol was imported + if (source_index_to_import_from.get() != source_index) { + const imports_to_bind = &g.meta.items(.imports_to_bind)[source_index]; + try imports_to_bind.put(g.allocator, ref, .{ + .data = .{ + .source_index = source_index_to_import_from, + .import_ref = ref, + }, + }); + } + + // Pull in all parts that declare this symbol + var dependencies = &part.dependencies; + const part_ids = g.topLevelSymbolToParts(source_index_to_import_from.get(), ref); + const new_dependencies = try dependencies.writableSlice(g.allocator, part_ids.len); + for (part_ids, new_dependencies) |part_id, *dependency| { + dependency.* = .{ + .source_index = source_index_to_import_from, + .part_index = @as(u32, @truncate(part_id)), + }; + } +} + +pub fn topLevelSymbolToParts(g: *LinkerGraph, id: u32, ref: Ref) []u32 { + if (g.meta.items(.top_level_symbol_to_parts_overlay)[id].get(ref)) |overlay| { + return overlay.slice(); + } + + if (g.ast.items(.top_level_symbols_to_parts)[id].get(ref)) |list| { + return list.slice(); + } + + return &.{}; +} + +pub fn load( + this: *LinkerGraph, + entry_points: []const Index, + sources: []const Logger.Source, + server_component_boundaries: ServerComponentBoundary.List, + dynamic_import_entry_points: []const Index.Int, +) !void { + const scb = server_component_boundaries.slice(); + try this.files.setCapacity(this.allocator, sources.len); + this.files.zero(); + this.files_live = try BitSet.initEmpty( + this.allocator, + sources.len, + ); + this.files.len = sources.len; + var files = this.files.slice(); + + var entry_point_kinds = files.items(.entry_point_kind); + { + const kinds = std.mem.sliceAsBytes(entry_point_kinds); + @memset(kinds, 0); + } + + // Setup entry points + { + try this.entry_points.setCapacity(this.allocator, entry_points.len + server_component_boundaries.list.len + dynamic_import_entry_points.len); + this.entry_points.len = entry_points.len; + const source_indices = this.entry_points.items(.source_index); + + const path_strings: []bun.PathString = this.entry_points.items(.output_path); + { + const output_was_auto_generated = std.mem.sliceAsBytes(this.entry_points.items(.output_path_was_auto_generated)); + @memset(output_was_auto_generated, 0); + } + + for (entry_points, path_strings, source_indices) |i, *path_string, *source_index| { + const source = sources[i.get()]; + if (comptime Environment.allow_assert) { + bun.assert(source.index.get() == i.get()); + } + entry_point_kinds[source.index.get()] = EntryPoint.Kind.user_specified; + path_string.* = bun.PathString.init(source.path.text); + source_index.* = source.index.get(); + } + + for (dynamic_import_entry_points) |id| { + bun.assert(this.code_splitting); // this should never be a thing without code splitting + + if (entry_point_kinds[id] != .none) { + // You could dynamic import a file that is already an entry point + continue; + } + + const source = &sources[id]; + entry_point_kinds[id] = EntryPoint.Kind.dynamic_import; + + this.entry_points.appendAssumeCapacity(.{ + .source_index = id, + .output_path = bun.PathString.init(source.path.text), + .output_path_was_auto_generated = true, + }); + } + + var import_records_list: []ImportRecord.List = this.ast.items(.import_records); + try this.meta.setCapacity(this.allocator, import_records_list.len); + this.meta.len = this.ast.len; + this.meta.zero(); + + if (scb.list.len > 0) { + this.is_scb_bitset = BitSet.initEmpty(this.allocator, this.files.len) catch unreachable; + + // Index all SCBs into the bitset. This is needed so chunking + // can track the chunks that SCBs belong to. + for (scb.list.items(.use_directive), scb.list.items(.source_index), scb.list.items(.reference_source_index)) |use, original_id, ref_id| { + switch (use) { + .none => {}, + .client => { + this.is_scb_bitset.set(original_id); + this.is_scb_bitset.set(ref_id); + }, + .server => { + bun.todoPanic(@src(), "um", .{}); + }, + } + } + + // For client components, the import record index currently points to the original source index, instead of the reference source index. + for (this.reachable_files) |source_id| { + for (import_records_list[source_id.get()].slice()) |*import_record| { + if (import_record.source_index.isValid() and this.is_scb_bitset.isSet(import_record.source_index.get())) { + import_record.source_index = Index.init( + scb.getReferenceSourceIndex(import_record.source_index.get()) orelse + // If this gets hit, might be fine to switch this to `orelse continue` + // not confident in this assertion + Output.panic("Missing SCB boundary for file #{d}", .{import_record.source_index.get()}), + ); + bun.assert(import_record.source_index.isValid()); // did not generate + } + } + } + } else { + this.is_scb_bitset = .{}; + } + } + + // Setup files + { + var stable_source_indices = try this.allocator.alloc(Index, sources.len + 1); + + // set it to max value so that if we access an invalid one, it crashes + @memset(std.mem.sliceAsBytes(stable_source_indices), 255); + + for (this.reachable_files, 0..) |source_index, i| { + stable_source_indices[source_index.get()] = Index.source(i); + } + + @memset( + files.items(.distance_from_entry_point), + (LinkerGraph.File{}).distance_from_entry_point, + ); + this.stable_source_indices = @as([]const u32, @ptrCast(stable_source_indices)); + } + + { + var input_symbols = js_ast.Symbol.Map.initList(js_ast.Symbol.NestedList.init(this.ast.items(.symbols))); + var symbols = input_symbols.symbols_for_source.clone(this.allocator) catch bun.outOfMemory(); + for (symbols.slice(), input_symbols.symbols_for_source.slice()) |*dest, src| { + dest.* = src.clone(this.allocator) catch bun.outOfMemory(); + } + this.symbols = js_ast.Symbol.Map.initList(symbols); + } + + // TODO: const_values + // { + // var const_values = this.const_values; + // var count: usize = 0; + + // for (this.ast.items(.const_values)) |const_value| { + // count += const_value.count(); + // } + + // if (count > 0) { + // try const_values.ensureTotalCapacity(this.allocator, count); + // for (this.ast.items(.const_values)) |const_value| { + // for (const_value.keys(), const_value.values()) |key, value| { + // const_values.putAssumeCapacityNoClobber(key, value); + // } + // } + // } + + // this.const_values = const_values; + // } + + { + var count: usize = 0; + for (this.ast.items(.ts_enums)) |ts_enums| { + count += ts_enums.count(); + } + if (count > 0) { + try this.ts_enums.ensureTotalCapacity(this.allocator, count); + for (this.ast.items(.ts_enums)) |ts_enums| { + for (ts_enums.keys(), ts_enums.values()) |key, value| { + this.ts_enums.putAssumeCapacityNoClobber(key, value); + } + } + } + } + + const src_named_exports: []js_ast.Ast.NamedExports = this.ast.items(.named_exports); + const dest_resolved_exports: []ResolvedExports = this.meta.items(.resolved_exports); + for (src_named_exports, dest_resolved_exports, 0..) |src, *dest, source_index| { + var resolved = ResolvedExports{}; + resolved.ensureTotalCapacity(this.allocator, src.count()) catch unreachable; + for (src.keys(), src.values()) |key, value| { + resolved.putAssumeCapacityNoClobber(key, .{ .data = .{ + .import_ref = value.ref, + .name_loc = value.alias_loc, + .source_index = Index.source(source_index), + } }); + } + dest.* = resolved; + } +} + +pub const File = struct { + entry_bits: AutoBitSet = undefined, + + input_file: Index = Index.source(0), + + /// The minimum number of links in the module graph to get from an entry point + /// to this file + distance_from_entry_point: u32 = std.math.maxInt(u32), + + /// This file is an entry point if and only if this is not ".none". + /// Note that dynamically-imported files are allowed to also be specified by + /// the user as top-level entry points, so some dynamically-imported files + /// may be ".user_specified" instead of ".dynamic_import". + entry_point_kind: EntryPoint.Kind = .none, + + /// If "entry_point_kind" is not ".none", this is the index of the + /// corresponding entry point chunk. + /// + /// This is also initialized for files that are a SCB's generated + /// reference, pointing to its destination. This forms a lookup map from + /// a Source.Index to its output path inb reakOutputIntoPieces + entry_point_chunk_index: u32 = std.math.maxInt(u32), + + line_offset_table: bun.sourcemap.LineOffsetTable.List = .empty, + quoted_source_contents: string = "", + + pub fn isEntryPoint(this: *const File) bool { + return this.entry_point_kind.isEntryPoint(); + } + + pub fn isUserSpecifiedEntryPoint(this: *const File) bool { + return this.entry_point_kind.isUserSpecifiedEntryPoint(); + } + + pub const List = MultiArrayList(File); +}; + +const bun = @import("bun"); +const Environment = bun.Environment; +const std = @import("std"); +const string = bun.string; +const Output = bun.Output; +const BitSet = bun.bit_set.DynamicBitSetUnmanaged; +const BabyList = bun.BabyList; + +const Logger = bun.bundle_v2.Logger; +const TopLevelSymbolToParts = bun.bundle_v2.TopLevelSymbolToParts; +const Index = bun.bundle_v2.Index; +const Part = bun.bundle_v2.Part; +const Ref = bun.bundle_v2.Ref; +const EntryPoint = bun.bundle_v2.EntryPoint; +const ServerComponentBoundary = bun.bundle_v2.ServerComponentBoundary; +const MultiArrayList = bun.MultiArrayList; +const JSAst = bun.bundle_v2.JSAst; +const JSMeta = bun.bundle_v2.JSMeta; +const js_ast = @import("../js_ast.zig"); +const Symbol = @import("../js_ast.zig").Symbol; +const ImportRecord = bun.ImportRecord; +const ResolvedExports = bun.bundle_v2.ResolvedExports; +const AutoBitSet = bun.bit_set.AutoBitSet; diff --git a/src/bundler/ParseTask.zig b/src/bundler/ParseTask.zig new file mode 100644 index 0000000000..37419eae0e --- /dev/null +++ b/src/bundler/ParseTask.zig @@ -0,0 +1,1446 @@ +pub const ContentsOrFd = union(enum) { + fd: struct { + dir: StoredFileDescriptorType, + file: StoredFileDescriptorType, + }, + contents: string, + + const Tag = @typeInfo(ContentsOrFd).@"union".tag_type.?; +}; + +pub const ParseTask = @This(); + +path: Fs.Path, +secondary_path_for_commonjs_interop: ?Fs.Path = null, +contents_or_fd: ContentsOrFd, +external_free_function: CacheEntry.ExternalFreeFunction = .none, +side_effects: _resolver.SideEffects, +loader: ?Loader = null, +jsx: options.JSX.Pragma, +source_index: Index = Index.invalid, +task: ThreadPoolLib.Task = .{ .callback = &taskCallback }, + +// Split this into a different task so that we don't accidentally run the +// tasks for io on the threads that are meant for parsing. +io_task: ThreadPoolLib.Task = .{ .callback = &ioTaskCallback }, + +// Used for splitting up the work between the io and parse steps. +stage: ParseTaskStage = .needs_source_code, + +tree_shaking: bool = false, +known_target: options.Target, +module_type: options.ModuleType = .unknown, +emit_decorator_metadata: bool = false, +ctx: *BundleV2, +package_version: string = "", +is_entry_point: bool = false, +/// This is set when the file is an entrypoint, and it has an onLoad plugin. +/// In this case we want to defer adding this to additional_files until after +/// the onLoad plugin has finished. +defer_copy_for_bundling: bool = false, + +const ParseTaskStage = union(enum) { + needs_source_code: void, + needs_parse: CacheEntry, +}; + +/// The information returned to the Bundler thread when a parse finishes. +pub const Result = struct { + task: EventLoop.Task, + ctx: *BundleV2, + value: Value, + watcher_data: WatcherData, + /// This is used for native onBeforeParsePlugins to store + /// a function pointer and context pointer to free the + /// returned source code by the plugin. + external: CacheEntry.ExternalFreeFunction = .none, + + pub const Value = union(enum) { + success: Success, + err: Error, + empty: struct { + source_index: Index, + }, + }; + + const WatcherData = struct { + fd: bun.StoredFileDescriptorType, + dir_fd: bun.StoredFileDescriptorType, + + /// When no files to watch, this encoding is used. + pub const none: WatcherData = .{ + .fd = bun.invalid_fd, + .dir_fd = bun.invalid_fd, + }; + }; + + pub const Success = struct { + ast: JSAst, + source: Logger.Source, + log: Logger.Log, + use_directive: UseDirective, + side_effects: _resolver.SideEffects, + + /// Used by "file" loader files. + unique_key_for_additional_file: []const u8 = "", + /// Used by "file" loader files. + content_hash_for_additional_file: u64 = 0, + + loader: Loader, + }; + + pub const Error = struct { + err: anyerror, + step: Step, + log: Logger.Log, + target: options.Target, + source_index: Index, + + pub const Step = enum { + pending, + read_file, + parse, + resolve, + }; + }; +}; + +const debug = Output.scoped(.ParseTask, true); + +pub fn init(resolve_result: *const _resolver.Result, source_index: Index, ctx: *BundleV2) ParseTask { + return .{ + .ctx = ctx, + .path = resolve_result.path_pair.primary, + .contents_or_fd = .{ + .fd = .{ + .dir = resolve_result.dirname_fd, + .file = resolve_result.file_fd, + }, + }, + .side_effects = resolve_result.primary_side_effects_data, + .jsx = resolve_result.jsx, + .source_index = source_index, + .module_type = resolve_result.module_type, + .emit_decorator_metadata = resolve_result.emit_decorator_metadata, + .package_version = if (resolve_result.package_json) |package_json| package_json.version else "", + .known_target = ctx.transpiler.options.target, + }; +} + +const RuntimeSource = struct { + parse_task: ParseTask, + source: Logger.Source, +}; + +fn getRuntimeSourceComptime(comptime target: options.Target) RuntimeSource { + // When the `require` identifier is visited, it is replaced with e_require_call_target + // and then that is either replaced with the module itself, or an import to the + // runtime here. + const runtime_require = switch (target) { + // Previously, Bun inlined `import.meta.require` at all usages. This broke + // code that called `fn.toString()` and parsed the code outside a module + // context. + .bun, .bun_macro => + \\export var __require = import.meta.require; + , + + .node => + \\import { createRequire } from "node:module"; + \\export var __require = /* @__PURE__ */ createRequire(import.meta.url); + \\ + , + + // Copied from esbuild's runtime.go: + // + // > This fallback "require" function exists so that "typeof require" can + // > naturally be "function" even in non-CommonJS environments since esbuild + // > emulates a CommonJS environment (issue #1202). However, people want this + // > shim to fall back to "globalThis.require" even if it's defined later + // > (including property accesses such as "require.resolve") so we need to + // > use a proxy (issue #1614). + // + // When bundling to node, esbuild picks this code path as well, but `globalThis.require` + // is not always defined there. The `createRequire` call approach is more reliable. + else => + \\export var __require = /* @__PURE__ */ (x => + \\ typeof require !== 'undefined' ? require : + \\ typeof Proxy !== 'undefined' ? new Proxy(x, { + \\ get: (a, b) => (typeof require !== 'undefined' ? require : a)[b] + \\ }) : x + \\)(function (x) { + \\ if (typeof require !== 'undefined') return require.apply(this, arguments) + \\ throw Error('Dynamic require of "' + x + '" is not supported') + \\}); + \\ + }; + const runtime_using_symbols = switch (target) { + // bun's webkit has Symbol.asyncDispose, Symbol.dispose, and SuppressedError, but not the syntax support + .bun => + \\export var __using = (stack, value, async) => { + \\ if (value != null) { + \\ if (typeof value !== 'object' && typeof value !== 'function') throw TypeError('Object expected to be assigned to "using" declaration') + \\ let dispose + \\ if (async) dispose = value[Symbol.asyncDispose] + \\ if (dispose === void 0) dispose = value[Symbol.dispose] + \\ if (typeof dispose !== 'function') throw TypeError('Object not disposable') + \\ stack.push([async, dispose, value]) + \\ } else if (async) { + \\ stack.push([async]) + \\ } + \\ return value + \\} + \\ + \\export var __callDispose = (stack, error, hasError) => { + \\ let fail = e => error = hasError ? new SuppressedError(e, error, 'An error was suppressed during disposal') : (hasError = true, e) + \\ , next = (it) => { + \\ while (it = stack.pop()) { + \\ try { + \\ var result = it[1] && it[1].call(it[2]) + \\ if (it[0]) return Promise.resolve(result).then(next, (e) => (fail(e), next())) + \\ } catch (e) { + \\ fail(e) + \\ } + \\ } + \\ if (hasError) throw error + \\ } + \\ return next() + \\} + \\ + , + // Other platforms may or may not have the symbol or errors + // The definitions of __dispose and __asyncDispose match what esbuild's __wellKnownSymbol() helper does + else => + \\var __dispose = Symbol.dispose || /* @__PURE__ */ Symbol.for('Symbol.dispose'); + \\var __asyncDispose = Symbol.asyncDispose || /* @__PURE__ */ Symbol.for('Symbol.asyncDispose'); + \\ + \\export var __using = (stack, value, async) => { + \\ if (value != null) { + \\ if (typeof value !== 'object' && typeof value !== 'function') throw TypeError('Object expected to be assigned to "using" declaration') + \\ var dispose + \\ if (async) dispose = value[__asyncDispose] + \\ if (dispose === void 0) dispose = value[__dispose] + \\ if (typeof dispose !== 'function') throw TypeError('Object not disposable') + \\ stack.push([async, dispose, value]) + \\ } else if (async) { + \\ stack.push([async]) + \\ } + \\ return value + \\} + \\ + \\export var __callDispose = (stack, error, hasError) => { + \\ var E = typeof SuppressedError === 'function' ? SuppressedError : + \\ function (e, s, m, _) { return _ = Error(m), _.name = 'SuppressedError', _.error = e, _.suppressed = s, _ }, + \\ fail = e => error = hasError ? new E(e, error, 'An error was suppressed during disposal') : (hasError = true, e), + \\ next = (it) => { + \\ while (it = stack.pop()) { + \\ try { + \\ var result = it[1] && it[1].call(it[2]) + \\ if (it[0]) return Promise.resolve(result).then(next, (e) => (fail(e), next())) + \\ } catch (e) { + \\ fail(e) + \\ } + \\ } + \\ if (hasError) throw error + \\ } + \\ return next() + \\} + \\ + }; + const runtime_code = @embedFile("../runtime.js") ++ runtime_require ++ runtime_using_symbols; + + const parse_task = ParseTask{ + .ctx = undefined, + .path = Fs.Path.initWithNamespace("runtime", "bun:runtime"), + .side_effects = .no_side_effects__pure_data, + .jsx = .{ + .parse = false, + }, + .contents_or_fd = .{ + .contents = runtime_code, + }, + .source_index = Index.runtime, + .loader = .js, + .known_target = target, + }; + const source = Logger.Source{ + .path = parse_task.path, + .contents = parse_task.contents_or_fd.contents, + .index = Index.runtime, + }; + return .{ .parse_task = parse_task, .source = source }; +} + +pub fn getRuntimeSource(target: options.Target) RuntimeSource { + return switch (target) { + inline else => |t| comptime getRuntimeSourceComptime(t), + }; +} + +threadlocal var override_file_path_buf: bun.PathBuffer = undefined; + +fn getEmptyCSSAST( + log: *Logger.Log, + transpiler: *Transpiler, + opts: js_parser.Parser.Options, + allocator: std.mem.Allocator, + source: Logger.Source, +) !JSAst { + const root = Expr.init(E.Object, E.Object{}, Logger.Loc{ .start = 0 }); + var ast = JSAst.init((try js_parser.newLazyExportAST(allocator, transpiler.options.define, opts, log, root, &source, "")).?); + ast.css = bun.create(allocator, bun.css.BundlerStyleSheet, bun.css.BundlerStyleSheet.empty(allocator)); + return ast; +} + +fn getEmptyAST(log: *Logger.Log, transpiler: *Transpiler, opts: js_parser.Parser.Options, allocator: std.mem.Allocator, source: Logger.Source, comptime RootType: type) !JSAst { + const root = Expr.init(RootType, RootType{}, Logger.Loc.Empty); + return JSAst.init((try js_parser.newLazyExportAST(allocator, transpiler.options.define, opts, log, root, &source, "")).?); +} + +const FileLoaderHash = struct { + key: []const u8, + content_hash: u64, +}; + +fn getAST( + log: *Logger.Log, + transpiler: *Transpiler, + opts: js_parser.Parser.Options, + allocator: std.mem.Allocator, + resolver: *Resolver, + source: Logger.Source, + loader: Loader, + unique_key_prefix: u64, + unique_key_for_additional_file: *FileLoaderHash, + has_any_css_locals: *std.atomic.Value(u32), +) !JSAst { + switch (loader) { + .jsx, .tsx, .js, .ts => { + const trace = bun.perf.trace("Bundler.ParseJS"); + defer trace.end(); + return if (try resolver.caches.js.parse( + transpiler.allocator, + opts, + transpiler.options.define, + log, + &source, + )) |res| + JSAst.init(res.ast) + else switch (opts.module_type == .esm) { + inline else => |as_undefined| try getEmptyAST( + log, + transpiler, + opts, + allocator, + source, + if (as_undefined) E.Undefined else E.Object, + ), + }; + }, + .json, .jsonc => |v| { + const trace = bun.perf.trace("Bundler.ParseJSON"); + defer trace.end(); + const root = (try resolver.caches.json.parseJSON(log, source, allocator, if (v == .jsonc) .jsonc else .json, true)) orelse Expr.init(E.Object, E.Object{}, Logger.Loc.Empty); + return JSAst.init((try js_parser.newLazyExportAST(allocator, transpiler.options.define, opts, log, root, &source, "")).?); + }, + .toml => { + const trace = bun.perf.trace("Bundler.ParseTOML"); + defer trace.end(); + var temp_log = bun.logger.Log.init(allocator); + defer { + temp_log.cloneToWithRecycled(log, true) catch bun.outOfMemory(); + temp_log.msgs.clearAndFree(); + } + const root = try TOML.parse(&source, &temp_log, allocator, false); + return JSAst.init((try js_parser.newLazyExportAST(allocator, transpiler.options.define, opts, &temp_log, root, &source, "")).?); + }, + .text => { + const root = Expr.init(E.String, E.String{ + .data = source.contents, + }, Logger.Loc{ .start = 0 }); + var ast = JSAst.init((try js_parser.newLazyExportAST(allocator, transpiler.options.define, opts, log, root, &source, "")).?); + ast.addUrlForCss(allocator, &source, "text/plain", null); + return ast; + }, + + .sqlite_embedded, .sqlite => { + if (!transpiler.options.target.isBun()) { + log.addError( + &source, + Logger.Loc.Empty, + "To use the \"sqlite\" loader, set target to \"bun\"", + ) catch bun.outOfMemory(); + return error.ParserError; + } + + const path_to_use = brk: { + // Implements embedded sqlite + if (loader == .sqlite_embedded) { + const embedded_path = std.fmt.allocPrint(allocator, "{any}A{d:0>8}", .{ bun.fmt.hexIntLower(unique_key_prefix), source.index.get() }) catch unreachable; + unique_key_for_additional_file.* = .{ + .key = embedded_path, + .content_hash = ContentHasher.run(source.contents), + }; + break :brk embedded_path; + } + + break :brk source.path.text; + }; + + // This injects the following code: + // + // import.meta.require(unique_key).db + // + const import_path = Expr.init(E.String, E.String{ + .data = path_to_use, + }, Logger.Loc{ .start = 0 }); + + const import_meta = Expr.init(E.ImportMeta, E.ImportMeta{}, Logger.Loc{ .start = 0 }); + const require_property = Expr.init(E.Dot, E.Dot{ + .target = import_meta, + .name_loc = Logger.Loc.Empty, + .name = "require", + }, Logger.Loc{ .start = 0 }); + const require_args = allocator.alloc(Expr, 2) catch unreachable; + require_args[0] = import_path; + const object_properties = allocator.alloc(G.Property, 1) catch unreachable; + object_properties[0] = G.Property{ + .key = Expr.init(E.String, E.String{ + .data = "type", + }, Logger.Loc{ .start = 0 }), + .value = Expr.init(E.String, E.String{ + .data = "sqlite", + }, Logger.Loc{ .start = 0 }), + }; + require_args[1] = Expr.init(E.Object, E.Object{ + .properties = G.Property.List.init(object_properties), + .is_single_line = true, + }, Logger.Loc{ .start = 0 }); + const require_call = Expr.init(E.Call, E.Call{ + .target = require_property, + .args = BabyList(Expr).init(require_args), + }, Logger.Loc{ .start = 0 }); + + const root = Expr.init(E.Dot, E.Dot{ + .target = require_call, + .name_loc = Logger.Loc.Empty, + .name = "db", + }, Logger.Loc{ .start = 0 }); + + return JSAst.init((try js_parser.newLazyExportAST(allocator, transpiler.options.define, opts, log, root, &source, "")).?); + }, + .napi => { + // (dap-eval-cb "source.contents.ptr") + if (transpiler.options.target == .browser) { + log.addError( + &source, + Logger.Loc.Empty, + "Loading .node files won't work in the browser. Make sure to set target to \"bun\" or \"node\"", + ) catch bun.outOfMemory(); + return error.ParserError; + } + + const unique_key = std.fmt.allocPrint(allocator, "{any}A{d:0>8}", .{ bun.fmt.hexIntLower(unique_key_prefix), source.index.get() }) catch unreachable; + // This injects the following code: + // + // require(unique_key) + // + const import_path = Expr.init(E.String, E.String{ + .data = unique_key, + }, Logger.Loc{ .start = 0 }); + + const require_args = allocator.alloc(Expr, 1) catch unreachable; + require_args[0] = import_path; + + const root = Expr.init(E.Call, E.Call{ + .target = .{ .data = .{ .e_require_call_target = {} }, .loc = .{ .start = 0 } }, + .args = BabyList(Expr).init(require_args), + }, Logger.Loc{ .start = 0 }); + + unique_key_for_additional_file.* = .{ + .key = unique_key, + .content_hash = ContentHasher.run(source.contents), + }; + return JSAst.init((try js_parser.newLazyExportAST(allocator, transpiler.options.define, opts, log, root, &source, "")).?); + }, + .html => { + var scanner = HTMLScanner.init(allocator, log, &source); + try scanner.scan(source.contents); + + // Reuse existing code for creating the AST + // because it handles the various Ref and other structs we + // need in order to print code later. + var ast = (try js_parser.newLazyExportAST( + allocator, + transpiler.options.define, + opts, + log, + Expr.init(E.Missing, E.Missing{}, Logger.Loc.Empty), + &source, + "", + )).?; + ast.import_records = scanner.import_records; + + // We're banning import default of html loader files for now. + // + // TLDR: it kept including: + // + // var name_default = ...; + // + // in the bundle because of the exports AST, and + // gave up on figuring out how to fix it so that + // this feature could ship. + ast.has_lazy_export = false; + ast.parts.ptr[1] = .{ + .stmts = &.{}, + .is_live = true, + .import_record_indices = brk2: { + // Generate a single part that depends on all the import records. + // This is to ensure that we generate a JavaScript bundle containing all the user's code. + var import_record_indices = try Part.ImportRecordIndices.initCapacity(allocator, scanner.import_records.len); + import_record_indices.len = @truncate(scanner.import_records.len); + for (import_record_indices.slice(), 0..) |*import_record, index| { + import_record.* = @intCast(index); + } + break :brk2 import_record_indices; + }, + }; + + // Try to avoid generating unnecessary ESM <> CJS wrapper code. + if (opts.output_format == .esm or opts.output_format == .iife) { + ast.exports_kind = .esm; + } + + return JSAst.init(ast); + }, + .css => { + // make css ast + var import_records = BabyList(ImportRecord){}; + const source_code = source.contents; + var temp_log = bun.logger.Log.init(allocator); + defer { + temp_log.appendToMaybeRecycled(log, &source) catch bun.outOfMemory(); + } + + const css_module_suffix = ".module.css"; + const enable_css_modules = source.path.pretty.len > css_module_suffix.len and + strings.eqlComptime(source.path.pretty[source.path.pretty.len - css_module_suffix.len ..], css_module_suffix); + const parser_options = if (enable_css_modules) init: { + var parseropts = bun.css.ParserOptions.default(allocator, &temp_log); + parseropts.filename = bun.path.basename(source.path.pretty); + parseropts.css_modules = bun.css.CssModuleConfig{}; + break :init parseropts; + } else bun.css.ParserOptions.default(allocator, &temp_log); + + var css_ast, var extra = switch (bun.css.BundlerStyleSheet.parseBundler( + allocator, + source_code, + parser_options, + &import_records, + source.index, + )) { + .result => |v| v, + .err => |e| { + try e.addToLogger(&temp_log, &source, allocator); + return error.SyntaxError; + }, + }; + // Make sure the css modules local refs have a valid tag + if (comptime bun.Environment.isDebug) { + if (css_ast.local_scope.count() > 0) { + for (css_ast.local_scope.values()) |entry| { + const ref = entry.ref; + bun.assert(ref.innerIndex() < extra.symbols.len); + } + } + } + if (css_ast.minify(allocator, bun.css.MinifyOptions{ + .targets = bun.css.Targets.forBundlerTarget(transpiler.options.target), + .unused_symbols = .{}, + }, &extra).asErr()) |e| { + try e.addToLogger(&temp_log, &source, allocator); + return error.MinifyError; + } + if (css_ast.local_scope.count() > 0) { + _ = has_any_css_locals.fetchAdd(1, .monotonic); + } + // If this is a css module, the final exports object wil be set in `generateCodeForLazyExport`. + const root = Expr.init(E.Object, E.Object{}, Logger.Loc{ .start = 0 }); + const css_ast_heap = bun.create(allocator, bun.css.BundlerStyleSheet, css_ast); + var ast = JSAst.init((try js_parser.newLazyExportASTImpl(allocator, transpiler.options.define, opts, &temp_log, root, &source, "", extra.symbols)).?); + ast.css = css_ast_heap; + ast.import_records = import_records; + return ast; + }, + // TODO: + .dataurl, .base64, .bunsh => { + return try getEmptyAST(log, transpiler, opts, allocator, source, E.String); + }, + .file, .wasm => { + bun.assert(loader.shouldCopyForBundling()); + + // Put a unique key in the AST to implement the URL loader. At the end + // of the bundle, the key is replaced with the actual URL. + const content_hash = ContentHasher.run(source.contents); + + const unique_key: []const u8 = if (transpiler.options.dev_server != null) + // With DevServer, the actual URL is added now, since it can be + // known this far ahead of time, and it means the unique key code + // does not have to perform an additional pass over files. + // + // To avoid a mutex, the actual insertion of the asset to DevServer + // is done on the bundler thread. + try std.fmt.allocPrint( + allocator, + bun.bake.DevServer.asset_prefix ++ "/{s}{s}", + .{ + &std.fmt.bytesToHex(std.mem.asBytes(&content_hash), .lower), + std.fs.path.extension(source.path.text), + }, + ) + else + try std.fmt.allocPrint( + allocator, + "{any}A{d:0>8}", + .{ bun.fmt.hexIntLower(unique_key_prefix), source.index.get() }, + ); + const root = Expr.init(E.String, .{ .data = unique_key }, .{ .start = 0 }); + unique_key_for_additional_file.* = .{ + .key = unique_key, + .content_hash = content_hash, + }; + var ast = JSAst.init((try js_parser.newLazyExportAST(allocator, transpiler.options.define, opts, log, root, &source, "")).?); + ast.addUrlForCss(allocator, &source, null, unique_key); + return ast; + }, + } +} + +fn getCodeForParseTaskWithoutPlugins( + task: *ParseTask, + log: *Logger.Log, + transpiler: *Transpiler, + resolver: *Resolver, + allocator: std.mem.Allocator, + file_path: *Fs.Path, + loader: Loader, +) !CacheEntry { + return switch (task.contents_or_fd) { + .fd => |contents| brk: { + const trace = bun.perf.trace("Bundler.readFile"); + defer trace.end(); + + if (strings.eqlComptime(file_path.namespace, "node")) lookup_builtin: { + if (task.ctx.framework) |f| { + if (f.built_in_modules.get(file_path.text)) |file| { + switch (file) { + .code => |code| break :brk .{ .contents = code, .fd = bun.invalid_fd }, + .import => |path| { + file_path.* = Fs.Path.init(path); + break :lookup_builtin; + }, + } + } + } + + break :brk .{ + .contents = NodeFallbackModules.contentsFromPath(file_path.text) orelse "", + .fd = bun.invalid_fd, + }; + } + + break :brk resolver.caches.fs.readFileWithAllocator( + // TODO: this allocator may be wrong for native plugins + if (loader.shouldCopyForBundling()) + // The OutputFile will own the memory for the contents + bun.default_allocator + else + allocator, + transpiler.fs, + file_path.text, + task.contents_or_fd.fd.dir, + false, + contents.file.unwrapValid(), + ) catch |err| { + const source = &Logger.Source.initEmptyFile(log.msgs.allocator.dupe(u8, file_path.text) catch unreachable); + switch (err) { + error.ENOENT, error.FileNotFound => { + log.addErrorFmt( + source, + Logger.Loc.Empty, + allocator, + "File not found {}", + .{bun.fmt.quote(file_path.text)}, + ) catch {}; + return error.FileNotFound; + }, + else => { + log.addErrorFmt( + source, + Logger.Loc.Empty, + allocator, + "{s} reading file: {}", + .{ @errorName(err), bun.fmt.quote(file_path.text) }, + ) catch {}; + }, + } + return err; + }; + }, + .contents => |contents| .{ + .contents = contents, + .fd = bun.invalid_fd, + }, + }; +} + +fn getCodeForParseTask( + task: *ParseTask, + log: *Logger.Log, + transpiler: *Transpiler, + resolver: *Resolver, + allocator: std.mem.Allocator, + file_path: *Fs.Path, + loader: *Loader, + from_plugin: *bool, +) !CacheEntry { + const might_have_on_parse_plugins = brk: { + if (task.source_index.isRuntime()) break :brk false; + const plugin = task.ctx.plugins orelse break :brk false; + if (!plugin.hasOnBeforeParsePlugins()) break :brk false; + + if (strings.eqlComptime(file_path.namespace, "node")) { + break :brk false; + } + break :brk true; + }; + + if (!might_have_on_parse_plugins) { + return getCodeForParseTaskWithoutPlugins(task, log, transpiler, resolver, allocator, file_path, loader.*); + } + + var should_continue_running: i32 = 1; + + var ctx = OnBeforeParsePlugin{ + .task = task, + .log = log, + .transpiler = transpiler, + .resolver = resolver, + .allocator = allocator, + .file_path = file_path, + .loader = loader, + .deferred_error = null, + .should_continue_running = &should_continue_running, + }; + + return try ctx.run(task.ctx.plugins.?, from_plugin); +} + +const OnBeforeParsePlugin = struct { + task: *ParseTask, + log: *Logger.Log, + transpiler: *Transpiler, + resolver: *Resolver, + allocator: std.mem.Allocator, + file_path: *Fs.Path, + loader: *Loader, + deferred_error: ?anyerror = null, + should_continue_running: *i32, + + result: ?*OnBeforeParseResult = null, + + const headers = bun.c; + + comptime { + bun.assert(@sizeOf(OnBeforeParseArguments) == @sizeOf(headers.OnBeforeParseArguments)); + bun.assert(@alignOf(OnBeforeParseArguments) == @alignOf(headers.OnBeforeParseArguments)); + + bun.assert(@sizeOf(BunLogOptions) == @sizeOf(headers.BunLogOptions)); + bun.assert(@alignOf(BunLogOptions) == @alignOf(headers.BunLogOptions)); + + bun.assert(@sizeOf(OnBeforeParseResult) == @sizeOf(headers.OnBeforeParseResult)); + bun.assert(@alignOf(OnBeforeParseResult) == @alignOf(headers.OnBeforeParseResult)); + + bun.assert(@sizeOf(BunLogOptions) == @sizeOf(headers.BunLogOptions)); + bun.assert(@alignOf(BunLogOptions) == @alignOf(headers.BunLogOptions)); + } + + const OnBeforeParseArguments = extern struct { + struct_size: usize = @sizeOf(OnBeforeParseArguments), + context: *OnBeforeParsePlugin, + path_ptr: ?[*]const u8 = "", + path_len: usize = 0, + namespace_ptr: ?[*]const u8 = "file", + namespace_len: usize = "file".len, + default_loader: Loader = .file, + external: ?*anyopaque = null, + }; + + const BunLogOptions = extern struct { + struct_size: usize = @sizeOf(BunLogOptions), + message_ptr: ?[*]const u8 = null, + message_len: usize = 0, + path_ptr: ?[*]const u8 = null, + path_len: usize = 0, + source_line_text_ptr: ?[*]const u8 = null, + source_line_text_len: usize = 0, + level: Logger.Log.Level = .err, + line: i32 = 0, + column: i32 = 0, + line_end: i32 = 0, + column_end: i32 = 0, + + pub fn sourceLineText(this: *const BunLogOptions) string { + if (this.source_line_text_ptr) |ptr| { + if (this.source_line_text_len > 0) { + return ptr[0..this.source_line_text_len]; + } + } + return ""; + } + + pub fn path(this: *const BunLogOptions) string { + if (this.path_ptr) |ptr| { + if (this.path_len > 0) { + return ptr[0..this.path_len]; + } + } + return ""; + } + + pub fn message(this: *const BunLogOptions) string { + if (this.message_ptr) |ptr| { + if (this.message_len > 0) { + return ptr[0..this.message_len]; + } + } + return ""; + } + + pub fn append(this: *const BunLogOptions, log: *Logger.Log, namespace: string) void { + const allocator = log.msgs.allocator; + const source_line_text = this.sourceLineText(); + const location = Logger.Location.init( + this.path(), + namespace, + @max(this.line, -1), + @max(this.column, -1), + @max(this.column_end - this.column, 0), + if (source_line_text.len > 0) allocator.dupe(u8, source_line_text) catch bun.outOfMemory() else null, + null, + ); + var msg = Logger.Msg{ .data = .{ .location = location, .text = allocator.dupe(u8, this.message()) catch bun.outOfMemory() } }; + switch (this.level) { + .err => msg.kind = .err, + .warn => msg.kind = .warn, + .verbose => msg.kind = .verbose, + .debug => msg.kind = .debug, + else => {}, + } + if (msg.kind == .err) { + log.errors += 1; + } else if (msg.kind == .warn) { + log.warnings += 1; + } + log.addMsg(msg) catch bun.outOfMemory(); + } + + pub fn logFn( + args_: ?*OnBeforeParseArguments, + log_options_: ?*BunLogOptions, + ) callconv(.C) void { + const args = args_ orelse return; + const log_options = log_options_ orelse return; + log_options.append(args.context.log, args.context.file_path.namespace); + } + }; + + const OnBeforeParseResultWrapper = extern struct { + original_source: ?[*]const u8 = null, + original_source_len: usize = 0, + original_source_fd: bun.FileDescriptor = bun.invalid_fd, + loader: Loader, + check: if (bun.Environment.isDebug) u32 else u0 = if (bun.Environment.isDebug) 42069 else 0, // Value to ensure OnBeforeParseResult is wrapped in this struct + result: OnBeforeParseResult, + }; + + const OnBeforeParseResult = extern struct { + struct_size: usize = @sizeOf(OnBeforeParseResult), + source_ptr: ?[*]const u8 = null, + source_len: usize = 0, + loader: Loader, + + fetch_source_code_fn: *const fn (*OnBeforeParseArguments, *OnBeforeParseResult) callconv(.C) i32 = &fetchSourceCode, + + user_context: ?*anyopaque = null, + free_user_context: ?*const fn (?*anyopaque) callconv(.C) void = null, + + log: *const fn ( + args_: ?*OnBeforeParseArguments, + log_options_: ?*BunLogOptions, + ) callconv(.C) void = &BunLogOptions.logFn, + + pub fn getWrapper(result: *OnBeforeParseResult) *OnBeforeParseResultWrapper { + const wrapper: *OnBeforeParseResultWrapper = @fieldParentPtr("result", result); + bun.debugAssert(wrapper.check == 42069); + return wrapper; + } + }; + + pub fn fetchSourceCode(args: *OnBeforeParseArguments, result: *OnBeforeParseResult) callconv(.C) i32 { + debug("fetchSourceCode", .{}); + const this = args.context; + if (this.log.errors > 0 or this.deferred_error != null or this.should_continue_running.* != 1) { + return 1; + } + + if (result.source_ptr != null) { + return 0; + } + + const entry = getCodeForParseTaskWithoutPlugins( + this.task, + this.log, + this.transpiler, + this.resolver, + this.allocator, + this.file_path, + + result.loader, + ) catch |err| { + this.deferred_error = err; + this.should_continue_running.* = 0; + return 1; + }; + result.source_ptr = entry.contents.ptr; + result.source_len = entry.contents.len; + result.free_user_context = null; + result.user_context = null; + const wrapper: *OnBeforeParseResultWrapper = result.getWrapper(); + wrapper.original_source = entry.contents.ptr; + wrapper.original_source_len = entry.contents.len; + wrapper.original_source_fd = entry.fd; + return 0; + } + + pub export fn OnBeforeParseResult__reset(this: *OnBeforeParseResult) void { + const wrapper = this.getWrapper(); + this.loader = wrapper.loader; + if (wrapper.original_source) |src_ptr| { + const src = src_ptr[0..wrapper.original_source_len]; + this.source_ptr = src.ptr; + this.source_len = src.len; + } else { + this.source_ptr = null; + this.source_len = 0; + } + } + + pub export fn OnBeforeParsePlugin__isDone(this: *OnBeforeParsePlugin) i32 { + if (this.should_continue_running.* != 1) { + return 1; + } + + const result = this.result orelse return 1; + // The first plugin to set the source wins. + // But, we must check that they actually modified it + // since fetching the source stores it inside `result.source_ptr` + if (result.source_ptr != null) { + const wrapper: *OnBeforeParseResultWrapper = result.getWrapper(); + return @intFromBool(result.source_ptr.? != wrapper.original_source.?); + } + + return 0; + } + + pub fn run(this: *OnBeforeParsePlugin, plugin: *JSC.API.JSBundler.Plugin, from_plugin: *bool) !CacheEntry { + var args = OnBeforeParseArguments{ + .context = this, + .path_ptr = this.file_path.text.ptr, + .path_len = this.file_path.text.len, + .default_loader = this.loader.*, + }; + if (this.file_path.namespace.len > 0) { + args.namespace_ptr = this.file_path.namespace.ptr; + args.namespace_len = this.file_path.namespace.len; + } + var wrapper = OnBeforeParseResultWrapper{ + .loader = this.loader.*, + .result = OnBeforeParseResult{ + .loader = this.loader.*, + }, + }; + + this.result = &wrapper.result; + const count = plugin.callOnBeforeParsePlugins( + this, + if (bun.strings.eqlComptime(this.file_path.namespace, "file")) + &bun.String.empty + else + &bun.String.init(this.file_path.namespace), + + &bun.String.init(this.file_path.text), + &args, + &wrapper.result, + this.should_continue_running, + ); + if (comptime Environment.enable_logs) + debug("callOnBeforeParsePlugins({s}:{s}) = {d}", .{ this.file_path.namespace, this.file_path.text, count }); + if (count > 0) { + if (this.deferred_error) |err| { + if (wrapper.result.free_user_context) |free_user_context| { + free_user_context(wrapper.result.user_context); + } + + return err; + } + + // If the plugin sets the `free_user_context` function pointer, it _must_ set the `user_context` pointer. + // Otherwise this is just invalid behavior. + if (wrapper.result.user_context == null and wrapper.result.free_user_context != null) { + var msg = Logger.Msg{ .data = .{ .location = null, .text = bun.default_allocator.dupe( + u8, + "Native plugin set the `free_plugin_source_code_context` field without setting the `plugin_source_code_context` field.", + ) catch bun.outOfMemory() } }; + msg.kind = .err; + args.context.log.errors += 1; + args.context.log.addMsg(msg) catch bun.outOfMemory(); + return error.InvalidNativePlugin; + } + + if (this.log.errors > 0) { + if (wrapper.result.free_user_context) |free_user_context| { + free_user_context(wrapper.result.user_context); + } + + return error.SyntaxError; + } + + if (wrapper.result.source_ptr) |ptr| { + if (wrapper.result.free_user_context != null) { + this.task.external_free_function = .{ + .ctx = wrapper.result.user_context, + .function = wrapper.result.free_user_context, + }; + } + from_plugin.* = true; + this.loader.* = wrapper.result.loader; + return .{ + .contents = ptr[0..wrapper.result.source_len], + .external_free_function = .{ + .ctx = wrapper.result.user_context, + .function = wrapper.result.free_user_context, + }, + .fd = wrapper.original_source_fd, + }; + } + } + + return try getCodeForParseTaskWithoutPlugins(this.task, this.log, this.transpiler, this.resolver, this.allocator, this.file_path, this.loader.*); + } +}; + +fn getSourceCode( + task: *ParseTask, + this: *ThreadPool.Worker, + log: *Logger.Log, +) anyerror!CacheEntry { + const allocator = this.allocator; + + var data = this.data; + var transpiler = &data.transpiler; + errdefer transpiler.resetStore(); + const resolver: *Resolver = &transpiler.resolver; + var file_path = task.path; + var loader = task.loader orelse file_path.loader(&transpiler.options.loaders) orelse options.Loader.file; + + // Do not process files as HTML if any of the following are true: + // - building for node or bun.js + // + // We allow non-entrypoints to import HTML so that people could + // potentially use an onLoad plugin that returns HTML. + if (task.known_target != .browser) { + loader = loader.disableHTML(); + task.loader = loader; + } + + var contents_came_from_plugin: bool = false; + return try getCodeForParseTask(task, log, transpiler, resolver, allocator, &file_path, &loader, &contents_came_from_plugin); +} + +fn runWithSourceCode( + task: *ParseTask, + this: *ThreadPool.Worker, + step: *ParseTask.Result.Error.Step, + log: *Logger.Log, + entry: *CacheEntry, +) anyerror!Result.Success { + const allocator = this.allocator; + + var data = this.data; + var transpiler = &data.transpiler; + errdefer transpiler.resetStore(); + var resolver: *Resolver = &transpiler.resolver; + var file_path = task.path; + var loader = task.loader orelse file_path.loader(&transpiler.options.loaders) orelse options.Loader.file; + + // Do not process files as HTML if any of the following are true: + // - building for node or bun.js + // + // We allow non-entrypoints to import HTML so that people could + // potentially use an onLoad plugin that returns HTML. + if (task.known_target != .browser) { + loader = loader.disableHTML(); + task.loader = loader; + } + + // WARNING: Do not change the variant of `task.contents_or_fd` from + // `.fd` to `.contents` (or back) after this point! + // + // When `task.contents_or_fd == .fd`, `entry.contents` is an owned string. + // When `task.contents_or_fd == .contents`, `entry.contents` is NOT owned! Freeing it here will cause a double free! + // + // Changing from `.contents` to `.fd` will cause a double free. + // This was the case in the situation where the ParseTask receives its `.contents` from an onLoad plugin, which caused it to be + // allocated by `bun.default_allocator` and then freed in `BundleV2.deinit` (and also by `entry.deinit(allocator)` below). + const debug_original_variant_check: if (bun.Environment.isDebug) ContentsOrFd.Tag else void = + if (bun.Environment.isDebug) @as(ContentsOrFd.Tag, task.contents_or_fd); + errdefer { + if (comptime bun.Environment.isDebug) { + if (@as(ContentsOrFd.Tag, task.contents_or_fd) != debug_original_variant_check) { + std.debug.panic("BUG: `task.contents_or_fd` changed in a way that will cause a double free or memory to leak!\n\n Original = {s}\n New = {s}\n", .{ + @tagName(debug_original_variant_check), + @tagName(task.contents_or_fd), + }); + } + } + if (task.contents_or_fd == .fd) entry.deinit(allocator); + } + + const will_close_file_descriptor = task.contents_or_fd == .fd and + entry.fd.isValid() and + entry.fd.stdioTag() == null and + this.ctx.bun_watcher == null; + if (will_close_file_descriptor) { + _ = entry.closeFD(); + task.contents_or_fd = .{ .fd = .{ + .file = bun.invalid_fd, + .dir = bun.invalid_fd, + } }; + } else if (task.contents_or_fd == .fd) { + task.contents_or_fd = .{ .fd = .{ + .file = entry.fd, + .dir = bun.invalid_fd, + } }; + } + step.* = .parse; + + const is_empty = strings.isAllWhitespace(entry.contents); + + const use_directive: UseDirective = if (!is_empty and transpiler.options.server_components) + if (UseDirective.parse(entry.contents)) |use| + use + else + .none + else + .none; + + if ( + // separate_ssr_graph makes boundaries switch to client because the server file uses that generated file as input. + // this is not done when there is one server graph because it is easier for plugins to deal with. + (use_directive == .client and + task.known_target != .bake_server_components_ssr and + this.ctx.framework.?.server_components.?.separate_ssr_graph) or + // set the target to the client when bundling client-side files + ((transpiler.options.server_components or transpiler.options.dev_server != null) and + task.known_target == .browser)) + { + transpiler = this.ctx.client_transpiler; + resolver = &transpiler.resolver; + bun.assert(transpiler.options.target == .browser); + } + + var source = Logger.Source{ + .path = file_path, + .index = task.source_index, + .contents = entry.contents, + .contents_is_recycled = false, + }; + + const target = (if (task.source_index.get() == 1) targetFromHashbang(entry.contents) else null) orelse + if (task.known_target == .bake_server_components_ssr and transpiler.options.framework.?.server_components.?.separate_ssr_graph) + .bake_server_components_ssr + else + transpiler.options.target; + + const output_format = transpiler.options.output_format; + + var opts = js_parser.Parser.Options.init(task.jsx, loader); + opts.bundle = true; + opts.warn_about_unbundled_modules = false; + opts.macro_context = &this.data.macro_context; + opts.package_version = task.package_version; + + opts.features.allow_runtime = !source.index.isRuntime(); + opts.features.unwrap_commonjs_to_esm = output_format == .esm and FeatureFlags.unwrap_commonjs_to_esm; + opts.features.top_level_await = output_format == .esm or output_format == .internal_bake_dev; + opts.features.auto_import_jsx = task.jsx.parse and transpiler.options.auto_import_jsx; + opts.features.trim_unused_imports = loader.isTypeScript() or (transpiler.options.trim_unused_imports orelse false); + opts.features.inlining = transpiler.options.minify_syntax; + opts.output_format = output_format; + opts.features.minify_syntax = transpiler.options.minify_syntax; + opts.features.minify_identifiers = transpiler.options.minify_identifiers; + opts.features.emit_decorator_metadata = transpiler.options.emit_decorator_metadata; + opts.features.unwrap_commonjs_packages = transpiler.options.unwrap_commonjs_packages; + opts.features.hot_module_reloading = output_format == .internal_bake_dev and !source.index.isRuntime(); + opts.features.auto_polyfill_require = output_format == .esm and !opts.features.hot_module_reloading; + opts.features.react_fast_refresh = target == .browser and + transpiler.options.react_fast_refresh and + loader.isJSX() and + !source.path.isNodeModule(); + + opts.features.server_components = if (transpiler.options.server_components) switch (target) { + .browser => .client_side, + else => switch (use_directive) { + .none => .wrap_anon_server_functions, + .client => if (transpiler.options.framework.?.server_components.?.separate_ssr_graph) + .client_side + else + .wrap_exports_for_client_reference, + .server => .wrap_exports_for_server_reference, + }, + } else .none; + + opts.framework = transpiler.options.framework; + + opts.ignore_dce_annotations = transpiler.options.ignore_dce_annotations and !source.index.isRuntime(); + + // For files that are not user-specified entrypoints, set `import.meta.main` to `false`. + // Entrypoints will have `import.meta.main` set as "unknown", unless we use `--compile`, + // in which we inline `true`. + if (transpiler.options.inline_entrypoint_import_meta_main or !task.is_entry_point) { + opts.import_meta_main_value = task.is_entry_point and transpiler.options.dev_server == null; + } else if (target == .node) { + opts.lower_import_meta_main_for_node_js = true; + } + + opts.tree_shaking = if (source.index.isRuntime()) true else transpiler.options.tree_shaking; + opts.module_type = task.module_type; + + task.jsx.parse = loader.isJSX(); + + var unique_key_for_additional_file: FileLoaderHash = .{ + .key = "", + .content_hash = 0, + }; + var ast: JSAst = if (!is_empty or loader.handlesEmptyFile()) + try getAST(log, transpiler, opts, allocator, resolver, source, loader, task.ctx.unique_key, &unique_key_for_additional_file, &task.ctx.linker.has_any_css_locals) + else switch (opts.module_type == .esm) { + inline else => |as_undefined| if (loader.isCSS()) try getEmptyCSSAST( + log, + transpiler, + opts, + allocator, + source, + ) else try getEmptyAST( + log, + transpiler, + opts, + allocator, + source, + if (as_undefined) E.Undefined else E.Object, + ), + }; + + ast.target = target; + if (ast.parts.len <= 1 and ast.css == null and (task.loader == null or task.loader.? != .html)) { + task.side_effects = .no_side_effects__empty_ast; + } + + // bun.debugAssert(ast.parts.len > 0); // when parts.len == 0, it is assumed to be pending/failed. empty ast has at least 1 part. + + step.* = .resolve; + + return .{ + .ast = ast, + .source = source, + .log = log.*, + .use_directive = use_directive, + .unique_key_for_additional_file = unique_key_for_additional_file.key, + .side_effects = task.side_effects, + .loader = loader, + + // Hash the files in here so that we do it in parallel. + .content_hash_for_additional_file = if (loader.shouldCopyForBundling()) + unique_key_for_additional_file.content_hash + else + 0, + }; +} + +fn ioTaskCallback(task: *ThreadPoolLib.Task) void { + runFromThreadPool(@fieldParentPtr("io_task", task)); +} + +fn taskCallback(task: *ThreadPoolLib.Task) void { + runFromThreadPool(@fieldParentPtr("task", task)); +} + +pub fn runFromThreadPool(this: *ParseTask) void { + var worker = ThreadPool.Worker.get(this.ctx); + defer worker.unget(); + debug("ParseTask(0x{x}, {s}) callback", .{ @intFromPtr(this), this.path.text }); + + var step: ParseTask.Result.Error.Step = .pending; + var log = Logger.Log.init(worker.allocator); + bun.assert(this.source_index.isValid()); // forgot to set source_index + + const value: ParseTask.Result.Value = value: { + if (this.stage == .needs_source_code) { + this.stage = .{ + .needs_parse = getSourceCode(this, worker, &log) catch |err| { + break :value .{ .err = .{ + .err = err, + .step = step, + .log = log, + .source_index = this.source_index, + .target = this.known_target, + } }; + }, + }; + + if (log.hasErrors()) { + break :value .{ .err = .{ + .err = error.SyntaxError, + .step = step, + .log = log, + .source_index = this.source_index, + .target = this.known_target, + } }; + } + + if (this.ctx.graph.pool.usesIOPool()) { + this.ctx.graph.pool.scheduleInsideThreadPool(this); + return; + } + } + + if (runWithSourceCode(this, worker, &step, &log, &this.stage.needs_parse)) |ast| { + // When using HMR, always flag asts with errors as parse failures. + // Not done outside of the dev server out of fear of breaking existing code. + if (this.ctx.transpiler.options.dev_server != null and ast.log.hasErrors()) { + break :value .{ + .err = .{ + .err = error.SyntaxError, + .step = .parse, + .log = ast.log, + .source_index = this.source_index, + .target = this.known_target, + }, + }; + } + + break :value .{ .success = ast }; + } else |err| { + if (err == error.EmptyAST) { + log.deinit(); + break :value .{ .empty = .{ + .source_index = this.source_index, + } }; + } + + break :value .{ .err = .{ + .err = err, + .step = step, + .log = log, + .source_index = this.source_index, + .target = this.known_target, + } }; + } + }; + + const result = bun.default_allocator.create(Result) catch bun.outOfMemory(); + + result.* = .{ + .ctx = this.ctx, + .task = .{}, + .value = value, + .external = this.external_free_function, + .watcher_data = switch (this.contents_or_fd) { + .fd => |fd| .{ .fd = fd.file, .dir_fd = fd.dir }, + .contents => .none, + }, + }; + + switch (worker.ctx.loop().*) { + .js => |jsc_event_loop| { + jsc_event_loop.enqueueTaskConcurrent(JSC.ConcurrentTask.fromCallback(result, onComplete)); + }, + .mini => |*mini| { + mini.enqueueTaskConcurrentWithExtraCtx( + Result, + BundleV2, + result, + BundleV2.onParseTaskComplete, + .task, + ); + }, + } +} + +pub fn onComplete(result: *Result) void { + BundleV2.onParseTaskComplete(result, result.ctx); +} + +const Transpiler = bun.Transpiler; +const bun = @import("bun"); +const string = bun.string; +const Output = bun.Output; +const Environment = bun.Environment; +const strings = bun.strings; +const default_allocator = bun.default_allocator; +const StoredFileDescriptorType = bun.StoredFileDescriptorType; +const FeatureFlags = bun.FeatureFlags; + +const bundler = bun.bundle_v2; +const BundleV2 = bundler.BundleV2; + +const ContentHasher = bundler.ContentHasher; +const UseDirective = bundler.UseDirective; +const targetFromHashbang = bundler.targetFromHashbang; + +const std = @import("std"); +const Logger = @import("../logger.zig"); +const options = @import("../options.zig"); +const js_parser = bun.js_parser; +const Part = js_ast.Part; +const js_ast = @import("../js_ast.zig"); +const linker = @import("../linker.zig"); +const base64 = bun.base64; +pub const Ref = @import("../ast/base.zig").Ref; +const ThreadPoolLib = @import("../thread_pool.zig"); +const BabyList = @import("../baby_list.zig").BabyList; +const Fs = @import("../fs.zig"); +const _resolver = @import("../resolver/resolver.zig"); +const ImportRecord = bun.ImportRecord; +const runtime = @import("../runtime.zig"); + +const HTMLScanner = @import("../HTMLScanner.zig"); +const NodeFallbackModules = @import("../node_fallbacks.zig"); +const CacheEntry = @import("../cache.zig").Fs.Entry; +const URL = @import("../url.zig").URL; +const Resolver = _resolver.Resolver; +const TOML = @import("../toml/toml_parser.zig").TOML; +const JSAst = js_ast.BundledAst; +const Loader = options.Loader; +pub const Index = @import("../ast/base.zig").Index; +const Symbol = js_ast.Symbol; +const EventLoop = bun.JSC.AnyEventLoop; +const Expr = js_ast.Expr; +const E = js_ast.E; +const G = js_ast.G; +const JSC = bun.JSC; +const Loc = Logger.Loc; +const bake = bun.bake; + +pub const DeferredBatchTask = bun.bundle_v2.DeferredBatchTask; +pub const ThreadPool = bun.bundle_v2.ThreadPool; diff --git a/src/bundler/ServerComponentParseTask.zig b/src/bundler/ServerComponentParseTask.zig new file mode 100644 index 0000000000..3bd3974c1d --- /dev/null +++ b/src/bundler/ServerComponentParseTask.zig @@ -0,0 +1,236 @@ +/// Files for Server Components are generated using `AstBuilder`, instead of +/// running through the js_parser. It emits a ParseTask.Result and joins +/// with the same logic that it runs though. +pub const ServerComponentParseTask = @This(); + +task: ThreadPoolLib.Task = .{ .callback = &taskCallbackWrap }, +data: Data, +ctx: *BundleV2, +source: Logger.Source, + +pub const Data = union(enum) { + /// Generate server-side code for a "use client" module. Given the + /// client ast, a "reference proxy" is created with identical exports. + client_reference_proxy: ReferenceProxy, + + client_entry_wrapper: ClientEntryWrapper, + + pub const ReferenceProxy = struct { + other_source: Logger.Source, + named_exports: JSAst.NamedExports, + }; + + pub const ClientEntryWrapper = struct { + path: []const u8, + }; +}; + +fn taskCallbackWrap(thread_pool_task: *ThreadPoolLib.Task) void { + const task: *ServerComponentParseTask = @fieldParentPtr("task", thread_pool_task); + var worker = ThreadPool.Worker.get(task.ctx); + defer worker.unget(); + var log = Logger.Log.init(worker.allocator); + + const result = bun.default_allocator.create(ParseTask.Result) catch bun.outOfMemory(); + result.* = .{ + .ctx = task.ctx, + .task = undefined, + + .value = if (taskCallback( + task, + &log, + worker.allocator, + )) |success| + .{ .success = success } + else |err| switch (err) { + error.OutOfMemory => bun.outOfMemory(), + }, + + .watcher_data = .none, + }; + + switch (worker.ctx.loop().*) { + .js => |jsc_event_loop| { + jsc_event_loop.enqueueTaskConcurrent(JSC.ConcurrentTask.fromCallback(result, ParseTask.onComplete)); + }, + .mini => |*mini| { + mini.enqueueTaskConcurrentWithExtraCtx( + ParseTask.Result, + BundleV2, + result, + BundleV2.onParseTaskComplete, + .task, + ); + }, + } +} + +fn taskCallback( + task: *ServerComponentParseTask, + log: *Logger.Log, + allocator: std.mem.Allocator, +) bun.OOM!ParseTask.Result.Success { + var ab = try AstBuilder.init(allocator, &task.source, task.ctx.transpiler.options.hot_module_reloading); + + switch (task.data) { + .client_reference_proxy => |data| try task.generateClientReferenceProxy(data, &ab), + .client_entry_wrapper => |data| try task.generateClientEntryWrapper(data, &ab), + } + + return .{ + .ast = try ab.toBundledAst(switch (task.data) { + // Server-side + .client_reference_proxy => task.ctx.transpiler.options.target, + // Client-side, + .client_entry_wrapper => .browser, + }), + .source = task.source, + .loader = .js, + .log = log.*, + .use_directive = .none, + .side_effects = .no_side_effects__pure_data, + }; +} + +fn generateClientEntryWrapper(_: *ServerComponentParseTask, data: Data.ClientEntryWrapper, b: *AstBuilder) !void { + const record = try b.addImportRecord(data.path, .stmt); + const namespace_ref = try b.newSymbol(.other, "main"); + try b.appendStmt(S.Import{ + .namespace_ref = namespace_ref, + .import_record_index = record, + .items = &.{}, + .is_single_line = true, + }); + b.import_records.items[record].was_originally_bare_import = true; +} + +fn generateClientReferenceProxy(task: *ServerComponentParseTask, data: Data.ReferenceProxy, b: *AstBuilder) !void { + const server_components = task.ctx.framework.?.server_components orelse + unreachable; // config must be non-null to enter this function + + const client_named_exports = data.named_exports; + + const register_client_reference = (try b.addImportStmt( + server_components.server_runtime_import, + &.{server_components.server_register_client_reference}, + ))[0]; + + const module_path = b.newExpr(E.String{ + // In development, the path loaded is the source file: Easy! + // + // In production, the path here must be the final chunk path, but + // that information is not yet available since chunks are not + // computed. The unique_key replacement system is used here. + .data = if (task.ctx.transpiler.options.dev_server != null) + data.other_source.path.pretty + else + try std.fmt.allocPrint(b.allocator, "{}S{d:0>8}", .{ + bun.fmt.hexIntLower(task.ctx.unique_key), + data.other_source.index.get(), + }), + }); + + for (client_named_exports.keys()) |key| { + const is_default = bun.strings.eqlComptime(key, "default"); + + // This error message is taken from + // https://github.com/facebook/react/blob/c5b9375767e2c4102d7e5559d383523736f1c902/packages/react-server-dom-webpack/src/ReactFlightWebpackNodeLoader.js#L323-L354 + const err_msg_string = try if (is_default) + std.fmt.allocPrint( + b.allocator, + "Attempted to call the default export of {[module_path]s} from " ++ + "the server, but it's on the client. It's not possible to invoke a " ++ + "client function from the server, it can only be rendered as a " ++ + "Component or passed to props of a Client Component.", + .{ .module_path = data.other_source.path.pretty }, + ) + else + std.fmt.allocPrint( + b.allocator, + "Attempted to call {[key]s}() from the server but {[key]s} " ++ + "is on the client. It's not possible to invoke a client function from " ++ + "the server, it can only be rendered as a Component or passed to " ++ + "props of a Client Component.", + .{ .key = key }, + ); + + // throw new Error(...) + const err_msg = b.newExpr(E.New{ + .target = b.newExpr(E.Identifier{ + .ref = try b.newExternalSymbol("Error"), + }), + .args = try BabyList(Expr).fromSlice(b.allocator, &.{ + b.newExpr(E.String{ .data = err_msg_string }), + }), + .close_parens_loc = Logger.Loc.Empty, + }); + + // registerClientReference( + // () => { throw new Error(...) }, + // "src/filepath.tsx", + // "Comp" + // ); + const value = b.newExpr(E.Call{ + .target = register_client_reference, + .args = try js_ast.ExprNodeList.fromSlice(b.allocator, &.{ + b.newExpr(E.Arrow{ .body = .{ + .stmts = try b.allocator.dupe(Stmt, &.{ + b.newStmt(S.Throw{ .value = err_msg }), + }), + .loc = Logger.Loc.Empty, + } }), + module_path, + b.newExpr(E.String{ .data = key }), + }), + }); + + if (is_default) { + // export default registerClientReference(...); + try b.appendStmt(S.ExportDefault{ .value = .{ .expr = value }, .default_name = .{} }); + } else { + // export const Component = registerClientReference(...); + const export_ref = try b.newSymbol(.other, key); + try b.appendStmt(S.Local{ + .decls = try G.Decl.List.fromSlice(b.allocator, &.{.{ + .binding = Binding.alloc(b.allocator, B.Identifier{ .ref = export_ref }, Logger.Loc.Empty), + .value = value, + }}), + .is_export = true, + .kind = .k_const, + }); + } + } +} + +const bun = @import("bun"); +const strings = bun.strings; +const default_allocator = bun.default_allocator; + +const std = @import("std"); +const Logger = @import("../logger.zig"); +const options = @import("../options.zig"); +const js_parser = bun.js_parser; +const js_ast = @import("../js_ast.zig"); +pub const Ref = @import("../ast/base.zig").Ref; +const ThreadPoolLib = @import("../thread_pool.zig"); +const BabyList = @import("../baby_list.zig").BabyList; +const OOM = bun.OOM; + +const JSAst = js_ast.BundledAst; +pub const Index = @import("../ast/base.zig").Index; +const Stmt = js_ast.Stmt; +const Expr = js_ast.Expr; +const E = js_ast.E; +const S = js_ast.S; +const G = js_ast.G; +const B = js_ast.B; +const Binding = js_ast.Binding; +const JSC = bun.JSC; +const Loc = Logger.Loc; +const bundler = bun.bundle_v2; +const BundleV2 = bundler.BundleV2; + +pub const DeferredBatchTask = bun.bundle_v2.DeferredBatchTask; +pub const ThreadPool = bun.bundle_v2.ThreadPool; +pub const ParseTask = bun.bundle_v2.ParseTask; +const AstBuilder = bundler.AstBuilder; diff --git a/src/bundler/ThreadPool.zig b/src/bundler/ThreadPool.zig new file mode 100644 index 0000000000..23017f9e93 --- /dev/null +++ b/src/bundler/ThreadPool.zig @@ -0,0 +1,293 @@ +pub const ThreadPool = struct { + /// macOS holds an IORWLock on every file open. + /// This causes massive contention after about 4 threads as of macOS 15.2 + /// On Windows, this seemed to be a small performance improvement. + /// On Linux, this was a performance regression. + /// In some benchmarks on macOS, this yielded up to a 60% performance improvement in microbenchmarks that load ~10,000 files. + io_pool: *ThreadPoolLib = undefined, + worker_pool: *ThreadPoolLib = undefined, + workers_assignments: std.AutoArrayHashMap(std.Thread.Id, *Worker) = std.AutoArrayHashMap(std.Thread.Id, *Worker).init(bun.default_allocator), + workers_assignments_lock: bun.Mutex = .{}, + v2: *BundleV2 = undefined, + + const debug = Output.scoped(.ThreadPool, false); + + pub fn reset(this: *ThreadPool) void { + if (this.usesIOPool()) { + if (this.io_pool.threadpool_context == @as(?*anyopaque, @ptrCast(this))) { + this.io_pool.threadpool_context = null; + } + } + + if (this.worker_pool.threadpool_context == @as(?*anyopaque, @ptrCast(this))) { + this.worker_pool.threadpool_context = null; + } + } + + pub fn go(this: *ThreadPool, allocator: std.mem.Allocator, comptime Function: anytype) !ThreadPoolLib.ConcurrentFunction(Function) { + return this.worker_pool.go(allocator, Function); + } + + pub fn start(this: *ThreadPool, v2: *BundleV2, existing_thread_pool: ?*ThreadPoolLib) !void { + this.v2 = v2; + + if (existing_thread_pool) |pool| { + this.worker_pool = pool; + } else { + const cpu_count = bun.getThreadCount(); + this.worker_pool = try v2.graph.allocator.create(ThreadPoolLib); + this.worker_pool.* = ThreadPoolLib.init(.{ + .max_threads = cpu_count, + }); + debug("{d} workers", .{cpu_count}); + } + + this.worker_pool.setThreadContext(this); + + this.worker_pool.warm(8); + + const IOThreadPool = struct { + var thread_pool: ThreadPoolLib = undefined; + var once = bun.once(startIOThreadPool); + + fn startIOThreadPool() void { + thread_pool = ThreadPoolLib.init(.{ + .max_threads = @max(@min(bun.getThreadCount(), 4), 2), + + // Use a much smaller stack size for the IO thread pool + .stack_size = 512 * 1024, + }); + } + + pub fn get() *ThreadPoolLib { + once.call(.{}); + return &thread_pool; + } + }; + + if (this.usesIOPool()) { + this.io_pool = IOThreadPool.get(); + this.io_pool.setThreadContext(this); + this.io_pool.warm(1); + } + } + + pub fn usesIOPool(_: *const ThreadPool) bool { + if (bun.getRuntimeFeatureFlag(.BUN_FEATURE_FLAG_FORCE_IO_POOL)) { + // For testing. + return true; + } + + if (bun.getRuntimeFeatureFlag(.BUN_FEATURE_FLAG_DISABLE_IO_POOL)) { + // For testing. + return false; + } + + if (Environment.isMac or Environment.isWindows) { + // 4 was the sweet spot on macOS. Didn't check the sweet spot on Windows. + return bun.getThreadCount() > 3; + } + + return false; + } + + pub fn scheduleWithOptions(this: *ThreadPool, parse_task: *ParseTask, is_inside_thread_pool: bool) void { + if (parse_task.contents_or_fd == .contents and parse_task.stage == .needs_source_code) { + parse_task.stage = .{ + .needs_parse = .{ + .contents = parse_task.contents_or_fd.contents, + .fd = bun.invalid_fd, + }, + }; + } + + const scheduleFn = if (is_inside_thread_pool) &ThreadPoolLib.scheduleInsideThreadPool else &ThreadPoolLib.schedule; + + if (this.usesIOPool()) { + switch (parse_task.stage) { + .needs_parse => { + scheduleFn(this.worker_pool, .from(&parse_task.task)); + }, + .needs_source_code => { + scheduleFn(this.io_pool, .from(&parse_task.io_task)); + }, + } + } else { + scheduleFn(this.worker_pool, .from(&parse_task.task)); + } + } + + pub fn schedule(this: *ThreadPool, parse_task: *ParseTask) void { + this.scheduleWithOptions(parse_task, false); + } + + pub fn scheduleInsideThreadPool(this: *ThreadPool, parse_task: *ParseTask) void { + this.scheduleWithOptions(parse_task, true); + } + + pub fn getWorker(this: *ThreadPool, id: std.Thread.Id) *Worker { + var worker: *Worker = undefined; + { + this.workers_assignments_lock.lock(); + defer this.workers_assignments_lock.unlock(); + const entry = this.workers_assignments.getOrPut(id) catch unreachable; + if (entry.found_existing) { + return entry.value_ptr.*; + } + + worker = bun.default_allocator.create(Worker) catch unreachable; + entry.value_ptr.* = worker; + } + + worker.* = .{ + .ctx = this.v2, + .allocator = undefined, + .thread = ThreadPoolLib.Thread.current, + }; + worker.init(this.v2); + + return worker; + } + + pub const Worker = struct { + heap: ThreadlocalArena = ThreadlocalArena{}, + + /// Thread-local memory allocator + /// All allocations are freed in `deinit` at the very end of bundling. + allocator: std.mem.Allocator, + + ctx: *BundleV2, + + data: WorkerData = undefined, + quit: bool = false, + + ast_memory_allocator: js_ast.ASTMemoryAllocator = undefined, + has_created: bool = false, + thread: ?*ThreadPoolLib.Thread = null, + + deinit_task: ThreadPoolLib.Task = .{ .callback = deinitCallback }, + + temporary_arena: bun.ArenaAllocator = undefined, + stmt_list: LinkerContext.StmtList = undefined, + + pub fn deinitCallback(task: *ThreadPoolLib.Task) void { + debug("Worker.deinit()", .{}); + var this: *Worker = @alignCast(@fieldParentPtr("deinit_task", task)); + this.deinit(); + } + + pub fn deinitSoon(this: *Worker) void { + if (this.thread) |thread| { + thread.pushIdleTask(&this.deinit_task); + } + } + + pub fn deinit(this: *Worker) void { + if (this.has_created) { + this.heap.deinit(); + } + + bun.default_allocator.destroy(this); + } + + pub fn get(ctx: *BundleV2) *Worker { + var worker = ctx.graph.pool.getWorker(std.Thread.getCurrentId()); + if (!worker.has_created) { + worker.create(ctx); + } + + worker.ast_memory_allocator.push(); + + if (comptime FeatureFlags.help_catch_memory_issues) { + worker.heap.helpCatchMemoryIssues(); + } + + return worker; + } + + pub fn unget(this: *Worker) void { + if (comptime FeatureFlags.help_catch_memory_issues) { + this.heap.helpCatchMemoryIssues(); + } + + this.ast_memory_allocator.pop(); + } + + pub const WorkerData = struct { + log: *Logger.Log, + estimated_input_lines_of_code: usize = 0, + macro_context: js_ast.Macro.MacroContext, + transpiler: Transpiler = undefined, + }; + + pub fn init(worker: *Worker, v2: *BundleV2) void { + worker.ctx = v2; + } + + fn create(this: *Worker, ctx: *BundleV2) void { + const trace = bun.perf.trace("Bundler.Worker.create"); + defer trace.end(); + + this.has_created = true; + Output.Source.configureThread(); + this.heap = ThreadlocalArena.init() catch unreachable; + this.allocator = this.heap.allocator(); + + var allocator = this.allocator; + + this.ast_memory_allocator = .{ .allocator = this.allocator }; + this.ast_memory_allocator.reset(); + + this.data = WorkerData{ + .log = allocator.create(Logger.Log) catch unreachable, + .estimated_input_lines_of_code = 0, + .macro_context = undefined, + }; + this.data.log.* = Logger.Log.init(allocator); + this.ctx = ctx; + this.data.transpiler = ctx.transpiler.*; + this.data.transpiler.setLog(this.data.log); + this.data.transpiler.setAllocator(allocator); + this.data.transpiler.linker.resolver = &this.data.transpiler.resolver; + this.data.transpiler.macro_context = js_ast.Macro.MacroContext.init(&this.data.transpiler); + this.data.macro_context = this.data.transpiler.macro_context.?; + this.temporary_arena = bun.ArenaAllocator.init(this.allocator); + this.stmt_list = LinkerContext.StmtList.init(this.allocator); + + const CacheSet = @import("../cache.zig"); + + this.data.transpiler.resolver.caches = CacheSet.Set.init(this.allocator); + debug("Worker.create()", .{}); + } + + pub fn run(this: *Worker, ctx: *BundleV2) void { + if (!this.has_created) { + this.create(ctx); + } + + // no funny business mr. cache + + } + }; +}; + +const Transpiler = bun.Transpiler; +const bun = @import("bun"); +const Output = bun.Output; +const Environment = bun.Environment; +const default_allocator = bun.default_allocator; +const FeatureFlags = bun.FeatureFlags; + +const std = @import("std"); +const Logger = @import("../logger.zig"); +const js_ast = @import("../js_ast.zig"); +const linker = @import("../linker.zig"); +pub const Ref = @import("../ast/base.zig").Ref; +const ThreadPoolLib = @import("../thread_pool.zig"); +const ThreadlocalArena = @import("../allocators/mimalloc_arena.zig").Arena; +const allocators = @import("../allocators.zig"); + +pub const Index = @import("../ast/base.zig").Index; +const BundleV2 = bun.bundle_v2.BundleV2; +const ParseTask = bun.bundle_v2.ParseTask; +const LinkerContext = bun.bundle_v2.LinkerContext; diff --git a/src/bundler/bundle_v2.zig b/src/bundler/bundle_v2.zig index 284fdb9895..ff200c2dd4 100644 --- a/src/bundler/bundle_v2.zig +++ b/src/bundler/bundle_v2.zig @@ -41,363 +41,17 @@ // // make mimalloc-debug // -const Transpiler = bun.Transpiler; -const bun = @import("bun"); -const string = bun.string; -const Output = bun.Output; -const Environment = bun.Environment; -const strings = bun.strings; -const MutableString = bun.MutableString; -const default_allocator = bun.default_allocator; -const StoredFileDescriptorType = bun.StoredFileDescriptorType; -const FeatureFlags = bun.FeatureFlags; -const std = @import("std"); -const lex = @import("../js_lexer.zig"); -const Logger = @import("../logger.zig"); -const options = @import("../options.zig"); -const js_parser = bun.js_parser; -const Part = js_ast.Part; -const js_printer = @import("../js_printer.zig"); -const js_ast = @import("../js_ast.zig"); -const linker = @import("../linker.zig"); -const sourcemap = bun.sourcemap; -const StringJoiner = bun.StringJoiner; -const base64 = bun.base64; -pub const Ref = @import("../ast/base.zig").Ref; -const ThreadPoolLib = @import("../thread_pool.zig"); -const ThreadlocalArena = @import("../allocators/mimalloc_arena.zig").Arena; -const BabyList = @import("../baby_list.zig").BabyList; -const Fs = @import("../fs.zig"); -const schema = @import("../api/schema.zig"); -const Api = schema.Api; -const _resolver = @import("../resolver/resolver.zig"); -const sync = bun.ThreadPool; -const ImportRecord = bun.ImportRecord; -const ImportKind = bun.ImportKind; -const allocators = @import("../allocators.zig"); -const resolve_path = @import("../resolver/resolve_path.zig"); -const runtime = @import("../runtime.zig"); -const Timer = @import("../system_timer.zig"); -const OOM = bun.OOM; - -const HTMLScanner = @import("../HTMLScanner.zig"); -const isPackagePath = _resolver.isPackagePath; -const NodeFallbackModules = @import("../node_fallbacks.zig"); -const CacheEntry = @import("../cache.zig").Fs.Entry; -const URL = @import("../url.zig").URL; -const Resolver = _resolver.Resolver; -const TOML = @import("../toml/toml_parser.zig").TOML; -const Dependency = js_ast.Dependency; -const JSAst = js_ast.BundledAst; -const Loader = options.Loader; -pub const Index = @import("../ast/base.zig").Index; -const Symbol = js_ast.Symbol; -const EventLoop = bun.JSC.AnyEventLoop; -const MultiArrayList = bun.MultiArrayList; -const Stmt = js_ast.Stmt; -const Expr = js_ast.Expr; -const E = js_ast.E; -const S = js_ast.S; -const G = js_ast.G; -const B = js_ast.B; -const Binding = js_ast.Binding; -const AutoBitSet = bun.bit_set.AutoBitSet; -const renamer = bun.renamer; -const StableSymbolCount = renamer.StableSymbolCount; -const MinifyRenamer = renamer.MinifyRenamer; -const Scope = js_ast.Scope; -const JSC = bun.JSC; -const debugTreeShake = Output.scoped(.TreeShake, true); -const debugPartRanges = Output.scoped(.PartRanges, true); -const BitSet = bun.bit_set.DynamicBitSetUnmanaged; -const Async = bun.Async; -const Loc = Logger.Loc; -const bake = bun.bake; -const lol = bun.LOLHTML; -const DataURL = @import("../resolver/resolver.zig").DataURL; - -const logPartDependencyTree = Output.scoped(.part_dep_tree, false); +pub const logPartDependencyTree = Output.scoped(.part_dep_tree, false); pub const MangledProps = std.AutoArrayHashMapUnmanaged(Ref, []const u8); -pub const ThreadPool = struct { - /// macOS holds an IORWLock on every file open. - /// This causes massive contention after about 4 threads as of macOS 15.2 - /// On Windows, this seemed to be a small performance improvement. - /// On Linux, this was a performance regression. - /// In some benchmarks on macOS, this yielded up to a 60% performance improvement in microbenchmarks that load ~10,000 files. - io_pool: *ThreadPoolLib = undefined, - worker_pool: *ThreadPoolLib = undefined, - workers_assignments: std.AutoArrayHashMap(std.Thread.Id, *Worker) = std.AutoArrayHashMap(std.Thread.Id, *Worker).init(bun.default_allocator), - workers_assignments_lock: bun.Mutex = .{}, - v2: *BundleV2 = undefined, +pub const PathToSourceIndexMap = std.HashMapUnmanaged(u64, Index.Int, IdentityContext(u64), 80); - const debug = Output.scoped(.ThreadPool, false); - - pub fn reset(this: *ThreadPool) void { - if (this.usesIOPool()) { - if (this.io_pool.threadpool_context == @as(?*anyopaque, @ptrCast(this))) { - this.io_pool.threadpool_context = null; - } - } - - if (this.worker_pool.threadpool_context == @as(?*anyopaque, @ptrCast(this))) { - this.worker_pool.threadpool_context = null; - } - } - - pub fn go(this: *ThreadPool, allocator: std.mem.Allocator, comptime Function: anytype) !ThreadPoolLib.ConcurrentFunction(Function) { - return this.worker_pool.go(allocator, Function); - } - - pub fn start(this: *ThreadPool, v2: *BundleV2, existing_thread_pool: ?*ThreadPoolLib) !void { - this.v2 = v2; - - if (existing_thread_pool) |pool| { - this.worker_pool = pool; - } else { - const cpu_count = bun.getThreadCount(); - this.worker_pool = try v2.graph.allocator.create(ThreadPoolLib); - this.worker_pool.* = ThreadPoolLib.init(.{ - .max_threads = cpu_count, - }); - debug("{d} workers", .{cpu_count}); - } - - this.worker_pool.setThreadContext(this); - - this.worker_pool.warm(8); - - const IOThreadPool = struct { - var thread_pool: ThreadPoolLib = undefined; - var once = bun.once(startIOThreadPool); - - fn startIOThreadPool() void { - thread_pool = ThreadPoolLib.init(.{ - .max_threads = @max(@min(bun.getThreadCount(), 4), 2), - - // Use a much smaller stack size for the IO thread pool - .stack_size = 512 * 1024, - }); - } - - pub fn get() *ThreadPoolLib { - once.call(.{}); - return &thread_pool; - } - }; - - if (this.usesIOPool()) { - this.io_pool = IOThreadPool.get(); - this.io_pool.setThreadContext(this); - this.io_pool.warm(1); - } - } - - pub fn usesIOPool(_: *const ThreadPool) bool { - if (bun.getRuntimeFeatureFlag(.BUN_FEATURE_FLAG_FORCE_IO_POOL)) { - // For testing. - return true; - } - - if (bun.getRuntimeFeatureFlag(.BUN_FEATURE_FLAG_DISABLE_IO_POOL)) { - // For testing. - return false; - } - - if (Environment.isMac or Environment.isWindows) { - // 4 was the sweet spot on macOS. Didn't check the sweet spot on Windows. - return bun.getThreadCount() > 3; - } - - return false; - } - - pub fn scheduleWithOptions(this: *ThreadPool, parse_task: *ParseTask, is_inside_thread_pool: bool) void { - if (parse_task.contents_or_fd == .contents and parse_task.stage == .needs_source_code) { - parse_task.stage = .{ - .needs_parse = .{ - .contents = parse_task.contents_or_fd.contents, - .fd = bun.invalid_fd, - }, - }; - } - - const scheduleFn = if (is_inside_thread_pool) &ThreadPoolLib.scheduleInsideThreadPool else &ThreadPoolLib.schedule; - - if (this.usesIOPool()) { - switch (parse_task.stage) { - .needs_parse => { - scheduleFn(this.worker_pool, .from(&parse_task.task)); - }, - .needs_source_code => { - scheduleFn(this.io_pool, .from(&parse_task.io_task)); - }, - } - } else { - scheduleFn(this.worker_pool, .from(&parse_task.task)); - } - } - - pub fn schedule(this: *ThreadPool, parse_task: *ParseTask) void { - this.scheduleWithOptions(parse_task, false); - } - - pub fn scheduleInsideThreadPool(this: *ThreadPool, parse_task: *ParseTask) void { - this.scheduleWithOptions(parse_task, true); - } - - pub fn getWorker(this: *ThreadPool, id: std.Thread.Id) *Worker { - var worker: *Worker = undefined; - { - this.workers_assignments_lock.lock(); - defer this.workers_assignments_lock.unlock(); - const entry = this.workers_assignments.getOrPut(id) catch unreachable; - if (entry.found_existing) { - return entry.value_ptr.*; - } - - worker = bun.default_allocator.create(Worker) catch unreachable; - entry.value_ptr.* = worker; - } - - worker.* = .{ - .ctx = this.v2, - .allocator = undefined, - .thread = ThreadPoolLib.Thread.current, - }; - worker.init(this.v2); - - return worker; - } - - pub const Worker = struct { - heap: ThreadlocalArena = ThreadlocalArena{}, - - /// Thread-local memory allocator - /// All allocations are freed in `deinit` at the very end of bundling. - allocator: std.mem.Allocator, - - ctx: *BundleV2, - - data: WorkerData = undefined, - quit: bool = false, - - ast_memory_allocator: js_ast.ASTMemoryAllocator = undefined, - has_created: bool = false, - thread: ?*ThreadPoolLib.Thread = null, - - deinit_task: ThreadPoolLib.Task = .{ .callback = deinitCallback }, - - temporary_arena: bun.ArenaAllocator = undefined, - stmt_list: LinkerContext.StmtList = undefined, - - pub fn deinitCallback(task: *ThreadPoolLib.Task) void { - debug("Worker.deinit()", .{}); - var this: *Worker = @alignCast(@fieldParentPtr("deinit_task", task)); - this.deinit(); - } - - pub fn deinitSoon(this: *Worker) void { - if (this.thread) |thread| { - thread.pushIdleTask(&this.deinit_task); - } - } - - pub fn deinit(this: *Worker) void { - if (this.has_created) { - this.heap.deinit(); - } - - bun.default_allocator.destroy(this); - } - - pub fn get(ctx: *BundleV2) *Worker { - var worker = ctx.graph.pool.getWorker(std.Thread.getCurrentId()); - if (!worker.has_created) { - worker.create(ctx); - } - - worker.ast_memory_allocator.push(); - - if (comptime FeatureFlags.help_catch_memory_issues) { - worker.heap.helpCatchMemoryIssues(); - } - - return worker; - } - - pub fn unget(this: *Worker) void { - if (comptime FeatureFlags.help_catch_memory_issues) { - this.heap.helpCatchMemoryIssues(); - } - - this.ast_memory_allocator.pop(); - } - - pub const WorkerData = struct { - log: *Logger.Log, - estimated_input_lines_of_code: usize = 0, - macro_context: js_ast.Macro.MacroContext, - transpiler: Transpiler = undefined, - }; - - pub fn init(worker: *Worker, v2: *BundleV2) void { - worker.ctx = v2; - } - - fn create(this: *Worker, ctx: *BundleV2) void { - const trace = bun.perf.trace("Bundler.Worker.create"); - defer trace.end(); - - this.has_created = true; - Output.Source.configureThread(); - this.heap = ThreadlocalArena.init() catch unreachable; - this.allocator = this.heap.allocator(); - - var allocator = this.allocator; - - this.ast_memory_allocator = .{ .allocator = this.allocator }; - this.ast_memory_allocator.reset(); - - this.data = WorkerData{ - .log = allocator.create(Logger.Log) catch unreachable, - .estimated_input_lines_of_code = 0, - .macro_context = undefined, - }; - this.data.log.* = Logger.Log.init(allocator); - this.ctx = ctx; - this.data.transpiler = ctx.transpiler.*; - this.data.transpiler.setLog(this.data.log); - this.data.transpiler.setAllocator(allocator); - this.data.transpiler.linker.resolver = &this.data.transpiler.resolver; - this.data.transpiler.macro_context = js_ast.Macro.MacroContext.init(&this.data.transpiler); - this.data.macro_context = this.data.transpiler.macro_context.?; - this.temporary_arena = bun.ArenaAllocator.init(this.allocator); - this.stmt_list = LinkerContext.StmtList.init(this.allocator); - - const CacheSet = @import("../cache.zig"); - - this.data.transpiler.resolver.caches = CacheSet.Set.init(this.allocator); - debug("Worker.create()", .{}); - } - - pub fn run(this: *Worker, ctx: *BundleV2) void { - if (!this.has_created) { - this.create(ctx); - } - - // no funny business mr. cache - - } - }; -}; - -const Watcher = bun.JSC.hot_reloader.NewHotReloader(BundleV2, EventLoop, true); +pub const Watcher = bun.JSC.hot_reloader.NewHotReloader(BundleV2, EventLoop, true); /// This assigns a concise, predictable, and unique `.pretty` attribute to a Path. /// DevServer relies on pretty paths for identifying modules, so they must be unique. -fn genericPathWithPrettyInitialized(path: Fs.Path, target: options.Target, top_level_dir: string, allocator: std.mem.Allocator) !Fs.Path { +pub fn genericPathWithPrettyInitialized(path: Fs.Path, target: options.Target, top_level_dir: string, allocator: std.mem.Allocator) !Fs.Path { var buf: bun.PathBuffer = undefined; const is_node = bun.strings.eqlComptime(path.namespace, "node"); @@ -3731,1823 +3385,18 @@ pub const BundleV2 = struct { } }; -/// Used to keep the bundle thread from spinning on Windows -pub fn timerCallback(_: *bun.windows.libuv.Timer) callconv(.C) void {} +pub const BundleThread = @import("./BundleThread.zig").BundleThread; -/// Originally, bake.DevServer required a separate bundling thread, but that was -/// later removed. The bundling thread's scheduling logic is generalized over -/// the completion structure. -/// -/// CompletionStruct's interface: -/// -/// - `configureBundler` is used to configure `Bundler`. -/// - `completeOnBundleThread` is used to tell the task that it is done. -pub fn BundleThread(CompletionStruct: type) type { - return struct { - const Self = @This(); - - waker: bun.Async.Waker, - ready_event: std.Thread.ResetEvent, - queue: bun.UnboundedQueue(CompletionStruct, .next), - generation: bun.Generation = 0, - - /// To initialize, put this somewhere in memory, and then call `spawn()` - pub const uninitialized: Self = .{ - .waker = undefined, - .queue = .{}, - .generation = 0, - .ready_event = .{}, - }; - - pub fn spawn(instance: *Self) !std.Thread { - const thread = try std.Thread.spawn(.{}, threadMain, .{instance}); - instance.ready_event.wait(); - return thread; - } - - /// Lazily-initialized singleton. This is used for `Bun.build` since the - /// bundle thread may not be needed. - pub const singleton = struct { - var once = std.once(loadOnceImpl); - var instance: ?*Self = null; - - // Blocks the calling thread until the bun build thread is created. - // std.once also blocks other callers of this function until the first caller is done. - fn loadOnceImpl() void { - const bundle_thread = bun.default_allocator.create(Self) catch bun.outOfMemory(); - bundle_thread.* = uninitialized; - instance = bundle_thread; - - // 2. Spawn the bun build thread. - const os_thread = bundle_thread.spawn() catch - Output.panic("Failed to spawn bun build thread", .{}); - os_thread.detach(); - } - - pub fn get() *Self { - once.call(); - return instance.?; - } - - pub fn enqueue(completion: *CompletionStruct) void { - get().enqueue(completion); - } - }; - - pub fn enqueue(instance: *Self, completion: *CompletionStruct) void { - instance.queue.push(completion); - instance.waker.wake(); - } - - fn threadMain(instance: *Self) void { - Output.Source.configureNamedThread("Bundler"); - - instance.waker = bun.Async.Waker.init() catch @panic("Failed to create waker"); - - // Unblock the calling thread so it can continue. - instance.ready_event.set(); - - var timer: bun.windows.libuv.Timer = undefined; - if (bun.Environment.isWindows) { - timer.init(instance.waker.loop.uv_loop); - timer.start(std.math.maxInt(u64), std.math.maxInt(u64), &timerCallback); - } - - var has_bundled = false; - while (true) { - while (instance.queue.pop()) |completion| { - generateInNewThread(completion, instance.generation) catch |err| { - completion.result = .{ .err = err }; - completion.completeOnBundleThread(); - }; - has_bundled = true; - } - instance.generation +|= 1; - - if (has_bundled) { - bun.Mimalloc.mi_collect(false); - has_bundled = false; - } - - _ = instance.waker.wait(); - } - } - - /// This is called from `Bun.build` in JavaScript. - fn generateInNewThread(completion: *CompletionStruct, generation: bun.Generation) !void { - var heap = try ThreadlocalArena.init(); - defer heap.deinit(); - - const allocator = heap.allocator(); - var ast_memory_allocator = try allocator.create(js_ast.ASTMemoryAllocator); - ast_memory_allocator.* = .{ .allocator = allocator }; - ast_memory_allocator.reset(); - ast_memory_allocator.push(); - - const transpiler = try allocator.create(bun.Transpiler); - - try completion.configureBundler(transpiler, allocator); - - transpiler.resolver.generation = generation; - - const this = try BundleV2.init( - transpiler, - null, // TODO: Kit - allocator, - JSC.AnyEventLoop.init(allocator), - false, - JSC.WorkPool.get(), - heap, - ); - - this.plugins = completion.plugins; - this.completion = switch (CompletionStruct) { - BundleV2.JSBundleCompletionTask => completion, - else => @compileError("Unknown completion struct: " ++ CompletionStruct), - }; - completion.transpiler = this; - - defer { - this.graph.pool.reset(); - ast_memory_allocator.pop(); - this.deinitWithoutFreeingArena(); - } - - errdefer { - // Wait for wait groups to finish. There still may be ongoing work. - this.linker.source_maps.line_offset_wait_group.wait(); - this.linker.source_maps.quoted_contents_wait_group.wait(); - - var out_log = Logger.Log.init(bun.default_allocator); - this.transpiler.log.appendToWithRecycled(&out_log, true) catch bun.outOfMemory(); - completion.log = out_log; - } - - completion.result = .{ .value = .{ - .output_files = try this.runFromJSInNewThread(transpiler.options.entry_points), - } }; - - var out_log = Logger.Log.init(bun.default_allocator); - this.transpiler.log.appendToWithRecycled(&out_log, true) catch bun.outOfMemory(); - completion.log = out_log; - completion.completeOnBundleThread(); - } - }; -} - -const UseDirective = js_ast.UseDirective; -const ServerComponentBoundary = js_ast.ServerComponentBoundary; - -/// This task is run once all parse and resolve tasks have been complete -/// and we have deferred onLoad plugins that we need to resume -/// -/// It enqueues a task to be run on the JS thread which resolves the promise -/// for every onLoad callback which called `.defer()`. -pub const DeferredBatchTask = struct { - running: if (Environment.isDebug) bool else u0 = if (Environment.isDebug) false else 0, - - const AnyTask = JSC.AnyTask.New(@This(), runOnJSThread); - - pub fn init(this: *DeferredBatchTask) void { - if (comptime Environment.isDebug) bun.debugAssert(!this.running); - this.* = .{ - .running = if (comptime Environment.isDebug) false else 0, - }; - } - - pub fn getBundleV2(this: *DeferredBatchTask) *bun.BundleV2 { - return @alignCast(@fieldParentPtr("drain_defer_task", this)); - } - - pub fn schedule(this: *DeferredBatchTask) void { - if (comptime Environment.isDebug) { - bun.assert(!this.running); - this.running = false; - } - this.getBundleV2().jsLoopForPlugins().enqueueTaskConcurrent(JSC.ConcurrentTask.create(JSC.Task.init(this))); - } - - pub fn deinit(this: *DeferredBatchTask) void { - if (comptime Environment.isDebug) { - this.running = false; - } - } - - pub fn runOnJSThread(this: *DeferredBatchTask) void { - defer this.deinit(); - var bv2 = this.getBundleV2(); - bv2.plugins.?.drainDeferred( - if (bv2.completion) |completion| - completion.result == .err - else - false, - ); - } -}; - -const ContentsOrFd = union(enum) { - fd: struct { - dir: StoredFileDescriptorType, - file: StoredFileDescriptorType, - }, - contents: string, - - const Tag = @typeInfo(ContentsOrFd).@"union".tag_type.?; -}; - -pub const ParseTask = struct { - path: Fs.Path, - secondary_path_for_commonjs_interop: ?Fs.Path = null, - contents_or_fd: ContentsOrFd, - external_free_function: CacheEntry.ExternalFreeFunction = .none, - side_effects: _resolver.SideEffects, - loader: ?Loader = null, - jsx: options.JSX.Pragma, - source_index: Index = Index.invalid, - task: ThreadPoolLib.Task = .{ .callback = &taskCallback }, - - // Split this into a different task so that we don't accidentally run the - // tasks for io on the threads that are meant for parsing. - io_task: ThreadPoolLib.Task = .{ .callback = &ioTaskCallback }, - - // Used for splitting up the work between the io and parse steps. - stage: ParseTaskStage = .needs_source_code, - - tree_shaking: bool = false, - known_target: options.Target, - module_type: options.ModuleType = .unknown, - emit_decorator_metadata: bool = false, - ctx: *BundleV2, - package_version: string = "", - is_entry_point: bool = false, - /// This is set when the file is an entrypoint, and it has an onLoad plugin. - /// In this case we want to defer adding this to additional_files until after - /// the onLoad plugin has finished. - defer_copy_for_bundling: bool = false, - - const ParseTaskStage = union(enum) { - needs_source_code: void, - needs_parse: CacheEntry, - }; - - /// The information returned to the Bundler thread when a parse finishes. - pub const Result = struct { - task: EventLoop.Task, - ctx: *BundleV2, - value: Value, - watcher_data: WatcherData, - /// This is used for native onBeforeParsePlugins to store - /// a function pointer and context pointer to free the - /// returned source code by the plugin. - external: CacheEntry.ExternalFreeFunction = .none, - - pub const Value = union(enum) { - success: Success, - err: Error, - empty: struct { - source_index: Index, - }, - }; - - const WatcherData = struct { - fd: bun.StoredFileDescriptorType, - dir_fd: bun.StoredFileDescriptorType, - - /// When no files to watch, this encoding is used. - const none: WatcherData = .{ - .fd = bun.invalid_fd, - .dir_fd = bun.invalid_fd, - }; - }; - - pub const Success = struct { - ast: JSAst, - source: Logger.Source, - log: Logger.Log, - use_directive: UseDirective, - side_effects: _resolver.SideEffects, - - /// Used by "file" loader files. - unique_key_for_additional_file: []const u8 = "", - /// Used by "file" loader files. - content_hash_for_additional_file: u64 = 0, - - loader: Loader, - }; - - pub const Error = struct { - err: anyerror, - step: Step, - log: Logger.Log, - target: options.Target, - source_index: Index, - - pub const Step = enum { - pending, - read_file, - parse, - resolve, - }; - }; - }; - - const debug = Output.scoped(.ParseTask, true); - - pub fn init(resolve_result: *const _resolver.Result, source_index: Index, ctx: *BundleV2) ParseTask { - return .{ - .ctx = ctx, - .path = resolve_result.path_pair.primary, - .contents_or_fd = .{ - .fd = .{ - .dir = resolve_result.dirname_fd, - .file = resolve_result.file_fd, - }, - }, - .side_effects = resolve_result.primary_side_effects_data, - .jsx = resolve_result.jsx, - .source_index = source_index, - .module_type = resolve_result.module_type, - .emit_decorator_metadata = resolve_result.emit_decorator_metadata, - .package_version = if (resolve_result.package_json) |package_json| package_json.version else "", - .known_target = ctx.transpiler.options.target, - }; - } - - const RuntimeSource = struct { - parse_task: ParseTask, - source: Logger.Source, - }; - - fn getRuntimeSourceComptime(comptime target: options.Target) RuntimeSource { - // When the `require` identifier is visited, it is replaced with e_require_call_target - // and then that is either replaced with the module itself, or an import to the - // runtime here. - const runtime_require = switch (target) { - // Previously, Bun inlined `import.meta.require` at all usages. This broke - // code that called `fn.toString()` and parsed the code outside a module - // context. - .bun, .bun_macro => - \\export var __require = import.meta.require; - , - - .node => - \\import { createRequire } from "node:module"; - \\export var __require = /* @__PURE__ */ createRequire(import.meta.url); - \\ - , - - // Copied from esbuild's runtime.go: - // - // > This fallback "require" function exists so that "typeof require" can - // > naturally be "function" even in non-CommonJS environments since esbuild - // > emulates a CommonJS environment (issue #1202). However, people want this - // > shim to fall back to "globalThis.require" even if it's defined later - // > (including property accesses such as "require.resolve") so we need to - // > use a proxy (issue #1614). - // - // When bundling to node, esbuild picks this code path as well, but `globalThis.require` - // is not always defined there. The `createRequire` call approach is more reliable. - else => - \\export var __require = /* @__PURE__ */ (x => - \\ typeof require !== 'undefined' ? require : - \\ typeof Proxy !== 'undefined' ? new Proxy(x, { - \\ get: (a, b) => (typeof require !== 'undefined' ? require : a)[b] - \\ }) : x - \\)(function (x) { - \\ if (typeof require !== 'undefined') return require.apply(this, arguments) - \\ throw Error('Dynamic require of "' + x + '" is not supported') - \\}); - \\ - }; - const runtime_using_symbols = switch (target) { - // bun's webkit has Symbol.asyncDispose, Symbol.dispose, and SuppressedError, but not the syntax support - .bun => - \\export var __using = (stack, value, async) => { - \\ if (value != null) { - \\ if (typeof value !== 'object' && typeof value !== 'function') throw TypeError('Object expected to be assigned to "using" declaration') - \\ let dispose - \\ if (async) dispose = value[Symbol.asyncDispose] - \\ if (dispose === void 0) dispose = value[Symbol.dispose] - \\ if (typeof dispose !== 'function') throw TypeError('Object not disposable') - \\ stack.push([async, dispose, value]) - \\ } else if (async) { - \\ stack.push([async]) - \\ } - \\ return value - \\} - \\ - \\export var __callDispose = (stack, error, hasError) => { - \\ let fail = e => error = hasError ? new SuppressedError(e, error, 'An error was suppressed during disposal') : (hasError = true, e) - \\ , next = (it) => { - \\ while (it = stack.pop()) { - \\ try { - \\ var result = it[1] && it[1].call(it[2]) - \\ if (it[0]) return Promise.resolve(result).then(next, (e) => (fail(e), next())) - \\ } catch (e) { - \\ fail(e) - \\ } - \\ } - \\ if (hasError) throw error - \\ } - \\ return next() - \\} - \\ - , - // Other platforms may or may not have the symbol or errors - // The definitions of __dispose and __asyncDispose match what esbuild's __wellKnownSymbol() helper does - else => - \\var __dispose = Symbol.dispose || /* @__PURE__ */ Symbol.for('Symbol.dispose'); - \\var __asyncDispose = Symbol.asyncDispose || /* @__PURE__ */ Symbol.for('Symbol.asyncDispose'); - \\ - \\export var __using = (stack, value, async) => { - \\ if (value != null) { - \\ if (typeof value !== 'object' && typeof value !== 'function') throw TypeError('Object expected to be assigned to "using" declaration') - \\ var dispose - \\ if (async) dispose = value[__asyncDispose] - \\ if (dispose === void 0) dispose = value[__dispose] - \\ if (typeof dispose !== 'function') throw TypeError('Object not disposable') - \\ stack.push([async, dispose, value]) - \\ } else if (async) { - \\ stack.push([async]) - \\ } - \\ return value - \\} - \\ - \\export var __callDispose = (stack, error, hasError) => { - \\ var E = typeof SuppressedError === 'function' ? SuppressedError : - \\ function (e, s, m, _) { return _ = Error(m), _.name = 'SuppressedError', _.error = e, _.suppressed = s, _ }, - \\ fail = e => error = hasError ? new E(e, error, 'An error was suppressed during disposal') : (hasError = true, e), - \\ next = (it) => { - \\ while (it = stack.pop()) { - \\ try { - \\ var result = it[1] && it[1].call(it[2]) - \\ if (it[0]) return Promise.resolve(result).then(next, (e) => (fail(e), next())) - \\ } catch (e) { - \\ fail(e) - \\ } - \\ } - \\ if (hasError) throw error - \\ } - \\ return next() - \\} - \\ - }; - const runtime_code = @embedFile("../runtime.js") ++ runtime_require ++ runtime_using_symbols; - - const parse_task = ParseTask{ - .ctx = undefined, - .path = Fs.Path.initWithNamespace("runtime", "bun:runtime"), - .side_effects = .no_side_effects__pure_data, - .jsx = .{ - .parse = false, - }, - .contents_or_fd = .{ - .contents = runtime_code, - }, - .source_index = Index.runtime, - .loader = .js, - .known_target = target, - }; - const source = Logger.Source{ - .path = parse_task.path, - .contents = parse_task.contents_or_fd.contents, - .index = Index.runtime, - }; - return .{ .parse_task = parse_task, .source = source }; - } - - fn getRuntimeSource(target: options.Target) RuntimeSource { - return switch (target) { - inline else => |t| comptime getRuntimeSourceComptime(t), - }; - } - - threadlocal var override_file_path_buf: bun.PathBuffer = undefined; - - fn getEmptyCSSAST( - log: *Logger.Log, - transpiler: *Transpiler, - opts: js_parser.Parser.Options, - allocator: std.mem.Allocator, - source: Logger.Source, - ) !JSAst { - const root = Expr.init(E.Object, E.Object{}, Logger.Loc{ .start = 0 }); - var ast = JSAst.init((try js_parser.newLazyExportAST(allocator, transpiler.options.define, opts, log, root, &source, "")).?); - ast.css = bun.create(allocator, bun.css.BundlerStyleSheet, bun.css.BundlerStyleSheet.empty(allocator)); - return ast; - } - - fn getEmptyAST(log: *Logger.Log, transpiler: *Transpiler, opts: js_parser.Parser.Options, allocator: std.mem.Allocator, source: Logger.Source, comptime RootType: type) !JSAst { - const root = Expr.init(RootType, RootType{}, Logger.Loc.Empty); - return JSAst.init((try js_parser.newLazyExportAST(allocator, transpiler.options.define, opts, log, root, &source, "")).?); - } - - const FileLoaderHash = struct { - key: []const u8, - content_hash: u64, - }; - - fn getAST( - log: *Logger.Log, - transpiler: *Transpiler, - opts: js_parser.Parser.Options, - allocator: std.mem.Allocator, - resolver: *Resolver, - source: Logger.Source, - loader: Loader, - unique_key_prefix: u64, - unique_key_for_additional_file: *FileLoaderHash, - has_any_css_locals: *std.atomic.Value(u32), - ) !JSAst { - switch (loader) { - .jsx, .tsx, .js, .ts => { - const trace = bun.perf.trace("Bundler.ParseJS"); - defer trace.end(); - return if (try resolver.caches.js.parse( - transpiler.allocator, - opts, - transpiler.options.define, - log, - &source, - )) |res| - JSAst.init(res.ast) - else switch (opts.module_type == .esm) { - inline else => |as_undefined| try getEmptyAST( - log, - transpiler, - opts, - allocator, - source, - if (as_undefined) E.Undefined else E.Object, - ), - }; - }, - .json, .jsonc => |v| { - const trace = bun.perf.trace("Bundler.ParseJSON"); - defer trace.end(); - const root = (try resolver.caches.json.parseJSON(log, source, allocator, if (v == .jsonc) .jsonc else .json, true)) orelse Expr.init(E.Object, E.Object{}, Logger.Loc.Empty); - return JSAst.init((try js_parser.newLazyExportAST(allocator, transpiler.options.define, opts, log, root, &source, "")).?); - }, - .toml => { - const trace = bun.perf.trace("Bundler.ParseTOML"); - defer trace.end(); - var temp_log = bun.logger.Log.init(allocator); - defer { - temp_log.cloneToWithRecycled(log, true) catch bun.outOfMemory(); - temp_log.msgs.clearAndFree(); - } - const root = try TOML.parse(&source, &temp_log, allocator, false); - return JSAst.init((try js_parser.newLazyExportAST(allocator, transpiler.options.define, opts, &temp_log, root, &source, "")).?); - }, - .text => { - const root = Expr.init(E.String, E.String{ - .data = source.contents, - }, Logger.Loc{ .start = 0 }); - var ast = JSAst.init((try js_parser.newLazyExportAST(allocator, transpiler.options.define, opts, log, root, &source, "")).?); - ast.addUrlForCss(allocator, &source, "text/plain", null); - return ast; - }, - - .sqlite_embedded, .sqlite => { - if (!transpiler.options.target.isBun()) { - log.addError( - &source, - Logger.Loc.Empty, - "To use the \"sqlite\" loader, set target to \"bun\"", - ) catch bun.outOfMemory(); - return error.ParserError; - } - - const path_to_use = brk: { - // Implements embedded sqlite - if (loader == .sqlite_embedded) { - const embedded_path = std.fmt.allocPrint(allocator, "{any}A{d:0>8}", .{ bun.fmt.hexIntLower(unique_key_prefix), source.index.get() }) catch unreachable; - unique_key_for_additional_file.* = .{ - .key = embedded_path, - .content_hash = ContentHasher.run(source.contents), - }; - break :brk embedded_path; - } - - break :brk source.path.text; - }; - - // This injects the following code: - // - // import.meta.require(unique_key).db - // - const import_path = Expr.init(E.String, E.String{ - .data = path_to_use, - }, Logger.Loc{ .start = 0 }); - - const import_meta = Expr.init(E.ImportMeta, E.ImportMeta{}, Logger.Loc{ .start = 0 }); - const require_property = Expr.init(E.Dot, E.Dot{ - .target = import_meta, - .name_loc = Logger.Loc.Empty, - .name = "require", - }, Logger.Loc{ .start = 0 }); - const require_args = allocator.alloc(Expr, 2) catch unreachable; - require_args[0] = import_path; - const object_properties = allocator.alloc(G.Property, 1) catch unreachable; - object_properties[0] = G.Property{ - .key = Expr.init(E.String, E.String{ - .data = "type", - }, Logger.Loc{ .start = 0 }), - .value = Expr.init(E.String, E.String{ - .data = "sqlite", - }, Logger.Loc{ .start = 0 }), - }; - require_args[1] = Expr.init(E.Object, E.Object{ - .properties = G.Property.List.init(object_properties), - .is_single_line = true, - }, Logger.Loc{ .start = 0 }); - const require_call = Expr.init(E.Call, E.Call{ - .target = require_property, - .args = BabyList(Expr).init(require_args), - }, Logger.Loc{ .start = 0 }); - - const root = Expr.init(E.Dot, E.Dot{ - .target = require_call, - .name_loc = Logger.Loc.Empty, - .name = "db", - }, Logger.Loc{ .start = 0 }); - - return JSAst.init((try js_parser.newLazyExportAST(allocator, transpiler.options.define, opts, log, root, &source, "")).?); - }, - .napi => { - // (dap-eval-cb "source.contents.ptr") - if (transpiler.options.target == .browser) { - log.addError( - &source, - Logger.Loc.Empty, - "Loading .node files won't work in the browser. Make sure to set target to \"bun\" or \"node\"", - ) catch bun.outOfMemory(); - return error.ParserError; - } - - const unique_key = std.fmt.allocPrint(allocator, "{any}A{d:0>8}", .{ bun.fmt.hexIntLower(unique_key_prefix), source.index.get() }) catch unreachable; - // This injects the following code: - // - // require(unique_key) - // - const import_path = Expr.init(E.String, E.String{ - .data = unique_key, - }, Logger.Loc{ .start = 0 }); - - const require_args = allocator.alloc(Expr, 1) catch unreachable; - require_args[0] = import_path; - - const root = Expr.init(E.Call, E.Call{ - .target = .{ .data = .{ .e_require_call_target = {} }, .loc = .{ .start = 0 } }, - .args = BabyList(Expr).init(require_args), - }, Logger.Loc{ .start = 0 }); - - unique_key_for_additional_file.* = .{ - .key = unique_key, - .content_hash = ContentHasher.run(source.contents), - }; - return JSAst.init((try js_parser.newLazyExportAST(allocator, transpiler.options.define, opts, log, root, &source, "")).?); - }, - .html => { - var scanner = HTMLScanner.init(allocator, log, &source); - try scanner.scan(source.contents); - - // Reuse existing code for creating the AST - // because it handles the various Ref and other structs we - // need in order to print code later. - var ast = (try js_parser.newLazyExportAST( - allocator, - transpiler.options.define, - opts, - log, - Expr.init(E.Missing, E.Missing{}, Logger.Loc.Empty), - &source, - "", - )).?; - ast.import_records = scanner.import_records; - - // We're banning import default of html loader files for now. - // - // TLDR: it kept including: - // - // var name_default = ...; - // - // in the bundle because of the exports AST, and - // gave up on figuring out how to fix it so that - // this feature could ship. - ast.has_lazy_export = false; - ast.parts.ptr[1] = .{ - .stmts = &.{}, - .is_live = true, - .import_record_indices = brk2: { - // Generate a single part that depends on all the import records. - // This is to ensure that we generate a JavaScript bundle containing all the user's code. - var import_record_indices = try Part.ImportRecordIndices.initCapacity(allocator, scanner.import_records.len); - import_record_indices.len = @truncate(scanner.import_records.len); - for (import_record_indices.slice(), 0..) |*import_record, index| { - import_record.* = @intCast(index); - } - break :brk2 import_record_indices; - }, - }; - - // Try to avoid generating unnecessary ESM <> CJS wrapper code. - if (opts.output_format == .esm or opts.output_format == .iife) { - ast.exports_kind = .esm; - } - - return JSAst.init(ast); - }, - .css => { - // make css ast - var import_records = BabyList(ImportRecord){}; - const source_code = source.contents; - var temp_log = bun.logger.Log.init(allocator); - defer { - temp_log.appendToMaybeRecycled(log, &source) catch bun.outOfMemory(); - } - - const css_module_suffix = ".module.css"; - const enable_css_modules = source.path.pretty.len > css_module_suffix.len and - strings.eqlComptime(source.path.pretty[source.path.pretty.len - css_module_suffix.len ..], css_module_suffix); - const parser_options = if (enable_css_modules) init: { - var parseropts = bun.css.ParserOptions.default(allocator, &temp_log); - parseropts.filename = bun.path.basename(source.path.pretty); - parseropts.css_modules = bun.css.CssModuleConfig{}; - break :init parseropts; - } else bun.css.ParserOptions.default(allocator, &temp_log); - - var css_ast, var extra = switch (bun.css.BundlerStyleSheet.parseBundler( - allocator, - source_code, - parser_options, - &import_records, - source.index, - )) { - .result => |v| v, - .err => |e| { - try e.addToLogger(&temp_log, &source, allocator); - return error.SyntaxError; - }, - }; - // Make sure the css modules local refs have a valid tag - if (comptime bun.Environment.isDebug) { - if (css_ast.local_scope.count() > 0) { - for (css_ast.local_scope.values()) |entry| { - const ref = entry.ref; - bun.assert(ref.innerIndex() < extra.symbols.len); - } - } - } - if (css_ast.minify(allocator, bun.css.MinifyOptions{ - .targets = bun.css.Targets.forBundlerTarget(transpiler.options.target), - .unused_symbols = .{}, - }, &extra).asErr()) |e| { - try e.addToLogger(&temp_log, &source, allocator); - return error.MinifyError; - } - if (css_ast.local_scope.count() > 0) { - _ = has_any_css_locals.fetchAdd(1, .monotonic); - } - // If this is a css module, the final exports object wil be set in `generateCodeForLazyExport`. - const root = Expr.init(E.Object, E.Object{}, Logger.Loc{ .start = 0 }); - const css_ast_heap = bun.create(allocator, bun.css.BundlerStyleSheet, css_ast); - var ast = JSAst.init((try js_parser.newLazyExportASTImpl(allocator, transpiler.options.define, opts, &temp_log, root, &source, "", extra.symbols)).?); - ast.css = css_ast_heap; - ast.import_records = import_records; - return ast; - }, - // TODO: - .dataurl, .base64, .bunsh => { - return try getEmptyAST(log, transpiler, opts, allocator, source, E.String); - }, - .file, .wasm => { - bun.assert(loader.shouldCopyForBundling()); - - // Put a unique key in the AST to implement the URL loader. At the end - // of the bundle, the key is replaced with the actual URL. - const content_hash = ContentHasher.run(source.contents); - - const unique_key: []const u8 = if (transpiler.options.dev_server != null) - // With DevServer, the actual URL is added now, since it can be - // known this far ahead of time, and it means the unique key code - // does not have to perform an additional pass over files. - // - // To avoid a mutex, the actual insertion of the asset to DevServer - // is done on the bundler thread. - try std.fmt.allocPrint( - allocator, - bun.bake.DevServer.asset_prefix ++ "/{s}{s}", - .{ - &std.fmt.bytesToHex(std.mem.asBytes(&content_hash), .lower), - std.fs.path.extension(source.path.text), - }, - ) - else - try std.fmt.allocPrint( - allocator, - "{any}A{d:0>8}", - .{ bun.fmt.hexIntLower(unique_key_prefix), source.index.get() }, - ); - const root = Expr.init(E.String, .{ .data = unique_key }, .{ .start = 0 }); - unique_key_for_additional_file.* = .{ - .key = unique_key, - .content_hash = content_hash, - }; - var ast = JSAst.init((try js_parser.newLazyExportAST(allocator, transpiler.options.define, opts, log, root, &source, "")).?); - ast.addUrlForCss(allocator, &source, null, unique_key); - return ast; - }, - } - } - - fn getCodeForParseTaskWithoutPlugins( - task: *ParseTask, - log: *Logger.Log, - transpiler: *Transpiler, - resolver: *Resolver, - allocator: std.mem.Allocator, - file_path: *Fs.Path, - loader: Loader, - ) !CacheEntry { - return switch (task.contents_or_fd) { - .fd => |contents| brk: { - const trace = bun.perf.trace("Bundler.readFile"); - defer trace.end(); - - if (strings.eqlComptime(file_path.namespace, "node")) lookup_builtin: { - if (task.ctx.framework) |f| { - if (f.built_in_modules.get(file_path.text)) |file| { - switch (file) { - .code => |code| break :brk .{ .contents = code, .fd = bun.invalid_fd }, - .import => |path| { - file_path.* = Fs.Path.init(path); - break :lookup_builtin; - }, - } - } - } - - break :brk .{ - .contents = NodeFallbackModules.contentsFromPath(file_path.text) orelse "", - .fd = bun.invalid_fd, - }; - } - - break :brk resolver.caches.fs.readFileWithAllocator( - // TODO: this allocator may be wrong for native plugins - if (loader.shouldCopyForBundling()) - // The OutputFile will own the memory for the contents - bun.default_allocator - else - allocator, - transpiler.fs, - file_path.text, - task.contents_or_fd.fd.dir, - false, - contents.file.unwrapValid(), - ) catch |err| { - const source = &Logger.Source.initEmptyFile(log.msgs.allocator.dupe(u8, file_path.text) catch unreachable); - switch (err) { - error.ENOENT, error.FileNotFound => { - log.addErrorFmt( - source, - Logger.Loc.Empty, - allocator, - "File not found {}", - .{bun.fmt.quote(file_path.text)}, - ) catch {}; - return error.FileNotFound; - }, - else => { - log.addErrorFmt( - source, - Logger.Loc.Empty, - allocator, - "{s} reading file: {}", - .{ @errorName(err), bun.fmt.quote(file_path.text) }, - ) catch {}; - }, - } - return err; - }; - }, - .contents => |contents| .{ - .contents = contents, - .fd = bun.invalid_fd, - }, - }; - } - - fn getCodeForParseTask( - task: *ParseTask, - log: *Logger.Log, - transpiler: *Transpiler, - resolver: *Resolver, - allocator: std.mem.Allocator, - file_path: *Fs.Path, - loader: *Loader, - from_plugin: *bool, - ) !CacheEntry { - const might_have_on_parse_plugins = brk: { - if (task.source_index.isRuntime()) break :brk false; - const plugin = task.ctx.plugins orelse break :brk false; - if (!plugin.hasOnBeforeParsePlugins()) break :brk false; - - if (strings.eqlComptime(file_path.namespace, "node")) { - break :brk false; - } - break :brk true; - }; - - if (!might_have_on_parse_plugins) { - return getCodeForParseTaskWithoutPlugins(task, log, transpiler, resolver, allocator, file_path, loader.*); - } - - var should_continue_running: i32 = 1; - - var ctx = OnBeforeParsePlugin{ - .task = task, - .log = log, - .transpiler = transpiler, - .resolver = resolver, - .allocator = allocator, - .file_path = file_path, - .loader = loader, - .deferred_error = null, - .should_continue_running = &should_continue_running, - }; - - return try ctx.run(task.ctx.plugins.?, from_plugin); - } - - const OnBeforeParsePlugin = struct { - task: *ParseTask, - log: *Logger.Log, - transpiler: *Transpiler, - resolver: *Resolver, - allocator: std.mem.Allocator, - file_path: *Fs.Path, - loader: *Loader, - deferred_error: ?anyerror = null, - should_continue_running: *i32, - - result: ?*OnBeforeParseResult = null, - - const headers = bun.c; - - comptime { - bun.assert(@sizeOf(OnBeforeParseArguments) == @sizeOf(headers.OnBeforeParseArguments)); - bun.assert(@alignOf(OnBeforeParseArguments) == @alignOf(headers.OnBeforeParseArguments)); - - bun.assert(@sizeOf(BunLogOptions) == @sizeOf(headers.BunLogOptions)); - bun.assert(@alignOf(BunLogOptions) == @alignOf(headers.BunLogOptions)); - - bun.assert(@sizeOf(OnBeforeParseResult) == @sizeOf(headers.OnBeforeParseResult)); - bun.assert(@alignOf(OnBeforeParseResult) == @alignOf(headers.OnBeforeParseResult)); - - bun.assert(@sizeOf(BunLogOptions) == @sizeOf(headers.BunLogOptions)); - bun.assert(@alignOf(BunLogOptions) == @alignOf(headers.BunLogOptions)); - } - - const OnBeforeParseArguments = extern struct { - struct_size: usize = @sizeOf(OnBeforeParseArguments), - context: *OnBeforeParsePlugin, - path_ptr: ?[*]const u8 = "", - path_len: usize = 0, - namespace_ptr: ?[*]const u8 = "file", - namespace_len: usize = "file".len, - default_loader: Loader = .file, - external: ?*anyopaque = null, - }; - - const BunLogOptions = extern struct { - struct_size: usize = @sizeOf(BunLogOptions), - message_ptr: ?[*]const u8 = null, - message_len: usize = 0, - path_ptr: ?[*]const u8 = null, - path_len: usize = 0, - source_line_text_ptr: ?[*]const u8 = null, - source_line_text_len: usize = 0, - level: Logger.Log.Level = .err, - line: i32 = 0, - column: i32 = 0, - line_end: i32 = 0, - column_end: i32 = 0, - - pub fn sourceLineText(this: *const BunLogOptions) string { - if (this.source_line_text_ptr) |ptr| { - if (this.source_line_text_len > 0) { - return ptr[0..this.source_line_text_len]; - } - } - return ""; - } - - pub fn path(this: *const BunLogOptions) string { - if (this.path_ptr) |ptr| { - if (this.path_len > 0) { - return ptr[0..this.path_len]; - } - } - return ""; - } - - pub fn message(this: *const BunLogOptions) string { - if (this.message_ptr) |ptr| { - if (this.message_len > 0) { - return ptr[0..this.message_len]; - } - } - return ""; - } - - pub fn append(this: *const BunLogOptions, log: *Logger.Log, namespace: string) void { - const allocator = log.msgs.allocator; - const source_line_text = this.sourceLineText(); - const location = Logger.Location.init( - this.path(), - namespace, - @max(this.line, -1), - @max(this.column, -1), - @max(this.column_end - this.column, 0), - if (source_line_text.len > 0) allocator.dupe(u8, source_line_text) catch bun.outOfMemory() else null, - null, - ); - var msg = Logger.Msg{ .data = .{ .location = location, .text = allocator.dupe(u8, this.message()) catch bun.outOfMemory() } }; - switch (this.level) { - .err => msg.kind = .err, - .warn => msg.kind = .warn, - .verbose => msg.kind = .verbose, - .debug => msg.kind = .debug, - else => {}, - } - if (msg.kind == .err) { - log.errors += 1; - } else if (msg.kind == .warn) { - log.warnings += 1; - } - log.addMsg(msg) catch bun.outOfMemory(); - } - - pub fn logFn( - args_: ?*OnBeforeParseArguments, - log_options_: ?*BunLogOptions, - ) callconv(.C) void { - const args = args_ orelse return; - const log_options = log_options_ orelse return; - log_options.append(args.context.log, args.context.file_path.namespace); - } - }; - - const OnBeforeParseResultWrapper = extern struct { - original_source: ?[*]const u8 = null, - original_source_len: usize = 0, - original_source_fd: bun.FileDescriptor = bun.invalid_fd, - loader: Loader, - check: if (bun.Environment.isDebug) u32 else u0 = if (bun.Environment.isDebug) 42069 else 0, // Value to ensure OnBeforeParseResult is wrapped in this struct - result: OnBeforeParseResult, - }; - - const OnBeforeParseResult = extern struct { - struct_size: usize = @sizeOf(OnBeforeParseResult), - source_ptr: ?[*]const u8 = null, - source_len: usize = 0, - loader: Loader, - - fetch_source_code_fn: *const fn (*OnBeforeParseArguments, *OnBeforeParseResult) callconv(.C) i32 = &fetchSourceCode, - - user_context: ?*anyopaque = null, - free_user_context: ?*const fn (?*anyopaque) callconv(.C) void = null, - - log: *const fn ( - args_: ?*OnBeforeParseArguments, - log_options_: ?*BunLogOptions, - ) callconv(.C) void = &BunLogOptions.logFn, - - pub fn getWrapper(result: *OnBeforeParseResult) *OnBeforeParseResultWrapper { - const wrapper: *OnBeforeParseResultWrapper = @fieldParentPtr("result", result); - bun.debugAssert(wrapper.check == 42069); - return wrapper; - } - }; - - pub fn fetchSourceCode(args: *OnBeforeParseArguments, result: *OnBeforeParseResult) callconv(.C) i32 { - debug("fetchSourceCode", .{}); - const this = args.context; - if (this.log.errors > 0 or this.deferred_error != null or this.should_continue_running.* != 1) { - return 1; - } - - if (result.source_ptr != null) { - return 0; - } - - const entry = getCodeForParseTaskWithoutPlugins( - this.task, - this.log, - this.transpiler, - this.resolver, - this.allocator, - this.file_path, - - result.loader, - ) catch |err| { - this.deferred_error = err; - this.should_continue_running.* = 0; - return 1; - }; - result.source_ptr = entry.contents.ptr; - result.source_len = entry.contents.len; - result.free_user_context = null; - result.user_context = null; - const wrapper: *OnBeforeParseResultWrapper = result.getWrapper(); - wrapper.original_source = entry.contents.ptr; - wrapper.original_source_len = entry.contents.len; - wrapper.original_source_fd = entry.fd; - return 0; - } - - pub export fn OnBeforeParseResult__reset(this: *OnBeforeParseResult) void { - const wrapper = this.getWrapper(); - this.loader = wrapper.loader; - if (wrapper.original_source) |src_ptr| { - const src = src_ptr[0..wrapper.original_source_len]; - this.source_ptr = src.ptr; - this.source_len = src.len; - } else { - this.source_ptr = null; - this.source_len = 0; - } - } - - pub export fn OnBeforeParsePlugin__isDone(this: *OnBeforeParsePlugin) i32 { - if (this.should_continue_running.* != 1) { - return 1; - } - - const result = this.result orelse return 1; - // The first plugin to set the source wins. - // But, we must check that they actually modified it - // since fetching the source stores it inside `result.source_ptr` - if (result.source_ptr != null) { - const wrapper: *OnBeforeParseResultWrapper = result.getWrapper(); - return @intFromBool(result.source_ptr.? != wrapper.original_source.?); - } - - return 0; - } - - pub fn run(this: *OnBeforeParsePlugin, plugin: *JSC.API.JSBundler.Plugin, from_plugin: *bool) !CacheEntry { - var args = OnBeforeParseArguments{ - .context = this, - .path_ptr = this.file_path.text.ptr, - .path_len = this.file_path.text.len, - .default_loader = this.loader.*, - }; - if (this.file_path.namespace.len > 0) { - args.namespace_ptr = this.file_path.namespace.ptr; - args.namespace_len = this.file_path.namespace.len; - } - var wrapper = OnBeforeParseResultWrapper{ - .loader = this.loader.*, - .result = OnBeforeParseResult{ - .loader = this.loader.*, - }, - }; - - this.result = &wrapper.result; - const count = plugin.callOnBeforeParsePlugins( - this, - if (bun.strings.eqlComptime(this.file_path.namespace, "file")) - &bun.String.empty - else - &bun.String.init(this.file_path.namespace), - - &bun.String.init(this.file_path.text), - &args, - &wrapper.result, - this.should_continue_running, - ); - if (comptime Environment.enable_logs) - debug("callOnBeforeParsePlugins({s}:{s}) = {d}", .{ this.file_path.namespace, this.file_path.text, count }); - if (count > 0) { - if (this.deferred_error) |err| { - if (wrapper.result.free_user_context) |free_user_context| { - free_user_context(wrapper.result.user_context); - } - - return err; - } - - // If the plugin sets the `free_user_context` function pointer, it _must_ set the `user_context` pointer. - // Otherwise this is just invalid behavior. - if (wrapper.result.user_context == null and wrapper.result.free_user_context != null) { - var msg = Logger.Msg{ .data = .{ .location = null, .text = bun.default_allocator.dupe( - u8, - "Native plugin set the `free_plugin_source_code_context` field without setting the `plugin_source_code_context` field.", - ) catch bun.outOfMemory() } }; - msg.kind = .err; - args.context.log.errors += 1; - args.context.log.addMsg(msg) catch bun.outOfMemory(); - return error.InvalidNativePlugin; - } - - if (this.log.errors > 0) { - if (wrapper.result.free_user_context) |free_user_context| { - free_user_context(wrapper.result.user_context); - } - - return error.SyntaxError; - } - - if (wrapper.result.source_ptr) |ptr| { - if (wrapper.result.free_user_context != null) { - this.task.external_free_function = .{ - .ctx = wrapper.result.user_context, - .function = wrapper.result.free_user_context, - }; - } - from_plugin.* = true; - this.loader.* = wrapper.result.loader; - return .{ - .contents = ptr[0..wrapper.result.source_len], - .external_free_function = .{ - .ctx = wrapper.result.user_context, - .function = wrapper.result.free_user_context, - }, - .fd = wrapper.original_source_fd, - }; - } - } - - return try getCodeForParseTaskWithoutPlugins(this.task, this.log, this.transpiler, this.resolver, this.allocator, this.file_path, this.loader.*); - } - }; - - fn getSourceCode( - task: *ParseTask, - this: *ThreadPool.Worker, - log: *Logger.Log, - ) anyerror!CacheEntry { - const allocator = this.allocator; - - var data = this.data; - var transpiler = &data.transpiler; - errdefer transpiler.resetStore(); - const resolver: *Resolver = &transpiler.resolver; - var file_path = task.path; - var loader = task.loader orelse file_path.loader(&transpiler.options.loaders) orelse options.Loader.file; - - // Do not process files as HTML if any of the following are true: - // - building for node or bun.js - // - // We allow non-entrypoints to import HTML so that people could - // potentially use an onLoad plugin that returns HTML. - if (task.known_target != .browser) { - loader = loader.disableHTML(); - task.loader = loader; - } - - var contents_came_from_plugin: bool = false; - return try getCodeForParseTask(task, log, transpiler, resolver, allocator, &file_path, &loader, &contents_came_from_plugin); - } - - fn runWithSourceCode( - task: *ParseTask, - this: *ThreadPool.Worker, - step: *ParseTask.Result.Error.Step, - log: *Logger.Log, - entry: *CacheEntry, - ) anyerror!Result.Success { - const allocator = this.allocator; - - var data = this.data; - var transpiler = &data.transpiler; - errdefer transpiler.resetStore(); - var resolver: *Resolver = &transpiler.resolver; - var file_path = task.path; - var loader = task.loader orelse file_path.loader(&transpiler.options.loaders) orelse options.Loader.file; - - // Do not process files as HTML if any of the following are true: - // - building for node or bun.js - // - // We allow non-entrypoints to import HTML so that people could - // potentially use an onLoad plugin that returns HTML. - if (task.known_target != .browser) { - loader = loader.disableHTML(); - task.loader = loader; - } - - // WARNING: Do not change the variant of `task.contents_or_fd` from - // `.fd` to `.contents` (or back) after this point! - // - // When `task.contents_or_fd == .fd`, `entry.contents` is an owned string. - // When `task.contents_or_fd == .contents`, `entry.contents` is NOT owned! Freeing it here will cause a double free! - // - // Changing from `.contents` to `.fd` will cause a double free. - // This was the case in the situation where the ParseTask receives its `.contents` from an onLoad plugin, which caused it to be - // allocated by `bun.default_allocator` and then freed in `BundleV2.deinit` (and also by `entry.deinit(allocator)` below). - const debug_original_variant_check: if (bun.Environment.isDebug) ContentsOrFd.Tag else void = - if (bun.Environment.isDebug) @as(ContentsOrFd.Tag, task.contents_or_fd); - errdefer { - if (comptime bun.Environment.isDebug) { - if (@as(ContentsOrFd.Tag, task.contents_or_fd) != debug_original_variant_check) { - std.debug.panic("BUG: `task.contents_or_fd` changed in a way that will cause a double free or memory to leak!\n\n Original = {s}\n New = {s}\n", .{ - @tagName(debug_original_variant_check), - @tagName(task.contents_or_fd), - }); - } - } - if (task.contents_or_fd == .fd) entry.deinit(allocator); - } - - const will_close_file_descriptor = task.contents_or_fd == .fd and - entry.fd.isValid() and - entry.fd.stdioTag() == null and - this.ctx.bun_watcher == null; - if (will_close_file_descriptor) { - _ = entry.closeFD(); - task.contents_or_fd = .{ .fd = .{ - .file = bun.invalid_fd, - .dir = bun.invalid_fd, - } }; - } else if (task.contents_or_fd == .fd) { - task.contents_or_fd = .{ .fd = .{ - .file = entry.fd, - .dir = bun.invalid_fd, - } }; - } - step.* = .parse; - - const is_empty = strings.isAllWhitespace(entry.contents); - - const use_directive: UseDirective = if (!is_empty and transpiler.options.server_components) - if (UseDirective.parse(entry.contents)) |use| - use - else - .none - else - .none; - - if ( - // separate_ssr_graph makes boundaries switch to client because the server file uses that generated file as input. - // this is not done when there is one server graph because it is easier for plugins to deal with. - (use_directive == .client and - task.known_target != .bake_server_components_ssr and - this.ctx.framework.?.server_components.?.separate_ssr_graph) or - // set the target to the client when bundling client-side files - ((transpiler.options.server_components or transpiler.options.dev_server != null) and - task.known_target == .browser)) - { - transpiler = this.ctx.client_transpiler; - resolver = &transpiler.resolver; - bun.assert(transpiler.options.target == .browser); - } - - var source = Logger.Source{ - .path = file_path, - .index = task.source_index, - .contents = entry.contents, - .contents_is_recycled = false, - }; - - const target = (if (task.source_index.get() == 1) targetFromHashbang(entry.contents) else null) orelse - if (task.known_target == .bake_server_components_ssr and transpiler.options.framework.?.server_components.?.separate_ssr_graph) - .bake_server_components_ssr - else - transpiler.options.target; - - const output_format = transpiler.options.output_format; - - var opts = js_parser.Parser.Options.init(task.jsx, loader); - opts.bundle = true; - opts.warn_about_unbundled_modules = false; - opts.macro_context = &this.data.macro_context; - opts.package_version = task.package_version; - - opts.features.allow_runtime = !source.index.isRuntime(); - opts.features.unwrap_commonjs_to_esm = output_format == .esm and FeatureFlags.unwrap_commonjs_to_esm; - opts.features.top_level_await = output_format == .esm or output_format == .internal_bake_dev; - opts.features.auto_import_jsx = task.jsx.parse and transpiler.options.auto_import_jsx; - opts.features.trim_unused_imports = loader.isTypeScript() or (transpiler.options.trim_unused_imports orelse false); - opts.features.inlining = transpiler.options.minify_syntax; - opts.output_format = output_format; - opts.features.minify_syntax = transpiler.options.minify_syntax; - opts.features.minify_identifiers = transpiler.options.minify_identifiers; - opts.features.emit_decorator_metadata = transpiler.options.emit_decorator_metadata; - opts.features.unwrap_commonjs_packages = transpiler.options.unwrap_commonjs_packages; - opts.features.hot_module_reloading = output_format == .internal_bake_dev and !source.index.isRuntime(); - opts.features.auto_polyfill_require = output_format == .esm and !opts.features.hot_module_reloading; - opts.features.react_fast_refresh = target == .browser and - transpiler.options.react_fast_refresh and - loader.isJSX() and - !source.path.isNodeModule(); - - opts.features.server_components = if (transpiler.options.server_components) switch (target) { - .browser => .client_side, - else => switch (use_directive) { - .none => .wrap_anon_server_functions, - .client => if (transpiler.options.framework.?.server_components.?.separate_ssr_graph) - .client_side - else - .wrap_exports_for_client_reference, - .server => .wrap_exports_for_server_reference, - }, - } else .none; - - opts.framework = transpiler.options.framework; - - opts.ignore_dce_annotations = transpiler.options.ignore_dce_annotations and !source.index.isRuntime(); - - // For files that are not user-specified entrypoints, set `import.meta.main` to `false`. - // Entrypoints will have `import.meta.main` set as "unknown", unless we use `--compile`, - // in which we inline `true`. - if (transpiler.options.inline_entrypoint_import_meta_main or !task.is_entry_point) { - opts.import_meta_main_value = task.is_entry_point and transpiler.options.dev_server == null; - } else if (target == .node) { - opts.lower_import_meta_main_for_node_js = true; - } - - opts.tree_shaking = if (source.index.isRuntime()) true else transpiler.options.tree_shaking; - opts.module_type = task.module_type; - - task.jsx.parse = loader.isJSX(); - - var unique_key_for_additional_file: FileLoaderHash = .{ - .key = "", - .content_hash = 0, - }; - var ast: JSAst = if (!is_empty or loader.handlesEmptyFile()) - try getAST(log, transpiler, opts, allocator, resolver, source, loader, task.ctx.unique_key, &unique_key_for_additional_file, &task.ctx.linker.has_any_css_locals) - else switch (opts.module_type == .esm) { - inline else => |as_undefined| if (loader.isCSS()) try getEmptyCSSAST( - log, - transpiler, - opts, - allocator, - source, - ) else try getEmptyAST( - log, - transpiler, - opts, - allocator, - source, - if (as_undefined) E.Undefined else E.Object, - ), - }; - - ast.target = target; - if (ast.parts.len <= 1 and ast.css == null and (task.loader == null or task.loader.? != .html)) { - task.side_effects = .no_side_effects__empty_ast; - } - - // bun.debugAssert(ast.parts.len > 0); // when parts.len == 0, it is assumed to be pending/failed. empty ast has at least 1 part. - - step.* = .resolve; - - return .{ - .ast = ast, - .source = source, - .log = log.*, - .use_directive = use_directive, - .unique_key_for_additional_file = unique_key_for_additional_file.key, - .side_effects = task.side_effects, - .loader = loader, - - // Hash the files in here so that we do it in parallel. - .content_hash_for_additional_file = if (loader.shouldCopyForBundling()) - unique_key_for_additional_file.content_hash - else - 0, - }; - } - - fn ioTaskCallback(task: *ThreadPoolLib.Task) void { - runFromThreadPool(@fieldParentPtr("io_task", task)); - } - - fn taskCallback(task: *ThreadPoolLib.Task) void { - runFromThreadPool(@fieldParentPtr("task", task)); - } - - pub fn runFromThreadPool(this: *ParseTask) void { - var worker = ThreadPool.Worker.get(this.ctx); - defer worker.unget(); - debug("ParseTask(0x{x}, {s}) callback", .{ @intFromPtr(this), this.path.text }); - - var step: ParseTask.Result.Error.Step = .pending; - var log = Logger.Log.init(worker.allocator); - bun.assert(this.source_index.isValid()); // forgot to set source_index - - const value: ParseTask.Result.Value = value: { - if (this.stage == .needs_source_code) { - this.stage = .{ - .needs_parse = getSourceCode(this, worker, &log) catch |err| { - break :value .{ .err = .{ - .err = err, - .step = step, - .log = log, - .source_index = this.source_index, - .target = this.known_target, - } }; - }, - }; - - if (log.hasErrors()) { - break :value .{ .err = .{ - .err = error.SyntaxError, - .step = step, - .log = log, - .source_index = this.source_index, - .target = this.known_target, - } }; - } - - if (this.ctx.graph.pool.usesIOPool()) { - this.ctx.graph.pool.scheduleInsideThreadPool(this); - return; - } - } - - if (runWithSourceCode(this, worker, &step, &log, &this.stage.needs_parse)) |ast| { - // When using HMR, always flag asts with errors as parse failures. - // Not done outside of the dev server out of fear of breaking existing code. - if (this.ctx.transpiler.options.dev_server != null and ast.log.hasErrors()) { - break :value .{ - .err = .{ - .err = error.SyntaxError, - .step = .parse, - .log = ast.log, - .source_index = this.source_index, - .target = this.known_target, - }, - }; - } - - break :value .{ .success = ast }; - } else |err| { - if (err == error.EmptyAST) { - log.deinit(); - break :value .{ .empty = .{ - .source_index = this.source_index, - } }; - } - - break :value .{ .err = .{ - .err = err, - .step = step, - .log = log, - .source_index = this.source_index, - .target = this.known_target, - } }; - } - }; - - const result = bun.default_allocator.create(Result) catch bun.outOfMemory(); - - result.* = .{ - .ctx = this.ctx, - .task = .{}, - .value = value, - .external = this.external_free_function, - .watcher_data = switch (this.contents_or_fd) { - .fd => |fd| .{ .fd = fd.file, .dir_fd = fd.dir }, - .contents => .none, - }, - }; - - switch (worker.ctx.loop().*) { - .js => |jsc_event_loop| { - jsc_event_loop.enqueueTaskConcurrent(JSC.ConcurrentTask.fromCallback(result, onComplete)); - }, - .mini => |*mini| { - mini.enqueueTaskConcurrentWithExtraCtx( - Result, - BundleV2, - result, - BundleV2.onParseTaskComplete, - .task, - ); - }, - } - } - - pub fn onComplete(result: *Result) void { - BundleV2.onParseTaskComplete(result, result.ctx); - } -}; - -/// Files for Server Components are generated using `AstBuilder`, instead of -/// running through the js_parser. It emits a ParseTask.Result and joins -/// with the same logic that it runs though. -pub const ServerComponentParseTask = struct { - task: ThreadPoolLib.Task = .{ .callback = &taskCallbackWrap }, - data: Data, - ctx: *BundleV2, - source: Logger.Source, - - pub const Data = union(enum) { - /// Generate server-side code for a "use client" module. Given the - /// client ast, a "reference proxy" is created with identical exports. - client_reference_proxy: ReferenceProxy, - - client_entry_wrapper: ClientEntryWrapper, - - pub const ReferenceProxy = struct { - other_source: Logger.Source, - named_exports: JSAst.NamedExports, - }; - - pub const ClientEntryWrapper = struct { - path: []const u8, - }; - }; - - fn taskCallbackWrap(thread_pool_task: *ThreadPoolLib.Task) void { - const task: *ServerComponentParseTask = @fieldParentPtr("task", thread_pool_task); - var worker = ThreadPool.Worker.get(task.ctx); - defer worker.unget(); - var log = Logger.Log.init(worker.allocator); - - const result = bun.default_allocator.create(ParseTask.Result) catch bun.outOfMemory(); - result.* = .{ - .ctx = task.ctx, - .task = undefined, - - .value = if (taskCallback( - task, - &log, - worker.allocator, - )) |success| - .{ .success = success } - else |err| switch (err) { - error.OutOfMemory => bun.outOfMemory(), - }, - - .watcher_data = .none, - }; - - switch (worker.ctx.loop().*) { - .js => |jsc_event_loop| { - jsc_event_loop.enqueueTaskConcurrent(JSC.ConcurrentTask.fromCallback(result, ParseTask.onComplete)); - }, - .mini => |*mini| { - mini.enqueueTaskConcurrentWithExtraCtx( - ParseTask.Result, - BundleV2, - result, - BundleV2.onParseTaskComplete, - .task, - ); - }, - } - } - - fn taskCallback( - task: *ServerComponentParseTask, - log: *Logger.Log, - allocator: std.mem.Allocator, - ) bun.OOM!ParseTask.Result.Success { - var ab = try AstBuilder.init(allocator, &task.source, task.ctx.transpiler.options.hot_module_reloading); - - switch (task.data) { - .client_reference_proxy => |data| try task.generateClientReferenceProxy(data, &ab), - .client_entry_wrapper => |data| try task.generateClientEntryWrapper(data, &ab), - } - - return .{ - .ast = try ab.toBundledAst(switch (task.data) { - // Server-side - .client_reference_proxy => task.ctx.transpiler.options.target, - // Client-side, - .client_entry_wrapper => .browser, - }), - .source = task.source, - .loader = .js, - .log = log.*, - .use_directive = .none, - .side_effects = .no_side_effects__pure_data, - }; - } - - fn generateClientEntryWrapper(_: *ServerComponentParseTask, data: Data.ClientEntryWrapper, b: *AstBuilder) !void { - const record = try b.addImportRecord(data.path, .stmt); - const namespace_ref = try b.newSymbol(.other, "main"); - try b.appendStmt(S.Import{ - .namespace_ref = namespace_ref, - .import_record_index = record, - .items = &.{}, - .is_single_line = true, - }); - b.import_records.items[record].was_originally_bare_import = true; - } - - fn generateClientReferenceProxy(task: *ServerComponentParseTask, data: Data.ReferenceProxy, b: *AstBuilder) !void { - const server_components = task.ctx.framework.?.server_components orelse - unreachable; // config must be non-null to enter this function - - const client_named_exports = data.named_exports; - - const register_client_reference = (try b.addImportStmt( - server_components.server_runtime_import, - &.{server_components.server_register_client_reference}, - ))[0]; - - const module_path = b.newExpr(E.String{ - // In development, the path loaded is the source file: Easy! - // - // In production, the path here must be the final chunk path, but - // that information is not yet available since chunks are not - // computed. The unique_key replacement system is used here. - .data = if (task.ctx.transpiler.options.dev_server != null) - data.other_source.path.pretty - else - try std.fmt.allocPrint(b.allocator, "{}S{d:0>8}", .{ - bun.fmt.hexIntLower(task.ctx.unique_key), - data.other_source.index.get(), - }), - }); - - for (client_named_exports.keys()) |key| { - const is_default = bun.strings.eqlComptime(key, "default"); - - // This error message is taken from - // https://github.com/facebook/react/blob/c5b9375767e2c4102d7e5559d383523736f1c902/packages/react-server-dom-webpack/src/ReactFlightWebpackNodeLoader.js#L323-L354 - const err_msg_string = try if (is_default) - std.fmt.allocPrint( - b.allocator, - "Attempted to call the default export of {[module_path]s} from " ++ - "the server, but it's on the client. It's not possible to invoke a " ++ - "client function from the server, it can only be rendered as a " ++ - "Component or passed to props of a Client Component.", - .{ .module_path = data.other_source.path.pretty }, - ) - else - std.fmt.allocPrint( - b.allocator, - "Attempted to call {[key]s}() from the server but {[key]s} " ++ - "is on the client. It's not possible to invoke a client function from " ++ - "the server, it can only be rendered as a Component or passed to " ++ - "props of a Client Component.", - .{ .key = key }, - ); - - // throw new Error(...) - const err_msg = b.newExpr(E.New{ - .target = b.newExpr(E.Identifier{ - .ref = try b.newExternalSymbol("Error"), - }), - .args = try BabyList(Expr).fromSlice(b.allocator, &.{ - b.newExpr(E.String{ .data = err_msg_string }), - }), - .close_parens_loc = Logger.Loc.Empty, - }); - - // registerClientReference( - // () => { throw new Error(...) }, - // "src/filepath.tsx", - // "Comp" - // ); - const value = b.newExpr(E.Call{ - .target = register_client_reference, - .args = try js_ast.ExprNodeList.fromSlice(b.allocator, &.{ - b.newExpr(E.Arrow{ .body = .{ - .stmts = try b.allocator.dupe(Stmt, &.{ - b.newStmt(S.Throw{ .value = err_msg }), - }), - .loc = Logger.Loc.Empty, - } }), - module_path, - b.newExpr(E.String{ .data = key }), - }), - }); - - if (is_default) { - // export default registerClientReference(...); - try b.appendStmt(S.ExportDefault{ .value = .{ .expr = value }, .default_name = .{} }); - } else { - // export const Component = registerClientReference(...); - const export_ref = try b.newSymbol(.other, key); - try b.appendStmt(S.Local{ - .decls = try G.Decl.List.fromSlice(b.allocator, &.{.{ - .binding = Binding.alloc(b.allocator, B.Identifier{ .ref = export_ref }, Logger.Loc.Empty), - .value = value, - }}), - .is_export = true, - .kind = .k_const, - }); - } - } - } -}; +pub const UseDirective = js_ast.UseDirective; +pub const ServerComponentBoundary = js_ast.ServerComponentBoundary; +pub const ServerComponentParseTask = @import("./ServerComponentParseTask.zig").ServerComponentParseTask; const IdentityContext = @import("../identity_context.zig").IdentityContext; const RefVoidMap = std.ArrayHashMapUnmanaged(Ref, void, Ref.ArrayHashCtx, false); -const RefImportData = std.ArrayHashMapUnmanaged(Ref, ImportData, Ref.ArrayHashCtx, false); +pub const RefImportData = std.ArrayHashMapUnmanaged(Ref, ImportData, Ref.ArrayHashCtx, false); pub const ResolvedExports = bun.StringArrayHashMapUnmanaged(ExportData); -const TopLevelSymbolToParts = js_ast.Ast.TopLevelSymbolToParts; +pub const TopLevelSymbolToParts = js_ast.Ast.TopLevelSymbolToParts; pub const WrapKind = enum(u2) { none, @@ -5713,118 +3562,12 @@ pub const JSMeta = struct { }; }; -pub const Graph = struct { - pool: *ThreadPool, - heap: ThreadlocalArena = .{}, - /// This allocator is thread-local to the Bundler thread - /// .allocator == .heap.allocator() - allocator: std.mem.Allocator = undefined, - - /// Mapping user-specified entry points to their Source Index - entry_points: std.ArrayListUnmanaged(Index) = .{}, - /// Every source index has an associated InputFile - input_files: MultiArrayList(InputFile) = .{}, - /// Every source index has an associated Ast - /// When a parse is in progress / queued, it is `Ast.empty` - ast: MultiArrayList(JSAst) = .{}, - - /// During the scan + parse phase, this value keeps a count of the remaining - /// tasks. Once it hits zero, the scan phase ends and linking begins. Note - /// that if `deferred_pending > 0`, it means there are plugin callbacks - /// to invoke before linking, which can initiate another scan phase. - /// - /// Increment and decrement this via `incrementScanCounter` and - /// `decrementScanCounter`, as asynchronous bundles check for `0` in the - /// decrement function, instead of at the top of the event loop. - /// - /// - Parsing a file (ParseTask and ServerComponentParseTask) - /// - onResolve and onLoad functions - /// - Resolving an onDefer promise - pending_items: u32 = 0, - /// When an `onLoad` plugin calls `.defer()`, the count from `pending_items` - /// is "moved" into this counter (pending_items -= 1; deferred_pending += 1) - /// - /// When `pending_items` hits zero and there are deferred pending tasks, those - /// tasks will be run, and the count is "moved" back to `pending_items` - deferred_pending: u32 = 0, - - /// Maps a hashed path string to a source index, if it exists in the compilation. - /// Instead of accessing this directly, consider using BundleV2.pathToSourceIndexMap - path_to_source_index_map: PathToSourceIndexMap = .{}, - /// When using server components, a completely separate file listing is - /// required to avoid incorrect inlining of defines and dependencies on - /// other files. This is relevant for files shared between server and client - /// and have no "use " directive, and must be duplicated. - /// - /// To make linking easier, this second graph contains indices into the - /// same `.ast` and `.input_files` arrays. - client_path_to_source_index_map: PathToSourceIndexMap = .{}, - /// When using server components with React, there is an additional module - /// graph which is used to contain SSR-versions of all client components; - /// the SSR graph. The difference between the SSR graph and the server - /// graph is that this one does not apply '--conditions react-server' - /// - /// In Bun's React Framework, it includes SSR versions of 'react' and - /// 'react-dom' (an export condition is used to provide a different - /// implementation for RSC, which is potentially how they implement - /// server-only features such as async components). - ssr_path_to_source_index_map: PathToSourceIndexMap = .{}, - - /// When Server Components is enabled, this holds a list of all boundary - /// files. This happens for all files with a "use " directive. - server_component_boundaries: ServerComponentBoundary.List = .{}, - - estimated_file_loader_count: usize = 0, - - /// For Bake, a count of the CSS asts is used to make precise - /// pre-allocations without re-iterating the file listing. - css_file_count: usize = 0, - - additional_output_files: std.ArrayListUnmanaged(options.OutputFile) = .{}, - - kit_referenced_server_data: bool, - kit_referenced_client_data: bool, - - pub const InputFile = struct { - source: Logger.Source, - loader: options.Loader = options.Loader.file, - side_effects: _resolver.SideEffects, - allocator: std.mem.Allocator = bun.default_allocator, - additional_files: BabyList(AdditionalFile) = .{}, - unique_key_for_additional_file: string = "", - content_hash_for_additional_file: u64 = 0, - is_plugin_file: bool = false, - }; - - /// Schedule a task to be run on the JS thread which resolves the promise of - /// each `.defer()` called in an onLoad plugin. - /// - /// Returns true if there were more tasks queued. - pub fn drainDeferredTasks(this: *@This(), transpiler: *BundleV2) bool { - transpiler.thread_lock.assertLocked(); - - if (this.deferred_pending > 0) { - this.pending_items += this.deferred_pending; - this.deferred_pending = 0; - - transpiler.drain_defer_task.init(); - transpiler.drain_defer_task.schedule(); - - return true; - } - - return false; - } -}; - pub const AdditionalFile = union(enum) { source_index: Index.Int, output_file: Index.Int, }; -pub const PathToSourceIndexMap = std.HashMapUnmanaged(u64, Index.Int, IdentityContext(u64), 80); - -const EntryPoint = struct { +pub const EntryPoint = struct { /// This may be an absolute path or a relative path. If absolute, it will /// eventually be turned into a relative path by computing the path relative /// to the "outbase" directory. Then this relative path will be joined onto @@ -5875,10952 +3618,13 @@ const AstSourceIDMapping = struct { source_index: Index.Int, }; -const LinkerGraph = struct { - const debug = Output.scoped(.LinkerGraph, false); - - files: File.List = .{}, - files_live: BitSet = undefined, - entry_points: EntryPoint.List = .{}, - symbols: js_ast.Symbol.Map = .{}, - - allocator: std.mem.Allocator, - - code_splitting: bool = false, - - // This is an alias from Graph - // it is not a clone! - ast: MultiArrayList(JSAst) = .{}, - meta: MultiArrayList(JSMeta) = .{}, - - /// We should avoid traversing all files in the bundle, because the linker - /// should be able to run a linking operation on a large bundle where only - /// a few files are needed (e.g. an incremental compilation scenario). This - /// holds all files that could possibly be reached through the entry points. - /// If you need to iterate over all files in the linking operation, iterate - /// over this array. This array is also sorted in a deterministic ordering - /// to help ensure deterministic builds (source indices are random). - reachable_files: []Index = &[_]Index{}, - - /// Index from `.parse_graph.input_files` to index in `.files` - stable_source_indices: []const u32 = &[_]u32{}, - - is_scb_bitset: BitSet = .{}, - has_client_components: bool = false, - has_server_components: bool = false, - - /// This is for cross-module inlining of detected inlinable constants - // const_values: js_ast.Ast.ConstValuesMap = .{}, - /// This is for cross-module inlining of TypeScript enum constants - ts_enums: js_ast.Ast.TsEnumsMap = .{}, - - pub fn init(allocator: std.mem.Allocator, file_count: usize) !LinkerGraph { - return LinkerGraph{ - .allocator = allocator, - .files_live = try BitSet.initEmpty(allocator, file_count), - }; - } - - pub fn runtimeFunction(this: *const LinkerGraph, name: string) Ref { - return this.ast.items(.named_exports)[Index.runtime.value].get(name).?.ref; - } - - pub fn generateNewSymbol(this: *LinkerGraph, source_index: u32, kind: Symbol.Kind, original_name: string) Ref { - const source_symbols = &this.symbols.symbols_for_source.slice()[source_index]; - - var ref = Ref.init( - @truncate(source_symbols.len), - @truncate(source_index), - false, - ); - ref.tag = .symbol; - - // TODO: will this crash on resize due to using threadlocal mimalloc heap? - source_symbols.push( - this.allocator, - .{ - .kind = kind, - .original_name = original_name, - }, - ) catch unreachable; - - this.ast.items(.module_scope)[source_index].generated.push(this.allocator, ref) catch unreachable; - return ref; - } - - pub fn generateRuntimeSymbolImportAndUse( - graph: *LinkerGraph, - source_index: Index.Int, - entry_point_part_index: Index, - name: []const u8, - count: u32, - ) !void { - if (count == 0) return; - debug("generateRuntimeSymbolImportAndUse({s}) for {d}", .{ name, source_index }); - - const ref = graph.runtimeFunction(name); - try graph.generateSymbolImportAndUse( - source_index, - entry_point_part_index.get(), - ref, - count, - Index.runtime, - ); - } - - pub fn addPartToFile( - graph: *LinkerGraph, - id: u32, - part: Part, - ) !u32 { - var parts: *Part.List = &graph.ast.items(.parts)[id]; - const part_id = @as(u32, @truncate(parts.len)); - try parts.push(graph.allocator, part); - var top_level_symbol_to_parts_overlay: ?*TopLevelSymbolToParts = null; - - const Iterator = struct { - graph: *LinkerGraph, - id: u32, - top_level_symbol_to_parts_overlay: *?*TopLevelSymbolToParts, - part_id: u32, - - pub fn next(self: *@This(), ref: Ref) void { - var overlay = brk: { - if (self.top_level_symbol_to_parts_overlay.*) |out| { - break :brk out; - } - - const out = &self.graph.meta.items(.top_level_symbol_to_parts_overlay)[self.id]; - - self.top_level_symbol_to_parts_overlay.* = out; - break :brk out; - }; - - var entry = overlay.getOrPut(self.graph.allocator, ref) catch unreachable; - if (!entry.found_existing) { - if (self.graph.ast.items(.top_level_symbols_to_parts)[self.id].get(ref)) |original_parts| { - var list = std.ArrayList(u32).init(self.graph.allocator); - list.ensureTotalCapacityPrecise(original_parts.len + 1) catch unreachable; - list.appendSliceAssumeCapacity(original_parts.slice()); - list.appendAssumeCapacity(self.part_id); - - entry.value_ptr.* = .init(list.items); - } else { - entry.value_ptr.* = BabyList(u32).fromSlice(self.graph.allocator, &.{self.part_id}) catch bun.outOfMemory(); - } - } else { - entry.value_ptr.push(self.graph.allocator, self.part_id) catch unreachable; - } - } - }; - - var ctx = Iterator{ - .graph = graph, - .id = id, - .part_id = part_id, - .top_level_symbol_to_parts_overlay = &top_level_symbol_to_parts_overlay, - }; - - js_ast.DeclaredSymbol.forEachTopLevelSymbol(&parts.ptr[part_id].declared_symbols, &ctx, Iterator.next); - - return part_id; - } - - pub fn generateSymbolImportAndUse( - g: *LinkerGraph, - source_index: u32, - part_index: u32, - ref: Ref, - use_count: u32, - source_index_to_import_from: Index, - ) !void { - if (use_count == 0) return; - - var parts_list = g.ast.items(.parts)[source_index].slice(); - var part: *Part = &parts_list[part_index]; - - // Mark this symbol as used by this part - - var uses = &part.symbol_uses; - var uses_entry = uses.getOrPut(g.allocator, ref) catch unreachable; - - if (!uses_entry.found_existing) { - uses_entry.value_ptr.* = .{ .count_estimate = use_count }; - } else { - uses_entry.value_ptr.count_estimate += use_count; - } - - const exports_ref = g.ast.items(.exports_ref)[source_index]; - const module_ref = g.ast.items(.module_ref)[source_index]; - if (!exports_ref.isNull() and ref.eql(exports_ref)) { - g.ast.items(.flags)[source_index].uses_exports_ref = true; - } - - if (!module_ref.isNull() and ref.eql(module_ref)) { - g.ast.items(.flags)[source_index].uses_module_ref = true; - } - - // null ref shouldn't be there. - bun.assert(!ref.isEmpty()); - - // Track that this specific symbol was imported - if (source_index_to_import_from.get() != source_index) { - const imports_to_bind = &g.meta.items(.imports_to_bind)[source_index]; - try imports_to_bind.put(g.allocator, ref, .{ - .data = .{ - .source_index = source_index_to_import_from, - .import_ref = ref, - }, - }); - } - - // Pull in all parts that declare this symbol - var dependencies = &part.dependencies; - const part_ids = g.topLevelSymbolToParts(source_index_to_import_from.get(), ref); - const new_dependencies = try dependencies.writableSlice(g.allocator, part_ids.len); - for (part_ids, new_dependencies) |part_id, *dependency| { - dependency.* = .{ - .source_index = source_index_to_import_from, - .part_index = @as(u32, @truncate(part_id)), - }; - } - } - - pub fn topLevelSymbolToParts(g: *LinkerGraph, id: u32, ref: Ref) []u32 { - if (g.meta.items(.top_level_symbol_to_parts_overlay)[id].get(ref)) |overlay| { - return overlay.slice(); - } - - if (g.ast.items(.top_level_symbols_to_parts)[id].get(ref)) |list| { - return list.slice(); - } - - return &.{}; - } - - pub fn load( - this: *LinkerGraph, - entry_points: []const Index, - sources: []const Logger.Source, - server_component_boundaries: ServerComponentBoundary.List, - dynamic_import_entry_points: []const Index.Int, - ) !void { - const scb = server_component_boundaries.slice(); - try this.files.setCapacity(this.allocator, sources.len); - this.files.zero(); - this.files_live = try BitSet.initEmpty( - this.allocator, - sources.len, - ); - this.files.len = sources.len; - var files = this.files.slice(); - - var entry_point_kinds = files.items(.entry_point_kind); - { - const kinds = std.mem.sliceAsBytes(entry_point_kinds); - @memset(kinds, 0); - } - - // Setup entry points - { - try this.entry_points.setCapacity(this.allocator, entry_points.len + server_component_boundaries.list.len + dynamic_import_entry_points.len); - this.entry_points.len = entry_points.len; - const source_indices = this.entry_points.items(.source_index); - - const path_strings: []bun.PathString = this.entry_points.items(.output_path); - { - const output_was_auto_generated = std.mem.sliceAsBytes(this.entry_points.items(.output_path_was_auto_generated)); - @memset(output_was_auto_generated, 0); - } - - for (entry_points, path_strings, source_indices) |i, *path_string, *source_index| { - const source = sources[i.get()]; - if (comptime Environment.allow_assert) { - bun.assert(source.index.get() == i.get()); - } - entry_point_kinds[source.index.get()] = EntryPoint.Kind.user_specified; - path_string.* = bun.PathString.init(source.path.text); - source_index.* = source.index.get(); - } - - for (dynamic_import_entry_points) |id| { - bun.assert(this.code_splitting); // this should never be a thing without code splitting - - if (entry_point_kinds[id] != .none) { - // You could dynamic import a file that is already an entry point - continue; - } - - const source = &sources[id]; - entry_point_kinds[id] = EntryPoint.Kind.dynamic_import; - - this.entry_points.appendAssumeCapacity(.{ - .source_index = id, - .output_path = bun.PathString.init(source.path.text), - .output_path_was_auto_generated = true, - }); - } - - var import_records_list: []ImportRecord.List = this.ast.items(.import_records); - try this.meta.setCapacity(this.allocator, import_records_list.len); - this.meta.len = this.ast.len; - this.meta.zero(); - - if (scb.list.len > 0) { - this.is_scb_bitset = BitSet.initEmpty(this.allocator, this.files.len) catch unreachable; - - // Index all SCBs into the bitset. This is needed so chunking - // can track the chunks that SCBs belong to. - for (scb.list.items(.use_directive), scb.list.items(.source_index), scb.list.items(.reference_source_index)) |use, original_id, ref_id| { - switch (use) { - .none => {}, - .client => { - this.is_scb_bitset.set(original_id); - this.is_scb_bitset.set(ref_id); - }, - .server => { - bun.todoPanic(@src(), "um", .{}); - }, - } - } - - // For client components, the import record index currently points to the original source index, instead of the reference source index. - for (this.reachable_files) |source_id| { - for (import_records_list[source_id.get()].slice()) |*import_record| { - if (import_record.source_index.isValid() and this.is_scb_bitset.isSet(import_record.source_index.get())) { - import_record.source_index = Index.init( - scb.getReferenceSourceIndex(import_record.source_index.get()) orelse - // If this gets hit, might be fine to switch this to `orelse continue` - // not confident in this assertion - Output.panic("Missing SCB boundary for file #{d}", .{import_record.source_index.get()}), - ); - bun.assert(import_record.source_index.isValid()); // did not generate - } - } - } - } else { - this.is_scb_bitset = .{}; - } - } - - // Setup files - { - var stable_source_indices = try this.allocator.alloc(Index, sources.len + 1); - - // set it to max value so that if we access an invalid one, it crashes - @memset(std.mem.sliceAsBytes(stable_source_indices), 255); - - for (this.reachable_files, 0..) |source_index, i| { - stable_source_indices[source_index.get()] = Index.source(i); - } - - @memset( - files.items(.distance_from_entry_point), - (LinkerGraph.File{}).distance_from_entry_point, - ); - this.stable_source_indices = @as([]const u32, @ptrCast(stable_source_indices)); - } - - { - var input_symbols = js_ast.Symbol.Map.initList(js_ast.Symbol.NestedList.init(this.ast.items(.symbols))); - var symbols = input_symbols.symbols_for_source.clone(this.allocator) catch bun.outOfMemory(); - for (symbols.slice(), input_symbols.symbols_for_source.slice()) |*dest, src| { - dest.* = src.clone(this.allocator) catch bun.outOfMemory(); - } - this.symbols = js_ast.Symbol.Map.initList(symbols); - } - - // TODO: const_values - // { - // var const_values = this.const_values; - // var count: usize = 0; - - // for (this.ast.items(.const_values)) |const_value| { - // count += const_value.count(); - // } - - // if (count > 0) { - // try const_values.ensureTotalCapacity(this.allocator, count); - // for (this.ast.items(.const_values)) |const_value| { - // for (const_value.keys(), const_value.values()) |key, value| { - // const_values.putAssumeCapacityNoClobber(key, value); - // } - // } - // } - - // this.const_values = const_values; - // } - - { - var count: usize = 0; - for (this.ast.items(.ts_enums)) |ts_enums| { - count += ts_enums.count(); - } - if (count > 0) { - try this.ts_enums.ensureTotalCapacity(this.allocator, count); - for (this.ast.items(.ts_enums)) |ts_enums| { - for (ts_enums.keys(), ts_enums.values()) |key, value| { - this.ts_enums.putAssumeCapacityNoClobber(key, value); - } - } - } - } - - const src_named_exports: []js_ast.Ast.NamedExports = this.ast.items(.named_exports); - const dest_resolved_exports: []ResolvedExports = this.meta.items(.resolved_exports); - for (src_named_exports, dest_resolved_exports, 0..) |src, *dest, source_index| { - var resolved = ResolvedExports{}; - resolved.ensureTotalCapacity(this.allocator, src.count()) catch unreachable; - for (src.keys(), src.values()) |key, value| { - resolved.putAssumeCapacityNoClobber(key, .{ .data = .{ - .import_ref = value.ref, - .name_loc = value.alias_loc, - .source_index = Index.source(source_index), - } }); - } - dest.* = resolved; - } - } - - pub const File = struct { - entry_bits: AutoBitSet = undefined, - - input_file: Index = Index.source(0), - - /// The minimum number of links in the module graph to get from an entry point - /// to this file - distance_from_entry_point: u32 = std.math.maxInt(u32), - - /// This file is an entry point if and only if this is not ".none". - /// Note that dynamically-imported files are allowed to also be specified by - /// the user as top-level entry points, so some dynamically-imported files - /// may be ".user_specified" instead of ".dynamic_import". - entry_point_kind: EntryPoint.Kind = .none, - - /// If "entry_point_kind" is not ".none", this is the index of the - /// corresponding entry point chunk. - /// - /// This is also initialized for files that are a SCB's generated - /// reference, pointing to its destination. This forms a lookup map from - /// a Source.Index to its output path inb reakOutputIntoPieces - entry_point_chunk_index: u32 = std.math.maxInt(u32), - - line_offset_table: bun.sourcemap.LineOffsetTable.List = .empty, - quoted_source_contents: string = "", - - pub fn isEntryPoint(this: *const File) bool { - return this.entry_point_kind.isEntryPoint(); - } - - pub fn isUserSpecifiedEntryPoint(this: *const File) bool { - return this.entry_point_kind.isUserSpecifiedEntryPoint(); - } - - pub const List = MultiArrayList(File); - }; -}; - -pub const LinkerContext = struct { - const debug = Output.scoped(.LinkerCtx, false); - - parse_graph: *Graph = undefined, - graph: LinkerGraph = undefined, - allocator: std.mem.Allocator = undefined, - log: *Logger.Log = undefined, - - resolver: *Resolver = undefined, - cycle_detector: std.ArrayList(ImportTracker) = undefined, - - /// We may need to refer to the "__esm" and/or "__commonJS" runtime symbols - cjs_runtime_ref: Ref = Ref.None, - esm_runtime_ref: Ref = Ref.None, - - /// We may need to refer to the CommonJS "module" symbol for exports - unbound_module_ref: Ref = Ref.None, - - options: LinkerOptions = .{}, - - wait_group: ThreadPoolLib.WaitGroup = .{}, - - ambiguous_result_pool: std.ArrayList(MatchImport) = undefined, - - loop: EventLoop, - - /// string buffer containing pre-formatted unique keys - unique_key_buf: []u8 = "", - - /// string buffer containing prefix for each unique keys - unique_key_prefix: string = "", - - source_maps: SourceMapData = .{}, - - /// This will eventually be used for reference-counting LinkerContext - /// to know whether or not we can free it safely. - pending_task_count: std.atomic.Value(u32) = std.atomic.Value(u32).init(0), - - /// - has_any_css_locals: std.atomic.Value(u32) = std.atomic.Value(u32).init(0), - - /// Used by Bake to extract []CompileResult before it is joined - dev_server: ?*bun.bake.DevServer = null, - framework: ?*const bake.Framework = null, - - mangled_props: MangledProps = .{}, - - fn pathWithPrettyInitialized(this: *LinkerContext, path: Fs.Path) !Fs.Path { - return genericPathWithPrettyInitialized(path, this.options.target, this.resolver.fs.top_level_dir, this.graph.allocator); - } - - pub const LinkerOptions = struct { - generate_bytecode_cache: bool = false, - output_format: options.Format = .esm, - ignore_dce_annotations: bool = false, - emit_dce_annotations: bool = true, - tree_shaking: bool = true, - minify_whitespace: bool = false, - minify_syntax: bool = false, - minify_identifiers: bool = false, - banner: []const u8 = "", - footer: []const u8 = "", - css_chunking: bool = false, - source_maps: options.SourceMapOption = .none, - target: options.Target = .browser, - - mode: Mode = .bundle, - - public_path: []const u8 = "", - - pub const Mode = enum { - passthrough, - bundle, - }; - }; - - pub const SourceMapData = struct { - line_offset_wait_group: sync.WaitGroup = .{}, - line_offset_tasks: []Task = &.{}, - - quoted_contents_wait_group: sync.WaitGroup = .{}, - quoted_contents_tasks: []Task = &.{}, - - pub const Task = struct { - ctx: *LinkerContext, - source_index: Index.Int, - thread_task: ThreadPoolLib.Task = .{ .callback = &runLineOffset }, - - pub fn runLineOffset(thread_task: *ThreadPoolLib.Task) void { - var task: *Task = @fieldParentPtr("thread_task", thread_task); - defer { - task.ctx.markPendingTaskDone(); - task.ctx.source_maps.line_offset_wait_group.finish(); - } - - const worker = ThreadPool.Worker.get(@fieldParentPtr("linker", task.ctx)); - defer worker.unget(); - SourceMapData.computeLineOffsets(task.ctx, worker.allocator, task.source_index); - } - - pub fn runQuotedSourceContents(thread_task: *ThreadPoolLib.Task) void { - var task: *Task = @fieldParentPtr("thread_task", thread_task); - defer { - task.ctx.markPendingTaskDone(); - task.ctx.source_maps.quoted_contents_wait_group.finish(); - } - - const worker = ThreadPool.Worker.get(@fieldParentPtr("linker", task.ctx)); - defer worker.unget(); - - // Use the default allocator when using DevServer and the file - // was generated. This will be preserved so that remapping - // stack traces can show the source code, even after incremental - // rebuilds occur. - const allocator = if (worker.ctx.transpiler.options.dev_server) |dev| - dev.allocator - else - worker.allocator; - - SourceMapData.computeQuotedSourceContents(task.ctx, allocator, task.source_index); - } - }; - - pub fn computeLineOffsets(this: *LinkerContext, allocator: std.mem.Allocator, source_index: Index.Int) void { - debug("Computing LineOffsetTable: {d}", .{source_index}); - const line_offset_table: *bun.sourcemap.LineOffsetTable.List = &this.graph.files.items(.line_offset_table)[source_index]; - - const source: *const Logger.Source = &this.parse_graph.input_files.items(.source)[source_index]; - const loader: options.Loader = this.parse_graph.input_files.items(.loader)[source_index]; - - if (!loader.canHaveSourceMap()) { - // This is not a file which we support generating source maps for - line_offset_table.* = .{}; - return; - } - - const approximate_line_count = this.graph.ast.items(.approximate_newline_count)[source_index]; - - line_offset_table.* = bun.sourcemap.LineOffsetTable.generate( - allocator, - source.contents, - - // We don't support sourcemaps for source files with more than 2^31 lines - @as(i32, @intCast(@as(u31, @truncate(approximate_line_count)))), - ); - } - - pub fn computeQuotedSourceContents(this: *LinkerContext, allocator: std.mem.Allocator, source_index: Index.Int) void { - debug("Computing Quoted Source Contents: {d}", .{source_index}); - const loader: options.Loader = this.parse_graph.input_files.items(.loader)[source_index]; - const quoted_source_contents: *string = &this.graph.files.items(.quoted_source_contents)[source_index]; - if (!loader.canHaveSourceMap()) { - quoted_source_contents.* = ""; - return; - } - - const source: *const Logger.Source = &this.parse_graph.input_files.items(.source)[source_index]; - const mutable = MutableString.initEmpty(allocator); - quoted_source_contents.* = (js_printer.quoteForJSON(source.contents, mutable, false) catch bun.outOfMemory()).list.items; - } - }; - - fn isExternalDynamicImport(this: *LinkerContext, record: *const ImportRecord, source_index: u32) bool { - return this.graph.code_splitting and - record.kind == .dynamic and - this.graph.files.items(.entry_point_kind)[record.source_index.get()].isEntryPoint() and - record.source_index.get() != source_index; - } - - inline fn shouldCallRuntimeRequire(format: options.Format) bool { - return format != .cjs; - } - - pub fn shouldIncludePart(c: *LinkerContext, source_index: Index.Int, part: Part) bool { - // As an optimization, ignore parts containing a single import statement to - // an internal non-wrapped file. These will be ignored anyway and it's a - // performance hit to spin up a goroutine only to discover this later. - if (part.stmts.len == 1) { - if (part.stmts[0].data == .s_import) { - const record = c.graph.ast.items(.import_records)[source_index].at(part.stmts[0].data.s_import.import_record_index); - if (record.source_index.isValid() and c.graph.meta.items(.flags)[record.source_index.get()].wrap == .none) { - return false; - } - } - } - - return true; - } - - fn load( - this: *LinkerContext, - bundle: *BundleV2, - entry_points: []Index, - server_component_boundaries: ServerComponentBoundary.List, - reachable: []Index, - ) !void { - const trace = bun.perf.trace("Bundler.CloneLinkerGraph"); - defer trace.end(); - this.parse_graph = &bundle.graph; - - this.graph.code_splitting = bundle.transpiler.options.code_splitting; - this.log = bundle.transpiler.log; - - this.resolver = &bundle.transpiler.resolver; - this.cycle_detector = std.ArrayList(ImportTracker).init(this.allocator); - - this.graph.reachable_files = reachable; - - const sources: []const Logger.Source = this.parse_graph.input_files.items(.source); - - try this.graph.load(entry_points, sources, server_component_boundaries, bundle.dynamic_import_entry_points.keys()); - bundle.dynamic_import_entry_points.deinit(); - this.wait_group.init(); - this.ambiguous_result_pool = std.ArrayList(MatchImport).init(this.allocator); - - var runtime_named_exports = &this.graph.ast.items(.named_exports)[Index.runtime.get()]; - - this.esm_runtime_ref = runtime_named_exports.get("__esm").?.ref; - this.cjs_runtime_ref = runtime_named_exports.get("__commonJS").?.ref; - - if (this.options.output_format == .cjs) { - this.unbound_module_ref = this.graph.generateNewSymbol(Index.runtime.get(), .unbound, "module"); - } - - if (this.options.output_format == .cjs or this.options.output_format == .iife) { - const exports_kind = this.graph.ast.items(.exports_kind); - const ast_flags_list = this.graph.ast.items(.flags); - const meta_flags_list = this.graph.meta.items(.flags); - - for (entry_points) |entry_point| { - var ast_flags: js_ast.BundledAst.Flags = ast_flags_list[entry_point.get()]; - - // Loaders default to CommonJS when they are the entry point and the output - // format is not ESM-compatible since that avoids generating the ESM-to-CJS - // machinery. - if (ast_flags.has_lazy_export) { - exports_kind[entry_point.get()] = .cjs; - } - - // Entry points with ES6 exports must generate an exports object when - // targeting non-ES6 formats. Note that the IIFE format only needs this - // when the global name is present, since that's the only way the exports - // can actually be observed externally. - if (ast_flags.uses_export_keyword) { - ast_flags.uses_exports_ref = true; - ast_flags_list[entry_point.get()] = ast_flags; - meta_flags_list[entry_point.get()].force_include_exports_for_entry_point = true; - } - } - } - } - - pub fn computeDataForSourceMap( - this: *LinkerContext, - reachable: []const Index.Int, - ) void { - bun.assert(this.options.source_maps != .none); - this.source_maps.line_offset_wait_group.init(); - this.source_maps.quoted_contents_wait_group.init(); - this.source_maps.line_offset_wait_group.counter = @as(u32, @truncate(reachable.len)); - this.source_maps.quoted_contents_wait_group.counter = @as(u32, @truncate(reachable.len)); - this.source_maps.line_offset_tasks = this.allocator.alloc(SourceMapData.Task, reachable.len) catch unreachable; - this.source_maps.quoted_contents_tasks = this.allocator.alloc(SourceMapData.Task, reachable.len) catch unreachable; - - var batch = ThreadPoolLib.Batch{}; - var second_batch = ThreadPoolLib.Batch{}; - for (reachable, this.source_maps.line_offset_tasks, this.source_maps.quoted_contents_tasks) |source_index, *line_offset, *quoted| { - line_offset.* = .{ - .ctx = this, - .source_index = source_index, - .thread_task = .{ .callback = &SourceMapData.Task.runLineOffset }, - }; - quoted.* = .{ - .ctx = this, - .source_index = source_index, - .thread_task = .{ .callback = &SourceMapData.Task.runQuotedSourceContents }, - }; - batch.push(.from(&line_offset.thread_task)); - second_batch.push(.from("ed.thread_task)); - } - - // line offsets block sooner and are faster to compute, so we should schedule those first - batch.push(second_batch); - - this.scheduleTasks(batch); - } - - pub fn scheduleTasks(this: *LinkerContext, batch: ThreadPoolLib.Batch) void { - _ = this.pending_task_count.fetchAdd(@as(u32, @truncate(batch.len)), .monotonic); - this.parse_graph.pool.worker_pool.schedule(batch); - } - - pub fn markPendingTaskDone(this: *LinkerContext) void { - _ = this.pending_task_count.fetchSub(1, .monotonic); - } - - pub noinline fn link( - this: *LinkerContext, - bundle: *BundleV2, - entry_points: []Index, - server_component_boundaries: ServerComponentBoundary.List, - reachable: []Index, - ) ![]Chunk { - try this.load( - bundle, - entry_points, - server_component_boundaries, - reachable, - ); - - if (this.options.source_maps != .none) { - this.computeDataForSourceMap(@as([]Index.Int, @ptrCast(reachable))); - } - - if (comptime FeatureFlags.help_catch_memory_issues) { - this.checkForMemoryCorruption(); - } - - try this.scanImportsAndExports(); - - // Stop now if there were errors - if (this.log.hasErrors()) { - return error.BuildFailed; - } - - if (comptime FeatureFlags.help_catch_memory_issues) { - this.checkForMemoryCorruption(); - } - - try this.treeShakingAndCodeSplitting(); - - if (comptime FeatureFlags.help_catch_memory_issues) { - this.checkForMemoryCorruption(); - } - - const chunks = try this.computeChunks(bundle.unique_key); - - if (comptime FeatureFlags.help_catch_memory_issues) { - this.checkForMemoryCorruption(); - } - - try this.computeCrossChunkDependencies(chunks); - - if (comptime FeatureFlags.help_catch_memory_issues) { - this.checkForMemoryCorruption(); - } - - this.graph.symbols.followAll(); - - return chunks; - } - - fn checkForMemoryCorruption(this: *LinkerContext) void { - // For this to work, you need mimalloc's debug build enabled. - // make mimalloc-debug - this.parse_graph.heap.helpCatchMemoryIssues(); - } - - const JSChunkKeyFormatter = struct { - has_html: bool, - entry_bits: []const u8, - - pub fn format(this: @This(), comptime _: []const u8, _: anytype, writer: anytype) !void { - try writer.writeAll(&[_]u8{@intFromBool(!this.has_html)}); - try writer.writeAll(this.entry_bits); - } - }; - pub noinline fn computeChunks( - this: *LinkerContext, - unique_key: u64, - ) ![]Chunk { - const trace = bun.perf.trace("Bundler.computeChunks"); - defer trace.end(); - - bun.assert(this.dev_server == null); // use - - var stack_fallback = std.heap.stackFallback(4096, this.allocator); - const stack_all = stack_fallback.get(); - var arena = bun.ArenaAllocator.init(stack_all); - defer arena.deinit(); - - var temp_allocator = arena.allocator(); - var js_chunks = bun.StringArrayHashMap(Chunk).init(temp_allocator); - try js_chunks.ensureUnusedCapacity(this.graph.entry_points.len); - - // Key is the hash of the CSS order. This deduplicates identical CSS files. - var css_chunks = std.AutoArrayHashMap(u64, Chunk).init(temp_allocator); - var js_chunks_with_css: usize = 0; - - const entry_source_indices = this.graph.entry_points.items(.source_index); - const css_asts = this.graph.ast.items(.css); - const css_chunking = this.options.css_chunking; - var html_chunks = bun.StringArrayHashMap(Chunk).init(temp_allocator); - const loaders = this.parse_graph.input_files.items(.loader); - - const code_splitting = this.graph.code_splitting; - - // Create chunks for entry points - for (entry_source_indices, 0..) |source_index, entry_id_| { - const entry_bit = @as(Chunk.EntryPoint.ID, @truncate(entry_id_)); - - var entry_bits = &this.graph.files.items(.entry_bits)[source_index]; - entry_bits.set(entry_bit); - - const has_html_chunk = loaders[source_index] == .html; - const js_chunk_key = brk: { - if (code_splitting) { - break :brk try temp_allocator.dupe(u8, entry_bits.bytes(this.graph.entry_points.len)); - } else { - // Force HTML chunks to always be generated, even if there's an identical JS file. - break :brk try std.fmt.allocPrint(temp_allocator, "{}", .{JSChunkKeyFormatter{ - .has_html = has_html_chunk, - .entry_bits = entry_bits.bytes(this.graph.entry_points.len), - }}); - } - }; - - // Put this early on in this loop so that CSS-only entry points work. - if (has_html_chunk) { - const html_chunk_entry = try html_chunks.getOrPut(js_chunk_key); - if (!html_chunk_entry.found_existing) { - html_chunk_entry.value_ptr.* = .{ - .entry_point = .{ - .entry_point_id = entry_bit, - .source_index = source_index, - .is_entry_point = true, - }, - .entry_bits = entry_bits.*, - .content = .html, - .output_source_map = sourcemap.SourceMapPieces.init(this.allocator), - }; - } - } - - if (css_asts[source_index] != null) { - const order = this.findImportedFilesInCSSOrder(temp_allocator, &.{Index.init(source_index)}); - // Create a chunk for the entry point here to ensure that the chunk is - // always generated even if the resulting file is empty - const hash_to_use = if (!this.options.css_chunking) - bun.hash(try temp_allocator.dupe(u8, entry_bits.bytes(this.graph.entry_points.len))) - else brk: { - var hasher = std.hash.Wyhash.init(5); - bun.writeAnyToHasher(&hasher, order.len); - for (order.slice()) |x| x.hash(&hasher); - break :brk hasher.final(); - }; - const css_chunk_entry = try css_chunks.getOrPut(hash_to_use); - if (!css_chunk_entry.found_existing) { - // const css_chunk_entry = try js_chunks.getOrPut(); - css_chunk_entry.value_ptr.* = .{ - .entry_point = .{ - .entry_point_id = entry_bit, - .source_index = source_index, - .is_entry_point = true, - }, - .entry_bits = entry_bits.*, - .content = .{ - .css = .{ - .imports_in_chunk_in_order = order, - .asts = this.allocator.alloc(bun.css.BundlerStyleSheet, order.len) catch bun.outOfMemory(), - }, - }, - .output_source_map = sourcemap.SourceMapPieces.init(this.allocator), - .has_html_chunk = has_html_chunk, - }; - } - - continue; - } - - // Create a chunk for the entry point here to ensure that the chunk is - // always generated even if the resulting file is empty - const js_chunk_entry = try js_chunks.getOrPut(js_chunk_key); - js_chunk_entry.value_ptr.* = .{ - .entry_point = .{ - .entry_point_id = entry_bit, - .source_index = source_index, - .is_entry_point = true, - }, - .entry_bits = entry_bits.*, - .content = .{ - .javascript = .{}, - }, - .has_html_chunk = has_html_chunk, - .output_source_map = sourcemap.SourceMapPieces.init(this.allocator), - }; - - { - // If this JS entry point has an associated CSS entry point, generate it - // now. This is essentially done by generating a virtual CSS file that - // only contains "@import" statements in the order that the files were - // discovered in JS source order, where JS source order is arbitrary but - // consistent for dynamic imports. Then we run the CSS import order - // algorithm to determine the final CSS file order for the chunk. - const css_source_indices = this.findImportedCSSFilesInJSOrder(temp_allocator, Index.init(source_index)); - if (css_source_indices.len > 0) { - const order = this.findImportedFilesInCSSOrder(temp_allocator, css_source_indices.slice()); - - const hash_to_use = if (!css_chunking) - bun.hash(try temp_allocator.dupe(u8, entry_bits.bytes(this.graph.entry_points.len))) - else brk: { - var hasher = std.hash.Wyhash.init(5); - bun.writeAnyToHasher(&hasher, order.len); - for (order.slice()) |x| x.hash(&hasher); - break :brk hasher.final(); - }; - - const css_chunk_entry = try css_chunks.getOrPut(hash_to_use); - - js_chunk_entry.value_ptr.content.javascript.css_chunks = try this.allocator.dupe(u32, &.{ - @intCast(css_chunk_entry.index), - }); - js_chunks_with_css += 1; - - if (!css_chunk_entry.found_existing) { - var css_files_with_parts_in_chunk = std.AutoArrayHashMapUnmanaged(Index.Int, void){}; - for (order.slice()) |entry| { - if (entry.kind == .source_index) { - css_files_with_parts_in_chunk.put(this.allocator, entry.kind.source_index.get(), {}) catch bun.outOfMemory(); - } - } - css_chunk_entry.value_ptr.* = .{ - .entry_point = .{ - .entry_point_id = entry_bit, - .source_index = source_index, - .is_entry_point = true, - }, - .entry_bits = entry_bits.*, - .content = .{ - .css = .{ - .imports_in_chunk_in_order = order, - .asts = this.allocator.alloc(bun.css.BundlerStyleSheet, order.len) catch bun.outOfMemory(), - }, - }, - .files_with_parts_in_chunk = css_files_with_parts_in_chunk, - .output_source_map = sourcemap.SourceMapPieces.init(this.allocator), - .has_html_chunk = has_html_chunk, - }; - } - } - } - } - var file_entry_bits: []AutoBitSet = this.graph.files.items(.entry_bits); - - const Handler = struct { - chunks: []Chunk, - allocator: std.mem.Allocator, - source_id: u32, - - pub fn next(c: *@This(), chunk_id: usize) void { - _ = c.chunks[chunk_id].files_with_parts_in_chunk.getOrPut(c.allocator, @as(u32, @truncate(c.source_id))) catch unreachable; - } - }; - - const css_reprs = this.graph.ast.items(.css); - - // Figure out which JS files are in which chunk - if (js_chunks.count() > 0) { - for (this.graph.reachable_files) |source_index| { - if (this.graph.files_live.isSet(source_index.get())) { - if (this.graph.ast.items(.css)[source_index.get()] == null) { - const entry_bits: *const AutoBitSet = &file_entry_bits[source_index.get()]; - if (css_reprs[source_index.get()] != null) continue; - - if (this.graph.code_splitting) { - const js_chunk_key = try temp_allocator.dupe(u8, entry_bits.bytes(this.graph.entry_points.len)); - var js_chunk_entry = try js_chunks.getOrPut(js_chunk_key); - - if (!js_chunk_entry.found_existing) { - js_chunk_entry.value_ptr.* = .{ - .entry_bits = entry_bits.*, - .entry_point = .{ - .source_index = source_index.get(), - }, - .content = .{ - .javascript = .{}, - }, - .output_source_map = sourcemap.SourceMapPieces.init(this.allocator), - }; - } - - _ = js_chunk_entry.value_ptr.files_with_parts_in_chunk.getOrPut(this.allocator, @as(u32, @truncate(source_index.get()))) catch unreachable; - } else { - var handler = Handler{ - .chunks = js_chunks.values(), - .allocator = this.allocator, - .source_id = source_index.get(), - }; - entry_bits.forEach(Handler, &handler, Handler.next); - } - } - } - } - } - - // Sort the chunks for determinism. This matters because we use chunk indices - // as sorting keys in a few places. - const chunks: []Chunk = sort_chunks: { - var sorted_chunks = try BabyList(Chunk).initCapacity(this.allocator, js_chunks.count() + css_chunks.count() + html_chunks.count()); - - var sorted_keys = try BabyList(string).initCapacity(temp_allocator, js_chunks.count()); - - // JS Chunks - sorted_keys.appendSliceAssumeCapacity(js_chunks.keys()); - sorted_keys.sortAsc(); - var js_chunk_indices_with_css = try BabyList(u32).initCapacity(temp_allocator, js_chunks_with_css); - for (sorted_keys.slice()) |key| { - const chunk = js_chunks.get(key) orelse unreachable; - - if (chunk.content.javascript.css_chunks.len > 0) - js_chunk_indices_with_css.appendAssumeCapacity(sorted_chunks.len); - - sorted_chunks.appendAssumeCapacity(chunk); - - // Attempt to order the JS HTML chunk immediately after the non-html one. - if (chunk.has_html_chunk) { - if (html_chunks.fetchSwapRemove(key)) |html_chunk| { - sorted_chunks.appendAssumeCapacity(html_chunk.value); - } - } - } - - if (css_chunks.count() > 0) { - const sorted_css_keys = try temp_allocator.dupe(u64, css_chunks.keys()); - std.sort.pdq(u64, sorted_css_keys, {}, std.sort.asc(u64)); - - // A map from the index in `css_chunks` to it's final index in `sorted_chunks` - const remapped_css_indexes = try temp_allocator.alloc(u32, css_chunks.count()); - - const css_chunk_values = css_chunks.values(); - for (sorted_css_keys, js_chunks.count()..) |key, sorted_index| { - const index = css_chunks.getIndex(key) orelse unreachable; - sorted_chunks.appendAssumeCapacity(css_chunk_values[index]); - remapped_css_indexes[index] = @intCast(sorted_index); - } - - // Update all affected JS chunks to point at the correct CSS chunk index. - for (js_chunk_indices_with_css.slice()) |js_index| { - for (sorted_chunks.slice()[js_index].content.javascript.css_chunks) |*idx| { - idx.* = remapped_css_indexes[idx.*]; - } - } - } - - // We don't care about the order of the HTML chunks that have no JS chunks. - try sorted_chunks.append(this.allocator, html_chunks.values()); - - break :sort_chunks sorted_chunks.slice(); - }; - - const entry_point_chunk_indices: []u32 = this.graph.files.items(.entry_point_chunk_index); - // Map from the entry point file to this chunk. We will need this later if - // a file contains a dynamic import to this entry point, since we'll need - // to look up the path for this chunk to use with the import. - for (chunks, 0..) |*chunk, chunk_id| { - if (chunk.entry_point.is_entry_point) { - entry_point_chunk_indices[chunk.entry_point.source_index] = @intCast(chunk_id); - } - } - - // Determine the order of JS files (and parts) within the chunk ahead of time - try this.findAllImportedPartsInJSOrder(temp_allocator, chunks); - - const unique_key_item_len = std.fmt.count("{any}C{d:0>8}", .{ bun.fmt.hexIntLower(unique_key), chunks.len }); - var unique_key_builder = try bun.StringBuilder.initCapacity(this.allocator, unique_key_item_len * chunks.len); - this.unique_key_buf = unique_key_builder.allocatedSlice(); - - errdefer { - unique_key_builder.deinit(this.allocator); - this.unique_key_buf = ""; - } - - const kinds = this.graph.files.items(.entry_point_kind); - const output_paths = this.graph.entry_points.items(.output_path); - for (chunks, 0..) |*chunk, chunk_id| { - // Assign a unique key to each chunk. This key encodes the index directly so - // we can easily recover it later without needing to look it up in a map. The - // last 8 numbers of the key are the chunk index. - chunk.unique_key = unique_key_builder.fmt("{}C{d:0>8}", .{ bun.fmt.hexIntLower(unique_key), chunk_id }); - if (this.unique_key_prefix.len == 0) - this.unique_key_prefix = chunk.unique_key[0..std.fmt.count("{}", .{bun.fmt.hexIntLower(unique_key)})]; - - if (chunk.entry_point.is_entry_point and - (chunk.content == .html or (kinds[chunk.entry_point.source_index] == .user_specified and !chunk.has_html_chunk))) - { - chunk.template = PathTemplate.file; - if (this.resolver.opts.entry_naming.len > 0) - chunk.template.data = this.resolver.opts.entry_naming; - } else { - chunk.template = PathTemplate.chunk; - if (this.resolver.opts.chunk_naming.len > 0) - chunk.template.data = this.resolver.opts.chunk_naming; - } - - const pathname = Fs.PathName.init(output_paths[chunk.entry_point.entry_point_id].slice()); - chunk.template.placeholder.name = pathname.base; - chunk.template.placeholder.ext = chunk.content.ext(); - - // this if check is a specific fix for `bun build hi.ts --external '*'`, without leading `./` - const dir_path = if (pathname.dir.len > 0) pathname.dir else "."; - - var real_path_buf: bun.PathBuffer = undefined; - const dir = dir: { - var dir = std.fs.cwd().openDir(dir_path, .{}) catch { - break :dir bun.path.normalizeBuf(dir_path, &real_path_buf, .auto); - }; - defer dir.close(); - - break :dir try bun.FD.fromStdDir(dir).getFdPath(&real_path_buf); - }; - - chunk.template.placeholder.dir = try resolve_path.relativeAlloc(this.allocator, this.resolver.opts.root_dir, dir); - } - - return chunks; - } - - pub fn findAllImportedPartsInJSOrder(this: *LinkerContext, temp_allocator: std.mem.Allocator, chunks: []Chunk) !void { - const trace = bun.perf.trace("Bundler.findAllImportedPartsInJSOrder"); - defer trace.end(); - - var part_ranges_shared = std.ArrayList(PartRange).init(temp_allocator); - var parts_prefix_shared = std.ArrayList(PartRange).init(temp_allocator); - defer part_ranges_shared.deinit(); - defer parts_prefix_shared.deinit(); - for (chunks, 0..) |*chunk, index| { - switch (chunk.content) { - .javascript => { - try this.findImportedPartsInJSOrder( - chunk, - &part_ranges_shared, - &parts_prefix_shared, - @intCast(index), - ); - }, - .css => {}, // handled in `findImportedCSSFilesInJSOrder` - .html => {}, - } - } - } - - pub fn findImportedPartsInJSOrder( - this: *LinkerContext, - chunk: *Chunk, - part_ranges_shared: *std.ArrayList(PartRange), - parts_prefix_shared: *std.ArrayList(PartRange), - chunk_index: u32, - ) !void { - var chunk_order_array = try std.ArrayList(Chunk.Order).initCapacity(this.allocator, chunk.files_with_parts_in_chunk.count()); - defer chunk_order_array.deinit(); - const distances = this.graph.files.items(.distance_from_entry_point); - for (chunk.files_with_parts_in_chunk.keys()) |source_index| { - chunk_order_array.appendAssumeCapacity( - .{ - .source_index = source_index, - .distance = distances[source_index], - .tie_breaker = this.graph.stable_source_indices[source_index], - }, - ); - } - - Chunk.Order.sort(chunk_order_array.items); - - const FindImportedPartsVisitor = struct { - entry_bits: *const AutoBitSet, - flags: []const JSMeta.Flags, - parts: []BabyList(Part), - import_records: []BabyList(ImportRecord), - files: std.ArrayList(Index.Int), - part_ranges: std.ArrayList(PartRange), - visited: std.AutoHashMap(Index.Int, void), - parts_prefix: std.ArrayList(PartRange), - c: *LinkerContext, - entry_point: Chunk.EntryPoint, - chunk_index: u32, - - fn appendOrExtendRange( - ranges: *std.ArrayList(PartRange), - source_index: Index.Int, - part_index: Index.Int, - ) void { - if (ranges.items.len > 0) { - var last_range = &ranges.items[ranges.items.len - 1]; - if (last_range.source_index.get() == source_index and last_range.part_index_end == part_index) { - last_range.part_index_end += 1; - return; - } - } - - ranges.append(.{ - .source_index = Index.init(source_index), - .part_index_begin = part_index, - .part_index_end = part_index + 1, - }) catch unreachable; - } - - // Traverse the graph using this stable order and linearize the files with - // dependencies before dependents - pub fn visit( - v: *@This(), - source_index: Index.Int, - comptime with_code_splitting: bool, - comptime with_scb: bool, - ) void { - if (source_index == Index.invalid.value) return; - const visited_entry = v.visited.getOrPut(source_index) catch unreachable; - if (visited_entry.found_existing) return; - - var is_file_in_chunk = if (with_code_splitting and v.c.graph.ast.items(.css)[source_index] == null) - // when code splitting, include the file in the chunk if ALL of the entry points overlap - v.entry_bits.eql(&v.c.graph.files.items(.entry_bits)[source_index]) - else - // when NOT code splitting, include the file in the chunk if ANY of the entry points overlap - v.entry_bits.hasIntersection(&v.c.graph.files.items(.entry_bits)[source_index]); - - // Wrapped files can't be split because they are all inside the wrapper - const can_be_split = v.flags[source_index].wrap == .none; - - const parts = v.parts[source_index].slice(); - if (can_be_split and is_file_in_chunk and parts[js_ast.namespace_export_part_index].is_live) { - appendOrExtendRange(&v.part_ranges, source_index, js_ast.namespace_export_part_index); - } - - const records = v.import_records[source_index].slice(); - - for (parts, 0..) |part, part_index_| { - const part_index = @as(u32, @truncate(part_index_)); - const is_part_in_this_chunk = is_file_in_chunk and part.is_live; - for (part.import_record_indices.slice()) |record_id| { - const record: *const ImportRecord = &records[record_id]; - if (record.source_index.isValid() and (record.kind == .stmt or is_part_in_this_chunk)) { - if (v.c.isExternalDynamicImport(record, source_index)) { - // Don't follow import() dependencies - continue; - } - - v.visit(record.source_index.get(), with_code_splitting, with_scb); - } - } - - // Then include this part after the files it imports - if (is_part_in_this_chunk) { - is_file_in_chunk = true; - - if (can_be_split and - part_index != js_ast.namespace_export_part_index and - v.c.shouldIncludePart(source_index, part)) - { - const js_parts = if (source_index == Index.runtime.value) - &v.parts_prefix - else - &v.part_ranges; - - appendOrExtendRange(js_parts, source_index, part_index); - } - } - } - - if (is_file_in_chunk) { - if (with_scb and v.c.graph.is_scb_bitset.isSet(source_index)) { - v.c.graph.files.items(.entry_point_chunk_index)[source_index] = v.chunk_index; - } - - v.files.append(source_index) catch bun.outOfMemory(); - - // CommonJS files are all-or-nothing so all parts must be contiguous - if (!can_be_split) { - v.parts_prefix.append( - .{ - .source_index = Index.init(source_index), - .part_index_begin = 0, - .part_index_end = @as(u32, @truncate(parts.len)), - }, - ) catch bun.outOfMemory(); - } - } - } - }; - - part_ranges_shared.clearRetainingCapacity(); - parts_prefix_shared.clearRetainingCapacity(); - - var visitor = FindImportedPartsVisitor{ - .files = std.ArrayList(Index.Int).init(this.allocator), - .part_ranges = part_ranges_shared.*, - .parts_prefix = parts_prefix_shared.*, - .visited = std.AutoHashMap(Index.Int, void).init(this.allocator), - .flags = this.graph.meta.items(.flags), - .parts = this.graph.ast.items(.parts), - .import_records = this.graph.ast.items(.import_records), - .entry_bits = chunk.entryBits(), - .c = this, - .entry_point = chunk.entry_point, - .chunk_index = chunk_index, - }; - defer { - part_ranges_shared.* = visitor.part_ranges; - parts_prefix_shared.* = visitor.parts_prefix; - visitor.visited.deinit(); - } - - switch (this.graph.code_splitting) { - inline else => |with_code_splitting| switch (this.graph.is_scb_bitset.bit_length > 0) { - inline else => |with_scb| { - visitor.visit(Index.runtime.value, with_code_splitting, with_scb); - - for (chunk_order_array.items) |order| { - visitor.visit(order.source_index, with_code_splitting, with_scb); - } - }, - }, - } - - const parts_in_chunk_order = try this.allocator.alloc(PartRange, visitor.part_ranges.items.len + visitor.parts_prefix.items.len); - bun.concat(PartRange, parts_in_chunk_order, &.{ - visitor.parts_prefix.items, - visitor.part_ranges.items, - }); - chunk.content.javascript.files_in_chunk_order = visitor.files.items; - chunk.content.javascript.parts_in_chunk_in_order = parts_in_chunk_order; - } - - // CSS files are traversed in depth-first postorder just like JavaScript. But - // unlike JavaScript import statements, CSS "@import" rules are evaluated every - // time instead of just the first time. - // - // A - // / \ - // B C - // \ / - // D - // - // If A imports B and then C, B imports D, and C imports D, then the CSS - // traversal order is D B D C A. - // - // However, evaluating a CSS file multiple times is sort of equivalent to - // evaluating it once at the last location. So we basically drop all but the - // last evaluation in the order. - // - // The only exception to this is "@layer". Evaluating a CSS file multiple - // times is sort of equivalent to evaluating it once at the first location - // as far as "@layer" is concerned. So we may in some cases keep both the - // first and last locations and only write out the "@layer" information - // for the first location. - pub fn findImportedFilesInCSSOrder(this: *LinkerContext, temp_allocator: std.mem.Allocator, entry_points: []const Index) BabyList(Chunk.CssImportOrder) { - const Visitor = struct { - allocator: std.mem.Allocator, - temp_allocator: std.mem.Allocator, - css_asts: []?*bun.css.BundlerStyleSheet, - all_import_records: []const BabyList(ImportRecord), - - graph: *LinkerGraph, - parse_graph: *Graph, - - has_external_import: bool = false, - visited: BabyList(Index), - order: BabyList(Chunk.CssImportOrder) = .{}, - - pub fn visit( - visitor: *@This(), - source_index: Index, - wrapping_conditions: *BabyList(bun.css.ImportConditions), - wrapping_import_records: *BabyList(ImportRecord), - ) void { - debug( - "Visit file: {d}={s}", - .{ source_index.get(), visitor.parse_graph.input_files.items(.source)[source_index.get()].path.pretty }, - ); - // The CSS specification strangely does not describe what to do when there - // is a cycle. So we are left with reverse-engineering the behavior from a - // real browser. Here's what the WebKit code base has to say about this: - // - // "Check for a cycle in our import chain. If we encounter a stylesheet - // in our parent chain with the same URL, then just bail." - // - // So that's what we do here. See "StyleRuleImport::requestStyleSheet()" in - // WebKit for more information. - for (visitor.visited.slice()) |visitedSourceIndex| { - if (visitedSourceIndex.get() == source_index.get()) { - debug( - "Skip file: {d}={s}", - .{ source_index.get(), visitor.parse_graph.input_files.items(.source)[source_index.get()].path.pretty }, - ); - return; - } - } - - visitor.visited.push( - visitor.temp_allocator, - source_index, - ) catch bun.outOfMemory(); - - const repr: *const bun.css.BundlerStyleSheet = visitor.css_asts[source_index.get()] orelse return; // Sanity check - const top_level_rules = &repr.rules; - - // TODO: should we even do this? @import rules have to be the first rules in the stylesheet, why even allow pre-import layers? - // Any pre-import layers come first - // if len(repr.AST.LayersPreImport) > 0 { - // order = append(order, cssImportOrder{ - // kind: cssImportLayers, - // layers: repr.AST.LayersPreImport, - // conditions: wrappingConditions, - // conditionImportRecords: wrappingImportRecords, - // }) - // } - - defer { - _ = visitor.visited.pop(); - } - - // Iterate over the top-level "@import" rules - var import_record_idx: usize = 0; - for (top_level_rules.v.items) |*rule| { - if (rule.* == .import) { - defer import_record_idx += 1; - const record = visitor.all_import_records[source_index.get()].at(import_record_idx); - - // Follow internal dependencies - if (record.source_index.isValid()) { - // If this import has conditions, fork our state so that the entire - // imported stylesheet subtree is wrapped in all of the conditions - if (rule.import.hasConditions()) { - // Fork our state - var nested_conditions = wrapping_conditions.deepClone2(visitor.allocator); - var nested_import_records = wrapping_import_records.clone(visitor.allocator) catch bun.outOfMemory(); - - // Clone these import conditions and append them to the state - nested_conditions.push(visitor.allocator, rule.import.conditionsWithImportRecords(visitor.allocator, &nested_import_records)) catch bun.outOfMemory(); - visitor.visit(record.source_index, &nested_conditions, wrapping_import_records); - continue; - } - visitor.visit(record.source_index, wrapping_conditions, wrapping_import_records); - continue; - } - - // Record external depednencies - if (!record.is_internal) { - var all_conditions = wrapping_conditions.deepClone2(visitor.allocator); - var all_import_records = wrapping_import_records.clone(visitor.allocator) catch bun.outOfMemory(); - // If this import has conditions, append it to the list of overall - // conditions for this external import. Note that an external import - // may actually have multiple sets of conditions that can't be - // merged. When this happens we need to generate a nested imported - // CSS file using a data URL. - if (rule.import.hasConditions()) { - all_conditions.push(visitor.allocator, rule.import.conditionsWithImportRecords(visitor.allocator, &all_import_records)) catch bun.outOfMemory(); - visitor.order.push( - visitor.allocator, - Chunk.CssImportOrder{ - .kind = .{ - .external_path = record.path, - }, - .conditions = all_conditions, - .condition_import_records = all_import_records, - }, - ) catch bun.outOfMemory(); - } else { - visitor.order.push( - visitor.allocator, - Chunk.CssImportOrder{ - .kind = .{ - .external_path = record.path, - }, - .conditions = wrapping_conditions.*, - .condition_import_records = wrapping_import_records.*, - }, - ) catch bun.outOfMemory(); - } - debug( - "Push external: {d}={s}", - .{ source_index.get(), visitor.parse_graph.input_files.items(.source)[source_index.get()].path.pretty }, - ); - visitor.has_external_import = true; - } - } - } - - // Iterate over the "composes" directives. Note that the order doesn't - // matter for these because the output order is explicitly undfened - // in the specification. - for (visitor.all_import_records[source_index.get()].sliceConst()) |*record| { - if (record.kind == .composes and record.source_index.isValid()) { - visitor.visit(record.source_index, wrapping_conditions, wrapping_import_records); - } - } - - if (comptime bun.Environment.isDebug) { - debug( - "Push file: {d}={s}", - .{ source_index.get(), visitor.parse_graph.input_files.items(.source)[source_index.get()].path.pretty }, - ); - } - // Accumulate imports in depth-first postorder - visitor.order.push(visitor.allocator, Chunk.CssImportOrder{ - .kind = .{ .source_index = source_index }, - .conditions = wrapping_conditions.*, - }) catch bun.outOfMemory(); - } - }; - - var visitor = Visitor{ - .allocator = this.allocator, - .temp_allocator = temp_allocator, - .graph = &this.graph, - .parse_graph = this.parse_graph, - .visited = BabyList(Index).initCapacity(temp_allocator, 16) catch bun.outOfMemory(), - .css_asts = this.graph.ast.items(.css), - .all_import_records = this.graph.ast.items(.import_records), - }; - var wrapping_conditions: BabyList(bun.css.ImportConditions) = .{}; - var wrapping_import_records: BabyList(ImportRecord) = .{}; - // Include all files reachable from any entry point - for (entry_points) |entry_point| { - visitor.visit(entry_point, &wrapping_conditions, &wrapping_import_records); - } - - var order = visitor.order; - var wip_order = BabyList(Chunk.CssImportOrder).initCapacity(temp_allocator, order.len) catch bun.outOfMemory(); - - const css_asts: []const ?*bun.css.BundlerStyleSheet = this.graph.ast.items(.css); - - debugCssOrder(this, &order, .BEFORE_HOISTING); - - // CSS syntax unfortunately only allows "@import" rules at the top of the - // file. This means we must hoist all external "@import" rules to the top of - // the file when bundling, even though doing so will change the order of CSS - // evaluation. - if (visitor.has_external_import) { - // Pass 1: Pull out leading "@layer" and external "@import" rules - var is_at_layer_prefix = true; - for (order.slice()) |*entry| { - if ((entry.kind == .layers and is_at_layer_prefix) or entry.kind == .external_path) { - wip_order.push(temp_allocator, entry.*) catch bun.outOfMemory(); - } - if (entry.kind != .layers) { - is_at_layer_prefix = false; - } - } - - // Pass 2: Append everything that we didn't pull out in pass 1 - is_at_layer_prefix = true; - for (order.slice()) |*entry| { - if ((entry.kind != .layers or !is_at_layer_prefix) and entry.kind != .external_path) { - wip_order.push(temp_allocator, entry.*) catch bun.outOfMemory(); - } - if (entry.kind != .layers) { - is_at_layer_prefix = false; - } - } - - order.len = wip_order.len; - @memcpy(order.slice(), wip_order.slice()); - wip_order.clearRetainingCapacity(); - } - debugCssOrder(this, &order, .AFTER_HOISTING); - - // Next, optimize import order. If there are duplicate copies of an imported - // file, replace all but the last copy with just the layers that are in that - // file. This works because in CSS, the last instance of a declaration - // overrides all previous instances of that declaration. - { - var source_index_duplicates = std.AutoArrayHashMap(u32, BabyList(u32)).init(temp_allocator); - var external_path_duplicates = std.StringArrayHashMap(BabyList(u32)).init(temp_allocator); - - var i: u32 = visitor.order.len; - next_backward: while (i != 0) { - i -= 1; - const entry = visitor.order.at(i); - switch (entry.kind) { - .source_index => |idx| { - const gop = source_index_duplicates.getOrPut(idx.get()) catch bun.outOfMemory(); - if (!gop.found_existing) { - gop.value_ptr.* = BabyList(u32){}; - } - for (gop.value_ptr.slice()) |j| { - if (isConditionalImportRedundant(&entry.conditions, &order.at(j).conditions)) { - // This import is redundant, but it might have @layer rules. - // So we should keep the @layer rules so that the cascade ordering of layers - // is preserved - order.mut(i).kind = .{ - .layers = Chunk.CssImportOrder.Layers.borrow(&css_asts[idx.get()].?.layer_names), - }; - continue :next_backward; - } - } - gop.value_ptr.push(temp_allocator, i) catch bun.outOfMemory(); - }, - .external_path => |p| { - const gop = external_path_duplicates.getOrPut(p.text) catch bun.outOfMemory(); - if (!gop.found_existing) { - gop.value_ptr.* = BabyList(u32){}; - } - for (gop.value_ptr.slice()) |j| { - if (isConditionalImportRedundant(&entry.conditions, &order.at(j).conditions)) { - // Don't remove duplicates entirely. The import conditions may - // still introduce layers to the layer order. Represent this as a - // file with an empty layer list. - order.mut(i).kind = .{ - .layers = .{ .owned = .{} }, - }; - continue :next_backward; - } - } - gop.value_ptr.push(temp_allocator, i) catch bun.outOfMemory(); - }, - .layers => {}, - } - } - } - debugCssOrder(this, &order, .AFTER_REMOVING_DUPLICATES); - - // Then optimize "@layer" rules by removing redundant ones. This loop goes - // forward instead of backward because "@layer" takes effect at the first - // copy instead of the last copy like other things in CSS. - { - const DuplicateEntry = struct { - layers: []const bun.css.LayerName, - indices: bun.BabyList(u32) = .{}, - }; - var layer_duplicates = bun.BabyList(DuplicateEntry){}; - - next_forward: for (order.slice()) |*entry| { - debugCssOrder(this, &wip_order, .WHILE_OPTIMIZING_REDUNDANT_LAYER_RULES); - switch (entry.kind) { - // Simplify the conditions since we know they only wrap "@layer" - .layers => |*layers| { - // Truncate the conditions at the first anonymous layer - for (entry.conditions.slice(), 0..) |*condition_, i| { - const conditions: *bun.css.ImportConditions = condition_; - // The layer is anonymous if it's a "layer" token without any - // children instead of a "layer(...)" token with children: - // - // /* entry.css */ - // @import "foo.css" layer; - // - // /* foo.css */ - // @layer foo; - // - // We don't need to generate this (as far as I can tell): - // - // @layer { - // @layer foo; - // } - // - if (conditions.hasAnonymousLayer()) { - entry.conditions.len = @intCast(i); - layers.replace(temp_allocator, .{}); - break; - } - } - - // If there are no layer names for this file, trim all conditions - // without layers because we know they have no effect. - // - // (They have no effect because this is a `.layer` import with no rules - // and only layer declarations.) - // - // /* entry.css */ - // @import "foo.css" layer(foo) supports(display: flex); - // - // /* foo.css */ - // @import "empty.css" supports(display: grid); - // - // That would result in this: - // - // @supports (display: flex) { - // @layer foo { - // @supports (display: grid) {} - // } - // } - // - // Here we can trim "supports(display: grid)" to generate this: - // - // @supports (display: flex) { - // @layer foo; - // } - // - if (layers.inner().len == 0) { - var i: u32 = entry.conditions.len; - while (i != 0) { - i -= 1; - const condition = entry.conditions.at(i); - if (condition.layer != null) { - break; - } - entry.conditions.len = i; - } - } - - // Remove unnecessary entries entirely - if (entry.conditions.len == 0 and layers.inner().len == 0) { - continue; - } - }, - else => {}, - } - - // Omit redundant "@layer" rules with the same set of layer names. Note - // that this tests all import order entries (not just layer ones) because - // sometimes non-layer ones can make following layer ones redundant. - // layers_post_import - const layers_key: []const bun.css.LayerName = switch (entry.kind) { - .source_index => css_asts[entry.kind.source_index.get()].?.layer_names.sliceConst(), - .layers => entry.kind.layers.inner().sliceConst(), - .external_path => &.{}, - }; - var index: usize = 0; - while (index < layer_duplicates.len) : (index += 1) { - const both_equal = both_equal: { - if (layers_key.len != layer_duplicates.at(index).layers.len) { - break :both_equal false; - } - - for (layers_key, layer_duplicates.at(index).layers) |*a, *b| { - if (!a.eql(b)) { - break :both_equal false; - } - } - - break :both_equal true; - }; - - if (both_equal) { - break; - } - } - if (index == layer_duplicates.len) { - // This is the first time we've seen this combination of layer names. - // Allocate a new set of duplicate indices to track this combination. - layer_duplicates.push(temp_allocator, DuplicateEntry{ - .layers = layers_key, - }) catch bun.outOfMemory(); - } - var duplicates = layer_duplicates.at(index).indices.slice(); - var j = duplicates.len; - while (j != 0) { - j -= 1; - const duplicate_index = duplicates[j]; - if (isConditionalImportRedundant(&entry.conditions, &wip_order.at(duplicate_index).conditions)) { - if (entry.kind != .layers) { - // If an empty layer is followed immediately by a full layer and - // everything else is identical, then we don't need to emit the - // empty layer. For example: - // - // @media screen { - // @supports (display: grid) { - // @layer foo; - // } - // } - // @media screen { - // @supports (display: grid) { - // @layer foo { - // div { - // color: red; - // } - // } - // } - // } - // - // This can be improved by dropping the empty layer. But we can - // only do this if there's nothing in between these two rules. - if (j == duplicates.len - 1 and duplicate_index == wip_order.len - 1) { - const other = wip_order.at(duplicate_index); - if (other.kind == .layers and importConditionsAreEqual(entry.conditions.sliceConst(), other.conditions.sliceConst())) { - // Remove the previous entry and then overwrite it below - duplicates = duplicates[0..j]; - wip_order.len = duplicate_index; - break; - } - } - - // Non-layer entries still need to be present because they have - // other side effects beside inserting things in the layer order - wip_order.push(temp_allocator, entry.*) catch bun.outOfMemory(); - } - - // Don't add this to the duplicate list below because it's redundant - continue :next_forward; - } - } - - layer_duplicates.mut(index).indices.push( - temp_allocator, - wip_order.len, - ) catch bun.outOfMemory(); - wip_order.push(temp_allocator, entry.*) catch bun.outOfMemory(); - } - - debugCssOrder(this, &wip_order, .WHILE_OPTIMIZING_REDUNDANT_LAYER_RULES); - - order.len = wip_order.len; - @memcpy(order.slice(), wip_order.slice()); - wip_order.clearRetainingCapacity(); - } - debugCssOrder(this, &order, .AFTER_OPTIMIZING_REDUNDANT_LAYER_RULES); - - // Finally, merge adjacent "@layer" rules with identical conditions together. - { - var did_clone: i32 = -1; - for (order.slice()) |*entry| { - if (entry.kind == .layers and wip_order.len > 0) { - const prev_index = wip_order.len - 1; - const prev = wip_order.at(prev_index); - if (prev.kind == .layers and importConditionsAreEqual(prev.conditions.sliceConst(), entry.conditions.sliceConst())) { - if (did_clone != prev_index) { - did_clone = @intCast(prev_index); - } - // need to clone the layers here as they could be references to css ast - wip_order.mut(prev_index).kind.layers.toOwned(temp_allocator).append( - temp_allocator, - entry.kind.layers.inner().sliceConst(), - ) catch bun.outOfMemory(); - } - } - } - } - debugCssOrder(this, &order, .AFTER_MERGING_ADJACENT_LAYER_RULES); - - return order; - } - - const CssOrderDebugStep = enum { - BEFORE_HOISTING, - AFTER_HOISTING, - AFTER_REMOVING_DUPLICATES, - WHILE_OPTIMIZING_REDUNDANT_LAYER_RULES, - AFTER_OPTIMIZING_REDUNDANT_LAYER_RULES, - AFTER_MERGING_ADJACENT_LAYER_RULES, - }; - - fn debugCssOrder(this: *LinkerContext, order: *const BabyList(Chunk.CssImportOrder), comptime step: CssOrderDebugStep) void { - if (comptime bun.Environment.isDebug) { - const env_var = "BUN_DEBUG_CSS_ORDER_" ++ @tagName(step); - const enable_all = bun.getenvTruthy("BUN_DEBUG_CSS_ORDER"); - if (enable_all or bun.getenvTruthy(env_var)) { - debugCssOrderImpl(this, order, step); - } - } - } - - fn debugCssOrderImpl(this: *LinkerContext, order: *const BabyList(Chunk.CssImportOrder), comptime step: CssOrderDebugStep) void { - if (comptime bun.Environment.isDebug) { - debug("CSS order {s}:\n", .{@tagName(step)}); - var arena = bun.ArenaAllocator.init(bun.default_allocator); - defer arena.deinit(); - for (order.slice(), 0..) |entry, i| { - const conditions_str = if (entry.conditions.len > 0) conditions_str: { - var arrlist = std.ArrayListUnmanaged(u8){}; - const writer = arrlist.writer(arena.allocator()); - const W = @TypeOf(writer); - arrlist.appendSlice(arena.allocator(), "[") catch unreachable; - var symbols = Symbol.Map{}; - for (entry.conditions.sliceConst(), 0..) |*condition_, j| { - const condition: *const bun.css.ImportConditions = condition_; - const scratchbuf = std.ArrayList(u8).init(arena.allocator()); - var printer = bun.css.Printer(W).new( - arena.allocator(), - scratchbuf, - writer, - bun.css.PrinterOptions.default(), - .{ - .import_records = &entry.condition_import_records, - .ast_urls_for_css = this.parse_graph.ast.items(.url_for_css), - .ast_unique_key_for_additional_file = this.parse_graph.input_files.items(.unique_key_for_additional_file), - }, - &this.mangled_props, - &symbols, - ); - - condition.toCss(W, &printer) catch unreachable; - if (j != entry.conditions.len - 1) { - arrlist.appendSlice(arena.allocator(), ", ") catch unreachable; - } - } - arrlist.appendSlice(arena.allocator(), " ]") catch unreachable; - break :conditions_str arrlist.items; - } else "[]"; - - debug(" {d}: {} {s}\n", .{ i, entry.fmt(this), conditions_str }); - } - } - } - - fn importConditionsAreEqual(a: []const bun.css.ImportConditions, b: []const bun.css.ImportConditions) bool { - if (a.len != b.len) { - return false; - } - - for (a, b) |*ai, *bi| { - if (!ai.layersEql(bi) or !ai.supportsEql(bi) or !ai.media.eql(&bi.media)) return false; - } - - return true; - } - - /// Given two "@import" rules for the same source index (an earlier one and a - /// later one), the earlier one is masked by the later one if the later one's - /// condition list is a prefix of the earlier one's condition list. - /// - /// For example: - /// - /// // entry.css - /// @import "foo.css" supports(display: flex); - /// @import "bar.css" supports(display: flex); - /// - /// // foo.css - /// @import "lib.css" screen; - /// - /// // bar.css - /// @import "lib.css"; - /// - /// When we bundle this code we'll get an import order as follows: - /// - /// 1. lib.css [supports(display: flex), screen] - /// 2. foo.css [supports(display: flex)] - /// 3. lib.css [supports(display: flex)] - /// 4. bar.css [supports(display: flex)] - /// 5. entry.css [] - /// - /// For "lib.css", the entry with the conditions [supports(display: flex)] should - /// make the entry with the conditions [supports(display: flex), screen] redundant. - /// - /// Note that all of this deliberately ignores the existence of "@layer" because - /// that is handled separately. All of this is only for handling unlayered styles. - pub fn isConditionalImportRedundant(earlier: *const BabyList(bun.css.ImportConditions), later: *const BabyList(bun.css.ImportConditions)) bool { - if (later.len > earlier.len) return false; - - for (0..later.len) |i| { - const a = earlier.at(i); - const b = later.at(i); - - // Only compare "@supports" and "@media" if "@layers" is equal - if (a.layersEql(b)) { - const same_supports = a.supportsEql(b); - const same_media = a.media.eql(&b.media); - - // If the import conditions are exactly equal, then only keep - // the later one. The earlier one is redundant. Example: - // - // @import "foo.css" layer(abc) supports(display: flex) screen; - // @import "foo.css" layer(abc) supports(display: flex) screen; - // - // The later one makes the earlier one redundant. - if (same_supports and same_media) { - continue; - } - - // If the media conditions are exactly equal and the later one - // doesn't have any supports conditions, then the later one will - // apply in all cases where the earlier one applies. Example: - // - // @import "foo.css" layer(abc) supports(display: flex) screen; - // @import "foo.css" layer(abc) screen; - // - // The later one makes the earlier one redundant. - if (same_media and b.supports == null) { - continue; - } - - // If the supports conditions are exactly equal and the later one - // doesn't have any media conditions, then the later one will - // apply in all cases where the earlier one applies. Example: - // - // @import "foo.css" layer(abc) supports(display: flex) screen; - // @import "foo.css" layer(abc) supports(display: flex); - // - // The later one makes the earlier one redundant. - if (same_supports and b.media.media_queries.items.len == 0) { - continue; - } - } - - return false; - } - - return true; - } - - // JavaScript modules are traversed in depth-first postorder. This is the - // order that JavaScript modules were evaluated in before the top-level await - // feature was introduced. - // - // A - // / \ - // B C - // \ / - // D - // - // If A imports B and then C, B imports D, and C imports D, then the JavaScript - // traversal order is D B C A. - // - // This function may deviate from ESM import order for dynamic imports (both - // "require()" and "import()"). This is because the import order is impossible - // to determine since the imports happen at run-time instead of compile-time. - // In this case we just pick an arbitrary but consistent order. - pub fn findImportedCSSFilesInJSOrder(this: *LinkerContext, temp_allocator: std.mem.Allocator, entry_point: Index) BabyList(Index) { - var visited = BitSet.initEmpty(temp_allocator, this.graph.files.len) catch bun.outOfMemory(); - var order: BabyList(Index) = .{}; - - const all_import_records = this.graph.ast.items(.import_records); - const all_loaders = this.parse_graph.input_files.items(.loader); - const all_parts = this.graph.ast.items(.parts); - - const visit = struct { - fn visit( - c: *LinkerContext, - import_records: []const BabyList(ImportRecord), - parts: []const Part.List, - loaders: []const Loader, - temp: std.mem.Allocator, - visits: *BitSet, - o: *BabyList(Index), - source_index: Index, - is_css: bool, - ) void { - if (visits.isSet(source_index.get())) return; - visits.set(source_index.get()); - - const records: []ImportRecord = import_records[source_index.get()].slice(); - const p = &parts[source_index.get()]; - - // Iterate over each part in the file in order - for (p.sliceConst()) |part| { - // Traverse any files imported by this part. Note that CommonJS calls - // to "require()" count as imports too, sort of as if the part has an - // ESM "import" statement in it. This may seem weird because ESM imports - // are a compile-time concept while CommonJS imports are a run-time - // concept. But we don't want to manipulate