use anyhow::{Context, Result};
use std::fs;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use super::args::Cli;
use super::daemon::{execute, script_label};
use super::compile::lang::SourceLang;
#[cfg(feature = "wasm")]
use afterburner_wasi::embedder_vm::WasiCommandOpts;
fn is_native_script(path: &Path) -> bool {
let ext = path
.extension()
.and_then(|e| e.to_str())
.map(str::to_ascii_lowercase);
matches!(
ext.as_deref(),
Some("rs" | "go" | "c" | "cpp" | "cxx" | "cc" | "py" | "pyw" | "rb")
)
}
pub fn script_args_from_argv(file: &std::path::Path) -> Vec<String> {
let file_str = file.to_string_lossy();
let argv: Vec<String> = std::env::args().collect();
match argv.iter().skip(1).position(|a| *a == *file_str) {
Some(pos) => argv[pos + 2..].to_vec(),
None => Vec::new(),
}
}
pub fn eval_args_from_argv(code: &str) -> Vec<String> {
let argv: Vec<String> = std::env::args().collect();
let tail = argv.iter().skip(1).enumerate();
for (i, tok) in tail.clone() {
if tok == code {
return argv[i + 2..].to_vec();
}
}
for (i, tok) in tail {
let attached =
tok.strip_prefix("-e") == Some(code) || tok.strip_prefix("--eval=") == Some(code);
if attached {
return argv[i + 2..].to_vec();
}
}
Vec::new()
}
pub fn run_package_or_file(
cli: &Cli,
file: Option<&std::path::Path>,
user_args: &[String],
) -> Result<()> {
match file {
Some(p) => {
if p.extension().is_some_and(|e| e.eq_ignore_ascii_case("afb")) {
return run_afb(p, user_args);
}
run_file(cli, &p.to_path_buf(), user_args)
}
None => {
let dir = std::path::Path::new(".");
let (lang, afb_name) = resolve_package_lang_and_output(dir)?;
match SourceLang::from_str(&lang) {
Ok(l @ (SourceLang::Python | SourceLang::Ruby)) => {
run_interpreted_dir(dir, l, user_args)
}
Ok(l) if !l.is_js_family() => {
let afb_path = dir.join(&afb_name);
if !afb_path.exists() {
anyhow::bail!(
"no compiled package found at {}; \
run `burn compile` first to build the native WASM",
afb_path.display()
);
}
run_afb(&afb_path, user_args)
}
_ => {
let entry = resolve_package_entry(dir)?;
super::registry::ensure_npm_linked(dir)?;
run_file(cli, &entry, user_args)
}
}
}
}
}
fn resolve_package_lang_and_output(dir: &Path) -> Result<(String, String)> {
let manifest_path = dir.join("afb.toml");
if !manifest_path.exists() {
anyhow::bail!(
"no afb.toml in the current directory - `burn run` with no FILE \
runs the current package's entry. Pass a file (`burn run script.js`) \
or run inside a package (`burn init` to create one)."
);
}
let text = fs::read_to_string(&manifest_path)
.with_context(|| format!("reading {}", manifest_path.display()))?;
let doc: toml::Value =
toml::from_str(&text).with_context(|| format!("parsing {}", manifest_path.display()))?;
let lang = doc
.get("package")
.and_then(|p| p.get("language"))
.and_then(|l| l.as_str())
.unwrap_or("js")
.to_string();
let name = doc
.get("package")
.and_then(|p| p.get("name"))
.and_then(|n| n.as_str())
.unwrap_or("package");
let namespace = doc
.get("package")
.and_then(|p| p.get("namespace"))
.and_then(|n| n.as_str())
.unwrap_or("local");
let version = doc
.get("package")
.and_then(|p| p.get("version"))
.and_then(|v| v.as_str())
.unwrap_or("0.1.0");
let afb_name = format!("{namespace}-{name}-{version}.afb");
Ok((lang, afb_name))
}
#[cfg(feature = "wasm")]
fn run_afb(afb_path: &Path, user_args: &[String]) -> Result<()> {
use afterburner_cloud::afterburner_afb::Afb;
let bytes = fs::read(afb_path).with_context(|| format!("reading {}", afb_path.display()))?;
let afb =
Afb::from_bytes(&bytes).with_context(|| format!("parsing .afb {}", afb_path.display()))?;
let runtime_target = afb.manifest.runtime.target.as_deref().unwrap_or("");
if runtime_target == crate::cli::compile::python_wasm::RUNTIME_TARGET {
return run_python_wasm_afb(afb_path, &afb, user_args);
}
match SourceLang::from_str(&afb.manifest.package.language) {
Ok(SourceLang::Python) => run_python_afb(afb_path, &afb, user_args),
Ok(SourceLang::Ruby) => {
let is_compiled = afb
.manifest
.runtime
.target
.as_deref()
.is_some_and(|t| t == "wasm32-wasip1");
if is_compiled {
run_ruby_wasm_afb(afb_path, &afb, user_args)
} else {
run_ruby_afb(afb_path, &afb, user_args)
}
}
_ => run_wasm_afb(afb_path, &afb, user_args),
}
}
#[cfg(feature = "wasm")]
fn run_wasm_afb(
afb_path: &Path,
afb: &afterburner_cloud::afterburner_afb::Afb,
user_args: &[String],
) -> Result<()> {
use afterburner_wasi::embedder_vm::EmbedderVm;
let wasm_bytes = afb
.precompiled
.iter()
.find(|(k, _)| k.as_str() == "precompiled/wasm32-wasip1/main.wasm")
.map(|(_, v)| v.as_slice())
.ok_or_else(|| {
anyhow::anyhow!(
"{} has no precompiled/wasm32-wasip1/main.wasm; \
run `burn compile` to produce a native WASM package",
afb_path.display()
)
})?;
let vm = EmbedderVm::new().context("creating EmbedderVm")?;
let module = vm
.compile(wasm_bytes, true, |_| Ok(()))
.context("compiling WASM module")?;
let mut args = vec![afb_path.to_string_lossy().into_owned()];
args.extend_from_slice(user_args);
let opts = WasiCommandOpts::new().args(args);
let output = vm
.run_command(&module, opts, None)
.context("running WASM command")?;
if !output.stdout.is_empty() {
use std::io::Write;
std::io::stdout()
.write_all(&output.stdout)
.context("writing WASM stdout")?;
}
let exit_code = output.result as i32;
if exit_code != 0 {
std::process::exit(exit_code);
}
Ok(())
}
#[cfg(feature = "wasm")]
fn afb_entry_source(afb: &afterburner_cloud::afterburner_afb::Afb) -> Result<&str> {
let entry = &afb.manifest.package.entry;
let bytes = afb.source.get(entry).ok_or_else(|| {
anyhow::anyhow!(
"package entry {entry:?} (from afb.toml) is not present under source/ in the .afb"
)
})?;
std::str::from_utf8(bytes)
.map_err(|_| anyhow::anyhow!("package entry {entry:?} is not valid UTF-8"))
}
#[cfg(feature = "wasm")]
fn run_python_afb(
afb_path: &Path,
afb: &afterburner_cloud::afterburner_afb::Afb,
user_args: &[String],
) -> Result<()> {
use afterburner_wasi::pyodide_runner::{PyPackage, run_python_package};
use std::collections::BTreeMap;
use std::io::Write;
let entry_source = afb_entry_source(afb)
.with_context(|| format!("reading Python entry of {}", afb_path.display()))?;
let vendor_pip_wheels: Vec<Vec<u8>> = afb
.vendor
.iter()
.filter(|(k, _)| k.starts_with("vendor/pip/") && k.ends_with(".whl"))
.map(|(_, v)| v.clone())
.collect();
const GUEST_PKG_ROOT: &str = "/pkg";
let sys_path_dir = format!("{GUEST_PKG_ROOT}/source");
let mut files: BTreeMap<String, Vec<u8>> = BTreeMap::new();
for (rel, data) in &afb.source {
files.insert(format!("{GUEST_PKG_ROOT}/{rel}"), data.clone());
}
let pkg = PyPackage {
files,
sys_path_dir,
vendor_pip_wheels,
};
let out = run_python_package(entry_source, &pkg)
.map_err(|e| anyhow::anyhow!("python runtime error: {e}"))?;
if !out.stdout.is_empty() {
std::io::stdout()
.write_all(&out.stdout)
.context("writing python stdout")?;
}
if out.exit_code != 0 {
std::process::exit(out.exit_code);
}
let _ = user_args;
Ok(())
}
#[cfg(feature = "wasm")]
fn run_ruby_wasm_afb(
afb_path: &Path,
afb: &afterburner_cloud::afterburner_afb::Afb,
user_args: &[String],
) -> Result<()> {
use crate::cli::compile::guest_entry_path;
use afterburner_wasi::embedder_vm::EmbedderVm;
let wasm_bytes = afb
.precompiled
.iter()
.find(|(k, _)| k.as_str() == "precompiled/wasm32-wasip1/main.wasm")
.map(|(_, v)| v.as_slice())
.ok_or_else(|| {
anyhow::anyhow!(
"{}: Ruby compiled package has no precompiled/wasm32-wasip1/main.wasm; \
re-run `burn compile` to rebuild it",
afb_path.display()
)
})?;
let vm = EmbedderVm::new().context("creating EmbedderVm")?;
let module = vm
.compile(wasm_bytes, true, |_| Ok(()))
.context("compiling Ruby wasm module")?;
let entry_rel = &afb.manifest.package.entry;
let guest_script = guest_entry_path(entry_rel);
let mut args = vec![afb_path.to_string_lossy().into_owned(), guest_script];
args.extend_from_slice(user_args);
let opts = WasiCommandOpts::new().args(args);
let output = vm
.run_command(
&module,
opts,
Some(afterburner_wasi::ruby_runner::RUBY_FUEL),
)
.context("running Ruby wasm command")?;
if !output.stdout.is_empty() {
use std::io::Write;
std::io::stdout()
.write_all(&output.stdout)
.context("writing Ruby wasm stdout")?;
}
if !output.stderr.is_empty() {
use std::io::Write;
std::io::stderr()
.write_all(&output.stderr)
.context("writing Ruby wasm stderr")?;
}
let exit_code = output.result as i32;
if exit_code != 0 {
std::process::exit(exit_code);
}
Ok(())
}
#[cfg(feature = "wasm")]
fn run_python_wasm_afb(
afb_path: &Path,
afb: &afterburner_cloud::afterburner_afb::Afb,
user_args: &[String],
) -> Result<()> {
use crate::cli::compile::python_wasm::reconstruct_runtime_from_afb;
use afterburner_wasi::pyodide_runner::{PyPackage, run_pyodide_package_with};
use std::collections::BTreeMap;
use std::io::Write;
let entry_source = afb_entry_source(afb)
.with_context(|| format!("reading Python entry of {}", afb_path.display()))?;
let tmp_root = std::env::temp_dir().join(format!(
"burn-py-wasm-afb-{}-{}",
std::process::id(),
afb_path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_default()
));
let (rt, pip_wheel_bytes) = reconstruct_runtime_from_afb(afb, &tmp_root)
.with_context(|| format!("reconstructing Python runtime from {}", afb_path.display()))?;
const GUEST_PKG_ROOT: &str = "/pkg";
let sys_path_dir = format!("{GUEST_PKG_ROOT}/source");
let mut files: BTreeMap<String, Vec<u8>> = BTreeMap::new();
for (rel, data) in &afb.source {
files.insert(format!("{GUEST_PKG_ROOT}/{rel}"), data.clone());
}
let pkg = PyPackage {
files,
sys_path_dir,
vendor_pip_wheels: pip_wheel_bytes,
};
let run_result = run_pyodide_package_with(&rt, entry_source, &pkg)
.map_err(|e| anyhow::anyhow!("python runtime error: {e}"));
let _ = fs::remove_dir_all(&tmp_root);
let out = run_result?;
if !out.stdout.is_empty() {
std::io::stdout()
.write_all(&out.stdout)
.context("writing python stdout")?;
}
if out.exit_code != 0 {
std::process::exit(out.exit_code);
}
let _ = user_args;
Ok(())
}
#[cfg(not(feature = "wasm"))]
fn run_python_wasm_afb(
afb_path: &Path,
_afb: &afterburner_cloud::afterburner_afb::Afb,
_user_args: &[String],
) -> Result<()> {
anyhow::bail!(
"running compiled Python packages requires the `wasm` feature \
(rebuild with `--features wasm`). Package: {}",
afb_path.display()
)
}
#[cfg(feature = "wasm")]
fn run_ruby_afb(
afb_path: &Path,
afb: &afterburner_cloud::afterburner_afb::Afb,
user_args: &[String],
) -> Result<()> {
use afterburner_wasi::ruby_runner::{resolve_ruby_runtime, run_ruby_afb_with};
use std::io::Write;
let entry_rel = &afb.manifest.package.entry;
if !afb.source.contains_key(entry_rel) {
anyhow::bail!(
"package entry {entry_rel:?} (from afb.toml) is not present under source/ in {}",
afb_path.display()
);
}
let rt = resolve_ruby_runtime()
.with_context(|| format!("resolving Ruby runtime for {}", afb_path.display()))?;
let tmp_root = std::env::temp_dir().join(format!("burn-rb-pkg-{}", std::process::id()));
let _ = fs::remove_dir_all(&tmp_root);
for (rel, data) in &afb.source {
let dest = tmp_root.join(rel);
if let Some(parent) = dest.parent() {
fs::create_dir_all(parent).with_context(|| format!("creating {}", parent.display()))?;
}
fs::write(&dest, data).with_context(|| format!("writing {}", dest.display()))?;
}
let run_result = run_ruby_afb_with(&rt, &tmp_root, entry_rel, &afb.vendor);
let _ = fs::remove_dir_all(&tmp_root);
let out = run_result.map_err(|e| anyhow::anyhow!("ruby runtime error: {e}"))?;
if !out.stdout.is_empty() {
std::io::stdout()
.write_all(&out.stdout)
.context("writing ruby stdout")?;
}
if !out.stderr.is_empty() {
std::io::stderr()
.write_all(&out.stderr)
.context("writing ruby stderr")?;
}
if out.exit_code != 0 {
std::process::exit(out.exit_code);
}
let _ = user_args;
Ok(())
}
#[cfg(feature = "wasm")]
fn run_python_package_from_sources(
entry_source: &str,
sources: &std::collections::BTreeMap<String, Vec<u8>>,
_user_args: &[String],
) -> Result<()> {
use afterburner_wasi::pyodide_runner::{PyPackage, run_python_package};
use std::collections::BTreeMap;
use std::io::Write;
const GUEST_PKG_ROOT: &str = "/pkg";
let sys_path_dir = format!("{GUEST_PKG_ROOT}/source");
let mut files: BTreeMap<String, Vec<u8>> = BTreeMap::new();
for (rel, data) in sources {
files.insert(format!("{GUEST_PKG_ROOT}/{rel}"), data.clone());
}
let pkg = PyPackage {
files,
sys_path_dir,
vendor_pip_wheels: Vec::new(),
};
let out = run_python_package(entry_source, &pkg)
.map_err(|e| anyhow::anyhow!("python runtime error: {e}"))?;
if !out.stdout.is_empty() {
std::io::stdout()
.write_all(&out.stdout)
.context("writing python stdout")?;
}
if out.exit_code != 0 {
std::process::exit(out.exit_code);
}
Ok(())
}
#[cfg(feature = "wasm")]
fn run_ruby_package_from_sources(
entry_rel: &str,
sources: &std::collections::BTreeMap<String, Vec<u8>>,
_user_args: &[String],
) -> Result<()> {
use afterburner_wasi::ruby_runner::run_ruby_package;
use std::io::Write;
let tmp_root = std::env::temp_dir().join(format!("burn-rb-pkg-{}", std::process::id()));
let _ = fs::remove_dir_all(&tmp_root);
for (rel, data) in sources {
let dest = tmp_root.join(rel);
if let Some(parent) = dest.parent() {
fs::create_dir_all(parent).with_context(|| format!("creating {}", parent.display()))?;
}
fs::write(&dest, data).with_context(|| format!("writing {}", dest.display()))?;
}
let run_result = run_ruby_package(&tmp_root, entry_rel);
let _ = fs::remove_dir_all(&tmp_root);
let out = run_result.map_err(|e| anyhow::anyhow!("ruby runtime error: {e}"))?;
if !out.stdout.is_empty() {
std::io::stdout()
.write_all(&out.stdout)
.context("writing ruby stdout")?;
}
if !out.stderr.is_empty() {
std::io::stderr()
.write_all(&out.stderr)
.context("writing ruby stderr")?;
}
if out.exit_code != 0 {
std::process::exit(out.exit_code);
}
Ok(())
}
#[cfg(feature = "wasm")]
fn run_interpreted_dir(dir: &Path, lang: SourceLang, user_args: &[String]) -> Result<()> {
use afterburner_cloud::pkg::LocalPackage;
let local =
LocalPackage::load(dir).with_context(|| format!("loading package at {}", dir.display()))?;
let entry_rel = local.manifest.package.entry.clone();
let entry_bytes = local.sources.get(&entry_rel).ok_or_else(|| {
anyhow::anyhow!("package entry {entry_rel:?} (from afb.toml) is not present under source/")
})?;
match lang {
SourceLang::Python => {
let entry_source = std::str::from_utf8(entry_bytes)
.map_err(|_| anyhow::anyhow!("Python entry {entry_rel:?} is not valid UTF-8"))?;
run_python_package_from_sources(entry_source, &local.sources, user_args)
}
SourceLang::Ruby => run_ruby_package_from_sources(&entry_rel, &local.sources, user_args),
other => anyhow::bail!("run_interpreted_dir called for non-interpreted language {other:?}"),
}
}
#[cfg(not(feature = "wasm"))]
fn run_interpreted_dir(dir: &Path, _lang: SourceLang, _user_args: &[String]) -> Result<()> {
anyhow::bail!(
"running interpreted packages requires the `wasm` feature \
(rebuild with `--features wasm`). Package: {}",
dir.display()
)
}
#[cfg(not(feature = "wasm"))]
fn run_afb(afb_path: &Path, _user_args: &[String]) -> Result<()> {
anyhow::bail!(
"running native WASM packages requires the `wasm` feature (rebuild with `--features wasm`). \
Package: {}",
afb_path.display()
)
}
#[cfg(feature = "wasm")]
fn run_native_script(cli: &Cli, path: &Path, user_args: &[String]) -> Result<()> {
let ext = path
.extension()
.and_then(|e| e.to_str())
.map(str::to_ascii_lowercase);
if matches!(ext.as_deref(), Some("py" | "pyw")) {
return run_python_source(path, user_args);
}
if matches!(ext.as_deref(), Some("rb")) {
return run_ruby_source(path, user_args);
}
use super::compile::lang::compile_single_file;
let abs = path
.canonicalize()
.with_context(|| format!("resolving path {}", path.display()))?;
let wasm_bytes = compile_single_file(&abs)?;
run_wasm_bytes(cli, &abs, &wasm_bytes, user_args)
}
#[cfg(not(feature = "wasm"))]
fn run_native_script(_cli: &Cli, path: &Path, _user_args: &[String]) -> Result<()> {
anyhow::bail!(
"running native source files requires the `wasm` feature \
(rebuild with `--features wasm`). File: {}",
path.display()
)
}
#[cfg(feature = "wasm")]
fn run_python_source(path: &Path, _user_args: &[String]) -> Result<()> {
use afterburner_wasi::pyodide_runner::run_python;
use std::io::Write;
let source = fs::read_to_string(path).with_context(|| format!("reading {}", path.display()))?;
let out = run_python(&source).map_err(|e| anyhow::anyhow!("python runtime error: {e}"))?;
if !out.stdout.is_empty() {
std::io::stdout()
.write_all(&out.stdout)
.context("writing python stdout")?;
}
if out.exit_code != 0 {
std::process::exit(out.exit_code);
}
Ok(())
}
#[cfg(not(feature = "wasm"))]
fn run_python_source(path: &Path, _user_args: &[String]) -> Result<()> {
anyhow::bail!(
"running Python source files requires the `wasm` feature \
(rebuild with `--features wasm`). File: {}",
path.display()
)
}
#[cfg(feature = "wasm")]
fn run_ruby_source(path: &Path, _user_args: &[String]) -> Result<()> {
use afterburner_wasi::ruby_runner::run_ruby;
use std::io::Write;
let source = fs::read_to_string(path).with_context(|| format!("reading {}", path.display()))?;
let out = run_ruby(&source).map_err(|e| anyhow::anyhow!("ruby runtime error: {e}"))?;
if !out.stdout.is_empty() {
std::io::stdout()
.write_all(&out.stdout)
.context("writing ruby stdout")?;
}
if !out.stderr.is_empty() {
std::io::stderr()
.write_all(&out.stderr)
.context("writing ruby stderr")?;
}
if out.exit_code != 0 {
std::process::exit(out.exit_code);
}
Ok(())
}
#[cfg(not(feature = "wasm"))]
fn run_ruby_source(path: &Path, _user_args: &[String]) -> Result<()> {
anyhow::bail!(
"running Ruby source files requires the `wasm` feature \
(rebuild with `--features wasm`). File: {}",
path.display()
)
}
#[cfg(feature = "wasm")]
fn run_wasm_bytes(cli: &Cli, path: &Path, wasm_bytes: &[u8], user_args: &[String]) -> Result<()> {
use afterburner_wasi::embedder_vm::EmbedderVm;
let vm = EmbedderVm::new().context("creating EmbedderVm")?;
let module = vm
.compile(wasm_bytes, true, |_| Ok(()))
.context("compiling WASM module")?;
let mut argv = vec![path.to_string_lossy().into_owned()];
argv.extend_from_slice(user_args);
let opts = wasi_opts_from_cli(cli, WasiCommandOpts::new().args(argv));
let output = vm
.run_command(&module, opts, None)
.context("running WASM command")?;
if !output.stdout.is_empty() {
use std::io::Write;
std::io::stdout()
.write_all(&output.stdout)
.context("writing WASM stdout")?;
}
let exit_code = output.result as i32;
if exit_code != 0 {
std::process::exit(exit_code);
}
Ok(())
}
#[cfg(feature = "wasm")]
fn wasi_opts_from_cli(cli: &Cli, mut opts: WasiCommandOpts) -> WasiCommandOpts {
use super::manifold::parse_allow_list;
if cli.allow_all {
opts = opts.preopen_rw("/", "/");
for (k, v) in std::env::vars() {
opts = opts.env_var(k, v);
}
return opts;
}
if let Some(s) = cli.allow_fs.as_deref() {
let paths = parse_allow_list(s);
let roots: Vec<String> = if paths.is_empty() || paths.iter().any(|p| p == "*") {
vec!["/".into()]
} else {
paths
};
for root in roots {
opts = opts.preopen_rw(&root, &root);
}
}
if let Some(s) = cli.allow_fs_read.as_deref() {
let paths = parse_allow_list(s);
let roots: Vec<String> = if paths.is_empty() || paths.iter().any(|p| p == "*") {
vec!["/".into()]
} else {
paths
};
for root in roots {
opts = opts.preopen_ro(&root, &root);
}
}
if let Some(s) = cli.allow_fs_write.as_deref() {
let paths = parse_allow_list(s);
let roots: Vec<String> = if paths.is_empty() || paths.iter().any(|p| p == "*") {
vec!["/".into()]
} else {
paths
};
for root in roots {
opts = opts.preopen_rw(&root, &root);
}
}
if let Some(s) = cli.allow_env.as_deref() {
let vars = parse_allow_list(s);
if vars.is_empty() || vars.iter().any(|v| v == "*") {
for (k, v) in std::env::vars() {
opts = opts.env_var(k, v);
}
} else {
for key in vars {
if let Ok(val) = std::env::var(&key) {
opts = opts.env_var(key, val);
}
}
}
}
opts
}
fn resolve_package_entry(dir: &std::path::Path) -> Result<PathBuf> {
let manifest_path = dir.join("afb.toml");
if !manifest_path.exists() {
anyhow::bail!(
"no afb.toml in the current directory - `burn run` with no FILE \
runs the current package's entry. Pass a file (`burn run script.js`) \
or run inside a package (`burn init` to create one)."
);
}
let text = fs::read_to_string(&manifest_path)
.with_context(|| format!("reading {}", manifest_path.display()))?;
let manifest: toml::Value =
toml::from_str(&text).with_context(|| format!("parsing {}", manifest_path.display()))?;
let entry = manifest
.get("package")
.and_then(|p| p.get("entry"))
.and_then(|e| e.as_str())
.ok_or_else(|| anyhow::anyhow!("afb.toml has no [package].entry"))?;
let path = dir.join(entry);
if !path.exists() {
anyhow::bail!(
"package entry {entry:?} (from afb.toml) does not exist at {}",
path.display()
);
}
Ok(path)
}
pub fn run_file(cli: &Cli, path: &PathBuf, user_args: &[String]) -> Result<()> {
if is_native_script(path) {
return run_native_script(cli, path, user_args);
}
let opened;
let cli = if cli.sandbox && is_pm_internal(path) {
opened = pm_open(cli);
&opened
} else {
cli
};
if cli.watch {
return watch::run_with_watch(cli, path, user_args);
}
let source = fs::read_to_string(path).with_context(|| format!("reading {path:?}"))?;
let label = script_label(path);
let js_source = with_preload(cli, &maybe_transpile_ts(&source, path)?);
if cli.internal_worker {
return super::worker::execute(cli, &js_source, &label, user_args);
}
execute(cli, &js_source, &label, user_args)
}
fn is_pm_internal(path: &Path) -> bool {
let real = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
let p = real.to_string_lossy().replace('\\', "/");
let name = p.rsplit('/').next().unwrap_or("");
matches!(name, "npm-cli.js" | "npx-cli.js" | "yarn.js" | "pnpm.cjs")
|| [
"/node_modules/npm/",
"/node_modules/yarn/",
"/node_modules/pnpm/",
"/node_modules/corepack/",
]
.iter()
.any(|frag| p.contains(frag))
}
fn pm_open(cli: &Cli) -> Cli {
let mut open = cli.clone();
open.sandbox = false;
open.allow_net = None;
open.allow_listen = None;
open.allow_fs = None;
open.allow_fs_read = None;
open.allow_fs_write = None;
open.allow_env = None;
open
}
pub fn run_source(cli: &Cli, source: &str, user_args: &[String]) -> Result<()> {
let prepared = with_preload(cli, source);
if cli.internal_worker {
return super::worker::execute(cli, &prepared, "[eval]", &[]);
}
execute(cli, &prepared, "[eval]", user_args)
}
fn with_preload(cli: &Cli, source: &str) -> String {
let permission_prelude = build_permission_prelude(cli);
if cli.require.is_empty() && cli.import.is_empty() && permission_prelude.is_empty() {
return source.to_string();
}
let mut out = String::with_capacity(source.len() + 256);
out.push_str(&permission_prelude);
for spec in cli.require.iter().chain(cli.import.iter()) {
let escaped = spec.replace('\\', "\\\\").replace('\'', "\\'");
out.push_str(&format!(
"try {{ require('{escaped}'); }} catch (e) {{ \
console.error('burn: preload failed for', '{escaped}', ':', e && e.message); \
}}\n"
));
}
out.push_str(source);
out
}
fn build_permission_prelude(cli: &Cli) -> String {
if !cli.permission {
return String::new();
}
let mut entries: Vec<String> = Vec::new();
if let Some(v) = cli.allow_fs_read.as_deref() {
entries.push(format!("'fs.read': {}", json_string(v)));
}
if let Some(v) = cli.allow_fs_write.as_deref() {
entries.push(format!("'fs.write': {}", json_string(v)));
}
if let Some(v) = cli.allow_fs.as_deref() {
entries.push(format!("'fs.read': {}", json_string(v)));
entries.push(format!("'fs.write': {}", json_string(v)));
}
if let Some(v) = cli.allow_net.as_deref() {
entries.push(format!("'net': {}", json_string(v)));
}
if let Some(v) = cli.allow_env.as_deref() {
entries.push(format!("'env': {}", json_string(v)));
}
if cli.allow_child_process {
entries.push("'child_process': true".to_string());
}
if cli.allow_worker {
entries.push("'worker': true".to_string());
}
format!(
"globalThis.__ab_permission_grants = {{ {} }};\n",
entries.join(", ")
)
}
fn json_string(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
for ch in s.chars() {
match ch {
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
ch if (ch as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", ch as u32)),
ch => out.push(ch),
}
}
out.push('"');
out
}
mod watch {
use super::{maybe_transpile_ts, script_label, with_preload};
use crate::cli::args::Cli;
use crate::cli::daemon::execute;
use anyhow::{Context, Result};
use std::fs;
use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime};
pub(super) fn run_with_watch(cli: &Cli, path: &Path, user_args: &[String]) -> Result<()> {
let mut last_mtime = mtime_of(path);
run_once(cli, path, user_args)?;
eprintln!("burn --watch: watching {} (Ctrl-C to exit)", path.display());
loop {
std::thread::sleep(Duration::from_millis(250));
let cur = mtime_of(path);
if cur > last_mtime {
last_mtime = cur;
eprintln!("burn --watch: change detected, re-running…");
if let Err(e) = run_once(cli, path, user_args) {
eprintln!("burn --watch: error: {e}");
}
}
}
}
fn run_once(cli: &Cli, path: &Path, user_args: &[String]) -> Result<()> {
let buf = path.to_path_buf();
let _: PathBuf = buf;
let source = fs::read_to_string(path).with_context(|| format!("reading {path:?}"))?;
let label = script_label(path);
let js_source = with_preload(cli, &maybe_transpile_ts(&source, path)?);
execute(cli, &js_source, &label, user_args)
}
fn mtime_of(path: &Path) -> SystemTime {
std::fs::metadata(path)
.and_then(|m| m.modified())
.unwrap_or(SystemTime::UNIX_EPOCH)
}
}
#[cfg(feature = "ts")]
fn maybe_transpile_ts(source: &str, path: &std::path::Path) -> Result<String> {
if crate::ts::is_typescript(path) {
return crate::ts::transpile(source, path).map_err(|e| anyhow::anyhow!("{e}"));
}
crate::ts::lower_esm_js(source, path).map_err(|e| anyhow::anyhow!("{e}"))
}
#[cfg(not(feature = "ts"))]
fn maybe_transpile_ts(source: &str, path: &std::path::Path) -> Result<String> {
let ext = path
.extension()
.and_then(|e| e.to_str())
.map(str::to_ascii_lowercase);
if matches!(
ext.as_deref(),
Some("ts") | Some("mts") | Some("cts") | Some("tsx")
) {
anyhow::bail!(
"burn: TypeScript support requires the `ts` cargo feature (rebuild with `cargo install afterburner --features ts`). \
File: {}",
path.display()
);
}
Ok(source.to_string())
}