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 | String 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. |
18 | var _currentDirectory = " $appRootDirectory"; |
19 | |
20 | window.\$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. |
43 | var 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. |
61 | let _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. |
149 | String 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. |
160 | let _scriptUrls = { |
161 | "mapper": " $mapperUrl", |
162 | "requireJs": " $requireUrl" |
163 | }; |
164 | |
165 | // Create a TrustedTypes policy so we can attach Scripts... |
166 | let _ttPolicy; |
167 | if (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. |
181 | function getTTScriptUrl(scriptName) { |
182 | let defaultUrl = _scriptUrls[scriptName]; |
183 | return _ttPolicy ? _ttPolicy.createScriptURL(scriptName) : defaultUrl; |
184 | } |
185 | |
186 | // Attach source mapping. |
187 | var mapperEl = document.createElement("script"); |
188 | mapperEl.defer = true; |
189 | mapperEl.async = false; |
190 | mapperEl.src = getTTScriptUrl("mapper"); |
191 | document.head.appendChild(mapperEl); |
192 | |
193 | // Attach require JS. |
194 | var requireEl = document.createElement("script"); |
195 | requireEl.defer = true; |
196 | requireEl.async = false; |
197 | requireEl.src = getTTScriptUrl("requireJs"); |
198 | // This attribute tells require JS what to load as main (defined below). |
199 | requireEl.setAttribute("data-main", "main_module.bootstrap"); |
200 | document.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. |
212 | String _generateLoadingIndicator() { |
213 | return ''' |
214 | var 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 | |
270 | var styleSheet = document.createElement("style") |
271 | styleSheet.type = "text/css"; |
272 | styleSheet.innerText = styles; |
273 | document.head.appendChild(styleSheet); |
274 | |
275 | var loader = document.createElement('div'); |
276 | loader.className = "flutter-loader"; |
277 | document.body.append(loader); |
278 | |
279 | var indeterminate = document.createElement('div'); |
280 | indeterminate.className = "indeterminate"; |
281 | loader.appendChild(indeterminate); |
282 | |
283 | document.addEventListener('dart-app-ready', function (e) { |
284 | loader.parentNode.removeChild(loader); |
285 | styleSheet.parentNode.removeChild(styleSheet); |
286 | }); |
287 | ''' ; |
288 | } |
289 | |
290 | String 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. |
335 | String 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 |
345 | require.config({ |
346 | waitSeconds: 0 |
347 | }); |
348 | // Create the main module loaded below. |
349 | define(" $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 | |
390 | typedef 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. |
405 | String 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 | |
441 | import 'package:flutter_test/flutter_test.dart'; |
442 | |
443 | Map<String, WebTest> webTestMap = <String, WebTest>{ |
444 | ${webTestPairs.join('\n')} |
445 | }; |
446 | |
447 | Future<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. |
458 | String 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 | |
487 | String 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 | |