diff options
Diffstat (limited to 'resolvers/deno')
-rw-r--r-- | resolvers/deno/Cargo.toml | 2 | ||||
-rw-r--r-- | resolvers/deno/cjs.rs | 272 | ||||
-rw-r--r-- | resolvers/deno/fs.rs | 1 | ||||
-rw-r--r-- | resolvers/deno/lib.rs | 434 | ||||
-rw-r--r-- | resolvers/deno/npm/byonm.rs | 23 | ||||
-rw-r--r-- | resolvers/deno/npm/mod.rs | 250 |
6 files changed, 978 insertions, 4 deletions
diff --git a/resolvers/deno/Cargo.toml b/resolvers/deno/Cargo.toml index 24d50587b..89c0232dc 100644 --- a/resolvers/deno/Cargo.toml +++ b/resolvers/deno/Cargo.toml @@ -16,6 +16,8 @@ path = "lib.rs" [dependencies] anyhow.workspace = true base32.workspace = true +dashmap.workspace = true +deno_config.workspace = true deno_media_type.workspace = true deno_package_json.workspace = true deno_package_json.features = ["sync"] diff --git a/resolvers/deno/cjs.rs b/resolvers/deno/cjs.rs new file mode 100644 index 000000000..dbcbd8b6b --- /dev/null +++ b/resolvers/deno/cjs.rs @@ -0,0 +1,272 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use std::sync::Arc; + +use dashmap::DashMap; +use deno_media_type::MediaType; +use node_resolver::env::NodeResolverEnv; +use node_resolver::errors::ClosestPkgJsonError; +use node_resolver::InNpmPackageChecker; +use node_resolver::NodeModuleKind; +use node_resolver::PackageJsonResolver; +use url::Url; + +/// Keeps track of what module specifiers were resolved as CJS. +/// +/// Modules that are `.js`, `.ts`, `.jsx`, and `tsx` are only known to +/// be CJS or ESM after they're loaded based on their contents. So these +/// files will be "maybe CJS" until they're loaded. +#[derive(Debug)] +pub struct CjsTracker<TEnv: NodeResolverEnv> { + is_cjs_resolver: IsCjsResolver<TEnv>, + known: DashMap<Url, NodeModuleKind>, +} + +impl<TEnv: NodeResolverEnv> CjsTracker<TEnv> { + pub fn new( + in_npm_pkg_checker: Arc<dyn InNpmPackageChecker>, + pkg_json_resolver: Arc<PackageJsonResolver<TEnv>>, + options: IsCjsResolverOptions, + ) -> Self { + Self { + is_cjs_resolver: IsCjsResolver::new( + in_npm_pkg_checker, + pkg_json_resolver, + options, + ), + known: Default::default(), + } + } + + /// Checks whether the file might be treated as CJS, but it's not for sure + /// yet because the source hasn't been loaded to see whether it contains + /// imports or exports. + pub fn is_maybe_cjs( + &self, + specifier: &Url, + media_type: MediaType, + ) -> Result<bool, ClosestPkgJsonError> { + self.treat_as_cjs_with_is_script(specifier, media_type, None) + } + + /// Gets whether the file is CJS. If true, this is for sure + /// cjs because `is_script` is provided. + /// + /// `is_script` should be `true` when the contents of the file at the + /// provided specifier are known to be a script and not an ES module. + pub fn is_cjs_with_known_is_script( + &self, + specifier: &Url, + media_type: MediaType, + is_script: bool, + ) -> Result<bool, ClosestPkgJsonError> { + self.treat_as_cjs_with_is_script(specifier, media_type, Some(is_script)) + } + + fn treat_as_cjs_with_is_script( + &self, + specifier: &Url, + media_type: MediaType, + is_script: Option<bool>, + ) -> Result<bool, ClosestPkgJsonError> { + let kind = match self + .get_known_kind_with_is_script(specifier, media_type, is_script) + { + Some(kind) => kind, + None => self.is_cjs_resolver.check_based_on_pkg_json(specifier)?, + }; + Ok(kind == NodeModuleKind::Cjs) + } + + /// Gets the referrer for the specified module specifier. + /// + /// Generally the referrer should already be tracked by calling + /// `is_cjs_with_known_is_script` before calling this method. + pub fn get_referrer_kind(&self, specifier: &Url) -> NodeModuleKind { + if specifier.scheme() != "file" { + return NodeModuleKind::Esm; + } + self + .get_known_kind(specifier, MediaType::from_specifier(specifier)) + .unwrap_or(NodeModuleKind::Esm) + } + + fn get_known_kind( + &self, + specifier: &Url, + media_type: MediaType, + ) -> Option<NodeModuleKind> { + self.get_known_kind_with_is_script(specifier, media_type, None) + } + + fn get_known_kind_with_is_script( + &self, + specifier: &Url, + media_type: MediaType, + is_script: Option<bool>, + ) -> Option<NodeModuleKind> { + self.is_cjs_resolver.get_known_kind_with_is_script( + specifier, + media_type, + is_script, + &self.known, + ) + } +} + +#[derive(Debug)] +pub struct IsCjsResolverOptions { + pub detect_cjs: bool, + pub is_node_main: bool, +} + +/// Resolves whether a module is CJS or ESM. +#[derive(Debug)] +pub struct IsCjsResolver<TEnv: NodeResolverEnv> { + in_npm_pkg_checker: Arc<dyn InNpmPackageChecker>, + pkg_json_resolver: Arc<PackageJsonResolver<TEnv>>, + options: IsCjsResolverOptions, +} + +impl<TEnv: NodeResolverEnv> IsCjsResolver<TEnv> { + pub fn new( + in_npm_pkg_checker: Arc<dyn InNpmPackageChecker>, + pkg_json_resolver: Arc<PackageJsonResolver<TEnv>>, + options: IsCjsResolverOptions, + ) -> Self { + Self { + in_npm_pkg_checker, + pkg_json_resolver, + options, + } + } + + /// Gets the referrer kind for a script in the LSP. + pub fn get_lsp_referrer_kind( + &self, + specifier: &Url, + is_script: Option<bool>, + ) -> NodeModuleKind { + if specifier.scheme() != "file" { + return NodeModuleKind::Esm; + } + match MediaType::from_specifier(specifier) { + MediaType::Mts | MediaType::Mjs | MediaType::Dmts => NodeModuleKind::Esm, + MediaType::Cjs | MediaType::Cts | MediaType::Dcts => NodeModuleKind::Cjs, + MediaType::Dts => { + // dts files are always determined based on the package.json because + // they contain imports/exports even when considered CJS + self.check_based_on_pkg_json(specifier).unwrap_or(NodeModuleKind::Esm) + } + MediaType::Wasm | + MediaType::Json => NodeModuleKind::Esm, + MediaType::JavaScript + | MediaType::Jsx + | MediaType::TypeScript + | MediaType::Tsx + // treat these as unknown + | MediaType::Css + | MediaType::SourceMap + | MediaType::Unknown => { + match is_script { + Some(true) => self.check_based_on_pkg_json(specifier).unwrap_or(NodeModuleKind::Esm), + Some(false) | None => NodeModuleKind::Esm, + } + } + } + } + + fn get_known_kind_with_is_script( + &self, + specifier: &Url, + media_type: MediaType, + is_script: Option<bool>, + known_cache: &DashMap<Url, NodeModuleKind>, + ) -> Option<NodeModuleKind> { + if specifier.scheme() != "file" { + return Some(NodeModuleKind::Esm); + } + + match media_type { + MediaType::Mts | MediaType::Mjs | MediaType::Dmts => Some(NodeModuleKind::Esm), + MediaType::Cjs | MediaType::Cts | MediaType::Dcts => Some(NodeModuleKind::Cjs), + MediaType::Dts => { + // dts files are always determined based on the package.json because + // they contain imports/exports even when considered CJS + if let Some(value) = known_cache.get(specifier).map(|v| *v) { + Some(value) + } else { + let value = self.check_based_on_pkg_json(specifier).ok(); + if let Some(value) = value { + known_cache.insert(specifier.clone(), value); + } + Some(value.unwrap_or(NodeModuleKind::Esm)) + } + } + MediaType::Wasm | + MediaType::Json => Some(NodeModuleKind::Esm), + MediaType::JavaScript + | MediaType::Jsx + | MediaType::TypeScript + | MediaType::Tsx + // treat these as unknown + | MediaType::Css + | MediaType::SourceMap + | MediaType::Unknown => { + if let Some(value) = known_cache.get(specifier).map(|v| *v) { + if value == NodeModuleKind::Cjs && is_script == Some(false) { + // we now know this is actually esm + known_cache.insert(specifier.clone(), NodeModuleKind::Esm); + Some(NodeModuleKind::Esm) + } else { + Some(value) + } + } else if is_script == Some(false) { + // we know this is esm + known_cache.insert(specifier.clone(), NodeModuleKind::Esm); + Some(NodeModuleKind::Esm) + } else { + None + } + } + } + } + + fn check_based_on_pkg_json( + &self, + specifier: &Url, + ) -> Result<NodeModuleKind, ClosestPkgJsonError> { + if self.in_npm_pkg_checker.in_npm_package(specifier) { + if let Some(pkg_json) = + self.pkg_json_resolver.get_closest_package_json(specifier)? + { + let is_file_location_cjs = pkg_json.typ != "module"; + Ok(if is_file_location_cjs { + NodeModuleKind::Cjs + } else { + NodeModuleKind::Esm + }) + } else { + Ok(NodeModuleKind::Cjs) + } + } else if self.options.detect_cjs || self.options.is_node_main { + if let Some(pkg_json) = + self.pkg_json_resolver.get_closest_package_json(specifier)? + { + let is_cjs_type = pkg_json.typ == "commonjs" + || self.options.is_node_main && pkg_json.typ == "none"; + Ok(if is_cjs_type { + NodeModuleKind::Cjs + } else { + NodeModuleKind::Esm + }) + } else if self.options.is_node_main { + Ok(NodeModuleKind::Cjs) + } else { + Ok(NodeModuleKind::Esm) + } + } else { + Ok(NodeModuleKind::Esm) + } + } +} diff --git a/resolvers/deno/fs.rs b/resolvers/deno/fs.rs index 44495fa7c..4929f4508 100644 --- a/resolvers/deno/fs.rs +++ b/resolvers/deno/fs.rs @@ -12,6 +12,7 @@ pub struct DirEntry { pub trait DenoResolverFs { fn read_to_string_lossy(&self, path: &Path) -> std::io::Result<String>; fn realpath_sync(&self, path: &Path) -> std::io::Result<PathBuf>; + fn exists_sync(&self, path: &Path) -> bool; fn is_dir_sync(&self, path: &Path) -> bool; fn read_dir_sync(&self, dir_path: &Path) -> std::io::Result<Vec<DirEntry>>; } diff --git a/resolvers/deno/lib.rs b/resolvers/deno/lib.rs index 57fa67512..a2b6b642f 100644 --- a/resolvers/deno/lib.rs +++ b/resolvers/deno/lib.rs @@ -1,5 +1,439 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +#![deny(clippy::print_stderr)] +#![deny(clippy::print_stdout)] + +use std::path::PathBuf; +use std::sync::Arc; + +use deno_config::workspace::MappedResolution; +use deno_config::workspace::MappedResolutionDiagnostic; +use deno_config::workspace::MappedResolutionError; +use deno_config::workspace::WorkspaceResolvePkgJsonFolderError; +use deno_config::workspace::WorkspaceResolver; +use deno_package_json::PackageJsonDepValue; +use deno_package_json::PackageJsonDepValueParseError; +use deno_semver::npm::NpmPackageReqReference; +use fs::DenoResolverFs; +use node_resolver::env::NodeResolverEnv; +use node_resolver::errors::NodeResolveError; +use node_resolver::errors::PackageSubpathResolveError; +use node_resolver::InNpmPackageChecker; +use node_resolver::NodeModuleKind; +use node_resolver::NodeResolution; +use node_resolver::NodeResolutionMode; +use node_resolver::NodeResolver; +use npm::MissingPackageNodeModulesFolderError; +use npm::NodeModulesOutOfDateError; +use npm::NpmReqResolver; +use npm::ResolveIfForNpmPackageError; +use npm::ResolvePkgFolderFromDenoReqError; +use npm::ResolveReqWithSubPathError; +use sloppy_imports::SloppyImportResolverFs; +use sloppy_imports::SloppyImportsResolutionMode; +use sloppy_imports::SloppyImportsResolver; +use thiserror::Error; +use url::Url; + +pub mod cjs; pub mod fs; pub mod npm; pub mod sloppy_imports; + +#[derive(Debug, Clone)] +pub struct DenoResolution { + pub url: Url, + pub maybe_diagnostic: Option<Box<MappedResolutionDiagnostic>>, + pub found_package_json_dep: bool, +} + +#[derive(Debug, Error)] +pub enum DenoResolveErrorKind { + #[error("Importing from the vendor directory is not permitted. Use a remote specifier instead or disable vendoring.")] + InvalidVendorFolderImport, + #[error(transparent)] + MappedResolution(#[from] MappedResolutionError), + #[error(transparent)] + MissingPackageNodeModulesFolder(#[from] MissingPackageNodeModulesFolderError), + #[error(transparent)] + Node(#[from] NodeResolveError), + #[error(transparent)] + NodeModulesOutOfDate(#[from] NodeModulesOutOfDateError), + #[error(transparent)] + PackageJsonDepValueParse(#[from] PackageJsonDepValueParseError), + #[error(transparent)] + PackageJsonDepValueUrlParse(url::ParseError), + #[error(transparent)] + PackageSubpathResolve(#[from] PackageSubpathResolveError), + #[error(transparent)] + ResolvePkgFolderFromDenoReq(#[from] ResolvePkgFolderFromDenoReqError), + #[error(transparent)] + WorkspaceResolvePkgJsonFolder(#[from] WorkspaceResolvePkgJsonFolderError), +} + +impl DenoResolveErrorKind { + pub fn into_box(self) -> DenoResolveError { + DenoResolveError(Box::new(self)) + } +} + +#[derive(Error, Debug)] +#[error(transparent)] +pub struct DenoResolveError(pub Box<DenoResolveErrorKind>); + +impl DenoResolveError { + pub fn as_kind(&self) -> &DenoResolveErrorKind { + &self.0 + } + + pub fn into_kind(self) -> DenoResolveErrorKind { + *self.0 + } +} + +impl<E> From<E> for DenoResolveError +where + DenoResolveErrorKind: From<E>, +{ + fn from(err: E) -> Self { + DenoResolveError(Box::new(DenoResolveErrorKind::from(err))) + } +} + +#[derive(Debug)] +pub struct NodeAndNpmReqResolver< + Fs: DenoResolverFs, + TNodeResolverEnv: NodeResolverEnv, +> { + pub node_resolver: Arc<NodeResolver<TNodeResolverEnv>>, + pub npm_req_resolver: Arc<NpmReqResolver<Fs, TNodeResolverEnv>>, +} + +pub struct DenoResolverOptions< + 'a, + Fs: DenoResolverFs, + TNodeResolverEnv: NodeResolverEnv, + TSloppyImportResolverFs: SloppyImportResolverFs, +> { + pub in_npm_pkg_checker: Arc<dyn InNpmPackageChecker>, + pub node_and_req_resolver: + Option<NodeAndNpmReqResolver<Fs, TNodeResolverEnv>>, + pub sloppy_imports_resolver: + Option<Arc<SloppyImportsResolver<TSloppyImportResolverFs>>>, + pub workspace_resolver: Arc<WorkspaceResolver>, + /// Whether "bring your own node_modules" is enabled where Deno does not + /// setup the node_modules directories automatically, but instead uses + /// what already exists on the file system. + pub is_byonm: bool, + pub maybe_vendor_dir: Option<&'a PathBuf>, +} + +/// A resolver that takes care of resolution, taking into account loaded +/// import map, JSX settings. +#[derive(Debug)] +pub struct DenoResolver< + Fs: DenoResolverFs, + TNodeResolverEnv: NodeResolverEnv, + TSloppyImportResolverFs: SloppyImportResolverFs, +> { + in_npm_pkg_checker: Arc<dyn InNpmPackageChecker>, + node_and_npm_resolver: Option<NodeAndNpmReqResolver<Fs, TNodeResolverEnv>>, + sloppy_imports_resolver: + Option<Arc<SloppyImportsResolver<TSloppyImportResolverFs>>>, + workspace_resolver: Arc<WorkspaceResolver>, + is_byonm: bool, + maybe_vendor_specifier: Option<Url>, +} + +impl< + Fs: DenoResolverFs, + TNodeResolverEnv: NodeResolverEnv, + TSloppyImportResolverFs: SloppyImportResolverFs, + > DenoResolver<Fs, TNodeResolverEnv, TSloppyImportResolverFs> +{ + pub fn new( + options: DenoResolverOptions<Fs, TNodeResolverEnv, TSloppyImportResolverFs>, + ) -> Self { + Self { + in_npm_pkg_checker: options.in_npm_pkg_checker, + node_and_npm_resolver: options.node_and_req_resolver, + sloppy_imports_resolver: options.sloppy_imports_resolver, + workspace_resolver: options.workspace_resolver, + is_byonm: options.is_byonm, + maybe_vendor_specifier: options + .maybe_vendor_dir + .and_then(|v| deno_path_util::url_from_directory_path(v).ok()), + } + } + + pub fn resolve( + &self, + raw_specifier: &str, + referrer: &Url, + referrer_kind: NodeModuleKind, + mode: NodeResolutionMode, + ) -> Result<DenoResolution, DenoResolveError> { + let mut found_package_json_dep = false; + let mut maybe_diagnostic = None; + // Use node resolution if we're in an npm package + if let Some(node_and_npm_resolver) = self.node_and_npm_resolver.as_ref() { + let node_resolver = &node_and_npm_resolver.node_resolver; + if referrer.scheme() == "file" + && self.in_npm_pkg_checker.in_npm_package(referrer) + { + return node_resolver + .resolve(raw_specifier, referrer, referrer_kind, mode) + .map(|res| DenoResolution { + url: res.into_url(), + found_package_json_dep, + maybe_diagnostic, + }) + .map_err(|e| e.into()); + } + } + + // Attempt to resolve with the workspace resolver + let result: Result<_, DenoResolveError> = self + .workspace_resolver + .resolve(raw_specifier, referrer) + .map_err(|err| err.into()); + let result = match result { + Ok(resolution) => match resolution { + MappedResolution::Normal { + specifier, + maybe_diagnostic: current_diagnostic, + } + | MappedResolution::ImportMap { + specifier, + maybe_diagnostic: current_diagnostic, + } => { + maybe_diagnostic = current_diagnostic; + // do sloppy imports resolution if enabled + if let Some(sloppy_imports_resolver) = &self.sloppy_imports_resolver { + Ok( + sloppy_imports_resolver + .resolve( + &specifier, + match mode { + NodeResolutionMode::Execution => { + SloppyImportsResolutionMode::Execution + } + NodeResolutionMode::Types => { + SloppyImportsResolutionMode::Types + } + }, + ) + .map(|s| s.into_specifier()) + .unwrap_or(specifier), + ) + } else { + Ok(specifier) + } + } + MappedResolution::WorkspaceJsrPackage { specifier, .. } => { + Ok(specifier) + } + MappedResolution::WorkspaceNpmPackage { + target_pkg_json: pkg_json, + sub_path, + .. + } => self + .node_and_npm_resolver + .as_ref() + .unwrap() + .node_resolver + .resolve_package_subpath_from_deno_module( + pkg_json.dir_path(), + sub_path.as_deref(), + Some(referrer), + referrer_kind, + mode, + ) + .map_err(|e| e.into()), + MappedResolution::PackageJson { + dep_result, + alias, + sub_path, + .. + } => { + // found a specifier in the package.json, so mark that + // we need to do an "npm install" later + found_package_json_dep = true; + + dep_result + .as_ref() + .map_err(|e| { + DenoResolveErrorKind::PackageJsonDepValueParse(e.clone()) + .into_box() + }) + .and_then(|dep| match dep { + // todo(dsherret): it seems bad that we're converting this + // to a url because the req might not be a valid url. + PackageJsonDepValue::Req(req) => Url::parse(&format!( + "npm:{}{}", + req, + sub_path.map(|s| format!("/{}", s)).unwrap_or_default() + )) + .map_err(|e| { + DenoResolveErrorKind::PackageJsonDepValueUrlParse(e).into_box() + }), + PackageJsonDepValue::Workspace(version_req) => self + .workspace_resolver + .resolve_workspace_pkg_json_folder_for_pkg_json_dep( + alias, + version_req, + ) + .map_err(|e| { + DenoResolveErrorKind::WorkspaceResolvePkgJsonFolder(e) + .into_box() + }) + .and_then(|pkg_folder| { + self + .node_and_npm_resolver + .as_ref() + .unwrap() + .node_resolver + .resolve_package_subpath_from_deno_module( + pkg_folder, + sub_path.as_deref(), + Some(referrer), + referrer_kind, + mode, + ) + .map_err(|e| { + DenoResolveErrorKind::PackageSubpathResolve(e).into_box() + }) + }), + }) + } + }, + Err(err) => Err(err), + }; + + // When the user is vendoring, don't allow them to import directly from the vendor/ directory + // as it might cause them confusion or duplicate dependencies. Additionally, this folder has + // special treatment in the language server so it will definitely cause issues/confusion there + // if they do this. + if let Some(vendor_specifier) = &self.maybe_vendor_specifier { + if let Ok(specifier) = &result { + if specifier.as_str().starts_with(vendor_specifier.as_str()) { + return Err( + DenoResolveErrorKind::InvalidVendorFolderImport.into_box(), + ); + } + } + } + + let Some(NodeAndNpmReqResolver { + node_resolver, + npm_req_resolver, + }) = &self.node_and_npm_resolver + else { + return Ok(DenoResolution { + url: result?, + maybe_diagnostic, + found_package_json_dep, + }); + }; + + match result { + Ok(specifier) => { + if let Ok(npm_req_ref) = + NpmPackageReqReference::from_specifier(&specifier) + { + // check if the npm specifier resolves to a workspace member + if let Some(pkg_folder) = self + .workspace_resolver + .resolve_workspace_pkg_json_folder_for_npm_specifier( + npm_req_ref.req(), + ) + { + return node_resolver + .resolve_package_subpath_from_deno_module( + pkg_folder, + npm_req_ref.sub_path(), + Some(referrer), + referrer_kind, + mode, + ) + .map(|url| DenoResolution { + url, + maybe_diagnostic, + found_package_json_dep, + }) + .map_err(|e| e.into()); + } + + // do npm resolution for byonm + if self.is_byonm { + return npm_req_resolver + .resolve_req_reference( + &npm_req_ref, + referrer, + referrer_kind, + mode, + ) + .map(|url| DenoResolution { + url, + maybe_diagnostic, + found_package_json_dep, + }) + .map_err(|err| match err { + ResolveReqWithSubPathError::MissingPackageNodeModulesFolder( + err, + ) => err.into(), + ResolveReqWithSubPathError::ResolvePkgFolderFromDenoReq( + err, + ) => err.into(), + ResolveReqWithSubPathError::PackageSubpathResolve(err) => { + err.into() + } + }); + } + } + + Ok(DenoResolution { + url: node_resolver + .handle_if_in_node_modules(&specifier) + .unwrap_or(specifier), + maybe_diagnostic, + found_package_json_dep, + }) + } + Err(err) => { + // If byonm, check if the bare specifier resolves to an npm package + if self.is_byonm && referrer.scheme() == "file" { + let maybe_resolution = npm_req_resolver + .resolve_if_for_npm_pkg( + raw_specifier, + referrer, + referrer_kind, + mode, + ) + .map_err(|e| match e { + ResolveIfForNpmPackageError::NodeResolve(e) => { + DenoResolveErrorKind::Node(e).into_box() + } + ResolveIfForNpmPackageError::NodeModulesOutOfDate(e) => e.into(), + })?; + if let Some(res) = maybe_resolution { + match res { + NodeResolution::Module(url) => { + return Ok(DenoResolution { + url, + maybe_diagnostic, + found_package_json_dep, + }) + } + NodeResolution::BuiltIn(_) => { + // don't resolve bare specifiers for built-in modules via node resolution + } + } + } + } + + Err(err) + } + } + } +} diff --git a/resolvers/deno/npm/byonm.rs b/resolvers/deno/npm/byonm.rs index b85117052..e9182d47a 100644 --- a/resolvers/deno/npm/byonm.rs +++ b/resolvers/deno/npm/byonm.rs @@ -16,7 +16,7 @@ use node_resolver::errors::PackageFolderResolveIoError; use node_resolver::errors::PackageJsonLoadError; use node_resolver::errors::PackageNotFoundError; use node_resolver::InNpmPackageChecker; -use node_resolver::NpmResolver; +use node_resolver::NpmPackageFolderResolver; use node_resolver::PackageJsonResolverRc; use thiserror::Error; use url::Url; @@ -24,6 +24,8 @@ use url::Url; use crate::fs::DenoResolverFs; use super::local::normalize_pkg_name_for_node_modules_deno_folder; +use super::CliNpmReqResolver; +use super::ResolvePkgFolderFromDenoReqError; #[derive(Debug, Error)] pub enum ByonmResolvePkgFolderFromDenoReqError { @@ -303,7 +305,24 @@ impl<Fs: DenoResolverFs, TEnv: NodeResolverEnv> ByonmNpmResolver<Fs, TEnv> { impl< Fs: DenoResolverFs + Send + Sync + std::fmt::Debug, TEnv: NodeResolverEnv, - > NpmResolver for ByonmNpmResolver<Fs, TEnv> + > CliNpmReqResolver for ByonmNpmResolver<Fs, TEnv> +{ + fn resolve_pkg_folder_from_deno_module_req( + &self, + req: &PackageReq, + referrer: &Url, + ) -> Result<PathBuf, ResolvePkgFolderFromDenoReqError> { + ByonmNpmResolver::resolve_pkg_folder_from_deno_module_req( + self, req, referrer, + ) + .map_err(ResolvePkgFolderFromDenoReqError::Byonm) + } +} + +impl< + Fs: DenoResolverFs + Send + Sync + std::fmt::Debug, + TEnv: NodeResolverEnv, + > NpmPackageFolderResolver for ByonmNpmResolver<Fs, TEnv> { fn resolve_package_folder_from_package( &self, diff --git a/resolvers/deno/npm/mod.rs b/resolvers/deno/npm/mod.rs index 45e2341c7..b0aec71b0 100644 --- a/resolvers/deno/npm/mod.rs +++ b/resolvers/deno/npm/mod.rs @@ -1,10 +1,256 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -mod byonm; -mod local; +use std::fmt::Debug; +use std::path::PathBuf; +use std::sync::Arc; + +use deno_semver::npm::NpmPackageReqReference; +use deno_semver::package::PackageReq; +use node_resolver::env::NodeResolverEnv; +use node_resolver::errors::NodeResolveError; +use node_resolver::errors::NodeResolveErrorKind; +use node_resolver::errors::PackageFolderResolveErrorKind; +use node_resolver::errors::PackageFolderResolveIoError; +use node_resolver::errors::PackageNotFoundError; +use node_resolver::errors::PackageResolveErrorKind; +use node_resolver::errors::PackageSubpathResolveError; +use node_resolver::InNpmPackageChecker; +use node_resolver::NodeModuleKind; +use node_resolver::NodeResolution; +use node_resolver::NodeResolutionMode; +use node_resolver::NodeResolver; +use thiserror::Error; +use url::Url; + +use crate::fs::DenoResolverFs; pub use byonm::ByonmInNpmPackageChecker; pub use byonm::ByonmNpmResolver; pub use byonm::ByonmNpmResolverCreateOptions; pub use byonm::ByonmResolvePkgFolderFromDenoReqError; pub use local::normalize_pkg_name_for_node_modules_deno_folder; + +mod byonm; +mod local; + +#[derive(Debug, Error)] +#[error("Could not resolve \"{}\", but found it in a package.json. Deno expects the node_modules/ directory to be up to date. Did you forget to run `deno install`?", specifier)] +pub struct NodeModulesOutOfDateError { + pub specifier: String, +} + +#[derive(Debug, Error)] +#[error("Could not find '{}'. Deno expects the node_modules/ directory to be up to date. Did you forget to run `deno install`?", package_json_path.display())] +pub struct MissingPackageNodeModulesFolderError { + pub package_json_path: PathBuf, +} + +#[derive(Debug, Error)] +pub enum ResolveIfForNpmPackageError { + #[error(transparent)] + NodeResolve(#[from] NodeResolveError), + #[error(transparent)] + NodeModulesOutOfDate(#[from] NodeModulesOutOfDateError), +} + +#[derive(Debug, Error)] +pub enum ResolveReqWithSubPathError { + #[error(transparent)] + MissingPackageNodeModulesFolder(#[from] MissingPackageNodeModulesFolderError), + #[error(transparent)] + ResolvePkgFolderFromDenoReq(#[from] ResolvePkgFolderFromDenoReqError), + #[error(transparent)] + PackageSubpathResolve(#[from] PackageSubpathResolveError), +} + +#[derive(Debug, Error)] +pub enum ResolvePkgFolderFromDenoReqError { + // todo(dsherret): don't use anyhow here + #[error(transparent)] + Managed(anyhow::Error), + #[error(transparent)] + Byonm(#[from] ByonmResolvePkgFolderFromDenoReqError), +} + +// todo(dsherret): a temporary trait until we extract +// out the CLI npm resolver into here +pub trait CliNpmReqResolver: Debug + Send + Sync { + fn resolve_pkg_folder_from_deno_module_req( + &self, + req: &PackageReq, + referrer: &Url, + ) -> Result<PathBuf, ResolvePkgFolderFromDenoReqError>; +} + +pub struct NpmReqResolverOptions< + Fs: DenoResolverFs, + TNodeResolverEnv: NodeResolverEnv, +> { + /// The resolver when "bring your own node_modules" is enabled where Deno + /// does not setup the node_modules directories automatically, but instead + /// uses what already exists on the file system. + pub byonm_resolver: Option<Arc<ByonmNpmResolver<Fs, TNodeResolverEnv>>>, + pub fs: Fs, + pub in_npm_pkg_checker: Arc<dyn InNpmPackageChecker>, + pub node_resolver: Arc<NodeResolver<TNodeResolverEnv>>, + pub npm_req_resolver: Arc<dyn CliNpmReqResolver>, +} + +#[derive(Debug)] +pub struct NpmReqResolver<Fs: DenoResolverFs, TNodeResolverEnv: NodeResolverEnv> +{ + byonm_resolver: Option<Arc<ByonmNpmResolver<Fs, TNodeResolverEnv>>>, + fs: Fs, + in_npm_pkg_checker: Arc<dyn InNpmPackageChecker>, + node_resolver: Arc<NodeResolver<TNodeResolverEnv>>, + npm_resolver: Arc<dyn CliNpmReqResolver>, +} + +impl<Fs: DenoResolverFs, TNodeResolverEnv: NodeResolverEnv> + NpmReqResolver<Fs, TNodeResolverEnv> +{ + pub fn new(options: NpmReqResolverOptions<Fs, TNodeResolverEnv>) -> Self { + Self { + byonm_resolver: options.byonm_resolver, + fs: options.fs, + in_npm_pkg_checker: options.in_npm_pkg_checker, + node_resolver: options.node_resolver, + npm_resolver: options.npm_req_resolver, + } + } + + pub fn resolve_req_reference( + &self, + req_ref: &NpmPackageReqReference, + referrer: &Url, + referrer_kind: NodeModuleKind, + mode: NodeResolutionMode, + ) -> Result<Url, ResolveReqWithSubPathError> { + self.resolve_req_with_sub_path( + req_ref.req(), + req_ref.sub_path(), + referrer, + referrer_kind, + mode, + ) + } + + pub fn resolve_req_with_sub_path( + &self, + req: &PackageReq, + sub_path: Option<&str>, + referrer: &Url, + referrer_kind: NodeModuleKind, + mode: NodeResolutionMode, + ) -> Result<Url, ResolveReqWithSubPathError> { + let package_folder = self + .npm_resolver + .resolve_pkg_folder_from_deno_module_req(req, referrer)?; + let resolution_result = + self.node_resolver.resolve_package_subpath_from_deno_module( + &package_folder, + sub_path, + Some(referrer), + referrer_kind, + mode, + ); + match resolution_result { + Ok(url) => Ok(url), + Err(err) => { + if self.byonm_resolver.is_some() { + let package_json_path = package_folder.join("package.json"); + if !self.fs.exists_sync(&package_json_path) { + return Err( + MissingPackageNodeModulesFolderError { package_json_path }.into(), + ); + } + } + Err(err.into()) + } + } + } + + pub fn resolve_if_for_npm_pkg( + &self, + specifier: &str, + referrer: &Url, + referrer_kind: NodeModuleKind, + mode: NodeResolutionMode, + ) -> Result<Option<NodeResolution>, ResolveIfForNpmPackageError> { + let resolution_result = + self + .node_resolver + .resolve(specifier, referrer, referrer_kind, mode); + match resolution_result { + Ok(res) => Ok(Some(res)), + Err(err) => { + let err = err.into_kind(); + match err { + NodeResolveErrorKind::RelativeJoin(_) + | NodeResolveErrorKind::PackageImportsResolve(_) + | NodeResolveErrorKind::UnsupportedEsmUrlScheme(_) + | NodeResolveErrorKind::DataUrlReferrer(_) + | NodeResolveErrorKind::TypesNotFound(_) + | NodeResolveErrorKind::FinalizeResolution(_) => { + Err(ResolveIfForNpmPackageError::NodeResolve(err.into())) + } + NodeResolveErrorKind::PackageResolve(err) => { + let err = err.into_kind(); + match err { + PackageResolveErrorKind::ClosestPkgJson(_) + | PackageResolveErrorKind::InvalidModuleSpecifier(_) + | PackageResolveErrorKind::ExportsResolve(_) + | PackageResolveErrorKind::SubpathResolve(_) => { + Err(ResolveIfForNpmPackageError::NodeResolve( + NodeResolveErrorKind::PackageResolve(err.into()).into(), + )) + } + PackageResolveErrorKind::PackageFolderResolve(err) => { + match err.as_kind() { + PackageFolderResolveErrorKind::Io( + PackageFolderResolveIoError { package_name, .. }, + ) + | PackageFolderResolveErrorKind::PackageNotFound( + PackageNotFoundError { package_name, .. }, + ) => { + if self.in_npm_pkg_checker.in_npm_package(referrer) { + return Err(ResolveIfForNpmPackageError::NodeResolve( + NodeResolveErrorKind::PackageResolve(err.into()).into(), + )); + } + if let Some(byonm_npm_resolver) = &self.byonm_resolver { + if byonm_npm_resolver + .find_ancestor_package_json_with_dep( + package_name, + referrer, + ) + .is_some() + { + return Err( + ResolveIfForNpmPackageError::NodeModulesOutOfDate( + NodeModulesOutOfDateError { + specifier: specifier.to_string(), + }, + ), + ); + } + } + Ok(None) + } + PackageFolderResolveErrorKind::ReferrerNotFound(_) => { + if self.in_npm_pkg_checker.in_npm_package(referrer) { + return Err(ResolveIfForNpmPackageError::NodeResolve( + NodeResolveErrorKind::PackageResolve(err.into()).into(), + )); + } + Ok(None) + } + } + } + } + } + } + } + } + } +} |