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    fn is_changed(&self) -> bool {
97        self.is_changed
98    }
99
100    fn default_publish_command(&self) -> String {
101        detect_package_manager_recursive(&self.path)
102            .publish_command()
103            .to_string()
104    }
105
106    fn dependencies(&self) -> &HashSet<String> {
107        &self.dependencies
108    }
109
110    fn add_dependency(&mut self, dependency: &str) {
111        self.dependencies.insert(dependency.to_string());
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118    use changepacks_core::UpdateType;
119    use std::fs;
120    use tempfile::TempDir;
121    use tokio::fs::read_to_string;
122
123    #[tokio::test]
124    async fn test_node_package_new() {
125        let package = NodePackage::new(
126            Some("test-package".to_string()),
127            Some("1.0.0".to_string()),
128            PathBuf::from("/test/package.json"),
129            PathBuf::from("test/package.json"),
130        );
131
132        assert_eq!(package.name(), Some("test-package"));
133        assert_eq!(package.version(), Some("1.0.0"));
134        assert_eq!(package.path(), PathBuf::from("/test/package.json"));
135        assert_eq!(package.relative_path(), PathBuf::from("test/package.json"));
136        assert_eq!(package.language(), Language::Node);
137        assert!(!package.is_changed());
138        assert_eq!(package.default_publish_command(), "npm publish");
139    }
140
141    #[tokio::test]
142    async fn test_node_package_set_changed() {
143        let mut package = NodePackage::new(
144            Some("test-package".to_string()),
145            Some("1.0.0".to_string()),
146            PathBuf::from("/test/package.json"),
147            PathBuf::from("test/package.json"),
148        );
149
150        assert!(!package.is_changed());
151        package.set_changed(true);
152        assert!(package.is_changed());
153        package.set_changed(false);
154        assert!(!package.is_changed());
155    }
156
157    #[tokio::test]
158    async fn test_node_package_update_version_patch() {
159        let temp_dir = TempDir::new().unwrap();
160        let package_json = temp_dir.path().join("package.json");
161        fs::write(
162            &package_json,
163            r#"{
164  "name": "test-package",
165  "version": "1.0.0"
166}
167"#,
168        )
169        .unwrap();
170
171        let mut package = NodePackage::new(
172            Some("test-package".to_string()),
173            Some("1.0.0".to_string()),
174            package_json.clone(),
175            PathBuf::from("package.json"),
176        );
177
178        package.update_version(UpdateType::Patch).await.unwrap();
179
180        let content = read_to_string(&package_json).await.unwrap();
181        assert!(content.contains(r#""version": "1.0.1""#));
182
183        temp_dir.close().unwrap();
184    }
185
186    #[tokio::test]
187    async fn test_node_package_update_version_minor() {
188        let temp_dir = TempDir::new().unwrap();
189        let package_json = temp_dir.path().join("package.json");
190        fs::write(
191            &package_json,
192            r#"{
193  "name": "test-package",
194  "version": "1.0.0"
195}
196"#,
197        )
198        .unwrap();
199
200        let mut package = NodePackage::new(
201            Some("test-package".to_string()),
202            Some("1.0.0".to_string()),
203            package_json.clone(),
204            PathBuf::from("package.json"),
205        );
206
207        package.update_version(UpdateType::Minor).await.unwrap();
208
209        let content = read_to_string(&package_json).await.unwrap();
210        assert!(content.contains(r#""version": "1.1.0""#));
211
212        temp_dir.close().unwrap();
213    }
214
215    #[tokio::test]
216    async fn test_node_package_update_version_major() {
217        let temp_dir = TempDir::new().unwrap();
218        let package_json = temp_dir.path().join("package.json");
219        fs::write(
220            &package_json,
221            r#"{
222  "name": "test-package",
223  "version": "1.0.0"
224}
225"#,
226        )
227        .unwrap();
228
229        let mut package = NodePackage::new(
230            Some("test-package".to_string()),
231            Some("1.0.0".to_string()),
232            package_json.clone(),
233            PathBuf::from("package.json"),
234        );
235
236        package.update_version(UpdateType::Major).await.unwrap();
237
238        let content = read_to_string(&package_json).await.unwrap();
239        assert!(content.contains(r#""version": "2.0.0""#));
240
241        temp_dir.close().unwrap();
242    }
243
244    #[tokio::test]
245    async fn test_node_package_update_version_preserves_formatting() {
246        let temp_dir = TempDir::new().unwrap();
247        let package_json = temp_dir.path().join("package.json");
248        fs::write(
249            &package_json,
250            r#"{
251  "name": "test-package",
252  "version": "1.2.3",
253  "description": "A test package",
254  "dependencies": {
255    "express": "^4.18.0"
256  }
257}
258"#,
259        )
260        .unwrap();
261
262        let mut package = NodePackage::new(
263            Some("test-package".to_string()),
264            Some("1.2.3".to_string()),
265            package_json.clone(),
266            PathBuf::from("package.json"),
267        );
268
269        package.update_version(UpdateType::Patch).await.unwrap();
270
271        let content = read_to_string(&package_json).await.unwrap();
272        assert!(content.contains(r#""version": "1.2.4""#));
273        assert!(content.contains(r#""name": "test-package""#));
274        assert!(content.contains(r#""description": "A test package""#));
275        assert!(content.contains(r#""dependencies""#));
276
277        temp_dir.close().unwrap();
278    }
279
280    #[tokio::test]
281    async fn test_node_package_update_version_preserves_newline() {
282        let temp_dir = TempDir::new().unwrap();
283        let package_json = temp_dir.path().join("package.json");
284        fs::write(
285            &package_json,
286            r#"{"name":"test-package","version":"1.0.0"}
287"#,
288        )
289        .unwrap();
290
291        let mut package = NodePackage::new(
292            Some("test-package".to_string()),
293            Some("1.0.0".to_string()),
294            package_json.clone(),
295            PathBuf::from("package.json"),
296        );
297
298        package.update_version(UpdateType::Patch).await.unwrap();
299
300        let content = read_to_string(&package_json).await.unwrap();
301        assert!(content.ends_with('\n'));
302        assert!(content.contains(r#""version": "1.0.1""#));
303
304        temp_dir.close().unwrap();
305    }
306}