const std = @import("std"); const bun = @import("root").bun; const Global = bun.Global; const Output = bun.Output; const Command = bun.CLI.Command; const Install = bun.install; const PackageManager = Install.PackageManager; const Lockfile = Install.Lockfile; 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; const Table = bun.fmt.Table; const WorkspaceFilter = PackageManager.WorkspaceFilter; const OOM = bun.OOM; pub const OutdatedCommand = struct { pub fn exec(ctx: Command.Context) !void { Output.prettyln("bun outdated v" ++ Global.package_json_version_with_sha ++ "", .{}); Output.flush(); const cli = try PackageManager.CommandLineArguments.parse(ctx.allocator, .outdated); 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", .{}); } Output.errGeneric("failed to initialize bun install: {s}", .{@errorName(err)}); } Global.crash(); }; defer ctx.allocator.free(original_cwd); return switch (manager.options.log_level) { inline else => |log_level| outdated(ctx, original_cwd, manager, log_level), }; } fn outdated(ctx: Command.Context, original_cwd: string, manager: *PackageManager, comptime log_level: PackageManager.Options.LogLevel) !void { const load_lockfile_result = manager.lockfile.loadFromCwd( manager, manager.allocator, manager.log, true, ); manager.lockfile = switch (load_lockfile_result) { .not_found => { if (log_level != .silent) { Output.errGeneric("missing lockfile, nothing outdated", .{}); } Global.crash(); }, .err => |cause| { if (log_level != .silent) { switch (cause.step) { .open_file => Output.errGeneric("failed to open lockfile: {s}", .{ @errorName(cause.value), }), .parse_file => Output.errGeneric("failed to parse lockfile: {s}", .{ @errorName(cause.value), }), .read_file => Output.errGeneric("failed to read lockfile: {s}", .{ @errorName(cause.value), }), .migrating => Output.errGeneric("failed to migrate lockfile: {s}", .{ @errorName(cause.value), }), } if (ctx.log.hasErrors()) { try manager.log.print(Output.errorWriter()); } } Global.crash(); }, .ok => |ok| ok.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; try updateManifestsIfNecessary(manager, log_level, &.{root_pkg_id}); try printOutdatedInfoTable(manager, &.{root_pkg_id}, false, enable_ansi_colors); } }, } } // TODO: use in `bun pack, publish, run, ...` const FilterType = union(enum) { all, name: []const u8, path: []const u8, pub fn init(pattern: []const u8, is_path: bool) @This() { return if (is_path) .{ .path = pattern, } else .{ .name = pattern, }; } /// *NOTE*: Currently this does nothing since name and path are not /// allocated. pub fn deinit(_: @This(), _: std.mem.Allocator) void {} }; fn findMatchingWorkspaces( allocator: std.mem.Allocator, original_cwd: string, manager: *PackageManager, filters: []const string, ) OOM![]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(WorkspaceFilter, filters.len); var path_buf: bun.PathBuffer = undefined; for (filters, buf) |filter, *converted| { converted.* = try WorkspaceFilter.init(allocator, filter, original_cwd, &path_buf); } break :converted_filters buf; }; defer { for (converted_filters) |filter| { filter.deinit(allocator); } 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) |filter| { switch (filter) { .path => |pattern| { if (pattern.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.walk.matchImpl(allocator, pattern, strings.withoutTrailingSlash(abs_res_path)).matches()) { break :matched false; } }, .name => |pattern| { const name = pkg_names[workspace_pkg_id].slice(string_buf); if (!glob.walk.matchImpl(allocator, pattern, name).matches()) { break :matched false; } }, .all => {}, } } break :matched true; }; if (matched) { i += 1; } else { _ = workspace_pkg_ids.swapRemove(i); } } return workspace_pkg_ids.items; } 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(FilterType, args.len) catch bun.outOfMemory(); for (args, patterns_buf) |arg, *converted| { if (arg.len == 0) { converted.* = FilterType.init(&.{}, false); continue; } if ((arg.len == 1 and arg[0] == '*') or strings.eqlComptime(arg, "**")) { converted.* = .all; at_least_one_greater_than_zero = true; continue; } converted.* = FilterType.init(arg, false); at_least_one_greater_than_zero = at_least_one_greater_than_zero or arg.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| { pattern.deinit(bun.default_allocator); } 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; const dependencies = lockfile.buffers.dependencies.items; 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(); var outdated_ids: std.ArrayListUnmanaged(struct { package_id: PackageID, dep_id: DependencyID, workspace_pkg_id: PackageID }) = .{}; defer outdated_ids.deinit(manager.allocator); 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| { switch (pattern) { .path => unreachable, .name => |name_pattern| { if (name_pattern.len == 0) continue; if (!glob.walk.matchImpl(bun.default_allocator, name_pattern, dep.name.slice(string_buf)).matches()) { break :match false; } }, .all => {}, } } break :match true; }; if (!match) { continue; } } const package_name = pkg_names[package_id].slice(string_buf); var expired = false; const manifest = manager.manifests.byNameAllowExpired( manager, manager.scopeForPackageName(package_name), package_name, &expired, .load_from_memory_fallback_to_disk, ) 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("{}", .{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(); 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), .workspace_pkg_id = workspace_pkg_id, }, ) catch bun.outOfMemory(); } } if (outdated_ids.items.len == 0) return; const package_column_inside_length = @max("Packages".len, max_name); 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("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(); for (workspace_pkg_ids) |workspace_pkg_id| { inline for ([_]Behavior{ .{ .prod = true }, .{ .dev = true }, .{ .peer = true }, .{ .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 (!dep.behavior.includes(group_behavior)) 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, manager.scopeForPackageName(package_name), package_name, &expired, .load_from_memory_fallback_to_disk, ) 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.symbols.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(" ", .{}); } { // current version Output.pretty("{s}", .{table.symbols.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(); } { // update version Output.pretty("{s}", .{table.symbols.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(); } { // latest version Output.pretty("{s}", .{table.symbols.verticalEdge()}); for (0..column_left_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(); } if (was_filtered) { Output.pretty("{s}", .{table.symbols.verticalEdge()}); for (0..column_left_pad) |_| Output.pretty(" ", .{}); const workspace_name = pkg_names[workspace_pkg_id].slice(string_buf); Output.pretty("{s}", .{workspace_name}); for (workspace_name.len..workspace_column_inside_length + column_right_pad) |_| Output.pretty(" ", .{}); } Output.pretty("{s}\n", .{table.symbols.verticalEdge()}); } } } table.printBottomLineSeparator(); } 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; const string_buf = lockfile.buffers.string_bytes.items; 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; const package_name = pkg_names[package_id].slice(string_buf); _ = manager.manifests.byName( manager, manager.scopeForPackageName(package_name), package_name, .load_from_memory_fallback_to_disk, ) 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 = manager, .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.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(); _ = manager.scheduleTasks(); const RunClosure = struct { manager: *PackageManager, err: ?anyerror = null, pub fn isDone(closure: *@This()) bool { if (closure.manager.pendingTaskCount() > 0) { closure.manager.runTasks( *PackageManager, closure.manager, .{ .onExtract = {}, .onResolve = {}, .onPackageManifestError = {}, .onPackageDownloadError = {}, .progress_bar = true, .manifests_only = true, }, true, log_level, ) catch |err| { closure.err = err; return true; }; } return closure.manager.pendingTaskCount() == 0; } }; var run_closure: RunClosure = .{ .manager = manager }; manager.sleepUntil(&run_closure, &RunClosure.isDone); if (comptime log_level.showProgress()) { manager.endProgressBar(); Output.flush(); } if (run_closure.err) |err| { return err; } } };