mirror of
https://github.com/oven-sh/bun
synced 2026-02-06 08:58:52 +00:00
Compare commits
3 Commits
dylan/pyth
...
claude/fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e105a88bc5 | ||
|
|
93b1527faa | ||
|
|
5c7f3dc4f3 |
@@ -1246,18 +1246,36 @@ fn runWithSourceCode(
|
||||
|
||||
step.* = .resolve;
|
||||
|
||||
// For copy-only entrypoints (JSON, PNG, etc.), we need to set the unique key
|
||||
// so they get copied properly, even though they don't use the .file loader
|
||||
var final_unique_key = unique_key_for_additional_file;
|
||||
var should_copy_with_hash = loader.shouldCopyForBundling();
|
||||
|
||||
if (!should_copy_with_hash and task.is_entry_point and loader.shouldCopyAsEntrypoint()) {
|
||||
// This is a copy-only entrypoint - generate a unique key for it
|
||||
should_copy_with_hash = true;
|
||||
final_unique_key = .{
|
||||
.key = try std.fmt.allocPrint(
|
||||
allocator,
|
||||
"{any}A{d:0>8}",
|
||||
.{ bun.fmt.hexIntLower(task.ctx.unique_key), source.index.get() },
|
||||
),
|
||||
.content_hash = ContentHasher.run(source.contents),
|
||||
};
|
||||
}
|
||||
|
||||
return .{
|
||||
.ast = ast,
|
||||
.source = source.*,
|
||||
.log = log.*,
|
||||
.use_directive = use_directive,
|
||||
.unique_key_for_additional_file = unique_key_for_additional_file.key,
|
||||
.unique_key_for_additional_file = final_unique_key.key,
|
||||
.side_effects = task.side_effects,
|
||||
.loader = loader,
|
||||
|
||||
// Hash the files in here so that we do it in parallel.
|
||||
.content_hash_for_additional_file = if (loader.shouldCopyForBundling())
|
||||
unique_key_for_additional_file.content_hash
|
||||
.content_hash_for_additional_file = if (should_copy_with_hash)
|
||||
final_unique_key.content_hash
|
||||
else
|
||||
0,
|
||||
};
|
||||
|
||||
@@ -829,7 +829,11 @@ pub const BundleV2 = struct {
|
||||
|
||||
// Handle onLoad plugins as entry points
|
||||
if (!this.enqueueOnLoadPluginIfNeeded(task)) {
|
||||
if (loader.shouldCopyForBundling()) {
|
||||
// Mark files for copying if they use the file loader OR if they're copy-only entrypoints
|
||||
const should_copy = loader.shouldCopyForBundling() or
|
||||
(is_entry_point and loader.shouldCopyAsEntrypoint());
|
||||
|
||||
if (should_copy) {
|
||||
var additional_files: *BabyList(AdditionalFile) = &this.graph.input_files.items(.additional_files)[source_index.get()];
|
||||
bun.handleOom(additional_files.append(this.allocator(), .{ .source_index = task.source_index.get() }));
|
||||
this.graph.input_files.items(.side_effects)[source_index.get()] = _resolver.SideEffects.no_side_effects__pure_data;
|
||||
@@ -1473,6 +1477,33 @@ pub const BundleV2 = struct {
|
||||
try fetcher.onFetch(fetcher.ctx, &result);
|
||||
}
|
||||
|
||||
/// Filter entry points into those that need JS chunks vs those that should only be copied as assets.
|
||||
/// Returns a slice of entry points that should go through chunk creation.
|
||||
/// Copy-only entrypoints (static assets when used as user-specified entrypoints) are filtered out.
|
||||
///
|
||||
/// Note: This is called before addServerComponentBoundariesAsExtraEntryPoints(), so at this point
|
||||
/// entry_points only contains user-specified entries.
|
||||
fn filterEntryPointsForChunking(this: *BundleV2, alloc: std.mem.Allocator) ![]Index {
|
||||
const loaders = this.graph.input_files.items(.loader);
|
||||
const entry_points = this.graph.entry_points.items;
|
||||
|
||||
var js_entry_points = std.ArrayList(Index).init(alloc);
|
||||
|
||||
for (entry_points) |entry_point| {
|
||||
const source_index = entry_point.get();
|
||||
const loader = loaders[source_index];
|
||||
|
||||
// Skip copy-only entrypoints (JSON, PNG, etc.) - they'll be copied as assets
|
||||
if (loader.shouldCopyAsEntrypoint()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try js_entry_points.append(entry_point);
|
||||
}
|
||||
|
||||
return js_entry_points.toOwnedSlice();
|
||||
}
|
||||
|
||||
pub fn generateFromCLI(
|
||||
transpiler: *Transpiler,
|
||||
alloc: std.mem.Allocator,
|
||||
@@ -1526,12 +1557,9 @@ pub const BundleV2 = struct {
|
||||
|
||||
try this.cloneAST();
|
||||
|
||||
const chunks = try this.linker.link(
|
||||
this,
|
||||
this.graph.entry_points.items,
|
||||
this.graph.server_component_boundaries,
|
||||
reachable_files,
|
||||
);
|
||||
// Filter out copy-only entrypoints (like JSON/PNG files) that don't need JS chunks
|
||||
const entry_points_for_chunking = try this.filterEntryPointsForChunking(alloc);
|
||||
defer alloc.free(entry_points_for_chunking);
|
||||
|
||||
// Do this at the very end, after processing all the imports/exports so that we can follow exports as needed.
|
||||
if (fetcher) |fetch| {
|
||||
@@ -1539,7 +1567,48 @@ pub const BundleV2 = struct {
|
||||
return std.ArrayList(options.OutputFile).init(alloc);
|
||||
}
|
||||
|
||||
return try this.linker.generateChunksInParallel(chunks, false);
|
||||
// Only create chunks if there are JS/CSS/HTML entrypoints
|
||||
if (entry_points_for_chunking.len > 0) {
|
||||
const chunks = try this.linker.link(
|
||||
this,
|
||||
entry_points_for_chunking,
|
||||
this.graph.server_component_boundaries,
|
||||
reachable_files,
|
||||
);
|
||||
|
||||
return try this.linker.generateChunksInParallel(chunks, false);
|
||||
} else {
|
||||
// All entrypoints are copy-only assets - return the additional output files (copied assets)
|
||||
if (this.transpiler.log.errors > 0) {
|
||||
return error.BuildFailed;
|
||||
}
|
||||
|
||||
// Write files to disk if outdir is specified
|
||||
if (this.transpiler.options.output_dir.len > 0) {
|
||||
var root_dir = std.fs.cwd().makeOpenPath(this.transpiler.options.output_dir, .{}) catch |err| {
|
||||
this.transpiler.log.addErrorFmt(null, Logger.Loc.Empty, bun.default_allocator, "Failed to create output directory {s} {}", .{
|
||||
@errorName(err),
|
||||
bun.fmt.quote(this.transpiler.options.output_dir),
|
||||
}) catch unreachable;
|
||||
return err;
|
||||
};
|
||||
defer root_dir.close();
|
||||
|
||||
for (this.graph.additional_output_files.items) |*f| {
|
||||
f.writeToDisk(root_dir, this.transpiler.fs.top_level_dir) catch |err| {
|
||||
this.transpiler.log.addErrorFmt(null, Logger.Loc.Empty, bun.default_allocator, "Failed to write file {s} {}", .{
|
||||
@errorName(err),
|
||||
bun.fmt.quote(f.dest_path),
|
||||
}) catch unreachable;
|
||||
return err;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
var output_files = std.ArrayList(options.OutputFile).init(alloc);
|
||||
try output_files.appendSlice(this.graph.additional_output_files.items);
|
||||
return output_files;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generateFromBakeProductionCLI(
|
||||
@@ -1588,9 +1657,46 @@ pub const BundleV2 = struct {
|
||||
|
||||
try this.cloneAST();
|
||||
|
||||
// Filter out copy-only entrypoints (like JSON/PNG files) that don't need JS chunks
|
||||
const entry_points_for_chunking = try this.filterEntryPointsForChunking(bun.default_allocator);
|
||||
defer bun.default_allocator.free(entry_points_for_chunking);
|
||||
|
||||
if (entry_points_for_chunking.len == 0) {
|
||||
// All entrypoints are copy-only assets - return the additional output files (copied assets)
|
||||
if (this.transpiler.log.errors > 0) {
|
||||
return error.BuildFailed;
|
||||
}
|
||||
|
||||
// Write files to disk if outdir is specified
|
||||
if (this.transpiler.options.output_dir.len > 0) {
|
||||
var root_dir = std.fs.cwd().makeOpenPath(this.transpiler.options.output_dir, .{}) catch |err| {
|
||||
this.transpiler.log.addErrorFmt(null, Logger.Loc.Empty, bun.default_allocator, "Failed to create output directory {s} {}", .{
|
||||
@errorName(err),
|
||||
bun.fmt.quote(this.transpiler.options.output_dir),
|
||||
}) catch unreachable;
|
||||
return err;
|
||||
};
|
||||
defer root_dir.close();
|
||||
|
||||
for (this.graph.additional_output_files.items) |*f| {
|
||||
f.writeToDisk(root_dir, this.transpiler.fs.top_level_dir) catch |err| {
|
||||
this.transpiler.log.addErrorFmt(null, Logger.Loc.Empty, bun.default_allocator, "Failed to write file {s} {}", .{
|
||||
@errorName(err),
|
||||
bun.fmt.quote(f.dest_path),
|
||||
}) catch unreachable;
|
||||
return err;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
var output_files = std.ArrayList(options.OutputFile).init(bun.default_allocator);
|
||||
try output_files.appendSlice(this.graph.additional_output_files.items);
|
||||
return output_files;
|
||||
}
|
||||
|
||||
const chunks = try this.linker.link(
|
||||
this,
|
||||
this.graph.entry_points.items,
|
||||
entry_points_for_chunking,
|
||||
this.graph.server_component_boundaries,
|
||||
reachable_files,
|
||||
);
|
||||
@@ -1632,18 +1738,42 @@ pub const BundleV2 = struct {
|
||||
const additional_files: []BabyList(AdditionalFile) = this.graph.input_files.items(.additional_files);
|
||||
const loaders = this.graph.input_files.items(.loader);
|
||||
|
||||
// Check which files are copy-only entrypoints (should not have hash in filename)
|
||||
const entry_points = this.graph.entry_points.items;
|
||||
|
||||
for (reachable_files) |reachable_source| {
|
||||
const index = reachable_source.get();
|
||||
const key = unique_key_for_additional_files[index];
|
||||
if (key.len > 0) {
|
||||
var template = if (this.graph.html_imports.server_source_indices.len > 0 and this.transpiler.options.asset_naming.len == 0)
|
||||
const loader = loaders[index];
|
||||
|
||||
// Check if this is a copy-only entrypoint
|
||||
const is_copy_only_entrypoint = blk: {
|
||||
if (!loader.shouldCopyAsEntrypoint()) break :blk false;
|
||||
|
||||
// Check if it's in the entry_points list
|
||||
for (entry_points) |entry_point| {
|
||||
if (entry_point.get() == index) break :blk true;
|
||||
}
|
||||
break :blk false;
|
||||
};
|
||||
|
||||
var template = if (is_copy_only_entrypoint) brk: {
|
||||
// Use entry naming for copy-only entrypoints (no hash by default)
|
||||
const entry_naming = this.transpiler.options.entry_naming;
|
||||
if (entry_naming.len > 0) {
|
||||
break :brk PathTemplate{ .data = entry_naming };
|
||||
}
|
||||
// If no entry naming specified, use "[dir]/[name][ext]" (no hash)
|
||||
break :brk PathTemplate{ .data = "[dir]/[name][ext]" };
|
||||
} else if (this.graph.html_imports.server_source_indices.len > 0 and this.transpiler.options.asset_naming.len == 0)
|
||||
PathTemplate.assetWithTarget
|
||||
else
|
||||
PathTemplate.asset;
|
||||
|
||||
const target = targets[index];
|
||||
const asset_naming = this.transpilerForTarget(target).options.asset_naming;
|
||||
if (asset_naming.len > 0) {
|
||||
if (!is_copy_only_entrypoint and asset_naming.len > 0) {
|
||||
template.data = asset_naming;
|
||||
}
|
||||
|
||||
@@ -1671,8 +1801,6 @@ pub const BundleV2 = struct {
|
||||
break :brk bun.handleOom(std.fmt.allocPrint(bun.default_allocator, "{}", .{template}));
|
||||
};
|
||||
|
||||
const loader = loaders[index];
|
||||
|
||||
additional_output_files.append(options.OutputFile.init(.{
|
||||
.source_index = .init(index),
|
||||
.data = .{ .buffer = .{
|
||||
@@ -2635,9 +2763,47 @@ pub const BundleV2 = struct {
|
||||
|
||||
try this.addServerComponentBoundariesAsExtraEntryPoints();
|
||||
|
||||
// Filter out copy-only entrypoints (like JSON/PNG files) that don't need JS chunks
|
||||
const entry_points_for_chunking = try this.filterEntryPointsForChunking(bun.default_allocator);
|
||||
defer bun.default_allocator.free(entry_points_for_chunking);
|
||||
|
||||
if (entry_points_for_chunking.len == 0) {
|
||||
// All entrypoints are copy-only assets
|
||||
if (this.transpiler.log.errors > 0) {
|
||||
return error.BuildFailed;
|
||||
}
|
||||
|
||||
// Write files to disk if outdir is specified
|
||||
if (this.transpiler.options.output_dir.len > 0) {
|
||||
var root_dir = std.fs.cwd().makeOpenPath(this.transpiler.options.output_dir, .{}) catch |err| {
|
||||
this.transpiler.log.addErrorFmt(null, Logger.Loc.Empty, bun.default_allocator, "Failed to create output directory {s} {}", .{
|
||||
@errorName(err),
|
||||
bun.fmt.quote(this.transpiler.options.output_dir),
|
||||
}) catch unreachable;
|
||||
return err;
|
||||
};
|
||||
defer root_dir.close();
|
||||
|
||||
for (this.graph.additional_output_files.items) |*f| {
|
||||
f.writeToDisk(root_dir, this.transpiler.fs.top_level_dir) catch |err| {
|
||||
this.transpiler.log.addErrorFmt(null, Logger.Loc.Empty, bun.default_allocator, "Failed to write file {s} {}", .{
|
||||
@errorName(err),
|
||||
bun.fmt.quote(f.dest_path),
|
||||
}) catch unreachable;
|
||||
return err;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Return the additional output files (copied assets)
|
||||
var output_files = std.ArrayList(options.OutputFile).init(bun.default_allocator);
|
||||
try output_files.appendSlice(this.graph.additional_output_files.items);
|
||||
return output_files;
|
||||
}
|
||||
|
||||
const chunks = try this.linker.link(
|
||||
this,
|
||||
this.graph.entry_points.items,
|
||||
entry_points_for_chunking,
|
||||
this.graph.server_component_boundaries,
|
||||
reachable_files,
|
||||
);
|
||||
|
||||
@@ -691,6 +691,27 @@ pub const Loader = enum(u8) {
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns true if this loader creates "proxy" JS modules that just export file paths/handles.
|
||||
/// When these are used as entrypoints, we should copy the file directly instead of
|
||||
/// creating both a proxy JS file and the asset.
|
||||
///
|
||||
/// Loaders like .json, .text, etc. are NOT included here because they inline content
|
||||
/// into JS modules, which is useful for tree-shaking and optimization.
|
||||
pub fn shouldCopyAsEntrypoint(this: Loader) bool {
|
||||
return switch (this) {
|
||||
// These create proxy JS files that export paths/handles - copy directly for entrypoints
|
||||
.file,
|
||||
.wasm,
|
||||
.napi,
|
||||
.sqlite,
|
||||
.sqlite_embedded,
|
||||
=> true,
|
||||
// Everything else should go through normal bundling
|
||||
// (json, text, yaml, etc. inline their content into JS which is useful)
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn handlesEmptyFile(this: Loader) bool {
|
||||
return switch (this) {
|
||||
.wasm, .file, .text => true,
|
||||
|
||||
277
test/bundler/bun-build-static-entrypoints.test.ts
Normal file
277
test/bundler/bun-build-static-entrypoints.test.ts
Normal file
@@ -0,0 +1,277 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { tempDirWithFiles } from "harness";
|
||||
import { join } from "path";
|
||||
|
||||
describe("Bun.build with static file entrypoints", () => {
|
||||
test("JSON entrypoint should create JS module with inlined content", async () => {
|
||||
const dir = tempDirWithFiles("bun-build-json-entry", {
|
||||
"data.json": JSON.stringify({ hello: "world", foo: 123 }),
|
||||
});
|
||||
|
||||
const build = await Bun.build({
|
||||
entrypoints: [join(dir, "data.json")],
|
||||
outdir: join(dir, "out"),
|
||||
});
|
||||
|
||||
expect(build.success).toBe(true);
|
||||
expect(build.outputs).toHaveLength(1);
|
||||
expect(build.outputs[0].kind).toBe("entry-point");
|
||||
expect(build.outputs[0].path).toEndWith("data.js");
|
||||
|
||||
const content = await build.outputs[0].text();
|
||||
// Should contain the actual JSON data inlined, not a path to a separate file
|
||||
expect(content).toContain("hello");
|
||||
expect(content).toContain("world");
|
||||
expect(content).toContain('var hello = "world"'); // Data should be inlined as JS vars
|
||||
});
|
||||
|
||||
test("file loader entrypoint should copy file directly without JS wrapper", async () => {
|
||||
const dir = tempDirWithFiles("bun-build-file-entry", {
|
||||
"logo.png": Buffer.from([
|
||||
0x89,
|
||||
0x50,
|
||||
0x4e,
|
||||
0x47,
|
||||
0x0d,
|
||||
0x0a,
|
||||
0x1a,
|
||||
0x0a, // PNG signature
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0d,
|
||||
0x49,
|
||||
0x48,
|
||||
0x44,
|
||||
0x52, // IHDR chunk
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01, // 1x1 pixel
|
||||
0x08,
|
||||
0x06,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x1f,
|
||||
0x15,
|
||||
0xc4,
|
||||
0x89,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0a,
|
||||
0x49,
|
||||
0x44,
|
||||
0x41,
|
||||
0x54,
|
||||
0x78,
|
||||
0x9c,
|
||||
0x63,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x05,
|
||||
0x00,
|
||||
0x01,
|
||||
0x0d,
|
||||
0x0a,
|
||||
0x2d,
|
||||
0xb4,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x49,
|
||||
0x45,
|
||||
0x4e,
|
||||
0x44,
|
||||
0xae,
|
||||
0x42,
|
||||
0x60,
|
||||
0x82,
|
||||
]),
|
||||
});
|
||||
|
||||
const build = await Bun.build({
|
||||
entrypoints: [join(dir, "logo.png")],
|
||||
outdir: join(dir, "out"),
|
||||
loader: { ".png": "file" }, // Explicitly use file loader
|
||||
});
|
||||
|
||||
expect(build.success).toBe(true);
|
||||
expect(build.outputs).toHaveLength(1);
|
||||
expect(build.outputs[0].kind).toBe("asset");
|
||||
expect(build.outputs[0].path).toEndWith("logo.png"); // Should preserve original filename (no hash)
|
||||
|
||||
const content = await build.outputs[0].arrayBuffer();
|
||||
const buffer = Buffer.from(content);
|
||||
// Check PNG signature
|
||||
expect(buffer.subarray(0, 8)).toEqual(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]));
|
||||
});
|
||||
|
||||
test("text file with file loader should copy directly", async () => {
|
||||
const dir = tempDirWithFiles("bun-build-text-file", {
|
||||
"readme.txt": "Hello World",
|
||||
});
|
||||
|
||||
const build = await Bun.build({
|
||||
entrypoints: [join(dir, "readme.txt")],
|
||||
outdir: join(dir, "out"),
|
||||
loader: { ".txt": "file" },
|
||||
});
|
||||
|
||||
expect(build.success).toBe(true);
|
||||
expect(build.outputs).toHaveLength(1);
|
||||
expect(build.outputs[0].kind).toBe("asset");
|
||||
expect(build.outputs[0].path).toEndWith("readme.txt"); // Should preserve original filename (no hash)
|
||||
|
||||
const content = await build.outputs[0].text();
|
||||
expect(content).toBe("Hello World");
|
||||
});
|
||||
|
||||
test("wasm entrypoint should copy directly without JS wrapper", async () => {
|
||||
// Minimal valid WASM module
|
||||
const wasmBytes = new Uint8Array([
|
||||
0x00,
|
||||
0x61,
|
||||
0x73,
|
||||
0x6d, // magic number
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00, // version
|
||||
]);
|
||||
|
||||
const dir = tempDirWithFiles("bun-build-wasm-entry", {
|
||||
"module.wasm": Buffer.from(wasmBytes),
|
||||
});
|
||||
|
||||
const build = await Bun.build({
|
||||
entrypoints: [join(dir, "module.wasm")],
|
||||
outdir: join(dir, "out"),
|
||||
});
|
||||
|
||||
expect(build.success).toBe(true);
|
||||
expect(build.outputs).toHaveLength(1);
|
||||
expect(build.outputs[0].kind).toBe("asset");
|
||||
expect(build.outputs[0].path).toEndWith("module.wasm"); // Should preserve original filename (no hash)
|
||||
|
||||
const content = await build.outputs[0].arrayBuffer();
|
||||
const buffer = Buffer.from(content);
|
||||
// Check WASM magic number
|
||||
expect(buffer.subarray(0, 4)).toEqual(Buffer.from([0x00, 0x61, 0x73, 0x6d]));
|
||||
});
|
||||
|
||||
test("multiple file loader entrypoints", async () => {
|
||||
const dir = tempDirWithFiles("bun-build-multi-file", {
|
||||
"a.txt": "File A",
|
||||
"b.txt": "File B",
|
||||
"c.txt": "File C",
|
||||
});
|
||||
|
||||
const build = await Bun.build({
|
||||
entrypoints: [join(dir, "a.txt"), join(dir, "b.txt"), join(dir, "c.txt")],
|
||||
outdir: join(dir, "out"),
|
||||
loader: { ".txt": "file" },
|
||||
});
|
||||
|
||||
expect(build.success).toBe(true);
|
||||
expect(build.outputs).toHaveLength(3);
|
||||
expect(build.outputs.every(o => o.kind === "asset")).toBe(true);
|
||||
expect(build.outputs.every(o => o.path.endsWith(".txt"))).toBe(true);
|
||||
// Should preserve original filenames (no hash)
|
||||
expect(build.outputs.some(o => o.path.endsWith("a.txt"))).toBe(true);
|
||||
expect(build.outputs.some(o => o.path.endsWith("b.txt"))).toBe(true);
|
||||
expect(build.outputs.some(o => o.path.endsWith("c.txt"))).toBe(true);
|
||||
});
|
||||
|
||||
test("importing static files from JS should still create proxy + asset", async () => {
|
||||
// When a JS file imports a static asset, it should create:
|
||||
// 1. JS bundle with the asset path inlined
|
||||
// 2. The hashed asset file
|
||||
const pngData = Buffer.from([
|
||||
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00,
|
||||
0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, 0x00, 0x1f, 0x15, 0xc4, 0x89, 0x00, 0x00, 0x00, 0x0a, 0x49,
|
||||
0x44, 0x41, 0x54, 0x78, 0x9c, 0x63, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0d, 0x0a, 0x2d, 0xb4, 0x00, 0x00,
|
||||
0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82,
|
||||
]);
|
||||
|
||||
const dir = tempDirWithFiles("bun-build-import-static", {
|
||||
"index.js": 'export { default as logo } from "./logo.png";',
|
||||
"logo.png": pngData,
|
||||
});
|
||||
|
||||
const build = await Bun.build({
|
||||
entrypoints: [join(dir, "index.js")],
|
||||
outdir: join(dir, "out"),
|
||||
});
|
||||
|
||||
expect(build.success).toBe(true);
|
||||
expect(build.outputs).toHaveLength(2);
|
||||
|
||||
// Should have 1 entry-point (index.js) and 1 asset (logo.png)
|
||||
const entryPoint = build.outputs.find(o => o.kind === "entry-point");
|
||||
const asset = build.outputs.find(o => o.kind === "asset");
|
||||
|
||||
expect(entryPoint).toBeDefined();
|
||||
expect(asset).toBeDefined();
|
||||
|
||||
expect(entryPoint!.path).toMatch(/index\.js$/);
|
||||
expect(asset!.path).toMatch(/logo.*\.png$/);
|
||||
|
||||
// The JS should contain a reference to the hashed PNG
|
||||
const jsContent = await entryPoint!.text();
|
||||
expect(jsContent).toContain("logo");
|
||||
expect(jsContent).toContain(".png");
|
||||
|
||||
// The asset should be the actual PNG
|
||||
const assetContent = await asset!.arrayBuffer();
|
||||
const buffer = Buffer.from(assetContent);
|
||||
expect(buffer.subarray(0, 8)).toEqual(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]));
|
||||
});
|
||||
|
||||
test("copying files use case - PNG without explicit loader", async () => {
|
||||
// PNG files default to .file loader, so they should be copied directly
|
||||
const pngData = Buffer.from([
|
||||
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00,
|
||||
0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, 0x00, 0x1f, 0x15, 0xc4, 0x89, 0x00, 0x00, 0x00, 0x0a, 0x49,
|
||||
0x44, 0x41, 0x54, 0x78, 0x9c, 0x63, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0d, 0x0a, 0x2d, 0xb4, 0x00, 0x00,
|
||||
0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82,
|
||||
]);
|
||||
|
||||
const dir = tempDirWithFiles("bun-build-copy-png", {
|
||||
"logo.png": pngData,
|
||||
"favicon.png": pngData,
|
||||
});
|
||||
|
||||
const build = await Bun.build({
|
||||
entrypoints: [join(dir, "logo.png"), join(dir, "favicon.png")],
|
||||
outdir: join(dir, "out"),
|
||||
// No explicit loader - PNG defaults to .file
|
||||
});
|
||||
|
||||
expect(build.success).toBe(true);
|
||||
expect(build.outputs).toHaveLength(2);
|
||||
|
||||
// Should produce ONLY asset files, no JS wrappers
|
||||
expect(build.outputs.every(o => o.kind === "asset")).toBe(true);
|
||||
expect(build.outputs.every(o => o.path.endsWith(".png"))).toBe(true);
|
||||
|
||||
// Should preserve original filenames (no hash) for entrypoints
|
||||
expect(build.outputs.some(o => o.path.endsWith("logo.png"))).toBe(true);
|
||||
expect(build.outputs.some(o => o.path.endsWith("favicon.png"))).toBe(true);
|
||||
|
||||
// Verify actual PNG content
|
||||
for (const output of build.outputs) {
|
||||
const content = await output.arrayBuffer();
|
||||
const buffer = Buffer.from(content);
|
||||
expect(buffer.subarray(0, 8)).toEqual(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]));
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -672,15 +672,16 @@ describe("bundler", () => {
|
||||
},
|
||||
outdir: "/out",
|
||||
entryPointsRaw: ["./entry.zig"],
|
||||
runtimeFiles: {
|
||||
"/exec.js": `
|
||||
import assert from 'node:assert';
|
||||
import the_path from './out/entry.js';
|
||||
assert.strictEqual(the_path, './entry-z5artd5z.zig');
|
||||
`,
|
||||
},
|
||||
run: {
|
||||
file: "./exec.js",
|
||||
// With the new behavior, asset entrypoints (file loader) are copied directly
|
||||
// without creating JS wrappers. Need to specify expected output path explicitly.
|
||||
outputPaths: ["/out/entry.zig"],
|
||||
onAfterBundle(api) {
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const outPath = path.join(api.outdir, "entry.zig");
|
||||
api.assertFileExists("out/entry.zig");
|
||||
const content = fs.readFileSync(outPath, "utf8");
|
||||
expect(content).toContain("Hello, world!");
|
||||
},
|
||||
});
|
||||
itBundled("edgecase/ExportDefaultUndefined", {
|
||||
|
||||
@@ -127,8 +127,11 @@ describe("bundler", async () => {
|
||||
},
|
||||
});
|
||||
|
||||
const loaders: Loader[] = ["wasm", "json", "file" /* "napi" */, "text"];
|
||||
const exts = ["wasm", "json", "lmao" /* ".node" */, "txt"];
|
||||
// Test loaders that inline content into JS (json, text)
|
||||
// Note: wasm and file loaders are excluded because when used as entrypoints,
|
||||
// they now copy files directly without creating JS wrappers
|
||||
const loaders: Loader[] = ["json", "text"];
|
||||
const exts = ["json", "txt"];
|
||||
for (let i = 0; i < loaders.length; i++) {
|
||||
const loader = loaders[i];
|
||||
const ext = exts[i];
|
||||
@@ -153,8 +156,6 @@ describe("bundler", async () => {
|
||||
expect(module.default).toStrictEqual({ hello: "friends" });
|
||||
} else if (loader === "text") {
|
||||
expect(module.default).toStrictEqual('{ "hello": "friends" }');
|
||||
} else {
|
||||
api.assertFileExists(join("out", module.default));
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -179,8 +180,6 @@ describe("bundler", async () => {
|
||||
expect(module.default).toStrictEqual({ hello: "friends" });
|
||||
} else if (loader === "text") {
|
||||
expect(module.default).toStrictEqual('{ "hello": "friends" }');
|
||||
} else {
|
||||
api.assertFileExists(join("out", module.default));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user