Compare commits

...

4 Commits

Author SHA1 Message Date
Claude Bot
ebbaaf10e1 Use full dependency type names in JSON output
- production instead of prod
- development instead of dev
- More descriptive and professional API
- Updated test expectations to match full names
- JSON format: {"package": {"current": "1.0.0", "update": "1.0.0", "latest": "1.0.1", "type": "production"}}

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-15 06:31:24 +00:00
Claude Bot
4846215a27 Use 'type' instead of 'dependencyType' in JSON output
- More concise and follows Bun's JSON naming conventions
- Consistent with bun audit which uses short field names (id, severity, etc.)
- Updated tests to expect 'type' field
- JSON format now: {"package": {"current": "1.0.0", "update": "1.0.0", "latest": "1.0.1", "type": "prod"}}

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-15 06:24:22 +00:00
Claude Bot
4108f770c0 Fix --json flag test to work properly in CI with Verdaccio
- Remove test.skip() and workarounds that would fail in working environment
- Use both prod and dev dependencies to test different dependency types
- Set proper expectations for test registry package versions
- Test validates JSON structure, dependency types, and version fields

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-15 06:16:50 +00:00
Claude Bot
beb1127be0 Add --json flag to bun outdated command
- Enable --json flag in outdated command parameters
- Add outdated to supportsJsonOutput() function
- Implement printOutdatedInfoJson() function with structured JSON output
- JSON format includes current, update, latest versions and dependency type
- Add test case for JSON output validation (currently skipped due to registry issues)
- JSON output matches format: {"package": {"current": "1.0.0", "update": "1.0.0", "latest": "1.0.1", "dependencyType": "prod"}}

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-15 03:09:00 +00:00
5 changed files with 271 additions and 6 deletions

View File

