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