diff --git a/src/bake/DevServer.zig b/src/bake/DevServer.zig index 78aec7e11e6670..c536c79ebb1f72 100644 --- a/src/bake/DevServer.zig +++ b/src/bake/DevServer.zig @@ -338,7 +338,7 @@ pub fn init(options: Options) bun.JSOOM!*DevServer { for (dev.framework.file_system_router_types) |fsr| { bun.writeAnyToHasher(&hash, fsr.allow_layouts); bun.writeAnyToHasher(&hash, fsr.ignore_underscores); - hash.update(fsr.entry_server); + hash.update(fsr.entry_server orelse ""); hash.update(&.{0}); hash.update(fsr.entry_client orelse ""); hash.update(&.{0}); @@ -402,18 +402,20 @@ pub fn init(options: Options) bun.JSOOM!*DevServer { dev.initServerRuntime(); + var has_client_routes = false; + var has_server_routes = false; + // Initialize FrameworkRouter dev.router = router: { var types = try std.ArrayListUnmanaged(FrameworkRouter.Type).initCapacity(allocator, options.framework.file_system_router_types.len); errdefer types.deinit(allocator); for (options.framework.file_system_router_types, 0..) |fsr, i| { + _ = i; // autofix const joined_root = bun.path.joinAbs(dev.root, .auto, fsr.root); const entry = dev.server_bundler.resolver.readDirInfoIgnoreError(joined_root) orelse continue; - const server_file = try dev.server_graph.insertStaleExtra(fsr.entry_server, false, true); - try types.append(allocator, .{ .abs_root = bun.strings.withoutTrailingSlash(entry.abs_path), .prefix = fsr.prefix, @@ -422,17 +424,28 @@ pub fn init(options: Options) bun.JSOOM!*DevServer { .extensions = fsr.extensions, .style = fsr.style, .allow_layouts = fsr.allow_layouts, - .server_file = toOpaqueFileId(.server, server_file), - .client_file = if (fsr.entry_client) |client| - toOpaqueFileId(.client, try dev.client_graph.insertStale(client, false)).toOptional() - else - .none, - .server_file_string = .{}, - }); + .server_file = brk: { + if (fsr.entry_server) |server| { + const server_entry = try dev.server_graph.insertStaleExtra(server, false, true); + const server_file = toOpaqueFileId(.server, server_entry).toOptional(); + has_server_routes = true; + break :brk server_file; + } - try dev.route_lookup.put(allocator, server_file, .{ - .route_index = FrameworkRouter.Route.Index.init(@intCast(i)), - .should_recurse_when_visiting = true, + break :brk .none; + }, + .client_file = brk: { + const is_route = fsr.entry_server == null; + if (fsr.entry_client) |client| { + const client_entry = try dev.client_graph.insertStaleExtra(client, false, is_route); + const client_file = toOpaqueFileId(.client, client_entry).toOptional(); + has_client_routes = true; + break :brk client_file; + } + + break :brk .none; + }, + .server_file_string = .{}, }); } @@ -442,7 +455,7 @@ pub fn init(options: Options) bun.JSOOM!*DevServer { // TODO: move scanning to be one tick after server startup. this way the // line saying the server is ready shows quicker, and route errors show up // after that line. - try dev.scanInitialRoutes(); + try dev.scanInitialRoutes(if (has_server_routes) .server else .client); if (bun.FeatureFlags.bake_debugging_features and dev.has_pre_crash_handler) try bun.crash_handler.appendPreCrashHandler(DevServer, dev, dumpStateDueToCrash); @@ -476,10 +489,13 @@ fn initServerRuntime(dev: *DevServer) void { } /// Deferred one tick so that the server can be up faster -fn scanInitialRoutes(dev: *DevServer) !void { +fn scanInitialRoutes(dev: *DevServer, side: bake.Side) !void { try dev.router.scanAll( dev.allocator, - &dev.server_bundler.resolver, + switch (side) { + .server => &dev.server_bundler.resolver, + .client => &dev.client_bundler.resolver, + }, FrameworkRouter.InsertionContext.wrap(DevServer, dev), ); @@ -760,12 +776,15 @@ fn onRequestWithBundle( .{ // routerTypeMain router_type.server_file_string.get() orelse str: { - const name = dev.server_graph.bundled_files.keys()[fromOpaqueFileId(.server, router_type.server_file).get()]; - const str = bun.String.createUTF8(dev.relativePath(name)); - defer str.deref(); - const js = str.toJS(dev.vm.global); - router_type.server_file_string = JSC.Strong.create(js, dev.vm.global); - break :str js; + if (router_type.server_file.unwrap()) |id| { + const name = dev.server_graph.bundled_files.keys()[fromOpaqueFileId(.server, id).get()]; + const str = bun.String.createUTF8(dev.relativePath(name)); + defer str.deref(); + const js = str.toJS(dev.vm.global); + router_type.server_file_string = JSC.Strong.create(js, dev.vm.global); + break :str js; + } + break :str bun.String.empty.toJS(dev.vm.global); }, // routeModules route_bundle.cached_module_list.get() orelse arr: { @@ -1064,7 +1083,10 @@ fn traceAllRouteImports(dev: *DevServer, route_bundle: *RouteBundle, gts: *Graph const router_type = dev.router.typePtr(route.type); // Both framework entry points are considered - try dev.server_graph.traceImports(fromOpaqueFileId(.server, router_type.server_file), gts, .{ .find_css = true }); + if (router_type.server_file.unwrap()) |id| { + try dev.server_graph.traceImports(fromOpaqueFileId(.server, id), gts, .{ .find_css = true }); + } + if (router_type.client_file.unwrap()) |id| { try dev.client_graph.traceImports(fromOpaqueFileId(.client, id), gts, goal); } @@ -1950,6 +1972,10 @@ pub fn IncrementalGraph(side: bake.Side) type { fn fileKind(file: @This()) FileKind { return file.kind; } + + pub fn isRoute(file: @This()) bool { + return file.is_route; + } }, .client => struct { /// Allocated by default_allocator. Access with `.code()` @@ -1959,7 +1985,7 @@ pub fn IncrementalGraph(side: bake.Side) type { code_len: u32, flags: Flags, - const Flags = struct { + const Flags = packed struct { /// If the file has an error, the failure can be looked up /// in the `.failures` map. failed: bool, @@ -1971,6 +1997,10 @@ pub fn IncrementalGraph(side: bake.Side) type { is_special_framework_file: bool, /// CSS and Asset files get special handling kind: FileKind, + + /// If this file is a route root, the route can be looked up in + /// the route list. This also stops dependency propagation. + is_route: bool, }; comptime { @@ -1986,6 +2016,10 @@ pub fn IncrementalGraph(side: bake.Side) type { }; } + pub fn isRoute(file: *const @This()) bool { + return file.flags.is_route; + } + fn code(file: @This()) []const u8 { return file.code_ptr[0..file.code_len]; } @@ -2123,6 +2157,7 @@ pub fn IncrementalGraph(side: bake.Side) type { .is_hmr_root = ctx.server_to_client_bitset.isSet(index.get()), .is_special_framework_file = false, .kind = kind, + .is_route = false, }; if (kind == .css) { if (!gop.found_existing or gop.value_ptr.code_len == 0) { @@ -2397,7 +2432,7 @@ pub fn IncrementalGraph(side: bake.Side) type { switch (side) { .server => { const dev = g.owner(); - if (file.is_route) { + if (file.isRoute()) { const route_index = dev.route_lookup.get(file_index) orelse Output.panic("Route not in lookup index: {d} {}", .{ file_index.get(), bun.fmt.quote(g.bundled_files.keys()[file_index.get()]) }); igLog("\\<- Route", .{}); @@ -2409,7 +2444,13 @@ pub fn IncrementalGraph(side: bake.Side) type { } }, .client => { - if (file.flags.is_hmr_root or (file.flags.kind == .css and trace_kind == .css_to_route)) { + if (file.isRoute()) { + const dev = g.owner(); + const key = g.bundled_files.keys()[file_index.get()]; + const index = dev.client_graph.getFileIndex(key) orelse + Output.panic("Client Incremental Graph is missing component for {}", .{bun.fmt.quote(key)}); + try dev.client_graph.traceDependencies(index, gts, trace_kind); + } else if (file.flags.is_hmr_root or (file.flags.kind == .css and trace_kind == .css_to_route)) { const dev = g.owner(); const key = g.bundled_files.keys()[file_index.get()]; const index = dev.server_graph.getFileIndex(key) orelse @@ -2517,6 +2558,10 @@ pub fn IncrementalGraph(side: bake.Side) type { } else { if (side == .server) { if (is_route) gop.value_ptr.*.is_route = is_route; + } else if (side == .client) { + if (is_route) { + gop.value_ptr.*.flags.is_route = true; + } } } @@ -2531,6 +2576,7 @@ pub fn IncrementalGraph(side: bake.Side) type { .is_hmr_root = false, .is_special_framework_file = false, .kind = .unknown, + .is_route = is_route, }); }, .server => { @@ -2614,6 +2660,7 @@ pub fn IncrementalGraph(side: bake.Side) type { .is_hmr_root = false, .is_special_framework_file = false, .kind = .unknown, + .is_route = false, }); }, .server => { diff --git a/src/bake/FrameworkRouter.zig b/src/bake/FrameworkRouter.zig index bab239a9f69e29..24b22524cbe46a 100644 --- a/src/bake/FrameworkRouter.zig +++ b/src/bake/FrameworkRouter.zig @@ -89,7 +89,7 @@ pub const Type = struct { /// `FrameworkRouter` itself does not use this value. client_file: OpaqueFileId.Optional, /// `FrameworkRouter` itself does not use this value. - server_file: OpaqueFileId, + server_file: OpaqueFileId.Optional, /// `FrameworkRouter` itself does not use this value. server_file_string: JSC.Strong, diff --git a/src/bake/bake.zig b/src/bake/bake.zig index eead195a75e82d..f04ae4ba78db57 100644 --- a/src/bake/bake.zig +++ b/src/bake/bake.zig @@ -223,7 +223,7 @@ pub const Framework = struct { pub const FileSystemRouterType = struct { root: []const u8, prefix: []const u8, - entry_server: []const u8, + entry_server: ?[]const u8, entry_client: ?[]const u8, ignore_underscores: bool, ignore_dirs: []const []const u8, @@ -281,7 +281,7 @@ pub const Framework = struct { 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 (fsr.entry_server) |*entry_server| f.resolveHelper(client, entry_server, &had_errors, "server side entrypoint"); } if (had_errors) return error.ModuleNotFound; @@ -443,9 +443,7 @@ pub const Framework = struct { 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 server_entry_point = try getOptionalString(fsr_opts, global, "serverEntryPoint", refs, arena); 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; diff --git a/src/bake/production.zig b/src/bake/production.zig index f9414927d11803..837116c716cb04 100644 --- a/src/bake/production.zig +++ b/src/bake/production.zig @@ -36,17 +36,12 @@ pub fn buildCommand(ctx: bun.CLI.Command.Context) !void { .smol = ctx.runtime_options.smol, }); defer vm.deinit(); - // A special global object is used to allow registering virtual modules - // that bypass Bun's normal module resolver and plugin system. - vm.global = BakeCreateProdGlobal(vm.console); - vm.regular_event_loop.global = vm.global; - vm.jsc = vm.global.vm(); - vm.event_loop.ensureWaker(); - const b = &vm.transpiler; vm.preload = ctx.preloads; vm.argv = ctx.passthrough; vm.arena = &arena; vm.allocator = arena.allocator(); + const b = &vm.transpiler; + b.options.install = ctx.install; b.resolver.opts.install = ctx.install; b.resolver.opts.global_cache = ctx.debug.global_cache; @@ -62,7 +57,24 @@ pub fn buildCommand(ctx: bun.CLI.Command.Context) !void { b.resolver.opts.minify_identifiers = ctx.bundler_options.minify_identifiers; b.resolver.opts.minify_whitespace = ctx.bundler_options.minify_whitespace; b.options.env.behavior = .load_all_without_inlining; + b.configureDefines() catch { + bun.bun_js.failWithBuildError(vm); + }; + JSC.VirtualMachine.is_main_thread_vm = true; + + vm.event_loop.ensureWaker(); + + bun.http.AsyncHTTP.loadEnv(vm.allocator, vm.log, b.env); + vm.loadExtraEnvAndSourceCodePrinter(); + vm.is_main_thread = true; + + // A special global object is used to allow registering virtual modules + // that bypass Bun's normal module resolver and plugin system. + vm.global = BakeCreateProdGlobal(vm.console); + vm.regular_event_loop.global = vm.global; + vm.jsc = vm.global.vm(); vm.event_loop.ensureWaker(); + switch (ctx.debug.macros) { .disable => { b.options.no_macros = true; @@ -72,13 +84,6 @@ pub fn buildCommand(ctx: bun.CLI.Command.Context) !void { }, .unspecified => {}, } - b.configureDefines() catch { - bun.bun_js.failWithBuildError(vm); - }; - bun.http.AsyncHTTP.loadEnv(vm.allocator, vm.log, b.env); - vm.loadExtraEnvAndSourceCodePrinter(); - vm.is_main_thread = true; - JSC.VirtualMachine.is_main_thread_vm = true; const api_lock = vm.jsc.getAPILock(); defer api_lock.release(); @@ -232,7 +237,10 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa .extensions = fsr.extensions, .style = fsr.style, .allow_layouts = fsr.allow_layouts, - .server_file = try entry_points.getOrPutEntryPoint(fsr.entry_server, .server), + .server_file = if (fsr.entry_server) |server| + (try entry_points.getOrPutEntryPoint(server, .server)).toOptional() + else + .none, .client_file = if (fsr.entry_client) |client| (try entry_points.getOrPutEntryPoint(client, .client)).toOptional() else @@ -371,25 +379,10 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa client_entry_urls.putIndex(global, @intCast(i), .null); } - const server_entry_point = try pt.loadBundledModule(router_type.server_file); - const server_render_func = brk: { - const raw = BakeGetOnModuleNamespace(global, server_entry_point, "prerender") orelse - break :brk null; - if (!raw.isCallable(vm.jsc)) { - break :brk null; - } - break :brk raw; - } orelse { - Output.errGeneric("Framework does not support static site generation", .{}); - Output.note("The file {s} is missing the \"prerender\" export, which defines how to generate static files.", .{ - bun.fmt.quote(bun.path.relative(cwd, entry_points.files.keys()[router_type.server_file.get()].absPath())), - }); - bun.Global.crash(); - }; - - const server_param_func = if (router.dynamic_routes.count() > 0) - brk: { - const raw = BakeGetOnModuleNamespace(global, server_entry_point, "getParams") orelse + if (router_type.server_file.unwrap()) |server_file| { + const server_entry_point = try pt.loadBundledModule(server_file); + const server_render_func = brk: { + const raw = BakeGetOnModuleNamespace(global, server_entry_point, "prerender") orelse break :brk null; if (!raw.isCallable(vm.jsc)) { break :brk null; @@ -397,15 +390,32 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa break :brk raw; } orelse { Output.errGeneric("Framework does not support static site generation", .{}); - Output.note("The file {s} is missing the \"getParams\" export, which defines how to generate static files.", .{ - bun.fmt.quote(bun.path.relative(cwd, entry_points.files.keys()[router_type.server_file.get()].absPath())), + Output.note("The file {s} is missing the \"prerender\" export, which defines how to generate static files.", .{ + bun.fmt.quote(bun.path.relative(cwd, entry_points.files.keys()[server_file.get()].absPath())), }); bun.Global.crash(); - } - else - JSValue.null; - server_render_funcs.putIndex(global, @intCast(i), server_render_func); - server_param_funcs.putIndex(global, @intCast(i), server_param_func); + }; + + const server_param_func = if (router.dynamic_routes.count() > 0) + brk: { + const raw = BakeGetOnModuleNamespace(global, server_entry_point, "getParams") orelse + break :brk null; + if (!raw.isCallable(vm.jsc)) { + break :brk null; + } + break :brk raw; + } orelse { + Output.errGeneric("Framework does not support static site generation", .{}); + Output.note("The file {s} is missing the \"getParams\" export, which defines how to generate static files.", .{ + bun.fmt.quote(bun.path.relative(cwd, entry_points.files.keys()[server_file.get()].absPath())), + }); + bun.Global.crash(); + } + else + JSValue.null; + server_render_funcs.putIndex(global, @intCast(i), server_render_func); + server_param_funcs.putIndex(global, @intCast(i), server_param_func); + } } var navigatable_routes = std.ArrayList(FrameworkRouter.Route.Index).init(allocator);