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::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}