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 | |
5 | import 'package:package_config/package_config.dart' ; |
6 | |
7 | /// Used to load prerequisite scripts such as ddc_module_loader.js |
8 | const _simpleLoaderScript = r''' |
9 | window.$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. |
32 | var 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. |
51 | String 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. |
62 | var _currentDirectory = " $appRootDirectory"; |
63 | |
64 | $_simpleLoaderScript |
65 | |
66 | // A map containing the URLs for the bootstrap scripts in debug. |
67 | let _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 | |
143 | String 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. |
153 | var _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. |
319 | String 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. |
330 | let _scriptUrls = { |
331 | "mapper": " $mapperUrl", |
332 | "requireJs": " $requireUrl" |
333 | }; |
334 | |
335 | // Create a TrustedTypes policy so we can attach Scripts... |
336 | let _ttPolicy; |
337 | if (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. |
351 | function getTTScriptUrl(scriptName) { |
352 | let defaultUrl = _scriptUrls[scriptName]; |
353 | return _ttPolicy ? _ttPolicy.createScriptURL(scriptName) : defaultUrl; |
354 | } |
355 | |
356 | // Attach source mapping. |
357 | var mapperEl = document.createElement("script"); |
358 | mapperEl.defer = true; |
359 | mapperEl.async = false; |
360 | mapperEl.src = getTTScriptUrl("mapper"); |
361 | document.head.appendChild(mapperEl); |
362 | |
363 | // Attach require JS. |
364 | var requireEl = document.createElement("script"); |
365 | requireEl.defer = true; |
366 | requireEl.async = false; |
367 | requireEl.src = getTTScriptUrl("requireJs"); |
368 | // This attribute tells require JS what to load as main (defined below). |
369 | requireEl.setAttribute("data-main", "main_module.bootstrap"); |
370 | document.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. |
382 | String _generateLoadingIndicator() { |
383 | return ''' |
384 | var 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 | |
440 | var styleSheet = document.createElement("style") |
441 | styleSheet.type = "text/css"; |
442 | styleSheet.innerText = styles; |
443 | document.head.appendChild(styleSheet); |
444 | |
445 | var loader = document.createElement('div'); |
446 | loader.className = "flutter-loader"; |
447 | document.body.append(loader); |
448 | |
449 | var indeterminate = document.createElement('div'); |
450 | indeterminate.className = "indeterminate"; |
451 | loader.appendChild(indeterminate); |
452 | |
453 | document.addEventListener('dart-app-ready', function (e) { |
454 | loader.parentNode.removeChild(loader); |
455 | styleSheet.parentNode.removeChild(styleSheet); |
456 | }); |
457 | ''' ; |
458 | } |
459 | |
460 | const _onLoadEndCallback = r'$onLoadEndCallback' ; |
461 | |
462 | String 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 | |
507 | String 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. |
523 | String 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 |
533 | require.config({ |
534 | waitSeconds: 0 |
535 | }); |
536 | // Create the main module loaded below. |
537 | define("$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 | |
577 | typedef 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. |
588 | String 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 | |
626 | import 'package:flutter_test/flutter_test.dart'; |
627 | |
628 | Map<String, WebTest> webTestMap = <String, WebTest>{ |
629 | ${webTestPairs.join('\n')} |
630 | }; |
631 | |
632 | Future<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. |
643 | String 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 | |
671 | String 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 | |