@@ -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.
2936pub 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
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