From 23383b32b094dfd476a83ba42cbc8ec48dd9f284 Mon Sep 17 00:00:00 2001 From: robobun Date: Fri, 5 Dec 2025 14:43:53 -0800 Subject: [PATCH] feat(compile): add --compile-autoload-tsconfig and --compile-autoload-package-json flags (#25340) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary By default, standalone executables no longer load `tsconfig.json` and `package.json` at runtime. This improves startup performance and prevents unexpected behavior from config files in the runtime environment. - Added `--compile-autoload-tsconfig` / `--no-compile-autoload-tsconfig` CLI flags (default: false) - Added `--compile-autoload-package-json` / `--no-compile-autoload-package-json` CLI flags (default: false) - Added `autoloadTsconfig` and `autoloadPackageJson` options to the `Bun.build()` compile config - Flags are stored in `StandaloneModuleGraph.Flags` and applied at runtime boot This follows the same pattern as the existing `--compile-autoload-dotenv` and `--compile-autoload-bunfig` flags. ## Test plan - [x] Added tests in `test/bundler/bundler_compile_autoload.test.ts` - [x] Verified standalone executables work correctly with runtime config files that differ from compile-time configs - [x] Verified the new CLI flags are properly parsed and applied - [x] Verified the JS API options work correctly 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Bot Co-authored-by: Claude Co-authored-by: Jarred Sumner --- src/StandaloneModuleGraph.zig | 4 +- src/bun.js.zig | 5 + src/bun.js/api/JSBundler.zig | 10 + src/bundler/bundle_v2.zig | 2 + src/cli.zig | 2 + src/cli/Arguments.zig | 40 +++ src/cli/build_command.zig | 2 + src/options.zig | 1 + src/resolver/resolver.zig | 42 ++-- test/bundler/bundler_compile_autoload.test.ts | 228 ++++++++++++++++++ test/bundler/expectBundled.ts | 6 + 11 files changed, 321 insertions(+), 21 deletions(-) diff --git a/src/StandaloneModuleGraph.zig b/src/StandaloneModuleGraph.zig index df838ae007..6d8c57ed48 100644 --- a/src/StandaloneModuleGraph.zig +++ b/src/StandaloneModuleGraph.zig @@ -296,7 +296,9 @@ pub const StandaloneModuleGraph = struct { pub const Flags = packed struct(u32) { disable_default_env_files: bool = false, disable_autoload_bunfig: bool = false, - _padding: u30 = 0, + disable_autoload_tsconfig: bool = false, + disable_autoload_package_json: bool = false, + _padding: u28 = 0, }; const trailer = "\n---- Bun! ----\n"; diff --git a/src/bun.js.zig b/src/bun.js.zig index 37086368f0..1573c5fdbd 100644 --- a/src/bun.js.zig +++ b/src/bun.js.zig @@ -90,6 +90,11 @@ pub const Run = struct { b.options.env.behavior = .load_all_without_inlining; } + // Control loading of tsconfig.json and package.json at runtime + // By default, these are disabled for standalone executables + b.resolver.opts.load_tsconfig_json = !graph.flags.disable_autoload_tsconfig; + b.resolver.opts.load_package_json = !graph.flags.disable_autoload_package_json; + b.configureDefines() catch { failWithBuildError(vm); }; diff --git a/src/bun.js/api/JSBundler.zig b/src/bun.js/api/JSBundler.zig index 89e9035cd9..83136594fa 100644 --- a/src/bun.js/api/JSBundler.zig +++ b/src/bun.js/api/JSBundler.zig @@ -59,6 +59,8 @@ pub const JSBundler = struct { outfile: OwnedString = OwnedString.initEmpty(bun.default_allocator), autoload_dotenv: bool = true, autoload_bunfig: bool = true, + autoload_tsconfig: bool = false, + autoload_package_json: bool = false, pub fn fromJS(globalThis: *jsc.JSGlobalObject, config: jsc.JSValue, allocator: std.mem.Allocator, compile_target: ?CompileTarget) JSError!?CompileOptions { var this = CompileOptions{ @@ -187,6 +189,14 @@ pub const JSBundler = struct { this.autoload_bunfig = autoload_bunfig; } + if (try object.getBooleanLoose(globalThis, "autoloadTsconfig")) |autoload_tsconfig| { + this.autoload_tsconfig = autoload_tsconfig; + } + + if (try object.getBooleanLoose(globalThis, "autoloadPackageJson")) |autoload_package_json| { + this.autoload_package_json = autoload_package_json; + } + return this; } diff --git a/src/bundler/bundle_v2.zig b/src/bundler/bundle_v2.zig index 57810ddc40..669e84de69 100644 --- a/src/bundler/bundle_v2.zig +++ b/src/bundler/bundle_v2.zig @@ -2045,6 +2045,8 @@ pub const BundleV2 = struct { .{ .disable_default_env_files = !compile_options.autoload_dotenv, .disable_autoload_bunfig = !compile_options.autoload_bunfig, + .disable_autoload_tsconfig = !compile_options.autoload_tsconfig, + .disable_autoload_package_json = !compile_options.autoload_package_json, }, ) catch |err| { return bun.StandaloneModuleGraph.CompileResult.failFmt("{s}", .{@errorName(err)}); diff --git a/src/cli.zig b/src/cli.zig index b536d76556..5aa859a828 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -461,6 +461,8 @@ pub const Command = struct { compile_exec_argv: ?[]const u8 = null, compile_autoload_dotenv: bool = true, compile_autoload_bunfig: bool = true, + compile_autoload_tsconfig: bool = false, + compile_autoload_package_json: bool = false, windows: options.WindowsOptions = .{}, }; diff --git a/src/cli/Arguments.zig b/src/cli/Arguments.zig index 9f163ad617..5d9f8751e2 100644 --- a/src/cli/Arguments.zig +++ b/src/cli/Arguments.zig @@ -153,6 +153,10 @@ pub const build_only_params = [_]ParamType{ 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("--compile-autoload-tsconfig Enable autoloading of tsconfig.json at runtime in standalone executable (default: false)") catch unreachable, + clap.parseParam("--no-compile-autoload-tsconfig Disable autoloading of tsconfig.json at runtime in standalone executable") catch unreachable, + clap.parseParam("--compile-autoload-package-json Enable autoloading of package.json at runtime in standalone executable (default: false)") catch unreachable, + clap.parseParam("--no-compile-autoload-package-json Disable autoloading of package.json at runtime 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, @@ -1063,6 +1067,42 @@ pub fn parse(allocator: std.mem.Allocator, ctx: Command.Context, comptime cmd: C } } + // Handle --compile-autoload-tsconfig flags (default: false, tsconfig not loaded at runtime) + { + const has_positive = args.flag("--compile-autoload-tsconfig"); + const has_negative = args.flag("--no-compile-autoload-tsconfig"); + + if (has_positive or has_negative) { + if (!ctx.bundler_options.compile) { + Output.errGeneric("--compile-autoload-tsconfig requires --compile", .{}); + Global.crash(); + } + if (has_positive and has_negative) { + Output.errGeneric("Cannot use both --compile-autoload-tsconfig and --no-compile-autoload-tsconfig", .{}); + Global.crash(); + } + ctx.bundler_options.compile_autoload_tsconfig = has_positive; + } + } + + // Handle --compile-autoload-package-json flags (default: false, package.json not loaded at runtime) + { + const has_positive = args.flag("--compile-autoload-package-json"); + const has_negative = args.flag("--no-compile-autoload-package-json"); + + if (has_positive or has_negative) { + if (!ctx.bundler_options.compile) { + Output.errGeneric("--compile-autoload-package-json requires --compile", .{}); + Global.crash(); + } + if (has_positive and has_negative) { + Output.errGeneric("Cannot use both --compile-autoload-package-json and --no-compile-autoload-package-json", .{}); + Global.crash(); + } + ctx.bundler_options.compile_autoload_package_json = 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 031fcb9be7..04d19e0c6e 100644 --- a/src/cli/build_command.zig +++ b/src/cli/build_command.zig @@ -443,6 +443,8 @@ pub const BuildCommand = struct { .{ .disable_default_env_files = !ctx.bundler_options.compile_autoload_dotenv, .disable_autoload_bunfig = !ctx.bundler_options.compile_autoload_bunfig, + .disable_autoload_tsconfig = !ctx.bundler_options.compile_autoload_tsconfig, + .disable_autoload_package_json = !ctx.bundler_options.compile_autoload_package_json, }, ) catch |err| { Output.printErrorln("failed to create executable: {s}", .{@errorName(err)}); diff --git a/src/options.zig b/src/options.zig index f131f9ddd9..2d212e686c 100644 --- a/src/options.zig +++ b/src/options.zig @@ -1772,6 +1772,7 @@ pub const BundleOptions = struct { polyfill_node_globals: bool = false, transform_only: bool = false, load_tsconfig_json: bool = true, + load_package_json: bool = true, rewrite_jest_for_tests: bool = false, diff --git a/src/resolver/resolver.zig b/src/resolver/resolver.zig index 53ea6d7ea7..85d22c013c 100644 --- a/src/resolver/resolver.zig +++ b/src/resolver/resolver.zig @@ -4122,30 +4122,32 @@ pub const Resolver = struct { } // Record if this directory has a package.json file - if (entries.getComptimeQuery("package.json")) |lookup| { - const entry = lookup.entry; - if (entry.kind(rfs, r.store_fd) == .file) { - info.package_json = if (r.usePackageManager() and !info.hasNodeModules() and !info.isNodeModules()) - r.parsePackageJSON(path, if (FeatureFlags.store_file_descriptors) fd else .invalid, package_id, true) catch null - else - r.parsePackageJSON(path, if (FeatureFlags.store_file_descriptors) fd else .invalid, null, false) catch null; + if (r.opts.load_package_json) { + if (entries.getComptimeQuery("package.json")) |lookup| { + const entry = lookup.entry; + if (entry.kind(rfs, r.store_fd) == .file) { + info.package_json = if (r.usePackageManager() and !info.hasNodeModules() and !info.isNodeModules()) + r.parsePackageJSON(path, if (FeatureFlags.store_file_descriptors) fd else .invalid, package_id, true) catch null + else + r.parsePackageJSON(path, if (FeatureFlags.store_file_descriptors) fd else .invalid, null, false) catch null; - if (info.package_json) |pkg| { - if (pkg.browser_map.count() > 0) { - info.enclosing_browser_scope = result.index; - info.package_json_for_browser_field = pkg; - } + if (info.package_json) |pkg| { + if (pkg.browser_map.count() > 0) { + info.enclosing_browser_scope = result.index; + info.package_json_for_browser_field = pkg; + } - if (pkg.name.len > 0 or r.care_about_bin_folder) - info.enclosing_package_json = pkg; + if (pkg.name.len > 0 or r.care_about_bin_folder) + info.enclosing_package_json = pkg; - if (pkg.dependencies.map.count() > 0 or pkg.package_manager_package_id != Install.invalid_package_id) - info.package_json_for_dependencies = pkg; + if (pkg.dependencies.map.count() > 0 or pkg.package_manager_package_id != Install.invalid_package_id) + info.package_json_for_dependencies = pkg; - if (r.debug_logs) |*logs| { - logs.addNoteFmt("Resolved package.json in \"{s}\"", .{ - path, - }); + if (r.debug_logs) |*logs| { + logs.addNoteFmt("Resolved package.json in \"{s}\"", .{ + path, + }); + } } } } diff --git a/test/bundler/bundler_compile_autoload.test.ts b/test/bundler/bundler_compile_autoload.test.ts index be4ca6e119..e494023706 100644 --- a/test/bundler/bundler_compile_autoload.test.ts +++ b/test/bundler/bundler_compile_autoload.test.ts @@ -264,4 +264,232 @@ console.log("PRELOAD"); setCwd: true, }, }); + + // Test that tsconfig.json paths are loaded at runtime when autoloadTsconfig: true + // Uses a dynamic import path that the bundler cannot resolve at compile time + itBundled("compile/AutoloadTsconfigPathsEnabled", { + compile: { + autoloadTsconfig: true, + }, + files: { + "/entry.ts": /* ts */ ` + // Use a dynamic path that can't be resolved at compile time + // This forces runtime resolution using the runtime tsconfig.json + const modulePath = "@utils/" + "helper"; + import(modulePath) + .then(m => console.log(m.default)) + .catch(e => console.log("import-failed: " + e.message)); + `, + }, + runtimeFiles: { + "/tsconfig.json": JSON.stringify({ + compilerOptions: { + baseUrl: ".", + paths: { + "@utils/*": ["./src/utils/*"], + }, + }, + }), + "/src/utils/helper.ts": `export default "helper-from-tsconfig-paths";`, + }, + run: { + stdout: "helper-from-tsconfig-paths", + setCwd: true, + }, + }); + + // Test that tsconfig.json paths are NOT loaded when autoloadTsconfig: false (default) + // The import should fail because @utils/helper cannot be resolved without tsconfig paths + itBundled("compile/AutoloadTsconfigPathsDisabled", { + compile: { + autoloadTsconfig: false, + }, + files: { + "/entry.ts": /* ts */ ` + // Without runtime tsconfig.json, @utils/helper cannot be resolved + const modulePath = "@utils/" + "helper"; + import(modulePath) + .then(m => console.log(m.default)) + .catch(() => console.log("import-failed-as-expected")); + `, + }, + runtimeFiles: { + "/tsconfig.json": JSON.stringify({ + compilerOptions: { + baseUrl: ".", + paths: { + "@utils/*": ["./src/utils/*"], + }, + }, + }), + "/src/utils/helper.ts": `export default "helper-from-tsconfig-paths";`, + }, + run: { + stdout: "import-failed-as-expected", + setCwd: true, + }, + }); + + // Test that package.json exports are loaded at runtime when autoloadPackageJson: true + // Uses a dynamic import path that the bundler cannot resolve at compile time + itBundled("compile/AutoloadPackageJsonExportsEnabled", { + compile: { + autoloadPackageJson: true, + }, + files: { + "/entry.js": /* js */ ` + // Use a dynamic path that can't be resolved at compile time + const pkgName = "my-runtime-pkg"; + const subpath = "utils"; + import(pkgName + "/" + subpath) + .then(m => console.log(m.default)) + .catch(e => console.log("import-failed: " + e.message)); + `, + }, + runtimeFiles: { + "/node_modules/my-runtime-pkg/package.json": JSON.stringify({ + name: "my-runtime-pkg", + exports: { + "./utils": "./lib/utilities.js", + }, + }), + "/node_modules/my-runtime-pkg/lib/utilities.js": `export default "utilities-from-package-exports";`, + }, + run: { + stdout: "utilities-from-package-exports", + setCwd: true, + }, + }); + + // Test that package.json exports are NOT loaded when autoloadPackageJson: false (default) + // The import should fail because my-runtime-pkg/utils cannot be resolved without package.json exports + itBundled("compile/AutoloadPackageJsonExportsDisabled", { + compile: { + autoloadPackageJson: false, + }, + files: { + "/entry.js": /* js */ ` + // Without runtime package.json, my-runtime-pkg/utils cannot be resolved + const pkgName = "my-runtime-pkg"; + const subpath = "utils"; + import(pkgName + "/" + subpath) + .then(m => console.log(m.default)) + .catch(() => console.log("import-failed-as-expected")); + `, + }, + runtimeFiles: { + "/node_modules/my-runtime-pkg/package.json": JSON.stringify({ + name: "my-runtime-pkg", + exports: { + "./utils": "./lib/utilities.js", + }, + }), + "/node_modules/my-runtime-pkg/lib/utilities.js": `export default "utilities-from-package-exports";`, + }, + run: { + stdout: "import-failed-as-expected", + setCwd: true, + }, + }); + + // Test CLI backend with autoloadTsconfig: true using tsconfig paths + itBundled("compile/AutoloadTsconfigPathsCLI", { + compile: { + autoloadTsconfig: true, + }, + backend: "cli", + files: { + "/entry.ts": /* ts */ ` + const modulePath = "@lib/" + "mymodule"; + import(modulePath) + .then(m => console.log(m.default)) + .catch(e => console.log("import-failed: " + e.message)); + `, + }, + runtimeFiles: { + "/tsconfig.json": JSON.stringify({ + compilerOptions: { + baseUrl: ".", + paths: { + "@lib/*": ["./lib/*"], + }, + }, + }), + "/lib/mymodule.ts": `export default "mymodule-from-cli-tsconfig";`, + }, + run: { + stdout: "mymodule-from-cli-tsconfig", + setCwd: true, + }, + }); + + // Test CLI backend with autoloadPackageJson: true using package.json exports + itBundled("compile/AutoloadPackageJsonExportsCLI", { + compile: { + autoloadPackageJson: true, + }, + backend: "cli", + files: { + "/entry.js": /* js */ ` + const pkgName = "cli-pkg"; + const subpath = "feature"; + import(pkgName + "/" + subpath) + .then(m => console.log(m.default)) + .catch(e => console.log("import-failed: " + e.message)); + `, + }, + runtimeFiles: { + "/node_modules/cli-pkg/package.json": JSON.stringify({ + name: "cli-pkg", + exports: { + "./feature": "./features/main.js", + }, + }), + "/node_modules/cli-pkg/features/main.js": `export default "feature-from-cli-package-exports";`, + }, + run: { + stdout: "feature-from-cli-package-exports", + setCwd: true, + }, + }); + + // Test that both tsconfig and package.json can be enabled together + itBundled("compile/AutoloadBothTsconfigAndPackageJson", { + compile: { + autoloadTsconfig: true, + autoloadPackageJson: true, + }, + files: { + "/entry.ts": /* ts */ ` + // Both imports require runtime config files + const tsconfigPath = "@utils/" + "helper"; + const pkgPath = "runtime-pkg/" + "utils"; + Promise.all([import(tsconfigPath), import(pkgPath)]) + .then(([helper, utils]) => console.log(helper.default + " " + utils.default)) + .catch(e => console.log("import-failed: " + e.message)); + `, + }, + runtimeFiles: { + "/tsconfig.json": JSON.stringify({ + compilerOptions: { + baseUrl: ".", + paths: { + "@utils/*": ["./src/utils/*"], + }, + }, + }), + "/src/utils/helper.ts": `export default "tsconfig-helper";`, + "/node_modules/runtime-pkg/package.json": JSON.stringify({ + name: "runtime-pkg", + exports: { + "./utils": "./lib/utils.js", + }, + }), + "/node_modules/runtime-pkg/lib/utils.js": `export default "package-utils";`, + }, + run: { + stdout: "tsconfig-helper package-utils", + setCwd: true, + }, + }); }); diff --git a/test/bundler/expectBundled.ts b/test/bundler/expectBundled.ts index 66bd3b6894..9bf60bacff 100644 --- a/test/bundler/expectBundled.ts +++ b/test/bundler/expectBundled.ts @@ -731,6 +731,12 @@ function expectBundled( : [], compileFlag("autoloadDotenv", "--compile-autoload-dotenv", "--no-compile-autoload-dotenv"), compileFlag("autoloadBunfig", "--compile-autoload-bunfig", "--no-compile-autoload-bunfig"), + compileFlag("autoloadTsconfig", "--compile-autoload-tsconfig", "--no-compile-autoload-tsconfig"), + compileFlag( + "autoloadPackageJson", + "--compile-autoload-package-json", + "--no-compile-autoload-package-json", + ), outfile ? `--outfile=${outfile}` : `--outdir=${outdir}`, define && Object.entries(define).map(([k, v]) => ["--define", `${k}=${v}`]), `--target=${target}`,