From d6ca6b37d26b153662f62d681b2f538cfec263dd Mon Sep 17 00:00:00 2001 From: Jackson Gardner Date: Tue, 31 Oct 2023 12:19:17 -0700 Subject: [PATCH] Move flutter.js into the engine. (flutter/engine#47240) This will allow us to add tooling to do some bundling/minifying of `flutter.js`, which should make this more scalable/extensible long-term. Also, this removes a few redundant build rules that produce artifacts that the flutter tool doesn't use anymore. --- .../ci/licenses_golden/licenses_flutter | 2 + .../flutter/lib/web_ui/flutter_js/BUILD.gn | 8 + .../flutter/lib/web_ui/flutter_js/flutter.js | 380 ++++++++++++++++++ engine/src/flutter/web_sdk/BUILD.gn | 39 +- 4 files changed, 392 insertions(+), 37 deletions(-) create mode 100644 engine/src/flutter/lib/web_ui/flutter_js/BUILD.gn create mode 100644 engine/src/flutter/lib/web_ui/flutter_js/flutter.js diff --git a/engine/src/flutter/ci/licenses_golden/licenses_flutter b/engine/src/flutter/ci/licenses_golden/licenses_flutter index 0ec458c88d1..9a6d66506bf 100644 --- a/engine/src/flutter/ci/licenses_golden/licenses_flutter +++ b/engine/src/flutter/ci/licenses_golden/licenses_flutter @@ -3796,6 +3796,7 @@ ORIGIN: ../../../flutter/lib/ui/window/pointer_data_packet_converter.cc + ../../ ORIGIN: ../../../flutter/lib/ui/window/pointer_data_packet_converter.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/ui/window/viewport_metrics.cc + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/ui/window/viewport_metrics.h + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/lib/web_ui/flutter_js/flutter.js + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/annotations.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/canvas.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/channel_buffers.dart + ../../../flutter/LICENSE @@ -6584,6 +6585,7 @@ FILE: ../../../flutter/lib/ui/window/pointer_data_packet_converter.cc FILE: ../../../flutter/lib/ui/window/pointer_data_packet_converter.h FILE: ../../../flutter/lib/ui/window/viewport_metrics.cc FILE: ../../../flutter/lib/ui/window/viewport_metrics.h +FILE: ../../../flutter/lib/web_ui/flutter_js/flutter.js FILE: ../../../flutter/lib/web_ui/lib/annotations.dart FILE: ../../../flutter/lib/web_ui/lib/canvas.dart FILE: ../../../flutter/lib/web_ui/lib/channel_buffers.dart diff --git a/engine/src/flutter/lib/web_ui/flutter_js/BUILD.gn b/engine/src/flutter/lib/web_ui/flutter_js/BUILD.gn new file mode 100644 index 00000000000..626f5062b51 --- /dev/null +++ b/engine/src/flutter/lib/web_ui/flutter_js/BUILD.gn @@ -0,0 +1,8 @@ +# Copyright 2019 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. + +copy("flutter_js") { + sources = [ "flutter.js" ] + outputs = [ "$root_out_dir/flutter_web_sdk/{{source_file_part}}" ] +} diff --git a/engine/src/flutter/lib/web_ui/flutter_js/flutter.js b/engine/src/flutter/lib/web_ui/flutter_js/flutter.js new file mode 100644 index 00000000000..cda3ebcf8df --- /dev/null +++ b/engine/src/flutter/lib/web_ui/flutter_js/flutter.js @@ -0,0 +1,380 @@ +// Copyright 2013 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. + +if (!_flutter) { + var _flutter = {}; +} +_flutter.loader = null; + +(function () { + "use strict"; + + const baseUri = ensureTrailingSlash(getBaseURI()); + + function getBaseURI() { + const base = document.querySelector("base"); + return (base && base.getAttribute("href")) || ""; + } + + function ensureTrailingSlash(uri) { + if (uri == "") { + return uri; + } + return uri.endsWith("/") ? uri : `${uri}/`; + } + + /** + * Wraps `promise` in a timeout of the given `duration` in ms. + * + * Resolves/rejects with whatever the original `promises` does, or rejects + * if `promise` takes longer to complete than `duration`. In that case, + * `debugName` is used to compose a legible error message. + * + * If `duration` is < 0, the original `promise` is returned unchanged. + * @param {Promise} promise + * @param {number} duration + * @param {string} debugName + * @returns {Promise} a wrapped promise. + */ + async function timeout(promise, duration, debugName) { + if (duration < 0) { + return promise; + } + let timeoutId; + const _clock = new Promise((_, reject) => { + timeoutId = setTimeout(() => { + reject( + new Error( + `${debugName} took more than ${duration}ms to resolve. Moving on.`, + { + cause: timeout, + } + ) + ); + }, duration); + }); + + return Promise.race([promise, _clock]).finally(() => { + clearTimeout(timeoutId); + }); + } + + /** + * Handles the creation of a TrustedTypes `policy` that validates URLs based + * on an (optional) incoming array of RegExes. + */ + class FlutterTrustedTypesPolicy { + /** + * Constructs the policy. + * @param {[RegExp]} validPatterns the patterns to test URLs + * @param {String} policyName the policy name (optional) + */ + constructor(validPatterns, policyName = "flutter-js") { + const patterns = validPatterns || [ + /\.js$/, + ]; + if (window.trustedTypes) { + this.policy = trustedTypes.createPolicy(policyName, { + createScriptURL: function(url) { + const parsed = new URL(url, window.location); + const file = parsed.pathname.split("/").pop(); + const matches = patterns.some((pattern) => pattern.test(file)); + if (matches) { + return parsed.toString(); + } + console.error( + "URL rejected by TrustedTypes policy", + policyName, ":", url, "(download prevented)"); + } + }); + } + } + } + + /** + * Handles loading/reloading Flutter's service worker, if configured. + * + * @see: https://developers.google.com/web/fundamentals/primers/service-workers + */ + class FlutterServiceWorkerLoader { + /** + * Injects a TrustedTypesPolicy (or undefined if the feature is not supported). + * @param {TrustedTypesPolicy | undefined} policy + */ + setTrustedTypesPolicy(policy) { + this._ttPolicy = policy; + } + + /** + * Returns a Promise that resolves when the latest Flutter service worker, + * configured by `settings` has been loaded and activated. + * + * Otherwise, the promise is rejected with an error message. + * @param {*} settings Service worker settings + * @returns {Promise} that resolves when the latest serviceWorker is ready. + */ + loadServiceWorker(settings) { + if (settings == null) { + // In the future, settings = null -> uninstall service worker? + console.debug("Null serviceWorker configuration. Skipping."); + return Promise.resolve(); + } + if (!("serviceWorker" in navigator)) { + let errorMessage = "Service Worker API unavailable."; + if (!window.isSecureContext) { + errorMessage += "\nThe current context is NOT secure." + errorMessage += "\nRead more: https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts"; + } + return Promise.reject( + new Error(errorMessage) + ); + } + const { + serviceWorkerVersion, + serviceWorkerUrl = `${baseUri}flutter_service_worker.js?v=${serviceWorkerVersion}`, + timeoutMillis = 4000, + } = settings; + + // Apply the TrustedTypes policy, if present. + let url = serviceWorkerUrl; + if (this._ttPolicy != null) { + url = this._ttPolicy.createScriptURL(url); + } + + const serviceWorkerActivation = navigator.serviceWorker + .register(url) + .then((serviceWorkerRegistration) => this._getNewServiceWorker(serviceWorkerRegistration, serviceWorkerVersion)) + .then(this._waitForServiceWorkerActivation); + + // Timeout race promise + return timeout( + serviceWorkerActivation, + timeoutMillis, + "prepareServiceWorker" + ); + } + + /** + * Returns the latest service worker for the given `serviceWorkerRegistration`. + * + * This might return the current service worker, if there's no new service worker + * awaiting to be installed/updated. + * + * @param {ServiceWorkerRegistration} serviceWorkerRegistration + * @param {String} serviceWorkerVersion + * @returns {Promise} + */ + async _getNewServiceWorker(serviceWorkerRegistration, serviceWorkerVersion) { + if (!serviceWorkerRegistration.active && (serviceWorkerRegistration.installing || serviceWorkerRegistration.waiting)) { + // No active web worker and we have installed or are installing + // one for the first time. Simply wait for it to activate. + console.debug("Installing/Activating first service worker."); + return serviceWorkerRegistration.installing || serviceWorkerRegistration.waiting; + } else if (!serviceWorkerRegistration.active.scriptURL.endsWith(serviceWorkerVersion)) { + // When the app updates the serviceWorkerVersion changes, so we + // need to ask the service worker to update. + const newRegistration = await serviceWorkerRegistration.update(); + console.debug("Updating service worker."); + return newRegistration.installing || newRegistration.waiting || newRegistration.active; + } else { + console.debug("Loading from existing service worker."); + return serviceWorkerRegistration.active; + } + } + + /** + * Returns a Promise that resolves when the `serviceWorker` changes its + * state to "activated". + * + * @param {ServiceWorker} serviceWorker + * @returns {Promise} + */ + async _waitForServiceWorkerActivation(serviceWorker) { + if (!serviceWorker || serviceWorker.state == "activated") { + if (!serviceWorker) { + throw new Error("Cannot activate a null service worker!"); + } else { + console.debug("Service worker already active."); + return; + } + } + return new Promise((resolve, _) => { + serviceWorker.addEventListener("statechange", () => { + if (serviceWorker.state == "activated") { + console.debug("Activated new service worker."); + resolve(); + } + }); + }); + } + } + + /** + * Handles injecting the main Flutter web entrypoint (main.dart.js), and notifying + * the user when Flutter is ready, through `didCreateEngineInitializer`. + * + * @see https://docs.flutter.dev/development/platform-integration/web/initialization + */ + class FlutterEntrypointLoader { + /** + * Creates a FlutterEntrypointLoader. + */ + constructor() { + // Watchdog to prevent injecting the main entrypoint multiple times. + this._scriptLoaded = false; + } + + /** + * Injects a TrustedTypesPolicy (or undefined if the feature is not supported). + * @param {TrustedTypesPolicy | undefined} policy + */ + setTrustedTypesPolicy(policy) { + this._ttPolicy = policy; + } + + /** + * Loads flutter main entrypoint, specified by `entrypointUrl`, and calls a + * user-specified `onEntrypointLoaded` callback with an EngineInitializer + * object when it's done. + * + * @param {*} options + * @returns {Promise | undefined} that will eventually resolve with an + * EngineInitializer, or will be rejected with the error caused by the loader. + * Returns undefined when an `onEntrypointLoaded` callback is supplied in `options`. + */ + async loadEntrypoint(options) { + const { entrypointUrl = `${baseUri}main.dart.js`, onEntrypointLoaded, nonce } = + options || {}; + + return this._loadEntrypoint(entrypointUrl, onEntrypointLoaded, nonce); + } + + /** + * Resolves the promise created by loadEntrypoint, and calls the `onEntrypointLoaded` + * function supplied by the user (if needed). + * + * Called by Flutter through `_flutter.loader.didCreateEngineInitializer` method, + * which is bound to the correct instance of the FlutterEntrypointLoader by + * the FlutterLoader object. + * + * @param {Function} engineInitializer @see https://github.com/flutter/engine/blob/main/lib/web_ui/lib/src/engine/js_interop/js_loader.dart#L42 + */ + didCreateEngineInitializer(engineInitializer) { + if (typeof this._didCreateEngineInitializerResolve === "function") { + this._didCreateEngineInitializerResolve(engineInitializer); + // Remove the resolver after the first time, so Flutter Web can hot restart. + this._didCreateEngineInitializerResolve = null; + // Make the engine revert to "auto" initialization on hot restart. + delete _flutter.loader.didCreateEngineInitializer; + } + if (typeof this._onEntrypointLoaded === "function") { + this._onEntrypointLoaded(engineInitializer); + } + } + + /** + * Injects a script tag into the DOM, and configures this loader to be able to + * handle the "entrypoint loaded" notifications received from Flutter web. + * + * @param {string} entrypointUrl the URL of the script that will initialize + * Flutter. + * @param {Function} onEntrypointLoaded a callback that will be called when + * Flutter web notifies this object that the entrypoint is + * loaded. + * @returns {Promise | undefined} a Promise that resolves when the entrypoint + * is loaded, or undefined if `onEntrypointLoaded` + * is a function. + */ + _loadEntrypoint(entrypointUrl, onEntrypointLoaded, nonce) { + const useCallback = typeof onEntrypointLoaded === "function"; + + if (!this._scriptLoaded) { + this._scriptLoaded = true; + const scriptTag = this._createScriptTag(entrypointUrl, nonce); + if (useCallback) { + // Just inject the script tag, and return nothing; Flutter will call + // `didCreateEngineInitializer` when it's done. + console.debug("Injecting