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