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