From e03029ef6a0c2040476bf6602da4e5d1271eb77d Mon Sep 17 00:00:00 2001 From: Mouad Debbar Date: Tue, 7 Feb 2023 10:51:52 -0500 Subject: [PATCH] [web] Move JS content to its own `.js` files (#117691) --- packages/flutter_tools/lib/src/artifacts.dart | 39 ++ .../lib/src/build_system/targets/web.dart | 8 +- .../lib/src/isolated/devfs_web.dart | 5 +- .../src/web/file_generators/flutter_js.dart | 387 +----------------- .../flutter_service_worker_js.dart | 198 +-------- .../lib/src/web/file_generators/js/flutter.js | 375 +++++++++++++++++ .../js/flutter_service_worker.js | 172 ++++++++ .../build_system/targets/web_test.dart | 44 +- .../integration.shard/test_data/project.dart | 7 +- 9 files changed, 667 insertions(+), 568 deletions(-) create mode 100644 packages/flutter_tools/lib/src/web/file_generators/js/flutter.js create mode 100644 packages/flutter_tools/lib/src/web/file_generators/js/flutter_service_worker.js diff --git a/packages/flutter_tools/lib/src/artifacts.dart b/packages/flutter_tools/lib/src/artifacts.dart index fd56d3c2e65..4546a3dc76d 100644 --- a/packages/flutter_tools/lib/src/artifacts.dart +++ b/packages/flutter_tools/lib/src/artifacts.dart @@ -9,6 +9,7 @@ import 'base/common.dart'; import 'base/file_system.dart'; import 'base/os.dart'; import 'base/platform.dart'; +import 'base/user_messages.dart'; import 'base/utils.dart'; import 'build_info.dart'; import 'cache.dart'; @@ -62,6 +63,9 @@ enum Artifact { /// Tools related to subsetting or icon font files. fontSubset, constFinder, + + /// The location of file generators. + flutterToolsFileGenerators, } /// A subset of [Artifact]s that are platform and build mode independent @@ -202,6 +206,8 @@ String? _artifactToFileName(Artifact artifact, Platform hostPlatform, [ BuildMod return 'font-subset$exe'; case Artifact.constFinder: return 'const_finder.dart.snapshot'; + case Artifact.flutterToolsFileGenerators: + return ''; } } @@ -525,6 +531,8 @@ class CachedArtifacts implements Artifacts { case Artifact.windowsCppClientWrapper: case Artifact.windowsDesktopPath: return _getHostArtifactPath(artifact, platform, mode); + case Artifact.flutterToolsFileGenerators: + return _getFileGeneratorsPath(); } } @@ -562,6 +570,8 @@ class CachedArtifacts implements Artifacts { case Artifact.windowsCppClientWrapper: case Artifact.windowsDesktopPath: return _getHostArtifactPath(artifact, platform, mode); + case Artifact.flutterToolsFileGenerators: + return _getFileGeneratorsPath(); } } @@ -611,6 +621,8 @@ class CachedArtifacts implements Artifacts { case Artifact.windowsCppClientWrapper: case Artifact.windowsDesktopPath: return _getHostArtifactPath(artifact, platform, mode); + case Artifact.flutterToolsFileGenerators: + return _getFileGeneratorsPath(); } } @@ -685,6 +697,8 @@ class CachedArtifacts implements Artifacts { case Artifact.fuchsiaFlutterRunner: case Artifact.fuchsiaKernelCompiler: throw StateError('Artifact $artifact not available for platform $platform.'); + case Artifact.flutterToolsFileGenerators: + return _getFileGeneratorsPath(); } } @@ -952,6 +966,8 @@ class CachedLocalEngineArtifacts implements Artifacts { case Artifact.dart2wasmSnapshot: case Artifact.frontendServerSnapshotForEngineDartSdk: return _fileSystem.path.join(_getDartSdkPath(), 'bin', 'snapshots', artifactFileName); + case Artifact.flutterToolsFileGenerators: + return _getFileGeneratorsPath(); } } @@ -1099,6 +1115,7 @@ class CachedLocalWebSdkArtifacts implements Artifacts { case Artifact.fuchsiaFlutterRunner: case Artifact.fontSubset: case Artifact.constFinder: + case Artifact.flutterToolsFileGenerators: break; } } @@ -1298,6 +1315,11 @@ class _TestArtifacts implements Artifacts { BuildMode? mode, EnvironmentType? environmentType, }) { + // The path to file generators is the same even in the test environment. + if (artifact == Artifact.flutterToolsFileGenerators) { + return _getFileGeneratorsPath(); + } + final StringBuffer buffer = StringBuffer(); buffer.write(artifact); if (platform != null) { @@ -1340,3 +1362,20 @@ class _TestLocalEngine extends _TestArtifacts { @override final LocalEngineInfo localEngineInfo; } + +String _getFileGeneratorsPath() { + final String flutterRoot = Cache.defaultFlutterRoot( + fileSystem: globals.localFileSystem, + platform: const LocalPlatform(), + userMessages: UserMessages(), + ); + return globals.localFileSystem.path.join( + flutterRoot, + 'packages', + 'flutter_tools', + 'lib', + 'src', + 'web', + 'file_generators', + ); +} 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 3812729f54d..fde180c282f 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/web.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/web.dart @@ -529,7 +529,10 @@ class WebBuiltInAssets extends Target { // Write the flutter.js file final File flutterJsFile = environment.outputDir.childFile('flutter.js'); - flutterJsFile.writeAsStringSync(flutter_js.generateFlutterJsFile()); + final String fileGeneratorsPath = + globals.artifacts!.getArtifactPath(Artifact.flutterToolsFileGenerators); + flutterJsFile.writeAsStringSync( + flutter_js.generateFlutterJsFile(fileGeneratorsPath)); } } @@ -598,7 +601,10 @@ class WebServiceWorker extends Target { final ServiceWorkerStrategy serviceWorkerStrategy = _serviceWorkerStrategyFromString( environment.defines[kServiceWorkerStrategy], ); + final String fileGeneratorsPath = + globals.artifacts!.getArtifactPath(Artifact.flutterToolsFileGenerators); final String serviceWorker = generateServiceWorker( + fileGeneratorsPath, urlToHash, [ 'main.dart.js', diff --git a/packages/flutter_tools/lib/src/isolated/devfs_web.dart b/packages/flutter_tools/lib/src/isolated/devfs_web.dart index 3c86d02ff99..cdd04e37d6c 100644 --- a/packages/flutter_tools/lib/src/isolated/devfs_web.dart +++ b/packages/flutter_tools/lib/src/isolated/devfs_web.dart @@ -828,7 +828,10 @@ 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()); + final String fileGeneratorsPath = globals.artifacts! + .getArtifactPath(Artifact.flutterToolsFileGenerators); + webAssetServer.writeFile( + 'flutter.js', flutter_js.generateFlutterJsFile(fileGeneratorsPath)); webAssetServer.writeFile('flutter_service_worker.js', '// Service worker not loaded in run mode.'); webAssetServer.writeFile( diff --git a/packages/flutter_tools/lib/src/web/file_generators/flutter_js.dart b/packages/flutter_tools/lib/src/web/file_generators/flutter_js.dart index 81a611dd301..991719d369a 100644 --- a/packages/flutter_tools/lib/src/web/file_generators/flutter_js.dart +++ b/packages/flutter_tools/lib/src/web/file_generators/flutter_js.dart @@ -2,388 +2,17 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import '../../globals.dart' as globals; /// 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 r''' -// 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. - -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 || [ - /\.dart\.js$/, - /^flutter_service_worker.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 (!("serviceWorker" in navigator) || settings == null) { - // In the future, settings = null -> uninstall service worker? - return Promise.reject( - new Error("Service worker not supported (or configured).") - ); - } - 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(this._getNewServiceWorker) - .then(this._waitForServiceWorkerActivation); - - // Timeout race promise - return timeout( - serviceWorkerActivation, - timeoutMillis, - "prepareServiceWorker" - ); - } - - /** - * Returns the latest service worker for the given `serviceWorkerRegistrationPromise`. - * - * This might return the current service worker, if there's no new service worker - * awaiting to be installed/updated. - * - * @param {Promise} serviceWorkerRegistrationPromise - * @returns {Promise} - */ - async _getNewServiceWorker(serviceWorkerRegistrationPromise) { - const reg = await serviceWorkerRegistrationPromise; - - 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. - console.debug("Installing/Activating first service worker."); - return 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. - return reg.update().then((newReg) => { - console.debug("Updating service worker."); - return newReg.installing || newReg.waiting || newReg.active; - }); - } else { - console.debug("Loading from existing service worker."); - return reg.active; - } - } - - /** - * Returns a Promise that resolves when the `latestServiceWorker` changes its - * state to "activated". - * - * @param {Promise} latestServiceWorkerPromise - * @returns {Promise} - */ - async _waitForServiceWorkerActivation(latestServiceWorkerPromise) { - const serviceWorker = await latestServiceWorkerPromise; - - if (!serviceWorker || serviceWorker.state == "activated") { - if (!serviceWorker) { - return Promise.reject( - new Error("Cannot activate a null service worker!") - ); - } else { - console.debug("Service worker already active."); - return Promise.resolve(); - } - } - 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 } = - options || {}; - - return this._loadEntrypoint(entrypointUrl, onEntrypointLoaded); - } - - /** - * 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) { - const useCallback = typeof onEntrypointLoaded === "function"; - - if (!this._scriptLoaded) { - this._scriptLoaded = true; - const scriptTag = this._createScriptTag(entrypointUrl); - if (useCallback) { - // Just inject the script tag, and return nothing; Flutter will call - // `didCreateEngineInitializer` when it's done. - console.debug("Injecting