use std::path::{Path, PathBuf};
use oxc_resolver::{Resolution, ResolveError, ResolveOptions, Resolver};
use super::fallbacks::{
extract_package_name_from_node_modules_path, try_path_alias_fallback,
try_pnpm_workspace_fallback, try_scss_include_path_fallback, try_scss_partial_fallback,
try_source_fallback, try_workspace_package_fallback,
};
use super::path_info::{
extract_package_name, is_bare_specifier, is_path_alias, is_valid_package_name,
};
use super::react_native::{build_condition_names, build_extensions};
use super::types::{ResolveContext, ResolveResult};
pub(super) fn create_resolver(active_plugins: &[String]) -> Resolver {
let mut options = ResolveOptions {
extensions: build_extensions(active_plugins),
extension_alias: vec![
(
".js".into(),
vec![".ts".into(), ".tsx".into(), ".js".into()],
),
(".jsx".into(), vec![".tsx".into(), ".jsx".into()]),
(".mjs".into(), vec![".mts".into(), ".mjs".into()]),
(".cjs".into(), vec![".cts".into(), ".cjs".into()]),
],
condition_names: build_condition_names(active_plugins),
main_fields: vec!["module".into(), "main".into()],
..Default::default()
};
options.tsconfig = Some(oxc_resolver::TsconfigDiscovery::Auto);
Resolver::new(options)
}
const fn is_tsconfig_error(err: &ResolveError) -> bool {
matches!(
err,
ResolveError::TsconfigNotFound(_)
| ResolveError::TsconfigCircularExtend(_)
| ResolveError::TsconfigSelfReference(_)
| ResolveError::Json(_)
| ResolveError::IOError(_)
)
}
fn resolve_file_with_tsconfig_fallback(
ctx: &ResolveContext<'_>,
from_file: &Path,
specifier: &str,
) -> Result<Resolution, ResolveError> {
match ctx.resolver.resolve_file(from_file, specifier) {
Ok(resolution) => Ok(resolution),
Err(err) if is_tsconfig_error(&err) => {
warn_once_tsconfig(ctx, &err);
let dir = from_file.parent().unwrap_or(from_file);
ctx.resolver.resolve(dir, specifier)
}
Err(err) => Err(err),
}
}
fn warn_once_tsconfig(ctx: &ResolveContext<'_>, err: &ResolveError) {
let message = err.to_string();
let should_warn = {
let Ok(mut seen) = ctx.tsconfig_warned.lock() else {
return;
};
seen.insert(message.clone())
};
if should_warn {
tracing::warn!(
"Broken tsconfig chain: {message}. Falling back to resolver-less resolution for \
affected files. Relative and bare imports still work, but tsconfig path aliases \
(e.g., `@/...`) will not. Fix the extends/references chain to restore alias support."
);
}
}
pub(super) fn resolve_specifier(
ctx: &ResolveContext<'_>,
from_file: &Path,
specifier: &str,
) -> ResolveResult {
if specifier.contains("://") || specifier.starts_with("data:") {
return ResolveResult::ExternalFile(PathBuf::from(specifier));
}
if specifier.starts_with('/')
&& from_file.extension().is_some_and(|e| {
matches!(
e.to_str(),
Some("html" | "jsx" | "tsx" | "js" | "ts" | "mjs" | "cjs" | "mts" | "cts")
)
})
{
let relative = format!(".{specifier}");
let source_dir = from_file.parent().unwrap_or(ctx.root);
if let Ok(resolved) = ctx.resolver.resolve(source_dir, &relative) {
let resolved_path = resolved.path();
if let Some(&file_id) = ctx.raw_path_to_id.get(resolved_path) {
return 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 ResolveResult::InternalModule(file_id);
}
if let Some(fallback) = ctx.canonical_fallback
&& let Some(file_id) = fallback.get(&canonical)
{
return ResolveResult::InternalModule(file_id);
}
}
}
if source_dir != ctx.root
&& let Ok(resolved) = ctx.resolver.resolve(ctx.root, &relative)
{
let resolved_path = resolved.path();
if let Some(&file_id) = ctx.raw_path_to_id.get(resolved_path) {
return 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 ResolveResult::InternalModule(file_id);
}
if let Some(fallback) = ctx.canonical_fallback
&& let Some(file_id) = fallback.get(&canonical)
{
return ResolveResult::InternalModule(file_id);
}
}
}
return ResolveResult::Unresolvable(specifier.to_string());
}
let is_bare = is_bare_specifier(specifier);
let is_alias = is_path_alias(specifier);
let matches_plugin_alias = ctx
.path_aliases
.iter()
.any(|(prefix, _)| specifier.starts_with(prefix));
match resolve_file_with_tsconfig_fallback(ctx, from_file, specifier) {
Ok(resolved) => {
let resolved_path = resolved.path();
if let Some(&file_id) = ctx.raw_path_to_id.get(resolved_path) {
return ResolveResult::InternalModule(file_id);
}
if is_bare
&& !resolved_path
.as_os_str()
.as_encoded_bytes()
.windows(7)
.any(|w| w == b"/.pnpm/" || w == b"\\.pnpm\\")
&& let Some(pkg_name) = extract_package_name_from_node_modules_path(resolved_path)
&& !ctx.workspace_roots.contains_key(pkg_name.as_str())
{
return ResolveResult::NpmPackage(pkg_name);
}
match dunce::canonicalize(resolved_path) {
Ok(canonical) => {
if let Some(&file_id) = ctx.path_to_id.get(canonical.as_path()) {
ResolveResult::InternalModule(file_id)
} else if let Some(fallback) = ctx.canonical_fallback
&& let Some(file_id) = fallback.get(&canonical)
{
ResolveResult::InternalModule(file_id)
} else if let Some(file_id) = try_source_fallback(&canonical, ctx.path_to_id) {
ResolveResult::InternalModule(file_id)
} else if let Some(file_id) =
try_pnpm_workspace_fallback(&canonical, ctx.path_to_id, ctx.workspace_roots)
{
ResolveResult::InternalModule(file_id)
} else if let Some(pkg_name) =
extract_package_name_from_node_modules_path(&canonical)
{
if ctx.workspace_roots.contains_key(pkg_name.as_str())
&& let Some(result) = try_workspace_package_fallback(ctx, specifier)
{
return result;
}
ResolveResult::NpmPackage(pkg_name)
} else {
ResolveResult::ExternalFile(canonical)
}
}
Err(_) => {
if let Some(file_id) = try_source_fallback(resolved_path, ctx.path_to_id) {
ResolveResult::InternalModule(file_id)
} else if let Some(file_id) = try_pnpm_workspace_fallback(
resolved_path,
ctx.path_to_id,
ctx.workspace_roots,
) {
ResolveResult::InternalModule(file_id)
} else if let Some(pkg_name) =
extract_package_name_from_node_modules_path(resolved_path)
{
if ctx.workspace_roots.contains_key(pkg_name.as_str())
&& let Some(result) = try_workspace_package_fallback(ctx, specifier)
{
return result;
}
ResolveResult::NpmPackage(pkg_name)
} else {
ResolveResult::ExternalFile(resolved_path.to_path_buf())
}
}
}
}
Err(_) => {
if from_file
.extension()
.is_some_and(|e| e == "scss" || e == "sass")
{
if let Some(result) = try_scss_partial_fallback(ctx, from_file, specifier) {
return result;
}
if let Some(result) = try_scss_include_path_fallback(ctx, from_file, specifier) {
return result;
}
}
if is_alias || matches_plugin_alias {
try_path_alias_fallback(ctx, specifier)
.unwrap_or_else(|| ResolveResult::Unresolvable(specifier.to_string()))
} else if is_bare && is_valid_package_name(specifier) {
if let Some(result) = try_workspace_package_fallback(ctx, specifier) {
return result;
}
let pkg_name = extract_package_name(specifier);
ResolveResult::NpmPackage(pkg_name)
} else {
ResolveResult::Unresolvable(specifier.to_string())
}
}
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use oxc_resolver::{JSONError, ResolveError};
use super::is_tsconfig_error;
#[test]
fn tsconfig_not_found_is_tsconfig_error() {
assert!(is_tsconfig_error(&ResolveError::TsconfigNotFound(
PathBuf::from("/nonexistent/tsconfig.json")
)));
}
#[test]
fn tsconfig_self_reference_is_tsconfig_error() {
assert!(is_tsconfig_error(&ResolveError::TsconfigSelfReference(
PathBuf::from("/project/tsconfig.json")
)));
}
#[test]
fn io_error_is_tsconfig_error() {
let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied");
assert!(is_tsconfig_error(&ResolveError::from(io_err)));
}
#[test]
fn json_error_is_tsconfig_error() {
assert!(is_tsconfig_error(&ResolveError::Json(JSONError {
path: PathBuf::from("/project/tsconfig.json"),
message: "unexpected token".to_string(),
line: 1,
column: 1,
})));
}
#[test]
fn module_not_found_is_not_tsconfig_error() {
assert!(!is_tsconfig_error(&ResolveError::NotFound(
"./missing-module".to_string()
)));
}
#[test]
fn ignored_is_not_tsconfig_error() {
assert!(!is_tsconfig_error(&ResolveError::Ignored(PathBuf::from(
"/ignored"
))));
}
}