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.
This commit is contained in:
David Sherret 2024-11-14 15:24:25 -05:00 committed by GitHub
parent de34c7ed29
commit 617350e79c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 1520 additions and 1048 deletions

View file

@ -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"]

272
resolvers/deno/cjs.rs Normal file
View file

@ -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)
}
}
}

View file

@ -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>>;
}

View file

@ -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)
}
}
}
}

View file

@ -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,

View file

@ -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)
}
}
}
}
}
}
}
}
}
}

View file

@ -23,7 +23,7 @@ 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;
@ -66,7 +66,7 @@ pub struct NodeCodeTranslator<
env: TNodeResolverEnv,
in_npm_pkg_checker: InNpmPackageCheckerRc,
node_resolver: NodeResolverRc<TNodeResolverEnv>,
npm_resolver: NpmResolverRc,
npm_resolver: NpmPackageFolderResolverRc,
pkg_json_resolver: PackageJsonResolverRc<TNodeResolverEnv>,
}
@ -78,7 +78,7 @@ impl<TCjsCodeAnalyzer: CjsCodeAnalyzer, TNodeResolverEnv: NodeResolverEnv>
env: TNodeResolverEnv,
in_npm_pkg_checker: InNpmPackageCheckerRc,
node_resolver: NodeResolverRc<TNodeResolverEnv>,
npm_resolver: NpmResolverRc,
npm_resolver: NpmPackageFolderResolverRc,
pkg_json_resolver: PackageJsonResolverRc<TNodeResolverEnv>,
) -> Self {
Self {

View file

@ -15,13 +15,14 @@ mod sync;
pub use deno_package_json::PackageJson;
pub use npm::InNpmPackageChecker;
pub use npm::InNpmPackageCheckerRc;
pub use npm::NpmResolver;
pub use npm::NpmResolverRc;
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;

View file

@ -13,10 +13,13 @@ 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,

View file

@ -41,7 +41,7 @@ use crate::errors::TypesNotFoundErrorData;
use crate::errors::UnsupportedDirImportError;
use crate::errors::UnsupportedEsmUrlSchemeError;
use crate::npm::InNpmPackageCheckerRc;
use crate::NpmResolverRc;
use crate::NpmPackageFolderResolverRc;
use crate::PackageJsonResolverRc;
use crate::PathClean;
use deno_package_json::PackageJson;
@ -101,7 +101,7 @@ pub type NodeResolverRc<TEnv> = crate::sync::MaybeArc<NodeResolver<TEnv>>;
pub struct NodeResolver<TEnv: NodeResolverEnv> {
env: TEnv,
in_npm_pkg_checker: InNpmPackageCheckerRc,
npm_resolver: NpmResolverRc,
npm_pkg_folder_resolver: NpmPackageFolderResolverRc,
pkg_json_resolver: PackageJsonResolverRc<TEnv>,
}
@ -109,13 +109,13 @@ impl<TEnv: NodeResolverEnv> NodeResolver<TEnv> {
pub fn new(
env: TEnv,
in_npm_pkg_checker: InNpmPackageCheckerRc,
npm_resolver: NpmResolverRc,
npm_pkg_folder_resolver: NpmPackageFolderResolverRc,
pkg_json_resolver: PackageJsonResolverRc<TEnv>,
) -> Self {
Self {
env,
in_npm_pkg_checker,
npm_resolver,
npm_pkg_folder_resolver,
pkg_json_resolver,
}
}
@ -1126,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
@ -1412,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>(
@ -1660,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('*');