// 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 testInfos, required LanguageVersion languageVersion, }) { final importMainStatements = []; final importTestConfigStatements = []; final webTestPairs = []; 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 webTestMap = { ${webTestPairs.join('\n')} }; Future 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); '''; }