diff --git a/src/runtime/cli/init_command.rs b/src/runtime/cli/init_command.rs index 9e5f947a2265..546629cae024 100644 --- a/src/runtime/cli/init_command.rs +++ b/src/runtime/cli/init_command.rs @@ -966,15 +966,6 @@ impl Assets { Self::create_full_inner(asset, asset_name, "", is_template, args) } - pub fn create_with_contents( - asset_name: &[u8], - contents: &'static [u8], - args: &[(&[u8], &[u8])], - ) -> Result<(), Error> { - let is_template = !args.is_empty(); - Self::create_full_with_contents(asset_name, contents, "", is_template, args) - } - /// Substitutes named placeholders `{[key]s}` in `template` with the /// corresponding value from `args`. fn substitute(template: &[u8], args: &[(&[u8], &[u8])]) -> Vec { @@ -1064,40 +1055,6 @@ impl Assets { Output::flush(); Ok(()) } - - fn create_full_with_contents( - // name of asset file to create - filename: &[u8], - contents: &'static [u8], - // optionally add a suffix to the end of the `+ filename` message. Must have a leading space. - message_suffix: &'static str, - // Treat the asset as a format string, using `args` to populate it. Only applies to known assets. - is_template: bool, - // Format arguments - args: &[(&[u8], &[u8])], - ) -> Result<(), Error> { - let file = bun_sys::File::openat( - Fd::cwd(), - filename, - bun_sys::O::WRONLY | bun_sys::O::CREAT | bun_sys::O::TRUNC, - 0o666, - )?; - - if is_template { - let buf = Self::substitute(contents, args); - file.write_all(&buf)?; - } else { - file.write_all(contents)?; - } - - bun_core::prettyln!( - " + {}{}", - bstr::BStr::new(filename), - message_suffix, - ); - Output::flush(); - Ok(()) - } } // ────────────────────────────────────────────────────────────────────────── @@ -1656,14 +1613,18 @@ impl Template { let contents = file.contents; let result = if path == b"README.md" { - Assets::create_with_contents( - b"README.md", - contents, - &[ - (b"name", self.name()), - (b"bunVersion", Environment::VERSION_STRING.as_bytes()), - ], - ) + if exists_z(b"README") || exists_z(b"README.txt") || exists_z(b"README.mdx") { + Err(crate::Error::Sys(bun_errno::SystemErrno::EEXIST)) + } else { + let buf = Assets::substitute( + contents, + &[ + (b"name", self.name()), + (b"bunVersion", Environment::VERSION_STRING.as_bytes()), + ], + ); + Assets::create_new(ZStr::from_slice_with_nul(b"README.md\0"), &buf) + } } else { let mut p = path.to_vec(); p.push(0); diff --git a/test/cli/init/init.test.ts b/test/cli/init/init.test.ts index 15e602586d74..d00115ef309c 100644 --- a/test/cli/init/init.test.ts +++ b/test/cli/init/init.test.ts @@ -271,6 +271,50 @@ const initEnv = { ...bunEnv, BUN_AGENT_RULE_DISABLED: "1" }; expect(fs.existsSync(path.join(temp, "tsconfig.json"))).toBe(true); }, 30_000); + test("bun init --react does not overwrite an existing README.md", async () => { + // https://github.com/oven-sh/bun/issues/2892 + const temp = tempDirWithFiles("bun-init--react-preserve-readme", { + "README.md": "MY README - do not lose me\n", + "src/index.ts": "export const mine = 1;\n", + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "init", "--react"], + cwd: temp, + stdio: ["ignore", "pipe", "pipe"], + env: initEnv, + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(stdout).toContain("README.md (already exists, skipping)"); + expect(stdout).toContain("src/index.ts (already exists, skipping)"); + expect(stderr).not.toContain("error"); + + expect(fs.readFileSync(path.join(temp, "README.md"), "utf8")).toBe("MY README - do not lose me\n"); + expect(fs.readFileSync(path.join(temp, "src/index.ts"), "utf8")).toBe("export const mine = 1;\n"); + // a file the user did *not* have should still be created + expect(fs.existsSync(path.join(temp, "src/index.html"))).toBe(true); + expect(exitCode).toBe(0); + }, 30_000); + + test("bun init --react into an empty dir still writes a templated README.md", async () => { + const temp = tempDirWithFiles("bun-init--react-fresh-readme", {}); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "init", "--react"], + cwd: temp, + stdio: ["ignore", "pipe", "pipe"], + env: initEnv, + }); + const [stdout, , exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(stdout).toMatch(/\+ README\.md/); + const readme = fs.readFileSync(path.join(temp, "README.md"), "utf8"); + expect(readme).toStartWith("# bun-react-template"); + expect(readme).toInclude("v" + Bun.version.replaceAll("-debug", "")); + expect(exitCode).toBe(0); + }, 30_000); + test("bun init --react=tailwind works", async () => { const temp = tempDirWithFiles("bun-init--react=tailwind-works", {});