use super::{FileSet, ImportTarget};
use crate::types::ImportStatement;
pub fn resolve_ts_import(
import: &ImportStatement,
from_file: usize,
files: &FileSet,
) -> ImportTarget {
let spec = &import.module_path;
if is_relative(spec) {
let dir = files.dir_of(from_file).unwrap_or_default();
let mut candidates = vec![
format!("{spec}.ts"),
format!("{spec}.tsx"),
format!("{spec}.mts"),
format!("{spec}.cts"),
format!("{spec}.js"),
format!("{spec}.jsx"),
format!("{spec}.mjs"),
format!("{spec}.cjs"),
format!("{spec}/index.ts"),
format!("{spec}/index.tsx"),
format!("{spec}/index.mts"),
format!("{spec}/index.cts"),
format!("{spec}/index.js"),
format!("{spec}/index.jsx"),
format!("{spec}/index.mjs"),
format!("{spec}/index.cjs"),
spec.clone(),
];
if let Some(stem) = js_extension_stem(spec) {
candidates.splice(
0..0,
[
format!("{stem}.ts"),
format!("{stem}.tsx"),
format!("{stem}.mts"),
format!("{stem}.cts"),
format!("{stem}.jsx"),
],
);
}
for cand in &candidates {
if let Some(idx) = files.probe(&dir, cand) {
return ImportTarget::File(idx);
}
}
return ImportTarget::Unresolved(spec.clone());
}
if spec.starts_with('@') {
return ImportTarget::Unresolved(spec.clone());
}
ImportTarget::External(spec.clone())
}
fn is_relative(spec: &str) -> bool {
spec == "." || spec == ".." || spec.starts_with("./") || spec.starts_with("../")
}
fn js_extension_stem(spec: &str) -> Option<&str> {
[".js", ".jsx", ".mjs", ".cjs"]
.iter()
.find_map(|ext| spec.strip_suffix(ext))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::TreeNode;
fn ts_file(path: &str) -> TreeNode {
TreeNode::new(path.to_string(), "typescript".to_string())
}
fn import(spec: &str, from_path: &str, is_bare: bool) -> ImportStatement {
ImportStatement::new(spec.to_string(), from_path.to_string()).with_external(is_bare)
}
fn resolve(files: &[TreeNode], from: usize, spec: &str, is_bare: bool) -> ImportTarget {
let fs = FileSet::new(files);
let from_path = files[from].file_path.clone();
resolve_ts_import(&import(spec, &from_path, is_bare), from, &fs)
}
#[test]
fn relative_resolves_to_sibling_ts() {
let files = vec![ts_file("src/a.ts"), ts_file("src/x.ts")];
assert_eq!(resolve(&files, 0, "./x", false), ImportTarget::File(1));
}
#[test]
fn relative_resolves_via_index_ts() {
let files = vec![ts_file("src/a.ts"), ts_file("src/x/index.ts")];
assert_eq!(resolve(&files, 0, "./x", false), ImportTarget::File(1));
}
#[test]
fn relative_resolves_via_tsx() {
let files = vec![ts_file("src/a.ts"), ts_file("src/x.tsx")];
assert_eq!(resolve(&files, 0, "./x", false), ImportTarget::File(1));
}
#[test]
fn relative_resolves_via_index_tsx() {
let files = vec![ts_file("src/a.ts"), ts_file("src/x/index.tsx")];
assert_eq!(resolve(&files, 0, "./x", false), ImportTarget::File(1));
}
#[test]
fn parent_relative_resolves_across_dirs() {
let files = vec![ts_file("src/pkg/c.ts"), ts_file("src/a/b.ts")];
assert_eq!(resolve(&files, 0, "../a/b", false), ImportTarget::File(1));
}
#[test]
fn specifier_with_explicit_extension_matches_verbatim() {
let files = vec![ts_file("src/a.ts"), ts_file("src/x.ts")];
assert_eq!(resolve(&files, 0, "./x.ts", false), ImportTarget::File(1));
}
#[test]
fn bare_specifier_is_external() {
let files = vec![ts_file("src/a.ts")];
assert_eq!(
resolve(&files, 0, "react", true),
ImportTarget::External("react".to_string())
);
}
#[test]
fn node_builtin_is_external() {
let files = vec![ts_file("src/a.ts")];
assert_eq!(
resolve(&files, 0, "node:fs", true),
ImportTarget::External("node:fs".to_string())
);
}
#[test]
fn at_prefixed_specifier_is_unresolved_alias() {
let files = vec![ts_file("src/a.ts")];
assert_eq!(
resolve(&files, 0, "@app/thing", true),
ImportTarget::Unresolved("@app/thing".to_string())
);
}
#[test]
fn at_slash_specifier_is_unresolved_alias() {
let files = vec![ts_file("src/a.ts")];
assert_eq!(
resolve(&files, 0, "@/utils", true),
ImportTarget::Unresolved("@/utils".to_string())
);
}
#[test]
fn missing_relative_is_unresolved_never_file() {
let files = vec![ts_file("src/a.ts"), ts_file("src/x.ts")];
assert_eq!(
resolve(&files, 0, "./missing", false),
ImportTarget::Unresolved("./missing".to_string())
);
}
#[test]
fn nodenext_js_extension_maps_to_the_ts_source() {
let files = vec![ts_file("src/app.ts"), ts_file("src/util.ts")];
assert_eq!(
resolve(&files, 0, "./util.js", false),
ImportTarget::File(1)
);
}
#[test]
fn explicit_js_extension_still_prefers_a_real_js_sibling() {
let files = vec![ts_file("src/app.ts"), ts_file("src/util.js")];
assert_eq!(
resolve(&files, 0, "./util.js", false),
ImportTarget::File(1)
);
}
#[test]
fn a_same_named_file_beats_a_directory_index() {
let files = vec![
ts_file("src/a.ts"),
ts_file("src/foo/index.ts"),
ts_file("src/foo.js"),
];
assert_eq!(resolve(&files, 0, "./foo", false), ImportTarget::File(2));
}
#[test]
fn directory_index_still_resolves_when_no_file_matches() {
let files = vec![ts_file("src/a.ts"), ts_file("src/foo/index.ts")];
assert_eq!(resolve(&files, 0, "./foo", false), ImportTarget::File(1));
}
#[test]
fn mts_and_cts_sources_resolve() {
let files = vec![ts_file("src/a.mts"), ts_file("src/util.mts")];
assert_eq!(resolve(&files, 0, "./util", false), ImportTarget::File(1));
}
#[test]
fn bare_dot_resolves_to_the_directory_index() {
let files = vec![ts_file("src/pkg/a.ts"), ts_file("src/pkg/index.ts")];
assert_eq!(resolve(&files, 0, ".", false), ImportTarget::File(1));
}
#[test]
fn bare_double_dot_resolves_to_the_parent_index() {
let files = vec![ts_file("src/pkg/a.ts"), ts_file("src/index.ts")];
assert_eq!(resolve(&files, 0, "..", false), ImportTarget::File(1));
}
#[test]
fn ts_preferred_over_tsx_in_probe_order() {
let files = vec![
ts_file("src/a.ts"),
ts_file("src/x.ts"),
ts_file("src/x.tsx"),
];
assert_eq!(resolve(&files, 0, "./x", false), ImportTarget::File(1));
}
}