aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGravatar Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> 2022-08-04 21:42:49 -0700
committerGravatar Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> 2022-08-04 21:42:49 -0700
commitd034c004f99cb660b88ed95a851d92291c45b7cb (patch)
tree8b6122581cf859cecf7e303fd9ba2a7c968faa8a /src
parent7ba61bc98341c1a357a8b0a9323643d6162976e0 (diff)
downloadbun-d034c004f99cb660b88ed95a851d92291c45b7cb.tar.gz
bun-d034c004f99cb660b88ed95a851d92291c45b7cb.tar.zst
bun-d034c004f99cb660b88ed95a851d92291c45b7cb.zip
Implement `bun init` subcommand
Diffstat (limited to 'src')
-rw-r--r--src/cli.zig7
-rw-r--r--src/cli/README-for-init.md15
-rw-r--r--src/cli/gitignore-for-init169
-rw-r--r--src/cli/init_command.zig402
-rw-r--r--src/cli/tsconfig-for-init.json14
-rw-r--r--src/fs.zig8
-rw-r--r--src/js_ast.zig17
-rw-r--r--src/string_immutable.zig2
8 files changed, 628 insertions, 6 deletions
diff --git a/src/cli.zig b/src/cli.zig
index e849d258f..5dc954be9 100644
--- a/src/cli.zig
+++ b/src/cli.zig
@@ -645,9 +645,8 @@ const AutoCommand = struct {
try HelpCommand.execWithReason(allocator, .invalid_command);
}
};
-const InitCommand = struct {
- pub fn exec(_: std.mem.Allocator) !void {}
-};
+const InitCommand = @import("./cli/init_command.zig").InitCommand;
+
pub const HelpCommand = struct {
pub fn exec(allocator: std.mem.Allocator) !void {
@setCold(true);
@@ -909,7 +908,7 @@ pub const Command = struct {
switch (tag) {
.DiscordCommand => return try DiscordCommand.exec(allocator),
.HelpCommand => return try HelpCommand.exec(allocator),
- .InitCommand => return try InitCommand.exec(allocator),
+ .InitCommand => return try InitCommand.exec(allocator, std.os.argv),
else => {},
}
diff --git a/src/cli/README-for-init.md b/src/cli/README-for-init.md
new file mode 100644
index 000000000..7a0dced5d
--- /dev/null
+++ b/src/cli/README-for-init.md
@@ -0,0 +1,15 @@
+# {[name]s}
+
+To install dependencies:
+
+```bash
+bun install
+```
+
+To run:
+
+```bash
+bun run {[entryPoint]s}
+```
+
+This project was created using `bun init` in bun v{[bunVersion]any}. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
diff --git a/src/cli/gitignore-for-init b/src/cli/gitignore-for-init
new file mode 100644
index 000000000..f81d56eaa
--- /dev/null
+++ b/src/cli/gitignore-for-init
@@ -0,0 +1,169 @@
+# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
+
+# Logs
+
+logs
+_.log
+npm-debug.log_
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+.pnpm-debug.log*
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+
+report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
+
+# Runtime data
+
+pids
+_.pid
+_.seed
+\*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+
+lib-cov
+
+# Coverage directory used by tools like istanbul
+
+coverage
+\*.lcov
+
+# nyc test coverage
+
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+
+bower_components
+
+# node-waf configuration
+
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+
+build/Release
+
+# Dependency directories
+
+node_modules/
+jspm_packages/
+
+# Snowpack dependency directory (https://snowpack.dev/)
+
+web_modules/
+
+# TypeScript cache
+
+\*.tsbuildinfo
+
+# Optional npm cache directory
+
+.npm
+
+# Optional eslint cache
+
+.eslintcache
+
+# Optional stylelint cache
+
+.stylelintcache
+
+# Microbundle cache
+
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+
+.node_repl_history
+
+# Output of 'npm pack'
+
+\*.tgz
+
+# Yarn Integrity file
+
+.yarn-integrity
+
+# dotenv environment variable files
+
+.env
+.env.development.local
+.env.test.local
+.env.production.local
+.env.local
+
+# parcel-bundler cache (https://parceljs.org/)
+
+.cache
+.parcel-cache
+
+# Next.js build output
+
+.next
+out
+
+# Nuxt.js build / generate output
+
+.nuxt
+dist
+
+# Gatsby files
+
+.cache/
+
+# Comment in the public line in if your project uses Gatsby and not Next.js
+
+# https://nextjs.org/blog/next-9-1#public-directory-support
+
+# public
+
+# vuepress build output
+
+.vuepress/dist
+
+# vuepress v2.x temp and cache directory
+
+.temp
+.cache
+
+# Docusaurus cache and generated files
+
+.docusaurus
+
+# Serverless directories
+
+.serverless/
+
+# FuseBox cache
+
+.fusebox/
+
+# DynamoDB Local files
+
+.dynamodb/
+
+# TernJS port file
+
+.tern-port
+
+# Stores VSCode versions used for testing VSCode extensions
+
+.vscode-test
+
+# yarn v2
+
+.yarn/cache
+.yarn/unplugged
+.yarn/build-state.yml
+.yarn/install-state.gz
+.pnp.\*
diff --git a/src/cli/init_command.zig b/src/cli/init_command.zig
new file mode 100644
index 000000000..fe5164110
--- /dev/null
+++ b/src/cli/init_command.zig
@@ -0,0 +1,402 @@
+const bun = @import("../global.zig");
+const string = bun.string;
+const Output = bun.Output;
+const Global = bun.Global;
+const Environment = bun.Environment;
+const strings = bun.strings;
+const MutableString = bun.MutableString;
+const stringZ = bun.stringZ;
+const default_allocator = bun.default_allocator;
+const C = bun.C;
+const std = @import("std");
+const open = @import("../open.zig");
+const CLI = @import("../cli.zig");
+const Fs = @import("../fs.zig");
+const ParseJSON = @import("../json_parser.zig").ParseJSONUTF8;
+const js_parser = @import("../js_parser.zig");
+const js_ast = @import("../js_ast.zig");
+const linker = @import("../linker.zig");
+const options = @import("../options.zig");
+const initializeStore = @import("./create_command.zig").initializeStore;
+const lex = @import("../js_lexer.zig");
+const logger = @import("../logger.zig");
+const JSPrinter = @import("../js_printer.zig");
+
+fn exists(path: anytype) bool {
+ if (@TypeOf(path) == [:0]const u8 or @TypeOf(path) == [:0]u8) {
+ if (std.os.accessZ(path, 0)) {
+ return true;
+ } else |_| {
+ return false;
+ }
+ } else {
+ if (std.os.access(path, 0)) {
+ return true;
+ } else |_| {
+ return false;
+ }
+ }
+}
+pub const InitCommand = struct {
+ fn prompt(
+ alloc: std.mem.Allocator,
+ comptime label: string,
+ default: []const u8,
+ _: bool,
+ ) ![]const u8 {
+ Output.pretty(label, .{});
+ if (default.len > 0) {
+ Output.pretty("<d>({s}):<r> ", .{default});
+ }
+
+ Output.flush();
+
+ const input = try std.io.getStdIn().reader().readUntilDelimiterAlloc(alloc, '\n', 1024);
+ if (input.len > 0) {
+ return input;
+ } else {
+ return default;
+ }
+ }
+
+ const default_gitignore = @embedFile("gitignore-for-init");
+ const default_tsconfig = @embedFile("tsconfig-for-init.json");
+ const README = @embedFile("README-for-init.md");
+
+ // TODO: unicode case folding
+ fn normalizePackageName(allocator: std.mem.Allocator, input: []const u8) ![]const u8 {
+ // toLowerCase
+ const needs_normalize = brk: {
+ for (input) |c| {
+ if ((c >= 'A' and c <= 'Z') or c == ' ' or c == '"' or c == '\'') {
+ break :brk true;
+ }
+ }
+ break :brk false;
+ };
+
+ if (!needs_normalize) {
+ return input;
+ }
+
+ var new = try allocator.alloc(u8, input.len);
+ for (new) |c, i| {
+ if (c >= 'A' and c <= 'Z') {
+ new[i] = c + ('a' - 'A');
+ } else if (c == ' ' or c == '"' or c == '\'') {
+ new[i] = '-';
+ } else {
+ new[i] = c;
+ }
+ }
+
+ return new;
+ }
+
+ const PackageJSONFields = struct {
+ name: string = "project",
+ @"type": string = "module",
+ object: *js_ast.E.Object = undefined,
+ entry_point: string = "",
+ };
+
+ pub fn exec(alloc: std.mem.Allocator, argv: [][*:0]u8) !void {
+ var fs = try Fs.FileSystem.init1(alloc, null);
+ const pathname = Fs.PathName.init(fs.topLevelDirWithoutTrailingSlash());
+ const destination_dir = std.fs.cwd();
+
+ var fields = PackageJSONFields{};
+
+ var package_json_file = destination_dir.openFile("package.json", .{ .mode = .read_write }) catch null;
+ var package_json_contents: MutableString = MutableString.initEmpty(alloc);
+ initializeStore();
+ read_package_json: {
+ if (package_json_file) |pkg| {
+ const stat = pkg.stat() catch break :read_package_json;
+
+ if (stat.kind != .File or stat.size == 0) {
+ break :read_package_json;
+ }
+ package_json_contents = try MutableString.init(alloc, stat.size);
+ package_json_contents.list.expandToCapacity();
+
+ _ = pkg.preadAll(package_json_contents.list.items, 0) catch {
+ package_json_file = null;
+ break :read_package_json;
+ };
+ }
+ }
+
+ fields.name = brk: {
+ if (normalizePackageName(alloc, if (pathname.filename.len > 0) pathname.filename else "")) |name| {
+ if (name.len > 0) {
+ break :brk name;
+ }
+ } else |_| {}
+
+ break :brk "project";
+ };
+ var did_load_package_json = false;
+ if (package_json_contents.list.items.len > 0) {
+ process_package_json: {
+ var source = logger.Source.initPathString("package.json", package_json_contents.list.items);
+ var log = logger.Log.init(alloc);
+ var package_json_expr = ParseJSON(&source, &log, alloc) catch {
+ package_json_file = null;
+ break :process_package_json;
+ };
+
+ if (package_json_expr.data != .e_object) {
+ package_json_file = null;
+ break :process_package_json;
+ }
+
+ fields.object = package_json_expr.data.e_object;
+
+ if (package_json_expr.get("name")) |name| {
+ if (name.asString(alloc)) |str| {
+ fields.name = str;
+ }
+ }
+
+ if (package_json_expr.get("module") orelse package_json_expr.get("main")) |name| {
+ if (name.asString(alloc)) |str| {
+ fields.entry_point = str;
+ }
+ }
+
+ did_load_package_json = true;
+ }
+ }
+
+ if (fields.entry_point.len == 0) {
+ infer: {
+ const paths_to_try = [_][:0]const u8{
+ @as([:0]const u8, "index.mts"),
+ @as([:0]const u8, "index.tsx"),
+ @as([:0]const u8, "index.ts"),
+ @as([:0]const u8, "index.jsx"),
+ @as([:0]const u8, "index.mjs"),
+ @as([:0]const u8, "index.js"),
+ };
+
+ for (paths_to_try) |path| {
+ if (exists(path)) {
+ fields.entry_point = std.mem.span(path);
+ break :infer;
+ }
+ }
+
+ fields.entry_point = "index.ts";
+ }
+ }
+
+ if (!did_load_package_json) {
+ fields.object = js_ast.Expr.init(
+ js_ast.E.Object,
+ .{},
+ logger.Loc.Empty,
+ ).data.e_object;
+ }
+
+ const auto_yes = brk: {
+ for (argv) |arg_| {
+ const arg = bun.span(arg_);
+ if (strings.eqlComptime(arg, "-y") or strings.eqlComptime(arg, "--yes")) {
+ break :brk true;
+ }
+ }
+ break :brk false;
+ };
+
+ if (!auto_yes) {
+ Output.prettyln("<r><b>bun init<r> helps you get started with a minimal project and tries to guess sensible defaults. <d>Press ^C anytime to quit<r>\n\n", .{});
+ Output.flush();
+
+ fields.name = try normalizePackageName(alloc, try prompt(
+ alloc,
+ "<r><cyan>package name<r> ",
+ fields.name,
+ Output.enable_ansi_colors_stdout,
+ ));
+ fields.entry_point = try prompt(
+ alloc,
+ "<r><cyan>entry point<r> ",
+ fields.entry_point,
+ Output.enable_ansi_colors_stdout,
+ );
+ try Output.writer().writeAll("\n");
+ Output.flush();
+ }
+
+ const Steps = struct {
+ write_gitignore: bool = true,
+ write_package_json: bool = true,
+ write_tsconfig: bool = true,
+ write_readme: bool = true,
+ };
+
+ var steps = Steps{};
+
+ steps.write_gitignore = brk: {
+ if (exists(".gitignore")) {
+ break :brk false;
+ }
+
+ break :brk true;
+ };
+
+ steps.write_readme = !exists("README.md") and !exists("README") and !exists("README.txt") and !exists("README.mdx");
+
+ steps.write_tsconfig = brk: {
+ if (exists("tsconfig.json")) {
+ break :brk false;
+ }
+
+ if (exists("jsconfig.json")) {
+ break :brk false;
+ }
+
+ break :brk true;
+ };
+
+ {
+ try fields.object.putString(alloc, "name", fields.name);
+ if (fields.entry_point.len > 0) {
+ if (fields.object.hasProperty("module")) {
+ try fields.object.putString(alloc, "module", fields.entry_point);
+ try fields.object.putString(alloc, "type", "module");
+ } else if (fields.object.hasProperty("main")) {
+ try fields.object.putString(alloc, "main", fields.entry_point);
+ } else {
+ try fields.object.putString(alloc, "module", fields.entry_point);
+ try fields.object.putString(alloc, "type", "module");
+ }
+ }
+
+ const needs_dev_dependencies = brk: {
+ if (fields.object.get("devDependencies")) |deps| {
+ if (deps.hasAnyPropertyNamed(&.{"bun-types"})) {
+ break :brk false;
+ }
+ }
+
+ break :brk true;
+ };
+
+ if (needs_dev_dependencies) {
+ var dev_dependencies = fields.object.get("devDependencies") orelse js_ast.Expr.init(js_ast.E.Object, js_ast.E.Object{}, logger.Loc.Empty);
+ const version = comptime brk: {
+ var base = Global.version;
+ base.patch = 0;
+ break :brk base;
+ };
+
+ try dev_dependencies.data.e_object.putString(alloc, "bun-types", comptime std.fmt.comptimePrint("^{any}", .{version.fmt("")}));
+ try fields.object.put(alloc, "devDependencies", dev_dependencies);
+ }
+ }
+
+ write_package_json: {
+ if (package_json_file == null) {
+ package_json_file = try std.fs.cwd().createFileZ("package.json", .{});
+ }
+ var package_json_writer = JSPrinter.NewFileWriter(package_json_file.?);
+
+ const written = JSPrinter.printJSON(
+ @TypeOf(package_json_writer),
+ package_json_writer,
+ js_ast.Expr{ .data = .{ .e_object = fields.object }, .loc = logger.Loc.Empty },
+ &logger.Source.initEmptyFile("package.json"),
+ ) catch |err| {
+ Output.prettyErrorln("package.json failed to write due to error {s}", .{@errorName(err)});
+ package_json_file = null;
+ break :write_package_json;
+ };
+
+ std.os.ftruncate(package_json_file.?.handle, written + 1) catch {};
+ package_json_file.?.close();
+ }
+
+ if (package_json_file != null) {
+ Output.prettyln("<r><green>Done!<r> A package.json file was saved in the current directory.", .{});
+ }
+
+ if (fields.entry_point.len > 0 and !exists(fields.entry_point)) {
+ var entry = try std.fs.cwd().createFile(fields.entry_point, .{ .truncate = true });
+ entry.writeAll("console.log(\"Hello via Bun!\");") catch {};
+ entry.close();
+ Output.prettyln(" + <r><d>{s}<r>", .{fields.entry_point});
+ Output.flush();
+ }
+
+ if (steps.write_gitignore) {
+ brk: {
+ var file = std.fs.cwd().createFileZ(".gitignore", .{ .truncate = true }) catch break :brk;
+ defer file.close();
+ file.writeAll(default_gitignore) catch break :brk;
+ Output.prettyln(" + <r><d>.gitignore<r>", .{});
+ Output.flush();
+ }
+ }
+
+ if (steps.write_tsconfig) {
+ brk: {
+ const extname = std.fs.path.extension(fields.entry_point);
+ const loader = options.defaultLoaders.get(extname) orelse options.Loader.ts;
+ const filename = if (loader.isTypeScript())
+ "tsconfig.json"
+ else
+ "jsconfig.json";
+ var file = std.fs.cwd().createFileZ(filename, .{ .truncate = true }) catch break :brk;
+ defer file.close();
+ file.writeAll(default_tsconfig) catch break :brk;
+ Output.prettyln(" + <r><d>{s}<r><d> (for editor auto-complete)<r>", .{filename});
+ Output.flush();
+ }
+ }
+
+ if (steps.write_readme) {
+ brk: {
+ const filename = "README.md";
+ var file = std.fs.cwd().createFileZ(filename, .{ .truncate = true }) catch break :brk;
+ defer file.close();
+ file.writer().print(README, .{
+ .name = fields.name,
+ .bunVersion = Global.version.fmt(""),
+ .entryPoint = fields.entry_point,
+ }) catch break :brk;
+ Output.prettyln(" + <r><d>{s}<r>", .{filename});
+ Output.flush();
+ }
+ }
+
+ if (fields.entry_point.len > 0) {
+ Output.prettyln("\nTo get started, run:", .{});
+ if (strings.containsAny(
+ " \"'",
+ fields.entry_point,
+ )) {
+ Output.prettyln(" <r><cyan>bun run {any}<r>", .{JSPrinter.formatJSONString(fields.entry_point)});
+ } else {
+ Output.prettyln(" <r><cyan>bun run {s}<r>", .{fields.entry_point});
+ }
+ }
+
+ Output.flush();
+
+ if (exists("package.json")) {
+ var process = std.ChildProcess.init(
+ &.{
+ try std.fs.selfExePathAlloc(alloc),
+ "install",
+ },
+ alloc,
+ );
+ process.stderr_behavior = .Pipe;
+ process.stdin_behavior = .Pipe;
+ process.stdout_behavior = .Pipe;
+ _ = try process.spawnAndWait();
+ }
+ }
+};
diff --git a/src/cli/tsconfig-for-init.json b/src/cli/tsconfig-for-init.json
new file mode 100644
index 000000000..feee4b584
--- /dev/null
+++ b/src/cli/tsconfig-for-init.json
@@ -0,0 +1,14 @@
+{
+ "compilerOptions": {
+ "lib": ["ESNext"],
+ "module": "esnext",
+ "target": "esnext",
+ "moduleResolution": "node",
+
+ // so that if your project isn't using TypeScript, it still has autocomplete
+ "allowJs": true,
+
+ // "bun-types" is the important part
+ "types": ["bun-types"]
+ }
+}
diff --git a/src/fs.zig b/src/fs.zig
index 78888dcd4..e54c7e0e6 100644
--- a/src/fs.zig
+++ b/src/fs.zig
@@ -93,6 +93,14 @@ pub const FileSystem = struct {
threadlocal var tmpdir_handle: ?std.fs.Dir = null;
+ pub fn topLevelDirWithoutTrailingSlash(this: *const FileSystem) []const u8 {
+ if (this.top_level_dir.len > 1 and this.top_level_dir[this.top_level_dir.len - 1] == std.fs.path.sep) {
+ return this.top_level_dir[0 .. this.top_level_dir.len - 1];
+ } else {
+ return this.top_level_dir;
+ }
+ }
+
pub fn tmpdir(fs: *FileSystem) std.fs.Dir {
if (tmpdir_handle == null) {
tmpdir_handle = fs.fs.openTmpDir() catch unreachable;
diff --git a/src/js_ast.zig b/src/js_ast.zig
index eb28091c9..aa53efadd 100644
--- a/src/js_ast.zig
+++ b/src/js_ast.zig
@@ -1335,6 +1335,21 @@ pub const E = struct {
return if (asProperty(self, key)) |query| query.expr else @as(?Expr, null);
}
+ pub fn put(self: *Object, allocator: std.mem.Allocator, key: string, expr: Expr) !void {
+ if (asProperty(self, key)) |query| {
+ self.properties.ptr[query.i].value = expr;
+ } else {
+ try self.properties.push(allocator, .{
+ .key = Expr.init(E.String, E.String.init(key), expr.loc),
+ .value = expr,
+ });
+ }
+ }
+
+ pub fn putString(self: *Object, allocator: std.mem.Allocator, key: string, value: string) !void {
+ return try put(self, allocator, key, Expr.init(E.String, E.String.init(value), logger.Loc.Empty));
+ }
+
pub const SetError = error{ OutOfMemory, Clobber };
pub fn set(self: *const Object, key: Expr, allocator: std.mem.Allocator, value: Expr) SetError!void {
@@ -1502,7 +1517,7 @@ pub const E = struct {
for (obj.properties.slice()) |prop| {
const key = prop.key orelse continue;
if (std.meta.activeTag(key.data) != .e_string) continue;
- if (key.eql(string, name)) return true;
+ if (key.data.e_string.eql(string, name)) return true;
}
return false;
}
diff --git a/src/string_immutable.zig b/src/string_immutable.zig
index 3a2bf4fc1..4d856b966 100644
--- a/src/string_immutable.zig
+++ b/src/string_immutable.zig
@@ -78,7 +78,7 @@ pub inline fn containsComptime(self: string, comptime str: string) bool {
pub const includes = contains;
pub inline fn containsAny(in: anytype, target: string) bool {
- for (in) |str| if (contains(bun.span(str), target)) return true;
+ for (in) |str| if (contains(if (@TypeOf(str) == u8) &[1]u8{str} else bun.span(str), target)) return true;
return false;
}