diff options
author | 2023-08-08 06:11:32 +0300 | |
---|---|---|
committer | 2023-08-07 20:11:32 -0700 | |
commit | a32097aa9f53f7c8ea1331c61dc2658bc1b11208 (patch) | |
tree | efd62b1e3471923c92cbe2c2fb312ad9cabfefb4 | |
parent | 1239c9460ac1d10376e27653b4c34d789f2a8f43 (diff) | |
download | bun-a32097aa9f53f7c8ea1331c61dc2658bc1b11208.tar.gz bun-a32097aa9f53f7c8ea1331c61dc2658bc1b11208.tar.zst bun-a32097aa9f53f7c8ea1331c61dc2658bc1b11208.zip |
implement `bun update` (#4046)
- analogous to `npm update`
- `bun update <name>` to refresh specified package under `package.json`
- `bun update` to refresh all package to latest versions
-rw-r--r-- | src/cli.zig | 24 | ||||
-rw-r--r-- | src/cli/update_command.zig | 8 | ||||
-rw-r--r-- | src/install/install.zig | 172 | ||||
-rw-r--r-- | src/install/lockfile.zig | 15 | ||||
-rw-r--r-- | test/cli/install/bun-update.test.ts | 259 |
5 files changed, 389 insertions, 89 deletions
diff --git a/src/cli.zig b/src/cli.zig index ca0993dbb..759c45fde 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -1092,11 +1092,11 @@ pub const Command = struct { for (args_iter.buf) |arg| { const span = std.mem.span(arg); if (span.len > 0 and (strings.eqlComptime(span, "-g") or strings.eqlComptime(span, "--global"))) { - break :brk Command.Tag.AddCommand; + break :brk .AddCommand; } } - break :brk Command.Tag.InstallCommand; + break :brk .InstallCommand; }, RootCommandMatcher.case("c"), RootCommandMatcher.case("create") => .CreateCommand, @@ -1104,7 +1104,8 @@ pub const Command = struct { RootCommandMatcher.case("pm") => .PackageManagerCommand, - RootCommandMatcher.case("add"), RootCommandMatcher.case("update"), RootCommandMatcher.case("a") => .AddCommand, + RootCommandMatcher.case("add"), RootCommandMatcher.case("a") => .AddCommand, + RootCommandMatcher.case("update") => .UpdateCommand, RootCommandMatcher.case("r"), RootCommandMatcher.case("remove"), RootCommandMatcher.case("rm"), RootCommandMatcher.case("uninstall") => .RemoveCommand, RootCommandMatcher.case("run") => .RunCommand, @@ -1154,6 +1155,7 @@ pub const Command = struct { const RemoveCommand = @import("./cli/remove_command.zig").RemoveCommand; const RunCommand = @import("./cli/run_command.zig").RunCommand; const ShellCompletions = @import("./cli/shell_completions.zig"); + const UpdateCommand = @import("./cli/update_command.zig").UpdateCommand; const UpgradeCommand = @import("./cli/upgrade_command.zig").UpgradeCommand; const BunxCommand = @import("./cli/bunx_command.zig").BunxCommand; @@ -1173,6 +1175,7 @@ pub const Command = struct { // _ = RunCommand; // _ = ShellCompletions; // _ = TestCommand; + // _ = UpdateCommand; // _ = UpgradeCommand; // _ = BunxCommand; } @@ -1243,6 +1246,13 @@ pub const Command = struct { try AddCommand.exec(ctx); return; }, + .UpdateCommand => { + if (comptime bun.fast_debug_build_mode and bun.fast_debug_build_cmd != .UpdateCommand) unreachable; + const ctx = try Command.Context.create(allocator, log, .UpdateCommand); + + try UpdateCommand.exec(ctx); + return; + }, .BunxCommand => { if (comptime bun.fast_debug_build_mode and bun.fast_debug_build_cmd != .BunxCommand) unreachable; const ctx = try Command.Context.create(allocator, log, .BunxCommand); @@ -1624,6 +1634,7 @@ pub const Command = struct { RunCommand, TestCommand, UnlinkCommand, + UpdateCommand, UpgradeCommand, pub fn params(comptime cmd: Tag) []const Arguments.ParamType { @@ -1637,14 +1648,14 @@ pub const Command = struct { pub fn readGlobalConfig(this: Tag) bool { return switch (this) { - .BunxCommand, .PackageManagerCommand, .InstallCommand, .AddCommand, .RemoveCommand => true, + .BunxCommand, .PackageManagerCommand, .InstallCommand, .AddCommand, .RemoveCommand, .UpdateCommand => true, else => false, }; } pub fn isNPMRelated(this: Tag) bool { return switch (this) { - .BunxCommand, .LinkCommand, .UnlinkCommand, .PackageManagerCommand, .InstallCommand, .AddCommand, .RemoveCommand => true, + .BunxCommand, .LinkCommand, .UnlinkCommand, .PackageManagerCommand, .InstallCommand, .AddCommand, .RemoveCommand, .UpdateCommand => true, else => false, }; } @@ -1656,6 +1667,7 @@ pub const Command = struct { .InstallCommand = true, .AddCommand = true, .RemoveCommand = true, + .UpdateCommand = true, .PackageManagerCommand = true, .BunxCommand = true, .AutoCommand = true, @@ -1669,6 +1681,7 @@ pub const Command = struct { .InstallCommand = true, .AddCommand = true, .RemoveCommand = true, + .UpdateCommand = true, .PackageManagerCommand = true, .BunxCommand = true, }); @@ -1678,6 +1691,7 @@ pub const Command = struct { .InstallCommand = false, .AddCommand = false, .RemoveCommand = false, + .UpdateCommand = false, .PackageManagerCommand = false, .LinkCommand = false, .UnlinkCommand = false, diff --git a/src/cli/update_command.zig b/src/cli/update_command.zig new file mode 100644 index 000000000..bc2bfa76c --- /dev/null +++ b/src/cli/update_command.zig @@ -0,0 +1,8 @@ +const Command = @import("../cli.zig").Command; +const PackageManager = @import("../install/install.zig").PackageManager; + +pub const UpdateCommand = struct { + pub fn exec(ctx: Command.Context) !void { + try PackageManager.update(ctx); + } +}; diff --git a/src/install/install.zig b/src/install/install.zig index 5e1b54aa6..38f2ae11f 100644 --- a/src/install/install.zig +++ b/src/install/install.zig @@ -1641,6 +1641,7 @@ pub const PackageManager = struct { root_download_node: std.Progress.Node = undefined, to_remove: []const UpdateRequest = &[_]UpdateRequest{}, + to_update: bool = false, root_package_json_file: std.fs.File, root_dependency_list: Lockfile.DependencySlice = .{}, @@ -3821,9 +3822,9 @@ pub const PackageManager = struct { }, } } - for (manager.package_json_updates) |*update| { - if (strings.eql(update.name, name.slice())) { - update.failed = true; + for (manager.package_json_updates) |*request| { + if (strings.eql(request.name, name.slice())) { + request.failed = true; manager.options.do.save_lockfile = false; manager.options.do.save_yarn_lock = false; manager.options.do.install_packages = false; @@ -4880,21 +4881,21 @@ pub const PackageManager = struct { // 3. There is a "dependencies" (or equivalent list), and the package name exists in multiple lists ast_modifier: { // Try to use the existing spot in the dependencies list if possible - for (updates) |*update| { + for (updates) |*request| { inline for ([_]string{ "dependencies", "devDependencies", "optionalDependencies" }) |list| { if (current_package_json.asProperty(list)) |query| { if (query.expr.data == .e_object) { if (query.expr.asProperty( - if (update.is_aliased) - update.name + if (request.is_aliased) + request.name else - update.version.literal.slice(update.version_buf), + request.version.literal.slice(request.version_buf), )) |value| { if (value.expr.data == .e_string) { - if (!update.resolved_name.isEmpty() and strings.eql(list, dependency_list)) { + if (!request.resolved_name.isEmpty() and strings.eql(list, dependency_list)) { replacing += 1; } else { - update.e_string = value.expr.data.e_string; + request.e_string = value.expr.data.e_string; remaining -= 1; } } @@ -4919,16 +4920,16 @@ pub const PackageManager = struct { bun.copy(G.Property, new_dependencies, dependencies); @memset(new_dependencies[dependencies.len..], G.Property{}); - outer: for (updates) |*update| { - if (update.e_string != null) continue; - defer if (comptime Environment.allow_assert) std.debug.assert(update.e_string != null); + outer: for (updates) |*request| { + if (request.e_string != null) continue; + defer if (comptime Environment.allow_assert) std.debug.assert(request.e_string != null); var k: usize = 0; while (k < new_dependencies.len) : (k += 1) { if (new_dependencies[k].key) |key| { - if (!update.is_aliased and !update.resolved_name.isEmpty() and key.data.e_string.eql( + if (!request.is_aliased and !request.resolved_name.isEmpty() and key.data.e_string.eql( string, - update.resolved_name.slice(update.version_buf), + request.resolved_name.slice(request.version_buf), )) { // This actually is a duplicate which we did not // pick up before dependency resolution. @@ -4943,12 +4944,12 @@ pub const PackageManager = struct { } if (key.data.e_string.eql( string, - if (update.is_aliased) - update.name + if (request.is_aliased) + request.name else - update.version.literal.slice(update.version_buf), + request.version.literal.slice(request.version_buf), )) { - if (update.resolved_name.isEmpty()) { + if (request.resolved_name.isEmpty()) { // This actually is a duplicate like "react" // appearing in both "dependencies" and "optionalDependencies". // For this case, we'll just swap remove it @@ -4968,12 +4969,12 @@ pub const PackageManager = struct { new_dependencies[k].key = try JSAst.Expr.init( JSAst.E.String, JSAst.E.String{ - .data = try allocator.dupe(u8, if (update.is_aliased) - update.name - else if (update.resolved_name.isEmpty()) - update.version.literal.slice(update.version_buf) + .data = try allocator.dupe(u8, if (request.is_aliased) + request.name + else if (request.resolved_name.isEmpty()) + request.version.literal.slice(request.version_buf) else - update.resolved_name.slice(update.version_buf)), + request.resolved_name.slice(request.version_buf)), }, logger.Loc.Empty, ).clone(allocator); @@ -4986,8 +4987,8 @@ pub const PackageManager = struct { }, logger.Loc.Empty, ).clone(allocator); - update.e_string = new_dependencies[k].value.?.data.e_string; - if (update.is_aliased) continue :outer; + request.e_string = new_dependencies[k].value.?.data.e_string; + if (request.is_aliased) continue :outer; } } } @@ -5051,26 +5052,26 @@ pub const PackageManager = struct { } } - for (updates) |*update| { - if (update.e_string) |e_string| { - e_string.data = switch (update.resolution.tag) { - .npm => if (update.version.tag == .dist_tag and update.version.literal.isEmpty()) + for (updates) |*request| { + if (request.e_string) |e_string| { + e_string.data = switch (request.resolution.tag) { + .npm => if (request.version.tag == .dist_tag and request.version.literal.isEmpty()) switch (exact_versions) { false => std.fmt.allocPrint(allocator, "^{}", .{ - update.resolution.value.npm.version.fmt(update.version_buf), + request.resolution.value.npm.version.fmt(request.version_buf), }) catch unreachable, true => std.fmt.allocPrint(allocator, "{}", .{ - update.resolution.value.npm.version.fmt(update.version_buf), + request.resolution.value.npm.version.fmt(request.version_buf), }) catch unreachable, } else null, - .uninitialized => switch (update.version.tag) { + .uninitialized => switch (request.version.tag) { .uninitialized => try allocator.dupe(u8, latest), else => null, }, else => null, - } orelse try allocator.dupe(u8, update.version.literal.slice(update.version_buf)); + } orelse try allocator.dupe(u8, request.version.literal.slice(request.version_buf)); } } } @@ -5079,6 +5080,7 @@ pub const PackageManager = struct { // Corresponds to possible commands from the CLI. pub const Subcommand = enum { install, + update, pm, add, remove, @@ -5086,17 +5088,7 @@ pub const PackageManager = struct { unlink, }; - pub fn init( - ctx: Command.Context, - comptime subcommand: Subcommand, - ) !*PackageManager { - return initMaybeInstall(ctx, subcommand); - } - - fn initMaybeInstall( - ctx: Command.Context, - comptime subcommand: Subcommand, - ) !*PackageManager { + pub fn init(ctx: Command.Context, comptime subcommand: Subcommand) !*PackageManager { const cli = try CommandLineArguments.parse(ctx.allocator, subcommand); if (comptime subcommand == .install) { @@ -5443,21 +5435,19 @@ pub const PackageManager = struct { try package_json_file.pwriteAll("{\"dependencies\": {}}", 0); } - pub inline fn add( - ctx: Command.Context, - ) !void { + pub inline fn update(ctx: Command.Context) !void { + try updatePackageJSONAndInstall(ctx, .update, .update); + } + + pub inline fn add(ctx: Command.Context) !void { try updatePackageJSONAndInstall(ctx, .add, .add); } - pub inline fn remove( - ctx: Command.Context, - ) !void { + pub inline fn remove(ctx: Command.Context) !void { try updatePackageJSONAndInstall(ctx, .remove, .remove); } - pub inline fn link( - ctx: Command.Context, - ) !void { + pub inline fn link(ctx: Command.Context) !void { var manager = PackageManager.init(ctx, .link) catch |err| brk: { if (err == error.MissingPackageJSON) { try attemptToCreatePackageJSON(); @@ -5602,14 +5592,12 @@ pub const PackageManager = struct { } else { // bun link lodash switch (manager.options.log_level) { - inline else => |log_level| try updatePackageJSONAndInstallWithManager(ctx, manager, .link, log_level), + inline else => |log_level| try manager.updatePackageJSONAndInstallWithManager(ctx, .link, log_level), } } } - pub inline fn unlink( - ctx: Command.Context, - ) !void { + pub inline fn unlink(ctx: Command.Context) !void { var manager = PackageManager.init(ctx, .unlink) catch |err| brk: { if (err == error.MissingPackageJSON) { try attemptToCreatePackageJSON(); @@ -5770,6 +5758,10 @@ pub const PackageManager = struct { clap.parseParam("<POS> ... ") catch unreachable, }; + const update_params = install_params_ ++ [_]ParamType{ + clap.parseParam("<POS> ... \"name\" of packages to update") catch unreachable, + }; + const pm_params = install_params_ ++ [_]ParamType{ clap.parseParam("<POS> ... ") catch unreachable, }; @@ -5848,6 +5840,7 @@ pub const PackageManager = struct { pub fn parse(allocator: std.mem.Allocator, comptime subcommand: Subcommand) !CommandLineArguments { comptime var params: []const ParamType = &switch (subcommand) { .install => install_params, + .update => update_params, .pm => pm_params, .add => add_params, .remove => remove_params, @@ -6087,15 +6080,22 @@ pub const PackageManager = struct { comptime op: Lockfile.Package.Diff.Op, comptime subcommand: Subcommand, ) !void { - var manager = PackageManager.init(ctx, subcommand) catch |err| brk: { + var manager = init(ctx, subcommand) catch |err| brk: { if (err == error.MissingPackageJSON) { - if (op == .remove) { - Output.prettyErrorln("<r>No package.json, so nothing to remove\n", .{}); - Global.crash(); + switch (op) { + .update => { + Output.prettyErrorln("<r>No package.json, so nothing to update\n", .{}); + Global.crash(); + }, + .remove => { + Output.prettyErrorln("<r>No package.json, so nothing to remove\n", .{}); + Global.crash(); + }, + else => { + try attemptToCreatePackageJSON(); + break :brk try PackageManager.init(ctx, subcommand); + }, } - - try attemptToCreatePackageJSON(); - break :brk try PackageManager.init(ctx, subcommand); } return err; @@ -6107,13 +6107,13 @@ pub const PackageManager = struct { } switch (manager.options.log_level) { - inline else => |log_level| try updatePackageJSONAndInstallWithManager(ctx, manager, op, log_level), + inline else => |log_level| try manager.updatePackageJSONAndInstallWithManager(ctx, op, log_level), } } fn updatePackageJSONAndInstallWithManager( - ctx: Command.Context, manager: *PackageManager, + ctx: Command.Context, comptime op: Lockfile.Package.Diff.Op, comptime log_level: Options.LogLevel, ) !void { @@ -6125,7 +6125,7 @@ pub const PackageManager = struct { const off = @as(u64, @intCast(std.time.milliTimestamp())); switch (op) { - .update, .add => { + .add => { const filler = @import("../cli.zig").HelpCommand.packages_to_add_filler; examples_to_print[0] = filler[@as(usize, @intCast((off) % filler.len))]; @@ -6210,9 +6210,8 @@ pub const PackageManager = struct { } var updates = UpdateRequest.parse(ctx.allocator, ctx.log, manager.options.positionals[1..], &update_requests, op); - try updatePackageJSONAndInstallWithManagerWithUpdates( + try manager.updatePackageJSONAndInstallWithManagerWithUpdates( ctx, - manager, updates, false, op, @@ -6221,8 +6220,8 @@ pub const PackageManager = struct { } fn updatePackageJSONAndInstallWithManagerWithUpdates( - ctx: Command.Context, manager: *PackageManager, + ctx: Command.Context, updates: []UpdateRequest, auto_free: bool, comptime op: Lockfile.Package.Diff.Op, @@ -6304,7 +6303,7 @@ pub const PackageManager = struct { .remove => { // if we're removing, they don't have to specify where it is installed in the dependencies list // they can even put it multiple times and we will just remove all of them - for (updates) |update| { + for (updates) |request| { inline for ([_]string{ "dependencies", "devDependencies", "optionalDependencies", "peerDependencies" }) |list| { if (current_package_json.asProperty(list)) |query| { if (query.expr.data == .e_object) { @@ -6313,7 +6312,7 @@ pub const PackageManager = struct { var new_len = dependencies.len; while (i < dependencies.len) : (i += 1) { if (dependencies[i].key.?.data == .e_string) { - if (dependencies[i].key.?.data.e_string.eql(string, update.name)) { + if (dependencies[i].key.?.data.e_string.eql(string, request.name)) { if (new_len > 1) { dependencies[i] = dependencies[new_len - 1]; new_len -= 1; @@ -6354,7 +6353,7 @@ pub const PackageManager = struct { } manager.to_remove = updates; }, - .link, .add, .update => { + .link, .add => { try PackageJSONEditor.edit( ctx.allocator, updates, @@ -6364,6 +6363,10 @@ pub const PackageManager = struct { ); manager.package_json_updates = updates; }, + .update => { + manager.package_json_updates = updates; + manager.to_update = true; + }, else => {}, } @@ -6397,8 +6400,8 @@ pub const PackageManager = struct { try manager.installWithManager(ctx, new_package_json_source, log_level); if (op == .update or op == .add or op == .link) { - for (manager.package_json_updates) |update| { - if (update.failed) { + for (manager.package_json_updates) |request| { + if (request.failed) { Global.exit(1); return; } @@ -6454,14 +6457,14 @@ pub const PackageManager = struct { bun.copy(u8, &node_modules_buf, "node_modules" ++ std.fs.path.sep_str); var offset_buf = node_modules_buf["node_modules/".len..]; const name_hashes = manager.lockfile.packages.items(.name_hash); - for (updates) |update| { + for (updates) |request| { // If the package no longer exists in the updated lockfile, delete the directory // This is not thorough. // It does not handle nested dependencies // This is a quick & dirty cleanup intended for when deleting top-level dependencies - if (std.mem.indexOfScalar(PackageNameHash, name_hashes, String.Builder.stringHash(update.name)) == null) { - bun.copy(u8, offset_buf, update.name); - cwd.deleteTree(node_modules_buf[0 .. "node_modules/".len + update.name.len]) catch {}; + if (std.mem.indexOfScalar(PackageNameHash, name_hashes, String.Builder.stringHash(request.name)) == null) { + bun.copy(u8, offset_buf, request.name); + cwd.deleteTree(node_modules_buf[0 .. "node_modules/".len + request.name.len]) catch {}; } } @@ -6500,7 +6503,7 @@ pub const PackageManager = struct { var package_json_cwd: string = ""; pub inline fn install(ctx: Command.Context) !void { - var manager = initMaybeInstall(ctx, .install) catch |err| { + var manager = init(ctx, .install) catch |err| { if (err == error.SwitchToBunAdd) { return add(ctx); } @@ -7493,6 +7496,7 @@ pub const PackageManager = struct { &lockfile, &root, &maybe_root, + if (manager.to_update) manager.package_json_updates else null, mapping, ); @@ -7666,9 +7670,9 @@ pub const PackageManager = struct { } if (manager.lockfile.packages.len > 0) { - for (manager.package_json_updates) |update| { + for (manager.package_json_updates) |request| { // prevent redundant errors - if (update.failed) { + if (request.failed) { return error.InstallFailed; } } @@ -7845,8 +7849,8 @@ pub const PackageManager = struct { } } else if (manager.summary.remove > 0) { if (manager.to_remove.len > 0) { - for (manager.to_remove) |update| { - Output.prettyln(" <r><red>-<r> {s}", .{update.name}); + for (manager.to_remove) |request| { + Output.prettyln(" <r><red>-<r> {s}", .{request.name}); } } diff --git a/src/install/lockfile.zig b/src/install/lockfile.zig index 869d46f4d..e293b2213 100644 --- a/src/install/lockfile.zig +++ b/src/install/lockfile.zig @@ -2393,6 +2393,7 @@ pub const Package = extern struct { to_lockfile: *Lockfile, from: *Lockfile.Package, to: *Lockfile.Package, + update_requests: ?[]PackageManager.UpdateRequest, id_mapping: ?[]PackageID, ) !Summary { var summary = Summary{}; @@ -2427,6 +2428,19 @@ pub const Package = extern struct { defer to_i += 1; if (to_deps[to_i].eql(from_dep, to_lockfile.buffers.string_bytes.items, from_lockfile.buffers.string_bytes.items)) { + if (update_requests) |updates| { + if (updates.len == 0 or brk: { + for (updates) |request| { + if (from_dep.name_hash == request.name_hash) break :brk true; + } + break :brk false; + }) { + // Listed as to be updated + summary.update += 1; + continue; + } + } + if (id_mapping) |mapping| { const version = to_deps[to_i].version; if (switch (version.tag) { @@ -2452,6 +2466,7 @@ pub const Package = extern struct { to_lockfile, &from_pkg, &workspace, + update_requests, null, ); diff --git a/test/cli/install/bun-update.test.ts b/test/cli/install/bun-update.test.ts new file mode 100644 index 000000000..2a6ee4eaf --- /dev/null +++ b/test/cli/install/bun-update.test.ts @@ -0,0 +1,259 @@ +import { file, listen, Socket, spawn } from "bun"; +import { afterAll, afterEach, beforeAll, beforeEach, expect, it } from "bun:test"; +import { bunExe, bunEnv as env } from "harness"; +import { access, mkdir, readlink, realpath, rm, writeFile } from "fs/promises"; +import { join } from "path"; +import { + dummyAfterAll, + dummyAfterEach, + dummyBeforeAll, + dummyBeforeEach, + dummyRegistry, + package_dir, + readdirSorted, + requested, + root_url, + setHandler, +} from "./dummy.registry.js"; + +beforeAll(dummyBeforeAll); +afterAll(dummyAfterAll); +beforeEach(dummyBeforeEach); +afterEach(dummyAfterEach); + +it("should update to latest version of dependency", async () => { + const urls: string[] = []; + const registry = { + "0.0.3": { + bin: { + "baz-run": "index.js", + }, + }, + "0.0.5": { + bin: { + "baz-exec": "index.js", + }, + }, + latest: "0.0.3", + }; + setHandler(dummyRegistry(urls, registry)); + await writeFile( + join(package_dir, "package.json"), + JSON.stringify({ + name: "foo", + dependencies: { + baz: "~0.0.2", + }, + }), + ); + const { + stdout: stdout1, + stderr: stderr1, + exited: exited1, + } = spawn({ + cmd: [bunExe(), "install"], + cwd: package_dir, + stdout: null, + stdin: "pipe", + stderr: "pipe", + env, + }); + expect(stderr1).toBeDefined(); + const err1 = await new Response(stderr1).text(); + expect(err1).not.toContain("error:"); + expect(err1).toContain("Saved lockfile"); + expect(stdout1).toBeDefined(); + const out1 = await new Response(stdout1).text(); + expect(out1.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + " + baz@0.0.3", + "", + " 1 packages installed", + ]); + expect(await exited1).toBe(0); + expect(urls.sort()).toEqual([`${root_url}/baz`, `${root_url}/baz-0.0.3.tgz`]); + expect(requested).toBe(2); + expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".bin", ".cache", "baz"]); + expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toEqual(["baz-run"]); + expect(await readlink(join(package_dir, "node_modules", ".bin", "baz-run"))).toBe(join("..", "baz", "index.js")); + expect(await readdirSorted(join(package_dir, "node_modules", "baz"))).toEqual(["index.js", "package.json"]); + expect(await file(join(package_dir, "node_modules", "baz", "package.json")).json()).toEqual({ + name: "baz", + version: "0.0.3", + bin: { + "baz-run": "index.js", + }, + }); + await access(join(package_dir, "bun.lockb")); + // Perform `bun update` with updated registry & lockfile from before + await rm(join(package_dir, "node_modules"), { force: true, recursive: true }); + urls.length = 0; + registry.latest = "0.0.5"; + setHandler(dummyRegistry(urls, registry)); + const { + stdout: stdout2, + stderr: stderr2, + exited: exited2, + } = spawn({ + cmd: [bunExe(), "update", "baz"], + cwd: package_dir, + stdout: null, + stdin: "pipe", + stderr: "pipe", + env, + }); + expect(stderr2).toBeDefined(); + const err2 = await new Response(stderr2).text(); + expect(err2).not.toContain("error:"); + expect(err2).toContain("Saved lockfile"); + expect(stdout2).toBeDefined(); + const out2 = await new Response(stdout2).text(); + expect(out2.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + "", + " installed baz@0.0.5 with binaries:", + " - baz-exec", + "", + "", + " 1 packages installed", + ]); + expect(await exited2).toBe(0); + expect(urls.sort()).toEqual([`${root_url}/baz`, `${root_url}/baz-0.0.5.tgz`]); + expect(requested).toBe(4); + expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".bin", ".cache", "baz"]); + expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toEqual(["baz-exec"]); + expect(await readlink(join(package_dir, "node_modules", ".bin", "baz-exec"))).toBe(join("..", "baz", "index.js")); + expect(await readdirSorted(join(package_dir, "node_modules", "baz"))).toEqual(["index.js", "package.json"]); + expect(await file(join(package_dir, "node_modules", "baz", "package.json")).json()).toEqual({ + name: "baz", + version: "0.0.5", + bin: { + "baz-exec": "index.js", + }, + }); + await access(join(package_dir, "bun.lockb")); +}); + +it("should update to latest versions of dependencies", async () => { + const urls: string[] = []; + const registry = { + "0.0.3": { + bin: { + "baz-run": "index.js", + }, + }, + "0.0.5": { + bin: { + "baz-exec": "index.js", + }, + }, + "0.1.0": {}, + latest: "0.0.3", + }; + setHandler(dummyRegistry(urls, registry)); + await writeFile( + join(package_dir, "package.json"), + JSON.stringify({ + name: "foo", + dependencies: { + "@barn/moo": "^0.1.0", + baz: "~0.0.2", + }, + }), + ); + const { + stdout: stdout1, + stderr: stderr1, + exited: exited1, + } = spawn({ + cmd: [bunExe(), "install"], + cwd: package_dir, + stdout: null, + stdin: "pipe", + stderr: "pipe", + env, + }); + expect(stderr1).toBeDefined(); + const err1 = await new Response(stderr1).text(); + expect(err1).not.toContain("error:"); + expect(err1).toContain("Saved lockfile"); + expect(stdout1).toBeDefined(); + const out1 = await new Response(stdout1).text(); + expect(out1.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + " + @barn/moo@0.1.0", + " + baz@0.0.3", + "", + " 2 packages installed", + ]); + expect(await exited1).toBe(0); + expect(urls.sort()).toEqual([ + `${root_url}/@barn/moo`, + `${root_url}/@barn/moo-0.1.0.tgz`, + `${root_url}/baz`, + `${root_url}/baz-0.0.3.tgz`, + ]); + expect(requested).toBe(4); + expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".bin", ".cache", "@barn", "baz"]); + expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toEqual(["baz-run"]); + expect(await readlink(join(package_dir, "node_modules", ".bin", "baz-run"))).toBe(join("..", "baz", "index.js")); + expect(await readdirSorted(join(package_dir, "node_modules", "@barn"))).toEqual(["moo"]); + expect(await readdirSorted(join(package_dir, "node_modules", "@barn", "moo"))).toEqual(["package.json"]); + expect(await readdirSorted(join(package_dir, "node_modules", "baz"))).toEqual(["index.js", "package.json"]); + expect(await file(join(package_dir, "node_modules", "baz", "package.json")).json()).toEqual({ + name: "baz", + version: "0.0.3", + bin: { + "baz-run": "index.js", + }, + }); + await access(join(package_dir, "bun.lockb")); + // Perform `bun update` with updated registry & lockfile from before + await rm(join(package_dir, "node_modules"), { force: true, recursive: true }); + urls.length = 0; + registry.latest = "0.0.5"; + setHandler(dummyRegistry(urls, registry)); + const { + stdout: stdout2, + stderr: stderr2, + exited: exited2, + } = spawn({ + cmd: [bunExe(), "update"], + cwd: package_dir, + stdout: null, + stdin: "pipe", + stderr: "pipe", + env, + }); + expect(stderr2).toBeDefined(); + const err2 = await new Response(stderr2).text(); + expect(err2).not.toContain("error:"); + expect(err2).toContain("Saved lockfile"); + expect(stdout2).toBeDefined(); + const out2 = await new Response(stdout2).text(); + expect(out2.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + " + @barn/moo@0.1.0", + " + baz@0.0.5", + "", + " 2 packages installed", + ]); + expect(await exited2).toBe(0); + expect(urls.sort()).toEqual([ + `${root_url}/@barn/moo`, + `${root_url}/@barn/moo-0.1.0.tgz`, + `${root_url}/baz`, + `${root_url}/baz-0.0.5.tgz`, + ]); + expect(requested).toBe(8); + expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".bin", ".cache", "@barn", "baz"]); + expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toEqual(["baz-exec"]); + expect(await readlink(join(package_dir, "node_modules", ".bin", "baz-exec"))).toBe(join("..", "baz", "index.js")); + expect(await readdirSorted(join(package_dir, "node_modules", "@barn"))).toEqual(["moo"]); + expect(await readdirSorted(join(package_dir, "node_modules", "@barn", "moo"))).toEqual(["package.json"]); + expect(await readdirSorted(join(package_dir, "node_modules", "baz"))).toEqual(["index.js", "package.json"]); + expect(await file(join(package_dir, "node_modules", "baz", "package.json")).json()).toEqual({ + name: "baz", + version: "0.0.5", + bin: { + "baz-exec": "index.js", + }, + }); + await access(join(package_dir, "bun.lockb")); +}); |