1use std::io::{BufRead, BufReader, Read, Write};
2use std::path::{Path, PathBuf};
3use std::process::{Command, Stdio};
4use std::time::Instant;
5
6use anyhow::{Context, Result};
7use std::sync::{Arc, Mutex};
8use std::thread;
9
10use super::{ExecutionOutcome, ExecutionPayload, LanguageEngine, LanguageSession};
11
12pub struct JavascriptEngine {
13 executable: PathBuf,
14}
15
16impl JavascriptEngine {
17 pub fn new() -> Self {
18 let executable = resolve_node_binary();
19 Self { executable }
20 }
21
22 fn binary(&self) -> &Path {
23 &self.executable
24 }
25
26 fn run_command(&self) -> Command {
27 Command::new(self.binary())
28 }
29}
30
31impl LanguageEngine for JavascriptEngine {
32 fn id(&self) -> &'static str {
33 "javascript"
34 }
35
36 fn display_name(&self) -> &'static str {
37 "JavaScript"
38 }
39
40 fn aliases(&self) -> &[&'static str] {
41 &["js", "node", "nodejs"]
42 }
43
44 fn supports_sessions(&self) -> bool {
45 true
46 }
47
48 fn validate(&self) -> Result<()> {
49 let mut cmd = self.run_command();
50 cmd.arg("--version")
51 .stdout(Stdio::null())
52 .stderr(Stdio::null());
53 cmd.status()
54 .with_context(|| format!("failed to invoke {}", self.binary().display()))?
55 .success()
56 .then_some(())
57 .ok_or_else(|| anyhow::anyhow!("{} is not executable", self.binary().display()))
58 }
59
60 fn execute(&self, payload: &ExecutionPayload) -> Result<ExecutionOutcome> {
61 let start = Instant::now();
62 let output = match payload {
63 ExecutionPayload::Inline { code } => {
64 let mut cmd = self.run_command();
65 cmd.arg("-e").arg(code);
66 cmd.stdin(Stdio::inherit());
67 cmd.output()
68 }
69 ExecutionPayload::File { path } => {
70 let mut cmd = self.run_command();
71 cmd.arg(path);
72 cmd.stdin(Stdio::inherit());
73 cmd.output()
74 }
75 ExecutionPayload::Stdin { code } => {
76 let mut cmd = self.run_command();
77 cmd.arg("-")
78 .stdin(Stdio::piped())
79 .stdout(Stdio::piped())
80 .stderr(Stdio::piped());
81 let mut child = cmd.spawn().with_context(|| {
82 format!(
83 "failed to start {} for stdin execution",
84 self.binary().display()
85 )
86 })?;
87 if let Some(mut stdin) = child.stdin.take() {
88 stdin.write_all(code.as_bytes())?;
89 if !code.ends_with('\n') {
90 stdin.write_all(b"\n")?;
91 }
92 stdin.flush()?;
93 }
94 child.wait_with_output()
95 }
96 }?;
97
98 Ok(ExecutionOutcome {
99 language: self.id().to_string(),
100 exit_code: output.status.code(),
101 stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
102 stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
103 duration: start.elapsed(),
104 })
105 }
106
107 fn start_session(&self) -> Result<Box<dyn LanguageSession>> {
108 let mut cmd = self.run_command();
109 cmd.arg("--interactive")
110 .arg("--no-warnings")
111 .arg("--experimental-repl-await")
112 .stdin(Stdio::piped())
113 .stdout(Stdio::piped())
114 .stderr(Stdio::piped());
115
116 let mut child = cmd
117 .spawn()
118 .with_context(|| format!("failed to start {} REPL", self.binary().display()))?;
119
120 let stdout = child.stdout.take().context("missing stdout handle")?;
121 let stderr = child.stderr.take().context("missing stderr handle")?;
122
123 let stderr_buffer = Arc::new(Mutex::new(String::new()));
124 let stderr_collector = stderr_buffer.clone();
125 thread::spawn(move || {
126 let mut reader = BufReader::new(stderr);
127 let mut buf = String::new();
128 loop {
129 buf.clear();
130 match reader.read_line(&mut buf) {
131 Ok(0) => break,
132 Ok(_) => {
133 let mut lock = stderr_collector.lock().expect("stderr collector poisoned");
134 lock.push_str(&buf);
135 }
136 Err(_) => break,
137 }
138 }
139 });
140
141 let mut session = JavascriptSession {
142 child,
143 stdout: BufReader::new(stdout),
144 stderr: stderr_buffer,
145 };
146
147 session.discard_prompt()?;
148
149 Ok(Box::new(session))
150 }
151}
152
153fn resolve_node_binary() -> PathBuf {
154 let candidates = ["node", "nodejs"];
155 for name in candidates {
156 if let Ok(path) = which::which(name) {
157 return path;
158 }
159 }
160 PathBuf::from("node")
161}
162
163struct JavascriptSession {
164 child: std::process::Child,
165 stdout: BufReader<std::process::ChildStdout>,
166 stderr: Arc<Mutex<String>>,
167}
168
169impl JavascriptSession {
170 fn write_code(&mut self, code: &str) -> Result<()> {
171 let stdin = self
172 .child
173 .stdin
174 .as_mut()
175 .context("javascript session stdin closed")?;
176 stdin.write_all(code.as_bytes())?;
177 if !code.ends_with('\n') {
178 stdin.write_all(b"\n")?;
179 }
180 stdin.flush()?;
181 Ok(())
182 }
183
184 fn read_until_prompt(&mut self) -> Result<String> {
185 const PROMPT: &[u8] = b"> ";
186 const CONT_PROMPT: &[u8] = b"... ";
187 let mut buffer = Vec::new();
188 loop {
189 let mut byte = [0u8; 1];
190 let read = self.stdout.read(&mut byte)?;
191 if read == 0 {
192 break;
193 }
194 buffer.extend_from_slice(&byte[..read]);
195 if buffer.ends_with(PROMPT) {
196 if !buffer.ends_with(CONT_PROMPT) {
197 break;
198 }
199 }
200 }
201
202 while buffer.ends_with(PROMPT) {
203 buffer.truncate(buffer.len() - PROMPT.len());
204 }
205
206 let mut text = String::from_utf8_lossy(&buffer).into_owned();
207 text = text.replace("\r\n", "\n");
208 text = text.replace('\r', "");
209 text = trim_continuation_prompt(text, "... ");
210 Ok(text.trim_start_matches('\n').to_string())
211 }
212
213 fn take_stderr(&self) -> String {
214 let mut lock = self.stderr.lock().expect("stderr lock poisoned");
215 if lock.is_empty() {
216 String::new()
217 } else {
218 let mut output = String::new();
219 std::mem::swap(&mut output, &mut *lock);
220 output
221 }
222 }
223
224 fn discard_prompt(&mut self) -> Result<()> {
225 let _ = self.read_until_prompt()?;
226 let _ = self.take_stderr();
227 Ok(())
228 }
229}
230
231impl LanguageSession for JavascriptSession {
232 fn language_id(&self) -> &str {
233 "javascript"
234 }
235
236 fn eval(&mut self, code: &str) -> Result<ExecutionOutcome> {
237 let start = Instant::now();
238 self.write_code(code)?;
239 let stdout = self.read_until_prompt()?;
240 let stderr = self.take_stderr();
241 Ok(ExecutionOutcome {
242 language: self.language_id().to_string(),
243 exit_code: None,
244 stdout,
245 stderr,
246 duration: start.elapsed(),
247 })
248 }
249
250 fn shutdown(&mut self) -> Result<()> {
251 if let Some(mut stdin) = self.child.stdin.take() {
252 let _ = stdin.write_all(b".exit\n");
253 let _ = stdin.flush();
254 }
255 let _ = self.child.wait();
256 Ok(())
257 }
258}
259
260fn trim_continuation_prompt(mut text: String, prompt: &str) -> String {
261 if text.contains(prompt) {
262 text = text
263 .lines()
264 .map(|line| line.strip_prefix(prompt).unwrap_or(line))
265 .collect::<Vec<_>>()
266 .join("\n");
267 }
268 text
269}