Files
bun.sh/test/cli/init/init.test.ts
Claude Bot b4493db088 Add interactive prompt for bun init in non-empty directories
When running `bun init` in a non-empty directory in TTY mode, users are now presented with an interactive prompt asking whether to:
1. Create in a new subdirectory (default)
2. Use the current directory (may overwrite files)
3. Cancel

This prevents accidental file creation in wrong directories and provides a better UX, similar to tools like `create-next-app`.

The prompt is only shown when:
- No folder was explicitly specified as an argument
- stdin is a TTY (not piped input)
- Not using auto-yes flags (-y, --react, etc.)
- The directory is not empty (ignores .DS_Store and Thumbs.db)

Fixes #24555

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-11 03:40:24 +00:00

378 lines
13 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import fs, { readdirSync } from "fs";
import { bunEnv, bunExe, isWindows, tempDirWithFiles } from "harness";
import path from "path";
(isWindows ? describe : describe.concurrent)("bun init", () => {
test("bun init works", async () => {
const temp = tempDirWithFiles("bun-init-works", {});
const { exited } = Bun.spawn({
cmd: [bunExe(), "init", "-y"],
cwd: temp,
stdio: ["ignore", "inherit", "inherit"],
env: bunEnv,
});
expect(await exited).toBe(0);
const pkg = JSON.parse(fs.readFileSync(path.join(temp, "package.json"), "utf8"));
expect(pkg).toEqual({
"name": path.basename(temp).toLowerCase().replaceAll(" ", "-"),
"module": "index.ts",
"type": "module",
"private": true,
"devDependencies": {
"@types/bun": "latest",
},
"peerDependencies": {
"typescript": "^5",
},
});
const readme = fs.readFileSync(path.join(temp, "README.md"), "utf8");
expect(readme).toStartWith("# " + path.basename(temp).toLowerCase().replaceAll(" ", "-") + "\n");
expect(readme).toInclude("v" + Bun.version.replaceAll("-debug", ""));
expect(readme).toInclude("index.ts");
expect(fs.existsSync(path.join(temp, "index.ts"))).toBe(true);
expect(fs.existsSync(path.join(temp, ".gitignore"))).toBe(true);
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 with piped cli", async () => {
const temp = tempDirWithFiles("bun-init-with-piped-cli", {});
const { exited } = Bun.spawn({
cmd: [bunExe(), "init"],
cwd: temp,
stdio: [new Blob(["\n\n\n\n\n\n\n\n\n\n\n\n"]), "inherit", "inherit"],
env: bunEnv,
});
expect(await exited).toBe(0);
const pkg = JSON.parse(fs.readFileSync(path.join(temp, "package.json"), "utf8"));
expect(pkg).toEqual({
"name": path.basename(temp).toLowerCase().replaceAll(" ", "-"),
"module": "index.ts",
"private": true,
"type": "module",
"devDependencies": {
"@types/bun": "latest",
},
"peerDependencies": {
"typescript": "^5",
},
});
const readme = fs.readFileSync(path.join(temp, "README.md"), "utf8");
expect(readme).toStartWith("# " + path.basename(temp).toLowerCase().replaceAll(" ", "-") + "\n");
expect(readme).toInclude("v" + Bun.version.replaceAll("-debug", ""));
expect(readme).toInclude("index.ts");
expect(fs.existsSync(path.join(temp, "index.ts"))).toBe(true);
expect(fs.existsSync(path.join(temp, ".gitignore"))).toBe(true);
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", async () => {
const temp = tempDirWithFiles("bun-init-in-folder", {
"mydir": {
"index.ts": "// mydir/index.ts",
"README.md": "// mydir/README.md",
".gitignore": "// mydir/.gitignore",
"package.json": '{ "name": "mydir" }',
"tsconfig.json": "// mydir/tsconfig.json",
},
});
const { exited } = Bun.spawn({
cmd: [bunExe(), "init", "-y", "mydir"],
cwd: temp,
stdio: ["ignore", "inherit", "inherit"],
env: bunEnv,
});
expect(await exited).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("bun-init-error-rather-than-overwriting-file", {
"mydir": "don't delete me!!!",
});
const { exited } = Bun.spawn({
cmd: [bunExe(), "init", "-y", "mydir"],
cwd: temp,
stdio: ["ignore", "pipe", "pipe"],
env: bunEnv,
});
expect(await exited).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("bun-init-utf-8", {});
const { exited } = Bun.spawn({
cmd: [bunExe(), "init", "-y", "u t f ∞™/subpath"],
cwd: temp,
stdio: ["ignore", "inherit", "inherit"],
env: bunEnv,
});
expect(await exited).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("bun-init-twice", {});
const { exited } = Bun.spawn({
cmd: [bunExe(), "init", "-y", "mydir"],
cwd: temp,
stdio: ["ignore", "inherit", "inherit"],
env: bunEnv,
});
expect(await exited).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 { exited: exited2, stderr } = Bun.spawn({
cmd: [bunExe(), "init", "mydir"],
cwd: temp,
stdio: ["ignore", "pipe", "pipe"],
env: bunEnv,
});
expect(await exited2).toBe(0);
expect(await stderr.text()).toMatchInlineSnapshot(`
"note: package.json already exists, configuring existing project
"
`);
expect(await exited2).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"`,
);
});
test("bun init --react works", async () => {
const temp = tempDirWithFiles("bun-init--react-works", {});
const { exited } = Bun.spawn({
cmd: [bunExe(), "init", "--react"],
cwd: temp,
stdio: ["ignore", "inherit", "inherit"],
env: bunEnv,
});
expect(await exited).toBe(0);
const pkg = JSON.parse(fs.readFileSync(path.join(temp, "package.json"), "utf8"));
expect(pkg).toHaveProperty("dependencies.react");
expect(pkg).toHaveProperty("dependencies.react-dom");
expect(pkg).toHaveProperty("devDependencies.@types/react");
expect(pkg).toHaveProperty("devDependencies.@types/react-dom");
expect(fs.existsSync(path.join(temp, "src"))).toBe(true);
expect(fs.existsSync(path.join(temp, "src/index.ts"))).toBe(true);
expect(fs.existsSync(path.join(temp, "tsconfig.json"))).toBe(true);
}, 30_000);
test("bun init --react=tailwind works", async () => {
const temp = tempDirWithFiles("bun-init--react=tailwind-works", {});
const { exited } = Bun.spawn({
cmd: [bunExe(), "init", "--react=tailwind"],
cwd: temp,
stdio: ["ignore", "inherit", "inherit"],
env: bunEnv,
});
expect(await exited).toBe(0);
const pkg = JSON.parse(fs.readFileSync(path.join(temp, "package.json"), "utf8"));
expect(pkg).toHaveProperty("dependencies.react");
expect(pkg).toHaveProperty("dependencies.react-dom");
expect(pkg).toHaveProperty("devDependencies.@types/react");
expect(pkg).toHaveProperty("devDependencies.@types/react-dom");
expect(pkg).toHaveProperty("dependencies.bun-plugin-tailwind");
expect(fs.existsSync(path.join(temp, "src"))).toBe(true);
expect(fs.existsSync(path.join(temp, "src/index.ts"))).toBe(true);
}, 30_000);
test("bun init --react=shadcn works", async () => {
const temp = tempDirWithFiles("bun-init--react=shadcn-works", {});
const { exited } = Bun.spawn({
cmd: [bunExe(), "init", "--react=shadcn"],
cwd: temp,
stdio: ["ignore", "inherit", "inherit"],
env: bunEnv,
});
expect(await exited).toBe(0);
const pkg = JSON.parse(fs.readFileSync(path.join(temp, "package.json"), "utf8"));
expect(pkg).toHaveProperty("dependencies.react");
expect(pkg).toHaveProperty("dependencies.react-dom");
expect(pkg).toHaveProperty("dependencies.@radix-ui/react-slot");
expect(pkg).toHaveProperty("dependencies.class-variance-authority");
expect(pkg).toHaveProperty("dependencies.clsx");
expect(pkg).toHaveProperty("dependencies.bun-plugin-tailwind");
expect(fs.existsSync(path.join(temp, "src"))).toBe(true);
expect(fs.existsSync(path.join(temp, "src/index.ts"))).toBe(true);
expect(fs.existsSync(path.join(temp, "src/components"))).toBe(true);
expect(fs.existsSync(path.join(temp, "src/components/ui"))).toBe(true);
}, 30_000);
test("bun init in non-empty directory with --yes skips prompt", async () => {
const temp = tempDirWithFiles("bun-init-non-empty-auto-yes", {
"existing-file.txt": "I exist",
});
const { exited } = Bun.spawn({
cmd: [bunExe(), "init", "-y"],
cwd: temp,
stdio: ["ignore", "inherit", "inherit"],
env: bunEnv,
});
expect(await exited).toBe(0);
// With -y flag, should proceed in current directory without prompting
expect(fs.existsSync(path.join(temp, "existing-file.txt"))).toBe(true);
expect(fs.existsSync(path.join(temp, "package.json"))).toBe(true);
expect(fs.existsSync(path.join(temp, "index.ts"))).toBe(true);
}, 30_000);
test("bun init --react in non-empty directory skips prompt (auto-yes)", async () => {
const temp = tempDirWithFiles("bun-init-react-non-empty", {
"existing-file.txt": "I exist",
});
const { exited } = Bun.spawn({
cmd: [bunExe(), "init", "--react"],
cwd: temp,
stdio: ["ignore", "inherit", "inherit"],
env: bunEnv,
});
expect(await exited).toBe(0);
// --react flag implies auto-yes, should proceed in current directory
expect(fs.existsSync(path.join(temp, "existing-file.txt"))).toBe(true);
expect(fs.existsSync(path.join(temp, "package.json"))).toBe(true);
expect(fs.existsSync(path.join(temp, "src"))).toBe(true);
}, 30_000);
test("bun init with explicit folder ignores non-empty directory check", async () => {
const temp = tempDirWithFiles("bun-init-explicit-folder", {
"existing-file.txt": "I exist",
});
const { exited } = Bun.spawn({
cmd: [bunExe(), "init", "-y", "new-folder"],
cwd: temp,
stdio: ["ignore", "inherit", "inherit"],
env: bunEnv,
});
expect(await exited).toBe(0);
// Should create in specified folder without prompting
expect(fs.existsSync(path.join(temp, "existing-file.txt"))).toBe(true);
expect(fs.existsSync(path.join(temp, "new-folder"))).toBe(true);
expect(fs.existsSync(path.join(temp, "new-folder/package.json"))).toBe(true);
}, 30_000);
test("bun init in directory with only .DS_Store is considered empty", async () => {
const temp = tempDirWithFiles("bun-init-only-ds-store", {
".DS_Store": "macOS metadata",
});
const { exited } = Bun.spawn({
cmd: [bunExe(), "init", "-y"],
cwd: temp,
stdio: ["ignore", "inherit", "inherit"],
env: bunEnv,
});
expect(await exited).toBe(0);
// Should proceed without prompting since .DS_Store is ignored
expect(fs.existsSync(path.join(temp, "package.json"))).toBe(true);
expect(fs.existsSync(path.join(temp, "index.ts"))).toBe(true);
}, 30_000);
});