mirror of
https://github.com/denoland/deno.git
synced 2024-11-24 15:19:26 -05:00
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 <syrupthinker@gryphno.de> Co-authored-by: David Sherret <dsherret@gmail.com>
This commit is contained in:
parent
48c5c3a3fb
commit
1713df1352
17 changed files with 933 additions and 108 deletions
|
@ -212,11 +212,13 @@ impl RunFlags {
|
||||||
|
|
||||||
#[derive(Clone, Default, Debug, Eq, PartialEq)]
|
#[derive(Clone, Default, Debug, Eq, PartialEq)]
|
||||||
pub struct WatchFlags {
|
pub struct WatchFlags {
|
||||||
|
pub hmr: bool,
|
||||||
pub no_clear_screen: bool,
|
pub no_clear_screen: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Default, Debug, Eq, PartialEq)]
|
#[derive(Clone, Default, Debug, Eq, PartialEq)]
|
||||||
pub struct WatchFlagsWithPaths {
|
pub struct WatchFlagsWithPaths {
|
||||||
|
pub hmr: bool,
|
||||||
pub paths: Vec<PathBuf>,
|
pub paths: Vec<PathBuf>,
|
||||||
pub no_clear_screen: bool,
|
pub no_clear_screen: bool,
|
||||||
}
|
}
|
||||||
|
@ -1860,6 +1862,7 @@ fn run_subcommand() -> Command {
|
||||||
runtime_args(Command::new("run"), true, true)
|
runtime_args(Command::new("run"), true, true)
|
||||||
.arg(check_arg(false))
|
.arg(check_arg(false))
|
||||||
.arg(watch_arg(true))
|
.arg(watch_arg(true))
|
||||||
|
.arg(hmr_arg(true))
|
||||||
.arg(no_clear_screen_arg())
|
.arg(no_clear_screen_arg())
|
||||||
.arg(executable_ext_arg())
|
.arg(executable_ext_arg())
|
||||||
.arg(
|
.arg(
|
||||||
|
@ -2728,6 +2731,33 @@ fn seed_arg() -> Arg {
|
||||||
.value_parser(value_parser!(u64))
|
.value_parser(value_parser!(u64))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn hmr_arg(takes_files: bool) -> Arg {
|
||||||
|
let arg = Arg::new("hmr")
|
||||||
|
.long("unstable-hmr")
|
||||||
|
.help("UNSTABLE: Watch for file changes and hot replace modules")
|
||||||
|
.conflicts_with("watch");
|
||||||
|
|
||||||
|
if takes_files {
|
||||||
|
arg
|
||||||
|
.value_name("FILES")
|
||||||
|
.num_args(0..)
|
||||||
|
.value_parser(value_parser!(PathBuf))
|
||||||
|
.use_value_delimiter(true)
|
||||||
|
.require_equals(true)
|
||||||
|
.long_help(
|
||||||
|
"Watch for file changes and restart process automatically.
|
||||||
|
Local files from entry point module graph are watched by default.
|
||||||
|
Additional paths might be watched by passing them as arguments to this flag.",
|
||||||
|
)
|
||||||
|
.value_hint(ValueHint::AnyPath)
|
||||||
|
} else {
|
||||||
|
arg.action(ArgAction::SetTrue).long_help(
|
||||||
|
"Watch for file changes and restart process automatically.
|
||||||
|
Only local files from entry point module graph are watched.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn watch_arg(takes_files: bool) -> Arg {
|
fn watch_arg(takes_files: bool) -> Arg {
|
||||||
let arg = Arg::new("watch")
|
let arg = Arg::new("watch")
|
||||||
.long("watch")
|
.long("watch")
|
||||||
|
@ -3849,6 +3879,7 @@ fn reload_arg_validate(urlstr: &str) -> Result<String, String> {
|
||||||
fn watch_arg_parse(matches: &mut ArgMatches) -> Option<WatchFlags> {
|
fn watch_arg_parse(matches: &mut ArgMatches) -> Option<WatchFlags> {
|
||||||
if matches.get_flag("watch") {
|
if matches.get_flag("watch") {
|
||||||
Some(WatchFlags {
|
Some(WatchFlags {
|
||||||
|
hmr: false,
|
||||||
no_clear_screen: matches.get_flag("no-clear-screen"),
|
no_clear_screen: matches.get_flag("no-clear-screen"),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
@ -3859,10 +3890,19 @@ fn watch_arg_parse(matches: &mut ArgMatches) -> Option<WatchFlags> {
|
||||||
fn watch_arg_parse_with_paths(
|
fn watch_arg_parse_with_paths(
|
||||||
matches: &mut ArgMatches,
|
matches: &mut ArgMatches,
|
||||||
) -> Option<WatchFlagsWithPaths> {
|
) -> Option<WatchFlagsWithPaths> {
|
||||||
|
if let Some(paths) = matches.remove_many::<PathBuf>("watch") {
|
||||||
|
return Some(WatchFlagsWithPaths {
|
||||||
|
paths: paths.collect(),
|
||||||
|
hmr: false,
|
||||||
|
no_clear_screen: matches.get_flag("no-clear-screen"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
matches
|
matches
|
||||||
.remove_many::<PathBuf>("watch")
|
.remove_many::<PathBuf>("hmr")
|
||||||
.map(|f| WatchFlagsWithPaths {
|
.map(|paths| WatchFlagsWithPaths {
|
||||||
paths: f.collect(),
|
paths: paths.collect(),
|
||||||
|
hmr: true,
|
||||||
no_clear_screen: matches.get_flag("no-clear-screen"),
|
no_clear_screen: matches.get_flag("no-clear-screen"),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -3980,6 +4020,7 @@ mod tests {
|
||||||
subcommand: DenoSubcommand::Run(RunFlags {
|
subcommand: DenoSubcommand::Run(RunFlags {
|
||||||
script: "script.ts".to_string(),
|
script: "script.ts".to_string(),
|
||||||
watch: Some(WatchFlagsWithPaths {
|
watch: Some(WatchFlagsWithPaths {
|
||||||
|
hmr: false,
|
||||||
paths: vec![],
|
paths: vec![],
|
||||||
no_clear_screen: false,
|
no_clear_screen: false,
|
||||||
}),
|
}),
|
||||||
|
@ -3987,6 +4028,79 @@ mod tests {
|
||||||
..Flags::default()
|
..Flags::default()
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let r = flags_from_vec(svec![
|
||||||
|
"deno",
|
||||||
|
"run",
|
||||||
|
"--watch",
|
||||||
|
"--no-clear-screen",
|
||||||
|
"script.ts"
|
||||||
|
]);
|
||||||
|
let flags = r.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
flags,
|
||||||
|
Flags {
|
||||||
|
subcommand: DenoSubcommand::Run(RunFlags {
|
||||||
|
script: "script.ts".to_string(),
|
||||||
|
watch: Some(WatchFlagsWithPaths {
|
||||||
|
hmr: false,
|
||||||
|
paths: vec![],
|
||||||
|
no_clear_screen: true,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
..Flags::default()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let r = flags_from_vec(svec![
|
||||||
|
"deno",
|
||||||
|
"run",
|
||||||
|
"--unstable-hmr",
|
||||||
|
"--no-clear-screen",
|
||||||
|
"script.ts"
|
||||||
|
]);
|
||||||
|
let flags = r.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
flags,
|
||||||
|
Flags {
|
||||||
|
subcommand: DenoSubcommand::Run(RunFlags {
|
||||||
|
script: "script.ts".to_string(),
|
||||||
|
watch: Some(WatchFlagsWithPaths {
|
||||||
|
hmr: true,
|
||||||
|
paths: vec![],
|
||||||
|
no_clear_screen: true,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
..Flags::default()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let r = flags_from_vec(svec![
|
||||||
|
"deno",
|
||||||
|
"run",
|
||||||
|
"--unstable-hmr=foo.txt",
|
||||||
|
"--no-clear-screen",
|
||||||
|
"script.ts"
|
||||||
|
]);
|
||||||
|
let flags = r.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
flags,
|
||||||
|
Flags {
|
||||||
|
subcommand: DenoSubcommand::Run(RunFlags {
|
||||||
|
script: "script.ts".to_string(),
|
||||||
|
watch: Some(WatchFlagsWithPaths {
|
||||||
|
hmr: true,
|
||||||
|
paths: vec![PathBuf::from("foo.txt")],
|
||||||
|
no_clear_screen: true,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
..Flags::default()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let r =
|
||||||
|
flags_from_vec(svec!["deno", "run", "--hmr", "--watch", "script.ts"]);
|
||||||
|
assert!(r.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -4000,6 +4114,7 @@ mod tests {
|
||||||
subcommand: DenoSubcommand::Run(RunFlags {
|
subcommand: DenoSubcommand::Run(RunFlags {
|
||||||
script: "script.ts".to_string(),
|
script: "script.ts".to_string(),
|
||||||
watch: Some(WatchFlagsWithPaths {
|
watch: Some(WatchFlagsWithPaths {
|
||||||
|
hmr: false,
|
||||||
paths: vec![PathBuf::from("file1"), PathBuf::from("file2")],
|
paths: vec![PathBuf::from("file1"), PathBuf::from("file2")],
|
||||||
no_clear_screen: false,
|
no_clear_screen: false,
|
||||||
}),
|
}),
|
||||||
|
@ -4026,6 +4141,7 @@ mod tests {
|
||||||
subcommand: DenoSubcommand::Run(RunFlags {
|
subcommand: DenoSubcommand::Run(RunFlags {
|
||||||
script: "script.ts".to_string(),
|
script: "script.ts".to_string(),
|
||||||
watch: Some(WatchFlagsWithPaths {
|
watch: Some(WatchFlagsWithPaths {
|
||||||
|
hmr: false,
|
||||||
paths: vec![],
|
paths: vec![],
|
||||||
no_clear_screen: true,
|
no_clear_screen: true,
|
||||||
})
|
})
|
||||||
|
@ -4347,9 +4463,7 @@ mod tests {
|
||||||
single_quote: None,
|
single_quote: None,
|
||||||
prose_wrap: None,
|
prose_wrap: None,
|
||||||
no_semicolons: None,
|
no_semicolons: None,
|
||||||
watch: Some(WatchFlags {
|
watch: Some(Default::default()),
|
||||||
no_clear_screen: false,
|
|
||||||
})
|
|
||||||
}),
|
}),
|
||||||
ext: Some("ts".to_string()),
|
ext: Some("ts".to_string()),
|
||||||
..Flags::default()
|
..Flags::default()
|
||||||
|
@ -4374,6 +4488,7 @@ mod tests {
|
||||||
prose_wrap: None,
|
prose_wrap: None,
|
||||||
no_semicolons: None,
|
no_semicolons: None,
|
||||||
watch: Some(WatchFlags {
|
watch: Some(WatchFlags {
|
||||||
|
hmr: false,
|
||||||
no_clear_screen: true,
|
no_clear_screen: true,
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
|
@ -4405,9 +4520,7 @@ mod tests {
|
||||||
single_quote: None,
|
single_quote: None,
|
||||||
prose_wrap: None,
|
prose_wrap: None,
|
||||||
no_semicolons: None,
|
no_semicolons: None,
|
||||||
watch: Some(WatchFlags {
|
watch: Some(Default::default()),
|
||||||
no_clear_screen: false,
|
|
||||||
})
|
|
||||||
}),
|
}),
|
||||||
ext: Some("ts".to_string()),
|
ext: Some("ts".to_string()),
|
||||||
..Flags::default()
|
..Flags::default()
|
||||||
|
@ -4461,9 +4574,7 @@ mod tests {
|
||||||
single_quote: None,
|
single_quote: None,
|
||||||
prose_wrap: None,
|
prose_wrap: None,
|
||||||
no_semicolons: None,
|
no_semicolons: None,
|
||||||
watch: Some(WatchFlags {
|
watch: Some(Default::default()),
|
||||||
no_clear_screen: false,
|
|
||||||
})
|
|
||||||
}),
|
}),
|
||||||
config_flag: ConfigFlag::Path("deno.jsonc".to_string()),
|
config_flag: ConfigFlag::Path("deno.jsonc".to_string()),
|
||||||
ext: Some("ts".to_string()),
|
ext: Some("ts".to_string()),
|
||||||
|
@ -4587,9 +4698,7 @@ mod tests {
|
||||||
maybe_rules_exclude: None,
|
maybe_rules_exclude: None,
|
||||||
json: false,
|
json: false,
|
||||||
compact: false,
|
compact: false,
|
||||||
watch: Some(WatchFlags {
|
watch: Some(Default::default()),
|
||||||
no_clear_screen: false,
|
|
||||||
})
|
|
||||||
}),
|
}),
|
||||||
..Flags::default()
|
..Flags::default()
|
||||||
}
|
}
|
||||||
|
@ -4621,6 +4730,7 @@ mod tests {
|
||||||
json: false,
|
json: false,
|
||||||
compact: false,
|
compact: false,
|
||||||
watch: Some(WatchFlags {
|
watch: Some(WatchFlags {
|
||||||
|
hmr: false,
|
||||||
no_clear_screen: true,
|
no_clear_screen: true,
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
|
@ -5823,9 +5933,7 @@ mod tests {
|
||||||
subcommand: DenoSubcommand::Bundle(BundleFlags {
|
subcommand: DenoSubcommand::Bundle(BundleFlags {
|
||||||
source_file: "source.ts".to_string(),
|
source_file: "source.ts".to_string(),
|
||||||
out_file: None,
|
out_file: None,
|
||||||
watch: Some(WatchFlags {
|
watch: Some(Default::default()),
|
||||||
no_clear_screen: false,
|
|
||||||
}),
|
|
||||||
}),
|
}),
|
||||||
type_check_mode: TypeCheckMode::Local,
|
type_check_mode: TypeCheckMode::Local,
|
||||||
..Flags::default()
|
..Flags::default()
|
||||||
|
@ -5849,6 +5957,7 @@ mod tests {
|
||||||
source_file: "source.ts".to_string(),
|
source_file: "source.ts".to_string(),
|
||||||
out_file: None,
|
out_file: None,
|
||||||
watch: Some(WatchFlags {
|
watch: Some(WatchFlags {
|
||||||
|
hmr: false,
|
||||||
no_clear_screen: true,
|
no_clear_screen: true,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
@ -7017,9 +7126,7 @@ mod tests {
|
||||||
concurrent_jobs: None,
|
concurrent_jobs: None,
|
||||||
trace_ops: false,
|
trace_ops: false,
|
||||||
coverage_dir: None,
|
coverage_dir: None,
|
||||||
watch: Some(WatchFlags {
|
watch: Some(Default::default()),
|
||||||
no_clear_screen: false,
|
|
||||||
}),
|
|
||||||
reporter: Default::default(),
|
reporter: Default::default(),
|
||||||
junit_path: None,
|
junit_path: None,
|
||||||
}),
|
}),
|
||||||
|
@ -7049,9 +7156,7 @@ mod tests {
|
||||||
concurrent_jobs: None,
|
concurrent_jobs: None,
|
||||||
trace_ops: false,
|
trace_ops: false,
|
||||||
coverage_dir: None,
|
coverage_dir: None,
|
||||||
watch: Some(WatchFlags {
|
watch: Some(Default::default()),
|
||||||
no_clear_screen: false,
|
|
||||||
}),
|
|
||||||
reporter: Default::default(),
|
reporter: Default::default(),
|
||||||
junit_path: None,
|
junit_path: None,
|
||||||
}),
|
}),
|
||||||
|
@ -7084,6 +7189,7 @@ mod tests {
|
||||||
trace_ops: false,
|
trace_ops: false,
|
||||||
coverage_dir: None,
|
coverage_dir: None,
|
||||||
watch: Some(WatchFlags {
|
watch: Some(WatchFlags {
|
||||||
|
hmr: false,
|
||||||
no_clear_screen: true,
|
no_clear_screen: true,
|
||||||
}),
|
}),
|
||||||
reporter: Default::default(),
|
reporter: Default::default(),
|
||||||
|
@ -7851,9 +7957,7 @@ mod tests {
|
||||||
include: vec![],
|
include: vec![],
|
||||||
ignore: vec![],
|
ignore: vec![],
|
||||||
},
|
},
|
||||||
watch: Some(WatchFlags {
|
watch: Some(Default::default()),
|
||||||
no_clear_screen: false,
|
|
||||||
}),
|
|
||||||
}),
|
}),
|
||||||
no_prompt: true,
|
no_prompt: true,
|
||||||
type_check_mode: TypeCheckMode::Local,
|
type_check_mode: TypeCheckMode::Local,
|
||||||
|
|
|
@ -1130,6 +1130,18 @@ impl CliOptions {
|
||||||
&self.flags.ext
|
&self.flags.ext
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn has_hmr(&self) -> bool {
|
||||||
|
if let DenoSubcommand::Run(RunFlags {
|
||||||
|
watch: Some(WatchFlagsWithPaths { hmr, .. }),
|
||||||
|
..
|
||||||
|
}) = &self.flags.subcommand
|
||||||
|
{
|
||||||
|
*hmr
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// If the --inspect or --inspect-brk flags are used.
|
/// If the --inspect or --inspect-brk flags are used.
|
||||||
pub fn is_inspecting(&self) -> bool {
|
pub fn is_inspecting(&self) -> bool {
|
||||||
self.flags.inspect.is_some()
|
self.flags.inspect.is_some()
|
||||||
|
|
20
cli/emit.rs
20
cli/emit.rs
|
@ -101,6 +101,26 @@ impl Emitter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Expects a file URL, panics otherwise.
|
||||||
|
pub async fn load_and_emit_for_hmr(
|
||||||
|
&self,
|
||||||
|
specifier: &ModuleSpecifier,
|
||||||
|
) -> Result<String, AnyError> {
|
||||||
|
let media_type = MediaType::from_specifier(specifier);
|
||||||
|
let source_code = tokio::fs::read_to_string(
|
||||||
|
ModuleSpecifier::to_file_path(specifier).unwrap(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let source_arc: Arc<str> = source_code.into();
|
||||||
|
let parsed_source = self
|
||||||
|
.parsed_source_cache
|
||||||
|
.get_or_parse_module(specifier, source_arc, media_type)?;
|
||||||
|
let mut options = self.emit_options.clone();
|
||||||
|
options.inline_source_map = false;
|
||||||
|
let transpiled_source = parsed_source.transpile(&options)?;
|
||||||
|
Ok(transpiled_source.text)
|
||||||
|
}
|
||||||
|
|
||||||
/// A hashing function that takes the source code and uses the global emit
|
/// A hashing function that takes the source code and uses the global emit
|
||||||
/// options then generates a string hash which can be stored to
|
/// options then generates a string hash which can be stored to
|
||||||
/// determine if the cached emit is valid or not.
|
/// determine if the cached emit is valid or not.
|
||||||
|
|
|
@ -66,7 +66,7 @@ use std::future::Future;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
pub struct CliFactoryBuilder {
|
pub struct CliFactoryBuilder {
|
||||||
watcher_communicator: Option<WatcherCommunicator>,
|
watcher_communicator: Option<Arc<WatcherCommunicator>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CliFactoryBuilder {
|
impl CliFactoryBuilder {
|
||||||
|
@ -86,7 +86,7 @@ impl CliFactoryBuilder {
|
||||||
pub async fn build_from_flags_for_watcher(
|
pub async fn build_from_flags_for_watcher(
|
||||||
mut self,
|
mut self,
|
||||||
flags: Flags,
|
flags: Flags,
|
||||||
watcher_communicator: WatcherCommunicator,
|
watcher_communicator: Arc<WatcherCommunicator>,
|
||||||
) -> Result<CliFactory, AnyError> {
|
) -> Result<CliFactory, AnyError> {
|
||||||
self.watcher_communicator = Some(watcher_communicator);
|
self.watcher_communicator = Some(watcher_communicator);
|
||||||
self.build_from_flags(flags).await
|
self.build_from_flags(flags).await
|
||||||
|
@ -171,7 +171,7 @@ struct CliFactoryServices {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct CliFactory {
|
pub struct CliFactory {
|
||||||
watcher_communicator: Option<WatcherCommunicator>,
|
watcher_communicator: Option<Arc<WatcherCommunicator>>,
|
||||||
options: Arc<CliOptions>,
|
options: Arc<CliOptions>,
|
||||||
services: CliFactoryServices,
|
services: CliFactoryServices,
|
||||||
}
|
}
|
||||||
|
@ -620,6 +620,11 @@ impl CliFactory {
|
||||||
let npm_resolver = self.npm_resolver().await?;
|
let npm_resolver = self.npm_resolver().await?;
|
||||||
let fs = self.fs();
|
let fs = self.fs();
|
||||||
let cli_node_resolver = self.cli_node_resolver().await?;
|
let cli_node_resolver = self.cli_node_resolver().await?;
|
||||||
|
let maybe_file_watcher_communicator = if self.options.has_hmr() {
|
||||||
|
Some(self.watcher_communicator.clone().unwrap())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
Ok(CliMainWorkerFactory::new(
|
Ok(CliMainWorkerFactory::new(
|
||||||
StorageKeyResolver::from_options(&self.options),
|
StorageKeyResolver::from_options(&self.options),
|
||||||
|
@ -643,6 +648,8 @@ impl CliFactory {
|
||||||
)),
|
)),
|
||||||
self.root_cert_store_provider().clone(),
|
self.root_cert_store_provider().clone(),
|
||||||
self.fs().clone(),
|
self.fs().clone(),
|
||||||
|
Some(self.emitter()?.clone()),
|
||||||
|
maybe_file_watcher_communicator,
|
||||||
self.maybe_inspector_server().clone(),
|
self.maybe_inspector_server().clone(),
|
||||||
self.maybe_lockfile().clone(),
|
self.maybe_lockfile().clone(),
|
||||||
self.feature_checker().clone(),
|
self.feature_checker().clone(),
|
||||||
|
@ -659,6 +666,7 @@ impl CliFactory {
|
||||||
coverage_dir: self.options.coverage_dir(),
|
coverage_dir: self.options.coverage_dir(),
|
||||||
enable_testing_features: self.options.enable_testing_features(),
|
enable_testing_features: self.options.enable_testing_features(),
|
||||||
has_node_modules_dir: self.options.has_node_modules_dir(),
|
has_node_modules_dir: self.options.has_node_modules_dir(),
|
||||||
|
hmr: self.options.has_hmr(),
|
||||||
inspect_brk: self.options.inspect_brk().is_some(),
|
inspect_brk: self.options.inspect_brk().is_some(),
|
||||||
inspect_wait: self.options.inspect_wait().is_some(),
|
inspect_wait: self.options.inspect_wait().is_some(),
|
||||||
is_inspecting: self.options.is_inspecting(),
|
is_inspecting: self.options.is_inspecting(),
|
||||||
|
|
|
@ -681,12 +681,12 @@ impl<'a> ModuleGraphUpdatePermit<'a> {
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct FileWatcherReporter {
|
pub struct FileWatcherReporter {
|
||||||
watcher_communicator: WatcherCommunicator,
|
watcher_communicator: Arc<WatcherCommunicator>,
|
||||||
file_paths: Arc<Mutex<Vec<PathBuf>>>,
|
file_paths: Arc<Mutex<Vec<PathBuf>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FileWatcherReporter {
|
impl FileWatcherReporter {
|
||||||
pub fn new(watcher_communicator: WatcherCommunicator) -> Self {
|
pub fn new(watcher_communicator: Arc<WatcherCommunicator>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
watcher_communicator,
|
watcher_communicator,
|
||||||
file_paths: Default::default(),
|
file_paths: Default::default(),
|
||||||
|
|
|
@ -446,6 +446,8 @@ pub async fn run(
|
||||||
fs,
|
fs,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
feature_checker,
|
feature_checker,
|
||||||
CliMainWorkerOptions {
|
CliMainWorkerOptions {
|
||||||
argv: metadata.argv,
|
argv: metadata.argv,
|
||||||
|
@ -453,6 +455,7 @@ pub async fn run(
|
||||||
coverage_dir: None,
|
coverage_dir: None,
|
||||||
enable_testing_features: false,
|
enable_testing_features: false,
|
||||||
has_node_modules_dir,
|
has_node_modules_dir,
|
||||||
|
hmr: false,
|
||||||
inspect_brk: false,
|
inspect_brk: false,
|
||||||
inspect_wait: false,
|
inspect_wait: false,
|
||||||
is_inspecting: false,
|
is_inspecting: false,
|
||||||
|
|
|
@ -1645,3 +1645,257 @@ async fn run_watch_inspect() {
|
||||||
|
|
||||||
check_alive_then_kill(child);
|
check_alive_then_kill(child);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn run_hmr_server() {
|
||||||
|
let t = TempDir::new();
|
||||||
|
let file_to_watch = t.path().join("file_to_watch.js");
|
||||||
|
file_to_watch.write(
|
||||||
|
r#"
|
||||||
|
globalThis.state = { i: 0 };
|
||||||
|
|
||||||
|
function bar() {
|
||||||
|
globalThis.state.i = 0;
|
||||||
|
console.log("got request", globalThis.state.i);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handler(_req) {
|
||||||
|
bar();
|
||||||
|
return new Response("Hello world!");
|
||||||
|
}
|
||||||
|
|
||||||
|
Deno.serve({ port: 11111 }, handler);
|
||||||
|
console.log("Listening...")
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut child = util::deno_cmd()
|
||||||
|
.current_dir(util::testdata_path())
|
||||||
|
.arg("run")
|
||||||
|
.arg("--unstable-hmr")
|
||||||
|
.arg("--allow-net")
|
||||||
|
.arg("-L")
|
||||||
|
.arg("debug")
|
||||||
|
.arg(&file_to_watch)
|
||||||
|
.env("NO_COLOR", "1")
|
||||||
|
.stdout(std::process::Stdio::piped())
|
||||||
|
.stderr(std::process::Stdio::piped())
|
||||||
|
.spawn()
|
||||||
|
.unwrap();
|
||||||
|
let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);
|
||||||
|
wait_contains("Process started", &mut stderr_lines).await;
|
||||||
|
wait_contains("No package.json file found", &mut stderr_lines).await;
|
||||||
|
|
||||||
|
wait_for_watcher("file_to_watch.js", &mut stderr_lines).await;
|
||||||
|
wait_contains("Listening...", &mut stdout_lines).await;
|
||||||
|
|
||||||
|
file_to_watch.write(
|
||||||
|
r#"
|
||||||
|
globalThis.state = { i: 0 };
|
||||||
|
|
||||||
|
function bar() {
|
||||||
|
globalThis.state.i = 0;
|
||||||
|
console.log("got request1", globalThis.state.i);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handler(_req) {
|
||||||
|
bar();
|
||||||
|
return new Response("Hello world!");
|
||||||
|
}
|
||||||
|
|
||||||
|
Deno.serve({ port: 11111 }, handler);
|
||||||
|
console.log("Listening...")
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
wait_contains("Failed to reload module", &mut stderr_lines).await;
|
||||||
|
wait_contains("File change detected", &mut stderr_lines).await;
|
||||||
|
|
||||||
|
check_alive_then_kill(child);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn run_hmr_jsx() {
|
||||||
|
let t = TempDir::new();
|
||||||
|
let file_to_watch = t.path().join("file_to_watch.js");
|
||||||
|
file_to_watch.write(
|
||||||
|
r#"
|
||||||
|
import { foo } from "./foo.jsx";
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
setInterval(() => {
|
||||||
|
console.log(i++, foo());
|
||||||
|
}, 100);
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
let file_to_watch2 = t.path().join("foo.jsx");
|
||||||
|
file_to_watch2.write(
|
||||||
|
r#"
|
||||||
|
export function foo() {
|
||||||
|
return `<h1>Hello</h1>`;
|
||||||
|
}
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut child = util::deno_cmd()
|
||||||
|
.current_dir(util::testdata_path())
|
||||||
|
.arg("run")
|
||||||
|
.arg("--unstable-hmr")
|
||||||
|
.arg("-L")
|
||||||
|
.arg("debug")
|
||||||
|
.arg(&file_to_watch)
|
||||||
|
.env("NO_COLOR", "1")
|
||||||
|
.stdout(std::process::Stdio::piped())
|
||||||
|
.stderr(std::process::Stdio::piped())
|
||||||
|
.spawn()
|
||||||
|
.unwrap();
|
||||||
|
let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);
|
||||||
|
wait_contains("Process started", &mut stderr_lines).await;
|
||||||
|
wait_contains("No package.json file found", &mut stderr_lines).await;
|
||||||
|
|
||||||
|
wait_for_watcher("file_to_watch.js", &mut stderr_lines).await;
|
||||||
|
wait_contains("5 <h1>Hello</h1>", &mut stdout_lines).await;
|
||||||
|
|
||||||
|
file_to_watch2.write(
|
||||||
|
r#"
|
||||||
|
export function foo() {
|
||||||
|
return `<h1>Hello world</h1>`;
|
||||||
|
}
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
wait_contains("Replaced changed module", &mut stderr_lines).await;
|
||||||
|
wait_contains("<h1>Hello world</h1>", &mut stdout_lines).await;
|
||||||
|
|
||||||
|
check_alive_then_kill(child);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn run_hmr_uncaught_error() {
|
||||||
|
let t = TempDir::new();
|
||||||
|
let file_to_watch = t.path().join("file_to_watch.js");
|
||||||
|
file_to_watch.write(
|
||||||
|
r#"
|
||||||
|
import { foo } from "./foo.jsx";
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
setInterval(() => {
|
||||||
|
console.log(i++, foo());
|
||||||
|
}, 100);
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
let file_to_watch2 = t.path().join("foo.jsx");
|
||||||
|
file_to_watch2.write(
|
||||||
|
r#"
|
||||||
|
export function foo() {
|
||||||
|
setTimeout(() => {
|
||||||
|
throw new Error("fail");
|
||||||
|
});
|
||||||
|
return `<h1>asd1</h1>`;
|
||||||
|
}
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut child = util::deno_cmd()
|
||||||
|
.current_dir(util::testdata_path())
|
||||||
|
.arg("run")
|
||||||
|
.arg("--unstable-hmr")
|
||||||
|
.arg("-L")
|
||||||
|
.arg("debug")
|
||||||
|
.arg(&file_to_watch)
|
||||||
|
.env("NO_COLOR", "1")
|
||||||
|
.stdout(std::process::Stdio::piped())
|
||||||
|
.stderr(std::process::Stdio::piped())
|
||||||
|
.spawn()
|
||||||
|
.unwrap();
|
||||||
|
let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);
|
||||||
|
wait_contains("Process started", &mut stderr_lines).await;
|
||||||
|
wait_contains("No package.json file found", &mut stderr_lines).await;
|
||||||
|
|
||||||
|
wait_for_watcher("file_to_watch.js", &mut stderr_lines).await;
|
||||||
|
wait_contains("<h1>asd1</h1>", &mut stdout_lines).await;
|
||||||
|
wait_contains("fail", &mut stderr_lines).await;
|
||||||
|
|
||||||
|
file_to_watch2.write(
|
||||||
|
r#"
|
||||||
|
export function foo() {
|
||||||
|
return `<h1>asd2</h1>`;
|
||||||
|
}
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
wait_contains("Process failed", &mut stderr_lines).await;
|
||||||
|
wait_contains("File change detected", &mut stderr_lines).await;
|
||||||
|
wait_contains("<h1>asd2</h1>", &mut stdout_lines).await;
|
||||||
|
|
||||||
|
check_alive_then_kill(child);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn run_hmr_unhandled_rejection() {
|
||||||
|
let t = TempDir::new();
|
||||||
|
let file_to_watch = t.path().join("file_to_watch.js");
|
||||||
|
file_to_watch.write(
|
||||||
|
r#"
|
||||||
|
import { foo } from "./foo.jsx";
|
||||||
|
|
||||||
|
// deno-lint-ignore require-await
|
||||||
|
async function rejection() {
|
||||||
|
throw new Error("boom!");
|
||||||
|
}
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
setInterval(() => {
|
||||||
|
if (i == 3) {
|
||||||
|
rejection();
|
||||||
|
}
|
||||||
|
console.log(i++, foo());
|
||||||
|
}, 100);
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
let file_to_watch2 = t.path().join("foo.jsx");
|
||||||
|
file_to_watch2.write(
|
||||||
|
r#"
|
||||||
|
export function foo() {
|
||||||
|
return `<h1>asd1</h1>`;
|
||||||
|
}
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut child = util::deno_cmd()
|
||||||
|
.current_dir(util::testdata_path())
|
||||||
|
.arg("run")
|
||||||
|
.arg("--unstable-hmr")
|
||||||
|
.arg("-L")
|
||||||
|
.arg("debug")
|
||||||
|
.arg(&file_to_watch)
|
||||||
|
.env("NO_COLOR", "1")
|
||||||
|
.stdout(std::process::Stdio::piped())
|
||||||
|
.stderr(std::process::Stdio::piped())
|
||||||
|
.spawn()
|
||||||
|
.unwrap();
|
||||||
|
let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);
|
||||||
|
wait_contains("Process started", &mut stderr_lines).await;
|
||||||
|
wait_contains("No package.json file found", &mut stderr_lines).await;
|
||||||
|
|
||||||
|
wait_for_watcher("file_to_watch.js", &mut stderr_lines).await;
|
||||||
|
wait_contains("2 <h1>asd1</h1>", &mut stdout_lines).await;
|
||||||
|
wait_contains("boom", &mut stderr_lines).await;
|
||||||
|
|
||||||
|
file_to_watch.write(
|
||||||
|
r#"
|
||||||
|
import { foo } from "./foo.jsx";
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
setInterval(() => {
|
||||||
|
console.log(i++, foo());
|
||||||
|
}, 100);
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
wait_contains("Process failed", &mut stderr_lines).await;
|
||||||
|
wait_contains("File change detected", &mut stderr_lines).await;
|
||||||
|
wait_contains("<h1>asd1</h1>", &mut stdout_lines).await;
|
||||||
|
|
||||||
|
check_alive_then_kill(child);
|
||||||
|
}
|
||||||
|
|
|
@ -409,14 +409,14 @@ pub async fn run_benchmarks_with_watch(
|
||||||
) -> Result<(), AnyError> {
|
) -> Result<(), AnyError> {
|
||||||
file_watcher::watch_func(
|
file_watcher::watch_func(
|
||||||
flags,
|
flags,
|
||||||
file_watcher::PrintConfig {
|
file_watcher::PrintConfig::new(
|
||||||
job_name: "Bench".to_string(),
|
"Bench",
|
||||||
clear_screen: bench_flags
|
bench_flags
|
||||||
.watch
|
.watch
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|w| !w.no_clear_screen)
|
.map(|w| !w.no_clear_screen)
|
||||||
.unwrap_or(true),
|
.unwrap_or(true),
|
||||||
},
|
),
|
||||||
move |flags, watcher_communicator, changed_paths| {
|
move |flags, watcher_communicator, changed_paths| {
|
||||||
let bench_flags = bench_flags.clone();
|
let bench_flags = bench_flags.clone();
|
||||||
Ok(async move {
|
Ok(async move {
|
||||||
|
|
|
@ -31,10 +31,10 @@ pub async fn bundle(
|
||||||
if let Some(watch_flags) = &bundle_flags.watch {
|
if let Some(watch_flags) = &bundle_flags.watch {
|
||||||
util::file_watcher::watch_func(
|
util::file_watcher::watch_func(
|
||||||
flags,
|
flags,
|
||||||
util::file_watcher::PrintConfig {
|
util::file_watcher::PrintConfig::new(
|
||||||
job_name: "Bundle".to_string(),
|
"Bundle",
|
||||||
clear_screen: !watch_flags.no_clear_screen,
|
!watch_flags.no_clear_screen,
|
||||||
},
|
),
|
||||||
move |flags, watcher_communicator, _changed_paths| {
|
move |flags, watcher_communicator, _changed_paths| {
|
||||||
let bundle_flags = bundle_flags.clone();
|
let bundle_flags = bundle_flags.clone();
|
||||||
Ok(async move {
|
Ok(async move {
|
||||||
|
|
|
@ -64,10 +64,7 @@ pub async fn format(flags: Flags, fmt_flags: FmtFlags) -> Result<(), AnyError> {
|
||||||
if let Some(watch_flags) = &fmt_flags.watch {
|
if let Some(watch_flags) = &fmt_flags.watch {
|
||||||
file_watcher::watch_func(
|
file_watcher::watch_func(
|
||||||
flags,
|
flags,
|
||||||
file_watcher::PrintConfig {
|
file_watcher::PrintConfig::new("Fmt", !watch_flags.no_clear_screen),
|
||||||
job_name: "Fmt".to_string(),
|
|
||||||
clear_screen: !watch_flags.no_clear_screen,
|
|
||||||
},
|
|
||||||
move |flags, watcher_communicator, changed_paths| {
|
move |flags, watcher_communicator, changed_paths| {
|
||||||
let fmt_flags = fmt_flags.clone();
|
let fmt_flags = fmt_flags.clone();
|
||||||
Ok(async move {
|
Ok(async move {
|
||||||
|
@ -82,7 +79,7 @@ pub async fn format(flags: Flags, fmt_flags: FmtFlags) -> Result<(), AnyError> {
|
||||||
Ok(files)
|
Ok(files)
|
||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
_ = watcher_communicator.watch_paths(files.clone());
|
let _ = watcher_communicator.watch_paths(files.clone());
|
||||||
let refmt_files = if let Some(paths) = changed_paths {
|
let refmt_files = if let Some(paths) = changed_paths {
|
||||||
if fmt_options.check {
|
if fmt_options.check {
|
||||||
// check all files on any changed (https://github.com/denoland/deno/issues/12446)
|
// check all files on any changed (https://github.com/denoland/deno/issues/12446)
|
||||||
|
|
|
@ -59,10 +59,7 @@ pub async fn lint(flags: Flags, lint_flags: LintFlags) -> Result<(), AnyError> {
|
||||||
}
|
}
|
||||||
file_watcher::watch_func(
|
file_watcher::watch_func(
|
||||||
flags,
|
flags,
|
||||||
file_watcher::PrintConfig {
|
file_watcher::PrintConfig::new("Lint", !watch_flags.no_clear_screen),
|
||||||
job_name: "Lint".to_string(),
|
|
||||||
clear_screen: !watch_flags.no_clear_screen,
|
|
||||||
},
|
|
||||||
move |flags, watcher_communicator, changed_paths| {
|
move |flags, watcher_communicator, changed_paths| {
|
||||||
let lint_flags = lint_flags.clone();
|
let lint_flags = lint_flags.clone();
|
||||||
Ok(async move {
|
Ok(async move {
|
||||||
|
|
59
cli/tools/run/hmr/json_types.rs
Normal file
59
cli/tools/run/hmr/json_types.rs
Normal file
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
242
cli/tools/run/hmr/mod.rs
Normal file
242
cli/tools/run/hmr/mod.rs
Normal file
|
@ -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(¶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<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() => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,6 +15,9 @@ use crate::factory::CliFactory;
|
||||||
use crate::factory::CliFactoryBuilder;
|
use crate::factory::CliFactoryBuilder;
|
||||||
use crate::file_fetcher::File;
|
use crate::file_fetcher::File;
|
||||||
use crate::util;
|
use crate::util;
|
||||||
|
use crate::util::file_watcher::WatcherRestartMode;
|
||||||
|
|
||||||
|
pub mod hmr;
|
||||||
|
|
||||||
pub async fn run_script(
|
pub async fn run_script(
|
||||||
flags: Flags,
|
flags: Flags,
|
||||||
|
@ -104,12 +107,14 @@ async fn run_with_watch(
|
||||||
flags: Flags,
|
flags: Flags,
|
||||||
watch_flags: WatchFlagsWithPaths,
|
watch_flags: WatchFlagsWithPaths,
|
||||||
) -> Result<i32, AnyError> {
|
) -> Result<i32, AnyError> {
|
||||||
util::file_watcher::watch_func(
|
util::file_watcher::watch_recv(
|
||||||
flags,
|
flags,
|
||||||
util::file_watcher::PrintConfig {
|
util::file_watcher::PrintConfig::new_with_banner(
|
||||||
job_name: "Process".to_string(),
|
if watch_flags.hmr { "HMR" } else { "Watcher" },
|
||||||
clear_screen: !watch_flags.no_clear_screen,
|
"Process",
|
||||||
},
|
!watch_flags.no_clear_screen,
|
||||||
|
),
|
||||||
|
WatcherRestartMode::Automatic,
|
||||||
move |flags, watcher_communicator, _changed_paths| {
|
move |flags, watcher_communicator, _changed_paths| {
|
||||||
Ok(async move {
|
Ok(async move {
|
||||||
let factory = CliFactoryBuilder::new()
|
let factory = CliFactoryBuilder::new()
|
||||||
|
@ -125,12 +130,17 @@ async fn run_with_watch(
|
||||||
let permissions = PermissionsContainer::new(Permissions::from_options(
|
let permissions = PermissionsContainer::new(Permissions::from_options(
|
||||||
&cli_options.permissions_options(),
|
&cli_options.permissions_options(),
|
||||||
)?);
|
)?);
|
||||||
let worker = factory
|
let mut worker = factory
|
||||||
.create_cli_main_worker_factory()
|
.create_cli_main_worker_factory()
|
||||||
.await?
|
.await?
|
||||||
.create_main_worker(main_module, permissions)
|
.create_main_worker(main_module, permissions)
|
||||||
.await?;
|
.await?;
|
||||||
worker.run_for_watcher().await?;
|
|
||||||
|
if watch_flags.hmr {
|
||||||
|
worker.run().await?;
|
||||||
|
} else {
|
||||||
|
worker.run_for_watcher().await?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
|
@ -1205,14 +1205,14 @@ pub async fn run_tests_with_watch(
|
||||||
|
|
||||||
file_watcher::watch_func(
|
file_watcher::watch_func(
|
||||||
flags,
|
flags,
|
||||||
file_watcher::PrintConfig {
|
file_watcher::PrintConfig::new(
|
||||||
job_name: "Test".to_string(),
|
"Test",
|
||||||
clear_screen: test_flags
|
test_flags
|
||||||
.watch
|
.watch
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|w| !w.no_clear_screen)
|
.map(|w| !w.no_clear_screen)
|
||||||
.unwrap_or(true),
|
.unwrap_or(true),
|
||||||
},
|
),
|
||||||
move |flags, watcher_communicator, changed_paths| {
|
move |flags, watcher_communicator, changed_paths| {
|
||||||
let test_flags = test_flags.clone();
|
let test_flags = test_flags.clone();
|
||||||
Ok(async move {
|
Ok(async move {
|
||||||
|
|
|
@ -8,6 +8,7 @@ use deno_core::error::AnyError;
|
||||||
use deno_core::error::JsError;
|
use deno_core::error::JsError;
|
||||||
use deno_core::futures::Future;
|
use deno_core::futures::Future;
|
||||||
use deno_core::futures::FutureExt;
|
use deno_core::futures::FutureExt;
|
||||||
|
use deno_core::parking_lot::Mutex;
|
||||||
use deno_runtime::fmt_errors::format_js_error;
|
use deno_runtime::fmt_errors::format_js_error;
|
||||||
use log::info;
|
use log::info;
|
||||||
use notify::event::Event as NotifyEvent;
|
use notify::event::Event as NotifyEvent;
|
||||||
|
@ -16,9 +17,11 @@ use notify::Error as NotifyError;
|
||||||
use notify::RecommendedWatcher;
|
use notify::RecommendedWatcher;
|
||||||
use notify::RecursiveMode;
|
use notify::RecursiveMode;
|
||||||
use notify::Watcher;
|
use notify::Watcher;
|
||||||
|
use std::cell::RefCell;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::io::IsTerminal;
|
use std::io::IsTerminal;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use std::rc::Rc;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::select;
|
use tokio::select;
|
||||||
|
@ -91,20 +94,49 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct PrintConfig {
|
pub struct PrintConfig {
|
||||||
/// printing watcher status to terminal.
|
banner: &'static str,
|
||||||
pub job_name: String,
|
/// Printing watcher status to terminal.
|
||||||
/// determine whether to clear the terminal screen; applicable to TTY environments only.
|
job_name: &'static str,
|
||||||
pub clear_screen: bool,
|
/// Determine whether to clear the terminal screen; applicable to TTY environments only.
|
||||||
|
clear_screen: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_print_after_restart_fn(clear_screen: bool) -> impl Fn() {
|
impl PrintConfig {
|
||||||
|
/// By default `PrintConfig` uses "Watcher" as a banner name that will
|
||||||
|
/// be printed in color. If you need to customize it, use
|
||||||
|
/// `PrintConfig::new_with_banner` instead.
|
||||||
|
pub fn new(job_name: &'static str, clear_screen: bool) -> Self {
|
||||||
|
Self {
|
||||||
|
banner: "Watcher",
|
||||||
|
job_name,
|
||||||
|
clear_screen,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_with_banner(
|
||||||
|
banner: &'static str,
|
||||||
|
job_name: &'static str,
|
||||||
|
clear_screen: bool,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
banner,
|
||||||
|
job_name,
|
||||||
|
clear_screen,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_print_after_restart_fn(
|
||||||
|
banner: &'static str,
|
||||||
|
clear_screen: bool,
|
||||||
|
) -> impl Fn() {
|
||||||
move || {
|
move || {
|
||||||
if clear_screen && std::io::stderr().is_terminal() {
|
if clear_screen && std::io::stderr().is_terminal() {
|
||||||
eprint!("{CLEAR_SCREEN}");
|
eprint!("{CLEAR_SCREEN}");
|
||||||
}
|
}
|
||||||
info!(
|
info!(
|
||||||
"{} File change detected! Restarting!",
|
"{} File change detected! Restarting!",
|
||||||
colors::intense_blue("Watcher"),
|
colors::intense_blue(banner),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -120,22 +152,38 @@ pub struct WatcherCommunicator {
|
||||||
|
|
||||||
/// Send a message to force a restart.
|
/// Send a message to force a restart.
|
||||||
restart_tx: tokio::sync::mpsc::UnboundedSender<()>,
|
restart_tx: tokio::sync::mpsc::UnboundedSender<()>,
|
||||||
}
|
|
||||||
|
|
||||||
impl Clone for WatcherCommunicator {
|
restart_mode: Mutex<WatcherRestartMode>,
|
||||||
fn clone(&self) -> Self {
|
|
||||||
Self {
|
banner: String,
|
||||||
paths_to_watch_tx: self.paths_to_watch_tx.clone(),
|
|
||||||
changed_paths_rx: self.changed_paths_rx.resubscribe(),
|
|
||||||
restart_tx: self.restart_tx.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WatcherCommunicator {
|
impl WatcherCommunicator {
|
||||||
pub fn watch_paths(&self, paths: Vec<PathBuf>) -> Result<(), AnyError> {
|
pub fn watch_paths(&self, paths: Vec<PathBuf>) -> Result<(), AnyError> {
|
||||||
self.paths_to_watch_tx.send(paths).map_err(AnyError::from)
|
self.paths_to_watch_tx.send(paths).map_err(AnyError::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn force_restart(&self) -> Result<(), AnyError> {
|
||||||
|
// Change back to automatic mode, so that HMR can set up watching
|
||||||
|
// from scratch.
|
||||||
|
*self.restart_mode.lock() = WatcherRestartMode::Automatic;
|
||||||
|
self.restart_tx.send(()).map_err(AnyError::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn watch_for_changed_paths(
|
||||||
|
&self,
|
||||||
|
) -> Result<Option<Vec<PathBuf>>, AnyError> {
|
||||||
|
let mut rx = self.changed_paths_rx.resubscribe();
|
||||||
|
rx.recv().await.map_err(AnyError::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn change_restart_mode(&self, restart_mode: WatcherRestartMode) {
|
||||||
|
*self.restart_mode.lock() = restart_mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print(&self, msg: String) {
|
||||||
|
log::info!("{} {}", self.banner, msg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a file watcher.
|
/// Creates a file watcher.
|
||||||
|
@ -151,7 +199,7 @@ pub async fn watch_func<O, F>(
|
||||||
where
|
where
|
||||||
O: FnMut(
|
O: FnMut(
|
||||||
Flags,
|
Flags,
|
||||||
WatcherCommunicator,
|
Arc<WatcherCommunicator>,
|
||||||
Option<Vec<PathBuf>>,
|
Option<Vec<PathBuf>>,
|
||||||
) -> Result<F, AnyError>,
|
) -> Result<F, AnyError>,
|
||||||
F: Future<Output = Result<(), AnyError>>,
|
F: Future<Output = Result<(), AnyError>>,
|
||||||
|
@ -173,9 +221,7 @@ pub enum WatcherRestartMode {
|
||||||
Automatic,
|
Automatic,
|
||||||
|
|
||||||
/// When a file path changes the caller will trigger a restart, using
|
/// When a file path changes the caller will trigger a restart, using
|
||||||
/// `WatcherCommunicator.restart_tx`.
|
/// `WatcherInterface.restart_tx`.
|
||||||
// TODO(bartlomieju): this mode will be used in a follow up PR
|
|
||||||
#[allow(dead_code)]
|
|
||||||
Manual,
|
Manual,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -193,7 +239,7 @@ pub async fn watch_recv<O, F>(
|
||||||
where
|
where
|
||||||
O: FnMut(
|
O: FnMut(
|
||||||
Flags,
|
Flags,
|
||||||
WatcherCommunicator,
|
Arc<WatcherCommunicator>,
|
||||||
Option<Vec<PathBuf>>,
|
Option<Vec<PathBuf>>,
|
||||||
) -> Result<F, AnyError>,
|
) -> Result<F, AnyError>,
|
||||||
F: Future<Output = Result<(), AnyError>>,
|
F: Future<Output = Result<(), AnyError>>,
|
||||||
|
@ -206,19 +252,42 @@ where
|
||||||
DebouncedReceiver::new_with_sender();
|
DebouncedReceiver::new_with_sender();
|
||||||
|
|
||||||
let PrintConfig {
|
let PrintConfig {
|
||||||
|
banner,
|
||||||
job_name,
|
job_name,
|
||||||
clear_screen,
|
clear_screen,
|
||||||
} = print_config;
|
} = print_config;
|
||||||
|
|
||||||
let print_after_restart = create_print_after_restart_fn(clear_screen);
|
let print_after_restart = create_print_after_restart_fn(banner, clear_screen);
|
||||||
let watcher_communicator = WatcherCommunicator {
|
let watcher_communicator = Arc::new(WatcherCommunicator {
|
||||||
paths_to_watch_tx: paths_to_watch_tx.clone(),
|
paths_to_watch_tx: paths_to_watch_tx.clone(),
|
||||||
changed_paths_rx: changed_paths_rx.resubscribe(),
|
changed_paths_rx: changed_paths_rx.resubscribe(),
|
||||||
restart_tx: restart_tx.clone(),
|
restart_tx: restart_tx.clone(),
|
||||||
};
|
restart_mode: Mutex::new(restart_mode),
|
||||||
info!("{} {} started.", colors::intense_blue("Watcher"), job_name,);
|
banner: colors::intense_blue(banner).to_string(),
|
||||||
|
});
|
||||||
|
info!("{} {} started.", colors::intense_blue(banner), job_name);
|
||||||
|
|
||||||
|
let changed_paths = Rc::new(RefCell::new(None));
|
||||||
|
let changed_paths_ = changed_paths.clone();
|
||||||
|
let watcher_ = watcher_communicator.clone();
|
||||||
|
|
||||||
|
deno_core::unsync::spawn(async move {
|
||||||
|
loop {
|
||||||
|
let received_changed_paths = watcher_receiver.recv().await;
|
||||||
|
*changed_paths_.borrow_mut() = received_changed_paths.clone();
|
||||||
|
|
||||||
|
match *watcher_.restart_mode.lock() {
|
||||||
|
WatcherRestartMode::Automatic => {
|
||||||
|
let _ = restart_tx.send(());
|
||||||
|
}
|
||||||
|
WatcherRestartMode::Manual => {
|
||||||
|
// TODO(bartlomieju): should we fail on sending changed paths?
|
||||||
|
let _ = changed_paths_tx.send(received_changed_paths);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let mut changed_paths = None;
|
|
||||||
loop {
|
loop {
|
||||||
// We may need to give the runtime a tick to settle, as cancellations may need to propagate
|
// We may need to give the runtime a tick to settle, as cancellations may need to propagate
|
||||||
// to tasks. We choose yielding 10 times to the runtime as a decent heuristic. If watch tests
|
// to tasks. We choose yielding 10 times to the runtime as a decent heuristic. If watch tests
|
||||||
|
@ -239,7 +308,7 @@ where
|
||||||
let operation_future = error_handler(operation(
|
let operation_future = error_handler(operation(
|
||||||
flags.clone(),
|
flags.clone(),
|
||||||
watcher_communicator.clone(),
|
watcher_communicator.clone(),
|
||||||
changed_paths.take(),
|
changed_paths.borrow_mut().take(),
|
||||||
)?);
|
)?);
|
||||||
|
|
||||||
// don't reload dependencies after the first run
|
// don't reload dependencies after the first run
|
||||||
|
@ -251,26 +320,12 @@ where
|
||||||
print_after_restart();
|
print_after_restart();
|
||||||
continue;
|
continue;
|
||||||
},
|
},
|
||||||
received_changed_paths = watcher_receiver.recv() => {
|
|
||||||
changed_paths = received_changed_paths.clone();
|
|
||||||
|
|
||||||
match restart_mode {
|
|
||||||
WatcherRestartMode::Automatic => {
|
|
||||||
print_after_restart();
|
|
||||||
continue;
|
|
||||||
},
|
|
||||||
WatcherRestartMode::Manual => {
|
|
||||||
// TODO(bartlomieju): should we fail on sending changed paths?
|
|
||||||
let _ = changed_paths_tx.send(received_changed_paths);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
success = operation_future => {
|
success = operation_future => {
|
||||||
consume_paths_to_watch(&mut watcher, &mut paths_to_watch_rx);
|
consume_paths_to_watch(&mut watcher, &mut paths_to_watch_rx);
|
||||||
// TODO(bartlomieju): print exit code here?
|
// TODO(bartlomieju): print exit code here?
|
||||||
info!(
|
info!(
|
||||||
"{} {} {}. Restarting on file change...",
|
"{} {} {}. Restarting on file change...",
|
||||||
colors::intense_blue("Watcher"),
|
colors::intense_blue(banner),
|
||||||
job_name,
|
job_name,
|
||||||
if success {
|
if success {
|
||||||
"finished"
|
"finished"
|
||||||
|
@ -280,7 +335,6 @@ where
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let receiver_future = async {
|
let receiver_future = async {
|
||||||
loop {
|
loop {
|
||||||
let maybe_paths = paths_to_watch_rx.recv().await;
|
let maybe_paths = paths_to_watch_rx.recv().await;
|
||||||
|
@ -293,9 +347,8 @@ where
|
||||||
// watched paths has changed.
|
// watched paths has changed.
|
||||||
select! {
|
select! {
|
||||||
_ = receiver_future => {},
|
_ = receiver_future => {},
|
||||||
received_changed_paths = watcher_receiver.recv() => {
|
_ = restart_rx.recv() => {
|
||||||
print_after_restart();
|
print_after_restart();
|
||||||
changed_paths = received_changed_paths;
|
|
||||||
continue;
|
continue;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -43,15 +43,20 @@ use deno_runtime::BootstrapOptions;
|
||||||
use deno_runtime::WorkerLogLevel;
|
use deno_runtime::WorkerLogLevel;
|
||||||
use deno_semver::npm::NpmPackageReqReference;
|
use deno_semver::npm::NpmPackageReqReference;
|
||||||
use deno_semver::package::PackageReqReference;
|
use deno_semver::package::PackageReqReference;
|
||||||
|
use tokio::select;
|
||||||
|
|
||||||
use crate::args::package_json::PackageJsonDeps;
|
use crate::args::package_json::PackageJsonDeps;
|
||||||
use crate::args::StorageKeyResolver;
|
use crate::args::StorageKeyResolver;
|
||||||
|
use crate::emit::Emitter;
|
||||||
use crate::errors;
|
use crate::errors;
|
||||||
use crate::npm::CliNpmResolver;
|
use crate::npm::CliNpmResolver;
|
||||||
use crate::ops;
|
use crate::ops;
|
||||||
use crate::tools;
|
use crate::tools;
|
||||||
use crate::tools::coverage::CoverageCollector;
|
use crate::tools::coverage::CoverageCollector;
|
||||||
|
use crate::tools::run::hmr::HmrRunner;
|
||||||
use crate::util::checksum;
|
use crate::util::checksum;
|
||||||
|
use crate::util::file_watcher::WatcherCommunicator;
|
||||||
|
use crate::util::file_watcher::WatcherRestartMode;
|
||||||
use crate::version;
|
use crate::version;
|
||||||
|
|
||||||
pub trait ModuleLoaderFactory: Send + Sync {
|
pub trait ModuleLoaderFactory: Send + Sync {
|
||||||
|
@ -83,6 +88,7 @@ pub struct CliMainWorkerOptions {
|
||||||
pub coverage_dir: Option<String>,
|
pub coverage_dir: Option<String>,
|
||||||
pub enable_testing_features: bool,
|
pub enable_testing_features: bool,
|
||||||
pub has_node_modules_dir: bool,
|
pub has_node_modules_dir: bool,
|
||||||
|
pub hmr: bool,
|
||||||
pub inspect_brk: bool,
|
pub inspect_brk: bool,
|
||||||
pub inspect_wait: bool,
|
pub inspect_wait: bool,
|
||||||
pub is_inspecting: bool,
|
pub is_inspecting: bool,
|
||||||
|
@ -108,6 +114,8 @@ struct SharedWorkerState {
|
||||||
module_loader_factory: Box<dyn ModuleLoaderFactory>,
|
module_loader_factory: Box<dyn ModuleLoaderFactory>,
|
||||||
root_cert_store_provider: Arc<dyn RootCertStoreProvider>,
|
root_cert_store_provider: Arc<dyn RootCertStoreProvider>,
|
||||||
fs: Arc<dyn deno_fs::FileSystem>,
|
fs: Arc<dyn deno_fs::FileSystem>,
|
||||||
|
emitter: Option<Arc<Emitter>>,
|
||||||
|
maybe_file_watcher_communicator: Option<Arc<WatcherCommunicator>>,
|
||||||
maybe_inspector_server: Option<Arc<InspectorServer>>,
|
maybe_inspector_server: Option<Arc<InspectorServer>>,
|
||||||
maybe_lockfile: Option<Arc<Mutex<Lockfile>>>,
|
maybe_lockfile: Option<Arc<Mutex<Lockfile>>>,
|
||||||
feature_checker: Arc<FeatureChecker>,
|
feature_checker: Arc<FeatureChecker>,
|
||||||
|
@ -137,6 +145,8 @@ impl CliMainWorker {
|
||||||
pub async fn run(&mut self) -> Result<i32, AnyError> {
|
pub async fn run(&mut self) -> Result<i32, AnyError> {
|
||||||
let mut maybe_coverage_collector =
|
let mut maybe_coverage_collector =
|
||||||
self.maybe_setup_coverage_collector().await?;
|
self.maybe_setup_coverage_collector().await?;
|
||||||
|
let mut maybe_hmr_runner = self.maybe_setup_hmr_runner().await?;
|
||||||
|
|
||||||
log::debug!("main_module {}", self.main_module);
|
log::debug!("main_module {}", self.main_module);
|
||||||
|
|
||||||
if self.is_main_cjs {
|
if self.is_main_cjs {
|
||||||
|
@ -153,10 +163,34 @@ impl CliMainWorker {
|
||||||
self.worker.dispatch_load_event(located_script_name!())?;
|
self.worker.dispatch_load_event(located_script_name!())?;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
self
|
if let Some(hmr_runner) = maybe_hmr_runner.as_mut() {
|
||||||
.worker
|
let watcher_communicator =
|
||||||
.run_event_loop(maybe_coverage_collector.is_none())
|
self.shared.maybe_file_watcher_communicator.clone().unwrap();
|
||||||
.await?;
|
|
||||||
|
let hmr_future = hmr_runner.run().boxed_local();
|
||||||
|
let event_loop_future = self.worker.run_event_loop(false).boxed_local();
|
||||||
|
|
||||||
|
let result;
|
||||||
|
select! {
|
||||||
|
hmr_result = hmr_future => {
|
||||||
|
result = hmr_result;
|
||||||
|
},
|
||||||
|
event_loop_result = event_loop_future => {
|
||||||
|
result = event_loop_result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Err(e) = result {
|
||||||
|
watcher_communicator
|
||||||
|
.change_restart_mode(WatcherRestartMode::Automatic);
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self
|
||||||
|
.worker
|
||||||
|
.run_event_loop(maybe_coverage_collector.is_none())
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
if !self
|
if !self
|
||||||
.worker
|
.worker
|
||||||
.dispatch_beforeunload_event(located_script_name!())?
|
.dispatch_beforeunload_event(located_script_name!())?
|
||||||
|
@ -173,6 +207,12 @@ impl CliMainWorker {
|
||||||
.with_event_loop(coverage_collector.stop_collecting().boxed_local())
|
.with_event_loop(coverage_collector.stop_collecting().boxed_local())
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
if let Some(hmr_runner) = maybe_hmr_runner.as_mut() {
|
||||||
|
self
|
||||||
|
.worker
|
||||||
|
.with_event_loop(hmr_runner.stop().boxed_local())
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(self.worker.exit_code())
|
Ok(self.worker.exit_code())
|
||||||
}
|
}
|
||||||
|
@ -287,6 +327,28 @@ impl CliMainWorker {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn maybe_setup_hmr_runner(
|
||||||
|
&mut self,
|
||||||
|
) -> Result<Option<HmrRunner>, AnyError> {
|
||||||
|
if !self.shared.options.hmr {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let watcher_communicator =
|
||||||
|
self.shared.maybe_file_watcher_communicator.clone().unwrap();
|
||||||
|
let emitter = self.shared.emitter.clone().unwrap();
|
||||||
|
|
||||||
|
let session = self.worker.create_inspector_session().await;
|
||||||
|
let mut hmr_runner = HmrRunner::new(emitter, session, watcher_communicator);
|
||||||
|
|
||||||
|
self
|
||||||
|
.worker
|
||||||
|
.with_event_loop(hmr_runner.start().boxed_local())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(Some(hmr_runner))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn execute_script_static(
|
pub fn execute_script_static(
|
||||||
&mut self,
|
&mut self,
|
||||||
name: &'static str,
|
name: &'static str,
|
||||||
|
@ -313,6 +375,8 @@ impl CliMainWorkerFactory {
|
||||||
module_loader_factory: Box<dyn ModuleLoaderFactory>,
|
module_loader_factory: Box<dyn ModuleLoaderFactory>,
|
||||||
root_cert_store_provider: Arc<dyn RootCertStoreProvider>,
|
root_cert_store_provider: Arc<dyn RootCertStoreProvider>,
|
||||||
fs: Arc<dyn deno_fs::FileSystem>,
|
fs: Arc<dyn deno_fs::FileSystem>,
|
||||||
|
emitter: Option<Arc<Emitter>>,
|
||||||
|
maybe_file_watcher_communicator: Option<Arc<WatcherCommunicator>>,
|
||||||
maybe_inspector_server: Option<Arc<InspectorServer>>,
|
maybe_inspector_server: Option<Arc<InspectorServer>>,
|
||||||
maybe_lockfile: Option<Arc<Mutex<Lockfile>>>,
|
maybe_lockfile: Option<Arc<Mutex<Lockfile>>>,
|
||||||
feature_checker: Arc<FeatureChecker>,
|
feature_checker: Arc<FeatureChecker>,
|
||||||
|
@ -330,7 +394,9 @@ impl CliMainWorkerFactory {
|
||||||
compiled_wasm_module_store: Default::default(),
|
compiled_wasm_module_store: Default::default(),
|
||||||
module_loader_factory,
|
module_loader_factory,
|
||||||
root_cert_store_provider,
|
root_cert_store_provider,
|
||||||
|
emitter,
|
||||||
fs,
|
fs,
|
||||||
|
maybe_file_watcher_communicator,
|
||||||
maybe_inspector_server,
|
maybe_inspector_server,
|
||||||
maybe_lockfile,
|
maybe_lockfile,
|
||||||
feature_checker,
|
feature_checker,
|
||||||
|
|
Loading…
Reference in a new issue