Compare commits

...

7 Commits

Author SHA1 Message Date
Claude Bot
d68da6fb05 test(bundler): reuse TextDecoder instance in port detection loop
Address review feedback: move TextDecoder instantiation outside the
loop to avoid recreating it each iteration. Also use stream: true
option for proper incremental decoding.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 00:50:31 +00:00
Claude Bot
98df25e85c test(bundler): improve error message when port detection fails
Address review feedback: when port detection fails, include the
captured stderr output in the error message to aid debugging.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 00:45:30 +00:00
Claude Bot
279ef4fb4c test(bundler): handle Windows .exe extension for compiled binary
Address review feedback: on Windows the compiler may append ".exe"
to the output file, so check for both paths before spawning.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 00:40:31 +00:00
Claude Bot
8dce97dfc6 test(bundler): add status check before reading response body
Address review feedback: explicitly check HTTP status is 200
before reading the response text.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 00:28:31 +00:00
Claude Bot
960ae22cd4 test(bundler): use dynamic port in compile asset paths test
Address review feedback:
- Use port: 0 to let the OS assign a dynamic port
- Parse the port from stderr output instead of using hardcoded port
- Remove explicit timeout option (use default)
- Read stderr with getReader() to detect when server is ready

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 00:23:44 +00:00
Claude Bot
aa2ff1bbf7 test(bundler): improve test for compile asset paths
- Skip test in debug builds (compile is unreliable with debug builds)
- Use fixed port instead of parsing output (simpler and more reliable)
- Use HTTP polling to wait for server ready
- Use await using for automatic process cleanup
- Add timeout to handle slow builds

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 23:57:42 +00:00
Claude Bot
b033c3defe fix(bundler): use absolute paths for assets in compile mode
When building fullstack apps with `--compile`, asset paths in HTML
were relative (starting with `./`) which caused 404 errors when
navigating to routes other than `/`. For example, `/about` would
try to load `/about/style.css` instead of `/style.css`.

This fix:
1. Sets asset/chunk/entry naming templates to use `/` prefix for
   client transpiler in compile mode
2. Preserves absolute paths in `cheapPrefixNormalizer` instead of
   prepending `./`
3. Skips relative path calculation when paths start with `/`

Fixes #24180

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 22:51:29 +00:00
3 changed files with 164 additions and 8 deletions

View File

