const std = @import("std"); const logger = bun.logger; const bun = @import("root").bun; const string = bun.string; const Output = bun.Output; const Global = bun.Global; const Environment = bun.Environment; const strings = bun.strings; const MutableString = bun.MutableString; const stringZ = bun.stringZ; const default_allocator = bun.default_allocator; const CodePoint = bun.CodePoint; const C = bun.C; const CodepointIterator = @import("./string_immutable.zig").CodepointIterator; const Analytics = @import("./analytics/analytics_thread.zig"); const Fs = @import("./fs.zig"); const URL = @import("./url.zig").URL; const Api = @import("./api/schema.zig").Api; const which = @import("./which.zig").which; const DotEnvFileSuffix = enum { development, production, @"test", }; pub const Loader = struct { map: *Map, allocator: std.mem.Allocator, @".env.local": ?logger.Source = null, @".env.development": ?logger.Source = null, @".env.production": ?logger.Source = null, @".env.test": ?logger.Source = null, @".env.development.local": ?logger.Source = null, @".env.production.local": ?logger.Source = null, @".env.test.local": ?logger.Source = null, @".env": ?logger.Source = null, // only populated with files specified explicitely (e.g. --env-file arg) custom_files_loaded: std.StringArrayHashMap(logger.Source), quiet: bool = false, did_load_process: bool = false, reject_unauthorized: ?bool = null, pub fn iterator(this: *const Loader) Map.HashTable.Iterator { return this.map.iterator(); } pub fn has(this: *const Loader, input: []const u8) bool { const value = this.get(input) orelse return false; if (value.len == 0) return false; return !strings.eqlComptime(value, "\"\"") and !strings.eqlComptime(value, "''") and !strings.eqlComptime(value, "0") and !strings.eqlComptime(value, "false"); } pub fn isProduction(this: *const Loader) bool { const env = this.get("BUN_ENV") orelse this.get("NODE_ENV") orelse return false; return strings.eqlComptime(env, "production"); } pub fn isTest(this: *const Loader) bool { const env = this.get("BUN_ENV") orelse this.get("NODE_ENV") orelse return false; return strings.eqlComptime(env, "test"); } pub fn getNodePath(this: *Loader, fs: *Fs.FileSystem, buf: *bun.PathBuffer) ?[:0]const u8 { if (this.get("NODE") orelse this.get("npm_node_execpath")) |node| { @memcpy(buf[0..node.len], node); buf[node.len] = 0; return buf[0..node.len :0]; } if (which(buf, this.get("PATH") orelse return null, fs.top_level_dir, "node")) |node| { return node; } return null; } pub fn isCI(this: *const Loader) bool { return (this.get("CI") orelse this.get("TDDIUM") orelse this.get("GITHUB_ACTIONS") orelse this.get("JENKINS_URL") orelse this.get("bamboo.buildKey")) != null; } pub fn loadTracy(this: *const Loader) void { tracy: { if (this.get("BUN_TRACY") != null) { if (!bun.tracy.init()) { Output.prettyErrorln("Failed to load Tracy. Is it installed in your include path?", .{}); Output.flush(); break :tracy; } bun.tracy.start(); if (!bun.tracy.isConnected()) { std.time.sleep(std.time.ns_per_ms * 10); } if (!bun.tracy.isConnected()) { Output.prettyErrorln("Tracy is not connected. Is Tracy running on your computer?", .{}); Output.flush(); break :tracy; } } } } pub fn getTLSRejectUnauthorized(this: *Loader) bool { if (this.reject_unauthorized) |reject_unauthorized| { return reject_unauthorized; } if (this.get("NODE_TLS_REJECT_UNAUTHORIZED")) |reject| { if (strings.eql(reject, "0")) { this.reject_unauthorized = false; return false; } if (strings.eql(reject, "false")) { this.reject_unauthorized = false; return false; } } // default: true this.reject_unauthorized = true; return true; } pub fn getHttpProxy(this: *Loader, url: URL) ?URL { // TODO: When Web Worker support is added, make sure to intern these strings var http_proxy: ?URL = null; if (url.isHTTP()) { if (this.get("http_proxy") orelse this.get("HTTP_PROXY")) |proxy| { if (proxy.len > 0 and !strings.eqlComptime(proxy, "\"\"") and !strings.eqlComptime(proxy, "''")) { http_proxy = URL.parse(proxy); } } } else { if (this.get("https_proxy") orelse this.get("HTTPS_PROXY")) |proxy| { if (proxy.len > 0 and !strings.eqlComptime(proxy, "\"\"") and !strings.eqlComptime(proxy, "''")) { http_proxy = URL.parse(proxy); } } } // NO_PROXY filter // See the syntax at https://about.gitlab.com/blog/2021/01/27/we-need-to-talk-no-proxy/ if (http_proxy != null) { if (this.get("no_proxy") orelse this.get("NO_PROXY")) |no_proxy_text| { if (no_proxy_text.len == 0 or strings.eqlComptime(no_proxy_text, "\"\"") or strings.eqlComptime(no_proxy_text, "''")) { return http_proxy; } var no_proxy_list = std.mem.split(u8, no_proxy_text, ","); var next = no_proxy_list.next(); while (next != null) { var host = strings.trim(next.?, &strings.whitespace_chars); if (strings.eql(host, "*")) { return null; } //strips . if (host[0] == '.') { host = host[1..]; } //hostname ends with suffix if (strings.endsWith(url.hostname, host)) { return null; } next = no_proxy_list.next(); } } } return http_proxy; } var did_load_ccache_path: bool = false; pub fn loadCCachePath(this: *Loader, fs: *Fs.FileSystem) void { if (did_load_ccache_path) { return; } did_load_ccache_path = true; loadCCachePathImpl(this, fs) catch {}; } fn loadCCachePathImpl(this: *Loader, fs: *Fs.FileSystem) !void { // if they have ccache installed, put it in env variable `CMAKE_CXX_COMPILER_LAUNCHER` so // cmake can use it to hopefully speed things up var buf: [bun.MAX_PATH_BYTES]u8 = undefined; const ccache_path = bun.which( &buf, this.get("PATH") orelse return, fs.top_level_dir, "ccache", ) orelse ""; if (ccache_path.len > 0) { const cxx_gop = try this.map.getOrPutWithoutValue("CMAKE_CXX_COMPILER_LAUNCHER"); if (!cxx_gop.found_existing) { cxx_gop.key_ptr.* = try this.allocator.dupe(u8, cxx_gop.key_ptr.*); cxx_gop.value_ptr.* = .{ .value = try this.allocator.dupe(u8, ccache_path), .conditional = false, }; } const c_gop = try this.map.getOrPutWithoutValue("CMAKE_C_COMPILER_LAUNCHER"); if (!c_gop.found_existing) { c_gop.key_ptr.* = try this.allocator.dupe(u8, c_gop.key_ptr.*); c_gop.value_ptr.* = .{ .value = try this.allocator.dupe(u8, ccache_path), .conditional = false, }; } } } var node_path_to_use_set_once: []const u8 = ""; pub fn loadNodeJSConfig(this: *Loader, fs: *Fs.FileSystem, override_node: []const u8) !bool { var buf: bun.PathBuffer = undefined; var node_path_to_use = override_node; if (node_path_to_use.len == 0) { if (node_path_to_use_set_once.len > 0) { node_path_to_use = node_path_to_use_set_once; } else { const node = this.getNodePath(fs, &buf) orelse return false; node_path_to_use = try fs.dirname_store.append([]const u8, bun.asByteSlice(node)); } } node_path_to_use_set_once = node_path_to_use; try this.map.put("NODE", node_path_to_use); try this.map.put("npm_node_execpath", node_path_to_use); return true; } pub fn get(this: *const Loader, key: string) ?string { var _key = key; if (_key.len > 0 and _key[0] == '$') { _key = key[1..]; } if (_key.len == 0) return null; return this.map.get(_key); } pub fn getAuto(this: *const Loader, key: string) string { // If it's "" or "$", it's not a variable if (key.len < 2 or key[0] != '$') { return key; } return this.get(key[1..]) orelse key; } /// Load values from the environment into Define. /// /// If there is a framework, values from the framework are inserted with a /// **lower priority** so that users may override defaults. Unlike regular /// defines, environment variables are loaded as JavaScript string literals. /// /// Empty enivronment variables become empty strings. pub fn copyForDefine( this: *Loader, comptime JSONStore: type, to_json: *JSONStore, comptime StringStore: type, to_string: *StringStore, framework_defaults: Api.StringMap, behavior: Api.DotEnvBehavior, prefix: string, allocator: std.mem.Allocator, ) !void { var iter = this.map.iterator(); var key_count: usize = 0; var string_map_hashes = try allocator.alloc(u64, framework_defaults.keys.len); defer allocator.free(string_map_hashes); const invalid_hash = std.math.maxInt(u64) - 1; @memset(string_map_hashes, invalid_hash); var key_buf: []u8 = ""; // Frameworks determine an allowlist of values for (framework_defaults.keys, 0..) |key, i| { if (key.len > "process.env.".len and strings.eqlComptime(key[0.."process.env.".len], "process.env.")) { const hashable_segment = key["process.env.".len..]; string_map_hashes[i] = bun.hash(hashable_segment); } } // We have to copy all the keys to prepend "process.env" :/ var key_buf_len: usize = 0; var e_strings_to_allocate: usize = 0; if (behavior != .disable and behavior != .load_all_without_inlining) { if (behavior == .prefix) { bun.assert(prefix.len > 0); while (iter.next()) |entry| { if (strings.startsWith(entry.key_ptr.*, prefix)) { key_buf_len += entry.key_ptr.len; key_count += 1; e_strings_to_allocate += 1; bun.assert(entry.key_ptr.len > 0); } } } else { while (iter.next()) |entry| { if (entry.key_ptr.len > 0) { key_buf_len += entry.key_ptr.len; key_count += 1; e_strings_to_allocate += 1; bun.assert(entry.key_ptr.len > 0); } } } if (key_buf_len > 0) { iter.reset(); key_buf = try allocator.alloc(u8, key_buf_len + key_count * "process.env.".len); const js_ast = bun.JSAst; var e_strings = try allocator.alloc(js_ast.E.String, e_strings_to_allocate * 2); errdefer allocator.free(e_strings); errdefer allocator.free(key_buf); var key_fixed_allocator = std.heap.FixedBufferAllocator.init(key_buf); const key_allocator = key_fixed_allocator.allocator(); if (behavior == .prefix) { while (iter.next()) |entry| { const value: string = entry.value_ptr.value; if (strings.startsWith(entry.key_ptr.*, prefix)) { const key_str = std.fmt.allocPrint(key_allocator, "process.env.{s}", .{entry.key_ptr.*}) catch unreachable; e_strings[0] = js_ast.E.String{ .data = if (value.len > 0) @as([*]u8, @ptrFromInt(@intFromPtr(value.ptr)))[0..value.len] else &[_]u8{}, }; const expr_data = js_ast.Expr.Data{ .e_string = &e_strings[0] }; _ = try to_string.getOrPutValue( key_str, .{ .can_be_removed_if_unused = true, .call_can_be_unwrapped_if_unused = true, .value = expr_data, }, ); e_strings = e_strings[1..]; } else { const hash = bun.hash(entry.key_ptr.*); bun.assert(hash != invalid_hash); if (std.mem.indexOfScalar(u64, string_map_hashes, hash)) |key_i| { e_strings[0] = js_ast.E.String{ .data = if (value.len > 0) @as([*]u8, @ptrFromInt(@intFromPtr(value.ptr)))[0..value.len] else &[_]u8{}, }; const expr_data = js_ast.Expr.Data{ .e_string = &e_strings[0] }; _ = try to_string.getOrPutValue( framework_defaults.keys[key_i], .{ .can_be_removed_if_unused = true, .call_can_be_unwrapped_if_unused = true, .value = expr_data, }, ); e_strings = e_strings[1..]; } } } } else { while (iter.next()) |entry| { const value: string = entry.value_ptr.value; const key = std.fmt.allocPrint(key_allocator, "process.env.{s}", .{entry.key_ptr.*}) catch unreachable; e_strings[0] = js_ast.E.String{ .data = if (entry.value_ptr.value.len > 0) @as([*]u8, @ptrFromInt(@intFromPtr(entry.value_ptr.value.ptr)))[0..value.len] else &[_]u8{}, }; const expr_data = js_ast.Expr.Data{ .e_string = &e_strings[0] }; _ = try to_string.getOrPutValue( key, .{ .can_be_removed_if_unused = true, .call_can_be_unwrapped_if_unused = true, .value = expr_data, }, ); e_strings = e_strings[1..]; } } } } for (framework_defaults.keys, 0..) |key, i| { const value = framework_defaults.values[i]; if (!to_string.contains(key) and !to_json.contains(key)) { _ = try to_json.getOrPutValue(key, value); } } } pub fn init(map: *Map, allocator: std.mem.Allocator) Loader { return Loader{ .map = map, .allocator = allocator, .custom_files_loaded = std.StringArrayHashMap(logger.Source).init(allocator), }; } pub fn loadProcess(this: *Loader) void { if (this.did_load_process) return; this.map.map.ensureTotalCapacity(std.os.environ.len) catch unreachable; for (std.os.environ) |_env| { var env = bun.span(_env); if (strings.indexOfChar(env, '=')) |i| { const key = env[0..i]; const value = env[i + 1 ..]; if (key.len > 0) { this.map.put(key, value) catch unreachable; } } else { if (env.len > 0) { this.map.put(env, "") catch unreachable; } } } this.did_load_process = true; } // mostly for tests pub fn loadFromString(this: *Loader, str: string, comptime overwrite: bool) void { var source = logger.Source.initPathString("test", str); Parser.parse(&source, this.allocator, this.map, overwrite, false); std.mem.doNotOptimizeAway(&source); } pub fn load( this: *Loader, dir: *Fs.FileSystem.DirEntry, env_files: []const []const u8, comptime suffix: DotEnvFileSuffix, skip_default_env: bool, ) !void { const start = std.time.nanoTimestamp(); if (env_files.len > 0) { try this.loadExplicitFiles(env_files); } else { // Do not automatically load .env files in `bun run