diff --git a/src/bundler/LinkerContext.zig b/src/bundler/LinkerContext.zig
index e05da53777..793fe3f32e 100644
--- a/src/bundler/LinkerContext.zig
+++ b/src/bundler/LinkerContext.zig
@@ -821,11 +821,28 @@ pub const LinkerContext = struct {
}
pub fn generateIsolatedHash(c: *LinkerContext, chunk: *const Chunk) u64 {
+ return c.generateIsolatedHashWithChunks(chunk, &[_]Chunk{});
+ }
+
+ pub fn generateIsolatedHashWithChunks(c: *LinkerContext, chunk: *const Chunk, chunks: []Chunk) u64 {
const trace = bun.perf.trace("Bundler.generateIsolatedHash");
defer trace.end();
var hasher = ContentHasher{};
+ // For HTML chunks, include the isolated hashes of the JS and CSS chunks they depend on
+ // This ensures the HTML chunk hash changes when its dependencies change
+ if (chunk.content == .html and chunks.len > 0) {
+ if (chunk.getJSChunkForHTML(chunks)) |js_chunk| {
+ const hash_bytes = std.mem.asBytes(&js_chunk.isolated_hash);
+ hasher.write(hash_bytes);
+ }
+ if (chunk.getCSSChunkForHTML(chunks)) |css_chunk| {
+ const hash_bytes = std.mem.asBytes(&css_chunk.isolated_hash);
+ hasher.write(hash_bytes);
+ }
+ }
+
// Mix the file names and part ranges of all of the files in this chunk into
// the hash. Objects that appear identical but that live in separate files or
// that live in separate parts in the same file must not be merged. This only
diff --git a/src/bundler/linker_context/generateChunksInParallel.zig b/src/bundler/linker_context/generateChunksInParallel.zig
index 1cc1a05bf1..88e183817f 100644
--- a/src/bundler/linker_context/generateChunksInParallel.zig
+++ b/src/bundler/linker_context/generateChunksInParallel.zig
@@ -181,15 +181,22 @@ pub fn generateChunksInParallel(
const chunks_to_do = if (is_dev_server) chunks[1..] else chunks;
if (!is_dev_server or chunks_to_do.len > 0) {
bun.assert(chunks_to_do.len > 0);
- debug(" START {d} postprocess chunks", .{chunks_to_do.len});
- defer debug(" DONE {d} postprocess chunks", .{chunks_to_do.len});
- try c.parse_graph.pool.worker_pool.eachPtr(
- c.allocator(),
- chunk_contexts[0],
- generateChunk,
- chunks_to_do,
- );
+ // Process JS and CSS chunks first (so their isolated_hash is computed)
+ // before processing HTML chunks (which depend on those hashes)
+ // First, process all non-HTML chunks
+ for (chunks_to_do, 0..) |*chunk, i| {
+ if (chunk.content != .html) {
+ generateChunk(chunk_contexts[0], chunk, i);
+ }
+ }
+
+ // Then process HTML chunks (which can now use the computed hashes)
+ for (chunks_to_do, 0..) |*chunk, i| {
+ if (chunk.content == .html) {
+ generateChunk(chunk_contexts[0], chunk, i);
+ }
+ }
}
}
diff --git a/src/bundler/linker_context/postProcessHTMLChunk.zig b/src/bundler/linker_context/postProcessHTMLChunk.zig
index 65ff00a67c..cba3b854b5 100644
--- a/src/bundler/linker_context/postProcessHTMLChunk.zig
+++ b/src/bundler/linker_context/postProcessHTMLChunk.zig
@@ -22,7 +22,7 @@ pub fn postProcessHTMLChunk(ctx: GenerateChunkCtx, worker: *ThreadPool.Worker, c
@as(u32, @truncate(ctx.chunks.len)),
) catch |err| bun.handleOom(err);
- chunk.isolated_hash = c.generateIsolatedHash(chunk);
+ chunk.isolated_hash = c.generateIsolatedHashWithChunks(chunk, ctx.chunks);
}
const bun = @import("bun");
diff --git a/test/bundler/bundler_html_entrypoint_hash.test.ts b/test/bundler/bundler_html_entrypoint_hash.test.ts
new file mode 100644
index 0000000000..2a54cd217a
--- /dev/null
+++ b/test/bundler/bundler_html_entrypoint_hash.test.ts
@@ -0,0 +1,162 @@
+import { describe, test, expect } from "bun:test";
+import { bunEnv, bunExe, tempDir } from "harness";
+import { join } from "path";
+
+describe("HTML entrypoint isolated_hash", () => {
+ test("HTML chunk hash should change when JS dependencies change", async () => {
+ using dir = tempDir("html-hash-js-test", {
+ "index.html": `
+
+
+ Test
+
+
+
+ Hello World
+
+`,
+ "index.js": `console.log("version 1");`,
+ });
+
+ // First build
+ const result1 = await Bun.build({
+ entrypoints: [join(String(dir), "index.html")],
+ outdir: join(String(dir), "dist1"),
+ naming: "[name]-[hash].[ext]",
+ });
+
+ expect(result1.success).toBe(true);
+
+ // Find HTML output
+ const htmlOutput1 = result1.outputs.find((o) => o.path.endsWith(".html"));
+ expect(htmlOutput1).toBeDefined();
+ const htmlPath1 = htmlOutput1!.path;
+ const htmlHash1 = htmlPath1.match(/index-([a-z0-9]+)\.html/)?.[1];
+ expect(htmlHash1).toBeDefined();
+
+ // Modify JS
+ await Bun.write(join(String(dir), "index.js"), `console.log("version 2");`);
+
+ // Second build
+ const result2 = await Bun.build({
+ entrypoints: [join(String(dir), "index.html")],
+ outdir: join(String(dir), "dist2"),
+ naming: "[name]-[hash].[ext]",
+ });
+
+ expect(result2.success).toBe(true);
+
+ // Find HTML output
+ const htmlOutput2 = result2.outputs.find((o) => o.path.endsWith(".html"));
+ expect(htmlOutput2).toBeDefined();
+ const htmlPath2 = htmlOutput2!.path;
+ const htmlHash2 = htmlPath2.match(/index-([a-z0-9]+)\.html/)?.[1];
+ expect(htmlHash2).toBeDefined();
+
+ // Check if HTML hash changed when JS changed
+ expect(htmlHash1).not.toBe(htmlHash2);
+ });
+
+ test("HTML chunk hash should change when CSS dependencies change", async () => {
+ using dir = tempDir("html-hash-css-test", {
+ "index.html": `
+
+
+ Test
+
+
+
+ Hello World
+
+`,
+ "index.css": `body { color: red; }`,
+ });
+
+ // First build
+ const result1 = await Bun.build({
+ entrypoints: [join(String(dir), "index.html")],
+ outdir: join(String(dir), "dist1"),
+ naming: "[name]-[hash].[ext]",
+ });
+
+ expect(result1.success).toBe(true);
+
+ // Find HTML output
+ const htmlOutput1 = result1.outputs.find((o) => o.path.endsWith(".html"));
+ expect(htmlOutput1).toBeDefined();
+ const htmlPath1 = htmlOutput1!.path;
+ const htmlHash1 = htmlPath1.match(/index-([a-z0-9]+)\.html/)?.[1];
+ expect(htmlHash1).toBeDefined();
+
+ // Modify CSS
+ await Bun.write(join(String(dir), "index.css"), `body { color: blue; }`);
+
+ // Second build
+ const result2 = await Bun.build({
+ entrypoints: [join(String(dir), "index.html")],
+ outdir: join(String(dir), "dist2"),
+ naming: "[name]-[hash].[ext]",
+ });
+
+ expect(result2.success).toBe(true);
+
+ // Find HTML output
+ const htmlOutput2 = result2.outputs.find((o) => o.path.endsWith(".html"));
+ expect(htmlOutput2).toBeDefined();
+ const htmlPath2 = htmlOutput2!.path;
+ const htmlHash2 = htmlPath2.match(/index-([a-z0-9]+)\.html/)?.[1];
+ expect(htmlHash2).toBeDefined();
+
+ // Check if HTML hash changed when CSS changed
+ expect(htmlHash1).not.toBe(htmlHash2);
+ });
+
+ test("HTML chunk hash should not change when dependencies don't change", async () => {
+ using dir = tempDir("html-hash-stable-test", {
+ "index.html": `
+
+
+ Test
+
+
+
+
+ Hello World
+
+`,
+ "index.js": `console.log("stable");`,
+ "index.css": `body { color: green; }`,
+ });
+
+ // First build
+ const result1 = await Bun.build({
+ entrypoints: [join(String(dir), "index.html")],
+ outdir: join(String(dir), "dist1"),
+ naming: "[name]-[hash].[ext]",
+ });
+
+ expect(result1.success).toBe(true);
+
+ // Second build without any changes
+ const result2 = await Bun.build({
+ entrypoints: [join(String(dir), "index.html")],
+ outdir: join(String(dir), "dist2"),
+ naming: "[name]-[hash].[ext]",
+ });
+
+ expect(result2.success).toBe(true);
+
+ // Find HTML outputs
+ const htmlOutput1 = result1.outputs.find((o) => o.path.endsWith(".html"));
+ const htmlOutput2 = result2.outputs.find((o) => o.path.endsWith(".html"));
+
+ expect(htmlOutput1).toBeDefined();
+ expect(htmlOutput2).toBeDefined();
+
+ const htmlHash1 = htmlOutput1!.path.match(/index-([a-z0-9]+)\.html/)?.[1];
+ const htmlHash2 = htmlOutput2!.path.match(/index-([a-z0-9]+)\.html/)?.[1];
+
+ // Hashes should be the same when nothing changes
+ expect(htmlHash1).toBe(htmlHash2);
+ });
+});
\ No newline at end of file