Unverified Commit e52b777a authored by David Iglesias's avatar David Iglesias Committed by GitHub

[web] flutter.js initialization with ui.webOnlyWarmupEngine (#100177)

parent 9e77d3ef
......@@ -17,6 +17,7 @@ import '../../dart/language_version.dart';
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 @@ class WebEntrypointTarget extends Target {
@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 @@ class WebEntrypointTarget extends Target {
// 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;
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');
Future<void> main() async {
await ui.webOnlyInitializePlatform();
entrypoint.main();
}
''';
}
environment.buildDir.childFile('main.dart')
.writeAsStringSync(contents);
}
......@@ -449,6 +456,10 @@ class WebBuiltInAssets extends Target {
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());
}
}
......
......@@ -41,6 +41,7 @@ import '../vmservice.dart';
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 @@ class WebDevFS implements DevFS {
'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(
......
......@@ -473,13 +473,19 @@ class ResidentWebRunner extends ResidentRunner {
'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');
......
// 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();
}());
''';
}
......@@ -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>
......@@ -13,6 +13,7 @@ import 'package:flutter_tools/src/build_system/build_system.dart';
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 @@ void main() {
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 @@ void main() {
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 @@ void main() {
// 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 @@ void main() {
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 @@ void main() {
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()));
}));
}
......@@ -569,7 +569,7 @@ void main() {
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);
......
......@@ -685,13 +685,14 @@ void main() {
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 @@ void main() {
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');
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment