Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions src/bunfig/bunfig.rs
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,10 @@ impl<'a> Parser<'a> {
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");
if !self.ctx.cli_overrides.smol {
self.ctx.runtime_options.smol =
expr.as_bool().expect("infallible: type checked");
}
}
}

Expand All @@ -420,8 +423,10 @@ impl<'a> Parser<'a> {

if let Some(expr) = test.get(b"smol") {
self.expect(&expr, ExprTag::EBoolean)?;
self.ctx.runtime_options.smol =
expr.as_bool().expect("infallible: type checked");
if !self.ctx.cli_overrides.smol {
self.ctx.runtime_options.smol =
expr.as_bool().expect("infallible: type checked");
}
}

if let Some(expr) = test.get(b"coverage") {
Expand Down Expand Up @@ -754,7 +759,9 @@ impl<'a> Parser<'a> {
self.expect_string(&prefer_expr)?;
let key = prefer_expr.as_string(self.bump).unwrap_or(b"");
if let Some(setting) = OFFLINE_PREFER.get(key) {
self.ctx.debug.offline_mode_setting = Some(*setting);
if !self.ctx.cli_overrides.install_prefer {
self.ctx.debug.offline_mode_setting = Some(*setting);
}
} else {
self.add_error(
prefer_expr.loc,
Expand Down
17 changes: 17 additions & 0 deletions src/options_types/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,22 @@ pub struct ContextData {

pub preloads: Vec<Box<[u8]>>,
pub has_loaded_global_config: bool,
pub cli_overrides: CliOverrides,
}

/// Settings that were given on the command line.
///
/// `Arguments::parse` loads bunfig.toml before it applies the flags for
/// `bun file.js` / `bun -e` / `bun test`, but `bun run <target>` (and the
/// `node` shim, `bun repl`) only load it afterwards, from `RunCommand`; the
/// bunfig parser skips the keys recorded here so the flag wins in that order
/// too.
#[derive(Clone, Copy, Default)]
pub struct CliOverrides {
/// `--smol`
pub smol: bool,
/// `--prefer-offline` or `--prefer-latest`
pub install_prefer: bool,
}

impl Default for ContextData {
Expand Down Expand Up @@ -84,6 +100,7 @@ impl Default for ContextData {
no_exit_on_error: false,
preloads: Vec::new(),
has_loaded_global_config: false,
cli_overrides: CliOverrides::default(),
}
}
}
Expand Down
23 changes: 17 additions & 6 deletions src/runtime/cli/Arguments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1171,13 +1171,19 @@ pub(crate) fn parse(cmd: CommandTag, ctx: Context<'_>) -> crate::Result<api::Tra
let _ = bun_http::OVERRIDDEN_DEFAULT_USER_AGENT.set(user_agent);
}

ctx.debug.offline_mode_setting = Some(if args.flag(b"--prefer-offline") {
bun_options_types::offline_mode::OfflineMode::Offline
// "install.prefer" in bunfig.toml; without a flag, leave whatever
// bunfig.toml set (None reads as online).
let prefer = if args.flag(b"--prefer-offline") {
Some(bun_options_types::offline_mode::OfflineMode::Offline)
} else if args.flag(b"--prefer-latest") {
bun_options_types::offline_mode::OfflineMode::Latest
Some(bun_options_types::offline_mode::OfflineMode::Latest)
} else {
bun_options_types::offline_mode::OfflineMode::Online
});
None
};
if let Some(prefer) = prefer {
ctx.debug.offline_mode_setting = Some(prefer);
ctx.cli_overrides.install_prefer = true;
}

if args.flag(b"--no-install") {
ctx.debug.global_cache = options::GlobalCache::disable;
Expand Down Expand Up @@ -1208,7 +1214,12 @@ pub(crate) fn parse(cmd: CommandTag, ctx: Context<'_>) -> crate::Result<api::Tra
ctx.runtime_options.eval.script = script.into();
}
ctx.runtime_options.if_present = args.flag(b"--if-present");
ctx.runtime_options.smol = args.flag(b"--smol");
// "smol" / "test.smol" in bunfig.toml; without the flag, leave
// whatever bunfig.toml set.
if args.flag(b"--smol") {
ctx.runtime_options.smol = true;
ctx.cli_overrides.smol = true;
}
// node's `-i` is an alias for --interactive; elsewhere `-i` is --install=fallback.
ctx.runtime_options.interactive = args.flag(b"--interactive")
|| (cmd == CommandTag::RunAsNodeCommand && args.flag(b"-i"));
Expand Down
75 changes: 75 additions & 0 deletions test/config/bunfig/smol.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe, isLinux, tempDir } from "harness";

// Nothing in JS reports smol mode directly. On Linux it lowers the size at
// which a Blob is backed by a memfd (LinuxMemFdAllocator::should_use: 1 MiB in
// smol mode, 8 MiB otherwise), and the memfd is visible in /proc/self/fd while
// the Blob is alive, so a 2 MiB Blob tells the two modes apart.
const probe = /* js */ `
const { readdirSync, readlinkSync } = require("node:fs");
const blob = new Blob([new Uint8Array(2 * 1024 * 1024)]);
const memfd = readdirSync("/proc/self/fd").some(fd => {
try {
return readlinkSync("/proc/self/fd/" + fd).startsWith("/memfd:memfd-num-");
} catch {
return false;
}
});
console.log("mode:" + (memfd ? "smol" : "normal") + ":" + blob.size);
`;

// Stands in for the probe source in argv so test names stay readable.
const PROBE = "<probe>";

type Case = [bunfig: string, argv: string[], expected: "smol" | "normal"];

async function runCase([bunfig, argv, expected]: Case, files: Record<string, string>) {
if (bunfig) files["bunfig.toml"] = bunfig + "\n";
using dir = tempDir("bunfig-smol", files);
await using proc = Bun.spawn({
cmd: [bunExe(), ...argv.map(arg => (arg === PROBE ? probe : arg))],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout.match(/^mode:(\w+):2097152$/m)?.[1], stderr).toBe(expected);
expect(exitCode, stderr).toBe(0);
}

describe.skipIf(!isLinux).concurrent("bunfig.toml smol", () => {
const cases: Case[] = [
["", ["index.js"], "normal"],
["smol = false", ["index.js"], "normal"],
["[test]\nsmol = true", ["index.js"], "normal"],
// bunfig.toml is read while argv is being parsed for these.
["smol = true", ["index.js"], "smol"],
["smol = true", ["-e", PROBE], "smol"],
// `bun run` reads bunfig.toml after argv has been applied.
["smol = true", ["run", "index.js"], "smol"],
// The flag wins over the file in either order.
["smol = false", ["--smol", "index.js"], "smol"],
["smol = false", ["run", "--smol", "index.js"], "smol"],
["smol = false", ["--smol", "run", "index.js"], "smol"],
];

test.each(cases)("%j + bun %j -> %s", (bunfig, argv, expected) =>
runCase([bunfig, argv, expected], { "index.js": probe }),
);
});

describe.skipIf(!isLinux).concurrent("bunfig.toml test.smol", () => {
const testFile = `import { test } from "bun:test";\ntest("probe", () => {${probe}});\n`;

const cases: Case[] = [
["", ["test", "probe.test.js"], "normal"],
["[test]\nsmol = true", ["test", "probe.test.js"], "smol"],
["[test]\nsmol = false", ["test", "--smol", "probe.test.js"], "smol"],
["[test]\nsmol = false", ["--smol", "test", "probe.test.js"], "smol"],
];

test.each(cases)("%j + bun %j -> %s", (bunfig, argv, expected) =>
runCase([bunfig, argv, expected], { "probe.test.js": testFile }),
);
});
Loading