Skip to main content

tsz_cli/
driver_resolution.rs

1use rustc_hash::{FxHashMap, FxHashSet};
2use serde::Deserialize;
3use std::path::{Path, PathBuf};
4
5use crate::config::{ModuleResolutionKind, PathMapping, ResolvedCompilerOptions};
6use crate::fs::is_valid_module_file;
7use tsz::emitter::ModuleKind;
8use tsz::parser::NodeIndex;
9use tsz::parser::ParserState;
10use tsz::parser::node::{NodeAccess, NodeArena};
11use tsz::scanner::SyntaxKind;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14enum PackageType {
15    Module,
16    CommonJs,
17}
18
19#[derive(Default)]
20pub(crate) struct ModuleResolutionCache {
21    package_type_by_dir: FxHashMap<PathBuf, Option<PackageType>>,
22}
23
24impl ModuleResolutionCache {
25    fn package_type_for_dir(&mut self, dir: &Path, base_dir: &Path) -> Option<PackageType> {
26        let mut current = dir;
27        let mut visited = Vec::new();
28
29        loop {
30            if let Some(value) = self.package_type_by_dir.get(current).copied() {
31                for path in visited {
32                    self.package_type_by_dir.insert(path, value);
33                }
34                return value;
35            }
36
37            visited.push(current.to_path_buf());
38
39            if let Some(package_json) = read_package_json(&current.join("package.json")) {
40                let value = package_type_from_json(Some(&package_json));
41                for path in visited {
42                    self.package_type_by_dir.insert(path, value);
43                }
44                return value;
45            }
46
47            if current == base_dir {
48                for path in visited {
49                    self.package_type_by_dir.insert(path, None);
50                }
51                return None;
52            }
53
54            let Some(parent) = current.parent() else {
55                for path in visited {
56                    self.package_type_by_dir.insert(path, None);
57                }
58                return None;
59            };
60            current = parent;
61        }
62    }
63}
64
65pub(crate) fn resolve_type_package_from_roots(
66    name: &str,
67    roots: &[PathBuf],
68    options: &ResolvedCompilerOptions,
69) -> Option<PathBuf> {
70    let candidates = type_package_candidates(name);
71    if candidates.is_empty() {
72        return None;
73    }
74
75    for root in roots {
76        for candidate in &candidates {
77            let package_root = root.join(candidate);
78            if !package_root.is_dir() {
79                continue;
80            }
81            if let Some(entry) = resolve_type_package_entry(&package_root, options) {
82                return Some(entry);
83            }
84        }
85    }
86
87    None
88}
89
90/// Public wrapper for `type_package_candidates`.
91pub(crate) fn type_package_candidates_pub(name: &str) -> Vec<String> {
92    type_package_candidates(name)
93}
94
95fn type_package_candidates(name: &str) -> Vec<String> {
96    let trimmed = name.trim();
97    if trimmed.is_empty() {
98        return Vec::new();
99    }
100
101    let normalized = trimmed.replace('\\', "/");
102    let mut candidates = Vec::new();
103
104    if let Some(stripped) = normalized.strip_prefix("@types/")
105        && !stripped.is_empty()
106    {
107        candidates.push(stripped.to_string());
108    }
109
110    if !candidates.iter().any(|value| value == &normalized) {
111        candidates.push(normalized);
112    }
113
114    candidates
115}
116
117pub(crate) fn collect_type_packages_from_root(root: &Path) -> Vec<PathBuf> {
118    let mut packages = Vec::new();
119    let entries = match std::fs::read_dir(root) {
120        Ok(entries) => entries,
121        Err(_) => return packages,
122    };
123
124    for entry in entries.flatten() {
125        let path = entry.path();
126        if !path.is_dir() {
127            continue;
128        }
129        let name = entry.file_name();
130        let name = name.to_string_lossy();
131        if name.starts_with('.') {
132            continue;
133        }
134        if name.starts_with('@') {
135            if let Ok(scope_entries) = std::fs::read_dir(&path) {
136                for scope_entry in scope_entries.flatten() {
137                    let scope_path = scope_entry.path();
138                    if scope_path.is_dir() {
139                        packages.push(scope_path);
140                    }
141                }
142            }
143            continue;
144        }
145        packages.push(path);
146    }
147
148    packages
149}
150
151pub(crate) fn resolve_type_package_entry(
152    package_root: &Path,
153    options: &ResolvedCompilerOptions,
154) -> Option<PathBuf> {
155    let package_json = read_package_json(&package_root.join("package.json"));
156
157    // In node10/classic module resolution, type package fallback resolution
158    // should NOT try .d.mts/.d.cts extensions (those require exports map).
159    // Only bundler/node16/nodenext try the full extension set.
160    let use_restricted_extensions = matches!(
161        options.effective_module_resolution(),
162        ModuleResolutionKind::Node | ModuleResolutionKind::Classic
163    );
164
165    if use_restricted_extensions {
166        // Use restricted resolution: only types/typings/main + index.d.ts fallback
167        let mut candidates = Vec::new();
168        if let Some(ref pj) = package_json {
169            candidates = collect_package_entry_candidates(pj);
170        }
171        if !candidates
172            .iter()
173            .any(|entry| entry == "index" || entry == "./index")
174        {
175            candidates.push("index".to_string());
176        }
177        // Only try .ts, .tsx, .d.ts extensions (no .d.mts/.d.cts)
178        let restricted_extensions = &["ts", "tsx", "d.ts"];
179        for entry_name in candidates {
180            let entry_name = entry_name.trim().trim_start_matches("./");
181            let path = package_root.join(entry_name);
182            for ext in restricted_extensions {
183                let candidate = path.with_extension(ext);
184                if candidate.is_file() && is_declaration_file(&candidate) {
185                    return Some(canonicalize_or_owned(&candidate));
186                }
187            }
188        }
189        None
190    } else {
191        // For bundler/node16/nodenext, use resolve_package_specifier which respects
192        // the exports map. This is needed for type packages that use conditional exports
193        // (e.g. `"exports": { ".": { "import": "./index.d.mts", "require": "./index.d.cts" } }`)
194        let conditions = export_conditions(options);
195        let resolved = resolve_package_specifier(
196            package_root,
197            None,
198            package_json.as_ref(),
199            &conditions,
200            options,
201        )?;
202        is_declaration_file(&resolved).then_some(resolved)
203    }
204}
205
206/// Resolve a type package entry using a specific resolution-mode condition.
207///
208/// When `resolution_mode` is "import" or "require", the exports map is consulted
209/// with the corresponding condition. This implements the `resolution-mode` attribute
210/// of `/// <reference types="..." resolution-mode="..." />` directives.
211pub(crate) fn resolve_type_package_entry_with_mode(
212    package_root: &Path,
213    resolution_mode: &str,
214    options: &ResolvedCompilerOptions,
215) -> Option<PathBuf> {
216    let package_json = read_package_json(&package_root.join("package.json"));
217    let package_json = package_json.as_ref()?;
218
219    // Build conditions based on resolution mode
220    let conditions: Vec<&str> = match resolution_mode {
221        "require" => vec!["require", "types", "default"],
222        "import" => vec!["import", "types", "default"],
223        _ => return None,
224    };
225
226    // Try the exports map first
227    if let Some(exports) = &package_json.exports
228        && let Some(target) = resolve_exports_subpath(exports, ".", &conditions)
229    {
230        let target_path = package_root.join(target.trim_start_matches("./"));
231        // Try to find a declaration file at the target
232        let package_type = package_type_from_json(Some(package_json));
233        for candidate in expand_module_path_candidates(&target_path, options, package_type) {
234            if candidate.is_file() && is_declaration_file(&candidate) {
235                return Some(canonicalize_or_owned(&candidate));
236            }
237        }
238        // Try exact path
239        if target_path.is_file() && is_declaration_file(&target_path) {
240            return Some(canonicalize_or_owned(&target_path));
241        }
242    }
243
244    None
245}
246
247pub(crate) fn default_type_roots(base_dir: &Path) -> Vec<PathBuf> {
248    let candidate = base_dir.join("node_modules").join("@types");
249    if candidate.is_dir() {
250        vec![canonicalize_or_owned(&candidate)]
251    } else {
252        Vec::new()
253    }
254}
255
256pub(crate) fn collect_module_specifiers_from_text(path: &Path, text: &str) -> Vec<String> {
257    let file_name = path.to_string_lossy().into_owned();
258    let mut parser = ParserState::new(file_name, text.to_string());
259    let source_file = parser.parse_source_file();
260    let (arena, _diagnostics) = parser.into_parts();
261    collect_module_specifiers(&arena, source_file)
262        .into_iter()
263        .map(|(specifier, _, _)| specifier)
264        .collect()
265}
266
267pub(crate) fn collect_module_specifiers(
268    arena: &NodeArena,
269    source_file: NodeIndex,
270) -> Vec<(String, NodeIndex, tsz::module_resolver::ImportKind)> {
271    use tsz::module_resolver::ImportKind;
272    let mut specifiers = Vec::new();
273
274    let Some(source) = arena.get_source_file_at(source_file) else {
275        return specifiers;
276    };
277
278    // Helper to strip surrounding quotes from a module specifier
279    let strip_quotes =
280        |s: &str| -> String { s.trim_matches(|c| c == '"' || c == '\'').to_string() };
281
282    for &stmt_idx in &source.statements.nodes {
283        if stmt_idx.is_none() {
284            continue;
285        }
286        let Some(stmt) = arena.get(stmt_idx) else {
287            continue;
288        };
289
290        // Handle ES6 imports: import { x } from './module'
291        // and import equals with require: import x = require('./module')
292        if let Some(import_decl) = arena.get_import_decl(stmt) {
293            // Check if this is an import equals declaration (kind 272 = CJS require)
294            // vs a regular import declaration (kind 273 = ESM import)
295            let is_import_equals =
296                stmt.kind == tsz::parser::syntax_kind_ext::IMPORT_EQUALS_DECLARATION;
297
298            if let Some(text) = arena.get_literal_text(import_decl.module_specifier) {
299                let kind = if is_import_equals {
300                    ImportKind::CjsRequire
301                } else {
302                    ImportKind::EsmImport
303                };
304                specifiers.push((strip_quotes(text), import_decl.module_specifier, kind));
305            } else {
306                // Handle import equals declaration: import x = require('./module')
307                // The module_specifier might be a CallExpression for require()
308                if let Some(spec_text) =
309                    extract_require_specifier(arena, import_decl.module_specifier)
310                {
311                    specifiers.push((
312                        spec_text,
313                        import_decl.module_specifier,
314                        ImportKind::CjsRequire,
315                    ));
316                }
317            }
318        }
319
320        // Handle exports: export { x } from './module'
321        if let Some(export_decl) = arena.get_export_decl(stmt) {
322            if let Some(text) = arena.get_literal_text(export_decl.module_specifier) {
323                specifiers.push((
324                    strip_quotes(text),
325                    export_decl.module_specifier,
326                    ImportKind::EsmReExport,
327                ));
328            } else if export_decl.export_clause.is_some()
329                && let Some(import_decl) = arena.get_import_decl_at(export_decl.export_clause)
330                && let Some(text) = arena.get_literal_text(import_decl.module_specifier)
331            {
332                specifiers.push((
333                    strip_quotes(text),
334                    import_decl.module_specifier,
335                    ImportKind::EsmReExport,
336                ));
337            }
338        }
339
340        // Handle ambient module declarations: declare module "x" { ... }
341        if let Some(module_decl) = arena.get_module(stmt) {
342            let has_declare = module_decl.modifiers.as_ref().is_some_and(|mods| {
343                mods.nodes.iter().any(|&mod_idx| {
344                    arena
345                        .get(mod_idx)
346                        .is_some_and(|node| node.kind == SyntaxKind::DeclareKeyword as u16)
347                })
348            });
349            if has_declare && let Some(text) = arena.get_literal_text(module_decl.name) {
350                specifiers.push((strip_quotes(text), module_decl.name, ImportKind::EsmImport));
351            }
352        }
353    }
354
355    // Also collect dynamic imports from expression statements
356    collect_dynamic_imports(arena, source_file, &strip_quotes, &mut specifiers);
357
358    specifiers
359}
360
361/// Collect dynamic `import()` expressions from the AST
362fn collect_dynamic_imports(
363    arena: &NodeArena,
364    _source_file: NodeIndex,
365    strip_quotes: &dyn Fn(&str) -> String,
366    specifiers: &mut Vec<(String, NodeIndex, tsz::module_resolver::ImportKind)>,
367) {
368    use tsz::parser::syntax_kind_ext;
369    use tsz::scanner::SyntaxKind;
370
371    // Iterate all nodes looking for CallExpression with ImportKeyword callee
372    for i in 0..arena.nodes.len() {
373        let node = &arena.nodes[i];
374        if node.kind != syntax_kind_ext::CALL_EXPRESSION {
375            continue;
376        }
377        let Some(call) = arena.get_call_expr(node) else {
378            continue;
379        };
380        // Check if the callee is an ImportKeyword (dynamic import)
381        let Some(callee) = arena.get(call.expression) else {
382            continue;
383        };
384        if callee.kind != SyntaxKind::ImportKeyword as u16 {
385            continue;
386        }
387        // Get the first argument (the module specifier)
388        let Some(args) = call.arguments.as_ref() else {
389            continue;
390        };
391        let Some(&arg_idx) = args.nodes.first() else {
392            continue;
393        };
394        if arg_idx.is_none() {
395            continue;
396        }
397        if let Some(text) = arena.get_literal_text(arg_idx) {
398            specifiers.push((
399                strip_quotes(text),
400                arg_idx,
401                tsz::module_resolver::ImportKind::DynamicImport,
402            ));
403        }
404    }
405}
406
407/// Extract module specifier from a `require()` call expression
408/// e.g., `require('./module')` -> `./module` (without quotes)
409fn extract_require_specifier(arena: &NodeArena, idx: NodeIndex) -> Option<String> {
410    use tsz::parser::syntax_kind_ext;
411    use tsz::scanner::SyntaxKind;
412
413    let node = arena.get(idx)?;
414
415    // Helper to strip surrounding quotes from a string
416    let strip_quotes =
417        |s: &str| -> String { s.trim_matches(|c| c == '"' || c == '\'').to_string() };
418
419    // If it's directly a string literal, return it (without quotes)
420    if let Some(text) = arena.get_literal_text(idx) {
421        return Some(strip_quotes(text));
422    }
423
424    // Check if it's a require() call expression
425    if node.kind != syntax_kind_ext::CALL_EXPRESSION {
426        return None;
427    }
428
429    let call = arena.get_call_expr(node)?;
430
431    // Check that the callee is 'require' (an identifier)
432    let callee_node = arena.get(call.expression)?;
433    if callee_node.kind != SyntaxKind::Identifier as u16 {
434        return None;
435    }
436    let callee_text = arena.get_identifier_text(call.expression)?;
437    if callee_text != "require" {
438        return None;
439    }
440
441    // Get the first argument (the module specifier)
442    let args = call.arguments.as_ref()?;
443    let arg_idx = args.nodes.first()?;
444    if arg_idx.is_none() {
445        return None;
446    }
447
448    // Get the literal text of the argument (without quotes)
449    arena.get_literal_text(*arg_idx).map(strip_quotes)
450}
451
452pub(crate) fn collect_import_bindings(
453    arena: &NodeArena,
454    source_file: NodeIndex,
455) -> Vec<(String, Vec<String>)> {
456    let mut bindings = Vec::new();
457    let Some(source) = arena.get_source_file_at(source_file) else {
458        return bindings;
459    };
460
461    for &stmt_idx in &source.statements.nodes {
462        if stmt_idx.is_none() {
463            continue;
464        }
465        let Some(import_decl) = arena.get_import_decl_at(stmt_idx) else {
466            continue;
467        };
468        let Some(specifier) = arena.get_literal_text(import_decl.module_specifier) else {
469            continue;
470        };
471        let local_names = collect_import_local_names(arena, import_decl);
472        if !local_names.is_empty() {
473            bindings.push((specifier.to_string(), local_names));
474        }
475    }
476
477    bindings
478}
479
480pub(crate) fn collect_export_binding_nodes(
481    arena: &NodeArena,
482    source_file: NodeIndex,
483) -> Vec<(String, Vec<NodeIndex>)> {
484    let mut bindings = Vec::new();
485    let Some(source) = arena.get_source_file_at(source_file) else {
486        return bindings;
487    };
488
489    for &stmt_idx in &source.statements.nodes {
490        if stmt_idx.is_none() {
491            continue;
492        }
493        let Some(export_decl) = arena.get_export_decl_at(stmt_idx) else {
494            continue;
495        };
496        if export_decl.export_clause.is_none() {
497            continue;
498        }
499        let clause_idx = export_decl.export_clause;
500        let Some(clause_node) = arena.get(clause_idx) else {
501            continue;
502        };
503
504        let import_decl = arena.get_import_decl(clause_node);
505        let mut specifier = arena
506            .get_literal_text(export_decl.module_specifier)
507            .map(std::string::ToString::to_string);
508        if specifier.is_none()
509            && let Some(import_decl) = import_decl
510            && let Some(text) = arena.get_literal_text(import_decl.module_specifier)
511        {
512            specifier = Some(text.to_string());
513        }
514        let Some(specifier) = specifier else {
515            continue;
516        };
517
518        let mut nodes = Vec::new();
519        if import_decl.is_some() {
520            nodes.push(clause_idx);
521        } else if let Some(named) = arena.get_named_imports(clause_node) {
522            for &spec_idx in &named.elements.nodes {
523                if spec_idx.is_some() {
524                    nodes.push(spec_idx);
525                }
526            }
527        } else if arena.get_identifier_text(clause_idx).is_some() {
528            nodes.push(clause_idx);
529        }
530
531        if !nodes.is_empty() {
532            bindings.push((specifier.to_string(), nodes));
533        }
534    }
535
536    bindings
537}
538
539pub(crate) fn collect_star_export_specifiers(
540    arena: &NodeArena,
541    source_file: NodeIndex,
542) -> Vec<String> {
543    let mut specifiers = Vec::new();
544    let Some(source) = arena.get_source_file_at(source_file) else {
545        return specifiers;
546    };
547
548    for &stmt_idx in &source.statements.nodes {
549        if stmt_idx.is_none() {
550            continue;
551        }
552        let Some(export_decl) = arena.get_export_decl_at(stmt_idx) else {
553            continue;
554        };
555        if export_decl.export_clause.is_some() {
556            continue;
557        }
558        if let Some(text) = arena.get_literal_text(export_decl.module_specifier) {
559            specifiers.push(text.to_string());
560        }
561    }
562
563    specifiers
564}
565
566fn collect_import_local_names(
567    arena: &NodeArena,
568    import_decl: &tsz::parser::node::ImportDeclData,
569) -> Vec<String> {
570    let mut names = Vec::new();
571    if import_decl.import_clause.is_none() {
572        return names;
573    }
574
575    let clause_idx = import_decl.import_clause;
576    if let Some(clause_node) = arena.get(clause_idx) {
577        if let Some(clause) = arena.get_import_clause(clause_node) {
578            if clause.name.is_some()
579                && let Some(name) = arena.get_identifier_text(clause.name)
580            {
581                names.push(name.to_string());
582            }
583
584            if clause.named_bindings.is_some()
585                && let Some(bindings_node) = arena.get(clause.named_bindings)
586            {
587                if bindings_node.kind == SyntaxKind::Identifier as u16 {
588                    if let Some(name) = arena.get_identifier_text(clause.named_bindings) {
589                        names.push(name.to_string());
590                    }
591                } else if let Some(named) = arena.get_named_imports(bindings_node) {
592                    if named.name.is_some()
593                        && let Some(name) = arena.get_identifier_text(named.name)
594                    {
595                        names.push(name.to_string());
596                    }
597                    for &spec_idx in &named.elements.nodes {
598                        let Some(spec) = arena.get_specifier_at(spec_idx) else {
599                            continue;
600                        };
601                        let local_ident = if spec.name.is_some() {
602                            spec.name
603                        } else {
604                            spec.property_name
605                        };
606                        if let Some(name) = arena.get_identifier_text(local_ident) {
607                            names.push(name.to_string());
608                        }
609                    }
610                }
611            }
612        } else if let Some(name) = arena.get_identifier_text(clause_idx) {
613            names.push(name.to_string());
614        }
615    } else if let Some(name) = arena.get_identifier_text(clause_idx) {
616        names.push(name.to_string());
617    }
618
619    names
620}
621
622pub(crate) fn resolve_module_specifier(
623    from_file: &Path,
624    module_specifier: &str,
625    options: &ResolvedCompilerOptions,
626    base_dir: &Path,
627    resolution_cache: &mut ModuleResolutionCache,
628    known_files: &FxHashSet<PathBuf>,
629) -> Option<PathBuf> {
630    let debug = std::env::var_os("TSZ_DEBUG_RESOLVE").is_some();
631    if debug {
632        tracing::debug!(
633            "resolve_module_specifier: from_file={from_file:?}, specifier={module_specifier:?}, resolution={:?}, base_url={:?}",
634            options.effective_module_resolution(),
635            options.base_url
636        );
637    }
638    let specifier = module_specifier.trim();
639    if specifier.is_empty() {
640        return None;
641    }
642    let specifier = specifier.replace('\\', "/");
643    if specifier.starts_with('#') {
644        if options.resolve_package_json_imports {
645            return resolve_package_imports_specifier(from_file, &specifier, base_dir, options);
646        }
647        return None;
648    }
649    let resolution = options.effective_module_resolution();
650    let mut candidates = Vec::new();
651
652    let from_dir = from_file.parent().unwrap_or(base_dir);
653    let package_type = match resolution {
654        ModuleResolutionKind::Node16 | ModuleResolutionKind::NodeNext => {
655            resolution_cache.package_type_for_dir(from_dir, base_dir)
656        }
657        _ => None,
658    };
659
660    let mut allow_node_modules = false;
661    let mut path_mapping_attempted = false;
662
663    if Path::new(&specifier).is_absolute() {
664        candidates.extend(expand_module_path_candidates(
665            &PathBuf::from(specifier.as_str()),
666            options,
667            package_type,
668        ));
669    } else if specifier.starts_with('.') {
670        let joined = from_dir.join(&specifier);
671        candidates.extend(expand_module_path_candidates(
672            &joined,
673            options,
674            package_type,
675        ));
676    } else if matches!(resolution, ModuleResolutionKind::Classic) {
677        if options.base_url.is_some()
678            && let Some(paths) = options.paths.as_ref()
679            && let Some((mapping, wildcard)) = select_path_mapping(paths, &specifier)
680        {
681            path_mapping_attempted = true;
682            let base = options.base_url.as_ref().expect("baseUrl present");
683            for target in &mapping.targets {
684                let substituted = substitute_path_target(target, &wildcard);
685                let path = if Path::new(&substituted).is_absolute() {
686                    PathBuf::from(substituted)
687                } else {
688                    base.join(substituted)
689                };
690                candidates.extend(expand_module_path_candidates(&path, options, package_type));
691            }
692        }
693
694        // Classic resolution always walks up the directory tree from the containing
695        // file's directory, probing for <specifier>.ts/.tsx/.d.ts and related candidates.
696        // This runs even when baseUrl/path-mapping candidates were generated, matching
697        // TypeScript behavior where classic resolution falls back to relative ancestor checks.
698        // Unlike Node resolution, Classic resolution walks up for all specifiers including
699        // bare module specifiers (e.g., "module3") since it has no node_modules concept.
700        {
701            let mut current = from_dir.to_path_buf();
702            loop {
703                candidates.extend(expand_module_path_candidates(
704                    &current.join(&specifier),
705                    options,
706                    package_type,
707                ));
708
709                match current.parent() {
710                    Some(parent) if parent != current => current = parent.to_path_buf(),
711                    _ => break,
712                }
713            }
714        }
715    } else if let Some(base_url) = options.base_url.as_ref() {
716        allow_node_modules = true;
717        if let Some(paths) = options.paths.as_ref()
718            && let Some((mapping, wildcard)) = select_path_mapping(paths, &specifier)
719        {
720            path_mapping_attempted = true;
721            for target in &mapping.targets {
722                let substituted = substitute_path_target(target, &wildcard);
723                let path = if Path::new(&substituted).is_absolute() {
724                    PathBuf::from(substituted)
725                } else {
726                    base_url.join(substituted)
727                };
728                candidates.extend(expand_module_path_candidates(&path, options, package_type));
729            }
730        }
731
732        if candidates.is_empty() {
733            candidates.extend(expand_module_path_candidates(
734                &base_url.join(&specifier),
735                options,
736                package_type,
737            ));
738        }
739    } else {
740        allow_node_modules = true;
741    }
742
743    for candidate in candidates {
744        // Check if candidate exists in known files (for virtual test files) or on filesystem
745        let exists = known_files.contains(&candidate)
746            || (candidate.is_file() && is_valid_module_file(&candidate));
747        if debug {
748            tracing::debug!("candidate={candidate:?} exists={exists}");
749        }
750
751        if exists {
752            return Some(canonicalize_or_owned(&candidate));
753        }
754    }
755
756    // TypeScript falls through to Classic-style directory walking when path mappings
757    // were attempted but did not resolve. This matches behavior where path mapping
758    // misses are not treated as terminal failures in classic mode.
759    if path_mapping_attempted && matches!(resolution, ModuleResolutionKind::Classic) {
760        let mut current = from_dir.to_path_buf();
761        loop {
762            for candidate in
763                expand_module_path_candidates(&current.join(&specifier), options, package_type)
764            {
765                let exists = known_files.contains(&candidate)
766                    || (candidate.is_file() && is_valid_module_file(&candidate));
767                if debug {
768                    tracing::debug!("classic-fallback candidate={candidate:?} exists={exists}");
769                }
770                if exists {
771                    return Some(canonicalize_or_owned(&candidate));
772                }
773            }
774
775            match current.parent() {
776                Some(parent) if parent != current => current = parent.to_path_buf(),
777                _ => break,
778            }
779        }
780    }
781
782    if allow_node_modules {
783        return resolve_node_module_specifier(from_file, &specifier, base_dir, options);
784    }
785
786    None
787}
788
789fn select_path_mapping<'a>(
790    mappings: &'a [PathMapping],
791    specifier: &str,
792) -> Option<(&'a PathMapping, String)> {
793    let mut best: Option<(&PathMapping, String)> = None;
794    let mut best_score = 0usize;
795    let mut best_pattern_len = 0usize;
796
797    for mapping in mappings {
798        let Some(wildcard) = mapping.match_specifier(specifier) else {
799            continue;
800        };
801        let score = mapping.specificity();
802        let pattern_len = mapping.pattern.len();
803
804        let is_better = match &best {
805            None => true,
806            Some((current, _)) => {
807                score > best_score
808                    || (score == best_score && pattern_len > best_pattern_len)
809                    || (score == best_score
810                        && pattern_len == best_pattern_len
811                        && mapping.pattern < current.pattern)
812            }
813        };
814
815        if is_better {
816            best_score = score;
817            best_pattern_len = pattern_len;
818            best = Some((mapping, wildcard));
819        }
820    }
821
822    best
823}
824
825fn substitute_path_target(target: &str, wildcard: &str) -> String {
826    if target.contains('*') {
827        target.replace('*', wildcard)
828    } else {
829        target.to_string()
830    }
831}
832
833fn expand_module_path_candidates(
834    path: &Path,
835    options: &ResolvedCompilerOptions,
836    package_type: Option<PackageType>,
837) -> Vec<PathBuf> {
838    let base = normalize_path(path);
839    let mut default_suffixes: Vec<String> = Vec::new();
840    let suffixes = if options.module_suffixes.is_empty() {
841        default_suffixes.push(String::new());
842        &default_suffixes
843    } else {
844        &options.module_suffixes
845    };
846    if let Some((base_no_ext, extension)) = split_path_extension(&base) {
847        // Try extension substitution (.js → .ts/.tsx/.d.ts) for all resolution modes.
848        // TypeScript resolves `.js` imports to `.ts` sources in all modes.
849        let mut candidates = Vec::new();
850        if let Some(rewritten) = node16_extension_substitution(&base, extension) {
851            for candidate in rewritten {
852                candidates.extend(candidates_with_suffixes(&candidate, suffixes));
853            }
854        }
855        // Also include the original extension as fallback
856        candidates.extend(candidates_with_suffixes_and_extension(
857            &base_no_ext,
858            extension,
859            suffixes,
860        ));
861        return candidates;
862    }
863
864    let extensions = extension_candidates_for_resolution(options, package_type);
865    let mut candidates = Vec::new();
866    for ext in extensions {
867        candidates.extend(candidates_with_suffixes_and_extension(&base, ext, suffixes));
868    }
869    if options.resolve_json_module {
870        candidates.extend(candidates_with_suffixes_and_extension(
871            &base, "json", suffixes,
872        ));
873    }
874    let index = base.join("index");
875    for ext in extensions {
876        candidates.extend(candidates_with_suffixes_and_extension(
877            &index, ext, suffixes,
878        ));
879    }
880    if options.resolve_json_module {
881        candidates.extend(candidates_with_suffixes_and_extension(
882            &index, "json", suffixes,
883        ));
884    }
885    candidates
886}
887
888fn expand_export_path_candidates(
889    path: &Path,
890    options: &ResolvedCompilerOptions,
891    package_type: Option<PackageType>,
892) -> Vec<PathBuf> {
893    let base = normalize_path(path);
894    let suffixes = &options.module_suffixes;
895    if let Some((base_no_ext, extension)) = split_path_extension(&base) {
896        return candidates_with_suffixes_and_extension(&base_no_ext, extension, suffixes);
897    }
898
899    let extensions = extension_candidates_for_resolution(options, package_type);
900    let mut candidates = Vec::new();
901    for ext in extensions {
902        candidates.extend(candidates_with_suffixes_and_extension(&base, ext, suffixes));
903    }
904    if options.resolve_json_module {
905        candidates.extend(candidates_with_suffixes_and_extension(
906            &base, "json", suffixes,
907        ));
908    }
909    let index = base.join("index");
910    for ext in extensions {
911        candidates.extend(candidates_with_suffixes_and_extension(
912            &index, ext, suffixes,
913        ));
914    }
915    if options.resolve_json_module {
916        candidates.extend(candidates_with_suffixes_and_extension(
917            &index, "json", suffixes,
918        ));
919    }
920    candidates
921}
922
923fn split_path_extension(path: &Path) -> Option<(PathBuf, &'static str)> {
924    let path_str = path.to_string_lossy();
925    for ext in KNOWN_EXTENSIONS {
926        if path_str.ends_with(ext) {
927            let base = &path_str[..path_str.len().saturating_sub(ext.len())];
928            if base.is_empty() {
929                return None;
930            }
931            return Some((PathBuf::from(base), ext.trim_start_matches('.')));
932        }
933    }
934    None
935}
936
937fn candidates_with_suffixes(path: &Path, suffixes: &[String]) -> Vec<PathBuf> {
938    let Some((base, extension)) = split_path_extension(path) else {
939        return Vec::new();
940    };
941    candidates_with_suffixes_and_extension(&base, extension, suffixes)
942}
943
944fn candidates_with_suffixes_and_extension(
945    base: &Path,
946    extension: &str,
947    suffixes: &[String],
948) -> Vec<PathBuf> {
949    let mut candidates = Vec::new();
950    for suffix in suffixes {
951        if let Some(candidate) = path_with_suffix_and_extension(base, suffix, extension) {
952            candidates.push(candidate);
953        }
954    }
955    candidates
956}
957
958fn path_with_suffix_and_extension(base: &Path, suffix: &str, extension: &str) -> Option<PathBuf> {
959    let file_name = base.file_name()?.to_string_lossy();
960    let mut candidate = base.to_path_buf();
961    let mut new_name = String::with_capacity(file_name.len() + suffix.len() + extension.len() + 1);
962    new_name.push_str(&file_name);
963    new_name.push_str(suffix);
964    new_name.push('.');
965    new_name.push_str(extension);
966    candidate.set_file_name(new_name);
967    Some(candidate)
968}
969
970fn node16_extension_substitution(path: &Path, extension: &str) -> Option<Vec<PathBuf>> {
971    let replacements: &[&str] = match extension {
972        "js" => &["ts", "tsx", "d.ts"],
973        "jsx" => &["tsx", "d.ts"],
974        "mjs" => &["mts", "d.mts"],
975        "cjs" => &["cts", "d.cts"],
976        _ => return None,
977    };
978
979    Some(
980        replacements
981            .iter()
982            .map(|ext| path.with_extension(ext))
983            .collect(),
984    )
985}
986
987const fn extension_candidates_for_resolution(
988    options: &ResolvedCompilerOptions,
989    package_type: Option<PackageType>,
990) -> &'static [&'static str] {
991    match options.effective_module_resolution() {
992        ModuleResolutionKind::Node16 | ModuleResolutionKind::NodeNext => match package_type {
993            Some(PackageType::Module) => &NODE16_MODULE_EXTENSION_CANDIDATES,
994            Some(PackageType::CommonJs) => &NODE16_COMMONJS_EXTENSION_CANDIDATES,
995            None => &TS_EXTENSION_CANDIDATES,
996        },
997        _ => &TS_EXTENSION_CANDIDATES,
998    }
999}
1000
1001fn normalize_path(path: &Path) -> PathBuf {
1002    let mut normalized = PathBuf::new();
1003
1004    for component in path.components() {
1005        match component {
1006            std::path::Component::CurDir => {}
1007            std::path::Component::ParentDir => {
1008                normalized.pop();
1009            }
1010            std::path::Component::RootDir
1011            | std::path::Component::Normal(_)
1012            | std::path::Component::Prefix(_) => {
1013                normalized.push(component.as_os_str());
1014            }
1015        }
1016    }
1017
1018    normalized
1019}
1020
1021const KNOWN_EXTENSIONS: [&str; 12] = [
1022    ".d.mts", ".d.cts", ".d.ts", ".mts", ".cts", ".tsx", ".ts", ".mjs", ".cjs", ".jsx", ".js",
1023    ".json",
1024];
1025const TS_EXTENSION_CANDIDATES: [&str; 7] = ["ts", "tsx", "d.ts", "mts", "cts", "d.mts", "d.cts"];
1026const NODE16_MODULE_EXTENSION_CANDIDATES: [&str; 7] =
1027    ["mts", "d.mts", "ts", "tsx", "d.ts", "cts", "d.cts"];
1028const NODE16_COMMONJS_EXTENSION_CANDIDATES: [&str; 7] =
1029    ["cts", "d.cts", "ts", "tsx", "d.ts", "mts", "d.mts"];
1030
1031#[derive(Debug, Deserialize)]
1032struct PackageJson {
1033    #[serde(default)]
1034    types: Option<String>,
1035    #[serde(default)]
1036    typings: Option<String>,
1037    #[serde(default)]
1038    main: Option<String>,
1039    #[serde(default)]
1040    module: Option<String>,
1041    #[serde(default, rename = "type")]
1042    package_type: Option<String>,
1043    #[serde(default)]
1044    exports: Option<serde_json::Value>,
1045    #[serde(default)]
1046    imports: Option<serde_json::Value>,
1047    #[serde(default, rename = "typesVersions")]
1048    types_versions: Option<serde_json::Value>,
1049}
1050
1051#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
1052struct SemVer {
1053    major: u32,
1054    minor: u32,
1055    patch: u32,
1056}
1057
1058impl SemVer {
1059    const ZERO: Self = Self {
1060        major: 0,
1061        minor: 0,
1062        patch: 0,
1063    };
1064}
1065
1066// NOTE: Keep this in sync with the TypeScript version this compiler targets.
1067// TODO: Make this configurable once CLI plumbing is available.
1068const TYPES_VERSIONS_COMPILER_VERSION_FALLBACK: SemVer = SemVer {
1069    major: 6,
1070    minor: 0,
1071    patch: 0,
1072};
1073
1074fn types_versions_compiler_version(options: &ResolvedCompilerOptions) -> SemVer {
1075    options
1076        .types_versions_compiler_version
1077        .as_deref()
1078        .and_then(parse_semver)
1079        .unwrap_or_else(default_types_versions_compiler_version)
1080}
1081
1082const fn default_types_versions_compiler_version() -> SemVer {
1083    // Use the fallback version directly since the project's package.json version
1084    // is not a TypeScript version. The fallback represents the TypeScript version
1085    // that this compiler is compatible with for typesVersions resolution.
1086    TYPES_VERSIONS_COMPILER_VERSION_FALLBACK
1087}
1088
1089fn export_conditions(options: &ResolvedCompilerOptions) -> Vec<&'static str> {
1090    let resolution = options.effective_module_resolution();
1091    let mut conditions = Vec::new();
1092    push_condition(&mut conditions, "types");
1093
1094    match resolution {
1095        ModuleResolutionKind::Bundler => push_condition(&mut conditions, "browser"),
1096        ModuleResolutionKind::Classic
1097        | ModuleResolutionKind::Node
1098        | ModuleResolutionKind::Node16
1099        | ModuleResolutionKind::NodeNext => {
1100            push_condition(&mut conditions, "node");
1101        }
1102    }
1103
1104    match options.printer.module {
1105        ModuleKind::CommonJS | ModuleKind::AMD | ModuleKind::UMD | ModuleKind::System => {
1106            push_condition(&mut conditions, "require");
1107        }
1108        ModuleKind::ES2015
1109        | ModuleKind::ES2020
1110        | ModuleKind::ES2022
1111        | ModuleKind::ESNext
1112        | ModuleKind::Node16
1113        | ModuleKind::NodeNext => {
1114            push_condition(&mut conditions, "import");
1115        }
1116        _ => {}
1117    }
1118
1119    push_condition(&mut conditions, "default");
1120    match resolution {
1121        ModuleResolutionKind::Bundler => {
1122            push_condition(&mut conditions, "import");
1123            push_condition(&mut conditions, "require");
1124            push_condition(&mut conditions, "node");
1125        }
1126        ModuleResolutionKind::Classic
1127        | ModuleResolutionKind::Node
1128        | ModuleResolutionKind::Node16
1129        | ModuleResolutionKind::NodeNext => {
1130            push_condition(&mut conditions, "import");
1131            push_condition(&mut conditions, "require");
1132            push_condition(&mut conditions, "browser");
1133        }
1134    }
1135
1136    conditions
1137}
1138
1139fn push_condition(conditions: &mut Vec<&'static str>, condition: &'static str) {
1140    if !conditions.contains(&condition) {
1141        conditions.push(condition);
1142    }
1143}
1144
1145fn resolve_node_module_specifier(
1146    from_file: &Path,
1147    module_specifier: &str,
1148    base_dir: &Path,
1149    options: &ResolvedCompilerOptions,
1150) -> Option<PathBuf> {
1151    let (package_name, subpath) = split_package_specifier(module_specifier)?;
1152    let conditions = export_conditions(options);
1153    let mut current = from_file.parent().unwrap_or(base_dir);
1154
1155    loop {
1156        // 1. Look for the package itself in node_modules
1157        let package_root = current.join("node_modules").join(&package_name);
1158        if package_root.is_dir() {
1159            let package_json = read_package_json(&package_root.join("package.json"));
1160            let resolved = resolve_package_specifier(
1161                &package_root,
1162                subpath.as_deref(),
1163                package_json.as_ref(),
1164                &conditions,
1165                options,
1166            );
1167            if resolved.is_some() {
1168                return resolved;
1169            }
1170        } else if subpath.is_none()
1171            && options.effective_module_resolution() == ModuleResolutionKind::Bundler
1172        {
1173            let candidates = expand_module_path_candidates(&package_root, options, None);
1174            for candidate in candidates {
1175                if candidate.is_file() && is_valid_module_file(&candidate) {
1176                    return Some(canonicalize_or_owned(&candidate));
1177                }
1178            }
1179        }
1180
1181        // 2. Look for @types package (if not already looking for one)
1182        // TypeScript looks up @types/foo for 'foo', and @types/scope__pkg for '@scope/pkg'
1183        if !package_name.starts_with("@types/") {
1184            let types_package_name = if let Some(scope_pkg) = package_name.strip_prefix('@') {
1185                // Scoped package: @scope/pkg -> @types/scope__pkg
1186                // Skip the '@' (1 char) and replace '/' with '__'
1187                format!("@types/{}", scope_pkg.replace('/', "__"))
1188            } else {
1189                format!("@types/{package_name}")
1190            };
1191
1192            let types_root = current.join("node_modules").join(&types_package_name);
1193            if types_root.is_dir() {
1194                let package_json = read_package_json(&types_root.join("package.json"));
1195                let resolved = resolve_package_specifier(
1196                    &types_root,
1197                    subpath.as_deref(),
1198                    package_json.as_ref(),
1199                    &conditions,
1200                    options,
1201                );
1202                if resolved.is_some() {
1203                    return resolved;
1204                }
1205            }
1206        }
1207
1208        if current == base_dir {
1209            break;
1210        }
1211        let Some(parent) = current.parent() else {
1212            break;
1213        };
1214        current = parent;
1215    }
1216
1217    None
1218}
1219
1220fn resolve_package_imports_specifier(
1221    from_file: &Path,
1222    module_specifier: &str,
1223    base_dir: &Path,
1224    options: &ResolvedCompilerOptions,
1225) -> Option<PathBuf> {
1226    let conditions = export_conditions(options);
1227    let mut current = from_file.parent().unwrap_or(base_dir);
1228
1229    loop {
1230        let package_json_path = current.join("package.json");
1231        if package_json_path.is_file()
1232            && let Some(package_json) = read_package_json(&package_json_path)
1233            && let Some(imports) = package_json.imports.as_ref()
1234            && let Some(target) = resolve_imports_subpath(imports, module_specifier, &conditions)
1235        {
1236            let package_type = package_type_from_json(Some(&package_json));
1237            if let Some(resolved) = resolve_package_entry(current, &target, options, package_type) {
1238                return Some(resolved);
1239            }
1240        }
1241
1242        if current == base_dir {
1243            break;
1244        }
1245        let Some(parent) = current.parent() else {
1246            break;
1247        };
1248        current = parent;
1249    }
1250
1251    None
1252}
1253
1254fn resolve_package_specifier(
1255    package_root: &Path,
1256    subpath: Option<&str>,
1257    package_json: Option<&PackageJson>,
1258    conditions: &[&str],
1259    options: &ResolvedCompilerOptions,
1260) -> Option<PathBuf> {
1261    let package_type = package_type_from_json(package_json);
1262    if let Some(package_json) = package_json {
1263        if options.resolve_package_json_exports
1264            && let Some(exports) = package_json.exports.as_ref()
1265        {
1266            let subpath_key = match subpath {
1267                Some(value) => format!("./{value}"),
1268                None => ".".to_string(),
1269            };
1270            if let Some(target) = resolve_exports_subpath(exports, &subpath_key, conditions)
1271                && let Some(resolved) =
1272                    resolve_export_entry(package_root, &target, options, package_type)
1273            {
1274                return Some(resolved);
1275            }
1276        }
1277
1278        if let Some(types_versions) = package_json.types_versions.as_ref() {
1279            let types_subpath = subpath.unwrap_or("index");
1280            if let Some(resolved) = resolve_types_versions(
1281                package_root,
1282                types_subpath,
1283                types_versions,
1284                options,
1285                package_type,
1286            ) {
1287                return Some(resolved);
1288            }
1289        }
1290    }
1291
1292    if let Some(subpath) = subpath {
1293        return resolve_package_entry(package_root, subpath, options, package_type);
1294    }
1295
1296    resolve_package_root(package_root, package_json, options, package_type)
1297}
1298
1299fn split_package_specifier(specifier: &str) -> Option<(String, Option<String>)> {
1300    let mut parts = specifier.split('/');
1301    let first = parts.next()?;
1302
1303    if first.starts_with('@') {
1304        let second = parts.next()?;
1305        let package = format!("{first}/{second}");
1306        let rest = parts.collect::<Vec<_>>().join("/");
1307        let subpath = if rest.is_empty() { None } else { Some(rest) };
1308        return Some((package, subpath));
1309    }
1310
1311    let rest = parts.collect::<Vec<_>>().join("/");
1312    let subpath = if rest.is_empty() { None } else { Some(rest) };
1313    Some((first.to_string(), subpath))
1314}
1315
1316fn resolve_package_root(
1317    package_root: &Path,
1318    package_json: Option<&PackageJson>,
1319    options: &ResolvedCompilerOptions,
1320    package_type: Option<PackageType>,
1321) -> Option<PathBuf> {
1322    let mut candidates = Vec::new();
1323
1324    if let Some(package_json) = package_json {
1325        candidates = collect_package_entry_candidates(package_json);
1326    }
1327
1328    if !candidates
1329        .iter()
1330        .any(|entry| entry == "index" || entry == "./index")
1331    {
1332        candidates.push("index".to_string());
1333    }
1334
1335    for entry in candidates {
1336        if let Some(resolved) = resolve_package_entry(package_root, &entry, options, package_type) {
1337            return Some(resolved);
1338        }
1339    }
1340
1341    None
1342}
1343
1344fn resolve_package_entry(
1345    package_root: &Path,
1346    entry: &str,
1347    options: &ResolvedCompilerOptions,
1348    package_type: Option<PackageType>,
1349) -> Option<PathBuf> {
1350    let entry = entry.trim();
1351    if entry.is_empty() {
1352        return None;
1353    }
1354    let entry = entry.trim_start_matches("./");
1355    let path = if Path::new(entry).is_absolute() {
1356        PathBuf::from(entry)
1357    } else {
1358        package_root.join(entry)
1359    };
1360
1361    for candidate in expand_module_path_candidates(&path, options, package_type) {
1362        if candidate.is_file() && is_valid_module_file(&candidate) {
1363            return Some(canonicalize_or_owned(&candidate));
1364        }
1365    }
1366
1367    // Check subpath's package.json for types/main fields
1368    if path.is_dir()
1369        && let Some(pj) = read_package_json(&path.join("package.json"))
1370    {
1371        let sub_type = package_type_from_json(Some(&pj));
1372        // Try types/typings field
1373        if let Some(types) = pj.types.or(pj.typings) {
1374            let types_path = path.join(&types);
1375            for candidate in expand_module_path_candidates(&types_path, options, sub_type) {
1376                if candidate.is_file() && is_valid_module_file(&candidate) {
1377                    return Some(canonicalize_or_owned(&candidate));
1378                }
1379            }
1380            if types_path.is_file() {
1381                return Some(canonicalize_or_owned(&types_path));
1382            }
1383        }
1384        // Try main field
1385        if let Some(main) = &pj.main {
1386            let main_path = path.join(main);
1387            for candidate in expand_module_path_candidates(&main_path, options, sub_type) {
1388                if candidate.is_file() && is_valid_module_file(&candidate) {
1389                    return Some(canonicalize_or_owned(&candidate));
1390                }
1391            }
1392        }
1393    }
1394
1395    None
1396}
1397
1398fn resolve_export_entry(
1399    package_root: &Path,
1400    entry: &str,
1401    options: &ResolvedCompilerOptions,
1402    package_type: Option<PackageType>,
1403) -> Option<PathBuf> {
1404    let entry = entry.trim();
1405    if entry.is_empty() {
1406        return None;
1407    }
1408    let entry = entry.trim_start_matches("./");
1409    let path = if Path::new(entry).is_absolute() {
1410        PathBuf::from(entry)
1411    } else {
1412        package_root.join(entry)
1413    };
1414
1415    for candidate in expand_export_path_candidates(&path, options, package_type) {
1416        if candidate.is_file() && is_valid_module_file(&candidate) {
1417            return Some(canonicalize_or_owned(&candidate));
1418        }
1419    }
1420
1421    None
1422}
1423
1424fn package_type_from_json(package_json: Option<&PackageJson>) -> Option<PackageType> {
1425    let package_json = package_json?;
1426
1427    match package_json.package_type.as_deref() {
1428        Some("module") => Some(PackageType::Module),
1429        Some("commonjs") | None => Some(PackageType::CommonJs),
1430        Some(_) => None,
1431    }
1432}
1433
1434fn read_package_json(path: &Path) -> Option<PackageJson> {
1435    let contents = std::fs::read_to_string(path).ok()?;
1436    serde_json::from_str(&contents).ok()
1437}
1438
1439fn collect_package_entry_candidates(package_json: &PackageJson) -> Vec<String> {
1440    let mut seen = FxHashSet::default();
1441    let mut candidates = Vec::new();
1442
1443    for value in [package_json.types.as_ref(), package_json.typings.as_ref()]
1444        .into_iter()
1445        .flatten()
1446    {
1447        if seen.insert(value.clone()) {
1448            candidates.push(value.clone());
1449        }
1450    }
1451
1452    for value in [package_json.module.as_ref(), package_json.main.as_ref()]
1453        .into_iter()
1454        .flatten()
1455    {
1456        if seen.insert(value.clone()) {
1457            candidates.push(value.clone());
1458        }
1459    }
1460
1461    candidates
1462}
1463
1464fn resolve_types_versions(
1465    package_root: &Path,
1466    subpath: &str,
1467    types_versions: &serde_json::Value,
1468    options: &ResolvedCompilerOptions,
1469    package_type: Option<PackageType>,
1470) -> Option<PathBuf> {
1471    let compiler_version = types_versions_compiler_version(options);
1472    let paths = select_types_versions_paths(types_versions, compiler_version)?;
1473    let mut best_pattern: Option<&String> = None;
1474    let mut best_value: Option<&serde_json::Value> = None;
1475    let mut best_wildcard = String::new();
1476    let mut best_specificity = 0usize;
1477    let mut best_len = 0usize;
1478
1479    for (pattern, value) in paths {
1480        let Some(wildcard) = match_types_versions_pattern(pattern, subpath) else {
1481            continue;
1482        };
1483        let specificity = types_versions_specificity(pattern);
1484        let pattern_len = pattern.len();
1485        let is_better = match best_pattern {
1486            None => true,
1487            Some(current) => {
1488                specificity > best_specificity
1489                    || (specificity == best_specificity && pattern_len > best_len)
1490                    || (specificity == best_specificity
1491                        && pattern_len == best_len
1492                        && pattern < current)
1493            }
1494        };
1495
1496        if is_better {
1497            best_specificity = specificity;
1498            best_len = pattern_len;
1499            best_pattern = Some(pattern);
1500            best_value = Some(value);
1501            best_wildcard = wildcard;
1502        }
1503    }
1504
1505    let value = best_value?;
1506
1507    let mut targets = Vec::new();
1508    match value {
1509        serde_json::Value::String(value) => targets.push(value.as_str()),
1510        serde_json::Value::Array(list) => {
1511            for entry in list {
1512                if let Some(value) = entry.as_str() {
1513                    targets.push(value);
1514                }
1515            }
1516        }
1517        _ => {}
1518    }
1519
1520    for target in targets {
1521        let substituted = substitute_path_target(target, &best_wildcard);
1522        if let Some(resolved) =
1523            resolve_package_entry(package_root, &substituted, options, package_type)
1524        {
1525            return Some(resolved);
1526        }
1527    }
1528
1529    None
1530}
1531
1532fn select_types_versions_paths(
1533    types_versions: &serde_json::Value,
1534    compiler_version: SemVer,
1535) -> Option<&serde_json::Map<String, serde_json::Value>> {
1536    select_types_versions_paths_for_version(types_versions, compiler_version)
1537}
1538
1539fn select_types_versions_paths_for_version(
1540    types_versions: &serde_json::Value,
1541    compiler_version: SemVer,
1542) -> Option<&serde_json::Map<String, serde_json::Value>> {
1543    let map = types_versions.as_object()?;
1544    let mut best_score: Option<RangeScore> = None;
1545    let mut best_key: Option<&str> = None;
1546    let mut best_value: Option<&serde_json::Map<String, serde_json::Value>> = None;
1547
1548    for (key, value) in map {
1549        let Some(value_map) = value.as_object() else {
1550            continue;
1551        };
1552        let Some(score) = match_types_versions_range(key, compiler_version) else {
1553            continue;
1554        };
1555        let is_better = match best_score {
1556            None => true,
1557            Some(best) => {
1558                score > best
1559                    || (score == best && best_key.is_none_or(|best_key| key.as_str() < best_key))
1560            }
1561        };
1562
1563        if is_better {
1564            best_score = Some(score);
1565            best_key = Some(key);
1566            best_value = Some(value_map);
1567        }
1568    }
1569
1570    best_value
1571}
1572
1573fn match_types_versions_pattern(pattern: &str, subpath: &str) -> Option<String> {
1574    if !pattern.contains('*') {
1575        return (pattern == subpath).then(String::new);
1576    }
1577
1578    let star = pattern.find('*')?;
1579    let (prefix, suffix) = pattern.split_at(star);
1580    let suffix = &suffix[1..];
1581
1582    if !subpath.starts_with(prefix) || !subpath.ends_with(suffix) {
1583        return None;
1584    }
1585
1586    let start = prefix.len();
1587    let end = subpath.len().saturating_sub(suffix.len());
1588    if end < start {
1589        return None;
1590    }
1591
1592    Some(subpath[start..end].to_string())
1593}
1594
1595fn types_versions_specificity(pattern: &str) -> usize {
1596    if let Some(star) = pattern.find('*') {
1597        star + (pattern.len() - star - 1)
1598    } else {
1599        pattern.len()
1600    }
1601}
1602
1603#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
1604struct RangeScore {
1605    constraints: usize,
1606    min_version: SemVer,
1607    key_len: usize,
1608}
1609
1610fn match_types_versions_range(range: &str, compiler_version: SemVer) -> Option<RangeScore> {
1611    let range = range.trim();
1612    if range.is_empty() || range == "*" {
1613        return Some(RangeScore {
1614            constraints: 0,
1615            min_version: SemVer::ZERO,
1616            key_len: range.len(),
1617        });
1618    }
1619
1620    let mut best: Option<RangeScore> = None;
1621    for segment in range.split("||") {
1622        let segment = segment.trim();
1623        let Some(score) =
1624            match_types_versions_range_segment(segment, compiler_version, range.len())
1625        else {
1626            continue;
1627        };
1628        if best.is_none_or(|current| score > current) {
1629            best = Some(score);
1630        }
1631    }
1632
1633    best
1634}
1635
1636fn match_types_versions_range_segment(
1637    segment: &str,
1638    compiler_version: SemVer,
1639    key_len: usize,
1640) -> Option<RangeScore> {
1641    if segment.is_empty() {
1642        return None;
1643    }
1644    if segment == "*" {
1645        return Some(RangeScore {
1646            constraints: 0,
1647            min_version: SemVer::ZERO,
1648            key_len,
1649        });
1650    }
1651
1652    let mut min_version = SemVer::ZERO;
1653    let mut constraints = 0usize;
1654
1655    for token in segment.split_whitespace() {
1656        if token.is_empty() || token == "*" {
1657            continue;
1658        }
1659        let (op, version) = parse_range_token(token)?;
1660        if !compare_range(compiler_version, op, version) {
1661            return None;
1662        }
1663        constraints += 1;
1664        if matches!(op, RangeOp::Gt | RangeOp::Gte | RangeOp::Eq) && version > min_version {
1665            min_version = version;
1666        }
1667    }
1668
1669    Some(RangeScore {
1670        constraints,
1671        min_version,
1672        key_len,
1673    })
1674}
1675
1676#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1677enum RangeOp {
1678    Gt,
1679    Gte,
1680    Lt,
1681    Lte,
1682    Eq,
1683}
1684
1685fn parse_range_token(token: &str) -> Option<(RangeOp, SemVer)> {
1686    let token = token.trim();
1687    if token.is_empty() {
1688        return None;
1689    }
1690
1691    let (op, rest) = if let Some(rest) = token.strip_prefix(">=") {
1692        (RangeOp::Gte, rest)
1693    } else if let Some(rest) = token.strip_prefix("<=") {
1694        (RangeOp::Lte, rest)
1695    } else if let Some(rest) = token.strip_prefix('>') {
1696        (RangeOp::Gt, rest)
1697    } else if let Some(rest) = token.strip_prefix('<') {
1698        (RangeOp::Lt, rest)
1699    } else if let Some(rest) = token.strip_prefix('=') {
1700        (RangeOp::Eq, rest)
1701    } else {
1702        (RangeOp::Eq, token)
1703    };
1704
1705    parse_semver(rest).map(|version| (op, version))
1706}
1707
1708fn compare_range(version: SemVer, op: RangeOp, bound: SemVer) -> bool {
1709    match op {
1710        RangeOp::Gt => version > bound,
1711        RangeOp::Gte => version >= bound,
1712        RangeOp::Lt => version < bound,
1713        RangeOp::Lte => version <= bound,
1714        RangeOp::Eq => version == bound,
1715    }
1716}
1717
1718fn parse_semver(value: &str) -> Option<SemVer> {
1719    let value = value.trim();
1720    if value.is_empty() {
1721        return None;
1722    }
1723    let core = value.split(['-', '+']).next().unwrap_or(value);
1724    let mut parts = core.split('.');
1725    let major: u32 = parts.next()?.parse().ok()?;
1726    let minor: u32 = parts.next().unwrap_or("0").parse().ok()?;
1727    let patch: u32 = parts.next().unwrap_or("0").parse().ok()?;
1728    Some(SemVer {
1729        major,
1730        minor,
1731        patch,
1732    })
1733}
1734
1735fn resolve_exports_subpath(
1736    exports: &serde_json::Value,
1737    subpath_key: &str,
1738    conditions: &[&str],
1739) -> Option<String> {
1740    match exports {
1741        serde_json::Value::String(value) => (subpath_key == ".").then(|| value.clone()),
1742        serde_json::Value::Array(list) => {
1743            for entry in list {
1744                if let Some(resolved) = resolve_exports_subpath(entry, subpath_key, conditions) {
1745                    return Some(resolved);
1746                }
1747            }
1748            None
1749        }
1750        serde_json::Value::Object(map) => {
1751            let has_subpath_keys = map.keys().any(|key| key.starts_with('.'));
1752            if has_subpath_keys {
1753                if let Some(value) = map.get(subpath_key)
1754                    && let Some(target) = resolve_exports_target(value, conditions)
1755                {
1756                    return Some(target);
1757                }
1758
1759                let mut best_match: Option<(usize, String, &serde_json::Value)> = None;
1760                for (key, value) in map {
1761                    let Some(wildcard) = match_exports_subpath(key, subpath_key) else {
1762                        continue;
1763                    };
1764                    let specificity = key.len();
1765                    let is_better = match &best_match {
1766                        None => true,
1767                        Some((best_len, _, _)) => specificity > *best_len,
1768                    };
1769                    if is_better {
1770                        best_match = Some((specificity, wildcard, value));
1771                    }
1772                }
1773
1774                if let Some((_, wildcard, value)) = best_match
1775                    && let Some(target) = resolve_exports_target(value, conditions)
1776                {
1777                    return Some(apply_exports_subpath(&target, &wildcard));
1778                }
1779
1780                None
1781            } else if subpath_key == "." {
1782                resolve_exports_target(exports, conditions)
1783            } else {
1784                None
1785            }
1786        }
1787        _ => None,
1788    }
1789}
1790
1791fn resolve_exports_target(target: &serde_json::Value, conditions: &[&str]) -> Option<String> {
1792    match target {
1793        serde_json::Value::String(value) => Some(value.clone()),
1794        serde_json::Value::Array(list) => {
1795            for entry in list {
1796                if let Some(resolved) = resolve_exports_target(entry, conditions) {
1797                    return Some(resolved);
1798                }
1799            }
1800            None
1801        }
1802        serde_json::Value::Object(map) => {
1803            for condition in conditions {
1804                if let Some(value) = map.get(*condition)
1805                    && let Some(resolved) = resolve_exports_target(value, conditions)
1806                {
1807                    return Some(resolved);
1808                }
1809            }
1810            None
1811        }
1812        _ => None,
1813    }
1814}
1815
1816fn resolve_imports_subpath(
1817    imports: &serde_json::Value,
1818    subpath_key: &str,
1819    conditions: &[&str],
1820) -> Option<String> {
1821    let serde_json::Value::Object(map) = imports else {
1822        return None;
1823    };
1824
1825    let has_subpath_keys = map.keys().any(|key| key.starts_with('#'));
1826    if !has_subpath_keys {
1827        return None;
1828    }
1829
1830    if let Some(value) = map.get(subpath_key) {
1831        return resolve_exports_target(value, conditions);
1832    }
1833
1834    let mut best_match: Option<(usize, String, &serde_json::Value)> = None;
1835    for (key, value) in map {
1836        let Some(wildcard) = match_imports_subpath(key, subpath_key) else {
1837            continue;
1838        };
1839        let specificity = key.len();
1840        let is_better = match &best_match {
1841            None => true,
1842            Some((best_len, _, _)) => specificity > *best_len,
1843        };
1844        if is_better {
1845            best_match = Some((specificity, wildcard, value));
1846        }
1847    }
1848
1849    if let Some((_, wildcard, value)) = best_match
1850        && let Some(target) = resolve_exports_target(value, conditions)
1851    {
1852        return Some(apply_exports_subpath(&target, &wildcard));
1853    }
1854
1855    None
1856}
1857
1858fn match_exports_subpath(pattern: &str, subpath_key: &str) -> Option<String> {
1859    if !pattern.contains('*') {
1860        return None;
1861    }
1862    let pattern = pattern.strip_prefix("./")?;
1863    let subpath = subpath_key.strip_prefix("./")?;
1864
1865    let star = pattern.find('*')?;
1866    let (prefix, suffix) = pattern.split_at(star);
1867    let suffix = &suffix[1..];
1868
1869    if !subpath.starts_with(prefix) || !subpath.ends_with(suffix) {
1870        return None;
1871    }
1872
1873    let start = prefix.len();
1874    let end = subpath.len().saturating_sub(suffix.len());
1875    if end < start {
1876        return None;
1877    }
1878
1879    Some(subpath[start..end].to_string())
1880}
1881
1882fn match_imports_subpath(pattern: &str, subpath_key: &str) -> Option<String> {
1883    if !pattern.contains('*') {
1884        return None;
1885    }
1886    let pattern = pattern.strip_prefix('#')?;
1887    let subpath = subpath_key.strip_prefix('#')?;
1888
1889    let star = pattern.find('*')?;
1890    let (prefix, suffix) = pattern.split_at(star);
1891    let suffix = &suffix[1..];
1892
1893    if !subpath.starts_with(prefix) || !subpath.ends_with(suffix) {
1894        return None;
1895    }
1896
1897    let start = prefix.len();
1898    let end = subpath.len().saturating_sub(suffix.len());
1899    if end < start {
1900        return None;
1901    }
1902
1903    Some(subpath[start..end].to_string())
1904}
1905
1906fn apply_exports_subpath(target: &str, wildcard: &str) -> String {
1907    if target.contains('*') {
1908        target.replace('*', wildcard)
1909    } else {
1910        target.to_string()
1911    }
1912}
1913
1914pub(crate) fn is_declaration_file(path: &Path) -> bool {
1915    let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
1916        return false;
1917    };
1918
1919    name.ends_with(".d.ts") || name.ends_with(".d.mts") || name.ends_with(".d.cts")
1920}
1921
1922pub(crate) fn canonicalize_or_owned(path: &Path) -> PathBuf {
1923    std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
1924}
1925
1926pub(crate) fn env_flag(name: &str) -> bool {
1927    let Ok(value) = std::env::var(name) else {
1928        return false;
1929    };
1930    let normalized = value.trim().to_ascii_lowercase();
1931    matches!(normalized.as_str(), "1" | "true" | "yes" | "on")
1932}
1933
1934#[cfg(test)]
1935#[path = "driver_resolution_tests.rs"]
1936mod tests;