summaryrefslogtreecommitdiff
path: root/cli/tools/run
diff options
context:
space:
mode:
Diffstat (limited to 'cli/tools/run')
-rw-r--r--cli/tools/run/hmr/json_types.rs59
-rw-r--r--cli/tools/run/hmr/mod.rs242
-rw-r--r--cli/tools/run/mod.rs205
3 files changed, 506 insertions, 0 deletions
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<WatcherCommunicator>,
+ script_ids: HashMap<String, String>,
+ emitter: Arc<Emitter>,
+}
+
+impl HmrRunner {
+ pub fn new(
+ emitter: Arc<Emitter>,
+ session: LocalInspectorSession,
+ watcher_communicator: Arc<WatcherCommunicator>,
+ ) -> 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<SetScriptSourceReturnObject, AnyError> {
+ let result = self
+ .session
+ .post_message(
+ "Debugger.setScriptSource",
+ Some(json!({
+ "scriptId": script_id,
+ "scriptSource": source,
+ "allowTopFrameEditing": true,
+ })),
+ )
+ .await?;
+
+ Ok(serde_json::from_value::<SetScriptSourceReturnObject>(
+ 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::<RpcNotification>(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::<ScriptParsed>(notification.params)?;
+ if params.url.starts_with("file://") {
+ let file_url = Url::parse(&params.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<PathBuf> = 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=<file>` 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<i32, AnyError> {
+ 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<i32, AnyError> {
+ 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<i32, AnyError> {
+ 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<i32, AnyError> {
+ 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(())
+}