diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index 57f6d82965..7937b4976c 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -1933,6 +1933,26 @@ declare module "bun" { execArgv?: string[]; executablePath?: string; outfile?: string; + /** + * Whether to autoload .env files when the standalone executable runs + * + * Standalone-only: applies only when building/running the standalone executable. + * + * Equivalent CLI flags: `--compile-autoload-dotenv`, `--no-compile-autoload-dotenv` + * + * @default true + */ + autoloadDotenv?: boolean; + /** + * Whether to autoload bunfig.toml when the standalone executable runs + * + * Standalone-only: applies only when building/running the standalone executable. + * + * Equivalent CLI flags: `--compile-autoload-bunfig`, `--no-compile-autoload-bunfig` + * + * @default true + */ + autoloadBunfig?: boolean; windows?: { hideConsole?: boolean; icon?: string; diff --git a/src/StandaloneModuleGraph.zig b/src/StandaloneModuleGraph.zig index 823ec33974..b51a807d3a 100644 --- a/src/StandaloneModuleGraph.zig +++ b/src/StandaloneModuleGraph.zig @@ -7,6 +7,7 @@ pub const StandaloneModuleGraph = struct { files: bun.StringArrayHashMap(File), entry_point_id: u32 = 0, compile_exec_argv: []const u8 = "", + flags: Flags = .{}, // We never want to hit the filesystem for these files // We use the `/$bunfs/` prefix to indicate that it's a virtual path @@ -289,6 +290,13 @@ pub const StandaloneModuleGraph = struct { modules_ptr: bun.StringPointer = .{}, entry_point_id: u32 = 0, compile_exec_argv_ptr: bun.StringPointer = .{}, + flags: Flags = .{}, + }; + + pub const Flags = packed struct(u32) { + disable_default_env_files: bool = false, + disable_autoload_bunfig: bool = false, + _padding: u30 = 0, }; const trailer = "\n---- Bun! ----\n"; @@ -334,6 +342,7 @@ pub const StandaloneModuleGraph = struct { .files = modules, .entry_point_id = offsets.entry_point_id, .compile_exec_argv = sliceToZ(raw_bytes, offsets.compile_exec_argv_ptr), + .flags = offsets.flags, }; } @@ -349,7 +358,7 @@ pub const StandaloneModuleGraph = struct { return bytes[ptr.offset..][0..ptr.length :0]; } - pub fn toBytes(allocator: std.mem.Allocator, prefix: []const u8, output_files: []const bun.options.OutputFile, output_format: bun.options.Format, compile_exec_argv: []const u8) ![]u8 { + pub fn toBytes(allocator: std.mem.Allocator, prefix: []const u8, output_files: []const bun.options.OutputFile, output_format: bun.options.Format, compile_exec_argv: []const u8, flags: Flags) ![]u8 { var serialize_trace = bun.perf.trace("StandaloneModuleGraph.serialize"); defer serialize_trace.end(); @@ -498,6 +507,7 @@ pub const StandaloneModuleGraph = struct { .modules_ptr = string_builder.appendCount(std.mem.sliceAsBytes(modules.items)), .compile_exec_argv_ptr = string_builder.appendCountZ(compile_exec_argv), .byte_count = string_builder.len, + .flags = flags, }; _ = string_builder.append(std.mem.asBytes(&offsets)); @@ -979,8 +989,9 @@ pub const StandaloneModuleGraph = struct { windows_options: bun.options.WindowsOptions, compile_exec_argv: []const u8, self_exe_path: ?[]const u8, + flags: Flags, ) !CompileResult { - const bytes = toBytes(allocator, module_prefix, output_files, output_format, compile_exec_argv) catch |err| { + const bytes = toBytes(allocator, module_prefix, output_files, output_format, compile_exec_argv, flags) catch |err| { return CompileResult.failFmt("failed to generate module graph bytes: {s}", .{@errorName(err)}); }; if (bytes.len == 0) return CompileResult.fail(.no_output_files); diff --git a/src/bun.js.zig b/src/bun.js.zig index 1c79e41f64..84ab5016b1 100644 --- a/src/bun.js.zig +++ b/src/bun.js.zig @@ -26,7 +26,8 @@ pub const Run = struct { js_ast.Stmt.Data.Store.create(); const arena = Arena.init(); - if (!ctx.debug.loaded_bunfig) { + // Load bunfig.toml unless disabled by compile flags + if (!ctx.debug.loaded_bunfig and !graph.flags.disable_autoload_bunfig) { try bun.cli.Arguments.loadConfigPath(ctx.allocator, true, "bunfig.toml", ctx, .RunCommand); } @@ -81,7 +82,13 @@ pub const Run = struct { .unspecified => {}, } - b.options.env.behavior = .load_all_without_inlining; + // If .env loading is disabled, only load process env vars + // Otherwise, load all .env files + if (graph.flags.disable_default_env_files) { + b.options.env.behavior = .disable; + } else { + b.options.env.behavior = .load_all_without_inlining; + } b.configureDefines() catch { failWithBuildError(vm); diff --git a/src/bun.js/api/JSBundler.zig b/src/bun.js/api/JSBundler.zig index 0b549f9a5d..dfbf1adc9e 100644 --- a/src/bun.js/api/JSBundler.zig +++ b/src/bun.js/api/JSBundler.zig @@ -57,6 +57,8 @@ pub const JSBundler = struct { windows_description: OwnedString = OwnedString.initEmpty(bun.default_allocator), windows_copyright: OwnedString = OwnedString.initEmpty(bun.default_allocator), outfile: OwnedString = OwnedString.initEmpty(bun.default_allocator), + autoload_dotenv: bool = true, + autoload_bunfig: bool = true, pub fn fromJS(globalThis: *jsc.JSGlobalObject, config: jsc.JSValue, allocator: std.mem.Allocator, compile_target: ?CompileTarget) JSError!?CompileOptions { var this = CompileOptions{ @@ -177,6 +179,14 @@ pub const JSBundler = struct { try this.outfile.appendSliceExact(slice.slice()); } + if (try object.getBooleanLoose(globalThis, "autoloadDotenv")) |autoload_dotenv| { + this.autoload_dotenv = autoload_dotenv; + } + + if (try object.getBooleanLoose(globalThis, "autoloadBunfig")) |autoload_bunfig| { + this.autoload_bunfig = autoload_bunfig; + } + return this; } diff --git a/src/bundler/bundle_v2.zig b/src/bundler/bundle_v2.zig index b83e7121fb..76c35c3430 100644 --- a/src/bundler/bundle_v2.zig +++ b/src/bundler/bundle_v2.zig @@ -2042,6 +2042,10 @@ pub const BundleV2 = struct { compile_options.executable_path.slice() else null, + .{ + .disable_default_env_files = !compile_options.autoload_dotenv, + .disable_autoload_bunfig = !compile_options.autoload_bunfig, + }, ) catch |err| { return bun.StandaloneModuleGraph.CompileResult.failFmt("{s}", .{@errorName(err)}); }; diff --git a/src/cli.zig b/src/cli.zig index 245925dc34..63b605339c 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -458,6 +458,8 @@ pub const Command = struct { compile: bool = false, compile_target: Cli.CompileTarget = .{}, compile_exec_argv: ?[]const u8 = null, + compile_autoload_dotenv: bool = true, + compile_autoload_bunfig: bool = true, windows: options.WindowsOptions = .{}, }; diff --git a/src/cli/Arguments.zig b/src/cli/Arguments.zig index efc50819a5..9f163ad617 100644 --- a/src/cli/Arguments.zig +++ b/src/cli/Arguments.zig @@ -149,6 +149,10 @@ pub const build_only_params = [_]ParamType{ clap.parseParam("--production Set NODE_ENV=production and enable minification") catch unreachable, clap.parseParam("--compile Generate a standalone Bun executable containing your bundled code. Implies --production") catch unreachable, clap.parseParam("--compile-exec-argv Prepend arguments to the standalone executable's execArgv") catch unreachable, + clap.parseParam("--compile-autoload-dotenv Enable autoloading of .env files in standalone executable (default: true)") catch unreachable, + clap.parseParam("--no-compile-autoload-dotenv Disable autoloading of .env files in standalone executable") catch unreachable, + clap.parseParam("--compile-autoload-bunfig Enable autoloading of bunfig.toml in standalone executable (default: true)") catch unreachable, + clap.parseParam("--no-compile-autoload-bunfig Disable autoloading of bunfig.toml in standalone executable") catch unreachable, clap.parseParam("--bytecode Use a bytecode cache") catch unreachable, clap.parseParam("--watch Automatically restart the process on file change") catch unreachable, clap.parseParam("--no-clear-screen Disable clearing the terminal screen on reload when --watch is enabled") catch unreachable, @@ -1023,6 +1027,42 @@ pub fn parse(allocator: std.mem.Allocator, ctx: Command.Context, comptime cmd: C ctx.bundler_options.compile_exec_argv = compile_exec_argv; } + // Handle --compile-autoload-dotenv flags + { + const has_positive = args.flag("--compile-autoload-dotenv"); + const has_negative = args.flag("--no-compile-autoload-dotenv"); + + if (has_positive or has_negative) { + if (!ctx.bundler_options.compile) { + Output.errGeneric("--compile-autoload-dotenv requires --compile", .{}); + Global.crash(); + } + if (has_positive and has_negative) { + Output.errGeneric("Cannot use both --compile-autoload-dotenv and --no-compile-autoload-dotenv", .{}); + Global.crash(); + } + ctx.bundler_options.compile_autoload_dotenv = has_positive; + } + } + + // Handle --compile-autoload-bunfig flags + { + const has_positive = args.flag("--compile-autoload-bunfig"); + const has_negative = args.flag("--no-compile-autoload-bunfig"); + + if (has_positive or has_negative) { + if (!ctx.bundler_options.compile) { + Output.errGeneric("--compile-autoload-bunfig requires --compile", .{}); + Global.crash(); + } + if (has_positive and has_negative) { + Output.errGeneric("Cannot use both --compile-autoload-bunfig and --no-compile-autoload-bunfig", .{}); + Global.crash(); + } + ctx.bundler_options.compile_autoload_bunfig = has_positive; + } + } + if (args.flag("--windows-hide-console")) { // --windows-hide-console technically doesnt depend on WinAPI, but since since --windows-icon // does, all of these customization options have been gated to windows-only diff --git a/src/cli/build_command.zig b/src/cli/build_command.zig index 8aa3e780f0..031fcb9be7 100644 --- a/src/cli/build_command.zig +++ b/src/cli/build_command.zig @@ -440,6 +440,10 @@ pub const BuildCommand = struct { ctx.bundler_options.windows, ctx.bundler_options.compile_exec_argv orelse "", null, + .{ + .disable_default_env_files = !ctx.bundler_options.compile_autoload_dotenv, + .disable_autoload_bunfig = !ctx.bundler_options.compile_autoload_bunfig, + }, ) catch |err| { Output.printErrorln("failed to create executable: {s}", .{@errorName(err)}); Global.exit(1); diff --git a/test/bundler/bundler_compile_autoload.test.ts b/test/bundler/bundler_compile_autoload.test.ts new file mode 100644 index 0000000000..be4ca6e119 --- /dev/null +++ b/test/bundler/bundler_compile_autoload.test.ts @@ -0,0 +1,267 @@ +import { describe } from "bun:test"; +import { itBundled } from "./expectBundled"; + +describe("bundler", () => { + // Test that .env files are loaded by default in standalone executables + itBundled("compile/AutoloadDotenvDefault", { + compile: true, + files: { + "/entry.ts": /* js */ ` + console.log(process.env.TEST_VAR || "not found"); + `, + }, + runtimeFiles: { + "/.env": `TEST_VAR=from_dotenv`, + }, + run: { + stdout: "from_dotenv", + setCwd: true, + }, + }); + + // Test that .env files can be disabled with autoloadDotenv: false + itBundled("compile/AutoloadDotenvDisabled", { + compile: { + autoloadDotenv: false, + }, + files: { + "/entry.ts": /* js */ ` + console.log(process.env.TEST_VAR || "not found"); + `, + }, + runtimeFiles: { + "/.env": `TEST_VAR=from_dotenv`, + }, + run: { + stdout: "not found", + setCwd: true, + }, + }); + + // Test that .env files can be explicitly enabled with autoloadDotenv: true + itBundled("compile/AutoloadDotenvEnabledExplicitly", { + compile: { + autoloadDotenv: true, + }, + files: { + "/entry.ts": /* js */ ` + console.log(process.env.TEST_VAR || "not found"); + `, + }, + runtimeFiles: { + "/.env": `TEST_VAR=from_dotenv`, + }, + run: { + stdout: "from_dotenv", + setCwd: true, + }, + }); + + // Test that process environment variables take precedence over .env files + itBundled("compile/AutoloadDotenvWithExistingEnv", { + compile: true, + files: { + "/entry.ts": /* js */ ` + console.log(process.env.TEST_VAR || "not found"); + `, + }, + runtimeFiles: { + "/.env": `TEST_VAR=from_dotenv`, + }, + run: { + stdout: "from_shell", + setCwd: true, + env: { + TEST_VAR: "from_shell", + }, + }, + }); + + // Test that bunfig.toml is loaded by default (preload is executed) + itBundled("compile/AutoloadBunfigDefault", { + compile: true, + files: { + "/entry.ts": /* js */ ` + console.log("ENTRY"); + `, + }, + runtimeFiles: { + "/bunfig.toml": ` +preload = ["./preload.ts"] + `, + "/preload.ts": ` +console.log("PRELOAD"); + `, + }, + run: { + stdout: "PRELOAD\nENTRY", + setCwd: true, + }, + }); + + // Test that bunfig.toml can be disabled with autoloadBunfig: false + itBundled("compile/AutoloadBunfigDisabled", { + compile: { + autoloadBunfig: false, + }, + files: { + "/entry.ts": /* js */ ` + console.log("ENTRY"); + `, + }, + runtimeFiles: { + "/bunfig.toml": ` +preload = ["./preload.ts"] + `, + "/preload.ts": ` +console.log("PRELOAD"); + `, + }, + run: { + // When bunfig is disabled, preload should NOT execute + stdout: "ENTRY", + setCwd: true, + }, + }); + + // Test that bunfig.toml can be explicitly enabled with autoloadBunfig: true + itBundled("compile/AutoloadBunfigEnabled", { + compile: { + autoloadBunfig: true, + }, + files: { + "/entry.ts": /* js */ ` + console.log("ENTRY"); + `, + }, + runtimeFiles: { + "/bunfig.toml": ` +preload = ["./preload.ts"] + `, + "/preload.ts": ` +console.log("PRELOAD"); + `, + }, + run: { + stdout: "PRELOAD\nENTRY", + setCwd: true, + }, + }); + + // Test CLI backend with autoloadDotenv: false + itBundled("compile/AutoloadDotenvDisabledCLI", { + compile: { + autoloadDotenv: false, + }, + backend: "cli", + files: { + "/entry.ts": /* js */ ` + console.log(process.env.TEST_VAR || "not found"); + `, + }, + runtimeFiles: { + "/.env": `TEST_VAR=from_dotenv`, + }, + run: { + stdout: "not found", + setCwd: true, + }, + }); + + // Test CLI backend with autoloadDotenv: true + itBundled("compile/AutoloadDotenvEnabledCLI", { + compile: { + autoloadDotenv: true, + }, + backend: "cli", + files: { + "/entry.ts": /* js */ ` + console.log(process.env.TEST_VAR || "not found"); + `, + }, + runtimeFiles: { + "/.env": `TEST_VAR=from_dotenv`, + }, + run: { + stdout: "from_dotenv", + setCwd: true, + }, + }); + + // Test CLI backend with autoloadBunfig: false + itBundled("compile/AutoloadBunfigDisabledCLI", { + compile: { + autoloadBunfig: false, + }, + backend: "cli", + files: { + "/entry.ts": /* js */ ` + console.log("ENTRY"); + `, + }, + runtimeFiles: { + "/bunfig.toml": ` +preload = ["./preload.ts"] + `, + "/preload.ts": ` +console.log("PRELOAD"); + `, + }, + run: { + stdout: "ENTRY", + setCwd: true, + }, + }); + + // Test CLI backend with autoloadBunfig: true + itBundled("compile/AutoloadBunfigEnabledCLI", { + compile: { + autoloadBunfig: true, + }, + backend: "cli", + files: { + "/entry.ts": /* js */ ` + console.log("ENTRY"); + `, + }, + runtimeFiles: { + "/bunfig.toml": ` +preload = ["./preload.ts"] + `, + "/preload.ts": ` +console.log("PRELOAD"); + `, + }, + run: { + stdout: "PRELOAD\nENTRY", + setCwd: true, + }, + }); + + // Test that both flags can be disabled together without interference + itBundled("compile/AutoloadBothDisabled", { + compile: { + autoloadDotenv: false, + autoloadBunfig: false, + }, + files: { + "/entry.ts": /* js */ ` + console.log(process.env.TEST_VAR || "not found"); + console.log("ENTRY"); + `, + }, + runtimeFiles: { + "/.env": `TEST_VAR=from_dotenv`, + "/bunfig.toml": ` +preload = ["./preload.ts"] + `, + "/preload.ts": ` +console.log("PRELOAD"); + `, + }, + run: { + stdout: "not found\nENTRY", + setCwd: true, + }, + }); +}); diff --git a/test/bundler/expectBundled.ts b/test/bundler/expectBundled.ts index 4379263dbc..66bd3b6894 100644 --- a/test/bundler/expectBundled.ts +++ b/test/bundler/expectBundled.ts @@ -702,6 +702,16 @@ function expectBundled( ? Object.entries(bundleErrors).flatMap(([file, v]) => v.map(error => ({ file, error }))) : null; + // Helper to add compile boolean flags + const compileFlag = (prop: string, trueFlag: string, falseFlag: string): string[] => { + if (compile && typeof compile === "object" && Object.prototype.hasOwnProperty.call(compile, prop)) { + const value = (compile as any)[prop]; + if (value === true) return [trueFlag]; + if (value === false) return [falseFlag]; + } + return []; + }; + if (backend === "cli") { if (plugins) { throw new Error("plugins not possible in backend=CLI"); @@ -719,6 +729,8 @@ function expectBundled( compile && typeof compile === "object" && "execArgv" in compile ? `--compile-exec-argv=${Array.isArray(compile.execArgv) ? compile.execArgv.join(" ") : compile.execArgv}` : [], + compileFlag("autoloadDotenv", "--compile-autoload-dotenv", "--no-compile-autoload-dotenv"), + compileFlag("autoloadBunfig", "--compile-autoload-bunfig", "--no-compile-autoload-bunfig"), outfile ? `--outfile=${outfile}` : `--outdir=${outdir}`, define && Object.entries(define).map(([k, v]) => ["--define", `${k}=${v}`]), `--target=${target}`, @@ -1058,6 +1070,12 @@ function expectBundled( target: compile, outfile: outfile, }; + } else if (typeof compile === "object") { + // When compile is already an object, ensure it has outfile set + compile = { + ...compile, + outfile: outfile, + }; } }