From 1713df13524ad20c6bd0413bcf4aa57adc3f9735 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Tue, 31 Oct 2023 01:25:58 +0100 Subject: feat: deno run --unstable-hmr (#20876) This commit adds `--unstable-hmr` flag, that enabled Hot Module Replacement. This flag works like `--watch` and accepts the same arguments. If HMR is not possible the process will be restarted instead. Currently HMR is only supported in `deno run` subcommand. Upon HMR a `CustomEvent("hmr")` will be dispatched that contains information which file was changed in its `details` property. --------- Co-authored-by: Valentin Anger Co-authored-by: David Sherret --- cli/tools/bench/mod.rs | 8 +- cli/tools/bundle.rs | 8 +- cli/tools/fmt.rs | 7 +- cli/tools/lint.rs | 5 +- cli/tools/run.rs | 195 -------------------------------- cli/tools/run/hmr/json_types.rs | 59 ++++++++++ cli/tools/run/hmr/mod.rs | 242 ++++++++++++++++++++++++++++++++++++++++ cli/tools/run/mod.rs | 205 ++++++++++++++++++++++++++++++++++ cli/tools/test/mod.rs | 8 +- 9 files changed, 521 insertions(+), 216 deletions(-) delete mode 100644 cli/tools/run.rs create mode 100644 cli/tools/run/hmr/json_types.rs create mode 100644 cli/tools/run/hmr/mod.rs create mode 100644 cli/tools/run/mod.rs (limited to 'cli/tools') diff --git a/cli/tools/bench/mod.rs b/cli/tools/bench/mod.rs index eb400442e..70551a767 100644 --- a/cli/tools/bench/mod.rs +++ b/cli/tools/bench/mod.rs @@ -409,14 +409,14 @@ pub async fn run_benchmarks_with_watch( ) -> Result<(), AnyError> { file_watcher::watch_func( flags, - file_watcher::PrintConfig { - job_name: "Bench".to_string(), - clear_screen: bench_flags + file_watcher::PrintConfig::new( + "Bench", + bench_flags .watch .as_ref() .map(|w| !w.no_clear_screen) .unwrap_or(true), - }, + ), move |flags, watcher_communicator, changed_paths| { let bench_flags = bench_flags.clone(); Ok(async move { diff --git a/cli/tools/bundle.rs b/cli/tools/bundle.rs index b36ff023a..0946c728b 100644 --- a/cli/tools/bundle.rs +++ b/cli/tools/bundle.rs @@ -31,10 +31,10 @@ pub async fn bundle( if let Some(watch_flags) = &bundle_flags.watch { util::file_watcher::watch_func( flags, - util::file_watcher::PrintConfig { - job_name: "Bundle".to_string(), - clear_screen: !watch_flags.no_clear_screen, - }, + util::file_watcher::PrintConfig::new( + "Bundle", + !watch_flags.no_clear_screen, + ), move |flags, watcher_communicator, _changed_paths| { let bundle_flags = bundle_flags.clone(); Ok(async move { diff --git a/cli/tools/fmt.rs b/cli/tools/fmt.rs index 92facc7ec..5c47b5497 100644 --- a/cli/tools/fmt.rs +++ b/cli/tools/fmt.rs @@ -64,10 +64,7 @@ pub async fn format(flags: Flags, fmt_flags: FmtFlags) -> Result<(), AnyError> { if let Some(watch_flags) = &fmt_flags.watch { file_watcher::watch_func( flags, - file_watcher::PrintConfig { - job_name: "Fmt".to_string(), - clear_screen: !watch_flags.no_clear_screen, - }, + file_watcher::PrintConfig::new("Fmt", !watch_flags.no_clear_screen), move |flags, watcher_communicator, changed_paths| { let fmt_flags = fmt_flags.clone(); Ok(async move { @@ -82,7 +79,7 @@ pub async fn format(flags: Flags, fmt_flags: FmtFlags) -> Result<(), AnyError> { Ok(files) } })?; - _ = watcher_communicator.watch_paths(files.clone()); + let _ = watcher_communicator.watch_paths(files.clone()); let refmt_files = if let Some(paths) = changed_paths { if fmt_options.check { // check all files on any changed (https://github.com/denoland/deno/issues/12446) diff --git a/cli/tools/lint.rs b/cli/tools/lint.rs index b7f4a3f0d..5b9387eb1 100644 --- a/cli/tools/lint.rs +++ b/cli/tools/lint.rs @@ -59,10 +59,7 @@ pub async fn lint(flags: Flags, lint_flags: LintFlags) -> Result<(), AnyError> { } file_watcher::watch_func( flags, - file_watcher::PrintConfig { - job_name: "Lint".to_string(), - clear_screen: !watch_flags.no_clear_screen, - }, + file_watcher::PrintConfig::new("Lint", !watch_flags.no_clear_screen), move |flags, watcher_communicator, changed_paths| { let lint_flags = lint_flags.clone(); Ok(async move { diff --git a/cli/tools/run.rs b/cli/tools/run.rs deleted file mode 100644 index 80e80577e..000000000 --- a/cli/tools/run.rs +++ /dev/null @@ -1,195 +0,0 @@ -// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. - -use std::io::Read; - -use deno_ast::MediaType; -use deno_core::error::AnyError; -use deno_runtime::permissions::Permissions; -use deno_runtime::permissions::PermissionsContainer; - -use crate::args::EvalFlags; -use crate::args::Flags; -use crate::args::RunFlags; -use crate::args::WatchFlagsWithPaths; -use crate::factory::CliFactory; -use crate::factory::CliFactoryBuilder; -use crate::file_fetcher::File; -use crate::util; - -pub async fn run_script( - flags: Flags, - run_flags: RunFlags, -) -> Result { - if !flags.has_permission() && flags.has_permission_in_argv() { - log::warn!( - "{}", - crate::colors::yellow( - r#"Permission flags have likely been incorrectly set after the script argument. -To grant permissions, set them before the script argument. For example: - deno run --allow-read=. main.js"# - ) - ); - } - - if let Some(watch_flags) = run_flags.watch { - return run_with_watch(flags, watch_flags).await; - } - - // TODO(bartlomieju): actually I think it will also fail if there's an import - // map specified and bare specifier is used on the command line - let factory = CliFactory::from_flags(flags).await?; - let deno_dir = factory.deno_dir()?; - let http_client = factory.http_client(); - let cli_options = factory.cli_options(); - - // Run a background task that checks for available upgrades. If an earlier - // run of this background task found a new version of Deno. - super::upgrade::check_for_upgrades( - http_client.clone(), - deno_dir.upgrade_check_file_path(), - ); - - let main_module = cli_options.resolve_main_module()?; - - maybe_npm_install(&factory).await?; - - let permissions = PermissionsContainer::new(Permissions::from_options( - &cli_options.permissions_options(), - )?); - let worker_factory = factory.create_cli_main_worker_factory().await?; - let mut worker = worker_factory - .create_main_worker(main_module, permissions) - .await?; - - let exit_code = worker.run().await?; - Ok(exit_code) -} - -pub async fn run_from_stdin(flags: Flags) -> Result { - let factory = CliFactory::from_flags(flags).await?; - let cli_options = factory.cli_options(); - let main_module = cli_options.resolve_main_module()?; - - maybe_npm_install(&factory).await?; - - let file_fetcher = factory.file_fetcher()?; - let worker_factory = factory.create_cli_main_worker_factory().await?; - let permissions = PermissionsContainer::new(Permissions::from_options( - &cli_options.permissions_options(), - )?); - let mut source = Vec::new(); - std::io::stdin().read_to_end(&mut source)?; - // Create a dummy source file. - let source_file = File { - maybe_types: None, - media_type: MediaType::TypeScript, - source: String::from_utf8(source)?.into(), - specifier: main_module.clone(), - maybe_headers: None, - }; - // Save our fake file into file fetcher cache - // to allow module access by TS compiler - file_fetcher.insert_cached(source_file); - - let mut worker = worker_factory - .create_main_worker(main_module, permissions) - .await?; - let exit_code = worker.run().await?; - Ok(exit_code) -} - -// TODO(bartlomieju): this function is not handling `exit_code` set by the runtime -// code properly. -async fn run_with_watch( - flags: Flags, - watch_flags: WatchFlagsWithPaths, -) -> Result { - util::file_watcher::watch_func( - flags, - util::file_watcher::PrintConfig { - job_name: "Process".to_string(), - clear_screen: !watch_flags.no_clear_screen, - }, - move |flags, watcher_communicator, _changed_paths| { - Ok(async move { - let factory = CliFactoryBuilder::new() - .build_from_flags_for_watcher(flags, watcher_communicator.clone()) - .await?; - let cli_options = factory.cli_options(); - let main_module = cli_options.resolve_main_module()?; - - maybe_npm_install(&factory).await?; - - let _ = watcher_communicator.watch_paths(cli_options.watch_paths()); - - let permissions = PermissionsContainer::new(Permissions::from_options( - &cli_options.permissions_options(), - )?); - let worker = factory - .create_cli_main_worker_factory() - .await? - .create_main_worker(main_module, permissions) - .await?; - worker.run_for_watcher().await?; - - Ok(()) - }) - }, - ) - .await?; - - Ok(0) -} - -pub async fn eval_command( - flags: Flags, - eval_flags: EvalFlags, -) -> Result { - let factory = CliFactory::from_flags(flags).await?; - let cli_options = factory.cli_options(); - let file_fetcher = factory.file_fetcher()?; - let main_module = cli_options.resolve_main_module()?; - - maybe_npm_install(&factory).await?; - - // Create a dummy source file. - let source_code = if eval_flags.print { - format!("console.log({})", eval_flags.code) - } else { - eval_flags.code - } - .into_bytes(); - - let file = File { - maybe_types: None, - media_type: MediaType::Unknown, - source: String::from_utf8(source_code)?.into(), - specifier: main_module.clone(), - maybe_headers: None, - }; - - // Save our fake file into file fetcher cache - // to allow module access by TS compiler. - file_fetcher.insert_cached(file); - - let permissions = PermissionsContainer::new(Permissions::from_options( - &cli_options.permissions_options(), - )?); - let worker_factory = factory.create_cli_main_worker_factory().await?; - let mut worker = worker_factory - .create_main_worker(main_module, permissions) - .await?; - let exit_code = worker.run().await?; - Ok(exit_code) -} - -async fn maybe_npm_install(factory: &CliFactory) -> Result<(), AnyError> { - // ensure an "npm install" is done if the user has explicitly - // opted into using a managed node_modules directory - if factory.cli_options().node_modules_dir_enablement() == Some(true) { - if let Some(npm_resolver) = factory.npm_resolver().await?.as_managed() { - npm_resolver.ensure_top_level_package_json_install().await?; - } - } - Ok(()) -} diff --git a/cli/tools/run/hmr/json_types.rs b/cli/tools/run/hmr/json_types.rs new file mode 100644 index 000000000..3ac80344b --- /dev/null +++ b/cli/tools/run/hmr/json_types.rs @@ -0,0 +1,59 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +// TODO(bartlomieju): this code should be factored out to `cli/cdp.rs` along +// with code in `cli/tools/repl/` and `cli/tools/coverage/`. These are all +// Chrome Devtools Protocol message types. + +use deno_core::serde_json::Value; +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct RpcNotification { + pub method: String, + pub params: Value, +} + +#[derive(Debug, Deserialize)] +pub struct SetScriptSourceReturnObject { + pub status: Status, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ScriptParsed { + pub script_id: String, + pub url: String, +} + +#[derive(Debug, Deserialize)] +pub enum Status { + Ok, + CompileError, + BlockedByActiveGenerator, + BlockedByActiveFunction, + BlockedByTopLevelEsModuleChange, +} + +impl Status { + pub(crate) fn explain(&self) -> &'static str { + match self { + Status::Ok => "OK", + Status::CompileError => "compile error", + Status::BlockedByActiveGenerator => "blocked by active generator", + Status::BlockedByActiveFunction => "blocked by active function", + Status::BlockedByTopLevelEsModuleChange => { + "blocked by top-level ES module change" + } + } + } + + pub(crate) fn should_retry(&self) -> bool { + match self { + Status::Ok => false, + Status::CompileError => false, + Status::BlockedByActiveGenerator => true, + Status::BlockedByActiveFunction => true, + Status::BlockedByTopLevelEsModuleChange => false, + } + } +} diff --git a/cli/tools/run/hmr/mod.rs b/cli/tools/run/hmr/mod.rs new file mode 100644 index 000000000..1a5772307 --- /dev/null +++ b/cli/tools/run/hmr/mod.rs @@ -0,0 +1,242 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +use crate::emit::Emitter; +use crate::util::file_watcher::WatcherCommunicator; +use crate::util::file_watcher::WatcherRestartMode; +use deno_core::error::generic_error; +use deno_core::error::AnyError; +use deno_core::futures::StreamExt; +use deno_core::serde_json::json; +use deno_core::serde_json::{self}; +use deno_core::url::Url; +use deno_core::LocalInspectorSession; +use deno_runtime::colors; +use std::collections::HashMap; +use std::path::PathBuf; +use std::sync::Arc; +use tokio::select; + +mod json_types; + +use json_types::RpcNotification; +use json_types::ScriptParsed; +use json_types::SetScriptSourceReturnObject; +use json_types::Status; + +/// This structure is responsible for providing Hot Module Replacement +/// functionality. +/// +/// It communicates with V8 inspector over a local session and waits for +/// notifications about changed files from the `FileWatcher`. +/// +/// Upon receiving such notification, the runner decides if the changed +/// path should be handled the `FileWatcher` itself (as if we were running +/// in `--watch` mode), or if the path is eligible to be hot replaced in the +/// current program. +/// +/// Even if the runner decides that a path will be hot-replaced, the V8 isolate +/// can refuse to perform hot replacement, eg. a top-level variable/function +/// of an ES module cannot be hot-replaced. In such situation the runner will +/// force a full restart of a program by notifying the `FileWatcher`. +pub struct HmrRunner { + session: LocalInspectorSession, + watcher_communicator: Arc, + script_ids: HashMap, + emitter: Arc, +} + +impl HmrRunner { + pub fn new( + emitter: Arc, + session: LocalInspectorSession, + watcher_communicator: Arc, + ) -> Self { + Self { + session, + emitter, + watcher_communicator, + script_ids: HashMap::new(), + } + } + + // TODO(bartlomieju): this code is duplicated in `cli/tools/coverage/mod.rs` + pub async fn start(&mut self) -> Result<(), AnyError> { + self.enable_debugger().await + } + + // TODO(bartlomieju): this code is duplicated in `cli/tools/coverage/mod.rs` + pub async fn stop(&mut self) -> Result<(), AnyError> { + self + .watcher_communicator + .change_restart_mode(WatcherRestartMode::Automatic); + self.disable_debugger().await + } + + // TODO(bartlomieju): this code is duplicated in `cli/tools/coverage/mod.rs` + async fn enable_debugger(&mut self) -> Result<(), AnyError> { + self + .session + .post_message::<()>("Debugger.enable", None) + .await?; + self + .session + .post_message::<()>("Runtime.enable", None) + .await?; + Ok(()) + } + + // TODO(bartlomieju): this code is duplicated in `cli/tools/coverage/mod.rs` + async fn disable_debugger(&mut self) -> Result<(), AnyError> { + self + .session + .post_message::<()>("Debugger.disable", None) + .await?; + self + .session + .post_message::<()>("Runtime.disable", None) + .await?; + Ok(()) + } + + async fn set_script_source( + &mut self, + script_id: &str, + source: &str, + ) -> Result { + let result = self + .session + .post_message( + "Debugger.setScriptSource", + Some(json!({ + "scriptId": script_id, + "scriptSource": source, + "allowTopFrameEditing": true, + })), + ) + .await?; + + Ok(serde_json::from_value::( + result, + )?) + } + + async fn dispatch_hmr_event( + &mut self, + script_id: &str, + ) -> Result<(), AnyError> { + let expr = format!( + "dispatchEvent(new CustomEvent(\"hmr\", {{ detail: {{ path: \"{}\" }} }}));", + script_id + ); + + let _result = self + .session + .post_message( + "Runtime.evaluate", + Some(json!({ + "expression": expr, + "contextId": Some(1), + })), + ) + .await?; + + Ok(()) + } + + pub async fn run(&mut self) -> Result<(), AnyError> { + self + .watcher_communicator + .change_restart_mode(WatcherRestartMode::Manual); + let mut session_rx = self.session.take_notification_rx(); + loop { + select! { + biased; + Some(notification) = session_rx.next() => { + let notification = serde_json::from_value::(notification)?; + // TODO(bartlomieju): this is not great... and the code is duplicated with the REPL. + if notification.method == "Runtime.exceptionThrown" { + let params = notification.params; + let exception_details = params.get("exceptionDetails").unwrap().as_object().unwrap(); + let text = exception_details.get("text").unwrap().as_str().unwrap(); + let exception = exception_details.get("exception").unwrap().as_object().unwrap(); + let description = exception.get("description").and_then(|d| d.as_str()).unwrap_or("undefined"); + break Err(generic_error(format!("{text} {description}"))); + } else if notification.method == "Debugger.scriptParsed" { + let params = serde_json::from_value::(notification.params)?; + if params.url.starts_with("file://") { + let file_url = Url::parse(¶ms.url).unwrap(); + let file_path = file_url.to_file_path().unwrap(); + if let Ok(canonicalized_file_path) = file_path.canonicalize() { + let canonicalized_file_url = Url::from_file_path(canonicalized_file_path).unwrap(); + self.script_ids.insert(canonicalized_file_url.to_string(), params.script_id); + } + } + } + } + changed_paths = self.watcher_communicator.watch_for_changed_paths() => { + let changed_paths = changed_paths?; + + let Some(changed_paths) = changed_paths else { + let _ = self.watcher_communicator.force_restart(); + continue; + }; + + let filtered_paths: Vec = changed_paths.into_iter().filter(|p| p.extension().map_or(false, |ext| { + let ext_str = ext.to_str().unwrap(); + matches!(ext_str, "js" | "ts" | "jsx" | "tsx") + })).collect(); + + // If after filtering there are no paths it means it's either a file + // we can't HMR or an external file that was passed explicitly to + // `--unstable-hmr=` path. + if filtered_paths.is_empty() { + let _ = self.watcher_communicator.force_restart(); + continue; + } + + for path in filtered_paths { + let Some(path_str) = path.to_str() else { + let _ = self.watcher_communicator.force_restart(); + continue; + }; + let Ok(module_url) = Url::from_file_path(path_str) else { + let _ = self.watcher_communicator.force_restart(); + continue; + }; + + let Some(id) = self.script_ids.get(module_url.as_str()).cloned() else { + let _ = self.watcher_communicator.force_restart(); + continue; + }; + + let source_code = self.emitter.load_and_emit_for_hmr( + &module_url + ).await?; + + let mut tries = 1; + loop { + let result = self.set_script_source(&id, source_code.as_str()).await?; + + if matches!(result.status, Status::Ok) { + self.dispatch_hmr_event(module_url.as_str()).await?; + self.watcher_communicator.print(format!("Replaced changed module {}", module_url.as_str())); + break; + } + + self.watcher_communicator.print(format!("Failed to reload module {}: {}.", module_url, colors::gray(result.status.explain()))); + if result.status.should_retry() && tries <= 2 { + tries += 1; + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + continue; + } + + let _ = self.watcher_communicator.force_restart(); + break; + } + } + } + _ = self.session.receive_from_v8_session() => {} + } + } + } +} diff --git a/cli/tools/run/mod.rs b/cli/tools/run/mod.rs new file mode 100644 index 000000000..119129b1b --- /dev/null +++ b/cli/tools/run/mod.rs @@ -0,0 +1,205 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +use std::io::Read; + +use deno_ast::MediaType; +use deno_core::error::AnyError; +use deno_runtime::permissions::Permissions; +use deno_runtime::permissions::PermissionsContainer; + +use crate::args::EvalFlags; +use crate::args::Flags; +use crate::args::RunFlags; +use crate::args::WatchFlagsWithPaths; +use crate::factory::CliFactory; +use crate::factory::CliFactoryBuilder; +use crate::file_fetcher::File; +use crate::util; +use crate::util::file_watcher::WatcherRestartMode; + +pub mod hmr; + +pub async fn run_script( + flags: Flags, + run_flags: RunFlags, +) -> Result { + if !flags.has_permission() && flags.has_permission_in_argv() { + log::warn!( + "{}", + crate::colors::yellow( + r#"Permission flags have likely been incorrectly set after the script argument. +To grant permissions, set them before the script argument. For example: + deno run --allow-read=. main.js"# + ) + ); + } + + if let Some(watch_flags) = run_flags.watch { + return run_with_watch(flags, watch_flags).await; + } + + // TODO(bartlomieju): actually I think it will also fail if there's an import + // map specified and bare specifier is used on the command line + let factory = CliFactory::from_flags(flags).await?; + let deno_dir = factory.deno_dir()?; + let http_client = factory.http_client(); + let cli_options = factory.cli_options(); + + // Run a background task that checks for available upgrades. If an earlier + // run of this background task found a new version of Deno. + super::upgrade::check_for_upgrades( + http_client.clone(), + deno_dir.upgrade_check_file_path(), + ); + + let main_module = cli_options.resolve_main_module()?; + + maybe_npm_install(&factory).await?; + + let permissions = PermissionsContainer::new(Permissions::from_options( + &cli_options.permissions_options(), + )?); + let worker_factory = factory.create_cli_main_worker_factory().await?; + let mut worker = worker_factory + .create_main_worker(main_module, permissions) + .await?; + + let exit_code = worker.run().await?; + Ok(exit_code) +} + +pub async fn run_from_stdin(flags: Flags) -> Result { + let factory = CliFactory::from_flags(flags).await?; + let cli_options = factory.cli_options(); + let main_module = cli_options.resolve_main_module()?; + + maybe_npm_install(&factory).await?; + + let file_fetcher = factory.file_fetcher()?; + let worker_factory = factory.create_cli_main_worker_factory().await?; + let permissions = PermissionsContainer::new(Permissions::from_options( + &cli_options.permissions_options(), + )?); + let mut source = Vec::new(); + std::io::stdin().read_to_end(&mut source)?; + // Create a dummy source file. + let source_file = File { + maybe_types: None, + media_type: MediaType::TypeScript, + source: String::from_utf8(source)?.into(), + specifier: main_module.clone(), + maybe_headers: None, + }; + // Save our fake file into file fetcher cache + // to allow module access by TS compiler + file_fetcher.insert_cached(source_file); + + let mut worker = worker_factory + .create_main_worker(main_module, permissions) + .await?; + let exit_code = worker.run().await?; + Ok(exit_code) +} + +// TODO(bartlomieju): this function is not handling `exit_code` set by the runtime +// code properly. +async fn run_with_watch( + flags: Flags, + watch_flags: WatchFlagsWithPaths, +) -> Result { + util::file_watcher::watch_recv( + flags, + util::file_watcher::PrintConfig::new_with_banner( + if watch_flags.hmr { "HMR" } else { "Watcher" }, + "Process", + !watch_flags.no_clear_screen, + ), + WatcherRestartMode::Automatic, + move |flags, watcher_communicator, _changed_paths| { + Ok(async move { + let factory = CliFactoryBuilder::new() + .build_from_flags_for_watcher(flags, watcher_communicator.clone()) + .await?; + let cli_options = factory.cli_options(); + let main_module = cli_options.resolve_main_module()?; + + maybe_npm_install(&factory).await?; + + let _ = watcher_communicator.watch_paths(cli_options.watch_paths()); + + let permissions = PermissionsContainer::new(Permissions::from_options( + &cli_options.permissions_options(), + )?); + let mut worker = factory + .create_cli_main_worker_factory() + .await? + .create_main_worker(main_module, permissions) + .await?; + + if watch_flags.hmr { + worker.run().await?; + } else { + worker.run_for_watcher().await?; + } + + Ok(()) + }) + }, + ) + .await?; + + Ok(0) +} + +pub async fn eval_command( + flags: Flags, + eval_flags: EvalFlags, +) -> Result { + let factory = CliFactory::from_flags(flags).await?; + let cli_options = factory.cli_options(); + let file_fetcher = factory.file_fetcher()?; + let main_module = cli_options.resolve_main_module()?; + + maybe_npm_install(&factory).await?; + + // Create a dummy source file. + let source_code = if eval_flags.print { + format!("console.log({})", eval_flags.code) + } else { + eval_flags.code + } + .into_bytes(); + + let file = File { + maybe_types: None, + media_type: MediaType::Unknown, + source: String::from_utf8(source_code)?.into(), + specifier: main_module.clone(), + maybe_headers: None, + }; + + // Save our fake file into file fetcher cache + // to allow module access by TS compiler. + file_fetcher.insert_cached(file); + + let permissions = PermissionsContainer::new(Permissions::from_options( + &cli_options.permissions_options(), + )?); + let worker_factory = factory.create_cli_main_worker_factory().await?; + let mut worker = worker_factory + .create_main_worker(main_module, permissions) + .await?; + let exit_code = worker.run().await?; + Ok(exit_code) +} + +async fn maybe_npm_install(factory: &CliFactory) -> Result<(), AnyError> { + // ensure an "npm install" is done if the user has explicitly + // opted into using a managed node_modules directory + if factory.cli_options().node_modules_dir_enablement() == Some(true) { + if let Some(npm_resolver) = factory.npm_resolver().await?.as_managed() { + npm_resolver.ensure_top_level_package_json_install().await?; + } + } + Ok(()) +} diff --git a/cli/tools/test/mod.rs b/cli/tools/test/mod.rs index 8e29ba2cb..5e34e345f 100644 --- a/cli/tools/test/mod.rs +++ b/cli/tools/test/mod.rs @@ -1205,14 +1205,14 @@ pub async fn run_tests_with_watch( file_watcher::watch_func( flags, - file_watcher::PrintConfig { - job_name: "Test".to_string(), - clear_screen: test_flags + file_watcher::PrintConfig::new( + "Test", + test_flags .watch .as_ref() .map(|w| !w.no_clear_screen) .unwrap_or(true), - }, + ), move |flags, watcher_communicator, changed_paths| { let test_flags = test_flags.clone(); Ok(async move { -- cgit v1.2.3