Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
64836a9a85 fix(install): respect package selection in bun update --interactive
Previously, `bun update --interactive` would update ALL outdated packages
instead of only the packages selected by the user. This happened because:

1. The interactive command set `manager.to_update = true` but did not
   populate `manager.updating_packages` with the selected package names.

2. In PackageManagerEnqueue.zig, the condition for determining if a
   package should be updated was:
   `(update_requests.len == 0 or updating_packages.contains(...))`

   Since `update_requests.len` was always 0 in interactive mode, this
   condition was always true for all packages.

The fix:
1. Populate `manager.updating_packages` with the selected packages
   before calling `installWithManager()`.

2. Change the condition to:
   `((update_requests.len == 0 and updating_packages.count() == 0) or
     updating_packages.contains(...))`

   This ensures that when `updating_packages` is populated (as in
   interactive mode), only those packages are updated.

Closes #26758

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 18:26:06 +00:00
3 changed files with 238 additions and 2 deletions

View File

@@ -525,6 +525,40 @@ pub const UpdateInteractiveCommand = struct {
try updatePackageJsonFilesFromUpdates(manager, package_updates.items);
}
// Populate updating_packages with the selected packages so that
// PackageManagerEnqueue.zig only updates these packages.
// This is critical - without this, all packages would be updated.
for (package_updates.items) |update| {
const entry = bun.handleOom(manager.updating_packages.getOrPut(manager.allocator, update.name));
if (!entry.found_existing) {
entry.value_ptr.* = .{
.original_version_literal = update.original_version,
.is_alias = false,
.original_version = null,
};
}
}
// Also populate for catalog updates
if (has_catalog_updates) {
var catalog_it = catalog_updates.iterator();
while (catalog_it.next()) |entry| {
const catalog_key = entry.key_ptr.*;
// Parse catalog_key (format: "package_name" or "package_name:catalog_name")
const colon_index = std.mem.indexOf(u8, catalog_key, ":");
const package_name = if (colon_index) |idx| catalog_key[0..idx] else catalog_key;
const pkg_entry = bun.handleOom(manager.updating_packages.getOrPut(manager.allocator, package_name));
if (!pkg_entry.found_existing) {
pkg_entry.value_ptr.* = .{
.original_version_literal = "",
.is_alias = false,
.original_version = null,
};
}
}
}
manager.to_update = true;
// Reset the timer to show actual install time instead of total command time

View File

