use std::path::{Component, Path, PathBuf};
use super::fallbacks::{is_declaration_file_name, output_source_stem};
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();
let file_name = suffix.file_name()?.to_str()?;
let stem = output_source_stem(file_name);
let source_dir = match suffix.parent() {
Some(parent) => base.join(&prefix).join("src").join(parent),
None => base.join(&prefix).join("src"),
};
let same_stem = source_extensions
.iter()
.map(|ext| source_dir.join(format!("{stem}.{ext}")));
let declaration = is_declaration_file_name(file_name).then(|| source_dir.join(file_name));
let directory_index = source_extensions
.iter()
.map(|ext| source_dir.join(stem).join(format!("index.{ext}")));
same_stem
.chain(declaration)
.chain(directory_index)
.find(|candidate| candidate.is_file())
}
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_file_maps_to_the_source_directory_index() {
let dir = tempfile::tempdir().expect("temporary directory");
write(&dir.path().join("src/core/index.ts"));
for entry in ["./dist/core.mjs", "./dist/core.cjs", "dist/core.js"] {
assert_eq!(
output_entry_to_source_path(dir.path(), entry, EXTS, |_| true),
Some(dir.path().join("src/core/index.ts")),
"{entry}"
);
}
}
#[cfg_attr(miri, ignore)]
#[test]
fn same_stem_source_file_wins_over_the_directory_index() {
let dir = tempfile::tempdir().expect("temporary directory");
write(&dir.path().join("src/core.ts"));
write(&dir.path().join("src/core/index.ts"));
assert_eq!(
output_entry_to_source_path(dir.path(), "./dist/core.mjs", EXTS, |_| true),
Some(dir.path().join("src/core.ts"))
);
}
#[cfg_attr(miri, ignore)]
#[test]
fn declaration_output_maps_to_the_source_without_the_declaration_suffix() {
let dir = tempfile::tempdir().expect("temporary directory");
write(&dir.path().join("src/core/index.ts"));
write(&dir.path().join("src/feature.port.ts"));
for (entry, expected) in [
("./dist/core/index.d.ts", "src/core/index.ts"),
("./dist/core/index.d.mts", "src/core/index.ts"),
("./dist/core/index.d.cts", "src/core/index.ts"),
("./dist/feature.port.d.ts", "src/feature.port.ts"),
] {
assert_eq!(
output_entry_to_source_path(dir.path(), entry, EXTS, |_| true),
Some(dir.path().join(expected)),
"{entry}"
);
}
}
#[cfg_attr(miri, ignore)]
#[test]
fn declaration_output_keeps_a_hand_written_source_declaration() {
let dir = tempfile::tempdir().expect("temporary directory");
write(&dir.path().join("src/types.d.ts"));
assert_eq!(
output_entry_to_source_path(dir.path(), "./dist/types.d.ts", EXTS, |_| true),
Some(dir.path().join("src/types.d.ts"))
);
}
#[cfg_attr(miri, ignore)]
#[test]
fn output_entry_without_a_source_does_not_map() {
let dir = tempfile::tempdir().expect("temporary directory");
write(&dir.path().join("src/other/index.ts"));
for entry in ["./dist/core.mjs", "./dist/core/index.d.ts"] {
assert_eq!(
output_entry_to_source_path(dir.path(), entry, EXTS, |_| true),
None,
"{entry}"
);
}
}
#[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"))
);
}
}