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
1 change: 1 addition & 0 deletions completions/bun.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,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
3 changes: 3 additions & 0 deletions src/options_types/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,8 @@ pub struct RuntimeOptions {
pub redis_preconnect: bool,
pub sql_preconnect: bool,
pub eval: Eval,
/// `--check`: parse the entry point and exit without executing.
pub syntax_check: bool,
pub preconnect: Vec<Box<[u8]>>,
pub experimental_http2_fetch: bool,
pub experimental_http3_fetch: bool,
Expand Down Expand Up @@ -615,6 +617,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
10 changes: 10 additions & 0 deletions src/runtime/cli/Arguments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,9 @@ 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 @@ -1205,6 +1208,13 @@ pub(crate) fn parse(cmd: CommandTag, ctx: Context<'_>) -> crate::Result<api::Tra
} 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.
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", ());
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");
// node's `-i` is an alias for --interactive; elsewhere `-i` is --install=fallback.
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 @@ -1433,6 +1433,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
102 changes: 102 additions & 0 deletions src/runtime/cli/run_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2857,6 +2857,104 @@ impl RunCommand {
Self::exec_eval(ctx)
}

/// `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.
Comment thread
robobun marked this conversation as resolved.
#[cold]
#[inline(never)]
pub(crate) fn exec_check(ctx: &mut ContextData) -> crate::Result<()> {
bun_ast::initialize_store();
let arena = runner_arena();

let mut positionals: &[Box<[u8]>] = &ctx.positionals[..];
// `bun run --check <file>` leaves "run" in positionals[0]; strip it
// (but not under node emulation, where positionals[0] is the script).
Comment thread
robobun marked this conversation as resolved.
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 (not pread-based `read_from`) so pipes passed as
// paths (process substitution, FIFOs) work like `node --check`.
Comment thread
robobun marked this conversation as resolved.
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);
}
let verb: &[u8] = if err.get_errno() == sys::E::ENOENT {
b"find module"
} else {
b"read"
};
pretty_errorln!(
"<r><red>error<r><d>:<r> Cannot {} {} ({})",
bstr::BStr::new(verb),
bun_core::fmt::quote(&entry),
bstr::BStr::new(err.name()),
);
Global::exit(1);
Comment thread
robobun marked this conversation as resolved.
}
Comment thread
robobun marked this conversation as resolved.
}
} else {
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);
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 @@ -2897,6 +2995,10 @@ impl RunCommand {
// values. Explicit `--env-file` is still honored. #6338
ctx.args.disable_default_env_files = true;

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

// `node --interactive [-e code]`: same gate as AutoCommand — a script
// positional wins, and `-p` currently bypasses the REPL (see mod.rs).
if ctx.runtime_options.interactive
Expand Down
Loading
Loading