@@ -35,12 +35,12 @@ pub struct DepsSyncConfig {
3535 /// Dependencies that should be pinned to a specific version across all
3636 /// packages by default. Packages can be excluded using the `exceptions`
3737 /// field.
38- #[ serde( default ) ]
38+ #[ serde( default , skip_serializing_if = "HashMap::is_empty" ) ]
3939 pub pinned_dependencies : HashMap < String , PinnedDependency > ,
4040 /// Dependencies that should be ignored in specific packages.
4141 /// The `exceptions` field lists the packages where the dependency should be
4242 /// ignored.
43- #[ serde( default ) ]
43+ #[ serde( default , skip_serializing_if = "HashMap::is_empty" ) ]
4444 pub ignored_dependencies : HashMap < String , IgnoredDependency > ,
4545}
4646
@@ -60,7 +60,7 @@ pub struct PinnedDependency {
6060#[ serde( rename_all = "camelCase" ) ]
6161pub struct IgnoredDependency {
6262 /// Packages where this dependency should be ignored
63- #[ serde( default ) ]
63+ #[ serde( default , skip_serializing_if = "Vec::is_empty" ) ]
6464 pub exceptions : Vec < String > ,
6565}
6666
@@ -113,12 +113,36 @@ struct DependencyConflict {
113113 conflict_reason : Option < String > ,
114114}
115115
116- pub async fn run ( base : & CommandBase ) -> Result < i32 , Error > {
116+ pub async fn run ( base : & CommandBase , allowlist : bool ) -> Result < i32 , Error > {
117117 let color_config = base. color_config ;
118118
119- println ! ( "🔍 Scanning workspace packages for dependency conflicts...\n " ) ;
119+ println ! ( "🔍 Scanning workspace packages for dependency conflicts..." ) ;
120120
121121 let config = load_deps_sync_config ( & base. repo_root ) . await ?;
122+
123+ // Print ignored dependencies count if any exist
124+ if !config. ignored_dependencies . is_empty ( ) {
125+ let ignored_count = config. ignored_dependencies . len ( ) ;
126+ let dependency_word = if ignored_count == 1 {
127+ "dependency"
128+ } else {
129+ "dependencies"
130+ } ;
131+ let message = format ! (
132+ "→ {} ignored {} in `turbo.json`" ,
133+ ignored_count, dependency_word
134+ ) ;
135+
136+ if color_config. should_strip_ansi {
137+ println ! ( "{}" , message) ;
138+ } else {
139+ use turborepo_ui:: GREY ;
140+ println ! ( "{}" , GREY . apply_to( & message) ) ;
141+ }
142+ }
143+
144+ println ! ( ) ;
145+
122146 let all_deps = collect_all_dependencies ( & base. repo_root ) . await ?;
123147 let conflicts = find_dependency_conflicts ( & all_deps, & config) ;
124148 let pinned_conflicts = find_pinned_version_conflicts ( & all_deps, & config) ;
@@ -128,6 +152,20 @@ pub async fn run(base: &CommandBase) -> Result<i32, Error> {
128152 if all_conflicts. is_empty ( ) {
129153 print_success ( "✅ All dependencies are in sync!" , color_config) ;
130154 Ok ( 0 )
155+ } else if allowlist {
156+ // Generate allowlist configuration instead of reporting conflicts
157+ let allowlist_config = generate_allowlist_config ( & all_conflicts, & config) ;
158+ write_allowlist_config ( & base. repo_root , & allowlist_config) . await ?;
159+
160+ print_success (
161+ & format ! (
162+ "✅ Generated allowlist configuration for {} conflicts in turbo.json. \
163+ Dependencies are now synchronized!",
164+ all_conflicts. len( )
165+ ) ,
166+ color_config,
167+ ) ;
168+ Ok ( 0 )
131169 } else {
132170 print_conflicts ( & all_conflicts, color_config) ;
133171 Ok ( 1 )
@@ -489,6 +527,99 @@ fn print_success(message: &str, color_config: ColorConfig) {
489527 }
490528}
491529
530+ fn generate_allowlist_config (
531+ conflicts : & [ DependencyConflict ] ,
532+ current_config : & DepsSyncConfig ,
533+ ) -> DepsSyncConfig {
534+ let mut new_config = DepsSyncConfig {
535+ pinned_dependencies : HashMap :: new ( ) ,
536+ ignored_dependencies : HashMap :: new ( ) ,
537+ } ;
538+
539+ // Only copy existing pinned dependencies that are being modified
540+ for conflict in conflicts {
541+ if conflict. conflict_reason . is_some ( ) {
542+ // This is a pinned dependency conflict
543+ // Copy the existing pinned dependency and add exceptions
544+ if let Some ( existing_pinned_dep) = current_config
545+ . pinned_dependencies
546+ . get ( & conflict. dependency_name )
547+ {
548+ let mut pinned_dep = existing_pinned_dep. clone ( ) ;
549+ for usage in & conflict. conflicting_packages {
550+ if !pinned_dep. exceptions . contains ( & usage. package_name ) {
551+ pinned_dep. exceptions . push ( usage. package_name . clone ( ) ) ;
552+ }
553+ }
554+ new_config
555+ . pinned_dependencies
556+ . insert ( conflict. dependency_name . clone ( ) , pinned_dep) ;
557+ }
558+ } else {
559+ // This is a regular version conflict
560+ // Add the dependency to ignored_dependencies with all conflicting packages as
561+ // exceptions
562+ let package_names: Vec < String > = conflict
563+ . conflicting_packages
564+ . iter ( )
565+ . map ( |usage| usage. package_name . clone ( ) )
566+ . collect ( ) ;
567+
568+ new_config. ignored_dependencies . insert (
569+ conflict. dependency_name . clone ( ) ,
570+ IgnoredDependency {
571+ exceptions : package_names,
572+ } ,
573+ ) ;
574+ }
575+ }
576+
577+ // Also copy any existing ignored dependencies
578+ for ( dep_name, ignored_dep) in & current_config. ignored_dependencies {
579+ if !new_config. ignored_dependencies . contains_key ( dep_name) {
580+ new_config
581+ . ignored_dependencies
582+ . insert ( dep_name. clone ( ) , ignored_dep. clone ( ) ) ;
583+ }
584+ }
585+
586+ // Copy any existing pinned dependencies that weren't modified
587+ for ( dep_name, pinned_dep) in & current_config. pinned_dependencies {
588+ if !new_config. pinned_dependencies . contains_key ( dep_name) {
589+ new_config
590+ . pinned_dependencies
591+ . insert ( dep_name. clone ( ) , pinned_dep. clone ( ) ) ;
592+ }
593+ }
594+
595+ new_config
596+ }
597+
598+ async fn write_allowlist_config (
599+ repo_root : & AbsoluteSystemPath ,
600+ config : & DepsSyncConfig ,
601+ ) -> Result < ( ) , Error > {
602+ let config_opts = crate :: config:: ConfigurationOptions :: default ( ) ;
603+ let turbo_json_path = config_opts
604+ . root_turbo_json_path ( repo_root)
605+ . map_err ( |e| Error :: Config ( e) ) ?;
606+
607+ // Read the current turbo.json file
608+ let mut raw_turbo_json = match RawTurboJson :: read ( repo_root, & turbo_json_path) ? {
609+ Some ( turbo_json) => turbo_json,
610+ None => RawTurboJson :: default ( ) ,
611+ } ;
612+
613+ // Update the deps_sync configuration
614+ raw_turbo_json. deps_sync = Some ( config. clone ( ) ) ;
615+
616+ // Write the updated configuration back to the file
617+ let json_content = serde_json:: to_string_pretty ( & raw_turbo_json) ?;
618+ std:: fs:: write ( & turbo_json_path, json_content) ?;
619+
620+ Ok ( ( ) )
621+ }
622+
492623#[ test]
493624fn test_ignored_dependencies_with_exceptions ( ) {
494625 let dependencies = vec ! [
@@ -759,4 +890,64 @@ mod tests {
759890 assert_eq ! ( conflict. conflicting_packages[ 0 ] . package_name, "app2" ) ;
760891 assert_eq ! ( conflict. conflicting_packages[ 0 ] . version, "16.0.0" ) ;
761892 }
893+
894+ #[ test]
895+ fn test_generate_allowlist_config ( ) {
896+ let conflicts = vec ! [
897+ // Regular version conflict
898+ DependencyConflict {
899+ dependency_name: "lodash" . to_string( ) ,
900+ conflicting_packages: vec![
901+ DependencyUsage {
902+ package_name: "app1" . to_string( ) ,
903+ version: "4.17.0" . to_string( ) ,
904+ package_path: "packages/app1" . to_string( ) ,
905+ } ,
906+ DependencyUsage {
907+ package_name: "app2" . to_string( ) ,
908+ version: "4.18.0" . to_string( ) ,
909+ package_path: "packages/app2" . to_string( ) ,
910+ } ,
911+ ] ,
912+ conflict_reason: None ,
913+ } ,
914+ // Pinned dependency conflict
915+ DependencyConflict {
916+ dependency_name: "react" . to_string( ) ,
917+ conflicting_packages: vec![ DependencyUsage {
918+ package_name: "app3" . to_string( ) ,
919+ version: "17.0.0" . to_string( ) ,
920+ package_path: "packages/app3" . to_string( ) ,
921+ } ] ,
922+ conflict_reason: Some ( "pinned to 18.0.0" . to_string( ) ) ,
923+ } ,
924+ ] ;
925+
926+ let current_config = DepsSyncConfig {
927+ pinned_dependencies : HashMap :: from ( [ (
928+ "react" . to_string ( ) ,
929+ PinnedDependency {
930+ version : "18.0.0" . to_string ( ) ,
931+ exceptions : vec ! [ ] ,
932+ } ,
933+ ) ] ) ,
934+ ignored_dependencies : HashMap :: new ( ) ,
935+ } ;
936+
937+ let allowlist_config = generate_allowlist_config ( & conflicts, & current_config) ;
938+
939+ // lodash should be added to ignored_dependencies with all conflicting packages
940+ // as exceptions
941+ assert ! ( allowlist_config. ignored_dependencies. contains_key( "lodash" ) ) ;
942+ let lodash_exceptions = & allowlist_config. ignored_dependencies [ "lodash" ] . exceptions ;
943+ assert_eq ! ( lodash_exceptions. len( ) , 2 ) ;
944+ assert ! ( lodash_exceptions. contains( & "app1" . to_string( ) ) ) ;
945+ assert ! ( lodash_exceptions. contains( & "app2" . to_string( ) ) ) ;
946+
947+ // app3 should be added to react's exceptions
948+ assert ! ( allowlist_config. pinned_dependencies. contains_key( "react" ) ) ;
949+ assert ! ( allowlist_config. pinned_dependencies[ "react" ]
950+ . exceptions
951+ . contains( & "app3" . to_string( ) ) ) ;
952+ }
762953}
0 commit comments