summaryrefslogtreecommitdiff
path: root/resolvers/deno
diff options
context:
space:
mode:
authorDavid Sherret <dsherret@users.noreply.github.com>2024-11-14 15:24:25 -0500
committerGitHub <noreply@github.com>2024-11-14 15:24:25 -0500
commit617350e79c58b6e01984e3d7c7436d243d0e5cff (patch)
treedadb3c7675d1d49952a30c525bbc6ee3086a78e3 /resolvers/deno
parentde34c7ed29bcce8b46a65f5effe45090b8493ba5 (diff)
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.
Diffstat (limited to 'resolvers/deno')
-rw-r--r--resolvers/deno/Cargo.toml2
-rw-r--r--resolvers/deno/cjs.rs272
-rw-r--r--resolvers/deno/fs.rs1
-rw-r--r--resolvers/deno/lib.rs434
-rw-r--r--resolvers/deno/npm/byonm.rs23
-rw-r--r--resolvers/deno/npm/mod.rs250
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)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}