Adding a `pub remove` command (#2620)
* Initial implementation of `pub remove <package>`
diff --git a/lib/src/command/remove.dart b/lib/src/command/remove.dart
new file mode 100644
index 0000000..dc3bd26
--- /dev/null
+++ b/lib/src/command/remove.dart
@@ -0,0 +1,116 @@
+// Copyright (c) 2020, 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:yaml_edit/yaml_edit.dart';
+
+import '../command.dart';
+import '../entrypoint.dart';
+import '../io.dart';
+import '../log.dart' as log;
+import '../package.dart';
+import '../pubspec.dart';
+import '../solver.dart';
+
+/// Handles the `remove` pub command. Removes dependencies from `pubspec.yaml`,
+/// and performs an operation similar to `pub get`. Unlike `pub add`, this
+/// command supports the removal of multiple dependencies.
+class RemoveCommand extends PubCommand {
+ @override
+ String get name => 'remove';
+ @override
+ String get description => 'Removes a dependency from the current package.';
+ @override
+ String get invocation => 'pub remove <package>';
+ @override
+ String get docUrl => 'https://dart.dev/tools/pub/cmd/pub-remove';
+ @override
+ bool get isOffline => argResults['offline'];
+
+ bool get isDryRun => argResults['dry-run'];
+
+ RemoveCommand() {
+ argParser.addFlag('offline',
+ help: 'Use cached packages instead of accessing the network.');
+
+ argParser.addFlag('dry-run',
+ abbr: 'n',
+ negatable: false,
+ help: "Report what dependencies would change but don't change any.");
+
+ argParser.addFlag('precompile',
+ help: 'Precompile executables in immediate dependencies.');
+ }
+
+ @override
+ Future run() async {
+ if (argResults.rest.isEmpty) {
+ usageException('Must specify a package to be removed.');
+ }
+
+ final packages = Set<String>.from(argResults.rest);
+
+ if (isDryRun) {
+ final rootPubspec = entrypoint.root.pubspec;
+ final newPubspec = _removePackagesFromPubspec(rootPubspec, packages);
+ final newRoot = Package.inMemory(newPubspec);
+
+ await Entrypoint.global(newRoot, entrypoint.lockFile, cache)
+ .acquireDependencies(SolveType.GET,
+ precompile: argResults['precompile']);
+ } else {
+ /// Update the pubspec.
+ _writeRemovalToPubspec(packages);
+
+ await Entrypoint.current(cache).acquireDependencies(SolveType.GET,
+ precompile: argResults['precompile']);
+ }
+ }
+
+ Pubspec _removePackagesFromPubspec(Pubspec original, Set<String> packages) {
+ final originalDependencies = original.dependencies.values;
+ final originalDevDependencies = original.devDependencies.values;
+
+ final newDependencies = originalDependencies
+ .where((dependency) => !packages.contains(dependency.name));
+ final newDevDependencies = originalDevDependencies
+ .where((dependency) => !packages.contains(dependency.name));
+
+ return Pubspec(
+ original.name,
+ version: original.version,
+ sdkConstraints: original.sdkConstraints,
+ dependencies: newDependencies,
+ devDependencies: newDevDependencies,
+ dependencyOverrides: original.dependencyOverrides.values,
+ );
+ }
+
+ /// Writes the changes to the pubspec file
+ void _writeRemovalToPubspec(Set<String> packages) {
+ ArgumentError.checkNotNull(packages, 'packages');
+
+ final yamlEditor = YamlEditor(readTextFile(entrypoint.pubspecPath));
+
+ for (var package in packages) {
+ var found = false;
+
+ /// There may be packages where the dependency is declared both in
+ /// dependencies and dev_dependencies.
+ for (final dependencyKey in ['dependencies', 'dev_dependencies']) {
+ if (yamlEditor.parseAt([dependencyKey, package], orElse: () => null) !=
+ null) {
+ yamlEditor.remove([dependencyKey, package]);
+ found = true;
+ }
+ }
+
+ if (!found) {
+ log.warning('Package "$package" was not found in pubspec.yaml!');
+ }
+
+ /// Windows line endings are already handled by [yamlEditor]
+ writeTextFile(entrypoint.pubspecPath, yamlEditor.toString());
+ }
+ }
+}
diff --git a/lib/src/command_runner.dart b/lib/src/command_runner.dart
index 4a3f547..d5e98a0 100644
--- a/lib/src/command_runner.dart
+++ b/lib/src/command_runner.dart
@@ -22,6 +22,7 @@
import 'command/list_package_dirs.dart';
import 'command/logout.dart';
import 'command/outdated.dart';
+import 'command/remove.dart';
import 'command/run.dart';
import 'command/serve.dart';
import 'command/upgrade.dart';
@@ -115,6 +116,7 @@
addCommand(ListPackageDirsCommand());
addCommand(LishCommand());
addCommand(OutdatedCommand());
+ addCommand(RemoveCommand());
addCommand(RunCommand());
addCommand(ServeCommand());
addCommand(UpgradeCommand());
diff --git a/test/descriptor/yaml.dart b/test/descriptor/yaml.dart
index fc56ee3..da79cd1 100644
--- a/test/descriptor/yaml.dart
+++ b/test/descriptor/yaml.dart
@@ -14,9 +14,7 @@
import '../descriptor.dart';
-/// Describes a YAML file and its contents.
class YamlDescriptor extends FileDescriptor {
- /// Contents of the YAML file. Must be valid YAML.
final String _contents;
YamlDescriptor(String name, this._contents) : super.protected(name);
diff --git a/test/pub_test.dart b/test/pub_test.dart
index 0d0c91b..3e52f94 100644
--- a/test/pub_test.dart
+++ b/test/pub_test.dart
@@ -38,6 +38,7 @@
logout Log out of pub.dartlang.org.
outdated Analyze your dependencies to find which ones can be upgraded.
publish Publish the current package to pub.dartlang.org.
+ remove Removes a dependency from the current package.
run Run an executable from a package.
upgrade Upgrade the current package's dependencies to latest versions.
uploader Manage uploaders for a package on pub.dartlang.org.
diff --git a/test/remove/remove_test.dart b/test/remove/remove_test.dart
new file mode 100644
index 0000000..50dba5d
--- /dev/null
+++ b/test/remove/remove_test.dart
@@ -0,0 +1,215 @@
+// Copyright (c) 2020, 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 'dart:io' show File;
+
+import 'package:path/path.dart' as p;
+import 'package:test/test.dart';
+
+import '../descriptor.dart' as d;
+import '../descriptor/yaml.dart';
+import '../test_pub.dart';
+
+void main() {
+ test('removes a package from dependencies', () async {
+ await servePackages((builder) => builder.serve('foo', '1.2.3'));
+
+ await d.appDir({'foo': '1.2.3'}).create();
+ await pubGet();
+
+ await pubRemove(args: ['foo']);
+
+ await d.cacheDir({}).validate();
+ await d.appPackagesFile({}).validate();
+ await d.appDir({}).validate();
+ });
+
+ test('dry-run does not actually remove dependency', () async {
+ await servePackages((builder) => builder.serve('foo', '1.2.3'));
+
+ await d.appDir({'foo': '1.2.3'}).create();
+ await pubGet();
+
+ await pubRemove(
+ args: ['foo', '--dry-run'],
+ output: allOf([
+ contains('These packages are no longer being depended on:'),
+ contains('- foo 1.2.3')
+ ]));
+
+ await d.appDir({'foo': '1.2.3'}).validate();
+ });
+
+ test('prints a warning if package does not exist', () async {
+ await d.appDir().create();
+ await pubRemove(
+ args: ['foo'],
+ warning: contains('Package "foo" was not found in pubspec.yaml!'));
+
+ await d.appDir().validate();
+ });
+
+ test('prints a warning if the dependencies map does not exist', () async {
+ await d.dir(appPath, [
+ d.pubspec({'name': 'myapp'})
+ ]).create();
+ await pubRemove(
+ args: ['foo'],
+ warning: contains('Package "foo" was not found in pubspec.yaml!'));
+
+ await d.dir(appPath, [
+ d.pubspec({'name': 'myapp'})
+ ]).validate();
+ });
+
+ test('removes a package from dev_dependencies', () async {
+ await servePackages((builder) => builder.serve('foo', '1.2.3'));
+
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'dev_dependencies': {'foo': '1.2.3'}
+ })
+ ]).create();
+ await pubGet();
+
+ await pubRemove(args: ['foo']);
+
+ await d.cacheDir({}).validate();
+ await d.appPackagesFile({}).validate();
+
+ await d.dir(appPath, [
+ d.pubspec({'name': 'myapp', 'dev_dependencies': {}})
+ ]).validate();
+ });
+
+ test('removes multiple packages from dependencies and dev_dependencies',
+ () async {
+ await servePackages((builder) {
+ builder.serve('foo', '1.2.3');
+ builder.serve('bar', '2.3.4');
+ builder.serve('baz', '3.2.1');
+ builder.serve('jfj', '0.2.1');
+ });
+
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'dependencies': {'bar': '>=2.3.4', 'jfj': '0.2.1'},
+ 'dev_dependencies': {'foo': '^1.2.3', 'baz': '3.2.1'}
+ })
+ ]).create();
+ await pubGet();
+
+ await pubRemove(args: ['foo', 'bar', 'baz']);
+
+ await d.cacheDir({'jfj': '0.2.1'}).validate();
+ await d.appPackagesFile({'jfj': '0.2.1'}).validate();
+
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'dependencies': {'jfj': '0.2.1'},
+ 'dev_dependencies': {}
+ })
+ ]).validate();
+ });
+
+ test('removes git dependencies', () async {
+ await servePackages((builder) => builder.serve('bar', '1.2.3'));
+
+ ensureGit();
+ final repo = d.git('foo.git', [
+ d.dir('subdir', [d.libPubspec('foo', '1.0.0'), d.libDir('foo', '1.0.0')])
+ ]);
+ await repo.create();
+
+ await d.appDir({
+ 'foo': {
+ 'git': {'url': '../foo.git', 'path': 'subdir'}
+ },
+ 'bar': '1.2.3'
+ }).create();
+
+ await pubGet();
+
+ await pubRemove(args: ['foo']);
+ await d.appPackagesFile({'bar': '1.2.3'}).validate();
+ await d.appDir({'bar': '1.2.3'}).validate();
+ });
+
+ test('removes path dependencies', () async {
+ await servePackages((builder) => builder.serve('bar', '1.2.3'));
+ await d
+ .dir('foo', [d.libDir('foo'), d.libPubspec('foo', '0.0.1')]).create();
+
+ await d.appDir({
+ 'foo': {'path': '../foo'},
+ 'bar': '1.2.3'
+ }).create();
+
+ await pubGet();
+
+ await pubRemove(args: ['foo']);
+ await d.appPackagesFile({'bar': '1.2.3'}).validate();
+ await d.appDir({'bar': '1.2.3'}).validate();
+ });
+
+ test('removes hosted dependencies', () async {
+ await servePackages((builder) => builder.serve('bar', '2.0.1'));
+
+ var server = await PackageServer.start((builder) {
+ builder.serve('foo', '1.2.3');
+ });
+
+ await d.appDir({
+ 'foo': {
+ 'version': '1.2.3',
+ 'hosted': {'name': 'foo', 'url': 'http://localhost:${server.port}'}
+ },
+ 'bar': '2.0.1'
+ }).create();
+
+ await pubGet();
+
+ await pubRemove(args: ['foo']);
+ await d.appPackagesFile({'bar': '2.0.1'}).validate();
+ await d.appDir({'bar': '2.0.1'}).validate();
+ });
+
+ test('preserves comments', () async {
+ await servePackages((builder) {
+ builder.serve('bar', '1.0.0');
+ builder.serve('foo', '1.0.0');
+ });
+
+ final initialPubspec = YamlDescriptor('pubspec.yaml', '''
+ name: myapp
+ dependencies: # comment A
+ # comment B
+ bar: 1.0.0
+ # comment C
+ foo: 1.0.0 # comment D
+ # comment E
+ ''');
+ await d.dir(appPath, [initialPubspec]).create();
+
+ await pubGet();
+
+ await pubRemove(args: ['bar']);
+
+ final fullPath = p.join(d.sandbox, appPath, 'pubspec.yaml');
+ expect(File(fullPath).existsSync(), true);
+ final contents = File(fullPath).readAsStringSync();
+ expect(
+ contents,
+ allOf([
+ contains('# comment A'),
+ contains('# comment B'),
+ contains('# comment C'),
+ contains('# comment D'),
+ contains('# comment E')
+ ]));
+ });
+}
diff --git a/test/test_pub.dart b/test/test_pub.dart
index 8c5cc73..14986a7 100644
--- a/test/test_pub.dart
+++ b/test/test_pub.dart
@@ -93,6 +93,8 @@
Try `pub outdated` for more information.$)'''));
static final downgrade = RunCommand('downgrade',
RegExp(r'(No dependencies changed\.|Changed \d+ dependenc(y|ies)!)$'));
+ static final remove = RunCommand(
+ 'remove', RegExp(r'Got dependencies!|Changed \d+ dependenc(y|ies)!'));
final String name;
final RegExp success;
@@ -214,6 +216,21 @@
exitCode: exitCode,
environment: environment);
+Future pubRemove(
+ {Iterable<String> args,
+ output,
+ error,
+ warning,
+ int exitCode,
+ Map<String, String> environment}) =>
+ pubCommand(RunCommand.remove,
+ args: args,
+ output: output,
+ error: error,
+ warning: warning,
+ exitCode: exitCode,
+ environment: environment);
+
/// Schedules starting the "pub [global] run" process and validates the
/// expected startup output.
///