Files
bun.sh/src/bake.zig
taylor.fish 437e15bae5 Replace catch bun.outOfMemory() with safer alternatives (#22141)
Replace `catch bun.outOfMemory()`, which can accidentally catch
non-OOM-related errors, with either `bun.handleOom` or a manual `catch
|err| switch (err)`.

(For internal tracking: fixes STAB-1070)

---------

Co-authored-by: Dylan Conway <dylan.conway567@gmail.com>
2025-08-26 12:50:25 -07:00

980 lines
39 KiB
Zig

//! 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 {
if (!config.isObject()) {
return global.throwInvalidArguments("'" ++ api_name ++ "' is not an object", .{});
}
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 (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.getenvZ("BUN_DEV_SERVER_TEST_RUNNER") == null) {
bun.Output.warn(
\\Be advised that Bun Bake is highly experimental, and its API
\\will have breaking changes. Join the <magenta>#bake<r> Discord
\\channel to help us find bugs: <blue>https://bun.com/discord<r>
\\
\\
, .{});
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;