From 3f04bc02a6f0c4c311a286eba2cd06a82fcf73d5 Mon Sep 17 00:00:00 2001 From: Claude Bot Date: Sat, 14 Feb 2026 15:20:31 +0000 Subject: [PATCH] fix(bundler): include HTML-only asset references in HTMLBundle files array Images and other assets referenced only via HTML tags (, , etc.) were missing from the HTMLBundle manifest's files array. These assets use .url import records which don't propagate entry_bits through the linker's code splitting graph, causing the intersection check in HTMLImportManifest to exclude them. The fix builds a set of source indices directly referenced by the HTML file's import records and includes matching additional output files in the manifest alongside the existing entry_bits check. Closes #27031 Co-Authored-By: Claude --- src/bundler/HTMLImportManifest.zig | 15 +- test/regression/issue/27031.test.ts | 214 ++++++++++++++++++++++++++++ 2 files changed, 228 insertions(+), 1 deletion(-) create mode 100644 test/regression/issue/27031.test.ts diff --git a/src/bundler/HTMLImportManifest.zig b/src/bundler/HTMLImportManifest.zig index d2716e45aa..054e216167 100644 --- a/src/bundler/HTMLImportManifest.zig +++ b/src/bundler/HTMLImportManifest.zig @@ -165,6 +165,19 @@ pub fn write(index: u32, graph: *const Graph, linker_graph: *const LinkerGraph, var already_visited_output_file = try bun.bit_set.AutoBitSet.initEmpty(bun.default_allocator, additional_output_files.len); defer already_visited_output_file.deinit(bun.default_allocator); + // Build a set of source indices directly referenced by the HTML file's import records. + // Assets referenced only via HTML tags (e.g. , ) use .url + // import records which don't propagate entry_bits through the linker's code splitting + // graph. We need to include these files in the manifest so they are served correctly. + const import_records = graph.ast.items(.import_records); + var html_referenced_sources = try bun.bit_set.AutoBitSet.initEmpty(bun.default_allocator, graph.input_files.len); + defer html_referenced_sources.deinit(bun.default_allocator); + for (import_records[browser_source_index].slice()) |*record| { + if (record.source_index.isValid()) { + html_referenced_sources.set(record.source_index.get()); + } + } + // Write all chunks that have files associated with this entry point. // Also include browser chunks from server builds (lazy-loaded chunks from dynamic imports). // When there's only one HTML import, all browser chunks belong to that manifest. @@ -219,7 +232,7 @@ pub fn write(index: u32, graph: *const Graph, linker_graph: *const LinkerGraph, if (source_index.get() == server_source_index) continue; const bits: *const AutoBitSet = &file_entry_bits[source_index.get()]; - if (bits.hasIntersection(&entry_point_bits)) { + if (bits.hasIntersection(&entry_point_bits) or html_referenced_sources.isSet(source_index.get())) { already_visited_output_file.set(i); if (!first) try writer.writeAll(","); first = false; diff --git a/test/regression/issue/27031.test.ts b/test/regression/issue/27031.test.ts new file mode 100644 index 0000000000..64dc92725f --- /dev/null +++ b/test/regression/issue/27031.test.ts @@ -0,0 +1,214 @@ +import { describe, expect } from "bun:test"; +import { itBundled } from "../../bundler/expectBundled"; + +// Small valid PNG bytes for test assets +const pngBytes = Buffer.from([ + 0x89, + 0x50, + 0x4e, + 0x47, + 0x0d, + 0x0a, + 0x1a, + 0x0a, // PNG header + 0x00, + 0x00, + 0x00, + 0x0d, + 0x49, + 0x48, + 0x44, + 0x52, // IHDR chunk + 0x00, + 0x00, + 0x00, + 0x10, + 0x00, + 0x00, + 0x00, + 0x10, // 16x16 + 0x08, + 0x02, + 0x00, + 0x00, + 0x00, + 0x90, + 0x91, + 0x68, // 8-bit RGB + 0x36, + 0x00, + 0x00, + 0x00, + 0x00, + 0x49, + 0x45, + 0x4e, // IEND chunk + 0x44, + 0xae, + 0x42, + 0x60, + 0x82, +]); + +// Different content so each file gets a unique hash +const pngBytes2 = Buffer.from([ + 0x89, + 0x50, + 0x4e, + 0x47, + 0x0d, + 0x0a, + 0x1a, + 0x0a, + 0x00, + 0x00, + 0x00, + 0x0d, + 0x49, + 0x48, + 0x44, + 0x52, + 0x00, + 0x00, + 0x00, + 0x20, + 0x00, + 0x00, + 0x00, + 0x20, // 32x32 + 0x08, + 0x02, + 0x00, + 0x00, + 0x00, + 0xfc, + 0x18, + 0xed, + 0xa3, + 0x00, + 0x00, + 0x00, + 0x00, + 0x49, + 0x45, + 0x4e, + 0x44, + 0xae, + 0x42, + 0x60, + 0x82, +]); + +const pngBytes3 = Buffer.from([ + 0x89, + 0x50, + 0x4e, + 0x47, + 0x0d, + 0x0a, + 0x1a, + 0x0a, + 0x00, + 0x00, + 0x00, + 0x0d, + 0x49, + 0x48, + 0x44, + 0x52, + 0x00, + 0x00, + 0x00, + 0x08, + 0x00, + 0x00, + 0x00, + 0x08, // 8x8 + 0x08, + 0x02, + 0x00, + 0x00, + 0x00, + 0x4b, + 0x6d, + 0x29, + 0xde, + 0x00, + 0x00, + 0x00, + 0x00, + 0x49, + 0x45, + 0x4e, + 0x44, + 0xae, + 0x42, + 0x60, + 0x82, +]); + +describe.concurrent("bundler", () => { + // Regression test: Images referenced only via HTML tags should appear in + // HTMLBundle.files array, not just images imported via JavaScript. + // https://github.com/oven-sh/bun/issues/27031 + itBundled("html-import/html-only-asset-references", { + outdir: "out/", + files: { + "/server.js": ` +import html from "./index.html"; + +const manifest = html; + +// All three images should be in the files array +const fileLoaders = manifest.files.map(f => f.loader); +const fileInputs = manifest.files.map(f => f.input); + +// logo.png and banner.png are only referenced via HTML tags, not JS imports +// icon.png is imported via both HTML and JS +const hasLogo = fileInputs.some(i => i === "logo.png"); +const hasBanner = fileInputs.some(i => i === "banner.png"); +const hasIcon = fileInputs.some(i => i === "icon.png"); + +if (!hasLogo) throw new Error("logo.png missing from manifest files (referenced via )"); +if (!hasBanner) throw new Error("banner.png missing from manifest files (referenced via )"); +if (!hasIcon) throw new Error("icon.png missing from manifest files (referenced via and JS import)"); + +// All image files should have loader "file" +const imageFiles = manifest.files.filter(f => f.path.includes(".png")); +for (const img of imageFiles) { + if (img.loader !== "file") throw new Error("Expected loader 'file' for " + img.path + ", got " + img.loader); + if (!img.headers || !img.headers["content-type"]) throw new Error("Missing content-type header for " + img.path); +} + +console.log("OK: " + imageFiles.length + " image files in manifest"); +`, + "/index.html": ` + + + + + + + Logo + Banner + + +`, + "/app.js": ` +import icon from './icon.png'; +console.log("Icon imported via JS:", icon); +`, + "/logo.png": pngBytes, + "/banner.png": pngBytes2, + "/icon.png": pngBytes3, + }, + entryPoints: ["/server.js"], + target: "bun", + + run: { + validate({ stdout }) { + expect(stdout).toContain("OK: 3 image files in manifest"); + }, + }, + }); +});