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