diff --git a/app/lib/frontend/templates/views/pkg/info_box.dart b/app/lib/frontend/templates/views/pkg/info_box.dart index 0622035f87..1ba471d7b4 100644 --- a/app/lib/frontend/templates/views/pkg/info_box.dart +++ b/app/lib/frontend/templates/views/pkg/info_box.dart @@ -3,6 +3,7 @@ // BSD-style license that can be found in the LICENSE file. import 'package:_pub_shared/format/encoding.dart'; +import 'package:_pub_shared/search/tags.dart'; import 'package:pana/pana.dart'; import 'package:pub_dev/service/download_counts/download_counts.dart'; import 'package:pubspec_parse/pubspec_parse.dart' as pubspek; @@ -105,7 +106,10 @@ d.Node packageInfoBoxNode({ ), if (license != null) _block('License', license), if (dependencies != null) _block('Dependencies', dependencies), - _more(package.name!), + _more( + package.name!, + showImplementsLink: data.version.pubspec?.hasFlutterPlugin ?? false, + ), ]); } @@ -160,14 +164,26 @@ d.Node _metadata({ ]); } -d.Node _more(String packageName) { +d.Node _more(String packageName, {required bool showImplementsLink}) { return _block( 'More', - d.a( - href: urls.searchUrl(q: 'dependency:$packageName'), - rel: 'nofollow', - text: 'Packages that depend on $packageName', - ), + d.fragment([ + d.a( + href: urls.searchUrl(q: 'dependency:$packageName'), + rel: 'nofollow', + text: 'Packages that depend on $packageName', + ), + if (showImplementsLink) ...[ + d.br(), + d.br(), + d.a( + href: urls.searchUrl( + q: PackageVersionTags.implementsFederatedPlugin(packageName)), + rel: 'nofollow', + text: 'Packages that implement $packageName', + ), + ], + ]), ); } diff --git a/app/lib/package/model_properties.dart b/app/lib/package/model_properties.dart index 06c272a543..912599ead3 100644 --- a/app/lib/package/model_properties.dart +++ b/app/lib/package/model_properties.dart @@ -29,7 +29,6 @@ Map _loadYaml(String yamlString) { class Pubspec { final pubspek.Pubspec _inner; final String jsonString; - Map? _json; String? _canonicalVersion; Pubspec._(this._inner, this.jsonString); @@ -44,10 +43,9 @@ class Pubspec { factory Pubspec.fromJson(Map map) => Pubspec._(pubspek.Pubspec.fromJson(map, lenient: true), json.encode(map)); - Map get asJson { - _load(); - return _json!; - } + late final _json = _loadYaml(jsonString); + + Map get asJson => _json; String get name => _inner.name; @@ -84,8 +82,7 @@ class Pubspec { .toList(); Map? get executables { - _load(); - final map = _json!['executables']; + final map = _json['executables']; return map is Map ? map : null; } @@ -97,7 +94,6 @@ class Pubspec { /// Returns null if the constraint is missing or does not follow the /// `>=` pattern. MinSdkVersion? get minSdkVersion { - _load(); return MinSdkVersion.tryParse(_inner.environment['sdk']); } @@ -106,7 +102,6 @@ class Pubspec { /// Returns null if the constraint is missing or does not follow the /// `>=` pattern. late final _minFlutterSdkVersion = () { - _load(); return MinSdkVersion.tryParse(_inner.environment['flutter']); }(); @@ -162,27 +157,32 @@ class Pubspec { .intersect(VersionConstraint.parse('<2.12.0-0')) .isEmpty; - /// Whether the pubspec file contains a flutter.plugin entry. - bool get hasFlutterPlugin { - _load(); - final flutter = _json!['flutter']; - if (flutter == null || flutter is! Map) return false; + late final _flutterPluginMap = () { + final flutter = _json['flutter']; + if (flutter == null || flutter is! Map) { + return null; + } final plugin = flutter['plugin']; - return plugin != null && plugin is Map; - } + if (plugin != null && plugin is Map) { + return plugin; + } else { + return null; + } + }(); + + /// Whether the pubspec file contains a flutter.plugin entry. + bool get hasFlutterPlugin => _flutterPluginMap != null; /// Whether the package has a dependency on flutter. bool get dependsOnFlutter { - _load(); - final dependencies = _json!['dependencies']; + final dependencies = _json['dependencies']; if (dependencies == null || dependencies is! Map) return false; return dependencies.containsKey('flutter'); } /// Whether the package has a dependency on flutter and it refers to the SDK. bool get dependsOnFlutterSdk { - _load(); - final dependencies = _json!['dependencies']; + final dependencies = _json['dependencies']; if (dependencies == null || dependencies is! Map) return false; final flutter = dependencies['flutter']; if (flutter == null || flutter is! Map) return false; @@ -195,14 +195,24 @@ class Pubspec { bool get hasOptedIntoNullSafety => _sdkConstraintStatus.hasOptedIntoNullSafety; - void _load() { - _json ??= _loadYaml(jsonString); - } - late final List funding = _inner.funding ?? const []; /// Whether the pubspec has any topic entry. bool get hasTopic => canonicalizedTopics.isNotEmpty; + + /// If package is implementing a federated Flutter plugin, this will be name + /// of the plugin package, `null` otherwise. + late final implementsFederatedPluginName = () { + if (_flutterPluginMap == null) { + return null; + } + final implements = _flutterPluginMap['implements']; + if (implements != null && implements is String) { + return implements; + } else { + return null; + } + }(); } class MinSdkVersion { diff --git a/app/lib/package/models.dart b/app/lib/package/models.dart index db264a9880..e7bc04a5a2 100644 --- a/app/lib/package/models.dart +++ b/app/lib/package/models.dart @@ -658,6 +658,7 @@ class PackageVersion extends db.ExpandoModel { /// List of tags from the properties on the current [PackageVersion] entity. Iterable getTags() { + final pluginForName = pubspec!.implementsFederatedPluginName; return { if (pubspec!.supportsOnlyLegacySdk) ...[ PackageVersionTags.isLegacy, @@ -665,6 +666,10 @@ class PackageVersion extends db.ExpandoModel { ], if (pubspec!.funding.isNotEmpty) PackageVersionTags.hasFundingLink, if (pubspec!.hasTopic) PackageVersionTags.hasTopic, + if (pluginForName != null) ...[ + PackageVersionTags.hasImplementsFederatedPlugin, + PackageVersionTags.implementsFederatedPlugin(pluginForName), + ], }; } diff --git a/pkg/_pub_shared/lib/search/search_form.dart b/pkg/_pub_shared/lib/search/search_form.dart index 9083bdc660..a6e5eef3dc 100644 --- a/pkg/_pub_shared/lib/search/search_form.dart +++ b/pkg/_pub_shared/lib/search/search_form.dart @@ -17,7 +17,7 @@ final RegExp _allDependencyRegExp = final _sortRegExp = RegExp('sort:([a-z]+)'); final _updatedRegExp = RegExp('updated:([0-9][0-9a-z]*)'); final _tagRegExp = - RegExp(r'([\+|\-]?[a-z0-9]+:[a-z0-9\-_\.]+)', caseSensitive: false); + RegExp(r'([\+|\-]?[a-z0-9\-]+:[a-z0-9\-_\.]+)', caseSensitive: false); /// The tag prefixes that we can detect in the user-provided search query. final _detectedTagPrefixes = { diff --git a/pkg/_pub_shared/lib/search/tags.dart b/pkg/_pub_shared/lib/search/tags.dart index 21d8dd2d0e..326a08d864 100644 --- a/pkg/_pub_shared/lib/search/tags.dart +++ b/pkg/_pub_shared/lib/search/tags.dart @@ -16,7 +16,8 @@ const allowedTagPrefixes = [ 'sdk:', 'show:', 'has:', - 'topic:' + 'topic:', + 'implements-federated-plugin:', ]; /// Collection of package-related tags. @@ -87,6 +88,14 @@ abstract class PackageVersionTags { /// Package version may be used in WASM compilation. static const String isWasmReady = 'is:wasm-ready'; + /// Package version has an entry indicating it implements a federated plugin. + static const String hasImplementsFederatedPlugin = + 'has:implements-federated-plugin'; + + /// The `implements-federated-plugin:` tag. + static String implementsFederatedPlugin(String name) => + 'implements-federated-plugin:$name'; + /// Version tags that provide a positive, forward-looking property /// of a prerelease or preview version. /// @@ -167,5 +176,7 @@ const _futureVersionTags = { /// Returns whether a [tag] is relevant to the package search, /// if it is a value from a preview or prerelease version. bool isFutureVersionTag(String tag) { - return _futureVersionTags.contains(tag) || tag.startsWith('runtime:'); + return _futureVersionTags.contains(tag) || + tag.startsWith('runtime:') || + tag.startsWith('plugin-for:'); } diff --git a/pkg/_pub_shared/test/search/search_form_test.dart b/pkg/_pub_shared/test/search/search_form_test.dart index 5b768e90fc..8640c55bc7 100644 --- a/pkg/_pub_shared/test/search/search_form_test.dart +++ b/pkg/_pub_shared/test/search/search_form_test.dart @@ -180,6 +180,14 @@ void main() { query.parsedQuery.tagsPredicate.toQueryParameters(), ['is:legacy']); }); + test('complex tag', () { + final query = + SearchForm(query: 'implements-federated-plugin:url_launcher'); + expect(query.parsedQuery.text, isNull); + expect(query.parsedQuery.tagsPredicate.toQueryParameters(), + ['implements-federated-plugin:url_launcher']); + }); + test('forbidden known tag', () { final query = SearchForm(query: '-is:legacy'); expect(query.parsedQuery.text, isNull);