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