mirror of
https://github.com/denoland/deno.git
synced 2024-11-21 15:04:11 -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)]
|
||||
pub struct WatchFlags {
|
||||
pub hmr: bool,
|
||||
pub no_clear_screen: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Debug, Eq, PartialEq)]
|
||||
pub struct WatchFlagsWithPaths {
|
||||
pub hmr: bool,
|
||||
pub paths: Vec<PathBuf>,
|
||||
pub no_clear_screen: bool,
|
||||
}
|
||||
|
@ -1860,6 +1862,7 @@ fn run_subcommand() -> Command {
|
|||
runtime_args(Command::new("run"), true, true)
|
||||
.arg(check_arg(false))
|
||||
.arg(watch_arg(true))
|
||||
.arg(hmr_arg(true))
|
||||
.arg(no_clear_screen_arg())
|
||||
.arg(executable_ext_arg())
|
||||
.arg(
|
||||
|
@ -2728,6 +2731,33 @@ fn seed_arg() -> Arg {
|
|||
.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 {
|
||||
let arg = Arg::new("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> {
|
||||
if matches.get_flag("watch") {
|
||||
Some(WatchFlags {
|
||||
hmr: false,
|
||||
no_clear_screen: matches.get_flag("no-clear-screen"),
|
||||
})
|
||||
} else {
|
||||
|
@ -3859,10 +3890,19 @@ fn watch_arg_parse(matches: &mut ArgMatches) -> Option<WatchFlags> {
|
|||
fn watch_arg_parse_with_paths(
|
||||
matches: &mut ArgMatches,
|
||||
) -> 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
|
||||
.remove_many::<PathBuf>("watch")
|
||||
.map(|f| WatchFlagsWithPaths {
|
||||
paths: f.collect(),
|
||||
.remove_many::<PathBuf>("hmr")
|
||||
.map(|paths| WatchFlagsWithPaths {
|
||||
paths: paths.collect(),
|
||||
hmr: true,
|
||||
no_clear_screen: matches.get_flag("no-clear-screen"),
|
||||
})
|
||||
}
|
||||
|
@ -3980,6 +4020,7 @@ mod tests {
|
|||
subcommand: DenoSubcommand::Run(RunFlags {
|
||||
script: "script.ts".to_string(),
|
||||
watch: Some(WatchFlagsWithPaths {
|
||||
hmr: false,
|
||||
paths: vec![],
|
||||
no_clear_screen: false,
|
||||
}),
|
||||
|
@ -3987,6 +4028,79 @@ mod tests {
|
|||
..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]
|
||||
|
@ -4000,6 +4114,7 @@ mod tests {
|
|||
subcommand: DenoSubcommand::Run(RunFlags {
|
||||
script: "script.ts".to_string(),
|
||||
watch: Some(WatchFlagsWithPaths {
|
||||
hmr: false,
|
||||
paths: vec![PathBuf::from("file1"), PathBuf::from("file2")],
|
||||
no_clear_screen: false,
|
||||
}),
|
||||
|
@ -4026,6 +4141,7 @@ mod tests {
|
|||
subcommand: DenoSubcommand::Run(RunFlags {
|
||||
script: "script.ts".to_string(),
|
||||
watch: Some(WatchFlagsWithPaths {
|
||||
hmr: false,
|
||||
paths: vec![],
|
||||
no_clear_screen: true,
|
||||
})
|
||||
|
@ -4347,9 +4463,7 @@ mod tests {
|
|||
single_quote: None,
|
||||
prose_wrap: None,
|
||||
no_semicolons: None,
|
||||
watch: Some(WatchFlags {
|
||||
no_clear_screen: false,
|
||||
})
|
||||
watch: Some(Default::default()),
|
||||
}),
|
||||
ext: Some("ts".to_string()),
|
||||
..Flags::default()
|
||||
|
@ -4374,6 +4488,7 @@ mod tests {
|
|||
prose_wrap: None,
|
||||
no_semicolons: None,
|
||||
watch: Some(WatchFlags {
|
||||
hmr: false,
|
||||
no_clear_screen: true,
|
||||
})
|
||||
}),
|
||||
|
@ -4405,9 +4520,7 @@ mod tests {
|
|||
single_quote: None,
|
||||
prose_wrap: None,
|
||||
no_semicolons: None,
|
||||
watch: Some(WatchFlags {
|
||||
no_clear_screen: false,
|
||||
})
|
||||
watch: Some(Default::default()),
|
||||
}),
|
||||
ext: Some("ts".to_string()),
|
||||
..Flags::default()
|
||||
|
@ -4461,9 +4574,7 @@ mod tests {
|
|||
single_quote: None,
|
||||
prose_wrap: None,
|
||||
no_semicolons: None,
|
||||
watch: Some(WatchFlags {
|
||||
no_clear_screen: false,
|
||||
})
|
||||
watch: Some(Default::default()),
|
||||
}),
|
||||
config_flag: ConfigFlag::Path("deno.jsonc".to_string()),
|
||||
ext: Some("ts".to_string()),
|
||||
|
@ -4587,9 +4698,7 @@ mod tests {
|
|||
maybe_rules_exclude: None,
|
||||
json: false,
|
||||
compact: false,
|
||||
watch: Some(WatchFlags {
|
||||
no_clear_screen: false,
|
||||
})
|
||||
watch: Some(Default::default()),
|
||||
}),
|
||||
..Flags::default()
|
||||
}
|
||||
|
@ -4621,6 +4730,7 @@ mod tests {
|
|||
json: false,
|
||||
compact: false,
|
||||
watch: Some(WatchFlags {
|
||||
hmr: false,
|
||||
no_clear_screen: true,
|
||||
})
|
||||
}),
|
||||
|
@ -5823,9 +5933,7 @@ mod tests {
|
|||
subcommand: DenoSubcommand::Bundle(BundleFlags {
|
||||
source_file: "source.ts".to_string(),
|
||||
out_file: None,
|
||||
watch: Some(WatchFlags {
|
||||
no_clear_screen: false,
|
||||
}),
|
||||
watch: Some(Default::default()),
|
||||
}),
|
||||
type_check_mode: TypeCheckMode::Local,
|
||||
..Flags::default()
|
||||
|
@ -5849,6 +5957,7 @@ mod tests {
|
|||
source_file: "source.ts".to_string(),
|
||||
out_file: None,
|
||||
watch: Some(WatchFlags {
|
||||
hmr: false,
|
||||
no_clear_screen: true,
|
||||
}),
|
||||
}),
|
||||
|
@ -7017,9 +7126,7 @@ mod tests {
|
|||
concurrent_jobs: None,
|
||||
trace_ops: false,
|
||||
coverage_dir: None,
|
||||
watch: Some(WatchFlags {
|
||||
no_clear_screen: false,
|
||||
}),
|
||||
watch: Some(Default::default()),
|
||||
reporter: Default::default(),
|
||||
junit_path: None,
|
||||
}),
|
||||
|
@ -7049,9 +7156,7 @@ mod tests {
|
|||
concurrent_jobs: None,
|
||||
trace_ops: false,
|
||||
coverage_dir: None,
|
||||
watch: Some(WatchFlags {
|
||||
no_clear_screen: false,
|
||||
}),
|
||||
watch: Some(Default::default()),
|
||||
reporter: Default::default(),
|
||||
junit_path: None,
|
||||
}),
|
||||
|
@ -7084,6 +7189,7 @@ mod tests {
|
|||
trace_ops: false,
|
||||
coverage_dir: None,
|
||||
watch: Some(WatchFlags {
|
||||
hmr: false,
|
||||
no_clear_screen: true,
|
||||
}),
|
||||
reporter: Default::default(),
|
||||
|
@ -7851,9 +7957,7 @@ mod tests {
|
|||
include: vec![],
|
||||
ignore: vec![],
|
||||
},
|
||||
watch: Some(WatchFlags {
|
||||
no_clear_screen: false,
|
||||
}),
|
||||
watch: Some(Default::default()),
|
||||
}),
|
||||
no_prompt: true,
|
||||
type_check_mode: TypeCheckMode::Local,
|
||||
|
|
|
@ -1130,6 +1130,18 @@ impl CliOptions {
|
|||
&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.
|
||||
pub fn is_inspecting(&self) -> bool {
|
||||
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
|
||||
/// options then generates a string hash which can be stored to
|
||||
/// determine if the cached emit is valid or not.
|
||||
|
|
|
@ -66,7 +66,7 @@ use std::future::Future;
|
|||
use std::sync::Arc;
|
||||
|
||||
pub struct CliFactoryBuilder {
|
||||
watcher_communicator: Option<WatcherCommunicator>,
|
||||
watcher_communicator: Option<Arc<WatcherCommunicator>>,
|
||||
}
|
||||
|
||||
impl CliFactoryBuilder {
|
||||
|
@ -86,7 +86,7 @@ impl CliFactoryBuilder {
|
|||
pub async fn build_from_flags_for_watcher(
|
||||
mut self,
|
||||
flags: Flags,
|
||||
watcher_communicator: WatcherCommunicator,
|
||||
watcher_communicator: Arc<WatcherCommunicator>,
|
||||
) -> Result<CliFactory, AnyError> {
|
||||
self.watcher_communicator = Some(watcher_communicator);
|
||||
self.build_from_flags(flags).await
|
||||
|
@ -171,7 +171,7 @@ struct CliFactoryServices {
|
|||
}
|
||||
|
||||
pub struct CliFactory {
|
||||
watcher_communicator: Option<WatcherCommunicator>,
|
||||
watcher_communicator: Option<Arc<WatcherCommunicator>>,
|
||||
options: Arc<CliOptions>,
|
||||
services: CliFactoryServices,
|
||||
}
|
||||
|
@ -620,6 +620,11 @@ impl CliFactory {
|
|||
let npm_resolver = self.npm_resolver().await?;
|
||||
let fs = self.fs();
|
||||
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(
|
||||
StorageKeyResolver::from_options(&self.options),
|
||||
|
@ -643,6 +648,8 @@ impl CliFactory {
|
|||
)),
|
||||
self.root_cert_store_provider().clone(),
|
||||
self.fs().clone(),
|
||||
Some(self.emitter()?.clone()),
|
||||
maybe_file_watcher_communicator,
|
||||
self.maybe_inspector_server().clone(),
|
||||
self.maybe_lockfile().clone(),
|
||||
self.feature_checker().clone(),
|
||||
|
@ -659,6 +666,7 @@ impl CliFactory {
|
|||
coverage_dir: self.options.coverage_dir(),
|
||||
enable_testing_features: self.options.enable_testing_features(),
|
||||
has_node_modules_dir: self.options.has_node_modules_dir(),
|
||||
hmr: self.options.has_hmr(),
|
||||
inspect_brk: self.options.inspect_brk().is_some(),
|
||||
inspect_wait: self.options.inspect_wait().is_some(),
|
||||
is_inspecting: self.options.is_inspecting(),
|
||||
|
|
|
@ -681,12 +681,12 @@ impl<'a> ModuleGraphUpdatePermit<'a> {
|
|||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FileWatcherReporter {
|
||||
watcher_communicator: WatcherCommunicator,
|
||||
watcher_communicator: Arc<WatcherCommunicator>,
|
||||
file_paths: Arc<Mutex<Vec<PathBuf>>>,
|
||||
}
|
||||
|
||||
impl FileWatcherReporter {
|
||||
pub fn new(watcher_communicator: WatcherCommunicator) -> Self {
|
||||
pub fn new(watcher_communicator: Arc<WatcherCommunicator>) -> Self {
|
||||
Self {
|
||||
watcher_communicator,
|
||||
file_paths: Default::default(),
|
||||
|
|
|
@ -446,6 +446,8 @@ pub async fn run(
|
|||
fs,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
feature_checker,
|
||||
CliMainWorkerOptions {
|
||||
argv: metadata.argv,
|
||||
|
@ -453,6 +455,7 @@ pub async fn run(
|
|||
coverage_dir: None,
|
||||
enable_testing_features: false,
|
||||
has_node_modules_dir,
|
||||
hmr: false,
|
||||
inspect_brk: false,
|
||||
inspect_wait: false,
|
||||
is_inspecting: false,
|
||||
|
|
|
@ -1645,3 +1645,257 @@ async fn run_watch_inspect() {
|
|||
|
||||
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> {
|
||||
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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
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::file_fetcher::File;
|
||||
use crate::util;
|
||||
use crate::util::file_watcher::WatcherRestartMode;
|
||||
|
||||
pub mod hmr;
|
||||
|
||||
pub async fn run_script(
|
||||
flags: Flags,
|
||||
|
@ -104,12 +107,14 @@ async fn run_with_watch(
|
|||
flags: Flags,
|
||||
watch_flags: WatchFlagsWithPaths,
|
||||
) -> Result<i32, AnyError> {
|
||||
util::file_watcher::watch_func(
|
||||
util::file_watcher::watch_recv(
|
||||
flags,
|
||||
util::file_watcher::PrintConfig {
|
||||
job_name: "Process".to_string(),
|
||||
clear_screen: !watch_flags.no_clear_screen,
|
||||
},
|
||||
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()
|
||||
|
@ -125,12 +130,17 @@ async fn run_with_watch(
|
|||
let permissions = PermissionsContainer::new(Permissions::from_options(
|
||||
&cli_options.permissions_options(),
|
||||
)?);
|
||||
let worker = factory
|
||||
let mut worker = factory
|
||||
.create_cli_main_worker_factory()
|
||||
.await?
|
||||
.create_main_worker(main_module, permissions)
|
||||
.await?;
|
||||
worker.run_for_watcher().await?;
|
||||
|
||||
if watch_flags.hmr {
|
||||
worker.run().await?;
|
||||
} else {
|
||||
worker.run_for_watcher().await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
|
@ -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 {
|
||||
|
|
|
@ -8,6 +8,7 @@ use deno_core::error::AnyError;
|
|||
use deno_core::error::JsError;
|
||||
use deno_core::futures::Future;
|
||||
use deno_core::futures::FutureExt;
|
||||
use deno_core::parking_lot::Mutex;
|
||||
use deno_runtime::fmt_errors::format_js_error;
|
||||
use log::info;
|
||||
use notify::event::Event as NotifyEvent;
|
||||
|
@ -16,9 +17,11 @@ use notify::Error as NotifyError;
|
|||
use notify::RecommendedWatcher;
|
||||
use notify::RecursiveMode;
|
||||
use notify::Watcher;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashSet;
|
||||
use std::io::IsTerminal;
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::select;
|
||||
|
@ -91,20 +94,49 @@ where
|
|||
}
|
||||
|
||||
pub struct PrintConfig {
|
||||
/// printing watcher status to terminal.
|
||||
pub job_name: String,
|
||||
/// determine whether to clear the terminal screen; applicable to TTY environments only.
|
||||
pub clear_screen: bool,
|
||||
banner: &'static str,
|
||||
/// Printing watcher status to terminal.
|
||||
job_name: &'static str,
|
||||
/// 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 || {
|
||||
if clear_screen && std::io::stderr().is_terminal() {
|
||||
eprint!("{CLEAR_SCREEN}");
|
||||
}
|
||||
info!(
|
||||
"{} 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.
|
||||
restart_tx: tokio::sync::mpsc::UnboundedSender<()>,
|
||||
}
|
||||
|
||||
impl Clone for WatcherCommunicator {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
paths_to_watch_tx: self.paths_to_watch_tx.clone(),
|
||||
changed_paths_rx: self.changed_paths_rx.resubscribe(),
|
||||
restart_tx: self.restart_tx.clone(),
|
||||
}
|
||||
}
|
||||
restart_mode: Mutex<WatcherRestartMode>,
|
||||
|
||||
banner: String,
|
||||
}
|
||||
|
||||
impl WatcherCommunicator {
|
||||
pub fn watch_paths(&self, paths: Vec<PathBuf>) -> Result<(), AnyError> {
|
||||
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.
|
||||
|
@ -151,7 +199,7 @@ pub async fn watch_func<O, F>(
|
|||
where
|
||||
O: FnMut(
|
||||
Flags,
|
||||
WatcherCommunicator,
|
||||
Arc<WatcherCommunicator>,
|
||||
Option<Vec<PathBuf>>,
|
||||
) -> Result<F, AnyError>,
|
||||
F: Future<Output = Result<(), AnyError>>,
|
||||
|
@ -173,9 +221,7 @@ pub enum WatcherRestartMode {
|
|||
Automatic,
|
||||
|
||||
/// When a file path changes the caller will trigger a restart, using
|
||||
/// `WatcherCommunicator.restart_tx`.
|
||||
// TODO(bartlomieju): this mode will be used in a follow up PR
|
||||
#[allow(dead_code)]
|
||||
/// `WatcherInterface.restart_tx`.
|
||||
Manual,
|
||||
}
|
||||
|
||||
|
@ -193,7 +239,7 @@ pub async fn watch_recv<O, F>(
|
|||
where
|
||||
O: FnMut(
|
||||
Flags,
|
||||
WatcherCommunicator,
|
||||
Arc<WatcherCommunicator>,
|
||||
Option<Vec<PathBuf>>,
|
||||
) -> Result<F, AnyError>,
|
||||
F: Future<Output = Result<(), AnyError>>,
|
||||
|
@ -206,19 +252,42 @@ where
|
|||
DebouncedReceiver::new_with_sender();
|
||||
|
||||
let PrintConfig {
|
||||
banner,
|
||||
job_name,
|
||||
clear_screen,
|
||||
} = print_config;
|
||||
|
||||
let print_after_restart = create_print_after_restart_fn(clear_screen);
|
||||
let watcher_communicator = WatcherCommunicator {
|
||||
let print_after_restart = create_print_after_restart_fn(banner, clear_screen);
|
||||
let watcher_communicator = Arc::new(WatcherCommunicator {
|
||||
paths_to_watch_tx: paths_to_watch_tx.clone(),
|
||||
changed_paths_rx: changed_paths_rx.resubscribe(),
|
||||
restart_tx: restart_tx.clone(),
|
||||
};
|
||||
info!("{} {} started.", colors::intense_blue("Watcher"), job_name,);
|
||||
restart_mode: Mutex::new(restart_mode),
|
||||
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 {
|
||||
// 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
|
||||
|
@ -239,7 +308,7 @@ where
|
|||
let operation_future = error_handler(operation(
|
||||
flags.clone(),
|
||||
watcher_communicator.clone(),
|
||||
changed_paths.take(),
|
||||
changed_paths.borrow_mut().take(),
|
||||
)?);
|
||||
|
||||
// don't reload dependencies after the first run
|
||||
|
@ -251,26 +320,12 @@ where
|
|||
print_after_restart();
|
||||
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 => {
|
||||
consume_paths_to_watch(&mut watcher, &mut paths_to_watch_rx);
|
||||
// TODO(bartlomieju): print exit code here?
|
||||
info!(
|
||||
"{} {} {}. Restarting on file change...",
|
||||
colors::intense_blue("Watcher"),
|
||||
colors::intense_blue(banner),
|
||||
job_name,
|
||||
if success {
|
||||
"finished"
|
||||
|
@ -280,7 +335,6 @@ where
|
|||
);
|
||||
},
|
||||
};
|
||||
|
||||
let receiver_future = async {
|
||||
loop {
|
||||
let maybe_paths = paths_to_watch_rx.recv().await;
|
||||
|
@ -293,9 +347,8 @@ where
|
|||
// watched paths has changed.
|
||||
select! {
|
||||
_ = receiver_future => {},
|
||||
received_changed_paths = watcher_receiver.recv() => {
|
||||
_ = restart_rx.recv() => {
|
||||
print_after_restart();
|
||||
changed_paths = received_changed_paths;
|
||||
continue;
|
||||
},
|
||||
};
|
||||
|
|
|
@ -43,15 +43,20 @@ use deno_runtime::BootstrapOptions;
|
|||
use deno_runtime::WorkerLogLevel;
|
||||
use deno_semver::npm::NpmPackageReqReference;
|
||||
use deno_semver::package::PackageReqReference;
|
||||
use tokio::select;
|
||||
|
||||
use crate::args::package_json::PackageJsonDeps;
|
||||
use crate::args::StorageKeyResolver;
|
||||
use crate::emit::Emitter;
|
||||
use crate::errors;
|
||||
use crate::npm::CliNpmResolver;
|
||||
use crate::ops;
|
||||
use crate::tools;
|
||||
use crate::tools::coverage::CoverageCollector;
|
||||
use crate::tools::run::hmr::HmrRunner;
|
||||
use crate::util::checksum;
|
||||
use crate::util::file_watcher::WatcherCommunicator;
|
||||
use crate::util::file_watcher::WatcherRestartMode;
|
||||
use crate::version;
|
||||
|
||||
pub trait ModuleLoaderFactory: Send + Sync {
|
||||
|
@ -83,6 +88,7 @@ pub struct CliMainWorkerOptions {
|
|||
pub coverage_dir: Option<String>,
|
||||
pub enable_testing_features: bool,
|
||||
pub has_node_modules_dir: bool,
|
||||
pub hmr: bool,
|
||||
pub inspect_brk: bool,
|
||||
pub inspect_wait: bool,
|
||||
pub is_inspecting: bool,
|
||||
|
@ -108,6 +114,8 @@ struct SharedWorkerState {
|
|||
module_loader_factory: Box<dyn ModuleLoaderFactory>,
|
||||
root_cert_store_provider: Arc<dyn RootCertStoreProvider>,
|
||||
fs: Arc<dyn deno_fs::FileSystem>,
|
||||
emitter: Option<Arc<Emitter>>,
|
||||
maybe_file_watcher_communicator: Option<Arc<WatcherCommunicator>>,
|
||||
maybe_inspector_server: Option<Arc<InspectorServer>>,
|
||||
maybe_lockfile: Option<Arc<Mutex<Lockfile>>>,
|
||||
feature_checker: Arc<FeatureChecker>,
|
||||
|
@ -137,6 +145,8 @@ impl CliMainWorker {
|
|||
pub async fn run(&mut self) -> Result<i32, AnyError> {
|
||||
let mut maybe_coverage_collector =
|
||||
self.maybe_setup_coverage_collector().await?;
|
||||
let mut maybe_hmr_runner = self.maybe_setup_hmr_runner().await?;
|
||||
|
||||
log::debug!("main_module {}", self.main_module);
|
||||
|
||||
if self.is_main_cjs {
|
||||
|
@ -153,10 +163,34 @@ impl CliMainWorker {
|
|||
self.worker.dispatch_load_event(located_script_name!())?;
|
||||
|
||||
loop {
|
||||
self
|
||||
.worker
|
||||
.run_event_loop(maybe_coverage_collector.is_none())
|
||||
.await?;
|
||||
if let Some(hmr_runner) = maybe_hmr_runner.as_mut() {
|
||||
let watcher_communicator =
|
||||
self.shared.maybe_file_watcher_communicator.clone().unwrap();
|
||||
|
||||
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
|
||||
.worker
|
||||
.dispatch_beforeunload_event(located_script_name!())?
|
||||
|
@ -173,6 +207,12 @@ impl CliMainWorker {
|
|||
.with_event_loop(coverage_collector.stop_collecting().boxed_local())
|
||||
.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())
|
||||
}
|
||||
|
@ -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(
|
||||
&mut self,
|
||||
name: &'static str,
|
||||
|
@ -313,6 +375,8 @@ impl CliMainWorkerFactory {
|
|||
module_loader_factory: Box<dyn ModuleLoaderFactory>,
|
||||
root_cert_store_provider: Arc<dyn RootCertStoreProvider>,
|
||||
fs: Arc<dyn deno_fs::FileSystem>,
|
||||
emitter: Option<Arc<Emitter>>,
|
||||
maybe_file_watcher_communicator: Option<Arc<WatcherCommunicator>>,
|
||||
maybe_inspector_server: Option<Arc<InspectorServer>>,
|
||||
maybe_lockfile: Option<Arc<Mutex<Lockfile>>>,
|
||||
feature_checker: Arc<FeatureChecker>,
|
||||
|
@ -330,7 +394,9 @@ impl CliMainWorkerFactory {
|
|||
compiled_wasm_module_store: Default::default(),
|
||||
module_loader_factory,
|
||||
root_cert_store_provider,
|
||||
emitter,
|
||||
fs,
|
||||
maybe_file_watcher_communicator,
|
||||
maybe_inspector_server,
|
||||
maybe_lockfile,
|
||||
feature_checker,
|
||||
|
|
Loading…
Reference in a new issue