/
githubmirror
/
deno
Обзор
Документация
Войти
/
githubmirror
/
deno
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
libs/resolver/graph.rs
1 197 строк
36 KB
Nathan Whitaker
fix(resolver): scope sloppy import hints to prepared modules (#36221)
23 июл 2026, 04:23
Не верифицирован
23 июл 2026, 04:23
b23cb76
Код
Авторство
О чём код?
// Copyright 2018-2026 the Deno authors. MIT license. use std::borrow::Cow; use boxed_error::Boxed; use deno_error::JsErrorClass; use deno_graph::JsrLoadError; use deno_graph::Module; use deno_graph::ModuleError; use deno_graph::ModuleErrorKind; use deno_graph::ModuleGraphError; use deno_graph::ModuleLoadError; use deno_graph::Resolution; use deno_graph::ResolutionError; use deno_graph::SpecifierError; use deno_graph::source::ResolveError; use deno_media_type::MediaType; use deno_semver::npm::NpmPackageReqReference; use deno_semver::package::PackageReq; use deno_unsync::sync::AtomicFlag; use import_map::ImportMapErrorKind; use node_resolver::DenoIsBuiltInNodeModuleChecker; use node_resolver::InNpmPackageChecker; use node_resolver::IsBuiltInNodeModuleChecker; use node_resolver::NpmPackageFolderResolver; use node_resolver::UrlOrPath; use node_resolver::errors::NodeJsErrorCode; use node_resolver::errors::NodeJsErrorCoded; use url::Url; use crate::DenoResolveError; use crate::DenoResolverSys; use crate::RawDenoResolverRc; use crate::cjs::CjsTracker; use crate::deno_json::JsxImportSourceConfigResolver; use crate::npm; use crate::npm::managed::ManagedResolvePkgFolderFromDenoReqError; use crate::workspace::MappedResolutionDiagnostic; use crate::workspace::sloppy_imports_resolve; #[allow(clippy::disallowed_types, reason = "definition")] pub type FoundPackageJsonDepFlagRc = deno_maybe_sync::MaybeArc<FoundPackageJsonDepFlag>; /// A flag that indicates if a package.json dependency was /// found during resolution. #[derive(Debug, Default)] pub struct FoundPackageJsonDepFlag(AtomicFlag); impl FoundPackageJsonDepFlag { #[inline(always)] pub fn raise(&self) -> bool { self.0.raise() } #[inline(always)] pub fn is_raised(&self) -> bool { self.0.is_raised() } } #[derive(Debug, deno_error::JsError, Boxed)] pub struct ResolveWithGraphError(pub Box<ResolveWithGraphErrorKind>); impl ResolveWithGraphError { pub fn maybe_specifier(&self) -> Option<Cow<'_, UrlOrPath>> { match self.as_kind() { ResolveWithGraphErrorKind::ManagedResolvePkgFolderFromDenoReq(_) => None, ResolveWithGraphErrorKind::CouldNotResolveNpmReqRef(err) => { err.source.maybe_specifier() } ResolveWithGraphErrorKind::ResolveNpmReqRef(err) => { err.err.maybe_specifier() } ResolveWithGraphErrorKind::Resolution(err) => match err { deno_graph::ResolutionError::InvalidDowngrade { specifier, .. } => { Some(specifier) } deno_graph::ResolutionError::InvalidJsrHttpsTypesImport { specifier, .. } => Some(specifier), deno_graph::ResolutionError::InvalidLocalImport { specifier, .. } => Some(specifier), deno_graph::ResolutionError::ResolverError { .. } | deno_graph::ResolutionError::InvalidSpecifier { .. } => None, } .map(|s| Cow::Owned(UrlOrPath::Url(s.clone()))), ResolveWithGraphErrorKind::Resolve(err) => err.maybe_specifier(), ResolveWithGraphErrorKind::PathToUrl(err) => { Some(Cow::Owned(UrlOrPath::Path(err.0.clone()))) } ResolveWithGraphErrorKind::ResolvePkgFolderFromDenoModule(_) => None, } } pub fn into_deno_graph_error(self) -> deno_graph::source::ResolveError { use deno_graph::source::ResolveError; match self.into_kind() { ResolveWithGraphErrorKind::Resolve(deno_resolve_error) => { deno_resolve_error.into_deno_graph_error() } err => ResolveError::from_err(err), } } } #[derive(Debug, thiserror::Error, deno_error::JsError)] pub enum ResolveWithGraphErrorKind { #[error(transparent)] #[class(inherit)] ManagedResolvePkgFolderFromDenoReq( #[from] ManagedResolvePkgFolderFromDenoReqError, ), #[error(transparent)] #[class(inherit)] CouldNotResolveNpmReqRef(#[from] CouldNotResolveNpmReqRefError), #[error(transparent)] #[class(inherit)] ResolvePkgFolderFromDenoModule( #[from] npm::managed::ResolvePkgFolderFromDenoModuleError, ), #[error(transparent)] #[class(inherit)] ResolveNpmReqRef(#[from] npm::ResolveNpmReqRefError), #[error(transparent)] #[class(inherit)] Resolution(#[from] deno_graph::ResolutionError), #[error(transparent)] #[class(inherit)] Resolve(#[from] DenoResolveError), #[error(transparent)] #[class(inherit)] PathToUrl(#[from] deno_path_util::PathToUrlError), } #[derive(Debug, thiserror::Error, deno_error::JsError)] #[class(inherit)] #[error("Could not resolve '{reference}'")] pub struct CouldNotResolveNpmReqRefError { pub reference: deno_semver::npm::NpmPackageReqReference, #[source] #[inherit] pub source: node_resolver::errors::PackageSubpathFromDenoModuleResolveError, } impl NodeJsErrorCoded for CouldNotResolveNpmReqRefError { fn code(&self) -> node_resolver::errors::NodeJsErrorCode { self.source.code() } } pub struct MappedResolutionDiagnosticWithPosition<'a> { pub diagnostic: &'a MappedResolutionDiagnostic, pub referrer: &'a Url, pub start: deno_graph::Position, } #[allow(clippy::disallowed_types, reason = "definition")] pub type OnMappedResolutionDiagnosticFn = deno_maybe_sync::MaybeArc< dyn Fn(MappedResolutionDiagnosticWithPosition) + Send + Sync, >; pub struct ResolveWithGraphOptions { pub mode: node_resolver::ResolutionMode, pub kind: node_resolver::NodeResolutionKind, /// Whether to maintain npm specifiers as-is. It's necessary for the /// deno_core module loader to resolve npm specifiers as-is so that /// the loader can properly dynamic import and install npm packages /// when managed. pub maintain_npm_specifiers: bool, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum NpmTypesResolutionMode { Strict, FallbackToExecution, } pub type DefaultDenoResolverRc<TSys> = DenoResolverRc< npm::DenoInNpmPackageChecker, DenoIsBuiltInNodeModuleChecker, npm::NpmResolver<TSys>, TSys, >; #[allow(clippy::disallowed_types, reason = "definition")] pub type DenoResolverRc< TInNpmPackageChecker, TIsBuiltInNodeModuleChecker, TNpmPackageFolderResolver, TSys, > = deno_maybe_sync::MaybeArc< DenoResolver< TInNpmPackageChecker, TIsBuiltInNodeModuleChecker, TNpmPackageFolderResolver, TSys, >, >; /// The resolver used in the CLI for resolving and interfacing /// with deno_graph. pub struct DenoResolver< TInNpmPackageChecker: InNpmPackageChecker, TIsBuiltInNodeModuleChecker: IsBuiltInNodeModuleChecker, TNpmPackageFolderResolver: NpmPackageFolderResolver, TSys: DenoResolverSys, > { resolver: RawDenoResolverRc< TInNpmPackageChecker, TIsBuiltInNodeModuleChecker, TNpmPackageFolderResolver, TSys, >, sys: TSys, found_package_json_dep_flag: FoundPackageJsonDepFlagRc, warned_pkgs: deno_maybe_sync::MaybeDashSet<PackageReq>, on_warning: Option<OnMappedResolutionDiagnosticFn>, } impl< TInNpmPackageChecker: InNpmPackageChecker, TIsBuiltInNodeModuleChecker: IsBuiltInNodeModuleChecker, TNpmPackageFolderResolver: NpmPackageFolderResolver, TSys: DenoResolverSys, > std::fmt::Debug for DenoResolver< TInNpmPackageChecker, TIsBuiltInNodeModuleChecker, TNpmPackageFolderResolver, TSys, > { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("DenoResolver").finish() } } impl< TInNpmPackageChecker: InNpmPackageChecker, TIsBuiltInNodeModuleChecker: IsBuiltInNodeModuleChecker, TNpmPackageFolderResolver: NpmPackageFolderResolver, TSys: DenoResolverSys, > DenoResolver< TInNpmPackageChecker, TIsBuiltInNodeModuleChecker, TNpmPackageFolderResolver, TSys, > { pub fn new( resolver: RawDenoResolverRc< TInNpmPackageChecker, TIsBuiltInNodeModuleChecker, TNpmPackageFolderResolver, TSys, >, sys: TSys, found_package_json_dep_flag: FoundPackageJsonDepFlagRc, on_warning: Option<OnMappedResolutionDiagnosticFn>, ) -> Self { Self { resolver, sys, found_package_json_dep_flag, warned_pkgs: Default::default(), on_warning, } } pub fn resolve_with_graph( &self, graph: &deno_graph::ModuleGraph, raw_specifier: &str, referrer: &Url, referrer_range_start: deno_graph::Position, options: ResolveWithGraphOptions, ) -> Result<Url, ResolveWithGraphError> { let resolution = match graph.get(referrer) { Some(Module::Js(module)) => module .dependencies .get(raw_specifier) .map(|d| &d.maybe_code) .unwrap_or(&Resolution::None), _ => &Resolution::None, }; let specifier = match resolution { Resolution::Ok(resolved) => Cow::Borrowed(&resolved.specifier), Resolution::Err(err) => { return Err( ResolveWithGraphErrorKind::Resolution((**err).clone()).into(), ); } Resolution::None => Cow::Owned(self.resolve( raw_specifier, referrer, referrer_range_start, options.mode, options.kind, )?), }; let specifier = match graph.get(&specifier) { Some(Module::Npm(_)) => { if options.maintain_npm_specifiers { specifier.into_owned() } else { let req_ref = NpmPackageReqReference::from_specifier(&specifier).unwrap(); self.resolve_managed_npm_req_ref( &req_ref, Some(referrer), options.mode, options.kind, )? } } Some(Module::Node(module)) => module.specifier.clone(), Some(Module::Js(module)) => module.specifier.clone(), Some(Module::Json(module)) => module.specifier.clone(), Some(Module::Wasm(module)) => module.specifier.clone(), Some(Module::External(module)) => { node_resolver::resolve_specifier_into_node_modules( &self.sys, &module.specifier, ) } None => { if options.maintain_npm_specifiers { specifier.into_owned() } else { match NpmPackageReqReference::from_specifier(&specifier) { Ok(reference) => { let url = self.resolver.resolve_non_workspace_npm_req_ref_to_file( &reference, referrer, options.mode, options.kind, )?; url.into_url()? } _ => specifier.into_owned(), } } } }; Ok(specifier) } pub fn resolve_non_workspace_npm_req_ref_to_file( &self, npm_req_ref: &NpmPackageReqReference, referrer: &Url, resolution_mode: node_resolver::ResolutionMode, resolution_kind: node_resolver::NodeResolutionKind, ) -> Result<node_resolver::UrlOrPath, npm::ResolveNpmReqRefError> { self.resolver.resolve_non_workspace_npm_req_ref_to_file( npm_req_ref, referrer, resolution_mode, resolution_kind, ) } pub fn resolve_managed_npm_req_ref( &self, req_ref: &NpmPackageReqReference, maybe_referrer: Option<&Url>, resolution_mode: node_resolver::ResolutionMode, resolution_kind: node_resolver::NodeResolutionKind, ) -> Result<Url, ResolveWithGraphError> { let node_and_npm_resolver = self.resolver.node_and_npm_resolver.as_ref().unwrap(); let managed_resolver = node_and_npm_resolver .npm_resolver .as_managed() .expect("do not call this unless managed"); let package_folder = managed_resolver .resolve_pkg_folder_from_deno_module_req(req_ref.req())?; Ok( node_and_npm_resolver .node_resolver .resolve_package_subpath_from_deno_module( &package_folder, req_ref.sub_path(), maybe_referrer, resolution_mode, resolution_kind, ) .map_err(|source| CouldNotResolveNpmReqRefError { reference: req_ref.clone(), source, })? .into_url()?, ) } /// Best-effort resolution of a bare specifier against the managed npm /// snapshot, regardless of whether the referrer's `package.json` declares /// it. /// /// `deno compile --bundle` follows `new Worker(new URL(...))` and similar /// references into modules that can live outside the entrypoint's package /// scope (e.g. a worker authored in a sibling source tree, pulled in /// alongside a `dist/`-rooted entrypoint). A bare npm import from such a /// referrer can fail to map even though the package is installed and /// resolves fine elsewhere in the same build. This lets the bundler fall /// back to the snapshot by package name, matching Node/Bun's "find it in a /// reachable node_modules" behavior. /// /// Known limitation: the specifier carries no version constraint, so this /// resolves `name@*`. If the build's snapshot holds more than one version /// of the package, the snapshot picks one by name alone, which may not be /// the version the referrer's own `node_modules` tree would select. Node /// and Bun resolve to the nearest reachable version; this does not. For the /// common single-version case the result is identical. /// /// Returns `None` when npm resolution isn't managed or the package isn't /// present in the snapshot. pub fn resolve_bare_specifier_in_npm_snapshot( &self, raw_specifier: &str, maybe_referrer: Option<&Url>, resolution_mode: node_resolver::ResolutionMode, resolution_kind: node_resolver::NodeResolutionKind, ) -> Option<Url> { let req_ref = NpmPackageReqReference::from_str(&format!("npm:{raw_specifier}")).ok()?; // Bail unless npm resolution is managed; `resolve_managed_npm_req_ref` // unwraps these and would otherwise panic. let node_and_npm_resolver = self.resolver.node_and_npm_resolver.as_ref()?; node_and_npm_resolver.npm_resolver.as_managed()?; self .resolve_managed_npm_req_ref( &req_ref, maybe_referrer, resolution_mode, resolution_kind, ) .ok() } pub fn resolve( &self, raw_specifier: &str, referrer: &Url, referrer_range_start: deno_graph::Position, resolution_mode: node_resolver::ResolutionMode, resolution_kind: node_resolver::NodeResolutionKind, ) -> Result<Url, DenoResolveError> { let resolution = self.resolver.resolve( raw_specifier, referrer, resolution_mode, resolution_kind, )?; if resolution.found_package_json_dep { // mark that we need to do an "npm install" later self.found_package_json_dep_flag.raise(); } if let Some(diagnostic) = resolution.maybe_diagnostic { let diagnostic = &*diagnostic; match diagnostic { MappedResolutionDiagnostic::ConstraintNotMatchedLocalVersion { reference, .. } => { if let Some(on_warning) = &self.on_warning && self.warned_pkgs.insert(reference.req().clone()) { on_warning(MappedResolutionDiagnosticWithPosition { diagnostic, referrer, start: referrer_range_start, }); } } } } Ok(resolution.url) } pub fn as_graph_resolver<'a>( &'a self, cjs_tracker: &'a CjsTracker<TInNpmPackageChecker, TSys>, jsx_import_source_config_resolver: &'a JsxImportSourceConfigResolver, maybe_graph: Option<&'a deno_graph::ModuleGraph>, npm_types_resolution_mode: NpmTypesResolutionMode, ) -> DenoGraphResolverAdapter< 'a, TInNpmPackageChecker, TIsBuiltInNodeModuleChecker, TNpmPackageFolderResolver, TSys, > { DenoGraphResolverAdapter { cjs_tracker, resolver: self, jsx_import_source_config_resolver, maybe_graph, npm_types_resolution_mode, } } } pub struct DenoGraphResolverAdapter< 'a, TInNpmPackageChecker: InNpmPackageChecker, TIsBuiltInNodeModuleChecker: IsBuiltInNodeModuleChecker, TNpmPackageFolderResolver: NpmPackageFolderResolver, TSys: DenoResolverSys, > { cjs_tracker: &'a CjsTracker<TInNpmPackageChecker, TSys>, resolver: &'a DenoResolver< TInNpmPackageChecker, TIsBuiltInNodeModuleChecker, TNpmPackageFolderResolver, TSys, >, jsx_import_source_config_resolver: &'a JsxImportSourceConfigResolver, maybe_graph: Option<&'a deno_graph::ModuleGraph>, npm_types_resolution_mode: NpmTypesResolutionMode, } impl< TInNpmPackageChecker: InNpmPackageChecker, TIsBuiltInNodeModuleChecker: IsBuiltInNodeModuleChecker, TNpmPackageFolderResolver: NpmPackageFolderResolver, TSys: DenoResolverSys, > std::fmt::Debug for DenoGraphResolverAdapter< '_, TInNpmPackageChecker, TIsBuiltInNodeModuleChecker, TNpmPackageFolderResolver, TSys, > { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("DenoGraphResolverAdapter").finish() } } impl< TInNpmPackageChecker: InNpmPackageChecker, TIsBuiltInNodeModuleChecker: IsBuiltInNodeModuleChecker, TNpmPackageFolderResolver: NpmPackageFolderResolver, TSys: DenoResolverSys, > deno_graph::source::Resolver for DenoGraphResolverAdapter< '_, TInNpmPackageChecker, TIsBuiltInNodeModuleChecker, TNpmPackageFolderResolver, TSys, > { fn default_jsx_import_source(&self, referrer: &Url) -> Option<String> { self .jsx_import_source_config_resolver .for_specifier(referrer) .and_then(|c| c.import_source.as_ref().map(|s| s.specifier.clone())) } fn default_jsx_import_source_types(&self, referrer: &Url) -> Option<String> { self .jsx_import_source_config_resolver .for_specifier(referrer) .and_then(|c| c.import_source_types.as_ref().map(|s| s.specifier.clone())) } fn jsx_import_source_module(&self, referrer: &Url) -> &str { self .jsx_import_source_config_resolver .for_specifier(referrer) .map(|c| c.module.as_str()) .unwrap_or(deno_graph::source::DEFAULT_JSX_IMPORT_SOURCE_MODULE) } fn resolve( &self, raw_specifier: &str, referrer_range: &deno_graph::Range, resolution_kind: deno_graph::source::ResolutionKind, ) -> Result<Url, ResolveError> { let resolution_mode = referrer_range .resolution_mode .map(node_resolver::ResolutionMode::from_deno_graph) .unwrap_or_else(|| { self .cjs_tracker .get_referrer_kind(&referrer_range.specifier) }); let node_resolution_kind = node_resolver::NodeResolutionKind::from_deno_graph(resolution_kind); let resolve = |kind| match self.maybe_graph { Some(graph) => self.resolver.resolve_with_graph( graph, raw_specifier, &referrer_range.specifier, referrer_range.range.start, ResolveWithGraphOptions { mode: resolution_mode, kind, maintain_npm_specifiers: false, }, ), None => self .resolver .resolve( raw_specifier, &referrer_range.specifier, referrer_range.range.start, resolution_mode, kind, ) .map_err(|err| err.into()), }; let result = resolve(node_resolution_kind); if node_resolution_kind == node_resolver::NodeResolutionKind::Types && self.npm_types_resolution_mode == NpmTypesResolutionMode::FallbackToExecution && result .as_ref() .is_err_and(|err| err.is_types_not_found_for_npm_resolution()) { return resolve(node_resolver::NodeResolutionKind::Execution) .map_err(|err| err.into_deno_graph_error()); } result.map_err(|err| err.into_deno_graph_error()) } } impl ResolveWithGraphError { fn is_types_not_found_for_npm_resolution(&self) -> bool { match self.as_kind() { ResolveWithGraphErrorKind::CouldNotResolveNpmReqRef(err) => { err.source.code() == NodeJsErrorCode::ERR_TYPES_NOT_FOUND } ResolveWithGraphErrorKind::ResolveNpmReqRef(err) => { err.err.as_kind().maybe_code() == Some(NodeJsErrorCode::ERR_TYPES_NOT_FOUND) } ResolveWithGraphErrorKind::Resolve(err) => { err.maybe_node_code() == Some(NodeJsErrorCode::ERR_TYPES_NOT_FOUND) } _ => false, } } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum EnhanceGraphErrorMode { ShowRange, HideRange, } #[derive(Debug, deno_error::JsError)] #[class(inherit)] pub struct EnhancedGraphError { #[inherit] original: ModuleGraphError, message: String, mode: EnhanceGraphErrorMode, } impl EnhancedGraphError { pub fn original(&self) -> &ModuleGraphError { &self.original } } impl std::fmt::Display for EnhancedGraphError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(&self.message)?; if let Some(range) = self.original.maybe_range() && self.mode == EnhanceGraphErrorMode::ShowRange && !range.specifier.as_str().contains("/$deno$eval") { write!(f, "\n at {}", format_range_with_colors(range))?; } Ok(()) } } impl std::error::Error for EnhancedGraphError {} pub fn enhance_graph_error( sys: &(impl sys_traits::FsMetadata + Clone), error: ModuleGraphError, mode: EnhanceGraphErrorMode, allow_sloppy_imports_hints: bool, // Names of packages importable by bare specifier (workspace members and // packages linked via the "links" field). Used to produce a better hint // when a bare import almost matches one of them. bare_importable_pkg_names: &[String], ) -> EnhancedGraphError { let mut message = match &error { ModuleGraphError::ResolutionError(resolution_error) => { enhanced_resolution_error_message( resolution_error, bare_importable_pkg_names, ) } ModuleGraphError::TypesResolutionError(resolution_error) => { format!( "Failed resolving types. {}", enhanced_resolution_error_message( resolution_error, bare_importable_pkg_names, ) ) } ModuleGraphError::ModuleError(error) => { enhanced_integrity_error_message(error) .or_else(|| { if allow_sloppy_imports_hints { enhanced_sloppy_imports_error_message(sys, error) } else { None } }) .or_else(|| enhanced_unsupported_import_attribute(error)) .unwrap_or_else(|| format_deno_graph_error(error)) } }; if let Some(docs_url) = maybe_docs_url_for_graph_error(&error) { message.push_str(&format!( "\n {} {}", deno_terminal::colors::cyan("docs:"), docs_url, )); } EnhancedGraphError { original: error, message, mode, } } /// Returns a link to relevant Deno documentation for known module graph /// errors so users can quickly find the recommended fix. fn maybe_docs_url_for_graph_error( error: &ModuleGraphError, ) -> Option<&'static str> { let ModuleGraphError::ModuleError(error) = error else { return None; }; match error.as_kind() { ModuleErrorKind::UnsupportedMediaType { media_type: MediaType::Json, .. } | ModuleErrorKind::InvalidTypeAssertion { actual_media_type: MediaType::Json, .. } => Some("https://docs.deno.com/examples/importing_json/"), _ => None, } } /// Adds more explanatory information to a resolution error. pub fn enhanced_resolution_error_message( error: &ResolutionError, bare_importable_pkg_names: &[String], ) -> String { let mut message = format_deno_graph_error(error); let maybe_hint = if let Some(specifier) = get_resolution_error_bare_node_specifier(error) { Some(format!( "If you want to use a built-in Node module, add a \"node:\" prefix (ex. \"node:{specifier}\")." )) } else { get_import_prefix_missing_error(error).map(|specifier| { // If the specifier looks like a workspace member or linked package whose // declared name is scoped (e.g. importing "foo" when "@scope/foo" is // linked), point at the real name instead of suggesting `deno add`. if let Some(name) = maybe_matching_bare_pkg_name(specifier, bare_importable_pkg_names) { format!( "\"{name}\" is available in this workspace (via a workspace member or the \"links\" field). Import it by its full name.", ) } else if specifier.starts_with("@std/") { format!( "If you want to use the JSR package, try running `deno add jsr:{}`", specifier ) } else if specifier.starts_with('@') { format!( "If you want to use a JSR or npm package, try running `deno add jsr:{0}` or `deno add npm:{0}`", specifier ) } else { format!( "If you want to use the npm package, try running `deno add npm:{0}`", specifier ) } }) }; if let Some(hint) = maybe_hint { message.push_str(&format!( "\n {} {}", deno_terminal::colors::cyan("hint:"), hint )); } message } static RUN_WITH_SLOPPY_IMPORTS_MSG: &str = "or run with --sloppy-imports"; fn enhanced_sloppy_imports_error_message( sys: &(impl sys_traits::FsMetadata + Clone), error: &ModuleError, ) -> Option<String> { match error.as_kind() { ModuleErrorKind::Load { specifier, err: ModuleLoadError::Loader(_), .. } // ex. "Is a directory" error | ModuleErrorKind::Missing { specifier, .. } => { let additional_message = maybe_additional_sloppy_imports_message(sys, specifier)?; Some(format!( "{} {}", error, additional_message, )) } _ => None, } } pub fn maybe_additional_sloppy_imports_message( sys: &(impl sys_traits::FsMetadata + Clone), specifier: &Url, ) -> Option<String> { let (resolved, sloppy_reason) = sloppy_imports_resolve( specifier, crate::workspace::ResolutionKind::Execution, sys.clone(), )?; Some(format!( "{} {}", sloppy_reason.suggestion_message_for_specifier(&resolved), RUN_WITH_SLOPPY_IMPORTS_MSG )) } pub fn enhanced_integrity_error_message(err: &ModuleError) -> Option<String> { match err.as_kind() { ModuleErrorKind::Load { specifier, err: ModuleLoadError::Jsr(JsrLoadError::ContentChecksumIntegrity(checksum_err)), .. } => Some(format!( concat!( "Integrity check failed in package. The package may have been tampered with.\n\n", " Specifier: {}\n", " Actual: {}\n", " Expected: {}\n\n", "If you modified your global cache, run again with the --reload flag to restore ", "its state. If you want to modify dependencies locally run again with the ", "--vendor flag or specify `\"vendor\": true` in a deno.json then modify the contents ", "of the vendor/ folder." ), specifier, checksum_err.actual, checksum_err.expected, )), ModuleErrorKind::Load { err: ModuleLoadError::Jsr( JsrLoadError::PackageVersionManifestChecksumIntegrity( package_nv, checksum_err, ), ), .. } => Some(format!( concat!( "Integrity check failed for package. The source code is invalid, as it does not match the expected hash in the lock file.\n\n", " Package: {}\n", " Actual: {}\n", " Expected: {}\n\n", "This could be caused by:\n", " * the lock file may be corrupt\n", " * the source itself may be corrupt\n\n", "Investigate the lockfile; delete it to regenerate the lockfile or --reload to reload the source code from the server." ), package_nv, checksum_err.actual, checksum_err.expected, )), ModuleErrorKind::Load { specifier, err: ModuleLoadError::HttpsChecksumIntegrity(checksum_err), .. } => Some(format!( concat!( "Integrity check failed for remote specifier. The source code is invalid, as it does not match the expected hash in the lock file.\n\n", " Specifier: {}\n", " Actual: {}\n", " Expected: {}\n\n", "This could be caused by:\n", " * the lock file may be corrupt\n", " * the source itself may be corrupt\n\n", "Investigate the lockfile; delete it to regenerate the lockfile or --reload to reload the source code from the server." ), specifier, checksum_err.actual, checksum_err.expected, )), _ => None, } } fn enhanced_unsupported_import_attribute(err: &ModuleError) -> Option<String> { match err.as_kind() { ModuleErrorKind::UnsupportedImportAttributeType { kind, .. } if kind == "bytes" || kind == "css" => { let mut text = format_deno_graph_error(err); text.push_str(&format!( "\n {} run with --unstable-raw-imports", deno_terminal::colors::cyan("hint:") )); Some(text) } _ => None, } } pub fn get_resolution_error_bare_node_specifier( error: &ResolutionError, ) -> Option<&str> { get_resolution_error_bare_specifier(error).filter(|specifier| { DenoIsBuiltInNodeModuleChecker.is_builtin_node_module(specifier) }) } fn get_resolution_error_bare_specifier( error: &ResolutionError, ) -> Option<&str> { if let ResolutionError::InvalidSpecifier { error: SpecifierError::ImportPrefixMissing { specifier, .. }, .. } = error { Some(specifier.as_str()) } else if let ResolutionError::ResolverError { error, .. } = error && let ResolveError::Other(error) = (*error).as_ref() && let Some(error) = error.get_ref().downcast_ref::<import_map::ImportMapError>() && let import_map::ImportMapErrorKind::UnmappedBareSpecifier(specifier, _) = error.as_kind() { Some(specifier.as_str()) } else { None } } /// If `specifier` (a bare import that failed to resolve) matches the unscoped /// name of a known bare-importable package, returns that package's full name. /// /// This catches the common mistake of importing a scoped package by its /// unscoped tail, e.g. importing `"my-pkg"` when `"@scope/my-pkg"` is linked. /// /// If two packages share an unscoped tail (e.g. `@a/foo` and `@b/foo`), the /// first match wins; that's acceptable since this only produces a hint. fn maybe_matching_bare_pkg_name<'a>( specifier: &str, bare_importable_pkg_names: &'a [String], ) -> Option<&'a str> { // Compare only the package portion, ignoring any imported subpath. let specifier_head = specifier.split('/').next().unwrap_or(specifier); bare_importable_pkg_names .iter() .map(|name| name.as_str()) .find(|name| { // The unscoped tail of `@scope/foo` is `foo`; an unscoped name is its // own tail. let unscoped = name .strip_prefix('@') .and_then(|rest| rest.split_once('/')) .map(|(_scope, tail)| tail) .unwrap_or(name); // Only suggest when the user didn't already type the full name (that // would have resolved), but the unscoped tail matches. *name != specifier_head && unscoped == specifier_head }) } fn get_import_prefix_missing_error(error: &ResolutionError) -> Option<&str> { // not exact, but ok because this is just a hint let media_type = MediaType::from_specifier_and_headers(&error.range().specifier, None); if media_type == MediaType::Wasm { return None; } let mut maybe_specifier = None; if let ResolutionError::InvalidSpecifier { error: SpecifierError::ImportPrefixMissing { specifier, .. }, range, } = error { if range.specifier.scheme() == "file" { maybe_specifier = Some(specifier); } } else if let ResolutionError::ResolverError { error, range, .. } = error && range.specifier.scheme() == "file" { match error.as_ref() { ResolveError::Specifier(specifier_error) => { if let SpecifierError::ImportPrefixMissing { specifier, .. } = specifier_error { maybe_specifier = Some(specifier); } } ResolveError::Other(other_error) => { if let Some(SpecifierError::ImportPrefixMissing { specifier, .. }) = other_error.get_ref().downcast_ref::<SpecifierError>() { maybe_specifier = Some(specifier); } else if let Some(err) = other_error .get_ref() .downcast_ref::<import_map::ImportMapError>() && let ImportMapErrorKind::UnmappedBareSpecifier(specifier, _referrer) = err.as_kind() { maybe_specifier = Some(specifier); } } } } if let Some(specifier) = maybe_specifier { // NOTE(bartlomieju): For now, return None if a specifier contains a dot or a space. This is because // suggesting to `deno add bad-module.ts` makes no sense and is worse than not providing // a suggestion at all. This should be improved further in the future if specifier.contains('.') || specifier.contains(' ') { return None; } // Do not return a hint for specifiers starting with `@`, but not containing a `/` if specifier.starts_with('@') && !specifier.contains('/') { return None; } } maybe_specifier.map(|s| s.as_str()) } pub fn format_range_with_colors(referrer: &deno_graph::Range) -> String { use deno_terminal::colors; format!( "{}:{}:{}", colors::cyan(referrer.specifier.as_str()), colors::yellow(&(referrer.range.start.line + 1).to_string()), colors::yellow(&(referrer.range.start.character + 1).to_string()) ) } pub fn format_deno_graph_error(err: &dyn std::error::Error) -> String { use std::fmt::Write; let mut message = format!("{}", err); let mut maybe_source = err.source(); if maybe_source.is_some() { let mut past_message = message.clone(); let mut count = 0; let mut display_count = 0; while let Some(source) = maybe_source { let current_message = format!("{}", source); maybe_source = source.source(); // sometimes an error might be repeated due to // being boxed multiple times in another AnyError if current_message != past_message { write!(message, "\n {}: ", display_count,).unwrap(); for (i, line) in current_message.lines().enumerate() { if i > 0 { write!(message, "\n {}", line).unwrap(); } else { write!(message, "{}", line).unwrap(); } } display_count += 1; } if count > 8 { write!(message, "\n {}: ...", count).unwrap(); break; } past_message = current_message; count += 1; } } message } #[cfg(test)] mod test { use deno_graph::PositionRange; use deno_graph::Range; use deno_graph::ResolutionError; use deno_graph::SpecifierError; use deno_graph::source::ResolveError; use super::*; #[test] fn import_map_node_resolution_error() { let cases = vec![("fs", Some("fs")), ("other", None)]; for (input, output) in cases { let import_map = import_map::ImportMap::new(Url::parse("file:///deno.json").unwrap()); let specifier = Url::parse("file:///file.ts").unwrap(); let err = import_map.resolve(input, &specifier).err().unwrap(); let err = ResolutionError::ResolverError { #[allow( clippy::disallowed_types, reason = "ResolutionError requires Arc" )] error: std::sync::Arc::new(ResolveError::from_err(err)), specifier: input.to_string(), range: Range { specifier, resolution_mode: None, range: PositionRange::zeroed(), }, }; assert_eq!(get_resolution_error_bare_node_specifier(&err), output); } } #[test] fn bare_specifier_node_resolution_error() { let cases = vec![("process", Some("process")), ("other", None)]; for (input, output) in cases { let specifier = Url::parse("file:///file.ts").unwrap(); let err = ResolutionError::InvalidSpecifier { range: Range { specifier, resolution_mode: None, range: PositionRange::zeroed(), }, error: SpecifierError::ImportPrefixMissing { specifier: input.to_string(), }, }; assert_eq!(get_resolution_error_bare_node_specifier(&err), output,); } } #[test] fn matching_bare_pkg_name() { let names = [ "@scope/my-pkg".to_string(), "plain-pkg".to_string(), "@org/utils".to_string(), ]; // Unscoped tail of a scoped package matches. assert_eq!( maybe_matching_bare_pkg_name("my-pkg", &names), Some("@scope/my-pkg") ); // Importing a subpath still matches on the package head. assert_eq!( maybe_matching_bare_pkg_name("utils/helpers", &names), Some("@org/utils") ); // The full scoped name resolves on its own, so no suggestion. assert_eq!(maybe_matching_bare_pkg_name("@scope/my-pkg", &names), None); // An unscoped name that already matches exactly would have resolved. assert_eq!(maybe_matching_bare_pkg_name("plain-pkg", &names), None); // No relation at all. assert_eq!(maybe_matching_bare_pkg_name("unrelated", &names), None); // Nothing linked. assert_eq!(maybe_matching_bare_pkg_name("my-pkg", &[]), None); } }