Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
a3303ab5dd fix(install): remove symlinks from workspace package's node_modules on bun remove
When running `bun remove` from a workspace package, the package symlink was
not being removed from the workspace package's node_modules directory.
This was because the cleanup code used `std.fs.cwd()` which points to the
workspace root after PackageManager.init(), but with the isolated linker,
symlinks are in the individual workspace package's node_modules directory.

The fix uses `original_cwd` (which is already passed as a parameter) to
delete from the correct directory.

Fixes #26305

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 20:02:27 +00:00
2 changed files with 106 additions and 4 deletions

View File

@@ -433,8 +433,12 @@ fn updatePackageJSONAndInstallWithManagerWithUpdates(
return;
}
var cwd = std.fs.cwd();
// This is not exactly correct
// Use original_cwd to delete from the correct directory - this is important for
// workspace packages where CWD is changed to the workspace root but we want to
// delete from the original package's node_modules directory
var pkg_cwd = bun.openDirAbsolute(original_cwd) catch std.fs.cwd();
defer if (pkg_cwd.fd != std.fs.cwd().fd) pkg_cwd.close();
var node_modules_buf: bun.PathBuffer = undefined;
bun.copy(u8, &node_modules_buf, "node_modules" ++ std.fs.path.sep_str);
const offset_buf = node_modules_buf["node_modules/".len..];
@@ -446,13 +450,13 @@ fn updatePackageJSONAndInstallWithManagerWithUpdates(
// This is a quick & dirty cleanup intended for when deleting top-level dependencies
if (std.mem.indexOfScalar(PackageNameHash, name_hashes, String.Builder.stringHash(request.name)) == null) {
bun.copy(u8, offset_buf, request.name);
cwd.deleteTree(node_modules_buf[0 .. "node_modules/".len + request.name.len]) catch {};
pkg_cwd.deleteTree(node_modules_buf[0 .. "node_modules/".len + request.name.len]) catch {};
}
}
// This is where we clean dangling symlinks
// This could be slow if there are a lot of symlinks
if (bun.openDir(cwd, manager.options.bin_path)) |node_modules_bin_handle| {
if (bun.openDir(pkg_cwd, manager.options.bin_path)) |node_modules_bin_handle| {
var node_modules_bin: std.fs.Dir = node_modules_bin_handle;
defer node_modules_bin.close();
var iter: std.fs.Dir.Iterator = node_modules_bin.iterate();

View File

@@ -0,0 +1,98 @@
import { expect, test } from "bun:test";
import { existsSync } from "fs";
import { bunEnv, bunExe, runBunInstall, tempDirWithFiles } from "harness";
import { join } from "path";
// GitHub Issue #26305: bun remove doesn't remove package symlink from node_modules
// in workspace packages when using the isolated linker.
//
// The bug was that the cleanup code used std.fs.cwd() which points to the workspace
// root after init, but the symlinks are in the workspace package's node_modules directory.
test("bun remove removes symlink from workspace package's node_modules with isolated linker", async () => {
const testDir = tempDirWithFiles("issue-26305", {
"package.json": JSON.stringify({
name: "test-workspace",
workspaces: ["packages/*"],
}),
"bunfig.toml": `[install]\nlinker = "isolated"`,
"packages/pkg-a/package.json": JSON.stringify({
name: "pkg-a",
dependencies: {
"is-number": "7.0.0",
},
}),
"packages/pkg-b/package.json": JSON.stringify({
name: "pkg-b",
}),
});
// Install packages
await runBunInstall(bunEnv, testDir);
const pkgADir = join(testDir, "packages", "pkg-a");
const pkgANodeModules = join(pkgADir, "node_modules");
const isNumberSymlink = join(pkgANodeModules, "is-number");
// Verify is-number symlink exists in pkg-a's node_modules
expect(existsSync(isNumberSymlink)).toBe(true);
// Remove is-number from pkg-a
const { exited, stderr } = Bun.spawn({
cmd: [bunExe(), "remove", "is-number"],
cwd: pkgADir,
stdout: "pipe",
stderr: "pipe",
env: bunEnv,
});
const exitCode = await exited;
const stderrText = await stderr.text();
expect(stderrText).not.toContain("error:");
expect(exitCode).toBe(0);
// Verify is-number was removed from package.json
const pkgAPackageJson = await Bun.file(join(pkgADir, "package.json")).json();
expect(pkgAPackageJson.dependencies).toBeUndefined();
// BUG FIX: The symlink should be removed from pkg-a's node_modules
// Before the fix, this would fail because the symlink was not deleted
expect(existsSync(isNumberSymlink)).toBe(false);
});
test("bun remove removes symlink from root package's node_modules with isolated linker", async () => {
const testDir = tempDirWithFiles("issue-26305-root", {
"package.json": JSON.stringify({
name: "test-pkg",
dependencies: {
"is-number": "7.0.0",
},
}),
"bunfig.toml": `[install]\nlinker = "isolated"`,
});
// Install packages
await runBunInstall(bunEnv, testDir);
const isNumberSymlink = join(testDir, "node_modules", "is-number");
// Verify is-number symlink exists
expect(existsSync(isNumberSymlink)).toBe(true);
// Remove is-number
const { exited, stderr } = Bun.spawn({
cmd: [bunExe(), "remove", "is-number"],
cwd: testDir,
stdout: "pipe",
stderr: "pipe",
env: bunEnv,
});
const exitCode = await exited;
const stderrText = await stderr.text();
expect(stderrText).not.toContain("error:");
expect(exitCode).toBe(0);
// Verify symlink is removed
expect(existsSync(isNumberSymlink)).toBe(false);
});