Shorthand syntax for hosted dependencies (#3133)
If you have language-version 2.15 or higher you can specify hosted dependencies like:
```
dependencies:
foo:
hosted: <url>
bar: # alternatively
hosted:
url: <url>
```
Before you had to write:
```
dependencies:
foo:
hosted:
name: foo
url: <url>
```
diff --git a/lib/src/command/add.dart b/lib/src/command/add.dart
index b481fe4..09cc426 100644
--- a/lib/src/command/add.dart
+++ b/lib/src/command/add.dart
@@ -389,8 +389,13 @@
pubspecInformation = {'hosted': hostInfo};
}
- packageRange = PackageRange(packageName, cache.sources['hosted'],
- constraint ?? VersionConstraint.any, hostInfo ?? packageName);
+ packageRange = cache.hosted.source
+ .parseRef(
+ packageName,
+ hostInfo,
+ languageVersion: entrypoint.root.pubspec.languageVersion,
+ )
+ .withConstraint(constraint ?? VersionConstraint.any);
}
if (pubspecInformation is Map && constraint != null) {
diff --git a/lib/src/pubspec.dart b/lib/src/pubspec.dart
index b700638..cc75b2d 100644
--- a/lib/src/pubspec.dart
+++ b/lib/src/pubspec.dart
@@ -441,10 +441,8 @@
VersionConstraint versionConstraint = VersionRange();
var features = const <String, FeatureDependency>{};
if (spec == null) {
- descriptionNode = nameNode;
sourceName = _sources!.defaultSource.name;
} else if (spec is String) {
- descriptionNode = nameNode;
sourceName = _sources!.defaultSource.name;
versionConstraint = _parseVersionConstraint(specNode);
} else if (spec is Map) {
@@ -468,7 +466,6 @@
} else if (sourceNames.isEmpty) {
// Default to a hosted dependency if no source is specified.
sourceName = 'hosted';
- descriptionNode = nameNode;
}
sourceName ??= sourceNames.single;
@@ -491,8 +488,12 @@
pubspecPath = path.fromUri(_location);
}
- return _sources![sourceName]!.parseRef(name, descriptionNode?.value,
- containingPath: pubspecPath);
+ return _sources![sourceName]!.parseRef(
+ name,
+ descriptionNode?.value,
+ containingPath: pubspecPath,
+ languageVersion: languageVersion,
+ );
}, targetPackage: name);
dependencies[name] =
@@ -611,6 +612,9 @@
try {
return fn();
} on FormatException catch (e) {
+ // If we already have a pub exception with a span, re-use that
+ if (e is PubspecException) rethrow;
+
var msg = 'Invalid $description';
if (targetPackage != null) {
msg = '$msg in the "$name" pubspec on the "$targetPackage" dependency';
diff --git a/lib/src/source.dart b/lib/src/source.dart
index 53c694d..add55fa 100644
--- a/lib/src/source.dart
+++ b/lib/src/source.dart
@@ -8,6 +8,7 @@
import 'package:pub_semver/pub_semver.dart';
import 'exceptions.dart';
+import 'language_version.dart';
import 'package_name.dart';
import 'pubspec.dart';
import 'system_cache.dart';
@@ -76,16 +77,27 @@
/// should be interpreted. This will be called during parsing to validate that
/// the given [description] is well-formed according to this source, and to
/// give the source a chance to canonicalize the description.
+ /// For simple hosted dependencies like `foo:` or `foo: ^1.2.3`, the
+ /// [description] may also be `null`.
///
/// [containingPath] is the path to the pubspec where this description
/// appears. It may be `null` if the description is coming from some in-memory
/// source (such as pulling down a pubspec from pub.dartlang.org).
///
+ /// [languageVersion] is the minimum Dart version parsed from the pubspec's
+ /// `environment` field. Source implementations may use this parameter to only
+ /// support specific syntax for some versions.
+ ///
/// The description in the returned [PackageRef] need bear no resemblance to
/// the original user-provided description.
///
/// Throws a [FormatException] if the description is not valid.
- PackageRef parseRef(String name, description, {String? containingPath});
+ PackageRef parseRef(
+ String name,
+ description, {
+ String? containingPath,
+ required LanguageVersion languageVersion,
+ });
/// Parses a [PackageId] from a name and a serialized description.
///
diff --git a/lib/src/source/git.dart b/lib/src/source/git.dart
index 161c540..0d98ae9 100644
--- a/lib/src/source/git.dart
+++ b/lib/src/source/git.dart
@@ -12,6 +12,7 @@
import '../git.dart' as git;
import '../io.dart';
+import '../language_version.dart';
import '../log.dart' as log;
import '../package.dart';
import '../package_name.dart';
@@ -43,7 +44,12 @@
}
@override
- PackageRef parseRef(String name, description, {String? containingPath}) {
+ PackageRef parseRef(
+ String name,
+ description, {
+ String? containingPath,
+ LanguageVersion? languageVersion,
+ }) {
dynamic url;
dynamic ref;
dynamic path;
diff --git a/lib/src/source/hosted.dart b/lib/src/source/hosted.dart
index 803bbc5..cfe582a 100644
--- a/lib/src/source/hosted.dart
+++ b/lib/src/source/hosted.dart
@@ -18,6 +18,7 @@
import '../exceptions.dart';
import '../http.dart';
import '../io.dart';
+import '../language_version.dart';
import '../log.dart' as log;
import '../package.dart';
import '../package_name.dart';
@@ -129,19 +130,19 @@
/// should be downloaded. [url] most be normalized and validated using
/// [validateAndNormalizeHostedUrl].
PackageRef refFor(String name, {Uri? url}) =>
- PackageRef(name, this, _descriptionFor(name, url));
+ PackageRef(name, this, _HostedDescription(name, url ?? defaultUrl));
/// Returns an ID for a hosted package named [name] at [version].
///
/// If [url] is passed, it's the URL of the pub server from which the package
/// should be downloaded. [url] most be normalized and validated using
/// [validateAndNormalizeHostedUrl].
- PackageId idFor(String name, Version version, {Uri? url}) =>
- PackageId(name, this, version, _descriptionFor(name, url));
+ PackageId idFor(String name, Version version, {Uri? url}) => PackageId(
+ name, this, version, _HostedDescription(name, url ?? defaultUrl));
/// Returns the description for a hosted package named [name] with the
/// given package server [url].
- dynamic _descriptionFor(String name, [Uri? url]) {
+ dynamic _serializedDescriptionFor(String name, [Uri? url]) {
if (url == null) {
return name;
}
@@ -154,54 +155,119 @@
}
@override
+ dynamic serializeDescription(String containingPath, description) {
+ final desc = _asDescription(description);
+ return _serializedDescriptionFor(desc.packageName, desc.uri);
+ }
+
+ @override
String formatDescription(description) =>
- 'on ${_parseDescription(description).last}';
+ 'on ${_asDescription(description).uri}';
@override
bool descriptionsEqual(description1, description2) =>
- _parseDescription(description1) == _parseDescription(description2);
+ _asDescription(description1) == _asDescription(description2);
@override
- int hashDescription(description) => _parseDescription(description).hashCode;
+ int hashDescription(description) => _asDescription(description).hashCode;
/// Ensures that [description] is a valid hosted package description.
///
- /// There are two valid formats. A plain string refers to a package with the
- /// given name from the default host, while a map with keys "name" and "url"
- /// refers to a package with the given name from the host at the given URL.
+ /// Simple hosted dependencies only consist of a plain string, which is
+ /// resolved against the default host. In this case, [description] will be
+ /// null.
+ ///
+ /// Hosted dependencies may also specify a custom host from which the package
+ /// is fetched. There are two syntactic forms of those dependencies:
+ ///
+ /// 1. With an url and an optional name in a map: `hosted: {url: <url>}`
+ /// 2. With a direct url: `hosted: <url>`
@override
- PackageRef parseRef(String name, description, {String? containingPath}) {
- _parseDescription(description);
- return PackageRef(name, this, description);
+ PackageRef parseRef(String name, description,
+ {String? containingPath, required LanguageVersion languageVersion}) {
+ return PackageRef(
+ name, this, _parseDescription(name, description, languageVersion));
}
@override
PackageId parseId(String name, Version version, description,
{String? containingPath}) {
- _parseDescription(description);
- return PackageId(name, this, version, description);
+ // Old pub versions only wrote `description: <pkg>` into the lock file.
+ if (description is String) {
+ if (description != name) {
+ throw FormatException('The description should be the same as the name');
+ }
+ return PackageId(
+ name, this, version, _HostedDescription(name, defaultUrl));
+ }
+
+ final serializedDescription = (description as Map).cast<String, String>();
+
+ return PackageId(
+ name,
+ this,
+ version,
+ _HostedDescription(serializedDescription['name']!,
+ Uri.parse(serializedDescription['url']!)),
+ );
}
+ _HostedDescription _asDescription(desc) => desc as _HostedDescription;
+
/// Parses the description for a package.
///
/// If the package parses correctly, this returns a (name, url) pair. If not,
/// this throws a descriptive FormatException.
- Pair<String, Uri> _parseDescription(description) {
+ _HostedDescription _parseDescription(
+ String packageName,
+ description,
+ LanguageVersion languageVersion,
+ ) {
+ if (description == null) {
+ // Simple dependency without a `hosted` block, use the default server.
+ return _HostedDescription(packageName, defaultUrl);
+ }
+
+ final canUseShorthandSyntax =
+ languageVersion >= _minVersionForShorterHostedSyntax;
+
if (description is String) {
- return Pair<String, Uri>(description, defaultUrl);
+ // Old versions of pub (pre Dart 2.15) interpret `hosted: foo` as
+ // `hosted: {name: foo, url: <default>}`.
+ // For later versions, we treat it as `hosted: {name: <inferred>,
+ // url: foo}` if a user opts in by raising their min SDK environment.
+ //
+ // Since the old behavior is very rarely used and we want to show a
+ // helpful error message if the new syntax is used without raising the SDK
+ // environment, we throw an error if something that looks like a URI is
+ // used as a package name.
+ if (canUseShorthandSyntax) {
+ return _HostedDescription(
+ packageName, validateAndNormalizeHostedUrl(description));
+ } else {
+ if (_looksLikePackageName.hasMatch(description)) {
+ // Valid use of `hosted: package` dependency with an old SDK
+ // environment.
+ return _HostedDescription(description, defaultUrl);
+ } else {
+ throw FormatException(
+ 'Using `hosted: <url>` is only supported with a minimum SDK '
+ 'constraint of $_minVersionForShorterHostedSyntax.',
+ );
+ }
+ }
}
if (description is! Map) {
throw FormatException('The description must be a package name or map.');
}
- if (!description.containsKey('name')) {
- throw FormatException("The description map must contain a 'name' key.");
- }
-
var name = description['name'];
+ if (canUseShorthandSyntax) name ??= packageName;
+
if (name is! String) {
- throw FormatException("The 'name' key must have a string value.");
+ throw FormatException("The 'name' key must have a string value without "
+ 'a minimum Dart SDK constraint of $_minVersionForShorterHostedSyntax.0 or higher.');
}
var url = defaultUrl;
@@ -213,8 +279,26 @@
url = validateAndNormalizeHostedUrl(u);
}
- return Pair<String, Uri>(name, url);
+ return _HostedDescription(name, url);
}
+
+ /// Minimum language version at which short hosted syntax is supported.
+ ///
+ /// This allows `hosted` dependencies to be expressed as:
+ /// ```yaml
+ /// dependencies:
+ /// foo:
+ /// hosted: https://some-pub.com/path
+ /// version: ^1.0.0
+ /// ```
+ ///
+ /// At older versions, `hosted` dependencies had to be a map with a `url` and
+ /// a `name` key.
+ static const LanguageVersion _minVersionForShorterHostedSyntax =
+ LanguageVersion(2, 15);
+
+ static final RegExp _looksLikePackageName =
+ RegExp(r'^[a-zA-Z_]+[a-zA-Z0-9_]*$');
}
/// Information about a package version retrieved from /api/packages/$package
@@ -226,6 +310,28 @@
_VersionInfo(this.pubspec, this.archiveUrl, this.status);
}
+/// The [PackageName.description] for a [HostedSource], storing the package name
+/// and resolved URI of the package server.
+class _HostedDescription {
+ final String packageName;
+ final Uri uri;
+
+ _HostedDescription(this.packageName, this.uri) {
+ ArgumentError.checkNotNull(packageName, 'packageName');
+ ArgumentError.checkNotNull(uri, 'uri');
+ }
+
+ @override
+ int get hashCode => Object.hash(packageName, uri);
+
+ @override
+ bool operator ==(Object other) {
+ return other is _HostedDescription &&
+ other.packageName == packageName &&
+ other.uri == uri;
+ }
+}
+
/// The [BoundSource] for [HostedSource].
class BoundHostedSource extends CachedSource {
@override
@@ -277,9 +383,9 @@
final url = _listVersionsUrl(ref.description);
log.io('Get versions from $url.');
- String bodyText;
- Map<String, dynamic>? body;
- Map<PackageId, _VersionInfo>? result;
+ late final String bodyText;
+ late final dynamic body;
+ late final Map<PackageId, _VersionInfo> result;
try {
// TODO(sigurdm): Implement cancellation of requests. This probably
// requires resolution of: https://github.com/dart-lang/sdk/issues/22265.
@@ -288,16 +394,20 @@
serverUrl,
(client) => client.read(url, headers: pubApiHeaders),
);
- body = jsonDecode(bodyText);
- result = _versionInfoFromPackageListing(body!, ref, url);
+ final decoded = jsonDecode(bodyText);
+ if (decoded is! Map<String, dynamic>) {
+ throw FormatException('version listing must be a mapping');
+ }
+ body = decoded;
+ result = _versionInfoFromPackageListing(body, ref, url);
} on Exception catch (error, stackTrace) {
- var parsed = source._parseDescription(ref.description);
- _throwFriendlyError(error, stackTrace, parsed.first, parsed.last);
+ var parsed = source._asDescription(ref.description);
+ _throwFriendlyError(error, stackTrace, parsed.packageName, parsed.uri);
}
// Cache the response on disk.
// Don't cache overly big responses.
- if (body!.length < 100 * 1024) {
+ if (bodyText.length < 100 * 1024) {
await _cacheVersionListingResponse(body, ref);
}
return result;
@@ -466,8 +576,8 @@
// The path where the response from the package-listing api is cached.
String _versionListingCachePath(PackageRef ref) {
- final parsed = source._parseDescription(ref.description);
- final dir = _urlToDirectory(parsed.last);
+ final parsed = source._asDescription(ref.description);
+ final dir = _urlToDirectory(parsed.uri);
// Use a dot-dir because older versions of pub won't choke on that
// name when iterating the cache (it is not listed by [listDir]).
return p.join(systemCacheRoot, dir, _versionListingDirectory,
@@ -494,16 +604,16 @@
/// Parses [description] into its server and package name components, then
/// converts that to a Uri for listing versions of the given package.
Uri _listVersionsUrl(description) {
- final parsed = source._parseDescription(description);
- final hostedUrl = parsed.last;
- final package = Uri.encodeComponent(parsed.first);
+ final parsed = source._asDescription(description);
+ final hostedUrl = parsed.uri;
+ final package = Uri.encodeComponent(parsed.packageName);
return hostedUrl.resolve('api/packages/$package');
}
/// Parses [description] into server name component.
Uri _hostedUrl(description) {
- final parsed = source._parseDescription(description);
- return parsed.last;
+ final parsed = source._asDescription(description);
+ return parsed.uri;
}
/// Retrieves the pubspec for a specific version of a package that is
@@ -535,9 +645,9 @@
/// package downloaded from that site.
@override
String getDirectoryInCache(PackageId id) {
- var parsed = source._parseDescription(id.description);
- var dir = _urlToDirectory(parsed.last);
- return p.join(systemCacheRoot, dir, '${parsed.first}-${id.version}');
+ var parsed = source._asDescription(id.description);
+ var dir = _urlToDirectory(parsed.uri);
+ return p.join(systemCacheRoot, dir, '${parsed.packageName}-${id.version}');
}
/// Re-downloads all packages that have been previously downloaded into the
@@ -675,8 +785,8 @@
throw PackageNotFoundException(
'Package $packageName has no version $version');
}
- final parsedDescription = source._parseDescription(id.description);
- final server = parsedDescription.last;
+ final parsedDescription = source._asDescription(id.description);
+ final server = parsedDescription.uri;
var url = versionInfo.archiveUrl;
log.io('Get package from $url.');
@@ -713,7 +823,7 @@
/// this tries to translate into a more user friendly error message.
///
/// Always throws an error, either the original one or a better one.
- void _throwFriendlyError(
+ Never _throwFriendlyError(
error,
StackTrace stackTrace,
String package,
@@ -809,7 +919,7 @@
}
/// Returns the server URL for [description].
- Uri _serverFor(description) => source._parseDescription(description).last;
+ Uri _serverFor(description) => source._asDescription(description).uri;
/// Enables speculative prefetching of dependencies of packages queried with
/// [getVersions].
@@ -836,9 +946,11 @@
/// Gets the list of all versions of [ref] that are in the system cache.
@override
Future<List<PackageId>> doGetVersions(
- PackageRef ref, Duration? maxAge) async {
- var parsed = source._parseDescription(ref.description);
- var server = parsed.last;
+ PackageRef ref,
+ Duration? maxAge,
+ ) async {
+ var parsed = source._asDescription(ref.description);
+ var server = parsed.uri;
log.io('Finding versions of ${ref.name} in '
'$systemCacheRoot/${_urlToDirectory(server)}');
diff --git a/lib/src/source/path.dart b/lib/src/source/path.dart
index 545eae8..0401ebd 100644
--- a/lib/src/source/path.dart
+++ b/lib/src/source/path.dart
@@ -9,6 +9,7 @@
import '../exceptions.dart';
import '../io.dart';
+import '../language_version.dart';
import '../package_name.dart';
import '../pubspec.dart';
import '../source.dart';
@@ -62,7 +63,12 @@
/// original path but resolved relative to the containing path. The
/// "relative" key will be `true` if the original path was relative.
@override
- PackageRef parseRef(String name, description, {String? containingPath}) {
+ PackageRef parseRef(
+ String name,
+ description, {
+ String? containingPath,
+ LanguageVersion? languageVersion,
+ }) {
if (description is! String) {
throw FormatException('The description must be a path string.');
}
diff --git a/lib/src/source/sdk.dart b/lib/src/source/sdk.dart
index 3ff93fe..f1dde9e 100644
--- a/lib/src/source/sdk.dart
+++ b/lib/src/source/sdk.dart
@@ -7,6 +7,7 @@
import 'package:pub_semver/pub_semver.dart';
import '../exceptions.dart';
+import '../language_version.dart';
import '../package_name.dart';
import '../pubspec.dart';
import '../sdk.dart';
@@ -33,7 +34,8 @@
/// Parses an SDK dependency.
@override
- PackageRef parseRef(String name, description, {String? containingPath}) {
+ PackageRef parseRef(String name, description,
+ {String? containingPath, LanguageVersion? languageVersion}) {
if (description is! String) {
throw FormatException('The description must be an SDK name.');
}
diff --git a/lib/src/source/unknown.dart b/lib/src/source/unknown.dart
index eaf94fd..baa46ea 100644
--- a/lib/src/source/unknown.dart
+++ b/lib/src/source/unknown.dart
@@ -6,6 +6,7 @@
import 'package:pub_semver/pub_semver.dart';
+import '../language_version.dart';
import '../package_name.dart';
import '../pubspec.dart';
import '../source.dart';
@@ -42,7 +43,12 @@
int hashDescription(description) => description.hashCode;
@override
- PackageRef parseRef(String name, description, {String? containingPath}) =>
+ PackageRef parseRef(
+ String name,
+ description, {
+ String? containingPath,
+ LanguageVersion? languageVersion,
+ }) =>
PackageRef(name, this, description);
@override
diff --git a/test/hosted/short_syntax_test.dart b/test/hosted/short_syntax_test.dart
new file mode 100644
index 0000000..bfbb3b4
--- /dev/null
+++ b/test/hosted/short_syntax_test.dart
@@ -0,0 +1,89 @@
+// Copyright (c) 2021, 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.
+
+// @dart=2.10
+
+import 'dart:io';
+
+import 'package:path/path.dart' as p;
+import 'package:test/test.dart';
+import 'package:yaml/yaml.dart';
+
+import '../descriptor.dart' as d;
+import '../test_pub.dart';
+
+void main() {
+ setUp(() => servePackages((b) => b.serve('foo', '1.2.3', pubspec: {
+ 'environment': {'sdk': '^2.0.0'}
+ })));
+
+ forBothPubGetAndUpgrade((command) {
+ Future<void> testWith(dynamic dependency) async {
+ await d.dir(appPath, [
+ d.libPubspec(
+ 'app',
+ '1.0.0',
+ deps: {'foo': dependency},
+ sdk: '^2.15.0',
+ ),
+ ]).create();
+
+ await pubCommand(
+ command,
+ exitCode: 0,
+ environment: {'_PUB_TEST_SDK_VERSION': '2.15.0'},
+ );
+
+ final lockFile = loadYaml(
+ await File(p.join(d.sandbox, appPath, 'pubspec.lock')).readAsString(),
+ );
+
+ expect(lockFile['packages']['foo'], {
+ 'dependency': 'direct main',
+ 'source': 'hosted',
+ 'description': {
+ 'name': 'foo',
+ 'url': globalPackageServer.url,
+ },
+ 'version': '1.2.3',
+ });
+ }
+
+ test('supports hosted: <url> syntax', () async {
+ return testWith({'hosted': globalPackageServer.url});
+ });
+
+ test('supports hosted map without name', () {
+ return testWith({
+ 'hosted': {'url': globalPackageServer.url},
+ });
+ });
+
+ test('interprets hosted string as name for older versions', () async {
+ await d.dir(appPath, [
+ d.libPubspec(
+ 'app',
+ '1.0.0',
+ deps: {
+ 'foo': {'hosted': 'foo', 'version': '^1.2.3'}
+ },
+ sdk: '^2.0.0',
+ ),
+ ]).create();
+
+ await pubCommand(
+ command,
+ exitCode: 0,
+ environment: {'_PUB_TEST_SDK_VERSION': '2.15.0'},
+ );
+
+ final lockFile = loadYaml(
+ await File(p.join(d.sandbox, appPath, 'pubspec.lock')).readAsString(),
+ );
+
+ expect(lockFile['packages']['foo']['description']['url'],
+ globalPackageServer.url);
+ });
+ });
+}
diff --git a/test/lock_file_test.dart b/test/lock_file_test.dart
index 18edc94..2faf8c5 100644
--- a/test/lock_file_test.dart
+++ b/test/lock_file_test.dart
@@ -4,6 +4,7 @@
// @dart=2.10
+import 'package:pub/src/language_version.dart';
import 'package:pub/src/lock_file.dart';
import 'package:pub/src/package_name.dart';
import 'package:pub/src/source.dart';
@@ -22,7 +23,8 @@
throw UnsupportedError('Cannot download fake packages.');
@override
- PackageRef parseRef(String name, description, {String containingPath}) {
+ PackageRef parseRef(String name, description,
+ {String containingPath, LanguageVersion languageVersion}) {
if (!description.endsWith(' desc')) throw FormatException('Bad');
return PackageRef(name, this, description);
}
diff --git a/test/pubspec_test.dart b/test/pubspec_test.dart
index 5785b16..c293d85 100644
--- a/test/pubspec_test.dart
+++ b/test/pubspec_test.dart
@@ -4,6 +4,7 @@
// @dart=2.10
+import 'package:pub/src/language_version.dart';
import 'package:pub/src/package_name.dart';
import 'package:pub/src/pubspec.dart';
import 'package:pub/src/sdk.dart';
@@ -22,7 +23,8 @@
throw UnsupportedError('Cannot download fake packages.');
@override
- PackageRef parseRef(String name, description, {String containingPath}) {
+ PackageRef parseRef(String name, description,
+ {String containingPath, LanguageVersion languageVersion}) {
if (description != 'ok') throw FormatException('Bad');
return PackageRef(name, this, description);
}
@@ -294,6 +296,170 @@
'local pubspec.');
});
+ group('source dependencies', () {
+ test('with url and name', () {
+ var pubspec = Pubspec.parse(
+ '''
+name: pkg
+dependencies:
+ foo:
+ hosted:
+ url: https://example.org/pub/
+ name: bar
+''',
+ sources,
+ );
+
+ var foo = pubspec.dependencies['foo'];
+ expect(foo.name, equals('foo'));
+ expect(foo.source.name, 'hosted');
+ expect(foo.source.serializeDescription(null, foo.description), {
+ 'url': 'https://example.org/pub/',
+ 'name': 'bar',
+ });
+ });
+
+ test('with url only', () {
+ var pubspec = Pubspec.parse(
+ '''
+name: pkg
+environment:
+ sdk: ^2.15.0
+dependencies:
+ foo:
+ hosted:
+ url: https://example.org/pub/
+''',
+ sources,
+ );
+
+ var foo = pubspec.dependencies['foo'];
+ expect(foo.name, equals('foo'));
+ expect(foo.source.name, 'hosted');
+ expect(foo.source.serializeDescription(null, foo.description), {
+ 'url': 'https://example.org/pub/',
+ 'name': 'foo',
+ });
+ });
+
+ test('with url as string', () {
+ var pubspec = Pubspec.parse(
+ '''
+name: pkg
+environment:
+ sdk: ^2.15.0
+dependencies:
+ foo:
+ hosted: https://example.org/pub/
+''',
+ sources,
+ );
+
+ var foo = pubspec.dependencies['foo'];
+ expect(foo.name, equals('foo'));
+ expect(foo.source.name, 'hosted');
+ expect(foo.source.serializeDescription(null, foo.description), {
+ 'url': 'https://example.org/pub/',
+ 'name': 'foo',
+ });
+ });
+
+ test('interprets string description as name for older versions', () {
+ var pubspec = Pubspec.parse(
+ '''
+name: pkg
+environment:
+ sdk: ^2.14.0
+dependencies:
+ foo:
+ hosted: bar
+''',
+ sources,
+ );
+
+ var foo = pubspec.dependencies['foo'];
+ expect(foo.name, equals('foo'));
+ expect(foo.source.name, 'hosted');
+ expect(foo.source.serializeDescription(null, foo.description), {
+ 'url': 'https://pub.dartlang.org',
+ 'name': 'bar',
+ });
+ });
+
+ test(
+ 'reports helpful span when using new syntax with invalid environment',
+ () {
+ var pubspec = Pubspec.parse('''
+name: pkg
+environment:
+ sdk: invalid value
+dependencies:
+ foo:
+ hosted: https://example.org/pub/
+''', sources);
+
+ expect(
+ () => pubspec.dependencies,
+ throwsA(
+ isA<PubspecException>()
+ .having((e) => e.span.text, 'span.text', 'invalid value'),
+ ),
+ );
+ },
+ );
+
+ test('without a description', () {
+ var pubspec = Pubspec.parse(
+ '''
+name: pkg
+dependencies:
+ foo:
+''',
+ sources,
+ );
+
+ var foo = pubspec.dependencies['foo'];
+ expect(foo.name, equals('foo'));
+ expect(foo.source.name, 'hosted');
+ expect(foo.source.serializeDescription(null, foo.description), {
+ 'url': 'https://pub.dartlang.org',
+ 'name': 'foo',
+ });
+ });
+
+ group('throws without a min SDK constraint', () {
+ test('and without a name', () {
+ expectPubspecException(
+ '''
+name: pkg
+dependencies:
+ foo:
+ hosted:
+ url: https://example.org/pub/
+''',
+ (pubspec) => pubspec.dependencies,
+ "The 'name' key must have a string value without a minimum Dart "
+ 'SDK constraint of 2.15.');
+ });
+
+ test(
+ 'and a hosted: <value> syntax that looks like an URI was meant',
+ () {
+ expectPubspecException(
+ '''
+name: pkg
+dependencies:
+ foo:
+ hosted: http://pub.example.org
+''',
+ (pubspec) => pubspec.dependencies,
+ 'Using `hosted: <url>` is only supported with a minimum SDK constraint of 2.15.',
+ );
+ },
+ );
+ });
+ });
+
group('git dependencies', () {
test('path must be a string', () {
expectPubspecException('''
diff --git a/test/version_solver_test.dart b/test/version_solver_test.dart
index caca301..cf6f65f 100644
--- a/test/version_solver_test.dart
+++ b/test/version_solver_test.dart
@@ -3003,8 +3003,9 @@
for (var dep in resultPubspec.dependencies.values) {
expect(ids, contains(dep.name));
var id = ids.remove(dep.name);
+ final source = dep.source;
- if (dep.source is HostedSource && dep.description is String) {
+ if (source is HostedSource && (dep.description.uri == source.defaultUrl)) {
// If the dep uses the default hosted source, grab it from the test
// package server rather than pub.dartlang.org.
dep = registry.hosted