1use anyhow::Result;
2use async_trait::async_trait;
3use changepacks_core::{Language, Package, UpdateType};
4use changepacks_utils::{detect_indent, next_version};
5use serde::Serialize;
6use std::collections::HashSet;
7use std::path::{Path, PathBuf};
8use tokio::fs::{read_to_string, write};
9
10use crate::detect_package_manager_recursive;
11
12#[derive(Debug)]
13pub struct NodePackage {
14 name: Option<String>,
15 version: Option<String>,
16 path: PathBuf,
17 relative_path: PathBuf,
18 is_changed: bool,
19 dependencies: HashSet<String>,
20}
21
22impl NodePackage {
23 #[must_use]
24 pub fn new(
25 name: Option<String>,
26 version: Option<String>,
27 path: PathBuf,
28 relative_path: PathBuf,
29 ) -> Self {
30 Self {
31 name,
32 version,
33 path,
34 relative_path,
35 is_changed: false,
36 dependencies: HashSet::new(),
37 }
38 }
39}
40
41#[async_trait]
42impl Package for NodePackage {
43 fn name(&self) -> Option<&str> {
44 self.name.as_deref()
45 }
46
47 fn version(&self) -> Option<&str> {
48 self.version.as_deref()
49 }
50
51 fn path(&self) -> &Path {
52 &self.path
53 }
54
55 fn relative_path(&self) -> &Path {
56 &self.relative_path
57 }
58
59 async fn update_version(&mut self, update_type: UpdateType) -> Result<()> {
60 let current_version = self.version.as_deref().unwrap_or("0.0.0");
61 let new_version = next_version(current_version, update_type)?;
62
63 let package_json_raw = read_to_string(&self.path).await?;
64 let indent = detect_indent(&package_json_raw);
65 let mut package_json: serde_json::Value = serde_json::from_str(&package_json_raw)?;
66 package_json["version"] = serde_json::Value::String(new_version.clone());
67 let ind = &b" ".repeat(indent);
68 let formatter = serde_json::ser::PrettyFormatter::with_indent(ind);
69 let writer = Vec::new();
70 let mut ser = serde_json::Serializer::with_formatter(writer, formatter);
71 package_json.serialize(&mut ser)?;
72 write(
73 &self.path,
74 format!(
75 "{}{}",
76 String::from_utf8(ser.into_inner())?.trim_end(),
77 if package_json_raw.ends_with('\n') {
78 "\n"
79 } else {
80 ""
81 }
82 ),
83 )
84 .await?;
85 self.version = Some(new_version);
86 Ok(())
87 }
88
89 fn language(&self) -> Language {
90 Language::Node
91 }
92
93 fn set_changed(&mut self, changed: bool) {
94 self.is_changed = changed;
95 }
96
97 fn is_changed(&self) -> bool {
98 self.is_changed
99 }
100
101 fn set_name(&mut self, name: String) {
102 self.name = Some(name);
103 }
104
105 fn default_publish_command(&self) -> String {
106 detect_package_manager_recursive(&self.path)
107 .publish_command()
108 .to_string()
109 }
110
111 fn dependencies(&self) -> &HashSet<String> {
112 &self.dependencies
113 }
114
115 fn add_dependency(&mut self, dependency: &str) {
116 self.dependencies.insert(dependency.to_string());
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123 use changepacks_core::UpdateType;
124 use std::fs;
125 use tempfile::TempDir;
126 use tokio::fs::read_to_string;
127
128 #[tokio::test]
129 async fn test_node_package_new() {
130 let package = NodePackage::new(
131 Some("test-package".to_string()),
132 Some("1.0.0".to_string()),
133 PathBuf::from("/test/package.json"),
134 PathBuf::from("test/package.json"),
135 );
136
137 assert_eq!(package.name(), Some("test-package"));
138 assert_eq!(package.version(), Some("1.0.0"));
139 assert_eq!(package.path(), PathBuf::from("/test/package.json"));
140 assert_eq!(package.relative_path(), PathBuf::from("test/package.json"));
141 assert_eq!(package.language(), Language::Node);
142 assert!(!package.is_changed());
143 assert_eq!(package.default_publish_command(), "npm publish");
144 }
145
146 #[tokio::test]
147 async fn test_node_package_set_changed() {
148 let mut package = NodePackage::new(
149 Some("test-package".to_string()),
150 Some("1.0.0".to_string()),
151 PathBuf::from("/test/package.json"),
152 PathBuf::from("test/package.json"),
153 );
154
155 assert!(!package.is_changed());
156 package.set_changed(true);
157 assert!(package.is_changed());
158 package.set_changed(false);
159 assert!(!package.is_changed());
160 }
161
162 #[tokio::test]
163 async fn test_node_package_update_version_patch() {
164 let temp_dir = TempDir::new().unwrap();
165 let package_json = temp_dir.path().join("package.json");
166 fs::write(
167 &package_json,
168 r#"{
169 "name": "test-package",
170 "version": "1.0.0"
171}
172"#,
173 )
174 .unwrap();
175
176 let mut package = NodePackage::new(
177 Some("test-package".to_string()),
178 Some("1.0.0".to_string()),
179 package_json.clone(),
180 PathBuf::from("package.json"),
181 );
182
183 package.update_version(UpdateType::Patch).await.unwrap();
184
185 let content = read_to_string(&package_json).await.unwrap();
186 assert!(content.contains(r#""version": "1.0.1""#));
187
188 temp_dir.close().unwrap();
189 }
190
191 #[tokio::test]
192 async fn test_node_package_update_version_minor() {
193 let temp_dir = TempDir::new().unwrap();
194 let package_json = temp_dir.path().join("package.json");
195 fs::write(
196 &package_json,
197 r#"{
198 "name": "test-package",
199 "version": "1.0.0"
200}
201"#,
202 )
203 .unwrap();
204
205 let mut package = NodePackage::new(
206 Some("test-package".to_string()),
207 Some("1.0.0".to_string()),
208 package_json.clone(),
209 PathBuf::from("package.json"),
210 );
211
212 package.update_version(UpdateType::Minor).await.unwrap();
213
214 let content = read_to_string(&package_json).await.unwrap();
215 assert!(content.contains(r#""version": "1.1.0""#));
216
217 temp_dir.close().unwrap();
218 }
219
220 #[tokio::test]
221 async fn test_node_package_update_version_major() {
222 let temp_dir = TempDir::new().unwrap();
223 let package_json = temp_dir.path().join("package.json");
224 fs::write(
225 &package_json,
226 r#"{
227 "name": "test-package",
228 "version": "1.0.0"
229}
230"#,
231 )
232 .unwrap();
233
234 let mut package = NodePackage::new(
235 Some("test-package".to_string()),
236 Some("1.0.0".to_string()),
237 package_json.clone(),
238 PathBuf::from("package.json"),
239 );
240
241 package.update_version(UpdateType::Major).await.unwrap();
242
243 let content = read_to_string(&package_json).await.unwrap();
244 assert!(content.contains(r#""version": "2.0.0""#));
245
246 temp_dir.close().unwrap();
247 }
248
249 #[tokio::test]
250 async fn test_node_package_update_version_preserves_formatting() {
251 let temp_dir = TempDir::new().unwrap();
252 let package_json = temp_dir.path().join("package.json");
253 fs::write(
254 &package_json,
255 r#"{
256 "name": "test-package",
257 "version": "1.2.3",
258 "description": "A test package",
259 "dependencies": {
260 "express": "^4.18.0"
261 }
262}
263"#,
264 )
265 .unwrap();
266
267 let mut package = NodePackage::new(
268 Some("test-package".to_string()),
269 Some("1.2.3".to_string()),
270 package_json.clone(),
271 PathBuf::from("package.json"),
272 );
273
274 package.update_version(UpdateType::Patch).await.unwrap();
275
276 let content = read_to_string(&package_json).await.unwrap();
277 assert!(content.contains(r#""version": "1.2.4""#));
278 assert!(content.contains(r#""name": "test-package""#));
279 assert!(content.contains(r#""description": "A test package""#));
280 assert!(content.contains(r#""dependencies""#));
281
282 temp_dir.close().unwrap();
283 }
284
285 #[tokio::test]
286 async fn test_node_package_update_version_preserves_newline() {
287 let temp_dir = TempDir::new().unwrap();
288 let package_json = temp_dir.path().join("package.json");
289 fs::write(
290 &package_json,
291 r#"{"name":"test-package","version":"1.0.0"}
292"#,
293 )
294 .unwrap();
295
296 let mut package = NodePackage::new(
297 Some("test-package".to_string()),
298 Some("1.0.0".to_string()),
299 package_json.clone(),
300 PathBuf::from("package.json"),
301 );
302
303 package.update_version(UpdateType::Patch).await.unwrap();
304
305 let content = read_to_string(&package_json).await.unwrap();
306 assert!(content.ends_with('\n'));
307 assert!(content.contains(r#""version": "1.0.1""#));
308
309 temp_dir.close().unwrap();
310 }
311
312 #[test]
313 fn test_set_name() {
314 let mut package = NodePackage::new(
315 None,
316 Some("1.0.0".to_string()),
317 PathBuf::from("/test/package.json"),
318 PathBuf::from("package.json"),
319 );
320 assert_eq!(package.name(), None);
321 package.set_name("my-project".to_string());
322 assert_eq!(package.name(), Some("my-project"));
323 }
324}