Skip to content

Commit 82aef34

Browse files
Wenxin-Jiangclaude
andcommitted
fix: only add postinstall to root package.json for pnpm monorepos
pnpm runs root lifecycle scripts on `pnpm install`, so adding postinstall scripts to individual workspace package.json files is unnecessary. Worse, it breaks because workspaces may not have `@socketsecurity/socket-patch` as a dependency, and pnpm's strict module isolation prevents `npx` from resolving it. The setup command now detects pnpm monorepos (via pnpm-workspace.yaml) and only updates the root package.json. Also fixes detection order: pnpm-workspace.yaml is now checked before the "workspaces" field in package.json, since pnpm projects often have both (e.g. for compatibility), and pnpm-workspace.yaml is the definitive signal. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 9e676fb commit 82aef34

File tree

2 files changed

+90
-37
lines changed
  • crates
    • socket-patch-cli/src/commands
    • socket-patch-core/src/package_json

2 files changed

+90
-37
lines changed

crates/socket-patch-cli/src/commands/setup.rs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use clap::Args;
2-
use socket_patch_core::package_json::find::find_package_json_files;
2+
use socket_patch_core::package_json::find::{find_package_json_files, WorkspaceType};
33
use socket_patch_core::package_json::update::{update_package_json, UpdateStatus};
44
use std::io::{self, Write};
55
use std::path::{Path, PathBuf};
@@ -30,7 +30,22 @@ pub async fn run(args: SetupArgs) -> i32 {
3030
println!("Searching for package.json files...");
3131
}
3232

33-
let package_json_files = find_package_json_files(&args.cwd).await;
33+
let find_result = find_package_json_files(&args.cwd).await;
34+
35+
// For pnpm monorepos, only update root package.json.
36+
// pnpm runs root postinstall on `pnpm install`, so workspace-level
37+
// postinstall scripts are unnecessary. Individual workspaces may not
38+
// have `@socketsecurity/socket-patch` as a dependency, causing
39+
// `npx @socketsecurity/socket-patch apply` to fail due to pnpm's
40+
// strict module isolation.
41+
let package_json_files = match find_result.workspace_type {
42+
WorkspaceType::Pnpm => find_result
43+
.files
44+
.into_iter()
45+
.filter(|loc| loc.is_root)
46+
.collect(),
47+
_ => find_result.files,
48+
};
3449

