diff options
author | 2021-10-14 16:14:48 -0700 | |
---|---|---|
committer | 2021-10-14 16:14:48 -0700 | |
commit | fe24b96d61b372af49a346db595d8261ecfdfe5f (patch) | |
tree | 571bee2ffeafe2e4c6fd999f4e3f182a75bc6d9a | |
parent | 4b6d8a152f2d298bfd8333cdc1534eb540ffbb79 (diff) | |
download | bun-fe24b96d61b372af49a346db595d8261ecfdfe5f.tar.gz bun-fe24b96d61b372af49a346db595d8261ecfdfe5f.tar.zst bun-fe24b96d61b372af49a346db595d8261ecfdfe5f.zip |
Support passing an absolute path
-rw-r--r-- | examples/README.md | 109 | ||||
-rw-r--r-- | examples/bun-create.md | 65 | ||||
-rw-r--r-- | misctools/tgz.zig | 2 | ||||
-rw-r--r-- | src/cli/create_command.zig | 308 |
4 files changed, 313 insertions, 171 deletions
diff --git a/examples/README.md b/examples/README.md index e69de29bb..5b857090a 100644 --- a/examples/README.md +++ b/examples/README.md @@ -0,0 +1,109 @@ +# bun create + +`bun create` is a fast way to create a new project from a template. At the time of writing, `bun create react app` runs ~14x faster on my local computer than `yarn create react-app app`. `bun create` currently does no caching (though your npm client does) + +## Usage + +Templates are downloaded from folders inside `examples/` in Bun's GitHub repo. Running `bun create react ./local-path` downloads the `react` folder from `examples/react`. + +Create a new Next.js project: + +```bash +bun create next ./app` +``` + +Create a new React project: + +```bash +bun create react ./app +``` + +To see a list of available templates, run + +```bash +bun create +``` + +### Advanced + +| Flag | Description | +| ---------------------- | -------------------------------------- | +| --npm | Use `npm` for tasks & install | +| --yarn | Use `yarn` for tasks & install | +| --pnpm | Use `pnpm` for tasks & install | +| --force | Overwrite existing files | +| --no-install | Skip installing `node_modules` & tasks | +| --no-git | Don't initialize a git repository | +| ---------------------- | ----------------------------------- | + +By default, `bun create` will cancel if there are existing files it would overwrite. You can pass `--force` to disable this behavior. + +## Adding a new template + +Clone this repository and a new folder in `examples/` with your new template. The `package.json` must have a `name` that starts with `@bun-examples/`. Do not worry about publishing it, that will happen automaticallly after the PR is merged. + +Make sure to include a `.gitignore` that includes `node_modules` so that `node_modules` aren't checked in to git when people download the template. + +#### Testing your new template + +### Config + +The `bun-create` section of package.json is automatically removed from the `package.json` on disk. This lets you add create-only steps without waiting for an extra package to install. + +There are currently two options: + +- `postinstall` +- `preinstall` + +They can be an array of strings or one string. An array of steps will be executed in order. + +Here is an example: + +```json +{ + "name": "@bun-examples/next", + "version": "0.0.31", + "main": "index.js", + "dependencies": { + "next": "11.1.2", + "react": "^17.0.2", + "react-dom": "^17.0.2", + "react-is": "^17.0.2" + }, + "devDependencies": { + "@types/react": "^17.0.19", + "bun-framework-next": "^0.0.0-21", + "typescript": "^4.3.5" + }, + "bun-create": { + "postinstall": ["bun bun --use next"] + } +} +``` + +By default, all commands run inside the environment exposed by the auto-detected npm client. This incurs a significant performance penalty, something like 150ms spent waiting for the npm client to start on each invocation. + +Any command that starts with `"bun "` will be run without npm, relying on the first `bun` binary in `$PATH`. + +## How `bun create` works + +When you run `bun create ${template} ${destination}`, here's what happens: + +1. GET `registry.npmjs.org/@bun-examples/${template}/latest` and parse it +2. GET `registry.npmjs.org/@bun-examples/${template}/-/${template}-${latestVersion}.tgz` +3. Decompress & extract `${template}-${latestVersion}.tgz` into `${destination}` + + - If there are files that would overwrite, warn and exit unless `--force` is passed + +4. Parse the `package.json` (again!), update `name` to be `${basename(destination)}`, remove the `bun-create` section from the `package.json` and save updated `package.json` to disk +5. Auto-detect the npm client, preferring `pnpm`, `yarn` (v1), and lastly `npm` +6. Run any tasks defined in `"bun-create": { "preinstall" }` with the npm client +7. Run `${npmClient} install` +8. Run any tasks defined in `"bun-create": { "preinstall" }` with the npm client +9. Run `git init; git add -A .; git commit -am "Initial Commit";`. + + - Rename `gitignore` to `.gitignore`. NPM automatically removes `.gitignore` files from appearing in packages. + +10. Done + +`../misctools/publish-examples.js` publishes all examples to npm. diff --git a/examples/bun-create.md b/examples/bun-create.md deleted file mode 100644 index c74db89e4..000000000 --- a/examples/bun-create.md +++ /dev/null @@ -1,65 +0,0 @@ -# `bun create` - -## Config - -The `bun-create` section of package.json is automatically removed from the final output. This lets you add create-only steps without installing an extra package. - -There are currently two options: - -- `postinstall` -- `preinstall` - -They can be an array of strings or one string. An array of strings will be executed one after another. - -Here are examples: - -``` -{ - "name": "@bun-examples/next", - "version": "0.0.31", - "main": "index.js", - "dependencies": { - "next": "11.1.2", - "react": "^17.0.2", - "react-dom": "^17.0.2", - "react-is": "^17.0.2" - }, - "devDependencies": { - "@types/react": "^17.0.19", - "bun-framework-next": "^0.0.0-21", - "typescript": "^4.3.5" - }, - "bun-create": { - "postinstall": [ - "bun bun --use next" - ] - } -} -``` - -By default, all commands run inside the environment exposed by the auto-detected npm client. This incurs a significant performance penalty, something like 150ms wasted on waiting for the npm client to start on each invocation. - -Any command that starts with `"bun "` will be run without npm. - -## How it works - -When you run `bun create ${template} ${destination}`, here's what happens: - -1. GET `registry.npmjs.org/@bun-examples/${template}/latest` and parse it -2. GET `registry.npmjs.org/@bun-examples/${template}/-/${template}-${latestVersion}.tgz` -3. Decompress & extract `${template}-${latestVersion}.tgz` into `${destination}` - - - If there are files that would overwrite, warn and exit unless `--force` is passed - -4. Parse the `package.json` (again!), update `name` to be `${basename(destination)}`, remove the `bun-create` section from the `package.json` and save updated `package.json` to disk -5. Auto-detect the npm client, preferring `pnpm`, `yarn` (v1), and lastly `npm` -6. Run any tasks defined in `"bun-create": { "preinstall" }` with the npm client -7. Run `${npmClient} install` -8. Run any tasks defined in `"bun-create": { "preinstall" }` with the npm client -9. Run `git init; git add -A .; git commit -am "Initial Commit";`. - - - Rename `gitignore` to `.gitignore`. NPM automatically removes `.gitignore` files from appearing in packages. - -10. Done - -`../misctools/publish-examples.js` publishes all examples to npm. diff --git a/misctools/tgz.zig b/misctools/tgz.zig index dc18564bf..5e792d0fb 100644 --- a/misctools/tgz.zig +++ b/misctools/tgz.zig @@ -71,5 +71,5 @@ pub fn main() anyerror!void { tarball_buf_list = std.ArrayListUnmanaged(u8){ .capacity = file_buf.len, .items = file_buf }; } - try Archive.extractToDisk(file_buf, folder, 1, false); + try Archive.extractToDisk(file_buf, folder, null, 1, false); } diff --git a/src/cli/create_command.zig b/src/cli/create_command.zig index a5740a331..d501e0473 100644 --- a/src/cli/create_command.zig +++ b/src/cli/create_command.zig @@ -95,18 +95,18 @@ const CreateOptions = struct { 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, - }; - + 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("<POS>... ") 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| { @@ -115,8 +115,15 @@ const CreateOptions = struct { return err; }; - if (args.flag("--help")) { - clap.help(Output.writer(), ¶ms) catch {}; + 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("<r><b>bun create<r> flags:\n", .{}); + clap.help(Output.writer(), params[1..]) catch {}; + Output.flush(); std.os.exit(0); } @@ -153,9 +160,23 @@ pub const CreateCommand = struct { 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); + 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 = positionals[0]; 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 std.fmt.bufPrint(&extracting_name_buf, "Loading {s}", .{template}), 0); @@ -163,7 +184,7 @@ pub const CreateCommand = struct { var node = node_.start("Downloading", 0); // alacritty is fast - if (std.os.getenvZ("ALACRITTY_LOG") != null) { + if (env_loader.map.get("ALACRITTY_LOG") != null) { progress.refresh_rate_ns = std.time.ns_per_ms * 8; } @@ -172,115 +193,193 @@ pub const CreateCommand = struct { progress.refresh(); } - var filesystem = try fs.FileSystem.init1(ctx.allocator, null); + var package_json_contents: MutableString = undefined; + var package_json_file: std.fs.File = undefined; - 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); - }; + if (!std.fs.path.isAbsolute(template)) { + var tarball_bytes: MutableString = try Example.fetch(ctx, template, &progress, &node); - node.end(); + 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(); + 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 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(); + 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.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(); + 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 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 archive_context = Archive.Context{ + .pluckers = &pluckers, + .overwrite_list = std.StringArrayHashMap(void).init(ctx.allocator), + }; - var filename_writer = filesystem.dirname_store; + if (!create_options.overwrite) { + try Archive.getOverwritingFileList( + tarball_buf_list.items, + destination, + &archive_context, + @TypeOf(filesystem.dirname_store), + filesystem.dirname_store, + 1, + ); - const destination = try filesystem.dirname_store.append([]const u8, resolve_path.joinAbs(filesystem.top_level_dir, .auto, dirname)); + if (archive_context.overwrite_list.count() > 0) { + node.end(); + progress.root.end(); + progress.refresh(); - if (!create_options.overwrite) { - try Archive.getOverwritingFileList( + // 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, - @TypeOf(filesystem.dirname_store), - filesystem.dirname_store, 1, + false, ); - if (archive_context.overwrite_list.count() > 0) { + 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(); + + package_json_contents = plucker.contents; + package_json_file = std.fs.File{ .handle = plucker.fd }; + } else { + const template_dir = std.fs.openDirAbsolute(template, .{ .iterate = true }) catch |err| { + node.end(); + progress.root.end(); + progress.refresh(); + + Output.prettyErrorln("<r><red>{s}<r>: 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.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.prettyErrorln("<r><red>{s}<r>: creating dir {s}", .{ @errorName(err), destination }); Output.flush(); std.os.exit(1); + }; + + var walker = try template_dir.walk(ctx.allocator); + defer walker.deinit(); + while (try walker.next()) |entry| { + // TODO: make this not walk these folders entirely + // rather than checking each file path..... + if (entry.kind != .File or + std.mem.indexOf(u8, entry.path, "node_modules") != null or + std.mem.indexOf(u8, entry.path, ".git") != null) continue; + + entry.dir.copyFile(entry.basename, destination_dir, entry.path, .{}) catch { + if (std.fs.path.dirname(entry.path)) |entry_dirname| { + destination_dir.makePath(entry_dirname) catch {}; + } + entry.dir.copyFile(entry.basename, destination_dir, entry.path, .{}) catch |err| { + node.end(); + progress.root.end(); + progress.refresh(); + + Output.prettyErrorln("<r><red>{s}<r>: copying file {s}", .{ @errorName(err), entry.path }); + Output.flush(); + std.os.exit(1); + }; + }; } - } - const extracted_file_count = try Archive.extractToDisk( - tarball_buf_list.items, - destination, - &archive_context, - 1, - false, - ); + package_json_file = destination_dir.openFile("package.json", .{ .read = true, .write = true }) catch |err| { + node.end(); + progress.root.end(); + progress.refresh(); - var plucker = pluckers[0]; + Output.prettyErrorln("Failed to open package.json due to error <r><red>{s}", .{@errorName(err)}); + Output.flush(); + std.os.exit(1); + }; + const stat = package_json_file.stat() catch |err| { + node.end(); + progress.root.end(); + progress.refresh(); - 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); - } + Output.prettyErrorln("Failed to stat package.json due to error <r><red>{s}", .{@errorName(err)}); + Output.flush(); + std.os.exit(1); + }; - node.end(); - node = progress.root.start(try std.fmt.bufPrint(&extracting_name_buf, "Updating package.json", .{}), 0); + if (stat.kind != .File or stat.size == 0) { + node.end(); + progress.root.end(); + progress.refresh(); - node.activate(); - 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.inflate(package_json_file.readAll(package_json_contents.list.items) catch |err| { + node.end(); + progress.root.end(); + progress.refresh(); - var source = logger.Source.initPathString("package.json", plucker.contents.toOwnedSliceLeaky()); + Output.prettyErrorln("Error reading package.json: <r><red>{s}", .{@errorName(err)}); + Output.flush(); + std.os.exit(1); + }) catch unreachable; + } + + var source = logger.Source.initPathString("package.json", package_json_contents.list.items); var package_json_expr = ParseJSON(&source, ctx.log, ctx.allocator) catch |err| { node.end(); progress.root.end(); @@ -399,7 +498,6 @@ pub const CreateCommand = struct { 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| { @@ -408,15 +506,6 @@ pub const CreateCommand = struct { 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; @@ -457,6 +546,7 @@ pub const CreateCommand = struct { node.end(); if (npm_client_) |npm_client| { + const start_time = std.time.nanoTimestamp(); var install_args = [_]string{ npm_client.bin, "install" }; Output.printError("\n", .{}); Output.flush(); @@ -468,12 +558,18 @@ pub const CreateCommand = struct { process.cwd = destination; defer { + Output.printErrorln("\n", .{}); + Output.printStartEnd(start_time, std.time.nanoTimestamp()); + Output.prettyError(" <r><d>{s} install<r>\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", .{}); @@ -530,7 +626,7 @@ pub const CreateCommand = struct { 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.prettyErrorln(" <r><d>bun create {s}<r>", .{template}); Output.flush(); } }; @@ -848,6 +944,8 @@ pub const CreateListExamplesCommand = struct { Example.print(examples); + _ = try CreateOptions.parse(ctx.allocator, true); + 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(); } |