Wire dart2js through flutter tool, add compilation test (#27668)

diff --git a/packages/flutter_tools/lib/src/application_package.dart b/packages/flutter_tools/lib/src/application_package.dart
index fe8f968..989ca6f 100644
--- a/packages/flutter_tools/lib/src/application_package.dart
+++ b/packages/flutter_tools/lib/src/application_package.dart
@@ -49,6 +49,7 @@
       case TargetPlatform.linux_x64:
       case TargetPlatform.windows_x64:
       case TargetPlatform.fuchsia:
+      case TargetPlatform.web:
         return null;
     }
     assert(platform != null);
@@ -352,6 +353,7 @@
       case TargetPlatform.windows_x64:
       case TargetPlatform.fuchsia:
       case TargetPlatform.tester:
+      case TargetPlatform.web:
         return null;
     }
     return null;
diff --git a/packages/flutter_tools/lib/src/artifacts.dart b/packages/flutter_tools/lib/src/artifacts.dart
index 6cfb8bc..9bf9280 100644
--- a/packages/flutter_tools/lib/src/artifacts.dart
+++ b/packages/flutter_tools/lib/src/artifacts.dart
@@ -26,6 +26,8 @@
   frontendServerSnapshotForEngineDartSdk,
   engineDartSdkPath,
   engineDartBinary,
+  dart2jsSnapshot,
+  kernelWorkerSnapshot,
 }
 
 String _artifactToFileName(Artifact artifact, [TargetPlatform platform, BuildMode mode]) {
@@ -70,6 +72,10 @@
       return 'frontend_server.dart.snapshot';
     case Artifact.engineDartBinary:
       return 'dart';
+    case Artifact.dart2jsSnapshot:
+      return 'flutter_dart2js.dart.snapshot';
+    case Artifact.kernelWorkerSnapshot:
+      return 'flutter_kernel_worker.dart.snapshot';
   }
   assert(false, 'Invalid artifact $artifact.');
   return null;
@@ -121,6 +127,7 @@
       case TargetPlatform.windows_x64:
       case TargetPlatform.fuchsia:
       case TargetPlatform.tester:
+      case TargetPlatform.web:
         return _getHostArtifactPath(artifact, platform, mode);
     }
     assert(false, 'Invalid platform $platform.');
@@ -183,13 +190,17 @@
       case Artifact.engineDartSdkPath:
         return dartSdkPath;
       case Artifact.engineDartBinary:
-        return fs.path.join(dartSdkPath,'bin', _artifactToFileName(artifact));
+        return fs.path.join(dartSdkPath, 'bin', _artifactToFileName(artifact));
       case Artifact.platformKernelDill:
         return fs.path.join(_getFlutterPatchedSdkPath(), _artifactToFileName(artifact));
       case Artifact.platformLibrariesJson:
         return fs.path.join(_getFlutterPatchedSdkPath(), 'lib', _artifactToFileName(artifact));
       case Artifact.flutterPatchedSdkPath:
         return _getFlutterPatchedSdkPath();
+      case Artifact.dart2jsSnapshot:
+        return fs.path.join(dartSdkPath, 'bin', 'snapshots', _artifactToFileName(artifact));
+      case Artifact.kernelWorkerSnapshot:
+        return fs.path.join(dartSdkPath, 'bin', 'snapshots', _artifactToFileName(artifact));
       default:
         assert(false, 'Artifact $artifact not available for platform $platform.');
         return null;
@@ -205,6 +216,7 @@
       case TargetPlatform.windows_x64:
       case TargetPlatform.fuchsia:
       case TargetPlatform.tester:
+      case TargetPlatform.web:
         assert(mode == null, 'Platform $platform does not support different build modes.');
         return fs.path.join(engineDir, platformName);
       case TargetPlatform.ios:
@@ -265,6 +277,10 @@
         return fs.path.join(_hostEngineOutPath, 'dart-sdk');
       case Artifact.engineDartBinary:
         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.kernelWorkerSnapshot:
+        return fs.path.join(_hostEngineOutPath, 'dart-sdk', 'bin', 'snapshots', _artifactToFileName(artifact));
     }
     assert(false, 'Invalid artifact $artifact.');
     return null;
