Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
a319321ba1 fix(runtime): preserve symlink path in process.argv[1]
When running a script through a symlink, `process.argv[1]` now contains
the symlink path instead of the resolved real path, matching Node.js
behavior.

Previously, `bun bar.mjs` (where bar.mjs -> foo.mjs) would set
`process.argv[1]` to `/path/to/foo.mjs`. Now it correctly sets it to
`/path/to/bar.mjs`.

The fix handles two code paths in run_command.zig:
1. maybeOpenWithBunJS: Use the original file_path instead of calling
   getFdPath() which resolves symlinks
2. exec via resolver: Use path.pretty (original symlink path) when
   path.is_symlink is true

Fixes #2900

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

View File

@@ -1270,6 +1270,10 @@ pub const RunCommand = struct {
if (comptime Environment.isWindows) {
resolved = resolve_path.normalizeString(resolved, false, .windows);
}
// Set file_path to the normalized input path (issue #2900). This preserves
// the symlink path for process.argv[1], unlike getFdPath() which would
// resolve symlinks to their targets.
file_path = resolved;
break :brk bun.openFile(
resolved,
.{ .mode = .read_only },
@@ -1311,11 +1315,25 @@ pub const RunCommand = struct {
Global.configureAllocator(.{ .long_running = true });
// Use the original path (symlink path) for process.argv[1] (issue #2900)
// For relative paths, we need to make them absolute
absolute_script_path = brk: {
if (comptime !Environment.isWindows) break :brk bun.getFdPath(file, &script_name_buf) catch return false;
var fd_path_buf: bun.PathBuffer = undefined;
break :brk bun.getFdPath(file, &fd_path_buf) catch return false;
if (std.fs.path.isAbsolute(file_path)) {
break :brk file_path;
}
// Make the relative path absolute
var path_buf_2: bun.PathBuffer = undefined;
const cwd = bun.getcwd(&path_buf_2) catch return false;
path_buf_2[cwd.len] = std.fs.path.sep;
var parts = [_]string{file_path};
const abs_path = resolve_path.joinAbsStringBuf(
path_buf_2[0 .. cwd.len + 1],
&script_name_buf,
&parts,
.auto,
);
if (abs_path.len == 0) return false;
break :brk abs_path;
};
}
@@ -1520,7 +1538,10 @@ pub const RunCommand = struct {
const loader: bun.options.Loader = this_transpiler.options.loaders.get(path.name.ext) orelse .tsx;
if (loader.canBeRunByBun() or loader == .html) {
log("Resolved to: `{s}`", .{path.text});
return _bootAndHandleError(ctx, path.text, loader);
// Use the original symlink path for process.argv[1] (issue #2900)
// When path.is_symlink is true, path.pretty contains the original symlink path
const entry_path = if (path.is_symlink) path.pretty else path.text;
return _bootAndHandleError(ctx, entry_path, loader);
} else {
log("Resolved file `{s}` but ignoring because loader is {s}", .{ path.text, @tagName(loader) });
}

View File

@@ -0,0 +1,97 @@
import { expect, test } from "bun:test";
import { symlinkSync } from "fs";
import { bunEnv, bunExe, tempDir } from "harness";
import { join } from "path";
// https://github.com/oven-sh/bun/issues/2900
// process.argv[1] should preserve the symlink path, not resolve to the real path
test("process.argv[1] preserves symlink path", async () => {
using dir = tempDir("issue-2900", {
"foo.mjs": "console.log(process.argv[1]);",
});
// Create symlink bar.mjs -> foo.mjs
const fooPath = join(String(dir), "foo.mjs");
const barPath = join(String(dir), "bar.mjs");
try {
symlinkSync(fooPath, barPath);
} catch (e: any) {
if (process.platform === "win32") {
console.log("symlinkSync failed on Windows, skipping test:", e.message);
return;
}
throw e;
}
// Run through symlink
await using proc = Bun.spawn({
cmd: [bunExe(), barPath],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
// process.argv[1] should be the symlink path, not the resolved path
expect(stdout.trim()).toBe(barPath);
expect(exitCode).toBe(0);
});
test("process.argv[1] preserves relative symlink path (resolved to absolute)", async () => {
using dir = tempDir("issue-2900-rel", {
"foo.mjs": "console.log(process.argv[1]);",
});
// Create relative symlink bar.mjs -> foo.mjs
const barPath = join(String(dir), "bar.mjs");
try {
symlinkSync("foo.mjs", barPath);
} catch (e: any) {
if (process.platform === "win32") {
console.log("symlinkSync failed on Windows, skipping test:", e.message);
return;
}
throw e;
}
// Run through symlink
await using proc = Bun.spawn({
cmd: [bunExe(), "bar.mjs"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
// process.argv[1] should be the absolute symlink path
expect(stdout.trim()).toBe(barPath);
expect(exitCode).toBe(0);
});
test("process.argv[1] works correctly for non-symlink files", async () => {
using dir = tempDir("issue-2900-nosym", {
"foo.mjs": "console.log(process.argv[1]);",
});
const fooPath = join(String(dir), "foo.mjs");
await using proc = Bun.spawn({
cmd: [bunExe(), fooPath],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(stdout.trim()).toBe(fooPath);
expect(exitCode).toBe(0);
});

View File

@@ -29,8 +29,9 @@ test("absolute path to a file that is symlinked has import.meta.main", () => {
});
expect(result.stdout.trim()).toBe(
[
//
import.meta.path,
// process.argv[1] should be the symlink path (issue #2900)
root + "/main.js",
// Bun.main should be the resolved path
import.meta.path,
"true",
import.meta.dir,