use std::path::Path;
use serde_json::{json, Map, Value};
use crate::mount::Mount;
use crate::{Error, Result};
pub fn tsconfig_paths(mounts: &[Mount], base: &Path) -> Value {
let mut paths = Map::new();
for m in mounts {
let spec = m.specifier_prefix();
if spec.is_empty() {
continue;
}
let key = format!("{}*", spec);
let target = format!("{}/*", relative_dir(base, m.dir()));
paths.insert(key, json!([target]));
}
Value::Object(paths)
}
pub fn write_tsconfig_base(mounts: &[Mount], base: &Path, path: &Path) -> Result<()> {
let doc = json!({
"compilerOptions": {
"moduleResolution": "bundler",
"paths": tsconfig_paths(mounts, base),
}
});
let json = serde_json::to_string_pretty(&doc).map_err(|e| Error::Compose(e.to_string()))?;
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(path, json)?;
Ok(())
}
pub fn tsconfig_node_modules_paths(package_json: &Path) -> Result<Value> {
let specs = crate::vendor::specs_from_package_json(package_json)?;
let mut paths = Map::new();
for spec in &specs {
let name = spec.name();
paths.insert(name.to_string(), json!([format!("./node_modules/{name}")]));
paths.insert(
format!("{name}/*"),
json!([format!("./node_modules/{name}/*")]),
);
}
Ok(Value::Object(paths))
}
fn relative_dir(base: &Path, dir: &Path) -> String {
let rel = dir.strip_prefix(base).unwrap_or(dir);
let s = rel.to_string_lossy().replace('\\', "/");
if s.is_empty() {
".".to_string()
} else if rel.is_absolute() {
s
} else {
format!("./{s}")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn paths_from_mounts_are_specifier_to_relative_dir() {
let base = Path::new("/work");
let mounts = [
Mount::new("contacts", "/work/modules/contacts/web/src")
.specifier("@module/contacts/")
.url("/modules/contacts/"),
Mount::new("lib", "/work/packages/frontend/web/src/lib"),
Mount::root("/work/packages/frontend/web/src"),
];
let paths = tsconfig_paths(&mounts, base);
let obj = paths.as_object().unwrap();
assert_eq!(
obj["@module/contacts/*"],
json!(["./modules/contacts/web/src/*"])
);
assert_eq!(obj["lib/*"], json!(["./packages/frontend/web/src/lib/*"]));
assert_eq!(obj.len(), 2, "root mount has no specifier → no path entry");
}
#[test]
fn write_base_roundtrips() {
let dir = tempfile::tempdir().unwrap();
let base = dir.path();
let mounts = [Mount::new("ui", base.join("ui/src"))];
let out = base.join("tsconfig.base.json");
write_tsconfig_base(&mounts, base, &out).unwrap();
let written: Value = serde_json::from_slice(&std::fs::read(&out).unwrap()).unwrap();
assert_eq!(
written["compilerOptions"]["paths"]["ui/*"],
json!(["./ui/src/*"])
);
}
#[test]
fn node_modules_paths_from_dependencies() {
let dir = tempfile::tempdir().unwrap();
let pkg = dir.path().join("package.json");
std::fs::write(
&pkg,
r#"{ "dependencies": { "lit": "^3", "@lit/context": "1.1.6", "jose": "6.2.3" } }"#,
)
.unwrap();
let paths = tsconfig_node_modules_paths(&pkg).unwrap();
let obj = paths.as_object().unwrap();
assert_eq!(obj["lit"], json!(["./node_modules/lit"]));
assert_eq!(obj["lit/*"], json!(["./node_modules/lit/*"]));
assert_eq!(obj["@lit/context"], json!(["./node_modules/@lit/context"]));
assert_eq!(
obj["@lit/context/*"],
json!(["./node_modules/@lit/context/*"])
);
assert_eq!(obj["jose"], json!(["./node_modules/jose"]));
assert_eq!(obj.len(), 6, "3 packages × (bare + /*)");
}
#[test]
fn node_modules_paths_honor_webdependencies_whitelist() {
let dir = tempfile::tempdir().unwrap();
let pkg = dir.path().join("package.json");
std::fs::write(
&pkg,
r#"{ "dependencies": { "lit": "^3", "pg": "^8" },
"web_modules": { "webDependencies": ["lit"] } }"#,
)
.unwrap();
let paths = tsconfig_node_modules_paths(&pkg).unwrap();
let obj = paths.as_object().unwrap();
assert!(obj.contains_key("lit"));
assert!(
!obj.contains_key("pg"),
"pg is not in webDependencies → no tsconfig path"
);
assert_eq!(obj.len(), 2, "only lit (bare + /*)");
}
}