From 5e6a40c46f47282bfacf46c08fa45c8d86e334c2 Mon Sep 17 00:00:00 2001 From: Claude Bot Date: Tue, 14 Oct 2025 05:44:32 +0000 Subject: [PATCH] Update publishConfig.directory to match pnpm's behavior MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After reviewing pnpm's implementation, updated to match their behavior: - Reads initial package.json from project root to get publishConfig.directory - Re-reads package.json from the subdirectory specified in publishConfig.directory - Uses the subdirectory's package.json for packing (name, version, files field, etc.) - The subdirectory MUST contain its own package.json This matches pnpm's implementation where build scripts typically: 1. Compile/transpile code into a dist/ directory 2. Copy package.json (possibly modified) to dist/ 3. Run bun publish to publish from dist/ Updated all tests to include package.json in the subdirectories. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/cli/pack_command.zig | 108 ++++++++++++++++-------------- test/cli/install/bun-pack.test.ts | 31 ++++++++- 2 files changed, 86 insertions(+), 53 deletions(-) diff --git a/src/cli/pack_command.zig b/src/cli/pack_command.zig index af476c4ec3..c516b40683 100644 --- a/src/cli/pack_command.zig +++ b/src/cli/pack_command.zig @@ -1174,7 +1174,47 @@ pub const PackCommand = struct { // maybe otp } - const package_name_expr: Expr = json.root.get("name") orelse return error.MissingPackageName; + // If publishConfig.directory is set, re-read package.json from that directory + const actual_package_json_path: stringZ = if (publish_config_directory) |directory| path_with_dir: { + var path_buf: PathBuffer = undefined; + const normalized_dir = strings.withoutTrailingSlash(strings.withoutPrefixComptime( + bun.path.normalizeBuf(directory, &path_buf, .posix), + "./", + )); + + const abs_workspace_path: string = strings.withoutTrailingSlash(strings.withoutSuffixComptime(abs_package_json_path, "package.json")); + const abs_dir = bun.path.joinAbsStringBuf( + abs_workspace_path, + &path_buf, + &[_]string{normalized_dir}, + .auto, + ); + + break :path_with_dir bun.path.joinAbsStringBufZ( + abs_dir, + &path_buf, + &[_]string{"package.json"}, + .auto, + ); + } else abs_package_json_path; + + // Re-read package.json from the actual directory + const actual_json = if (publish_config_directory != null) switch (manager.workspace_package_json_cache.getWithPath(manager.allocator, manager.log, actual_package_json_path, .{ + .guess_indentation = true, + })) { + .read_err => |err| { + Output.err(err, "failed to read package.json from publishConfig.directory: {s}", .{actual_package_json_path}); + Global.crash(); + }, + .parse_err => |err| { + Output.err(err, "failed to parse package.json from publishConfig.directory: {s}", .{actual_package_json_path}); + manager.log.print(Output.errorWriter()) catch {}; + Global.crash(); + }, + .entry => |entry| entry, + } else json; + + const package_name_expr: Expr = actual_json.root.get("name") orelse return error.MissingPackageName; const package_name = try package_name_expr.asStringCloned(ctx.allocator) orelse return error.InvalidPackageName; if (comptime for_publish) { const is_scoped = try Dependency.isScopedPackageName(package_name); @@ -1187,13 +1227,13 @@ pub const PackCommand = struct { defer if (comptime !for_publish) ctx.allocator.free(package_name); if (package_name.len == 0) return error.InvalidPackageName; - const package_version_expr: Expr = json.root.get("version") orelse return error.MissingPackageVersion; + const package_version_expr: Expr = actual_json.root.get("version") orelse return error.MissingPackageVersion; const package_version = try package_version_expr.asStringCloned(ctx.allocator) orelse return error.InvalidPackageVersion; defer if (comptime !for_publish) ctx.allocator.free(package_version); if (package_version.len == 0) return error.InvalidPackageVersion; if (comptime for_publish) { - if (json.root.get("private")) |private| { + if (actual_json.root.get("private")) |private| { if (private.asBool()) |is_private| { if (is_private) { return error.PrivatePackage; @@ -1202,7 +1242,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, actual_json); var this_transpiler: bun.transpiler.Transpiler = undefined; @@ -1327,63 +1367,27 @@ pub const PackCommand = struct { break :post_scripts .{ postpack_script, null, null }; }; - // Keep track of the workspace directory for package.json - var workspace_dir = workspace_dir: { + // Open the directory where files will be packed from (and where package.json is) + const pack_dir_path: string = strings.withoutTrailingSlash(strings.withoutSuffixComptime(actual_package_json_path, "package.json")); + var root_dir = root_dir: { var path_buf: PathBuffer = undefined; - @memcpy(path_buf[0..abs_workspace_path.len], abs_workspace_path); - path_buf[abs_workspace_path.len] = 0; - break :workspace_dir std.fs.openDirAbsoluteZ(path_buf[0..abs_workspace_path.len :0], .{ + @memcpy(path_buf[0..pack_dir_path.len], pack_dir_path); + path_buf[pack_dir_path.len] = 0; + break :root_dir std.fs.openDirAbsoluteZ(path_buf[0..pack_dir_path.len :0], .{ .iterate = true, }) catch |err| { - Output.err(err, "failed to open workspace directory: {s}\n", .{abs_workspace_path}); + Output.err(err, "failed to open pack directory: {s}\n", .{pack_dir_path}); Global.crash(); }; }; - defer workspace_dir.close(); - - var root_dir = root_dir: { - if (publish_config_directory) |directory| { - // Use publishConfig.directory if specified - var path_buf: PathBuffer = undefined; - const normalized_dir = strings.withoutTrailingSlash(strings.withoutPrefixComptime( - bun.path.normalizeBuf(directory, &path_buf, .posix), - "./", - )); - - const root_path = bun.path.joinAbsStringBufZ( - abs_workspace_path, - &path_buf, - &[_]string{normalized_dir}, - .auto, - ); - - break :root_dir std.fs.openDirAbsoluteZ(root_path, .{ - .iterate = true, - }) catch |err| { - Output.err(err, "failed to open root directory: {s}\n", .{root_path}); - Global.crash(); - }; - } else { - // Use the same directory as workspace_dir - var path_buf: PathBuffer = undefined; - @memcpy(path_buf[0..abs_workspace_path.len], abs_workspace_path); - path_buf[abs_workspace_path.len] = 0; - break :root_dir std.fs.openDirAbsoluteZ(path_buf[0..abs_workspace_path.len :0], .{ - .iterate = true, - }) catch |err| { - Output.err(err, "failed to open root directory: {s}\n", .{abs_workspace_path}); - Global.crash(); - }; - } - }; defer root_dir.close(); - ctx.bundled_deps = try getBundledDeps(ctx.allocator, json.root, "bundledDependencies") orelse - try getBundledDeps(ctx.allocator, json.root, "bundleDependencies") orelse + ctx.bundled_deps = try getBundledDeps(ctx.allocator, actual_json.root, "bundledDependencies") orelse + try getBundledDeps(ctx.allocator, actual_json.root, "bundleDependencies") orelse .{}; var pack_queue = pack_queue: { - if (json.root.get("files")) |files| { + if (actual_json.root.get("files")) |files| { files_error: { if (files.asArray()) |_files_array| { var includes: std.ArrayListUnmanaged(Pattern) = .{}; @@ -1518,7 +1522,7 @@ pub const PackCommand = struct { return; } - const bins = try getPackageBins(ctx.allocator, json.root); + const bins = try getPackageBins(ctx.allocator, actual_json.root); defer for (bins) |bin| ctx.allocator.free(bin.path); var print_buf = std.ArrayList(u8).init(ctx.allocator); @@ -1623,7 +1627,7 @@ pub const PackCommand = struct { } defer if (log_level.showProgress()) node.end(); - entry = try archivePackageJSON(ctx, archive, entry, workspace_dir, edited_package_json); + entry = try archivePackageJSON(ctx, archive, entry, root_dir, edited_package_json); if (log_level.showProgress()) node.completeOne(); while (pack_queue.removeOrNull()) |pathname| { diff --git a/test/cli/install/bun-pack.test.ts b/test/cli/install/bun-pack.test.ts index 10f4fbf2a7..f4d8e0526a 100644 --- a/test/cli/install/bun-pack.test.ts +++ b/test/cli/install/bun-pack.test.ts @@ -1376,6 +1376,14 @@ describe("publishConfig.directory", () => { }, }), ), + // package.json must also be in the dist directory (matches pnpm behavior) + write( + join(packageDir, "dist", "package.json"), + JSON.stringify({ + name: "pack-publishconfig-directory", + version: "1.0.0", + }), + ), write(join(packageDir, "src", "index.js"), "console.log('src');"), write(join(packageDir, "dist", "index.js"), "console.log('dist');"), write(join(packageDir, "dist", "other.js"), "console.log('other');"), @@ -1407,6 +1415,13 @@ describe("publishConfig.directory", () => { }), ), mkdir(join(packageDir, "build"), { recursive: true }), + write( + join(packageDir, "build", "package.json"), + JSON.stringify({ + name: "pack-publishconfig-dir-prefix", + version: "2.0.0", + }), + ), write(join(packageDir, "build", "main.js"), "console.log('main');"), ]); @@ -1429,6 +1444,13 @@ describe("publishConfig.directory", () => { }), ), mkdir(join(packageDir, "output", "final"), { recursive: true }), + write( + join(packageDir, "output", "final", "package.json"), + JSON.stringify({ + name: "pack-publishconfig-nested", + version: "3.0.0", + }), + ), write(join(packageDir, "output", "final", "result.js"), "console.log('result');"), write(join(packageDir, "output", "temp.js"), "console.log('temp');"), ]); @@ -1464,7 +1486,6 @@ describe("publishConfig.directory", () => { JSON.stringify({ name: "pack-publishconfig-with-files", version: "1.0.0", - files: ["lib/**/*"], publishConfig: { directory: "dist", }, @@ -1472,6 +1493,14 @@ describe("publishConfig.directory", () => { ), mkdir(join(packageDir, "dist", "lib"), { recursive: true }), mkdir(join(packageDir, "dist", "other"), { recursive: true }), + write( + join(packageDir, "dist", "package.json"), + JSON.stringify({ + name: "pack-publishconfig-with-files", + version: "1.0.0", + files: ["lib/**/*"], + }), + ), write(join(packageDir, "dist", "lib", "main.js"), "console.log('lib');"), write(join(packageDir, "dist", "other", "other.js"), "console.log('other');"), ]);