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 super::work::note_oxc_resolve();
52 if let Ok(resolved) = ctx.resolver.resolve(ctx.root, &substituted) {
53 let resolved_path = resolved.path();
54 if let Some(&file_id) = ctx.raw_path_to_id.get(resolved_path) {
55 return Some(ResolveResult::InternalModule(file_id));
56 }
57 if let Some(canonical) = ctx.canonicalize_cache.get(resolved_path) {
58 if let Some(&file_id) = ctx.path_to_id.get(canonical.as_path()) {
59 return Some(ResolveResult::InternalModule(file_id));
60 }
61 if let Some(file_id) = try_source_fallback(&canonical, ctx.path_to_id) {
62 return Some(ResolveResult::InternalModule(file_id));
63 }
64 if let Some(file_id) =
65 try_pnpm_workspace_fallback(&canonical, ctx.path_to_id, ctx.workspace_roots)
66 {
67 return Some(ResolveResult::InternalModule(file_id));
68 }
69 if let Some(pkg_name) = extract_package_name_from_node_modules_path(&canonical) {
70 return Some(ResolveResult::NpmPackage(pkg_name));
71 }
72 return Some(ResolveResult::ExternalFile(canonical));
73 }
74 }
75 }
76 None
77}
78
79pub(super) fn try_scss_partial_fallback(
88 ctx: &ResolveContext<'_>,
89 from_file: &Path,
90 specifier: &str,
91) -> Option<ResolveResult> {
92 if specifier.contains(':') {
93 return None;
94 }
95
96 let spec_path = Path::new(specifier);
97 let filename = spec_path.file_name()?.to_str()?;
98
99 if filename.starts_with('_') {
100 return None;
101 }
102
103 let partial_filename = format!("_{filename}");
104 let partial_specifier = if let Some(parent) = spec_path.parent()
105 && !parent.as_os_str().is_empty()
106 {
107 format!("{}/{partial_filename}", parent.display())
108 } else {
109 partial_filename
110 };
111
112 if let Some(result) = try_resolve_scss(ctx, from_file, &partial_specifier) {
113 return Some(result);
114 }
115
116 let index_partial = format!("{specifier}/_index");
117 if let Some(result) = try_resolve_scss(ctx, from_file, &index_partial) {
118 return Some(result);
119 }
120
121 let index_plain = format!("{specifier}/index");
122 try_resolve_scss(ctx, from_file, &index_plain)
123}
124
125pub(super) fn try_css_extension_fallback(
135 ctx: &ResolveContext<'_>,
136 from_file: &Path,
137 specifier: &str,
138) -> Option<ResolveResult> {
139 if specifier.contains(':') {
140 return None;
141 }
142 let spec_path = Path::new(specifier);
143 let already_css_ext = spec_path
144 .extension()
145 .and_then(|e| e.to_str())
146 .is_some_and(|e| {
147 e.eq_ignore_ascii_case("css")
148 || e.eq_ignore_ascii_case("scss")
149 || e.eq_ignore_ascii_case("sass")
150 });
151 if already_css_ext {
152 return try_resolve_scss(ctx, from_file, specifier);
153 }
154 for ext in ["scss", "sass", "css"] {
155 let candidate = format!("{specifier}.{ext}");
156 if let Some(result) = try_resolve_scss(ctx, from_file, &candidate) {
157 return Some(result);
158 }
159 }
160 None
161}
162
163fn try_resolve_scss(
165 ctx: &ResolveContext<'_>,
166 from_file: &Path,
167 specifier: &str,
168) -> Option<ResolveResult> {
169 super::work::note_oxc_resolve();
170 let resolved = ctx.resolver.resolve_file(from_file, specifier).ok()?;
171 let resolved_path = resolved.path();
172
173 if let Some(&file_id) = ctx.raw_path_to_id.get(resolved_path) {
174 return Some(ResolveResult::InternalModule(file_id));
175 }
176 if let Some(canonical) = ctx.canonicalize_cache.get(resolved_path)
177 && let Some(&file_id) = ctx.path_to_id.get(canonical.as_path())
178 {
179 return Some(ResolveResult::InternalModule(file_id));
180 }
181 None
182}
183
184pub(super) fn try_scss_include_path_fallback(
204 ctx: &ResolveContext<'_>,
205 from_file: &Path,
206 specifier: &str,
207 from_style: bool,
208) -> Option<ResolveResult> {
209 if ctx.scss_include_paths.is_empty() {
210 return None;
211 }
212 let is_scss_importer = from_file
213 .extension()
214 .is_some_and(|e| e == "scss" || e == "sass");
215 if !is_scss_importer && !from_style {
216 return None;
217 }
218 if specifier.contains(':') {
219 return None;
220 }
221 let bare = specifier.strip_prefix("./")?;
222 if bare.starts_with("..") || bare.starts_with('/') {
223 return None;
224 }
225
226 for include_dir in ctx.scss_include_paths {
227 if let Some(file_id) = find_scss_in_dir(include_dir, bare, ctx) {
228 return Some(ResolveResult::InternalModule(file_id));
229 }
230 }
231 None
232}
233
234fn find_scss_in_dir(include_dir: &Path, bare: &str, ctx: &ResolveContext<'_>) -> Option<FileId> {
238 let bare_path = Path::new(bare);
239 let has_scss_ext = matches!(
240 bare_path.extension().and_then(|e| e.to_str()),
241 Some(ext) if ext.eq_ignore_ascii_case("scss") || ext.eq_ignore_ascii_case("sass")
242 );
243
244 let parent = bare_path.parent();
245 let stem_with_ext = bare_path.file_name()?.to_str()?;
246 let stem_without_ext = bare_path.file_stem().and_then(|s| s.to_str())?;
247
248 let build = |rel: &Path| -> std::path::PathBuf { include_dir.join(rel) };
249 let join_with_parent = |name: &str| -> std::path::PathBuf {
250 parent.map_or_else(|| build(Path::new(name)), |p| build(&p.join(name)))
251 };
252
253 let exts: &[&str] = if has_scss_ext {
254 &[""]
255 } else {
256 &["scss", "sass"]
257 };
258
259 for ext in exts {
260 let suffix = if ext.is_empty() {
261 String::new()
262 } else {
263 format!(".{ext}")
264 };
265 let direct = if ext.is_empty() {
266 build(bare_path)
267 } else {
268 join_with_parent(&format!("{stem_with_ext}{suffix}"))
269 };
270 if let Some(fid) = lookup_scss_path(&direct, ctx) {
271 return Some(fid);
272 }
273 let partial_name = if ext.is_empty() {
274 format!("_{stem_with_ext}")
275 } else {
276 format!("_{stem_without_ext}{suffix}")
277 };
278 let partial = join_with_parent(&partial_name);
279 if let Some(fid) = lookup_scss_path(&partial, ctx) {
280 return Some(fid);
281 }
282 if ext.is_empty() {
283 continue;
284 }
285 let idx_partial = build(bare_path).join(format!("_index{suffix}"));
286 if let Some(fid) = lookup_scss_path(&idx_partial, ctx) {
287 return Some(fid);
288 }
289 let idx_plain = build(bare_path).join(format!("index{suffix}"));
290 if let Some(fid) = lookup_scss_path(&idx_plain, ctx) {
291 return Some(fid);
292 }
293 }
294 None
295}
296
297fn lookup_scss_path(candidate: &Path, ctx: &ResolveContext<'_>) -> Option<FileId> {
300 if let Some(&file_id) = ctx.raw_path_to_id.get(candidate) {
301 return Some(file_id);
302 }
303 if let Some(canonical) = ctx.canonicalize_cache.get(candidate) {
304 if let Some(&file_id) = ctx.path_to_id.get(canonical.as_path()) {
305 return Some(file_id);
306 }
307 if let Some(fallback) = ctx.canonical_fallback
308 && let Some(file_id) = fallback.get(&canonical)
309 {
310 return Some(file_id);
311 }
312 }
313 None
314}
315
316pub(super) fn try_scss_node_modules_fallback(
338 _ctx: &ResolveContext<'_>,
339 from_file: &Path,
340 specifier: &str,
341 from_style: bool,
342) -> Option<ResolveResult> {
343 if specifier.contains(':') {
344 return None;
345 }
346 let is_scss_importer = from_file
347 .extension()
348 .is_some_and(|e| e == "scss" || e == "sass");
349 if !is_scss_importer && !from_style {
350 return None;
351 }
352 let bare = specifier.strip_prefix("./")?;
353 if bare.starts_with("..") || bare.starts_with('/') {
354 return None;
355 }
356 if bare.is_empty() {
357 return None;
358 }
359
360 let mut dir = from_file.parent()?;
361 loop {
362 let nm_dir = dir.join("node_modules");
363 if nm_dir.is_dir()
364 && let Some(path) = find_scss_in_node_modules(&nm_dir, bare)
365 && let Some(pkg_name) = extract_package_name_from_node_modules_path(&path)
366 {
367 return Some(ResolveResult::NpmPackage(pkg_name));
368 }
369 let Some(parent) = dir.parent() else {
370 break;
371 };
372 dir = parent;
373 }
374 None
375}
376
377fn find_scss_in_node_modules(nm_dir: &Path, bare: &str) -> Option<PathBuf> {
386 let bare_path = Path::new(bare);
387 let file_name = bare_path.file_name()?.to_str()?;
388 let parent = bare_path.parent();
389 let join_with_parent = |name: &str| -> PathBuf {
390 parent.map_or_else(|| nm_dir.join(name), |p| nm_dir.join(p).join(name))
391 };
392
393 for ext in &["scss", "sass", "css"] {
394 let candidate = join_with_parent(&format!("{file_name}.{ext}"));
395 if candidate.is_file() {
396 return Some(candidate);
397 }
398 }
399 for ext in &["scss", "sass"] {
400 let candidate = join_with_parent(&format!("_{file_name}.{ext}"));
401 if candidate.is_file() {
402 return Some(candidate);
403 }
404 }
405 for ext in &["scss", "sass"] {
406 let idx_partial = nm_dir.join(bare).join(format!("_index.{ext}"));
407 if idx_partial.is_file() {
408 return Some(idx_partial);
409 }
410 let idx_plain = nm_dir.join(bare).join(format!("index.{ext}"));
411 if idx_plain.is_file() {
412 return Some(idx_plain);
413 }
414 }
415 let exact = nm_dir.join(bare);
416 if exact.is_file() {
417 return Some(exact);
418 }
419 None
420}
421
422pub(super) fn try_source_fallback(
434 resolved: &Path,
435 path_to_id: &FxHashMap<&Path, FileId>,
436) -> Option<FileId> {
437 let components: Vec<_> = resolved.components().collect();
438
439 let is_output_dir = |c: &std::path::Component| -> bool {
440 if let std::path::Component::Normal(s) = c
441 && let Some(name) = s.to_str()
442 {
443 return OUTPUT_DIRS.contains(&name);
444 }
445 false
446 };
447
448 let last_output_pos = components.iter().rposition(&is_output_dir)?;
449
450 let mut first_output_pos = last_output_pos;
451 while first_output_pos > 0 && is_output_dir(&components[first_output_pos - 1]) {
452 first_output_pos -= 1;
453 }
454
455 let prefix: PathBuf = components[..first_output_pos].iter().collect();
456
457 let suffix: PathBuf = components[last_output_pos + 1..].iter().collect();
458 suffix.file_stem()?; for ext in SOURCE_EXTS {
461 let source_candidate = prefix.join("src").join(suffix.with_extension(ext));
462 if let Some(&file_id) = path_to_id.get(source_candidate.as_path()) {
463 return Some(file_id);
464 }
465 }
466
467 None
468}
469
470pub(super) fn try_package_imports_fallback(
476 ctx: &ResolveContext<'_>,
477 from_file: &Path,
478 specifier: &str,
479) -> Option<ResolveResult> {
480 if !specifier.starts_with('#') {
481 return None;
482 }
483 let manifest = nearest_package_manifest(ctx.package_manifests, from_file)?;
484 let imports = manifest.package_json.imports.as_ref()?;
485 let PackageMapTarget::Targets(targets) =
486 package_map_target(imports, specifier, ctx.condition_names)
487 else {
488 return None;
489 };
490 let source_subpath = package_import_source_subpath(manifest, specifier);
491 resolve_package_import_targets(ctx, manifest, &targets, source_subpath.as_deref()).map(
492 |target| match target {
493 PackageImportTarget::Internal(file_id) => match &manifest.name {
494 Some(package_name) => ResolveResult::InternalPackageModule {
495 file_id,
496 package_name: package_name.clone(),
497 },
498 None => ResolveResult::InternalModule(file_id),
499 },
500 PackageImportTarget::ExternalPackage(package_name) => {
501 ResolveResult::NpmPackage(package_name)
502 }
503 },
504 )
505}
506
507pub(super) fn package_imports_workspace_target(
520 ctx: &ResolveContext<'_>,
521 from_file: &Path,
522 specifier: &str,
523 resolved_file: &Path,
524) -> Option<String> {
525 if !specifier.starts_with('#') {
526 return None;
527 }
528 let manifest = nearest_package_manifest(ctx.package_manifests, from_file)?;
529 let imports = manifest.package_json.imports.as_ref()?;
530 let PackageMapTarget::Targets(targets) =
531 package_map_target(imports, specifier, ctx.condition_names)
532 else {
533 return None;
534 };
535 let owner = nearest_package_manifest(ctx.package_manifests, resolved_file)?
536 .name
537 .as_deref()?;
538 targets
539 .iter()
540 .filter(|target| !target.starts_with('#'))
541 .filter_map(|target| package_import_external_target(target))
542 .find(|package_name| package_name == owner)
543}
544
545pub(super) fn installed_workspace_package_target(
556 ctx: &ResolveContext<'_>,
557 from_file: &Path,
558 specifier: &str,
559 resolved_file: &Path,
560) -> Option<String> {
561 let package_name = super::specifier::package_usage_name_for_external_bare_specifier(specifier)?;
562 let workspace_root = *ctx.workspace_roots.get(package_name.as_str())?;
563 let resolved = ctx
564 .canonicalize_cache
565 .get(resolved_file)
566 .unwrap_or_else(|| resolved_file.to_path_buf());
567 if !resolved.starts_with(workspace_root) {
568 return None;
569 }
570 let installed = from_file.ancestors().skip(1).find_map(|dir| {
571 ctx.canonicalize_cache
572 .get(&dir.join("node_modules").join(&package_name))
573 })?;
574 (installed == workspace_root).then_some(package_name)
575}
576
577pub(super) fn try_relative_package_root_source_fallback(
580 ctx: &ResolveContext<'_>,
581 from_file: &Path,
582 specifier: &str,
583) -> Option<ResolveResult> {
584 if !specifier.starts_with("./") && !specifier.starts_with("../") {
585 return None;
586 }
587
588 let from_dir = from_file.parent()?;
589 let candidate = from_dir.join(specifier);
590 let normalized_candidate = normalize_path_lexically(&candidate);
591 #[cfg(not(miri))]
592 let canonical_candidate = ctx.canonicalize_cache.get(&candidate);
593 #[cfg(miri)]
594 let canonical_candidate: Option<PathBuf> = None;
595
596 ctx.package_manifests.iter().find_map(|manifest| {
597 let matches_manifest = candidate == manifest.root
598 || normalized_candidate == manifest.root
599 || canonical_candidate
600 .as_deref()
601 .is_some_and(|canonical| canonical == manifest.canonical_root);
602 matches_manifest
603 .then(|| try_source_subpath(ctx, manifest, Path::new("")))
604 .flatten()
605 .map(ResolveResult::InternalModule)
606 })
607}
608
609pub(super) fn normalize_path_lexically(path: &Path) -> PathBuf {
610 let mut normalized = PathBuf::new();
611 for component in path.components() {
612 match component {
613 std::path::Component::CurDir => {}
614 std::path::Component::ParentDir => {
615 if !normalized.pop() {
616 normalized.push(component.as_os_str());
617 }
618 }
619 std::path::Component::Prefix(_)
620 | std::path::Component::RootDir
621 | std::path::Component::Normal(_) => normalized.push(component.as_os_str()),
622 }
623 }
624 normalized
625}
626
627#[derive(Debug, Clone, PartialEq, Eq)]
628enum PackageMapTarget {
629 NoMatch,
630 Blocked,
631 Targets(Vec<String>),
632}
633
634enum PackageImportTarget {
635 Internal(FileId),
636 ExternalPackage(String),
637}
638
639fn package_map_match_value(
640 value: &Value,
641 condition_names: &[String],
642 capture: Option<&str>,
643) -> PackageMapTarget {
644 resolve_package_map_value(value, condition_names, capture)
645 .filter(|targets| !targets.is_empty())
646 .map_or(PackageMapTarget::Blocked, PackageMapTarget::Targets)
647}
648
649fn package_map_target(
650 map: &Value,
651 specifier_key: &str,
652 condition_names: &[String],
653) -> PackageMapTarget {
654 let Some(obj) = map.as_object() else {
655 if specifier_key == "." {
656 return package_map_match_value(map, condition_names, None);
657 }
658 return PackageMapTarget::NoMatch;
659 };
660
661 let has_subpath_keys = obj
662 .keys()
663 .any(|key| key == "." || key.starts_with("./") || key.starts_with('#'));
664 if !has_subpath_keys {
665 if specifier_key == "." {
666 return package_map_match_value(map, condition_names, None);
667 }
668 return PackageMapTarget::NoMatch;
669 }
670
671 if let Some(value) = obj.get(specifier_key) {
672 return package_map_match_value(value, condition_names, None);
673 }
674
675 let mut patterns: Vec<(&str, &Value, String)> = obj
676 .iter()
677 .filter_map(|(pattern, value)| {
678 package_map_pattern_capture(pattern, specifier_key)
679 .map(|capture| (pattern.as_str(), value, capture))
680 })
681 .collect();
682 patterns.sort_by(|(left, _, _), (right, _, _)| {
683 package_map_pattern_specificity(right).cmp(&package_map_pattern_specificity(left))
684 });
685
686 patterns
687 .first()
688 .map_or(PackageMapTarget::NoMatch, |(_, value, capture)| {
689 package_map_match_value(value, condition_names, Some(capture))
690 })
691}
692
693fn resolve_package_map_value(
694 value: &Value,
695 condition_names: &[String],
696 capture: Option<&str>,
697) -> Option<Vec<String>> {
698 match value {
699 Value::String(target) => Some(vec![match capture {
700 Some(capture) => target.replace('*', capture),
701 None => target.clone(),
702 }]),
703 Value::Object(map) => {
704 for (condition, value) in map {
705 if (condition == "default"
706 || condition_names
707 .iter()
708 .any(|active_condition| active_condition == condition))
709 && let Some(targets) =
710 resolve_package_map_value(value, condition_names, capture)
711 {
712 return Some(targets);
713 }
714 }
715 None
716 }
717 Value::Array(values) => {
718 let targets: Vec<String> = values
719 .iter()
720 .filter_map(|value| resolve_package_map_value(value, condition_names, capture))
721 .flatten()
722 .collect();
723 (!targets.is_empty()).then_some(targets)
724 }
725 Value::Bool(_) | Value::Null | Value::Number(_) => None,
726 }
727}
728
729fn package_map_pattern_capture(pattern: &str, specifier: &str) -> Option<String> {
730 let star = pattern.find('*')?;
731 if pattern[star + 1..].contains('*') {
732 return None;
733 }
734 let (prefix, suffix_with_star) = pattern.split_at(star);
735 let suffix = &suffix_with_star[1..];
736 let captured = specifier.strip_prefix(prefix)?.strip_suffix(suffix)?;
737 Some(captured.to_string())
738}
739
740fn package_map_pattern_specificity(pattern: &str) -> (usize, usize) {
741 let star = pattern.find('*').unwrap_or(pattern.len());
742 (star, pattern.len())
743}
744
745fn package_import_source_subpath(
746 manifest: &PackageManifestInfo,
747 specifier: &str,
748) -> Option<PathBuf> {
749 let stripped = specifier.strip_prefix('#')?;
750 let without_package_name = manifest
751 .name
752 .as_deref()
753 .and_then(|name| stripped.strip_prefix(name))
754 .and_then(|rest| rest.strip_prefix('/'))
755 .unwrap_or(stripped);
756 if without_package_name.is_empty() {
757 None
758 } else {
759 Some(PathBuf::from(without_package_name))
760 }
761}
762
763pub(super) fn nearest_package_manifest<'a>(
764 manifests: &'a [PackageManifestInfo],
765 from_file: &Path,
766) -> Option<&'a PackageManifestInfo> {
767 manifests
768 .iter()
769 .filter(|manifest| {
770 from_file.starts_with(&manifest.root) || from_file.starts_with(&manifest.canonical_root)
771 })
772 .max_by_key(|manifest| manifest.root.components().count())
773}
774
775fn find_package_manifest<'a>(
776 manifests: &'a [PackageManifestInfo],
777 package_name: &str,
778) -> Option<&'a PackageManifestInfo> {
779 manifests
780 .iter()
781 .find(|manifest| manifest.name.as_deref() == Some(package_name))
782}
783
784fn resolve_package_map_target(
785 ctx: &ResolveContext<'_>,
786 manifest: &PackageManifestInfo,
787 target: &str,
788 source_subpath: Option<&Path>,
789) -> Option<FileId> {
790 let target = target.strip_prefix("./")?;
791 if target.starts_with("../") || target.starts_with('/') {
792 return None;
793 }
794 let target_path = manifest.root.join(target);
795
796 lookup_internal_file_id(ctx, &target_path)
797 .or_else(|| try_source_fallback(&target_path, ctx.raw_path_to_id))
798 .or_else(|| try_source_fallback(&target_path, ctx.path_to_id))
799 .or_else(|| source_subpath.and_then(|subpath| try_source_subpath(ctx, manifest, subpath)))
800}
801
802fn resolve_package_map_targets(
803 ctx: &ResolveContext<'_>,
804 manifest: &PackageManifestInfo,
805 targets: &[String],
806 source_subpath: Option<&Path>,
807) -> Option<FileId> {
808 targets
809 .iter()
810 .find_map(|target| resolve_package_map_target(ctx, manifest, target, source_subpath))
811}
812
813fn resolve_package_import_targets(
814 ctx: &ResolveContext<'_>,
815 manifest: &PackageManifestInfo,
816 targets: &[String],
817 source_subpath: Option<&Path>,
818) -> Option<PackageImportTarget> {
819 targets.iter().find_map(|target| {
820 resolve_package_map_target(ctx, manifest, target, source_subpath)
821 .map(PackageImportTarget::Internal)
822 .or_else(|| {
823 package_import_external_target(target).map(PackageImportTarget::ExternalPackage)
824 })
825 })
826}
827
828fn package_import_external_target(target: &str) -> Option<String> {
829 if is_bare_specifier(target) && is_valid_package_name(target) {
830 Some(extract_package_name(target))
831 } else {
832 None
833 }
834}
835
836fn try_source_subpath(
837 ctx: &ResolveContext<'_>,
838 manifest: &PackageManifestInfo,
839 subpath: &Path,
840) -> Option<FileId> {
841 if subpath.as_os_str().is_empty()
842 && let Some(source) = manifest.package_json.source.as_deref()
843 && let Some(source_path) = safe_relative_package_source_path(source)
844 && let Some(file_id) = lookup_internal_file_id(ctx, &manifest.root.join(source_path))
845 {
846 return Some(file_id);
847 }
848
849 for ext in SOURCE_EXTS {
850 let direct = if subpath.as_os_str().is_empty() {
851 manifest.root.join("src").join(format!("index.{ext}"))
852 } else {
853 manifest.root.join("src").join(subpath).with_extension(ext)
854 };
855 if let Some(file_id) = lookup_internal_file_id(ctx, &direct) {
856 return Some(file_id);
857 }
858
859 if !subpath.as_os_str().is_empty() {
860 let index = manifest
861 .root
862 .join("src")
863 .join(subpath)
864 .join(format!("index.{ext}"));
865 if let Some(file_id) = lookup_internal_file_id(ctx, &index) {
866 return Some(file_id);
867 }
868 }
869
870 if subpath.as_os_str().is_empty() {
871 let root_index = manifest.root.join(format!("index.{ext}"));
872 if let Some(file_id) = lookup_internal_file_id(ctx, &root_index) {
873 return Some(file_id);
874 }
875 }
876 }
877
878 None
879}
880
881fn safe_relative_package_source_path(source: &str) -> Option<&Path> {
882 let source = source.strip_prefix("./").unwrap_or(source);
883 let path = Path::new(source);
884 if path.as_os_str().is_empty()
885 || path.components().any(|component| {
886 matches!(
887 component,
888 std::path::Component::ParentDir
889 | std::path::Component::RootDir
890 | std::path::Component::Prefix(_)
891 )
892 })
893 {
894 None
895 } else {
896 Some(path)
897 }
898}
899
900pub(super) fn lookup_internal_file_id(
901 ctx: &ResolveContext<'_>,
902 candidate: &Path,
903) -> Option<FileId> {
904 if let Some(&file_id) = ctx.raw_path_to_id.get(candidate) {
905 return Some(file_id);
906 }
907 if let Some(&file_id) = ctx.path_to_id.get(candidate) {
908 return Some(file_id);
909 }
910 #[cfg(not(miri))]
911 if let Some(canonical) = ctx.canonicalize_cache.get(candidate) {
912 if let Some(&file_id) = ctx.path_to_id.get(canonical.as_path()) {
913 return Some(file_id);
914 }
915 if let Some(fallback) = ctx.canonical_fallback
916 && let Some(file_id) = fallback.get(&canonical)
917 {
918 return Some(file_id);
919 }
920 }
921 None
922}
923
924pub fn extract_package_name_from_node_modules_path(path: &Path) -> Option<String> {
930 let components: Vec<&str> = path
931 .components()
932 .filter_map(|c| match c {
933 std::path::Component::Normal(s) => s.to_str(),
934 _ => None,
935 })
936 .collect();
937
938 let nm_idx = components.iter().rposition(|&c| c == "node_modules")?;
939
940 let after = &components[nm_idx + 1..];
941 if after.is_empty() {
942 return None;
943 }
944
945 if after[0].starts_with('@') {
946 if after.len() >= 2 {
947 Some(format!("{}/{}", after[0], after[1]))
948 } else {
949 Some(after[0].to_string())
950 }
951 } else {
952 Some(after[0].to_string())
953 }
954}
955
956pub(super) fn try_pnpm_workspace_fallback(
965 path: &Path,
966 path_to_id: &FxHashMap<&Path, FileId>,
967 workspace_roots: &FxHashMap<&str, &Path>,
968) -> Option<FileId> {
969 let components: Vec<&str> = path
970 .components()
971 .filter_map(|c| match c {
972 std::path::Component::Normal(s) => s.to_str(),
973 _ => None,
974 })
975 .collect();
976
977 let pnpm_idx = components.iter().position(|&c| c == ".pnpm")?;
978
979 let after_pnpm = &components[pnpm_idx + 1..];
980
981 let inner_nm_idx = after_pnpm.iter().position(|&c| c == "node_modules")?;
982 let after_inner_nm = &after_pnpm[inner_nm_idx + 1..];
983
984 if after_inner_nm.is_empty() {
985 return None;
986 }
987
988 let (pkg_name, pkg_name_components) = if after_inner_nm[0].starts_with('@') {
989 if after_inner_nm.len() >= 2 {
990 (format!("{}/{}", after_inner_nm[0], after_inner_nm[1]), 2)
991 } else {
992 return None;
993 }
994 } else {
995 (after_inner_nm[0].to_string(), 1)
996 };
997
998 let ws_root = workspace_roots.get(pkg_name.as_str())?;
999
1000 let relative_parts = &after_inner_nm[pkg_name_components..];
1001 if relative_parts.is_empty() {
1002 return None;
1003 }
1004
1005 let relative_path: PathBuf = relative_parts.iter().collect();
1006
1007 let direct = ws_root.join(&relative_path);
1008 if let Some(&file_id) = path_to_id.get(direct.as_path()) {
1009 return Some(file_id);
1010 }
1011
1012 try_source_fallback(&direct, path_to_id)
1013}
1014
1015pub(super) fn try_workspace_package_fallback(
1038 ctx: &ResolveContext<'_>,
1039 specifier: &str,
1040) -> Option<ResolveResult> {
1041 if !super::path_info::is_bare_specifier(specifier) {
1042 return None;
1043 }
1044 let pkg_name = super::path_info::extract_package_name(specifier);
1045
1046 let subpath = specifier
1047 .strip_prefix(pkg_name.as_str())
1048 .and_then(|s| s.strip_prefix('/'))
1049 .unwrap_or("");
1050 let source_subpath = PathBuf::from(subpath);
1051
1052 match try_manifest_workspace_resolution(ctx, &pkg_name, subpath, &source_subpath) {
1053 ManifestWorkspaceResolution::Resolved(result) => return Some(result),
1054 ManifestWorkspaceResolution::Blocked => return None,
1055 ManifestWorkspaceResolution::Continue => {}
1056 }
1057
1058 let ws_root =
1059 if let Some(manifest) = find_package_manifest(ctx.package_manifests, pkg_name.as_str()) {
1060 manifest.root.as_path()
1061 } else {
1062 *ctx.workspace_roots.get(pkg_name.as_str())?
1063 };
1064
1065 resolve_workspace_self_reference(ctx, ws_root, subpath, pkg_name)
1066}
1067
1068enum ManifestWorkspaceResolution {
1071 Resolved(ResolveResult),
1073 Blocked,
1075 Continue,
1077}
1078
1079fn try_manifest_workspace_resolution(
1082 ctx: &ResolveContext<'_>,
1083 pkg_name: &str,
1084 subpath: &str,
1085 source_subpath: &Path,
1086) -> ManifestWorkspaceResolution {
1087 let Some(manifest) = find_package_manifest(ctx.package_manifests, pkg_name) else {
1088 return ManifestWorkspaceResolution::Continue;
1089 };
1090
1091 if let Some(exports) = manifest.package_json.exports.as_ref() {
1092 let export_key = if subpath.is_empty() {
1093 ".".to_string()
1094 } else {
1095 format!("./{subpath}")
1096 };
1097 return match package_map_target(exports, &export_key, ctx.condition_names) {
1098 PackageMapTarget::Targets(targets) => {
1099 match resolve_package_map_targets(ctx, manifest, &targets, Some(source_subpath)) {
1100 Some(file_id) => ManifestWorkspaceResolution::Resolved(
1101 ResolveResult::InternalPackageModule {
1102 file_id,
1103 package_name: pkg_name.to_string(),
1104 },
1105 ),
1106 None => ManifestWorkspaceResolution::Continue,
1107 }
1108 }
1109 PackageMapTarget::NoMatch | PackageMapTarget::Blocked => {
1110 ManifestWorkspaceResolution::Blocked
1111 }
1112 };
1113 }
1114
1115 if let Some(file_id) = try_source_subpath(ctx, manifest, source_subpath) {
1116 return ManifestWorkspaceResolution::Resolved(ResolveResult::InternalPackageModule {
1117 file_id,
1118 package_name: pkg_name.to_string(),
1119 });
1120 }
1121
1122 ManifestWorkspaceResolution::Continue
1123}
1124
1125fn resolve_workspace_self_reference(
1129 ctx: &ResolveContext<'_>,
1130 ws_root: &Path,
1131 subpath: &str,
1132 package_name: String,
1133) -> Option<ResolveResult> {
1134 let root_file = ws_root.join("__fallow_ws_self_resolve__");
1135 let rel_spec = if subpath.is_empty() {
1136 "./".to_string()
1137 } else {
1138 format!("./{subpath}")
1139 };
1140
1141 super::work::note_oxc_resolve();
1142 let resolved = ctx.resolver.resolve_file(&root_file, &rel_spec).ok()?;
1143 let resolved_path = resolved.path();
1144
1145 if let Some(&file_id) = ctx.raw_path_to_id.get(resolved_path) {
1146 return Some(ResolveResult::InternalPackageModule {
1147 file_id,
1148 package_name,
1149 });
1150 }
1151 if let Some(canonical) = ctx.canonicalize_cache.get(resolved_path) {
1152 if let Some(&file_id) = ctx.path_to_id.get(canonical.as_path()) {
1153 return Some(ResolveResult::InternalPackageModule {
1154 file_id,
1155 package_name,
1156 });
1157 }
1158 if let Some(fallback) = ctx.canonical_fallback
1159 && let Some(file_id) = fallback.get(&canonical)
1160 {
1161 return Some(ResolveResult::InternalPackageModule {
1162 file_id,
1163 package_name,
1164 });
1165 }
1166 if let Some(file_id) = try_source_fallback(&canonical, ctx.path_to_id) {
1167 return Some(ResolveResult::InternalPackageModule {
1168 file_id,
1169 package_name,
1170 });
1171 }
1172 }
1173 None
1174}
1175
1176pub(super) fn make_glob_from_pattern(
1178 pattern: &fallow_types::extract::DynamicImportPattern,
1179) -> String {
1180 if pattern.prefix.contains('*') || pattern.prefix.contains('{') {
1181 return pattern.prefix.clone();
1182 }
1183 pattern.suffix.as_ref().map_or_else(
1184 || format!("{}*", pattern.prefix),
1185 |suffix| format!("{}*{}", pattern.prefix, suffix),
1186 )
1187}
1188
1189#[cfg(test)]
1190mod tests {
1191 use super::*;
1192 use crate::resolve::types::{CanonicalizeCache, TsconfigCache};
1193 use fallow_types::extract::ModuleLoadMechanism;
1194 use rustc_hash::FxHashSet;
1195
1196 fn with_package_map_ctx(
1197 root: PathBuf,
1198 name: Option<&str>,
1199 package_json: fallow_config::PackageJson,
1200 raw_files: &[(PathBuf, FileId)],
1201 f: impl FnOnce(&ResolveContext<'_>, &PackageManifestInfo, &Path),
1202 ) {
1203 let manifest = PackageManifestInfo {
1204 root: root.clone(),
1205 canonical_root: root,
1206 name: name.map(str::to_string),
1207 package_json,
1208 deno_import_map: Vec::new(),
1209 };
1210 let manifests = [manifest];
1211 let mut raw_path_to_id = FxHashMap::default();
1212 for (path, file_id) in raw_files {
1213 raw_path_to_id.insert(path.as_path(), *file_id);
1214 }
1215 let path_to_id: FxHashMap<&Path, FileId> = FxHashMap::default();
1216 let workspace_roots: FxHashMap<&str, &Path> = FxHashMap::default();
1217 let condition_names = conditions();
1218 let resolver = oxc_resolver::Resolver::new(oxc_resolver::ResolveOptions::default());
1219 let tsconfig_warned = std::sync::Mutex::new(FxHashSet::default());
1220 let tsconfig_cache = TsconfigCache::default();
1221 let canonicalize_cache = CanonicalizeCache::default();
1222 let ctx = ResolveContext {
1223 resolver: &resolver,
1224 style_resolver: &resolver,
1225 extensions: &[],
1226 path_to_id: &path_to_id,
1227 raw_path_to_id: &raw_path_to_id,
1228 workspace_roots: &workspace_roots,
1229 package_manifests: &manifests,
1230 has_deno_import_maps: false,
1231 condition_names: &condition_names,
1232 path_aliases: &[],
1233 scss_include_paths: &[],
1234 static_dir_mappings: &[],
1235 framework_static_dir_mappings: &[],
1236 root: &manifests[0].root,
1237 canonical_fallback: None,
1238 tsconfig_warned: &tsconfig_warned,
1239 tsconfig_cache: &tsconfig_cache,
1240 canonicalize_cache: &canonicalize_cache,
1241 };
1242
1243 f(&ctx, &manifests[0], &manifests[0].root);
1244 }
1245
1246 #[test]
1247 fn alias_match_remainder_exact_key() {
1248 assert_eq!(alias_match_remainder("vscode", "vscode"), Some(""));
1249 assert_eq!(alias_match_remainder("@scope/sdk", "@scope/sdk"), Some(""));
1250 }
1251
1252 #[test]
1253 fn alias_match_remainder_slash_continuation() {
1254 assert_eq!(
1255 alias_match_remainder("@scope/sdk/sub", "@scope/sdk"),
1256 Some("/sub")
1257 );
1258 assert_eq!(alias_match_remainder("@/foo", "@/"), Some("foo"));
1259 assert_eq!(
1260 alias_match_remainder("~/components/x", "~/"),
1261 Some("components/x")
1262 );
1263 assert_eq!(alias_match_remainder("$lib/util", "$lib/"), Some("util"));
1264 }
1265
1266 #[test]
1267 fn alias_match_remainder_rejects_prefix_collision() {
1268 assert_eq!(
1269 alias_match_remainder("@scope/sdk-extra", "@scope/sdk"),
1270 None
1271 );
1272 assert_eq!(
1273 alias_match_remainder("vscode-languageserver", "vscode"),
1274 None
1275 );
1276 assert_eq!(alias_match_remainder("#shared-utils", "#shared"), None);
1277 }
1278
1279 #[test]
1280 fn alias_match_remainder_non_match() {
1281 assert_eq!(alias_match_remainder("react", "vscode"), None);
1282 }
1283
1284 #[test]
1285 fn test_extract_package_name_from_node_modules_path_regular() {
1286 let path = PathBuf::from("/project/node_modules/react/index.js");
1287 assert_eq!(
1288 extract_package_name_from_node_modules_path(&path),
1289 Some("react".to_string())
1290 );
1291 }
1292
1293 #[test]
1294 fn test_extract_package_name_from_node_modules_path_scoped() {
1295 let path = PathBuf::from("/project/node_modules/@babel/core/lib/index.js");
1296 assert_eq!(
1297 extract_package_name_from_node_modules_path(&path),
1298 Some("@babel/core".to_string())
1299 );
1300 }
1301
1302 #[test]
1303 fn test_extract_package_name_from_node_modules_path_nested() {
1304 let path = PathBuf::from("/project/node_modules/pkg-a/node_modules/pkg-b/dist/index.js");
1305 assert_eq!(
1306 extract_package_name_from_node_modules_path(&path),
1307 Some("pkg-b".to_string())
1308 );
1309 }
1310
1311 #[test]
1312 fn test_extract_package_name_from_node_modules_path_deep_subpath() {
1313 let path = PathBuf::from("/project/node_modules/react-dom/cjs/react-dom.production.min.js");
1314 assert_eq!(
1315 extract_package_name_from_node_modules_path(&path),
1316 Some("react-dom".to_string())
1317 );
1318 }
1319
1320 #[test]
1321 fn test_extract_package_name_from_node_modules_path_no_node_modules() {
1322 let path = PathBuf::from("/project/src/components/Button.tsx");
1323 assert_eq!(extract_package_name_from_node_modules_path(&path), None);
1324 }
1325
1326 #[test]
1327 fn test_extract_package_name_from_node_modules_path_just_node_modules() {
1328 let path = PathBuf::from("/project/node_modules");
1329 assert_eq!(extract_package_name_from_node_modules_path(&path), None);
1330 }
1331
1332 #[test]
1333 fn test_extract_package_name_from_node_modules_path_scoped_only_scope() {
1334 let path = PathBuf::from("/project/node_modules/@scope");
1335 assert_eq!(
1336 extract_package_name_from_node_modules_path(&path),
1337 Some("@scope".to_string())
1338 );
1339 }
1340
1341 #[test]
1342 fn test_resolve_specifier_node_modules_returns_npm_package() {
1343 let path =
1344 PathBuf::from("/project/node_modules/styled-components/dist/styled-components.esm.js");
1345 assert_eq!(
1346 extract_package_name_from_node_modules_path(&path),
1347 Some("styled-components".to_string())
1348 );
1349
1350 let path = PathBuf::from("/project/node_modules/next/dist/server/next.js");
1351 assert_eq!(
1352 extract_package_name_from_node_modules_path(&path),
1353 Some("next".to_string())
1354 );
1355 }
1356
1357 #[test]
1358 fn test_try_source_fallback_dist_to_src() {
1359 let src_path = PathBuf::from("/project/packages/ui/src/utils.ts");
1360 let mut path_to_id = FxHashMap::default();
1361 path_to_id.insert(src_path.as_path(), FileId(0));
1362
1363 let dist_path = PathBuf::from("/project/packages/ui/dist/utils.js");
1364 assert_eq!(
1365 try_source_fallback(&dist_path, &path_to_id),
1366 Some(FileId(0)),
1367 "dist/utils.js should fall back to src/utils.ts"
1368 );
1369 }
1370
1371 #[test]
1372 fn test_try_source_fallback_build_to_src() {
1373 let src_path = PathBuf::from("/project/packages/core/src/index.tsx");
1374 let mut path_to_id = FxHashMap::default();
1375 path_to_id.insert(src_path.as_path(), FileId(1));
1376
1377 let build_path = PathBuf::from("/project/packages/core/build/index.js");
1378 assert_eq!(
1379 try_source_fallback(&build_path, &path_to_id),
1380 Some(FileId(1)),
1381 "build/index.js should fall back to src/index.tsx"
1382 );
1383 }
1384
1385 #[test]
1386 fn test_try_source_fallback_no_match() {
1387 let path_to_id: FxHashMap<&Path, FileId> = FxHashMap::default();
1388
1389 let dist_path = PathBuf::from("/project/packages/ui/dist/utils.js");
1390 assert_eq!(
1391 try_source_fallback(&dist_path, &path_to_id),
1392 None,
1393 "should return None when no source file exists"
1394 );
1395 }
1396
1397 #[test]
1398 fn test_try_source_fallback_non_output_dir() {
1399 let src_path = PathBuf::from("/project/packages/ui/src/utils.ts");
1400 let mut path_to_id = FxHashMap::default();
1401 path_to_id.insert(src_path.as_path(), FileId(0));
1402
1403 let normal_path = PathBuf::from("/project/packages/ui/scripts/utils.js");
1404 assert_eq!(
1405 try_source_fallback(&normal_path, &path_to_id),
1406 None,
1407 "non-output directory path should not trigger fallback"
1408 );
1409 }
1410
1411 #[test]
1412 fn test_try_source_fallback_nested_path() {
1413 let src_path = PathBuf::from("/project/packages/ui/src/components/Button.ts");
1414 let mut path_to_id = FxHashMap::default();
1415 path_to_id.insert(src_path.as_path(), FileId(2));
1416
1417 let dist_path = PathBuf::from("/project/packages/ui/dist/components/Button.js");
1418 assert_eq!(
1419 try_source_fallback(&dist_path, &path_to_id),
1420 Some(FileId(2)),
1421 "nested dist path should fall back to nested src path"
1422 );
1423 }
1424
1425 #[test]
1426 fn test_try_source_fallback_nested_dist_esm() {
1427 let src_path = PathBuf::from("/project/packages/ui/src/utils.ts");
1428 let mut path_to_id = FxHashMap::default();
1429 path_to_id.insert(src_path.as_path(), FileId(0));
1430
1431 let dist_path = PathBuf::from("/project/packages/ui/dist/esm/utils.mjs");
1432 assert_eq!(
1433 try_source_fallback(&dist_path, &path_to_id),
1434 Some(FileId(0)),
1435 "dist/esm/utils.mjs should fall back to src/utils.ts"
1436 );
1437 }
1438
1439 #[test]
1440 fn test_try_source_fallback_nested_build_cjs() {
1441 let src_path = PathBuf::from("/project/packages/core/src/index.ts");
1442 let mut path_to_id = FxHashMap::default();
1443 path_to_id.insert(src_path.as_path(), FileId(1));
1444
1445 let build_path = PathBuf::from("/project/packages/core/build/cjs/index.cjs");
1446 assert_eq!(
1447 try_source_fallback(&build_path, &path_to_id),
1448 Some(FileId(1)),
1449 "build/cjs/index.cjs should fall back to src/index.ts"
1450 );
1451 }
1452
1453 #[test]
1454 fn test_try_source_fallback_nested_dist_esm_deep_path() {
1455 let src_path = PathBuf::from("/project/packages/ui/src/components/Button.ts");
1456 let mut path_to_id = FxHashMap::default();
1457 path_to_id.insert(src_path.as_path(), FileId(2));
1458
1459 let dist_path = PathBuf::from("/project/packages/ui/dist/esm/components/Button.mjs");
1460 assert_eq!(
1461 try_source_fallback(&dist_path, &path_to_id),
1462 Some(FileId(2)),
1463 "dist/esm/components/Button.mjs should fall back to src/components/Button.ts"
1464 );
1465 }
1466
1467 #[test]
1468 fn test_try_source_fallback_triple_nested_output_dirs() {
1469 let src_path = PathBuf::from("/project/packages/ui/src/utils.ts");
1470 let mut path_to_id = FxHashMap::default();
1471 path_to_id.insert(src_path.as_path(), FileId(0));
1472
1473 let dist_path = PathBuf::from("/project/packages/ui/out/dist/esm/utils.mjs");
1474 assert_eq!(
1475 try_source_fallback(&dist_path, &path_to_id),
1476 Some(FileId(0)),
1477 "out/dist/esm/utils.mjs should fall back to src/utils.ts"
1478 );
1479 }
1480
1481 #[test]
1482 fn test_try_source_fallback_parent_dir_named_build() {
1483 let src_path = PathBuf::from("/home/user/build/my-project/src/utils.ts");
1484 let mut path_to_id = FxHashMap::default();
1485 path_to_id.insert(src_path.as_path(), FileId(0));
1486
1487 let dist_path = PathBuf::from("/home/user/build/my-project/dist/utils.js");
1488 assert_eq!(
1489 try_source_fallback(&dist_path, &path_to_id),
1490 Some(FileId(0)),
1491 "should resolve dist/ within project, not match parent 'build' dir"
1492 );
1493 }
1494
1495 #[test]
1496 fn package_map_exact_entry_beats_pattern_entry() {
1497 let map = serde_json::json!({
1498 "#nitro/runtime/task": "./dist/special/task.mjs",
1499 "#nitro/runtime/*": "./dist/runtime/internal/*.mjs"
1500 });
1501 assert_eq!(
1502 package_map_target(&map, "#nitro/runtime/task", &conditions()),
1503 PackageMapTarget::Targets(vec!["./dist/special/task.mjs".to_string()])
1504 );
1505 }
1506
1507 #[test]
1508 fn package_map_wildcard_substitutes_capture() {
1509 let map = serde_json::json!({
1510 "#nitro/runtime/*": "./dist/runtime/internal/*.mjs"
1511 });
1512 assert_eq!(
1513 package_map_target(&map, "#nitro/runtime/task", &conditions()),
1514 PackageMapTarget::Targets(vec!["./dist/runtime/internal/task.mjs".to_string()])
1515 );
1516 }
1517
1518 #[test]
1519 fn package_map_exact_entry_with_no_target_blocks_pattern_entry() {
1520 let map = serde_json::json!({
1521 "#nitro/runtime/task": null,
1522 "#nitro/runtime/*": "./dist/runtime/internal/*.mjs"
1523 });
1524 assert_eq!(
1525 package_map_target(&map, "#nitro/runtime/task", &conditions()),
1526 PackageMapTarget::Blocked
1527 );
1528 }
1529
1530 #[test]
1531 fn package_map_best_pattern_with_no_target_blocks_broader_pattern() {
1532 let map = serde_json::json!({
1533 "#nitro/runtime/internal/*": null,
1534 "#nitro/runtime/*": "./dist/runtime/*.mjs"
1535 });
1536 assert_eq!(
1537 package_map_target(&map, "#nitro/runtime/internal/task", &conditions()),
1538 PackageMapTarget::Blocked
1539 );
1540 }
1541
1542 #[test]
1543 fn package_map_unmatched_subpath_is_not_a_target() {
1544 let map = serde_json::json!({
1545 "./query": "./dist/query/index.js"
1546 });
1547 assert_eq!(
1548 package_map_target(&map, "./private", &conditions()),
1549 PackageMapTarget::NoMatch
1550 );
1551 }
1552
1553 #[test]
1554 fn package_map_nested_conditions_follow_manifest_order() {
1555 let map = serde_json::json!({
1556 "./query/react": {
1557 "types": "./dist/query/react/index.d.ts",
1558 "import": {
1559 "development": "./src/query/react/index.ts",
1560 "default": "./dist/query/react/index.js"
1561 },
1562 "default": "./dist/query/react/index.cjs"
1563 }
1564 });
1565 assert_eq!(
1566 package_map_target(&map, "./query/react", &conditions()),
1567 PackageMapTarget::Targets(vec!["./dist/query/react/index.d.ts".to_string()])
1568 );
1569 }
1570
1571 #[test]
1572 fn package_map_import_before_types_selects_runtime_branch() {
1573 let map = serde_json::json!({
1574 ".": {
1575 "import": "./dist/index.js",
1576 "types": "./dist/index.d.ts"
1577 }
1578 });
1579 assert_eq!(
1580 package_map_target(&map, ".", &conditions()),
1581 PackageMapTarget::Targets(vec!["./dist/index.js".to_string()])
1582 );
1583 }
1584
1585 #[test]
1586 fn package_map_condition_order_follows_manifest_order() {
1587 let map = serde_json::json!({
1588 ".": {
1589 "node": "./dist/node.js",
1590 "import": "./dist/index.js"
1591 }
1592 });
1593 assert_eq!(
1594 package_map_target(&map, ".", &conditions()),
1595 PackageMapTarget::Targets(vec!["./dist/node.js".to_string()])
1596 );
1597 }
1598
1599 #[test]
1600 fn package_map_arrays_preserve_fallback_order() {
1601 let map = serde_json::json!({
1602 "#array": ["./dist/missing.js", "./src/array.ts"],
1603 "#null": null,
1604 "#false": false
1605 });
1606 assert_eq!(
1607 package_map_target(&map, "#array", &conditions()),
1608 PackageMapTarget::Targets(vec![
1609 "./dist/missing.js".to_string(),
1610 "./src/array.ts".to_string()
1611 ])
1612 );
1613 assert_eq!(
1614 package_map_target(&map, "#null", &conditions()),
1615 PackageMapTarget::Blocked
1616 );
1617 assert_eq!(
1618 package_map_target(&map, "#false", &conditions()),
1619 PackageMapTarget::Blocked
1620 );
1621 }
1622
1623 #[test]
1624 fn package_map_non_relative_target_does_not_trigger_source_fallback() {
1625 with_package_map_ctx(
1626 PathBuf::from("/project"),
1627 Some("pkg"),
1628 fallow_config::PackageJson::default(),
1629 &[],
1630 |ctx, manifest, _| {
1631 assert!(resolve_package_map_target(ctx, manifest, "lodash", None).is_none());
1632 assert!(
1633 resolve_package_map_target(ctx, manifest, "../dist/index.js", None).is_none()
1634 );
1635 },
1636 );
1637 }
1638
1639 #[test]
1640 fn package_map_targets_use_first_reachable_target() {
1641 let root = PathBuf::from("/project");
1642 let src_path = root.join("src/feature.ts");
1643 let targets = vec![
1644 "./dist/missing.js".to_string(),
1645 "./src/feature.ts".to_string(),
1646 ];
1647
1648 with_package_map_ctx(
1649 root,
1650 Some("pkg"),
1651 fallow_config::PackageJson::default(),
1652 &[(src_path, FileId(9))],
1653 |ctx, manifest, _| {
1654 assert_eq!(
1655 resolve_package_map_targets(ctx, manifest, &targets, None),
1656 Some(FileId(9))
1657 );
1658 },
1659 );
1660 }
1661
1662 #[test]
1663 fn package_imports_fallback_supports_external_package_targets() {
1664 let root = PathBuf::from("/project");
1665 with_package_map_ctx(
1666 root,
1667 Some("pkg"),
1668 fallow_config::PackageJson {
1669 imports: Some(serde_json::json!({
1670 "#pad": "left-pad",
1671 "#scoped": "@scope/pkg/subpath"
1672 })),
1673 ..Default::default()
1674 },
1675 &[],
1676 |ctx, _, root| {
1677 let pad = try_package_imports_fallback(ctx, &root.join("src/index.ts"), "#pad");
1678 assert!(matches!(pad, Some(ResolveResult::NpmPackage(pkg)) if pkg == "left-pad"));
1679
1680 let scoped =
1681 try_package_imports_fallback(ctx, &root.join("src/index.ts"), "#scoped");
1682 assert!(
1683 matches!(scoped, Some(ResolveResult::NpmPackage(pkg)) if pkg == "@scope/pkg")
1684 );
1685 },
1686 );
1687 }
1688
1689 #[test]
1690 fn package_imports_workspace_target_names_only_known_packages() {
1691 let root = PathBuf::from("/project");
1692 with_package_map_ctx(
1693 root,
1694 Some("pkg"),
1695 fallow_config::PackageJson {
1696 imports: Some(serde_json::json!({
1697 "#self/*": "pkg/*",
1698 "#pad": "left-pad",
1699 "#local/*": "./src/*.ts"
1700 })),
1701 ..Default::default()
1702 },
1703 &[],
1704 |ctx, _, root| {
1705 let from = root.join("src/index.ts");
1706 let feature = root.join("src/feature.ts");
1707 assert_eq!(
1708 package_imports_workspace_target(ctx, &from, "#self/feature", &feature),
1709 Some("pkg".to_string())
1710 );
1711 assert_eq!(
1712 package_imports_workspace_target(
1713 ctx,
1714 &from,
1715 "#self/feature",
1716 Path::new("/elsewhere/feature.ts")
1717 ),
1718 None,
1719 "a file outside the named package must not credit it"
1720 );
1721 assert_eq!(
1722 package_imports_workspace_target(
1723 ctx,
1724 &from,
1725 "#pad",
1726 &root.join("node_modules/left-pad/index.js")
1727 ),
1728 None
1729 );
1730 assert_eq!(
1731 package_imports_workspace_target(ctx, &from, "#local/feature", &feature),
1732 None
1733 );
1734 assert_eq!(
1735 package_imports_workspace_target(ctx, &from, "pkg/feature", &feature),
1736 None
1737 );
1738 },
1739 );
1740 }
1741
1742 #[test]
1743 fn package_imports_fallback_supports_unnamed_packages() {
1744 let root = PathBuf::from("/project");
1745 let src_path = root.join("src/runtime/task.ts");
1746 with_package_map_ctx(
1747 root,
1748 None,
1749 fallow_config::PackageJson {
1750 imports: Some(serde_json::json!({
1751 "#runtime/*": "./dist/runtime/*.mjs"
1752 })),
1753 ..Default::default()
1754 },
1755 &[(src_path, FileId(7))],
1756 |ctx, _, root| {
1757 let result =
1758 try_package_imports_fallback(ctx, &root.join("src/index.ts"), "#runtime/task");
1759 assert!(matches!(
1760 result,
1761 Some(ResolveResult::InternalModule(FileId(7)))
1762 ));
1763 },
1764 );
1765 }
1766
1767 #[test]
1768 #[cfg_attr(miri, ignore)]
1769 fn relative_package_root_source_fallback_uses_package_source_entry() {
1770 let root = PathBuf::from("/project");
1771 let source_path = root.join("custom/entry.js");
1772 with_package_map_ctx(
1773 root,
1774 Some("pkg"),
1775 fallow_config::PackageJson {
1776 source: Some("custom/entry.js".to_string()),
1777 ..Default::default()
1778 },
1779 &[(source_path, FileId(11))],
1780 |ctx, _, root| {
1781 let result = try_relative_package_root_source_fallback(
1782 ctx,
1783 &root.join("test/shared/exports.test.js"),
1784 "../../",
1785 );
1786 assert!(matches!(
1787 result,
1788 Some(ResolveResult::InternalModule(FileId(11)))
1789 ));
1790 },
1791 );
1792 }
1793
1794 #[test]
1795 fn package_source_path_accepts_relative_source_entries() {
1796 assert_eq!(
1797 safe_relative_package_source_path("src/index.js"),
1798 Some(Path::new("src/index.js"))
1799 );
1800 assert_eq!(
1801 safe_relative_package_source_path("./custom/entry.ts"),
1802 Some(Path::new("custom/entry.ts"))
1803 );
1804 }
1805
1806 #[test]
1807 fn package_source_path_rejects_unsafe_entries() {
1808 assert_eq!(safe_relative_package_source_path(""), None);
1809 assert_eq!(safe_relative_package_source_path("./"), None);
1810 assert_eq!(safe_relative_package_source_path("../src/index.js"), None);
1811 assert_eq!(safe_relative_package_source_path("src/../index.js"), None);
1812 assert_eq!(safe_relative_package_source_path("/src/index.js"), None);
1813
1814 #[cfg(windows)]
1815 assert_eq!(safe_relative_package_source_path("C:\\src\\index.js"), None);
1816 }
1817
1818 #[test]
1819 fn test_pnpm_store_path_extract_package_name() {
1820 let path =
1821 PathBuf::from("/project/node_modules/.pnpm/react@18.2.0/node_modules/react/index.js");
1822 assert_eq!(
1823 extract_package_name_from_node_modules_path(&path),
1824 Some("react".to_string())
1825 );
1826 }
1827
1828 #[test]
1829 fn test_pnpm_store_path_scoped_package() {
1830 let path = PathBuf::from(
1831 "/project/node_modules/.pnpm/@babel+core@7.24.0/node_modules/@babel/core/lib/index.js",
1832 );
1833 assert_eq!(
1834 extract_package_name_from_node_modules_path(&path),
1835 Some("@babel/core".to_string())
1836 );
1837 }
1838
1839 fn conditions() -> Vec<String> {
1840 vec![
1841 "development".to_string(),
1842 "import".to_string(),
1843 "require".to_string(),
1844 "default".to_string(),
1845 "types".to_string(),
1846 "node".to_string(),
1847 ]
1848 }
1849
1850 #[test]
1851 fn test_pnpm_store_path_with_peer_deps() {
1852 let path = PathBuf::from(
1853 "/project/node_modules/.pnpm/webpack@5.0.0_esbuild@0.19.0/node_modules/webpack/lib/index.js",
1854 );
1855 assert_eq!(
1856 extract_package_name_from_node_modules_path(&path),
1857 Some("webpack".to_string())
1858 );
1859 }
1860
1861 #[test]
1862 fn test_try_pnpm_workspace_fallback_dist_to_src() {
1863 let src_path = PathBuf::from("/project/packages/ui/src/utils.ts");
1864 let mut path_to_id = FxHashMap::default();
1865 path_to_id.insert(src_path.as_path(), FileId(0));
1866
1867 let mut workspace_roots = FxHashMap::default();
1868 let ws_root = PathBuf::from("/project/packages/ui");
1869 workspace_roots.insert("@myorg/ui", ws_root.as_path());
1870
1871 let pnpm_path = PathBuf::from(
1872 "/project/node_modules/.pnpm/@myorg+ui@1.0.0/node_modules/@myorg/ui/dist/utils.js",
1873 );
1874 assert_eq!(
1875 try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
1876 Some(FileId(0)),
1877 ".pnpm workspace path should fall back to src/utils.ts"
1878 );
1879 }
1880
1881 #[test]
1882 fn test_try_pnpm_workspace_fallback_direct_source() {
1883 let src_path = PathBuf::from("/project/packages/core/src/index.ts");
1884 let mut path_to_id = FxHashMap::default();
1885 path_to_id.insert(src_path.as_path(), FileId(1));
1886
1887 let mut workspace_roots = FxHashMap::default();
1888 let ws_root = PathBuf::from("/project/packages/core");
1889 workspace_roots.insert("@myorg/core", ws_root.as_path());
1890
1891 let pnpm_path = PathBuf::from(
1892 "/project/node_modules/.pnpm/@myorg+core@workspace/node_modules/@myorg/core/src/index.ts",
1893 );
1894 assert_eq!(
1895 try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
1896 Some(FileId(1)),
1897 ".pnpm workspace path with src/ should resolve directly"
1898 );
1899 }
1900
1901 #[test]
1902 fn test_try_pnpm_workspace_fallback_non_workspace_package() {
1903 let path_to_id: FxHashMap<&Path, FileId> = FxHashMap::default();
1904
1905 let mut workspace_roots = FxHashMap::default();
1906 let ws_root = PathBuf::from("/project/packages/ui");
1907 workspace_roots.insert("@myorg/ui", ws_root.as_path());
1908
1909 let pnpm_path =
1910 PathBuf::from("/project/node_modules/.pnpm/react@18.2.0/node_modules/react/index.js");
1911 assert_eq!(
1912 try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
1913 None,
1914 "non-workspace package in .pnpm should return None"
1915 );
1916 }
1917
1918 #[test]
1919 fn test_try_pnpm_workspace_fallback_unscoped_package() {
1920 let src_path = PathBuf::from("/project/packages/utils/src/index.ts");
1921 let mut path_to_id = FxHashMap::default();
1922 path_to_id.insert(src_path.as_path(), FileId(2));
1923
1924 let mut workspace_roots = FxHashMap::default();
1925 let ws_root = PathBuf::from("/project/packages/utils");
1926 workspace_roots.insert("my-utils", ws_root.as_path());
1927
1928 let pnpm_path = PathBuf::from(
1929 "/project/node_modules/.pnpm/my-utils@1.0.0/node_modules/my-utils/dist/index.js",
1930 );
1931 assert_eq!(
1932 try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
1933 Some(FileId(2)),
1934 "unscoped workspace package in .pnpm should resolve"
1935 );
1936 }
1937
1938 #[test]
1939 fn test_try_pnpm_workspace_fallback_nested_path() {
1940 let src_path = PathBuf::from("/project/packages/ui/src/components/Button.ts");
1941 let mut path_to_id = FxHashMap::default();
1942 path_to_id.insert(src_path.as_path(), FileId(3));
1943
1944 let mut workspace_roots = FxHashMap::default();
1945 let ws_root = PathBuf::from("/project/packages/ui");
1946 workspace_roots.insert("@myorg/ui", ws_root.as_path());
1947
1948 let pnpm_path = PathBuf::from(
1949 "/project/node_modules/.pnpm/@myorg+ui@1.0.0/node_modules/@myorg/ui/dist/components/Button.js",
1950 );
1951 assert_eq!(
1952 try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
1953 Some(FileId(3)),
1954 "nested .pnpm workspace path should resolve through source fallback"
1955 );
1956 }
1957
1958 #[test]
1959 fn test_try_pnpm_workspace_fallback_no_pnpm() {
1960 let path_to_id: FxHashMap<&Path, FileId> = FxHashMap::default();
1961 let workspace_roots: FxHashMap<&str, &Path> = FxHashMap::default();
1962
1963 let regular_path = PathBuf::from("/project/node_modules/react/index.js");
1964 assert_eq!(
1965 try_pnpm_workspace_fallback(®ular_path, &path_to_id, &workspace_roots),
1966 None,
1967 );
1968 }
1969
1970 #[test]
1971 fn test_try_pnpm_workspace_fallback_with_peer_deps() {
1972 let src_path = PathBuf::from("/project/packages/ui/src/index.ts");
1973 let mut path_to_id = FxHashMap::default();
1974 path_to_id.insert(src_path.as_path(), FileId(4));
1975
1976 let mut workspace_roots = FxHashMap::default();
1977 let ws_root = PathBuf::from("/project/packages/ui");
1978 workspace_roots.insert("@myorg/ui", ws_root.as_path());
1979
1980 let pnpm_path = PathBuf::from(
1981 "/project/node_modules/.pnpm/@myorg+ui@1.0.0_react@18.2.0/node_modules/@myorg/ui/dist/index.js",
1982 );
1983 assert_eq!(
1984 try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
1985 Some(FileId(4)),
1986 ".pnpm path with peer dep suffix should still resolve"
1987 );
1988 }
1989
1990 #[test]
1991 fn make_glob_prefix_only_no_suffix() {
1992 let pattern = fallow_types::extract::DynamicImportPattern {
1993 prefix: "./locales/".to_string(),
1994 suffix: None,
1995 span: oxc_span::Span::default(),
1996 mechanism: ModuleLoadMechanism::EsModule,
1997 };
1998 assert_eq!(make_glob_from_pattern(&pattern), "./locales/*");
1999 }
2000
2001 #[test]
2002 fn make_glob_prefix_with_suffix() {
2003 let pattern = fallow_types::extract::DynamicImportPattern {
2004 prefix: "./locales/".to_string(),
2005 suffix: Some(".json".to_string()),
2006 span: oxc_span::Span::default(),
2007 mechanism: ModuleLoadMechanism::EsModule,
2008 };
2009 assert_eq!(make_glob_from_pattern(&pattern), "./locales/*.json");
2010 }
2011
2012 #[test]
2013 fn make_glob_passthrough_star() {
2014 let pattern = fallow_types::extract::DynamicImportPattern {
2015 prefix: "./pages/**/*.tsx".to_string(),
2016 suffix: None,
2017 span: oxc_span::Span::default(),
2018 mechanism: ModuleLoadMechanism::EsModule,
2019 };
2020 assert_eq!(make_glob_from_pattern(&pattern), "./pages/**/*.tsx");
2021 }
2022
2023 #[test]
2024 fn make_glob_passthrough_brace() {
2025 let pattern = fallow_types::extract::DynamicImportPattern {
2026 prefix: "./i18n/{en,de,fr}.json".to_string(),
2027 suffix: None,
2028 span: oxc_span::Span::default(),
2029 mechanism: ModuleLoadMechanism::EsModule,
2030 };
2031 assert_eq!(make_glob_from_pattern(&pattern), "./i18n/{en,de,fr}.json");
2032 }
2033
2034 #[test]
2035 fn make_glob_empty_prefix_no_suffix() {
2036 let pattern = fallow_types::extract::DynamicImportPattern {
2037 prefix: String::new(),
2038 suffix: None,
2039 span: oxc_span::Span::default(),
2040 mechanism: ModuleLoadMechanism::EsModule,
2041 };
2042 assert_eq!(make_glob_from_pattern(&pattern), "*");
2043 }
2044
2045 #[test]
2046 fn make_glob_empty_prefix_with_suffix() {
2047 let pattern = fallow_types::extract::DynamicImportPattern {
2048 prefix: String::new(),
2049 suffix: Some(".ts".to_string()),
2050 span: oxc_span::Span::default(),
2051 mechanism: ModuleLoadMechanism::EsModule,
2052 };
2053 assert_eq!(make_glob_from_pattern(&pattern), "*.ts");
2054 }
2055
2056 #[test]
2057 fn make_glob_template_literal_prefix_only() {
2058 let pattern = fallow_types::extract::DynamicImportPattern {
2059 prefix: "./pages/".to_string(),
2060 suffix: None,
2061 span: oxc_span::Span::default(),
2062 mechanism: ModuleLoadMechanism::EsModule,
2063 };
2064 assert_eq!(make_glob_from_pattern(&pattern), "./pages/*");
2065 }
2066
2067 #[test]
2068 fn make_glob_template_literal_deep_prefix() {
2069 let pattern = fallow_types::extract::DynamicImportPattern {
2070 prefix: "./modules/".to_string(),
2071 suffix: None,
2072 span: oxc_span::Span::default(),
2073 mechanism: ModuleLoadMechanism::EsModule,
2074 };
2075 assert_eq!(make_glob_from_pattern(&pattern), "./modules/*");
2076 }
2077
2078 #[test]
2079 fn make_glob_string_concat_with_extension() {
2080 let pattern = fallow_types::extract::DynamicImportPattern {
2081 prefix: "./views/".to_string(),
2082 suffix: Some(".vue".to_string()),
2083 span: oxc_span::Span::default(),
2084 mechanism: ModuleLoadMechanism::EsModule,
2085 };
2086 assert_eq!(make_glob_from_pattern(&pattern), "./views/*.vue");
2087 }
2088
2089 #[test]
2090 fn make_glob_import_meta_glob_recursive() {
2091 let pattern = fallow_types::extract::DynamicImportPattern {
2092 prefix: "./components/**/*.vue".to_string(),
2093 suffix: None,
2094 span: oxc_span::Span::default(),
2095 mechanism: ModuleLoadMechanism::EsModule,
2096 };
2097 assert_eq!(
2098 make_glob_from_pattern(&pattern),
2099 "./components/**/*.vue",
2100 "import.meta.glob patterns with * should pass through as-is"
2101 );
2102 }
2103
2104 #[test]
2105 fn make_glob_import_meta_glob_brace_expansion() {
2106 let pattern = fallow_types::extract::DynamicImportPattern {
2107 prefix: "./plugins/{auth,analytics}.ts".to_string(),
2108 suffix: None,
2109 span: oxc_span::Span::default(),
2110 mechanism: ModuleLoadMechanism::EsModule,
2111 };
2112 assert_eq!(
2113 make_glob_from_pattern(&pattern),
2114 "./plugins/{auth,analytics}.ts",
2115 "import.meta.glob patterns with braces should pass through as-is"
2116 );
2117 }
2118
2119 #[test]
2120 fn make_glob_import_meta_glob_star_with_brace() {
2121 let pattern = fallow_types::extract::DynamicImportPattern {
2122 prefix: "./routes/**/*.{ts,tsx}".to_string(),
2123 suffix: None,
2124 span: oxc_span::Span::default(),
2125 mechanism: ModuleLoadMechanism::EsModule,
2126 };
2127 assert_eq!(
2128 make_glob_from_pattern(&pattern),
2129 "./routes/**/*.{ts,tsx}",
2130 "combined * and brace patterns should pass through"
2131 );
2132 }
2133
2134 #[test]
2135 fn make_glob_import_meta_glob_ignores_suffix_when_star_present() {
2136 let pattern = fallow_types::extract::DynamicImportPattern {
2137 prefix: "./*.ts".to_string(),
2138 suffix: Some(".extra".to_string()),
2139 span: oxc_span::Span::default(),
2140 mechanism: ModuleLoadMechanism::EsModule,
2141 };
2142 assert_eq!(
2143 make_glob_from_pattern(&pattern),
2144 "./*.ts",
2145 "when prefix has glob chars, suffix is ignored (prefix used as-is)"
2146 );
2147 }
2148
2149 #[test]
2150 fn make_glob_single_dot_prefix() {
2151 let pattern = fallow_types::extract::DynamicImportPattern {
2152 prefix: "./".to_string(),
2153 suffix: None,
2154 span: oxc_span::Span::default(),
2155 mechanism: ModuleLoadMechanism::EsModule,
2156 };
2157 assert_eq!(make_glob_from_pattern(&pattern), "./*");
2158 }
2159
2160 #[test]
2161 fn make_glob_prefix_without_trailing_slash() {
2162 let pattern = fallow_types::extract::DynamicImportPattern {
2163 prefix: "./config".to_string(),
2164 suffix: None,
2165 span: oxc_span::Span::default(),
2166 mechanism: ModuleLoadMechanism::EsModule,
2167 };
2168 assert_eq!(make_glob_from_pattern(&pattern), "./config*");
2169 }
2170
2171 #[test]
2172 fn make_glob_prefix_with_dotdot() {
2173 let pattern = fallow_types::extract::DynamicImportPattern {
2174 prefix: "../shared/".to_string(),
2175 suffix: Some(".ts".to_string()),
2176 span: oxc_span::Span::default(),
2177 mechanism: ModuleLoadMechanism::EsModule,
2178 };
2179 assert_eq!(make_glob_from_pattern(&pattern), "../shared/*.ts");
2180 }
2181
2182 #[test]
2183 fn test_extract_package_name_with_pnpm_plus_encoded_scope() {
2184 let path = PathBuf::from(
2185 "/project/node_modules/.pnpm/@mui+material@5.15.0/node_modules/@mui/material/index.js",
2186 );
2187 assert_eq!(
2188 extract_package_name_from_node_modules_path(&path),
2189 Some("@mui/material".to_string())
2190 );
2191 }
2192
2193 #[test]
2194 fn test_extract_package_name_windows_style_path() {
2195 let path = PathBuf::from("/project/node_modules/typescript/lib/tsc.js");
2196 assert_eq!(
2197 extract_package_name_from_node_modules_path(&path),
2198 Some("typescript".to_string())
2199 );
2200 }
2201
2202 #[test]
2203 fn test_try_source_fallback_out_dir() {
2204 let src_path = PathBuf::from("/project/packages/api/src/handler.ts");
2205 let mut path_to_id = FxHashMap::default();
2206 path_to_id.insert(src_path.as_path(), FileId(5));
2207
2208 let out_path = PathBuf::from("/project/packages/api/out/handler.js");
2209 assert_eq!(
2210 try_source_fallback(&out_path, &path_to_id),
2211 Some(FileId(5)),
2212 "out/handler.js should fall back to src/handler.ts"
2213 );
2214 }
2215
2216 #[test]
2217 fn test_try_source_fallback_mts_extension() {
2218 let src_path = PathBuf::from("/project/packages/lib/src/utils.mts");
2219 let mut path_to_id = FxHashMap::default();
2220 path_to_id.insert(src_path.as_path(), FileId(6));
2221
2222 let dist_path = PathBuf::from("/project/packages/lib/dist/utils.mjs");
2223 assert_eq!(
2224 try_source_fallback(&dist_path, &path_to_id),
2225 Some(FileId(6)),
2226 "dist/utils.mjs should fall back to src/utils.mts"
2227 );
2228 }
2229
2230 #[test]
2231 fn test_try_source_fallback_cts_extension() {
2232 let src_path = PathBuf::from("/project/packages/lib/src/config.cts");
2233 let mut path_to_id = FxHashMap::default();
2234 path_to_id.insert(src_path.as_path(), FileId(7));
2235
2236 let dist_path = PathBuf::from("/project/packages/lib/dist/config.cjs");
2237 assert_eq!(
2238 try_source_fallback(&dist_path, &path_to_id),
2239 Some(FileId(7)),
2240 "dist/config.cjs should fall back to src/config.cts"
2241 );
2242 }
2243
2244 #[test]
2245 fn test_try_source_fallback_jsx_extension() {
2246 let src_path = PathBuf::from("/project/packages/ui/src/App.jsx");
2247 let mut path_to_id = FxHashMap::default();
2248 path_to_id.insert(src_path.as_path(), FileId(8));
2249
2250 let build_path = PathBuf::from("/project/packages/ui/build/App.js");
2251 assert_eq!(
2252 try_source_fallback(&build_path, &path_to_id),
2253 Some(FileId(8)),
2254 "build/App.js should fall back to src/App.jsx"
2255 );
2256 }
2257
2258 #[test]
2259 fn test_try_source_fallback_no_file_stem() {
2260 let path_to_id: FxHashMap<&Path, FileId> = FxHashMap::default();
2261 let dist_path = PathBuf::from("/project/packages/ui/dist/");
2262 assert_eq!(
2263 try_source_fallback(&dist_path, &path_to_id),
2264 None,
2265 "directory path with no file should return None"
2266 );
2267 }
2268
2269 #[test]
2270 fn test_try_source_fallback_esm_subdir() {
2271 let src_path = PathBuf::from("/project/lib/src/index.ts");
2272 let mut path_to_id = FxHashMap::default();
2273 path_to_id.insert(src_path.as_path(), FileId(10));
2274
2275 let dist_path = PathBuf::from("/project/lib/esm/index.mjs");
2276 assert_eq!(
2277 try_source_fallback(&dist_path, &path_to_id),
2278 Some(FileId(10)),
2279 "standalone esm/ directory should fall back to src/"
2280 );
2281 }
2282
2283 #[test]
2284 fn test_try_source_fallback_cjs_subdir() {
2285 let src_path = PathBuf::from("/project/lib/src/index.ts");
2286 let mut path_to_id = FxHashMap::default();
2287 path_to_id.insert(src_path.as_path(), FileId(11));
2288
2289 let cjs_path = PathBuf::from("/project/lib/cjs/index.cjs");
2290 assert_eq!(
2291 try_source_fallback(&cjs_path, &path_to_id),
2292 Some(FileId(11)),
2293 "standalone cjs/ directory should fall back to src/"
2294 );
2295 }
2296
2297 #[test]
2298 fn test_try_pnpm_workspace_fallback_empty_after_pnpm() {
2299 let path_to_id: FxHashMap<&Path, FileId> = FxHashMap::default();
2300 let workspace_roots: FxHashMap<&str, &Path> = FxHashMap::default();
2301
2302 let pnpm_path = PathBuf::from("/project/node_modules/.pnpm/pkg@1.0.0/node_modules");
2303 assert_eq!(
2304 try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
2305 None,
2306 "path ending at node_modules with nothing after should return None"
2307 );
2308 }
2309
2310 #[test]
2311 fn test_try_pnpm_workspace_fallback_scoped_package_only_scope() {
2312 let path_to_id: FxHashMap<&Path, FileId> = FxHashMap::default();
2313 let workspace_roots: FxHashMap<&str, &Path> = FxHashMap::default();
2314
2315 let pnpm_path =
2316 PathBuf::from("/project/node_modules/.pnpm/@scope+pkg@1.0.0/node_modules/@scope");
2317 assert_eq!(
2318 try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
2319 None,
2320 "scoped package without full name and no matching workspace should return None"
2321 );
2322 }
2323
2324 #[test]
2325 fn test_try_pnpm_workspace_fallback_no_inner_node_modules() {
2326 let path_to_id: FxHashMap<&Path, FileId> = FxHashMap::default();
2327 let workspace_roots: FxHashMap<&str, &Path> = FxHashMap::default();
2328
2329 let pnpm_path = PathBuf::from("/project/node_modules/.pnpm/pkg@1.0.0/dist/index.js");
2330 assert_eq!(
2331 try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
2332 None,
2333 "path without inner node_modules after .pnpm should return None"
2334 );
2335 }
2336
2337 #[test]
2338 fn test_try_pnpm_workspace_fallback_package_without_relative_path() {
2339 let path_to_id: FxHashMap<&Path, FileId> = FxHashMap::default();
2340 let mut workspace_roots = FxHashMap::default();
2341 let ws_root = PathBuf::from("/project/packages/ui");
2342 workspace_roots.insert("@myorg/ui", ws_root.as_path());
2343
2344 let pnpm_path =
2345 PathBuf::from("/project/node_modules/.pnpm/@myorg+ui@1.0.0/node_modules/@myorg/ui");
2346 assert_eq!(
2347 try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
2348 None,
2349 "path ending at package name with no relative file should return None"
2350 );
2351 }
2352
2353 #[test]
2354 fn test_try_pnpm_workspace_fallback_nested_dist_esm() {
2355 let src_path = PathBuf::from("/project/packages/ui/src/Button.ts");
2356 let mut path_to_id = FxHashMap::default();
2357 path_to_id.insert(src_path.as_path(), FileId(10));
2358
2359 let mut workspace_roots = FxHashMap::default();
2360 let ws_root = PathBuf::from("/project/packages/ui");
2361 workspace_roots.insert("@myorg/ui", ws_root.as_path());
2362
2363 let pnpm_path = PathBuf::from(
2364 "/project/node_modules/.pnpm/@myorg+ui@1.0.0/node_modules/@myorg/ui/dist/esm/Button.mjs",
2365 );
2366 assert_eq!(
2367 try_pnpm_workspace_fallback(&pnpm_path, &path_to_id, &workspace_roots),
2368 Some(FileId(10)),
2369 "pnpm path with nested dist/esm should resolve through source fallback"
2370 );
2371 }
2372
2373 #[test]
2376 fn package_map_target_string_value_dot_key() {
2377 let map = serde_json::Value::String("./src/index.ts".to_string());
2380 let conds = conditions();
2381 let result = package_map_target(&map, ".", &conds);
2383 assert!(
2384 matches!(result, PackageMapTarget::Targets(_)),
2385 "string map with '.' key should return Targets"
2386 );
2387 }
2388
2389 #[test]
2390 fn package_map_target_string_value_non_dot_key_no_match() {
2391 let map = serde_json::Value::String("./src/index.ts".to_string());
2393 let conds = conditions();
2394 let result = package_map_target(&map, "./sub", &conds);
2395 assert!(
2396 matches!(result, PackageMapTarget::NoMatch),
2397 "string map with non-dot key should return NoMatch"
2398 );
2399 }
2400
2401 #[test]
2402 fn package_map_target_null_value_dot_key() {
2403 let map = serde_json::Value::Null;
2405 let conds = conditions();
2406 let result = package_map_target(&map, ".", &conds);
2407 assert!(
2408 matches!(result, PackageMapTarget::Blocked),
2409 "null map with '.' key should return Blocked"
2410 );
2411 }
2412
2413 #[test]
2414 fn package_map_target_bool_value_non_dot_key() {
2415 let map = serde_json::Value::Bool(true);
2417 let conds = conditions();
2418 let result = package_map_target(&map, "./sub", &conds);
2419 assert!(
2420 matches!(result, PackageMapTarget::NoMatch),
2421 "bool map with non-dot key should return NoMatch"
2422 );
2423 }
2424
2425 #[test]
2428 fn package_map_target_condition_only_object_dot_key() {
2429 let map = serde_json::json!({
2432 "import": "./src/index.mjs",
2433 "require": "./src/index.cjs"
2434 });
2435 let conds = conditions();
2436 let result = package_map_target(&map, ".", &conds);
2437 assert!(
2438 matches!(result, PackageMapTarget::Targets(_)),
2439 "condition-only object with '.' key should return Targets"
2440 );
2441 }
2442
2443 #[test]
2444 fn package_map_target_condition_only_object_non_dot_key() {
2445 let map = serde_json::json!({
2448 "import": "./src/index.mjs",
2449 "require": "./src/index.cjs"
2450 });
2451 let conds = conditions();
2452 let result = package_map_target(&map, "./nonexistent", &conds);
2453 assert!(
2454 matches!(result, PackageMapTarget::NoMatch),
2455 "condition-only object with non-dot key should return NoMatch"
2456 );
2457 }
2458
2459 #[test]
2462 fn resolve_package_map_value_unmatched_conditions_returns_none() {
2463 let value = serde_json::json!({ "browser": "./src/browser.js" });
2465 let conds = conditions(); assert_eq!(
2467 resolve_package_map_value(&value, &conds, None),
2468 None,
2469 "unmatched condition should return None"
2470 );
2471 }
2472
2473 #[test]
2474 fn resolve_package_map_value_bool_returns_none() {
2475 let value = serde_json::Value::Bool(false);
2476 let conds = conditions();
2477 assert_eq!(
2478 resolve_package_map_value(&value, &conds, None),
2479 None,
2480 "bool value should return None"
2481 );
2482 }
2483
2484 #[test]
2485 fn resolve_package_map_value_number_returns_none() {
2486 let value = serde_json::Value::Number(42.into());
2487 let conds = conditions();
2488 assert_eq!(
2489 resolve_package_map_value(&value, &conds, None),
2490 None,
2491 "number value should return None"
2492 );
2493 }
2494
2495 #[test]
2496 fn resolve_package_map_value_null_returns_none() {
2497 let value = serde_json::Value::Null;
2498 let conds = conditions();
2499 assert_eq!(
2500 resolve_package_map_value(&value, &conds, None),
2501 None,
2502 "null value should return None"
2503 );
2504 }
2505
2506 #[test]
2507 fn resolve_package_map_value_array_all_null_returns_none() {
2508 let value = serde_json::json!([null, false, 42]);
2510 let conds = conditions();
2511 assert_eq!(
2512 resolve_package_map_value(&value, &conds, None),
2513 None,
2514 "array of unresolvable values should return None"
2515 );
2516 }
2517
2518 #[test]
2519 fn resolve_package_map_value_array_with_valid_entry() {
2520 let value = serde_json::json!([null, "./src/index.ts"]);
2522 let conds = conditions();
2523 let result = resolve_package_map_value(&value, &conds, None);
2524 assert_eq!(
2525 result,
2526 Some(vec!["./src/index.ts".to_string()]),
2527 "array with a valid string entry should return that entry"
2528 );
2529 }
2530
2531 #[test]
2534 fn package_map_pattern_capture_no_star_returns_none() {
2535 assert_eq!(
2537 package_map_pattern_capture("./exact", "./exact"),
2538 None,
2539 "pattern with no star should return None"
2540 );
2541 }
2542
2543 #[test]
2544 fn package_map_pattern_capture_two_stars_returns_none() {
2545 assert_eq!(
2547 package_map_pattern_capture("./*/*.js", "./foo/bar.js"),
2548 None,
2549 "pattern with two stars should return None"
2550 );
2551 }
2552
2553 #[test]
2554 fn package_map_pattern_capture_single_star_captures() {
2555 assert_eq!(
2557 package_map_pattern_capture("./dist/*/index.js", "./dist/utils/index.js"),
2558 Some("utils".to_string()),
2559 );
2560 }
2561
2562 #[test]
2563 fn package_map_pattern_capture_no_prefix_match_returns_none() {
2564 assert_eq!(
2566 package_map_pattern_capture("./lib/*.js", "./src/foo.js"),
2567 None,
2568 );
2569 }
2570
2571 #[test]
2574 fn resolve_package_map_target_no_dot_slash_prefix_returns_none() {
2575 let root = PathBuf::from("/project/packages/ui");
2577 let pj = fallow_config::PackageJson::default();
2578 with_package_map_ctx(root, Some("@myorg/ui"), pj, &[], |ctx, manifest, _root| {
2579 let result = resolve_package_map_target(ctx, manifest, "src/index.ts", None);
2580 assert_eq!(result, None, "target without './' should return None");
2581 });
2582 }
2583
2584 #[test]
2585 fn resolve_package_map_target_parent_dir_returns_none() {
2586 let root = PathBuf::from("/project/packages/ui");
2588 let pj = fallow_config::PackageJson::default();
2589 with_package_map_ctx(root, Some("@myorg/ui"), pj, &[], |ctx, manifest, _root| {
2590 let result = resolve_package_map_target(ctx, manifest, "./../outside/file.ts", None);
2591 assert_eq!(result, None, "parent-dir target should return None");
2592 });
2593 }
2594
2595 #[test]
2596 fn resolve_package_map_target_absolute_path_returns_none() {
2597 let root = PathBuf::from("/project/packages/ui");
2599 let pj = fallow_config::PackageJson::default();
2600 with_package_map_ctx(root, Some("@myorg/ui"), pj, &[], |ctx, manifest, _root| {
2601 let result = resolve_package_map_target(ctx, manifest, ".//abs/path.ts", None);
2604 assert_eq!(result, None, "absolute target should return None");
2605 });
2606 }
2607
2608 #[test]
2609 fn resolve_package_map_target_valid_target_hits_raw_path_map() {
2610 let root = PathBuf::from("/project/packages/ui");
2612 let src = root.join("src/index.ts");
2613 let pj = fallow_config::PackageJson::default();
2614 with_package_map_ctx(
2615 root,
2616 Some("@myorg/ui"),
2617 pj,
2618 &[(src, FileId(5))],
2619 |ctx, manifest, _root| {
2620 let result = resolve_package_map_target(ctx, manifest, "./src/index.ts", None);
2621 assert_eq!(
2622 result,
2623 Some(FileId(5)),
2624 "valid target in raw_path_to_id should resolve"
2625 );
2626 },
2627 );
2628 }
2629
2630 #[test]
2633 fn package_import_source_subpath_strips_hash_and_package_name() {
2634 let manifest = PackageManifestInfo {
2635 root: PathBuf::from("/project"),
2636 canonical_root: PathBuf::from("/project"),
2637 name: Some("my-pkg".to_string()),
2638 package_json: fallow_config::PackageJson::default(),
2639 deno_import_map: Vec::new(),
2640 };
2641 let result = package_import_source_subpath(&manifest, "#my-pkg/utils");
2642 assert_eq!(
2643 result,
2644 Some(PathBuf::from("utils")),
2645 "should strip '#', package name, and '/' separator"
2646 );
2647 }
2648
2649 #[test]
2650 fn package_import_source_subpath_no_package_name_match_keeps_full_subpath() {
2651 let manifest = PackageManifestInfo {
2654 root: PathBuf::from("/project"),
2655 canonical_root: PathBuf::from("/project"),
2656 name: Some("my-pkg".to_string()),
2657 package_json: fallow_config::PackageJson::default(),
2658 deno_import_map: Vec::new(),
2659 };
2660 let result = package_import_source_subpath(&manifest, "#utils");
2661 assert_eq!(
2662 result,
2663 Some(PathBuf::from("utils")),
2664 "without package-name prefix the full subpath should be kept"
2665 );
2666 }
2667
2668 #[test]
2669 fn package_import_source_subpath_empty_hash_returns_none() {
2670 let manifest = PackageManifestInfo {
2673 root: PathBuf::from("/project"),
2674 canonical_root: PathBuf::from("/project"),
2675 name: Some("my-pkg".to_string()),
2676 package_json: fallow_config::PackageJson::default(),
2677 deno_import_map: Vec::new(),
2678 };
2679 let result = package_import_source_subpath(&manifest, "#");
2681 assert_eq!(
2682 result, None,
2683 "specifier '#' with empty body should return None"
2684 );
2685 }
2686
2687 #[test]
2688 fn package_import_source_subpath_no_hash_returns_none() {
2689 let manifest = PackageManifestInfo {
2691 root: PathBuf::from("/project"),
2692 canonical_root: PathBuf::from("/project"),
2693 name: Some("my-pkg".to_string()),
2694 package_json: fallow_config::PackageJson::default(),
2695 deno_import_map: Vec::new(),
2696 };
2697 let result = package_import_source_subpath(&manifest, "no-hash");
2698 assert_eq!(result, None, "specifier without '#' should return None");
2699 }
2700
2701 #[test]
2702 fn package_import_source_subpath_no_manifest_name() {
2703 let manifest = PackageManifestInfo {
2705 root: PathBuf::from("/project"),
2706 canonical_root: PathBuf::from("/project"),
2707 name: None,
2708 package_json: fallow_config::PackageJson::default(),
2709 deno_import_map: Vec::new(),
2710 };
2711 let result = package_import_source_subpath(&manifest, "#internal/helper");
2712 assert_eq!(
2713 result,
2714 Some(PathBuf::from("internal/helper")),
2715 "manifest without name should return the full stripped specifier"
2716 );
2717 }
2718
2719 #[test]
2722 fn nearest_package_manifest_returns_deepest_match() {
2723 let root1 = PathBuf::from("/project");
2724 let root2 = PathBuf::from("/project/packages/ui");
2725 let m1 = PackageManifestInfo {
2726 root: root1.clone(),
2727 canonical_root: root1,
2728 name: Some("root".to_string()),
2729 package_json: fallow_config::PackageJson::default(),
2730 deno_import_map: Vec::new(),
2731 };
2732 let m2 = PackageManifestInfo {
2733 root: root2.clone(),
2734 canonical_root: root2,
2735 name: Some("@myorg/ui".to_string()),
2736 package_json: fallow_config::PackageJson::default(),
2737 deno_import_map: Vec::new(),
2738 };
2739 let manifests = [m1, m2];
2740 let from_file = Path::new("/project/packages/ui/src/index.ts");
2741 let result = nearest_package_manifest(&manifests, from_file);
2742 assert_eq!(
2743 result.and_then(|m| m.name.as_deref()),
2744 Some("@myorg/ui"),
2745 "should pick the deepest (longest path) manifest that contains the file"
2746 );
2747 }
2748
2749 #[test]
2750 fn nearest_package_manifest_no_match_returns_none() {
2751 let root = PathBuf::from("/project/packages/ui");
2752 let m = PackageManifestInfo {
2753 root: root.clone(),
2754 canonical_root: root,
2755 name: Some("@myorg/ui".to_string()),
2756 package_json: fallow_config::PackageJson::default(),
2757 deno_import_map: Vec::new(),
2758 };
2759 let manifests = [m];
2760 let from_file = Path::new("/other/project/src/index.ts");
2762 let result = nearest_package_manifest(&manifests, from_file);
2763 assert!(
2764 result.is_none(),
2765 "file outside all manifest roots should return None"
2766 );
2767 }
2768
2769 #[test]
2772 fn lookup_internal_file_id_uses_path_to_id_when_raw_misses() {
2773 let target = PathBuf::from("/project/src/index.ts");
2776 let mut path_to_id: FxHashMap<&Path, FileId> = FxHashMap::default();
2777 path_to_id.insert(target.as_path(), FileId(99));
2778 let raw_path_to_id: FxHashMap<&Path, FileId> = FxHashMap::default();
2779 let workspace_roots: FxHashMap<&str, &Path> = FxHashMap::default();
2780 let condition_names = conditions();
2781 let resolver = oxc_resolver::Resolver::new(oxc_resolver::ResolveOptions::default());
2782 let tsconfig_warned = std::sync::Mutex::new(FxHashSet::default());
2783 let tsconfig_cache = TsconfigCache::default();
2784 let canonicalize_cache = CanonicalizeCache::default();
2785 let root = PathBuf::from("/project");
2786 let ctx = ResolveContext {
2787 resolver: &resolver,
2788 style_resolver: &resolver,
2789 extensions: &[],
2790 path_to_id: &path_to_id,
2791 raw_path_to_id: &raw_path_to_id,
2792 workspace_roots: &workspace_roots,
2793 package_manifests: &[],
2794 has_deno_import_maps: false,
2795 condition_names: &condition_names,
2796 path_aliases: &[],
2797 scss_include_paths: &[],
2798 static_dir_mappings: &[],
2799 framework_static_dir_mappings: &[],
2800 root: &root,
2801 canonical_fallback: None,
2802 tsconfig_warned: &tsconfig_warned,
2803 tsconfig_cache: &tsconfig_cache,
2804 canonicalize_cache: &canonicalize_cache,
2805 };
2806 let result = lookup_internal_file_id(&ctx, &target);
2807 assert_eq!(
2808 result,
2809 Some(FileId(99)),
2810 "should fall back from raw_path_to_id to path_to_id"
2811 );
2812 }
2813
2814 #[test]
2817 fn try_scss_partial_fallback_rejects_colon_specifier() {
2818 let root = PathBuf::from("/project");
2821 let pj = fallow_config::PackageJson::default();
2822 with_package_map_ctx(root.clone(), None, pj, &[], |ctx, _manifest, _r| {
2823 let from_file = root.join("src/main.scss");
2824 let result = try_scss_partial_fallback(ctx, &from_file, "sass:math");
2825 assert!(
2826 result.is_none(),
2827 "specifier with ':' should short-circuit to None"
2828 );
2829 });
2830 }
2831
2832 #[test]
2833 fn try_scss_partial_fallback_rejects_already_partial_filename() {
2834 let root = PathBuf::from("/project");
2837 let pj = fallow_config::PackageJson::default();
2838 with_package_map_ctx(root.clone(), None, pj, &[], |ctx, _manifest, _r| {
2839 let from_file = root.join("src/main.scss");
2840 let result = try_scss_partial_fallback(ctx, &from_file, "./_variables");
2841 assert!(
2842 result.is_none(),
2843 "already-partial filename should short-circuit to None"
2844 );
2845 });
2846 }
2847
2848 #[test]
2851 fn try_workspace_package_fallback_rejects_relative_specifier() {
2852 let root = PathBuf::from("/project");
2855 let pj = fallow_config::PackageJson::default();
2856 with_package_map_ctx(root, None, pj, &[], |ctx, _manifest, _r| {
2857 let result = try_workspace_package_fallback(ctx, "./local/module");
2858 assert!(
2859 result.is_none(),
2860 "relative specifier should return None from workspace fallback"
2861 );
2862 });
2863 }
2864
2865 #[test]
2866 fn try_workspace_package_fallback_rejects_absolute_path() {
2867 let root = PathBuf::from("/project");
2869 let pj = fallow_config::PackageJson::default();
2870 with_package_map_ctx(root, None, pj, &[], |ctx, _manifest, _r| {
2871 let result = try_workspace_package_fallback(ctx, "/absolute/path");
2872 assert!(
2873 result.is_none(),
2874 "absolute path should return None from workspace fallback"
2875 );
2876 });
2877 }
2878}