From 3dc0e45024b3866a297d48c05f7b203cb423a8c2 Mon Sep 17 00:00:00 2001 From: Ryan Dahl Date: Fri, 2 Oct 2020 12:26:36 -0400 Subject: [PATCH] Revert "refactor(repl): use an inspector session (#7763)" This reverts commit 4c779b5e8ca427faf24c26443a8054004827d450. --- Cargo.lock | 11 -- cli/Cargo.toml | 1 - cli/flags.rs | 4 - cli/inspector.rs | 1 - cli/main.rs | 22 +-- cli/ops/mod.rs | 1 + cli/ops/repl.rs | 78 +++++++++ cli/ops/runtime.rs | 2 + cli/repl.rs | 298 +++++++-------------------------- cli/rt/40_repl.js | 197 ++++++++++++++++++++++ cli/rt/99_main.js | 8 +- cli/tests/integration_tests.rs | 5 +- cli/worker.rs | 3 +- 13 files changed, 349 insertions(+), 282 deletions(-) create mode 100644 cli/ops/repl.rs create mode 100644 cli/rt/40_repl.js diff --git a/Cargo.lock b/Cargo.lock index 196cb4e7f7d526..fd4c0fbc089669 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -426,7 +426,6 @@ dependencies = [ "regex", "ring", "rustyline", - "rustyline-derive", "semver-parser 0.9.0", "serde", "sourcemap", @@ -1947,16 +1946,6 @@ dependencies = [ "winapi 0.3.9", ] -[[package]] -name = "rustyline-derive" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54a50e29610a5be68d4a586a5cce3bfb572ed2c2a74227e4168444b7bf4e5235" -dependencies = [ - "quote 1.0.7", - "syn 1.0.41", -] - [[package]] name = "ryu" version = "1.0.5" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 2bed17fbfa6d0e..1c265d56d98902 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -57,7 +57,6 @@ rand = "0.7.3" regex = "1.3.9" ring = "0.16.15" rustyline = { version = "6.3.0", default-features = false } -rustyline-derive = "0.3.1" serde = { version = "1.0.116", features = ["derive"] } sys-info = "0.7.0" sourcemap = "6.0.1" diff --git a/cli/flags.rs b/cli/flags.rs index 06fbafdc3d4238..af7bc2a08a8018 100644 --- a/cli/flags.rs +++ b/cli/flags.rs @@ -120,7 +120,6 @@ pub struct Flags { pub no_remote: bool, pub read_allowlist: Vec, pub reload: bool, - pub repl: bool, pub seed: Option, pub unstable: bool, pub v8_flags: Option>, @@ -448,7 +447,6 @@ fn completions_parse(flags: &mut Flags, matches: &clap::ArgMatches) { fn repl_parse(flags: &mut Flags, matches: &clap::ArgMatches) { runtime_args_parse(flags, matches, false); - flags.repl = true; flags.subcommand = DenoSubcommand::Repl; flags.allow_net = true; flags.allow_env = true; @@ -2144,7 +2142,6 @@ mod tests { assert_eq!( r.unwrap(), Flags { - repl: true, subcommand: DenoSubcommand::Repl, allow_net: true, allow_env: true, @@ -2165,7 +2162,6 @@ mod tests { assert_eq!( r.unwrap(), Flags { - repl: true, subcommand: DenoSubcommand::Repl, unstable: true, import_map_path: Some("import_map.json".to_string()), diff --git a/cli/inspector.rs b/cli/inspector.rs index 171be512b8b108..83e75bce4dc0d2 100644 --- a/cli/inspector.rs +++ b/cli/inspector.rs @@ -857,7 +857,6 @@ impl v8::inspector::ChannelImpl for InspectorSession { ) { let raw_message = message.unwrap().string().to_string(); let message = serde_json::from_str(&raw_message).unwrap(); - self .response_tx_map .remove(&call_id) diff --git a/cli/main.rs b/cli/main.rs index 4546bc3748496b..f888a6c6cf5a1c 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -63,7 +63,6 @@ use crate::file_fetcher::SourceFileFetcher; use crate::file_fetcher::TextDocument; use crate::fs as deno_fs; use crate::global_state::GlobalState; -use crate::inspector::InspectorSession; use crate::media_type::MediaType; use crate::permissions::Permissions; use crate::worker::MainWorker; @@ -429,26 +428,9 @@ async fn run_repl(flags: Flags) -> Result<(), AnyError> { let main_module = ModuleSpecifier::resolve_url_or_path("./$deno$repl.ts").unwrap(); let global_state = GlobalState::new(flags)?; - let mut worker = MainWorker::new(&global_state, main_module.clone()); - (&mut *worker).await?; - - let inspector = worker - .inspector - .as_mut() - .expect("Inspector is not created."); - - let inspector_session = InspectorSession::new(&mut **inspector); - let repl = repl::run(&global_state, inspector_session); - - tokio::pin!(repl); - + let mut worker = MainWorker::new(&global_state, main_module); loop { - tokio::select! { - result = &mut repl => { - return result; - } - _ = &mut *worker => {} - } + (&mut *worker).await?; } } diff --git a/cli/ops/mod.rs b/cli/ops/mod.rs index b1ec5c344f6f8b..c6d5cc1dca180f 100644 --- a/cli/ops/mod.rs +++ b/cli/ops/mod.rs @@ -16,6 +16,7 @@ pub mod permissions; pub mod plugin; pub mod process; pub mod random; +pub mod repl; pub mod runtime; pub mod runtime_compiler; pub mod signal; diff --git a/cli/ops/repl.rs b/cli/ops/repl.rs new file mode 100644 index 00000000000000..a2c26b2abaaf85 --- /dev/null +++ b/cli/ops/repl.rs @@ -0,0 +1,78 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +use crate::repl; +use crate::repl::Repl; +use deno_core::error::bad_resource_id; +use deno_core::error::AnyError; +use deno_core::serde_json; +use deno_core::serde_json::json; +use deno_core::serde_json::Value; +use deno_core::BufVec; +use deno_core::OpState; +use deno_core::ZeroCopyBuf; +use serde::Deserialize; +use std::cell::RefCell; +use std::rc::Rc; +use std::sync::Arc; +use std::sync::Mutex; + +pub fn init(rt: &mut deno_core::JsRuntime) { + super::reg_json_sync(rt, "op_repl_start", op_repl_start); + super::reg_json_async(rt, "op_repl_readline", op_repl_readline); +} + +struct ReplResource(Arc>); + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct ReplStartArgs { + history_file: String, +} + +fn op_repl_start( + state: &mut OpState, + args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + let args: ReplStartArgs = serde_json::from_value(args)?; + debug!("op_repl_start {}", args.history_file); + let history_path = { + let cli_state = super::global_state(state); + repl::history_path(&cli_state.dir, &args.history_file) + }; + let repl = repl::Repl::new(history_path); + let resource = ReplResource(Arc::new(Mutex::new(repl))); + let rid = state.resource_table.add("repl", Box::new(resource)); + Ok(json!(rid)) +} + +#[derive(Deserialize)] +struct ReplReadlineArgs { + rid: i32, + prompt: String, +} + +async fn op_repl_readline( + state: Rc>, + args: Value, + _zero_copy: BufVec, +) -> Result { + let args: ReplReadlineArgs = serde_json::from_value(args)?; + let rid = args.rid as u32; + let prompt = args.prompt; + debug!("op_repl_readline {} {}", rid, prompt); + let repl = { + let state = state.borrow(); + let resource = state + .resource_table + .get::(rid) + .ok_or_else(bad_resource_id)?; + resource.0.clone() + }; + tokio::task::spawn_blocking(move || { + let line = repl.lock().unwrap().readline(&prompt)?; + Ok(json!(line)) + }) + .await + .unwrap() +} diff --git a/cli/ops/runtime.rs b/cli/ops/runtime.rs index 3f73984791c27f..b1eddc2654294c 100644 --- a/cli/ops/runtime.rs +++ b/cli/ops/runtime.rs @@ -4,6 +4,7 @@ use crate::colors; use crate::metrics::Metrics; use crate::permissions::Permissions; use crate::version; +use crate::DenoSubcommand; use deno_core::error::AnyError; use deno_core::serde_json; use deno_core::serde_json::json; @@ -40,6 +41,7 @@ fn op_start( "noColor": !colors::use_color(), "pid": std::process::id(), "ppid": ppid(), + "repl": gs.flags.subcommand == DenoSubcommand::Repl, "target": env!("TARGET"), "tsVersion": version::TYPESCRIPT, "unstableFlag": gs.flags.unstable, diff --git a/cli/repl.rs b/cli/repl.rs index 5100706359b807..7873f7d0fc5a36 100644 --- a/cli/repl.rs +++ b/cli/repl.rs @@ -1,255 +1,73 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. -use crate::global_state::GlobalState; -use crate::inspector::InspectorSession; +use crate::deno_dir::DenoDir; use deno_core::error::AnyError; -use deno_core::serde_json::json; -use rustyline::error::ReadlineError; -use rustyline::validate::MatchingBracketValidator; -use rustyline::validate::ValidationContext; -use rustyline::validate::ValidationResult; -use rustyline::validate::Validator; use rustyline::Editor; -use rustyline_derive::{Completer, Helper, Highlighter, Hinter}; -use std::sync::Arc; -use std::sync::Mutex; +use std::fs; +use std::path::PathBuf; -// Provides syntax specific helpers to the editor like validation for multi-line edits. -#[derive(Completer, Helper, Highlighter, Hinter)] -struct Helper { - validator: MatchingBracketValidator, +pub struct Repl { + editor: Editor<()>, + history_file: PathBuf, } -impl Validator for Helper { - fn validate( - &self, - ctx: &mut ValidationContext, - ) -> Result { - self.validator.validate(ctx) - } -} - -pub async fn run( - global_state: &GlobalState, - mut session: Box, -) -> Result<(), AnyError> { - // Our inspector is unable to default to the default context id so we have to specify it here. - let context_id: u32 = 1; - - let history_file = global_state.dir.root.join("deno_history.txt"); - - session - .post_message("Runtime.enable".to_string(), None) - .await?; - - let helper = Helper { - validator: MatchingBracketValidator::new(), - }; - - let editor = Arc::new(Mutex::new(Editor::new())); - - editor.lock().unwrap().set_helper(Some(helper)); - - editor - .lock() - .unwrap() - .load_history(history_file.to_str().unwrap()) - .unwrap_or(()); - - println!("Deno {}", crate::version::DENO); - println!("exit using ctrl+d or close()"); - - let prelude = r#" - Object.defineProperty(globalThis, "_", { - configurable: true, - get: () => Deno[Deno.internal].lastEvalResult, - set: (value) => { - Object.defineProperty(globalThis, "_", { - value: value, - writable: true, - enumerable: true, - configurable: true, - }); - console.log("Last evaluation result is no longer saved to _."); - }, - }); - - Object.defineProperty(globalThis, "_error", { - configurable: true, - get: () => Deno[Deno.internal].lastThrownError, - set: (value) => { - Object.defineProperty(globalThis, "_error", { - value: value, - writable: true, - enumerable: true, - configurable: true, - }); - - console.log("Last thrown error is no longer saved to _error."); - }, - }); - "#; - - session - .post_message( - "Runtime.evaluate".to_string(), - Some(json!({ - "expression": prelude, - "contextId": context_id, - })), - ) - .await?; +impl Repl { + pub fn new(history_file: PathBuf) -> Self { + let mut repl = Self { + editor: Editor::<()>::new(), + history_file, + }; - loop { - let editor2 = editor.clone(); - let line = tokio::task::spawn_blocking(move || { - editor2.lock().unwrap().readline("> ") - }) - .await?; - - match line { - Ok(line) => { - // It is a bit unexpected that { "foo": "bar" } is interpreted as a block - // statement rather than an object literal so we interpret it as an expression statement - // to match the behavior found in a typical prompt including browser developer tools. - let wrapped_line = if line.trim_start().starts_with('{') - && !line.trim_end().ends_with(';') - { - format!("({})", &line) - } else { - line.clone() - }; - - let evaluate_response = session - .post_message( - "Runtime.evaluate".to_string(), - Some(json!({ - "expression": format!("'use strict'; void 0;\n{}", &wrapped_line), - "contextId": context_id, - // TODO(caspervonb) set repl mode to true to enable const redeclarations and top - // level await - "replMode": false, - })), - ) - .await?; - - // If that fails, we retry it without wrapping in parens letting the error bubble up to the - // user if it is still an error. - let evaluate_response = - if evaluate_response.get("exceptionDetails").is_some() - && wrapped_line != line - { - session - .post_message( - "Runtime.evaluate".to_string(), - Some(json!({ - "expression": format!("'use strict'; void 0;\n{}", &line), - "contextId": context_id, - // TODO(caspervonb) set repl mode to true to enable const redeclarations and top - // level await - "replMode": false, - })), - ) - .await? - } else { - evaluate_response - }; - - let is_closing = session - .post_message( - "Runtime.evaluate".to_string(), - Some(json!({ - "expression": "(globalThis.closed)", - "contextId": context_id, - })), - ) - .await? - .get("result") - .unwrap() - .get("value") - .unwrap() - .as_bool() - .unwrap(); - - if is_closing { - break; - } - - let evaluate_result = evaluate_response.get("result").unwrap(); - let evaluate_exception_details = - evaluate_response.get("exceptionDetails"); - - if evaluate_exception_details.is_some() { - session - .post_message( - "Runtime.callFunctionOn".to_string(), - Some(json!({ - "executionContextId": context_id, - "functionDeclaration": "function (object) { Deno[Deno.internal].lastThrownError = object; }", - "arguments": [ - evaluate_result, - ], - }))).await?; - } else { - session - .post_message( - "Runtime.callFunctionOn".to_string(), - Some(json!({ - "executionContextId": context_id, - "functionDeclaration": "function (object) { Deno[Deno.internal].lastEvalResult = object; }", - "arguments": [ - evaluate_result, - ], - }))).await?; - } - - // TODO(caspervonb) we should investigate using previews here but to keep things - // consistent with the previous implementation we just get the preview result from - // Deno.inspectArgs. - let inspect_response = session - .post_message( - "Runtime.callFunctionOn".to_string(), - Some(json!({ - "executionContextId": context_id, - "functionDeclaration": "function (object) { return Deno[Deno.internal].inspectArgs(['%o', object], { colors: true}); }", - "arguments": [ - evaluate_result, - ], - }))).await?; + repl.load_history(); + repl + } - let inspect_result = inspect_response.get("result").unwrap(); + fn load_history(&mut self) { + debug!("Loading REPL history: {:?}", self.history_file); + self + .editor + .load_history(&self.history_file.to_str().unwrap()) + .map_err(|e| { + debug!("Unable to load history file: {:?} {}", self.history_file, e) + }) + // ignore this error (e.g. it occurs on first load) + .unwrap_or(()) + } - match evaluate_exception_details { - Some(_) => eprintln!( - "Uncaught {}", - inspect_result.get("value").unwrap().as_str().unwrap() - ), - None => println!( - "{}", - inspect_result.get("value").unwrap().as_str().unwrap() - ), - } + fn save_history(&mut self) -> Result<(), AnyError> { + fs::create_dir_all(self.history_file.parent().unwrap())?; + self + .editor + .save_history(&self.history_file.to_str().unwrap()) + .map(|_| debug!("Saved REPL history to: {:?}", self.history_file)) + .map_err(|e| { + eprintln!("Unable to save REPL history: {:?} {}", self.history_file, e); + e.into() + }) + } - editor.lock().unwrap().add_history_entry(line.as_str()); - } - Err(ReadlineError::Interrupted) => { - break; - } - Err(ReadlineError::Eof) => { - break; - } - Err(err) => { - println!("Error: {:?}", err); - break; - } - } + pub fn readline(&mut self, prompt: &str) -> Result { + self + .editor + .readline(&prompt) + .map(|line| { + self.editor.add_history_entry(line.clone()); + line + }) + .map_err(AnyError::from) + + // Forward error to TS side for processing } +} - std::fs::create_dir_all(history_file.parent().unwrap())?; - editor - .lock() - .unwrap() - .save_history(history_file.to_str().unwrap())?; +impl Drop for Repl { + fn drop(&mut self) { + self.save_history().unwrap(); + } +} - Ok(()) +pub fn history_path(dir: &DenoDir, history_file: &str) -> PathBuf { + let mut p: PathBuf = dir.root.clone(); + p.push(history_file); + p } diff --git a/cli/rt/40_repl.js b/cli/rt/40_repl.js new file mode 100644 index 00000000000000..a249b578d8065d --- /dev/null +++ b/cli/rt/40_repl.js @@ -0,0 +1,197 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const core = window.Deno.core; + const exit = window.__bootstrap.os.exit; + const version = window.__bootstrap.version.version; + const inspectArgs = window.__bootstrap.console.inspectArgs; + + function opStartRepl(historyFile) { + return core.jsonOpSync("op_repl_start", { historyFile }); + } + + function opReadline(rid, prompt) { + return core.jsonOpAsync("op_repl_readline", { rid, prompt }); + } + + function replLog(...args) { + core.print(inspectArgs(args) + "\n"); + } + + function replError(...args) { + core.print(inspectArgs(args) + "\n", true); + } + + // Error messages that allow users to continue input + // instead of throwing an error to REPL + // ref: https://github.com/v8/v8/blob/master/src/message-template.h + // TODO(kevinkassimo): this list might not be comprehensive + const recoverableErrorMessages = [ + "Unexpected end of input", // { or [ or ( + "Missing initializer in const declaration", // const a + "Missing catch or finally after try", // try {} + "missing ) after argument list", // console.log(1 + "Unterminated template literal", // `template + // TODO(kevinkassimo): need a parser to handling errors such as: + // "Missing } in template expression" // `${ or `${ a 123 }` + ]; + + function isRecoverableError(e) { + return recoverableErrorMessages.includes(e.message); + } + + // Returns `true` if `close()` is called in REPL. + // We should quit the REPL when this function returns `true`. + function isCloseCalled() { + return globalThis.closed; + } + + let lastEvalResult = undefined; + let lastThrownError = undefined; + + // Evaluate code. + // Returns true if code is consumed (no error/irrecoverable error). + // Returns false if error is recoverable + function evaluate(code, preprocess = true) { + const rawCode = code; + if (preprocess) { + // It is a bit unexpected that { "foo": "bar" } is interpreted as a block + // statement rather than an object literal so we interpret it as an expression statement + // to match the behavior found in a typical prompt including browser developer tools. + if (code.trimLeft().startsWith("{") && !code.trimRight().endsWith(";")) { + code = `(${code})`; + } + } + + // each evalContext is a separate function body, and we want strict mode to + // work, so we should ensure that the code starts with "use strict" + const [result, errInfo] = core.evalContext(`"use strict";\n\n${code}`); + + if (!errInfo) { + // when a function is eval'ed with just "use strict" sometimes the result + // is "use strict" which should be discarded + lastEvalResult = typeof result === "string" && result === "use strict" + ? undefined + : result; + if (!isCloseCalled()) { + replLog("%o", lastEvalResult); + } + } else if (errInfo.isCompileError && code.length != rawCode.length) { + return evaluate(rawCode, false); + } else if (errInfo.isCompileError && isRecoverableError(errInfo.thrown)) { + // Recoverable compiler error + return false; // don't consume code. + } else { + lastThrownError = errInfo.thrown; + if (errInfo.isNativeError) { + const formattedError = core.formatError(errInfo.thrown); + replError(formattedError); + } else { + replError("Thrown:", errInfo.thrown); + } + } + return true; + } + + async function replLoop() { + const { console } = globalThis; + + const historyFile = "deno_history.txt"; + const rid = opStartRepl(historyFile); + + const quitRepl = (exitCode) => { + // Special handling in case user calls deno.close(3). + try { + core.close(rid); // close signals Drop on REPL and saves history. + } catch {} + exit(exitCode); + }; + + // Configure globalThis._ to give the last evaluation result. + Object.defineProperty(globalThis, "_", { + configurable: true, + get: () => lastEvalResult, + set: (value) => { + Object.defineProperty(globalThis, "_", { + value: value, + writable: true, + enumerable: true, + configurable: true, + }); + console.log("Last evaluation result is no longer saved to _."); + }, + }); + + // Configure globalThis._error to give the last thrown error. + Object.defineProperty(globalThis, "_error", { + configurable: true, + get: () => lastThrownError, + set: (value) => { + Object.defineProperty(globalThis, "_error", { + value: value, + writable: true, + enumerable: true, + configurable: true, + }); + console.log("Last thrown error is no longer saved to _error."); + }, + }); + + replLog(`Deno ${version.deno}`); + replLog("exit using ctrl+d or close()"); + + while (true) { + if (isCloseCalled()) { + quitRepl(0); + } + + let code = ""; + // Top level read + try { + code = await opReadline(rid, "> "); + if (code.trim() === "") { + continue; + } + } catch (err) { + if (err.message === "EOF") { + quitRepl(0); + } else { + // If interrupted, don't print error. + if (err.message !== "Interrupted") { + // e.g. this happens when we have deno.close(3). + // We want to display the problem. + const formattedError = core.formatError(err); + replError(formattedError); + } + // Quit REPL anyways. + quitRepl(1); + } + } + // Start continued read + while (!evaluate(code)) { + code += "\n"; + try { + code += await opReadline(rid, " "); + } catch (err) { + // If interrupted on continued read, + // abort this read instead of quitting. + if (err.message === "Interrupted") { + break; + } else if (err.message === "EOF") { + quitRepl(0); + } else { + // e.g. this happens when we have deno.close(3). + // We want to display the problem. + const formattedError = core.formatError(err); + replError(formattedError); + quitRepl(1); + } + } + } + } + } + + window.__bootstrap.repl = { + replLoop, + }; +})(this); diff --git a/cli/rt/99_main.js b/cli/rt/99_main.js index d8462ce662f0b0..26e8fd6da096a6 100644 --- a/cli/rt/99_main.js +++ b/cli/rt/99_main.js @@ -14,6 +14,7 @@ delete Object.prototype.__proto__; const errorStack = window.__bootstrap.errorStack; const os = window.__bootstrap.os; const timers = window.__bootstrap.timers; + const replLoop = window.__bootstrap.repl.replLoop; const Console = window.__bootstrap.console.Console; const worker = window.__bootstrap.worker; const signals = window.__bootstrap.signals; @@ -293,7 +294,8 @@ delete Object.prototype.__proto__; } }); - const { args, cwd, noColor, pid, ppid, unstableFlag } = runtimeStart(); + const { args, cwd, noColor, pid, ppid, repl, unstableFlag } = + runtimeStart(); registerErrors(); @@ -327,6 +329,10 @@ delete Object.prototype.__proto__; util.log("cwd", cwd); util.log("args", args); + + if (repl) { + replLoop(); + } } function bootstrapWorkerRuntime(name, useDenoNamespace, internalName) { diff --git a/cli/tests/integration_tests.rs b/cli/tests/integration_tests.rs index 20e43ca298a067..2cae5ea9807fbe 100644 --- a/cli/tests/integration_tests.rs +++ b/cli/tests/integration_tests.rs @@ -1239,7 +1239,6 @@ fn repl_test_function() { } #[test] -#[ignore] fn repl_test_multiline() { let (out, err) = util::run_and_collect_output( true, @@ -1375,7 +1374,7 @@ fn repl_test_save_last_thrown() { false, ); assert!(out.ends_with("1\n")); - assert_eq!(err, "Uncaught 1\n"); + assert_eq!(err, "Thrown: 1\n"); } #[test] @@ -1405,7 +1404,7 @@ fn repl_test_assign_underscore_error() { assert!( out.ends_with("Last thrown error is no longer saved to _error.\n1\n1\n") ); - assert_eq!(err, "Uncaught 2\n"); + assert_eq!(err, "Thrown: 2\n"); } #[test] diff --git a/cli/worker.rs b/cli/worker.rs index 08ccd418e5d479..242a2f4b336d83 100644 --- a/cli/worker.rs +++ b/cli/worker.rs @@ -134,7 +134,7 @@ impl Worker { &mut isolate, Some(inspector_server.clone()), )) - } else if global_state.flags.coverage || global_state.flags.repl { + } else if global_state.flags.coverage { Some(DenoInspector::new(&mut isolate, None)) } else { None @@ -309,6 +309,7 @@ impl MainWorker { ops::permissions::init(&mut worker); ops::plugin::init(&mut worker); ops::process::init(&mut worker); + ops::repl::init(&mut worker); ops::runtime_compiler::init(&mut worker); ops::signal::init(&mut worker); ops::tls::init(&mut worker);