[web] flutter.js initialization with ui.webOnlyWarmupEngine (#100177)
diff --git a/packages/flutter_tools/lib/src/build_system/targets/web.dart b/packages/flutter_tools/lib/src/build_system/targets/web.dart
index e26b0b4..c38d357 100644
--- a/packages/flutter_tools/lib/src/build_system/targets/web.dart
+++ b/packages/flutter_tools/lib/src/build_system/targets/web.dart
@@ -17,6 +17,7 @@
import '../../dart/package_map.dart';
import '../../globals.dart' as globals;
import '../../project.dart';
+import '../../web/flutter_js.dart' as flutter_js;
import '../build_system.dart';
import '../depfile.dart';
import '../exceptions.dart';
@@ -96,7 +97,7 @@
@override
Future<void> build(Environment environment) async {
final String? targetFile = environment.defines[kTargetFile];
- final bool hasPlugins = environment.defines[kHasWebPlugins] == 'true';
+ final bool hasWebPlugins = environment.defines[kHasWebPlugins] == 'true';
final Uri importUri = environment.fileSystem.file(targetFile).absolute.uri;
// TODO(zanderso): support configuration of this file.
const String packageFile = '.packages';
@@ -120,48 +121,54 @@
// By construction, this will only be null if the .packages file does not
// have an entry for the user's application or if the main file is
// outside of the lib/ directory.
- final String mainImport = packageConfig.toPackageUri(importUri)?.toString()
+ final String importedEntrypoint = packageConfig.toPackageUri(importUri)?.toString()
?? importUri.toString();
- String contents;
- if (hasPlugins) {
+ String? generatedImport;
+ if (hasWebPlugins) {
final Uri generatedUri = environment.projectDir
.childDirectory('lib')
.childFile('generated_plugin_registrant.dart')
.absolute
.uri;
- final String generatedImport = packageConfig.toPackageUri(generatedUri)?.toString()
+ generatedImport = packageConfig.toPackageUri(generatedUri)?.toString()
?? generatedUri.toString();
- contents = '''
-// @dart=${languageVersion.major}.${languageVersion.minor}
-
-import 'dart:ui' as ui;
-
-import 'package:flutter_web_plugins/flutter_web_plugins.dart';
-
-import '$generatedImport';
-import '$mainImport' as entrypoint;
-
-Future<void> main() async {
- registerPlugins(webPluginRegistrar);
- await ui.webOnlyInitializePlatform();
- entrypoint.main();
-}
-''';
- } else {
- contents = '''
-// @dart=${languageVersion.major}.${languageVersion.minor}
-
-import 'dart:ui' as ui;
-
-import '$mainImport' as entrypoint;
-
-Future<void> main() async {
- await ui.webOnlyInitializePlatform();
- entrypoint.main();
-}
-''';
}
+
+ final String contents = <String>[
+ '// @dart=${languageVersion.major}.${languageVersion.minor}',
+ '// Flutter web bootstrap script for $importedEntrypoint.',
+ '',
+ "import 'dart:ui' as ui;",
+ "import 'dart:async';",
+ '',
+ "import '$importedEntrypoint' as entrypoint;",
+ if (hasWebPlugins)
+ "import 'package:flutter_web_plugins/flutter_web_plugins.dart';",
+ if (hasWebPlugins)
+ "import '$generatedImport';",
+ '',
+ 'typedef _UnaryFunction = dynamic Function(List<String> args);',
+ 'typedef _NullaryFunction = dynamic Function();',
+ '',
+ 'Future<void> main() async {',
+ ' await ui.webOnlyWarmupEngine(',
+ ' runApp: () {',
+ ' if (entrypoint.main is _UnaryFunction) {',
+ ' return (entrypoint.main as _UnaryFunction)(<String>[]);',
+ ' }',
+ ' return (entrypoint.main as _NullaryFunction)();',
+ ' },',
+ if (hasWebPlugins) ...<String>[
+ ' registerPlugins: () {',
+ ' registerPlugins(webPluginRegistrar);',
+ ' },',
+ ],
+ ' );',
+ '}',
+ '',
+ ].join('\n');
+
environment.buildDir.childFile('main.dart')
.writeAsStringSync(contents);
}
@@ -449,6 +456,10 @@
final String targetPath = fileSystem.path.join(environment.outputDir.path, 'canvaskit', relativePath);
file.copySync(targetPath);
}
+
+ // Write the flutter.js file
+ final File flutterJsFile = environment.outputDir.childFile('flutter.js');
+ flutterJsFile.writeAsStringSync(flutter_js.generateFlutterJsFile());
}
}
diff --git a/packages/flutter_tools/lib/src/isolated/devfs_web.dart b/packages/flutter_tools/lib/src/isolated/devfs_web.dart
index e9fd1d0..b111111 100644
--- a/packages/flutter_tools/lib/src/isolated/devfs_web.dart
+++ b/packages/flutter_tools/lib/src/isolated/devfs_web.dart
@@ -41,6 +41,7 @@
import '../web/bootstrap.dart';
import '../web/chrome.dart';
import '../web/compile.dart';
+import '../web/flutter_js.dart' as flutter_js;
import '../web/memory_fs.dart';
typedef DwdsLauncher = Future<Dwds> Function({
@@ -809,6 +810,7 @@
'stack_trace_mapper.js', stackTraceMapper.readAsBytesSync());
webAssetServer.writeFile(
'manifest.json', '{"info":"manifest not generated in run mode."}');
+ webAssetServer.writeFile('flutter.js', flutter_js.generateFlutterJsFile());
webAssetServer.writeFile('flutter_service_worker.js',
'// Service worker not loaded in run mode.');
webAssetServer.writeFile(
diff --git a/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart b/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart
index 0ac5ce2..21c5bed 100644
--- a/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart
+++ b/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart
@@ -473,13 +473,19 @@
'typedef _UnaryFunction = dynamic Function(List<String> args);',
'typedef _NullaryFunction = dynamic Function();',
'Future<void> main() async {',
- if (hasWebPlugins)
- ' registerPlugins(webPluginRegistrar);',
- ' await ui.webOnlyInitializePlatform();',
- ' if (entrypoint.main is _UnaryFunction) {',
- ' return (entrypoint.main as _UnaryFunction)(<String>[]);',
- ' }',
- ' return (entrypoint.main as _NullaryFunction)();',
+ ' await ui.webOnlyWarmupEngine(',
+ ' runApp: () {',
+ ' if (entrypoint.main is _UnaryFunction) {',
+ ' return (entrypoint.main as _UnaryFunction)(<String>[]);',
+ ' }',
+ ' return (entrypoint.main as _NullaryFunction)();',
+ ' },',
+ if (hasWebPlugins) ...<String>[
+ ' registerPlugins: () {',
+ ' registerPlugins(webPluginRegistrar);',
+ ' },',
+ ],
+ ' );',
'}',
'',
].join('\n');
diff --git a/packages/flutter_tools/lib/src/web/flutter_js.dart b/packages/flutter_tools/lib/src/web/flutter_js.dart
new file mode 100644
index 0000000..dfff827
--- /dev/null
+++ b/packages/flutter_tools/lib/src/web/flutter_js.dart
@@ -0,0 +1,146 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+
+/// Generates the flutter.js file.
+///
+/// flutter.js should be completely static, so **do not use any parameter or
+/// environment variable to generate this file**.
+String generateFlutterJsFile() {
+ return '''
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * This script installs service_worker.js to provide PWA functionality to
+ * application. For more information, see:
+ * https://developers.google.com/web/fundamentals/primers/service-workers
+ */
+
+if (!_flutter) {
+ var _flutter = {};
+}
+_flutter.loader = null;
+
+(function() {
+ "use strict";
+ class FlutterLoader {
+ // TODO: Move the below methods to "#private" once supported by all the browsers
+ // we support. In the meantime, we use the "revealing module" pattern.
+
+ // Watchdog to prevent injecting the main entrypoint multiple times.
+ _scriptLoaded = false;
+
+ // Resolver for the pending promise returned by loadEntrypoint.
+ _didCreateEngineInitializerResolve = null;
+
+ /**
+ * Initializes the main.dart.js with/without serviceWorker.
+ * @param {*} options
+ * @returns a Promise that will eventually resolve with an EngineInitializer,
+ * or will be rejected with the error caused by the loader.
+ */
+ loadEntrypoint(options) {
+ const {
+ entrypointUrl = "main.dart.js",
+ serviceWorker,
+ } = (options || {});
+ return this._loadWithServiceWorker(entrypointUrl, serviceWorker);
+ }
+
+ /**
+ * Resolves the promise created by loadEntrypoint. Called by Flutter.
+ * Needs to be weirdly bound like it is, so "this" is preserved across
+ * the JS <-> Flutter jumps.
+ * @param {*} engineInitializer
+ */
+ didCreateEngineInitializer = (function(engineInitializer) {
+ if (typeof this._didCreateEngineInitializerResolve != "function") {
+ console.warn("Do not call didCreateEngineInitializer by hand. Start with loadEntrypoint instead.");
+ }
+ this._didCreateEngineInitializerResolve(engineInitializer);
+ }).bind(this);
+
+ _loadEntrypoint(entrypointUrl) {
+ if (this._scriptLoaded) {
+ return null;
+ }
+
+ this._scriptLoaded = true;
+
+ return new Promise((resolve, reject) => {
+ let scriptTag = document.createElement("script");
+ scriptTag.src = entrypointUrl;
+ scriptTag.type = "application/javascript";
+ this._didCreateEngineInitializerResolve = resolve; // Cache the resolve, so it can be called from Flutter.
+ scriptTag.addEventListener("error", reject);
+ document.body.append(scriptTag);
+ });
+ }
+
+ _waitForServiceWorkerActivation(serviceWorker, entrypointUrl) {
+ if (!serviceWorker) return;
+ return new Promise((resolve, _) => {
+ serviceWorker.addEventListener("statechange", () => {
+ if (serviceWorker.state == "activated") {
+ console.log("Installed new service worker.");
+ resolve(this._loadEntrypoint(entrypointUrl));
+ }
+ });
+ });
+ }
+
+ _loadWithServiceWorker(entrypointUrl, serviceWorkerOptions) {
+ if (!("serviceWorker" in navigator) || serviceWorkerOptions == null) {
+ console.warn("Service worker not supported (or configured). Falling back to plain <script> tag.", serviceWorkerOptions);
+ return this._loadEntrypoint(entrypointUrl);
+ }
+
+ const {
+ serviceWorkerVersion,
+ timeoutMillis = 4000,
+ } = serviceWorkerOptions;
+
+ var serviceWorkerUrl = "flutter_service_worker.js?v=" + serviceWorkerVersion;
+ let loader = navigator.serviceWorker.register(serviceWorkerUrl)
+ .then((reg) => {
+ if (!reg.active && (reg.installing || reg.waiting)) {
+ // No active web worker and we have installed or are installing
+ // one for the first time. Simply wait for it to activate.
+ return this._waitForServiceWorkerActivation(reg.installing || reg.waiting, entrypointUrl);
+ } else if (!reg.active.scriptURL.endsWith(serviceWorkerVersion)) {
+ // When the app updates the serviceWorkerVersion changes, so we
+ // need to ask the service worker to update.
+ console.log("New service worker available.");
+ reg.update();
+ return this._waitForServiceWorkerActivation(reg.installing, entrypointUrl);
+ } else {
+ // Existing service worker is still good.
+ console.log("Loading app from service worker.");
+ return this._loadEntrypoint(entrypointUrl);
+ }
+ });
+
+ // Timeout race promise
+ let timeout;
+ if (timeoutMillis > 0) {
+ timeout = new Promise((resolve, _) => {
+ setTimeout(() => {
+ if (!this._scriptLoaded) {
+ console.warn("Failed to load app from service worker. Falling back to plain <script> tag.");
+ resolve(this._loadEntrypoint(entrypointUrl));
+ }
+ }, timeoutMillis);
+ });
+ }
+
+ return Promise.race([loader, timeout]);
+ }
+ }
+
+ _flutter.loader = new FlutterLoader();
+}());
+''';
+}
diff --git a/packages/flutter_tools/templates/app_shared/web/index.html.tmpl b/packages/flutter_tools/templates/app_shared/web/index.html.tmpl
index 725f53d..2ac781f 100644
--- a/packages/flutter_tools/templates/app_shared/web/index.html.tmpl
+++ b/packages/flutter_tools/templates/app_shared/web/index.html.tmpl
@@ -31,74 +31,28 @@
<title>{{projectName}}</title>
<link rel="manifest" href="manifest.json">
+
+ <script>
+ // The value below is injected by flutter build, do not touch.
+ var serviceWorkerVersion = null;
+ </script>
+ <!-- This script adds the flutter initialization JS code -->
+ <script src="flutter.js" defer></script>
</head>
<body>
- <!-- This script installs service_worker.js to provide PWA functionality to
- application. For more information, see:
- https://developers.google.com/web/fundamentals/primers/service-workers -->
<script>
- var serviceWorkerVersion = null;
- var scriptLoaded = false;
- function loadMainDartJs() {
- if (scriptLoaded) {
- return;
- }
- scriptLoaded = true;
- var scriptTag = document.createElement('script');
- scriptTag.src = 'main.dart.js';
- scriptTag.type = 'application/javascript';
- document.body.append(scriptTag);
- }
-
- if ('serviceWorker' in navigator) {
- // Service workers are supported. Use them.
- window.addEventListener('load', function () {
- // Wait for registration to finish before dropping the <script> tag.
- // Otherwise, the browser will load the script multiple times,
- // potentially different versions.
- var serviceWorkerUrl = 'flutter_service_worker.js?v=' + serviceWorkerVersion;
- navigator.serviceWorker.register(serviceWorkerUrl)
- .then((reg) => {
- function waitForActivation(serviceWorker) {
- serviceWorker.addEventListener('statechange', () => {
- if (serviceWorker.state == 'activated') {
- console.log('Installed new service worker.');
- loadMainDartJs();
- }
- });
- }
- if (!reg.active && (reg.installing || reg.waiting)) {
- // No active web worker and we have installed or are installing
- // one for the first time. Simply wait for it to activate.
- waitForActivation(reg.installing || reg.waiting);
- } else if (!reg.active.scriptURL.endsWith(serviceWorkerVersion)) {
- // When the app updates the serviceWorkerVersion changes, so we
- // need to ask the service worker to update.
- console.log('New service worker available.');
- reg.update();
- waitForActivation(reg.installing);
- } else {
- // Existing service worker is still good.
- console.log('Loading app from service worker.');
- loadMainDartJs();
- }
- });
-
- // If service worker doesn't succeed in a reasonable amount of time,
- // fallback to plain <script> tag.
- setTimeout(() => {
- if (!scriptLoaded) {
- console.warn(
- 'Failed to load app from service worker. Falling back to plain <script> tag.',
- );
- loadMainDartJs();
- }
- }, 4000);
+ window.addEventListener('load', function(ev) {
+ // Download main.dart.js
+ _flutter.loader.loadEntrypoint({
+ serviceWorker: {
+ serviceWorkerVersion: serviceWorkerVersion,
+ }
+ }).then(function(engineInitializer) {
+ return engineInitializer.initializeEngine();
+ }).then(function(appRunner) {
+ return appRunner.runApp();
});
- } else {
- // Service workers not supported. Just drop the <script> tag.
- loadMainDartJs();
- }
+ });
</script>
</body>
</html>
diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/web_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/web_test.dart
index 0be396a..422189c 100644
--- a/packages/flutter_tools/test/general.shard/build_system/targets/web_test.dart
+++ b/packages/flutter_tools/test/general.shard/build_system/targets/web_test.dart
@@ -13,6 +13,7 @@
import 'package:flutter_tools/src/build_system/depfile.dart';
import 'package:flutter_tools/src/build_system/targets/web.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
+import 'package:flutter_tools/src/web/flutter_js.dart' as flutter_js;
import '../../../src/common.dart';
import '../../../src/fake_process_manager.dart';
@@ -82,11 +83,12 @@
expect(generated, contains("import 'package:foo/generated_plugin_registrant.dart';"));
expect(generated, contains('registerPlugins(webPluginRegistrar);'));
- // Main
- expect(generated, contains('entrypoint.main();'));
-
// Import.
expect(generated, contains("import 'package:foo/main.dart' as entrypoint;"));
+
+ // Main
+ expect(generated, contains('ui.webOnlyWarmupEngine('));
+ expect(generated, contains('entrypoint.main as _'));
}));
test('version.json is created after release build', () => testbed.run(() async {
@@ -211,11 +213,12 @@
expect(generated, contains("import 'package:foo/generated_plugin_registrant.dart';"));
expect(generated, contains('registerPlugins(webPluginRegistrar);'));
- // Main
- expect(generated, contains('entrypoint.main();'));
-
// Import.
expect(generated, contains("import 'package:foo/main.dart' as entrypoint;"));
+
+ // Main
+ expect(generated, contains('ui.webOnlyWarmupEngine('));
+ expect(generated, contains('entrypoint.main as _'));
}, overrides: <Type, Generator>{
Platform: () => windows,
}));
@@ -233,8 +236,14 @@
// Plugins
expect(generated, isNot(contains("import 'package:foo/generated_plugin_registrant.dart';")));
expect(generated, isNot(contains('registerPlugins(webPluginRegistrar);')));
+
+ // Import.
+ expect(generated, contains("import 'package:foo/main.dart' as entrypoint;"));
+
// Main
- expect(generated, contains('entrypoint.main();'));
+ expect(generated, contains('ui.webOnlyWarmupEngine('));
+ expect(generated, contains('entrypoint.main as _'));
+
}));
test('WebEntrypointTarget generates an entrypoint with a language version', () => testbed.run(() async {
@@ -279,8 +288,12 @@
expect(generated, isNot(contains("import 'package:foo/generated_plugin_registrant.dart';")));
expect(generated, isNot(contains('registerPlugins(webPluginRegistrar);')));
+ // Import.
+ expect(generated, contains("import 'package:foo/main.dart' as entrypoint;"));
+
// Main
- expect(generated, contains('entrypoint.main();'));
+ expect(generated, contains('ui.webOnlyWarmupEngine('));
+ expect(generated, contains('entrypoint.main as _'));
}));
test('Dart2JSTarget calls dart2js with expected args with csp', () => testbed.run(() async {
@@ -683,4 +696,16 @@
expect(environment.outputDir.childFile('flutter_service_worker.js').readAsStringSync(),
contains('"main.dart.js"'));
}));
+
+ test('flutter.js is not dynamically generated', () => testbed.run(() async {
+ globals.fs.file('bin/cache/flutter_web_sdk/canvaskit/foo')
+ ..createSync(recursive: true)
+ ..writeAsStringSync('OL');
+
+ await WebBuiltInAssets(globals.fs, globals.cache).build(environment);
+
+ // No caching of source maps.
+ expect(environment.outputDir.childFile('flutter.js').readAsStringSync(),
+ equals(flutter_js.generateFlutterJsFile()));
+ }));
}
diff --git a/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart b/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart
index 3065118..70738a8 100644
--- a/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart
+++ b/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart
@@ -569,7 +569,7 @@
final String entrypointContents = fileSystem.file(webDevFS.mainUri).readAsStringSync();
expect(entrypointContents, contains('// Flutter web bootstrap script'));
expect(entrypointContents, contains("import 'dart:ui' as ui;"));
- expect(entrypointContents, contains('await ui.webOnlyInitializePlatform();'));
+ expect(entrypointContents, contains('await ui.webOnlyWarmupEngine('));
expect(logger.statusText, contains('Restarted application in'));
expect(result.code, 0);
diff --git a/packages/flutter_tools/test/general.shard/web/devfs_web_test.dart b/packages/flutter_tools/test/general.shard/web/devfs_web_test.dart
index b9b4e19..9092588 100644
--- a/packages/flutter_tools/test/general.shard/web/devfs_web_test.dart
+++ b/packages/flutter_tools/test/general.shard/web/devfs_web_test.dart
@@ -685,13 +685,14 @@
expect(webDevFS.webAssetServer.getFile('stack_trace_mapper.js'), isNotNull);
expect(webDevFS.webAssetServer.getFile('main.dart'), isNotNull);
expect(webDevFS.webAssetServer.getFile('manifest.json'), isNotNull);
+ expect(webDevFS.webAssetServer.getFile('flutter.js'), isNotNull);
expect(webDevFS.webAssetServer.getFile('flutter_service_worker.js'), isNotNull);
expect(webDevFS.webAssetServer.getFile('version.json'),isNotNull);
expect(await webDevFS.webAssetServer.dartSourceContents('dart_sdk.js'), 'HELLO');
expect(await webDevFS.webAssetServer.dartSourceContents('dart_sdk.js.map'), 'THERE');
// Update to the SDK.
- globals.fs.file(webPrecompiledSdk).writeAsStringSync('BELLOW');
+ globals.fs.file(webPrecompiledSdk).writeAsStringSync('BELLOW');
// New SDK should be visible..
expect(await webDevFS.webAssetServer.dartSourceContents('dart_sdk.js'), 'BELLOW');
@@ -795,6 +796,7 @@
expect(webDevFS.webAssetServer.getFile('stack_trace_mapper.js'), isNotNull);
expect(webDevFS.webAssetServer.getFile('main.dart'), isNotNull);
expect(webDevFS.webAssetServer.getFile('manifest.json'), isNotNull);
+ expect(webDevFS.webAssetServer.getFile('flutter.js'), isNotNull);
expect(webDevFS.webAssetServer.getFile('flutter_service_worker.js'), isNotNull);
expect(webDevFS.webAssetServer.getFile('version.json'), isNotNull);
expect(await webDevFS.webAssetServer.dartSourceContents('dart_sdk.js'), 'HELLO');