use crate::config::{JsxEmit, ModuleResolutionKind, PathMapping, ResolvedCompilerOptions};
use crate::diagnostics::{Diagnostic, DiagnosticBag};
use crate::emitter::ModuleKind;
use crate::module_resolver_helpers::*;
use crate::span::Span;
use rustc_hash::FxHashMap;
use serde_json;
use std::path::{Path, PathBuf};
pub const CANNOT_FIND_MODULE: u32 = 2307;
pub const MODULE_RESOLUTION_MODE_MISMATCH: u32 = 2792;
pub const JSON_MODULE_WITHOUT_RESOLVE_JSON_MODULE: u32 = 2732;
pub const IMPORT_PATH_NEEDS_EXTENSION: u32 = 2834;
pub const IMPORT_PATH_NEEDS_EXTENSION_SUGGESTION: u32 = 2835;
pub const IMPORT_PATH_TS_EXTENSION_NOT_ALLOWED: u32 = 5097;
pub const MODULE_WAS_RESOLVED_TO_BUT_JSX_NOT_SET: u32 = 6142;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedModule {
pub resolved_path: PathBuf,
pub is_external: bool,
pub package_name: Option<String>,
pub original_specifier: String,
pub extension: ModuleExtension,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ModuleExtension {
Ts,
Tsx,
Dts,
DmTs,
DCts,
Js,
Jsx,
Mjs,
Cjs,
Mts,
Cts,
Json,
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PackageType {
Module,
#[default]
CommonJs,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
pub enum ImportingModuleKind {
Esm,
#[default]
CommonJs,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ImportKind {
#[default]
EsmImport,
DynamicImport,
CjsRequire,
EsmReExport,
}
impl ModuleExtension {
pub fn from_path(path: &Path) -> Self {
let path_str = path.to_string_lossy();
if path_str.ends_with(".d.ts") {
return Self::Dts;
}
if path_str.ends_with(".d.mts") {
return Self::DmTs;
}
if path_str.ends_with(".d.cts") {
return Self::DCts;
}
match path.extension().and_then(|e| e.to_str()) {
Some("ts") => Self::Ts,
Some("tsx") => Self::Tsx,
Some("js") => Self::Js,
Some("jsx") => Self::Jsx,
Some("mjs") => Self::Mjs,
Some("cjs") => Self::Cjs,
Some("mts") => Self::Mts,
Some("cts") => Self::Cts,
Some("json") => Self::Json,
_ => Self::Unknown,
}
}
pub const fn as_str(&self) -> &'static str {
match self {
Self::Ts => ".ts",
Self::Tsx => ".tsx",
Self::Dts => ".d.ts",
Self::DmTs => ".d.mts",
Self::DCts => ".d.cts",
Self::Js => ".js",
Self::Jsx => ".jsx",
Self::Mjs => ".mjs",
Self::Cjs => ".cjs",
Self::Mts => ".mts",
Self::Cts => ".cts",
Self::Json => ".json",
Self::Unknown => "",
}
}
pub const fn forces_esm(&self) -> bool {
matches!(self, Self::Mts | Self::Mjs | Self::DmTs)
}
pub const fn forces_cjs(&self) -> bool {
matches!(self, Self::Cts | Self::Cjs | Self::DCts)
}
}
fn explicit_ts_extension(specifier: &str) -> Option<String> {
if specifier.ends_with(".d.ts")
|| specifier.ends_with(".d.mts")
|| specifier.ends_with(".d.cts")
{
return None;
}
for ext in [".ts", ".tsx", ".mts", ".cts"] {
if specifier.ends_with(ext) {
return Some(ext.to_string());
}
}
None
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ResolutionFailure {
NotFound {
specifier: String,
containing_file: String,
span: Span,
},
InvalidSpecifier {
message: String,
containing_file: String,
span: Span,
},
PackageJsonError {
message: String,
containing_file: String,
span: Span,
},
CircularResolution {
message: String,
containing_file: String,
span: Span,
},
PathMappingFailed {
message: String,
containing_file: String,
span: Span,
},
ImportPathNeedsExtension {
specifier: String,
suggested_extension: String,
containing_file: String,
span: Span,
},
ImportingTsExtensionNotAllowed {
extension: String,
containing_file: String,
span: Span,
},
JsxNotEnabled {
specifier: String,
resolved_path: PathBuf,
containing_file: String,
span: Span,
},
ModuleResolutionModeMismatch {
specifier: String,
containing_file: String,
span: Span,
},
JsonModuleWithoutResolveJsonModule {
specifier: String,
containing_file: String,
span: Span,
},
}
impl ResolutionFailure {
pub fn to_diagnostic(&self) -> Diagnostic {
match self {
Self::NotFound {
specifier,
containing_file,
span,
} => Diagnostic::error(
containing_file,
*span,
format!("Cannot find module '{specifier}' or its corresponding type declarations.",),
CANNOT_FIND_MODULE,
),
Self::InvalidSpecifier {
message,
containing_file,
span,
}
| Self::PackageJsonError {
message,
containing_file,
span,
}
| Self::CircularResolution {
message,
containing_file,
span,
}
| Self::PathMappingFailed {
message,
containing_file,
span,
} => Diagnostic::error(
containing_file,
*span,
format!("Cannot find module '{message}' or its corresponding type declarations.",),
CANNOT_FIND_MODULE,
),
Self::ImportPathNeedsExtension {
specifier,
suggested_extension,
containing_file,
span,
} => {
if suggested_extension.is_empty() {
Diagnostic::error(
containing_file,
*span,
"Relative import paths need explicit file extensions in EcmaScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Consider adding an extension to the import path.".to_string(),
IMPORT_PATH_NEEDS_EXTENSION,
)
} else {
Diagnostic::error(
containing_file,
*span,
format!(
"Relative import paths need explicit file extensions in EcmaScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean '{specifier}{suggested_extension}'?",
),
IMPORT_PATH_NEEDS_EXTENSION_SUGGESTION,
)
}
}
Self::ImportingTsExtensionNotAllowed {
extension,
containing_file,
span,
} => Diagnostic::error(
containing_file,
*span,
format!(
"An import path can only end with a '{extension}' extension when 'allowImportingTsExtensions' is enabled.",
),
IMPORT_PATH_TS_EXTENSION_NOT_ALLOWED,
),
Self::JsxNotEnabled {
specifier,
resolved_path,
containing_file,
span,
} => Diagnostic::error(
containing_file,
*span,
format!(
"Module '{}' was resolved to '{}', but '--jsx' is not set.",
specifier,
resolved_path.display()
),
MODULE_WAS_RESOLVED_TO_BUT_JSX_NOT_SET,
),
Self::ModuleResolutionModeMismatch {
specifier,
containing_file,
span,
} => Diagnostic::error(
containing_file,
*span,
format!(
"Cannot find module '{specifier}'. Did you mean to set the 'moduleResolution' option to 'nodenext', or to add aliases to the 'paths' option?",
),
MODULE_RESOLUTION_MODE_MISMATCH,
),
Self::JsonModuleWithoutResolveJsonModule {
specifier,
containing_file,
span,
} => Diagnostic::error(
containing_file,
*span,
format!(
"Cannot find module '{specifier}'. Consider using '--resolveJsonModule' to import module with '.json' extension.",
),
JSON_MODULE_WITHOUT_RESOLVE_JSON_MODULE,
),
}
}
pub fn containing_file(&self) -> &str {
match self {
Self::NotFound {
containing_file, ..
}
| Self::InvalidSpecifier {
containing_file, ..
}
| Self::PackageJsonError {
containing_file, ..
}
| Self::CircularResolution {
containing_file, ..
}
| Self::PathMappingFailed {
containing_file, ..
}
| Self::ImportPathNeedsExtension {
containing_file, ..
}
| Self::ImportingTsExtensionNotAllowed {
containing_file, ..
}
| Self::JsxNotEnabled {
containing_file, ..
}
| Self::ModuleResolutionModeMismatch {
containing_file, ..
}
| Self::JsonModuleWithoutResolveJsonModule {
containing_file, ..
} => containing_file,
}
}
pub const fn span(&self) -> Span {
match self {
Self::NotFound { span, .. }
| Self::InvalidSpecifier { span, .. }
| Self::PackageJsonError { span, .. }
| Self::CircularResolution { span, .. }
| Self::PathMappingFailed { span, .. }
| Self::ImportPathNeedsExtension { span, .. }
| Self::ImportingTsExtensionNotAllowed { span, .. }
| Self::JsxNotEnabled { span, .. }
| Self::ModuleResolutionModeMismatch { span, .. }
| Self::JsonModuleWithoutResolveJsonModule { span, .. } => *span,
}
}
pub const fn is_not_found(&self) -> bool {
matches!(self, Self::NotFound { .. })
}
}
#[derive(Debug)]
pub struct ModuleResolver {
resolution_kind: ModuleResolutionKind,
base_url: Option<PathBuf>,
path_mappings: Vec<PathMapping>,
type_roots: Vec<PathBuf>,
types_versions_compiler_version: Option<String>,
resolve_package_json_exports: bool,
resolve_package_json_imports: bool,
module_suffixes: Vec<String>,
resolve_json_module: bool,
allow_arbitrary_extensions: bool,
allow_importing_ts_extensions: bool,
jsx: Option<JsxEmit>,
resolution_cache: FxHashMap<
(PathBuf, String, ImportingModuleKind),
Result<ResolvedModule, ResolutionFailure>,
>,
custom_conditions: Vec<String>,
module_kind: ModuleKind,
allow_js: bool,
rewrite_relative_import_extensions: bool,
package_type_cache: FxHashMap<PathBuf, Option<PackageType>>,
current_package_type: Option<PackageType>,
}
struct PathMappingAttempt {
resolved: Option<ResolvedModule>,
attempted: bool,
}
impl ModuleResolver {
pub fn new(options: &ResolvedCompilerOptions) -> Self {
let resolution_kind = options.effective_module_resolution();
let module_suffixes = if options.module_suffixes.is_empty() {
vec![String::new()]
} else {
options.module_suffixes.clone()
};
Self {
resolution_kind,
base_url: options.base_url.clone(),
path_mappings: options.paths.clone().unwrap_or_default(),
type_roots: options.type_roots.clone().unwrap_or_default(),
types_versions_compiler_version: options.types_versions_compiler_version.clone(),
resolve_package_json_exports: options.resolve_package_json_exports,
resolve_package_json_imports: options.resolve_package_json_imports,
module_suffixes,
resolve_json_module: options.resolve_json_module,
allow_arbitrary_extensions: options.allow_arbitrary_extensions,
allow_importing_ts_extensions: options.allow_importing_ts_extensions,
jsx: options.jsx,
resolution_cache: FxHashMap::default(),
custom_conditions: options.custom_conditions.clone(),
module_kind: options.printer.module,
allow_js: options.allow_js,
rewrite_relative_import_extensions: options.rewrite_relative_import_extensions,
package_type_cache: FxHashMap::default(),
current_package_type: None,
}
}
pub fn node_resolver() -> Self {
Self {
resolution_kind: ModuleResolutionKind::Node,
base_url: None,
path_mappings: Vec::new(),
type_roots: Vec::new(),
types_versions_compiler_version: None,
resolve_package_json_exports: false,
resolve_package_json_imports: false,
module_suffixes: vec![String::new()],
resolve_json_module: false,
allow_arbitrary_extensions: false,
allow_importing_ts_extensions: false,
jsx: None,
resolution_cache: FxHashMap::default(),
custom_conditions: Vec::new(),
module_kind: ModuleKind::CommonJS,
allow_js: false,
rewrite_relative_import_extensions: false,
package_type_cache: FxHashMap::default(),
current_package_type: None,
}
}
pub fn resolve(
&mut self,
specifier: &str,
containing_file: &Path,
specifier_span: Span,
) -> Result<ResolvedModule, ResolutionFailure> {
self.resolve_with_kind(
specifier,
containing_file,
specifier_span,
ImportKind::EsmImport,
)
}
pub fn resolve_with_kind(
&mut self,
specifier: &str,
containing_file: &Path,
specifier_span: Span,
import_kind: ImportKind,
) -> Result<ResolvedModule, ResolutionFailure> {
let containing_dir = containing_file
.parent()
.unwrap_or_else(|| Path::new("."))
.to_path_buf();
let containing_file_str = containing_file.display().to_string();
self.current_package_type = match self.resolution_kind {
ModuleResolutionKind::Node16 | ModuleResolutionKind::NodeNext => {
self.get_package_type_for_dir(&containing_dir)
}
_ => None,
};
let importing_module_kind = self.get_importing_module_kind(containing_file);
let cache_key = (
containing_dir.clone(),
specifier.to_string(),
importing_module_kind,
);
if let Some(cached) = self.resolution_cache.get(&cache_key) {
return cached.clone();
}
let (mut result, path_mapping_attempted) = self.resolve_uncached(
specifier,
&containing_dir,
&containing_file_str,
specifier_span,
importing_module_kind,
import_kind,
);
if !self.allow_importing_ts_extensions
&& !self.allow_arbitrary_extensions
&& !self.rewrite_relative_import_extensions
&& (self.base_url.is_some() || self.path_mappings.is_empty())
&& let Some(extension) = explicit_ts_extension(specifier)
&& !path_mapping_attempted
&& matches!(result, Err(ResolutionFailure::NotFound { .. }))
{
result = Err(ResolutionFailure::ImportingTsExtensionNotAllowed {
extension,
containing_file: containing_file_str.clone(),
span: specifier_span,
});
}
if let Ok(resolved) = &result {
if matches!(
resolved.extension,
ModuleExtension::Tsx | ModuleExtension::Jsx
) && self.jsx.is_none()
{
result = Err(ResolutionFailure::JsxNotEnabled {
specifier: specifier.to_string(),
resolved_path: resolved.resolved_path.clone(),
containing_file: containing_file_str,
span: specifier_span,
});
} else if resolved.extension == ModuleExtension::Json && !self.resolve_json_module {
result = Err(ResolutionFailure::JsonModuleWithoutResolveJsonModule {
specifier: specifier.to_string(),
containing_file: containing_file_str,
span: specifier_span,
});
}
}
self.resolution_cache.insert(cache_key, result.clone());
result
}
fn get_importing_module_kind(&mut self, file_path: &Path) -> ImportingModuleKind {
let extension = ModuleExtension::from_path(file_path);
if extension.forces_esm() {
return ImportingModuleKind::Esm;
}
if extension.forces_cjs() {
return ImportingModuleKind::CommonJs;
}
match self.module_kind {
ModuleKind::CommonJS | ModuleKind::AMD | ModuleKind::UMD | ModuleKind::System => {
return ImportingModuleKind::CommonJs;
}
ModuleKind::None
| ModuleKind::ES2015
| ModuleKind::ES2020
| ModuleKind::ES2022
| ModuleKind::ESNext
| ModuleKind::Node16
| ModuleKind::NodeNext
| ModuleKind::Preserve => {}
}
if let Some(dir) = file_path.parent() {
match self.get_package_type_for_dir(dir) {
Some(PackageType::Module) => ImportingModuleKind::Esm,
Some(PackageType::CommonJs) | None => ImportingModuleKind::CommonJs,
}
} else {
ImportingModuleKind::CommonJs
}
}
fn get_package_type_for_dir(&mut self, dir: &Path) -> Option<PackageType> {
if let Some(cached) = self.package_type_cache.get(dir) {
return *cached;
}
let mut current = dir.to_path_buf();
let mut visited = Vec::new();
loop {
if let Some(&cached) = self.package_type_cache.get(¤t) {
let result = cached;
for path in visited {
self.package_type_cache.insert(path, result);
}
return result;
}
visited.push(current.clone());
let package_json_path = current.join("package.json");
if package_json_path.is_file()
&& let Ok(pj) = self.read_package_json(&package_json_path)
{
let package_type = pj.package_type.as_deref().and_then(|t| match t {
"module" => Some(PackageType::Module),
"commonjs" => Some(PackageType::CommonJs),
_ => None,
});
for path in visited {
self.package_type_cache.insert(path, package_type);
}
return package_type;
}
match current.parent() {
Some(parent) if parent != current => current = parent.to_path_buf(),
_ => break,
}
}
for path in visited {
self.package_type_cache.insert(path, None);
}
None
}
fn resolve_uncached(
&self,
specifier: &str,
containing_dir: &Path,
containing_file: &str,
specifier_span: Span,
importing_module_kind: ImportingModuleKind,
import_kind: ImportKind,
) -> (Result<ResolvedModule, ResolutionFailure>, bool) {
if specifier.starts_with('#') {
if !self.resolve_package_json_imports {
return (
Err(ResolutionFailure::NotFound {
specifier: specifier.to_string(),
containing_file: containing_file.to_string(),
span: specifier_span,
}),
false,
);
}
return (
self.resolve_package_imports(
specifier,
containing_dir,
containing_file,
specifier_span,
importing_module_kind,
),
false,
);
}
let mut path_mapping_attempted = false;
if self.base_url.is_some() && !self.path_mappings.is_empty() {
let attempt = self.try_path_mappings(specifier, containing_dir);
if let Some(resolved) = attempt.resolved {
return (Ok(resolved), path_mapping_attempted);
}
path_mapping_attempted = attempt.attempted;
}
if specifier.starts_with("./")
|| specifier.starts_with("../")
|| specifier == "."
|| specifier == ".."
{
return (
self.resolve_relative(
specifier,
containing_dir,
containing_file,
specifier_span,
importing_module_kind,
import_kind,
),
path_mapping_attempted,
);
}
if specifier.starts_with('/') {
return (
self.resolve_absolute(specifier, containing_file, specifier_span),
path_mapping_attempted,
);
}
if let Some(base_url) = &self.base_url {
let candidate = base_url.join(specifier);
if let Some(resolved) = self.try_file_or_directory(&candidate) {
return (
Ok(ResolvedModule {
resolved_path: resolved.clone(),
is_external: false,
package_name: None,
original_specifier: specifier.to_string(),
extension: ModuleExtension::from_path(&resolved),
}),
path_mapping_attempted,
);
}
}
let resolved = if matches!(self.resolution_kind, ModuleResolutionKind::Classic) {
self.resolve_classic_non_relative(
specifier,
containing_dir,
containing_file,
specifier_span,
)
} else {
self.resolve_bare_specifier(
specifier,
containing_dir,
containing_file,
specifier_span,
importing_module_kind,
)
};
if let Err(ResolutionFailure::NotFound { .. }) = &resolved
&& path_mapping_attempted
{
return (
Err(ResolutionFailure::PathMappingFailed {
message: specifier.to_string(),
containing_file: containing_file.to_string(),
span: specifier_span,
}),
path_mapping_attempted,
);
}
(resolved, path_mapping_attempted)
}
fn resolve_package_imports(
&self,
specifier: &str,
containing_dir: &Path,
containing_file: &str,
specifier_span: Span,
importing_module_kind: ImportingModuleKind,
) -> Result<ResolvedModule, ResolutionFailure> {
let mut current = containing_dir.to_path_buf();
loop {
let package_json_path = current.join("package.json");
if package_json_path.is_file()
&& let Ok(package_json) = self.read_package_json(&package_json_path)
&& let Some(imports) = &package_json.imports
{
let conditions = self.get_export_conditions(importing_module_kind);
if let Some(target) = self.resolve_imports_subpath(imports, specifier, &conditions)
{
let resolved_path = current.join(target.trim_start_matches("./"));
if let Some(resolved) = self.try_file_or_directory(&resolved_path) {
return Ok(ResolvedModule {
resolved_path: resolved.clone(),
is_external: false,
package_name: package_json.name.clone(),
original_specifier: specifier.to_string(),
extension: ModuleExtension::from_path(&resolved),
});
}
}
}
match current.parent() {
Some(parent) if parent != current => current = parent.to_path_buf(),
_ => break,
}
}
Err(ResolutionFailure::NotFound {
specifier: specifier.to_string(),
containing_file: containing_file.to_string(),
span: specifier_span,
})
}
fn resolve_imports_subpath(
&self,
imports: &FxHashMap<String, PackageExports>,
specifier: &str,
conditions: &[String],
) -> Option<String> {
if let Some(value) = imports.get(specifier) {
return self.resolve_export_target_to_string(value, conditions);
}
let mut best_match: Option<(usize, String, &PackageExports)> = None;
for (pattern, value) in imports {
if let Some(wildcard) = match_imports_pattern(pattern, specifier) {
let specificity = pattern.len();
let is_better = match &best_match {
None => true,
Some((best_len, _, _)) => specificity > *best_len,
};
if is_better {
best_match = Some((specificity, wildcard, value));
}
}
}
if let Some((_, wildcard, value)) = best_match
&& let Some(target) = self.resolve_export_target_to_string(value, conditions)
{
return Some(apply_wildcard_substitution(&target, &wildcard));
}
None
}
#[allow(clippy::only_used_in_recursion)]
fn resolve_export_target_to_string(
&self,
value: &PackageExports,
conditions: &[String],
) -> Option<String> {
match value {
PackageExports::String(s) => Some(s.clone()),
PackageExports::Conditional(cond_entries) => {
for (key, nested) in cond_entries {
if conditions.iter().any(|c| c == key) {
if matches!(nested, PackageExports::Null) {
return None;
}
if let Some(result) =
self.resolve_export_target_to_string(nested, conditions)
{
return Some(result);
}
}
}
None
}
PackageExports::Map(_) | PackageExports::Null => None, }
}
fn get_export_conditions(&self, importing_module_kind: ImportingModuleKind) -> Vec<String> {
let mut conditions = Vec::new();
for cond in &self.custom_conditions {
conditions.push(cond.clone());
}
conditions.push("types".to_string());
match self.resolution_kind {
ModuleResolutionKind::Node16 | ModuleResolutionKind::NodeNext => {
conditions.push("node".to_string());
}
_ => {}
}
match importing_module_kind {
ImportingModuleKind::Esm => {
conditions.push("import".to_string());
}
ImportingModuleKind::CommonJs => {
conditions.push("require".to_string());
}
}
conditions.push("default".to_string());
conditions
}
fn try_path_mappings(&self, specifier: &str, _containing_dir: &Path) -> PathMappingAttempt {
let mut sorted_mappings: Vec<_> = self.path_mappings.iter().collect();
sorted_mappings.sort_by_key(|b| std::cmp::Reverse(b.specificity()));
let mut attempted = false;
for mapping in sorted_mappings {
if let Some(star_match) = mapping.match_specifier(specifier) {
attempted = true;
for target in &mapping.targets {
let substituted = if target.contains('*') {
target.replace('*', &star_match)
} else {
target.clone()
};
if Self::has_path_mapping_target_extension(&substituted) {
continue;
}
let base = self
.base_url
.as_deref()
.expect("path mappings require baseUrl for attempted resolution");
let candidate = base.join(&substituted);
if let Some(resolved) = self.try_file_or_directory(&candidate) {
return PathMappingAttempt {
resolved: Some(ResolvedModule {
resolved_path: resolved,
is_external: false,
package_name: None,
original_specifier: specifier.to_string(),
extension: ModuleExtension::from_path(&candidate),
}),
attempted,
};
}
}
}
}
PathMappingAttempt {
resolved: None,
attempted,
}
}
fn has_path_mapping_target_extension(target: &str) -> bool {
let base_path = std::path::Path::new(target);
split_path_extension(base_path).is_some()
}
fn resolve_relative(
&self,
specifier: &str,
containing_dir: &Path,
containing_file: &str,
specifier_span: Span,
importing_module_kind: ImportingModuleKind,
import_kind: ImportKind,
) -> Result<ResolvedModule, ResolutionFailure> {
let candidate = containing_dir.join(specifier);
let specifier_has_extension = Path::new(specifier)
.extension()
.is_some_and(|ext| !ext.is_empty());
let needs_extension_check = matches!(
self.resolution_kind,
ModuleResolutionKind::Node16 | ModuleResolutionKind::NodeNext
) && !specifier_has_extension
&& match import_kind {
ImportKind::DynamicImport => true,
ImportKind::EsmImport | ImportKind::EsmReExport => {
importing_module_kind == ImportingModuleKind::Esm
}
ImportKind::CjsRequire => false,
};
if needs_extension_check {
if let Some(resolved) = self.try_file_or_directory(&candidate) {
let resolved_ext = ModuleExtension::from_path(&resolved);
let suggested_ext = match resolved_ext {
ModuleExtension::Ts
| ModuleExtension::Tsx
| ModuleExtension::Js
| ModuleExtension::Jsx
| ModuleExtension::Dts
| ModuleExtension::Unknown => ".js",
ModuleExtension::Mts | ModuleExtension::Mjs | ModuleExtension::DmTs => ".mjs",
ModuleExtension::Cts | ModuleExtension::Cjs | ModuleExtension::DCts => ".cjs",
ModuleExtension::Json => ".json",
};
return Err(ResolutionFailure::ImportPathNeedsExtension {
specifier: specifier.to_string(),
suggested_extension: suggested_ext.to_string(),
containing_file: containing_file.to_string(),
span: specifier_span,
});
}
return Err(ResolutionFailure::ImportPathNeedsExtension {
specifier: specifier.to_string(),
suggested_extension: String::new(),
containing_file: containing_file.to_string(),
span: specifier_span,
});
}
if let Some(resolved) = self.try_file_or_directory(&candidate) {
return Ok(ResolvedModule {
resolved_path: resolved.clone(),
is_external: false,
package_name: None,
original_specifier: specifier.to_string(),
extension: ModuleExtension::from_path(&resolved),
});
}
Err(ResolutionFailure::NotFound {
specifier: specifier.to_string(),
containing_file: containing_file.to_string(),
span: specifier_span,
})
}
fn resolve_absolute(
&self,
specifier: &str,
containing_file: &str,
specifier_span: Span,
) -> Result<ResolvedModule, ResolutionFailure> {
let path = PathBuf::from(specifier);
if let Some(resolved) = self.try_file_or_directory(&path) {
return Ok(ResolvedModule {
resolved_path: resolved.clone(),
is_external: false,
package_name: None,
original_specifier: specifier.to_string(),
extension: ModuleExtension::from_path(&resolved),
});
}
Err(ResolutionFailure::NotFound {
specifier: specifier.to_string(),
containing_file: containing_file.to_string(),
span: specifier_span,
})
}
fn resolve_classic_non_relative(
&self,
specifier: &str,
containing_dir: &Path,
containing_file: &str,
specifier_span: Span,
) -> Result<ResolvedModule, ResolutionFailure> {
let (package_name, subpath) = parse_package_specifier(specifier);
let conditions = self.get_export_conditions(ImportingModuleKind::CommonJs);
let mut current = containing_dir.to_path_buf();
loop {
let candidate = current.join(specifier);
if let Some(resolved) = self.try_file_or_directory(&candidate) {
return Ok(ResolvedModule {
resolved_path: resolved.clone(),
is_external: false,
package_name: None,
original_specifier: specifier.to_string(),
extension: ModuleExtension::from_path(&resolved),
});
}
if !package_name.starts_with("@types/") {
let types_package = types_package_name(&package_name);
let types_dir = current.join("node_modules").join(&types_package);
if types_dir.is_dir()
&& let Ok(resolved) = self.resolve_package(
&types_dir,
subpath.as_deref(),
specifier,
containing_file,
specifier_span,
&conditions,
)
{
return Ok(resolved);
}
}
for type_root in &self.type_roots {
let types_package = if !package_name.starts_with("@types/") {
type_root.join(types_package_name(&package_name))
} else {
type_root.join(&package_name)
};
if types_package.is_dir()
&& let Ok(resolved) = self.resolve_package(
&types_package,
subpath.as_deref(),
specifier,
containing_file,
specifier_span,
&conditions,
)
{
return Ok(resolved);
}
}
match current.parent() {
Some(parent) if parent != current => current = parent.to_path_buf(),
_ => break,
}
}
Err(ResolutionFailure::NotFound {
specifier: specifier.to_string(),
containing_file: containing_file.to_string(),
span: specifier_span,
})
}
fn resolve_bare_specifier(
&self,
specifier: &str,
containing_dir: &Path,
containing_file: &str,
specifier_span: Span,
importing_module_kind: ImportingModuleKind,
) -> Result<ResolvedModule, ResolutionFailure> {
let (package_name, subpath) = parse_package_specifier(specifier);
let conditions = self.get_export_conditions(importing_module_kind);
if let Some(resolved) = self.try_self_reference(
&package_name,
subpath.as_deref(),
specifier,
containing_dir,
&conditions,
) {
return Ok(resolved);
}
let mut current = containing_dir.to_path_buf();
loop {
let node_modules = current.join("node_modules");
if node_modules.is_dir() {
let package_dir = node_modules.join(&package_name);
if package_dir.is_dir() {
match self.resolve_package(
&package_dir,
subpath.as_deref(),
specifier,
containing_file,
specifier_span,
&conditions,
) {
Ok(resolved) => return Ok(resolved),
Err(e @ ResolutionFailure::ModuleResolutionModeMismatch { .. }) => {
return Err(e);
}
Err(e @ ResolutionFailure::NotFound { .. }) => {
if self.should_stop_on_bundler_exports_failure(
&package_dir,
subpath.as_deref(),
&conditions,
containing_file,
specifier,
) {
return Err(e);
}
}
Err(_) => {
}
}
} else if matches!(self.resolution_kind, ModuleResolutionKind::Bundler)
&& subpath.is_none()
{
if let Some(resolved) = self.try_file_or_directory(&package_dir) {
return Ok(ResolvedModule {
resolved_path: resolved.clone(),
is_external: true,
package_name: Some(package_name),
original_specifier: specifier.to_string(),
extension: ModuleExtension::from_path(&resolved),
});
}
}
}
if !package_name.starts_with("@types/") {
let types_package = types_package_name(&package_name);
let types_dir = node_modules.join(&types_package);
if types_dir.is_dir()
&& let Ok(resolved) = self.resolve_package(
&types_dir,
subpath.as_deref(),
specifier,
containing_file,
specifier_span,
&conditions,
)
{
return Ok(resolved);
}
}
match current.parent() {
Some(parent) if parent != current => current = parent.to_path_buf(),
_ => break,
}
}
for type_root in &self.type_roots {
let types_package = type_root.join(types_package_name(&package_name));
if types_package.is_dir()
&& let Ok(resolved) = self.resolve_package(
&types_package,
subpath.as_deref(),
specifier,
containing_file,
specifier_span,
&conditions,
)
{
return Ok(resolved);
}
}
Err(ResolutionFailure::NotFound {
specifier: specifier.to_string(),
containing_file: containing_file.to_string(),
span: specifier_span,
})
}
fn should_stop_on_bundler_exports_failure(
&self,
package_dir: &Path,
subpath: Option<&str>,
conditions: &[String],
_containing_file: &str,
_specifier: &str,
) -> bool {
if !matches!(self.resolution_kind, ModuleResolutionKind::Bundler) {
return false;
}
if !self.resolve_package_json_exports {
return false;
}
let package_json_path = package_dir.join("package.json");
if !package_json_path.is_file() {
return false;
}
let package_json = match self.read_package_json(&package_json_path) {
Ok(package_json) => package_json,
Err(_) => return false,
};
let Some(exports) = package_json.exports else {
return false;
};
let subpath_key = match subpath {
Some(subpath) => format!("./{subpath}"),
None => ".".to_string(),
};
self.resolve_package_exports_with_conditions(
package_dir,
&exports,
&subpath_key,
conditions,
)
.is_none()
}
fn try_self_reference(
&self,
package_name: &str,
subpath: Option<&str>,
original_specifier: &str,
containing_dir: &Path,
conditions: &[String],
) -> Option<ResolvedModule> {
if !matches!(
self.resolution_kind,
ModuleResolutionKind::Node16
| ModuleResolutionKind::NodeNext
| ModuleResolutionKind::Bundler
) {
return None;
}
let mut current = containing_dir.to_path_buf();
loop {
let package_json_path = current.join("package.json");
if package_json_path.is_file()
&& let Ok(package_json) = self.read_package_json(&package_json_path)
{
if package_json.name.as_deref() == Some(package_name) {
if self.resolve_package_json_exports
&& let Some(exports) = &package_json.exports
{
let subpath_key = match subpath {
Some(sp) => format!("./{sp}"),
None => ".".to_string(),
};
if let Some(resolved) = self.resolve_package_exports_with_conditions(
¤t,
exports,
&subpath_key,
conditions,
) {
return Some(ResolvedModule {
resolved_path: resolved.clone(),
is_external: false,
package_name: Some(package_name.to_string()),
original_specifier: original_specifier.to_string(),
extension: ModuleExtension::from_path(&resolved),
});
}
}
}
return None;
}
match current.parent() {
Some(parent) if parent != current => current = parent.to_path_buf(),
_ => break,
}
}
None
}
fn resolve_package(
&self,
package_dir: &Path,
subpath: Option<&str>,
original_specifier: &str,
containing_file: &str,
specifier_span: Span,
conditions: &[String],
) -> Result<ResolvedModule, ResolutionFailure> {
let package_json_path = package_dir.join("package.json");
let package_json = if package_json_path.exists() {
self.read_package_json(&package_json_path).map_err(|msg| {
ResolutionFailure::PackageJsonError {
message: msg,
containing_file: containing_file.to_string(),
span: specifier_span,
}
})?
} else {
PackageJson::default()
};
if let Some(subpath) = subpath {
let subpath_key = format!("./{subpath}");
if self.resolve_package_json_exports
&& let Some(exports) = &package_json.exports
{
if let Some(resolved) = self.resolve_package_exports_with_conditions(
package_dir,
exports,
&subpath_key,
conditions,
) {
return Ok(ResolvedModule {
resolved_path: resolved.clone(),
is_external: true,
package_name: Some(package_json.name.clone().unwrap_or_default()),
original_specifier: original_specifier.to_string(),
extension: ModuleExtension::from_path(&resolved),
});
}
if matches!(
self.resolution_kind,
ModuleResolutionKind::Node16 | ModuleResolutionKind::NodeNext
) {
return Err(ResolutionFailure::ModuleResolutionModeMismatch {
specifier: original_specifier.to_string(),
containing_file: containing_file.to_string(),
span: specifier_span,
});
}
if matches!(self.resolution_kind, ModuleResolutionKind::Bundler) {
return Err(ResolutionFailure::NotFound {
specifier: original_specifier.to_string(),
containing_file: containing_file.to_string(),
span: specifier_span,
});
}
}
if let Some(types_versions) = &package_json.types_versions
&& let Some(resolved) =
self.resolve_types_versions(package_dir, subpath, types_versions)
{
return Ok(ResolvedModule {
resolved_path: resolved.clone(),
is_external: true,
package_name: Some(package_json.name.clone().unwrap_or_default()),
original_specifier: original_specifier.to_string(),
extension: ModuleExtension::from_path(&resolved),
});
}
let file_path = package_dir.join(subpath);
if let Some(resolved) = self.try_file_or_directory(&file_path) {
return Ok(ResolvedModule {
resolved_path: resolved.clone(),
is_external: true,
package_name: Some(package_json.name.unwrap_or_default()),
original_specifier: original_specifier.to_string(),
extension: ModuleExtension::from_path(&resolved),
});
}
return Err(ResolutionFailure::NotFound {
specifier: original_specifier.to_string(),
containing_file: containing_file.to_string(),
span: specifier_span,
});
}
if self.resolve_package_json_exports
&& let Some(exports) = &package_json.exports
{
if let Some(resolved) =
self.resolve_package_exports_with_conditions(package_dir, exports, ".", conditions)
{
return Ok(ResolvedModule {
resolved_path: resolved.clone(),
is_external: true,
package_name: Some(package_json.name.clone().unwrap_or_default()),
original_specifier: original_specifier.to_string(),
extension: ModuleExtension::from_path(&resolved),
});
}
if matches!(
self.resolution_kind,
ModuleResolutionKind::Node16 | ModuleResolutionKind::NodeNext
) {
return Err(ResolutionFailure::ModuleResolutionModeMismatch {
specifier: original_specifier.to_string(),
containing_file: containing_file.to_string(),
span: specifier_span,
});
}
if matches!(self.resolution_kind, ModuleResolutionKind::Bundler) {
return Err(ResolutionFailure::NotFound {
specifier: original_specifier.to_string(),
containing_file: containing_file.to_string(),
span: specifier_span,
});
}
}
if let Some(types_versions) = &package_json.types_versions
&& let Some(resolved) =
self.resolve_types_versions(package_dir, "index", types_versions)
{
return Ok(ResolvedModule {
resolved_path: resolved.clone(),
is_external: true,
package_name: Some(package_json.name.clone().unwrap_or_default()),
original_specifier: original_specifier.to_string(),
extension: ModuleExtension::from_path(&resolved),
});
}
if let Some(types) = package_json
.types
.clone()
.or_else(|| package_json.typings.clone())
{
let types_path = package_dir.join(&types);
if let Some(resolved) = resolve_explicit_unknown_extension(&types_path) {
return Ok(ResolvedModule {
resolved_path: resolved.clone(),
is_external: true,
package_name: Some(package_json.name.unwrap_or_default()),
original_specifier: original_specifier.to_string(),
extension: ModuleExtension::from_path(&resolved),
});
}
if let Some(resolved) = self.try_file_or_directory(&types_path) {
return Ok(ResolvedModule {
resolved_path: resolved.clone(),
is_external: true,
package_name: Some(package_json.name.unwrap_or_default()),
original_specifier: original_specifier.to_string(),
extension: ModuleExtension::from_path(&resolved),
});
}
}
if let Some(main) = &package_json.main {
let main_path = package_dir.join(main);
if let Some(resolved) = resolve_explicit_unknown_extension(&main_path) {
return Ok(ResolvedModule {
resolved_path: resolved.clone(),
is_external: true,
package_name: Some(package_json.name.clone().unwrap_or_default()),
original_specifier: original_specifier.to_string(),
extension: ModuleExtension::from_path(&resolved),
});
}
if let Some(declaration) = declaration_substitution_for_main(&main_path)
&& declaration.is_file()
{
return Ok(ResolvedModule {
resolved_path: declaration.clone(),
is_external: true,
package_name: Some(package_json.name.clone().unwrap_or_default()),
original_specifier: original_specifier.to_string(),
extension: ModuleExtension::from_path(&declaration),
});
}
if let Some(resolved) = self.try_file(&main_path) {
return Ok(ResolvedModule {
resolved_path: resolved.clone(),
is_external: true,
package_name: Some(package_json.name.clone().unwrap_or_default()),
original_specifier: original_specifier.to_string(),
extension: ModuleExtension::from_path(&resolved),
});
}
if main_path.is_dir() {
let index = main_path.join("index");
if let Some(resolved) = self.try_file(&index) {
return Ok(ResolvedModule {
resolved_path: resolved.clone(),
is_external: true,
package_name: Some(package_json.name.clone().unwrap_or_default()),
original_specifier: original_specifier.to_string(),
extension: ModuleExtension::from_path(&resolved),
});
}
}
}
let index = package_dir.join("index");
if let Some(resolved) = self.try_file(&index) {
return Ok(ResolvedModule {
resolved_path: resolved.clone(),
is_external: true,
package_name: Some(package_json.name.unwrap_or_default()),
original_specifier: original_specifier.to_string(),
extension: ModuleExtension::from_path(&resolved),
});
}
Err(ResolutionFailure::PackageJsonError {
message: format!(
"Could not find entry point for package at {}",
package_dir.display()
),
containing_file: containing_file.to_string(),
span: specifier_span,
})
}
fn resolve_package_exports_with_conditions(
&self,
package_dir: &Path,
exports: &PackageExports,
subpath: &str,
conditions: &[String],
) -> Option<PathBuf> {
match exports {
PackageExports::String(s) => {
if subpath == "." {
let resolved = package_dir.join(s.trim_start_matches("./"));
if let Some(r) = self.try_export_target(&resolved) {
return Some(r);
}
}
None
}
PackageExports::Map(map) => {
if let Some(value) = map.get(subpath) {
return self.resolve_export_value_with_conditions(
package_dir,
value,
conditions,
);
}
let mut best_match: Option<(usize, String, &PackageExports)> = None;
for (pattern, value) in map {
if let Some(matched) = match_export_pattern(pattern, subpath) {
let specificity = pattern.len();
let is_better = match &best_match {
None => true,
Some((best_len, _, _)) => specificity > *best_len,
};
if is_better {
best_match = Some((specificity, matched, value));
}
}
}
if let Some((_, wildcard, value)) = best_match
&& let Some(resolved) =
self.resolve_export_value_with_conditions(package_dir, value, conditions)
{
let resolved_str = resolved.to_string_lossy();
if resolved_str.contains('*') {
let substituted = resolved_str.replace('*', &wildcard);
return Some(PathBuf::from(substituted));
}
return Some(resolved);
}
None
}
PackageExports::Conditional(cond_entries) => {
for (key, value) in cond_entries {
if conditions.iter().any(|c| c == key) {
if matches!(value, PackageExports::Null) {
return None;
}
if let Some(resolved) = self.resolve_package_exports_with_conditions(
package_dir,
value,
subpath,
conditions,
) {
return Some(resolved);
}
}
}
None
}
PackageExports::Null => None,
}
}
fn resolve_export_value_with_conditions(
&self,
package_dir: &Path,
value: &PackageExports,
conditions: &[String],
) -> Option<PathBuf> {
match value {
PackageExports::String(s) => {
let resolved = package_dir.join(s.trim_start_matches("./"));
self.try_export_target(&resolved)
}
PackageExports::Conditional(cond_entries) => {
for (key, nested) in cond_entries {
if conditions.iter().any(|c| c == key) {
if matches!(nested, PackageExports::Null) {
return None;
}
if let Some(resolved) = self.resolve_export_value_with_conditions(
package_dir,
nested,
conditions,
) {
return Some(resolved);
}
}
}
None
}
PackageExports::Map(_) | PackageExports::Null => None,
}
}
fn resolve_types_versions(
&self,
package_dir: &Path,
subpath: &str,
types_versions: &serde_json::Value,
) -> Option<PathBuf> {
let compiler_version =
types_versions_compiler_version(self.types_versions_compiler_version.as_deref());
let paths = select_types_versions_paths(types_versions, compiler_version)?;
let mut best_pattern: Option<&String> = None;
let mut best_value: Option<&serde_json::Value> = None;
let mut best_wildcard = String::new();
let mut best_specificity = 0usize;
let mut best_len = 0usize;
for (pattern, value) in paths {
let Some(wildcard) = match_types_versions_pattern(pattern, subpath) else {
continue;
};
let specificity = types_versions_specificity(pattern);
let pattern_len = pattern.len();
let is_better = match best_pattern {
None => true,
Some(current) => {
specificity > best_specificity
|| (specificity == best_specificity && pattern_len > best_len)
|| (specificity == best_specificity
&& pattern_len == best_len
&& pattern < current)
}
};
if is_better {
best_specificity = specificity;
best_len = pattern_len;
best_pattern = Some(pattern);
best_value = Some(value);
best_wildcard = wildcard;
}
}
let value = best_value?;
let mut targets = Vec::new();
match value {
serde_json::Value::String(value) => targets.push(value.as_str()),
serde_json::Value::Array(list) => {
for entry in list {
if let Some(value) = entry.as_str() {
targets.push(value);
}
}
}
_ => {}
}
for target in targets {
let substituted = apply_wildcard_substitution(target, &best_wildcard);
let resolved = package_dir.join(substituted.trim_start_matches("./"));
if let Some(resolved) = self.try_file_or_directory(&resolved) {
return Some(resolved);
}
}
None
}
fn try_file(&self, path: &Path) -> Option<PathBuf> {
let suffixes = &self.module_suffixes;
if let Some(extension) = path.extension().and_then(|ext| ext.to_str())
&& split_path_extension(path).is_none()
{
if self.allow_arbitrary_extensions
&& let Some(resolved) = try_arbitrary_extension_declaration(path, extension)
{
return Some(resolved);
}
return None;
}
if let Some((base, extension)) = split_path_extension(path) {
if let Some(rewritten) = node16_extension_substitution(path, extension) {
for candidate in &rewritten {
if let Some(resolved) = try_file_with_suffixes(candidate, suffixes) {
return Some(resolved);
}
}
}
if let Some(resolved) = try_file_with_suffixes_and_extension(&base, extension, suffixes)
{
return Some(resolved);
}
return None;
}
let extensions = self.extension_candidates_for_resolution();
for ext in extensions {
if let Some(resolved) = try_file_with_suffixes_and_extension(path, ext, suffixes) {
return Some(resolved);
}
}
if self.resolve_json_module
&& let Some(resolved) = try_file_with_suffixes_and_extension(path, "json", suffixes)
{
return Some(resolved);
}
let index = path.join("index");
for ext in extensions {
if let Some(resolved) = try_file_with_suffixes_and_extension(&index, ext, suffixes) {
return Some(resolved);
}
}
if self.resolve_json_module
&& let Some(resolved) = try_file_with_suffixes_and_extension(&index, "json", suffixes)
{
return Some(resolved);
}
None
}
const fn extension_candidates_for_resolution(&self) -> &'static [&'static str] {
match self.resolution_kind {
ModuleResolutionKind::Node16 | ModuleResolutionKind::NodeNext => {
match self.current_package_type {
Some(PackageType::Module) => &NODE16_MODULE_EXTENSION_CANDIDATES,
Some(PackageType::CommonJs) => &NODE16_COMMONJS_EXTENSION_CANDIDATES,
None => {
if self.allow_js {
&TS_JS_EXTENSION_CANDIDATES
} else {
&TS_EXTENSION_CANDIDATES
}
}
}
}
ModuleResolutionKind::Classic => {
if self.allow_js {
&TS_JS_EXTENSION_CANDIDATES
} else {
&CLASSIC_EXTENSION_CANDIDATES
}
}
_ => {
if self.allow_js {
&TS_JS_EXTENSION_CANDIDATES
} else {
&TS_EXTENSION_CANDIDATES
}
}
}
}
fn try_file_or_directory(&self, path: &Path) -> Option<PathBuf> {
if let Some(resolved) = self.try_file(path) {
return Some(resolved);
}
if path.is_dir() {
let package_json_path = path.join("package.json");
if package_json_path.exists()
&& let Ok(pj) = self.read_package_json(&package_json_path)
{
if let Some(types) = pj.types.or(pj.typings) {
let types_path = path.join(&types);
if let Some(resolved) = self.try_file(&types_path) {
return Some(resolved);
}
if types_path.is_file() {
return Some(types_path);
}
}
if let Some(main) = &pj.main {
let main_path = path.join(main);
if let Some(resolved) = self.try_file(&main_path) {
return Some(resolved);
}
}
}
let index = path.join("index");
return self.try_file(&index);
}
None
}
fn try_export_target(&self, path: &Path) -> Option<PathBuf> {
if let Some(extension) = path.extension().and_then(|ext| ext.to_str()) {
if split_path_extension(path).is_some() {
if path.is_file() {
return Some(path.to_path_buf());
}
if let Some(rewritten) = node16_extension_substitution(path, extension) {
for candidate in &rewritten {
if candidate.is_file() {
return Some(candidate.clone());
}
}
}
return None;
}
if self.allow_arbitrary_extensions
&& let Some(resolved) = try_arbitrary_extension_declaration(path, extension)
{
return Some(resolved);
}
return None;
}
if let Some(resolved) = self.try_file(path) {
return Some(resolved);
}
if path.is_dir() {
let index = path.join("index");
return self.try_file(&index);
}
None
}
fn read_package_json(&self, path: &Path) -> Result<PackageJson, String> {
let content = std::fs::read_to_string(path)
.map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
serde_json::from_str(&content)
.map_err(|e| format!("Failed to parse {}: {}", path.display(), e))
}
pub fn probe_js_file(
&mut self,
specifier: &str,
containing_file: &Path,
specifier_span: Span,
import_kind: ImportKind,
) -> Option<PathBuf> {
if self.allow_js {
return None; }
let containing_dir = containing_file
.parent()
.unwrap_or_else(|| Path::new("."))
.to_path_buf();
let containing_file_str = containing_file.display().to_string();
let importing_module_kind = self.get_importing_module_kind(containing_file);
self.allow_js = true;
let (result, _) = self.resolve_uncached(
specifier,
&containing_dir,
&containing_file_str,
specifier_span,
importing_module_kind,
import_kind,
);
self.allow_js = false;
match result {
Ok(resolved)
if matches!(
resolved.extension,
ModuleExtension::Js
| ModuleExtension::Jsx
| ModuleExtension::Mjs
| ModuleExtension::Cjs
) =>
{
Some(resolved.resolved_path)
}
_ => None,
}
}
pub fn clear_cache(&mut self) {
self.resolution_cache.clear();
}
pub const fn resolution_kind(&self) -> ModuleResolutionKind {
self.resolution_kind
}
pub fn emit_resolution_error(
&self,
diagnostics: &mut DiagnosticBag,
failure: &ResolutionFailure,
) {
let diagnostic = failure.to_diagnostic();
diagnostics.add(diagnostic);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_package_specifier_simple() {
let (name, subpath) = parse_package_specifier("lodash");
assert_eq!(name, "lodash");
assert_eq!(subpath, None);
}
#[test]
fn test_parse_package_specifier_with_subpath() {
let (name, subpath) = parse_package_specifier("lodash/fp");
assert_eq!(name, "lodash");
assert_eq!(subpath, Some("fp".to_string()));
}
#[test]
fn test_parse_package_specifier_scoped() {
let (name, subpath) = parse_package_specifier("@babel/core");
assert_eq!(name, "@babel/core");
assert_eq!(subpath, None);
}
#[test]
fn test_parse_package_specifier_scoped_with_subpath() {
let (name, subpath) = parse_package_specifier("@babel/core/transform");
assert_eq!(name, "@babel/core");
assert_eq!(subpath, Some("transform".to_string()));
}
#[test]
fn test_match_export_pattern_exact() {
assert_eq!(match_export_pattern("./lib", "./lib"), Some(String::new()));
assert_eq!(match_export_pattern("./lib", "./src"), None);
}
#[test]
fn test_match_export_pattern_wildcard() {
assert_eq!(
match_export_pattern("./*", "./foo"),
Some("foo".to_string())
);
assert_eq!(
match_export_pattern("./lib/*", "./lib/utils"),
Some("utils".to_string())
);
assert_eq!(match_export_pattern("./lib/*", "./src/utils"), None);
}
#[test]
fn test_module_extension_from_path() {
assert_eq!(
ModuleExtension::from_path(Path::new("foo.ts")),
ModuleExtension::Ts
);
assert_eq!(
ModuleExtension::from_path(Path::new("foo.d.ts")),
ModuleExtension::Dts
);
assert_eq!(
ModuleExtension::from_path(Path::new("foo.tsx")),
ModuleExtension::Tsx
);
assert_eq!(
ModuleExtension::from_path(Path::new("foo.js")),
ModuleExtension::Js
);
}
#[test]
fn test_module_resolver_creation() {
let resolver = ModuleResolver::node_resolver();
assert_eq!(resolver.resolution_kind(), ModuleResolutionKind::Node);
}
#[test]
fn test_ts2307_error_code_constant() {
assert_eq!(CANNOT_FIND_MODULE, 2307);
}
#[test]
fn test_resolution_failure_not_found_diagnostic() {
let failure = ResolutionFailure::NotFound {
specifier: "./missing-module".to_string(),
containing_file: "/path/to/file.ts".to_string(),
span: Span::new(10, 30),
};
let diagnostic = failure.to_diagnostic();
assert_eq!(diagnostic.code, CANNOT_FIND_MODULE);
assert!(diagnostic.message.contains("Cannot find module"));
assert!(diagnostic.message.contains("./missing-module"));
assert_eq!(diagnostic.file_name, "/path/to/file.ts");
assert_eq!(diagnostic.span.start, 10);
assert_eq!(diagnostic.span.end, 30);
}
#[test]
fn test_resolution_failure_is_not_found() {
let not_found = ResolutionFailure::NotFound {
specifier: "test".to_string(),
containing_file: "test.ts".to_string(),
span: Span::dummy(),
};
assert!(not_found.is_not_found());
let other = ResolutionFailure::InvalidSpecifier {
message: "test".to_string(),
containing_file: "test.ts".to_string(),
span: Span::dummy(),
};
assert!(!other.is_not_found());
}
#[test]
fn test_module_extension_forces_esm() {
assert!(ModuleExtension::Mts.forces_esm());
assert!(ModuleExtension::Mjs.forces_esm());
assert!(ModuleExtension::DmTs.forces_esm());
assert!(!ModuleExtension::Ts.forces_esm());
assert!(!ModuleExtension::Cts.forces_esm());
}
#[test]
fn test_module_extension_forces_cjs() {
assert!(ModuleExtension::Cts.forces_cjs());
assert!(ModuleExtension::Cjs.forces_cjs());
assert!(ModuleExtension::DCts.forces_cjs());
assert!(!ModuleExtension::Ts.forces_cjs());
assert!(!ModuleExtension::Mts.forces_cjs());
}
#[test]
fn test_match_imports_pattern_exact() {
assert_eq!(
match_imports_pattern("#utils", "#utils"),
Some(String::new())
);
assert_eq!(match_imports_pattern("#utils", "#other"), None);
}
#[test]
fn test_match_imports_pattern_wildcard() {
assert_eq!(
match_imports_pattern("#utils/*", "#utils/foo"),
Some("foo".to_string())
);
assert_eq!(
match_imports_pattern("#internal/*", "#internal/helpers/bar"),
Some("helpers/bar".to_string())
);
assert_eq!(match_imports_pattern("#utils/*", "#other/foo"), None);
}
#[test]
fn test_match_types_versions_pattern() {
assert_eq!(
match_types_versions_pattern("*", "index"),
Some("index".to_string())
);
assert_eq!(
match_types_versions_pattern("lib/*", "lib/utils"),
Some("utils".to_string())
);
assert_eq!(
match_types_versions_pattern("exact", "exact"),
Some(String::new())
);
assert_eq!(match_types_versions_pattern("lib/*", "src/utils"), None);
}
#[test]
fn test_apply_wildcard_substitution() {
assert_eq!(
apply_wildcard_substitution("./lib/*.js", "utils"),
"./lib/utils.js"
);
assert_eq!(
apply_wildcard_substitution("./dist/index.js", "ignored"),
"./dist/index.js"
);
}
#[test]
fn test_package_type_enum() {
assert_eq!(PackageType::default(), PackageType::CommonJs);
assert_ne!(PackageType::Module, PackageType::CommonJs);
}
#[test]
fn test_importing_module_kind_enum() {
assert_eq!(
ImportingModuleKind::default(),
ImportingModuleKind::CommonJs
);
assert_ne!(ImportingModuleKind::Esm, ImportingModuleKind::CommonJs);
}
#[test]
fn test_package_json_deserialize_basic() {
let json = r#"{"name": "test-package", "type": "module", "main": "./index.js"}"#;
let package_json: PackageJson = serde_json::from_str(json).unwrap();
assert_eq!(package_json.name, Some("test-package".to_string()));
assert_eq!(package_json.package_type, Some("module".to_string()));
assert_eq!(package_json.main, Some("./index.js".to_string()));
}
#[test]
fn test_package_json_deserialize_exports() {
let json = r#"{"name": "pkg", "exports": {"." : "./dist/index.js"}}"#;
let package_json: PackageJson = serde_json::from_str(json).unwrap();
assert!(package_json.exports.is_some());
}
#[test]
fn test_package_json_deserialize_types_versions() {
let json = serde_json::json!({
"name": "typed-package",
"typesVersions": {
"*": {
"*": ["./types/index.d.ts"]
}
}
});
let package_json: PackageJson = serde_json::from_value(json).unwrap();
assert_eq!(package_json.name, Some("typed-package".to_string()));
assert!(package_json.types_versions.is_some());
}
#[test]
fn test_emit_resolution_error_for_not_found() {
let mut diagnostics = DiagnosticBag::new();
let resolver = ModuleResolver::node_resolver();
let failure = ResolutionFailure::NotFound {
specifier: "./missing-module".to_string(),
containing_file: "/src/file.ts".to_string(),
span: Span::new(10, 30),
};
resolver.emit_resolution_error(&mut diagnostics, &failure);
assert_eq!(diagnostics.len(), 1);
assert!(diagnostics.has_errors());
let errors: Vec<_> = diagnostics.errors().collect();
assert_eq!(errors[0].code, CANNOT_FIND_MODULE);
assert!(errors[0].message.contains("Cannot find module"));
assert!(errors[0].message.contains("./missing-module"));
}
#[test]
fn test_emit_resolution_error_all_variants_emit_ts2307() {
let mut diagnostics = DiagnosticBag::new();
let resolver = ModuleResolver::node_resolver();
let failure = ResolutionFailure::InvalidSpecifier {
message: "bad specifier".to_string(),
containing_file: "/src/a.ts".to_string(),
span: Span::new(0, 10),
};
resolver.emit_resolution_error(&mut diagnostics, &failure);
assert_eq!(diagnostics.len(), 1);
let failure = ResolutionFailure::PackageJsonError {
message: "parse error".to_string(),
containing_file: "/src/b.ts".to_string(),
span: Span::new(5, 15),
};
resolver.emit_resolution_error(&mut diagnostics, &failure);
assert_eq!(diagnostics.len(), 2);
let failure = ResolutionFailure::CircularResolution {
message: "a -> b -> a".to_string(),
containing_file: "/src/c.ts".to_string(),
span: Span::new(10, 20),
};
resolver.emit_resolution_error(&mut diagnostics, &failure);
assert_eq!(diagnostics.len(), 3);
let failure = ResolutionFailure::PathMappingFailed {
message: "@/ pattern".to_string(),
containing_file: "/src/d.ts".to_string(),
span: Span::new(15, 25),
};
resolver.emit_resolution_error(&mut diagnostics, &failure);
assert_eq!(diagnostics.len(), 4);
for diag in diagnostics.errors() {
assert_eq!(diag.code, CANNOT_FIND_MODULE);
}
}
#[test]
fn test_resolution_failure_all_variants_to_diagnostic() {
let failures = vec![
ResolutionFailure::NotFound {
specifier: "./test".to_string(),
containing_file: "file.ts".to_string(),
span: Span::new(0, 10),
},
ResolutionFailure::InvalidSpecifier {
message: "bad".to_string(),
containing_file: "file2.ts".to_string(),
span: Span::new(5, 15),
},
ResolutionFailure::PackageJsonError {
message: "error".to_string(),
containing_file: "file3.ts".to_string(),
span: Span::new(10, 20),
},
ResolutionFailure::CircularResolution {
message: "loop".to_string(),
containing_file: "file4.ts".to_string(),
span: Span::new(15, 25),
},
ResolutionFailure::PathMappingFailed {
message: "@/path".to_string(),
containing_file: "file5.ts".to_string(),
span: Span::new(20, 30),
},
];
for failure in failures {
let diagnostic = failure.to_diagnostic();
assert_eq!(diagnostic.code, CANNOT_FIND_MODULE);
assert!(!diagnostic.file_name.is_empty());
assert!(diagnostic.span.start < diagnostic.span.end);
}
}
#[test]
fn test_relative_import_failure_produces_ts2307() {
let failure = ResolutionFailure::NotFound {
specifier: "./components/Button".to_string(),
containing_file: "/src/App.tsx".to_string(),
span: Span::new(20, 45),
};
let diagnostic = failure.to_diagnostic();
assert_eq!(diagnostic.code, CANNOT_FIND_MODULE);
assert_eq!(diagnostic.file_name, "/src/App.tsx");
assert!(diagnostic.message.contains("./components/Button"));
assert_eq!(diagnostic.span.start, 20);
assert_eq!(diagnostic.span.end, 45);
}
#[test]
fn test_bare_specifier_failure_produces_ts2307() {
let failure = ResolutionFailure::NotFound {
specifier: "nonexistent-package".to_string(),
containing_file: "/project/src/index.ts".to_string(),
span: Span::new(7, 28),
};
let diagnostic = failure.to_diagnostic();
assert_eq!(diagnostic.code, CANNOT_FIND_MODULE);
assert!(diagnostic.message.contains("nonexistent-package"));
}
#[test]
fn test_scoped_package_failure_produces_ts2307() {
let failure = ResolutionFailure::NotFound {
specifier: "@org/missing-lib".to_string(),
containing_file: "/app/main.ts".to_string(),
span: Span::new(15, 35),
};
let diagnostic = failure.to_diagnostic();
assert_eq!(diagnostic.code, CANNOT_FIND_MODULE);
assert!(diagnostic.message.contains("@org/missing-lib"));
}
#[test]
fn test_hash_import_failure_produces_ts2307() {
let failure = ResolutionFailure::NotFound {
specifier: "#utils/helpers".to_string(),
containing_file: "/pkg/src/index.ts".to_string(),
span: Span::new(8, 25),
};
let diagnostic = failure.to_diagnostic();
assert_eq!(diagnostic.code, CANNOT_FIND_MODULE);
assert!(diagnostic.message.contains("#utils/helpers"));
}
#[test]
fn test_resolution_failure_span_preservation() {
let test_cases = vec![(0, 10), (100, 150), (1000, 1050)];
for (start, end) in test_cases {
let failure = ResolutionFailure::NotFound {
specifier: "test".to_string(),
containing_file: "file.ts".to_string(),
span: Span::new(start, end),
};
let diagnostic = failure.to_diagnostic();
assert_eq!(diagnostic.span.start, start);
assert_eq!(diagnostic.span.end, end);
}
}
#[test]
fn test_resolution_failure_accessors() {
let failure = ResolutionFailure::InvalidSpecifier {
message: "test error".to_string(),
containing_file: "/src/test.ts".to_string(),
span: Span::new(10, 20),
};
assert_eq!(failure.containing_file(), "/src/test.ts");
assert_eq!(failure.span().start, 10);
assert_eq!(failure.span().end, 20);
}
#[test]
fn test_path_mapping_failure_produces_ts2307() {
let failure = ResolutionFailure::PathMappingFailed {
message: "path mapping '@/utils/*' did not resolve to any file".to_string(),
containing_file: "/project/src/index.ts".to_string(),
span: Span::new(8, 30),
};
let diagnostic = failure.to_diagnostic();
assert_eq!(diagnostic.code, CANNOT_FIND_MODULE);
assert_eq!(diagnostic.file_name, "/project/src/index.ts");
assert!(diagnostic.message.contains("Cannot find module"));
assert!(diagnostic.message.contains("path mapping"));
}
#[test]
fn test_package_json_error_produces_ts2307() {
let failure = ResolutionFailure::PackageJsonError {
message: "invalid exports field in package.json".to_string(),
containing_file: "/project/src/app.ts".to_string(),
span: Span::new(15, 45),
};
let diagnostic = failure.to_diagnostic();
assert_eq!(diagnostic.code, CANNOT_FIND_MODULE);
assert_eq!(diagnostic.file_name, "/project/src/app.ts");
assert!(diagnostic.message.contains("Cannot find module"));
}
#[test]
fn test_circular_resolution_produces_ts2307() {
let failure = ResolutionFailure::CircularResolution {
message: "circular dependency: a.ts -> b.ts -> a.ts".to_string(),
containing_file: "/project/src/a.ts".to_string(),
span: Span::new(20, 50),
};
let diagnostic = failure.to_diagnostic();
assert_eq!(diagnostic.code, CANNOT_FIND_MODULE);
assert_eq!(diagnostic.file_name, "/project/src/a.ts");
assert!(diagnostic.message.contains("Cannot find module"));
assert!(diagnostic.message.contains("circular"));
}
#[test]
fn test_diagnostic_bag_collects_multiple_resolution_errors() {
let mut diagnostics = DiagnosticBag::new();
let resolver = ModuleResolver::node_resolver();
let failures = vec![
ResolutionFailure::NotFound {
specifier: "./module1".to_string(),
containing_file: "a.ts".to_string(),
span: Span::new(0, 10),
},
ResolutionFailure::NotFound {
specifier: "./module2".to_string(),
containing_file: "b.ts".to_string(),
span: Span::new(5, 15),
},
ResolutionFailure::NotFound {
specifier: "external-pkg".to_string(),
containing_file: "c.ts".to_string(),
span: Span::new(10, 25),
},
];
for failure in &failures {
resolver.emit_resolution_error(&mut diagnostics, failure);
}
assert_eq!(diagnostics.len(), 3);
assert_eq!(diagnostics.error_count(), 3);
let codes: Vec<_> = diagnostics.errors().map(|d| d.code).collect();
assert!(codes.iter().all(|&c| c == CANNOT_FIND_MODULE));
}
#[test]
fn test_ts2834_error_code_constant() {
assert_eq!(IMPORT_PATH_NEEDS_EXTENSION, 2834);
}
#[test]
fn test_import_path_needs_extension_produces_ts2835() {
let failure = ResolutionFailure::ImportPathNeedsExtension {
specifier: "./utils".to_string(),
suggested_extension: ".js".to_string(),
containing_file: "/src/index.mts".to_string(),
span: Span::new(20, 30),
};
let diagnostic = failure.to_diagnostic();
assert_eq!(diagnostic.code, IMPORT_PATH_NEEDS_EXTENSION_SUGGESTION);
assert_eq!(diagnostic.file_name, "/src/index.mts");
assert!(
diagnostic
.message
.contains("Relative import paths need explicit file extensions")
);
assert!(diagnostic.message.contains("node16"));
assert!(diagnostic.message.contains("nodenext"));
assert!(diagnostic.message.contains("./utils.js"));
}
#[test]
fn test_import_path_needs_extension_suggests_mjs() {
let failure = ResolutionFailure::ImportPathNeedsExtension {
specifier: "./esm-module".to_string(),
suggested_extension: ".mjs".to_string(),
containing_file: "/src/app.mts".to_string(),
span: Span::new(10, 25),
};
let diagnostic = failure.to_diagnostic();
assert_eq!(diagnostic.code, IMPORT_PATH_NEEDS_EXTENSION_SUGGESTION);
assert!(diagnostic.message.contains("./esm-module.mjs"));
}
#[test]
fn test_import_path_needs_extension_suggests_cjs() {
let failure = ResolutionFailure::ImportPathNeedsExtension {
specifier: "./cjs-module".to_string(),
suggested_extension: ".cjs".to_string(),
containing_file: "/src/legacy.cts".to_string(),
span: Span::new(5, 20),
};
let diagnostic = failure.to_diagnostic();
assert_eq!(diagnostic.code, IMPORT_PATH_NEEDS_EXTENSION_SUGGESTION);
assert!(diagnostic.message.contains("./cjs-module.cjs"));
}
#[test]
fn test_ts2792_error_code_constant() {
assert_eq!(MODULE_RESOLUTION_MODE_MISMATCH, 2792);
}
#[test]
fn test_module_resolution_mode_mismatch_produces_ts2792() {
let failure = ResolutionFailure::ModuleResolutionModeMismatch {
specifier: "modern-esm-package".to_string(),
containing_file: "/src/index.ts".to_string(),
span: Span::new(15, 35),
};
let diagnostic = failure.to_diagnostic();
assert_eq!(diagnostic.code, MODULE_RESOLUTION_MODE_MISMATCH);
assert_eq!(diagnostic.file_name, "/src/index.ts");
assert!(
diagnostic
.message
.contains("Cannot find module 'modern-esm-package'")
);
assert!(diagnostic.message.contains("moduleResolution"));
assert!(diagnostic.message.contains("nodenext"));
assert!(diagnostic.message.contains("paths"));
}
#[test]
fn test_module_resolution_mode_mismatch_accessors() {
let failure = ResolutionFailure::ModuleResolutionModeMismatch {
specifier: "pkg".to_string(),
containing_file: "/test.ts".to_string(),
span: Span::new(100, 110),
};
assert_eq!(failure.containing_file(), "/test.ts");
assert_eq!(failure.span().start, 100);
assert_eq!(failure.span().end, 110);
}
#[test]
fn test_import_path_needs_extension_accessors() {
let failure = ResolutionFailure::ImportPathNeedsExtension {
specifier: "./foo".to_string(),
suggested_extension: ".js".to_string(),
containing_file: "/bar.mts".to_string(),
span: Span::new(50, 60),
};
assert_eq!(failure.containing_file(), "/bar.mts");
assert_eq!(failure.span().start, 50);
assert_eq!(failure.span().end, 60);
}
#[test]
fn test_new_error_codes_emit_correctly() {
let mut diagnostics = DiagnosticBag::new();
let resolver = ModuleResolver::node_resolver();
let failure_2835 = ResolutionFailure::ImportPathNeedsExtension {
specifier: "./utils".to_string(),
suggested_extension: ".js".to_string(),
containing_file: "/src/app.mts".to_string(),
span: Span::new(0, 10),
};
resolver.emit_resolution_error(&mut diagnostics, &failure_2835);
let failure_2792 = ResolutionFailure::ModuleResolutionModeMismatch {
specifier: "esm-pkg".to_string(),
containing_file: "/src/index.ts".to_string(),
span: Span::new(5, 15),
};
resolver.emit_resolution_error(&mut diagnostics, &failure_2792);
assert_eq!(diagnostics.len(), 2);
let errors: Vec<_> = diagnostics.errors().collect();
assert_eq!(errors[0].code, IMPORT_PATH_NEEDS_EXTENSION_SUGGESTION);
assert_eq!(errors[1].code, MODULE_RESOLUTION_MODE_MISMATCH);
}
#[test]
fn test_extension_from_path_ts() {
assert_eq!(
ModuleExtension::from_path(Path::new("foo.ts")),
ModuleExtension::Ts
);
}
#[test]
fn test_extension_from_path_tsx() {
assert_eq!(
ModuleExtension::from_path(Path::new("Component.tsx")),
ModuleExtension::Tsx
);
}
#[test]
fn test_extension_from_path_dts() {
assert_eq!(
ModuleExtension::from_path(Path::new("types.d.ts")),
ModuleExtension::Dts
);
}
#[test]
fn test_extension_from_path_dmts() {
assert_eq!(
ModuleExtension::from_path(Path::new("types.d.mts")),
ModuleExtension::DmTs
);
}
#[test]
fn test_extension_from_path_dcts() {
assert_eq!(
ModuleExtension::from_path(Path::new("types.d.cts")),
ModuleExtension::DCts
);
}
#[test]
fn test_extension_from_path_js() {
assert_eq!(
ModuleExtension::from_path(Path::new("bundle.js")),
ModuleExtension::Js
);
}
#[test]
fn test_extension_from_path_jsx() {
assert_eq!(
ModuleExtension::from_path(Path::new("App.jsx")),
ModuleExtension::Jsx
);
}
#[test]
fn test_extension_from_path_mjs() {
assert_eq!(
ModuleExtension::from_path(Path::new("module.mjs")),
ModuleExtension::Mjs
);
}
#[test]
fn test_extension_from_path_cjs() {
assert_eq!(
ModuleExtension::from_path(Path::new("config.cjs")),
ModuleExtension::Cjs
);
}
#[test]
fn test_extension_from_path_mts() {
assert_eq!(
ModuleExtension::from_path(Path::new("utils.mts")),
ModuleExtension::Mts
);
}
#[test]
fn test_extension_from_path_cts() {
assert_eq!(
ModuleExtension::from_path(Path::new("config.cts")),
ModuleExtension::Cts
);
}
#[test]
fn test_extension_from_path_json() {
assert_eq!(
ModuleExtension::from_path(Path::new("package.json")),
ModuleExtension::Json
);
}
#[test]
fn test_extension_from_path_unknown() {
assert_eq!(
ModuleExtension::from_path(Path::new("style.css")),
ModuleExtension::Unknown
);
}
#[test]
fn test_extension_from_path_no_extension() {
assert_eq!(
ModuleExtension::from_path(Path::new("Makefile")),
ModuleExtension::Unknown
);
}
#[test]
fn test_extension_from_path_nested() {
assert_eq!(
ModuleExtension::from_path(Path::new("/project/src/lib/types.d.ts")),
ModuleExtension::Dts
);
}
#[test]
fn test_extension_as_str_roundtrip() {
let extensions = [
ModuleExtension::Ts,
ModuleExtension::Tsx,
ModuleExtension::Dts,
ModuleExtension::DmTs,
ModuleExtension::DCts,
ModuleExtension::Js,
ModuleExtension::Jsx,
ModuleExtension::Mjs,
ModuleExtension::Cjs,
ModuleExtension::Mts,
ModuleExtension::Cts,
ModuleExtension::Json,
];
for ext in &extensions {
let ext_str = ext.as_str();
assert!(
!ext_str.is_empty(),
"{ext:?} should have a non-empty string representation"
);
assert!(
ext_str.starts_with('.'),
"{ext:?}.as_str() should start with '.', got: {ext_str}"
);
}
assert_eq!(ModuleExtension::Unknown.as_str(), "");
}
#[test]
fn test_extension_forces_esm() {
assert!(ModuleExtension::Mts.forces_esm());
assert!(ModuleExtension::Mjs.forces_esm());
assert!(ModuleExtension::DmTs.forces_esm());
assert!(!ModuleExtension::Ts.forces_esm());
assert!(!ModuleExtension::Tsx.forces_esm());
assert!(!ModuleExtension::Dts.forces_esm());
assert!(!ModuleExtension::Js.forces_esm());
assert!(!ModuleExtension::Cjs.forces_esm());
assert!(!ModuleExtension::Cts.forces_esm());
}
#[test]
fn test_extension_forces_cjs() {
assert!(ModuleExtension::Cts.forces_cjs());
assert!(ModuleExtension::Cjs.forces_cjs());
assert!(ModuleExtension::DCts.forces_cjs());
assert!(!ModuleExtension::Ts.forces_cjs());
assert!(!ModuleExtension::Tsx.forces_cjs());
assert!(!ModuleExtension::Dts.forces_cjs());
assert!(!ModuleExtension::Js.forces_cjs());
assert!(!ModuleExtension::Mjs.forces_cjs());
assert!(!ModuleExtension::Mts.forces_cjs());
}
#[test]
fn test_extension_neutral_mode() {
let neutral = [
ModuleExtension::Ts,
ModuleExtension::Tsx,
ModuleExtension::Dts,
ModuleExtension::Js,
ModuleExtension::Jsx,
ModuleExtension::Json,
ModuleExtension::Unknown,
];
for ext in &neutral {
assert!(
!ext.forces_esm() && !ext.forces_cjs(),
"{ext:?} should be neutral (neither ESM nor CJS)"
);
}
}
#[test]
fn test_resolution_failure_not_found_is_not_found() {
let failure = ResolutionFailure::NotFound {
specifier: "./missing".to_string(),
containing_file: "main.ts".to_string(),
span: Span::new(0, 10),
};
assert!(failure.is_not_found());
}
#[test]
fn test_resolution_failure_other_is_not_not_found() {
let failure = ResolutionFailure::ImportPathNeedsExtension {
specifier: "./utils".to_string(),
suggested_extension: ".js".to_string(),
containing_file: "main.mts".to_string(),
span: Span::new(0, 10),
};
assert!(!failure.is_not_found());
}
#[test]
fn test_resolution_failure_containing_file() {
let failure = ResolutionFailure::NotFound {
specifier: "./missing".to_string(),
containing_file: "/project/src/main.ts".to_string(),
span: Span::new(5, 20),
};
assert_eq!(failure.containing_file(), "/project/src/main.ts");
}
#[test]
fn test_resolution_failure_span() {
let failure = ResolutionFailure::NotFound {
specifier: "./missing".to_string(),
containing_file: "main.ts".to_string(),
span: Span::new(10, 30),
};
let span = failure.span();
assert_eq!(span.start, 10);
assert_eq!(span.end, 30);
}
#[test]
fn test_resolution_failure_to_diagnostic_ts2307() {
let failure = ResolutionFailure::NotFound {
specifier: "./nonexistent".to_string(),
containing_file: "main.ts".to_string(),
span: Span::new(0, 20),
};
let diag = failure.to_diagnostic();
assert_eq!(diag.code, CANNOT_FIND_MODULE);
assert!(diag.message.contains("./nonexistent"));
}
#[test]
fn test_resolution_failure_to_diagnostic_ts2835() {
let failure = ResolutionFailure::ImportPathNeedsExtension {
specifier: "./utils".to_string(),
suggested_extension: ".js".to_string(),
containing_file: "app.mts".to_string(),
span: Span::new(0, 15),
};
let diag = failure.to_diagnostic();
assert_eq!(diag.code, IMPORT_PATH_NEEDS_EXTENSION_SUGGESTION);
}
#[test]
fn test_resolution_failure_to_diagnostic_ts2792() {
let failure = ResolutionFailure::ModuleResolutionModeMismatch {
specifier: "some-esm-pkg".to_string(),
containing_file: "index.ts".to_string(),
span: Span::new(0, 20),
};
let diag = failure.to_diagnostic();
assert_eq!(diag.code, MODULE_RESOLUTION_MODE_MISMATCH);
}
#[test]
fn test_resolver_relative_ts_file() {
use std::fs;
let dir = std::env::temp_dir().join("tsz_test_resolver_relative");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
fs::write(dir.join("main.ts"), "import { foo } from './utils';").unwrap();
fs::write(dir.join("utils.ts"), "export const foo = 42;").unwrap();
let mut resolver = ModuleResolver::node_resolver();
let result = resolver.resolve("./utils", &dir.join("main.ts"), Span::new(0, 10));
match result {
Ok(module) => {
assert_eq!(module.resolved_path, dir.join("utils.ts"));
assert_eq!(module.extension, ModuleExtension::Ts);
assert!(!module.is_external);
}
Err(_) => {
}
}
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn test_resolver_relative_tsx_file() {
use std::fs;
let dir = std::env::temp_dir().join("tsz_test_resolver_tsx");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
fs::write(dir.join("app.ts"), "").unwrap();
fs::write(
dir.join("Button.tsx"),
"export default function Button() {}",
)
.unwrap();
let mut resolver = ModuleResolver::node_resolver();
let result = resolver.resolve("./Button", &dir.join("app.ts"), Span::new(0, 10));
if let Ok(module) = result {
assert_eq!(module.resolved_path, dir.join("Button.tsx"));
assert_eq!(module.extension, ModuleExtension::Tsx);
}
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn test_resolver_index_file() {
use std::fs;
let dir = std::env::temp_dir().join("tsz_test_resolver_index");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(dir.join("utils")).unwrap();
fs::write(dir.join("main.ts"), "").unwrap();
fs::write(dir.join("utils").join("index.ts"), "export const foo = 42;").unwrap();
let mut resolver = ModuleResolver::node_resolver();
let result = resolver.resolve("./utils", &dir.join("main.ts"), Span::new(0, 10));
if let Ok(module) = result {
assert_eq!(module.resolved_path, dir.join("utils").join("index.ts"));
assert_eq!(module.extension, ModuleExtension::Ts);
}
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn test_exports_js_target_substitutes_dts() {
use std::fs;
let dir = std::env::temp_dir().join("tsz_test_exports_js_target");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(dir.join("node_modules/pkg")).unwrap();
fs::create_dir_all(dir.join("src")).unwrap();
fs::write(
dir.join("node_modules/pkg/package.json"),
r#"{"name":"pkg","version":"0.0.1","exports":"./entrypoint.js"}"#,
)
.unwrap();
fs::write(dir.join("node_modules/pkg/entrypoint.d.ts"), "export {};").unwrap();
fs::write(dir.join("src/index.ts"), "import * as p from 'pkg';").unwrap();
let options = ResolvedCompilerOptions {
module_resolution: Some(ModuleResolutionKind::Node16),
resolve_package_json_exports: true,
..Default::default()
};
let mut resolver = ModuleResolver::new(&options);
let result = resolver.resolve("pkg", &dir.join("src/index.ts"), Span::new(0, 3));
let resolved =
result.expect("Expected exports .js target to resolve via .d.ts substitution");
assert!(resolved.resolved_path.ends_with("entrypoint.d.ts"));
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn test_resolver_dts_file() {
use std::fs;
let dir = std::env::temp_dir().join("tsz_test_resolver_dts");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
fs::write(dir.join("main.ts"), "").unwrap();
fs::write(dir.join("types.d.ts"), "export interface Foo {}").unwrap();
let mut resolver = ModuleResolver::node_resolver();
let result = resolver.resolve("./types", &dir.join("main.ts"), Span::new(0, 10));
if let Ok(module) = result {
assert_eq!(module.resolved_path, dir.join("types.d.ts"));
assert_eq!(module.extension, ModuleExtension::Dts);
}
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn test_resolver_jsx_without_jsx_option_errors() {
use std::fs;
let dir = std::env::temp_dir().join("tsz_test_resolver_jsx_no_option");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
fs::write(dir.join("app.ts"), "import jsx from './jsx';").unwrap();
fs::write(dir.join("jsx.jsx"), "export default 1;").unwrap();
let options = ResolvedCompilerOptions {
allow_js: true,
jsx: None,
module_resolution: Some(ModuleResolutionKind::Node),
..Default::default()
};
let mut resolver = ModuleResolver::new(&options);
let result = resolver.resolve("./jsx", &dir.join("app.ts"), Span::new(0, 10));
let failure = result.expect_err("Expected jsx resolution to fail without jsx option");
let diagnostic = failure.to_diagnostic();
assert_eq!(diagnostic.code, 6142);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn test_resolver_tsx_without_jsx_option_errors() {
use std::fs;
let dir = std::env::temp_dir().join("tsz_test_resolver_tsx_no_option");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
fs::write(dir.join("app.ts"), "import tsx from './tsx';").unwrap();
fs::write(dir.join("tsx.tsx"), "export default 1;").unwrap();
let options = ResolvedCompilerOptions {
jsx: None,
module_resolution: Some(ModuleResolutionKind::Node),
..Default::default()
};
let mut resolver = ModuleResolver::new(&options);
let result = resolver.resolve("./tsx", &dir.join("app.ts"), Span::new(0, 10));
let failure = result.expect_err("Expected tsx resolution to fail without jsx option");
let diagnostic = failure.to_diagnostic();
assert_eq!(diagnostic.code, 6142);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn test_json_import_without_resolve_json_module() {
use std::fs;
let dir = std::env::temp_dir().join("tsz_test_ts2732");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
fs::write(dir.join("app.ts"), "import data from './data.json';").unwrap();
fs::write(dir.join("data.json"), "{\"value\": 42}").unwrap();
let options = ResolvedCompilerOptions {
resolve_json_module: false, ..Default::default()
};
let mut resolver = ModuleResolver::new(&options);
let result = resolver.resolve("./data.json", &dir.join("app.ts"), Span::new(0, 10));
let failure =
result.expect_err("Expected JSON resolution to fail without resolveJsonModule");
let diagnostic = failure.to_diagnostic();
assert_eq!(diagnostic.code, 2732);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn test_resolver_package_main_with_unknown_extension() {
use std::fs;
let dir = std::env::temp_dir().join("tsz_test_resolver_main_unknown");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(dir.join("node_modules").join("normalize.css")).unwrap();
fs::write(dir.join("app.ts"), "import 'normalize.css';").unwrap();
fs::write(
dir.join("node_modules")
.join("normalize.css")
.join("normalize.css"),
"body {}",
)
.unwrap();
fs::write(
dir.join("node_modules")
.join("normalize.css")
.join("package.json"),
r#"{ "main": "normalize.css" }"#,
)
.unwrap();
let mut resolver = ModuleResolver::node_resolver();
let result = resolver.resolve("normalize.css", &dir.join("app.ts"), Span::new(0, 10));
assert!(
result.is_ok(),
"Expected package main with unknown extension to resolve"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn test_resolver_package_types_with_unknown_extension() {
use std::fs;
let dir = std::env::temp_dir().join("tsz_test_resolver_types_unknown");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(dir.join("node_modules").join("foo")).unwrap();
fs::write(dir.join("app.ts"), "import 'foo';").unwrap();
fs::write(
dir.join("node_modules").join("foo").join("foo.js"),
"module.exports = {};",
)
.unwrap();
fs::write(
dir.join("node_modules").join("foo").join("package.json"),
r#"{ "types": "foo.js" }"#,
)
.unwrap();
let mut resolver = ModuleResolver::node_resolver();
let result = resolver.resolve("foo", &dir.join("app.ts"), Span::new(0, 10));
assert!(
result.is_ok(),
"Expected package types with unknown extension to resolve"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn test_resolver_package_types_js_without_allow_js_resolves() {
use std::fs;
let dir = std::env::temp_dir().join("tsz_test_resolver_types_js");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(dir.join("node_modules").join("foo")).unwrap();
fs::write(dir.join("app.ts"), "import 'foo';").unwrap();
fs::write(
dir.join("node_modules").join("foo").join("foo.js"),
"module.exports = {};",
)
.unwrap();
fs::write(
dir.join("node_modules").join("foo").join("package.json"),
r#"{ "types": "foo.js" }"#,
)
.unwrap();
let mut resolver = ModuleResolver::node_resolver();
let result = resolver.resolve("foo", &dir.join("app.ts"), Span::new(0, 10));
assert!(
result.is_ok(),
"Expected types .js to resolve even without allowJs"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn test_resolver_missing_file() {
use std::fs;
let dir = std::env::temp_dir().join("tsz_test_resolver_missing");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
fs::write(dir.join("main.ts"), "").unwrap();
let mut resolver = ModuleResolver::node_resolver();
let result = resolver.resolve("./nonexistent", &dir.join("main.ts"), Span::new(0, 10));
assert!(result.is_err(), "Missing file should produce error");
if let Err(failure) = result {
assert!(failure.is_not_found());
}
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn test_package_type_default_is_commonjs() {
assert_eq!(PackageType::default(), PackageType::CommonJs);
}
#[test]
fn test_importing_module_kind_default_is_commonjs() {
assert_eq!(
ImportingModuleKind::default(),
ImportingModuleKind::CommonJs
);
}
}