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);
}
}