diff options
Diffstat (limited to 'src/cli.zig')
| -rw-r--r-- | src/cli.zig | 1057 |
1 files changed, 453 insertions, 604 deletions
diff --git a/src/cli.zig b/src/cli.zig index 5c1be01de..b7cbc97ba 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -1,6 +1,6 @@ usingnamespace @import("global.zig"); - usingnamespace @import("./http.zig"); + const std = @import("std"); const lex = @import("js_lexer.zig"); const logger = @import("logger.zig"); @@ -28,679 +28,528 @@ const Router = @import("./router.zig"); const NodeModuleBundle = @import("./node_module_bundle.zig").NodeModuleBundle; +const BunCommand = @import("./cli/bun_command.zig").BunCommand; +const DevCommand = @import("./cli/dev_command.zig").DevCommand; +const DiscordCommand = @import("./cli/discord_command.zig").DiscordCommand; +const BuildCommand = @import("./cli/build_command.zig").BuildCommand; +const RunCommand = @import("./cli/run_command.zig").RunCommand; + +var start_time: i128 = undefined; + pub const Cli = struct { - const LoaderMatcher = strings.ExactSizeMatcher(4); - pub fn ColonListType(comptime t: type, value_resolver: anytype) type { - return struct { - pub fn init(allocator: *std.mem.Allocator, count: usize) !@This() { - var keys = try allocator.alloc(string, count); - var values = try allocator.alloc(t, count); - - return @This(){ .keys = keys, .values = values }; - } - keys: []string, - values: []t, - - pub fn load(self: *@This(), input: []const string) !void { - for (input) |str, i| { - // Support either ":" or "=" as the separator, preferring whichever is first. - // ":" is less confusing IMO because that syntax is used with flags - // but "=" is what esbuild uses and I want this to be somewhat familiar for people using esbuild - const midpoint = std.math.min(strings.indexOfChar(str, ':') orelse std.math.maxInt(usize), strings.indexOfChar(str, '=') orelse std.math.maxInt(usize)); - if (midpoint == std.math.maxInt(usize)) { - return error.InvalidSeparator; - } + var wait_group: sync.WaitGroup = undefined; + pub fn startTransform(allocator: *std.mem.Allocator, args: Api.TransformOptions, log: *logger.Log) anyerror!void {} + pub fn start(allocator: *std.mem.Allocator, stdout: anytype, stderr: anytype, comptime MainPanicHandler: type) anyerror!void { + start_time = std.time.nanoTimestamp(); + var log = logger.Log.init(allocator); + var panicker = MainPanicHandler.init(&log); + MainPanicHandler.Singleton = &panicker; + + try Command.start(allocator, &log); + std.mem.doNotOptimizeAway(&log); + } +}; + +const LoaderMatcher = strings.ExactSizeMatcher(4); +pub fn ColonListType(comptime t: type, value_resolver: anytype) type { + return struct { + pub fn init(allocator: *std.mem.Allocator, count: usize) !@This() { + var keys = try allocator.alloc(string, count); + var values = try allocator.alloc(t, count); - self.keys[i] = str[0..midpoint]; - self.values[i] = try value_resolver(str[midpoint + 1 .. str.len]); + return @This(){ .keys = keys, .values = values }; + } + keys: []string, + values: []t, + + pub fn load(self: *@This(), input: []const string) !void { + for (input) |str, i| { + // Support either ":" or "=" as the separator, preferring whichever is first. + // ":" is less confusing IMO because that syntax is used with flags + // but "=" is what esbuild uses and I want this to be somewhat familiar for people using esbuild + const midpoint = std.math.min(strings.indexOfChar(str, ':') orelse std.math.maxInt(usize), strings.indexOfChar(str, '=') orelse std.math.maxInt(usize)); + if (midpoint == std.math.maxInt(usize)) { + return error.InvalidSeparator; } - } - pub fn resolve(allocator: *std.mem.Allocator, input: []const string) !@This() { - var list = try init(allocator, input.len); - try list.load(input); - return list; - } - }; - } - pub const LoaderColonList = ColonListType(Api.Loader, Arguments.loader_resolver); - pub const DefineColonList = ColonListType(string, Arguments.noop_resolver); - - pub const Arguments = struct { - pub fn loader_resolver(in: string) !Api.Loader { - const Matcher = strings.ExactSizeMatcher(4); - switch (Matcher.match(in)) { - Matcher.case("jsx") => return Api.Loader.jsx, - Matcher.case("js") => return Api.Loader.js, - Matcher.case("ts") => return Api.Loader.ts, - Matcher.case("tsx") => return Api.Loader.tsx, - Matcher.case("css") => return Api.Loader.css, - Matcher.case("file") => return Api.Loader.file, - Matcher.case("json") => return Api.Loader.json, - else => { - return error.InvalidLoader; - }, + self.keys[i] = str[0..midpoint]; + self.values[i] = try value_resolver(str[midpoint + 1 .. str.len]); } } - pub fn noop_resolver(in: string) !string { - return in; + pub fn resolve(allocator: *std.mem.Allocator, input: []const string) !@This() { + var list = try init(allocator, input.len); + try list.load(input); + return list; } - - pub fn fileReadError(err: anyerror, stderr: anytype, filename: string, kind: string) noreturn { - stderr.writer().print("Error reading file \"{s}\" for {s}: {s}", .{ filename, kind, @errorName(err) }) catch {}; - std.process.exit(1); + }; +} +pub const LoaderColonList = ColonListType(Api.Loader, Arguments.loader_resolver); +pub const DefineColonList = ColonListType(string, Arguments.noop_resolver); + +pub const Arguments = struct { + pub fn loader_resolver(in: string) !Api.Loader { + const Matcher = strings.ExactSizeMatcher(4); + switch (Matcher.match(in)) { + Matcher.case("jsx") => return Api.Loader.jsx, + Matcher.case("js") => return Api.Loader.js, + Matcher.case("ts") => return Api.Loader.ts, + Matcher.case("tsx") => return Api.Loader.tsx, + Matcher.case("css") => return Api.Loader.css, + Matcher.case("file") => return Api.Loader.file, + Matcher.case("json") => return Api.Loader.json, + else => { + return error.InvalidLoader; + }, } + } - pub fn readFile( - allocator: *std.mem.Allocator, - cwd: string, - filename: string, - ) ![]u8 { - var paths = [_]string{ cwd, filename }; - const outpath = try std.fs.path.resolve(allocator, &paths); - defer allocator.free(outpath); - var file = try std.fs.openFileAbsolute(outpath, std.fs.File.OpenFlags{ .read = true, .write = false }); - defer file.close(); - const stats = try file.stat(); - return try file.readToEndAlloc(allocator, stats.size); - } + pub fn noop_resolver(in: string) !string { + return in; + } - pub fn parse(allocator: *std.mem.Allocator, stdout: anytype, stderr: anytype) !Api.TransformOptions { - @setEvalBranchQuota(9999); - const params = comptime [_]clap.Param(clap.Help){ - clap.parseParam("-h, --help Display this help and exit. ") catch unreachable, - clap.parseParam("-r, --resolve <STR> Determine import/require behavior. \"disable\" ignores. \"dev\" bundles node_modules and builds everything else as independent entry points") catch unreachable, - clap.parseParam("-d, --define <STR>... Substitute K:V while parsing, e.g. --define process.env.NODE_ENV:development") catch unreachable, - clap.parseParam("-l, --loader <STR>... Parse files with .ext:loader, e.g. --loader .js:jsx. Valid loaders: jsx, js, json, tsx (not implemented yet), ts (not implemented yet), css (not implemented yet)") catch unreachable, - clap.parseParam("-o, --outdir <STR> Save output to directory (default: \"out\" if none provided and multiple entry points passed)") catch unreachable, - clap.parseParam("-e, --external <STR>... Exclude module from transpilation (can use * wildcards). ex: -e react") catch unreachable, - clap.parseParam("-i, --inject <STR>... Inject module at the top of every file") catch unreachable, - clap.parseParam("--cwd <STR> Absolute path to resolve entry points from.") catch unreachable, - clap.parseParam("--origin <STR> Rewrite import paths to start with --origin. Useful for web browsers. Default: \"/\"") catch unreachable, - clap.parseParam("--serve Start a local dev server. This also sets resolve to \"lazy\".") catch unreachable, - clap.parseParam("--static-dir <STR> Top-level directory for .html files, fonts or anything external. Defaults to \"<cwd>/public\", to match create-react-app and Next.js") catch unreachable, - clap.parseParam("--jsx-factory <STR> Changes the function called when compiling JSX elements using the classic JSX runtime") catch unreachable, - clap.parseParam("--jsx-fragment <STR> Changes the function called when compiling JSX fragments using the classic JSX runtime") catch unreachable, - clap.parseParam("--jsx-import-source <STR> Declares the module specifier to be used for importing the jsx and jsxs factory functions. Default: \"react\"") catch unreachable, - clap.parseParam("--jsx-runtime <STR> \"automatic\" (default) or \"classic\"") catch unreachable, - clap.parseParam("--jsx-production Use jsx instead of jsxDEV (default) for the automatic runtime") catch unreachable, - clap.parseParam("--extension-order <STR>... defaults to: .tsx,.ts,.jsx,.js,.json ") catch unreachable, - clap.parseParam("--disable-react-fast-refresh Disable React Fast Refresh. Enabled if --serve is set and --jsx-production is not set. Otherwise, it's a noop.") catch unreachable, - clap.parseParam("--tsconfig-override <STR> Load tsconfig from path instead of cwd/tsconfig.json") catch unreachable, - clap.parseParam("--platform <STR> \"browser\" or \"node\". Defaults to \"browser\"") catch unreachable, - clap.parseParam("--main-fields <STR>... Main fields to lookup in package.json. Defaults to --platform dependent") catch unreachable, - clap.parseParam("--scan Instead of bundling or transpiling, print a list of every file imported by an entry point, recursively") catch unreachable, - clap.parseParam("--new-jsb Generate a new node_modules.jsb file from node_modules and entry point(s)") catch unreachable, - clap.parseParam("--jsb <STR> Use a Bun JavaScript Bundle (default: \"./node_modules.jsb\" if exists)") catch unreachable, - clap.parseParam("--jsb-for-server <STR> Use a server-only Bun JavaScript Bundle (default: \"./node_modules.server.jsb\" if exists)") catch unreachable, - clap.parseParam("--use <STR> Use a JavaScript framework (package name or path to package)") catch unreachable, - clap.parseParam("--production This sets the defaults to production. Applies to jsx & framework") catch unreachable, - - clap.parseParam("<POS>... Entry point(s) to use. Can be individual files, npm packages, or one directory. If one directory, it will auto-detect entry points using a filesystem router. If you're using a framework, passing entry points are optional.") catch unreachable, - }; + pub fn fileReadError(err: anyerror, stderr: anytype, filename: string, kind: string) noreturn { + stderr.writer().print("Error reading file \"{s}\" for {s}: {s}", .{ filename, kind, @errorName(err) }) catch {}; + std.process.exit(1); + } - var diag = clap.Diagnostic{}; + pub fn readFile( + allocator: *std.mem.Allocator, + cwd: string, + filename: string, + ) ![]u8 { + var paths = [_]string{ cwd, filename }; + const outpath = try std.fs.path.resolve(allocator, &paths); + defer allocator.free(outpath); + var file = try std.fs.openFileAbsolute(outpath, std.fs.File.OpenFlags{ .read = true, .write = false }); + defer file.close(); + const stats = try file.stat(); + return try file.readToEndAlloc(allocator, stats.size); + } - var args = clap.parse(clap.Help, ¶ms, .{ .diagnostic = &diag }) catch |err| { - // Report useful error and exit - diag.report(stderr.writer(), err) catch {}; - return err; - }; + pub fn resolve_jsx_runtime(str: string) !Api.JsxRuntime { + if (strings.eqlComptime(str, "automatic")) { + return Api.JsxRuntime.automatic; + } else if (strings.eqlComptime(str, "fallback")) { + return Api.JsxRuntime.classic; + } else { + return error.InvalidJSXRuntime; + } + } - if (args.flag("--help")) { - try clap.help(stderr.writer(), ¶ms); - std.process.exit(1); - } + const ParamType = clap.Param(clap.Help); + + const params: [25]ParamType = brk: { + @setEvalBranchQuota(9999); + break :brk [_]ParamType{ + clap.parseParam("-r, --resolve <STR> Determine import/require behavior. \"disable\" ignores. \"dev\" bundles node_modules and builds everything else as independent entry points") catch unreachable, + clap.parseParam("-h, --help Display this help and exit. ") catch unreachable, + clap.parseParam("-d, --define <STR>... Substitute K:V while parsing, e.g. --define process.env.NODE_ENV:development") catch unreachable, + clap.parseParam("-l, --loader <STR>... Parse files with .ext:loader, e.g. --loader .js:jsx. Valid loaders: jsx, js, json, tsx (not implemented yet), ts (not implemented yet), css (not implemented yet)") catch unreachable, + clap.parseParam("-o, --outdir <STR> Save output to directory (default: \"out\" if none provided and multiple entry points passed)") catch unreachable, + clap.parseParam("-e, --external <STR>... Exclude module from transpilation (can use * wildcards). ex: -e react") catch unreachable, + clap.parseParam("-i, --inject <STR>... Inject module at the top of every file") catch unreachable, + clap.parseParam("--cwd <STR> Absolute path to resolve files & entry points from. This just changes the process' cwd.") catch unreachable, + clap.parseParam("--use <STR> Choose a framework, e.g. \"--use next\". It checks first for a package named \"bun-framework-packagename\" and then \"packagename\".") catch unreachable, + clap.parseParam("--origin <STR> Rewrite import paths to start with --origin. Default: \"/\"") catch unreachable, + clap.parseParam("--static-dir <STR> Top-level directory for .html files, fonts or anything external. Defaults to \"<cwd>/public\", to match create-react-app and Next.js") catch unreachable, + clap.parseParam("--extension-order <STR>... defaults to: .tsx,.ts,.jsx,.js,.json ") catch unreachable, + clap.parseParam("--tsconfig-override <STR> Load tsconfig from path instead of cwd/tsconfig.json") catch unreachable, + clap.parseParam("--platform <STR> \"browser\" or \"node\". Defaults to \"browser\"") catch unreachable, + clap.parseParam("--main-fields <STR>... Main fields to lookup in package.json. Defaults to --platform dependent") catch unreachable, + clap.parseParam("--disable-react-fast-refresh Disable React Fast Refresh") catch unreachable, + clap.parseParam("--jsx-factory <STR> Changes the function called when compiling JSX elements using the classic JSX runtime") catch unreachable, + clap.parseParam("--jsx-fragment <STR> Changes the function called when compiling JSX fragments using the classic JSX runtime") catch unreachable, + clap.parseParam("--jsx-import-source <STR> Declares the module specifier to be used for importing the jsx and jsxs factory functions. Default: \"react\"") catch unreachable, + clap.parseParam("--jsx-runtime <STR> \"automatic\" (default) or \"classic\"") catch unreachable, + clap.parseParam("--jsx-production Use jsx instead of jsxDEV (default) for the automatic runtime") catch unreachable, + clap.parseParam("--bunfile <STR> Use a .bun file (default: node_modules.bun)") catch unreachable, + clap.parseParam("--server-bunfile <STR> Use a .server.bun file (default: node_modules.server.bun)") catch unreachable, + clap.parseParam("--production [work in progress] generate production code") catch unreachable, + + clap.parseParam("<POS>... ") catch unreachable, + }; + }; - var cwd_paths = [_]string{args.option("--cwd") orelse try std.process.getCwdAlloc(allocator)}; - var cwd = try std.fs.path.resolve(allocator, &cwd_paths); - var tsconfig_override = if (args.option("--tsconfig-override")) |ts| (Arguments.readFile(allocator, cwd, ts) catch |err| fileReadError(err, stderr, ts, "tsconfig.json")) else null; - var origin = args.option("--origin"); - var defines_tuple = try DefineColonList.resolve(allocator, args.options("--define")); - var loader_tuple = try LoaderColonList.resolve(allocator, args.options("--define")); - - var define_keys = defines_tuple.keys; - var define_values = defines_tuple.values; - var loader_keys = loader_tuple.keys; - var loader_values = loader_tuple.values; - var entry_points = args.positionals(); - var inject = args.options("--inject"); - var output_dir = args.option("--outdir"); - const serve = args.flag("--serve"); - - const production = args.flag("--production"); - - var write = entry_points.len > 1 or output_dir != null; - if (write and output_dir == null) { - var _paths = [_]string{ cwd, "out" }; - output_dir = try std.fs.path.resolve(allocator, &_paths); - } - var externals = std.mem.zeroes([][]u8); - if (args.options("--external").len > 0) { - externals = try allocator.alloc([]u8, args.options("--external").len); - for (args.options("--external")) |external, i| { - externals[i] = constStrToU8(external); - } - } + pub fn parse(allocator: *std.mem.Allocator, comptime cmd: Command.Tag) !Api.TransformOptions { + var diag = clap.Diagnostic{}; - var jsx_factory = args.option("--jsx-factory"); - var jsx_fragment = args.option("--jsx-fragment"); - var jsx_import_source = args.option("--jsx-import-source"); - var jsx_runtime = args.option("--jsx-runtime"); - var jsx_production = args.flag("--jsx-production") or production; - var react_fast_refresh = false; + var args = clap.parse(clap.Help, ¶ms, .{ .diagnostic = &diag }) catch |err| { + // Report useful error and exit + diag.report(Output.errorWriter(), err) catch {}; + return err; + }; - var framework_entry_point = args.option("--use"); + var cwd_paths = [_]string{args.option("--cwd") orelse try std.process.getCwdAlloc(allocator)}; + var cwd = try std.fs.path.resolve(allocator, &cwd_paths); - if (serve or args.flag("--new-jsb")) { - react_fast_refresh = true; - if (args.flag("--disable-react-fast-refresh") or jsx_production) { - react_fast_refresh = false; - } + var defines_tuple = try DefineColonList.resolve(allocator, args.options("--define")); + var loader_tuple = try LoaderColonList.resolve(allocator, args.options("--define")); + var externals = std.mem.zeroes([][]u8); + if (args.options("--external").len > 0) { + externals = try allocator.alloc([]u8, args.options("--external").len); + for (args.options("--external")) |external, i| { + externals[i] = constStrToU8(external); } + } - var main_fields = args.options("--main-fields"); - - var node_modules_bundle_path = args.option("--jsb") orelse brk: { - if (args.flag("--new-jsb")) { - break :brk null; - } + var opts = Api.TransformOptions{ + .tsconfig_override = if (args.option("--tsconfig-override")) |ts| (Arguments.readFile(allocator, cwd, ts) catch |err| fileReadError(err, Output.errorStream(), ts, "tsconfig.json")) else null, + .external = externals, + .absolute_working_dir = cwd, + .origin = args.option("--origin"), + .define = .{ + .keys = defines_tuple.keys, + .values = defines_tuple.values, + }, + .loaders = .{ + .extensions = loader_tuple.keys, + .loaders = loader_tuple.values, + }, - const node_modules_bundle_path_absolute = resolve_path.joinAbs(cwd, .auto, "node_modules.jsb"); + .serve = cmd == .DevCommand, + .main_fields = args.options("--main-fields"), + .generate_node_module_bundle = cmd == .BunCommand, + .inject = args.options("--inject"), + .extension_order = args.options("--extension-order"), + .entry_points = undefined, + }; - break :brk std.fs.realpathAlloc(allocator, node_modules_bundle_path_absolute) catch null; - }; + const print_help = args.flag("--help"); + if (print_help) { + clap.help(Output.errorWriter(), ¶ms) catch {}; + std.os.exit(0); + } - var node_modules_bundle_path_server = args.option("--jsb-for-server") orelse brk: { - if (args.flag("--new-jsb")) { - break :brk null; + var output_dir = args.option("--outdir"); + + var define_keys = defines_tuple.keys; + var define_values = defines_tuple.values; + var loader_keys = loader_tuple.keys; + var loader_values = loader_tuple.values; + var entry_points = args.positionals(); + + switch (comptime cmd) { + .BunCommand => { + if (entry_points.len > 0 and (strings.eqlComptime( + entry_points[0], + "bun", + ))) { + entry_points = entry_points[1..]; } + }, + .DevCommand => { + if (entry_points.len > 0 and (strings.eqlComptime( + entry_points[0], + "dev", + ) or strings.eqlComptime( + entry_points[0], + "d", + ))) { + entry_points = entry_points[1..]; + } + }, + .BuildCommand => { + if (entry_points.len > 0 and (strings.eqlComptime( + entry_points[0], + "build", + ) or strings.eqlComptime( + entry_points[0], + "b", + ))) { + entry_points = entry_points[1..]; + } + }, + .RunCommand => { + if (entry_points.len > 0 and (strings.eqlComptime( + entry_points[0], + "run", + ) or strings.eqlComptime( + entry_points[0], + "r", + ))) { + entry_points = entry_points[1..]; + } + }, + else => {}, + } - const node_modules_bundle_path_absolute = resolve_path.joinAbs(cwd, .auto, "node_modules.server.jsb"); + const production = args.flag("--production"); - break :brk std.fs.realpathAlloc(allocator, node_modules_bundle_path_absolute) catch null; - }; + var write = entry_points.len > 1 or output_dir != null; + if (write and output_dir == null) { + var _paths = [_]string{ cwd, "out" }; + output_dir = try std.fs.path.resolve(allocator, &_paths); + } + opts.write = write; + opts.entry_points = entry_points; + + var jsx_factory = args.option("--jsx-factory"); + var jsx_fragment = args.option("--jsx-fragment"); + var jsx_import_source = args.option("--jsx-import-source"); + var jsx_runtime = args.option("--jsx-runtime"); + var jsx_production = args.flag("--jsx-production") or production; + const react_fast_refresh = switch (comptime cmd) { + .BunCommand, .DevCommand => !(args.flag("--disable-react-fast-refresh") or jsx_production), + else => true, + }; - if (args.flag("--new-jsb")) { - node_modules_bundle_path = null; - node_modules_bundle_path_server = null; - } + opts.node_modules_bundle_path = args.option("--bunfile") orelse brk: { + const node_modules_bundle_path_absolute = resolve_path.joinAbs(cwd, .auto, "node_modules.bun"); - var route_config: ?Api.RouteConfig = null; - if (args.option("--static-dir")) |public_dir| { - route_config = route_config orelse Api.RouteConfig{ .extensions = &.{}, .dir = &.{} }; + break :brk std.fs.realpathAlloc(allocator, node_modules_bundle_path_absolute) catch null; + }; - route_config.?.static_dir = public_dir; - } + opts.node_modules_bundle_path_server = args.option("--server-bunfile") orelse brk: { + const node_modules_bundle_path_absolute = resolve_path.joinAbs(cwd, .auto, "node_modules.server.bun"); - const PlatformMatcher = strings.ExactSizeMatcher(8); - const ResoveMatcher = strings.ExactSizeMatcher(8); - - var resolve = Api.ResolveMode.lazy; - if (args.option("--resolve")) |_resolve| { - switch (PlatformMatcher.match(_resolve)) { - PlatformMatcher.case("disable") => { - resolve = Api.ResolveMode.disable; - }, - PlatformMatcher.case("bundle") => { - resolve = Api.ResolveMode.bundle; - }, - PlatformMatcher.case("dev") => { - resolve = Api.ResolveMode.dev; - }, - PlatformMatcher.case("lazy") => { - resolve = Api.ResolveMode.lazy; - }, - else => { - diag.name.long = "--resolve"; - diag.arg = _resolve; - try diag.report(stderr.writer(), error.InvalidResolveOption); - std.process.exit(1); - }, - } - } + break :brk std.fs.realpathAlloc(allocator, node_modules_bundle_path_absolute) catch null; + }; - var platform: ?Api.Platform = null; - - if (args.option("--platform")) |_platform| { - switch (PlatformMatcher.match(_platform)) { - PlatformMatcher.case("browser") => { - platform = Api.Platform.browser; - }, - PlatformMatcher.case("node") => { - platform = Api.Platform.node; - }, - else => { - diag.name.long = "--platform"; - diag.arg = _platform; - try diag.report(stderr.writer(), error.InvalidPlatform); - std.process.exit(1); - }, + switch (comptime cmd) { + .DevCommand, .BuildCommand => { + if (args.option("--static-dir")) |public_dir| { + opts.router = Api.RouteConfig{ .extensions = &.{}, .dir = &.{}, .static_dir = public_dir }; } - } + }, + else => {}, + } - var jsx: ?Api.Jsx = null; - if (jsx_factory != null or - jsx_fragment != null or - jsx_import_source != null or - jsx_runtime != null or - jsx_production or react_fast_refresh) - { - var default_factory = "".*; - var default_fragment = "".*; - var default_import_source = "".*; - jsx = Api.Jsx{ - .factory = constStrToU8(jsx_factory orelse &default_factory), - .fragment = constStrToU8(jsx_fragment orelse &default_fragment), - .import_source = constStrToU8(jsx_import_source orelse &default_import_source), - .runtime = if (jsx_runtime != null) try resolve_jsx_runtime(jsx_runtime.?) else Api.JsxRuntime.automatic, - .development = !jsx_production, - .react_fast_refresh = react_fast_refresh, - }; - } + const ResolveMatcher = strings.ExactSizeMatcher(8); - var javascript_framework: ?Api.FrameworkConfig = null; + opts.resolve = Api.ResolveMode.lazy; - if (framework_entry_point) |entry| { - javascript_framework = Api.FrameworkConfig{ - .package = entry, - .development = !production, - }; - } + switch (comptime cmd) { + .BuildCommand => { + if (args.option("--resolve")) |_resolve| { + switch (ResolveMatcher.match(_resolve)) { + ResolveMatcher.case("disable") => { + opts.resolve = Api.ResolveMode.disable; + }, + ResolveMatcher.case("bundle") => { + opts.resolve = Api.ResolveMode.bundle; + }, + ResolveMatcher.case("dev") => { + opts.resolve = Api.ResolveMode.dev; + }, + ResolveMatcher.case("lazy") => { + opts.resolve = Api.ResolveMode.lazy; + }, + else => { + diag.name.long = "--resolve"; + diag.arg = _resolve; + try diag.report(Output.errorWriter(), error.InvalidResolveOption); + std.process.exit(1); + }, + } + } + }, + else => {}, + } - if (entry_points.len == 0 and javascript_framework == null and node_modules_bundle_path == null) { - try clap.help(stderr.writer(), ¶ms); - try diag.report(stderr.writer(), error.MissingEntryPoint); - std.process.exit(1); - } + const PlatformMatcher = strings.ExactSizeMatcher(8); - return Api.TransformOptions{ - .jsx = jsx, - .output_dir = output_dir, - .resolve = resolve, - .external = externals, - .absolute_working_dir = cwd, - .tsconfig_override = tsconfig_override, - .origin = origin, - .define = .{ - .keys = define_keys, - .values = define_values, + if (args.option("--platform")) |_platform| { + switch (PlatformMatcher.match(_platform)) { + PlatformMatcher.case("browser") => { + opts.platform = Api.Platform.browser; }, - .loaders = .{ - .extensions = loader_keys, - .loaders = loader_values, + PlatformMatcher.case("node") => { + opts.platform = Api.Platform.node; }, - .node_modules_bundle_path = node_modules_bundle_path, - .node_modules_bundle_path_server = node_modules_bundle_path_server, - .write = write, - .router = route_config, - .serve = serve, - .inject = inject, - .entry_points = entry_points, - .extension_order = args.options("--extension-order"), - .main_fields = args.options("--main-fields"), - .platform = platform, - .only_scan_dependencies = if (args.flag("--scan")) Api.ScanDependencyMode.all else Api.ScanDependencyMode._none, - .generate_node_module_bundle = if (args.flag("--new-jsb")) true else false, - .framework = javascript_framework, - }; - } - }; - pub fn resolve_jsx_runtime(str: string) !Api.JsxRuntime { - if (strings.eql(str, "automatic")) { - return Api.JsxRuntime.automatic; - } else if (strings.eql(str, "fallback")) { - return Api.JsxRuntime.classic; - } else { - return error.InvalidJSXRuntime; + else => { + diag.name.long = "--platform"; + diag.arg = _platform; + try diag.report(Output.errorWriter(), error.InvalidPlatform); + std.process.exit(1); + }, + } } - } - pub fn printScanResults(scan_results: bundler.ScanResult.Summary, allocator: *std.mem.Allocator) !void { - var stdout = std.io.getStdOut(); - const print_start = std.time.nanoTimestamp(); - try std.json.stringify(scan_results.list(), .{}, stdout.writer()); - Output.printError("\nJSON printing took: {d}\n", .{std.time.nanoTimestamp() - print_start}); - } - var wait_group: sync.WaitGroup = undefined; - pub fn startTransform(allocator: *std.mem.Allocator, args: Api.TransformOptions, log: *logger.Log) anyerror!void {} - pub fn start(allocator: *std.mem.Allocator, stdout: anytype, stderr: anytype, comptime MainPanicHandler: type) anyerror!void { - const start_time = std.time.nanoTimestamp(); - var log = logger.Log.init(allocator); - var panicker = MainPanicHandler.init(&log); - MainPanicHandler.Singleton = &panicker; - - var args = try Arguments.parse(alloc.static, stdout, stderr); - if ((args.entry_points.len == 1 and args.entry_points[0].len > ".jsb".len and args.entry_points[0][args.entry_points[0].len - ".jsb".len] == '.' and strings.eqlComptime(args.entry_points[0][args.entry_points[0].len - "jsb".len ..], "jsb"))) { - var out_buffer: [std.fs.MAX_PATH_BYTES]u8 = undefined; - var input = try std.fs.openFileAbsolute(try std.os.realpath(args.entry_points[0], &out_buffer), .{ .read = true }); - const params = comptime [_]clap.Param(clap.Help){ - clap.parseParam("--summary Print a summary") catch unreachable, - clap.parseParam("<POS>... ") catch unreachable, + if (jsx_factory != null or + jsx_fragment != null or + jsx_import_source != null or + jsx_runtime != null or + jsx_production or react_fast_refresh) + { + var default_factory = "".*; + var default_fragment = "".*; + var default_import_source = "".*; + opts.jsx = Api.Jsx{ + .factory = constStrToU8(jsx_factory orelse &default_factory), + .fragment = constStrToU8(jsx_fragment orelse &default_fragment), + .import_source = constStrToU8(jsx_import_source orelse &default_import_source), + .runtime = if (jsx_runtime != null) try resolve_jsx_runtime(jsx_runtime.?) else Api.JsxRuntime.automatic, + .development = !jsx_production, + .react_fast_refresh = react_fast_refresh, }; + } - var jsBundleArgs = clap.parse(clap.Help, ¶ms, .{ .allocator = allocator }) catch |err| { - try NodeModuleBundle.printBundle(std.fs.File, input, @TypeOf(stdout), stdout); - return; + if (args.option("--use")) |entry| { + opts.framework = Api.FrameworkConfig{ + .package = entry, + .development = !production, }; + } - if (jsBundleArgs.flag("--summary")) { - try NodeModuleBundle.printSummaryFromDisk(std.fs.File, input, @TypeOf(stdout), stdout, allocator); - } else { - try NodeModuleBundle.printBundle(std.fs.File, input, @TypeOf(stdout), stdout); - } - - return; + if (entry_points.len == 0 and opts.framework == null and opts.node_modules_bundle_path == null) { + return error.MissingEntryPoint; } - if (args.serve orelse false) { - try Server.start(allocator, args); + opts.output_dir = output_dir; + return opts; + } +}; - return; - } +const AutoCommand = struct { + pub fn exec(allocator: *std.mem.Allocator) !void { + try HelpCommand.execWithReason(allocator, .invalid_command); + } +}; +const InitCommand = struct { + pub fn exec(allocator: *std.mem.Allocator) !void {} +}; +const HelpCommand = struct { + pub fn exec(allocator: *std.mem.Allocator) !void { + @setCold(true); + execWithReason(allocator, .explicit); + } - // if ((args.only_scan_dependencies orelse ._none) == .all) { - // return try printScanResults(try bundler.Bundler.scanDependencies(allocator, &log, args), allocator); - // } + pub const Reason = enum { + explicit, + invalid_command, + }; + pub fn execWithReason(allocator: *std.mem.Allocator, comptime reason: Reason) void { + @setCold(true); + var cwd_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; + const cwd = std.os.getcwd(&cwd_buf) catch unreachable; + const dirname = std.fs.path.basename(cwd); + const fmt = + \\> <r> <b><white>init<r> Setup Bun in \"{s}\" + \\> <r> <b><green>dev <r><d> ./a.ts ./b.jsx<r> Start a Bun Dev Server + \\<d>*<r> <b><cyan>build <r><d> ./a.ts ./b.jsx<r> Make JavaScript-like code runnable & bundle CSS + \\> <r> <b><magenta>bun <r><d> ./a.ts ./b.jsx<r> Bundle dependencies of input files into a <r><magenta>.bun<r> + \\> <r> <green>run <r><d> ./a.ts <r> Run a JavaScript-like file with Bun.js + \\> <r> <b><blue>discord<r> Open Bun's Discord server + \\> <r> <b><d>help <r> Print this help menu + \\ + ; + + switch (reason) { + .explicit => Output.pretty("Bun: a fast bundler & transpiler for web software.\n\n" ++ fmt, .{dirname}), + .invalid_command => Output.prettyError("<r><red>Uh-oh<r> not sure what to do with that command.\n\n" ++ fmt, .{dirname}), + } - if ((args.generate_node_module_bundle orelse false)) { - var log_ = try allocator.create(logger.Log); - log_.* = log; + Output.flush(); - var this_bundler = try bundler.ServeBundler.init(allocator, log_, args, null, null); - this_bundler.configureLinker(); - var filepath: [*:0]const u8 = "node_modules.jsb"; - var server_bundle_filepath: [*:0]const u8 = "node_modules.server.jsb"; - try this_bundler.configureRouter(true); + if (reason == .invalid_command) { + std.process.exit(1); + } + } +}; - var loaded_route_config: ?Api.LoadedRouteConfig = brk: { - if (this_bundler.options.routes.routes_enabled) { - break :brk this_bundler.options.routes.toAPI(); - } - break :brk null; - }; - var loaded_framework: ?Api.LoadedFramework = brk: { - if (this_bundler.options.framework) |*conf| { - break :brk conf.toAPI(allocator, this_bundler.fs.top_level_dir, true); - } - break :brk null; +pub const Command = struct { + pub const Context = struct { + start_time: i128, + args: Api.TransformOptions = std.mem.zeroes(Api.TransformOptions), + log: *logger.Log, + allocator: *std.mem.Allocator, + + pub fn create(allocator: *std.mem.Allocator, log: *logger.Log, comptime command: Command.Tag) !Context { + return Command.Context{ + .args = try Arguments.parse(allocator, command), + .log = log, + .start_time = start_time, + .allocator = allocator, }; - var env_loader = this_bundler.env; - wait_group = sync.WaitGroup.init(); - var server_bundler_generator_thread: ?std.Thread = null; - var generated_server = false; - if (this_bundler.options.framework) |*framework| { - if (framework.toAPI(allocator, this_bundler.fs.top_level_dir, false)) |_server_conf| { - const ServerBundleGeneratorThread = struct { - inline fn _generate( - logs: *logger.Log, - env_loader_: *DotEnv.Loader, - allocator_: *std.mem.Allocator, - transform_args: Api.TransformOptions, - _filepath: [*:0]const u8, - server_conf: Api.LoadedFramework, - route_conf_: ?Api.LoadedRouteConfig, - router: ?Router, - ) !void { - var server_bundler = try bundler.ServeBundler.init( - allocator_, - logs, - try configureTransformOptionsForBun(allocator_, transform_args), - null, - env_loader_, - ); - server_bundler.configureLinker(); - server_bundler.router = router; - try server_bundler.configureDefines(); - _ = try bundler.ServeBundler.GenerateNodeModuleBundle.generate( - &server_bundler, - allocator_, - server_conf, - route_conf_, - _filepath, - ); - std.mem.doNotOptimizeAway(&server_bundler); - } - pub fn generate( - logs: *logger.Log, - env_loader_: *DotEnv.Loader, - transform_args: Api.TransformOptions, - _filepath: [*:0]const u8, - server_conf: Api.LoadedFramework, - route_conf_: ?Api.LoadedRouteConfig, - router: ?Router, - ) void { - if (FeatureFlags.parallel_jsb) { - try alloc.setup(std.heap.c_allocator); - var stdout_ = std.io.getStdOut(); - var stderr_ = std.io.getStdErr(); - var output_source = Output.Source.init(stdout_, stderr_); - Output.Source.set(&output_source); - - Output.enable_ansi_colors = stderr_.isTty(); - } - - defer Output.flush(); - defer { - if (FeatureFlags.parallel_jsb) { - wait_group.done(); - } - } - - _generate(logs, env_loader_, std.heap.c_allocator, transform_args, _filepath, server_conf, route_conf_, router) catch return; - } - }; - - if (FeatureFlags.parallel_jsb) { - wait_group.add(); - server_bundler_generator_thread = try std.Thread.spawn( - .{}, - ServerBundleGeneratorThread.generate, - .{ - log_, - env_loader, - args, - server_bundle_filepath, - _server_conf, - loaded_route_config, - this_bundler.router, - }, - ); - generated_server = true; - } else { - ServerBundleGeneratorThread.generate( - log_, - env_loader, - args, - server_bundle_filepath, - _server_conf, - loaded_route_config, - this_bundler.router, - ); - generated_server = true; - } - } - } - - defer { - if (server_bundler_generator_thread) |thread| { - thread.join(); - } - } + } + }; - { - // Always generate the client-only bundle - // we can revisit this decision if people ask - var node_modules_ = try bundler.ServeBundler.GenerateNodeModuleBundle.generate( - &this_bundler, - allocator, - loaded_framework, - loaded_route_config, - filepath, - ); - - if (server_bundler_generator_thread) |thread| { - wait_group.wait(); - } + pub fn which(allocator: *std.mem.Allocator) Tag { + var args_iter = std.process.args(); + // first one is the executable name + const skipped = args_iter.skip(); - if (node_modules_) |node_modules| { - if (log_.errors > 0) { - try log_.print(Output.errorWriter()); - } else { - var elapsed = @divTrunc(std.time.nanoTimestamp() - start_time, @as(i128, std.time.ns_per_ms)); - var bundle = NodeModuleBundle.init(node_modules, allocator); - bundle.printSummary(); - const indent = comptime " "; - Output.prettyln(indent ++ "<d>{d:6}ms elapsed", .{@intCast(u32, elapsed)}); - - if (generated_server) { - Output.prettyln(indent ++ "<r>Saved to ./{s}, ./{s}", .{ filepath, server_bundle_filepath }); - } else { - Output.prettyln(indent ++ "<r>Saved to ./{s}", .{filepath}); - } - - try log_.printForLogLevel(Output.errorWriter()); - } - } else { - try log_.print(Output.errorWriter()); - } - } - return; + if (!skipped) { + return .AutoCommand; } - var result: options.TransformResult = undefined; - switch (args.resolve orelse Api.ResolveMode.dev) { - Api.ResolveMode.disable => { - result = try bundler.Transformer.transform( - allocator, - &log, - args, - ); - }, - .lazy => { - result = try bundler.ServeBundler.bundle( - allocator, - &log, - args, - ); - }, - else => { - result = try bundler.Bundler.bundle( - allocator, - &log, - args, - ); - }, - } - var did_write = false; - var stderr_writer = stderr.writer(); - var buffered_writer = std.io.bufferedWriter(stderr_writer); - defer buffered_writer.flush() catch {}; - var writer = buffered_writer.writer(); - var err_writer = writer; - - var open_file_limit: usize = 32; - if (args.write) |write| { - if (write) { - const root_dir = result.root_dir orelse unreachable; - if (std.os.getrlimit(.NOFILE)) |limit| { - open_file_limit = limit.cur; - } else |err| {} - - var all_paths = try allocator.alloc([]const u8, result.output_files.len); - var max_path_len: usize = 0; - var max_padded_size: usize = 0; - for (result.output_files) |f, i| { - all_paths[i] = f.input.text; - } + const next_arg = (args_iter.next(allocator) orelse return .AutoCommand) catch unreachable; - var from_path = resolve_path.longestCommonPath(all_paths); + const first_arg_name = std.mem.span(next_arg); + const RootCommandMatcher = strings.ExactSizeMatcher(8); - for (result.output_files) |f, i| { - max_path_len = std.math.max( - std.math.max(from_path.len, f.input.text.len) + 2 - from_path.len, - max_path_len, - ); - } + return switch (RootCommandMatcher.match(first_arg_name)) { + RootCommandMatcher.case("init") => .InitCommand, + RootCommandMatcher.case("bun") => .BunCommand, + RootCommandMatcher.case("discord") => .DiscordCommand, + + RootCommandMatcher.case("b"), RootCommandMatcher.case("build") => .BuildCommand, + RootCommandMatcher.case("r"), RootCommandMatcher.case("run") => .RunCommand, + RootCommandMatcher.case("d"), RootCommandMatcher.case("dev") => .DevCommand, - did_write = true; + RootCommandMatcher.case("help") => .HelpCommand, + else => .AutoCommand, + }; + } - // On posix, file handles automatically close on process exit by the OS - // Closing files shows up in profiling. - // So don't do that unless we actually need to. - const do_we_need_to_close = !FeatureFlags.store_file_descriptors or (@intCast(usize, root_dir.fd) + open_file_limit) < result.output_files.len; + pub fn start(allocator: *std.mem.Allocator, log: *logger.Log) !void { + const tag = which(allocator); + switch (tag) { + .DiscordCommand => return try DiscordCommand.exec(allocator), + .HelpCommand => return try HelpCommand.exec(allocator), + .InitCommand => return try InitCommand.exec(allocator), + else => {}, + } - var filepath_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; - filepath_buf[0] = '.'; - filepath_buf[1] = '/'; + switch (tag) { + .BunCommand => { + const ctx = try Command.Context.create(allocator, log, .BunCommand); - for (result.output_files) |f, i| { - var rel_path: []const u8 = undefined; - switch (f.value) { - // easy mode: write the buffer - .buffer => |value| { - rel_path = resolve_path.relative(from_path, f.input.text); + try BunCommand.exec(ctx); + }, + .DevCommand => { + const ctx = try Command.Context.create(allocator, log, .DevCommand); - try root_dir.writeFile(rel_path, value); - }, - .move => |value| { - // const primary = f.input.text[from_path.len..]; - // std.mem.copy(u8, filepath_buf[2..], primary); - // rel_path = filepath_buf[0 .. primary.len + 2]; - rel_path = value.pathname; + try DevCommand.exec(ctx); + }, + .BuildCommand => { + const ctx = try Command.Context.create(allocator, log, .BuildCommand); - // try f.moveTo(result.outbase, constStrToU8(rel_path), root_dir.fd); - }, - .copy => |value| { - rel_path = value.pathname; + try BuildCommand.exec(ctx); + }, + .RunCommand => { + const ctx = try Command.Context.create(allocator, log, .RunCommand); - try f.copyTo(result.outbase, constStrToU8(rel_path), root_dir.fd); + try RunCommand.exec(ctx); + }, + .AutoCommand => { + const ctx = Command.Context.create(allocator, log, .AutoCommand) catch |e| { + switch (e) { + error.MissingEntryPoint => { + HelpCommand.execWithReason(allocator, .explicit); + return; }, - .noop => {}, - .pending => |value| { - unreachable; + else => { + return e; }, } + }; - // Print summary - _ = try writer.write("\n"); - const padding_count = 2 + (std.math.max(rel_path.len, max_path_len) - rel_path.len); - try writer.writeByteNTimes(' ', 2); - try writer.writeAll(rel_path); - try writer.writeByteNTimes(' ', padding_count); - const size = @intToFloat(f64, f.size) / 1000.0; - try std.fmt.formatFloatDecimal(size, .{ .precision = 2 }, writer); - try writer.writeAll(" KB\n"); - } - } - } - - if (isDebug) { - err_writer.print("\nExpr count: {d}\n", .{js_ast.Expr.icount}) catch {}; - err_writer.print("Stmt count: {d}\n", .{js_ast.Stmt.icount}) catch {}; - err_writer.print("Binding count: {d}\n", .{js_ast.Binding.icount}) catch {}; - err_writer.print("File Descriptors: {d} / {d}\n", .{ - fs.FileSystem.max_fd, - open_file_limit, - }) catch {}; - } - - for (result.errors) |err| { - try err.writeFormat(err_writer); - _ = try err_writer.write("\n"); - } - - for (result.warnings) |err| { - try err.writeFormat(err_writer); - _ = try err_writer.write("\n"); - } - - const duration = std.time.nanoTimestamp() - start_time; - - if (did_write and duration < @as(i128, @as(i128, std.time.ns_per_s) * @as(i128, 2))) { - var elapsed = @divTrunc(duration, @as(i128, std.time.ns_per_ms)); - try err_writer.print("\nCompleted in {d}ms", .{elapsed}); + try BuildCommand.exec(ctx); + }, + else => unreachable, } } + + pub const Tag = enum { + InitCommand, + BunCommand, + DevCommand, + DiscordCommand, + BuildCommand, + RunCommand, + AutoCommand, + HelpCommand, + }; }; |
