From b3fe9c0cd3783cfec2a00338538192b4e66f650a Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Tue, 30 Sep 2025 20:55:16 -0700 Subject: [PATCH] better way to do static assets --- src/bake/prod/Manifest.zig | 31 ++++++++--- src/bake/prod/ProductionServerMethods.zig | 63 +++++++++++++++++++++-- src/bake/production.zig | 19 +++++++ src/bun.js/api/server/ServerConfig.zig | 17 ++++-- src/js/builtins/Bake.ts | 9 ++-- 5 files changed, 123 insertions(+), 16 deletions(-) diff --git a/src/bake/prod/Manifest.zig b/src/bake/prod/Manifest.zig index e946e977c6..d39323dab1 100644 --- a/src/bake/prod/Manifest.zig +++ b/src/bake/prod/Manifest.zig @@ -21,6 +21,8 @@ routes: []Route = &[_]Route{}, build_output_dir: []const u8 = "dist", /// Router types with their server entrypoints router_types: []RouterType = &[_]RouterType{}, +/// Static assets +assets: [][]const u8 = &[_][]const u8{}, /// All memory allocated with bun.default_allocator here router: bun.ptr.Owned(*FrameworkRouter), @@ -130,6 +132,23 @@ fn initFromJSON(self: *Manifest, source: *const logger.Source, log: *logger.Log) } } + if (json_obj.get("assets")) |assets_prop| { + if (assets_prop.data == .e_array) { + const items = assets_prop.data.e_array.items.slice(); + self.assets = try allocator.alloc([]const u8, items.len); + + for (items, self.assets) |*in, *out| { + if (in.data != .e_string) { + // All style array elements must be strings + try log.addError(&json_source, Loc.Empty, "\"assets\" must be an array of strings"); + return error.InvalidManifest; + } + const style_str = try in.data.e_string.string(allocator); + out.* = style_str; + } + } + } + // Parse router_types array (optional) if (json_obj.get("router_types")) |router_types_prop| { if (router_types_prop.data == .e_array) { @@ -165,18 +184,18 @@ fn initFromJSON(self: *Manifest, source: *const logger.Source, log: *logger.Log) } } - // Parse entries array - const entries_prop = json_obj.get("entries") orelse { - try log.addError(&json_source, json_expr.loc, "manifest.json must have an 'entries' field"); + // Parse routes array + const routes_prop = json_obj.get("routes") orelse { + try log.addError(&json_source, json_expr.loc, "manifest.json must have an 'routes' field"); return error.InvalidManifest; }; - if (entries_prop.data != .e_array) { - try log.addError(&json_source, entries_prop.loc, "manifest.json entries must be an array"); + if (routes_prop.data != .e_array) { + try log.addError(&json_source, routes_prop.loc, "manifest.json routes must be an array"); return error.InvalidManifest; } - const entries = entries_prop.data.e_array.items.slice(); + const entries = routes_prop.data.e_array.items.slice(); // Group entries by route_index var route_map = std.AutoHashMap(u32, std.ArrayList(RawManifestEntry)).init(temp_allocator); diff --git a/src/bake/prod/ProductionServerMethods.zig b/src/bake/prod/ProductionServerMethods.zig index 37709ba2f7..0b48386ff8 100644 --- a/src/bake/prod/ProductionServerMethods.zig +++ b/src/bake/prod/ProductionServerMethods.zig @@ -231,10 +231,7 @@ pub fn ProductionServerMethods(protocol_enum: bun.api.server.Protocol, developme pub fn setBakeManifestRoutes(server: *Server, app: *Server.App, manifest: *bun.bake.Manifest) void { // Add route handler for /_bun/* static chunk files - // FIXME: this is being done dynamically. Either put the _bun/* - // files in the manifest or read the directory and make the routes - // up front - app.get("/_bun/*", *Server, server, bakeStaticChunkRequestHandler); + setStaticRoutes(server, app, manifest); // First, we need to serve the client entrypoint files // These are shared across all SSG routes of the same type @@ -275,6 +272,64 @@ pub fn ProductionServerMethods(protocol_enum: bun.api.server.Protocol, developme } } + pub fn setStaticRoutes(server: *Server, app: *Server.App, manifest: *bun.bake.Manifest) void { + const assets = manifest.assets; + + const pathbuf = bun.path_buffer_pool.get(); + defer bun.path_buffer_pool.put(pathbuf); + + for (assets) |asset_path| { + bun.assert(bun.strings.hasPrefixComptime(asset_path, "/_bun/")); + const file = bun.strings.trimPrefixComptime(u8, asset_path, "/_bun/"); + + const file_path_copy = bun.default_allocator.dupe(u8, bun.path.joinStringBuf( + pathbuf, + &[_][]const u8{ manifest.build_output_dir, file }, + .auto, + )) catch |e| bun.handleOom(e); + + // Determine MIME type based on file extension + const mime_type = if (std.mem.endsWith(u8, asset_path, ".js")) + bun.http.MimeType.javascript + else if (std.mem.endsWith(u8, asset_path, ".css")) + bun.http.MimeType.css + else if (std.mem.endsWith(u8, asset_path, ".map")) + bun.http.MimeType.json + else + bun.http.MimeType.other; + + // Create a file blob for the static chunk + const store = jsc.WebCore.Blob.Store.initFile( + .{ .path = .{ .string = bun.PathString.init(file_path_copy) } }, + mime_type, + bun.default_allocator, + ) catch |e| bun.handleOom(e); + + const blob = jsc.WebCore.Blob{ + .size = jsc.WebCore.Blob.max_size, + .store = store, + .content_type = mime_type.value, + .globalThis = server.globalThis, + }; + + // Create a file route and serve it + const any_server = AnyServer.from(server); + const file_route = FileRoute.initFromBlob(blob, .{ + .server = any_server, + .status_code = 200, + }); + ServerConfig.applyStaticRoute( + any_server, + Server.ssl_enabled, + app, + *FileRoute, + file_route, + asset_path, + .{ .method = bun.http.Method.Set.init(.{ .GET = true }) }, + ); + } + } + pub fn bakeStaticChunkRequestHandler(server: *ThisServer, req: *uws.Request, resp: *App.Response) void { const manifest = server.bake_prod.get().?.manifest; diff --git a/src/bake/production.zig b/src/bake/production.zig index 55d7278cfb..3d4ffcd85a 100644 --- a/src/bake/production.zig +++ b/src/bake/production.zig @@ -758,11 +758,30 @@ pub fn buildWithVm(ctx: bun.cli.Command.Context, cwd: []const u8, vm: *VirtualMa // styles: string[][] route_style_references, ); + + const path_buffer = bun.path_buffer_pool.get(); + defer bun.path_buffer_pool.put(path_buffer); + render_promise.setHandled(vm.jsc_vm); vm.waitForPromise(.{ .normal = render_promise }); switch (render_promise.unwrap(vm.jsc_vm, .mark_handled)) { .pending => unreachable, .fulfilled => |manifest_value| { + // Add assets field to manifest with all client-side files in _bun directory + // First, count how many client assets we have + const asset_count: usize = bundled_outputs.len; + + // Create assets array and directly add filenames + const assets_js = try JSValue.createEmptyArray(global, asset_count); + for (bundled_outputs, 0..) |file, asset_index| { + const str = bun.path.joinStringBuf(path_buffer, [_][]const u8{ "/", file.dest_path }, .posix); + bun.assert(bun.strings.hasPrefixComptime(str, "/_bun/")); + var bunstr = bun.String.init(str); + try assets_js.putIndex(global, @intCast(asset_index), bunstr.transferToJS(global)); + } + + _ = manifest_value.put(global, "assets", assets_js); + // Write manifest to file const manifest_path = try std.fs.path.join(allocator, &.{ root_dir_path, "manifest.json" }); defer allocator.free(manifest_path); diff --git a/src/bun.js/api/server/ServerConfig.zig b/src/bun.js/api/server/ServerConfig.zig index 36ab5d0098..4afc51cc0f 100644 --- a/src/bun.js/api/server/ServerConfig.zig +++ b/src/bun.js/api/server/ServerConfig.zig @@ -850,6 +850,9 @@ pub fn fromJS( var log = bun.logger.Log.init(bun.default_allocator); defer log.deinit(); + var arena = std.heap.ArenaAllocator.init(bun.default_allocator); + errdefer arena.deinit(); + var types = try std.ArrayListUnmanaged(bun.bake.FrameworkRouter.Type).initCapacity( bun.default_allocator, args.bake.?.framework.file_system_router_types.len, @@ -865,7 +868,7 @@ pub fn fromJS( const entry = transpiler.resolver.readDirInfoIgnoreError(joined_root) orelse continue; - try types.append(bun.default_allocator, .{ + types.appendAssumeCapacity(.{ .abs_root = bun.strings.withoutTrailingSlash(entry.abs_path), .prefix = fsr.prefix, .ignore_underscores = fsr.ignore_underscores, @@ -883,14 +886,22 @@ pub fn fromJS( }); } - var router: ?bun.ptr.Owned(*bun.bake.FrameworkRouter) = bun.ptr.Owned(*bun.bake.FrameworkRouter).alloc(try bun.bake.FrameworkRouter.initEmpty(root, types.items, bun.default_allocator)) catch bun.outOfMemory(); + var router: ?bun.ptr.Owned(*bun.bake.FrameworkRouter) = bun.ptr.Owned(*bun.bake.FrameworkRouter).alloc(try bun.bake.FrameworkRouter.initEmpty(root, types: { + const ret = types.items; + types = .{}; + break :types ret; + }, bun.default_allocator)) catch bun.outOfMemory(); errdefer if (router) |*r| { r.get().deinit(bun.default_allocator); r.deinitShallow(); }; var manifest = bun.bake.Manifest{ - .arena = std.heap.ArenaAllocator.init(bun.default_allocator), + .arena = arena: { + const ret = arena; + arena = std.heap.ArenaAllocator.init(bun.default_allocator); + break :arena ret; + }, .router = bun.take(&router).?, }; errdefer manifest.deinit(); diff --git a/src/js/builtins/Bake.ts b/src/js/builtins/Bake.ts index 80e6562dbb..581fd6d08c 100644 --- a/src/js/builtins/Bake.ts +++ b/src/js/builtins/Bake.ts @@ -169,8 +169,10 @@ export async function renderRoutesForProdStatic( type Manifest = { version: string; - entries: ManifestEntry[]; + routes: ManifestEntry[]; + router_types: Array<{ server_entrypoint: string | null }>; server_runtime?: string; + assets: Array; }; let entries: ManifestEntry[] = []; @@ -279,7 +281,7 @@ export async function renderRoutesForProdStatic( ); // Build the router_types array (make server_entrypoint relative to _bun folder) - const routerTypes = []; + const routerTypes: Array<{ server_entrypoint: string | null }> = []; for (let i = 0; i < routerTypeServerEntrypoints.length; i++) { const serverEntrypoint = routerTypeServerEntrypoints[i]; if (serverEntrypoint) { @@ -298,8 +300,9 @@ export async function renderRoutesForProdStatic( const manifest: Manifest = { version: "0.0.1", - entries: entries, + routes: entries, router_types: routerTypes, + assets: [], }; return manifest;