Codegen an entrypoint for flutter web applications (#33956)

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 0ff1a9c..d829dea 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
@@ -3,13 +3,19 @@
 // found in the LICENSE file.
 
 // ignore_for_file: implementation_imports
+import 'dart:async';
+import 'dart:io' as io; // ignore: dart_io_import
+
 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_modules/src/workers.dart';
 import 'package:build_runner_core/build_runner_core.dart' as core;
+import 'package:build_runner_core/src/asset_graph/graph.dart';
+import 'package:build_runner_core/src/asset_graph/node.dart';
 import 'package:build_runner_core/src/generate/build_impl.dart';
 import 'package:build_runner_core/src/generate/options.dart';
 import 'package:build_test/builder.dart';
@@ -17,9 +23,11 @@
 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:crypto/crypto.dart';
 import 'package:logging/logging.dart';
 import 'package:meta/meta.dart';
 import 'package:path/path.dart' as path;
+import 'package:scratch_space/scratch_space.dart';
 import 'package:test_core/backend.dart';
 import 'package:watcher/watcher.dart';
 
@@ -28,6 +36,7 @@
 import '../base/logger.dart';
 import '../base/platform.dart';
 import '../compile.dart';
+import '../convert.dart';
 import '../dart/package_map.dart';
 import '../globals.dart';
 import '../web/compile.dart';
@@ -84,6 +93,22 @@
     ),
   ),
   core.apply(
+    'flutter_tools|shell',
+    <BuilderFactory>[
+      (BuilderOptions options) => FlutterWebShellBuilder(
+        options.config['targets'] ?? <String>['lib/main.dart']
+      ),
+    ],
+    core.toRoot(),
+    hideOutput: true,
+    defaultGenerateFor: const InputSet(
+      include: <String>[
+        'lib/**',
+        'web/**',
+      ],
+    ),
+  ),
+  core.apply(
       'flutter_tools|module_library',
       <Builder Function(BuilderOptions)>[moduleLibraryBuilder],
       core.toAllPackages(),
@@ -127,7 +152,9 @@
     'flutter_tools|entrypoint',
     <BuilderFactory>[
       (BuilderOptions options) => FlutterWebEntrypointBuilder(
-          options.config['targets'] ?? <String>['lib/main.dart']),
+          options.config['targets'] ?? <String>['lib/main.dart'],
+          options.config['release'],
+      ),
     ],
     core.toRoot(),
     hideOutput: true,
@@ -152,11 +179,15 @@
   PackageUriMapper _packageUriMapper;
 
   @override
