Skip to content
Closed
61 changes: 33 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 Down Expand Up @@ -105,19 +120,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 +164,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
8 changes: 5 additions & 3 deletions src/bunfig/bunfig.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)?;
Expand All @@ -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");
Expand Down Expand Up @@ -715,6 +716,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") {
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};
37 changes: 9 additions & 28 deletions src/install/PackageManager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1841,35 +1841,16 @@ 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");
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,
],
);
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, &[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 @@
}

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]

Check warning on line 93 in src/options_types/command_tag.rs

View check run for this annotation

Claude / Claude Code Review

Global bunfig's jsx.runtime/development clobbered by local bunfig defaults

The shallow merge this enables is incomplete for `jsx.runtime`/`jsx.development`: the jsx block in `bunfig.rs` initializes those to `Automatic`/`true` and writes them into `ctx.args.jsx` unconditionally in the `as_mut()` arm (unlike `factory`/`fragment`/`import_source`, which are gated on `!is_empty()`), so a local `bunfig.toml` without a `jsx` key resets a global `jsx = "solid"` back to Automatic. Not a regression — pre-PR the global `jsx` never applied at all for these commands, so observable
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