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'); }