use std::path::{Component, Path, PathBuf};
use super::types::{MISSING_ONLY_OUTPUT_DIRS, OUTPUT_DIRS};
pub fn output_entry_to_source_path(
base: &Path,
entry: &str,
source_extensions: &[&str],
is_discovered: impl Fn(&Path) -> bool,
) -> Option<PathBuf> {
output_dir_to_source_path(base, entry, OUTPUT_DIRS, source_extensions).or_else(|| {
let resolved = base.join(entry);
if is_bare_missing_only_dir(&resolved) {
return None;
}
let source =
output_dir_to_source_path(base, entry, MISSING_ONLY_OUTPUT_DIRS, source_extensions)?;
let target_present = probed_entry_target(&resolved, source_extensions)
.is_some_and(|target| is_discovered(&target));
(!target_present).then_some(source)
})
}
pub fn directory_index_entry(resolved: &Path, source_extensions: &[&str]) -> Option<PathBuf> {
source_extensions
.iter()
.map(|ext| resolved.join(format!("index.{ext}")))
.find(|candidate| candidate.is_file())
}
fn output_dir_to_source_path(
base: &Path,
entry: &str,
dirs: &[&str],
source_extensions: &[&str],
) -> Option<PathBuf> {
let components: Vec<_> = Path::new(entry).components().collect();
let output_pos = components.iter().rposition(|component| {
if let Component::Normal(name) = component
&& let Some(name) = name.to_str()
{
return dirs.contains(&name);
}
false
})?;
let prefix: PathBuf = components[..output_pos]
.iter()
.filter(|component| !matches!(component, Component::CurDir))
.collect();
let suffix: PathBuf = components[output_pos + 1..].iter().collect();
source_extensions
.iter()
.map(|ext| {
base.join(&prefix)
.join("src")
.join(suffix.with_extension(ext))
})
.find(|candidate| candidate.exists())
}
fn is_bare_missing_only_dir(path: &Path) -> bool {
path.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| MISSING_ONLY_OUTPUT_DIRS.contains(&name))
}
fn probed_entry_target(resolved: &Path, source_extensions: &[&str]) -> Option<PathBuf> {
if resolved.is_file() {
return Some(resolved.to_path_buf());
}
source_extensions
.iter()
.map(|ext| resolved.with_extension(ext))
.find(|candidate| candidate.is_file())
.or_else(|| directory_index_entry(resolved, source_extensions))
}
#[cfg(test)]
mod tests {
use super::*;
const EXTS: &[&str] = &["ts", "tsx", "js"];
fn write(path: &Path) {
std::fs::create_dir_all(path.parent().expect("parent directory")).expect("directory");
std::fs::write(path, "export {};\n").expect("file");
}
#[cfg_attr(miri, ignore)]
#[test]
fn missing_lib_entry_maps_to_source() {
let dir = tempfile::tempdir().expect("temporary directory");
write(&dir.path().join("src/index.ts"));
assert_eq!(
output_entry_to_source_path(dir.path(), "./lib/index.mjs", EXTS, |_| true),
Some(dir.path().join("src/index.ts"))
);
}
#[cfg_attr(miri, ignore)]
#[test]
fn discovered_lib_entry_stays_the_entry() {
let dir = tempfile::tempdir().expect("temporary directory");
write(&dir.path().join("src/index.ts"));
write(&dir.path().join("lib/index.js"));
assert_eq!(
output_entry_to_source_path(dir.path(), "./lib/index.js", EXTS, |_| true),
None
);
assert_eq!(
output_entry_to_source_path(dir.path(), "./lib/index", EXTS, |_| true),
None
);
}
#[cfg_attr(miri, ignore)]
#[test]
fn undiscovered_lib_entry_on_disk_maps_to_source() {
let dir = tempfile::tempdir().expect("temporary directory");
write(&dir.path().join("src/index.ts"));
write(&dir.path().join("lib/index.js"));
assert_eq!(
output_entry_to_source_path(dir.path(), "./lib/index.js", EXTS, |_| false),
Some(dir.path().join("src/index.ts"))
);
}
#[cfg_attr(miri, ignore)]
#[test]
fn discovered_check_runs_only_for_lib_entry_with_source() {
let dir = tempfile::tempdir().expect("temporary directory");
write(&dir.path().join("index.js"));
write(&dir.path().join("lib/other.js"));
write(&dir.path().join("lib/index.js"));
write(&dir.path().join("src/index.ts"));
let calls = std::cell::Cell::new(0);
let count = |_: &Path| {
calls.set(calls.get() + 1);
true
};
assert_eq!(
output_entry_to_source_path(dir.path(), "./index.js", EXTS, count),
None
);
assert_eq!(
output_entry_to_source_path(dir.path(), "./lib/other.js", EXTS, count),
None
);
assert_eq!(calls.get(), 0, "no lib/ entry with a source file, no check");
assert_eq!(
output_entry_to_source_path(dir.path(), "./lib/index.js", EXTS, count),
None
);
assert_eq!(calls.get(), 1);
}
#[cfg_attr(miri, ignore)]
#[test]
fn bare_lib_entry_does_not_map() {
let dir = tempfile::tempdir().expect("temporary directory");
write(&dir.path().join("src/index.ts"));
assert_eq!(
output_entry_to_source_path(dir.path(), "./lib", EXTS, |_| false),
None
);
}
#[cfg_attr(miri, ignore)]
#[test]
fn output_dirs_map_without_the_discovered_check() {
let dir = tempfile::tempdir().expect("temporary directory");
write(&dir.path().join("src/utils.ts"));
write(&dir.path().join("dist/utils.js"));
assert_eq!(
output_entry_to_source_path(dir.path(), "./dist/utils.js", EXTS, |_| true),
Some(dir.path().join("src/utils.ts"))
);
}
}