Support 'bun init <folder>' (#17743)

This commit is contained in:
pfg
2025-02-26 22:41:12 -08:00
committed by GitHub
parent 59551ebc79
commit 7a033e49c5
3 changed files with 180 additions and 34 deletions

View File

@@ -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 =
\\<b>Usage<r>: <b><green>bun init<r> <cyan>[flags]<r> <blue>[\<entrypoints\>]<r>
\\<b>Usage<r>: <b><green>bun init<r> <cyan>[flags]<r> <blue>[\<folder\>]<r>
\\ Initialize a Bun project in the current directory.
\\ Creates a package.json, tsconfig.json, and bunfig.toml if they don't exist.
\\
\\<b>Flags<r>:
\\ <cyan>--help<r> Print this menu
\\ <cyan>-y, --yes<r> Accept all default options
\\ <cyan>-m, --minimal<r> Only initialize type definitions
\\
\\<b>Examples:<r>
\\ <b><green>bun init<r>

View File

@@ -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) {

View File

@@ -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"`);
});