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

diff --git a/packages/flutter_tools/lib/executable.dart b/packages/flutter_tools/lib/executable.dart
index 1e75509..42c1808 100644
--- a/packages/flutter_tools/lib/executable.dart
+++ b/packages/flutter_tools/lib/executable.dart
@@ -10,6 +10,8 @@
 // avoid introducing the dependency into google3. Not all build* packages
 // are synced internally.
 import 'src/build_runner/build_runner.dart';
+import 'src/build_runner/web_compilation_delegate.dart';
+
 import 'src/codegen.dart';
 import 'src/commands/analyze.dart';
 import 'src/commands/attach.dart';
@@ -42,6 +44,7 @@
 import 'src/commands/upgrade.dart';
 import 'src/commands/version.dart';
 import 'src/runner/flutter_command.dart';
+import 'src/web/compile.dart';
 
 /// Main entry point for commands.
 ///
@@ -94,5 +97,6 @@
        // The build runner instance is not supported in google3 because
        // the build runner packages are not synced internally.
        CodeGenerator: () => const BuildRunner(),
+       WebCompilationProxy: () => BuildRunnerWebCompilationProxy(),
      });
 }
diff --git a/packages/flutter_tools/lib/src/artifacts.dart b/packages/flutter_tools/lib/src/artifacts.dart
index 8dae706..259ba67 100644
--- a/packages/flutter_tools/lib/src/artifacts.dart
+++ b/packages/flutter_tools/lib/src/artifacts.dart
@@ -28,6 +28,7 @@
   engineDartSdkPath,
   engineDartBinary,
   dart2jsSnapshot,
+  dartdevcSnapshot,
   kernelWorkerSnapshot,
   flutterWebSdk,
 }
@@ -80,6 +81,8 @@
       return 'dart';
     case Artifact.dart2jsSnapshot:
       return 'dart2js.dart.snapshot';
+    case Artifact.dartdevcSnapshot:
+      return 'dartdevc.dart.snapshot';
     case Artifact.kernelWorkerSnapshot:
       return 'kernel_worker.dart.snapshot';
   }
@@ -212,6 +215,8 @@
         return _getFlutterWebSdkPath();
       case Artifact.dart2jsSnapshot:
         return fs.path.join(dartSdkPath, 'bin', 'snapshots', _artifactToFileName(artifact));
+      case Artifact.dartdevcSnapshot:
+        return fs.path.join(dartSdkPath, 'bin', 'snapshots', _artifactToFileName(artifact));
       case Artifact.kernelWorkerSnapshot:
         return fs.path.join(dartSdkPath, 'bin', 'snapshots', _artifactToFileName(artifact));
       case Artifact.flutterMacOSFramework:
@@ -304,6 +309,8 @@
         return fs.path.join(_hostEngineOutPath, 'dart-sdk', 'bin', _artifactToFileName(artifact));
       case Artifact.dart2jsSnapshot:
         return fs.path.join(_hostEngineOutPath, 'dart-sdk', 'bin', 'snapshots', _artifactToFileName(artifact));
+      case Artifact.dartdevcSnapshot:
+        return fs.path.join(dartSdkPath, 'bin', 'snapshots', _artifactToFileName(artifact));
       case Artifact.kernelWorkerSnapshot:
         return fs.path.join(_hostEngineOutPath, 'dart-sdk', 'bin', 'snapshots', _artifactToFileName(artifact));
     }
diff --git a/packages/flutter_tools/lib/src/build_runner/build_runner.dart b/packages/flutter_tools/lib/src/build_runner/build_runner.dart
index 7c7ee32..de2d00d 100644
--- a/packages/flutter_tools/lib/src/build_runner/build_runner.dart
+++ b/packages/flutter_tools/lib/src/build_runner/build_runner.dart
@@ -155,6 +155,7 @@
         .path;
     final Status status = logger.startProgress('starting build daemon...', timeout: null);
     BuildDaemonClient buildDaemonClient;
