From 1de31292fb2625636a6720360d56adfc95ff0240 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Tue, 18 Feb 2025 10:38:37 -0800 Subject: [PATCH] Add react, tailwind, react+tailwind+shadcn to bun init (#17282) Co-authored-by: Dylan Conway Co-authored-by: Dylan Conway <35280289+dylan-conway@users.noreply.github.com> --- src/cli/create_command.zig | 2 +- src/cli/init/README2.default.md | 21 + src/cli/init_command.zig | 800 +++++++++++++++--- src/create/SourceFileProjectGenerator.zig | 150 ++-- src/create/projects/react-app/bunfig.toml | 2 + src/create/projects/react-app/package.json | 21 + .../projects/react-app/src/APITester.tsx | 50 ++ src/create/projects/react-app/src/App.tsx | 24 + .../projects/react-app/src/frontend.tsx | 13 + src/create/projects/react-app/src/index.css | 180 ++++ src/create/projects/react-app/src/index.html | 13 + src/create/projects/react-app/src/index.tsx | 35 + src/create/projects/react-app/src/logo.svg | 1 + src/create/projects/react-app/src/react.svg | 8 + src/create/projects/react-app/tsconfig.json | 16 + ...LACE_ME_WITH_YOUR_APP_FILE_NAME.client.tsx | 14 +- src/create/projects/react-shadcn/bunfig.toml | 4 + .../projects/react-shadcn/components.json | 21 + src/create/projects/react-shadcn/package.json | 34 + .../projects/react-shadcn/src/APITester.tsx | 80 ++ src/create/projects/react-shadcn/src/App.tsx | 41 + .../react-shadcn/src/components/ui/button.tsx | 58 ++ .../react-shadcn/src/components/ui/card.tsx | 68 ++ .../react-shadcn/src/components/ui/form.tsx | 165 ++++ .../react-shadcn/src/components/ui/input.tsx | 19 + .../react-shadcn/src/components/ui/label.tsx | 24 + .../react-shadcn/src/components/ui/select.tsx | 179 ++++ .../projects/react-shadcn/src/frontend.tsx | 6 + .../projects/react-shadcn/src/index.css | 43 + .../projects/react-shadcn/src/index.html | 13 + .../projects/react-shadcn/src/index.tsx | 35 + .../projects/react-shadcn/src/lib/utils.ts | 6 + src/create/projects/react-shadcn/src/logo.svg | 1 + .../projects/react-shadcn/src/react.svg | 8 + .../projects/react-shadcn/src/types.d.ts | 4 + .../projects/react-shadcn/styles/globals.css | 144 ++++ .../projects/react-shadcn/tsconfig.json | 19 + .../projects/react-tailwind/bunfig.toml | 4 + .../projects/react-tailwind/package.json | 23 + .../projects/react-tailwind/src/APITester.tsx | 63 ++ .../projects/react-tailwind/src/App.tsx | 36 + .../projects/react-tailwind/src/frontend.tsx | 13 + .../projects/react-tailwind/src/index.css | 42 + .../projects/react-tailwind/src/index.html | 13 + .../projects/react-tailwind/src/index.tsx | 35 + .../projects/react-tailwind/src/logo.svg | 1 + .../projects/react-tailwind/src/react.svg | 8 + .../projects/react-tailwind/tsconfig.json | 16 + src/logo.svg | 1 + src/output.zig | 7 + src/sys.zig | 24 + .../__snapshots__/create-jsx.test.ts.snap | 45 +- test/cli/create/create-jsx.test.ts | 248 +++--- .../create/react-spa-no-tailwind/index.jsx | 7 +- 54 files changed, 2569 insertions(+), 339 deletions(-) create mode 100644 src/cli/init/README2.default.md create mode 100644 src/create/projects/react-app/bunfig.toml create mode 100644 src/create/projects/react-app/package.json create mode 100644 src/create/projects/react-app/src/APITester.tsx create mode 100644 src/create/projects/react-app/src/App.tsx create mode 100644 src/create/projects/react-app/src/frontend.tsx create mode 100644 src/create/projects/react-app/src/index.css create mode 100644 src/create/projects/react-app/src/index.html create mode 100644 src/create/projects/react-app/src/index.tsx create mode 100644 src/create/projects/react-app/src/logo.svg create mode 100644 src/create/projects/react-app/src/react.svg create mode 100644 src/create/projects/react-app/tsconfig.json create mode 100644 src/create/projects/react-shadcn/bunfig.toml create mode 100644 src/create/projects/react-shadcn/components.json create mode 100644 src/create/projects/react-shadcn/package.json create mode 100644 src/create/projects/react-shadcn/src/APITester.tsx create mode 100644 src/create/projects/react-shadcn/src/App.tsx create mode 100644 src/create/projects/react-shadcn/src/components/ui/button.tsx create mode 100644 src/create/projects/react-shadcn/src/components/ui/card.tsx create mode 100644 src/create/projects/react-shadcn/src/components/ui/form.tsx create mode 100644 src/create/projects/react-shadcn/src/components/ui/input.tsx create mode 100644 src/create/projects/react-shadcn/src/components/ui/label.tsx create mode 100644 src/create/projects/react-shadcn/src/components/ui/select.tsx create mode 100644 src/create/projects/react-shadcn/src/frontend.tsx create mode 100644 src/create/projects/react-shadcn/src/index.css create mode 100644 src/create/projects/react-shadcn/src/index.html create mode 100644 src/create/projects/react-shadcn/src/index.tsx create mode 100644 src/create/projects/react-shadcn/src/lib/utils.ts create mode 100644 src/create/projects/react-shadcn/src/logo.svg create mode 100644 src/create/projects/react-shadcn/src/react.svg create mode 100644 src/create/projects/react-shadcn/src/types.d.ts create mode 100644 src/create/projects/react-shadcn/styles/globals.css create mode 100644 src/create/projects/react-shadcn/tsconfig.json create mode 100644 src/create/projects/react-tailwind/bunfig.toml create mode 100644 src/create/projects/react-tailwind/package.json create mode 100644 src/create/projects/react-tailwind/src/APITester.tsx create mode 100644 src/create/projects/react-tailwind/src/App.tsx create mode 100644 src/create/projects/react-tailwind/src/frontend.tsx create mode 100644 src/create/projects/react-tailwind/src/index.css create mode 100644 src/create/projects/react-tailwind/src/index.html create mode 100644 src/create/projects/react-tailwind/src/index.tsx create mode 100644 src/create/projects/react-tailwind/src/logo.svg create mode 100644 src/create/projects/react-tailwind/src/react.svg create mode 100644 src/create/projects/react-tailwind/tsconfig.json create mode 100644 src/logo.svg diff --git a/src/cli/create_command.zig b/src/cli/create_command.zig index e277e15052..3d8e339bc6 100644 --- a/src/cli/create_command.zig +++ b/src/cli/create_command.zig @@ -267,7 +267,7 @@ pub const CreateCommand = struct { break :brk positionals[1]; }; - const destination = try filesystem.dirname_store.append([]const u8, resolve_path.joinAbs(filesystem.top_level_dir, .auto, dirname)); + const destination = try filesystem.dirname_store.append([]const u8, resolve_path.joinAbs(filesystem.top_level_dir, .loose, dirname)); var progress = Progress{}; progress.supports_ansi_escape_codes = Output.enable_ansi_colors_stderr; diff --git a/src/cli/init/README2.default.md b/src/cli/init/README2.default.md new file mode 100644 index 0000000000..207f6af124 --- /dev/null +++ b/src/cli/init/README2.default.md @@ -0,0 +1,21 @@ +# {[name]s} + +To install dependencies: + +```bash +bun install +``` + +To start a development server: + +```bash +bun dev +``` + +To run for production: + +```bash +bun start +``` + +This project was created using `bun init` in bun v{[bunVersion]s}. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. diff --git a/src/cli/init_command.zig b/src/cli/init_command.zig index 9506ef0b55..13b51a555b 100644 --- a/src/cli/init_command.zig +++ b/src/cli/init_command.zig @@ -23,13 +23,14 @@ const logger = bun.logger; const JSPrinter = bun.js_printer; const exists = bun.sys.exists; const existsZ = bun.sys.existsZ; +const SourceFileProjectGenerator = @import("../create/SourceFileProjectGenerator.zig"); pub const InitCommand = struct { pub fn prompt( alloc: std.mem.Allocator, comptime label: string, default: []const u8, - ) ![]const u8 { + ) ![:0]const u8 { Output.pretty(label, .{}); if (default.len > 0) { Output.pretty("({s}): ", .{default}); @@ -48,22 +49,202 @@ pub const InitCommand = struct { } }; - var input = try bun.Output.buffered_stdin.reader().readUntilDelimiterAlloc(alloc, '\n', 1024); - if (strings.endsWithChar(input, '\r')) { - input = input[0 .. input.len - 1]; + var input: std.ArrayList(u8) = .init(alloc); + try bun.Output.buffered_stdin.reader().readUntilDelimiterArrayList(&input, '\n', 1024); + + if (strings.endsWithChar(input.items, '\r')) { + _ = input.pop(); } - if (input.len > 0) { - return input; + if (input.items.len > 0) { + try input.append(0); + return input.items[0 .. input.items.len - 1 :0]; } else { - return default; + input.clearRetainingCapacity(); + try input.appendSlice(default); + try input.append(0); + return input.items[0 .. input.items.len - 1 :0]; } } + extern fn Bun__ttySetMode(fd: i32, mode: i32) i32; + + fn processRadioButton( + label: string, + comptime choices: []const []const u8, + comptime choices_uncolored: []const []const u8, + default_value: usize, + comptime colors: bool, + ) !usize { + // Print the question prompt + if (colors) { + Output.prettyln("? {s} - Press return to submit.", .{label}); + } else { + Output.prettyln("? {s} - Press return to submit.", .{label}); + } + + var selected = default_value; + var initial_draw = true; + var reprint_menu = true; + errdefer reprint_menu = false; + defer { + if (!initial_draw) { + // Move cursor up to prompt line + Output.up(choices.len + 1); + } + + // Clear from cursor to end of screen + Output.clearToEnd(); + + if (reprint_menu) { + // Print final selection + if (colors) { + Output.prettyln("? {s} โ€บ {s}", .{ label, choices[selected] }); + } else { + Output.prettyln("? {s} > {s}", .{ label, choices_uncolored[selected] }); + } + } + } + + while (true) { + if (!initial_draw) { + // Move cursor up by number of choices + Output.up(choices.len); + } + initial_draw = false; + + // Clear from cursor to end of screen + Output.clearToEnd(); + + // Print options vertically + inline for (choices, choices_uncolored, 0..) |option_colored, option_uncolored, i| { + const option = if (colors) option_colored else option_uncolored; + if (i == selected) { + if (colors) { + Output.pretty("โฏ ", .{}); + } else { + Output.pretty("> ", .{}); + } + if (colors) { + Output.print("\x1B[4m" ++ option ++ "\x1B[24m\n", .{}); + } else { + Output.print(" " ++ option ++ "\n", .{}); + } + } else { + Output.print(" " ++ option ++ "\n", .{}); + } + } + + Output.flush(); + + // Read a single character + const byte = std.io.getStdIn().reader().readByte() catch return selected; + + switch (byte) { + '\n', '\r' => { + return selected; + }, + 3, 4 => return error.EndOfStream, // ctrl+c, ctrl+d + '1'...'9' => { + const choice = byte - '1'; + if (choice < choices.len) { + return choice; + } + }, + 'j' => { + if (selected == choices.len - 1) { + selected = 0; + } else { + selected += 1; + } + }, + 'k' => { + if (selected == 0) { + selected = choices.len - 1; + } else { + selected -= 1; + } + }, + 27 => { // ESC sequence + // Return immediately on plain ESC + const next = std.io.getStdIn().reader().readByte() catch return error.EndOfStream; + if (next != '[') return error.EndOfStream; + + // Read arrow key + const arrow = std.io.getStdIn().reader().readByte() catch return error.EndOfStream; + switch (arrow) { + 'A' => { // Up arrow + if (selected == 0) { + selected = choices.len - 1; + } else { + selected -= 1; + } + }, + 'B' => { // Down arrow + if (selected == choices.len - 1) { + selected = 0; + } else { + selected += 1; + } + }, + else => {}, + } + }, + else => {}, + } + } + } + + pub fn radio( + label: string, + comptime choices: []const []const u8, + comptime choices_uncolored: []const []const u8, + default_value: usize, + comptime colors: bool, + ) !usize { + + // Set raw mode to read single characters without echo + const original_mode: if (Environment.isWindows) ?bun.windows.DWORD else void = if (comptime Environment.isWindows) + bun.win32.unsetStdioModeFlags(0, bun.windows.ENABLE_VIRTUAL_TERMINAL_INPUT) catch null; + + if (Environment.isPosix) + _ = Bun__ttySetMode(0, 1); + + defer { + if (comptime Environment.isWindows) { + if (original_mode) |mode| { + _ = bun.windows.SetConsoleMode( + bun.win32.STDIN_FD.cast(), + mode, + ); + } + } + if (Environment.isPosix) { + _ = Bun__ttySetMode(0, 0); + } + } + + const selection = processRadioButton(label, choices, choices_uncolored, default_value, colors) catch |err| { + if (err == error.EndOfStream) { + Output.flush(); + // Add an "x" cancelled + Output.prettyln("\nx Cancelled", .{}); + Global.exit(0); + } + + return err; + }; + + Output.flush(); + + return selection; + } + const Assets = struct { // "known" assets const @".gitignore" = @embedFile("init/gitignore.default"); const @"tsconfig.json" = @embedFile("init/tsconfig.default.json"); const @"README.md" = @embedFile("init/README.default.md"); + const @"README2.md" = @embedFile("init/README2.default.md"); /// Create a new asset file, overriding anything that already exists. Known /// assets will have their contents pre-populated; otherwise the file will be empty. @@ -72,10 +253,16 @@ pub const InitCommand = struct { return createFull(asset_name, asset_name, "", is_template, args); } - fn createNew(filename: []const u8, contents: []const u8) !void { - var file = try std.fs.cwd().createFile(filename, .{ .truncate = true }); + pub fn createWithContents(comptime asset_name: []const u8, comptime contents: []const u8, args: anytype) !void { + const is_template = comptime (@TypeOf(args) != @TypeOf(null)) and @typeInfo(@TypeOf(args)).@"struct".fields.len > 0; + return createFullWithContents(asset_name, contents, "", is_template, args); + } + + fn createNew(filename: [:0]const u8, contents: []const u8) !void { + const file = try bun.sys.File.makeOpen(filename, bun.O.CREAT | bun.O.EXCL | bun.O.WRONLY, 0o666).unwrap(); defer file.close(); - try file.writeAll(contents); + + try file.writeAll(contents).unwrap(); Output.prettyln(" + {s}", .{filename}); Output.flush(); @@ -104,6 +291,31 @@ pub const InitCommand = struct { } else { try file.writeAll(asset); } + Output.prettyln(" + {s}{s}", .{ filename, message_suffix }); + Output.flush(); + } else { + @compileError("missing asset: " ++ asset_name); + } + } + + fn createFullWithContents( + /// name of asset file to create + filename: []const u8, + comptime contents: []const u8, + /// optionally add a suffix to the end of the `+ filename` message. Must have a leading space. + comptime message_suffix: []const u8, + /// Treat the asset as a format string, using `args` to populate it. Only applies to known assets. + comptime is_template: bool, + /// Format arguments + args: anytype, + ) !void { + var file = try std.fs.cwd().createFile(filename, .{ .truncate = true }); + defer file.close(); + + if (comptime is_template) { + try file.writer().print(contents, args); + } else { + try file.writeAll(contents); } Output.prettyln(" + {s}{s}", .{ filename, message_suffix }); @@ -143,7 +355,8 @@ pub const InitCommand = struct { name: string = "project", type: string = "module", object: *js_ast.E.Object = undefined, - entry_point: string = "", + entry_point: stringZ = "", + private: bool = true, }; pub fn exec(alloc: std.mem.Allocator, argv: [][:0]const u8) !void { @@ -235,7 +448,7 @@ pub const InitCommand = struct { } if (package_json_expr.get("module") orelse package_json_expr.get("main")) |name| { - if (name.asString(alloc)) |str| { + if (try name.asStringZ(alloc)) |str| { fields.entry_point = str; } } @@ -257,7 +470,7 @@ pub const InitCommand = struct { for (paths_to_try) |path| { if (existsZ(path)) { - fields.entry_point = bun.asByteSlice(path); + fields.entry_point = path; break :infer; } } @@ -284,38 +497,114 @@ pub const InitCommand = struct { break :brk false; }; + var template: Template = .blank; + if (!auto_yes) { if (!did_load_package_json) { - Output.prettyln("bun init helps you get started with a minimal project and tries to guess sensible defaults. Press ^C anytime to quit\n\n", .{}); - Output.flush(); + Output.pretty("\n", .{}); - const name = prompt( - alloc, - "package name ", - fields.name, - ) catch |err| { - if (err == error.EndOfStream) return; - return err; + const choices = &[_][]const u8{ + "TypeScript", + "React", + "TypeScript Library", + }; + const choices_colored = &[_][]const u8{ + comptime Output.prettyFmt("TypeScript (blank)", true), + comptime Output.prettyFmt("React", true), + comptime Output.prettyFmt("TypeScript (library)", true), }; - fields.name = try normalizePackageName(alloc, name); - - fields.entry_point = prompt( - alloc, - "entry point ", - fields.entry_point, - ) catch |err| { - if (err == error.EndOfStream) return; - return err; + const selected = switch (Output.enable_ansi_colors_stdout) { + inline else => |colors| try radio( + "Select a project", + choices_colored, + choices, + 0, + colors, + ), }; + switch (selected) { + 2 => { + template = .typescript_library; + fields.name = prompt( + alloc, + "package name ", + fields.name, + ) catch |err| { + if (err == error.EndOfStream) return; + return err; + }; + fields.name = try normalizePackageName(alloc, fields.name); + fields.entry_point = prompt( + alloc, + "entry point ", + fields.entry_point, + ) catch |err| { + if (err == error.EndOfStream) return; + return err; + }; + fields.private = false; + }, + 1 => { + const react_choices = &[_][]const u8{ + "Default (blank)", + "Tailwind CSS", + "Shadcn UI + Tailwind CSS", + }; + const react_choices_colored = &[_][]const u8{ + "Default (blank)", + // Tailwind CSS + "\x1B[36mTailwind CSS\x1B[39m", + // Shadcn + Tailwind CSS + "\x1B[32mshadcn + Tailwind CSS\x1B[39m\x1B[0m", + }; + + const react_selected = switch (Output.enable_ansi_colors_stdout) { + inline else => |colors| try radio( + "Select a React template", + react_choices_colored, + react_choices, + 0, + colors, + ), + }; + + switch (react_selected) { + 0 => { + template = .react_blank; + }, + 1 => { + template = .react_tailwind; + }, + 2 => { + template = .react_tailwind_shadcn; + }, + else => unreachable, + } + }, + 0 => { + template = .blank; + }, + else => unreachable, + } + try Output.writer().writeAll("\n"); Output.flush(); } else { - Output.prettyln("A package.json was found here. Would you like to configure", .{}); + Output.errGeneric("A package.json already exists here", .{}); + Global.crash(); } } + switch (template) { + inline .react_blank, .react_tailwind, .react_tailwind_shadcn => |t| { + try t.@"write files and run `bun dev`"(alloc); + return; + }, + else => {}, + } + const Steps = struct { write_gitignore: bool = true, write_package_json: bool = true, @@ -355,14 +644,41 @@ pub const InitCommand = struct { } } - const needs_dev_dependencies = brk: { - if (fields.object.get("devDependencies")) |deps| { - if (deps.hasAnyPropertyNamed(&.{"bun-types"})) { - break :brk false; + if (fields.private) { + try fields.object.put(alloc, "private", js_ast.Expr.init(js_ast.E.Boolean, .{ .value = true }, logger.Loc.Empty)); + } + } + { + const all_dependencies = template.dependencies(); + const dependencies = all_dependencies.dependencies; + const dev_dependencies = all_dependencies.devDependencies; + var needed_dependencies = bun.bit_set.IntegerBitSet(64).initEmpty(); + var needed_dev_dependencies = bun.bit_set.IntegerBitSet(64).initEmpty(); + needed_dependencies.setRangeValue(.{ .start = 0, .end = dependencies.len }, true); + needed_dev_dependencies.setRangeValue(.{ .start = 0, .end = dev_dependencies.len }, true); + + const needs_dependencies = brk: { + if (fields.object.get("dependencies")) |deps| { + for (dependencies, 0..) |*dep, i| { + if (deps.get(dep.name) != null) { + needed_dependencies.unset(i); + } } } - break :brk true; + break :brk needed_dependencies.count() > 0; + }; + + const needs_dev_dependencies = brk: { + if (fields.object.get("devDependencies")) |deps| { + for (dev_dependencies, 0..) |*dep, i| { + if (deps.get(dep.name) != null) { + needed_dev_dependencies.unset(i); + } + } + } + + break :brk needed_dev_dependencies.count() > 0; }; const needs_typescript_dependency = brk: { @@ -381,19 +697,37 @@ pub const InitCommand = struct { break :brk true; }; + if (needs_dependencies) { + var dependencies_object = fields.object.get("dependencies") orelse js_ast.Expr.init(js_ast.E.Object, js_ast.E.Object{}, logger.Loc.Empty); + var iter = needed_dependencies.iterator(.{ .kind = .set }); + while (iter.next()) |index| { + const dep = dependencies[index]; + try dependencies_object.data.e_object.putString(alloc, dep.name, dep.version); + } + try fields.object.put(alloc, "dependencies", dependencies_object); + } + if (needs_dev_dependencies) { - var dev_dependencies = fields.object.get("devDependencies") orelse js_ast.Expr.init(js_ast.E.Object, js_ast.E.Object{}, logger.Loc.Empty); - try dev_dependencies.data.e_object.putString(alloc, "@types/bun", "latest"); - try fields.object.put(alloc, "devDependencies", dev_dependencies); + var object = fields.object.get("devDependencies") orelse js_ast.Expr.init(js_ast.E.Object, js_ast.E.Object{}, logger.Loc.Empty); + var iter = needed_dev_dependencies.iterator(.{ .kind = .set }); + while (iter.next()) |index| { + const dep = dev_dependencies[index]; + try object.data.e_object.putString(alloc, dep.name, dep.version); + } + try fields.object.put(alloc, "devDependencies", object); } if (needs_typescript_dependency) { - var peer_dependencies = fields.object.get("peer_dependencies") orelse js_ast.Expr.init(js_ast.E.Object, js_ast.E.Object{}, logger.Loc.Empty); - try peer_dependencies.data.e_object.putString(alloc, "typescript", "^5.0.0"); + var peer_dependencies = fields.object.get("peerDependencies") orelse js_ast.Expr.init(js_ast.E.Object, js_ast.E.Object{}, logger.Loc.Empty); + try peer_dependencies.data.e_object.putString(alloc, "typescript", "^5"); try fields.object.put(alloc, "peerDependencies", peer_dependencies); } } + if (template.isReact()) { + try template.writeToPackageJson(alloc, &fields); + } + write_package_json: { if (package_json_file == null) { package_json_file = try std.fs.cwd().createFileZ("package.json", .{}); @@ -416,77 +750,337 @@ pub const InitCommand = struct { package_json_file.?.close(); } - if (package_json_file != null) { - Output.prettyln("Done! A package.json file was saved in the current directory.", .{}); - } - - if (fields.entry_point.len > 0 and !exists(fields.entry_point)) { - const cwd = std.fs.cwd(); - if (std.fs.path.dirname(fields.entry_point)) |dirname| { - if (!strings.eqlComptime(dirname, ".")) { - cwd.makePath(dirname) catch {}; - } - } - - Assets.createNew(fields.entry_point, "console.log(\"Hello via Bun!\");") catch { - // suppress - }; - } - if (steps.write_gitignore) { Assets.create(".gitignore", .{}) catch { // suppressed }; } - if (steps.write_tsconfig) { - brk: { - const extname = std.fs.path.extension(fields.entry_point); - const loader = options.defaultLoaders.get(extname) orelse options.Loader.ts; - const filename = if (loader.isTypeScript()) - "tsconfig.json" - else - "jsconfig.json"; - Assets.createFull("tsconfig.json", filename, " (for editor autocomplete)", false, .{}) catch break :brk; - } - } + switch (template) { + .blank, .typescript_library => { + if (package_json_file != null) { + Output.prettyln(" + package.json", .{}); + Output.flush(); + } - if (steps.write_readme) { - Assets.create("README.md", .{ - .name = fields.name, - .bunVersion = Environment.version_string, - .entryPoint = fields.entry_point, - }) catch { - // suppressed - }; - } + if (fields.entry_point.len > 0 and !exists(fields.entry_point)) { + const cwd = std.fs.cwd(); + if (std.fs.path.dirname(fields.entry_point)) |dirname| { + if (!strings.eqlComptime(dirname, ".")) { + cwd.makePath(dirname) catch {}; + } + } - if (fields.entry_point.len > 0) { - Output.prettyln("\nTo get started, run:", .{}); - if (strings.containsAny( - " \"'", - fields.entry_point, - )) { - Output.prettyln(" bun run {any}", .{bun.fmt.formatJSONStringLatin1(fields.entry_point)}); - } else { - Output.prettyln(" bun run {s}", .{fields.entry_point}); - } - } + Assets.createNew(fields.entry_point, "console.log(\"Hello via Bun!\");") catch { + // suppress + }; + } - Output.flush(); + if (steps.write_tsconfig) { + brk: { + const extname = std.fs.path.extension(fields.entry_point); + const loader = options.defaultLoaders.get(extname) orelse options.Loader.ts; + const filename = if (loader.isTypeScript()) + "tsconfig.json" + else + "jsconfig.json"; + Assets.createFull("tsconfig.json", filename, " (for editor autocomplete)", false, .{}) catch break :brk; + } + } - if (existsZ("package.json")) { - var process = std.process.Child.init( - &.{ - try bun.selfExePath(), - "install", - }, - alloc, - ); - process.stderr_behavior = .Ignore; - process.stdin_behavior = .Ignore; - process.stdout_behavior = .Ignore; - _ = try process.spawnAndWait(); + if (steps.write_readme) { + Assets.create("README.md", .{ + .name = fields.name, + .bunVersion = Environment.version_string, + .entryPoint = fields.entry_point, + }) catch { + // suppressed + }; + } + + if (fields.entry_point.len > 0) { + Output.pretty("\nTo get started, run:\n\n\t", .{}); + if (strings.containsAny( + " \"'", + fields.entry_point, + )) { + Output.prettyln("bun run {any}", .{bun.fmt.formatJSONStringLatin1(fields.entry_point)}); + } else { + Output.prettyln("bun run {s}", .{fields.entry_point}); + } + } + + Output.flush(); + + if (existsZ("package.json")) { + var process = std.process.Child.init( + &.{ + try bun.selfExePath(), + "install", + }, + alloc, + ); + process.stderr_behavior = .Ignore; + process.stdin_behavior = .Ignore; + process.stdout_behavior = .Ignore; + _ = try process.spawnAndWait(); + } + }, + else => {}, } } }; + +const DependencyNeeded = struct { + name: []const u8, + version: []const u8, +}; + +const DependencyGroup = struct { + dependencies: []const DependencyNeeded, + devDependencies: []const DependencyNeeded, + + pub const blank = DependencyGroup{ + .dependencies = &[_]DependencyNeeded{}, + .devDependencies = &[_]DependencyNeeded{ + .{ .name = "@types/bun", .version = "latest" }, + }, + }; + + pub const react = DependencyGroup{ + .dependencies = &[_]DependencyNeeded{ + .{ .name = "react", .version = "^19" }, + .{ .name = "react-dom", .version = "^19" }, + }, + .devDependencies = &[_]DependencyNeeded{ + .{ .name = "@types/react", .version = "^19" }, + .{ .name = "@types/react-dom", .version = "^19" }, + } ++ blank.devDependencies[0..1].*, + }; + + pub const tailwind = DependencyGroup{ + .dependencies = &[_]DependencyNeeded{ + .{ .name = "tailwindcss", .version = "^4" }, + } ++ react.dependencies[0..react.dependencies.len].*, + .devDependencies = &[_]DependencyNeeded{ + .{ .name = "bun-plugin-tailwind", .version = "latest" }, + } ++ react.devDependencies[0..react.devDependencies.len].*, + }; + + pub const shadcn = DependencyGroup{ + .dependencies = &[_]DependencyNeeded{ + .{ .name = "tailwindcss-animate", .version = "latest" }, + .{ .name = "class-variance-authority", .version = "latest" }, + .{ .name = "clsx", .version = "latest" }, + .{ .name = "tailwind-merge", .version = "latest" }, + } ++ tailwind.dependencies[0..tailwind.dependencies.len].*, + .devDependencies = &[_]DependencyNeeded{} ++ tailwind.devDependencies[0..tailwind.devDependencies.len].*, + }; +}; + +const Template = enum { + blank, + react_blank, + react_tailwind, + react_tailwind_shadcn, + typescript_library, + pub fn shouldUseSourceFileProjectGenerator(this: Template) bool { + return switch (this) { + .blank, .typescript_library => false, + else => true, + }; + } + pub fn isReact(this: Template) bool { + return switch (this) { + .react_blank, .react_tailwind, .react_tailwind_shadcn => true, + else => false, + }; + } + pub fn writeToPackageJson(this: Template, alloc: std.mem.Allocator, fields: *InitCommand.PackageJSONFields) !void { + const Rope = js_ast.E.Object.Rope; + fields.name = this.name(); + const key = try alloc.create(Rope); + key.* = Rope{ + .head = js_ast.Expr.init(js_ast.E.String, js_ast.E.String{ .data = "scripts" }, logger.Loc.Empty), + .next = null, + }; + var scripts_json = try fields.object.getOrPutObject(key, alloc); + const the_scripts = this.scripts(); + var i: usize = 0; + while (i < the_scripts.len) : (i += 2) { + const script_name = the_scripts[i]; + const script_command = the_scripts[i + 1]; + + try scripts_json.data.e_object.putString(alloc, script_name, script_command); + } + } + pub fn dependencies(this: Template) DependencyGroup { + return switch (this) { + .blank => DependencyGroup.blank, + .react_blank => DependencyGroup.react, + .react_tailwind => DependencyGroup.tailwind, + .react_tailwind_shadcn => DependencyGroup.shadcn, + .typescript_library => DependencyGroup.blank, + }; + } + pub fn name(this: Template) []const u8 { + return switch (this) { + .blank => "bun-blank-template", + .typescript_library => "bun-typescript-library-template", + .react_blank => "bun-react-template", + .react_tailwind => "bun-react-tailwind-template", + .react_tailwind_shadcn => "bun-react-tailwind-shadcn-template", + }; + } + pub fn scripts(this: Template) []const []const u8 { + const s: []const []const u8 = switch (this) { + .blank, .typescript_library => &.{}, + .react_tailwind, .react_tailwind_shadcn => &.{ + "dev", "bun './**/*.html'", + "build", "bun 'REPLACE_ME_WITH_YOUR_APP_FILE_NAME.build.ts'", + }, + .react_blank => &.{ + "dev", "bun ./src/", + "static", "bun build ./src/index.html --outdir=dist --sourcemap=linked --target=browser --minify --define:process.env.NODE_ENV='\"production\"' --env='BUN_PUBLIC_*'", + }, + }; + + return s; + } + + const ReactBlank = struct { + const files: []const struct { [:0]const u8, [:0]const u8 } = &.{ + .{ "bunfig.toml", @embedFile("../create/projects/react-app/bunfig.toml") }, + .{ "package.json", @embedFile("../create/projects/react-app/package.json") }, + .{ "tsconfig.json", @embedFile("../create/projects/react-app/tsconfig.json") }, + .{ "README.md", InitCommand.Assets.@"README2.md" }, + .{ ".gitignore", InitCommand.Assets.@".gitignore" }, + .{ "src/index.tsx", @embedFile("../create/projects/react-app/src/index.tsx") }, + .{ "src/App.tsx", @embedFile("../create/projects/react-app/src/App.tsx") }, + .{ "src/index.html", @embedFile("../create/projects/react-app/src/index.html") }, + .{ "src/index.css", @embedFile("../create/projects/react-app/src/index.css") }, + .{ "src/APITester.tsx", @embedFile("../create/projects/react-app/src/APITester.tsx") }, + .{ "src/react.svg", @embedFile("../create/projects/react-app/src/react.svg") }, + .{ "src/frontend.tsx", @embedFile("../create/projects/react-app/src/frontend.tsx") }, + .{ "src/logo.svg", @embedFile("../create/projects/react-app/src/logo.svg") }, + }; + }; + + const ReactTailwind = struct { + const files: []const struct { [:0]const u8, [:0]const u8 } = &.{ + .{ "bunfig.toml", @embedFile("../create/projects/react-tailwind/bunfig.toml") }, + .{ "package.json", @embedFile("../create/projects/react-tailwind/package.json") }, + .{ "tsconfig.json", @embedFile("../create/projects/react-tailwind/tsconfig.json") }, + .{ "README.md", InitCommand.Assets.@"README2.md" }, + .{ ".gitignore", InitCommand.Assets.@".gitignore" }, + .{ "src/index.tsx", @embedFile("../create/projects/react-tailwind/src/index.tsx") }, + .{ "src/App.tsx", @embedFile("../create/projects/react-tailwind/src/App.tsx") }, + .{ "src/index.html", @embedFile("../create/projects/react-tailwind/src/index.html") }, + .{ "src/index.css", @embedFile("../create/projects/react-tailwind/src/index.css") }, + .{ "src/APITester.tsx", @embedFile("../create/projects/react-tailwind/src/APITester.tsx") }, + .{ "src/react.svg", @embedFile("../create/projects/react-tailwind/src/react.svg") }, + .{ "src/frontend.tsx", @embedFile("../create/projects/react-tailwind/src/frontend.tsx") }, + .{ "src/logo.svg", @embedFile("../create/projects/react-tailwind/src/logo.svg") }, + }; + }; + + const ReactShadcn = struct { + const files: []const struct { [:0]const u8, [:0]const u8 } = &.{ + .{ "bunfig.toml", @embedFile("../create/projects/react-shadcn/bunfig.toml") }, + .{ "styles/globals.css", @embedFile("../create/projects/react-shadcn/styles/globals.css") }, + .{ "package.json", @embedFile("../create/projects/react-shadcn/package.json") }, + .{ "components.json", @embedFile("../create/projects/react-shadcn/components.json") }, + .{ "tsconfig.json", @embedFile("../create/projects/react-shadcn/tsconfig.json") }, + .{ "README.md", InitCommand.Assets.@"README2.md" }, + .{ ".gitignore", InitCommand.Assets.@".gitignore" }, + .{ "src/index.tsx", @embedFile("../create/projects/react-shadcn/src/index.tsx") }, + .{ "src/App.tsx", @embedFile("../create/projects/react-shadcn/src/App.tsx") }, + .{ "src/index.html", @embedFile("../create/projects/react-shadcn/src/index.html") }, + .{ "src/types.d.ts", @embedFile("../create/projects/react-shadcn/src/types.d.ts") }, + .{ "src/index.css", @embedFile("../create/projects/react-shadcn/src/index.css") }, + .{ "src/components/ui/card.tsx", @embedFile("../create/projects/react-shadcn/src/components/ui/card.tsx") }, + .{ "src/components/ui/label.tsx", @embedFile("../create/projects/react-shadcn/src/components/ui/label.tsx") }, + .{ "src/components/ui/button.tsx", @embedFile("../create/projects/react-shadcn/src/components/ui/button.tsx") }, + .{ "src/components/ui/select.tsx", @embedFile("../create/projects/react-shadcn/src/components/ui/select.tsx") }, + .{ "src/components/ui/input.tsx", @embedFile("../create/projects/react-shadcn/src/components/ui/input.tsx") }, + .{ "src/components/ui/form.tsx", @embedFile("../create/projects/react-shadcn/src/components/ui/form.tsx") }, + .{ "src/APITester.tsx", @embedFile("../create/projects/react-shadcn/src/APITester.tsx") }, + .{ "src/lib/utils.ts", @embedFile("../create/projects/react-shadcn/src/lib/utils.ts") }, + .{ "src/react.svg", @embedFile("../create/projects/react-shadcn/src/react.svg") }, + .{ "src/frontend.tsx", @embedFile("../create/projects/react-shadcn/src/frontend.tsx") }, + .{ "src/logo.svg", @embedFile("../create/projects/react-shadcn/src/logo.svg") }, + }; + }; + + pub fn files(this: Template) []const struct { [:0]const u8, []const u8 } { + return switch (this) { + .react_blank => ReactBlank.files, + .react_tailwind => ReactTailwind.files, + .react_tailwind_shadcn => ReactTailwind.files, + else => &.{.{ &.{}, &.{} }}, + }; + } + + pub fn @"write files and run `bun dev`"(comptime this: Template, allocator: std.mem.Allocator) !void { + inline for (comptime this.files()) |file| { + const path, const contents = file; + + if (comptime strings.eqlComptime(path, "README.md")) { + InitCommand.Assets.createWithContents("README.md", contents, .{ + .name = this.name(), + .bunVersion = Environment.version_string, + }) catch |err| { + if (err == error.EEXISTS) { + Output.errGeneric("'{s}' already exists", .{path}); + } else { + Output.err(err, "failed to create file: '{s}'", .{path}); + } + Global.crash(); + }; + } else { + InitCommand.Assets.createNew(path, contents) catch |err| { + if (err == error.EEXISTS) { + Output.errGeneric("'{s}' already exists", .{path}); + } else { + Output.err(err, "failed to create file: '{s}'", .{path}); + } + Global.crash(); + }; + } + } + + Output.pretty("\n", .{}); + Output.flush(); + + var install = std.process.Child.init( + &.{ + try bun.selfExePath(), + "install", + }, + allocator, + ); + install.stderr_behavior = .Inherit; + install.stdin_behavior = .Ignore; + install.stdout_behavior = .Inherit; + + _ = try install.spawnAndWait(); + + Output.prettyln("\nTo get started, run:\n\n\tbun dev", .{}); + Output.prettyln("\nTo run for production:\n\n\tbun start\n\n", .{}); + + Output.flush(); + + var process = std.process.Child.init( + &.{ + try bun.selfExePath(), + "dev", + }, + allocator, + ); + process.stderr_behavior = .Inherit; + process.stdin_behavior = .Inherit; + process.stdout_behavior = .Inherit; + + _ = try process.spawnAndWait(); + } +}; diff --git a/src/create/SourceFileProjectGenerator.zig b/src/create/SourceFileProjectGenerator.zig index 521a388e18..4da33c2857 100644 --- a/src/create/SourceFileProjectGenerator.zig +++ b/src/create/SourceFileProjectGenerator.zig @@ -53,19 +53,11 @@ pub fn generate(_: Command.Context, _: Example.Tag, entry_point: string, result: // We are JSX-only for now. // The versions of react & react-dom need to match up, and it's SO easy to mess that up. // So we have to be a little opinionated here. - if (needs_to_inject_shadcn_ui) { - // Use react 18 instead of 19 if shadcn is in use. - _ = result.dependencies.swapRemove("react"); - _ = result.dependencies.swapRemove("react-dom"); - try result.dependencies.insert("react@^18"); - try result.dependencies.insert("react-dom@^18"); - } else { - // Add react-dom if react is used - _ = result.dependencies.swapRemove("react"); - _ = result.dependencies.swapRemove("react-dom"); - try result.dependencies.insert("react-dom@19"); - try result.dependencies.insert("react@19"); - } + // Add react-dom if react is used + _ = result.dependencies.swapRemove("react"); + _ = result.dependencies.swapRemove("react-dom"); + try result.dependencies.insert("react-dom@19"); + try result.dependencies.insert("react@19"); // Choose template based on dependencies and example type const template: Template = brk: { @@ -79,7 +71,7 @@ pub fn generate(_: Command.Context, _: Example.Tag, entry_point: string, result: }; // Generate project files from template - try generateFiles(default_allocator, entry_point, result, template, react_component_export); + try generateFiles(default_allocator, entry_point, result.dependencies.keys(), template, react_component_export); Global.exit(0); } @@ -168,7 +160,7 @@ fn stringWithReplacements(original_input: []const u8, basename: []const u8, rela } // Generate all project files from template -fn generateFiles(allocator: std.mem.Allocator, entry_point: string, result: *BundleV2.DependenciesScanner.Result, template: Template, react_component_export: []const u8) !void { +pub fn generateFiles(allocator: std.mem.Allocator, entry_point: string, dependencies: []const []const u8, template: Template, react_component_export: []const u8) !void { var log = template.logger(); var basename = std.fs.path.basename(entry_point); const extension = std.fs.path.extension(basename); @@ -179,9 +171,9 @@ fn generateFiles(allocator: std.mem.Allocator, entry_point: string, result: *Bun // Normalize file paths var normalized_buf: bun.PathBuffer = undefined; var normalized_name: []const u8 = if (std.fs.path.isAbsolute(entry_point)) - bun.path.relativeNormalizedBuf(&normalized_buf, bun.fs.FileSystem.instance.top_level_dir, entry_point, .posix, true) + bun.path.relativeNormalizedBuf(&normalized_buf, bun.fs.FileSystem.instance.top_level_dir, entry_point, .loose, true) else - bun.path.normalizeBuf(entry_point, &normalized_buf, .posix); + bun.path.normalizeBuf(entry_point, &normalized_buf, .loose); if (extension.len > 0) { normalized_name = normalized_name[0 .. normalized_name.len - extension.len]; @@ -226,60 +218,62 @@ fn generateFiles(allocator: std.mem.Allocator, entry_point: string, result: *Bun }, } - // Install dependencies - var argv = std.ArrayList([]const u8).init(default_allocator); - try argv.append("bun"); - try argv.append("--only-missing"); - try argv.append("install"); - try argv.appendSlice(result.dependencies.keys()); - if (log.has_written_initial_message) { - Output.print("\n", .{}); - } - Output.pretty("๐Ÿ“ฆ Auto-installing {d} detected dependencies\n", .{result.dependencies.keys().len}); + if (dependencies.len > 0) { + // Install dependencies + var argv = std.ArrayList([]const u8).init(default_allocator); + try argv.append("bun"); + try argv.append("--only-missing"); + try argv.append("install"); + try argv.appendSlice(dependencies); + if (log.has_written_initial_message) { + Output.print("\n", .{}); + } + Output.pretty("๐Ÿ“ฆ Auto-installing {d} detected dependencies\n", .{dependencies.len}); - // print "bun" but use bun.selfExePath() - Output.commandOut(argv.items); + // print "bun" but use bun.selfExePath() + Output.commandOut(argv.items); - Output.flush(); + Output.flush(); - argv.items[0] = try bun.selfExePath(); + argv.items[0] = try bun.selfExePath(); - const process = bun.spawnSync(&.{ - .argv = argv.items, - .envp = null, - .cwd = bun.fs.FileSystem.instance.top_level_dir, - .stderr = .inherit, - .stdout = .inherit, - .stdin = .inherit, + const process = bun.spawnSync(&.{ + .argv = argv.items, + .envp = null, + .cwd = bun.fs.FileSystem.instance.top_level_dir, + .stderr = .inherit, + .stdout = .inherit, + .stdin = .inherit, - .windows = if (Environment.isWindows) .{ - .loop = bun.JSC.EventLoopHandle.init(bun.JSC.MiniEventLoop.initGlobal(null)), - }, - }) catch |err| { - Output.err(err, "failed to install dependencies", .{}); - Global.crash(); - }; - - switch (process) { - .err => |err| { + .windows = if (Environment.isWindows) .{ + .loop = bun.JSC.EventLoopHandle.init(bun.JSC.MiniEventLoop.initGlobal(null)), + }, + }) catch |err| { Output.err(err, "failed to install dependencies", .{}); Global.crash(); - }, - .result => |spawn_result| { - if (!spawn_result.status.isOK()) { - if (spawn_result.status.signalCode()) |signal| { - if (signal.toExitCode()) |exit_code| { - Global.exit(exit_code); - } - } - - if (spawn_result.status == .exited) { - Global.exit(spawn_result.status.exited.code); - } + }; + switch (process) { + .err => |err| { + Output.err(err, "failed to install dependencies", .{}); Global.crash(); - } - }, + }, + .result => |spawn_result| { + if (!spawn_result.status.isOK()) { + if (spawn_result.status.signalCode()) |signal| { + if (signal.toExitCode()) |exit_code| { + Global.exit(exit_code); + } + } + + if (spawn_result.status == .exited) { + Global.exit(spawn_result.status.exited.code); + } + + Global.crash(); + } + }, + } } // Show success message and start dev server @@ -672,22 +666,22 @@ const Reason = enum { const ReactTailwindSpa = struct { pub const files = &[_]TemplateFile{ .{ - .name = "REPLACE_ME_WITH_YOUR_APP_FILE_NAME.build.ts", + .name = "src/REPLACE_ME_WITH_YOUR_APP_FILE_NAME.build.ts", .content = shared_build_ts, .reason = .build, }, .{ - .name = "REPLACE_ME_WITH_YOUR_APP_FILE_NAME.css", + .name = "src/REPLACE_ME_WITH_YOUR_APP_FILE_NAME.css", .content = @embedFile("projects/react-tailwind-spa/REPLACE_ME_WITH_YOUR_APP_FILE_NAME.css"), .reason = .css, }, .{ - .name = "REPLACE_ME_WITH_YOUR_APP_FILE_NAME.html", + .name = "src/REPLACE_ME_WITH_YOUR_APP_FILE_NAME.html", .content = shared_html, .reason = .html, }, .{ - .name = "REPLACE_ME_WITH_YOUR_APP_FILE_NAME.client.tsx", + .name = "src/client.tsx", .content = shared_client_tsx, .reason = .bun, }, @@ -704,6 +698,8 @@ const ReactTailwindSpa = struct { .overwrite = false, }, }; + + pub const init_files = &[_]TemplateFile{}; }; const shared_build_ts = @embedFile("projects/react-shadcn-spa/REPLACE_ME_WITH_YOUR_APP_FILE_NAME.build.ts"); @@ -711,32 +707,32 @@ const shared_client_tsx = @embedFile("projects/react-shadcn-spa/REPLACE_ME_WITH_ const shared_html = @embedFile("projects/react-shadcn-spa/REPLACE_ME_WITH_YOUR_APP_FILE_NAME.html"); const shared_package_json = @embedFile("projects/react-shadcn-spa/package.json"); const shared_bunfig_toml = @embedFile("projects/react-shadcn-spa/bunfig.toml"); +const shared_index_tsx = @embedFile("projects/react-spa/index.tsx"); // Template for basic React project const ReactSpa = struct { pub const files = &[_]TemplateFile{ .{ - .name = "REPLACE_ME_WITH_YOUR_APP_FILE_NAME.build.ts", + .name = "src/REPLACE_ME_WITH_YOUR_APP_FILE_NAME.build.ts", .content = shared_build_ts, .reason = .build, }, .{ - .name = "REPLACE_ME_WITH_YOUR_APP_FILE_NAME.css", + .name = "src/REPLACE_ME_WITH_YOUR_APP_FILE_NAME.css", .content = @embedFile("projects/react-spa/REPLACE_ME_WITH_YOUR_APP_FILE_NAME.css"), .reason = .css, .overwrite = false, }, .{ - .name = "REPLACE_ME_WITH_YOUR_APP_FILE_NAME.html", + .name = "src/REPLACE_ME_WITH_YOUR_APP_FILE_NAME.html", .content = shared_html, .reason = .html, }, .{ - .name = "REPLACE_ME_WITH_YOUR_APP_FILE_NAME.client.tsx", + .name = "src/client.tsx", .content = shared_client_tsx, .reason = .bun, }, - .{ .name = "package.json", .content = @embedFile("projects/react-spa/package.json"), @@ -760,27 +756,27 @@ const ReactShadcnSpa = struct { .reason = .shadcn, }, .{ - .name = "REPLACE_ME_WITH_YOUR_APP_FILE_NAME.build.ts", + .name = "src/REPLACE_ME_WITH_YOUR_APP_FILE_NAME.build.ts", .content = shared_build_ts, .reason = .bun, }, .{ - .name = "REPLACE_ME_WITH_YOUR_APP_FILE_NAME.client.tsx", + .name = "src/client.tsx", .content = shared_client_tsx, .reason = .bun, }, .{ - .name = "REPLACE_ME_WITH_YOUR_APP_FILE_NAME.css", + .name = "src/REPLACE_ME_WITH_YOUR_APP_FILE_NAME.css", .content = @embedFile("projects/react-shadcn-spa/REPLACE_ME_WITH_YOUR_APP_FILE_NAME.css"), .reason = .css, }, .{ - .name = "REPLACE_ME_WITH_YOUR_APP_FILE_NAME.html", + .name = "src/REPLACE_ME_WITH_YOUR_APP_FILE_NAME.html", .content = shared_html, .reason = .html, }, .{ - .name = "styles/globals.css", + .name = "src/styles/globals.css", .content = @embedFile("projects/react-shadcn-spa/styles/globals.css"), .reason = .shadcn, }, @@ -812,7 +808,7 @@ const ReactShadcnSpa = struct { }; // Template type to handle different project types -const Template = union(Tag) { +pub const Template = union(Tag) { ReactTailwindSpa: void, ReactSpa: void, ReactShadcnSpa: struct { diff --git a/src/create/projects/react-app/bunfig.toml b/src/create/projects/react-app/bunfig.toml new file mode 100644 index 0000000000..9819bf6de1 --- /dev/null +++ b/src/create/projects/react-app/bunfig.toml @@ -0,0 +1,2 @@ +[serve.static] +env = "BUN_PUBLIC_*" \ No newline at end of file diff --git a/src/create/projects/react-app/package.json b/src/create/projects/react-app/package.json new file mode 100644 index 0000000000..c1d1e20464 --- /dev/null +++ b/src/create/projects/react-app/package.json @@ -0,0 +1,21 @@ +{ + "name": "bun-react-template", + "version": "0.1.0", + "private": true, + "type": "module", + "main": "src/index.tsx", + "module": "src/index.tsx", + "scripts": { + "dev": "bun --hot src/index.tsx", + "start": "NODE_ENV=production bun src/index.tsx" + }, + "dependencies": { + "react": "^19", + "react-dom": "^19" + }, + "devDependencies": { + "@types/react": "^19", + "@types/react-dom": "^19", + "@types/bun": "latest" + } +} diff --git a/src/create/projects/react-app/src/APITester.tsx b/src/create/projects/react-app/src/APITester.tsx new file mode 100644 index 0000000000..43ba33e5eb --- /dev/null +++ b/src/create/projects/react-app/src/APITester.tsx @@ -0,0 +1,50 @@ +import React, { useRef, type FormEvent } from "react"; + +export function APITester() { + const responseInputRef = useRef(null); + + const testEndpoint = async (e: FormEvent) => { + e.preventDefault(); + + try { + const form = e.currentTarget; + const formData = new FormData(form); + const endpoint = formData.get("endpoint") as string; + const url = new URL(endpoint, location.href); + const method = formData.get("method") as string; + const res = await fetch(url, { method }); + + const data = await res.json(); + responseInputRef.current!.value = JSON.stringify(data, null, 2); + } catch (error) { + responseInputRef.current!.value = String(error); + } + }; + + return ( +
+
+ + + +
+