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