1use std::path::{Path, PathBuf};
7
8use rustc_hash::FxHashMap;
9use serde_json::Value;
10
11use fallow_types::discover::FileId;
12
13use super::path_info::{extract_package_name, is_bare_specifier, is_valid_package_name};
14use super::types::{OUTPUT_DIRS, PackageManifestInfo, ResolveContext, ResolveResult, SOURCE_EXTS};
15
16fn alias_match_remainder<'a>(specifier: &'a str, prefix: &str) -> Option<&'a str> {
25 let remainder = specifier.strip_prefix(prefix)?;
26 (remainder.is_empty() || prefix.ends_with('/') || remainder.starts_with('/'))
27 .then_some(remainder)
28}
29
30pub(super) fn try_path_alias_fallback(
37 ctx: &ResolveContext<'_>,
38 specifier: &str,
39) -> Option<ResolveResult> {
40 for (prefix, replacement) in ctx.path_aliases {
41 let Some(remainder) = alias_match_remainder(specifier, prefix) else {
42 continue;
43 };
44
45 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) = ctx.resolver.resolve(ctx.root, &substituted) {
61 let resolved_path = resolved.path();
62 if let Some(&file_id) = ctx.raw_path_to_id.get(resolved_path) {
64 return Some(ResolveResult::InternalModule(file_id));
65 }
66 if let Ok(canonical) = dunce::canonicalize(resolved_path) {
68 if let Some(&file_id) = ctx.path_to_id.get(canonical.as_path()) {
69 return Some(ResolveResult::InternalModule(file_id));
70 }
71 if let Some(file_id) = try_source_fallback(&canonical, ctx.path_to_id) {
72 return Some(ResolveResult::InternalModule(file_id));
73 }
74 if let Some(file_id) =
75 try_pnpm_workspace_fallback(&canonical, ctx.path_to_id, ctx.workspace_roots)
76 {
77 return Some(ResolveResult::InternalModule(file_id));
78 }
79 if let Some(pkg_name) = extract_package_name_from_node_modules_path(&canonical) {
80 return Some(ResolveResult::NpmPackage(pkg_name));
81 }
82 return Some(ResolveResult::ExternalFile(canonical));
83 }
84 }
85 }
86 None
87}
88
89pub(super) fn try_scss_partial_fallback(
98 ctx: &ResolveContext<'_>,
99 from_file: &Path,
100 specifier: &str,
101) -> Option<ResolveResult> {
102 if specifier.contains(':') {
104 return None;
105 }
106
107 let spec_path = Path::new(specifier);
108 let filename = spec_path.file_name()?.to_str()?;
109
110 if filename.starts_with('_') {
112 return None;
113 }
114
115 let partial_filename = format!("_{filename}");
117 let partial_specifier = if let Some(parent) = spec_path.parent()
118 && !parent.as_os_str().is_empty()
119 {
120 format!("{}/{partial_filename}", parent.display())
121 } else {
122 partial_filename
123 };
124
125 if let Some(result) = try_resolve_scss(ctx, from_file, &partial_specifier) {
126 return Some(result);
127 }
128
129 let index_partial = format!("{specifier}/_index");
131 if let Some(result) = try_resolve_scss(ctx, from_file, &index_partial) {
132 return Some(result);
133 }
134
135 let index_plain = format!("{specifier}/index");
136 try_resolve_scss(ctx, from_file, &index_plain)
137}
138
139pub(super) fn try_css_extension_fallback(
149 ctx: &ResolveContext<'_>,
150 from_file: &Path,
151 specifier: &str,
152) -> Option<ResolveResult> {
153 if specifier.contains(':') {
154 return None;
155 }
156 let spec_path = Path::new(specifier);
160 let already_css_ext = spec_path
161 .extension()
162 .and_then(|e| e.to_str())
163 .is_some_and(|e| {
164 e.eq_ignore_ascii_case("css")
165 || e.eq_ignore_ascii_case("scss")
166 || e.eq_ignore_ascii_case("sass")
167 });
168 if already_css_ext {
169 return try_resolve_scss(ctx, from_file, specifier);
170 }
171 for ext in ["scss", "sass", "css"] {
172 let candidate = format!("{specifier}.{ext}");
173 if let Some(result) = try_resolve_scss(ctx, from_file, &candidate) {
174 return Some(result);
175 }
176 }
177 None
178}
179
180fn try_resolve_scss(
182 ctx: &ResolveContext<'_>,
183 from_file: &Path,
184 specifier: &str,
185) -> Option<ResolveResult> {
186 let resolved = ctx.resolver.resolve_file(from_file, specifier).ok()?;
187 let resolved_path = resolved.path();
188
189 if let Some(&file_id) = ctx.raw_path_to_id.get(resolved_path) {
190 return Some(ResolveResult::InternalModule(file_id));
191 }
192 if let Ok(canonical) = dunce::canonicalize(resolved_path)
193 && let Some(&file_id) = ctx.path_to_id.get(canonical.as_path())
194 {
195 return Some(ResolveResult::InternalModule(file_id));
196 }
197 None
198}
199
200pub(super) fn try_scss_include_path_fallback(
220 ctx: &ResolveContext<'_>,
221 from_file: &Path,
222 specifier: &str,
223 from_style: bool,
224) -> Option<ResolveResult> {
225 if ctx.scss_include_paths.is_empty() {
226 return None;
227 }
228 let is_scss_importer = from_file
229 .extension()
230 .is_some_and(|e| e == "scss" || e == "sass");
231 if !is_scss_importer && !from_style {
232 return None;
233 }
234 if specifier.contains(':') {
236 return None;
237 }
238 let bare = specifier.strip_prefix("./")?;
242 if bare.starts_with("..") || bare.starts_with('/') {
243 return None;
244 }
245
246 for include_dir in ctx.scss_include_paths {
247 if let Some(file_id) = find_scss_in_dir(include_dir, bare, ctx) {
248 return Some(ResolveResult::InternalModule(file_id));
249 }
250 }
251 None
252}
253
254fn find_scss_in_dir(include_dir: &Path, bare: &str, ctx: &ResolveContext<'_>) -> Option<FileId> {
258 let bare_path = Path::new(bare);
259 let has_scss_ext = matches!(
260 bare_path.extension().and_then(|e| e.to_str()),
261 Some(ext) if ext.eq_ignore_ascii_case("scss") || ext.eq_ignore_ascii_case("sass")
262 );
263
264 let parent = bare_path.parent();
267 let stem_with_ext = bare_path.file_name()?.to_str()?;
268 let stem_without_ext = bare_path.file_stem().and_then(|s| s.to_str())?;
269
270 let build = |rel: &Path| -> std::path::PathBuf { include_dir.join(rel) };
271 let join_with_parent = |name: &str| -> std::path::PathBuf {
272 parent.map_or_else(|| build(Path::new(name)), |p| build(&p.join(name)))
273 };
274
275 let exts: &[&str] = if has_scss_ext {
276 &[""]
277 } else {
278 &["scss", "sass"]
279 };
280
281 for ext in exts {
282 let suffix = if ext.is_empty() {
283 String::new()
284 } else {
285 format!(".{ext}")
286 };
287 let direct = if ext.is_empty() {
289 build(bare_path)
290 } else {
291 join_with_parent(&format!("{stem_with_ext}{suffix}"))
292 };
293 if let Some(fid) = lookup_scss_path(&direct, ctx) {
294 return Some(fid);
295 }
296 let partial_name = if ext.is_empty() {
298 format!("_{stem_with_ext}")
299 } else {
300 format!("_{stem_without_ext}{suffix}")
301 };
302 let partial = join_with_parent(&partial_name);
303 if let Some(fid) = lookup_scss_path(&partial, ctx) {
304 return Some(fid);
305 }
306 if ext.is_empty() {
307 continue;
309 }
310 let idx_partial = build(bare_path).join(format!("_index{suffix}"));
312 if let Some(fid) = lookup_scss_path(&idx_partial, ctx) {
313 return Some(fid);
314 }
315 let idx_plain = build(bare_path).join(format!("index{suffix}"));
316 if let Some(fid) = lookup_scss_path(&idx_plain, ctx) {
317 return Some(fid);
318 }
319 }
320 None
321}
322
323fn lookup_scss_path(candidate: &Path, ctx: &ResolveContext<'_>) -> Option<FileId> {
326 if let Some(&file_id) = ctx.raw_path_to_id.get(candidate) {
327 return Some(file_id);
328 }
329 if let Ok(canonical) = dunce::canonicalize(candidate) {
330 if let Some(&file_id) = ctx.path_to_id.get(canonical.as_path()) {
331 return Some(file_id);
332 }
333 if let Some(fallback) = ctx.canonical_fallback
334 && let Some(file_id) = fallback.get(&canonical)
335 {
336 return Some(file_id);
337 }
338 }
339 None
340}
341
342pub(super) fn try_scss_node_modules_fallback(
364 _ctx: &ResolveContext<'_>,
365 from_file: &Path,
366 specifier: &str,
367 from_style: bool,
368) -> Option<ResolveResult> {
369 if specifier.contains(':') {
371 return None;
372 }
373 let is_scss_importer = from_file
374 .extension()
375 .is_some_and(|e| e == "scss" || e == "sass");
376 if !is_scss_importer && !from_style {
377 return None;
378 }
379 let bare = specifier.strip_prefix("./")?;
383 if bare.starts_with("..") || bare.starts_with('/') {
384 return None;
385 }
386 if bare.is_empty() {
390 return None;
391 }
392
393 let mut dir = from_file.parent()?;
401 loop {
402 let nm_dir = dir.join("node_modules");
403 if nm_dir.is_dir()
404 && let Some(path) = find_scss_in_node_modules(&nm_dir, bare)
405 && let Some(pkg_name) = extract_package_name_from_node_modules_path(&path)
406 {
407 return Some(ResolveResult::NpmPackage(pkg_name));
408 }
409 let Some(parent) = dir.parent() else {
410 break;
411 };
412 dir = parent;
413 }
414 None
415}
416
417fn find_scss_in_node_modules(nm_dir: &Path, bare: &str) -> Option<PathBuf> {
426 let bare_path = Path::new(bare);
427 let file_name = bare_path.file_name()?.to_str()?;
428 let parent = bare_path.parent();
429 let join_with_parent = |name: &str| -> PathBuf {
430 parent.map_or_else(|| nm_dir.join(name), |p| nm_dir.join(p).join(name))
431 };
432
433 for ext in &["scss", "sass", "css"] {
437 let candidate = join_with_parent(&format!("{file_name}.{ext}"));
438 if candidate.is_file() {
439 return Some(candidate);
440 }
441 }
442 for ext in &["scss", "sass"] {
445 let candidate = join_with_parent(&format!("_{file_name}.{ext}"));
446 if candidate.is_file() {
447 return Some(candidate);
448 }
449 }
450 for ext in &["scss", "sass"] {
452 let idx_partial = nm_dir.join(bare).join(format!("_index.{ext}"));
453 if idx_partial.is_file() {
454 return Some(idx_partial);
455 }
456 let idx_plain = nm_dir.join(bare).join(format!("index.{ext}"));
457 if idx_plain.is_file() {
458 return Some(idx_plain);
459 }
460 }
461 let exact = nm_dir.join(bare);
464 if exact.is_file() {
465 return Some(exact);
466 }
467 None
468}
469
470pub(super) fn try_source_fallback(
482 resolved: &Path,
483 path_to_id: &FxHashMap<&Path, FileId>,
484) -> Option<FileId> {
485 let components: Vec<_> = resolved.components().collect();
486
487 let is_output_dir = |c: &std::path::Component| -> bool {
488 if let std::path::Component::Normal(s) = c
489 && let Some(name) = s.to_str()
490 {
491 return OUTPUT_DIRS.contains(&name);
492 }
493 false
494 };
495
496 let last_output_pos = components.iter().rposition(&is_output_dir)?;
500
501 let mut first_output_pos = last_output_pos;
504 while first_output_pos > 0 && is_output_dir(&components[first_output_pos - 1]) {
505 first_output_pos -= 1;
506 }
507
508 let prefix: PathBuf = components[..first_output_pos].iter().collect();
510
511 let suffix: PathBuf = components[last_output_pos + 1..].iter().collect();
513 suffix.file_stem()?; for ext in SOURCE_EXTS {
517 let source_candidate = prefix.join("src").join(suffix.with_extension(ext));
518 if let Some(&file_id) = path_to_id.get(source_candidate.as_path()) {
519 return Some(file_id);
520 }
521 }
522
523 None
524}
525
526pub(super) fn try_package_imports_fallback(
532 ctx: &ResolveContext<'_>,
533 from_file: &Path,
534 specifier: &str,
535) -> Option<ResolveResult> {
536 if !specifier.starts_with('#') {
537 return None;
538 }
539 let manifest = nearest_package_manifest(ctx.package_manifests, from_file)?;
540 let imports = manifest.package_json.imports.as_ref()?;
541 let PackageMapTarget::Targets(targets) =
542 package_map_target(imports, specifier, ctx.condition_names)
543 else {
544 return None;
545 };
546 let source_subpath = package_import_source_subpath(manifest, specifier);
547 resolve_package_import_targets(ctx, manifest, &targets, source_subpath.as_deref()).map(
548 |target| match target {
549 PackageImportTarget::Internal(file_id) => match &manifest.name {
550 Some(package_name) => ResolveResult::InternalPackageModule {
551 file_id,
552 package_name: package_name.clone(),
553 },
554 None => ResolveResult::InternalModule(file_id),
555 },
556 PackageImportTarget::ExternalPackage(package_name) => {
557 ResolveResult::NpmPackage(package_name)
558 }
559 },
560 )
561}
562
563#[derive(Debug, Clone, PartialEq, Eq)]
564enum PackageMapTarget {
565 NoMatch,
566 Blocked,
567 Targets(Vec<String>),
568}
569
570enum PackageImportTarget {
571 Internal(FileId),
572 ExternalPackage(String),
573}
574
575fn package_map_match_value(
576 value: &Value,
577 condition_names: &[String],
578 capture: Option<&str>,
579) -> PackageMapTarget {
580 resolve_package_map_value(value, condition_names, capture)
581 .filter(|targets| !targets.is_empty())
582 .map_or(PackageMapTarget::Blocked, PackageMapTarget::Targets)
583}
584
585fn package_map_target(
586 map: &Value,
587 specifier_key: &str,
588 condition_names: &[String],
589) -> PackageMapTarget {
590 let Some(obj) = map.as_object() else {
591 if specifier_key == "." {
592 return package_map_match_value(map, condition_names, None);
593 }
594 return PackageMapTarget::NoMatch;
595 };
596
597 let has_subpath_keys = obj
598 .keys()
599 .any(|key| key == "." || key.starts_with("./") || key.starts_with('#'));
600 if !has_subpath_keys {
601 if specifier_key == "." {
602 return package_map_match_value(map, condition_names, None);
603 }
604 return PackageMapTarget::NoMatch;
605 }
606
607 if let Some(value) = obj.get(specifier_key) {
608 return package_map_match_value(value, condition_names, None);
609 }
610
611 let mut patterns: Vec<(&str, &Value, String)> = obj
612 .iter()
613 .filter_map(|(pattern, value)| {
614 package_map_pattern_capture(pattern, specifier_key)
615 .map(|capture| (pattern.as_str(), value, capture))
616 })
617 .collect();
618 patterns.sort_by(|(left, _, _), (right, _, _)| {
619 package_map_pattern_specificity(right).cmp(&package_map_pattern_specificity(left))
620 });
621
622 patterns
623 .first()
624 .map_or(PackageMapTarget::NoMatch, |(_, value, capture)| {
625 package_map_match_value(value, condition_names, Some(capture))
626 })
627}
628
629fn resolve_package_map_value(
630 value: &Value,
631 condition_names: &[String],
632 capture: Option<&str>,
633) -> Option<Vec<String>> {
634 match value {
635 Value::String(target) => Some(vec![match capture {
636 Some(capture) => target.replace('*', capture),
637 None => target.clone(),
638 }]),
639 Value::Object(map) => {
640 for (condition, value) in map {
641 if (condition == "default"
642 || condition_names
643 .iter()
644 .any(|active_condition| active_condition == condition))
645 && let Some(targets) =
646 resolve_package_map_value(value, condition_names, capture)
647 {
648 return Some(targets);
649 }
650 }
651 None
652 }
653 Value::Array(values) => {
654 let targets: Vec<String> = values
655 .iter()
656 .filter_map(|value| resolve_package_map_value(value, condition_names, capture))
657 .flatten()
658 .collect();
659 (!targets.is_empty()).then_some(targets)
660 }
661 Value::Bool(_) | Value::Null | Value::Number(_) => None,
662 }
663}
664
665fn package_map_pattern_capture(pattern: &str, specifier: &str) -> Option<String> {
666 let star = pattern.find('*')?;
667 if pattern[star + 1..].contains('*') {
668 return None;
669 }
670 let (prefix, suffix_with_star) = pattern.split_at(star);
671 let suffix = &suffix_with_star[1..];
672 let captured = specifier.strip_prefix(prefix)?.strip_suffix(suffix)?;
673 Some(captured.to_string())
674}
675
676fn package_map_pattern_specificity(pattern: &str) -> (usize, usize) {
677 let star = pattern.find('*').unwrap_or(pattern.len());
678 (star, pattern.len())
679}
680
681fn package_import_source_subpath(
682 manifest: &PackageManifestInfo,
683 specifier: &str,
684) -> Option<PathBuf> {
685 let stripped = specifier.strip_prefix('#')?;
686 let without_package_name = manifest
687 .name
688 .as_deref()
689 .and_then(|name| stripped.strip_prefix(name))
690 .and_then(|rest| rest.strip_prefix('/'))
691 .unwrap_or(stripped);
692 if without_package_name.is_empty() {
693 None
694 } else {
695 Some(PathBuf::from(without_package_name))
696 }
697}
698
699fn nearest_package_manifest<'a>(
700 manifests: &'a [PackageManifestInfo],
701 from_file: &Path,
702) -> Option<&'a PackageManifestInfo> {
703 manifests
704 .iter()
705 .filter(|manifest| {
706 from_file.starts_with(&manifest.root) || from_file.starts_with(&manifest.canonical_root)
707 })
708 .max_by_key(|manifest| manifest.root.components().count())
709}
710
711fn find_package_manifest<'a>(
712 manifests: &'a [PackageManifestInfo],
713 package_name: &str,
714) -> Option<&'a PackageManifestInfo> {
715 manifests
716 .iter()
717 .find(|manifest| manifest.name.as_deref() == Some(package_name))
718}
719
720fn resolve_package_map_target(
721 ctx: &ResolveContext<'_>,
722 manifest: &PackageManifestInfo,
723 target: &str,
724 source_subpath: Option<&Path>,
725) -> Option<FileId> {
726 let target = target.strip_prefix("./")?;
727 if target.starts_with("../") || target.starts_with('/') {
728 return None;
729 }
730 let target_path = manifest.root.join(target);
731
732 lookup_internal_file_id(ctx, &target_path)
733 .or_else(|| try_source_fallback(&target_path, ctx.raw_path_to_id))
734 .or_else(|| try_source_fallback(&target_path, ctx.path_to_id))
735 .or_else(|| source_subpath.and_then(|subpath| try_source_subpath(ctx, manifest, subpath)))
736}
737
738fn resolve_package_map_targets(
739 ctx: &ResolveContext<'_>,
740 manifest: &PackageManifestInfo,
741 targets: &[String],
742 source_subpath: Option<&Path>,
743) -> Option<FileId> {
744 targets
745 .iter()
746 .find_map(|target| resolve_package_map_target(ctx, manifest, target, source_subpath))
747}
748
749fn resolve_package_import_targets(
750 ctx: &ResolveContext<'_>,
751 manifest: &PackageManifestInfo,
752 targets: &[String],
753 source_subpath: Option<&Path>,
754) -> Option<PackageImportTarget> {
755 targets.iter().find_map(|target| {
756 resolve_package_map_target(ctx, manifest, target, source_subpath)
757 .map(PackageImportTarget::Internal)
758 .or_else(|| {
759 package_import_external_target(target).map(PackageImportTarget::ExternalPackage)
760 })
761 })
762}
763
764fn package_import_external_target(target: &str) -> Option<String> {
765 if is_bare_specifier(target) && is_valid_package_name(target) {
766 Some(extract_package_name(target))
767 } else {
768 None
769 }
770}
771
772fn try_source_subpath(
773 ctx: &ResolveContext<'_>,
774 manifest: &PackageManifestInfo,
775 subpath: &Path,
776) -> Option<FileId> {
777 if subpath.as_os_str().is_empty()
778 && let Some(source) = manifest.package_json.source.as_deref()
779 && let Some(source) = source.strip_prefix("./")
780 && let Some(file_id) = lookup_internal_file_id(ctx, &manifest.root.join(source))
781 {
782 return Some(file_id);
783 }
784
785 for ext in SOURCE_EXTS {
786 let direct = if subpath.as_os_str().is_empty() {
787 manifest.root.join("src").join(format!("index.{ext}"))
788 } else {
789 manifest.root.join("src").join(subpath).with_extension(ext)
790 };
791 if let Some(file_id) = lookup_internal_file_id(ctx, &direct) {
792 return Some(file_id);
793 }
794
795 if !subpath.as_os_str().is_empty() {
796 let index = manifest
797 .root
798 .join("src")
799 .join(subpath)
800 .join(format!("index.{ext}"));
801 if let Some(file_id) = lookup_internal_file_id(ctx, &index) {
802 return Some(file_id);
803 }
804 }
805
806 if subpath.as_os_str().is_empty() {
807 let root_index = manifest.root.join(format!("index.{ext}"));
808 if let Some(file_id) = lookup_internal_file_id(ctx, &root_index) {
809 return Some(file_id);
810 }
811 }
812 }
813
814 None
815}
816
817fn lookup_internal_file_id(ctx: &ResolveContext<'_>, candidate: &Path) -> Option<FileId> {
818 if let Some(&file_id) = ctx.raw_path_to_id.get(candidate) {
819 return Some(file_id);
820 }
821 if let Some(&file_id) = ctx.path_to_id.get(candidate) {
822 return Some(file_id);
823 }
824 #[cfg(not(miri))]
825 if let Ok(canonical) = dunce::canonicalize(candidate) {
826 if let Some(&file_id) = ctx.path_to_id.get(canonical.as_path()) {
827 return Some(file_id);
828 }
829 if let Some(fallback) = ctx.canonical_fallback
830 && let Some(file_id) = fallback.get(&canonical)
831 {
832 return Some(file_id);
833 }
834 }
835 None
836}
837
838pub fn extract_package_name_from_node_modules_path(path: &Path) -> Option<String> {
844 let components: Vec<&str> = path
845 .components()
846 .filter_map(|c| match c {
847 std::path::Component::Normal(s) => s.to_str(),
848 _ => None,
849 })
850 .collect();
851
852 let nm_idx = components.iter().rposition(|&c| c == "node_modules")?;
854
855 let after = &components[nm_idx + 1..];
856 if after.is_empty() {
857 return None;
858 }
859
860 if after[0].starts_with('@') {
861 if after.len() >= 2 {
863 Some(format!("{}/{}", after[0], after[1]))
864 } else {
865 Some(after[0].to_string())
866 }
867 } else {
868 Some(after[0].to_string())
869 }
870}
871
872pub(super) fn try_pnpm_workspace_fallback(
881 path: &Path,
882 path_to_id: &FxHashMap<&Path, FileId>,
883 workspace_roots: &FxHashMap<&str, &Path>,
884) -> Option<FileId> {
885 let components: Vec<&str> = path
887 .components()
888 .filter_map(|c| match c {
889 std::path::Component::Normal(s) => s.to_str(),
890 _ => None,
891 })
892 .collect();
893
894 let pnpm_idx = components.iter().position(|&c| c == ".pnpm")?;
896
897 let after_pnpm = &components[pnpm_idx + 1..];
900
901 let inner_nm_idx = after_pnpm.iter().position(|&c| c == "node_modules")?;
903 let after_inner_nm = &after_pnpm[inner_nm_idx + 1..];
904
905 if after_inner_nm.is_empty() {
906 return None;
907 }
908
909 let (pkg_name, pkg_name_components) = if after_inner_nm[0].starts_with('@') {
911 if after_inner_nm.len() >= 2 {
912 (format!("{}/{}", after_inner_nm[0], after_inner_nm[1]), 2)
913 } else {
914 return None;
915 }
916 } else {
917 (after_inner_nm[0].to_string(), 1)
918 };
919
920 let ws_root = workspace_roots.get(pkg_name.as_str())?;
922
923 let relative_parts = &after_inner_nm[pkg_name_components..];
925 if relative_parts.is_empty() {
926 return None;
927 }
928
929 let relative_path: PathBuf = relative_parts.iter().collect();
930
931 let direct = ws_root.join(&relative_path);
933 if let Some(&file_id) = path_to_id.get(direct.as_path()) {
934 return Some(file_id);
935 }
936
937 try_source_fallback(&direct, path_to_id)
939}
940
941pub(super) fn try_workspace_package_fallback(
964 ctx: &ResolveContext<'_>,
965 specifier: &str,
966) -> Option<ResolveResult> {
967 if !super::path_info::is_bare_specifier(specifier) {
969 return None;
970 }
971 let pkg_name = super::path_info::extract_package_name(specifier);
972
973 let subpath = specifier
976 .strip_prefix(pkg_name.as_str())
977 .and_then(|s| s.strip_prefix('/'))
978 .unwrap_or("");
979 let source_subpath = PathBuf::from(subpath);
980
981 if let Some(manifest) = find_package_manifest(ctx.package_manifests, pkg_name.as_str()) {
982 let export_key = if subpath.is_empty() {
983 ".".to_string()
984 } else {
985 format!("./{subpath}")
986 };
987 if let Some(exports) = manifest.package_json.exports.as_ref() {
988 match package_map_target(exports, &export_key, ctx.condition_names) {
989 PackageMapTarget::Targets(targets) => {
990 if let Some(file_id) = resolve_package_map_targets(
991 ctx,
992 manifest,
993 &targets,
994 Some(source_subpath.as_path()),
995 ) {
996 return Some(ResolveResult::InternalPackageModule {
997 file_id,
998 package_name: pkg_name,
999 });
1000 }
1001 }
1002 PackageMapTarget::NoMatch | PackageMapTarget::Blocked => return None,
1003 }
1004 } else if let Some(file_id) = try_source_subpath(ctx, manifest, source_subpath.as_path()) {
1005 return Some(ResolveResult::InternalPackageModule {
1006 file_id,
1007 package_name: pkg_name,
1008 });
1009 }
1010 }
1011
1012 let (ws_root, package_name) =
1013 if let Some(manifest) = find_package_manifest(ctx.package_manifests, pkg_name.as_str()) {
1014 (manifest.root.as_path(), pkg_name)
1015 } else {
1016 (*ctx.workspace_roots.get(pkg_name.as_str())?, pkg_name)
1017 };
1018
1019 let root_file = ws_root.join("__fallow_ws_self_resolve__");
1022 let rel_spec = if subpath.is_empty() {
1023 "./".to_string()
1024 } else {
1025 format!("./{subpath}")
1026 };
1027
1028 let resolved = ctx.resolver.resolve_file(&root_file, &rel_spec).ok()?;
1029 let resolved_path = resolved.path();
1030
1031 if let Some(&file_id) = ctx.raw_path_to_id.get(resolved_path) {
1032 return Some(ResolveResult::InternalPackageModule {
1033 file_id,
1034 package_name,
1035 });
1036 }
1037 if let Ok(canonical) = dunce::canonicalize(resolved_path) {
1038 if let Some(&file_id) = ctx.path_to_id.get(canonical.as_path()) {
1039 return Some(ResolveResult::InternalPackageModule {
1040 file_id,
1041 package_name,
1042 });
1043 }
1044 if let Some(fallback) = ctx.canonical_fallback
1045 && let Some(file_id) = fallback.get(&canonical)
1046 {
1047 return Some(ResolveResult::InternalPackageModule {
1048 file_id,
1049 package_name,
1050 });
1051 }
1052 if let Some(file_id) = try_source_fallback(&canonical, ctx.path_to_id) {
1053 return Some(ResolveResult::InternalPackageModule {
1054 file_id,
1055 package_name,
1056 });
1057 }
1058 }
1059 None
1060}
1061
1062pub(super) fn make_glob_from_pattern(
1064 pattern: &fallow_types::extract::DynamicImportPattern,
1065) -> String {
1066 if pattern.prefix.contains('*') || pattern.prefix.contains('{') {
1068 return pattern.prefix.clone();
1069 }
1070 pattern.suffix.as_ref().map_or_else(
1071 || format!("{}*", pattern.prefix),
1072 |suffix| format!("{}*{}", pattern.prefix, suffix),
1073 )
1074}
1075
1076#[cfg(test)]
1077mod tests {
1078 use super::*;
1079 use rustc_hash::FxHashSet;
1080
1081 fn with_package_map_ctx(
1082 root: PathBuf,
1083 name: Option<&str>,
1084 package_json: fallow_config::PackageJson,
1085 raw_files: &[(PathBuf, FileId)],
1086 f: impl FnOnce(&ResolveContext<'_>, &PackageManifestInfo, &Path),
1087 ) {
1088 let manifest = PackageManifestInfo {
1089 root: root.clone(),
1090 canonical_root: root,
1091 name: name.map(str::to_string),
1092 package_json,
1093 };
1094 let manifests = [manifest];
1095 let mut raw_path_to_id = FxHashMap::default();
1096 for (path, file_id) in raw_files {
1097 raw_path_to_id.insert(path.as_path(), *file_id);
1098 }
1099 let path_to_id: FxHashMap<&Path, FileId> = FxHashMap::default();
1100 let workspace_roots: FxHashMap<&str, &Path> = FxHashMap::default();
1101 let condition_names = conditions();
1102 let resolver = oxc_resolver::Resolver::new(oxc_resolver::ResolveOptions::default());
1103 let tsconfig_warned = std::sync::Mutex::new(FxHashSet::default());
1104 let ctx = ResolveContext {
1105 resolver: &resolver,
1106 style_resolver: &resolver,
1107 extensions: &[],
1108 path_to_id: &path_to_id,
1109 raw_path_to_id: &raw_path_to_id,
1110 workspace_roots: &workspace_roots,
1111 package_manifests: &manifests,
1112 condition_names: &condition_names,
1113 path_aliases: &[],
1114 scss_include_paths: &[],
1115 static_dir_mappings: &[],
1116 root: &manifests[0].root,
1117 canonical_fallback: None,
1118 tsconfig_warned: &tsconfig_warned,
1119 };
1120
1121 f(&ctx, &manifests[0], &manifests[0].root);
1122 }
1123
1124 #[test]
1125 fn alias_match_remainder_exact_key() {
1126 assert_eq!(alias_match_remainder("vscode", "vscode"), Some(""));
1128 assert_eq!(alias_match_remainder("@scope/sdk", "@scope/sdk"), Some(""));
1129 }
1130
1131 #[test]
1132 fn alias_match_remainder_slash_continuation() {
1133 assert_eq!(
1135 alias_match_remainder("@scope/sdk/sub", "@scope/sdk"),
1136 Some("/sub")
1137 );
1138 assert_eq!(alias_match_remainder("@/foo", "@/"), Some("foo"));
1139 assert_eq!(
1140 alias_match_remainder("~/components/x", "~/"),
1141 Some("components/x")
1142 );
1143 assert_eq!(alias_match_remainder("$lib/util", "$lib/"), Some("util"));
1144 }
1145
1146 #[test]
1147 fn alias_match_remainder_rejects_prefix_collision() {
1148 assert_eq!(
1151 alias_match_remainder("@scope/sdk-extra", "@scope/sdk"),
1152 None
1153 );
1154 assert_eq!(
1155 alias_match_remainder("vscode-languageserver", "vscode"),
1156 None
1157 );
1158 assert_eq!(alias_match_remainder("#shared-utils", "#shared"), None);
1159 }
1160
1161 #[test]
1162 fn alias_match_remainder_non_match() {
1163 assert_eq!(alias_match_remainder("react", "vscode"), None);
1164 }
1165
1166 #[test]
1167 fn test_extract_package_name_from_node_modules_path_regular() {
1168 let path = PathBuf::from("/project/node_modules/react/index.js");
1169 assert_eq!(
1170 extract_package_name_from_node_modules_path(&path),
1171 Some("react".to_string())
1172 );
1173 }
1174
1175 #[test]
1176 fn test_extract_package_name_from_node_modules_path_scoped() {
1177 let path = PathBuf::from("/project/node_modules/@babel/core/lib/index.js");
1178 assert_eq!(
1179 extract_package_name_from_node_modules_path(&path),
1180 Some("@babel/core".to_string())
1181 );
1182 }
1183
1184 #[test]
1185 fn test_extract_package_name_from_node_modules_path_nested() {
1186 let path = PathBuf::from("/project/node_modules/pkg-a/node_modules/pkg-b/dist/index.js");
1188 assert_eq!(
1189 extract_package_name_from_node_modules_path(&path),
1190 Some("pkg-b".to_string())
1191 );
1192 }
1193
1194 #[test]
1195 fn test_extract_package_name_from_node_modules_path_deep_subpath() {
1196 let path = PathBuf::from("/project/node_modules/react-dom/cjs/react-dom.production.min.js");
1197 assert_eq!(
1198 extract_package_name_from_node_modules_path(&path),
1199 Some("react-dom".to_string())
1200 );
1201 }
1202
1203 #[test]
1204 fn test_extract_package_name_from_node_modules_path_no_node_modules() {
1205 let path = PathBuf::from("/project/src/components/Button.tsx");
1206 assert_eq!(extract_package_name_from_node_modules_path(&path), None);
1207 }
1208
1209 #[test]
1210 fn test_extract_package_name_from_node_modules_path_just_node_modules() {
1211 let path = PathBuf::from("/project/node_modules");
1212 assert_eq!(extract_package_name_from_node_modules_path(&path), None);
1213 }
1214
1215 #[test]
1216 fn test_extract_package_name_from_node_modules_path_scoped_only_scope() {
1217 let path = PathBuf::from("/project/node_modules/@scope");
1219 assert_eq!(
1220 extract_package_name_from_node_modules_path(&path),
1221 Some("@scope".to_string())
1222 );
1223 }
1224
1225 #[test]
1226 fn test_resolve_specifier_node_modules_returns_npm_package() {
1227 let path =
1233 PathBuf::from("/project/node_modules/styled-components/dist/styled-components.esm.js");
1234 assert_eq!(
1235 extract_package_name_from_node_modules_path(&path),
1236 Some("styled-components".to_string())
1237 );
1238
1239 let path = PathBuf::from("/project/node_modules/next/dist/server/next.js");
1240 assert_eq!(
1241 extract_package_name_from_node_modules_path(&path),
1242 Some("next".to_string())
1243 );
1244 }
1245
1246 #[test]
1247 fn test_try_source_fallback_dist_to_src() {
1248 let src_path = PathBuf::from("/project/packages/ui/src/utils.ts");
1249 let mut path_to_id = FxHashMap::default();
1250 path_to_id.insert(src_path.as_path(), FileId(0));
1251
1252 let dist_path = PathBuf::from("/project/packages/ui/dist/utils.js");
1253 assert_eq!(
1254 try_source_fallback(&dist_path, &path_to_id),
1255 Some(FileId(0)),
1256 "dist/utils.js should fall back to src/utils.ts"
1257 );
1258 }
1259
1260 #[test]
1261 fn test_try_source_fallback_build_to_src() {
1262 let src_path = PathBuf::from("/project/packages/core/src/index.tsx");
1263 let mut path_to_id = FxHashMap::default();
1264 path_to_id.insert(src_path.as_path(), FileId(1));
1265
1266 let build_path = PathBuf::from("/project/packages/core/build/index.js");
1267 assert_eq!(
1268 try_source_fallback(&build_path, &path_to_id),
1269 Some(FileId(1)),
1270 "build/index.js should fall back to src/index.tsx"
1271 );
1272 }
1273
1274 #[test]
1275 fn test_try_source_fallback_no_match() {
1276 let path_to_id: FxHashMap<&Path, FileId> = FxHashMap::default();
1277
1278 let dist_path = PathBuf::from("/project/packages/ui/dist/utils.js");
1279 assert_eq!(
1280 try_source_fallback(&dist_path, &path_to_id),
1281 None,
1282 "should return None when no source file exists"
1283 );
1284 }
1285
1286 #[test]
1287 fn test_try_source_fallback_non_output_dir() {
1288 let src_path = PathBuf::from("/project/packages/ui/src/utils.ts");
1289 let mut path_to_id = FxHashMap::default();
1290 path_to_id.insert(src_path.as_path(), FileId(0));
1291
1292 let normal_path = PathBuf::from("/project/packages/ui/scripts/utils.js");
1294 assert_eq!(
1295 try_source_fallback(&normal_path, &path_to_id),
1296 None,
1297 "non-output directory path should not trigger fallback"
1298 );
1299 }
1300
1301 #[test]
1302 fn test_try_source_fallback_nested_path() {
1303 let src_path = PathBuf::from("/project/packages/ui/src/components/Button.ts");
1304 let mut path_to_id = FxHashMap::default();
1305 path_to_id.insert(src_path.as_path(), FileId(2));
1306
1307 let dist_path = PathBuf::from("/project/packages/ui/dist/components/Button.js");
1308 assert_eq!(
1309 try_source_fallback(&dist_path, &path_to_id),
1310 Some(FileId(2)),
1311 "nested dist path should fall back to nested src path"
1312 );
1313 }
1314
1315 #[test]
1316 fn test_try_source_fallback_nested_dist_esm() {
1317 let src_path = PathBuf::from("/project/packages/ui/src/utils.ts");
1318 let mut path_to_id = FxHashMap::default();
1319 path_to_id.insert(src_path.as_path(), FileId(0));
1320
1321 let dist_path = PathBuf::from("/project/packages/ui/dist/esm/utils.mjs");
1322 assert_eq!(
1323 try_source_fallback(&dist_path, &path_to_id),
1324 Some(FileId(0)),
1325 "dist/esm/utils.mjs should fall back to src/utils.ts"
1326 );
1327 }
1328
1329 #[test]
1330 fn test_try_source_fallback_nested_build_cjs() {
1331 let src_path = PathBuf::from("/project/packages/core/src/index.ts");
1332 let mut path_to_id = FxHashMap::default();
1333 path_to_id.insert(src_path.as_path(), FileId(1));
1334
1335 let build_path = PathBuf::from("/project/packages/core/build/cjs/index.cjs");
1336 assert_eq!(
1337 try_source_fallback(&build_path, &path_to_id),
1338 Some(FileId(1)),
1339 "build/cjs/index.cjs should fall back to src/index.ts"
1340 );
1341 }
1342
1343 #[test]
1344 fn test_try_source_fallback_nested_dist_esm_deep_path() {
1345 let src_path = PathBuf::from("/project/packages/ui/src/components/Button.ts");
1346 let mut path_to_id = FxHashMap::default();
1347 path_to_id.insert(src_path.as_path(), FileId(2));
1348
1349 let dist_path = PathBuf::from("/project/packages/ui/dist/esm/components/Button.mjs");
1350 assert_eq!(
1351 try_source_fallback(&dist_path, &path_to_id),
1352 Some(FileId(2)),
1353 "dist/esm/components/Button.mjs should fall back to src/components/Button.ts"
1354 );
1355 }
1356
1357 #[test]
1358 fn test_try_source_fallback_triple_nested_output_dirs() {
1359 let src_path = PathBuf::from("/project/packages/ui/src/utils.ts");
1360 let mut path_to_id = FxHashMap::default();
1361 path_to_id.insert(src_path.as_path(), FileId(0));
1362
1363 let dist_path = PathBuf::from("/project/packages/ui/out/dist/esm/utils.mjs");
1364 assert_eq!(
1365 try_source_fallback(&dist_path, &path_to_id),
1366 Some(FileId(0)),
1367 "out/dist/esm/utils.mjs should fall back to src/utils.ts"
1368 );
1369 }
1370
1371 #[test]
1372 fn test_try_source_fallback_parent_dir_named_build() {
1373 let src_path = PathBuf::from("/home/user/build/my-project/src/utils.ts");
1374 let mut path_to_id = FxHashMap::default();
1375 path_to_id.insert(src_path.as_path(), FileId(0));
1376
1377 let dist_path = PathBuf::from("/home/user/build/my-project/dist/utils.js");
1378 assert_eq!(
1379 try_source_fallback(&dist_path, &path_to_id),
1380 Some(FileId(0)),
1381 "should resolve dist/ within project, not match parent 'build' dir"
1382 );
1383 }
1384
1385 #[test]
1386 fn package_map_exact_entry_beats_pattern_entry() {
1387 let map = serde_json::json!({
1388 "#nitro/runtime/task": "./dist/special/task.mjs",
1389 "#nitro/runtime/*": "./dist/runtime/internal/*.mjs"
1390 });
1391 assert_eq!(
1392 package_map_target(&map, "#nitro/runtime/task", &conditions()),
1393 PackageMapTarget::Targets(vec!["./dist/special/task.mjs".to_string()])
1394 );
1395 }
1396
1397 #[test]
1398 fn package_map_wildcard_substitutes_capture() {
1399 let map = serde_json::json!({
1400 "#nitro/runtime/*": "./dist/runtime/internal/*.mjs"
1401 });
1402 assert_eq!(
1403 package_map_target(&map, "#nitro/runtime/task", &conditions()),
1404 PackageMapTarget::Targets(vec!["./dist/runtime/internal/task.mjs".to_string()])
1405 );
1406 }
1407
1408 #[test]
1409 fn package_map_exact_entry_with_no_target_blocks_pattern_entry() {
1410 let map = serde_json::json!({
1411 "#nitro/runtime/task": null,
1412 "#nitro/runtime/*": "./dist/runtime/internal/*.mjs"
1413 });
1414 assert_eq!(
1415 package_map_target(&map, "#nitro/runtime/task", &conditions()),
1416 PackageMapTarget::Blocked
1417 );
1418 }
1419
1420 #[test]
1421 fn package_map_best_pattern_with_no_target_blocks_broader_pattern() {
1422 let map = serde_json::json!({
1423 "#nitro/runtime/internal/*": null,
1424 "#nitro/runtime/*": "./dist/runtime/*.mjs"
1425 });
1426 assert_eq!(
1427 package_map_target(&map, "#nitro/runtime/internal/task", &conditions()),
1428 PackageMapTarget::Blocked
1429 );
1430 }
1431
1432 #[test]
1433 fn package_map_unmatched_subpath_is_not_a_target() {
1434 let map = serde_json::json!({
1435 "./query": "./dist/query/index.js"
1436 });
1437 assert_eq!(
1438 package_map_target(&map, "./private", &conditions()),
1439 PackageMapTarget::NoMatch
1440 );
1441 }
1442
1443 #[test]
1444 fn package_map_nested_conditions_follow_manifest_order() {
1445 let map = serde_json::json!({
1446 "./query/react": {
1447 "types": "./dist/query/react/index.d.ts",
1448 "import": {
1449 "development": "./src/query/react/index.ts",
1450 "default": "./dist/query/react/index.js"
1451 },
1452 "default": "./dist/query/react/index.cjs"
1453 }
1454 });
1455 assert_eq!(
1456 package_map_target(&map, "./query/react", &conditions()),
1457 PackageMapTarget::Targets(vec!["./dist/query/react/index.d.ts".to_string()])
1458 );
1459 }
1460
1461 #[test]
1462 fn package_map_import_before_types_selects_runtime_branch() {
1463 let map = serde_json::json!({
1464 ".": {
1465 "import": "./dist/index.js",
1466 "types": "./dist/index.d.ts"
1467 }
1468 });
1469 assert_eq!(
1470 package_map_target(&map, ".", &conditions()),
1471 PackageMapTarget::Targets(vec!["./dist/index.js".to_string()])
1472 );
1473 }
1474
1475 #[test]
1476 fn package_map_condition_order_follows_manifest_order() {
1477 let map = serde_json::json!({
1478 ".": {
1479 "node": "./dist/node.js",
1480 "import": "./dist/index.js"
1481 }
1482 });
1483 assert_eq!(
1484 package_map_target(&map, ".", &conditions()),
1485 PackageMapTarget::Targets(vec!["./dist/node.js".to_string()])
1486 );
1487 }
1488
1489 #[test]
1490 fn package_map_arrays_preserve_fallback_order() {
1491 let map = serde_json::json!({
1492 "#array": ["./dist/missing.js", "./src/array.ts"],
1493 "#null": null,
1494 "#false": false
1495 });
1496 assert_eq!(
1497 package_map_target(&map, "#array", &conditions()),
1498 PackageMapTarget::Targets(vec![
1499 "./dist/missing.js".to_string(),
1500 "./src/array.ts".to_string()
1501 ])
1502 );
1503 assert_eq!(
1504 package_map_target(&map, "#null", &conditions()),
1505 PackageMapTarget::Blocked
1506 );
1507 assert_eq!(
1508 package_map_target(&map, "#false", &conditions()),
1509 PackageMapTarget::Blocked
1510 );
1511 }
1512
1513 #[test]
1514 fn package_map_non_relative_target_does_not_trigger_source_fallback() {
1515 with_package_map_ctx(
1516 PathBuf::from("/project"),
1517 Some("pkg"),
1518 fallow_config::PackageJson::default(),
1519 &[],
1520 |ctx, manifest, _| {
1521 assert!(resolve_package_map_target(ctx, manifest, "lodash", None).is_none());
1522 assert!(
1523 resolve_package_map_target(ctx, manifest, "../dist/index.js", None).is_none()
1524 );
1525 },
1526 );
1527 }
1528
1529 #[test]
1530 fn package_map_targets_use_first_reachable_target() {
1531 let root = PathBuf::from("/project");
1532 let src_path = root.join("src/feature.ts");
1533 let targets = vec![
1534 "./dist/missing.js".to_string(),
1535 "./src/feature.ts".to_string(),
1536 ];
1537
1538 with_package_map_ctx(
1539 root,
1540 Some("pkg"),
1541 fallow_config::PackageJson::default(),
1542 &[(src_path, FileId(9))],
1543 |ctx, manifest, _| {
1544 assert_eq!(
1545 resolve_package_map_targets(ctx, manifest, &targets, None),
1546 Some(FileId(9))
1547 );
1548 },
1549 );
1550 }
1551
1552 #[test]
1553 fn package_imports_fallback_supports_external_package_targets() {
1554 let root = PathBuf::from("/project");
1555 with_package_map_ctx(
1556 root,
1557 Some("pkg"),
1558 fallow_config::PackageJson {
1559 imports: Some(serde_json::json!({
1560 "#pad": "left-pad",
1561 "#scoped": "@scope/pkg/subpath"
1562 })),
1563 ..Default::default()
1564 },
1565 &[],
1566 |ctx, _, root| {
1567 let pad = try_package_imports_fallback(ctx, &root.join("src/index.ts"), "#pad");
1568 assert!(matches!(pad, Some(ResolveResult::NpmPackage(pkg)) if pkg == "left-pad"));
1569
1570 let scoped =
1571 try_package_imports_fallback(ctx, &root.join("src/index.ts"), "#scoped");
1572 assert!(
1573 matches!(scoped, Some(ResolveResult::NpmPackage(pkg)) if pkg == "@scope/pkg")
1574 );
1575 },
1576 );
1577 }
1578
1579 #[test]
1580 fn package_imports_fallback_supports_unnamed_packages() {
1581 let root = PathBuf::from("/project");
1582 let src_path = root.join("src/runtime/task.ts");
1583 with_package_map_ctx(
1584 root,
1585 None,
1586 fallow_config::PackageJson {
1587 imports: Some(serde_json::json!({
1588 "#runtime/*": "./dist/runtime/*.mjs"
1589 })),
1590 ..Default::default()
1591 },
1592 &[(src_path, FileId(7))],
1593 |ctx, _, root| {
1594 let result =
1595 try_package_imports_fallback(ctx, &root.join("src/index.ts"), "#runtime/task");
1596 assert!(matches!(
1597 result,
1598 Some(ResolveResult::InternalModule(FileId(7)))
1599 ));
1600 },
1601 );
1602 }
1603
1604 #[test]
1605 fn test_pnpm_store_path_extract_package_name() {
1606 let path =
1608 PathBuf::from("/project/node_modules/.pnpm/react@18.2.0/node_modules/react/index.js");
1609 assert_eq!(
1610 extract_package_name_from_node_modules_path(&path),
1611 Some("react".to_string())
1612 );
1613 }
1614
1615 #[test]
1616 fn test_pnpm_store_path_scoped_package() {
1617 let path = PathBuf::from(
1618 "/project/node_modules/.pnpm/@babel+core@7.24.0/node_modules/@babel/core/lib/index.js",
1619 );
1620 assert_eq!(
1621 extract_package_name_from_node_modules_path(&path),
1622 Some("@babel/core".to_string())
1623 );
1624 }
1625
1626 fn conditions() -> Vec<String> {
1627 vec![
1628 "development".to_string(),
1629 "import".to_string(),
1630 "require".to_string(),
1631 "default".to_string(),
1632 "types".to_string(),
1633 "node".to_string(),
1634 ]
1635 }
1636
1637 #[test]
1638 fn test_pnpm_store_path_with_peer_deps() {
1639 let path = PathBuf::from(
1640 "/project/node_modules/.pnpm/webpack@5.0.0_esbuild@0.19.0/node_modules/webpack/lib/index.js",
1641 );
1642 assert_eq!(
1643 extract_package_name_from_node_modules_path(&path),
1644 Some("webpack".to_string())
1645 );
1646 }
1647
1648 #[test]
1649 fn test_try_pnpm_workspace_fallback_dist_to_src() {
1650 let src_path = PathBuf::from("/project/packages/ui/src/utils.ts");
1651 let mut path_to_id = FxHashMap::default();
1652 path_to_id.insert(src_path.as_path(), FileId(0));
1653
1654 let mut workspace_roots = FxHashMap::default();
1655 let ws_root = PathBuf::from("/project/packages/ui");
1656 workspace_roots.insert("@myorg/ui", ws_root.as_path());
1657
1658 let pnpm_path = PathBuf::from(
1660 "/project/node_modules/.pnpm/@myorg+ui@1.0.0/node_modules/@myorg/ui/dist/utils.js",
1661 );
1662 assert_eq!(
1663 try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
1664 Some(FileId(0)),
1665 ".pnpm workspace path should fall back to src/utils.ts"
1666 );
1667 }
1668
1669 #[test]
1670 fn test_try_pnpm_workspace_fallback_direct_source() {
1671 let src_path = PathBuf::from("/project/packages/core/src/index.ts");
1672 let mut path_to_id = FxHashMap::default();
1673 path_to_id.insert(src_path.as_path(), FileId(1));
1674
1675 let mut workspace_roots = FxHashMap::default();
1676 let ws_root = PathBuf::from("/project/packages/core");
1677 workspace_roots.insert("@myorg/core", ws_root.as_path());
1678
1679 let pnpm_path = PathBuf::from(
1681 "/project/node_modules/.pnpm/@myorg+core@workspace/node_modules/@myorg/core/src/index.ts",
1682 );
1683 assert_eq!(
1684 try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
1685 Some(FileId(1)),
1686 ".pnpm workspace path with src/ should resolve directly"
1687 );
1688 }
1689
1690 #[test]
1691 fn test_try_pnpm_workspace_fallback_non_workspace_package() {
1692 let path_to_id: FxHashMap<&Path, FileId> = FxHashMap::default();
1693
1694 let mut workspace_roots = FxHashMap::default();
1695 let ws_root = PathBuf::from("/project/packages/ui");
1696 workspace_roots.insert("@myorg/ui", ws_root.as_path());
1697
1698 let pnpm_path =
1700 PathBuf::from("/project/node_modules/.pnpm/react@18.2.0/node_modules/react/index.js");
1701 assert_eq!(
1702 try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
1703 None,
1704 "non-workspace package in .pnpm should return None"
1705 );
1706 }
1707
1708 #[test]
1709 fn test_try_pnpm_workspace_fallback_unscoped_package() {
1710 let src_path = PathBuf::from("/project/packages/utils/src/index.ts");
1711 let mut path_to_id = FxHashMap::default();
1712 path_to_id.insert(src_path.as_path(), FileId(2));
1713
1714 let mut workspace_roots = FxHashMap::default();
1715 let ws_root = PathBuf::from("/project/packages/utils");
1716 workspace_roots.insert("my-utils", ws_root.as_path());
1717
1718 let pnpm_path = PathBuf::from(
1720 "/project/node_modules/.pnpm/my-utils@1.0.0/node_modules/my-utils/dist/index.js",
1721 );
1722 assert_eq!(
1723 try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
1724 Some(FileId(2)),
1725 "unscoped workspace package in .pnpm should resolve"
1726 );
1727 }
1728
1729 #[test]
1730 fn test_try_pnpm_workspace_fallback_nested_path() {
1731 let src_path = PathBuf::from("/project/packages/ui/src/components/Button.ts");
1732 let mut path_to_id = FxHashMap::default();
1733 path_to_id.insert(src_path.as_path(), FileId(3));
1734
1735 let mut workspace_roots = FxHashMap::default();
1736 let ws_root = PathBuf::from("/project/packages/ui");
1737 workspace_roots.insert("@myorg/ui", ws_root.as_path());
1738
1739 let pnpm_path = PathBuf::from(
1741 "/project/node_modules/.pnpm/@myorg+ui@1.0.0/node_modules/@myorg/ui/dist/components/Button.js",
1742 );
1743 assert_eq!(
1744 try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
1745 Some(FileId(3)),
1746 "nested .pnpm workspace path should resolve through source fallback"
1747 );
1748 }
1749
1750 #[test]
1751 fn test_try_pnpm_workspace_fallback_no_pnpm() {
1752 let path_to_id: FxHashMap<&Path, FileId> = FxHashMap::default();
1753 let workspace_roots: FxHashMap<&str, &Path> = FxHashMap::default();
1754
1755 let regular_path = PathBuf::from("/project/node_modules/react/index.js");
1757 assert_eq!(
1758 try_pnpm_workspace_fallback(®ular_path, &path_to_id, &workspace_roots),
1759 None,
1760 );
1761 }
1762
1763 #[test]
1764 fn test_try_pnpm_workspace_fallback_with_peer_deps() {
1765 let src_path = PathBuf::from("/project/packages/ui/src/index.ts");
1766 let mut path_to_id = FxHashMap::default();
1767 path_to_id.insert(src_path.as_path(), FileId(4));
1768
1769 let mut workspace_roots = FxHashMap::default();
1770 let ws_root = PathBuf::from("/project/packages/ui");
1771 workspace_roots.insert("@myorg/ui", ws_root.as_path());
1772
1773 let pnpm_path = PathBuf::from(
1775 "/project/node_modules/.pnpm/@myorg+ui@1.0.0_react@18.2.0/node_modules/@myorg/ui/dist/index.js",
1776 );
1777 assert_eq!(
1778 try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
1779 Some(FileId(4)),
1780 ".pnpm path with peer dep suffix should still resolve"
1781 );
1782 }
1783
1784 #[test]
1787 fn make_glob_prefix_only_no_suffix() {
1788 let pattern = fallow_types::extract::DynamicImportPattern {
1789 prefix: "./locales/".to_string(),
1790 suffix: None,
1791 span: oxc_span::Span::default(),
1792 };
1793 assert_eq!(make_glob_from_pattern(&pattern), "./locales/*");
1794 }
1795
1796 #[test]
1797 fn make_glob_prefix_with_suffix() {
1798 let pattern = fallow_types::extract::DynamicImportPattern {
1799 prefix: "./locales/".to_string(),
1800 suffix: Some(".json".to_string()),
1801 span: oxc_span::Span::default(),
1802 };
1803 assert_eq!(make_glob_from_pattern(&pattern), "./locales/*.json");
1804 }
1805
1806 #[test]
1807 fn make_glob_passthrough_star() {
1808 let pattern = fallow_types::extract::DynamicImportPattern {
1810 prefix: "./pages/**/*.tsx".to_string(),
1811 suffix: None,
1812 span: oxc_span::Span::default(),
1813 };
1814 assert_eq!(make_glob_from_pattern(&pattern), "./pages/**/*.tsx");
1815 }
1816
1817 #[test]
1818 fn make_glob_passthrough_brace() {
1819 let pattern = fallow_types::extract::DynamicImportPattern {
1820 prefix: "./i18n/{en,de,fr}.json".to_string(),
1821 suffix: None,
1822 span: oxc_span::Span::default(),
1823 };
1824 assert_eq!(make_glob_from_pattern(&pattern), "./i18n/{en,de,fr}.json");
1825 }
1826
1827 #[test]
1828 fn make_glob_empty_prefix_no_suffix() {
1829 let pattern = fallow_types::extract::DynamicImportPattern {
1830 prefix: String::new(),
1831 suffix: None,
1832 span: oxc_span::Span::default(),
1833 };
1834 assert_eq!(make_glob_from_pattern(&pattern), "*");
1835 }
1836
1837 #[test]
1838 fn make_glob_empty_prefix_with_suffix() {
1839 let pattern = fallow_types::extract::DynamicImportPattern {
1840 prefix: String::new(),
1841 suffix: Some(".ts".to_string()),
1842 span: oxc_span::Span::default(),
1843 };
1844 assert_eq!(make_glob_from_pattern(&pattern), "*.ts");
1845 }
1846
1847 #[test]
1850 fn make_glob_template_literal_prefix_only() {
1851 let pattern = fallow_types::extract::DynamicImportPattern {
1853 prefix: "./pages/".to_string(),
1854 suffix: None,
1855 span: oxc_span::Span::default(),
1856 };
1857 assert_eq!(make_glob_from_pattern(&pattern), "./pages/*");
1858 }
1859
1860 #[test]
1861 fn make_glob_template_literal_with_extension_suffix() {
1862 let pattern = fallow_types::extract::DynamicImportPattern {
1864 prefix: "./locales/".to_string(),
1865 suffix: Some(".json".to_string()),
1866 span: oxc_span::Span::default(),
1867 };
1868 assert_eq!(make_glob_from_pattern(&pattern), "./locales/*.json");
1869 }
1870
1871 #[test]
1872 fn make_glob_template_literal_deep_prefix() {
1873 let pattern = fallow_types::extract::DynamicImportPattern {
1876 prefix: "./modules/".to_string(),
1877 suffix: None,
1878 span: oxc_span::Span::default(),
1879 };
1880 assert_eq!(make_glob_from_pattern(&pattern), "./modules/*");
1881 }
1882
1883 #[test]
1884 fn make_glob_string_concat_prefix() {
1885 let pattern = fallow_types::extract::DynamicImportPattern {
1887 prefix: "./pages/".to_string(),
1888 suffix: None,
1889 span: oxc_span::Span::default(),
1890 };
1891 assert_eq!(make_glob_from_pattern(&pattern), "./pages/*");
1892 }
1893
1894 #[test]
1895 fn make_glob_string_concat_with_extension() {
1896 let pattern = fallow_types::extract::DynamicImportPattern {
1898 prefix: "./views/".to_string(),
1899 suffix: Some(".vue".to_string()),
1900 span: oxc_span::Span::default(),
1901 };
1902 assert_eq!(make_glob_from_pattern(&pattern), "./views/*.vue");
1903 }
1904
1905 #[test]
1908 fn make_glob_import_meta_glob_recursive() {
1909 let pattern = fallow_types::extract::DynamicImportPattern {
1911 prefix: "./components/**/*.vue".to_string(),
1912 suffix: None,
1913 span: oxc_span::Span::default(),
1914 };
1915 assert_eq!(
1916 make_glob_from_pattern(&pattern),
1917 "./components/**/*.vue",
1918 "import.meta.glob patterns with * should pass through as-is"
1919 );
1920 }
1921
1922 #[test]
1923 fn make_glob_import_meta_glob_brace_expansion() {
1924 let pattern = fallow_types::extract::DynamicImportPattern {
1926 prefix: "./plugins/{auth,analytics}.ts".to_string(),
1927 suffix: None,
1928 span: oxc_span::Span::default(),
1929 };
1930 assert_eq!(
1931 make_glob_from_pattern(&pattern),
1932 "./plugins/{auth,analytics}.ts",
1933 "import.meta.glob patterns with braces should pass through as-is"
1934 );
1935 }
1936
1937 #[test]
1938 fn make_glob_import_meta_glob_star_with_brace() {
1939 let pattern = fallow_types::extract::DynamicImportPattern {
1941 prefix: "./routes/**/*.{ts,tsx}".to_string(),
1942 suffix: None,
1943 span: oxc_span::Span::default(),
1944 };
1945 assert_eq!(
1946 make_glob_from_pattern(&pattern),
1947 "./routes/**/*.{ts,tsx}",
1948 "combined * and brace patterns should pass through"
1949 );
1950 }
1951
1952 #[test]
1953 fn make_glob_import_meta_glob_ignores_suffix_when_star_present() {
1954 let pattern = fallow_types::extract::DynamicImportPattern {
1956 prefix: "./*.ts".to_string(),
1957 suffix: Some(".extra".to_string()),
1958 span: oxc_span::Span::default(),
1959 };
1960 assert_eq!(
1961 make_glob_from_pattern(&pattern),
1962 "./*.ts",
1963 "when prefix has glob chars, suffix is ignored (prefix used as-is)"
1964 );
1965 }
1966
1967 #[test]
1970 fn make_glob_single_dot_prefix() {
1971 let pattern = fallow_types::extract::DynamicImportPattern {
1972 prefix: "./".to_string(),
1973 suffix: None,
1974 span: oxc_span::Span::default(),
1975 };
1976 assert_eq!(make_glob_from_pattern(&pattern), "./*");
1977 }
1978
1979 #[test]
1980 fn make_glob_prefix_without_trailing_slash() {
1981 let pattern = fallow_types::extract::DynamicImportPattern {
1983 prefix: "./config".to_string(),
1984 suffix: None,
1985 span: oxc_span::Span::default(),
1986 };
1987 assert_eq!(make_glob_from_pattern(&pattern), "./config*");
1988 }
1989
1990 #[test]
1991 fn make_glob_prefix_with_dotdot() {
1992 let pattern = fallow_types::extract::DynamicImportPattern {
1993 prefix: "../shared/".to_string(),
1994 suffix: Some(".ts".to_string()),
1995 span: oxc_span::Span::default(),
1996 };
1997 assert_eq!(make_glob_from_pattern(&pattern), "../shared/*.ts");
1998 }
1999
2000 #[test]
2003 fn test_extract_package_name_with_pnpm_plus_encoded_scope() {
2004 let path = PathBuf::from(
2007 "/project/node_modules/.pnpm/@mui+material@5.15.0/node_modules/@mui/material/index.js",
2008 );
2009 assert_eq!(
2010 extract_package_name_from_node_modules_path(&path),
2011 Some("@mui/material".to_string())
2012 );
2013 }
2014
2015 #[test]
2016 fn test_extract_package_name_windows_style_path() {
2017 let path = PathBuf::from("/project/node_modules/typescript/lib/tsc.js");
2019 assert_eq!(
2020 extract_package_name_from_node_modules_path(&path),
2021 Some("typescript".to_string())
2022 );
2023 }
2024
2025 #[test]
2028 fn test_try_source_fallback_out_dir() {
2029 let src_path = PathBuf::from("/project/packages/api/src/handler.ts");
2030 let mut path_to_id = FxHashMap::default();
2031 path_to_id.insert(src_path.as_path(), FileId(5));
2032
2033 let out_path = PathBuf::from("/project/packages/api/out/handler.js");
2034 assert_eq!(
2035 try_source_fallback(&out_path, &path_to_id),
2036 Some(FileId(5)),
2037 "out/handler.js should fall back to src/handler.ts"
2038 );
2039 }
2040
2041 #[test]
2042 fn test_try_source_fallback_mts_extension() {
2043 let src_path = PathBuf::from("/project/packages/lib/src/utils.mts");
2044 let mut path_to_id = FxHashMap::default();
2045 path_to_id.insert(src_path.as_path(), FileId(6));
2046
2047 let dist_path = PathBuf::from("/project/packages/lib/dist/utils.mjs");
2048 assert_eq!(
2049 try_source_fallback(&dist_path, &path_to_id),
2050 Some(FileId(6)),
2051 "dist/utils.mjs should fall back to src/utils.mts"
2052 );
2053 }
2054
2055 #[test]
2056 fn test_try_source_fallback_cts_extension() {
2057 let src_path = PathBuf::from("/project/packages/lib/src/config.cts");
2058 let mut path_to_id = FxHashMap::default();
2059 path_to_id.insert(src_path.as_path(), FileId(7));
2060
2061 let dist_path = PathBuf::from("/project/packages/lib/dist/config.cjs");
2062 assert_eq!(
2063 try_source_fallback(&dist_path, &path_to_id),
2064 Some(FileId(7)),
2065 "dist/config.cjs should fall back to src/config.cts"
2066 );
2067 }
2068
2069 #[test]
2070 fn test_try_source_fallback_jsx_extension() {
2071 let src_path = PathBuf::from("/project/packages/ui/src/App.jsx");
2072 let mut path_to_id = FxHashMap::default();
2073 path_to_id.insert(src_path.as_path(), FileId(8));
2074
2075 let build_path = PathBuf::from("/project/packages/ui/build/App.js");
2076 assert_eq!(
2077 try_source_fallback(&build_path, &path_to_id),
2078 Some(FileId(8)),
2079 "build/App.js should fall back to src/App.jsx"
2080 );
2081 }
2082
2083 #[test]
2084 fn test_try_source_fallback_no_file_stem() {
2085 let path_to_id: FxHashMap<&Path, FileId> = FxHashMap::default();
2087 let dist_path = PathBuf::from("/project/packages/ui/dist/");
2088 assert_eq!(
2089 try_source_fallback(&dist_path, &path_to_id),
2090 None,
2091 "directory path with no file should return None"
2092 );
2093 }
2094
2095 #[test]
2096 fn test_try_source_fallback_esm_subdir() {
2097 let src_path = PathBuf::from("/project/lib/src/index.ts");
2099 let mut path_to_id = FxHashMap::default();
2100 path_to_id.insert(src_path.as_path(), FileId(10));
2101
2102 let dist_path = PathBuf::from("/project/lib/esm/index.mjs");
2103 assert_eq!(
2104 try_source_fallback(&dist_path, &path_to_id),
2105 Some(FileId(10)),
2106 "standalone esm/ directory should fall back to src/"
2107 );
2108 }
2109
2110 #[test]
2111 fn test_try_source_fallback_cjs_subdir() {
2112 let src_path = PathBuf::from("/project/lib/src/index.ts");
2113 let mut path_to_id = FxHashMap::default();
2114 path_to_id.insert(src_path.as_path(), FileId(11));
2115
2116 let cjs_path = PathBuf::from("/project/lib/cjs/index.cjs");
2117 assert_eq!(
2118 try_source_fallback(&cjs_path, &path_to_id),
2119 Some(FileId(11)),
2120 "standalone cjs/ directory should fall back to src/"
2121 );
2122 }
2123
2124 #[test]
2127 fn test_try_pnpm_workspace_fallback_empty_after_pnpm() {
2128 let path_to_id: FxHashMap<&Path, FileId> = FxHashMap::default();
2130 let workspace_roots: FxHashMap<&str, &Path> = FxHashMap::default();
2131
2132 let pnpm_path = PathBuf::from("/project/node_modules/.pnpm/pkg@1.0.0/node_modules");
2133 assert_eq!(
2134 try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
2135 None,
2136 "path ending at node_modules with nothing after should return None"
2137 );
2138 }
2139
2140 #[test]
2141 fn test_try_pnpm_workspace_fallback_scoped_package_only_scope() {
2142 let path_to_id: FxHashMap<&Path, FileId> = FxHashMap::default();
2144 let workspace_roots: FxHashMap<&str, &Path> = FxHashMap::default();
2145
2146 let pnpm_path =
2147 PathBuf::from("/project/node_modules/.pnpm/@scope+pkg@1.0.0/node_modules/@scope");
2148 assert_eq!(
2149 try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
2150 None,
2151 "scoped package without full name and no matching workspace should return None"
2152 );
2153 }
2154
2155 #[test]
2156 fn test_try_pnpm_workspace_fallback_no_inner_node_modules() {
2157 let path_to_id: FxHashMap<&Path, FileId> = FxHashMap::default();
2159 let workspace_roots: FxHashMap<&str, &Path> = FxHashMap::default();
2160
2161 let pnpm_path = PathBuf::from("/project/node_modules/.pnpm/pkg@1.0.0/dist/index.js");
2162 assert_eq!(
2163 try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
2164 None,
2165 "path without inner node_modules after .pnpm should return None"
2166 );
2167 }
2168
2169 #[test]
2170 fn test_try_pnpm_workspace_fallback_package_without_relative_path() {
2171 let path_to_id: FxHashMap<&Path, FileId> = FxHashMap::default();
2173 let mut workspace_roots = FxHashMap::default();
2174 let ws_root = PathBuf::from("/project/packages/ui");
2175 workspace_roots.insert("@myorg/ui", ws_root.as_path());
2176
2177 let pnpm_path =
2178 PathBuf::from("/project/node_modules/.pnpm/@myorg+ui@1.0.0/node_modules/@myorg/ui");
2179 assert_eq!(
2180 try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
2181 None,
2182 "path ending at package name with no relative file should return None"
2183 );
2184 }
2185
2186 #[test]
2187 fn test_try_pnpm_workspace_fallback_nested_dist_esm() {
2188 let src_path = PathBuf::from("/project/packages/ui/src/Button.ts");
2189 let mut path_to_id = FxHashMap::default();
2190 path_to_id.insert(src_path.as_path(), FileId(10));
2191
2192 let mut workspace_roots = FxHashMap::default();
2193 let ws_root = PathBuf::from("/project/packages/ui");
2194 workspace_roots.insert("@myorg/ui", ws_root.as_path());
2195
2196 let pnpm_path = PathBuf::from(
2198 "/project/node_modules/.pnpm/@myorg+ui@1.0.0/node_modules/@myorg/ui/dist/esm/Button.mjs",
2199 );
2200 assert_eq!(
2201 try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
2202 Some(FileId(10)),
2203 "pnpm path with nested dist/esm should resolve through source fallback"
2204 );
2205 }
2206}