diff --git a/packages/flutter_tools/lib/src/build_info.dart b/packages/flutter_tools/lib/src/build_info.dart
index cd8ce05..2e3322b 100644
--- a/packages/flutter_tools/lib/src/build_info.dart
+++ b/packages/flutter_tools/lib/src/build_info.dart
@@ -266,6 +266,7 @@
   windows_x64,
   fuchsia,
   tester,
+  web,
 }
 
 /// iOS target device architecture.
@@ -325,6 +326,8 @@
       return 'fuchsia';
     case TargetPlatform.tester:
       return 'flutter-tester';
+    case TargetPlatform.web:
+      return 'web';
   }
   assert(false);
   return null;
@@ -346,6 +349,8 @@
       return TargetPlatform.darwin_x64;
     case 'linux-x64':
       return TargetPlatform.linux_x64;
+    case 'web':
+      return TargetPlatform.web;
   }
   assert(platform != null);
   return null;
@@ -400,6 +405,11 @@
   return fs.path.join(getBuildDirectory(), 'ios');
 }
 
+/// Returns the web build output directory.
+String getWebBuildDirectory() {
+  return fs.path.join(getBuildDirectory(), 'web');
+}
+
 /// Returns directory used by incremental compiler (IKG - incremental kernel
 /// generator) to store cached intermediate state.
 String getIncrementalCompilerByteStoreDirectory() {
diff --git a/packages/flutter_tools/lib/src/commands/build.dart b/packages/flutter_tools/lib/src/commands/build.dart
index c31f741..64100e2 100644
--- a/packages/flutter_tools/lib/src/commands/build.dart
+++ b/packages/flutter_tools/lib/src/commands/build.dart
@@ -11,6 +11,7 @@
 import 'build_bundle.dart';
 import 'build_flx.dart';
 import 'build_ios.dart';
+import 'build_web.dart';
 
 class BuildCommand extends FlutterCommand {
   BuildCommand({bool verboseHelp = false}) {
@@ -20,6 +21,7 @@
     addSubcommand(BuildIOSCommand());
     addSubcommand(BuildFlxCommand());
     addSubcommand(BuildBundleCommand(verboseHelp: verboseHelp));
+    addSubcommand(BuildWebCommand());
   }
 
   @override
diff --git a/packages/flutter_tools/lib/src/commands/build_web.dart b/packages/flutter_tools/lib/src/commands/build_web.dart
new file mode 100644
index 0000000..a1a4645
--- /dev/null
+++ b/packages/flutter_tools/lib/src/commands/build_web.dart
@@ -0,0 +1,38 @@
+// 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 '../base/logger.dart';
+import '../build_info.dart';
+import '../globals.dart';
+import '../runner/flutter_command.dart' show ExitStatus, FlutterCommandResult;
+import '../web/compile.dart';
+import 'build.dart';
+
+class BuildWebCommand extends BuildSubCommand {
+  BuildWebCommand() {
+    usesTargetOption();
+    usesPubOption();
+    defaultBuildMode = BuildMode.release;
+  }
+
+  @override
+  final String name = 'web';
+
+  @override
+  bool get hidden => true;
+
+  @override
+  final String description = '(EXPERIMENTAL) build a web application bundle.';
+
+  @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);
+    status.stop();
+    return FlutterCommandResult(result == 0 ? ExitStatus.success : ExitStatus.fail);
+  }
+}
diff --git a/packages/flutter_tools/lib/src/context_runner.dart b/packages/flutter_tools/lib/src/context_runner.dart
index ba6a2b1..7a4490b 100644
--- a/packages/flutter_tools/lib/src/context_runner.dart
+++ b/packages/flutter_tools/lib/src/context_runner.dart
@@ -39,6 +39,7 @@
 import 'run_hot.dart';
 import 'usage.dart';
 import 'version.dart';
+import 'web/compile.dart';
 import 'windows/windows_workflow.dart';
 
 Future<T> runInContext<T>(
@@ -91,6 +92,7 @@
       Usage: () => Usage(),
       UserMessages: () => UserMessages(),
       WindowsWorkflow: () => const WindowsWorkflow(),
+      WebCompiler: () => const WebCompiler(),
       Xcode: () => Xcode(),
       XcodeProjectInterpreter: () => XcodeProjectInterpreter(),
     },
