Reland - Wire up hot restart and incremental rebuilds for web (#33533)

diff --git a/packages/flutter_tools/lib/src/web/asset_server.dart b/packages/flutter_tools/lib/src/web/asset_server.dart
new file mode 100644
index 0000000..d4ad143
--- /dev/null
+++ b/packages/flutter_tools/lib/src/web/asset_server.dart
@@ -0,0 +1,186 @@
+// 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 '../artifacts.dart';
+import '../base/file_system.dart';
+import '../base/io.dart';
+import '../build_info.dart';
+import '../dart/package_map.dart';
+import '../globals.dart';
+import '../project.dart';
+
+/// Handles mapping requests from a dartdevc compiled application to assets.
+///
+/// The server will receive size different kinds of requests:
+///
+///  1. A request to assets in the form of `/assets/foo`. These are resolved
+///     relative to `build/flutter_assets`.
+///  2. A request to a bootstrap file, such as `main.dart.js`. These are
+///     resolved relative to the dart tool directory.
+///  3. A request to a JavaScript asset in the form of `/packages/foo/bar.js`.
+///     These are looked up relative to the correct package root of the
+///     dart_tool directory.
+///  4. A request to a Dart asset in the form of `/packages/foo/bar.dart` for
+///     sourcemaps. These either need to be looked up from the application lib
+///     directory (if the package is the same), or found in the .packages file.
+///  5. A request for a specific dart asset such as `stack_trace_mapper.js` or
+///     `dart_sdk.js`. These have fixed locations determined by [artifacts].
+///  6. A request to `/` which is translated into `index.html`.
+class WebAssetServer {
+  WebAssetServer(this.flutterProject, this.target, this.ipv6);
+
+  /// The flutter project corresponding to this application.
+  final FlutterProject flutterProject;
+
+  /// The entrypoint we have compiled for.
+  final String target;
+
+  /// Whether to serve from ipv6 localhost.
+  final bool ipv6;
+
+  HttpServer _server;
+  Map<String, Uri> _packages;
+
+  /// The port being served, or null if not initialized.
+  int get port => _server?.port;
+
+  /// Initialize the server.
+  ///
+  /// Throws a [StateError] if called multiple times.
+  Future<void> initialize() async {
+    if (_server != null) {
+      throw StateError('Already serving.');
+    }
+    _packages = PackageMap(PackageMap.globalPackagesPath).map;
+    _server = await HttpServer.bind(
+        ipv6 ? InternetAddress.loopbackIPv6 : InternetAddress.loopbackIPv4, 0)
+      ..autoCompress = false;
+    _server.listen(_onRequest);
+  }
+
+  /// Clean up the server.
+  Future<void> dispose() {
+    return _server.close();
+  }
+
+  /// An HTTP server which provides JavaScript and web assets to the browser.
+  Future<void> _onRequest(HttpRequest request) async {
+    if (request.method != 'GET') {
+      request.response.statusCode = HttpStatus.forbidden;
+      await request.response.close();
+      return;
+    }
+    final Uri uri = request.uri;
+    if (uri.path == '/') {
+      final File file = flutterProject.directory
+          .childDirectory('web')
+          .childFile('index.html');
+      await _completeRequest(request, file, 'text/html');
+    } else if (uri.path.contains('stack_trace_mapper')) {
+      final File file = fs.file(fs.path.join(
+        artifacts.getArtifactPath(Artifact.engineDartSdkPath),
+        'lib',
+        'dev_compiler',
+        'web',
+        'dart_stack_trace_mapper.js'
+      ));
+      await _completeRequest(request, file, 'text/javascript');
+    } else if (uri.path.contains('require.js')) {
+     final File file = fs.file(fs.path.join(
+        artifacts.getArtifactPath(Artifact.engineDartSdkPath),
+       'lib',
+       'dev_compiler',
+       'kernel',
+       'amd',
+       'require.js'
+     ));
+     await _completeRequest(request, file, 'text/javascript');
+    } else if (uri.path.endsWith('main.dart.js')) {
+      final File file = fs.file(fs.path.join(
+        flutterProject.dartTool.path,
+        'build',
+        'flutter_web',
+        flutterProject.manifest.appName,
+        'lib',
+        '${fs.path.basename(target)}.js',
+      ));
+      await _completeRequest(request, file, 'text/javascript');
+    } else if (uri.path.endsWith('${fs.path.basename(target)}.bootstrap.js')) {
+      final File file = fs.file(fs.path.join(
+        flutterProject.dartTool.path,
+        'build',
+        'flutter_web',
+        flutterProject.manifest.appName,
+        'lib',
+        '${fs.path.basename(target)}.bootstrap.js',
+      ));
+      await _completeRequest(request, file, 'text/javascript');
+    } else if (uri.path.contains('dart_sdk')) {
+      final File file = fs.file(fs.path.join(
+        artifacts.getArtifactPath(Artifact.flutterWebSdk),
+        'kernel',
+        'amd',
+        'dart_sdk.js',
+      ));
+      await _completeRequest(request, file, 'text/javascript');
+    } else if (uri.path.startsWith('/packages') && uri.path.endsWith('.dart')) {
+      await _resolveDart(request);
+    } else if (uri.path.startsWith('/packages')) {
+      await _resolveJavascript(request);
+    } else if (uri.path.contains('assets')) {
+      await _resolveAsset(request);
+    } else {
+      request.response.statusCode = HttpStatus.notFound;
+      await request.response.close();
+    }
+  }
+
+  /// Resolves requests in the form of `/packages/foo/bar.js` or
+  /// `/packages/foo/bar.js.map`.
+  Future<void> _resolveJavascript(HttpRequest request) async {
+    final List<String> segments = fs.path.split(request.uri.path);
+    final String packageName = segments[2];
+    final String filePath = fs.path.joinAll(segments.sublist(3));
+    final Uri packageUri = flutterProject.dartTool
+        .childDirectory('build')
+        .childDirectory('flutter_web')
+        .childDirectory(packageName)
+        .childDirectory('lib')
+        .uri;
+    await _completeRequest(
+        request, fs.file(packageUri.resolve(filePath)), 'text/javascript');
+  }
+
+  /// Resolves requests in the form of `/packages/foo/bar.dart`.
+  Future<void> _resolveDart(HttpRequest request) async {
+    final List<String> segments = fs.path.split(request.uri.path);
+    final String packageName = segments[2];
+    final String filePath = fs.path.joinAll(segments.sublist(3));
+    final Uri packageUri = _packages[packageName];
+    await _completeRequest(request, fs.file(packageUri.resolve(filePath)));
+  }
+
+  /// Resolves requests in the form of `/assets/foo`.
+  Future<void> _resolveAsset(HttpRequest request) async {
+    final String assetPath = request.uri.path.replaceFirst('/assets/', '');
+    await _completeRequest(
+        request, fs.file(fs.path.join(getAssetBuildDirectory(), assetPath)));
+  }
+
+  Future<void> _completeRequest(HttpRequest request, File file,
+      [String contentType = 'text']) async {
+    printTrace('looking for ${request.uri} at ${file.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();
+  }
+}
diff --git a/packages/flutter_tools/lib/src/web/compile.dart b/packages/flutter_tools/lib/src/web/compile.dart
index 892b2ba..b814d40 100644
--- a/packages/flutter_tools/lib/src/web/compile.dart
+++ b/packages/flutter_tools/lib/src/web/compile.dart
@@ -17,7 +17,11 @@
 /// The [WebCompiler] instance.
 WebCompiler get webCompiler => context.get<WebCompiler>();
 
-/// A wrapper around dart2js for web compilation.
+/// The [WebCompilationProxy] instance.
+WebCompilationProxy get webCompilationProxy =>
+    context.get<WebCompilationProxy>();
+
+/// A wrapper around dart tools for web compilation.
 class WebCompiler {
   const WebCompiler();
 
@@ -25,11 +29,19 @@
   ///
   /// `minify` controls whether minifaction of the source is enabled. Defaults to `true`.
   /// `enabledAssertions` controls whether assertions are enabled. Defaults to `false`.
-  Future<int> compile({@required String target, bool minify = true, bool enabledAssertions = false}) async {
-    final String engineDartPath = artifacts.getArtifactPath(Artifact.engineDartBinary);
-    final String dart2jsPath = artifacts.getArtifactPath(Artifact.dart2jsSnapshot);
-    final String flutterWebSdkPath = artifacts.getArtifactPath(Artifact.flutterWebSdk);
-    final String librariesPath = fs.path.join(flutterWebSdkPath, 'libraries.json');
+  Future<int> compileDart2js({
+    @required String target,
+    bool minify = true,
+    bool enabledAssertions = false,
+  }) async {
+    final String engineDartPath =
+        artifacts.getArtifactPath(Artifact.engineDartBinary);
+    final String dart2jsPath =
+        artifacts.getArtifactPath(Artifact.dart2jsSnapshot);
+    final String flutterWebSdkPath =
+        artifacts.getArtifactPath(Artifact.flutterWebSdk);
+    final String librariesPath =
+        fs.path.join(flutterWebSdkPath, 'libraries.json');
     final Directory outputDir = fs.directory(getWebBuildDirectory());
     if (!outputDir.existsSync()) {
       outputDir.createSync(recursive: true);
@@ -38,6 +50,7 @@
     if (!processManager.canRun(engineDartPath)) {
       throwToolExit('Unable to find Dart binary at $engineDartPath');
     }
+
     /// Compile Dart to JavaScript.
     final List<String> command = <String>[
       engineDartPath,
@@ -55,16 +68,35 @@
     }
     printTrace(command.join(' '));
     final Process result = await processManager.start(command);
-    result
-        .stdout
+    result.stdout
         .transform(utf8.decoder)
         .transform(const LineSplitter())
         .listen(printStatus);
-    result
-        .stderr
+    result.stderr
         .transform(utf8.decoder)
         .transform(const LineSplitter())
         .listen(printError);
     return result.exitCode;
   }
 }
+
+/// An indirection on web compilation.
+///
+/// Avoids issues with syncing build_runner_core to other repos.
+class WebCompilationProxy {
+  const WebCompilationProxy();
+
+  /// Initialize the web compiler output to `outputDirectory` from a project spawned at
+  /// `projectDirectory`.
+  Future<void> initialize({
+    @required Directory projectDirectory,
+    @required String target,
+  }) async {
+    throw UnimplementedError();
+  }
+
+  /// Invalidate the source files in `inputs` and recompile them to JavaScript.
+  Future<void> invalidate({@required List<Uri> inputs}) async {
+    throw UnimplementedError();
+  }
+}
diff --git a/packages/flutter_tools/lib/src/web/web_device.dart b/packages/flutter_tools/lib/src/web/web_device.dart
index 0c6905b..d8ce9ce 100644
--- a/packages/flutter_tools/lib/src/web/web_device.dart
+++ b/packages/flutter_tools/lib/src/web/web_device.dart
@@ -50,10 +50,10 @@
   WebApplicationPackage _package;
 
   @override
-  bool get supportsHotReload => false;
+  bool get supportsHotReload => true;
 
   @override
-  bool get supportsHotRestart => false;
+  bool get supportsHotRestart => true;
 
   @override
   bool get supportsStartPaused => true;
@@ -108,7 +108,7 @@
     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);
+    final int result = await webCompiler.compileDart2js(target: mainPath, minify: false, enabledAssertions: true);
     status.stop();
     if (result != 0) {
       printError('Failed to compile ${package.name} to JavaScript');
@@ -125,7 +125,7 @@
     _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}');
+    await chromeLauncher.launch('http://localhost:${_server.port}');
     return LaunchResult.succeeded(observatoryUri: null);
   }
 
@@ -201,22 +201,46 @@
 
   @override
   bool get supportsPlatform => flutterWebEnabled;
-
 }
 
+const String _klinuxExecutable = 'google-chrome';
+const String _kMacOSExecutable = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
+const String _kWindowsExecutable = r'Google\Chrome\Application\chrome.exe';
+final List<String> _kWindowsPrefixes = <String>[
+  platform.environment['LOCALAPPDATA'],
+  platform.environment['PROGRAMFILES'],
+  platform.environment['PROGRAMFILES(X86)'],
+];
+
 // 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 {
+  /// Launch the chrome browser to a particular `host` page.
+  Future<Process> launch(String host) async {
+    String executable;
     if (platform.isMacOS) {
-      return processManager.start(<String>[
-        _kMacosLocation,
-        host,
-      ]);
+      executable = _kMacOSExecutable;
+    } else if (platform.isLinux) {
+      executable = _klinuxExecutable;
+    } else if (platform.isWindows) {
+      final String filePath = _kWindowsPrefixes.firstWhere((String prefix) {
+        if (prefix == null) {
+          return false;
+        }
+        final String path = fs.path.join(prefix, _kWindowsExecutable);
+        return fs.file(path).existsSync();
+      }, orElse: () => '.');
+      executable = filePath;
+    } else {
+      throwToolExit('Platform ${platform.operatingSystem} is not supported.');
     }
-    throw UnsupportedError('$platform is not supported');
+    if (!fs.file(executable).existsSync()) {
+      throwToolExit('Chrome executable not found at $executable');
+    }
+    return processManager.start(<String>[
+      executable,
+      host,
+    ], mode: ProcessStartMode.detached);
   }
 }