David Iglesias 34b42acf1b
[web] Hides that Flutter uses requireJS in debug. (#129032)
Flutter web uses requireJS in `debug` mode to assemble a DDC-compiled app from a bunch of small files ("modules").

This caused that `canvaskit.js` (and all other modules that used a browserify-like loading header) didn't work because they attempted to use the `define` function provided by Flutter's instance of `requireJS` (which kept the defined modules private, rather than as globals on the page, as the users of the JS expected).

A [fix](https://github.com/flutter/engine/pull/27342) was added to `flutter/engine` to trick loaders into *not* using the `requireJS` module loader, but a recent change in the fix's js-interop layer *subtly* changed its JS output on the page (objects went from `undefined` to `null`), causing this:

* https://github.com/flutter/flutter/issues/126131 (and others)

This PR hides a bit of code that is commonly used by module loaders to decide that they may use the `define` function provided by requireJS (so the engine workaround can be removed).

## Next steps

* https://github.com/flutter/engine/pull/42941

## Issues

Partially addresses: https://github.com/flutter/flutter/issues/126131 (and others)

## Tests

* Added a unit test to ensure the `delete` stays
* Manually tested with the Gallery app in `debug` mode with a bunch of user-supplied scripts that currently fail to load.
  * Also tested hot restart as suggested by @nshahan
2023-06-23 02:09:12 +00:00

302 lines
9.1 KiB
Dart

// 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.
String generateBootstrapScript({
required String requireUrl,
required String mapperUrl,
}) {
return '''
"use strict";
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);
});
// 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);
''';
}
/// 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: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.debugEmulateFlutterTesterEnvironment = true;
await ui.webOnlyInitializePlatform();
webGoldenComparator = DefaultWebGoldenComparator(Uri.parse('${Uri.file(absolutePath)}'));
(ui.window as dynamic).debugOverrideDevicePixelRatio(3.0);
(ui.window as dynamic).webOnlyDebugPhysicalSizeOverride = 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(sync: true);
window.onMessage.firstWhere((message) {
return message.origin == window.location.origin && message.data == "port";
}).then((message) {
var port = message.ports.first;
var portSubscription = port.onMessage.listen((message) {
controller.local.sink.add(message.data);
});
controller.local.stream.listen((data) {
port.postMessage({"data": data});
}, onDone: () {
port.postMessage({"event": "done"});
portSubscription.cancel();
});
});
context['parent'].callMethod('postMessage', [
JsObject.jsify({"href": window.location.href, "ready": true}),
window.location.origin,
]);
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']);
}
})();
''';
}