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