Allow adding and removing dependency overrides (#3716)
diff --git a/lib/src/command/add.dart b/lib/src/command/add.dart
index 8245d89..c6e7c84 100644
--- a/lib/src/command/add.dart
+++ b/lib/src/command/add.dart
@@ -49,6 +49,8 @@
Add to dev_dependencies by prefixing with "dev:".
+Make dependency overrides by prefixing with "override:".
+
Add packages with specific constraints or other sources by giving a descriptor
after a colon.
@@ -69,13 +71,17 @@
`$topLevelProgram pub add 'foo:{"sdk":"flutter"}'`
* Add a git dependency:
`$topLevelProgram pub add 'foo:{"git":"https://github.com/foo/foo"}'`
+ * Add a dependency override:
+ `$topLevelProgram pub add 'override:foo:1.0.0`
* Add a git dependency with a path and ref specified:
`$topLevelProgram pub add \\
- 'foo:{"git":{"url":"../foo.git","ref":"branch","path":"subdir"}}'`''';
+ 'foo:{"git":{"url":"../foo.git","ref":"<branch>","path":"<subdir>"}}'`''';
@override
String get argumentsDescription =>
- '[options] [dev:]<package>[:descriptor] [[dev:]<package>[:descriptor] ...]';
+ '[options] [<section>:]<package>[:descriptor] '
+ '[<section>:]<package2>[:descriptor] ...]';
+
@override
String get docUrl => 'https://dart.dev/tools/pub/cmd/pub-add';
@@ -287,12 +293,17 @@
final name = package.ref.name;
final dependencies = [...original.dependencies.values];
var devDependencies = [...original.devDependencies.values];
+ var dependencyOverrides = [...original.dependencyOverrides.values];
+
final dependencyNames = dependencies.map((dependency) => dependency.name);
final devDependencyNames =
devDependencies.map((devDependency) => devDependency.name);
final range =
package.ref.withConstraint(package.constraint ?? VersionConstraint.any);
- if (package.isDev) {
+
+ if (package.isOverride) {
+ dependencyOverrides.add(range);
+ } else if (package.isDev) {
if (devDependencyNames.contains(name)) {
log.message('"$name" is already in "dev_dependencies". '
'Will try to update the constraint.');
@@ -336,28 +347,66 @@
sdkConstraints: original.sdkConstraints,
dependencies: dependencies,
devDependencies: devDependencies,
- dependencyOverrides: original.dependencyOverrides.values,
+ dependencyOverrides: dependencyOverrides,
);
}
+ static final _argRegExp = RegExp(
+ r'^(?:(?<prefix>dev|override):)?'
+ r'(?<name>[a-zA-Z0-9$.]+)'
+ r'(?::(?<descriptor>.*))?$',
+ );
+
+ static final _lenientArgRegExp = RegExp(
+ r'^(?:(?<prefix>[^:]*):)?'
+ r'(?<name>[^:]*)'
+ r'(?::(?<descriptor>.*))?$',
+ );
+
/// Split [arg] on ':' and interpret it with the flags in [argResult] either as
/// an old-style or a new-style descriptor to produce a PackageRef].
_ParseResult _parsePackage(String arg, ArgResults argResults) {
var isDev = argResults['dev'] as bool;
- if (arg.startsWith('dev:')) {
+ var isOverride = false;
+
+ final match = _argRegExp.firstMatch(arg);
+ if (match == null) {
+ final match2 = _lenientArgRegExp.firstMatch(arg);
+ if (match2 == null) {
+ usageException('Could not parse $arg');
+ } else {
+ if (match2.namedGroup('prefix') != null &&
+ match2.namedGroup('descriptor') != null) {
+ usageException(
+ 'The only allowed prefixes are "dev:" and "override:"',
+ );
+ } else {
+ final packageName = match2.namedGroup('descriptor') == null
+ ? match2.namedGroup('prefix')
+ : match2.namedGroup('name');
+ usageException('Not a valid package name: "$packageName"');
+ }
+ }
+ } else if (match.namedGroup('prefix') == 'dev') {
if (argResults.isDev) {
usageException("Cannot combine 'dev:' with --dev");
}
isDev = true;
- arg = arg.substring('dev:'.length);
+ } else if (match.namedGroup('prefix') == 'override') {
+ if (argResults.isDev) {
+ usageException("Cannot combine 'override:' with --dev");
+ }
+ isOverride = true;
}
- final nextColon = arg.indexOf(':');
- final packageName = nextColon == -1 ? arg : arg.substring(0, nextColon);
+ final packageName = match.namedGroup('name')!;
if (!packageNameRegExp.hasMatch(packageName)) {
usageException('Not a valid package name: "$packageName"');
}
- final descriptor = nextColon == -1 ? null : arg.substring(nextColon + 1);
+ final descriptor = match.namedGroup('descriptor');
+ if (isOverride && descriptor == null) {
+ usageException('A dependency override needs an explicit descriptor.');
+ }
final _PartialParseResult partial;
if (argResults.hasOldStyleOptions) {
partial = _parseDescriptorOldStyleArgs(
@@ -369,7 +418,12 @@
partial = _parseDescriptorNewStyle(packageName, descriptor);
}
- return _ParseResult(partial.ref, partial.constraint, isDev: isDev);
+ return _ParseResult(
+ partial.ref,
+ partial.constraint,
+ isDev: isDev,
+ isOverride: isOverride,
+ );
}
/// Parse [descriptor] to return the corresponding [_ParseResult] using the
@@ -612,7 +666,9 @@
log.fine('Contents:\n$yamlEditor');
for (final update in updates) {
- final dependencyKey = update.isDev ? 'dev_dependencies' : 'dependencies';
+ final dependencyKey = update.isDev
+ ? 'dev_dependencies'
+ : (update.isOverride ? 'dependency_overrides' : 'dependencies');
final constraint = update.constraint;
final ref = update.ref;
final name = ref.name;
@@ -657,7 +713,7 @@
/// Remove the package from dev_dependencies if we are adding it to
/// dependencies. Refer to [_addPackageToPubspec] for additional discussion.
- if (!update.isDev) {
+ if (!update.isDev && !update.isOverride) {
final devDependenciesNode = yamlEditor
.parseAt(['dev_dependencies'], orElse: () => YamlScalar.wrap(null));
@@ -689,7 +745,13 @@
final PackageRef ref;
final VersionConstraint? constraint;
final bool isDev;
- _ParseResult(this.ref, this.constraint, {required this.isDev});
+ final bool isOverride;
+ _ParseResult(
+ this.ref,
+ this.constraint, {
+ required this.isDev,
+ required this.isOverride,
+ });
}
extension on ArgResults {
diff --git a/lib/src/command/remove.dart b/lib/src/command/remove.dart
index 01e10c2..cf65241 100644
--- a/lib/src/command/remove.dart
+++ b/lib/src/command/remove.dart
@@ -20,9 +20,18 @@
@override
String get name => 'remove';
@override
- String get description => 'Removes a dependency from the current package.';
+ String get description => '''
+Removes dependencies from `pubspec.yaml`.
+
+Invoking `dart pub remove foo bar` will remove `foo` and `bar` from either
+`dependencies` or `dev_dependencies` in `pubspec.yaml`.
+
+To remove a dependency override of a package prefix the package name with
+'override:'.
+''';
+
@override
- String get argumentsDescription => '<package>';
+ String get argumentsDescription => '<package1> [<package2>...]';
@override
String get docUrl => 'https://dart.dev/tools/pub/cmd/pub-remove';
@override
@@ -68,11 +77,16 @@
usageException('Must specify a package to be removed.');
}
- final packages = Set<String>.from(argResults.rest);
+ final targets = Set<String>.from(argResults.rest).map((descriptor) {
+ final isOverride = descriptor.startsWith('override:');
+ final name =
+ isOverride ? descriptor.substring('override:'.length) : descriptor;
+ return _PackageRemoval(name, removeFromOverride: isOverride);
+ });
if (isDryRun) {
final rootPubspec = entrypoint.root.pubspec;
- final newPubspec = _removePackagesFromPubspec(rootPubspec, packages);
+ final newPubspec = _removePackagesFromPubspec(rootPubspec, targets);
final newRoot = Package.inMemory(newPubspec);
await Entrypoint.inMemory(newRoot, cache, lockFile: entrypoint.lockFile)
@@ -84,7 +98,7 @@
);
} else {
/// Update the pubspec.
- _writeRemovalToPubspec(packages);
+ _writeRemovalToPubspec(targets);
/// Create a new [Entrypoint] since we have to reprocess the updated
/// pubspec file.
@@ -107,43 +121,53 @@
}
}
- Pubspec _removePackagesFromPubspec(Pubspec original, Set<String> packages) {
- final originalDependencies = original.dependencies.values;
- final originalDevDependencies = original.devDependencies.values;
+ Pubspec _removePackagesFromPubspec(
+ Pubspec original,
+ Iterable<_PackageRemoval> packages,
+ ) {
+ final dependencies = {...original.dependencies};
+ final devDependencies = {...original.devDependencies};
+ final overrides = {...original.dependencyOverrides};
- final newDependencies = originalDependencies
- .where((dependency) => !packages.contains(dependency.name));
- final newDevDependencies = originalDevDependencies
- .where((dependency) => !packages.contains(dependency.name));
-
+ for (final package in packages) {
+ if (package.removeFromOverride) {
+ overrides.remove(package.name);
+ } else {
+ dependencies.remove(package.name);
+ devDependencies.remove(package.name);
+ }
+ }
return Pubspec(
original.name,
version: original.version,
sdkConstraints: original.sdkConstraints,
- dependencies: newDependencies,
- devDependencies: newDevDependencies,
- dependencyOverrides: original.dependencyOverrides.values,
+ dependencies: dependencies.values,
+ devDependencies: devDependencies.values,
+ dependencyOverrides: overrides.values,
);
}
/// Writes the changes to the pubspec file
- void _writeRemovalToPubspec(Set<String> packages) {
+ void _writeRemovalToPubspec(Iterable<_PackageRemoval> packages) {
ArgumentError.checkNotNull(packages, 'packages');
final yamlEditor = YamlEditor(readTextFile(entrypoint.pubspecPath));
- for (var package in packages) {
+ for (final package in packages) {
+ final dependencyKeys = package.removeFromOverride
+ ? ['dependency_overrides']
+ : ['dependencies', 'dev_dependencies'];
var found = false;
+ final name = package.name;
/// There may be packages where the dependency is declared both in
- /// dependencies and dev_dependencies.
- for (final dependencyKey in ['dependencies', 'dev_dependencies']) {
+ /// dependencies and dev_dependencies - remove it from both in that case.
+ for (final dependencyKey in dependencyKeys) {
final dependenciesNode = yamlEditor
.parseAt([dependencyKey], orElse: () => YamlScalar.wrap(null));
- if (dependenciesNode is YamlMap &&
- dependenciesNode.containsKey(package)) {
- yamlEditor.remove([dependencyKey, package]);
+ if (dependenciesNode is YamlMap && dependenciesNode.containsKey(name)) {
+ yamlEditor.remove([dependencyKey, name]);
found = true;
// Check if the dependencies or dev_dependencies map is now empty
// If it is empty, remove the key as well
@@ -152,13 +176,20 @@
}
}
}
-
if (!found) {
- log.warning('Package "$package" was not found in pubspec.yaml!');
+ log.warning(
+ 'Package "$name" was not found in ${entrypoint.pubspecPath}!',
+ );
}
-
- /// Windows line endings are already handled by [yamlEditor]
- writeTextFile(entrypoint.pubspecPath, yamlEditor.toString());
}
+
+ /// Windows line endings are already handled by [yamlEditor]
+ writeTextFile(entrypoint.pubspecPath, yamlEditor.toString());
}
}
+
+class _PackageRemoval {
+ final String name;
+ final bool removeFromOverride;
+ _PackageRemoval(this.name, {required this.removeFromOverride});
+}
diff --git a/test/add/common/add_test.dart b/test/add/common/add_test.dart
index 548f680..512a7ae 100644
--- a/test/add/common/add_test.dart
+++ b/test/add/common/add_test.dart
@@ -1010,4 +1010,58 @@
]),
);
});
+
+ test('adds to overrides', () async {
+ final server = await servePackages();
+ server.serve('foo', '1.0.0', deps: {'bar': '1.0.0'});
+ server.serve('bar', '1.0.0');
+ server.serve('bar', '2.0.0');
+
+ await d.dir('local_foo', [d.libPubspec('foo', '1.0.0')]).create();
+
+ await d.dir(appPath, [
+ d.file('pubspec.yaml', '''
+name: myapp
+dependencies:
+ foo: ^1.0.0
+environment:
+ sdk: '$defaultSdkConstraint'
+'''),
+ ]).create();
+
+ await pubGet();
+
+ await pubAdd(
+ args: ['override:bar'],
+ exitCode: exit_codes.USAGE,
+ error: contains('A dependency override needs an explicit descriptor.'),
+ );
+
+ // Can override a transitive dependency.
+ await pubAdd(args: ['override:bar:2.0.0']);
+ await d.dir(appPath, [
+ d.file(
+ 'pubspec.yaml',
+ contains('''
+dependency_overrides:
+ bar: 2.0.0
+'''),
+ )
+ ]).validate();
+
+ // Can override with a descriptor:
+ await pubAdd(args: ['override:foo:{"path": "../local_foo"}']);
+
+ await d.dir(appPath, [
+ d.file(
+ 'pubspec.yaml',
+ contains('''
+dependency_overrides:
+ bar: 2.0.0
+ foo:
+ path: ../local_foo
+'''),
+ )
+ ]).validate();
+ });
}
diff --git a/test/remove/remove_test.dart b/test/remove/remove_test.dart
index 689b70f..9abdfc6 100644
--- a/test/remove/remove_test.dart
+++ b/test/remove/remove_test.dart
@@ -235,6 +235,35 @@
await d.appDir(dependencies: {'bar': '2.0.1'}).validate();
});
+ test('removes overrides', () async {
+ final server = await servePackages();
+ server.serve('foo', '1.0.0', deps: {'bar': '1.0.0'});
+ server.serve('bar', '1.0.0');
+ server.serve('bar', '2.0.0');
+
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'dependencies': {'foo': '^1.0.0'},
+ 'dev_dependencies': {'bar': '^2.0.0'},
+ 'dependency_overrides': {'bar': '1.0.0'}
+ })
+ ]).create();
+
+ await pubGet();
+
+ // Cannot remove the constraint on bar, would create conflict.
+ await pubRemove(
+ args: ['override:bar'],
+ error: contains('version solving failed.'),
+ exitCode: 1,
+ );
+ await pubRemove(args: ['override:bar', 'foo']);
+ await d.appPackageConfigFile([
+ d.packageConfigEntry(name: 'bar', version: '2.0.0'),
+ ]).validate();
+ });
+
test('preserves comments', () async {
await servePackages()
..serve('bar', '1.0.0')
diff --git a/test/test_pub.dart b/test/test_pub.dart
index e389f29..2bb9002 100644
--- a/test/test_pub.dart
+++ b/test/test_pub.dart
@@ -538,7 +538,7 @@
dartArgs,
environment: mergedEnvironment,
workingDirectory: workingDirectory ?? _pathInSandbox(appPath),
- description: args.isEmpty ? 'pub' : 'pub ${args.first}',
+ description: args.isEmpty ? 'pub' : 'pub ${args.join(' ')}',
includeParentEnvironment: false,
);
}
diff --git a/test/testdata/goldens/embedding/embedding_test/--help.txt b/test/testdata/goldens/embedding/embedding_test/--help.txt
index 780b29e..0211fa4 100644
--- a/test/testdata/goldens/embedding/embedding_test/--help.txt
+++ b/test/testdata/goldens/embedding/embedding_test/--help.txt
@@ -24,7 +24,7 @@
logout Log out of pub.dev.
outdated Analyze your dependencies to find which ones can be upgraded.
publish Publish the current package to pub.dev.
- remove Removes a dependency from the current package.
+ remove Removes dependencies from `pubspec.yaml`.
token Manage authentication tokens for hosted pub repositories.
upgrade Upgrade the current package's dependencies to latest versions.
diff --git a/test/testdata/goldens/help_test/pub add --help.txt b/test/testdata/goldens/help_test/pub add --help.txt
index dfed108..f81cf7a 100644
--- a/test/testdata/goldens/help_test/pub add --help.txt
+++ b/test/testdata/goldens/help_test/pub add --help.txt
@@ -9,6 +9,8 @@
Add to dev_dependencies by prefixing with "dev:".
+Make dependency overrides by prefixing with "override:".
+
Add packages with specific constraints or other sources by giving a descriptor
after a colon.
@@ -29,11 +31,13 @@
`dart pub add 'foo:{"sdk":"flutter"}'`
* Add a git dependency:
`dart pub add 'foo:{"git":"https://github.com/foo/foo"}'`
+ * Add a dependency override:
+ `dart pub add 'override:foo:1.0.0`
* Add a git dependency with a path and ref specified:
`dart pub add \
- 'foo:{"git":{"url":"../foo.git","ref":"branch","path":"subdir"}}'`
+ 'foo:{"git":{"url":"../foo.git","ref":"<branch>","path":"<subdir>"}}'`
-Usage: pub add [options] [dev:]<package>[:descriptor] [[dev:]<package>[:descriptor] ...]
+Usage: pub add [options] [<section>:]<package>[:descriptor] [<section>:]<package2>[:descriptor] ...]
-h, --help Print this usage information.
--[no-]offline Use cached packages instead of accessing the network.
-n, --dry-run Report what dependencies would change but don't change any.
diff --git a/test/testdata/goldens/help_test/pub remove --help.txt b/test/testdata/goldens/help_test/pub remove --help.txt
index f8da56e..2032abf 100644
--- a/test/testdata/goldens/help_test/pub remove --help.txt
+++ b/test/testdata/goldens/help_test/pub remove --help.txt
@@ -2,9 +2,16 @@
## Section 0
$ pub remove --help
-Removes a dependency from the current package.
+Removes dependencies from `pubspec.yaml`.
-Usage: pub remove <package>
+Invoking `dart pub remove foo bar` will remove `foo` and `bar` from either
+`dependencies` or `dev_dependencies` in `pubspec.yaml`.
+
+To remove a dependency override of a package prefix the package name with
+'override:'.
+
+
+Usage: pub remove <package1> [<package2>...]
-h, --help Print this usage information.
--[no-]offline Use cached packages instead of accessing the network.
-n, --dry-run Report what dependencies would change but don't change any.