changepacks_node/
package.rs1use 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}