usingnamespace @import("../global.zig"); const std = @import("std"); const lex = @import("../js_lexer.zig"); const logger = @import("../logger.zig"); const alloc = @import("../alloc.zig"); const options = @import("../options.zig"); const js_parser = @import("../js_parser.zig"); const js_ast = @import("../js_ast.zig"); const linker = @import("../linker.zig"); usingnamespace @import("../ast/base.zig"); usingnamespace @import("../defines.zig"); const panicky = @import("../panic_handler.zig"); const allocators = @import("../allocators.zig"); const sync = @import(".././sync.zig"); const Api = @import("../api/schema.zig").Api; const resolve_path = @import("../resolver/resolve_path.zig"); const configureTransformOptionsForBun = @import("../javascript/jsc/config.zig").configureTransformOptionsForBun; const Command = @import("../cli.zig").Command; const bundler = @import("../bundler.zig"); const NodeModuleBundle = @import("../node_module_bundle.zig").NodeModuleBundle; const fs = @import("../fs.zig"); const URL = @import("../query_string_map.zig").URL; const HTTPClient = @import("../http_client.zig"); const ParseJSON = @import("../json_parser.zig").ParseJSON; const Archive = @import("../libarchive/libarchive.zig").Archive; const Zlib = @import("../zlib.zig"); const JSPrinter = @import("../js_printer.zig"); const DotEnv = @import("../env_loader.zig"); const NPMClient = @import("../which_npm_client.zig").NPMClient; const which = @import("../which.zig").which; const clap = @import("clap"); const Lock = @import("../lock.zig").Lock; const CopyFile = @import("../copy_file.zig"); var bun_path_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; // the standard library function for this returns false when statically linking fn getSelfExeSharedLibPaths(allocator: *std.mem.Allocator) error{OutOfMemory}![][:0]u8 { const List = std.ArrayList([:0]u8); const os = std.os; const Allocator = std.mem.Allocator; const builtin = std.builtin; const mem = std.mem; switch (builtin.os.tag) { .linux, .freebsd, .netbsd, .dragonfly, .openbsd, => { var paths = List.init(allocator); errdefer { const slice = paths.toOwnedSlice(); for (slice) |item| { allocator.free(item); } allocator.free(slice); } try os.dl_iterate_phdr(&paths, error{OutOfMemory}, struct { fn callback(info: *os.dl_phdr_info, size: usize, list: *List) !void { _ = size; const name = info.dlpi_name orelse return; if (name[0] == '/') { const item = try list.allocator.dupeZ(u8, mem.spanZ(name)); errdefer list.allocator.free(item); try list.append(item); } } }.callback); return paths.toOwnedSlice(); }, .macos, .ios, .watchos, .tvos => { var paths = List.init(allocator); errdefer { const slice = paths.toOwnedSlice(); for (slice) |item| { allocator.free(item); } allocator.free(slice); } const img_count = std.c._dyld_image_count(); var i: u32 = 0; while (i < img_count) : (i += 1) { const name = std.c._dyld_get_image_name(i); const item = try allocator.dupeZ(u8, mem.spanZ(name)); errdefer allocator.free(item); try paths.append(item); } return paths.toOwnedSlice(); }, // revisit if Haiku implements dl_iterat_phdr (https://dev.haiku-os.org/ticket/15743) .haiku => { var paths = List.init(allocator); errdefer { const slice = paths.toOwnedSlice(); for (slice) |item| { allocator.free(item); } allocator.free(slice); } var b = "/boot/system/runtime_loader"; const item = try allocator.dupeZ(u8, mem.spanZ(b)); errdefer allocator.free(item); try paths.append(item); return paths.toOwnedSlice(); }, else => @compileError("getSelfExeSharedLibPaths unimplemented for this target"), } } const skip_dirs = &[_]string{ "node_modules", ".git" }; const skip_files = &[_]string{ "package-lock.json", "yarn.lock", "pnpm-lock.yaml", }; const never_conflict = &[_]string{ "README.md", "gitignore", ".gitignore", ".git/", }; const npm_task_args = &[_]string{ "exec", }; var bun_path: ?[:0]const u8 = null; fn execTask(allocator: *std.mem.Allocator, task_: string, cwd: string, PATH: string, npm_client: NPMClient) void { const task = std.mem.trim(u8, task_, " \n\r\t"); if (task.len == 0) return; var splitter = std.mem.split(u8, task, " "); var count: usize = 0; while (splitter.next() != null) { count += 1; } const npm_args = 2; const total = count + npm_args; var argv = allocator.alloc(string, total) catch return; defer allocator.free(argv); argv[0] = npm_client.bin; argv[1] = npm_task_args[0]; { var i: usize = 2; splitter = std.mem.split(u8, task, " "); while (splitter.next()) |split| { argv[i] = split; i += 1; } } if (strings.startsWith(task, "bun ")) { // TODO: use self exe if (bun_path orelse which(&bun_path_buf, PATH, cwd, "bun")) |bun_path_| { bun_path = bun_path_; argv = argv[npm_args..]; argv[0] = std.mem.span(bun_path_); } } Output.pretty("\n$", .{}); for (argv) |arg, i| { if (i > argv.len - 1) { Output.print(" {s} ", .{arg}); } else { Output.print(" {s}", .{arg}); } } Output.pretty("", .{}); Output.print("\n", .{}); Output.flush(); Output.disableBuffering(); defer Output.enableBuffering(); var proc = std.ChildProcess.init(argv, allocator) catch return; defer proc.deinit(); proc.stdin_behavior = .Inherit; proc.stdout_behavior = .Inherit; proc.stderr_behavior = .Inherit; proc.cwd = cwd; _ = proc.spawnAndWait() catch undefined; } // We don't want to allocate memory each time // But we cannot print over an existing buffer or weird stuff will happen // so we keep two and switch between them pub const ProgressBuf = struct { var bufs: [2][1024]u8 = [2][1024]u8{ @as([1024]u8, undefined), @as([1024]u8, undefined), }; var buf_index: usize = 0; pub fn print(comptime fmt: string, args: anytype) !string { buf_index += 1; return try std.fmt.bufPrint(std.mem.span(&bufs[buf_index % 2]), fmt, args); } }; const CreateOptions = struct { npm_client: ?NPMClient.Tag = null, skip_install: bool = false, overwrite: bool = false, skip_git: bool = false, verbose: bool = false, const params = [_]clap.Param(clap.Help){ clap.parseParam("--help Print this menu") catch unreachable, clap.parseParam("--npm Use npm for tasks & install") catch unreachable, clap.parseParam("--yarn Use yarn for tasks & install") catch unreachable, clap.parseParam("--pnpm Use pnpm for tasks & install") catch unreachable, clap.parseParam("--force Overwrite existing files") catch unreachable, clap.parseParam("--no-install Don't install node_modules") catch unreachable, clap.parseParam("--no-git Don't create a git repository") catch unreachable, clap.parseParam("--verbose Too many logs") catch unreachable, clap.parseParam("... ") catch unreachable, }; pub fn parse(allocator: *std.mem.Allocator, comptime print_flags_only: bool) !CreateOptions { var diag = clap.Diagnostic{}; var args = clap.parse(clap.Help, ¶ms, .{ .diagnostic = &diag, .allocator = allocator }) catch |err| { // Report useful error and exit diag.report(Output.errorWriter(), err) catch {}; return err; }; if (args.flag("--help") or comptime print_flags_only) { if (comptime print_flags_only) { clap.help(Output.writer(), params[1..]) catch {}; return undefined; } Output.prettyln("bun create flags:\n", .{}); Output.flush(); clap.help(Output.writer(), params[1..]) catch {}; Output.flush(); std.os.exit(0); } var opts = CreateOptions{}; if (args.flag("--npm")) { opts.npm_client = NPMClient.Tag.npm; } if (args.flag("--yarn")) { opts.npm_client = NPMClient.Tag.yarn; } if (args.flag("--pnpm")) { opts.npm_client = NPMClient.Tag.pnpm; } opts.verbose = args.flag("--verbose"); opts.skip_install = args.flag("--no-install"); opts.skip_git = args.flag("--no-git"); opts.overwrite = args.flag("--force"); return opts; } }; const BUN_CREATE_DIR = ".bun-create"; 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 { var create_options = try CreateOptions.parse(ctx.allocator, false); var filesystem = try fs.FileSystem.init1(ctx.allocator, null); var env_loader: DotEnv.Loader = brk: { var map = try ctx.allocator.create(DotEnv.Map); map.* = DotEnv.Map.init(ctx.allocator); break :brk DotEnv.Loader.init(map, ctx.allocator); }; env_loader.loadProcess(); const template = brk: { var positional = positionals[0]; if (!std.fs.path.isAbsolute(positional)) { outer: { if (env_loader.map.get("BUN_CREATE_DIR")) |home_dir| { var parts = [_]string{ home_dir, positional }; var outdir_path = filesystem.absBuf(&parts, &home_dir_buf); home_dir_buf[outdir_path.len] = 0; var outdir_path_ = home_dir_buf[0..outdir_path.len :0]; std.fs.accessAbsoluteZ(outdir_path_, .{}) catch break :outer; if (create_options.verbose) { Output.prettyErrorln("reading from {s}", .{outdir_path}); } break :brk outdir_path; } } outer: { var parts = [_]string{ filesystem.top_level_dir, BUN_CREATE_DIR, positional }; var outdir_path = filesystem.absBuf(&parts, &home_dir_buf); home_dir_buf[outdir_path.len] = 0; var outdir_path_ = home_dir_buf[0..outdir_path.len :0]; std.fs.accessAbsoluteZ(outdir_path_, .{}) catch break :outer; if (create_options.verbose) { Output.prettyErrorln("reading from {s}", .{outdir_path}); } break :brk outdir_path; } outer: { if (env_loader.map.get("HOME")) |home_dir| { var parts = [_]string{ home_dir, BUN_CREATE_DIR, positional }; var outdir_path = filesystem.absBuf(&parts, &home_dir_buf); home_dir_buf[outdir_path.len] = 0; var outdir_path_ = home_dir_buf[0..outdir_path.len :0]; std.fs.accessAbsoluteZ(outdir_path_, .{}) catch break :outer; if (create_options.verbose) { Output.prettyErrorln("reading from {s}", .{outdir_path}); } break :brk outdir_path; } } } break :brk positional; }; const dirname = positionals[1]; var filename_writer = filesystem.dirname_store; const destination = try filesystem.dirname_store.append([]const u8, resolve_path.joinAbs(filesystem.top_level_dir, .auto, dirname)); var progress = std.Progress{}; var node = try progress.start(try ProgressBuf.print("Loading {s}", .{template}), 0); progress.supports_ansi_escape_codes = Output.enable_ansi_colors; // alacritty is fast if (env_loader.map.get("ALACRITTY_LOG") != null) { progress.refresh_rate_ns = std.time.ns_per_ms * 8; if (create_options.verbose) { Output.prettyErrorln("your using alacritty", .{}); } } defer { progress.refresh(); } var package_json_contents: MutableString = undefined; var package_json_file: std.fs.File = undefined; const is_remote_template = !std.fs.path.isAbsolute(template); if (create_options.verbose) { Output.prettyErrorln("is_remote_template {d}", .{@boolToInt(is_remote_template)}); } if (is_remote_template) { var tarball_bytes: MutableString = Example.fetch(ctx, template, &progress, node) catch |err| { switch (err) { error.HTTPForbidden, error.ExampleNotFound => { node.end(); progress.refresh(); Output.prettyError("\nerror: \"{s}\" was not found. Here are templates you can use:\n\n", .{ template, }); Output.flush(); const examples = try Example.fetchAllLocalAndRemote(ctx, null, &env_loader, filesystem); Example.print(examples.items, dirname); Output.flush(); std.os.exit(1); }, else => { return err; }, } }; node.name = try ProgressBuf.print("Decompressing {s}", .{template}); node.setCompletedItems(0); node.setEstimatedTotalItems(0); progress.refresh(); var file_buf = try ctx.allocator.alloc(u8, 16384); var tarball_buf_list = std.ArrayListUnmanaged(u8){ .capacity = file_buf.len, .items = file_buf }; var gunzip = try Zlib.ZlibReaderArrayList.init(tarball_bytes.list.items, &tarball_buf_list, ctx.allocator); try gunzip.readAll(); gunzip.deinit(); node.name = try ProgressBuf.print("Extracting {s}", .{template}); node.setCompletedItems(0); node.setEstimatedTotalItems(0); progress.refresh(); var pluckers = [_]Archive.Plucker{ try Archive.Plucker.init("package.json", 2048, ctx.allocator), try Archive.Plucker.init("GETTING_STARTED", 512, ctx.allocator), }; var archive_context = Archive.Context{ .pluckers = &pluckers, .all_files = undefined, .overwrite_list = std.StringArrayHashMap(void).init(ctx.allocator), }; if (!create_options.overwrite) { try Archive.getOverwritingFileList( tarball_buf_list.items, destination, &archive_context, @TypeOf(filesystem.dirname_store), filesystem.dirname_store, 1, ); inline for (never_conflict) |never_conflict_path| { _ = archive_context.overwrite_list.swapRemove(never_conflict_path); } if (archive_context.overwrite_list.count() > 0) { node.end(); progress.refresh(); // Thank you create-react-app for this copy (and idea) Output.prettyErrorln( "error: The directory {s}/ contains files that could conflict:\n\n", .{ std.fs.path.basename(destination), }, ); for (archive_context.overwrite_list.keys()) |path| { if (strings.endsWith(path, std.fs.path.sep_str)) { Output.prettyError(" {s}", .{path[0 .. std.math.max(path.len, 1) - 1]}); Output.prettyErrorln(std.fs.path.sep_str, .{}); } else { Output.prettyErrorln(" {s}", .{path}); } } Output.prettyErrorln("\nTo do it anyway, use --force", .{}); Output.flush(); std.os.exit(1); } } const extracted_file_count = try Archive.extractToDisk( tarball_buf_list.items, destination, &archive_context, void, void{}, 1, false, false, ); var plucker = pluckers[0]; if (!plucker.found or plucker.fd == 0) { node.end(); Output.prettyErrorln("package.json not found. This package is corrupt. Please try again or file an issue if it keeps happening.", .{}); Output.flush(); std.os.exit(1); } node.name = "Updating package.json"; progress.refresh(); package_json_contents = plucker.contents; package_json_file = std.fs.File{ .handle = plucker.fd }; } else { var template_parts = [_]string{template}; node.name = "Copying files"; progress.refresh(); const template_dir = std.fs.openDirAbsolute(filesystem.abs(&template_parts), .{ .iterate = true }) catch |err| { node.end(); progress.refresh(); Output.prettyErrorln("{s}: opening dir {s}", .{ @errorName(err), template }); Output.flush(); std.os.exit(1); }; std.fs.deleteTreeAbsolute(destination) catch {}; const destination_dir = std.fs.cwd().makeOpenPath(destination, .{ .iterate = true }) catch |err| { node.end(); progress.refresh(); Output.prettyErrorln("{s}: creating dir {s}", .{ @errorName(err), destination }); Output.flush(); std.os.exit(1); }; const Walker = @import("../walker_skippable.zig"); var walker_ = try Walker.walk(template_dir, ctx.allocator, skip_files, skip_dirs); defer walker_.deinit(); var count: usize = 0; const FileCopier = struct { pub fn copy( destination_dir_: std.fs.Dir, walker: *Walker, node_: *std.Progress.Node, progress_: *std.Progress, ) !void { while (try walker.next()) |entry| { // TODO: make this not walk these folders entirely // rather than checking each file path..... if (entry.kind != .File) continue; var outfile = destination_dir_.createFile(entry.path, .{}) catch brk: { if (std.fs.path.dirname(entry.path)) |entry_dirname| { destination_dir_.makePath(entry_dirname) catch {}; } break :brk destination_dir_.createFile(entry.path, .{}) catch |err| { node_.end(); progress_.refresh(); Output.prettyErrorln("{s}: copying file {s}", .{ @errorName(err), entry.path }); Output.flush(); std.os.exit(1); }; }; defer outfile.close(); defer node_.completeOne(); var infile = try entry.dir.openFile(entry.basename, .{ .read = true }); defer infile.close(); // Assumption: you only really care about making sure something that was executable is still executable const stat = infile.stat() catch continue; _ = C.fchmod(outfile.handle, stat.mode); CopyFile.copy(infile.handle, outfile.handle) catch { entry.dir.copyFile(entry.basename, destination_dir_, entry.path, .{}) catch |err| { node_.end(); progress_.refresh(); Output.prettyErrorln("{s}: copying file {s}", .{ @errorName(err), entry.path }); Output.flush(); std.os.exit(1); }; }; } } }; try FileCopier.copy(destination_dir, &walker_, node, &progress); package_json_file = destination_dir.openFile("package.json", .{ .read = true, .write = true }) catch |err| { node.end(); progress.refresh(); Output.prettyErrorln("Failed to open package.json due to error {s}", .{@errorName(err)}); Output.flush(); std.os.exit(1); }; const stat = package_json_file.stat() catch |err| { node.end(); progress.refresh(); Output.prettyErrorln("Failed to stat package.json due to error {s}", .{@errorName(err)}); Output.flush(); std.os.exit(1); }; if (stat.kind != .File or stat.size == 0) { node.end(); progress.refresh(); Output.prettyErrorln("package.json must be a file with content", .{}); Output.flush(); std.os.exit(1); } package_json_contents = try MutableString.init(ctx.allocator, stat.size); package_json_contents.list.expandToCapacity(); _ = package_json_file.preadAll(package_json_contents.list.items, 0) catch |err| { node.end(); progress.refresh(); Output.prettyErrorln("Error reading package.json: {s}", .{@errorName(err)}); Output.flush(); std.os.exit(1); }; // The printer doesn't truncate, so we must do so manually std.os.ftruncate(package_json_file.handle, 0) catch {}; js_ast.Expr.Data.Store.create(default_allocator); js_ast.Stmt.Data.Store.create(default_allocator); } node.end(); progress.refresh(); var source = logger.Source.initPathString("package.json", package_json_contents.list.items); var package_json_expr = ParseJSON(&source, ctx.log, ctx.allocator) catch |err| { Output.prettyErrorln("package.json failed to parse with error: {s}", .{@errorName(err)}); Output.flush(); std.os.exit(1); }; if (ctx.log.errors > 0) { if (Output.enable_ansi_colors) { try ctx.log.printForLogLevelWithEnableAnsiColors(Output.errorWriter(), true); } else { try ctx.log.printForLogLevelWithEnableAnsiColors(Output.errorWriter(), false); } Output.flush(); std.os.exit(1); } if (package_json_expr.asProperty("name")) |name_expr| { if (name_expr.expr.data != .e_string) { Output.prettyErrorln("package.json failed to parse correctly. its missing a name. it shouldnt be missing a name.", .{}); Output.flush(); std.os.exit(1); } var basename = std.fs.path.basename(destination); name_expr.expr.data.e_string.utf8 = @intToPtr([*]u8, @ptrToInt(basename.ptr))[0..basename.len]; } else { Output.prettyErrorln("package.json failed to parse correctly. its missing a name. it shouldnt be missing a name.", .{}); Output.flush(); std.os.exit(1); } package_json_expr.data.e_object.is_single_line = false; 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; { var i: usize = 0; var property_i: usize = 0; while (i < package_json_expr.data.e_object.properties.len) : (i += 1) { const property = package_json_expr.data.e_object.properties[i]; const key = property.key.?.asString(ctx.allocator).?; has_dependencies = has_dependencies or ((strings.eqlComptime(key, "dependencies") or strings.eqlComptime(key, "devDependencies") or std.mem.indexOf(u8, key, "optionalDependencies") != null or strings.eqlComptime(key, "peerDependencies")) and (property.value.?.data == .e_object and property.value.?.data.e_object.properties.len > 0)); if (key.len == 0 or !strings.eqlComptime(key, "bun-create")) { package_json_expr.data.e_object.properties[property_i] = property; property_i += 1; continue; } var value = property.value.?; if (value.asProperty("postinstall")) |postinstall| { switch (postinstall.expr.data) { .e_string => |single_task| { try postinstall_tasks.append( ctx.allocator, try single_task.string(ctx.allocator), ); }, .e_array => |tasks| { for (tasks.items) |task| { if (task.asString(ctx.allocator)) |task_entry| { try postinstall_tasks.append( ctx.allocator, task_entry, ); } } }, else => {}, } } if (value.asProperty("preinstall")) |preinstall| { switch (preinstall.expr.data) { .e_string => |single_task| { try preinstall_tasks.append( ctx.allocator, try single_task.string(ctx.allocator), ); }, .e_array => |tasks| { for (tasks.items) |task| { if (task.asString(ctx.allocator)) |task_entry| { try preinstall_tasks.append( ctx.allocator, task_entry, ); } } }, else => {}, } } } package_json_expr.data.e_object.properties = package_json_expr.data.e_object.properties[0..property_i]; } if (create_options.verbose) { Output.prettyErrorln("Has dependencies? {d}", .{@boolToInt(has_dependencies)}); } var package_json_writer = JSPrinter.NewFileWriter(package_json_file); _ = JSPrinter.printJSON(@TypeOf(package_json_writer), package_json_writer, package_json_expr, &source) catch |err| { Output.prettyErrorln("package.json failed to write due to error {s}", .{@errorName(err)}); Output.flush(); std.os.exit(1); }; { var parent_dir = try std.fs.openDirAbsolute(destination, .{}); defer parent_dir.close(); std.os.linkat(parent_dir.fd, "gitignore", parent_dir.fd, ".gitignore", 0) catch {}; std.os.unlinkat( parent_dir.fd, "gitignore", 0, ) catch {}; std.os.unlinkat( parent_dir.fd, ".npmignore", 0, ) catch {}; } const PATH = env_loader.map.get("PATH") orelse ""; var npm_client_: ?NPMClient = null; create_options.skip_install = create_options.skip_install or !has_dependencies; if (!create_options.skip_git) { if (!create_options.skip_install) { GitHandler.spawn(destination, PATH, create_options.verbose); } else { if (create_options.verbose) { create_options.skip_git = GitHandler.run(destination, PATH, true) catch false; } else { create_options.skip_git = GitHandler.run(destination, PATH, false) catch false; } } } if (!create_options.skip_install) { if (env_loader.map.get("NPM_CLIENT")) |npm_client_bin| { npm_client_ = NPMClient{ .tag = .npm, .bin = npm_client_bin }; } else if (PATH.len > 0) { var realpath_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; if (create_options.npm_client) |tag| { if (which(&realpath_buf, PATH, destination, @tagName(tag))) |bin| { npm_client_ = NPMClient{ .tag = tag, .bin = try ctx.allocator.dupe(u8, bin) }; } } else if (try NPMClient.detect(ctx.allocator, &realpath_buf, PATH, destination, true)) |npmclient| { npm_client_ = NPMClient{ .bin = try ctx.allocator.dupe(u8, npmclient.bin), .tag = npmclient.tag, }; } } } if (npm_client_ != null and preinstall_tasks.items.len > 0) { for (preinstall_tasks.items) |task, i| { execTask(ctx.allocator, task, destination, PATH, npm_client_.?); } } if (npm_client_) |npm_client| { const start_time = std.time.nanoTimestamp(); const install_args_ = [_]string{ npm_client.bin, "install", "--loglevel=error", "--no-fund", "--no-audit" }; const len: usize = switch (npm_client.tag) { .npm => install_args_.len, else => 2, }; const install_args = install_args_[0..len]; Output.flush(); Output.pretty("\n$ {s} install", .{@tagName(npm_client.tag)}); var writer = Output.writer(); if (install_args.len > 2) { for (install_args[2..]) |arg| { Output.pretty(" ", .{}); Output.pretty("{s}", .{arg}); } } Output.pretty("\n", .{}); Output.flush(); var process = try std.ChildProcess.init(install_args, ctx.allocator); process.cwd = destination; defer { Output.printErrorln("\n", .{}); Output.printStartEnd(start_time, std.time.nanoTimestamp()); Output.prettyError(" {s} install\n", .{@tagName(npm_client.tag)}); Output.flush(); Output.print("\n", .{}); Output.flush(); } defer process.deinit(); var term = try process.spawnAndWait(); _ = process.kill() catch undefined; } else if (!create_options.skip_install) { progress.log("Failed to detect npm client. Tried pnpm, yarn, and npm.\n", .{}); } if (npm_client_ != null and !create_options.skip_install and postinstall_tasks.items.len > 0) { for (postinstall_tasks.items) |task, i| { execTask(ctx.allocator, task, destination, PATH, npm_client_.?); } } if (!create_options.skip_install and !create_options.skip_git) { create_options.skip_git = !GitHandler.wait(); } Output.printError("\n", .{}); Output.printStartEnd(ctx.start_time, std.time.nanoTimestamp()); Output.prettyErrorln(" bun create {s}", .{template}); Output.flush(); if (!create_options.skip_install) { Output.pretty( \\ \\----- \\ , .{}); } if (!create_options.skip_git and !create_options.skip_install) { Output.pretty( \\ \\A local git repository was created for you and dependencies were installed automatically. \\ , .{}); } else if (!create_options.skip_git) { Output.pretty( \\ \\A local git repository was created for you. \\ , .{}); } else if (!create_options.skip_install) { Output.pretty( \\ \\Dependencies were installed automatically. \\ , .{}); } Output.pretty( \\ \\Created {s} project successfully \\ \\# To get started, run: \\ \\ cd {s} \\ bun \\ , .{ std.fs.path.basename(template), filesystem.relativeTo(destination), }); Output.flush(); } }; const Commands = .{ &[_]string{""}, &[_]string{""}, &[_]string{""}, }; const picohttp = @import("picohttp"); const PackageDownloadThread = struct { thread: std.Thread, client: HTTPClient, tarball_url: string, allocator: *std.mem.Allocator, buffer: MutableString, done: std.atomic.Atomic(u32), response: picohttp.Response = undefined, pub fn threadHandler(this: *PackageDownloadThread) !void { this.done.store(0, .Release); this.response = try this.client.send("", &this.buffer); this.done.store(1, .Release); std.Thread.Futex.wake(&this.done, 1); } pub fn spawn(allocator: *std.mem.Allocator, tarball_url: string, progress_node: *std.Progress.Node) !*PackageDownloadThread { var download = try allocator.create(PackageDownloadThread); download.* = PackageDownloadThread{ .allocator = allocator, .client = HTTPClient.init(allocator, .GET, URL.parse(tarball_url), .{}, ""), .tarball_url = tarball_url, .buffer = try MutableString.init(allocator, 1024), .done = std.atomic.Atomic(u32).init(0), .thread = undefined, }; if (Output.enable_ansi_colors) { download.client.progress_node = progress_node; } download.thread = try std.Thread.spawn(.{}, threadHandler, .{download}); return download; } }; pub const DownloadedExample = struct { tarball_bytes: MutableString, example: Example, }; pub const Example = struct { name: string, version: string, description: string, local: bool = false, var client: HTTPClient = undefined; const examples_url: string = "https://registry.npmjs.org/bun-examples-all/latest"; var url: URL = undefined; pub const timeout: u32 = 6000; var app_name_buf: [512]u8 = undefined; pub fn print(examples: []const Example, default_app_name: ?string) void { for (examples) |example, i| { var app_name = default_app_name orelse (std.fmt.bufPrint(&app_name_buf, "./{s}-app", .{example.name[0..std.math.min(example.name.len, 492)]}) catch unreachable); if (example.description.len > 0) { Output.pretty(" # {s}\n bun create {s} {s}\n \n\n", .{ example.description, example.name, app_name, }); } else { Output.pretty(" bun create {s} {s}\n\n", .{ example.name, app_name, }); } } } pub fn fetchAllLocalAndRemote(ctx: Command.Context, node: ?*std.Progress.Node, env_loader: *DotEnv.Loader, filesystem: *fs.FileSystem) !std.ArrayList(Example) { const remote_examples = try Example.fetchAll(ctx, node); if (node) |node_| node_.end(); var examples = std.ArrayList(Example).fromOwnedSlice(ctx.allocator, remote_examples); { var folders = [3]std.fs.Dir{ std.fs.Dir{ .fd = 0 }, std.fs.Dir{ .fd = 0 }, std.fs.Dir{ .fd = 0 } }; if (env_loader.map.get("BUN_CREATE_DIR")) |home_dir| { var parts = [_]string{home_dir}; var outdir_path = filesystem.absBuf(&parts, &home_dir_buf); folders[0] = std.fs.openDirAbsolute(outdir_path, .{ .iterate = true }) catch std.fs.Dir{ .fd = 0 }; } { var parts = [_]string{ filesystem.top_level_dir, BUN_CREATE_DIR }; var outdir_path = filesystem.absBuf(&parts, &home_dir_buf); folders[1] = std.fs.openDirAbsolute(outdir_path, .{ .iterate = true }) catch std.fs.Dir{ .fd = 0 }; } if (env_loader.map.get("HOME")) |home_dir| { var parts = [_]string{ home_dir, BUN_CREATE_DIR }; var outdir_path = filesystem.absBuf(&parts, &home_dir_buf); folders[2] = std.fs.openDirAbsolute(outdir_path, .{ .iterate = true }) catch std.fs.Dir{ .fd = 0 }; } // subfolders with package.json for (folders) |folder_| { if (folder_.fd != 0) { const folder: std.fs.Dir = folder_; var iter = folder.iterate(); loop: while (iter.next() catch null) |entry_| { const entry: std.fs.Dir.Entry = entry_; switch (entry.kind) { .Directory => { inline for (skip_dirs) |skip_dir| { if (strings.eqlComptime(entry.name, skip_dir)) { continue :loop; } } std.mem.copy(u8, &home_dir_buf, entry.name); home_dir_buf[entry.name.len] = std.fs.path.sep; std.mem.copy(u8, home_dir_buf[entry.name.len + 1 ..], "package.json"); home_dir_buf[entry.name.len + 1 + "package.json".len] = 0; var path: [:0]u8 = home_dir_buf[0 .. entry.name.len + 1 + "package.json".len :0]; folder.accessZ(path, .{ .read = true, }) catch continue :loop; try examples.append( Example{ .name = try filesystem.filename_store.append(@TypeOf(entry.name), entry.name), .version = "", .local = true, .description = "", }, ); continue :loop; }, else => continue, } } } } } return examples; } pub fn fetch(ctx: Command.Context, name: string, refresher: *std.Progress, progress: *std.Progress.Node) !MutableString { progress.name = "Fetching package.json"; refresher.refresh(); const example_start = std.time.nanoTimestamp(); var url_buf: [1024]u8 = undefined; var mutable = try MutableString.init(ctx.allocator, 2048); url = URL.parse(try std.fmt.bufPrint(&url_buf, "https://registry.npmjs.org/@bun-examples/{s}/latest", .{name})); client = HTTPClient.init(ctx.allocator, .GET, url, .{}, ""); client.timeout = timeout; client.progress_node = progress; var response = try client.send("", &mutable); switch (response.status_code) { 404 => return error.ExampleNotFound, 403 => return error.HTTPForbidden, 429 => return error.HTTPTooManyRequests, 499...599 => return error.NPMIsDown, 200 => {}, else => return error.HTTPError, } progress.name = "Parsing package.json"; refresher.refresh(); js_ast.Expr.Data.Store.create(default_allocator); js_ast.Stmt.Data.Store.create(default_allocator); var source = logger.Source.initPathString("package.json", mutable.list.items); var expr = ParseJSON(&source, ctx.log, ctx.allocator) catch |err| { progress.end(); refresher.refresh(); if (ctx.log.errors > 0) { if (Output.enable_ansi_colors) { try ctx.log.printForLogLevelWithEnableAnsiColors(Output.errorWriter(), true); } else { try ctx.log.printForLogLevelWithEnableAnsiColors(Output.errorWriter(), false); } Output.flush(); std.os.exit(1); } else { Output.prettyErrorln("Error parsing package: {s}", .{@errorName(err)}); Output.flush(); std.os.exit(1); } }; if (ctx.log.errors > 0) { progress.end(); refresher.refresh(); if (Output.enable_ansi_colors) { try ctx.log.printForLogLevelWithEnableAnsiColors(Output.errorWriter(), true); } else { try ctx.log.printForLogLevelWithEnableAnsiColors(Output.errorWriter(), false); } Output.flush(); std.os.exit(1); } const tarball_url: string = brk: { if (expr.asProperty("dist")) |q| { if (q.expr.asProperty("tarball")) |p| { if (p.expr.asString(ctx.allocator)) |s| { if (s.len > 0 and (strings.startsWith(s, "https://") or strings.startsWith(s, "http://"))) { break :brk s; } } } } progress.end(); refresher.refresh(); Output.prettyErrorln("package.json is missing tarball url. This is an internal error!", .{}); Output.flush(); std.os.exit(1); }; progress.name = "Downloading tarball"; refresher.refresh(); var thread: *PackageDownloadThread = try PackageDownloadThread.spawn(ctx.allocator, tarball_url, progress); refresher.maybeRefresh(); while (thread.done.load(.Acquire) == 0) { std.Thread.Futex.wait(&thread.done, 1, std.time.ns_per_ms * 10000) catch {}; } refresher.maybeRefresh(); if (thread.response.status_code != 200) { progress.end(); refresher.refresh(); Output.prettyErrorln("Error fetching tarball: {d}", .{thread.response.status_code}); Output.flush(); std.os.exit(1); } refresher.refresh(); thread.thread.join(); return thread.buffer; } pub fn fetchAll(ctx: Command.Context, progress_node: ?*std.Progress.Node) ![]Example { url = URL.parse(examples_url); client = HTTPClient.init(ctx.allocator, .GET, url, .{}, ""); client.timeout = timeout; if (Output.enable_ansi_colors) { client.progress_node = progress_node; } var mutable: MutableString = try MutableString.init(ctx.allocator, 1024); var response = client.send("", &mutable) catch |err| { switch (err) { error.WouldBlock => { Output.prettyErrorln("Request timed out while trying to fetch examples list. Please try again", .{}); Output.flush(); std.os.exit(1); }, else => { Output.prettyErrorln("{s} while trying to fetch examples list. Please try again", .{@errorName(err)}); Output.flush(); std.os.exit(1); }, } }; if (response.status_code != 200) { Output.prettyErrorln("{d} fetching examples :( {s}", .{ response.status_code, mutable.list.items }); Output.flush(); std.os.exit(1); } js_ast.Expr.Data.Store.create(default_allocator); js_ast.Stmt.Data.Store.create(default_allocator); var source = logger.Source.initPathString("examples.json", mutable.list.items); const examples_object = ParseJSON(&source, ctx.log, ctx.allocator) catch |err| { if (ctx.log.errors > 0) { if (Output.enable_ansi_colors) { try ctx.log.printForLogLevelWithEnableAnsiColors(Output.errorWriter(), true); } else { try ctx.log.printForLogLevelWithEnableAnsiColors(Output.errorWriter(), false); } std.os.exit(1); Output.flush(); } else { Output.prettyErrorln("Error parsing examples: {s}", .{@errorName(err)}); Output.flush(); std.os.exit(1); } }; if (ctx.log.errors > 0) { if (Output.enable_ansi_colors) { try ctx.log.printForLogLevelWithEnableAnsiColors(Output.errorWriter(), true); } else { try ctx.log.printForLogLevelWithEnableAnsiColors(Output.errorWriter(), false); } Output.flush(); std.os.exit(1); } if (examples_object.asProperty("examples")) |q| { if (q.expr.data == .e_object) { var count: usize = 0; for (q.expr.data.e_object.properties) |property| { count += 1; } var list = try ctx.allocator.alloc(Example, count); for (q.expr.data.e_object.properties) |property, i| { const name = property.key.?.data.e_string.utf8; list[i] = Example{ .name = if (std.mem.indexOfScalar(u8, name, '/')) |slash| name[slash + 1 ..] else name, .version = property.value.?.asProperty("version").?.expr.data.e_string.utf8, .description = property.value.?.asProperty("description").?.expr.data.e_string.utf8, }; } return list; } } Output.prettyErrorln("Corrupt examples data: expected object but received {s}", .{@tagName(examples_object.data)}); Output.flush(); std.os.exit(1); } }; pub const CreateListExamplesCommand = struct { pub fn exec(ctx: Command.Context) !void { var filesystem = try fs.FileSystem.init1(ctx.allocator, null); var env_loader: DotEnv.Loader = brk: { var map = try ctx.allocator.create(DotEnv.Map); map.* = DotEnv.Map.init(ctx.allocator); break :brk DotEnv.Loader.init(map, ctx.allocator); }; env_loader.loadProcess(); const time = std.time.nanoTimestamp(); var progress = std.Progress{}; var node = try progress.start("Fetching manifest", 0); progress.refresh(); const examples = try Example.fetchAllLocalAndRemote(ctx, node, &env_loader, filesystem); Output.printStartEnd(time, std.time.nanoTimestamp()); Output.prettyln(" Fetched manifest", .{}); Output.prettyln("Welcome to Bun! Create a new project by pasting any of the following:\n\n", .{}); Output.flush(); Example.print(examples.items, null); if (env_loader.map.get("HOME")) |homedir| { Output.prettyln( "This command is completely optional. To add a new local template, create a folder in {s}/.bun-create/. To publish a new template, git clone https://github.com/jarred-sumner/bun, add a new folder to the \"examples\" folder, and submit a PR.", .{homedir}, ); } else { Output.prettyln( "This command is completely optional. To add a new local template, create a folder in $HOME/.bun-create/. To publish a new template, git clone https://github.com/jarred-sumner/bun, add a new folder to the \"examples\" folder, and submit a PR.", .{}, ); } Output.flush(); } }; const GitHandler = struct { var success: std.atomic.Atomic(u32) = undefined; var thread: std.Thread = undefined; pub fn spawn( destination: string, PATH: string, verbose: bool, ) void { success = std.atomic.Atomic(u32).init(0); thread = std.Thread.spawn(.{}, spawnThread, .{ destination, PATH, verbose }) catch |err| { Output.prettyErrorln("{s}", .{@errorName(err)}); Output.flush(); std.os.exit(1); }; } fn spawnThread( destination: string, PATH: string, verbose: bool, ) void { Output.Source.configureThread(); std.Thread.setName(thread, "git") catch {}; defer Output.flush(); const outcome = if (verbose) run(destination, PATH, true) catch false else run(destination, PATH, false) catch false; @fence(.Acquire); success.store( if (outcome) 1 else 2, .Release, ); std.Thread.Futex.wake(&success, 1); } pub fn wait() bool { @fence(.Release); while (success.load(.Acquire) == 0) { std.Thread.Futex.wait(&success, 0, 1000) catch continue; } const outcome = success.load(.Acquire) == 1; thread.join(); return outcome; } pub fn run( destination: string, PATH: string, comptime verbose: bool, ) !bool { const git_start = std.time.nanoTimestamp(); // This feature flag is disabled. // using libgit2 is slower than the CLI. // [481.00ms] git // [89.00ms] git // if (comptime FeatureFlags.use_libgit2) { // } if (which(&bun_path_buf, PATH, destination, "git")) |git| { const git_commands = .{ &[_]string{ std.mem.span(git), "init", "--quiet" }, &[_]string{ std.mem.span(git), "add", destination, "--ignore-errors" }, &[_]string{ std.mem.span(git), "commit", "-am", "\"Initial commit (via bun create)\"", "--quiet" }, }; if (comptime verbose) { Output.prettyErrorln("git backend: {s}", .{git}); } // same names, just comptime known values inline for (comptime std.meta.fieldNames(@TypeOf(Commands))) |command_field| { const command: []const string = @field(git_commands, command_field); var process = try std.ChildProcess.init(command, default_allocator); process.cwd = destination; process.stdin_behavior = .Inherit; process.stdout_behavior = .Inherit; process.stderr_behavior = .Inherit; defer process.deinit(); var term = try process.spawnAndWait(); _ = process.kill() catch undefined; } Output.prettyError("\n", .{}); Output.printStartEnd(git_start, std.time.nanoTimestamp()); Output.prettyError(" git\n", .{}); return true; } return false; } };