Skip to main content

tsz_cli/
driver_resolution.rs

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