diff --git a/src/cli.zig b/src/cli.zig index 269bef8e41..5cc3d9d13f 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -1798,7 +1798,7 @@ pub const Command = struct { .DiscordCommand => return try DiscordCommand.exec(allocator), .HelpCommand => return try HelpCommand.exec(allocator), .ReservedCommand => return try ReservedCommand.exec(allocator), - .InitCommand => return try InitCommand.exec(allocator, bun.argv), + .InitCommand => return try InitCommand.exec(allocator, bun.argv[@min(2, bun.argv.len)..]), .BuildCommand => { if (comptime bun.fast_debug_build_mode and bun.fast_debug_build_cmd != .BuildCommand) unreachable; const ctx = try Command.init(allocator, log, .BuildCommand); @@ -2364,13 +2364,14 @@ pub const Command = struct { .InitCommand => { const intro_text = - \\Usage: bun init [flags] [\] + \\Usage: bun init [flags] [\] \\ Initialize a Bun project in the current directory. \\ Creates a package.json, tsconfig.json, and bunfig.toml if they don't exist. \\ \\Flags: \\ --help Print this menu \\ -y, --yes Accept all default options + \\ -m, --minimal Only initialize type definitions \\ \\Examples: \\ bun init diff --git a/src/cli/init_command.zig b/src/cli/init_command.zig index 0e063be053..a5bb0b0d89 100644 --- a/src/cli/init_command.zig +++ b/src/cli/init_command.zig @@ -366,19 +366,45 @@ pub const InitCommand = struct { private: bool = true, }; - pub fn exec(alloc: std.mem.Allocator, argv: [][:0]const u8) !void { - const print_help = brk: { - for (argv) |arg| { + pub fn exec(alloc: std.mem.Allocator, init_args: [][:0]const u8) !void { + // --minimal is a special preset to create only empty package.json + tsconfig.json + var minimal = false; + var auto_yes = false; + var parse_flags = true; + var initialize_in_folder: ?[]const u8 = null; + for (init_args) |arg_| { + const arg = bun.span(arg_); + if (parse_flags and arg.len > 0 and arg[0] == '-') { if (strings.eqlComptime(arg, "--help") or strings.eqlComptime(arg, "-h")) { - break :brk true; + CLI.Command.Tag.printHelp(.InitCommand, true); + Global.exit(0); + } else if (strings.eqlComptime(arg, "-m") or strings.eqlComptime(arg, "--minimal")) { + minimal = true; + } else if (strings.eqlComptime(arg, "-y") or strings.eqlComptime(arg, "--yes")) { + auto_yes = true; + } else if (strings.eqlComptime(arg, "--")) { + parse_flags = false; + } else { + // invalid flag; ignore + } + } else { + if (initialize_in_folder == null) { + initialize_in_folder = arg; + } else { + // invalid positional; ignore } } - break :brk false; - }; + } - if (print_help) { - CLI.Command.Tag.printHelp(.InitCommand, true); - Global.exit(0); + if (initialize_in_folder) |ifdir| { + std.fs.cwd().makePath(ifdir) catch |err| { + Output.prettyErrorln("Failed to create directory {s}: {s}", .{ ifdir, @errorName(err) }); + Global.exit(1); + }; + bun.sys.chdir("", ifdir).unwrap() catch |err| { + Output.prettyErrorln("Failed to change directory to {s}: {s}", .{ ifdir, @errorName(err) }); + Global.exit(1); + }; } var fs = try Fs.FileSystem.init(null); @@ -464,17 +490,6 @@ pub const InitCommand = struct { } } - // --minimal is a special preset to create only empty package.json + tsconfig.json - const minimal = brk: { - for (argv) |arg_| { - const arg = bun.span(arg_); - if (strings.eqlComptime(arg, "-m") or strings.eqlComptime(arg, "--minimal")) { - break :brk true; - } - } - break :brk false; - }; - if (fields.entry_point.len == 0 and !minimal) infer: { fields.entry_point = "index.ts"; @@ -526,16 +541,6 @@ pub const InitCommand = struct { ).data.e_object; } - const auto_yes = Output.stdout_descriptor_type != .terminal or minimal or brk: { - for (argv) |arg_| { - const arg = bun.span(arg_); - if (strings.eqlComptime(arg, "-y") or strings.eqlComptime(arg, "--yes")) { - break :brk true; - } - } - break :brk false; - }; - var template: Template = .blank; if (!auto_yes) { diff --git a/test/cli/init/init.test.ts b/test/cli/init/init.test.ts index 9516743530..4e5171aff1 100644 --- a/test/cli/init/init.test.ts +++ b/test/cli/init/init.test.ts @@ -1,6 +1,6 @@ import { expect, test } from "bun:test"; -import fs from "fs"; -import { bunEnv, bunExe, tmpdirSync } from "harness"; +import fs, { readdirSync } from "fs"; +import { bunEnv, bunExe, tempDirWithFiles, tmpdirSync } from "harness"; import path from "path"; test("bun init works", () => { @@ -76,3 +76,143 @@ test("bun init with piped cli", () => { expect(fs.existsSync(path.join(temp, "node_modules"))).toBe(true); expect(fs.existsSync(path.join(temp, "tsconfig.json"))).toBe(true); }, 30_000); + +test("bun init in folder", () => { + const temp = tmpdirSync(); + const out = Bun.spawnSync({ + cmd: [bunExe(), "init", "-y", "mydir"], + cwd: temp, + stdio: ["ignore", "inherit", "inherit"], + env: bunEnv, + }); + expect(out.exitCode).toBe(0); + expect(readdirSync(temp).sort()).toEqual(["mydir"]); + expect(readdirSync(path.join(temp, "mydir")).sort()).toMatchInlineSnapshot(` + [ + ".gitignore", + "README.md", + "bun.lock", + "index.ts", + "node_modules", + "package.json", + "tsconfig.json", + ] + `); +}); + +test("bun init error rather than overwriting file", async () => { + const temp = tempDirWithFiles("mytmp", { + "mydir": "don't delete me!!!", + }); + const out = Bun.spawnSync({ + cmd: [bunExe(), "init", "-y", "mydir"], + cwd: temp, + stdio: ["ignore", "pipe", "pipe"], + env: bunEnv, + }); + expect(out.stdout.toString()).toBe(""); + expect(out.stderr.toString()).toBe("Failed to create directory mydir: NotDir\n"); + expect(out.exitCode).not.toBe(0); + expect(readdirSync(temp).sort()).toEqual(["mydir"]); + expect(await Bun.file(path.join(temp, "mydir")).text()).toBe("don't delete me!!!"); +}); + +test("bun init utf-8", async () => { + const temp = tempDirWithFiles("mytmp", {}); + const out = Bun.spawnSync({ + cmd: [bunExe(), "init", "-y", "u t f ∞™/subpath"], + cwd: temp, + stdio: ["ignore", "inherit", "inherit"], + env: bunEnv, + }); + expect(out.exitCode).toBe(0); + expect(readdirSync(temp).sort()).toEqual(["u t f ∞™"]); + expect(readdirSync(path.join(temp, "u t f ∞™")).sort()).toEqual(["subpath"]); + expect(readdirSync(path.join(temp, "u t f ∞™/subpath")).sort()).toMatchInlineSnapshot(` + [ + ".gitignore", + "README.md", + "bun.lock", + "index.ts", + "node_modules", + "package.json", + "tsconfig.json", + ] + `); +}); + +test("bun init twice", async () => { + const temp = tempDirWithFiles("mytmp", {}); + const out = Bun.spawnSync({ + cmd: [bunExe(), "init", "-y", "mydir"], + cwd: temp, + stdio: ["ignore", "inherit", "inherit"], + env: bunEnv, + }); + expect(out.exitCode).toBe(0); + expect(readdirSync(temp).sort()).toEqual(["mydir"]); + expect(readdirSync(path.join(temp, "mydir")).sort()).toMatchInlineSnapshot(` + [ + ".gitignore", + "README.md", + "bun.lock", + "index.ts", + "node_modules", + "package.json", + "tsconfig.json", + ] + `); + await Bun.write(path.join(temp, "mydir/index.ts"), "my edited index.ts"); + await Bun.write(path.join(temp, "mydir/README.md"), "my edited README.md"); + await Bun.write(path.join(temp, "mydir/.gitignore"), "my edited .gitignore"); + await Bun.write( + path.join(temp, "mydir/package.json"), + JSON.stringify({ + ...(await Bun.file(path.join(temp, "mydir/package.json")).json()), + name: "my edited package.json", + }), + ); + await Bun.write(path.join(temp, "mydir/tsconfig.json"), `my edited tsconfig.json`); + const out2 = Bun.spawnSync({ + cmd: [bunExe(), "init", "mydir"], + cwd: temp, + stdio: ["ignore", "pipe", "pipe"], + env: bunEnv, + }); + expect(out2.stdout.toString()).toMatchInlineSnapshot(`""`); + expect(out2.stderr.toString()).toMatchInlineSnapshot(` + "note: package.json already exists, configuring existing project + " + `); + expect(out2.exitCode).toBe(0); + expect(readdirSync(temp).sort()).toEqual(["mydir"]); + expect(readdirSync(path.join(temp, "mydir")).sort()).toMatchInlineSnapshot(` + [ + ".gitignore", + "README.md", + "bun.lock", + "index.ts", + "node_modules", + "package.json", + "tsconfig.json", + ] + `); + expect(await Bun.file(path.join(temp, "mydir/index.ts")).text()).toMatchInlineSnapshot(`"my edited index.ts"`); + expect(await Bun.file(path.join(temp, "mydir/README.md")).text()).toMatchInlineSnapshot(`"my edited README.md"`); + expect(await Bun.file(path.join(temp, "mydir/.gitignore")).text()).toMatchInlineSnapshot(`"my edited .gitignore"`); + expect(await Bun.file(path.join(temp, "mydir/package.json")).json()).toMatchInlineSnapshot(` + { + "devDependencies": { + "@types/bun": "latest", + }, + "module": "index.ts", + "name": "my edited package.json", + "peerDependencies": { + "typescript": "^5", + }, + "private": true, + "type": "module", + } + `); + expect(await Bun.file(path.join(temp, "mydir/tsconfig.json")).text()).toMatchInlineSnapshot(`"my edited tsconfig.json"`); +});