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(¤t.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
100pub(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 let use_restricted_extensions = matches!(
171 options.effective_module_resolution(),
172 ModuleResolutionKind::Node | ModuleResolutionKind::Classic
173 );
174
175 if use_restricted_extensions {
176 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 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 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
216pub(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 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 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 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 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 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 if let Some(import_decl) = arena.get_import_decl(stmt) {
303 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 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 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 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 collect_dynamic_imports(arena, source_file, &strip_quotes, &mut specifiers);
367
368 specifiers
369}
370
371fn 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 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 let Some(callee) = arena.get(call.expression) else {
392 continue;
393 };
394 if callee.kind != SyntaxKind::ImportKeyword as u16 {
395 continue;
396 }
397 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
417fn 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 let strip_quotes =
427 |s: &str| -> String { s.trim_matches(|c| c == '"' || c == '\'').to_string() };
428
429 if let Some(text) = arena.get_literal_text(idx) {
431 return Some(strip_quotes(text));
432 }
433
434 if node.kind != syntax_kind_ext::CALL_EXPRESSION {
436 return None;
437 }
438
439 let call = arena.get_call_expr(node)?;
440
441 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 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 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 {
711 let mut current = from_dir.to_path_buf();
712 loop {
713 candidates.extend(expand_module_path_candidates(
714 ¤t.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 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 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(¤t.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 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 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
1076const 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 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 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 if !package_name.starts_with("@types/") {
1194 let types_package_name = if let Some(scope_pkg) = package_name.strip_prefix('@') {
1195 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 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 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 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 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 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 let mut printer_options = context.options.printer.clone();
1973 printer_options.type_only_nodes = type_only_nodes;
1974
1975 let mut ctx = tsz::emit_context::EmitContext::with_options(printer_options.clone());
1977 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 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 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 let file_path = PathBuf::from(&file.file_name);
2050 let type_cache = context.type_caches.get(&file_path).cloned();
2051
2052 let binder =
2054 tsz::parallel::create_binder_from_bound_file(file, context.program, file_idx);
2055
2056 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 emitter.set_current_arena(
2071 std::sync::Arc::clone(&file.arena),
2072 file.file_name.clone(),
2073 );
2074 emitter.set_arena_to_path(arena_to_path.clone());
2076 emitter
2077 } else {
2078 let mut emitter = DeclarationEmitter::new(&file.arena);
2079 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 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 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 let used_symbols = analyzer.analyze(file.source_file).clone();
2126 let foreign_symbols = analyzer.get_foreign_symbols().clone();
2127
2128 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;