diff --git a/docs/cli/init.md b/docs/cli/init.md index 6390f767a7..f136473b9e 100644 --- a/docs/cli/init.md +++ b/docs/cli/init.md @@ -40,3 +40,32 @@ At the end, it runs `bun install` to install `@types/bun`. {% /details %} {% bunCLIUsage command="init" /%} + +## React + +The `--react` flag will scaffold a React project: + +```bash +$ bun init --react +``` + +The `--react` flag accepts the following values: + +- `tailwind` - Scaffold a React project with Tailwind CSS +- `shadcn` - Scaffold a React project with Shadcn/UI and Tailwind CSS + +### React + TailwindCSS + +This will create a React project with Tailwind CSS configured with Bun's bundler and dev server. + +```bash +$ bun init --react=tailwind +``` + +### React + @shadcn/ui + +This will create a React project with shadcn/ui and Tailwind CSS configured with Bun's bundler and dev server. + +```bash +$ bun init --react=shadcn +``` diff --git a/src/cli.zig b/src/cli.zig index 90d01ef73b..e05b30b491 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -2430,10 +2430,15 @@ pub const Command = struct { \\ --help Print this menu \\ -y, --yes Accept all default options \\ -m, --minimal Only initialize type definitions + \\ -r, --react Initialize a React project + \\ --react=tailwind Initialize a React project with TailwindCSS + \\ --react=shadcn Initialize a React project with @shadcn/ui and TailwindCSS \\ \\Examples: \\ bun init \\ bun init --yes + \\ bun init --react + \\ bun init --react=tailwind my-app ; Output.pretty(intro_text ++ "\n", .{}); diff --git a/src/cli/init_command.zig b/src/cli/init_command.zig index 3de145ae89..c14bb65bd9 100644 --- a/src/cli/init_command.zig +++ b/src/cli/init_command.zig @@ -372,6 +372,9 @@ pub const InitCommand = struct { var auto_yes = false; var parse_flags = true; var initialize_in_folder: ?[]const u8 = null; + + var template: Template = .blank; + var prev_flag_was_react = false; for (init_args) |arg_| { const arg = bun.span(arg_); if (parse_flags and arg.len > 0 and arg[0] == '-') { @@ -380,12 +383,27 @@ pub const InitCommand = struct { Global.exit(0); } else if (strings.eqlComptime(arg, "-m") or strings.eqlComptime(arg, "--minimal")) { minimal = true; + prev_flag_was_react = false; } else if (strings.eqlComptime(arg, "-y") or strings.eqlComptime(arg, "--yes")) { auto_yes = true; + prev_flag_was_react = false; } else if (strings.eqlComptime(arg, "--")) { parse_flags = false; + prev_flag_was_react = false; + } else if (strings.eqlComptime(arg, "--react") or strings.eqlComptime(arg, "-r")) { + template = .react_blank; + prev_flag_was_react = true; + auto_yes = true; + } else if ((template == .react_blank and prev_flag_was_react and strings.eqlComptime(arg, "tailwind") or strings.eqlComptime(arg, "--react=tailwind")) or strings.eqlComptime(arg, "r=tailwind")) { + template = .react_tailwind; + prev_flag_was_react = false; + auto_yes = true; + } else if ((template == .react_blank and prev_flag_was_react and strings.eqlComptime(arg, "shadcn") or strings.eqlComptime(arg, "--react=shadcn")) or strings.eqlComptime(arg, "r=shadcn")) { + template = .react_tailwind_shadcn; + prev_flag_was_react = false; + auto_yes = true; } else { - // invalid flag; ignore + prev_flag_was_react = false; } } else { if (initialize_in_folder == null) { @@ -541,8 +559,6 @@ pub const InitCommand = struct { ).data.e_object; } - var template: Template = .blank; - if (!auto_yes) { if (!did_load_package_json) { Output.pretty("\n", .{}); diff --git a/test/cli/init/init.test.ts b/test/cli/init/init.test.ts index 614eb8a4cc..d68476dd5e 100644 --- a/test/cli/init/init.test.ts +++ b/test/cli/init/init.test.ts @@ -218,3 +218,78 @@ test("bun init twice", async () => { `"my edited tsconfig.json"`, ); }); + +test("bun init --react works", () => { + const temp = tmpdirSync(); + + const out = Bun.spawnSync({ + cmd: [bunExe(), "init", "--react"], + cwd: temp, + stdio: ["ignore", "inherit", "inherit"], + env: bunEnv, + }); + + expect(out.signalCode).toBeUndefined(); + expect(out.exitCode).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.tsx"))).toBe(true); + expect(fs.existsSync(path.join(temp, "tsconfig.json"))).toBe(true); +}, 30_000); + +test("bun init --react=tailwind works", () => { + const temp = tmpdirSync(); + + const out = Bun.spawnSync({ + cmd: [bunExe(), "init", "--react=tailwind"], + cwd: temp, + stdio: ["ignore", "inherit", "inherit"], + env: bunEnv, + }); + + expect(out.signalCode).toBeUndefined(); + expect(out.exitCode).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.tsx"))).toBe(true); +}, 30_000); + +test("bun init --react=shadcn works", () => { + const temp = tmpdirSync(); + + const out = Bun.spawnSync({ + cmd: [bunExe(), "init", "--react=shadcn"], + cwd: temp, + stdio: ["ignore", "inherit", "inherit"], + env: bunEnv, + }); + + expect(out.signalCode).toBeUndefined(); + expect(out.exitCode).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.tsx"))).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);