// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. use crate::args::CliLockfile; use crate::args::CliOptions; use crate::args::DenoSubcommand; use crate::args::Flags; use crate::args::PackageJsonInstallDepsProvider; use crate::args::StorageKeyResolver; use crate::args::TsConfigType; use crate::cache::Caches; use crate::cache::CodeCache; use crate::cache::DenoDir; use crate::cache::DenoDirProvider; use crate::cache::EmitCache; use crate::cache::GlobalHttpCache; use crate::cache::HttpCache; use crate::cache::LocalHttpCache; use crate::cache::ModuleInfoCache; use crate::cache::NodeAnalysisCache; use crate::cache::ParsedSourceCache; use crate::emit::Emitter; use crate::file_fetcher::FileFetcher; use crate::graph_container::MainModuleGraphContainer; use crate::graph_util::FileWatcherReporter; use crate::graph_util::ModuleGraphBuilder; use crate::graph_util::ModuleGraphCreator; use crate::http_util::HttpClientProvider; use crate::module_loader::CliModuleLoaderFactory; use crate::module_loader::ModuleLoadPreparer; use crate::node::CliCjsCodeAnalyzer; use crate::node::CliNodeCodeTranslator; use crate::npm::create_cli_npm_resolver; use crate::npm::CliNpmResolver; use crate::npm::CliNpmResolverByonmCreateOptions; use crate::npm::CliNpmResolverCreateOptions; use crate::npm::CliNpmResolverManagedCreateOptions; use crate::npm::CliNpmResolverManagedSnapshotOption; use crate::resolver::CjsResolutionStore; use crate::resolver::CliGraphResolver; use crate::resolver::CliGraphResolverOptions; use crate::resolver::CliNodeResolver; use crate::resolver::NpmModuleLoader; use crate::resolver::SloppyImportsResolver; use crate::standalone::DenoCompileBinaryWriter; use crate::tools::check::TypeChecker; use crate::tools::coverage::CoverageCollector; use crate::tools::run::hmr::HmrRunner; use crate::util::file_watcher::WatcherCommunicator; use crate::util::fs::canonicalize_path_maybe_not_exists; use crate::util::progress_bar::ProgressBar; use crate::util::progress_bar::ProgressBarStyle; use crate::worker::CliMainWorkerFactory; use crate::worker::CliMainWorkerOptions; use std::collections::BTreeSet; use std::path::PathBuf; use deno_config::deno_json::ConfigFile; use deno_config::package_json::PackageJsonDepValue; use deno_config::workspace::PackageJsonDepResolution; use deno_config::workspace::WorkspaceResolver; use deno_core::error::AnyError; use deno_core::futures::FutureExt; use deno_core::FeatureChecker; use deno_lockfile::WorkspaceMemberConfig; use deno_runtime::deno_fs; use deno_runtime::deno_node::analyze::NodeCodeTranslator; use deno_runtime::deno_node::NodeResolver; use deno_runtime::deno_node::PackageJson; use deno_runtime::deno_tls::RootCertStoreProvider; use deno_runtime::deno_web::BlobStore; use deno_runtime::inspector_server::InspectorServer; use log::warn; use std::future::Future; use std::sync::Arc; pub struct CliFactoryBuilder { watcher_communicator: Option>, } impl CliFactoryBuilder { pub fn new() -> Self { Self { watcher_communicator: None, } } pub fn build_from_flags(self, flags: Flags) -> Result { Ok(self.build_from_cli_options(Arc::new(CliOptions::from_flags(flags)?))) } pub fn build_from_flags_for_watcher( mut self, flags: Flags, watcher_communicator: Arc, ) -> Result { self.watcher_communicator = Some(watcher_communicator); self.build_from_flags(flags) } pub fn build_from_cli_options(self, options: Arc) -> CliFactory { CliFactory { watcher_communicator: self.watcher_communicator, options, services: Default::default(), } } } struct Deferred(once_cell::unsync::OnceCell); impl Default for Deferred { fn default() -> Self { Self(once_cell::unsync::OnceCell::default()) } } impl Deferred { #[inline(always)] pub fn get_or_try_init( &self, create: impl FnOnce() -> Result, ) -> Result<&T, AnyError> { self.0.get_or_try_init(create) } #[inline(always)] pub fn get_or_init(&self, create: impl FnOnce() -> T) -> &T { self.0.get_or_init(create) } pub async fn get_or_try_init_async( &self, // some futures passed here are boxed because it was discovered // that they were called a lot, causing other futures to get // really big causing stack overflows on Windows create: impl Future>, ) -> Result<&T, AnyError> { if self.0.get().is_none() { // todo(dsherret): it would be more ideal if this enforced a // single executor and then we could make some initialization // concurrent let val = create.await?; _ = self.0.set(val); } Ok(self.0.get().unwrap()) } } #[derive(Default)] struct CliFactoryServices { deno_dir_provider: Deferred>, caches: Deferred>, file_fetcher: Deferred>, global_http_cache: Deferred>, http_cache: Deferred>, http_client_provider: Deferred>, emit_cache: Deferred, emitter: Deferred>, fs: Deferred>, main_graph_container: Deferred>, lockfile: Deferred>>, maybe_inspector_server: Deferred>>, root_cert_store_provider: Deferred>, blob_store: Deferred>, module_info_cache: Deferred>, parsed_source_cache: Deferred>, resolver: Deferred>, maybe_file_watcher_reporter: Deferred>, module_graph_builder: Deferred>, module_graph_creator: Deferred>, module_load_preparer: Deferred>, node_code_translator: Deferred>, node_resolver: Deferred>, npm_resolver: Deferred>, text_only_progress_bar: Deferred, type_checker: Deferred>, cjs_resolutions: Deferred>, cli_node_resolver: Deferred>, feature_checker: Deferred>, code_cache: Deferred>, workspace_resolver: Deferred>, } pub struct CliFactory { watcher_communicator: Option>, options: Arc, services: CliFactoryServices, } impl CliFactory { pub fn from_flags(flags: Flags) -> Result { CliFactoryBuilder::new().build_from_flags(flags) } pub fn from_cli_options(options: Arc) -> Self { CliFactoryBuilder::new().build_from_cli_options(options) } pub fn cli_options(&self) -> &Arc { &self.options } pub fn deno_dir_provider(&self) -> &Arc { self.services.deno_dir_provider.get_or_init(|| { Arc::new(DenoDirProvider::new( self.options.maybe_custom_root().clone(), )) }) } pub fn deno_dir(&self) -> Result<&DenoDir, AnyError> { Ok(self.deno_dir_provider().get_or_create()?) } pub fn caches(&self) -> Result<&Arc, AnyError> { self.services.caches.get_or_try_init(|| { let caches = Arc::new(Caches::new(self.deno_dir_provider().clone())); // Warm up the caches we know we'll likely need based on the CLI mode match self.options.sub_command() { DenoSubcommand::Run(_) | DenoSubcommand::Serve(_) | DenoSubcommand::Bench(_) | DenoSubcommand::Test(_) | DenoSubcommand::Check(_) => { _ = caches.dep_analysis_db(); _ = caches.node_analysis_db(); if self.options.type_check_mode().is_true() { _ = caches.fast_check_db(); _ = caches.type_checking_cache_db(); } if self.options.code_cache_enabled() { _ = caches.code_cache_db(); } } _ => {} } Ok(caches) }) } pub fn blob_store(&self) -> &Arc { self.services.blob_store.get_or_init(Default::default) } pub fn root_cert_store_provider(&self) -> &Arc { self .services .root_cert_store_provider .get_or_init(|| self.options.resolve_root_cert_store_provider()) } pub fn text_only_progress_bar(&self) -> &ProgressBar { self .services .text_only_progress_bar .get_or_init(|| ProgressBar::new(ProgressBarStyle::TextOnly)) } pub fn global_http_cache(&self) -> Result<&Arc, AnyError> { self.services.global_http_cache.get_or_try_init(|| { Ok(Arc::new(GlobalHttpCache::new( self.deno_dir()?.deps_folder_path(), crate::cache::RealDenoCacheEnv, ))) }) } pub fn http_cache(&self) -> Result<&Arc, AnyError> { self.services.http_cache.get_or_try_init(|| { let global_cache = self.global_http_cache()?.clone(); match self.options.vendor_dir_path() { Some(local_path) => { let local_cache = LocalHttpCache::new(local_path.clone(), global_cache); Ok(Arc::new(local_cache)) } None => Ok(global_cache), } }) } pub fn http_client_provider(&self) -> &Arc { self.services.http_client_provider.get_or_init(|| { Arc::new(HttpClientProvider::new( Some(self.root_cert_store_provider().clone()), self.options.unsafely_ignore_certificate_errors().clone(), )) }) } pub fn file_fetcher(&self) -> Result<&Arc, AnyError> { self.services.file_fetcher.get_or_try_init(|| { Ok(Arc::new(FileFetcher::new( self.http_cache()?.clone(), self.options.cache_setting(), !self.options.no_remote(), self.http_client_provider().clone(), self.blob_store().clone(), Some(self.text_only_progress_bar().clone()), ))) }) } pub fn fs(&self) -> &Arc { self.services.fs.get_or_init(|| Arc::new(deno_fs::RealFs)) } pub fn maybe_lockfile(&self) -> &Option> { fn pkg_json_deps(maybe_pkg_json: Option<&PackageJson>) -> BTreeSet { let Some(pkg_json) = maybe_pkg_json else { return Default::default(); }; pkg_json .resolve_local_package_json_deps() .values() .filter_map(|dep| dep.as_ref().ok()) .filter_map(|dep| match dep { PackageJsonDepValue::Req(req) => Some(req), PackageJsonDepValue::Workspace(_) => None, }) .map(|r| format!("npm:{}", r)) .collect() } fn deno_json_deps( maybe_deno_json: Option<&ConfigFile>, ) -> BTreeSet { maybe_deno_json .map(|c| { crate::args::deno_json::deno_json_deps(c) .into_iter() .map(|req| req.to_string()) .collect() }) .unwrap_or_default() } self.services.lockfile.get_or_init(|| { let maybe_lockfile = self.options.maybe_lockfile(); // initialize the lockfile with the workspace's configuration if let Some(lockfile) = &maybe_lockfile { let root_url = self.options.workspace().root_dir(); let root_folder = self.options.workspace().root_folder_configs(); let config = deno_lockfile::WorkspaceConfig { root: WorkspaceMemberConfig { package_json_deps: pkg_json_deps(root_folder.pkg_json.as_deref()), dependencies: deno_json_deps(root_folder.deno_json.as_deref()), }, members: self .options .workspace() .config_folders() .iter() .filter(|(folder_url, _)| *folder_url != root_url) .filter_map(|(folder_url, folder)| { Some(( { // should never be None here, but just ignore members that // do fail for this let mut relative_path = root_url.make_relative(folder_url)?; if relative_path.ends_with('/') { // make it slightly cleaner by removing the trailing slash relative_path.pop(); } relative_path }, { let config = WorkspaceMemberConfig { package_json_deps: pkg_json_deps( folder.pkg_json.as_deref(), ), dependencies: deno_json_deps(folder.deno_json.as_deref()), }; if config.package_json_deps.is_empty() && config.dependencies.is_empty() { // exclude empty workspace members return None; } config }, )) }) .collect(), }; lockfile.set_workspace_config( deno_lockfile::SetWorkspaceConfigOptions { no_npm: self.options.no_npm(), no_config: self.options.no_config(), config, }, ); } maybe_lockfile }) } pub async fn npm_resolver( &self, ) -> Result<&Arc, AnyError> { self .services .npm_resolver .get_or_try_init_async(async { let fs = self.fs(); // For `deno install` we want to force the managed resolver so it can set up `node_modules/` directory. create_cli_npm_resolver(if self.options.use_byonm() && !matches!(self.options.sub_command(), DenoSubcommand::Install(_)) { CliNpmResolverCreateOptions::Byonm(CliNpmResolverByonmCreateOptions { fs: fs.clone(), root_node_modules_dir: Some(match self.options.node_modules_dir_path() { Some(node_modules_path) => node_modules_path.to_path_buf(), // path needs to be canonicalized for node resolution // (node_modules_dir_path above is already canonicalized) None => canonicalize_path_maybe_not_exists(self.options.initial_cwd())? .join("node_modules"), }), }) } else { CliNpmResolverCreateOptions::Managed(CliNpmResolverManagedCreateOptions { snapshot: match self.options.resolve_npm_resolution_snapshot()? { Some(snapshot) => { CliNpmResolverManagedSnapshotOption::Specified(Some(snapshot)) } None => match self.maybe_lockfile().as_ref() { Some(lockfile) => { CliNpmResolverManagedSnapshotOption::ResolveFromLockfile( lockfile.clone(), ) } None => CliNpmResolverManagedSnapshotOption::Specified(None), }, }, maybe_lockfile: self.maybe_lockfile().as_ref().cloned(), fs: fs.clone(), http_client_provider: self.http_client_provider().clone(), npm_global_cache_dir: self.deno_dir()?.npm_folder_path(), cache_setting: self.options.cache_setting(), text_only_progress_bar: self.text_only_progress_bar().clone(), maybe_node_modules_path: self.options.node_modules_dir_path().cloned(), package_json_deps_provider: Arc::new(PackageJsonInstallDepsProvider::from_workspace( self.options.workspace(), )), npm_system_info: self.options.npm_system_info(), npmrc: self.options.npmrc().clone(), lifecycle_scripts: self.options.lifecycle_scripts_config(), }) }).await }.boxed_local()) .await } pub async fn workspace_resolver( &self, ) -> Result<&Arc, AnyError> { self .services .workspace_resolver .get_or_try_init_async(async { let resolver = self .options .create_workspace_resolver( self.file_fetcher()?, if self.options.use_byonm() { PackageJsonDepResolution::Disabled } else { // todo(dsherret): this should be false for nodeModulesDir: true PackageJsonDepResolution::Enabled }, ) .await?; if !resolver.diagnostics().is_empty() { warn!( "Import map diagnostics:\n{}", resolver .diagnostics() .iter() .map(|d| format!(" - {d}")) .collect::>() .join("\n") ); } Ok(Arc::new(resolver)) }) .await } pub async fn resolver(&self) -> Result<&Arc, AnyError> { self .services .resolver .get_or_try_init_async( async { Ok(Arc::new(CliGraphResolver::new(CliGraphResolverOptions { sloppy_imports_resolver: if self.options.unstable_sloppy_imports() { Some(SloppyImportsResolver::new(self.fs().clone())) } else { None }, node_resolver: Some(self.cli_node_resolver().await?.clone()), npm_resolver: if self.options.no_npm() { None } else { Some(self.npm_resolver().await?.clone()) }, workspace_resolver: self.workspace_resolver().await?.clone(), bare_node_builtins_enabled: self .options .unstable_bare_node_builtins(), maybe_jsx_import_source_config: self .options .workspace() .to_maybe_jsx_import_source_config()?, maybe_vendor_dir: self.options.vendor_dir_path(), }))) } .boxed_local(), ) .await } pub fn maybe_file_watcher_reporter(&self) -> &Option { let maybe_file_watcher_reporter = self .watcher_communicator .as_ref() .map(|i| FileWatcherReporter::new(i.clone())); self .services .maybe_file_watcher_reporter .get_or_init(|| maybe_file_watcher_reporter) } pub fn emit_cache(&self) -> Result<&EmitCache, AnyError> { self.services.emit_cache.get_or_try_init(|| { Ok(EmitCache::new(self.deno_dir()?.gen_cache.clone())) }) } pub fn module_info_cache(&self) -> Result<&Arc, AnyError> { self.services.module_info_cache.get_or_try_init(|| { Ok(Arc::new(ModuleInfoCache::new( self.caches()?.dep_analysis_db(), ))) }) } pub fn code_cache(&self) -> Result<&Arc, AnyError> { self.services.code_cache.get_or_try_init(|| { Ok(Arc::new(CodeCache::new(self.caches()?.code_cache_db()))) }) } pub fn parsed_source_cache(&self) -> &Arc { self .services .parsed_source_cache .get_or_init(Default::default) } pub fn emitter(&self) -> Result<&Arc, AnyError> { self.services.emitter.get_or_try_init(|| { let ts_config_result = self .options .resolve_ts_config_for_emit(TsConfigType::Emit)?; if let Some(ignored_options) = ts_config_result.maybe_ignored_options { warn!("{}", ignored_options); } let (transpile_options, emit_options) = crate::args::ts_config_to_transpile_and_emit_options( ts_config_result.ts_config, )?; Ok(Arc::new(Emitter::new( self.emit_cache()?.clone(), self.parsed_source_cache().clone(), transpile_options, emit_options, ))) }) } pub async fn node_resolver(&self) -> Result<&Arc, AnyError> { self .services .node_resolver .get_or_try_init_async( async { Ok(Arc::new(NodeResolver::new( self.fs().clone(), self.npm_resolver().await?.clone().into_npm_resolver(), ))) } .boxed_local(), ) .await } pub async fn node_code_translator( &self, ) -> Result<&Arc, AnyError> { self .services .node_code_translator .get_or_try_init_async(async { let caches = self.caches()?; let node_analysis_cache = NodeAnalysisCache::new(caches.node_analysis_db()); let cjs_esm_analyzer = CliCjsCodeAnalyzer::new(node_analysis_cache, self.fs().clone()); Ok(Arc::new(NodeCodeTranslator::new( cjs_esm_analyzer, self.fs().clone(), self.node_resolver().await?.clone(), self.npm_resolver().await?.clone().into_npm_resolver(), ))) }) .await } pub async fn type_checker(&self) -> Result<&Arc, AnyError> { self .services .type_checker .get_or_try_init_async(async { Ok(Arc::new(TypeChecker::new( self.caches()?.clone(), self.options.clone(), self.module_graph_builder().await?.clone(), self.node_resolver().await?.clone(), self.npm_resolver().await?.clone(), ))) }) .await } pub async fn module_graph_builder( &self, ) -> Result<&Arc, AnyError> { self .services .module_graph_builder .get_or_try_init_async(async { Ok(Arc::new(ModuleGraphBuilder::new( self.options.clone(), self.caches()?.clone(), self.fs().clone(), self.resolver().await?.clone(), self.npm_resolver().await?.clone(), self.module_info_cache()?.clone(), self.parsed_source_cache().clone(), self.maybe_lockfile().clone(), self.maybe_file_watcher_reporter().clone(), self.emit_cache()?.clone(), self.file_fetcher()?.clone(), self.global_http_cache()?.clone(), ))) }) .await } pub async fn module_graph_creator( &self, ) -> Result<&Arc, AnyError> { self .services .module_graph_creator .get_or_try_init_async(async { Ok(Arc::new(ModuleGraphCreator::new( self.options.clone(), self.npm_resolver().await?.clone(), self.module_graph_builder().await?.clone(), self.type_checker().await?.clone(), ))) }) .await } pub async fn main_module_graph_container( &self, ) -> Result<&Arc, AnyError> { self .services .main_graph_container .get_or_try_init_async(async { Ok(Arc::new(MainModuleGraphContainer::new( self.cli_options().clone(), self.module_load_preparer().await?.clone(), ))) }) .await } pub fn maybe_inspector_server( &self, ) -> Result<&Option>, AnyError> { self.services.maybe_inspector_server.get_or_try_init(|| { match self.options.resolve_inspector_server() { Ok(server) => Ok(server.map(Arc::new)), Err(err) => Err(err), } }) } pub async fn module_load_preparer( &self, ) -> Result<&Arc, AnyError> { self .services .module_load_preparer .get_or_try_init_async(async { Ok(Arc::new(ModuleLoadPreparer::new( self.options.clone(), self.maybe_lockfile().clone(), self.module_graph_builder().await?.clone(), self.text_only_progress_bar().clone(), self.type_checker().await?.clone(), ))) }) .await } pub fn cjs_resolutions(&self) -> &Arc { self.services.cjs_resolutions.get_or_init(Default::default) } pub async fn cli_node_resolver( &self, ) -> Result<&Arc, AnyError> { self .services .cli_node_resolver .get_or_try_init_async(async { Ok(Arc::new(CliNodeResolver::new( self.cjs_resolutions().clone(), self.fs().clone(), self.node_resolver().await?.clone(), self.npm_resolver().await?.clone(), ))) }) .await } pub fn feature_checker(&self) -> &Arc { self.services.feature_checker.get_or_init(|| { let mut checker = FeatureChecker::default(); checker.set_exit_cb(Box::new(crate::unstable_exit_cb)); checker.set_warn_cb(Box::new(crate::unstable_warn_cb)); if self.options.legacy_unstable_flag() { checker.enable_legacy_unstable(); checker.warn_on_legacy_unstable(); } let unstable_features = self.options.unstable_features(); for (flag_name, _, _) in crate::UNSTABLE_GRANULAR_FLAGS { if unstable_features.contains(&flag_name.to_string()) { checker.enable_feature(flag_name); } } Arc::new(checker) }) } pub async fn create_compile_binary_writer( &self, ) -> Result { Ok(DenoCompileBinaryWriter::new( self.deno_dir()?, self.file_fetcher()?, self.http_client_provider(), self.npm_resolver().await?.as_ref(), self.workspace_resolver().await?.as_ref(), self.options.npm_system_info(), )) } pub async fn create_cli_main_worker_factory( &self, ) -> Result { let node_resolver = self.node_resolver().await?; 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), self.options.sub_command().clone(), npm_resolver.clone(), node_resolver.clone(), self.blob_store().clone(), Box::new(CliModuleLoaderFactory::new( &self.options, if self.options.code_cache_enabled() { Some(self.code_cache()?.clone()) } else { None }, self.emitter()?.clone(), self.main_module_graph_container().await?.clone(), self.module_load_preparer().await?.clone(), cli_node_resolver.clone(), NpmModuleLoader::new( self.cjs_resolutions().clone(), self.node_code_translator().await?.clone(), fs.clone(), cli_node_resolver.clone(), ), self.parsed_source_cache().clone(), self.resolver().await?.clone(), )), self.root_cert_store_provider().clone(), self.fs().clone(), maybe_file_watcher_communicator, self.maybe_inspector_server()?.clone(), self.maybe_lockfile().clone(), self.feature_checker().clone(), self.create_cli_main_worker_options()?, self.options.node_ipc_fd(), self.options.serve_port(), self.options.serve_host(), self.options.enable_future_features(), // TODO(bartlomieju): temporarily disabled // self.options.disable_deprecated_api_warning, true, self.options.verbose_deprecated_api_warning, if self.options.code_cache_enabled() { Some(self.code_cache()?.clone()) } else { None }, )) } fn create_cli_main_worker_options( &self, ) -> Result { let create_hmr_runner = if self.options.has_hmr() { let watcher_communicator = self.watcher_communicator.clone().unwrap(); let emitter = self.emitter()?.clone(); let fn_: crate::worker::CreateHmrRunnerCb = Box::new(move |session| { Box::new(HmrRunner::new( emitter.clone(), session, watcher_communicator.clone(), )) }); Some(fn_) } else { None }; let create_coverage_collector = if let Some(coverage_dir) = self.options.coverage_dir() { let coverage_dir = PathBuf::from(coverage_dir); let fn_: crate::worker::CreateCoverageCollectorCb = Box::new(move |session| { Box::new(CoverageCollector::new(coverage_dir.clone(), session)) }); Some(fn_) } else { None }; Ok(CliMainWorkerOptions { argv: self.options.argv().clone(), // This optimization is only available for "run" subcommand // because we need to register new ops for testing and jupyter // integration. skip_op_registration: self.options.sub_command().is_run(), log_level: self.options.log_level().unwrap_or(log::Level::Info).into(), enable_op_summary_metrics: self.options.enable_op_summary_metrics(), 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(), strace_ops: self.options.strace_ops().clone(), is_inspecting: self.options.is_inspecting(), is_npm_main: self.options.is_npm_main(), location: self.options.location_flag().clone(), // if the user ran a binary command, we'll need to set process.argv[0] // to be the name of the binary command instead of deno argv0: self .options .take_binary_npm_command_name() .or(std::env::args().next()), node_debug: std::env::var("NODE_DEBUG").ok(), origin_data_folder_path: Some(self.deno_dir()?.origin_data_folder_path()), seed: self.options.seed(), unsafely_ignore_certificate_errors: self .options .unsafely_ignore_certificate_errors() .clone(), unstable: self.options.legacy_unstable_flag(), create_hmr_runner, create_coverage_collector, }) } }