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