Fix detection of root packages in PackageGraph.transitiveDependencies (#4620)
diff --git a/lib/src/command/deps.dart b/lib/src/command/deps.dart index 222a594..f74aeb5 100644 --- a/lib/src/command/deps.dart +++ b/lib/src/command/deps.dart
@@ -420,7 +420,7 @@ .expand( (p) => graph.transitiveDependencies( p, - followDevDependenciesFromRoot: false, + followDevDependenciesFromPackage: false, ), ) .map((package) => package.name)
diff --git a/lib/src/command/upgrade.dart b/lib/src/command/upgrade.dart index 0e93a64..a817ff4 100644 --- a/lib/src/command/upgrade.dart +++ b/lib/src/command/upgrade.dart
@@ -124,7 +124,7 @@ (package) => graph .transitiveDependencies( package, - followDevDependenciesFromRoot: true, + followDevDependenciesFromPackage: true, ) .map((p) => p.name), )
diff --git a/lib/src/global_packages.dart b/lib/src/global_packages.dart index b4b708e..592592f 100644 --- a/lib/src/global_packages.dart +++ b/lib/src/global_packages.dart
@@ -212,7 +212,10 @@ final activatedPackage = entrypoint.workPackage; final name = activatedPackage.name; for (final package in (await entrypoint.packageGraph) - .transitiveDependencies(name, followDevDependenciesFromRoot: false)) { + .transitiveDependencies( + name, + followDevDependenciesFromPackage: false, + )) { _testForHooks(package, name); } _describeActive(name, cache);
diff --git a/lib/src/package_graph.dart b/lib/src/package_graph.dart index 07f0666..98a5a03 100644 --- a/lib/src/package_graph.dart +++ b/lib/src/package_graph.dart
@@ -46,12 +46,11 @@ /// Returns all transitive dependencies of [package]. /// - /// For the entrypoint this returns all packages in [packages], which includes - /// dev and override. For any other package, it ignores dev and override - /// dependencies. + /// If [package] is a root, this will explore the dev_dependencies of + /// [package] if [followDevDependenciesFromPackage] is true. Set<Package> transitiveDependencies( String package, { - required bool followDevDependenciesFromRoot, + required bool followDevDependenciesFromPackage, }) { final result = <Package>{}; @@ -63,7 +62,11 @@ final currentPackage = packages[current]!; result.add(currentPackage); stack.addAll(currentPackage.dependencies.keys); - if (followDevDependenciesFromRoot && current == package) { + if (followDevDependenciesFromPackage && + current == package && + entrypoint.workspaceRoot.transitiveWorkspace.any( + (p) => p.name == current, + )) { stack.addAll(currentPackage.devDependencies.keys); } } @@ -89,7 +92,9 @@ return transitiveDependencies( package, - followDevDependenciesFromRoot: true, + // If package is a root package it is not immutable itself, and we don't + // need to consider its dev_dependencies. + followDevDependenciesFromPackage: false, ).any((dep) => !_isPackageFromImmutableSource(dep.name)); } }
diff --git a/test/package_graph_test.dart b/test/package_graph_test.dart new file mode 100644 index 0000000..609c3b3 --- /dev/null +++ b/test/package_graph_test.dart
@@ -0,0 +1,114 @@ +// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:path/path.dart' as p; +import 'package:pub/src/entrypoint.dart'; +import 'package:pub/src/system_cache.dart'; +import 'package:test/test.dart'; + +import 'descriptor.dart' as d; +import 'test_pub.dart'; + +void main() { + test('transitiveDependencies', () async { + final server = await servePackages(); + server.serve( + 'foo', + '1.0.0', + deps: { + 'transitive': {'hosted': globalServer.url}, + }, + pubspec: { + 'dev_dependencies': { + 'transitive_dev_dep': {'hosted': globalServer.url}, + }, // This should **not** be included. + }, + ); + server.serve( + 'dev_dep', + '1.0.0', + deps: { + 'dev_dep_transitive': {'hosted': globalServer.url}, + }, + pubspec: { + 'dev_dependencies': { + 'transitive_dev_dep': { + 'hosted': globalServer.url, + }, // This should **not** be included. + }, + }, + ); + server.serve('dev_dep_transitive', '1.0.0'); + server.serve('transitive', '1.0.0'); + server.serve('a_dev_dep', '1.0.0'); + await d.dir(appPath, [ + d.appPubspec( + dependencies: { + 'a': null, + 'foo': {'hosted': globalServer.url}, + }, + extras: { + 'environment': {'sdk': '^3.5.0'}, + 'workspace': ['a'], + 'dev_dependencies': { + 'dev_dep': {'hosted': globalServer.url}, + }, + }, + ), + d.dir('a', [ + d.libPubspec( + 'a', + '1.0.0', + resolutionWorkspace: true, + devDeps: { + 'a_dev_dep': {'hosted': globalServer.url}, + }, + ), + ]), + ]).create(); + await pubGet(environment: {'_PUB_TEST_SDK_VERSION': '3.5.0'}); + final entrypoint = Entrypoint( + p.join(d.sandbox, appPath), + SystemCache(rootDir: p.join(d.sandbox, cachePath)), + ); + final graph = await entrypoint.packageGraph; + + expect( + graph + .transitiveDependencies('foo', followDevDependenciesFromPackage: true) + .map((p) => p.name), + {'foo', 'transitive'}, + ); + + expect( + graph + .transitiveDependencies( + 'foo', + followDevDependenciesFromPackage: false, + ) + .map((p) => p.name), + {'foo', 'transitive'}, + ); + + expect( + graph + .transitiveDependencies( + 'myapp', + followDevDependenciesFromPackage: true, + ) + .map((p) => p.name), + {'myapp', 'foo', 'dev_dep', 'dev_dep_transitive', 'transitive', 'a'}, + ); + + expect( + graph + .transitiveDependencies( + 'myapp', + followDevDependenciesFromPackage: false, + ) + .map((p) => p.name), + {'myapp', 'foo', 'transitive', 'a'}, + ); + }); +}