+    final String path = cache.getArtifactDirectory('web-sdk').path;
     try {
       final List<String> command = <String>[
         engineDartBinaryPath,
@@ -162,7 +163,8 @@
         buildSnapshot.path,
         'daemon',
          '--skip-build-script-check',
-         '--delete-conflicting-outputs'
+         '--delete-conflicting-outputs',
+         '--define', 'build|ddc=flutter_sdk_dir=$path',
       ];
       buildDaemonClient = await BuildDaemonClient.connect(
         flutterProject.directory.path,
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
new file mode 100644
index 0000000..913746b
--- /dev/null
+++ b/packages/flutter_tools/lib/src/build_runner/web_compilation_delegate.dart
@@ -0,0 +1,231 @@
+// 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.
+
+// ignore_for_file: implementation_imports
+import 'package:build/build.dart';
+import 'package:build_config/build_config.dart';
+import 'package:build_modules/build_modules.dart';
+import 'package:build_modules/builders.dart';
+import 'package:build_modules/src/module_builder.dart';
+import 'package:build_modules/src/platform.dart';
+import 'package:build_runner_core/build_runner_core.dart' as core;
+import 'package:build_runner_core/src/generate/build_impl.dart';
+import 'package:build_runner_core/src/generate/options.dart';
+import 'package:build_web_compilers/build_web_compilers.dart';
+import 'package:build_web_compilers/builders.dart';
+import 'package:build_web_compilers/src/dev_compiler_bootstrap.dart';
+import 'package:logging/logging.dart';
+import 'package:meta/meta.dart';
+import 'package:path/path.dart' as path;
+import 'package:watcher/watcher.dart';
+
+import '../artifacts.dart';
+import '../base/file_system.dart';
+import '../base/logger.dart';
+import '../compile.dart';
+import '../dart/package_map.dart';
+import '../globals.dart';
+import '../web/compile.dart';
+
+const String ddcBootstrapExtension = '.dart.bootstrap.js';
+const String jsEntrypointExtension = '.dart.js';
+const String jsEntrypointSourceMapExtension = '.dart.js.map';
+const String jsEntrypointArchiveExtension = '.dart.js.tar.gz';
+const String digestsEntrypointExtension = '.digests';
+const String jsModuleErrorsExtension = '.ddc.js.errors';
+const String jsModuleExtension = '.ddc.js';
+const String jsSourceMapExtension = '.ddc.js.map';
+
+final DartPlatform flutterWebPlatform =
+    DartPlatform.register('flutter_web', <String>[
+  'async',
+  'collection',
+  'convert',
+  'core',
+  'developer',
+  'html',
+  'html_common',
+  'indexed_db',
+  'js',
+  'js_util',
+  'math',
+  'svg',
+  'typed_data',
+  'web_audio',
+  'web_gl',
+  'web_sql',
+  '_internal',
+  // Flutter web specific libraries.
+  'ui',
+  '_engine',
+  'io',
+  'isolate',
+]);
+
+/// The build application to compile a flutter application to the web.
+final List<core.BuilderApplication> builders = <core.BuilderApplication>[
+  core.apply(
+      'flutter_tools|module_library',
+      <Builder Function(BuilderOptions)>[moduleLibraryBuilder],
+      core.toAllPackages(),
+      isOptional: true,
+      hideOutput: true,
+      appliesBuilders: <String>['flutter_tools|module_cleanup']),
+  core.apply(
+      'flutter_tools|ddc_modules',
+      <Builder Function(BuilderOptions)>[
+        (BuilderOptions options) => MetaModuleBuilder(flutterWebPlatform),
+        (BuilderOptions options) => MetaModuleCleanBuilder(flutterWebPlatform),
+        (BuilderOptions options) => ModuleBuilder(flutterWebPlatform),
+      ],
+      core.toNoneByDefault(),
+      isOptional: true,
+      hideOutput: true,
+      appliesBuilders: <String>['flutter_tools|module_cleanup']),
+  core.apply(
+      'flutter_tools|ddc',
+      <Builder Function(BuilderOptions)>[
+        (BuilderOptions builderOptions) => KernelBuilder(
+              platformSdk: artifacts.getArtifactPath(Artifact.flutterWebSdk),
+              summaryOnly: true,
+              sdkKernelPath: path.join('kernel', 'flutter_ddc_sdk.dill'),
+              outputExtension: ddcKernelExtension,
+              platform: flutterWebPlatform,
+              librariesPath: 'libraries.json',
+            ),
+        (BuilderOptions builderOptions) => DevCompilerBuilder(
+              useIncrementalCompiler: false,
+              platform: flutterWebPlatform,
+              platformSdk: artifacts.getArtifactPath(Artifact.flutterWebSdk),
+              sdkKernelPath: path.join('kernel', 'flutter_ddc_sdk.dill'),
+            ),
+      ],
+      core.toAllPackages(),
+      isOptional: true,
+      hideOutput: true,
+      appliesBuilders: <String>['flutter_tools|ddc_modules']),
+  core.apply(
+    'flutter_tools|entrypoint',
+    <BuilderFactory>[
+      (BuilderOptions options) => FlutterWebEntrypointBuilder(
+          options.config['target'] ?? 'lib/main.dart'),
+    ],
+    core.toRoot(),
+    hideOutput: true,
+    defaultGenerateFor: const InputSet(
+      include: <String>[
+        'lib/**',
+        'web/**',
+      ],
+    ),
+  ),
+  core.applyPostProcess('flutter_tools|module_cleanup', moduleCleanup,
+      defaultGenerateFor: const InputSet())
+];
+
+/// A build_runner specific implementation of the [WebCompilationProxy].
+class BuildRunnerWebCompilationProxy extends WebCompilationProxy {
+  BuildRunnerWebCompilationProxy();
+
+  core.PackageGraph _packageGraph;
+  BuildImpl _builder;
+  PackageUriMapper _packageUriMapper;
+
+  @override
+  Future<void> initialize({
+    @required Directory projectDirectory,
+    @required String target,
+  }) async {
+    // Override the generated output directory so this does not conflict with
+    // other build_runner output.
+    core.overrideGeneratedOutputDirectory('flutter_web');
+    _packageUriMapper = PackageUriMapper(
+        path.absolute(target), 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) {
+        printError(record.message);
+      } else {
+        printTrace(record.message);
+      }
+    });
+    final LogSubscription logSubscription = LogSubscription(
+      buildEnvironment,
+      verbose: false,
+      logLevel: Level.FINE,
+    );
+    final BuildOptions buildOptions = await BuildOptions.create(
+      logSubscription,
+      packageGraph: _packageGraph,
+      skipBuildScriptCheck: true,
+      trackPerformance: false,
+      deleteFilesByDefault: true,
+    );
+    final Status status =
+        logger.startProgress('Compiling $target for the Web...', timeout: null);
+    try {
+      _builder = await BuildImpl.create(
+        buildOptions,
+        buildEnvironment,
+        builders,
+        <String, Map<String, dynamic>>{
+          'flutter_tools|entrypoint': <String, dynamic>{
+            'target': target,
+          }
+        },
+        isReleaseBuild: false,
+      );
+      await _builder.run(const <AssetId, ChangeType>{});
+    } finally {
+      status.stop();
+    }
+  }
+
+  @override
+  Future<bool> invalidate({@required List<Uri> inputs}) async {
+    final Status status =
+        logger.startProgress('Recompiling sources...', timeout: null);
+    final Map<AssetId, ChangeType> updates = <AssetId, ChangeType>{};
+    for (Uri input in inputs) {
+      updates[AssetId.resolve(
+              _packageUriMapper.map(input.toFilePath()).toString())] =
+          ChangeType.MODIFY;
+    }
+    core.BuildResult result;
+    try {
+      result = await _builder.run(updates);
+    } finally {
+      status.cancel();
+    }
+    return result.status == core.BuildStatus.success;
+  }
+}
+
+/// A ddc-only entrypoint builder that respects the Flutter target flag.
+class FlutterWebEntrypointBuilder implements Builder {
+  const FlutterWebEntrypointBuilder(this.target);
+
+  final String target;
+
+  @override
+  Map<String, List<String>> get buildExtensions => const <String, List<String>>{
+        '.dart': <String>[
+          ddcBootstrapExtension,
+          jsEntrypointExtension,
+          jsEntrypointSourceMapExtension,
+          jsEntrypointArchiveExtension,
+          digestsEntrypointExtension,
+        ],
+      };
+
+  @override
+  Future<void> build(BuildStep buildStep) async {
+    if (!buildStep.inputId.path.contains(target)) {
+      return;
+    }
+    log.info('building for target ${buildStep.inputId.path}');
+    await bootstrapDdc(buildStep, platform: flutterWebPlatform);
+  }
+}
diff --git a/packages/flutter_tools/lib/src/commands/build_web.dart b/packages/flutter_tools/lib/src/commands/build_web.dart
index 8a1fd9e..7ed5191 100644
--- a/packages/flutter_tools/lib/src/commands/build_web.dart
+++ b/packages/flutter_tools/lib/src/commands/build_web.dart
@@ -8,7 +8,8 @@
 import '../base/logger.dart';
 import '../build_info.dart';
 import '../globals.dart';
