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
15 changes: 10 additions & 5 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 Expand Up @@ -987,6 +989,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() {
Expand All @@ -998,8 +1001,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,
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 { 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 };

Check warning on line 8 in test/cli/install/bun-run-bunfig.test.ts

View check run for this annotation

Claude / Claude Code Review

Suite-wide hermeticity: bunEnv now leaks host ~/.bunfig.toml into every runtime-spawning test

The per-file `isolatedEnv` fixes this file, but the same hermeticity break now applies suite-wide: `bunEnv` in `test/harness.ts:64` spreads `process.env` without overriding `HOME`/`USERPROFILE`/`XDG_CONFIG_HOME`, so after `read_global_config()` → `LOADS_CONFIG[self]` every test that spawns `bunExe()` with `env: bunEnv` for `bun <file>` / `bun -e` / `bun run` / `bun test` / `bun build` reads the host developer's `~/.bunfig.toml`. Per REVIEW.md "fix the whole class in the same PR" and "tests must
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 @@

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 @@

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 @@

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 @@

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 @@

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