/
githubmirror
/
deno
Обзор
Документация
Войти
/
githubmirror
/
deno
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
cli/graph_util.rs
1 393 строки
44 KB
Bartek Iwańczuk
refactor(cli): replace single-method flag ext traits with free functions (#36491)
08 авг 2026, 02:53
Не верифицирован
08 авг 2026, 02:53
ce9bc92
Код
Авторство
О чём код?
// Copyright 2018-2026 the Deno authors. MIT license. use std::borrow::Cow; use std::collections::HashMap; use std::collections::HashSet; use std::path::PathBuf; use std::sync::Arc; use deno_ast::MediaType; use deno_config::deno_json; use deno_config::deno_json::CompilerOptionTypesDeserializeError; use deno_config::deno_json::NodeModulesDirMode; use deno_config::workspace::JsrPackageConfig; use deno_core::ModuleSpecifier; use deno_core::error::AnyError; use deno_core::parking_lot::Mutex; use deno_core::serde_json; use deno_error::JsErrorBox; use deno_error::JsErrorClass; use deno_graph::CheckJsOption; use deno_graph::GraphKind; use deno_graph::JsrLoadError; use deno_graph::ModuleError; use deno_graph::ModuleErrorKind; use deno_graph::ModuleGraph; use deno_graph::ModuleGraphError; use deno_graph::ModuleLoadError; use deno_graph::ResolutionError; use deno_graph::SpecifierError; use deno_graph::WorkspaceFastCheckOption; use deno_graph::packages::JsrVersionResolver; use deno_graph::source::Loader; use deno_graph::source::ResolveError; use deno_lib::util::result::downcast_ref_deno_resolve_error; use deno_npm_installer::PackageCaching; use deno_npm_installer::format_unmet_peer_dep_warning; use deno_npm_installer::graph::NpmCachingStrategy; use deno_path_util::url_to_file_path; use deno_resolver::cache::ParsedSourceCache; use deno_resolver::deno_json::CompilerOptionsResolver; use deno_resolver::deno_json::JsxImportSourceConfigResolver; use deno_resolver::deno_json::ToMaybeJsxImportSourceConfigError; use deno_resolver::file_fetcher::GraphLoaderReporterRc; use deno_resolver::graph::EnhanceGraphErrorMode; use deno_resolver::graph::EnhancedGraphError; use deno_resolver::graph::NpmTypesResolutionMode; use deno_resolver::graph::enhance_graph_error; use deno_resolver::graph::enhanced_integrity_error_message; use deno_resolver::graph::format_deno_graph_error; use deno_resolver::npm::DenoInNpmPackageChecker; use deno_runtime::deno_permissions::PermissionsContainer; use deno_semver::SmallStackString; use deno_semver::jsr::JsrDepPackageReq; use deno_semver::npm::NpmPackageReqReference; use import_map::ImportMapErrorKind; use node_resolver::errors::NodeJsErrorCode; use sys_traits::FsMetadata; use crate::args::CliLockfile; use crate::args::CliOptions; use crate::args::DenoSubcommand; use crate::args::config_to_deno_graph_workspace_member; use crate::args::graph_kind; use crate::args::jsr_url; use crate::cache; use crate::cache::GlobalHttpCache; use crate::cache::ModuleInfoCache; use crate::colors; use crate::file_fetcher::CliDenoGraphLoader; use crate::file_fetcher::CliFileFetcher; use crate::npm::CliNpmGraphResolver; use crate::npm::CliNpmInstaller; use crate::npm::CliNpmResolver; use crate::resolver::CliCjsTracker; use crate::resolver::CliResolver; use crate::sys::CliSys; use crate::type_checker::CheckError; use crate::type_checker::CheckOptions; use crate::type_checker::TypeChecker; use crate::util::file_watcher::WatcherCommunicator; use crate::util::fs::canonicalize_path; use crate::util::progress_bar::ProgressBar; #[derive(Clone)] pub struct GraphValidOptions<'a> { pub check_js: CheckJsOption<'a>, pub kind: GraphKind, pub will_type_check: bool, /// Whether to exit the process for integrity check errors such as /// lockfile checksum mismatches and JSR integrity failures. /// Otherwise, surfaces integrity errors as errors. pub exit_integrity_errors: bool, pub allow_unknown_media_types: bool, pub allow_unknown_jsr_exports: bool, /// Whether errors for graph roots without a referrer may probe the file /// system for sloppy-import suggestions. Runtime-created dynamic roots set /// this to false because they were not part of the prepared module graph. pub allow_sloppy_imports_hints_for_unreferenced_roots: bool, /// Lazily collects the names of packages importable by bare specifier /// (workspace members and packages linked via the "links" field), used to /// enhance import errors. Only called when a resolution error is actually /// encountered, so the happy path pays nothing. pub collect_bare_importable_pkg_names: &'a dyn Fn() -> Vec<String>, } /// Check if `roots` and their deps are available. Returns `Ok(())` if /// so. Returns `Err(_)` if there is a known module graph or resolution /// error statically reachable from `roots`. /// /// It is preferable to use this over using deno_graph's API directly /// because it will have enhanced error message information specifically /// for the CLI. pub fn graph_valid( graph: &ModuleGraph, sys: &CliSys, roots: &[ModuleSpecifier], options: GraphValidOptions, ) -> Result<(), JsErrorBox> { if options.exit_integrity_errors { graph_exit_integrity_errors(graph); } let mut errors = graph_walk_errors( graph, sys, roots, GraphWalkErrorsOptions { check_js: options.check_js, kind: options.kind, will_type_check: options.will_type_check, allow_unknown_media_types: options.allow_unknown_media_types, allow_unknown_jsr_exports: options.allow_unknown_jsr_exports, allow_sloppy_imports_hints_for_unreferenced_roots: options .allow_sloppy_imports_hints_for_unreferenced_roots, collect_bare_importable_pkg_names: options .collect_bare_importable_pkg_names, }, ); match errors.next() { Some(error) => Err(JsErrorBox::from_err(error)), _ => { // finally surface the npm resolution result if let Err(err) = &graph.npm_dep_graph_result { return Err(JsErrorBox::new( err.get_class(), format_deno_graph_error(err), )); } Ok(()) } } } #[derive(Clone)] pub struct GraphWalkErrorsOptions<'a> { pub check_js: CheckJsOption<'a>, pub kind: GraphKind, pub will_type_check: bool, pub allow_unknown_media_types: bool, pub allow_unknown_jsr_exports: bool, pub allow_sloppy_imports_hints_for_unreferenced_roots: bool, /// Lazily collects the names of packages importable by bare specifier /// (workspace members and packages linked via the "links" field), used to /// enhance import errors. Only called when a resolution error is actually /// encountered, so the happy path pays nothing. pub collect_bare_importable_pkg_names: &'a dyn Fn() -> Vec<String>, } #[derive(Clone, Copy)] pub struct GraphRootsValidOptions { pub allow_unknown_media_types: bool, pub allow_unknown_jsr_exports: bool, pub allow_sloppy_imports_hints_for_unreferenced_roots: bool, } /// Walks the errors found in the module graph that should be surfaced to users /// and enhances them with CLI information. pub fn graph_walk_errors<'a>( graph: &'a ModuleGraph, sys: &'a CliSys, roots: &'a [ModuleSpecifier], options: GraphWalkErrorsOptions<'a>, ) -> impl Iterator<Item = EnhancedGraphError> + 'a { fn should_ignore_error( sys: &CliSys, allow_unknown_media_types: bool, will_type_check: bool, error: &ModuleGraphError, ) -> bool { if allow_unknown_media_types && matches!( error.as_module_error_kind(), Some(ModuleErrorKind::UnsupportedMediaType { .. }) ) { return true; } // surface these as typescript diagnostics instead will_type_check && has_module_graph_error_for_tsc_diagnostic(sys, error) } let collect_bare_importable_pkg_names = options.collect_bare_importable_pkg_names; // Built lazily on the first error that needs enhancing, then reused for the // rest of the walk. let mut bare_importable_pkg_names: Option<Vec<String>> = None; graph .walk( roots.iter(), deno_graph::WalkOptions { check_js: options.check_js, kind: options.kind, follow_dynamic: false, prefer_fast_check_graph: false, }, ) .errors() .flat_map(move |error| { if should_ignore_error( sys, options.allow_unknown_media_types, options.will_type_check, &error, ) { log::debug!("Ignoring: {}", error); return None; } let is_root = match &error { ModuleGraphError::ResolutionError(_) | ModuleGraphError::TypesResolutionError(_) => false, ModuleGraphError::ModuleError(error) => { roots.contains(error.specifier()) } }; if is_root && options.allow_unknown_jsr_exports && matches!( error.as_module_error_kind(), Some(ModuleErrorKind::Load { err: ModuleLoadError::Jsr(JsrLoadError::UnknownExport { .. }), .. }) ) { return None; } // Roots with no referrer are gated by the flag; everything else always // gets sloppy-import hints. let is_unreferenced_root = is_root && !matches!( &error, ModuleGraphError::ModuleError(error) if error.maybe_referrer().is_some() ); let allow_sloppy_imports_hints = options .allow_sloppy_imports_hints_for_unreferenced_roots || !is_unreferenced_root; let enhanced = enhance_graph_error( sys, error, if is_root { EnhanceGraphErrorMode::HideRange } else { EnhanceGraphErrorMode::ShowRange }, allow_sloppy_imports_hints, bare_importable_pkg_names .get_or_insert_with(&collect_bare_importable_pkg_names), ); Some(enhanced) }) } fn has_module_graph_error_for_tsc_diagnostic( sys: &CliSys, error: &ModuleGraphError, ) -> bool { match error { ModuleGraphError::ModuleError(error) => { module_error_for_tsc_diagnostic(sys, error).is_some() } ModuleGraphError::ResolutionError(error) => { resolution_error_for_tsc_diagnostic(error).is_some() } ModuleGraphError::TypesResolutionError(error) => { resolution_error_for_tsc_diagnostic(error).is_some() } } } pub struct ModuleNotFoundGraphErrorRef<'a> { pub specifier: &'a ModuleSpecifier, pub maybe_range: Option<&'a deno_graph::Range>, } pub fn module_error_for_tsc_diagnostic<'a>( sys: &CliSys, error: &'a ModuleError, ) -> Option<ModuleNotFoundGraphErrorRef<'a>> { match error.as_kind() { ModuleErrorKind::Missing { specifier, maybe_referrer, } => Some(ModuleNotFoundGraphErrorRef { specifier, maybe_range: maybe_referrer.as_ref(), }), ModuleErrorKind::Load { specifier, maybe_referrer, err: ModuleLoadError::Loader(_), } => { if let Ok(path) = deno_path_util::url_to_file_path(specifier) && sys.fs_is_dir_no_err(path) { return Some(ModuleNotFoundGraphErrorRef { specifier, maybe_range: maybe_referrer.as_ref(), }); } None } _ => None, } } #[derive(Debug)] pub struct ResolutionErrorRef<'a> { pub specifier: &'a str, pub range: &'a deno_graph::Range, pub is_module_not_found: bool, } pub fn resolution_error_for_tsc_diagnostic( error: &ResolutionError, ) -> Option<ResolutionErrorRef<'_>> { match error { ResolutionError::InvalidDowngrade { .. } | ResolutionError::InvalidJsrHttpsTypesImport { .. } | ResolutionError::InvalidLocalImport { .. } => None, ResolutionError::InvalidSpecifier { error, range } => match error { SpecifierError::InvalidUrl(..) => None, SpecifierError::ImportPrefixMissing { specifier, .. } => { Some(ResolutionErrorRef { specifier, range, is_module_not_found: false, }) } }, ResolutionError::ResolverError { error, specifier, range, } => match error.as_ref() { ResolveError::Specifier(error) => match error { SpecifierError::InvalidUrl(..) => None, SpecifierError::ImportPrefixMissing { specifier, .. } => { Some(ResolutionErrorRef { specifier, range, is_module_not_found: false, }) } }, ResolveError::Other(error) => { if let Some(error) = error.get_ref().downcast_ref::<import_map::ImportMapError>() { match error.as_kind() { ImportMapErrorKind::JsonParse(_) | ImportMapErrorKind::ImportMapNotObject | ImportMapErrorKind::ImportsFieldNotObject | ImportMapErrorKind::ScopesFieldNotObject | ImportMapErrorKind::ScopePrefixNotObject(_) | ImportMapErrorKind::BlockedByNullEntry(_) | ImportMapErrorKind::SpecifierResolutionFailure { .. } | ImportMapErrorKind::SpecifierBacktracksAbovePrefix { .. } => None, ImportMapErrorKind::UnmappedBareSpecifier(specifier, _) => { Some(ResolutionErrorRef { specifier, range, is_module_not_found: false, }) } } } else if let Some(err) = downcast_ref_deno_resolve_error(error) { let use_module_not_found_error = match err.maybe_node_code()? { NodeJsErrorCode::ERR_INVALID_MODULE_SPECIFIER | NodeJsErrorCode::ERR_INVALID_PACKAGE_CONFIG | NodeJsErrorCode::ERR_UNKNOWN_FILE_EXTENSION | NodeJsErrorCode::ERR_UNSUPPORTED_ESM_URL_SCHEME | NodeJsErrorCode::ERR_INVALID_FILE_URL_PATH => return None, // use generic "Cannot find module" message NodeJsErrorCode::ERR_MODULE_NOT_FOUND | NodeJsErrorCode::ERR_TYPES_NOT_FOUND => true, // use the original error message NodeJsErrorCode::ERR_UNKNOWN_BUILTIN_MODULE | NodeJsErrorCode::ERR_INVALID_PACKAGE_TARGET | NodeJsErrorCode::ERR_UNSUPPORTED_DIR_IMPORT | NodeJsErrorCode::ERR_PACKAGE_IMPORT_NOT_DEFINED | NodeJsErrorCode::ERR_PACKAGE_PATH_NOT_EXPORTED => false, }; Some(ResolutionErrorRef { specifier, range, is_module_not_found: use_module_not_found_error, }) } else { None } } }, } } pub fn graph_exit_integrity_errors(graph: &ModuleGraph) { for error in graph.module_errors() { exit_for_integrity_error(error); } } fn exit_for_integrity_error(err: &ModuleError) { if let Some(err_message) = enhanced_integrity_error_message(err) { log::error!("{} {}", colors::red("error:"), err_message); deno_runtime::exit(10); } } pub struct CreateGraphOptions<'a> { pub graph_kind: GraphKind, pub roots: Vec<ModuleSpecifier>, pub imports: Vec<deno_graph::ReferrerImports>, pub is_dynamic: bool, /// Specify `None` to use the default CLI loader. pub loader: Option<&'a dyn Loader>, pub npm_caching: NpmCachingStrategy, } pub struct CreatePublishGraphOptions<'a> { pub packages: &'a [JsrPackageConfig], pub build_fast_check_graph: bool, pub validate_graph: bool, /// Whether to skip exports that can't be analyzed as modules /// (ex. CSS files) instead of surfacing diagnostics for them. /// This is used when linting because validating the exports is /// the responsibility of `deno publish`. /// See https://github.com/denoland/deno/issues/26308 pub skip_unanalyzable_exports: bool, } fn is_analyzable_export(url: &ModuleSpecifier) -> bool { match MediaType::from_specifier(url) { MediaType::JavaScript | MediaType::Jsx | MediaType::Mjs | MediaType::Cjs | MediaType::TypeScript | MediaType::Mts | MediaType::Cts | MediaType::Dts | MediaType::Dmts | MediaType::Dcts | MediaType::Tsx | MediaType::Json | MediaType::Wasm => true, MediaType::Css | MediaType::Jsonc | MediaType::Json5 | MediaType::Html | MediaType::Markdown | MediaType::Sql | MediaType::SourceMap | MediaType::Unknown => false, } } pub struct ModuleGraphCreator { options: Arc<CliOptions>, module_graph_builder: Arc<ModuleGraphBuilder>, type_checker: Arc<TypeChecker>, } impl ModuleGraphCreator { pub fn new( options: Arc<CliOptions>, module_graph_builder: Arc<ModuleGraphBuilder>, type_checker: Arc<TypeChecker>, ) -> Self { Self { options, module_graph_builder, type_checker, } } pub fn module_graph_builder(&self) -> &Arc<ModuleGraphBuilder> { &self.module_graph_builder } pub async fn create_graph( &self, graph_kind: GraphKind, roots: Vec<ModuleSpecifier>, npm_caching: NpmCachingStrategy, ) -> Result<deno_graph::ModuleGraph, AnyError> { let mut cache = self .module_graph_builder .create_graph_loader_with_root_permissions(); self .create_graph_with_loader(graph_kind, roots, &mut cache, npm_caching) .await } pub async fn create_graph_with_loader( &self, graph_kind: GraphKind, roots: Vec<ModuleSpecifier>, loader: &mut dyn Loader, npm_caching: NpmCachingStrategy, ) -> Result<ModuleGraph, AnyError> { self .create_graph_with_options(CreateGraphOptions { is_dynamic: false, graph_kind, roots, imports: self .module_graph_builder .maybe_resolve_ts_config_imports(graph_kind), loader: Some(loader), npm_caching, }) .await } pub async fn create_publish_graph( &self, options: CreatePublishGraphOptions<'_>, ) -> Result<ModuleGraph, AnyError> { struct PublishLoader(CliDenoGraphLoader); impl Loader for PublishLoader { fn load( &self, specifier: &deno_ast::ModuleSpecifier, options: deno_graph::source::LoadOptions, ) -> deno_graph::source::LoadFuture { if matches!(specifier.scheme(), "bun" | "virtual" | "cloudflare") { Box::pin(std::future::ready(Ok(Some( deno_graph::source::LoadResponse::External { specifier: specifier.clone(), }, )))) } else if matches!(specifier.scheme(), "http" | "https") && !specifier.as_str().starts_with(jsr_url().as_str()) { // mark non-JSR remote modules as external so we don't need --allow-import // permissions as these will error out later when publishing Box::pin(std::future::ready(Ok(Some( deno_graph::source::LoadResponse::External { specifier: specifier.clone(), }, )))) } else { self.0.load(specifier, options) } } } fn graph_has_external_remote(graph: &ModuleGraph) -> bool { // Earlier on, we marked external non-JSR modules as external. // If the graph contains any of those, it would cause type checking // to crash, so since publishing is going to fail anyway, skip type // checking. graph.modules().any(|module| match module { deno_graph::Module::External(external_module) => { matches!(external_module.specifier.scheme(), "http" | "https") } _ => false, }) } let mut roots = Vec::new(); for package_config in options.packages { let export_urls = package_config.config_file.resolve_export_value_urls()?; if options.skip_unanalyzable_exports { roots.extend(export_urls.into_iter().filter(is_analyzable_export)); } else { roots.extend(export_urls); } } let loader = self .module_graph_builder .create_graph_loader_with_root_permissions(); let publish_loader = PublishLoader(loader); let mut graph = self .create_graph_with_options(CreateGraphOptions { is_dynamic: false, graph_kind: deno_graph::GraphKind::All, roots, // do not include the tsconfig imports for `deno publish` imports: Vec::new(), loader: Some(&publish_loader), npm_caching: self.options.default_npm_caching_strategy(), }) .await?; if options.validate_graph { self.graph_valid(&graph)?; } if self.options.type_check_mode().is_true() && !graph_has_external_remote(&graph) { // Include compilerOptions.types imports for type checking so that // ambient type declarations (e.g. Vite's import.meta.hot) are // available, but do not include them in the publish graph itself. let types_imports = self .module_graph_builder .resolve_compiler_options_types_imports(deno_graph::GraphKind::All); if !types_imports.is_empty() { let mut type_check_graph = graph.clone(); self .module_graph_builder .build_graph_with_npm_resolution( &mut type_check_graph, BuildGraphRequest::Roots(vec![], types_imports), BuildGraphWithNpmOptions { is_dynamic: false, loader: Some(&publish_loader), npm_caching: self.options.default_npm_caching_strategy(), }, ) .await?; self.type_check_graph(type_check_graph)?; } else { self.type_check_graph(graph.clone())?; } } if options.build_fast_check_graph { let mut fast_check_workspace_members = options .packages .iter() .map(|p| config_to_deno_graph_workspace_member(&p.config_file)) .collect::<Result<Vec<_>, _>>()?; if options.skip_unanalyzable_exports { for member in &mut fast_check_workspace_members { let base = member.base.clone(); member.exports.retain(|_, value| { base .join(value) .map(|url| is_analyzable_export(&url)) .unwrap_or(true) }); } } self.module_graph_builder.build_fast_check_graph( &mut graph, BuildFastCheckGraphOptions { workspace_fast_check: WorkspaceFastCheckOption::Enabled( &fast_check_workspace_members, ), fast_check_dts: false, }, )?; } Ok(graph) } pub async fn create_graph_with_options( &self, options: CreateGraphOptions<'_>, ) -> Result<ModuleGraph, AnyError> { let mut graph = ModuleGraph::new(options.graph_kind); self .module_graph_builder .build_graph_with_npm_resolution( &mut graph, BuildGraphRequest::Roots(options.roots, options.imports), BuildGraphWithNpmOptions { is_dynamic: options.is_dynamic, loader: options.loader, npm_caching: options.npm_caching, }, ) .await?; Ok(graph) } pub async fn create_graph_and_maybe_check( &self, roots: Vec<ModuleSpecifier>, ) -> Result<Arc<deno_graph::ModuleGraph>, AnyError> { let graph_kind = graph_kind(self.options.type_check_mode()); let graph = self .create_graph_with_options(CreateGraphOptions { is_dynamic: false, graph_kind, roots, imports: self .module_graph_builder .maybe_resolve_ts_config_imports(graph_kind), loader: None, npm_caching: self.options.default_npm_caching_strategy(), }) .await?; self.graph_valid(&graph)?; if self.options.type_check_mode().is_true() { // provide the graph to the type checker, then get it back after it's done let graph = self.type_check_graph(graph)?; Ok(graph) } else { Ok(Arc::new(graph)) } } pub fn graph_valid(&self, graph: &ModuleGraph) -> Result<(), JsErrorBox> { self.module_graph_builder.graph_valid(graph) } fn type_check_graph( &self, graph: ModuleGraph, ) -> Result<Arc<ModuleGraph>, CheckError> { self.type_checker.check( graph, CheckOptions { build_fast_check_graph: true, lib: self.options.ts_type_lib_window(), reload: self.options.reload_flag(), type_check_mode: self.options.type_check_mode(), }, ) } } pub struct BuildFastCheckGraphOptions<'a> { /// Whether to do fast check on workspace members. This /// is mostly only useful when publishing. pub workspace_fast_check: deno_graph::WorkspaceFastCheckOption<'a>, /// Whether to generate .d.ts files during fast check. pub fast_check_dts: bool, } #[derive(Debug, thiserror::Error, deno_error::JsError)] pub enum BuildGraphWithNpmResolutionError { #[class(inherit)] #[error(transparent)] CompilerOptionTypesDeserialize(#[from] CompilerOptionTypesDeserializeError), #[class(inherit)] #[error(transparent)] SerdeJson(#[from] serde_json::Error), #[class(inherit)] #[error(transparent)] ToMaybeJsxImportSourceConfig(#[from] ToMaybeJsxImportSourceConfigError), #[class(inherit)] #[error(transparent)] NodeModulesDirParse(#[from] deno_json::NodeModulesDirParseError), #[class(inherit)] #[error(transparent)] Other(#[from] JsErrorBox), #[class(generic)] #[error( "Resolving npm specifier entrypoints this way is currently not supported with \"nodeModules\": \"manual\". In the meantime, try with --node-modules-dir=auto instead" )] UnsupportedNpmSpecifierEntrypointResolutionWay, } pub enum BuildGraphRequest { Roots(Vec<ModuleSpecifier>, Vec<deno_graph::ReferrerImports>), Reload(Vec<ModuleSpecifier>), } pub struct BuildGraphWithNpmOptions<'a> { pub is_dynamic: bool, /// Specify `None` to use the default CLI loader. pub loader: Option<&'a dyn Loader>, pub npm_caching: NpmCachingStrategy, } pub struct ModuleGraphBuilder { caches: Arc<cache::Caches>, cjs_tracker: Arc<CliCjsTracker>, cli_options: Arc<CliOptions>, compiler_options_resolver: Arc<CompilerOptionsResolver>, file_fetcher: Arc<CliFileFetcher>, global_http_cache: Arc<GlobalHttpCache>, in_npm_pkg_checker: DenoInNpmPackageChecker, jsr_version_resolver: Arc<JsrVersionResolver>, lockfile: Option<Arc<CliLockfile>>, maybe_reporter: Option<Arc<dyn deno_graph::source::Reporter>>, module_info_cache: Arc<ModuleInfoCache>, npm_graph_resolver: Arc<CliNpmGraphResolver>, npm_installer: Option<Arc<CliNpmInstaller>>, npm_resolver: CliNpmResolver, parsed_source_cache: Arc<ParsedSourceCache>, progress_bar: ProgressBar, resolver: Arc<CliResolver>, root_permissions_container: PermissionsContainer, sys: CliSys, load_reporter: Option<GraphLoaderReporterRc>, } impl ModuleGraphBuilder { #[allow(clippy::too_many_arguments, reason = "construction")] pub fn new( caches: Arc<cache::Caches>, cjs_tracker: Arc<CliCjsTracker>, cli_options: Arc<CliOptions>, compiler_options_resolver: Arc<CompilerOptionsResolver>, file_fetcher: Arc<CliFileFetcher>, global_http_cache: Arc<GlobalHttpCache>, in_npm_pkg_checker: DenoInNpmPackageChecker, jsr_version_resolver: Arc<JsrVersionResolver>, lockfile: Option<Arc<CliLockfile>>, maybe_reporter: Option<Arc<dyn deno_graph::source::Reporter>>, module_info_cache: Arc<ModuleInfoCache>, npm_graph_resolver: Arc<CliNpmGraphResolver>, npm_installer: Option<Arc<CliNpmInstaller>>, npm_resolver: CliNpmResolver, parsed_source_cache: Arc<ParsedSourceCache>, progress_bar: ProgressBar, resolver: Arc<CliResolver>, root_permissions_container: PermissionsContainer, sys: CliSys, load_reporter: Option<GraphLoaderReporterRc>, ) -> Self { Self { caches, cjs_tracker, cli_options, compiler_options_resolver, file_fetcher, global_http_cache, in_npm_pkg_checker, jsr_version_resolver, lockfile, maybe_reporter, module_info_cache, npm_graph_resolver, npm_installer, npm_resolver, parsed_source_cache, progress_bar, resolver, root_permissions_container, sys, load_reporter, } } pub async fn build_graph_roots_with_npm_resolution( &self, graph: &mut ModuleGraph, roots: Vec<ModuleSpecifier>, options: BuildGraphWithNpmOptions<'_>, ) -> Result<(), BuildGraphWithNpmResolutionError> { let request = BuildGraphRequest::Roots( roots, self.maybe_resolve_ts_config_imports(graph.graph_kind()), ); self .build_graph_with_npm_resolution( graph, request, BuildGraphWithNpmOptions { is_dynamic: options.is_dynamic, loader: options.loader, npm_caching: options.npm_caching, }, ) .await } pub async fn build_graph_with_npm_resolution( &self, graph: &mut ModuleGraph, request: BuildGraphRequest, options: BuildGraphWithNpmOptions<'_>, ) -> Result<(), BuildGraphWithNpmResolutionError> { #[allow(clippy::large_enum_variant, reason = "not important")] enum LoaderRef<'a> { Borrowed(&'a dyn Loader), Owned(CliDenoGraphLoader), } impl LoaderRef<'_> { pub fn as_loader(&mut self) -> &dyn Loader { match self { Self::Borrowed(loader) => *loader, Self::Owned(loader) => loader, } } } let _clear_guard = self.progress_bar.deferred_keep_initialize_alive(); let analyzer = self.module_info_cache.as_module_analyzer(); let mut loader = match options.loader { Some(loader) => LoaderRef::Borrowed(loader), None => { LoaderRef::Owned(self.create_graph_loader_with_root_permissions()) } }; let jsx_import_source_config_resolver = JsxImportSourceConfigResolver::from_compiler_options_resolver( &self.compiler_options_resolver, )?; let graph_resolver = self.resolver.as_graph_resolver( self.cjs_tracker.as_ref(), &jsx_import_source_config_resolver, None, // When type checking, an npm package that has no type declarations // (e.g. a "bring your own node_modules" dependency without bundled // types and without a corresponding `@types` package) should be // treated as untyped rather than producing a hard "Failed resolving // types" error. Falling back to the execution entrypoint matches the // behaviour of the managed npm resolver. See: // https://github.com/denoland/deno/issues/23507 NpmTypesResolutionMode::FallbackToExecution, ); let maybe_reporter = self.maybe_reporter.as_deref(); let mut locker = self.lockfile.as_ref().map(|l| l.as_deno_graph_locker()); macro_rules! build_options { ($npm_resolver:expr, $resolver:expr) => { deno_graph::BuildOptions { skip_dynamic_deps: self.cli_options.unstable_lazy_dynamic_imports() && graph.graph_kind() == GraphKind::CodeOnly, is_dynamic: options.is_dynamic, passthrough_jsr_specifiers: false, executor: Default::default(), file_system: &self.sys, jsr_metadata_store: None, jsr_url_provider: &CliJsrUrlProvider, jsr_version_resolver: Cow::Borrowed( self.jsr_version_resolver.as_ref(), ), npm_resolver: $npm_resolver, module_analyzer: &analyzer, module_info_cacher: self.module_info_cache.as_ref(), reporter: maybe_reporter, resolver: Some($resolver), locker: locker.as_mut().map(|l| l as _), unstable_bytes_imports: self.cli_options.unstable_raw_imports(), unstable_text_imports: true, unstable_css_imports: self.cli_options.unstable_raw_imports(), unstable_config_imports: false, } }; } self .build_graph_with_npm_resolution_and_build_options( graph, request, loader.as_loader(), build_options!(Some(self.npm_graph_resolver.as_ref()), &graph_resolver), options.npm_caching, ) .await?; if self.analyze_npm_sources() { let has_npm_specifier = graph .modules() .any(|m| matches!(m, deno_graph::Module::Npm(_))) || graph.roots.iter().any(|r| r.scheme() == "npm"); if has_npm_specifier { // go through the graph and resolve npm: modules to real js modules let cloned_graph = graph.clone(); let graph_resolver = self.resolver.as_graph_resolver( self.cjs_tracker.as_ref(), &jsx_import_source_config_resolver, Some(&cloned_graph), NpmTypesResolutionMode::FallbackToExecution, ); graph .build_npm_packages( self.cli_options.start_dir.dir_url(), deno_graph::source::ResolutionKind::Types, loader.as_loader(), build_options!(None, &graph_resolver), ) .await; } } Ok(()) } async fn build_graph_with_npm_resolution_and_build_options<'a>( &self, graph: &mut ModuleGraph, request: BuildGraphRequest, loader: &'a dyn deno_graph::source::Loader, options: deno_graph::BuildOptions<'a>, npm_caching: NpmCachingStrategy, ) -> Result<(), BuildGraphWithNpmResolutionError> { // ensure an "npm install" is done if the user has explicitly // opted into using a node_modules directory if self .cli_options .specified_node_modules_dir()? .map(|m| m == NodeModulesDirMode::Auto) .unwrap_or(false) && let Some(npm_installer) = &self.npm_installer { let already_done = npm_installer .ensure_top_level_package_json_install() .await?; if !already_done && matches!(npm_caching, NpmCachingStrategy::Eager) { npm_installer.cache_packages(PackageCaching::All).await?; } } // fill the graph with the information from the lockfile let is_first_execution = graph.roots.is_empty(); if is_first_execution { // populate the information from the lockfile if let Some(lockfile) = &self.lockfile { lockfile.fill_graph(graph) } } let initial_redirects_len = graph.redirects.len(); let initial_package_deps_len = graph.packages.package_deps_sum(); let initial_package_mappings_len = graph.packages.mappings().len(); match request { BuildGraphRequest::Roots(roots, imports) => { if roots.iter().any(|r| r.scheme() == "npm") && self.npm_resolver.is_byonm() && !self.analyze_npm_sources() { return Err(BuildGraphWithNpmResolutionError::UnsupportedNpmSpecifierEntrypointResolutionWay); } graph.build(roots, imports, loader, options).await; } BuildGraphRequest::Reload(urls) => { graph.reload(urls, loader, options).await } } let has_redirects_changed = graph.redirects.len() != initial_redirects_len; let has_jsr_package_deps_changed = graph.packages.package_deps_sum() != initial_package_deps_len; let has_jsr_package_mappings_changed = graph.packages.mappings().len() != initial_package_mappings_len; if (has_redirects_changed || has_jsr_package_deps_changed || has_jsr_package_mappings_changed) && let Some(lockfile) = &self.lockfile { let mut lockfile = lockfile.lock(); // https redirects if has_redirects_changed { let graph_redirects = graph.redirects.iter().filter(|(from, _)| { !matches!(from.scheme(), "npm" | "file" | "deno") }); for (from, to) in graph_redirects { lockfile.insert_redirect(from.to_string(), to.to_string()); } } // jsr package mappings if has_jsr_package_mappings_changed { for (from, to) in graph.packages.mappings() { lockfile.insert_package_specifier( JsrDepPackageReq::jsr(from.clone()), to.version.to_custom_string::<SmallStackString>(), ); } } // jsr packages if has_jsr_package_deps_changed { for (nv, deps) in graph.packages.packages_with_deps() { lockfile.add_package_deps(nv, deps.cloned()); } } } // Report any unmet peer dependency issues found during npm resolution. This // is done here, after the graph is built, so the warning can point at the // user modules that import the offending packages. if let Some(npm_installer) = &self.npm_installer { let diagnostics = npm_installer.take_unmet_peer_diagnostics(); if !diagnostics.is_empty() && log::log_enabled!(log::Level::Warn) { // Only the top-level package of each diagnostic (its outermost // ancestor) is annotated, so restrict the graph walk to those. let wanted: HashSet<String> = diagnostics .iter() .filter_map(|d| d.ancestors.last().map(|nv| nv.to_string())) .collect(); let importers = self.npm_peer_dep_importers(graph, &wanted); log::warn!( "{}", format_unmet_peer_dep_warning(&diagnostics, &importers) ); } } Ok(()) } /// Builds a map from a top-level npm package (`name@version`) to the user /// modules that import it, used to annotate peer dependency warnings with the /// source of a transitively introduced package. Only packages in `wanted` /// are resolved, to avoid a full sweep of the graph. fn npm_peer_dep_importers( &self, graph: &ModuleGraph, wanted: &HashSet<String>, ) -> HashMap<String, Vec<String>> { let Some(managed) = self.npm_resolver.as_managed() else { return HashMap::new(); }; let mut importers: HashMap<String, Vec<String>> = HashMap::new(); for module in graph.modules() { let referrer = module.specifier(); for dep in module.dependencies().values() { for specifier in [dep.get_code(), dep.get_type()].into_iter().flatten() { let Ok(npm_ref) = NpmPackageReqReference::from_specifier(specifier) else { continue; }; let Ok(id) = managed.resolve_pkg_id_from_deno_module_req(npm_ref.req()) else { continue; }; let nv = id.nv.to_string(); if !wanted.contains(&nv) { continue; } let entry = importers.entry(nv).or_default(); let referrer = referrer.to_string(); if !entry.contains(&referrer) { entry.push(referrer); } } } } importers } pub fn build_fast_check_graph( &self, graph: &mut ModuleGraph, options: BuildFastCheckGraphOptions, ) -> Result<(), ToMaybeJsxImportSourceConfigError> { if !graph.graph_kind().include_types() { return Ok(()); } log::debug!("Building fast check graph"); let fast_check_cache = if matches!( options.workspace_fast_check, deno_graph::WorkspaceFastCheckOption::Disabled ) { Some(cache::FastCheckCache::new(self.caches.fast_check_db())) } else { None }; let parser = self.parsed_source_cache.as_capturing_parser(); let jsx_import_source_config_resolver = JsxImportSourceConfigResolver::from_compiler_options_resolver( &self.compiler_options_resolver, )?; let graph_resolver = self.resolver.as_graph_resolver( self.cjs_tracker.as_ref(), &jsx_import_source_config_resolver, None, NpmTypesResolutionMode::Strict, ); graph.build_fast_check_type_graph( deno_graph::BuildFastCheckTypeGraphOptions { es_parser: Some(&parser), fast_check_cache: fast_check_cache.as_ref().map(|c| c as _), fast_check_dts: options.fast_check_dts, jsr_url_provider: &CliJsrUrlProvider, resolver: Some(&graph_resolver), workspace_fast_check: options.workspace_fast_check, }, ); Ok(()) } /// Creates the default loader used for creating a graph. pub fn create_graph_loader_with_root_permissions( &self, ) -> CliDenoGraphLoader { self.create_graph_loader_with_permissions( self.root_permissions_container.clone(), None, ) } pub fn create_graph_loader_with_permissions( &self, permissions: PermissionsContainer, file_permission_api_name: Option<&'static str>, ) -> CliDenoGraphLoader { CliDenoGraphLoader::new( self.file_fetcher.clone(), self.global_http_cache.clone(), self.in_npm_pkg_checker.clone(), self.sys.clone(), deno_resolver::file_fetcher::DenoGraphLoaderOptions { file_header_overrides: self.cli_options.resolve_file_header_overrides(), permissions: Some(permissions), file_permission_api_name, reporter: self.load_reporter.clone(), include_npm_sources: self.analyze_npm_sources(), }, ) } fn analyze_npm_sources(&self) -> bool { // only analyze npm packages with deno_graph for deno doc matches!(self.cli_options.sub_command(), DenoSubcommand::Doc(_)) } /// Check if `roots` and their deps are available. Returns `Ok(())` if /// so. Returns `Err(_)` if there is a known module graph or resolution /// error statically reachable from `roots` and not a dynamic import. pub fn graph_valid(&self, graph: &ModuleGraph) -> Result<(), JsErrorBox> { self.graph_roots_valid( graph, &graph.roots.iter().cloned().collect::<Vec<_>>(), GraphRootsValidOptions { allow_unknown_media_types: false, allow_unknown_jsr_exports: false, allow_sloppy_imports_hints_for_unreferenced_roots: true, }, ) } pub fn graph_roots_valid( &self, graph: &ModuleGraph, roots: &[ModuleSpecifier], options: GraphRootsValidOptions, ) -> Result<(), JsErrorBox> { let will_type_check = self.cli_options.type_check_mode().is_true(); let collect_bare_importable_pkg_names = || { self .cli_options .workspace() .resolver_jsr_pkgs() .map(|pkg| pkg.name) .collect::<Vec<_>>() }; graph_valid( graph, &self.sys, roots, GraphValidOptions { kind: if will_type_check { GraphKind::All } else { GraphKind::CodeOnly }, will_type_check, check_js: CheckJsOption::Custom( self.compiler_options_resolver.as_ref(), ), exit_integrity_errors: true, allow_unknown_media_types: options.allow_unknown_media_types, allow_unknown_jsr_exports: options.allow_unknown_jsr_exports, allow_sloppy_imports_hints_for_unreferenced_roots: options .allow_sloppy_imports_hints_for_unreferenced_roots, collect_bare_importable_pkg_names: &collect_bare_importable_pkg_names, }, ) } fn resolve_compiler_options_types_imports( &self, graph_kind: GraphKind, ) -> Vec<deno_graph::ReferrerImports> { if graph_kind.include_types() { self .compiler_options_resolver .to_compiler_options_types_imports() } else { Vec::new() } } pub(crate) fn maybe_resolve_ts_config_imports( &self, graph_kind: GraphKind, ) -> Vec<deno_graph::ReferrerImports> { if graph_kind.include_types() { self.compiler_options_resolver.to_graph_imports() } else { Vec::new() } } } /// Gets if any of the specified root's "file:" dependents are in the /// provided changed set. pub fn has_graph_root_local_dependent_changed( graph: &ModuleGraph, root: &ModuleSpecifier, canonicalized_changed_paths: &HashSet<PathBuf>, ) -> bool { let mut dependent_specifiers = graph.walk( std::iter::once(root), deno_graph::WalkOptions { follow_dynamic: true, kind: GraphKind::All, prefer_fast_check_graph: true, check_js: CheckJsOption::True, }, ); while let Some((s, _)) = dependent_specifiers.next() { if let Ok(path) = url_to_file_path(s) { if let Ok(path) = canonicalize_path(&path) && canonicalized_changed_paths.contains(&path) { return true; } } else { // skip walking this remote module's dependencies dependent_specifiers.skip_previous_dependencies(); } } false } #[derive(Clone, Debug)] pub struct FileWatcherReporter { watcher_communicator: Arc<WatcherCommunicator>, file_paths: Arc<Mutex<Vec<PathBuf>>>, } impl FileWatcherReporter { pub fn new(watcher_communicator: Arc<WatcherCommunicator>) -> Self { Self { watcher_communicator, file_paths: Default::default(), } } } impl deno_graph::source::Reporter for FileWatcherReporter { fn on_load( &self, specifier: &ModuleSpecifier, modules_done: usize, modules_total: usize, ) { let mut file_paths = self.file_paths.lock(); if specifier.scheme() == "file" { // Don't trust that the path is a valid path at this point: // https://github.com/denoland/deno/issues/26209. if let Ok(file_path) = specifier.to_file_path() { file_paths.push(file_path); } } if modules_done == modules_total { self .watcher_communicator .watch_paths(file_paths.drain(..).collect()) .unwrap(); } } } #[derive(Debug, Default, Clone, Copy)] pub struct CliJsrUrlProvider; impl deno_graph::source::JsrUrlProvider for CliJsrUrlProvider { fn url(&self) -> &'static ModuleSpecifier { jsr_url() } }