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