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'},
+ );
+ });
+}