1use std::path::{Component, Path, PathBuf};
7
8use super::fallbacks::{is_declaration_file_name, output_source_stem};
9use super::types::{MISSING_ONLY_OUTPUT_DIRS, OUTPUT_DIRS};
10
11pub fn output_entry_to_source_path(
31 base: &Path,
32 entry: &str,
33 source_extensions: &[&str],
34 is_discovered: impl Fn(&Path) -> bool,
35) -> Option<PathBuf> {
36 output_dir_to_source_path(base, entry, OUTPUT_DIRS, source_extensions).or_else(|| {
37 let resolved = base.join(entry);
38 if is_bare_missing_only_dir(&resolved) {
39 return None;
40 }
41 let source =
42 output_dir_to_source_path(base, entry, MISSING_ONLY_OUTPUT_DIRS, source_extensions)?;
43 let target_present = probed_entry_target(&resolved, source_extensions)
44 .is_some_and(|target| is_discovered(&target));
45 (!target_present).then_some(source)
46 })
47}
48
49pub fn directory_index_entry(resolved: &Path, source_extensions: &[&str]) -> Option<PathBuf> {
51 source_extensions
52 .iter()
53 .map(|ext| resolved.join(format!("index.{ext}")))
54 .find(|candidate| candidate.is_file())
55}
56
57fn output_dir_to_source_path(
59 base: &Path,
60 entry: &str,
61 dirs: &[&str],
62 source_extensions: &[&str],
63) -> Option<PathBuf> {
64 let components: Vec<_> = Path::new(entry).components().collect();
65
66 let output_pos = components.iter().rposition(|component| {
67 if let Component::Normal(name) = component
68 && let Some(name) = name.to_str()
69 {
70 return dirs.contains(&name);
71 }
72 false
73 })?;
74
75 let prefix: PathBuf = components[..output_pos]
76 .iter()
77 .filter(|component| !matches!(component, Component::CurDir))
78 .collect();
79 let suffix: PathBuf = components[output_pos + 1..].iter().collect();
80 let file_name = suffix.file_name()?.to_str()?;
81 let stem = output_source_stem(file_name);
82 let source_dir = match suffix.parent() {
83 Some(parent) => base.join(&prefix).join("src").join(parent),
84 None => base.join(&prefix).join("src"),
85 };
86
87 let same_stem = source_extensions
88 .iter()
89 .map(|ext| source_dir.join(format!("{stem}.{ext}")));
90 let declaration = is_declaration_file_name(file_name).then(|| source_dir.join(file_name));
92 let directory_index = source_extensions
95 .iter()
96 .map(|ext| source_dir.join(stem).join(format!("index.{ext}")));
97
98 same_stem
99 .chain(declaration)
100 .chain(directory_index)
101 .find(|candidate| candidate.is_file())
102}
103
104fn is_bare_missing_only_dir(path: &Path) -> bool {
107 path.file_name()
108 .and_then(|name| name.to_str())
109 .is_some_and(|name| MISSING_ONLY_OUTPUT_DIRS.contains(&name))
110}
111
112fn probed_entry_target(resolved: &Path, source_extensions: &[&str]) -> Option<PathBuf> {
115 if resolved.is_file() {
116 return Some(resolved.to_path_buf());
117 }
118 source_extensions
119 .iter()
120 .map(|ext| resolved.with_extension(ext))
121 .find(|candidate| candidate.is_file())
122 .or_else(|| directory_index_entry(resolved, source_extensions))
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 const EXTS: &[&str] = &["ts", "tsx", "js"];
130
131 fn write(path: &Path) {
132 std::fs::create_dir_all(path.parent().expect("parent directory")).expect("directory");
133 std::fs::write(path, "export {};\n").expect("file");
134 }
135
136 #[cfg_attr(miri, ignore)]
137 #[test]
138 fn missing_lib_entry_maps_to_source() {
139 let dir = tempfile::tempdir().expect("temporary directory");
140 write(&dir.path().join("src/index.ts"));
141
142 assert_eq!(
143 output_entry_to_source_path(dir.path(), "./lib/index.mjs", EXTS, |_| true),
144 Some(dir.path().join("src/index.ts"))
145 );
146 }
147
148 #[cfg_attr(miri, ignore)]
149 #[test]
150 fn discovered_lib_entry_stays_the_entry() {
151 let dir = tempfile::tempdir().expect("temporary directory");
152 write(&dir.path().join("src/index.ts"));
153 write(&dir.path().join("lib/index.js"));
154
155 assert_eq!(
156 output_entry_to_source_path(dir.path(), "./lib/index.js", EXTS, |_| true),
157 None
158 );
159 assert_eq!(
160 output_entry_to_source_path(dir.path(), "./lib/index", EXTS, |_| true),
161 None
162 );
163 }
164
165 #[cfg_attr(miri, ignore)]
166 #[test]
167 fn undiscovered_lib_entry_on_disk_maps_to_source() {
168 let dir = tempfile::tempdir().expect("temporary directory");
169 write(&dir.path().join("src/index.ts"));
170 write(&dir.path().join("lib/index.js"));
171
172 assert_eq!(
173 output_entry_to_source_path(dir.path(), "./lib/index.js", EXTS, |_| false),
174 Some(dir.path().join("src/index.ts"))
175 );
176 }
177
178 #[cfg_attr(miri, ignore)]
179 #[test]
180 fn discovered_check_runs_only_for_lib_entry_with_source() {
181 let dir = tempfile::tempdir().expect("temporary directory");
182 write(&dir.path().join("index.js"));
183 write(&dir.path().join("lib/other.js"));
184 write(&dir.path().join("lib/index.js"));
185 write(&dir.path().join("src/index.ts"));
186 let calls = std::cell::Cell::new(0);
187 let count = |_: &Path| {
188 calls.set(calls.get() + 1);
189 true
190 };
191
192 assert_eq!(
193 output_entry_to_source_path(dir.path(), "./index.js", EXTS, count),
194 None
195 );
196 assert_eq!(
197 output_entry_to_source_path(dir.path(), "./lib/other.js", EXTS, count),
198 None
199 );
200 assert_eq!(calls.get(), 0, "no lib/ entry with a source file, no check");
201
202 assert_eq!(
203 output_entry_to_source_path(dir.path(), "./lib/index.js", EXTS, count),
204 None
205 );
206 assert_eq!(calls.get(), 1);
207 }
208
209 #[cfg_attr(miri, ignore)]
210 #[test]
211 fn bare_lib_entry_does_not_map() {
212 let dir = tempfile::tempdir().expect("temporary directory");
213 write(&dir.path().join("src/index.ts"));
214
215 assert_eq!(
216 output_entry_to_source_path(dir.path(), "./lib", EXTS, |_| false),
217 None
218 );
219 }
220
221 #[cfg_attr(miri, ignore)]
224 #[test]
225 fn output_file_maps_to_the_source_directory_index() {
226 let dir = tempfile::tempdir().expect("temporary directory");
227 write(&dir.path().join("src/core/index.ts"));
228
229 for entry in ["./dist/core.mjs", "./dist/core.cjs", "dist/core.js"] {
230 assert_eq!(
231 output_entry_to_source_path(dir.path(), entry, EXTS, |_| true),
232 Some(dir.path().join("src/core/index.ts")),
233 "{entry}"
234 );
235 }
236 }
237
238 #[cfg_attr(miri, ignore)]
239 #[test]
240 fn same_stem_source_file_wins_over_the_directory_index() {
241 let dir = tempfile::tempdir().expect("temporary directory");
242 write(&dir.path().join("src/core.ts"));
243 write(&dir.path().join("src/core/index.ts"));
244
245 assert_eq!(
246 output_entry_to_source_path(dir.path(), "./dist/core.mjs", EXTS, |_| true),
247 Some(dir.path().join("src/core.ts"))
248 );
249 }
250
251 #[cfg_attr(miri, ignore)]
253 #[test]
254 fn declaration_output_maps_to_the_source_without_the_declaration_suffix() {
255 let dir = tempfile::tempdir().expect("temporary directory");
256 write(&dir.path().join("src/core/index.ts"));
257 write(&dir.path().join("src/feature.port.ts"));
258
259 for (entry, expected) in [
260 ("./dist/core/index.d.ts", "src/core/index.ts"),
261 ("./dist/core/index.d.mts", "src/core/index.ts"),
262 ("./dist/core/index.d.cts", "src/core/index.ts"),
263 ("./dist/feature.port.d.ts", "src/feature.port.ts"),
264 ] {
265 assert_eq!(
266 output_entry_to_source_path(dir.path(), entry, EXTS, |_| true),
267 Some(dir.path().join(expected)),
268 "{entry}"
269 );
270 }
271 }
272
273 #[cfg_attr(miri, ignore)]
276 #[test]
277 fn declaration_output_keeps_a_hand_written_source_declaration() {
278 let dir = tempfile::tempdir().expect("temporary directory");
279 write(&dir.path().join("src/types.d.ts"));
280
281 assert_eq!(
282 output_entry_to_source_path(dir.path(), "./dist/types.d.ts", EXTS, |_| true),
283 Some(dir.path().join("src/types.d.ts"))
284 );
285 }
286
287 #[cfg_attr(miri, ignore)]
288 #[test]
289 fn output_entry_without_a_source_does_not_map() {
290 let dir = tempfile::tempdir().expect("temporary directory");
291 write(&dir.path().join("src/other/index.ts"));
292
293 for entry in ["./dist/core.mjs", "./dist/core/index.d.ts"] {
294 assert_eq!(
295 output_entry_to_source_path(dir.path(), entry, EXTS, |_| true),
296 None,
297 "{entry}"
298 );
299 }
300 }
301
302 #[cfg_attr(miri, ignore)]
303 #[test]
304 fn output_dirs_map_without_the_discovered_check() {
305 let dir = tempfile::tempdir().expect("temporary directory");
306 write(&dir.path().join("src/utils.ts"));
307 write(&dir.path().join("dist/utils.js"));
308
309 assert_eq!(
310 output_entry_to_source_path(dir.path(), "./dist/utils.js", EXTS, |_| true),
311 Some(dir.path().join("src/utils.ts"))
312 );
313 }
314}