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: 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}