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