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