diff --git a/packages/melos/lib/src/commands/list.dart b/packages/melos/lib/src/commands/list.dart index ceee3a35e..f2717cd3a 100644 --- a/packages/melos/lib/src/commands/list.dart +++ b/packages/melos/lib/src/commands/list.dart @@ -41,7 +41,8 @@ mixin _ListMixin on _Melos { void _listGraph(MelosWorkspace workspace) { final jsonGraph = >{}; for (final package in workspace.filteredPackages.values) { - jsonGraph[package.name] = package.dependenciesInWorkspace.keys.toList(); + jsonGraph[package.name] = + package.allDependenciesInWorkspace.keys.toList(); } const encoder = JsonEncoder.withIndent(' '); @@ -139,8 +140,8 @@ mixin _ListMixin on _Melos { 'flutter_package': package.isFlutterPackage, 'flutter_app': package.isFlutterApp, 'flutter_plugin': package.isFlutterPlugin, - 'dependencies': package.dependenciesInWorkspace.keys.toList(), - 'dependents': package.dependentsInWorkspace.keys.toList(), + 'dependencies': package.allDependenciesInWorkspace.keys.toList(), + 'dependents': package.allDependentsInWorkspace.keys.toList(), }); if (package.isFlutterApp) { @@ -220,6 +221,12 @@ mixin _ListMixin on _Melos { ' ${package.name} -> ${dep.name} [style="dashed"; color="${getColor(dep.name)}"];', ); } + + for (final dep in package.dependencyOverridesInWorkspace.values) { + buffer.add( + ' ${package.name} -> ${dep.name} [style="dotted"; color="${getColor(dep.name)}"];', + ); + } } final groupedPackages = workspace.filteredPackages.values diff --git a/packages/melos/lib/src/package.dart b/packages/melos/lib/src/package.dart index ab278820d..0a5d07732 100644 --- a/packages/melos/lib/src/package.dart +++ b/packages/melos/lib/src/package.dart @@ -667,37 +667,45 @@ class Package { late final allDependentsInWorkspace = { ...dependentsInWorkspace, ...devDependentsInWorkspace, + ...dependentsOverridesInWorkspace, }; - /// The dependencies listen in `dev_dependencies:` inside the package's `pubspec.yaml` - /// that are part of the melos workspace - late final Map devDependenciesInWorkspace = - _packagesInWorkspaceForNames(devDependencies); - - /// The dependencies listen in `dependencies:` inside the package's `pubspec.yaml` - /// that are part of the melos workspace + /// The dependencies listed in `dependencies:` inside the package's + /// `pubspec.yaml` that are part of the melos workspace late final Map dependenciesInWorkspace = _packagesInWorkspaceForNames(dependencies); - /// The dependencies listen in `dependency_overrides:` inside the package's `pubspec.yaml` - /// that are part of the melos workspace + /// The dependencies listed in `dev_dependencies:` inside the package's + /// `pubspec.yaml` that are part of the melos workspace + late final Map devDependenciesInWorkspace = + _packagesInWorkspaceForNames(devDependencies); + + /// The dependencies listed in `dependency_overrides:` inside the package's + /// `pubspec.yaml` that are part of the melos workspace late final Map dependencyOverridesInWorkspace = _packagesInWorkspaceForNames(dependencyOverrides); - /// The packages that depends on this package. + /// The packages that depend on this package as a dependency. late final Map dependentsInWorkspace = { for (final entry in _packageMap.entries) if (entry.value.dependenciesInWorkspace.containsKey(name)) entry.key: entry.value, }; - /// The packages that depends on this package. + /// The packages that depend on this package as a dev dependency.. late final Map devDependentsInWorkspace = { for (final entry in _packageMap.entries) if (entry.value.devDependenciesInWorkspace.containsKey(name)) entry.key: entry.value, }; + /// The packages that depend on this package as a dependency override. + late final Map dependentsOverridesInWorkspace = { + for (final entry in _packageMap.entries) + if (entry.value.dependencyOverridesInWorkspace.containsKey(name)) + entry.key: entry.value, + }; + late final Map allTransitiveDependenciesInWorkspace = _transitivelyRelatedPackages( root: this, diff --git a/packages/melos/test/commands/list_test.dart b/packages/melos/test/commands/list_test.dart index 489f7fd4a..1a1eaa558 100644 --- a/packages/melos/test/commands/list_test.dart +++ b/packages/melos/test/commands/list_test.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:melos/src/commands/runner.dart'; import 'package:melos/src/common/glob.dart'; import 'package:melos/src/common/platform.dart'; @@ -145,7 +147,9 @@ long_name 0.0.0 packages/long_name PRIVATE ); }), ); + }); + group('parsable', () { test( 'relativePaths flag prints relative paths only if true', withMockFs(() async { @@ -209,5 +213,161 @@ packages/c skip: currentPlatform.isWindows, ); }); + + group('graph', () { + test( + 'reports all dependencies in workspace', + withMockFs(() async { + final workspaceDir = createMockWorkspaceFs( + packages: [ + MockPackageFs(name: 'a'), + MockPackageFs(name: 'b'), + MockPackageFs(name: 'c'), + MockPackageFs( + name: 'd', + dependencies: ['a'], + devDependencies: ['b'], + dependencyOverrides: ['c'], + ), + ], + ); + + final config = await MelosWorkspaceConfig.fromDirectory(workspaceDir); + final melos = Melos(logger: logger, config: config); + await melos.list( + kind: ListOutputKind.graph, + ); + + expect( + logger.output, + ''' +{ + "a": [], + "b": [], + "c": [], + "d": [ + "a", + "b", + "c" + ] +} +''', + ); + }), + ); + }); + + group('json', () { + test( + 'reports all dependencies in workspace', + withMockFs(() async { + final workspaceDir = createMockWorkspaceFs( + packages: [ + MockPackageFs(name: 'a'), + MockPackageFs(name: 'b'), + MockPackageFs(name: 'c'), + MockPackageFs( + name: 'd', + dependencies: ['a'], + devDependencies: ['b'], + dependencyOverrides: ['c'], + ), + ], + ); + + final config = await MelosWorkspaceConfig.fromDirectory(workspaceDir); + final melos = Melos(logger: logger, config: config); + await melos.list( + kind: ListOutputKind.json, + long: true, + ); + final json = (jsonDecode(logger.output) as List) + .cast>(); + + expect( + json.map( + (pkg) => { + 'name': pkg['name'], + 'dependencies': pkg['dependencies'], + 'dependents': pkg['dependents'], + }, + ), + [ + { + 'name': 'a', + 'dependencies': [], + 'dependents': ['d'], + }, + { + 'name': 'b', + 'dependencies': [], + 'dependents': ['d'], + }, + { + 'name': 'c', + 'dependencies': [], + 'dependents': ['d'], + }, + { + 'name': 'd', + 'dependencies': ['a', 'b', 'c'], + 'dependents': [], + }, + ], + ); + }), + ); + }); + + group('gviz', () { + test( + 'reports all dependencies in workspace', + withMockFs(() async { + final workspaceDir = createMockWorkspaceFs( + packages: [ + MockPackageFs(name: 'a'), + MockPackageFs(name: 'b'), + MockPackageFs(name: 'c'), + MockPackageFs( + name: 'd', + dependencies: ['a'], + devDependencies: ['b'], + dependencyOverrides: ['c'], + ), + ], + ); + + final config = await MelosWorkspaceConfig.fromDirectory(workspaceDir); + final melos = Melos(logger: logger, config: config); + await melos.list( + kind: ListOutputKind.gviz, + ); + + expect( + logger.output, + ''' +digraph packages { + size="10"; ratio=fill; + a [shape="box"; color="#ff5307"]; + b [shape="box"; color="#e03cc2"]; + c [shape="box"; color="#fa533c"]; + d [shape="box"; color="#80dce6"]; + d -> a [style="filled"; color="#ff5307"]; + d -> b [style="dashed"; color="#e03cc2"]; + d -> c [style="dotted"; color="#fa533c"]; + subgraph "cluster packages" { + label="packages"; + color="#6b4949"; + a; + b; + c; + d; + } +} +''', + ); + }), + ); + }); }); } diff --git a/packages/melos/test/mock_workspace_fs.dart b/packages/melos/test/mock_workspace_fs.dart index 7cec5a474..c94a062c9 100644 --- a/packages/melos/test/mock_workspace_fs.dart +++ b/packages/melos/test/mock_workspace_fs.dart @@ -106,6 +106,12 @@ void _createPackage(MockPackageFs package, String workspaceRoot) { ''' dependencies: ${_yamlMap(package.dependencyMap, indent: 2)} + +dev_dependencies: +${_yamlMap(package.devDependencyMap, indent: 2)} + +dependency_overrides: +${_yamlMap(package.dependencyOverridesMap, indent: 2)} ''', ); @@ -129,11 +135,15 @@ class MockPackageFs { required this.name, String? path, List? dependencies, + List? devDependencies, + List? dependencyOverrides, this.version, this.publishToNone = false, bool generateExample = false, }) : _path = path, dependencies = dependencies ?? const [], + devDependencies = devDependencies ?? const [], + dependencyOverrides = dependencyOverrides ?? const [], createExamplePackage = generateExample; /// Name of the package (must be a valid Dart package name) @@ -148,13 +158,28 @@ class MockPackageFs { /// `true` if this package's yaml has a `publish_to: none` setting. final bool publishToNone; - /// A list of package names this one depends on + /// A list of package names that are dependencies of this one. final List dependencies; + /// A list of package names that are dev dependencies of this one. + final List devDependencies; + + /// A list of package names that are dependency overrides of this one. + final List dependencyOverrides; + /// A mapping of dependency names to their versions (always "any") - Map get dependencyMap { - return Map.fromEntries(dependencies.map((name) => MapEntry(name, 'any'))); - } + Map get dependencyMap => + Map.fromEntries(dependencies.map((name) => MapEntry(name, 'any'))); + + /// A mapping of dev dependency names to their versions (always "any") + Map get devDependencyMap => Map.fromEntries( + devDependencies.map((name) => MapEntry(name, 'any')), + ); + + /// A mapping of dependency overrides names to their versions (always "any") + Map get dependencyOverridesMap => Map.fromEntries( + dependencyOverrides.map((name) => MapEntry(name, 'any')), + ); /// `true` if an example package should be generated final bool createExamplePackage;