[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');