use serde_json::Value;
use std::path::{Path, PathBuf};
#[derive(Debug, Default, Clone)]
pub struct CargoManifest {
pub package_name: Option<String>,
pub lib_path: Option<PathBuf>,
pub bins: Vec<BinTarget>,
pub workspace_members: Vec<String>,
pub manifest_dir: PathBuf,
}
#[derive(Debug, Clone)]
pub struct BinTarget {
pub name: Option<String>,
pub path: Option<PathBuf>,
}
#[derive(Debug, Default, Clone)]
pub struct PyProject {
pub project_name: Option<String>,
}
#[derive(Debug, Default, Clone)]
pub struct PackageJson {
pub name: Option<String>,
pub main: Option<String>,
pub module: Option<String>,
pub types: Option<String>,
pub exports: Option<Value>,
pub manifest_dir: PathBuf,
}
pub const EXPORT_CONDITIONS: &[&str] = &[
"types",
"typescript",
"import",
"module",
"default",
"node",
"require",
"browser",
];
pub fn parse_cargo_toml(path: &Path) -> Option<CargoManifest> {
let raw = std::fs::read_to_string(path).ok()?;
let manifest_dir = path.parent().unwrap_or(Path::new(".")).to_path_buf();
let mut m = CargoManifest {
manifest_dir,
..Default::default()
};
let mut section = String::new();
let mut current_bin: Option<BinTarget> = None;
for raw_line in raw.lines() {
let line = _strip_comment(raw_line).trim();
if line.is_empty() {
continue;
}
if let Some(hdr) = line.strip_prefix('[').and_then(|s| s.strip_suffix(']')) {
if section == "[[bin]]" {
if let Some(b) = current_bin.take() {
m.bins.push(b);
}
}
section = format!("[{}]", hdr);
if hdr == "[bin]" {
section = "[[bin]]".to_string();
current_bin = Some(BinTarget { name: None, path: None });
}
continue;
}
let (key, value) = match line.split_once('=') {
Some(kv) => (kv.0.trim(), kv.1.trim()),
None => continue,
};
match section.as_str() {
"[package]" => {
if key == "name" {
m.package_name = _unquote(value);
}
}
"[lib]" => {
if key == "path" {
m.lib_path = _unquote(value).map(PathBuf::from);
}
}
"[[bin]]" => {
if let Some(b) = current_bin.as_mut() {
if key == "name" {
b.name = _unquote(value);
} else if key == "path" {
b.path = _unquote(value).map(PathBuf::from);
}
}
}
"[workspace]" => {
if key == "members" {
m.workspace_members = _parse_string_array(value);
}
}
_ => {}
}
}
if let Some(b) = current_bin.take() {
m.bins.push(b);
}
Some(m)
}
pub fn parse_pyproject_toml(path: &Path) -> Option<PyProject> {
let raw = std::fs::read_to_string(path).ok()?;
let mut p = PyProject::default();
let mut section = String::new();
for raw_line in raw.lines() {
let line = _strip_comment(raw_line).trim();
if line.is_empty() {
continue;
}
if let Some(hdr) = line.strip_prefix('[').and_then(|s| s.strip_suffix(']')) {
section = format!("[{}]", hdr);
continue;
}
let (key, value) = match line.split_once('=') {
Some(kv) => (kv.0.trim(), kv.1.trim()),
None => continue,
};
if section == "[project]" && key == "name" {
p.project_name = _unquote(value);
}
}
Some(p)
}
pub fn parse_package_json(path: &Path) -> Option<PackageJson> {
let raw = std::fs::read_to_string(path).ok()?;
let v: Value = serde_json::from_str(&raw).ok()?;
let manifest_dir = path.parent().unwrap_or(Path::new(".")).to_path_buf();
let name = v.get("name").and_then(|x| x.as_str()).map(|s| s.to_string());
let main = v.get("main").and_then(|x| x.as_str()).map(|s| s.to_string());
let module = v.get("module").and_then(|x| x.as_str()).map(|s| s.to_string());
let types = v
.get("types")
.or_else(|| v.get("typings"))
.and_then(|x| x.as_str())
.map(|s| s.to_string());
let exports = v.get("exports").cloned();
Some(PackageJson {
name,
main,
module,
types,
exports,
manifest_dir,
})
}
pub fn resolve_package_entry(pkg: &PackageJson) -> Option<PathBuf> {
if let Some(ex) = &pkg.exports {
if let Some(rel) = _resolve_exports_root(ex) {
if let Some(p) = _exists_with_source_pref(&pkg.manifest_dir.join(rel)) {
return Some(p);
}
}
}
if let Some(rel) = pkg.types.as_deref().or(pkg.module.as_deref()).or(pkg.main.as_deref()) {
if let Some(p) = _exists_with_source_pref(&pkg.manifest_dir.join(rel)) {
return Some(p);
}
}
for stem in ["index", "main", "src/index"] {
for ext in ["ts", "tsx", "mts", "cts", "js", "jsx", "mjs", "cjs", "d.ts"] {
let cand = pkg.manifest_dir.join(format!("{}.{}", stem, ext));
if cand.is_file() {
return Some(cand);
}
}
}
None
}
fn _resolve_exports_root(v: &Value) -> Option<String> {
match v {
Value::String(s) => Some(s.clone()),
Value::Object(map) => {
let is_subpath_map = map.keys().any(|k| k.starts_with("./") || k == ".");
if is_subpath_map {
if let Some(dot) = map.get(".") {
return _resolve_conditional(dot);
}
return None;
}
_resolve_conditional(v)
}
_ => None,
}
}
fn _resolve_conditional(v: &Value) -> Option<String> {
match v {
Value::String(s) => Some(s.clone()),
Value::Array(arr) => {
for item in arr {
if let Some(s) = _resolve_conditional(item) {
return Some(s);
}
}
None
}
Value::Object(map) => {
for cond in EXPORT_CONDITIONS {
if let Some(inner) = map.get(*cond) {
if let Some(s) = _resolve_conditional(inner) {
return Some(s);
}
}
}
for (k, inner) in map {
if EXPORT_CONDITIONS.iter().any(|c| c == k) {
continue;
}
if k.starts_with('.') {
continue;
}
if let Some(s) = _resolve_conditional(inner) {
return Some(s);
}
}
None
}
_ => None,
}
}
fn _exists_with_source_pref(p: &Path) -> Option<PathBuf> {
if !p.is_file() {
if let Some(stem) = p.file_stem().and_then(|s| s.to_str()) {
let parent = p.parent().unwrap_or(Path::new("."));
for ext in ["ts", "tsx", "mts", "cts"] {
let alt = parent.join(format!("{}.{}", stem, ext));
if alt.is_file() {
return Some(alt);
}
}
}
return None;
}
let ext = p.extension().and_then(|s| s.to_str()).unwrap_or("");
if matches!(ext, "ts" | "tsx" | "mts" | "cts") {
return Some(p.to_path_buf());
}
if matches!(ext, "js" | "jsx" | "mjs" | "cjs") {
if let Some(stem) = p.file_stem().and_then(|s| s.to_str()) {
let parent = p.parent().unwrap_or(Path::new("."));
for ext in ["ts", "tsx", "mts", "cts"] {
let alt = parent.join(format!("{}.{}", stem, ext));
if alt.is_file() {
return Some(alt);
}
}
}
}
Some(p.to_path_buf())
}
fn _strip_comment(s: &str) -> &str {
if let Some(i) = s.find('#') {
&s[..i]
} else {
s
}
}
fn _unquote(v: &str) -> Option<String> {
let v = v.trim();
if let Some(s) = v.strip_prefix('"').and_then(|s| s.strip_suffix('"')) {
Some(s.to_string())
} else if let Some(s) = v.strip_prefix('\'').and_then(|s| s.strip_suffix('\'')) {
Some(s.to_string())
} else {
None
}
}
fn _parse_string_array(v: &str) -> Vec<String> {
let v = v.trim();
let inner = v.strip_prefix('[').and_then(|s| s.strip_suffix(']'));
let inner = match inner {
Some(s) => s,
None => return Vec::new(),
};
inner
.split(',')
.filter_map(|item| _unquote(item.trim()))
.collect()
}