diff --git a/cli/args/flags.rs b/cli/args/flags.rs index a2f0b87324..92faa8d8b4 100644 --- a/cli/args/flags.rs +++ b/cli/args/flags.rs @@ -330,6 +330,7 @@ pub struct ReplFlags { pub eval_files: Option>, pub eval: Option, pub is_default_command: bool, + pub json: bool, } #[derive(Clone, Debug, Eq, PartialEq, Default)] @@ -658,6 +659,7 @@ impl Default for DenoSubcommand { eval_files: None, eval: None, is_default_command: true, + json: false, }) } } @@ -1564,6 +1566,7 @@ pub fn flags_from_vec(args: Vec) -> clap::error::Result { eval_files: None, eval: None, is_default_command: true, + json: false, }, ) } @@ -3355,6 +3358,7 @@ TypeScript is supported, however it is not type-checked, only transpiled." .help("Evaluates the provided code when the REPL starts") .value_name("code"), ) + .arg(Arg::new("json").long("json").action(ArgAction::SetTrue).hide(true)) .after_help(cstr!("Environment variables: DENO_REPL_HISTORY Set REPL history file path. History file is disabled when the value is empty. [default: $DENO_DIR/deno_history.txt]")) @@ -5722,12 +5726,15 @@ fn repl_parse( flags.argv.extend(args); } + let json = matches.remove_one::("json").unwrap_or(false); + handle_repl_flags( flags, ReplFlags { eval_files, eval: matches.remove_one::("eval"), is_default_command: false, + json, }, ); Ok(()) @@ -8406,6 +8413,7 @@ mod tests { eval_files: None, eval: None, is_default_command: true, + json: false, }), unsafely_ignore_certificate_errors: None, permissions: PermissionFlags { @@ -8441,6 +8449,7 @@ mod tests { eval_files: None, eval: None, is_default_command: false, + json: false, }), import_map_path: Some("import_map.json".to_string()), no_remote: true, @@ -8476,6 +8485,7 @@ mod tests { eval_files: None, eval: Some("console.log('hello');".to_string()), is_default_command: false, + json: false, }), permissions: PermissionFlags { allow_write: Some(vec![]), @@ -8501,6 +8511,7 @@ mod tests { ]), eval: None, is_default_command: false, + json: false, }), ..Flags::default() } @@ -9615,6 +9626,7 @@ mod tests { eval_files: None, eval: Some("console.log('hello');".to_string()), is_default_command: false, + json: false, }), unsafely_ignore_certificate_errors: Some(vec![]), type_check_mode: TypeCheckMode::None, @@ -9686,6 +9698,7 @@ mod tests { eval_files: None, eval: None, is_default_command: false, + json: false, }), unsafely_ignore_certificate_errors: Some(svec![ "deno.land", @@ -12234,6 +12247,7 @@ mod tests { eval_files: None, eval: None, is_default_command: true, + json: false, }), log_level: Some(Level::Debug), permissions: PermissionFlags { @@ -12257,6 +12271,7 @@ mod tests { eval_files: None, eval: None, is_default_command: false, + json: false, }), argv: svec!["foo"], ..Flags::default() diff --git a/cli/tools/repl/mod.rs b/cli/tools/repl/mod.rs index 82b995bf60..c5d6afd08a 100644 --- a/cli/tools/repl/mod.rs +++ b/cli/tools/repl/mod.rs @@ -178,10 +178,6 @@ pub async fn run( let file_fetcher = factory.file_fetcher()?; let compiler_options_resolver = factory.compiler_options_resolver()?; let worker_factory = factory.create_cli_main_worker_factory().await?; - let history_file_path = factory - .deno_dir() - .ok() - .and_then(|dir| dir.repl_history_file_path()); let (worker, test_event_receiver) = create_single_test_event_channel(); let test_event_sender = worker.sender; let mut worker = worker_factory @@ -208,6 +204,12 @@ pub async fn run( test_event_receiver, ) .await?; + + #[cfg(unix)] + if repl_flags.json { + return run_json(session).await; + } + let rustyline_channel = rustyline_channel(); let helper = EditorHelper { @@ -215,6 +217,10 @@ pub async fn run( sync_sender: rustyline_channel.0, }; + let history_file_path = factory + .deno_dir() + .ok() + .and_then(|dir| dir.repl_history_file_path()); let editor = ReplEditor::new(helper, history_file_path)?; let mut repl = Repl { @@ -278,3 +284,121 @@ pub async fn run( Ok(repl.session.worker.exit_code()) } + +#[cfg(unix)] +async fn run_json(mut repl_session: ReplSession) -> Result { + use bytes::Buf; + use bytes::Bytes; + use deno_runtime::deno_io::BiPipe; + use tokio::io::AsyncReadExt; + use tokio::io::AsyncWriteExt; + use tokio::io::BufReader; + + #[derive(serde::Serialize, serde::Deserialize, Debug)] + #[serde(tag = "type")] + enum ReplMessage { + Run { code: String, output: bool }, + RunOutput { output: String }, + Error { error: String }, + } + + let (receiver, mut sender) = BiPipe::from_raw(3)?.split(); + let mut receiver = BufReader::new(receiver); + + loop { + let mut line_fut = std::pin::pin!(async { + let len = receiver.read_u32_le().await?; + let mut buf = vec![0; len as _]; + receiver.read_exact(&mut buf).await?; + Ok::<_, AnyError>(buf) + }); + let mut poll_worker = true; + let line = loop { + tokio::select! { + line = &mut line_fut => break line?, + _ = repl_session.run_event_loop(), if poll_worker => { + poll_worker = false; + continue; + } + } + }; + let command: ReplMessage = serde_json::from_slice(&line)?; + + if let ReplMessage::Run { code, output } = command { + let result = repl_session.evaluate_line_with_object_wrapping(&code).await; + + // We check for close and break here instead of making it a loop condition to get + // consistent behavior in when the user evaluates a call to close(). + match repl_session.closing().await { + Ok(closing) if closing => break, + Ok(_) => {} + Err(err) => { + let buf = serde_json::to_vec(&ReplMessage::Error { + error: format!("{}", err), + })?; + sender + .write_all_buf( + &mut Bytes::from_owner((buf.len() as u32).to_le_bytes()) + .chain(Bytes::from(buf)), + ) + .await?; + } + }; + + match result { + Ok(evaluate_response) => { + let cdp::EvaluateResponse { + result, + exception_details, + } = evaluate_response.value; + + if exception_details.is_some() { + repl_session.set_last_thrown_error(&result).await?; + } else { + repl_session + .language_server + .commit_text(&evaluate_response.ts_code) + .await; + repl_session.set_last_eval_result(&result).await?; + } + + if output { + let response = repl_session + .call_function_on_repl_internal_obj( + "function (object) { return this.String(object); }".into(), + &[result], + ) + .await?; + let output = response + .result + .value + .map(|v| v.as_str().unwrap().to_string()) + .or(response.result.description) + .unwrap_or_else(|| "something went wrong".into()); + + let buf = serde_json::to_vec(&ReplMessage::RunOutput { output })?; + sender + .write_all_buf( + &mut Bytes::from_owner((buf.len() as u32).to_le_bytes()) + .chain(Bytes::from(buf)), + ) + .await?; + } + } + Err(err) => { + let buf = serde_json::to_vec(&ReplMessage::Error { + error: format!("{}", err), + })?; + sender + .write_all_buf( + &mut Bytes::from_owner((buf.len() as u32).to_le_bytes()) + .chain(Bytes::from(buf)), + ) + .await?; + } + } + } + } + + Ok(repl_session.worker.exit_code()) +} diff --git a/cli/tools/repl/session.rs b/cli/tools/repl/session.rs index 34a1d62232..ae32735d02 100644 --- a/cli/tools/repl/session.rs +++ b/cli/tools/repl/session.rs @@ -87,18 +87,20 @@ fn comment_source_to_position_range( fn get_prelude() -> String { r#"(() => { const repl_internal = { - lastEvalResult: undefined, - lastThrownError: undefined, - inspectArgs: Deno[Deno.internal].inspectArgs, - noColor: Deno.noColor, - get closed() { - try { - return typeof globalThis.closed === 'undefined' ? false : globalThis.closed; - } catch { - return false; - } + String, + lastEvalResult: undefined, + lastThrownError: undefined, + inspectArgs: Deno[Deno.internal].inspectArgs, + noColor: Deno.noColor, + get closed() { + try { + return typeof globalThis.closed === 'undefined' ? false : globalThis.closed; + } catch { + return false; } } + }; + Object.defineProperty(globalThis, "_", { configurable: true, get: () => repl_internal.lastEvalResult, @@ -130,7 +132,7 @@ fn get_prelude() -> String { globalThis.clear = console.clear.bind(console); - return repl_internal + return repl_internal; })()"#.to_string() } @@ -488,7 +490,7 @@ impl ReplSession { result } - async fn set_last_thrown_error( + pub async fn set_last_thrown_error( &mut self, error: &cdp::RemoteObject, ) -> Result<(), AnyError> { @@ -515,7 +517,7 @@ impl ReplSession { Ok(()) } - async fn set_last_eval_result( + pub async fn set_last_eval_result( &mut self, evaluate_result: &cdp::RemoteObject, ) -> Result<(), AnyError> { diff --git a/ext/process/40_process.js b/ext/process/40_process.js index b09cee8039..87fcbae879 100644 --- a/ext/process/40_process.js +++ b/ext/process/40_process.js @@ -229,6 +229,7 @@ const _extraPipeRids = Symbol("[[_extraPipeRids]]"); internals.getIpcPipeRid = (process) => process[_ipcPipeRid]; internals.getExtraPipeRids = (process) => process[_extraPipeRids]; +internals.kExtraStdio = kExtraStdio; class ChildProcess { #rid; diff --git a/ext/web/06_streams.js b/ext/web/06_streams.js index d2b28e6599..1a8ef1ba7c 100644 --- a/ext/web/06_streams.js +++ b/ext/web/06_streams.js @@ -6904,6 +6904,8 @@ webidl.converters["async iterable"] = webidl.createAsyncIterableConverter( ); internals.resourceForReadableStream = resourceForReadableStream; +internals.readableStreamForRid = readableStreamForRid; +internals.writableStreamForRid = writableStreamForRid; export default { // Non-Public diff --git a/tests/specs/repl/json/__test__.jsonc b/tests/specs/repl/json/__test__.jsonc new file mode 100644 index 0000000000..589a47b51d --- /dev/null +++ b/tests/specs/repl/json/__test__.jsonc @@ -0,0 +1,5 @@ +{ + "if": "unix", + "args": "run -A --quiet test.ts", + "output": "test.out" +} diff --git a/tests/specs/repl/json/test.out b/tests/specs/repl/json/test.out new file mode 100644 index 0000000000..f654329e3f --- /dev/null +++ b/tests/specs/repl/json/test.out @@ -0,0 +1,2 @@ +hello +2 diff --git a/tests/specs/repl/json/test.ts b/tests/specs/repl/json/test.ts new file mode 100644 index 0000000000..ab9ea8232b --- /dev/null +++ b/tests/specs/repl/json/test.ts @@ -0,0 +1,49 @@ +const { + kExtraStdio, + getExtraPipeRids, + writableStreamForRid, + readableStreamForRid, +} = Deno[Deno.internal]; + +const command = new Deno.Command(Deno.execPath(), { + args: [ + "repl", + "--json", + ], + stdio: "null", + stderr: "inherit", + stdout: "inherit", + [kExtraStdio]: ["piped"], +}); + +await using child = command.spawn(); + +const pipeRid = getExtraPipeRids(child)[0]; +const writable = writableStreamForRid(pipeRid); +const readable = readableStreamForRid(pipeRid); + +{ + const writer = writable.getWriter(); + const buf = new TextEncoder().encode( + JSON.stringify({ + type: "Run", + code: "console.log('hello'); 1 + 1", + output: true, + }), + ); + const u32 = new Uint8Array(4); + new DataView(u32.buffer).setUint32(0, buf.length, true); + await writer.write(u32); + await writer.write(buf); + writer.releaseLock(); +} + +{ + const reader = readable.getReader({ mode: "byob" }); + const { value: u32 } = await reader.read(new Uint8Array(4), { min: 4 }); + const { value } = await reader.read( + new Uint8Array(new DataView(u32.buffer).getUint32(0, true)), + ); + console.log(JSON.parse(new TextDecoder().decode(value)).output); + reader.releaseLock(); +}