mirror of
https://github.com/oven-sh/bun
synced 2026-02-16 22:01:47 +00:00
Compare commits
11 Commits
ali/inspec
...
codex/impl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a18ffd0510 | ||
|
|
a4aa1abaf6 | ||
|
|
cc86e6574f | ||
|
|
d9ba778e74 | ||
|
|
8aa06693bc | ||
|
|
db9c1257d1 | ||
|
|
04f9961f16 | ||
|
|
e87432d6de | ||
|
|
edef7e64bf | ||
|
|
de6cd953f5 | ||
|
|
3134fbd981 |
@@ -266,6 +266,7 @@ src/cli/test/Scanner.zig
|
||||
src/cli/unlink_command.zig
|
||||
src/cli/update_command.zig
|
||||
src/cli/upgrade_command.zig
|
||||
src/cli/why_command.zig
|
||||
src/codegen/process_windows_translate_c.zig
|
||||
src/compile_target.zig
|
||||
src/comptime_string_map.zig
|
||||
|
||||
@@ -20,6 +20,7 @@ const TrustCommand = @import("./pm_trusted_command.zig").TrustCommand;
|
||||
const DefaultTrustedCommand = @import("./pm_trusted_command.zig").DefaultTrustedCommand;
|
||||
const Environment = bun.Environment;
|
||||
pub const PackCommand = @import("./pack_command.zig").PackCommand;
|
||||
const WhyCommand = @import("./why_command.zig").WhyCommand;
|
||||
pub const AuditCommand = @import("./audit_command.zig").AuditCommand;
|
||||
const Npm = Install.Npm;
|
||||
const PmViewCommand = @import("./pm_view_command.zig");
|
||||
@@ -122,6 +123,7 @@ pub const PackageManagerCommand = struct {
|
||||
\\ <d>└<r> <cyan>-g<r> print the <b>global<r> path to bin folder
|
||||
\\ <b><green>bun pm<r> <blue>ls<r> list the dependency tree according to the current lockfile
|
||||
\\ <d>└<r> <cyan>--all<r> list the entire dependency tree according to the current lockfile
|
||||
\\ <b><green>bun pm<r> <blue>why<r> <d>pkg<r> explain why a package is installed
|
||||
\\ <b><green>bun pm<r> <blue>whoami<r> print the current npm username
|
||||
\\ <b><green>bun pm<r> <blue>view<r> <d>name[@version]<r> view package metadata from the registry
|
||||
\\ <b><green>bun pm<r> <blue>hash<r> generate & print the hash of the current lockfile
|
||||
@@ -317,6 +319,18 @@ pub const PackageManagerCommand = struct {
|
||||
} else if (strings.eqlComptime(subcommand, "trust")) {
|
||||
try TrustCommand.exec(ctx, pm, args);
|
||||
Global.exit(0);
|
||||
} else if (strings.eqlComptime(subcommand, "why")) {
|
||||
if (pm.options.positionals.len <= 1) {
|
||||
Output.prettyErrorln("<r><red>error<r>: missing package name", .{});
|
||||
Global.exit(1);
|
||||
}
|
||||
const load_lockfile = pm.lockfile.loadFromCwd(pm, ctx.allocator, ctx.log, true);
|
||||
handleLoadLockfileErrors(load_lockfile, pm);
|
||||
const lockfile = load_lockfile.ok.lockfile;
|
||||
|
||||
const name = pm.options.positionals[1];
|
||||
try WhyCommand.exec(lockfile, pm, name, pm.options.json_output);
|
||||
Global.exit(0);
|
||||
} else if (strings.eqlComptime(subcommand, "ls")) {
|
||||
const load_lockfile = pm.lockfile.loadFromCwd(pm, ctx.allocator, ctx.log, true);
|
||||
handleLoadLockfileErrors(load_lockfile, pm);
|
||||
@@ -568,3 +582,10 @@ fn printNodeModulesFolderStructure(
|
||||
Output.prettyln("{s}<d>@{s}<r>", .{ package_name, package_version });
|
||||
}
|
||||
}
|
||||
|
||||
fn behaviorPrefix(behavior: Dependency.Behavior) []const u8 {
|
||||
if (behavior.isDev()) return "dev ";
|
||||
if (behavior.isOptional()) return "optional ";
|
||||
if (behavior.isPeer()) return "peer ";
|
||||
return "";
|
||||
}
|
||||
|
||||
308
src/cli/why_command.zig
Normal file
308
src/cli/why_command.zig
Normal file
@@ -0,0 +1,308 @@
|
||||
const std = @import("std");
|
||||
const bun = @import("bun");
|
||||
const Global = bun.Global;
|
||||
const Output = bun.Output;
|
||||
const strings = bun.strings;
|
||||
const Install = @import("../install/install.zig");
|
||||
const PackageID = Install.PackageID;
|
||||
const DependencyID = Install.DependencyID;
|
||||
const PackageManager = Install.PackageManager;
|
||||
const Lockfile = @import("../install/lockfile.zig");
|
||||
const Dependency = @import("../install/dependency.zig");
|
||||
|
||||
const DependencyPath = struct {
|
||||
packages: std.ArrayList(PackageID),
|
||||
depth: usize,
|
||||
|
||||
fn init(allocator: std.mem.Allocator) @This() {
|
||||
return .{
|
||||
.packages = std.ArrayList(PackageID).init(allocator),
|
||||
.depth = 0,
|
||||
};
|
||||
}
|
||||
|
||||
fn deinit(self: *@This()) void {
|
||||
self.packages.deinit();
|
||||
}
|
||||
};
|
||||
|
||||
fn findDependencyPaths(lockfile: *Lockfile, target_pkg_id: PackageID, allocator: std.mem.Allocator) !std.ArrayList(DependencyPath) {
|
||||
const resolutions = lockfile.buffers.resolutions.items;
|
||||
const pkgs = lockfile.packages.slice();
|
||||
|
||||
var paths = std.ArrayList(DependencyPath).init(allocator);
|
||||
|
||||
const Queue = std.fifo.LinearFifo(DependencyPath, .Dynamic);
|
||||
// BFS to find all paths from root to target
|
||||
var queue: Queue = Queue.init(allocator);
|
||||
defer queue.deinit();
|
||||
|
||||
// Start with root package
|
||||
var root_path = DependencyPath.init(allocator);
|
||||
// Assuming 0 is the root package ID as per common convention in this codebase.
|
||||
// If Lockfile.root_package_id or similar exists, it should be used.
|
||||
try root_path.packages.append(0);
|
||||
try queue.writeItem(root_path);
|
||||
|
||||
while (queue.readItem()) |current_path_| {
|
||||
var current_path = current_path_;
|
||||
const current_pkg_id = current_path.packages.items[current_path.packages.items.len - 1];
|
||||
|
||||
// Iterate over dependencies of the current package
|
||||
const pkg_deps_slice = pkgs.items(.dependencies)[current_pkg_id];
|
||||
var dep_idx: usize = 0;
|
||||
while (dep_idx < pkg_deps_slice.len) : (dep_idx += 1) {
|
||||
const dep_id = @as(DependencyID, @truncate(pkg_deps_slice.off + dep_idx));
|
||||
const resolved_pkg_id = resolutions[dep_id];
|
||||
|
||||
if (resolved_pkg_id == target_pkg_id) {
|
||||
// Found a path to the target package
|
||||
var complete_path = DependencyPath.init(allocator);
|
||||
try complete_path.packages.appendSlice(current_path.packages.items);
|
||||
try complete_path.packages.append(target_pkg_id);
|
||||
complete_path.depth = complete_path.packages.items.len - 1; // Depth is number of edges
|
||||
try paths.append(complete_path);
|
||||
// Continue checking other dependencies of current_pkg_id,
|
||||
// as there might be multiple ways current_pkg_id depends on target_pkg_id
|
||||
// or other dependencies to explore.
|
||||
// The original code had `continue` here which would skip to the next dependency.
|
||||
} else if (resolved_pkg_id < lockfile.packages.len) { // Check if resolved_pkg_id is a valid package index
|
||||
// Continue exploring if this dependency leads to another package (not the target)
|
||||
|
||||
// Avoid cycles within the current path
|
||||
var has_cycle = false;
|
||||
for (current_path.packages.items) |visited_pkg_in_path| {
|
||||
if (visited_pkg_in_path == resolved_pkg_id) {
|
||||
has_cycle = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Limit search depth to avoid excessively long paths or performance issues
|
||||
// Max depth of 9 means paths of length 10 (10 packages, 9 edges)
|
||||
if (!has_cycle and current_path.packages.items.len < 10) {
|
||||
var new_path = DependencyPath.init(allocator);
|
||||
try new_path.packages.appendSlice(current_path.packages.items);
|
||||
try new_path.packages.append(resolved_pkg_id);
|
||||
try queue.writeItem(new_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
current_path.deinit();
|
||||
}
|
||||
|
||||
return paths;
|
||||
}
|
||||
|
||||
pub const WhyCommand = struct {
|
||||
pub fn exec(lockfile: *Lockfile, _: *PackageManager, query: []const u8, json_output: bool) !void {
|
||||
const string_bytes = lockfile.buffers.string_bytes.items;
|
||||
const pkgs = lockfile.packages.slice();
|
||||
const pkg_names = pkgs.items(.name);
|
||||
const pkg_resolutions = pkgs.items(.resolution);
|
||||
|
||||
var found = false;
|
||||
|
||||
var matching_package_ids = std.ArrayList(PackageID).init(lockfile.allocator);
|
||||
defer matching_package_ids.deinit();
|
||||
|
||||
for (pkg_names, 0..) |pkg_name, pkg_idx| {
|
||||
const name = pkg_name.slice(string_bytes);
|
||||
if (strings.eqlLong(name, query, true)) {
|
||||
try matching_package_ids.append(@as(PackageID, @truncate(pkg_idx)));
|
||||
}
|
||||
}
|
||||
|
||||
if (matching_package_ids.items.len == 0) {
|
||||
if (json_output) {
|
||||
Output.print("{{\"error\": \"package not found\"}}", .{});
|
||||
} else {
|
||||
Output.errGeneric("Package <b>{}<r> not found", .{bun.fmt.quote(query)});
|
||||
}
|
||||
Global.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!json_output) {
|
||||
Output.prettyln("Legend: production dependency, <magenta>optional only<r>, <yellow>dev only<r>", .{});
|
||||
Output.prettyln("", .{});
|
||||
|
||||
if (lockfile.rootPackage()) |root_pkg| {
|
||||
Output.prettyln("<b>{s}@{s}<r> {s}", .{ root_pkg.name.slice(string_bytes), root_pkg.resolution.fmt(string_bytes, .auto), bun.fs.FileSystem.instance.top_level_dir });
|
||||
} else {
|
||||
Output.prettyln("<b>{s}@{s}<r> {s}", .{ std.fs.path.basename(bun.fs.FileSystem.instance.top_level_dir), "", bun.fs.FileSystem.instance.top_level_dir });
|
||||
}
|
||||
}
|
||||
|
||||
for (matching_package_ids.items) |target_pkg_id| {
|
||||
found = true;
|
||||
|
||||
const target_name = pkg_names[target_pkg_id].slice(string_bytes);
|
||||
|
||||
// Find all dependency paths
|
||||
var paths = try findDependencyPaths(lockfile, target_pkg_id, lockfile.allocator);
|
||||
defer {
|
||||
for (paths.items) |*path| {
|
||||
path.deinit();
|
||||
}
|
||||
paths.deinit();
|
||||
}
|
||||
|
||||
if (json_output) {
|
||||
// JSON output with proper dependency chain
|
||||
Output.println("{{", .{});
|
||||
Output.println(" \"dependencies\": [", .{});
|
||||
Output.println(" {{", .{});
|
||||
Output.println(" \"name\": \"{s}\",", .{target_name});
|
||||
Output.println(" \"version\": \"{s}\",", .{pkg_resolutions[target_pkg_id].fmt(string_bytes, .auto)});
|
||||
Output.println(" \"hops\": {d},", .{if (paths.items.len > 0) paths.items[0].depth else 0});
|
||||
Output.println(" \"dependencyChain\": [", .{});
|
||||
|
||||
if (paths.items.len > 0) {
|
||||
// Sort paths by depth first
|
||||
std.sort.insertion(DependencyPath, paths.items, {}, struct {
|
||||
fn lessThan(_: void, a: DependencyPath, b: DependencyPath) bool {
|
||||
return a.depth < b.depth;
|
||||
}
|
||||
}.lessThan);
|
||||
|
||||
const first_path = paths.items[0];
|
||||
for (first_path.packages.items, 0..) |pkg_id, step| {
|
||||
const pkg_name = pkg_names[pkg_id].slice(string_bytes);
|
||||
const pkg_version_str = pkg_resolutions[pkg_id].fmt(string_bytes, .auto);
|
||||
|
||||
const from_name = if (step == 0) "root" else pkg_name;
|
||||
const comma = if (step == first_path.packages.items.len - 1) "" else ",";
|
||||
Output.println(" {{\"from\": \"{s}\", \"version\": \"{s}\"}}{s}", .{ from_name, pkg_version_str, comma });
|
||||
}
|
||||
}
|
||||
|
||||
Output.println(" ]", .{});
|
||||
Output.println(" }}", .{});
|
||||
Output.println(" ]", .{});
|
||||
Output.println("}}", .{});
|
||||
} else {
|
||||
// pnpm-style output
|
||||
if (paths.items.len == 0) {
|
||||
Output.errGeneric("Dependency <b>{}<r> not found", .{bun.fmt.quote(target_name)});
|
||||
Global.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort paths by depth (shortest first) and group by direct dependency
|
||||
const SortContext = struct {
|
||||
pkg_names: @TypeOf(pkg_names),
|
||||
string_bytes: @TypeOf(string_bytes),
|
||||
|
||||
fn lessThan(ctx: @This(), a: DependencyPath, b: DependencyPath) bool {
|
||||
if (a.depth < b.depth) return true;
|
||||
if (a.depth > b.depth) return false;
|
||||
|
||||
// If same depth, sort by first dependency name
|
||||
if (a.packages.items.len > 1 and b.packages.items.len > 1) {
|
||||
const a_name = ctx.pkg_names[a.packages.items[1]].slice(ctx.string_bytes);
|
||||
const b_name = ctx.pkg_names[b.packages.items[1]].slice(ctx.string_bytes);
|
||||
return strings.order(a_name, b_name) == .lt;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
std.sort.insertion(DependencyPath, paths.items, SortContext{
|
||||
.pkg_names = pkg_names,
|
||||
.string_bytes = string_bytes,
|
||||
}, SortContext.lessThan);
|
||||
|
||||
// Build a complete dependency tree showing all paths to target
|
||||
const dependencies = lockfile.buffers.dependencies.items;
|
||||
const resolutions = lockfile.buffers.resolutions.items;
|
||||
|
||||
Output.prettyln("", .{});
|
||||
Output.prettyln("dependencies:", .{});
|
||||
|
||||
// Group paths by their unique package chains
|
||||
for (paths.items) |path| {
|
||||
if (path.packages.items.len < 2) continue;
|
||||
|
||||
// Build the complete tree for this path
|
||||
for (path.packages.items[1..], 0..) |pkg_id, depth| {
|
||||
const pkg_name = pkg_names[pkg_id].slice(string_bytes);
|
||||
var pkg_version_buf: [512]u8 = undefined;
|
||||
const pkg_version_str = std.fmt.bufPrint(&pkg_version_buf, "{}", .{pkg_resolutions[pkg_id].fmt(string_bytes, .auto)}) catch continue;
|
||||
|
||||
// Get dependency type
|
||||
var dep_color: ?Output.ColorCode = null;
|
||||
var dep_suffix: []const u8 = "";
|
||||
const parent_pkg_id = path.packages.items[depth];
|
||||
const parent_deps = pkgs.items(.dependencies)[parent_pkg_id];
|
||||
|
||||
for (0..parent_deps.len) |i| {
|
||||
const dep_id = @as(DependencyID, @truncate(parent_deps.off + i));
|
||||
if (resolutions[dep_id] == pkg_id) {
|
||||
const dep = dependencies[dep_id];
|
||||
if (dep.behavior.isDev() and !dep.behavior.isWorkspace() and !dep.behavior.isBundled() and !dep.behavior.isOptional()) {
|
||||
dep_color = Output.ColorCode.yellow;
|
||||
dep_suffix = " dev";
|
||||
} else if (dep.behavior.isOptional()) {
|
||||
dep_color = Output.ColorCode.magenta;
|
||||
dep_suffix = " optional";
|
||||
} else if (dep.behavior.isPeer()) {
|
||||
dep_color = Output.ColorCode.cyan;
|
||||
dep_suffix = " peer";
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Create indentation
|
||||
var indent_buf: [64]u8 = undefined;
|
||||
var indent_len: usize = 0;
|
||||
for (0..depth) |_| {
|
||||
if (indent_len < 60) {
|
||||
@memcpy(indent_buf[indent_len .. indent_len + 1], " ");
|
||||
indent_len += 1;
|
||||
}
|
||||
}
|
||||
const indent = indent_buf[0..indent_len];
|
||||
|
||||
// Determine tree character based on position
|
||||
const is_last = depth == path.packages.items.len - 2;
|
||||
const is_first = depth == 0;
|
||||
|
||||
const tree_char = if (is_first and is_last) "└──" else if (is_first) "├──" else if (is_last) "└──" else "├──";
|
||||
|
||||
const writer = Output.writer();
|
||||
try writer.writeAll(indent);
|
||||
try writer.writeAll(tree_char);
|
||||
try writer.writeAll(" ");
|
||||
if (dep_color) |color| {
|
||||
if (Output.enable_ansi_colors_stdout) {
|
||||
try writer.writeAll(color.color());
|
||||
}
|
||||
}
|
||||
if (Output.enable_ansi_colors_stdout and pkg_id == target_pkg_id) {
|
||||
try writer.writeAll(Output.ColorCode.bold.color());
|
||||
}
|
||||
|
||||
try writer.writeAll(pkg_name);
|
||||
try writer.writeAll(" ");
|
||||
|
||||
if (pkg_id != target_pkg_id) {
|
||||
if (Output.enable_ansi_colors_stdout) {
|
||||
try writer.writeAll(Output.ColorCode.reset.color());
|
||||
}
|
||||
}
|
||||
|
||||
try writer.writeAll(pkg_version_str);
|
||||
try writer.writeAll(dep_suffix);
|
||||
if (Output.enable_ansi_colors_stdout) {
|
||||
try writer.writeAll(Output.ColorCode.reset.color());
|
||||
}
|
||||
|
||||
try writer.writeAll("\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
19
src/fmt.zig
19
src/fmt.zig
@@ -725,24 +725,7 @@ pub const QuickAndDirtyJavaScriptSyntaxHighlighter = struct {
|
||||
};
|
||||
};
|
||||
|
||||
const ColorCode = enum {
|
||||
magenta,
|
||||
blue,
|
||||
orange,
|
||||
red,
|
||||
pink,
|
||||
|
||||
pub fn color(this: ColorCode) []const u8 {
|
||||
return switch (this) {
|
||||
.magenta => "\x1b[35m",
|
||||
.blue => "\x1b[34m",
|
||||
.orange => "\x1b[33m",
|
||||
.red => "\x1b[31m",
|
||||
// light pink
|
||||
.pink => "\x1b[38;5;206m",
|
||||
};
|
||||
}
|
||||
};
|
||||
const ColorCode = Output.ColorCode;
|
||||
|
||||
pub const Keyword = enum {
|
||||
abstract,
|
||||
|
||||
@@ -9696,6 +9696,7 @@ pub const PackageManager = struct {
|
||||
});
|
||||
|
||||
pub const pm_params: []const ParamType = &(shared_params ++ [_]ParamType{
|
||||
clap.parseParam("--json Output in JSON format") catch unreachable,
|
||||
clap.parseParam("-a, --all") catch unreachable,
|
||||
clap.parseParam("--json Output in JSON format") catch unreachable,
|
||||
// clap.parseParam("--filter <STR>... Pack each matching workspace") catch unreachable,
|
||||
|
||||
@@ -876,6 +876,43 @@ pub const color_map = ComptimeStringMap(string, .{
|
||||
&.{ "bggreen", CSI ++ "42m" },
|
||||
});
|
||||
const RESET: string = "\x1b[0m";
|
||||
pub const ColorCode = enum {
|
||||
magenta,
|
||||
blue,
|
||||
orange,
|
||||
red,
|
||||
pink,
|
||||
green,
|
||||
yellow,
|
||||
cyan,
|
||||
white,
|
||||
black,
|
||||
gray,
|
||||
reset,
|
||||
bold,
|
||||
pub fn color(this: ColorCode) []const u8 {
|
||||
return switch (this) {
|
||||
.magenta => "\x1b[35m",
|
||||
.blue => "\x1b[34m",
|
||||
.orange => "\x1b[33m",
|
||||
.red => "\x1b[31m",
|
||||
// light pink
|
||||
.pink => "\x1b[38;5;206m",
|
||||
.green => "\x1b[32m",
|
||||
.yellow => "\x1b[33m",
|
||||
.cyan => "\x1b[36m",
|
||||
.white => "\x1b[37m",
|
||||
.black => "\x1b[30m",
|
||||
.gray => "\x1b[37m",
|
||||
.reset => "\x1b[0m",
|
||||
.bold => "\x1b[1m",
|
||||
};
|
||||
}
|
||||
|
||||
pub fn format(this: ColorCode, comptime _: []const u8, _: std.fmt.FormatOptions, w: anytype) !void {
|
||||
try w.writeAll(this.color());
|
||||
}
|
||||
};
|
||||
pub fn prettyFmt(comptime fmt: string, comptime is_enabled: bool) [:0]const u8 {
|
||||
if (comptime bun.fast_debug_build_mode)
|
||||
return fmt ++ "\x00";
|
||||
|
||||
263
test/cli/install/bun-pm-why.test.ts
Normal file
263
test/cli/install/bun-pm-why.test.ts
Normal file
@@ -0,0 +1,263 @@
|
||||
import { spawn } from "bun";
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, expect, it } from "bun:test";
|
||||
import { mkdir, writeFile } from "fs/promises";
|
||||
import { bunExe, bunEnv as env } from "harness";
|
||||
import { join } from "path";
|
||||
import {
|
||||
dummyAfterAll,
|
||||
dummyAfterEach,
|
||||
dummyBeforeAll,
|
||||
dummyBeforeEach,
|
||||
dummyRegistry,
|
||||
package_dir,
|
||||
setHandler,
|
||||
} from "./dummy.registry";
|
||||
|
||||
beforeAll(dummyBeforeAll);
|
||||
afterAll(dummyAfterAll);
|
||||
beforeEach(dummyBeforeEach);
|
||||
afterEach(dummyAfterEach);
|
||||
|
||||
it("should explain direct dependency with bun pm why", async () => {
|
||||
const urls: string[] = [];
|
||||
setHandler(dummyRegistry(urls));
|
||||
await writeFile(
|
||||
join(package_dir, "package.json"),
|
||||
JSON.stringify({
|
||||
name: "foo",
|
||||
version: "0.0.1",
|
||||
dependencies: {
|
||||
bar: "latest",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// Install dependencies first
|
||||
{
|
||||
const { stderr, exited } = spawn({
|
||||
cmd: [bunExe(), "install"],
|
||||
cwd: package_dir,
|
||||
stdout: "pipe",
|
||||
stdin: "pipe",
|
||||
stderr: "pipe",
|
||||
env,
|
||||
});
|
||||
const err = await new Response(stderr).text();
|
||||
expect(err).not.toContain("error:");
|
||||
expect(err).toContain("Saved lockfile");
|
||||
expect(await exited).toBe(0);
|
||||
}
|
||||
|
||||
// Test bun pm why
|
||||
{
|
||||
const { stdout, stderr, exited } = spawn({
|
||||
cmd: [bunExe(), "pm", "why", "bar"],
|
||||
cwd: package_dir,
|
||||
stdout: "pipe",
|
||||
stdin: "pipe",
|
||||
stderr: "pipe",
|
||||
env,
|
||||
});
|
||||
|
||||
const output = await new Response(stdout).text();
|
||||
expect(await new Response(stderr).text()).toBe("");
|
||||
expect(output).toContain("bar@0.0.2");
|
||||
expect(output).toContain("foo");
|
||||
expect(output).toContain("depth: 1");
|
||||
expect(await exited).toBe(0);
|
||||
}
|
||||
});
|
||||
|
||||
it("should explain transitive dependency with bun pm why", async () => {
|
||||
const urls: string[] = [];
|
||||
setHandler(dummyRegistry(urls));
|
||||
|
||||
// Create a nested dependency structure
|
||||
await writeFile(
|
||||
join(package_dir, "package.json"),
|
||||
JSON.stringify({
|
||||
name: "foo",
|
||||
version: "0.0.1",
|
||||
dependencies: {
|
||||
moo: "./moo",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await mkdir(join(package_dir, "moo"));
|
||||
await writeFile(
|
||||
join(package_dir, "moo", "package.json"),
|
||||
JSON.stringify({
|
||||
name: "moo",
|
||||
version: "0.1.0",
|
||||
dependencies: {
|
||||
bar: "latest",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// Install dependencies first
|
||||
{
|
||||
const { stderr, exited } = spawn({
|
||||
cmd: [bunExe(), "install"],
|
||||
cwd: package_dir,
|
||||
stdout: "pipe",
|
||||
stdin: "pipe",
|
||||
stderr: "pipe",
|
||||
env,
|
||||
});
|
||||
const err = await new Response(stderr).text();
|
||||
expect(err).not.toContain("error:");
|
||||
expect(err).toContain("Saved lockfile");
|
||||
expect(await exited).toBe(0);
|
||||
}
|
||||
|
||||
// Test bun pm why on the transitive dependency
|
||||
{
|
||||
const { stdout, stderr, exited } = spawn({
|
||||
cmd: [bunExe(), "pm", "why", "bar"],
|
||||
cwd: package_dir,
|
||||
stdout: "pipe",
|
||||
stdin: "pipe",
|
||||
stderr: "pipe",
|
||||
env,
|
||||
});
|
||||
|
||||
const output = await new Response(stdout).text();
|
||||
expect(await new Response(stderr).text()).toBe("");
|
||||
expect(output).toContain("bar@0.0.2");
|
||||
expect(output).toContain("foo");
|
||||
expect(output).toContain("moo");
|
||||
expect(output).toContain("depth: 2");
|
||||
expect(await exited).toBe(0);
|
||||
}
|
||||
});
|
||||
|
||||
it("should return error for non-existent package", async () => {
|
||||
const urls: string[] = [];
|
||||
setHandler(dummyRegistry(urls));
|
||||
await writeFile(
|
||||
join(package_dir, "package.json"),
|
||||
JSON.stringify({
|
||||
name: "foo",
|
||||
version: "0.0.1",
|
||||
dependencies: {
|
||||
bar: "latest",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// Install dependencies first
|
||||
{
|
||||
const { stderr, exited } = spawn({
|
||||
cmd: [bunExe(), "install"],
|
||||
cwd: package_dir,
|
||||
stdout: "pipe",
|
||||
stdin: "pipe",
|
||||
stderr: "pipe",
|
||||
env,
|
||||
});
|
||||
const err = await new Response(stderr).text();
|
||||
expect(err).not.toContain("error:");
|
||||
expect(err).toContain("Saved lockfile");
|
||||
expect(await exited).toBe(0);
|
||||
}
|
||||
|
||||
// Test bun pm why with a non-existent package
|
||||
{
|
||||
const { stdout, stderr, exited } = spawn({
|
||||
cmd: [bunExe(), "pm", "why", "non-existent-package"],
|
||||
cwd: package_dir,
|
||||
stdout: "pipe",
|
||||
stdin: "pipe",
|
||||
stderr: "pipe",
|
||||
env,
|
||||
});
|
||||
|
||||
// In test environment, the error message is written to stdout rather than stderr
|
||||
const output = await new Response(stdout).text();
|
||||
expect(output).toContain("error");
|
||||
expect(output).toContain("package 'non-existent-package' not found");
|
||||
expect(await new Response(stderr).text()).toBe(""); // stderr should be empty in test environments
|
||||
expect(await exited).toBe(0); // The command itself returns 0 even on not found
|
||||
}
|
||||
});
|
||||
|
||||
it("should output JSON format when --json flag is specified", async () => {
|
||||
const urls: string[] = [];
|
||||
setHandler(dummyRegistry(urls));
|
||||
await writeFile(
|
||||
join(package_dir, "package.json"),
|
||||
JSON.stringify({
|
||||
name: "foo",
|
||||
version: "0.0.1",
|
||||
dependencies: {
|
||||
bar: "latest",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// Install dependencies first
|
||||
{
|
||||
const { stderr, exited } = spawn({
|
||||
cmd: [bunExe(), "install"],
|
||||
cwd: package_dir,
|
||||
stdout: "pipe",
|
||||
stdin: "pipe",
|
||||
stderr: "pipe",
|
||||
env,
|
||||
});
|
||||
const err = await new Response(stderr).text();
|
||||
expect(err).not.toContain("error:");
|
||||
expect(err).toContain("Saved lockfile");
|
||||
expect(await exited).toBe(0);
|
||||
}
|
||||
|
||||
// Test bun pm why with JSON output
|
||||
{
|
||||
const { stdout, stderr, exited } = spawn({
|
||||
cmd: [bunExe(), "pm", "why", "--json", "bar"],
|
||||
cwd: package_dir,
|
||||
stdout: "pipe",
|
||||
stdin: "pipe",
|
||||
stderr: "pipe",
|
||||
env,
|
||||
});
|
||||
|
||||
const output = await new Response(stdout).text();
|
||||
expect(await new Response(stderr).text()).toBe("");
|
||||
|
||||
// Parse the JSON to verify it's valid
|
||||
const json = JSON.parse(output);
|
||||
expect(json).toHaveProperty("dependencies");
|
||||
expect(json.dependencies.length).toBe(1);
|
||||
expect(json.dependencies[0].name).toBe("bar");
|
||||
expect(json.dependencies[0].version).toBe("0.0.2");
|
||||
expect(json.dependencies[0]).toHaveProperty("dependencyChain");
|
||||
expect(json.dependencies[0]).toHaveProperty("dependencyChain");
|
||||
|
||||
expect(await exited).toBe(0);
|
||||
}
|
||||
|
||||
// Test JSON output with non-existent package
|
||||
{
|
||||
const { stdout, stderr, exited } = spawn({
|
||||
cmd: [bunExe(), "pm", "why", "--json", "non-existent-package"],
|
||||
cwd: package_dir,
|
||||
stdout: "pipe",
|
||||
stdin: "pipe",
|
||||
stderr: "pipe",
|
||||
env,
|
||||
});
|
||||
|
||||
const output = await new Response(stdout).text();
|
||||
expect(await new Response(stderr).text()).toBe("");
|
||||
|
||||
// Parse the JSON to verify it's valid
|
||||
const json = JSON.parse(output);
|
||||
expect(json).toHaveProperty("error");
|
||||
expect(json.error).toBe("package not found");
|
||||
|
||||
expect(await exited).toBe(0);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user