| // 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. |
| |
| import 'package:package_config/package_config.dart'; |
| |
| /// Used to load prerequisite scripts such as ddc_module_loader.js |
| const _simpleLoaderScript = r''' |
| window.$dartCreateScript = (function() { |
| // Find the nonce value. (Note, this is only computed once.) |
| const scripts = Array.from(document.getElementsByTagName("script")); |
| let nonce; |
| scripts.some( |
| script => (nonce = script.nonce || script.getAttribute("nonce"))); |
| // If present, return a closure that automatically appends the nonce. |
| if (nonce) { |
| return function() { |
| const script = document.createElement("script"); |
| script.nonce = nonce; |
| return script; |
| }; |
| } else { |
| return function() { |
| return document.createElement("script"); |
| }; |
| } |
| })(); |
| |
| // Loads a module [relativeUrl] relative to [root]. |
| // |
| // If not specified, [root] defaults to the directory serving the main app. |
| const forceLoadModule = function (relativeUrl, root) { |
| const actualRoot = root ?? _currentDirectory; |
| return new Promise(function(resolve, reject) { |
| const script = self.$dartCreateScript(); |
| let policy = { |
| createScriptURL: function(src) {return src;} |
| }; |
| if (self.trustedTypes && self.trustedTypes.createPolicy) { |
| policy = self.trustedTypes.createPolicy('dartDdcModuleUrl', policy); |
| } |
| script.onload = resolve; |
| script.onerror = reject; |
| script.src = policy.createScriptURL(actualRoot + relativeUrl); |
| document.head.appendChild(script); |
| }); |
| }; |
| '''; |
| |
| // TODO(srujzs): Delete this once it's no longer used internally. |
| String generateDDCBootstrapScript({ |
| required String entrypoint, |
| required String ddcModuleLoaderUrl, |
| required String mapperUrl, |
| required bool generateLoadingIndicator, |
| String appRootDirectory = '/', |
| }) { |
| return ''' |
| ${generateLoadingIndicator ? _generateLoadingIndicator() : ""} |
| // TODO(markzipan): This is safe if Flutter app roots are always equal to the |
| // host root '/'. Validate if this is true. |
| var _currentDirectory = "$appRootDirectory"; |
| |
| $_simpleLoaderScript |
| |
| // A map containing the URLs for the bootstrap scripts in debug. |
| let _scriptUrls = { |
| "mapper": "$mapperUrl", |
| "moduleLoader": "$ddcModuleLoaderUrl" |
| }; |
| |
| (function() { |
| let appName = "$entrypoint"; |
| |
| // A uuid that identifies a subapp. |
| // Stubbed out since subapps aren't supported in Flutter. |
| let uuid = "00000000-0000-0000-0000-000000000000"; |
| |
| window.postMessage( |
| {type: "DDC_STATE_CHANGE", state: "initial_load", targetUuid: uuid}, "*"); |
| |
| // Load pre-requisite DDC scripts. |
| // We intentionally use invalid names to avoid namespace clashes. |
| let prerequisiteScripts = [ |
| { |
| "src": "$ddcModuleLoaderUrl", |
| "id": "ddc_module_loader \x00" |
| }, |
| { |
| "src": "$mapperUrl", |
| "id": "dart_stack_trace_mapper \x00" |
| } |
| ]; |
| |
| // Load ddc_module_loader.js to access DDC's module loader API. |
| let prerequisiteLoads = []; |
| for (let i = 0; i < prerequisiteScripts.length; i++) { |
| prerequisiteLoads.push(forceLoadModule(prerequisiteScripts[i].src)); |
| } |
| Promise.all(prerequisiteLoads).then((_) => afterPrerequisiteLogic()); |
| |
| // Save the current script so we can access it in a closure. |
| var _currentScript = document.currentScript; |
| |
| var afterPrerequisiteLogic = function() { |
| window.\$dartLoader.rootDirectories.push(_currentDirectory); |
| let scripts = [ |
| { |
| "src": "dart_sdk.js", |
| "id": "dart_sdk" |
| }, |
| { |
| "src": "main_module.bootstrap.js", |
| "id": "data-main" |
| } |
| ]; |
| let loadConfig = new window.\$dartLoader.LoadConfiguration(); |
| loadConfig.bootstrapScript = scripts[scripts.length - 1]; |
| |
| loadConfig.loadScriptFn = function(loader) { |
| loader.addScriptsToQueue(scripts, null); |
| loader.loadEnqueuedModules(); |
| } |
| loadConfig.ddcEventForLoadStart = /* LOAD_ALL_MODULES_START */ 1; |
| loadConfig.ddcEventForLoadedOk = /* LOAD_ALL_MODULES_END_OK */ 2; |
| loadConfig.ddcEventForLoadedError = /* LOAD_ALL_MODULES_END_ERROR */ 3; |
| |
| let loader = new window.\$dartLoader.DDCLoader(loadConfig); |
| |
| // Record prerequisite scripts' fully resolved URLs. |
| prerequisiteScripts.forEach(script => loader.registerScript(script)); |
| |
| // Note: these variables should only be used in non-multi-app scenarios since |
| // they can be arbitrarily overridden based on multi-app load order. |
| window.\$dartLoader.loadConfig = loadConfig; |
| window.\$dartLoader.loader = loader; |
| loader.nextAttempt(); |
| } |
| })(); |
| '''; |
| } |
| |
| String generateDDCLibraryBundleBootstrapScript({ |
| required String entrypoint, |
| required String ddcModuleLoaderUrl, |
| required String mapperUrl, |
| required bool generateLoadingIndicator, |
| required bool isWindows, |
| }) { |
| return ''' |
| ${generateLoadingIndicator ? _generateLoadingIndicator() : ""} |
| // Save the current directory so we can access it in a closure. |
| const _currentDirectory = (function () { |
| const _url = document.currentScript.src; |
| const lastSlash = _url.lastIndexOf('/'); |
| if (lastSlash == -1) return _url; |
| const currentDirectory = _url.substring(0, lastSlash + 1); |
| return currentDirectory; |
| })(); |
| |
| $_simpleLoaderScript |
| |
| (function() { |
| let appName = "org-dartlang-app:/$entrypoint"; |
| |
| // Load pre-requisite DDC scripts. We intentionally use invalid names to avoid |
| // namespace clashes. |
| let prerequisiteScripts = [ |
| { |
| "src": "$ddcModuleLoaderUrl", |
| "id": "ddc_module_loader \x00" |
| }, |
| { |
| "src": "$mapperUrl", |
| "id": "dart_stack_trace_mapper \x00" |
| } |
| ]; |
| |
| // Load ddc_module_loader.js to access DDC's module loader API. |
| let prerequisiteLoads = []; |
| for (let i = 0; i < prerequisiteScripts.length; i++) { |
| prerequisiteLoads.push(forceLoadModule(prerequisiteScripts[i].src)); |
| } |
| Promise.all(prerequisiteLoads).then((_) => afterPrerequisiteLogic()); |
| |
| // Save the current script so we can access it in a closure. |
| const _currentScript = document.currentScript; |
| |
| // Create a policy if needed to load the files during a hot restart. |
| let policy = { |
| createScriptURL: function(src) {return src;} |
| }; |
| if (self.trustedTypes && self.trustedTypes.createPolicy) { |
| policy = self.trustedTypes.createPolicy('dartDdcModuleUrl', policy); |
| } |
| |
| const afterPrerequisiteLogic = function() { |
| window.\$dartLoader.rootDirectories.push(_currentDirectory); |
| let scripts = [ |
| { |
| "src": "dart_sdk.js", |
| "id": "dart_sdk" |
| }, |
| { |
| "src": "main_module.bootstrap.js", |
| "id": "data-main" |
| } |
| ]; |
| |
| let loadConfig = new window.\$dartLoader.LoadConfiguration(); |
| // TODO(srujzs): Verify this is sufficient for Windows. |
| loadConfig.isWindows = $isWindows; |
| loadConfig.bootstrapScript = scripts[scripts.length - 1]; |
| |
| loadConfig.loadScriptFn = function(loader) { |
| loader.addScriptsToQueue(scripts, null); |
| loader.loadEnqueuedModules(); |
| } |
| loadConfig.ddcEventForLoadStart = /* LOAD_ALL_MODULES_START */ 1; |
| loadConfig.ddcEventForLoadedOk = /* LOAD_ALL_MODULES_END_OK */ 2; |
| loadConfig.ddcEventForLoadedError = /* LOAD_ALL_MODULES_END_ERROR */ 3; |
| |
| let loader = new window.\$dartLoader.DDCLoader(loadConfig); |
| |
| // Record prerequisite scripts' fully resolved URLs. |
| prerequisiteScripts.forEach(script => loader.registerScript(script)); |
| |
| // Note: these variables should only be used in non-multi-app scenarios |
| // since they can be arbitrarily overridden based on multi-app load order. |
| window.\$dartLoader.loadConfig = loadConfig; |
| window.\$dartLoader.loader = loader; |
| |
| // Begin loading libraries |
| loader.nextAttempt(); |
| |
| // Set up stack trace mapper. |
| if (window.\$dartStackTraceUtility && |
| !window.\$dartStackTraceUtility.ready) { |
| window.\$dartStackTraceUtility.ready = true; |
| window.\$dartStackTraceUtility.setSourceMapProvider(function(url) { |
| const baseUrl = window.location.protocol + '//' + window.location.host; |
| url = url.replace(baseUrl + '/', ''); |
| if (url == 'dart_sdk.js') { |
| return dartDevEmbedder.debugger.getSourceMap('dart_sdk'); |
| } |
| url = url.replace(".lib.js", ""); |
| return dartDevEmbedder.debugger.getSourceMap(url); |
| }); |
| } |
| |
| let currentUri = _currentScript.src; |
| // We should have written a file containing all the scripts that need to be |
| // reloaded into the page. This is then read when a hot restart is triggered |
| // in DDC via the `\$dartReloadModifiedModules` callback. |
| // TODO(srujzs): We should avoid using a callback here in the bootstrap once |
| // the embedder supports passing a list of files/libraries to `hotRestart` |
| // instead. Currently, we're forced to read this file twice. |
| let reloadedSources = _currentDirectory + 'reloaded_sources.json'; |
| |
| if (!window.\$dartReloadModifiedModules) { |
| window.\$dartReloadModifiedModules = (function(appName, callback) { |
| const xhttp = new XMLHttpRequest(); |
| xhttp.withCredentials = true; |
| xhttp.onreadystatechange = function() { |
| // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/readyState |
| if (this.readyState == 4 && this.status == 200 || this.status == 304) { |
| const scripts = JSON.parse(this.responseText); |
| let numToLoad = 0; |
| let numLoaded = 0; |
| for (let i = 0; i < scripts.length; i++) { |
| const script = scripts[i]; |
| const module = script.module; |
| if (module == null) continue; |
| const src = script.src; |
| const oldSrc = window.\$dartLoader.moduleIdToUrl.get(module); |
| |
| // We might actually load from a different uri, delete the old one |
| // just to be sure. |
| window.\$dartLoader.urlToModuleId.delete(oldSrc); |
| |
| window.\$dartLoader.moduleIdToUrl.set(module, src); |
| window.\$dartLoader.urlToModuleId.set(src, module); |
| |
| numToLoad++; |
| |
| let el = document.getElementById(module); |
| if (el) el.remove(); |
| el = window.\$dartCreateScript(); |
| el.src = policy.createScriptURL(src); |
| el.async = false; |
| el.defer = true; |
| el.id = module; |
| el.onload = function() { |
| numLoaded++; |
| if (numToLoad == numLoaded) callback(); |
| }; |
| document.head.appendChild(el); |
| } |
| // Call `callback` right away if we found no updated scripts. |
| if (numToLoad == 0) callback(); |
| } |
| }; |
| xhttp.open("GET", reloadedSources, true); |
| xhttp.send(); |
| }); |
| } |
| }; |
| })(); |
| '''; |
| } |
| |
| /// The JavaScript bootstrap script to support in-browser hot restart. |
| /// |
| /// The [requireUrl] loads our cached RequireJS script file. The [mapperUrl] |
| /// loads the special Dart stack trace mapper. |
| /// |
| /// This file is served when the browser requests "main.dart.js" in debug mode, |
| /// and is responsible for bootstrapping the RequireJS modules and attaching |
| /// the hot reload hooks. |
| /// |
| /// If [generateLoadingIndicator] is `true`, embeds a loading indicator onto the |
| /// web page that's visible while the Flutter app is loading. |
| String generateBootstrapScript({ |
| required String requireUrl, |
| required String mapperUrl, |
| required bool generateLoadingIndicator, |
| }) { |
| return ''' |
| "use strict"; |
| |
| ${generateLoadingIndicator ? _generateLoadingIndicator() : ''} |
| |
| // A map containing the URLs for the bootstrap scripts in debug. |
| let _scriptUrls = { |
| "mapper": "$mapperUrl", |
| "requireJs": "$requireUrl" |
| }; |
| |
| // Create a TrustedTypes policy so we can attach Scripts... |
| let _ttPolicy; |
| if (window.trustedTypes) { |
| _ttPolicy = trustedTypes.createPolicy("flutter-tools-bootstrap", { |
| createScriptURL: (url) => { |
| let scriptUrl = _scriptUrls[url]; |
| if (!scriptUrl) { |
| console.error("Unknown Flutter Web bootstrap resource!", url); |
| } |
| return scriptUrl; |
| } |
| }); |
| } |
| |
| // Creates a TrustedScriptURL for a given `scriptName`. |
| // See `_scriptUrls` and `_ttPolicy` above. |
| function getTTScriptUrl(scriptName) { |
| let defaultUrl = _scriptUrls[scriptName]; |
| return _ttPolicy ? _ttPolicy.createScriptURL(scriptName) : defaultUrl; |
| } |
| |
| // Attach source mapping. |
| var mapperEl = document.createElement("script"); |
| mapperEl.defer = true; |
| mapperEl.async = false; |
| mapperEl.src = getTTScriptUrl("mapper"); |
| document.head.appendChild(mapperEl); |
| |
| // Attach require JS. |
| var requireEl = document.createElement("script"); |
| requireEl.defer = true; |
| requireEl.async = false; |
| requireEl.src = getTTScriptUrl("requireJs"); |
| // This attribute tells require JS what to load as main (defined below). |
| requireEl.setAttribute("data-main", "main_module.bootstrap"); |
| document.head.appendChild(requireEl); |
| '''; |
| } |
| |
| /// Creates a visual animated loading indicator and puts it on the page to |
| /// provide feedback to the developer that the app is being loaded. Otherwise, |
| /// the developer would be staring at a blank page wondering if the app will |
| /// come up or not. |
| /// |
| /// This indicator should only be used when DWDS is enabled, e.g. with the |
| /// `-d chrome` option. Debug builds without DWDS, e.g. `flutter run -d web-server` |
| /// or `flutter build web --debug` should not use this indicator. |
| String _generateLoadingIndicator() { |
| return ''' |
| const styles = ` |
| .flutter-loader { |
| width: 100%; |
| height: 8px; |
| background-color: #13B9FD; |
| position: absolute; |
| top: 0px; |
| left: 0px; |
| overflow: hidden; |
| } |
| |
| .indeterminate { |
| position: relative; |
| width: 100%; |
| height: 100%; |
| } |
| |
| .indeterminate:before { |
| content: ''; |
| position: absolute; |
| height: 100%; |
| background-color: #0175C2; |
| animation: indeterminate_first 2.0s infinite ease-out; |
| } |
| |
| .indeterminate:after { |
| content: ''; |
| position: absolute; |
| height: 100%; |
| background-color: #02569B; |
| animation: indeterminate_second 2.0s infinite ease-in; |
| } |
| |
| @keyframes indeterminate_first { |
| 0% { |
| left: -100%; |
| width: 100%; |
| } |
| 100% { |
| left: 100%; |
| width: 10%; |
| } |
| } |
| |
| @keyframes indeterminate_second { |
| 0% { |
| left: -150%; |
| width: 100%; |
| } |
| 100% { |
| left: 100%; |
| width: 10%; |
| } |
| } |
| `; |
| |
| const styleSheet = document.createElement("style") |
| styleSheet.type = "text/css"; |
| styleSheet.innerText = styles; |
| document.head.appendChild(styleSheet); |
| |
| const loader = document.createElement('div'); |
| loader.className = "flutter-loader"; |
| document.body.append(loader); |
| |
| const indeterminate = document.createElement('div'); |
| indeterminate.className = "indeterminate"; |
| loader.appendChild(indeterminate); |
| |
| document.addEventListener('dart-app-ready', function (e) { |
| loader.parentNode.removeChild(loader); |
| styleSheet.parentNode.removeChild(styleSheet); |
| }); |
| '''; |
| } |
| |
| const _onLoadEndCallback = r'$onLoadEndCallback'; |
| |
| String generateDDCLibraryBundleMainModule({ |
| required String entrypoint, |
| required bool nativeNullAssertions, |
| required String onLoadEndBootstrap, |
| required bool isCi, |
| }) { |
| // Chrome in CI seems to hang when there are too many requests at once, so we |
| // limit the max number of script requests for that environment. |
| // https://github.com/flutter/flutter/issues/169574 |
| final setMaxRequests = isCi ? r'window.$dartLoader.loadConfig.maxRequestPoolSize = 100;' : ''; |
| // The typo below in "EXTENTION" is load-bearing, package:build depends on it. |
| return ''' |
| /* ENTRYPOINT_EXTENTION_MARKER */ |
| |
| (function() { |
| const appName = "org-dartlang-app:/$entrypoint"; |
| |
| dartDevEmbedder.debugger.registerDevtoolsFormatter(); |
| |
| $setMaxRequests |
| // Set up a final script that lets us know when all scripts have been loaded. |
| // Only then can we call the main method. |
| const onLoadEndSrc = '$onLoadEndBootstrap'; |
| window.\$dartLoader.loadConfig.bootstrapScript = { |
| src: onLoadEndSrc, |
| id: onLoadEndSrc, |
| }; |
| window.\$dartLoader.loadConfig.tryLoadBootstrapScript = true; |
| // Should be called by $onLoadEndBootstrap once all the scripts have been |
| // loaded. |
| window.$_onLoadEndCallback = function() { |
| const child = {}; |
| child.main = function() { |
| const sdkOptions = { |
| nativeNonNullAsserts: $nativeNullAssertions, |
| }; |
| dartDevEmbedder.runMain(appName, sdkOptions); |
| } |
| /* MAIN_EXTENSION_MARKER */ |
| child.main(); |
| } |
| })(); |
| '''; |
| } |
| |
| String generateDDCLibraryBundleOnLoadEndBootstrap() { |
| return '''window.$_onLoadEndCallback();'''; |
| } |
| |
| /// Generate a synthetic main module which captures the application's main |
| /// method. |
| /// |
| /// If a [bootstrapModule] name is not provided, defaults to 'main_module.bootstrap'. |
| /// |
| /// RE: Object.keys usage in app.main: |
| /// This attaches the main entrypoint and hot reload functionality to the window. |
| /// The app module will have a single property which contains the actual application |
| /// code. The property name is based off of the entrypoint that is generated, for example |
| /// the file `foo/bar/baz.dart` will generate a property named approximately |
| /// `foo__bar__baz`. Rather than attempt to guess, we assume the first property of |
| /// this object is the module. |
| String generateMainModule({ |
| required String entrypoint, |
| required bool nativeNullAssertions, |
| String bootstrapModule = 'main_module.bootstrap', |
| String loaderRootDirectory = '', |
| }) { |
| // The typo below in "EXTENTION" is load-bearing, package:build depends on it. |
| return ''' |
| /* ENTRYPOINT_EXTENTION_MARKER */ |
| // Disable require module timeout |
| require.config({ |
| waitSeconds: 0 |
| }); |
| // Create the main module loaded below. |
| define("$bootstrapModule", ["$entrypoint", "dart_sdk"], function(app, dart_sdk) { |
| dart_sdk.dart.setStartAsyncSynchronously(true); |
| dart_sdk._debugger.registerDevtoolsFormatter(); |
| dart_sdk.dart.nativeNonNullAsserts($nativeNullAssertions); |
| |
| // See the generateMainModule doc comment. |
| var child = {}; |
| child.main = app[Object.keys(app)[0]].main; |
| |
| /* MAIN_EXTENSION_MARKER */ |
| child.main(); |
| |
| window.\$dartLoader = {}; |
| window.\$dartLoader.rootDirectories = ["$loaderRootDirectory"]; |
| if (window.\$requireLoader) { |
| window.\$requireLoader.getModuleLibraries = dart_sdk.dart.getModuleLibraries; |
| } |
| if (window.\$dartStackTraceUtility && !window.\$dartStackTraceUtility.ready) { |
| window.\$dartStackTraceUtility.ready = true; |
| let dart = dart_sdk.dart; |
| window.\$dartStackTraceUtility.setSourceMapProvider(function(url) { |
| var baseUrl = window.location.protocol + '//' + window.location.host; |
| url = url.replace(baseUrl + '/', ''); |
| if (url == 'dart_sdk.js') { |
| return dart.getSourceMap('dart_sdk'); |
| } |
| url = url.replace(".lib.js", ""); |
| return dart.getSourceMap(url); |
| }); |
| } |
| // Prevent DDC's requireJS to interfere with modern bundling. |
| if (typeof define === 'function' && define.amd) { |
| // Preserve a copy just in case... |
| define._amd = define.amd; |
| delete define.amd; |
| } |
| }); |
| '''; |
| } |
| |
| typedef WebTestInfo = ({String entryPoint, Uri goldensUri, String? configFile}); |
| |
| /// Generates the bootstrap logic required for running a group of unit test |
| /// files in the browser. |
| /// |
| /// This creates one "switchboard" main function that imports all the main |
| /// functions of the unit test files that need to be run. The javascript code |
| /// that starts the test sets a `window.testSelector` that specifies which main |
| /// function to invoke. This allows us to compile all the unit test files as a |
| /// single web application and invoke that with a different selector for each |
| /// test. |
| String generateTestEntrypoint({ |
| required List<WebTestInfo> testInfos, |
| required LanguageVersion languageVersion, |
| }) { |
| final importMainStatements = <String>[]; |
| final importTestConfigStatements = <String>[]; |
| final webTestPairs = <String>[]; |
| |
| for (var index = 0; index < testInfos.length; index++) { |
| final WebTestInfo testInfo = testInfos[index]; |
| final String entryPointPath = testInfo.entryPoint; |
| importMainStatements.add( |
| "import 'org-dartlang-app:///${Uri.file(entryPointPath)}' as test_$index show main;", |
| ); |
| |
| final String? testConfigPath = testInfo.configFile; |
| String? testConfigFunction = 'null'; |
| if (testConfigPath != null) { |
| importTestConfigStatements.add( |
| "import 'org-dartlang-app:///${Uri.file(testConfigPath)}' as test_config_$index show testExecutable;", |
| ); |
| testConfigFunction = 'test_config_$index.testExecutable'; |
| } |
| webTestPairs.add(''' |
| '$entryPointPath': ( |
| entryPoint: test_$index.main, |
| entryPointRunner: $testConfigFunction, |
| goldensUri: Uri.parse('${testInfo.goldensUri}'), |
| ), |
| '''); |
| } |
| return ''' |
| // @dart = ${languageVersion.major}.${languageVersion.minor} |
| |
| ${importMainStatements.join('\n')} |
| |
| ${importTestConfigStatements.join('\n')} |
| |
| import 'package:flutter_test/flutter_test.dart'; |
| |
| Map<String, WebTest> webTestMap = <String, WebTest>{ |
| ${webTestPairs.join('\n')} |
| }; |
| |
| Future<void> main() { |
| final WebTest? webTest = webTestMap[testSelector]; |
| if (webTest == null) { |
| throw Exception('Web test for \${testSelector} not found'); |
| } |
| return runWebTest(webTest); |
| } |
| '''; |
| } |
| |
| /// Generate the unit test bootstrap file. |
| String generateTestBootstrapFileContents(String mainUri, String requireUrl, String mapperUrl) { |
| return ''' |
| (function() { |
| if (typeof document != 'undefined') { |
| var el = document.createElement("script"); |
| el.defer = true; |
| el.async = false; |
| el.src = '$mapperUrl'; |
| document.head.appendChild(el); |
| |
| el = document.createElement("script"); |
| el.defer = true; |
| el.async = false; |
| el.src = '$requireUrl'; |
| el.setAttribute("data-main", '$mainUri'); |
| document.head.appendChild(el); |
| } else { |
| importScripts('$mapperUrl', '$requireUrl'); |
| require.config({ |
| baseUrl: baseUrl, |
| }); |
| window = self; |
| require(['$mainUri']); |
| } |
| })(); |
| '''; |
| } |
| |
| String generateDefaultFlutterBootstrapScript({required bool includeServiceWorkerSettings}) { |
| final serviceWorkerSettings = includeServiceWorkerSettings |
| ? ''' |
| { |
| serviceWorkerSettings: { |
| serviceWorkerVersion: {{flutter_service_worker_version}} |
| } |
| }''' |
| : ''; |
| return ''' |
| {{flutter_js}} |
| {{flutter_build_config}} |
| _flutter.loader.load($serviceWorkerSettings); |
| '''; |
| } |