From ba2ea6fbb2c6cc95cfb2d8033f940485c6fb3835 Mon Sep 17 00:00:00 2001 From: Dylan Conway <35280289+dylan-conway@users.noreply.github.com> Date: Tue, 27 Aug 2024 00:18:27 -0700 Subject: [PATCH] add `--filter` and package pattern arguments to `bun outdated` (#13557) --- docs/cli/outdated.md | 38 +- docs/nav.ts | 3 + src/bun.js/api/glob.zig | 2 +- src/cli/outdated_command.zig | 628 ++++++++++++------ src/cli/package_manager_command.zig | 6 +- src/install/install.zig | 48 +- .../bun-install-registry.test.ts.snap | 18 +- .../registry/bun-install-registry.test.ts | 97 +++ 8 files changed, 632 insertions(+), 208 deletions(-) diff --git a/docs/cli/outdated.md b/docs/cli/outdated.md index 75d38e9c903c9b..492aee9efae327 100644 --- a/docs/cli/outdated.md +++ b/docs/cli/outdated.md @@ -1,10 +1,10 @@ -Use `bun outdated` to display a table of outdated dependencies with their latest versions: +Use `bun outdated` to display a table of outdated dependencies with their latest versions for the current workspace: ```sh $ bun outdated |--------------------------------------------------------------------| -| Packages | Current | Update | Latest | +| Package | Current | Update | Latest | |----------------------------------------|---------|--------|--------| | @types/bun (dev) | 1.1.6 | 1.1.7 | 1.1.7 | |----------------------------------------|---------|--------|--------| @@ -25,3 +25,37 @@ $ bun outdated The `Update` column shows the version that would be installed if you ran `bun update [package]`. This version is the latest version that satisfies the version range specified in your `package.json`. The `Latest` column shows the latest version available from the registry. `bun update --latest [package]` will update to this version. + +Dependency names can be provided to filter the output (pattern matching is supported): + +```sh +$ bun outdated "@types/*" + +|------------------------------------------------| +| Package | Current | Update | Latest | +|--------------------|---------|--------|--------| +| @types/bun (dev) | 1.1.6 | 1.1.8 | 1.1.8 | +|--------------------|---------|--------|--------| +| @types/react (dev) | 18.3.3 | 18.3.4 | 18.3.4 | +|------------------------------------------------| +``` + +## `--filter` + +The `--filter` flag can be used to select workspaces to include in the output. Workspace names or paths can be used as patterns. + +```sh +$ bun outdated --filter +``` + +For example, to only show outdated dependencies for workspaces in the `./apps` directory: + +```sh +$ bun outdated --filter './apps/*' +``` + +If you want to do the same, but exclude the `./apps/api` workspace: + +```sh +$ bun outdated --filter './apps/*' --filter '!./apps/api' +``` diff --git a/docs/nav.ts b/docs/nav.ts index 93331f983fe5bb..aeff7140daf326 100644 --- a/docs/nav.ts +++ b/docs/nav.ts @@ -164,6 +164,9 @@ export default { page("cli/update", "`bun update`", { description: "Update your project's dependencies.", }), + page("cli/outdated", "`bun outdated`", { + description: "Check for outdated dependencies.", + }), page("cli/link", "`bun link`", { description: "Install local packages as dependencies in your project.", }), diff --git a/src/bun.js/api/glob.zig b/src/bun.js/api/glob.zig index 8e472cd3c406e3..6595574cf77e3c 100644 --- a/src/bun.js/api/glob.zig +++ b/src/bun.js/api/glob.zig @@ -480,7 +480,7 @@ pub fn match(this: *Glob, globalThis: *JSGlobalObject, callframe: *JSC.CallFrame break :codepoints codepoints.items[0..codepoints.items.len]; }; - return JSC.JSValue.jsBoolean(globImpl.matchImpl(codepoints, str.slice())); + return if (globImpl.matchImpl(codepoints, str.slice())) .true else .false; } pub fn convertUtf8(codepoints: *std.ArrayList(u32), pattern: []const u8) !void { diff --git a/src/cli/outdated_command.zig b/src/cli/outdated_command.zig index 2cbd5f9643add9..4787c8f1199e46 100644 --- a/src/cli/outdated_command.zig +++ b/src/cli/outdated_command.zig @@ -10,18 +10,23 @@ const PackageID = Install.PackageID; const DependencyID = Install.DependencyID; const Behavior = Install.Dependency.Behavior; const invalid_package_id = Install.invalid_package_id; +const Resolution = Install.Resolution; const string = bun.string; +const strings = bun.strings; +const PathBuffer = bun.PathBuffer; +const FileSystem = bun.fs.FileSystem; +const path = bun.path; +const glob = bun.glob; fn Table( - comptime num_columns: usize, comptime column_color: []const u8, comptime column_left_pad: usize, comptime column_right_pad: usize, comptime enable_ansi_colors: bool, ) type { return struct { - column_names: [num_columns][]const u8, - column_inside_lengths: [num_columns]usize, + column_names: []const []const u8, + column_inside_lengths: []const usize, pub fn topLeftSep(_: *const @This()) string { return if (enable_ansi_colors) "┌" else "|"; @@ -60,7 +65,7 @@ fn Table( return if (enable_ansi_colors) "│" else "|"; } - pub fn init(column_names_: [num_columns][]const u8, column_inside_lengths_: [num_columns]usize) @This() { + pub fn init(column_names_: []const []const u8, column_inside_lengths_: []const usize) @This() { return .{ .column_names = column_names_, .column_inside_lengths = column_inside_lengths_, @@ -116,7 +121,7 @@ pub const OutdatedCommand = struct { const cli = try PackageManager.CommandLineArguments.parse(ctx.allocator, .outdated); - const manager = PackageManager.init(ctx, cli, .outdated) catch |err| { + const manager, const original_cwd = PackageManager.init(ctx, cli, .outdated) catch |err| { if (!cli.silent) { if (err == error.MissingPackageJSON) { Output.errGeneric("missing package.json, nothing outdated", .{}); @@ -126,13 +131,14 @@ pub const OutdatedCommand = struct { Global.crash(); }; + defer ctx.allocator.free(original_cwd); return switch (manager.options.log_level) { - inline else => |log_level| outdated(ctx, manager, log_level), + inline else => |log_level| outdated(ctx, original_cwd, manager, log_level), }; } - fn outdated(ctx: Command.Context, manager: *PackageManager, comptime log_level: PackageManager.Options.LogLevel) !void { + fn outdated(ctx: Command.Context, original_cwd: string, manager: *PackageManager, comptime log_level: PackageManager.Options.LogLevel) !void { const load_lockfile_result = manager.lockfile.loadFromDisk( manager, manager.allocator, @@ -141,7 +147,7 @@ pub const OutdatedCommand = struct { true, ); - const lockfile = switch (load_lockfile_result) { + manager.lockfile = switch (load_lockfile_result) { .not_found => { if (log_level != .silent) { Output.errGeneric("missing lockfile, nothing outdated", .{}); @@ -180,27 +186,186 @@ pub const OutdatedCommand = struct { .ok => |ok| ok.lockfile, }; - manager.lockfile = lockfile; + switch (Output.enable_ansi_colors) { + inline else => |enable_ansi_colors| { + if (manager.options.filter_patterns.len > 0) { + const filters = manager.options.filter_patterns; + const workspace_pkg_ids = findMatchingWorkspaces( + bun.default_allocator, + original_cwd, + manager, + filters, + ) catch bun.outOfMemory(); + defer bun.default_allocator.free(workspace_pkg_ids); + + try updateManifestsIfNecessary(manager, log_level, workspace_pkg_ids); + try printOutdatedInfoTable(manager, workspace_pkg_ids, true, enable_ansi_colors); + } else { + // just the current workspace + const root_pkg_id = manager.root_package_id.get(manager.lockfile, manager.workspace_name_hash); + if (root_pkg_id == invalid_package_id) return; - const root_pkg_id = manager.root_package_id.get(lockfile, manager.workspace_name_hash); - if (root_pkg_id == invalid_package_id) return; - const root_pkg_deps = lockfile.packages.items(.dependencies)[root_pkg_id]; + try updateManifestsIfNecessary(manager, log_level, &.{root_pkg_id}); + try printOutdatedInfoTable(manager, &.{root_pkg_id}, false, enable_ansi_colors); + } + }, + } + } - try updateManifestsIfNecessary(manager, log_level, root_pkg_deps); + fn findMatchingWorkspaces( + allocator: std.mem.Allocator, + original_cwd: string, + manager: *PackageManager, + filters: []const string, + ) error{OutOfMemory}![]const PackageID { + const lockfile = manager.lockfile; + const packages = lockfile.packages.slice(); + const pkg_names = packages.items(.name); + const pkg_resolutions = packages.items(.resolution); + const string_buf = lockfile.buffers.string_bytes.items; + + var workspace_pkg_ids: std.ArrayListUnmanaged(PackageID) = .{}; + for (pkg_resolutions, 0..) |resolution, pkg_id| { + if (resolution.tag != .workspace and resolution.tag != .root) continue; + try workspace_pkg_ids.append(allocator, @intCast(pkg_id)); + } + + const converted_filters = converted_filters: { + const buf = try allocator.alloc(struct { []const u32, bool }, filters.len); + for (filters, buf) |filter, *converted| { + const is_path = filter.len > 0 and filter[0] == '.'; + + const joined_filter = if (is_path) + strings.withoutTrailingSlash(path.joinAbsString(original_cwd, &[_]string{filter}, .posix)) + else + filter; + + if (joined_filter.len == 0) { + converted.* = .{ &.{}, is_path }; + continue; + } - try switch (Output.enable_ansi_colors) { - inline else => |enable_ansi_colors| printOutdatedInfoTable(manager, root_pkg_deps, enable_ansi_colors), + const length = bun.simdutf.length.utf32.from.utf8.le(joined_filter); + const convert_buf = try allocator.alloc(u32, length); + + const convert_result = bun.simdutf.convert.utf8.to.utf32.with_errors.le(joined_filter, convert_buf); + if (!convert_result.isSuccessful()) { + // nothing would match + converted.* = .{ &.{}, false }; + continue; + } + + converted.* = .{ convert_buf[0..convert_result.count], is_path }; + } + break :converted_filters buf; }; + defer { + for (converted_filters) |converted| { + const filter, _ = converted; + allocator.free(filter); + } + allocator.free(converted_filters); + } + + // move all matched workspaces to front of array + var i: usize = 0; + while (i < workspace_pkg_ids.items.len) { + const workspace_pkg_id = workspace_pkg_ids.items[i]; + + const matched = matched: { + for (converted_filters) |converted| { + const filter, const is_path_filter = converted; + + if (is_path_filter) { + if (filter.len == 0) continue; + const res = pkg_resolutions[workspace_pkg_id]; + + const res_path = switch (res.tag) { + .workspace => res.value.workspace.slice(string_buf), + .root => FileSystem.instance.top_level_dir, + else => unreachable, + }; + + const abs_res_path = path.joinAbsString(FileSystem.instance.top_level_dir, &[_]string{res_path}, .posix); + + if (!glob.matchImpl(filter, strings.withoutTrailingSlash(abs_res_path))) { + break :matched false; + } + + continue; + } + + const name = pkg_names[workspace_pkg_id].slice(string_buf); + + if (!glob.matchImpl(filter, name)) { + break :matched false; + } + } + + break :matched true; + }; + + if (matched) { + i += 1; + } else { + _ = workspace_pkg_ids.swapRemove(i); + } + } + + return workspace_pkg_ids.items; } - fn printOutdatedInfoTable(manager: *PackageManager, root_pkg_deps: Lockfile.DependencySlice, comptime enable_ansi_colors: bool) !void { - var outdated_ids: std.ArrayListUnmanaged(struct { package_id: PackageID, dep_id: DependencyID }) = .{}; - defer outdated_ids.deinit(manager.allocator); + fn printOutdatedInfoTable( + manager: *PackageManager, + workspace_pkg_ids: []const PackageID, + was_filtered: bool, + comptime enable_ansi_colors: bool, + ) !void { + const package_patterns = package_patterns: { + const args = manager.options.positionals[1..]; + if (args.len == 0) break :package_patterns null; + + var at_least_one_greater_than_zero = false; + + const patterns_buf = bun.default_allocator.alloc([]const u32, args.len) catch bun.outOfMemory(); + for (args, patterns_buf) |arg, *converted| { + if (arg.len == 0) { + converted.* = &.{}; + continue; + } + + const length = bun.simdutf.length.utf32.from.utf8.le(arg); + const convert_buf = bun.default_allocator.alloc(u32, length) catch bun.outOfMemory(); + + const convert_result = bun.simdutf.convert.utf8.to.utf32.with_errors.le(arg, convert_buf); + if (!convert_result.isSuccessful()) { + converted.* = &.{}; + continue; + } + + converted.* = convert_buf[0..convert_result.count]; + at_least_one_greater_than_zero = at_least_one_greater_than_zero or converted.len > 0; + } + + // nothing will match + if (!at_least_one_greater_than_zero) return; + + break :package_patterns patterns_buf; + }; + defer { + if (package_patterns) |patterns| { + for (patterns) |pattern| { + bun.default_allocator.free(pattern); + } + bun.default_allocator.free(patterns); + } + } var max_name: usize = 0; var max_current: usize = 0; var max_update: usize = 0; var max_latest: usize = 0; + var max_workspace: usize = 0; const lockfile = manager.lockfile; const string_buf = lockfile.buffers.string_bytes.items; @@ -208,64 +373,95 @@ pub const OutdatedCommand = struct { const packages = lockfile.packages.slice(); const pkg_names = packages.items(.name); const pkg_resolutions = packages.items(.resolution); + const pkg_dependencies = packages.items(.dependencies); var version_buf = std.ArrayList(u8).init(bun.default_allocator); defer version_buf.deinit(); const version_writer = version_buf.writer(); - for (root_pkg_deps.begin()..root_pkg_deps.end()) |dep_id| { - const package_id = lockfile.buffers.resolutions.items[dep_id]; - if (package_id == invalid_package_id) continue; - const dep = lockfile.buffers.dependencies.items[dep_id]; - if (dep.version.tag != .npm and dep.version.tag != .dist_tag) continue; - const resolution = pkg_resolutions[package_id]; - if (resolution.tag != .npm) continue; - - const package_name = pkg_names[package_id].slice(string_buf); - var expired = false; - const manifest = manager.manifests.byNameAllowExpired( - manager.scopeForPackageName(package_name), - package_name, - &expired, - ) orelse continue; - - const latest = manifest.findByDistTag("latest") orelse continue; - - const update_version = if (dep.version.tag == .npm) - manifest.findBestVersion(dep.version.value.npm.version, string_buf) orelse continue - else - manifest.findByDistTag(dep.version.value.dist_tag.tag.slice(string_buf)) orelse continue; + var outdated_ids: std.ArrayListUnmanaged(struct { package_id: PackageID, dep_id: DependencyID, workspace_pkg_id: PackageID }) = .{}; + defer outdated_ids.deinit(manager.allocator); - if (resolution.value.npm.version.order(latest.version, string_buf, string_buf) != .lt) continue; + for (workspace_pkg_ids) |workspace_pkg_id| { + const pkg_deps = pkg_dependencies[workspace_pkg_id]; + for (pkg_deps.begin()..pkg_deps.end()) |dep_id| { + const package_id = lockfile.buffers.resolutions.items[dep_id]; + if (package_id == invalid_package_id) continue; + const dep = lockfile.buffers.dependencies.items[dep_id]; + if (dep.version.tag != .npm and dep.version.tag != .dist_tag) continue; + const resolution = pkg_resolutions[package_id]; + if (resolution.tag != .npm) continue; + + // package patterns match against dependency name (name in package.json) + if (package_patterns) |patterns| { + const match = match: { + for (patterns) |pattern| { + if (pattern.len == 0) continue; + if (!glob.matchImpl(pattern, dep.name.slice(string_buf))) { + break :match false; + } + } - const package_name_len = package_name.len + - if (dep.behavior.dev) - " (dev)".len - else if (dep.behavior.peer) - " (peer)".len - else if (dep.behavior.optional) - " (optional)".len - else - 0; + break :match true; + }; + if (!match) { + continue; + } + } - if (package_name_len > max_name) max_name = package_name_len; + const package_name = pkg_names[package_id].slice(string_buf); + var expired = false; + const manifest = manager.manifests.byNameAllowExpired( + manager.scopeForPackageName(package_name), + package_name, + &expired, + ) orelse continue; + + const latest = manifest.findByDistTag("latest") orelse continue; + + const update_version = if (dep.version.tag == .npm) + manifest.findBestVersion(dep.version.value.npm.version, string_buf) orelse continue + else + manifest.findByDistTag(dep.version.value.dist_tag.tag.slice(string_buf)) orelse continue; + + if (resolution.value.npm.version.order(latest.version, string_buf, manifest.string_buf) != .lt) continue; + + const package_name_len = package_name.len + + if (dep.behavior.dev) + " (dev)".len + else if (dep.behavior.peer) + " (peer)".len + else if (dep.behavior.optional) + " (optional)".len + else + 0; + + if (package_name_len > max_name) max_name = package_name_len; + + version_writer.print("{}", .{resolution.value.npm.version.fmt(string_buf)}) catch bun.outOfMemory(); + if (version_buf.items.len > max_current) max_current = version_buf.items.len; + version_buf.clearRetainingCapacity(); - version_writer.print("{}", .{resolution.value.npm.version.fmt(string_buf)}) catch bun.outOfMemory(); - if (version_buf.items.len > max_current) max_current = version_buf.items.len; - version_buf.clearRetainingCapacity(); + version_writer.print("{}", .{update_version.version.fmt(manifest.string_buf)}) catch bun.outOfMemory(); + if (version_buf.items.len > max_update) max_update = version_buf.items.len; + version_buf.clearRetainingCapacity(); - version_writer.print("{}", .{update_version.version.fmt(manifest.string_buf)}) catch bun.outOfMemory(); - if (version_buf.items.len > max_update) max_update = version_buf.items.len; - version_buf.clearRetainingCapacity(); + version_writer.print("{}", .{latest.version.fmt(manifest.string_buf)}) catch bun.outOfMemory(); + if (version_buf.items.len > max_latest) max_latest = version_buf.items.len; + version_buf.clearRetainingCapacity(); - version_writer.print("{}", .{latest.version.fmt(manifest.string_buf)}) catch bun.outOfMemory(); - if (version_buf.items.len > max_latest) max_latest = version_buf.items.len; - version_buf.clearRetainingCapacity(); + const workspace_name = pkg_names[workspace_pkg_id].slice(string_buf); + if (workspace_name.len > max_workspace) max_workspace = workspace_name.len; - outdated_ids.append( - bun.default_allocator, - .{ .package_id = package_id, .dep_id = @intCast(dep_id) }, - ) catch bun.outOfMemory(); + outdated_ids.append( + bun.default_allocator, + .{ + .package_id = package_id, + .dep_id = @intCast(dep_id), + .workspace_pkg_id = workspace_pkg_id, + }, + ) catch bun.outOfMemory(); + } } if (outdated_ids.items.len == 0) return; @@ -274,120 +470,156 @@ pub const OutdatedCommand = struct { const current_column_inside_length = @max("Current".len, max_current); const update_column_inside_length = @max("Update".len, max_update); const latest_column_inside_length = @max("Latest".len, max_latest); + const workspace_column_inside_length = @max("Workspace".len, max_workspace); const column_left_pad = 1; const column_right_pad = 1; - const table = Table(4, "blue", column_left_pad, column_right_pad, enable_ansi_colors).init( - [_][]const u8{ - "Packages", - "Current", - "Update", - "Latest", - }, - [_]usize{ - package_column_inside_length, - current_column_inside_length, - update_column_inside_length, - latest_column_inside_length, - }, + const table = Table("blue", column_left_pad, column_right_pad, enable_ansi_colors).init( + &if (was_filtered) + [_][]const u8{ + "Package", + "Current", + "Update", + "Latest", + "Workspace", + } + else + [_][]const u8{ + "Package", + "Current", + "Update", + "Latest", + }, + &if (was_filtered) + [_]usize{ + package_column_inside_length, + current_column_inside_length, + update_column_inside_length, + latest_column_inside_length, + workspace_column_inside_length, + } + else + [_]usize{ + package_column_inside_length, + current_column_inside_length, + update_column_inside_length, + latest_column_inside_length, + }, ); table.printTopLineSeparator(); table.printColumnNames(); - inline for ( - .{ - Behavior{ .normal = true }, - Behavior{ .dev = true }, - Behavior{ .peer = true }, - Behavior{ .optional = true }, - }, - ) |group_behavior| { - for (outdated_ids.items) |ids| { - const package_id = ids.package_id; - const dep_id = ids.dep_id; - - const dep = dependencies[dep_id]; - if (@as(u8, @bitCast(group_behavior)) & @as(u8, @bitCast(dep.behavior)) == 0) continue; - - const package_name = pkg_names[package_id].slice(string_buf); - const resolution = pkg_resolutions[package_id]; - - var expired = false; - const manifest = manager.manifests.byNameAllowExpired( - manager.scopeForPackageName(package_name), - package_name, - &expired, - ) orelse continue; - - const latest = manifest.findByDistTag("latest") orelse continue; - const update = if (dep.version.tag == .npm) - manifest.findBestVersion(dep.version.value.npm.version, string_buf) orelse continue - else - manifest.findByDistTag(dep.version.value.dist_tag.tag.slice(string_buf)) orelse continue; + for (workspace_pkg_ids) |workspace_pkg_id| { + inline for ( + .{ + Behavior{ .normal = true }, + Behavior{ .dev = true }, + Behavior{ .peer = true }, + Behavior{ .optional = true }, + }, + ) |group_behavior| { + for (outdated_ids.items) |ids| { + if (workspace_pkg_id != ids.workspace_pkg_id) continue; + const package_id = ids.package_id; + const dep_id = ids.dep_id; + + const dep = dependencies[dep_id]; + if (@as(u8, @bitCast(group_behavior)) & @as(u8, @bitCast(dep.behavior)) == 0) continue; + + const package_name = pkg_names[package_id].slice(string_buf); + const resolution = pkg_resolutions[package_id]; + + var expired = false; + const manifest = manager.manifests.byNameAllowExpired( + manager.scopeForPackageName(package_name), + package_name, + &expired, + ) orelse continue; + + const latest = manifest.findByDistTag("latest") orelse continue; + const update = if (dep.version.tag == .npm) + manifest.findBestVersion(dep.version.value.npm.version, string_buf) orelse continue + else + manifest.findByDistTag(dep.version.value.dist_tag.tag.slice(string_buf)) orelse continue; + + table.printLineSeparator(); + + { + // package name + const behavior_str = if (dep.behavior.dev) + " (dev)" + else if (dep.behavior.peer) + " (peer)" + else if (dep.behavior.optional) + " (optional)" + else + ""; + + Output.pretty("{s}", .{table.verticalEdge()}); + for (0..column_left_pad) |_| Output.pretty(" ", .{}); + + Output.pretty("{s}{s}", .{ package_name, behavior_str }); + for (package_name.len + behavior_str.len..package_column_inside_length + column_right_pad) |_| Output.pretty(" ", .{}); + } - table.printLineSeparator(); + { + // current version + Output.pretty("{s}", .{table.verticalEdge()}); + for (0..column_left_pad) |_| Output.pretty(" ", .{}); - { - // package name - const behavior_str = if (dep.behavior.dev) - " (dev)" - else if (dep.behavior.peer) - " (peer)" - else if (dep.behavior.optional) - " (optional)" - else - ""; + version_writer.print("{}", .{resolution.value.npm.version.fmt(string_buf)}) catch bun.outOfMemory(); + Output.pretty("{s}", .{version_buf.items}); + for (version_buf.items.len..current_column_inside_length + column_right_pad) |_| Output.pretty(" ", .{}); + version_buf.clearRetainingCapacity(); + } - Output.pretty("{s}", .{table.verticalEdge()}); - for (0..column_left_pad) |_| Output.pretty(" ", .{}); + { + // update version + Output.pretty("{s}", .{table.verticalEdge()}); + for (0..column_left_pad) |_| Output.pretty(" ", .{}); - Output.pretty("{s}{s}", .{ package_name, behavior_str }); - for (package_name.len + behavior_str.len..package_column_inside_length + column_right_pad) |_| Output.pretty(" ", .{}); - } + version_writer.print("{}", .{update.version.fmt(manifest.string_buf)}) catch bun.outOfMemory(); + Output.pretty("{s}", .{update.version.diffFmt(resolution.value.npm.version, manifest.string_buf, string_buf)}); + for (version_buf.items.len..update_column_inside_length + column_right_pad) |_| Output.pretty(" ", .{}); + version_buf.clearRetainingCapacity(); + } - { - // current version - Output.pretty("{s}", .{table.verticalEdge()}); - for (0..column_left_pad) |_| Output.pretty(" ", .{}); + { + // latest version + Output.pretty("{s}", .{table.verticalEdge()}); + for (0..column_left_pad) |_| Output.pretty(" ", .{}); - version_writer.print("{}", .{resolution.value.npm.version.fmt(string_buf)}) catch bun.outOfMemory(); - Output.pretty("{s}", .{version_buf.items}); - for (version_buf.items.len..current_column_inside_length + column_right_pad) |_| Output.pretty(" ", .{}); - version_buf.clearRetainingCapacity(); - } + version_writer.print("{}", .{latest.version.fmt(manifest.string_buf)}) catch bun.outOfMemory(); + Output.pretty("{s}", .{latest.version.diffFmt(resolution.value.npm.version, manifest.string_buf, string_buf)}); + for (version_buf.items.len..latest_column_inside_length + column_right_pad) |_| Output.pretty(" ", .{}); + version_buf.clearRetainingCapacity(); + } - { - // update version - Output.pretty("{s}", .{table.verticalEdge()}); - for (0..column_left_pad) |_| Output.pretty(" ", .{}); + if (was_filtered) { + Output.pretty("{s}", .{table.verticalEdge()}); + for (0..column_left_pad) |_| Output.pretty(" ", .{}); - version_writer.print("{}", .{update.version.fmt(manifest.string_buf)}) catch bun.outOfMemory(); - Output.pretty("{s}", .{update.version.diffFmt(resolution.value.npm.version, manifest.string_buf, string_buf)}); - for (version_buf.items.len..update_column_inside_length + column_right_pad) |_| Output.pretty(" ", .{}); - version_buf.clearRetainingCapacity(); - } + const workspace_name = pkg_names[workspace_pkg_id].slice(string_buf); + Output.pretty("{s}", .{workspace_name}); - { - // latest version - Output.pretty("{s}", .{table.verticalEdge()}); - for (0..column_left_pad) |_| Output.pretty(" ", .{}); + for (workspace_name.len..workspace_column_inside_length + column_right_pad) |_| Output.pretty(" ", .{}); + } - version_writer.print("{}", .{latest.version.fmt(manifest.string_buf)}) catch bun.outOfMemory(); - Output.pretty("{s}", .{latest.version.diffFmt(resolution.value.npm.version, manifest.string_buf, string_buf)}); - for (version_buf.items.len..latest_column_inside_length + column_right_pad) |_| Output.pretty(" ", .{}); - version_buf.clearRetainingCapacity(); + Output.pretty("{s}\n", .{table.verticalEdge()}); } - - Output.pretty("{s}\n", .{table.verticalEdge()}); } } table.printBottomLineSeparator(); } - fn updateManifestsIfNecessary(manager: *PackageManager, comptime log_level: PackageManager.Options.LogLevel, root_pkg_deps: Lockfile.DependencySlice) !void { + fn updateManifestsIfNecessary( + manager: *PackageManager, + comptime log_level: PackageManager.Options.LogLevel, + workspace_pkg_ids: []const PackageID, + ) !void { const lockfile = manager.lockfile; const resolutions = lockfile.buffers.resolutions.items; const dependencies = lockfile.buffers.dependencies.items; @@ -395,43 +627,67 @@ pub const OutdatedCommand = struct { const packages = lockfile.packages.slice(); const pkg_resolutions = packages.items(.resolution); const pkg_names = packages.items(.name); + const pkg_dependencies = packages.items(.dependencies); + + for (workspace_pkg_ids) |workspace_pkg_id| { + const pkg_deps = pkg_dependencies[workspace_pkg_id]; + for (pkg_deps.begin()..pkg_deps.end()) |dep_id| { + if (dep_id >= dependencies.len) continue; + const package_id = resolutions[dep_id]; + if (package_id == invalid_package_id) continue; + const dep = dependencies[dep_id]; + if (dep.version.tag != .npm and dep.version.tag != .dist_tag) continue; + const resolution: Install.Resolution = pkg_resolutions[package_id]; + if (resolution.tag != .npm) continue; - for (root_pkg_deps.begin()..root_pkg_deps.end()) |dep_id| { - if (dep_id >= dependencies.len) continue; - const package_id = resolutions[dep_id]; - if (package_id == invalid_package_id) continue; - const dep = dependencies[dep_id]; - if (dep.version.tag != .npm and dep.version.tag != .dist_tag) continue; - const resolution: Install.Resolution = pkg_resolutions[package_id]; - if (resolution.tag != .npm) continue; - - const package_name = pkg_names[package_id].slice(string_buf); - _ = manager.manifests.byName( - manager.scopeForPackageName(package_name), - package_name, - ) orelse { - const task_id = Install.Task.Id.forManifest(package_name); - if (manager.hasCreatedNetworkTask(task_id, dep.behavior.optional)) continue; - - manager.startProgressBarIfNone(); - - var task = manager.getNetworkTask(); - task.* = .{ - .package_manager = &PackageManager.instance, - .callback = undefined, - .task_id = task_id, - .allocator = manager.allocator, - }; - try task.forManifest( - package_name, - manager.allocator, + const package_name = pkg_names[package_id].slice(string_buf); + _ = manager.manifests.byName( manager.scopeForPackageName(package_name), - null, - dep.behavior.optional, - ); + package_name, + ) orelse { + const task_id = Install.Task.Id.forManifest(package_name); + if (manager.hasCreatedNetworkTask(task_id, dep.behavior.optional)) continue; + + manager.startProgressBarIfNone(); + + var task = manager.getNetworkTask(); + task.* = .{ + .package_manager = &PackageManager.instance, + .callback = undefined, + .task_id = task_id, + .allocator = manager.allocator, + }; + try task.forManifest( + package_name, + manager.allocator, + manager.scopeForPackageName(package_name), + null, + dep.behavior.optional, + ); + + manager.enqueueNetworkTask(task); + }; + } - manager.enqueueNetworkTask(task); - }; + manager.flushNetworkQueue(); + _ = manager.scheduleTasks(); + + if (manager.pendingTaskCount() > 1) { + try manager.runTasks( + *PackageManager, + manager, + .{ + .onExtract = {}, + .onResolve = {}, + .onPackageManifestError = {}, + .onPackageDownloadError = {}, + .progress_bar = true, + .manifests_only = true, + }, + true, + log_level, + ); + } } manager.flushNetworkQueue(); diff --git a/src/cli/package_manager_command.zig b/src/cli/package_manager_command.zig index 226d59d1c203b5..0645538225e546 100644 --- a/src/cli/package_manager_command.zig +++ b/src/cli/package_manager_command.zig @@ -62,7 +62,8 @@ pub const PackageManagerCommand = struct { lockfile_buffer[lockfile_.len] = 0; const lockfile = lockfile_buffer[0..lockfile_.len :0]; const cli = try PackageManager.CommandLineArguments.parse(ctx.allocator, .pm); - var pm = try PackageManager.init(ctx, cli, PackageManager.Subcommand.pm); + var pm, const cwd = try PackageManager.init(ctx, cli, PackageManager.Subcommand.pm); + defer ctx.allocator.free(cwd); const load_lockfile = pm.lockfile.loadFromDisk(pm, ctx.allocator, ctx.log, lockfile, true); handleLoadLockfileErrors(load_lockfile, pm); @@ -122,7 +123,7 @@ pub const PackageManagerCommand = struct { var args = try std.process.argsAlloc(ctx.allocator); args = args[1..]; const cli = try PackageManager.CommandLineArguments.parse(ctx.allocator, .pm); - var pm = PackageManager.init(ctx, cli, PackageManager.Subcommand.pm) catch |err| { + var pm, const cwd = PackageManager.init(ctx, cli, PackageManager.Subcommand.pm) catch |err| { if (err == error.MissingPackageJSON) { var cwd_buf: bun.PathBuffer = undefined; if (bun.getcwd(&cwd_buf)) |cwd| { @@ -135,6 +136,7 @@ pub const PackageManagerCommand = struct { } return err; }; + defer ctx.allocator.free(cwd); const subcommand = getSubcommand(&pm.options.positionals); if (pm.options.global) { diff --git a/src/install/install.zig b/src/install/install.zig index 6150c3a25f0d43..cb61f02200f155 100644 --- a/src/install/install.zig +++ b/src/install/install.zig @@ -6890,6 +6890,9 @@ pub const PackageManager = struct { }, } = .{ .nothing = .{} }, + filter_patterns: []const string = &.{}, + // json_output: bool = false, + max_retry_count: u16 = 5, min_simultaneous_requests: usize = 4, @@ -7219,6 +7222,9 @@ pub const PackageManager = struct { this.do.summary = false; } + this.filter_patterns = cli.filters; + // this.json_output = cli.json_output; + if (cli.no_cache) { this.enable.manifest_cache = false; this.enable.manifest_cache_control = false; @@ -8213,7 +8219,7 @@ pub const PackageManager = struct { ctx: Command.Context, cli: CommandLineArguments, subcommand: Subcommand, - ) !*PackageManager { + ) !struct { *PackageManager, string } { // assume that spawning a thread will take a lil so we do that asap HTTP.HTTPThread.init(); @@ -8241,6 +8247,7 @@ pub const PackageManager = struct { var original_package_json_path: stringZ = original_package_json_path_buf.items[0 .. top_level_dir_no_trailing_slash.len + "/package.json".len :0]; const original_cwd = strings.withoutSuffixComptime(original_package_json_path, std.fs.path.sep_str ++ "package.json"); + const original_cwd_clone = ctx.allocator.dupe(u8, original_cwd) catch bun.outOfMemory(); var workspace_names = Package.WorkspaceMap.init(ctx.allocator); var workspace_package_json_cache: WorkspacePackageJSONCache = .{ @@ -8541,7 +8548,10 @@ pub const PackageManager = struct { break :brk @truncate(@as(u64, @intCast(@max(std.time.timestamp(), 0)))); }; - return manager; + return .{ + manager, + original_cwd_clone, + }; } pub fn initWithRuntime( @@ -8739,7 +8749,7 @@ pub const PackageManager = struct { pub fn link(ctx: Command.Context) !void { const cli = try CommandLineArguments.parse(ctx.allocator, .link); - var manager = PackageManager.init(ctx, cli, .link) catch |err| brk: { + var manager, const original_cwd = PackageManager.init(ctx, cli, .link) catch |err| brk: { if (err == error.MissingPackageJSON) { try attemptToCreatePackageJSON(); break :brk try PackageManager.init(ctx, cli, .link); @@ -8747,6 +8757,7 @@ pub const PackageManager = struct { return err; }; + defer ctx.allocator.free(original_cwd); if (manager.options.shouldPrintCommandName()) { Output.prettyErrorln("bun link v" ++ Global.package_json_version_with_sha ++ "\n", .{}); @@ -8921,7 +8932,7 @@ pub const PackageManager = struct { pub fn unlink(ctx: Command.Context) !void { const cli = try PackageManager.CommandLineArguments.parse(ctx.allocator, .unlink); - var manager = PackageManager.init(ctx, cli, .unlink) catch |err| brk: { + var manager, const original_cwd = PackageManager.init(ctx, cli, .unlink) catch |err| brk: { if (err == error.MissingPackageJSON) { try attemptToCreatePackageJSON(); break :brk try PackageManager.init(ctx, cli, .unlink); @@ -8929,6 +8940,7 @@ pub const PackageManager = struct { return err; }; + defer ctx.allocator.free(original_cwd); if (manager.options.shouldPrintCommandName()) { Output.prettyErrorln("bun unlink v" ++ Global.package_json_version_with_sha ++ "\n", .{}); @@ -9124,6 +9136,12 @@ pub const PackageManager = struct { clap.parseParam("--patches-dir The directory to put the patch file") catch unreachable, }); + const outdated_params: []const ParamType = &(install_params_ ++ [_]ParamType{ + // clap.parseParam("--json Output outdated information in JSON format") catch unreachable, + clap.parseParam("--filter ... Display outdated dependencies for each matching workspace") catch unreachable, + clap.parseParam(" ... Package patterns to filter by") catch unreachable, + }); + pub const CommandLineArguments = struct { registry: string = "", cache_dir: string = "", @@ -9152,6 +9170,7 @@ pub const PackageManager = struct { no_summary: bool = false, latest: bool = false, // json_output: bool = false, + filters: []const string = &.{}, link_native_bins: []const string = &[_]string{}, @@ -9388,14 +9407,25 @@ pub const PackageManager = struct { const outro_text = \\Examples: + \\ Display outdated dependencies in the current workspace. \\ bun outdated \\ + \\ Use --filter to include more than one workspace. + \\ bun outdated --filter="*" + \\ bun outdated --filter="./app/*" + \\ bun outdated --filter="!frontend" + \\ + \\ Filter dependencies with name patterns. + \\ bun outdated jquery + \\ bun outdated "is-*" + \\ bun outdated "!is-even" + \\ ; Output.pretty("\n" ++ intro_text ++ "\n", .{}); Output.flush(); Output.pretty("\nFlags:", .{}); - clap.simpleHelp(PackageManager.install_params); + clap.simpleHelp(PackageManager.outdated_params); Output.pretty("\n\n" ++ outro_text ++ "\n", .{}); Output.flush(); }, @@ -9415,7 +9445,7 @@ pub const PackageManager = struct { .unlink => unlink_params, .patch => patch_params, .@"patch-commit" => patch_commit_params, - .outdated => install_params, + .outdated => outdated_params, }; var diag = clap.Diagnostic{}; @@ -9455,6 +9485,7 @@ pub const PackageManager = struct { // fake --dry-run, we don't actually resolve+clean the lockfile cli.dry_run = true; // cli.json_output = args.flag("--json"); + cli.filters = args.options("--filter"); } // link and unlink default to not saving, all others default to @@ -9715,7 +9746,7 @@ pub const PackageManager = struct { const cli = switch (subcommand) { inline else => |cmd| try PackageManager.CommandLineArguments.parse(ctx.allocator, cmd), }; - var manager = init(ctx, cli, subcommand) catch |err| brk: { + var manager, const original_cwd = init(ctx, cli, subcommand) catch |err| brk: { if (err == error.MissingPackageJSON) { switch (subcommand) { .update => { @@ -9739,6 +9770,7 @@ pub const PackageManager = struct { return err; }; + defer ctx.allocator.free(original_cwd); if (manager.options.shouldPrintCommandName()) { Output.prettyErrorln("bun {s} v" ++ Global.package_json_version_with_sha ++ "\n", .{@tagName(subcommand)}); @@ -11475,7 +11507,7 @@ pub const PackageManager = struct { pub fn install(ctx: Command.Context) !void { const cli = try CommandLineArguments.parse(ctx.allocator, .install); - var manager = try init(ctx, cli, .install); + var manager, _ = try init(ctx, cli, .install); // switch to `bun add ` if (manager.options.positionals.len > 1) { diff --git a/test/cli/install/registry/__snapshots__/bun-install-registry.test.ts.snap b/test/cli/install/registry/__snapshots__/bun-install-registry.test.ts.snap index c20750bd03f01d..0f33e6cd4f800d 100644 --- a/test/cli/install/registry/__snapshots__/bun-install-registry.test.ts.snap +++ b/test/cli/install/registry/__snapshots__/bun-install-registry.test.ts.snap @@ -48,7 +48,7 @@ what-bin@1.0.0: exports[`outdated normal dep, smaller than column title 1`] = ` "┌──────────┬─────────┬────────┬────────┐ -│ Packages │ Current │ Update │ Latest │ +│ Package │ Current │ Update │ Latest │ ├──────────┼─────────┼────────┼────────┤ │ no-deps │ 1.0.0 │ 1.0.0 │ 2.0.0 │ └──────────┴─────────┴────────┴────────┘ @@ -57,7 +57,7 @@ exports[`outdated normal dep, smaller than column title 1`] = ` exports[`outdated normal dep, larger than column title 1`] = ` "┌───────────────┬────────────────┬────────────────┬────────────────┐ -│ Packages │ Current │ Update │ Latest │ +│ Package │ Current │ Update │ Latest │ ├───────────────┼────────────────┼────────────────┼────────────────┤ │ prereleases-1 │ 1.0.0-future.1 │ 1.0.0-future.1 │ 1.0.0-future.4 │ └───────────────┴────────────────┴────────────────┴────────────────┘ @@ -66,7 +66,7 @@ exports[`outdated normal dep, larger than column title 1`] = ` exports[`outdated dev dep, smaller than column title 1`] = ` "┌───────────────┬─────────┬────────┬────────┐ -│ Packages │ Current │ Update │ Latest │ +│ Package │ Current │ Update │ Latest │ ├───────────────┼─────────┼────────┼────────┤ │ no-deps (dev) │ 1.0.0 │ 1.0.0 │ 2.0.0 │ └───────────────┴─────────┴────────┴────────┘ @@ -75,7 +75,7 @@ exports[`outdated dev dep, smaller than column title 1`] = ` exports[`outdated dev dep, larger than column title 1`] = ` "┌─────────────────────┬────────────────┬────────────────┬────────────────┐ -│ Packages │ Current │ Update │ Latest │ +│ Package │ Current │ Update │ Latest │ ├─────────────────────┼────────────────┼────────────────┼────────────────┤ │ prereleases-1 (dev) │ 1.0.0-future.1 │ 1.0.0-future.1 │ 1.0.0-future.4 │ └─────────────────────┴────────────────┴────────────────┴────────────────┘ @@ -84,7 +84,7 @@ exports[`outdated dev dep, larger than column title 1`] = ` exports[`outdated peer dep, smaller than column title 1`] = ` "┌────────────────┬─────────┬────────┬────────┐ -│ Packages │ Current │ Update │ Latest │ +│ Package │ Current │ Update │ Latest │ ├────────────────┼─────────┼────────┼────────┤ │ no-deps (peer) │ 1.0.0 │ 1.0.0 │ 2.0.0 │ └────────────────┴─────────┴────────┴────────┘ @@ -93,7 +93,7 @@ exports[`outdated peer dep, smaller than column title 1`] = ` exports[`outdated peer dep, larger than column title 1`] = ` "┌──────────────────────┬────────────────┬────────────────┬────────────────┐ -│ Packages │ Current │ Update │ Latest │ +│ Package │ Current │ Update │ Latest │ ├──────────────────────┼────────────────┼────────────────┼────────────────┤ │ prereleases-1 (peer) │ 1.0.0-future.1 │ 1.0.0-future.1 │ 1.0.0-future.4 │ └──────────────────────┴────────────────┴────────────────┴────────────────┘ @@ -102,7 +102,7 @@ exports[`outdated peer dep, larger than column title 1`] = ` exports[`outdated optional dep, smaller than column title 1`] = ` "┌────────────────────┬─────────┬────────┬────────┐ -│ Packages │ Current │ Update │ Latest │ +│ Package │ Current │ Update │ Latest │ ├────────────────────┼─────────┼────────┼────────┤ │ no-deps (optional) │ 1.0.0 │ 1.0.0 │ 2.0.0 │ └────────────────────┴─────────┴────────┴────────┘ @@ -111,7 +111,7 @@ exports[`outdated optional dep, smaller than column title 1`] = ` exports[`outdated optional dep, larger than column title 1`] = ` "┌──────────────────────────┬────────────────┬────────────────┬────────────────┐ -│ Packages │ Current │ Update │ Latest │ +│ Package │ Current │ Update │ Latest │ ├──────────────────────────┼────────────────┼────────────────┼────────────────┤ │ prereleases-1 (optional) │ 1.0.0-future.1 │ 1.0.0-future.1 │ 1.0.0-future.4 │ └──────────────────────────┴────────────────┴────────────────┴────────────────┘ @@ -120,7 +120,7 @@ exports[`outdated optional dep, larger than column title 1`] = ` exports[`outdated NO_COLOR works 1`] = ` "|--------------------------------------| -| Packages | Current | Update | Latest | +| Package | Current | Update | Latest | |----------|---------|--------|--------| | a-dep | 1.0.1 | 1.0.1 | 1.0.10 | |--------------------------------------| diff --git a/test/cli/install/registry/bun-install-registry.test.ts b/test/cli/install/registry/bun-install-registry.test.ts index d2af18d59e9c98..39cbd302bb004a 100644 --- a/test/cli/install/registry/bun-install-registry.test.ts +++ b/test/cli/install/registry/bun-install-registry.test.ts @@ -10068,6 +10068,103 @@ describe("outdated", () => { expect(await exited).toBe(0); }); + + async function setupWorkspace() { + await Promise.all([ + write( + join(packageDir, "package.json"), + JSON.stringify({ + name: "foo", + workspaces: ["packages/*"], + dependencies: { + "no-deps": "1.0.0", + }, + }), + ), + write( + join(packageDir, "packages", "pkg1", "package.json"), + JSON.stringify({ + name: "pkg1", + dependencies: { + "a-dep": "1.0.1", + }, + }), + ), + write( + join(packageDir, "packages", "pkg2", "package.json"), + JSON.stringify({ + name: "pkg2222222222222", + dependencies: { + "prereleases-1": "1.0.0-future.1", + }, + }), + ), + ]); + } + + async function runBunOutdated(env: any, cwd: string, ...args: string[]): Promise { + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "outdated", ...args], + cwd, + stdout: "pipe", + stderr: "pipe", + env, + }); + + const err = await Bun.readableStreamToText(stderr); + expect(err).not.toContain("error:"); + expect(err).not.toContain("panic:"); + const out = await Bun.readableStreamToText(stdout); + const exitCode = await exited; + expect(exitCode).toBe(0); + return out; + } + + test("--filter with workspace names and paths", async () => { + await setupWorkspace(); + await runBunInstall(env, packageDir); + + let out = await runBunOutdated(env, packageDir, "--filter", "*"); + expect(out).toContain("foo"); + expect(out).toContain("pkg1"); + expect(out).toContain("pkg2222222222222"); + + out = await runBunOutdated(env, join(packageDir, "packages", "pkg1"), "--filter", "./"); + expect(out).toContain("pkg1"); + expect(out).not.toContain("foo"); + expect(out).not.toContain("pkg2222222222222"); + + // in directory that isn't a workspace + out = await runBunOutdated(env, join(packageDir, "packages"), "--filter", "./*", "--filter", "!pkg1"); + expect(out).toContain("pkg2222222222222"); + expect(out).not.toContain("pkg1"); + expect(out).not.toContain("foo"); + + out = await runBunOutdated(env, join(packageDir, "packages", "pkg1"), "--filter", "../*"); + expect(out).not.toContain("foo"); + expect(out).toContain("pkg2222222222222"); + expect(out).toContain("pkg1"); + }); + + test("dependency pattern args", async () => { + await setupWorkspace(); + await runBunInstall(env, packageDir); + + let out = await runBunOutdated(env, packageDir, "no-deps", "--filter", "*"); + expect(out).toContain("no-deps"); + expect(out).not.toContain("a-dep"); + expect(out).not.toContain("prerelease-1"); + + out = await runBunOutdated(env, packageDir, "a-dep"); + expect(out).not.toContain("a-dep"); + expect(out).not.toContain("no-deps"); + expect(out).not.toContain("prerelease-1"); + + out = await runBunOutdated(env, packageDir, "*", "--filter", "*"); + expect(out).toContain("no-deps"); + expect(out).toContain("a-dep"); + expect(out).toContain("prereleases-1"); + }); }); // TODO: setup verdaccio to run across multiple test files, then move this and a few other describe