Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
73fcc6df71 fix(html): skip resolving URLs inside <template> tags
The HTML bundler was incorrectly resolving `src`, `href`, and other URL
attributes on elements inside `<template>` tags. Per the HTML spec,
template content is inert and should not be processed by the bundler.

Track `<template>` nesting depth in `HTMLProcessor` and skip URL
resolution for any elements inside templates.

Closes #27938

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-09 08:43:17 +00:00
3 changed files with 110 additions and 1 deletions

View File

@@ -4,6 +4,7 @@ allocator: std.mem.Allocator,
import_records: ImportRecord.List = .{},
log: *logger.Log,
source: *const logger.Source,
template_depth: u32 = 0,
pub fn init(allocator: std.mem.Allocator, log: *logger.Log, source: *const logger.Source) HTMLScanner {
return .{
@@ -201,6 +202,9 @@ pub fn HTMLProcessor(
fn generateHandlerForTag(comptime tag_info: TagHandler) fn (*T, *lol.Element) bool {
const Handler = struct {
pub fn handle(this: *T, element: *lol.Element) bool {
// Skip elements inside <template> tags — template content is inert HTML
if (this.template_depth > 0) return false;
// Handle URL attribute if present
if (tag_info.url_attribute.len > 0) {
if (element.hasAttribute(tag_info.url_attribute) catch false) {
@@ -218,15 +222,46 @@ pub fn HTMLProcessor(
return Handler.handle;
}
fn templateOpenHandler(this: *T, element: *lol.Element) bool {
this.template_depth += 1;
element.onEndTag(templateEndHandler, @ptrCast(this)) catch return true;
return false;
}
fn templateEndHandler(_: *lol.EndTag, opaque_this: ?*anyopaque) callconv(.c) lol.Directive {
const this: *T = @ptrCast(@alignCast(opaque_this.?));
this.template_depth -= 1;
return .@"continue";
}
pub fn run(this: *T, input: []const u8) !void {
var builder = lol.HTMLRewriter.Builder.init();
defer builder.deinit();
var selectors: bun.BoundedArray(*lol.HTMLSelector, tag_handlers.len + if (visit_document_tags) 3 else 0) = .{};
// +1 for the <template> depth tracking handler
var selectors: bun.BoundedArray(*lol.HTMLSelector, tag_handlers.len + 1 + if (visit_document_tags) 3 else 0) = .{};
defer for (selectors.slice()) |selector| {
selector.deinit();
};
// Track <template> depth so we skip resolving URLs inside inert template content
{
const template_selector = try lol.HTMLSelector.parse("template");
selectors.appendAssumeCapacity(template_selector);
try builder.addElementContentHandlers(
template_selector,
T,
templateOpenHandler,
this,
void,
null,
null,
void,
null,
null,
);
}
// Add handlers for each tag type
inline for (tag_handlers) |tag_info| {
const selector = try lol.HTMLSelector.parse(tag_info.selector);

View File

@@ -51,6 +51,7 @@ fn generateCompileResultForHTMLChunkImpl(worker: *ThreadPool.Worker, c: *LinkerC
},
added_head_tags: bool,
added_body_script: bool,
template_depth: u32 = 0,
pub fn onWriteHTML(this: *@This(), bytes: []const u8) void {
bun.handleOom(this.output.appendSlice(bytes));

View File

@@ -0,0 +1,73 @@
import { describe, expect } from "bun:test";
import { itBundled } from "../../bundler/expectBundled";
describe("bundler", () => {
// https://github.com/oven-sh/bun/issues/27938
// HTML bundler should not resolve URLs inside <template> tags
itBundled("html/TemplateTagNotProcessed", {
outdir: "out/",
files: {
"/index.html": `<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Document</title>
</head>
<body>
<template>
<img src="./assets/book-image" alt="Book" />
</template>
<p>Some text.</p>
</body>
</html>`,
},
entryPoints: ["/index.html"],
onAfterBundle(api) {
const html = api.readFile("out/index.html");
// The <template> content should be preserved as-is
expect(html).toContain('<img src="./assets/book-image" alt="Book"');
// The <template> tags should still be present
expect(html).toContain("<template>");
expect(html).toContain("</template>");
},
});
// Nested templates should also be skipped
itBundled("html/NestedTemplateTagNotProcessed", {
outdir: "out/",
files: {
"/index.html": `<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Document</title>
<link rel="stylesheet" href="./styles.css">
</head>
<body>
<template>
<template>
<img src="./nested-image.png" alt="Nested" />
</template>
<video src="./video.mp4"></video>
</template>
<img src="./real-image.png" alt="Real" />
</body>
</html>`,
"/styles.css": "body { color: red; }",
"/real-image.png": "fake-png-data",
},
entryPoints: ["/index.html"],
onAfterBundle(api) {
const html = api.readFile("out/index.html");
// URLs inside <template> should be preserved as-is
expect(html).toContain('src="./nested-image.png"');
expect(html).toContain('src="./video.mp4"');
// URL outside <template> should be rewritten (hashed)
expect(html).not.toContain('src="./real-image.png"');
// The stylesheet should be processed
expect(html).not.toContain('href="./styles.css"');
},
});
});