-  Future<void> initialize({
+  Future<bool> initialize({
     @required Directory projectDirectory,
     @required List<String> targets,
     String testOutputDir,
+    bool release = false,
   }) async {
+    // Create the .dart_tool directory if it doesn't exist.
+    projectDirectory.childDirectory('.dart_tool').createSync();
+
     // Override the generated output directory so this does not conflict with
     // other build_runner output.
     core.overrideGeneratedOutputDirectory('flutter_web');
@@ -195,19 +226,36 @@
     };
     final Status status =
         logger.startProgress('Compiling ${targets.first} for the Web...', timeout: null);
+    core.BuildResult result;
     try {
-      _builder = await BuildImpl.create(
-        buildOptions,
+      result = await _runBuilder(
         buildEnvironment,
-        builders,
-        <String, Map<String, dynamic>>{
-          'flutter_tools|entrypoint': <String, dynamic>{
-            'targets': targets,
-          }
-        },
-        isReleaseBuild: false,
+        buildOptions,
+        targets,
+        release,
+        buildDirs,
       );
-      await _builder.run(const <AssetId, ChangeType>{}, buildDirs: buildDirs);
+      return result.status == core.BuildStatus.success;
+    } on core.BuildConfigChangedException {
+      await _cleanAssets(projectDirectory);
+      result = await _runBuilder(
+        buildEnvironment,
+        buildOptions,
+        targets,
+        release,
+        buildDirs,
+      );
+      return result.status == core.BuildStatus.success;
+    } on core.BuildScriptChangedException {
+      await _cleanAssets(projectDirectory);
+      result = await _runBuilder(
+        buildEnvironment,
+        buildOptions,
+        targets,
+        release,
+        buildDirs,
+      );
+      return result.status == core.BuildStatus.success;
     } finally {
       status.stop();
     }
@@ -219,9 +267,8 @@
         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;
+      final AssetId assetId = AssetId.resolve(_packageUriMapper.map(input.toFilePath()).toString());
+      updates[assetId] = ChangeType.MODIFY;
     }
     core.BuildResult result;
     try {
@@ -231,13 +278,81 @@
     }
     return result.status == core.BuildStatus.success;
   }
+
+
+  Future<core.BuildResult> _runBuilder(core.BuildEnvironment buildEnvironment, BuildOptions buildOptions, List<String> targets, bool release, Set<core.BuildDirectory> buildDirs) async {
+    _builder = await BuildImpl.create(
+      buildOptions,
+      buildEnvironment,
+      builders,
+      <String, Map<String, dynamic>>{
+        'flutter_tools|entrypoint': <String, dynamic>{
+          'targets': targets,
+          'release': release,
+        },
+        'flutter_tools|shell': <String, dynamic>{
+          'targets': targets,
+        }
+      },
+      isReleaseBuild: false,
+    );
+    return _builder.run(
+      const <AssetId, ChangeType>{},
+      buildDirs: buildDirs,
+    );
+  }
+
+  Future<void> _cleanAssets(Directory projectDirectory) async {
+    final File assetGraphFile = fs.file(core.assetGraphPath);
+    AssetGraph assetGraph;
+    try {
+      assetGraph = AssetGraph.deserialize(await assetGraphFile.readAsBytes());
+    } catch (_) {
+      printTrace('Failed to clean up asset graph.');
+    }
+    final core.PackageGraph packageGraph = core.PackageGraph.forThisPackage();
+    await _cleanUpSourceOutputs(assetGraph, packageGraph);
+    final Directory cacheDirectory = fs.directory(fs.path.join(
+      projectDirectory.path,
+      '.dart_tool',
+      'build',
+      'flutter_web',
+    ));
+    if (assetGraphFile.existsSync()) {
+      assetGraphFile.deleteSync();
+    }
+    if (cacheDirectory.existsSync()) {
+      cacheDirectory.deleteSync(recursive: true);
+    }
+  }
+
+  Future<void> _cleanUpSourceOutputs(AssetGraph assetGraph, core.PackageGraph packageGraph) async {
+    final core.FileBasedAssetWriter writer = core.FileBasedAssetWriter(packageGraph);
+    if (assetGraph?.outputs == null) {
+      return;
+    }
+    for (AssetId id in assetGraph.outputs) {
+      if (id.package != packageGraph.root.name) {
+        continue;
+      }
+      final GeneratedAssetNode node = assetGraph.get(id);
+      if (node.wasOutput) {
+        // Note that this does a file.exists check in the root package and
+        // only tries to delete the file if it exists. This way we only
+        // actually delete to_source outputs, without reading in the build
+        // actions.
+        await writer.delete(id);
+      }
+    }
+  }
 }
 
 /// A ddc-only entrypoint builder that respects the Flutter target flag.
 class FlutterWebEntrypointBuilder implements Builder {
-  const FlutterWebEntrypointBuilder(this.targets);
+  const FlutterWebEntrypointBuilder(this.targets, this.release);
 
   final List<String> targets;
+  final bool release;
 
   @override
   Map<String, List<String>> get buildExtensions => const <String, List<String>>{
@@ -254,7 +369,7 @@
   Future<void> build(BuildStep buildStep) async {
     bool matches = false;
     for (String target in targets) {
-      if (buildStep.inputId.path.contains(target)) {
+      if (buildStep.inputId.path.contains(fs.path.setExtension(target, '_web_entrypoint.dart'))) {
         matches = true;
         break;
       }
@@ -263,10 +378,15 @@
       return;
     }
     log.info('building for target ${buildStep.inputId.path}');
-    await bootstrapDdc(buildStep, platform: flutterWebPlatform);
+    if (release) {
+      await bootstrapDart2Js(buildStep);
+    } else {
+      await bootstrapDdc(buildStep, platform: flutterWebPlatform);
+    }
   }
 }
 
+/// Bootstraps the test entrypoint.
 class FlutterWebTestBootstrapBuilder implements Builder {
   const FlutterWebTestBootstrapBuilder();
 
@@ -372,3 +492,117 @@
   }
 }
 
+/// A shell builder which generates the web specific entrypoint.
+class FlutterWebShellBuilder implements Builder {
+  const FlutterWebShellBuilder(this.targets);
+
+  final List<String> targets;
+
+  @override
+  FutureOr<void> build(BuildStep buildStep) async {
+    bool matches = false;
+    for (String target in targets) {
+      if (buildStep.inputId.path.contains(target)) {
+        matches = true;
+        break;
+      }
+    }
+    if (!matches) {
+      return;
+    }
+    final AssetId outputId = buildStep.inputId.changeExtension('_web_entrypoint.dart');
+    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
+  Map<String, List<String>> get buildExtensions => const <String, List<String>>{
+    '.dart': <String>['_web_entrypoint.dart'],
+  };
+}
+
+Future<void> bootstrapDart2Js(BuildStep buildStep) async {
+  final AssetId dartEntrypointId = buildStep.inputId;
+  final AssetId moduleId = dartEntrypointId.changeExtension(moduleExtension(flutterWebPlatform));
+  final Module module = Module.fromJson(json.decode(await buildStep.readAsString(moduleId)));
+
+  final List<Module> allDeps = await module.computeTransitiveDependencies(buildStep, throwIfUnsupported: false)..add(module);
+  final ScratchSpace scratchSpace = await buildStep.fetchResource(scratchSpaceResource);
+  final Iterable<AssetId> allSrcs = allDeps.expand((Module module) => module.sources);
+  await scratchSpace.ensureAssets(allSrcs, buildStep);
+
+  final String packageFile = await _createPackageFile(allSrcs, buildStep, scratchSpace);
+  final String dartPath = dartEntrypointId.path.startsWith('lib/')
+      ? 'package:${dartEntrypointId.package}/'
+          '${dartEntrypointId.path.substring('lib/'.length)}'
+      : dartEntrypointId.path;
+  final String jsOutputPath =
+      '${fs.path.withoutExtension(dartPath.replaceFirst('package:', 'packages/'))}'
+      '$jsEntrypointExtension';
+  final String flutterWebSdkPath = artifacts.getArtifactPath(Artifact.flutterWebSdk);
+  final String librariesPath = fs.path.join(flutterWebSdkPath, 'libraries.json');
+  final List<String> args = <String>[
+    '--libraries-spec="$librariesPath"',
+    '-m',
+    '-o4',
+    '-o',
+    '$jsOutputPath',
+    '--packages="$packageFile"',
+    dartPath,
+  ];
+  final Dart2JsBatchWorkerPool dart2js = await buildStep.fetchResource(dart2JsWorkerResource);
+  final Dart2JsResult result = await dart2js.compile(args);
+  final AssetId jsOutputId = dartEntrypointId.changeExtension(jsEntrypointExtension);
+  final io.File jsOutputFile = scratchSpace.fileFor(jsOutputId);
+  if (result.succeeded && jsOutputFile.existsSync()) {
+    log.info(result.output);
+    // Explicitly write out the original js file and sourcemap.
+    await scratchSpace.copyOutput(jsOutputId, buildStep);
+    final AssetId jsSourceMapId =
+        dartEntrypointId.changeExtension(jsEntrypointSourceMapExtension);
+    await _copyIfExists(jsSourceMapId, scratchSpace, buildStep);
+  } else {
+    log.severe(result.output);
+  }
+}
+
+Future<void> _copyIfExists(
+    AssetId id, ScratchSpace scratchSpace, AssetWriter writer) async {
+  final io.File file = scratchSpace.fileFor(id);
+  if (file.existsSync()) {
+    await scratchSpace.copyOutput(id, writer);
+  }
+}
+
+/// Creates a `.packages` file unique to this entrypoint at the root of the
+/// scratch space and returns it's filename.
+///
+/// Since mulitple invocations of Dart2Js will share a scratch space and we only
+/// know the set of packages involved the current entrypoint we can't construct
+/// a `.packages` file that will work for all invocations of Dart2Js so a unique
+/// file is created for every entrypoint that is run.
+///
+/// The filename is based off the MD5 hash of the asset path so that files are
+/// unique regarless of situations like `web/foo/bar.dart` vs
+/// `web/foo-bar.dart`.
+Future<String> _createPackageFile(Iterable<AssetId> inputSources, BuildStep buildStep, ScratchSpace scratchSpace) async {
+  final Uri inputUri = buildStep.inputId.uri;
+  final String packageFileName =
+      '.package-${md5.convert(inputUri.toString().codeUnits)}';
+  final io.File packagesFile =
+      scratchSpace.fileFor(AssetId(buildStep.inputId.package, packageFileName));
+  final Set<String> packageNames = inputSources.map((AssetId s) => s.package).toSet();
+  final String packagesFileContent =
+      packageNames.map((String name) => '$name:packages/$name/').join('\n');
+  await packagesFile
+      .writeAsString('# Generated for $inputUri\n$packagesFileContent');
+  return packageFileName;
+}
diff --git a/packages/flutter_tools/lib/src/commands/build_web.dart b/packages/flutter_tools/lib/src/commands/build_web.dart
index 7ed5191..32cea06 100644
--- a/packages/flutter_tools/lib/src/commands/build_web.dart
+++ b/packages/flutter_tools/lib/src/commands/build_web.dart
@@ -4,10 +4,8 @@
 
 import 'dart:async';
 
-import '../base/common.dart';
-import '../base/logger.dart';
 import '../build_info.dart';
-import '../globals.dart';
+import '../project.dart';
 import '../runner/flutter_command.dart'
     show DevelopmentArtifact, FlutterCommandResult;
 import '../web/compile.dart';
@@ -41,34 +39,10 @@
 
   @override
   Future<FlutterCommandResult> runCommand() async {
+    final FlutterProject flutterProject = FlutterProject.current();
     final String target = argResults['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.');
-    }
+    await buildWeb(flutterProject, target, buildInfo);
     return null;
   }
 }
