mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 18:38:55 +00:00
### What does this PR do? The `then` function in `transpiler.transform` can cause GC, which means it can cause the `Transpiler` to become freed, which means that if that same transpiler is in use by another run on the other thread, it could have pointers to invalid memory. Also, `ESMCondition` has unnecesasry memory allocations and there is a very tiny memory leak in optionsFromLoaders ### How did you verify your code works? Existing tests --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
980 lines
39 KiB
Zig
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 {
|
|
al.strings.append(bun.default_allocator, str) catch bun.outOfMemory();
|
|
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 bun.outOfMemory(),
|
|
};
|
|
}
|
|
|
|
/// 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;
|