Skip to main content

kdo_resolver/
node.rs

1//! Parser for `package.json` manifests (Node.js / TypeScript projects).
2
3use crate::ManifestParser;
4use kdo_core::{DepKind, Dependency, KdoError, Language, Project};
5use std::path::Path;
6use tracing::debug;
7
8/// Parses Node.js `package.json` manifests.
9pub struct NodeParser;
10
11impl ManifestParser for NodeParser {
12    fn manifest_name(&self) -> &str {
13        "package.json"
14    }
15
16    fn can_parse(&self, manifest_path: &Path) -> bool {
17        manifest_path
18            .file_name()
19            .map(|f| f == "package.json")
20            .unwrap_or(false)
21    }
22
23    fn parse(
24        &self,
25        manifest_path: &Path,
26        workspace_root: &Path,
27    ) -> Result<(Project, Vec<Dependency>), KdoError> {
28        let content = std::fs::read_to_string(manifest_path)?;
29        let doc: serde_json::Value =
30            serde_json::from_str(&content).map_err(|e| KdoError::ParseError {
31                path: manifest_path.to_path_buf(),
32                source: e.into(),
33            })?;
34
35        let name = doc
36            .get("name")
37            .and_then(|v| v.as_str())
38            .ok_or_else(|| KdoError::ParseError {
39                path: manifest_path.to_path_buf(),
40                source: anyhow::anyhow!("missing name field"),
41            })?
42            .to_string();
43
44        let description = doc
45            .get("description")
46            .and_then(|v| v.as_str())
47            .map(String::from);
48
49        let project_dir = manifest_path
50            .parent()
51            .unwrap_or(Path::new("."))
52            .to_path_buf();
53
54        // Detect TypeScript vs JavaScript
55        let language = if project_dir.join("tsconfig.json").exists()
56            || doc
57                .get("devDependencies")
58                .and_then(|v| v.as_object())
59                .map(|d| d.contains_key("typescript"))
60                .unwrap_or(false)
61        {
62            Language::TypeScript
63        } else {
64            Language::JavaScript
65        };
66
67        debug!(name = %name, language = %language, "parsed package.json");
68
69        let mut deps = Vec::new();
70
71        for (section, kind) in [
72            ("dependencies", DepKind::Source),
73            ("devDependencies", DepKind::Dev),
74            ("peerDependencies", DepKind::Source),
75        ] {
76            if let Some(obj) = doc.get(section).and_then(|v| v.as_object()) {
77                for (dep_name, dep_val) in obj {
78                    let version_req = dep_val.as_str().unwrap_or("*").to_string();
79                    let is_workspace = version_req.starts_with("workspace:");
80                    let resolved_path = if is_workspace || version_req.starts_with("file:") {
81                        let clean = version_req
82                            .trim_start_matches("workspace:")
83                            .trim_start_matches("file:");
84                        if clean == "*" || clean == "^" {
85                            // Workspace protocol — resolve by name in parent directories
86                            None
87                        } else {
88                            Some(workspace_root.join(clean))
89                        }
90                    } else {
91                        None
92                    };
93
94                    deps.push(Dependency {
95                        name: dep_name.clone(),
96                        version_req,
97                        kind: kind.clone(),
98                        is_workspace,
99                        resolved_path,
100                    });
101                }
102            }
103        }
104
105        let project = Project {
106            name,
107            path: project_dir,
108            language,
109            manifest_path: manifest_path.to_path_buf(),
110            context_summary: description,
111            public_api_files: Vec::new(),
112            internal_files: Vec::new(),
113            content_hash: [0u8; 32],
114        };
115
116        Ok((project, deps))
117    }
118}