fallow_graph/resolve/
output_entry.rs1use std::path::{Component, Path, PathBuf};
7
8use super::types::{MISSING_ONLY_OUTPUT_DIRS, OUTPUT_DIRS};
9
10pub fn output_entry_to_source_path(
28 base: &Path,
29 entry: &str,
30 source_extensions: &[&str],
31 is_discovered: impl Fn(&Path) -> bool,
32) -> Option<PathBuf> {
33 output_dir_to_source_path(base, entry, OUTPUT_DIRS, source_extensions).or_else(|| {
34 let resolved = base.join(entry);
35 if is_bare_missing_only_dir(&resolved) {
36 return None;
37 }
38 let source =
39 output_dir_to_source_path(base, entry, MISSING_ONLY_OUTPUT_DIRS, source_extensions)?;
40 let target_present = probed_entry_target(&resolved, source_extensions)
41 .is_some_and(|target| is_discovered(&target));
42 (!target_present).then_some(source)
43 })
44}
45
46pub fn directory_index_entry(resolved: &Path, source_extensions: &[&str]) -> Option<PathBuf> {
48 source_extensions
49 .iter()
50 .map(|ext| resolved.join(format!("index.{ext}")))
51 .find(|candidate| candidate.is_file())
52}
53
54fn output_dir_to_source_path(
56 base: &Path,
57 entry: &str,
58 dirs: &[&str],
59 source_extensions: &[&str],
60) -> Option<PathBuf> {
61 let components: Vec<_> = Path::new(entry).components().collect();
62
63 let output_pos = components.iter().rposition(|component| {
64 if let Component::Normal(name) = component
65 && let Some(name) = name.to_str()
66 {
67 return dirs.contains(&name);
68 }
69 false
70 })?;
71
72 let prefix: PathBuf = components[..output_pos]
73 .iter()
74 .filter(|component| !matches!(component, Component::CurDir))
75 .collect();
76 let suffix: PathBuf = components[output_pos + 1..].iter().collect();
77
78 source_extensions
79 .iter()
80 .map(|ext| {
81 base.join(&prefix)
82 .join("src")
83 .join(suffix.with_extension(ext))
84 })
85 .find(|candidate| candidate.exists())
86}
87
88fn is_bare_missing_only_dir(path: &Path) -> bool {
91 path.file_name()
92 .and_then(|name| name.to_str())
93 .is_some_and(|name| MISSING_ONLY_OUTPUT_DIRS.contains(&name))
94}
95
96fn probed_entry_target(resolved: &Path, source_extensions: &[&str]) -> Option<PathBuf> {
99 if resolved.is_file() {
100 return Some(resolved.to_path_buf());
101 }
102 source_extensions
103 .iter()
104 .map(|ext| resolved.with_extension(ext))
105 .find(|candidate| candidate.is_file())
106 .or_else(|| directory_index_entry(resolved, source_extensions))
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112
113 const EXTS: &[&str] = &["ts", "tsx", "js"];
114
115 fn write(path: &Path) {
116 std::fs::create_dir_all(path.parent().expect("parent directory")).expect("directory");
117 std::fs::write(path, "export {};\n").expect("file");
118 }
119
120 #[cfg_attr(miri, ignore)]
121 #[test]
122 fn missing_lib_entry_maps_to_source() {
123 let dir = tempfile::tempdir().expect("temporary directory");
124 write(&dir.path().join("src/index.ts"));
125
126 assert_eq!(
127 output_entry_to_source_path(dir.path(), "./lib/index.mjs", EXTS, |_| true),
128 Some(dir.path().join("src/index.ts"))
129 );
130 }
131
132 #[cfg_attr(miri, ignore)]
133 #[test]
134 fn discovered_lib_entry_stays_the_entry() {
135 let dir = tempfile::tempdir().expect("temporary directory");
136 write(&dir.path().join("src/index.ts"));
137 write(&dir.path().join("lib/index.js"));
138
139 assert_eq!(
140 output_entry_to_source_path(dir.path(), "./lib/index.js", EXTS, |_| true),
141 None
142 );
143 assert_eq!(
144 output_entry_to_source_path(dir.path(), "./lib/index", EXTS, |_| true),
145 None
146 );
147 }
148
149 #[cfg_attr(miri, ignore)]
150 #[test]
151 fn undiscovered_lib_entry_on_disk_maps_to_source() {
152 let dir = tempfile::tempdir().expect("temporary directory");
153 write(&dir.path().join("src/index.ts"));
154 write(&dir.path().join("lib/index.js"));
155
156 assert_eq!(
157 output_entry_to_source_path(dir.path(), "./lib/index.js", EXTS, |_| false),
158 Some(dir.path().join("src/index.ts"))
159 );
160 }
161
162 #[cfg_attr(miri, ignore)]
163 #[test]
164 fn discovered_check_runs_only_for_lib_entry_with_source() {
165 let dir = tempfile::tempdir().expect("temporary directory");
166 write(&dir.path().join("index.js"));
167 write(&dir.path().join("lib/other.js"));
168 write(&dir.path().join("lib/index.js"));
169 write(&dir.path().join("src/index.ts"));
170 let calls = std::cell::Cell::new(0);
171 let count = |_: &Path| {
172 calls.set(calls.get() + 1);
173 true
174 };
175
176 assert_eq!(
177 output_entry_to_source_path(dir.path(), "./index.js", EXTS, count),
178 None
179 );
180 assert_eq!(
181 output_entry_to_source_path(dir.path(), "./lib/other.js", EXTS, count),
182 None
183 );
184 assert_eq!(calls.get(), 0, "no lib/ entry with a source file, no check");
185
186 assert_eq!(
187 output_entry_to_source_path(dir.path(), "./lib/index.js", EXTS, count),
188 None
189 );
190 assert_eq!(calls.get(), 1);
191 }
192
193 #[cfg_attr(miri, ignore)]
194 #[test]
195 fn bare_lib_entry_does_not_map() {
196 let dir = tempfile::tempdir().expect("temporary directory");
197 write(&dir.path().join("src/index.ts"));
198
199 assert_eq!(
200 output_entry_to_source_path(dir.path(), "./lib", EXTS, |_| false),
201 None
202 );
203 }
204
205 #[cfg_attr(miri, ignore)]
206 #[test]
207 fn output_dirs_map_without_the_discovered_check() {
208 let dir = tempfile::tempdir().expect("temporary directory");
209 write(&dir.path().join("src/utils.ts"));
210 write(&dir.path().join("dist/utils.js"));
211
212 assert_eq!(
213 output_entry_to_source_path(dir.path(), "./dist/utils.js", EXTS, |_| true),
214 Some(dir.path().join("src/utils.ts"))
215 );
216 }
217}