use std::path::PathBuf;
use std::process::ExitCode;
use std::sync::Arc;
use std::time::Duration;
use clap::{Args, Parser, Subcommand};
use ferrijs::{
ConsoleEntry, ConsoleLevel, ConsoleSink, Limits, ModulePolicy, Permissions, RealmOptions, RunOptions, Runtime,
};
use ferrijs_bundle::{Bundler, BundlerOptions, BytecodeCache};
#[derive(Parser)]
#[command(name = "ferrijs", version, about = "An embeddable JavaScript runtime on QuickJS", long_about = None)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
Run {
file: PathBuf,
args: Vec<String>,
#[command(flatten)]
grants: Grants,
},
Eval {
source: String,
#[command(flatten)]
grants: Grants,
},
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Args, Clone)]
struct Grants {
#[arg(long, short = 'A')]
allow_all: bool,
#[arg(long, num_args = 0.., value_delimiter = ',')]
allow_read: Option<Vec<PathBuf>>,
#[arg(long, num_args = 0.., value_delimiter = ',')]
allow_write: Option<Vec<PathBuf>>,
#[arg(long, num_args = 0.., value_delimiter = ',')]
allow_net: Option<Vec<String>>,
#[arg(long, num_args = 0.., value_delimiter = ',')]
allow_env: Option<Vec<String>>,
#[arg(long, num_args = 0.., value_delimiter = ',')]
allow_sys: Option<Vec<String>>,
#[arg(long, value_delimiter = ',')]
deny_read: Vec<PathBuf>,
#[arg(long, value_delimiter = ',')]
deny_write: Vec<PathBuf>,
#[arg(long, value_delimiter = ',')]
deny_net: Vec<String>,
#[arg(long, default_value_t = 300)]
timeout: u64,
#[arg(long, default_value_t = 256)]
memory_mb: usize,
#[arg(long)]
no_eval: bool,
#[arg(long)]
freeze_intrinsics: bool,
#[arg(long)]
clock_ms: Option<u64>,
#[arg(long)]
no_builtins: bool,
#[arg(long)]
json: bool,
}
impl Grants {
fn permissions(&self) -> Result<Permissions, String> {
if self.allow_all {
return Ok(Permissions::all());
}
let mut p = Permissions::none();
match &self.allow_read {
Some(list) if list.is_empty() => p = p.allow_all_read(),
Some(list) => p = p.allow_read(list),
None => {},
}
match &self.allow_write {
Some(list) if list.is_empty() => p = p.allow_all_write(),
Some(list) => p = p.allow_write(list),
None => {},
}
match &self.allow_net {
Some(list) if list.is_empty() => p = p.allow_all_net(),
Some(list) => p = p.allow_net(list)?,
None => {},
}
match &self.allow_env {
Some(list) if list.is_empty() => p = p.allow_all_env(),
Some(list) => p = p.allow_env(list.clone()),
None => {},
}
match &self.allow_sys {
Some(list) if list.is_empty() => p = p.allow_all_sys(),
Some(list) => {
let items = list
.iter()
.map(|s| s.parse::<ferrijs::SysInfo>())
.collect::<Result<Vec<_>, _>>()?;
p = p.allow_sys(items);
},
None => {},
}
p = p
.deny_read(&self.deny_read)
.deny_write(&self.deny_write)
.deny_net(&self.deny_net)?;
Ok(p)
}
async fn runtime(&self, root: PathBuf) -> Result<Runtime, String> {
let mut modules = ModulePolicy::new(root);
if self.no_builtins {
modules = modules.no_builtins();
}
let rt = Runtime::builder()
.permissions(self.permissions()?)
.limits(Limits {
timeout: Duration::from_secs(self.timeout),
memory: self.memory_mb * 1024 * 1024,
..Limits::default()
})
.realm(RealmOptions {
eval: !self.no_eval,
freeze_intrinsics: self.freeze_intrinsics,
clock_resolution: self.clock_ms.map(Duration::from_millis),
..RealmOptions::default()
})
.modules(modules)
.console(ferrijs::ConsoleOptions {
sink: Some(Arc::new(Stdio { json: self.json })),
..ferrijs::ConsoleOptions::default()
})
.build()
.await
.map_err(|e| e.to_string())?;
Ok(rt)
}
}
#[derive(Debug)]
struct Stdio {
json: bool,
}
impl ConsoleSink for Stdio {
fn emit(&self, entry: &ConsoleEntry) {
use std::io::Write as _;
if self.json {
let line = serde_json::to_string(entry).unwrap_or_default();
let _ = writeln!(std::io::stdout(), "{line}");
return;
}
match entry.level {
ConsoleLevel::Warn | ConsoleLevel::Error | ConsoleLevel::Trace => {
let _ = writeln!(std::io::stderr(), "{}", entry.message);
},
_ => {
let _ = writeln!(std::io::stdout(), "{}", entry.message);
},
}
}
fn styled_for(&self, level: ConsoleLevel) -> bool {
use std::io::IsTerminal as _;
!self.json
&& match level {
ConsoleLevel::Warn | ConsoleLevel::Error | ConsoleLevel::Trace => std::io::stderr().is_terminal(),
_ => std::io::stdout().is_terminal(),
}
}
}
fn parse_args(raw: &[String]) -> Vec<serde_json::Value> {
raw
.iter()
.map(|a| serde_json::from_str(a).unwrap_or_else(|_| serde_json::Value::String(a.clone())))
.collect()
}
async fn run_file(file: PathBuf, args: Vec<String>, grants: Grants) -> Result<ferrijs::Run<serde_json::Value>, String> {
let file = std::fs::canonicalize(&file).map_err(|e| format!("{}: {e}", file.display()))?;
let root = file.parent().map(PathBuf::from).unwrap_or_default();
let rt = grants.runtime(root.clone()).await?;
let source = std::fs::read_to_string(&file).map_err(|e| format!("{}: {e}", file.display()))?;
let args = parse_args(&args);
let needs_bundle = ferrijs_bundle::is_typescript_path(&file) || ferrijs_bundle::source_is_es_module(&source);
if needs_bundle {
let bundler = Bundler::new(
BundlerOptions::default(),
Arc::clone(rt.registry()),
BytecodeCache::for_app("ferrijs"),
);
let name = file
.file_name()
.map_or_else(|| "entry.js".to_string(), |n| n.to_string_lossy().into_owned());
let module = bundler
.compile(std::slice::from_ref(&file), &root, &name)
.await
.map_err(|e| e.to_string())?;
Ok(rt.eval_module(&module, &args, RunOptions::default()).await)
} else {
Ok(rt.eval_script(&source, &args, RunOptions::default()).await)
}
}
fn report(run: &ferrijs::Run<serde_json::Value>) -> ExitCode {
match &run.result {
Ok(value) => {
if !value.is_null() {
println!("{}", serde_json::to_string_pretty(value).unwrap_or_default());
}
ExitCode::SUCCESS
},
Err(e) => {
eprintln!("{e}");
if let Some(snippet) = &e.source_snippet {
eprint!("{snippet}");
}
if let Some(stack) = &e.stack {
eprintln!("{}", stack.trim_end());
}
ExitCode::FAILURE
},
}
}
#[tokio::main]
async fn main() -> ExitCode {
tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.with_writer(std::io::stderr)
.init();
let cli = Cli::parse();
let outcome = match cli.command {
Command::Run { file, args, grants } => run_file(file, args, grants).await,
Command::Eval { source, grants } => match grants.runtime(std::env::current_dir().unwrap_or_default()).await {
Ok(rt) => Ok(rt.eval_script(&source, &[], RunOptions::default()).await),
Err(e) => Err(e),
},
};
match outcome {
Ok(run) => report(&run),
Err(message) => {
eprintln!("{message}");
ExitCode::FAILURE
},
}
}