1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'package:package_config/package_config.dart';
6
7/// Used to load prerequisite scripts such as ddc_module_loader.js
8const _simpleLoaderScript = r'''
9window.$dartCreateScript = (function() {
10 // Find the nonce value. (Note, this is only computed once.)
11 var scripts = Array.from(document.getElementsByTagName("script"));
12 var nonce;
13 scripts.some(
14 script => (nonce = script.nonce || script.getAttribute("nonce")));
15 // If present, return a closure that automatically appends the nonce.
16 if (nonce) {
17 return function() {
18 var script = document.createElement("script");
19 script.nonce = nonce;
20 return script;
21 };
22 } else {
23 return function() {
24 return document.createElement("script");
25 };
26 }
27})();
28
29// Loads a module [relativeUrl] relative to [root].
30//
31// If not specified, [root] defaults to the directory serving the main app.
32var forceLoadModule = function (relativeUrl, root) {
33 var actualRoot = root ?? _currentDirectory;
34 return new Promise(function(resolve, reject) {
35 var script = self.$dartCreateScript();
36 let policy = {
37 createScriptURL: function(src) {return src;}
38 };
39 if (self.trustedTypes && self.trustedTypes.createPolicy) {
40 policy = self.trustedTypes.createPolicy('dartDdcModuleUrl', policy);
41 }
42 script.onload = resolve;
43 script.onerror = reject;
44 script.src = policy.createScriptURL(actualRoot + relativeUrl);
45 document.head.appendChild(script);
46 });
47};
48''';
49
50// TODO(srujzs): Delete this once it's no longer used internally.
51String generateDDCBootstrapScript({
52 required String entrypoint,
53 required String ddcModuleLoaderUrl,
54 required String mapperUrl,
55 required bool generateLoadingIndicator,
56 String appRootDirectory = '/',
57}) {
58 return '''
59${generateLoadingIndicator ? _generateLoadingIndicator() : ""}
60// TODO(markzipan): This is safe if Flutter app roots are always equal to the
61// host root '/'. Validate if this is true.
62var _currentDirectory = "$appRootDirectory";
63
64$_simpleLoaderScript
65
66// A map containing the URLs for the bootstrap scripts in debug.
67let _scriptUrls = {
68 "mapper": "$mapperUrl",
69 "moduleLoader": "$ddcModuleLoaderUrl"
70};
71
72(function() {
73 let appName = "$entrypoint";
74
75 // A uuid that identifies a subapp.
76 // Stubbed out since subapps aren't supported in Flutter.
77 let uuid = "00000000-0000-0000-0000-000000000000";
78
79 window.postMessage(
80 {type: "DDC_STATE_CHANGE", state: "initial_load", targetUuid: uuid}, "*");
81
82 // Load pre-requisite DDC scripts.
83 // We intentionally use invalid names to avoid namespace clashes.
84 let prerequisiteScripts = [
85 {
86 "src": "$ddcModuleLoaderUrl",
87 "id": "ddc_module_loader \x00"
88 },
89 {
90 "src": "$mapperUrl",
91 "id": "dart_stack_trace_mapper \x00"
92 }
93 ];
94
95 // Load ddc_module_loader.js to access DDC's module loader API.
96 let prerequisiteLoads = [];
97 for (let i = 0; i < prerequisiteScripts.length; i++) {
98 prerequisiteLoads.push(forceLoadModule(prerequisiteScripts[i].src));
99 }
100 Promise.all(prerequisiteLoads).then((_) => afterPrerequisiteLogic());
101
102 // Save the current script so we can access it in a closure.
103 var _currentScript = document.currentScript;
104
105 var afterPrerequisiteLogic = function() {
106 window.\$dartLoader.rootDirectories.push(_currentDirectory);
107 let scripts = [
108 {
109 "src": "dart_sdk.js",
110 "id": "dart_sdk"
111 },
112 {
113 "src": "main_module.bootstrap.js",
114 "id": "data-main"
115 }
116 ];
117 let loadConfig = new window.\$dartLoader.LoadConfiguration();
118 loadConfig.bootstrapScript = scripts[scripts.length - 1];
119
120 loadConfig.loadScriptFn = function(loader) {
121 loader.addScriptsToQueue(scripts, null);
122 loader.loadEnqueuedModules();
123 }
124 loadConfig.ddcEventForLoadStart = /* LOAD_ALL_MODULES_START */ 1;
125 loadConfig.ddcEventForLoadedOk = /* LOAD_ALL_MODULES_END_OK */ 2;
126 loadConfig.ddcEventForLoadedError = /* LOAD_ALL_MODULES_END_ERROR */ 3;
127
128 let loader = new window.\$dartLoader.DDCLoader(loadConfig);
129
130 // Record prerequisite scripts' fully resolved URLs.
131 prerequisiteScripts.forEach(script => loader.registerScript(script));
132
133 // Note: these variables should only be used in non-multi-app scenarios since
134 // they can be arbitrarily overridden based on multi-app load order.
135 window.\$dartLoader.loadConfig = loadConfig;
136 window.\$dartLoader.loader = loader;
137 loader.nextAttempt();
138 }
139})();
140''';
141}
142
143String generateDDCLibraryBundleBootstrapScript({
144 required String entrypoint,
145 required String ddcModuleLoaderUrl,
146 required String mapperUrl,
147 required bool generateLoadingIndicator,
148 required bool isWindows,
149}) {
150 return '''
151${generateLoadingIndicator ? _generateLoadingIndicator() : ""}
152// Save the current directory so we can access it in a closure.
153var _currentDirectory = (function () {
154 var _url = document.currentScript.src;
155 var lastSlash = _url.lastIndexOf('/');
156 if (lastSlash == -1) return _url;
157 var currentDirectory = _url.substring(0, lastSlash + 1);
158 return currentDirectory;
159})();
160
161$_simpleLoaderScript
162
163(function() {
164 let appName = "org-dartlang-app:/$entrypoint";
165
166 // Load pre-requisite DDC scripts. We intentionally use invalid names to avoid
167 // namespace clashes.
168 let prerequisiteScripts = [
169 {
170 "src": "$ddcModuleLoaderUrl",
171 "id": "ddc_module_loader \x00"
172 },
173 {
174 "src": "$mapperUrl",
175 "id": "dart_stack_trace_mapper \x00"
176 }
177 ];
178
179 // Load ddc_module_loader.js to access DDC's module loader API.
180 let prerequisiteLoads = [];
181 for (let i = 0; i < prerequisiteScripts.length; i++) {
182 prerequisiteLoads.push(forceLoadModule(prerequisiteScripts[i].src));
183 }
184 Promise.all(prerequisiteLoads).then((_) => afterPrerequisiteLogic());
185
186 // Save the current script so we can access it in a closure.
187 var _currentScript = document.currentScript;
188
189 // Create a policy if needed to load the files during a hot restart.
190 let policy = {
191 createScriptURL: function(src) {return src;}
192 };
193 if (self.trustedTypes && self.trustedTypes.createPolicy) {
194 policy = self.trustedTypes.createPolicy('dartDdcModuleUrl', policy);
195 }
196
197 var afterPrerequisiteLogic = function() {
198 window.\$dartLoader.rootDirectories.push(_currentDirectory);
199 let scripts = [
200 {
201 "src": "dart_sdk.js",
202 "id": "dart_sdk"
203 },
204 {
205 "src": "main_module.bootstrap.js",
206 "id": "data-main"
207 }
208 ];
209
210 let loadConfig = new window.\$dartLoader.LoadConfiguration();
211 // TODO(srujzs): Verify this is sufficient for Windows.
212 loadConfig.isWindows = $isWindows;
213 loadConfig.bootstrapScript = scripts[scripts.length - 1];
214
215 loadConfig.loadScriptFn = function(loader) {
216 loader.addScriptsToQueue(scripts, null);
217 loader.loadEnqueuedModules();
218 }
219 loadConfig.ddcEventForLoadStart = /* LOAD_ALL_MODULES_START */ 1;
220 loadConfig.ddcEventForLoadedOk = /* LOAD_ALL_MODULES_END_OK */ 2;
221 loadConfig.ddcEventForLoadedError = /* LOAD_ALL_MODULES_END_ERROR */ 3;
222
223 let loader = new window.\$dartLoader.DDCLoader(loadConfig);
224
225 // Record prerequisite scripts' fully resolved URLs.
226 prerequisiteScripts.forEach(script => loader.registerScript(script));
227
228 // Note: these variables should only be used in non-multi-app scenarios
229 // since they can be arbitrarily overridden based on multi-app load order.
230 window.\$dartLoader.loadConfig = loadConfig;
231 window.\$dartLoader.loader = loader;
232
233 // Begin loading libraries
234 loader.nextAttempt();
235
236 // Set up stack trace mapper.
237 if (window.\$dartStackTraceUtility &&
238 !window.\$dartStackTraceUtility.ready) {
239 window.\$dartStackTraceUtility.ready = true;
240 window.\$dartStackTraceUtility.setSourceMapProvider(function(url) {
241 var baseUrl = window.location.protocol + '//' + window.location.host;
242 url = url.replace(baseUrl + '/', '');
243 if (url == 'dart_sdk.js') {
244 return dartDevEmbedder.debugger.getSourceMap('dart_sdk');
245 }
246 url = url.replace(".lib.js", "");
247 return dartDevEmbedder.debugger.getSourceMap(url);
248 });
249 }
250
251 let currentUri = _currentScript.src;
252 // We should have written a file containing all the scripts that need to be
253 // reloaded into the page. This is then read when a hot restart is triggered
254 // in DDC via the `\$dartReloadModifiedModules` callback.
255 let restartScripts = _currentDirectory + 'restart_scripts.json';
256
257 if (!window.\$dartReloadModifiedModules) {
258 window.\$dartReloadModifiedModules = (function(appName, callback) {
259 var xhttp = new XMLHttpRequest();
260 xhttp.withCredentials = true;
261 xhttp.onreadystatechange = function() {
262 // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/readyState
263 if (this.readyState == 4 && this.status == 200 || this.status == 304) {
264 var scripts = JSON.parse(this.responseText);
265 var numToLoad = 0;
266 var numLoaded = 0;
267 for (var i = 0; i < scripts.length; i++) {
268 var script = scripts[i];
269 if (script.id == null) continue;
270 var src = _currentDirectory + script.src.toString();
271 var oldSrc = window.\$dartLoader.moduleIdToUrl.get(script.id);
272
273 // We might actually load from a different uri, delete the old one
274 // just to be sure.
275 window.\$dartLoader.urlToModuleId.delete(oldSrc);
276
277 window.\$dartLoader.moduleIdToUrl.set(script.id, src);
278 window.\$dartLoader.urlToModuleId.set(src, script.id);
279
280 numToLoad++;
281
282 var el = document.getElementById(script.id);
283 if (el) el.remove();
284 el = window.\$dartCreateScript();
285 el.src = policy.createScriptURL(src);
286 el.async = false;
287 el.defer = true;
288 el.id = script.id;
289 el.onload = function() {
290 numLoaded++;
291 if (numToLoad == numLoaded) callback();
292 };
293 document.head.appendChild(el);
294 }
295 // Call `callback` right away if we found no updated scripts.
296 if (numToLoad == 0) callback();
297 }
298 };
299 xhttp.open("GET", restartScripts, true);
300 xhttp.send();
301 });
302 }
303 };
304})();
305''';
306}
307
308/// The JavaScript bootstrap script to support in-browser hot restart.
309///
310/// The [requireUrl] loads our cached RequireJS script file. The [mapperUrl]
311/// loads the special Dart stack trace mapper.
312///
313/// This file is served when the browser requests "main.dart.js" in debug mode,
314/// and is responsible for bootstrapping the RequireJS modules and attaching
315/// the hot reload hooks.
316///
317/// If [generateLoadingIndicator] is `true`, embeds a loading indicator onto the
318/// web page that's visible while the Flutter app is loading.
319String generateBootstrapScript({
320 required String requireUrl,
321 required String mapperUrl,
322 required bool generateLoadingIndicator,
323}) {
324 return '''
325"use strict";
326
327${generateLoadingIndicator ? _generateLoadingIndicator() : ''}
328
329// A map containing the URLs for the bootstrap scripts in debug.
330let _scriptUrls = {
331 "mapper": "$mapperUrl",
332 "requireJs": "$requireUrl"
333};
334
335// Create a TrustedTypes policy so we can attach Scripts...
336let _ttPolicy;
337if (window.trustedTypes) {
338 _ttPolicy = trustedTypes.createPolicy("flutter-tools-bootstrap", {
339 createScriptURL: (url) => {
340 let scriptUrl = _scriptUrls[url];
341 if (!scriptUrl) {
342 console.error("Unknown Flutter Web bootstrap resource!", url);
343 }
344 return scriptUrl;
345 }
346 });
347}
348
349// Creates a TrustedScriptURL for a given `scriptName`.
350// See `_scriptUrls` and `_ttPolicy` above.
351function getTTScriptUrl(scriptName) {
352 let defaultUrl = _scriptUrls[scriptName];
353 return _ttPolicy ? _ttPolicy.createScriptURL(scriptName) : defaultUrl;
354}
355
356// Attach source mapping.
357var mapperEl = document.createElement("script");
358mapperEl.defer = true;
359mapperEl.async = false;
360mapperEl.src = getTTScriptUrl("mapper");
361document.head.appendChild(mapperEl);
362
363// Attach require JS.
364var requireEl = document.createElement("script");
365requireEl.defer = true;
366requireEl.async = false;
367requireEl.src = getTTScriptUrl("requireJs");
368// This attribute tells require JS what to load as main (defined below).
369requireEl.setAttribute("data-main", "main_module.bootstrap");
370document.head.appendChild(requireEl);
371''';
372}
373
374/// Creates a visual animated loading indicator and puts it on the page to
375/// provide feedback to the developer that the app is being loaded. Otherwise,
376/// the developer would be staring at a blank page wondering if the app will
377/// come up or not.
378///
379/// This indicator should only be used when DWDS is enabled, e.g. with the
380/// `-d chrome` option. Debug builds without DWDS, e.g. `flutter run -d web-server`
381/// or `flutter build web --debug` should not use this indicator.
382String _generateLoadingIndicator() {
383 return '''
384var styles = `
385 .flutter-loader {
386 width: 100%;
387 height: 8px;
388 background-color: #13B9FD;
389 position: absolute;
390 top: 0px;
391 left: 0px;
392 overflow: hidden;
393 }
394
395 .indeterminate {
396 position: relative;
397 width: 100%;
398 height: 100%;
399 }
400
401 .indeterminate:before {
402 content: '';
403 position: absolute;
404 height: 100%;
405 background-color: #0175C2;
406 animation: indeterminate_first 2.0s infinite ease-out;
407 }
408
409 .indeterminate:after {
410 content: '';
411 position: absolute;
412 height: 100%;
413 background-color: #02569B;
414 animation: indeterminate_second 2.0s infinite ease-in;
415 }
416
417 @keyframes indeterminate_first {
418 0% {
419 left: -100%;
420 width: 100%;
421 }
422 100% {
423 left: 100%;
424 width: 10%;
425 }
426 }
427
428 @keyframes indeterminate_second {
429 0% {
430 left: -150%;
431 width: 100%;
432 }
433 100% {
434 left: 100%;
435 width: 10%;
436 }
437 }
438`;
439
440var styleSheet = document.createElement("style")
441styleSheet.type = "text/css";
442styleSheet.innerText = styles;
443document.head.appendChild(styleSheet);
444
445var loader = document.createElement('div');
446loader.className = "flutter-loader";
447document.body.append(loader);
448
449var indeterminate = document.createElement('div');
450indeterminate.className = "indeterminate";
451loader.appendChild(indeterminate);
452
453document.addEventListener('dart-app-ready', function (e) {
454 loader.parentNode.removeChild(loader);
455 styleSheet.parentNode.removeChild(styleSheet);
456});
457''';
458}
459
460const _onLoadEndCallback = r'$onLoadEndCallback';
461
462String generateDDCLibraryBundleMainModule({
463 required String entrypoint,
464 required bool nativeNullAssertions,
465 required String onLoadEndBootstrap,
466 required bool isCi,
467}) {
468 // Chrome in CI seems to hang when there are too many requests at once, so we
469 // limit the max number of script requests for that environment.
470 // https://github.com/flutter/flutter/issues/169574
471 final setMaxRequests = isCi ? r'window.$dartLoader.loadConfig.maxRequestPoolSize = 100;' : '';
472 // The typo below in "EXTENTION" is load-bearing, package:build depends on it.
473 return '''
474/* ENTRYPOINT_EXTENTION_MARKER */
475
476(function() {
477 let appName = "org-dartlang-app:/$entrypoint";
478
479 dartDevEmbedder.debugger.registerDevtoolsFormatter();
480
481 $setMaxRequests
482 // Set up a final script that lets us know when all scripts have been loaded.
483 // Only then can we call the main method.
484 let onLoadEndSrc = '$onLoadEndBootstrap';
485 window.\$dartLoader.loadConfig.bootstrapScript = {
486 src: onLoadEndSrc,
487 id: onLoadEndSrc,
488 };
489 window.\$dartLoader.loadConfig.tryLoadBootstrapScript = true;
490 // Should be called by $onLoadEndBootstrap once all the scripts have been
491 // loaded.
492 window.$_onLoadEndCallback = function() {
493 let child = {};
494 child.main = function() {
495 let sdkOptions = {
496 nativeNonNullAsserts: $nativeNullAssertions,
497 };
498 dartDevEmbedder.runMain(appName, sdkOptions);
499 }
500 /* MAIN_EXTENSION_MARKER */
501 child.main();
502 }
503})();
504''';
505}
506
507String generateDDCLibraryBundleOnLoadEndBootstrap() {
508 return '''window.$_onLoadEndCallback();''';
509}
510
511/// Generate a synthetic main module which captures the application's main
512/// method.
513///
514/// If a [bootstrapModule] name is not provided, defaults to 'main_module.bootstrap'.
515///
516/// RE: Object.keys usage in app.main:
517/// This attaches the main entrypoint and hot reload functionality to the window.
518/// The app module will have a single property which contains the actual application
519/// code. The property name is based off of the entrypoint that is generated, for example
520/// the file `foo/bar/baz.dart` will generate a property named approximately
521/// `foo__bar__baz`. Rather than attempt to guess, we assume the first property of
522/// this object is the module.
523String generateMainModule({
524 required String entrypoint,
525 required bool nativeNullAssertions,
526 String bootstrapModule = 'main_module.bootstrap',
527 String loaderRootDirectory = '',
528}) {
529 // The typo below in "EXTENTION" is load-bearing, package:build depends on it.
530 return '''
531/* ENTRYPOINT_EXTENTION_MARKER */
532// Disable require module timeout
533require.config({
534 waitSeconds: 0
535});
536// Create the main module loaded below.
537define("$bootstrapModule", ["$entrypoint", "dart_sdk"], function(app, dart_sdk) {
538 dart_sdk.dart.setStartAsyncSynchronously(true);
539 dart_sdk._debugger.registerDevtoolsFormatter();
540 dart_sdk.dart.nativeNonNullAsserts($nativeNullAssertions);
541
542 // See the generateMainModule doc comment.
543 var child = {};
544 child.main = app[Object.keys(app)[0]].main;
545
546 /* MAIN_EXTENSION_MARKER */
547 child.main();
548
549 window.\$dartLoader = {};
550 window.\$dartLoader.rootDirectories = ["$loaderRootDirectory"];
551 if (window.\$requireLoader) {
552 window.\$requireLoader.getModuleLibraries = dart_sdk.dart.getModuleLibraries;
553 }
554 if (window.\$dartStackTraceUtility && !window.\$dartStackTraceUtility.ready) {
555 window.\$dartStackTraceUtility.ready = true;
556 let dart = dart_sdk.dart;
557 window.\$dartStackTraceUtility.setSourceMapProvider(function(url) {
558 var baseUrl = window.location.protocol + '//' + window.location.host;
559 url = url.replace(baseUrl + '/', '');
560 if (url == 'dart_sdk.js') {
561 return dart.getSourceMap('dart_sdk');
562 }
563 url = url.replace(".lib.js", "");
564 return dart.getSourceMap(url);
565 });
566 }
567 // Prevent DDC's requireJS to interfere with modern bundling.
568 if (typeof define === 'function' && define.amd) {
569 // Preserve a copy just in case...
570 define._amd = define.amd;
571 delete define.amd;
572 }
573});
574''';
575}
576
577typedef WebTestInfo = ({String entryPoint, Uri goldensUri, String? configFile});
578
579/// Generates the bootstrap logic required for running a group of unit test
580/// files in the browser.
581///
582/// This creates one "switchboard" main function that imports all the main
583/// functions of the unit test files that need to be run. The javascript code
584/// that starts the test sets a `window.testSelector` that specifies which main
585/// function to invoke. This allows us to compile all the unit test files as a
586/// single web application and invoke that with a different selector for each
587/// test.
588String generateTestEntrypoint({
589 required List<WebTestInfo> testInfos,
590 required LanguageVersion languageVersion,
591}) {
592 final importMainStatements = <String>[];
593 final importTestConfigStatements = <String>[];
594 final webTestPairs = <String>[];
595
596 for (var index = 0; index < testInfos.length; index++) {
597 final WebTestInfo testInfo = testInfos[index];
598 final String entryPointPath = testInfo.entryPoint;
599 importMainStatements.add(
600 "import 'org-dartlang-app:///${Uri.file(entryPointPath)}' as test_$index show main;",
601 );
602
603 final String? testConfigPath = testInfo.configFile;
604 String? testConfigFunction = 'null';
605 if (testConfigPath != null) {
606 importTestConfigStatements.add(
607 "import 'org-dartlang-app:///${Uri.file(testConfigPath)}' as test_config_$index show testExecutable;",
608 );
609 testConfigFunction = 'test_config_$index.testExecutable';
610 }
611 webTestPairs.add('''
612 '$entryPointPath': (
613 entryPoint: test_$index.main,
614 entryPointRunner: $testConfigFunction,
615 goldensUri: Uri.parse('${testInfo.goldensUri}'),
616 ),
617''');
618 }
619 return '''
620// @dart = ${languageVersion.major}.${languageVersion.minor}
621
622${importMainStatements.join('\n')}
623
624${importTestConfigStatements.join('\n')}
625
626import 'package:flutter_test/flutter_test.dart';
627
628Map<String, WebTest> webTestMap = <String, WebTest>{
629 ${webTestPairs.join('\n')}
630};
631
632Future<void> main() {
633 final WebTest? webTest = webTestMap[testSelector];
634 if (webTest == null) {
635 throw Exception('Web test for \${testSelector} not found');
636 }
637 return runWebTest(webTest);
638}
639 ''';
640}
641
642/// Generate the unit test bootstrap file.
643String generateTestBootstrapFileContents(String mainUri, String requireUrl, String mapperUrl) {
644 return '''
645(function() {
646 if (typeof document != 'undefined') {
647 var el = document.createElement("script");
648 el.defer = true;
649 el.async = false;
650 el.src = '$mapperUrl';
651 document.head.appendChild(el);
652
653 el = document.createElement("script");
654 el.defer = true;
655 el.async = false;
656 el.src = '$requireUrl';
657 el.setAttribute("data-main", '$mainUri');
658 document.head.appendChild(el);
659 } else {
660 importScripts('$mapperUrl', '$requireUrl');
661 require.config({
662 baseUrl: baseUrl,
663 });
664 window = self;
665 require(['$mainUri']);
666 }
667})();
668''';
669}
670
671String generateDefaultFlutterBootstrapScript({required bool includeServiceWorkerSettings}) {
672 final serviceWorkerSettings = includeServiceWorkerSettings
673 ? '''
674{
675 serviceWorkerSettings: {
676 serviceWorkerVersion: {{flutter_service_worker_version}}
677 }
678}'''
679 : '';
680 return '''
681{{flutter_js}}
682{{flutter_build_config}}
683_flutter.loader.load($serviceWorkerSettings);
684''';
685}
686