Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/bundler/bundle_v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2589,6 +2589,17 @@ pub mod bv2_impl {
.loader(&self.transpiler.options.loaders)
.unwrap_or(Loader::File);

// When the resolver followed a symlink, `path.pretty` still holds the
// pre-symlink absolute path (`set_realpath` moved the old `text` there).
// Keep it for output naming so `outdir` mirrors the entry-point spelling
// the user wrote, matching esbuild. `path_with_pretty_initialized` below
// overwrites `pretty`.
if is_entry_point && path.is_symlink && path.is_file() && !path.pretty.is_empty() {
self.graph
.entry_point_original_names
.put(source_index.get(), path.pretty)?;
}

// SAFETY: `path_with_pretty_initialized` allocates into `self.graph.heap`, which
// outlives the bundle pass; erase the arena lifetime back to the resolver's
// `Path<'static>` alias so `path` doesn't keep `self` borrowed.
Expand Down
16 changes: 15 additions & 1 deletion src/bundler/linker_context/computeChunks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -640,8 +640,23 @@ pub fn compute_chunks(this: &mut LinkerContext, unique_key: u64) -> crate::Resul
} else {
b"."
};
let root_dir = &this.resolver().opts.root_dir;
let mut real_path_buf = PathBuffer::uninit();
let dir: &[u8] = 'dir: {
// When `dir_path` already sits under `root_dir`, skip the
// open+get_fd_path round-trip: that canonicalization would
// discard the symlink spelling an entry point was reached
// through (#8467). Fall through when the plain relativization
// walks above root so a short-name/symlinked cwd still finds a
// common prefix.
if bun_paths::is_absolute(dir_path)
&& !resolve_path::relative_platform::<bun_paths::platform::Auto, false>(
root_dir, dir_path,
)
.starts_with(b"..")
{
break 'dir dir_path;
}
let Ok(dir_file) = bun_sys::File::openat(
bun_sys::Fd::cwd(),
dir_path,
Expand Down Expand Up @@ -672,7 +687,6 @@ pub fn compute_chunks(this: &mut LinkerContext, unique_key: u64) -> crate::Resul
}
};

let root_dir = &this.resolver().opts.root_dir;
chunk.template.placeholder.dir = resolve_path::relative_alloc(root_dir, dir)?;
}
}
Expand Down
49 changes: 49 additions & 0 deletions test/bundler/bun-build-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,55 @@ describe("Bun.build", () => {
},
);

// https://github.com/oven-sh/bun/issues/8467
test.concurrent("output paths keep the symlink spelling of an entry point", async () => {
using dir = tempDir("build-symlink-entry-output", {
"packages/mypkg/index.ts": `export const a = 1;\n`,
"packages/mypkg/other.ts": `export const b = 2;\n`,
"app/src/local.ts": `export const c = 3;\n`,
"app/build.ts": `
const result = await Bun.build({
entrypoints: ["./node_modules/mypkg/index.ts", "./node_modules/mypkg/other.ts", "./src/local.ts"],
outdir: "./dist",
});
if (!result.success) {
for (const m of result.logs) console.error(String(m));
process.exit(1);
}
for (const out of result.outputs) {
console.log(out.kind + " " + out.path.slice(process.cwd().length + 1).replaceAll("\\\\", "/"));
}
`,
});
mkdirSync(join(String(dir), "app", "node_modules"), { recursive: true });
symlinkSync(
join(String(dir), "packages", "mypkg"),
join(String(dir), "app", "node_modules", "mypkg"),
isWindows ? "junction" : "dir",
);

await using proc = Bun.spawn({
cmd: [bunExe(), "build.ts"],
env: bunEnv,
cwd: join(String(dir), "app"),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

const lines = stdout.trim().split("\n").sort();
expect({ lines, stderr }).toEqual({
lines: [
"entry-point dist/node_modules/mypkg/index.js",
"entry-point dist/node_modules/mypkg/other.js",
"entry-point dist/src/local.js",
],
stderr: "",
});
expect(stdout).not.toContain("_.._");
expect(exitCode).toBe(0);
});

test.concurrent("errors are returned as an array", async () => {
const x = await buildNoThrow({
entrypoints: [join(import.meta.dir, "does-not-exist.ts")],
Expand Down
Loading