Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
b71dd33d79 fix(bundler): use absolute paths for chunks when publicPath is a URL
When `publicPath` is an absolute URL (http://, https://, or //), chunk
imports should use the chunk filename directly without calculating
relative paths. Previously, chunk imports would produce malformed URLs
like `http://localhost:3000/../chunk.js` when entry points were in
different directories.

Fixes #3322

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 01:08:11 +00:00
2 changed files with 123 additions and 2 deletions

View File

@@ -189,6 +189,12 @@ pub const Chunk = struct {
const unique_key_for_additional_files = graph.input_files.items(.unique_key_for_additional_file);
const relative_platform_buf = bun.path_buffer_pool.get();
defer bun.path_buffer_pool.put(relative_platform_buf);
// When publicPath is an absolute URL, use absolute paths for chunks
// to avoid generating malformed URLs like "http://localhost/../chunk.js"
const use_absolute_path = force_absolute_path or
strings.hasPrefixComptime(import_prefix, "https://") or
strings.hasPrefixComptime(import_prefix, "http://") or
strings.hasPrefixComptime(import_prefix, "//");
switch (this.*) {
.pieces => |*pieces| {
const entry_point_chunks_for_scb = linker_graph.files.items(.entry_point_chunk_index);
@@ -242,7 +248,7 @@ pub const Chunk = struct {
const cheap_normalizer = cheapPrefixNormalizer(
import_prefix,
if (from_chunk_dir.len == 0 or force_absolute_path)
if (from_chunk_dir.len == 0 or use_absolute_path)
file_path
else
bun.path.relativePlatformBuf(relative_platform_buf, from_chunk_dir, file_path, .posix, false),
@@ -334,7 +340,7 @@ pub const Chunk = struct {
bun.path.platformToPosixInPlace(u8, @constCast(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 use_absolute_path)
file_path
else
bun.path.relativePlatformBuf(relative_platform_buf, from_chunk_dir, file_path, .posix, false),

View File

@@ -0,0 +1,115 @@
// https://github.com/oven-sh/bun/issues/3322
// publicPath with absolute URL should not produce malformed chunk import URLs
// Bug: When entry points are in different directories, chunks would be imported with
// relative paths like "http://localhost:3000/../chunk.js" instead of "http://localhost:3000/chunk.js"
import { expect, test } from "bun:test";
import { tempDirWithFiles } from "harness";
import { join } from "path";
test("publicPath with absolute URL does not produce malformed chunk imports when entries are in different dirs", async () => {
// The bug occurs when entry points are in different directories (a/ and b/),
// causing output files to maintain that structure and chunk imports to use
// relative paths like "../" which get concatenated with the absolute URL
const dir = tempDirWithFiles("public-path-url-test", {
"a/entry1.js": `
import { shared } from '../shared.js';
console.log('entry1', shared);
`,
"b/entry2.js": `
import { shared } from '../shared.js';
console.log('entry2', shared);
`,
"shared.js": `
export const shared = 'shared value';
`,
});
const build = await Bun.build({
entrypoints: [join(dir, "a/entry1.js"), join(dir, "b/entry2.js")],
outdir: join(dir, "dist"),
splitting: true,
publicPath: "http://localhost:3000/",
});
expect(build.success).toBe(true);
expect(build.outputs.length).toBeGreaterThanOrEqual(2);
// Find and read the entry files
const entryOutputs = build.outputs.filter(o => o.path.includes("entry"));
expect(entryOutputs.length).toBe(2);
for (const output of entryOutputs) {
const content = await output.text();
// Should NOT contain relative path segments like "../" after the absolute URL
// This was the bug: "http://localhost:3000/../chunk-xxx.js"
expect(content).not.toMatch(/http:\/\/localhost:3000\/\.\.\//);
// Should contain properly formatted URL imports
if (content.includes("http://localhost:3000/chunk")) {
expect(content).toMatch(/http:\/\/localhost:3000\/chunk-[a-z0-9]+\.js/);
}
}
});
test("publicPath with https URL does not produce malformed chunk imports", async () => {
const dir = tempDirWithFiles("public-path-https-test", {
"a/entry1.js": `
import { shared } from '../shared.js';
console.log('entry1', shared);
`,
"b/entry2.js": `
import { shared } from '../shared.js';
console.log('entry2', shared);
`,
"shared.js": `
export const shared = 'shared value';
`,
});
const build = await Bun.build({
entrypoints: [join(dir, "a/entry1.js"), join(dir, "b/entry2.js")],
outdir: join(dir, "dist"),
splitting: true,
publicPath: "https://cdn.example.com/assets/",
});
expect(build.success).toBe(true);
const entryOutputs = build.outputs.filter(o => o.path.includes("entry"));
for (const output of entryOutputs) {
const content = await output.text();
// Should NOT contain relative path segments after the absolute URL
expect(content).not.toMatch(/https:\/\/cdn\.example\.com\/assets\/\.\.\//);
}
});
test("publicPath with protocol-relative URL does not produce malformed chunk imports", async () => {
const dir = tempDirWithFiles("public-path-protocol-relative-test", {
"a/entry1.js": `
import { shared } from '../shared.js';
console.log('entry1', shared);
`,
"b/entry2.js": `
import { shared } from '../shared.js';
console.log('entry2', shared);
`,
"shared.js": `
export const shared = 'shared value';
`,
});
const build = await Bun.build({
entrypoints: [join(dir, "a/entry1.js"), join(dir, "b/entry2.js")],
outdir: join(dir, "dist"),
splitting: true,
publicPath: "//cdn.example.com/",
});
expect(build.success).toBe(true);
const entryOutputs = build.outputs.filter(o => o.path.includes("entry"));
for (const output of entryOutputs) {
const content = await output.text();
// Should NOT contain relative path segments after the URL
expect(content).not.toMatch(/\/\/cdn\.example\.com\/\.\.\//);
}
});