diff options
Diffstat (limited to 'src/cli/create_command.zig')
-rw-r--r-- | src/cli/create_command.zig | 833 |
1 files changed, 828 insertions, 5 deletions
diff --git a/src/cli/create_command.zig b/src/cli/create_command.zig index 14e6ac428..a5740a331 100644 --- a/src/cli/create_command.zig +++ b/src/cli/create_command.zig @@ -20,12 +20,835 @@ 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"); + +var bun_path_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; +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; + } + + var argv = allocator.alloc(string, count + 2) catch return; + defer allocator.free(argv); + + argv[0] = npm_client.bin; + argv[1] = "exec"; + { + var i: usize = 2; + splitter = std.mem.split(u8, task, " "); + while (splitter.next()) |split| { + argv[i] = split; + i += 1; + } + } + + if (strings.startsWith(task, "bun ")) { + if (bun_path orelse which(&bun_path_buf, PATH, cwd, "bun")) |bun_path_| { + bun_path = bun_path_; + argv = argv[2..]; + argv[0] = std.mem.span(bun_path_); + } + } + + Output.pretty("\n<r><d>$<b>", .{}); + for (argv) |arg, i| { + if (i > argv.len - 1) { + Output.print(" {s} ", .{arg}); + } else { + Output.print(" {s}", .{arg}); + } + } + Output.pretty("<r>", .{}); + 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; +} + +const CreateOptions = struct { + npm_client: ?NPMClient.Tag = null, + skip_install: bool = false, + overwrite: bool = false, + skip_git: bool = false, + + pub fn parse(allocator: *std.mem.Allocator) !CreateOptions { + const params = comptime [_]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("<POS>... ") catch unreachable, + }; + + 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")) { + clap.help(Output.writer(), ¶ms) catch {}; + 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; + } + + if (args.flag("--no-install")) { + opts.skip_install = true; + } + + if (args.flag("--no-git")) { + opts.skip_git = true; + } + + if (args.flag("--force")) { + opts.overwrite = true; + } + + return opts; + } +}; pub const CreateCommand = struct { - pub const Args = struct { - template_name: string, - directory_name: string, - }; + var client: HTTPClient = undefined; + var extracting_name_buf: [1024]u8 = undefined; + pub fn exec(ctx: Command.Context, positionals: []const []const u8) !void { + var create_options = try CreateOptions.parse(ctx.allocator); + const template = positionals[0]; + const dirname = positionals[1]; + var progress = std.Progress{}; + + var node_ = try progress.start(try std.fmt.bufPrint(&extracting_name_buf, "Loading {s}", .{template}), 0); + progress.supports_ansi_escape_codes = Output.enable_ansi_colors; + var node = node_.start("Downloading", 0); + + // alacritty is fast + if (std.os.getenvZ("ALACRITTY_LOG") != null) { + progress.refresh_rate_ns = std.time.ns_per_ms * 8; + } + + defer { + progress.root.end(); + progress.refresh(); + } + + var filesystem = try fs.FileSystem.init1(ctx.allocator, null); + + var tarball_bytes: MutableString = if (!(strings.eqlComptime(std.fs.path.extension(template), ".tgz") or strings.eqlComptime(std.fs.path.extension(template), ".tar.gz"))) + try Example.fetch(ctx, template, &progress, &node) + else + Example.fetchFromDisk(ctx, template, &progress, &node) catch |err| { + node.end(); + progress.refresh(); + Output.prettyErrorln("Error loading package from disk {s}", .{@errorName(err)}); + Output.flush(); + std.os.exit(1); + }; + + node.end(); + + node = progress.root.start(try std.fmt.bufPrint(&extracting_name_buf, "Decompressing {s}", .{template}), 0); + node.setCompletedItems(0); + node.setEstimatedTotalItems(0); + node.activate(); + 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.end(); + + node = progress.root.start(try std.fmt.bufPrint(&extracting_name_buf, "Extracting {s}", .{template}), 0); + node.setCompletedItems(0); + node.setEstimatedTotalItems(0); + node.activate(); + 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, + .overwrite_list = std.StringArrayHashMap(void).init(ctx.allocator), + }; + + var filename_writer = filesystem.dirname_store; + + const destination = try filesystem.dirname_store.append([]const u8, resolve_path.joinAbs(filesystem.top_level_dir, .auto, dirname)); + + if (!create_options.overwrite) { + try Archive.getOverwritingFileList( + tarball_buf_list.items, + destination, + &archive_context, + @TypeOf(filesystem.dirname_store), + filesystem.dirname_store, + 1, + ); + + if (archive_context.overwrite_list.count() > 0) { + node.end(); + progress.root.end(); + progress.refresh(); + + // Thank you create-react-app for this copy (and idea) + Output.prettyErrorln( + "<r><red>error<r><d>: <r>The directory <b><green>{s}<r> contains files that could conflict:", + .{ + std.fs.path.basename(destination), + }, + ); + for (archive_context.overwrite_list.keys()) |path| { + if (strings.endsWith(path, std.fs.path.sep_str)) { + Output.prettyErrorln("<r> <cyan>{s}<r>", .{path}); + } else { + Output.prettyErrorln("<r> {s}", .{path}); + } + } + Output.flush(); + std.os.exit(1); + } + } + + const extracted_file_count = try Archive.extractToDisk( + tarball_buf_list.items, + destination, + &archive_context, + 1, + false, + ); + + var plucker = pluckers[0]; + + if (!plucker.found or plucker.fd == 0) { + node.end(); + progress.root.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.end(); + node = progress.root.start(try std.fmt.bufPrint(&extracting_name_buf, "Updating package.json", .{}), 0); + + node.activate(); + progress.refresh(); + + var source = logger.Source.initPathString("package.json", plucker.contents.toOwnedSliceLeaky()); + var package_json_expr = ParseJSON(&source, ctx.log, ctx.allocator) catch |err| { + node.end(); + progress.root.end(); + progress.refresh(); + + Output.prettyErrorln("package.json failed to parse with error: {s}", .{@errorName(err)}); + Output.flush(); + std.os.exit(1); + }; + + if (ctx.log.errors > 0) { + node.end(); + + progress.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); + } + + if (package_json_expr.asProperty("name")) |name_expr| { + if (name_expr.expr.data != .e_string) { + node.end(); + progress.root.end(); + + progress.refresh(); + + 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 { + node.end(); + progress.root.end(); + + progress.refresh(); + + 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 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).?; + + 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 => {}, + } + } + } + } + + node.name = "Saving package.json"; + progress.maybeRefresh(); + + const package_json_file = std.fs.File{ .handle = plucker.fd }; + 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 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 PATH = env_loader.map.get("PATH") orelse ""; + + var npm_client_: ?NPMClient = null; + + 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, filesystem.top_level_dir, @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, filesystem.top_level_dir, 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) { + node.end(); + node = progress.root.start("Running pre-install tasks", preinstall_tasks.items.len); + node.setCompletedItems(0); + progress.refresh(); + + for (preinstall_tasks.items) |task, i| { + execTask(ctx.allocator, task, destination, PATH, npm_client_.?); + + node.setCompletedItems(i); + progress.refresh(); + } + } + + node.end(); + + if (npm_client_) |npm_client| { + var install_args = [_]string{ npm_client.bin, "install" }; + Output.printError("\n", .{}); + Output.flush(); + + Output.prettyln("\n<r><d>$ <b><cyan>{s}<r><d> install<r>", .{@tagName(npm_client.tag)}); + Output.flush(); + + var process = try std.ChildProcess.init(&install_args, ctx.allocator); + process.cwd = destination; + + defer { + 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", .{}); + } + + progress.refresh(); + + if (npm_client_ != null and !create_options.skip_install and postinstall_tasks.items.len > 0) { + node.end(); + node = progress.root.start("Running post-install tasks", postinstall_tasks.items.len); + node.setCompletedItems(0); + progress.refresh(); + + for (postinstall_tasks.items) |task, i| { + execTask(ctx.allocator, task, destination, PATH, npm_client_.?); + + node.setCompletedItems(i); + progress.refresh(); + } + } + + var parent_dir = try std.fs.openDirAbsolute(destination, .{}); + std.os.linkat(parent_dir.fd, "gitignore", parent_dir.fd, ".gitignore", 0) catch {}; + std.os.unlinkat( + parent_dir.fd, + "gitignore", + 0, + ) catch {}; + parent_dir.close(); + + if (!create_options.skip_git) { + 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", "-A", destination, "--ignore-errors" }, + &[_]string{ std.mem.span(git), "commit", "-am", "\"Initial Commit\"", "--quiet" }, + }; + // 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, ctx.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.printError("\n", .{}); + Output.printStartEnd(ctx.start_time, std.time.nanoTimestamp()); + Output.prettyErrorln(" <r><d>bun create {s} <r><d><b>({d} files)<r>", .{ template, extracted_file_count }); + 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) !*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, + }; + + 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, + + 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; + + pub fn print(examples: []const Example) void { + for (examples) |example, i| { + var app_name = example.name; + + if (example.description.len > 0) { + Output.pretty(" <r># {s}<r>\n <b>bun create <cyan>{s}<r><b> ./{s}-app<r>\n<d> \n\n", .{ + example.description, + example.name, + app_name, + }); + } else { + Output.pretty(" <r><b>bun create <cyan>{s}<r><b> ./{s}-app<r>\n\n", .{ + example.name, + app_name, + }); + } + } + } + + pub fn fetchFromDisk(ctx: Command.Context, absolute_path: string, refresher: *std.Progress, progress: *std.Progress.Node) !MutableString { + progress.name = "Reading local package"; + refresher.refresh(); + + var package = try std.fs.openFileAbsolute(absolute_path, .{ .read = true }); + var stat = try package.stat(); + if (stat.kind != .File) { + progress.end(); + Output.prettyErrorln("<r>{s} is not a file", .{absolute_path}); + Output.flush(); + std.os.exit(1); + } + + if (stat.size == 0) { + progress.end(); + Output.prettyErrorln("<r>{s} is an empty file", .{absolute_path}); + Output.flush(); + std.os.exit(1); + } + + var mutable_string = try MutableString.init(ctx.allocator, stat.size); + mutable_string.list.expandToCapacity(); + var bytes = try package.readAll(mutable_string.list.items); + try mutable_string.inflate(bytes); + return mutable_string; + } + + 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; + 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: <r><red>{s}<r>", .{@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); + + std.Thread.Futex.wait(&thread.done, 1, std.time.ns_per_ms * 100) catch {}; + + progress.setEstimatedTotalItems(thread.client.body_size); + progress.setCompletedItems(thread.client.read_count); + refresher.maybeRefresh(); + if (thread.done.load(.Acquire) == 0) { + while (true) { + std.Thread.Futex.wait(&thread.done, 1, std.time.ns_per_ms * 100) catch {}; + progress.setEstimatedTotalItems(thread.client.body_size); + progress.setCompletedItems(thread.client.read_count); + refresher.maybeRefresh(); + if (thread.done.load(.Acquire) == 1) { + break; + } + } + } + + refresher.maybeRefresh(); + + if (thread.response.status_code != 200) { + progress.end(); + refresher.refresh(); + Output.prettyErrorln("Error fetching tarball: <r><red>{d}<r>", .{thread.response.status_code}); + Output.flush(); + std.os.exit(1); + } + + refresher.refresh(); + thread.thread.join(); + + return thread.buffer; + } + + pub fn fetchAll(ctx: Command.Context) ![]const Example { + url = URL.parse(examples_url); + client = HTTPClient.init(ctx.allocator, .GET, url, .{}, ""); + client.timeout = timeout; + 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("<r><red>{s}<r> while trying to fetch examples list. Please try again", .{@errorName(err)}); + Output.flush(); + std.os.exit(1); + }, + } + }; + + if (response.status_code != 200) { + Output.prettyErrorln("<r><red>{d}<r> 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: <r><red>{s}<r>", .{@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 { + const time = std.time.nanoTimestamp(); + const examples = try Example.fetchAll(ctx); + Output.printStartEnd(time, std.time.nanoTimestamp()); + Output.prettyln(" <d>Fetched examples<r>", .{}); + + Output.prettyln("Welcome to Bun! Create a new project by pasting any of the following:\n\n", .{}); + Output.flush(); + + Example.print(examples); - pub fn exec(ctx: Command.Context) !void {} + Output.pretty("<d>To add a new template, git clone https://github.com/jarred-sumner/bun, add a new folder to the \"examples\" folder, and submit a PR.<r>", .{}); + Output.flush(); + } }; |