From b94bfc5ca8092c78c87fae394fbfbc8c0a64efc8 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Thu, 3 Jul 2025 13:48:45 -0700 Subject: [PATCH 01/59] document that --- src/js_ast.zig | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/js_ast.zig b/src/js_ast.zig index 11cbcc9ba614..c21561a11af2 100644 --- a/src/js_ast.zig +++ b/src/js_ast.zig @@ -550,6 +550,13 @@ pub const NamedImport = struct { // Parts within this file that use this import local_parts_with_uses: BabyList(u32) = BabyList(u32){}, + // The original export name from the source module being imported. + // Examples: + // - `import { foo } from 'module'` → alias = "foo" + // - `import { foo as bar } from 'module'` → alias = "foo" (original export name) + // - `import * as ns from 'module'` → alias_is_star = true, alias = "" + // This field is used by the bundler to match imports with their corresponding + // exports and for error reporting when imports can't be resolved. alias: ?string, alias_loc: ?logger.Loc = null, namespace_ref: ?Ref, From 5004362713e7e558fe9e4339f400fde296acf270 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Thu, 3 Jul 2025 14:21:23 -0700 Subject: [PATCH 02/59] think that fixed that --- src/js_parser.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js_parser.zig b/src/js_parser.zig index dad2a9eb5ed5..8558046f39af 100644 --- a/src/js_parser.zig +++ b/src/js_parser.zig @@ -9017,7 +9017,7 @@ fn NewParser_( }) catch unreachable; } - item_refs.putAssumeCapacity(name, name_loc.*); + item_refs.putAssumeCapacity("default", name_loc.*); } var end: usize = 0; From f3be9b5d6f0e4a927c27ea031a9afb15b9b7c008 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Thu, 3 Jul 2025 15:45:15 -0700 Subject: [PATCH 03/59] Okay that's the fix --- src/js_parser.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js_parser.zig b/src/js_parser.zig index 8558046f39af..1112d467202a 100644 --- a/src/js_parser.zig +++ b/src/js_parser.zig @@ -9017,7 +9017,7 @@ fn NewParser_( }) catch unreachable; } - item_refs.putAssumeCapacity("default", name_loc.*); + // No need to add this to `item_refs` because `.scanForImportsAndExports` special cases `s } var end: usize = 0; From 0b15f986643b9e7aa11358c0ea6422c045ded43e Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Thu, 3 Jul 2025 15:45:24 -0700 Subject: [PATCH 04/59] Document --- src/js_ast.zig | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/js_ast.zig b/src/js_ast.zig index c21561a11af2..1a006e29605a 100644 --- a/src/js_ast.zig +++ b/src/js_ast.zig @@ -116,8 +116,16 @@ pub const Flags = struct { }; pub const ClauseItem = struct { + /// The local alias used for the imported/exported symbol in the current module. + /// For imports: `import { foo as bar }` - "bar" is the alias + /// For exports: `export { foo as bar }` - "bar" is the alias + /// For re-exports: `export { foo as bar } from 'path'` - "bar" is the alias alias: string, alias_loc: logger.Loc = logger.Loc.Empty, + /// Reference to the actual symbol being imported/exported. + /// For imports: `import { foo as bar }` - ref to the symbol representing "foo" from the source module + /// For exports: `export { foo as bar }` - ref to the local symbol "foo" + /// For re-exports: `export { foo as bar } from 'path'` - ref to an intermediate symbol name: LocRef, /// This is the original name of the symbol stored in "Name". It's needed for From 33d60b671a55b6fb214e9e035095288972e0e2d3 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Thu, 3 Jul 2025 16:16:22 -0700 Subject: [PATCH 05/59] Test the named import bug --- test/bake/dev/ssg-pages-router.test.ts | 31 ++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/test/bake/dev/ssg-pages-router.test.ts b/test/bake/dev/ssg-pages-router.test.ts index af60b9e88be5..ff592b4d680a 100644 --- a/test/bake/dev/ssg-pages-router.test.ts +++ b/test/bake/dev/ssg-pages-router.test.ts @@ -293,3 +293,34 @@ devTest("SSG pages router - file loading with Bun.file", { expect(await c2.elemText("div div")).toBe("This is the second post content"); }, }); + +devTest("SSG pages router - named import edge case", { + framework: "react", + fixture: "ssg-pages-router", + files: { + "pages/index.tsx": ` + import Markdoc, * as md from '../src/ooga' + + console.log(md); + + export default function IndexPage() { + return

Welcome to SSG

; + } + `, + "src/ooga.ts": `var Markdoc = function () { + return { + parse: () => {}, + transform: () => {}, + }; +}; + +export { Markdoc as default };`, + "posts/hello-world.txt": "This is the content of hello world post", + "posts/second-post.txt": "This is the second post content", + }, + async test(dev) { + // Should not error + await using c1 = await dev.client("/"); + expect(await c1.elemText("h1")).toBe("Welcome to SSG"); + }, +}); From 6347c7f843f1c2bc9ce854bdabb630b7c9fbb558 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Thu, 3 Jul 2025 16:17:12 -0700 Subject: [PATCH 06/59] Support catch-all routes --- src/bake/bake.d.ts | 8 ++++---- src/bake/production.zig | 10 ++++++++-- src/js/builtins/Bake.ts | 26 +++++++++++++++++++------- test/bake/bake-harness.ts | 2 +- 4 files changed, 32 insertions(+), 14 deletions(-) diff --git a/src/bake/bake.d.ts b/src/bake/bake.d.ts index af40b0cd7fb6..06189b6bd3da 100644 --- a/src/bake/bake.d.ts +++ b/src/bake/bake.d.ts @@ -418,9 +418,9 @@ declare module "bun" { } type GetParamIterator = - | AsyncIterable, GetParamsFinalOpts> - | Iterable, GetParamsFinalOpts> - | ({ pages: Array> } & GetParamsFinalOpts); + | AsyncIterable, GetParamsFinalOpts> + | Iterable, GetParamsFinalOpts> + | ({ pages: Array> } & GetParamsFinalOpts); type GetParamsFinalOpts = void | null | { /** @@ -478,7 +478,7 @@ declare module "bun" { */ readonly layouts: ReadonlyArray; /** Received route params. `null` if the route does not take params */ - readonly params: null | Record; + readonly params: null | Record; /** * A list of js files that the route will need to be interactive. */ diff --git a/src/bake/production.zig b/src/bake/production.zig index 7864940b660f..0807153cdec7 100644 --- a/src/bake/production.zig +++ b/src/bake/production.zig @@ -444,7 +444,10 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa .param => { params_buf.append(ctx.allocator, route.part.param) catch unreachable; }, - .catch_all, .catch_all_optional => { + .catch_all => { + params_buf.append(ctx.allocator, route.part.catch_all) catch unreachable; + }, + .catch_all_optional => { return global.throw("catch-all routes are not supported in static site generation", .{}); }, else => {}, @@ -463,7 +466,10 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa .param => { params_buf.append(ctx.allocator, parent.part.param) catch unreachable; }, - .catch_all, .catch_all_optional => { + .catch_all => { + params_buf.append(ctx.allocator, parent.part.catch_all) catch unreachable; + }, + .catch_all_optional => { return global.throw("catch-all routes are not supported in static site generation", .{}); }, else => {}, diff --git a/src/js/builtins/Bake.ts b/src/js/builtins/Bake.ts index bfe3cdfcb448..e083dcaf669a 100644 --- a/src/js/builtins/Bake.ts +++ b/src/js/builtins/Bake.ts @@ -40,6 +40,8 @@ export function renderRoutesForProdStatic( }); const { join: pathJoin } = require("node:path"); + const regex = /:(\w+)/g; + let loadedModules = new Array(allServerFiles.length); async function doGenerateRoute( @@ -47,7 +49,7 @@ export function renderRoutesForProdStatic( i: number, layouts: any[], pageModule: any, - params: Record | null, + params: Record | null, ) { // Call the framework's rendering function const callback = renderStatic[type]; @@ -77,10 +79,20 @@ export function renderRoutesForProdStatic( Object.entries(files).map(([key, value]) => { if (params != null) { $assert(patterns[i].includes(`:`)); - // replace the :paramName part of patterns[i] with the value of params[paramName] - // use a regex in replace with a callback - const newKey = patterns[i].replace(/:(\w+)/g, (_, p1) => params[p1]); - return Bun.write(pathJoin(outBase, newKey + key), value); + const matches = regex.exec(patterns[i]); + const [_, p1] = matches!; + if (typeof params[p1] === "string") { + // replace the :paramName part of patterns[i] with the value of params[paramName] + // use a regex in replace with a callback + const newKey = params[p1]; + return Bun.write(pathJoin(outBase, newKey + key), value); + } + if (Array.isArray(params[p1])) { + return Bun.write(pathJoin(outBase, ...params[p1], key), value); + } + throw new Error( + `Route ${JSON.stringify(sourceRouteFiles[i])} has a param that is not a string or array of strings: ${p1}`, + ); } return Bun.write(pathJoin(outBase, patterns[i] + key), value); }), @@ -92,10 +104,10 @@ export function renderRoutesForProdStatic( i: number, layouts: any[], pageModule: any, - params: Record, + params: Record, ) { for (const param of paramInformation[i]!) { - if (!params[param]) { + if (params[param] === undefined) { throw new Error(`Missing param ${param} for route ${JSON.stringify(sourceRouteFiles[i])}`); } } diff --git a/test/bake/bake-harness.ts b/test/bake/bake-harness.ts index 9d71c8c01a1b..da6959bd225c 100644 --- a/test/bake/bake-harness.ts +++ b/test/bake/bake-harness.ts @@ -314,7 +314,7 @@ export class Dev extends EventEmitter { if (wantsHmrEvent && interactive) { await seenFiles.promise; } else if (wantsHmrEvent) { - await Promise.race([seenFiles.promise, Bun.sleep(1000)]); + await Promise.race([seenFiles.promise]); } if (!fastBatches) { // Wait an extra delay to avoid double-triggering events. From 3881ee1d6140627c03db308bd8adcd6ea2bf317a Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Thu, 3 Jul 2025 19:12:18 -0700 Subject: [PATCH 07/59] kinda working sourcemap but with bug --- src/api/schema.zig | 21 +++++++++++++++++++++ src/bake/bake.zig | 25 +++++++++++++++++++++++-- src/bake/production.zig | 14 ++++++++++---- 3 files changed, 54 insertions(+), 6 deletions(-) diff --git a/src/api/schema.zig b/src/api/schema.zig index 82e5bb37b257..b2b5bc5b3551 100644 --- a/src/api/schema.zig +++ b/src/api/schema.zig @@ -1956,6 +1956,27 @@ pub const Api = struct { _, + pub fn fromJS(global: *bun.JSC.JSGlobalObject, value: bun.JSC.JSValue) bun.JSError!?SourceMapMode { + if (value.isString()) { + const str = try value.toSliceOrNull(global); + defer str.deinit(); + const utf8 = str.slice(); + if (bun.strings.eqlComptime(utf8, "none")) { + return .none; + } + if (bun.strings.eqlComptime(utf8, "inline")) { + return .@"inline"; + } + if (bun.strings.eqlComptime(utf8, "external")) { + return .external; + } + if (bun.strings.eqlComptime(utf8, "linked")) { + return .linked; + } + } + return null; + } + pub fn jsonStringify(self: @This(), writer: anytype) !void { return try writer.write(@tagName(self)); } diff --git a/src/bake/bake.zig b/src/bake/bake.zig index 40c96f361a4a..41646d0cc0b4 100644 --- a/src/bake/bake.zig +++ b/src/bake/bake.zig @@ -155,6 +155,20 @@ const BuildConfigSubset = struct { 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, + + pub fn fromJS(global: *JSC.JSGlobalObject, js_options: JSValue) bun.JSError!BuildConfigSubset { + var options = BuildConfigSubset{}; + + if (try js_options.getOptional(global, "sourcemap", JSValue)) |val| { + if (try bun.Schema.Api.SourceMapMode.fromJS(global, val)) |sourcemap| { + options.source_map = sourcemap; + } + return bun.JSC.Node.validators.throwErrInvalidArgType(global, "sourcemap", .{}, "string", val); + } + + return options; + } }; /// A "Framework" in our eyes is simply set of bundler options that a framework @@ -578,8 +592,15 @@ pub const Framework = struct { } if (try opts.getOptional(global, "bundlerOptions", JSValue)) |js_options| { - _ = js_options; // TODO: - // try bundler_options.parseInto(global, js_options, .root); + 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); + } } return framework; diff --git a/src/bake/production.zig b/src/bake/production.zig index 0807153cdec7..bc630958d38d 100644 --- a/src/bake/production.zig +++ b/src/bake/production.zig @@ -174,10 +174,10 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa var client_transpiler: bun.transpiler.Transpiler = undefined; var server_transpiler: bun.transpiler.Transpiler = undefined; var ssr_transpiler: bun.transpiler.Transpiler = undefined; - try framework.initTranspilerWithSourceMap(allocator, vm.log, .production_static, .server, &server_transpiler, &options.bundler_options.server, .@"inline"); - try framework.initTranspilerWithSourceMap(allocator, vm.log, .production_static, .client, &client_transpiler, &options.bundler_options.client, .@"inline"); + try framework.initTranspilerWithSourceMap(allocator, vm.log, .production_static, .server, &server_transpiler, &options.bundler_options.server, bun.options.SourceMapOption.fromApi(options.bundler_options.server.source_map)); + try framework.initTranspilerWithSourceMap(allocator, vm.log, .production_static, .client, &client_transpiler, &options.bundler_options.client, bun.options.SourceMapOption.fromApi(options.bundler_options.client.source_map)); if (separate_ssr_graph) { - try framework.initTranspilerWithSourceMap(allocator, vm.log, .production_static, .ssr, &ssr_transpiler, &options.bundler_options.ssr, .@"inline"); + try framework.initTranspilerWithSourceMap(allocator, vm.log, .production_static, .ssr, &ssr_transpiler, &options.bundler_options.ssr, bun.options.SourceMapOption.fromApi(options.bundler_options.ssr.source_map)); } if (ctx.bundler_options.bake_debug_disable_minify) { @@ -287,7 +287,11 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa file.entry_point_index, }); if (file.loader.isCSS()) { - if (css_chunks_count == 0) css_chunks_first = i; + if (css_chunks_count == 0) { + css_chunks_first = i; + } else { + css_chunks_first = @min(css_chunks_first, i); + } css_chunks_count += 1; } @@ -417,6 +421,7 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa const css_chunk_js_strings = try allocator.alloc(JSValue, css_chunks_count); for (bundled_outputs[css_chunks_first..][0..css_chunks_count], css_chunk_js_strings) |output_file, *str| { bun.assert(output_file.dest_path[0] != '.'); + // CSS chunks must be in contiguous order!! bun.assert(output_file.loader.isCSS()); str.* = (try bun.String.createFormat("{s}{s}", .{ public_path, output_file.dest_path })).toJS(global); } @@ -490,6 +495,7 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa css_file_count = 0; file_list.putIndex(global, 0, pt.preloadBundledModule(main_file_route_index)); for (main_file.referenced_css_files) |ref| { + bun.assert(ref.get() >= css_chunks_first); styles.putIndex(global, css_file_count, css_chunk_js_strings[ref.get() - css_chunks_first]); css_file_count += 1; } From 163e83fcbb95f8fb25ce0de42edfccfadc469e18 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Fri, 4 Jul 2025 17:22:46 -0700 Subject: [PATCH 08/59] Fix bug --- src/OutputFile.zig | 8 +- src/bake/production.zig | 13 +- src/bundler/LinkerContext.zig | 2 + .../linker_context/OutputFileListBuilder.zig | 133 ++++++++++++++++++ .../generateChunksInParallel.zig | 35 ++--- .../linker_context/writeOutputFilesToDisk.zig | 32 ++--- 6 files changed, 172 insertions(+), 51 deletions(-) create mode 100644 src/bundler/linker_context/OutputFileListBuilder.zig diff --git a/src/OutputFile.zig b/src/OutputFile.zig index 1b58c6ef24a7..62af25a1fc31 100644 --- a/src/OutputFile.zig +++ b/src/OutputFile.zig @@ -20,7 +20,7 @@ side: ?bun.bake.Side, /// This is only set for the JS bundle, and not files associated with an /// entrypoint like sourcemaps and bytecode entry_point_index: ?u32, -referenced_css_files: []const Index = &.{}, +referenced_css_chunks: []const Index = &.{}, source_index: Index.Optional = .none, pub const Index = bun.GenericIndex(u32, OutputFile); @@ -30,7 +30,7 @@ pub fn deinit(this: *OutputFile) void { bun.default_allocator.free(this.src_path.text); bun.default_allocator.free(this.dest_path); - bun.default_allocator.free(this.referenced_css_files); + bun.default_allocator.free(this.referenced_css_chunks); } // Depending on: @@ -206,7 +206,7 @@ pub const Options = struct { }, side: ?bun.bake.Side, entry_point_index: ?u32, - referenced_css_files: []const Index = &.{}, + referenced_css_chunks: []const Index = &.{}, }; pub fn init(options: Options) OutputFile { @@ -240,7 +240,7 @@ pub fn init(options: Options) OutputFile { }, .side = options.side, .entry_point_index = options.entry_point_index, - .referenced_css_files = options.referenced_css_files, + .referenced_css_chunks = options.referenced_css_chunks, }; } diff --git a/src/bake/production.zig b/src/bake/production.zig index bc630958d38d..bea7a3c07821 100644 --- a/src/bake/production.zig +++ b/src/bake/production.zig @@ -458,9 +458,9 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa else => {}, } var file_count: u32 = 1; - var css_file_count: u32 = @intCast(main_file.referenced_css_files.len); + var css_file_count: u32 = @intCast(main_file.referenced_css_chunks.len); if (route.file_layout.unwrap()) |file| { - css_file_count += @intCast(pt.outputFile(file).referenced_css_files.len); + css_file_count += @intCast(pt.outputFile(file).referenced_css_chunks.len); file_count += 1; } var next: ?FrameworkRouter.Route.Index = route.parent.unwrap(); @@ -480,7 +480,7 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa else => {}, } if (parent.file_layout.unwrap()) |file| { - css_file_count += @intCast(pt.outputFile(file).referenced_css_files.len); + css_file_count += @intCast(pt.outputFile(file).referenced_css_chunks.len); file_count += 1; } next = parent.parent.unwrap(); @@ -494,14 +494,15 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa file_count = 1; css_file_count = 0; file_list.putIndex(global, 0, pt.preloadBundledModule(main_file_route_index)); - for (main_file.referenced_css_files) |ref| { + for (main_file.referenced_css_chunks) |ref| { + const file = bundled_outputs[ref.get()]; bun.assert(ref.get() >= css_chunks_first); styles.putIndex(global, css_file_count, css_chunk_js_strings[ref.get() - css_chunks_first]); css_file_count += 1; } if (route.file_layout.unwrap()) |file| { file_list.putIndex(global, file_count, pt.preloadBundledModule(file)); - for (pt.outputFile(file).referenced_css_files) |ref| { + for (pt.outputFile(file).referenced_css_chunks) |ref| { styles.putIndex(global, css_file_count, css_chunk_js_strings[ref.get() - css_chunks_first]); css_file_count += 1; } @@ -512,7 +513,7 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa const parent = router.routePtr(parent_index); if (parent.file_layout.unwrap()) |file| { file_list.putIndex(global, file_count, pt.preloadBundledModule(file)); - for (pt.outputFile(file).referenced_css_files) |ref| { + for (pt.outputFile(file).referenced_css_chunks) |ref| { styles.putIndex(global, css_file_count, css_chunk_js_strings[ref.get() - css_chunks_first]); css_file_count += 1; } diff --git a/src/bundler/LinkerContext.zig b/src/bundler/LinkerContext.zig index 338962862450..4594db6ab9e6 100644 --- a/src/bundler/LinkerContext.zig +++ b/src/bundler/LinkerContext.zig @@ -2,6 +2,8 @@ pub const LinkerContext = struct { pub const debug = Output.scoped(.LinkerCtx, false); pub const CompileResult = bundler.CompileResult; + pub const OutputFileListBuilder = @import("./linker_context/OutputFileListBuilder.zig"); + parse_graph: *Graph = undefined, graph: LinkerGraph = undefined, allocator: std.mem.Allocator = undefined, diff --git a/src/bundler/linker_context/OutputFileListBuilder.zig b/src/bundler/linker_context/OutputFileListBuilder.zig new file mode 100644 index 000000000000..457dcfc96fba --- /dev/null +++ b/src/bundler/linker_context/OutputFileListBuilder.zig @@ -0,0 +1,133 @@ +//! Q: What does this struct do? +//! A: This struct segments the `OutputFile` list into 3 separate spaces so +//! chunk indexing remains the same: +//! +//! 1. chunks +//! 2. sourcemaps and bytecode +//! 3. additional output files +//! +//! We can calculate the space ahead of time and avoid having to do something +//! more complicated or which requires extra work. +//! +//! Q: Why does it need to do that? +//! A: We would like it so if we have a chunk index, we can also index its +//! corresponding output file in the output file list. +//! +//! The DevServer uses the `referenced_css_chunks` (a list of chunk indices) +//! field on `OutputFile` to know which CSS files to hand to the rendering +//! function. For React this just adds tags that point to each output CSS +//! file. +//! +//! However, we previously were pushing sourcemaps and bytecode output files +//! to the output file list directly after their corresponding chunk, meaning +//! the index of the chunk in the chunk list and its corresponding +//! `OutputFile` in the output file list got scrambled. +//! +//! If we maintain the property that `outputIndexForChunk(chunk[i]) == i` +//! then we don't need to do any allocations or extra work to get the output +//! file for a chunk. +pub const OutputFileList = @This(); + +output_files: std.ArrayList(options.OutputFile), +index_for_chunk: u32, +index_for_sourcemaps_and_bytecode: ?u32, +additional_output_files_start: u32, + +pub fn init( + allocator: std.mem.Allocator, + c: *const bun.bundle_v2.LinkerContext, + chunks: []const bun.bundle_v2.Chunk, + _: usize, +) !@This() { + const length, const source_map_and_bytecode_count = OutputFileList.calculateOutputFileListCapacity(c, chunks); + var output_files = try std.ArrayList(options.OutputFile).initCapacity( + allocator, + length, + ); + output_files.items.len = length; + + return .{ + .output_files = output_files, + .index_for_chunk = 0, + .index_for_sourcemaps_and_bytecode = if (source_map_and_bytecode_count == 0) null else @as(u32, @truncate(chunks.len)), + .additional_output_files_start = @as(u32, @intCast(chunks.len)) + source_map_and_bytecode_count, + }; +} + +pub fn take(this: *@This()) std.ArrayList(options.OutputFile) { + const list = this.output_files; + this.output_files = std.ArrayList(options.OutputFile).init(bun.default_allocator); + return list; +} + +pub fn calculateOutputFileListCapacity(c: *const bun.bundle_v2.LinkerContext, chunks: []const bun.bundle_v2.Chunk) struct { u32, u32 } { + const source_map_count = if (c.options.source_maps.hasExternalFiles()) chunks.len else 0; + const bytecode_count = if (c.options.generate_bytecode_cache) bytecode_count: { + var bytecode_count: usize = 0; + for (chunks) |*chunk| { + // TODO: this was the original logic, but it seems like it is + // incorrect / does unnecessary work? Leaving it here just in-case, + // as it moved from a different file and is not git blame-able. + // + // const loader: Loader = if (chunk.entry_point.is_entry_point) + // c.parse_graph.input_files.items(.loader)[ + // chunk.entry_point.source_index + // ] + // else + // .js; + // if (loader.isJavaScriptLike()) { + // bytecode_count += 1; + // } + + if (chunk.content == .javascript) { + bytecode_count += 1; + } + } + break :bytecode_count bytecode_count; + } else 0; + + return .{ @intCast(chunks.len + source_map_count + bytecode_count + c.parse_graph.additional_output_files.items.len), @intCast(source_map_count + bytecode_count) }; +} + +pub fn insertForChunk(this: *OutputFileList, output_file: options.OutputFile) u32 { + const index = this.indexForChunk(); + bun.assertf(index < this.index_for_sourcemaps_and_bytecode orelse std.math.maxInt(u32), "index ({d}) \\< index_for_sourcemaps_and_bytecode ({d})", .{ index, this.index_for_sourcemaps_and_bytecode orelse std.math.maxInt(u32) }); + this.output_files.items[index] = output_file; + return index; +} + +pub fn insertForSourcemapOrBytecode(this: *OutputFileList, output_file: options.OutputFile) !u32 { + const index = this.indexForSourcemapOrBytecode() orelse return error.NoSourceMapsOrBytecode; + bun.assertf(index < this.additional_output_files_start, "index ({d}) \\< additional_output_files_start ({d})", .{ index, this.additional_output_files_start }); + this.output_files.items[index] = output_file; + return index; +} + +pub fn insertAdditionalOutputFiles(this: *OutputFileList, additional_output_files: []const options.OutputFile) void { + bun.assertf(this.index_for_sourcemaps_and_bytecode orelse std.math.maxInt(u32) < this.additional_output_files_start, "index_for_sourcemaps_and_bytecode ({d}) \\< additional_output_files_start ({d})", .{ this.index_for_sourcemaps_and_bytecode orelse std.math.maxInt(u32), this.additional_output_files_start }); + bun.copy( + options.OutputFile, + this.getMutableAdditionalOutputFiles(), + additional_output_files, + ); +} + +pub fn getMutableAdditionalOutputFiles(this: *OutputFileList) []options.OutputFile { + return this.output_files.items[this.additional_output_files_start..]; +} + +fn indexForChunk(this: *@This()) u32 { + const result = this.index_for_chunk; + this.index_for_chunk += 1; + return result; +} + +fn indexForSourcemapOrBytecode(this: *@This()) ?u32 { + const result = this.index_for_sourcemaps_and_bytecode orelse return null; + this.index_for_sourcemaps_and_bytecode.? += 1; + return result; +} + +const std = @import("std"); +const bun = @import("bun"); +const options = bun.options; diff --git a/src/bundler/linker_context/generateChunksInParallel.zig b/src/bundler/linker_context/generateChunksInParallel.zig index e09ce74aea63..0e85627b8f3c 100644 --- a/src/bundler/linker_context/generateChunksInParallel.zig +++ b/src/bundler/linker_context/generateChunksInParallel.zig @@ -326,11 +326,7 @@ pub fn generateChunksInParallel(c: *LinkerContext, chunks: []Chunk, comptime is_ } } - var output_files = std.ArrayList(options.OutputFile).initCapacity( - bun.default_allocator, - (if (c.options.source_maps.hasExternalFiles()) chunks.len * 2 else chunks.len) + - @as(usize, c.parse_graph.additional_output_files.items.len), - ) catch unreachable; + var output_files = try OutputFileListBuilder.init(bun.default_allocator, c, chunks, c.parse_graph.additional_output_files.items.len); const root_path = c.resolver.opts.output_dir; const more_than_one_output = c.parse_graph.additional_output_files.items.len > 0 or c.options.generate_bytecode_cache or (has_css_chunk and has_js_chunk) or (has_html_chunk and (has_js_chunk or has_css_chunk)); @@ -346,7 +342,7 @@ pub fn generateChunksInParallel(c: *LinkerContext, chunks: []Chunk, comptime is_ try c.writeOutputFilesToDisk(root_path, chunks, &output_files); } else { // In-memory build - for (chunks) |*chunk| { + for (chunks, 0..) |*chunk, chunk_index_in_chunks_list| { var display_size: usize = 0; const public_path = if (chunk.is_browser_chunk_from_server_build) @@ -495,14 +491,12 @@ pub fn generateChunksInParallel(c: *LinkerContext, chunks: []Chunk, comptime is_ }; const source_map_index: ?u32 = if (sourcemap_output_file != null) - @as(u32, @truncate(output_files.items.len + 1)) + try output_files.insertForSourcemapOrBytecode(sourcemap_output_file.?) else null; - const bytecode_index: ?u32 = if (bytecode_output_file != null and source_map_index != null) - @as(u32, @truncate(output_files.items.len + 2)) - else if (bytecode_output_file != null) - @as(u32, @truncate(output_files.items.len + 1)) + const bytecode_index: ?u32 = if (bytecode_output_file != null) + try output_files.insertForSourcemapOrBytecode(bytecode_output_file.?) else null; @@ -512,7 +506,8 @@ pub fn generateChunksInParallel(c: *LinkerContext, chunks: []Chunk, comptime is_ c.graph.files.items(.entry_point_kind)[chunk.entry_point.source_index].outputKind() else .chunk; - try output_files.append(options.OutputFile.init(.{ + + const chunk_index = output_files.insertForChunk(options.OutputFile.init(.{ .data = .{ .buffer = .{ .data = code_result.buffer, @@ -539,24 +534,21 @@ pub fn generateChunksInParallel(c: *LinkerContext, chunks: []Chunk, comptime is_ chunk.entry_point.source_index - @as(u32, (if (c.framework) |fw| if (fw.server_components != null) 3 else 1 else 1)) else null, - .referenced_css_files = switch (chunk.content) { + .referenced_css_chunks = switch (chunk.content) { .javascript => |js| @ptrCast(try bun.default_allocator.dupe(u32, js.css_chunks)), .css => &.{}, .html => &.{}, }, })); - if (sourcemap_output_file) |sourcemap_file| { - try output_files.append(sourcemap_file); - } - if (bytecode_output_file) |bytecode_file| { - try output_files.append(bytecode_file); - } + + // We want the chunk index to remain the same in `output_files` so the indices in `OutputFile.referenced_css_chunks` work + bun.assertf(chunk_index == chunk_index_in_chunks_list, "chunk_index ({d}) != chunk_index_in_chunks_list ({d})", .{ chunk_index, chunk_index_in_chunks_list }); } - try output_files.appendSlice(c.parse_graph.additional_output_files.items); + output_files.insertAdditionalOutputFiles(c.parse_graph.additional_output_files.items); } - return output_files; + return output_files.take(); } const bun = @import("bun"); @@ -600,3 +592,4 @@ const base64 = bun.base64; const JSC = bun.JSC; pub const ThreadPoolLib = bun.ThreadPool; +const OutputFileListBuilder = bun.bundle_v2.LinkerContext.OutputFileListBuilder; diff --git a/src/bundler/linker_context/writeOutputFilesToDisk.zig b/src/bundler/linker_context/writeOutputFilesToDisk.zig index d3f5622f5c54..bed21e54387e 100644 --- a/src/bundler/linker_context/writeOutputFilesToDisk.zig +++ b/src/bundler/linker_context/writeOutputFilesToDisk.zig @@ -2,7 +2,7 @@ pub fn writeOutputFilesToDisk( c: *LinkerContext, root_path: string, chunks: []Chunk, - output_files: *std.ArrayList(options.OutputFile), + output_files: *OutputFileListBuilder, ) !void { const trace = bun.perf.trace("Bundler.writeOutputFilesToDisk"); defer trace.end(); @@ -41,7 +41,7 @@ pub fn writeOutputFilesToDisk( var pathbuf: bun.PathBuffer = undefined; const bv2: *bundler.BundleV2 = @fieldParentPtr("linker", c); - for (chunks) |*chunk| { + for (chunks, 0..) |*chunk, chunk_index_in_chunks_list| { const trace2 = bun.perf.trace("Bundler.writeChunkToDisk"); defer trace2.end(); defer max_heap_allocator.reset(); @@ -292,14 +292,12 @@ pub fn writeOutputFilesToDisk( } const source_map_index: ?u32 = if (source_map_output_file != null) - @as(u32, @truncate(output_files.items.len + 1)) + try output_files.insertForSourcemapOrBytecode(source_map_output_file.?) else null; - const bytecode_index: ?u32 = if (bytecode_output_file != null and source_map_index != null) - @as(u32, @truncate(output_files.items.len + 2)) - else if (bytecode_output_file != null) - @as(u32, @truncate(output_files.items.len + 1)) + const bytecode_index: ?u32 = if (bytecode_output_file != null) + try output_files.insertForSourcemapOrBytecode(bytecode_output_file.?) else null; @@ -309,7 +307,8 @@ pub fn writeOutputFilesToDisk( c.graph.files.items(.entry_point_kind)[chunk.entry_point.source_index].outputKind() else .chunk; - try output_files.append(options.OutputFile.init(.{ + + const chunk_index = output_files.insertForChunk(options.OutputFile.init(.{ .output_path = bun.default_allocator.dupe(u8, chunk.final_rel_path) catch unreachable, .input_path = input_path, .input_loader = if (chunk.entry_point.is_entry_point) @@ -337,27 +336,19 @@ pub fn writeOutputFilesToDisk( chunk.entry_point.source_index - @as(u32, (if (c.framework) |fw| if (fw.server_components != null) 3 else 1 else 1)) else null, - .referenced_css_files = switch (chunk.content) { + .referenced_css_chunks = switch (chunk.content) { .javascript => |js| @ptrCast(try bun.default_allocator.dupe(u32, js.css_chunks)), .css => &.{}, .html => &.{}, }, })); - if (source_map_output_file) |sourcemap_file| { - try output_files.append(sourcemap_file); - } - - if (bytecode_output_file) |bytecode_file| { - try output_files.append(bytecode_file); - } + // We want the chunk index to remain the same in `output_files` so the indices in `OutputFile.referenced_css_chunks` work + bun.assertf(chunk_index == chunk_index_in_chunks_list, "chunk_index ({d}) != chunk_index_in_chunks_list ({d})", .{ chunk_index, chunk_index_in_chunks_list }); } { - const offset = output_files.items.len; - output_files.items.len += c.parse_graph.additional_output_files.items.len; - - for (c.parse_graph.additional_output_files.items, output_files.items[offset..][0..c.parse_graph.additional_output_files.items.len]) |*src, *dest| { + for (c.parse_graph.additional_output_files.items, output_files.getMutableAdditionalOutputFiles()) |*src, *dest| { const bytes = src.value.buffer.bytes; src.value.buffer.bytes.len = 0; @@ -442,3 +433,4 @@ pub const ParseTask = bun.bundle_v2.ParseTask; const Chunk = bundler.Chunk; const cheapPrefixNormalizer = bundler.cheapPrefixNormalizer; const debug = LinkerContext.debug; +const OutputFileListBuilder = bun.bundle_v2.LinkerContext.OutputFileListBuilder; From f0a605af2eef7106a33730ec291d9a2ded8b8eaf Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Fri, 4 Jul 2025 17:28:53 -0700 Subject: [PATCH 09/59] yoops --- src/bundler/linker_context/OutputFileListBuilder.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bundler/linker_context/OutputFileListBuilder.zig b/src/bundler/linker_context/OutputFileListBuilder.zig index 457dcfc96fba..4b8188543ded 100644 --- a/src/bundler/linker_context/OutputFileListBuilder.zig +++ b/src/bundler/linker_context/OutputFileListBuilder.zig @@ -104,7 +104,7 @@ pub fn insertForSourcemapOrBytecode(this: *OutputFileList, output_file: options. } pub fn insertAdditionalOutputFiles(this: *OutputFileList, additional_output_files: []const options.OutputFile) void { - bun.assertf(this.index_for_sourcemaps_and_bytecode orelse std.math.maxInt(u32) < this.additional_output_files_start, "index_for_sourcemaps_and_bytecode ({d}) \\< additional_output_files_start ({d})", .{ this.index_for_sourcemaps_and_bytecode orelse std.math.maxInt(u32), this.additional_output_files_start }); + bun.assertf(this.index_for_sourcemaps_and_bytecode orelse std.math.maxInt(u32) <= this.additional_output_files_start, "index_for_sourcemaps_and_bytecode ({d}) \\< additional_output_files_start ({d})", .{ this.index_for_sourcemaps_and_bytecode orelse std.math.maxInt(u32), this.additional_output_files_start }); bun.copy( options.OutputFile, this.getMutableAdditionalOutputFiles(), From aeddc6b8babaf2aa202987aecef705b01e3d4460 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Fri, 4 Jul 2025 17:28:59 -0700 Subject: [PATCH 10/59] fix --- src/bake/production.zig | 1 - 1 file changed, 1 deletion(-) diff --git a/src/bake/production.zig b/src/bake/production.zig index bea7a3c07821..5959c14d4c66 100644 --- a/src/bake/production.zig +++ b/src/bake/production.zig @@ -495,7 +495,6 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa css_file_count = 0; file_list.putIndex(global, 0, pt.preloadBundledModule(main_file_route_index)); for (main_file.referenced_css_chunks) |ref| { - const file = bundled_outputs[ref.get()]; bun.assert(ref.get() >= css_chunks_first); styles.putIndex(global, css_file_count, css_chunk_js_strings[ref.get() - css_chunks_first]); css_file_count += 1; From 18b6b94c3a9172730335e2a7f3b2bc92ecb625a7 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Sat, 5 Jul 2025 01:22:54 -0700 Subject: [PATCH 11/59] that works --- cmake/sources/ZigSources.txt | 4 ++++ src/OutputFile.zig | 7 +++++++ src/bake/BakeGlobalObject.cpp | 6 ++++++ src/bake/BakeGlobalObject.h | 2 ++ src/bake/production.zig | 38 +++++++++++++++++++++++++++++++++++ src/sourcemap/sourcemap.zig | 37 ++++++++++++++++++++++++++++++++++ 6 files changed, 94 insertions(+) diff --git a/cmake/sources/ZigSources.txt b/cmake/sources/ZigSources.txt index aa592e5957e4..4bc2c2dd6768 100644 --- a/cmake/sources/ZigSources.txt +++ b/cmake/sources/ZigSources.txt @@ -121,6 +121,8 @@ src/bun.js/bindings/Exception.zig src/bun.js/bindings/FetchHeaders.zig src/bun.js/bindings/FFI.zig src/bun.js/bindings/generated_classes_list.zig +src/bun.js/bindings/GeneratedBindings.zig +src/bun.js/bindings/GeneratedJS2Native.zig src/bun.js/bindings/GetterSetter.zig src/bun.js/bindings/HTTPServerAgent.zig src/bun.js/bindings/JSArray.zig @@ -305,6 +307,7 @@ src/bundler/linker_context/generateCodeForLazyExport.zig src/bundler/linker_context/generateCompileResultForCssChunk.zig src/bundler/linker_context/generateCompileResultForHtmlChunk.zig src/bundler/linker_context/generateCompileResultForJSChunk.zig +src/bundler/linker_context/OutputFileListBuilder.zig src/bundler/linker_context/postProcessCSSChunk.zig src/bundler/linker_context/postProcessHTMLChunk.zig src/bundler/linker_context/postProcessJSChunk.zig @@ -357,6 +360,7 @@ src/cli/update_command.zig src/cli/upgrade_command.zig src/codegen/process_windows_translate_c.zig src/compile_target.zig +src/CompressedArray.zig src/comptime_string_map.zig src/copy_file.zig src/crash_handler.zig diff --git a/src/OutputFile.zig b/src/OutputFile.zig index 62af25a1fc31..d0381d8a7b45 100644 --- a/src/OutputFile.zig +++ b/src/OutputFile.zig @@ -99,6 +99,13 @@ pub const Value = union(Kind) { } } + pub fn asSlice(v: Value) []const u8 { + return switch (v) { + .buffer => |buf| buf.bytes, + else => "", + }; + } + pub fn toBunString(v: Value) bun.String { return switch (v) { .noop => bun.String.empty, diff --git a/src/bake/BakeGlobalObject.cpp b/src/bake/BakeGlobalObject.cpp index f7ce8b326e65..428b95e2e096 100644 --- a/src/bake/BakeGlobalObject.cpp +++ b/src/bake/BakeGlobalObject.cpp @@ -109,6 +109,12 @@ static JSC::JSInternalPromise* resolvedInternalPromise(JSC::JSGlobalObject* glob extern "C" BunString BakeProdLoad(ProductionPerThread* perThreadData, BunString a); +extern "C" ProductionPerThread* BakeGlobalObject__getPerThreadData(JSC::JSGlobalObject* global) +{ + Bake::GlobalObject* bake = jsCast(global); + return bake->m_perThreadData; +} + JSC::JSInternalPromise* bakeModuleLoaderFetch(JSC::JSGlobalObject* globalObject, JSC::JSModuleLoader* loader, JSC::JSValue key, JSC::JSValue parameters, JSC::JSValue script) diff --git a/src/bake/BakeGlobalObject.h b/src/bake/BakeGlobalObject.h index a308fe64c60c..792215ae0879 100644 --- a/src/bake/BakeGlobalObject.h +++ b/src/bake/BakeGlobalObject.h @@ -39,4 +39,6 @@ class GlobalObject : public Zig::GlobalObject { } }; +extern "C" ProductionPerThread* BakeGlobalObject__getPerThreadData(JSC::JSGlobalObject* global); + }; // namespace Kit diff --git a/src/bake/production.zig b/src/bake/production.zig index 5959c14d4c66..9e0eb25fe457 100644 --- a/src/bake/production.zig +++ b/src/bake/production.zig @@ -278,6 +278,7 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa const module_keys = try vm.allocator.alloc(bun.String, entry_points.files.count()); const output_indexes = entry_points.files.values(); var output_module_map: bun.StringArrayHashMapUnmanaged(OutputFile.Index) = .{}; + var source_maps: bun.StringArrayHashMapUnmanaged(OutputFile.Index) = .{}; @memset(module_keys, bun.String.dead); for (bundled_outputs, 0..) |file, i| { log("{s} - {s} : {s} - {?d}\n", .{ @@ -303,6 +304,7 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa switch (file.side orelse continue) { .client => { + // TODO: Maybe not do this all in 1 thread? // Client-side resources will be written to disk for usage in on the client side _ = file.writeToDisk(root_dir, ".") catch |err| { bun.handleErrorReturnTrace(err, @errorReturnTrace()); @@ -318,6 +320,27 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa }; } + // If the file has a sourcemap, store it so we can put it on + // `PerThread` so we can provide sourcemapped stacktraces for + // server components. + if (file.source_map_index != std.math.maxInt(u32)) { + const source_map_index = file.source_map_index; + const source_map_file: *const OutputFile = &bundled_outputs[source_map_index]; + bun.assert(source_map_file.output_kind == .sourcemap); + + const without_prefix = if (bun.strings.hasPrefixComptime(file.dest_path, "./") or + (Environment.isWindows and bun.strings.hasPrefixComptime(file.dest_path, ".\\"))) + file.dest_path[2..] + else + file.dest_path; + + try source_maps.put( + allocator, + try std.fmt.allocPrint(allocator, "bake:/{s}", .{without_prefix}), + OutputFile.Index.init(@intCast(source_map_index)), + ); + } + switch (file.output_kind) { .@"entry-point", .chunk => { const without_prefix = if (bun.strings.hasPrefixComptime(file.dest_path, "./") or @@ -354,6 +377,7 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa .output_indexes = output_indexes, .module_keys = module_keys, .module_map = output_module_map, + .source_maps = source_maps, }; var pt = try PerThread.init(vm, per_thread_options); @@ -755,6 +779,7 @@ pub const PerThread = struct { module_keys: []const bun.String, /// Unordered module_map: bun.StringArrayHashMapUnmanaged(OutputFile.Index), + source_maps: bun.StringArrayHashMapUnmanaged(OutputFile.Index), // Thread-local vm: *JSC.VirtualMachine, @@ -773,6 +798,7 @@ pub const PerThread = struct { module_keys: []const bun.String, /// Unordered module_map: bun.StringArrayHashMapUnmanaged(OutputFile.Index), + source_maps: bun.StringArrayHashMapUnmanaged(OutputFile.Index), }; extern fn BakeGlobalObject__attachPerThreadData(global: *JSC.JSGlobalObject, pt: ?*PerThread) void; @@ -794,6 +820,7 @@ pub const PerThread = struct { .vm = vm, .loaded_files = loaded_files, .all_server_files = all_server_files, + .source_maps = opts.source_maps, }; } @@ -860,6 +887,17 @@ pub export fn BakeProdLoad(pt: *PerThread, key: bun.String) bun.String { return bun.String.dead; } +pub export fn BakeProdSourceMap(pt: *PerThread, key: bun.String) bun.String { + var sfa = std.heap.stackFallback(4096, bun.default_allocator); + const allocator = sfa.get(); + const utf8 = key.toUTF8(allocator); + defer utf8.deinit(); + if (pt.source_maps.get(utf8.slice())) |value| { + return pt.bundled_outputs[value.get()].value.toBunString(); + } + return bun.String.dead; +} + const TypeAndFlags = packed struct(i32) { type: u8, unused: u24 = 0, diff --git a/src/sourcemap/sourcemap.zig b/src/sourcemap/sourcemap.zig index bb4d969fe097..4dbc371a6b59 100644 --- a/src/sourcemap/sourcemap.zig +++ b/src/sourcemap/sourcemap.zig @@ -836,6 +836,33 @@ pub fn getSourceMapImpl( // try to load a .map file if (load_hint != .is_inline_map) try_external: { + if (comptime SourceProviderKind == BakeSourceProvider) { + const data = BakeSourceProvider.getExternal( + provider, + bun.JSC.VirtualMachine.get().global, + source_filename, + ); + break :parsed .{ + .is_external_map, + parseJSON( + bun.default_allocator, + allocator, + data, + result, + ) catch |err| { + // Print warning even if this came from non-visible code like + // calling `error.stack`. This message is only printed if + // the sourcemap has been found but is invalid, such as being + // invalid JSON text or corrupt mappings. + bun.Output.warn("Could not decode sourcemap in '{s}': {s}", .{ + source_filename, + @errorName(err), + }); // Disable the "try using --sourcemap=external" hint + bun.JSC.SavedSourceMap.MissingSourceMapNoteInfo.seen_invalid = true; + return null; + }, + }; + } var load_path_buf: *bun.PathBuffer = bun.PathBufferPool.get(); defer bun.PathBufferPool.put(load_path_buf); if (source_filename.len + 4 > load_path_buf.len) @@ -918,6 +945,8 @@ pub const SourceProviderMap = opaque { } }; +extern "c" fn BakeGlobalObject__getPerThreadData(global: *bun.JSC.JSGlobalObject) *bun.bake.production.PerThread; + pub const BakeSourceProvider = opaque { extern fn BakeSourceProvider__getSourceSlice(*BakeSourceProvider) bun.String; pub const getSourceSlice = BakeSourceProvider__getSourceSlice; @@ -925,6 +954,14 @@ pub const BakeSourceProvider = opaque { return ParsedSourceMap.SourceContentPtr.fromBakeProvider(this); } + pub fn getExternal(_: *BakeSourceProvider, global: *bun.JSC.JSGlobalObject, source_filename: []const u8) []const u8 { + const pt = BakeGlobalObject__getPerThreadData(global); + if (pt.source_maps.get(source_filename)) |value| { + return pt.bundled_outputs[value.get()].value.asSlice(); + } + return ""; + } + /// The last two arguments to this specify loading hints pub fn getSourceMap( provider: *BakeSourceProvider, From ee44ef5a0325405d0b70bb222ceb5b213b1720a1 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Mon, 7 Jul 2025 16:37:19 -0700 Subject: [PATCH 12/59] Move per thread data outside --- src/bake/BakeGlobalObject.cpp | 6 +++--- src/bake/BakeGlobalObject.h | 9 ++++++--- src/bake/production.zig | 20 +++++++++++++++++--- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/src/bake/BakeGlobalObject.cpp b/src/bake/BakeGlobalObject.cpp index 428b95e2e096..a38da306d2bb 100644 --- a/src/bake/BakeGlobalObject.cpp +++ b/src/bake/BakeGlobalObject.cpp @@ -107,9 +107,9 @@ static JSC::JSInternalPromise* resolvedInternalPromise(JSC::JSGlobalObject* glob return promise; } -extern "C" BunString BakeProdLoad(ProductionPerThread* perThreadData, BunString a); +extern "C" BunString BakeProdLoad(void* perThreadData, BunString a); -extern "C" ProductionPerThread* BakeGlobalObject__getPerThreadData(JSC::JSGlobalObject* global) +extern "C" void* BakeGlobalObject__getPerThreadData(JSC::JSGlobalObject* global) { Bake::GlobalObject* bake = jsCast(global); return bake->m_perThreadData; @@ -246,7 +246,7 @@ extern "C" GlobalObject* BakeCreateProdGlobal(void* console) return global; } -extern "C" void BakeGlobalObject__attachPerThreadData(GlobalObject* global, ProductionPerThread* perThreadData) +extern "C" void BakeGlobalObject__attachPerThreadData(GlobalObject* global, void* perThreadData) { global->m_perThreadData = perThreadData; } diff --git a/src/bake/BakeGlobalObject.h b/src/bake/BakeGlobalObject.h index 792215ae0879..3370e71401fc 100644 --- a/src/bake/BakeGlobalObject.h +++ b/src/bake/BakeGlobalObject.h @@ -4,13 +4,15 @@ namespace Bake { -struct ProductionPerThread; +// Opaque pointer to Zig's bake.production.PerThread structure +// This must never be dereferenced in C++ code +// Using void* to avoid any issues with incomplete types class GlobalObject : public Zig::GlobalObject { public: using Base = Zig::GlobalObject; - ProductionPerThread* m_perThreadData = nullptr; + void* m_perThreadData = nullptr; DECLARE_INFO; template static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) @@ -39,6 +41,7 @@ class GlobalObject : public Zig::GlobalObject { } }; -extern "C" ProductionPerThread* BakeGlobalObject__getPerThreadData(JSC::JSGlobalObject* global); +extern "C" void* BakeGlobalObject__getPerThreadData(JSC::JSGlobalObject* global); +extern "C" void BakeGlobalObject__attachPerThreadData(GlobalObject* global, void* perThreadData); }; // namespace Kit diff --git a/src/bake/production.zig b/src/bake/production.zig index f2f04a69b910..65e0d2bf03b4 100644 --- a/src/bake/production.zig +++ b/src/bake/production.zig @@ -82,7 +82,21 @@ pub fn buildCommand(ctx: bun.CLI.Command.Context) !void { const api_lock = vm.jsc.getAPILock(); defer api_lock.release(); - buildWithVm(ctx, cwd, vm) catch |err| switch (err) { + + var pt: PerThread = .{ + .input_files = &.{}, + .bundled_outputs = &.{}, + .output_indexes = &.{}, + .module_keys = &.{}, + .module_map = .{}, + .source_maps = .{}, + + .vm = vm, + .loaded_files = bun.bit_set.AutoBitSet.initEmpty(vm.allocator, 0) catch unreachable, + .all_server_files = JSValue.null, + }; + + buildWithVm(ctx, cwd, vm, &pt) catch |err| switch (err) { error.JSError => |e| { bun.handleErrorReturnTrace(err, @errorReturnTrace()); const err_value = vm.global.takeException(e); @@ -96,7 +110,7 @@ pub fn buildCommand(ctx: bun.CLI.Command.Context) !void { }; } -pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMachine) !void { +pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMachine, pt: *PerThread) !void { // Load and evaluate the configuration module const global = vm.global; const b = &vm.transpiler; @@ -380,7 +394,7 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa .source_maps = source_maps, }; - var pt = try PerThread.init(vm, per_thread_options); + pt.* = try PerThread.init(vm, per_thread_options); pt.attach(); // Static site generator From d1bada7afe93f9333dc065ae80001697471eb7af Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Mon, 7 Jul 2025 18:13:31 -0700 Subject: [PATCH 13/59] fix import meta thing --- .../linker_context/OutputFileListBuilder.zig | 17 +- src/js_parser.zig | 23 +++ test/bake/bake-harness.ts | 87 +++++++++- .../dev/import-meta-inline-negative.test.ts | 40 +++++ test/bake/dev/import-meta-inline.test.ts | 148 ++++++++++++++++++ 5 files changed, 313 insertions(+), 2 deletions(-) create mode 100644 test/bake/dev/import-meta-inline-negative.test.ts create mode 100644 test/bake/dev/import-meta-inline.test.ts diff --git a/src/bundler/linker_context/OutputFileListBuilder.zig b/src/bundler/linker_context/OutputFileListBuilder.zig index 4b8188543ded..d9aab39eada0 100644 --- a/src/bundler/linker_context/OutputFileListBuilder.zig +++ b/src/bundler/linker_context/OutputFileListBuilder.zig @@ -33,6 +33,8 @@ index_for_chunk: u32, index_for_sourcemaps_and_bytecode: ?u32, additional_output_files_start: u32, +total_insertions: u32, + pub fn init( allocator: std.mem.Allocator, c: *const bun.bundle_v2.LinkerContext, @@ -51,17 +53,27 @@ pub fn init( .index_for_chunk = 0, .index_for_sourcemaps_and_bytecode = if (source_map_and_bytecode_count == 0) null else @as(u32, @truncate(chunks.len)), .additional_output_files_start = @as(u32, @intCast(chunks.len)) + source_map_and_bytecode_count, + .total_insertions = 0, }; } pub fn take(this: *@This()) std.ArrayList(options.OutputFile) { + bun.assertf(this.total_insertions == this.output_files.items.len, "total_insertions ({d}) != output_files.items.len ({d})", .{ this.total_insertions, this.output_files.items.len }); const list = this.output_files; this.output_files = std.ArrayList(options.OutputFile).init(bun.default_allocator); return list; } pub fn calculateOutputFileListCapacity(c: *const bun.bundle_v2.LinkerContext, chunks: []const bun.bundle_v2.Chunk) struct { u32, u32 } { - const source_map_count = if (c.options.source_maps.hasExternalFiles()) chunks.len else 0; + const source_map_count = if (c.options.source_maps.hasExternalFiles()) brk: { + var count: usize = 0; + for (chunks) |*chunk| { + if (chunk.content.sourcemap(c.options.source_maps).hasExternalFiles()) { + count += 1; + } + } + break :brk count; + } else 0; const bytecode_count = if (c.options.generate_bytecode_cache) bytecode_count: { var bytecode_count: usize = 0; for (chunks) |*chunk| { @@ -93,6 +105,7 @@ pub fn insertForChunk(this: *OutputFileList, output_file: options.OutputFile) u3 const index = this.indexForChunk(); bun.assertf(index < this.index_for_sourcemaps_and_bytecode orelse std.math.maxInt(u32), "index ({d}) \\< index_for_sourcemaps_and_bytecode ({d})", .{ index, this.index_for_sourcemaps_and_bytecode orelse std.math.maxInt(u32) }); this.output_files.items[index] = output_file; + this.total_insertions += 1; return index; } @@ -100,6 +113,7 @@ pub fn insertForSourcemapOrBytecode(this: *OutputFileList, output_file: options. const index = this.indexForSourcemapOrBytecode() orelse return error.NoSourceMapsOrBytecode; bun.assertf(index < this.additional_output_files_start, "index ({d}) \\< additional_output_files_start ({d})", .{ index, this.additional_output_files_start }); this.output_files.items[index] = output_file; + this.total_insertions += 1; return index; } @@ -110,6 +124,7 @@ pub fn insertAdditionalOutputFiles(this: *OutputFileList, additional_output_file this.getMutableAdditionalOutputFiles(), additional_output_files, ); + this.total_insertions += @as(u32, @intCast(additional_output_files.len)); } pub fn getMutableAdditionalOutputFiles(this: *OutputFileList) []options.OutputFile { diff --git a/src/js_parser.zig b/src/js_parser.zig index 1f6f60383e5b..e9379e3caae3 100644 --- a/src/js_parser.zig +++ b/src/js_parser.zig @@ -18761,6 +18761,29 @@ fn NewParser_( }, .loc = loc }; } + // Inline import.meta properties for Bake + if (p.options.framework != null) { + if (strings.eqlComptime(name, "dir")) { + // Inline import.meta.dir + return p.newExpr(E.String.init(p.source.path.name.dir), name_loc); + } else if (strings.eqlComptime(name, "dirname")) { + // Inline import.meta.dirname (same as dir) + return p.newExpr(E.String.init(p.source.path.name.dir), name_loc); + } else if (strings.eqlComptime(name, "file")) { + // Inline import.meta.file (filename only) + return p.newExpr(E.String.init(p.source.path.name.filename), name_loc); + } else if (strings.eqlComptime(name, "path")) { + // Inline import.meta.path (full path) + return p.newExpr(E.String.init(p.source.path.text), name_loc); + } else if (strings.eqlComptime(name, "url")) { + // Inline import.meta.url as file:// URL + const bunstr = bun.String.fromBytes(p.source.path.text); + defer bunstr.deref(); + const url = std.fmt.allocPrint(p.allocator, "file://{s}", .{JSC.URL.fileURLFromString(bunstr)}) catch unreachable; + return p.newExpr(E.String.init(url), name_loc); + } + } + // Make all property accesses on `import.meta.url` side effect free. return p.newExpr( E.Dot{ diff --git a/test/bake/bake-harness.ts b/test/bake/bake-harness.ts index dbb6b6294004..8e91c328c523 100644 --- a/test/bake/bake-harness.ts +++ b/test/bake/bake-harness.ts @@ -18,7 +18,7 @@ import { Matchers } from "bun:test"; import { EventEmitter } from "node:events"; // @ts-ignore import { dedent } from "../bundler/expectBundled.ts"; -import { bunEnv, bunExe, isCI, isWindows, mergeWindowEnvs } from "harness"; +import { bunEnv, bunExe, isCI, isWindows, mergeWindowEnvs, tempDirWithFiles } from "harness"; import { expect } from "bun:test"; import { exitCodeMapStrings } from "./exit-code-map.mjs"; @@ -1415,6 +1415,90 @@ async function installReactWithCache(root: string) { } } +// Global React cache management +let reactCachePromise: Promise | null = null; + +/** + * Ensures the React cache is populated. This is a global operation that + * only happens once per test run. + */ +export async function ensureReactCache(): Promise { + if (!reactCachePromise) { + reactCachePromise = (async () => { + const cacheFiles = ["node_modules", "package.json", "bun.lock"]; + const cacheValid = cacheFiles.every(file => fs.existsSync(path.join(reactCacheDir, file))); + + if (!cacheValid) { + // Create a temporary directory for installation + const tempInstallDir = fs.mkdtempSync(path.join(tempDir, "react-install-")); + + // Create a minimal package.json + fs.writeFileSync( + path.join(tempInstallDir, "package.json"), + JSON.stringify({ + name: "react-cache-install", + version: "1.0.0", + private: true, + }), + ); + + try { + // Install React packages + await Bun.$`${bunExe()} i react@experimental react-dom@experimental react-server-dom-bun react-refresh@experimental && ${bunExe()} install` + .cwd(tempInstallDir) + .env({ ...bunEnv }) + .throws(true); + + // Copy to cache + for (const file of cacheFiles) { + const src = path.join(tempInstallDir, file); + const dest = path.join(reactCacheDir, file); + if (fs.existsSync(src)) { + if (fs.statSync(src).isDirectory()) { + fs.cpSync(src, dest, { recursive: true, force: true }); + } else { + fs.copyFileSync(src, dest); + } + } + } + } finally { + // Clean up temp directory + fs.rmSync(tempInstallDir, { recursive: true, force: true }); + } + } + })(); + } + + return reactCachePromise; +} + +/** + * Copies cached React dependencies to the specified directory. + * This ensures React is available without running install. + */ +export async function copyCachedReactDeps(root: string): Promise { + // Ensure cache is populated + await ensureReactCache(); + + // Copy node_modules from cache to target directory + const src = path.join(reactCacheDir, "node_modules"); + const dest = path.join(root, "node_modules"); + + if (fs.existsSync(src)) { + fs.cpSync(src, dest, { recursive: true, force: true }); + } +} + +/** + * Creates a temporary directory with files and React dependencies pre-installed. + * This is a convenience wrapper that combines tempDirWithFiles with copyCachedReactDeps. + */ +export async function tempDirWithBakeDeps(name: string, files: Record): Promise { + const dir = tempDirWithFiles(name, files); + await copyCachedReactDeps(dir); + return dir; +} + const devTestRoot = path.join(import.meta.dir, "dev").replaceAll("\\", "/"); const prodTestRoot = path.join(import.meta.dir, "dev").replaceAll("\\", "/"); const counts: Record = {}; @@ -1632,6 +1716,7 @@ function testImpl( await writeAll(root, options.files); const runInstall = options.framework === "react"; if (runInstall) { + // await copyCachedReactDeps(root); await installReactWithCache(root); } if (options.files["bun.app.ts"] == undefined && htmlFiles.length === 0) { diff --git a/test/bake/dev/import-meta-inline-negative.test.ts b/test/bake/dev/import-meta-inline-negative.test.ts new file mode 100644 index 000000000000..59579b227ac2 --- /dev/null +++ b/test/bake/dev/import-meta-inline-negative.test.ts @@ -0,0 +1,40 @@ +import { expect, test } from "bun:test"; +import { bunEnv, bunExe, tempDirWithFiles } from "harness"; + +test("import.meta properties are NOT inlined without bake framework", async () => { + const dir = tempDirWithFiles("import-meta-no-inline", { + "index.ts": ` + console.log("dir:", import.meta.dir); + console.log("dirname:", import.meta.dirname); + console.log("file:", import.meta.file); + console.log("path:", import.meta.path); + console.log("url:", import.meta.url); + `, + }); + + // Run without bundling - should show actual values + await using proc = Bun.spawn({ + cmd: [bunExe(), "index.ts"], + env: bunEnv, + cwd: dir, + }); + + const [stdout, stderr, exitCode] = await Promise.all([ + new Response(proc.stdout).text(), + new Response(proc.stderr).text(), + proc.exited, + ]); + + expect(exitCode).toBe(0); + expect(stderr).toBe(""); + + // When not bundled, these properties should resolve to actual values + expect(stdout).toContain("dir:"); + expect(stdout).toContain("dirname:"); + expect(stdout).toContain("file:"); + expect(stdout).toContain("path:"); + expect(stdout).toContain("url:"); + + // The values should NOT be inlined - they should be the actual runtime values + expect(stdout).not.toContain("undefined"); +}); \ No newline at end of file diff --git a/test/bake/dev/import-meta-inline.test.ts b/test/bake/dev/import-meta-inline.test.ts new file mode 100644 index 000000000000..ef67545dae84 --- /dev/null +++ b/test/bake/dev/import-meta-inline.test.ts @@ -0,0 +1,148 @@ +// import.meta properties are inlined at parse time in Bake +import { expect } from "bun:test"; +import { devTest, emptyHtmlFile, minimalFramework } from "../bake-harness"; + +devTest("import.meta properties are inlined in bake", { + framework: minimalFramework, + files: { + "routes/index.ts": ` +export default function (req, meta) { + return Response.json({ + dir: import.meta.dir, + dirname: import.meta.dirname, + file: import.meta.file, + path: import.meta.path, + url: import.meta.url, + }); +} +`, + }, + async test(dev) { + const response = await dev.fetch("/"); + const json = await response.json(); + + // Check that all properties are strings, not undefined + expect(typeof json.dir).toBe("string"); + expect(typeof json.dirname).toBe("string"); + expect(typeof json.file).toBe("string"); + expect(typeof json.path).toBe("string"); + expect(typeof json.url).toBe("string"); + + // Check that dir and dirname are the same + expect(json.dir).toBe(json.dirname); + + // Check that file is just the filename + expect(json.file).toBe("index.ts"); + + // Check that path contains the full path including filename + expect(json.path).toContain("routes/index.ts"); + expect(json.path).toEndWith("index.ts"); + + // Check that url is a file:// URL + expect(json.url).toStartWith("file://"); + expect(json.url).toContain("routes/index.ts"); + }, +}); + +devTest("import.meta properties work with dynamic updates", { + framework: minimalFramework, + files: { + "routes/test.ts": ` +export default function (req, meta) { + const values = [ + "dir: " + import.meta.dir, + "file: " + import.meta.file, + "path: " + import.meta.path, + ]; + return new Response(values.join("\\n")); +} +`, + }, + async test(dev) { + const response = await dev.fetch("/test"); + const text = await response.text(); + + // Verify the values are inlined strings + expect(text).toContain("dir: "); + expect(text).toContain("file: test.ts"); + expect(text).toContain("path: "); + expect(text).toContain("routes/test.ts"); + + // Update the file with a meaningful change + await dev.patch("routes/test.ts", { + find: '"dir: "', + replace: '"directory: "', + }); + + const response2 = await dev.fetch("/test"); + const text2 = await response2.text(); + + // After the patch, the first line should say "directory:" instead of "dir:" + expect(text2).toContain("directory: "); + expect(text2).toContain("file: test.ts"); + expect(text2).toContain("path: "); + expect(text2).toContain("routes/test.ts"); + }, +}); + +devTest("import.meta properties with nested directories", { + framework: minimalFramework, + files: { + "routes/api/v1/handler.ts": ` +export default function (req, meta) { + return Response.json({ + dir: import.meta.dir, + file: import.meta.file, + path: import.meta.path, + url: import.meta.url, + }); +} +`, + }, + async test(dev) { + const response = await dev.fetch("/api/v1/handler"); + const json = await response.json(); + + expect(json.file).toBe("handler.ts"); + expect(json.path).toContain("routes/api/v1/handler.ts"); + expect(json.dir).toContain("routes/api/v1"); + expect(json.url).toMatch(/^file:\/\/.*routes\/api\/v1\/handler\.ts$/); + }, +}); + +devTest("import.meta properties in client-side code show runtime values", { + framework: minimalFramework, + files: { + "test_import_meta_inline.js": ` +// Test file for import.meta inlining +console.log("import.meta.dir:", import.meta.dir); +console.log("import.meta.dirname:", import.meta.dirname); +console.log("import.meta.file:", import.meta.file); +console.log("import.meta.path:", import.meta.path); +console.log("import.meta.url:", import.meta.url); +`, + "index.html": emptyHtmlFile({ + scripts: ["test_import_meta_inline.js"], + }), + }, + async test(dev) { + await using c = await dev.client("/"); + + // In client-side code, import.meta properties show runtime values + // They are NOT inlined because this is not server-side code + const messages = [ + await c.getStringMessage(), + await c.getStringMessage(), + await c.getStringMessage(), + await c.getStringMessage(), + await c.getStringMessage(), + ]; + + // Verify all properties are logged + expect(messages.some(m => m.startsWith("import.meta.dir:"))).toBe(true); + expect(messages.some(m => m.startsWith("import.meta.dirname:"))).toBe(true); + expect(messages.some(m => m.startsWith("import.meta.file:"))).toBe(true); + expect(messages.some(m => m.startsWith("import.meta.path:"))).toBe(true); + expect(messages.some(m => m.startsWith("import.meta.url:"))).toBe(true); + }, +}); \ No newline at end of file From ecf1d1b8cbb3f318750d33f5c2c90afd59e3d408 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Mon, 7 Jul 2025 18:21:21 -0700 Subject: [PATCH 14/59] more test --- test/bake/dev/production.test.ts | 132 +++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 test/bake/dev/production.test.ts diff --git a/test/bake/dev/production.test.ts b/test/bake/dev/production.test.ts new file mode 100644 index 000000000000..3e8873e45332 --- /dev/null +++ b/test/bake/dev/production.test.ts @@ -0,0 +1,132 @@ +import { describe, expect, test } from "bun:test"; +import { devTest, minimalFramework, tempDirWithBakeDeps } from "../bake-harness"; +import { bunEnv, bunExe } from "harness"; +import path from "path"; + +/** + * Production build tests + */ +describe("production", () => { + test("works with sourcemaps - error thrown in React component", async () => { + const dir = await tempDirWithBakeDeps("bake-production-sourcemap", { + "src/index.tsx": `export default { app: { framework: "react" } };`, + "pages/index.tsx": `export default function IndexPage() { + throw new Error("oh no!"); + return
Hello World
; +}`, + "package.json": JSON.stringify({ + "name": "test-app", + "version": "1.0.0", + "devDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0", + }, + }), + }); + + // Run the build command + const { + exitCode: buildExitCode, + stdout: buildStdout, + stderr: buildStderr, + } = await Bun.$`${bunExe()} build --app ./src/index.tsx`.cwd(dir).throws(false); + + // The build should fail due to the runtime error during SSG + expect(buildExitCode).toBe(1); + + // Check that the error message shows the proper source location + expect(buildStderr.toString()).toContain("throw new Error"); + expect(buildStderr.toString()).toContain("oh no!"); + }); + + test("import.meta properties are inlined in production build", async () => { + const dir = await tempDirWithBakeDeps("bake-production-import-meta", { + "src/index.tsx": `export default { + app: { + framework: "react", + } + };`, + "pages/index.tsx": ` +export default function IndexPage() { + const metaInfo = { + dir: import.meta.dir, + dirname: import.meta.dirname, + file: import.meta.file, + path: import.meta.path, + url: import.meta.url, + }; + + return ( +
+

Import Meta Test

+
{JSON.stringify(metaInfo, null, 2)}
+
{JSON.stringify(metaInfo)}
+
+ ); +} +`, + "pages/api/test.tsx": ` +export default function TestPage() { + const values = [ + "dir=" + import.meta.dir, + "dirname=" + import.meta.dirname, + "file=" + import.meta.file, + "path=" + import.meta.path, + "url=" + import.meta.url, + ]; + + return ( +
+

API Test

+
{values.join("\\n")}
+
{values.join("|")}
+
+ ); +} +`, + }); + + // Run the build command + const buildProc = await Bun.$`${bunExe()} build --app ./src/index.tsx --outdir ./dist` + .cwd(dir) + .env(bunEnv) + .throws(false); + + expect(buildProc.exitCode).toBe(0); + + // Check that the build output contains the generated files + const distFiles = await Bun.$`ls -la dist/`.cwd(dir).text(); + expect(distFiles).toContain("index.html"); + expect(distFiles).toContain("_bun"); + + // In production SSG, the import.meta values are inlined during build time + // and rendered into the static HTML. The values should appear in the HTML output. + + // Check the generated static HTML files + const indexHtml = await Bun.file(path.join(dir, "dist", "index.html")).text(); + const apiTestHtml = await Bun.file(path.join(dir, "dist", "api", "test", "index.html")).text(); + + // The HTML output should contain the rendered import.meta values + // Check for the presence of the expected values in the HTML + + // For the index page, check that it contains the expected file paths + expect(indexHtml).toContain("index.tsx"); + expect(indexHtml).toContain("pages"); + + // Check if the HTML contains evidence of import.meta values being used + // The exact format might be HTML-escaped, so we check for key patterns + const hasIndexPath = indexHtml.includes("pages/index.tsx") || + indexHtml.includes("pages/index.tsx") || + indexHtml.includes("pages\\index.tsx"); + expect(hasIndexPath).toBe(true); + + // For the API test page + expect(apiTestHtml).toContain("test.tsx"); + expect(apiTestHtml).toContain("pages"); + + const hasApiPath = apiTestHtml.includes("pages/api/test.tsx") || + apiTestHtml.includes("pages/api/test.tsx") || + apiTestHtml.includes("pages\\api\\test.tsx"); + expect(hasApiPath).toBe(true); + }); +}); From 5f6f2e95476274bfb36a57545ed4d9d920c3e11d Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Mon, 7 Jul 2025 19:56:30 -0700 Subject: [PATCH 15/59] why tf is that there --- cmake/sources/ZigSources.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/cmake/sources/ZigSources.txt b/cmake/sources/ZigSources.txt index 4bc2c2dd6768..b937f8a0ea4c 100644 --- a/cmake/sources/ZigSources.txt +++ b/cmake/sources/ZigSources.txt @@ -360,7 +360,6 @@ src/cli/update_command.zig src/cli/upgrade_command.zig src/codegen/process_windows_translate_c.zig src/compile_target.zig -src/CompressedArray.zig src/comptime_string_map.zig src/copy_file.zig src/crash_handler.zig From a6ac500a53526edde0ba7979b70d5fc1481c6c75 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Tue, 8 Jul 2025 12:49:52 -0700 Subject: [PATCH 16/59] stuff --- src/bake/BakeGlobalObject.cpp | 12 +- src/js_parser.zig | 7 +- test/bake/dev/import-meta-inline.test.ts | 176 +++++++++++++++++++++-- test/bake/dev/production.test.ts | 157 ++++++++++++++++++++ 4 files changed, 333 insertions(+), 19 deletions(-) diff --git a/src/bake/BakeGlobalObject.cpp b/src/bake/BakeGlobalObject.cpp index a38da306d2bb..63a12d5c5fbd 100644 --- a/src/bake/BakeGlobalObject.cpp +++ b/src/bake/BakeGlobalObject.cpp @@ -140,7 +140,17 @@ JSC::JSInternalPromise* bakeModuleLoaderFetch(JSC::JSGlobalObject* globalObject, JSC::SourceProviderSourceType::Module)); return resolvedInternalPromise(globalObject, JSC::JSSourceCode::create(vm, WTFMove(sourceCode))); } - return rejectedInternalPromise(globalObject, createTypeError(globalObject, makeString("Bundle does not have \""_s, moduleKey, "\". This is a bug in Bun's bundler."_s))); + + // We unconditionally prefix the key with "bake:" inside + // BakeProdResolve in production.zig. + // + // But if someone does: `await import(resolve(import.meta.dir, "nav.ts"))` + // we don't actually want to load it from the Bake production module + // map and instead make it go through the normal codepath. + auto bakePrefixRemoved = moduleKey.substringSharingImpl("bake:"_s.length()); + JSString* bakePrefixRemovedString = jsNontrivialString(vm, bakePrefixRemoved); + JSValue bakePrefixRemovedJsvalue = bakePrefixRemovedString; + return Zig::GlobalObject::moduleLoaderFetch(globalObject, loader, bakePrefixRemovedJsvalue, parameters, script); } return rejectedInternalPromise(globalObject, createTypeError(globalObject, "BakeGlobalObject does not have per-thread data configured"_s)); } diff --git a/src/js_parser.zig b/src/js_parser.zig index e9379e3caae3..ea0e0510c513 100644 --- a/src/js_parser.zig +++ b/src/js_parser.zig @@ -18763,12 +18763,9 @@ fn NewParser_( // Inline import.meta properties for Bake if (p.options.framework != null) { - if (strings.eqlComptime(name, "dir")) { + if (strings.eqlComptime(name, "dir") or strings.eqlComptime(name, "dirname")) { // Inline import.meta.dir return p.newExpr(E.String.init(p.source.path.name.dir), name_loc); - } else if (strings.eqlComptime(name, "dirname")) { - // Inline import.meta.dirname (same as dir) - return p.newExpr(E.String.init(p.source.path.name.dir), name_loc); } else if (strings.eqlComptime(name, "file")) { // Inline import.meta.file (filename only) return p.newExpr(E.String.init(p.source.path.name.filename), name_loc); @@ -18779,7 +18776,7 @@ fn NewParser_( // Inline import.meta.url as file:// URL const bunstr = bun.String.fromBytes(p.source.path.text); defer bunstr.deref(); - const url = std.fmt.allocPrint(p.allocator, "file://{s}", .{JSC.URL.fileURLFromString(bunstr)}) catch unreachable; + const url = std.fmt.allocPrint(p.allocator, "{s}", .{JSC.URL.fileURLFromString(bunstr)}) catch unreachable; return p.newExpr(E.String.init(url), name_loc); } } diff --git a/test/bake/dev/import-meta-inline.test.ts b/test/bake/dev/import-meta-inline.test.ts index ef67545dae84..e29bf4208087 100644 --- a/test/bake/dev/import-meta-inline.test.ts +++ b/test/bake/dev/import-meta-inline.test.ts @@ -20,24 +20,24 @@ export default function (req, meta) { async test(dev) { const response = await dev.fetch("/"); const json = await response.json(); - + // Check that all properties are strings, not undefined expect(typeof json.dir).toBe("string"); expect(typeof json.dirname).toBe("string"); expect(typeof json.file).toBe("string"); expect(typeof json.path).toBe("string"); expect(typeof json.url).toBe("string"); - + // Check that dir and dirname are the same expect(json.dir).toBe(json.dirname); - + // Check that file is just the filename expect(json.file).toBe("index.ts"); - + // Check that path contains the full path including filename expect(json.path).toContain("routes/index.ts"); expect(json.path).toEndWith("index.ts"); - + // Check that url is a file:// URL expect(json.url).toStartWith("file://"); expect(json.url).toContain("routes/index.ts"); @@ -61,22 +61,22 @@ export default function (req, meta) { async test(dev) { const response = await dev.fetch("/test"); const text = await response.text(); - + // Verify the values are inlined strings expect(text).toContain("dir: "); expect(text).toContain("file: test.ts"); expect(text).toContain("path: "); expect(text).toContain("routes/test.ts"); - + // Update the file with a meaningful change await dev.patch("routes/test.ts", { find: '"dir: "', replace: '"directory: "', }); - + const response2 = await dev.fetch("/test"); const text2 = await response2.text(); - + // After the patch, the first line should say "directory:" instead of "dir:" expect(text2).toContain("directory: "); expect(text2).toContain("file: test.ts"); @@ -102,7 +102,7 @@ export default function (req, meta) { async test(dev) { const response = await dev.fetch("/api/v1/handler"); const json = await response.json(); - + expect(json.file).toBe("handler.ts"); expect(json.path).toContain("routes/api/v1/handler.ts"); expect(json.dir).toContain("routes/api/v1"); @@ -127,7 +127,7 @@ console.log("import.meta.url:", import.meta.url); }, async test(dev) { await using c = await dev.client("/"); - + // In client-side code, import.meta properties show runtime values // They are NOT inlined because this is not server-side code const messages = [ @@ -137,7 +137,7 @@ console.log("import.meta.url:", import.meta.url); await c.getStringMessage(), await c.getStringMessage(), ]; - + // Verify all properties are logged expect(messages.some(m => m.startsWith("import.meta.dir:"))).toBe(true); expect(messages.some(m => m.startsWith("import.meta.dirname:"))).toBe(true); @@ -145,4 +145,154 @@ console.log("import.meta.url:", import.meta.url); expect(messages.some(m => m.startsWith("import.meta.path:"))).toBe(true); expect(messages.some(m => m.startsWith("import.meta.url:"))).toBe(true); }, -}); \ No newline at end of file +}); + +devTest("import.meta properties in catch-all routes", { + framework: minimalFramework, + files: { + "routes/blog/[...slug].ts": ` +export default function BlogPost(req, meta) { + const url = new URL(req.url); + const slug = url.pathname.replace('/blog/', '').split('/').filter(Boolean); + + const metaInfo = { + file: import.meta.file, + dir: import.meta.dir, + path: import.meta.path, + url: import.meta.url, + dirname: import.meta.dirname, + }; + + return Response.json({ + slug: slug, + title: slug.map(s => s.charAt(0).toUpperCase() + s.slice(1)).join(' '), + meta: metaInfo, + content: "This is a blog post at: " + slug.join('/'), + }); +} +`, + }, + async test(dev) { + // Test single segment + const post1 = await dev.fetch("/blog/hello"); + const json1 = await post1.json(); + + expect(json1.slug).toEqual(["hello"]); + expect(json1.title).toBe("Hello"); + expect(json1.content).toBe("This is a blog post at: hello"); + + // Verify import.meta properties are inlined + expect(json1.meta.file).toBe("[...slug].ts"); + expect(json1.meta.dir).toContain("routes/blog"); + expect(json1.meta.dirname).toBe(json1.meta.dir); + expect(json1.meta.path).toContain("routes/blog/[...slug].ts"); + expect(json1.meta.url).toMatch(/^file:\/\/.*routes\/blog\/\[\.\.\.slug\]\.ts$/); + + // Test multiple segments + const post2 = await dev.fetch("/blog/2024/tech/bun-framework"); + const json2 = await post2.json(); + + expect(json2.slug).toEqual(["2024", "tech", "bun-framework"]); + expect(json2.title).toBe("2024 Tech Bun-framework"); + expect(json2.content).toBe("This is a blog post at: 2024/tech/bun-framework"); + + // Meta properties should be the same regardless of the route + expect(json2.meta.file).toBe("[...slug].ts"); + expect(json2.meta.path).toContain("routes/blog/[...slug].ts"); + + // Test empty slug (just /blog/) + const post3 = await dev.fetch("/blog/"); + const json3 = await post3.json(); + + expect(json3.slug).toEqual([]); + expect(json3.title).toBe(""); + expect(json3.content).toBe("This is a blog post at: "); + }, +}); + +devTest("import.meta properties in nested catch-all routes with static siblings", { + framework: minimalFramework, + files: { + "routes/docs/[...path].ts": ` +export default function DocsPage(req, meta) { + const url = new URL(req.url); + const path = url.pathname.replace('/docs/', '').split('/').filter(Boolean); + + return Response.json({ + type: "catch-all", + path: path, + file: import.meta.file, + dir: import.meta.dir, + fullPath: import.meta.path, + }); +} +`, + "routes/docs/api.ts": ` +export default function ApiDocs(req, meta) { + return Response.json({ + type: "static", + page: "API Documentation", + file: import.meta.file, + dir: import.meta.dir, + fullPath: import.meta.path, + }); +} +`, + "routes/docs/getting-started.ts": ` +export default function GettingStarted(req, meta) { + return Response.json({ + type: "static", + page: "Getting Started", + file: import.meta.file, + dir: import.meta.dir, + fullPath: import.meta.path, + }); +} +`, + }, + async test(dev) { + // Test static route - should match api.ts, not catch-all + const apiResponse = await dev.fetch("/docs/api"); + const apiJson = await apiResponse.json(); + + expect(apiJson.type).toBe("static"); + expect(apiJson.page).toBe("API Documentation"); + expect(apiJson.file).toBe("api.ts"); + expect(apiJson.dir).toContain("routes/docs"); + expect(apiJson.fullPath).toContain("routes/docs/api.ts"); + + // Test another static route + const startResponse = await dev.fetch("/docs/getting-started"); + const startJson = await startResponse.json(); + + expect(startJson.type).toBe("static"); + expect(startJson.page).toBe("Getting Started"); + expect(startJson.file).toBe("getting-started.ts"); + expect(startJson.fullPath).toContain("routes/docs/getting-started.ts"); + + // Test catch-all route - should match for non-static paths + const guideResponse = await dev.fetch("/docs/guides/advanced/optimization"); + const guideText = await guideResponse.text(); + console.log("Guide text", guideText); + const guideJson = await guideResponse.json(); + + expect(guideJson.type).toBe("catch-all"); + expect(guideJson.path).toEqual(["guides", "advanced", "optimization"]); + expect(guideJson.file).toBe("[...path].ts"); + expect(guideJson.dir).toContain("routes/docs"); + expect(guideJson.fullPath).toContain("routes/docs/[...path].ts"); + + // Update catch-all route and verify import.meta values remain inlined + await dev.patch("routes/docs/[...path].ts", { + find: '"catch-all"', + replace: '"dynamic-catch-all"', + }); + + const updatedResponse = await dev.fetch("/docs/tutorials/intro"); + const updatedJson = await updatedResponse.json(); + + expect(updatedJson.type).toBe("dynamic-catch-all"); + expect(updatedJson.file).toBe("[...path].ts"); + expect(updatedJson.fullPath).toContain("routes/docs/[...path].ts"); + }, +}); diff --git a/test/bake/dev/production.test.ts b/test/bake/dev/production.test.ts index 3e8873e45332..b00d8244ae5d 100644 --- a/test/bake/dev/production.test.ts +++ b/test/bake/dev/production.test.ts @@ -129,4 +129,161 @@ export default function TestPage() { apiTestHtml.includes("pages\\api\\test.tsx"); expect(hasApiPath).toBe(true); }); + + test("import.meta properties are inlined in catch-all routes during production build", async () => { + const dir = await tempDirWithBakeDeps("bake-production-catch-all", { + "src/index.tsx": `export default { + app: { + framework: "react", + } + };`, + "pages/blog/[...slug].tsx": ` +export default function BlogPost({ params }) { + const slug = params.slug || []; + + const metaInfo = { + file: import.meta.file, + dir: import.meta.dir, + path: import.meta.path, + url: import.meta.url, + dirname: import.meta.dirname, + }; + + return ( +
+

Blog Post: {slug.join(' / ')}

+

You are reading: {slug.length === 0 ? 'the blog index' : slug.join('/')}

+
+
{JSON.stringify(metaInfo, null, 2)}
+
+
+ ); +} + +export async function getStaticPaths() { + return { + paths: [ + { params: { slug: ['2024', 'hello-world'] } }, + { params: { slug: ['2024', 'tech', 'bun-framework'] } }, + { params: { slug: ['tutorials', 'getting-started'] } }, + ], + fallback: false, + }; +} +`, + "pages/docs/[...path].tsx": ` +export default function DocsPage({ params }) { + const path = params.path || []; + + return ( +
+

Documentation

+ +
+

Reading docs at: /{path.join('/')}

+
+ + + + +
+
+
+ ); +} + +export async function getStaticPaths() { + return { + paths: [ + { params: { path: ['api', 'reference'] } }, + { params: { path: ['guides', 'advanced', 'optimization'] } }, + { params: { path: [] } }, // docs index + ], + fallback: false, + }; +} +`, + "pages/docs/getting-started.tsx": ` +export default function GettingStarted() { + return ( +
+

Getting Started

+

This is a static page, not a catch-all route.

+
+ + +
+
+ ); +} +`, + }); + + // Run the build command + const buildProc = await Bun.$`${bunExe()} build --app ./src/index.tsx --outdir ./dist` + .cwd(dir) + .env(bunEnv) + .throws(false); + + expect(buildProc.exitCode).toBe(0); + + // Check that the build output contains the generated files + const distFiles = await Bun.$`find dist -name "*.html" -type f | sort`.cwd(dir).text(); + const htmlFiles = distFiles.trim().split('\n').filter(Boolean); + + // Should have generated all the static paths + // Note: React's routing may flatten the paths + expect(htmlFiles).toContain('dist/2024/hello-world/index.html'); + expect(htmlFiles).toContain('dist/2024/tech/bun-framework/index.html'); + expect(htmlFiles).toContain('dist/tutorials/getting-started/index.html'); + expect(htmlFiles).toContain('dist/api/reference/index.html'); + expect(htmlFiles).toContain('dist/guides/advanced/optimization/index.html'); + expect(htmlFiles).toContain('dist/index.html'); + expect(htmlFiles).toContain('dist/docs/getting-started/index.html'); + + // Check blog post with multiple segments + const blogPostHtml = await Bun.file(path.join(dir, "dist", "2024", "tech", "bun-framework", "index.html")).text(); + + // Verify the content is rendered (may include HTML comments) + expect(blogPostHtml).toContain("Blog Post:"); + expect(blogPostHtml).toContain("2024 / tech / bun-framework"); + expect(blogPostHtml).toContain("You are reading:"); + expect(blogPostHtml).toContain("2024/tech/bun-framework"); + + // Check that import.meta values are inlined in the HTML + expect(blogPostHtml).toContain('data-file="[...slug].tsx"'); + expect(blogPostHtml).toContain('data-dir='); + expect(blogPostHtml).toContain('/pages/blog"'); // The full path will include the temp directory + expect(blogPostHtml).toContain('data-path='); + expect(blogPostHtml).toContain('/pages/blog/[...slug].tsx"'); + + // Check docs catch-all route + const docsHtml = await Bun.file(path.join(dir, "dist", "guides", "advanced", "optimization", "index.html")).text(); + + expect(docsHtml).toContain("Reading docs at:"); + expect(docsHtml).toContain("guides/advanced/optimization"); + expect(docsHtml).toContain('data-file="[...path].tsx"'); + expect(docsHtml).toContain('/pages/docs/[...path].tsx"'); + + // Check that the static getting-started page uses its own file name, not the catch-all + const staticHtml = await Bun.file(path.join(dir, "dist", "docs", "getting-started", "index.html")).text(); + + expect(staticHtml).toContain("Getting Started"); + expect(staticHtml).toContain("This is a static page"); + expect(staticHtml).toContain('data-file="getting-started.tsx"'); + expect(staticHtml).toContain('/pages/docs/getting-started.tsx"'); + expect(staticHtml).not.toContain('[...path].tsx'); + + // Verify that import.meta values are consistent across all catch-all instances + const blogIndex = await Bun.file(path.join(dir, "dist", "tutorials", "getting-started", "index.html")).text(); + expect(blogIndex).toContain('data-file="[...slug].tsx"'); + expect(blogIndex).toContain('/pages/blog/[...slug].tsx"'); + }); }); From ee319cf55a94897db52bbd15d3d8b9cff327969b Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Tue, 8 Jul 2025 13:29:18 -0700 Subject: [PATCH 17/59] don't crash with empty chunks list --- src/bake/production.zig | 5 +++++ src/bundler/bundle_v2.zig | 4 ++++ src/bundler/linker_context/computeChunks.zig | 6 ++++++ 3 files changed, 15 insertions(+) diff --git a/src/bake/production.zig b/src/bake/production.zig index 65e0d2bf03b4..0c26e3399c93 100644 --- a/src/bake/production.zig +++ b/src/bake/production.zig @@ -275,6 +275,11 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa .{ .js = vm.event_loop }, ); const bundled_outputs = bundled_outputs_list.items; + if (bundled_outputs.len == 0) { + Output.prettyln("done", .{}); + Output.flush(); + return; + } Output.prettyErrorln("Rendering routes", .{}); Output.flush(); diff --git a/src/bundler/bundle_v2.zig b/src/bundler/bundle_v2.zig index dd4d7fd766cc..f1cc8966398f 100644 --- a/src/bundler/bundle_v2.zig +++ b/src/bundler/bundle_v2.zig @@ -1484,6 +1484,10 @@ pub const BundleV2 = struct { reachable_files, ); + if (chunks.len == 0) { + return std.ArrayList(options.OutputFile).init(bun.default_allocator); + } + return try this.linker.generateChunksInParallel(chunks, false); } diff --git a/src/bundler/linker_context/computeChunks.zig b/src/bundler/linker_context/computeChunks.zig index d2f5abeef0c9..4cc71fbd25f3 100644 --- a/src/bundler/linker_context/computeChunks.zig +++ b/src/bundler/linker_context/computeChunks.zig @@ -310,6 +310,12 @@ pub noinline fn computeChunks( // Determine the order of JS files (and parts) within the chunk ahead of time try this.findAllImportedPartsInJSOrder(temp_allocator, chunks); + // Handle empty chunks case + if (chunks.len == 0) { + this.unique_key_buf = ""; + return chunks; + } + const unique_key_item_len = std.fmt.count("{any}C{d:0>8}", .{ bun.fmt.hexIntLower(unique_key), chunks.len }); var unique_key_builder = try bun.StringBuilder.initCapacity(this.allocator, unique_key_item_len * chunks.len); this.unique_key_buf = unique_key_builder.allocatedSlice(); From 52c2487f2471b376e6379c6283143d2172de80ab Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Tue, 8 Jul 2025 13:29:24 -0700 Subject: [PATCH 18/59] more tests --- test/bake/dev/production.test.ts | 91 +++++++++++++++++++++----------- 1 file changed, 61 insertions(+), 30 deletions(-) diff --git a/test/bake/dev/production.test.ts b/test/bake/dev/production.test.ts index b00d8244ae5d..10abd0cead1c 100644 --- a/test/bake/dev/production.test.ts +++ b/test/bake/dev/production.test.ts @@ -105,28 +105,30 @@ export default function TestPage() { // Check the generated static HTML files const indexHtml = await Bun.file(path.join(dir, "dist", "index.html")).text(); const apiTestHtml = await Bun.file(path.join(dir, "dist", "api", "test", "index.html")).text(); - + // The HTML output should contain the rendered import.meta values // Check for the presence of the expected values in the HTML - + // For the index page, check that it contains the expected file paths expect(indexHtml).toContain("index.tsx"); expect(indexHtml).toContain("pages"); - + // Check if the HTML contains evidence of import.meta values being used // The exact format might be HTML-escaped, so we check for key patterns - const hasIndexPath = indexHtml.includes("pages/index.tsx") || - indexHtml.includes("pages/index.tsx") || - indexHtml.includes("pages\\index.tsx"); + const hasIndexPath = + indexHtml.includes("pages/index.tsx") || + indexHtml.includes("pages/index.tsx") || + indexHtml.includes("pages\\index.tsx"); expect(hasIndexPath).toBe(true); - + // For the API test page expect(apiTestHtml).toContain("test.tsx"); expect(apiTestHtml).toContain("pages"); - - const hasApiPath = apiTestHtml.includes("pages/api/test.tsx") || - apiTestHtml.includes("pages/api/test.tsx") || - apiTestHtml.includes("pages\\api\\test.tsx"); + + const hasApiPath = + apiTestHtml.includes("pages/api/test.tsx") || + apiTestHtml.includes("pages/api/test.tsx") || + apiTestHtml.includes("pages\\api\\test.tsx"); expect(hasApiPath).toBe(true); }); @@ -236,54 +238,83 @@ export default function GettingStarted() { // Check that the build output contains the generated files const distFiles = await Bun.$`find dist -name "*.html" -type f | sort`.cwd(dir).text(); - const htmlFiles = distFiles.trim().split('\n').filter(Boolean); - + const htmlFiles = distFiles.trim().split("\n").filter(Boolean); + // Should have generated all the static paths // Note: React's routing may flatten the paths - expect(htmlFiles).toContain('dist/2024/hello-world/index.html'); - expect(htmlFiles).toContain('dist/2024/tech/bun-framework/index.html'); - expect(htmlFiles).toContain('dist/tutorials/getting-started/index.html'); - expect(htmlFiles).toContain('dist/api/reference/index.html'); - expect(htmlFiles).toContain('dist/guides/advanced/optimization/index.html'); - expect(htmlFiles).toContain('dist/index.html'); - expect(htmlFiles).toContain('dist/docs/getting-started/index.html'); + expect(htmlFiles).toContain("dist/2024/hello-world/index.html"); + expect(htmlFiles).toContain("dist/2024/tech/bun-framework/index.html"); + expect(htmlFiles).toContain("dist/tutorials/getting-started/index.html"); + expect(htmlFiles).toContain("dist/api/reference/index.html"); + expect(htmlFiles).toContain("dist/guides/advanced/optimization/index.html"); + expect(htmlFiles).toContain("dist/index.html"); + expect(htmlFiles).toContain("dist/docs/getting-started/index.html"); // Check blog post with multiple segments const blogPostHtml = await Bun.file(path.join(dir, "dist", "2024", "tech", "bun-framework", "index.html")).text(); - + // Verify the content is rendered (may include HTML comments) expect(blogPostHtml).toContain("Blog Post:"); expect(blogPostHtml).toContain("2024 / tech / bun-framework"); expect(blogPostHtml).toContain("You are reading:"); expect(blogPostHtml).toContain("2024/tech/bun-framework"); - + // Check that import.meta values are inlined in the HTML expect(blogPostHtml).toContain('data-file="[...slug].tsx"'); - expect(blogPostHtml).toContain('data-dir='); + expect(blogPostHtml).toContain("data-dir="); expect(blogPostHtml).toContain('/pages/blog"'); // The full path will include the temp directory - expect(blogPostHtml).toContain('data-path='); + expect(blogPostHtml).toContain("data-path="); expect(blogPostHtml).toContain('/pages/blog/[...slug].tsx"'); - + // Check docs catch-all route const docsHtml = await Bun.file(path.join(dir, "dist", "guides", "advanced", "optimization", "index.html")).text(); - + expect(docsHtml).toContain("Reading docs at:"); expect(docsHtml).toContain("guides/advanced/optimization"); expect(docsHtml).toContain('data-file="[...path].tsx"'); expect(docsHtml).toContain('/pages/docs/[...path].tsx"'); - + // Check that the static getting-started page uses its own file name, not the catch-all const staticHtml = await Bun.file(path.join(dir, "dist", "docs", "getting-started", "index.html")).text(); - + expect(staticHtml).toContain("Getting Started"); expect(staticHtml).toContain("This is a static page"); expect(staticHtml).toContain('data-file="getting-started.tsx"'); expect(staticHtml).toContain('/pages/docs/getting-started.tsx"'); - expect(staticHtml).not.toContain('[...path].tsx'); - + expect(staticHtml).not.toContain("[...path].tsx"); + // Verify that import.meta values are consistent across all catch-all instances const blogIndex = await Bun.file(path.join(dir, "dist", "tutorials", "getting-started", "index.html")).text(); expect(blogIndex).toContain('data-file="[...slug].tsx"'); expect(blogIndex).toContain('/pages/blog/[...slug].tsx"'); }); + + test("handles build with no pages directory without crashing", async () => { + const dir = await tempDirWithBakeDeps("bake-production-no-pages", { + "app.ts": `export default { app: { framework: "react" } };`, + "package.json": JSON.stringify({ + "name": "test-app", + "version": "1.0.0", + "devDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0", + }, + }), + }); + + // Run the build command - should not crash even with no pages + const { exitCode, stderr } = await Bun.$`${bunExe()} build --app ./app.ts` + .cwd(dir) + .throws(false); + + // The build should complete successfully (or fail gracefully, not crash) + // We're testing that it doesn't crash with the StringBuilder assertion + expect(exitCode).toBeDefined(); + + // If it fails, it should be a graceful failure, not a crash + if (exitCode !== 0) { + expect(stderr.toString()).not.toContain("reached unreachable code"); + expect(stderr.toString()).not.toContain("assert(this.cap > 0)"); + } + }); }); From 54f815357e8992a552bdf8c32332f5ff4191bf22 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Tue, 8 Jul 2025 14:25:09 -0700 Subject: [PATCH 19/59] fix catch all --- src/bake/FrameworkRouter.zig | 31 ++++++++++++++++++++++-- test/bake/dev/import-meta-inline.test.ts | 3 +-- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/bake/FrameworkRouter.zig b/src/bake/FrameworkRouter.zig index 98b9074cb822..5443e26f2abf 100644 --- a/src/bake/FrameworkRouter.zig +++ b/src/bake/FrameworkRouter.zig @@ -287,6 +287,11 @@ pub const EncodedPattern = struct { }, .param => |name| { const end = strings.indexOfCharPos(path, '/', i) orelse path.len; + // Check if we're about to exceed the maximum number of parameters + if (param_num >= MatchedParams.max_count) { + // TODO: ideally we should throw a nice user message + bun.Output.panic("Route pattern matched more than {d} parameters. Path: {s}", .{ MatchedParams.max_count, path }); + } params.params.len = @intCast(param_num + 1); params.params.buffer[param_num] = .{ .key = name, @@ -295,8 +300,30 @@ pub const EncodedPattern = struct { param_num += 1; i = if (end == path.len) end else end + 1; }, - .catch_all_optional => return true, - .catch_all => break, + .catch_all_optional, .catch_all => |name| { + // Capture remaining path segments as individual parameters + if (i < path.len) { + var segment_start = i; + while (segment_start < path.len) { + const segment_end = strings.indexOfCharPos(path, '/', segment_start) orelse path.len; + if (segment_start < segment_end) { + // Check if we're about to exceed the maximum number of parameters + if (param_num >= MatchedParams.max_count) { + // TODO: ideally we should throw a nice user message + bun.Output.panic("Route pattern matched more than {d} parameters. Path: {s}", .{ MatchedParams.max_count, path }); + } + params.params.len = @intCast(param_num + 1); + params.params.buffer[param_num] = .{ + .key = name, + .value = path[segment_start..segment_end], + }; + param_num += 1; + } + segment_start = if (segment_end == path.len) segment_end else segment_end + 1; + } + } + return true; + }, .group => continue, } } diff --git a/test/bake/dev/import-meta-inline.test.ts b/test/bake/dev/import-meta-inline.test.ts index e29bf4208087..8067195f57bc 100644 --- a/test/bake/dev/import-meta-inline.test.ts +++ b/test/bake/dev/import-meta-inline.test.ts @@ -272,8 +272,7 @@ export default function GettingStarted(req, meta) { // Test catch-all route - should match for non-static paths const guideResponse = await dev.fetch("/docs/guides/advanced/optimization"); - const guideText = await guideResponse.text(); - console.log("Guide text", guideText); + expect(guideResponse.status).toBe(200); const guideJson = await guideResponse.json(); expect(guideJson.type).toBe("catch-all"); From cf8c1185b90557299f8e22916b7ab05331698008 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Wed, 9 Jul 2025 15:11:31 -0700 Subject: [PATCH 20/59] fix some stuff --- src/bake/production.zig | 29 +++++++++++++++++++ .../linker_context/OutputFileListBuilder.zig | 3 +- src/js/builtins/Bake.ts | 17 +++-------- 3 files changed, 35 insertions(+), 14 deletions(-) diff --git a/src/bake/production.zig b/src/bake/production.zig index 0c26e3399c93..30f915d521bb 100644 --- a/src/bake/production.zig +++ b/src/bake/production.zig @@ -469,11 +469,28 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa str.* = (try bun.String.createFormat("{s}{s}", .{ public_path, output_file.dest_path })).toJS(global); } + // Route URL patterns with parameter placeholders. + // Examples: "/", "/about", "/blog/:slug", "/products/:category/:id" const route_patterns = try JSValue.createEmptyArray(global, navigatable_routes.items.len); + + // File indices for each route's components (page, layouts). + // Example: [2, 5, 0] = page at index 2, layout at 5, root layout at 0 const route_nested_files = try JSValue.createEmptyArray(global, navigatable_routes.items.len); + + // Router type index (lower 8 bits) and flags (upper 24 bits). + // Example: 0x00000001 = router type 1, no flags const route_type_and_flags = try JSValue.createEmptyArray(global, navigatable_routes.items.len); + + // Source file paths relative to project root. + // Examples: "pages/index.tsx", "pages/blog/[slug].tsx" const route_source_files = try JSValue.createEmptyArray(global, navigatable_routes.items.len); + + // Parameter names for dynamic routes (reversed order), null for static routes. + // Examples: ["slug"] for /blog/[slug], ["id", "category"] for /products/[category]/[id] const route_param_info = try JSValue.createEmptyArray(global, navigatable_routes.items.len); + + // CSS chunk URLs for each route. + // Example: ["/assets/main.css", "/assets/blog.css"] const route_style_references = try JSValue.createEmptyArray(global, navigatable_routes.items.len); var params_buf: std.ArrayListUnmanaged([]const u8) = .{}; @@ -652,18 +669,30 @@ fn BakeGetOnModuleNamespace(global: *JSC.JSGlobalObject, module: JSValue, proper return result; } +/// Renders all routes for static site generation by calling the JavaScript implementation. extern fn BakeRenderRoutesForProdStatic( *JSC.JSGlobalObject, + /// Output directory path (e.g., "./dist") out_base: bun.String, + /// Server module paths (e.g., ["bake://page.js", "bake://layout.js"]) all_server_files: JSValue, + /// Framework prerender functions by router type render_static: JSValue, + /// Framework getParams functions by router type get_params: JSValue, + /// Client entry URLs by router type (e.g., ["/client.js", null]) client_entry_urls: JSValue, + /// Route patterns (e.g., ["/", "/about", "/blog/:slug"]) patterns: JSValue, + /// File indices per route (e.g., [[0], [1], [2, 0]]) files: JSValue, + /// Packed router type and flags (e.g., [0x00000000, 0x00000001]) type_and_flags: JSValue, + /// Source paths (e.g., ["pages/index.tsx", "pages/blog/[slug].tsx"]) src_route_files: JSValue, + /// Dynamic route params (e.g., [null, null, ["slug"]]) param_information: JSValue, + /// CSS URLs per route (e.g., [["/main.css"], ["/main.css", "/blog.css"]]) styles: JSValue, ) *JSC.JSPromise; diff --git a/src/bundler/linker_context/OutputFileListBuilder.zig b/src/bundler/linker_context/OutputFileListBuilder.zig index d9aab39eada0..022ccdc4002d 100644 --- a/src/bundler/linker_context/OutputFileListBuilder.zig +++ b/src/bundler/linker_context/OutputFileListBuilder.zig @@ -58,6 +58,7 @@ pub fn init( } pub fn take(this: *@This()) std.ArrayList(options.OutputFile) { + // TODO: should this return an error bun.assertf(this.total_insertions == this.output_files.items.len, "total_insertions ({d}) != output_files.items.len ({d})", .{ this.total_insertions, this.output_files.items.len }); const list = this.output_files; this.output_files = std.ArrayList(options.OutputFile).init(bun.default_allocator); @@ -118,7 +119,7 @@ pub fn insertForSourcemapOrBytecode(this: *OutputFileList, output_file: options. } pub fn insertAdditionalOutputFiles(this: *OutputFileList, additional_output_files: []const options.OutputFile) void { - bun.assertf(this.index_for_sourcemaps_and_bytecode orelse std.math.maxInt(u32) <= this.additional_output_files_start, "index_for_sourcemaps_and_bytecode ({d}) \\< additional_output_files_start ({d})", .{ this.index_for_sourcemaps_and_bytecode orelse std.math.maxInt(u32), this.additional_output_files_start }); + bun.assertf(this.index_for_sourcemaps_and_bytecode orelse 0 <= this.additional_output_files_start, "index_for_sourcemaps_and_bytecode ({d}) \\< additional_output_files_start ({d})", .{ this.index_for_sourcemaps_and_bytecode orelse 0, this.additional_output_files_start }); bun.copy( options.OutputFile, this.getMutableAdditionalOutputFiles(), diff --git a/src/js/builtins/Bake.ts b/src/js/builtins/Bake.ts index e083dcaf669a..458dceac77a0 100644 --- a/src/js/builtins/Bake.ts +++ b/src/js/builtins/Bake.ts @@ -75,24 +75,15 @@ export function renderRoutesForProdStatic( if (files == null) { throw new Error(`Route ${JSON.stringify(sourceRouteFiles[i])} cannot be pre-rendered to a static page.`); } + await Promise.all( Object.entries(files).map(([key, value]) => { if (params != null) { $assert(patterns[i].includes(`:`)); - const matches = regex.exec(patterns[i]); - const [_, p1] = matches!; - if (typeof params[p1] === "string") { - // replace the :paramName part of patterns[i] with the value of params[paramName] - // use a regex in replace with a callback - const newKey = params[p1]; - return Bun.write(pathJoin(outBase, newKey + key), value); - } - if (Array.isArray(params[p1])) { - return Bun.write(pathJoin(outBase, ...params[p1], key), value); - } - throw new Error( - `Route ${JSON.stringify(sourceRouteFiles[i])} has a param that is not a string or array of strings: ${p1}`, + const newKey = patterns[i].replace(/:(\w+)/g, (_, p1) => + typeof params[p1] === "string" ? params[p1] : params[p1].join("/"), ); + return Bun.write(pathJoin(outBase, newKey + key), value); } return Bun.write(pathJoin(outBase, patterns[i] + key), value); }), From eb7e4d620c23979a44acf82448444d055fc8f27a Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Wed, 9 Jul 2025 17:14:06 -0700 Subject: [PATCH 21/59] fix sourcemap crash in dev server --- src/bake/BakeGlobalObject.cpp | 5 +++++ src/bake/BakeGlobalObject.h | 4 ---- src/bake/BakeSourceProvider.cpp | 2 +- src/sourcemap/sourcemap.zig | 15 +++++++++++++-- 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/bake/BakeGlobalObject.cpp b/src/bake/BakeGlobalObject.cpp index 63a12d5c5fbd..98766dd13bf3 100644 --- a/src/bake/BakeGlobalObject.cpp +++ b/src/bake/BakeGlobalObject.cpp @@ -109,6 +109,11 @@ static JSC::JSInternalPromise* resolvedInternalPromise(JSC::JSGlobalObject* glob extern "C" BunString BakeProdLoad(void* perThreadData, BunString a); +extern "C" bool BakeGlobalObject__isBaked(JSC::JSGlobalObject* global) +{ + return global->JSCell::inherits(Bake::GlobalObject::info()); +} + extern "C" void* BakeGlobalObject__getPerThreadData(JSC::JSGlobalObject* global) { Bake::GlobalObject* bake = jsCast(global); diff --git a/src/bake/BakeGlobalObject.h b/src/bake/BakeGlobalObject.h index 3370e71401fc..58e4d3f2753d 100644 --- a/src/bake/BakeGlobalObject.h +++ b/src/bake/BakeGlobalObject.h @@ -4,10 +4,6 @@ namespace Bake { -// Opaque pointer to Zig's bake.production.PerThread structure -// This must never be dereferenced in C++ code -// Using void* to avoid any issues with incomplete types - class GlobalObject : public Zig::GlobalObject { public: using Base = Zig::GlobalObject; diff --git a/src/bake/BakeSourceProvider.cpp b/src/bake/BakeSourceProvider.cpp index 15d17d28ba74..70c9778b3c00 100644 --- a/src/bake/BakeSourceProvider.cpp +++ b/src/bake/BakeSourceProvider.cpp @@ -22,7 +22,7 @@ extern "C" BunString BakeSourceProvider__getSourceSlice(SourceProvider* provider return Bun::toStringView(provider->source()); } -extern "C" JSC::EncodedJSValue BakeLoadInitialServerCode(GlobalObject* global, BunString source, bool separateSSRGraph) { +extern "C" JSC::EncodedJSValue BakeLoadInitialServerCode(JSC::JSGlobalObject* global, BunString source, bool separateSSRGraph) { auto& vm = JSC::getVM(global); auto scope = DECLARE_THROW_SCOPE(vm); diff --git a/src/sourcemap/sourcemap.zig b/src/sourcemap/sourcemap.zig index 4dbc371a6b59..a994073515f6 100644 --- a/src/sourcemap/sourcemap.zig +++ b/src/sourcemap/sourcemap.zig @@ -836,10 +836,17 @@ pub fn getSourceMapImpl( // try to load a .map file if (load_hint != .is_inline_map) try_external: { - if (comptime SourceProviderKind == BakeSourceProvider) { + if (comptime SourceProviderKind == BakeSourceProvider) fallback_to_normal: { + const global = bun.JSC.VirtualMachine.get().global; + // If we're using bake's production build the global object will + // be Bake::GlobalObject and we can fetch the sourcemap from it, + // if not fallback to the normal way + if (!BakeGlobalObject__isBaked(global)) { + break :fallback_to_normal; + } const data = BakeSourceProvider.getExternal( provider, - bun.JSC.VirtualMachine.get().global, + global, source_filename, ); break :parsed .{ @@ -945,6 +952,9 @@ pub const SourceProviderMap = opaque { } }; +/// ( ͡° ͜ʖ ͡°) +extern "c" fn BakeGlobalObject__isBaked(global: *bun.JSC.JSGlobalObject) bool; + extern "c" fn BakeGlobalObject__getPerThreadData(global: *bun.JSC.JSGlobalObject) *bun.bake.production.PerThread; pub const BakeSourceProvider = opaque { @@ -955,6 +965,7 @@ pub const BakeSourceProvider = opaque { } pub fn getExternal(_: *BakeSourceProvider, global: *bun.JSC.JSGlobalObject, source_filename: []const u8) []const u8 { + bun.assert(BakeGlobalObject__isBaked(global)); const pt = BakeGlobalObject__getPerThreadData(global); if (pt.source_maps.get(source_filename)) |value| { return pt.bundled_outputs[value.get()].value.asSlice(); From 4e338fc88e174d9584385714a48fe6661b913f84 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Thu, 10 Jul 2025 12:29:04 -0700 Subject: [PATCH 22/59] register client and server sourcemaps properly --- src/bake/production.zig | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/src/bake/production.zig b/src/bake/production.zig index 30f915d521bb..a0d7de5fa0c1 100644 --- a/src/bake/production.zig +++ b/src/bake/production.zig @@ -300,7 +300,8 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa var source_maps: bun.StringArrayHashMapUnmanaged(OutputFile.Index) = .{}; @memset(module_keys, bun.String.dead); for (bundled_outputs, 0..) |file, i| { - log("{s} - {s} : {s} - {?d}\n", .{ + log("src_index={any} {s} - {s} : {s} - {?d}\n", .{ + file.source_index.unwrap(), if (file.side) |s| @tagName(s) else "null", file.src_path.text, file.dest_path, @@ -331,7 +332,6 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa }; }, .server => { - // For Debugging if (ctx.bundler_options.bake_debug_dump_server) { _ = file.writeToDisk(root_dir, ".") catch |err| { bun.handleErrorReturnTrace(err, @errorReturnTrace()); @@ -388,6 +388,25 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa } }, } + + // TODO: should we just write the sourcemaps to disk? + if (file.source_map_index != std.math.maxInt(u32)) { + const source_map_index = file.source_map_index; + const source_map_file: *const OutputFile = &bundled_outputs[source_map_index]; + bun.assert(source_map_file.output_kind == .sourcemap); + + const without_prefix = if (bun.strings.hasPrefixComptime(file.dest_path, "./") or + (Environment.isWindows and bun.strings.hasPrefixComptime(file.dest_path, ".\\"))) + file.dest_path[2..] + else + file.dest_path; + + try source_maps.put( + allocator, + try std.fmt.allocPrint(allocator, "bake:/{s}", .{without_prefix}), + OutputFile.Index.init(@intCast(source_map_index)), + ); + } } const per_thread_options: PerThread.Options = .{ @@ -472,23 +491,23 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa // Route URL patterns with parameter placeholders. // Examples: "/", "/about", "/blog/:slug", "/products/:category/:id" const route_patterns = try JSValue.createEmptyArray(global, navigatable_routes.items.len); - + // File indices for each route's components (page, layouts). // Example: [2, 5, 0] = page at index 2, layout at 5, root layout at 0 const route_nested_files = try JSValue.createEmptyArray(global, navigatable_routes.items.len); - + // Router type index (lower 8 bits) and flags (upper 24 bits). // Example: 0x00000001 = router type 1, no flags const route_type_and_flags = try JSValue.createEmptyArray(global, navigatable_routes.items.len); - + // Source file paths relative to project root. // Examples: "pages/index.tsx", "pages/blog/[slug].tsx" const route_source_files = try JSValue.createEmptyArray(global, navigatable_routes.items.len); - + // Parameter names for dynamic routes (reversed order), null for static routes. // Examples: ["slug"] for /blog/[slug], ["id", "category"] for /products/[category]/[id] const route_param_info = try JSValue.createEmptyArray(global, navigatable_routes.items.len); - + // CSS chunk URLs for each route. // Example: ["/assets/main.css", "/assets/blog.css"] const route_style_references = try JSValue.createEmptyArray(global, navigatable_routes.items.len); From 0225723b19db5fc906eea3f6bf6e93cd0072bb6f Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Thu, 10 Jul 2025 15:46:15 -0700 Subject: [PATCH 23/59] add test for edgecase --- test/bake/dev/production.test.ts | 39 ++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/test/bake/dev/production.test.ts b/test/bake/dev/production.test.ts index 10abd0cead1c..3bc1eb6fd9df 100644 --- a/test/bake/dev/production.test.ts +++ b/test/bake/dev/production.test.ts @@ -317,4 +317,43 @@ export default function GettingStarted() { expect(stderr.toString()).not.toContain("assert(this.cap > 0)"); } }); + + test("client-side component with default import should work", async () => { + const dir = await tempDirWithBakeDeps("bake-production-client-import", { + "src/index.tsx": `export default { app: { framework: "react" } };`, + "pages/index.tsx": `import Client from "../components/Client"; + +export default function IndexPage() { + return ( +
+ LMAOHello World + +
+ ); +}`, + "components/Client.tsx": `"use client"; + +export function Client() { + console.log("Client-side!"); + return
Hello World
; +}`, + "package.json": JSON.stringify({ + "name": "test-app", + "version": "1.0.0", + "devDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0", + }, + }), + }); + + // Run the build command + const { exitCode, stderr } = await Bun.$`${bunExe()} build --app ./src/index.tsx` + .cwd(dir) + .throws(false); + + // The build should succeed - client components should support default imports + expect(stderr.toString()).toBe(""); + expect(exitCode).toBe(0); + }); }); From 7eb340a80f5710f04abd180cbc18049f16e7d2d7 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Thu, 10 Jul 2025 15:47:03 -0700 Subject: [PATCH 24/59] write runtime to disk for client side code --- src/OutputFile.zig | 3 + src/bake/bake.zig | 69 ++++++++++++++----- src/bake/production.zig | 41 ++++++++++- .../generateChunksInParallel.zig | 5 ++ 4 files changed, 96 insertions(+), 22 deletions(-) diff --git a/src/OutputFile.zig b/src/OutputFile.zig index d0381d8a7b45..09dba25260de 100644 --- a/src/OutputFile.zig +++ b/src/OutputFile.zig @@ -22,6 +22,7 @@ side: ?bun.bake.Side, entry_point_index: ?u32, referenced_css_chunks: []const Index = &.{}, source_index: Index.Optional = .none, +bake_is_runtime: bool = false, pub const Index = bun.GenericIndex(u32, OutputFile); @@ -214,6 +215,7 @@ pub const Options = struct { side: ?bun.bake.Side, entry_point_index: ?u32, referenced_css_chunks: []const Index = &.{}, + bake_is_runtime: bool = false, }; pub fn init(options: Options) OutputFile { @@ -248,6 +250,7 @@ pub fn init(options: Options) OutputFile { .side = options.side, .entry_point_index = options.entry_point_index, .referenced_css_chunks = options.referenced_css_chunks, + .bake_is_runtime = options.bake_is_runtime, }; } diff --git a/src/bake/bake.zig b/src/bake/bake.zig index 41646d0cc0b4..37ed5e9f0f3f 100644 --- a/src/bake/bake.zig +++ b/src/bake/bake.zig @@ -38,6 +38,18 @@ pub const UserOptions = struct { 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'", .{}); @@ -157,14 +169,39 @@ const BuildConfigSubset = struct { 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| { + 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 bun.JSC.Node.validators.throwErrInvalidArgType(global, "sourcemap", .{}, "string", val); } return options; @@ -591,18 +628,6 @@ pub const Framework = struct { try bundler_options.parsePluginArray(plugin_array, global); } - if (try opts.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); - } - } - return framework; } @@ -624,7 +649,7 @@ pub const Framework = struct { else => .none, }; - return initTranspilerWithSourceMap( + return initTranspilerWithOptions( framework, arena, log, @@ -633,10 +658,13 @@ pub const Framework = struct { out, bundler_options, source_map, + null, + null, + null, ); } - pub fn initTranspilerWithSourceMap( + pub fn initTranspilerWithOptions( framework: *Framework, arena: std.mem.Allocator, log: *bun.logger.Log, @@ -645,6 +673,9 @@ pub const Framework = struct { 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.JSAst; @@ -709,9 +740,9 @@ pub const Framework = struct { out.options.production = mode != .development; out.options.tree_shaking = mode != .development; - out.options.minify_syntax = mode != .development; - out.options.minify_identifiers = mode != .development; - out.options.minify_whitespace = 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; diff --git a/src/bake/production.zig b/src/bake/production.zig index a0d7de5fa0c1..62ce98cfae6a 100644 --- a/src/bake/production.zig +++ b/src/bake/production.zig @@ -188,10 +188,10 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa var client_transpiler: bun.transpiler.Transpiler = undefined; var server_transpiler: bun.transpiler.Transpiler = undefined; var ssr_transpiler: bun.transpiler.Transpiler = undefined; - try framework.initTranspilerWithSourceMap(allocator, vm.log, .production_static, .server, &server_transpiler, &options.bundler_options.server, bun.options.SourceMapOption.fromApi(options.bundler_options.server.source_map)); - try framework.initTranspilerWithSourceMap(allocator, vm.log, .production_static, .client, &client_transpiler, &options.bundler_options.client, bun.options.SourceMapOption.fromApi(options.bundler_options.client.source_map)); + try framework.initTranspilerWithOptions(allocator, vm.log, .production_static, .server, &server_transpiler, &options.bundler_options.server, bun.options.SourceMapOption.fromApi(options.bundler_options.server.source_map), options.bundler_options.server.minify_whitespace, options.bundler_options.server.minify_syntax, options.bundler_options.server.minify_identifiers); + try framework.initTranspilerWithOptions(allocator, vm.log, .production_static, .client, &client_transpiler, &options.bundler_options.client, bun.options.SourceMapOption.fromApi(options.bundler_options.client.source_map), options.bundler_options.client.minify_whitespace, options.bundler_options.client.minify_syntax, options.bundler_options.client.minify_identifiers); if (separate_ssr_graph) { - try framework.initTranspilerWithSourceMap(allocator, vm.log, .production_static, .ssr, &ssr_transpiler, &options.bundler_options.ssr, bun.options.SourceMapOption.fromApi(options.bundler_options.ssr.source_map)); + try framework.initTranspilerWithOptions(allocator, vm.log, .production_static, .ssr, &ssr_transpiler, &options.bundler_options.ssr, bun.options.SourceMapOption.fromApi(options.bundler_options.ssr.source_map), options.bundler_options.ssr.minify_whitespace, options.bundler_options.ssr.minify_syntax, options.bundler_options.ssr.minify_identifiers); } if (ctx.bundler_options.bake_debug_disable_minify) { @@ -287,6 +287,8 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa var root_dir = try std.fs.cwd().makeOpenPath("dist", .{}); defer root_dir.close(); + var maybe_runtime_file_index: ?u32 = null; + var css_chunks_count: usize = 0; var css_chunks_first: usize = 0; @@ -322,6 +324,16 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa } } + // The output file which contains the runtime (Index.runtime, contains + // wrapper functions like `__esm`) is marked as server side, but it is + // also used by client + if (file.bake_is_runtime) { + if (comptime bun.Environment.allow_assert) { + bun.assertf(maybe_runtime_file_index == null, "Runtime file should only be in one chunk.", .{}); + maybe_runtime_file_index = @intCast(i); + } + } + switch (file.side orelse continue) { .client => { // TODO: Maybe not do this all in 1 thread? @@ -408,6 +420,29 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa ); } } + // Write the runtime file to disk if there are any client chunks + { + const runtime_file_index = maybe_runtime_file_index orelse { + bun.Output.panic("Runtime file not found. This is an unexpected bug in Bun. Please file a bug report on GitHub.", .{}); + }; + const any_client_chunks = any_client_chunks: { + for (bundled_outputs) |file| { + if (file.side) |s| { + if (s == .client and !bun.strings.eqlComptime(file.src_path.text, "bun-framework-react/client.tsx")) { + break :any_client_chunks true; + } + } + } + break :any_client_chunks false; + }; + if (any_client_chunks) { + const runtime_file: *const OutputFile = &bundled_outputs[runtime_file_index]; + _ = runtime_file.writeToDisk(root_dir, ".") catch |err| { + bun.handleErrorReturnTrace(err, @errorReturnTrace()); + Output.err(err, "Failed to write {} to output directory", .{bun.fmt.quote(runtime_file.dest_path)}); + }; + } + } const per_thread_options: PerThread.Options = .{ .input_files = entry_points.files.keys(), diff --git a/src/bundler/linker_context/generateChunksInParallel.zig b/src/bundler/linker_context/generateChunksInParallel.zig index 0e85627b8f3c..d67a6d7737ea 100644 --- a/src/bundler/linker_context/generateChunksInParallel.zig +++ b/src/bundler/linker_context/generateChunksInParallel.zig @@ -539,6 +539,9 @@ pub fn generateChunksInParallel(c: *LinkerContext, chunks: []Chunk, comptime is_ .css => &.{}, .html => &.{}, }, + .bake_is_runtime = if (c.framework != null) brk: { + break :brk chunk.files_with_parts_in_chunk.contains(Index.runtime.get()); + } else false, })); // We want the chunk index to remain the same in `output_files` so the indices in `OutputFile.referenced_css_chunks` work @@ -581,6 +584,8 @@ const generateCompileResultForCssChunk = LinkerContext.generateCompileResultForC const generateCompileResultForHtmlChunk = LinkerContext.generateCompileResultForHtmlChunk; const generateChunk = LinkerContext.generateChunk; +const Index = bun.bundle_v2.Index; + const AutoBitSet = bun.bit_set.AutoBitSet; const ContentHasher = bun.bundle_v2.ContentHasher; From 8ab8e61346c754739339849f9197eb6a1ac96324 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Fri, 11 Jul 2025 17:13:09 -0700 Subject: [PATCH 25/59] don't include client files in static only route --- cmake/sources/ZigSources.txt | 2 + src/OutputFile.zig | 12 +- src/bake/bun-framework-react/server.tsx | 2 + src/bake/production.zig | 12 +- src/bundler/LinkerContext.zig | 1 + .../linker_context/StaticRouteVisitor.zig | 78 ++++++++++++ .../generateChunksInParallel.zig | 39 ++++-- src/js/builtins/Bake.ts | 17 +-- test/bake/dev/production.test.ts | 119 ++++++++++++++++-- 9 files changed, 251 insertions(+), 31 deletions(-) create mode 100644 src/bundler/linker_context/StaticRouteVisitor.zig diff --git a/cmake/sources/ZigSources.txt b/cmake/sources/ZigSources.txt index b937f8a0ea4c..852e5d2f6327 100644 --- a/cmake/sources/ZigSources.txt +++ b/cmake/sources/ZigSources.txt @@ -37,6 +37,7 @@ src/baby_list.zig src/bake/bake.zig src/bake/DevServer.zig src/bake/FrameworkRouter.zig +src/bake/OutputFileExtra.zig src/bake/production.zig src/base64/base64.zig src/bit_set.zig @@ -314,6 +315,7 @@ src/bundler/linker_context/postProcessJSChunk.zig src/bundler/linker_context/prepareCssAstsForChunk.zig src/bundler/linker_context/renameSymbolsInChunk.zig src/bundler/linker_context/scanImportsAndExports.zig +src/bundler/linker_context/StaticRouteVisitor.zig src/bundler/linker_context/writeOutputFilesToDisk.zig src/bundler/LinkerContext.zig src/bundler/LinkerGraph.zig diff --git a/src/OutputFile.zig b/src/OutputFile.zig index 09dba25260de..3fe20f7b484a 100644 --- a/src/OutputFile.zig +++ b/src/OutputFile.zig @@ -22,7 +22,13 @@ side: ?bun.bake.Side, entry_point_index: ?u32, referenced_css_chunks: []const Index = &.{}, source_index: Index.Optional = .none, -bake_is_runtime: bool = false, +bake_extra: BakeExtra = .{}, + +pub const BakeExtra = struct { + is_route: bool = false, + fully_static: bool = false, + bake_is_runtime: bool = false, +}; pub const Index = bun.GenericIndex(u32, OutputFile); @@ -215,7 +221,7 @@ pub const Options = struct { side: ?bun.bake.Side, entry_point_index: ?u32, referenced_css_chunks: []const Index = &.{}, - bake_is_runtime: bool = false, + bake_extra: BakeExtra = .{}, }; pub fn init(options: Options) OutputFile { @@ -250,7 +256,7 @@ pub fn init(options: Options) OutputFile { .side = options.side, .entry_point_index = options.entry_point_index, .referenced_css_chunks = options.referenced_css_chunks, - .bake_is_runtime = options.bake_is_runtime, + .bake_extra = options.bake_extra, }; } diff --git a/src/bake/bun-framework-react/server.tsx b/src/bake/bun-framework-react/server.tsx index ed4a6c1809f6..7d3e17116bc1 100644 --- a/src/bake/bun-framework-react/server.tsx +++ b/src/bake/bun-framework-react/server.tsx @@ -117,6 +117,8 @@ export async function render(request: Request, meta: Bake.RouteMetadata): Promis // function returns no files, the route is always dynamic. When building an app // to static files, all routes get pre-rendered (build failure if not possible). export async function prerender(meta: Bake.RouteMetadata) { + // meta.modules = []; + console.log("Meta", meta, "Server manifest", serverManifest); const page = getPage(meta, meta.styles); const rscPayload = renderToPipeableStream(page, serverManifest) diff --git a/src/bake/production.zig b/src/bake/production.zig index 62ce98cfae6a..c4721547dc1f 100644 --- a/src/bake/production.zig +++ b/src/bake/production.zig @@ -327,16 +327,16 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa // The output file which contains the runtime (Index.runtime, contains // wrapper functions like `__esm`) is marked as server side, but it is // also used by client - if (file.bake_is_runtime) { + if (file.bake_extra.bake_is_runtime) { if (comptime bun.Environment.allow_assert) { bun.assertf(maybe_runtime_file_index == null, "Runtime file should only be in one chunk.", .{}); maybe_runtime_file_index = @intCast(i); } } + // TODO: Maybe not do all the disk-writing in 1 thread? switch (file.side orelse continue) { .client => { - // TODO: Maybe not do this all in 1 thread? // Client-side resources will be written to disk for usage in on the client side _ = file.writeToDisk(root_dir, ".") catch |err| { bun.handleErrorReturnTrace(err, @errorReturnTrace()); @@ -645,6 +645,7 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa try route_nested_files.putIndex(global, @intCast(nav_index), file_list); try route_type_and_flags.putIndex(global, @intCast(nav_index), JSValue.jsNumberFromInt32(@bitCast(TypeAndFlags{ .type = route.type.get(), + .no_client = main_file.bake_extra.fully_static, }))); if (params_buf.items.len > 0) { @@ -1001,7 +1002,12 @@ pub export fn BakeProdSourceMap(pt: *PerThread, key: bun.String) bun.String { const TypeAndFlags = packed struct(i32) { type: u8, - unused: u24 = 0, + /// Don't inclue the runtime client code (e.g. + /// bun-framework-react/client.tsx). This is used if we know a server + /// component does not include any downstream usages of "use client" and so + /// we can omit the client code entirely. + no_client: bool = false, + unused: u23 = 0, }; const std = @import("std"); diff --git a/src/bundler/LinkerContext.zig b/src/bundler/LinkerContext.zig index 4594db6ab9e6..71e9785437ff 100644 --- a/src/bundler/LinkerContext.zig +++ b/src/bundler/LinkerContext.zig @@ -3,6 +3,7 @@ pub const LinkerContext = struct { pub const CompileResult = bundler.CompileResult; pub const OutputFileListBuilder = @import("./linker_context/OutputFileListBuilder.zig"); + pub const StaticRouteVisitor = @import("./linker_context/StaticRouteVisitor.zig"); parse_graph: *Graph = undefined, graph: LinkerGraph = undefined, diff --git a/src/bundler/linker_context/StaticRouteVisitor.zig b/src/bundler/linker_context/StaticRouteVisitor.zig new file mode 100644 index 000000000000..fbed81325ada --- /dev/null +++ b/src/bundler/linker_context/StaticRouteVisitor.zig @@ -0,0 +1,78 @@ +const StaticRouteVisitor = @This(); + +c: *LinkerContext, +cache: std.AutoArrayHashMapUnmanaged(Index.Int, bool) = .{}, + +pub fn deinit(this: *StaticRouteVisitor) void { + this.cache.deinit(bun.default_allocator); +} + +/// This the quickest, simplest, dumbest way I can think of doing this. Investigate performance. +pub fn isFullyStatic(this: *StaticRouteVisitor, entry_point_source_index: u32) bool { + const all_import_records: []const ImportRecord.List = this.c.parse_graph.ast.items(.import_records); + const referenced_source_indices: []const u32 = this.c.parse_graph.server_component_boundaries.list.items(.reference_source_index); + const use_directives: []const UseDirective = this.c.parse_graph.server_component_boundaries.list.items(.use_directive); + + return this.isFullyStaticImpl( + all_import_records, + referenced_source_indices, + use_directives, + Index.init(entry_point_source_index), + ); +} + +/// 1. Get AST for `source_index` +/// 2. Recursively traverse its imports in import records +/// 3. If any of the imports match any item in +/// `referenced_source_indices` which has `use_directive == +/// .client`, then we know `source_index` is NOT fully +/// static. +fn isFullyStaticImpl( + this: *StaticRouteVisitor, + all_import_records: []const ImportRecord.List, + referenced_source_indices: []const u32, + use_directives: []const UseDirective, + source_index: Index, +) bool { + if (this.cache.get(source_index.get())) |result| { + return result; + } + + const import_records = all_import_records[source_index.get()]; + + const result = result: { + for (import_records.sliceConst()) |*import_record| { + if (!import_record.source_index.isValid()) continue; + + // check if this import is a client boundary + for (referenced_source_indices, use_directives) |referenced_source_index, use_directive| { + if (use_directive != .client) continue; + // it's a client boundary + if (referenced_source_index == import_record.source_index.get()) break :result false; + } + + // otherwise check its children + if (!this.isFullyStaticImpl( + all_import_records, + referenced_source_indices, + use_directives, + import_record.source_index, + )) break :result false; + } + break :result true; + }; + + this.cache.put(bun.default_allocator, source_index.get(), result) catch unreachable; + + return result; +} + +const std = @import("std"); +const bun = @import("bun"); +const strings = bun.strings; +const LinkerContext = bun.bundle_v2.LinkerContext; +const Index = bun.bundle_v2.Index; + +const js_ast = bun.js_ast.UseDirective; +const ImportRecord = bun.bundle_v2.ImportRecord; +const UseDirective = bun.bundle_v2.UseDirective; diff --git a/src/bundler/linker_context/generateChunksInParallel.zig b/src/bundler/linker_context/generateChunksInParallel.zig index d67a6d7737ea..8bd45d7eca52 100644 --- a/src/bundler/linker_context/generateChunksInParallel.zig +++ b/src/bundler/linker_context/generateChunksInParallel.zig @@ -1,4 +1,8 @@ -pub fn generateChunksInParallel(c: *LinkerContext, chunks: []Chunk, comptime is_dev_server: bool) !if (is_dev_server) void else std.ArrayList(options.OutputFile) { +pub fn generateChunksInParallel( + c: *LinkerContext, + chunks: []Chunk, + comptime is_dev_server: bool, +) !if (is_dev_server) void else std.ArrayList(options.OutputFile) { const trace = bun.perf.trace("Bundler.generateChunksInParallel"); defer trace.end(); @@ -337,6 +341,8 @@ pub fn generateChunksInParallel(c: *LinkerContext, chunks: []Chunk, comptime is_ } const bundler = @as(*bun.bundle_v2.BundleV2, @fieldParentPtr("linker", c)); + var static_route_visitor = StaticRouteVisitor{ .c = c }; + defer static_route_visitor.deinit(); if (root_path.len > 0) { try c.writeOutputFilesToDisk(root_path, chunks, &output_files); @@ -507,6 +513,12 @@ pub fn generateChunksInParallel(c: *LinkerContext, chunks: []Chunk, comptime is_ else .chunk; + const side: bun.bake.Side = if (chunk.content == .css or chunk.is_browser_chunk_from_server_build) + .client + else switch (c.graph.ast.items(.target)[chunk.entry_point.source_index]) { + .browser => .client, + else => .server, + }; const chunk_index = output_files.insertForChunk(options.OutputFile.init(.{ .data = .{ .buffer = .{ @@ -524,12 +536,7 @@ pub fn generateChunksInParallel(c: *LinkerContext, chunks: []Chunk, comptime is_ .is_executable = chunk.is_executable, .source_map_index = source_map_index, .bytecode_index = bytecode_index, - .side = if (chunk.content == .css or chunk.is_browser_chunk_from_server_build) - .client - else switch (c.graph.ast.items(.target)[chunk.entry_point.source_index]) { - .browser => .client, - else => .server, - }, + .side = side, .entry_point_index = if (output_kind == .@"entry-point") chunk.entry_point.source_index - @as(u32, (if (c.framework) |fw| if (fw.server_components != null) 3 else 1 else 1)) else @@ -539,9 +546,19 @@ pub fn generateChunksInParallel(c: *LinkerContext, chunks: []Chunk, comptime is_ .css => &.{}, .html => &.{}, }, - .bake_is_runtime = if (c.framework != null) brk: { - break :brk chunk.files_with_parts_in_chunk.contains(Index.runtime.get()); - } else false, + .bake_extra = brk: { + if (c.framework == null or is_dev_server) break :brk .{}; + if (!c.framework.?.is_built_in_react) break :brk .{}; + + var extra: OutputFile.BakeExtra = .{}; + extra.bake_is_runtime = chunk.files_with_parts_in_chunk.contains(Index.runtime.get()); + if (output_kind == .@"entry-point" and side == .server) { + extra.is_route = true; + extra.fully_static = static_route_visitor.isFullyStatic(chunk.entry_point.source_index); + } + + break :brk extra; + }, })); // We want the chunk index to remain the same in `output_files` so the indices in `OutputFile.referenced_css_chunks` work @@ -577,6 +594,7 @@ const CompileResult = LinkerContext.CompileResult; const PendingPartRange = LinkerContext.PendingPartRange; const Output = bun.Output; +const OutputFile = bun.options.OutputFile; const debugPartRanges = Output.scoped(.PartRanges, true); const generateCompileResultForJSChunk = LinkerContext.generateCompileResultForJSChunk; @@ -598,3 +616,4 @@ const JSC = bun.JSC; pub const ThreadPoolLib = bun.ThreadPool; const OutputFileListBuilder = bun.bundle_v2.LinkerContext.OutputFileListBuilder; +const StaticRouteVisitor = bun.bundle_v2.LinkerContext.StaticRouteVisitor; diff --git a/src/js/builtins/Bake.ts b/src/js/builtins/Bake.ts index 458dceac77a0..296abf0040d4 100644 --- a/src/js/builtins/Bake.ts +++ b/src/js/builtins/Bake.ts @@ -40,12 +40,11 @@ export function renderRoutesForProdStatic( }); const { join: pathJoin } = require("node:path"); - const regex = /:(\w+)/g; - let loadedModules = new Array(allServerFiles.length); async function doGenerateRoute( type: number, + noClient: boolean, i: number, layouts: any[], pageModule: any, @@ -56,7 +55,7 @@ export function renderRoutesForProdStatic( $assert(callback != null && $isCallable(callback)); let client = clientEntryUrl[type]; const results = await callback({ - modules: client ? [client] : [], + modules: client && !noClient ? [client] : [], modulepreload: [], styles: styles[i], layouts, @@ -92,6 +91,7 @@ export function renderRoutesForProdStatic( function callRouteGenerator( type: number, + noClient: boolean, i: number, layouts: any[], pageModule: any, @@ -102,13 +102,14 @@ export function renderRoutesForProdStatic( throw new Error(`Missing param ${param} for route ${JSON.stringify(sourceRouteFiles[i])}`); } } - return doGenerateRoute(type, i, layouts, pageModule, params); + return doGenerateRoute(type, noClient, i, layouts, pageModule, params); } return Promise.all( files.map(async (fileList, i) => { const typeAndFlag = typeAndFlags[i]; const type = typeAndFlag & 0xff; + const noClient = (typeAndFlag & 0b100000000) !== 0; var pageModule: any, layouts: any[]; $assert(fileList.length > 0); @@ -134,14 +135,14 @@ export function renderRoutesForProdStatic( let result; if (paramGetter[Symbol.asyncIterator] != undefined) { for await (const params of paramGetter) { - result = callRouteGenerator(type, i, layouts, pageModule, params); + result = callRouteGenerator(type, noClient, i, layouts, pageModule, params); if ($isPromise(result) && $isPromisePending(result)) { await result; } } } else if (paramGetter[Symbol.iterator] != undefined) { for (const params of paramGetter) { - result = callRouteGenerator(type, i, layouts, pageModule, params); + result = callRouteGenerator(type, noClient, i, layouts, pageModule, params); if ($isPromise(result) && $isPromisePending(result)) { await result; } @@ -149,12 +150,12 @@ export function renderRoutesForProdStatic( } else { await Promise.all( paramGetter.pages.map(params => { - callRouteGenerator(type, i, layouts, pageModule, params); + callRouteGenerator(type, noClient, i, layouts, pageModule, params); }), ); } } else { - await doGenerateRoute(type, i, layouts, pageModule, null); + await doGenerateRoute(type, noClient, i, layouts, pageModule, null); } }), ); diff --git a/test/bake/dev/production.test.ts b/test/bake/dev/production.test.ts index 3bc1eb6fd9df..45a43557f744 100644 --- a/test/bake/dev/production.test.ts +++ b/test/bake/dev/production.test.ts @@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test"; import { devTest, minimalFramework, tempDirWithBakeDeps } from "../bake-harness"; import { bunEnv, bunExe } from "harness"; import path from "path"; +import { existsSync } from "fs"; /** * Production build tests @@ -303,14 +304,12 @@ export default function GettingStarted() { }); // Run the build command - should not crash even with no pages - const { exitCode, stderr } = await Bun.$`${bunExe()} build --app ./app.ts` - .cwd(dir) - .throws(false); + const { exitCode, stderr } = await Bun.$`${bunExe()} build --app ./app.ts`.cwd(dir).throws(false); // The build should complete successfully (or fail gracefully, not crash) // We're testing that it doesn't crash with the StringBuilder assertion expect(exitCode).toBeDefined(); - + // If it fails, it should be a graceful failure, not a crash if (exitCode !== 0) { expect(stderr.toString()).not.toContain("reached unreachable code"); @@ -348,12 +347,118 @@ export function Client() { }); // Run the build command - const { exitCode, stderr } = await Bun.$`${bunExe()} build --app ./src/index.tsx` - .cwd(dir) - .throws(false); + const { exitCode, stderr } = await Bun.$`${bunExe()} build --app ./src/index.tsx`.cwd(dir).throws(false); // The build should succeed - client components should support default imports expect(stderr.toString()).toBe(""); expect(exitCode).toBe(0); }); + + test("importing from react", async () => { + const dir = await tempDirWithBakeDeps("bake-production-react-import", { + "src/index.tsx": `export default { app: { framework: "react" } };`, + "pages/index.tsx": `import { useState } from 'react'; + +export default function IndexPage() { + const [count, setCount] = useState(0); + + return ( +
+ LMAOHello World + +
+ ); +}`, + "package.json": JSON.stringify({ + "name": "test-app", + "version": "1.0.0", + "devDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0", + }, + }), + }); + + // Run the build command + const { exitCode, stderr } = await Bun.$`${bunExe()} build --app ./src/index.tsx`.cwd(dir).throws(false); + + // The build should succeed - client components should support default imports + expect(stderr.toString()).toBe(""); + expect(exitCode).toBe(0); + + // Verify that the build output exists + const buildDir = path.join(dir, ".bun"); + expect(existsSync(buildDir)).toBe(true); + + // Verify the server bundle was created + const serverBundle = path.join(buildDir, "server", "pages_index.ssr.js"); + expect(existsSync(serverBundle)).toBe(true); + + // Verify the client bundle was created + const clientBundle = path.join(buildDir, "client", "_bun", "pages_index.bundle.js"); + expect(existsSync(clientBundle)).toBe(true); + + // Verify the client bundle contains React imports + const clientBundleContent = await Bun.file(clientBundle).text(); + expect(clientBundleContent).toContain("useState"); + + // Verify the generated HTML page exists + const htmlPage = path.join(dir, "dist", "index.html"); + expect(existsSync(htmlPage)).toBe(true); + + // Verify the HTML contains the expected content + const htmlContent = await Bun.file(htmlPage).text(); + expect(htmlContent).toContain("Hello World"); + expect(htmlContent).toContain("LMAO"); + }); + + test("don't include client code if fully static route", async () => { + const dir = await tempDirWithBakeDeps("bake-production-no-client-js", { + "src/index.tsx": `export default { app: { framework: "react" } };`, + "pages/index.tsx": ` +export default function IndexPage() { + return ( +
+ Hello World +
+ ); +}`, + "package.json": JSON.stringify({ + "name": "test-app", + "version": "1.0.0", + "devDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0", + }, + }), + }); + + // Run the build command + const { exitCode, stderr } = await Bun.$`${bunExe()} build --app ./src/index.tsx`.cwd(dir).throws(false); + + // The build should succeed + // expect(stderr.toString()).toBe(""); + expect(exitCode).toBe(0); + + // Check the generated HTML file + const htmlPage = path.join(dir, "dist", "index.html"); + expect(existsSync(htmlPage)).toBe(true); + + const htmlContent = await Bun.file(htmlPage).text(); + + // Verify the content is rendered + expect(htmlContent).toContain("Hello World"); + + // Verify NO JavaScript imports are included in the HTML + expect(htmlContent).not.toContain(" Date: Fri, 11 Jul 2025 17:13:47 -0700 Subject: [PATCH 26/59] yoops --- cmake/sources/ZigSources.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/cmake/sources/ZigSources.txt b/cmake/sources/ZigSources.txt index 852e5d2f6327..b371704f218e 100644 --- a/cmake/sources/ZigSources.txt +++ b/cmake/sources/ZigSources.txt @@ -37,7 +37,6 @@ src/baby_list.zig src/bake/bake.zig src/bake/DevServer.zig src/bake/FrameworkRouter.zig -src/bake/OutputFileExtra.zig src/bake/production.zig src/base64/base64.zig src/bit_set.zig From 7608393974c7e6ac1b62abd2affba57c85e93325 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Fri, 11 Jul 2025 23:29:58 -0700 Subject: [PATCH 27/59] error on using useState in server component --- src/js_parser.zig | 23 ++++++++++++++++++++-- src/runtime.zig | 9 +++++++++ test/bake/dev/production.test.ts | 33 +++++--------------------------- 3 files changed, 35 insertions(+), 30 deletions(-) diff --git a/src/js_parser.zig b/src/js_parser.zig index ea0e0510c513..2e862d97741b 100644 --- a/src/js_parser.zig +++ b/src/js_parser.zig @@ -17484,7 +17484,7 @@ fn NewParser_( // // When we see a hook call, we need to hash it, and then mark a flag so that if // it is assigned to a variable, that variable also get's hashed. - if (p.options.features.react_fast_refresh) try_record_hook: { + if (p.options.features.react_fast_refresh or p.options.features.server_components.isServerSide()) try_record_hook: { const original_name = switch (e_.target.data) { inline .e_identifier, .e_import_identifier, @@ -17494,7 +17494,26 @@ fn NewParser_( else => break :try_record_hook, }; if (!ReactRefresh.isHookName(original_name)) break :try_record_hook; - p.handleReactRefreshHookCall(e_, original_name); + if (p.options.features.react_fast_refresh) { + p.handleReactRefreshHookCall(e_, original_name); + } else { + // if we're in this branch then it is a server + // component, error if they try to use `useState` + if (bun.strings.eqlComptime(original_name, "useState") and + // this feels dumb, what's a better way to detect that it's not React? + !bun.strings.includes(p.source.path.text, "node_modules/react")) + { + p.log.addError( + p.source, + expr.loc, + std.fmt.allocPrint( + p.allocator, + "\"useState\" is not available in a server component. If you need interactivity, consider converting part of this to a Client Component.", + .{}, + ) catch bun.outOfMemory(), + ) catch bun.outOfMemory(); + } + } } // Implement constant folding for 'string'.charCodeAt(n) diff --git a/src/runtime.zig b/src/runtime.zig index 7854881b1c15..666fa8c1833d 100644 --- a/src/runtime.zig +++ b/src/runtime.zig @@ -283,6 +283,15 @@ pub const Runtime = struct { /// - Ban "use server" functions since it is on the client-side client_side, + pub fn isServerSide(mode: ServerComponentsMode) bool { + return switch (mode) { + .wrap_exports_for_server_reference, + .wrap_anon_server_functions, + => true, + else => false, + }; + } + pub fn wrapsExports(mode: ServerComponentsMode) bool { return switch (mode) { .wrap_exports_for_client_reference, diff --git a/test/bake/dev/production.test.ts b/test/bake/dev/production.test.ts index 45a43557f744..341f8de5522e 100644 --- a/test/bake/dev/production.test.ts +++ b/test/bake/dev/production.test.ts @@ -354,7 +354,7 @@ export function Client() { expect(exitCode).toBe(0); }); - test("importing from react", async () => { + test("importing useState", async () => { const dir = await tempDirWithBakeDeps("bake-production-react-import", { "src/index.tsx": `export default { app: { framework: "react" } };`, "pages/index.tsx": `import { useState } from 'react'; @@ -383,33 +383,10 @@ export default function IndexPage() { const { exitCode, stderr } = await Bun.$`${bunExe()} build --app ./src/index.tsx`.cwd(dir).throws(false); // The build should succeed - client components should support default imports - expect(stderr.toString()).toBe(""); - expect(exitCode).toBe(0); - - // Verify that the build output exists - const buildDir = path.join(dir, ".bun"); - expect(existsSync(buildDir)).toBe(true); - - // Verify the server bundle was created - const serverBundle = path.join(buildDir, "server", "pages_index.ssr.js"); - expect(existsSync(serverBundle)).toBe(true); - - // Verify the client bundle was created - const clientBundle = path.join(buildDir, "client", "_bun", "pages_index.bundle.js"); - expect(existsSync(clientBundle)).toBe(true); - - // Verify the client bundle contains React imports - const clientBundleContent = await Bun.file(clientBundle).text(); - expect(clientBundleContent).toContain("useState"); - - // Verify the generated HTML page exists - const htmlPage = path.join(dir, "dist", "index.html"); - expect(existsSync(htmlPage)).toBe(true); - - // Verify the HTML contains the expected content - const htmlContent = await Bun.file(htmlPage).text(); - expect(htmlContent).toContain("Hello World"); - expect(htmlContent).toContain("LMAO"); + expect(stderr.toString()).toContain( + '"useState" is not available in a server component. If you need interactivity, consider converting part of this to a Client Component.', + ); + expect(exitCode).toBe(1); }); test("don't include client code if fully static route", async () => { From c21e947aaf39316a873bbd4b41c8a0082903775e Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Sat, 12 Jul 2025 01:51:11 -0700 Subject: [PATCH 28/59] fix default import from client component --- src/bundler/ServerComponentParseTask.zig | 3 ++- src/js_parser.zig | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/bundler/ServerComponentParseTask.zig b/src/bundler/ServerComponentParseTask.zig index 3bd3974c1d43..a8a2fc765e21 100644 --- a/src/bundler/ServerComponentParseTask.zig +++ b/src/bundler/ServerComponentParseTask.zig @@ -185,8 +185,9 @@ fn generateClientReferenceProxy(task: *ServerComponentParseTask, data: Data.Refe }); if (is_default) { + const ref = try b.newSymbol(.other, "default"); // export default registerClientReference(...); - try b.appendStmt(S.ExportDefault{ .value = .{ .expr = value }, .default_name = .{} }); + try b.appendStmt(S.ExportDefault{ .value = .{ .expr = value }, .default_name = .{ .ref = ref } }); } else { // export const Component = registerClientReference(...); const export_ref = try b.newSymbol(.other, key); diff --git a/src/js_parser.zig b/src/js_parser.zig index 2e862d97741b..69614f59cf47 100644 --- a/src/js_parser.zig +++ b/src/js_parser.zig @@ -9017,7 +9017,7 @@ fn NewParser_( }) catch unreachable; } - // No need to add this to `item_refs` because `.scanForImportsAndExports` special cases `s + // No need to add this to `item_refs` because `.scanImportsAndExports` special cases this } var end: usize = 0; From 93431898ca9ddbb9387c6e5f4029791270da8432 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Sat, 12 Jul 2025 14:45:50 -0700 Subject: [PATCH 29/59] Fix tests --- test/bake/dev/production.test.ts | 49 ++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/test/bake/dev/production.test.ts b/test/bake/dev/production.test.ts index 341f8de5522e..bab19e1b8fba 100644 --- a/test/bake/dev/production.test.ts +++ b/test/bake/dev/production.test.ts @@ -243,16 +243,18 @@ export default function GettingStarted() { // Should have generated all the static paths // Note: React's routing may flatten the paths - expect(htmlFiles).toContain("dist/2024/hello-world/index.html"); - expect(htmlFiles).toContain("dist/2024/tech/bun-framework/index.html"); - expect(htmlFiles).toContain("dist/tutorials/getting-started/index.html"); - expect(htmlFiles).toContain("dist/api/reference/index.html"); - expect(htmlFiles).toContain("dist/guides/advanced/optimization/index.html"); - expect(htmlFiles).toContain("dist/index.html"); + expect(htmlFiles).toContain("dist/blog/2024/hello-world/index.html"); + expect(htmlFiles).toContain("dist/blog/2024/tech/bun-framework/index.html"); + expect(htmlFiles).toContain("dist/blog/tutorials/getting-started/index.html"); + expect(htmlFiles).toContain("dist/docs/api/reference/index.html"); + expect(htmlFiles).toContain("dist/docs/guides/advanced/optimization/index.html"); + expect(htmlFiles).toContain("dist/docs/index.html"); expect(htmlFiles).toContain("dist/docs/getting-started/index.html"); // Check blog post with multiple segments - const blogPostHtml = await Bun.file(path.join(dir, "dist", "2024", "tech", "bun-framework", "index.html")).text(); + const blogPostHtml = await Bun.file( + path.join(dir, "dist", "blog", "2024", "tech", "bun-framework", "index.html"), + ).text(); // Verify the content is rendered (may include HTML comments) expect(blogPostHtml).toContain("Blog Post:"); @@ -268,7 +270,9 @@ export default function GettingStarted() { expect(blogPostHtml).toContain('/pages/blog/[...slug].tsx"'); // Check docs catch-all route - const docsHtml = await Bun.file(path.join(dir, "dist", "guides", "advanced", "optimization", "index.html")).text(); + const docsHtml = await Bun.file( + path.join(dir, "dist", "docs", "guides", "advanced", "optimization", "index.html"), + ).text(); expect(docsHtml).toContain("Reading docs at:"); expect(docsHtml).toContain("guides/advanced/optimization"); @@ -285,7 +289,9 @@ export default function GettingStarted() { expect(staticHtml).not.toContain("[...path].tsx"); // Verify that import.meta values are consistent across all catch-all instances - const blogIndex = await Bun.file(path.join(dir, "dist", "tutorials", "getting-started", "index.html")).text(); + const blogIndex = await Bun.file( + path.join(dir, "dist", "blog", "tutorials", "getting-started", "index.html"), + ).text(); expect(blogIndex).toContain('data-file="[...slug].tsx"'); expect(blogIndex).toContain('/pages/blog/[...slug].tsx"'); }); @@ -332,7 +338,7 @@ export default function IndexPage() { }`, "components/Client.tsx": `"use client"; -export function Client() { +export default function Client() { console.log("Client-side!"); return
Hello World
; }`, @@ -349,9 +355,17 @@ export function Client() { // Run the build command const { exitCode, stderr } = await Bun.$`${bunExe()} build --app ./src/index.tsx`.cwd(dir).throws(false); - // The build should succeed - client components should support default imports - expect(stderr.toString()).toBe(""); expect(exitCode).toBe(0); + + // Check the generated HTML file for pages/index.tsx + const htmlPage = path.join(dir, "dist", "index.html"); + expect(existsSync(htmlPage)).toBe(true); + + const htmlContent = await Bun.file(htmlPage).text(); + + // Verify the static content is rendered + expect(htmlContent).toContain("LMAO"); + expect(htmlContent).toContain("Hello World"); }); test("importing useState", async () => { @@ -427,15 +441,6 @@ export default function IndexPage() { expect(htmlContent).toContain("Hello World"); // Verify NO JavaScript imports are included in the HTML - expect(htmlContent).not.toContain(" Date: Sat, 12 Jul 2025 15:08:58 -0700 Subject: [PATCH 30/59] don't include that --- src/bake/bun-framework-react/server.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/bake/bun-framework-react/server.tsx b/src/bake/bun-framework-react/server.tsx index 7d3e17116bc1..ed4a6c1809f6 100644 --- a/src/bake/bun-framework-react/server.tsx +++ b/src/bake/bun-framework-react/server.tsx @@ -117,8 +117,6 @@ export async function render(request: Request, meta: Bake.RouteMetadata): Promis // function returns no files, the route is always dynamic. When building an app // to static files, all routes get pre-rendered (build failure if not possible). export async function prerender(meta: Bake.RouteMetadata) { - // meta.modules = []; - console.log("Meta", meta, "Server manifest", serverManifest); const page = getPage(meta, meta.styles); const rscPayload = renderToPipeableStream(page, serverManifest) From a8604f57b730b9636356a31ac91ebfb6f715140d Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Sat, 12 Jul 2025 16:26:57 -0700 Subject: [PATCH 31/59] fuck --- src/bake/production.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bake/production.zig b/src/bake/production.zig index c4721547dc1f..a078196e6e87 100644 --- a/src/bake/production.zig +++ b/src/bake/production.zig @@ -330,8 +330,8 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa if (file.bake_extra.bake_is_runtime) { if (comptime bun.Environment.allow_assert) { bun.assertf(maybe_runtime_file_index == null, "Runtime file should only be in one chunk.", .{}); - maybe_runtime_file_index = @intCast(i); } + maybe_runtime_file_index = @intCast(i); } // TODO: Maybe not do all the disk-writing in 1 thread? From 901b66b7da1c577318d02b84fab03ac599deda2d Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Sat, 12 Jul 2025 20:11:37 -0700 Subject: [PATCH 32/59] stuff --- .../linker_context/StaticRouteVisitor.zig | 11 +++ .../generateChunksInParallel.zig | 2 +- test/bake/dev/production.test.ts | 84 ++++++++++++++++++- 3 files changed, 95 insertions(+), 2 deletions(-) diff --git a/src/bundler/linker_context/StaticRouteVisitor.zig b/src/bundler/linker_context/StaticRouteVisitor.zig index fbed81325ada..f4e051603ebe 100644 --- a/src/bundler/linker_context/StaticRouteVisitor.zig +++ b/src/bundler/linker_context/StaticRouteVisitor.zig @@ -1,10 +1,16 @@ +//! The `isFullyStatic(source_index)` function returns whether or not +//! `source_index` imports a file with `"use client"`. +//! +//! TODO: Could we move this into the ReachableFileVisitor inside `bundle_v2.zig`? const StaticRouteVisitor = @This(); c: *LinkerContext, cache: std.AutoArrayHashMapUnmanaged(Index.Int, bool) = .{}, +visited: bun.bit_set.AutoBitSet, pub fn deinit(this: *StaticRouteVisitor) void { this.cache.deinit(bun.default_allocator); + this.visited.deinit(bun.default_allocator); } /// This the quickest, simplest, dumbest way I can think of doing this. Investigate performance. @@ -34,6 +40,11 @@ fn isFullyStaticImpl( use_directives: []const UseDirective, source_index: Index, ) bool { + if (this.visited.isSet(source_index.get())) { + return false; + } + this.visited.set(source_index.get()); + if (this.cache.get(source_index.get())) |result| { return result; } diff --git a/src/bundler/linker_context/generateChunksInParallel.zig b/src/bundler/linker_context/generateChunksInParallel.zig index 8bd45d7eca52..e9c9559680a9 100644 --- a/src/bundler/linker_context/generateChunksInParallel.zig +++ b/src/bundler/linker_context/generateChunksInParallel.zig @@ -341,7 +341,7 @@ pub fn generateChunksInParallel( } const bundler = @as(*bun.bundle_v2.BundleV2, @fieldParentPtr("linker", c)); - var static_route_visitor = StaticRouteVisitor{ .c = c }; + var static_route_visitor = StaticRouteVisitor{ .c = c, .visited = bun.bit_set.AutoBitSet.initEmpty(bun.default_allocator, c.graph.files.len) catch unreachable }; defer static_route_visitor.deinit(); if (root_path.len > 0) { diff --git a/test/bake/dev/production.test.ts b/test/bake/dev/production.test.ts index bab19e1b8fba..6260a86c3427 100644 --- a/test/bake/dev/production.test.ts +++ b/test/bake/dev/production.test.ts @@ -368,7 +368,7 @@ export default function Client() { expect(htmlContent).toContain("Hello World"); }); - test("importing useState", async () => { + test("importing useState server-side", async () => { const dir = await tempDirWithBakeDeps("bake-production-react-import", { "src/index.tsx": `export default { app: { framework: "react" } };`, "pages/index.tsx": `import { useState } from 'react'; @@ -403,6 +403,88 @@ export default function IndexPage() { expect(exitCode).toBe(1); }); + test("importing useState from client component", async () => { + const dir = await tempDirWithBakeDeps("bake-production-client-useState", { + "src/index.tsx": `export default { app: { framework: "react" } };`, + "pages/index.tsx": `import Counter from "../components/Counter"; + +export default function IndexPage() { + return ( +
+

Counter Example

+ +
+ ); +}`, + "components/Counter.tsx": `"use client"; +import { useState } from 'react'; + +export default function Counter() { + const [count, setCount] = useState(0); + + return ( +
+

Count: {count}

+ +
+ ); +}`, + "package.json": JSON.stringify({ + "name": "test-app", + "version": "1.0.0", + "devDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0", + }, + }), + }); + + // Run the build command + const { exitCode, stderr } = await Bun.$`${bunExe()} build --app ./src/index.tsx`.cwd(dir).throws(false); + + // The build should succeed - client components CAN use useState + expect(stderr.toString()).not.toContain("useState"); + expect(exitCode).toBe(0); + + // Check the generated HTML file + const htmlPage = path.join(dir, "dist", "index.html"); + expect(existsSync(htmlPage)).toBe(true); + + const htmlContent = await Bun.file(htmlPage).text(); + + // Verify the static content is rendered + expect(htmlContent).toContain("

Counter Example

"); + + // Verify client component script tags exist + expect(htmlContent).toContain(" { const dir = await tempDirWithBakeDeps("bake-production-no-client-js", { "src/index.tsx": `export default { app: { framework: "react" } };`, From 2a3c283a43b70b75bfa210a5bc287e24969aced6 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Mon, 14 Jul 2025 15:17:19 -0700 Subject: [PATCH 33/59] stuff --- src/OutputFile.zig | 9 +++++++++ src/bake/BakeGlobalObject.cpp | 5 +++++ src/bundler/linker_context/OutputFileListBuilder.zig | 3 ++- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/OutputFile.zig b/src/OutputFile.zig index 3fe20f7b484a..bb01b0936ba0 100644 --- a/src/OutputFile.zig +++ b/src/OutputFile.zig @@ -24,6 +24,15 @@ referenced_css_chunks: []const Index = &.{}, source_index: Index.Optional = .none, bake_extra: BakeExtra = .{}, +pub const zero_value = OutputFile{ + .loader = .file, + .src_path = Fs.Path.init(""), + .value = .noop, + .output_kind = .chunk, + .side = null, + .entry_point_index = null, +}; + pub const BakeExtra = struct { is_route: bool = false, fully_static: bool = false, diff --git a/src/bake/BakeGlobalObject.cpp b/src/bake/BakeGlobalObject.cpp index 98766dd13bf3..a383df99cf92 100644 --- a/src/bake/BakeGlobalObject.cpp +++ b/src/bake/BakeGlobalObject.cpp @@ -246,6 +246,11 @@ extern "C" GlobalObject* BakeCreateProdGlobal(void* console) global->setStackTraceLimit(10); // Node.js defaults to 10 global->isThreadLocalDefaultGlobalObject = true; + // if (shouldDisableStopIfNecessaryTimer) { + vm.heap.disableStopIfNecessaryTimer(); + // } + + // if you process.nextTick on a microtask we need thsi // TODO: it segfaults! process.nextTick is scoped out for now i guess! // vm.setOnComputeErrorInfo(computeErrorInfoWrapper); // vm.setOnEachMicrotaskTick([global](JSC::VM &vm) -> void { diff --git a/src/bundler/linker_context/OutputFileListBuilder.zig b/src/bundler/linker_context/OutputFileListBuilder.zig index 022ccdc4002d..8c675a62a17e 100644 --- a/src/bundler/linker_context/OutputFileListBuilder.zig +++ b/src/bundler/linker_context/OutputFileListBuilder.zig @@ -46,7 +46,7 @@ pub fn init( allocator, length, ); - output_files.items.len = length; + output_files.appendNTimesAssumeCapacity(OutputFile.zero_value, length); return .{ .output_files = output_files, @@ -147,3 +147,4 @@ fn indexForSourcemapOrBytecode(this: *@This()) ?u32 { const std = @import("std"); const bun = @import("bun"); const options = bun.options; +const OutputFile = options.OutputFile; From 498e31373728af7f91338f1bed93469c497f9bf1 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Mon, 14 Jul 2025 15:17:33 -0700 Subject: [PATCH 34/59] stuff --- src/bundler/linker_context/StaticRouteVisitor.zig | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/bundler/linker_context/StaticRouteVisitor.zig b/src/bundler/linker_context/StaticRouteVisitor.zig index f4e051603ebe..6272246929e3 100644 --- a/src/bundler/linker_context/StaticRouteVisitor.zig +++ b/src/bundler/linker_context/StaticRouteVisitor.zig @@ -40,15 +40,14 @@ fn isFullyStaticImpl( use_directives: []const UseDirective, source_index: Index, ) bool { + if (this.cache.get(source_index.get())) |result| { + return result; + } if (this.visited.isSet(source_index.get())) { return false; } this.visited.set(source_index.get()); - if (this.cache.get(source_index.get())) |result| { - return result; - } - const import_records = all_import_records[source_index.get()]; const result = result: { From 39cf84533dc556916f118d70929cc3e0d54867b0 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Mon, 14 Jul 2025 16:11:08 -0700 Subject: [PATCH 35/59] Better name --- src/bundler/linker_context/StaticRouteVisitor.zig | 8 ++++---- src/bundler/linker_context/generateChunksInParallel.zig | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/bundler/linker_context/StaticRouteVisitor.zig b/src/bundler/linker_context/StaticRouteVisitor.zig index 6272246929e3..2c5818888018 100644 --- a/src/bundler/linker_context/StaticRouteVisitor.zig +++ b/src/bundler/linker_context/StaticRouteVisitor.zig @@ -14,12 +14,12 @@ pub fn deinit(this: *StaticRouteVisitor) void { } /// This the quickest, simplest, dumbest way I can think of doing this. Investigate performance. -pub fn isFullyStatic(this: *StaticRouteVisitor, entry_point_source_index: u32) bool { +pub fn hasTransitiveUseClient(this: *StaticRouteVisitor, entry_point_source_index: u32) bool { const all_import_records: []const ImportRecord.List = this.c.parse_graph.ast.items(.import_records); const referenced_source_indices: []const u32 = this.c.parse_graph.server_component_boundaries.list.items(.reference_source_index); const use_directives: []const UseDirective = this.c.parse_graph.server_component_boundaries.list.items(.use_directive); - return this.isFullyStaticImpl( + return this.hasTransitiveUseClientImpl( all_import_records, referenced_source_indices, use_directives, @@ -33,7 +33,7 @@ pub fn isFullyStatic(this: *StaticRouteVisitor, entry_point_source_index: u32) b /// `referenced_source_indices` which has `use_directive == /// .client`, then we know `source_index` is NOT fully /// static. -fn isFullyStaticImpl( +fn hasTransitiveUseClientImpl( this: *StaticRouteVisitor, all_import_records: []const ImportRecord.List, referenced_source_indices: []const u32, @@ -62,7 +62,7 @@ fn isFullyStaticImpl( } // otherwise check its children - if (!this.isFullyStaticImpl( + if (!this.hasTransitiveUseClientImpl( all_import_records, referenced_source_indices, use_directives, diff --git a/src/bundler/linker_context/generateChunksInParallel.zig b/src/bundler/linker_context/generateChunksInParallel.zig index e9c9559680a9..344c249004c9 100644 --- a/src/bundler/linker_context/generateChunksInParallel.zig +++ b/src/bundler/linker_context/generateChunksInParallel.zig @@ -554,7 +554,7 @@ pub fn generateChunksInParallel( extra.bake_is_runtime = chunk.files_with_parts_in_chunk.contains(Index.runtime.get()); if (output_kind == .@"entry-point" and side == .server) { extra.is_route = true; - extra.fully_static = static_route_visitor.isFullyStatic(chunk.entry_point.source_index); + extra.fully_static = !static_route_visitor.hasTransitiveUseClient(chunk.entry_point.source_index); } break :brk extra; From 49db94d26d1806182eb95ea7923301d8b95397ed Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Mon, 14 Jul 2025 16:12:24 -0700 Subject: [PATCH 36/59] yoops --- src/bundler/linker_context/StaticRouteVisitor.zig | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/bundler/linker_context/StaticRouteVisitor.zig b/src/bundler/linker_context/StaticRouteVisitor.zig index 2c5818888018..994b5b7a2a86 100644 --- a/src/bundler/linker_context/StaticRouteVisitor.zig +++ b/src/bundler/linker_context/StaticRouteVisitor.zig @@ -58,7 +58,7 @@ fn hasTransitiveUseClientImpl( for (referenced_source_indices, use_directives) |referenced_source_index, use_directive| { if (use_directive != .client) continue; // it's a client boundary - if (referenced_source_index == import_record.source_index.get()) break :result false; + if (referenced_source_index == import_record.source_index.get()) break :result true; } // otherwise check its children @@ -67,9 +67,9 @@ fn hasTransitiveUseClientImpl( referenced_source_indices, use_directives, import_record.source_index, - )) break :result false; + )) break :result true; } - break :result true; + break :result false; }; this.cache.put(bun.default_allocator, source_index.get(), result) catch unreachable; From 82f5acdfebc650710f56ed4e3f4e252bf52c208a Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Mon, 14 Jul 2025 16:12:38 -0700 Subject: [PATCH 37/59] yoops --- src/bundler/linker_context/StaticRouteVisitor.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bundler/linker_context/StaticRouteVisitor.zig b/src/bundler/linker_context/StaticRouteVisitor.zig index 994b5b7a2a86..983a44ab1c05 100644 --- a/src/bundler/linker_context/StaticRouteVisitor.zig +++ b/src/bundler/linker_context/StaticRouteVisitor.zig @@ -62,7 +62,7 @@ fn hasTransitiveUseClientImpl( } // otherwise check its children - if (!this.hasTransitiveUseClientImpl( + if (this.hasTransitiveUseClientImpl( all_import_records, referenced_source_indices, use_directives, From 1b55912e3860f282de6b7f5b6c86b7c52e5bea2e Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Mon, 14 Jul 2025 16:40:44 -0700 Subject: [PATCH 38/59] comment --- src/bundler/linker_context/StaticRouteVisitor.zig | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/bundler/linker_context/StaticRouteVisitor.zig b/src/bundler/linker_context/StaticRouteVisitor.zig index 983a44ab1c05..392d5ad01409 100644 --- a/src/bundler/linker_context/StaticRouteVisitor.zig +++ b/src/bundler/linker_context/StaticRouteVisitor.zig @@ -13,7 +13,9 @@ pub fn deinit(this: *StaticRouteVisitor) void { this.visited.deinit(bun.default_allocator); } -/// This the quickest, simplest, dumbest way I can think of doing this. Investigate performance. +/// This the quickest, simplest, dumbest way I can think of doing this. +/// Investigate performance. It can have false negatives (it doesn't properly +/// handle cycles), but that's okay as it's just used an optimization pub fn hasTransitiveUseClient(this: *StaticRouteVisitor, entry_point_source_index: u32) bool { const all_import_records: []const ImportRecord.List = this.c.parse_graph.ast.items(.import_records); const referenced_source_indices: []const u32 = this.c.parse_graph.server_component_boundaries.list.items(.reference_source_index); From be424e5cd35dfdf504c6235e45fc6e1d02ff4368 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Mon, 14 Jul 2025 22:42:23 -0700 Subject: [PATCH 39/59] fix crash --- src/bundler/linker_context/writeOutputFilesToDisk.zig | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/bundler/linker_context/writeOutputFilesToDisk.zig b/src/bundler/linker_context/writeOutputFilesToDisk.zig index bed21e54387e..a973142ebd2e 100644 --- a/src/bundler/linker_context/writeOutputFilesToDisk.zig +++ b/src/bundler/linker_context/writeOutputFilesToDisk.zig @@ -348,7 +348,9 @@ pub fn writeOutputFilesToDisk( } { - for (c.parse_graph.additional_output_files.items, output_files.getMutableAdditionalOutputFiles()) |*src, *dest| { + const additional_output_files = output_files.getMutableAdditionalOutputFiles(); + output_files.total_insertions += @intCast(additional_output_files.len); + for (c.parse_graph.additional_output_files.items, additional_output_files) |*src, *dest| { const bytes = src.value.buffer.bytes; src.value.buffer.bytes.len = 0; From b298cc8299e2b00e5826816c619a01f87b2474ec Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Tue, 15 Jul 2025 21:27:39 -0700 Subject: [PATCH 40/59] lmao --- src/bake/production.zig | 7 +++++- src/bun.js/bindings/ZigGlobalObject.cpp | 3 +++ .../linker_context/StaticRouteVisitor.zig | 4 ++++ src/js/builtins/Bake.ts | 24 +++++++++++-------- 4 files changed, 27 insertions(+), 11 deletions(-) diff --git a/src/bake/production.zig b/src/bake/production.zig index a078196e6e87..9a5c6c928b9a 100644 --- a/src/bake/production.zig +++ b/src/bake/production.zig @@ -302,7 +302,7 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa var source_maps: bun.StringArrayHashMapUnmanaged(OutputFile.Index) = .{}; @memset(module_keys, bun.String.dead); for (bundled_outputs, 0..) |file, i| { - log("src_index={any} {s} - {s} : {s} - {?d}\n", .{ + log("src_index={any} side={s} src={s} dest={s} - {?d}\n", .{ file.source_index.unwrap(), if (file.side) |s| @tagName(s) else "null", file.src_path.text, @@ -388,6 +388,8 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa } } + log(" adding module map entry: output_module_map(bake:/{s}) = {d}\n", .{ without_prefix, i }); + try output_module_map.put( allocator, try std.fmt.allocPrint(allocator, "bake:/{s}", .{without_prefix}), @@ -696,6 +698,7 @@ fn loadModule(vm: *VirtualMachine, global: *JSC.JSGlobalObject, key: JSValue) !J const promise = BakeLoadModuleByKey(global, key).asAnyPromise().?.internal; promise.setHandled(vm.jsc); vm.waitForPromise(.{ .internal = promise }); + vm.eventLoop().drainMicrotasks() catch unreachable; switch (promise.unwrap(vm.jsc, .mark_handled)) { .pending => unreachable, .fulfilled => |val| { @@ -983,7 +986,9 @@ pub export fn BakeProdLoad(pt: *PerThread, key: bun.String) bun.String { const allocator = sfa.get(); const utf8 = key.toUTF8(allocator); defer utf8.deinit(); + log("BakeProdLoad: {s}\n", .{utf8.slice()}); if (pt.module_map.get(utf8.slice())) |value| { + log(" found in module_map: {s}\n", .{utf8.slice()}); return pt.bundled_outputs[value.get()].value.toBunString(); } return bun.String.dead; diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index d69a6da4f63b..533c6eafe1cb 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -1007,6 +1007,7 @@ extern "C" JSC::JSGlobalObject* Zig__GlobalObject__create(void* console_client, vm.setOnComputeErrorInfo(computeErrorInfoWrapperToString); vm.setOnComputeErrorInfoJSValue(computeErrorInfoWrapperToJSValue); vm.setOnEachMicrotaskTick([](JSC::VM& vm) -> void { + // if you process.nextTick on a microtask we need this auto* globalObject = defaultGlobalObject(); if (auto queue = globalObject->m_nextTickQueue.get()) { globalObject->resetOnEachMicrotaskTick(); @@ -4134,6 +4135,7 @@ JSC::JSInternalPromise* GlobalObject::moduleLoaderImportModule(JSGlobalObject* j JSValue parameters, const SourceOrigin& sourceOrigin) { + printf("GlobalObject::moduleLoaderImportModule: %s, %s\n", moduleNameValue->getString(jsGlobalObject).utf8().data(), sourceOrigin.url().string().utf8().data()); auto* globalObject = static_cast(jsGlobalObject); if (JSC::JSInternalPromise* result = NodeVM::importModule(globalObject, moduleNameValue, parameters, sourceOrigin)) { @@ -4272,6 +4274,7 @@ JSC::JSInternalPromise* GlobalObject::moduleLoaderFetch(JSGlobalObject* globalOb auto moduleKeyJS = key.toString(globalObject); RETURN_IF_EXCEPTION(scope, {}); auto moduleKey = moduleKeyJS->value(globalObject); + printf("GlobalObject::moduleLoaderFetch: %s\n", moduleKey->utf8().data()); if (scope.exception()) [[unlikely]] return rejectedInternalPromise(globalObject, scope.exception()->value()); diff --git a/src/bundler/linker_context/StaticRouteVisitor.zig b/src/bundler/linker_context/StaticRouteVisitor.zig index 392d5ad01409..b8b23e1568a9 100644 --- a/src/bundler/linker_context/StaticRouteVisitor.zig +++ b/src/bundler/linker_context/StaticRouteVisitor.zig @@ -17,6 +17,10 @@ pub fn deinit(this: *StaticRouteVisitor) void { /// Investigate performance. It can have false negatives (it doesn't properly /// handle cycles), but that's okay as it's just used an optimization pub fn hasTransitiveUseClient(this: *StaticRouteVisitor, entry_point_source_index: u32) bool { + if (bun.Environment.isDebug and bun.getenvZ("BUN_SSG_DISABLE_STATIC_ROUTE_VISITOR") != null) { + return false; + } + const all_import_records: []const ImportRecord.List = this.c.parse_graph.ast.items(.import_records); const referenced_source_indices: []const u32 = this.c.parse_graph.server_component_boundaries.list.items(.reference_source_index); const use_directives: []const UseDirective = this.c.parse_graph.server_component_boundaries.list.items(.use_directive); diff --git a/src/js/builtins/Bake.ts b/src/js/builtins/Bake.ts index 296abf0040d4..d959243ee420 100644 --- a/src/js/builtins/Bake.ts +++ b/src/js/builtins/Bake.ts @@ -11,7 +11,7 @@ type FileIndex = number; * This layer is implemented in JavaScript to reduce Native <-> JS context switches, * as well as use the async primitives provided by the language. */ -export function renderRoutesForProdStatic( +export async function renderRoutesForProdStatic( outBase: string, allServerFiles: string[], // Indexed by router type index @@ -105,25 +105,29 @@ export function renderRoutesForProdStatic( return doGenerateRoute(type, noClient, i, layouts, pageModule, params); } - return Promise.all( + const modulesForFiles = await Promise.all( files.map(async (fileList, i) => { - const typeAndFlag = typeAndFlags[i]; - const type = typeAndFlag & 0xff; - const noClient = (typeAndFlag & 0b100000000) !== 0; - - var pageModule: any, layouts: any[]; $assert(fileList.length > 0); if (fileList.length > 1) { let anyPromise = false; let loaded = fileList.map( x => loadedModules[x] ?? ((anyPromise = true), import(allServerFiles[x]).then(x => (loadedModules[x] = x))), ); - [pageModule, ...layouts] = anyPromise ? await Promise.all(loaded) : loaded; + return anyPromise ? await Promise.all(loaded) : loaded; } else { const id = fileList[0]; - pageModule = loadedModules[id] ?? (loadedModules[id] = await import(allServerFiles[id])); - layouts = []; + return [loadedModules[id] ?? (loadedModules[id] = await import(allServerFiles[id]))]; } + }), + ); + + return Promise.all( + modulesForFiles.map(async (modules, i) => { + const typeAndFlag = typeAndFlags[i]; + const type = typeAndFlag & 0xff; + const noClient = (typeAndFlag & 0b100000000) !== 0; + + let [pageModule, ...layouts] = modules; if (paramInformation[i] != null) { const getParam = getParams[type]; From 913d48c4944a3a93070b8c0af34b372d88562b97 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Wed, 16 Jul 2025 14:07:02 -0700 Subject: [PATCH 41/59] damn --- src/js/builtins/Bake.ts | 48 ++++++++++++++++++++++++++++------------- 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/src/js/builtins/Bake.ts b/src/js/builtins/Bake.ts index d959243ee420..63e93f46b315 100644 --- a/src/js/builtins/Bake.ts +++ b/src/js/builtins/Bake.ts @@ -105,21 +105,39 @@ export async function renderRoutesForProdStatic( return doGenerateRoute(type, noClient, i, layouts, pageModule, params); } - const modulesForFiles = await Promise.all( - files.map(async (fileList, i) => { - $assert(fileList.length > 0); - if (fileList.length > 1) { - let anyPromise = false; - let loaded = fileList.map( - x => loadedModules[x] ?? ((anyPromise = true), import(allServerFiles[x]).then(x => (loadedModules[x] = x))), - ); - return anyPromise ? await Promise.all(loaded) : loaded; - } else { - const id = fileList[0]; - return [loadedModules[id] ?? (loadedModules[id] = await import(allServerFiles[id]))]; - } - }), - ); + console.time("modulesForFiles"); + // const modulesForFiles = await Promise.all( + // files.map(async (fileList, i) => { + // $assert(fileList.length > 0); + // if (fileList.length > 1) { + // let anyPromise = false; + // // let loaded = fileList.map( + // // x => loadedModules[x] ?? ((anyPromise = true), import(allServerFiles[x]).then(x => (loadedModules[x] = x))), + // // ); + // anyPromise = true; + // let loaded = fileList.map(x => import(allServerFiles[x]).then(x => (loadedModules[x] = x))); + // return anyPromise ? await Promise.all(loaded) : loaded; + // } else { + // const id = fileList[0]; + // return [loadedModules[id] ?? (loadedModules[id] = await import(allServerFiles[id]))]; + // } + // }), + // ); + let modulesForFiles = []; + for (const fileList of files) { + $assert(fileList.length > 0); + if (fileList.length > 1) { + let anyPromise = false; + let loaded = fileList.map( + x => loadedModules[x] ?? ((anyPromise = true), import(allServerFiles[x]).then(x => (loadedModules[x] = x))), + ); + modulesForFiles.push(anyPromise ? await Promise.all(loaded) : loaded); + } else { + const id = fileList[0]; + modulesForFiles.push([loadedModules[id] ?? (loadedModules[id] = await import(allServerFiles[id]))]); + } + } + console.timeEnd("modulesForFiles"); return Promise.all( modulesForFiles.map(async (modules, i) => { From 28030d778ac28ab742f038b1fbf1dd7efdde4597 Mon Sep 17 00:00:00 2001 From: zackradisic <56137411+zackradisic@users.noreply.github.com> Date: Thu, 17 Jul 2025 05:20:54 +0000 Subject: [PATCH 42/59] `bun scripts/glob-sources.mjs` --- cmake/sources/ZigSources.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/cmake/sources/ZigSources.txt b/cmake/sources/ZigSources.txt index 4f52f85ccaf7..acffd1acac2c 100644 --- a/cmake/sources/ZigSources.txt +++ b/cmake/sources/ZigSources.txt @@ -121,8 +121,6 @@ src/bun.js/bindings/Exception.zig src/bun.js/bindings/FetchHeaders.zig src/bun.js/bindings/FFI.zig src/bun.js/bindings/generated_classes_list.zig -src/bun.js/bindings/GeneratedBindings.zig -src/bun.js/bindings/GeneratedJS2Native.zig src/bun.js/bindings/GetterSetter.zig src/bun.js/bindings/HTTPServerAgent.zig src/bun.js/bindings/JSArray.zig From 4def9fa33b0f3968e2bf1cbb855bd2bfdee73ef1 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 17 Jul 2025 23:23:29 +0000 Subject: [PATCH 43/59] [autofix.ci] apply automated fixes --- src/bundler/linker_context/StaticRouteVisitor.zig | 2 -- test/bake/dev/import-meta-inline-negative.test.ts | 6 +++--- test/bake/dev/production.test.ts | 8 ++++---- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/bundler/linker_context/StaticRouteVisitor.zig b/src/bundler/linker_context/StaticRouteVisitor.zig index b8b23e1568a9..758ec32f9e72 100644 --- a/src/bundler/linker_context/StaticRouteVisitor.zig +++ b/src/bundler/linker_context/StaticRouteVisitor.zig @@ -85,10 +85,8 @@ fn hasTransitiveUseClientImpl( const std = @import("std"); const bun = @import("bun"); -const strings = bun.strings; const LinkerContext = bun.bundle_v2.LinkerContext; const Index = bun.bundle_v2.Index; -const js_ast = bun.js_ast.UseDirective; const ImportRecord = bun.bundle_v2.ImportRecord; const UseDirective = bun.bundle_v2.UseDirective; diff --git a/test/bake/dev/import-meta-inline-negative.test.ts b/test/bake/dev/import-meta-inline-negative.test.ts index 59579b227ac2..599b9bd17686 100644 --- a/test/bake/dev/import-meta-inline-negative.test.ts +++ b/test/bake/dev/import-meta-inline-negative.test.ts @@ -27,14 +27,14 @@ test("import.meta properties are NOT inlined without bake framework", async () = expect(exitCode).toBe(0); expect(stderr).toBe(""); - + // When not bundled, these properties should resolve to actual values expect(stdout).toContain("dir:"); expect(stdout).toContain("dirname:"); expect(stdout).toContain("file:"); expect(stdout).toContain("path:"); expect(stdout).toContain("url:"); - + // The values should NOT be inlined - they should be the actual runtime values expect(stdout).not.toContain("undefined"); -}); \ No newline at end of file +}); diff --git a/test/bake/dev/production.test.ts b/test/bake/dev/production.test.ts index 6260a86c3427..49fb9feebcd5 100644 --- a/test/bake/dev/production.test.ts +++ b/test/bake/dev/production.test.ts @@ -1,8 +1,8 @@ import { describe, expect, test } from "bun:test"; -import { devTest, minimalFramework, tempDirWithBakeDeps } from "../bake-harness"; +import { existsSync } from "fs"; import { bunEnv, bunExe } from "harness"; import path from "path"; -import { existsSync } from "fs"; +import { tempDirWithBakeDeps } from "../bake-harness"; /** * Production build tests @@ -471,7 +471,7 @@ export default function Counter() { // Also check for component-specific bundle by looking for all JS files const bundles = await Bun.$`ls ${path.join(dir, "dist", "_bun")}/*.js`.cwd(dir).text(); const bundleFiles = bundles.trim().split("\n").filter(Boolean); - + // Read all bundles to find the one with our component code let foundCounterBundle = false; for (const bundleFile of bundleFiles) { @@ -481,7 +481,7 @@ export default function Counter() { break; } } - + expect(foundCounterBundle).toBe(true); }); From 50053b6b299905ac3352186559bff25e0eb91e0b Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Thu, 17 Jul 2025 16:54:16 -0700 Subject: [PATCH 44/59] resolve comments --- src/bake/BakeGlobalObject.cpp | 2 +- src/bake/production.zig | 68 ++++++++++++++++++----------------- src/js/builtins/Bake.ts | 19 ---------- src/js_parser.zig | 6 ++-- src/sourcemap/sourcemap.zig | 7 ++-- 5 files changed, 44 insertions(+), 58 deletions(-) diff --git a/src/bake/BakeGlobalObject.cpp b/src/bake/BakeGlobalObject.cpp index a383df99cf92..2a3805e523c9 100644 --- a/src/bake/BakeGlobalObject.cpp +++ b/src/bake/BakeGlobalObject.cpp @@ -109,7 +109,7 @@ static JSC::JSInternalPromise* resolvedInternalPromise(JSC::JSGlobalObject* glob extern "C" BunString BakeProdLoad(void* perThreadData, BunString a); -extern "C" bool BakeGlobalObject__isBaked(JSC::JSGlobalObject* global) +extern "C" bool BakeGlobalObject__isBakeGlobalObject(JSC::JSGlobalObject* global) { return global->JSCell::inherits(Bake::GlobalObject::info()); } diff --git a/src/bake/production.zig b/src/bake/production.zig index 76dbd39f26e2..00349c7f77a9 100644 --- a/src/bake/production.zig +++ b/src/bake/production.zig @@ -110,6 +110,33 @@ pub fn buildCommand(ctx: bun.CLI.Command.Context) !void { }; } +pub fn writeSourcemapToDisk( + allocator: std.mem.Allocator, + file: *const OutputFile, + bundled_outputs: []const OutputFile, + source_maps: *bun.StringArrayHashMapUnmanaged(OutputFile.Index), +) !void { + // don't call this if the file does not have sourcemaps! + bun.assert(file.source_map_index != std.math.maxInt(u32)); + + // TODO: should we just write the sourcemaps to disk? + const source_map_index = file.source_map_index; + const source_map_file: *const OutputFile = &bundled_outputs[source_map_index]; + bun.assert(source_map_file.output_kind == .sourcemap); + + const without_prefix = if (bun.strings.hasPrefixComptime(file.dest_path, "./") or + (Environment.isWindows and bun.strings.hasPrefixComptime(file.dest_path, ".\\"))) + file.dest_path[2..] + else + file.dest_path; + + try source_maps.put( + allocator, + try std.fmt.allocPrint(allocator, "bake:/{s}", .{without_prefix}), + OutputFile.Index.init(@intCast(source_map_index)), + ); +} + pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMachine, pt: *PerThread) !void { // Load and evaluate the configuration module const global = vm.global; @@ -355,21 +382,7 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa // `PerThread` so we can provide sourcemapped stacktraces for // server components. if (file.source_map_index != std.math.maxInt(u32)) { - const source_map_index = file.source_map_index; - const source_map_file: *const OutputFile = &bundled_outputs[source_map_index]; - bun.assert(source_map_file.output_kind == .sourcemap); - - const without_prefix = if (bun.strings.hasPrefixComptime(file.dest_path, "./") or - (Environment.isWindows and bun.strings.hasPrefixComptime(file.dest_path, ".\\"))) - file.dest_path[2..] - else - file.dest_path; - - try source_maps.put( - allocator, - try std.fmt.allocPrint(allocator, "bake:/{s}", .{without_prefix}), - OutputFile.Index.init(@intCast(source_map_index)), - ); + try writeSourcemapToDisk(allocator, &file, bundled_outputs, &source_maps); } switch (file.output_kind) { @@ -405,21 +418,7 @@ pub fn buildWithVm(ctx: bun.CLI.Command.Context, cwd: []const u8, vm: *VirtualMa // TODO: should we just write the sourcemaps to disk? if (file.source_map_index != std.math.maxInt(u32)) { - const source_map_index = file.source_map_index; - const source_map_file: *const OutputFile = &bundled_outputs[source_map_index]; - bun.assert(source_map_file.output_kind == .sourcemap); - - const without_prefix = if (bun.strings.hasPrefixComptime(file.dest_path, "./") or - (Environment.isWindows and bun.strings.hasPrefixComptime(file.dest_path, ".\\"))) - file.dest_path[2..] - else - file.dest_path; - - try source_maps.put( - allocator, - try std.fmt.allocPrint(allocator, "bake:/{s}", .{without_prefix}), - OutputFile.Index.init(@intCast(source_map_index)), - ); + try writeSourcemapToDisk(allocator, &file, bundled_outputs, &source_maps); } } // Write the runtime file to disk if there are any client chunks @@ -698,7 +697,12 @@ fn loadModule(vm: *VirtualMachine, global: *JSC.JSGlobalObject, key: JSValue) !J const promise = BakeLoadModuleByKey(global, key).asAnyPromise().?.internal; promise.setHandled(vm.jsc); vm.waitForPromise(.{ .internal = promise }); - vm.eventLoop().drainMicrotasks() catch unreachable; + // TODO: Specially draining microtasks here because `waitForPromise` has a + // bug which forgets to do it, but I don't want to fix it right now as it + // could affect a lot of the codebase. This should be removed. + vm.eventLoop().drainMicrotasks() catch { + bun.Global.crash(); + }; switch (promise.unwrap(vm.jsc, .mark_handled)) { .pending => unreachable, .fulfilled => |val| { @@ -951,7 +955,7 @@ pub const PerThread = struct { } // Must be run at the top of the event loop - pub fn loadBundledModule(pt: *PerThread, id: OpaqueFileId) bun.JSError!JSValue { + pub fn loadBundledModule(pt: *PerThread, id: OpaqueFileId) !JSValue { return try loadModule( pt.vm, pt.vm.global, diff --git a/src/js/builtins/Bake.ts b/src/js/builtins/Bake.ts index 63e93f46b315..8b48b607070e 100644 --- a/src/js/builtins/Bake.ts +++ b/src/js/builtins/Bake.ts @@ -105,24 +105,6 @@ export async function renderRoutesForProdStatic( return doGenerateRoute(type, noClient, i, layouts, pageModule, params); } - console.time("modulesForFiles"); - // const modulesForFiles = await Promise.all( - // files.map(async (fileList, i) => { - // $assert(fileList.length > 0); - // if (fileList.length > 1) { - // let anyPromise = false; - // // let loaded = fileList.map( - // // x => loadedModules[x] ?? ((anyPromise = true), import(allServerFiles[x]).then(x => (loadedModules[x] = x))), - // // ); - // anyPromise = true; - // let loaded = fileList.map(x => import(allServerFiles[x]).then(x => (loadedModules[x] = x))); - // return anyPromise ? await Promise.all(loaded) : loaded; - // } else { - // const id = fileList[0]; - // return [loadedModules[id] ?? (loadedModules[id] = await import(allServerFiles[id]))]; - // } - // }), - // ); let modulesForFiles = []; for (const fileList of files) { $assert(fileList.length > 0); @@ -137,7 +119,6 @@ export async function renderRoutesForProdStatic( modulesForFiles.push([loadedModules[id] ?? (loadedModules[id] = await import(allServerFiles[id]))]); } } - console.timeEnd("modulesForFiles"); return Promise.all( modulesForFiles.map(async (modules, i) => { diff --git a/src/js_parser.zig b/src/js_parser.zig index d797bfc7ff9d..3ac9c77c2bd8 100644 --- a/src/js_parser.zig +++ b/src/js_parser.zig @@ -9049,7 +9049,9 @@ fn NewParser_( }) catch unreachable; } - // No need to add this to `item_refs` because `.scanImportsAndExports` special cases this + // No need to add the `default_name` to `item_refs` because + // `.scanImportsAndExports(...)` special cases and handles + // `default_name` separately } var end: usize = 0; @@ -17543,7 +17545,7 @@ fn NewParser_( expr.loc, std.fmt.allocPrint( p.allocator, - "\"useState\" is not available in a server component. If you need interactivity, consider converting part of this to a Client Component.", + "\"useState\" is not available in a server component. If you need interactivity, consider converting part of this to a Client Component (by adding `\"use client\";` to the top of the file).", .{}, ) catch bun.outOfMemory(), ) catch bun.outOfMemory(); diff --git a/src/sourcemap/sourcemap.zig b/src/sourcemap/sourcemap.zig index 5c2788a7c0d0..a71ee0569d42 100644 --- a/src/sourcemap/sourcemap.zig +++ b/src/sourcemap/sourcemap.zig @@ -1105,7 +1105,7 @@ pub fn getSourceMapImpl( // If we're using bake's production build the global object will // be Bake::GlobalObject and we can fetch the sourcemap from it, // if not fallback to the normal way - if (!BakeGlobalObject__isBaked(global)) { + if (!BakeGlobalObject__isBakeGlobalObject(global)) { break :fallback_to_normal; } const data = BakeSourceProvider.getExternal( @@ -1216,8 +1216,7 @@ pub const SourceProviderMap = opaque { } }; -/// ( ͡° ͜ʖ ͡°) -extern "c" fn BakeGlobalObject__isBaked(global: *bun.JSC.JSGlobalObject) bool; +extern "c" fn BakeGlobalObject__isBakeGlobalObject(global: *bun.JSC.JSGlobalObject) bool; extern "c" fn BakeGlobalObject__getPerThreadData(global: *bun.JSC.JSGlobalObject) *bun.bake.production.PerThread; @@ -1229,7 +1228,7 @@ pub const BakeSourceProvider = opaque { } pub fn getExternal(_: *BakeSourceProvider, global: *bun.JSC.JSGlobalObject, source_filename: []const u8) []const u8 { - bun.assert(BakeGlobalObject__isBaked(global)); + bun.assert(BakeGlobalObject__isBakeGlobalObject(global)); const pt = BakeGlobalObject__getPerThreadData(global); if (pt.source_maps.get(source_filename)) |value| { return pt.bundled_outputs[value.get()].value.asSlice(); From 5d2a7c8356abcad5bc3b3af80a73456248bea173 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Thu, 17 Jul 2025 17:03:54 -0700 Subject: [PATCH 45/59] remove stupid printfs --- src/bun.js/bindings/ZigGlobalObject.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index cbbf1bc7f537..4a0c86d94702 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -4195,7 +4195,6 @@ JSC::JSInternalPromise* GlobalObject::moduleLoaderImportModule(JSGlobalObject* j JSValue parameters, const SourceOrigin& sourceOrigin) { - printf("GlobalObject::moduleLoaderImportModule: %s, %s\n", moduleNameValue->getString(jsGlobalObject).utf8().data(), sourceOrigin.url().string().utf8().data()); auto* globalObject = static_cast(jsGlobalObject); VM& vm = JSC::getVM(globalObject); @@ -4338,7 +4337,6 @@ JSC::JSInternalPromise* GlobalObject::moduleLoaderFetch(JSGlobalObject* globalOb auto moduleKeyJS = key.toString(globalObject); RETURN_IF_EXCEPTION(scope, {}); auto moduleKey = moduleKeyJS->value(globalObject); - printf("GlobalObject::moduleLoaderFetch: %s\n", moduleKey->utf8().data()); if (scope.exception()) [[unlikely]] return rejectedInternalPromise(globalObject, scope.exception()->value()); From ed55ccce9a00f2835d594e5425f1fb38498bcfa6 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Thu, 17 Jul 2025 17:42:40 -0700 Subject: [PATCH 46/59] unsafely ref to stay alive --- src/bake/BakeGlobalObject.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/bake/BakeGlobalObject.cpp b/src/bake/BakeGlobalObject.cpp index 2a3805e523c9..da65b30428de 100644 --- a/src/bake/BakeGlobalObject.cpp +++ b/src/bake/BakeGlobalObject.cpp @@ -226,7 +226,13 @@ const JSC::GlobalObjectMethodTable& GlobalObject::globalObjectMethodTable() // TODO: remove this entire method extern "C" GlobalObject* BakeCreateProdGlobal(void* console) { - JSC::VM& vm = JSC::VM::create(JSC::HeapType::Large).leakRef(); + RefPtr vmPtr = JSC::VM::tryCreate(JSC::HeapType::Large); + // We need to unsafely ref this so it stays alive, later in + // `Zig__GlobalObject__destructOnExit` will call + // `vm.derefSuppressingSaferCPPChecking()` to free it. + vmPtr->refSuppressingSaferCPPChecking(); + JSC::VM& vm = *vmPtr; + vm.heap.acquireAccess(); JSC::JSLockHolder locker(vm); BunVirtualMachine* bunVM = Bun__getVM(); From c8da04f302fe52129f7afe935bc394720a15360d Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Thu, 17 Jul 2025 19:02:51 -0700 Subject: [PATCH 47/59] fix bad crash --- src/bake/BakeGlobalObject.cpp | 3 +++ src/bake/production.zig | 4 ---- src/bun.js/VirtualMachine.zig | 4 ++++ src/bun.js/bindings/BunJSCEventLoop.cpp | 3 +++ 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/bake/BakeGlobalObject.cpp b/src/bake/BakeGlobalObject.cpp index da65b30428de..b27bc9ce650e 100644 --- a/src/bake/BakeGlobalObject.cpp +++ b/src/bake/BakeGlobalObject.cpp @@ -227,6 +227,9 @@ const JSC::GlobalObjectMethodTable& GlobalObject::globalObjectMethodTable() extern "C" GlobalObject* BakeCreateProdGlobal(void* console) { RefPtr vmPtr = JSC::VM::tryCreate(JSC::HeapType::Large); + if (!vmPtr) [[unlikely]] { + BUN_PANIC("Failed to allocate JavaScriptCore Virtual Machine. Did your computer run out of memory? Or maybe you compiled Bun with a mismatching libc++ version or compiler?"); + } // We need to unsafely ref this so it stays alive, later in // `Zig__GlobalObject__destructOnExit` will call // `vm.derefSuppressingSaferCPPChecking()` to free it. diff --git a/src/bake/production.zig b/src/bake/production.zig index 00349c7f77a9..aa3d83af98a4 100644 --- a/src/bake/production.zig +++ b/src/bake/production.zig @@ -38,9 +38,7 @@ pub fn buildCommand(ctx: bun.CLI.Command.Context) !void { 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; @@ -758,8 +756,6 @@ extern fn BakeRenderRoutesForProdStatic( styles: JSValue, ) *JSC.JSPromise; -extern fn BakeCreateProdGlobal(console_ptr: *anyopaque) *JSC.JSGlobalObject; - /// The result of this function is a JSValue that wont be garbage collected, as /// it will always have at least one reference by the module loader. fn BakeRegisterProductionChunk(global: *JSC.JSGlobalObject, key: bun.String, source_code: bun.String) bun.JSError!JSValue { diff --git a/src/bun.js/VirtualMachine.zig b/src/bun.js/VirtualMachine.zig index 8021c023e427..505180956670 100644 --- a/src/bun.js/VirtualMachine.zig +++ b/src/bun.js/VirtualMachine.zig @@ -1307,6 +1307,8 @@ pub fn initWorker( return vm; } +extern fn BakeCreateProdGlobal(console_ptr: *anyopaque) *JSC.JSGlobalObject; + pub fn initBake(opts: Options) anyerror!*VirtualMachine { JSC.markBinding(@src()); const allocator = opts.allocator; @@ -1359,6 +1361,8 @@ pub fn initBake(opts: Options) anyerror!*VirtualMachine { vm.regular_event_loop.tasks.ensureUnusedCapacity(64) catch unreachable; vm.regular_event_loop.concurrent_tasks = .{}; vm.event_loop = &vm.regular_event_loop; + vm.global = BakeCreateProdGlobal(vm.console); + vm.jsc = vm.global.vm(); vm.eventLoop().ensureWaker(); vm.transpiler.macro_context = null; diff --git a/src/bun.js/bindings/BunJSCEventLoop.cpp b/src/bun.js/bindings/BunJSCEventLoop.cpp index 6ca417034c3f..9fafa9f6c288 100644 --- a/src/bun.js/bindings/BunJSCEventLoop.cpp +++ b/src/bun.js/bindings/BunJSCEventLoop.cpp @@ -17,6 +17,9 @@ extern "C" void Bun__JSC_onBeforeWait(JSC::VM* vm) ASSERT(!drop_all_locks.has_value()); if (vm) { bool previouslyHadAccess = vm->heap.hasHeapAccess(); + // sanity check for debug builds to ensure we're not doing a + // use-after-free here + ASSERT(vm->refCount() > 0); drop_all_locks.emplace(*vm); if (previouslyHadAccess) { vm->heap.releaseAccess(); From 9bee24a9b900552386a592a7cfbf0b370257b7b3 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Thu, 17 Jul 2025 19:06:07 -0700 Subject: [PATCH 48/59] Fix tests --- src/bun.js/bindings/ZigGlobalObject.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index 4a0c86d94702..d3224b1dcfe0 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -265,7 +265,7 @@ extern "C" void JSCInitialize(const char* envp[], size_t envc, void (*onCrash)(c has_loaded_jsc = true; JSC::Config::enableRestrictedOptions(); - std::set_terminate([]() { Zig__GlobalObject__onCrash(); }); + // std::set_terminate([]() { Zig__GlobalObject__onCrash(); }); WTF::initializeMainThread(); #if ASAN_ENABLED && OS(LINUX) From ecc8557df4faf25beb97d94be260428a9004c5fd Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Thu, 17 Jul 2025 19:06:55 -0700 Subject: [PATCH 49/59] fix tests --- test/bake/dev/production.test.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/test/bake/dev/production.test.ts b/test/bake/dev/production.test.ts index 49fb9feebcd5..2dc28d7fa883 100644 --- a/test/bake/dev/production.test.ts +++ b/test/bake/dev/production.test.ts @@ -229,6 +229,8 @@ export default function GettingStarted() { `, }); + console.error("DIR", dir); + // Run the build command const buildProc = await Bun.$`${bunExe()} build --app ./src/index.tsx --outdir ./dist` .cwd(dir) @@ -398,14 +400,23 @@ export default function IndexPage() { // The build should succeed - client components should support default imports expect(stderr.toString()).toContain( - '"useState" is not available in a server component. If you need interactivity, consider converting part of this to a Client Component.', + '"useState" is not available in a server component. If you need interactivity, consider converting part of this to a Client Component (by adding `"use client";` to the top of the file).', ); expect(exitCode).toBe(1); }); test("importing useState from client component", async () => { const dir = await tempDirWithBakeDeps("bake-production-client-useState", { - "src/index.tsx": `export default { app: { framework: "react" } };`, + "src/index.tsx": ` + const bundlerOptions = { + sourcemap: "inline", + minify: { + whitespace: false, + identifiers: false, + syntax: false, + }, +}; +export default { app: { framework: "react", bundlerOptions: { server: bundlerOptions, client: bundlerOptions, ssr: bundlerOptions } } };`, "pages/index.tsx": `import Counter from "../components/Counter"; export default function IndexPage() { From 0aafdfe2b53d2b28b13fd12155b253836846a1b2 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Thu, 17 Jul 2025 20:22:55 -0700 Subject: [PATCH 50/59] proper useState check that does not apply to react internally --- src/js_parser.zig | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/js_parser.zig b/src/js_parser.zig index 3ac9c77c2bd8..897338f187be 100644 --- a/src/js_parser.zig +++ b/src/js_parser.zig @@ -17533,13 +17533,20 @@ fn NewParser_( if (!ReactRefresh.isHookName(original_name)) break :try_record_hook; if (p.options.features.react_fast_refresh) { p.handleReactRefreshHookCall(e_, original_name); - } else { - // if we're in this branch then it is a server - // component, error if they try to use `useState` - if (bun.strings.eqlComptime(original_name, "useState") and - // this feels dumb, what's a better way to detect that it's not React? - !bun.strings.includes(p.source.path.text, "node_modules/react")) - { + } else if ( + // If we're here it means we're in server component. + // Error if the user is using the `useState` hook as it + // is disallowed in server components. + // + // We're also specifically checking that the target is + // `.e_import_identifier`. + // + // Why? Because we *don't* want to check for uses of + // `useState` _inside_ React, and we know React uses + // commonjs so it will never be `.e_import_identifier`. + e_.target.data == .e_import_identifier) { + bun.assert(p.options.features.server_components.isServerSide()); + if (bun.strings.eqlComptime(original_name, "useState")) { p.log.addError( p.source, expr.loc, From 22af318ecac2295bc68e84f7d911de6677a20af9 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Thu, 17 Jul 2025 20:24:16 -0700 Subject: [PATCH 51/59] check scope --- src/bake/BakeGlobalObject.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/bake/BakeGlobalObject.cpp b/src/bake/BakeGlobalObject.cpp index b27bc9ce650e..0a98c1c6c40b 100644 --- a/src/bake/BakeGlobalObject.cpp +++ b/src/bake/BakeGlobalObject.cpp @@ -160,7 +160,9 @@ JSC::JSInternalPromise* bakeModuleLoaderFetch(JSC::JSGlobalObject* globalObject, return rejectedInternalPromise(globalObject, createTypeError(globalObject, "BakeGlobalObject does not have per-thread data configured"_s)); } - return Zig::GlobalObject::moduleLoaderFetch(globalObject, loader, key, parameters, script); + auto result = Zig::GlobalObject::moduleLoaderFetch(globalObject, loader, key, parameters, script); + RETURN_IF_EXCEPTION(scope, rejectedInternalPromise(globalObject, scope.exception()->value())); + return result; } GlobalObject* GlobalObject::create(JSC::VM& vm, JSC::Structure* structure, From 439d620d68186aee681dd74e75e51e79dc7b2192 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Thu, 17 Jul 2025 23:24:28 -0700 Subject: [PATCH 52/59] fix --- test/bake/dev/import-meta-inline.test.ts | 3 ++- test/no-validate-exceptions.txt | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/test/bake/dev/import-meta-inline.test.ts b/test/bake/dev/import-meta-inline.test.ts index 8067195f57bc..8bac4211bb90 100644 --- a/test/bake/dev/import-meta-inline.test.ts +++ b/test/bake/dev/import-meta-inline.test.ts @@ -186,7 +186,8 @@ export default function BlogPost(req, meta) { expect(json1.meta.dir).toContain("routes/blog"); expect(json1.meta.dirname).toBe(json1.meta.dir); expect(json1.meta.path).toContain("routes/blog/[...slug].ts"); - expect(json1.meta.url).toMatch(/^file:\/\/.*routes\/blog\/\[\.\.\.slug\]\.ts$/); + // url encoded! + expect(json1.meta.url).toMatch(/^file:\/\/.*routes\/blog\/%5B\.\.\.slug%5D\.ts$/); // Test multiple segments const post2 = await dev.fetch("/blog/2024/tech/bun-framework"); diff --git a/test/no-validate-exceptions.txt b/test/no-validate-exceptions.txt index 94dcdb2e0b2f..5eb95781423b 100644 --- a/test/no-validate-exceptions.txt +++ b/test/no-validate-exceptions.txt @@ -126,6 +126,7 @@ test/js/node/process/call-constructor.test.js test/js/node/stubs.test.js test/js/node/timers/node-timers.test.ts test/bake/dev/vfile.test.ts +test/bake/dev/import-meta-inline.test.ts test/cli/run/run-eval.test.ts test/cli/run/self-reference.test.ts test/js/bun/resolve/import-meta-resolve.test.mjs From 438164641daca9ef1bb1354ac61a5bd9f012f368 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Thu, 17 Jul 2025 23:27:21 -0700 Subject: [PATCH 53/59] fix windows --- test/bake/dev/import-meta-inline.test.ts | 47 ++++++++++++++++-------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/test/bake/dev/import-meta-inline.test.ts b/test/bake/dev/import-meta-inline.test.ts index 8bac4211bb90..08e640d2b99f 100644 --- a/test/bake/dev/import-meta-inline.test.ts +++ b/test/bake/dev/import-meta-inline.test.ts @@ -2,6 +2,13 @@ import { expect } from "bun:test"; import { devTest, emptyHtmlFile, minimalFramework } from "../bake-harness"; +const platformPath = (path: string) => { + if (process.platform === "win32") { + return path.replace(/\//g, "\\"); + } + return path; +}; + devTest("import.meta properties are inlined in bake", { framework: minimalFramework, files: { @@ -35,12 +42,12 @@ export default function (req, meta) { expect(json.file).toBe("index.ts"); // Check that path contains the full path including filename - expect(json.path).toContain("routes/index.ts"); + expect(json.path).toContain(platformPath("routes/index.ts")); expect(json.path).toEndWith("index.ts"); // Check that url is a file:// URL expect(json.url).toStartWith("file://"); - expect(json.url).toContain("routes/index.ts"); + expect(json.url).toContain(platformPath("routes/index.ts")); }, }); @@ -66,7 +73,7 @@ export default function (req, meta) { expect(text).toContain("dir: "); expect(text).toContain("file: test.ts"); expect(text).toContain("path: "); - expect(text).toContain("routes/test.ts"); + expect(text).toContain(platformPath("routes/test.ts")); // Update the file with a meaningful change await dev.patch("routes/test.ts", { @@ -81,7 +88,7 @@ export default function (req, meta) { expect(text2).toContain("directory: "); expect(text2).toContain("file: test.ts"); expect(text2).toContain("path: "); - expect(text2).toContain("routes/test.ts"); + expect(text2).toContain(platformPath("routes/test.ts")); }, }); @@ -104,9 +111,13 @@ export default function (req, meta) { const json = await response.json(); expect(json.file).toBe("handler.ts"); - expect(json.path).toContain("routes/api/v1/handler.ts"); - expect(json.dir).toContain("routes/api/v1"); - expect(json.url).toMatch(/^file:\/\/.*routes\/api\/v1\/handler\.ts$/); + expect(json.path).toContain(platformPath("routes/api/v1/handler.ts")); + expect(json.dir).toContain(platformPath("routes/api/v1")); + expect(json.url).toMatch( + process.platform === "win32" + ? /^file:\/\/.*routes\\api\\v1\\handler\.ts$/ + : /^file:\/\/.*routes\/api\/v1\/handler\.ts$/, + ); }, }); @@ -185,9 +196,13 @@ export default function BlogPost(req, meta) { expect(json1.meta.file).toBe("[...slug].ts"); expect(json1.meta.dir).toContain("routes/blog"); expect(json1.meta.dirname).toBe(json1.meta.dir); - expect(json1.meta.path).toContain("routes/blog/[...slug].ts"); + expect(json1.meta.path).toContain(platformPath("routes/blog/[...slug].ts")); // url encoded! - expect(json1.meta.url).toMatch(/^file:\/\/.*routes\/blog\/%5B\.\.\.slug%5D\.ts$/); + expect(json1.meta.url).toMatch( + process.platform === "win32" + ? /^file:\/\/.*routes\\blog\\%5B\\.\\.\\.slug%5D\\.ts$/ + : /^file:\/\/.*routes\/blog\/%5B\.\.\.slug%5D\.ts$/, + ); // Test multiple segments const post2 = await dev.fetch("/blog/2024/tech/bun-framework"); @@ -199,7 +214,7 @@ export default function BlogPost(req, meta) { // Meta properties should be the same regardless of the route expect(json2.meta.file).toBe("[...slug].ts"); - expect(json2.meta.path).toContain("routes/blog/[...slug].ts"); + expect(json2.meta.path).toContain(platformPath("routes/blog/[...slug].ts")); // Test empty slug (just /blog/) const post3 = await dev.fetch("/blog/"); @@ -259,8 +274,8 @@ export default function GettingStarted(req, meta) { expect(apiJson.type).toBe("static"); expect(apiJson.page).toBe("API Documentation"); expect(apiJson.file).toBe("api.ts"); - expect(apiJson.dir).toContain("routes/docs"); - expect(apiJson.fullPath).toContain("routes/docs/api.ts"); + expect(apiJson.dir).toContain(platformPath("routes/docs")); + expect(apiJson.fullPath).toContain(platformPath("routes/docs/api.ts")); // Test another static route const startResponse = await dev.fetch("/docs/getting-started"); @@ -269,7 +284,7 @@ export default function GettingStarted(req, meta) { expect(startJson.type).toBe("static"); expect(startJson.page).toBe("Getting Started"); expect(startJson.file).toBe("getting-started.ts"); - expect(startJson.fullPath).toContain("routes/docs/getting-started.ts"); + expect(startJson.fullPath).toContain(platformPath("routes/docs/getting-started.ts")); // Test catch-all route - should match for non-static paths const guideResponse = await dev.fetch("/docs/guides/advanced/optimization"); @@ -279,8 +294,8 @@ export default function GettingStarted(req, meta) { expect(guideJson.type).toBe("catch-all"); expect(guideJson.path).toEqual(["guides", "advanced", "optimization"]); expect(guideJson.file).toBe("[...path].ts"); - expect(guideJson.dir).toContain("routes/docs"); - expect(guideJson.fullPath).toContain("routes/docs/[...path].ts"); + expect(guideJson.dir).toContain(platformPath("routes/docs")); + expect(guideJson.fullPath).toContain(platformPath("routes/docs/[...path].ts")); // Update catch-all route and verify import.meta values remain inlined await dev.patch("routes/docs/[...path].ts", { @@ -293,6 +308,6 @@ export default function GettingStarted(req, meta) { expect(updatedJson.type).toBe("dynamic-catch-all"); expect(updatedJson.file).toBe("[...path].ts"); - expect(updatedJson.fullPath).toContain("routes/docs/[...path].ts"); + expect(updatedJson.fullPath).toContain(platformPath("routes/docs/[...path].ts")); }, }); From 6b815e1ad38473ad664c735db370d5598bf6973e Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Thu, 17 Jul 2025 23:28:44 -0700 Subject: [PATCH 54/59] more fix --- test/no-validate-exceptions.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/test/no-validate-exceptions.txt b/test/no-validate-exceptions.txt index 5eb95781423b..ee15469c4309 100644 --- a/test/no-validate-exceptions.txt +++ b/test/no-validate-exceptions.txt @@ -127,6 +127,7 @@ test/js/node/stubs.test.js test/js/node/timers/node-timers.test.ts test/bake/dev/vfile.test.ts test/bake/dev/import-meta-inline.test.ts +test/bake/dev/production.test.ts test/cli/run/run-eval.test.ts test/cli/run/self-reference.test.ts test/js/bun/resolve/import-meta-resolve.test.mjs From 14aa227eee2de9c9084b66a3258cb98435b26409 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Fri, 18 Jul 2025 14:58:15 -0700 Subject: [PATCH 55/59] lol --- src/bun.js/bindings/ZigGlobalObject.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index d3224b1dcfe0..4a0c86d94702 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -265,7 +265,7 @@ extern "C" void JSCInitialize(const char* envp[], size_t envc, void (*onCrash)(c has_loaded_jsc = true; JSC::Config::enableRestrictedOptions(); - // std::set_terminate([]() { Zig__GlobalObject__onCrash(); }); + std::set_terminate([]() { Zig__GlobalObject__onCrash(); }); WTF::initializeMainThread(); #if ASAN_ENABLED && OS(LINUX) From 37db72b569905a5ebfd81c67f0e7ac8c5509543f Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Fri, 18 Jul 2025 15:44:22 -0700 Subject: [PATCH 56/59] fix windows tests --- test/bake/dev/import-meta-inline.test.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/test/bake/dev/import-meta-inline.test.ts b/test/bake/dev/import-meta-inline.test.ts index 08e640d2b99f..928dfd24328d 100644 --- a/test/bake/dev/import-meta-inline.test.ts +++ b/test/bake/dev/import-meta-inline.test.ts @@ -47,7 +47,7 @@ export default function (req, meta) { // Check that url is a file:// URL expect(json.url).toStartWith("file://"); - expect(json.url).toContain(platformPath("routes/index.ts")); + expect(json.url).toContain("routes/index.ts"); }, }); @@ -113,11 +113,7 @@ export default function (req, meta) { expect(json.file).toBe("handler.ts"); expect(json.path).toContain(platformPath("routes/api/v1/handler.ts")); expect(json.dir).toContain(platformPath("routes/api/v1")); - expect(json.url).toMatch( - process.platform === "win32" - ? /^file:\/\/.*routes\\api\\v1\\handler\.ts$/ - : /^file:\/\/.*routes\/api\/v1\/handler\.ts$/, - ); + expect(json.url).toMatch(/^file:\/\/.*routes\/api\/v1\/handler\.ts$/); }, }); From bf9fdb18730bcdaa90afcf6960e7e1e37312a681 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Fri, 18 Jul 2025 18:15:01 -0700 Subject: [PATCH 57/59] windows --- src/bun.js/VirtualMachine.zig | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/bun.js/VirtualMachine.zig b/src/bun.js/VirtualMachine.zig index 505180956670..6d75c1b922f0 100644 --- a/src/bun.js/VirtualMachine.zig +++ b/src/bun.js/VirtualMachine.zig @@ -1361,9 +1361,16 @@ pub fn initBake(opts: Options) anyerror!*VirtualMachine { vm.regular_event_loop.tasks.ensureUnusedCapacity(64) catch unreachable; vm.regular_event_loop.concurrent_tasks = .{}; vm.event_loop = &vm.regular_event_loop; - vm.global = BakeCreateProdGlobal(vm.console); - vm.jsc = vm.global.vm(); - vm.eventLoop().ensureWaker(); + if (comptime Environment.isWindows) { + vm.eventLoop().ensureWaker(); + vm.global = BakeCreateProdGlobal(vm.console); + vm.jsc = vm.global.vm(); + uws.Loop.get().internal_loop_data.jsc_vm = vm.jsc; + } else { + vm.global = BakeCreateProdGlobal(vm.console); + vm.jsc = vm.global.vm(); + vm.eventLoop().ensureWaker(); + } vm.transpiler.macro_context = null; vm.transpiler.resolver.store_fd = opts.store_fd; From f62302374cb0e6dce7c1e8c623ae9f1932c5b532 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 19 Jul 2025 10:58:10 +0000 Subject: [PATCH 58/59] [autofix.ci] apply automated fixes --- src/bundler/linker_context/generateChunksInParallel.zig | 1 - 1 file changed, 1 deletion(-) diff --git a/src/bundler/linker_context/generateChunksInParallel.zig b/src/bundler/linker_context/generateChunksInParallel.zig index 7b309a8bc8cc..e6ab6ceaf09a 100644 --- a/src/bundler/linker_context/generateChunksInParallel.zig +++ b/src/bundler/linker_context/generateChunksInParallel.zig @@ -587,7 +587,6 @@ const base64 = bun.base64; const JSC = bun.JSC; - const OutputFileListBuilder = bun.bundle_v2.LinkerContext.OutputFileListBuilder; const StaticRouteVisitor = bun.bundle_v2.LinkerContext.StaticRouteVisitor; const ThreadPoolLib = bun.ThreadPool; From dec703d5b664b4df39c1ba74c9e9fcf3dacffe85 Mon Sep 17 00:00:00 2001 From: Zack Radisic Date: Sun, 20 Jul 2025 18:18:10 -0700 Subject: [PATCH 59/59] fix window --- src/bake/BakeGlobalObject.cpp | 10 ++++++++++ src/bake/production.zig | 17 ++++++++++++++++- src/bun.js/VirtualMachine.zig | 2 +- test/bake/dev/import-meta-inline.test.ts | 6 ++---- test/bake/dev/production.test.ts | 20 ++++++++++++-------- 5 files changed, 41 insertions(+), 14 deletions(-) diff --git a/src/bake/BakeGlobalObject.cpp b/src/bake/BakeGlobalObject.cpp index 0a98c1c6c40b..c48f2af84e97 100644 --- a/src/bake/BakeGlobalObject.cpp +++ b/src/bake/BakeGlobalObject.cpp @@ -9,6 +9,7 @@ #include "JavaScriptCore/JSSourceCode.h" extern "C" BunString BakeProdResolve(JSC::JSGlobalObject*, BunString a, BunString b); +extern "C" BunString BakeToWindowsPath(BunString a); namespace Bake { using namespace JSC; @@ -153,6 +154,15 @@ JSC::JSInternalPromise* bakeModuleLoaderFetch(JSC::JSGlobalObject* globalObject, // we don't actually want to load it from the Bake production module // map and instead make it go through the normal codepath. auto bakePrefixRemoved = moduleKey.substringSharingImpl("bake:"_s.length()); + +#ifdef _WIN32 + // We normalize paths to contain forward slashes in bake so we don't + // have to worry about platform paths. Now we have to worry about + // it, because `moduleLoaderFetch(...)` may read the path from disk + // and so we need to give a Windows path to it. + auto temp = BakeToWindowsPath(Bun::toString(bakePrefixRemoved)); + bakePrefixRemoved = temp.toWTFString(); +#endif JSString* bakePrefixRemovedString = jsNontrivialString(vm, bakePrefixRemoved); JSValue bakePrefixRemovedJsvalue = bakePrefixRemovedString; return Zig::GlobalObject::moduleLoaderFetch(globalObject, loader, bakePrefixRemovedJsvalue, parameters, script); diff --git a/src/bake/production.zig b/src/bake/production.zig index aa3d83af98a4..f4f029d92124 100644 --- a/src/bake/production.zig +++ b/src/bake/production.zig @@ -768,6 +768,21 @@ fn BakeRegisterProductionChunk(global: *JSC.JSGlobalObject, key: bun.String, sou return result; } +pub export fn BakeToWindowsPath(input: bun.String) callconv(.C) bun.String { + if (comptime bun.Environment.isPosix) { + @panic("This code should not be called on POSIX systems."); + } + var sfa = std.heap.stackFallback(1024, bun.default_allocator); + const alloc = sfa.get(); + const input_utf8 = input.toUTF8(alloc); + defer input_utf8.deinit(); + const input_slice = input_utf8.slice(); + const output = bun.w_path_buffer_pool.get(); + defer bun.w_path_buffer_pool.put(output); + const output_slice = bun.strings.toWPathNormalizeAutoExtend(output.*[0..], input_slice); + return bun.String.cloneUTF16(output_slice); +} + pub export fn BakeProdResolve(global: *JSC.JSGlobalObject, a_str: bun.String, specifier_str: bun.String) callconv(.C) bun.String { var sfa = std.heap.stackFallback(@sizeOf(bun.PathBuffer) * 2, bun.default_allocator); const alloc = sfa.get(); @@ -794,7 +809,7 @@ pub export fn BakeProdResolve(global: *JSC.JSGlobalObject, a_str: bun.String, sp return bun.String.createFormat("bake:{s}", .{bun.path.joinAbs( bun.Dirname.dirname(u8, referrer.slice()[5..]) orelse referrer.slice()[5..], - .auto, + .posix, // force posix paths in bake specifier.slice(), )}) catch return bun.String.dead; } diff --git a/src/bun.js/VirtualMachine.zig b/src/bun.js/VirtualMachine.zig index 6d75c1b922f0..7fad6ae7069c 100644 --- a/src/bun.js/VirtualMachine.zig +++ b/src/bun.js/VirtualMachine.zig @@ -1361,7 +1361,7 @@ pub fn initBake(opts: Options) anyerror!*VirtualMachine { vm.regular_event_loop.tasks.ensureUnusedCapacity(64) catch unreachable; vm.regular_event_loop.concurrent_tasks = .{}; vm.event_loop = &vm.regular_event_loop; - if (comptime Environment.isWindows) { + if (comptime bun.Environment.isWindows) { vm.eventLoop().ensureWaker(); vm.global = BakeCreateProdGlobal(vm.console); vm.jsc = vm.global.vm(); diff --git a/test/bake/dev/import-meta-inline.test.ts b/test/bake/dev/import-meta-inline.test.ts index 928dfd24328d..9f0bbdc0c985 100644 --- a/test/bake/dev/import-meta-inline.test.ts +++ b/test/bake/dev/import-meta-inline.test.ts @@ -190,14 +190,12 @@ export default function BlogPost(req, meta) { // Verify import.meta properties are inlined expect(json1.meta.file).toBe("[...slug].ts"); - expect(json1.meta.dir).toContain("routes/blog"); + expect(json1.meta.dir).toContain(platformPath("routes/blog")); expect(json1.meta.dirname).toBe(json1.meta.dir); expect(json1.meta.path).toContain(platformPath("routes/blog/[...slug].ts")); // url encoded! expect(json1.meta.url).toMatch( - process.platform === "win32" - ? /^file:\/\/.*routes\\blog\\%5B\\.\\.\\.slug%5D\\.ts$/ - : /^file:\/\/.*routes\/blog\/%5B\.\.\.slug%5D\.ts$/, + /^file:\/\/.*routes\/blog\/%5B\.\.\.slug%5D\.ts$/, ); // Test multiple segments diff --git a/test/bake/dev/production.test.ts b/test/bake/dev/production.test.ts index 2dc28d7fa883..d064214977ea 100644 --- a/test/bake/dev/production.test.ts +++ b/test/bake/dev/production.test.ts @@ -1,9 +1,12 @@ import { describe, expect, test } from "bun:test"; -import { existsSync } from "fs"; +import { existsSync, readdirSync } from "fs"; import { bunEnv, bunExe } from "harness"; import path from "path"; import { tempDirWithBakeDeps } from "../bake-harness"; +const normalizePath = (path: string) => (process.platform === "win32" ? path.replaceAll("\\", "/") : path); +const platformPath = (path: string) => (process.platform === "win32" ? path.replaceAll("/", "\\") : path); + /** * Production build tests */ @@ -240,8 +243,9 @@ export default function GettingStarted() { expect(buildProc.exitCode).toBe(0); // Check that the build output contains the generated files - const distFiles = await Bun.$`find dist -name "*.html" -type f | sort`.cwd(dir).text(); - const htmlFiles = distFiles.trim().split("\n").filter(Boolean); + const htmlFiles = Array.from(new Bun.Glob("dist/**/*.html").scanSync(dir)) + .sort() + .map(p => normalizePath(p)); // Should have generated all the static paths // Note: React's routing may flatten the paths @@ -267,9 +271,9 @@ export default function GettingStarted() { // Check that import.meta values are inlined in the HTML expect(blogPostHtml).toContain('data-file="[...slug].tsx"'); expect(blogPostHtml).toContain("data-dir="); - expect(blogPostHtml).toContain('/pages/blog"'); // The full path will include the temp directory + expect(blogPostHtml).toContain(platformPath('/pages/blog"')); // The full path will include the temp directory expect(blogPostHtml).toContain("data-path="); - expect(blogPostHtml).toContain('/pages/blog/[...slug].tsx"'); + expect(blogPostHtml).toContain(platformPath('/pages/blog/[...slug].tsx"')); // Check docs catch-all route const docsHtml = await Bun.file( @@ -279,7 +283,7 @@ export default function GettingStarted() { expect(docsHtml).toContain("Reading docs at:"); expect(docsHtml).toContain("guides/advanced/optimization"); expect(docsHtml).toContain('data-file="[...path].tsx"'); - expect(docsHtml).toContain('/pages/docs/[...path].tsx"'); + expect(docsHtml).toContain(platformPath('/pages/docs/[...path].tsx"')); // Check that the static getting-started page uses its own file name, not the catch-all const staticHtml = await Bun.file(path.join(dir, "dist", "docs", "getting-started", "index.html")).text(); @@ -287,7 +291,7 @@ export default function GettingStarted() { expect(staticHtml).toContain("Getting Started"); expect(staticHtml).toContain("This is a static page"); expect(staticHtml).toContain('data-file="getting-started.tsx"'); - expect(staticHtml).toContain('/pages/docs/getting-started.tsx"'); + expect(staticHtml).toContain(platformPath('/pages/docs/getting-started.tsx"')); expect(staticHtml).not.toContain("[...path].tsx"); // Verify that import.meta values are consistent across all catch-all instances @@ -295,7 +299,7 @@ export default function GettingStarted() { path.join(dir, "dist", "blog", "tutorials", "getting-started", "index.html"), ).text(); expect(blogIndex).toContain('data-file="[...slug].tsx"'); - expect(blogIndex).toContain('/pages/blog/[...slug].tsx"'); + expect(blogIndex).toContain(platformPath('/pages/blog/[...slug].tsx"')); }); test("handles build with no pages directory without crashing", async () => {