Automatically generated registrants for web plugins (#39628)

* WIP on web plugin registry

* WIP on registering plugins

* WIP on web plugin registration

* Only generate `package:flutter_web_plugins` imports if plugins are
defined

* Add parsing test

* Add documentation

* Fix analyzer warnings

* add license headers

* Add tests for package:flutter_web_plugins

* Run `flutter update-packages --force-upgrade`

* Fix analyzer errors

* Fix analyzer error in test

* Update copyright and remove flutter SDK constraints

* Enable tests since engine has rolled

* add flutter_web_plugins tests to bots

* Create an empty .packages file for WebFs test
diff --git a/packages/flutter_tools/lib/src/build_runner/build_script.dart b/packages/flutter_tools/lib/src/build_runner/build_script.dart
index 3c69efe..433abd8 100644
--- a/packages/flutter_tools/lib/src/build_runner/build_script.dart
+++ b/packages/flutter_tools/lib/src/build_runner/build_script.dart
@@ -86,7 +86,10 @@
   core.apply(
     'flutter_tools:shell',
     <BuilderFactory>[
-      (BuilderOptions options) => const FlutterWebShellBuilder(),
+      (BuilderOptions options) {
+        final bool hasPlugins = options.config['hasPlugins'] == true;
+        return FlutterWebShellBuilder(hasPlugins: hasPlugins);
+      }
     ],
     core.toRoot(),
     hideOutput: true,
@@ -342,7 +345,9 @@
 
 /// A shell builder which generates the web specific entrypoint.
 class FlutterWebShellBuilder implements Builder {
-  const FlutterWebShellBuilder();
+  const FlutterWebShellBuilder({this.hasPlugins = false});
+
+  final bool hasPlugins;
 
   @override
   Future<void> build(BuildStep buildStep) async {
@@ -352,16 +357,33 @@
       return;
     }
     final AssetId outputId = buildStep.inputId.changeExtension('_web_entrypoint.dart');
-    await buildStep.writeAsString(outputId, '''
+    if (hasPlugins) {
+      await buildStep.writeAsString(outputId, '''
 import 'dart:ui' as ui;
+
+import 'package:flutter_web_plugins/flutter_web_plugins.dart';
+
+import 'generated_plugin_registrant.dart';
+import "${path.url.basename(buildStep.inputId.path)}" as entrypoint;
+
+Future<void> main() async {
+  registerPlugins(webPluginRegistry);
+  await ui.webOnlyInitializePlatform();
+  entrypoint.main();
+}
+''');
+    } else {
+      await buildStep.writeAsString(outputId, '''
+import 'dart:ui' as ui;
+
 import "${path.url.basename(buildStep.inputId.path)}" as entrypoint;
 
 Future<void> main() async {
   await ui.webOnlyInitializePlatform();
   entrypoint.main();
 }
-
 ''');
+    }
   }
 
   @override
diff --git a/packages/flutter_tools/lib/src/build_runner/web_compilation_delegate.dart b/packages/flutter_tools/lib/src/build_runner/web_compilation_delegate.dart
index a62eccb..60d8be3 100644
--- a/packages/flutter_tools/lib/src/build_runner/web_compilation_delegate.dart
+++ b/packages/flutter_tools/lib/src/build_runner/web_compilation_delegate.dart
@@ -57,6 +57,7 @@
     _packageUriMapper = PackageUriMapper(
         path.absolute('lib/main.dart'), PackageMap.globalPackagesPath, null, null);
     _packageGraph = core.PackageGraph.forPath(projectDirectory.path);
+
     final core.BuildEnvironment buildEnvironment = core.OverrideableEnvironment(
         core.IOEnvironment(_packageGraph), onLog: (LogRecord record) {
       if (record.level == Level.SEVERE || record.level == Level.SHOUT) {
diff --git a/packages/flutter_tools/lib/src/build_runner/web_fs.dart b/packages/flutter_tools/lib/src/build_runner/web_fs.dart
index 0222ab8..6e5a114 100644
--- a/packages/flutter_tools/lib/src/build_runner/web_fs.dart
+++ b/packages/flutter_tools/lib/src/build_runner/web_fs.dart
@@ -31,6 +31,8 @@
 import '../cache.dart';
 import '../dart/package_map.dart';
 import '../globals.dart';
+import '../platform_plugins.dart';
+import '../plugins.dart';
 import '../project.dart';
 import '../web/chrome.dart';
 
@@ -38,7 +40,7 @@
 const String kBuildTargetName = 'web';
 
 /// A factory for creating a [Dwds] instance.
-DwdsFactory get dwdsFactpory => context.get<DwdsFactory>() ?? Dwds.start;
+DwdsFactory get dwdsFactory => context.get<DwdsFactory>() ?? Dwds.start;
 
 /// The [BuildDaemonCreator] instance.
 BuildDaemonCreator get buildDaemonCreator => context.get<BuildDaemonCreator>() ?? const BuildDaemonCreator();
@@ -49,10 +51,10 @@
 /// A factory for creating an [HttpMultiServer] instance.
 HttpMultiServerFactory get httpMultiServerFactory => context.get<HttpMultiServerFactory>() ?? HttpMultiServer.bind;
 
-/// A function with the same signature as [HttpMultiServier.bind].
+/// A function with the same signature as [HttpMultiServer.bind].
 typedef HttpMultiServerFactory = Future<HttpServer> Function(dynamic address, int port);
 
-/// A function with the same signatire as [Dwds.start].
+/// A function with the same signature as [Dwds.start].
 typedef DwdsFactory = Future<Dwds> Function({
   @required int applicationPort,
   @required int assetServerPort,
@@ -144,9 +146,11 @@
     if (!flutterProject.dartTool.existsSync()) {
       flutterProject.dartTool.createSync(recursive: true);
     }
+
+    final bool hasWebPlugins = findPlugins(flutterProject).any((Plugin p) => p.platforms.containsKey(WebPlugin.kConfigKey));
     // Start the build daemon and run an initial build.
     final BuildDaemonClient client = await buildDaemonCreator
-      .startBuildDaemon(fs.currentDirectory.path, release: buildInfo.isRelease, profile: buildInfo.isProfile);
+      .startBuildDaemon(fs.currentDirectory.path, release: buildInfo.isRelease, profile: buildInfo.isProfile, hasPlugins: hasWebPlugins);
     client.startBuild();
     // Only provide relevant build results
     final Stream<BuildResult> filteredBuildResults = client.buildResults
@@ -163,7 +167,7 @@
 
     // Initialize the dwds server.
     final int port = await os.findFreePort();
-    final Dwds dwds = await dwdsFactpory(
+    final Dwds dwds = await dwdsFactory(
       hostname: _kHostName,
       applicationPort: port,
       applicationTarget: kBuildTargetName,
@@ -307,12 +311,13 @@
   static const String _ignoredLine3 = 'have your dependencies specified fully in your pubspec.yaml';
 
   /// Start a build daemon and register the web targets.
-  Future<BuildDaemonClient> startBuildDaemon(String workingDirectory, {bool release = false, bool profile = false }) async {
+  Future<BuildDaemonClient> startBuildDaemon(String workingDirectory, {bool release = false, bool profile = false, bool hasPlugins = false}) async {
     try {
       final BuildDaemonClient client = await _connectClient(
         workingDirectory,
         release: release,
         profile: profile,
+        hasPlugins: hasPlugins,
       );
       _registerBuildTargets(client);
       return client;
@@ -339,7 +344,7 @@
 
   Future<BuildDaemonClient> _connectClient(
     String workingDirectory,
-    { bool release, bool profile }
+    { bool release, bool profile, bool hasPlugins }
   ) {
     final String flutterToolsPackages = fs.path.join(Cache.flutterRoot, 'packages', 'flutter_tools', '.packages');
     final String buildScript = fs.path.join(Cache.flutterRoot, 'packages', 'flutter_tools', 'lib', 'src', 'build_runner', 'build_script.dart');
@@ -360,6 +365,7 @@
         '--define', 'flutter_tools:entrypoint=release=$release',
         '--define', 'flutter_tools:entrypoint=profile=$profile',
         '--define', 'flutter_tools:shell=flutterWebSdk=$flutterWebSdk',
+        '--define', 'flutter_tools:shell=hasPlugins=$hasPlugins',
       ],
       logHandler: (ServerLog serverLog) {
         switch (serverLog.level) {
@@ -392,4 +398,4 @@
     final String portFilePath = fs.path.join(daemonWorkspace(workingDirectory.path), '.asset_server_port');
     return int.tryParse(fs.file(portFilePath).readAsStringSync());
   }
-}
\ No newline at end of file
+}
diff --git a/packages/flutter_tools/lib/src/platform_plugins.dart b/packages/flutter_tools/lib/src/platform_plugins.dart
index ed9b4ea..aeaab30 100644
--- a/packages/flutter_tools/lib/src/platform_plugins.dart
+++ b/packages/flutter_tools/lib/src/platform_plugins.dart
@@ -139,3 +139,54 @@
     };
   }
 }
+
+/// Contains the parameters to template a web plugin.
+///
+/// The required fields include: [name] of the plugin, the [pluginClass] that will
+/// be the entry point to the plugin's implementation, and the [fileName]
+/// containing the code.
+class WebPlugin extends PluginPlatform {
+  const WebPlugin({
+    @required this.name,
+    @required this.pluginClass,
+    @required this.fileName,
+  });
+
+  factory WebPlugin.fromYaml(String name, YamlMap yaml) {
+    assert(validate(yaml));
+    return WebPlugin(
+      name: name,
+      pluginClass: yaml['pluginClass'],
+      fileName: yaml['fileName'],
+    );
+  }
+
+  static bool validate(YamlMap yaml) {
+    if (yaml == null) {
+      return false;
+    }
+    return yaml['pluginClass'] is String && yaml['fileName'] is String;
+  }
+
+  static const String kConfigKey = 'web';
+
+  /// The name of the plugin.
+  final String name;
+
+  /// The class containing the plugin implementation details.
+  ///
+  /// This class should have a static `registerWith` method defined.
+  final String pluginClass;
+
+  /// The name of the file containing the class implementation above.
+  final String fileName;
+
+  @override
+  Map<String, dynamic> toMap() {
+    return <String, dynamic>{
+      'name': name,
+      'class': pluginClass,
+      'file': fileName,
+    };
+  }
+}
diff --git a/packages/flutter_tools/lib/src/plugins.dart b/packages/flutter_tools/lib/src/plugins.dart
index a274c7e..fe6475c 100644
--- a/packages/flutter_tools/lib/src/plugins.dart
+++ b/packages/flutter_tools/lib/src/plugins.dart
@@ -89,6 +89,11 @@
           MacOSPlugin.fromYaml(name, platformsYaml[MacOSPlugin.kConfigKey]);
     }
 
+    if (platformsYaml[WebPlugin.kConfigKey] != null) {
+      platforms[WebPlugin.kConfigKey] =
+          WebPlugin.fromYaml(name, platformsYaml[WebPlugin.kConfigKey]);
+    }
+
     return Plugin(
       name: name,
       path: path,
@@ -384,6 +389,25 @@
 end
 ''';
 
+const String _dartPluginRegistryTemplate = '''//
+// Generated file. Do not edit.
+//
+import 'dart:ui';
+
+{{#plugins}}
+import 'package:{{name}}/{{file}}';
+{{/plugins}}
+
+import 'package:flutter_web_plugins/flutter_web_plugins.dart';
+
+void registerPlugins(PluginRegistry registry) {
+{{#plugins}}
+  {{class}}.registerWith(registry.registrarFor({{class}}));
+{{/plugins}}
+  registry.registerMessageHandler();
+}
+''';
+
 Future<void> _writeIOSPluginRegistrant(FlutterProject project, List<Plugin> plugins) async {
   final List<Map<String, dynamic>> iosPlugins = _extractPlatformMaps(plugins, IOSPlugin.kConfigKey);
   final Map<String, dynamic> context = <String, dynamic>{
@@ -439,6 +463,29 @@
   );
 }
 
+Future<void> _writeWebPluginRegistrant(FlutterProject project, List<Plugin> plugins) async {
+  final List<Map<String, dynamic>> webPlugins = _extractPlatformMaps(plugins, WebPlugin.kConfigKey);
+  final Map<String, dynamic> context = <String, dynamic>{
+    'plugins': webPlugins,
+  };
+  final String registryDirectory = project.web.libDirectory.path;
+  final String filePath = fs.path.join(registryDirectory, 'generated_plugin_registrant.dart');
+  if (webPlugins.isEmpty) {
+    final File file = fs.file(filePath);
+    file.createSync(recursive: true);
+    file.writeAsStringSync('''
+// Generated file. Intentionally left empty due to no web plugins registered.
+void registerPlugins(ignored) {}
+''');
+  } else {
+    _renderTemplateToFile(
+      _dartPluginRegistryTemplate,
+      context,
+      filePath,
+    );
+  }
+}
+
 /// Rewrites the `.flutter-plugins` file of [project] based on the plugin
 /// dependencies declared in `pubspec.yaml`.
 ///
@@ -490,6 +537,9 @@
     }
   }
   }
+  if (featureFlags.isWebEnabled && project.web.existsSync()) {
+    await _writeWebPluginRegistrant(project, plugins);
+  }
 }
 
 /// Returns whether the specified Flutter [project] has any plugin dependencies.
diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart
index e33f679..e53d187 100644
--- a/packages/flutter_tools/lib/src/project.dart
+++ b/packages/flutter_tools/lib/src/project.dart
@@ -623,6 +623,9 @@
       && indexFile.existsSync();
   }
 
+  /// The 'lib' directory for the application.
+  Directory get libDirectory => parent.directory.childDirectory('lib');
+
   /// The html file used to host the flutter web application.
   File get indexFile => parent.directory
       .childDirectory('web')
diff --git a/packages/flutter_tools/lib/src/web/chrome.dart b/packages/flutter_tools/lib/src/web/chrome.dart
index 1ea6168..34e5ecf 100644
--- a/packages/flutter_tools/lib/src/web/chrome.dart
+++ b/packages/flutter_tools/lib/src/web/chrome.dart
@@ -140,7 +140,6 @@
         await chrome.chromeConnection.getTabs();
       } catch (e) {
         await chrome.close();
-        print('here');
         throwToolExit(
             'Unable to connect to Chrome debug port: ${chrome.debugPort}\n $e');
       }