Skip to content
Closed
68 changes: 40 additions & 28 deletions src/bunfig/arguments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<platform::Auto>(
data_dir, &mut **buf, &paths,
));
/// Resolve `$XDG_CONFIG_HOME/<name>` when it exists, otherwise
/// `$HOME/<name>` (`$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::<platform::Auto>(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::<platform::Auto>(
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,
Expand Down Expand Up @@ -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)
}

Expand All @@ -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)?;
Expand All @@ -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)
}

Expand Down Expand Up @@ -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);
}
}

Expand Down
23 changes: 15 additions & 8 deletions src/bunfig/bunfig.rs
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,10 @@
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)?;
Expand All @@ -401,9 +404,7 @@
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");
Expand Down Expand Up @@ -562,24 +563,26 @@
self.ctx.test_options.seed = Some(seed_value);
}

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"));

Check warning on line 585 in src/bunfig/bunfig.rs

View check run for this annotation

Claude / Claude Code Review

retry/rerunEach presence-check fix has two collateral regressions

The presence-based `test_.get(b"retry").is_some()` guard + counterpart-zeroing added here has two narrow side-effects unrelated to global→local merging: **(a)** a single bunfig with `[test] rerunEach = 0` and `retry = 3` (or both `= 0`) was accepted on main but now hard-fails via `report_bunfig_load_failure → Global::crash()`, since presence is checked regardless of value; **(b)** because `parse_test_command_options` runs before `load_config_with_cmd_args`, `bun test --retry 3` with a local `[te
Comment thread
robobun marked this conversation as resolved.
}

if let Some(expr) = test_.get(b"concurrentTestGlob") {
Expand Down Expand Up @@ -715,6 +718,7 @@
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") {
Expand Down Expand Up @@ -987,6 +991,7 @@
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() {
Expand All @@ -998,8 +1003,10 @@
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,
Expand Down Expand Up @@ -1583,6 +1590,7 @@
};
// 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") {
Expand Down Expand Up @@ -1616,7 +1624,6 @@
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) {
Expand Down
2 changes: 1 addition & 1 deletion src/bunfig/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
32 changes: 9 additions & 23 deletions src/install/PackageManager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<platform::Auto>(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());
Expand Down
18 changes: 4 additions & 14 deletions src/options_types/command_tag.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Comment thread
robobun marked this conversation as resolved.
Comment thread
robobun marked this conversation as resolved.
Comment thread
robobun marked this conversation as resolved.
}
Comment thread
robobun marked this conversation as resolved.

pub fn is_npm_related(self) -> bool {
Expand Down
24 changes: 17 additions & 7 deletions test/cli/install/bun-run-bunfig.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <script>` now read the global `~/.bunfig.toml`; keep these
// assertions independent of whatever the developer has in theirs.
const isolatedEnv = { ...bunEnv, HOME: undefined, USERPROFILE: undefined, XDG_CONFIG_HOME: undefined };
Comment thread
robobun marked this conversation as resolved.

describe.each(["bun run", "bun"])(`%s`, cmd => {
const runCmd = cmd === "bun" ? ["-c=bunfig.toml", "run"] : ["-c=bunfig.toml"];
const node = Bun.which("node")!;
Expand Down Expand Up @@ -33,7 +37,7 @@ describe.each(["bun run", "bun"])(`%s`, cmd => {

const result = Bun.spawnSync({
cmd: [bunExe(), "--silent", ...bunFlag, ...runCmd, "where-node"],
env: bunEnv,
env: isolatedEnv,
stderr: "inherit",
stdout: "pipe",
stdin: "ignore",
Expand Down Expand Up @@ -83,7 +87,7 @@ describe.each(["bun run", "bun"])(`%s`, cmd => {

const result = Bun.spawnSync({
cmd: [bunExe(), ...runCmd, "startScript"],
env: bunEnv,
env: isolatedEnv,
stderr: "pipe",
stdout: "pipe",
stdin: "ignore",
Expand Down Expand Up @@ -119,7 +123,7 @@ describe.each(["bun run", "bun"])(`%s`, cmd => {

const result = Bun.spawnSync({
cmd: [bunExe(), "--silent", ...runCmd, "start"],
env: bunEnv,
env: isolatedEnv,
stderr: "pipe",
stdout: "inherit",
stdin: "ignore",
Expand Down Expand Up @@ -157,7 +161,7 @@ describe.each(["bun run", "bun"])(`%s`, cmd => {

const result = Bun.spawnSync({
cmd: [bunExe(), "--silent", ...runCmd, "where-node"],
env: bunEnv,
env: isolatedEnv,
stderr: "inherit",
stdout: "pipe",
stdin: "ignore",
Expand Down Expand Up @@ -197,7 +201,7 @@ describe.each(["bun run", "bun"])(`%s`, cmd => {

const result = Bun.spawnSync({
cmd: [bunExe(), "--silent", ...runCmd, "where-node"],
env: bunEnv,
env: isolatedEnv,
stderr: "inherit",
stdout: "pipe",
stdin: "ignore",
Expand All @@ -209,7 +213,7 @@ describe.each(["bun run", "bun"])(`%s`, cmd => {
expect(result.success).toBeTrue();
});

test("NOT autoload home bunfig.toml", async () => {
test("autoload home .bunfig.toml", async () => {
const runCmd = cmd === "bun" ? ["run"] : [];

const bunfig = toTOMLString({
Expand All @@ -236,6 +240,8 @@ describe.each(["bun run", "bun"])(`%s`, cmd => {
env: {
...bunEnv,
HOME: pathJoin(cwd, "./my-home"),
USERPROFILE: pathJoin(cwd, "./my-home"),
XDG_CONFIG_HOME: undefined,
Comment thread
robobun marked this conversation as resolved.
},
stderr: "inherit",
stdout: "pipe",
Expand All @@ -244,7 +250,11 @@ describe.each(["bun run", "bun"])(`%s`, cmd => {
});
const nodeBin = result.stdout.toString().trim();

expect(realpathSync(nodeBin)).toBe(realpathSync(node));
if (isWindows) {
expect(realpathSync(nodeBin)).toContain("\\bun-node-");
} else {
expect(realpathSync(nodeBin)).toBe(realpathSync(execPath));
}
expect(result.success).toBeTrue();
});
});
Loading
Loading