Skip to main content

changepacks_node/
package.rs

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}