diff --git a/packages/flutter_tools/lib/src/context_runner.dart b/packages/flutter_tools/lib/src/context_runner.dart
index 51bb7e7..8aec528 100644
--- a/packages/flutter_tools/lib/src/context_runner.dart
+++ b/packages/flutter_tools/lib/src/context_runner.dart
@@ -44,7 +44,6 @@
 import 'usage.dart';
 import 'version.dart';
 import 'web/chrome.dart';
-import 'web/compile.dart';
 import 'web/workflow.dart';
 import 'windows/visual_studio.dart';
 import 'windows/visual_studio_validator.dart';
@@ -104,7 +103,6 @@
       UserMessages: () => UserMessages(),
       VisualStudio: () => VisualStudio(),
       VisualStudioValidator: () => const VisualStudioValidator(),
-      WebCompiler: () => const WebCompiler(),
       WebWorkflow: () => const WebWorkflow(),
       WindowsWorkflow: () => const WindowsWorkflow(),
       Xcode: () => Xcode(),
diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart
index 6b51446..03efe52 100644
--- a/packages/flutter_tools/lib/src/project.dart
+++ b/packages/flutter_tools/lib/src/project.dart
@@ -583,6 +583,9 @@
     return parent.directory.childDirectory('web').existsSync();
   }
 
