| // 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'; | |
| /// 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. The [entrypoint] is the | |
| /// actual main.dart file. | |
| /// | |
| /// 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 ''' | |
| var 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%; | |
| } | |
| } | |
| `; | |
| var styleSheet = document.createElement("style") | |
| styleSheet.type = "text/css"; | |
| styleSheet.innerText = styles; | |
| document.head.appendChild(styleSheet); | |
| var loader = document.createElement('div'); | |
| loader.className = "flutter-loader"; | |
| document.body.append(loader); | |
| var 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); | |
| }); | |
| '''; | |
| } | |
| /// 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 nullAssertions, | |
| required bool nativeNullAssertions, | |
| String bootstrapModule = 'main_module.bootstrap', | |
| }) { | |
| // 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.nonNullAsserts($nullAssertions); | |
| 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 = []; | |
| 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; | |
| } | |
| }); | |
| '''; | |
| } | |
| /// Generates the bootstrap logic required for a flutter test running in a browser. | |
| /// | |
| /// This hard-codes the device pixel ratio to 3.0 and a 2400 x 1800 window size. | |
| String generateTestEntrypoint({ | |
| required String relativeTestPath, | |
| required String absolutePath, | |
| required String? testConfigPath, | |
| required LanguageVersion languageVersion, | |
| }) { | |
| return ''' | |
| // @dart = ${languageVersion.major}.${languageVersion.minor} | |
| import 'org-dartlang-app:///$relativeTestPath' as test; | |
| import 'dart:ui' as ui; | |
| import 'dart:ui_web' as ui_web; | |
| import 'dart:html'; | |
| import 'dart:js'; | |
| ${testConfigPath != null ? "import '${Uri.file(testConfigPath)}' as test_config;" : ""} | |
| import 'package:stream_channel/stream_channel.dart'; | |
| import 'package:flutter_test/flutter_test.dart'; | |
| import 'package:test_api/backend.dart'; | |
| Future<void> main() async { | |
| ui_web.debugEmulateFlutterTesterEnvironment = true; | |
| await ui_web.bootstrapEngine(); | |
| webGoldenComparator = DefaultWebGoldenComparator(Uri.parse('${Uri.file(absolutePath)}')); | |
| ui_web.debugOverrideDevicePixelRatio(3.0); | |
| ui.window.debugPhysicalSizeOverride = const ui.Size(2400, 1800); | |
| internalBootstrapBrowserTest(() { | |
| return ${testConfigPath != null ? "() => test_config.testExecutable(test.main)" : "test.main"}; | |
| }); | |
| } | |
| void internalBootstrapBrowserTest(Function getMain()) { | |
| var channel = serializeSuite(getMain, hidePrints: false); | |
| postMessageChannel().pipe(channel); | |
| } | |
| StreamChannel serializeSuite(Function getMain(), {bool hidePrints = true}) => RemoteListener.start(getMain, hidePrints: hidePrints); | |
| StreamChannel postMessageChannel() { | |
| var controller = StreamChannelController<Object?>(sync: true); | |
| var channel = MessageChannel(); | |
| window.parent!.postMessage('port', window.location.origin, [channel.port2]); | |
| var portSubscription = channel.port1.onMessage.listen((message) { | |
| controller.local.sink.add(message.data); | |
| }); | |
| controller.local.stream | |
| .listen(channel.port1.postMessage, onDone: portSubscription.cancel); | |
| return controller.foreign; | |
| } | |
| '''; | |
| } | |
| /// 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']); | |
| } | |
| })(); | |
| '''; | |
| } |