diff options
Diffstat (limited to 'resolvers/node')
-rw-r--r-- | resolvers/node/Cargo.toml | 3 | ||||
-rw-r--r-- | resolvers/node/analyze.rs | 56 | ||||
-rw-r--r-- | resolvers/node/errors.rs | 128 | ||||
-rw-r--r-- | resolvers/node/lib.rs | 10 | ||||
-rw-r--r-- | resolvers/node/npm.rs | 15 | ||||
-rw-r--r-- | resolvers/node/package_json.rs | 106 | ||||
-rw-r--r-- | resolvers/node/resolution.rs | 279 |
7 files changed, 284 insertions, 313 deletions
diff --git a/resolvers/node/Cargo.toml b/resolvers/node/Cargo.toml index f05b209fd..eeac79c25 100644 --- a/resolvers/node/Cargo.toml +++ b/resolvers/node/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "node_resolver" -version = "0.10.0" +version = "0.16.0" authors.workspace = true edition.workspace = true license.workspace = true @@ -19,6 +19,7 @@ sync = ["deno_package_json/sync"] [dependencies] anyhow.workspace = true async-trait.workspace = true +boxed_error.workspace = true deno_media_type.workspace = true deno_package_json.workspace = true deno_path_util.workspace = true diff --git a/resolvers/node/analyze.rs b/resolvers/node/analyze.rs index 009296006..912689080 100644 --- a/resolvers/node/analyze.rs +++ b/resolvers/node/analyze.rs @@ -19,18 +19,19 @@ use anyhow::Error as AnyError; use url::Url; use crate::env::NodeResolverEnv; -use crate::package_json::load_pkg_json; +use crate::npm::InNpmPackageCheckerRc; use crate::resolution::NodeResolverRc; use crate::NodeModuleKind; use crate::NodeResolutionMode; -use crate::NpmResolverRc; +use crate::NpmPackageFolderResolverRc; +use crate::PackageJsonResolverRc; use crate::PathClean; #[derive(Debug, Clone)] -pub enum CjsAnalysis { +pub enum CjsAnalysis<'a> { /// File was found to be an ES module and the translator should /// load the code as ESM. - Esm(String), + Esm(Cow<'a, str>), Cjs(CjsAnalysisExports), } @@ -50,11 +51,11 @@ pub trait CjsCodeAnalyzer { /// already has it. If the source is needed by the implementation, /// then it can use the provided source, or otherwise load it if /// necessary. - async fn analyze_cjs( + async fn analyze_cjs<'a>( &self, specifier: &Url, - maybe_source: Option<String>, - ) -> Result<CjsAnalysis, AnyError>; + maybe_source: Option<Cow<'a, str>>, + ) -> Result<CjsAnalysis<'a>, AnyError>; } pub struct NodeCodeTranslator< @@ -63,8 +64,10 @@ pub struct NodeCodeTranslator< > { cjs_code_analyzer: TCjsCodeAnalyzer, env: TNodeResolverEnv, + in_npm_pkg_checker: InNpmPackageCheckerRc, node_resolver: NodeResolverRc<TNodeResolverEnv>, - npm_resolver: NpmResolverRc, + npm_resolver: NpmPackageFolderResolverRc, + pkg_json_resolver: PackageJsonResolverRc<TNodeResolverEnv>, } impl<TCjsCodeAnalyzer: CjsCodeAnalyzer, TNodeResolverEnv: NodeResolverEnv> @@ -73,14 +76,18 @@ impl<TCjsCodeAnalyzer: CjsCodeAnalyzer, TNodeResolverEnv: NodeResolverEnv> pub fn new( cjs_code_analyzer: TCjsCodeAnalyzer, env: TNodeResolverEnv, + in_npm_pkg_checker: InNpmPackageCheckerRc, node_resolver: NodeResolverRc<TNodeResolverEnv>, - npm_resolver: NpmResolverRc, + npm_resolver: NpmPackageFolderResolverRc, + pkg_json_resolver: PackageJsonResolverRc<TNodeResolverEnv>, ) -> Self { Self { cjs_code_analyzer, env, + in_npm_pkg_checker, node_resolver, npm_resolver, + pkg_json_resolver, } } @@ -90,11 +97,11 @@ impl<TCjsCodeAnalyzer: CjsCodeAnalyzer, TNodeResolverEnv: NodeResolverEnv> /// For all discovered reexports the analysis will be performed recursively. /// /// If successful a source code for equivalent ES module is returned. - pub async fn translate_cjs_to_esm( + pub async fn translate_cjs_to_esm<'a>( &self, entry_specifier: &Url, - source: Option<String>, - ) -> Result<String, AnyError> { + source: Option<Cow<'a, str>>, + ) -> Result<Cow<'a, str>, AnyError> { let mut temp_var_count = 0; let analysis = self @@ -108,7 +115,7 @@ impl<TCjsCodeAnalyzer: CjsCodeAnalyzer, TNodeResolverEnv: NodeResolverEnv> }; let mut source = vec![ - r#"import {createRequire as __internalCreateRequire} from "node:module"; + r#"import {createRequire as __internalCreateRequire, Module as __internalModule } from "node:module"; const require = __internalCreateRequire(import.meta.url);"# .to_string(), ]; @@ -135,7 +142,12 @@ impl<TCjsCodeAnalyzer: CjsCodeAnalyzer, TNodeResolverEnv: NodeResolverEnv> } source.push(format!( - "const mod = require(\"{}\");", + r#"let mod; + if (import.meta.main) {{ + mod = __internalModule._load("{0}", null, true) + }} else {{ + mod = require("{0}"); + }}"#, url_to_file_path(entry_specifier) .unwrap() .to_str() @@ -159,7 +171,7 @@ impl<TCjsCodeAnalyzer: CjsCodeAnalyzer, TNodeResolverEnv: NodeResolverEnv> source.push("export default mod;".to_string()); let translated_source = source.join("\n"); - Ok(translated_source) + Ok(Cow::Owned(translated_source)) } async fn analyze_reexports<'a>( @@ -174,7 +186,7 @@ impl<TCjsCodeAnalyzer: CjsCodeAnalyzer, TNodeResolverEnv: NodeResolverEnv> struct Analysis { reexport_specifier: url::Url, referrer: url::Url, - analysis: CjsAnalysis, + analysis: CjsAnalysis<'static>, } type AnalysisFuture<'a> = LocalBoxFuture<'a, Result<Analysis, AnyError>>; @@ -329,8 +341,9 @@ impl<TCjsCodeAnalyzer: CjsCodeAnalyzer, TNodeResolverEnv: NodeResolverEnv> }?; let package_json_path = module_dir.join("package.json"); - let maybe_package_json = - load_pkg_json(self.env.pkg_json_fs(), &package_json_path)?; + let maybe_package_json = self + .pkg_json_resolver + .load_package_json(&package_json_path)?; if let Some(package_json) = maybe_package_json { if let Some(exports) = &package_json.exports { return Some( @@ -356,8 +369,9 @@ impl<TCjsCodeAnalyzer: CjsCodeAnalyzer, TNodeResolverEnv: NodeResolverEnv> if self.env.is_dir_sync(&d) { // subdir might have a package.json that specifies the entrypoint let package_json_path = d.join("package.json"); - let maybe_package_json = - load_pkg_json(self.env.pkg_json_fs(), &package_json_path)?; + let maybe_package_json = self + .pkg_json_resolver + .load_package_json(&package_json_path)?; if let Some(package_json) = maybe_package_json { if let Some(main) = package_json.main(NodeModuleKind::Cjs) { return Ok(Some(url_from_file_path(&d.join(main).clean())?)); @@ -382,7 +396,7 @@ impl<TCjsCodeAnalyzer: CjsCodeAnalyzer, TNodeResolverEnv: NodeResolverEnv> // as a fallback, attempt to resolve it via the ancestor directories let mut last = referrer_path.as_path(); while let Some(parent) = last.parent() { - if !self.npm_resolver.in_npm_package_at_dir_path(parent) { + if !self.in_npm_pkg_checker.in_npm_package_at_dir_path(parent) { break; } let path = if parent.ends_with("node_modules") { diff --git a/resolvers/node/errors.rs b/resolvers/node/errors.rs index 4ba829eda..0f332d2c9 100644 --- a/resolvers/node/errors.rs +++ b/resolvers/node/errors.rs @@ -4,39 +4,13 @@ use std::borrow::Cow; use std::fmt::Write; use std::path::PathBuf; +use boxed_error::Boxed; use thiserror::Error; use url::Url; use crate::NodeModuleKind; use crate::NodeResolutionMode; -macro_rules! kinded_err { - ($name:ident, $kind_name:ident) => { - #[derive(Error, Debug)] - #[error(transparent)] - pub struct $name(pub Box<$kind_name>); - - impl $name { - pub fn as_kind(&self) -> &$kind_name { - &self.0 - } - - pub fn into_kind(self) -> $kind_name { - *self.0 - } - } - - impl<E> From<E> for $name - where - $kind_name: From<E>, - { - fn from(err: E) -> Self { - $name(Box::new($kind_name::from(err))) - } - } - }; -} - #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] #[allow(non_camel_case_types)] pub enum NodeJsErrorCode { @@ -81,29 +55,6 @@ pub trait NodeJsErrorCoded { fn code(&self) -> NodeJsErrorCode; } -kinded_err!( - ResolvePkgSubpathFromDenoModuleError, - ResolvePkgSubpathFromDenoModuleErrorKind -); - -impl NodeJsErrorCoded for ResolvePkgSubpathFromDenoModuleError { - fn code(&self) -> NodeJsErrorCode { - use ResolvePkgSubpathFromDenoModuleErrorKind::*; - match self.as_kind() { - PackageSubpathResolve(e) => e.code(), - UrlToNodeResolution(e) => e.code(), - } - } -} - -#[derive(Debug, Error)] -pub enum ResolvePkgSubpathFromDenoModuleErrorKind { - #[error(transparent)] - PackageSubpathResolve(#[from] PackageSubpathResolveError), - #[error(transparent)] - UrlToNodeResolution(#[from] UrlToNodeResolutionError), -} - // todo(https://github.com/denoland/deno_core/issues/810): make this a TypeError #[derive(Debug, Clone, Error)] #[error( @@ -125,7 +76,8 @@ impl NodeJsErrorCoded for InvalidModuleSpecifierError { } } -kinded_err!(LegacyResolveError, LegacyResolveErrorKind); +#[derive(Debug, Boxed)] +pub struct LegacyResolveError(pub Box<LegacyResolveErrorKind>); #[derive(Debug, Error)] pub enum LegacyResolveErrorKind { @@ -144,8 +96,6 @@ impl NodeJsErrorCoded for LegacyResolveError { } } -kinded_err!(PackageFolderResolveError, PackageFolderResolveErrorKind); - #[derive(Debug, Error)] #[error( "Could not find package '{}' from referrer '{}'{}.", @@ -209,6 +159,9 @@ impl NodeJsErrorCoded for PackageFolderResolveError { } } +#[derive(Debug, Boxed)] +pub struct PackageFolderResolveError(pub Box<PackageFolderResolveErrorKind>); + #[derive(Debug, Error)] pub enum PackageFolderResolveErrorKind { #[error(transparent)] @@ -219,8 +172,6 @@ pub enum PackageFolderResolveErrorKind { Io(#[from] PackageFolderResolveIoError), } -kinded_err!(PackageSubpathResolveError, PackageSubpathResolveErrorKind); - impl NodeJsErrorCoded for PackageSubpathResolveError { fn code(&self) -> NodeJsErrorCode { match self.as_kind() { @@ -231,6 +182,9 @@ impl NodeJsErrorCoded for PackageSubpathResolveError { } } +#[derive(Debug, Boxed)] +pub struct PackageSubpathResolveError(pub Box<PackageSubpathResolveErrorKind>); + #[derive(Debug, Error)] pub enum PackageSubpathResolveErrorKind { #[error(transparent)] @@ -275,8 +229,6 @@ impl NodeJsErrorCoded for PackageTargetNotFoundError { } } -kinded_err!(PackageTargetResolveError, PackageTargetResolveErrorKind); - impl NodeJsErrorCoded for PackageTargetResolveError { fn code(&self) -> NodeJsErrorCode { match self.as_kind() { @@ -289,6 +241,9 @@ impl NodeJsErrorCoded for PackageTargetResolveError { } } +#[derive(Debug, Boxed)] +pub struct PackageTargetResolveError(pub Box<PackageTargetResolveErrorKind>); + #[derive(Debug, Error)] pub enum PackageTargetResolveErrorKind { #[error(transparent)] @@ -303,8 +258,6 @@ pub enum PackageTargetResolveErrorKind { TypesNotFound(#[from] TypesNotFoundError), } -kinded_err!(PackageExportsResolveError, PackageExportsResolveErrorKind); - impl NodeJsErrorCoded for PackageExportsResolveError { fn code(&self) -> NodeJsErrorCode { match self.as_kind() { @@ -314,6 +267,9 @@ impl NodeJsErrorCoded for PackageExportsResolveError { } } +#[derive(Debug, Boxed)] +pub struct PackageExportsResolveError(pub Box<PackageExportsResolveErrorKind>); + #[derive(Debug, Error)] pub enum PackageExportsResolveErrorKind { #[error(transparent)] @@ -361,8 +317,6 @@ impl NodeJsErrorCoded for PackageJsonLoadError { } } -kinded_err!(ClosestPkgJsonError, ClosestPkgJsonErrorKind); - impl NodeJsErrorCoded for ClosestPkgJsonError { fn code(&self) -> NodeJsErrorCode { match self.as_kind() { @@ -372,6 +326,9 @@ impl NodeJsErrorCoded for ClosestPkgJsonError { } } +#[derive(Debug, Boxed)] +pub struct ClosestPkgJsonError(pub Box<ClosestPkgJsonErrorKind>); + #[derive(Debug, Error)] pub enum ClosestPkgJsonErrorKind { #[error(transparent)] @@ -394,37 +351,6 @@ impl NodeJsErrorCoded for CanonicalizingPkgJsonDirError { } } -#[derive(Debug, Error)] -#[error("TypeScript files are not supported in npm packages: {specifier}")] -pub struct TypeScriptNotSupportedInNpmError { - pub specifier: Url, -} - -impl NodeJsErrorCoded for TypeScriptNotSupportedInNpmError { - fn code(&self) -> NodeJsErrorCode { - NodeJsErrorCode::ERR_UNKNOWN_FILE_EXTENSION - } -} - -kinded_err!(UrlToNodeResolutionError, UrlToNodeResolutionErrorKind); - -impl NodeJsErrorCoded for UrlToNodeResolutionError { - fn code(&self) -> NodeJsErrorCode { - match self.as_kind() { - UrlToNodeResolutionErrorKind::TypeScriptNotSupported(e) => e.code(), - UrlToNodeResolutionErrorKind::ClosestPkgJson(e) => e.code(), - } - } -} - -#[derive(Debug, Error)] -pub enum UrlToNodeResolutionErrorKind { - #[error(transparent)] - TypeScriptNotSupported(#[from] TypeScriptNotSupportedInNpmError), - #[error(transparent)] - ClosestPkgJson(#[from] ClosestPkgJsonError), -} - // todo(https://github.com/denoland/deno_core/issues/810): make this a TypeError #[derive(Debug, Error)] #[error( @@ -446,7 +372,8 @@ impl NodeJsErrorCoded for PackageImportNotDefinedError { } } -kinded_err!(PackageImportsResolveError, PackageImportsResolveErrorKind); +#[derive(Debug, Boxed)] +pub struct PackageImportsResolveError(pub Box<PackageImportsResolveErrorKind>); #[derive(Debug, Error)] pub enum PackageImportsResolveErrorKind { @@ -471,8 +398,6 @@ impl NodeJsErrorCoded for PackageImportsResolveErrorKind { } } -kinded_err!(PackageResolveError, PackageResolveErrorKind); - impl NodeJsErrorCoded for PackageResolveError { fn code(&self) -> NodeJsErrorCode { match self.as_kind() { @@ -485,6 +410,9 @@ impl NodeJsErrorCoded for PackageResolveError { } } +#[derive(Debug, Boxed)] +pub struct PackageResolveError(pub Box<PackageResolveErrorKind>); + #[derive(Debug, Error)] pub enum PackageResolveErrorKind { #[error(transparent)] @@ -515,7 +443,8 @@ pub struct DataUrlReferrerError { pub source: url::ParseError, } -kinded_err!(NodeResolveError, NodeResolveErrorKind); +#[derive(Debug, Boxed)] +pub struct NodeResolveError(pub Box<NodeResolveErrorKind>); #[derive(Debug, Error)] pub enum NodeResolveErrorKind { @@ -533,11 +462,10 @@ pub enum NodeResolveErrorKind { TypesNotFound(#[from] TypesNotFoundError), #[error(transparent)] FinalizeResolution(#[from] FinalizeResolutionError), - #[error(transparent)] - UrlToNodeResolution(#[from] UrlToNodeResolutionError), } -kinded_err!(FinalizeResolutionError, FinalizeResolutionErrorKind); +#[derive(Debug, Boxed)] +pub struct FinalizeResolutionError(pub Box<FinalizeResolutionErrorKind>); #[derive(Debug, Error)] pub enum FinalizeResolutionErrorKind { @@ -728,8 +656,6 @@ pub enum ResolvePkgJsonBinExportError { MissingPkgJson { pkg_json_path: PathBuf }, #[error("Failed resolving binary export. {message}")] InvalidBinProperty { message: String }, - #[error(transparent)] - UrlToNodeResolution(#[from] UrlToNodeResolutionError), } #[derive(Debug, Error)] diff --git a/resolvers/node/lib.rs b/resolvers/node/lib.rs index f03f77048..87bd62994 100644 --- a/resolvers/node/lib.rs +++ b/resolvers/node/lib.rs @@ -13,12 +13,16 @@ mod resolution; mod sync; pub use deno_package_json::PackageJson; -pub use npm::NpmResolver; -pub use npm::NpmResolverRc; -pub use package_json::load_pkg_json; +pub use npm::InNpmPackageChecker; +pub use npm::InNpmPackageCheckerRc; +pub use npm::NpmPackageFolderResolver; +pub use npm::NpmPackageFolderResolverRc; +pub use package_json::PackageJsonResolver; +pub use package_json::PackageJsonResolverRc; pub use package_json::PackageJsonThreadLocalCache; pub use path::PathClean; pub use resolution::parse_npm_pkg_name; +pub use resolution::resolve_specifier_into_node_modules; pub use resolution::NodeModuleKind; pub use resolution::NodeResolution; pub use resolution::NodeResolutionMode; diff --git a/resolvers/node/npm.rs b/resolvers/node/npm.rs index 6b5f21db6..ab3a17942 100644 --- a/resolvers/node/npm.rs +++ b/resolvers/node/npm.rs @@ -13,16 +13,25 @@ use crate::sync::MaybeSend; use crate::sync::MaybeSync; #[allow(clippy::disallowed_types)] -pub type NpmResolverRc = crate::sync::MaybeArc<dyn NpmResolver>; +pub type NpmPackageFolderResolverRc = + crate::sync::MaybeArc<dyn NpmPackageFolderResolver>; -pub trait NpmResolver: std::fmt::Debug + MaybeSend + MaybeSync { - /// Resolves an npm package folder path from an npm package referrer. +pub trait NpmPackageFolderResolver: + std::fmt::Debug + MaybeSend + MaybeSync +{ + /// Resolves an npm package folder path from the specified referrer. fn resolve_package_folder_from_package( &self, specifier: &str, referrer: &Url, ) -> Result<PathBuf, errors::PackageFolderResolveError>; +} + +#[allow(clippy::disallowed_types)] +pub type InNpmPackageCheckerRc = crate::sync::MaybeArc<dyn InNpmPackageChecker>; +/// Checks if a provided specifier is in an npm package. +pub trait InNpmPackageChecker: std::fmt::Debug + MaybeSend + MaybeSync { fn in_npm_package(&self, specifier: &Url) -> bool; fn in_npm_package_at_dir_path(&self, path: &Path) -> bool { diff --git a/resolvers/node/package_json.rs b/resolvers/node/package_json.rs index de750f1d7..ae016ebe3 100644 --- a/resolvers/node/package_json.rs +++ b/resolvers/node/package_json.rs @@ -2,15 +2,21 @@ use deno_package_json::PackageJson; use deno_package_json::PackageJsonRc; +use deno_path_util::strip_unc_prefix; use std::cell::RefCell; use std::collections::HashMap; use std::io::ErrorKind; use std::path::Path; use std::path::PathBuf; +use url::Url; +use crate::env::NodeResolverEnv; +use crate::errors::CanonicalizingPkgJsonDirError; +use crate::errors::ClosestPkgJsonError; use crate::errors::PackageJsonLoadError; -// use a thread local cache so that workers have their own distinct cache +// it would be nice if this was passed down as a ctor arg to the package.json resolver, +// but it's a little bit complicated to do that, so we just maintain a thread local cache thread_local! { static CACHE: RefCell<HashMap<PathBuf, PackageJsonRc>> = RefCell::new(HashMap::new()); } @@ -33,21 +39,91 @@ impl deno_package_json::PackageJsonCache for PackageJsonThreadLocalCache { } } -/// Helper to load a package.json file using the thread local cache -/// in node_resolver. -pub fn load_pkg_json( - fs: &dyn deno_package_json::fs::DenoPkgJsonFs, - path: &Path, -) -> Result<Option<PackageJsonRc>, PackageJsonLoadError> { - let result = - PackageJson::load_from_path(path, fs, Some(&PackageJsonThreadLocalCache)); - match result { - Ok(pkg_json) => Ok(Some(pkg_json)), - Err(deno_package_json::PackageJsonLoadError::Io { source, .. }) - if source.kind() == ErrorKind::NotFound => - { +#[allow(clippy::disallowed_types)] +pub type PackageJsonResolverRc<TEnv> = + crate::sync::MaybeArc<PackageJsonResolver<TEnv>>; + +#[derive(Debug)] +pub struct PackageJsonResolver<TEnv: NodeResolverEnv> { + env: TEnv, +} + +impl<TEnv: NodeResolverEnv> PackageJsonResolver<TEnv> { + pub fn new(env: TEnv) -> Self { + Self { env } + } + + pub fn get_closest_package_json( + &self, + url: &Url, + ) -> Result<Option<PackageJsonRc>, ClosestPkgJsonError> { + let Ok(file_path) = deno_path_util::url_to_file_path(url) else { + return Ok(None); + }; + self.get_closest_package_json_from_path(&file_path) + } + + pub fn get_closest_package_json_from_path( + &self, + file_path: &Path, + ) -> Result<Option<PackageJsonRc>, ClosestPkgJsonError> { + // we use this for deno compile using byonm because the script paths + // won't be in virtual file system, but the package.json paths will be + fn canonicalize_first_ancestor_exists<TEnv: NodeResolverEnv>( + dir_path: &Path, + env: &TEnv, + ) -> Result<Option<PathBuf>, std::io::Error> { + for ancestor in dir_path.ancestors() { + match env.realpath_sync(ancestor) { + Ok(dir_path) => return Ok(Some(dir_path)), + Err(err) if err.kind() == std::io::ErrorKind::NotFound => { + // keep searching + } + Err(err) => return Err(err), + } + } Ok(None) } - Err(err) => Err(PackageJsonLoadError(err)), + + let parent_dir = file_path.parent().unwrap(); + let Some(start_dir) = canonicalize_first_ancestor_exists( + parent_dir, &self.env, + ) + .map_err(|source| CanonicalizingPkgJsonDirError { + dir_path: parent_dir.to_path_buf(), + source, + })? + else { + return Ok(None); + }; + let start_dir = strip_unc_prefix(start_dir); + for current_dir in start_dir.ancestors() { + let package_json_path = current_dir.join("package.json"); + if let Some(pkg_json) = self.load_package_json(&package_json_path)? { + return Ok(Some(pkg_json)); + } + } + + Ok(None) + } + + pub fn load_package_json( + &self, + path: &Path, + ) -> Result<Option<PackageJsonRc>, PackageJsonLoadError> { + let result = PackageJson::load_from_path( + path, + self.env.pkg_json_fs(), + Some(&PackageJsonThreadLocalCache), + ); + match result { + Ok(pkg_json) => Ok(Some(pkg_json)), + Err(deno_package_json::PackageJsonLoadError::Io { source, .. }) + if source.kind() == ErrorKind::NotFound => + { + Ok(None) + } + Err(err) => Err(PackageJsonLoadError(err)), + } } } diff --git a/resolvers/node/resolution.rs b/resolvers/node/resolution.rs index 811583a5e..c2ec25aca 100644 --- a/resolvers/node/resolution.rs +++ b/resolvers/node/resolution.rs @@ -6,9 +6,6 @@ use std::path::PathBuf; use anyhow::bail; use anyhow::Error as AnyError; -use deno_media_type::MediaType; -use deno_package_json::PackageJsonRc; -use deno_path_util::strip_unc_prefix; use deno_path_util::url_from_file_path; use serde_json::Map; use serde_json::Value; @@ -16,8 +13,6 @@ use url::Url; use crate::env::NodeResolverEnv; use crate::errors; -use crate::errors::CanonicalizingPkgJsonDirError; -use crate::errors::ClosestPkgJsonError; use crate::errors::DataUrlReferrerError; use crate::errors::FinalizeResolutionError; use crate::errors::InvalidModuleSpecifierError; @@ -32,7 +27,6 @@ use crate::errors::PackageExportsResolveError; use crate::errors::PackageImportNotDefinedError; use crate::errors::PackageImportsResolveError; use crate::errors::PackageImportsResolveErrorKind; -use crate::errors::PackageJsonLoadError; use crate::errors::PackagePathNotExportedError; use crate::errors::PackageResolveError; use crate::errors::PackageSubpathResolveError; @@ -42,14 +36,13 @@ use crate::errors::PackageTargetResolveError; use crate::errors::PackageTargetResolveErrorKind; use crate::errors::ResolveBinaryCommandsError; use crate::errors::ResolvePkgJsonBinExportError; -use crate::errors::ResolvePkgSubpathFromDenoModuleError; -use crate::errors::TypeScriptNotSupportedInNpmError; use crate::errors::TypesNotFoundError; use crate::errors::TypesNotFoundErrorData; use crate::errors::UnsupportedDirImportError; use crate::errors::UnsupportedEsmUrlSchemeError; -use crate::errors::UrlToNodeResolutionError; -use crate::NpmResolverRc; +use crate::npm::InNpmPackageCheckerRc; +use crate::NpmPackageFolderResolverRc; +use crate::PackageJsonResolverRc; use crate::PathClean; use deno_package_json::PackageJson; @@ -57,6 +50,15 @@ pub static DEFAULT_CONDITIONS: &[&str] = &["deno", "node", "import"]; pub static REQUIRE_CONDITIONS: &[&str] = &["require", "node"]; static TYPES_ONLY_CONDITIONS: &[&str] = &["types"]; +fn conditions_from_module_kind( + kind: NodeModuleKind, +) -> &'static [&'static str] { + match kind { + NodeModuleKind::Esm => DEFAULT_CONDITIONS, + NodeModuleKind::Cjs => REQUIRE_CONDITIONS, + } +} + pub type NodeModuleKind = deno_package_json::NodeModuleKind; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -73,16 +75,14 @@ impl NodeResolutionMode { #[derive(Debug)] pub enum NodeResolution { - Esm(Url), - CommonJs(Url), + Module(Url), BuiltIn(String), } impl NodeResolution { pub fn into_url(self) -> Url { match self { - Self::Esm(u) => u, - Self::CommonJs(u) => u, + Self::Module(u) => u, Self::BuiltIn(specifier) => { if specifier.starts_with("node:") { Url::parse(&specifier).unwrap() @@ -92,42 +92,6 @@ impl NodeResolution { } } } - - pub fn into_specifier_and_media_type( - resolution: Option<Self>, - ) -> (Url, MediaType) { - match resolution { - Some(NodeResolution::CommonJs(specifier)) => { - let media_type = MediaType::from_specifier(&specifier); - ( - specifier, - match media_type { - MediaType::JavaScript | MediaType::Jsx => MediaType::Cjs, - MediaType::TypeScript | MediaType::Tsx => MediaType::Cts, - MediaType::Dts => MediaType::Dcts, - _ => media_type, - }, - ) - } - Some(NodeResolution::Esm(specifier)) => { - let media_type = MediaType::from_specifier(&specifier); - ( - specifier, - match media_type { - MediaType::JavaScript | MediaType::Jsx => MediaType::Mjs, - MediaType::TypeScript | MediaType::Tsx => MediaType::Mts, - MediaType::Dts => MediaType::Dmts, - _ => media_type, - }, - ) - } - Some(resolution) => (resolution.into_url(), MediaType::Dts), - None => ( - Url::parse("internal:///missing_dependency.d.ts").unwrap(), - MediaType::Dts, - ), - } - } } #[allow(clippy::disallowed_types)] @@ -136,16 +100,28 @@ pub type NodeResolverRc<TEnv> = crate::sync::MaybeArc<NodeResolver<TEnv>>; #[derive(Debug)] pub struct NodeResolver<TEnv: NodeResolverEnv> { env: TEnv, - npm_resolver: NpmResolverRc, + in_npm_pkg_checker: InNpmPackageCheckerRc, + npm_pkg_folder_resolver: NpmPackageFolderResolverRc, + pkg_json_resolver: PackageJsonResolverRc<TEnv>, } impl<TEnv: NodeResolverEnv> NodeResolver<TEnv> { - pub fn new(env: TEnv, npm_resolver: NpmResolverRc) -> Self { - Self { env, npm_resolver } + pub fn new( + env: TEnv, + in_npm_pkg_checker: InNpmPackageCheckerRc, + npm_pkg_folder_resolver: NpmPackageFolderResolverRc, + pkg_json_resolver: PackageJsonResolverRc<TEnv>, + ) -> Self { + Self { + env, + in_npm_pkg_checker, + npm_pkg_folder_resolver, + pkg_json_resolver, + } } pub fn in_npm_package(&self, specifier: &Url) -> bool { - self.npm_resolver.in_npm_package(specifier) + self.in_npm_pkg_checker.in_npm_package(specifier) } /// This function is an implementation of `defaultResolve` in @@ -166,7 +142,7 @@ impl<TEnv: NodeResolverEnv> NodeResolver<TEnv> { if let Ok(url) = Url::parse(specifier) { if url.scheme() == "data" { - return Ok(NodeResolution::Esm(url)); + return Ok(NodeResolution::Module(url)); } if let Some(module_name) = @@ -191,7 +167,7 @@ impl<TEnv: NodeResolverEnv> NodeResolver<TEnv> { let url = referrer .join(specifier) .map_err(|source| DataUrlReferrerError { source })?; - return Ok(NodeResolution::Esm(url)); + return Ok(NodeResolution::Module(url)); } } @@ -199,8 +175,7 @@ impl<TEnv: NodeResolverEnv> NodeResolver<TEnv> { specifier, referrer, referrer_kind, - // even though the referrer may be CJS, if we're here that means we're doing ESM resolution - DEFAULT_CONDITIONS, + conditions_from_module_kind(referrer_kind), mode, )?; @@ -212,7 +187,7 @@ impl<TEnv: NodeResolverEnv> NodeResolver<TEnv> { }; let url = self.finalize_resolution(url, Some(referrer))?; - let resolve_response = self.url_to_node_resolution(url)?; + let resolve_response = NodeResolution::Module(url); // TODO(bartlomieju): skipped checking errors for commonJS resolution and // "preserveSymlinksMain"/"preserveSymlinks" options. Ok(resolve_response) @@ -227,7 +202,7 @@ impl<TEnv: NodeResolverEnv> NodeResolver<TEnv> { mode: NodeResolutionMode, ) -> Result<Url, NodeResolveError> { if should_be_treated_as_relative_or_absolute_path(specifier) { - Ok(referrer.join(specifier).map_err(|err| { + Ok(node_join_url(referrer, specifier).map_err(|err| { NodeResolveRelativeJoinError { path: specifier.to_string(), base: referrer.clone(), @@ -236,6 +211,7 @@ impl<TEnv: NodeResolverEnv> NodeResolver<TEnv> { })?) } else if specifier.starts_with('#') { let pkg_config = self + .pkg_json_resolver .get_closest_package_json(referrer) .map_err(PackageImportsResolveErrorKind::ClosestPkgJson) .map_err(|err| PackageImportsResolveError(Box::new(err)))?; @@ -331,9 +307,9 @@ impl<TEnv: NodeResolverEnv> NodeResolver<TEnv> { package_dir: &Path, package_subpath: Option<&str>, maybe_referrer: Option<&Url>, + referrer_kind: NodeModuleKind, mode: NodeResolutionMode, - ) -> Result<NodeResolution, ResolvePkgSubpathFromDenoModuleError> { - let node_module_kind = NodeModuleKind::Esm; + ) -> Result<Url, PackageSubpathResolveError> { let package_subpath = package_subpath .map(|s| format!("./{s}")) .unwrap_or_else(|| ".".to_string()); @@ -341,14 +317,13 @@ impl<TEnv: NodeResolverEnv> NodeResolver<TEnv> { package_dir, &package_subpath, maybe_referrer, - node_module_kind, - DEFAULT_CONDITIONS, + referrer_kind, + conditions_from_module_kind(referrer_kind), mode, )?; - let resolve_response = self.url_to_node_resolution(resolved_url)?; // TODO(bartlomieju): skipped checking errors for commonJS resolution and // "preserveSymlinksMain"/"preserveSymlinks" options. - Ok(resolve_response) + Ok(resolved_url) } pub fn resolve_binary_commands( @@ -356,7 +331,9 @@ impl<TEnv: NodeResolverEnv> NodeResolver<TEnv> { package_folder: &Path, ) -> Result<Vec<String>, ResolveBinaryCommandsError> { let pkg_json_path = package_folder.join("package.json"); - let Some(package_json) = self.load_package_json(&pkg_json_path)? else { + let Some(package_json) = + self.pkg_json_resolver.load_package_json(&pkg_json_path)? + else { return Ok(Vec::new()); }; @@ -381,9 +358,11 @@ impl<TEnv: NodeResolverEnv> NodeResolver<TEnv> { &self, package_folder: &Path, sub_path: Option<&str>, - ) -> Result<NodeResolution, ResolvePkgJsonBinExportError> { + ) -> Result<Url, ResolvePkgJsonBinExportError> { let pkg_json_path = package_folder.join("package.json"); - let Some(package_json) = self.load_package_json(&pkg_json_path)? else { + let Some(package_json) = + self.pkg_json_resolver.load_package_json(&pkg_json_path)? + else { return Err(ResolvePkgJsonBinExportError::MissingPkgJson { pkg_json_path, }); @@ -396,37 +375,9 @@ impl<TEnv: NodeResolverEnv> NodeResolver<TEnv> { })?; let url = url_from_file_path(&package_folder.join(bin_entry)).unwrap(); - let resolve_response = self.url_to_node_resolution(url)?; // TODO(bartlomieju): skipped checking errors for commonJS resolution and // "preserveSymlinksMain"/"preserveSymlinks" options. - Ok(resolve_response) - } - - pub fn url_to_node_resolution( - &self, - url: Url, - ) -> Result<NodeResolution, UrlToNodeResolutionError> { - let url_str = url.as_str().to_lowercase(); - if url_str.starts_with("http") || url_str.ends_with(".json") { - Ok(NodeResolution::Esm(url)) - } else if url_str.ends_with(".js") || url_str.ends_with(".d.ts") { - let maybe_package_config = self.get_closest_package_json(&url)?; - match maybe_package_config { - Some(c) if c.typ == "module" => Ok(NodeResolution::Esm(url)), - Some(_) => Ok(NodeResolution::CommonJs(url)), - None => Ok(NodeResolution::Esm(url)), - } - } else if url_str.ends_with(".mjs") || url_str.ends_with(".d.mts") { - Ok(NodeResolution::Esm(url)) - } else if url_str.ends_with(".ts") || url_str.ends_with(".mts") { - if self.in_npm_package(&url) { - Err(TypeScriptNotSupportedInNpmError { specifier: url }.into()) - } else { - Ok(NodeResolution::Esm(url)) - } - } else { - Ok(NodeResolution::CommonJs(url)) - } + Ok(url) } /// Checks if the resolved file has a corresponding declaration file. @@ -498,10 +449,7 @@ impl<TEnv: NodeResolverEnv> NodeResolver<TEnv> { /* sub path */ ".", maybe_referrer, referrer_kind, - match referrer_kind { - NodeModuleKind::Esm => DEFAULT_CONDITIONS, - NodeModuleKind::Cjs => REQUIRE_CONDITIONS, - }, + conditions_from_module_kind(referrer_kind), NodeResolutionMode::Types, ); if let Ok(resolution) = resolution_result { @@ -1101,7 +1049,9 @@ impl<TEnv: NodeResolverEnv> NodeResolver<TEnv> { let (package_name, package_subpath, _is_scoped) = parse_npm_pkg_name(specifier, referrer)?; - if let Some(package_config) = self.get_closest_package_json(referrer)? { + if let Some(package_config) = + self.pkg_json_resolver.get_closest_package_json(referrer)? + { // ResolveSelf if package_config.name.as_ref() == Some(&package_name) { if let Some(exports) = &package_config.exports { @@ -1176,7 +1126,7 @@ impl<TEnv: NodeResolverEnv> NodeResolver<TEnv> { mode: NodeResolutionMode, ) -> Result<Url, PackageResolveError> { let package_dir_path = self - .npm_resolver + .npm_pkg_folder_resolver .resolve_package_folder_from_package(package_name, referrer)?; // todo: error with this instead when can't find package @@ -1216,7 +1166,10 @@ impl<TEnv: NodeResolverEnv> NodeResolver<TEnv> { mode: NodeResolutionMode, ) -> Result<Url, PackageSubpathResolveError> { let package_json_path = package_dir_path.join("package.json"); - match self.load_package_json(&package_json_path)? { + match self + .pkg_json_resolver + .load_package_json(&package_json_path)? + { Some(pkg_json) => self.resolve_package_subpath( &pkg_json, package_subpath, @@ -1337,70 +1290,6 @@ impl<TEnv: NodeResolverEnv> NodeResolver<TEnv> { } } - pub fn get_closest_package_json( - &self, - url: &Url, - ) -> Result<Option<PackageJsonRc>, ClosestPkgJsonError> { - let Ok(file_path) = deno_path_util::url_to_file_path(url) else { - return Ok(None); - }; - self.get_closest_package_json_from_path(&file_path) - } - - pub fn get_closest_package_json_from_path( - &self, - file_path: &Path, - ) -> Result<Option<PackageJsonRc>, ClosestPkgJsonError> { - // we use this for deno compile using byonm because the script paths - // won't be in virtual file system, but the package.json paths will be - fn canonicalize_first_ancestor_exists( - dir_path: &Path, - env: &dyn NodeResolverEnv, - ) -> Result<Option<PathBuf>, std::io::Error> { - for ancestor in dir_path.ancestors() { - match env.realpath_sync(ancestor) { - Ok(dir_path) => return Ok(Some(dir_path)), - Err(err) if err.kind() == std::io::ErrorKind::NotFound => { - // keep searching - } - Err(err) => return Err(err), - } - } - Ok(None) - } - - let parent_dir = file_path.parent().unwrap(); - let Some(start_dir) = canonicalize_first_ancestor_exists( - parent_dir, &self.env, - ) - .map_err(|source| CanonicalizingPkgJsonDirError { - dir_path: parent_dir.to_path_buf(), - source, - })? - else { - return Ok(None); - }; - let start_dir = strip_unc_prefix(start_dir); - for current_dir in start_dir.ancestors() { - let package_json_path = current_dir.join("package.json"); - if let Some(pkg_json) = self.load_package_json(&package_json_path)? { - return Ok(Some(pkg_json)); - } - } - - Ok(None) - } - - pub fn load_package_json( - &self, - package_json_path: &Path, - ) -> Result<Option<PackageJsonRc>, PackageJsonLoadError> { - crate::package_json::load_pkg_json( - self.env.pkg_json_fs(), - package_json_path, - ) - } - pub(super) fn legacy_main_resolve( &self, package_json: &PackageJson, @@ -1523,6 +1412,25 @@ impl<TEnv: NodeResolverEnv> NodeResolver<TEnv> { ) } } + + /// Resolves a specifier that is pointing into a node_modules folder by canonicalizing it. + /// + /// Returns `None` when the specifier is not in a node_modules folder. + pub fn handle_if_in_node_modules(&self, specifier: &Url) -> Option<Url> { + // skip canonicalizing if we definitely know it's unnecessary + if specifier.scheme() == "file" + && specifier.path().contains("/node_modules/") + { + // Specifiers in the node_modules directory are canonicalized + // so canoncalize then check if it's in the node_modules directory. + let specifier = resolve_specifier_into_node_modules(specifier, &|path| { + self.env.realpath_sync(path) + }); + return Some(specifier); + } + + None + } } fn resolve_bin_entry_value<'a>( @@ -1771,6 +1679,28 @@ pub fn parse_npm_pkg_name( Ok((package_name, package_subpath, is_scoped)) } +/// Resolves a specifier that is pointing into a node_modules folder. +/// +/// Note: This should be called whenever getting the specifier from +/// a Module::External(module) reference because that module might +/// not be fully resolved at the time deno_graph is analyzing it +/// because the node_modules folder might not exist at that time. +pub fn resolve_specifier_into_node_modules( + specifier: &Url, + canonicalize: &impl Fn(&Path) -> std::io::Result<PathBuf>, +) -> Url { + deno_path_util::url_to_file_path(specifier) + .ok() + // this path might not exist at the time the graph is being created + // because the node_modules folder might not yet exist + .and_then(|path| { + deno_path_util::canonicalize_path_maybe_not_exists(&path, canonicalize) + .ok() + }) + .and_then(|path| deno_path_util::url_from_file_path(&path).ok()) + .unwrap_or_else(|| specifier.clone()) +} + fn pattern_key_compare(a: &str, b: &str) -> i32 { let a_pattern_index = a.find('*'); let b_pattern_index = b.find('*'); @@ -1833,6 +1763,17 @@ fn get_module_name_from_builtin_node_module_specifier( Some(specifier) } +/// Node is more lenient joining paths than the url crate is, +/// so this function handles that. +fn node_join_url(url: &Url, path: &str) -> Result<Url, url::ParseError> { + if let Some(suffix) = path.strip_prefix(".//") { + // specifier had two leading slashes + url.join(&format!("./{}", suffix)) + } else { + url.join(path) + } +} + #[cfg(test)] mod tests { use serde_json::json; |