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