diff --git a/packages/flutter_tools/lib/src/web/compile.dart b/packages/flutter_tools/lib/src/web/compile.dart
new file mode 100644
index 0000000..4dab748
--- /dev/null
+++ b/packages/flutter_tools/lib/src/web/compile.dart
@@ -0,0 +1,70 @@
+// 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:meta/meta.dart';
+
+import '../artifacts.dart';
+import '../base/common.dart';
+import '../base/context.dart';
+import '../base/file_system.dart';
+import '../base/io.dart';
+import '../base/process_manager.dart';
+import '../build_info.dart';
+import '../convert.dart';
+import '../globals.dart';
+
+/// The [WebCompiler] instance.
+WebCompiler get webCompiler => context[WebCompiler];
+
+/// A wrapper around dart2js for web compilation.
+class WebCompiler {
+  const WebCompiler();
+
+  /// 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> 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 flutterPatchedSdkPath = artifacts.getArtifactPath(Artifact.flutterPatchedSdkPath);
+    final String librariesPath = fs.path.join(flutterPatchedSdkPath, 'libraries.json');
+    final Directory outputDir = fs.directory(getWebBuildDirectory());
+    if (!outputDir.existsSync()) {
+      outputDir.createSync(recursive: true);
+    }
+    final String outputPath = fs.path.join(outputDir.path, 'main.dart.js');
+    if (!processManager.canRun(engineDartPath)) {
+      throwToolExit('Unable to find Dart binary at $engineDartPath');
+    }
+    final List<String> command = <String>[
+      engineDartPath,
+      dart2jsPath,
+      target,
+      '-o',
+      '$outputPath',
+      '--libraries-spec=$librariesPath',
+      '--platform-binaries=$flutterPatchedSdkPath',
+    ];
+    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;
+  }
+}
diff --git a/packages/flutter_tools/test/web/compile_test.dart b/packages/flutter_tools/test/web/compile_test.dart
new file mode 100644
index 0000000..0316a33
--- /dev/null
+++ b/packages/flutter_tools/test/web/compile_test.dart
@@ -0,0 +1,54 @@
+// 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/base/file_system.dart';
+import 'package:flutter_tools/src/base/io.dart';
+import 'package:flutter_tools/src/base/logger.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/context.dart';
+
+void main() {
+  final MockProcessManager mockProcessManager = MockProcessManager();
+  final MockProcess mockProcess = MockProcess();
+  final BufferLogger mockLogger = BufferLogger();
+
+  testUsingContext('invokes dart2js with correct arguments', () async {
+    const WebCompiler webCompiler = WebCompiler();
+    final String engineDartPath = artifacts.getArtifactPath(Artifact.engineDartBinary);
+    final String dart2jsPath = artifacts.getArtifactPath(Artifact.dart2jsSnapshot);
+    final String flutterPatchedSdkPath = artifacts.getArtifactPath(Artifact.flutterPatchedSdkPath);
+    final String librariesPath = fs.path.join(flutterPatchedSdkPath, 'libraries.json');
+
+    when(mockProcess.stdout).thenAnswer((Invocation invocation) => const Stream<List<int>>.empty());
+    when(mockProcess.stderr).thenAnswer((Invocation invocation) => const Stream<List<int>>.empty());
+    when(mockProcess.exitCode).thenAnswer((Invocation invocation) async => 0);
+    when(mockProcessManager.start(any)).thenAnswer((Invocation invocation) async => mockProcess);
+    when(mockProcessManager.canRun(engineDartPath)).thenReturn(true);
+
+    await webCompiler.compile(target: 'lib/main.dart');
+
+    final String outputPath = fs.path.join('build', 'web', 'main.dart.js');
+    verify(mockProcessManager.start(<String>[
+      engineDartPath,
+      dart2jsPath,
+      'lib/main.dart',
+      '-o',
+      outputPath,
+      '--libraries-spec=$librariesPath',
+      '--platform-binaries=$flutterPatchedSdkPath',
+      '-m',
+    ])).called(1);
+  }, overrides: <Type, Generator>{
+    ProcessManager: () => mockProcessManager,
+    Logger: () => mockLogger,
+  });
+}
+
+class MockProcessManager extends Mock implements ProcessManager {}
+class MockProcess extends Mock implements Process {}
\ No newline at end of file