Update publishConfig.directory to match pnpm's behavior

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 <noreply@anthropic.com>
This commit is contained in:
Claude Bot
2025-10-14 05:44:32 +00:00
parent 8d3c406157
commit 5e6a40c46f
2 changed files with 86 additions and 53 deletions

View File

@@ -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| {

View File

@@ -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');"),
]);