use std::path::{Path, PathBuf};
use rustc_hash::FxHashMap;
use fallow_types::discover::FileId;
use super::types::{OUTPUT_DIRS, ResolveContext, ResolveResult, SOURCE_EXTS};
pub(super) fn try_path_alias_fallback(
ctx: &ResolveContext<'_>,
specifier: &str,
) -> Option<ResolveResult> {
for (prefix, replacement) in ctx.path_aliases {
if !specifier.starts_with(prefix.as_str()) {
continue;
}
let remainder = &specifier[prefix.len()..];
let substituted = if replacement.is_empty() {
format!("./{remainder}")
} else {
format!("./{replacement}/{remainder}")
};
let root_file = ctx.root.join("__resolve_root__");
if let Ok(resolved) = ctx.resolver.resolve_file(&root_file, &substituted) {
let resolved_path = resolved.path();
if let Some(&file_id) = ctx.raw_path_to_id.get(resolved_path) {
return Some(ResolveResult::InternalModule(file_id));
}
if let Ok(canonical) = dunce::canonicalize(resolved_path) {
if let Some(&file_id) = ctx.path_to_id.get(canonical.as_path()) {
return Some(ResolveResult::InternalModule(file_id));
}
if let Some(file_id) = try_source_fallback(&canonical, ctx.path_to_id) {
return Some(ResolveResult::InternalModule(file_id));
}
if let Some(file_id) =
try_pnpm_workspace_fallback(&canonical, ctx.path_to_id, ctx.workspace_roots)
{
return Some(ResolveResult::InternalModule(file_id));
}
if let Some(pkg_name) = extract_package_name_from_node_modules_path(&canonical) {
return Some(ResolveResult::NpmPackage(pkg_name));
}
return Some(ResolveResult::ExternalFile(canonical));
}
}
}
None
}
pub(super) fn try_scss_partial_fallback(
ctx: &ResolveContext<'_>,
from_file: &Path,
specifier: &str,
) -> Option<ResolveResult> {
if specifier.contains(':') {
return None;
}
let spec_path = Path::new(specifier);
let filename = spec_path.file_name()?.to_str()?;
if filename.starts_with('_') {
return None;
}
let partial_filename = format!("_{filename}");
let partial_specifier = if let Some(parent) = spec_path.parent()
&& !parent.as_os_str().is_empty()
{
format!("{}/{partial_filename}", parent.display())
} else {
partial_filename
};
if let Some(result) = try_resolve_scss(ctx, from_file, &partial_specifier) {
return Some(result);
}
let index_partial = format!("{specifier}/_index");
if let Some(result) = try_resolve_scss(ctx, from_file, &index_partial) {
return Some(result);
}
let index_plain = format!("{specifier}/index");
try_resolve_scss(ctx, from_file, &index_plain)
}
fn try_resolve_scss(
ctx: &ResolveContext<'_>,
from_file: &Path,
specifier: &str,
) -> Option<ResolveResult> {
let resolved = ctx.resolver.resolve_file(from_file, specifier).ok()?;
let resolved_path = resolved.path();
if let Some(&file_id) = ctx.raw_path_to_id.get(resolved_path) {
return Some(ResolveResult::InternalModule(file_id));
}
if let Ok(canonical) = dunce::canonicalize(resolved_path)
&& let Some(&file_id) = ctx.path_to_id.get(canonical.as_path())
{
return Some(ResolveResult::InternalModule(file_id));
}
None
}
pub(super) fn try_scss_include_path_fallback(
ctx: &ResolveContext<'_>,
from_file: &Path,
specifier: &str,
) -> Option<ResolveResult> {
if ctx.scss_include_paths.is_empty() {
return None;
}
if !from_file
.extension()
.is_some_and(|e| e == "scss" || e == "sass")
{
return None;
}
if specifier.contains(':') {
return None;
}
let bare = specifier.strip_prefix("./")?;
if bare.starts_with("..") || bare.starts_with('/') {
return None;
}
for include_dir in ctx.scss_include_paths {
if let Some(file_id) = find_scss_in_dir(include_dir, bare, ctx) {
return Some(ResolveResult::InternalModule(file_id));
}
}
None
}
fn find_scss_in_dir(include_dir: &Path, bare: &str, ctx: &ResolveContext<'_>) -> Option<FileId> {
let bare_path = Path::new(bare);
let has_scss_ext = matches!(
bare_path.extension().and_then(|e| e.to_str()),
Some(ext) if ext.eq_ignore_ascii_case("scss") || ext.eq_ignore_ascii_case("sass")
);
let parent = bare_path.parent();
let stem_with_ext = bare_path.file_name()?.to_str()?;
let stem_without_ext = bare_path.file_stem().and_then(|s| s.to_str())?;
let build = |rel: &Path| -> std::path::PathBuf { include_dir.join(rel) };
let join_with_parent = |name: &str| -> std::path::PathBuf {
parent.map_or_else(|| build(Path::new(name)), |p| build(&p.join(name)))
};
let exts: &[&str] = if has_scss_ext {
&[""]
} else {
&["scss", "sass"]
};
for ext in exts {
let suffix = if ext.is_empty() {
String::new()
} else {
format!(".{ext}")
};
let direct = if ext.is_empty() {
build(bare_path)
} else {
join_with_parent(&format!("{stem_with_ext}{suffix}"))
};
if let Some(fid) = lookup_scss_path(&direct, ctx) {
return Some(fid);
}
let partial_name = if ext.is_empty() {
format!("_{stem_with_ext}")
} else {
format!("_{stem_without_ext}{suffix}")
};
let partial = join_with_parent(&partial_name);
if let Some(fid) = lookup_scss_path(&partial, ctx) {
return Some(fid);
}
if ext.is_empty() {
continue;
}
let idx_partial = build(bare_path).join(format!("_index{suffix}"));
if let Some(fid) = lookup_scss_path(&idx_partial, ctx) {
return Some(fid);
}
let idx_plain = build(bare_path).join(format!("index{suffix}"));
if let Some(fid) = lookup_scss_path(&idx_plain, ctx) {
return Some(fid);
}
}
None
}
fn lookup_scss_path(candidate: &Path, ctx: &ResolveContext<'_>) -> Option<FileId> {
if let Some(&file_id) = ctx.raw_path_to_id.get(candidate) {
return Some(file_id);
}
if let Ok(canonical) = dunce::canonicalize(candidate) {
if let Some(&file_id) = ctx.path_to_id.get(canonical.as_path()) {
return Some(file_id);
}
if let Some(fallback) = ctx.canonical_fallback
&& let Some(file_id) = fallback.get(&canonical)
{
return Some(file_id);
}
}
None
}
pub(super) fn try_source_fallback(
resolved: &Path,
path_to_id: &FxHashMap<&Path, FileId>,
) -> Option<FileId> {
let components: Vec<_> = resolved.components().collect();
let is_output_dir = |c: &std::path::Component| -> bool {
if let std::path::Component::Normal(s) = c
&& let Some(name) = s.to_str()
{
return OUTPUT_DIRS.contains(&name);
}
false
};
let last_output_pos = components.iter().rposition(&is_output_dir)?;
let mut first_output_pos = last_output_pos;
while first_output_pos > 0 && is_output_dir(&components[first_output_pos - 1]) {
first_output_pos -= 1;
}
let prefix: PathBuf = components[..first_output_pos].iter().collect();
let suffix: PathBuf = components[last_output_pos + 1..].iter().collect();
suffix.file_stem()?;
for ext in SOURCE_EXTS {
let source_candidate = prefix.join("src").join(suffix.with_extension(ext));
if let Some(&file_id) = path_to_id.get(source_candidate.as_path()) {
return Some(file_id);
}
}
None
}
pub(super) fn extract_package_name_from_node_modules_path(path: &Path) -> Option<String> {
let components: Vec<&str> = path
.components()
.filter_map(|c| match c {
std::path::Component::Normal(s) => s.to_str(),
_ => None,
})
.collect();
let nm_idx = components.iter().rposition(|&c| c == "node_modules")?;
let after = &components[nm_idx + 1..];
if after.is_empty() {
return None;
}
if after[0].starts_with('@') {
if after.len() >= 2 {
Some(format!("{}/{}", after[0], after[1]))
} else {
Some(after[0].to_string())
}
} else {
Some(after[0].to_string())
}
}
pub(super) fn try_pnpm_workspace_fallback(
path: &Path,
path_to_id: &FxHashMap<&Path, FileId>,
workspace_roots: &FxHashMap<&str, &Path>,
) -> Option<FileId> {
let components: Vec<&str> = path
.components()
.filter_map(|c| match c {
std::path::Component::Normal(s) => s.to_str(),
_ => None,
})
.collect();
let pnpm_idx = components.iter().position(|&c| c == ".pnpm")?;
let after_pnpm = &components[pnpm_idx + 1..];
let inner_nm_idx = after_pnpm.iter().position(|&c| c == "node_modules")?;
let after_inner_nm = &after_pnpm[inner_nm_idx + 1..];
if after_inner_nm.is_empty() {
return None;
}
let (pkg_name, pkg_name_components) = if after_inner_nm[0].starts_with('@') {
if after_inner_nm.len() >= 2 {
(format!("{}/{}", after_inner_nm[0], after_inner_nm[1]), 2)
} else {
return None;
}
} else {
(after_inner_nm[0].to_string(), 1)
};
let ws_root = workspace_roots.get(pkg_name.as_str())?;
let relative_parts = &after_inner_nm[pkg_name_components..];
if relative_parts.is_empty() {
return None;
}
let relative_path: PathBuf = relative_parts.iter().collect();
let direct = ws_root.join(&relative_path);
if let Some(&file_id) = path_to_id.get(direct.as_path()) {
return Some(file_id);
}
try_source_fallback(&direct, path_to_id)
}
pub(super) fn try_workspace_package_fallback(
ctx: &ResolveContext<'_>,
specifier: &str,
) -> Option<ResolveResult> {
if !super::path_info::is_bare_specifier(specifier) {
return None;
}
let pkg_name = super::path_info::extract_package_name(specifier);
let ws_root = *ctx.workspace_roots.get(pkg_name.as_str())?;
let subpath = specifier
.strip_prefix(pkg_name.as_str())
.and_then(|s| s.strip_prefix('/'))
.unwrap_or("");
let root_file = ws_root.join("__fallow_ws_self_resolve__");
let rel_spec = if subpath.is_empty() {
"./".to_string()
} else {
format!("./{subpath}")
};
let resolved = ctx.resolver.resolve_file(&root_file, &rel_spec).ok()?;
let resolved_path = resolved.path();
if let Some(&file_id) = ctx.raw_path_to_id.get(resolved_path) {
return Some(ResolveResult::InternalModule(file_id));
}
if let Ok(canonical) = dunce::canonicalize(resolved_path) {
if let Some(&file_id) = ctx.path_to_id.get(canonical.as_path()) {
return Some(ResolveResult::InternalModule(file_id));
}
if let Some(fallback) = ctx.canonical_fallback
&& let Some(file_id) = fallback.get(&canonical)
{
return Some(ResolveResult::InternalModule(file_id));
}
if let Some(file_id) = try_source_fallback(&canonical, ctx.path_to_id) {
return Some(ResolveResult::InternalModule(file_id));
}
}
None
}
pub(super) fn make_glob_from_pattern(
pattern: &fallow_types::extract::DynamicImportPattern,
) -> String {
if pattern.prefix.contains('*') || pattern.prefix.contains('{') {
return pattern.prefix.clone();
}
pattern.suffix.as_ref().map_or_else(
|| format!("{}*", pattern.prefix),
|suffix| format!("{}*{}", pattern.prefix, suffix),
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_package_name_from_node_modules_path_regular() {
let path = PathBuf::from("/project/node_modules/react/index.js");
assert_eq!(
extract_package_name_from_node_modules_path(&path),
Some("react".to_string())
);
}
#[test]
fn test_extract_package_name_from_node_modules_path_scoped() {
let path = PathBuf::from("/project/node_modules/@babel/core/lib/index.js");
assert_eq!(
extract_package_name_from_node_modules_path(&path),
Some("@babel/core".to_string())
);
}
#[test]
fn test_extract_package_name_from_node_modules_path_nested() {
let path = PathBuf::from("/project/node_modules/pkg-a/node_modules/pkg-b/dist/index.js");
assert_eq!(
extract_package_name_from_node_modules_path(&path),
Some("pkg-b".to_string())
);
}
#[test]
fn test_extract_package_name_from_node_modules_path_deep_subpath() {
let path = PathBuf::from("/project/node_modules/react-dom/cjs/react-dom.production.min.js");
assert_eq!(
extract_package_name_from_node_modules_path(&path),
Some("react-dom".to_string())
);
}
#[test]
fn test_extract_package_name_from_node_modules_path_no_node_modules() {
let path = PathBuf::from("/project/src/components/Button.tsx");
assert_eq!(extract_package_name_from_node_modules_path(&path), None);
}
#[test]
fn test_extract_package_name_from_node_modules_path_just_node_modules() {
let path = PathBuf::from("/project/node_modules");
assert_eq!(extract_package_name_from_node_modules_path(&path), None);
}
#[test]
fn test_extract_package_name_from_node_modules_path_scoped_only_scope() {
let path = PathBuf::from("/project/node_modules/@scope");
assert_eq!(
extract_package_name_from_node_modules_path(&path),
Some("@scope".to_string())
);
}
#[test]
fn test_resolve_specifier_node_modules_returns_npm_package() {
let path =
PathBuf::from("/project/node_modules/styled-components/dist/styled-components.esm.js");
assert_eq!(
extract_package_name_from_node_modules_path(&path),
Some("styled-components".to_string())
);
let path = PathBuf::from("/project/node_modules/next/dist/server/next.js");
assert_eq!(
extract_package_name_from_node_modules_path(&path),
Some("next".to_string())
);
}
#[test]
fn test_try_source_fallback_dist_to_src() {
let src_path = PathBuf::from("/project/packages/ui/src/utils.ts");
let mut path_to_id = FxHashMap::default();
path_to_id.insert(src_path.as_path(), FileId(0));
let dist_path = PathBuf::from("/project/packages/ui/dist/utils.js");
assert_eq!(
try_source_fallback(&dist_path, &path_to_id),
Some(FileId(0)),
"dist/utils.js should fall back to src/utils.ts"
);
}
#[test]
fn test_try_source_fallback_build_to_src() {
let src_path = PathBuf::from("/project/packages/core/src/index.tsx");
let mut path_to_id = FxHashMap::default();
path_to_id.insert(src_path.as_path(), FileId(1));
let build_path = PathBuf::from("/project/packages/core/build/index.js");
assert_eq!(
try_source_fallback(&build_path, &path_to_id),
Some(FileId(1)),
"build/index.js should fall back to src/index.tsx"
);
}
#[test]
fn test_try_source_fallback_no_match() {
let path_to_id: FxHashMap<&Path, FileId> = FxHashMap::default();
let dist_path = PathBuf::from("/project/packages/ui/dist/utils.js");
assert_eq!(
try_source_fallback(&dist_path, &path_to_id),
None,
"should return None when no source file exists"
);
}
#[test]
fn test_try_source_fallback_non_output_dir() {
let src_path = PathBuf::from("/project/packages/ui/src/utils.ts");
let mut path_to_id = FxHashMap::default();
path_to_id.insert(src_path.as_path(), FileId(0));
let normal_path = PathBuf::from("/project/packages/ui/scripts/utils.js");
assert_eq!(
try_source_fallback(&normal_path, &path_to_id),
None,
"non-output directory path should not trigger fallback"
);
}
#[test]
fn test_try_source_fallback_nested_path() {
let src_path = PathBuf::from("/project/packages/ui/src/components/Button.ts");
let mut path_to_id = FxHashMap::default();
path_to_id.insert(src_path.as_path(), FileId(2));
let dist_path = PathBuf::from("/project/packages/ui/dist/components/Button.js");
assert_eq!(
try_source_fallback(&dist_path, &path_to_id),
Some(FileId(2)),
"nested dist path should fall back to nested src path"
);
}
#[test]
fn test_try_source_fallback_nested_dist_esm() {
let src_path = PathBuf::from("/project/packages/ui/src/utils.ts");
let mut path_to_id = FxHashMap::default();
path_to_id.insert(src_path.as_path(), FileId(0));
let dist_path = PathBuf::from("/project/packages/ui/dist/esm/utils.mjs");
assert_eq!(
try_source_fallback(&dist_path, &path_to_id),
Some(FileId(0)),
"dist/esm/utils.mjs should fall back to src/utils.ts"
);
}
#[test]
fn test_try_source_fallback_nested_build_cjs() {
let src_path = PathBuf::from("/project/packages/core/src/index.ts");
let mut path_to_id = FxHashMap::default();
path_to_id.insert(src_path.as_path(), FileId(1));
let build_path = PathBuf::from("/project/packages/core/build/cjs/index.cjs");
assert_eq!(
try_source_fallback(&build_path, &path_to_id),
Some(FileId(1)),
"build/cjs/index.cjs should fall back to src/index.ts"
);
}
#[test]
fn test_try_source_fallback_nested_dist_esm_deep_path() {
let src_path = PathBuf::from("/project/packages/ui/src/components/Button.ts");
let mut path_to_id = FxHashMap::default();
path_to_id.insert(src_path.as_path(), FileId(2));
let dist_path = PathBuf::from("/project/packages/ui/dist/esm/components/Button.mjs");
assert_eq!(
try_source_fallback(&dist_path, &path_to_id),
Some(FileId(2)),
"dist/esm/components/Button.mjs should fall back to src/components/Button.ts"
);
}
#[test]
fn test_try_source_fallback_triple_nested_output_dirs() {
let src_path = PathBuf::from("/project/packages/ui/src/utils.ts");
let mut path_to_id = FxHashMap::default();
path_to_id.insert(src_path.as_path(), FileId(0));
let dist_path = PathBuf::from("/project/packages/ui/out/dist/esm/utils.mjs");
assert_eq!(
try_source_fallback(&dist_path, &path_to_id),
Some(FileId(0)),
"out/dist/esm/utils.mjs should fall back to src/utils.ts"
);
}
#[test]
fn test_try_source_fallback_parent_dir_named_build() {
let src_path = PathBuf::from("/home/user/build/my-project/src/utils.ts");
let mut path_to_id = FxHashMap::default();
path_to_id.insert(src_path.as_path(), FileId(0));
let dist_path = PathBuf::from("/home/user/build/my-project/dist/utils.js");
assert_eq!(
try_source_fallback(&dist_path, &path_to_id),
Some(FileId(0)),
"should resolve dist/ within project, not match parent 'build' dir"
);
}
#[test]
fn test_pnpm_store_path_extract_package_name() {
let path =
PathBuf::from("/project/node_modules/.pnpm/react@18.2.0/node_modules/react/index.js");
assert_eq!(
extract_package_name_from_node_modules_path(&path),
Some("react".to_string())
);
}
#[test]
fn test_pnpm_store_path_scoped_package() {
let path = PathBuf::from(
"/project/node_modules/.pnpm/@babel+core@7.24.0/node_modules/@babel/core/lib/index.js",
);
assert_eq!(
extract_package_name_from_node_modules_path(&path),
Some("@babel/core".to_string())
);
}
#[test]
fn test_pnpm_store_path_with_peer_deps() {
let path = PathBuf::from(
"/project/node_modules/.pnpm/webpack@5.0.0_esbuild@0.19.0/node_modules/webpack/lib/index.js",
);
assert_eq!(
extract_package_name_from_node_modules_path(&path),
Some("webpack".to_string())
);
}
#[test]
fn test_try_pnpm_workspace_fallback_dist_to_src() {
let src_path = PathBuf::from("/project/packages/ui/src/utils.ts");
let mut path_to_id = FxHashMap::default();
path_to_id.insert(src_path.as_path(), FileId(0));
let mut workspace_roots = FxHashMap::default();
let ws_root = PathBuf::from("/project/packages/ui");
workspace_roots.insert("@myorg/ui", ws_root.as_path());
let pnpm_path = PathBuf::from(
"/project/node_modules/.pnpm/@myorg+ui@1.0.0/node_modules/@myorg/ui/dist/utils.js",
);
assert_eq!(
try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
Some(FileId(0)),
".pnpm workspace path should fall back to src/utils.ts"
);
}
#[test]
fn test_try_pnpm_workspace_fallback_direct_source() {
let src_path = PathBuf::from("/project/packages/core/src/index.ts");
let mut path_to_id = FxHashMap::default();
path_to_id.insert(src_path.as_path(), FileId(1));
let mut workspace_roots = FxHashMap::default();
let ws_root = PathBuf::from("/project/packages/core");
workspace_roots.insert("@myorg/core", ws_root.as_path());
let pnpm_path = PathBuf::from(
"/project/node_modules/.pnpm/@myorg+core@workspace/node_modules/@myorg/core/src/index.ts",
);
assert_eq!(
try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
Some(FileId(1)),
".pnpm workspace path with src/ should resolve directly"
);
}
#[test]
fn test_try_pnpm_workspace_fallback_non_workspace_package() {
let path_to_id: FxHashMap<&Path, FileId> = FxHashMap::default();
let mut workspace_roots = FxHashMap::default();
let ws_root = PathBuf::from("/project/packages/ui");
workspace_roots.insert("@myorg/ui", ws_root.as_path());
let pnpm_path =
PathBuf::from("/project/node_modules/.pnpm/react@18.2.0/node_modules/react/index.js");
assert_eq!(
try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
None,
"non-workspace package in .pnpm should return None"
);
}
#[test]
fn test_try_pnpm_workspace_fallback_unscoped_package() {
let src_path = PathBuf::from("/project/packages/utils/src/index.ts");
let mut path_to_id = FxHashMap::default();
path_to_id.insert(src_path.as_path(), FileId(2));
let mut workspace_roots = FxHashMap::default();
let ws_root = PathBuf::from("/project/packages/utils");
workspace_roots.insert("my-utils", ws_root.as_path());
let pnpm_path = PathBuf::from(
"/project/node_modules/.pnpm/my-utils@1.0.0/node_modules/my-utils/dist/index.js",
);
assert_eq!(
try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
Some(FileId(2)),
"unscoped workspace package in .pnpm should resolve"
);
}
#[test]
fn test_try_pnpm_workspace_fallback_nested_path() {
let src_path = PathBuf::from("/project/packages/ui/src/components/Button.ts");
let mut path_to_id = FxHashMap::default();
path_to_id.insert(src_path.as_path(), FileId(3));
let mut workspace_roots = FxHashMap::default();
let ws_root = PathBuf::from("/project/packages/ui");
workspace_roots.insert("@myorg/ui", ws_root.as_path());
let pnpm_path = PathBuf::from(
"/project/node_modules/.pnpm/@myorg+ui@1.0.0/node_modules/@myorg/ui/dist/components/Button.js",
);
assert_eq!(
try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
Some(FileId(3)),
"nested .pnpm workspace path should resolve through source fallback"
);
}
#[test]
fn test_try_pnpm_workspace_fallback_no_pnpm() {
let path_to_id: FxHashMap<&Path, FileId> = FxHashMap::default();
let workspace_roots: FxHashMap<&str, &Path> = FxHashMap::default();
let regular_path = PathBuf::from("/project/node_modules/react/index.js");
assert_eq!(
try_pnpm_workspace_fallback(®ular_path, &path_to_id, &workspace_roots),
None,
);
}
#[test]
fn test_try_pnpm_workspace_fallback_with_peer_deps() {
let src_path = PathBuf::from("/project/packages/ui/src/index.ts");
let mut path_to_id = FxHashMap::default();
path_to_id.insert(src_path.as_path(), FileId(4));
let mut workspace_roots = FxHashMap::default();
let ws_root = PathBuf::from("/project/packages/ui");
workspace_roots.insert("@myorg/ui", ws_root.as_path());
let pnpm_path = PathBuf::from(
"/project/node_modules/.pnpm/@myorg+ui@1.0.0_react@18.2.0/node_modules/@myorg/ui/dist/index.js",
);
assert_eq!(
try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
Some(FileId(4)),
".pnpm path with peer dep suffix should still resolve"
);
}
#[test]
fn make_glob_prefix_only_no_suffix() {
let pattern = fallow_types::extract::DynamicImportPattern {
prefix: "./locales/".to_string(),
suffix: None,
span: oxc_span::Span::default(),
};
assert_eq!(make_glob_from_pattern(&pattern), "./locales/*");
}
#[test]
fn make_glob_prefix_with_suffix() {
let pattern = fallow_types::extract::DynamicImportPattern {
prefix: "./locales/".to_string(),
suffix: Some(".json".to_string()),
span: oxc_span::Span::default(),
};
assert_eq!(make_glob_from_pattern(&pattern), "./locales/*.json");
}
#[test]
fn make_glob_passthrough_star() {
let pattern = fallow_types::extract::DynamicImportPattern {
prefix: "./pages/**/*.tsx".to_string(),
suffix: None,
span: oxc_span::Span::default(),
};
assert_eq!(make_glob_from_pattern(&pattern), "./pages/**/*.tsx");
}
#[test]
fn make_glob_passthrough_brace() {
let pattern = fallow_types::extract::DynamicImportPattern {
prefix: "./i18n/{en,de,fr}.json".to_string(),
suffix: None,
span: oxc_span::Span::default(),
};
assert_eq!(make_glob_from_pattern(&pattern), "./i18n/{en,de,fr}.json");
}
#[test]
fn make_glob_empty_prefix_no_suffix() {
let pattern = fallow_types::extract::DynamicImportPattern {
prefix: String::new(),
suffix: None,
span: oxc_span::Span::default(),
};
assert_eq!(make_glob_from_pattern(&pattern), "*");
}
#[test]
fn make_glob_empty_prefix_with_suffix() {
let pattern = fallow_types::extract::DynamicImportPattern {
prefix: String::new(),
suffix: Some(".ts".to_string()),
span: oxc_span::Span::default(),
};
assert_eq!(make_glob_from_pattern(&pattern), "*.ts");
}
#[test]
fn make_glob_template_literal_prefix_only() {
let pattern = fallow_types::extract::DynamicImportPattern {
prefix: "./pages/".to_string(),
suffix: None,
span: oxc_span::Span::default(),
};
assert_eq!(make_glob_from_pattern(&pattern), "./pages/*");
}
#[test]
fn make_glob_template_literal_with_extension_suffix() {
let pattern = fallow_types::extract::DynamicImportPattern {
prefix: "./locales/".to_string(),
suffix: Some(".json".to_string()),
span: oxc_span::Span::default(),
};
assert_eq!(make_glob_from_pattern(&pattern), "./locales/*.json");
}
#[test]
fn make_glob_template_literal_deep_prefix() {
let pattern = fallow_types::extract::DynamicImportPattern {
prefix: "./modules/".to_string(),
suffix: None,
span: oxc_span::Span::default(),
};
assert_eq!(make_glob_from_pattern(&pattern), "./modules/*");
}
#[test]
fn make_glob_string_concat_prefix() {
let pattern = fallow_types::extract::DynamicImportPattern {
prefix: "./pages/".to_string(),
suffix: None,
span: oxc_span::Span::default(),
};
assert_eq!(make_glob_from_pattern(&pattern), "./pages/*");
}
#[test]
fn make_glob_string_concat_with_extension() {
let pattern = fallow_types::extract::DynamicImportPattern {
prefix: "./views/".to_string(),
suffix: Some(".vue".to_string()),
span: oxc_span::Span::default(),
};
assert_eq!(make_glob_from_pattern(&pattern), "./views/*.vue");
}
#[test]
fn make_glob_import_meta_glob_recursive() {
let pattern = fallow_types::extract::DynamicImportPattern {
prefix: "./components/**/*.vue".to_string(),
suffix: None,
span: oxc_span::Span::default(),
};
assert_eq!(
make_glob_from_pattern(&pattern),
"./components/**/*.vue",
"import.meta.glob patterns with * should pass through as-is"
);
}
#[test]
fn make_glob_import_meta_glob_brace_expansion() {
let pattern = fallow_types::extract::DynamicImportPattern {
prefix: "./plugins/{auth,analytics}.ts".to_string(),
suffix: None,
span: oxc_span::Span::default(),
};
assert_eq!(
make_glob_from_pattern(&pattern),
"./plugins/{auth,analytics}.ts",
"import.meta.glob patterns with braces should pass through as-is"
);
}
#[test]
fn make_glob_import_meta_glob_star_with_brace() {
let pattern = fallow_types::extract::DynamicImportPattern {
prefix: "./routes/**/*.{ts,tsx}".to_string(),
suffix: None,
span: oxc_span::Span::default(),
};
assert_eq!(
make_glob_from_pattern(&pattern),
"./routes/**/*.{ts,tsx}",
"combined * and brace patterns should pass through"
);
}
#[test]
fn make_glob_import_meta_glob_ignores_suffix_when_star_present() {
let pattern = fallow_types::extract::DynamicImportPattern {
prefix: "./*.ts".to_string(),
suffix: Some(".extra".to_string()),
span: oxc_span::Span::default(),
};
assert_eq!(
make_glob_from_pattern(&pattern),
"./*.ts",
"when prefix has glob chars, suffix is ignored (prefix used as-is)"
);
}
#[test]
fn make_glob_single_dot_prefix() {
let pattern = fallow_types::extract::DynamicImportPattern {
prefix: "./".to_string(),
suffix: None,
span: oxc_span::Span::default(),
};
assert_eq!(make_glob_from_pattern(&pattern), "./*");
}
#[test]
fn make_glob_prefix_without_trailing_slash() {
let pattern = fallow_types::extract::DynamicImportPattern {
prefix: "./config".to_string(),
suffix: None,
span: oxc_span::Span::default(),
};
assert_eq!(make_glob_from_pattern(&pattern), "./config*");
}
#[test]
fn make_glob_prefix_with_dotdot() {
let pattern = fallow_types::extract::DynamicImportPattern {
prefix: "../shared/".to_string(),
suffix: Some(".ts".to_string()),
span: oxc_span::Span::default(),
};
assert_eq!(make_glob_from_pattern(&pattern), "../shared/*.ts");
}
#[test]
fn test_extract_package_name_with_pnpm_plus_encoded_scope() {
let path = PathBuf::from(
"/project/node_modules/.pnpm/@mui+material@5.15.0/node_modules/@mui/material/index.js",
);
assert_eq!(
extract_package_name_from_node_modules_path(&path),
Some("@mui/material".to_string())
);
}
#[test]
fn test_extract_package_name_windows_style_path() {
let path = PathBuf::from("/project/node_modules/typescript/lib/tsc.js");
assert_eq!(
extract_package_name_from_node_modules_path(&path),
Some("typescript".to_string())
);
}
#[test]
fn test_try_source_fallback_out_dir() {
let src_path = PathBuf::from("/project/packages/api/src/handler.ts");
let mut path_to_id = FxHashMap::default();
path_to_id.insert(src_path.as_path(), FileId(5));
let out_path = PathBuf::from("/project/packages/api/out/handler.js");
assert_eq!(
try_source_fallback(&out_path, &path_to_id),
Some(FileId(5)),
"out/handler.js should fall back to src/handler.ts"
);
}
#[test]
fn test_try_source_fallback_mts_extension() {
let src_path = PathBuf::from("/project/packages/lib/src/utils.mts");
let mut path_to_id = FxHashMap::default();
path_to_id.insert(src_path.as_path(), FileId(6));
let dist_path = PathBuf::from("/project/packages/lib/dist/utils.mjs");
assert_eq!(
try_source_fallback(&dist_path, &path_to_id),
Some(FileId(6)),
"dist/utils.mjs should fall back to src/utils.mts"
);
}
#[test]
fn test_try_source_fallback_cts_extension() {
let src_path = PathBuf::from("/project/packages/lib/src/config.cts");
let mut path_to_id = FxHashMap::default();
path_to_id.insert(src_path.as_path(), FileId(7));
let dist_path = PathBuf::from("/project/packages/lib/dist/config.cjs");
assert_eq!(
try_source_fallback(&dist_path, &path_to_id),
Some(FileId(7)),
"dist/config.cjs should fall back to src/config.cts"
);
}
#[test]
fn test_try_source_fallback_jsx_extension() {
let src_path = PathBuf::from("/project/packages/ui/src/App.jsx");
let mut path_to_id = FxHashMap::default();
path_to_id.insert(src_path.as_path(), FileId(8));
let build_path = PathBuf::from("/project/packages/ui/build/App.js");
assert_eq!(
try_source_fallback(&build_path, &path_to_id),
Some(FileId(8)),
"build/App.js should fall back to src/App.jsx"
);
}
#[test]
fn test_try_source_fallback_no_file_stem() {
let path_to_id: FxHashMap<&Path, FileId> = FxHashMap::default();
let dist_path = PathBuf::from("/project/packages/ui/dist/");
assert_eq!(
try_source_fallback(&dist_path, &path_to_id),
None,
"directory path with no file should return None"
);
}
#[test]
fn test_try_source_fallback_esm_subdir() {
let src_path = PathBuf::from("/project/lib/src/index.ts");
let mut path_to_id = FxHashMap::default();
path_to_id.insert(src_path.as_path(), FileId(10));
let dist_path = PathBuf::from("/project/lib/esm/index.mjs");
assert_eq!(
try_source_fallback(&dist_path, &path_to_id),
Some(FileId(10)),
"standalone esm/ directory should fall back to src/"
);
}
#[test]
fn test_try_source_fallback_cjs_subdir() {
let src_path = PathBuf::from("/project/lib/src/index.ts");
let mut path_to_id = FxHashMap::default();
path_to_id.insert(src_path.as_path(), FileId(11));
let cjs_path = PathBuf::from("/project/lib/cjs/index.cjs");
assert_eq!(
try_source_fallback(&cjs_path, &path_to_id),
Some(FileId(11)),
"standalone cjs/ directory should fall back to src/"
);
}
#[test]
fn test_try_pnpm_workspace_fallback_empty_after_pnpm() {
let path_to_id: FxHashMap<&Path, FileId> = FxHashMap::default();
let workspace_roots: FxHashMap<&str, &Path> = FxHashMap::default();
let pnpm_path = PathBuf::from("/project/node_modules/.pnpm/pkg@1.0.0/node_modules");
assert_eq!(
try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
None,
"path ending at node_modules with nothing after should return None"
);
}
#[test]
fn test_try_pnpm_workspace_fallback_scoped_package_only_scope() {
let path_to_id: FxHashMap<&Path, FileId> = FxHashMap::default();
let workspace_roots: FxHashMap<&str, &Path> = FxHashMap::default();
let pnpm_path =
PathBuf::from("/project/node_modules/.pnpm/@scope+pkg@1.0.0/node_modules/@scope");
assert_eq!(
try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
None,
"scoped package without full name and no matching workspace should return None"
);
}
#[test]
fn test_try_pnpm_workspace_fallback_no_inner_node_modules() {
let path_to_id: FxHashMap<&Path, FileId> = FxHashMap::default();
let workspace_roots: FxHashMap<&str, &Path> = FxHashMap::default();
let pnpm_path = PathBuf::from("/project/node_modules/.pnpm/pkg@1.0.0/dist/index.js");
assert_eq!(
try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
None,
"path without inner node_modules after .pnpm should return None"
);
}
#[test]
fn test_try_pnpm_workspace_fallback_package_without_relative_path() {
let path_to_id: FxHashMap<&Path, FileId> = FxHashMap::default();
let mut workspace_roots = FxHashMap::default();
let ws_root = PathBuf::from("/project/packages/ui");
workspace_roots.insert("@myorg/ui", ws_root.as_path());
let pnpm_path =
PathBuf::from("/project/node_modules/.pnpm/@myorg+ui@1.0.0/node_modules/@myorg/ui");
assert_eq!(
try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
None,
"path ending at package name with no relative file should return None"
);
}
#[test]
fn test_try_pnpm_workspace_fallback_nested_dist_esm() {
let src_path = PathBuf::from("/project/packages/ui/src/Button.ts");
let mut path_to_id = FxHashMap::default();
path_to_id.insert(src_path.as_path(), FileId(10));
let mut workspace_roots = FxHashMap::default();
let ws_root = PathBuf::from("/project/packages/ui");
workspace_roots.insert("@myorg/ui", ws_root.as_path());
let pnpm_path = PathBuf::from(
"/project/node_modules/.pnpm/@myorg+ui@1.0.0/node_modules/@myorg/ui/dist/esm/Button.mjs",
);
assert_eq!(
try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
Some(FileId(10)),
"pnpm path with nested dist/esm should resolve through source fallback"
);
}
}