blob: b13adb7c659874c4931bd87982e4ca7108ba9d8d [file] [log] [blame]
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:convert';
import 'dart:ffi';
import 'dart:io';
/// Maps a generation to its modified libraries' ID and JS URL.
///
/// Example:
/// {
/// 0: [['main', '/some/path/main.0.js']],
/// 1: [['main', '/some/path/main.1.js'], ['lib', '/some/path/lib.1.js']]
/// }
typedef FileDataPerGeneration = Map<String, List<List<String>>>;
final _encoder = JsonEncoder.withIndent(' ');
class D8Configuration {
final Uri sdkRoot;
final Uri binary;
final Uri preamblesScript;
final Uri sealNativeObjectScript;
D8Configuration._(this.sdkRoot, this.binary, this.preamblesScript,
this.sealNativeObjectScript);
factory D8Configuration(Uri sdkRoot) {
final preamblesScript = sdkRoot
.resolve('sdk/lib/_internal/js_dev_runtime/private/preambles/d8.js');
final sealNativeObjectScript = sdkRoot.resolve(
'sdk/lib/_internal/js_runtime/lib/preambles/seal_native_object.js');
final arch = Abi.current().toString().split('_')[1];
final Uri binaryFromRoot;
if (Platform.isWindows) {
binaryFromRoot = Uri.file('third_party/d8/windows/$arch/d8.exe');
} else if (Platform.isLinux) {
binaryFromRoot = Uri.file('third_party/d8/linux/$arch/d8');
} else if (Platform.isMacOS) {
binaryFromRoot = Uri.file('third_party/d8/macos/$arch/d8');
} else {
throw UnsupportedError('Unsupported platform for running d8: '
'${Platform.operatingSystem}');
}
final binary = sdkRoot.resolveUri(binaryFromRoot);
return D8Configuration._(
sdkRoot, binary, preamblesScript, sealNativeObjectScript);
}
}
/// Generates the JS bootstrapper for DDC with the DDC module system
/// (executed on D8).
///
/// `scriptDescriptors` maps module IDs to their JS script paths.
/// It has the form:
/// [
/// {
/// "id": "some__module__id.dart"
/// "src": "/path/to/file.js"
/// },
/// ...
/// ]
///
/// `modifiedFilesPerGeneration` maps generation ids to JS files modified in
/// that generation. It has the form:
/// {
/// "0": ["/path/to/file.js", "/path/to/file2.js", ...],
/// "1": ...
/// }
///
/// Note: All JS paths above are relative to `jsFileRoot`.
String generateD8Bootstrapper({
required String ddcModuleLoaderJsPath,
required String dartSdkJsPath,
required String entrypointModuleName,
String jsFileRoot = '',
String uuid = '00000000-0000-0000-0000-000000000000',
required String entrypointLibraryExportName,
required List<Map<String, String?>> scriptDescriptors,
required FileDataPerGeneration modifiedFilesPerGeneration,
}) {
final d8BootstrapJS = '''
load("$ddcModuleLoaderJsPath");
load("$dartSdkJsPath");
var prerequisiteScripts = [
{
"id": "ddc_module_loader \\0",
"src": "$ddcModuleLoaderJsPath"
},
{
"id": "dart_sdk \\0",
"src": "$dartSdkJsPath"
}
];
let scripts = ${_encoder.convert(scriptDescriptors)};
let loadConfig = new self.\$dartLoader.LoadConfiguration();
loadConfig.isWindows = ${Platform.isWindows};
loadConfig.root = '$jsFileRoot';
// Loading the entrypoint late is only necessary in Chrome.
loadConfig.bootstrapScript = '';
loadConfig.loadScriptFn = function(loader) {
loader.addScriptsToQueue(scripts, null);
loader.loadEnqueuedModulesForD8();
}
loadConfig.ddcEventForLoadStart = /* LOAD_ALL_MODULES_START */ 1;
loadConfig.ddcEventForLoadedOk = /* LOAD_ALL_MODULES_END_OK */ 2;
loadConfig.ddcEventForLoadedError = /* LOAD_ALL_MODULES_END_ERROR */ 3;
let loader = new self.\$dartLoader.DDCLoader(loadConfig);
// Record prerequisite scripts' fully resolved URLs.
prerequisiteScripts.forEach(script => loader.registerScript(script));
// Note: these variables should only be used in non-multi-app scenarios since
// they can be arbitrarily overridden based on multi-app load order.
self.\$dartLoader.loadConfig = loadConfig;
self.\$dartLoader.loader = loader;
// Append hot reload runner-specific logic.
let modifiedFilesPerGeneration = ${_encoder.convert(modifiedFilesPerGeneration)};
let previousGenerations = new Set();
self.\$dartReloadModifiedModules = function(subAppName, callback) {
let expectedName = "$entrypointModuleName";
if (subAppName !== expectedName) {
throw Error("Unexpected app name " + subAppName
+ " (expected: " + expectedName + "). "
+ "Hot Reload Runner does not support multiple subapps, so only "
+ "one app name should be provided across reloads/restarts.");
}
// Resolve the next generation's directory and load all modified files.
let nextGeneration = self.\$dartLoader.loader.intendedHotRestartGeneration;
if (previousGenerations.has(nextGeneration)) {
throw Error('Fatal error: Previous generations are being re-run.');
}
previousGenerations.add(nextGeneration);
let modifiedFilePaths = modifiedFilesPerGeneration[nextGeneration];
// Stop if the next generation does not exist.
if (modifiedFilePaths == void 0) {
return;
}
// Load all modified files.
for (let i = 0; i < modifiedFilePaths.length; i++) {
let modifiedFilePath = modifiedFilePaths[i][1];
self.\$dartLoader.forceLoadScript(modifiedFilePath);
}
// Run main.
callback();
}
// D8 does not support the core Timer API methods beside `setTimeout` so our
// D8 preambles provide a custom implementation.
//
// Timers in this implementation are simulated, so they all complete before
// native JS `await` boundaries. If this boundary occurs before our runtime's
// `hotRestartIteration` counter increments, we can observe Futures not being
// cancelled in D8 when they might otherwise have been in Chrome.
//
// To resolve this, we record and increment hot restart generations early
// and wrap timer functions with custom cancellation logic.
self.setTimeout = function(setTimeout) {
let currentHotRestartIteration =
self.\$dartLoader.loader.intendedHotRestartGeneration;
return function(f, ms) {
var internalCallback = function() {
if (currentHotRestartIteration ==
self.\$dartLoader.loader.intendedHotRestartGeneration) {
f();
}
}
setTimeout(internalCallback, ms);
};
}(self.setTimeout);
// DDC also has a runtime implementation of microtasks' `scheduleImmediate`
// that more closely matches Chrome's behavior. We enable this implementation
// by deleting the our custom implementation in D8's preamble.
self.scheduleImmediate = void 0;
// Begin loading libraries
loader.nextAttempt();
// Invoke main through the d8 preamble to ensure the code is running
// within the fake event loop.
self.dartMainRunner(function () {
dartDevEmbedder.runMain("$entrypointModuleName", {});
});
''';
return d8BootstrapJS;
}
/// Generates the two files required to bootstrap a chrome app: a main
/// entrypoint file and an app bootstrapper - respectively.
(String, String) generateChromeBootstrapperFiles({
required String ddcModuleLoaderJsPath,
required String dartSdkJsPath,
required String entrypointModuleName,
required String mainModuleEntrypointJsPath,
String jsFileRoot = '',
String uuid = '00000000-0000-0000-0000-000000000000',
required String entrypointLibraryExportName,
required List<Map<String, String?>> scriptDescriptors,
required FileDataPerGeneration modifiedFilesPerGeneration,
}) {
var mainModuleText = generateChromeMainEntrypoint(
entrypointModuleName: entrypointModuleName,
entrypointLibraryExportName: entrypointLibraryExportName,
);
var chromeBootstrapper = generateChromeBootstrapper(
ddcModuleLoaderJsPath: ddcModuleLoaderJsPath,
dartSdkJsPath: dartSdkJsPath,
entrypointModuleName: entrypointModuleName,
mainModuleEntrypointJsPath: mainModuleEntrypointJsPath,
jsFileRoot: jsFileRoot,
uuid: uuid,
scriptDescriptors: scriptDescriptors,
modifiedFilesPerGeneration: modifiedFilesPerGeneration,
);
return (mainModuleText, chromeBootstrapper);
}
/// Generates a bootstrap entrypoint script for calling 'main'.
///
/// Only used for the DDC module system when run in Chrome.
String generateChromeMainEntrypoint({
required String entrypointModuleName,
bool nullAssertions = true,
bool nativeNullAssertions = true,
String uuid = '00000000-0000-0000-0000-000000000000',
required String entrypointLibraryExportName,
}) {
return '''
(function() {
// Flutter Web uses a generated main entrypoint, which shares app and module names.
// We adopt their convention in this framework to make comparisons more direct.
let appName = "$entrypointModuleName";
let moduleName = "$entrypointModuleName";
// Multi-apps are not supported in this framework, so uuids are irrelevant.
let uuid = "$uuid";
let child = {};
child.main = function() {
dartDevEmbedder.runMain("$entrypointModuleName", {});
}
child.main();
})();
''';
}
/// Generates the JS bootstrapper for DDC with the DDC module system
/// (executed on Chrome).
///
/// `scriptDescriptors` maps module IDs to their JS script paths.
/// It has the form:
/// [
/// {
/// "id": "some__module__id.dart"
/// "src": "/path/to/file.js"
/// },
/// ...
/// ]
///
/// `modifiedFilesPerGeneration` maps generation ids to JS files modified in
/// that generation. It has the form:
/// {
/// "0": ["/path/to/file.js", "/path/to/file2.js", ...],
/// "1": ...
/// }
///
/// Note: All JS paths above are relative to `jsFileRoot`.
String generateChromeBootstrapper({
required String ddcModuleLoaderJsPath,
required String dartSdkJsPath,
required String entrypointModuleName,
required String mainModuleEntrypointJsPath,
String jsFileRoot = '',
String uuid = '00000000-0000-0000-0000-000000000000',
required List<Map<String, String?>> scriptDescriptors,
required FileDataPerGeneration modifiedFilesPerGeneration,
}) {
final bootstrapJS = '''
var _currentDirectory = "$jsFileRoot";
window.\$dartCreateScript = (function() {
// Find the nonce value. (Note, this is only computed once.)
var scripts = Array.from(document.getElementsByTagName("script"));
var nonce;
scripts.some(
script => (nonce = script.nonce || script.getAttribute("nonce")));
// If present, return a closure that automatically appends the nonce.
if (nonce) {
return function() {
var script = document.createElement("script");
script.nonce = nonce;
return script;
};
} else {
return function() {
return document.createElement("script");
};
}
})();
// Creates and loads a script during hot restart.
var loadHotRestartScript = function(id, src, callback) {
var script = self.\$dartCreateScript();
let policy = {
createScriptURL: function(src) {return src;}
};
if (self.trustedTypes && self.trustedTypes.createPolicy) {
policy = self.trustedTypes.createPolicy('dartDdcModuleUrl', policy);
}
script.src = policy.createScriptURL(src);
script.async = false
script.defer = true;
script.id = id;
script.onload = callback;
document.head.appendChild(script);
}
// Loads a module [relativeUrl] relative to [root].
//
// If not specified, [root] defaults to the directory serving the main app.
//
// Used for appending pre-requisite modules to the page.
var forceLoadModule = function (relativeUrl, root) {
var actualRoot = root ?? _currentDirectory;
return new Promise(function(resolve, reject) {
var script = self.\$dartCreateScript();
let policy = {
createScriptURL: function(src) {return src;}
};
if (self.trustedTypes && self.trustedTypes.createPolicy) {
policy = self.trustedTypes.createPolicy('dartDdcModuleUrl', policy);
}
script.onload = resolve;
script.onerror = reject;
script.src = policy.createScriptURL(actualRoot + relativeUrl);
document.head.appendChild(script);
});
};
// A map containing the URLs for the bootstrap scripts in debug.
let _scriptUrls = {
"moduleLoader": "$ddcModuleLoaderJsPath"
};
(function() {
let appName = "$entrypointModuleName";
// A uuid that identifies a subapp. Unused for this framework.
let uuid = $uuid;
// Load pre-requisite DDC scripts.
// We intentionally use invalid names to avoid namespace clashes.
let prerequisiteScripts = [
{
"src": "$ddcModuleLoaderJsPath",
"id": "ddc_module_loader \x00"
}
];
// Load ddc_module_loader.js to access DDC's module loader API.
let prerequisiteLoads = [];
for (let i = 0; i < prerequisiteScripts.length; i++) {
prerequisiteLoads.push(forceLoadModule(prerequisiteScripts[i].src));
}
Promise.all(prerequisiteLoads).then((_) => afterPrerequisiteLogic());
// Save the current script so we can access it in a closure.
var _currentScript = document.currentScript;
var afterPrerequisiteLogic = function() {
window.\$dartLoader.rootDirectories.push(_currentDirectory);
let scripts = [
{
"src": "$dartSdkJsPath",
"id": "dart_sdk \\0"
},
].concat(${_encoder.convert(scriptDescriptors)});
let loadConfig = new window.\$dartLoader.LoadConfiguration();
loadConfig.isWindows = ${Platform.isWindows};
loadConfig.root = '$jsFileRoot';
loadConfig.bootstrapScript = {
"src": "$mainModuleEntrypointJsPath",
"id": "data-main"
};
scripts.push(loadConfig.bootstrapScript);
loadConfig.loadScriptFn = function(loader) {
loader.addScriptsToQueue(scripts, null);
loader.loadEnqueuedModules();
}
loadConfig.ddcEventForLoadStart = /* LOAD_ALL_MODULES_START */ 1;
loadConfig.ddcEventForLoadedOk = /* LOAD_ALL_MODULES_END_OK */ 2;
loadConfig.ddcEventForLoadedError = /* LOAD_ALL_MODULES_END_ERROR */ 3;
let loader = new window.\$dartLoader.DDCLoader(loadConfig);
// Record prerequisite scripts' fully resolved URLs.
prerequisiteScripts.forEach(script => loader.registerScript(script));
// Note: these variables should only be used in non-multi-app scenarios since
// they can be arbitrarily overridden based on multi-app load order.
window.\$dartLoader.loadConfig = loadConfig;
window.\$dartLoader.loader = loader;
// Append hot reload runner-specific logic.
let modifiedFilesPerGeneration = ${_encoder.convert(modifiedFilesPerGeneration)};
let previousGenerations = new Set();
self.\$dartReloadModifiedModules = function(subAppName, callback) {
let expectedName = "$entrypointModuleName";
if (subAppName !== expectedName) {
throw Error("Unexpected app name " + subAppName
+ " (expected: " + expectedName + "). "
+ "Hot Reload Runner does not support multiple subapps, so only "
+ "one app name should be provided across reloads/restarts.");
}
// Resolve the next generation's directory and load all modified files.
let nextGeneration = self.\$dartLoader.loader.intendedHotRestartGeneration;
if (previousGenerations.has(nextGeneration)) {
throw Error('Fatal error: Previous generations are being re-run.');
}
previousGenerations.add(nextGeneration);
let modifiedFilePaths = modifiedFilesPerGeneration[nextGeneration];
// Stop if the next generation does not exist.
if (modifiedFilePaths == void 0) {
return;
}
// Load all modified files.
var numToLoad = 0;
var numLoaded = 0;
for (let i = 0; i < modifiedFilePaths.length; i++) {
numToLoad++
let modifiedFileId = modifiedFilePaths[i][0];
let modifiedFilePath = modifiedFilePaths[i][1];
// Invalidate DDC state for hot restart.
self.\$dartLoader.moduleIdToUrl.set(modifiedFileId, modifiedFilePath);
self.\$dartLoader.urlToModuleId.set(modifiedFilePath, modifiedFileId);
// Remove the old script.
var el = document.getElementById(modifiedFileId);
if (el) el.remove();
loadHotRestartScript(modifiedFileId, modifiedFilePath, function() {
numLoaded++;
if (numToLoad == numLoaded) callback();
});
}
// Call the callback immediately if we found no updated scripts.
if (numToLoad == 0) callback();
}
// Begin loading libraries
loader.nextAttempt();
}
})();
''';
return bootstrapJS;
}
class ChromeConfiguration {
final Uri sdkRoot;
final Uri binary;
ChromeConfiguration._(this.sdkRoot, this.binary);
factory ChromeConfiguration(Uri sdkRoot) {
String chromeBinary;
if (Platform.isWindows) {
chromeBinary =
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe';
} else if (Platform.isMacOS) {
chromeBinary =
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
} else {
// Assume Linux
chromeBinary = 'google-chrome';
}
final binary = Uri.file(chromeBinary);
return ChromeConfiguration._(sdkRoot, binary);
}
}