spinne_core/
package_json.rs1use serde_json::Value;
2use spinne_logger::Logger;
3use std::collections::HashSet;
4use std::fs;
5use std::path::PathBuf;
6
7#[derive(Debug, Clone, Default)]
9pub struct PackageJson {
10 pub path: PathBuf,
12 pub name: Option<String>,
14 pub workspaces: Option<Vec<String>>,
16 pub dependencies: Option<HashSet<String>>,
18 pub dev_dependencies: Option<HashSet<String>>,
20 pub peer_dependencies: Option<HashSet<String>>,
22}
23
24impl PackageJson {
25 pub fn read(path: &PathBuf, with_dependencies: bool) -> Option<Self> {
32 if !path.exists() {
33 Logger::error(&format!("No package.json found at {}", path.display()));
34 return None;
35 }
36
37 let mut package_json = Self::default();
38 package_json.path = path.clone();
39
40 match fs::read_to_string(&path) {
41 Ok(content) => match serde_json::from_str::<Value>(&content) {
42 Ok(mut parsed) => {
43 if let Some(json_object) = parsed.as_object_mut() {
44 json_object.remove("scripts");
46 json_object.remove("optionalDependencies");
47 json_object.remove("resolutions");
48 json_object.remove("overrides");
49 json_object.remove("packageManager");
50 json_object.remove("engines");
51
52 package_json.name = json_object
54 .get("name")
55 .and_then(|field| field.as_str())
56 .map(ToString::to_string);
57
58 package_json.workspaces =
60 Self::get_workspaces(json_object.get("workspaces"));
61
62 if with_dependencies {
63 package_json.dependencies =
65 Self::get_dependencies(json_object.get("dependencies"));
66 package_json.dev_dependencies =
67 Self::get_dependencies(json_object.get("devDependencies"));
68 package_json.peer_dependencies =
69 Self::get_dependencies(json_object.get("peerDependencies"));
70 }
71 }
72
73 Some(package_json)
74 }
75 Err(e) => {
76 Logger::error(&format!("Failed to parse package.json: {}", e));
77 None
78 }
79 },
80 Err(e) => {
81 Logger::error(&format!("Failed to read package.json: {}", e));
82 None
83 }
84 }
85 }
86
87 pub fn get_all_dependencies(&self) -> Option<HashSet<String>> {
89 let mut all_deps = HashSet::new();
90
91 if let Some(deps) = &self.dependencies {
92 all_deps.extend(deps.iter().cloned());
93 }
94
95 if let Some(dev_deps) = &self.dev_dependencies {
96 all_deps.extend(dev_deps.iter().cloned());
97 }
98
99 if let Some(peer_deps) = &self.peer_dependencies {
100 all_deps.extend(peer_deps.iter().cloned());
101 }
102
103 if all_deps.is_empty() {
104 None
105 } else {
106 Some(all_deps)
107 }
108 }
109
110 pub fn find_dependency(&self, name: &str) -> Option<String> {
116 if let Some(deps) = &self.dependencies {
117 if deps.contains(name) {
118 return Some(name.to_string());
119 }
120 }
121
122 if let Some(dev_deps) = &self.dev_dependencies {
123 if dev_deps.contains(name) {
124 return Some(name.to_string());
125 }
126 }
127
128 if let Some(peer_deps) = &self.peer_dependencies {
129 if peer_deps.contains(name) {
130 return Some(name.to_string());
131 }
132 }
133
134 None
135 }
136
137 fn get_dependencies(deps_value: Option<&Value>) -> Option<HashSet<String>> {
138 deps_value.and_then(|deps| {
139 if let Some(obj) = deps.as_object() {
140 let deps: HashSet<String> = obj.keys().cloned().collect();
141 if deps.is_empty() {
142 None
143 } else {
144 Some(deps)
145 }
146 } else {
147 None
148 }
149 })
150 }
151
152 fn get_workspaces(json: Option<&Value>) -> Option<Vec<String>> {
154 let workspaces = json.and_then(|field| field.as_array());
155
156 match workspaces {
157 Some(workspaces) => Some(
158 workspaces
159 .iter()
160 .map(|item| item.as_str().unwrap().to_string())
161 .collect(),
162 ),
163 None => None,
164 }
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use crate::util::test_utils::create_mock_project;
171
172 use super::*;
173
174 #[test]
175 fn test_read_package_json() {
176 let temp_dir = create_mock_project(&vec![(
177 "package.json",
178 r#"
179 {
180 "name": "test-project",
181 "version": "1.0.0",
182 "workspaces": ["packages/*"]
183 }
184 "#,
185 )]);
186
187 let package_json =
188 PackageJson::read(&PathBuf::from(temp_dir.path().join("package.json")), true)
189 .expect("Failed to read package.json");
190 assert_eq!(package_json.name, Some("test-project".to_string()));
191 }
192
193 #[test]
194 fn test_read_package_json_with_invalid_name() {
195 let temp_dir = create_mock_project(&vec![(
196 "package.json",
197 r#"
198 {
199 "name": 123,
200 "version": "1.0.0",
201 "workspaces": ["packages/*"]
202 }
203 "#,
204 )]);
205
206 let package_json =
207 PackageJson::read(&PathBuf::from(temp_dir.path().join("package.json")), true)
208 .expect("Failed to read package.json");
209 assert_eq!(package_json.name, None);
210 }
211
212 #[test]
213 fn test_missing_package_json() {
214 assert!(PackageJson::read(&PathBuf::from("package.json"), true).is_none());
215 }
216
217 #[test]
218 fn test_resolves_workspaces() {
219 let temp_dir = create_mock_project(&vec![(
220 "package.json",
221 r#"
222 {
223 "name": "test-project",
224 "version": "1.0.0",
225 "workspaces": ["packages/*"]
226 }"#,
227 )]);
228
229 let package_json =
230 PackageJson::read(&PathBuf::from(temp_dir.path().join("package.json")), true).unwrap();
231
232 assert_eq!(
233 package_json.workspaces,
234 Some(vec!["packages/*".to_string()])
235 );
236 }
237
238 #[test]
239 fn test_get_all_dependencies() {
240 let temp_dir = create_mock_project(&vec![(
241 "package.json",
242 r#"
243 {
244 "name": "test-project",
245 "version": "1.0.0",
246 "dependencies": { "react": "18.3.1" },
247 "devDependencies": { "typescript": "5.0.0" },
248 "peerDependencies": { "react-dom": "18.3.1" }
249 }
250 "#,
251 )]);
252
253 let package_json =
254 PackageJson::read(&PathBuf::from(temp_dir.path().join("package.json")), true).unwrap();
255
256 assert_eq!(
257 package_json.dependencies,
258 Some(HashSet::from(["react".to_string()]))
259 );
260 assert_eq!(
261 package_json.dev_dependencies,
262 Some(HashSet::from(["typescript".to_string()]))
263 );
264 assert_eq!(
265 package_json.peer_dependencies,
266 Some(HashSet::from(["react-dom".to_string()]))
267 );
268
269 assert_eq!(
270 package_json.get_all_dependencies(),
271 Some(HashSet::from([
272 "react".to_string(),
273 "typescript".to_string(),
274 "react-dom".to_string(),
275 ]))
276 );
277 }
278
279 #[test]
280 fn test_find_dependency() {
281 let temp_dir = create_mock_project(&vec![(
282 "package.json",
283 r#"
284 {
285 "name": "test-project",
286 "version": "1.0.0",
287 "dependencies": { "react": "18.3.1" },
288 "devDependencies": { "typescript": "5.0.0" },
289 "peerDependencies": { "react-dom": "18.3.1" }
290 }
291 "#,
292 )]);
293
294 let package_json =
295 PackageJson::read(&PathBuf::from(temp_dir.path().join("package.json")), true).unwrap();
296
297 assert_eq!(
298 package_json.find_dependency("react"),
299 Some("react".to_string())
300 );
301 assert_eq!(
302 package_json.find_dependency("typescript"),
303 Some("typescript".to_string())
304 );
305 assert_eq!(
306 package_json.find_dependency("react-dom"),
307 Some("react-dom".to_string())
308 );
309 assert_eq!(package_json.find_dependency("react-router"), None);
310 }
311}