1use std::path::{Path, PathBuf};
7
8use rustc_hash::FxHashMap;
9use serde_json::Value;
10
11use fallow_config::TsconfigOutputResolution;
12use fallow_types::discover::FileId;
13
14use super::path_info::{extract_package_name, is_bare_specifier, is_valid_package_name};
15use super::types::{
16 MISSING_ONLY_OUTPUT_DIRS, OUTPUT_DIRS, PackageManifestInfo, ResolveContext, ResolveResult,
17 SOURCE_EXTS,
18};
19
20fn alias_match_remainder<'a>(specifier: &'a str, prefix: &str) -> Option<&'a str> {
29 let remainder = specifier.strip_prefix(prefix)?;
30 (remainder.is_empty() || prefix.ends_with('/') || remainder.starts_with('/'))
31 .then_some(remainder)
32}
33
34pub(super) fn try_path_alias_fallback(
41 ctx: &ResolveContext<'_>,
42 specifier: &str,
43 style_context: bool,
44) -> Option<ResolveResult> {
45 for (prefix, replacement) in ctx.path_aliases {
46 let Some(remainder) = alias_match_remainder(specifier, prefix) else {
47 continue;
48 };
49
50 let substituted = match (replacement.is_empty(), remainder.is_empty()) {
51 (true, _) => format!("./{remainder}"),
52 (false, true) => format!("./{replacement}"),
53 (false, false) => format!("./{replacement}/{remainder}"),
54 };
55
56 if let Ok(resolved) = super::specifier::resolve_with_extension_policy(
57 ctx,
58 ctx.root,
59 &substituted,
60 style_context,
61 ) {
62 let resolved_path = resolved.path();
63 if let Some(&file_id) = ctx.raw_path_to_id.get(resolved_path) {
64 return Some(ResolveResult::InternalModule(file_id));
65 }
66 if let Some(canonical) = ctx.canonicalize_cache.get(resolved_path) {
67 if let Some(&file_id) = ctx.path_to_id.get(canonical.as_path()) {
68 return Some(ResolveResult::InternalModule(file_id));
69 }
70 if let Some(file_id) = try_source_fallback(&canonical, ctx.path_to_id) {
71 return Some(ResolveResult::InternalModule(file_id));
72 }
73 if let Some(file_id) =
74 try_pnpm_workspace_fallback(&canonical, ctx.path_to_id, ctx.workspace_roots)
75 {
76 return Some(ResolveResult::InternalModule(file_id));
77 }
78 if let Some(pkg_name) = extract_package_name_from_node_modules_path(&canonical) {
79 return Some(ResolveResult::NpmPackage(pkg_name));
80 }
81 return Some(ResolveResult::ExternalFile(canonical));
82 }
83 }
84 }
85 None
86}
87
88pub(super) fn try_scss_partial_fallback(
97 ctx: &ResolveContext<'_>,
98 from_file: &Path,
99 specifier: &str,
100) -> Option<ResolveResult> {
101 if specifier.contains(':') {
102 return None;
103 }
104
105 let spec_path = Path::new(specifier);
106 let filename = spec_path.file_name()?.to_str()?;
107
108 if filename.starts_with('_') {
109 return None;
110 }
111
112 let partial_filename = format!("_{filename}");
113 let partial_specifier = if let Some(parent) = spec_path.parent()
114 && !parent.as_os_str().is_empty()
115 {
116 format!("{}/{partial_filename}", parent.display())
117 } else {
118 partial_filename
119 };
120
121 if let Some(result) = try_resolve_scss(ctx, from_file, &partial_specifier) {
122 return Some(result);
123 }
124
125 let index_partial = format!("{specifier}/_index");
126 if let Some(result) = try_resolve_scss(ctx, from_file, &index_partial) {
127 return Some(result);
128 }
129
130 let index_plain = format!("{specifier}/index");
131 try_resolve_scss(ctx, from_file, &index_plain)
132}
133
134pub(super) fn try_css_extension_fallback(
144 ctx: &ResolveContext<'_>,
145 from_file: &Path,
146 specifier: &str,
147) -> Option<ResolveResult> {
148 if specifier.contains(':') {
149 return None;
150 }
151 let spec_path = Path::new(specifier);
152 let already_css_ext = spec_path
153 .extension()
154 .and_then(|e| e.to_str())
155 .is_some_and(|e| {
156 e.eq_ignore_ascii_case("css")
157 || e.eq_ignore_ascii_case("scss")
158 || e.eq_ignore_ascii_case("sass")
159 });
160 if already_css_ext {
161 return try_resolve_scss(ctx, from_file, specifier);
162 }
163 for ext in ["scss", "sass", "css"] {
164 let candidate = format!("{specifier}.{ext}");
165 if let Some(result) = try_resolve_scss(ctx, from_file, &candidate) {
166 return Some(result);
167 }
168 }
169 None
170}
171
172fn try_resolve_scss(
174 ctx: &ResolveContext<'_>,
175 from_file: &Path,
176 specifier: &str,
177) -> Option<ResolveResult> {
178 super::work::note_oxc_resolve();
179 let resolved = ctx.resolver.resolve_file(from_file, specifier).ok()?;
180 let resolved_path = resolved.path();
181
182 if let Some(&file_id) = ctx.raw_path_to_id.get(resolved_path) {
183 return Some(ResolveResult::InternalModule(file_id));
184 }
185 if let Some(canonical) = ctx.canonicalize_cache.get(resolved_path)
186 && let Some(&file_id) = ctx.path_to_id.get(canonical.as_path())
187 {
188 return Some(ResolveResult::InternalModule(file_id));
189 }
190 None
191}
192
193pub(super) fn try_scss_include_path_fallback(
213 ctx: &ResolveContext<'_>,
214 from_file: &Path,
215 specifier: &str,
216 from_style: bool,
217) -> Option<ResolveResult> {
218 if ctx.scss_include_paths.is_empty() {
219 return None;
220 }
221 let is_scss_importer = from_file
222 .extension()
223 .is_some_and(|e| e == "scss" || e == "sass");
224 if !is_scss_importer && !from_style {
225 return None;
226 }
227 if specifier.contains(':') {
228 return None;
229 }
230 let bare = specifier.strip_prefix("./")?;
231 if bare.starts_with("..") || bare.starts_with('/') {
232 return None;
233 }
234
235 for include_dir in ctx.scss_include_paths {
236 if let Some(file_id) = find_scss_in_dir(include_dir, bare, ctx) {
237 return Some(ResolveResult::InternalModule(file_id));
238 }
239 }
240 None
241}
242
243fn find_scss_in_dir(include_dir: &Path, bare: &str, ctx: &ResolveContext<'_>) -> Option<FileId> {
247 let bare_path = Path::new(bare);
248 let has_scss_ext = matches!(
249 bare_path.extension().and_then(|e| e.to_str()),
250 Some(ext) if ext.eq_ignore_ascii_case("scss") || ext.eq_ignore_ascii_case("sass")
251 );
252
253 let parent = bare_path.parent();
254 let stem_with_ext = bare_path.file_name()?.to_str()?;
255 let stem_without_ext = bare_path.file_stem().and_then(|s| s.to_str())?;
256
257 let build = |rel: &Path| -> std::path::PathBuf { include_dir.join(rel) };
258 let join_with_parent = |name: &str| -> std::path::PathBuf {
259 parent.map_or_else(|| build(Path::new(name)), |p| build(&p.join(name)))
260 };
261
262 let exts: &[&str] = if has_scss_ext {
263 &[""]
264 } else {
265 &["scss", "sass"]
266 };
267
268 for ext in exts {
269 let suffix = if ext.is_empty() {
270 String::new()
271 } else {
272 format!(".{ext}")
273 };
274 let direct = if ext.is_empty() {
275 build(bare_path)
276 } else {
277 join_with_parent(&format!("{stem_with_ext}{suffix}"))
278 };
279 if let Some(fid) = lookup_scss_path(&direct, ctx) {
280 return Some(fid);
281 }
282 let partial_name = if ext.is_empty() {
283 format!("_{stem_with_ext}")
284 } else {
285 format!("_{stem_without_ext}{suffix}")
286 };
287 let partial = join_with_parent(&partial_name);
288 if let Some(fid) = lookup_scss_path(&partial, ctx) {
289 return Some(fid);
290 }
291 if ext.is_empty() {
292 continue;
293 }
294 let idx_partial = build(bare_path).join(format!("_index{suffix}"));
295 if let Some(fid) = lookup_scss_path(&idx_partial, ctx) {
296 return Some(fid);
297 }
298 let idx_plain = build(bare_path).join(format!("index{suffix}"));
299 if let Some(fid) = lookup_scss_path(&idx_plain, ctx) {
300 return Some(fid);
301 }
302 }
303 None
304}
305
306fn lookup_scss_path(candidate: &Path, ctx: &ResolveContext<'_>) -> Option<FileId> {
309 if let Some(&file_id) = ctx.raw_path_to_id.get(candidate) {
310 return Some(file_id);
311 }
312 if let Some(canonical) = ctx.canonicalize_cache.get(candidate) {
313 if let Some(&file_id) = ctx.path_to_id.get(canonical.as_path()) {
314 return Some(file_id);
315 }
316 if let Some(fallback) = ctx.canonical_fallback
317 && let Some(file_id) = fallback.get(&canonical)
318 {
319 return Some(file_id);
320 }
321 }
322 None
323}
324
325pub(super) fn try_scss_node_modules_fallback(
347 _ctx: &ResolveContext<'_>,
348 from_file: &Path,
349 specifier: &str,
350 from_style: bool,
351) -> Option<ResolveResult> {
352 if specifier.contains(':') {
353 return None;
354 }
355 let is_scss_importer = from_file
356 .extension()
357 .is_some_and(|e| e == "scss" || e == "sass");
358 if !is_scss_importer && !from_style {
359 return None;
360 }
361 let bare = specifier.strip_prefix("./")?;
362 if bare.starts_with("..") || bare.starts_with('/') {
363 return None;
364 }
365 if bare.is_empty() {
366 return None;
367 }
368
369 let mut dir = from_file.parent()?;
370 loop {
371 let nm_dir = dir.join("node_modules");
372 if nm_dir.is_dir()
373 && let Some(path) = find_scss_in_node_modules(&nm_dir, bare)
374 && let Some(pkg_name) = extract_package_name_from_node_modules_path(&path)
375 {
376 return Some(ResolveResult::NpmPackage(pkg_name));
377 }
378 let Some(parent) = dir.parent() else {
379 break;
380 };
381 dir = parent;
382 }
383 None
384}
385
386fn find_scss_in_node_modules(nm_dir: &Path, bare: &str) -> Option<PathBuf> {
395 let bare_path = Path::new(bare);
396 let file_name = bare_path.file_name()?.to_str()?;
397 let parent = bare_path.parent();
398 let join_with_parent = |name: &str| -> PathBuf {
399 parent.map_or_else(|| nm_dir.join(name), |p| nm_dir.join(p).join(name))
400 };
401
402 for ext in &["scss", "sass", "css"] {
403 let candidate = join_with_parent(&format!("{file_name}.{ext}"));
404 if candidate.is_file() {
405 return Some(candidate);
406 }
407 }
408 for ext in &["scss", "sass"] {
409 let candidate = join_with_parent(&format!("_{file_name}.{ext}"));
410 if candidate.is_file() {
411 return Some(candidate);
412 }
413 }
414 for ext in &["scss", "sass"] {
415 let idx_partial = nm_dir.join(bare).join(format!("_index.{ext}"));
416 if idx_partial.is_file() {
417 return Some(idx_partial);
418 }
419 let idx_plain = nm_dir.join(bare).join(format!("index.{ext}"));
420 if idx_plain.is_file() {
421 return Some(idx_plain);
422 }
423 }
424 let exact = nm_dir.join(bare);
425 if exact.is_file() {
426 return Some(exact);
427 }
428 None
429}
430
431pub(super) fn try_source_fallback(
443 resolved: &Path,
444 path_to_id: &FxHashMap<&Path, FileId>,
445) -> Option<FileId> {
446 try_source_fallback_with(Path::new(""), resolved, path_to_id, &[OUTPUT_DIRS])
447}
448
449fn try_missing_only_source_fallback(
457 package_root: &Path,
458 resolved: &Path,
459 path_to_id: &FxHashMap<&Path, FileId>,
460) -> Option<FileId> {
461 let relative = resolved.strip_prefix(package_root).ok()?;
462 try_source_fallback_with(
463 package_root,
464 relative,
465 path_to_id,
466 &[OUTPUT_DIRS, MISSING_ONLY_OUTPUT_DIRS],
467 )
468}
469
470fn try_source_fallback_with(
473 base: &Path,
474 relative: &Path,
475 path_to_id: &FxHashMap<&Path, FileId>,
476 dir_lists: &[&[&str]],
477) -> Option<FileId> {
478 let components: Vec<_> = relative.components().collect();
479
480 let is_output_dir = |c: &std::path::Component| -> bool {
481 if let std::path::Component::Normal(s) = c
482 && let Some(name) = s.to_str()
483 {
484 return dir_lists.iter().any(|dirs| dirs.contains(&name));
485 }
486 false
487 };
488
489 let last_output_pos = components.iter().rposition(&is_output_dir)?;
490
491 let mut first_output_pos = last_output_pos;
492 while first_output_pos > 0 && is_output_dir(&components[first_output_pos - 1]) {
493 first_output_pos -= 1;
494 }
495
496 let relative_prefix: PathBuf = components[..first_output_pos].iter().collect();
497 let prefix = if base.as_os_str().is_empty() {
498 relative_prefix
499 } else {
500 base.join(relative_prefix)
501 };
502
503 let suffix: PathBuf = components[last_output_pos + 1..].iter().collect();
504 let file_name = suffix.file_name()?.to_str()?;
505 let stem = output_source_stem(file_name);
506 let source_dir = match suffix.parent() {
507 Some(parent) => prefix.join("src").join(parent),
508 None => prefix.join("src"),
509 };
510
511 for ext in SOURCE_EXTS {
512 let source_candidate = source_dir.join(format!("{stem}.{ext}"));
513 if let Some(&file_id) = path_to_id.get(source_candidate.as_path()) {
514 return Some(file_id);
515 }
516 }
517
518 if is_declaration_file_name(file_name) {
521 return path_to_id
522 .get(source_dir.join(file_name).as_path())
523 .copied();
524 }
525
526 None
527}
528
529const DECLARATION_SUFFIXES: &[&str] = &[".d.ts", ".d.mts", ".d.cts"];
531
532fn declaration_stem(file_name: &str) -> Option<&str> {
533 DECLARATION_SUFFIXES
534 .iter()
535 .find_map(|suffix| file_name.strip_suffix(suffix))
536 .filter(|stem| !stem.is_empty())
537}
538
539pub(super) fn is_declaration_file_name(file_name: &str) -> bool {
540 declaration_stem(file_name).is_some()
541}
542
543pub(super) fn output_source_stem(file_name: &str) -> &str {
549 if let Some(stem) = declaration_stem(file_name) {
550 return stem;
551 }
552 match file_name.rsplit_once('.') {
553 Some((stem, _)) if !stem.is_empty() => stem,
554 _ => file_name,
555 }
556}
557
558pub(super) fn try_package_imports_fallback(
564 ctx: &ResolveContext<'_>,
565 from_file: &Path,
566 specifier: &str,
567) -> Option<ResolveResult> {
568 if !specifier.starts_with('#') {
569 return None;
570 }
571 let manifest = nearest_package_manifest(ctx.package_manifests, from_file)?;
572 let imports = manifest.package_json.imports.as_ref()?;
573 let PackageMapTarget::Targets(targets) =
574 package_map_target(imports, specifier, ctx.condition_names)
575 else {
576 return None;
577 };
578 let source_subpath = package_import_source_subpath(manifest, specifier);
579 resolve_package_import_targets(ctx, manifest, &targets, source_subpath.as_deref()).map(
580 |target| match target {
581 PackageImportTarget::Internal(file_id) => match &manifest.name {
582 Some(package_name) => ResolveResult::InternalPackageModule {
583 file_id,
584 package_name: package_name.clone(),
585 },
586 None => ResolveResult::InternalModule(file_id),
587 },
588 PackageImportTarget::ExternalPackage(package_name) => {
589 ResolveResult::NpmPackage(package_name)
590 }
591 },
592 )
593}
594
595pub(super) fn package_imports_workspace_target(
608 ctx: &ResolveContext<'_>,
609 from_file: &Path,
610 specifier: &str,
611 resolved_file: &Path,
612) -> Option<String> {
613 if !specifier.starts_with('#') {
614 return None;
615 }
616 let manifest = nearest_package_manifest(ctx.package_manifests, from_file)?;
617 let imports = manifest.package_json.imports.as_ref()?;
618 let PackageMapTarget::Targets(targets) =
619 package_map_target(imports, specifier, ctx.condition_names)
620 else {
621 return None;
622 };
623 let owner = nearest_package_manifest(ctx.package_manifests, resolved_file)?
624 .name
625 .as_deref()?;
626 targets
627 .iter()
628 .filter(|target| !target.starts_with('#'))
629 .filter_map(|target| package_import_external_target(target))
630 .find(|package_name| package_name == owner)
631}
632
633pub(super) fn installed_workspace_package_target(
644 ctx: &ResolveContext<'_>,
645 from_file: &Path,
646 specifier: &str,
647 resolved_file: &Path,
648) -> Option<String> {
649 let package_name = super::specifier::package_usage_name_for_external_bare_specifier(specifier)?;
650 let workspace_root = *ctx.workspace_roots.get(package_name.as_str())?;
651 let resolved = ctx
652 .canonicalize_cache
653 .get(resolved_file)
654 .unwrap_or_else(|| resolved_file.to_path_buf());
655 if !resolved.starts_with(workspace_root) {
656 return None;
657 }
658 let installed = from_file.ancestors().skip(1).find_map(|dir| {
659 ctx.canonicalize_cache
660 .get(&dir.join("node_modules").join(&package_name))
661 })?;
662 (installed == workspace_root).then_some(package_name)
663}
664
665pub(super) fn try_relative_package_root_source_fallback(
668 ctx: &ResolveContext<'_>,
669 from_file: &Path,
670 specifier: &str,
671) -> Option<ResolveResult> {
672 if !specifier.starts_with("./") && !specifier.starts_with("../") {
673 return None;
674 }
675
676 let from_dir = from_file.parent()?;
677 let candidate = from_dir.join(specifier);
678 let normalized_candidate = normalize_path_lexically(&candidate);
679 #[cfg(not(miri))]
680 let canonical_candidate = ctx.canonicalize_cache.get(&candidate);
681 #[cfg(miri)]
682 let canonical_candidate: Option<PathBuf> = None;
683
684 ctx.package_manifests.iter().find_map(|manifest| {
685 let matches_manifest = candidate == manifest.root
686 || normalized_candidate == manifest.root
687 || canonical_candidate
688 .as_deref()
689 .is_some_and(|canonical| canonical == manifest.canonical_root);
690 matches_manifest
691 .then(|| try_source_subpath(ctx, manifest, Path::new("")))
692 .flatten()
693 .map(ResolveResult::InternalModule)
694 })
695}
696
697pub(super) fn try_relative_tsconfig_output_fallback(
704 ctx: &ResolveContext<'_>,
705 from_file: &Path,
706 specifier: &str,
707) -> Option<ResolveResult> {
708 if !specifier.starts_with("./") && !specifier.starts_with("../") {
709 return None;
710 }
711 let target = normalize_path_lexically(&from_file.parent()?.join(specifier));
712 let manifest = nearest_package_manifest(ctx.package_manifests, &target)?;
713 let output_map = ctx.tsconfig_cache.output_map(&manifest.root);
714 let TsconfigOutputResolution::Resolved(source) =
715 output_map.resolve_source_for_output_path(&target, SOURCE_EXTS)
716 else {
717 return None;
718 };
719 lookup_internal_file_id(ctx, &source).map(ResolveResult::InternalModule)
720}
721
722pub(super) fn normalize_path_lexically(path: &Path) -> PathBuf {
723 let mut normalized = PathBuf::new();
724 for component in path.components() {
725 match component {
726 std::path::Component::CurDir => {}
727 std::path::Component::ParentDir => {
728 if !normalized.pop() {
729 normalized.push(component.as_os_str());
730 }
731 }
732 std::path::Component::Prefix(_)
733 | std::path::Component::RootDir
734 | std::path::Component::Normal(_) => normalized.push(component.as_os_str()),
735 }
736 }
737 normalized
738}
739
740#[derive(Debug, Clone, PartialEq, Eq)]
741enum PackageMapTarget {
742 NoMatch,
743 Blocked,
744 Targets(Vec<String>),
745}
746
747enum PackageImportTarget {
748 Internal(FileId),
749 ExternalPackage(String),
750}
751
752fn package_map_match_value(
753 value: &Value,
754 condition_names: &[String],
755 capture: Option<&str>,
756) -> PackageMapTarget {
757 resolve_package_map_value(value, condition_names, capture)
758 .filter(|targets| !targets.is_empty())
759 .map_or(PackageMapTarget::Blocked, PackageMapTarget::Targets)
760}
761
762fn package_map_target(
763 map: &Value,
764 specifier_key: &str,
765 condition_names: &[String],
766) -> PackageMapTarget {
767 let Some(obj) = map.as_object() else {
768 if specifier_key == "." {
769 return package_map_match_value(map, condition_names, None);
770 }
771 return PackageMapTarget::NoMatch;
772 };
773
774 let has_subpath_keys = obj
775 .keys()
776 .any(|key| key == "." || key.starts_with("./") || key.starts_with('#'));
777 if !has_subpath_keys {
778 if specifier_key == "." {
779 return package_map_match_value(map, condition_names, None);
780 }
781 return PackageMapTarget::NoMatch;
782 }
783
784 if let Some(value) = obj.get(specifier_key) {
785 return package_map_match_value(value, condition_names, None);
786 }
787
788 let mut patterns: Vec<(&str, &Value, String)> = obj
789 .iter()
790 .filter_map(|(pattern, value)| {
791 package_map_pattern_capture(pattern, specifier_key)
792 .map(|capture| (pattern.as_str(), value, capture))
793 })
794 .collect();
795 patterns.sort_by(|(left, _, _), (right, _, _)| {
796 package_map_pattern_specificity(right).cmp(&package_map_pattern_specificity(left))
797 });
798
799 patterns
800 .first()
801 .map_or(PackageMapTarget::NoMatch, |(_, value, capture)| {
802 package_map_match_value(value, condition_names, Some(capture))
803 })
804}
805
806fn resolve_package_map_value(
807 value: &Value,
808 condition_names: &[String],
809 capture: Option<&str>,
810) -> Option<Vec<String>> {
811 match value {
812 Value::String(target) => Some(vec![match capture {
813 Some(capture) => target.replace('*', capture),
814 None => target.clone(),
815 }]),
816 Value::Object(map) => {
817 for (condition, value) in map {
818 if (condition == "default"
819 || condition_names
820 .iter()
821 .any(|active_condition| active_condition == condition))
822 && let Some(targets) =
823 resolve_package_map_value(value, condition_names, capture)
824 {
825 return Some(targets);
826 }
827 }
828 None
829 }
830 Value::Array(values) => {
831 let targets: Vec<String> = values
832 .iter()
833 .filter_map(|value| resolve_package_map_value(value, condition_names, capture))
834 .flatten()
835 .collect();
836 (!targets.is_empty()).then_some(targets)
837 }
838 Value::Bool(_) | Value::Null | Value::Number(_) => None,
839 }
840}
841
842fn package_map_pattern_capture(pattern: &str, specifier: &str) -> Option<String> {
843 let star = pattern.find('*')?;
844 if pattern[star + 1..].contains('*') {
845 return None;
846 }
847 let (prefix, suffix_with_star) = pattern.split_at(star);
848 let suffix = &suffix_with_star[1..];
849 let captured = specifier.strip_prefix(prefix)?.strip_suffix(suffix)?;
850 Some(captured.to_string())
851}
852
853fn package_map_pattern_specificity(pattern: &str) -> (usize, usize) {
854 let star = pattern.find('*').unwrap_or(pattern.len());
855 (star, pattern.len())
856}
857
858fn package_import_source_subpath(
859 manifest: &PackageManifestInfo,
860 specifier: &str,
861) -> Option<PathBuf> {
862 let stripped = specifier.strip_prefix('#')?;
863 let without_package_name = manifest
864 .name
865 .as_deref()
866 .and_then(|name| stripped.strip_prefix(name))
867 .and_then(|rest| rest.strip_prefix('/'))
868 .unwrap_or(stripped);
869 if without_package_name.is_empty() {
870 None
871 } else {
872 Some(PathBuf::from(without_package_name))
873 }
874}
875
876pub(super) fn nearest_package_manifest<'a>(
877 manifests: &'a [PackageManifestInfo],
878 from_file: &Path,
879) -> Option<&'a PackageManifestInfo> {
880 manifests
881 .iter()
882 .filter(|manifest| {
883 from_file.starts_with(&manifest.root) || from_file.starts_with(&manifest.canonical_root)
884 })
885 .max_by_key(|manifest| manifest.root.components().count())
886}
887
888fn find_package_manifest<'a>(
889 manifests: &'a [PackageManifestInfo],
890 package_name: &str,
891) -> Option<&'a PackageManifestInfo> {
892 manifests
893 .iter()
894 .find(|manifest| manifest.name.as_deref() == Some(package_name))
895}
896
897fn resolve_package_map_target(
898 ctx: &ResolveContext<'_>,
899 manifest: &PackageManifestInfo,
900 target: &str,
901 source_subpath: Option<&Path>,
902) -> Option<FileId> {
903 let target_path = package_map_target_path(manifest, target)?;
904
905 lookup_internal_file_id(ctx, &target_path)
906 .or_else(|| try_source_fallback(&target_path, ctx.raw_path_to_id))
907 .or_else(|| try_source_fallback(&target_path, ctx.path_to_id))
908 .or_else(|| {
909 try_missing_only_source_fallback(&manifest.root, &target_path, ctx.raw_path_to_id)
910 })
911 .or_else(|| try_missing_only_source_fallback(&manifest.root, &target_path, ctx.path_to_id))
912 .or_else(|| source_subpath.and_then(|subpath| try_source_subpath(ctx, manifest, subpath)))
913}
914
915fn package_map_target_path(manifest: &PackageManifestInfo, target: &str) -> Option<PathBuf> {
918 let target = target.strip_prefix("./")?;
919 if target.starts_with("../") || target.starts_with('/') {
920 return None;
921 }
922 Some(manifest.root.join(target))
923}
924
925fn resolve_package_map_targets(
926 ctx: &ResolveContext<'_>,
927 manifest: &PackageManifestInfo,
928 targets: &[String],
929 source_subpath: Option<&Path>,
930) -> Option<FileId> {
931 targets
932 .iter()
933 .find_map(|target| resolve_package_map_target(ctx, manifest, target, source_subpath))
934}
935
936fn resolve_package_import_targets(
937 ctx: &ResolveContext<'_>,
938 manifest: &PackageManifestInfo,
939 targets: &[String],
940 source_subpath: Option<&Path>,
941) -> Option<PackageImportTarget> {
942 targets.iter().find_map(|target| {
943 resolve_package_map_target(ctx, manifest, target, source_subpath)
944 .map(PackageImportTarget::Internal)
945 .or_else(|| {
946 package_import_external_target(target).map(PackageImportTarget::ExternalPackage)
947 })
948 })
949}
950
951fn package_import_external_target(target: &str) -> Option<String> {
952 if is_bare_specifier(target) && is_valid_package_name(target) {
953 Some(extract_package_name(target))
954 } else {
955 None
956 }
957}
958
959fn try_source_subpath(
960 ctx: &ResolveContext<'_>,
961 manifest: &PackageManifestInfo,
962 subpath: &Path,
963) -> Option<FileId> {
964 if subpath.as_os_str().is_empty()
965 && let Some(source) = manifest.package_json.source.as_deref()
966 && let Some(source_path) = safe_relative_package_source_path(source)
967 && let Some(file_id) = lookup_internal_file_id(ctx, &manifest.root.join(source_path))
968 {
969 return Some(file_id);
970 }
971
972 for ext in SOURCE_EXTS {
973 let direct = if subpath.as_os_str().is_empty() {
974 manifest.root.join("src").join(format!("index.{ext}"))
975 } else {
976 manifest.root.join("src").join(subpath).with_extension(ext)
977 };
978 if let Some(file_id) = lookup_internal_file_id(ctx, &direct) {
979 return Some(file_id);
980 }
981
982 if !subpath.as_os_str().is_empty() {
983 let index = manifest
984 .root
985 .join("src")
986 .join(subpath)
987 .join(format!("index.{ext}"));
988 if let Some(file_id) = lookup_internal_file_id(ctx, &index) {
989 return Some(file_id);
990 }
991 }
992
993 if subpath.as_os_str().is_empty() {
994 let root_index = manifest.root.join(format!("index.{ext}"));
995 if let Some(file_id) = lookup_internal_file_id(ctx, &root_index) {
996 return Some(file_id);
997 }
998 }
999 }
1000
1001 None
1002}
1003
1004fn safe_relative_package_source_path(source: &str) -> Option<&Path> {
1005 let source = source.strip_prefix("./").unwrap_or(source);
1006 let path = Path::new(source);
1007 if path.as_os_str().is_empty()
1008 || path.components().any(|component| {
1009 matches!(
1010 component,
1011 std::path::Component::ParentDir
1012 | std::path::Component::RootDir
1013 | std::path::Component::Prefix(_)
1014 )
1015 })
1016 {
1017 None
1018 } else {
1019 Some(path)
1020 }
1021}
1022
1023pub(super) fn lookup_internal_file_id(
1024 ctx: &ResolveContext<'_>,
1025 candidate: &Path,
1026) -> Option<FileId> {
1027 if let Some(&file_id) = ctx.raw_path_to_id.get(candidate) {
1028 return Some(file_id);
1029 }
1030 if let Some(&file_id) = ctx.path_to_id.get(candidate) {
1031 return Some(file_id);
1032 }
1033 #[cfg(not(miri))]
1034 if let Some(canonical) = ctx.canonicalize_cache.get(candidate) {
1035 if let Some(&file_id) = ctx.path_to_id.get(canonical.as_path()) {
1036 return Some(file_id);
1037 }
1038 if let Some(fallback) = ctx.canonical_fallback
1039 && let Some(file_id) = fallback.get(&canonical)
1040 {
1041 return Some(file_id);
1042 }
1043 }
1044 None
1045}
1046
1047pub fn extract_package_name_from_node_modules_path(path: &Path) -> Option<String> {
1053 let components: Vec<&str> = path
1054 .components()
1055 .filter_map(|c| match c {
1056 std::path::Component::Normal(s) => s.to_str(),
1057 _ => None,
1058 })
1059 .collect();
1060
1061 let nm_idx = components.iter().rposition(|&c| c == "node_modules")?;
1062
1063 let after = &components[nm_idx + 1..];
1064 if after.is_empty() {
1065 return None;
1066 }
1067
1068 if after[0].starts_with('@') {
1069 if after.len() >= 2 {
1070 Some(format!("{}/{}", after[0], after[1]))
1071 } else {
1072 Some(after[0].to_string())
1073 }
1074 } else {
1075 Some(after[0].to_string())
1076 }
1077}
1078
1079pub(super) fn try_pnpm_workspace_fallback(
1088 path: &Path,
1089 path_to_id: &FxHashMap<&Path, FileId>,
1090 workspace_roots: &FxHashMap<&str, &Path>,
1091) -> Option<FileId> {
1092 let components: Vec<&str> = path
1093 .components()
1094 .filter_map(|c| match c {
1095 std::path::Component::Normal(s) => s.to_str(),
1096 _ => None,
1097 })
1098 .collect();
1099
1100 let pnpm_idx = components.iter().position(|&c| c == ".pnpm")?;
1101
1102 let after_pnpm = &components[pnpm_idx + 1..];
1103
1104 let inner_nm_idx = after_pnpm.iter().position(|&c| c == "node_modules")?;
1105 let after_inner_nm = &after_pnpm[inner_nm_idx + 1..];
1106
1107 if after_inner_nm.is_empty() {
1108 return None;
1109 }
1110
1111 let (pkg_name, pkg_name_components) = if after_inner_nm[0].starts_with('@') {
1112 if after_inner_nm.len() >= 2 {
1113 (format!("{}/{}", after_inner_nm[0], after_inner_nm[1]), 2)
1114 } else {
1115 return None;
1116 }
1117 } else {
1118 (after_inner_nm[0].to_string(), 1)
1119 };
1120
1121 let ws_root = workspace_roots.get(pkg_name.as_str())?;
1122
1123 let relative_parts = &after_inner_nm[pkg_name_components..];
1124 if relative_parts.is_empty() {
1125 return None;
1126 }
1127
1128 let relative_path: PathBuf = relative_parts.iter().collect();
1129
1130 let direct = ws_root.join(&relative_path);
1131 if let Some(&file_id) = path_to_id.get(direct.as_path()) {
1132 return Some(file_id);
1133 }
1134
1135 try_source_fallback(&direct, path_to_id)
1136}
1137
1138pub(super) fn try_workspace_package_fallback(
1161 ctx: &ResolveContext<'_>,
1162 specifier: &str,
1163 style_context: bool,
1164) -> Option<ResolveResult> {
1165 if !super::path_info::is_bare_specifier(specifier) {
1166 return None;
1167 }
1168 let pkg_name = super::path_info::extract_package_name(specifier);
1169
1170 let subpath = specifier
1171 .strip_prefix(pkg_name.as_str())
1172 .and_then(|s| s.strip_prefix('/'))
1173 .unwrap_or("");
1174 let source_subpath = PathBuf::from(subpath);
1175
1176 match try_manifest_workspace_resolution(ctx, &pkg_name, subpath, &source_subpath) {
1177 ManifestWorkspaceResolution::Resolved(result) => return Some(result),
1178 ManifestWorkspaceResolution::Blocked => return None,
1179 ManifestWorkspaceResolution::Continue => {}
1180 }
1181
1182 let ws_root =
1183 if let Some(manifest) = find_package_manifest(ctx.package_manifests, pkg_name.as_str()) {
1184 manifest.root.as_path()
1185 } else {
1186 *ctx.workspace_roots.get(pkg_name.as_str())?
1187 };
1188
1189 resolve_workspace_self_reference(ctx, ws_root, subpath, pkg_name, style_context)
1190}
1191
1192enum ManifestWorkspaceResolution {
1195 Resolved(ResolveResult),
1197 Blocked,
1199 Continue,
1201}
1202
1203fn try_manifest_workspace_resolution(
1206 ctx: &ResolveContext<'_>,
1207 pkg_name: &str,
1208 subpath: &str,
1209 source_subpath: &Path,
1210) -> ManifestWorkspaceResolution {
1211 let Some(manifest) = find_package_manifest(ctx.package_manifests, pkg_name) else {
1212 return ManifestWorkspaceResolution::Continue;
1213 };
1214
1215 if let Some(exports) = manifest.package_json.exports.as_ref() {
1216 return match package_map_target(exports, &export_key(subpath), ctx.condition_names) {
1217 PackageMapTarget::Targets(targets) => {
1218 match resolve_package_map_targets(ctx, manifest, &targets, Some(source_subpath)) {
1219 Some(file_id) => ManifestWorkspaceResolution::Resolved(
1220 ResolveResult::InternalPackageModule {
1221 file_id,
1222 package_name: pkg_name.to_string(),
1223 },
1224 ),
1225 None => ManifestWorkspaceResolution::Continue,
1226 }
1227 }
1228 PackageMapTarget::NoMatch | PackageMapTarget::Blocked => {
1229 ManifestWorkspaceResolution::Blocked
1230 }
1231 };
1232 }
1233
1234 if let Some(file_id) = try_source_subpath(ctx, manifest, source_subpath) {
1235 return ManifestWorkspaceResolution::Resolved(ResolveResult::InternalPackageModule {
1236 file_id,
1237 package_name: pkg_name.to_string(),
1238 });
1239 }
1240
1241 ManifestWorkspaceResolution::Continue
1242}
1243
1244fn export_key(subpath: &str) -> String {
1247 if subpath.is_empty() {
1248 ".".to_string()
1249 } else {
1250 format!("./{subpath}")
1251 }
1252}
1253
1254pub(super) fn missing_package_export_paths(
1264 ctx: &ResolveContext<'_>,
1265 specifier: &str,
1266) -> Option<Vec<PathBuf>> {
1267 if !is_bare_specifier(specifier) {
1268 return None;
1269 }
1270 let pkg_name = extract_package_name(specifier);
1271 let manifest = find_package_manifest(ctx.package_manifests, &pkg_name)?;
1272 let exports = manifest.package_json.exports.as_ref()?;
1273 let subpath = specifier
1274 .strip_prefix(pkg_name.as_str())
1275 .and_then(|s| s.strip_prefix('/'))
1276 .unwrap_or("");
1277 let PackageMapTarget::Targets(targets) =
1278 package_map_target(exports, &export_key(subpath), ctx.condition_names)
1279 else {
1280 return None;
1281 };
1282 let paths = targets
1283 .iter()
1284 .map(|target| package_map_target_path(manifest, target))
1285 .collect::<Option<Vec<_>>>()?;
1286 (!paths.is_empty() && paths.iter().all(|path| !path.exists())).then_some(paths)
1287}
1288
1289fn resolve_workspace_self_reference(
1293 ctx: &ResolveContext<'_>,
1294 ws_root: &Path,
1295 subpath: &str,
1296 package_name: String,
1297 style_context: bool,
1298) -> Option<ResolveResult> {
1299 let rel_spec = if subpath.is_empty() {
1300 "./".to_string()
1301 } else {
1302 format!("./{subpath}")
1303 };
1304
1305 let root_file = ws_root.join("__fallow_ws_self_resolve__");
1306 let super::specifier::ResolveFileAttempt::Resolved {
1307 resolution: resolved,
1308 ..
1309 } = super::specifier::resolve_file_with_tsconfig_fallback(
1310 ctx,
1311 &root_file,
1312 &rel_spec,
1313 style_context,
1314 )
1315 else {
1316 return None;
1317 };
1318 let resolved_path = resolved.path();
1319
1320 if let Some(&file_id) = ctx.raw_path_to_id.get(resolved_path) {
1321 return Some(ResolveResult::InternalPackageModule {
1322 file_id,
1323 package_name,
1324 });
1325 }
1326 if let Some(canonical) = ctx.canonicalize_cache.get(resolved_path) {
1327 if let Some(&file_id) = ctx.path_to_id.get(canonical.as_path()) {
1328 return Some(ResolveResult::InternalPackageModule {
1329 file_id,
1330 package_name,
1331 });
1332 }
1333 if let Some(fallback) = ctx.canonical_fallback
1334 && let Some(file_id) = fallback.get(&canonical)
1335 {
1336 return Some(ResolveResult::InternalPackageModule {
1337 file_id,
1338 package_name,
1339 });
1340 }
1341 if let Some(file_id) = try_source_fallback(&canonical, ctx.path_to_id) {
1342 return Some(ResolveResult::InternalPackageModule {
1343 file_id,
1344 package_name,
1345 });
1346 }
1347 }
1348 None
1349}
1350
1351pub(super) fn make_glob_from_pattern(
1353 pattern: &fallow_types::extract::DynamicImportPattern,
1354) -> String {
1355 if has_wildcard_or_alternates(&pattern.prefix)
1356 || fallow_config::extglob::has_extglob_group(&pattern.prefix)
1357 {
1358 return pattern.prefix.clone();
1359 }
1360 pattern.suffix.as_ref().map_or_else(
1361 || format!("{}*", pattern.prefix),
1362 |suffix| format!("{}*{}", pattern.prefix, suffix),
1363 )
1364}
1365
1366fn has_wildcard_or_alternates(prefix: &str) -> bool {
1370 let bytes = prefix.as_bytes();
1371 let mut index = 0;
1372 while index < bytes.len() {
1373 match bytes[index] {
1374 b'[' if bytes.get(index + 2) == Some(&b']') => index += 3,
1375 b'*' | b'{' => return true,
1376 _ => index += 1,
1377 }
1378 }
1379 false
1380}
1381
1382#[cfg(test)]
1383mod tests {
1384 use super::*;
1385 use crate::resolve::types::{CanonicalizeCache, TsconfigCache};
1386 use fallow_types::extract::ModuleLoadMechanism;
1387 use rustc_hash::FxHashSet;
1388
1389 fn with_package_map_ctx(
1390 root: PathBuf,
1391 name: Option<&str>,
1392 package_json: fallow_config::PackageJson,
1393 raw_files: &[(PathBuf, FileId)],
1394 f: impl FnOnce(&ResolveContext<'_>, &PackageManifestInfo, &Path),
1395 ) {
1396 let manifest = PackageManifestInfo {
1397 root: root.clone(),
1398 canonical_root: root,
1399 name: name.map(str::to_string),
1400 package_json,
1401 deno_import_map: Vec::new(),
1402 };
1403 let manifests = [manifest];
1404 let mut raw_path_to_id = FxHashMap::default();
1405 for (path, file_id) in raw_files {
1406 raw_path_to_id.insert(path.as_path(), *file_id);
1407 }
1408 let path_to_id: FxHashMap<&Path, FileId> = FxHashMap::default();
1409 let workspace_roots: FxHashMap<&str, &Path> = FxHashMap::default();
1410 let condition_names = conditions();
1411 let resolver = oxc_resolver::Resolver::new(oxc_resolver::ResolveOptions::default());
1412 let tsconfig_warned = std::sync::Mutex::new(FxHashSet::default());
1413 let tsconfig_cache = TsconfigCache::default();
1414 let canonicalize_cache = CanonicalizeCache::default();
1415 let script_resolver = crate::resolve::specifier::create_script_resolver(&resolver);
1416 let ctx = ResolveContext {
1417 resolver: &resolver,
1418 script_resolver: &script_resolver,
1419 style_resolver: &resolver,
1420 extensions: &[],
1421 path_to_id: &path_to_id,
1422 raw_path_to_id: &raw_path_to_id,
1423 workspace_roots: &workspace_roots,
1424 package_manifests: &manifests,
1425 has_deno_import_maps: false,
1426 condition_names: &condition_names,
1427 path_aliases: &[],
1428 scss_include_paths: &[],
1429 static_dir_mappings: &[],
1430 framework_static_dir_mappings: &[],
1431 root: &manifests[0].root,
1432 canonical_fallback: None,
1433 tsconfig_warned: &tsconfig_warned,
1434 tsconfig_cache: &tsconfig_cache,
1435 canonicalize_cache: &canonicalize_cache,
1436 };
1437
1438 f(&ctx, &manifests[0], &manifests[0].root);
1439 }
1440
1441 #[test]
1442 fn alias_match_remainder_exact_key() {
1443 assert_eq!(alias_match_remainder("vscode", "vscode"), Some(""));
1444 assert_eq!(alias_match_remainder("@scope/sdk", "@scope/sdk"), Some(""));
1445 }
1446
1447 #[test]
1448 fn alias_match_remainder_slash_continuation() {
1449 assert_eq!(
1450 alias_match_remainder("@scope/sdk/sub", "@scope/sdk"),
1451 Some("/sub")
1452 );
1453 assert_eq!(alias_match_remainder("@/foo", "@/"), Some("foo"));
1454 assert_eq!(
1455 alias_match_remainder("~/components/x", "~/"),
1456 Some("components/x")
1457 );
1458 assert_eq!(alias_match_remainder("$lib/util", "$lib/"), Some("util"));
1459 }
1460
1461 #[test]
1462 fn alias_match_remainder_rejects_prefix_collision() {
1463 assert_eq!(
1464 alias_match_remainder("@scope/sdk-extra", "@scope/sdk"),
1465 None
1466 );
1467 assert_eq!(
1468 alias_match_remainder("vscode-languageserver", "vscode"),
1469 None
1470 );
1471 assert_eq!(alias_match_remainder("#shared-utils", "#shared"), None);
1472 }
1473
1474 #[test]
1475 fn alias_match_remainder_non_match() {
1476 assert_eq!(alias_match_remainder("react", "vscode"), None);
1477 }
1478
1479 #[test]
1480 fn test_extract_package_name_from_node_modules_path_regular() {
1481 let path = PathBuf::from("/project/node_modules/react/index.js");
1482 assert_eq!(
1483 extract_package_name_from_node_modules_path(&path),
1484 Some("react".to_string())
1485 );
1486 }
1487
1488 #[test]
1489 fn test_extract_package_name_from_node_modules_path_scoped() {
1490 let path = PathBuf::from("/project/node_modules/@babel/core/lib/index.js");
1491 assert_eq!(
1492 extract_package_name_from_node_modules_path(&path),
1493 Some("@babel/core".to_string())
1494 );
1495 }
1496
1497 #[test]
1498 fn test_extract_package_name_from_node_modules_path_nested() {
1499 let path = PathBuf::from("/project/node_modules/pkg-a/node_modules/pkg-b/dist/index.js");
1500 assert_eq!(
1501 extract_package_name_from_node_modules_path(&path),
1502 Some("pkg-b".to_string())
1503 );
1504 }
1505
1506 #[test]
1507 fn test_extract_package_name_from_node_modules_path_deep_subpath() {
1508 let path = PathBuf::from("/project/node_modules/react-dom/cjs/react-dom.production.min.js");
1509 assert_eq!(
1510 extract_package_name_from_node_modules_path(&path),
1511 Some("react-dom".to_string())
1512 );
1513 }
1514
1515 #[test]
1516 fn test_extract_package_name_from_node_modules_path_no_node_modules() {
1517 let path = PathBuf::from("/project/src/components/Button.tsx");
1518 assert_eq!(extract_package_name_from_node_modules_path(&path), None);
1519 }
1520
1521 #[test]
1522 fn test_extract_package_name_from_node_modules_path_just_node_modules() {
1523 let path = PathBuf::from("/project/node_modules");
1524 assert_eq!(extract_package_name_from_node_modules_path(&path), None);
1525 }
1526
1527 #[test]
1528 fn test_extract_package_name_from_node_modules_path_scoped_only_scope() {
1529 let path = PathBuf::from("/project/node_modules/@scope");
1530 assert_eq!(
1531 extract_package_name_from_node_modules_path(&path),
1532 Some("@scope".to_string())
1533 );
1534 }
1535
1536 #[test]
1537 fn test_resolve_specifier_node_modules_returns_npm_package() {
1538 let path =
1539 PathBuf::from("/project/node_modules/styled-components/dist/styled-components.esm.js");
1540 assert_eq!(
1541 extract_package_name_from_node_modules_path(&path),
1542 Some("styled-components".to_string())
1543 );
1544
1545 let path = PathBuf::from("/project/node_modules/next/dist/server/next.js");
1546 assert_eq!(
1547 extract_package_name_from_node_modules_path(&path),
1548 Some("next".to_string())
1549 );
1550 }
1551
1552 #[test]
1553 fn test_try_source_fallback_dist_to_src() {
1554 let src_path = PathBuf::from("/project/packages/ui/src/utils.ts");
1555 let mut path_to_id = FxHashMap::default();
1556 path_to_id.insert(src_path.as_path(), FileId(0));
1557
1558 let dist_path = PathBuf::from("/project/packages/ui/dist/utils.js");
1559 assert_eq!(
1560 try_source_fallback(&dist_path, &path_to_id),
1561 Some(FileId(0)),
1562 "dist/utils.js should fall back to src/utils.ts"
1563 );
1564 }
1565
1566 #[test]
1567 fn test_try_source_fallback_build_to_src() {
1568 let src_path = PathBuf::from("/project/packages/core/src/index.tsx");
1569 let mut path_to_id = FxHashMap::default();
1570 path_to_id.insert(src_path.as_path(), FileId(1));
1571
1572 let build_path = PathBuf::from("/project/packages/core/build/index.js");
1573 assert_eq!(
1574 try_source_fallback(&build_path, &path_to_id),
1575 Some(FileId(1)),
1576 "build/index.js should fall back to src/index.tsx"
1577 );
1578 }
1579
1580 #[test]
1581 fn test_try_source_fallback_no_match() {
1582 let path_to_id: FxHashMap<&Path, FileId> = FxHashMap::default();
1583
1584 let dist_path = PathBuf::from("/project/packages/ui/dist/utils.js");
1585 assert_eq!(
1586 try_source_fallback(&dist_path, &path_to_id),
1587 None,
1588 "should return None when no source file exists"
1589 );
1590 }
1591
1592 #[test]
1593 fn test_try_source_fallback_non_output_dir() {
1594 let src_path = PathBuf::from("/project/packages/ui/src/utils.ts");
1595 let mut path_to_id = FxHashMap::default();
1596 path_to_id.insert(src_path.as_path(), FileId(0));
1597
1598 let normal_path = PathBuf::from("/project/packages/ui/scripts/utils.js");
1599 assert_eq!(
1600 try_source_fallback(&normal_path, &path_to_id),
1601 None,
1602 "non-output directory path should not trigger fallback"
1603 );
1604 }
1605
1606 #[test]
1607 fn test_try_source_fallback_nested_path() {
1608 let src_path = PathBuf::from("/project/packages/ui/src/components/Button.ts");
1609 let mut path_to_id = FxHashMap::default();
1610 path_to_id.insert(src_path.as_path(), FileId(2));
1611
1612 let dist_path = PathBuf::from("/project/packages/ui/dist/components/Button.js");
1613 assert_eq!(
1614 try_source_fallback(&dist_path, &path_to_id),
1615 Some(FileId(2)),
1616 "nested dist path should fall back to nested src path"
1617 );
1618 }
1619
1620 #[test]
1621 fn test_try_source_fallback_nested_dist_esm() {
1622 let src_path = PathBuf::from("/project/packages/ui/src/utils.ts");
1623 let mut path_to_id = FxHashMap::default();
1624 path_to_id.insert(src_path.as_path(), FileId(0));
1625
1626 let dist_path = PathBuf::from("/project/packages/ui/dist/esm/utils.mjs");
1627 assert_eq!(
1628 try_source_fallback(&dist_path, &path_to_id),
1629 Some(FileId(0)),
1630 "dist/esm/utils.mjs should fall back to src/utils.ts"
1631 );
1632 }
1633
1634 #[test]
1635 fn test_try_source_fallback_nested_build_cjs() {
1636 let src_path = PathBuf::from("/project/packages/core/src/index.ts");
1637 let mut path_to_id = FxHashMap::default();
1638 path_to_id.insert(src_path.as_path(), FileId(1));
1639
1640 let build_path = PathBuf::from("/project/packages/core/build/cjs/index.cjs");
1641 assert_eq!(
1642 try_source_fallback(&build_path, &path_to_id),
1643 Some(FileId(1)),
1644 "build/cjs/index.cjs should fall back to src/index.ts"
1645 );
1646 }
1647
1648 #[test]
1649 fn test_try_source_fallback_nested_dist_esm_deep_path() {
1650 let src_path = PathBuf::from("/project/packages/ui/src/components/Button.ts");
1651 let mut path_to_id = FxHashMap::default();
1652 path_to_id.insert(src_path.as_path(), FileId(2));
1653
1654 let dist_path = PathBuf::from("/project/packages/ui/dist/esm/components/Button.mjs");
1655 assert_eq!(
1656 try_source_fallback(&dist_path, &path_to_id),
1657 Some(FileId(2)),
1658 "dist/esm/components/Button.mjs should fall back to src/components/Button.ts"
1659 );
1660 }
1661
1662 #[test]
1663 fn test_try_source_fallback_triple_nested_output_dirs() {
1664 let src_path = PathBuf::from("/project/packages/ui/src/utils.ts");
1665 let mut path_to_id = FxHashMap::default();
1666 path_to_id.insert(src_path.as_path(), FileId(0));
1667
1668 let dist_path = PathBuf::from("/project/packages/ui/out/dist/esm/utils.mjs");
1669 assert_eq!(
1670 try_source_fallback(&dist_path, &path_to_id),
1671 Some(FileId(0)),
1672 "out/dist/esm/utils.mjs should fall back to src/utils.ts"
1673 );
1674 }
1675
1676 #[test]
1677 fn test_try_source_fallback_parent_dir_named_build() {
1678 let src_path = PathBuf::from("/home/user/build/my-project/src/utils.ts");
1679 let mut path_to_id = FxHashMap::default();
1680 path_to_id.insert(src_path.as_path(), FileId(0));
1681
1682 let dist_path = PathBuf::from("/home/user/build/my-project/dist/utils.js");
1683 assert_eq!(
1684 try_source_fallback(&dist_path, &path_to_id),
1685 Some(FileId(0)),
1686 "should resolve dist/ within project, not match parent 'build' dir"
1687 );
1688 }
1689
1690 #[test]
1691 fn package_map_exact_entry_beats_pattern_entry() {
1692 let map = serde_json::json!({
1693 "#nitro/runtime/task": "./dist/special/task.mjs",
1694 "#nitro/runtime/*": "./dist/runtime/internal/*.mjs"
1695 });
1696 assert_eq!(
1697 package_map_target(&map, "#nitro/runtime/task", &conditions()),
1698 PackageMapTarget::Targets(vec!["./dist/special/task.mjs".to_string()])
1699 );
1700 }
1701
1702 #[test]
1703 fn package_map_wildcard_substitutes_capture() {
1704 let map = serde_json::json!({
1705 "#nitro/runtime/*": "./dist/runtime/internal/*.mjs"
1706 });
1707 assert_eq!(
1708 package_map_target(&map, "#nitro/runtime/task", &conditions()),
1709 PackageMapTarget::Targets(vec!["./dist/runtime/internal/task.mjs".to_string()])
1710 );
1711 }
1712
1713 #[test]
1714 fn package_map_exact_entry_with_no_target_blocks_pattern_entry() {
1715 let map = serde_json::json!({
1716 "#nitro/runtime/task": null,
1717 "#nitro/runtime/*": "./dist/runtime/internal/*.mjs"
1718 });
1719 assert_eq!(
1720 package_map_target(&map, "#nitro/runtime/task", &conditions()),
1721 PackageMapTarget::Blocked
1722 );
1723 }
1724
1725 #[test]
1726 fn package_map_best_pattern_with_no_target_blocks_broader_pattern() {
1727 let map = serde_json::json!({
1728 "#nitro/runtime/internal/*": null,
1729 "#nitro/runtime/*": "./dist/runtime/*.mjs"
1730 });
1731 assert_eq!(
1732 package_map_target(&map, "#nitro/runtime/internal/task", &conditions()),
1733 PackageMapTarget::Blocked
1734 );
1735 }
1736
1737 #[test]
1738 fn package_map_unmatched_subpath_is_not_a_target() {
1739 let map = serde_json::json!({
1740 "./query": "./dist/query/index.js"
1741 });
1742 assert_eq!(
1743 package_map_target(&map, "./private", &conditions()),
1744 PackageMapTarget::NoMatch
1745 );
1746 }
1747
1748 #[test]
1749 fn package_map_nested_conditions_follow_manifest_order() {
1750 let map = serde_json::json!({
1751 "./query/react": {
1752 "types": "./dist/query/react/index.d.ts",
1753 "import": {
1754 "development": "./src/query/react/index.ts",
1755 "default": "./dist/query/react/index.js"
1756 },
1757 "default": "./dist/query/react/index.cjs"
1758 }
1759 });
1760 assert_eq!(
1761 package_map_target(&map, "./query/react", &conditions()),
1762 PackageMapTarget::Targets(vec!["./dist/query/react/index.d.ts".to_string()])
1763 );
1764 }
1765
1766 #[test]
1767 fn package_map_import_before_types_selects_runtime_branch() {
1768 let map = serde_json::json!({
1769 ".": {
1770 "import": "./dist/index.js",
1771 "types": "./dist/index.d.ts"
1772 }
1773 });
1774 assert_eq!(
1775 package_map_target(&map, ".", &conditions()),
1776 PackageMapTarget::Targets(vec!["./dist/index.js".to_string()])
1777 );
1778 }
1779
1780 #[test]
1781 fn package_map_condition_order_follows_manifest_order() {
1782 let map = serde_json::json!({
1783 ".": {
1784 "node": "./dist/node.js",
1785 "import": "./dist/index.js"
1786 }
1787 });
1788 assert_eq!(
1789 package_map_target(&map, ".", &conditions()),
1790 PackageMapTarget::Targets(vec!["./dist/node.js".to_string()])
1791 );
1792 }
1793
1794 #[test]
1795 fn package_map_arrays_preserve_fallback_order() {
1796 let map = serde_json::json!({
1797 "#array": ["./dist/missing.js", "./src/array.ts"],
1798 "#null": null,
1799 "#false": false
1800 });
1801 assert_eq!(
1802 package_map_target(&map, "#array", &conditions()),
1803 PackageMapTarget::Targets(vec![
1804 "./dist/missing.js".to_string(),
1805 "./src/array.ts".to_string()
1806 ])
1807 );
1808 assert_eq!(
1809 package_map_target(&map, "#null", &conditions()),
1810 PackageMapTarget::Blocked
1811 );
1812 assert_eq!(
1813 package_map_target(&map, "#false", &conditions()),
1814 PackageMapTarget::Blocked
1815 );
1816 }
1817
1818 #[test]
1819 fn package_map_non_relative_target_does_not_trigger_source_fallback() {
1820 with_package_map_ctx(
1821 PathBuf::from("/project"),
1822 Some("pkg"),
1823 fallow_config::PackageJson::default(),
1824 &[],
1825 |ctx, manifest, _| {
1826 assert!(resolve_package_map_target(ctx, manifest, "lodash", None).is_none());
1827 assert!(
1828 resolve_package_map_target(ctx, manifest, "../dist/index.js", None).is_none()
1829 );
1830 },
1831 );
1832 }
1833
1834 #[test]
1835 fn package_map_targets_use_first_reachable_target() {
1836 let root = PathBuf::from("/project");
1837 let src_path = root.join("src/feature.ts");
1838 let targets = vec![
1839 "./dist/missing.js".to_string(),
1840 "./src/feature.ts".to_string(),
1841 ];
1842
1843 with_package_map_ctx(
1844 root,
1845 Some("pkg"),
1846 fallow_config::PackageJson::default(),
1847 &[(src_path, FileId(9))],
1848 |ctx, manifest, _| {
1849 assert_eq!(
1850 resolve_package_map_targets(ctx, manifest, &targets, None),
1851 Some(FileId(9))
1852 );
1853 },
1854 );
1855 }
1856
1857 #[test]
1858 fn package_map_lib_target_maps_to_source_when_absent() {
1859 let root = PathBuf::from("/project/packages/uses-lib");
1860 let feature = root.join("src/features/feature.ts");
1861 let index = root.join("src/index.ts");
1862
1863 with_package_map_ctx(
1864 root,
1865 Some("@repro/uses-lib"),
1866 fallow_config::PackageJson::default(),
1867 &[(feature, FileId(3)), (index, FileId(4))],
1868 |ctx, manifest, _| {
1869 assert_eq!(
1870 resolve_package_map_target(ctx, manifest, "./lib/features/feature.mjs", None),
1871 Some(FileId(3))
1872 );
1873 assert_eq!(
1874 resolve_package_map_target(ctx, manifest, "./lib/index.d.ts", None),
1875 Some(FileId(4))
1876 );
1877 },
1878 );
1879 }
1880
1881 #[test]
1882 fn package_map_tracked_lib_target_wins_over_source() {
1883 let root = PathBuf::from("/project");
1884 let lib_path = root.join("lib/index.js");
1885 let src_path = root.join("src/index.ts");
1886
1887 with_package_map_ctx(
1888 root,
1889 Some("pkg"),
1890 fallow_config::PackageJson::default(),
1891 &[(lib_path, FileId(1)), (src_path, FileId(2))],
1892 |ctx, manifest, _| {
1893 assert_eq!(
1894 resolve_package_map_target(ctx, manifest, "./lib/index.js", None),
1895 Some(FileId(1))
1896 );
1897 },
1898 );
1899 }
1900
1901 #[test]
1902 fn package_map_lib_mapping_ignores_lib_above_package_root() {
1903 let root = PathBuf::from("/project/packages/lib");
1904 let outer_src = PathBuf::from("/project/packages/src/index.ts");
1905
1906 with_package_map_ctx(
1907 root,
1908 Some("pkg"),
1909 fallow_config::PackageJson::default(),
1910 &[(outer_src, FileId(5))],
1911 |ctx, manifest, _| {
1912 assert_eq!(
1913 resolve_package_map_target(ctx, manifest, "./index.js", None),
1914 None
1915 );
1916 },
1917 );
1918 }
1919
1920 #[test]
1921 fn generic_source_fallback_does_not_map_lib() {
1922 let src_path = PathBuf::from("/project/src/x.ts");
1923 let mut path_to_id = FxHashMap::default();
1924 path_to_id.insert(src_path.as_path(), FileId(0));
1925
1926 assert_eq!(
1927 try_source_fallback(Path::new("/project/lib/x.js"), &path_to_id),
1928 None
1929 );
1930 }
1931
1932 #[test]
1933 fn package_map_dist_lib_target_prefers_output_dirs_mapping() {
1934 let root = PathBuf::from("/project");
1935 let nested = root.join("src/lib/x.ts");
1936 let flat = root.join("src/x.ts");
1937
1938 with_package_map_ctx(
1939 root,
1940 Some("pkg"),
1941 fallow_config::PackageJson::default(),
1942 &[(nested, FileId(6)), (flat, FileId(7))],
1943 |ctx, manifest, _| {
1944 assert_eq!(
1945 resolve_package_map_target(ctx, manifest, "./dist/lib/x.js", None),
1946 Some(FileId(6))
1947 );
1948 },
1949 );
1950 }
1951
1952 #[test]
1953 fn package_imports_fallback_supports_external_package_targets() {
1954 let root = PathBuf::from("/project");
1955 with_package_map_ctx(
1956 root,
1957 Some("pkg"),
1958 fallow_config::PackageJson {
1959 imports: Some(serde_json::json!({
1960 "#pad": "left-pad",
1961 "#scoped": "@scope/pkg/subpath"
1962 })),
1963 ..Default::default()
1964 },
1965 &[],
1966 |ctx, _, root| {
1967 let pad = try_package_imports_fallback(ctx, &root.join("src/index.ts"), "#pad");
1968 assert!(matches!(pad, Some(ResolveResult::NpmPackage(pkg)) if pkg == "left-pad"));
1969
1970 let scoped =
1971 try_package_imports_fallback(ctx, &root.join("src/index.ts"), "#scoped");
1972 assert!(
1973 matches!(scoped, Some(ResolveResult::NpmPackage(pkg)) if pkg == "@scope/pkg")
1974 );
1975 },
1976 );
1977 }
1978
1979 #[test]
1980 fn package_imports_workspace_target_names_only_known_packages() {
1981 let root = PathBuf::from("/project");
1982 with_package_map_ctx(
1983 root,
1984 Some("pkg"),
1985 fallow_config::PackageJson {
1986 imports: Some(serde_json::json!({
1987 "#self/*": "pkg/*",
1988 "#pad": "left-pad",
1989 "#local/*": "./src/*.ts"
1990 })),
1991 ..Default::default()
1992 },
1993 &[],
1994 |ctx, _, root| {
1995 let from = root.join("src/index.ts");
1996 let feature = root.join("src/feature.ts");
1997 assert_eq!(
1998 package_imports_workspace_target(ctx, &from, "#self/feature", &feature),
1999 Some("pkg".to_string())
2000 );
2001 assert_eq!(
2002 package_imports_workspace_target(
2003 ctx,
2004 &from,
2005 "#self/feature",
2006 Path::new("/elsewhere/feature.ts")
2007 ),
2008 None,
2009 "a file outside the named package must not credit it"
2010 );
2011 assert_eq!(
2012 package_imports_workspace_target(
2013 ctx,
2014 &from,
2015 "#pad",
2016 &root.join("node_modules/left-pad/index.js")
2017 ),
2018 None
2019 );
2020 assert_eq!(
2021 package_imports_workspace_target(ctx, &from, "#local/feature", &feature),
2022 None
2023 );
2024 assert_eq!(
2025 package_imports_workspace_target(ctx, &from, "pkg/feature", &feature),
2026 None
2027 );
2028 },
2029 );
2030 }
2031
2032 #[test]
2033 fn package_imports_fallback_supports_unnamed_packages() {
2034 let root = PathBuf::from("/project");
2035 let src_path = root.join("src/runtime/task.ts");
2036 with_package_map_ctx(
2037 root,
2038 None,
2039 fallow_config::PackageJson {
2040 imports: Some(serde_json::json!({
2041 "#runtime/*": "./dist/runtime/*.mjs"
2042 })),
2043 ..Default::default()
2044 },
2045 &[(src_path, FileId(7))],
2046 |ctx, _, root| {
2047 let result =
2048 try_package_imports_fallback(ctx, &root.join("src/index.ts"), "#runtime/task");
2049 assert!(matches!(
2050 result,
2051 Some(ResolveResult::InternalModule(FileId(7)))
2052 ));
2053 },
2054 );
2055 }
2056
2057 #[test]
2058 #[cfg_attr(miri, ignore)]
2059 fn relative_package_root_source_fallback_uses_package_source_entry() {
2060 let root = PathBuf::from("/project");
2061 let source_path = root.join("custom/entry.js");
2062 with_package_map_ctx(
2063 root,
2064 Some("pkg"),
2065 fallow_config::PackageJson {
2066 source: Some("custom/entry.js".to_string()),
2067 ..Default::default()
2068 },
2069 &[(source_path, FileId(11))],
2070 |ctx, _, root| {
2071 let result = try_relative_package_root_source_fallback(
2072 ctx,
2073 &root.join("test/shared/exports.test.js"),
2074 "../../",
2075 );
2076 assert!(matches!(
2077 result,
2078 Some(ResolveResult::InternalModule(FileId(11)))
2079 ));
2080 },
2081 );
2082 }
2083
2084 #[test]
2085 fn package_source_path_accepts_relative_source_entries() {
2086 assert_eq!(
2087 safe_relative_package_source_path("src/index.js"),
2088 Some(Path::new("src/index.js"))
2089 );
2090 assert_eq!(
2091 safe_relative_package_source_path("./custom/entry.ts"),
2092 Some(Path::new("custom/entry.ts"))
2093 );
2094 }
2095
2096 #[test]
2097 fn package_source_path_rejects_unsafe_entries() {
2098 assert_eq!(safe_relative_package_source_path(""), None);
2099 assert_eq!(safe_relative_package_source_path("./"), None);
2100 assert_eq!(safe_relative_package_source_path("../src/index.js"), None);
2101 assert_eq!(safe_relative_package_source_path("src/../index.js"), None);
2102 assert_eq!(safe_relative_package_source_path("/src/index.js"), None);
2103
2104 #[cfg(windows)]
2105 assert_eq!(safe_relative_package_source_path("C:\\src\\index.js"), None);
2106 }
2107
2108 #[test]
2109 fn test_pnpm_store_path_extract_package_name() {
2110 let path =
2111 PathBuf::from("/project/node_modules/.pnpm/react@18.2.0/node_modules/react/index.js");
2112 assert_eq!(
2113 extract_package_name_from_node_modules_path(&path),
2114 Some("react".to_string())
2115 );
2116 }
2117
2118 #[test]
2119 fn test_pnpm_store_path_scoped_package() {
2120 let path = PathBuf::from(
2121 "/project/node_modules/.pnpm/@babel+core@7.24.0/node_modules/@babel/core/lib/index.js",
2122 );
2123 assert_eq!(
2124 extract_package_name_from_node_modules_path(&path),
2125 Some("@babel/core".to_string())
2126 );
2127 }
2128
2129 fn conditions() -> Vec<String> {
2130 vec![
2131 "development".to_string(),
2132 "import".to_string(),
2133 "require".to_string(),
2134 "default".to_string(),
2135 "types".to_string(),
2136 "node".to_string(),
2137 ]
2138 }
2139
2140 #[test]
2141 fn test_pnpm_store_path_with_peer_deps() {
2142 let path = PathBuf::from(
2143 "/project/node_modules/.pnpm/webpack@5.0.0_esbuild@0.19.0/node_modules/webpack/lib/index.js",
2144 );
2145 assert_eq!(
2146 extract_package_name_from_node_modules_path(&path),
2147 Some("webpack".to_string())
2148 );
2149 }
2150
2151 #[test]
2152 fn test_try_pnpm_workspace_fallback_dist_to_src() {
2153 let src_path = PathBuf::from("/project/packages/ui/src/utils.ts");
2154 let mut path_to_id = FxHashMap::default();
2155 path_to_id.insert(src_path.as_path(), FileId(0));
2156
2157 let mut workspace_roots = FxHashMap::default();
2158 let ws_root = PathBuf::from("/project/packages/ui");
2159 workspace_roots.insert("@myorg/ui", ws_root.as_path());
2160
2161 let pnpm_path = PathBuf::from(
2162 "/project/node_modules/.pnpm/@myorg+ui@1.0.0/node_modules/@myorg/ui/dist/utils.js",
2163 );
2164 assert_eq!(
2165 try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
2166 Some(FileId(0)),
2167 ".pnpm workspace path should fall back to src/utils.ts"
2168 );
2169 }
2170
2171 #[test]
2172 fn test_try_pnpm_workspace_fallback_direct_source() {
2173 let src_path = PathBuf::from("/project/packages/core/src/index.ts");
2174 let mut path_to_id = FxHashMap::default();
2175 path_to_id.insert(src_path.as_path(), FileId(1));
2176
2177 let mut workspace_roots = FxHashMap::default();
2178 let ws_root = PathBuf::from("/project/packages/core");
2179 workspace_roots.insert("@myorg/core", ws_root.as_path());
2180
2181 let pnpm_path = PathBuf::from(
2182 "/project/node_modules/.pnpm/@myorg+core@workspace/node_modules/@myorg/core/src/index.ts",
2183 );
2184 assert_eq!(
2185 try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
2186 Some(FileId(1)),
2187 ".pnpm workspace path with src/ should resolve directly"
2188 );
2189 }
2190
2191 #[test]
2192 fn test_try_pnpm_workspace_fallback_non_workspace_package() {
2193 let path_to_id: FxHashMap<&Path, FileId> = FxHashMap::default();
2194
2195 let mut workspace_roots = FxHashMap::default();
2196 let ws_root = PathBuf::from("/project/packages/ui");
2197 workspace_roots.insert("@myorg/ui", ws_root.as_path());
2198
2199 let pnpm_path =
2200 PathBuf::from("/project/node_modules/.pnpm/react@18.2.0/node_modules/react/index.js");
2201 assert_eq!(
2202 try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
2203 None,
2204 "non-workspace package in .pnpm should return None"
2205 );
2206 }
2207
2208 #[test]
2209 fn test_try_pnpm_workspace_fallback_unscoped_package() {
2210 let src_path = PathBuf::from("/project/packages/utils/src/index.ts");
2211 let mut path_to_id = FxHashMap::default();
2212 path_to_id.insert(src_path.as_path(), FileId(2));
2213
2214 let mut workspace_roots = FxHashMap::default();
2215 let ws_root = PathBuf::from("/project/packages/utils");
2216 workspace_roots.insert("my-utils", ws_root.as_path());
2217
2218 let pnpm_path = PathBuf::from(
2219 "/project/node_modules/.pnpm/my-utils@1.0.0/node_modules/my-utils/dist/index.js",
2220 );
2221 assert_eq!(
2222 try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
2223 Some(FileId(2)),
2224 "unscoped workspace package in .pnpm should resolve"
2225 );
2226 }
2227
2228 #[test]
2229 fn test_try_pnpm_workspace_fallback_nested_path() {
2230 let src_path = PathBuf::from("/project/packages/ui/src/components/Button.ts");
2231 let mut path_to_id = FxHashMap::default();
2232 path_to_id.insert(src_path.as_path(), FileId(3));
2233
2234 let mut workspace_roots = FxHashMap::default();
2235 let ws_root = PathBuf::from("/project/packages/ui");
2236 workspace_roots.insert("@myorg/ui", ws_root.as_path());
2237
2238 let pnpm_path = PathBuf::from(
2239 "/project/node_modules/.pnpm/@myorg+ui@1.0.0/node_modules/@myorg/ui/dist/components/Button.js",
2240 );
2241 assert_eq!(
2242 try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
2243 Some(FileId(3)),
2244 "nested .pnpm workspace path should resolve through source fallback"
2245 );
2246 }
2247
2248 #[test]
2249 fn test_try_pnpm_workspace_fallback_no_pnpm() {
2250 let path_to_id: FxHashMap<&Path, FileId> = FxHashMap::default();
2251 let workspace_roots: FxHashMap<&str, &Path> = FxHashMap::default();
2252
2253 let regular_path = PathBuf::from("/project/node_modules/react/index.js");
2254 assert_eq!(
2255 try_pnpm_workspace_fallback(®ular_path, &path_to_id, &workspace_roots),
2256 None,
2257 );
2258 }
2259
2260 #[test]
2261 fn test_try_pnpm_workspace_fallback_with_peer_deps() {
2262 let src_path = PathBuf::from("/project/packages/ui/src/index.ts");
2263 let mut path_to_id = FxHashMap::default();
2264 path_to_id.insert(src_path.as_path(), FileId(4));
2265
2266 let mut workspace_roots = FxHashMap::default();
2267 let ws_root = PathBuf::from("/project/packages/ui");
2268 workspace_roots.insert("@myorg/ui", ws_root.as_path());
2269
2270 let pnpm_path = PathBuf::from(
2271 "/project/node_modules/.pnpm/@myorg+ui@1.0.0_react@18.2.0/node_modules/@myorg/ui/dist/index.js",
2272 );
2273 assert_eq!(
2274 try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
2275 Some(FileId(4)),
2276 ".pnpm path with peer dep suffix should still resolve"
2277 );
2278 }
2279
2280 #[test]
2281 fn make_glob_prefix_only_no_suffix() {
2282 let pattern = fallow_types::extract::DynamicImportPattern {
2283 prefix: "./locales/".to_string(),
2284 suffix: None,
2285 span: oxc_span::Span::default(),
2286 mechanism: ModuleLoadMechanism::EsModule,
2287 excludes: Vec::new(),
2288 };
2289 assert_eq!(make_glob_from_pattern(&pattern), "./locales/*");
2290 }
2291
2292 #[test]
2293 fn make_glob_passthrough_extglob_group() {
2294 let pattern = fallow_types::extract::DynamicImportPattern {
2295 prefix: "./locales/@(en|nl).js".to_string(),
2296 suffix: None,
2297 span: oxc_span::Span::default(),
2298 mechanism: ModuleLoadMechanism::EsModule,
2299 excludes: Vec::new(),
2300 };
2301 assert_eq!(make_glob_from_pattern(&pattern), "./locales/@(en|nl).js");
2302 }
2303
2304 #[test]
2305 fn make_glob_keeps_suffix_after_escaped_metacharacters() {
2306 let pattern = fallow_types::extract::DynamicImportPattern {
2307 prefix: "./[{]group[}]/[*]/".to_string(),
2308 suffix: Some(".js".to_string()),
2309 span: oxc_span::Span::default(),
2310 mechanism: ModuleLoadMechanism::EsModule,
2311 excludes: Vec::new(),
2312 };
2313 assert_eq!(make_glob_from_pattern(&pattern), "./[{]group[}]/[*]/*.js");
2314 }
2315
2316 #[test]
2317 fn make_glob_prefix_with_suffix() {
2318 let pattern = fallow_types::extract::DynamicImportPattern {
2319 prefix: "./locales/".to_string(),
2320 suffix: Some(".json".to_string()),
2321 span: oxc_span::Span::default(),
2322 mechanism: ModuleLoadMechanism::EsModule,
2323 excludes: Vec::new(),
2324 };
2325 assert_eq!(make_glob_from_pattern(&pattern), "./locales/*.json");
2326 }
2327
2328 #[test]
2329 fn make_glob_passthrough_star() {
2330 let pattern = fallow_types::extract::DynamicImportPattern {
2331 prefix: "./pages/**/*.tsx".to_string(),
2332 suffix: None,
2333 span: oxc_span::Span::default(),
2334 mechanism: ModuleLoadMechanism::EsModule,
2335 excludes: Vec::new(),
2336 };
2337 assert_eq!(make_glob_from_pattern(&pattern), "./pages/**/*.tsx");
2338 }
2339
2340 #[test]
2341 fn make_glob_passthrough_brace() {
2342 let pattern = fallow_types::extract::DynamicImportPattern {
2343 prefix: "./i18n/{en,de,fr}.json".to_string(),
2344 suffix: None,
2345 span: oxc_span::Span::default(),
2346 mechanism: ModuleLoadMechanism::EsModule,
2347 excludes: Vec::new(),
2348 };
2349 assert_eq!(make_glob_from_pattern(&pattern), "./i18n/{en,de,fr}.json");
2350 }
2351
2352 #[test]
2353 fn make_glob_empty_prefix_no_suffix() {
2354 let pattern = fallow_types::extract::DynamicImportPattern {
2355 prefix: String::new(),
2356 suffix: None,
2357 span: oxc_span::Span::default(),
2358 mechanism: ModuleLoadMechanism::EsModule,
2359 excludes: Vec::new(),
2360 };
2361 assert_eq!(make_glob_from_pattern(&pattern), "*");
2362 }
2363
2364 #[test]
2365 fn make_glob_empty_prefix_with_suffix() {
2366 let pattern = fallow_types::extract::DynamicImportPattern {
2367 prefix: String::new(),
2368 suffix: Some(".ts".to_string()),
2369 span: oxc_span::Span::default(),
2370 mechanism: ModuleLoadMechanism::EsModule,
2371 excludes: Vec::new(),
2372 };
2373 assert_eq!(make_glob_from_pattern(&pattern), "*.ts");
2374 }
2375
2376 #[test]
2377 fn make_glob_template_literal_prefix_only() {
2378 let pattern = fallow_types::extract::DynamicImportPattern {
2379 prefix: "./pages/".to_string(),
2380 suffix: None,
2381 span: oxc_span::Span::default(),
2382 mechanism: ModuleLoadMechanism::EsModule,
2383 excludes: Vec::new(),
2384 };
2385 assert_eq!(make_glob_from_pattern(&pattern), "./pages/*");
2386 }
2387
2388 #[test]
2389 fn make_glob_template_literal_deep_prefix() {
2390 let pattern = fallow_types::extract::DynamicImportPattern {
2391 prefix: "./modules/".to_string(),
2392 suffix: None,
2393 span: oxc_span::Span::default(),
2394 mechanism: ModuleLoadMechanism::EsModule,
2395 excludes: Vec::new(),
2396 };
2397 assert_eq!(make_glob_from_pattern(&pattern), "./modules/*");
2398 }
2399
2400 #[test]
2401 fn make_glob_string_concat_with_extension() {
2402 let pattern = fallow_types::extract::DynamicImportPattern {
2403 prefix: "./views/".to_string(),
2404 suffix: Some(".vue".to_string()),
2405 span: oxc_span::Span::default(),
2406 mechanism: ModuleLoadMechanism::EsModule,
2407 excludes: Vec::new(),
2408 };
2409 assert_eq!(make_glob_from_pattern(&pattern), "./views/*.vue");
2410 }
2411
2412 #[test]
2413 fn make_glob_import_meta_glob_recursive() {
2414 let pattern = fallow_types::extract::DynamicImportPattern {
2415 prefix: "./components/**/*.vue".to_string(),
2416 suffix: None,
2417 span: oxc_span::Span::default(),
2418 mechanism: ModuleLoadMechanism::EsModule,
2419 excludes: Vec::new(),
2420 };
2421 assert_eq!(
2422 make_glob_from_pattern(&pattern),
2423 "./components/**/*.vue",
2424 "import.meta.glob patterns with * should pass through as-is"
2425 );
2426 }
2427
2428 #[test]
2429 fn make_glob_import_meta_glob_brace_expansion() {
2430 let pattern = fallow_types::extract::DynamicImportPattern {
2431 prefix: "./plugins/{auth,analytics}.ts".to_string(),
2432 suffix: None,
2433 span: oxc_span::Span::default(),
2434 mechanism: ModuleLoadMechanism::EsModule,
2435 excludes: Vec::new(),
2436 };
2437 assert_eq!(
2438 make_glob_from_pattern(&pattern),
2439 "./plugins/{auth,analytics}.ts",
2440 "import.meta.glob patterns with braces should pass through as-is"
2441 );
2442 }
2443
2444 #[test]
2445 fn make_glob_import_meta_glob_star_with_brace() {
2446 let pattern = fallow_types::extract::DynamicImportPattern {
2447 prefix: "./routes/**/*.{ts,tsx}".to_string(),
2448 suffix: None,
2449 span: oxc_span::Span::default(),
2450 mechanism: ModuleLoadMechanism::EsModule,
2451 excludes: Vec::new(),
2452 };
2453 assert_eq!(
2454 make_glob_from_pattern(&pattern),
2455 "./routes/**/*.{ts,tsx}",
2456 "combined * and brace patterns should pass through"
2457 );
2458 }
2459
2460 #[test]
2461 fn make_glob_import_meta_glob_ignores_suffix_when_star_present() {
2462 let pattern = fallow_types::extract::DynamicImportPattern {
2463 prefix: "./*.ts".to_string(),
2464 suffix: Some(".extra".to_string()),
2465 span: oxc_span::Span::default(),
2466 mechanism: ModuleLoadMechanism::EsModule,
2467 excludes: Vec::new(),
2468 };
2469 assert_eq!(
2470 make_glob_from_pattern(&pattern),
2471 "./*.ts",
2472 "when prefix has glob chars, suffix is ignored (prefix used as-is)"
2473 );
2474 }
2475
2476 #[test]
2477 fn make_glob_single_dot_prefix() {
2478 let pattern = fallow_types::extract::DynamicImportPattern {
2479 prefix: "./".to_string(),
2480 suffix: None,
2481 span: oxc_span::Span::default(),
2482 mechanism: ModuleLoadMechanism::EsModule,
2483 excludes: Vec::new(),
2484 };
2485 assert_eq!(make_glob_from_pattern(&pattern), "./*");
2486 }
2487
2488 #[test]
2489 fn make_glob_prefix_without_trailing_slash() {
2490 let pattern = fallow_types::extract::DynamicImportPattern {
2491 prefix: "./config".to_string(),
2492 suffix: None,
2493 span: oxc_span::Span::default(),
2494 mechanism: ModuleLoadMechanism::EsModule,
2495 excludes: Vec::new(),
2496 };
2497 assert_eq!(make_glob_from_pattern(&pattern), "./config*");
2498 }
2499
2500 #[test]
2501 fn make_glob_prefix_with_dotdot() {
2502 let pattern = fallow_types::extract::DynamicImportPattern {
2503 prefix: "../shared/".to_string(),
2504 suffix: Some(".ts".to_string()),
2505 span: oxc_span::Span::default(),
2506 mechanism: ModuleLoadMechanism::EsModule,
2507 excludes: Vec::new(),
2508 };
2509 assert_eq!(make_glob_from_pattern(&pattern), "../shared/*.ts");
2510 }
2511
2512 #[test]
2513 fn test_extract_package_name_with_pnpm_plus_encoded_scope() {
2514 let path = PathBuf::from(
2515 "/project/node_modules/.pnpm/@mui+material@5.15.0/node_modules/@mui/material/index.js",
2516 );
2517 assert_eq!(
2518 extract_package_name_from_node_modules_path(&path),
2519 Some("@mui/material".to_string())
2520 );
2521 }
2522
2523 #[test]
2524 fn test_extract_package_name_windows_style_path() {
2525 let path = PathBuf::from("/project/node_modules/typescript/lib/tsc.js");
2526 assert_eq!(
2527 extract_package_name_from_node_modules_path(&path),
2528 Some("typescript".to_string())
2529 );
2530 }
2531
2532 #[test]
2533 fn test_try_source_fallback_out_dir() {
2534 let src_path = PathBuf::from("/project/packages/api/src/handler.ts");
2535 let mut path_to_id = FxHashMap::default();
2536 path_to_id.insert(src_path.as_path(), FileId(5));
2537
2538 let out_path = PathBuf::from("/project/packages/api/out/handler.js");
2539 assert_eq!(
2540 try_source_fallback(&out_path, &path_to_id),
2541 Some(FileId(5)),
2542 "out/handler.js should fall back to src/handler.ts"
2543 );
2544 }
2545
2546 #[test]
2547 fn test_try_source_fallback_mts_extension() {
2548 let src_path = PathBuf::from("/project/packages/lib/src/utils.mts");
2549 let mut path_to_id = FxHashMap::default();
2550 path_to_id.insert(src_path.as_path(), FileId(6));
2551
2552 let dist_path = PathBuf::from("/project/packages/lib/dist/utils.mjs");
2553 assert_eq!(
2554 try_source_fallback(&dist_path, &path_to_id),
2555 Some(FileId(6)),
2556 "dist/utils.mjs should fall back to src/utils.mts"
2557 );
2558 }
2559
2560 #[test]
2561 fn test_try_source_fallback_declaration_with_dotted_name() {
2562 let src_path = PathBuf::from("/project/packages/lib/src/x.port.ts");
2563 let mut path_to_id = FxHashMap::default();
2564 path_to_id.insert(src_path.as_path(), FileId(6));
2565
2566 let dist_path = PathBuf::from("/project/packages/lib/dist/x.port.d.ts");
2567 assert_eq!(
2568 try_source_fallback(&dist_path, &path_to_id),
2569 Some(FileId(6)),
2570 "dist/x.port.d.ts should fall back to src/x.port.ts"
2571 );
2572 }
2573
2574 #[test]
2575 fn test_try_source_fallback_module_declaration() {
2576 let src_path = PathBuf::from("/project/packages/lib/src/x.mts");
2577 let mut path_to_id = FxHashMap::default();
2578 path_to_id.insert(src_path.as_path(), FileId(6));
2579
2580 let dist_path = PathBuf::from("/project/packages/lib/dist/x.d.mts");
2581 assert_eq!(
2582 try_source_fallback(&dist_path, &path_to_id),
2583 Some(FileId(6)),
2584 "dist/x.d.mts should fall back to src/x.mts"
2585 );
2586 }
2587
2588 #[test]
2589 fn test_try_source_fallback_commonjs_declaration_in_subdir() {
2590 let src_path = PathBuf::from("/project/packages/lib/src/sub/x.cts");
2591 let mut path_to_id = FxHashMap::default();
2592 path_to_id.insert(src_path.as_path(), FileId(6));
2593
2594 let dist_path = PathBuf::from("/project/packages/lib/dist/sub/x.d.cts");
2595 assert_eq!(
2596 try_source_fallback(&dist_path, &path_to_id),
2597 Some(FileId(6)),
2598 "dist/sub/x.d.cts should fall back to src/sub/x.cts"
2599 );
2600 }
2601
2602 #[test]
2603 fn test_try_source_fallback_declaration_copied_from_src() {
2604 let src_path = PathBuf::from("/project/packages/lib/src/globals.d.ts");
2605 let mut path_to_id = FxHashMap::default();
2606 path_to_id.insert(src_path.as_path(), FileId(6));
2607
2608 let dist_path = PathBuf::from("/project/packages/lib/dist/globals.d.ts");
2609 assert_eq!(
2610 try_source_fallback(&dist_path, &path_to_id),
2611 Some(FileId(6)),
2612 "dist/globals.d.ts should fall back to a copied src/globals.d.ts"
2613 );
2614 }
2615
2616 #[test]
2617 fn test_try_source_fallback_js_with_dotted_name() {
2618 let src_path = PathBuf::from("/project/packages/lib/src/x.port.ts");
2619 let mut path_to_id = FxHashMap::default();
2620 path_to_id.insert(src_path.as_path(), FileId(6));
2621
2622 let dist_path = PathBuf::from("/project/packages/lib/dist/x.port.js");
2623 assert_eq!(
2624 try_source_fallback(&dist_path, &path_to_id),
2625 Some(FileId(6)),
2626 "dist/x.port.js should fall back to src/x.port.ts"
2627 );
2628 }
2629
2630 #[test]
2631 fn test_try_source_fallback_cts_extension() {
2632 let src_path = PathBuf::from("/project/packages/lib/src/config.cts");
2633 let mut path_to_id = FxHashMap::default();
2634 path_to_id.insert(src_path.as_path(), FileId(7));
2635
2636 let dist_path = PathBuf::from("/project/packages/lib/dist/config.cjs");
2637 assert_eq!(
2638 try_source_fallback(&dist_path, &path_to_id),
2639 Some(FileId(7)),
2640 "dist/config.cjs should fall back to src/config.cts"
2641 );
2642 }
2643
2644 #[test]
2645 fn test_try_source_fallback_jsx_extension() {
2646 let src_path = PathBuf::from("/project/packages/ui/src/App.jsx");
2647 let mut path_to_id = FxHashMap::default();
2648 path_to_id.insert(src_path.as_path(), FileId(8));
2649
2650 let build_path = PathBuf::from("/project/packages/ui/build/App.js");
2651 assert_eq!(
2652 try_source_fallback(&build_path, &path_to_id),
2653 Some(FileId(8)),
2654 "build/App.js should fall back to src/App.jsx"
2655 );
2656 }
2657
2658 #[test]
2659 fn test_try_source_fallback_no_file_stem() {
2660 let path_to_id: FxHashMap<&Path, FileId> = FxHashMap::default();
2661 let dist_path = PathBuf::from("/project/packages/ui/dist/");
2662 assert_eq!(
2663 try_source_fallback(&dist_path, &path_to_id),
2664 None,
2665 "directory path with no file should return None"
2666 );
2667 }
2668
2669 #[test]
2670 fn test_try_source_fallback_esm_subdir() {
2671 let src_path = PathBuf::from("/project/lib/src/index.ts");
2672 let mut path_to_id = FxHashMap::default();
2673 path_to_id.insert(src_path.as_path(), FileId(10));
2674
2675 let dist_path = PathBuf::from("/project/lib/esm/index.mjs");
2676 assert_eq!(
2677 try_source_fallback(&dist_path, &path_to_id),
2678 Some(FileId(10)),
2679 "standalone esm/ directory should fall back to src/"
2680 );
2681 }
2682
2683 #[test]
2684 fn test_try_source_fallback_cjs_subdir() {
2685 let src_path = PathBuf::from("/project/lib/src/index.ts");
2686 let mut path_to_id = FxHashMap::default();
2687 path_to_id.insert(src_path.as_path(), FileId(11));
2688
2689 let cjs_path = PathBuf::from("/project/lib/cjs/index.cjs");
2690 assert_eq!(
2691 try_source_fallback(&cjs_path, &path_to_id),
2692 Some(FileId(11)),
2693 "standalone cjs/ directory should fall back to src/"
2694 );
2695 }
2696
2697 #[test]
2698 fn test_try_pnpm_workspace_fallback_empty_after_pnpm() {
2699 let path_to_id: FxHashMap<&Path, FileId> = FxHashMap::default();
2700 let workspace_roots: FxHashMap<&str, &Path> = FxHashMap::default();
2701
2702 let pnpm_path = PathBuf::from("/project/node_modules/.pnpm/pkg@1.0.0/node_modules");
2703 assert_eq!(
2704 try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
2705 None,
2706 "path ending at node_modules with nothing after should return None"
2707 );
2708 }
2709
2710 #[test]
2711 fn test_try_pnpm_workspace_fallback_scoped_package_only_scope() {
2712 let path_to_id: FxHashMap<&Path, FileId> = FxHashMap::default();
2713 let workspace_roots: FxHashMap<&str, &Path> = FxHashMap::default();
2714
2715 let pnpm_path =
2716 PathBuf::from("/project/node_modules/.pnpm/@scope+pkg@1.0.0/node_modules/@scope");
2717 assert_eq!(
2718 try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
2719 None,
2720 "scoped package without full name and no matching workspace should return None"
2721 );
2722 }
2723
2724 #[test]
2725 fn test_try_pnpm_workspace_fallback_no_inner_node_modules() {
2726 let path_to_id: FxHashMap<&Path, FileId> = FxHashMap::default();
2727 let workspace_roots: FxHashMap<&str, &Path> = FxHashMap::default();
2728
2729 let pnpm_path = PathBuf::from("/project/node_modules/.pnpm/pkg@1.0.0/dist/index.js");
2730 assert_eq!(
2731 try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
2732 None,
2733 "path without inner node_modules after .pnpm should return None"
2734 );
2735 }
2736
2737 #[test]
2738 fn test_try_pnpm_workspace_fallback_package_without_relative_path() {
2739 let path_to_id: FxHashMap<&Path, FileId> = FxHashMap::default();
2740 let mut workspace_roots = FxHashMap::default();
2741 let ws_root = PathBuf::from("/project/packages/ui");
2742 workspace_roots.insert("@myorg/ui", ws_root.as_path());
2743
2744 let pnpm_path =
2745 PathBuf::from("/project/node_modules/.pnpm/@myorg+ui@1.0.0/node_modules/@myorg/ui");
2746 assert_eq!(
2747 try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
2748 None,
2749 "path ending at package name with no relative file should return None"
2750 );
2751 }
2752
2753 #[test]
2754 fn test_try_pnpm_workspace_fallback_nested_dist_esm() {
2755 let src_path = PathBuf::from("/project/packages/ui/src/Button.ts");
2756 let mut path_to_id = FxHashMap::default();
2757 path_to_id.insert(src_path.as_path(), FileId(10));
2758
2759 let mut workspace_roots = FxHashMap::default();
2760 let ws_root = PathBuf::from("/project/packages/ui");
2761 workspace_roots.insert("@myorg/ui", ws_root.as_path());
2762
2763 let pnpm_path = PathBuf::from(
2764 "/project/node_modules/.pnpm/@myorg+ui@1.0.0/node_modules/@myorg/ui/dist/esm/Button.mjs",
2765 );
2766 assert_eq!(
2767 try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
2768 Some(FileId(10)),
2769 "pnpm path with nested dist/esm should resolve through source fallback"
2770 );
2771 }
2772
2773 #[test]
2776 fn package_map_target_string_value_dot_key() {
2777 let map = serde_json::Value::String("./src/index.ts".to_string());
2780 let conds = conditions();
2781 let result = package_map_target(&map, ".", &conds);
2783 assert!(
2784 matches!(result, PackageMapTarget::Targets(_)),
2785 "string map with '.' key should return Targets"
2786 );
2787 }
2788
2789 #[test]
2790 fn package_map_target_string_value_non_dot_key_no_match() {
2791 let map = serde_json::Value::String("./src/index.ts".to_string());
2793 let conds = conditions();
2794 let result = package_map_target(&map, "./sub", &conds);
2795 assert!(
2796 matches!(result, PackageMapTarget::NoMatch),
2797 "string map with non-dot key should return NoMatch"
2798 );
2799 }
2800
2801 #[test]
2802 fn package_map_target_null_value_dot_key() {
2803 let map = serde_json::Value::Null;
2805 let conds = conditions();
2806 let result = package_map_target(&map, ".", &conds);
2807 assert!(
2808 matches!(result, PackageMapTarget::Blocked),
2809 "null map with '.' key should return Blocked"
2810 );
2811 }
2812
2813 #[test]
2814 fn package_map_target_bool_value_non_dot_key() {
2815 let map = serde_json::Value::Bool(true);
2817 let conds = conditions();
2818 let result = package_map_target(&map, "./sub", &conds);
2819 assert!(
2820 matches!(result, PackageMapTarget::NoMatch),
2821 "bool map with non-dot key should return NoMatch"
2822 );
2823 }
2824
2825 #[test]
2828 fn package_map_target_condition_only_object_dot_key() {
2829 let map = serde_json::json!({
2832 "import": "./src/index.mjs",
2833 "require": "./src/index.cjs"
2834 });
2835 let conds = conditions();
2836 let result = package_map_target(&map, ".", &conds);
2837 assert!(
2838 matches!(result, PackageMapTarget::Targets(_)),
2839 "condition-only object with '.' key should return Targets"
2840 );
2841 }
2842
2843 #[test]
2844 fn package_map_target_condition_only_object_non_dot_key() {
2845 let map = serde_json::json!({
2848 "import": "./src/index.mjs",
2849 "require": "./src/index.cjs"
2850 });
2851 let conds = conditions();
2852 let result = package_map_target(&map, "./nonexistent", &conds);
2853 assert!(
2854 matches!(result, PackageMapTarget::NoMatch),
2855 "condition-only object with non-dot key should return NoMatch"
2856 );
2857 }
2858
2859 #[test]
2862 fn resolve_package_map_value_unmatched_conditions_returns_none() {
2863 let value = serde_json::json!({ "browser": "./src/browser.js" });
2865 let conds = conditions(); assert_eq!(
2867 resolve_package_map_value(&value, &conds, None),
2868 None,
2869 "unmatched condition should return None"
2870 );
2871 }
2872
2873 #[test]
2874 fn resolve_package_map_value_bool_returns_none() {
2875 let value = serde_json::Value::Bool(false);
2876 let conds = conditions();
2877 assert_eq!(
2878 resolve_package_map_value(&value, &conds, None),
2879 None,
2880 "bool value should return None"
2881 );
2882 }
2883
2884 #[test]
2885 fn resolve_package_map_value_number_returns_none() {
2886 let value = serde_json::Value::Number(42.into());
2887 let conds = conditions();
2888 assert_eq!(
2889 resolve_package_map_value(&value, &conds, None),
2890 None,
2891 "number value should return None"
2892 );
2893 }
2894
2895 #[test]
2896 fn resolve_package_map_value_null_returns_none() {
2897 let value = serde_json::Value::Null;
2898 let conds = conditions();
2899 assert_eq!(
2900 resolve_package_map_value(&value, &conds, None),
2901 None,
2902 "null value should return None"
2903 );
2904 }
2905
2906 #[test]
2907 fn resolve_package_map_value_array_all_null_returns_none() {
2908 let value = serde_json::json!([null, false, 42]);
2910 let conds = conditions();
2911 assert_eq!(
2912 resolve_package_map_value(&value, &conds, None),
2913 None,
2914 "array of unresolvable values should return None"
2915 );
2916 }
2917
2918 #[test]
2919 fn resolve_package_map_value_array_with_valid_entry() {
2920 let value = serde_json::json!([null, "./src/index.ts"]);
2922 let conds = conditions();
2923 let result = resolve_package_map_value(&value, &conds, None);
2924 assert_eq!(
2925 result,
2926 Some(vec!["./src/index.ts".to_string()]),
2927 "array with a valid string entry should return that entry"
2928 );
2929 }
2930
2931 #[test]
2934 fn package_map_pattern_capture_no_star_returns_none() {
2935 assert_eq!(
2937 package_map_pattern_capture("./exact", "./exact"),
2938 None,
2939 "pattern with no star should return None"
2940 );
2941 }
2942
2943 #[test]
2944 fn package_map_pattern_capture_two_stars_returns_none() {
2945 assert_eq!(
2947 package_map_pattern_capture("./*/*.js", "./foo/bar.js"),
2948 None,
2949 "pattern with two stars should return None"
2950 );
2951 }
2952
2953 #[test]
2954 fn package_map_pattern_capture_single_star_captures() {
2955 assert_eq!(
2957 package_map_pattern_capture("./dist/*/index.js", "./dist/utils/index.js"),
2958 Some("utils".to_string()),
2959 );
2960 }
2961
2962 #[test]
2963 fn package_map_pattern_capture_no_prefix_match_returns_none() {
2964 assert_eq!(
2966 package_map_pattern_capture("./lib/*.js", "./src/foo.js"),
2967 None,
2968 );
2969 }
2970
2971 #[test]
2974 fn resolve_package_map_target_no_dot_slash_prefix_returns_none() {
2975 let root = PathBuf::from("/project/packages/ui");
2977 let pj = fallow_config::PackageJson::default();
2978 with_package_map_ctx(root, Some("@myorg/ui"), pj, &[], |ctx, manifest, _root| {
2979 let result = resolve_package_map_target(ctx, manifest, "src/index.ts", None);
2980 assert_eq!(result, None, "target without './' should return None");
2981 });
2982 }
2983
2984 #[test]
2985 fn resolve_package_map_target_parent_dir_returns_none() {
2986 let root = PathBuf::from("/project/packages/ui");
2988 let pj = fallow_config::PackageJson::default();
2989 with_package_map_ctx(root, Some("@myorg/ui"), pj, &[], |ctx, manifest, _root| {
2990 let result = resolve_package_map_target(ctx, manifest, "./../outside/file.ts", None);
2991 assert_eq!(result, None, "parent-dir target should return None");
2992 });
2993 }
2994
2995 #[test]
2996 fn resolve_package_map_target_absolute_path_returns_none() {
2997 let root = PathBuf::from("/project/packages/ui");
2999 let pj = fallow_config::PackageJson::default();
3000 with_package_map_ctx(root, Some("@myorg/ui"), pj, &[], |ctx, manifest, _root| {
3001 let result = resolve_package_map_target(ctx, manifest, ".//abs/path.ts", None);
3004 assert_eq!(result, None, "absolute target should return None");
3005 });
3006 }
3007
3008 #[test]
3009 fn resolve_package_map_target_valid_target_hits_raw_path_map() {
3010 let root = PathBuf::from("/project/packages/ui");
3012 let src = root.join("src/index.ts");
3013 let pj = fallow_config::PackageJson::default();
3014 with_package_map_ctx(
3015 root,
3016 Some("@myorg/ui"),
3017 pj,
3018 &[(src, FileId(5))],
3019 |ctx, manifest, _root| {
3020 let result = resolve_package_map_target(ctx, manifest, "./src/index.ts", None);
3021 assert_eq!(
3022 result,
3023 Some(FileId(5)),
3024 "valid target in raw_path_to_id should resolve"
3025 );
3026 },
3027 );
3028 }
3029
3030 #[test]
3033 fn package_import_source_subpath_strips_hash_and_package_name() {
3034 let manifest = PackageManifestInfo {
3035 root: PathBuf::from("/project"),
3036 canonical_root: PathBuf::from("/project"),
3037 name: Some("my-pkg".to_string()),
3038 package_json: fallow_config::PackageJson::default(),
3039 deno_import_map: Vec::new(),
3040 };
3041 let result = package_import_source_subpath(&manifest, "#my-pkg/utils");
3042 assert_eq!(
3043 result,
3044 Some(PathBuf::from("utils")),
3045 "should strip '#', package name, and '/' separator"
3046 );
3047 }
3048
3049 #[test]
3050 fn package_import_source_subpath_no_package_name_match_keeps_full_subpath() {
3051 let manifest = PackageManifestInfo {
3054 root: PathBuf::from("/project"),
3055 canonical_root: PathBuf::from("/project"),
3056 name: Some("my-pkg".to_string()),
3057 package_json: fallow_config::PackageJson::default(),
3058 deno_import_map: Vec::new(),
3059 };
3060 let result = package_import_source_subpath(&manifest, "#utils");
3061 assert_eq!(
3062 result,
3063 Some(PathBuf::from("utils")),
3064 "without package-name prefix the full subpath should be kept"
3065 );
3066 }
3067
3068 #[test]
3069 fn package_import_source_subpath_empty_hash_returns_none() {
3070 let manifest = PackageManifestInfo {
3073 root: PathBuf::from("/project"),
3074 canonical_root: PathBuf::from("/project"),
3075 name: Some("my-pkg".to_string()),
3076 package_json: fallow_config::PackageJson::default(),
3077 deno_import_map: Vec::new(),
3078 };
3079 let result = package_import_source_subpath(&manifest, "#");
3081 assert_eq!(
3082 result, None,
3083 "specifier '#' with empty body should return None"
3084 );
3085 }
3086
3087 #[test]
3088 fn package_import_source_subpath_no_hash_returns_none() {
3089 let manifest = PackageManifestInfo {
3091 root: PathBuf::from("/project"),
3092 canonical_root: PathBuf::from("/project"),
3093 name: Some("my-pkg".to_string()),
3094 package_json: fallow_config::PackageJson::default(),
3095 deno_import_map: Vec::new(),
3096 };
3097 let result = package_import_source_subpath(&manifest, "no-hash");
3098 assert_eq!(result, None, "specifier without '#' should return None");
3099 }
3100
3101 #[test]
3102 fn package_import_source_subpath_no_manifest_name() {
3103 let manifest = PackageManifestInfo {
3105 root: PathBuf::from("/project"),
3106 canonical_root: PathBuf::from("/project"),
3107 name: None,
3108 package_json: fallow_config::PackageJson::default(),
3109 deno_import_map: Vec::new(),
3110 };
3111 let result = package_import_source_subpath(&manifest, "#internal/helper");
3112 assert_eq!(
3113 result,
3114 Some(PathBuf::from("internal/helper")),
3115 "manifest without name should return the full stripped specifier"
3116 );
3117 }
3118
3119 #[test]
3122 fn nearest_package_manifest_returns_deepest_match() {
3123 let root1 = PathBuf::from("/project");
3124 let root2 = PathBuf::from("/project/packages/ui");
3125 let m1 = PackageManifestInfo {
3126 root: root1.clone(),
3127 canonical_root: root1,
3128 name: Some("root".to_string()),
3129 package_json: fallow_config::PackageJson::default(),
3130 deno_import_map: Vec::new(),
3131 };
3132 let m2 = PackageManifestInfo {
3133 root: root2.clone(),
3134 canonical_root: root2,
3135 name: Some("@myorg/ui".to_string()),
3136 package_json: fallow_config::PackageJson::default(),
3137 deno_import_map: Vec::new(),
3138 };
3139 let manifests = [m1, m2];
3140 let from_file = Path::new("/project/packages/ui/src/index.ts");
3141 let result = nearest_package_manifest(&manifests, from_file);
3142 assert_eq!(
3143 result.and_then(|m| m.name.as_deref()),
3144 Some("@myorg/ui"),
3145 "should pick the deepest (longest path) manifest that contains the file"
3146 );
3147 }
3148
3149 #[test]
3150 fn nearest_package_manifest_no_match_returns_none() {
3151 let root = PathBuf::from("/project/packages/ui");
3152 let m = PackageManifestInfo {
3153 root: root.clone(),
3154 canonical_root: root,
3155 name: Some("@myorg/ui".to_string()),
3156 package_json: fallow_config::PackageJson::default(),
3157 deno_import_map: Vec::new(),
3158 };
3159 let manifests = [m];
3160 let from_file = Path::new("/other/project/src/index.ts");
3162 let result = nearest_package_manifest(&manifests, from_file);
3163 assert!(
3164 result.is_none(),
3165 "file outside all manifest roots should return None"
3166 );
3167 }
3168
3169 #[test]
3172 fn lookup_internal_file_id_uses_path_to_id_when_raw_misses() {
3173 let target = PathBuf::from("/project/src/index.ts");
3176 let mut path_to_id: FxHashMap<&Path, FileId> = FxHashMap::default();
3177 path_to_id.insert(target.as_path(), FileId(99));
3178 let raw_path_to_id: FxHashMap<&Path, FileId> = FxHashMap::default();
3179 let workspace_roots: FxHashMap<&str, &Path> = FxHashMap::default();
3180 let condition_names = conditions();
3181 let resolver = oxc_resolver::Resolver::new(oxc_resolver::ResolveOptions::default());
3182 let tsconfig_warned = std::sync::Mutex::new(FxHashSet::default());
3183 let tsconfig_cache = TsconfigCache::default();
3184 let canonicalize_cache = CanonicalizeCache::default();
3185 let root = PathBuf::from("/project");
3186 let script_resolver = crate::resolve::specifier::create_script_resolver(&resolver);
3187 let ctx = ResolveContext {
3188 resolver: &resolver,
3189 script_resolver: &script_resolver,
3190 style_resolver: &resolver,
3191 extensions: &[],
3192 path_to_id: &path_to_id,
3193 raw_path_to_id: &raw_path_to_id,
3194 workspace_roots: &workspace_roots,
3195 package_manifests: &[],
3196 has_deno_import_maps: false,
3197 condition_names: &condition_names,
3198 path_aliases: &[],
3199 scss_include_paths: &[],
3200 static_dir_mappings: &[],
3201 framework_static_dir_mappings: &[],
3202 root: &root,
3203 canonical_fallback: None,
3204 tsconfig_warned: &tsconfig_warned,
3205 tsconfig_cache: &tsconfig_cache,
3206 canonicalize_cache: &canonicalize_cache,
3207 };
3208 let result = lookup_internal_file_id(&ctx, &target);
3209 assert_eq!(
3210 result,
3211 Some(FileId(99)),
3212 "should fall back from raw_path_to_id to path_to_id"
3213 );
3214 }
3215
3216 #[test]
3219 fn try_scss_partial_fallback_rejects_colon_specifier() {
3220 let root = PathBuf::from("/project");
3223 let pj = fallow_config::PackageJson::default();
3224 with_package_map_ctx(root.clone(), None, pj, &[], |ctx, _manifest, _r| {
3225 let from_file = root.join("src/main.scss");
3226 let result = try_scss_partial_fallback(ctx, &from_file, "sass:math");
3227 assert!(
3228 result.is_none(),
3229 "specifier with ':' should short-circuit to None"
3230 );
3231 });
3232 }
3233
3234 #[test]
3235 fn try_scss_partial_fallback_rejects_already_partial_filename() {
3236 let root = PathBuf::from("/project");
3239 let pj = fallow_config::PackageJson::default();
3240 with_package_map_ctx(root.clone(), None, pj, &[], |ctx, _manifest, _r| {
3241 let from_file = root.join("src/main.scss");
3242 let result = try_scss_partial_fallback(ctx, &from_file, "./_variables");
3243 assert!(
3244 result.is_none(),
3245 "already-partial filename should short-circuit to None"
3246 );
3247 });
3248 }
3249
3250 #[test]
3253 fn try_workspace_package_fallback_rejects_relative_specifier() {
3254 let root = PathBuf::from("/project");
3257 let pj = fallow_config::PackageJson::default();
3258 with_package_map_ctx(root, None, pj, &[], |ctx, _manifest, _r| {
3259 let result = try_workspace_package_fallback(ctx, "./local/module", false);
3260 assert!(
3261 result.is_none(),
3262 "relative specifier should return None from workspace fallback"
3263 );
3264 });
3265 }
3266
3267 #[test]
3268 fn try_workspace_package_fallback_rejects_absolute_path() {
3269 let root = PathBuf::from("/project");
3271 let pj = fallow_config::PackageJson::default();
3272 with_package_map_ctx(root, None, pj, &[], |ctx, _manifest, _r| {
3273 let result = try_workspace_package_fallback(ctx, "/absolute/path", false);
3274 assert!(
3275 result.is_none(),
3276 "absolute path should return None from workspace fallback"
3277 );
3278 });
3279 }
3280}