Compare commits

...

7 Commits

Author SHA1 Message Date
Jarred Sumner
8da5652b69 Merge branch 'main' into claude/fix-pm-no-package-json 2025-07-15 02:37:55 -07:00
Claude Bot
070833d208 Use custom cache directories in tests for cross-platform compatibility
- Use BUN_INSTALL_CACHE_DIR environment variable to set predictable cache paths
- Test validates exact path on all platforms (Windows, macOS, Linux)
- Focus on ensuring commands work without package.json rather than specific output format

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-15 00:45:24 +00:00
jarred-sumner-bot
ca157907bb bun run prettier 2025-07-15 00:38:30 +00:00
Claude Bot
92927644ab Fix test assertions for pm cache command output
The cache directory path varies by platform and doesn't always contain
"cache" substring. Changed assertions to check for absolute path format
instead of specific directory name patterns.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-15 00:35:35 +00:00
jarred-sumner-bot
d436f54eda bun run prettier 2025-07-14 23:42:51 +00:00
jarred-sumner-bot
fb99767588 bun run zig-format 2025-07-14 23:42:02 +00:00
Claude Bot
70e38e8146 Make pm cache/whoami/default-trusted/bin -g work without package.json
Some pm commands like `cache`, `whoami`, `default-trusted`, and `bin -g`
are global operations that don't need a project-specific package.json.
This change allows these commands to work in directories without package.json
while still requiring package.json for project-specific commands like `ls`,
`version`, etc.

Fixes #18733

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-14 23:12:48 +00:00
3 changed files with 421 additions and 24 deletions

View File

