Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
7dc54c1cff Fix HTML imports using relative paths at nested routes in compiled executables
Fixes #23431

When HTML files were compiled into executables and served at different routes,
script and style tags used relative paths (e.g., `./chunk.js`), which failed
to load correctly from nested routes like `/foo/bar`.

The issue occurred because:
1. HTML chunks generated script tags with unique keys
2. These keys were replaced with file paths during bundling
3. The path replacement used relative paths when public_path was empty
4. Relative paths resolve differently depending on the URL path

The fix ensures that when compiling executables without a custom public_path,
HTML asset references use absolute paths (`/chunk.js` instead of `./chunk.js`).
This allows the HTML to be served at any route while correctly loading assets.

Changes:
- Modified `generateChunksInParallel.zig` to use "/" as public_path for compiled
  executables when no custom public_path is set
- Applied same fix to `writeOutputFilesToDisk.zig` for consistency
- Added regression test to verify fix
2025-10-10 05:21:06 +00:00
3 changed files with 90 additions and 2 deletions

View File

@@ -327,8 +327,14 @@ pub fn generateChunksInParallel(
for (chunks, 0..) |*chunk, chunk_index_in_chunks_list| {
var display_size: usize = 0;
// For compiled executables with HTML imports, use absolute paths for asset references.
// This ensures scripts/styles load correctly when HTML is served at different routes.
// Without this, relative paths like "./chunk.js" would resolve differently
// from /foo vs /foo/bar, causing asset loading failures at nested routes.
const public_path = if (chunk.is_browser_chunk_from_server_build)
bundler.transpilerForTarget(.browser).options.public_path
else if (c.resolver.opts.compile and c.options.public_path.len == 0)
"/"
else
c.options.public_path;
@@ -340,7 +346,7 @@ pub fn generateChunksInParallel(
chunk,
chunks,
&display_size,
c.resolver.opts.compile and !chunk.is_browser_chunk_from_server_build,
c.resolver.opts.compile and (!chunk.is_browser_chunk_from_server_build or chunk.content == .html),
chunk.content.sourcemap(c.options.source_maps) != .none,
);
var code_result = _code_result catch @panic("Failed to allocate memory for output file");

View File

@@ -62,6 +62,8 @@ pub fn writeOutputFilesToDisk(
var display_size: usize = 0;
const public_path = if (chunk.is_browser_chunk_from_server_build)
bv2.transpilerForTarget(.browser).options.public_path
else if (c.resolver.opts.compile and c.resolver.opts.public_path.len == 0)
"/"
else
c.resolver.opts.public_path;
@@ -73,7 +75,7 @@ pub fn writeOutputFilesToDisk(
chunk,
chunks,
&display_size,
c.resolver.opts.compile and !chunk.is_browser_chunk_from_server_build,
c.resolver.opts.compile and (!chunk.is_browser_chunk_from_server_build or chunk.content == .html),
chunk.content.sourcemap(c.options.source_maps) != .none,
) catch |err| bun.Output.panic("Failed to create output chunk: {s}", .{@errorName(err)});

View File

@@ -0,0 +1,80 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
import { join } from "path";
// https://github.com/oven-sh/bun/issues/23431
test("HTML imports in compiled executable use absolute paths for assets", async () => {
using dir = tempDir("issue-23431", {
"server.ts": `
import indexPage from "./index.html";
const server = Bun.serve({
port: 0,
routes: {
'/*': indexPage
}
});
console.log(\`PORT:\${server.port}\`);
`,
"index.html": `<!doctype html>
<html>
<head>
<title>Test</title>
<script type="module" src="./client.ts"></script>
</head>
<body></body>
</html>`,
"client.ts": `console.log("loaded");`,
});
// Compile the server
const buildResult = Bun.spawnSync({
cmd: [bunExe(), "build", "server.ts", "--compile", "--outfile", "server"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
expect(buildResult.exitCode).toBe(0);
// Run the compiled server
await using server = Bun.spawn({
cmd: [join(String(dir), "server")],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
// Read port from stdout
const reader = server.stdout.getReader();
const { value } = await reader.read();
const output = new TextDecoder().decode(value);
const portMatch = output.match(/PORT:(\d+)/);
if (!portMatch) {
throw new Error(`Could not find port in output: ${output}`);
}
const port = parseInt(portMatch[1]);
try {
// Test that assets use absolute paths at nested routes
const response = await fetch(`http://localhost:${port}/foo/bar`);
const html = await response.text();
// Should use /chunk.js (absolute), not ./chunk.js (relative)
// The chunk name will have a hash, so we check for the pattern
expect(html).toMatch(/src="\/chunk-[a-z0-9]+\.js"/);
expect(html).not.toContain('src="./chunk-');
// Also verify at root path
const rootResponse = await fetch(`http://localhost:${port}/`);
const rootHtml = await rootResponse.text();
expect(rootHtml).toMatch(/src="\/chunk-[a-z0-9]+\.js"/);
} finally {
server.kill();
}
});