use anyhow::{Context, Result};
use std::fs;
use std::path::{Path, PathBuf};
use super::args::Cli;
use super::daemon::{execute, script_label};
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) => run_file(cli, &p.to_path_buf(), user_args),
None => {
let dir = std::path::Path::new(".");
let entry = resolve_package_entry(dir)?;
super::registry::ensure_npm_linked(dir)?;
run_file(cli, &entry, user_args)
}
}
}
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<()> {
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())
}