use async_trait::async_trait;
use std::future::Future;
use std::path::{Path, PathBuf};
use std::pin::Pin;
use std::sync::Arc;
use std::time::Duration;
use zapcode_core::{ResourceLimits, RunResult, Value, VmState, ZapcodeRun};
use super::{Builtin, Context, resolve_path};
use crate::error::Result;
use crate::fs::FileSystem;
use crate::interpreter::ExecResult;
const DEFAULT_MAX_DURATION: Duration = Duration::from_secs(30);
const DEFAULT_MAX_MEMORY: usize = 64 * 1024 * 1024; const DEFAULT_MAX_STACK_DEPTH: usize = 512;
const DEFAULT_MAX_ALLOCATIONS: usize = 1_000_000;
#[derive(Debug, Clone)]
pub struct TypeScriptLimits {
pub max_duration: Duration,
pub max_memory: usize,
pub max_stack_depth: usize,
pub max_allocations: usize,
}
impl Default for TypeScriptLimits {
fn default() -> Self {
Self {
max_duration: DEFAULT_MAX_DURATION,
max_memory: DEFAULT_MAX_MEMORY,
max_stack_depth: DEFAULT_MAX_STACK_DEPTH,
max_allocations: DEFAULT_MAX_ALLOCATIONS,
}
}
}
impl TypeScriptLimits {
#[must_use]
pub fn max_duration(mut self, d: Duration) -> Self {
self.max_duration = d;
self
}
#[must_use]
pub fn max_memory(mut self, bytes: usize) -> Self {
self.max_memory = bytes;
self
}
#[must_use]
pub fn max_stack_depth(mut self, depth: usize) -> Self {
self.max_stack_depth = depth;
self
}
#[must_use]
pub fn max_allocations(mut self, n: usize) -> Self {
self.max_allocations = n;
self
}
fn to_zapcode_limits(&self) -> ResourceLimits {
ResourceLimits {
memory_limit_bytes: self.max_memory,
time_limit_ms: self.max_duration.as_millis() as u64,
max_stack_depth: self.max_stack_depth,
max_allocations: self.max_allocations,
}
}
}
pub type TypeScriptExternalFnHandler = Arc<
dyn Fn(
String,
Vec<Value>,
) -> Pin<Box<dyn Future<Output = std::result::Result<Value, String>> + Send>>
+ Send
+ Sync,
>;
#[derive(Clone)]
pub struct TypeScriptExternalFns {
names: Vec<String>,
handler: TypeScriptExternalFnHandler,
}
impl std::fmt::Debug for TypeScriptExternalFns {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TypeScriptExternalFns")
.field("names", &self.names)
.field("handler", &"<fn>")
.finish()
}
}
const VFS_FUNCTIONS: &[&str] = &[
"readFile",
"writeFile",
"exists",
"readDir",
"mkdir",
"remove",
"stat",
];
#[derive(Debug, Clone)]
pub struct TypeScriptConfig {
pub limits: TypeScriptLimits,
pub enable_compat_aliases: bool,
pub enable_unsupported_mode_hint: bool,
}
impl Default for TypeScriptConfig {
fn default() -> Self {
Self {
limits: TypeScriptLimits::default(),
enable_compat_aliases: true,
enable_unsupported_mode_hint: true,
}
}
}
impl TypeScriptConfig {
#[must_use]
pub fn limits(mut self, limits: TypeScriptLimits) -> Self {
self.limits = limits;
self
}
#[must_use]
pub fn compat_aliases(mut self, enable: bool) -> Self {
self.enable_compat_aliases = enable;
self
}
#[must_use]
pub fn unsupported_mode_hint(mut self, enable: bool) -> Self {
self.enable_unsupported_mode_hint = enable;
self
}
}
pub struct TypeScript {
pub limits: TypeScriptLimits,
external_fns: Option<TypeScriptExternalFns>,
unsupported_mode_hint: bool,
cmd_name: String,
}
impl TypeScript {
pub fn new() -> Self {
Self {
limits: TypeScriptLimits::default(),
external_fns: None,
unsupported_mode_hint: true,
cmd_name: "ts".to_string(),
}
}
pub fn from_config(config: &TypeScriptConfig, cmd_name: &str) -> Self {
Self {
limits: config.limits.clone(),
external_fns: None,
unsupported_mode_hint: config.enable_unsupported_mode_hint,
cmd_name: cmd_name.to_string(),
}
}
pub fn with_external_handler(
mut self,
names: Vec<String>,
handler: TypeScriptExternalFnHandler,
) -> Self {
self.external_fns = Some(TypeScriptExternalFns { names, handler });
self
}
}
impl Default for TypeScript {
fn default() -> Self {
Self::new()
}
}
const UNSUPPORTED_NODE_FLAGS: &[&str] = &[
"--inspect",
"--inspect-brk",
"--prof",
"--watch",
"--experimental-modules",
"--loader",
"--require",
"--preserve-symlinks",
"--max-old-space-size",
"--expose-gc",
"--harmony",
"--trace-warnings",
"--no-warnings",
"--pending-deprecation",
];
const UNSUPPORTED_DENO_SUBCOMMANDS: &[&str] = &[
"run",
"compile",
"install",
"uninstall",
"lint",
"fmt",
"test",
"bench",
"check",
"serve",
"task",
"repl",
"upgrade",
"doc",
"publish",
"add",
"remove",
"init",
"info",
"cache",
"eval",
"coverage",
"types",
"completions",
];
const UNSUPPORTED_BUN_SUBCOMMANDS: &[&str] = &[
"run", "install", "add", "remove", "update", "link", "unlink", "pm", "build", "init", "test",
"x", "create",
];
fn unsupported_mode_message(cmd: &str, arg: &str) -> String {
let base = format!("{cmd}: unsupported option or subcommand: {arg}\n");
let runtime = match cmd {
"node" => "Node.js",
"deno" => "Deno",
"bun" => "Bun",
_ => "a full runtime",
};
let flag = if cmd == "ts" || cmd == "typescript" {
"-c"
} else {
"-e"
};
format!(
"{base}\
hint: This is an embedded TypeScript interpreter (ZapCode), not {runtime}.\n\
hint: Only inline execution is supported:\n\
hint: {cmd} {flag} \"console.log('hello')\" # run inline code\n\
hint: {cmd} script.ts # run file from VFS\n\
hint: echo \"code\" | {cmd} # pipe code via stdin\n"
)
}
fn is_unsupported_mode(cmd: &str, arg: &str) -> bool {
if UNSUPPORTED_NODE_FLAGS.iter().any(|f| arg.starts_with(f)) {
return true;
}
if cmd == "deno" && UNSUPPORTED_DENO_SUBCOMMANDS.contains(&arg) {
return true;
}
if cmd == "bun" && UNSUPPORTED_BUN_SUBCOMMANDS.contains(&arg) {
return true;
}
false
}
#[async_trait]
impl Builtin for TypeScript {
fn llm_hint(&self) -> Option<&'static str> {
Some(
"ts/node/deno/bun: Embedded TypeScript (ZapCode). \
Supports ES2024 subset: let/const, arrow fns, async/await, \
template literals, destructuring, array methods. \
File I/O via readFile()/writeFile() async functions. \
No npm/import/require. No HTTP/network. No eval().",
)
}
async fn execute(&self, ctx: Context<'_>) -> Result<ExecResult> {
let args = ctx.args;
let cmd = &self.cmd_name;
if args.first().map(|s| s.as_str()) == Some("--version")
|| args.first().map(|s| s.as_str()) == Some("-V")
{
return Ok(ExecResult::ok("TypeScript 5.0.0 (zapcode)\n".to_string()));
}
if args.first().map(|s| s.as_str()) == Some("--help")
|| args.first().map(|s| s.as_str()) == Some("-h")
{
return Ok(ExecResult::ok(format!(
"usage: {cmd} [-c cmd | -e cmd | file | -] [arg ...]\n\
Options:\n \
-c cmd : execute code from string\n \
-e cmd : execute code from string (Node.js compat)\n \
file : execute code from file (VFS)\n \
- : read code from stdin\n \
-V : print version\n"
)));
}
let (code, _filename) = if let Some(first) = args.first() {
match first.as_str() {
"-c" | "-e" => {
let code = args.get(1).map(|s| s.as_str()).unwrap_or("");
if code.is_empty() {
return Ok(ExecResult::err(
format!("{cmd}: option {} requires argument\n", first),
2,
));
}
(code.to_string(), "<string>".to_string())
}
"-" => {
match ctx.stdin {
Some(input) if !input.is_empty() => {
(input.to_string(), "<stdin>".to_string())
}
_ => {
return Ok(ExecResult::err(format!("{cmd}: no input from stdin\n"), 1));
}
}
}
arg if arg.starts_with('-') => {
if self.unsupported_mode_hint && is_unsupported_mode(cmd, arg) {
return Ok(ExecResult::err(unsupported_mode_message(cmd, arg), 2));
}
return Ok(ExecResult::err(
format!("{cmd}: unknown option: {arg}\n"),
2,
));
}
arg if !arg.contains('.')
&& self.unsupported_mode_hint
&& is_unsupported_mode(cmd, arg) =>
{
return Ok(ExecResult::err(unsupported_mode_message(cmd, arg), 2));
}
script_path => {
let path = resolve_path(ctx.cwd, script_path);
match ctx.fs.read_file(&path).await {
Ok(bytes) => match String::from_utf8(bytes) {
Ok(code) => (code, script_path.to_string()),
Err(_) => {
return Ok(ExecResult::err(
format!(
"{cmd}: can't decode file '{script_path}': not UTF-8\n"
),
1,
));
}
},
Err(_) => {
return Ok(ExecResult::err(
format!(
"{cmd}: can't open file '{script_path}': No such file or directory\n"
),
2,
));
}
}
}
}
} else if let Some(input) = ctx.stdin {
if input.is_empty() {
return Ok(ExecResult::ok(String::new()));
}
(input.to_string(), "<stdin>".to_string())
} else {
if self.unsupported_mode_hint {
return Ok(ExecResult::err(
format!(
"{cmd}: interactive mode not supported\n\
hint: Use inline execution instead:\n\
hint: {cmd} -c \"console.log('hello')\" # run inline code\n\
hint: {cmd} script.ts # run file from VFS\n\
hint: echo \"code\" | {cmd} # pipe code via stdin\n"
),
1,
));
}
return Ok(ExecResult::err(
format!("{cmd}: interactive mode not supported in virtual mode\n"),
1,
));
};
run_typescript(
&code,
ctx.fs.clone(),
ctx.cwd,
&self.limits,
self.external_fns.as_ref(),
)
.await
}
}
async fn run_typescript(
code: &str,
fs: Arc<dyn FileSystem>,
cwd: &Path,
ts_limits: &TypeScriptLimits,
external_fns: Option<&TypeScriptExternalFns>,
) -> Result<ExecResult> {
let code = if code.starts_with("#!") {
match code.find('\n') {
Some(pos) => &code[pos + 1..],
None => "",
}
} else {
code
};
let mut ext_fn_names: Vec<String> = VFS_FUNCTIONS.iter().map(|s| (*s).to_string()).collect();
if let Some(ef) = external_fns {
ext_fn_names.extend(ef.names.iter().cloned());
}
let runner = match ZapcodeRun::new(
code.to_string(),
Vec::new(),
ext_fn_names,
ts_limits.to_zapcode_limits(),
) {
Ok(r) => r,
Err(e) => {
return Ok(ExecResult::err(format!("{e}\n"), 1));
}
};
let result = match runner.run(Vec::new()) {
Ok(r) => r,
Err(e) => {
return Ok(ExecResult::err(format!("{e}\n"), 1));
}
};
process_vm_result(result, &fs, cwd, external_fns).await
}
async fn process_vm_result(
result: RunResult,
fs: &Arc<dyn FileSystem>,
cwd: &Path,
external_fns: Option<&TypeScriptExternalFns>,
) -> Result<ExecResult> {
let stdout = result.stdout;
let mut state = result.state;
loop {
match state {
VmState::Complete(value) => {
let mut out = stdout;
if !matches!(value, Value::Undefined) && out.is_empty() {
out = format!("{}\n", value.to_js_string());
}
return Ok(ExecResult::ok(out));
}
VmState::Suspended {
function_name,
args,
snapshot,
} => {
let return_value =
handle_external_call(&function_name, &args, fs, cwd, external_fns).await;
state = match snapshot.resume(return_value) {
Ok(s) => s,
Err(e) => {
return Ok(format_error_with_output(e, &stdout));
}
};
}
}
}
}
async fn handle_external_call(
function_name: &str,
args: &[Value],
fs: &Arc<dyn FileSystem>,
cwd: &Path,
external_fns: Option<&TypeScriptExternalFns>,
) -> Value {
match function_name {
"readFile" => handle_read_file(args, fs, cwd).await,
"writeFile" => handle_write_file(args, fs, cwd).await,
"exists" => handle_exists(args, fs, cwd).await,
"readDir" => handle_read_dir(args, fs, cwd).await,
"mkdir" => handle_mkdir(args, fs, cwd).await,
"remove" => handle_remove(args, fs, cwd).await,
"stat" => handle_stat(args, fs, cwd).await,
_ => {
if let Some(ef) = external_fns {
if ef.names.contains(&function_name.to_string()) {
match (ef.handler)(function_name.to_string(), args.to_vec()).await {
Ok(v) => v,
Err(e) => Value::String(Arc::from(format!("Error: {e}"))),
}
} else {
Value::String(Arc::from(format!(
"Error: unknown external function '{function_name}'"
)))
}
} else {
Value::String(Arc::from(format!(
"Error: unknown external function '{function_name}'"
)))
}
}
}
}
fn extract_path(args: &[Value], cwd: &Path) -> Option<PathBuf> {
match args.first()? {
Value::String(s) => {
let p = Path::new(s.as_ref());
if p.is_absolute() {
Some(p.to_owned())
} else {
Some(cwd.join(p))
}
}
_ => None,
}
}
async fn handle_read_file(args: &[Value], fs: &Arc<dyn FileSystem>, cwd: &Path) -> Value {
let Some(path) = extract_path(args, cwd) else {
return Value::String(Arc::from("Error: readFile requires a path argument"));
};
match fs.read_file(&path).await {
Ok(bytes) => match String::from_utf8(bytes) {
Ok(s) => Value::String(Arc::from(s.as_str())),
Err(_) => Value::String(Arc::from(format!(
"Error: can't decode '{}': not valid UTF-8",
path.display()
))),
},
Err(e) => Value::String(Arc::from(format!("Error: {e}"))),
}
}
async fn handle_write_file(args: &[Value], fs: &Arc<dyn FileSystem>, cwd: &Path) -> Value {
let Some(path) = extract_path(args, cwd) else {
return Value::String(Arc::from("Error: writeFile requires a path argument"));
};
let content = match args.get(1) {
Some(Value::String(s)) => s.as_ref().as_bytes().to_vec(),
Some(v) => v.to_js_string().into_bytes(),
None => {
return Value::String(Arc::from("Error: writeFile requires a content argument"));
}
};
match fs.write_file(&path, &content).await {
Ok(()) => Value::Undefined,
Err(e) => Value::String(Arc::from(format!("Error: {e}"))),
}
}
async fn handle_exists(args: &[Value], fs: &Arc<dyn FileSystem>, cwd: &Path) -> Value {
let Some(path) = extract_path(args, cwd) else {
return Value::Bool(false);
};
Value::Bool(fs.exists(&path).await.unwrap_or(false))
}
async fn handle_read_dir(args: &[Value], fs: &Arc<dyn FileSystem>, cwd: &Path) -> Value {
let Some(path) = extract_path(args, cwd) else {
return Value::String(Arc::from("Error: readDir requires a path argument"));
};
match fs.read_dir(&path).await {
Ok(entries) => {
let items: Vec<Value> = entries
.into_iter()
.map(|e| Value::String(Arc::from(e.name.as_str())))
.collect();
Value::Array(items)
}
Err(e) => Value::String(Arc::from(format!("Error: {e}"))),
}
}
async fn handle_mkdir(args: &[Value], fs: &Arc<dyn FileSystem>, cwd: &Path) -> Value {
let Some(path) = extract_path(args, cwd) else {
return Value::String(Arc::from("Error: mkdir requires a path argument"));
};
match fs.mkdir(&path, true).await {
Ok(()) => Value::Undefined,
Err(e) => Value::String(Arc::from(format!("Error: {e}"))),
}
}
async fn handle_remove(args: &[Value], fs: &Arc<dyn FileSystem>, cwd: &Path) -> Value {
let Some(path) = extract_path(args, cwd) else {
return Value::String(Arc::from("Error: remove requires a path argument"));
};
match fs.remove(&path, false).await {
Ok(()) => Value::Undefined,
Err(e) => Value::String(Arc::from(format!("Error: {e}"))),
}
}
async fn handle_stat(args: &[Value], fs: &Arc<dyn FileSystem>, cwd: &Path) -> Value {
let Some(path) = extract_path(args, cwd) else {
return Value::String(Arc::from("Error: stat requires a path argument"));
};
match fs.stat(&path).await {
Ok(meta) => {
let json = format!(
r#"{{"size":{},"isFile":{},"isDir":{}}}"#,
meta.size,
meta.file_type.is_file(),
meta.file_type.is_dir(),
);
Value::String(Arc::from(json.as_str()))
}
Err(e) => Value::String(Arc::from(format!("Error: {e}"))),
}
}
fn format_error_with_output(e: zapcode_core::ZapcodeError, stdout: &str) -> ExecResult {
let mut result = ExecResult::err(format!("{e}\n"), 1);
result.stdout = stdout.to_string();
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::builtins::Context;
use crate::fs::InMemoryFs;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
async fn run(args: &[&str], stdin: Option<&str>) -> ExecResult {
let args: Vec<String> = args.iter().map(|s| s.to_string()).collect();
let env = HashMap::new();
let mut variables = HashMap::new();
let mut cwd = PathBuf::from("/home/user");
let fs = Arc::new(InMemoryFs::new());
let ctx = Context::new_for_test(&args, &env, &mut variables, &mut cwd, fs, stdin);
TypeScript::new().execute(ctx).await.unwrap()
}
async fn run_with_file(args: &[&str], file_path: &str, content: &str) -> ExecResult {
let args: Vec<String> = args.iter().map(|s| s.to_string()).collect();
let env = HashMap::new();
let mut variables = HashMap::new();
let mut cwd = PathBuf::from("/home/user");
let fs = Arc::new(InMemoryFs::new());
fs.write_file(std::path::Path::new(file_path), content.as_bytes())
.await
.unwrap();
let ctx = Context::new_for_test(&args, &env, &mut variables, &mut cwd, fs, None);
TypeScript::new().execute(ctx).await.unwrap()
}
async fn run_with_vfs(args: &[&str], files: &[(&str, &str)]) -> ExecResult {
let args: Vec<String> = args.iter().map(|s| s.to_string()).collect();
let env = HashMap::new();
let mut variables = HashMap::new();
let mut cwd = PathBuf::from("/home/user");
let fs = Arc::new(InMemoryFs::new());
for (path, content) in files {
let p = std::path::Path::new(path);
if let Some(parent) = p.parent() {
let _ = fs.mkdir(parent, true).await;
}
fs.write_file(p, content.as_bytes()).await.unwrap();
}
let ctx = Context::new_for_test(&args, &env, &mut variables, &mut cwd, fs, None);
TypeScript::new().execute(ctx).await.unwrap()
}
#[tokio::test]
async fn test_version() {
let r = run(&["--version"], None).await;
assert_eq!(r.exit_code, 0);
assert!(r.stdout.contains("TypeScript"));
assert!(r.stdout.contains("zapcode"));
}
#[tokio::test]
async fn test_version_short() {
let r = run(&["-V"], None).await;
assert_eq!(r.exit_code, 0);
assert!(r.stdout.contains("TypeScript"));
}
#[tokio::test]
async fn test_help() {
let r = run(&["--help"], None).await;
assert_eq!(r.exit_code, 0);
assert!(r.stdout.contains("usage:"));
}
#[tokio::test]
async fn test_inline_console_log() {
let r = run(&["-c", "console.log('hello world')"], None).await;
assert_eq!(r.exit_code, 0);
assert_eq!(r.stdout, "hello world\n");
}
#[tokio::test]
async fn test_inline_expression() {
let r = run(&["-c", "1 + 2 * 3"], None).await;
assert_eq!(r.exit_code, 0);
assert_eq!(r.stdout, "7\n");
}
#[tokio::test]
async fn test_eval_flag() {
let r = run(&["-e", "console.log('hi')"], None).await;
assert_eq!(r.exit_code, 0);
assert_eq!(r.stdout, "hi\n");
}
#[tokio::test]
async fn test_inline_missing_code() {
let r = run(&["-c", ""], None).await;
assert_eq!(r.exit_code, 2);
assert!(r.stderr.contains("requires argument"));
}
#[tokio::test]
async fn test_unknown_option() {
let r = run(&["-x"], None).await;
assert_eq!(r.exit_code, 2);
assert!(r.stderr.contains("unknown option"));
}
#[tokio::test]
async fn test_no_args_no_stdin() {
let r = run(&[], None).await;
assert_eq!(r.exit_code, 1);
assert!(r.stderr.contains("interactive mode"));
}
#[tokio::test]
async fn test_stdin_pipe() {
let r = run(&[], Some("console.log('piped')")).await;
assert_eq!(r.exit_code, 0);
assert_eq!(r.stdout, "piped\n");
}
#[tokio::test]
async fn test_stdin_explicit_dash() {
let r = run(&["-"], Some("console.log('from stdin')")).await;
assert_eq!(r.exit_code, 0);
assert_eq!(r.stdout, "from stdin\n");
}
#[tokio::test]
async fn test_stdin_empty() {
let r = run(&[], Some("")).await;
assert_eq!(r.exit_code, 0);
assert_eq!(r.stdout, "");
}
#[tokio::test]
async fn test_script_file() {
let r = run_with_file(
&["script.ts"],
"/home/user/script.ts",
"console.log('from file')",
)
.await;
assert_eq!(r.exit_code, 0);
assert_eq!(r.stdout, "from file\n");
}
#[tokio::test]
async fn test_script_file_not_found() {
let r = run(&["missing.ts"], None).await;
assert_eq!(r.exit_code, 2);
assert!(r.stderr.contains("No such file"));
}
#[tokio::test]
async fn test_shebang_stripped() {
let r = run_with_file(
&["script.ts"],
"/home/user/script.ts",
"#!/usr/bin/env ts\nconsole.log('shebang')",
)
.await;
assert_eq!(r.exit_code, 0);
assert_eq!(r.stdout, "shebang\n");
}
#[tokio::test]
async fn test_let_const() {
let r = run(
&["-c", "let x = 10; const y = 20; console.log(x + y)"],
None,
)
.await;
assert_eq!(r.exit_code, 0);
assert_eq!(r.stdout, "30\n");
}
#[tokio::test]
async fn test_arrow_function() {
let r = run(
&[
"-c",
"const add = (a: number, b: number) => a + b; console.log(add(3, 4))",
],
None,
)
.await;
assert_eq!(r.exit_code, 0);
assert_eq!(r.stdout, "7\n");
}
#[tokio::test]
async fn test_template_literal() {
let r = run(
&["-c", "const name = 'world'; console.log(`hello ${name}`)"],
None,
)
.await;
assert_eq!(r.exit_code, 0);
assert_eq!(r.stdout, "hello world\n");
}
#[tokio::test]
async fn test_array_methods() {
let r = run(
&[
"-c",
"const arr = [1, 2, 3]; console.log(arr.map(x => x * 2).join(','))",
],
None,
)
.await;
assert_eq!(r.exit_code, 0);
assert_eq!(r.stdout, "2,4,6\n");
}
#[tokio::test]
async fn test_for_loop() {
let r = run(
&[
"-c",
"let sum = 0; for (let i = 0; i < 5; i++) { sum += i; } console.log(sum)",
],
None,
)
.await;
assert_eq!(r.exit_code, 0);
assert_eq!(r.stdout, "10\n");
}
#[tokio::test]
async fn test_syntax_error() {
let r = run(&["-c", "const x = {"], None).await;
assert_eq!(r.exit_code, 1);
assert!(!r.stderr.is_empty());
}
#[tokio::test]
async fn test_runtime_error() {
let r = run(&["-c", "const x: any = null; x.foo()"], None).await;
assert_eq!(r.exit_code, 1);
assert!(!r.stderr.is_empty());
}
#[tokio::test]
async fn test_vfs_read_file() {
let r = run_with_vfs(
&["-c", "await readFile('/tmp/test.txt')"],
&[("/tmp/test.txt", "hello from vfs")],
)
.await;
assert_eq!(r.exit_code, 0);
assert_eq!(r.stdout, "hello from vfs\n");
}
#[tokio::test]
async fn test_vfs_write_and_read() {
let args: Vec<String> = vec![
"-c".to_string(),
"await writeFile('/tmp/out.txt', 'written by ts'); await readFile('/tmp/out.txt')"
.to_string(),
];
let env = HashMap::new();
let mut variables = HashMap::new();
let mut cwd = PathBuf::from("/home/user");
let fs = Arc::new(InMemoryFs::new());
let _ = fs.mkdir(std::path::Path::new("/tmp"), true).await;
let ctx = Context::new_for_test(&args, &env, &mut variables, &mut cwd, fs, None);
let r = TypeScript::new().execute(ctx).await.unwrap();
assert_eq!(r.exit_code, 0);
assert_eq!(r.stdout, "written by ts\n");
}
#[tokio::test]
async fn test_vfs_exists() {
let r = run_with_vfs(
&["-c", "await exists('/tmp/test.txt')"],
&[("/tmp/test.txt", "data")],
)
.await;
assert_eq!(r.exit_code, 0);
assert_eq!(r.stdout, "true\n");
}
#[tokio::test]
async fn test_vfs_exists_false() {
let r = run(&["-c", "await exists('/tmp/nope.txt')"], None).await;
assert_eq!(r.exit_code, 0);
assert_eq!(r.stdout, "false\n");
}
#[tokio::test]
async fn test_console_log_before_vfs() {
let r = run_with_vfs(
&["-c", "console.log('before'); await readFile('/tmp/f.txt')"],
&[("/tmp/f.txt", "data")],
)
.await;
assert_eq!(r.exit_code, 0);
assert!(r.stdout.contains("before"));
}
#[test]
fn test_limits_default() {
let limits = TypeScriptLimits::default();
assert_eq!(limits.max_duration, Duration::from_secs(30));
assert_eq!(limits.max_memory, 64 * 1024 * 1024);
assert_eq!(limits.max_stack_depth, 512);
assert_eq!(limits.max_allocations, 1_000_000);
}
#[test]
fn test_limits_builder() {
let limits = TypeScriptLimits::default()
.max_duration(Duration::from_secs(5))
.max_memory(1024)
.max_stack_depth(100)
.max_allocations(500);
assert_eq!(limits.max_duration, Duration::from_secs(5));
assert_eq!(limits.max_memory, 1024);
assert_eq!(limits.max_stack_depth, 100);
assert_eq!(limits.max_allocations, 500);
}
#[test]
fn test_llm_hint() {
let ts = TypeScript::new();
let hint = ts.llm_hint().unwrap();
assert!(hint.contains("TypeScript"));
assert!(hint.contains("ZapCode"));
}
#[test]
fn test_config_defaults() {
let config = TypeScriptConfig::default();
assert!(config.enable_compat_aliases);
assert!(config.enable_unsupported_mode_hint);
}
#[test]
fn test_config_builder() {
let config = TypeScriptConfig::default()
.compat_aliases(false)
.unsupported_mode_hint(false)
.limits(TypeScriptLimits::default().max_duration(Duration::from_secs(5)));
assert!(!config.enable_compat_aliases);
assert!(!config.enable_unsupported_mode_hint);
assert_eq!(config.limits.max_duration, Duration::from_secs(5));
}
#[tokio::test]
async fn test_unsupported_node_inspect() {
let ts = TypeScript::from_config(&TypeScriptConfig::default(), "node");
let args = vec!["--inspect".to_string()];
let env = HashMap::new();
let mut variables = HashMap::new();
let mut cwd = PathBuf::from("/home/user");
let fs = Arc::new(InMemoryFs::new());
let ctx = Context::new_for_test(&args, &env, &mut variables, &mut cwd, fs, None);
let r = ts.execute(ctx).await.unwrap();
assert_eq!(r.exit_code, 2);
assert!(r.stderr.contains("hint:"), "should contain hint text");
assert!(r.stderr.contains("Node.js"), "should mention Node.js");
assert!(r.stderr.contains("node -e"), "should suggest -e flag");
}
#[tokio::test]
async fn test_unsupported_deno_run() {
let ts = TypeScript::from_config(&TypeScriptConfig::default(), "deno");
let args = vec!["run".to_string()];
let env = HashMap::new();
let mut variables = HashMap::new();
let mut cwd = PathBuf::from("/home/user");
let fs = Arc::new(InMemoryFs::new());
let ctx = Context::new_for_test(&args, &env, &mut variables, &mut cwd, fs, None);
let r = ts.execute(ctx).await.unwrap();
assert_eq!(r.exit_code, 2);
assert!(r.stderr.contains("hint:"));
assert!(r.stderr.contains("Deno"));
}
#[tokio::test]
async fn test_unsupported_bun_install() {
let ts = TypeScript::from_config(&TypeScriptConfig::default(), "bun");
let args = vec!["install".to_string()];
let env = HashMap::new();
let mut variables = HashMap::new();
let mut cwd = PathBuf::from("/home/user");
let fs = Arc::new(InMemoryFs::new());
let ctx = Context::new_for_test(&args, &env, &mut variables, &mut cwd, fs, None);
let r = ts.execute(ctx).await.unwrap();
assert_eq!(r.exit_code, 2);
assert!(r.stderr.contains("hint:"));
assert!(r.stderr.contains("Bun"));
}
#[tokio::test]
async fn test_hint_disabled() {
let config = TypeScriptConfig::default().unsupported_mode_hint(false);
let ts = TypeScript::from_config(&config, "node");
let args = vec!["--inspect".to_string()];
let env = HashMap::new();
let mut variables = HashMap::new();
let mut cwd = PathBuf::from("/home/user");
let fs = Arc::new(InMemoryFs::new());
let ctx = Context::new_for_test(&args, &env, &mut variables, &mut cwd, fs, None);
let r = ts.execute(ctx).await.unwrap();
assert_eq!(r.exit_code, 2);
assert!(!r.stderr.contains("hint:"), "should not contain hint text");
assert!(r.stderr.contains("unknown option"));
}
#[tokio::test]
async fn test_interactive_mode_hint() {
let ts = TypeScript::from_config(&TypeScriptConfig::default(), "ts");
let args: Vec<String> = vec![];
let env = HashMap::new();
let mut variables = HashMap::new();
let mut cwd = PathBuf::from("/home/user");
let fs = Arc::new(InMemoryFs::new());
let ctx = Context::new_for_test(&args, &env, &mut variables, &mut cwd, fs, None);
let r = ts.execute(ctx).await.unwrap();
assert_eq!(r.exit_code, 1);
assert!(r.stderr.contains("hint:"), "should contain hint text");
assert!(r.stderr.contains("ts -c"), "should suggest -c flag");
}
#[tokio::test]
async fn test_interactive_mode_hint_disabled() {
let config = TypeScriptConfig::default().unsupported_mode_hint(false);
let ts = TypeScript::from_config(&config, "ts");
let args: Vec<String> = vec![];
let env = HashMap::new();
let mut variables = HashMap::new();
let mut cwd = PathBuf::from("/home/user");
let fs = Arc::new(InMemoryFs::new());
let ctx = Context::new_for_test(&args, &env, &mut variables, &mut cwd, fs, None);
let r = ts.execute(ctx).await.unwrap();
assert_eq!(r.exit_code, 1);
assert!(!r.stderr.contains("hint:"), "should not contain hint text");
}
}