/// This file is mostly the API schema but with all the options normalized. /// Normalization is necessary because most fields in the API schema are optional const std = @import("std"); const logger = @import("logger.zig"); const Fs = @import("fs.zig"); const alloc = @import("alloc.zig"); const resolver = @import("./resolver/resolver.zig"); const api = @import("./api/schema.zig"); const Api = api.Api; const defines = @import("./defines.zig"); const resolve_path = @import("./resolver/resolve_path.zig"); const NodeModuleBundle = @import("./node_module_bundle.zig").NodeModuleBundle; const URL = @import("./query_string_map.zig").URL; const ConditionsMap = @import("./resolver/package_json.zig").ESModule.ConditionsMap; usingnamespace @import("global.zig"); const Dir = Fs.Dir; const File = Fs.File; const Analytics = @import("./analytics/analytics_thread.zig"); const FileSystem = Fs.FileSystem; const DotEnv = @import("./env_loader.zig"); const assert = std.debug.assert; pub const WriteDestination = enum { stdout, disk, // eventaully: wasm }; pub fn validatePath(log: *logger.Log, fs: *Fs.FileSystem.Implementation, cwd: string, rel_path: string, allocator: *std.mem.Allocator, path_kind: string) string { if (rel_path.len == 0) { return ""; } const paths = [_]string{ cwd, rel_path }; const out = std.fs.path.resolve(allocator, &paths) catch |err| { Global.invariant(false, "{s} resolving external: \"{s}\"", .{ @errorName(err), rel_path }); return ""; }; return out; } pub fn stringHashMapFromArrays(comptime t: type, allocator: *std.mem.Allocator, keys: anytype, values: anytype) !t { var hash_map = t.init(allocator); if (keys.len > 0) { try hash_map.ensureCapacity(@intCast(u32, keys.len)); for (keys) |key, i| { hash_map.putAssumeCapacity(key, values[i]); } } return hash_map; } pub const ExternalModules = struct { node_modules: std.BufSet, abs_paths: std.BufSet, patterns: []const WildcardPattern, pub const WildcardPattern = struct { prefix: string, suffix: string, }; pub fn isNodeBuiltin(str: string) bool { return NodeBuiltinsMap.has(str); } const default_wildcard_patterns = &[_]WildcardPattern{ .{ .prefix = "/bun:", .suffix = "", }, // .{ // .prefix = "/src:", // .suffix = "", // }, // .{ // .prefix = "/blob:", // .suffix = "", // }, }; pub fn init( allocator: *std.mem.Allocator, fs: *Fs.FileSystem.Implementation, cwd: string, externals: []const string, log: *logger.Log, platform: Platform, ) ExternalModules { var result = ExternalModules{ .node_modules = std.BufSet.init(allocator), .abs_paths = std.BufSet.init(allocator), .patterns = std.mem.span(default_wildcard_patterns), }; switch (platform) { .node => { // TODO: fix this stupid copy result.node_modules.hash_map.ensureCapacity(NodeBuiltinPatterns.len) catch unreachable; for (NodeBuiltinPatterns) |pattern| { result.node_modules.insert(pattern) catch unreachable; } }, .bun => { // // TODO: fix this stupid copy // result.node_modules.hash_map.ensureCapacity(BunNodeBuiltinPatternsCompat.len) catch unreachable; // for (BunNodeBuiltinPatternsCompat) |pattern| { // result.node_modules.insert(pattern) catch unreachable; // } }, else => {}, } if (externals.len == 0) { return result; } var patterns = std.ArrayList(WildcardPattern).initCapacity(allocator, default_wildcard_patterns.len) catch unreachable; patterns.appendSliceAssumeCapacity(std.mem.span(default_wildcard_patterns)); for (externals) |external| { const path = external; if (strings.indexOfChar(path, '*')) |i| { if (strings.indexOfChar(path[i + 1 .. path.len], '*') != null) { log.addErrorFmt(null, logger.Loc.Empty, allocator, "External path \"{s}\" cannot have more than one \"*\" wildcard", .{external}) catch unreachable; return result; } patterns.append(WildcardPattern{ .prefix = external[0..i], .suffix = external[i + 1 .. external.len], }) catch unreachable; } else if (resolver.isPackagePath(external)) { result.node_modules.insert(external) catch unreachable; } else { const normalized = validatePath(log, fs, cwd, external, allocator, "external path"); if (normalized.len > 0) { result.abs_paths.insert(normalized) catch unreachable; } } } result.patterns = patterns.toOwnedSlice(); return result; } pub const NodeBuiltinPatterns = [_]string{ "_http_agent", "_http_client", "_http_common", "_http_incoming", "_http_outgoing", "_http_server", "_stream_duplex", "_stream_passthrough", "_stream_readable", "_stream_transform", "_stream_wrap", "_stream_writable", "_tls_common", "_tls_wrap", "assert", "async_hooks", "buffer", "child_process", "cluster", "console", "constants", "crypto", "dgram", "diagnostics_channel", "dns", "domain", "events", "fs", "http", "http2", "https", "inspector", "module", "net", "os", "path", "perf_hooks", "process", "punycode", "querystring", "readline", "repl", "stream", "string_decoder", "sys", "timers", "tls", "trace_events", "tty", "url", "util", "v8", "vm", "wasi", "worker_threads", "zlib", }; pub const BunNodeBuiltinPatternsCompat = [_]string{ "_http_agent", "_http_client", "_http_common", "_http_incoming", "_http_outgoing", "_http_server", "_stream_duplex", "_stream_passthrough", "_stream_readable", "_stream_transform", "_stream_wrap", "_stream_writable", "_tls_common", "_tls_wrap", "assert", "async_hooks", // "buffer", "child_process", "cluster", "console", "constants", "crypto", "dgram", "diagnostics_channel", "dns", "domain", "events", "fs", "http", "http2", "https", "inspector", "module", "net", "os", // "path", "perf_hooks", // "process", "punycode", "querystring", "readline", "repl", "stream", "string_decoder", "sys", "timers", "tls", "trace_events", "tty", "url", "util", "v8", "vm", "wasi", "worker_threads", "zlib", }; pub const NodeBuiltinsMap = std.ComptimeStringMap(bool, .{ .{ "_http_agent", true }, .{ "_http_client", true }, .{ "_http_common", true }, .{ "_http_incoming", true }, .{ "_http_outgoing", true }, .{ "_http_server", true }, .{ "_stream_duplex", true }, .{ "_stream_passthrough", true }, .{ "_stream_readable", true }, .{ "_stream_transform", true }, .{ "_stream_wrap", true }, .{ "_stream_writable", true }, .{ "_tls_common", true }, .{ "_tls_wrap", true }, .{ "assert", true }, .{ "async_hooks", true }, .{ "buffer", true }, .{ "child_process", true }, .{ "cluster", true }, .{ "console", true }, .{ "constants", true }, .{ "crypto", true }, .{ "dgram", true }, .{ "diagnostics_channel", true }, .{ "dns", true }, .{ "domain", true }, .{ "events", true }, .{ "fs", true }, .{ "http", true }, .{ "http2", true }, .{ "https", true }, .{ "inspector", true }, .{ "module", true }, .{ "net", true }, .{ "os", true }, .{ "path", true }, .{ "perf_hooks", true }, .{ "process", true }, .{ "punycode", true }, .{ "querystring", true }, .{ "readline", true }, .{ "repl", true }, .{ "stream", true }, .{ "string_decoder", true }, .{ "sys", true }, .{ "timers", true }, .{ "tls", true }, .{ "trace_events", true }, .{ "tty", true }, .{ "url", true }, .{ "util", true }, .{ "v8", true }, .{ "vm", true }, .{ "wasi", true }, .{ "worker_threads", true }, .{ "zlib", true }, }); }; pub const ModuleType = enum { unknown, cjs, esm, pub const List = std.ComptimeStringMap(ModuleType, .{ .{ "commonjs", ModuleType.cjs }, .{ "module", ModuleType.esm }, }); }; pub const Platform = enum { neutral, browser, bun, bun_macro, node, pub inline fn isBun(this: Platform) bool { return switch (this) { .bun_macro, .bun => true, else => false, }; } pub inline fn isNotBun(this: Platform) bool { return switch (this) { .bun_macro, .bun => false, else => true, }; } pub inline fn isClient(this: Platform) bool { return switch (this) { .bun_macro, .bun => false, else => true, }; } pub inline fn supportsBrowserField(this: Platform) bool { return switch (this) { .bun_macro, .neutral, .browser, .bun => true, else => false, }; } const browser_define_value_true = "true"; const browser_define_value_false = "false"; pub inline fn processBrowserDefineValue(this: Platform) ?string { return switch (this) { .browser => browser_define_value_true, .bun_macro, .bun, .node => browser_define_value_false, else => null, }; } pub inline fn isWebLike(platform: Platform) bool { return switch (platform) { .neutral, .browser => true, else => false, }; } pub const Extensions = struct { pub const In = struct { pub const JavaScript = [_]string{ ".js", ".ts", ".tsx", ".jsx", ".json" }; }; pub const Out = struct { pub const JavaScript = [_]string{ ".js", ".mjs", }; }; }; pub fn outExtensions(platform: Platform, allocator: *std.mem.Allocator) std.StringHashMap(string) { var exts = std.StringHashMap(string).init(allocator); const js = Extensions.Out.JavaScript[0]; const mjs = Extensions.Out.JavaScript[1]; if (platform == .node) { for (Extensions.In.JavaScript) |ext| { exts.put(ext, mjs) catch unreachable; } } else { exts.put(mjs, js) catch unreachable; } for (Extensions.In.JavaScript) |ext| { exts.put(ext, js) catch unreachable; } return exts; } pub fn from(plat: ?api.Api.Platform) Platform { return switch (plat orelse api.Api.Platform._none) { .node => .node, .browser => .browser, .bun => .bun, else => .browser, }; } const MAIN_FIELD_NAMES = [_]string{ "browser", "module", "main" }; pub const DefaultMainFields: std.EnumArray(Platform, []const string) = brk: { var array = std.EnumArray(Platform, []const string).initUndefined(); // Note that this means if a package specifies "module" and "main", the ES6 // module will not be selected. This means tree shaking will not work when // targeting node environments. // // This is unfortunately necessary for compatibility. Some packages // incorrectly treat the "module" field as "code for the browser". It // actually means "code for ES6 environments" which includes both node // and the browser. // // For example, the package "@firebase/app" prints a warning on startup about // the bundler incorrectly using code meant for the browser if the bundler // selects the "module" field instead of the "main" field. // // If you want to enable tree shaking when targeting node, you will have to // configure the main fields to be "module" and then "main". Keep in mind // that some packages may break if you do this. var list = [_]string{ MAIN_FIELD_NAMES[1], MAIN_FIELD_NAMES[2] }; array.set(Platform.node, &list); // Note that this means if a package specifies "main", "module", and // "browser" then "browser" will win out over "module". This is the // same behavior as webpack: https://github.com/webpack/webpack/issues/4674. // // This is deliberate because the presence of the "browser" field is a // good signal that the "module" field may have non-browser stuff in it, // which will crash or fail to be bundled when targeting the browser. var listc = [_]string{ MAIN_FIELD_NAMES[0], MAIN_FIELD_NAMES[1], MAIN_FIELD_NAMES[2] }; array.set(Platform.browser, &listc); array.set(Platform.bun, &listc); // Original comment: // The neutral platform is for people that don't want esbuild to try to // pick good defaults for their platform. In that case, the list of main // fields is empty by default. You must explicitly configure it yourself. array.set(Platform.neutral, &listc); break :brk array; }; pub const default_conditions_strings = .{ .browser = @as(string, "browser"), .import = @as(string, "import"), .require = @as(string, "require"), .node = @as(string, "node"), .default = @as(string, "default"), .bun = @as(string, "bun"), .bun_macro = @as(string, "bun_macro"), .module = @as(string, "module"), // used in tslib }; pub const DefaultConditions: std.EnumArray(Platform, []const string) = brk: { var array = std.EnumArray(Platform, []const string).initUndefined(); // Note that this means if a package specifies "module" and "main", the ES6 // module will not be selected. This means tree shaking will not work when // targeting node environments. // // This is unfortunately necessary for compatibility. Some packages // incorrectly treat the "module" field as "code for the browser". It // actually means "code for ES6 environments" which includes both node // and the browser. // // For example, the package "@firebase/app" prints a warning on startup about // the bundler incorrectly using code meant for the browser if the bundler // selects the "module" field instead of the "main" field. // // If you want to enable tree shaking when targeting node, you will have to // configure the main fields to be "module" and then "main". Keep in mind // that some packages may break if you do this. array.set(Platform.node, &[_]string{default_conditions_strings.node}); // Note that this means if a package specifies "main", "module", and // "browser" then "browser" will win out over "module". This is the // same behavior as webpack: https://github.com/webpack/webpack/issues/4674. // // This is deliberate because the presence of the "browser" field is a // good signal that the "module" field may have non-browser stuff in it, // which will crash or fail to be bundled when targeting the browser. var listc = [_]string{ default_conditions_strings.browser, default_conditions_strings.module, }; array.set(Platform.browser, &listc); array.set( Platform.bun, &[_]string{ default_conditions_strings.bun, default_conditions_strings.module, default_conditions_strings.browser, }, ); // array.set(Platform.bun_macro, [_]string{ default_conditions_strings.bun_macro, default_conditions_strings.browser, default_conditions_strings.default, },); // Original comment: // The neutral platform is for people that don't want esbuild to try to // pick good defaults for their platform. In that case, the list of main // fields is empty by default. You must explicitly configure it yourself. array.set(Platform.neutral, &listc); break :brk array; }; }; pub const Loader = enum(u3) { jsx, js, ts, tsx, css, file, json, pub fn supportsClientEntryPoint(this: Loader) bool { return switch (this) { .jsx, .js, .ts, .tsx => true, else => false, }; } pub fn toAPI(loader: Loader) Api.Loader { return switch (loader) { .jsx => .jsx, .js => .js, .ts => .ts, .tsx => .tsx, .css => .css, .json => .json, else => .file, }; } pub fn isJSX(loader: Loader) bool { return loader == .jsx or loader == .tsx; } pub fn isTypeScript(loader: Loader) bool { return loader == .tsx or loader == .ts; } pub fn isJavaScriptLike(loader: Loader) bool { return switch (loader) { .jsx, .js, .ts, .tsx => true, else => false, }; } pub fn isJavaScriptLikeOrJSON(loader: Loader) bool { return switch (loader) { .jsx, .js, .ts, .tsx, .json => true, else => false, }; } pub fn forFileName(filename: string, obj: anytype) ?Loader { const ext = std.fs.path.extension(filename); if (ext.len == 0 or (ext.len == 1 and ext[0] == '.')) return null; return obj.get(ext); } }; pub const defaultLoaders = std.ComptimeStringMap(Loader, .{ .{ ".jsx", Loader.jsx }, .{ ".json", Loader.json }, .{ ".js", Loader.jsx }, .{ ".mjs", Loader.js }, .{ ".css", Loader.css }, .{ ".ts", Loader.ts }, .{ ".tsx", Loader.tsx }, }); // https://webpack.js.org/guides/package-exports/#reference-syntax pub const ESMConditions = struct { default: ConditionsMap = undefined, import: ConditionsMap = undefined, require: ConditionsMap = undefined, pub fn init(allocator: *std.mem.Allocator, defaults: []const string) !ESMConditions { var default_condition_amp = ConditionsMap.init(allocator); var import_condition_map = ConditionsMap.init(allocator); var require_condition_map = ConditionsMap.init(allocator); try default_condition_amp.ensureTotalCapacity(defaults.len + 1); try import_condition_map.ensureTotalCapacity(defaults.len + 1); try require_condition_map.ensureTotalCapacity(defaults.len + 1); import_condition_map.putAssumeCapacityNoClobber(Platform.default_conditions_strings.import, void{}); require_condition_map.putAssumeCapacityNoClobber(Platform.default_conditions_strings.require, void{}); default_condition_amp.putAssumeCapacityNoClobber(Platform.default_conditions_strings.default, void{}); for (defaults) |default| { default_condition_amp.putAssumeCapacityNoClobber(default, void{}); import_condition_map.putAssumeCapacityNoClobber(default, void{}); require_condition_map.putAssumeCapacityNoClobber(default, void{}); } return ESMConditions{ .default = default_condition_amp, .import = import_condition_map, .require = require_condition_map, }; } }; pub const JSX = struct { pub const Pragma = struct { // these need to be arrays factory: []const string = Defaults.Factory, fragment: []const string = Defaults.Fragment, runtime: JSX.Runtime = JSX.Runtime.automatic, /// Facilitates automatic JSX importing /// Set on a per file basis like this: /// /** @jsxImportSource @emotion/core */ import_source: string = "react/jsx-dev-runtime", classic_import_source: string = "react", package_name: []const u8 = "react", refresh_runtime: string = "react-refresh/runtime", supports_fast_refresh: bool = true, jsx: string = Defaults.JSXFunctionDev, jsx_static: string = Defaults.JSXStaticFunction, development: bool = true, parse: bool = true, pub fn parsePackageName(str: string) string { if (str[0] == '@') { if (strings.indexOfChar(str[1..], '/')) |first_slash| { var remainder = str[1 + first_slash + 1 ..]; if (strings.indexOfChar(remainder, '/')) |last_slash| { return str[0 .. first_slash + 1 + last_slash + 1]; } } } if (strings.indexOfChar(str, '/')) |first_slash| { return str[0..first_slash]; } return str; } pub fn isReactLike(pragma: *const Pragma) bool { return strings.eqlComptime(pragma.package_name, "react") or strings.eqlComptime(pragma.package_name, "@emotion/jsx") or strings.eqlComptime(pragma.package_name, "@emotion/react"); } pub const Defaults = struct { pub const Factory = &[_]string{"createElement"}; pub const Fragment = &[_]string{"Fragment"}; pub const ImportSourceDev = "react/jsx-dev-runtime"; pub const ImportSource = "react/jsx-runtime"; pub const JSXFunction = "jsx"; pub const JSXStaticFunction = "jsxs"; pub const JSXFunctionDev = "jsxDEV"; }; // "React.createElement" => ["React", "createElement"] // ...unless new is "React.createElement" and original is ["React", "createElement"] // saves an allocation for the majority case pub fn memberListToComponentsIfDifferent(allocator: *std.mem.Allocator, original: []const string, new: string) ![]const string { var splitter = std.mem.split(u8, new, "."); var needs_alloc = false; var count: usize = 0; while (splitter.next()) |str| { const i = (splitter.index orelse break); count = i; if (i > original.len) { needs_alloc = true; break; } if (!strings.eql(original[i], str)) { needs_alloc = true; break; } } if (!needs_alloc) { return original; } var out = try allocator.alloc(string, count + 1); splitter = std.mem.split(u8, new, "."); var i: usize = 0; while (splitter.next()) |str| { out[i] = str; i += 1; } return out; } pub fn fromApi(jsx: api.Api.Jsx, allocator: *std.mem.Allocator) !Pragma { var pragma = JSX.Pragma{}; if (jsx.fragment.len > 0) { pragma.fragment = try memberListToComponentsIfDifferent(allocator, pragma.fragment, jsx.fragment); } if (jsx.factory.len > 0) { pragma.factory = try memberListToComponentsIfDifferent(allocator, pragma.factory, jsx.factory); } if (jsx.import_source.len > 0) { pragma.import_source = jsx.import_source; pragma.package_name = parsePackageName(pragma.import_source); } else if (jsx.development) { pragma.import_source = Defaults.ImportSourceDev; pragma.jsx = Defaults.JSXFunctionDev; pragma.package_name = "react"; } else { pragma.import_source = Defaults.ImportSource; pragma.jsx = Defaults.JSXFunction; } pragma.development = jsx.development; pragma.runtime = jsx.runtime; pragma.parse = true; return pragma; } }; pub const Runtime = api.Api.JsxRuntime; }; const TypeScript = struct { parse: bool = false, }; pub const Timings = struct { resolver: i128 = 0, parse: i128 = 0, print: i128 = 0, http: i128 = 0, read_file: i128 = 0, }; pub const DefaultUserDefines = struct { pub const HotModuleReloading = struct { pub const Key = "process.env.BUN_HMR_ENABLED"; pub const Value = "true"; }; pub const HotModuleReloadingVerbose = struct { pub const Key = "process.env.BUN_HMR_VERBOSE"; pub const Value = "true"; }; // This must be globally scoped so it doesn't disappear pub const NodeEnv = struct { pub const Key = "process.env.NODE_ENV"; pub const Value = "\"development\""; }; pub const PlatformDefine = struct { pub const Key = "process.browser"; pub const Value = []string{ "false", "true" }; }; }; pub fn definesFromTransformOptions( allocator: *std.mem.Allocator, log: *logger.Log, _input_define: ?Api.StringMap, hmr: bool, platform: Platform, loader: ?*DotEnv.Loader, framework_env: ?*const Env, ) !*defines.Define { var input_user_define = _input_define orelse std.mem.zeroes(Api.StringMap); var user_defines = try stringHashMapFromArrays( defines.RawDefines, allocator, input_user_define.keys, input_user_define.values, ); if (loader) |_loader| { if (framework_env) |framework| { _ = try _loader.copyForDefine( defines.RawDefines, &user_defines, framework.toAPI().defaults, framework.behavior, framework.prefix, allocator, ); } else { _ = try _loader.copyForDefine( defines.RawDefines, &user_defines, std.mem.zeroes(Api.StringMap), Api.DotEnvBehavior.disable, "", allocator, ); } } if (input_user_define.keys.len == 0) { try user_defines.put(DefaultUserDefines.NodeEnv.Key, DefaultUserDefines.NodeEnv.Value); } if (hmr) { try user_defines.put(DefaultUserDefines.HotModuleReloading.Key, DefaultUserDefines.HotModuleReloading.Value); } // Automatically set `process.browser` to `true` for browsers and false for node+js // This enables some extra dead code elimination if (platform.processBrowserDefineValue()) |value| { _ = try user_defines.getOrPutValue(DefaultUserDefines.PlatformDefine.Key, value); } var resolved_defines = try defines.DefineData.from_input(user_defines, log, allocator); return try defines.Define.init( allocator, resolved_defines, ); } pub fn loadersFromTransformOptions(allocator: *std.mem.Allocator, _loaders: ?Api.LoaderMap) !std.StringHashMap(Loader) { var input_loaders = _loaders orelse std.mem.zeroes(Api.LoaderMap); var loader_values = try allocator.alloc(Loader, input_loaders.loaders.len); for (loader_values) |_, i| { const loader = switch (input_loaders.loaders[i]) { .jsx => Loader.jsx, .js => Loader.js, .ts => Loader.ts, .css => Loader.css, .tsx => Loader.tsx, .json => Loader.json, else => unreachable, }; loader_values[i] = loader; } var loaders = try stringHashMapFromArrays( std.StringHashMap(Loader), allocator, input_loaders.extensions, loader_values, ); const default_loader_ext = comptime [_]string{ ".jsx", ".json", ".js", ".mjs", ".css", ".ts", ".tsx" }; inline for (default_loader_ext) |ext| { if (!loaders.contains(ext)) { try loaders.put(ext, defaultLoaders.get(ext).?); } } return loaders; } /// BundleOptions is used when ResolveMode is not set to "disable". /// BundleOptions is effectively webpack + babel pub const BundleOptions = struct { footer: string = "", banner: string = "", define: *defines.Define, loaders: std.StringHashMap(Loader), resolve_dir: string = "/", jsx: JSX.Pragma = JSX.Pragma{}, hot_module_reloading: bool = false, inject: ?[]string = null, origin: URL = URL{}, output_dir: string = "", output_dir_handle: ?Dir = null, node_modules_bundle_url: string = "", node_modules_bundle_pretty_path: string = "", write: bool = false, preserve_symlinks: bool = false, preserve_extensions: bool = false, timings: Timings = Timings{}, node_modules_bundle: ?*NodeModuleBundle = null, production: bool = false, serve: bool = false, append_package_version_in_query_string: bool = false, resolve_mode: api.Api.ResolveMode, tsconfig_override: ?string = null, platform: Platform = Platform.browser, main_fields: []const string = Platform.DefaultMainFields.get(Platform.browser), log: *logger.Log, external: ExternalModules = ExternalModules{}, entry_points: []const string, extension_order: []const string = &Defaults.ExtensionOrder, out_extensions: std.StringHashMap(string), import_path_format: ImportPathFormat = ImportPathFormat.relative, framework: ?Framework = null, routes: RouteConfig = RouteConfig.zero(), defines_loaded: bool = false, env: Env = Env{}, transform_options: Api.TransformOptions, polyfill_node_globals: bool = true, conditions: ESMConditions = undefined, pub inline fn cssImportBehavior(this: *const BundleOptions) Api.CssInJsBehavior { switch (this.platform) { .neutral, .browser => { if (this.framework) |framework| { return framework.client_css_in_js; } return .auto_onimportcss; }, else => return .facade, } } pub fn areDefinesUnset(this: *const BundleOptions) bool { return !this.defines_loaded; } pub fn loadDefines(this: *BundleOptions, allocator: *std.mem.Allocator, loader_: ?*DotEnv.Loader, env: ?*const Env) !void { if (this.defines_loaded) { return; } this.define = try definesFromTransformOptions( allocator, this.log, this.transform_options.define, this.transform_options.serve orelse false, this.platform, loader_, env, ); this.defines_loaded = true; } pub fn loader(this: *const BundleOptions, ext: string) Loader { return this.loaders.get(ext) orelse .file; } pub fn asJavascriptBundleConfig(this: *const BundleOptions) Api.JavascriptBundleConfig {} pub fn isFrontendFrameworkEnabled(this: *const BundleOptions) bool { const framework: *const Framework = &(this.framework orelse return false); return framework.resolved and (framework.client.isEnabled() or framework.fallback.isEnabled()); } pub const ImportPathFormat = enum { relative, // omit file extension for Node.js packages relative_nodejs, absolute_url, // omit file extension absolute_path, package_path, }; pub const Defaults = struct { pub const ExtensionOrder = [_]string{ ".tsx", ".ts", ".jsx", ".js", ".json", }; pub const CSSExtensionOrder = [_]string{ ".css", }; }; pub fn fromApi( allocator: *std.mem.Allocator, fs: *Fs.FileSystem, log: *logger.Log, transform: Api.TransformOptions, node_modules_bundle_existing: ?*NodeModuleBundle, ) !BundleOptions { const output_dir_parts = [_]string{ try std.process.getCwdAlloc(allocator), transform.output_dir orelse "out" }; var opts: BundleOptions = BundleOptions{ .log = log, .resolve_mode = transform.resolve orelse .dev, .define = undefined, .loaders = try loadersFromTransformOptions(allocator, transform.loaders), .output_dir = try fs.absAlloc(allocator, &output_dir_parts), .platform = Platform.from(transform.platform), .write = transform.write orelse false, .external = undefined, .entry_points = transform.entry_points, .out_extensions = undefined, .env = Env.init(allocator), .transform_options = transform, }; Analytics.Features.define = Analytics.Features.define or transform.define != null; Analytics.Features.loaders = Analytics.Features.loaders or transform.loaders != null; if (transform.origin) |origin| { opts.origin = URL.parse(origin); } if (transform.jsx) |jsx| { opts.jsx = try JSX.Pragma.fromApi(jsx, allocator); } if (transform.extension_order.len > 0) { opts.extension_order = transform.extension_order; } if (transform.platform) |plat| { opts.platform = Platform.from(plat); opts.main_fields = Platform.DefaultMainFields.get(opts.platform); } opts.conditions = try ESMConditions.init(allocator, Platform.DefaultConditions.get(opts.platform)); if (transform.serve orelse false) { // When we're serving, we need some kind of URL. if (!opts.origin.isAbsolute()) { const protocol: string = if (opts.origin.hasHTTPLikeProtocol()) opts.origin.protocol else "http"; const had_valid_port = opts.origin.hasValidPort(); const port: string = if (had_valid_port) opts.origin.port else "3000"; opts.origin = URL.parse( try std.fmt.allocPrint( allocator, "{s}://localhost:{s}{s}", .{ protocol, port, opts.origin.path, }, ), ); opts.origin.port_was_automatically_set = !had_valid_port; } } switch (opts.platform) { .node => { opts.import_path_format = .relative_nodejs; }, .bun => { // If we're doing SSR, we want all the URLs to be the same as what it would be in the browser // If we're not doing SSR, we want all the import paths to be absolute opts.import_path_format = if (opts.import_path_format == .absolute_url) .absolute_url else .absolute_path; }, else => {}, } const is_generating_bundle = (transform.generate_node_module_bundle orelse false); // if (!(transform.generate_node_module_bundle orelse false)) { if (node_modules_bundle_existing) |node_mods| { opts.node_modules_bundle = node_mods; const pretty_path = fs.relativeTo(transform.node_modules_bundle_path.?); opts.node_modules_bundle_url = try std.fmt.allocPrint(allocator, "{s}{s}", .{ opts.origin, pretty_path, }); } else if (transform.node_modules_bundle_path) |bundle_path| { if (bundle_path.len > 0) { load_bundle: { const pretty_path = fs.relativeTo(bundle_path); const bundle_file = Fs.FileSystem.openFileAbsolute(bundle_path, .{ .read = true, .write = true }) catch |err| { if (is_generating_bundle) { break :load_bundle; } Output.disableBuffering(); defer Output.enableBuffering(); Output.prettyErrorln("error opening \"{s}\": {s}", .{ pretty_path, @errorName(err) }); break :load_bundle; }; defer { if (is_generating_bundle) Fs.FileSystem.close(bundle_file); } const time_start = std.time.nanoTimestamp(); if (NodeModuleBundle.loadBundle(allocator, Fs.File{ .handle = bundle_file })) |bundle| { if (!is_generating_bundle) { var node_module_bundle = try allocator.create(NodeModuleBundle); node_module_bundle.* = bundle; opts.node_modules_bundle = node_module_bundle; if (opts.origin.isAbsolute()) { opts.node_modules_bundle_url = try opts.origin.joinAlloc( allocator, "", "", node_module_bundle.bundle.import_from_name, "", "", ); opts.node_modules_bundle_pretty_path = opts.node_modules_bundle_url[opts.node_modules_bundle_url.len - node_module_bundle.bundle.import_from_name.len - 1 ..]; } else { opts.node_modules_bundle_pretty_path = try allocator.dupe(u8, pretty_path); } const elapsed = @intToFloat(f64, (std.time.nanoTimestamp() - time_start)) / std.time.ns_per_ms; Output.printElapsed(elapsed); Output.prettyErrorln( " \"{s}\" - {d} modules, {d} packages", .{ pretty_path, bundle.bundle.modules.len, bundle.bundle.packages.len, }, ); Output.flush(); } if (transform.framework == null) { if (bundle.container.framework) |loaded_framework| { opts.framework = try Framework.fromLoadedFramework(loaded_framework, allocator); if (transform.define == null) { if (opts.platform.isClient()) { if (opts.framework.?.client.kind != .disabled) { opts.env = opts.framework.?.client.env; } else if (opts.framework.?.fallback.kind != .disabled) { opts.env = opts.framework.?.fallback.env; } } else { opts.env = opts.framework.?.server.env; } } } } if (transform.router == null) { if (bundle.container.routes) |routes| { opts.routes = RouteConfig.fromLoadedRoutes(routes); } } } else |err| { if (!is_generating_bundle) { Output.disableBuffering(); Output.prettyErrorln( "error reading \"{s}\": {s}, deleting it so you don't keep seeing this message.", .{ pretty_path, @errorName(err) }, ); FileSystem.close(bundle_file); } } } } } // } if (transform.framework) |_framework| { opts.framework = try Framework.fromApi(_framework, allocator); } if (transform.router) |routes| { opts.routes = try RouteConfig.fromApi(routes, allocator); } if (transform.main_fields.len > 0) { opts.main_fields = transform.main_fields; } opts.external = ExternalModules.init(allocator, &fs.fs, fs.top_level_dir, transform.external, log, opts.platform); opts.out_extensions = opts.platform.outExtensions(allocator); if (transform.serve orelse false) { opts.preserve_extensions = true; opts.append_package_version_in_query_string = true; opts.resolve_mode = .lazy; var dir_to_use: string = opts.routes.static_dir; const static_dir_set = !opts.routes.static_dir_enabled or dir_to_use.len > 0; var disabled_static = false; var chosen_dir = dir_to_use; if (!static_dir_set) { chosen_dir = choice: { if (fs.fs.readDirectory(fs.top_level_dir, null)) |dir_| { const dir: *const Fs.EntriesOption = dir_; switch (dir.*) { .entries => { if (dir.entries.getComptimeQuery("public")) |q| { if (q.entry.kind(&fs.fs) == .dir) { break :choice "public"; } } if (dir.entries.getComptimeQuery("static")) |q| { if (q.entry.kind(&fs.fs) == .dir) { break :choice "static"; } } break :choice "."; }, else => { break :choice ""; }, } } else |err| { break :choice ""; } }; if (chosen_dir.len == 0) { disabled_static = true; opts.routes.static_dir_enabled = false; } } if (!disabled_static) { var _dirs = [_]string{chosen_dir}; opts.routes.static_dir = try fs.absAlloc(allocator, &_dirs); opts.routes.static_dir_handle = FileSystem.openDirectory(opts.routes.static_dir, .{ .iterate = true }) catch |err| brk: { var did_warn = false; switch (err) { error.FileNotFound => { opts.routes.static_dir_enabled = false; }, error.AccessDenied => { Output.prettyErrorln( "error: access denied when trying to open directory for static files: \"{s}\".\nPlease re-open Bun with access to this folder or pass a different folder via \"--public-dir\". Note: --public-dir is relative to --cwd (or the process' current working directory).\n\nThe public folder is where static assets such as images, fonts, and .html files go.", .{opts.routes.static_dir}, ); std.process.exit(1); }, else => { Output.prettyErrorln( "error: \"{s}\" when accessing public folder: \"{s}\"", .{ @errorName(err), opts.routes.static_dir }, ); std.process.exit(1); }, } break :brk null; }; opts.routes.static_dir_enabled = opts.routes.static_dir_handle != null; } if (opts.routes.static_dir_enabled and (opts.framework == null or !opts.framework.?.server.isEnabled()) and !opts.routes.routes_enabled) { const dir = opts.routes.static_dir_handle.?; var index_html_file = dir.openFile("index.html", .{ .read = true }) catch |err| brk: { switch (err) { error.FileNotFound => {}, else => { Output.prettyErrorln( "{s} when trying to open {s}/index.html. single page app routing is disabled.", .{ @errorName(err), opts.routes.static_dir }, ); }, } opts.routes.single_page_app_routing = false; break :brk null; }; if (index_html_file) |index_dot_html| { opts.routes.single_page_app_routing = true; opts.routes.single_page_app_fd = index_dot_html.handle; } } // Windows has weird locking rules for file access. // so it's a bad idea to keep a file handle open for a long time on Windows. if (isWindows and opts.routes.static_dir_handle != null) { opts.routes.static_dir_handle.?.close(); } opts.hot_module_reloading = opts.platform.isWebLike(); if (transform.disable_hmr orelse false) opts.hot_module_reloading = false; opts.serve = true; } if (opts.origin.isAbsolute()) { opts.import_path_format = ImportPathFormat.absolute_url; } if (opts.write and opts.output_dir.len > 0) { opts.output_dir_handle = try openOutputDir(opts.output_dir); } opts.polyfill_node_globals = opts.platform != .node; Analytics.Features.framework = Analytics.Features.framework or opts.framework != null; Analytics.Features.filesystem_router = Analytics.Features.filesystem_router or opts.routes.routes_enabled; Analytics.Features.origin = Analytics.Features.origin or transform.origin != null; Analytics.Features.public_folder = Analytics.Features.public_folder or opts.routes.static_dir_enabled; Analytics.Features.bun_bun = Analytics.Features.bun_bun or transform.node_modules_bundle_path != null; Analytics.Features.bunjs = Analytics.Features.bunjs or transform.node_modules_bundle_path_server != null; Analytics.Features.macros = Analytics.Features.macros or opts.platform == .bun_macro; Analytics.Features.external = Analytics.Features.external or transform.external.len > 0; Analytics.Features.single_page_app_routing = Analytics.Features.single_page_app_routing or opts.routes.single_page_app_routing; return opts; } }; pub fn openOutputDir(output_dir: string) !Dir { return FileSystem.openDirectory(output_dir, Dir.OpenDirOptions{}) catch brk: { FileSystem.mkdir(output_dir) catch |err| { Output.printErrorln("error: Unable to mkdir \"{s}\": \"{s}\"", .{ output_dir, @errorName(err) }); Global.crash(); }; var handle = FileSystem.openDirectory(output_dir, Dir.OpenDirOptions{}) catch |err2| { Output.printErrorln("error: Unable to open \"{s}\": \"{s}\"", .{ output_dir, @errorName(err2) }); Global.crash(); }; break :brk handle; }; } pub const TransformOptions = struct { footer: string = "", banner: string = "", define: std.StringHashMap(string), loader: Loader = Loader.js, resolve_dir: string = "/", jsx: ?JSX.Pragma, react_fast_refresh: bool = false, inject: ?[]string = null, origin: string = "", preserve_symlinks: bool = false, entry_point: Fs.LoadedFile, resolve_paths: bool = false, tsconfig_override: ?string = null, platform: Platform = Platform.browser, main_fields: []string = Platform.DefaultMainFields.get(Platform.browser), pub fn initUncached(allocator: *std.mem.Allocator, entryPointName: string, code: string) !TransformOptions { assert(entryPointName.len > 0); var entryPoint = Fs.LoadedFile{ .path = Fs.Path.init(entryPointName), .contents = code, }; var cwd: string = "/"; if (isWasi or isNative) { cwd = try std.process.getCwdAlloc(allocator); } var define = std.StringHashMap(string).init(allocator); try define.ensureCapacity(1); define.putAssumeCapacity("process.env.NODE_ENV", "development"); var loader = Loader.file; if (defaultLoaders.get(entryPoint.path.name.ext)) |defaultLoader| { loader = defaultLoader; } assert(code.len > 0); return TransformOptions{ .entry_point = entryPoint, .define = define, .loader = loader, .resolve_dir = entryPoint.path.name.dir, .main_fields = Platform.DefaultMainFields.get(Platform.browser), .jsx = if (Loader.isJSX(loader)) JSX.Pragma{} else null, }; } }; // Instead of keeping files in-memory, we: // 1. Write directly to disk // 2. (Optional) move the file to the destination // This saves us from allocating a buffer pub const OutputFile = struct { loader: Loader, input: Fs.Path, value: Value, size: usize = 0, mtime: ?i128 = null, // Depending on: // - The platform // - The number of open file handles // - Whether or not a file of the same name exists // We may use a different system call pub const FileOperation = struct { pathname: string, fd: FileDescriptorType = 0, dir: FileDescriptorType = 0, is_tmpdir: bool = false, is_outdir: bool = false, close_handle_on_complete: bool = false, autowatch: bool = true, pub fn fromFile(fd: FileDescriptorType, pathname: string) FileOperation { return .{ .pathname = pathname, .fd = fd, }; } pub fn getPathname(file: *const FileOperation) string { if (file.is_tmpdir) { return resolve_path.joinAbs(@TypeOf(Fs.FileSystem.instance.fs).tmpdir_path, .auto, file.pathname); } else { return file.pathname; } } }; pub const Value = union(Kind) { buffer: []const u8, move: FileOperation, copy: FileOperation, noop: u0, pending: resolver.Result, }; pub const Kind = enum { move, copy, noop, buffer, pending }; pub fn initPending(loader: Loader, pending: resolver.Result) OutputFile { return .{ .loader = .file, .input = pending.pathConst().?.*, .size = 0, .value = .{ .pending = pending }, }; } pub fn initFile(handle: FileDescriptorType, pathname: string, size: usize) OutputFile { return .{ .loader = .file, .input = Fs.Path.init(pathname), .size = size, .value = .{ .copy = FileOperation.fromFile(handle, pathname) }, }; } pub fn initFileWithDir(file: File, pathname: string, size: usize, dir: Dir) OutputFile { var res = initFile(file, pathname, size); res.value.copy.dir_handle = dir.fd; return res; } pub fn initBuf(buf: []const u8, pathname: string, loader: Loader) OutputFile { return .{ .loader = loader, .input = Fs.Path.init(pathname), .size = buf.len, .value = .{ .buffer = buf }, }; } pub fn moveTo(file: *const OutputFile, base_path: string, rel_path: []u8, dir: FileDescriptorType) !void { var move = file.value.move; if (move.dir > 0) { std.os.renameat(move.dir, move.pathname, dir, rel_path) catch |err| { const dir_ = Dir{ .fd = dir }; if (std.fs.path.dirname(rel_path)) |dirname| { dir_.makePath(dirname) catch {}; std.os.renameat(move.dir, move.pathname, dir, rel_path) catch {}; return; } }; return; } try std.os.rename(move.pathname, resolve_path.joinAbs(base_path, .auto, rel_path)); } pub fn copyTo(file: *const OutputFile, base_path: string, rel_path: []u8, dir: FileDescriptorType) !void { var copy = file.value.copy; var dir_obj = Dir{ .fd = dir }; const file_out = (try dir_obj.createFile(rel_path, .{})); const fd_out = file_out.handle; var do_close = false; // TODO: close file_out on error const fd_in = (try std.fs.openFileAbsolute(file.input.text, .{ .read = true })).handle; if (isNative) { Fs.FileSystem.setMaxFd(fd_out); Fs.FileSystem.setMaxFd(fd_in); do_close = Fs.FileSystem.instance.fs.needToCloseFiles(); } defer { if (do_close) { std.os.close(fd_out); std.os.close(fd_in); } } const os = std.os; if (comptime std.Target.current.isDarwin()) { const rc = os.system.fcopyfile(fd_in, fd_out, null, os.system.COPYFILE_DATA); if (rc == 0) { return; } } if (std.Target.current.os.tag == .linux) { // Try copy_file_range first as that works at the FS level and is the // most efficient method (if available). var offset: u64 = 0; cfr_loop: while (true) { const math = std.math; // The kernel checks the u64 value `offset+count` for overflow, use // a 32 bit value so that the syscall won't return EINVAL except for // impossibly large files (> 2^64-1 - 2^32-1). const amt = try os.copy_file_range(fd_in, offset, fd_out, offset, math.maxInt(u32), 0); // Terminate when no data was copied if (amt == 0) break :cfr_loop; offset += amt; } return; } // Sendfile is a zero-copy mechanism iff the OS supports it, otherwise the // fallback code will copy the contents chunk by chunk. const empty_iovec = [0]os.iovec_const{}; var offset: u64 = 0; sendfile_loop: while (true) { const amt = try os.sendfile(fd_out, fd_in, offset, 0, &empty_iovec, &empty_iovec, 0); // Terminate when no data was copied if (amt == 0) break :sendfile_loop; offset += amt; } } }; pub const TransformResult = struct { errors: []logger.Msg = &([_]logger.Msg{}), warnings: []logger.Msg = &([_]logger.Msg{}), output_files: []OutputFile = &([_]OutputFile{}), outbase: string, root_dir: ?Dir = null, pub fn init( outbase: string, output_files: []OutputFile, log: *logger.Log, allocator: *std.mem.Allocator, ) !TransformResult { var errors = try std.ArrayList(logger.Msg).initCapacity(allocator, log.errors); var warnings = try std.ArrayList(logger.Msg).initCapacity(allocator, log.warnings); for (log.msgs.items) |msg| { switch (msg.kind) { logger.Kind.err => { errors.append(msg) catch unreachable; }, logger.Kind.warn => { warnings.append(msg) catch unreachable; }, else => {}, } } return TransformResult{ .outbase = outbase, .output_files = output_files, .errors = errors.toOwnedSlice(), .warnings = warnings.toOwnedSlice(), }; } }; pub const Env = struct { const Entry = struct { key: string, value: string, }; const List = std.MultiArrayList(Entry); behavior: Api.DotEnvBehavior = Api.DotEnvBehavior.disable, prefix: string = "", defaults: List = List{}, allocator: *std.mem.Allocator = undefined, pub fn init( allocator: *std.mem.Allocator, ) Env { return Env{ .allocator = allocator, .defaults = List{}, .prefix = "", .behavior = Api.DotEnvBehavior.disable, }; } pub fn ensureTotalCapacity(this: *Env, capacity: u64) !void { try this.defaults.ensureTotalCapacity(this.allocator, capacity); } pub fn setDefaultsMap(this: *Env, defaults: Api.StringMap) !void { this.defaults.shrinkRetainingCapacity(0); if (defaults.keys.len == 0) { return; } try this.defaults.ensureTotalCapacity(this.allocator, defaults.keys.len); for (defaults.keys) |key, i| { this.defaults.appendAssumeCapacity(.{ .key = key, .value = defaults.values[i] }); } } // For reading from API pub fn setFromAPI(this: *Env, config: Api.EnvConfig) !void { this.setBehaviorFromPrefix(config.prefix orelse ""); if (config.defaults) |defaults| { try this.setDefaultsMap(defaults); } } pub fn setBehaviorFromPrefix(this: *Env, prefix: string) void { this.behavior = Api.DotEnvBehavior.disable; this.prefix = ""; if (strings.eqlComptime(prefix, "*")) { this.behavior = Api.DotEnvBehavior.load_all; } else if (prefix.len > 0) { this.behavior = Api.DotEnvBehavior.prefix; this.prefix = prefix; } } pub fn setFromLoaded(this: *Env, config: Api.LoadedEnvConfig, allocator: *std.mem.Allocator) !void { this.allocator = allocator; this.behavior = switch (config.dotenv) { Api.DotEnvBehavior.prefix => Api.DotEnvBehavior.prefix, Api.DotEnvBehavior.load_all => Api.DotEnvBehavior.load_all, else => Api.DotEnvBehavior.disable, }; this.prefix = config.prefix; try this.setDefaultsMap(config.defaults); } pub fn toAPI(this: *const Env) Api.LoadedEnvConfig { var slice = this.defaults.slice(); return Api.LoadedEnvConfig{ .dotenv = this.behavior, .prefix = this.prefix, .defaults = .{ .keys = slice.items(.key), .values = slice.items(.value) }, }; } // For reading from package.json pub fn getOrPutValue(this: *Env, key: string, value: string) !void { var slice = this.defaults.slice(); const keys = slice.items(.key); for (keys) |_key, i| { if (strings.eql(key, _key)) { return; } } try this.defaults.append(this.allocator, .{ .key = key, .value = value }); } }; pub const EntryPoint = struct { path: string = "", env: Env = Env{}, kind: Kind = Kind.disabled, pub fn isEnabled(this: *const EntryPoint) bool { return this.kind != .disabled and this.path.len > 0; } pub const Kind = enum { client, server, fallback, disabled, pub fn toAPI(this: Kind) Api.FrameworkEntryPointType { return switch (this) { .client => .client, .server => .server, .fallback => .fallback, else => unreachable, }; } }; pub fn toAPI(this: *const EntryPoint, allocator: *std.mem.Allocator, toplevel_path: string, kind: Kind) !?Api.FrameworkEntryPoint { if (this.kind == .disabled) return null; return Api.FrameworkEntryPoint{ .kind = kind.toAPI(), .env = this.env.toAPI(), .path = try this.normalizedPath(allocator, toplevel_path) }; } fn normalizedPath(this: *const EntryPoint, allocator: *std.mem.Allocator, toplevel_path: string) !string { std.debug.assert(std.fs.path.isAbsolute(this.path)); var str = this.path; if (strings.indexOf(str, toplevel_path)) |top| { str = str[top + toplevel_path.len ..]; } // if it *was* a node_module path, we don't do any allocation, we just keep it as a package path if (strings.indexOf(str, "node_modules" ++ std.fs.path.sep_str)) |node_module_i| { return str[node_module_i + "node_modules".len + 1 ..]; // otherwise, we allocate a new string and copy the path into it with a leading "./" } else { var out = try allocator.alloc(u8, str.len + 2); out[0] = '.'; out[1] = '/'; std.mem.copy(u8, out[2..], str); return out; } } pub fn fromLoaded( this: *EntryPoint, framework_entry_point: Api.FrameworkEntryPoint, allocator: *std.mem.Allocator, kind: Kind, ) !void { this.path = framework_entry_point.path; this.kind = kind; this.env.setFromLoaded(framework_entry_point.env, allocator) catch {}; } pub fn fromAPI( this: *EntryPoint, framework_entry_point: Api.FrameworkEntryPointMessage, allocator: *std.mem.Allocator, kind: Kind, ) !void { this.path = framework_entry_point.path orelse ""; this.kind = kind; if (this.path.len == 0) { this.kind = .disabled; return; } if (framework_entry_point.env) |env| { this.env.allocator = allocator; try this.env.setFromAPI(env); } } }; pub const Framework = struct { client: EntryPoint = EntryPoint{}, server: EntryPoint = EntryPoint{}, fallback: EntryPoint = EntryPoint{}, display_name: string = "", package: string = "", development: bool = true, resolved: bool = false, from_bundle: bool = false, resolved_dir: string = "", override_modules: Api.StringMap = Api.StringMap{}, override_modules_hashes: []u64 = &[_]u64{}, client_css_in_js: Api.CssInJsBehavior = .auto_onimportcss, pub const fallback_html: string = @embedFile("./fallback.html"); pub fn platformEntryPoint(this: *const Framework, platform: Platform) ?*const EntryPoint { const entry: *const EntryPoint = switch (platform) { .neutral, .browser => &this.client, .bun => &this.server, .node => return null, }; if (entry.kind == .disabled) return null; return entry; } pub fn fromLoadedFramework(loaded: Api.LoadedFramework, allocator: *std.mem.Allocator) !Framework { var framework = Framework{ .package = loaded.package, .development = loaded.development, .from_bundle = true, .client_css_in_js = loaded.client_css_in_js, .display_name = loaded.display_name, .override_modules = loaded.override_modules, }; if (loaded.entry_points.fallback) |fallback| { try framework.fallback.fromLoaded(fallback, allocator, .fallback); } if (loaded.entry_points.client) |client| { try framework.client.fromLoaded(client, allocator, .client); } if (loaded.entry_points.server) |server| { try framework.server.fromLoaded(server, allocator, .server); } return framework; } pub fn toAPI( this: *const Framework, allocator: *std.mem.Allocator, toplevel_path: string, ) !?Api.LoadedFramework { if (this.client.kind == .disabled and this.server.kind == .disabled and this.fallback.kind == .disabled) return null; return Api.LoadedFramework{ .package = this.package, .development = this.development, .display_name = this.display_name, .entry_points = .{ .client = try this.client.toAPI(allocator, toplevel_path, .client), .fallback = try this.fallback.toAPI(allocator, toplevel_path, .fallback), .server = try this.server.toAPI(allocator, toplevel_path, .server), }, .client_css_in_js = this.client_css_in_js, .override_modules = this.override_modules, }; } pub fn needsResolveFromPackage(this: *const Framework) bool { return !this.resolved and this.package.len > 0; } pub fn fromApi( transform: Api.FrameworkConfig, allocator: *std.mem.Allocator, ) !Framework { var client = EntryPoint{}; var server = EntryPoint{}; var fallback = EntryPoint{}; if (transform.client) |_client| { try client.fromAPI(_client, allocator, .client); } if (transform.server) |_server| { try server.fromAPI(_server, allocator, .server); } if (transform.fallback) |_fallback| { try fallback.fromAPI(_fallback, allocator, .fallback); } return Framework{ .client = client, .server = server, .fallback = fallback, .package = transform.package orelse "", .display_name = transform.display_name orelse "", .development = transform.development orelse true, .override_modules = transform.override_modules orelse .{ .keys = &.{}, .values = &.{} }, .resolved = false, .client_css_in_js = switch (transform.client_css_in_js orelse .auto_onimportcss) { .facade_onimportcss => .facade_onimportcss, .facade => .facade, else => .auto_onimportcss, }, }; } }; pub const RouteConfig = struct { dir: string = "", possible_dirs: []const string = &[_]string{}, // Frameworks like Next.js (and others) use a special prefix for bundled/transpiled assets // This is combined with "origin" when printing import paths asset_prefix_path: string = "", // TODO: do we need a separate list for data-only extensions? // e.g. /foo.json just to get the data for the route, without rendering the html // I think it's fine to hardcode as .json for now, but if I personally were writing a framework // I would consider using a custom binary format to minimize request size // maybe like CBOR extensions: []const string = &[_][]const string{}, routes_enabled: bool = false, static_dir: string = "", static_dir_handle: ?Dir = null, static_dir_enabled: bool = false, single_page_app_routing: bool = false, single_page_app_fd: StoredFileDescriptorType = 0, pub fn toAPI(this: *const RouteConfig) Api.LoadedRouteConfig { return .{ .asset_prefix = this.asset_prefix_path, .dir = if (this.routes_enabled) this.dir else "", .extensions = this.extensions, .static_dir = if (this.static_dir_enabled) this.static_dir else "", }; } pub const DefaultDir = "pages"; pub const DefaultStaticDir = "public"; pub const DefaultExtensions = [_]string{ "tsx", "ts", "mjs", "jsx", "js" }; pub inline fn zero() RouteConfig { return RouteConfig{ .dir = DefaultDir, .extensions = std.mem.span(&DefaultExtensions), .static_dir = DefaultStaticDir, .routes_enabled = false, }; } pub fn fromLoadedRoutes(loaded: Api.LoadedRouteConfig) RouteConfig { return RouteConfig{ .extensions = loaded.extensions, .dir = loaded.dir, .asset_prefix_path = loaded.asset_prefix, .static_dir = loaded.static_dir, .routes_enabled = loaded.dir.len > 0, .static_dir_enabled = loaded.static_dir.len > 0, }; } pub fn fromApi(router_: Api.RouteConfig, allocator: *std.mem.Allocator) !RouteConfig { var router = zero(); var static_dir: string = std.mem.trimRight(u8, router_.static_dir orelse "", "/\\"); var asset_prefix: string = std.mem.trimRight(u8, router_.asset_prefix orelse "", "/\\"); switch (router_.dir.len) { 0 => {}, 1 => { router.dir = std.mem.trimRight(u8, router_.dir[0], "/\\"); router.routes_enabled = router.dir.len > 0; }, else => { router.possible_dirs = router_.dir; for (router_.dir) |dir| { const trimmed = std.mem.trimRight(u8, dir, "/\\"); if (trimmed.len > 0) { router.dir = trimmed; } } router.routes_enabled = router.dir.len > 0; }, } if (static_dir.len > 0) { router.static_dir = static_dir; } if (asset_prefix.len > 0) { router.asset_prefix_path = asset_prefix; } if (router_.extensions.len > 0) { var count: usize = 0; for (router_.extensions) |_ext| { const ext = std.mem.trimLeft(u8, _ext, "."); if (ext.len == 0) { continue; } count += 1; } var extensions = try allocator.alloc(string, count); var remainder = extensions; for (router_.extensions) |_ext| { const ext = std.mem.trimLeft(u8, _ext, "."); if (ext.len == 0) { continue; } remainder[0] = ext; remainder = remainder[1..]; } router.extensions = extensions; } return router; } };