Allow adding from multiple sources (#3571)
And having some packages be dev_dependencies while others are not.
By having:
each package carry its own (yaml-formatted) descriptor as it would be written in pubspec.yaml.
dev: be a prefix to the package instead of a top-level argument.
This allows eg.:
dart pub add foo:^1.2.3 'dev:bar:{"git":"../bar.git"}
This is a more full resolution to #3273 than #3324 but implemented in a backwards compatible way (the old arguments still work as long as they are not combined with descriptors (other than bare constraints)).
If a constraint is not given explicitly, either as foo:^1.2.3 or foo:{<source>:<descriptor>,"version":"<constraint"} the "best" constraint is inferred and inserted as before (instead of being interpreted as 'any' as it would in a pubspec.yaml).diff --git a/lib/src/command/add.dart b/lib/src/command/add.dart
index 15b64a4..844f389 100644
--- a/lib/src/command/add.dart
+++ b/lib/src/command/add.dart
@@ -2,7 +2,8 @@
// 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:collection/collection.dart' show IterableExtension;
+import 'package:args/args.dart';
+import 'package:collection/collection.dart';
import 'package:path/path.dart' as p;
import 'package:pub_semver/pub_semver.dart';
import 'package:yaml/yaml.dart';
@@ -13,7 +14,6 @@
import '../exceptions.dart';
import '../git.dart';
import '../io.dart';
-import '../language_version.dart';
import '../log.dart' as log;
import '../package.dart';
import '../package_name.dart';
@@ -31,54 +31,95 @@
/// the other dependencies in `pubspec.yaml`, and then enter that as the lower
/// bound in a ^x.y.z constraint.
///
-/// Currently supports only adding one dependency at a time.
+/// The descriptor used to be given with args like --path, --sdk,
+/// --git-<option>.
+///
+/// We still support these arguments, but now the documented way to give the
+/// descriptor is to give a yaml-descriptor as in pubspec.yaml.
class AddCommand extends PubCommand {
@override
String get name => 'add';
@override
- String get description => 'Add dependencies to pubspec.yaml.';
+ String get description => r'''
+Add dependencies to `pubspec.yaml`.
+
+Invoking `dart pub add foo bar` will add `foo` and `bar` to `pubspec.yaml`
+with a default constraint derived from latest compatible version.
+
+Add to dev_dependencies by prefixing with "dev:".
+
+Add packages with specific constraints or other sources by giving a descriptor
+after a colon.
+
+For example:
+ * Add a hosted dependency at newest compatible stable version:
+ `$topLevelProgram pub add foo`
+ * Add a hosted dev dependency at newest compatible stable version:
+ `$topLevelProgram pub add dev:foo`
+ * Add a hosted dependency with the given constraint
+ `$topLevelProgram pub add foo:^1.2.3`
+ * Add multiple dependencies:
+ `$topLevelProgram pub add foo dev:bar`
+ * Add a path dependency:
+ `$topLevelProgram pub add 'foo{"path":"../foo"}'`
+ * Add a hosted dependency:
+ `$topLevelProgram pub add 'foo{"hosted":"my-pub.dev"}'`
+ * Add an sdk dependency:
+ `$topLevelProgram pub add 'foo{"sdk":"flutter"}'`
+ * Add a git dependency:
+ `$topLevelProgram pub add 'foo{"git":"https://github.com/foo/foo"}'`
+ * Add a git dependency with a path and ref specified:
+ `$topLevelProgram pub add \
+ 'foo{"git":{"url":"../foo.git","ref":"branch","path":"subdir"}}'`''';
+
@override
String get argumentsDescription =>
- '<package>[:<constraint>] [<package2>[:<constraint2>]...] [options]';
+ '[options] [dev:]<package>[:descriptor] [[dev:]<package>[:descriptor] ...]';
@override
String get docUrl => 'https://dart.dev/tools/pub/cmd/pub-add';
- @override
- bool get isOffline => argResults['offline'];
-
- bool get isDev => argResults['dev'];
- bool get isDryRun => argResults['dry-run'];
- String? get gitUrl => argResults['git-url'];
- String? get gitPath => argResults['git-path'];
- String? get gitRef => argResults['git-ref'];
- String? get hostUrl => argResults['hosted-url'];
- String? get path => argResults['path'];
- String? get sdk => argResults['sdk'];
-
- bool get hasGitOptions => gitUrl != null || gitRef != null || gitPath != null;
- bool get hasHostOptions => hostUrl != null;
-
- bool get isHosted => !hasGitOptions && path == null && path == null;
AddCommand() {
- argParser.addFlag('dev',
- abbr: 'd',
- negatable: false,
- help: 'Adds to the development dependencies instead.');
-
- argParser.addOption('git-url', help: 'Git URL of the package');
- argParser.addOption('git-ref',
- help: 'Git branch or commit to be retrieved');
- argParser.addOption('git-path', help: 'Path of git package in repository');
- argParser.addOption('hosted-url', help: 'URL of package host server');
- argParser.addOption('path', help: 'Add package from local path');
- argParser.addOption('sdk',
- help: 'add package from SDK source',
- allowed: ['flutter'],
- valueHelp: '[flutter]');
argParser.addFlag(
- 'example',
- help:
- 'Also update dependencies in `example/` after modifying pubspec.yaml in the root package (if it exists).',
+ 'dev',
+ abbr: 'd',
+ negatable: false,
+ help: 'Adds to the development dependencies instead.',
+ hide: true,
+ );
+
+ // Following options are hidden/deprecated in favor of the new syntax: [dev:]<package>[:descriptor] ...
+ // To avoid breaking changes we keep supporting them, but hide them from --help to discourage
+ // further use. Combining these with new syntax will fail.
+ argParser.addOption(
+ 'git-url',
+ help: 'Git URL of the package',
+ hide: true,
+ );
+ argParser.addOption(
+ 'git-ref',
+ help: 'Git branch or commit to be retrieved',
+ hide: true,
+ );
+ argParser.addOption(
+ 'git-path',
+ help: 'Path of git package in repository',
+ hide: true,
+ );
+ argParser.addOption(
+ 'hosted-url',
+ help: 'URL of package host server',
+ hide: true,
+ );
+ argParser.addOption(
+ 'path',
+ help: 'Add package from local path',
+ hide: true,
+ );
+ argParser.addOption(
+ 'sdk',
+ help: 'add package from SDK source',
+ allowed: ['flutter'],
+ valueHelp: '[flutter]',
hide: true,
);
@@ -94,20 +135,37 @@
help: 'Build executables in immediate dependencies.');
argParser.addOption('directory',
abbr: 'C', help: 'Run this in the directory <dir>.', valueHelp: 'dir');
+ argParser.addFlag(
+ 'example',
+ help:
+ 'Also update dependencies in `example/` after modifying pubspec.yaml in the root package (if it exists).',
+ hide: true,
+ );
}
@override
Future<void> runProtected() async {
+ if (argResults.rest.length > 1) {
+ if (argResults.gitUrl != null) {
+ usageException('''
+--git-url cannot be used with multiple packages.
+Specify multiple git packages with descriptors.''');
+ } else if (argResults.path != null) {
+ usageException('''
+--path cannot be used with multiple packages.
+Specify multiple path packages with descriptors.''');
+ } else if (argResults.sdk != null) {
+ usageException('''
+--sdk cannot be used with multiple packages.
+Specify multiple sdk packages with descriptors.''');
+ }
+ }
if (argResults.rest.isEmpty) {
usageException('Must specify at least one package to be added.');
- } else if (argResults.rest.length > 1 && gitUrl != null) {
- usageException('Can only add a single git package at a time.');
- } else if (argResults.rest.length > 1 && path != null) {
- usageException('Can only add a single local package at a time.');
}
- final languageVersion = entrypoint.root.pubspec.languageVersion;
+
final updates =
- argResults.rest.map((p) => _parsePackage(p, languageVersion)).toList();
+ argResults.rest.map((p) => _parsePackage(p, argResults)).toList();
var updatedPubSpec = entrypoint.root.pubspec;
for (final update in updates) {
@@ -154,7 +212,7 @@
}
}
}
- if (isDryRun) {
+ if (argResults.isDryRun) {
/// Even if it is a dry run, run `acquireDependencies` so that the user
/// gets a report on the other packages that might change version due
/// to this new dependency.
@@ -165,7 +223,7 @@
.acquireDependencies(
SolveType.get,
dryRun: true,
- precompile: argResults['precompile'],
+ precompile: argResults.shouldPrecompile,
analytics: analytics,
);
} else {
@@ -173,21 +231,24 @@
/// ensure that the modification timestamp on `pubspec.lock` and
/// `.dart_tool/package_config.json` is newer than `pubspec.yaml`,
/// ensuring that [entrypoint.assertUptoDate] will pass.
- _updatePubspec(solveResult.packages, updates, isDev);
+ _updatePubspec(
+ solveResult.packages,
+ updates,
+ );
/// Create a new [Entrypoint] since we have to reprocess the updated
/// pubspec file.
final updatedEntrypoint = Entrypoint(directory, cache);
await updatedEntrypoint.acquireDependencies(
SolveType.get,
- precompile: argResults['precompile'],
+ precompile: argResults.shouldPrecompile,
analytics: analytics,
);
- if (argResults['example'] && entrypoint.example != null) {
+ if (argResults.example && entrypoint.example != null) {
await entrypoint.example!.acquireDependencies(
SolveType.get,
- precompile: argResults['precompile'],
+ precompile: argResults.shouldPrecompile,
onlyReportSuccessOrFailure: true,
analytics: analytics,
);
@@ -203,7 +264,9 @@
/// Creates a new in-memory [Pubspec] by adding [package] to the
/// dependencies of [original].
Future<Pubspec> _addPackageToPubspec(
- Pubspec original, _ParseResult package) async {
+ Pubspec original,
+ _ParseResult package,
+ ) async {
final name = package.ref.name;
final dependencies = [...original.dependencies.values];
var devDependencies = [...original.devDependencies.values];
@@ -212,7 +275,7 @@
devDependencies.map((devDependency) => devDependency.name);
final range =
package.ref.withConstraint(package.constraint ?? VersionConstraint.any);
- if (isDev) {
+ if (package.isDev) {
if (devDependencyNames.contains(name)) {
log.message('"$name" is already in "dev_dependencies". '
'Will try to update the constraint.');
@@ -259,12 +322,45 @@
);
}
- /// Parse [package] to return the corresponding [PackageRange], as well as its
- /// representation in `pubspec.yaml`.
+ /// 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:')) {
+ if (argResults.isDev) {
+ usageException("Cannot combine 'dev:' with --dev");
+ }
+ isDev = true;
+ arg = arg.substring('dev:'.length);
+ }
+ final nextColon = arg.indexOf(':');
+ final packageName = nextColon == -1 ? arg : arg.substring(0, nextColon);
+ if (!packageNameRegExp.hasMatch(packageName)) {
+ usageException('Not a valid package name: "$packageName"');
+ }
+ final descriptor = nextColon == -1 ? null : arg.substring(nextColon + 1);
+
+ final _PartialParseResult partial;
+ if (argResults.hasOldStyleOptions) {
+ partial = _parseDescriptorOldStyleArgs(
+ packageName,
+ descriptor,
+ argResults,
+ );
+ } else {
+ partial = _parseDescriptorNewStyle(packageName, descriptor);
+ }
+
+ return _ParseResult(partial.ref, partial.constraint, isDev: isDev);
+ }
+
+ /// Parse [descriptor] to return the corresponding [_ParseResult] using the
+ /// arguments given in [argResults] to configure the description.
///
- /// [package] must be written in the format
- /// `<package-name>[:<version-constraint>]`, where quotations should be used
- /// if necessary.
+ /// [descriptor] should be a constraint as parsed by
+ /// [VersionConstraint.parse]. If it fails to parse as a version constraint
+ /// but could parse with [_parseDescriptorNewStyle()] a specific usage
+ /// description is issued.
///
/// Examples:
/// ```
@@ -278,14 +374,20 @@
/// ```
///
/// If a version constraint is provided when the `--path` or any of the
- /// `--git-<option>` options are used, a [PackageParseError] will be thrown.
+ /// `--git-<option>` options are used, a [UsageException] will be thrown.
///
/// Packages must either be a git, hosted, sdk, or path package. Mixing of
- /// options is not allowed and will cause a [PackageParseError] to be thrown.
+ /// options is not allowed and will cause a [UsageException] to be thrown.
///
/// If any of the other git options are defined when `--git-url` is not
/// defined, an error will be thrown.
- _ParseResult _parsePackage(String package, LanguageVersion languageVersion) {
+ ///
+ /// The returned [_PartialParseResult] will always have `ref!=null`.
+ _PartialParseResult _parseDescriptorOldStyleArgs(
+ String packageName,
+ String? descriptor,
+ ArgResults argResults,
+ ) {
final conflictingFlagSets = [
['git-url', 'git-ref', 'git-path'],
['hosted-url'],
@@ -306,31 +408,34 @@
}
}
- final splitPackage = package.split(':');
- final packageName = splitPackage[0];
-
- /// There shouldn't be more than one `:` in the package information
- if (splitPackage.length > 2) {
- usageException('Invalid package and version constraint: $package');
- }
-
/// We want to allow for [constraint] to take on a `null` value here to
/// preserve the fact that the user did not specify a constraint.
VersionConstraint? constraint;
-
try {
- constraint = splitPackage.length == 2
- ? VersionConstraint.parse(splitPackage[1])
- : null;
+ constraint =
+ descriptor == null ? null : VersionConstraint.parse(descriptor);
} on FormatException catch (e) {
- usageException('Invalid version constraint: ${e.message}');
+ var couldParseAsNewStyle = true;
+ try {
+ _parseDescriptorNewStyle(packageName, descriptor);
+ // If parsing the descriptor as a new-style descriptor succeeds we
+ // can give this more specific error message.
+ } catch (_) {
+ couldParseAsNewStyle = false;
+ }
+ if (couldParseAsNewStyle) {
+ usageException(
+ '--dev, --path, --sdk, --git-url, --git-path and --git-ref cannot be combined with a descriptor.');
+ } else {
+ usageException('Invalid version constraint: ${e.message}');
+ }
}
/// The package to be added.
late final PackageRef ref;
- final path = this.path;
- if (hasGitOptions) {
- final gitUrl = this.gitUrl;
+ final path = argResults.path;
+ if (argResults.hasGitOptions) {
+ final gitUrl = argResults.gitUrl;
if (gitUrl == null) {
usageException('The `--git-url` is required for git dependencies.');
}
@@ -349,37 +454,141 @@
GitDescription(
url: parsed.toString(),
containingDir: p.current,
- ref: gitRef,
- path: gitPath,
+ ref: argResults.gitRef,
+ path: argResults.gitPath,
),
);
} else if (path != null) {
ref = PackageRef(
packageName, PathDescription(p.absolute(path), p.isRelative(path)));
- } else if (sdk != null) {
- ref = cache.sdk.parseRef(packageName, sdk);
+ } else if (argResults.sdk != null) {
+ ref = cache.sdk.parseRef(packageName, argResults.sdk);
} else {
ref = PackageRef(
packageName,
HostedDescription(
packageName,
- hostUrl ?? cache.hosted.defaultUrl,
+ argResults.hostedUrl ?? cache.hosted.defaultUrl,
),
);
}
- return _ParseResult(ref, constraint);
+ return _PartialParseResult(ref, constraint);
+ }
+
+ /// Parse [package] to return the corresponding [_ParseResult].
+ ///
+ /// [package] must be written in the format
+ /// `<package-name>[:descriptor>]`, where quotations should be used if
+ /// necessary.
+ ///
+ /// `descriptor` is what you would put in a pubspec.yaml in the dependencies
+ /// section.
+ ///
+ /// Assumes that none of '--git-url', '--git-ref', '--git-path', '--path' and
+ /// '--sdk' are present in [argResults].
+ ///
+ ///
+ /// Examples:
+ /// ```
+ /// retry
+ /// retry:2.0.0
+ /// dev:retry:^2.0.0
+ /// retry:'>=2.0.0'
+ /// retry:'>2.0.0 <3.0.1'
+ /// 'retry:>2.0.0 <3.0.1'
+ /// retry:any
+ /// 'retry:{"path":"../foo"}'
+ /// 'retry:{"git":{"url":"../foo","ref":"branchname"},"version":"^1.2.3"}'
+ /// 'retry:{"sdk":"flutter"}'
+ /// 'retry:{"hosted":"mypub.dev"}'
+ /// ```
+ ///
+ /// The --path --sdk and --git-<option> arguments cannot be combined with a
+ /// non-string descriptor.
+ ///
+ /// If a version constraint is provided when the `--path` or any of the
+ /// `--git-<option>` options are used, a [PackageParseError] will be thrown.
+ ///
+ /// Packages must either be a git, hosted, sdk, or path package. Mixing of
+ /// options is not allowed and will cause a [PackageParseError] to be thrown.
+ ///
+ /// If any of the other git options are defined when `--git-url` is not
+ /// defined, an error will be thrown.
+ ///
+ /// Returns a `ref` of `null` if the descriptor did not specify a source.
+ /// Then the source will be determined by the old-style arguments.
+ _PartialParseResult _parseDescriptorNewStyle(
+ String packageName,
+ String? descriptor,
+ ) {
+ /// We want to allow for [constraint] to take on a `null` value here to
+ /// preserve the fact that the user did not specify a constraint.
+ VersionConstraint? constraint;
+
+ /// The package to be added.
+ PackageRef? ref;
+
+ if (descriptor != null) {
+ try {
+ // An unquoted version constraint is not always valid yaml.
+ // But we want to allow it here anyways.
+ constraint = VersionConstraint.parse(descriptor);
+ } on FormatException {
+ final parsedDescriptor = loadYaml(descriptor);
+ // Use the pubspec parsing mechanism for parsing the descriptor.
+ final Pubspec dummyPubspec;
+ try {
+ dummyPubspec = Pubspec.fromMap({
+ 'dependencies': {
+ packageName: parsedDescriptor,
+ }
+ }, cache.sources,
+ // Resolve relative paths relative to current, not where the pubspec.yaml is.
+ location: p.toUri(p.join(p.current, 'descriptor')));
+ } on FormatException catch (e) {
+ usageException('Failed parsing package specification: ${e.message}');
+ }
+ final range = dummyPubspec.dependencies[packageName]!;
+ if (parsedDescriptor is String) {
+ // Ref will be constructed by the default behavior below.
+ ref = null;
+ } else {
+ ref = range.toRef();
+ }
+ final hasExplicitConstraint = parsedDescriptor is String ||
+ (parsedDescriptor is Map &&
+ parsedDescriptor.containsKey('version'));
+ // If the descriptor has an explicit constraint, use that. Otherwise we
+ // infer it.
+ if (hasExplicitConstraint) {
+ constraint = range.constraint;
+ }
+ }
+ }
+ return _PartialParseResult(
+ ref ??
+ PackageRef(
+ packageName,
+ HostedDescription(
+ packageName,
+ argResults.hostedUrl ?? cache.hosted.defaultUrl,
+ ),
+ ),
+ constraint,
+ );
}
/// Writes the changes to the pubspec file.
- void _updatePubspec(List<PackageId> resultPackages,
- List<_ParseResult> updates, bool isDevelopment) {
+ void _updatePubspec(
+ List<PackageId> resultPackages,
+ List<_ParseResult> updates,
+ ) {
final yamlEditor = YamlEditor(readTextFile(entrypoint.pubspecPath));
log.io('Reading ${entrypoint.pubspecPath}.');
log.fine('Contents:\n$yamlEditor');
- final dependencyKey = isDevelopment ? 'dev_dependencies' : 'dependencies';
-
for (final update in updates) {
+ final dependencyKey = update.isDev ? 'dev_dependencies' : 'dependencies';
final constraint = update.constraint;
final ref = update.ref;
final name = ref.name;
@@ -420,7 +629,7 @@
/// Remove the package from dev_dependencies if we are adding it to
/// dependencies. Refer to [_addPackageToPubspec] for additional discussion.
- if (!isDevelopment) {
+ if (!update.isDev) {
final devDependenciesNode = yamlEditor
.parseAt(['dev_dependencies'], orElse: () => YamlScalar.wrap(null));
@@ -442,8 +651,35 @@
}
}
+class _PartialParseResult {
+ final PackageRef ref;
+ final VersionConstraint? constraint;
+ _PartialParseResult(this.ref, this.constraint);
+}
+
class _ParseResult {
- PackageRef ref;
- VersionConstraint? constraint;
- _ParseResult(this.ref, this.constraint);
+ final PackageRef ref;
+ final VersionConstraint? constraint;
+ final bool isDev;
+ _ParseResult(this.ref, this.constraint, {required this.isDev});
+}
+
+extension on ArgResults {
+ bool get isDev => this['dev'];
+ bool get isDryRun => this['dry-run'];
+ String? get gitUrl => this['git-url'];
+ String? get gitPath => this['git-path'];
+ String? get gitRef => this['git-ref'];
+ String? get hostedUrl => this['hosted-url'];
+ String? get path => this['path'];
+ String? get sdk => this['sdk'];
+ bool get hasOldStyleOptions =>
+ hasGitOptions ||
+ path != null ||
+ sdk != null ||
+ hostedUrl != null ||
+ isDev;
+ bool get shouldPrecompile => this['precompile'];
+ bool get example => this['example'];
+ bool get hasGitOptions => gitUrl != null || gitRef != null || gitPath != null;
}
diff --git a/test/add/common/add_test.dart b/test/add/common/add_test.dart
index 25f9d8d..87ac53b 100644
--- a/test/add/common/add_test.dart
+++ b/test/add/common/add_test.dart
@@ -20,13 +20,8 @@
await pubAdd(
args: ['bad name!:1.2.3'],
- error: allOf([
- contains(
- "Because myapp depends on bad name! any which doesn't exist (could "
- 'not find package bad name! at http://localhost:'),
- contains('), version solving failed.')
- ]),
- exitCode: exit_codes.DATA);
+ error: contains('Not a valid package name: "bad name!"'),
+ exitCode: exit_codes.USAGE);
await d.appDir({}).validate();
@@ -84,7 +79,7 @@
await d.dir(appPath, [
d.file('pubspec.yaml', '''
name: myapp
- dependencies:
+ dependencies:
dev_dependencies:
@@ -370,7 +365,7 @@
await pubAdd(
args: ['foo:one-two-three'],
- exitCode: exit_codes.USAGE,
+ exitCode: exit_codes.DATA,
error: contains('Invalid version constraint: Could '
'not parse version "one-two-three".'));
@@ -492,6 +487,18 @@
});
});
+ test('Cannot combine descriptor with old-style args', () async {
+ await d.appDir().create();
+
+ await pubAdd(
+ args: ['foo:{"path":"../foo"}', '--path=../foo'],
+ error: contains(
+ '--dev, --path, --sdk, --git-url, --git-path and --git-ref cannot be combined',
+ ),
+ exitCode: exit_codes.USAGE,
+ );
+ });
+
group('--dev', () {
test('--dev adds packages to dev_dependencies instead', () async {
final server = await servePackages();
@@ -515,6 +522,84 @@
]).validate();
});
+ test('--dev cannot be used with a descriptor', () async {
+ await d.dir('foo', [d.libPubspec('foo', '1.2.3')]).create();
+
+ await d.dir(appPath, [
+ d.pubspec({'name': 'myapp', 'dev_dependencies': {}})
+ ]).create();
+
+ await pubAdd(
+ args: ['--dev', 'foo:{"path":../foo}'],
+ error: contains(
+ '--dev, --path, --sdk, --git-url, --git-path and --git-ref cannot be combined',
+ ),
+ exitCode: exit_codes.USAGE,
+ );
+ });
+
+ test('dev: adds packages to dev_dependencies instead without a descriptor',
+ () async {
+ final server = await servePackages();
+ server.serve('foo', '1.2.3');
+
+ await d.dir(appPath, [
+ d.pubspec({'name': 'myapp', 'dev_dependencies': {}})
+ ]).create();
+
+ await pubAdd(args: ['dev:foo:1.2.3']);
+
+ await d.appPackageConfigFile([
+ d.packageConfigEntry(name: 'foo', version: '1.2.3'),
+ ]).validate();
+
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'dev_dependencies': {'foo': '1.2.3'}
+ })
+ ]).validate();
+ });
+
+ test('Cannot combine --dev with :dev', () async {
+ await d.dir('foo', [d.libPubspec('foo', '1.2.3')]).create();
+
+ await d.dir(appPath, [
+ d.pubspec({'name': 'myapp', 'dev_dependencies': {}})
+ ]).create();
+
+ await pubAdd(
+ args: ['--dev', 'dev:foo:1.2.3'],
+ error: contains("Cannot combine 'dev:' with --dev"),
+ exitCode: exit_codes.USAGE,
+ );
+ });
+
+ test('Can add both dev and regular dependencies', () async {
+ final server = await servePackages();
+ server.serve('foo', '1.2.3');
+ server.serve('bar', '1.2.3');
+
+ await d.dir(appPath, [
+ d.pubspec({'name': 'myapp', 'dev_dependencies': {}})
+ ]).create();
+
+ await pubAdd(args: ['dev:foo:1.2.3', 'bar:1.2.3']);
+
+ await d.appPackageConfigFile([
+ d.packageConfigEntry(name: 'foo', version: '1.2.3'),
+ d.packageConfigEntry(name: 'bar', version: '1.2.3'),
+ ]).validate();
+
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'dependencies': {'bar': '1.2.3'},
+ 'dev_dependencies': {'foo': '1.2.3'},
+ })
+ ]).validate();
+ });
+
group('notifies user if package exists', () {
test('if package is added without a version constraint', () async {
await servePackages()
diff --git a/test/add/git/git_test.dart b/test/add/git/git_test.dart
index d976172..9890835 100644
--- a/test/add/git/git_test.dart
+++ b/test/add/git/git_test.dart
@@ -4,7 +4,6 @@
import 'package:pub/src/exit_codes.dart' as exit_codes;
import 'package:test/test.dart';
-
import '../../descriptor.dart' as d;
import '../../test_pub.dart';
@@ -190,14 +189,59 @@
test('fails if multiple packages passed for git source', () async {
ensureGit();
- await d.git(
- 'foo.git', [d.libDir('foo'), d.libPubspec('foo', '1.0.0')]).create();
-
await d.appDir({}).create();
await pubAdd(
args: ['foo', 'bar', 'baz', '--git-url', '../foo.git'],
exitCode: exit_codes.USAGE,
- error: contains('Can only add a single git package at a time.'));
+ error: contains('Specify multiple git packages with descriptors.'));
+ });
+
+ test('Can add a package with a git descriptor and relative path', () async {
+ await d.git('foo.git', [
+ d.dir('subdir', [d.libPubspec('foo', '1.2.3')])
+ ]).create();
+ await d.appDir({}).create();
+ await pubAdd(
+ args: [
+ '--directory',
+ appPath,
+ 'foo:{"git": {"url":"foo.git", "path":"subdir"}}',
+ ],
+ workingDirectory: d.sandbox,
+ output: contains('Changed 1 dependency in myapp!'),
+ );
+
+ await d.appDir({
+ 'foo': {
+ 'git': {'url': '../foo.git', 'path': 'subdir'}
+ }
+ }).validate();
+ });
+
+ test('Can add multiple git packages using descriptors', () async {
+ ensureGit();
+
+ await d.git(
+ 'foo.git', [d.libDir('foo'), d.libPubspec('foo', '1.0.0')]).create();
+ await d.git(
+ 'bar.git', [d.libDir('foo'), d.libPubspec('bar', '1.0.0')]).create();
+
+ await d.appDir({}).create();
+
+ await pubAdd(args: [
+ 'foo:{"git":"../foo.git"}',
+ 'bar:{"git":"../bar.git"}',
+ ]);
+
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'dependencies': {
+ 'foo': {'git': '../foo.git'},
+ 'bar': {'git': '../bar.git'},
+ },
+ })
+ ]).validate();
});
}
diff --git a/test/add/path/absolute_path_test.dart b/test/add/path/absolute_path_test.dart
index 5e63679..afdc3f7 100644
--- a/test/add/path/absolute_path_test.dart
+++ b/test/add/path/absolute_path_test.dart
@@ -54,7 +54,7 @@
await pubAdd(
args: ['foo:2.0.0', 'bar:0.1.3', 'baz:1.3.1', '--path', absolutePath],
- error: contains('Can only add a single local package at a time.'),
+ error: contains('--path cannot be used with multiple packages.'),
exitCode: exit_codes.USAGE);
await d.appDir({}).validate();
diff --git a/test/add/path/relative_path_test.dart b/test/add/path/relative_path_test.dart
index e08ba3c..a25e557 100644
--- a/test/add/path/relative_path_test.dart
+++ b/test/add/path/relative_path_test.dart
@@ -28,6 +28,26 @@
}).validate();
});
+ test('can use relative path with a path descriptor', () async {
+ await d
+ .dir('foo', [d.libDir('foo'), d.libPubspec('foo', '1.2.3')]).create();
+
+ await d.appDir().create();
+
+ await pubAdd(
+ args: ['dev:foo:{"path":"../foo"}'],
+ );
+
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'dev_dependencies': {
+ 'foo': {'path': '../foo'}
+ }
+ })
+ ]).validate();
+ });
+
test('can use relative path with --directory', () async {
await d
.dir('foo', [d.libDir('foo'), d.libPubspec('foo', '0.0.1')]).create();
@@ -135,4 +155,34 @@
})
]).validate();
});
+
+ test('Can add multiple path packages using descriptors', () async {
+ await d
+ .dir('foo', [d.libDir('foo'), d.libPubspec('foo', '0.0.1')]).create();
+ await d
+ .dir('bar', [d.libDir('bar'), d.libPubspec('bar', '0.0.1')]).create();
+
+ await d.appDir({}).create();
+
+ await pubAdd(
+ args: [
+ '--directory',
+ appPath,
+ 'foo:{"path":"foo"}',
+ 'bar:{"path":"bar"}',
+ ],
+ workingDirectory: d.sandbox,
+ output: contains('Changed 2 dependencies in myapp!'),
+ );
+
+ await d.appPackageConfigFile([
+ d.packageConfigEntry(name: 'foo', path: '../foo'),
+ d.packageConfigEntry(name: 'bar', path: '../bar'),
+ ]).validate();
+
+ await d.appDir({
+ 'foo': {'path': '../foo'},
+ 'bar': {'path': '../bar'},
+ }).validate();
+ });
}
diff --git a/test/test_pub.dart b/test/test_pub.dart
index a932b6b..57dc3a0 100644
--- a/test/test_pub.dart
+++ b/test/test_pub.dart
@@ -871,9 +871,7 @@
// Any paths in output should be relative to the sandbox and with forward
// slashes to be stable across platforms.
.map((line) {
- line = line
- .replaceAll(d.sandbox, r'$SANDBOX')
- .replaceAll(Platform.pathSeparator, '/');
+ line = line.replaceAll(d.sandbox, r'$SANDBOX').replaceAll(r'\', '/');
var port = _globalServer?.port;
if (port != null) {
line = line.replaceAll(port.toString(), '\$PORT');
diff --git a/test/testdata/goldens/embedding/embedding_test/--help.txt b/test/testdata/goldens/embedding/embedding_test/--help.txt
index 13647a0..780b29e 100644
--- a/test/testdata/goldens/embedding/embedding_test/--help.txt
+++ b/test/testdata/goldens/embedding/embedding_test/--help.txt
@@ -13,7 +13,7 @@
(defaults to ".")
Available subcommands:
- add Add dependencies to pubspec.yaml.
+ add Add dependencies to `pubspec.yaml`.
cache Work with the system cache.
deps Print package dependencies.
downgrade Downgrade the current package's dependencies to oldest versions.
diff --git a/test/testdata/goldens/help_test/pub add --help.txt b/test/testdata/goldens/help_test/pub add --help.txt
index 2a37c21..9aa4016 100644
--- a/test/testdata/goldens/help_test/pub add --help.txt
+++ b/test/testdata/goldens/help_test/pub add --help.txt
@@ -2,18 +2,40 @@
## Section 0
$ pub add --help
-Add dependencies to pubspec.yaml.
+Add dependencies to `pubspec.yaml`.
-Usage: pub add <package>[:<constraint>] [<package2>[:<constraint2>]...] [options]
+Invoking `dart pub add foo bar` will add `foo` and `bar` to `pubspec.yaml`
+with a default constraint derived from latest compatible version.
+
+Add to dev_dependencies by prefixing with "dev:".
+
+Add packages with specific constraints or other sources by giving a descriptor
+after a colon.
+
+For example:
+ * Add a hosted dependency at newest compatible stable version:
+ `$topLevelProgram pub add foo`
+ * Add a hosted dev dependency at newest compatible stable version:
+ `$topLevelProgram pub add dev:foo`
+ * Add a hosted dependency with the given constraint
+ `$topLevelProgram pub add foo:^1.2.3`
+ * Add multiple dependencies:
+ `$topLevelProgram pub add foo dev:bar`
+ * Add a path dependency:
+ `$topLevelProgram pub add 'foo{"path":"../foo"}'`
+ * Add a hosted dependency:
+ `$topLevelProgram pub add 'foo{"hosted":"my-pub.dev"}'`
+ * Add an sdk dependency:
+ `$topLevelProgram pub add 'foo{"sdk":"flutter"}'`
+ * Add a git dependency:
+ `$topLevelProgram pub add 'foo{"git":"https://github.com/foo/foo"}'`
+ * Add a git dependency with a path and ref specified:
+ `$topLevelProgram pub add /
+ 'foo{"git":{"url":"../foo.git","ref":"branch","path":"subdir"}}'`
+
+Usage: pub add [options] [dev:]<package>[:descriptor] [[dev:]<package>[:descriptor]
+ ...]
-h, --help Print this usage information.
--d, --dev Adds to the development dependencies instead.
- --git-url Git URL of the package
- --git-ref Git branch or commit to be retrieved
- --git-path Path of git package in repository
- --hosted-url URL of package host server
- --path Add package from local path
- --sdk=<[flutter]> add package from SDK source
- [flutter]
--[no-]offline Use cached packages instead of accessing the network.
-n, --dry-run Report what dependencies would change but don't change
any.