From dcc29d3db670ef84d2ea32708b62f5727b1933b2 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sat, 16 Oct 2021 23:24:23 -0700 Subject: [PATCH] Improve support for Create React App --- README.md | 8 ++- src/cli/create_command.zig | 141 ++++++++++++++++++++++++++++++++++--- 2 files changed, 137 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 62b621bcecd33d..1ad3a5acf415ae 100644 --- a/README.md +++ b/README.md @@ -435,7 +435,13 @@ Create a new React project: bun create react ./app ``` -To see a list of available templates, run +Create from a GitHub repo: + +```bash +bun create ahfarmer/calculator ./app +``` + +To see a list of examples, run: ```bash bun create diff --git a/src/cli/create_command.zig b/src/cli/create_command.zig index e79f8817a18df3..cc486b4a63aced 100644 --- a/src/cli/create_command.zig +++ b/src/cli/create_command.zig @@ -179,6 +179,7 @@ const CreateOptions = struct { overwrite: bool = false, skip_git: bool = false, skip_package_json: bool = false, + positionals: []const string, verbose: bool = false, const params = [_]clap.Param(clap.Help){ @@ -209,20 +210,25 @@ const CreateOptions = struct { return undefined; } - Output.prettyln("bun create\n flags:\n", .{}); + Output.prettyln("bun create\n\n flags:\n", .{}); Output.flush(); clap.help(Output.writer(), params[1..]) catch {}; Output.pretty("\n", .{}); Output.prettyln(" environment variables:\n\n", .{}); - Output.prettyln(" GITHUB_ACCESS_TOKEN Use a GitHub access token for downloading code from GitHub.", .{}); - Output.prettyln(" GITHUB_API_DOMAIN Instead of \"api.github.com\", useful for GitHub Enterprise\n", .{}); - Output.prettyln(" NPM_CLIENT Absolute path to the npm client executable", .{}); + Output.prettyln(" GITHUB_ACCESS_TOKEN Downloading code from GitHub with a higher rate limit", .{}); + Output.prettyln(" GITHUB_API_DOMAIN Change \"api.github.com\", useful for GitHub Enterprise\n", .{}); + Output.prettyln(" NPM_CLIENT Absolute path to the npm client executable", .{}); Output.flush(); std.os.exit(0); } - var opts = CreateOptions{}; + var opts = CreateOptions{ .positionals = args.positionals() }; + + if (opts.positionals.len >= 1 and (strings.eqlComptime(opts.positionals[0], "c") or strings.eqlComptime(opts.positionals[0], "create"))) { + opts.positionals = opts.positionals[1..]; + } + if (args.flag("--npm")) { opts.npm_client = NPMClient.Tag.npm; } @@ -251,8 +257,9 @@ var home_dir_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; pub const CreateCommand = struct { var client: HTTPClient = undefined; - pub fn exec(ctx: Command.Context, positionals: []const []const u8) !void { + pub fn exec(ctx: Command.Context, positionals_: []const []const u8) !void { var create_options = try CreateOptions.parse(ctx, false); + const positionals = create_options.positionals; if (positionals.len == 0) { return try CreateListExamplesCommand.exec(ctx); @@ -690,6 +697,8 @@ pub const CreateCommand = struct { progress.refresh(); var is_nextjs = false; + var is_create_react_app = false; + var create_react_app_entry_point_path: string = ""; var preinstall_tasks = std.mem.zeroes(std.ArrayListUnmanaged([]const u8)); var postinstall_tasks = std.mem.zeroes(std.ArrayListUnmanaged([]const u8)); var has_dependencies: bool = false; @@ -762,6 +771,7 @@ pub const CreateCommand = struct { var has_react_refresh = false; var has_bun_macro_relay = false; var has_react = false; + var has_react_scripts = false; const Prune = struct { pub const packages = std.ComptimeStringMap(void, .{ @@ -810,6 +820,7 @@ pub const CreateCommand = struct { if (property.data == .e_object and property.data.e_object.properties.len > 0) { unsupported_packages.update(property); + has_react_scripts = has_react_scripts or property.hasAnyPropertyNamed(&.{"react-scripts"}); has_relay = has_relay or property.hasAnyPropertyNamed(&.{ "react-relay", "relay-runtime", "babel-plugin-relay" }); property.data.e_object.properties = Prune.prune(property.data.e_object.properties); @@ -831,6 +842,7 @@ pub const CreateCommand = struct { if (property.data == .e_object and property.data.e_object.properties.len > 0) { unsupported_packages.update(property); + has_react_scripts = has_react_scripts or property.hasAnyPropertyNamed(&.{"react-scripts"}); has_relay = has_relay or property.hasAnyPropertyNamed(&.{ "react-relay", "relay-runtime", "babel-plugin-relay" }); property.data.e_object.properties = Prune.prune(property.data.e_object.properties); @@ -859,6 +871,7 @@ pub const CreateCommand = struct { needs.bun_framework_next = is_nextjs and !has_bun_framework_next; needs.bun_bun_for_nextjs = is_nextjs; needs.bun_macro_relay_dependency = needs.bun_macro_relay; + var bun_bun_for_react_scripts = false; var bun_macros_prop: ?js_ast.Expr = null; var bun_prop: ?js_ast.Expr = null; @@ -893,15 +906,15 @@ pub const CreateCommand = struct { // if (create_options.verbose) { if (needs.bun_macro_relay) { - Output.prettyErrorln("[package.json] Detected Relay -> added \"bun-macro-relay\" for compatibility", .{}); + Output.prettyErrorln("[package.json] Detected Relay -> added \"bun-macro-relay\"", .{}); } if (needs.react_refresh) { - Output.prettyErrorln("[package.json] Detected React -> added \"react-refresh\" to enable React Fast Refresh", .{}); + Output.prettyErrorln("[package.json] Detected React -> added \"react-refresh\"", .{}); } if (needs.bun_framework_next) { - Output.prettyErrorln("[package.json] Detected Next -> added \"bun-framework-next\" for compatibility", .{}); + Output.prettyErrorln("[package.json] Detected Next -> added \"bun-framework-next\"", .{}); } else if (is_nextjs) { Output.prettyErrorln("[package.json] Detected Next.js", .{}); } @@ -1235,6 +1248,101 @@ pub const CreateCommand = struct { } } + // this is a little dicey + // The idea is: + // Before the closing tag of Create React App's public/index.html + // Inject "" + // Only do this for create-react-app + // Which we define as: + // 1. has a "public/index.html" + // 2. "react-scripts" in package.json dependencies or devDependencies + // 3. has a src/index.{jsx,tsx,ts,mts,mcjs} + // If at any point those expectations are not matched OR the string /src/index.js already exists in the HTML + // don't do it! + if (has_react_scripts) { + bail: { + var public_index_html_parts = [_]string{ destination, "public/index.html" }; + var public_index_html_path = filesystem.absBuf(&public_index_html_parts, &bun_path_buf); + + const public_index_html_file = std.fs.openFileAbsolute(public_index_html_path, .{ .read = true, .write = true }) catch break :bail; + defer public_index_html_file.close(); + + const file_extensions_to_try = [_]string{ ".tsx", ".ts", ".jsx", ".js", ".mts", ".mcjs" }; + + var found_file = false; + var entry_point_path: string = ""; + var entry_point_file_parts = [_]string{ destination, "src/index" }; + var entry_point_file_path_base = filesystem.absBuf(&entry_point_file_parts, &bun_path_buf); + + for (file_extensions_to_try) |ext| { + std.mem.copy(u8, bun_path_buf[entry_point_file_path_base.len..], ext); + entry_point_path = bun_path_buf[0 .. entry_point_file_path_base.len + ext.len]; + std.fs.accessAbsolute(entry_point_path, .{}) catch continue; + found_file = true; + break; + } + if (!found_file) break :bail; + + var public_index_file_contents = public_index_html_file.readToEndAlloc(ctx.allocator, public_index_html_file.getEndPos() catch break :bail) catch break :bail; + + if (std.mem.indexOf(u8, public_index_file_contents, entry_point_path[destination.len..]) != null) { + break :bail; + } + + var body_closing_tag: usize = std.mem.lastIndexOf(u8, public_index_file_contents, "") orelse break :bail; + + var public_index_file_out = std.ArrayList(u8).initCapacity(ctx.allocator, public_index_file_contents.len) catch break :bail; + var html_writer = public_index_file_out.writer(); + + _ = html_writer.writeAll(public_index_file_contents[0..body_closing_tag]) catch break :bail; + + create_react_app_entry_point_path = std.fmt.allocPrint( + ctx.allocator, + "./{s}", + + .{ + std.mem.trimLeft( + u8, + entry_point_path[destination.len..], + "/", + ), + }, + ) catch break :bail; + + html_writer.print( + "\n{s}", + .{ + create_react_app_entry_point_path[2..], + public_index_file_contents[body_closing_tag..], + }, + ) catch break :bail; + + var outfile = std.mem.replaceOwned(u8, ctx.allocator, public_index_file_out.items, "%PUBLIC_URL%", "") catch break :bail; + + // don't do this actually + // it completely breaks when there is more than one CSS file loaded + // // bonus: check for an index.css file + // // inject it into the .html file statically if the file exists but isn't already in + // inject_css: { + // const head_i: usize = std.mem.indexOf(u8, outfile, "") orelse break :inject_css; + // if (std.mem.indexOf(u8, outfile, "/src/index.css") != null) break :inject_css; + + // std.mem.copy(u8, bun_path_buf[destination.len + "/src/index".len ..], ".css"); + // var index_css_file_path = bun_path_buf[0 .. destination.len + "/src/index.css".len]; + // std.fs.accessAbsolute(index_css_file_path, .{}) catch break :inject_css; + // var list = std.ArrayList(u8).fromOwnedSlice(ctx.allocator, outfile); + // list.insertSlice(head_i + "".len, "\n") catch break :inject_css; + // outfile = list.toOwnedSlice(); + // } + + public_index_html_file.pwriteAll(outfile, 0) catch break :bail; + std.os.ftruncate(public_index_html_file.handle, outfile.len + 1) catch break :bail; + bun_bun_for_react_scripts = true; + is_create_react_app = true; + Output.prettyln("[package.json] Added entry point {s} to public/index.html", .{create_react_app_entry_point_path}); + } + } + package_json_expr.data.e_object.is_single_line = false; package_json_expr.data.e_object.properties = properties_list.items; @@ -1298,13 +1406,14 @@ pub const CreateCommand = struct { .e_array => |tasks| { for (tasks.items) |task| { if (task.asString(ctx.allocator)) |task_entry| { - if (needs.bun_bun_for_nextjs) { + if (needs.bun_bun_for_nextjs or bun_bun_for_react_scripts) { var iter = std.mem.split(u8, task_entry, " "); var last_was_bun = false; while (iter.next()) |current| { if (strings.eqlComptime(current, "bun")) { if (last_was_bun) { needs.bun_bun_for_nextjs = false; + bun_bun_for_react_scripts = false; break; } last_was_bun = true; @@ -1360,6 +1469,8 @@ pub const CreateCommand = struct { if (needs.bun_bun_for_nextjs) { try postinstall_tasks.append(ctx.allocator, InjectionPrefill.bun_bun_for_nextjs_task); + } else if (bun_bun_for_react_scripts) { + try postinstall_tasks.append(ctx.allocator, try std.fmt.allocPrint(ctx.allocator, "bun bun {s}", .{create_react_app_entry_point_path})); } } } @@ -1541,6 +1652,14 @@ pub const CreateCommand = struct { \\ bun bun --use next \\ , .{}); + } else if (is_create_react_app) { + Output.pretty( + \\ + \\# When dependencies change, run this to update node_modules.bun: + \\ + \\ bun bun {s} + \\ + , .{create_react_app_entry_point_path}); } Output.pretty( @@ -2052,7 +2171,7 @@ pub const CreateListExamplesCommand = struct { Example.print(examples.items, null); - Output.prettyln("You can also paste a GitHub repository:\n\n bun create github-user/github-repo\n\n bun create https://github.com/user/repo\n\n", .{}); + Output.prettyln("# You can also paste a GitHub repository:\n\n bun create ahfarmer/calculator calc\n\n", .{}); if (env_loader.map.get("HOME")) |homedir| { Output.prettyln(