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::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 NodeWorkspace {
14 path: PathBuf,
15 relative_path: PathBuf,
16 version: Option<String>,
17 name: Option<String>,
18 is_changed: bool,
19 dependencies: HashSet<String>,
20}
21
22impl NodeWorkspace {
23 pub fn new(
24 name: Option<String>,
25 version: Option<String>,
26 path: PathBuf,
27 relative_path: PathBuf,
28 ) -> Self {
29 Self {
30 path,
31 relative_path,
32 name,
33 version,
34 is_changed: false,
35 dependencies: HashSet::new(),
36 }
37 }
38}
39
40#[async_trait]
41impl Workspace for NodeWorkspace {
42 fn name(&self) -> Option<&str> {
43 self.name.as_deref()
44 }
45
46 fn path(&self) -> &Path {
47 &self.path
48 }
49
50 fn version(&self) -> Option<&str> {
51 self.version.as_deref()
52 }
53
54 async fn update_version(&mut self, update_type: UpdateType) -> Result<()> {
55 let next_version = next_version(
56 self.version.as_ref().unwrap_or(&String::from("0.0.0")),
57 update_type,
58 )?;
59
60 let package_json_raw = read_to_string(Path::new(&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(next_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(next_version);
83 Ok(())
84 }
85
86 fn language(&self) -> Language {
87 Language::Node
88 }
89
90 fn is_changed(&self) -> bool {
91 self.is_changed
92 }
93
94 fn set_changed(&mut self, changed: bool) {
95 self.is_changed = changed;
96 }
97
98 fn relative_path(&self) -> &Path {
99 &self.relative_path
100 }
101
102 fn default_publish_command(&self) -> String {
103 detect_package_manager_recursive(&self.path)
104 .publish_command()
105 .to_string()
106 }
107
108 fn dependencies(&self) -> &HashSet<String> {
109 &self.dependencies
110 }
111
112 fn add_dependency(&mut self, dependency: &str) {
113 self.dependencies.insert(dependency.to_string());
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120 use changepacks_core::UpdateType;
121 use std::fs;
122 use tempfile::TempDir;
123 use tokio::fs::read_to_string;
124
125 #[tokio::test]
126 async fn test_node_workspace_new() {
127 let workspace = NodeWorkspace::new(
128 Some("test-workspace".to_string()),
129 Some("1.0.0".to_string()),
130 PathBuf::from("/test/package.json"),
131 PathBuf::from("test/package.json"),
132 );
133
134 assert_eq!(workspace.name(), Some("test-workspace"));
135 assert_eq!(workspace.version(), Some("1.0.0"));
136 assert_eq!(workspace.path(), PathBuf::from("/test/package.json"));
137 assert_eq!(
138 workspace.relative_path(),
139 PathBuf::from("test/package.json")
140 );
141 assert_eq!(workspace.language(), Language::Node);
142 assert!(!workspace.is_changed());
143 assert_eq!(workspace.default_publish_command(), "npm publish");
144 }
145
146 #[tokio::test]
147 async fn test_node_workspace_new_without_name_and_version() {
148 let workspace = NodeWorkspace::new(
149 None,
150 None,
151 PathBuf::from("/test/package.json"),
152 PathBuf::from("test/package.json"),
153 );
154
155 assert_eq!(workspace.name(), None);
156 assert_eq!(workspace.version(), None);
157 }
158
159 #[tokio::test]
160 async fn test_node_workspace_set_changed() {
161 let mut workspace = NodeWorkspace::new(
162 Some("test-workspace".to_string()),
163 Some("1.0.0".to_string()),
164 PathBuf::from("/test/package.json"),
165 PathBuf::from("test/package.json"),
166 );
167
168 assert!(!workspace.is_changed());
169 workspace.set_changed(true);
170 assert!(workspace.is_changed());
171 workspace.set_changed(false);
172 assert!(!workspace.is_changed());
173 }
174
175 #[tokio::test]
176 async fn test_node_workspace_update_version_with_existing_version() {
177 let temp_dir = TempDir::new().unwrap();
178 let package_json = temp_dir.path().join("package.json");
179 fs::write(
180 &package_json,
181 r#"{
182 "name": "test-workspace",
183 "version": "1.0.0",
184 "workspaces": ["packages/*"]
185}
186"#,
187 )
188 .unwrap();
189
190 let mut workspace = NodeWorkspace::new(
191 Some("test-workspace".to_string()),
192 Some("1.0.0".to_string()),
193 package_json.clone(),
194 PathBuf::from("package.json"),
195 );
196
197 workspace.update_version(UpdateType::Patch).await.unwrap();
198
199 let content = read_to_string(&package_json).await.unwrap();
200 assert!(content.contains(r#""version": "1.0.1""#));
201
202 temp_dir.close().unwrap();
203 }
204
205 #[tokio::test]
206 async fn test_node_workspace_update_version_without_version() {
207 let temp_dir = TempDir::new().unwrap();
208 let package_json = temp_dir.path().join("package.json");
209 fs::write(
210 &package_json,
211 r#"{
212 "name": "test-workspace",
213 "workspaces": ["packages/*"]
214}
215"#,
216 )
217 .unwrap();
218
219 let mut workspace = NodeWorkspace::new(
220 Some("test-workspace".to_string()),
221 None,
222 package_json.clone(),
223 PathBuf::from("package.json"),
224 );
225
226 workspace.update_version(UpdateType::Patch).await.unwrap();
227
228 let content = read_to_string(&package_json).await.unwrap();
229 assert!(content.contains(r#""version": "0.0.1""#));
230
231 temp_dir.close().unwrap();
232 }
233
234 #[tokio::test]
235 async fn test_node_workspace_update_version_minor() {
236 let temp_dir = TempDir::new().unwrap();
237 let package_json = temp_dir.path().join("package.json");
238 fs::write(
239 &package_json,
240 r#"{
241 "name": "test-workspace",
242 "version": "1.0.0",
243 "workspaces": ["packages/*"]
244}
245"#,
246 )
247 .unwrap();
248
249 let mut workspace = NodeWorkspace::new(
250 Some("test-workspace".to_string()),
251 Some("1.0.0".to_string()),
252 package_json.clone(),
253 PathBuf::from("package.json"),
254 );
255
256 workspace.update_version(UpdateType::Minor).await.unwrap();
257
258 let content = read_to_string(&package_json).await.unwrap();
259 assert!(content.contains(r#""version": "1.1.0""#));
260
261 temp_dir.close().unwrap();
262 }
263
264 #[tokio::test]
265 async fn test_node_workspace_update_version_major() {
266 let temp_dir = TempDir::new().unwrap();
267 let package_json = temp_dir.path().join("package.json");
268 fs::write(
269 &package_json,
270 r#"{
271 "name": "test-workspace",
272 "version": "1.0.0",
273 "workspaces": ["packages/*"]
274}
275"#,
276 )
277 .unwrap();
278
279 let mut workspace = NodeWorkspace::new(
280 Some("test-workspace".to_string()),
281 Some("1.0.0".to_string()),
282 package_json.clone(),
283 PathBuf::from("package.json"),
284 );
285
286 workspace.update_version(UpdateType::Major).await.unwrap();
287
288 let content = read_to_string(&package_json).await.unwrap();
289 assert!(content.contains(r#""version": "2.0.0""#));
290
291 temp_dir.close().unwrap();
292 }
293
294 #[tokio::test]
295 async fn test_node_workspace_update_version_preserves_formatting() {
296 let temp_dir = TempDir::new().unwrap();
297 let package_json = temp_dir.path().join("package.json");
298 fs::write(
299 &package_json,
300 r#"{
301 "name": "test-workspace",
302 "version": "1.0.0",
303 "workspaces": ["packages/*"],
304 "scripts": {
305 "test": "jest"
306 }
307}
308"#,
309 )
310 .unwrap();
311
312 let mut workspace = NodeWorkspace::new(
313 Some("test-workspace".to_string()),
314 Some("1.0.0".to_string()),
315 package_json.clone(),
316 PathBuf::from("package.json"),
317 );
318
319 workspace.update_version(UpdateType::Patch).await.unwrap();
320
321 let content = read_to_string(&package_json).await.unwrap();
322 assert!(content.contains(r#""version": "1.0.1""#));
323 assert!(content.contains(r#""name": "test-workspace""#));
324 assert!(content.contains(r#""workspaces""#));
325 assert!(content.contains(r#""scripts""#));
326
327 temp_dir.close().unwrap();
328 }
329
330 #[test]
331 fn test_node_workspace_dependencies() {
332 let mut workspace = NodeWorkspace::new(
333 Some("test-workspace".to_string()),
334 Some("1.0.0".to_string()),
335 PathBuf::from("/test/package.json"),
336 PathBuf::from("test/package.json"),
337 );
338
339 assert!(workspace.dependencies().is_empty());
341
342 workspace.add_dependency("core");
344 workspace.add_dependency("utils");
345
346 let deps = workspace.dependencies();
347 assert_eq!(deps.len(), 2);
348 assert!(deps.contains("core"));
349 assert!(deps.contains("utils"));
350
351 workspace.add_dependency("core");
353 assert_eq!(workspace.dependencies().len(), 2);
354 }
355}