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
7String generateDDCBootstrapScript({
8 required String entrypoint,
9 required String ddcModuleLoaderUrl,
10 required String mapperUrl,
11 required bool generateLoadingIndicator,
12 String appRootDirectory = '/',
13}) {
14 return '''
15${generateLoadingIndicator ? _generateLoadingIndicator() : ""}
16// TODO(markzipan): This is safe if Flutter app roots are always equal to the
17// host root '/'. Validate if this is true.
18var _currentDirectory = "$appRootDirectory";
19
20window.\$dartCreateScript = (function() {
21 // Find the nonce value. (Note, this is only computed once.)
22 var scripts = Array.from(document.getElementsByTagName("script"));
23 var nonce;
24 scripts.some(
25 script => (nonce = script.nonce || script.getAttribute("nonce")));
26 // If present, return a closure that automatically appends the nonce.
27 if (nonce) {
28 return function() {
29 var script = document.createElement("script");
30 script.nonce = nonce;
31 return script;
32 };
33 } else {
34 return function() {
35 return document.createElement("script");
36 };
37 }
38})();
39
40// Loads a module [relativeUrl] relative to [root].
41//
42// If not specified, [root] defaults to the directory serving the main app.
43var forceLoadModule = function (relativeUrl, root) {
44 var actualRoot = root ?? _currentDirectory;
45 return new Promise(function(resolve, reject) {
46 var script = self.\$dartCreateScript();
47 let policy = {
48 createScriptURL: function(src) {return src;}
49 };
50 if (self.trustedTypes && self.trustedTypes.createPolicy) {
51 policy = self.trustedTypes.createPolicy('dartDdcModuleUrl', policy);
52 }
53 script.onload = resolve;
54 script.onerror = reject;
55 script.src = policy.createScriptURL(actualRoot + relativeUrl);
56 document.head.appendChild(script);
57 });
58};
59
60// A map containing the URLs for the bootstrap scripts in debug.
61let _scriptUrls = {
62 "mapper": "$mapperUrl",
63 "moduleLoader": "$ddcModuleLoaderUrl"
64};
65
66(function() {
67 let appName = "$entrypoint";
68
69 // A uuid that identifies a subapp.
70 // Stubbed out since subapps aren't supported in Flutter.
71 let uuid = "00000000-0000-0000-0000-000000000000";
72
73 window.postMessage(
74 {type: "DDC_STATE_CHANGE", state: "initial_load", targetUuid: uuid}, "*");
75
76 // Load pre-requisite DDC scripts.
77 // We intentionally use invalid names to avoid namespace clashes.
78 let prerequisiteScripts = [
79 {
80 "src": "$ddcModuleLoaderUrl",
81 "id": "ddc_module_loader \x00"
82 },
83 {
84 "src": "$mapperUrl",
85 "id": "dart_stack_trace_mapper \x00"
86 }
87 ];
88
89 // Load ddc_module_loader.js to access DDC's module loader API.
90 let prerequisiteLoads = [];
91 for (let i = 0; i < prerequisiteScripts.length; i++) {
92 prerequisiteLoads.push(forceLoadModule(prerequisiteScripts[i].src));
93 }
94 Promise.all(prerequisiteLoads).then((_) => afterPrerequisiteLogic());
95
96 // Save the current script so we can access it in a closure.
97 var _currentScript = document.currentScript;
98
99 var afterPrerequisiteLogic = function() {
100 window.\$dartLoader.rootDirectories.push(_currentDirectory);
101 let scripts = [
102 {
103 "src": "dart_sdk.js",
104 "id": "dart_sdk"
105 },
106 {
107 "src": "main_module.bootstrap.js",
108 "id": "data-main"
109 }
110 ];
111 let loadConfig = new window.\$dartLoader.LoadConfiguration();
112 loadConfig.bootstrapScript = scripts[scripts.length - 1];
113
114 loadConfig.loadScriptFn = function(loader) {
115 loader.addScriptsToQueue(scripts, null);
116 loader.loadEnqueuedModules();
117 }
118 loadConfig.ddcEventForLoadStart = /* LOAD_ALL_MODULES_START */ 1;
119 loadConfig.ddcEventForLoadedOk = /* LOAD_ALL_MODULES_END_OK */ 2;
120 loadConfig.ddcEventForLoadedError = /* LOAD_ALL_MODULES_END_ERROR */ 3;
121
122 let loader = new window.\$dartLoader.DDCLoader(loadConfig);
123
124 // Record prerequisite scripts' fully resolved URLs.
125 prerequisiteScripts.forEach(script => loader.registerScript(script));
126
127 // Note: these variables should only be used in non-multi-app scenarios since
128 // they can be arbitrarily overridden based on multi-app load order.
129 window.\$dartLoader.loadConfig = loadConfig;
130 window.\$dartLoader.loader = loader;
131 loader.nextAttempt();
132 }
133})();
134''';
135}
136
137/// The JavaScript bootstrap script to support in-browser hot restart.
138///
139/// The [requireUrl] loads our cached RequireJS script file. The [mapperUrl]
140/// loads the special Dart stack trace mapper. The [entrypoint] is the
141/// actual main.dart file.
142///
143/// This file is served when the browser requests "main.dart.js" in debug mode,
144/// and is responsible for bootstrapping the RequireJS modules and attaching
145/// the hot reload hooks.
146///
147/// If `generateLoadingIndicator` is true, embeds a loading indicator onto the
148/// web page that's visible while the Flutter app is loading.
149String generateBootstrapScript({
150 required String requireUrl,
151 required String mapperUrl,
152 required bool generateLoadingIndicator,
153}) {
154 return '''
155"use strict";
156
157${generateLoadingIndicator ? _generateLoadingIndicator() : ''}
158
159// A map containing the URLs for the bootstrap scripts in debug.
160let _scriptUrls = {
161 "mapper": "$mapperUrl",
162 "requireJs": "$requireUrl"
163};
164
165// Create a TrustedTypes policy so we can attach Scripts...
166let _ttPolicy;
167if (window.trustedTypes) {
168 _ttPolicy = trustedTypes.createPolicy("flutter-tools-bootstrap", {
169 createScriptURL: (url) => {
170 let scriptUrl = _scriptUrls[url];
171 if (!scriptUrl) {
172 console.error("Unknown Flutter Web bootstrap resource!", url);
173 }
174 return scriptUrl;
175 }
176 });
177}
178
179// Creates a TrustedScriptURL for a given `scriptName`.
180// See `_scriptUrls` and `_ttPolicy` above.
181function getTTScriptUrl(scriptName) {
182 let defaultUrl = _scriptUrls[scriptName];
183 return _ttPolicy ? _ttPolicy.createScriptURL(scriptName) : defaultUrl;
184}
185
186// Attach source mapping.
187var mapperEl = document.createElement("script");
188mapperEl.defer = true;
189mapperEl.async = false;
190mapperEl.src = getTTScriptUrl("mapper");
191document.head.appendChild(mapperEl);
192
193// Attach require JS.
194var requireEl = document.createElement("script");
195requireEl.defer = true;
196requireEl.async = false;
197requireEl.src = getTTScriptUrl("requireJs");
198// This attribute tells require JS what to load as main (defined below).
199requireEl.setAttribute("data-main", "main_module.bootstrap");
200document.head.appendChild(requireEl);
201''';
202}
203
204/// Creates a visual animated loading indicator and puts it on the page to
205/// provide feedback to the developer that the app is being loaded. Otherwise,
206/// the developer would be staring at a blank page wondering if the app will
207/// come up or not.
208///
209/// This indicator should only be used when DWDS is enabled, e.g. with the
210/// `-d chrome` option. Debug builds without DWDS, e.g. `flutter run -d web-server`
211/// or `flutter build web --debug` should not use this indicator.
212String _generateLoadingIndicator() {
213 return '''
214var styles = `
215 .flutter-loader {
216 width: 100%;
217 height: 8px;
218 background-color: #13B9FD;
219 position: absolute;
220 top: 0px;
221 left: 0px;
222 overflow: hidden;
223 }
224
225 .indeterminate {
226 position: relative;
227 width: 100%;
228 height: 100%;
229 }
230
231 .indeterminate:before {
232 content: '';
233 position: absolute;
234 height: 100%;
235 background-color: #0175C2;
236 animation: indeterminate_first 2.0s infinite ease-out;
237 }
238
239 .indeterminate:after {
240 content: '';
241 position: absolute;
242 height: 100%;
243 background-color: #02569B;
244 animation: indeterminate_second 2.0s infinite ease-in;
245 }
246
247 @keyframes indeterminate_first {
248 0% {
249 left: -100%;
250 width: 100%;
251 }
252 100% {
253 left: 100%;
254 width: 10%;
255 }
256 }
257
258 @keyframes indeterminate_second {
259 0% {
260 left: -150%;
261 width: 100%;
262 }
263 100% {
264 left: 100%;
265 width: 10%;
266 }
267 }
268`;
269
270var styleSheet = document.createElement("style")
271styleSheet.type = "text/css";
272styleSheet.innerText = styles;
273document.head.appendChild(styleSheet);
274
275var loader = document.createElement('div');
276loader.className = "flutter-loader";
277document.body.append(loader);
278
279var indeterminate = document.createElement('div');
280indeterminate.className = "indeterminate";
281loader.appendChild(indeterminate);
282
283document.addEventListener('dart-app-ready', function (e) {
284 loader.parentNode.removeChild(loader);
285 styleSheet.parentNode.removeChild(styleSheet);
286});
287''';
288}
289
290String generateDDCMainModule({
291 required String entrypoint,
292 required bool nullAssertions,
293 required bool nativeNullAssertions,
294 String? exportedMain,
295}) {
296 final String entrypointMainName = exportedMain ?? entrypoint.split('.')[0];
297 // The typo below in "EXTENTION" is load-bearing, package:build depends on it.
298 return '''
299/* ENTRYPOINT_EXTENTION_MARKER */
300
301(function() {
302 // Flutter Web uses a generated main entrypoint, which shares app and module names.
303 let appName = "$entrypoint";
304 let moduleName = "$entrypoint";
305
306 // Use a dummy UUID since multi-apps are not supported on Flutter Web.
307 let uuid = "00000000-0000-0000-0000-000000000000";
308
309 let child = {};
310 child.main = function() {
311 let dart = self.dart_library.import('dart_sdk', appName).dart;
312 dart.nonNullAsserts($nullAssertions);
313 dart.nativeNonNullAsserts($nativeNullAssertions);
314 self.dart_library.start(appName, uuid, moduleName, "$entrypointMainName");
315 }
316
317 /* MAIN_EXTENSION_MARKER */
318 child.main();
319})();
320''';
321}
322
323/// Generate a synthetic main module which captures the application's main
324/// method.
325///
326/// If a [bootstrapModule] name is not provided, defaults to 'main_module.bootstrap'.
327///
328/// RE: Object.keys usage in app.main:
329/// This attaches the main entrypoint and hot reload functionality to the window.
330/// The app module will have a single property which contains the actual application
331/// code. The property name is based off of the entrypoint that is generated, for example
332/// the file `foo/bar/baz.dart` will generate a property named approximately
333/// `foo__bar__baz`. Rather than attempt to guess, we assume the first property of
334/// this object is the module.
335String generateMainModule({
336 required String entrypoint,
337 required bool nullAssertions,
338 required bool nativeNullAssertions,
339 String bootstrapModule = 'main_module.bootstrap',
340}) {
341 // The typo below in "EXTENTION" is load-bearing, package:build depends on it.
342 return '''
343/* ENTRYPOINT_EXTENTION_MARKER */
344// Disable require module timeout
345require.config({
346 waitSeconds: 0
347});
348// Create the main module loaded below.
349define("$bootstrapModule", ["$entrypoint", "dart_sdk"], function(app, dart_sdk) {
350 dart_sdk.dart.setStartAsyncSynchronously(true);
351 dart_sdk._debugger.registerDevtoolsFormatter();
352 dart_sdk.dart.nonNullAsserts($nullAssertions);
353 dart_sdk.dart.nativeNonNullAsserts($nativeNullAssertions);
354
355 // See the generateMainModule doc comment.
356 var child = {};
357 child.main = app[Object.keys(app)[0]].main;
358
359 /* MAIN_EXTENSION_MARKER */
360 child.main();
361
362 window.\$dartLoader = {};
363 window.\$dartLoader.rootDirectories = [];
364 if (window.\$requireLoader) {
365 window.\$requireLoader.getModuleLibraries = dart_sdk.dart.getModuleLibraries;
366 }
367 if (window.\$dartStackTraceUtility && !window.\$dartStackTraceUtility.ready) {
368 window.\$dartStackTraceUtility.ready = true;
369 let dart = dart_sdk.dart;
370 window.\$dartStackTraceUtility.setSourceMapProvider(function(url) {
371 var baseUrl = window.location.protocol + '//' + window.location.host;
372 url = url.replace(baseUrl + '/', '');
373 if (url == 'dart_sdk.js') {
374 return dart.getSourceMap('dart_sdk');
375 }
376 url = url.replace(".lib.js", "");
377 return dart.getSourceMap(url);
378 });
379 }
380 // Prevent DDC's requireJS to interfere with modern bundling.
381 if (typeof define === 'function' && define.amd) {
382 // Preserve a copy just in case...
383 define._amd = define.amd;
384 delete define.amd;
385 }
386});
387''';
388}
389
390typedef WebTestInfo = ({
391 String entryPoint,
392 Uri goldensUri,
393 String? configFile,
394});
395
396/// Generates the bootstrap logic required for running a group of unit test
397/// files in the browser.
398///
399/// This creates one "switchboard" main function that imports all the main
400/// functions of the unit test files that need to be run. The javascript code
401/// that starts the test sets a `window.testSelector` that specifies which main
402/// function to invoke. This allows us to compile all the unit test files as a
403/// single web application and invoke that with a different selector for each
404/// test.
405String generateTestEntrypoint({
406 required List<WebTestInfo> testInfos,
407 required LanguageVersion languageVersion,
408}) {
409 final List<String> importMainStatements = <String>[];
410 final List<String> importTestConfigStatements = <String>[];
411 final List<String> webTestPairs = <String>[];
412
413 for (int index = 0; index < testInfos.length; index++) {
414 final WebTestInfo testInfo = testInfos[index];
415 final String entryPointPath = testInfo.entryPoint;
416 importMainStatements.add("import 'org-dartlang-app:///${Uri.file(entryPointPath)}' as test_$index show main;");
417
418 final String? testConfigPath = testInfo.configFile;
419 String? testConfigFunction = 'null';
420 if (testConfigPath != null) {
421 importTestConfigStatements.add(
422 "import 'org-dartlang-app:///${Uri.file(testConfigPath)}' as test_config_$index show testExecutable;"
423 );
424 testConfigFunction = 'test_config_$index.testExecutable';
425 }
426 webTestPairs.add('''
427 '$entryPointPath': (
428 entryPoint: test_$index.main,
429 entryPointRunner: $testConfigFunction,
430 goldensUri: Uri.parse('${testInfo.goldensUri}'),
431 ),
432''');
433 }
434 return '''
435// @dart = ${languageVersion.major}.${languageVersion.minor}
436
437${importMainStatements.join('\n')}
438
439${importTestConfigStatements.join('\n')}
440
441import 'package:flutter_test/flutter_test.dart';
442
443Map<String, WebTest> webTestMap = <String, WebTest>{
444 ${webTestPairs.join('\n')}
445};
446
447Future<void> main() {
448 final WebTest? webTest = webTestMap[testSelector];
449 if (webTest == null) {
450 throw Exception('Web test for \${testSelector} not found');
451 }
452 return runWebTest(webTest);
453}
454 ''';
455}
456
457/// Generate the unit test bootstrap file.
458String generateTestBootstrapFileContents(
459 String mainUri, String requireUrl, String mapperUrl) {
460 return '''
461(function() {
462 if (typeof document != 'undefined') {
463 var el = document.createElement("script");
464 el.defer = true;
465 el.async = false;
466 el.src = '$mapperUrl';
467 document.head.appendChild(el);
468
469 el = document.createElement("script");
470 el.defer = true;
471 el.async = false;
472 el.src = '$requireUrl';
473 el.setAttribute("data-main", '$mainUri');
474 document.head.appendChild(el);
475 } else {
476 importScripts('$mapperUrl', '$requireUrl');
477 require.config({
478 baseUrl: baseUrl,
479 });
480 window = self;
481 require(['$mainUri']);
482 }
483})();
484''';
485}
486
487String generateDefaultFlutterBootstrapScript() {
488 return '''
489{{flutter_js}}
490{{flutter_build_config}}
491
492_flutter.loader.load({
493 serviceWorkerSettings: {
494 serviceWorkerVersion: {{flutter_service_worker_version}}
495 }
496});
497''';
498}
499

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com