Skip to main content

socket_patch_core/package_json/
update.rs

1use std::path::Path;
2use tokio::fs;
3
4use super::detect::{is_postinstall_configured_str, update_package_json_content};
5
6/// Result of updating a single package.json.
7#[derive(Debug, Clone)]
8pub struct UpdateResult {
9    pub path: String,
10    pub status: UpdateStatus,
11    pub old_script: String,
12    pub new_script: String,
13    pub error: Option<String>,
14}
15
16#[derive(Debug, Clone, PartialEq)]
17pub enum UpdateStatus {
18    Updated,
19    AlreadyConfigured,
20    Error,
21}
22
23/// Update a single package.json file with socket-patch postinstall script.
24pub async fn update_package_json(
25    package_json_path: &Path,
26    dry_run: bool,
27) -> UpdateResult {
28    let path_str = package_json_path.display().to_string();
29
30    let content = match fs::read_to_string(package_json_path).await {
31        Ok(c) => c,
32        Err(e) => {
33            return UpdateResult {
34                path: path_str,
35                status: UpdateStatus::Error,
36                old_script: String::new(),
37                new_script: String::new(),
38                error: Some(e.to_string()),
39            };
40        }
41    };
42
43    let status = is_postinstall_configured_str(&content);
44    if !status.needs_update {
45        return UpdateResult {
46            path: path_str,
47            status: UpdateStatus::AlreadyConfigured,
48            old_script: status.current_script.clone(),
49            new_script: status.current_script,
50            error: None,
51        };
52    }
53
54    match update_package_json_content(&content) {
55        Ok((modified, new_content, old_script, new_script)) => {
56            if !modified {
57                return UpdateResult {
58                    path: path_str,
59                    status: UpdateStatus::AlreadyConfigured,
60                    old_script,
61                    new_script,
62                    error: None,
63                };
64            }
65
66            if !dry_run {
67                if let Err(e) = fs::write(package_json_path, &new_content).await {
68                    return UpdateResult {
69                        path: path_str,
70                        status: UpdateStatus::Error,
71                        old_script,
72                        new_script,
73                        error: Some(e.to_string()),
74                    };
75                }
76            }
77
78            UpdateResult {
79                path: path_str,
80                status: UpdateStatus::Updated,
81                old_script,
82                new_script,
83                error: None,
84            }
85        }
86        Err(e) => UpdateResult {
87            path: path_str,
88            status: UpdateStatus::Error,
89            old_script: String::new(),
90            new_script: String::new(),
91            error: Some(e),
92        },
93    }
94}
95
96/// Update multiple package.json files.
97pub async fn update_multiple_package_jsons(
98    paths: &[&Path],
99    dry_run: bool,
100) -> Vec<UpdateResult> {
101    let mut results = Vec::new();
102    for path in paths {
103        let result = update_package_json(path, dry_run).await;
104        results.push(result);
105    }
106    results
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[tokio::test]
114    async fn test_update_file_not_found() {
115        let dir = tempfile::tempdir().unwrap();
116        let missing = dir.path().join("nonexistent.json");
117        let result = update_package_json(&missing, false).await;
118        assert_eq!(result.status, UpdateStatus::Error);
119        assert!(result.error.is_some());
120    }
121
122    #[tokio::test]
123    async fn test_update_already_configured() {
124        let dir = tempfile::tempdir().unwrap();
125        let pkg = dir.path().join("package.json");
126        fs::write(
127            &pkg,
128            r#"{"name":"test","scripts":{"postinstall":"socket patch apply --silent --ecosystems npm"}}"#,
129        )
130        .await
131        .unwrap();
132        let result = update_package_json(&pkg, false).await;
133        assert_eq!(result.status, UpdateStatus::AlreadyConfigured);
134    }
135
136    #[tokio::test]
137    async fn test_update_dry_run_does_not_write() {
138        let dir = tempfile::tempdir().unwrap();
139        let pkg = dir.path().join("package.json");
140        let original = r#"{"name":"test","scripts":{"build":"tsc"}}"#;
141        fs::write(&pkg, original).await.unwrap();
142        let result = update_package_json(&pkg, true).await;
143        assert_eq!(result.status, UpdateStatus::Updated);
144        // File should remain unchanged
145        let content = fs::read_to_string(&pkg).await.unwrap();
146        assert_eq!(content, original);
147    }
148
149    #[tokio::test]
150    async fn test_update_writes_file() {
151        let dir = tempfile::tempdir().unwrap();
152        let pkg = dir.path().join("package.json");
153        fs::write(&pkg, r#"{"name":"test","scripts":{"build":"tsc"}}"#)
154            .await
155            .unwrap();
156        let result = update_package_json(&pkg, false).await;
157        assert_eq!(result.status, UpdateStatus::Updated);
158        let content = fs::read_to_string(&pkg).await.unwrap();
159        assert!(content.contains("socket-patch apply"));
160    }
161
162    #[tokio::test]
163    async fn test_update_invalid_json() {
164        let dir = tempfile::tempdir().unwrap();
165        let pkg = dir.path().join("package.json");
166        fs::write(&pkg, "not json!!!").await.unwrap();
167        let result = update_package_json(&pkg, false).await;
168        assert_eq!(result.status, UpdateStatus::Error);
169        assert!(result.error.is_some());
170    }
171
172    #[tokio::test]
173    async fn test_update_no_scripts_key() {
174        let dir = tempfile::tempdir().unwrap();
175        let pkg = dir.path().join("package.json");
176        fs::write(&pkg, r#"{"name":"x"}"#).await.unwrap();
177        let result = update_package_json(&pkg, false).await;
178        assert_eq!(result.status, UpdateStatus::Updated);
179        let content = fs::read_to_string(&pkg).await.unwrap();
180        assert!(content.contains("postinstall"));
181        assert!(content.contains("socket-patch apply"));
182    }
183
184    #[tokio::test]
185    async fn test_update_multiple_mixed() {
186        let dir = tempfile::tempdir().unwrap();
187
188        let p1 = dir.path().join("a.json");
189        fs::write(&p1, r#"{"name":"a"}"#).await.unwrap();
190
191        let p2 = dir.path().join("b.json");
192        fs::write(
193            &p2,
194            r#"{"name":"b","scripts":{"postinstall":"socket patch apply --silent --ecosystems npm"}}"#,
195        )
196        .await
197        .unwrap();
198
199        let p3 = dir.path().join("c.json");
200        // Don't create p3 — file not found
201
202        let paths: Vec<&Path> = vec![p1.as_path(), p2.as_path(), p3.as_path()];
203        let results = update_multiple_package_jsons(&paths, false).await;
204        assert_eq!(results.len(), 3);
205        assert_eq!(results[0].status, UpdateStatus::Updated);
206        assert_eq!(results[1].status, UpdateStatus::AlreadyConfigured);
207        assert_eq!(results[2].status, UpdateStatus::Error);
208    }
209}