mod package_json;
mod parse_specifier;
pub use package_json::PackageJson;
use parse_specifier::parse_specifier;
use path_clean::PathClean;
use serde_json::Map;
use serde_json::Value;
use std::path::Path;
use std::path::PathBuf;
pub fn resolve(
specifier: &str,
referrer: &Path,
conditions: &[&str],
) -> anyhow::Result<PathBuf> {
if specifier.starts_with('/') {
todo!();
}
if specifier.starts_with("./") || specifier.starts_with("../") {
if let Some(parent) = referrer.parent() {
return file_extension_probe(parent.join(specifier), referrer);
} else {
todo!();
}
}
let (package_name, package_subpath) = parse_specifier(specifier).unwrap();
for ancestor in referrer.ancestors() {
let module_dir = ancestor.join("node_modules").join(&package_name);
let package_json_path = module_dir.join("package.json");
if package_json_path.exists() {
let package_json = PackageJson::load(package_json_path)?;
if let Some(map) = package_json.exports_map {
if let Some((key, subpath)) = exports_resolve(&map, &package_subpath) {
let value = map.get(&key).unwrap();
let s = conditions_resolve(value, conditions);
let t = resolve_package_target_string(&s, subpath);
return Ok(module_dir.join(t).clean());
} else {
todo!()
}
}
if package_subpath != "." {
let d = module_dir.join(package_subpath);
if let Ok(m) = d.metadata() {
if m.is_dir() {
return Ok(d.join("index.js").clean());
}
}
return file_extension_probe(d, referrer);
} else if let Some(main) = package_json.main {
return Ok(module_dir.join(main).clean());
} else {
return Ok(module_dir.join("index.js").clean());
}
}
}
Err(not_found(specifier, referrer))
}
fn resolve_package_target_string(
target: &str,
subpath: Option<String>,
) -> String {
if let Some(subpath) = subpath {
target.replace('*', &subpath)
} else {
target.to_string()
}
}
fn conditions_resolve(value: &Value, conditions: &[&str]) -> String {
match value {
Value::String(s) => s.to_string(),
Value::Object(map) => {
for condition in conditions {
if let Some(x) = map.get(&condition.to_string()) {
if let Value::String(s) = x {
return s.to_string();
} else {
todo!()
}
}
}
todo!()
}
_ => todo!(),
}
}
fn exports_resolve(
map: &Map<String, Value>,
subpath: &str,
) -> Option<(String, Option<String>)> {
if map.contains_key(subpath) {
return Some((subpath.to_string(), None));
}
let mut best_match = None;
for key in map.keys() {
if let Some(pattern_index) = key.find('*') {
let key_sub = &key[0..pattern_index];
if subpath.starts_with(key_sub) {
if subpath.ends_with('/') {
todo!()
}
let pattern_trailer = &key[pattern_index + 1..];
if subpath.len() > key.len()
&& subpath.ends_with(pattern_trailer)
&& key.rfind('*') == Some(pattern_index)
{
let rest = subpath
[pattern_index..(subpath.len() - pattern_trailer.len())]
.to_string();
best_match = Some((key, rest));
}
}
}
}
if let Some((key, subpath_)) = best_match {
return Some((key.to_string(), Some(subpath_)));
}
None
}
fn file_extension_probe(
mut p: PathBuf,
referrer: &Path,
) -> anyhow::Result<PathBuf> {
if p.exists() {
Ok(p.clean())
} else {
p.set_extension("js");
if p.exists() {
Ok(p)
} else {
Err(not_found(&p.clean().to_string_lossy(), referrer))
}
}
}
fn not_found(path: &str, referrer: &Path) -> anyhow::Error {
let msg = format!(
"[ERR_MODULE_NOT_FOUND] Cannot find module \"{}\" imported from \"{}\"",
path,
referrer.to_string_lossy()
);
std::io::Error::new(std::io::ErrorKind::NotFound, msg).into()
}
#[cfg(test)]
mod tests {
use super::*;
fn testdir(name: &str) -> PathBuf {
let c = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
c.join("src/testdata").join(name)
}
fn check_node(main: &Path) {
let status = std::process::Command::new("node")
.args([main])
.status()
.unwrap();
assert!(status.success());
}
#[test]
fn cjs_no_main() {
let d = testdir("cjs_no_main");
let main_js = &d.join("main.js");
check_node(main_js);
let p = resolve("foo", main_js, &[]).unwrap();
assert_eq!(p, d.join("node_modules/foo/index.js"));
}
#[test]
fn cjs_main_basic() {
let d = testdir("cjs_main");
let main_js = &d.join("main.js");
check_node(main_js);
let p = resolve("foo", main_js, &[]).unwrap();
assert_eq!(p, d.join("node_modules/foo/main.js"));
}
#[test]
fn cjs_main_reach_inside() {
let d = testdir("cjs_main");
let main_js = &d.join("main.js");
check_node(main_js);
let p = resolve("foo/bar.js", main_js, &[]).unwrap();
assert_eq!(p, d.join("node_modules/foo/bar.js"));
let p = resolve("foo/bar", main_js, &[]).unwrap();
assert_eq!(p, d.join("node_modules/foo/bar.js"));
let p = resolve("foo/dir/cat.js", main_js, &[]).unwrap();
assert_eq!(p, d.join("node_modules/foo/dir/cat.js"));
let p = resolve("foo/dir/cat", main_js, &[]).unwrap();
assert_eq!(p, d.join("node_modules/foo/dir/cat.js"));
let p = resolve("foo/dir", main_js, &[]).unwrap();
assert_eq!(p, d.join("node_modules/foo/dir/index.js"));
let p = resolve("foo/dir/", main_js, &[]).unwrap();
assert_eq!(p, d.join("node_modules/foo/dir/index.js"));
}
#[test]
fn cjs_main_not_found() {
let d = testdir("cjs_main");
let main_js = &d.join("main.js");
check_node(main_js);
let e = resolve("bar", main_js, &[]).unwrap_err();
let ioerr = e.downcast_ref::<std::io::Error>().unwrap();
assert_eq!(ioerr.kind(), std::io::ErrorKind::NotFound);
let msg = format!(
"[ERR_MODULE_NOT_FOUND] Cannot find \"bar\" imported from \"{}\"",
main_js.to_string_lossy()
);
assert_eq!(ioerr.to_string(), msg);
}
#[test]
fn cjs_main_sibling() {
let d = testdir("cjs_main");
let main_js = &d.join("main.js");
check_node(main_js);
let p = resolve("./sibling.js", main_js, &[]).unwrap();
assert_eq!(p, d.join("sibling.js"));
let p = resolve("./sibling", main_js, &[]).unwrap();
assert_eq!(p, d.join("sibling.js"));
}
#[test]
fn cjs_exports_string() {
let d = testdir("cjs_exports_string");
let main_js = &d.join("main.js");
check_node(main_js);
let p = resolve("exports_string", main_js, &[]).unwrap();
assert_eq!(p, d.join("node_modules/exports_string/foo.js"));
}
#[test]
fn cjs_exports_conditional() {
let d = testdir("cjs_exports_conditional");
let main_js = &d.join("main.js");
check_node(main_js);
let p = resolve("exports", main_js, &["require"]).unwrap();
assert_eq!(p, d.join("node_modules/exports/main-require.cjs"));
let p = resolve("exports", main_js, &["import"]).unwrap();
assert_eq!(p, d.join("node_modules/exports/main-module.js"));
}
#[test]
fn cjs_exports_dot() {
let d = testdir("cjs_exports_dot");
let main_js = &d.join("main.js");
check_node(main_js);
let p = resolve("exports", main_js, &[]).unwrap();
assert_eq!(p, d.join("node_modules/exports/main-require.cjs"));
}
#[test]
fn cjs_exports_multi() {
let d = testdir("cjs_exports_multi");
let main_js = &d.join("main.js");
check_node(main_js);
let p = resolve("exports", main_js, &[]).unwrap();
assert_eq!(p, d.join("node_modules/exports/main-require.cjs"));
let p = resolve("exports/foo", main_js, &[]).unwrap();
assert_eq!(p, d.join("node_modules/exports/foo.js"));
let p = resolve("exports/bar/baz", main_js, &[]).unwrap();
assert_eq!(p, d.join("node_modules/exports/bar/baz.js"));
}
#[test]
fn cjs_scoped() {
let d = testdir("cjs_scoped");
let main_js = &d.join("main.js");
check_node(main_js);
let p = resolve("@ne-test-org/hello-world", main_js, &[]).unwrap();
assert_eq!(p, d.join("node_modules/@ne-test-org/hello-world/index.js"));
}
}