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