3550
if package_json_files.is_empty() {
3651
if args.json {

crates/socket-patch-core/src/package_json/find.rs

Lines changed: 73 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,17 @@ pub struct PackageJsonLocation {
2525
pub workspace_pattern: Option<String>,
2626
}
2727

28+
/// Result of finding package.json files.
29+
#[derive(Debug)]
30+
pub struct PackageJsonFindResult {
31+
pub files: Vec<PackageJsonLocation>,
32+
pub workspace_type: WorkspaceType,
33+
}
34+
2835
/// Find all package.json files, respecting workspace configurations.
2936
pub async fn find_package_json_files(
3037
start_path: &Path,
31-
) -> Vec<PackageJsonLocation> {
38+
) -> PackageJsonFindResult {
3239
let mut results = Vec::new();
3340
let root_package_json = start_path.join("package.json");
3441

@@ -63,7 +70,10 @@ pub async fn find_package_json_files(
6370
}
6471
}
6572

66-
results
73+
PackageJsonFindResult {
74+
files: results,
75+
workspace_type: workspace_config.ws_type,
76+
}
6777
}
6878

6979
/// Detect workspace configuration from package.json.
@@ -83,6 +93,19 @@ pub async fn detect_workspaces(package_json_path: &Path) -> WorkspaceConfig {
8393
Err(_) => return default,
8494
};
8595

96+
// Check for pnpm workspaces first — pnpm projects may also have
97+
// "workspaces" in package.json for compatibility, but
98+
// pnpm-workspace.yaml is the definitive signal.
99+
let dir = package_json_path.parent().unwrap_or(Path::new("."));
100+
let pnpm_workspace = dir.join("pnpm-workspace.yaml");
101+
if let Ok(yaml_content) = fs::read_to_string(&pnpm_workspace).await {
102+
let patterns = parse_pnpm_workspace_patterns(&yaml_content);
103+
return WorkspaceConfig {
104+
ws_type: WorkspaceType::Pnpm,
105+
patterns,
106+
};
107+
}
108+
86109
// Check for npm/yarn workspaces
87110
if let Some(workspaces) = pkg.get("workspaces") {
88111
let patterns = if let Some(arr) = workspaces.as_array() {
@@ -108,17 +131,6 @@ pub async fn detect_workspaces(package_json_path: &Path) -> WorkspaceConfig {
108131
};
109132
}
110133

111-
// Check for pnpm workspaces
112-
let dir = package_json_path.parent().unwrap_or(Path::new("."));
113-
let pnpm_workspace = dir.join("pnpm-workspace.yaml");
114-
if let Ok(yaml_content) = fs::read_to_string(&pnpm_workspace).await {
115-
let patterns = parse_pnpm_workspace_patterns(&yaml_content);
116-
return WorkspaceConfig {
117-
ws_type: WorkspaceType::Pnpm,
118-
patterns,
119-
};
120-
}
121-
122134
default
123135
}
124136

@@ -440,6 +452,28 @@ mod tests {
440452
assert_eq!(config.patterns, vec!["packages/*"]);
441453
}
442454

455+
#[tokio::test]
456+
async fn test_detect_workspaces_pnpm_with_workspaces_field() {
457+
// When both pnpm-workspace.yaml AND "workspaces" in package.json
458+
// exist, pnpm should take priority (e.g. depscan repo)
459+
let dir = tempfile::tempdir().unwrap();
460+
let pkg = dir.path().join("package.json");
461+
fs::write(
462+
&pkg,
463+
r#"{"name": "root", "workspaces": ["packages/*"]}"#,
464+
)
465+
.await
466+
.unwrap();
467+
let pnpm = dir.path().join("pnpm-workspace.yaml");
468+
fs::write(&pnpm, "packages:\n - workspaces/*")
469+
.await
470+
.unwrap();
471+
let config = detect_workspaces(&pkg).await;
472+
assert!(matches!(config.ws_type, WorkspaceType::Pnpm));
473+
// Should use pnpm-workspace.yaml patterns, not package.json workspaces
474+
assert_eq!(config.patterns, vec!["workspaces/*"]);
475+
}
476+
443477
#[tokio::test]
444478
async fn test_detect_workspaces_none() {
445479
let dir = tempfile::tempdir().unwrap();
@@ -470,8 +504,8 @@ mod tests {
470504
#[tokio::test]
471505
async fn test_find_no_root_package_json() {
472506
let dir = tempfile::tempdir().unwrap();
473-
let results = find_package_json_files(dir.path()).await;
474-
assert!(results.is_empty());
507+
let result = find_package_json_files(dir.path()).await;
508+
assert!(result.files.is_empty());
475509
}
476510

477511
#[tokio::test]
@@ -480,9 +514,9 @@ mod tests {
480514
fs::write(dir.path().join("package.json"), r#"{"name":"root"}"#)
481515
.await
482516
.unwrap();
483-
let results = find_package_json_files(dir.path()).await;
484-
assert_eq!(results.len(), 1);
485-
assert!(results[0].is_root);
517+
let result = find_package_json_files(dir.path()).await;
518+
assert_eq!(result.files.len(), 1);
519+
assert!(result.files[0].is_root);
486520
}
487521

488522
#[tokio::test]
@@ -499,11 +533,12 @@ mod tests {
499533
fs::write(pkg_a.join("package.json"), r#"{"name":"a"}"#)
500534
.await
501535
.unwrap();
502-
let results = find_package_json_files(dir.path()).await;
536+
let result = find_package_json_files(dir.path()).await;
537+
assert!(matches!(result.workspace_type, WorkspaceType::Npm));
503538
// root + workspace member
504-
assert_eq!(results.len(), 2);
505-
assert!(results[0].is_root);
506-
assert!(results[1].is_workspace);
539+
assert_eq!(result.files.len(), 2);
540+
assert!(result.files[0].is_root);
541+
assert!(result.files[1].is_workspace);
507542
}
508543

509544
#[tokio::test]
@@ -523,10 +558,13 @@ mod tests {
523558
fs::write(pkg_a.join("package.json"), r#"{"name":"a"}"#)
524559
.await
525560
.unwrap();
526-
let results = find_package_json_files(dir.path()).await;
527-
assert_eq!(results.len(), 2);
528-
assert!(results[0].is_root);
529-
assert!(results[1].is_workspace);
561+
let result = find_package_json_files(dir.path()).await;
562+
assert!(matches!(result.workspace_type, WorkspaceType::Pnpm));
563+
// find_package_json_files still returns all files;
564+
// filtering for pnpm is done by the caller (setup command)
565+
assert_eq!(result.files.len(), 2);
566+
assert!(result.files[0].is_root);
567+
assert!(result.files[1].is_workspace);
530568
}
531569

532570
#[tokio::test]
@@ -540,10 +578,10 @@ mod tests {
540578
fs::write(nm.join("package.json"), r#"{"name":"lodash"}"#)
541579
.await
542580
.unwrap();
543-
let results = find_package_json_files(dir.path()).await;
581+
let result = find_package_json_files(dir.path()).await;
544582
// Only root, node_modules should be skipped
545-
assert_eq!(results.len(), 1);
546-
assert!(results[0].is_root);
583+
assert_eq!(result.files.len(), 1);
584+
assert!(result.files[0].is_root);
547585
}
548586

549587
#[tokio::test]
@@ -561,9 +599,9 @@ mod tests {
561599
fs::write(deep.join("package.json"), r#"{"name":"deep"}"#)
562600
.await
563601
.unwrap();
564-
let results = find_package_json_files(dir.path()).await;
602+
let result = find_package_json_files(dir.path()).await;
565603
// Only root (the deep one exceeds depth limit)
566-
assert_eq!(results.len(), 1);
604+
assert_eq!(result.files.len(), 1);
567605
}
568606

569607
#[tokio::test]
@@ -580,9 +618,9 @@ mod tests {
580618
fs::write(nested.join("package.json"), r#"{"name":"client"}"#)
581619
.await
582620
.unwrap();
583-
let results = find_package_json_files(dir.path()).await;
621+
let result = find_package_json_files(dir.path()).await;
584622
// root + recursively found workspace member
585-
assert!(results.len() >= 2);
623+
assert!(result.files.len() >= 2);
586624
}
587625

588626
#[tokio::test]
@@ -599,7 +637,7 @@ mod tests {
599637
fs::write(core.join("package.json"), r#"{"name":"core"}"#)
600638
.await
601639
.unwrap();
602-
let results = find_package_json_files(dir.path()).await;
603-
assert_eq!(results.len(), 2);
640+
let result = find_package_json_files(dir.path()).await;
641+
assert_eq!(result.files.len(), 2);
604642
}
605643
}

0 commit comments

Comments
 (0)