//! Bake is Bun's toolkit for building client+server web applications. It //! combines `Bun.build` and `Bun.serve`, providing a hot-reloading development //! server, server components, and other integrations. Instead of taking the //! role as a framework, Bake is tool for frameworks to build on top of. pub const production = @import("./bake/production.zig"); pub const DevServer = @import("./bake/DevServer.zig"); pub const FrameworkRouter = @import("./bake/FrameworkRouter.zig"); /// export default { app: ... }; pub const api_name = "app"; /// Zig version of the TS definition 'Bake.Options' in 'bake.d.ts' pub const UserOptions = struct { /// This arena contains some miscellaneous allocations at startup arena: std.heap.ArenaAllocator, allocations: StringRefList, root: [:0]const u8, framework: Framework, bundler_options: SplitBundlerOptions, pub fn deinit(options: *UserOptions) void { options.arena.deinit(); options.allocations.free(); if (options.bundler_options.plugin) |p| p.deinit(); } /// Currently, this function must run at the top of the event loop. pub fn fromJS(config: JSValue, global: *jsc.JSGlobalObject) !UserOptions { var arena = std.heap.ArenaAllocator.init(bun.default_allocator); errdefer arena.deinit(); const alloc = arena.allocator(); var allocations = StringRefList.empty; errdefer allocations.free(); var bundler_options = SplitBundlerOptions.empty; if (!config.isObject()) { // Allow users to do `export default { app: 'react' }` for convenience if (config.isString()) { const bunstr = try config.toBunString(global); defer bunstr.deref(); const utf8_string = bunstr.toUTF8(bun.default_allocator); defer utf8_string.deinit(); if (bun.strings.eql(utf8_string.byteSlice(), "react")) { const root = bun.getcwdAlloc(alloc) catch |err| switch (err) { error.OutOfMemory => { return global.throwOutOfMemory(); }, else => { return global.throwError(err, "while querying current working directory"); }, }; const framework = try Framework.react(alloc); return UserOptions{ .arena = arena, .allocations = allocations, .root = root, .framework = framework, .bundler_options = bundler_options, }; } } return global.throwInvalidArguments("'" ++ api_name ++ "' is not an object", .{}); } if (try config.getOptional(global, "bundlerOptions", JSValue)) |js_options| { if (try js_options.getOptional(global, "server", JSValue)) |server_options| { bundler_options.server = try BuildConfigSubset.fromJS(global, server_options); } if (try js_options.getOptional(global, "client", JSValue)) |client_options| { bundler_options.client = try BuildConfigSubset.fromJS(global, client_options); } if (try js_options.getOptional(global, "ssr", JSValue)) |ssr_options| { bundler_options.ssr = try BuildConfigSubset.fromJS(global, ssr_options); } } const framework = try Framework.fromJS( try config.get(global, "framework") orelse { return global.throwInvalidArguments("'" ++ api_name ++ "' is missing 'framework'", .{}); }, global, &allocations, &bundler_options, alloc, ); const root = if (try config.getOptional(global, "root", ZigString.Slice)) |slice| allocations.track(slice) else bun.getcwdAlloc(alloc) catch |err| switch (err) { error.OutOfMemory => { return global.throwOutOfMemory(); }, else => { return global.throwError(err, "while querying current working directory"); }, }; if (try config.get(global, "plugins")) |plugin_array| { try bundler_options.parsePluginArray(plugin_array, global); } return .{ .arena = arena, .allocations = allocations, .root = try alloc.dupeZ(u8, root), .framework = framework, .bundler_options = bundler_options, }; } }; /// Each string stores its allocator since some may hold reference counts to JSC pub const StringRefList = struct { strings: std.ArrayListUnmanaged(ZigString.Slice), pub const empty: StringRefList = .{ .strings = .{} }; pub fn track(al: *StringRefList, str: ZigString.Slice) []const u8 { bun.handleOom(al.strings.append(bun.default_allocator, str)); return str.slice(); } pub fn free(al: *StringRefList) void { for (al.strings.items) |item| item.deinit(); al.strings.clearAndFree(bun.default_allocator); } }; pub const SplitBundlerOptions = struct { plugin: ?*Plugin = null, client: BuildConfigSubset = .{}, server: BuildConfigSubset = .{}, ssr: BuildConfigSubset = .{}, pub const empty: SplitBundlerOptions = .{ .plugin = null, .client = .{}, .server = .{}, .ssr = .{}, }; pub fn parsePluginArray(opts: *SplitBundlerOptions, plugin_array: JSValue, global: *jsc.JSGlobalObject) bun.JSError!void { const plugin = opts.plugin orelse Plugin.create(global, .bun); opts.plugin = plugin; const empty_object = JSValue.createEmptyObject(global, 0); var iter = try plugin_array.arrayIterator(global); while (try iter.next()) |plugin_config| { if (!plugin_config.isObject()) { return global.throwInvalidArguments("Expected plugin to be an object", .{}); } if (try plugin_config.getOptional(global, "name", ZigString.Slice)) |slice| { defer slice.deinit(); if (slice.len == 0) { return global.throwInvalidArguments("Expected plugin to have a non-empty name", .{}); } } else { return global.throwInvalidArguments("Expected plugin to have a name", .{}); } const function = try plugin_config.getFunction(global, "setup") orelse { return global.throwInvalidArguments("Expected plugin to have a setup() function", .{}); }; const plugin_result = try plugin.addPlugin(function, empty_object, .null, false, true); if (plugin_result.asAnyPromise()) |promise| { promise.setHandled(global.vm()); // TODO: remove this call, replace with a promise list that must // be resolved before the first bundle task can begin. global.bunVM().waitForPromise(promise); switch (promise.unwrap(global.vm(), .mark_handled)) { .pending => unreachable, .fulfilled => |val| { _ = val; }, .rejected => |err| { return global.throwValue(err); }, } } } } }; const BuildConfigSubset = struct { loader: ?bun.schema.api.LoaderMap = null, ignoreDCEAnnotations: ?bool = null, conditions: bun.StringArrayHashMapUnmanaged(void) = .{}, drop: bun.StringArrayHashMapUnmanaged(void) = .{}, env: bun.schema.api.DotEnvBehavior = ._none, env_prefix: ?[]const u8 = null, define: bun.schema.api.StringMap = .{ .keys = &.{}, .values = &.{} }, source_map: bun.schema.api.SourceMapMode = .external, minify_syntax: ?bool = null, minify_identifiers: ?bool = null, minify_whitespace: ?bool = null, pub fn fromJS(global: *jsc.JSGlobalObject, js_options: JSValue) bun.JSError!BuildConfigSubset { var options = BuildConfigSubset{}; if (try js_options.getOptional(global, "sourcemap", JSValue)) |val| brk: { if (try bun.schema.api.SourceMapMode.fromJS(global, val)) |sourcemap| { options.source_map = sourcemap; break :brk; } return bun.jsc.Node.validators.throwErrInvalidArgType(global, "sourcemap", .{}, "\"inline\" | \"external\" | \"linked\"", val); } if (try js_options.getOptional(global, "minify", JSValue)) |minify_options| brk: { if (minify_options.isBoolean() and minify_options.asBoolean()) { options.minify_syntax = minify_options.asBoolean(); options.minify_identifiers = minify_options.asBoolean(); options.minify_whitespace = minify_options.asBoolean(); break :brk; } if (try minify_options.getBooleanLoose(global, "whitespace")) |value| { options.minify_whitespace = value; } if (try minify_options.getBooleanLoose(global, "syntax")) |value| { options.minify_syntax = value; } if (try minify_options.getBooleanLoose(global, "identifiers")) |value| { options.minify_identifiers = value; } } return options; } }; /// A "Framework" in our eyes is simply set of bundler options that a framework /// author would set in order to integrate the framework with the application. /// Since many fields have default values which may point to static memory, this /// structure is always arena-allocated, usually owned by the arena in `UserOptions` /// /// Full documentation on these fields is located in the TypeScript definitions. pub const Framework = struct { is_built_in_react: bool, file_system_router_types: []FileSystemRouterType, // static_routers: [][]const u8, server_components: ?ServerComponents = null, react_fast_refresh: ?ReactFastRefresh = null, built_in_modules: bun.StringArrayHashMapUnmanaged(BuiltInModule) = .{}, /// Bun provides built-in support for using React as a framework. /// Depends on externally provided React /// /// $ bun i react@experimental react-dom@experimental react-refresh@experimental react-server-dom-bun pub fn react(arena: std.mem.Allocator) !Framework { return .{ .is_built_in_react = true, .server_components = .{ .separate_ssr_graph = true, .server_runtime_import = "react-server-dom-bun/server", }, .react_fast_refresh = .{}, .file_system_router_types = try arena.dupe(FileSystemRouterType, &.{ .{ .root = "pages", .prefix = "/", .entry_client = "bun-framework-react/client.tsx", .entry_server = "bun-framework-react/server.tsx", .ignore_underscores = true, .ignore_dirs = &.{ "node_modules", ".git" }, .extensions = &.{ ".tsx", ".jsx" }, .style = .nextjs_pages, .allow_layouts = true, }, }), // .static_routers = try arena.dupe([]const u8, &.{"public"}), .built_in_modules = bun.StringArrayHashMapUnmanaged(BuiltInModule).init(arena, &.{ "bun-framework-react/client.tsx", "bun-framework-react/server.tsx", "bun-framework-react/ssr.tsx", }, if (Environment.codegen_embed) &.{ .{ .code = @embedFile("./bake/bun-framework-react/client.tsx") }, .{ .code = @embedFile("./bake/bun-framework-react/server.tsx") }, .{ .code = @embedFile("./bake/bun-framework-react/ssr.tsx") }, } else &.{ // Cannot use .import because resolution must happen from the user's POV .{ .code = bun.runtimeEmbedFile(.src, "bake/bun-framework-react/client.tsx") }, .{ .code = bun.runtimeEmbedFile(.src, "bake/bun-framework-react/server.tsx") }, .{ .code = bun.runtimeEmbedFile(.src, "bake/bun-framework-react/ssr.tsx") }, }) catch |err| bun.handleOom(err), }; } /// Default that requires no packages or configuration. /// - If `react-refresh` is installed, enable react fast refresh with it. /// - Otherwise, if `react` is installed, use a bundled copy of /// react-refresh so that it still works. /// - If any file system router types are provided, configure using /// the above react configuration. /// The provided allocator is not stored. pub fn auto( arena: std.mem.Allocator, resolver: *bun.resolver.Resolver, file_system_router_types: []FileSystemRouterType, ) !Framework { var fw: Framework = Framework.none; if (file_system_router_types.len > 0) { fw = try react(arena); arena.free(fw.file_system_router_types); fw.file_system_router_types = file_system_router_types; } if (resolveOrNull(resolver, "react-refresh/runtime")) |rfr| { fw.react_fast_refresh = .{ .import_source = rfr }; } else if (resolveOrNull(resolver, "react")) |_| { fw.react_fast_refresh = .{ .import_source = "react-refresh/runtime/index.js" }; try fw.built_in_modules.put( arena, "react-refresh/runtime/index.js", if (Environment.codegen_embed) .{ .code = @embedFile("node-fallbacks/react-refresh.js") } else .{ .code = bun.runtimeEmbedFile(.codegen, "node-fallbacks/react-refresh.js") }, ); } return fw; } /// Unopiniated default. pub const none: Framework = .{ .is_built_in_react = false, .file_system_router_types = &.{}, .server_components = null, .react_fast_refresh = null, .built_in_modules = .empty, }; pub const FileSystemRouterType = struct { root: []const u8, prefix: []const u8, entry_server: []const u8, entry_client: ?[]const u8, ignore_underscores: bool, ignore_dirs: []const []const u8, extensions: []const []const u8, style: FrameworkRouter.Style, allow_layouts: bool, }; pub const BuiltInModule = union(enum) { import: []const u8, code: []const u8, }; pub const ServerComponents = struct { separate_ssr_graph: bool = false, server_runtime_import: []const u8, // client_runtime_import: []const u8, server_register_client_reference: []const u8 = "registerClientReference", server_register_server_reference: []const u8 = "registerServerReference", client_register_server_reference: []const u8 = "registerServerReference", }; const ReactFastRefresh = struct { import_source: []const u8 = "react-refresh/runtime", }; pub const react_install_command = "bun i react@experimental react-dom@experimental react-server-dom-bun react-refresh@experimental"; pub fn addReactInstallCommandNote(log: *bun.logger.Log) !void { try log.addMsg(.{ .kind = .note, .data = try bun.logger.rangeData(null, bun.logger.Range.none, "Install the built in react integration with \"" ++ react_install_command ++ "\"") .cloneLineText(log.clone_line_text, log.msgs.allocator), }); } /// Given a Framework configuration, this returns another one with all paths resolved. /// New memory allocated into provided arena. /// /// All resolution errors will happen before returning error.ModuleNotFound /// Errors written into `r.log` pub fn resolve(f: Framework, server: *bun.resolver.Resolver, client: *bun.resolver.Resolver, arena: Allocator) !Framework { var clone = f; var had_errors: bool = false; if (clone.react_fast_refresh) |*react_fast_refresh| { f.resolveHelper(client, &react_fast_refresh.import_source, &had_errors, "react refresh runtime"); } if (clone.server_components) |*sc| { f.resolveHelper(server, &sc.server_runtime_import, &had_errors, "server components runtime"); // f.resolveHelper(client, &sc.client_runtime_import, &had_errors); } for (clone.file_system_router_types) |*fsr| { fsr.root = try arena.dupe(u8, bun.path.joinAbs(server.fs.top_level_dir, .auto, fsr.root)); if (fsr.entry_client) |*entry_client| f.resolveHelper(client, entry_client, &had_errors, "client side entrypoint"); f.resolveHelper(client, &fsr.entry_server, &had_errors, "server side entrypoint"); } if (had_errors) return error.ModuleNotFound; return clone; } inline fn resolveHelper(f: *const Framework, r: *bun.resolver.Resolver, path: *[]const u8, had_errors: *bool, desc: []const u8) void { if (f.built_in_modules.get(path.*)) |mod| { switch (mod) { .import => |p| path.* = p, .code => {}, } return; } var result = r.resolve(r.fs.top_level_dir, path.*, .stmt) catch |err| { bun.Output.err(err, "Failed to resolve '{s}' for framework ({s})", .{ path.*, desc }); had_errors.* = true; return; }; path.* = result.path().?.text; } inline fn resolveOrNull(r: *bun.resolver.Resolver, path: []const u8) ?[]const u8 { return (r.resolve(r.fs.top_level_dir, path, .stmt) catch { r.log.reset(); return null; }).pathConst().?.text; } fn fromJS( opts: JSValue, global: *jsc.JSGlobalObject, refs: *StringRefList, bundler_options: *SplitBundlerOptions, arena: Allocator, ) bun.JSError!Framework { if (opts.isString()) { const str = try opts.toBunString(global); defer str.deref(); // Deprecated if (str.eqlComptime("react-server-components")) { bun.Output.warn("deprecation notice: 'react-server-components' will be renamed to 'react'", .{}); return Framework.react(arena); } if (str.eqlComptime("react")) { return Framework.react(arena); } } if (!opts.isObject()) { return global.throwInvalidArguments("Framework must be an object", .{}); } if (try opts.get(global, "serverEntryPoint") != null) { bun.Output.warn("deprecation notice: 'framework.serverEntryPoint' has been replaced with 'fileSystemRouterTypes[n].serverEntryPoint'", .{}); } if (try opts.get(global, "clientEntryPoint") != null) { bun.Output.warn("deprecation notice: 'framework.clientEntryPoint' has been replaced with 'fileSystemRouterTypes[n].clientEntryPoint'", .{}); } const react_fast_refresh: ?ReactFastRefresh = brk: { const rfr: JSValue = try opts.get(global, "reactFastRefresh") orelse break :brk null; if (rfr == .true) break :brk .{}; if (rfr == .false or rfr.isUndefinedOrNull()) break :brk null; if (!rfr.isObject()) { return global.throwInvalidArguments("'framework.reactFastRefresh' must be an object or 'true'", .{}); } const prop = try rfr.get(global, "importSource") orelse { return global.throwInvalidArguments("'framework.reactFastRefresh' is missing 'importSource'", .{}); }; const str = try prop.toBunString(global); defer str.deref(); break :brk .{ .import_source = refs.track(str.toUTF8(arena)), }; }; const server_components: ?ServerComponents = sc: { const sc: JSValue = try opts.get(global, "serverComponents") orelse break :sc null; if (sc == .false or sc.isUndefinedOrNull()) break :sc null; if (!sc.isObject()) { return global.throwInvalidArguments("'framework.serverComponents' must be an object or 'undefined'", .{}); } break :sc .{ .separate_ssr_graph = brk: { // Intentionally not using a truthiness check const prop = try sc.getOptional(global, "separateSSRGraph", JSValue) orelse { return global.throwInvalidArguments("Missing 'framework.serverComponents.separateSSRGraph'", .{}); }; if (prop == .true) break :brk true; if (prop == .false) break :brk false; return global.throwInvalidArguments("'framework.serverComponents.separateSSRGraph' must be a boolean", .{}); }, .server_runtime_import = refs.track( try sc.getOptional(global, "serverRuntimeImportSource", ZigString.Slice) orelse { return global.throwInvalidArguments("Missing 'framework.serverComponents.serverRuntimeImportSource'", .{}); }, ), .server_register_client_reference = if (try sc.getOptional( global, "serverRegisterClientReferenceExport", ZigString.Slice, )) |slice| refs.track(slice) else "registerClientReference", }; }; const built_in_modules: bun.StringArrayHashMapUnmanaged(BuiltInModule) = built_in_modules: { const array = try opts.getArray(global, "builtInModules") orelse break :built_in_modules .{}; const len = try array.getLength(global); var files: bun.StringArrayHashMapUnmanaged(BuiltInModule) = .{}; try files.ensureTotalCapacity(arena, len); var it = try array.arrayIterator(global); var i: usize = 0; while (try it.next()) |file| : (i += 1) { if (!file.isObject()) { return global.throwInvalidArguments("'builtInModules[{d}]' is not an object", .{i}); } const path = try getOptionalString(file, global, "import", refs, arena) orelse { return global.throwInvalidArguments("'builtInModules[{d}]' is missing 'import'", .{i}); }; const value: BuiltInModule = if (try getOptionalString(file, global, "path", refs, arena)) |str| .{ .import = str } else if (try getOptionalString(file, global, "code", refs, arena)) |str| .{ .code = str } else return global.throwInvalidArguments("'builtInModules[{d}]' needs either 'path' or 'code'", .{i}); files.putAssumeCapacity(path, value); } break :built_in_modules files; }; const file_system_router_types: []FileSystemRouterType = brk: { const array: JSValue = try opts.getArray(global, "fileSystemRouterTypes") orelse { return global.throwInvalidArguments("Missing 'framework.fileSystemRouterTypes'", .{}); }; const len = try array.getLength(global); if (len > 256) { return global.throwInvalidArguments("Framework can only define up to 256 file-system router types", .{}); } const file_system_router_types = try arena.alloc(FileSystemRouterType, len); var it = try array.arrayIterator(global); var i: usize = 0; errdefer for (file_system_router_types[0..i]) |*fsr| fsr.style.deinit(); while (try it.next()) |fsr_opts| : (i += 1) { const root = try getOptionalString(fsr_opts, global, "root", refs, arena) orelse { return global.throwInvalidArguments("'fileSystemRouterTypes[{d}]' is missing 'root'", .{i}); }; const server_entry_point = try getOptionalString(fsr_opts, global, "serverEntryPoint", refs, arena) orelse { return global.throwInvalidArguments("'fileSystemRouterTypes[{d}]' is missing 'serverEntryPoint'", .{i}); }; const client_entry_point = try getOptionalString(fsr_opts, global, "clientEntryPoint", refs, arena); const prefix = try getOptionalString(fsr_opts, global, "prefix", refs, arena) orelse "/"; const ignore_underscores = try fsr_opts.getBooleanStrict(global, "ignoreUnderscores") orelse false; const layouts = try fsr_opts.getBooleanStrict(global, "layouts") orelse false; var style = try FrameworkRouter.Style.fromJS(try fsr_opts.get(global, "style") orelse { return global.throwInvalidArguments("'fileSystemRouterTypes[{d}]' is missing 'style'", .{i}); }, global); errdefer style.deinit(); const extensions: []const []const u8 = if (try fsr_opts.get(global, "extensions")) |exts_js| exts: { if (exts_js.isString()) { const str = try exts_js.toSlice(global, arena); defer str.deinit(); if (bun.strings.eqlComptime(str.slice(), "*")) { break :exts &.{}; } } else if (exts_js.isArray()) { var it_2 = try exts_js.arrayIterator(global); var i_2: usize = 0; const extensions = try arena.alloc([]const u8, try exts_js.getLength(global)); while (try it_2.next()) |array_item| : (i_2 += 1) { const slice = refs.track(try array_item.toSlice(global, arena)); if (bun.strings.eqlComptime(slice, "*")) return global.throwInvalidArguments("'extensions' cannot include \"*\" as an extension. Pass \"*\" instead of the array.", .{}); if (slice.len == 0) { return global.throwInvalidArguments("'extensions' cannot include \"\" as an extension.", .{}); } extensions[i_2] = if (slice[0] == '.') slice else try std.mem.concat(arena, u8, &.{ ".", slice }); } break :exts extensions; } return global.throwInvalidArguments("'extensions' must be an array of strings or \"*\" for all extensions", .{}); } else &.{ ".jsx", ".tsx", ".js", ".ts", ".cjs", ".cts", ".mjs", ".mts" }; const ignore_dirs: []const []const u8 = if (try fsr_opts.get(global, "ignoreDirs")) |exts_js| exts: { if (exts_js.isArray()) { var it_2 = try array.arrayIterator(global); var i_2: usize = 0; const dirs = try arena.alloc([]const u8, len); while (try it_2.next()) |array_item| : (i_2 += 1) { dirs[i_2] = refs.track(try array_item.toSlice(global, arena)); } break :exts dirs; } return global.throwInvalidArguments("'ignoreDirs' must be an array of strings or \"*\" for all extensions", .{}); } else &.{ ".git", "node_modules" }; file_system_router_types[i] = .{ .root = root, .prefix = prefix, .style = style, .entry_server = server_entry_point, .entry_client = client_entry_point, .ignore_underscores = ignore_underscores, .extensions = extensions, .ignore_dirs = ignore_dirs, .allow_layouts = layouts, }; } break :brk file_system_router_types; }; errdefer for (file_system_router_types) |*fsr| fsr.style.deinit(); const framework: Framework = .{ .is_built_in_react = false, .file_system_router_types = file_system_router_types, .react_fast_refresh = react_fast_refresh, .server_components = server_components, .built_in_modules = built_in_modules, }; if (try opts.getOptional(global, "plugins", JSValue)) |plugin_array| { try bundler_options.parsePluginArray(plugin_array, global); } return framework; } pub fn initTranspiler( framework: *Framework, arena: std.mem.Allocator, log: *bun.logger.Log, mode: Mode, renderer: Graph, out: *bun.transpiler.Transpiler, bundler_options: *const BuildConfigSubset, ) !void { const source_map: bun.options.SourceMapOption = switch (mode) { // Source maps must always be external, as DevServer special cases // the linking and part of the generation of these. It also relies // on source maps always being enabled. .development => .external, // TODO: follow user configuration else => .none, }; return initTranspilerWithOptions( framework, arena, log, mode, renderer, out, bundler_options, source_map, null, null, null, ); } pub fn initTranspilerWithOptions( framework: *Framework, arena: std.mem.Allocator, log: *bun.logger.Log, mode: Mode, renderer: Graph, out: *bun.transpiler.Transpiler, bundler_options: *const BuildConfigSubset, source_map: bun.options.SourceMapOption, minify_whitespace: ?bool, minify_syntax: ?bool, minify_identifiers: ?bool, ) !void { const JSAst = bun.ast; var ast_memory_allocator: JSAst.ASTMemoryAllocator = undefined; ast_memory_allocator.initWithoutStack(arena); var ast_scope = JSAst.ASTMemoryAllocator.Scope{ .previous = JSAst.Stmt.Data.Store.memory_allocator, .current = &ast_memory_allocator, }; ast_scope.enter(); defer ast_scope.exit(); out.* = try bun.Transpiler.init( arena, log, std.mem.zeroes(bun.schema.api.TransformOptions), null, ); out.options.target = switch (renderer) { .client => .browser, .server, .ssr => .bun, }; out.options.public_path = switch (renderer) { .client => DevServer.client_prefix, .server, .ssr => "", }; out.options.entry_points = &.{}; out.options.log = log; out.options.output_format = switch (mode) { .development => .internal_bake_dev, .production_dynamic, .production_static => .esm, }; out.options.out_extensions = bun.StringHashMap([]const u8).init(out.allocator); out.options.hot_module_reloading = mode == .development; out.options.code_splitting = mode != .development; // force disable filesystem output, even though bundle_v2 // is special cased to return before that code is reached. out.options.output_dir = ""; // framework configuration out.options.react_fast_refresh = mode == .development and renderer == .client and framework.react_fast_refresh != null; out.options.server_components = framework.server_components != null; out.options.conditions = try bun.options.ESMConditions.init( arena, out.options.target.defaultConditions(), out.options.target.isServerSide(), bundler_options.conditions.keys(), ); if (renderer == .server and framework.server_components != null) { try out.options.conditions.appendSlice(&.{"react-server"}); } if (mode == .development) { // Support `esm-env` package using this condition. try out.options.conditions.appendSlice(&.{"development"}); } // Ensure "node" condition is included for server-side rendering // This helps with package.json imports field resolution if (renderer == .server or renderer == .ssr) { try out.options.conditions.appendSlice(&.{"node"}); } out.options.production = mode != .development; out.options.tree_shaking = mode != .development; out.options.minify_syntax = minify_syntax orelse (mode != .development); out.options.minify_identifiers = minify_identifiers orelse (mode != .development); out.options.minify_whitespace = minify_whitespace orelse (mode != .development); out.options.css_chunking = true; out.options.framework = framework; out.options.inline_entrypoint_import_meta_main = true; if (bundler_options.ignoreDCEAnnotations) |ignore| out.options.ignore_dce_annotations = ignore; out.options.source_map = source_map; if (bundler_options.env != ._none) { out.options.env.behavior = bundler_options.env; out.options.env.prefix = bundler_options.env_prefix orelse ""; } out.resolver.opts = out.options; out.configureLinker(); try out.configureDefines(); out.options.jsx.development = mode == .development; try addImportMetaDefines(arena, out.options.define, mode, switch (renderer) { .client => .client, .server, .ssr => .server, }); if ((bundler_options.define.keys.len + bundler_options.drop.count()) > 0) { for (bundler_options.define.keys, bundler_options.define.values) |k, v| { const parsed = try bun.options.Define.Data.parse(k, v, false, false, log, arena); try out.options.define.insert(arena, k, parsed); } for (bundler_options.drop.keys()) |drop_item| { if (drop_item.len > 0) { const parsed = try bun.options.Define.Data.parse(drop_item, "", true, true, log, arena); try out.options.define.insert(arena, drop_item, parsed); } } } if (mode != .development) { // Hide information about the source repository, at the cost of debugging quality. out.options.entry_naming = "_bun/[hash].[ext]"; out.options.chunk_naming = "_bun/[hash].[ext]"; out.options.asset_naming = "_bun/[hash].[ext]"; } out.resolver.opts = out.options; } }; fn getOptionalString( target: JSValue, global: *jsc.JSGlobalObject, property: []const u8, allocations: *StringRefList, arena: Allocator, ) !?[]const u8 { const value = try target.get(global, property) orelse return null; if (value.isUndefinedOrNull()) return null; const str = try value.toBunString(global); return allocations.track(str.toUTF8(arena)); } pub const HmrRuntime = struct { code: [:0]const u8, /// The number of lines in the HMR runtime. This is used for sourcemap /// generation, where the first n lines are skipped. In release, these /// are always precalculated. line_count: u32, pub fn init(code: [:0]const u8) HmrRuntime { return .{ .code = code, .line_count = @intCast(std.mem.count(u8, code, "\n")), }; } }; pub fn getHmrRuntime(side: Side) callconv(bun.callconv_inline) HmrRuntime { return if (Environment.codegen_embed) switch (side) { .client => .init(@embedFile("bake-codegen/bake.client.js")), .server => .init(@embedFile("bake-codegen/bake.server.js")), } else .init(switch (side) { .client => bun.runtimeEmbedFile(.codegen_eager, "bake.client.js"), // server runtime is loaded once, so it is pointless to make this eager. .server => bun.runtimeEmbedFile(.codegen, "bake.server.js"), }); } pub const Mode = enum { development, production_dynamic, production_static, }; pub const Side = enum(u1) { client, server, pub fn graph(s: Side) Graph { return switch (s) { .client => .client, .server => .server, }; } }; pub const Graph = enum(u2) { client, server, /// Only used when Framework has .server_components.separate_ssr_graph set ssr, }; pub fn addImportMetaDefines( allocator: std.mem.Allocator, define: *bun.options.Define, mode: Mode, side: Side, ) !void { const Define = bun.options.Define; // The following are from Vite: https://vitejs.dev/guide/env-and-mode // Note that it is not currently possible to have mixed // modes (production + hmr dev server) // TODO: BASE_URL try define.insert( allocator, "import.meta.env.DEV", Define.Data.initBoolean(mode == .development), ); try define.insert( allocator, "import.meta.env.PROD", Define.Data.initBoolean(mode != .development), ); try define.insert( allocator, "import.meta.env.MODE", Define.Data.initStaticString(switch (mode) { .development => &.{ .data = "development" }, .production_dynamic, .production_static => &.{ .data = "production" }, }), ); try define.insert( allocator, "import.meta.env.SSR", Define.Data.initBoolean(side == .server), ); // To indicate a static build, `STATIC` is set to true then. try define.insert( allocator, "import.meta.env.STATIC", Define.Data.initBoolean(mode == .production_static), ); } pub const server_virtual_source: bun.logger.Source = .{ .path = bun.fs.Path.initForKitBuiltIn("bun", "bake/server"), .contents = "", // Virtual .index = bun.ast.Index.bake_server_data, }; pub const client_virtual_source: bun.logger.Source = .{ .path = bun.fs.Path.initForKitBuiltIn("bun", "bake/client"), .contents = "", // Virtual .index = bun.ast.Index.bake_client_data, }; /// Stack-allocated structure that is written to from end to start. /// Used as a staging area for building pattern strings. pub const PatternBuffer = struct { bytes: bun.PathBuffer, i: std.math.IntFittingRange(0, @sizeOf(bun.PathBuffer)), pub const empty: PatternBuffer = .{ .bytes = undefined, .i = @sizeOf(bun.PathBuffer), }; pub fn prepend(pb: *PatternBuffer, chunk: []const u8) void { bun.assert(pb.i >= chunk.len); pb.i -= @intCast(chunk.len); @memcpy(pb.slice()[0..chunk.len], chunk); } pub fn prependPart(pb: *PatternBuffer, part: FrameworkRouter.Part) void { switch (part) { .text => |text| { bun.assert(text.len == 0 or text[0] != '/'); pb.prepend(text); pb.prepend("/"); }, .param, .catch_all, .catch_all_optional => |name| { pb.prepend(name); pb.prepend("/:"); }, .group => {}, } } pub fn slice(pb: *PatternBuffer) []u8 { return pb.bytes[pb.i..]; } }; pub fn printWarning() void { // Silence this for the test suite if (bun.env_var.BUN_DEV_SERVER_TEST_RUNNER.get() == null) { bun.Output.warn( \\Be advised that Bun Bake is highly experimental, and its API \\will have breaking changes. Join the #bake Discord \\channel to help us find bugs: https://bun.com/discord \\ \\ , .{}); bun.Output.flush(); } } const std = @import("std"); const Allocator = std.mem.Allocator; const bun = @import("bun"); const Environment = bun.Environment; const jsc = bun.jsc; const JSValue = jsc.JSValue; const ZigString = jsc.ZigString; const Plugin = jsc.API.JSBundler.Plugin;