Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions docs/quickstart.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -228,10 +228,12 @@ Bun can also execute `"scripts"` from your `package.json`. Add the following scr
"start": "bun run index.ts" // [!code ++]
}, // [!code ++]
"devDependencies": {
"@types/bun": "latest"
"@types/bun": "latest",
"@types/react": "^19"
},
"peerDependencies": {
"typescript": "^6"
"typescript": "^6",
"react": "^19"
}
}
```
Expand Down
56 changes: 48 additions & 8 deletions src/runtime/cli/init_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -736,8 +736,28 @@ impl InitCommand {
true
};

need_run_bun_install =
needs_dependencies || needs_dev_dependencies || needs_typescript_dependency;
// tsconfig.default.json sets `"jsx": "react-jsx"`; without react and
// @types/react a .tsx file can neither run nor typecheck (#5056).
Comment thread
robobun marked this conversation as resolved.
Resolved
let needs_react_dependency = !minimal
&& 'brk: {
for key in [
b"dependencies".as_slice(),
b"devDependencies",
b"peerDependencies",
] {
if let Some(deps) = object.get(key) {
if deps.has_any_property_named(&[b"react", b"@types/react"]) {
break 'brk false;
}
}
}
true
};
Comment thread
robobun marked this conversation as resolved.
Resolved

need_run_bun_install = needs_dependencies
|| needs_dev_dependencies
|| needs_typescript_dependency
|| needs_react_dependency;

if needs_dependencies {
let mut dependencies_object = object.get(b"dependencies").unwrap_or_else(|| {
Expand Down Expand Up @@ -770,15 +790,35 @@ impl InitCommand {
object.put(&bump, b"devDependencies", obj)?;
}

if needs_typescript_dependency {
if needs_react_dependency {
let mut obj = object.get(b"devDependencies").unwrap_or_else(|| {
bun_ast::Expr::init(bun_ast::E::Object::default(), bun_ast::Loc::EMPTY)
});
obj.data
.e_object_mut()
.unwrap()
.put_string(&bump, b"@types/react", b"^19")?;
object.put(&bump, b"devDependencies", obj)?;
}

if needs_typescript_dependency || needs_react_dependency {
let mut peer_dependencies = object.get(b"peerDependencies").unwrap_or_else(|| {
bun_ast::Expr::init(bun_ast::E::Object::default(), bun_ast::Loc::EMPTY)
});
peer_dependencies.data.e_object_mut().unwrap().put_string(
&bump,
b"typescript",
b"^6",
)?;
if needs_typescript_dependency {
peer_dependencies.data.e_object_mut().unwrap().put_string(
&bump,
b"typescript",
b"^6",
)?;
}
if needs_react_dependency {
peer_dependencies
.data
.e_object_mut()
.unwrap()
.put_string(&bump, b"react", b"^19")?;
}
object.put(&bump, b"peerDependencies", peer_dependencies)?;
}
}
Expand Down
83 changes: 83 additions & 0 deletions test/cli/init/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,11 @@ const initEnv = { ...bunEnv, BUN_AGENT_RULE_DISABLED: "1" };
"private": true,
"devDependencies": {
"@types/bun": "latest",
"@types/react": "^19",
},
"peerDependencies": {
"typescript": "^6",
"react": "^19",
},
});
const readme = fs.readFileSync(path.join(temp, "README.md"), "utf8");
Expand Down Expand Up @@ -222,10 +224,12 @@ const initEnv = { ...bunEnv, BUN_AGENT_RULE_DISABLED: "1" };
{
"devDependencies": {
"@types/bun": "latest",
"@types/react": "^19",
},
"module": "index.ts",
"name": "my edited package.json",
"peerDependencies": {
"react": "^19",
"typescript": "^6",
},
"private": true,
Expand All @@ -237,6 +241,80 @@ const initEnv = { ...bunEnv, BUN_AGENT_RULE_DISABLED: "1" };
);
});

// https://github.com/oven-sh/bun/issues/5056
// The blank scaffold's tsconfig sets `"jsx": "react-jsx"`, so a bare `.tsx`
// file must both run and typecheck without any manual `bun add`.
test("bun init -y: a .tsx file runs and typechecks out of the box", async () => {
await using temp = tempDir("bun-init-tsx-works", {});

await using init = Bun.spawn({
cmd: [bunExe(), "init", "-y"],
cwd: temp,
stdio: ["ignore", "pipe", "pipe"],
env: initEnv,
});
const [, , initExit] = await Promise.all([init.stdout.text(), init.stderr.text(), init.exited]);
expect(initExit).toBe(0);

const pkg = JSON.parse(fs.readFileSync(path.join(temp, "package.json"), "utf8"));
expect(pkg.peerDependencies).toHaveProperty("react");
expect(pkg.devDependencies).toHaveProperty("@types/react");
expect(fs.existsSync(path.join(temp, "node_modules/react"))).toBe(true);
expect(fs.existsSync(path.join(temp, "node_modules/@types/react"))).toBe(true);

const tsx = `function Component(props: { message: string }): any {
return (
<body>
<h1 style={{ color: "red" }}>{props.message}</h1>
</body>
);
}
console.log(<Component message="Hello world!" />);
`;
fs.writeFileSync(path.join(temp, "react.tsx"), tsx);

await using run = Bun.spawn({
cmd: [bunExe(), "run", "react.tsx"],
cwd: temp,
stdio: ["ignore", "pipe", "pipe"],
env: bunEnv,
});
const [runOut, runErr, runExit] = await Promise.all([run.stdout.text(), run.stderr.text(), run.exited]);
expect(runErr).not.toContain("Cannot find module");
expect(runOut).toContain("Hello world!");
expect(runExit).toBe(0);

await using tsc = Bun.spawn({
cmd: [nodeExe() ?? bunExe(), "node_modules/typescript/bin/tsc", "--noEmit"],
cwd: temp,
stdio: ["ignore", "pipe", "pipe"],
env: bunEnv,
});
const [tscOut, tscErr, tscExit] = await Promise.all([tsc.stdout.text(), tsc.stderr.text(), tsc.exited]);
expect({ tscOut, tscErr }).toEqual({ tscOut: "", tscErr: "" });
expect(tscExit).toBe(0);
}, 60_000);

test("bun init leaves an existing react dependency alone", async () => {
await using temp = tempDir("bun-init-existing-react", {
"package.json": JSON.stringify({ name: "existing", dependencies: { react: "^18" } }),
});

await using proc = Bun.spawn({
cmd: [bunExe(), "init", "-y"],
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, stderr, exitCode }).toMatchObject({ exitCode: 0 });

const pkg = JSON.parse(fs.readFileSync(path.join(temp, "package.json"), "utf8"));
expect(pkg.dependencies).toEqual({ react: "^18" });
expect(pkg.peerDependencies ?? {}).not.toHaveProperty("react");
expect(pkg.devDependencies ?? {}).not.toHaveProperty("@types/react");
}, 30_000);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

test("bun init --react works", async () => {
await using temp = tempDir("bun-init--react-works", {});

Expand Down Expand Up @@ -418,6 +496,11 @@ const initEnv = { ...bunEnv, BUN_AGENT_RULE_DISABLED: "1" };
expect(fs.existsSync(path.join(temp, "package.json"))).toBe(true);
expect(fs.existsSync(path.join(temp, "tsconfig.json"))).toBe(true);

// --minimal stays minimal: no react/@types/react/typescript added
const pkg = JSON.parse(fs.readFileSync(path.join(temp, "package.json"), "utf8"));
expect(pkg.peerDependencies).toBeUndefined();
expect(pkg.devDependencies).toEqual({ "@types/bun": "latest" });

// Should NOT create these extra files with --minimal
expect(fs.existsSync(path.join(temp, "index.ts"))).toBe(false);
expect(fs.existsSync(path.join(temp, ".gitignore"))).toBe(false);
Expand Down
Loading