-import '../runner/flutter_command.dart' show DevelopmentArtifact, FlutterCommandResult;
+import '../runner/flutter_command.dart'
+    show DevelopmentArtifact, FlutterCommandResult;
 import '../web/compile.dart';
 import 'build.dart';
 
@@ -16,14 +17,15 @@
   BuildWebCommand() {
     usesTargetOption();
     usesPubOption();
-    defaultBuildMode = BuildMode.release;
+    addBuildModeFlags();
   }
 
   @override
-  Future<Set<DevelopmentArtifact>> get requiredArtifacts async => const <DevelopmentArtifact>{
-    DevelopmentArtifact.universal,
-    DevelopmentArtifact.web,
-  };
+  Future<Set<DevelopmentArtifact>> get requiredArtifacts async =>
+      const <DevelopmentArtifact>{
+        DevelopmentArtifact.universal,
+        DevelopmentArtifact.web,
+      };
 
   @override
   final String name = 'web';
@@ -40,8 +42,29 @@
   @override
   Future<FlutterCommandResult> runCommand() async {
     final String target = argResults['target'];
-    final Status status = logger.startProgress('Compiling $target to JavaScript...', timeout: null);
-    final int result = await webCompiler.compile(target: target);
+    final Status status = logger
+        .startProgress('Compiling $target for the Web...', timeout: null);
+    final BuildInfo buildInfo = getBuildInfo();
+    int result;
+    switch (buildInfo.mode) {
+      case BuildMode.release:
+        result = await webCompiler.compileDart2js(target: target);
+        break;
+      case BuildMode.profile:
+        result = await webCompiler.compileDart2js(target: target, minify: false);
+        break;
+      case BuildMode.debug:
+        throwToolExit(
+            'Debug mode is not supported as a build target. Instead use '
+            '"flutter run -d web".');
+        break;
+      case BuildMode.dynamicProfile:
+      case BuildMode.dynamicRelease:
+        throwToolExit(
+            'Build mode ${buildInfo.mode} is not supported with JavaScript '
+            'compilation');
+        break;
+    }
     status.stop();
     if (result == 1) {
       throwToolExit('Failed to compile $target to JavaScript.');
diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart
index 4e08dd1..55292dc 100644
--- a/packages/flutter_tools/lib/src/commands/run.dart
+++ b/packages/flutter_tools/lib/src/commands/run.dart
@@ -15,11 +15,13 @@
 import '../macos/xcode.dart';
 import '../project.dart';
 import '../resident_runner.dart';
+import '../resident_web_runner.dart';
 import '../run_cold.dart';
 import '../run_hot.dart';
 import '../runner/flutter_command.dart';
 import '../tracing.dart';
 import '../usage.dart';
+import '../version.dart';
 import 'daemon.dart';
 
 abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopmentArtifacts {
@@ -397,10 +399,16 @@
       );
       flutterDevices.add(flutterDevice);
     }
+    // Only support "web mode" on non-stable branches with a single web device
+    // in a "hot mode".
+    final bool webMode = !FlutterVersion.instance.isStable
+      && devices.length == 1
+      && await devices.single.targetPlatform == TargetPlatform.web
+      && hotMode;
 
     ResidentRunner runner;
     final String applicationBinaryPath = argResults['use-application-binary'];
-    if (hotMode) {
+    if (hotMode && !webMode) {
       runner = HotRunner(
         flutterDevices,
         target: targetFile,
@@ -416,6 +424,13 @@
         stayResident: stayResident,
         ipv6: ipv6,
       );
+    } else if (webMode) {
+      runner = ResidentWebRunner(
+        flutterDevices,
+        target: targetFile,
+        flutterProject: flutterProject,
+        ipv6: ipv6,
+      );
     } else {
       runner = ColdRunner(
         flutterDevices,
diff --git a/packages/flutter_tools/lib/src/commands/update_packages.dart b/packages/flutter_tools/lib/src/commands/update_packages.dart
index 2e33161..c3afb68 100644
--- a/packages/flutter_tools/lib/src/commands/update_packages.dart
+++ b/packages/flutter_tools/lib/src/commands/update_packages.dart
@@ -23,6 +23,7 @@
 const Map<String, String> _kManuallyPinnedDependencies = <String, String>{
   // Add pinned packages here.
   'flutter_gallery_assets': '0.1.8', // See //examples/flutter_gallery/pubspec.yaml
+  'build_daemon': '0.6.1',
 };
 
 class UpdatePackagesCommand extends FlutterCommand {
diff --git a/packages/flutter_tools/lib/src/resident_web_runner.dart b/packages/flutter_tools/lib/src/resident_web_runner.dart
new file mode 100644
index 0000000..1652628
--- /dev/null
+++ b/packages/flutter_tools/lib/src/resident_web_runner.dart
@@ -0,0 +1,161 @@
+// 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 'dart:async';
+
+import 'package:meta/meta.dart';
+
+import 'asset.dart';
+import 'base/common.dart';
+import 'base/file_system.dart';
+import 'base/terminal.dart';
+import 'build_info.dart';
+import 'bundle.dart';
+import 'dart/package_map.dart';
+import 'device.dart';
+import 'globals.dart';
+import 'project.dart';
+import 'resident_runner.dart';
+import 'run_hot.dart';
+import 'web/asset_server.dart';
+import 'web/compile.dart';
+import 'web/web_device.dart';
+
+/// A hot-runner which handles browser specific delegation.
+class ResidentWebRunner extends ResidentRunner {
+  ResidentWebRunner(
+    List<FlutterDevice> flutterDevices, {
+    String target,
+    @required this.flutterProject,
+    @required bool ipv6,
+  }) : super(
+          flutterDevices,
+          target: target,
+          usesTerminalUI: true,
+          stayResident: true,
+          saveCompilationTrace: false,
+          debuggingOptions: DebuggingOptions.enabled(
+            const BuildInfo(BuildMode.debug, ''),
+          ),
+          ipv6: ipv6,
+        );
+
+  WebAssetServer _server;
+  ProjectFileInvalidator projectFileInvalidator;
+  DateTime _lastCompiled;
+  final FlutterProject flutterProject;
+
+  @override
+  Future<int> attach(
+      {Completer<DebugConnectionInfo> connectionInfoCompleter,
+      Completer<void> appStartedCompleter}) async {
+    connectionInfoCompleter?.complete(DebugConnectionInfo());
+    setupTerminal();
+    final int result = await waitForAppToFinish();
+    await cleanupAtFinish();
+    return result;
+  }
+
+  @override
+  Future<void> cleanupAfterSignal() {
+    return _server?.dispose();
+  }
+
+  @override
+  Future<void> cleanupAtFinish() {
+    return _server?.dispose();
+  }
+
+  @override
+  Future<void> handleTerminalCommand(String code) async {
+    if (code == 'R') {
+      // If hot restart is not supported for all devices, ignore the command.
+      if (!canHotRestart) {
+        return;
+      }
+      await restart(fullRestart: true);
+    }
+  }
+
+  @override
+  void printHelp({bool details}) {
+    const String fire = '🔥';
+    const String rawMessage =
+        '  To hot restart (and rebuild state), press "R".';
+    final String message = terminal.color(
+      fire + terminal.bolden(rawMessage),
+      TerminalColor.red,
+    );
+    printStatus(message);
+    const String quitMessage = 'To quit, press "q".';
+    printStatus('For a more detailed help message, press "h". $quitMessage');
+  }
+
+  @override
+  Future<int> run({
+    Completer<DebugConnectionInfo> connectionInfoCompleter,
+    Completer<void> appStartedCompleter,
+    String route,
+    bool shouldBuild = true,
+  }) async {
+    final FlutterProject currentProject = FlutterProject.current();
+    if (!fs.isFileSync(mainPath)) {
+      String message = 'Tried to run $mainPath, but that file does not exist.';
+      if (target == null) {
+        message +=
+            '\nConsider using the -t option to specify the Dart file to start.';
+      }
+      printError(message);
+      return 1;
+    }
+    // Start the web compiler and build the assets.
+    await webCompilationProxy.initialize(
+      projectDirectory: currentProject.directory,
+      target: target,
+    );
+    _lastCompiled = DateTime.now();
+    final AssetBundle assetBundle = AssetBundleFactory.instance.createBundle();
+    final int build = await assetBundle.build();
+    if (build != 0) {
+      throwToolExit('Error: Failed to build asset bundle');
+    }
+    await writeBundle(
+        fs.directory(getAssetBuildDirectory()), assetBundle.entries);
+
+    // Step 2: Start an HTTP server
+    _server = WebAssetServer(flutterProject, target, ipv6);
+    await _server.initialize();
+
+    // Step 3: Spawn an instance of Chrome and direct it to the created server.
+    await chromeLauncher.launch('http:localhost:${_server.port}');
+
+    // We don't support the debugging proxy yet.
+    appStartedCompleter?.complete();
+    return attach(
+      connectionInfoCompleter: connectionInfoCompleter,
+      appStartedCompleter: appStartedCompleter,
+    );
+  }
+
+  @override
+  Future<OperationResult> restart(
+      {bool fullRestart = false,
+      bool pauseAfterRestart = false,
+      String reason,
+      bool benchmarkMode = false}) async {
+    final List<Uri> invalidatedSources = ProjectFileInvalidator.findInvalidated(
+      lastCompiled: _lastCompiled,
+      urisToMonitor: <Uri>[
+        for (FileSystemEntity entity in flutterProject.directory
+            .childDirectory('lib')
+            .listSync(recursive: true))
+          if (entity is File && entity.path.endsWith('.dart')) entity.uri
+      ], // Add new class to track this for web.
+      packagesPath: PackageMap.globalPackagesPath,
+    );
+    await webCompilationProxy.invalidate(inputs: invalidatedSources);
+    printStatus('Sources updated, refresh browser');
+    return OperationResult.ok;
+  }
+}
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);
   }
 }