#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![doc = include_str!("../examples/resolver.rs")]
mod builtins;
mod cache;
mod context;
mod error;
mod file_system;
mod options;
mod package_json;
mod path;
mod resolution;
mod specifier;
mod tsconfig;
#[cfg(test)]
mod tests;
use std::{
borrow::Cow,
cmp::Ordering,
ffi::OsStr,
fmt,
path::{Component, Path, PathBuf},
sync::Arc,
};
use dashmap::DashSet;
use futures::future::{try_join_all, BoxFuture};
use rustc_hash::FxHashSet;
pub use crate::{
builtins::NODEJS_BUILTINS,
error::{JSONError, ResolveError, SpecifierError},
file_system::{FileMetadata, FileSystem, FileSystemOptions, FileSystemOs},
options::{
Alias, AliasValue, EnforceExtension, ResolveOptions, Restriction, TsconfigOptions,
TsconfigReferences,
},
package_json::{JSONValue, ModuleType, PackageJson},
resolution::Resolution,
};
use crate::{
cache::{Cache, CachedPath},
context::ResolveContext as Ctx,
package_json::JSONMap,
path::{PathUtil, SLASH_START},
specifier::Specifier,
tsconfig::{ExtendsField, ProjectReference, TsConfig},
};
type ResolveResult = Result<Option<CachedPath>, ResolveError>;
#[derive(Debug, Default, Clone)]
pub struct ResolveContext {
pub file_dependencies: FxHashSet<PathBuf>,
pub missing_dependencies: FxHashSet<PathBuf>,
}
pub type Resolver = ResolverGeneric<FileSystemOs>;
pub struct ResolverGeneric<Fs> {
options: ResolveOptions,
cache: Arc<Cache<Fs>>,
#[cfg(feature = "yarn_pnp")]
pnp_manifest: Arc<arc_swap::ArcSwapOption<pnp::Manifest>>,
#[cfg(feature = "yarn_pnp")]
pnp_no_manifest_cache: Arc<DashSet<CachedPath>>,
}
impl<Fs> fmt::Debug for ResolverGeneric<Fs> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.options.fmt(f)
}
}
impl<Fs: Send + Sync + FileSystem + Default> Default for ResolverGeneric<Fs> {
fn default() -> Self {
Self::new(ResolveOptions::default())
}
}
impl<Fs: Send + Sync + FileSystem + Default> ResolverGeneric<Fs> {
pub fn new(options: ResolveOptions) -> Self {
Self {
options: options.sanitize(),
cache: Arc::new(Cache::new(Fs::default())),
#[cfg(feature = "yarn_pnp")]
pnp_manifest: Arc::new(arc_swap::ArcSwapOption::empty()),
#[cfg(feature = "yarn_pnp")]
pnp_no_manifest_cache: Arc::new(DashSet::new()),
}
}
}
impl<Fs: FileSystem + Send + Sync> ResolverGeneric<Fs> {
pub fn new_with_file_system(file_system: Fs, options: ResolveOptions) -> Self {
Self {
options: options.sanitize(),
cache: Arc::new(Cache::new(file_system)),
#[cfg(feature = "yarn_pnp")]
pnp_manifest: Arc::new(arc_swap::ArcSwapOption::empty()),
#[cfg(feature = "yarn_pnp")]
pnp_no_manifest_cache: Arc::new(DashSet::new()),
}
}
#[must_use]
pub fn clone_with_options(&self, options: ResolveOptions) -> Self {
Self {
options: options.sanitize(),
cache: Arc::clone(&self.cache),
#[cfg(feature = "yarn_pnp")]
pnp_manifest: Arc::clone(&self.pnp_manifest),
#[cfg(feature = "yarn_pnp")]
pnp_no_manifest_cache: Arc::clone(&self.pnp_no_manifest_cache),
}
}
pub fn options(&self) -> &ResolveOptions {
&self.options
}
pub fn clear_cache(&self) {
self.cache.clear();
#[cfg(feature = "yarn_pnp")]
{
self.pnp_manifest.store(None);
self.pnp_no_manifest_cache.clear();
}
}
pub async fn resolve<P: Send + AsRef<Path>>(
&self,
directory: P,
specifier: &str,
) -> Result<Resolution, ResolveError> {
let mut ctx = Ctx::default();
self
.resolve_tracing(directory.as_ref(), specifier, &mut ctx)
.await
}
pub async fn resolve_with_context<P: Send + AsRef<Path>>(
&self,
directory: P,
specifier: &str,
resolve_context: &mut ResolveContext,
) -> Result<Resolution, ResolveError> {
let mut ctx = Ctx::default();
ctx.init_file_dependencies();
let result = self
.resolve_tracing(directory.as_ref(), specifier, &mut ctx)
.await;
if let Some(deps) = &mut ctx.file_dependencies {
resolve_context.file_dependencies.extend(deps.drain(..));
}
if let Some(deps) = &mut ctx.missing_dependencies {
resolve_context.missing_dependencies.extend(deps.drain(..));
}
result
}
#[cfg_attr(feature="enable_instrument", tracing::instrument(level=tracing::Level::DEBUG, skip_all, fields(path = %directory.to_string_lossy(), specifier = specifier)))]
async fn resolve_tracing(
&self,
directory: &Path,
specifier: &str,
ctx: &mut Ctx,
) -> Result<Resolution, ResolveError> {
let span = tracing::debug_span!("resolve", path = ?directory, specifier = specifier);
let _enter = span.enter();
let r = self.resolve_impl(directory, specifier, ctx).await;
match &r {
Ok(r) => {
tracing::debug!(options = ?self.options, ret = ?r.path);
}
Err(err) => {
tracing::debug!(options = ?self.options, err = ?err);
}
}
r
}
async fn resolve_impl(
&self,
path: &Path,
specifier: &str,
ctx: &mut Ctx,
) -> Result<Resolution, ResolveError> {
ctx.with_fully_specified(self.options.fully_specified);
let cached_path = self.cache.value(path);
let cached_path = self.require(&cached_path, specifier, ctx).await?;
let path = self.load_realpath(&cached_path, ctx).await?;
let package_json = cached_path
.find_package_json(&self.cache.fs, &self.options, ctx)
.await?;
if let Some(package_json) = &package_json {
debug_assert!(path.starts_with(package_json.directory()));
}
Ok(Resolution {
path,
query: ctx.query.take(),
fragment: ctx.fragment.take(),
package_json,
})
}
fn require<'a>(
&'a self,
cached_path: &'a CachedPath,
specifier: &'a str,
ctx: &'a mut Ctx,
) -> BoxFuture<'a, Result<CachedPath, ResolveError>> {
let fut = async move {
ctx.test_for_infinite_recursion()?;
let (parsed, try_fragment_as_path) = self.load_parse(cached_path, specifier, ctx).await?;
if let Some(path) = try_fragment_as_path {
return Ok(path);
}
self
.require_without_parse(cached_path, parsed.path(), ctx)
.await
};
Box::pin(fut)
}
async fn require_without_parse(
&self,
cached_path: &CachedPath,
specifier: &str,
ctx: &mut Ctx,
) -> Result<CachedPath, ResolveError> {
if let Some(path) = self
.load_tsconfig_paths(cached_path, specifier, &mut Ctx::default())
.await?
{
return Ok(path);
}
if let Some(path) = self
.load_alias(cached_path, specifier, &self.options.alias, ctx)
.await?
{
return Ok(path);
}
let result = match Path::new(specifier).components().next() {
Some(Component::RootDir | Component::Prefix(_)) => {
self.require_absolute(cached_path, specifier, ctx).await
}
Some(Component::CurDir | Component::ParentDir) => {
self.require_relative(cached_path, specifier, ctx).await
}
Some(Component::Normal(_)) if specifier.as_bytes()[0] == b'#' => {
self.require_hash(cached_path, specifier, ctx).await
}
_ => {
self.require_core(specifier)?;
self.require_bare(cached_path, specifier, ctx).await
}
};
match result {
Ok(_) => result,
Err(err) => {
if err.is_ignore() {
return Err(err);
}
self
.load_alias(cached_path, specifier, &self.options.fallback, ctx)
.await
.and_then(|value| value.ok_or(err))
}
}
}
fn require_core(&self, specifier: &str) -> Result<(), ResolveError> {
if self.options.builtin_modules {
let starts_with_node = specifier.starts_with("node:");
if starts_with_node || NODEJS_BUILTINS.binary_search(&specifier).is_ok() {
let mut specifier = specifier.to_string();
if !starts_with_node {
specifier = format!("node:{specifier}");
}
return Err(ResolveError::Builtin(specifier));
}
}
Ok(())
}
async fn require_absolute(
&self,
cached_path: &CachedPath,
specifier: &str,
ctx: &mut Ctx,
) -> Result<CachedPath, ResolveError> {
debug_assert!(Path::new(specifier)
.components()
.next()
.is_some_and(|c| matches!(c, Component::RootDir | Component::Prefix(_))));
if !self.options.prefer_relative && self.options.prefer_absolute {
if let Ok(path) = self
.load_package_self_or_node_modules(cached_path, specifier, ctx)
.await
{
return Ok(path);
}
}
if let Some(path) = self.load_roots(specifier, ctx).await {
return Ok(path);
}
let path = self.cache.value(
#[cfg(windows)]
&Path::new(specifier).normalize(),
#[cfg(not(windows))]
Path::new(specifier),
);
if let Some(path) = self
.load_as_file_or_directory(&path, specifier, ctx)
.await?
{
return Ok(path);
}
Err(ResolveError::NotFound(specifier.to_string()))
}
#[cfg_attr(feature="enable_instrument", tracing::instrument(level=tracing::Level::DEBUG, skip_all, fields(specifier = specifier, path = %cached_path.path().to_string_lossy())))]
async fn require_relative(
&self,
cached_path: &CachedPath,
specifier: &str,
ctx: &mut Ctx,
) -> Result<CachedPath, ResolveError> {
debug_assert!(Path::new(specifier)
.components()
.next()
.is_some_and(|c| matches!(
c,
Component::CurDir | Component::ParentDir | Component::Normal(_)
)));
let path = cached_path.path().normalize_with(specifier);
let cached_path = self.cache.value(&path);
if let Some(path) = self
.load_as_file_or_directory(&cached_path, specifier, ctx)
.await?
{
return Ok(path);
}
Err(ResolveError::NotFound(specifier.to_string()))
}
async fn require_hash(
&self,
cached_path: &CachedPath,
specifier: &str,
ctx: &mut Ctx,
) -> Result<CachedPath, ResolveError> {
debug_assert_eq!(specifier.chars().next(), Some('#'));
if let Some(path) = self
.load_package_imports(cached_path, specifier, ctx)
.await?
{
return Ok(path);
}
self
.load_package_self_or_node_modules(cached_path, specifier, ctx)
.await
}
#[cfg_attr(feature="enable_instrument", tracing::instrument(level=tracing::Level::DEBUG, skip_all, fields(specifier = specifier)))]
async fn require_bare(
&self,
cached_path: &CachedPath,
specifier: &str,
ctx: &mut Ctx,
) -> Result<CachedPath, ResolveError> {
debug_assert!(Path::new(specifier)
.components()
.next()
.is_some_and(|c| matches!(c, Component::Normal(_))));
if self.options.prefer_relative {
if let Ok(path) = self.require_relative(cached_path, specifier, ctx).await {
return Ok(path);
}
}
self
.load_package_self_or_node_modules(cached_path, specifier, ctx)
.await
}
async fn load_parse<'s>(
&self,
cached_path: &CachedPath,
specifier: &'s str,
ctx: &mut Ctx,
) -> Result<(Specifier<'s>, Option<CachedPath>), ResolveError> {
let parsed = Specifier::parse(specifier).map_err(ResolveError::Specifier)?;
ctx.with_query_fragment(parsed.query, parsed.fragment);
if ctx.fragment.is_some() && ctx.query.is_none() {
let specifier = parsed.path();
let fragment = ctx.fragment.take().unwrap();
let path = format!("{specifier}{fragment}");
if let Ok(path) = self.require_without_parse(cached_path, &path, ctx).await {
return Ok((parsed, Some(path)));
}
ctx.fragment.replace(fragment);
}
Ok((parsed, None))
}
#[cfg_attr(feature="enable_instrument", tracing::instrument(level=tracing::Level::DEBUG, skip_all, fields(specifier = specifier, path = %cached_path.path().to_string_lossy())))]
async fn load_package_self_or_node_modules(
&self,
cached_path: &CachedPath,
specifier: &str,
ctx: &mut Ctx,
) -> Result<CachedPath, ResolveError> {
let (_, subpath) = Self::parse_package_specifier(specifier);
if subpath.is_empty() {
ctx.with_fully_specified(false);
}
if let Some(path) = self.load_package_self(cached_path, specifier, ctx).await? {
return Ok(path);
}
if let Some(path) = self.load_node_modules(cached_path, specifier, ctx).await? {
return Ok(path);
}
Err(ResolveError::NotFound(specifier.to_string()))
}
async fn load_package_imports(
&self,
cached_path: &CachedPath,
specifier: &str,
ctx: &mut Ctx,
) -> ResolveResult {
let Some(package_json) = cached_path
.find_package_json(&self.cache.fs, &self.options, ctx)
.await?
else {
return Ok(None);
};
if let Some(path) = self
.package_imports_resolve(specifier, &package_json, ctx)
.await?
{
return self.resolve_esm_match(specifier, &path, ctx).await;
}
Ok(None)
}
#[cfg_attr(feature="enable_instrument", tracing::instrument(level=tracing::Level::DEBUG, skip_all, fields(path = %cached_path.path().to_string_lossy())))]
async fn load_as_file(&self, cached_path: &CachedPath, ctx: &mut Ctx) -> ResolveResult {
if let Some(path) = self.load_extension_alias(cached_path, ctx).await? {
return Ok(Some(path));
}
if self.options.enforce_extension.is_disabled() {
if let Some(path) = self.load_alias_or_file(cached_path, ctx).await? {
return Ok(Some(path));
}
}
if let Some(path) = self
.load_extensions(cached_path, &self.options.extensions, ctx)
.await?
{
return Ok(Some(path));
}
Ok(None)
}
async fn load_as_directory(&self, cached_path: &CachedPath, ctx: &mut Ctx) -> ResolveResult {
if !self.options.description_files.is_empty() {
if let Some(package_json) = cached_path
.package_json(&self.cache.fs, &self.options, ctx)
.await?
{
for main_field in package_json.main_fields(&self.options.main_fields) {
let main_field = if main_field.starts_with("./") || main_field.starts_with("../") {
Cow::Borrowed(main_field)
} else {
Cow::Owned(format!("./{main_field}"))
};
let main_field_path = cached_path.path().normalize_with(main_field.as_ref());
let cached_path = self.cache.value(&main_field_path);
if let Ok(Some(path)) = self.load_as_file(&cached_path, ctx).await {
return Ok(Some(path));
}
if let Some(path) = self.load_index(&cached_path, ctx).await? {
return Ok(Some(path));
}
}
}
}
self.load_index(cached_path, ctx).await
}
#[cfg_attr(feature="enable_instrument", tracing::instrument(level=tracing::Level::DEBUG, skip_all, fields(specifier = specifier, path = %cached_path.path().to_string_lossy())))]
async fn load_as_file_or_directory(
&self,
cached_path: &CachedPath,
specifier: &str,
ctx: &mut Ctx,
) -> ResolveResult {
if self.options.resolve_to_context {
return Ok(
cached_path
.is_dir(&self.cache.fs, ctx)
.await
.then(|| cached_path.clone()),
);
}
if !specifier.ends_with('/') {
if let Some(path) = self.load_as_file(cached_path, ctx).await? {
return Ok(Some(path));
}
}
if cached_path.is_dir(&self.cache.fs, ctx).await {
if let Some(path) = self.load_as_directory(cached_path, ctx).await? {
return Ok(Some(path));
}
}
Ok(None)
}
#[cfg_attr(feature="enable_instrument", tracing::instrument(level=tracing::Level::DEBUG, skip_all, fields(path = %path.path().to_string_lossy())))]
async fn load_extensions(
&self,
path: &CachedPath,
extensions: &[String],
ctx: &mut Ctx,
) -> ResolveResult {
if ctx.fully_specified {
return Ok(None);
}
let path = path.path().as_os_str();
let mut path_with_extension_buffer = String::with_capacity(path.len() + 8);
path_with_extension_buffer.push_str(&path.to_string_lossy());
let base_len = path_with_extension_buffer.len();
for extension in extensions {
path_with_extension_buffer.truncate(base_len);
path_with_extension_buffer.push_str(extension);
let cached_path = self.cache.value(Path::new(&path_with_extension_buffer));
if let Some(path) = self.load_alias_or_file(&cached_path, ctx).await? {
return Ok(Some(path));
}
}
Ok(None)
}
#[cfg_attr(feature="enable_instrument", tracing::instrument(level=tracing::Level::DEBUG, skip_all, fields(path = %cached_path.path().to_string_lossy())))]
async fn load_realpath(
&self,
cached_path: &CachedPath,
ctx: &mut Ctx,
) -> Result<PathBuf, ResolveError> {
if self.options.symlinks {
cached_path
.realpath(&self.cache.fs)
.await
.map(|path| {
ctx.add_file_dependency(&path);
path
})
.map_err(ResolveError::from)
} else {
Ok(cached_path.to_path_buf())
}
}
fn check_restrictions(&self, path: &Path) -> bool {
fn is_inside(path: &Path, parent: &Path) -> bool {
if !path.starts_with(parent) {
return false;
}
if path.as_os_str().len() == parent.as_os_str().len() {
return true;
}
path
.strip_prefix(parent)
.is_ok_and(|p| p == Path::new("./"))
}
for restriction in &self.options.restrictions {
match restriction {
Restriction::Path(restricted_path) => {
if !is_inside(path, restricted_path) {
return false;
}
}
Restriction::Fn(f) => {
if !f(path) {
return false;
}
}
}
}
true
}
#[cfg_attr(feature="enable_instrument", tracing::instrument(level=tracing::Level::DEBUG, skip_all, fields(path = %cached_path.path().to_string_lossy())))]
async fn load_index(&self, cached_path: &CachedPath, ctx: &mut Ctx) -> ResolveResult {
for main_file in &self.options.main_files {
let main_path = cached_path.path().normalize_with(main_file);
let cached_path = self.cache.value(&main_path);
if self.options.enforce_extension.is_disabled() {
if let Some(path) = self.load_alias_or_file(&cached_path, ctx).await? {
if self.check_restrictions(path.path()) {
return Ok(Some(path));
}
}
}
if let Some(path) = self
.load_extensions(&cached_path, &self.options.extensions, ctx)
.await?
{
return Ok(Some(path));
}
}
Ok(None)
}
async fn load_alias_or_file(&self, cached_path: &CachedPath, ctx: &mut Ctx) -> ResolveResult {
if !self.options.alias_fields.is_empty() {
if let Some(package_json) = cached_path
.find_package_json(&self.cache.fs, &self.options, ctx)
.await?
{
if let Some(path) = self
.load_browser_field(cached_path, None, &package_json, ctx)
.await?
{
return Ok(Some(path));
}
}
}
let alias_specifier = cached_path.path().to_string_lossy();
if let Some(path) = self
.load_alias(cached_path, &alias_specifier, &self.options.alias, ctx)
.await?
{
return Ok(Some(path));
}
if cached_path.is_file(&self.cache.fs, ctx).await && self.check_restrictions(cached_path.path())
{
return Ok(Some(cached_path.clone()));
}
Ok(None)
}
#[cfg_attr(feature="enable_instrument", tracing::instrument(level=tracing::Level::DEBUG, skip_all, fields(specifier = specifier, path = %cached_path.path().to_string_lossy())))]
async fn load_node_modules(
&self,
cached_path: &CachedPath,
specifier: &str,
ctx: &mut Ctx,
) -> ResolveResult {
#[cfg(feature = "yarn_pnp")]
{
if self.options.enable_pnp {
if let Some(resolved_path) = self.load_pnp(cached_path, specifier, ctx).await? {
return Ok(Some(resolved_path));
}
}
}
let (package_name, subpath) = Self::parse_package_specifier(specifier);
for module_name in &self.options.modules {
for cached_path in std::iter::successors(Some(cached_path), |p| p.parent()) {
if !cached_path.is_dir(&self.cache.fs, ctx).await {
continue;
}
let Some(cached_path) = self
.get_module_directory(cached_path, module_name, ctx)
.await
else {
continue;
};
if !package_name.is_empty() {
let package_path = cached_path.path().normalize_with(package_name);
let cached_path = self.cache.value(&package_path);
if cached_path.is_dir(&self.cache.fs, ctx).await {
if let Some(path) = self
.load_package_exports(specifier, subpath, &cached_path, ctx)
.await?
{
return Ok(Some(path));
}
} else {
if !subpath.is_empty() {
continue;
}
if package_name.starts_with('@') {
if let Some(path) = cached_path.parent() {
if !path.is_dir(&self.cache.fs, ctx).await {
continue;
}
}
}
}
}
let node_module_file = cached_path.path().normalize_with(specifier);
let cached_path = self.cache.value(&node_module_file);
if let Some(path) = self
.load_as_file_or_directory(&cached_path, specifier, ctx)
.await?
{
return Ok(Some(path));
}
}
}
Ok(None)
}
#[cfg(feature = "yarn_pnp")]
#[cfg_attr(feature = "enable_instrument", tracing::instrument(level=tracing::Level::DEBUG, skip_all, fields(path = %cached_path.path().to_string_lossy())))]
fn find_pnp_manifest(&self, cached_path: &CachedPath) -> Option<Arc<pnp::Manifest>> {
if let Some(manifest) = self.pnp_manifest.load_full() {
return Some(manifest);
}
if self.pnp_no_manifest_cache.contains(cached_path) {
return None;
}
let base_path = cached_path.to_path_buf();
let Some(manifest_path) = pnp::find_closest_pnp_manifest_path(&base_path) else {
self.pnp_no_manifest_cache.insert(cached_path.clone());
for p in base_path.ancestors() {
let p_cached = self.cache.value(p);
if self.pnp_no_manifest_cache.contains(&p_cached) {
break;
}
self.pnp_no_manifest_cache.insert(p_cached);
}
return None;
};
tracing::debug!("use manifest path: {:?}", manifest_path);
let manifest = pnp::load_pnp_manifest(&manifest_path).ok()?;
let manifest = Arc::new(manifest);
let previous = self
.pnp_manifest
.compare_and_swap(&None::<Arc<_>>, Some(Arc::clone(&manifest)));
if let Some(existing) = previous.as_ref() {
return Some(Arc::clone(existing));
}
Some(manifest)
}
#[cfg(feature = "yarn_pnp")]
#[cfg_attr(feature="enable_instrument", tracing::instrument(level=tracing::Level::DEBUG, skip_all, fields(specifier = specifier, path = %cached_path.path().to_string_lossy())))]
async fn load_pnp(
&self,
cached_path: &CachedPath,
specifier: &str,
ctx: &mut Ctx,
) -> Result<Option<CachedPath>, ResolveError> {
let Some(manifest) = self.find_pnp_manifest(cached_path) else {
return Ok(None);
};
let mut path = cached_path.to_path_buf();
path.push("");
let resolution = pnp::resolve_to_unqualified_via_manifest(&manifest, specifier, &path);
tracing::debug!("pnp resolve unqualified as: {:?}", resolution);
match resolution {
Ok(pnp::Resolution::Resolved(path, subpath)) => {
let cached_path = self.cache.value(&path);
let export_resolution = self.load_package_self(&cached_path, specifier, ctx).await?;
if export_resolution.is_some() {
return Ok(export_resolution);
}
let inner_request = subpath.map_or_else(
|| ".".to_string(),
|mut p| {
p.insert_str(0, "./");
p
},
);
let inner_resolver = self.clone_with_options(self.options().clone());
let Ok(inner_resolution) = inner_resolver.resolve(&path, &inner_request).await else {
return Err(ResolveError::NotFound(specifier.to_string()));
};
Ok(Some(self.cache.value(inner_resolution.path())))
}
Ok(pnp::Resolution::Skipped) => Ok(None),
Err(_) => Err(ResolveError::NotFound(specifier.to_string())),
}
}
async fn get_module_directory(
&self,
cached_path: &CachedPath,
module_name: &str,
ctx: &mut Ctx,
) -> Option<CachedPath> {
if module_name == "node_modules" {
cached_path.cached_node_modules(&self.cache, ctx).await
} else if cached_path.path().components().next_back()
== Some(Component::Normal(OsStr::new(module_name)))
{
Some(cached_path.clone())
} else {
cached_path
.module_directory(module_name, &self.cache, ctx)
.await
}
}
async fn load_package_exports(
&self,
specifier: &str,
subpath: &str,
cached_path: &CachedPath,
ctx: &mut Ctx,
) -> ResolveResult {
let Some(package_json) = cached_path
.package_json(&self.cache.fs, &self.options, ctx)
.await?
else {
return Ok(None);
};
for exports in package_json.exports_fields(&self.options.exports_fields) {
if let Some(path) = self
.package_exports_resolve(cached_path.path(), &format!(".{subpath}"), exports, ctx)
.await?
{
return self.resolve_esm_match(specifier, &path, ctx).await;
};
}
Ok(None)
}
#[cfg_attr(feature="enable_instrument", tracing::instrument(level=tracing::Level::DEBUG, skip_all, fields(specifier = specifier, path = %cached_path.path().to_string_lossy())))]
async fn load_package_self(
&self,
cached_path: &CachedPath,
specifier: &str,
ctx: &mut Ctx,
) -> ResolveResult {
let Some(package_json) = cached_path
.find_package_json(&self.cache.fs, &self.options, ctx)
.await?
else {
return Ok(None);
};
if let Some(subpath) = package_json
.name
.as_ref()
.and_then(|package_name| Self::strip_package_name(specifier, package_name))
{
let package_url = package_json.directory();
for exports in package_json.exports_fields(&self.options.exports_fields) {
if let Some(cached_path) = self
.package_exports_resolve(package_url, &format!(".{subpath}"), exports, ctx)
.await?
{
return self.resolve_esm_match(specifier, &cached_path, ctx).await;
}
}
}
self
.load_browser_field(cached_path, Some(specifier), &package_json, ctx)
.await
}
#[cfg_attr(feature="enable_instrument", tracing::instrument(level=tracing::Level::DEBUG, skip_all, fields(specifier = specifier, path = %cached_path.path().to_string_lossy())))]
async fn resolve_esm_match(
&self,
specifier: &str,
cached_path: &CachedPath,
ctx: &mut Ctx,
) -> ResolveResult {
if let Some(path) = self.load_as_file_or_directory(cached_path, "", ctx).await? {
return Ok(Some(path));
}
let mut path_str = cached_path.path().to_str();
while let Some(s) = path_str {
if let Some((before, _)) = s.rsplit_once('?') {
if (self
.load_as_file_or_directory(&self.cache.value(Path::new(before)), "", ctx)
.await?)
.is_some()
{
return Ok(Some(cached_path.clone()));
}
path_str = Some(before);
} else {
break;
}
}
Err(ResolveError::NotFound(specifier.to_string()))
}
#[cfg_attr(feature="enable_instrument", tracing::instrument(level=tracing::Level::DEBUG, skip_all, fields(specifier = module_specifier, path = %cached_path.path().to_string_lossy())))]
async fn load_browser_field(
&self,
cached_path: &CachedPath,
module_specifier: Option<&str>,
package_json: &PackageJson,
ctx: &mut Ctx,
) -> ResolveResult {
let path = cached_path.path();
let Some(new_specifier) =
package_json.resolve_browser_field(path, module_specifier, &self.options.alias_fields)?
else {
return Ok(None);
};
if module_specifier.is_some_and(|s| s == new_specifier) {
return Ok(None);
}
if ctx
.resolving_alias
.as_ref()
.is_some_and(|s| s == new_specifier)
{
if new_specifier
.strip_prefix("./")
.filter(|s| path.ends_with(Path::new(s)))
.is_some()
{
return if cached_path.is_file(&self.cache.fs, ctx).await {
if self.check_restrictions(cached_path.path()) {
Ok(Some(cached_path.clone()))
} else {
Ok(None)
}
} else {
Err(ResolveError::NotFound(new_specifier.to_string()))
};
}
return Err(ResolveError::Recursion);
}
ctx.with_resolving_alias(new_specifier.to_string());
ctx.with_fully_specified(false);
let cached_path = self.cache.value(package_json.directory());
self
.require(&cached_path, new_specifier, ctx)
.await
.map(Some)
}
async fn load_alias(
&self,
cached_path: &CachedPath,
specifier: &str,
aliases: &Alias,
ctx: &mut Ctx,
) -> ResolveResult {
for (alias_key_raw, specifiers) in aliases {
let alias_key = if let Some(alias_key) = alias_key_raw.strip_suffix('$') {
if alias_key != specifier {
continue;
}
alias_key
} else {
let strip_package_name = Self::strip_package_name(specifier, alias_key_raw);
if strip_package_name.is_none() {
continue;
}
alias_key_raw
};
let mut should_stop = false;
for r in specifiers {
match r {
AliasValue::Path(alias_value) => {
if let Some(path) = self
.load_alias_value(
cached_path,
alias_key,
alias_value,
specifier,
ctx,
&mut should_stop,
)
.await?
{
return Ok(Some(path));
}
}
AliasValue::Ignore => {
let path = cached_path.path().normalize_with(alias_key);
return Err(ResolveError::Ignored(path));
}
}
}
if should_stop {
return Err(ResolveError::MatchedAliasNotFound(
specifier.to_string(),
alias_key.to_string(),
));
}
}
Ok(None)
}
async fn load_alias_value(
&self,
cached_path: &CachedPath,
alias_key: &str,
alias_value: &str,
request: &str,
ctx: &mut Ctx,
should_stop: &mut bool,
) -> ResolveResult {
if request != alias_value
&& !request
.strip_prefix(alias_value)
.is_some_and(|prefix| prefix.starts_with('/'))
{
let tail = &request[alias_key.len()..];
let new_specifier = if tail.is_empty() {
Cow::Borrowed(alias_value)
} else {
let alias_path = Path::new(alias_value).normalize();
let alias_value_cached_path = self.cache.value(&alias_path);
if alias_value_cached_path.is_file(&self.cache.fs, ctx).await {
return Ok(None);
}
let tail = tail.trim_start_matches(SLASH_START);
if tail.is_empty() {
Cow::Borrowed(alias_value)
} else {
let normalized = alias_path.normalize_with(tail);
Cow::Owned(normalized.to_string_lossy().to_string())
}
};
*should_stop = true;
ctx.with_fully_specified(false);
return match self.require(cached_path, new_specifier.as_ref(), ctx).await {
Err(ResolveError::NotFound(_) | ResolveError::MatchedAliasNotFound(_, _)) => Ok(None),
Ok(path) => return Ok(Some(path)),
Err(err) => return Err(err),
};
}
Ok(None)
}
async fn load_extension_alias(&self, cached_path: &CachedPath, ctx: &mut Ctx) -> ResolveResult {
if self.options.extension_alias.is_empty() {
return Ok(None);
}
let Some(path_extension) = cached_path.path().extension() else {
return Ok(None);
};
let Some((_, extensions)) = self
.options
.extension_alias
.iter()
.find(|(ext, _)| OsStr::new(ext.trim_start_matches('.')) == path_extension)
else {
return Ok(None);
};
let path = cached_path.path();
let Some(filename) = path.file_name() else {
return Ok(None);
};
let path_without_extension = path.with_extension("");
ctx.with_fully_specified(true);
for extension in extensions {
let mut path_with_extension = path_without_extension.clone().into_os_string();
path_with_extension.reserve_exact(extension.len());
path_with_extension.push(extension);
let cached_path = self.cache.value(Path::new(&path_with_extension));
if let Some(path) = self.load_alias_or_file(&cached_path, ctx).await? {
ctx.with_fully_specified(false);
return Ok(Some(path));
}
}
if !cached_path.is_file(&self.cache.fs, ctx).await
|| !self.check_restrictions(cached_path.path())
{
ctx.with_fully_specified(false);
return Ok(None);
}
let dir = path.parent().unwrap().to_path_buf();
let filename_without_extension = Path::new(filename).with_extension("");
let filename_without_extension = filename_without_extension.to_string_lossy();
let files = extensions
.iter()
.map(|ext| format!("{filename_without_extension}{ext}"))
.collect::<Vec<_>>()
.join(",");
Err(ResolveError::ExtensionAlias(
filename.to_string_lossy().to_string(),
files,
dir,
))
}
async fn load_roots(&self, specifier: &str, ctx: &mut Ctx) -> Option<CachedPath> {
if self.options.roots.is_empty() {
return None;
}
if let Some(specifier) = specifier.strip_prefix(SLASH_START) {
for root in &self.options.roots {
let cached_path = self.cache.value(root);
if let Ok(path) = self.require_relative(&cached_path, specifier, ctx).await {
return Some(path);
}
}
}
None
}
async fn load_tsconfig_paths(
&self,
cached_path: &CachedPath,
specifier: &str,
ctx: &mut Ctx,
) -> ResolveResult {
let Some(tsconfig_options) = &self.options.tsconfig else {
return Ok(None);
};
let tsconfig = self
.load_tsconfig(
true,
&tsconfig_options.config_file,
&tsconfig_options.references,
)
.await?;
let paths = tsconfig.resolve(cached_path.path(), specifier);
for path in paths {
let cached_path = self.cache.value(&path);
if let Ok(path) = self.require_relative(&cached_path, ".", ctx).await {
return Ok(Some(path));
}
}
Ok(None)
}
#[cfg_attr(feature="enable_instrument", tracing::instrument(level=tracing::Level::DEBUG, skip(self), fields(path = path.display().to_string())))]
fn load_tsconfig<'a>(
&'a self,
root: bool,
path: &'a Path,
references: &'a TsconfigReferences,
) -> BoxFuture<'a, Result<Arc<TsConfig>, ResolveError>> {
let fut = async move {
self
.cache
.tsconfig(root, path, |mut tsconfig| async move {
let directory = self.cache.value(tsconfig.directory());
tracing::trace!(tsconfig = ?tsconfig, "load_tsconfig");
if let Some(extends) = &tsconfig.extends {
let extended_tsconfig_paths = match extends {
ExtendsField::Single(s) => {
vec![
self
.get_extended_tsconfig_path(&directory, &tsconfig, s)
.await?,
]
}
ExtendsField::Multiple(specifiers) => {
try_join_all(
specifiers
.iter()
.map(|s| self.get_extended_tsconfig_path(&directory, &tsconfig, s)),
)
.await?
}
};
for extended_tsconfig_path in extended_tsconfig_paths {
let extended_tsconfig = self
.load_tsconfig(
false,
&extended_tsconfig_path,
&TsconfigReferences::Disabled,
)
.await?;
tsconfig.extend_tsconfig(&extended_tsconfig);
}
}
match references {
TsconfigReferences::Disabled => {
tsconfig.references.drain(..);
}
TsconfigReferences::Auto => {}
TsconfigReferences::Paths(paths) => {
tsconfig.references = paths
.iter()
.map(|path| ProjectReference {
path: path.clone(),
tsconfig: None,
})
.collect();
}
}
if !tsconfig.references.is_empty() {
let directory = tsconfig.directory().to_path_buf();
for reference in &mut tsconfig.references {
let reference_tsconfig_path = directory.normalize_with(&reference.path);
let tsconfig = self
.cache
.tsconfig(
true,
&reference_tsconfig_path,
|reference_tsconfig| async {
if reference_tsconfig.path == tsconfig.path {
return Err(ResolveError::TsconfigSelfReference(
reference_tsconfig.path.clone(),
));
}
Ok(reference_tsconfig)
},
)
.await?;
reference.tsconfig.replace(tsconfig);
}
}
Ok(tsconfig)
})
.await
};
Box::pin(fut)
}
async fn get_extended_tsconfig_path(
&self,
directory: &CachedPath,
tsconfig: &TsConfig,
specifier: &str,
) -> Result<PathBuf, ResolveError> {
match specifier.as_bytes().first() {
None => Err(ResolveError::Specifier(SpecifierError::Empty(
specifier.to_string(),
))),
Some(b'/') => Ok(PathBuf::from(specifier)),
Some(b'.') => Ok(tsconfig.directory().normalize_with(specifier)),
_ => self
.clone_with_options(ResolveOptions {
description_files: vec![],
extensions: vec![".json".into()],
main_files: vec!["tsconfig.json".into()],
..ResolveOptions::default()
})
.load_package_self_or_node_modules(directory, specifier, &mut Ctx::default())
.await
.map(|p| p.to_path_buf())
.map_err(|err| match err {
ResolveError::NotFound(_) => ResolveError::TsconfigNotFound(PathBuf::from(specifier)),
_ => err,
}),
}
}
async fn package_resolve(
&self,
cached_path: &CachedPath,
specifier: &str,
ctx: &mut Ctx,
) -> ResolveResult {
let (package_name, subpath) = Self::parse_package_specifier(specifier);
self.require_core(package_name)?;
for module_name in &self.options.modules {
for cached_path in std::iter::successors(Some(cached_path), |p| p.parent()) {
let Some(cached_path) = self
.get_module_directory(cached_path, module_name, ctx)
.await
else {
continue;
};
let package_path = cached_path.path().normalize_with(package_name);
let cached_path = self.cache.value(&package_path);
if cached_path.is_dir(&self.cache.fs, ctx).await {
if let Some(package_json) = cached_path
.package_json(&self.cache.fs, &self.options, ctx)
.await?
{
for exports in package_json.exports_fields(&self.options.exports_fields) {
if let Some(path) = self
.package_exports_resolve(cached_path.path(), &format!(".{subpath}"), exports, ctx)
.await?
{
return Ok(Some(path));
}
}
if subpath == "." {
for main_field in package_json.main_fields(&self.options.main_fields) {
let path = cached_path.path().normalize_with(main_field);
let cached_path = self.cache.value(&path);
if cached_path.is_file(&self.cache.fs, ctx).await
&& self.check_restrictions(cached_path.path())
{
return Ok(Some(cached_path.clone()));
}
}
}
}
let subpath = format!(".{subpath}");
ctx.with_fully_specified(false);
return self.require(&cached_path, &subpath, ctx).await.map(Some);
}
}
}
Err(ResolveError::NotFound(specifier.to_string()))
}
fn package_exports_resolve<'a>(
&'a self,
package_url: &'a Path,
subpath: &'a str,
exports: &'a JSONValue,
ctx: &'a mut Ctx,
) -> BoxFuture<'a, ResolveResult> {
let fut = async move {
let conditions = &self.options.condition_names;
if let JSONValue::Object(map) = exports {
let mut has_dot = false;
let mut without_dot = false;
for key in map.keys() {
let starts_with_dot_or_hash = key.starts_with(['.', '#']);
has_dot = has_dot || starts_with_dot_or_hash;
without_dot = without_dot || !starts_with_dot_or_hash;
if has_dot && without_dot {
return Err(ResolveError::InvalidPackageConfig(
package_url.join("package.json"),
));
}
}
}
if subpath == "." {
if ctx.query.is_some() || ctx.fragment.is_some() {
let query = ctx.query.clone().unwrap_or_default();
let fragment = ctx.fragment.clone().unwrap_or_default();
return Err(ResolveError::PackagePathNotExported(
format!("./{}{query}{fragment}", subpath.trim_start_matches('.')),
package_url.join("package.json"),
));
}
let main_export = match exports {
JSONValue::String(_) | JSONValue::Array(_) => {
Some(exports)
}
JSONValue::Object(map) => {
map.get(".").map_or_else(
|| {
if map
.keys()
.any(|key| key.starts_with("./") || key.starts_with('#'))
{
None
} else {
Some(exports)
}
},
Some,
)
}
JSONValue::Static(_) => None,
};
if let Some(main_export) = main_export {
let resolved = self
.package_target_resolve(
package_url,
".",
main_export,
None,
false,
conditions,
ctx,
)
.await?;
if let Some(path) = resolved {
return Ok(Some(path));
}
}
}
if let JSONValue::Object(exports) = exports {
let match_key = &subpath;
if let Some(path) = self
.package_imports_exports_resolve(
match_key,
exports,
package_url,
false,
conditions,
ctx,
)
.await?
{
return Ok(Some(path));
}
}
Err(ResolveError::PackagePathNotExported(
subpath.to_string(),
package_url.join("package.json"),
))
};
Box::pin(fut)
}
async fn package_imports_resolve(
&self,
specifier: &str,
package_json: &PackageJson,
ctx: &mut Ctx,
) -> Result<Option<CachedPath>, ResolveError> {
debug_assert!(specifier.starts_with('#'), "{specifier}");
let mut has_imports = false;
for imports in package_json.imports_fields(&self.options.imports_fields) {
if !has_imports {
has_imports = true;
if specifier == "#" || specifier.starts_with("#/") {
return Err(ResolveError::InvalidModuleSpecifier(
specifier.to_string(),
package_json.path.clone(),
));
}
}
if let Some(path) = self
.package_imports_exports_resolve(
specifier,
imports,
package_json.directory(),
true,
&self.options.condition_names,
ctx,
)
.await?
{
return Ok(Some(path));
}
}
if has_imports {
Err(ResolveError::PackageImportNotDefined(
specifier.to_string(),
package_json.path.clone(),
))
} else {
Ok(None)
}
}
async fn package_imports_exports_resolve(
&self,
match_key: &str,
match_obj: &JSONMap<'_>,
package_url: &Path,
is_imports: bool,
conditions: &[String],
ctx: &mut Ctx,
) -> ResolveResult {
if match_key.ends_with('/') {
return Ok(None);
}
if !match_key.contains('*') {
if let Some(target) = match_obj.get(match_key) {
return self
.package_target_resolve(
package_url,
match_key,
target,
None,
is_imports,
conditions,
ctx,
)
.await;
}
}
let mut best_target = None;
let mut best_match = "";
let mut best_key = String::new();
for (expansion_key, target) in match_obj {
if expansion_key.starts_with("./") || expansion_key.starts_with('#') {
if let Some((pattern_base, pattern_trailer)) = expansion_key.split_once('*') {
if match_key.starts_with(pattern_base)
&& !pattern_trailer.contains('*')
&& (pattern_trailer.is_empty()
|| (match_key.len() >= expansion_key.len()
&& match_key.ends_with(pattern_trailer)))
&& Self::pattern_key_compare(&best_key, expansion_key).is_gt()
{
best_target = Some(target);
best_match = &match_key[pattern_base.len()..match_key.len() - pattern_trailer.len()];
best_key = expansion_key.to_string();
}
} else if expansion_key.ends_with('/')
&& match_key.starts_with(&**expansion_key)
&& Self::pattern_key_compare(&best_key, expansion_key).is_gt()
{
best_target = Some(target);
best_match = &match_key[expansion_key.len()..];
best_key = expansion_key.to_string();
}
}
}
if let Some(best_target) = best_target {
return self
.package_target_resolve(
package_url,
&best_key,
best_target,
Some(best_match),
is_imports,
conditions,
ctx,
)
.await;
}
Ok(None)
}
#[allow(clippy::too_many_arguments)]
fn package_target_resolve<'a>(
&'a self,
package_url: &'a Path,
target_key: &'a str,
target: &'a JSONValue,
pattern_match: Option<&'a str>,
is_imports: bool,
conditions: &'a [String],
ctx: &'a mut Ctx,
) -> BoxFuture<'a, ResolveResult> {
let fut = async move {
fn normalize_string_target<'a>(
target_key: &'a str,
target: &'a str,
pattern_match: Option<&'a str>,
package_url: &Path,
) -> Result<Cow<'a, str>, ResolveError> {
let target = if let Some(pattern_match) = pattern_match {
if !target_key.contains('*') && !target.contains('*') {
if target_key.ends_with('/') && target.ends_with('/') {
Cow::Owned(format!("{target}{pattern_match}"))
} else {
return Err(ResolveError::InvalidPackageConfigDirectory(
package_url.join("package.json"),
));
}
} else {
Cow::Owned(target.replace('*', pattern_match))
}
} else {
Cow::Borrowed(target)
};
Ok(target)
}
match target {
JSONValue::String(target) => {
if !target.starts_with("./") {
if !is_imports || target.starts_with("../") || target.starts_with('/') {
return Err(ResolveError::InvalidPackageTarget(
target.to_string(),
target_key.to_string(),
package_url.join("package.json"),
));
}
let target = normalize_string_target(target_key, target, pattern_match, package_url)?;
let package_url = self.cache.value(package_url);
return self.package_resolve(&package_url, &target, ctx).await;
}
let target = normalize_string_target(target_key, target, pattern_match, package_url)?;
if Path::new(target.as_ref()).is_invalid_exports_target() {
return Err(ResolveError::InvalidPackageTarget(
target.to_string(),
target_key.to_string(),
package_url.join("package.json"),
));
}
let resolved_target = package_url.normalize_with(target.as_ref());
let value = self.cache.value(&resolved_target);
return Ok(Some(value));
}
JSONValue::Object(target) => {
for (key, target_value) in target.iter() {
let key = key.to_string();
if key == "default" || conditions.contains(&key) {
let resolved = self
.package_target_resolve(
package_url,
target_key,
target_value,
pattern_match,
is_imports,
conditions,
ctx,
)
.await;
if let Some(path) = resolved? {
return Ok(Some(path));
}
}
}
return Ok(None);
}
JSONValue::Array(targets) => {
if targets.is_empty() {
return Err(ResolveError::PackagePathNotExported(
pattern_match.unwrap_or(".").to_string(),
package_url.join("package.json"),
));
}
for (i, target_value) in targets.iter().enumerate() {
let resolved = self
.package_target_resolve(
package_url,
target_key,
target_value,
pattern_match,
is_imports,
conditions,
ctx,
)
.await;
if resolved.is_err() && i == targets.len() {
return resolved;
}
if let Ok(Some(path)) = resolved {
return Ok(Some(path));
}
}
}
JSONValue::Static(_) => {}
}
Ok(None)
};
Box::pin(fut)
}
fn parse_package_specifier(specifier: &str) -> (&str, &str) {
let mut separator_index = specifier.as_bytes().iter().position(|b| *b == b'/');
if specifier.starts_with('@') {
if separator_index.is_none() || specifier.is_empty() {
} else if let Some(index) = &separator_index {
separator_index = specifier.as_bytes()[*index + 1..]
.iter()
.position(|b| *b == b'/')
.map(|i| i + *index + 1);
}
}
let package_name =
separator_index.map_or(specifier, |separator_index| &specifier[..separator_index]);
let package_subpath =
separator_index.map_or("", |separator_index| &specifier[separator_index..]);
(package_name, package_subpath)
}
fn pattern_key_compare(key_a: &str, key_b: &str) -> Ordering {
if key_a.is_empty() {
return Ordering::Greater;
}
debug_assert!(
key_a.ends_with('/') || key_a.match_indices('*').count() == 1,
"{key_a}"
);
debug_assert!(
key_b.ends_with('/') || key_b.match_indices('*').count() == 1,
"{key_b}"
);
let a_pos = key_a.chars().position(|c| c == '*');
let base_length_a = a_pos.map_or(key_a.len(), |p| p + 1);
let b_pos = key_b.chars().position(|c| c == '*');
let base_length_b = b_pos.map_or(key_b.len(), |p| p + 1);
if base_length_a > base_length_b {
return Ordering::Less;
}
if base_length_b > base_length_a {
return Ordering::Greater;
}
if !key_a.contains('*') {
return Ordering::Greater;
}
if !key_b.contains('*') {
return Ordering::Less;
}
if key_a.len() > key_b.len() {
return Ordering::Less;
}
if key_b.len() > key_a.len() {
return Ordering::Greater;
}
Ordering::Equal
}
fn strip_package_name<'a>(specifier: &'a str, package_name: &'a str) -> Option<&'a str> {
specifier
.strip_prefix(package_name)
.filter(|tail| tail.is_empty() || tail.starts_with(SLASH_START))
}
}