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 Some(canonical) = ctx.canonicalize_cache.get(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 Some(canonical) = ctx.canonicalize_cache.get(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 Some(canonical) = ctx.canonicalize_cache.get(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 = ctx.canonicalize_cache.get(&candidate);
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
537pub(super) fn 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
691pub(super) fn 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
828pub(super) fn lookup_internal_file_id(
829 ctx: &ResolveContext<'_>,
830 candidate: &Path,
831) -> Option<FileId> {
832 if let Some(&file_id) = ctx.raw_path_to_id.get(candidate) {
833 return Some(file_id);
834 }
835 if let Some(&file_id) = ctx.path_to_id.get(candidate) {
836 return Some(file_id);
837 }
838 #[cfg(not(miri))]
839 if let Some(canonical) = ctx.canonicalize_cache.get(candidate) {
840 if let Some(&file_id) = ctx.path_to_id.get(canonical.as_path()) {
841 return Some(file_id);
842 }
843 if let Some(fallback) = ctx.canonical_fallback
844 && let Some(file_id) = fallback.get(&canonical)
845 {
846 return Some(file_id);
847 }
848 }
849 None
850}
851
852pub fn extract_package_name_from_node_modules_path(path: &Path) -> Option<String> {
858 let components: Vec<&str> = path
859 .components()
860 .filter_map(|c| match c {
861 std::path::Component::Normal(s) => s.to_str(),
862 _ => None,
863 })
864 .collect();
865
866 let nm_idx = components.iter().rposition(|&c| c == "node_modules")?;
867
868 let after = &components[nm_idx + 1..];
869 if after.is_empty() {
870 return None;
871 }
872
873 if after[0].starts_with('@') {
874 if after.len() >= 2 {
875 Some(format!("{}/{}", after[0], after[1]))
876 } else {
877 Some(after[0].to_string())
878 }
879 } else {
880 Some(after[0].to_string())
881 }
882}
883
884pub(super) fn try_pnpm_workspace_fallback(
893 path: &Path,
894 path_to_id: &FxHashMap<&Path, FileId>,
895 workspace_roots: &FxHashMap<&str, &Path>,
896) -> Option<FileId> {
897 let components: Vec<&str> = path
898 .components()
899 .filter_map(|c| match c {
900 std::path::Component::Normal(s) => s.to_str(),
901 _ => None,
902 })
903 .collect();
904
905 let pnpm_idx = components.iter().position(|&c| c == ".pnpm")?;
906
907 let after_pnpm = &components[pnpm_idx + 1..];
908
909 let inner_nm_idx = after_pnpm.iter().position(|&c| c == "node_modules")?;
910 let after_inner_nm = &after_pnpm[inner_nm_idx + 1..];
911
912 if after_inner_nm.is_empty() {
913 return None;
914 }
915
916 let (pkg_name, pkg_name_components) = if after_inner_nm[0].starts_with('@') {
917 if after_inner_nm.len() >= 2 {
918 (format!("{}/{}", after_inner_nm[0], after_inner_nm[1]), 2)
919 } else {
920 return None;
921 }
922 } else {
923 (after_inner_nm[0].to_string(), 1)
924 };
925
926 let ws_root = workspace_roots.get(pkg_name.as_str())?;
927
928 let relative_parts = &after_inner_nm[pkg_name_components..];
929 if relative_parts.is_empty() {
930 return None;
931 }
932
933 let relative_path: PathBuf = relative_parts.iter().collect();
934
935 let direct = ws_root.join(&relative_path);
936 if let Some(&file_id) = path_to_id.get(direct.as_path()) {
937 return Some(file_id);
938 }
939
940 try_source_fallback(&direct, path_to_id)
941}
942
943pub(super) fn try_workspace_package_fallback(
966 ctx: &ResolveContext<'_>,
967 specifier: &str,
968) -> Option<ResolveResult> {
969 if !super::path_info::is_bare_specifier(specifier) {
970 return None;
971 }
972 let pkg_name = super::path_info::extract_package_name(specifier);
973
974 let subpath = specifier
975 .strip_prefix(pkg_name.as_str())
976 .and_then(|s| s.strip_prefix('/'))
977 .unwrap_or("");
978 let source_subpath = PathBuf::from(subpath);
979
980 match try_manifest_workspace_resolution(ctx, &pkg_name, subpath, &source_subpath) {
981 ManifestWorkspaceResolution::Resolved(result) => return Some(result),
982 ManifestWorkspaceResolution::Blocked => return None,
983 ManifestWorkspaceResolution::Continue => {}
984 }
985
986 let ws_root =
987 if let Some(manifest) = find_package_manifest(ctx.package_manifests, pkg_name.as_str()) {
988 manifest.root.as_path()
989 } else {
990 *ctx.workspace_roots.get(pkg_name.as_str())?
991 };
992
993 resolve_workspace_self_reference(ctx, ws_root, subpath, pkg_name)
994}
995
996enum ManifestWorkspaceResolution {
999 Resolved(ResolveResult),
1001 Blocked,
1003 Continue,
1005}
1006
1007fn try_manifest_workspace_resolution(
1010 ctx: &ResolveContext<'_>,
1011 pkg_name: &str,
1012 subpath: &str,
1013 source_subpath: &Path,
1014) -> ManifestWorkspaceResolution {
1015 let Some(manifest) = find_package_manifest(ctx.package_manifests, pkg_name) else {
1016 return ManifestWorkspaceResolution::Continue;
1017 };
1018
1019 if let Some(exports) = manifest.package_json.exports.as_ref() {
1020 let export_key = if subpath.is_empty() {
1021 ".".to_string()
1022 } else {
1023 format!("./{subpath}")
1024 };
1025 return match package_map_target(exports, &export_key, ctx.condition_names) {
1026 PackageMapTarget::Targets(targets) => {
1027 match resolve_package_map_targets(ctx, manifest, &targets, Some(source_subpath)) {
1028 Some(file_id) => ManifestWorkspaceResolution::Resolved(
1029 ResolveResult::InternalPackageModule {
1030 file_id,
1031 package_name: pkg_name.to_string(),
1032 },
1033 ),
1034 None => ManifestWorkspaceResolution::Continue,
1035 }
1036 }
1037 PackageMapTarget::NoMatch | PackageMapTarget::Blocked => {
1038 ManifestWorkspaceResolution::Blocked
1039 }
1040 };
1041 }
1042
1043 if let Some(file_id) = try_source_subpath(ctx, manifest, source_subpath) {
1044 return ManifestWorkspaceResolution::Resolved(ResolveResult::InternalPackageModule {
1045 file_id,
1046 package_name: pkg_name.to_string(),
1047 });
1048 }
1049
1050 ManifestWorkspaceResolution::Continue
1051}
1052
1053fn resolve_workspace_self_reference(
1057 ctx: &ResolveContext<'_>,
1058 ws_root: &Path,
1059 subpath: &str,
1060 package_name: String,
1061) -> Option<ResolveResult> {
1062 let root_file = ws_root.join("__fallow_ws_self_resolve__");
1063 let rel_spec = if subpath.is_empty() {
1064 "./".to_string()
1065 } else {
1066 format!("./{subpath}")
1067 };
1068
1069 let resolved = ctx.resolver.resolve_file(&root_file, &rel_spec).ok()?;
1070 let resolved_path = resolved.path();
1071
1072 if let Some(&file_id) = ctx.raw_path_to_id.get(resolved_path) {
1073 return Some(ResolveResult::InternalPackageModule {
1074 file_id,
1075 package_name,
1076 });
1077 }
1078 if let Some(canonical) = ctx.canonicalize_cache.get(resolved_path) {
1079 if let Some(&file_id) = ctx.path_to_id.get(canonical.as_path()) {
1080 return Some(ResolveResult::InternalPackageModule {
1081 file_id,
1082 package_name,
1083 });
1084 }
1085 if let Some(fallback) = ctx.canonical_fallback
1086 && let Some(file_id) = fallback.get(&canonical)
1087 {
1088 return Some(ResolveResult::InternalPackageModule {
1089 file_id,
1090 package_name,
1091 });
1092 }
1093 if let Some(file_id) = try_source_fallback(&canonical, ctx.path_to_id) {
1094 return Some(ResolveResult::InternalPackageModule {
1095 file_id,
1096 package_name,
1097 });
1098 }
1099 }
1100 None
1101}
1102
1103pub(super) fn make_glob_from_pattern(
1105 pattern: &fallow_types::extract::DynamicImportPattern,
1106) -> String {
1107 if pattern.prefix.contains('*') || pattern.prefix.contains('{') {
1108 return pattern.prefix.clone();
1109 }
1110 pattern.suffix.as_ref().map_or_else(
1111 || format!("{}*", pattern.prefix),
1112 |suffix| format!("{}*{}", pattern.prefix, suffix),
1113 )
1114}
1115
1116#[cfg(test)]
1117mod tests {
1118 use super::*;
1119 use crate::resolve::types::{CanonicalizeCache, TsconfigCache};
1120 use fallow_types::extract::ModuleLoadMechanism;
1121 use rustc_hash::FxHashSet;
1122
1123 fn with_package_map_ctx(
1124 root: PathBuf,
1125 name: Option<&str>,
1126 package_json: fallow_config::PackageJson,
1127 raw_files: &[(PathBuf, FileId)],
1128 f: impl FnOnce(&ResolveContext<'_>, &PackageManifestInfo, &Path),
1129 ) {
1130 let manifest = PackageManifestInfo {
1131 root: root.clone(),
1132 canonical_root: root,
1133 name: name.map(str::to_string),
1134 package_json,
1135 deno_import_map: Vec::new(),
1136 };
1137 let manifests = [manifest];
1138 let mut raw_path_to_id = FxHashMap::default();
1139 for (path, file_id) in raw_files {
1140 raw_path_to_id.insert(path.as_path(), *file_id);
1141 }
1142 let path_to_id: FxHashMap<&Path, FileId> = FxHashMap::default();
1143 let workspace_roots: FxHashMap<&str, &Path> = FxHashMap::default();
1144 let condition_names = conditions();
1145 let resolver = oxc_resolver::Resolver::new(oxc_resolver::ResolveOptions::default());
1146 let tsconfig_warned = std::sync::Mutex::new(FxHashSet::default());
1147 let tsconfig_cache = TsconfigCache::default();
1148 let canonicalize_cache = CanonicalizeCache::default();
1149 let ctx = ResolveContext {
1150 resolver: &resolver,
1151 style_resolver: &resolver,
1152 extensions: &[],
1153 path_to_id: &path_to_id,
1154 raw_path_to_id: &raw_path_to_id,
1155 workspace_roots: &workspace_roots,
1156 package_manifests: &manifests,
1157 has_deno_import_maps: false,
1158 condition_names: &condition_names,
1159 path_aliases: &[],
1160 scss_include_paths: &[],
1161 static_dir_mappings: &[],
1162 framework_static_dir_mappings: &[],
1163 root: &manifests[0].root,
1164 canonical_fallback: None,
1165 tsconfig_warned: &tsconfig_warned,
1166 tsconfig_cache: &tsconfig_cache,
1167 canonicalize_cache: &canonicalize_cache,
1168 };
1169
1170 f(&ctx, &manifests[0], &manifests[0].root);
1171 }
1172
1173 #[test]
1174 fn alias_match_remainder_exact_key() {
1175 assert_eq!(alias_match_remainder("vscode", "vscode"), Some(""));
1176 assert_eq!(alias_match_remainder("@scope/sdk", "@scope/sdk"), Some(""));
1177 }
1178
1179 #[test]
1180 fn alias_match_remainder_slash_continuation() {
1181 assert_eq!(
1182 alias_match_remainder("@scope/sdk/sub", "@scope/sdk"),
1183 Some("/sub")
1184 );
1185 assert_eq!(alias_match_remainder("@/foo", "@/"), Some("foo"));
1186 assert_eq!(
1187 alias_match_remainder("~/components/x", "~/"),
1188 Some("components/x")
1189 );
1190 assert_eq!(alias_match_remainder("$lib/util", "$lib/"), Some("util"));
1191 }
1192
1193 #[test]
1194 fn alias_match_remainder_rejects_prefix_collision() {
1195 assert_eq!(
1196 alias_match_remainder("@scope/sdk-extra", "@scope/sdk"),
1197 None
1198 );
1199 assert_eq!(
1200 alias_match_remainder("vscode-languageserver", "vscode"),
1201 None
1202 );
1203 assert_eq!(alias_match_remainder("#shared-utils", "#shared"), None);
1204 }
1205
1206 #[test]
1207 fn alias_match_remainder_non_match() {
1208 assert_eq!(alias_match_remainder("react", "vscode"), None);
1209 }
1210
1211 #[test]
1212 fn test_extract_package_name_from_node_modules_path_regular() {
1213 let path = PathBuf::from("/project/node_modules/react/index.js");
1214 assert_eq!(
1215 extract_package_name_from_node_modules_path(&path),
1216 Some("react".to_string())
1217 );
1218 }
1219
1220 #[test]
1221 fn test_extract_package_name_from_node_modules_path_scoped() {
1222 let path = PathBuf::from("/project/node_modules/@babel/core/lib/index.js");
1223 assert_eq!(
1224 extract_package_name_from_node_modules_path(&path),
1225 Some("@babel/core".to_string())
1226 );
1227 }
1228
1229 #[test]
1230 fn test_extract_package_name_from_node_modules_path_nested() {
1231 let path = PathBuf::from("/project/node_modules/pkg-a/node_modules/pkg-b/dist/index.js");
1232 assert_eq!(
1233 extract_package_name_from_node_modules_path(&path),
1234 Some("pkg-b".to_string())
1235 );
1236 }
1237
1238 #[test]
1239 fn test_extract_package_name_from_node_modules_path_deep_subpath() {
1240 let path = PathBuf::from("/project/node_modules/react-dom/cjs/react-dom.production.min.js");
1241 assert_eq!(
1242 extract_package_name_from_node_modules_path(&path),
1243 Some("react-dom".to_string())
1244 );
1245 }
1246
1247 #[test]
1248 fn test_extract_package_name_from_node_modules_path_no_node_modules() {
1249 let path = PathBuf::from("/project/src/components/Button.tsx");
1250 assert_eq!(extract_package_name_from_node_modules_path(&path), None);
1251 }
1252
1253 #[test]
1254 fn test_extract_package_name_from_node_modules_path_just_node_modules() {
1255 let path = PathBuf::from("/project/node_modules");
1256 assert_eq!(extract_package_name_from_node_modules_path(&path), None);
1257 }
1258
1259 #[test]
1260 fn test_extract_package_name_from_node_modules_path_scoped_only_scope() {
1261 let path = PathBuf::from("/project/node_modules/@scope");
1262 assert_eq!(
1263 extract_package_name_from_node_modules_path(&path),
1264 Some("@scope".to_string())
1265 );
1266 }
1267
1268 #[test]
1269 fn test_resolve_specifier_node_modules_returns_npm_package() {
1270 let path =
1271 PathBuf::from("/project/node_modules/styled-components/dist/styled-components.esm.js");
1272 assert_eq!(
1273 extract_package_name_from_node_modules_path(&path),
1274 Some("styled-components".to_string())
1275 );
1276
1277 let path = PathBuf::from("/project/node_modules/next/dist/server/next.js");
1278 assert_eq!(
1279 extract_package_name_from_node_modules_path(&path),
1280 Some("next".to_string())
1281 );
1282 }
1283
1284 #[test]
1285 fn test_try_source_fallback_dist_to_src() {
1286 let src_path = PathBuf::from("/project/packages/ui/src/utils.ts");
1287 let mut path_to_id = FxHashMap::default();
1288 path_to_id.insert(src_path.as_path(), FileId(0));
1289
1290 let dist_path = PathBuf::from("/project/packages/ui/dist/utils.js");
1291 assert_eq!(
1292 try_source_fallback(&dist_path, &path_to_id),
1293 Some(FileId(0)),
1294 "dist/utils.js should fall back to src/utils.ts"
1295 );
1296 }
1297
1298 #[test]
1299 fn test_try_source_fallback_build_to_src() {
1300 let src_path = PathBuf::from("/project/packages/core/src/index.tsx");
1301 let mut path_to_id = FxHashMap::default();
1302 path_to_id.insert(src_path.as_path(), FileId(1));
1303
1304 let build_path = PathBuf::from("/project/packages/core/build/index.js");
1305 assert_eq!(
1306 try_source_fallback(&build_path, &path_to_id),
1307 Some(FileId(1)),
1308 "build/index.js should fall back to src/index.tsx"
1309 );
1310 }
1311
1312 #[test]
1313 fn test_try_source_fallback_no_match() {
1314 let path_to_id: FxHashMap<&Path, FileId> = FxHashMap::default();
1315
1316 let dist_path = PathBuf::from("/project/packages/ui/dist/utils.js");
1317 assert_eq!(
1318 try_source_fallback(&dist_path, &path_to_id),
1319 None,
1320 "should return None when no source file exists"
1321 );
1322 }
1323
1324 #[test]
1325 fn test_try_source_fallback_non_output_dir() {
1326 let src_path = PathBuf::from("/project/packages/ui/src/utils.ts");
1327 let mut path_to_id = FxHashMap::default();
1328 path_to_id.insert(src_path.as_path(), FileId(0));
1329
1330 let normal_path = PathBuf::from("/project/packages/ui/scripts/utils.js");
1331 assert_eq!(
1332 try_source_fallback(&normal_path, &path_to_id),
1333 None,
1334 "non-output directory path should not trigger fallback"
1335 );
1336 }
1337
1338 #[test]
1339 fn test_try_source_fallback_nested_path() {
1340 let src_path = PathBuf::from("/project/packages/ui/src/components/Button.ts");
1341 let mut path_to_id = FxHashMap::default();
1342 path_to_id.insert(src_path.as_path(), FileId(2));
1343
1344 let dist_path = PathBuf::from("/project/packages/ui/dist/components/Button.js");
1345 assert_eq!(
1346 try_source_fallback(&dist_path, &path_to_id),
1347 Some(FileId(2)),
1348 "nested dist path should fall back to nested src path"
1349 );
1350 }
1351
1352 #[test]
1353 fn test_try_source_fallback_nested_dist_esm() {
1354 let src_path = PathBuf::from("/project/packages/ui/src/utils.ts");
1355 let mut path_to_id = FxHashMap::default();
1356 path_to_id.insert(src_path.as_path(), FileId(0));
1357
1358 let dist_path = PathBuf::from("/project/packages/ui/dist/esm/utils.mjs");
1359 assert_eq!(
1360 try_source_fallback(&dist_path, &path_to_id),
1361 Some(FileId(0)),
1362 "dist/esm/utils.mjs should fall back to src/utils.ts"
1363 );
1364 }
1365
1366 #[test]
1367 fn test_try_source_fallback_nested_build_cjs() {
1368 let src_path = PathBuf::from("/project/packages/core/src/index.ts");
1369 let mut path_to_id = FxHashMap::default();
1370 path_to_id.insert(src_path.as_path(), FileId(1));
1371
1372 let build_path = PathBuf::from("/project/packages/core/build/cjs/index.cjs");
1373 assert_eq!(
1374 try_source_fallback(&build_path, &path_to_id),
1375 Some(FileId(1)),
1376 "build/cjs/index.cjs should fall back to src/index.ts"
1377 );
1378 }
1379
1380 #[test]
1381 fn test_try_source_fallback_nested_dist_esm_deep_path() {
1382 let src_path = PathBuf::from("/project/packages/ui/src/components/Button.ts");
1383 let mut path_to_id = FxHashMap::default();
1384 path_to_id.insert(src_path.as_path(), FileId(2));
1385
1386 let dist_path = PathBuf::from("/project/packages/ui/dist/esm/components/Button.mjs");
1387 assert_eq!(
1388 try_source_fallback(&dist_path, &path_to_id),
1389 Some(FileId(2)),
1390 "dist/esm/components/Button.mjs should fall back to src/components/Button.ts"
1391 );
1392 }
1393
1394 #[test]
1395 fn test_try_source_fallback_triple_nested_output_dirs() {
1396 let src_path = PathBuf::from("/project/packages/ui/src/utils.ts");
1397 let mut path_to_id = FxHashMap::default();
1398 path_to_id.insert(src_path.as_path(), FileId(0));
1399
1400 let dist_path = PathBuf::from("/project/packages/ui/out/dist/esm/utils.mjs");
1401 assert_eq!(
1402 try_source_fallback(&dist_path, &path_to_id),
1403 Some(FileId(0)),
1404 "out/dist/esm/utils.mjs should fall back to src/utils.ts"
1405 );
1406 }
1407
1408 #[test]
1409 fn test_try_source_fallback_parent_dir_named_build() {
1410 let src_path = PathBuf::from("/home/user/build/my-project/src/utils.ts");
1411 let mut path_to_id = FxHashMap::default();
1412 path_to_id.insert(src_path.as_path(), FileId(0));
1413
1414 let dist_path = PathBuf::from("/home/user/build/my-project/dist/utils.js");
1415 assert_eq!(
1416 try_source_fallback(&dist_path, &path_to_id),
1417 Some(FileId(0)),
1418 "should resolve dist/ within project, not match parent 'build' dir"
1419 );
1420 }
1421
1422 #[test]
1423 fn package_map_exact_entry_beats_pattern_entry() {
1424 let map = serde_json::json!({
1425 "#nitro/runtime/task": "./dist/special/task.mjs",
1426 "#nitro/runtime/*": "./dist/runtime/internal/*.mjs"
1427 });
1428 assert_eq!(
1429 package_map_target(&map, "#nitro/runtime/task", &conditions()),
1430 PackageMapTarget::Targets(vec!["./dist/special/task.mjs".to_string()])
1431 );
1432 }
1433
1434 #[test]
1435 fn package_map_wildcard_substitutes_capture() {
1436 let map = serde_json::json!({
1437 "#nitro/runtime/*": "./dist/runtime/internal/*.mjs"
1438 });
1439 assert_eq!(
1440 package_map_target(&map, "#nitro/runtime/task", &conditions()),
1441 PackageMapTarget::Targets(vec!["./dist/runtime/internal/task.mjs".to_string()])
1442 );
1443 }
1444
1445 #[test]
1446 fn package_map_exact_entry_with_no_target_blocks_pattern_entry() {
1447 let map = serde_json::json!({
1448 "#nitro/runtime/task": null,
1449 "#nitro/runtime/*": "./dist/runtime/internal/*.mjs"
1450 });
1451 assert_eq!(
1452 package_map_target(&map, "#nitro/runtime/task", &conditions()),
1453 PackageMapTarget::Blocked
1454 );
1455 }
1456
1457 #[test]
1458 fn package_map_best_pattern_with_no_target_blocks_broader_pattern() {
1459 let map = serde_json::json!({
1460 "#nitro/runtime/internal/*": null,
1461 "#nitro/runtime/*": "./dist/runtime/*.mjs"
1462 });
1463 assert_eq!(
1464 package_map_target(&map, "#nitro/runtime/internal/task", &conditions()),
1465 PackageMapTarget::Blocked
1466 );
1467 }
1468
1469 #[test]
1470 fn package_map_unmatched_subpath_is_not_a_target() {
1471 let map = serde_json::json!({
1472 "./query": "./dist/query/index.js"
1473 });
1474 assert_eq!(
1475 package_map_target(&map, "./private", &conditions()),
1476 PackageMapTarget::NoMatch
1477 );
1478 }
1479
1480 #[test]
1481 fn package_map_nested_conditions_follow_manifest_order() {
1482 let map = serde_json::json!({
1483 "./query/react": {
1484 "types": "./dist/query/react/index.d.ts",
1485 "import": {
1486 "development": "./src/query/react/index.ts",
1487 "default": "./dist/query/react/index.js"
1488 },
1489 "default": "./dist/query/react/index.cjs"
1490 }
1491 });
1492 assert_eq!(
1493 package_map_target(&map, "./query/react", &conditions()),
1494 PackageMapTarget::Targets(vec!["./dist/query/react/index.d.ts".to_string()])
1495 );
1496 }
1497
1498 #[test]
1499 fn package_map_import_before_types_selects_runtime_branch() {
1500 let map = serde_json::json!({
1501 ".": {
1502 "import": "./dist/index.js",
1503 "types": "./dist/index.d.ts"
1504 }
1505 });
1506 assert_eq!(
1507 package_map_target(&map, ".", &conditions()),
1508 PackageMapTarget::Targets(vec!["./dist/index.js".to_string()])
1509 );
1510 }
1511
1512 #[test]
1513 fn package_map_condition_order_follows_manifest_order() {
1514 let map = serde_json::json!({
1515 ".": {
1516 "node": "./dist/node.js",
1517 "import": "./dist/index.js"
1518 }
1519 });
1520 assert_eq!(
1521 package_map_target(&map, ".", &conditions()),
1522 PackageMapTarget::Targets(vec!["./dist/node.js".to_string()])
1523 );
1524 }
1525
1526 #[test]
1527 fn package_map_arrays_preserve_fallback_order() {
1528 let map = serde_json::json!({
1529 "#array": ["./dist/missing.js", "./src/array.ts"],
1530 "#null": null,
1531 "#false": false
1532 });
1533 assert_eq!(
1534 package_map_target(&map, "#array", &conditions()),
1535 PackageMapTarget::Targets(vec![
1536 "./dist/missing.js".to_string(),
1537 "./src/array.ts".to_string()
1538 ])
1539 );
1540 assert_eq!(
1541 package_map_target(&map, "#null", &conditions()),
1542 PackageMapTarget::Blocked
1543 );
1544 assert_eq!(
1545 package_map_target(&map, "#false", &conditions()),
1546 PackageMapTarget::Blocked
1547 );
1548 }
1549
1550 #[test]
1551 fn package_map_non_relative_target_does_not_trigger_source_fallback() {
1552 with_package_map_ctx(
1553 PathBuf::from("/project"),
1554 Some("pkg"),
1555 fallow_config::PackageJson::default(),
1556 &[],
1557 |ctx, manifest, _| {
1558 assert!(resolve_package_map_target(ctx, manifest, "lodash", None).is_none());
1559 assert!(
1560 resolve_package_map_target(ctx, manifest, "../dist/index.js", None).is_none()
1561 );
1562 },
1563 );
1564 }
1565
1566 #[test]
1567 fn package_map_targets_use_first_reachable_target() {
1568 let root = PathBuf::from("/project");
1569 let src_path = root.join("src/feature.ts");
1570 let targets = vec![
1571 "./dist/missing.js".to_string(),
1572 "./src/feature.ts".to_string(),
1573 ];
1574
1575 with_package_map_ctx(
1576 root,
1577 Some("pkg"),
1578 fallow_config::PackageJson::default(),
1579 &[(src_path, FileId(9))],
1580 |ctx, manifest, _| {
1581 assert_eq!(
1582 resolve_package_map_targets(ctx, manifest, &targets, None),
1583 Some(FileId(9))
1584 );
1585 },
1586 );
1587 }
1588
1589 #[test]
1590 fn package_imports_fallback_supports_external_package_targets() {
1591 let root = PathBuf::from("/project");
1592 with_package_map_ctx(
1593 root,
1594 Some("pkg"),
1595 fallow_config::PackageJson {
1596 imports: Some(serde_json::json!({
1597 "#pad": "left-pad",
1598 "#scoped": "@scope/pkg/subpath"
1599 })),
1600 ..Default::default()
1601 },
1602 &[],
1603 |ctx, _, root| {
1604 let pad = try_package_imports_fallback(ctx, &root.join("src/index.ts"), "#pad");
1605 assert!(matches!(pad, Some(ResolveResult::NpmPackage(pkg)) if pkg == "left-pad"));
1606
1607 let scoped =
1608 try_package_imports_fallback(ctx, &root.join("src/index.ts"), "#scoped");
1609 assert!(
1610 matches!(scoped, Some(ResolveResult::NpmPackage(pkg)) if pkg == "@scope/pkg")
1611 );
1612 },
1613 );
1614 }
1615
1616 #[test]
1617 fn package_imports_fallback_supports_unnamed_packages() {
1618 let root = PathBuf::from("/project");
1619 let src_path = root.join("src/runtime/task.ts");
1620 with_package_map_ctx(
1621 root,
1622 None,
1623 fallow_config::PackageJson {
1624 imports: Some(serde_json::json!({
1625 "#runtime/*": "./dist/runtime/*.mjs"
1626 })),
1627 ..Default::default()
1628 },
1629 &[(src_path, FileId(7))],
1630 |ctx, _, root| {
1631 let result =
1632 try_package_imports_fallback(ctx, &root.join("src/index.ts"), "#runtime/task");
1633 assert!(matches!(
1634 result,
1635 Some(ResolveResult::InternalModule(FileId(7)))
1636 ));
1637 },
1638 );
1639 }
1640
1641 #[test]
1642 #[cfg_attr(miri, ignore)]
1643 fn relative_package_root_source_fallback_uses_package_source_entry() {
1644 let root = PathBuf::from("/project");
1645 let source_path = root.join("custom/entry.js");
1646 with_package_map_ctx(
1647 root,
1648 Some("pkg"),
1649 fallow_config::PackageJson {
1650 source: Some("custom/entry.js".to_string()),
1651 ..Default::default()
1652 },
1653 &[(source_path, FileId(11))],
1654 |ctx, _, root| {
1655 let result = try_relative_package_root_source_fallback(
1656 ctx,
1657 &root.join("test/shared/exports.test.js"),
1658 "../../",
1659 );
1660 assert!(matches!(
1661 result,
1662 Some(ResolveResult::InternalModule(FileId(11)))
1663 ));
1664 },
1665 );
1666 }
1667
1668 #[test]
1669 fn package_source_path_accepts_relative_source_entries() {
1670 assert_eq!(
1671 safe_relative_package_source_path("src/index.js"),
1672 Some(Path::new("src/index.js"))
1673 );
1674 assert_eq!(
1675 safe_relative_package_source_path("./custom/entry.ts"),
1676 Some(Path::new("custom/entry.ts"))
1677 );
1678 }
1679
1680 #[test]
1681 fn package_source_path_rejects_unsafe_entries() {
1682 assert_eq!(safe_relative_package_source_path(""), None);
1683 assert_eq!(safe_relative_package_source_path("./"), None);
1684 assert_eq!(safe_relative_package_source_path("../src/index.js"), None);
1685 assert_eq!(safe_relative_package_source_path("src/../index.js"), None);
1686 assert_eq!(safe_relative_package_source_path("/src/index.js"), None);
1687
1688 #[cfg(windows)]
1689 assert_eq!(safe_relative_package_source_path("C:\\src\\index.js"), None);
1690 }
1691
1692 #[test]
1693 fn test_pnpm_store_path_extract_package_name() {
1694 let path =
1695 PathBuf::from("/project/node_modules/.pnpm/react@18.2.0/node_modules/react/index.js");
1696 assert_eq!(
1697 extract_package_name_from_node_modules_path(&path),
1698 Some("react".to_string())
1699 );
1700 }
1701
1702 #[test]
1703 fn test_pnpm_store_path_scoped_package() {
1704 let path = PathBuf::from(
1705 "/project/node_modules/.pnpm/@babel+core@7.24.0/node_modules/@babel/core/lib/index.js",
1706 );
1707 assert_eq!(
1708 extract_package_name_from_node_modules_path(&path),
1709 Some("@babel/core".to_string())
1710 );
1711 }
1712
1713 fn conditions() -> Vec<String> {
1714 vec![
1715 "development".to_string(),
1716 "import".to_string(),
1717 "require".to_string(),
1718 "default".to_string(),
1719 "types".to_string(),
1720 "node".to_string(),
1721 ]
1722 }
1723
1724 #[test]
1725 fn test_pnpm_store_path_with_peer_deps() {
1726 let path = PathBuf::from(
1727 "/project/node_modules/.pnpm/webpack@5.0.0_esbuild@0.19.0/node_modules/webpack/lib/index.js",
1728 );
1729 assert_eq!(
1730 extract_package_name_from_node_modules_path(&path),
1731 Some("webpack".to_string())
1732 );
1733 }
1734
1735 #[test]
1736 fn test_try_pnpm_workspace_fallback_dist_to_src() {
1737 let src_path = PathBuf::from("/project/packages/ui/src/utils.ts");
1738 let mut path_to_id = FxHashMap::default();
1739 path_to_id.insert(src_path.as_path(), FileId(0));
1740
1741 let mut workspace_roots = FxHashMap::default();
1742 let ws_root = PathBuf::from("/project/packages/ui");
1743 workspace_roots.insert("@myorg/ui", ws_root.as_path());
1744
1745 let pnpm_path = PathBuf::from(
1746 "/project/node_modules/.pnpm/@myorg+ui@1.0.0/node_modules/@myorg/ui/dist/utils.js",
1747 );
1748 assert_eq!(
1749 try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
1750 Some(FileId(0)),
1751 ".pnpm workspace path should fall back to src/utils.ts"
1752 );
1753 }
1754
1755 #[test]
1756 fn test_try_pnpm_workspace_fallback_direct_source() {
1757 let src_path = PathBuf::from("/project/packages/core/src/index.ts");
1758 let mut path_to_id = FxHashMap::default();
1759 path_to_id.insert(src_path.as_path(), FileId(1));
1760
1761 let mut workspace_roots = FxHashMap::default();
1762 let ws_root = PathBuf::from("/project/packages/core");
1763 workspace_roots.insert("@myorg/core", ws_root.as_path());
1764
1765 let pnpm_path = PathBuf::from(
1766 "/project/node_modules/.pnpm/@myorg+core@workspace/node_modules/@myorg/core/src/index.ts",
1767 );
1768 assert_eq!(
1769 try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
1770 Some(FileId(1)),
1771 ".pnpm workspace path with src/ should resolve directly"
1772 );
1773 }
1774
1775 #[test]
1776 fn test_try_pnpm_workspace_fallback_non_workspace_package() {
1777 let path_to_id: FxHashMap<&Path, FileId> = FxHashMap::default();
1778
1779 let mut workspace_roots = FxHashMap::default();
1780 let ws_root = PathBuf::from("/project/packages/ui");
1781 workspace_roots.insert("@myorg/ui", ws_root.as_path());
1782
1783 let pnpm_path =
1784 PathBuf::from("/project/node_modules/.pnpm/react@18.2.0/node_modules/react/index.js");
1785 assert_eq!(
1786 try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
1787 None,
1788 "non-workspace package in .pnpm should return None"
1789 );
1790 }
1791
1792 #[test]
1793 fn test_try_pnpm_workspace_fallback_unscoped_package() {
1794 let src_path = PathBuf::from("/project/packages/utils/src/index.ts");
1795 let mut path_to_id = FxHashMap::default();
1796 path_to_id.insert(src_path.as_path(), FileId(2));
1797
1798 let mut workspace_roots = FxHashMap::default();
1799 let ws_root = PathBuf::from("/project/packages/utils");
1800 workspace_roots.insert("my-utils", ws_root.as_path());
1801
1802 let pnpm_path = PathBuf::from(
1803 "/project/node_modules/.pnpm/my-utils@1.0.0/node_modules/my-utils/dist/index.js",
1804 );
1805 assert_eq!(
1806 try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
1807 Some(FileId(2)),
1808 "unscoped workspace package in .pnpm should resolve"
1809 );
1810 }
1811
1812 #[test]
1813 fn test_try_pnpm_workspace_fallback_nested_path() {
1814 let src_path = PathBuf::from("/project/packages/ui/src/components/Button.ts");
1815 let mut path_to_id = FxHashMap::default();
1816 path_to_id.insert(src_path.as_path(), FileId(3));
1817
1818 let mut workspace_roots = FxHashMap::default();
1819 let ws_root = PathBuf::from("/project/packages/ui");
1820 workspace_roots.insert("@myorg/ui", ws_root.as_path());
1821
1822 let pnpm_path = PathBuf::from(
1823 "/project/node_modules/.pnpm/@myorg+ui@1.0.0/node_modules/@myorg/ui/dist/components/Button.js",
1824 );
1825 assert_eq!(
1826 try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
1827 Some(FileId(3)),
1828 "nested .pnpm workspace path should resolve through source fallback"
1829 );
1830 }
1831
1832 #[test]
1833 fn test_try_pnpm_workspace_fallback_no_pnpm() {
1834 let path_to_id: FxHashMap<&Path, FileId> = FxHashMap::default();
1835 let workspace_roots: FxHashMap<&str, &Path> = FxHashMap::default();
1836
1837 let regular_path = PathBuf::from("/project/node_modules/react/index.js");
1838 assert_eq!(
1839 try_pnpm_workspace_fallback(®ular_path, &path_to_id, &workspace_roots),
1840 None,
1841 );
1842 }
1843
1844 #[test]
1845 fn test_try_pnpm_workspace_fallback_with_peer_deps() {
1846 let src_path = PathBuf::from("/project/packages/ui/src/index.ts");
1847 let mut path_to_id = FxHashMap::default();
1848 path_to_id.insert(src_path.as_path(), FileId(4));
1849
1850 let mut workspace_roots = FxHashMap::default();
1851 let ws_root = PathBuf::from("/project/packages/ui");
1852 workspace_roots.insert("@myorg/ui", ws_root.as_path());
1853
1854 let pnpm_path = PathBuf::from(
1855 "/project/node_modules/.pnpm/@myorg+ui@1.0.0_react@18.2.0/node_modules/@myorg/ui/dist/index.js",
1856 );
1857 assert_eq!(
1858 try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
1859 Some(FileId(4)),
1860 ".pnpm path with peer dep suffix should still resolve"
1861 );
1862 }
1863
1864 #[test]
1865 fn make_glob_prefix_only_no_suffix() {
1866 let pattern = fallow_types::extract::DynamicImportPattern {
1867 prefix: "./locales/".to_string(),
1868 suffix: None,
1869 span: oxc_span::Span::default(),
1870 mechanism: ModuleLoadMechanism::EsModule,
1871 };
1872 assert_eq!(make_glob_from_pattern(&pattern), "./locales/*");
1873 }
1874
1875 #[test]
1876 fn make_glob_prefix_with_suffix() {
1877 let pattern = fallow_types::extract::DynamicImportPattern {
1878 prefix: "./locales/".to_string(),
1879 suffix: Some(".json".to_string()),
1880 span: oxc_span::Span::default(),
1881 mechanism: ModuleLoadMechanism::EsModule,
1882 };
1883 assert_eq!(make_glob_from_pattern(&pattern), "./locales/*.json");
1884 }
1885
1886 #[test]
1887 fn make_glob_passthrough_star() {
1888 let pattern = fallow_types::extract::DynamicImportPattern {
1889 prefix: "./pages/**/*.tsx".to_string(),
1890 suffix: None,
1891 span: oxc_span::Span::default(),
1892 mechanism: ModuleLoadMechanism::EsModule,
1893 };
1894 assert_eq!(make_glob_from_pattern(&pattern), "./pages/**/*.tsx");
1895 }
1896
1897 #[test]
1898 fn make_glob_passthrough_brace() {
1899 let pattern = fallow_types::extract::DynamicImportPattern {
1900 prefix: "./i18n/{en,de,fr}.json".to_string(),
1901 suffix: None,
1902 span: oxc_span::Span::default(),
1903 mechanism: ModuleLoadMechanism::EsModule,
1904 };
1905 assert_eq!(make_glob_from_pattern(&pattern), "./i18n/{en,de,fr}.json");
1906 }
1907
1908 #[test]
1909 fn make_glob_empty_prefix_no_suffix() {
1910 let pattern = fallow_types::extract::DynamicImportPattern {
1911 prefix: String::new(),
1912 suffix: None,
1913 span: oxc_span::Span::default(),
1914 mechanism: ModuleLoadMechanism::EsModule,
1915 };
1916 assert_eq!(make_glob_from_pattern(&pattern), "*");
1917 }
1918
1919 #[test]
1920 fn make_glob_empty_prefix_with_suffix() {
1921 let pattern = fallow_types::extract::DynamicImportPattern {
1922 prefix: String::new(),
1923 suffix: Some(".ts".to_string()),
1924 span: oxc_span::Span::default(),
1925 mechanism: ModuleLoadMechanism::EsModule,
1926 };
1927 assert_eq!(make_glob_from_pattern(&pattern), "*.ts");
1928 }
1929
1930 #[test]
1931 fn make_glob_template_literal_prefix_only() {
1932 let pattern = fallow_types::extract::DynamicImportPattern {
1933 prefix: "./pages/".to_string(),
1934 suffix: None,
1935 span: oxc_span::Span::default(),
1936 mechanism: ModuleLoadMechanism::EsModule,
1937 };
1938 assert_eq!(make_glob_from_pattern(&pattern), "./pages/*");
1939 }
1940
1941 #[test]
1942 fn make_glob_template_literal_with_extension_suffix() {
1943 let pattern = fallow_types::extract::DynamicImportPattern {
1944 prefix: "./locales/".to_string(),
1945 suffix: Some(".json".to_string()),
1946 span: oxc_span::Span::default(),
1947 mechanism: ModuleLoadMechanism::EsModule,
1948 };
1949 assert_eq!(make_glob_from_pattern(&pattern), "./locales/*.json");
1950 }
1951
1952 #[test]
1953 fn make_glob_template_literal_deep_prefix() {
1954 let pattern = fallow_types::extract::DynamicImportPattern {
1955 prefix: "./modules/".to_string(),
1956 suffix: None,
1957 span: oxc_span::Span::default(),
1958 mechanism: ModuleLoadMechanism::EsModule,
1959 };
1960 assert_eq!(make_glob_from_pattern(&pattern), "./modules/*");
1961 }
1962
1963 #[test]
1964 fn make_glob_string_concat_prefix() {
1965 let pattern = fallow_types::extract::DynamicImportPattern {
1966 prefix: "./pages/".to_string(),
1967 suffix: None,
1968 span: oxc_span::Span::default(),
1969 mechanism: ModuleLoadMechanism::EsModule,
1970 };
1971 assert_eq!(make_glob_from_pattern(&pattern), "./pages/*");
1972 }
1973
1974 #[test]
1975 fn make_glob_string_concat_with_extension() {
1976 let pattern = fallow_types::extract::DynamicImportPattern {
1977 prefix: "./views/".to_string(),
1978 suffix: Some(".vue".to_string()),
1979 span: oxc_span::Span::default(),
1980 mechanism: ModuleLoadMechanism::EsModule,
1981 };
1982 assert_eq!(make_glob_from_pattern(&pattern), "./views/*.vue");
1983 }
1984
1985 #[test]
1986 fn make_glob_import_meta_glob_recursive() {
1987 let pattern = fallow_types::extract::DynamicImportPattern {
1988 prefix: "./components/**/*.vue".to_string(),
1989 suffix: None,
1990 span: oxc_span::Span::default(),
1991 mechanism: ModuleLoadMechanism::EsModule,
1992 };
1993 assert_eq!(
1994 make_glob_from_pattern(&pattern),
1995 "./components/**/*.vue",
1996 "import.meta.glob patterns with * should pass through as-is"
1997 );
1998 }
1999
2000 #[test]
2001 fn make_glob_import_meta_glob_brace_expansion() {
2002 let pattern = fallow_types::extract::DynamicImportPattern {
2003 prefix: "./plugins/{auth,analytics}.ts".to_string(),
2004 suffix: None,
2005 span: oxc_span::Span::default(),
2006 mechanism: ModuleLoadMechanism::EsModule,
2007 };
2008 assert_eq!(
2009 make_glob_from_pattern(&pattern),
2010 "./plugins/{auth,analytics}.ts",
2011 "import.meta.glob patterns with braces should pass through as-is"
2012 );
2013 }
2014
2015 #[test]
2016 fn make_glob_import_meta_glob_star_with_brace() {
2017 let pattern = fallow_types::extract::DynamicImportPattern {
2018 prefix: "./routes/**/*.{ts,tsx}".to_string(),
2019 suffix: None,
2020 span: oxc_span::Span::default(),
2021 mechanism: ModuleLoadMechanism::EsModule,
2022 };
2023 assert_eq!(
2024 make_glob_from_pattern(&pattern),
2025 "./routes/**/*.{ts,tsx}",
2026 "combined * and brace patterns should pass through"
2027 );
2028 }
2029
2030 #[test]
2031 fn make_glob_import_meta_glob_ignores_suffix_when_star_present() {
2032 let pattern = fallow_types::extract::DynamicImportPattern {
2033 prefix: "./*.ts".to_string(),
2034 suffix: Some(".extra".to_string()),
2035 span: oxc_span::Span::default(),
2036 mechanism: ModuleLoadMechanism::EsModule,
2037 };
2038 assert_eq!(
2039 make_glob_from_pattern(&pattern),
2040 "./*.ts",
2041 "when prefix has glob chars, suffix is ignored (prefix used as-is)"
2042 );
2043 }
2044
2045 #[test]
2046 fn make_glob_single_dot_prefix() {
2047 let pattern = fallow_types::extract::DynamicImportPattern {
2048 prefix: "./".to_string(),
2049 suffix: None,
2050 span: oxc_span::Span::default(),
2051 mechanism: ModuleLoadMechanism::EsModule,
2052 };
2053 assert_eq!(make_glob_from_pattern(&pattern), "./*");
2054 }
2055
2056 #[test]
2057 fn make_glob_prefix_without_trailing_slash() {
2058 let pattern = fallow_types::extract::DynamicImportPattern {
2059 prefix: "./config".to_string(),
2060 suffix: None,
2061 span: oxc_span::Span::default(),
2062 mechanism: ModuleLoadMechanism::EsModule,
2063 };
2064 assert_eq!(make_glob_from_pattern(&pattern), "./config*");
2065 }
2066
2067 #[test]
2068 fn make_glob_prefix_with_dotdot() {
2069 let pattern = fallow_types::extract::DynamicImportPattern {
2070 prefix: "../shared/".to_string(),
2071 suffix: Some(".ts".to_string()),
2072 span: oxc_span::Span::default(),
2073 mechanism: ModuleLoadMechanism::EsModule,
2074 };
2075 assert_eq!(make_glob_from_pattern(&pattern), "../shared/*.ts");
2076 }
2077
2078 #[test]
2079 fn test_extract_package_name_with_pnpm_plus_encoded_scope() {
2080 let path = PathBuf::from(
2081 "/project/node_modules/.pnpm/@mui+material@5.15.0/node_modules/@mui/material/index.js",
2082 );
2083 assert_eq!(
2084 extract_package_name_from_node_modules_path(&path),
2085 Some("@mui/material".to_string())
2086 );
2087 }
2088
2089 #[test]
2090 fn test_extract_package_name_windows_style_path() {
2091 let path = PathBuf::from("/project/node_modules/typescript/lib/tsc.js");
2092 assert_eq!(
2093 extract_package_name_from_node_modules_path(&path),
2094 Some("typescript".to_string())
2095 );
2096 }
2097
2098 #[test]
2099 fn test_try_source_fallback_out_dir() {
2100 let src_path = PathBuf::from("/project/packages/api/src/handler.ts");
2101 let mut path_to_id = FxHashMap::default();
2102 path_to_id.insert(src_path.as_path(), FileId(5));
2103
2104 let out_path = PathBuf::from("/project/packages/api/out/handler.js");
2105 assert_eq!(
2106 try_source_fallback(&out_path, &path_to_id),
2107 Some(FileId(5)),
2108 "out/handler.js should fall back to src/handler.ts"
2109 );
2110 }
2111
2112 #[test]
2113 fn test_try_source_fallback_mts_extension() {
2114 let src_path = PathBuf::from("/project/packages/lib/src/utils.mts");
2115 let mut path_to_id = FxHashMap::default();
2116 path_to_id.insert(src_path.as_path(), FileId(6));
2117
2118 let dist_path = PathBuf::from("/project/packages/lib/dist/utils.mjs");
2119 assert_eq!(
2120 try_source_fallback(&dist_path, &path_to_id),
2121 Some(FileId(6)),
2122 "dist/utils.mjs should fall back to src/utils.mts"
2123 );
2124 }
2125
2126 #[test]
2127 fn test_try_source_fallback_cts_extension() {
2128 let src_path = PathBuf::from("/project/packages/lib/src/config.cts");
2129 let mut path_to_id = FxHashMap::default();
2130 path_to_id.insert(src_path.as_path(), FileId(7));
2131
2132 let dist_path = PathBuf::from("/project/packages/lib/dist/config.cjs");
2133 assert_eq!(
2134 try_source_fallback(&dist_path, &path_to_id),
2135 Some(FileId(7)),
2136 "dist/config.cjs should fall back to src/config.cts"
2137 );
2138 }
2139
2140 #[test]
2141 fn test_try_source_fallback_jsx_extension() {
2142 let src_path = PathBuf::from("/project/packages/ui/src/App.jsx");
2143 let mut path_to_id = FxHashMap::default();
2144 path_to_id.insert(src_path.as_path(), FileId(8));
2145
2146 let build_path = PathBuf::from("/project/packages/ui/build/App.js");
2147 assert_eq!(
2148 try_source_fallback(&build_path, &path_to_id),
2149 Some(FileId(8)),
2150 "build/App.js should fall back to src/App.jsx"
2151 );
2152 }
2153
2154 #[test]
2155 fn test_try_source_fallback_no_file_stem() {
2156 let path_to_id: FxHashMap<&Path, FileId> = FxHashMap::default();
2157 let dist_path = PathBuf::from("/project/packages/ui/dist/");
2158 assert_eq!(
2159 try_source_fallback(&dist_path, &path_to_id),
2160 None,
2161 "directory path with no file should return None"
2162 );
2163 }
2164
2165 #[test]
2166 fn test_try_source_fallback_esm_subdir() {
2167 let src_path = PathBuf::from("/project/lib/src/index.ts");
2168 let mut path_to_id = FxHashMap::default();
2169 path_to_id.insert(src_path.as_path(), FileId(10));
2170
2171 let dist_path = PathBuf::from("/project/lib/esm/index.mjs");
2172 assert_eq!(
2173 try_source_fallback(&dist_path, &path_to_id),
2174 Some(FileId(10)),
2175 "standalone esm/ directory should fall back to src/"
2176 );
2177 }
2178
2179 #[test]
2180 fn test_try_source_fallback_cjs_subdir() {
2181 let src_path = PathBuf::from("/project/lib/src/index.ts");
2182 let mut path_to_id = FxHashMap::default();
2183 path_to_id.insert(src_path.as_path(), FileId(11));
2184
2185 let cjs_path = PathBuf::from("/project/lib/cjs/index.cjs");
2186 assert_eq!(
2187 try_source_fallback(&cjs_path, &path_to_id),
2188 Some(FileId(11)),
2189 "standalone cjs/ directory should fall back to src/"
2190 );
2191 }
2192
2193 #[test]
2194 fn test_try_pnpm_workspace_fallback_empty_after_pnpm() {
2195 let path_to_id: FxHashMap<&Path, FileId> = FxHashMap::default();
2196 let workspace_roots: FxHashMap<&str, &Path> = FxHashMap::default();
2197
2198 let pnpm_path = PathBuf::from("/project/node_modules/.pnpm/pkg@1.0.0/node_modules");
2199 assert_eq!(
2200 try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
2201 None,
2202 "path ending at node_modules with nothing after should return None"
2203 );
2204 }
2205
2206 #[test]
2207 fn test_try_pnpm_workspace_fallback_scoped_package_only_scope() {
2208 let path_to_id: FxHashMap<&Path, FileId> = FxHashMap::default();
2209 let workspace_roots: FxHashMap<&str, &Path> = FxHashMap::default();
2210
2211 let pnpm_path =
2212 PathBuf::from("/project/node_modules/.pnpm/@scope+pkg@1.0.0/node_modules/@scope");
2213 assert_eq!(
2214 try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
2215 None,
2216 "scoped package without full name and no matching workspace should return None"
2217 );
2218 }
2219
2220 #[test]
2221 fn test_try_pnpm_workspace_fallback_no_inner_node_modules() {
2222 let path_to_id: FxHashMap<&Path, FileId> = FxHashMap::default();
2223 let workspace_roots: FxHashMap<&str, &Path> = FxHashMap::default();
2224
2225 let pnpm_path = PathBuf::from("/project/node_modules/.pnpm/pkg@1.0.0/dist/index.js");
2226 assert_eq!(
2227 try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
2228 None,
2229 "path without inner node_modules after .pnpm should return None"
2230 );
2231 }
2232
2233 #[test]
2234 fn test_try_pnpm_workspace_fallback_package_without_relative_path() {
2235 let path_to_id: FxHashMap<&Path, FileId> = FxHashMap::default();
2236 let mut workspace_roots = FxHashMap::default();
2237 let ws_root = PathBuf::from("/project/packages/ui");
2238 workspace_roots.insert("@myorg/ui", ws_root.as_path());
2239
2240 let pnpm_path =
2241 PathBuf::from("/project/node_modules/.pnpm/@myorg+ui@1.0.0/node_modules/@myorg/ui");
2242 assert_eq!(
2243 try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
2244 None,
2245 "path ending at package name with no relative file should return None"
2246 );
2247 }
2248
2249 #[test]
2250 fn test_try_pnpm_workspace_fallback_nested_dist_esm() {
2251 let src_path = PathBuf::from("/project/packages/ui/src/Button.ts");
2252 let mut path_to_id = FxHashMap::default();
2253 path_to_id.insert(src_path.as_path(), FileId(10));
2254
2255 let mut workspace_roots = FxHashMap::default();
2256 let ws_root = PathBuf::from("/project/packages/ui");
2257 workspace_roots.insert("@myorg/ui", ws_root.as_path());
2258
2259 let pnpm_path = PathBuf::from(
2260 "/project/node_modules/.pnpm/@myorg+ui@1.0.0/node_modules/@myorg/ui/dist/esm/Button.mjs",
2261 );
2262 assert_eq!(
2263 try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
2264 Some(FileId(10)),
2265 "pnpm path with nested dist/esm should resolve through source fallback"
2266 );
2267 }
2268
2269 #[test]
2272 fn package_map_target_string_value_dot_key() {
2273 let map = serde_json::Value::String("./src/index.ts".to_string());
2276 let conds = conditions();
2277 let result = package_map_target(&map, ".", &conds);
2279 assert!(
2280 matches!(result, PackageMapTarget::Targets(_)),
2281 "string map with '.' key should return Targets"
2282 );
2283 }
2284
2285 #[test]
2286 fn package_map_target_string_value_non_dot_key_no_match() {
2287 let map = serde_json::Value::String("./src/index.ts".to_string());
2289 let conds = conditions();
2290 let result = package_map_target(&map, "./sub", &conds);
2291 assert!(
2292 matches!(result, PackageMapTarget::NoMatch),
2293 "string map with non-dot key should return NoMatch"
2294 );
2295 }
2296
2297 #[test]
2298 fn package_map_target_null_value_dot_key() {
2299 let map = serde_json::Value::Null;
2301 let conds = conditions();
2302 let result = package_map_target(&map, ".", &conds);
2303 assert!(
2304 matches!(result, PackageMapTarget::Blocked),
2305 "null map with '.' key should return Blocked"
2306 );
2307 }
2308
2309 #[test]
2310 fn package_map_target_bool_value_non_dot_key() {
2311 let map = serde_json::Value::Bool(true);
2313 let conds = conditions();
2314 let result = package_map_target(&map, "./sub", &conds);
2315 assert!(
2316 matches!(result, PackageMapTarget::NoMatch),
2317 "bool map with non-dot key should return NoMatch"
2318 );
2319 }
2320
2321 #[test]
2324 fn package_map_target_condition_only_object_dot_key() {
2325 let map = serde_json::json!({
2328 "import": "./src/index.mjs",
2329 "require": "./src/index.cjs"
2330 });
2331 let conds = conditions();
2332 let result = package_map_target(&map, ".", &conds);
2333 assert!(
2334 matches!(result, PackageMapTarget::Targets(_)),
2335 "condition-only object with '.' key should return Targets"
2336 );
2337 }
2338
2339 #[test]
2340 fn package_map_target_condition_only_object_non_dot_key() {
2341 let map = serde_json::json!({
2344 "import": "./src/index.mjs",
2345 "require": "./src/index.cjs"
2346 });
2347 let conds = conditions();
2348 let result = package_map_target(&map, "./nonexistent", &conds);
2349 assert!(
2350 matches!(result, PackageMapTarget::NoMatch),
2351 "condition-only object with non-dot key should return NoMatch"
2352 );
2353 }
2354
2355 #[test]
2358 fn resolve_package_map_value_unmatched_conditions_returns_none() {
2359 let value = serde_json::json!({ "browser": "./src/browser.js" });
2361 let conds = conditions(); assert_eq!(
2363 resolve_package_map_value(&value, &conds, None),
2364 None,
2365 "unmatched condition should return None"
2366 );
2367 }
2368
2369 #[test]
2370 fn resolve_package_map_value_bool_returns_none() {
2371 let value = serde_json::Value::Bool(false);
2372 let conds = conditions();
2373 assert_eq!(
2374 resolve_package_map_value(&value, &conds, None),
2375 None,
2376 "bool value should return None"
2377 );
2378 }
2379
2380 #[test]
2381 fn resolve_package_map_value_number_returns_none() {
2382 let value = serde_json::Value::Number(42.into());
2383 let conds = conditions();
2384 assert_eq!(
2385 resolve_package_map_value(&value, &conds, None),
2386 None,
2387 "number value should return None"
2388 );
2389 }
2390
2391 #[test]
2392 fn resolve_package_map_value_null_returns_none() {
2393 let value = serde_json::Value::Null;
2394 let conds = conditions();
2395 assert_eq!(
2396 resolve_package_map_value(&value, &conds, None),
2397 None,
2398 "null value should return None"
2399 );
2400 }
2401
2402 #[test]
2403 fn resolve_package_map_value_array_all_null_returns_none() {
2404 let value = serde_json::json!([null, false, 42]);
2406 let conds = conditions();
2407 assert_eq!(
2408 resolve_package_map_value(&value, &conds, None),
2409 None,
2410 "array of unresolvable values should return None"
2411 );
2412 }
2413
2414 #[test]
2415 fn resolve_package_map_value_array_with_valid_entry() {
2416 let value = serde_json::json!([null, "./src/index.ts"]);
2418 let conds = conditions();
2419 let result = resolve_package_map_value(&value, &conds, None);
2420 assert_eq!(
2421 result,
2422 Some(vec!["./src/index.ts".to_string()]),
2423 "array with a valid string entry should return that entry"
2424 );
2425 }
2426
2427 #[test]
2430 fn package_map_pattern_capture_no_star_returns_none() {
2431 assert_eq!(
2433 package_map_pattern_capture("./exact", "./exact"),
2434 None,
2435 "pattern with no star should return None"
2436 );
2437 }
2438
2439 #[test]
2440 fn package_map_pattern_capture_two_stars_returns_none() {
2441 assert_eq!(
2443 package_map_pattern_capture("./*/*.js", "./foo/bar.js"),
2444 None,
2445 "pattern with two stars should return None"
2446 );
2447 }
2448
2449 #[test]
2450 fn package_map_pattern_capture_single_star_captures() {
2451 assert_eq!(
2453 package_map_pattern_capture("./dist/*/index.js", "./dist/utils/index.js"),
2454 Some("utils".to_string()),
2455 );
2456 }
2457
2458 #[test]
2459 fn package_map_pattern_capture_no_prefix_match_returns_none() {
2460 assert_eq!(
2462 package_map_pattern_capture("./lib/*.js", "./src/foo.js"),
2463 None,
2464 );
2465 }
2466
2467 #[test]
2470 fn resolve_package_map_target_no_dot_slash_prefix_returns_none() {
2471 let root = PathBuf::from("/project/packages/ui");
2473 let pj = fallow_config::PackageJson::default();
2474 with_package_map_ctx(root, Some("@myorg/ui"), pj, &[], |ctx, manifest, _root| {
2475 let result = resolve_package_map_target(ctx, manifest, "src/index.ts", None);
2476 assert_eq!(result, None, "target without './' should return None");
2477 });
2478 }
2479
2480 #[test]
2481 fn resolve_package_map_target_parent_dir_returns_none() {
2482 let root = PathBuf::from("/project/packages/ui");
2484 let pj = fallow_config::PackageJson::default();
2485 with_package_map_ctx(root, Some("@myorg/ui"), pj, &[], |ctx, manifest, _root| {
2486 let result = resolve_package_map_target(ctx, manifest, "./../outside/file.ts", None);
2487 assert_eq!(result, None, "parent-dir target should return None");
2488 });
2489 }
2490
2491 #[test]
2492 fn resolve_package_map_target_absolute_path_returns_none() {
2493 let root = PathBuf::from("/project/packages/ui");
2495 let pj = fallow_config::PackageJson::default();
2496 with_package_map_ctx(root, Some("@myorg/ui"), pj, &[], |ctx, manifest, _root| {
2497 let result = resolve_package_map_target(ctx, manifest, ".//abs/path.ts", None);
2500 assert_eq!(result, None, "absolute target should return None");
2501 });
2502 }
2503
2504 #[test]
2505 fn resolve_package_map_target_valid_target_hits_raw_path_map() {
2506 let root = PathBuf::from("/project/packages/ui");
2508 let src = root.join("src/index.ts");
2509 let pj = fallow_config::PackageJson::default();
2510 with_package_map_ctx(
2511 root,
2512 Some("@myorg/ui"),
2513 pj,
2514 &[(src, FileId(5))],
2515 |ctx, manifest, _root| {
2516 let result = resolve_package_map_target(ctx, manifest, "./src/index.ts", None);
2517 assert_eq!(
2518 result,
2519 Some(FileId(5)),
2520 "valid target in raw_path_to_id should resolve"
2521 );
2522 },
2523 );
2524 }
2525
2526 #[test]
2529 fn package_import_source_subpath_strips_hash_and_package_name() {
2530 let manifest = PackageManifestInfo {
2531 root: PathBuf::from("/project"),
2532 canonical_root: PathBuf::from("/project"),
2533 name: Some("my-pkg".to_string()),
2534 package_json: fallow_config::PackageJson::default(),
2535 deno_import_map: Vec::new(),
2536 };
2537 let result = package_import_source_subpath(&manifest, "#my-pkg/utils");
2538 assert_eq!(
2539 result,
2540 Some(PathBuf::from("utils")),
2541 "should strip '#', package name, and '/' separator"
2542 );
2543 }
2544
2545 #[test]
2546 fn package_import_source_subpath_no_package_name_match_keeps_full_subpath() {
2547 let manifest = PackageManifestInfo {
2550 root: PathBuf::from("/project"),
2551 canonical_root: PathBuf::from("/project"),
2552 name: Some("my-pkg".to_string()),
2553 package_json: fallow_config::PackageJson::default(),
2554 deno_import_map: Vec::new(),
2555 };
2556 let result = package_import_source_subpath(&manifest, "#utils");
2557 assert_eq!(
2558 result,
2559 Some(PathBuf::from("utils")),
2560 "without package-name prefix the full subpath should be kept"
2561 );
2562 }
2563
2564 #[test]
2565 fn package_import_source_subpath_empty_hash_returns_none() {
2566 let manifest = PackageManifestInfo {
2569 root: PathBuf::from("/project"),
2570 canonical_root: PathBuf::from("/project"),
2571 name: Some("my-pkg".to_string()),
2572 package_json: fallow_config::PackageJson::default(),
2573 deno_import_map: Vec::new(),
2574 };
2575 let result = package_import_source_subpath(&manifest, "#");
2577 assert_eq!(
2578 result, None,
2579 "specifier '#' with empty body should return None"
2580 );
2581 }
2582
2583 #[test]
2584 fn package_import_source_subpath_no_hash_returns_none() {
2585 let manifest = PackageManifestInfo {
2587 root: PathBuf::from("/project"),
2588 canonical_root: PathBuf::from("/project"),
2589 name: Some("my-pkg".to_string()),
2590 package_json: fallow_config::PackageJson::default(),
2591 deno_import_map: Vec::new(),
2592 };
2593 let result = package_import_source_subpath(&manifest, "no-hash");
2594 assert_eq!(result, None, "specifier without '#' should return None");
2595 }
2596
2597 #[test]
2598 fn package_import_source_subpath_no_manifest_name() {
2599 let manifest = PackageManifestInfo {
2601 root: PathBuf::from("/project"),
2602 canonical_root: PathBuf::from("/project"),
2603 name: None,
2604 package_json: fallow_config::PackageJson::default(),
2605 deno_import_map: Vec::new(),
2606 };
2607 let result = package_import_source_subpath(&manifest, "#internal/helper");
2608 assert_eq!(
2609 result,
2610 Some(PathBuf::from("internal/helper")),
2611 "manifest without name should return the full stripped specifier"
2612 );
2613 }
2614
2615 #[test]
2618 fn nearest_package_manifest_returns_deepest_match() {
2619 let root1 = PathBuf::from("/project");
2620 let root2 = PathBuf::from("/project/packages/ui");
2621 let m1 = PackageManifestInfo {
2622 root: root1.clone(),
2623 canonical_root: root1,
2624 name: Some("root".to_string()),
2625 package_json: fallow_config::PackageJson::default(),
2626 deno_import_map: Vec::new(),
2627 };
2628 let m2 = PackageManifestInfo {
2629 root: root2.clone(),
2630 canonical_root: root2,
2631 name: Some("@myorg/ui".to_string()),
2632 package_json: fallow_config::PackageJson::default(),
2633 deno_import_map: Vec::new(),
2634 };
2635 let manifests = [m1, m2];
2636 let from_file = Path::new("/project/packages/ui/src/index.ts");
2637 let result = nearest_package_manifest(&manifests, from_file);
2638 assert_eq!(
2639 result.and_then(|m| m.name.as_deref()),
2640 Some("@myorg/ui"),
2641 "should pick the deepest (longest path) manifest that contains the file"
2642 );
2643 }
2644
2645 #[test]
2646 fn nearest_package_manifest_no_match_returns_none() {
2647 let root = PathBuf::from("/project/packages/ui");
2648 let m = PackageManifestInfo {
2649 root: root.clone(),
2650 canonical_root: root,
2651 name: Some("@myorg/ui".to_string()),
2652 package_json: fallow_config::PackageJson::default(),
2653 deno_import_map: Vec::new(),
2654 };
2655 let manifests = [m];
2656 let from_file = Path::new("/other/project/src/index.ts");
2658 let result = nearest_package_manifest(&manifests, from_file);
2659 assert!(
2660 result.is_none(),
2661 "file outside all manifest roots should return None"
2662 );
2663 }
2664
2665 #[test]
2668 fn lookup_internal_file_id_uses_path_to_id_when_raw_misses() {
2669 let target = PathBuf::from("/project/src/index.ts");
2672 let mut path_to_id: FxHashMap<&Path, FileId> = FxHashMap::default();
2673 path_to_id.insert(target.as_path(), FileId(99));
2674 let raw_path_to_id: FxHashMap<&Path, FileId> = FxHashMap::default();
2675 let workspace_roots: FxHashMap<&str, &Path> = FxHashMap::default();
2676 let condition_names = conditions();
2677 let resolver = oxc_resolver::Resolver::new(oxc_resolver::ResolveOptions::default());
2678 let tsconfig_warned = std::sync::Mutex::new(FxHashSet::default());
2679 let tsconfig_cache = TsconfigCache::default();
2680 let canonicalize_cache = CanonicalizeCache::default();
2681 let root = PathBuf::from("/project");
2682 let ctx = ResolveContext {
2683 resolver: &resolver,
2684 style_resolver: &resolver,
2685 extensions: &[],
2686 path_to_id: &path_to_id,
2687 raw_path_to_id: &raw_path_to_id,
2688 workspace_roots: &workspace_roots,
2689 package_manifests: &[],
2690 has_deno_import_maps: false,
2691 condition_names: &condition_names,
2692 path_aliases: &[],
2693 scss_include_paths: &[],
2694 static_dir_mappings: &[],
2695 framework_static_dir_mappings: &[],
2696 root: &root,
2697 canonical_fallback: None,
2698 tsconfig_warned: &tsconfig_warned,
2699 tsconfig_cache: &tsconfig_cache,
2700 canonicalize_cache: &canonicalize_cache,
2701 };
2702 let result = lookup_internal_file_id(&ctx, &target);
2703 assert_eq!(
2704 result,
2705 Some(FileId(99)),
2706 "should fall back from raw_path_to_id to path_to_id"
2707 );
2708 }
2709
2710 #[test]
2713 fn try_scss_partial_fallback_rejects_colon_specifier() {
2714 let root = PathBuf::from("/project");
2717 let pj = fallow_config::PackageJson::default();
2718 with_package_map_ctx(root.clone(), None, pj, &[], |ctx, _manifest, _r| {
2719 let from_file = root.join("src/main.scss");
2720 let result = try_scss_partial_fallback(ctx, &from_file, "sass:math");
2721 assert!(
2722 result.is_none(),
2723 "specifier with ':' should short-circuit to None"
2724 );
2725 });
2726 }
2727
2728 #[test]
2729 fn try_scss_partial_fallback_rejects_already_partial_filename() {
2730 let root = PathBuf::from("/project");
2733 let pj = fallow_config::PackageJson::default();
2734 with_package_map_ctx(root.clone(), None, pj, &[], |ctx, _manifest, _r| {
2735 let from_file = root.join("src/main.scss");
2736 let result = try_scss_partial_fallback(ctx, &from_file, "./_variables");
2737 assert!(
2738 result.is_none(),
2739 "already-partial filename should short-circuit to None"
2740 );
2741 });
2742 }
2743
2744 #[test]
2747 fn try_workspace_package_fallback_rejects_relative_specifier() {
2748 let root = PathBuf::from("/project");
2751 let pj = fallow_config::PackageJson::default();
2752 with_package_map_ctx(root, None, pj, &[], |ctx, _manifest, _r| {
2753 let result = try_workspace_package_fallback(ctx, "./local/module");
2754 assert!(
2755 result.is_none(),
2756 "relative specifier should return None from workspace fallback"
2757 );
2758 });
2759 }
2760
2761 #[test]
2762 fn try_workspace_package_fallback_rejects_absolute_path() {
2763 let root = PathBuf::from("/project");
2765 let pj = fallow_config::PackageJson::default();
2766 with_package_map_ctx(root, None, pj, &[], |ctx, _manifest, _r| {
2767 let result = try_workspace_package_fallback(ctx, "/absolute/path");
2768 assert!(
2769 result.is_none(),
2770 "absolute path should return None from workspace fallback"
2771 );
2772 });
2773 }
2774}