Skip to main content

tsz_core/
module_resolver.rs

1//! Module Resolution Implementation
2//!
3//! This module implements TypeScript's module resolution algorithms:
4//! - Node (classic Node.js resolution)
5//! - Node16/NodeNext (modern Node.js with ESM support)
6//! - Bundler (for webpack/vite-style resolution)
7//!
8//! The resolver handles:
9//! - Relative imports (./foo, ../bar)
10//! - Bare specifiers (lodash, @scope/pkg)
11//! - Path mapping from tsconfig (paths, baseUrl)
12//! - Package.json exports/imports fields
13//! - TypeScript-specific extensions (.ts, .tsx, .d.ts)
14//!
15//! Resolver invariants:
16//! - Module existence truth comes from `resolve_with_kind` outcomes.
17//! - Diagnostic code selection for module-not-found family (TS2307/TS2792/TS2834/TS2835/TS5097/TS2732)
18//!   is owned here and propagated to checker via resolution records.
19//! - Callers should not recompute not-found codes/messages from partial checker state.
20
21use crate::config::{JsxEmit, ModuleResolutionKind, PathMapping, ResolvedCompilerOptions};
22use crate::diagnostics::{Diagnostic, DiagnosticBag};
23use crate::emitter::ModuleKind;
24use crate::module_resolver_helpers::*;
25use crate::span::Span;
26use rustc_hash::FxHashMap;
27use serde_json;
28use std::path::{Path, PathBuf};
29
30/// TS2307: Cannot find module
31///
32/// This error code is emitted when a module specifier cannot be resolved.
33/// Example: `import { foo } from './missing-module'`
34///
35/// Usage example:
36/// ```ignore
37/// let mut resolver = ModuleResolver::new(&options);
38/// let mut diagnostics = DiagnosticBag::new();
39///
40/// match resolver.resolve("./missing-module", containing_file, specifier_span) {
41///     Ok(module) => { /* use module */ }
42///     Err(failure) => {
43///         resolver.emit_resolution_error(&mut diagnostics, &failure);
44///     }
45/// }
46/// ```
47pub const CANNOT_FIND_MODULE: u32 = 2307;
48
49/// TS2792: Cannot find module. Did you mean to set the 'moduleResolution' option to 'nodenext'?
50///
51/// This error code is emitted when a module specifier cannot be resolved under the current
52/// module resolution mode, but the package.json has an 'exports' field that would likely
53/// resolve correctly under Node16/NodeNext/Bundler mode.
54pub const MODULE_RESOLUTION_MODE_MISMATCH: u32 = 2792;
55
56/// TS2732: Cannot find module. Consider using '--resolveJsonModule' to import module with '.json' extension.
57///
58/// This error code is emitted when trying to import a .json file without the resolveJsonModule
59/// compiler option enabled. Unlike TS2307 (generic cannot find module), this error provides
60/// specific guidance to enable JSON module support.
61/// Example: `import data from './config.json'` without resolveJsonModule enabled
62pub const JSON_MODULE_WITHOUT_RESOLVE_JSON_MODULE: u32 = 2732;
63
64/// TS2834: Relative import paths need explicit file extensions in `EcmaScript` imports
65///
66/// This error code is emitted when a relative import in an ESM context under Node16/NodeNext
67/// resolution mode does not include an explicit file extension. ESM requires explicit extensions.
68/// Example: `import { foo } from './utils'` should be `import { foo } from './utils.js'`
69pub const IMPORT_PATH_NEEDS_EXTENSION: u32 = 2834;
70pub const IMPORT_PATH_NEEDS_EXTENSION_SUGGESTION: u32 = 2835;
71pub const IMPORT_PATH_TS_EXTENSION_NOT_ALLOWED: u32 = 5097;
72pub const MODULE_WAS_RESOLVED_TO_BUT_JSX_NOT_SET: u32 = 6142;
73
74/// Result of module resolution
75#[derive(Debug, Clone, PartialEq, Eq)]
76pub struct ResolvedModule {
77    /// Resolved file path
78    pub resolved_path: PathBuf,
79    /// Whether the module is an external package (from `node_modules`)
80    pub is_external: bool,
81    /// Package name if resolved from `node_modules`
82    pub package_name: Option<String>,
83    /// Original specifier used in import
84    pub original_specifier: String,
85    /// Extension of the resolved file
86    pub extension: ModuleExtension,
87}
88
89/// Module file extensions TypeScript can resolve
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub enum ModuleExtension {
92    Ts,
93    Tsx,
94    Dts,
95    DmTs,
96    DCts,
97    Js,
98    Jsx,
99    Mjs,
100    Cjs,
101    Mts,
102    Cts,
103    Json,
104    Unknown,
105}
106
107/// Package type from package.json "type" field
108/// Used for ESM vs CommonJS distinction in Node16/NodeNext
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
110pub enum PackageType {
111    /// ESM package ("type": "module")
112    Module,
113    /// CommonJS package ("type": "commonjs" or default)
114    #[default]
115    CommonJs,
116}
117
118/// Module kind for the importing file
119/// Determines whether to use "import" or "require" conditions
120#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
121pub enum ImportingModuleKind {
122    /// ESM module (uses "import" condition)
123    Esm,
124    /// CommonJS module (uses "require" condition)
125    #[default]
126    CommonJs,
127}
128
129/// Import syntax kind - determines which error codes to use
130/// for extensionless imports in Node16/NodeNext resolution.
131#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
132pub enum ImportKind {
133    /// ESM static import: `import { x } from "./foo"`
134    #[default]
135    EsmImport,
136    /// Dynamic import: `import("./foo")` - always ESM regardless of file type
137    DynamicImport,
138    /// CommonJS require: `import x = require("./foo")` or `require("./foo")`
139    CjsRequire,
140    /// Re-export: `export { x } from "./foo"`
141    EsmReExport,
142}
143
144impl ModuleExtension {
145    /// Parse extension from file path
146    pub fn from_path(path: &Path) -> Self {
147        let path_str = path.to_string_lossy();
148
149        // Check compound extensions first
150        if path_str.ends_with(".d.ts") {
151            return Self::Dts;
152        }
153        if path_str.ends_with(".d.mts") {
154            return Self::DmTs;
155        }
156        if path_str.ends_with(".d.cts") {
157            return Self::DCts;
158        }
159
160        match path.extension().and_then(|e| e.to_str()) {
161            Some("ts") => Self::Ts,
162            Some("tsx") => Self::Tsx,
163            Some("js") => Self::Js,
164            Some("jsx") => Self::Jsx,
165            Some("mjs") => Self::Mjs,
166            Some("cjs") => Self::Cjs,
167            Some("mts") => Self::Mts,
168            Some("cts") => Self::Cts,
169            Some("json") => Self::Json,
170            _ => Self::Unknown,
171        }
172    }
173
174    /// Get the extension string
175    pub const fn as_str(&self) -> &'static str {
176        match self {
177            Self::Ts => ".ts",
178            Self::Tsx => ".tsx",
179            Self::Dts => ".d.ts",
180            Self::DmTs => ".d.mts",
181            Self::DCts => ".d.cts",
182            Self::Js => ".js",
183            Self::Jsx => ".jsx",
184            Self::Mjs => ".mjs",
185            Self::Cjs => ".cjs",
186            Self::Mts => ".mts",
187            Self::Cts => ".cts",
188            Self::Json => ".json",
189            Self::Unknown => "",
190        }
191    }
192
193    /// Check if this extension forces ESM mode
194    /// .mts, .mjs, .d.mts files are always ESM
195    pub const fn forces_esm(&self) -> bool {
196        matches!(self, Self::Mts | Self::Mjs | Self::DmTs)
197    }
198
199    /// Check if this extension forces CommonJS mode
200    /// .cts, .cjs, .d.cts files are always CommonJS
201    pub const fn forces_cjs(&self) -> bool {
202        matches!(self, Self::Cts | Self::Cjs | Self::DCts)
203    }
204}
205
206fn explicit_ts_extension(specifier: &str) -> Option<String> {
207    if specifier.ends_with(".d.ts")
208        || specifier.ends_with(".d.mts")
209        || specifier.ends_with(".d.cts")
210    {
211        return None;
212    }
213    for ext in [".ts", ".tsx", ".mts", ".cts"] {
214        if specifier.ends_with(ext) {
215            return Some(ext.to_string());
216        }
217    }
218    None
219}
220
221/// Reason why module resolution failed
222#[derive(Debug, Clone, PartialEq, Eq)]
223pub enum ResolutionFailure {
224    /// Module specifier not found
225    NotFound {
226        /// Module specifier that was not found
227        specifier: String,
228        /// File containing the import
229        containing_file: String,
230        /// Span of the module specifier in source
231        span: Span,
232    },
233    /// Invalid module specifier
234    InvalidSpecifier {
235        /// Error message describing why the specifier is invalid
236        message: String,
237        /// File containing the import
238        containing_file: String,
239        /// Span of the module specifier in source
240        span: Span,
241    },
242    /// Package.json not found or invalid
243    PackageJsonError {
244        /// Error message describing the package.json issue
245        message: String,
246        /// File containing the import
247        containing_file: String,
248        /// Span of the module specifier in source
249        span: Span,
250    },
251    /// Circular resolution detected
252    CircularResolution {
253        /// Error message describing the circular dependency
254        message: String,
255        /// File containing the import
256        containing_file: String,
257        /// Span of the module specifier in source
258        span: Span,
259    },
260    /// Path mapping did not resolve to a file
261    PathMappingFailed {
262        /// Error message describing the path mapping failure
263        message: String,
264        /// File containing the import
265        containing_file: String,
266        /// Span of the module specifier in source
267        span: Span,
268    },
269    /// TS2834: Relative import paths need explicit file extensions in `EcmaScript` imports
270    /// when '--moduleResolution' is 'node16' or 'nodenext'.
271    ImportPathNeedsExtension {
272        /// Module specifier that was used without an extension
273        specifier: String,
274        /// Suggested extension to add (e.g., ".js")
275        suggested_extension: String,
276        /// File containing the import
277        containing_file: String,
278        /// Span of the module specifier in source
279        span: Span,
280    },
281    /// TS5097: Import path ends with a TypeScript extension without allowImportingTsExtensions.
282    ImportingTsExtensionNotAllowed {
283        /// Extension that was used (e.g., ".ts")
284        extension: String,
285        /// File containing the import
286        containing_file: String,
287        /// Span of the module specifier in source
288        span: Span,
289    },
290    /// TS6142: Module resolved to JSX/TSX without jsx option enabled.
291    JsxNotEnabled {
292        /// Module specifier that was resolved
293        specifier: String,
294        /// Resolved file path
295        resolved_path: PathBuf,
296        /// File containing the import
297        containing_file: String,
298        /// Span of the module specifier in source
299        span: Span,
300    },
301    /// TS2792: Cannot find module. Did you mean to set the 'moduleResolution' option to 'nodenext'?
302    /// Emitted when package.json has 'exports' but resolution mode doesn't support it.
303    ModuleResolutionModeMismatch {
304        /// Module specifier that could not be resolved
305        specifier: String,
306        /// File containing the import
307        containing_file: String,
308        /// Span of the module specifier in source
309        span: Span,
310    },
311    /// TS2732: Cannot find module. Consider using '--resolveJsonModule' to import module with '.json' extension.
312    /// Emitted when trying to import a .json file without resolveJsonModule enabled.
313    JsonModuleWithoutResolveJsonModule {
314        /// Module specifier ending in .json
315        specifier: String,
316        /// File containing the import
317        containing_file: String,
318        /// Span of the module specifier in source
319        span: Span,
320    },
321}
322
323impl ResolutionFailure {
324    /// Convert a resolution failure to a diagnostic
325    ///
326    /// All resolution failure variants produce TS2307 diagnostics with proper
327    /// source location information for IDE integration and error reporting.
328    ///
329    /// The message format matches TypeScript's exactly:
330    /// "Cannot find module '{specifier}' or its corresponding type declarations."
331    pub fn to_diagnostic(&self) -> Diagnostic {
332        match self {
333            Self::NotFound {
334                specifier,
335                containing_file,
336                span,
337            } => Diagnostic::error(
338                containing_file,
339                *span,
340                format!("Cannot find module '{specifier}' or its corresponding type declarations.",),
341                CANNOT_FIND_MODULE,
342            ),
343            Self::InvalidSpecifier {
344                message,
345                containing_file,
346                span,
347            }
348            | Self::PackageJsonError {
349                message,
350                containing_file,
351                span,
352            }
353            | Self::CircularResolution {
354                message,
355                containing_file,
356                span,
357            }
358            | Self::PathMappingFailed {
359                message,
360                containing_file,
361                span,
362            } => Diagnostic::error(
363                containing_file,
364                *span,
365                format!("Cannot find module '{message}' or its corresponding type declarations.",),
366                CANNOT_FIND_MODULE,
367            ),
368            Self::ImportPathNeedsExtension {
369                specifier,
370                suggested_extension,
371                containing_file,
372                span,
373            } => {
374                if suggested_extension.is_empty() {
375                    // TS2834: No suggestion available
376                    Diagnostic::error(
377                        containing_file,
378                        *span,
379                        "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(),
380                        IMPORT_PATH_NEEDS_EXTENSION,
381                    )
382                } else {
383                    // TS2835: With extension suggestion
384                    Diagnostic::error(
385                        containing_file,
386                        *span,
387                        format!(
388                            "Relative import paths need explicit file extensions in EcmaScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean '{specifier}{suggested_extension}'?",
389                        ),
390                        IMPORT_PATH_NEEDS_EXTENSION_SUGGESTION,
391                    )
392                }
393            }
394            Self::ImportingTsExtensionNotAllowed {
395                extension,
396                containing_file,
397                span,
398            } => Diagnostic::error(
399                containing_file,
400                *span,
401                format!(
402                    "An import path can only end with a '{extension}' extension when 'allowImportingTsExtensions' is enabled.",
403                ),
404                IMPORT_PATH_TS_EXTENSION_NOT_ALLOWED,
405            ),
406            Self::JsxNotEnabled {
407                specifier,
408                resolved_path,
409                containing_file,
410                span,
411            } => Diagnostic::error(
412                containing_file,
413                *span,
414                format!(
415                    "Module '{}' was resolved to '{}', but '--jsx' is not set.",
416                    specifier,
417                    resolved_path.display()
418                ),
419                MODULE_WAS_RESOLVED_TO_BUT_JSX_NOT_SET,
420            ),
421            Self::ModuleResolutionModeMismatch {
422                specifier,
423                containing_file,
424                span,
425            } => Diagnostic::error(
426                containing_file,
427                *span,
428                format!(
429                    "Cannot find module '{specifier}'. Did you mean to set the 'moduleResolution' option to 'nodenext', or to add aliases to the 'paths' option?",
430                ),
431                MODULE_RESOLUTION_MODE_MISMATCH,
432            ),
433            Self::JsonModuleWithoutResolveJsonModule {
434                specifier,
435                containing_file,
436                span,
437            } => Diagnostic::error(
438                containing_file,
439                *span,
440                format!(
441                    "Cannot find module '{specifier}'. Consider using '--resolveJsonModule' to import module with '.json' extension.",
442                ),
443                JSON_MODULE_WITHOUT_RESOLVE_JSON_MODULE,
444            ),
445        }
446    }
447
448    /// Get the containing file for this resolution failure
449    pub fn containing_file(&self) -> &str {
450        match self {
451            Self::NotFound {
452                containing_file, ..
453            }
454            | Self::InvalidSpecifier {
455                containing_file, ..
456            }
457            | Self::PackageJsonError {
458                containing_file, ..
459            }
460            | Self::CircularResolution {
461                containing_file, ..
462            }
463            | Self::PathMappingFailed {
464                containing_file, ..
465            }
466            | Self::ImportPathNeedsExtension {
467                containing_file, ..
468            }
469            | Self::ImportingTsExtensionNotAllowed {
470                containing_file, ..
471            }
472            | Self::JsxNotEnabled {
473                containing_file, ..
474            }
475            | Self::ModuleResolutionModeMismatch {
476                containing_file, ..
477            }
478            | Self::JsonModuleWithoutResolveJsonModule {
479                containing_file, ..
480            } => containing_file,
481        }
482    }
483
484    /// Get the span for this resolution failure
485    pub const fn span(&self) -> Span {
486        match self {
487            Self::NotFound { span, .. }
488            | Self::InvalidSpecifier { span, .. }
489            | Self::PackageJsonError { span, .. }
490            | Self::CircularResolution { span, .. }
491            | Self::PathMappingFailed { span, .. }
492            | Self::ImportPathNeedsExtension { span, .. }
493            | Self::ImportingTsExtensionNotAllowed { span, .. }
494            | Self::JsxNotEnabled { span, .. }
495            | Self::ModuleResolutionModeMismatch { span, .. }
496            | Self::JsonModuleWithoutResolveJsonModule { span, .. } => *span,
497        }
498    }
499
500    /// Check if this is a `NotFound` error
501    pub const fn is_not_found(&self) -> bool {
502        matches!(self, Self::NotFound { .. })
503    }
504}
505
506/// Module resolver that implements TypeScript's resolution algorithms
507#[derive(Debug)]
508pub struct ModuleResolver {
509    /// Resolution strategy to use
510    resolution_kind: ModuleResolutionKind,
511    /// Base URL for path resolution
512    base_url: Option<PathBuf>,
513    /// Path mappings from tsconfig
514    path_mappings: Vec<PathMapping>,
515    /// Type roots for @types packages
516    type_roots: Vec<PathBuf>,
517    types_versions_compiler_version: Option<String>,
518    resolve_package_json_exports: bool,
519    resolve_package_json_imports: bool,
520    module_suffixes: Vec<String>,
521    resolve_json_module: bool,
522    allow_arbitrary_extensions: bool,
523    allow_importing_ts_extensions: bool,
524    jsx: Option<JsxEmit>,
525    /// Cache of resolved modules
526    resolution_cache: FxHashMap<
527        (PathBuf, String, ImportingModuleKind),
528        Result<ResolvedModule, ResolutionFailure>,
529    >,
530    /// Custom conditions from tsconfig (for customConditions option)
531    custom_conditions: Vec<String>,
532    module_kind: ModuleKind,
533    /// Whether allowJs is enabled (affects extension candidates)
534    allow_js: bool,
535    /// Whether to rewrite relative imports with TypeScript extensions during emit.
536    rewrite_relative_import_extensions: bool,
537    /// Cache for package.json package type lookups
538    package_type_cache: FxHashMap<PathBuf, Option<PackageType>>,
539    /// Cached package type for the current resolution
540    current_package_type: Option<PackageType>,
541}
542
543struct PathMappingAttempt {
544    resolved: Option<ResolvedModule>,
545    attempted: bool,
546}
547
548impl ModuleResolver {
549    /// Create a new module resolver with the given options
550    pub fn new(options: &ResolvedCompilerOptions) -> Self {
551        let resolution_kind = options.effective_module_resolution();
552
553        let module_suffixes = if options.module_suffixes.is_empty() {
554            vec![String::new()]
555        } else {
556            options.module_suffixes.clone()
557        };
558
559        Self {
560            resolution_kind,
561            base_url: options.base_url.clone(),
562            path_mappings: options.paths.clone().unwrap_or_default(),
563            type_roots: options.type_roots.clone().unwrap_or_default(),
564            types_versions_compiler_version: options.types_versions_compiler_version.clone(),
565            resolve_package_json_exports: options.resolve_package_json_exports,
566            resolve_package_json_imports: options.resolve_package_json_imports,
567            module_suffixes,
568            resolve_json_module: options.resolve_json_module,
569            allow_arbitrary_extensions: options.allow_arbitrary_extensions,
570            allow_importing_ts_extensions: options.allow_importing_ts_extensions,
571            jsx: options.jsx,
572            resolution_cache: FxHashMap::default(),
573            custom_conditions: options.custom_conditions.clone(),
574            module_kind: options.printer.module,
575            allow_js: options.allow_js,
576            rewrite_relative_import_extensions: options.rewrite_relative_import_extensions,
577            package_type_cache: FxHashMap::default(),
578            current_package_type: None,
579        }
580    }
581
582    /// Create a resolver with default Node resolution
583    pub fn node_resolver() -> Self {
584        Self {
585            resolution_kind: ModuleResolutionKind::Node,
586            base_url: None,
587            path_mappings: Vec::new(),
588            type_roots: Vec::new(),
589            types_versions_compiler_version: None,
590            resolve_package_json_exports: false,
591            resolve_package_json_imports: false,
592            module_suffixes: vec![String::new()],
593            resolve_json_module: false,
594            allow_arbitrary_extensions: false,
595            allow_importing_ts_extensions: false,
596            jsx: None,
597            resolution_cache: FxHashMap::default(),
598            custom_conditions: Vec::new(),
599            module_kind: ModuleKind::CommonJS,
600            allow_js: false,
601            rewrite_relative_import_extensions: false,
602            package_type_cache: FxHashMap::default(),
603            current_package_type: None,
604        }
605    }
606
607    /// Resolve a module specifier from a containing file
608    pub fn resolve(
609        &mut self,
610        specifier: &str,
611        containing_file: &Path,
612        specifier_span: Span,
613    ) -> Result<ResolvedModule, ResolutionFailure> {
614        self.resolve_with_kind(
615            specifier,
616            containing_file,
617            specifier_span,
618            ImportKind::EsmImport,
619        )
620    }
621
622    /// Resolve a module specifier from a containing file, with import kind information.
623    /// The `import_kind` is used to determine whether to emit TS2834 (extensionless ESM import)
624    /// or TS2307 (cannot find module) for extensionless imports in Node16/NodeNext.
625    pub fn resolve_with_kind(
626        &mut self,
627        specifier: &str,
628        containing_file: &Path,
629        specifier_span: Span,
630        import_kind: ImportKind,
631    ) -> Result<ResolvedModule, ResolutionFailure> {
632        let containing_dir = containing_file
633            .parent()
634            .unwrap_or_else(|| Path::new("."))
635            .to_path_buf();
636        let containing_file_str = containing_file.display().to_string();
637
638        self.current_package_type = match self.resolution_kind {
639            ModuleResolutionKind::Node16 | ModuleResolutionKind::NodeNext => {
640                self.get_package_type_for_dir(&containing_dir)
641            }
642            _ => None,
643        };
644
645        // Determine the module kind of the importing file
646        let importing_module_kind = self.get_importing_module_kind(containing_file);
647        let cache_key = (
648            containing_dir.clone(),
649            specifier.to_string(),
650            importing_module_kind,
651        );
652        if let Some(cached) = self.resolution_cache.get(&cache_key) {
653            return cached.clone();
654        }
655
656        let (mut result, path_mapping_attempted) = self.resolve_uncached(
657            specifier,
658            &containing_dir,
659            &containing_file_str,
660            specifier_span,
661            importing_module_kind,
662            import_kind,
663        );
664
665        if !self.allow_importing_ts_extensions
666            && !self.allow_arbitrary_extensions
667            && !self.rewrite_relative_import_extensions
668            && (self.base_url.is_some() || self.path_mappings.is_empty())
669            && let Some(extension) = explicit_ts_extension(specifier)
670            && !path_mapping_attempted
671            && matches!(result, Err(ResolutionFailure::NotFound { .. }))
672        {
673            result = Err(ResolutionFailure::ImportingTsExtensionNotAllowed {
674                extension,
675                containing_file: containing_file_str.clone(),
676                span: specifier_span,
677            });
678        }
679
680        if let Ok(resolved) = &result {
681            if matches!(
682                resolved.extension,
683                ModuleExtension::Tsx | ModuleExtension::Jsx
684            ) && self.jsx.is_none()
685            {
686                result = Err(ResolutionFailure::JsxNotEnabled {
687                    specifier: specifier.to_string(),
688                    resolved_path: resolved.resolved_path.clone(),
689                    containing_file: containing_file_str,
690                    span: specifier_span,
691                });
692            } else if resolved.extension == ModuleExtension::Json && !self.resolve_json_module {
693                result = Err(ResolutionFailure::JsonModuleWithoutResolveJsonModule {
694                    specifier: specifier.to_string(),
695                    containing_file: containing_file_str,
696                    span: specifier_span,
697                });
698            }
699        }
700
701        // Cache the result
702        self.resolution_cache.insert(cache_key, result.clone());
703
704        result
705    }
706
707    /// Determine the module kind of the importing file based on extension and package.json type
708    fn get_importing_module_kind(&mut self, file_path: &Path) -> ImportingModuleKind {
709        let extension = ModuleExtension::from_path(file_path);
710
711        // .mts, .mjs force ESM mode
712        if extension.forces_esm() {
713            return ImportingModuleKind::Esm;
714        }
715
716        // .cts, .cjs force CommonJS mode
717        if extension.forces_cjs() {
718            return ImportingModuleKind::CommonJs;
719        }
720
721        // `--module commonjs` and other CJS-style module targets ignore package.json `type`.
722        match self.module_kind {
723            ModuleKind::CommonJS | ModuleKind::AMD | ModuleKind::UMD | ModuleKind::System => {
724                return ImportingModuleKind::CommonJs;
725            }
726            ModuleKind::None
727            | ModuleKind::ES2015
728            | ModuleKind::ES2020
729            | ModuleKind::ES2022
730            | ModuleKind::ESNext
731            | ModuleKind::Node16
732            | ModuleKind::NodeNext
733            | ModuleKind::Preserve => {}
734        }
735
736        // Check package.json "type" field
737        if let Some(dir) = file_path.parent() {
738            match self.get_package_type_for_dir(dir) {
739                Some(PackageType::Module) => ImportingModuleKind::Esm,
740                Some(PackageType::CommonJs) | None => ImportingModuleKind::CommonJs,
741            }
742        } else {
743            ImportingModuleKind::CommonJs
744        }
745    }
746
747    /// Get the package type for a directory by walking up to find package.json
748    fn get_package_type_for_dir(&mut self, dir: &Path) -> Option<PackageType> {
749        // Check cache first
750        if let Some(cached) = self.package_type_cache.get(dir) {
751            return *cached;
752        }
753
754        let mut current = dir.to_path_buf();
755        let mut visited = Vec::new();
756
757        loop {
758            // Check cache for this path - copy the value to avoid borrow conflict
759            if let Some(&cached) = self.package_type_cache.get(&current) {
760                let result = cached;
761                // Cache all visited paths with this result
762                for path in visited {
763                    self.package_type_cache.insert(path, result);
764                }
765                return result;
766            }
767
768            visited.push(current.clone());
769
770            // Check for package.json
771            let package_json_path = current.join("package.json");
772            if package_json_path.is_file()
773                && let Ok(pj) = self.read_package_json(&package_json_path)
774            {
775                let package_type = pj.package_type.as_deref().and_then(|t| match t {
776                    "module" => Some(PackageType::Module),
777                    "commonjs" => Some(PackageType::CommonJs),
778                    _ => None,
779                });
780                // Cache all visited paths
781                for path in visited {
782                    self.package_type_cache.insert(path, package_type);
783                }
784                return package_type;
785            }
786
787            // Move to parent
788            match current.parent() {
789                Some(parent) if parent != current => current = parent.to_path_buf(),
790                _ => break,
791            }
792        }
793
794        // No package.json found, cache as None
795        for path in visited {
796            self.package_type_cache.insert(path, None);
797        }
798        None
799    }
800
801    /// Resolve without checking cache
802    fn resolve_uncached(
803        &self,
804        specifier: &str,
805        containing_dir: &Path,
806        containing_file: &str,
807        specifier_span: Span,
808        importing_module_kind: ImportingModuleKind,
809        import_kind: ImportKind,
810    ) -> (Result<ResolvedModule, ResolutionFailure>, bool) {
811        // Step 1: Handle #-prefixed imports (package.json imports field)
812        // This is a Node16/NodeNext feature for subpath imports
813        if specifier.starts_with('#') {
814            if !self.resolve_package_json_imports {
815                return (
816                    Err(ResolutionFailure::NotFound {
817                        specifier: specifier.to_string(),
818                        containing_file: containing_file.to_string(),
819                        span: specifier_span,
820                    }),
821                    false,
822                );
823            }
824            return (
825                self.resolve_package_imports(
826                    specifier,
827                    containing_dir,
828                    containing_file,
829                    specifier_span,
830                    importing_module_kind,
831                ),
832                false,
833            );
834        }
835
836        // Step 2: Try path mappings first (if configured and baseUrl is available).
837        // TypeScript treats `paths` mappings as requiring `baseUrl` to avoid surprising
838        // absolute lookups that behave like relative resolution.
839        let mut path_mapping_attempted = false;
840        if self.base_url.is_some() && !self.path_mappings.is_empty() {
841            let attempt = self.try_path_mappings(specifier, containing_dir);
842            if let Some(resolved) = attempt.resolved {
843                return (Ok(resolved), path_mapping_attempted);
844            }
845            path_mapping_attempted = attempt.attempted;
846        }
847
848        // Step 3: Handle relative imports
849        if specifier.starts_with("./")
850            || specifier.starts_with("../")
851            || specifier == "."
852            || specifier == ".."
853        {
854            return (
855                self.resolve_relative(
856                    specifier,
857                    containing_dir,
858                    containing_file,
859                    specifier_span,
860                    importing_module_kind,
861                    import_kind,
862                ),
863                path_mapping_attempted,
864            );
865        }
866
867        // Step 4: Handle absolute imports (rare but valid)
868        if specifier.starts_with('/') {
869            return (
870                self.resolve_absolute(specifier, containing_file, specifier_span),
871                path_mapping_attempted,
872            );
873        }
874
875        // Step 5: Try baseUrl fallback for non-relative specifiers
876        if let Some(base_url) = &self.base_url {
877            let candidate = base_url.join(specifier);
878            if let Some(resolved) = self.try_file_or_directory(&candidate) {
879                return (
880                    Ok(ResolvedModule {
881                        resolved_path: resolved.clone(),
882                        is_external: false,
883                        package_name: None,
884                        original_specifier: specifier.to_string(),
885                        extension: ModuleExtension::from_path(&resolved),
886                    }),
887                    path_mapping_attempted,
888                );
889            }
890        }
891
892        // Step 6: Classic resolution walks up the directory tree looking for
893        // <specifier>.ts, <specifier>.tsx, <specifier>.d.ts at each level.
894        // It does NOT consult node_modules.
895        let resolved = if matches!(self.resolution_kind, ModuleResolutionKind::Classic) {
896            self.resolve_classic_non_relative(
897                specifier,
898                containing_dir,
899                containing_file,
900                specifier_span,
901            )
902        } else {
903            self.resolve_bare_specifier(
904                specifier,
905                containing_dir,
906                containing_file,
907                specifier_span,
908                importing_module_kind,
909            )
910        };
911
912        if let Err(ResolutionFailure::NotFound { .. }) = &resolved
913            && path_mapping_attempted
914        {
915            return (
916                Err(ResolutionFailure::PathMappingFailed {
917                    message: specifier.to_string(),
918                    containing_file: containing_file.to_string(),
919                    span: specifier_span,
920                }),
921                path_mapping_attempted,
922            );
923        }
924
925        (resolved, path_mapping_attempted)
926    }
927
928    /// Resolve package.json imports field (#-prefixed specifiers)
929    fn resolve_package_imports(
930        &self,
931        specifier: &str,
932        containing_dir: &Path,
933        containing_file: &str,
934        specifier_span: Span,
935        importing_module_kind: ImportingModuleKind,
936    ) -> Result<ResolvedModule, ResolutionFailure> {
937        // Walk up directory tree looking for package.json with imports field
938        let mut current = containing_dir.to_path_buf();
939
940        loop {
941            let package_json_path = current.join("package.json");
942
943            if package_json_path.is_file()
944                && let Ok(package_json) = self.read_package_json(&package_json_path)
945                && let Some(imports) = &package_json.imports
946            {
947                let conditions = self.get_export_conditions(importing_module_kind);
948
949                if let Some(target) = self.resolve_imports_subpath(imports, specifier, &conditions)
950                {
951                    // Resolve the target path
952                    let resolved_path = current.join(target.trim_start_matches("./"));
953
954                    if let Some(resolved) = self.try_file_or_directory(&resolved_path) {
955                        return Ok(ResolvedModule {
956                            resolved_path: resolved.clone(),
957                            is_external: false,
958                            package_name: package_json.name.clone(),
959                            original_specifier: specifier.to_string(),
960                            extension: ModuleExtension::from_path(&resolved),
961                        });
962                    }
963                }
964            }
965
966            // Move to parent directory
967            match current.parent() {
968                Some(parent) if parent != current => current = parent.to_path_buf(),
969                _ => break,
970            }
971        }
972
973        Err(ResolutionFailure::NotFound {
974            specifier: specifier.to_string(),
975            containing_file: containing_file.to_string(),
976            span: specifier_span,
977        })
978    }
979
980    /// Resolve imports field subpath (similar to exports but with # prefix)
981    fn resolve_imports_subpath(
982        &self,
983        imports: &FxHashMap<String, PackageExports>,
984        specifier: &str,
985        conditions: &[String],
986    ) -> Option<String> {
987        // Try exact match first
988        if let Some(value) = imports.get(specifier) {
989            return self.resolve_export_target_to_string(value, conditions);
990        }
991
992        // Try pattern matching (e.g., "#utils/*")
993        let mut best_match: Option<(usize, String, &PackageExports)> = None;
994
995        for (pattern, value) in imports {
996            if let Some(wildcard) = match_imports_pattern(pattern, specifier) {
997                let specificity = pattern.len();
998                let is_better = match &best_match {
999                    None => true,
1000                    Some((best_len, _, _)) => specificity > *best_len,
1001                };
1002                if is_better {
1003                    best_match = Some((specificity, wildcard, value));
1004                }
1005            }
1006        }
1007
1008        if let Some((_, wildcard, value)) = best_match
1009            && let Some(target) = self.resolve_export_target_to_string(value, conditions)
1010        {
1011            return Some(apply_wildcard_substitution(&target, &wildcard));
1012        }
1013
1014        None
1015    }
1016
1017    /// Resolve an export/import value to a string path
1018    #[allow(clippy::only_used_in_recursion)]
1019    fn resolve_export_target_to_string(
1020        &self,
1021        value: &PackageExports,
1022        conditions: &[String],
1023    ) -> Option<String> {
1024        match value {
1025            PackageExports::String(s) => Some(s.clone()),
1026            PackageExports::Conditional(cond_entries) => {
1027                // Iterate condition map entries in JSON key order
1028                for (key, nested) in cond_entries {
1029                    if conditions.iter().any(|c| c == key) {
1030                        if matches!(nested, PackageExports::Null) {
1031                            return None;
1032                        }
1033                        if let Some(result) =
1034                            self.resolve_export_target_to_string(nested, conditions)
1035                        {
1036                            return Some(result);
1037                        }
1038                    }
1039                }
1040                None
1041            }
1042            PackageExports::Map(_) | PackageExports::Null => None, // Subpath maps not valid here
1043        }
1044    }
1045
1046    /// Get export conditions based on resolution kind and module kind
1047    ///
1048    /// Returns conditions in priority order for conditional exports resolution.
1049    /// The order follows TypeScript's algorithm:
1050    /// 1. Custom conditions from tsconfig (prepended to defaults)
1051    /// 2. "types" - TypeScript always checks this first
1052    /// 3. Platform condition ("node" for Node.js, "browser" for bundler)
1053    /// 4. Primary module condition based on importing file ("import" for ESM, "require" for CJS)
1054    /// 5. "default" - fallback for unmatched conditions
1055    /// 6. Opposite module condition as fallback (allows ESM-first packages to work with CJS imports)
1056    /// 7. Additional platform fallbacks
1057    fn get_export_conditions(&self, importing_module_kind: ImportingModuleKind) -> Vec<String> {
1058        let mut conditions = Vec::new();
1059
1060        // Custom conditions from tsconfig are prepended to defaults
1061        for cond in &self.custom_conditions {
1062            conditions.push(cond.clone());
1063        }
1064
1065        // TypeScript always checks "types" first
1066        conditions.push("types".to_string());
1067
1068        // Add platform condition: Node modes get "node", bundler does NOT
1069        match self.resolution_kind {
1070            ModuleResolutionKind::Node16 | ModuleResolutionKind::NodeNext => {
1071                conditions.push("node".to_string());
1072            }
1073            _ => {}
1074        }
1075
1076        // Add module kind condition
1077        match importing_module_kind {
1078            ImportingModuleKind::Esm => {
1079                conditions.push("import".to_string());
1080            }
1081            ImportingModuleKind::CommonJs => {
1082                conditions.push("require".to_string());
1083            }
1084        }
1085
1086        // "default" is always a fallback condition
1087        conditions.push("default".to_string());
1088
1089        conditions
1090    }
1091
1092    /// Try resolving through path mappings
1093    fn try_path_mappings(&self, specifier: &str, _containing_dir: &Path) -> PathMappingAttempt {
1094        // Sort path mappings by specificity (most specific first)
1095        let mut sorted_mappings: Vec<_> = self.path_mappings.iter().collect();
1096        sorted_mappings.sort_by_key(|b| std::cmp::Reverse(b.specificity()));
1097
1098        let mut attempted = false;
1099        for mapping in sorted_mappings {
1100            if let Some(star_match) = mapping.match_specifier(specifier) {
1101                attempted = true;
1102                // Try each target path
1103                for target in &mapping.targets {
1104                    let substituted = if target.contains('*') {
1105                        target.replace('*', &star_match)
1106                    } else {
1107                        target.clone()
1108                    };
1109                    if Self::has_path_mapping_target_extension(&substituted) {
1110                        continue;
1111                    }
1112
1113                    let base = self
1114                        .base_url
1115                        .as_deref()
1116                        .expect("path mappings require baseUrl for attempted resolution");
1117                    let candidate = base.join(&substituted);
1118
1119                    if let Some(resolved) = self.try_file_or_directory(&candidate) {
1120                        return PathMappingAttempt {
1121                            resolved: Some(ResolvedModule {
1122                                resolved_path: resolved,
1123                                is_external: false,
1124                                package_name: None,
1125                                original_specifier: specifier.to_string(),
1126                                extension: ModuleExtension::from_path(&candidate),
1127                            }),
1128                            attempted,
1129                        };
1130                    }
1131                }
1132            }
1133        }
1134
1135        PathMappingAttempt {
1136            resolved: None,
1137            attempted,
1138        }
1139    }
1140
1141    fn has_path_mapping_target_extension(target: &str) -> bool {
1142        let base_path = std::path::Path::new(target);
1143        split_path_extension(base_path).is_some()
1144    }
1145
1146    /// Resolve a relative import
1147    fn resolve_relative(
1148        &self,
1149        specifier: &str,
1150        containing_dir: &Path,
1151        containing_file: &str,
1152        specifier_span: Span,
1153        importing_module_kind: ImportingModuleKind,
1154        import_kind: ImportKind,
1155    ) -> Result<ResolvedModule, ResolutionFailure> {
1156        let candidate = containing_dir.join(specifier);
1157
1158        // Check if specifier has an explicit extension
1159        let specifier_has_extension = Path::new(specifier)
1160            .extension()
1161            .is_some_and(|ext| !ext.is_empty());
1162
1163        // TS2834/TS2835 Check: In Node16/NodeNext, ESM-style imports must have explicit extensions.
1164        // This applies when:
1165        // - The resolution mode is Node16 or NodeNext
1166        // - The import is ESM syntax in an ESM context:
1167        //   - Dynamic import() always counts as ESM regardless of file type
1168        //   - Static import/export only counts if the file is an ESM module
1169        //   - require() never triggers this check
1170        // - The specifier has no extension
1171        let needs_extension_check = matches!(
1172            self.resolution_kind,
1173            ModuleResolutionKind::Node16 | ModuleResolutionKind::NodeNext
1174        ) && !specifier_has_extension
1175            && match import_kind {
1176                // Dynamic import() is always ESM, even in .cts files
1177                ImportKind::DynamicImport => true,
1178                // Static import/export only triggers TS2834 in ESM files
1179                ImportKind::EsmImport | ImportKind::EsmReExport => {
1180                    importing_module_kind == ImportingModuleKind::Esm
1181                }
1182                // require() never triggers TS2834
1183                ImportKind::CjsRequire => false,
1184            };
1185
1186        if needs_extension_check {
1187            // Try to resolve to determine what extension to suggest (TS2835)
1188            if let Some(resolved) = self.try_file_or_directory(&candidate) {
1189                // Resolution succeeded implicitly - this is an error in ESM mode
1190                let resolved_ext = ModuleExtension::from_path(&resolved);
1191                // Suggest the .js extension (TypeScript convention: import .js, compile from .ts)
1192                let suggested_ext = match resolved_ext {
1193                    ModuleExtension::Ts
1194                    | ModuleExtension::Tsx
1195                    | ModuleExtension::Js
1196                    | ModuleExtension::Jsx
1197                    | ModuleExtension::Dts
1198                    | ModuleExtension::Unknown => ".js",
1199                    ModuleExtension::Mts | ModuleExtension::Mjs | ModuleExtension::DmTs => ".mjs",
1200                    ModuleExtension::Cts | ModuleExtension::Cjs | ModuleExtension::DCts => ".cjs",
1201                    ModuleExtension::Json => ".json",
1202                };
1203                return Err(ResolutionFailure::ImportPathNeedsExtension {
1204                    specifier: specifier.to_string(),
1205                    suggested_extension: suggested_ext.to_string(),
1206                    containing_file: containing_file.to_string(),
1207                    span: specifier_span,
1208                });
1209            }
1210            // File doesn't exist - emit TS2834 (no suggestion) for ESM imports
1211            return Err(ResolutionFailure::ImportPathNeedsExtension {
1212                specifier: specifier.to_string(),
1213                suggested_extension: String::new(),
1214                containing_file: containing_file.to_string(),
1215                span: specifier_span,
1216            });
1217        }
1218
1219        if let Some(resolved) = self.try_file_or_directory(&candidate) {
1220            return Ok(ResolvedModule {
1221                resolved_path: resolved.clone(),
1222                is_external: false,
1223                package_name: None,
1224                original_specifier: specifier.to_string(),
1225                extension: ModuleExtension::from_path(&resolved),
1226            });
1227        }
1228
1229        // Module not found - emit TS2307 (standard "Cannot find module" error).
1230        // TS2792 should only be emitted when we've detected a package.json exports
1231        // field that would work in a different resolution mode.
1232        Err(ResolutionFailure::NotFound {
1233            specifier: specifier.to_string(),
1234            containing_file: containing_file.to_string(),
1235            span: specifier_span,
1236        })
1237    }
1238
1239    /// Resolve an absolute import
1240    fn resolve_absolute(
1241        &self,
1242        specifier: &str,
1243        containing_file: &str,
1244        specifier_span: Span,
1245    ) -> Result<ResolvedModule, ResolutionFailure> {
1246        let path = PathBuf::from(specifier);
1247
1248        if let Some(resolved) = self.try_file_or_directory(&path) {
1249            return Ok(ResolvedModule {
1250                resolved_path: resolved.clone(),
1251                is_external: false,
1252                package_name: None,
1253                original_specifier: specifier.to_string(),
1254                extension: ModuleExtension::from_path(&resolved),
1255            });
1256        }
1257
1258        Err(ResolutionFailure::NotFound {
1259            specifier: specifier.to_string(),
1260            containing_file: containing_file.to_string(),
1261            span: specifier_span,
1262        })
1263    }
1264
1265    /// Resolve a non-relative module specifier using Classic resolution.
1266    ///
1267    /// TypeScript's Classic algorithm walks up the directory tree from the containing
1268    /// file's directory, probing for `<specifier>.ts`, `<specifier>.tsx`,
1269    /// `<specifier>.d.ts` at each level. It does NOT consult `node_modules`.
1270    ///
1271    /// Example: importing `"foo"` from `/a/b/c/app.ts` will try:
1272    ///   /a/b/c/foo.ts, /a/b/c/foo.tsx, /a/b/c/foo.d.ts, ...
1273    ///   /a/b/foo.ts, /a/b/foo.tsx, /a/b/foo.d.ts, ...
1274    ///   /a/foo.ts, /a/foo.tsx, /a/foo.d.ts, ...
1275    ///   /foo.ts, /foo.tsx, /foo.d.ts, ...
1276    fn resolve_classic_non_relative(
1277        &self,
1278        specifier: &str,
1279        containing_dir: &Path,
1280        containing_file: &str,
1281        specifier_span: Span,
1282    ) -> Result<ResolvedModule, ResolutionFailure> {
1283        let (package_name, subpath) = parse_package_specifier(specifier);
1284        let conditions = self.get_export_conditions(ImportingModuleKind::CommonJs);
1285
1286        let mut current = containing_dir.to_path_buf();
1287        loop {
1288            let candidate = current.join(specifier);
1289            if let Some(resolved) = self.try_file_or_directory(&candidate) {
1290                return Ok(ResolvedModule {
1291                    resolved_path: resolved.clone(),
1292                    is_external: false,
1293                    package_name: None,
1294                    original_specifier: specifier.to_string(),
1295                    extension: ModuleExtension::from_path(&resolved),
1296                });
1297            }
1298
1299            // Also check @types packages in node_modules (TypeScript classic resolution
1300            // still resolves @types packages for bare specifiers)
1301            if !package_name.starts_with("@types/") {
1302                let types_package = types_package_name(&package_name);
1303                let types_dir = current.join("node_modules").join(&types_package);
1304                if types_dir.is_dir()
1305                    && let Ok(resolved) = self.resolve_package(
1306                        &types_dir,
1307                        subpath.as_deref(),
1308                        specifier,
1309                        containing_file,
1310                        specifier_span,
1311                        &conditions,
1312                    )
1313                {
1314                    return Ok(resolved);
1315                }
1316            }
1317
1318            // Check type_roots for the package
1319            for type_root in &self.type_roots {
1320                let types_package = if !package_name.starts_with("@types/") {
1321                    type_root.join(types_package_name(&package_name))
1322                } else {
1323                    type_root.join(&package_name)
1324                };
1325                if types_package.is_dir()
1326                    && let Ok(resolved) = self.resolve_package(
1327                        &types_package,
1328                        subpath.as_deref(),
1329                        specifier,
1330                        containing_file,
1331                        specifier_span,
1332                        &conditions,
1333                    )
1334                {
1335                    return Ok(resolved);
1336                }
1337            }
1338
1339            // Move to parent directory
1340            match current.parent() {
1341                Some(parent) if parent != current => current = parent.to_path_buf(),
1342                _ => break,
1343            }
1344        }
1345
1346        // Module not found - emit TS2307 (standard "Cannot find module" error).
1347        // Classic resolution walks up the directory tree but doesn't use node_modules.
1348        Err(ResolutionFailure::NotFound {
1349            specifier: specifier.to_string(),
1350            containing_file: containing_file.to_string(),
1351            span: specifier_span,
1352        })
1353    }
1354
1355    /// Resolve a bare specifier (npm package)
1356    fn resolve_bare_specifier(
1357        &self,
1358        specifier: &str,
1359        containing_dir: &Path,
1360        containing_file: &str,
1361        specifier_span: Span,
1362        importing_module_kind: ImportingModuleKind,
1363    ) -> Result<ResolvedModule, ResolutionFailure> {
1364        // Parse package name and subpath
1365        let (package_name, subpath) = parse_package_specifier(specifier);
1366        let conditions = self.get_export_conditions(importing_module_kind);
1367
1368        // First, try self-reference: check if we're inside a package that matches the specifier
1369        if let Some(resolved) = self.try_self_reference(
1370            &package_name,
1371            subpath.as_deref(),
1372            specifier,
1373            containing_dir,
1374            &conditions,
1375        ) {
1376            return Ok(resolved);
1377        }
1378
1379        // Walk up directory tree looking for node_modules
1380        let mut current = containing_dir.to_path_buf();
1381        loop {
1382            let node_modules = current.join("node_modules");
1383
1384            if node_modules.is_dir() {
1385                let package_dir = node_modules.join(&package_name);
1386
1387                if package_dir.is_dir() {
1388                    match self.resolve_package(
1389                        &package_dir,
1390                        subpath.as_deref(),
1391                        specifier,
1392                        containing_file,
1393                        specifier_span,
1394                        &conditions,
1395                    ) {
1396                        Ok(resolved) => return Ok(resolved),
1397                        Err(e @ ResolutionFailure::ModuleResolutionModeMismatch { .. }) => {
1398                            // Package found with exports field but resolution failed.
1399                            // exports is authoritative — do not continue searching.
1400                            return Err(e);
1401                        }
1402                        Err(e @ ResolutionFailure::NotFound { .. }) => {
1403                            if self.should_stop_on_bundler_exports_failure(
1404                                &package_dir,
1405                                subpath.as_deref(),
1406                                &conditions,
1407                                containing_file,
1408                                specifier,
1409                            ) {
1410                                return Err(e);
1411                            }
1412                        }
1413                        Err(_) => {
1414                            // Continue searching in parent directories
1415                        }
1416                    }
1417                } else if matches!(self.resolution_kind, ModuleResolutionKind::Bundler)
1418                    && subpath.is_none()
1419                {
1420                    // In bundler mode, package specifiers may resolve directly to
1421                    // files like `node_modules/foo.d.ts`.
1422                    if let Some(resolved) = self.try_file_or_directory(&package_dir) {
1423                        return Ok(ResolvedModule {
1424                            resolved_path: resolved.clone(),
1425                            is_external: true,
1426                            package_name: Some(package_name),
1427                            original_specifier: specifier.to_string(),
1428                            extension: ModuleExtension::from_path(&resolved),
1429                        });
1430                    }
1431                }
1432            }
1433
1434            if !package_name.starts_with("@types/") {
1435                let types_package = types_package_name(&package_name);
1436                let types_dir = node_modules.join(&types_package);
1437                if types_dir.is_dir()
1438                    && let Ok(resolved) = self.resolve_package(
1439                        &types_dir,
1440                        subpath.as_deref(),
1441                        specifier,
1442                        containing_file,
1443                        specifier_span,
1444                        &conditions,
1445                    )
1446                {
1447                    return Ok(resolved);
1448                }
1449            }
1450
1451            // Move to parent directory
1452            match current.parent() {
1453                Some(parent) if parent != current => current = parent.to_path_buf(),
1454                _ => break,
1455            }
1456        }
1457
1458        // Try type roots (for @types packages)
1459        for type_root in &self.type_roots {
1460            let types_package = type_root.join(types_package_name(&package_name));
1461            if types_package.is_dir()
1462                && let Ok(resolved) = self.resolve_package(
1463                    &types_package,
1464                    subpath.as_deref(),
1465                    specifier,
1466                    containing_file,
1467                    specifier_span,
1468                    &conditions,
1469                )
1470            {
1471                return Ok(resolved);
1472            }
1473        }
1474
1475        Err(ResolutionFailure::NotFound {
1476            specifier: specifier.to_string(),
1477            containing_file: containing_file.to_string(),
1478            span: specifier_span,
1479        })
1480    }
1481
1482    fn should_stop_on_bundler_exports_failure(
1483        &self,
1484        package_dir: &Path,
1485        subpath: Option<&str>,
1486        conditions: &[String],
1487        _containing_file: &str,
1488        _specifier: &str,
1489    ) -> bool {
1490        if !matches!(self.resolution_kind, ModuleResolutionKind::Bundler) {
1491            return false;
1492        }
1493        if !self.resolve_package_json_exports {
1494            return false;
1495        }
1496
1497        let package_json_path = package_dir.join("package.json");
1498        if !package_json_path.is_file() {
1499            return false;
1500        }
1501
1502        let package_json = match self.read_package_json(&package_json_path) {
1503            Ok(package_json) => package_json,
1504            Err(_) => return false,
1505        };
1506
1507        let Some(exports) = package_json.exports else {
1508            return false;
1509        };
1510
1511        let subpath_key = match subpath {
1512            Some(subpath) => format!("./{subpath}"),
1513            None => ".".to_string(),
1514        };
1515
1516        self.resolve_package_exports_with_conditions(
1517            package_dir,
1518            &exports,
1519            &subpath_key,
1520            conditions,
1521        )
1522        .is_none()
1523    }
1524
1525    /// Try to resolve a self-reference (package importing itself by name)
1526    fn try_self_reference(
1527        &self,
1528        package_name: &str,
1529        subpath: Option<&str>,
1530        original_specifier: &str,
1531        containing_dir: &Path,
1532        conditions: &[String],
1533    ) -> Option<ResolvedModule> {
1534        // Only available in Node16/NodeNext/Bundler
1535        if !matches!(
1536            self.resolution_kind,
1537            ModuleResolutionKind::Node16
1538                | ModuleResolutionKind::NodeNext
1539                | ModuleResolutionKind::Bundler
1540        ) {
1541            return None;
1542        }
1543
1544        // Walk up to find the closest package.json
1545        let mut current = containing_dir.to_path_buf();
1546
1547        loop {
1548            let package_json_path = current.join("package.json");
1549
1550            if package_json_path.is_file()
1551                && let Ok(package_json) = self.read_package_json(&package_json_path)
1552            {
1553                // Check if the package name matches
1554                if package_json.name.as_deref() == Some(package_name) {
1555                    // This is a self-reference!
1556                    if self.resolve_package_json_exports
1557                        && let Some(exports) = &package_json.exports
1558                    {
1559                        let subpath_key = match subpath {
1560                            Some(sp) => format!("./{sp}"),
1561                            None => ".".to_string(),
1562                        };
1563
1564                        if let Some(resolved) = self.resolve_package_exports_with_conditions(
1565                            &current,
1566                            exports,
1567                            &subpath_key,
1568                            conditions,
1569                        ) {
1570                            return Some(ResolvedModule {
1571                                resolved_path: resolved.clone(),
1572                                is_external: false,
1573                                package_name: Some(package_name.to_string()),
1574                                original_specifier: original_specifier.to_string(),
1575                                extension: ModuleExtension::from_path(&resolved),
1576                            });
1577                        }
1578                    }
1579                }
1580                // Found a package.json but it's not a match - stop searching
1581                return None;
1582            }
1583
1584            // Move to parent directory
1585            match current.parent() {
1586                Some(parent) if parent != current => current = parent.to_path_buf(),
1587                _ => break,
1588            }
1589        }
1590
1591        None
1592    }
1593
1594    /// Resolve within a package directory
1595    fn resolve_package(
1596        &self,
1597        package_dir: &Path,
1598        subpath: Option<&str>,
1599        original_specifier: &str,
1600        containing_file: &str,
1601        specifier_span: Span,
1602        conditions: &[String],
1603    ) -> Result<ResolvedModule, ResolutionFailure> {
1604        // Read package.json
1605        let package_json_path = package_dir.join("package.json");
1606        let package_json = if package_json_path.exists() {
1607            self.read_package_json(&package_json_path).map_err(|msg| {
1608                ResolutionFailure::PackageJsonError {
1609                    message: msg,
1610                    containing_file: containing_file.to_string(),
1611                    span: specifier_span,
1612                }
1613            })?
1614        } else {
1615            PackageJson::default()
1616        };
1617
1618        // If there's a subpath, resolve it directly
1619        if let Some(subpath) = subpath {
1620            let subpath_key = format!("./{subpath}");
1621
1622            // Try exports field first (Node16+)
1623            if self.resolve_package_json_exports
1624                && let Some(exports) = &package_json.exports
1625            {
1626                if let Some(resolved) = self.resolve_package_exports_with_conditions(
1627                    package_dir,
1628                    exports,
1629                    &subpath_key,
1630                    conditions,
1631                ) {
1632                    return Ok(ResolvedModule {
1633                        resolved_path: resolved.clone(),
1634                        is_external: true,
1635                        package_name: Some(package_json.name.clone().unwrap_or_default()),
1636                        original_specifier: original_specifier.to_string(),
1637                        extension: ModuleExtension::from_path(&resolved),
1638                    });
1639                }
1640                // In Node16/NodeNext/Bundler, exports field is authoritative for subpaths.
1641                if matches!(
1642                    self.resolution_kind,
1643                    ModuleResolutionKind::Node16 | ModuleResolutionKind::NodeNext
1644                ) {
1645                    return Err(ResolutionFailure::ModuleResolutionModeMismatch {
1646                        specifier: original_specifier.to_string(),
1647                        containing_file: containing_file.to_string(),
1648                        span: specifier_span,
1649                    });
1650                }
1651                if matches!(self.resolution_kind, ModuleResolutionKind::Bundler) {
1652                    return Err(ResolutionFailure::NotFound {
1653                        specifier: original_specifier.to_string(),
1654                        containing_file: containing_file.to_string(),
1655                        span: specifier_span,
1656                    });
1657                }
1658            }
1659
1660            // Try typesVersions field
1661            if let Some(types_versions) = &package_json.types_versions
1662                && let Some(resolved) =
1663                    self.resolve_types_versions(package_dir, subpath, types_versions)
1664            {
1665                return Ok(ResolvedModule {
1666                    resolved_path: resolved.clone(),
1667                    is_external: true,
1668                    package_name: Some(package_json.name.clone().unwrap_or_default()),
1669                    original_specifier: original_specifier.to_string(),
1670                    extension: ModuleExtension::from_path(&resolved),
1671                });
1672            }
1673
1674            // Fall back to direct file resolution
1675            let file_path = package_dir.join(subpath);
1676            if let Some(resolved) = self.try_file_or_directory(&file_path) {
1677                return Ok(ResolvedModule {
1678                    resolved_path: resolved.clone(),
1679                    is_external: true,
1680                    package_name: Some(package_json.name.unwrap_or_default()),
1681                    original_specifier: original_specifier.to_string(),
1682                    extension: ModuleExtension::from_path(&resolved),
1683                });
1684            }
1685
1686            return Err(ResolutionFailure::NotFound {
1687                specifier: original_specifier.to_string(),
1688                containing_file: containing_file.to_string(),
1689                span: specifier_span,
1690            });
1691        }
1692
1693        // No subpath - resolve package entry point
1694
1695        // Try exports "." field first (Node16+)
1696        if self.resolve_package_json_exports
1697            && let Some(exports) = &package_json.exports
1698        {
1699            if let Some(resolved) =
1700                self.resolve_package_exports_with_conditions(package_dir, exports, ".", conditions)
1701            {
1702                return Ok(ResolvedModule {
1703                    resolved_path: resolved.clone(),
1704                    is_external: true,
1705                    package_name: Some(package_json.name.clone().unwrap_or_default()),
1706                    original_specifier: original_specifier.to_string(),
1707                    extension: ModuleExtension::from_path(&resolved),
1708                });
1709            }
1710            // In Node16/NodeNext/Bundler, exports field is authoritative.
1711            // Node16/NodeNext emit TS2792; Bundler uses TS2307.
1712            if matches!(
1713                self.resolution_kind,
1714                ModuleResolutionKind::Node16 | ModuleResolutionKind::NodeNext
1715            ) {
1716                return Err(ResolutionFailure::ModuleResolutionModeMismatch {
1717                    specifier: original_specifier.to_string(),
1718                    containing_file: containing_file.to_string(),
1719                    span: specifier_span,
1720                });
1721            }
1722            if matches!(self.resolution_kind, ModuleResolutionKind::Bundler) {
1723                return Err(ResolutionFailure::NotFound {
1724                    specifier: original_specifier.to_string(),
1725                    containing_file: containing_file.to_string(),
1726                    span: specifier_span,
1727                });
1728            }
1729        }
1730
1731        // Try typesVersions field for index
1732        if let Some(types_versions) = &package_json.types_versions
1733            && let Some(resolved) =
1734                self.resolve_types_versions(package_dir, "index", types_versions)
1735        {
1736            return Ok(ResolvedModule {
1737                resolved_path: resolved.clone(),
1738                is_external: true,
1739                package_name: Some(package_json.name.clone().unwrap_or_default()),
1740                original_specifier: original_specifier.to_string(),
1741                extension: ModuleExtension::from_path(&resolved),
1742            });
1743        }
1744
1745        // Try types/typings field
1746        if let Some(types) = package_json
1747            .types
1748            .clone()
1749            .or_else(|| package_json.typings.clone())
1750        {
1751            let types_path = package_dir.join(&types);
1752            if let Some(resolved) = resolve_explicit_unknown_extension(&types_path) {
1753                return Ok(ResolvedModule {
1754                    resolved_path: resolved.clone(),
1755                    is_external: true,
1756                    package_name: Some(package_json.name.unwrap_or_default()),
1757                    original_specifier: original_specifier.to_string(),
1758                    extension: ModuleExtension::from_path(&resolved),
1759                });
1760            }
1761            if let Some(resolved) = self.try_file_or_directory(&types_path) {
1762                return Ok(ResolvedModule {
1763                    resolved_path: resolved.clone(),
1764                    is_external: true,
1765                    package_name: Some(package_json.name.unwrap_or_default()),
1766                    original_specifier: original_specifier.to_string(),
1767                    extension: ModuleExtension::from_path(&resolved),
1768                });
1769            }
1770        }
1771
1772        // Try main field
1773        if let Some(main) = &package_json.main {
1774            let main_path = package_dir.join(main);
1775            if let Some(resolved) = resolve_explicit_unknown_extension(&main_path) {
1776                return Ok(ResolvedModule {
1777                    resolved_path: resolved.clone(),
1778                    is_external: true,
1779                    package_name: Some(package_json.name.clone().unwrap_or_default()),
1780                    original_specifier: original_specifier.to_string(),
1781                    extension: ModuleExtension::from_path(&resolved),
1782                });
1783            }
1784            if let Some(declaration) = declaration_substitution_for_main(&main_path)
1785                && declaration.is_file()
1786            {
1787                return Ok(ResolvedModule {
1788                    resolved_path: declaration.clone(),
1789                    is_external: true,
1790                    package_name: Some(package_json.name.clone().unwrap_or_default()),
1791                    original_specifier: original_specifier.to_string(),
1792                    extension: ModuleExtension::from_path(&declaration),
1793                });
1794            }
1795            // Try the main path as a file (with extension probing)
1796            if let Some(resolved) = self.try_file(&main_path) {
1797                return Ok(ResolvedModule {
1798                    resolved_path: resolved.clone(),
1799                    is_external: true,
1800                    package_name: Some(package_json.name.clone().unwrap_or_default()),
1801                    original_specifier: original_specifier.to_string(),
1802                    extension: ModuleExtension::from_path(&resolved),
1803                });
1804            }
1805            // For main field targets that are directories, only try index files.
1806            // Do NOT read nested package.json — main field resolution is non-recursive.
1807            if main_path.is_dir() {
1808                let index = main_path.join("index");
1809                if let Some(resolved) = self.try_file(&index) {
1810                    return Ok(ResolvedModule {
1811                        resolved_path: resolved.clone(),
1812                        is_external: true,
1813                        package_name: Some(package_json.name.clone().unwrap_or_default()),
1814                        original_specifier: original_specifier.to_string(),
1815                        extension: ModuleExtension::from_path(&resolved),
1816                    });
1817                }
1818            }
1819        }
1820
1821        // Try index.ts/index.js
1822        let index = package_dir.join("index");
1823        if let Some(resolved) = self.try_file(&index) {
1824            return Ok(ResolvedModule {
1825                resolved_path: resolved.clone(),
1826                is_external: true,
1827                package_name: Some(package_json.name.unwrap_or_default()),
1828                original_specifier: original_specifier.to_string(),
1829                extension: ModuleExtension::from_path(&resolved),
1830            });
1831        }
1832
1833        Err(ResolutionFailure::PackageJsonError {
1834            message: format!(
1835                "Could not find entry point for package at {}",
1836                package_dir.display()
1837            ),
1838            containing_file: containing_file.to_string(),
1839            span: specifier_span,
1840        })
1841    }
1842
1843    /// Resolve package exports with explicit conditions
1844    fn resolve_package_exports_with_conditions(
1845        &self,
1846        package_dir: &Path,
1847        exports: &PackageExports,
1848        subpath: &str,
1849        conditions: &[String],
1850    ) -> Option<PathBuf> {
1851        match exports {
1852            PackageExports::String(s) => {
1853                if subpath == "." {
1854                    let resolved = package_dir.join(s.trim_start_matches("./"));
1855                    if let Some(r) = self.try_export_target(&resolved) {
1856                        return Some(r);
1857                    }
1858                }
1859                None
1860            }
1861            PackageExports::Map(map) => {
1862                // First try exact match
1863                if let Some(value) = map.get(subpath) {
1864                    return self.resolve_export_value_with_conditions(
1865                        package_dir,
1866                        value,
1867                        conditions,
1868                    );
1869                }
1870
1871                // Try pattern matching (e.g., "./*" or "./lib/*")
1872                let mut best_match: Option<(usize, String, &PackageExports)> = None;
1873
1874                for (pattern, value) in map {
1875                    if let Some(matched) = match_export_pattern(pattern, subpath) {
1876                        let specificity = pattern.len();
1877                        let is_better = match &best_match {
1878                            None => true,
1879                            Some((best_len, _, _)) => specificity > *best_len,
1880                        };
1881                        if is_better {
1882                            best_match = Some((specificity, matched, value));
1883                        }
1884                    }
1885                }
1886
1887                if let Some((_, wildcard, value)) = best_match
1888                    && let Some(resolved) =
1889                        self.resolve_export_value_with_conditions(package_dir, value, conditions)
1890                {
1891                    // Substitute wildcard in path
1892                    let resolved_str = resolved.to_string_lossy();
1893                    if resolved_str.contains('*') {
1894                        let substituted = resolved_str.replace('*', &wildcard);
1895                        return Some(PathBuf::from(substituted));
1896                    }
1897                    return Some(resolved);
1898                }
1899
1900                None
1901            }
1902            PackageExports::Conditional(cond_entries) => {
1903                // Iterate condition map entries in JSON key order (not our conditions order)
1904                for (key, value) in cond_entries {
1905                    if conditions.iter().any(|c| c == key) {
1906                        // null means explicitly blocked - stop here
1907                        if matches!(value, PackageExports::Null) {
1908                            return None;
1909                        }
1910                        if let Some(resolved) = self.resolve_package_exports_with_conditions(
1911                            package_dir,
1912                            value,
1913                            subpath,
1914                            conditions,
1915                        ) {
1916                            return Some(resolved);
1917                        }
1918                    }
1919                }
1920                None
1921            }
1922            PackageExports::Null => None,
1923        }
1924    }
1925
1926    /// Resolve a single export value with conditions
1927    fn resolve_export_value_with_conditions(
1928        &self,
1929        package_dir: &Path,
1930        value: &PackageExports,
1931        conditions: &[String],
1932    ) -> Option<PathBuf> {
1933        match value {
1934            PackageExports::String(s) => {
1935                let resolved = package_dir.join(s.trim_start_matches("./"));
1936                self.try_export_target(&resolved)
1937            }
1938            PackageExports::Conditional(cond_entries) => {
1939                // Iterate condition map entries in JSON key order
1940                for (key, nested) in cond_entries {
1941                    if conditions.iter().any(|c| c == key) {
1942                        // null means explicitly blocked - stop here
1943                        if matches!(nested, PackageExports::Null) {
1944                            return None;
1945                        }
1946                        if let Some(resolved) = self.resolve_export_value_with_conditions(
1947                            package_dir,
1948                            nested,
1949                            conditions,
1950                        ) {
1951                            return Some(resolved);
1952                        }
1953                    }
1954                }
1955                None
1956            }
1957            PackageExports::Map(_) | PackageExports::Null => None,
1958        }
1959    }
1960
1961    /// Resolve typesVersions field
1962    fn resolve_types_versions(
1963        &self,
1964        package_dir: &Path,
1965        subpath: &str,
1966        types_versions: &serde_json::Value,
1967    ) -> Option<PathBuf> {
1968        let compiler_version =
1969            types_versions_compiler_version(self.types_versions_compiler_version.as_deref());
1970        let paths = select_types_versions_paths(types_versions, compiler_version)?;
1971        let mut best_pattern: Option<&String> = None;
1972        let mut best_value: Option<&serde_json::Value> = None;
1973        let mut best_wildcard = String::new();
1974        let mut best_specificity = 0usize;
1975        let mut best_len = 0usize;
1976
1977        for (pattern, value) in paths {
1978            let Some(wildcard) = match_types_versions_pattern(pattern, subpath) else {
1979                continue;
1980            };
1981            let specificity = types_versions_specificity(pattern);
1982            let pattern_len = pattern.len();
1983            let is_better = match best_pattern {
1984                None => true,
1985                Some(current) => {
1986                    specificity > best_specificity
1987                        || (specificity == best_specificity && pattern_len > best_len)
1988                        || (specificity == best_specificity
1989                            && pattern_len == best_len
1990                            && pattern < current)
1991                }
1992            };
1993
1994            if is_better {
1995                best_specificity = specificity;
1996                best_len = pattern_len;
1997                best_pattern = Some(pattern);
1998                best_value = Some(value);
1999                best_wildcard = wildcard;
2000            }
2001        }
2002
2003        let value = best_value?;
2004
2005        let mut targets = Vec::new();
2006        match value {
2007            serde_json::Value::String(value) => targets.push(value.as_str()),
2008            serde_json::Value::Array(list) => {
2009                for entry in list {
2010                    if let Some(value) = entry.as_str() {
2011                        targets.push(value);
2012                    }
2013                }
2014            }
2015            _ => {}
2016        }
2017
2018        for target in targets {
2019            let substituted = apply_wildcard_substitution(target, &best_wildcard);
2020            let resolved = package_dir.join(substituted.trim_start_matches("./"));
2021            if let Some(resolved) = self.try_file_or_directory(&resolved) {
2022                return Some(resolved);
2023            }
2024        }
2025
2026        None
2027    }
2028
2029    /// Try to resolve a file with various extensions
2030    fn try_file(&self, path: &Path) -> Option<PathBuf> {
2031        let suffixes = &self.module_suffixes;
2032        if let Some(extension) = path.extension().and_then(|ext| ext.to_str())
2033            && split_path_extension(path).is_none()
2034        {
2035            if self.allow_arbitrary_extensions
2036                && let Some(resolved) = try_arbitrary_extension_declaration(path, extension)
2037            {
2038                return Some(resolved);
2039            }
2040            return None;
2041        }
2042        if let Some((base, extension)) = split_path_extension(path) {
2043            // Try extension substitution (.js → .ts/.tsx/.d.ts) for all resolution modes.
2044            // TypeScript resolves `.js` imports to `.ts` sources in all modes.
2045            if let Some(rewritten) = node16_extension_substitution(path, extension) {
2046                for candidate in &rewritten {
2047                    if let Some(resolved) = try_file_with_suffixes(candidate, suffixes) {
2048                        return Some(resolved);
2049                    }
2050                }
2051            }
2052
2053            // Fall back to the original extension (e.g., literal .js file)
2054            if let Some(resolved) = try_file_with_suffixes_and_extension(&base, extension, suffixes)
2055            {
2056                return Some(resolved);
2057            }
2058
2059            return None;
2060        }
2061
2062        let extensions = self.extension_candidates_for_resolution();
2063        for ext in extensions {
2064            if let Some(resolved) = try_file_with_suffixes_and_extension(path, ext, suffixes) {
2065                return Some(resolved);
2066            }
2067        }
2068        if self.resolve_json_module
2069            && let Some(resolved) = try_file_with_suffixes_and_extension(path, "json", suffixes)
2070        {
2071            return Some(resolved);
2072        }
2073
2074        let index = path.join("index");
2075        for ext in extensions {
2076            if let Some(resolved) = try_file_with_suffixes_and_extension(&index, ext, suffixes) {
2077                return Some(resolved);
2078            }
2079        }
2080        if self.resolve_json_module
2081            && let Some(resolved) = try_file_with_suffixes_and_extension(&index, "json", suffixes)
2082        {
2083            return Some(resolved);
2084        }
2085
2086        None
2087    }
2088
2089    const fn extension_candidates_for_resolution(&self) -> &'static [&'static str] {
2090        match self.resolution_kind {
2091            ModuleResolutionKind::Node16 | ModuleResolutionKind::NodeNext => {
2092                match self.current_package_type {
2093                    Some(PackageType::Module) => &NODE16_MODULE_EXTENSION_CANDIDATES,
2094                    Some(PackageType::CommonJs) => &NODE16_COMMONJS_EXTENSION_CANDIDATES,
2095                    None => {
2096                        if self.allow_js {
2097                            &TS_JS_EXTENSION_CANDIDATES
2098                        } else {
2099                            &TS_EXTENSION_CANDIDATES
2100                        }
2101                    }
2102                }
2103            }
2104            ModuleResolutionKind::Classic => {
2105                if self.allow_js {
2106                    &TS_JS_EXTENSION_CANDIDATES
2107                } else {
2108                    &CLASSIC_EXTENSION_CANDIDATES
2109                }
2110            }
2111            _ => {
2112                if self.allow_js {
2113                    &TS_JS_EXTENSION_CANDIDATES
2114                } else {
2115                    &TS_EXTENSION_CANDIDATES
2116                }
2117            }
2118        }
2119    }
2120
2121    /// Try to resolve a path as a file or directory
2122    fn try_file_or_directory(&self, path: &Path) -> Option<PathBuf> {
2123        // Try as file first
2124        if let Some(resolved) = self.try_file(path) {
2125            return Some(resolved);
2126        }
2127
2128        // Try as directory: check package.json for types/main, then index
2129        if path.is_dir() {
2130            let package_json_path = path.join("package.json");
2131            if package_json_path.exists()
2132                && let Ok(pj) = self.read_package_json(&package_json_path)
2133            {
2134                // Try types/typings field first
2135                if let Some(types) = pj.types.or(pj.typings) {
2136                    let types_path = path.join(&types);
2137                    if let Some(resolved) = self.try_file(&types_path) {
2138                        return Some(resolved);
2139                    }
2140                    if types_path.is_file() {
2141                        return Some(types_path);
2142                    }
2143                }
2144                // Try main field with extension remapping
2145                if let Some(main) = &pj.main {
2146                    let main_path = path.join(main);
2147                    if let Some(resolved) = self.try_file(&main_path) {
2148                        return Some(resolved);
2149                    }
2150                }
2151            }
2152            let index = path.join("index");
2153            return self.try_file(&index);
2154        }
2155
2156        None
2157    }
2158
2159    /// Resolve an exports target without Node16 extension substitution.
2160    ///
2161    /// Explicit extensions must exist exactly; extensionless targets follow normal lookup.
2162    fn try_export_target(&self, path: &Path) -> Option<PathBuf> {
2163        if let Some(extension) = path.extension().and_then(|ext| ext.to_str()) {
2164            if split_path_extension(path).is_some() {
2165                if path.is_file() {
2166                    return Some(path.to_path_buf());
2167                }
2168                // For JS export targets, try declaration substitution
2169                if let Some(rewritten) = node16_extension_substitution(path, extension) {
2170                    for candidate in &rewritten {
2171                        if candidate.is_file() {
2172                            return Some(candidate.clone());
2173                        }
2174                    }
2175                }
2176                return None;
2177            }
2178            if self.allow_arbitrary_extensions
2179                && let Some(resolved) = try_arbitrary_extension_declaration(path, extension)
2180            {
2181                return Some(resolved);
2182            }
2183            return None;
2184        }
2185
2186        if let Some(resolved) = self.try_file(path) {
2187            return Some(resolved);
2188        }
2189        if path.is_dir() {
2190            let index = path.join("index");
2191            return self.try_file(&index);
2192        }
2193        None
2194    }
2195
2196    /// Read and parse package.json
2197    ///
2198    /// Returns a String error for flexibility - callers can convert to `ResolutionFailure`
2199    /// with appropriate span/file information at the call site.
2200    fn read_package_json(&self, path: &Path) -> Result<PackageJson, String> {
2201        let content = std::fs::read_to_string(path)
2202            .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
2203
2204        serde_json::from_str(&content)
2205            .map_err(|e| format!("Failed to parse {}: {}", path.display(), e))
2206    }
2207
2208    /// Probe for a JS file that would resolve for this specifier.
2209    ///
2210    /// Used for TS7016: when normal resolution fails but a JS file exists,
2211    /// we can report "Could not find declaration file" instead of "Cannot find module".
2212    /// Returns the resolved JS file path if found.
2213    pub fn probe_js_file(
2214        &mut self,
2215        specifier: &str,
2216        containing_file: &Path,
2217        specifier_span: Span,
2218        import_kind: ImportKind,
2219    ) -> Option<PathBuf> {
2220        if self.allow_js {
2221            return None; // Already tried JS in normal resolution
2222        }
2223        let containing_dir = containing_file
2224            .parent()
2225            .unwrap_or_else(|| Path::new("."))
2226            .to_path_buf();
2227        let containing_file_str = containing_file.display().to_string();
2228        let importing_module_kind = self.get_importing_module_kind(containing_file);
2229
2230        self.allow_js = true;
2231        let (result, _) = self.resolve_uncached(
2232            specifier,
2233            &containing_dir,
2234            &containing_file_str,
2235            specifier_span,
2236            importing_module_kind,
2237            import_kind,
2238        );
2239        self.allow_js = false;
2240
2241        match result {
2242            Ok(resolved)
2243                if matches!(
2244                    resolved.extension,
2245                    ModuleExtension::Js
2246                        | ModuleExtension::Jsx
2247                        | ModuleExtension::Mjs
2248                        | ModuleExtension::Cjs
2249                ) =>
2250            {
2251                Some(resolved.resolved_path)
2252            }
2253            _ => None,
2254        }
2255    }
2256
2257    /// Clear the resolution cache
2258    pub fn clear_cache(&mut self) {
2259        self.resolution_cache.clear();
2260    }
2261
2262    /// Get the current resolution kind
2263    pub const fn resolution_kind(&self) -> ModuleResolutionKind {
2264        self.resolution_kind
2265    }
2266
2267    /// Emit TS2307 error for a resolution failure into a diagnostic bag
2268    ///
2269    /// All module resolution failures emit TS2307 "Cannot find module" error.
2270    /// This includes:
2271    /// - `NotFound`: Module specifier could not be resolved
2272    /// - `InvalidSpecifier`: Module specifier is malformed
2273    /// - `PackageJsonError`: Package.json is missing or invalid
2274    /// - `CircularResolution`: Circular dependency detected during resolution
2275    /// - `PathMappingFailed`: Path mapping from tsconfig did not resolve
2276    pub fn emit_resolution_error(
2277        &self,
2278        diagnostics: &mut DiagnosticBag,
2279        failure: &ResolutionFailure,
2280    ) {
2281        let diagnostic = failure.to_diagnostic();
2282        diagnostics.add(diagnostic);
2283    }
2284}
2285
2286/// Parse a package specifier into package name and subpath
2287#[cfg(test)]
2288mod tests {
2289    use super::*;
2290
2291    #[test]
2292    fn test_parse_package_specifier_simple() {
2293        let (name, subpath) = parse_package_specifier("lodash");
2294        assert_eq!(name, "lodash");
2295        assert_eq!(subpath, None);
2296    }
2297
2298    #[test]
2299    fn test_parse_package_specifier_with_subpath() {
2300        let (name, subpath) = parse_package_specifier("lodash/fp");
2301        assert_eq!(name, "lodash");
2302        assert_eq!(subpath, Some("fp".to_string()));
2303    }
2304
2305    #[test]
2306    fn test_parse_package_specifier_scoped() {
2307        let (name, subpath) = parse_package_specifier("@babel/core");
2308        assert_eq!(name, "@babel/core");
2309        assert_eq!(subpath, None);
2310    }
2311
2312    #[test]
2313    fn test_parse_package_specifier_scoped_with_subpath() {
2314        let (name, subpath) = parse_package_specifier("@babel/core/transform");
2315        assert_eq!(name, "@babel/core");
2316        assert_eq!(subpath, Some("transform".to_string()));
2317    }
2318
2319    #[test]
2320    fn test_match_export_pattern_exact() {
2321        assert_eq!(match_export_pattern("./lib", "./lib"), Some(String::new()));
2322        assert_eq!(match_export_pattern("./lib", "./src"), None);
2323    }
2324
2325    #[test]
2326    fn test_match_export_pattern_wildcard() {
2327        assert_eq!(
2328            match_export_pattern("./*", "./foo"),
2329            Some("foo".to_string())
2330        );
2331        assert_eq!(
2332            match_export_pattern("./lib/*", "./lib/utils"),
2333            Some("utils".to_string())
2334        );
2335        assert_eq!(match_export_pattern("./lib/*", "./src/utils"), None);
2336    }
2337
2338    #[test]
2339    fn test_module_extension_from_path() {
2340        assert_eq!(
2341            ModuleExtension::from_path(Path::new("foo.ts")),
2342            ModuleExtension::Ts
2343        );
2344        assert_eq!(
2345            ModuleExtension::from_path(Path::new("foo.d.ts")),
2346            ModuleExtension::Dts
2347        );
2348        assert_eq!(
2349            ModuleExtension::from_path(Path::new("foo.tsx")),
2350            ModuleExtension::Tsx
2351        );
2352        assert_eq!(
2353            ModuleExtension::from_path(Path::new("foo.js")),
2354            ModuleExtension::Js
2355        );
2356    }
2357
2358    #[test]
2359    fn test_module_resolver_creation() {
2360        let resolver = ModuleResolver::node_resolver();
2361        assert_eq!(resolver.resolution_kind(), ModuleResolutionKind::Node);
2362    }
2363
2364    #[test]
2365    fn test_ts2307_error_code_constant() {
2366        assert_eq!(CANNOT_FIND_MODULE, 2307);
2367    }
2368
2369    #[test]
2370    fn test_resolution_failure_not_found_diagnostic() {
2371        let failure = ResolutionFailure::NotFound {
2372            specifier: "./missing-module".to_string(),
2373            containing_file: "/path/to/file.ts".to_string(),
2374            span: Span::new(10, 30),
2375        };
2376
2377        let diagnostic = failure.to_diagnostic();
2378        assert_eq!(diagnostic.code, CANNOT_FIND_MODULE);
2379        assert!(diagnostic.message.contains("Cannot find module"));
2380        assert!(diagnostic.message.contains("./missing-module"));
2381        assert_eq!(diagnostic.file_name, "/path/to/file.ts");
2382        assert_eq!(diagnostic.span.start, 10);
2383        assert_eq!(diagnostic.span.end, 30);
2384    }
2385
2386    #[test]
2387    fn test_resolution_failure_is_not_found() {
2388        let not_found = ResolutionFailure::NotFound {
2389            specifier: "test".to_string(),
2390            containing_file: "test.ts".to_string(),
2391            span: Span::dummy(),
2392        };
2393        assert!(not_found.is_not_found());
2394
2395        let other = ResolutionFailure::InvalidSpecifier {
2396            message: "test".to_string(),
2397            containing_file: "test.ts".to_string(),
2398            span: Span::dummy(),
2399        };
2400        assert!(!other.is_not_found());
2401    }
2402
2403    #[test]
2404    fn test_module_extension_forces_esm() {
2405        assert!(ModuleExtension::Mts.forces_esm());
2406        assert!(ModuleExtension::Mjs.forces_esm());
2407        assert!(ModuleExtension::DmTs.forces_esm());
2408        assert!(!ModuleExtension::Ts.forces_esm());
2409        assert!(!ModuleExtension::Cts.forces_esm());
2410    }
2411
2412    #[test]
2413    fn test_module_extension_forces_cjs() {
2414        assert!(ModuleExtension::Cts.forces_cjs());
2415        assert!(ModuleExtension::Cjs.forces_cjs());
2416        assert!(ModuleExtension::DCts.forces_cjs());
2417        assert!(!ModuleExtension::Ts.forces_cjs());
2418        assert!(!ModuleExtension::Mts.forces_cjs());
2419    }
2420
2421    #[test]
2422    fn test_match_imports_pattern_exact() {
2423        assert_eq!(
2424            match_imports_pattern("#utils", "#utils"),
2425            Some(String::new())
2426        );
2427        assert_eq!(match_imports_pattern("#utils", "#other"), None);
2428    }
2429
2430    #[test]
2431    fn test_match_imports_pattern_wildcard() {
2432        assert_eq!(
2433            match_imports_pattern("#utils/*", "#utils/foo"),
2434            Some("foo".to_string())
2435        );
2436        assert_eq!(
2437            match_imports_pattern("#internal/*", "#internal/helpers/bar"),
2438            Some("helpers/bar".to_string())
2439        );
2440        assert_eq!(match_imports_pattern("#utils/*", "#other/foo"), None);
2441    }
2442
2443    #[test]
2444    fn test_match_types_versions_pattern() {
2445        assert_eq!(
2446            match_types_versions_pattern("*", "index"),
2447            Some("index".to_string())
2448        );
2449        assert_eq!(
2450            match_types_versions_pattern("lib/*", "lib/utils"),
2451            Some("utils".to_string())
2452        );
2453        assert_eq!(
2454            match_types_versions_pattern("exact", "exact"),
2455            Some(String::new())
2456        );
2457        assert_eq!(match_types_versions_pattern("lib/*", "src/utils"), None);
2458    }
2459
2460    #[test]
2461    fn test_apply_wildcard_substitution() {
2462        assert_eq!(
2463            apply_wildcard_substitution("./lib/*.js", "utils"),
2464            "./lib/utils.js"
2465        );
2466        assert_eq!(
2467            apply_wildcard_substitution("./dist/index.js", "ignored"),
2468            "./dist/index.js"
2469        );
2470    }
2471
2472    #[test]
2473    fn test_package_type_enum() {
2474        assert_eq!(PackageType::default(), PackageType::CommonJs);
2475        assert_ne!(PackageType::Module, PackageType::CommonJs);
2476    }
2477
2478    #[test]
2479    fn test_importing_module_kind_enum() {
2480        assert_eq!(
2481            ImportingModuleKind::default(),
2482            ImportingModuleKind::CommonJs
2483        );
2484        assert_ne!(ImportingModuleKind::Esm, ImportingModuleKind::CommonJs);
2485    }
2486
2487    #[test]
2488    fn test_package_json_deserialize_basic() {
2489        let json = r#"{"name": "test-package", "type": "module", "main": "./index.js"}"#;
2490
2491        let package_json: PackageJson = serde_json::from_str(json).unwrap();
2492        assert_eq!(package_json.name, Some("test-package".to_string()));
2493        assert_eq!(package_json.package_type, Some("module".to_string()));
2494        assert_eq!(package_json.main, Some("./index.js".to_string()));
2495    }
2496
2497    #[test]
2498    fn test_package_json_deserialize_exports() {
2499        let json = r#"{"name": "pkg", "exports": {"." : "./dist/index.js"}}"#;
2500
2501        let package_json: PackageJson = serde_json::from_str(json).unwrap();
2502        assert!(package_json.exports.is_some());
2503    }
2504
2505    #[test]
2506    fn test_package_json_deserialize_types_versions() {
2507        // Build JSON programmatically to avoid raw string issues
2508        let json = serde_json::json!({
2509            "name": "typed-package",
2510            "typesVersions": {
2511                "*": {
2512                    "*": ["./types/index.d.ts"]
2513                }
2514            }
2515        });
2516
2517        let package_json: PackageJson = serde_json::from_value(json).unwrap();
2518        assert_eq!(package_json.name, Some("typed-package".to_string()));
2519        assert!(package_json.types_versions.is_some());
2520    }
2521
2522    // =========================================================================
2523    // TS2307 Diagnostic Emission Tests
2524    // =========================================================================
2525
2526    #[test]
2527    fn test_emit_resolution_error_for_not_found() {
2528        let mut diagnostics = DiagnosticBag::new();
2529        let resolver = ModuleResolver::node_resolver();
2530
2531        let failure = ResolutionFailure::NotFound {
2532            specifier: "./missing-module".to_string(),
2533            containing_file: "/src/file.ts".to_string(),
2534            span: Span::new(10, 30),
2535        };
2536
2537        resolver.emit_resolution_error(&mut diagnostics, &failure);
2538
2539        assert_eq!(diagnostics.len(), 1);
2540        assert!(diagnostics.has_errors());
2541        let errors: Vec<_> = diagnostics.errors().collect();
2542        assert_eq!(errors[0].code, CANNOT_FIND_MODULE);
2543        assert!(errors[0].message.contains("Cannot find module"));
2544        assert!(errors[0].message.contains("./missing-module"));
2545    }
2546
2547    #[test]
2548    fn test_emit_resolution_error_all_variants_emit_ts2307() {
2549        let mut diagnostics = DiagnosticBag::new();
2550        let resolver = ModuleResolver::node_resolver();
2551
2552        // All resolution failure variants should emit TS2307 diagnostics
2553        let failure = ResolutionFailure::InvalidSpecifier {
2554            message: "bad specifier".to_string(),
2555            containing_file: "/src/a.ts".to_string(),
2556            span: Span::new(0, 10),
2557        };
2558        resolver.emit_resolution_error(&mut diagnostics, &failure);
2559        assert_eq!(diagnostics.len(), 1);
2560
2561        let failure = ResolutionFailure::PackageJsonError {
2562            message: "parse error".to_string(),
2563            containing_file: "/src/b.ts".to_string(),
2564            span: Span::new(5, 15),
2565        };
2566        resolver.emit_resolution_error(&mut diagnostics, &failure);
2567        assert_eq!(diagnostics.len(), 2);
2568
2569        let failure = ResolutionFailure::CircularResolution {
2570            message: "a -> b -> a".to_string(),
2571            containing_file: "/src/c.ts".to_string(),
2572            span: Span::new(10, 20),
2573        };
2574        resolver.emit_resolution_error(&mut diagnostics, &failure);
2575        assert_eq!(diagnostics.len(), 3);
2576
2577        let failure = ResolutionFailure::PathMappingFailed {
2578            message: "@/ pattern".to_string(),
2579            containing_file: "/src/d.ts".to_string(),
2580            span: Span::new(15, 25),
2581        };
2582        resolver.emit_resolution_error(&mut diagnostics, &failure);
2583        assert_eq!(diagnostics.len(), 4);
2584
2585        // Verify all have TS2307 code
2586        for diag in diagnostics.errors() {
2587            assert_eq!(diag.code, CANNOT_FIND_MODULE);
2588        }
2589    }
2590
2591    #[test]
2592    fn test_resolution_failure_all_variants_to_diagnostic() {
2593        // Test that all ResolutionFailure variants can produce diagnostics with proper location info
2594        let failures = vec![
2595            ResolutionFailure::NotFound {
2596                specifier: "./test".to_string(),
2597                containing_file: "file.ts".to_string(),
2598                span: Span::new(0, 10),
2599            },
2600            ResolutionFailure::InvalidSpecifier {
2601                message: "bad".to_string(),
2602                containing_file: "file2.ts".to_string(),
2603                span: Span::new(5, 15),
2604            },
2605            ResolutionFailure::PackageJsonError {
2606                message: "error".to_string(),
2607                containing_file: "file3.ts".to_string(),
2608                span: Span::new(10, 20),
2609            },
2610            ResolutionFailure::CircularResolution {
2611                message: "loop".to_string(),
2612                containing_file: "file4.ts".to_string(),
2613                span: Span::new(15, 25),
2614            },
2615            ResolutionFailure::PathMappingFailed {
2616                message: "@/path".to_string(),
2617                containing_file: "file5.ts".to_string(),
2618                span: Span::new(20, 30),
2619            },
2620        ];
2621
2622        for failure in failures {
2623            let diagnostic = failure.to_diagnostic();
2624            // All failures should produce TS2307 diagnostic code
2625            assert_eq!(diagnostic.code, CANNOT_FIND_MODULE);
2626            // All failures should have non-empty file names
2627            assert!(!diagnostic.file_name.is_empty());
2628            // All failures should have valid spans
2629            assert!(diagnostic.span.start < diagnostic.span.end);
2630        }
2631    }
2632
2633    #[test]
2634    fn test_relative_import_failure_produces_ts2307() {
2635        let failure = ResolutionFailure::NotFound {
2636            specifier: "./components/Button".to_string(),
2637            containing_file: "/src/App.tsx".to_string(),
2638            span: Span::new(20, 45),
2639        };
2640
2641        let diagnostic = failure.to_diagnostic();
2642        assert_eq!(diagnostic.code, CANNOT_FIND_MODULE);
2643        assert_eq!(diagnostic.file_name, "/src/App.tsx");
2644        assert!(diagnostic.message.contains("./components/Button"));
2645        assert_eq!(diagnostic.span.start, 20);
2646        assert_eq!(diagnostic.span.end, 45);
2647    }
2648
2649    #[test]
2650    fn test_bare_specifier_failure_produces_ts2307() {
2651        let failure = ResolutionFailure::NotFound {
2652            specifier: "nonexistent-package".to_string(),
2653            containing_file: "/project/src/index.ts".to_string(),
2654            span: Span::new(7, 28),
2655        };
2656
2657        let diagnostic = failure.to_diagnostic();
2658        assert_eq!(diagnostic.code, CANNOT_FIND_MODULE);
2659        assert!(diagnostic.message.contains("nonexistent-package"));
2660    }
2661
2662    #[test]
2663    fn test_scoped_package_failure_produces_ts2307() {
2664        let failure = ResolutionFailure::NotFound {
2665            specifier: "@org/missing-lib".to_string(),
2666            containing_file: "/app/main.ts".to_string(),
2667            span: Span::new(15, 35),
2668        };
2669
2670        let diagnostic = failure.to_diagnostic();
2671        assert_eq!(diagnostic.code, CANNOT_FIND_MODULE);
2672        assert!(diagnostic.message.contains("@org/missing-lib"));
2673    }
2674
2675    #[test]
2676    fn test_hash_import_failure_produces_ts2307() {
2677        // Package.json subpath import failure
2678        let failure = ResolutionFailure::NotFound {
2679            specifier: "#utils/helpers".to_string(),
2680            containing_file: "/pkg/src/index.ts".to_string(),
2681            span: Span::new(8, 25),
2682        };
2683
2684        let diagnostic = failure.to_diagnostic();
2685        assert_eq!(diagnostic.code, CANNOT_FIND_MODULE);
2686        assert!(diagnostic.message.contains("#utils/helpers"));
2687    }
2688
2689    #[test]
2690    fn test_resolution_failure_span_preservation() {
2691        // Ensure span information is correctly preserved in diagnostics
2692        let test_cases = vec![(0, 10), (100, 150), (1000, 1050)];
2693
2694        for (start, end) in test_cases {
2695            let failure = ResolutionFailure::NotFound {
2696                specifier: "test".to_string(),
2697                containing_file: "file.ts".to_string(),
2698                span: Span::new(start, end),
2699            };
2700
2701            let diagnostic = failure.to_diagnostic();
2702            assert_eq!(diagnostic.span.start, start);
2703            assert_eq!(diagnostic.span.end, end);
2704        }
2705    }
2706
2707    #[test]
2708    fn test_resolution_failure_accessors() {
2709        // Test that accessor methods work correctly
2710        let failure = ResolutionFailure::InvalidSpecifier {
2711            message: "test error".to_string(),
2712            containing_file: "/src/test.ts".to_string(),
2713            span: Span::new(10, 20),
2714        };
2715
2716        assert_eq!(failure.containing_file(), "/src/test.ts");
2717        assert_eq!(failure.span().start, 10);
2718        assert_eq!(failure.span().end, 20);
2719    }
2720
2721    #[test]
2722    fn test_path_mapping_failure_produces_ts2307() {
2723        let failure = ResolutionFailure::PathMappingFailed {
2724            message: "path mapping '@/utils/*' did not resolve to any file".to_string(),
2725            containing_file: "/project/src/index.ts".to_string(),
2726            span: Span::new(8, 30),
2727        };
2728
2729        let diagnostic = failure.to_diagnostic();
2730        assert_eq!(diagnostic.code, CANNOT_FIND_MODULE);
2731        assert_eq!(diagnostic.file_name, "/project/src/index.ts");
2732        assert!(diagnostic.message.contains("Cannot find module"));
2733        assert!(diagnostic.message.contains("path mapping"));
2734    }
2735
2736    #[test]
2737    fn test_package_json_error_produces_ts2307() {
2738        let failure = ResolutionFailure::PackageJsonError {
2739            message: "invalid exports field in package.json".to_string(),
2740            containing_file: "/project/src/app.ts".to_string(),
2741            span: Span::new(15, 45),
2742        };
2743
2744        let diagnostic = failure.to_diagnostic();
2745        assert_eq!(diagnostic.code, CANNOT_FIND_MODULE);
2746        assert_eq!(diagnostic.file_name, "/project/src/app.ts");
2747        assert!(diagnostic.message.contains("Cannot find module"));
2748    }
2749
2750    #[test]
2751    fn test_circular_resolution_produces_ts2307() {
2752        let failure = ResolutionFailure::CircularResolution {
2753            message: "circular dependency: a.ts -> b.ts -> a.ts".to_string(),
2754            containing_file: "/project/src/a.ts".to_string(),
2755            span: Span::new(20, 50),
2756        };
2757
2758        let diagnostic = failure.to_diagnostic();
2759        assert_eq!(diagnostic.code, CANNOT_FIND_MODULE);
2760        assert_eq!(diagnostic.file_name, "/project/src/a.ts");
2761        assert!(diagnostic.message.contains("Cannot find module"));
2762        assert!(diagnostic.message.contains("circular"));
2763    }
2764
2765    #[test]
2766    fn test_diagnostic_bag_collects_multiple_resolution_errors() {
2767        let mut diagnostics = DiagnosticBag::new();
2768        let resolver = ModuleResolver::node_resolver();
2769
2770        let failures = vec![
2771            ResolutionFailure::NotFound {
2772                specifier: "./module1".to_string(),
2773                containing_file: "a.ts".to_string(),
2774                span: Span::new(0, 10),
2775            },
2776            ResolutionFailure::NotFound {
2777                specifier: "./module2".to_string(),
2778                containing_file: "b.ts".to_string(),
2779                span: Span::new(5, 15),
2780            },
2781            ResolutionFailure::NotFound {
2782                specifier: "external-pkg".to_string(),
2783                containing_file: "c.ts".to_string(),
2784                span: Span::new(10, 25),
2785            },
2786        ];
2787
2788        for failure in &failures {
2789            resolver.emit_resolution_error(&mut diagnostics, failure);
2790        }
2791
2792        assert_eq!(diagnostics.len(), 3);
2793        assert_eq!(diagnostics.error_count(), 3);
2794
2795        // Verify all have TS2307 code
2796        let codes: Vec<_> = diagnostics.errors().map(|d| d.code).collect();
2797        assert!(codes.iter().all(|&c| c == CANNOT_FIND_MODULE));
2798    }
2799
2800    // =========================================================================
2801    // TS2835 (Import Path Needs Extension Suggestion) Tests
2802    // =========================================================================
2803
2804    #[test]
2805    fn test_ts2834_error_code_constant() {
2806        assert_eq!(IMPORT_PATH_NEEDS_EXTENSION, 2834);
2807    }
2808
2809    #[test]
2810    fn test_import_path_needs_extension_produces_ts2835() {
2811        let failure = ResolutionFailure::ImportPathNeedsExtension {
2812            specifier: "./utils".to_string(),
2813            suggested_extension: ".js".to_string(),
2814            containing_file: "/src/index.mts".to_string(),
2815            span: Span::new(20, 30),
2816        };
2817
2818        let diagnostic = failure.to_diagnostic();
2819        assert_eq!(diagnostic.code, IMPORT_PATH_NEEDS_EXTENSION_SUGGESTION);
2820        assert_eq!(diagnostic.file_name, "/src/index.mts");
2821        assert!(
2822            diagnostic
2823                .message
2824                .contains("Relative import paths need explicit file extensions")
2825        );
2826        assert!(diagnostic.message.contains("node16"));
2827        assert!(diagnostic.message.contains("nodenext"));
2828        assert!(diagnostic.message.contains("./utils.js"));
2829    }
2830
2831    #[test]
2832    fn test_import_path_needs_extension_suggests_mjs() {
2833        let failure = ResolutionFailure::ImportPathNeedsExtension {
2834            specifier: "./esm-module".to_string(),
2835            suggested_extension: ".mjs".to_string(),
2836            containing_file: "/src/app.mts".to_string(),
2837            span: Span::new(10, 25),
2838        };
2839
2840        let diagnostic = failure.to_diagnostic();
2841        assert_eq!(diagnostic.code, IMPORT_PATH_NEEDS_EXTENSION_SUGGESTION);
2842        assert!(diagnostic.message.contains("./esm-module.mjs"));
2843    }
2844
2845    #[test]
2846    fn test_import_path_needs_extension_suggests_cjs() {
2847        let failure = ResolutionFailure::ImportPathNeedsExtension {
2848            specifier: "./cjs-module".to_string(),
2849            suggested_extension: ".cjs".to_string(),
2850            containing_file: "/src/legacy.cts".to_string(),
2851            span: Span::new(5, 20),
2852        };
2853
2854        let diagnostic = failure.to_diagnostic();
2855        assert_eq!(diagnostic.code, IMPORT_PATH_NEEDS_EXTENSION_SUGGESTION);
2856        assert!(diagnostic.message.contains("./cjs-module.cjs"));
2857    }
2858
2859    // =========================================================================
2860    // TS2792 (Module Resolution Mode Mismatch) Tests
2861    // =========================================================================
2862
2863    #[test]
2864    fn test_ts2792_error_code_constant() {
2865        assert_eq!(MODULE_RESOLUTION_MODE_MISMATCH, 2792);
2866    }
2867
2868    #[test]
2869    fn test_module_resolution_mode_mismatch_produces_ts2792() {
2870        let failure = ResolutionFailure::ModuleResolutionModeMismatch {
2871            specifier: "modern-esm-package".to_string(),
2872            containing_file: "/src/index.ts".to_string(),
2873            span: Span::new(15, 35),
2874        };
2875
2876        let diagnostic = failure.to_diagnostic();
2877        assert_eq!(diagnostic.code, MODULE_RESOLUTION_MODE_MISMATCH);
2878        assert_eq!(diagnostic.file_name, "/src/index.ts");
2879        assert!(
2880            diagnostic
2881                .message
2882                .contains("Cannot find module 'modern-esm-package'")
2883        );
2884        assert!(diagnostic.message.contains("moduleResolution"));
2885        assert!(diagnostic.message.contains("nodenext"));
2886        assert!(diagnostic.message.contains("paths"));
2887    }
2888
2889    #[test]
2890    fn test_module_resolution_mode_mismatch_accessors() {
2891        let failure = ResolutionFailure::ModuleResolutionModeMismatch {
2892            specifier: "pkg".to_string(),
2893            containing_file: "/test.ts".to_string(),
2894            span: Span::new(100, 110),
2895        };
2896
2897        assert_eq!(failure.containing_file(), "/test.ts");
2898        assert_eq!(failure.span().start, 100);
2899        assert_eq!(failure.span().end, 110);
2900    }
2901
2902    #[test]
2903    fn test_import_path_needs_extension_accessors() {
2904        let failure = ResolutionFailure::ImportPathNeedsExtension {
2905            specifier: "./foo".to_string(),
2906            suggested_extension: ".js".to_string(),
2907            containing_file: "/bar.mts".to_string(),
2908            span: Span::new(50, 60),
2909        };
2910
2911        assert_eq!(failure.containing_file(), "/bar.mts");
2912        assert_eq!(failure.span().start, 50);
2913        assert_eq!(failure.span().end, 60);
2914    }
2915
2916    #[test]
2917    fn test_new_error_codes_emit_correctly() {
2918        let mut diagnostics = DiagnosticBag::new();
2919        let resolver = ModuleResolver::node_resolver();
2920
2921        // Test TS2835
2922        let failure_2835 = ResolutionFailure::ImportPathNeedsExtension {
2923            specifier: "./utils".to_string(),
2924            suggested_extension: ".js".to_string(),
2925            containing_file: "/src/app.mts".to_string(),
2926            span: Span::new(0, 10),
2927        };
2928        resolver.emit_resolution_error(&mut diagnostics, &failure_2835);
2929
2930        // Test TS2792
2931        let failure_2792 = ResolutionFailure::ModuleResolutionModeMismatch {
2932            specifier: "esm-pkg".to_string(),
2933            containing_file: "/src/index.ts".to_string(),
2934            span: Span::new(5, 15),
2935        };
2936        resolver.emit_resolution_error(&mut diagnostics, &failure_2792);
2937
2938        assert_eq!(diagnostics.len(), 2);
2939
2940        let errors: Vec<_> = diagnostics.errors().collect();
2941        assert_eq!(errors[0].code, IMPORT_PATH_NEEDS_EXTENSION_SUGGESTION);
2942        assert_eq!(errors[1].code, MODULE_RESOLUTION_MODE_MISMATCH);
2943    }
2944
2945    // =========================================================================
2946    // ModuleExtension::from_path tests
2947    // =========================================================================
2948
2949    #[test]
2950    fn test_extension_from_path_ts() {
2951        assert_eq!(
2952            ModuleExtension::from_path(Path::new("foo.ts")),
2953            ModuleExtension::Ts
2954        );
2955    }
2956
2957    #[test]
2958    fn test_extension_from_path_tsx() {
2959        assert_eq!(
2960            ModuleExtension::from_path(Path::new("Component.tsx")),
2961            ModuleExtension::Tsx
2962        );
2963    }
2964
2965    #[test]
2966    fn test_extension_from_path_dts() {
2967        assert_eq!(
2968            ModuleExtension::from_path(Path::new("types.d.ts")),
2969            ModuleExtension::Dts
2970        );
2971    }
2972
2973    #[test]
2974    fn test_extension_from_path_dmts() {
2975        assert_eq!(
2976            ModuleExtension::from_path(Path::new("types.d.mts")),
2977            ModuleExtension::DmTs
2978        );
2979    }
2980
2981    #[test]
2982    fn test_extension_from_path_dcts() {
2983        assert_eq!(
2984            ModuleExtension::from_path(Path::new("types.d.cts")),
2985            ModuleExtension::DCts
2986        );
2987    }
2988
2989    #[test]
2990    fn test_extension_from_path_js() {
2991        assert_eq!(
2992            ModuleExtension::from_path(Path::new("bundle.js")),
2993            ModuleExtension::Js
2994        );
2995    }
2996
2997    #[test]
2998    fn test_extension_from_path_jsx() {
2999        assert_eq!(
3000            ModuleExtension::from_path(Path::new("App.jsx")),
3001            ModuleExtension::Jsx
3002        );
3003    }
3004
3005    #[test]
3006    fn test_extension_from_path_mjs() {
3007        assert_eq!(
3008            ModuleExtension::from_path(Path::new("module.mjs")),
3009            ModuleExtension::Mjs
3010        );
3011    }
3012
3013    #[test]
3014    fn test_extension_from_path_cjs() {
3015        assert_eq!(
3016            ModuleExtension::from_path(Path::new("config.cjs")),
3017            ModuleExtension::Cjs
3018        );
3019    }
3020
3021    #[test]
3022    fn test_extension_from_path_mts() {
3023        assert_eq!(
3024            ModuleExtension::from_path(Path::new("utils.mts")),
3025            ModuleExtension::Mts
3026        );
3027    }
3028
3029    #[test]
3030    fn test_extension_from_path_cts() {
3031        assert_eq!(
3032            ModuleExtension::from_path(Path::new("config.cts")),
3033            ModuleExtension::Cts
3034        );
3035    }
3036
3037    #[test]
3038    fn test_extension_from_path_json() {
3039        assert_eq!(
3040            ModuleExtension::from_path(Path::new("package.json")),
3041            ModuleExtension::Json
3042        );
3043    }
3044
3045    #[test]
3046    fn test_extension_from_path_unknown() {
3047        assert_eq!(
3048            ModuleExtension::from_path(Path::new("style.css")),
3049            ModuleExtension::Unknown
3050        );
3051    }
3052
3053    #[test]
3054    fn test_extension_from_path_no_extension() {
3055        assert_eq!(
3056            ModuleExtension::from_path(Path::new("Makefile")),
3057            ModuleExtension::Unknown
3058        );
3059    }
3060
3061    #[test]
3062    fn test_extension_from_path_nested() {
3063        assert_eq!(
3064            ModuleExtension::from_path(Path::new("/project/src/lib/types.d.ts")),
3065            ModuleExtension::Dts
3066        );
3067    }
3068
3069    // =========================================================================
3070    // ModuleExtension::as_str tests
3071    // =========================================================================
3072
3073    #[test]
3074    fn test_extension_as_str_roundtrip() {
3075        let extensions = [
3076            ModuleExtension::Ts,
3077            ModuleExtension::Tsx,
3078            ModuleExtension::Dts,
3079            ModuleExtension::DmTs,
3080            ModuleExtension::DCts,
3081            ModuleExtension::Js,
3082            ModuleExtension::Jsx,
3083            ModuleExtension::Mjs,
3084            ModuleExtension::Cjs,
3085            ModuleExtension::Mts,
3086            ModuleExtension::Cts,
3087            ModuleExtension::Json,
3088        ];
3089        for ext in &extensions {
3090            let ext_str = ext.as_str();
3091            assert!(
3092                !ext_str.is_empty(),
3093                "{ext:?} should have a non-empty string representation"
3094            );
3095            // Verify the string starts with a dot
3096            assert!(
3097                ext_str.starts_with('.'),
3098                "{ext:?}.as_str() should start with '.', got: {ext_str}"
3099            );
3100        }
3101        assert_eq!(ModuleExtension::Unknown.as_str(), "");
3102    }
3103
3104    // =========================================================================
3105    // ModuleExtension ESM/CJS mode tests
3106    // =========================================================================
3107
3108    #[test]
3109    fn test_extension_forces_esm() {
3110        assert!(ModuleExtension::Mts.forces_esm());
3111        assert!(ModuleExtension::Mjs.forces_esm());
3112        assert!(ModuleExtension::DmTs.forces_esm());
3113
3114        assert!(!ModuleExtension::Ts.forces_esm());
3115        assert!(!ModuleExtension::Tsx.forces_esm());
3116        assert!(!ModuleExtension::Dts.forces_esm());
3117        assert!(!ModuleExtension::Js.forces_esm());
3118        assert!(!ModuleExtension::Cjs.forces_esm());
3119        assert!(!ModuleExtension::Cts.forces_esm());
3120    }
3121
3122    #[test]
3123    fn test_extension_forces_cjs() {
3124        assert!(ModuleExtension::Cts.forces_cjs());
3125        assert!(ModuleExtension::Cjs.forces_cjs());
3126        assert!(ModuleExtension::DCts.forces_cjs());
3127
3128        assert!(!ModuleExtension::Ts.forces_cjs());
3129        assert!(!ModuleExtension::Tsx.forces_cjs());
3130        assert!(!ModuleExtension::Dts.forces_cjs());
3131        assert!(!ModuleExtension::Js.forces_cjs());
3132        assert!(!ModuleExtension::Mjs.forces_cjs());
3133        assert!(!ModuleExtension::Mts.forces_cjs());
3134    }
3135
3136    #[test]
3137    fn test_extension_neutral_mode() {
3138        // .ts, .tsx, .js, .jsx, .d.ts, .json should be neutral (neither ESM nor CJS forced)
3139        let neutral = [
3140            ModuleExtension::Ts,
3141            ModuleExtension::Tsx,
3142            ModuleExtension::Dts,
3143            ModuleExtension::Js,
3144            ModuleExtension::Jsx,
3145            ModuleExtension::Json,
3146            ModuleExtension::Unknown,
3147        ];
3148        for ext in &neutral {
3149            assert!(
3150                !ext.forces_esm() && !ext.forces_cjs(),
3151                "{ext:?} should be neutral (neither ESM nor CJS)"
3152            );
3153        }
3154    }
3155
3156    // =========================================================================
3157    // ResolutionFailure tests
3158    // =========================================================================
3159
3160    #[test]
3161    fn test_resolution_failure_not_found_is_not_found() {
3162        let failure = ResolutionFailure::NotFound {
3163            specifier: "./missing".to_string(),
3164            containing_file: "main.ts".to_string(),
3165            span: Span::new(0, 10),
3166        };
3167        assert!(failure.is_not_found());
3168    }
3169
3170    #[test]
3171    fn test_resolution_failure_other_is_not_not_found() {
3172        let failure = ResolutionFailure::ImportPathNeedsExtension {
3173            specifier: "./utils".to_string(),
3174            suggested_extension: ".js".to_string(),
3175            containing_file: "main.mts".to_string(),
3176            span: Span::new(0, 10),
3177        };
3178        assert!(!failure.is_not_found());
3179    }
3180
3181    #[test]
3182    fn test_resolution_failure_containing_file() {
3183        let failure = ResolutionFailure::NotFound {
3184            specifier: "./missing".to_string(),
3185            containing_file: "/project/src/main.ts".to_string(),
3186            span: Span::new(5, 20),
3187        };
3188        assert_eq!(failure.containing_file(), "/project/src/main.ts");
3189    }
3190
3191    #[test]
3192    fn test_resolution_failure_span() {
3193        let failure = ResolutionFailure::NotFound {
3194            specifier: "./missing".to_string(),
3195            containing_file: "main.ts".to_string(),
3196            span: Span::new(10, 30),
3197        };
3198        let span = failure.span();
3199        assert_eq!(span.start, 10);
3200        assert_eq!(span.end, 30);
3201    }
3202
3203    #[test]
3204    fn test_resolution_failure_to_diagnostic_ts2307() {
3205        let failure = ResolutionFailure::NotFound {
3206            specifier: "./nonexistent".to_string(),
3207            containing_file: "main.ts".to_string(),
3208            span: Span::new(0, 20),
3209        };
3210        let diag = failure.to_diagnostic();
3211        assert_eq!(diag.code, CANNOT_FIND_MODULE);
3212        assert!(diag.message.contains("./nonexistent"));
3213    }
3214
3215    #[test]
3216    fn test_resolution_failure_to_diagnostic_ts2835() {
3217        let failure = ResolutionFailure::ImportPathNeedsExtension {
3218            specifier: "./utils".to_string(),
3219            suggested_extension: ".js".to_string(),
3220            containing_file: "app.mts".to_string(),
3221            span: Span::new(0, 15),
3222        };
3223        let diag = failure.to_diagnostic();
3224        assert_eq!(diag.code, IMPORT_PATH_NEEDS_EXTENSION_SUGGESTION);
3225    }
3226
3227    #[test]
3228    fn test_resolution_failure_to_diagnostic_ts2792() {
3229        let failure = ResolutionFailure::ModuleResolutionModeMismatch {
3230            specifier: "some-esm-pkg".to_string(),
3231            containing_file: "index.ts".to_string(),
3232            span: Span::new(0, 20),
3233        };
3234        let diag = failure.to_diagnostic();
3235        assert_eq!(diag.code, MODULE_RESOLUTION_MODE_MISMATCH);
3236    }
3237
3238    // =========================================================================
3239    // ModuleResolver with temp files (integration)
3240    // =========================================================================
3241
3242    #[test]
3243    fn test_resolver_relative_ts_file() {
3244        use std::fs;
3245        let dir = std::env::temp_dir().join("tsz_test_resolver_relative");
3246        let _ = fs::remove_dir_all(&dir);
3247        fs::create_dir_all(&dir).unwrap();
3248
3249        fs::write(dir.join("main.ts"), "import { foo } from './utils';").unwrap();
3250        fs::write(dir.join("utils.ts"), "export const foo = 42;").unwrap();
3251
3252        let mut resolver = ModuleResolver::node_resolver();
3253        let result = resolver.resolve("./utils", &dir.join("main.ts"), Span::new(0, 10));
3254
3255        match result {
3256            Ok(module) => {
3257                assert_eq!(module.resolved_path, dir.join("utils.ts"));
3258                assert_eq!(module.extension, ModuleExtension::Ts);
3259                assert!(!module.is_external);
3260            }
3261            Err(_) => {
3262                // Resolution might fail in some environments, that's OK for this test
3263            }
3264        }
3265
3266        let _ = fs::remove_dir_all(&dir);
3267    }
3268
3269    #[test]
3270    fn test_resolver_relative_tsx_file() {
3271        use std::fs;
3272        let dir = std::env::temp_dir().join("tsz_test_resolver_tsx");
3273        let _ = fs::remove_dir_all(&dir);
3274        fs::create_dir_all(&dir).unwrap();
3275
3276        fs::write(dir.join("app.ts"), "").unwrap();
3277        fs::write(
3278            dir.join("Button.tsx"),
3279            "export default function Button() {}",
3280        )
3281        .unwrap();
3282
3283        let mut resolver = ModuleResolver::node_resolver();
3284        let result = resolver.resolve("./Button", &dir.join("app.ts"), Span::new(0, 10));
3285
3286        if let Ok(module) = result {
3287            assert_eq!(module.resolved_path, dir.join("Button.tsx"));
3288            assert_eq!(module.extension, ModuleExtension::Tsx);
3289        }
3290
3291        let _ = fs::remove_dir_all(&dir);
3292    }
3293
3294    #[test]
3295    fn test_resolver_index_file() {
3296        use std::fs;
3297        let dir = std::env::temp_dir().join("tsz_test_resolver_index");
3298        let _ = fs::remove_dir_all(&dir);
3299        fs::create_dir_all(dir.join("utils")).unwrap();
3300
3301        fs::write(dir.join("main.ts"), "").unwrap();
3302        fs::write(dir.join("utils").join("index.ts"), "export const foo = 42;").unwrap();
3303
3304        let mut resolver = ModuleResolver::node_resolver();
3305        let result = resolver.resolve("./utils", &dir.join("main.ts"), Span::new(0, 10));
3306
3307        if let Ok(module) = result {
3308            assert_eq!(module.resolved_path, dir.join("utils").join("index.ts"));
3309            assert_eq!(module.extension, ModuleExtension::Ts);
3310        }
3311
3312        let _ = fs::remove_dir_all(&dir);
3313    }
3314
3315    #[test]
3316    fn test_exports_js_target_substitutes_dts() {
3317        use std::fs;
3318        let dir = std::env::temp_dir().join("tsz_test_exports_js_target");
3319        let _ = fs::remove_dir_all(&dir);
3320        fs::create_dir_all(dir.join("node_modules/pkg")).unwrap();
3321        fs::create_dir_all(dir.join("src")).unwrap();
3322
3323        fs::write(
3324            dir.join("node_modules/pkg/package.json"),
3325            r#"{"name":"pkg","version":"0.0.1","exports":"./entrypoint.js"}"#,
3326        )
3327        .unwrap();
3328        fs::write(dir.join("node_modules/pkg/entrypoint.d.ts"), "export {};").unwrap();
3329        fs::write(dir.join("src/index.ts"), "import * as p from 'pkg';").unwrap();
3330
3331        let options = ResolvedCompilerOptions {
3332            module_resolution: Some(ModuleResolutionKind::Node16),
3333            resolve_package_json_exports: true,
3334            ..Default::default()
3335        };
3336
3337        let mut resolver = ModuleResolver::new(&options);
3338        let result = resolver.resolve("pkg", &dir.join("src/index.ts"), Span::new(0, 3));
3339
3340        // TypeScript resolves export targets with declaration substitution:
3341        // exports: "./entrypoint.js" → finds entrypoint.d.ts
3342        let resolved =
3343            result.expect("Expected exports .js target to resolve via .d.ts substitution");
3344        assert!(resolved.resolved_path.ends_with("entrypoint.d.ts"));
3345
3346        let _ = fs::remove_dir_all(&dir);
3347    }
3348
3349    #[test]
3350    fn test_resolver_dts_file() {
3351        use std::fs;
3352        let dir = std::env::temp_dir().join("tsz_test_resolver_dts");
3353        let _ = fs::remove_dir_all(&dir);
3354        fs::create_dir_all(&dir).unwrap();
3355
3356        fs::write(dir.join("main.ts"), "").unwrap();
3357        fs::write(dir.join("types.d.ts"), "export interface Foo {}").unwrap();
3358
3359        let mut resolver = ModuleResolver::node_resolver();
3360        let result = resolver.resolve("./types", &dir.join("main.ts"), Span::new(0, 10));
3361
3362        if let Ok(module) = result {
3363            assert_eq!(module.resolved_path, dir.join("types.d.ts"));
3364            assert_eq!(module.extension, ModuleExtension::Dts);
3365        }
3366
3367        let _ = fs::remove_dir_all(&dir);
3368    }
3369
3370    #[test]
3371    fn test_resolver_jsx_without_jsx_option_errors() {
3372        use std::fs;
3373        let dir = std::env::temp_dir().join("tsz_test_resolver_jsx_no_option");
3374        let _ = fs::remove_dir_all(&dir);
3375        fs::create_dir_all(&dir).unwrap();
3376
3377        fs::write(dir.join("app.ts"), "import jsx from './jsx';").unwrap();
3378        fs::write(dir.join("jsx.jsx"), "export default 1;").unwrap();
3379
3380        let options = ResolvedCompilerOptions {
3381            allow_js: true,
3382            jsx: None,
3383            // Use Node resolution so allowJs is respected (Classic never resolves .jsx)
3384            module_resolution: Some(ModuleResolutionKind::Node),
3385            ..Default::default()
3386        };
3387        let mut resolver = ModuleResolver::new(&options);
3388        let result = resolver.resolve("./jsx", &dir.join("app.ts"), Span::new(0, 10));
3389
3390        let failure = result.expect_err("Expected jsx resolution to fail without jsx option");
3391        let diagnostic = failure.to_diagnostic();
3392        assert_eq!(diagnostic.code, 6142);
3393
3394        let _ = fs::remove_dir_all(&dir);
3395    }
3396
3397    #[test]
3398    fn test_resolver_tsx_without_jsx_option_errors() {
3399        use std::fs;
3400        let dir = std::env::temp_dir().join("tsz_test_resolver_tsx_no_option");
3401        let _ = fs::remove_dir_all(&dir);
3402        fs::create_dir_all(&dir).unwrap();
3403
3404        fs::write(dir.join("app.ts"), "import tsx from './tsx';").unwrap();
3405        fs::write(dir.join("tsx.tsx"), "export default 1;").unwrap();
3406
3407        let options = ResolvedCompilerOptions {
3408            jsx: None,
3409            // Use Node resolution so .tsx files are found (Classic also finds .tsx, but be explicit)
3410            module_resolution: Some(ModuleResolutionKind::Node),
3411            ..Default::default()
3412        };
3413        let mut resolver = ModuleResolver::new(&options);
3414        let result = resolver.resolve("./tsx", &dir.join("app.ts"), Span::new(0, 10));
3415
3416        let failure = result.expect_err("Expected tsx resolution to fail without jsx option");
3417        let diagnostic = failure.to_diagnostic();
3418        assert_eq!(diagnostic.code, 6142);
3419
3420        let _ = fs::remove_dir_all(&dir);
3421    }
3422
3423    #[test]
3424    fn test_json_import_without_resolve_json_module() {
3425        use std::fs;
3426        let dir = std::env::temp_dir().join("tsz_test_ts2732");
3427        let _ = fs::remove_dir_all(&dir);
3428        fs::create_dir_all(&dir).unwrap();
3429
3430        fs::write(dir.join("app.ts"), "import data from './data.json';").unwrap();
3431        fs::write(dir.join("data.json"), "{\"value\": 42}").unwrap();
3432
3433        let options = ResolvedCompilerOptions {
3434            resolve_json_module: false, // JSON modules disabled
3435            ..Default::default()
3436        };
3437        let mut resolver = ModuleResolver::new(&options);
3438
3439        let result = resolver.resolve("./data.json", &dir.join("app.ts"), Span::new(0, 10));
3440
3441        let failure =
3442            result.expect_err("Expected JSON resolution to fail without resolveJsonModule");
3443        let diagnostic = failure.to_diagnostic();
3444        assert_eq!(diagnostic.code, 2732); // TS2732
3445
3446        let _ = fs::remove_dir_all(&dir);
3447    }
3448
3449    #[test]
3450    fn test_resolver_package_main_with_unknown_extension() {
3451        use std::fs;
3452        let dir = std::env::temp_dir().join("tsz_test_resolver_main_unknown");
3453        let _ = fs::remove_dir_all(&dir);
3454        fs::create_dir_all(dir.join("node_modules").join("normalize.css")).unwrap();
3455
3456        fs::write(dir.join("app.ts"), "import 'normalize.css';").unwrap();
3457        fs::write(
3458            dir.join("node_modules")
3459                .join("normalize.css")
3460                .join("normalize.css"),
3461            "body {}",
3462        )
3463        .unwrap();
3464        fs::write(
3465            dir.join("node_modules")
3466                .join("normalize.css")
3467                .join("package.json"),
3468            r#"{ "main": "normalize.css" }"#,
3469        )
3470        .unwrap();
3471
3472        let mut resolver = ModuleResolver::node_resolver();
3473        let result = resolver.resolve("normalize.css", &dir.join("app.ts"), Span::new(0, 10));
3474        assert!(
3475            result.is_ok(),
3476            "Expected package main with unknown extension to resolve"
3477        );
3478
3479        let _ = fs::remove_dir_all(&dir);
3480    }
3481
3482    #[test]
3483    fn test_resolver_package_types_with_unknown_extension() {
3484        use std::fs;
3485        let dir = std::env::temp_dir().join("tsz_test_resolver_types_unknown");
3486        let _ = fs::remove_dir_all(&dir);
3487        fs::create_dir_all(dir.join("node_modules").join("foo")).unwrap();
3488
3489        fs::write(dir.join("app.ts"), "import 'foo';").unwrap();
3490        fs::write(
3491            dir.join("node_modules").join("foo").join("foo.js"),
3492            "module.exports = {};",
3493        )
3494        .unwrap();
3495        fs::write(
3496            dir.join("node_modules").join("foo").join("package.json"),
3497            r#"{ "types": "foo.js" }"#,
3498        )
3499        .unwrap();
3500
3501        let mut resolver = ModuleResolver::node_resolver();
3502        let result = resolver.resolve("foo", &dir.join("app.ts"), Span::new(0, 10));
3503        assert!(
3504            result.is_ok(),
3505            "Expected package types with unknown extension to resolve"
3506        );
3507
3508        let _ = fs::remove_dir_all(&dir);
3509    }
3510
3511    #[test]
3512    fn test_resolver_package_types_js_without_allow_js_resolves() {
3513        use std::fs;
3514        let dir = std::env::temp_dir().join("tsz_test_resolver_types_js");
3515        let _ = fs::remove_dir_all(&dir);
3516        fs::create_dir_all(dir.join("node_modules").join("foo")).unwrap();
3517
3518        fs::write(dir.join("app.ts"), "import 'foo';").unwrap();
3519        fs::write(
3520            dir.join("node_modules").join("foo").join("foo.js"),
3521            "module.exports = {};",
3522        )
3523        .unwrap();
3524        fs::write(
3525            dir.join("node_modules").join("foo").join("package.json"),
3526            r#"{ "types": "foo.js" }"#,
3527        )
3528        .unwrap();
3529
3530        let mut resolver = ModuleResolver::node_resolver();
3531        let result = resolver.resolve("foo", &dir.join("app.ts"), Span::new(0, 10));
3532        assert!(
3533            result.is_ok(),
3534            "Expected types .js to resolve even without allowJs"
3535        );
3536
3537        let _ = fs::remove_dir_all(&dir);
3538    }
3539
3540    #[test]
3541    fn test_resolver_missing_file() {
3542        use std::fs;
3543        let dir = std::env::temp_dir().join("tsz_test_resolver_missing");
3544        let _ = fs::remove_dir_all(&dir);
3545        fs::create_dir_all(&dir).unwrap();
3546        fs::write(dir.join("main.ts"), "").unwrap();
3547
3548        let mut resolver = ModuleResolver::node_resolver();
3549        let result = resolver.resolve("./nonexistent", &dir.join("main.ts"), Span::new(0, 10));
3550
3551        assert!(result.is_err(), "Missing file should produce error");
3552        if let Err(failure) = result {
3553            assert!(failure.is_not_found());
3554        }
3555
3556        let _ = fs::remove_dir_all(&dir);
3557    }
3558
3559    // =========================================================================
3560    // PackageType tests
3561    // =========================================================================
3562
3563    #[test]
3564    fn test_package_type_default_is_commonjs() {
3565        assert_eq!(PackageType::default(), PackageType::CommonJs);
3566    }
3567
3568    #[test]
3569    fn test_importing_module_kind_default_is_commonjs() {
3570        assert_eq!(
3571            ImportingModuleKind::default(),
3572            ImportingModuleKind::CommonJs
3573        );
3574    }
3575}