@@ -240,9 +240,12 @@ pub const Chunk = struct {
.none => unreachable,
};
// If the path is absolute (starts with `/`), use it directly without computing relative path.
// This preserves absolute paths for compile mode where assets should be served from root.
const is_absolute_path = strings.startsWithChar(file_path, '/');
const cheap_normalizer = cheapPrefixNormalizer(
import_prefix,
if (from_chunk_dir.len == 0 or force_absolute_path)
if (from_chunk_dir.len == 0 or force_absolute_path or is_absolute_path)
file_path
else
bun.path.relativePlatformBuf(relative_platform_buf, from_chunk_dir, file_path, .posix, false),
@@ -332,12 +335,16 @@ pub const Chunk = struct {
// normalize windows paths to '/'
bun.path.platformToPosixInPlace(u8, @constCast(file_path));
// If the path is absolute (starts with `/`), use it directly without computing relative path.
// This preserves absolute paths for compile mode where assets should be served from root.
const is_absolute_path = strings.startsWithChar(file_path, '/');
const used_file_path = if (from_chunk_dir.len == 0 or force_absolute_path or is_absolute_path)
file_path
else
bun.path.relativePlatformBuf(relative_platform_buf, from_chunk_dir, file_path, .posix, false);
const cheap_normalizer = cheapPrefixNormalizer(
import_prefix,
if (from_chunk_dir.len == 0 or force_absolute_path)
file_path
else
bun.path.relativePlatformBuf(relative_platform_buf, from_chunk_dir, file_path, .posix, false),
used_file_path,
);
if (cheap_normalizer[0].len > 0) {

View File

@@ -206,9 +206,12 @@ pub const BundleV2 = struct {
// We need to make sure it has [hash] in the names so we don't get conflicts.
if (this_transpiler.options.compile) {
client_transpiler.options.asset_naming = bun.options.PathTemplate.asset.data;
client_transpiler.options.chunk_naming = bun.options.PathTemplate.chunk.data;
client_transpiler.options.entry_naming = "./[name]-[hash].[ext]";
// Use absolute paths (starting with `/`) so that assets are correctly
// resolved from any route, not just the root. Relative paths like
// `./style.css` would resolve to `/about/style.css` on the `/about` route.
client_transpiler.options.asset_naming = "/[name]-[hash].[ext]";
client_transpiler.options.chunk_naming = "/[name]-[hash].[ext]";
client_transpiler.options.entry_naming = "/[name]-[hash].[ext]";
// Avoid setting a public path for --compile since all the assets
// will be served relative to the server root.
@@ -4509,6 +4512,11 @@ pub const ContentHasher = struct {
// this is just being nice
pub fn cheapPrefixNormalizer(prefix: []const u8, suffix: []const u8) [2]string {
if (prefix.len == 0) {
// If path is absolute (starts with /), don't add ./ prefix.
// This preserves absolute paths for compile mode where assets should be served from root.
if (strings.startsWithChar(suffix, '/')) {
return .{ "", suffix };
}
const suffix_no_slash = bun.strings.removeLeadingDotSlash(suffix);
return .{
if (strings.hasPrefixComptime(suffix_no_slash, "../")) "" else "./",

View File

@@ -0,0 +1,141 @@
import { expect, test } from "bun:test";
import fs from "fs";
import { bunEnv, bunExe, isDebug, tempDirWithFiles } from "harness";
import path from "path";
// https://github.com/oven-sh/bun/issues/24180
// When building fullstack apps with --compile, asset paths in HTML should be
// absolute (starting with `/`) not relative (starting with `./`). Relative paths
// cause 404 errors when navigating to routes other than `/`.
test.skipIf(isDebug)("fullstack compile uses absolute asset paths in generated HTML", async () => {
const dir = tempDirWithFiles("24180", {
"index.html": `
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<script type="module" src="./client.ts"></script>
<div id="app">Hello</div>
</body>
</html>
`,
"styles.css": `
body {
background: red;
}
`,
"client.ts": `
console.log("loaded");
`,
"server.ts": `
import html from "./index.html";
export default {
port: 0,
static: {
"/": html,
"/about": html,
},
fetch(req) {
return new Response("Not found", { status: 404 });
},
};
`,
});
const outfile = path.join(dir, "myapp");
// Build the fullstack app with --compile
const buildProc = Bun.spawn({
cmd: [bunExe(), "build", "--compile", "./server.ts", "--outfile", outfile],
cwd: dir,
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [buildStdout, buildStderr, buildExitCode] = await Promise.all([
new Response(buildProc.stdout).text(),
new Response(buildProc.stderr).text(),
buildProc.exited,
]);
expect(buildStderr).not.toContain("error");
expect(buildExitCode).toBe(0);
// On Windows, the compiler may append ".exe" to the output file
const exePath = fs.existsSync(outfile) ? outfile : outfile + ".exe";
expect(fs.existsSync(exePath)).toBe(true);
// Run the compiled executable with await using for automatic cleanup
await using serverProc = Bun.spawn({
cmd: [exePath],
cwd: dir,
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
// Read stderr to get the dynamically assigned port
// The server outputs something like "Started development server: http://localhost:PORT"
const reader = serverProc.stderr.getReader();
const decoder = new TextDecoder();
let stderrOutput = "";
let port: number | null = null;
const deadline = Date.now() + 10_000;
while (Date.now() < deadline) {
const { done, value } = await reader.read();
if (done) break;
stderrOutput += decoder.decode(value, { stream: true });
// Match various host formats: localhost:PORT, 127.0.0.1:PORT, [::1]:PORT, http://host:PORT
const match = stderrOutput.match(/(?:https?:\/\/)?(?:\[[^\]]+\]|[\w.-]+):(\d+)/);
if (match) {
port = parseInt(match[1], 10);
break;
}
}
reader.releaseLock();
if (port === null) {
throw new Error(`Failed to detect server port from stderr output:\n${stderrOutput}`);
}
// Fetch the HTML from the root route
const rootUrl = `http://localhost:${port}/`;
const res = await fetch(rootUrl);
expect(res.status).toBe(200);
const html = await res.text();
// Check that the CSS and JS paths are absolute, not relative
// They should start with `/` not `./`
const cssMatch = html.match(/href="([^"]+\.css)"/);
const jsMatch = html.match(/src="([^"]+\.js)"/);
expect(cssMatch).not.toBeNull();
expect(jsMatch).not.toBeNull();
const cssPath = cssMatch![1];
const jsPath = jsMatch![1];
// Verify paths are absolute (start with /)
expect(cssPath.startsWith("/")).toBe(true);
expect(jsPath.startsWith("/")).toBe(true);
// Verify paths don't use relative notation
expect(cssPath.startsWith("./")).toBe(false);
expect(jsPath.startsWith("./")).toBe(false);
// Also verify the assets are actually accessible
const cssRes = await fetch(`http://localhost:${port}${cssPath}`);
expect(cssRes.status).toBe(200);
const cssContent = await cssRes.text();
expect(cssContent).toContain("background");
const jsRes = await fetch(`http://localhost:${port}${jsPath}`);
expect(jsRes.status).toBe(200);
const jsContent = await jsRes.text();
expect(jsContent).toContain("loaded");
});