diff --git a/src/bunfig/arguments.rs b/src/bunfig/arguments.rs index d0a388d3ef7c..87c4f04e76e5 100644 --- a/src/bunfig/arguments.rs +++ b/src/bunfig/arguments.rs @@ -17,24 +17,40 @@ use crate::bunfig::Bunfig; // ─── bunfig loading ────────────────────────────────────────────────────────── -fn get_home_config_path(buf: &mut PathBuffer) -> Option<&ZStr> { - let paths: [&[u8]; 1] = [b".bunfig.toml"]; - - if let Some(data_dir) = env_var::XDG_CONFIG_HOME.get() { - return Some(resolve_path::join_abs_string_buf_z::( - data_dir, &mut **buf, &paths, - )); +/// Resolve `$XDG_CONFIG_HOME/` when it exists, otherwise +/// `$HOME/` (`$USERPROFILE` on Windows). Used for both the user-level +/// `.bunfig.toml` and `.npmrc`; `None` when neither env var is set. +pub fn home_config_path<'a>(buf: &'a mut PathBuffer, name: &[u8]) -> Option<&'a ZStr> { + let parts: [&[u8]; 1] = [name]; + + if let Some(data_dir) = env_var::XDG_CONFIG_HOME.get_not_empty() { + let len = { + let path = + resolve_path::join_abs_string_buf_z::(data_dir, &mut **buf, &parts); + if bun_sys::exists_z(path) { + path.len() + } else { + 0 + } + }; + if len > 0 { + return Some(ZStr::from_buf(&buf[..], len)); + } } - if let Some(home_dir) = env_var::HOME.get() { + if let Some(home_dir) = env_var::HOME.get_not_empty() { return Some(resolve_path::join_abs_string_buf_z::( - home_dir, &mut **buf, &paths, + home_dir, &mut **buf, &parts, )); } None } +fn get_home_config_path(buf: &mut PathBuffer) -> Option<&ZStr> { + home_config_path(buf, b".bunfig.toml") +} + fn load_bunfig( cmd: CommandTag, auto_loaded: bool, @@ -76,7 +92,6 @@ fn load_bunfig( // SAFETY: same as above; runs on the same thread. unsafe { (*log_ptr).level = lvl }; }); - ctx.debug.loaded_bunfig = true; Bunfig::parse(cmd, &source, ctx) } @@ -86,6 +101,13 @@ fn load_global_bunfig(cmd: CommandTag, ctx: Context<'_>) -> Result<(), crate::Er } ctx.has_loaded_global_config = true; + // A compiled standalone executable never reads the end user's + // `~/.bunfig.toml`; the `autoloadBunfig` compile flag only opts into + // the cwd-local `bunfig.toml`. + if StandaloneModuleGraph::get().is_some() { + return Ok(()); + } + let mut config_buf = PathBuffer::uninit(); if let Some(path) = get_home_config_path(&mut config_buf) { load_bunfig(cmd, true, path, ctx)?; @@ -105,19 +127,15 @@ pub fn load_config_path( // lookup so the dead arm is still a single branch. if cmd.read_global_config() { if let Err(err) = load_global_bunfig(cmd, ctx) { - if auto_loaded { - return Ok(()); - } - - bun_core::pretty_errorln!( - "{}\nreading global config \"{}\"", - err, - BStr::new(config_path.as_bytes()), - ); - Global::exit(1); + // A malformed global config is reported the same way `load_config` + // would; swallowing it here would also skip the local load below. + report_bunfig_load_failure(ctx.log, err); } } + // `loaded_bunfig` tracks whether the local-config load has been attempted + // so the `run_command.rs`/`repl_command.rs` fallbacks don't repeat it. + ctx.debug.loaded_bunfig = true; load_bunfig(cmd, auto_loaded, config_path, ctx) } @@ -153,14 +171,8 @@ pub fn load_config( let mut config_buf = PathBuffer::uninit(); if cmd.read_global_config() { - if !ctx.has_loaded_global_config { - ctx.has_loaded_global_config = true; - - if let Some(path) = get_home_config_path(&mut config_buf) { - if let Err(err) = load_config_path(cmd, true, path, ctx) { - report_bunfig_load_failure(ctx.log, err); - } - } + if let Err(err) = load_global_bunfig(cmd, ctx) { + report_bunfig_load_failure(ctx.log, err); } } diff --git a/src/bunfig/bunfig.rs b/src/bunfig/bunfig.rs index e7636ca1c4eb..342abfc6433a 100644 --- a/src/bunfig/bunfig.rs +++ b/src/bunfig/bunfig.rs @@ -380,7 +380,10 @@ impl<'a> Parser<'a> { self.load_env_config(&env_expr)?; } - if cmd == CommandTag::RunCommand || cmd == CommandTag::AutoCommand { + if matches!( + cmd, + CommandTag::RunCommand | CommandTag::AutoCommand | CommandTag::RunAsNodeCommand + ) { if let Some(expr) = json.get(b"serve") { if let Some(port) = expr.get(b"port") { self.expect(&port, ExprTag::ENumber)?; @@ -401,9 +404,7 @@ impl<'a> Parser<'a> { bun_analytics::TriState::No }); } - } - if cmd == CommandTag::RunCommand || cmd == CommandTag::AutoCommand { if let Some(expr) = json.get(b"smol") { self.expect(&expr, ExprTag::EBoolean)?; self.ctx.runtime_options.smol = expr.as_bool().expect("infallible: type checked"); @@ -564,20 +565,22 @@ impl<'a> Parser<'a> { if let Some(expr) = test_.get(b"rerunEach") { self.expect(&expr, ExprTag::ENumber)?; - if self.ctx.test_options.retry != 0 { + if test_.get(b"retry").is_some() { self.add_error(expr.loc, b"\"rerunEach\" cannot be used with \"retry\"")?; return Ok(()); } + self.ctx.test_options.retry = 0; self.ctx.test_options.repeat_count = num_to_u32(expr.as_number().expect("infallible: type checked")); } if let Some(expr) = test_.get(b"retry") { self.expect(&expr, ExprTag::ENumber)?; - if self.ctx.test_options.repeat_count != 0 { + if test_.get(b"rerunEach").is_some() { self.add_error(expr.loc, b"\"retry\" cannot be used with \"rerunEach\"")?; return Ok(()); } + self.ctx.test_options.repeat_count = 0; self.ctx.test_options.retry = num_to_u32(expr.as_number().expect("infallible: type checked")); } @@ -715,6 +718,7 @@ impl<'a> Parser<'a> { if cmd.is_npm_related() || cmd == CommandTag::RunCommand || cmd == CommandTag::AutoCommand + || cmd == CommandTag::RunAsNodeCommand || cmd == CommandTag::TestCommand { if let Some(install_obj) = json.get_object(b"install") { @@ -987,6 +991,7 @@ impl<'a> Parser<'a> { jsx_factory = Box::<[u8]>::from(value); } } + let jsx_present = json.get(b"jsx").is_some(); { if let Some(jsx) = self.ctx.args.jsx.as_mut() { if !jsx_factory.is_empty() { @@ -998,8 +1003,10 @@ impl<'a> Parser<'a> { if !jsx_import_source.is_empty() { jsx.import_source = jsx_import_source; } - jsx.runtime = jsx_runtime; - jsx.development = jsx_dev; + if jsx_present { + jsx.runtime = jsx_runtime; + jsx.development = jsx_dev; + } } else { self.ctx.args.jsx = Some(api::Jsx { factory: jsx_factory, @@ -1583,6 +1590,7 @@ impl<'a> Parser<'a> { }; // TODO: accept entire config object. self.ctx.args.serve_plugins = plugins; + self.ctx.args.bunfig_path = Box::<[u8]>::from(self.source.path.text); } if let Some(hmr) = serve_obj.get(b"hmr") { @@ -1616,7 +1624,6 @@ impl<'a> Parser<'a> { if let Some(expr) = serve_obj.get(b"define") { self.ctx.args.serve_define = Some(self.parse_define_map(&expr)?); } - self.ctx.args.bunfig_path = Box::<[u8]>::from(self.source.path.text); if let Some(public_path) = serve_obj.get(b"publicPath") { if let Some(v) = public_path.as_string(self.bump) { diff --git a/src/bunfig/lib.rs b/src/bunfig/lib.rs index 224bba7f64ad..553ce6becaa1 100644 --- a/src/bunfig/lib.rs +++ b/src/bunfig/lib.rs @@ -12,6 +12,6 @@ pub mod arguments; pub mod bunfig; pub mod error; -pub use arguments::{load_config, load_config_path, load_config_with_cmd_args}; +pub use arguments::{home_config_path, load_config, load_config_path, load_config_with_cmd_args}; pub use bunfig::Bunfig; pub use error::{Error, Result}; diff --git a/src/install/PackageManager.rs b/src/install/PackageManager.rs index 929dd885bc4b..6caf4ba2175b 100644 --- a/src/install/PackageManager.rs +++ b/src/install/PackageManager.rs @@ -1841,35 +1841,21 @@ pub fn init( initialize_store(); - if let Some(data_dir) = bun_core::env_var::XDG_CONFIG_HOME - .get() - .or_else(|| bun_core::env_var::HOME.get()) - { - let mut buf = PathBuffer::uninit(); - let parts = [b"./.npmrc" as &[u8]]; - - let install_ref = ctx.install.get_or_insert_with(|| { - // `Api::BunInstall` derives `Default` (all fields `None`/empty). - // Own via `Box` — never `Box::leak`. - Box::new(Api::BunInstall::default()) - }); - let npmrc_local = ZBox::from_bytes(b".npmrc"); + let install_ref = ctx.install.get_or_insert_with(|| { + // `Api::BunInstall` derives `Default` (all fields `None`/empty). + // Own via `Box` — never `Box::leak`. + Box::new(Api::BunInstall::default()) + }); + let npmrc_local = ZBox::from_bytes(b".npmrc"); + let mut buf = PathBuffer::uninit(); + if let Some(global_npmrc) = ::bun_bunfig::home_config_path(&mut buf, b".npmrc") { ini::load_npmrc_config( &mut **install_ref, env, true, - &[ - resolve_path::join_abs_string_buf_z::(data_dir, &mut buf, &parts), - &*npmrc_local, - ], + &[global_npmrc, &*npmrc_local], ); } else { - let install_ref = ctx.install.get_or_insert_with(|| { - // `Api::BunInstall` derives `Default` (all fields `None`/empty). - // Own via `Box` — never `Box::leak`. - Box::new(Api::BunInstall::default()) - }); - let npmrc_local = ZBox::from_bytes(b".npmrc"); ini::load_npmrc_config(&mut **install_ref, env, true, &[&*npmrc_local]); } let cpu_count: u32 = u32::from(bun_core::get_thread_count()); diff --git a/src/options_types/command_tag.rs b/src/options_types/command_tag.rs index f667abf1fa50..c743463af754 100644 --- a/src/options_types/command_tag.rs +++ b/src/options_types/command_tag.rs @@ -87,20 +87,10 @@ impl Tag { } pub fn read_global_config(self) -> bool { - matches!( - self, - Tag::BunxCommand - | Tag::PackageManagerCommand - | Tag::InstallCommand - | Tag::AddCommand - | Tag::RemoveCommand - | Tag::UpdateCommand - | Tag::PatchCommand - | Tag::PatchCommitCommand - | Tag::OutdatedCommand - | Tag::PublishCommand - | Tag::AuditCommand - ) + // Every command that loads a local `bunfig.toml` also loads the global + // one first so the documented shallow merge (local overrides global) + // applies uniformly to runtime and install settings alike. + LOADS_CONFIG[self] } pub fn is_npm_related(self) -> bool { diff --git a/test/cli/install/bun-run-bunfig.test.ts b/test/cli/install/bun-run-bunfig.test.ts index c3a90d514c45..c4822fcecdec 100644 --- a/test/cli/install/bun-run-bunfig.test.ts +++ b/test/cli/install/bun-run-bunfig.test.ts @@ -3,6 +3,10 @@ import { realpathSync } from "fs"; import { bunEnv, bunExe, isWindows, tempDirWithFiles, toTOMLString } from "harness"; import { join as pathJoin } from "node:path"; +// `bun run` / `bun