Skip to content
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
1 change: 1 addition & 0 deletions completions/bun.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,7 @@ _bun_run_completion() {
'--cwd[Absolute path to resolve files & entry points from. This just changes the process cwd]:cwd' \
'--config[Config file to load bun from (e.g. -c bunfig.toml]: :->config' \
'-c[Config file to load bun from (e.g. -c bunfig.toml]: :->config' \
'--check[Check the syntax of the entry point without executing it]' \
'--env-file[Load environment variables from the specified file(s)]:env-file' \
'--extension-order[Defaults to: .tsx,.ts,.jsx,.js,.json]:extension-order' \
'--jsx-factory[Changes the function called when compiling JSX elements using the classic JSX runtime]:jsx-factory' \
Expand Down
5 changes: 5 additions & 0 deletions docs/snippets/cli/run.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ bun run <file or script>
Evaluate argument as a script and print the result. Alias: <code>-p</code>
</ParamField>

<ParamField path="--check" type="boolean">
Check the syntax of the entry point without executing it. With no file (or <code>-</code>), reads from stdin.
Equivalent to <code>node --check</code>
</ParamField>

<ParamField path="--help" type="boolean">
Display this menu and exit. Alias: <code>-h</code>
</ParamField>
Expand Down
4 changes: 4 additions & 0 deletions src/options_types/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,9 @@ pub struct RuntimeOptions {
pub redis_preconnect: bool,
pub sql_preconnect: bool,
pub eval: Eval,
/// `--check` — parse the entry point for syntax errors and exit without
/// executing it (`node --check` / `-c`).
pub syntax_check: bool,
pub preconnect: Vec<Box<[u8]>>,
pub experimental_http2_fetch: bool,
pub experimental_http3_fetch: bool,
Expand Down Expand Up @@ -598,6 +601,7 @@ impl Default for RuntimeOptions {
redis_preconnect: false,
sql_preconnect: false,
eval: Eval::default(),
syntax_check: false,
preconnect: Vec::new(),
experimental_http2_fetch: false,
experimental_http3_fetch: false,
Expand Down
13 changes: 13 additions & 0 deletions src/runtime/cli/Arguments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,9 @@ pub(crate) const RUNTIME_PARAMS_: &[ParamType] = &[
parse_param!(
"-p, --print <STR> Evaluate argument as a script and print the result"
),
parse_param!(
"--check Check the syntax of the entry point without executing it"
),
Comment thread
robobun marked this conversation as resolved.
parse_param!(
"--prefer-offline Skip staleness checks for packages in the Bun runtime and resolve from disk"
),
Expand Down Expand Up @@ -1079,6 +1082,16 @@ pub fn parse(cmd: CommandTag, ctx: Context<'_>) -> Result<api::TransformOptions,
} else if let Some(script) = args.option(b"--eval") {
ctx.runtime_options.eval.script = script.into();
}
ctx.runtime_options.syntax_check = args.flag(b"--check");
Comment thread
robobun marked this conversation as resolved.
if ctx.runtime_options.syntax_check
&& (args.option(b"--eval").is_some() || args.option(b"--print").is_some())
{
Output::err_generic(
"either --check or --eval can be used, not both",
format_args!(""),
);
Global::exit(9);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
claude[bot] marked this conversation as resolved.
ctx.runtime_options.if_present = args.flag(b"--if-present");
ctx.runtime_options.smol = args.flag(b"--smol");
ctx.runtime_options.preconnect = slice_to_owned(args.options(b"--fetch-preconnect"));
Expand Down
4 changes: 4 additions & 0 deletions src/runtime/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1445,6 +1445,10 @@ pub mod command {
};
ctx.args.target = Some(bun_options_types::schema::api::Target::Bun);

if ctx.runtime_options.syntax_check {
return run_command::RunCommand::exec_check(ctx);
}

if ctx.parallel || ctx.sequential {
// Result<Infallible, _>: if this returns at all, it's Err.
let Err(err) = super::multi_run::run(ctx);
Expand Down
103 changes: 103 additions & 0 deletions src/runtime/cli/run_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2950,6 +2950,105 @@
Ok(true)
}

/// `bun --check <file>` / `node --check` — parse the entry point and exit
/// 0 on success, or print syntax errors and exit 1. Never boots the JS VM.
#[cold]
#[inline(never)]
pub fn exec_check(ctx: &mut ContextData) -> Result<(), bun_core::Error> {
bun_ast::initialize_store();
let arena = runner_arena();

let mut positionals: &[Box<[u8]>] = &ctx.positionals[..];
// `bun run --check <file>` puts "run" in positionals[0]; strip it.
// Not applicable under `node`-argv0 emulation, where positionals[0]
// is always the script name (a file literally named "run" must not
// be stripped).
if !crate::cli::PRETEND_TO_BE_NODE.load(::core::sync::atomic::Ordering::Relaxed)
&& !positionals.is_empty()
&& positionals[0].as_ref() == b"run"
{
positionals = &positionals[1..];
}

let entry = positionals.first().filter(|e| e.as_ref() != b"-");
let (path, contents): (Box<[u8]>, Vec<u8>) = if let Some(entry) = entry {
let entry: Box<[u8]> = entry.clone();
// Cursor-read instead of `read_from` (pread-based) so pipes
// reached via a path — process substitution (`/dev/fd/N`), FIFOs —
// work like they do in `node --check`.
match sys::File::openat(Fd::cwd(), &entry, sys::O::RDONLY, 0).and_then(|f| {
let mut bytes = Vec::new();
f.read_to_end_into(&mut bytes).map(|_| bytes)
}) {
Ok(bytes) => (entry, bytes),
Err(err) => {
if ctx.runtime_options.if_present && err.get_errno() == sys::E::ENOENT {
Output::flush();
Global::exit(0);
}
pretty_errorln!(
"<r><red>error<r><d>:<r> Cannot find module {} ({})",
bun_core::fmt::quote(&entry),
bstr::BStr::new(err.name()),
);
Global::exit(1);

Check warning on line 2994 in src/runtime/cli/run_command.rs

View check run for this annotation

Claude / Claude Code Review

Misleading 'Cannot find module' for non-ENOENT errors (EISDIR/EACCES)

nit: this prints "Cannot find module …" for *any* openat/read failure, not just ENOENT — after cf293a04 switched to openat + cursor-read, `bun --check some-dir/` reaches this arm with EISDIR (open succeeds on a directory on Linux; read fails) and an unreadable file reaches it with EACCES, producing e.g. `error: Cannot find module "src" (EISDIR)` even though the path *was* found. Consider "Cannot read …" or branching the prefix on `err.get_errno() == ENOENT` (Node prints `EISDIR: illegal operatio
Comment thread
robobun marked this conversation as resolved.
}
Comment thread
robobun marked this conversation as resolved.
}
} else {
// `read_to_end()` pread()s from offset 0, which fails on a pipe
// (ESPIPE). Stream-read stdin into a growable buffer instead.
let mut bytes = Vec::new();
match sys::File::stdin().read_to_end_into(&mut bytes) {
Ok(_) => (Box::from(&b"[stdin]"[..]), bytes),
Err(err) => {
pretty_errorln!(
"<r><red>error<r><d>:<r> Failed to read stdin ({})",
bstr::BStr::new(err.name()),
);
Global::exit(1);
}
}
};

let ext = paths::extension(&path);
// `--loader .ext:loader` overrides take precedence over the defaults,
// matching the run path.
let user_loader = ctx.args.loaders.as_ref().and_then(|m| {
m.extensions
.iter()
.position(|e| strings::eql(e, ext))
.map(|i| <Loader as bun_options_types::LoaderExt>::from_api(m.loaders[i]))
});
let loader = user_loader
.or_else(|| bun_bundler::options::DEFAULT_LOADERS.get(ext).copied())
.filter(|l| l.is_javascript_like())
.unwrap_or(Loader::Tsx);
Comment thread
robobun marked this conversation as resolved.

let source = bun_ast::Source::init_path_string(&*path, &*contents);
let define = bun_js_parser::Define::default();
let mut opts =
bun_js_parser::ParserOptions::init(bun_options_types::jsx::Pragma::default(), loader);
opts.features.top_level_await = true;

// SAFETY: `ctx.log` is the process-lifetime CLI log set in
// `Command::start`; no other borrow is live here.
let log: &mut bun_ast::Log = unsafe { ctx.log() };
let had_error = match bun_js_parser::Parser::init(opts, log, &source, &define, arena) {
Ok(parser) => parser.parse().is_err(),
Err(_) => true,
};

// SAFETY: see above; the parser's `&mut Log` borrow has ended.
let log: &mut bun_ast::Log = unsafe { ctx.log() };
if had_error || log.has_errors() {
let _ = log.print(std::ptr::from_mut(Output::error_writer()));
Output::flush();
Global::exit(1);
}
Output::flush();
Global::exit(0);
}

/// Synthetic `cwd/[eval]`
/// entry point + boot. `Arguments::parse` has already stashed the script
/// in `ctx.runtime_options.eval.script`. Public so `Command::start` can
Expand Down Expand Up @@ -2985,6 +3084,10 @@
// `Command::which()` before dispatch.
debug_assert!(crate::cli::PRETEND_TO_BE_NODE.load(::core::sync::atomic::Ordering::Relaxed));

if ctx.runtime_options.syntax_check {
return Self::exec_check(ctx);
}

if !ctx.runtime_options.eval.script.is_empty() {
// synthetic `[eval]` path under cwd
let mut entry_point_buf = [0u8; MAX_PATH_BYTES + EVAL_TRIGGER.len()];
Expand Down
Loading
Loading