Add basic web device and run support (#28302)
diff --git a/packages/flutter_tools/lib/src/application_package.dart b/packages/flutter_tools/lib/src/application_package.dart index 3822ea2..6cd1314 100644 --- a/packages/flutter_tools/lib/src/application_package.dart +++ b/packages/flutter_tools/lib/src/application_package.dart
@@ -21,6 +21,7 @@ import 'macos/application_package.dart'; import 'project.dart'; import 'tester/flutter_tester.dart'; +import 'web/web_device.dart'; class ApplicationPackageFactory { static ApplicationPackageFactory get instance => context[ApplicationPackageFactory]; @@ -50,10 +51,11 @@ return applicationBinary != null ? MacOSApp.fromPrebuiltApp(applicationBinary) : null; + case TargetPlatform.web: + return WebApplicationPackage(await FlutterProject.current()); case TargetPlatform.linux_x64: case TargetPlatform.windows_x64: case TargetPlatform.fuchsia: - case TargetPlatform.web: return null; } assert(platform != null);
diff --git a/packages/flutter_tools/lib/src/context_runner.dart b/packages/flutter_tools/lib/src/context_runner.dart index af2976d..0f23de0 100644 --- a/packages/flutter_tools/lib/src/context_runner.dart +++ b/packages/flutter_tools/lib/src/context_runner.dart
@@ -41,6 +41,7 @@ import 'usage.dart'; import 'version.dart'; import 'web/compile.dart'; +import 'web/web_device.dart'; import 'windows/windows_workflow.dart'; Future<T> runInContext<T>( @@ -65,6 +66,7 @@ CocoaPods: () => CocoaPods(), CocoaPodsValidator: () => const CocoaPodsValidator(), Config: () => Config(), + ChromeLauncher: () => const ChromeLauncher(), DevFSConfig: () => DevFSConfig(), DeviceManager: () => DeviceManager(), Doctor: () => const Doctor(),
diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart index 52a3153..cad682f 100644 --- a/packages/flutter_tools/lib/src/device.dart +++ b/packages/flutter_tools/lib/src/device.dart
@@ -20,6 +20,7 @@ import 'linux/linux_device.dart'; import 'macos/macos_device.dart'; import 'tester/flutter_tester.dart'; +import 'web/web_device.dart'; import 'windows/windows_device.dart'; DeviceManager get deviceManager => context[DeviceManager]; @@ -36,7 +37,7 @@ IOSSimulators(), FuchsiaDevices(), FlutterTesterDevices(), - ] + _conditionalDesktopDevices); + ] + _conditionalDesktopDevices + _conditionalWebDevices); /// Only add desktop devices if the flag is enabled. static List<DeviceDiscovery> get _conditionalDesktopDevices { @@ -47,6 +48,13 @@ ] : <DeviceDiscovery>[]; } + /// Only add web devices if the flag is enabled. + static List<DeviceDiscovery> get _conditionalWebDevices { + return flutterWebEnabled ? <DeviceDiscovery>[ + WebDevices(), + ] : <DeviceDiscovery>[]; + } + String _specifiedDeviceId; /// A user-specified device ID.
diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart index dc7587c..b2a0fde 100644 --- a/packages/flutter_tools/lib/src/project.dart +++ b/packages/flutter_tools/lib/src/project.dart
@@ -19,6 +19,7 @@ import 'ios/xcodeproj.dart' as xcode; import 'plugins.dart'; import 'template.dart'; +import 'web/web_device.dart'; /// Represents the contents of a Flutter project at the specified [directory]. /// @@ -95,6 +96,9 @@ /// The Android sub project of this project. AndroidProject get android => AndroidProject._(this); + /// The web sub project of this project. + WebProject get web => WebProject._(this); + /// The `pubspec.yaml` file of this project. File get pubspecFile => directory.childFile('pubspec.yaml'); @@ -150,6 +154,9 @@ refreshPluginsList(this); await android.ensureReadyForPlatformSpecificTooling(); await ios.ensureReadyForPlatformSpecificTooling(); + if (flutterWebEnabled) { + await web.ensureReadyForPlatformSpecificTooling(); + } await injectPlugins(this); } @@ -454,6 +461,31 @@ } } +/// Represents the web sub-project of a Flutter project. +class WebProject { + WebProject._(this.parent); + + final FlutterProject parent; + + Future<void> ensureReadyForPlatformSpecificTooling() async { + /// Generate index.html in build/web. Eventually we could support + /// a custom html under the web sub directory. + final Directory outputDir = fs.directory(getWebBuildDirectory()); + if (!outputDir.existsSync()) { + outputDir.createSync(recursive: true); + } + final Template template = Template.fromName('web/index.html.tmpl'); + template.render( + outputDir, + <String, dynamic>{ + 'appName': parent.manifest.appName, + }, + printStatusWhenWriting: false, + overwriteExisting: true, + ); + } +} + /// Deletes [directory] with all content. void _deleteIfExistsSync(Directory directory) { if (directory.existsSync())
diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart index 8db4102..2addb59 100644 --- a/packages/flutter_tools/lib/src/resident_runner.dart +++ b/packages/flutter_tools/lib/src/resident_runner.dart
@@ -326,7 +326,11 @@ await stopEchoingDeviceLog(); return 2; } - observatoryUris = <Uri>[result.observatoryUri]; + if (result.hasObservatory) { + observatoryUris = <Uri>[result.observatoryUri]; + } else { + observatoryUris = <Uri>[]; + } return 0; } @@ -384,8 +388,11 @@ await stopEchoingDeviceLog(); return 2; } - if (result.hasObservatory) + if (result.hasObservatory) { observatoryUris = <Uri>[result.observatoryUri]; + } else { + observatoryUris = <Uri>[]; + } return 0; }
diff --git a/packages/flutter_tools/lib/src/web/compile.dart b/packages/flutter_tools/lib/src/web/compile.dart index 4dab748..51febc0 100644 --- a/packages/flutter_tools/lib/src/web/compile.dart +++ b/packages/flutter_tools/lib/src/web/compile.dart
@@ -38,6 +38,7 @@ if (!processManager.canRun(engineDartPath)) { throwToolExit('Unable to find Dart binary at $engineDartPath'); } + /// Compile Dart to JavaScript. final List<String> command = <String>[ engineDartPath, dart2jsPath,
diff --git a/packages/flutter_tools/lib/src/web/web_device.dart b/packages/flutter_tools/lib/src/web/web_device.dart new file mode 100644 index 0000000..d98bb97 --- /dev/null +++ b/packages/flutter_tools/lib/src/web/web_device.dart
@@ -0,0 +1,206 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '../application_package.dart'; +import '../base/context.dart'; +import '../base/file_system.dart'; +import '../base/io.dart'; +import '../base/logger.dart'; +import '../base/platform.dart'; +import '../base/process_manager.dart'; +import '../build_info.dart'; +import '../device.dart'; +import '../globals.dart'; +import '../project.dart'; +import '../web/compile.dart'; + +ChromeLauncher get chromeLauncher => context[ChromeLauncher]; + +/// Only launch or display web devices if `FLUTTER_WEB` +/// environment variable is set to true. +bool get flutterWebEnabled { + _flutterWebEnabled = platform.environment['FLUTTER_WEB']?.toLowerCase() == 'true'; + return _flutterWebEnabled; +} +bool _flutterWebEnabled; + + +class WebApplicationPackage extends ApplicationPackage { + WebApplicationPackage(this._flutterProject) : super(id: _flutterProject.manifest.appName); + + final FlutterProject _flutterProject; + + @override + String get name => _flutterProject.manifest.appName; + + /// The location of the web source assets. + Directory get webSourcePath => _flutterProject.directory.childDirectory('web'); +} + + +class WebDevice extends Device { + WebDevice() : super('web'); + + HttpServer _server; + WebApplicationPackage _package; + + @override + bool get supportsHotReload => false; + + @override + bool get supportsHotRestart => false; + + @override + bool get supportsStartPaused => true; + + @override + bool get supportsStopApp => true; + + @override + bool get supportsScreenshot => false; + + @override + void clearLogs() {} + + @override + DeviceLogReader getLogReader({ApplicationPackage app}) { + return NoOpDeviceLogReader(app.name); + } + + @override + Future<bool> installApp(ApplicationPackage app) async => true; + + @override + Future<bool> isAppInstalled(ApplicationPackage app) async => true; + + @override + Future<bool> isLatestBuildInstalled(ApplicationPackage app) async => true; + + @override + Future<bool> get isLocalEmulator async => false; + + @override + bool isSupported() => flutterWebEnabled; + + @override + String get name => 'web'; + + @override + DevicePortForwarder get portForwarder => const NoOpDevicePortForwarder(); + + @override + Future<String> get sdkNameAndVersion async => 'web'; + + @override + Future<LaunchResult> startApp( + covariant WebApplicationPackage package, { + String mainPath, + String route, + DebuggingOptions debuggingOptions, + Map<String, Object> platformArgs, + bool prebuiltApplication = false, + bool applicationNeedsRebuild = false, + bool usesTerminalUi = true, + bool ipv6 = false, + }) async { + final Status status = logger.startProgress('Compiling ${package.name} to JavaScript...', timeout: null); + final int result = await webCompiler.compile(target: mainPath, minify: false, enabledAssertions: true); + status.stop(); + if (result != 0) { + printError('Failed to compile ${package.name} to JavaScript'); + return LaunchResult.failed(); + } + _package = package; + _server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0); + _server.listen(_basicAssetServer); + printStatus('Serving assets from http:localhost:${_server.port}'); + await chromeLauncher.launch('http:localhost:${_server.port}'); + return LaunchResult.succeeded(observatoryUri: null); + } + + // Note: we don't currently have a way to track which chrome processes + // belong to the flutter tool, so we'll err on the side of caution by + // keeping these open. + @override + Future<bool> stopApp(ApplicationPackage app) async { + await _server?.close(); + _server = null; + return true; + } + + @override + Future<TargetPlatform> get targetPlatform async => TargetPlatform.web; + + @override + Future<bool> uninstallApp(ApplicationPackage app) async => true; + + Future<void> _basicAssetServer(HttpRequest request) async { + if (request.method != 'GET') { + request.response.statusCode = HttpStatus.forbidden; + await request.response.close(); + return; + } + // Resolve all get requests to the build/web/asset directory. + final Uri uri = request.uri; + File file; + String contentType; + if (uri.path == '/') { + file = _package.webSourcePath.childFile('index.html'); + contentType = 'text/html'; + } else if (uri.path == '/main.dart.js') { + file = fs.file(fs.path.join(getWebBuildDirectory(), 'main.dart.js')); + contentType = 'text/javascript'; + } else { + file = fs.file(fs.path.join(getAssetBuildDirectory(), uri.path)); + } + if (!file.existsSync()) { + request.response.statusCode = HttpStatus.notFound; + await request.response.close(); + return; + } + request.response.statusCode = HttpStatus.ok; + if (contentType != null) { + request.response.headers.add(HttpHeaders.contentTypeHeader, contentType); + } + await request.response.addStream(file.openRead()); + await request.response.close(); + } +} + +class WebDevices extends PollingDeviceDiscovery { + WebDevices() : super('web'); + + final WebDevice _webDevice = WebDevice(); + + @override + bool get canListAnything => flutterWebEnabled; + + @override + Future<List<Device>> pollingGetDevices() async { + return <Device>[ + _webDevice, + ]; + } + + @override + bool get supportsPlatform => flutterWebEnabled; + +} + +// Responsible for launching chrome with devtools configured. +class ChromeLauncher { + const ChromeLauncher(); + + static const String _kMacosLocation = '/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome'; + + Future<void> launch(String host) async { + if (platform.isMacOS) { + await processManager.start(<String>[ + _kMacosLocation, + host, + ]); + } + throw UnsupportedError('$platform is not supported'); + } +}