socket_patch_core/package_json/
detect.rs1#[derive(Debug, Clone, Copy, PartialEq)]
3pub enum PackageManager {
4 Npm,
5 Pnpm,
6}
7
8fn socket_patch_command(pm: PackageManager) -> &'static str {
10 match pm {
11 PackageManager::Npm => "npx @socketsecurity/socket-patch apply --silent --ecosystems npm",
12 PackageManager::Pnpm => {
13 "pnpm dlx @socketsecurity/socket-patch apply --silent --ecosystems npm"
14 }
15 }
16}
17
18const LEGACY_PATCH_PATTERNS: &[&str] = &[
20 "socket-patch apply",
21 "npx @socketsecurity/socket-patch apply",
22 "socket patch apply",
23];
24
25fn script_is_configured(script: &str) -> bool {
27 LEGACY_PATCH_PATTERNS
28 .iter()
29 .any(|pattern| script.contains(pattern))
30}
31
32#[derive(Debug, Clone)]
34pub struct ScriptSetupStatus {
35 pub postinstall_configured: bool,
36 pub postinstall_script: String,
37 pub dependencies_configured: bool,
38 pub dependencies_script: String,
39 pub needs_update: bool,
40}
41
42pub fn is_setup_configured(package_json: &serde_json::Value) -> ScriptSetupStatus {
45 let scripts = package_json.get("scripts");
46
47 let postinstall_script = scripts
48 .and_then(|s| s.get("postinstall"))
49 .and_then(|v| v.as_str())
50 .unwrap_or("")
51 .to_string();
52 let postinstall_configured = script_is_configured(&postinstall_script);
53
54 let dependencies_script = scripts
55 .and_then(|s| s.get("dependencies"))
56 .and_then(|v| v.as_str())
57 .unwrap_or("")
58 .to_string();
59 let dependencies_configured = script_is_configured(&dependencies_script);
60
61 ScriptSetupStatus {
62 postinstall_configured,
63 postinstall_script,
64 dependencies_configured,
65 dependencies_script,
66 needs_update: !postinstall_configured || !dependencies_configured,
67 }
68}
69
70pub fn is_setup_configured_str(content: &str) -> ScriptSetupStatus {
72 match serde_json::from_str::<serde_json::Value>(content) {
73 Ok(val) => is_setup_configured(&val),
74 Err(_) => ScriptSetupStatus {
75 postinstall_configured: false,
76 postinstall_script: String::new(),
77 dependencies_configured: false,
78 dependencies_script: String::new(),
79 needs_update: true,
80 },
81 }
82}
83
84pub fn generate_updated_script(current_script: &str, pm: PackageManager) -> String {
87 let command = socket_patch_command(pm);
88 let trimmed = current_script.trim();
89
90 if trimmed.is_empty() {
92 return command.to_string();
93 }
94
95 if script_is_configured(trimmed) {
97 return trimmed.to_string();
98 }
99
100 format!("{command} && {trimmed}")
102}
103
104pub fn update_package_json_object(
108 package_json: &mut serde_json::Value,
109 pm: PackageManager,
110) -> (bool, String, String) {
111 let status = is_setup_configured(package_json);
112
113 if !status.needs_update {
114 return (
115 false,
116 status.postinstall_script,
117 status.dependencies_script,
118 );
119 }
120
121 if package_json.get("scripts").is_none() {
123 package_json["scripts"] = serde_json::json!({});
124 }
125
126 let mut modified = false;
127
128 let new_postinstall = if !status.postinstall_configured {
129 modified = true;
130 let s = generate_updated_script(&status.postinstall_script, pm);
131 package_json["scripts"]["postinstall"] = serde_json::Value::String(s.clone());
132 s
133 } else {
134 status.postinstall_script
135 };
136
137 let new_dependencies = if !status.dependencies_configured {
138 modified = true;
139 let s = generate_updated_script(&status.dependencies_script, pm);
140 package_json["scripts"]["dependencies"] = serde_json::Value::String(s.clone());
141 s
142 } else {
143 status.dependencies_script
144 };
145
146 (modified, new_postinstall, new_dependencies)
147}
148
149pub fn update_package_json_content(
153 content: &str,
154 pm: PackageManager,
155) -> Result<(bool, String, String, String, String, String), String> {
156 let mut package_json: serde_json::Value =
157 serde_json::from_str(content).map_err(|e| format!("Invalid package.json: {e}"))?;
158
159 let status = is_setup_configured(&package_json);
160
161 if !status.needs_update {
162 return Ok((
163 false,
164 content.to_string(),
165 status.postinstall_script.clone(),
166 status.postinstall_script,
167 status.dependencies_script.clone(),
168 status.dependencies_script,
169 ));
170 }
171
172 let old_postinstall = status.postinstall_script.clone();
173 let old_dependencies = status.dependencies_script.clone();
174
175 let (_, new_postinstall, new_dependencies) =
176 update_package_json_object(&mut package_json, pm);
177 let new_content = serde_json::to_string_pretty(&package_json).unwrap() + "\n";
178
179 Ok((
180 true,
181 new_content,
182 old_postinstall,
183 new_postinstall,
184 old_dependencies,
185 new_dependencies,
186 ))
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192
193 #[test]
196 fn test_not_configured() {
197 let pkg: serde_json::Value = serde_json::json!({
198 "name": "test",
199 "scripts": {
200 "build": "tsc"
201 }
202 });
203 let status = is_setup_configured(&pkg);
204 assert!(!status.postinstall_configured);
205 assert!(!status.dependencies_configured);
206 assert!(status.needs_update);
207 }
208
209 #[test]
210 fn test_postinstall_configured_dependencies_not() {
211 let pkg: serde_json::Value = serde_json::json!({
212 "name": "test",
213 "scripts": {
214 "postinstall": "npx @socketsecurity/socket-patch apply --silent --ecosystems npm"
215 }
216 });
217 let status = is_setup_configured(&pkg);
218 assert!(status.postinstall_configured);
219 assert!(!status.dependencies_configured);
220 assert!(status.needs_update);
221 }
222
223 #[test]
224 fn test_both_configured() {
225 let pkg: serde_json::Value = serde_json::json!({
226 "name": "test",
227 "scripts": {
228 "postinstall": "npx @socketsecurity/socket-patch apply --silent --ecosystems npm",
229 "dependencies": "npx @socketsecurity/socket-patch apply --silent --ecosystems npm"
230 }
231 });
232 let status = is_setup_configured(&pkg);
233 assert!(status.postinstall_configured);
234 assert!(status.dependencies_configured);
235 assert!(!status.needs_update);
236 }
237
238 #[test]
239 fn test_legacy_socket_patch_apply_recognized() {
240 let pkg: serde_json::Value = serde_json::json!({
241 "scripts": {
242 "postinstall": "socket patch apply --silent --ecosystems npm",
243 "dependencies": "socket-patch apply"
244 }
245 });
246 let status = is_setup_configured(&pkg);
247 assert!(status.postinstall_configured);
248 assert!(status.dependencies_configured);
249 assert!(!status.needs_update);
250 }
251
252 #[test]
253 fn test_no_scripts() {
254 let pkg: serde_json::Value = serde_json::json!({"name": "test"});
255 let status = is_setup_configured(&pkg);
256 assert!(!status.postinstall_configured);
257 assert!(status.postinstall_script.is_empty());
258 assert!(!status.dependencies_configured);
259 assert!(status.dependencies_script.is_empty());
260 }
261
262 #[test]
263 fn test_no_postinstall() {
264 let pkg: serde_json::Value = serde_json::json!({
265 "scripts": {"build": "tsc"}
266 });
267 let status = is_setup_configured(&pkg);
268 assert!(!status.postinstall_configured);
269 assert!(status.postinstall_script.is_empty());
270 }
271
272 #[test]
275 fn test_configured_str_invalid_json() {
276 let status = is_setup_configured_str("not json");
277 assert!(!status.postinstall_configured);
278 assert!(status.needs_update);
279 }
280
281 #[test]
282 fn test_configured_str_legacy_npx_pattern() {
283 let content = r#"{"scripts":{"postinstall":"npx @socketsecurity/socket-patch apply --silent"}}"#;
284 let status = is_setup_configured_str(content);
285 assert!(status.postinstall_configured);
286 }
287
288 #[test]
289 fn test_configured_str_socket_dash_patch() {
290 let content =
291 r#"{"scripts":{"postinstall":"socket-patch apply --silent --ecosystems npm"}}"#;
292 let status = is_setup_configured_str(content);
293 assert!(status.postinstall_configured);
294 }
295
296 #[test]
297 fn test_configured_str_pnpm_dlx_pattern() {
298 let content = r#"{"scripts":{"postinstall":"pnpm dlx @socketsecurity/socket-patch apply --silent --ecosystems npm"}}"#;
299 let status = is_setup_configured_str(content);
300 assert!(status.postinstall_configured);
302 }
303
304 #[test]
307 fn test_generate_empty_npm() {
308 assert_eq!(
309 generate_updated_script("", PackageManager::Npm),
310 "npx @socketsecurity/socket-patch apply --silent --ecosystems npm"
311 );
312 }
313
314 #[test]
315 fn test_generate_empty_pnpm() {
316 assert_eq!(
317 generate_updated_script("", PackageManager::Pnpm),
318 "pnpm dlx @socketsecurity/socket-patch apply --silent --ecosystems npm"
319 );
320 }
321
322 #[test]
323 fn test_generate_prepend_npm() {
324 assert_eq!(
325 generate_updated_script("echo done", PackageManager::Npm),
326 "npx @socketsecurity/socket-patch apply --silent --ecosystems npm && echo done"
327 );
328 }
329
330 #[test]
331 fn test_generate_prepend_pnpm() {
332 assert_eq!(
333 generate_updated_script("echo done", PackageManager::Pnpm),
334 "pnpm dlx @socketsecurity/socket-patch apply --silent --ecosystems npm && echo done"
335 );
336 }
337
338 #[test]
339 fn test_generate_already_configured() {
340 let current = "socket-patch apply && echo done";
341 assert_eq!(
342 generate_updated_script(current, PackageManager::Npm),
343 current
344 );
345 }
346
347 #[test]
348 fn test_generate_whitespace_only() {
349 let result = generate_updated_script(" \t ", PackageManager::Npm);
350 assert_eq!(
351 result,
352 "npx @socketsecurity/socket-patch apply --silent --ecosystems npm"
353 );
354 }
355
356 #[test]
359 fn test_update_object_creates_scripts() {
360 let mut pkg: serde_json::Value = serde_json::json!({"name": "test"});
361 let (modified, new_postinstall, new_dependencies) =
362 update_package_json_object(&mut pkg, PackageManager::Npm);
363 assert!(modified);
364 assert!(new_postinstall.contains("npx @socketsecurity/socket-patch apply"));
365 assert!(new_dependencies.contains("npx @socketsecurity/socket-patch apply"));
366 assert!(pkg.get("scripts").is_some());
367 assert!(pkg["scripts"]["postinstall"].is_string());
368 assert!(pkg["scripts"]["dependencies"].is_string());
369 }
370
371 #[test]
372 fn test_update_object_creates_scripts_pnpm() {
373 let mut pkg: serde_json::Value = serde_json::json!({"name": "test"});
374 let (modified, new_postinstall, new_dependencies) =
375 update_package_json_object(&mut pkg, PackageManager::Pnpm);
376 assert!(modified);
377 assert!(new_postinstall.contains("pnpm dlx @socketsecurity/socket-patch apply"));
378 assert!(new_dependencies.contains("pnpm dlx @socketsecurity/socket-patch apply"));
379 }
380
381 #[test]
382 fn test_update_object_noop_when_both_configured() {
383 let mut pkg: serde_json::Value = serde_json::json!({
384 "scripts": {
385 "postinstall": "npx @socketsecurity/socket-patch apply --silent --ecosystems npm",
386 "dependencies": "npx @socketsecurity/socket-patch apply --silent --ecosystems npm"
387 }
388 });
389 let (modified, _, _) = update_package_json_object(&mut pkg, PackageManager::Npm);
390 assert!(!modified);
391 }
392
393 #[test]
394 fn test_update_object_adds_dependencies_when_postinstall_exists() {
395 let mut pkg: serde_json::Value = serde_json::json!({
396 "scripts": {
397 "postinstall": "npx @socketsecurity/socket-patch apply --silent --ecosystems npm"
398 }
399 });
400 let (modified, _, new_dependencies) =
401 update_package_json_object(&mut pkg, PackageManager::Npm);
402 assert!(modified);
403 assert!(new_dependencies.contains("npx @socketsecurity/socket-patch apply"));
404 assert_eq!(
406 pkg["scripts"]["postinstall"].as_str().unwrap(),
407 "npx @socketsecurity/socket-patch apply --silent --ecosystems npm"
408 );
409 }
410
411 #[test]
414 fn test_update_content_roundtrip_no_scripts() {
415 let content = r#"{"name": "test"}"#;
416 let (modified, new_content, old_pi, new_pi, old_dep, new_dep) =
417 update_package_json_content(content, PackageManager::Npm).unwrap();
418 assert!(modified);
419 assert!(old_pi.is_empty());
420 assert!(new_pi.contains("npx @socketsecurity/socket-patch apply"));
421 assert!(old_dep.is_empty());
422 assert!(new_dep.contains("npx @socketsecurity/socket-patch apply"));
423 let parsed: serde_json::Value = serde_json::from_str(&new_content).unwrap();
424 assert!(parsed["scripts"]["postinstall"].is_string());
425 assert!(parsed["scripts"]["dependencies"].is_string());
426 }
427
428 #[test]
429 fn test_update_content_already_configured() {
430 let content = r#"{"scripts":{"postinstall":"socket patch apply --silent --ecosystems npm","dependencies":"socket patch apply --silent --ecosystems npm"}}"#;
431 let (modified, _, _, _, _, _) =
432 update_package_json_content(content, PackageManager::Npm).unwrap();
433 assert!(!modified);
434 }
435
436 #[test]
437 fn test_update_content_invalid_json() {
438 let result = update_package_json_content("not json", PackageManager::Npm);
439 assert!(result.is_err());
440 assert!(result.unwrap_err().contains("Invalid package.json"));
441 }
442
443 #[test]
444 fn test_update_content_pnpm() {
445 let content = r#"{"name": "test"}"#;
446 let (modified, new_content, _, new_pi, _, new_dep) =
447 update_package_json_content(content, PackageManager::Pnpm).unwrap();
448 assert!(modified);
449 assert!(new_pi.contains("pnpm dlx @socketsecurity/socket-patch apply"));
450 assert!(new_dep.contains("pnpm dlx @socketsecurity/socket-patch apply"));
451 let parsed: serde_json::Value = serde_json::from_str(&new_content).unwrap();
452 assert!(parsed["scripts"]["postinstall"]
453 .as_str()
454 .unwrap()
455 .contains("pnpm dlx"));
456 assert!(parsed["scripts"]["dependencies"]
457 .as_str()
458 .unwrap()
459 .contains("pnpm dlx"));
460 }
461}