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