diff options
-rw-r--r-- | src/cli/package_manager_command.zig | 6 | ||||
-rw-r--r-- | src/install/install.zig | 121 | ||||
-rw-r--r-- | test/cli/install/bun-add.test.ts | 45 |
3 files changed, 116 insertions, 56 deletions
diff --git a/src/cli/package_manager_command.zig b/src/cli/package_manager_command.zig index b7e547c7a..83d3dd023 100644 --- a/src/cli/package_manager_command.zig +++ b/src/cli/package_manager_command.zig @@ -51,7 +51,7 @@ pub const PackageManagerCommand = struct { @memcpy(&lockfile_buffer, lockfile_.ptr, lockfile_.len); lockfile_buffer[lockfile_.len] = 0; var lockfile = lockfile_buffer[0..lockfile_.len :0]; - var pm = try PackageManager.init(ctx, null, &PackageManager.install_params); + var pm = try PackageManager.init(ctx, null, PackageManager.Subcommand.pm); const load_lockfile = pm.lockfile.loadFromDisk(ctx.allocator, ctx.log, lockfile); handleLoadLockfileErrors(load_lockfile, pm); @@ -87,11 +87,11 @@ pub const PackageManagerCommand = struct { var args = try std.process.argsAlloc(ctx.allocator); args = args[1..]; - var pm = PackageManager.init(ctx, null, &PackageManager.install_params) catch |err| { + var pm = PackageManager.init(ctx, null, PackageManager.Subcommand.pm) catch |err| { // TODO: error messages here // if (err == error.MissingPackageJSON) { // // TODO: error messages - // // var cli = try PackageManager.CommandLineArguments.parse(ctx.allocator, &PackageManager.install_params, &_ctx); + // // var cli = try PackageManager.CommandLineArguments.parse(ctx.allocator, PackageManager.Subcommand.pm, &_ctx); // } return err; diff --git a/src/install/install.zig b/src/install/install.zig index 37aafcc85..cfbb0cb7f 100644 --- a/src/install/install.zig +++ b/src/install/install.zig @@ -4968,37 +4968,46 @@ pub const PackageManager = struct { } }; + // Corresponds to possible commands from the CLI. + pub const Subcommand = enum { + install, + pm, + add, + remove, + link, + unlink, + }; + pub fn init( ctx: Command.Context, package_json_file_: ?std.fs.File, - comptime params: []const ParamType, + comptime subcommand: Subcommand, ) !*PackageManager { - return initMaybeInstall(ctx, package_json_file_, params, false); + return initMaybeInstall(ctx, package_json_file_, subcommand); } fn initMaybeInstall( ctx: Command.Context, package_json_file_: ?std.fs.File, - comptime params: []const ParamType, - comptime is_install: bool, + comptime subcommand: Subcommand, ) !*PackageManager { - const cli = try CommandLineArguments.parse(ctx.allocator, params); + const cli = try CommandLineArguments.parse(ctx.allocator, subcommand); - if (comptime is_install) { + if (comptime subcommand == .install) { if (cli.positionals.len > 1) { return error.SwitchToBunAdd; } } var _ctx = ctx; - return initWithCLI(&_ctx, package_json_file_, cli, is_install); + return initWithCLI(&_ctx, package_json_file_, cli, subcommand); } fn initWithCLI( ctx: *Command.Context, package_json_file_: ?std.fs.File, cli: CommandLineArguments, - comptime is_install: bool, + comptime subcommand: Subcommand, ) !*PackageManager { // assume that spawning a thread will take a lil so we do that asap try HTTP.HTTPThread.init(); @@ -5086,7 +5095,7 @@ pub const PackageManager = struct { for (workspace_names.keys()) |path| { if (strings.eql(child_cwd, path)) { fs.top_level_dir = parent; - if (comptime is_install) { + if (comptime subcommand == .install) { found = true; child_json.close(); break :brk json_file; @@ -5323,31 +5332,35 @@ pub const PackageManager = struct { return manager; } + fn attemptToCreatePackageJSON() !std.fs.File { + var package_json_file = std.fs.cwd().createFileZ("package.json", .{ .read = true }) catch |err| { + Output.prettyErrorln("<r><red>error:<r> {s} create package.json", .{@errorName(err)}); + Global.crash(); + }; + try package_json_file.pwriteAll("{\"dependencies\": {}}", 0); + return package_json_file; + } + pub inline fn add( ctx: Command.Context, ) !void { - try updatePackageJSONAndInstall(ctx, .add, &add_params); + try updatePackageJSONAndInstall(ctx, .add, .add); } pub inline fn remove( ctx: Command.Context, ) !void { - try updatePackageJSONAndInstall(ctx, .remove, &remove_params); + try updatePackageJSONAndInstall(ctx, .remove, .remove); } pub inline fn link( ctx: Command.Context, ) !void { - var manager = PackageManager.init(ctx, null, &link_params) catch |err| brk: { + var manager = PackageManager.init(ctx, null, .link) catch |err| brk: { switch (err) { error.MissingPackageJSON => { - var package_json_file = std.fs.cwd().createFileZ("package.json", .{ .read = true }) catch |err2| { - Output.prettyErrorln("<r><red>error:<r> {s} create package.json", .{@errorName(err2)}); - Global.crash(); - }; - try package_json_file.pwriteAll("{\"dependencies\": {}}", 0); - - break :brk try PackageManager.init(ctx, package_json_file, &link_params); + var package_json_file = try attemptToCreatePackageJSON(); + break :brk try PackageManager.init(ctx, package_json_file, .link); }, else => return err, } @@ -5502,16 +5515,11 @@ pub const PackageManager = struct { pub inline fn unlink( ctx: Command.Context, ) !void { - var manager = PackageManager.init(ctx, null, &unlink_params) catch |err| brk: { + var manager = PackageManager.init(ctx, null, .unlink) catch |err| brk: { switch (err) { error.MissingPackageJSON => { - var package_json_file = std.fs.cwd().createFileZ("package.json", .{ .read = true }) catch |err2| { - Output.prettyErrorln("<r><red>error:<r> {s} create package.json", .{@errorName(err2)}); - Global.crash(); - }; - try package_json_file.pwriteAll("{\"dependencies\": {}}", 0); - - break :brk try PackageManager.init(ctx, package_json_file, &unlink_params); + var package_json_file = try attemptToCreatePackageJSON(); + break :brk try PackageManager.init(ctx, package_json_file, .unlink); }, else => return err, } @@ -5633,11 +5641,12 @@ pub const PackageManager = struct { else "Possible values: \"hardlink\" (default), \"symlink\", \"copyfile\""; - pub const install_params_ = [_]ParamType{ + const install_params_ = [_]ParamType{ clap.parseParam("-c, --config <STR>? Load config (bunfig.toml)") catch unreachable, clap.parseParam("-y, --yarn Write a yarn.lock file (yarn v1)") catch unreachable, clap.parseParam("-p, --production Don't install devDependencies") catch unreachable, clap.parseParam("--no-save Don't save a lockfile") catch unreachable, + clap.parseParam("--save Save to package.json") catch unreachable, clap.parseParam("--dry-run Don't install anything") catch unreachable, clap.parseParam("--lockfile <PATH> Store & load a lockfile at a specific filepath") catch unreachable, clap.parseParam("-f, --force Always request the latest versions from the registry & reinstall all dependencies") catch unreachable, @@ -5660,27 +5669,29 @@ pub const PackageManager = struct { clap.parseParam("--help Print this help menu") catch unreachable, }; - pub const install_params = install_params_ ++ [_]ParamType{ + const install_params = install_params_ ++ [_]ParamType{ clap.parseParam("<POS> ... ") catch unreachable, }; - pub const add_params = install_params_ ++ [_]ParamType{ + const pm_params = install_params_ ++ [_]ParamType{ + clap.parseParam("<POS> ... ") catch unreachable, + }; + + const add_params = install_params_ ++ [_]ParamType{ clap.parseParam("-d, --development Add dependency to \"devDependencies\"") catch unreachable, clap.parseParam("--optional Add dependency to \"optionalDependencies\"") catch unreachable, clap.parseParam("<POS> ... \"name\" or \"name@version\" of packages to install") catch unreachable, }; - pub const remove_params = install_params_ ++ [_]ParamType{ + const remove_params = install_params_ ++ [_]ParamType{ clap.parseParam("<POS> ... \"name\" of packages to remove from package.json") catch unreachable, }; - pub const link_params = install_params_ ++ [_]ParamType{ - clap.parseParam("--save Save to package.json") catch unreachable, + const link_params = install_params_ ++ [_]ParamType{ clap.parseParam("<POS> ... \"name\" install package as a link") catch unreachable, }; - pub const unlink_params = install_params_ ++ [_]ParamType{ - clap.parseParam("--save Save to package.json") catch unreachable, + const unlink_params = install_params_ ++ [_]ParamType{ clap.parseParam("<POS> ... \"name\" uninstall package as a link") catch unreachable, }; @@ -5732,7 +5743,16 @@ pub const PackageManager = struct { } }; - pub fn parse(allocator: std.mem.Allocator, comptime params: []const ParamType) !CommandLineArguments { + pub fn parse(allocator: std.mem.Allocator, comptime subcommand: Subcommand) !CommandLineArguments { + comptime var params: []const ParamType = &switch (subcommand) { + .install => install_params, + .pm => pm_params, + .add => add_params, + .remove => remove_params, + .link => link_params, + .unlink => unlink_params, + }; + var diag = clap.Diagnostic{}; var args = clap.parse(clap.Help, params, .{ @@ -5757,7 +5777,6 @@ pub const PackageManager = struct { var cli = CommandLineArguments{}; cli.yarn = args.flag("--yarn"); cli.production = args.flag("--production"); - cli.no_save = args.flag("--no-save"); cli.no_progress = args.flag("--no-progress"); cli.dry_run = args.flag("--dry-run"); cli.global = args.flag("--global"); @@ -5769,12 +5788,13 @@ pub const PackageManager = struct { cli.verbose = args.flag("--verbose"); cli.ignore_scripts = args.flag("--ignore-scripts"); cli.no_summary = args.flag("--no-summary"); - if (comptime @TypeOf(args).hasFlag("--save")) { - cli.no_save = true; - if (args.flag("--save")) { - cli.no_save = false; - } + // link and unlink default to not saving, all others default to + // saving. + if (comptime subcommand == .link or subcommand == .unlink) { + cli.no_save = !args.flag("--save"); + } else { + cli.no_save = args.flag("--no-save"); } if (args.option("--config")) |opt| { @@ -5783,7 +5803,7 @@ pub const PackageManager = struct { cli.link_native_bins = args.options("--link-native-bins"); - if (comptime params.len == add_params.len) { + if (comptime subcommand == .add) { cli.development = args.flag("--development"); cli.optional = args.flag("--optional"); } @@ -5961,19 +5981,14 @@ pub const PackageManager = struct { fn updatePackageJSONAndInstall( ctx: Command.Context, comptime op: Lockfile.Package.Diff.Op, - comptime params: []const ParamType, + comptime subcommand: Subcommand, ) !void { - var manager = PackageManager.init(ctx, null, params) catch |err| brk: { + var manager = PackageManager.init(ctx, null, subcommand) catch |err| brk: { switch (err) { error.MissingPackageJSON => { if (op == .add or op == .update) { - var package_json_file = std.fs.cwd().createFileZ("package.json", .{ .read = true }) catch |err2| { - Output.prettyErrorln("<r><red>error:<r> {s} create package.json", .{@errorName(err2)}); - Global.crash(); - }; - try package_json_file.pwriteAll("{\"dependencies\": {}}", 0); - - break :brk try PackageManager.init(ctx, package_json_file, params); + var package_json_file = try attemptToCreatePackageJSON(); + break :brk try PackageManager.init(ctx, package_json_file, subcommand); } Output.prettyErrorln("<r>No package.json, so nothing to remove\n", .{}); @@ -6385,7 +6400,7 @@ pub const PackageManager = struct { var package_json_cwd: string = ""; pub inline fn install(ctx: Command.Context) !void { - var manager = initMaybeInstall(ctx, null, &install_params, true) catch |err| { + var manager = initMaybeInstall(ctx, null, .install) catch |err| { if (err == error.SwitchToBunAdd) { return add(ctx); } diff --git a/test/cli/install/bun-add.test.ts b/test/cli/install/bun-add.test.ts index 66ad6afa2..79804f0e0 100644 --- a/test/cli/install/bun-add.test.ts +++ b/test/cli/install/bun-add.test.ts @@ -1416,3 +1416,48 @@ it("should add dependencies to workspaces directly", async () => { expect(await readdirSorted(join(package_dir, "node_modules", "foo"))).toEqual(["package.json"]); expect(await file(join(package_dir, "node_modules", "foo", "package.json")).text()).toEqual(foo_package); }); + +it("should redirect 'install --save X' to 'add'", async () => { + await installRedirectsToAdd(true); +}); + +it("should redirect 'install X --save' to 'add'", async () => { + await installRedirectsToAdd(false); +}); + +async function installRedirectsToAdd(saveFlagFirst) { + await writeFile( + join(add_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + }), + ); + const add_path = relative(package_dir, add_dir); + + const args = [`file:${add_path}`, "--save"]; + if (saveFlagFirst) args.reverse(); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install", ...args], + cwd: package_dir, + stdout: null, + stdin: "pipe", + stderr: "pipe", + env, + }); + expect(stderr).toBeDefined(); + const err = await new Response(stderr).text(); + expect(err.replace(/^(.*?) v[^\n]+/, "$1").split(/\r?\n/)).toEqual(["bun add", " Saved lockfile", ""]); + expect(stdout).toBeDefined(); + const out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + "", + ` installed foo@${add_path}`, + "", + "", + " 1 packages installed", + ]); + expect(await exited).toBe(0); + expect((await file(join(package_dir, "package.json")).text()).includes("bun-add.test")); +} |