Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
2b581ec990 fix(install): make bun update <package> update package.json with preserved version ranges
Previously, `bun update <package>` would only update the lockfile but leave the version specification in package.json unchanged. For example, if package.json had `"react": "^18.0.0"` and the lockfile resolved to `18.0.0`, running `bun update react` would update the lockfile to the latest 18.x version but package.json would still show `"^18.0.0"`.

This fix ensures that:
- `bun update <package>` updates both lockfile AND package.json
- Version range prefixes are preserved (^ stays ^, ~ stays ~, exact stays exact)
- The updated version in package.json reflects the actual resolved version from the lockfile

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-24 19:43:40 +00:00
2 changed files with 130 additions and 26 deletions

View File

@@ -411,35 +411,37 @@ pub fn edit(
if (query.expr.asProperty(name)) |value| {
if (value.expr.data == .e_string) {
// For update command, always populate updating_packages during before_install phase
if (manager.subcommand == .update and options.before_install) add_packages_to_update: {
const version_literal = try value.expr.asStringCloned(allocator) orelse break :add_packages_to_update;
var tag = Dependency.Version.Tag.infer(version_literal);
if (tag != .npm and tag != .dist_tag) break :add_packages_to_update;
const entry = manager.updating_packages.getOrPut(allocator, name) catch bun.outOfMemory();
// first come, first serve
if (entry.found_existing) break :add_packages_to_update;
var is_alias = false;
if (strings.hasPrefixComptime(strings.trim(version_literal, &strings.whitespace_chars), "npm:")) {
if (strings.lastIndexOfChar(version_literal, '@')) |at_index| {
tag = Dependency.Version.Tag.infer(version_literal[at_index + 1 ..]);
if (tag != .npm and tag != .dist_tag) break :add_packages_to_update;
is_alias = true;
}
}
entry.value_ptr.* = .{
.original_version_literal = version_literal,
.is_alias = is_alias,
.original_version = null,
};
}
if (request.package_id != invalid_package_id and strings.eqlLong(list, dependency_list, true)) {
replacing += 1;
} else {
if (manager.subcommand == .update and options.before_install) add_packages_to_update: {
const version_literal = try value.expr.asStringCloned(allocator) orelse break :add_packages_to_update;
var tag = Dependency.Version.Tag.infer(version_literal);
if (tag != .npm and tag != .dist_tag) break :add_packages_to_update;
const entry = manager.updating_packages.getOrPut(allocator, name) catch bun.outOfMemory();
// first come, first serve
if (entry.found_existing) break :add_packages_to_update;
var is_alias = false;
if (strings.hasPrefixComptime(strings.trim(version_literal, &strings.whitespace_chars), "npm:")) {
if (strings.lastIndexOfChar(version_literal, '@')) |at_index| {
tag = Dependency.Version.Tag.infer(version_literal[at_index + 1 ..]);
if (tag != .npm and tag != .dist_tag) break :add_packages_to_update;
is_alias = true;
}
}
entry.value_ptr.* = .{
.original_version_literal = version_literal,
.is_alias = is_alias,
.original_version = null,
};
}
if (!only_add_missing) {
request.e_string = value.expr.data.e_string;
remaining -= 1;

View File

@@ -0,0 +1,102 @@
import { test, expect } from "bun:test";
import { bunEnv, bunExe, tempDirWithFiles } from "harness";
import { join } from "node:path";
test("bun update should update package.json with preserved version range prefixes", async () => {
const dir = tempDirWithFiles("bun-update-test", {
"package.json": JSON.stringify({
name: "test-project",
version: "1.0.0",
dependencies: {
// Use different version range formats
"react": "^18.0.0",
"lodash": "~4.17.20",
"express": "4.17.1", // exact version
},
}, null, 2),
});
// Install dependencies first
await using proc1 = Bun.spawn({
cmd: [bunExe(), "install"],
env: bunEnv,
cwd: dir,
});
await proc1.exited;
// Update react (caret range)
await using proc2 = Bun.spawn({
cmd: [bunExe(), "update", "react"],
env: bunEnv,
cwd: dir,
});
await proc2.exited;
// Update lodash (tilde range)
await using proc3 = Bun.spawn({
cmd: [bunExe(), "update", "lodash"],
env: bunEnv,
cwd: dir,
});
await proc3.exited;
// Update express (exact version)
await using proc4 = Bun.spawn({
cmd: [bunExe(), "update", "express"],
env: bunEnv,
cwd: dir,
});
await proc4.exited;
// Read the updated package.json
const packageJsonPath = join(dir, "package.json");
const packageJsonContent = await Bun.file(packageJsonPath).text();
const packageJson = JSON.parse(packageJsonContent);
// Verify that version range prefixes are preserved
expect(packageJson.dependencies.react).toMatch(/^\^18\./);
expect(packageJson.dependencies.lodash).toMatch(/^~4\.17\./);
expect(packageJson.dependencies.express).toMatch(/^4\.17\./); // should remain exact
// Verify versions were actually updated (not just the exact same versions)
expect(packageJson.dependencies.react).not.toBe("^18.0.0");
expect(packageJson.dependencies.lodash).not.toBe("~4.17.20");
// express might stay the same if no patch updates are available in the 4.17.x range
});
test("bun update with --latest should update to latest versions", async () => {
const dir = tempDirWithFiles("bun-update-latest-test", {
"package.json": JSON.stringify({
name: "test-project",
version: "1.0.0",
dependencies: {
"react": "^17.0.0", // old major version
},
}, null, 2),
});
// Install dependencies first
await using proc1 = Bun.spawn({
cmd: [bunExe(), "install"],
env: bunEnv,
cwd: dir,
});
await proc1.exited;
// Update react with --latest flag
await using proc2 = Bun.spawn({
cmd: [bunExe(), "update", "--latest", "react"],
env: bunEnv,
cwd: dir,
});
await proc2.exited;
// Read the updated package.json
const packageJsonPath = join(dir, "package.json");
const packageJsonContent = await Bun.file(packageJsonPath).text();
const packageJson = JSON.parse(packageJsonContent);
// With --latest, should update to newest major version with caret prefix
expect(packageJson.dependencies.react).toMatch(/^\^1[89]\./); // React 18 or 19
expect(packageJson.dependencies.react).not.toBe("^17.0.0");
});