From 74cfdcdd197b9c50553fbda2f8cee2469270b43a Mon Sep 17 00:00:00 2001 From: Claude Bot Date: Sat, 25 Oct 2025 05:28:38 +0000 Subject: [PATCH] Fix script injection for HTML with no source scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When bundling HTML that has no script tags in the source, we now inject the bundled scripts/CSS before instead of before . This is done by removing the immediate injection in endHtmlTagHandler for production bundling and handling it in post-processing instead, where we can search for and insert at that position. The fix resolves the "basic plugin" test failure while maintaining all other test passes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../generateCompileResultForHtmlChunk.zig | 65 ++++++++++++++----- 1 file changed, 48 insertions(+), 17 deletions(-) diff --git a/src/bundler/linker_context/generateCompileResultForHtmlChunk.zig b/src/bundler/linker_context/generateCompileResultForHtmlChunk.zig index 2dc342bc76..2e5e3de1d5 100644 --- a/src/bundler/linker_context/generateCompileResultForHtmlChunk.zig +++ b/src/bundler/linker_context/generateCompileResultForHtmlChunk.zig @@ -204,14 +204,13 @@ fn generateCompileResultForHTMLChunkImpl(worker: *ThreadPool.Worker, c: *LinkerC return .@"continue"; } - fn endHtmlTagHandler(end: *lol.EndTag, opaque_this: ?*anyopaque) callconv(.C) lol.Directive { + fn endHtmlTagHandler(_: *lol.EndTag, opaque_this: ?*anyopaque) callconv(.C) lol.Directive { const this: *@This() = @alignCast(@ptrCast(opaque_this.?)); - if (this.linker.dev_server == null) { - // Fallback: inject at end of html if no head or body tags injected yet - this.addHeadTags(end) catch return .stop; - } else { + if (this.linker.dev_server != null) { this.end_tag_indices.html = @intCast(this.output.items.len); } + // For production bundling, don't inject here - let post-processing handle it + // so we can search for and inject there for HTML with no scripts return .@"continue"; } }; @@ -250,11 +249,11 @@ fn generateCompileResultForHTMLChunkImpl(worker: *ThreadPool.Worker, c: *LinkerC // element. In this case, we do a simple search through the page. // See https://github.com/oven-sh/bun/issues/17554 const script_injection_offset: u32 = if (c.dev_server != null) brk: { - // Original dev server logic - try head first, then body - if (html_loader.end_tag_indices.head) |head| - break :brk head; - if (bun.strings.indexOf(html_loader.output.items, "")) |head| - break :brk @intCast(head); + // Dev server logic - try head first, then body, then html, then end of file + if (html_loader.end_tag_indices.head) |idx| + break :brk idx; + if (bun.strings.indexOf(html_loader.output.items, "")) |idx| + break :brk @intCast(idx); if (html_loader.end_tag_indices.body) |body| break :brk body; if (html_loader.end_tag_indices.html) |html| @@ -262,13 +261,45 @@ fn generateCompileResultForHTMLChunkImpl(worker: *ThreadPool.Worker, c: *LinkerC break :brk @intCast(html_loader.output.items.len); // inject at end of file. } else brk: { if (!html_loader.added_head_tags) { - @branchHint(.cold); // this is if the document is missing all head, body, and html elements. - var html_appender = std.heap.stackFallback(256, bun.default_allocator); - const allocator = html_appender.get(); - const slices = html_loader.getHeadTags(allocator); - for (slices.slice()) |slice| { - bun.handleOom(html_loader.output.appendSlice(slice)); - allocator.free(slice); + // If we never injected during parsing, try to inject at position + // This happens when the HTML has no scripts in the source + if (bun.strings.indexOf(html_loader.output.items, "")) |head_idx| { + // Found , insert before it + var html_appender = std.heap.stackFallback(256, bun.default_allocator); + const allocator = html_appender.get(); + const slices = html_loader.getHeadTags(allocator); + defer for (slices.slice()) |slice| + allocator.free(slice); + + // Calculate total size needed for inserted tags + var total_insert_size: usize = 0; + for (slices.slice()) |slice| + total_insert_size += slice.len; + + // Make room for the tags before + const old_len = html_loader.output.items.len; + bun.handleOom(html_loader.output.resize(old_len + total_insert_size)); + + // Move everything after to make room + const items = html_loader.output.items; + std.mem.copyBackwards(u8, items[head_idx + total_insert_size .. items.len], items[head_idx..old_len]); + + // Insert the tags + var offset: usize = head_idx; + for (slices.slice()) |slice| { + @memcpy(items[offset .. offset + slice.len], slice); + offset += slice.len; + } + } else { + @branchHint(.cold); // this is if the document is missing all head, body, and html elements. + // No tag found - fallback to appending at end + var html_appender = std.heap.stackFallback(256, bun.default_allocator); + const allocator = html_appender.get(); + const slices = html_loader.getHeadTags(allocator); + for (slices.slice()) |slice| { + bun.handleOom(html_loader.output.appendSlice(slice)); + allocator.free(slice); + } } } break :brk if (Environment.isDebug) undefined else 0; // value is ignored. fail loud if hit in debug