use std::path::PathBuf;
use std::collections::{BTreeMap, HashMap};
use std::fs::File;
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FileType {
JavaScript,
EsModule,
CommonJs,
TypeScript,
Json,
Unknown,
}
impl FileType {
pub fn needs_transpilation(&self) -> bool {
matches!(self, FileType::TypeScript)
}
pub fn is_executable(&self) -> bool {
matches!(
self,
FileType::JavaScript
| FileType::EsModule
| FileType::CommonJs
| FileType::TypeScript
)
}
pub fn module_system(&self) -> ModuleSystem {
match self {
FileType::EsModule => ModuleSystem::ESModule,
FileType::CommonJs => ModuleSystem::CommonJS,
FileType::JavaScript => ModuleSystem::Auto, FileType::TypeScript => ModuleSystem::Auto,
_ => ModuleSystem::None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ModuleSystem {
ESModule,
CommonJS,
Auto,
None,
}
pub fn detect_file_type(path: &PathBuf) -> FileType {
let extension: _ = path.extension().and_then(|ext| ext.to_str());
match extension {
Some("js") => FileType::JavaScript,
Some("mjs") => FileType::EsModule,
Some("cjs") => FileType::CommonJs,
Some("ts") | Some("tsx") | Some("mts") | Some("cts") => FileType::TypeScript,
Some("json") => FileType::Json,
_ => FileType::Unknown,
}
}
#[derive(Debug, Clone)]
pub struct ExecutionContext {
pub cwd: PathBuf,
pub script_path: PathBuf,
pub dirname: PathBuf,
pub filename: PathBuf,
pub argv: Vec<String>,
pub env: HashMap<String, String>,
pub file_type: FileType,
pub module_system: ModuleSystem,
}
impl ExecutionContext {
pub fn new(script_path: PathBuf) -> Self {
let cwd: _ = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
let absolute_path: _ = if script_path.is_absolute() {
script_path.clone()
} else {
cwd.join(&script_path)
};
let dirname: _ = absolute_path
.parent()
.map(|p| p.to_path_buf())
.unwrap_or_else(|| PathBuf::from("."));
let file_type: _ = detect_file_type(&absolute_path);
let module_system: _ = file_type.module_system();
let argv: _ = vec![
"amber".to_string(),
script_path.to_string_lossy().to_string(),
];
let env: HashMap<String, String> = std::env::vars().collect();
Self {
cwd,
script_path: absolute_path.clone(),
dirname,
filename: absolute_path,
argv,
env,
file_type,
module_system,
}
}
pub fn with_args(mut self, args: Vec<String>) -> Self {
self.argv.extend(args);
self
}
pub fn with_env(mut self, key: &str, value: &str) -> Self {
self.env.insert(key.to_string(), value.to_string());
self
}
pub fn with_module_system(mut self, system: ModuleSystem) -> Self {
self.module_system = system;
self
}
pub fn to_setup_code(&self) -> String {
let dirname_escaped: _ = self.dirname.to_string_lossy().replace('\\', "\\\\");
let filename_escaped: _ = self.filename.to_string_lossy().replace('\\', "\\\\");
let cwd_escaped: _ = self.cwd.to_string_lossy().replace('\\', "\\\\");
let argv_json: Vec<String> = self
.argv
.iter()
.map(|a| format!("\"{}\"", a.replace('\\', "\\\\").replace('"', "\\\"")))
.collect();
format!(
r#"
// Amber execution context setup
globalThis.__dirname = "{}";
globalThis.__filename = "{}";
// Ensure process object exists
if (typeof globalThis.process === 'undefined') {{
globalThis.process = {{}};
}}
// Set process.cwd()
globalThis.process.cwd = function() {{ return "{}"; }};
// Set process.argv
globalThis.process.argv = [{}];
// Note: process.env is set up separately via native binding
"#,
dirname_escaped,
filename_escaped,
cwd_escaped,
argv_json.join(", ")
)
}
}
#[derive(Debug, Clone)]
pub struct ExecutorConfig {
pub transpile_ts: bool,
pub hot_reload: bool,
pub source_maps: bool,
pub verbose: bool,
}
impl Default for ExecutorConfig {
fn default() -> Self {
Self {
transpile_ts: true,
hot_reload: false,
source_maps: true,
verbose: false,
}
}
}
pub struct ScriptExecutor {
config: ExecutorConfig,
}
impl ScriptExecutor {
pub fn new(config: ExecutorConfig) -> Self {
Self { config }
}
pub fn validate_script(&self, path: &PathBuf) -> Result<FileType, String> {
if !path.exists() {
return Err(format!("Script not found: {}", path.display()));
}
let file_type: _ = detect_file_type(path);
match file_type {
FileType::JavaScript | FileType::EsModule | FileType::CommonJs => Ok(file_type),
FileType::TypeScript => {
if self.config.transpile_ts {
Ok(file_type)
} else {
Err("TypeScript transpilation is disabled".to_string())
}
}
FileType::Json => Ok(file_type), FileType::Unknown => Err(format!(
"Unknown file type: {}. Supported: .js, .mjs, .cjs, .ts, .tsx, .json",
path.display())),
}
}
pub fn needs_transpilation(&self, path: &PathBuf) -> bool {
let file_type: _ = detect_file_type(path);
file_type.needs_transpilation()
}
pub fn build_context(&self, path: PathBuf, args: Vec<String>) -> ExecutionContext {
ExecutionContext::new(path).with_args(args)
}
pub fn config(&self) -> &ExecutorConfig {
&self.config
}
}
pub mod shebang {
pub fn detect(content: &str) -> Option<String> {
let first_line: _ = content.lines().next()?;
if first_line.starts_with("#!") {
Some(first_line[2..].trim().to_string())
} else {
None
}
}
pub fn is_compatible(shebang: &str) -> bool {
shebang.contains("amber")
|| shebang.ends_with("/env amber")
|| shebang.ends_with("/env node") || shebang.ends_with("/env bun") || shebang.ends_with("/node")
|| shebang.ends_with("/bun")
}
pub fn strip(content: &str) -> &str {
if content.starts_with("#!") {
content.find('\n').map(|i| &content[i + 1..]).unwrap_or("")
} else {
content
}
}
}
pub mod args {
const OPTIONS_WITH_VALUES: &[&str] = &[
"--config",
"-c",
"--env",
"-e",
"--loader",
"--outdir",
"--outfile",
"--target",
"--tsconfig",
"--define",
"--external",
];
#[derive(Debug, Clone)]
pub struct ParsedArgs {
pub runtime_args: Vec<String>,
pub script_args: Vec<String>,
pub script_path: Option<String>,
}
impl ParsedArgs {
pub fn parse(args: Vec<String>) -> Self {
let mut runtime_args = Vec::new();
let mut script_args = Vec::new();
let mut script_path = None;
let mut found_separator = false;
let mut found_script = false;
let mut expect_value = false;
for arg in args.into_iter().skip(1) {
if arg == "--" {
found_separator = true;
continue;
}
if found_separator {
script_args.push(arg);
} else if expect_value {
runtime_args.push(arg);
expect_value = false;
} else if !found_script && !arg.starts_with('-') {
script_path = Some(arg);
found_script = true;
} else if found_script {
script_args.push(arg);
} else {
runtime_args.push(arg.clone());
if OPTIONS_WITH_VALUES.contains(&arg.as_str()) {
expect_value = true;
}
}
}
Self {
runtime_args,
script_args,
script_path,
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_file_type_detection() {
assert_eq!(detect_file_type(&PathBuf::from("test.js")), FileType::JavaScript);
assert_eq!(detect_file_type(&PathBuf::from("test.mjs")), FileType::EsModule);
assert_eq!(detect_file_type(&PathBuf::from("test.cjs")), FileType::CommonJs);
assert_eq!(detect_file_type(&PathBuf::from("test.ts")), FileType::TypeScript);
assert_eq!(detect_file_type(&PathBuf::from("test.tsx")), FileType::TypeScript);
assert_eq!(detect_file_type(&PathBuf::from("test.json")), FileType::Json);
assert_eq!(detect_file_type(&PathBuf::from("test.txt")), FileType::Unknown);
}
#[test]
fn test_file_type_needs_transpilation() {
assert!(!FileType::JavaScript.needs_transpilation());
assert!(!FileType::EsModule.needs_transpilation());
assert!(FileType::TypeScript.needs_transpilation());
}
#[test]
fn test_execution_context_creation() {
let ctx: _ = ExecutionContext::new(PathBuf::from("test.js"));
assert!(ctx.argv.len() >= 2);
assert_eq!(ctx.argv[0], "amber");
}
#[test]
fn test_execution_context_with_args() {
let ctx: _ = ExecutionContext::new(PathBuf::from("test.js"))
.with_args(vec!["--port".to_string(), "3000".to_string()]);
assert_eq!(ctx.argv.len(), 4);
}
#[test]
fn test_shebang_detection() {
assert_eq!(
shebang::detect("#!/usr/bin/env amber\nconsole.log('hi')"),
Some("/usr/bin/env amber".to_string()));
assert!(shebang::detect("console.log('hi')").is_none());
}
#[test]
fn test_shebang_strip() {
let content: _ = "#!/usr/bin/env amber\nconsole.log('hi')";
assert_eq!(shebang::strip(content), "console.log('hi')");
}
#[test]
fn test_shebang_compatibility() {
assert!(shebang::is_compatible("/usr/bin/env amber"));
assert!(shebang::is_compatible("/usr/bin/env node"));
assert!(shebang::is_compatible("/usr/bin/env bun"));
assert!(!shebang::is_compatible("/usr/bin/python"));
}
}