Compare commits

...

2 Commits

Author SHA1 Message Date
Jarred Sumner
0c75516f20 a 2025-02-08 19:55:16 -08:00
Jarred Sumner
0b1376b3ca wip 2025-02-08 19:37:18 -08:00
11 changed files with 689 additions and 77 deletions

View File

@@ -1811,8 +1811,8 @@ pub const HotUpdateContext = struct {
const subslice = rc.resolved_index_cache[start..][0..rc.sources.len];
comptime assert(@alignOf(IncrementalGraph(side).FileIndex.Optional) == @alignOf(u32));
comptime assert(@sizeOf(IncrementalGraph(side).FileIndex.Optional) == @sizeOf(u32));
// comptime assert(@alignOf(IncrementalGraph(side).FileIndex.Optional) == @alignOf(u32));
// comptime assert(@sizeOf(IncrementalGraph(side).FileIndex.Optional) == @sizeOf(u32));
return @ptrCast(&subslice[i.get()]);
}
};
@@ -2825,7 +2825,7 @@ pub fn IncrementalGraph(side: bake.Side) type {
// code because there is only one instance of the server. Instead,
// it stores which module graphs it is a part of. This makes sure
// that recompilation knows what bundler options to use.
.server => packed struct(u8) {
.server => struct {
/// Is this file built for the Server graph.
is_rsc: bool,
/// Is this file built for the SSR graph.
@@ -2871,7 +2871,7 @@ pub fn IncrementalGraph(side: bake.Side) type {
code_len: u32,
flags: Flags,
const Flags = packed struct(u32) {
const Flags = struct {
/// If the file has an error, the failure can be looked up
/// in the `.failures` map.
failed: bool,
@@ -2890,10 +2890,10 @@ pub fn IncrementalGraph(side: bake.Side) type {
unused: enum(u26) { unused } = .unused,
};
comptime {
assert(@sizeOf(@This()) == @sizeOf(u64) * 2);
assert(@alignOf(@This()) == @alignOf([*]u8));
}
// comptime {
// assert(@sizeOf(@This()) == @sizeOf(u64) * 2);
// assert(@alignOf(@This()) == @alignOf([*]u8));
// }
fn initJavaScript(code_slice: []const u8, flags: Flags) @This() {
assert(flags.kind == .js);
@@ -3007,11 +3007,6 @@ pub fn IncrementalGraph(side: bake.Side) type {
},
} else .empty;
}
comptime {
assert(@sizeOf(@This()) == @sizeOf(u32) * 5);
assert(@alignOf(@This()) == @alignOf(u32));
}
};
// If this data structure is not clear, see `DirectoryWatchStore.Dep`
@@ -3692,7 +3687,7 @@ pub fn IncrementalGraph(side: bake.Side) type {
/// Returns the key that was inserted.
pub fn insertEmpty(g: *@This(), abs_path: []const u8) bun.OOM![]const u8 {
comptime assert(side == .client); // not implemented
// comptime assert(side == .client); // not implemented
g.owner().graph_safety_lock.assertLocked();
const gop = try g.bundled_files.getOrPut(g.owner().allocator, abs_path);
if (!gop.found_existing) {
@@ -3783,7 +3778,7 @@ pub fn IncrementalGraph(side: bake.Side) type {
};
if (!found_existing) {
comptime assert(mode == .abs_path);
// comptime assert(mode == .abs_path);
gop.key_ptr.* = try bun.default_allocator.dupe(u8, key);
try g.first_dep.append(g.owner().allocator, .none);
try g.first_import.append(g.owner().allocator, .none);
@@ -4614,7 +4609,7 @@ pub const SerializedFailure = struct {
};
}
pub const Packed = packed struct(u32) {
pub const Packed = struct {
kind: enum(u2) { none, route, client, server },
data: u30,
@@ -5061,29 +5056,12 @@ const HmrTopic = enum(u8) {
_,
pub const max_count = @typeInfo(HmrTopic).@"enum".fields.len;
pub const Bits = @Type(.{ .@"struct" = .{
.backing_integer = @Type(.{ .int = .{
.bits = max_count,
.signedness = .unsigned,
} }),
.fields = &brk: {
const enum_fields = @typeInfo(HmrTopic).@"enum".fields;
var fields: [enum_fields.len]std.builtin.Type.StructField = undefined;
for (enum_fields, &fields) |e, *s| {
s.* = .{
.name = e.name,
.type = bool,
.default_value_ptr = &false,
.is_comptime = false,
.alignment = 0,
};
}
break :brk fields;
},
.decls = &.{},
.is_tuple = false,
.layout = .@"packed",
} });
pub const Bits = struct {
hot_update: bool = false,
errors: bool = false,
browser_error: bool = false,
visualizer: bool = false,
};
};
const HmrSocket = struct {
@@ -5263,7 +5241,7 @@ fn markAllRouteChildrenFailed(dev: *DevServer, route_index: Route.Index) void {
/// This task informs the DevServer's thread about new files to be bundled.
pub const HotReloadEvent = struct {
/// Align to cache lines to eliminate contention.
const Aligned = struct { aligned: HotReloadEvent align(std.atomic.cache_line) };
const Aligned = struct { aligned: HotReloadEvent };
owner: *DevServer,
/// Initialized in WatcherAtomics.watcherReleaseAndSubmitEvent
@@ -5391,14 +5369,14 @@ const WatcherAtomics = struct {
/// once. Memory is reused by swapping between these two. These items are
/// aligned to cache lines to reduce contention, since these structures are
/// carefully passed between two threads.
events: [2]HotReloadEvent.Aligned align(std.atomic.cache_line),
events: [2]HotReloadEvent.Aligned,
/// 0 - no watch
/// 1 - has fired additional watch
/// 2+ - new events available, watcher is waiting on bundler to finish
watcher_events_emitted: std.atomic.Value(u32),
/// Which event is the watcher holding on to.
/// This is not atomic because only the watcher thread uses this value.
current: u1 align(std.atomic.cache_line),
current: u1,
watcher_has_event: std.debug.SafetyLock,
dev_server_has_event: std.debug.SafetyLock,
@@ -5641,7 +5619,7 @@ pub fn numSubscribers(dev: *DevServer, topic: HmrTopic) u32 {
return if (dev.server) |s| s.numSubscribers(&.{@intFromEnum(topic)}) else 0;
}
const SafeFileId = packed struct(u32) {
const SafeFileId = struct {
side: bake.Side,
index: u30,
unused: enum(u1) { unused = 0 } = .unused,
@@ -5719,7 +5697,7 @@ fn relativePath(dev: *const DevServer, path: []const u8) []const u8 {
}
fn dumpStateDueToCrash(dev: *DevServer) !void {
comptime assert(bun.FeatureFlags.bake_debugging_features);
// comptime assert(bun.FeatureFlags.bake_debugging_features);
// being conservative about how much stuff is put on the stack.
var filepath_buf: [@min(4096, bun.MAX_PATH_BYTES)]u8 = undefined;
@@ -5757,7 +5735,7 @@ fn dumpStateDueToCrash(dev: *DevServer) !void {
Output.note("Dumped incremental bundler graph to {}", .{bun.fmt.quote(filepath)});
}
const RouteIndexAndRecurseFlag = packed struct(u32) {
const RouteIndexAndRecurseFlag = struct {
route_index: Route.Index,
/// Set true for layout
should_recurse_when_visiting: bool,
@@ -5771,7 +5749,7 @@ pub const EntryPointList = struct {
pub const empty: EntryPointList = .{ .set = .{} };
const Flags = packed struct(u8) {
const Flags = struct {
client: bool = false,
server: bool = false,
ssr: bool = false,
@@ -5918,7 +5896,7 @@ pub const Assets = struct {
.mime_type = mime_type,
.server = assets.owner().server orelse unreachable,
});
comptime assert(@TypeOf(slice.items(.hash)[0]) == void);
// comptime assert(@TypeOf(slice.items(.hash)[0]) == void);
assets.needs_reindex = true;
return .{ .index = @intCast(i) };
} else {

View File

@@ -219,4 +219,16 @@ export default [
},
klass: {},
}),
define({
name: "PageBundle",
noConstructor: true,
finalize: true,
proto: {
src: {
getter: "getSrc",
cache: true,
},
},
klass: {},
}),
];

View File

@@ -179,14 +179,16 @@ pub const StaticRoute = @import("./server/StaticRoute.zig");
const HTMLBundle = JSC.API.HTMLBundle;
const HTMLBundleRoute = HTMLBundle.HTMLBundleRoute;
const PageBundle = JSC.API.PageBundle;
const PageBundleRoute = PageBundle.PageBundleRoute;
pub const AnyStaticRoute = union(enum) {
StaticRoute: *StaticRoute,
HTMLBundleRoute: *HTMLBundleRoute,
PageBundleRoute: *PageBundleRoute,
pub fn memoryCost(this: AnyStaticRoute) usize {
return switch (this) {
.StaticRoute => |static_route| static_route.memoryCost(),
.HTMLBundleRoute => |html_bundle_route| html_bundle_route.memoryCost(),
inline else => |route| route.memoryCost(),
};
}
@@ -194,6 +196,7 @@ pub const AnyStaticRoute = union(enum) {
switch (this) {
.StaticRoute => |static_route| static_route.server = server,
.HTMLBundleRoute => |html_bundle_route| html_bundle_route.server = server,
.PageBundleRoute => |page_bundle_route| page_bundle_route.server = server,
}
}
@@ -201,6 +204,7 @@ pub const AnyStaticRoute = union(enum) {
switch (this) {
.StaticRoute => |static_route| static_route.deref(),
.HTMLBundleRoute => |html_bundle_route| html_bundle_route.deref(),
.PageBundleRoute => |page_bundle_route| page_bundle_route.deref(),
}
}
@@ -208,10 +212,16 @@ pub const AnyStaticRoute = union(enum) {
switch (this) {
.StaticRoute => |static_route| static_route.ref(),
.HTMLBundleRoute => |html_bundle_route| html_bundle_route.ref(),
.PageBundleRoute => |page_bundle_route| page_bundle_route.ref(),
}
}
pub fn fromJS(globalThis: *JSC.JSGlobalObject, argument: JSC.JSValue, dedupe_html_bundle_map: *std.AutoHashMap(*HTMLBundle, *HTMLBundleRoute)) bun.JSError!AnyStaticRoute {
pub fn fromJS(
globalThis: *JSC.JSGlobalObject,
argument: JSC.JSValue,
dedupe_html_bundle_map: *std.AutoHashMap(*HTMLBundle, *HTMLBundleRoute),
dedupe_page_bundle_map: *std.AutoHashMap(*PageBundle, *PageBundleRoute),
) bun.JSError!AnyStaticRoute {
if (argument.as(HTMLBundle)) |html_bundle| {
const entry = try dedupe_html_bundle_map.getOrPut(html_bundle);
if (!entry.found_existing) {
@@ -223,6 +233,17 @@ pub const AnyStaticRoute = union(enum) {
return .{ .HTMLBundleRoute = entry.value_ptr.* };
}
if (argument.as(PageBundle)) |page_bundle| {
const entry = try dedupe_page_bundle_map.getOrPut(page_bundle);
if (!entry.found_existing) {
entry.value_ptr.* = PageBundleRoute.init(page_bundle);
} else {
entry.value_ptr.*.ref();
}
return .{ .PageBundleRoute = entry.value_ptr.* };
}
return .{ .StaticRoute = try StaticRoute.fromJS(globalThis, argument) };
}
};
@@ -1118,6 +1139,12 @@ pub const ServerConfig = struct {
return global.throwInvalidArguments("Bun.serve expects an object", .{});
}
if (try arg.get(global, "development")) |dev| {
args.development = dev.coerce(bool, global);
args.reuse_port = !args.development;
}
if (global.hasException()) return error.JSError;
if (try arg.get(global, "static")) |static| {
if (!static.isObject()) {
return global.throwInvalidArguments("Bun.serve expects 'static' to be an object shaped like { [pathname: string]: Response }", .{});
@@ -1131,6 +1158,8 @@ pub const ServerConfig = struct {
var dedupe_html_bundle_map = std.AutoHashMap(*HTMLBundle, *HTMLBundleRoute).init(bun.default_allocator);
defer dedupe_html_bundle_map.deinit();
var dedupe_page_bundle_map = std.AutoHashMap(*PageBundle, *PageBundleRoute).init(bun.default_allocator);
defer dedupe_page_bundle_map.deinit();
errdefer {
for (args.static_routes.items) |*static_route| {
@@ -1153,7 +1182,7 @@ pub const ServerConfig = struct {
return global.throwInvalidArguments("Invalid static route \"{s}\". Please encode all non-ASCII characters in the path.", .{path});
}
const route = try AnyStaticRoute.fromJS(global, value, &dedupe_html_bundle_map);
const route = try AnyStaticRoute.fromJS(global, value, &dedupe_html_bundle_map, &dedupe_page_bundle_map);
args.static_routes.append(.{
.path = path,
.route = route,
@@ -1162,10 +1191,11 @@ pub const ServerConfig = struct {
// When HTML bundles are provided, ensure DevServer options are ready
// The presence of these options causes Bun.serve to initialize things.
if (bun.bake.DevServer.enabled and dedupe_html_bundle_map.count() > 0) {
if (!args.development and bun.bake.DevServer.enabled and dedupe_html_bundle_map.count() > 0) {
// TODO: this should be the dir with bunfig??
const root = bun.fs.FileSystem.instance.top_level_dir;
var arena = std.heap.ArenaAllocator.init(bun.default_allocator);
errdefer arena.deinit();
const framework = try bun.bake.Framework.auto(arena.allocator(), &global.bunVM().transpiler.resolver);
args.bake = .{
.arena = arena,
@@ -1278,12 +1308,6 @@ pub const ServerConfig = struct {
}
if (global.hasException()) return error.JSError;
if (try arg.get(global, "development")) |dev| {
args.development = dev.coerce(bool, global);
args.reuse_port = !args.development;
}
if (global.hasException()) return error.JSError;
if (try arg.getTruthy(global, "app")) |bake_args_js| {
if (args.bake != null) {
// "app" is likely to be removed in favor of the HTML loader.
@@ -5818,6 +5842,7 @@ const ServePlugins = struct {
plugin: *bun.JSC.API.JSBundler.Plugin,
promise: JSC.JSPromise.Strong,
html_bundle_routes: std.ArrayListUnmanaged(*HTMLBundleRoute),
page_bundle_routes: std.ArrayListUnmanaged(*PageBundleRoute),
dev_server: ?*bun.bake.DevServer,
},
loaded: *bun.JSC.API.JSBundler.Plugin,
@@ -5834,6 +5859,7 @@ const ServePlugins = struct {
pub const Callback = union(enum) {
html_bundle_route: *HTMLBundleRoute,
page_bundle_route: *PageBundleRoute,
dev_server: *bun.bake.DevServer,
};
@@ -5863,6 +5889,10 @@ const ServePlugins = struct {
route.ref();
try pending.html_bundle_routes.append(bun.default_allocator, route);
},
.page_bundle_route => |route| {
route.ref();
try pending.page_bundle_routes.append(bun.default_allocator, route);
},
.dev_server => |server| {
assert(pending.dev_server == null or pending.dev_server == server); // one dev server per server
pending.dev_server = server;
@@ -5903,7 +5933,8 @@ const ServePlugins = struct {
this.state = .{ .pending = .{
.promise = JSC.JSPromise.Strong.init(global),
.plugin = plugin,
.html_bundle_routes = .empty,
.html_bundle_routes = .{},
.page_bundle_routes = .{},
.dev_server = null,
} };
@@ -5969,11 +6000,20 @@ const ServePlugins = struct {
const pending = &this.state.pending;
const plugin = pending.plugin;
pending.promise.deinit();
defer pending.html_bundle_routes.deinit(bun.default_allocator);
var html_bundle_routes = pending.html_bundle_routes;
var page_bundle_routes = pending.page_bundle_routes;
pending.html_bundle_routes = .{};
pending.page_bundle_routes = .{};
defer html_bundle_routes.deinit(bun.default_allocator);
defer page_bundle_routes.deinit(bun.default_allocator);
this.state = .{ .loaded = plugin };
for (pending.html_bundle_routes.items) |route| {
for (html_bundle_routes.items) |route| {
route.onPluginsResolved(plugin) catch bun.outOfMemory();
route.deref();
}
for (page_bundle_routes.items) |route| {
route.onPluginsResolved(plugin) catch bun.outOfMemory();
route.deref();
}
@@ -5997,14 +6037,23 @@ const ServePlugins = struct {
const pending = &this.state.pending;
pending.plugin.deinit();
pending.promise.deinit();
defer pending.html_bundle_routes.deinit(bun.default_allocator);
var html_bundle_routes = pending.html_bundle_routes;
var page_bundle_routes = pending.page_bundle_routes;
pending.html_bundle_routes = .{};
pending.page_bundle_routes = .{};
defer html_bundle_routes.deinit(bun.default_allocator);
defer page_bundle_routes.deinit(bun.default_allocator);
this.state = .err;
Output.errGeneric("Failed to load plugins for Bun.serve:", .{});
global.bunVM().runErrorHandler(err, null);
for (pending.html_bundle_routes.items) |route| {
for (html_bundle_routes.items) |route| {
route.onPluginsRejected() catch bun.outOfMemory();
route.deref();
}
for (page_bundle_routes.items) |route| {
route.onPluginsRejected() catch bun.outOfMemory();
route.deref();
}
@@ -7471,10 +7520,10 @@ pub fn NewServer(comptime NamespaceType: type, comptime ssl_enabled_: bool, comp
.StaticRoute => |static_route| {
ServerConfig.applyStaticRoute(any_server, ssl_enabled, app, *StaticRoute, static_route, entry.path);
},
.HTMLBundleRoute => |html_bundle_route| {
ServerConfig.applyStaticRoute(any_server, ssl_enabled, app, *HTMLBundleRoute, html_bundle_route, entry.path);
inline .PageBundleRoute, .HTMLBundleRoute => |route| {
ServerConfig.applyStaticRoute(any_server, ssl_enabled, app, @TypeOf(route), route, entry.path);
if (dev_server) |dev| {
dev.html_router.put(dev.allocator, entry.path, html_bundle_route) catch bun.outOfMemory();
dev.html_router.put(dev.allocator, entry.path, route) catch bun.outOfMemory();
}
needs_plugins = true;
},

View File

@@ -254,7 +254,7 @@ pub const HTMLBundleRoute = struct {
bun.default_allocator,
);
completion_task.started_at_ns = bun.getRoughTickCount().ns();
completion_task.html_build_task = this;
completion_task.build_task = .{ .html = this };
this.state = .{ .building = completion_task };
// While we're building, ensure this doesn't get freed.
@@ -477,7 +477,7 @@ const JSBundler = JSC.API.JSBundler;
const HTTPResponse = bun.uws.AnyResponse;
const uws = bun.uws;
const AnyServer = JSC.API.AnyServer;
const StaticRoute = @import("./StaticRoute.zig");
const StaticRoute = bun.server.StaticRoute;
const debug = bun.Output.scoped(.HTMLBundle, true);
const strings = bun.strings;

View File

@@ -0,0 +1,479 @@
//! This object is a description of an HTML bundle. It is created by importing an
//! HTML file, and can be passed to the `static` option in `Bun.serve`. The build
//! is done lazily (state held in PageBundle.Route or DevServer.RouteBundle.HTML).
pub const PageBundle = @This();
pub usingnamespace JSC.Codegen.JSPageBundle;
// PageBundle can be owned by JavaScript as well as any number of Server instances.
pub usingnamespace bun.NewRefCounted(PageBundle, deinit, null);
ref_count: u32 = 1,
global: *JSGlobalObject,
path: []const u8,
/// Initialize an PageBundle given a path.
pub fn init(global: *JSGlobalObject, path: []const u8) !*PageBundle {
return PageBundle.new(.{
.global = global,
.path = try bun.default_allocator.dupe(u8, path),
});
}
pub fn finalize(this: *PageBundle) void {
this.deref();
}
pub fn deinit(this: *PageBundle) void {
bun.default_allocator.free(this.path);
this.destroy();
}
pub fn getSrc(this: *PageBundle, globalObject: *JSGlobalObject) JSValue {
var str = bun.String.createUTF8(this.path);
return str.transferToJS(globalObject);
}
// TODO: Rename to `Route`
/// An PageBundle can be used across multiple server instances, an
/// PageBundle.Route can only be used on one server, but is also
/// reference-counted because a server can have multiple instances of the same
/// html file on multiple endpoints.
pub const PageBundleRoute = struct {
/// Rename to `bundle`
html_bundle: *PageBundle,
ref_count: u32 = 1,
// TODO: attempt to remove the null case. null is only present during server
// initialization as only a ServerConfig object is present.
server: ?AnyServer = null,
/// When using DevServer, this value is never read or written to.
state: State,
/// Written and read by DevServer to identify if this route has been
/// registered with the bundler.
dev_server_id: bun.bake.DevServer.RouteBundle.Index.Optional = .none,
/// When state == .pending, incomplete responses are stored here.
pending_responses: std.ArrayListUnmanaged(*PendingResponse) = .{},
/// One PageBundle.Route can be specified multiple times
pub usingnamespace bun.NewRefCounted(@This(), _deinit, null);
pub fn memoryCost(this: *const PageBundleRoute) usize {
var cost: usize = 0;
cost += @sizeOf(PageBundleRoute);
cost += this.pending_responses.items.len * @sizeOf(PendingResponse);
cost += this.state.memoryCost();
return cost;
}
pub fn init(html_bundle: *PageBundle) *PageBundleRoute {
html_bundle.ref();
return PageBundleRoute.new(.{
.html_bundle = html_bundle,
.pending_responses = .{},
.ref_count = 1,
.server = null,
.state = .pending,
});
}
pub const State = union(enum) {
pending,
building: ?*bun.BundleV2.JSBundleCompletionTask,
rendering,
err: bun.logger.Log,
html: *StaticRoute,
pub fn deinit(this: *State) void {
switch (this.*) {
.err => |*log| {
log.deinit();
},
.building => |completion| if (completion) |c| {
c.cancelled = true;
c.deref();
},
.html => {
this.html.deref();
},
.pending => {},
}
}
pub fn memoryCost(this: *const State) usize {
return switch (this.*) {
.pending => 0,
.building => 0,
.err => |log| log.memoryCost(),
.html => |html| html.memoryCost(),
};
}
};
fn _deinit(this: *PageBundleRoute) void {
for (this.pending_responses.items) |pending_response| {
pending_response.deref();
}
this.pending_responses.deinit(bun.default_allocator);
this.html_bundle.deref();
this.state.deinit();
this.destroy();
}
pub fn onRequest(this: *PageBundleRoute, req: *uws.Request, resp: HTTPResponse) void {
this.onAnyRequest(req, resp, false);
}
pub fn onHEADRequest(this: *PageBundleRoute, req: *uws.Request, resp: HTTPResponse) void {
this.onAnyRequest(req, resp, true);
}
fn onAnyRequest(this: *PageBundleRoute, req: *uws.Request, resp: HTTPResponse, is_head: bool) void {
this.ref();
defer this.deref();
const server: AnyServer = this.server orelse {
resp.endWithoutBody(true);
return;
};
if (server.config().development) {
// Simpler development workflow which rebundles on every request.
if (this.state == .html) {
this.state.html.deref();
this.state = .pending;
} else if (this.state == .err) {
this.state.err.deinit();
this.state = .pending;
}
}
state: switch (this.state) {
.pending => {
if (bun.Environment.enable_logs)
debug("onRequest: {s} - pending", .{req.url()});
this.scheduleBundle(server) catch bun.outOfMemory();
continue :state this.state;
},
.building => {
if (bun.Environment.enable_logs)
debug("onRequest: {s} - building", .{req.url()});
// create the PendingResponse, add it to the list
var pending = PendingResponse.new(.{
.method = bun.http.Method.which(req.method()) orelse {
resp.writeStatus("405 Method Not Allowed");
resp.endWithoutBody(true);
return;
},
.resp = resp,
.server = this.server,
.route = this,
.ref_count = 1,
});
this.pending_responses.append(bun.default_allocator, pending) catch bun.outOfMemory();
this.ref();
pending.ref();
resp.onAborted(*PendingResponse, PendingResponse.onAborted, pending);
req.setYield(false);
},
.err => |log| {
if (bun.Environment.enable_logs)
debug("onRequest: {s} - err", .{req.url()});
_ = log; // TODO: use the code from DevServer.zig to render the error
resp.endWithoutBody(true);
},
.html => |html| {
if (bun.Environment.enable_logs)
debug("onRequest: {s} - html", .{req.url()});
if (is_head) {
html.onHEADRequest(req, resp);
} else {
html.onRequest(req, resp);
}
},
}
}
/// Schedule a bundle to be built.
/// If success, bumps the ref count and returns true;
fn scheduleBundle(this: *PageBundleRoute, server: AnyServer) !void {
switch (server.getOrLoadPlugins(.{ .page_bundle_route = this })) {
.err => this.state = .{ .err = bun.logger.Log.init(bun.default_allocator) },
.ready => |plugins| try onPluginsResolved(this, plugins),
.pending => this.state = .{ .building = null },
}
}
pub fn onPluginsResolved(this: *PageBundleRoute, plugins: ?*bun.JSC.API.JSBundler.Plugin) !void {
const global = this.html_bundle.global;
const server = this.server.?;
const development = server.config().development;
const vm = global.bunVM();
var config: JSBundler.Config = .{};
errdefer config.deinit(bun.default_allocator);
try config.entry_points.insert(this.html_bundle.path);
try config.public_path.appendChar('/');
config.target = .browser;
if (bun.CLI.Command.get().args.serve_minify_identifiers) |minify_identifiers| {
config.minify.identifiers = minify_identifiers;
} else if (!development) {
config.minify.identifiers = true;
}
if (bun.CLI.Command.get().args.serve_minify_whitespace) |minify_whitespace| {
config.minify.whitespace = minify_whitespace;
} else if (!development) {
config.minify.whitespace = true;
}
if (bun.CLI.Command.get().args.serve_minify_syntax) |minify_syntax| {
config.minify.syntax = minify_syntax;
} else if (!development) {
config.minify.syntax = true;
}
if (!development) {
config.define.put("process.env.NODE_ENV", "\"production\"") catch bun.outOfMemory();
config.jsx.development = false;
} else {
config.force_node_env = .development;
config.jsx.development = true;
}
config.source_map = .linked;
const completion_task = try bun.BundleV2.createAndScheduleCompletionTask(
config,
plugins,
global,
vm.eventLoop(),
bun.default_allocator,
);
completion_task.started_at_ns = bun.getRoughTickCount().ns();
completion_task.build_task = .{ .page = this };
this.state = .{ .building = completion_task };
// While we're building, ensure this doesn't get freed.
this.ref();
}
pub fn onPluginsRejected(this: *PageBundleRoute) !void {
debug("PageBundleRoute(0x{x}) plugins rejected", .{@intFromPtr(this)});
this.state = .{ .err = bun.logger.Log.init(bun.default_allocator) };
this.resumePendingResponses();
}
pub fn onComplete(this: *PageBundleRoute, completion_task: *bun.BundleV2.JSBundleCompletionTask) void {
// For the build task.
defer this.deref();
switch (completion_task.result) {
.err => |err| {
if (bun.Environment.enable_logs)
debug("onComplete: err - {s}", .{@errorName(err)});
this.state = .{ .err = bun.logger.Log.init(bun.default_allocator) };
completion_task.log.cloneToWithRecycled(&this.state.err, true) catch bun.outOfMemory();
if (this.server) |server| {
if (server.config().development) {
switch (bun.Output.enable_ansi_colors_stderr) {
inline else => |enable_ansi_colors| {
var writer = bun.Output.errorWriterBuffered();
this.state.err.printWithEnableAnsiColors(&writer, enable_ansi_colors) catch {};
writer.context.flush() catch {};
},
}
}
}
},
.value => |bundle| {
if (bun.Environment.enable_logs)
debug("onComplete: success", .{});
// Find the HTML entry point and create static routes
const server: AnyServer = this.server orelse return;
const globalThis = server.globalThis();
const output_files = bundle.output_files.items;
if (server.config().development) {
const now = bun.getRoughTickCount().ns();
const duration = now - completion_task.started_at_ns;
var duration_f64: f64 = @floatFromInt(duration);
duration_f64 /= std.time.ns_per_s;
bun.Output.printElapsed(duration_f64);
var byte_length: u64 = 0;
for (output_files) |*output_file| {
byte_length += output_file.size_without_sourcemap;
}
bun.Output.prettyErrorln(" <green>bundle<r> {s} <d>{d:.2} KB<r>", .{ std.fs.path.basename(this.html_bundle.path), @as(f64, @floatFromInt(byte_length)) / 1000.0 });
bun.Output.flush();
}
var this_html_route: ?*StaticRoute = null;
// Create static routes for each output file
for (output_files) |*output_file| {
const blob = JSC.WebCore.AnyBlob{ .Blob = output_file.toBlob(bun.default_allocator, globalThis) catch bun.outOfMemory() };
var headers = JSC.WebCore.Headers{ .allocator = bun.default_allocator };
headers.append("Content-Type", blob.Blob.contentTypeOrMimeType() orelse output_file.loader.toMimeType().value) catch bun.outOfMemory();
// Do not apply etags to html.
if (output_file.loader != .html and output_file.value == .buffer) {
var hashbuf: [64]u8 = undefined;
const etag_str = std.fmt.bufPrint(&hashbuf, "{}", .{bun.fmt.hexIntLower(output_file.hash)}) catch bun.outOfMemory();
headers.append("ETag", etag_str) catch bun.outOfMemory();
if (!server.config().development and (output_file.output_kind == .chunk))
headers.append("Cache-Control", "public, max-age=31536000") catch bun.outOfMemory();
}
// Add a SourceMap header if we have a source map index
// and it's in development mode.
if (server.config().development) {
if (output_file.source_map_index != std.math.maxInt(u32)) {
var route_path = output_files[output_file.source_map_index].dest_path;
if (strings.hasPrefixComptime(route_path, "./") or strings.hasPrefixComptime(route_path, ".\\")) {
route_path = route_path[1..];
}
headers.append("SourceMap", route_path) catch bun.outOfMemory();
}
}
const static_route = StaticRoute.new(.{
.blob = blob,
.server = server,
.status_code = 200,
.headers = headers,
.cached_blob_size = blob.size(),
});
if (this_html_route == null and output_file.output_kind == .@"entry-point") {
if (output_file.loader == .html) {
this_html_route = static_route;
}
}
var route_path = output_file.dest_path;
// The route path gets cloned inside of appendStaticRoute.
if (strings.hasPrefixComptime(route_path, "./") or strings.hasPrefixComptime(route_path, ".\\")) {
route_path = route_path[1..];
}
server.appendStaticRoute(route_path, .{ .StaticRoute = static_route }) catch bun.outOfMemory();
}
const html_route: *StaticRoute = this_html_route orelse @panic("Internal assertion failure: HTML entry point not found in PageBundle.");
const html_route_clone = html_route.clone(globalThis) catch bun.outOfMemory();
this.state = .{ .html = html_route_clone };
if (!(server.reloadStaticRoutes() catch bun.outOfMemory())) {
// Server has shutdown, so it won't receive any new requests
// TODO: handle this case
}
},
.pending => unreachable,
}
// Handle pending responses
this.resumePendingResponses();
}
pub fn resumePendingResponses(this: *PageBundleRoute) void {
var pending = this.pending_responses;
defer pending.deinit(bun.default_allocator);
this.pending_responses = .{};
for (pending.items) |pending_response| {
defer pending_response.deref(); // First ref for being in the pending items array.
const resp = pending_response.resp;
const method = pending_response.method;
if (!pending_response.is_response_pending) {
// Aborted
continue;
}
// Second ref for UWS abort callback.
defer pending_response.deref();
pending_response.is_response_pending = false;
resp.clearAborted();
switch (this.state) {
.html => |html| {
if (method == .HEAD) {
html.onHEAD(resp);
} else {
html.on(resp);
}
},
.err => |log| {
if (this.server.?.config().development) {
_ = log; // TODO: use the code from DevServer.zig to render the error
} else {
// To protect privacy, do not show errors to end users in production.
// TODO: Show a generic error page.
}
resp.writeStatus("500 Build Failed");
resp.endWithoutBody(false);
},
else => {
resp.endWithoutBody(false);
},
}
}
}
/// Represents an in-flight response before the bundle has finished building.
pub const PendingResponse = struct {
method: bun.http.Method,
resp: HTTPResponse,
ref_count: u32 = 1,
is_response_pending: bool = true,
server: ?AnyServer = null,
route: *PageBundleRoute,
pub usingnamespace bun.NewRefCounted(@This(), destroyInternal, null);
fn destroyInternal(this: *PendingResponse) void {
if (this.is_response_pending) {
this.resp.clearAborted();
this.resp.clearOnWritable();
this.resp.endWithoutBody(true);
}
this.route.deref();
this.destroy();
}
pub fn onAborted(this: *PendingResponse, resp: HTTPResponse) void {
_ = resp; // autofix
bun.debugAssert(this.is_response_pending == true);
this.is_response_pending = false;
// Technically, this could be the final ref count, but we don't want to risk it
this.route.ref();
defer this.route.deref();
while (std.mem.indexOfScalar(*PendingResponse, this.route.pending_responses.items, this)) |index| {
_ = this.route.pending_responses.orderedRemove(index);
this.route.deref();
}
this.deref();
}
};
};
const bun = @import("root").bun;
const std = @import("std");
const JSC = bun.JSC;
const JSValue = JSC.JSValue;
const JSGlobalObject = JSC.JSGlobalObject;
const JSString = JSC.JSString;
const JSValueRef = JSC.JSValueRef;
const JSBundler = JSC.API.JSBundler;
const HTTPResponse = bun.uws.AnyResponse;
const uws = bun.uws;
const AnyServer = JSC.API.AnyServer;
const StaticRoute = bun.server.StaticRoute;
const debug = bun.Output.scoped(.PageBundle, true);
const strings = bun.strings;

View File

@@ -79,6 +79,7 @@ pub const Classes = struct {
pub const S3Client = JSC.WebCore.S3Client;
pub const S3Stat = JSC.WebCore.S3Stat;
pub const HTMLBundle = JSC.API.HTMLBundle;
pub const PageBundle = JSC.API.PageBundle;
pub const StatFs = JSC.Node.StatFSSmall;
pub const BigIntStatFs = JSC.Node.StatFSBig;

View File

@@ -2102,6 +2102,33 @@ pub const ModuleLoader = struct {
};
},
.page_reference => {
if (flags.disableTranspiling()) {
return ResolvedSource{
.allocator = null,
.source_code = bun.String.empty,
.specifier = input_specifier,
.source_url = input_specifier.createIfDifferent(path.text),
.hash = 0,
.tag = .esm,
};
}
if (globalObject == null) {
return error.NotSupported;
}
const bundle = try JSC.API.PageBundle.init(globalObject.?, path.text);
return ResolvedSource{
.allocator = &jsc_vm.allocator,
.jsvalue_for_export = bundle.toJS(globalObject.?),
.specifier = input_specifier,
.source_url = input_specifier.createIfDifferent(path.text),
.hash = 0,
.tag = .export_default_object,
};
},
else => {
if (flags.disableTranspiling()) {
return ResolvedSource{
@@ -2343,6 +2370,8 @@ pub const ModuleLoader = struct {
loader = .tsx;
} else if (attribute.eqlComptime("html")) {
loader = .html;
} else if (attribute.eqlComptime("page")) {
loader = .page_reference;
}
}

View File

@@ -1556,6 +1556,52 @@ pub const BundleV2 = struct {
return try this.linker.generateChunksInParallel(chunks, false);
}
pub fn generateFromPageBundle(
entry_points: bake.production.EntryPointMap,
server_transpiler: *Transpiler,
kit_options: BakeOptions,
allocator: std.mem.Allocator,
event_loop: EventLoop,
) !std.ArrayList(options.OutputFile) {
var this = try BundleV2.init(server_transpiler, kit_options, allocator, event_loop, false, null, null);
this.unique_key = generateUniqueKey();
if (this.transpiler.log.hasErrors()) {
return error.BuildFailed;
}
this.graph.pool.pool.schedule(try this.enqueueEntryPoints(.bake_production, entry_points));
if (this.transpiler.log.hasErrors()) {
return error.BuildFailed;
}
this.waitForParse();
if (this.transpiler.log.hasErrors()) {
return error.BuildFailed;
}
try this.processServerComponentManifestFiles();
const reachable_files = try this.findReachableFiles();
try this.processFilesToCopy(reachable_files);
try this.addServerComponentBoundariesAsExtraEntryPoints();
try this.cloneAST();
const chunks = try this.linker.link(
this,
this.graph.entry_points.items,
this.graph.server_component_boundaries,
reachable_files,
);
return try this.linker.generateChunksInParallel(chunks, false);
}
pub fn addServerComponentBoundariesAsExtraEntryPoints(this: *BundleV2) !void {
// Prepare server component boundaries. Each boundary turns into two
// entry points, a client entrypoint and a server entrypoint.
@@ -1722,7 +1768,7 @@ pub const BundleV2 = struct {
log: Logger.Log,
cancelled: bool = false,
html_build_task: ?*JSC.API.HTMLBundle.HTMLBundleRoute = null,
build_task: BuildTask = .none,
result: Result = .{ .pending = {} },
@@ -1734,6 +1780,20 @@ pub const BundleV2 = struct {
pub usingnamespace bun.NewThreadSafeRefCounted(JSBundleCompletionTask, _deinit, null);
pub const BuildTask = union(enum) {
none: void,
html: *JSC.API.HTMLBundle.HTMLBundleRoute,
page: *bun.JSC.API.Server.PageBundleRoute,
pub fn onComplete(this: BuildTask, completion: *JSBundleCompletionTask) void {
switch (this) {
.none => unreachable,
.html => |html| html.onComplete(completion),
.page => |page| page.onComplete(completion),
}
}
};
pub fn configureBundler(
completion: *JSBundleCompletionTask,
transpiler: *Transpiler,
@@ -1834,9 +1894,9 @@ pub const BundleV2 = struct {
return;
}
if (this.html_build_task) |html_build_task| {
if (this.build_task != .none) {
this.plugins = null;
html_build_task.onComplete(this);
this.build_task.onComplete(this);
return;
}
@@ -4043,7 +4103,7 @@ pub const ParseTask = struct {
.dataurl, .base64, .bunsh => {
return try getEmptyAST(log, transpiler, opts, allocator, source, E.String);
},
.file, .wasm => {
.file, .wasm, .page_reference => {
bun.assert(loader.shouldCopyForBundling());
// Put a unique key in the AST to implement the URL loader. At the end
@@ -7634,13 +7694,13 @@ pub const LinkerContext = struct {
.{@tagName(loader)},
) catch bun.outOfMemory();
},
.sqlite_embedded => {
.sqlite_embedded, .page_reference => {
this.log.addErrorFmt(
source,
record.range.loc,
this.allocator,
"Cannot import a \"sqlite_embedded\" file into a CSS file",
.{},
"Cannot import a \"{s}\" file into a CSS file",
.{@tagName(loader)},
) catch bun.outOfMemory();
},
.css, .file, .toml, .wasm, .base64, .dataurl, .text, .bunsh => {},

View File

@@ -53,6 +53,7 @@ pub const API = struct {
pub const NativeZlib = @import("./bun.js/node/node_zlib_binding.zig").SNativeZlib;
pub const NativeBrotli = @import("./bun.js/node/node_zlib_binding.zig").SNativeBrotli;
pub const HTMLBundle = @import("./bun.js/api/server/HTMLBundle.zig");
pub const PageBundle = @import("./bun.js/api/server/PageBundle.zig");
};
pub const Postgres = @import("./sql/postgres.zig");
pub const DNS = @import("./bun.js/api/bun/dns_resolver.zig");

View File

@@ -645,6 +645,8 @@ pub const Loader = enum(u8) {
sqlite,
sqlite_embedded,
html,
/// Returns
page_reference,
pub fn disableHTML(this: Loader) Loader {
return switch (this) {
@@ -825,6 +827,7 @@ pub const Loader = enum(u8) {
.dataurl => .dataurl,
.text => .text,
.sqlite_embedded, .sqlite => .sqlite,
.page_reference => .file,
};
}

View File

@@ -981,7 +981,7 @@ pub const Transpiler = struct {
output_file.value = .{ .buffer = .{ .allocator = alloc, .bytes = result.code } };
},
.html, .bunsh, .sqlite_embedded, .sqlite, .wasm, .file, .napi => {
else => {
const hashed_name = try transpiler.linker.getHashedFilename(file_path, null);
var pathname = try transpiler.allocator.alloc(u8, hashed_name.len + file_path.name.ext.len);
bun.copy(u8, pathname, hashed_name);