Skip to main content

changepacks_node/
workspace.rs

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