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