Fix bun pm pack to use workspace package.json version instead of lockfile version

Fixes #20477

When packing a workspace package that depends on another workspace package,
`bun pm pack` now reads the version from the workspace dependency's package.json
instead of using the cached version in bun.lock. This ensures that if a workspace
package version is updated without running `bun install`, the pack command will
use the current version from package.json.

Changes:
- Modified editRootPackageJSON to read workspace package.json files directly
- Added detailed error messages for read/parse errors and missing version fields
- Added test to verify correct behavior when workspace versions are updated

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Claude Bot
2025-11-08 04:53:50 +00:00
parent 6f8138b6e4
commit eb09e22152
2 changed files with 99 additions and 5 deletions

View File

@@ -1235,7 +1235,7 @@ pub const PackCommand = struct {
}
}
const edited_package_json = try editRootPackageJSON(ctx.allocator, ctx.lockfile, json);
const edited_package_json = try editRootPackageJSON(ctx.allocator, ctx.lockfile, json, manager, abs_package_json_path);
var this_transpiler: bun.transpiler.Transpiler = undefined;
@@ -2073,6 +2073,8 @@ pub const PackCommand = struct {
allocator: std.mem.Allocator,
maybe_lockfile: ?*Lockfile,
json: *PackageManager.WorkspacePackageJSONCache.MapEntry,
manager: *PackageManager,
abs_package_json_path: stringZ,
) OOM!string {
for ([_]string{
"dependencies",
@@ -2116,23 +2118,76 @@ pub const PackCommand = struct {
};
failed_to_resolve: {
// find the current workspace version and append to package spec without `workspace:`
// find the current workspace version from its package.json and append to package spec without `workspace:`
const lockfile = maybe_lockfile orelse break :failed_to_resolve;
const workspace_version = lockfile.workspace_versions.get(Semver.String.Builder.stringHash(dependency_name)) orelse break :failed_to_resolve;
const dependency_name_hash = Semver.String.Builder.stringHash(dependency_name);
const workspace_rel_path = lockfile.workspace_paths.get(dependency_name_hash) orelse break :failed_to_resolve;
// Get the project root directory (where lockfile lives)
const project_root = std.fs.path.dirname(abs_package_json_path) orelse break :failed_to_resolve;
// Build absolute path to workspace package.json
const workspace_package_json_path = try std.fs.path.join(allocator, &.{
project_root,
workspace_rel_path.slice(lockfile.buffers.string_bytes.items),
"package.json",
});
defer allocator.free(workspace_package_json_path);
// Read the workspace package.json
const workspace_json = switch (manager.workspace_package_json_cache.getWithPath(
allocator,
manager.log,
workspace_package_json_path,
.{ .guess_indentation = false },
)) {
.entry => |entry| entry,
.read_err => |err| {
Output.err(err, "Failed to read workspace package.json for \"{s}\" at \"{s}\"", .{
dependency_name,
workspace_package_json_path,
});
Global.crash();
},
.parse_err => |err| {
Output.err(err, "Failed to parse workspace package.json for \"{s}\" at \"{s}\"", .{
dependency_name,
workspace_package_json_path,
});
manager.log.print(Output.errorWriter()) catch {};
Global.crash();
},
};
// Extract version from workspace package.json
const version_expr = workspace_json.root.get("version") orelse {
Output.errGeneric("Workspace package \"{s}\" at \"{s}\" is missing a `version` field", .{
dependency_name,
workspace_package_json_path,
});
Global.crash();
};
const version_str = version_expr.asString(allocator) orelse {
Output.errGeneric("Workspace package \"{s}\" at \"{s}\" has an invalid `version` field", .{
dependency_name,
workspace_package_json_path,
});
Global.crash();
};
dependency.value = Expr.allocate(
allocator,
E.String,
.{
.data = try std.fmt.allocPrint(allocator, "{s}{}", .{
.data = try std.fmt.allocPrint(allocator, "{s}{s}", .{
switch (c) {
'^' => "^",
'~' => "~",
'*' => "",
else => unreachable,
},
workspace_version.fmt(lockfile.buffers.string_bytes.items),
version_str,
}),
},
.{},

View File

@@ -615,6 +615,45 @@ describe("workspaces", () => {
{ "pathname": "package/root.js" },
]);
});
test("uses package.json version not lockfile version when workspace version changes", async () => {
await Promise.all([
write(
join(packageDir, "package.json"),
JSON.stringify({
name: "pack-workspace-version-update",
version: "3.0.0",
workspaces: ["pkgs/*"],
dependencies: {
"pkg1": "workspace:*",
},
}),
),
write(join(packageDir, "root.js"), "console.log('hello ./root.js')"),
write(join(packageDir, "pkgs", "pkg1", "package.json"), JSON.stringify({ name: "pkg1", version: "1.0.0" })),
write(join(packageDir, "pkgs", "pkg1", "index.js"), "console.log('pkg1')"),
]);
// Install with version 1.0.0 to create lockfile
await runBunInstall(bunEnv, packageDir);
// Update workspace package version to 2.0.0 without running install
await write(join(packageDir, "pkgs", "pkg1", "package.json"), JSON.stringify({ name: "pkg1", version: "2.0.0" }));
// Pack should use 2.0.0 from package.json, not 1.0.0 from lockfile
await pack(packageDir, bunEnv);
const tarball = readTarball(join(packageDir, "pack-workspace-version-update-3.0.0.tgz"));
const packageJson = JSON.parse(tarball.entries[0].contents);
expect(packageJson).toEqual({
name: "pack-workspace-version-update",
version: "3.0.0",
workspaces: ["pkgs/*"],
dependencies: {
"pkg1": "2.0.0", // Should be 2.0.0 from package.json, not 1.0.0 from lockfile
},
});
});
});
test("lifecycle scripts execution order", async () => {