From d3323c84bbd58d9c5f3d01defbc7d060bb77663a Mon Sep 17 00:00:00 2001 From: Dylan Conway <35280289+dylan-conway@users.noreply.github.com> Date: Fri, 11 Oct 2024 21:28:47 -0700 Subject: [PATCH] fix(publish): missing bins bugfix (#14488) Co-authored-by: Jarred Sumner --- src/cli/pack_command.zig | 37 +- src/cli/publish_command.zig | 392 ++++++++++++++++-- src/install/bin.zig | 27 +- src/js_ast.zig | 50 +++ src/resolver/resolve_path.zig | 6 + src/string_immutable.zig | 7 + test/cli/install/bun-pack.test.ts | 1 - .../registry/bun-install-registry.test.ts | 125 ++++++ 8 files changed, 583 insertions(+), 62 deletions(-) diff --git a/src/cli/pack_command.zig b/src/cli/pack_command.zig index 4d4bc36b40..8ef1f9b0b9 100644 --- a/src/cli/pack_command.zig +++ b/src/cli/pack_command.zig @@ -1148,6 +1148,8 @@ pub const PackCommand = struct { } } + const edited_package_json = try editRootPackageJSON(ctx.allocator, ctx.lockfile, json); + var this_bundler: bun.bundler.Bundler = undefined; _ = RunCommand.configureEnvForRun( @@ -1401,6 +1403,7 @@ pub const PackCommand = struct { .publish_script = publish_script, .postpublish_script = postpublish_script, .script_env = this_bundler.env, + .normalized_pkg_info = "", }; } @@ -1500,7 +1503,7 @@ pub const PackCommand = struct { var entry = Archive.Entry.new2(archive); - const package_json = archive_with_progress: { + { var progress: if (log_level == .silent) void else Progress = if (comptime log_level == .silent) {} else .{}; var node = if (comptime log_level == .silent) {} else node: { progress.supports_ansi_escape_codes = Output.enable_ansi_colors; @@ -1510,7 +1513,7 @@ pub const PackCommand = struct { }; defer if (comptime log_level != .silent) node.end(); - entry, const edited_package_json = try editAndArchivePackageJSON(ctx, archive, entry, root_dir, json); + entry = try archivePackageJSON(ctx, archive, entry, root_dir, edited_package_json); if (comptime log_level != .silent) node.completeOne(); while (pack_queue.removeOrNull()) |pathname| { @@ -1575,9 +1578,7 @@ pub const PackCommand = struct { bins, ); } - - break :archive_with_progress edited_package_json; - }; + } entry.free(); @@ -1655,12 +1656,25 @@ pub const PackCommand = struct { ctx.stats.packed_size = size; }; + const normalized_pkg_info: if (for_publish) string else void = if (comptime for_publish) + try Publish.normalizedPackage( + ctx.allocator, + manager, + package_name, + package_version, + &json.root, + json.source, + shasum, + integrity, + abs_tarball_dest, + ); + printArchivedFilesAndPackages( ctx, root_dir, false, pack_list, - package_json.len, + edited_package_json.len, ); if (comptime !for_publish) { @@ -1715,6 +1729,7 @@ pub const PackCommand = struct { .publish_script = publish_script, .postpublish_script = postpublish_script, .script_env = this_bundler.env, + .normalized_pkg_info = normalized_pkg_info, }; } } @@ -1785,15 +1800,13 @@ pub const PackCommand = struct { } }; - fn editAndArchivePackageJSON( + fn archivePackageJSON( ctx: *Context, archive: *Archive, entry: *Archive.Entry, root_dir: std.fs.Dir, - json: *PackageManager.WorkspacePackageJSONCache.MapEntry, - ) OOM!struct { *Archive.Entry, string } { - const edited_package_json = try editRootPackageJSON(ctx.allocator, ctx.lockfile, json); - + edited_package_json: string, + ) OOM!*Archive.Entry { const stat = bun.sys.fstatat(bun.toFD(root_dir), "package.json").unwrap() catch |err| { Output.err(err, "failed to stat package.json", .{}); Global.crash(); @@ -1818,7 +1831,7 @@ pub const PackCommand = struct { ctx.stats.unpacked_size += @intCast(archive.writeData(edited_package_json)); - return .{ entry.clear(), edited_package_json }; + return entry.clear(); } fn addArchiveEntry( diff --git a/src/cli/publish_command.zig b/src/cli/publish_command.zig index c54903373a..03ec775d58 100644 --- a/src/cli/publish_command.zig +++ b/src/cli/publish_command.zig @@ -32,6 +32,9 @@ const Npm = install.Npm; const Run = bun.CLI.RunCommand; const DotEnv = bun.DotEnv; const Open = @import("../open.zig"); +const E = bun.JSAst.E; +const G = bun.JSAst.G; +const BabyList = bun.BabyList; pub const PublishCommand = struct { pub fn Context(comptime directory_publish: bool) type { @@ -48,6 +51,8 @@ pub const PublishCommand = struct { integrity: sha.SHA512.Digest, uses_workspaces: bool, + normalized_pkg_info: string, + publish_script: if (directory_publish) ?[]const u8 else void = if (directory_publish) null else {}, postpublish_script: if (directory_publish) ?[]const u8 else void = if (directory_publish) null else {}, script_env: if (directory_publish) *DotEnv.Loader else void, @@ -163,9 +168,7 @@ pub const PublishCommand = struct { const package_json_contents = maybe_package_json_contents orelse return error.MissingPackageJSON; - const package_name, const package_version = package_info: { - defer ctx.allocator.free(package_json_contents); - + const package_name, const package_version, var json, const json_source = package_info: { const source = logger.Source.initPathString("package.json", package_json_contents); const json = JSON.parsePackageJSONUTF8(&source, manager.log, ctx.allocator) catch |err| { return switch (err) { @@ -213,7 +216,7 @@ pub const PublishCommand = struct { const version = try json.getStringCloned(ctx.allocator, "version") orelse return error.MissingPackageVersion; if (version.len == 0) return error.InvalidPackageVersion; - break :package_info .{ name, version }; + break :package_info .{ name, version, json, source }; }; var shasum: sha.SHA1.Digest = undefined; @@ -230,6 +233,18 @@ pub const PublishCommand = struct { sha512.update(tarball_bytes); sha512.final(&integrity); + const normalized_pkg_info = try normalizedPackage( + ctx.allocator, + manager, + package_name, + package_version, + &json, + json_source, + shasum, + integrity, + abs_tarball_path, + ); + Pack.Context.printSummary( .{ .total_files = total_files, @@ -253,6 +268,7 @@ pub const PublishCommand = struct { .uses_workspaces = false, .command_ctx = ctx, .script_env = {}, + .normalized_pkg_info = normalized_pkg_info, }; } @@ -508,7 +524,7 @@ pub const PublishCommand = struct { // dry-run stops here if (ctx.manager.options.dry_run) return; - const publish_req_body = try constructPublishRequestBody(directory_publish, ctx, registry); + const publish_req_body = try constructPublishRequestBody(directory_publish, ctx); var print_buf: std.ArrayListUnmanaged(u8) = .{}; defer print_buf.deinit(ctx.allocator); @@ -859,6 +875,338 @@ pub const PublishCommand = struct { }; } + pub fn normalizedPackage( + allocator: std.mem.Allocator, + manager: *PackageManager, + package_name: string, + package_version: string, + json: *Expr, + json_source: logger.Source, + shasum: sha.SHA1.Digest, + integrity: sha.SHA512.Digest, + abs_tarball_path: stringZ, + ) OOM!string { + bun.assertWithLocation(json.isObject(), @src()); + + const registry = manager.scopeForPackageName(package_name); + + const version_without_build_tag = Dependency.withoutBuildTag(package_version); + + const integrity_fmt = try std.fmt.allocPrint(allocator, "{}", .{bun.fmt.integrity(integrity, .full)}); + + try json.setString(allocator, "_id", try std.fmt.allocPrint(allocator, "{s}@{s}", .{ package_name, version_without_build_tag })); + try json.setString(allocator, "_integrity", integrity_fmt); + try json.setString(allocator, "_nodeVersion", Environment.reported_nodejs_version); + // TODO: npm version + try json.setString(allocator, "_npmVersion", "10.8.3"); + try json.setString(allocator, "integrity", integrity_fmt); + try json.setString(allocator, "shasum", try std.fmt.allocPrint(allocator, "{s}", .{bun.fmt.bytesToHex(shasum, .lower)})); + + var dist_props = try allocator.alloc(G.Property, 3); + dist_props[0] = .{ + .key = Expr.init( + E.String, + .{ .data = "integrity" }, + logger.Loc.Empty, + ), + .value = Expr.init( + E.String, + .{ .data = try std.fmt.allocPrint(allocator, "{}", .{bun.fmt.integrity(integrity, .full)}) }, + logger.Loc.Empty, + ), + }; + dist_props[1] = .{ + .key = Expr.init( + E.String, + .{ .data = "shasum" }, + logger.Loc.Empty, + ), + .value = Expr.init( + E.String, + .{ .data = try std.fmt.allocPrint(allocator, "{s}", .{bun.fmt.bytesToHex(shasum, .lower)}) }, + logger.Loc.Empty, + ), + }; + dist_props[2] = .{ + .key = Expr.init( + E.String, + .{ .data = "tarball" }, + logger.Loc.Empty, + ), + .value = Expr.init( + E.String, + .{ + .data = try bun.fmt.allocPrint(allocator, "http://{s}/{s}/-/{s}", .{ + strings.withoutTrailingSlash(registry.url.href), + package_name, + std.fs.path.basename(abs_tarball_path), + }), + }, + logger.Loc.Empty, + ), + }; + + try json.set(allocator, "dist", Expr.init( + E.Object, + .{ .properties = G.Property.List.init(dist_props) }, + logger.Loc.Empty, + )); + + { + const workspace_root = bun.sys.openA( + strings.withoutSuffixComptime(manager.original_package_json_path, "package.json"), + bun.O.DIRECTORY, + 0, + ).unwrap() catch |err| { + Output.err(err, "failed to open workspace directory", .{}); + Global.crash(); + }; + defer _ = bun.sys.close(workspace_root); + + try normalizeBin( + allocator, + json, + package_name, + workspace_root, + ); + } + + const buffer_writer = try bun.js_printer.BufferWriter.init(allocator); + var writer = bun.js_printer.BufferPrinter.init(buffer_writer); + + const written = bun.js_printer.printJSON( + @TypeOf(&writer), + &writer, + json.*, + &json_source, + .{ + .minify_whitespace = true, + }, + ) catch |err| { + switch (err) { + error.OutOfMemory => |oom| return oom, + else => { + Output.errGeneric("failed to print normalized package.json: {s}", .{@errorName(err)}); + Global.crash(); + }, + } + }; + _ = written; + + return writer.ctx.writtenWithoutTrailingZero(); + } + + fn normalizeBin( + allocator: std.mem.Allocator, + json: *Expr, + package_name: string, + workspace_root: bun.FileDescriptor, + ) OOM!void { + var path_buf: bun.PathBuffer = undefined; + if (json.asProperty("bin")) |bin_query| { + switch (bin_query.expr.data) { + .e_string => |bin_str| { + var bin_props = std.ArrayList(G.Property).init(allocator); + const normalized = strings.withoutPrefixComptimeZ( + path.normalizeBufZ( + try bin_str.string(allocator), + &path_buf, + .posix, + ), + "./", + ); + if (!bun.sys.existsAt(workspace_root, normalized)) { + Output.warn("bin '{s}' does not exist", .{normalized}); + } + + try bin_props.append(.{ + .key = Expr.init( + E.String, + .{ .data = package_name }, + logger.Loc.Empty, + ), + .value = Expr.init( + E.String, + .{ .data = try allocator.dupe(u8, normalized) }, + logger.Loc.Empty, + ), + }); + + json.data.e_object.properties.ptr[bin_query.i].value = Expr.init( + E.Object, + .{ + .properties = G.Property.List.fromList(bin_props), + }, + logger.Loc.Empty, + ); + }, + .e_object => |bin_obj| { + var bin_props = std.ArrayList(G.Property).init(allocator); + for (bin_obj.properties.slice()) |bin_prop| { + const key = key: { + if (bin_prop.key) |key| { + if (key.isString() and key.data.e_string.len() != 0) { + break :key try allocator.dupeZ( + u8, + strings.withoutPrefixComptime( + path.normalizeBuf( + try key.data.e_string.string(allocator), + &path_buf, + .posix, + ), + "./", + ), + ); + } + } + + continue; + }; + + if (key.len == 0) { + continue; + } + + const value = value: { + if (bin_prop.value) |value| { + if (value.isString() and value.data.e_string.len() != 0) { + break :value try allocator.dupeZ( + u8, + strings.withoutPrefixComptimeZ( + // replace separators + path.normalizeBufZ( + try value.data.e_string.string(allocator), + &path_buf, + .posix, + ), + "./", + ), + ); + } + } + + continue; + }; + if (value.len == 0) { + continue; + } + + if (!bun.sys.existsAt(workspace_root, value)) { + Output.warn("bin '{s}' does not exist", .{value}); + } + + try bin_props.append(.{ + .key = Expr.init( + E.String, + .{ .data = key }, + logger.Loc.Empty, + ), + .value = Expr.init( + E.String, + .{ .data = value }, + logger.Loc.Empty, + ), + }); + } + + json.data.e_object.properties.ptr[bin_query.i].value = Expr.init( + E.Object, + .{ .properties = G.Property.List.fromList(bin_props) }, + logger.Loc.Empty, + ); + }, + else => {}, + } + } else if (json.asProperty("directories")) |directories_query| { + if (directories_query.expr.asProperty("bin")) |bin_query| { + const bin_dir_str = bin_query.expr.asString(allocator) orelse { + return; + }; + var bin_props = std.ArrayList(G.Property).init(allocator); + const normalized_bin_dir = try allocator.dupeZ( + u8, + strings.withoutTrailingSlash( + strings.withoutPrefixComptime( + path.normalizeBuf( + bin_dir_str, + &path_buf, + .posix, + ), + "./", + ), + ), + ); + + if (normalized_bin_dir.len == 0) { + return; + } + + const bin_dir = bun.sys.openat(workspace_root, normalized_bin_dir, bun.O.DIRECTORY, 0).unwrap() catch |err| { + if (err == error.ENOENT) { + Output.warn("bin directory '{s}' does not exist", .{normalized_bin_dir}); + return; + } else { + Output.err(err, "failed to open bin directory: '{s}'", .{normalized_bin_dir}); + Global.crash(); + } + }; + + var dirs: std.ArrayListUnmanaged(struct { std.fs.Dir, string, bool }) = .{}; + defer dirs.deinit(allocator); + + try dirs.append(allocator, .{ bin_dir.asDir(), normalized_bin_dir, false }); + + while (dirs.popOrNull()) |dir_info| { + var dir, const dir_subpath, const close_dir = dir_info; + defer if (close_dir) dir.close(); + + var iter = bun.DirIterator.iterate(dir, .u8); + while (iter.next().unwrap() catch null) |entry| { + const name, const subpath = name_and_subpath: { + const name = entry.name.slice(); + const join = try bun.fmt.allocPrintZ(allocator, "{s}{s}{s}", .{ + dir_subpath, + // only using posix separators + if (dir_subpath.len == 0) "" else std.fs.path.sep_str_posix, + strings.withoutTrailingSlash(name), + }); + + break :name_and_subpath .{ join[join.len - name.len ..][0..name.len :0], join }; + }; + + if (name.len == 0 or (name.len == 1 and name[0] == '.') or (name.len == 2 and name[0] == '.' and name[1] == '.')) { + continue; + } + + try bin_props.append(.{ + .key = Expr.init( + E.String, + .{ .data = std.fs.path.basenamePosix(subpath) }, + logger.Loc.Empty, + ), + .value = Expr.init( + E.String, + .{ .data = subpath }, + logger.Loc.Empty, + ), + }); + + if (entry.kind == .directory) { + const subdir = dir.openDirZ(name, .{ .iterate = true }) catch { + continue; + }; + try dirs.append(allocator, .{ subdir, subpath, true }); + } + } + } + + try json.set(allocator, "bin", Expr.init(E.Object, .{ .properties = G.Property.List.fromList(bin_props) }, logger.Loc.Empty)); + } + } + + // no bins + } + fn constructPublishHeaders( allocator: std.mem.Allocator, print_buf: *std.ArrayListUnmanaged(u8), @@ -978,7 +1326,6 @@ pub const PublishCommand = struct { fn constructPublishRequestBody( comptime directory_publish: bool, ctx: *const Context(directory_publish), - registry: *const Npm.Registry.Scope, ) OOM![]const u8 { const tag = if (ctx.manager.options.publish_config.tag.len > 0) ctx.manager.options.publish_config.tag @@ -1009,38 +1356,9 @@ pub const PublishCommand = struct { // "versions" { - try writer.print(",\"versions\":{{\"{s}\":{{\"name\":\"{s}\",\"version\":\"{s}\"", .{ + try writer.print(",\"versions\":{{\"{s}\":{s}}}", .{ version_without_build_tag, - ctx.package_name, - version_without_build_tag, - }); - - try writer.print(",\"_id\": \"{s}@{s}\"", .{ - ctx.package_name, - version_without_build_tag, - }); - - try writer.print(",\"_integrity\":\"{}\"", .{ - bun.fmt.integrity(ctx.integrity, .full), - }); - - try writer.print(",\"_nodeVersion\":\"{s}\",\"_npmVersion\":\"{s}\"", .{ - Environment.reported_nodejs_version, - // TODO: npm version - "10.8.3", - }); - - try writer.print(",\"dist\":{{\"integrity\":\"{}\",\"shasum\":\"{s}\"", .{ - bun.fmt.integrity(ctx.integrity, .full), - bun.fmt.bytesToHex(ctx.shasum, .lower), - }); - - // https://github.com/npm/cli/blob/63d6a732c3c0e9c19fd4d147eaa5cc27c29b168d/workspaces/libnpmpublish/lib/publish.js#L118 - // https:// -> http:// - try writer.print(",\"tarball\":\"http://{s}/{s}/-/{s}\"}}}}}}", .{ - strings.withoutTrailingSlash(registry.url.href), - ctx.package_name, - std.fs.path.basename(ctx.abs_tarball_path), + ctx.normalized_pkg_info, }); } diff --git a/src/install/bin.zig b/src/install/bin.zig index 8361b90bc9..e610529bd6 100644 --- a/src/install/bin.zig +++ b/src/install/bin.zig @@ -368,10 +368,19 @@ pub const Bin = extern struct { bun.Analytics.Features.binlinks += 1; - if (comptime Environment.isWindows) - this.createWindowsShim(abs_target, abs_dest, global) - else - this.createSymlink(abs_target, abs_dest, global); + if (comptime !Environment.isWindows) + this.createSymlink(abs_target, abs_dest, global) + else { + const target = bun.sys.openat(bun.invalid_fd, abs_target, bun.O.RDONLY, 0).unwrap() catch |err| { + if (err != error.EISDIR) { + // ignore directories, creating a shim for one won't do anything + this.err = err; + } + return; + }; + defer _ = bun.sys.close(target); + this.createWindowsShim(target, abs_target, abs_dest, global); + } if (this.err != null) { // cleanup on error just in case @@ -401,7 +410,7 @@ pub const Bin = extern struct { } } - fn createWindowsShim(this: *Linker, abs_target: [:0]const u8, abs_dest: [:0]const u8, global: bool) void { + fn createWindowsShim(this: *Linker, target: bun.FileDescriptor, abs_target: [:0]const u8, abs_dest: [:0]const u8, global: bool) void { const WinBinLinkingShim = @import("./windows-shim/BinLinkingShim.zig"); var shim_buf: [65536]u8 = undefined; @@ -435,13 +444,7 @@ pub const Bin = extern struct { const shebang = shebang: { const first_content_chunk = contents: { - const target = bun.openFileZ(abs_target, .{ .mode = .read_only }) catch |err| { - // it should exist, this error is real - this.err = err; - return; - }; - defer target.close(); - const reader = target.reader(); + const reader = target.asFile().reader(); const read = reader.read(&read_in_buf) catch break :contents null; if (read == 0) break :contents null; break :contents read_in_buf[0..read]; diff --git a/src/js_ast.zig b/src/js_ast.zig index d815627c45..f3c4bc578f 100644 --- a/src/js_ast.zig +++ b/src/js_ast.zig @@ -3436,6 +3436,56 @@ pub const Expr = struct { return if (asProperty(expr, name)) |query| query.expr else null; } + /// Don't use this if you care about performance. + /// + /// Sets the value of a property, creating it if it doesn't exist. + /// `expr` must be an object. + pub fn set(expr: *Expr, allocator: std.mem.Allocator, name: string, value: Expr) OOM!void { + bun.assertWithLocation(expr.isObject(), @src()); + for (0..expr.data.e_object.properties.len) |i| { + const prop = &expr.data.e_object.properties.ptr[i]; + const key = prop.key orelse continue; + if (std.meta.activeTag(key.data) != .e_string) continue; + if (key.data.e_string.eql(string, name)) { + prop.value = value; + return; + } + } + + var new_props = expr.data.e_object.properties.listManaged(allocator); + try new_props.append(.{ + .key = Expr.init(E.String, .{ .data = name }, logger.Loc.Empty), + .value = value, + }); + + expr.data.e_object.properties = BabyList(G.Property).fromList(new_props); + } + + /// Don't use this if you care about performance. + /// + /// Sets the value of a property to a string, creating it if it doesn't exist. + /// `expr` must be an object. + pub fn setString(expr: *Expr, allocator: std.mem.Allocator, name: string, value: string) OOM!void { + bun.assertWithLocation(expr.isObject(), @src()); + for (0..expr.data.e_object.properties.len) |i| { + const prop = &expr.data.e_object.properties.ptr[i]; + const key = prop.key orelse continue; + if (std.meta.activeTag(key.data) != .e_string) continue; + if (key.data.e_string.eql(string, name)) { + prop.value = Expr.init(E.String, .{ .data = value }, logger.Loc.Empty); + return; + } + } + + var new_props = expr.data.e_object.properties.listManaged(allocator); + try new_props.append(.{ + .key = Expr.init(E.String, .{ .data = name }, logger.Loc.Empty), + .value = Expr.init(E.String, .{ .data = value }, logger.Loc.Empty), + }); + + expr.data.e_object.properties = BabyList(G.Property).fromList(new_props); + } + pub fn getObject(expr: *const Expr, name: string) ?Expr { if (expr.asProperty(name)) |query| { if (query.expr.isObject()) { diff --git a/src/resolver/resolve_path.zig b/src/resolver/resolve_path.zig index 9cb74221a7..f2f54ef1f4 100644 --- a/src/resolver/resolve_path.zig +++ b/src/resolver/resolve_path.zig @@ -1147,6 +1147,12 @@ pub fn normalizeBuf(str: []const u8, buf: []u8, comptime _platform: Platform) [] return normalizeBufT(u8, str, buf, _platform); } +pub fn normalizeBufZ(str: []const u8, buf: []u8, comptime _platform: Platform) [:0]u8 { + const norm = normalizeBufT(u8, str, buf, _platform); + buf[norm.len] = 0; + return buf[0..norm.len :0]; +} + pub fn normalizeBufT(comptime T: type, str: []const T, buf: []T, comptime _platform: Platform) []T { if (str.len == 0) { buf[0] = '.'; diff --git a/src/string_immutable.zig b/src/string_immutable.zig index da92209a5e..c4412eafd8 100644 --- a/src/string_immutable.zig +++ b/src/string_immutable.zig @@ -6387,6 +6387,13 @@ pub fn withoutPrefixComptime(input: []const u8, comptime prefix: []const u8) []c return input; } +pub fn withoutPrefixComptimeZ(input: [:0]const u8, comptime prefix: []const u8) [:0]const u8 { + if (hasPrefixComptime(input, prefix)) { + return input[prefix.len..]; + } + return input; +} + pub fn withoutPrefixIfPossibleComptime(input: string, comptime prefix: string) ?string { if (hasPrefixComptime(input, prefix)) { return input[prefix.len..]; diff --git a/test/cli/install/bun-pack.test.ts b/test/cli/install/bun-pack.test.ts index 4f82725bc3..52f0e1d7f6 100644 --- a/test/cli/install/bun-pack.test.ts +++ b/test/cli/install/bun-pack.test.ts @@ -510,7 +510,6 @@ describe("workspaces", () => { 'error: Failed to resolve workspace version for "pkg1" in `dependencies`. Run `bun install` and try again.', ); - await rm(join(packageDir, "pack-workspace-protocol-fail-2.2.3.tgz")); await runBunInstall(bunEnv, packageDir); await pack(packageDir, bunEnv); const tarball = readTarball(join(packageDir, "pack-workspace-protocol-fail-2.2.3.tgz")); diff --git a/test/cli/install/registry/bun-install-registry.test.ts b/test/cli/install/registry/bun-install-registry.test.ts index 7c2d32126e..69bd30114e 100644 --- a/test/cli/install/registry/bun-install-registry.test.ts +++ b/test/cli/install/registry/bun-install-registry.test.ts @@ -1168,6 +1168,131 @@ describe("publish", async () => { expect(await file(join(packageDir, "node_modules", "publish-pkg-2", "package.json")).json()).toEqual(json); }); + for (const info of [ + { user: "bin1", bin: "bin1.js" }, + { user: "bin2", bin: { bin1: "bin1.js", bin2: "bin2.js" } }, + { user: "bin3", directories: { bin: "bins" } }, + ]) { + test(`can publish and install binaries with ${JSON.stringify(info)}`, async () => { + const publishDir = tmpdirSync(); + const bunfig = await authBunfig("binaries-" + info.user); + console.log({ packageDir, publishDir }); + + await Promise.all([ + rm(join(import.meta.dir, "packages", "publish-pkg-bins"), { recursive: true, force: true }), + write( + join(publishDir, "package.json"), + JSON.stringify({ + name: "publish-pkg-bins", + version: "1.1.1", + ...info, + }), + ), + write(join(publishDir, "bunfig.toml"), bunfig), + write(join(publishDir, "bin1.js"), `#!/usr/bin/env bun\nconsole.log("bin1!")`), + write(join(publishDir, "bin2.js"), `#!/usr/bin/env bun\nconsole.log("bin2!")`), + write(join(publishDir, "bins", "bin3.js"), `#!/usr/bin/env bun\nconsole.log("bin3!")`), + write(join(publishDir, "bins", "moredir", "bin4.js"), `#!/usr/bin/env bun\nconsole.log("bin4!")`), + + write( + join(packageDir, "package.json"), + JSON.stringify({ + name: "foo", + dependencies: { + "publish-pkg-bins": "1.1.1", + }, + }), + ), + ]); + + const { out, err, exitCode } = await publish(env, publishDir); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + expect(out).toContain("+ publish-pkg-bins@1.1.1"); + expect(exitCode).toBe(0); + + await runBunInstall(env, packageDir); + + const results = await Promise.all([ + exists(join(packageDir, "node_modules", ".bin", isWindows ? "bin1.bunx" : "bin1")), + exists(join(packageDir, "node_modules", ".bin", isWindows ? "bin2.bunx" : "bin2")), + exists(join(packageDir, "node_modules", ".bin", isWindows ? "bin3.js.bunx" : "bin3.js")), + exists(join(packageDir, "node_modules", ".bin", isWindows ? "bin4.js.bunx" : "bin4.js")), + exists(join(packageDir, "node_modules", ".bin", isWindows ? "moredir" : "moredir/bin4.js")), + exists(join(packageDir, "node_modules", ".bin", isWindows ? "publish-pkg-bins.bunx" : "publish-pkg-bins")), + ]); + + switch (info.user) { + case "bin1": { + expect(results).toEqual([false, false, false, false, false, true]); + break; + } + case "bin2": { + expect(results).toEqual([true, true, false, false, false, false]); + break; + } + case "bin3": { + expect(results).toEqual([false, false, true, true, !isWindows, false]); + break; + } + } + }); + } + + test("dependencies are installed", async () => { + const publishDir = tmpdirSync(); + const bunfig = await authBunfig("manydeps"); + await Promise.all([ + rm(join(import.meta.dir, "packages", "publish-pkg-deps"), { recursive: true, force: true }), + write( + join(publishDir, "package.json"), + JSON.stringify( + { + name: "publish-pkg-deps", + version: "1.1.1", + dependencies: { + "no-deps": "1.0.0", + }, + peerDependencies: { + "a-dep": "1.0.1", + }, + optionalDependencies: { + "basic-1": "1.0.0", + }, + }, + null, + 2, + ), + ), + write(join(publishDir, "bunfig.toml"), bunfig), + write( + join(packageDir, "package.json"), + JSON.stringify({ + name: "foo", + dependencies: { + "publish-pkg-deps": "1.1.1", + }, + }), + ), + ]); + + let { out, err, exitCode } = await publish(env, publishDir); + expect(err).not.toContain("error:"); + expect(err).not.toContain("warn:"); + expect(out).toContain("+ publish-pkg-deps@1.1.1"); + expect(exitCode).toBe(0); + + await runBunInstall(env, packageDir); + + const results = await Promise.all([ + exists(join(packageDir, "node_modules", "no-deps", "package.json")), + exists(join(packageDir, "node_modules", "a-dep", "package.json")), + exists(join(packageDir, "node_modules", "basic-1", "package.json")), + ]); + + expect(results).toEqual([true, true, true]); + }); + test("can publish workspace package", async () => { const bunfig = await authBunfig("workspace"); const pkgJson = {