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