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 (
+