+  /// The html file used to host the flutter web application.
+  File get indexFile => parent.directory.childDirectory('web').childFile('index.html');
+
   Future<void> ensureReadyForPlatformSpecificTooling() async {
     /// Generate index.html in build/web. Eventually we could support
     /// a custom html under the web sub directory.
diff --git a/packages/flutter_tools/lib/src/web/asset_server.dart b/packages/flutter_tools/lib/src/web/asset_server.dart
index 730692c..ebe41be 100644
--- a/packages/flutter_tools/lib/src/web/asset_server.dart
+++ b/packages/flutter_tools/lib/src/web/asset_server.dart
@@ -66,6 +66,7 @@
 
   /// An HTTP server which provides JavaScript and web assets to the browser.
   Future<void> _onRequest(HttpRequest request) async {
+    final String targetName = '${fs.path.basenameWithoutExtension(target)}_web_entrypoint';
     if (request.method != 'GET') {
       request.response.statusCode = HttpStatus.forbidden;
       await request.response.close();
@@ -103,17 +104,17 @@
         'flutter_web',
         flutterProject.manifest.appName,
         'lib',
-        '${fs.path.basename(target)}.js',
+        '$targetName.dart.js',
       ));
       await _completeRequest(request, file, 'text/javascript');
-    } else if (uri.path.endsWith('${fs.path.basename(target)}.bootstrap.js')) {
+    } else if (uri.path.endsWith('$targetName.dart.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',
+        '$targetName.dart.bootstrap.js',
       ));
       await _completeRequest(request, file, 'text/javascript');
     } else if (uri.path.contains('dart_sdk')) {
diff --git a/packages/flutter_tools/lib/src/web/compile.dart b/packages/flutter_tools/lib/src/web/compile.dart
index 732769a..90b7ea3 100644
--- a/packages/flutter_tools/lib/src/web/compile.dart
+++ b/packages/flutter_tools/lib/src/web/compile.dart
@@ -4,80 +4,56 @@
 
 import 'package:meta/meta.dart';
 
-import '../artifacts.dart';
+import '../asset.dart';
 import '../base/common.dart';
 import '../base/context.dart';
 import '../base/file_system.dart';
-import '../base/io.dart';
-import '../base/process_manager.dart';
+import '../base/logger.dart';
 import '../build_info.dart';
-import '../convert.dart';
+import '../bundle.dart';
 import '../globals.dart';
-
-/// The [WebCompiler] instance.
-WebCompiler get webCompiler => context.get<WebCompiler>();
+import '../project.dart';
 
 /// The [WebCompilationProxy] instance.
-WebCompilationProxy get webCompilationProxy =>
-    context.get<WebCompilationProxy>();
+WebCompilationProxy get webCompilationProxy => context.get<WebCompilationProxy>();
 
-/// A wrapper around dart tools for web compilation.
-class WebCompiler {
-  const WebCompiler();
+Future<void> buildWeb(FlutterProject flutterProject, String target, BuildInfo buildInfo) async {
+  final Status status = logger.startProgress('Compiling $target for the Web...', timeout: null);
+  final Directory outputDir = fs.directory(getWebBuildDirectory())
+    ..createSync(recursive: true);
+  bool result;
+  try {
+    result = await webCompilationProxy.initialize(
+      projectDirectory: FlutterProject.current().directory,
+      targets: <String>[target],
+      release: buildInfo.isRelease,
+    );
+    if (result) {
+      // Places assets adjacent to the web stuff.
+      final AssetBundle assetBundle = AssetBundleFactory.instance.createBundle();
+      await assetBundle.build();
+      await writeBundle(fs.directory(fs.path.join(outputDir.path, 'assets')), assetBundle.entries);
 
-  /// Compile `target` using dart2js.
-  ///
-  /// `minify` controls whether minifaction of the source is enabled. Defaults to `true`.
-  /// `enabledAssertions` controls whether assertions are enabled. Defaults to `false`.
-  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);
+      // Copy results to output directory.
+      final String outputPath = fs.path.join(
+        flutterProject.dartTool.path,
+        'build',
+        'flutter_web',
+        flutterProject.manifest.appName,
+        '${fs.path.withoutExtension(target)}_web_entrypoint.dart.js'
+      );
+      fs.file(outputPath).copySync(fs.path.join(outputDir.path, 'main.dart.js'));
+      fs.file('$outputPath.map').copySync(fs.path.join(outputDir.path, 'main.dart.js.map'));
+      flutterProject.web.indexFile.copySync(fs.path.join(outputDir.path, 'index.html'));
     }
-    final String outputPath = fs.path.join(outputDir.path, 'main.dart.js');
-    if (!processManager.canRun(engineDartPath)) {
-      throwToolExit('Unable to find Dart binary at $engineDartPath');
-    }
-
-    /// Compile Dart to JavaScript.
-    final List<String> command = <String>[
-      engineDartPath,
-      dart2jsPath,
-      target,
-      '-o',
-      '$outputPath',
-      '-O4',
-      '--libraries-spec=$librariesPath',
-    ];
-    if (minify) {
-      command.add('-m');
-    }
-    if (enabledAssertions) {
-      command.add('--enable-asserts');
-    }
-    printTrace(command.join(' '));
-    final Process result = await processManager.start(command);
-    result.stdout
-        .transform(utf8.decoder)
-        .transform(const LineSplitter())
-        .listen(printStatus);
-    result.stderr
-        .transform(utf8.decoder)
-        .transform(const LineSplitter())
-        .listen(printError);
-    return result.exitCode;
+  } catch (err) {
+    printError(err.toString());
+    result = false;
+  } finally {
+    status.stop();
+  }
+  if (result == false) {
+    throwToolExit('Failed to compile $target for the Web.');
   }
 }
 
@@ -87,12 +63,19 @@
 class WebCompilationProxy {
   const WebCompilationProxy();
 
-  /// Initialize the web compiler output to `outputDirectory` from a project spawned at
-  /// `projectDirectory`.
-  Future<void> initialize({
+  /// Initialize the web compiler from the `projectDirectory`.
+  ///
+  /// Returns whether or not the build was successful.
+  ///
+  /// `release` controls whether we build the bundle for dartdevc or only
+  /// the entrypoints for dart2js to later take over.
+  ///
+  /// `targets` controls the specific compiler targets.
+  Future<bool> initialize({
     @required Directory projectDirectory,
     @required List<String> targets,
     String testOutputDir,
+    bool release,
   }) 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 b824343..86a10e8 100644
--- a/packages/flutter_tools/lib/src/web/web_device.dart
+++ b/packages/flutter_tools/lib/src/web/web_device.dart
@@ -5,15 +5,11 @@
 import 'package:meta/meta.dart';
 
 import '../application_package.dart';
-import '../asset.dart';
-import '../base/common.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 '../bundle.dart';
 import '../device.dart';
 import '../globals.dart';
 import '../project.dart';
@@ -22,15 +18,15 @@
 import 'chrome.dart';
 
 class WebApplicationPackage extends ApplicationPackage {
-  WebApplicationPackage(this._flutterProject) : super(id: _flutterProject.manifest.appName);
+  WebApplicationPackage(this.flutterProject) : super(id: flutterProject.manifest.appName);
 
-  final FlutterProject _flutterProject;
+  final FlutterProject flutterProject;
 
   @override
-  String get name => _flutterProject.manifest.appName;
+  String get name => flutterProject.manifest.appName;
 
   /// The location of the web source assets.
-  Directory get webSourcePath => _flutterProject.directory.childDirectory('web');
+  Directory get webSourcePath => flutterProject.directory.childDirectory('web');
 }
 
 class WebDevice extends Device {
@@ -121,20 +117,11 @@
     bool usesTerminalUi = true,
     bool ipv6 = false,
   }) async {
-    final Status status = logger.startProgress('Compiling ${package.name} to JavaScript...', timeout: null);
-    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');
-      return LaunchResult.failed();
-    }
-    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);
-
+    await buildWeb(
+      package.flutterProject,
+      fs.path.relative(mainPath, from: package.flutterProject.directory.path),
+      debuggingOptions.buildInfo,
+    );
     _package = package;
     _server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
     _server.listen(_basicAssetServer);
diff --git a/packages/flutter_tools/test/web/compile_test.dart b/packages/flutter_tools/test/web/compile_test.dart
deleted file mode 100644
index 425c79d..0000000
--- a/packages/flutter_tools/test/web/compile_test.dart
+++ /dev/null
@@ -1,50 +0,0 @@
-// 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 'package:flutter_tools/src/artifacts.dart';
-import 'package:flutter_tools/src/globals.dart';
-import 'package:flutter_tools/src/web/compile.dart';
-import 'package:mockito/mockito.dart';
-import 'package:process/process.dart';
-
-import '../src/common.dart';
-import '../src/mocks.dart';
-import '../src/testbed.dart';
-
-void main() {
-  group(WebCompiler, () {
-    MockProcessManager mockProcessManager;
-    Testbed testBed;
-
-    setUp(() {
-      mockProcessManager = MockProcessManager();
-      testBed = Testbed(setup: () async {
-        final String engineDartPath = artifacts.getArtifactPath(Artifact.engineDartBinary);
-        when(mockProcessManager.start(any)).thenAnswer((Invocation invocation) async => FakeProcess());
-        when(mockProcessManager.canRun(engineDartPath)).thenReturn(true);
-
-      }, overrides: <Type, Generator>{
-        ProcessManager: () => mockProcessManager,
-      });
-    });
-
-    test('invokes dart2js with correct arguments', () => testBed.run(() async {
-      await webCompiler.compileDart2js(target: 'lib/main.dart');
-
-      verify(mockProcessManager.start(<String>[
-        'bin/cache/dart-sdk/bin/dart',
-        'bin/cache/dart-sdk/bin/snapshots/dart2js.dart.snapshot',
-        'lib/main.dart',
-        '-o',
-        'build/web/main.dart.js',
-        '-O4',
-        '--libraries-spec=bin/cache/flutter_web_sdk/libraries.json',
-        '-m',
-      ])).called(1);
-
-    }));
-  });
-}
-
-class MockProcessManager extends Mock implements ProcessManager {}
diff --git a/packages/flutter_tools/test/web/devices_test.dart b/packages/flutter_tools/test/web/devices_test.dart
index 62f94e1..5541048 100644
--- a/packages/flutter_tools/test/web/devices_test.dart
+++ b/packages/flutter_tools/test/web/devices_test.dart
@@ -2,12 +2,9 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import 'package:flutter_tools/src/base/file_system.dart';
 import 'package:flutter_tools/src/base/io.dart';
 import 'package:flutter_tools/src/base/platform.dart';
-import 'package:flutter_tools/src/project.dart';
 import 'package:flutter_tools/src/web/chrome.dart';
-import 'package:flutter_tools/src/web/compile.dart';
 import 'package:flutter_tools/src/web/web_device.dart';
 import 'package:mockito/mockito.dart';
 import 'package:process/process.dart';
@@ -17,37 +14,18 @@
 
 void main() {
   group(WebDevice, () {
-   MockWebCompiler mockWebCompiler;
    MockChromeLauncher mockChromeLauncher;
    MockPlatform mockPlatform;
-   FlutterProject flutterProject;
    MockProcessManager mockProcessManager;
 
     setUp(() async {
       mockProcessManager = MockProcessManager();
       mockChromeLauncher = MockChromeLauncher();
       mockPlatform = MockPlatform();
-      mockWebCompiler = MockWebCompiler();
-      flutterProject = FlutterProject.fromPath(fs.path.join(getFlutterRoot(), 'dev', 'integration_tests', 'web'));
-      when(mockWebCompiler.compileDart2js(
-        target: anyNamed('target'),
-        minify: anyNamed('minify'),
-        enabledAssertions: anyNamed('enabledAssertions'),
-      )).thenAnswer((Invocation invocation) async => 0);
       when(mockChromeLauncher.launch(any)).thenAnswer((Invocation invocation) async {
         return null;
       });
     });
-
-    testUsingContext('can build and connect to chrome', () async {
-      final WebDevice device = WebDevice();
-      await device.startApp(WebApplicationPackage(flutterProject));
-    }, overrides: <Type, Generator>{
-      ChromeLauncher: () => mockChromeLauncher,
-      WebCompiler: () => mockWebCompiler,
-      Platform: () => mockPlatform,
-    });
-
     testUsingContext('Invokes version command on non-Windows platforms', () async{
       when(mockPlatform.isWindows).thenReturn(false);
       when(mockPlatform.environment).thenReturn(<String, String>{
@@ -86,7 +64,6 @@
 }
 
 class MockChromeLauncher extends Mock implements ChromeLauncher {}
-class MockWebCompiler extends Mock implements WebCompiler {}
 class MockPlatform extends Mock implements Platform {}
 class MockProcessManager extends Mock implements ProcessManager {}
 class MockProcessResult extends Mock implements ProcessResult {