use std::path::{Path, PathBuf};
use std::sync::Mutex;
use oxc_span::Span;
use rustc_hash::FxHashMap;
use fallow_types::discover::{DiscoveredFile, FileId};
use fallow_types::extract::{DynamicImportInfo, DynamicImportPattern, ImportInfo, ImportedName};
use super::ResolveResult;
use super::ResolvedImport;
use super::fallbacks::make_glob_from_pattern;
use super::specifier::resolve_specifier;
use super::types::ResolveContext;
pub(super) fn resolve_dynamic_imports(
ctx: &ResolveContext,
file_path: &Path,
dynamic_imports: &[DynamicImportInfo],
) -> Vec<ResolvedImport> {
dynamic_imports
.iter()
.flat_map(|imp| resolve_single_dynamic_import(ctx, file_path, imp))
.collect()
}
pub(super) fn resolve_single_dynamic_import(
ctx: &ResolveContext,
file_path: &Path,
imp: &DynamicImportInfo,
) -> Vec<ResolvedImport> {
let target = resolve_specifier(ctx, file_path, &imp.source, false);
if imp.is_speculative
&& (target.is_bare_package() || matches!(target, ResolveResult::Unresolvable(_)))
{
if let Some(file_id) = resolve_root_manual_mock(ctx, file_path, &imp.source) {
return vec![dynamic_import_with(
imp,
ImportedName::Namespace,
imp.local_name.clone().unwrap_or_default(),
ResolveResult::SyntheticAutoImport(file_id),
)];
}
return Vec::new();
}
if !imp.destructured_names.is_empty() {
return resolve_destructured_dynamic_import(imp, &target);
}
if imp.local_name.is_some() {
return vec![dynamic_import_with(
imp,
ImportedName::Namespace,
imp.local_name.clone().unwrap_or_default(),
target,
)];
}
vec![dynamic_import_with(
imp,
ImportedName::SideEffect,
String::new(),
target,
)]
}
fn resolve_root_manual_mock(
ctx: &ResolveContext<'_>,
file_path: &Path,
source: &str,
) -> Option<FileId> {
let candidate = source.strip_prefix("__mocks__/")?;
if candidate.is_empty() {
return None;
}
let mut dir = file_path.parent();
while let Some(current) = dir {
let base = current.join("__mocks__").join(candidate);
for ext in ctx.extensions {
let mut with_ext = base.clone().into_os_string();
with_ext.push(ext);
if let Some(file_id) = project_file_id(ctx, Path::new(&with_ext)) {
return Some(file_id);
}
let index = base.join(format!("index{ext}"));
if let Some(file_id) = project_file_id(ctx, &index) {
return Some(file_id);
}
}
if current == ctx.root {
break;
}
dir = current.parent();
}
None
}
fn project_file_id(ctx: &ResolveContext<'_>, path: &Path) -> Option<FileId> {
ctx.raw_path_to_id
.get(path)
.or_else(|| ctx.path_to_id.get(path))
.copied()
}
fn resolve_destructured_dynamic_import(
imp: &DynamicImportInfo,
target: &ResolveResult,
) -> Vec<ResolvedImport> {
imp.destructured_names
.iter()
.map(|name| {
let imported_name = if name == "default" {
ImportedName::Default
} else {
ImportedName::Named(name.clone())
};
dynamic_import_with(imp, imported_name, name.clone(), target.clone())
})
.collect()
}
fn dynamic_import_with(
imp: &DynamicImportInfo,
imported_name: ImportedName,
local_name: String,
target: ResolveResult,
) -> ResolvedImport {
ResolvedImport {
info: ImportInfo {
source: imp.source.clone(),
imported_name,
local_name,
is_type_only: false,
is_type_only_star: false,
from_style: false,
span: imp.span,
source_span: Span::default(),
},
target,
}
}
#[derive(Default)]
pub(super) struct GlobMatcherCache {
map: Mutex<FxHashMap<String, Option<globset::GlobMatcher>>>,
}
impl GlobMatcherCache {
fn get(&self, glob_str: &str) -> Option<globset::GlobMatcher> {
if let Ok(cache) = self.map.lock()
&& let Some(value) = cache.get(glob_str)
{
return value.clone();
}
let value = globset::Glob::new(glob_str)
.ok()
.map(|g| g.compile_matcher());
if let Ok(mut cache) = self.map.lock() {
cache.insert(glob_str.to_string(), value.clone());
}
value
}
}
pub(super) fn resolve_dynamic_patterns(
glob_cache: &GlobMatcherCache,
from_dir: &Path,
patterns: &[DynamicImportPattern],
canonical_paths: &[PathBuf],
files: &[DiscoveredFile],
) -> Vec<(DynamicImportPattern, Vec<FileId>)> {
patterns
.iter()
.filter_map(|pattern| {
let glob_str = make_glob_from_pattern(pattern);
let matcher = glob_cache.get(glob_str.strip_prefix("./").unwrap_or(&glob_str))?;
let matched: Vec<FileId> = if canonical_paths.is_empty() {
files
.iter()
.filter(|f| {
f.path
.strip_prefix(from_dir)
.is_ok_and(|relative| matcher.is_match(relative))
})
.map(|f| f.id)
.collect()
} else {
canonical_paths
.iter()
.enumerate()
.filter(|(_idx, canonical)| {
canonical
.strip_prefix(from_dir)
.is_ok_and(|relative| matcher.is_match(relative))
})
.map(|(idx, _)| files[idx].id)
.collect()
};
if matched.is_empty() {
None
} else {
Some((pattern.clone(), matched))
}
})
.collect()
}