Compare commits

...

3 Commits

Author SHA1 Message Date
Claude Bot
e105a88bc5 fix(bundler): copy static file entrypoints directly without JS wrappers
When static files (PNG, WASM, etc.) are used as entrypoints, they are now
copied directly to the output directory without creating JavaScript wrapper
files. This fixes the unexpected behavior where `bun build logo.png` would
create both `logo.js` (proxy) and `logo-[hash].png` (asset).

Changes:
- Added `Loader.shouldCopyAsEntrypoint()` to identify loaders that create
  proxy JS modules (.file, .wasm, .napi, .sqlite)
- Filter copy-only entrypoints before chunk creation to avoid generating
  unnecessary JS wrappers
- Mark copy-only entrypoints for file copying during parse phase
- Generate unique keys for copy-only entrypoints so they get content hashing
- Write files to disk when all entrypoints are copy-only assets
- Use entry naming (no hash) for entrypoints, asset naming (with hash) for imports

Behavior changes:
- Entrypoints: `bun build logo.png` → `logo.png` (no JS wrapper, no hash)
- Imports: `import logo from './logo.png'` → `logo-[hash].png` (JS inlined, asset hashed)
- JSON/YAML/text still create JS modules (content inlined for tree-shaking)

Test updates:
- Updated bundler_loader.test.ts to remove .file and .wasm from entrypoint tests
  (they now copy directly, so no JS wrapper to test)
- Updated bundler_edgecase.test.ts AssetEntryPoint to expect direct file copy
  instead of JS wrapper

Fixes the issue where bun build creates unnecessary JS wrapper files for
static asset entrypoints.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-21 10:21:58 +00:00
Claude Bot
93b1527faa fix: entrypoints preserve filename, imports get hash
- Copy-only entrypoints now preserve original filenames (no hash)
- Imported assets still get content hash for cache busting
- Uses entry naming template for entrypoints, asset naming for imports
- This allows: bun build logo.png → logo.png (entrypoint)
- But: import logo from './logo.png' → logo-[hash].png (import)
- Prevents accidental duplicate files, as you noted is fine for this edge case
2025-10-20 20:34:07 +00:00
Claude Bot
5c7f3dc4f3 fix(bundler): copy file/wasm/napi entrypoints directly without JS wrapper
When files with the .file, .wasm, or .napi loaders are used as entrypoints,
they should be copied directly instead of creating both a JS proxy file that
exports a path string AND the asset file.

Previously, `bun build logo.png --loader .png:file` would create:
- `logo.js` - A JS module that exports "./logo-[hash].png"
- `logo-[hash].png` - The actual PNG file

This was confusing because users expected just the PNG file to be copied.

Now, file/wasm/napi loader entrypoints are copied directly:
- `logo-[hash].png` - Just the PNG file

This enables the legitimate use case of copying files:
  bun build logo.png favicon.ico assets/* --outdir dist

Note: JSON, YAML, text, etc. are NOT affected by this change. They continue
to be bundled into JS modules with inlined content, which is useful for:
- Tree-shaking unused properties
- Minification
- Type safety with TypeScript

Only loaders that create "proxy" JS files (file, wasm, napi) skip the JS
wrapper when used as entrypoints.

Importing static files from JS still works correctly:
  import logo from './logo.png'  // Creates JS bundle + hashed asset

Changes:
- Updated `shouldCopyAsEntrypoint()` to only include proxy-creating loaders
- Filter entrypoints before chunk creation to separate JS/CSS/HTML from copy-only
- Mark copy-only entrypoints for file copying during parse phase
- Generate unique keys for copy-only entrypoints so they get hashed properly
- Return asset files directly when all entrypoints are copy-only (no chunks)
- Added comprehensive tests for both entrypoint and import scenarios

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 20:14:41 +00:00
6 changed files with 514 additions and 32 deletions

View File

@@ -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,
};

View File

@@ -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,
);

View File

@@ -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,

View 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]));
}
});
});

View File

@@ -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", {

View File

@@ -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));
}
},
});