Skip to content
Closed
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
12 changes: 12 additions & 0 deletions docs/runtime/check.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,18 @@ bun check # runs "biome check"
bun --check # type checks the project
```

Pass checker options before the file or script name:

```bash terminal icon="terminal"
bun --check --threads 2 --project tsconfig.app.json --pretty false
bun run --check --threads 2 --timing src/index.ts
bun test --check --threads 2 --no-pretty
```

`--threads`, `--project`, `--pretty`, `--no-pretty`, `--all`, and `--timing` also work with `--check`.
`bun build --check` supports `--threads` and `--project`; it uses the build's error format.
Use the long `--project` name here: `-p` still means `--print`. With `bun run`, options after a file or script name belong to that program.

Inside the `check` script, and in any script it starts, `bun check` is the type checker. That makes `"check": "bun check"` work.

<Warning>
Expand Down
2 changes: 2 additions & 0 deletions src/clap/comptime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,8 @@ impl<Id> ComptimeClap<Id> {
params: table.converted,
iter,
diagnostic: opt.diagnostic,
suppress_warnings: opt.suppress_warnings,
optional_booleans: opt.optional_booleans,
state: streaming::State::Normal,
positional: None,
short_aliases: opt.short_aliases,
Expand Down
8 changes: 8 additions & 0 deletions src/clap/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,10 @@ impl Default for Help {
#[derive(Default)]
pub struct ParseOptions<'a> {
pub diagnostic: Option<&'a mut Diagnostic>,
/// A trial parse must not print warnings before its caller selects a table.
pub suppress_warnings: bool,
/// Optional boolean values may also be separate `true` or `false` tokens.
pub optional_booleans: &'static [&'static [u8]],
pub stop_after_positional_at: usize,
/// Whole-token rewrites applied only where a token is being classified as a
/// flag, never to an option's value or a `--` target. Node keeps its own
Expand Down Expand Up @@ -457,6 +461,8 @@ pub fn parse<Id: 'static>(
&mut iter,
ParseOptions {
diagnostic: opt.diagnostic,
suppress_warnings: opt.suppress_warnings,
optional_booleans: opt.optional_booleans,
stop_after_positional_at: opt.stop_after_positional_at,
short_aliases: opt.short_aliases,
unknown_long_flags_are_positional: opt.unknown_long_flags_are_positional,
Expand All @@ -478,6 +484,8 @@ pub fn parse_with_table<Id: 'static>(
&mut iter,
ParseOptions {
diagnostic: opt.diagnostic,
suppress_warnings: opt.suppress_warnings,
optional_booleans: opt.optional_booleans,
stop_after_positional_at: opt.stop_after_positional_at,
short_aliases: opt.short_aliases,
unknown_long_flags_are_positional: opt.unknown_long_flags_are_positional,
Expand Down
27 changes: 24 additions & 3 deletions src/clap/streaming.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ pub struct StreamingClap<'p, 'a, Id, ArgIterator> {
pub(crate) state: State<'a>,
pub(crate) positional: Option<&'p clap::Param<Id>>,
pub(crate) diagnostic: Option<&'p mut clap::Diagnostic>,
pub(crate) suppress_warnings: bool,
pub(crate) optional_booleans: &'static [&'static [u8]],
pub(crate) short_aliases: &'static [(&'static [u8], &'static [u8])],
pub(crate) unknown_long_flags_are_positional: bool,
}
Expand Down Expand Up @@ -127,7 +129,19 @@ where

return Ok(Some(Arg {
param,
value: maybe_value,
value: maybe_value.or_else(|| {
if self.optional_booleans.contains(&name)
&& self
.iter
.remain()
.first()
.is_some_and(|value| matches!(*value, b"true" | b"false"))
{
self.iter.next()
} else {
None
}
}),
}));
}

Expand Down Expand Up @@ -158,7 +172,8 @@ where
// unrecognized command
// if flag else arg
if arg_info.kind == ArgKind::Long || arg_info.kind == ArgKind::Short {
if WARN_ON_UNRECOGNIZED_FLAG.load(Ordering::Relaxed) {
if !self.suppress_warnings && WARN_ON_UNRECOGNIZED_FLAG.load(Ordering::Relaxed)
{
bun_core::warn!(
"unrecognized flag: {}{}\n",
if arg_info.kind == ArgKind::Long {
Expand All @@ -175,7 +190,7 @@ where
return self.next();
}

if WARN_ON_UNRECOGNIZED_FLAG.load(Ordering::Relaxed) {
if !self.suppress_warnings && WARN_ON_UNRECOGNIZED_FLAG.load(Ordering::Relaxed) {
bun_core::warn!("unrecognized argument: {}\n", bstr::BStr::new(name));
Output::flush();
}
Expand Down Expand Up @@ -382,6 +397,8 @@ mod tests {
remain: args_strings,
};
let mut c = StreamingClap::<u8, args::SliceIterator> {
suppress_warnings: false,
optional_booleans: &[],
short_aliases: &[],
unknown_long_flags_are_positional: false,
params,
Expand Down Expand Up @@ -416,6 +433,8 @@ mod tests {
remain: args_strings,
};
let mut c = StreamingClap::<u8, args::SliceIterator> {
suppress_warnings: false,
optional_booleans: &[],
short_aliases: &[],
unknown_long_flags_are_positional: false,
params,
Expand Down Expand Up @@ -889,6 +908,8 @@ mod tests {
let args: &[&[u8]] = &[b"--q", b"x", b"--aa", b"--q=1", b"--", b"--aa"];
let mut iter = args::SliceIterator { remain: args };
let mut c = StreamingClap::<u8, args::SliceIterator> {
suppress_warnings: false,
optional_booleans: &[],
short_aliases: &[],
unknown_long_flags_are_positional: true,
params: &params,
Expand Down
10 changes: 10 additions & 0 deletions src/options_types/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,7 @@ pub struct RuntimeOptions {
pub smol: bool,
/// `--check`: type check what is about to be run, and do not run it if there are errors.
pub check: bool,
pub check_options: CheckOptions,
pub debugger: Debugger,
pub if_present: bool,
pub redis_preconnect: bool,
Expand All @@ -584,6 +585,14 @@ pub struct RuntimeOptions {
pub heap_prof: HeapProf,
}

#[derive(Clone, Copy, Default)]
pub struct CheckOptions {
pub threads: usize,
pub pretty: Option<bool>,
pub all: bool,
pub timing: bool,
}

#[derive(Default)]
pub struct Eval {
pub script: Box<[u8]>,
Expand Down Expand Up @@ -633,6 +642,7 @@ impl Default for RuntimeOptions {
Self {
smol: false,
check: false,
check_options: CheckOptions::default(),
debugger: Debugger::Unspecified,
if_present: false,
redis_preconnect: false,
Expand Down
140 changes: 127 additions & 13 deletions src/runtime/cli/Arguments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -704,6 +704,54 @@ pub(crate) static TEST_TABLE: &clap::ConvertedTable = clap::comptime_table!(TEST
static BASE_RUNTIME_TRANSPILER_TABLE: &clap::ConvertedTable =
clap::comptime_table!(BASE_RUNTIME_TRANSPILER_PARAMS, cold);

// `-p` remains `--print`. In check mode, --project shares the existing override's value slot,
// so the last spelling wins without making --project a runtime option everywhere.
const fn with_check_project<const N: usize>(params: &[ParamType]) -> [ParamType; N] {
let mut result = [params[0]; N];
let mut i = 0;
while i < N {
result[i] = params[i];
if let Some(name) = result[i].names.long {
if strings::const_bytes_eq(name, b"tsconfig-override") {
result[i].names.long_aliases = &[b"project"];
}
}
i += 1;
}
result
}
const CHECK_PARAMS: &[ParamType] = concat_params!(
cli::check_command::THREAD_PARAMS,
cli::check_command::OUTPUT_PARAMS,
cli::check_command::PRETTY
);
pub(crate) const CHECK_AUTO_PARAMS: &[ParamType] = concat_params!(
&with_check_project::<{ AUTO_PARAMS.len() }>(AUTO_PARAMS),
CHECK_PARAMS
);
static CHECK_AUTO_TABLE: &clap::ConvertedTable = clap::comptime_table!(CHECK_AUTO_PARAMS, cold);
static CHECK_RUN_TABLE: &clap::ConvertedTable = clap::comptime_table!(
concat_params!(
&with_check_project::<{ RUN_PARAMS.len() }>(RUN_PARAMS),
CHECK_PARAMS
),
cold
);
static CHECK_TEST_TABLE: &clap::ConvertedTable = clap::comptime_table!(
concat_params!(
&with_check_project::<{ TEST_PARAMS.len() }>(TEST_PARAMS),
CHECK_PARAMS
),
cold
);
static CHECK_BUILD_TABLE: &clap::ConvertedTable = clap::comptime_table!(
concat_params!(
&with_check_project::<{ BUILD_PARAMS.len() }>(BUILD_PARAMS),
cli::check_command::THREAD_PARAMS
),
cold
);

/// Per-tag pre-converted clap table (rodata, built at compile time via
/// `comptime_table!`). This is what `parse` consumes so the startup path never
/// hits `ConvertedTable::build`'s alloc/sort/lock.
Expand All @@ -719,6 +767,71 @@ fn tag_table(cmd: CommandTag) -> &'static clap::ConvertedTable {
}
}

fn parsing_options(cmd: CommandTag) -> clap::ParseOptions<'static> {
clap::ParseOptions {
optional_booleans: &[b"pretty"],
stop_after_positional_at: match cmd {
CommandTag::RunCommand => 2,
CommandTag::AutoCommand | CommandTag::RunAsNodeCommand => 1,
_ => 0,
},
short_aliases: match cmd {
CommandTag::AutoCommand | CommandTag::RunAsNodeCommand => NODE_SHORT_ALIASES,
_ => &[],
},
..Default::default()
}
}

/// Extra value-taking options must not change where an ordinary invocation finds its script.
/// Trial parses honor the same entrypoint and `--` boundaries, without printing anything.
fn check_table(cmd: CommandTag) -> Option<&'static clap::ConvertedTable> {
if !bun_core::argv().iter().any(|arg| arg == b"--check") {
return None;
}
let table = match cmd {
CommandTag::AutoCommand => CHECK_AUTO_TABLE,
CommandTag::RunCommand => CHECK_RUN_TABLE,
CommandTag::TestCommand => CHECK_TEST_TABLE,
CommandTag::BuildCommand => CHECK_BUILD_TABLE,
_ => return None,
};
for candidate in [table, tag_table(cmd)] {
let parsed = clap::parse_with_table::<clap::Help>(
candidate,
clap::ParseOptions {
suppress_warnings: true,
..parsing_options(cmd)
},
);
if parsed.is_ok_and(|args| args.flag(b"--check")) {
return Some(table);
}
}
None
}

/// Command dispatch must not mistake the value of `--project` for `build`, `test`, or `run`.
pub(crate) fn checked_command_name() -> Option<Option<&'static [u8]>> {
let table = check_table(CommandTag::AutoCommand)?;
let parsed = clap::parse_with_table::<clap::Help>(
table,
clap::ParseOptions {
suppress_warnings: true,
..parsing_options(CommandTag::AutoCommand)
},
);
Some(parsed.ok().and_then(|args| {
// --filter owns a workspace script, even when its name is a built-in command.
// AutoCommand routes the parsed filter to the workspace script runner instead.
if !args.options(b"--filter").is_empty() {
None
} else {
args.positionals().first().copied()
}
}))
}

// ─── exported FFI globals (written by parse(), read from C++) ────────────────
// `AtomicBool` has the same size/alignment/bit-validity as `bool`, so the
// `#[no_mangle]` symbol layout is unchanged for the C++ side that reads these
Expand Down Expand Up @@ -827,23 +940,14 @@ pub(crate) fn disallow_code_generation_from_strings_as_compiled(embedded: &[&bun
/// `if matches!(cmd, …)`.
pub(crate) fn parse(cmd: CommandTag, ctx: Context<'_>) -> crate::Result<api::TransformOptions> {
let mut diag = clap::Diagnostic::default();
let table = tag_table(cmd);
let check_table = check_table(cmd);
let table = check_table.unwrap_or_else(|| tag_table(cmd));

let args = match clap::parse_with_table::<clap::Help>(
table,
clap::ParseOptions {
diagnostic: Some(&mut diag),
stop_after_positional_at: match cmd {
CommandTag::RunCommand => 2,
CommandTag::AutoCommand | CommandTag::RunAsNodeCommand => 1,
_ => 0,
},
// Only the paths standing in for `node` get node's aliases.
short_aliases: match cmd {
CommandTag::AutoCommand | CommandTag::RunAsNodeCommand => NODE_SHORT_ALIASES,
_ => &[],
},
..Default::default()
..parsing_options(cmd)
},
) {
Ok(a) => a,
Expand All @@ -855,6 +959,16 @@ pub(crate) fn parse(cmd: CommandTag, ctx: Context<'_>) -> crate::Result<api::Tra
}
};

if check_table.is_some() {
ctx.runtime_options.check_options =
cli::check_command::runtime_options(&args, cmd == CommandTag::BuildCommand);
if !args.flag(b"--check") {
cli::check_command::usage_error(format_args!(
"an option is missing its value before --check"
));
}
}

if args.flag(b"--help") {
command::tag_print_help(cmd, true);
Output::flush();
Expand Down Expand Up @@ -1258,7 +1372,7 @@ pub(crate) fn parse(cmd: CommandTag, ctx: Context<'_>) -> crate::Result<api::Tra
ctx.runtime_options.if_present = args.flag(b"--if-present");
ctx.runtime_options.smol = args.flag(b"--smol");
// To `node`, `--check` means something else.
ctx.runtime_options.check = args.flag(b"--check") && cmd != CommandTag::RunAsNodeCommand;
ctx.runtime_options.check = check_table.is_some();
// Nothing runs unchecked, so the process starts again, as under `--watch`. Decided here: on
// Windows the process that starts it again is set up right after this.
if ctx.runtime_options.check && ctx.debug.hot_reload == HotReload::Hot {
Expand Down
Loading
Loading