@@ -1375,8 +1375,9 @@ fn getOrPutResolvedPackageWithFindResult(
const should_update = this.to_update and
// If updating, only update packages in the current workspace
this.lockfile.isRootDependency(this, dependency_id) and
// no need to do a look up if update requests are empty (`bun update` with no args)
(this.update_requests.len == 0 or
// Update all packages if both update_requests and updating_packages are empty (`bun update` with no args).
// Otherwise, only update packages explicitly listed in updating_packages.
((this.update_requests.len == 0 and this.updating_packages.count() == 0) or
this.updating_packages.contains(dependency.name.slice(this.lockfile.buffers.string_bytes.items)));
// Was this package already allocated? Let's reuse the existing one.

View File

@@ -0,0 +1,201 @@
// Regression test for https://github.com/oven-sh/bun/issues/26758
// bun update --interactive does not respect user selections - updates ALL packages
// instead of only the selected ones.
import { describe, expect, test } from "bun:test";
import { readFileSync } from "fs";
import { bunEnv, bunExe, tempDir } from "harness";
import { join } from "path";
describe("issue #26758 - bun update --interactive respects selections", () => {
test("should only update selected package to latest, not all packages", async () => {
// Create a project with multiple outdated dependencies using exact versions.
// The bug manifests when using --latest or 'l' to toggle latest in interactive mode.
using dir = tempDir("update-interactive-26758", {
"package.json": JSON.stringify({
name: "test-project",
version: "1.0.0",
dependencies: {
// Use exact versions - these have newer versions available
lodash: "4.17.0", // 4.17.0 -> 4.17.23
debug: "4.0.0", // 4.0.0 -> 4.4.3
},
}),
});
// First, run bun install to create initial node_modules and lockfile
await using installProc = Bun.spawn({
cmd: [bunExe(), "install"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const installExitCode = await installProc.exited;
expect(installExitCode).toBe(0);
// Verify initial installation
const initialLodashPkg = JSON.parse(
readFileSync(join(String(dir), "node_modules", "lodash", "package.json"), "utf8"),
);
const initialDebugPkg = JSON.parse(
readFileSync(join(String(dir), "node_modules", "debug", "package.json"), "utf8"),
);
expect(initialLodashPkg.version).toBe("4.17.0");
expect(initialDebugPkg.version).toBe("4.0.0");
// Now run update --interactive
// Select only lodash using 'l' (which toggles latest AND selects)
// debug comes before lodash alphabetically
await using updateProc = Bun.spawn({
cmd: [bunExe(), "update", "--interactive"],
cwd: String(dir),
env: bunEnv,
stdin: "pipe",
stdout: "pipe",
stderr: "pipe",
});
try {
// Move down to lodash (second item) and use 'l' to toggle latest + select
updateProc.stdin.write("j"); // Move down to lodash
updateProc.stdin.write("l"); // Toggle latest (also selects)
updateProc.stdin.write("\r"); // Enter to confirm
updateProc.stdin.end();
const [stdout, stderr, exitCode] = await Promise.all([
updateProc.stdout.text(),
updateProc.stderr.text(),
updateProc.exited,
]);
// Debug output if test fails
if (exitCode !== 0) {
console.log("STDOUT:", stdout);
console.log("STDERR:", stderr);
}
expect(exitCode).toBe(0);
// Check the installed versions
const updatedLodashPkg = JSON.parse(
readFileSync(join(String(dir), "node_modules", "lodash", "package.json"), "utf8"),
);
const updatedDebugPkg = JSON.parse(
readFileSync(join(String(dir), "node_modules", "debug", "package.json"), "utf8"),
);
// The SELECTED package (lodash) should be updated to latest
expect(updatedLodashPkg.version).not.toBe("4.17.0");
expect(Bun.semver.satisfies(updatedLodashPkg.version, ">4.17.0")).toBe(true);
// The UNSELECTED package (debug) should NOT be updated - THIS IS THE BUG FIX
// Before the fix, debug would also be updated even though it wasn't selected
expect(updatedDebugPkg.version).toBe("4.0.0");
// Verify package.json was only updated for the selected package
const updatedPackageJson = JSON.parse(readFileSync(join(String(dir), "package.json"), "utf8"));
expect(updatedPackageJson.dependencies["debug"]).toBe("4.0.0");
expect(updatedPackageJson.dependencies["lodash"]).not.toBe("4.17.0");
} catch (err) {
updateProc.stdin.end();
updateProc.kill();
throw err;
}
});
test("should update multiple selected packages but not unselected ones", async () => {
// Create a project with three outdated dependencies using exact versions
using dir = tempDir("update-interactive-26758-multi", {
"package.json": JSON.stringify({
name: "test-project",
version: "1.0.0",
dependencies: {
// Using exact versions
chalk: "4.0.0", // 4.0.0 -> 5.x
debug: "4.0.0", // 4.0.0 -> 4.4.3
lodash: "4.17.0", // 4.17.0 -> 4.17.23
},
}),
});
// First install
await using installProc = Bun.spawn({
cmd: [bunExe(), "install"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const installExitCode = await installProc.exited;
expect(installExitCode).toBe(0);
// Verify initial versions
expect(JSON.parse(readFileSync(join(String(dir), "node_modules", "chalk", "package.json"), "utf8")).version).toBe(
"4.0.0",
);
expect(JSON.parse(readFileSync(join(String(dir), "node_modules", "debug", "package.json"), "utf8")).version).toBe(
"4.0.0",
);
expect(JSON.parse(readFileSync(join(String(dir), "node_modules", "lodash", "package.json"), "utf8")).version).toBe(
"4.17.0",
);
// Select chalk (first) and lodash (third), skip debug (second)
// Packages sorted: chalk, debug, lodash
await using updateProc = Bun.spawn({
cmd: [bunExe(), "update", "--interactive"],
cwd: String(dir),
env: bunEnv,
stdin: "pipe",
stdout: "pipe",
stderr: "pipe",
});
try {
updateProc.stdin.write("l"); // Toggle latest + select chalk (cursor at first item)
updateProc.stdin.write("j"); // Move down to debug
// Don't select debug
updateProc.stdin.write("j"); // Move down to lodash
updateProc.stdin.write("l"); // Toggle latest + select lodash
updateProc.stdin.write("\r"); // Confirm
updateProc.stdin.end();
const [stdout, stderr, exitCode] = await Promise.all([
updateProc.stdout.text(),
updateProc.stderr.text(),
updateProc.exited,
]);
if (exitCode !== 0) {
console.log("STDOUT:", stdout);
console.log("STDERR:", stderr);
}
expect(exitCode).toBe(0);
// Check updated versions
const updatedChalkPkg = JSON.parse(
readFileSync(join(String(dir), "node_modules", "chalk", "package.json"), "utf8"),
);
const updatedDebugPkg = JSON.parse(
readFileSync(join(String(dir), "node_modules", "debug", "package.json"), "utf8"),
);
const updatedLodashPkg = JSON.parse(
readFileSync(join(String(dir), "node_modules", "lodash", "package.json"), "utf8"),
);
// Selected packages (chalk and lodash) should be updated
expect(updatedChalkPkg.version).not.toBe("4.0.0");
expect(updatedLodashPkg.version).not.toBe("4.17.0");
// Unselected package (debug) should NOT be updated - THIS IS THE BUG FIX
expect(updatedDebugPkg.version).toBe("4.0.0");
} catch (err) {
updateProc.stdin.end();
updateProc.kill();
throw err;
}
});
});