From 617350e79c58b6e01984e3d7c7436d243d0e5cff Mon Sep 17 00:00:00 2001 From: David Sherret Date: Thu, 14 Nov 2024 15:24:25 -0500 Subject: refactor(resolver): move more resolution code into deno_resolver (#26873) Follow-up to cjs refactor. This moves most of the resolution code into the deno_resolver crate. Still pending is the npm resolution code. --- resolvers/deno/lib.rs | 434 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 434 insertions(+) (limited to 'resolvers/deno/lib.rs') 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>, + 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); + +impl DenoResolveError { + pub fn as_kind(&self) -> &DenoResolveErrorKind { + &self.0 + } + + pub fn into_kind(self) -> DenoResolveErrorKind { + *self.0 + } +} + +impl From for DenoResolveError +where + DenoResolveErrorKind: From, +{ + fn from(err: E) -> Self { + DenoResolveError(Box::new(DenoResolveErrorKind::from(err))) + } +} + +#[derive(Debug)] +pub struct NodeAndNpmReqResolver< + Fs: DenoResolverFs, + TNodeResolverEnv: NodeResolverEnv, +> { + pub node_resolver: Arc>, + pub npm_req_resolver: Arc>, +} + +pub struct DenoResolverOptions< + 'a, + Fs: DenoResolverFs, + TNodeResolverEnv: NodeResolverEnv, + TSloppyImportResolverFs: SloppyImportResolverFs, +> { + pub in_npm_pkg_checker: Arc, + pub node_and_req_resolver: + Option>, + pub sloppy_imports_resolver: + Option>>, + pub workspace_resolver: Arc, + /// 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, + node_and_npm_resolver: Option>, + sloppy_imports_resolver: + Option>>, + workspace_resolver: Arc, + is_byonm: bool, + maybe_vendor_specifier: Option, +} + +impl< + Fs: DenoResolverFs, + TNodeResolverEnv: NodeResolverEnv, + TSloppyImportResolverFs: SloppyImportResolverFs, + > DenoResolver +{ + pub fn new( + options: DenoResolverOptions, + ) -> 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 { + 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) + } + } + } +} -- cgit v1.2.3 From 48b94c099526eb262287e101a75cb4571b8972b0 Mon Sep 17 00:00:00 2001 From: David Sherret Date: Fri, 15 Nov 2024 23:22:50 -0500 Subject: refactor: use boxed_error in some places (#26887) --- resolvers/deno/lib.rs | 55 ++++++++++++++++----------------------------------- 1 file changed, 17 insertions(+), 38 deletions(-) (limited to 'resolvers/deno/lib.rs') diff --git a/resolvers/deno/lib.rs b/resolvers/deno/lib.rs index a2b6b642f..303a82710 100644 --- a/resolvers/deno/lib.rs +++ b/resolvers/deno/lib.rs @@ -6,6 +6,7 @@ use std::path::PathBuf; use std::sync::Arc; +use boxed_error::Boxed; use deno_config::workspace::MappedResolution; use deno_config::workspace::MappedResolutionDiagnostic; use deno_config::workspace::MappedResolutionError; @@ -26,9 +27,9 @@ use node_resolver::NodeResolver; use npm::MissingPackageNodeModulesFolderError; use npm::NodeModulesOutOfDateError; use npm::NpmReqResolver; -use npm::ResolveIfForNpmPackageError; +use npm::ResolveIfForNpmPackageErrorKind; use npm::ResolvePkgFolderFromDenoReqError; -use npm::ResolveReqWithSubPathError; +use npm::ResolveReqWithSubPathErrorKind; use sloppy_imports::SloppyImportResolverFs; use sloppy_imports::SloppyImportsResolutionMode; use sloppy_imports::SloppyImportsResolver; @@ -47,6 +48,9 @@ pub struct DenoResolution { pub found_package_json_dep: bool, } +#[derive(Debug, Boxed)] +pub struct DenoResolveError(pub Box); + #[derive(Debug, Error)] pub enum DenoResolveErrorKind { #[error("Importing from the vendor directory is not permitted. Use a remote specifier instead or disable vendoring.")] @@ -71,35 +75,6 @@ pub enum DenoResolveErrorKind { 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); - -impl DenoResolveError { - pub fn as_kind(&self) -> &DenoResolveErrorKind { - &self.0 - } - - pub fn into_kind(self) -> DenoResolveErrorKind { - *self.0 - } -} - -impl From for DenoResolveError -where - DenoResolveErrorKind: From, -{ - fn from(err: E) -> Self { - DenoResolveError(Box::new(DenoResolveErrorKind::from(err))) - } -} - #[derive(Debug)] pub struct NodeAndNpmReqResolver< Fs: DenoResolverFs, @@ -378,16 +353,18 @@ impl< maybe_diagnostic, found_package_json_dep, }) - .map_err(|err| match err { - ResolveReqWithSubPathError::MissingPackageNodeModulesFolder( + .map_err(|err| { + match err.into_kind() { + ResolveReqWithSubPathErrorKind::MissingPackageNodeModulesFolder( err, ) => err.into(), - ResolveReqWithSubPathError::ResolvePkgFolderFromDenoReq( + ResolveReqWithSubPathErrorKind::ResolvePkgFolderFromDenoReq( err, ) => err.into(), - ResolveReqWithSubPathError::PackageSubpathResolve(err) => { + ResolveReqWithSubPathErrorKind::PackageSubpathResolve(err) => { err.into() } + } }); } } @@ -410,11 +387,13 @@ impl< referrer_kind, mode, ) - .map_err(|e| match e { - ResolveIfForNpmPackageError::NodeResolve(e) => { + .map_err(|e| match e.into_kind() { + ResolveIfForNpmPackageErrorKind::NodeResolve(e) => { DenoResolveErrorKind::Node(e).into_box() } - ResolveIfForNpmPackageError::NodeModulesOutOfDate(e) => e.into(), + ResolveIfForNpmPackageErrorKind::NodeModulesOutOfDate(e) => { + e.into() + } })?; if let Some(res) = maybe_resolution { match res { -- cgit v1.2.3