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
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        // Initially empty
336        assert!(workspace.dependencies().is_empty());
337
338        // Add dependencies
339        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        // Adding duplicate should not increase count
348        workspace.add_dependency("core");
349        assert_eq!(workspace.dependencies().len(), 2);
350    }
351}