@@ -50,10 +50,10 @@ pub const OutdatedCommand = struct {
};
defer ctx.allocator.free(original_cwd);
try outdated(ctx, original_cwd, manager);
try outdated(ctx, original_cwd, manager, cli.json_output);
}
fn outdated(ctx: Command.Context, original_cwd: string, manager: *PackageManager) !void {
fn outdated(ctx: Command.Context, original_cwd: string, manager: *PackageManager, json_output: bool) !void {
const load_lockfile_result = manager.lockfile.loadFromCwd(
manager,
manager.allocator,
@@ -108,14 +108,22 @@ pub const OutdatedCommand = struct {
defer bun.default_allocator.free(workspace_pkg_ids);
try updateManifestsIfNecessary(manager, workspace_pkg_ids);
try printOutdatedInfoTable(manager, workspace_pkg_ids, true, enable_ansi_colors);
if (json_output) {
try printOutdatedInfoJson(manager, workspace_pkg_ids, true);
} else {
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, &.{root_pkg_id});
try printOutdatedInfoTable(manager, &.{root_pkg_id}, false, enable_ansi_colors);
if (json_output) {
try printOutdatedInfoJson(manager, &.{root_pkg_id}, false);
} else {
try printOutdatedInfoTable(manager, &.{root_pkg_id}, false, enable_ansi_colors);
}
}
},
}
@@ -222,6 +230,199 @@ pub const OutdatedCommand = struct {
return workspace_pkg_ids.items;
}
fn printOutdatedInfoJson(
manager: *PackageManager,
workspace_pkg_ids: []const PackageID,
was_filtered: 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| bun.default_allocator.free(patterns);
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 outdated_ids: std.ArrayListUnmanaged(struct {
package_id: PackageID,
dep_id: DependencyID,
workspace_pkg_id: PackageID
}) = .{};
defer outdated_ids.deinit(bun.default_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];
const resolved_version = resolveCatalogDependency(manager, dep) orelse continue;
if (resolved_version.tag != .npm and resolved_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;
if (resolution.value.npm.version.order(latest.version, string_buf, manifest.string_buf) != .lt) continue;
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) {
Output.print("{{}}\n", .{});
return;
}
var json_obj = std.ArrayList(u8).init(bun.default_allocator);
defer json_obj.deinit();
var writer = json_obj.writer();
try writer.writeAll("{\n");
var first_package = true;
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 resolved_version = resolveCatalogDependency(manager, dep) orelse continue;
const update = if (resolved_version.tag == .npm)
manifest.findBestVersion(resolved_version.value.npm.version, string_buf) orelse continue
else
manifest.findByDistTag(resolved_version.value.dist_tag.tag.slice(string_buf)) orelse continue;
if (!first_package) {
try writer.writeAll(",\n");
}
first_package = false;
const dependency_type = if (dep.behavior.dev)
"development"
else if (dep.behavior.peer)
"peer"
else if (dep.behavior.optional)
"optional"
else
"production";
try writer.print(" \"{s}\": {{\n", .{package_name});
try writer.print(" \"current\": \"{}\",\n", .{resolution.value.npm.version.fmt(string_buf)});
try writer.print(" \"update\": \"{}\",\n", .{update.version.fmt(manifest.string_buf)});
try writer.print(" \"latest\": \"{}\",\n", .{latest.version.fmt(manifest.string_buf)});
try writer.print(" \"type\": \"{s}\"", .{dependency_type});
if (was_filtered) {
const workspace_name = pkg_names[workspace_pkg_id].slice(string_buf);
try writer.print(",\n \"workspace\": \"{s}\"", .{workspace_name});
}
try writer.writeAll("\n }");
}
}
}
try writer.writeAll("\n}\n");
Output.print("{s}", .{json_obj.items});
Output.flush();
}
fn printOutdatedInfoTable(
manager: *PackageManager,
workspace_pkg_ids: []const PackageID,

View File

@@ -186,6 +186,7 @@ pub const Subcommand = enum {
.audit,
.pm,
.info,
.outdated,
=> true,
else => false,
};

View File

@@ -117,7 +117,7 @@ const patch_commit_params: []const ParamType = &(shared_params ++ [_]ParamType{
});
const outdated_params: []const ParamType = &(shared_params ++ [_]ParamType{
// clap.parseParam("--json Output outdated information in JSON format") catch unreachable,
clap.parseParam("--json Output outdated information in JSON format") catch unreachable,
clap.parseParam("-F, --filter <STR>... Display outdated dependencies for each matching workspace") catch unreachable,
clap.parseParam("<POS> ... Package patterns to filter by") catch unreachable,
});

View File

@@ -82,7 +82,7 @@
"tsyringe": "4.8.0",
"type-graphql": "2.0.0-rc.2",
"typeorm": "0.3.20",
"typescript": "^5.8.3",
"typescript": "5.8.3",
"undici": "5.20.0",
"unzipper": "0.12.3",
"uuid": "11.1.0",

View File

@@ -8484,6 +8484,69 @@ describe("outdated", () => {
expect(out).toContain("no-deps");
expect(out).toContain("a-dep");
});
test("--json flag", async () => {
await write(
packageJson,
JSON.stringify({
name: "json-test",
dependencies: {
"no-deps": "1.0.0", // outdated - latest is 1.0.5 in test registry
},
devDependencies: {
"a-dep": "1.0.1", // outdated - latest is 1.0.3 in test registry
},
}),
);
await runBunInstall(env, packageDir);
// Test JSON output
const { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "outdated", "--json"],
cwd: packageDir,
stdout: "pipe",
stderr: "pipe",
env,
});
const err = await stderr.text();
expect(err).not.toContain("error:");
expect(err).not.toContain("panic:");
const out = await stdout.text();
expect(await exited).toBe(0);
// Skip version line and parse JSON
const jsonStr = out.slice(out.indexOf("\n") + 1);
expect(() => JSON.parse(jsonStr)).not.toThrow();
const parsed = JSON.parse(jsonStr);
expect(parsed).toBeDefined();
expect(typeof parsed).toBe("object");
// Should contain no-deps with production dependency type
expect(parsed["no-deps"]).toBeDefined();
expect(parsed["no-deps"].current).toBe("1.0.0");
expect(parsed["no-deps"].latest).toBe("1.0.5");
expect(parsed["no-deps"].type).toBe("production");
// Should contain a-dep with development dependency type
expect(parsed["a-dep"]).toBeDefined();
expect(parsed["a-dep"].current).toBe("1.0.1");
expect(parsed["a-dep"].latest).toBe("1.0.3");
expect(parsed["a-dep"].type).toBe("development");
// Verify JSON structure for all packages
for (const pkgName of Object.keys(parsed)) {
const pkg = parsed[pkgName];
expect(pkg).toHaveProperty("current");
expect(pkg).toHaveProperty("update");
expect(pkg).toHaveProperty("latest");
expect(pkg).toHaveProperty("type");
expect(["production", "development", "peer", "optional"]).toContain(pkg.type);
}
});
});
// TODO: setup registry to run across multiple test files, then move this and a few other describe