1use crate::ManifestParser;
4use kdo_core::{DepKind, Dependency, KdoError, Language, Project};
5use std::path::Path;
6use tracing::debug;
7
8pub 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 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 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}