@@ -33,17 +33,41 @@ struct PnpmWorkspace {
3333 packages : Vec < Str > ,
3434}
3535
36- /// The workspace configuration for npm/yarn.
36+ /// The `workspaces` field in package.json can be either an array of glob patterns
37+ /// or an object with a `packages` field (used by Bun catalogs and Yarn classic nohoist).
38+ ///
39+ /// Array form: `"workspaces": ["packages/*", "apps/*"]`
40+ /// Object form: `"workspaces": {"packages": ["packages/*", "apps/*"], "catalog": {...}}`
41+ ///
42+ /// Bun: <https://bun.sh/docs/install/workspaces>
43+ /// Yarn classic: <https://classic.yarnpkg.com/en/docs/workspaces/>
44+ #[ derive( Debug , Deserialize ) ]
45+ #[ serde( untagged) ]
46+ enum NpmWorkspaces {
47+ /// Array of glob patterns (npm, yarn, bun).
48+ Array ( Vec < Str > ) ,
49+ /// Object form with a `packages` field (Bun catalogs, Yarn classic nohoist).
50+ Object { packages : Vec < Str > } ,
51+ }
52+
53+ impl NpmWorkspaces {
54+ fn into_packages ( self ) -> Vec < Str > {
55+ match self {
56+ Self :: Array ( packages) | Self :: Object { packages } => packages,
57+ }
58+ }
59+ }
60+
61+ /// The workspace configuration for npm/yarn/bun.
3762///
3863/// npm: <https://docs.npmjs.com/cli/v11/using-npm/workspaces>
3964/// yarn: <https://yarnpkg.com/features/workspaces>
65+ /// bun: <https://bun.sh/docs/install/workspaces>
4066#[ derive( Debug , Deserialize ) ]
4167struct NpmWorkspace {
42- /// Array of folder glob patterns referencing the workspaces of the project.
43- ///
44- /// <https://docs.npmjs.com/cli/v11/configuring-npm/package-json#workspaces>
45- /// <https://yarnpkg.com/configuration/manifest#workspaces>
46- workspaces : Vec < Str > ,
68+ /// Glob patterns referencing the workspaces of the project.
69+ /// Accepts both array form and object form (with `packages` key).
70+ workspaces : NpmWorkspaces ,
4771}
4872
4973#[ derive( Debug ) ]
@@ -237,7 +261,7 @@ pub fn load_package_graph(
237261 file_path : Arc :: clone ( file_with_path. path ( ) ) ,
238262 serde_json_error : e,
239263 } ) ?;
240- workspace. workspaces
264+ workspace. workspaces . into_packages ( )
241265 }
242266 WorkspaceFile :: NonWorkspacePackage ( file_with_path) => {
243267 // For non-workspace packages, add the package.json to the graph as a root package
@@ -1096,4 +1120,115 @@ mod tests {
10961120 // External dependencies should not create edges
10971121 assert_eq ! ( graph. edge_count( ) , 1 , "Should only have one edge for workspace dependency" ) ;
10981122 }
1123+
1124+ #[ test]
1125+ fn test_get_package_graph_npm_workspace_object_form ( ) {
1126+ let temp_dir = TempDir :: new ( ) . unwrap ( ) ;
1127+ let temp_dir_path = AbsolutePath :: new ( temp_dir. path ( ) ) . unwrap ( ) ;
1128+
1129+ // Create package.json with object-form workspaces (Bun/Yarn classic style)
1130+ let root_package = serde_json:: json!( {
1131+ "name" : "bun-monorepo" ,
1132+ "private" : true ,
1133+ "workspaces" : {
1134+ "packages" : [ "packages/*" , "apps/*" ]
1135+ }
1136+ } ) ;
1137+ fs:: write ( temp_dir_path. join ( "package.json" ) , root_package. to_string ( ) ) . unwrap ( ) ;
1138+
1139+ // Create packages directory structure
1140+ fs:: create_dir_all ( temp_dir_path. join ( "packages" ) ) . unwrap ( ) ;
1141+ fs:: create_dir_all ( temp_dir_path. join ( "apps" ) ) . unwrap ( ) ;
1142+
1143+ // Create shared library package
1144+ fs:: create_dir_all ( temp_dir_path. join ( "packages/shared" ) ) . unwrap ( ) ;
1145+ let shared_pkg = serde_json:: json!( {
1146+ "name" : "@myorg/shared" ,
1147+ "version" : "1.0.0"
1148+ } ) ;
1149+ fs:: write ( temp_dir_path. join ( "packages/shared/package.json" ) , shared_pkg. to_string ( ) )
1150+ . unwrap ( ) ;
1151+
1152+ // Create app that depends on shared
1153+ fs:: create_dir_all ( temp_dir_path. join ( "apps/web" ) ) . unwrap ( ) ;
1154+ let web_app = serde_json:: json!( {
1155+ "name" : "web-app" ,
1156+ "version" : "0.1.0" ,
1157+ "dependencies" : {
1158+ "@myorg/shared" : "workspace:*"
1159+ }
1160+ } ) ;
1161+ fs:: write ( temp_dir_path. join ( "apps/web/package.json" ) , web_app. to_string ( ) ) . unwrap ( ) ;
1162+
1163+ let graph = discover_package_graph ( temp_dir_path) . unwrap ( ) ;
1164+
1165+ // Should have 3 nodes: root + shared + web-app
1166+ assert_eq ! ( graph. node_count( ) , 3 ) ;
1167+
1168+ // Verify packages were found
1169+ let mut packages_found = FxHashSet :: < Str > :: default ( ) ;
1170+ for node in graph. node_weights ( ) {
1171+ packages_found. insert ( node. package_json . name . clone ( ) ) ;
1172+ }
1173+ assert ! ( packages_found. contains( "bun-monorepo" ) ) ;
1174+ assert ! ( packages_found. contains( "@myorg/shared" ) ) ;
1175+ assert ! ( packages_found. contains( "web-app" ) ) ;
1176+
1177+ // Verify dependency edge
1178+ let mut found_web_to_shared = false ;
1179+ for edge_ref in graph. edge_references ( ) {
1180+ let source = & graph[ edge_ref. source ( ) ] ;
1181+ let target = & graph[ edge_ref. target ( ) ] ;
1182+ if source. package_json . name == "web-app" && target. package_json . name == "@myorg/shared"
1183+ {
1184+ found_web_to_shared = true ;
1185+ }
1186+ }
1187+ assert ! ( found_web_to_shared, "Web app should depend on shared" ) ;
1188+ }
1189+
1190+ #[ test]
1191+ fn test_get_package_graph_bun_workspace_with_catalog ( ) {
1192+ let temp_dir = TempDir :: new ( ) . unwrap ( ) ;
1193+ let temp_dir_path = AbsolutePath :: new ( temp_dir. path ( ) ) . unwrap ( ) ;
1194+
1195+ // Create package.json with Bun catalog in object-form workspaces
1196+ let root_package = serde_json:: json!( {
1197+ "name" : "bun-catalog-monorepo" ,
1198+ "private" : true ,
1199+ "workspaces" : {
1200+ "packages" : [ "packages/*" ] ,
1201+ "catalog" : {
1202+ "react" : "^19.0.0" ,
1203+ "vite" : "npm:@voidzero-dev/vite-plus-core@latest"
1204+ }
1205+ }
1206+ } ) ;
1207+ fs:: write ( temp_dir_path. join ( "package.json" ) , root_package. to_string ( ) ) . unwrap ( ) ;
1208+
1209+ // Create packages directory
1210+ fs:: create_dir_all ( temp_dir_path. join ( "packages" ) ) . unwrap ( ) ;
1211+
1212+ // Create a package
1213+ fs:: create_dir_all ( temp_dir_path. join ( "packages/app" ) ) . unwrap ( ) ;
1214+ let app_pkg = serde_json:: json!( {
1215+ "name" : "my-app" ,
1216+ "dependencies" : {
1217+ "react" : "catalog:"
1218+ }
1219+ } ) ;
1220+ fs:: write ( temp_dir_path. join ( "packages/app/package.json" ) , app_pkg. to_string ( ) ) . unwrap ( ) ;
1221+
1222+ let graph = discover_package_graph ( temp_dir_path) . unwrap ( ) ;
1223+
1224+ // Should have 2 nodes: root + app (catalog field is silently ignored)
1225+ assert_eq ! ( graph. node_count( ) , 2 ) ;
1226+
1227+ let mut packages_found = FxHashSet :: < Str > :: default ( ) ;
1228+ for node in graph. node_weights ( ) {
1229+ packages_found. insert ( node. package_json . name . clone ( ) ) ;
1230+ }
1231+ assert ! ( packages_found. contains( "bun-catalog-monorepo" ) ) ;
1232+ assert ! ( packages_found. contains( "my-app" ) ) ;
1233+ }
10991234}
0 commit comments