@@ -24,6 +24,7 @@ const Npm = Install.Npm;
const PmViewCommand = @import("./pm_view_command.zig");
const PmVersionCommand = @import("./pm_version_command.zig").PmVersionCommand;
const File = bun.sys.File;
const DotEnv = bun.DotEnv;
const ByName = struct {
dependencies: []const Dependency,
@@ -98,6 +99,211 @@ pub const PackageManagerCommand = struct {
return subcommand;
}
fn requiresPackageJson(subcommand: []const u8, global: bool) bool {
// Commands that work globally and don't need package.json
if (strings.eqlComptime(subcommand, "cache")) return false;
if (strings.eqlComptime(subcommand, "whoami")) return false;
if (strings.eqlComptime(subcommand, "default-trusted")) return false;
if (strings.eqlComptime(subcommand, "bin") and global) return false;
// All other commands require package.json
return true;
}
fn execWithoutPackageJson(ctx: Command.Context, cli: PackageManager.CommandLineArguments, subcommand: []const u8) !void {
if (strings.eqlComptime(subcommand, "whoami")) {
// Create a minimal environment for npm registry access
var env: *DotEnv.Loader = brk: {
const map = try ctx.allocator.create(DotEnv.Map);
map.* = DotEnv.Map.init(ctx.allocator);
const loader = try ctx.allocator.create(DotEnv.Loader);
loader.* = DotEnv.Loader.init(map, ctx.allocator);
break :brk loader;
};
env.loadProcess();
const pm = try createMinimalPackageManager(ctx, cli, env);
const username = Npm.whoami(ctx.allocator, pm) catch |err| {
switch (err) {
error.OutOfMemory => bun.outOfMemory(),
error.NeedAuth => {
Output.errGeneric("missing authentication (run <cyan>`bunx npm login`<r>)", .{});
},
error.ProbablyInvalidAuth => {
Output.errGeneric("failed to authenticate with registry '{}'", .{
bun.fmt.redactedNpmUrl(pm.options.scope.url.href),
});
},
}
Global.crash();
};
Output.println("{s}", .{username});
Global.exit(0);
} else if (strings.eqlComptime(subcommand, "cache")) {
const pm = try createMinimalPackageManager(ctx, cli, null);
var dir: bun.PathBuffer = undefined;
var fd = pm.getCacheDirectory();
const outpath = bun.getFdPath(.fromStdDir(fd), &dir) catch |err| {
Output.prettyErrorln("{s} getting cache directory", .{@errorName(err)});
Global.crash();
};
if (cli.positionals.len > 1 and strings.eqlComptime(cli.positionals[1], "rm")) {
fd.close();
var had_err = false;
std.fs.deleteTreeAbsolute(outpath) catch |err| {
Output.err(err, "Could not delete {s}", .{outpath});
had_err = true;
};
Output.prettyln("Cleared 'bun install' cache", .{});
bunx: {
const tmp = bun.fs.FileSystem.RealFS.platformTempDir();
const tmp_dir = std.fs.openDirAbsolute(tmp, .{ .iterate = true }) catch |err| {
Output.err(err, "Could not open {s}", .{tmp});
had_err = true;
break :bunx;
};
var iter = tmp_dir.iterate();
// This is to match 'bunx_command.BunxCommand.exec's logic
const prefix = try std.fmt.allocPrint(ctx.allocator, "bunx-{d}-", .{
if (bun.Environment.isPosix) bun.c.getuid() else bun.windows.userUniqueId(),
});
var deleted: usize = 0;
while (iter.next() catch |err| {
Output.err(err, "Could not read {s}", .{tmp});
had_err = true;
break :bunx;
}) |entry| {
if (std.mem.startsWith(u8, entry.name, prefix)) {
tmp_dir.deleteTree(entry.name) catch |err| {
Output.err(err, "Could not delete {s}", .{entry.name});
had_err = true;
continue;
};
deleted += 1;
}
}
Output.prettyln("Cleared {d} cached 'bunx' packages", .{deleted});
}
Global.exit(if (had_err) 1 else 0);
}
Output.writer().writeAll(outpath) catch {};
Global.exit(0);
} else if (strings.eqlComptime(subcommand, "default-trusted")) {
try DefaultTrustedCommand.exec();
Global.exit(0);
} else if (strings.eqlComptime(subcommand, "bin") and cli.global) {
const pm = try createMinimalPackageManager(ctx, cli, null);
const output_path = Path.joinAbs(Fs.FileSystem.instance.top_level_dir, .auto, bun.asByteSlice(pm.options.bin_path));
Output.prettyln("{s}", .{output_path});
if (Output.stdout_descriptor_type == .terminal) {
Output.prettyln("\n", .{});
}
warner: {
if (Output.enable_ansi_colors_stderr) {
if (bun.getenvZ("PATH")) |path| {
var path_iter = std.mem.tokenizeScalar(u8, path, std.fs.path.delimiter);
while (path_iter.next()) |entry| {
if (strings.eql(entry, output_path)) {
break :warner;
}
}
Output.prettyErrorln("\n<r><yellow>warn<r>: not in $PATH\n", .{});
}
}
}
Output.flush();
return;
}
// Unknown command or shouldn't reach here
Global.exit(1);
}
fn createMinimalPackageManager(ctx: Command.Context, cli: PackageManager.CommandLineArguments, env_loader: ?*DotEnv.Loader) !*PackageManager {
const env: *DotEnv.Loader = env_loader orelse brk: {
const map = try ctx.allocator.create(DotEnv.Map);
map.* = DotEnv.Map.init(ctx.allocator);
const loader = try ctx.allocator.create(DotEnv.Loader);
loader.* = DotEnv.Loader.init(map, ctx.allocator);
loader.loadProcess();
break :brk loader;
};
PackageManager.allocatePackageManager();
const manager = PackageManager.get();
const cpu_count = bun.getThreadCount();
const options = PackageManager.Options{
.global = cli.global,
.max_concurrent_lifecycle_scripts = cpu_count * 2,
};
// Get current directory
var cwd_buf: bun.PathBuffer = undefined;
const cwd = try bun.getcwd(&cwd_buf);
var entries = try Fs.FileSystem.init(null);
const entries_option = try entries.fs.readDirectory(cwd, null, 0, true);
manager.* = PackageManager{
.preallocated_network_tasks = .init(bun.default_allocator),
.preallocated_resolve_tasks = .init(bun.default_allocator),
.options = options,
.active_lifecycle_scripts = .{
.context = manager,
},
.network_task_fifo = std.fifo.LinearFifo(*Install.NetworkTask, .{ .Static = 32 }).init(),
.patch_task_fifo = std.fifo.LinearFifo(*Install.PatchTask, .{ .Static = 32 }).init(),
.allocator = ctx.allocator,
.log = ctx.log,
.root_dir = entries_option.entries,
.env = env,
.cpu_count = cpu_count,
.thread_pool = bun.ThreadPool.init(.{
.max_threads = cpu_count,
}),
.resolve_tasks = .{},
.lockfile = undefined,
.root_package_json_file = undefined,
.event_loop = .{
.mini = bun.JSC.MiniEventLoop.init(bun.default_allocator),
},
.original_package_json_path = try ctx.allocator.dupeZ(u8, cwd),
.workspace_package_json_cache = .{},
.workspace_name_hash = null,
.subcommand = .pm,
.root_package_json_name_at_time_of_init = "",
};
manager.lockfile = try ctx.allocator.create(Lockfile);
manager.lockfile.initEmpty(ctx.allocator);
try manager.options.load(
ctx.allocator,
ctx.log,
env,
cli,
ctx.install,
.pm,
);
return manager;
}
pub fn printHelp() void {
// the output of --help uses the following syntax highlighting
@@ -155,30 +361,41 @@ pub const PackageManagerCommand = struct {
var args = try std.process.argsAlloc(ctx.allocator);
args = args[1..];
const cli = try PackageManager.CommandLineArguments.parse(ctx.allocator, .pm);
// Get subcommand early to determine if package.json is needed
var positionals_copy = cli.positionals;
const subcommand = getSubcommand(&positionals_copy);
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| {
Output.errGeneric("No package.json was found for directory \"{s}\"", .{cwd});
} else |_| {
Output.errGeneric("No package.json was found", .{});
if (requiresPackageJson(subcommand, cli.global)) {
var cwd_buf: bun.PathBuffer = undefined;
if (bun.getcwd(&cwd_buf)) |cwd| {
Output.errGeneric("No package.json was found for directory \"{s}\"", .{cwd});
} else |_| {
Output.errGeneric("No package.json was found", .{});
}
Output.note("Run \"bun init\" to initialize a project", .{});
Global.exit(1);
} else {
// For commands that don't require package.json, create a minimal PackageManager
return execWithoutPackageJson(ctx, cli, subcommand);
}
Output.note("Run \"bun init\" to initialize a project", .{});
Global.exit(1);
}
return err;
};
defer ctx.allocator.free(cwd);
const subcommand = getSubcommand(&pm.options.positionals);
// Get the subcommand for the normal flow
const subcommand_final = getSubcommand(&pm.options.positionals);
if (pm.options.global) {
try pm.setupGlobalDir(ctx);
}
if (strings.eqlComptime(subcommand, "pack")) {
if (strings.eqlComptime(subcommand_final, "pack")) {
try PackCommand.execWithManager(ctx, pm);
Global.exit(0);
} else if (strings.eqlComptime(subcommand, "whoami")) {
} else if (strings.eqlComptime(subcommand_final, "whoami")) {
const username = Npm.whoami(ctx.allocator, pm) catch |err| {
switch (err) {
error.OutOfMemory => bun.outOfMemory(),
@@ -195,11 +412,11 @@ pub const PackageManagerCommand = struct {
};
Output.println("{s}", .{username});
Global.exit(0);
} else if (strings.eqlComptime(subcommand, "view")) {
} else if (strings.eqlComptime(subcommand_final, "view")) {
const property_path = if (pm.options.positionals.len > 2) pm.options.positionals[2] else null;
try PmViewCommand.view(ctx.allocator, pm, if (pm.options.positionals.len > 1) pm.options.positionals[1] else "", property_path, pm.options.json_output);
Global.exit(0);
} else if (strings.eqlComptime(subcommand, "bin")) {
} else if (strings.eqlComptime(subcommand_final, "bin")) {
const output_path = Path.joinAbs(Fs.FileSystem.instance.top_level_dir, .auto, bun.asByteSlice(pm.options.bin_path));
Output.prettyln("{s}", .{output_path});
if (Output.stdout_descriptor_type == .terminal) {
@@ -225,7 +442,7 @@ pub const PackageManagerCommand = struct {
Output.flush();
return;
} else if (strings.eqlComptime(subcommand, "hash")) {
} else if (strings.eqlComptime(subcommand_final, "hash")) {
const load_lockfile = pm.lockfile.loadFromCwd(pm, ctx.allocator, ctx.log, true);
handleLoadLockfileErrors(load_lockfile, pm);
@@ -236,7 +453,7 @@ pub const PackageManagerCommand = struct {
try Output.writer().print("{}", .{load_lockfile.ok.lockfile.fmtMetaHash()});
Output.enableBuffering();
Global.exit(0);
} else if (strings.eqlComptime(subcommand, "hash-print")) {
} else if (strings.eqlComptime(subcommand_final, "hash-print")) {
const load_lockfile = pm.lockfile.loadFromCwd(pm, ctx.allocator, ctx.log, true);
handleLoadLockfileErrors(load_lockfile, pm);
@@ -245,13 +462,13 @@ pub const PackageManagerCommand = struct {
try Output.writer().print("{}", .{load_lockfile.ok.lockfile.fmtMetaHash()});
Output.enableBuffering();
Global.exit(0);
} else if (strings.eqlComptime(subcommand, "hash-string")) {
} else if (strings.eqlComptime(subcommand_final, "hash-string")) {
const load_lockfile = pm.lockfile.loadFromCwd(pm, ctx.allocator, ctx.log, true);
handleLoadLockfileErrors(load_lockfile, pm);
_ = try pm.lockfile.hasMetaHashChanged(true, pm.lockfile.packages.len);
Global.exit(0);
} else if (strings.eqlComptime(subcommand, "cache")) {
} else if (strings.eqlComptime(subcommand_final, "cache")) {
var dir: bun.PathBuffer = undefined;
var fd = pm.getCacheDirectory();
const outpath = bun.getFdPath(.fromStdDir(fd), &dir) catch |err| {
@@ -309,16 +526,16 @@ pub const PackageManagerCommand = struct {
Output.writer().writeAll(outpath) catch {};
Global.exit(0);
} else if (strings.eqlComptime(subcommand, "default-trusted")) {
} else if (strings.eqlComptime(subcommand_final, "default-trusted")) {
try DefaultTrustedCommand.exec();
Global.exit(0);
} else if (strings.eqlComptime(subcommand, "untrusted")) {
} else if (strings.eqlComptime(subcommand_final, "untrusted")) {
try UntrustedCommand.exec(ctx, pm, args);
Global.exit(0);
} else if (strings.eqlComptime(subcommand, "trust")) {
} else if (strings.eqlComptime(subcommand_final, "trust")) {
try TrustCommand.exec(ctx, pm, args);
Global.exit(0);
} else if (strings.eqlComptime(subcommand, "ls")) {
} else if (strings.eqlComptime(subcommand_final, "ls")) {
const load_lockfile = pm.lockfile.loadFromCwd(pm, ctx.allocator, ctx.log, true);
handleLoadLockfileErrors(load_lockfile, pm);
@@ -396,7 +613,7 @@ pub const PackageManagerCommand = struct {
}
Global.exit(0);
} else if (strings.eqlComptime(subcommand, "migrate")) {
} else if (strings.eqlComptime(subcommand_final, "migrate")) {
if (!pm.options.enable.force_save_lockfile) {
if (bun.sys.existsZ("bun.lock")) {
Output.prettyErrorln(
@@ -432,15 +649,15 @@ pub const PackageManagerCommand = struct {
lockfile.saveToDisk(&load_lockfile, &pm.options);
Global.exit(0);
} else if (strings.eqlComptime(subcommand, "version")) {
} else if (strings.eqlComptime(subcommand_final, "version")) {
try PmVersionCommand.exec(ctx, pm, pm.options.positionals, cwd);
Global.exit(0);
}
printHelp();
if (subcommand.len > 0) {
Output.prettyErrorln("\n<red>error<r>: \"{s}\" unknown command\n", .{subcommand});
if (subcommand_final.len > 0) {
Output.prettyErrorln("\n<red>error<r>: \"{s}\" unknown command\n", .{subcommand_final});
Output.flush();
Global.exit(1);

View File

@@ -372,3 +372,131 @@ it("bun pm migrate", async () => {
expect(hash).toMatchSnapshot();
});
it("should work without package.json for global commands", async () => {
const test_dir = tmpdirSync();
const cache_dir = join(test_dir, ".cache");
// Test pm cache without package.json
const {
stdout: cacheOut,
stderr: cacheErr,
exitCode: cacheCode,
} = Bun.spawnSync({
cmd: [bunExe(), "pm", "cache"],
cwd: test_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env: {
...bunEnv,
BUN_INSTALL_CACHE_DIR: cache_dir,
},
});
expect(cacheCode).toBe(0);
expect(cacheErr.toString("utf-8")).toBe("");
expect(cacheOut.toString("utf-8")).toBe(cache_dir);
// Test pm whoami without package.json (will fail auth but shouldn't fail for missing package.json)
const {
stdout: whoamiOut,
stderr: whoamiErr,
exitCode: whoamiCode,
} = Bun.spawnSync({
cmd: [bunExe(), "pm", "whoami"],
cwd: test_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env: bunEnv,
});
expect(whoamiCode).toBe(1); // Expected to fail due to missing auth
expect(whoamiErr.toString("utf-8")).toContain("missing authentication");
expect(whoamiErr.toString("utf-8")).not.toContain("No package.json");
// Test pm bin -g without package.json
const {
stdout: binOut,
stderr: binErr,
exitCode: binCode,
} = Bun.spawnSync({
cmd: [bunExe(), "pm", "bin", "-g"],
cwd: test_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env: bunEnv,
});
expect(binCode).toBe(0);
expect(binErr.toString("utf-8")).toBe("");
expect(binOut.toString("utf-8")).toMatch(/bin/);
// Test pm default-trusted without package.json
const {
stdout: trustedOut,
stderr: trustedErr,
exitCode: trustedCode,
} = Bun.spawnSync({
cmd: [bunExe(), "pm", "default-trusted"],
cwd: test_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env: bunEnv,
});
expect(trustedCode).toBe(0);
expect(trustedErr.toString("utf-8")).toBe("");
expect(trustedOut.toString("utf-8")).toContain("esbuild");
});
it("should require package.json for project-specific commands", async () => {
const test_dir = tmpdirSync();
// Test pm ls without package.json (should fail)
const {
stdout: lsOut,
stderr: lsErr,
exitCode: lsCode,
} = Bun.spawnSync({
cmd: [bunExe(), "pm", "ls"],
cwd: test_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env: bunEnv,
});
expect(lsCode).toBe(1);
expect(lsErr.toString("utf-8")).toContain("No package.json");
// Test pm version without package.json (should fail)
const {
stdout: versionOut,
stderr: versionErr,
exitCode: versionCode,
} = Bun.spawnSync({
cmd: [bunExe(), "pm", "version"],
cwd: test_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env: bunEnv,
});
expect(versionCode).toBe(1);
expect(versionErr.toString("utf-8")).toContain("No package.json");
// Test pm bin (without -g) without package.json (should fail)
const {
stdout: binOut,
stderr: binErr,
exitCode: binCode,
} = Bun.spawnSync({
cmd: [bunExe(), "pm", "bin"],
cwd: test_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env: bunEnv,
});
expect(binCode).toBe(1);
expect(binErr.toString("utf-8")).toContain("No package.json");
});

View File

@@ -0,0 +1,52 @@
// Regression test for https://github.com/oven-sh/bun/issues/18733
// bun pm cache and bun pm cache rm should work without package.json
import { expect, it } from "bun:test";
import { bunEnv, bunExe, tmpdirSync } from "harness";
import { join } from "path";
it("pm cache commands work without package.json (#18733)", async () => {
const test_dir = tmpdirSync();
const cache_dir = join(test_dir, ".cache");
// Test pm cache without package.json
const {
stdout: cacheOut,
stderr: cacheErr,
exitCode: cacheCode,
} = Bun.spawnSync({
cmd: [bunExe(), "pm", "cache"],
cwd: test_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env: {
...bunEnv,
BUN_INSTALL_CACHE_DIR: cache_dir,
},
});
expect(cacheCode).toBe(0);
expect(cacheErr.toString("utf-8")).toBe("");
expect(cacheOut.toString("utf-8")).toBe(cache_dir);
// Test pm cache rm without package.json
const {
stdout: cacheRmOut,
stderr: cacheRmErr,
exitCode: cacheRmCode,
} = Bun.spawnSync({
cmd: [bunExe(), "pm", "cache", "rm"],
cwd: test_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env: {
...bunEnv,
BUN_INSTALL_CACHE_DIR: cache_dir,
},
});
expect(cacheRmCode).toBe(0);
expect(cacheRmErr.toString("utf-8")).toBe("");
// The important thing is that it doesn't error with "No package.json"
expect(cacheRmErr.toString("utf-8")).not.toContain("No package.json");
});