1use std::path::{Path, PathBuf};
7
8use rustc_hash::FxHashMap;
9
10use fallow_types::discover::FileId;
11
12use super::types::{OUTPUT_DIRS, ResolveContext, ResolveResult, SOURCE_EXTS};
13
14pub(super) fn try_path_alias_fallback(
21 ctx: &ResolveContext<'_>,
22 specifier: &str,
23) -> Option<ResolveResult> {
24 for (prefix, replacement) in ctx.path_aliases {
25 if !specifier.starts_with(prefix.as_str()) {
26 continue;
27 }
28
29 let remainder = &specifier[prefix.len()..];
30 let substituted = if replacement.is_empty() {
33 format!("./{remainder}")
34 } else {
35 format!("./{replacement}/{remainder}")
36 };
37
38 if let Ok(resolved) = ctx.resolver.resolve(ctx.root, &substituted) {
43 let resolved_path = resolved.path();
44 if let Some(&file_id) = ctx.raw_path_to_id.get(resolved_path) {
46 return Some(ResolveResult::InternalModule(file_id));
47 }
48 if let Ok(canonical) = dunce::canonicalize(resolved_path) {
50 if let Some(&file_id) = ctx.path_to_id.get(canonical.as_path()) {
51 return Some(ResolveResult::InternalModule(file_id));
52 }
53 if let Some(file_id) = try_source_fallback(&canonical, ctx.path_to_id) {
54 return Some(ResolveResult::InternalModule(file_id));
55 }
56 if let Some(file_id) =
57 try_pnpm_workspace_fallback(&canonical, ctx.path_to_id, ctx.workspace_roots)
58 {
59 return Some(ResolveResult::InternalModule(file_id));
60 }
61 if let Some(pkg_name) = extract_package_name_from_node_modules_path(&canonical) {
62 return Some(ResolveResult::NpmPackage(pkg_name));
63 }
64 return Some(ResolveResult::ExternalFile(canonical));
65 }
66 }
67 }
68 None
69}
70
71pub(super) fn try_scss_partial_fallback(
80 ctx: &ResolveContext<'_>,
81 from_file: &Path,
82 specifier: &str,
83) -> Option<ResolveResult> {
84 if specifier.contains(':') {
86 return None;
87 }
88
89 let spec_path = Path::new(specifier);
90 let filename = spec_path.file_name()?.to_str()?;
91
92 if filename.starts_with('_') {
94 return None;
95 }
96
97 let partial_filename = format!("_{filename}");
99 let partial_specifier = if let Some(parent) = spec_path.parent()
100 && !parent.as_os_str().is_empty()
101 {
102 format!("{}/{partial_filename}", parent.display())
103 } else {
104 partial_filename
105 };
106
107 if let Some(result) = try_resolve_scss(ctx, from_file, &partial_specifier) {
108 return Some(result);
109 }
110
111 let index_partial = format!("{specifier}/_index");
113 if let Some(result) = try_resolve_scss(ctx, from_file, &index_partial) {
114 return Some(result);
115 }
116
117 let index_plain = format!("{specifier}/index");
118 try_resolve_scss(ctx, from_file, &index_plain)
119}
120
121pub(super) fn try_css_extension_fallback(
131 ctx: &ResolveContext<'_>,
132 from_file: &Path,
133 specifier: &str,
134) -> Option<ResolveResult> {
135 if specifier.contains(':') {
136 return None;
137 }
138 let spec_path = Path::new(specifier);
142 let already_css_ext = spec_path
143 .extension()
144 .and_then(|e| e.to_str())
145 .is_some_and(|e| {
146 e.eq_ignore_ascii_case("css")
147 || e.eq_ignore_ascii_case("scss")
148 || e.eq_ignore_ascii_case("sass")
149 });
150 if already_css_ext {
151 return try_resolve_scss(ctx, from_file, specifier);
152 }
153 for ext in ["scss", "sass", "css"] {
154 let candidate = format!("{specifier}.{ext}");
155 if let Some(result) = try_resolve_scss(ctx, from_file, &candidate) {
156 return Some(result);
157 }
158 }
159 None
160}
161
162fn try_resolve_scss(
164 ctx: &ResolveContext<'_>,
165 from_file: &Path,
166 specifier: &str,
167) -> Option<ResolveResult> {
168 let resolved = ctx.resolver.resolve_file(from_file, specifier).ok()?;
169 let resolved_path = resolved.path();
170
171 if let Some(&file_id) = ctx.raw_path_to_id.get(resolved_path) {
172 return Some(ResolveResult::InternalModule(file_id));
173 }
174 if let Ok(canonical) = dunce::canonicalize(resolved_path)
175 && let Some(&file_id) = ctx.path_to_id.get(canonical.as_path())
176 {
177 return Some(ResolveResult::InternalModule(file_id));
178 }
179 None
180}
181
182pub(super) fn try_scss_include_path_fallback(
202 ctx: &ResolveContext<'_>,
203 from_file: &Path,
204 specifier: &str,
205 from_style: bool,
206) -> Option<ResolveResult> {
207 if ctx.scss_include_paths.is_empty() {
208 return None;
209 }
210 let is_scss_importer = from_file
211 .extension()
212 .is_some_and(|e| e == "scss" || e == "sass");
213 if !is_scss_importer && !from_style {
214 return None;
215 }
216 if specifier.contains(':') {
218 return None;
219 }
220 let bare = specifier.strip_prefix("./")?;
224 if bare.starts_with("..") || bare.starts_with('/') {
225 return None;
226 }
227
228 for include_dir in ctx.scss_include_paths {
229 if let Some(file_id) = find_scss_in_dir(include_dir, bare, ctx) {
230 return Some(ResolveResult::InternalModule(file_id));
231 }
232 }
233 None
234}
235
236fn find_scss_in_dir(include_dir: &Path, bare: &str, ctx: &ResolveContext<'_>) -> Option<FileId> {
240 let bare_path = Path::new(bare);
241 let has_scss_ext = matches!(
242 bare_path.extension().and_then(|e| e.to_str()),
243 Some(ext) if ext.eq_ignore_ascii_case("scss") || ext.eq_ignore_ascii_case("sass")
244 );
245
246 let parent = bare_path.parent();
249 let stem_with_ext = bare_path.file_name()?.to_str()?;
250 let stem_without_ext = bare_path.file_stem().and_then(|s| s.to_str())?;
251
252 let build = |rel: &Path| -> std::path::PathBuf { include_dir.join(rel) };
253 let join_with_parent = |name: &str| -> std::path::PathBuf {
254 parent.map_or_else(|| build(Path::new(name)), |p| build(&p.join(name)))
255 };
256
257 let exts: &[&str] = if has_scss_ext {
258 &[""]
259 } else {
260 &["scss", "sass"]
261 };
262
263 for ext in exts {
264 let suffix = if ext.is_empty() {
265 String::new()
266 } else {
267 format!(".{ext}")
268 };
269 let direct = if ext.is_empty() {
271 build(bare_path)
272 } else {
273 join_with_parent(&format!("{stem_with_ext}{suffix}"))
274 };
275 if let Some(fid) = lookup_scss_path(&direct, ctx) {
276 return Some(fid);
277 }
278 let partial_name = if ext.is_empty() {
280 format!("_{stem_with_ext}")
281 } else {
282 format!("_{stem_without_ext}{suffix}")
283 };
284 let partial = join_with_parent(&partial_name);
285 if let Some(fid) = lookup_scss_path(&partial, ctx) {
286 return Some(fid);
287 }
288 if ext.is_empty() {
289 continue;
291 }
292 let idx_partial = build(bare_path).join(format!("_index{suffix}"));
294 if let Some(fid) = lookup_scss_path(&idx_partial, ctx) {
295 return Some(fid);
296 }
297 let idx_plain = build(bare_path).join(format!("index{suffix}"));
298 if let Some(fid) = lookup_scss_path(&idx_plain, ctx) {
299 return Some(fid);
300 }
301 }
302 None
303}
304
305fn lookup_scss_path(candidate: &Path, ctx: &ResolveContext<'_>) -> Option<FileId> {
308 if let Some(&file_id) = ctx.raw_path_to_id.get(candidate) {
309 return Some(file_id);
310 }
311 if let Ok(canonical) = dunce::canonicalize(candidate) {
312 if let Some(&file_id) = ctx.path_to_id.get(canonical.as_path()) {
313 return Some(file_id);
314 }
315 if let Some(fallback) = ctx.canonical_fallback
316 && let Some(file_id) = fallback.get(&canonical)
317 {
318 return Some(file_id);
319 }
320 }
321 None
322}
323
324pub(super) fn try_scss_node_modules_fallback(
346 _ctx: &ResolveContext<'_>,
347 from_file: &Path,
348 specifier: &str,
349 from_style: bool,
350) -> Option<ResolveResult> {
351 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("./")?;
365 if bare.starts_with("..") || bare.starts_with('/') {
366 return None;
367 }
368 if bare.is_empty() {
372 return None;
373 }
374
375 let mut dir = from_file.parent()?;
383 loop {
384 let nm_dir = dir.join("node_modules");
385 if nm_dir.is_dir()
386 && let Some(path) = find_scss_in_node_modules(&nm_dir, bare)
387 && let Some(pkg_name) = extract_package_name_from_node_modules_path(&path)
388 {
389 return Some(ResolveResult::NpmPackage(pkg_name));
390 }
391 let Some(parent) = dir.parent() else {
392 break;
393 };
394 dir = parent;
395 }
396 None
397}
398
399fn find_scss_in_node_modules(nm_dir: &Path, bare: &str) -> Option<PathBuf> {
408 let bare_path = Path::new(bare);
409 let file_name = bare_path.file_name()?.to_str()?;
410 let parent = bare_path.parent();
411 let join_with_parent = |name: &str| -> PathBuf {
412 parent.map_or_else(|| nm_dir.join(name), |p| nm_dir.join(p).join(name))
413 };
414
415 for ext in &["scss", "sass", "css"] {
419 let candidate = join_with_parent(&format!("{file_name}.{ext}"));
420 if candidate.is_file() {
421 return Some(candidate);
422 }
423 }
424 for ext in &["scss", "sass"] {
427 let candidate = join_with_parent(&format!("_{file_name}.{ext}"));
428 if candidate.is_file() {
429 return Some(candidate);
430 }
431 }
432 for ext in &["scss", "sass"] {
434 let idx_partial = nm_dir.join(bare).join(format!("_index.{ext}"));
435 if idx_partial.is_file() {
436 return Some(idx_partial);
437 }
438 let idx_plain = nm_dir.join(bare).join(format!("index.{ext}"));
439 if idx_plain.is_file() {
440 return Some(idx_plain);
441 }
442 }
443 let exact = nm_dir.join(bare);
446 if exact.is_file() {
447 return Some(exact);
448 }
449 None
450}
451
452pub(super) fn try_source_fallback(
464 resolved: &Path,
465 path_to_id: &FxHashMap<&Path, FileId>,
466) -> Option<FileId> {
467 let components: Vec<_> = resolved.components().collect();
468
469 let is_output_dir = |c: &std::path::Component| -> bool {
470 if let std::path::Component::Normal(s) = c
471 && let Some(name) = s.to_str()
472 {
473 return OUTPUT_DIRS.contains(&name);
474 }
475 false
476 };
477
478 let last_output_pos = components.iter().rposition(&is_output_dir)?;
482
483 let mut first_output_pos = last_output_pos;
486 while first_output_pos > 0 && is_output_dir(&components[first_output_pos - 1]) {
487 first_output_pos -= 1;
488 }
489
490 let prefix: PathBuf = components[..first_output_pos].iter().collect();
492
493 let suffix: PathBuf = components[last_output_pos + 1..].iter().collect();
495 suffix.file_stem()?; for ext in SOURCE_EXTS {
499 let source_candidate = prefix.join("src").join(suffix.with_extension(ext));
500 if let Some(&file_id) = path_to_id.get(source_candidate.as_path()) {
501 return Some(file_id);
502 }
503 }
504
505 None
506}
507
508pub fn extract_package_name_from_node_modules_path(path: &Path) -> Option<String> {
514 let components: Vec<&str> = path
515 .components()
516 .filter_map(|c| match c {
517 std::path::Component::Normal(s) => s.to_str(),
518 _ => None,
519 })
520 .collect();
521
522 let nm_idx = components.iter().rposition(|&c| c == "node_modules")?;
524
525 let after = &components[nm_idx + 1..];
526 if after.is_empty() {
527 return None;
528 }
529
530 if after[0].starts_with('@') {
531 if after.len() >= 2 {
533 Some(format!("{}/{}", after[0], after[1]))
534 } else {
535 Some(after[0].to_string())
536 }
537 } else {
538 Some(after[0].to_string())
539 }
540}
541
542pub(super) fn try_pnpm_workspace_fallback(
551 path: &Path,
552 path_to_id: &FxHashMap<&Path, FileId>,
553 workspace_roots: &FxHashMap<&str, &Path>,
554) -> Option<FileId> {
555 let components: Vec<&str> = path
557 .components()
558 .filter_map(|c| match c {
559 std::path::Component::Normal(s) => s.to_str(),
560 _ => None,
561 })
562 .collect();
563
564 let pnpm_idx = components.iter().position(|&c| c == ".pnpm")?;
566
567 let after_pnpm = &components[pnpm_idx + 1..];
570
571 let inner_nm_idx = after_pnpm.iter().position(|&c| c == "node_modules")?;
573 let after_inner_nm = &after_pnpm[inner_nm_idx + 1..];
574
575 if after_inner_nm.is_empty() {
576 return None;
577 }
578
579 let (pkg_name, pkg_name_components) = if after_inner_nm[0].starts_with('@') {
581 if after_inner_nm.len() >= 2 {
582 (format!("{}/{}", after_inner_nm[0], after_inner_nm[1]), 2)
583 } else {
584 return None;
585 }
586 } else {
587 (after_inner_nm[0].to_string(), 1)
588 };
589
590 let ws_root = workspace_roots.get(pkg_name.as_str())?;
592
593 let relative_parts = &after_inner_nm[pkg_name_components..];
595 if relative_parts.is_empty() {
596 return None;
597 }
598
599 let relative_path: PathBuf = relative_parts.iter().collect();
600
601 let direct = ws_root.join(&relative_path);
603 if let Some(&file_id) = path_to_id.get(direct.as_path()) {
604 return Some(file_id);
605 }
606
607 try_source_fallback(&direct, path_to_id)
609}
610
611pub(super) fn try_workspace_package_fallback(
635 ctx: &ResolveContext<'_>,
636 specifier: &str,
637) -> Option<ResolveResult> {
638 if !super::path_info::is_bare_specifier(specifier) {
640 return None;
641 }
642 let pkg_name = super::path_info::extract_package_name(specifier);
643 let ws_root = *ctx.workspace_roots.get(pkg_name.as_str())?;
644
645 let subpath = specifier
648 .strip_prefix(pkg_name.as_str())
649 .and_then(|s| s.strip_prefix('/'))
650 .unwrap_or("");
651
652 let root_file = ws_root.join("__fallow_ws_self_resolve__");
655 let rel_spec = if subpath.is_empty() {
656 "./".to_string()
657 } else {
658 format!("./{subpath}")
659 };
660
661 let resolved = ctx.resolver.resolve_file(&root_file, &rel_spec).ok()?;
662 let resolved_path = resolved.path();
663
664 if let Some(&file_id) = ctx.raw_path_to_id.get(resolved_path) {
665 return Some(ResolveResult::InternalModule(file_id));
666 }
667 if let Ok(canonical) = dunce::canonicalize(resolved_path) {
668 if let Some(&file_id) = ctx.path_to_id.get(canonical.as_path()) {
669 return Some(ResolveResult::InternalModule(file_id));
670 }
671 if let Some(fallback) = ctx.canonical_fallback
672 && let Some(file_id) = fallback.get(&canonical)
673 {
674 return Some(ResolveResult::InternalModule(file_id));
675 }
676 if let Some(file_id) = try_source_fallback(&canonical, ctx.path_to_id) {
677 return Some(ResolveResult::InternalModule(file_id));
678 }
679 }
680 None
681}
682
683pub(super) fn make_glob_from_pattern(
685 pattern: &fallow_types::extract::DynamicImportPattern,
686) -> String {
687 if pattern.prefix.contains('*') || pattern.prefix.contains('{') {
689 return pattern.prefix.clone();
690 }
691 pattern.suffix.as_ref().map_or_else(
692 || format!("{}*", pattern.prefix),
693 |suffix| format!("{}*{}", pattern.prefix, suffix),
694 )
695}
696
697#[cfg(test)]
698mod tests {
699 use super::*;
700
701 #[test]
702 fn test_extract_package_name_from_node_modules_path_regular() {
703 let path = PathBuf::from("/project/node_modules/react/index.js");
704 assert_eq!(
705 extract_package_name_from_node_modules_path(&path),
706 Some("react".to_string())
707 );
708 }
709
710 #[test]
711 fn test_extract_package_name_from_node_modules_path_scoped() {
712 let path = PathBuf::from("/project/node_modules/@babel/core/lib/index.js");
713 assert_eq!(
714 extract_package_name_from_node_modules_path(&path),
715 Some("@babel/core".to_string())
716 );
717 }
718
719 #[test]
720 fn test_extract_package_name_from_node_modules_path_nested() {
721 let path = PathBuf::from("/project/node_modules/pkg-a/node_modules/pkg-b/dist/index.js");
723 assert_eq!(
724 extract_package_name_from_node_modules_path(&path),
725 Some("pkg-b".to_string())
726 );
727 }
728
729 #[test]
730 fn test_extract_package_name_from_node_modules_path_deep_subpath() {
731 let path = PathBuf::from("/project/node_modules/react-dom/cjs/react-dom.production.min.js");
732 assert_eq!(
733 extract_package_name_from_node_modules_path(&path),
734 Some("react-dom".to_string())
735 );
736 }
737
738 #[test]
739 fn test_extract_package_name_from_node_modules_path_no_node_modules() {
740 let path = PathBuf::from("/project/src/components/Button.tsx");
741 assert_eq!(extract_package_name_from_node_modules_path(&path), None);
742 }
743
744 #[test]
745 fn test_extract_package_name_from_node_modules_path_just_node_modules() {
746 let path = PathBuf::from("/project/node_modules");
747 assert_eq!(extract_package_name_from_node_modules_path(&path), None);
748 }
749
750 #[test]
751 fn test_extract_package_name_from_node_modules_path_scoped_only_scope() {
752 let path = PathBuf::from("/project/node_modules/@scope");
754 assert_eq!(
755 extract_package_name_from_node_modules_path(&path),
756 Some("@scope".to_string())
757 );
758 }
759
760 #[test]
761 fn test_resolve_specifier_node_modules_returns_npm_package() {
762 let path =
768 PathBuf::from("/project/node_modules/styled-components/dist/styled-components.esm.js");
769 assert_eq!(
770 extract_package_name_from_node_modules_path(&path),
771 Some("styled-components".to_string())
772 );
773
774 let path = PathBuf::from("/project/node_modules/next/dist/server/next.js");
775 assert_eq!(
776 extract_package_name_from_node_modules_path(&path),
777 Some("next".to_string())
778 );
779 }
780
781 #[test]
782 fn test_try_source_fallback_dist_to_src() {
783 let src_path = PathBuf::from("/project/packages/ui/src/utils.ts");
784 let mut path_to_id = FxHashMap::default();
785 path_to_id.insert(src_path.as_path(), FileId(0));
786
787 let dist_path = PathBuf::from("/project/packages/ui/dist/utils.js");
788 assert_eq!(
789 try_source_fallback(&dist_path, &path_to_id),
790 Some(FileId(0)),
791 "dist/utils.js should fall back to src/utils.ts"
792 );
793 }
794
795 #[test]
796 fn test_try_source_fallback_build_to_src() {
797 let src_path = PathBuf::from("/project/packages/core/src/index.tsx");
798 let mut path_to_id = FxHashMap::default();
799 path_to_id.insert(src_path.as_path(), FileId(1));
800
801 let build_path = PathBuf::from("/project/packages/core/build/index.js");
802 assert_eq!(
803 try_source_fallback(&build_path, &path_to_id),
804 Some(FileId(1)),
805 "build/index.js should fall back to src/index.tsx"
806 );
807 }
808
809 #[test]
810 fn test_try_source_fallback_no_match() {
811 let path_to_id: FxHashMap<&Path, FileId> = FxHashMap::default();
812
813 let dist_path = PathBuf::from("/project/packages/ui/dist/utils.js");
814 assert_eq!(
815 try_source_fallback(&dist_path, &path_to_id),
816 None,
817 "should return None when no source file exists"
818 );
819 }
820
821 #[test]
822 fn test_try_source_fallback_non_output_dir() {
823 let src_path = PathBuf::from("/project/packages/ui/src/utils.ts");
824 let mut path_to_id = FxHashMap::default();
825 path_to_id.insert(src_path.as_path(), FileId(0));
826
827 let normal_path = PathBuf::from("/project/packages/ui/scripts/utils.js");
829 assert_eq!(
830 try_source_fallback(&normal_path, &path_to_id),
831 None,
832 "non-output directory path should not trigger fallback"
833 );
834 }
835
836 #[test]
837 fn test_try_source_fallback_nested_path() {
838 let src_path = PathBuf::from("/project/packages/ui/src/components/Button.ts");
839 let mut path_to_id = FxHashMap::default();
840 path_to_id.insert(src_path.as_path(), FileId(2));
841
842 let dist_path = PathBuf::from("/project/packages/ui/dist/components/Button.js");
843 assert_eq!(
844 try_source_fallback(&dist_path, &path_to_id),
845 Some(FileId(2)),
846 "nested dist path should fall back to nested src path"
847 );
848 }
849
850 #[test]
851 fn test_try_source_fallback_nested_dist_esm() {
852 let src_path = PathBuf::from("/project/packages/ui/src/utils.ts");
853 let mut path_to_id = FxHashMap::default();
854 path_to_id.insert(src_path.as_path(), FileId(0));
855
856 let dist_path = PathBuf::from("/project/packages/ui/dist/esm/utils.mjs");
857 assert_eq!(
858 try_source_fallback(&dist_path, &path_to_id),
859 Some(FileId(0)),
860 "dist/esm/utils.mjs should fall back to src/utils.ts"
861 );
862 }
863
864 #[test]
865 fn test_try_source_fallback_nested_build_cjs() {
866 let src_path = PathBuf::from("/project/packages/core/src/index.ts");
867 let mut path_to_id = FxHashMap::default();
868 path_to_id.insert(src_path.as_path(), FileId(1));
869
870 let build_path = PathBuf::from("/project/packages/core/build/cjs/index.cjs");
871 assert_eq!(
872 try_source_fallback(&build_path, &path_to_id),
873 Some(FileId(1)),
874 "build/cjs/index.cjs should fall back to src/index.ts"
875 );
876 }
877
878 #[test]
879 fn test_try_source_fallback_nested_dist_esm_deep_path() {
880 let src_path = PathBuf::from("/project/packages/ui/src/components/Button.ts");
881 let mut path_to_id = FxHashMap::default();
882 path_to_id.insert(src_path.as_path(), FileId(2));
883
884 let dist_path = PathBuf::from("/project/packages/ui/dist/esm/components/Button.mjs");
885 assert_eq!(
886 try_source_fallback(&dist_path, &path_to_id),
887 Some(FileId(2)),
888 "dist/esm/components/Button.mjs should fall back to src/components/Button.ts"
889 );
890 }
891
892 #[test]
893 fn test_try_source_fallback_triple_nested_output_dirs() {
894 let src_path = PathBuf::from("/project/packages/ui/src/utils.ts");
895 let mut path_to_id = FxHashMap::default();
896 path_to_id.insert(src_path.as_path(), FileId(0));
897
898 let dist_path = PathBuf::from("/project/packages/ui/out/dist/esm/utils.mjs");
899 assert_eq!(
900 try_source_fallback(&dist_path, &path_to_id),
901 Some(FileId(0)),
902 "out/dist/esm/utils.mjs should fall back to src/utils.ts"
903 );
904 }
905
906 #[test]
907 fn test_try_source_fallback_parent_dir_named_build() {
908 let src_path = PathBuf::from("/home/user/build/my-project/src/utils.ts");
909 let mut path_to_id = FxHashMap::default();
910 path_to_id.insert(src_path.as_path(), FileId(0));
911
912 let dist_path = PathBuf::from("/home/user/build/my-project/dist/utils.js");
913 assert_eq!(
914 try_source_fallback(&dist_path, &path_to_id),
915 Some(FileId(0)),
916 "should resolve dist/ within project, not match parent 'build' dir"
917 );
918 }
919
920 #[test]
921 fn test_pnpm_store_path_extract_package_name() {
922 let path =
924 PathBuf::from("/project/node_modules/.pnpm/react@18.2.0/node_modules/react/index.js");
925 assert_eq!(
926 extract_package_name_from_node_modules_path(&path),
927 Some("react".to_string())
928 );
929 }
930
931 #[test]
932 fn test_pnpm_store_path_scoped_package() {
933 let path = PathBuf::from(
934 "/project/node_modules/.pnpm/@babel+core@7.24.0/node_modules/@babel/core/lib/index.js",
935 );
936 assert_eq!(
937 extract_package_name_from_node_modules_path(&path),
938 Some("@babel/core".to_string())
939 );
940 }
941
942 #[test]
943 fn test_pnpm_store_path_with_peer_deps() {
944 let path = PathBuf::from(
945 "/project/node_modules/.pnpm/webpack@5.0.0_esbuild@0.19.0/node_modules/webpack/lib/index.js",
946 );
947 assert_eq!(
948 extract_package_name_from_node_modules_path(&path),
949 Some("webpack".to_string())
950 );
951 }
952
953 #[test]
954 fn test_try_pnpm_workspace_fallback_dist_to_src() {
955 let src_path = PathBuf::from("/project/packages/ui/src/utils.ts");
956 let mut path_to_id = FxHashMap::default();
957 path_to_id.insert(src_path.as_path(), FileId(0));
958
959 let mut workspace_roots = FxHashMap::default();
960 let ws_root = PathBuf::from("/project/packages/ui");
961 workspace_roots.insert("@myorg/ui", ws_root.as_path());
962
963 let pnpm_path = PathBuf::from(
965 "/project/node_modules/.pnpm/@myorg+ui@1.0.0/node_modules/@myorg/ui/dist/utils.js",
966 );
967 assert_eq!(
968 try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
969 Some(FileId(0)),
970 ".pnpm workspace path should fall back to src/utils.ts"
971 );
972 }
973
974 #[test]
975 fn test_try_pnpm_workspace_fallback_direct_source() {
976 let src_path = PathBuf::from("/project/packages/core/src/index.ts");
977 let mut path_to_id = FxHashMap::default();
978 path_to_id.insert(src_path.as_path(), FileId(1));
979
980 let mut workspace_roots = FxHashMap::default();
981 let ws_root = PathBuf::from("/project/packages/core");
982 workspace_roots.insert("@myorg/core", ws_root.as_path());
983
984 let pnpm_path = PathBuf::from(
986 "/project/node_modules/.pnpm/@myorg+core@workspace/node_modules/@myorg/core/src/index.ts",
987 );
988 assert_eq!(
989 try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
990 Some(FileId(1)),
991 ".pnpm workspace path with src/ should resolve directly"
992 );
993 }
994
995 #[test]
996 fn test_try_pnpm_workspace_fallback_non_workspace_package() {
997 let path_to_id: FxHashMap<&Path, FileId> = FxHashMap::default();
998
999 let mut workspace_roots = FxHashMap::default();
1000 let ws_root = PathBuf::from("/project/packages/ui");
1001 workspace_roots.insert("@myorg/ui", ws_root.as_path());
1002
1003 let pnpm_path =
1005 PathBuf::from("/project/node_modules/.pnpm/react@18.2.0/node_modules/react/index.js");
1006 assert_eq!(
1007 try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
1008 None,
1009 "non-workspace package in .pnpm should return None"
1010 );
1011 }
1012
1013 #[test]
1014 fn test_try_pnpm_workspace_fallback_unscoped_package() {
1015 let src_path = PathBuf::from("/project/packages/utils/src/index.ts");
1016 let mut path_to_id = FxHashMap::default();
1017 path_to_id.insert(src_path.as_path(), FileId(2));
1018
1019 let mut workspace_roots = FxHashMap::default();
1020 let ws_root = PathBuf::from("/project/packages/utils");
1021 workspace_roots.insert("my-utils", ws_root.as_path());
1022
1023 let pnpm_path = PathBuf::from(
1025 "/project/node_modules/.pnpm/my-utils@1.0.0/node_modules/my-utils/dist/index.js",
1026 );
1027 assert_eq!(
1028 try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
1029 Some(FileId(2)),
1030 "unscoped workspace package in .pnpm should resolve"
1031 );
1032 }
1033
1034 #[test]
1035 fn test_try_pnpm_workspace_fallback_nested_path() {
1036 let src_path = PathBuf::from("/project/packages/ui/src/components/Button.ts");
1037 let mut path_to_id = FxHashMap::default();
1038 path_to_id.insert(src_path.as_path(), FileId(3));
1039
1040 let mut workspace_roots = FxHashMap::default();
1041 let ws_root = PathBuf::from("/project/packages/ui");
1042 workspace_roots.insert("@myorg/ui", ws_root.as_path());
1043
1044 let pnpm_path = PathBuf::from(
1046 "/project/node_modules/.pnpm/@myorg+ui@1.0.0/node_modules/@myorg/ui/dist/components/Button.js",
1047 );
1048 assert_eq!(
1049 try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
1050 Some(FileId(3)),
1051 "nested .pnpm workspace path should resolve through source fallback"
1052 );
1053 }
1054
1055 #[test]
1056 fn test_try_pnpm_workspace_fallback_no_pnpm() {
1057 let path_to_id: FxHashMap<&Path, FileId> = FxHashMap::default();
1058 let workspace_roots: FxHashMap<&str, &Path> = FxHashMap::default();
1059
1060 let regular_path = PathBuf::from("/project/node_modules/react/index.js");
1062 assert_eq!(
1063 try_pnpm_workspace_fallback(®ular_path, &path_to_id, &workspace_roots),
1064 None,
1065 );
1066 }
1067
1068 #[test]
1069 fn test_try_pnpm_workspace_fallback_with_peer_deps() {
1070 let src_path = PathBuf::from("/project/packages/ui/src/index.ts");
1071 let mut path_to_id = FxHashMap::default();
1072 path_to_id.insert(src_path.as_path(), FileId(4));
1073
1074 let mut workspace_roots = FxHashMap::default();
1075 let ws_root = PathBuf::from("/project/packages/ui");
1076 workspace_roots.insert("@myorg/ui", ws_root.as_path());
1077
1078 let pnpm_path = PathBuf::from(
1080 "/project/node_modules/.pnpm/@myorg+ui@1.0.0_react@18.2.0/node_modules/@myorg/ui/dist/index.js",
1081 );
1082 assert_eq!(
1083 try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
1084 Some(FileId(4)),
1085 ".pnpm path with peer dep suffix should still resolve"
1086 );
1087 }
1088
1089 #[test]
1092 fn make_glob_prefix_only_no_suffix() {
1093 let pattern = fallow_types::extract::DynamicImportPattern {
1094 prefix: "./locales/".to_string(),
1095 suffix: None,
1096 span: oxc_span::Span::default(),
1097 };
1098 assert_eq!(make_glob_from_pattern(&pattern), "./locales/*");
1099 }
1100
1101 #[test]
1102 fn make_glob_prefix_with_suffix() {
1103 let pattern = fallow_types::extract::DynamicImportPattern {
1104 prefix: "./locales/".to_string(),
1105 suffix: Some(".json".to_string()),
1106 span: oxc_span::Span::default(),
1107 };
1108 assert_eq!(make_glob_from_pattern(&pattern), "./locales/*.json");
1109 }
1110
1111 #[test]
1112 fn make_glob_passthrough_star() {
1113 let pattern = fallow_types::extract::DynamicImportPattern {
1115 prefix: "./pages/**/*.tsx".to_string(),
1116 suffix: None,
1117 span: oxc_span::Span::default(),
1118 };
1119 assert_eq!(make_glob_from_pattern(&pattern), "./pages/**/*.tsx");
1120 }
1121
1122 #[test]
1123 fn make_glob_passthrough_brace() {
1124 let pattern = fallow_types::extract::DynamicImportPattern {
1125 prefix: "./i18n/{en,de,fr}.json".to_string(),
1126 suffix: None,
1127 span: oxc_span::Span::default(),
1128 };
1129 assert_eq!(make_glob_from_pattern(&pattern), "./i18n/{en,de,fr}.json");
1130 }
1131
1132 #[test]
1133 fn make_glob_empty_prefix_no_suffix() {
1134 let pattern = fallow_types::extract::DynamicImportPattern {
1135 prefix: String::new(),
1136 suffix: None,
1137 span: oxc_span::Span::default(),
1138 };
1139 assert_eq!(make_glob_from_pattern(&pattern), "*");
1140 }
1141
1142 #[test]
1143 fn make_glob_empty_prefix_with_suffix() {
1144 let pattern = fallow_types::extract::DynamicImportPattern {
1145 prefix: String::new(),
1146 suffix: Some(".ts".to_string()),
1147 span: oxc_span::Span::default(),
1148 };
1149 assert_eq!(make_glob_from_pattern(&pattern), "*.ts");
1150 }
1151
1152 #[test]
1155 fn make_glob_template_literal_prefix_only() {
1156 let pattern = fallow_types::extract::DynamicImportPattern {
1158 prefix: "./pages/".to_string(),
1159 suffix: None,
1160 span: oxc_span::Span::default(),
1161 };
1162 assert_eq!(make_glob_from_pattern(&pattern), "./pages/*");
1163 }
1164
1165 #[test]
1166 fn make_glob_template_literal_with_extension_suffix() {
1167 let pattern = fallow_types::extract::DynamicImportPattern {
1169 prefix: "./locales/".to_string(),
1170 suffix: Some(".json".to_string()),
1171 span: oxc_span::Span::default(),
1172 };
1173 assert_eq!(make_glob_from_pattern(&pattern), "./locales/*.json");
1174 }
1175
1176 #[test]
1177 fn make_glob_template_literal_deep_prefix() {
1178 let pattern = fallow_types::extract::DynamicImportPattern {
1181 prefix: "./modules/".to_string(),
1182 suffix: None,
1183 span: oxc_span::Span::default(),
1184 };
1185 assert_eq!(make_glob_from_pattern(&pattern), "./modules/*");
1186 }
1187
1188 #[test]
1189 fn make_glob_string_concat_prefix() {
1190 let pattern = fallow_types::extract::DynamicImportPattern {
1192 prefix: "./pages/".to_string(),
1193 suffix: None,
1194 span: oxc_span::Span::default(),
1195 };
1196 assert_eq!(make_glob_from_pattern(&pattern), "./pages/*");
1197 }
1198
1199 #[test]
1200 fn make_glob_string_concat_with_extension() {
1201 let pattern = fallow_types::extract::DynamicImportPattern {
1203 prefix: "./views/".to_string(),
1204 suffix: Some(".vue".to_string()),
1205 span: oxc_span::Span::default(),
1206 };
1207 assert_eq!(make_glob_from_pattern(&pattern), "./views/*.vue");
1208 }
1209
1210 #[test]
1213 fn make_glob_import_meta_glob_recursive() {
1214 let pattern = fallow_types::extract::DynamicImportPattern {
1216 prefix: "./components/**/*.vue".to_string(),
1217 suffix: None,
1218 span: oxc_span::Span::default(),
1219 };
1220 assert_eq!(
1221 make_glob_from_pattern(&pattern),
1222 "./components/**/*.vue",
1223 "import.meta.glob patterns with * should pass through as-is"
1224 );
1225 }
1226
1227 #[test]
1228 fn make_glob_import_meta_glob_brace_expansion() {
1229 let pattern = fallow_types::extract::DynamicImportPattern {
1231 prefix: "./plugins/{auth,analytics}.ts".to_string(),
1232 suffix: None,
1233 span: oxc_span::Span::default(),
1234 };
1235 assert_eq!(
1236 make_glob_from_pattern(&pattern),
1237 "./plugins/{auth,analytics}.ts",
1238 "import.meta.glob patterns with braces should pass through as-is"
1239 );
1240 }
1241
1242 #[test]
1243 fn make_glob_import_meta_glob_star_with_brace() {
1244 let pattern = fallow_types::extract::DynamicImportPattern {
1246 prefix: "./routes/**/*.{ts,tsx}".to_string(),
1247 suffix: None,
1248 span: oxc_span::Span::default(),
1249 };
1250 assert_eq!(
1251 make_glob_from_pattern(&pattern),
1252 "./routes/**/*.{ts,tsx}",
1253 "combined * and brace patterns should pass through"
1254 );
1255 }
1256
1257 #[test]
1258 fn make_glob_import_meta_glob_ignores_suffix_when_star_present() {
1259 let pattern = fallow_types::extract::DynamicImportPattern {
1261 prefix: "./*.ts".to_string(),
1262 suffix: Some(".extra".to_string()),
1263 span: oxc_span::Span::default(),
1264 };
1265 assert_eq!(
1266 make_glob_from_pattern(&pattern),
1267 "./*.ts",
1268 "when prefix has glob chars, suffix is ignored (prefix used as-is)"
1269 );
1270 }
1271
1272 #[test]
1275 fn make_glob_single_dot_prefix() {
1276 let pattern = fallow_types::extract::DynamicImportPattern {
1277 prefix: "./".to_string(),
1278 suffix: None,
1279 span: oxc_span::Span::default(),
1280 };
1281 assert_eq!(make_glob_from_pattern(&pattern), "./*");
1282 }
1283
1284 #[test]
1285 fn make_glob_prefix_without_trailing_slash() {
1286 let pattern = fallow_types::extract::DynamicImportPattern {
1288 prefix: "./config".to_string(),
1289 suffix: None,
1290 span: oxc_span::Span::default(),
1291 };
1292 assert_eq!(make_glob_from_pattern(&pattern), "./config*");
1293 }
1294
1295 #[test]
1296 fn make_glob_prefix_with_dotdot() {
1297 let pattern = fallow_types::extract::DynamicImportPattern {
1298 prefix: "../shared/".to_string(),
1299 suffix: Some(".ts".to_string()),
1300 span: oxc_span::Span::default(),
1301 };
1302 assert_eq!(make_glob_from_pattern(&pattern), "../shared/*.ts");
1303 }
1304
1305 #[test]
1308 fn test_extract_package_name_with_pnpm_plus_encoded_scope() {
1309 let path = PathBuf::from(
1312 "/project/node_modules/.pnpm/@mui+material@5.15.0/node_modules/@mui/material/index.js",
1313 );
1314 assert_eq!(
1315 extract_package_name_from_node_modules_path(&path),
1316 Some("@mui/material".to_string())
1317 );
1318 }
1319
1320 #[test]
1321 fn test_extract_package_name_windows_style_path() {
1322 let path = PathBuf::from("/project/node_modules/typescript/lib/tsc.js");
1324 assert_eq!(
1325 extract_package_name_from_node_modules_path(&path),
1326 Some("typescript".to_string())
1327 );
1328 }
1329
1330 #[test]
1333 fn test_try_source_fallback_out_dir() {
1334 let src_path = PathBuf::from("/project/packages/api/src/handler.ts");
1335 let mut path_to_id = FxHashMap::default();
1336 path_to_id.insert(src_path.as_path(), FileId(5));
1337
1338 let out_path = PathBuf::from("/project/packages/api/out/handler.js");
1339 assert_eq!(
1340 try_source_fallback(&out_path, &path_to_id),
1341 Some(FileId(5)),
1342 "out/handler.js should fall back to src/handler.ts"
1343 );
1344 }
1345
1346 #[test]
1347 fn test_try_source_fallback_mts_extension() {
1348 let src_path = PathBuf::from("/project/packages/lib/src/utils.mts");
1349 let mut path_to_id = FxHashMap::default();
1350 path_to_id.insert(src_path.as_path(), FileId(6));
1351
1352 let dist_path = PathBuf::from("/project/packages/lib/dist/utils.mjs");
1353 assert_eq!(
1354 try_source_fallback(&dist_path, &path_to_id),
1355 Some(FileId(6)),
1356 "dist/utils.mjs should fall back to src/utils.mts"
1357 );
1358 }
1359
1360 #[test]
1361 fn test_try_source_fallback_cts_extension() {
1362 let src_path = PathBuf::from("/project/packages/lib/src/config.cts");
1363 let mut path_to_id = FxHashMap::default();
1364 path_to_id.insert(src_path.as_path(), FileId(7));
1365
1366 let dist_path = PathBuf::from("/project/packages/lib/dist/config.cjs");
1367 assert_eq!(
1368 try_source_fallback(&dist_path, &path_to_id),
1369 Some(FileId(7)),
1370 "dist/config.cjs should fall back to src/config.cts"
1371 );
1372 }
1373
1374 #[test]
1375 fn test_try_source_fallback_jsx_extension() {
1376 let src_path = PathBuf::from("/project/packages/ui/src/App.jsx");
1377 let mut path_to_id = FxHashMap::default();
1378 path_to_id.insert(src_path.as_path(), FileId(8));
1379
1380 let build_path = PathBuf::from("/project/packages/ui/build/App.js");
1381 assert_eq!(
1382 try_source_fallback(&build_path, &path_to_id),
1383 Some(FileId(8)),
1384 "build/App.js should fall back to src/App.jsx"
1385 );
1386 }
1387
1388 #[test]
1389 fn test_try_source_fallback_no_file_stem() {
1390 let path_to_id: FxHashMap<&Path, FileId> = FxHashMap::default();
1392 let dist_path = PathBuf::from("/project/packages/ui/dist/");
1393 assert_eq!(
1394 try_source_fallback(&dist_path, &path_to_id),
1395 None,
1396 "directory path with no file should return None"
1397 );
1398 }
1399
1400 #[test]
1401 fn test_try_source_fallback_esm_subdir() {
1402 let src_path = PathBuf::from("/project/lib/src/index.ts");
1404 let mut path_to_id = FxHashMap::default();
1405 path_to_id.insert(src_path.as_path(), FileId(10));
1406
1407 let dist_path = PathBuf::from("/project/lib/esm/index.mjs");
1408 assert_eq!(
1409 try_source_fallback(&dist_path, &path_to_id),
1410 Some(FileId(10)),
1411 "standalone esm/ directory should fall back to src/"
1412 );
1413 }
1414
1415 #[test]
1416 fn test_try_source_fallback_cjs_subdir() {
1417 let src_path = PathBuf::from("/project/lib/src/index.ts");
1418 let mut path_to_id = FxHashMap::default();
1419 path_to_id.insert(src_path.as_path(), FileId(11));
1420
1421 let cjs_path = PathBuf::from("/project/lib/cjs/index.cjs");
1422 assert_eq!(
1423 try_source_fallback(&cjs_path, &path_to_id),
1424 Some(FileId(11)),
1425 "standalone cjs/ directory should fall back to src/"
1426 );
1427 }
1428
1429 #[test]
1432 fn test_try_pnpm_workspace_fallback_empty_after_pnpm() {
1433 let path_to_id: FxHashMap<&Path, FileId> = FxHashMap::default();
1435 let workspace_roots: FxHashMap<&str, &Path> = FxHashMap::default();
1436
1437 let pnpm_path = PathBuf::from("/project/node_modules/.pnpm/pkg@1.0.0/node_modules");
1438 assert_eq!(
1439 try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
1440 None,
1441 "path ending at node_modules with nothing after should return None"
1442 );
1443 }
1444
1445 #[test]
1446 fn test_try_pnpm_workspace_fallback_scoped_package_only_scope() {
1447 let path_to_id: FxHashMap<&Path, FileId> = FxHashMap::default();
1449 let workspace_roots: FxHashMap<&str, &Path> = FxHashMap::default();
1450
1451 let pnpm_path =
1452 PathBuf::from("/project/node_modules/.pnpm/@scope+pkg@1.0.0/node_modules/@scope");
1453 assert_eq!(
1454 try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
1455 None,
1456 "scoped package without full name and no matching workspace should return None"
1457 );
1458 }
1459
1460 #[test]
1461 fn test_try_pnpm_workspace_fallback_no_inner_node_modules() {
1462 let path_to_id: FxHashMap<&Path, FileId> = FxHashMap::default();
1464 let workspace_roots: FxHashMap<&str, &Path> = FxHashMap::default();
1465
1466 let pnpm_path = PathBuf::from("/project/node_modules/.pnpm/pkg@1.0.0/dist/index.js");
1467 assert_eq!(
1468 try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
1469 None,
1470 "path without inner node_modules after .pnpm should return None"
1471 );
1472 }
1473
1474 #[test]
1475 fn test_try_pnpm_workspace_fallback_package_without_relative_path() {
1476 let path_to_id: FxHashMap<&Path, FileId> = FxHashMap::default();
1478 let mut workspace_roots = FxHashMap::default();
1479 let ws_root = PathBuf::from("/project/packages/ui");
1480 workspace_roots.insert("@myorg/ui", ws_root.as_path());
1481
1482 let pnpm_path =
1483 PathBuf::from("/project/node_modules/.pnpm/@myorg+ui@1.0.0/node_modules/@myorg/ui");
1484 assert_eq!(
1485 try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
1486 None,
1487 "path ending at package name with no relative file should return None"
1488 );
1489 }
1490
1491 #[test]
1492 fn test_try_pnpm_workspace_fallback_nested_dist_esm() {
1493 let src_path = PathBuf::from("/project/packages/ui/src/Button.ts");
1494 let mut path_to_id = FxHashMap::default();
1495 path_to_id.insert(src_path.as_path(), FileId(10));
1496
1497 let mut workspace_roots = FxHashMap::default();
1498 let ws_root = PathBuf::from("/project/packages/ui");
1499 workspace_roots.insert("@myorg/ui", ws_root.as_path());
1500
1501 let pnpm_path = PathBuf::from(
1503 "/project/node_modules/.pnpm/@myorg+ui@1.0.0/node_modules/@myorg/ui/dist/esm/Button.mjs",
1504 );
1505 assert_eq!(
1506 try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
1507 Some(FileId(10)),
1508 "pnpm path with nested dist/esm should resolve through source fallback"
1509 );
1510 }
1511}