blob: 2845d458dfad1d7fb32735bdbf1ea0a8f5c07a53 [file] [log] [blame]
// Copyright (c) 2017, 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:async';
import 'dart:collection';
import 'dart:convert';
import 'package:build/build.dart';
import 'package:build_modules/build_modules.dart';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as _p; // ignore: library_prefixes
import 'package:pool/pool.dart';
import 'ddc_names.dart';
import 'dev_compiler_builder.dart';
import 'platforms.dart';
import 'web_entrypoint_builder.dart';
/// Alias `_p.url` to `p`.
_p.Context get _context => _p.url;
var _modulePartialExtension = _context.withoutExtension(jsModuleExtension);
/// Bootstraps a ddc application, creating the main entrypoint as well as the
/// bootstrap and digest entrypoints.
///
/// If [skipPlatformCheck] is `true` then all `dart:` imports will be
/// allowed in all packages.
///
/// Deprecated: If [skipPlatformCheckPackages] is provided then any dart:
/// imports will be allowed in the specified packages.
///
/// If [requiredAssets] is provided then this will ensure those assets are
/// available to the app by making them inputs of this build action.
Future<void> bootstrapDdc(
BuildStep buildStep, {
DartPlatform platform,
bool skipPlatformCheck = false,
@deprecated Set<String> skipPlatformCheckPackages = const {},
Iterable<AssetId> requiredAssets,
}) async {
requiredAssets ??= [];
skipPlatformCheck ??= false;
// Ensures that the sdk resources are built and available.
await _ensureResources(buildStep, requiredAssets);
var dartEntrypointId = buildStep.inputId;
var moduleId = buildStep.inputId
.changeExtension(moduleExtension(platform ?? ddcPlatform));
var module = Module.fromJson(json
.decode(await buildStep.readAsString(moduleId)) as Map<String, dynamic>);
// First, ensure all transitive modules are built.
List<AssetId> transitiveJsModules;
try {
transitiveJsModules = await _ensureTransitiveJsModules(module, buildStep,
skipPlatformCheck: skipPlatformCheck,
skipPlatformCheckPackages: skipPlatformCheckPackages);
} on UnsupportedModules catch (e) {
var librariesString = (await e.exactLibraries(buildStep).toList())
.map((lib) => AssetId(lib.id.package,
lib.id.path.replaceFirst(moduleLibraryExtension, '.dart')))
.join('\n');
log.warning('''
Skipping compiling ${buildStep.inputId} with ddc because some of its
transitive libraries have sdk dependencies that not supported on this platform:
$librariesString
https://github.com/dart-lang/build/blob/master/docs/faq.md#how-can-i-resolve-skipped-compiling-warnings
''');
return;
}
var jsId = module.primarySource.changeExtension(jsModuleExtension);
var appModuleName = ddcModuleName(jsId);
var appDigestsOutput =
dartEntrypointId.changeExtension(digestsEntrypointExtension);
// The name of the entrypoint dart library within the entrypoint JS module.
//
// This is used to invoke `main()` from within the bootstrap script.
//
// TODO(jakemac53): Sane module name creation, this only works in the most
// basic of cases.
//
// See https://github.com/dart-lang/sdk/issues/27262 for the root issue
// which will allow us to not rely on the naming schemes that dartdevc uses
// internally, but instead specify our own.
var oldAppModuleScope = toJSIdentifier(
_context.withoutExtension(_context.basename(buildStep.inputId.path)));
// Like above but with a package-relative entrypoint.
var appModuleScope =
pathToJSIdentifier(_context.withoutExtension(buildStep.inputId.path));
// Map from module name to module path for custom modules.
var modulePaths = SplayTreeMap.of(
{'dart_sdk': r'packages/build_web_compilers/src/dev_compiler/dart_sdk'});
for (var jsId in transitiveJsModules) {
// Strip out the top level dir from the path for any module, and set it to
// `packages/` for lib modules. We set baseUrl to `/` to simplify things,
// and we only allow you to serve top level directories.
var moduleName = ddcModuleName(jsId);
modulePaths[moduleName] = _context.withoutExtension(
jsId.path.startsWith('lib')
? '$moduleName$jsModuleExtension'
: _context.joinAll(_context.split(jsId.path).skip(1)));
}
var bootstrapId = dartEntrypointId.changeExtension(ddcBootstrapExtension);
var bootstrapModuleName = _context.withoutExtension(_context.relative(
bootstrapId.path,
from: _context.dirname(dartEntrypointId.path)));
var dartEntrypointParts = _context.split(dartEntrypointId.path);
var entrypointLibraryName = _context.joinAll([
// Convert to a package: uri for files under lib.
if (dartEntrypointParts.first == 'lib')
'package:${module.primarySource.package}',
// Strip top-level directory from the path.
...dartEntrypointParts.skip(1),
]);
var bootstrapContent =
StringBuffer('$_entrypointExtensionMarker\n(function() {\n')
..write(_dartLoaderSetup(
modulePaths,
_p.url.relative(appDigestsOutput.path,
from: _p.url.dirname(bootstrapId.path))))
..write(_requireJsConfig)
..write(_appBootstrap(bootstrapModuleName, appModuleName,
appModuleScope, entrypointLibraryName,
oldModuleScope: oldAppModuleScope));
await buildStep.writeAsString(bootstrapId, bootstrapContent.toString());
var entrypointJsContent = _entryPointJs(bootstrapModuleName);
await buildStep.writeAsString(
dartEntrypointId.changeExtension(jsEntrypointExtension),
entrypointJsContent);
// Output the digests for transitive modules.
// These can be consumed for hot reloads.
var moduleDigests = <String, String>{
for (var jsId in transitiveJsModules)
_moduleDigestKey(jsId): '${await buildStep.digest(jsId)}',
};
await buildStep.writeAsString(appDigestsOutput, jsonEncode(moduleDigests));
}
String _moduleDigestKey(AssetId jsId) =>
'${ddcModuleName(jsId)}$jsModuleExtension';
final _lazyBuildPool = Pool(16);
/// Ensures that all transitive js modules for [module] are available and built.
///
/// Throws an [UnsupportedModules] exception if there are any
/// unsupported modules.
Future<List<AssetId>> _ensureTransitiveJsModules(
Module module, BuildStep buildStep,
{@required bool skipPlatformCheck,
@required Set<String> skipPlatformCheckPackages}) async {
// Collect all the modules this module depends on, plus this module.
var transitiveDeps = await module.computeTransitiveDependencies(buildStep,
throwIfUnsupported: !skipPlatformCheck,
// ignore: deprecated_member_use
skipPlatformCheckPackages: skipPlatformCheckPackages);
var jsModules = [
module.primarySource.changeExtension(jsModuleExtension),
for (var dep in transitiveDeps)
dep.primarySource.changeExtension(jsModuleExtension),
];
// Check that each module is readable, and warn otherwise.
await Future.wait(jsModules.map((jsId) async {
if (await _lazyBuildPool.withResource(() => buildStep.canRead(jsId))) {
return;
}
var errorsId = jsId.addExtension('.errors');
await buildStep.canRead(errorsId);
log.warning('Unable to read $jsId, check your console or the '
'`.dart_tool/build/generated/${errorsId.package}/${errorsId.path}` '
'log file.');
}));
return jsModules;
}
/// Code that actually imports the [moduleName] module, and calls the
/// `[moduleScope].main()` function on it.
///
/// Also performs other necessary initialization.
String _appBootstrap(String bootstrapModuleName, String moduleName,
String moduleScope, String entrypointLibraryName,
{String oldModuleScope}) =>
'''
define("$bootstrapModuleName", ["$moduleName", "dart_sdk"], function(app, dart_sdk) {
dart_sdk.dart.setStartAsyncSynchronously(true);
dart_sdk._isolate_helper.startRootIsolate(() => {}, []);
$_initializeTools
$_mainExtensionMarker
(app.$moduleScope || app.$oldModuleScope).main();
var bootstrap = {
hot\$onChildUpdate: function(childName, child) {
// Special handling for the multi-root scheme uris. We need to strip
// out the scheme and the top level directory, to match the source path
// that chrome sees.
if (childName.startsWith('$multiRootScheme:///')) {
childName = childName.substring('$multiRootScheme:///'.length);
var firstSlash = childName.indexOf('/');
if (firstSlash == -1) return false;
childName = childName.substring(firstSlash + 1);
}
if (childName === "$entrypointLibraryName") {
// Clear static caches.
dart_sdk.dart.hotRestart();
child.main();
return true;
}
}
}
dart_sdk.dart.trackLibraries("$bootstrapModuleName", {
"$bootstrapModuleName": bootstrap
}, '');
return {
bootstrap: bootstrap
};
});
})();
''';
/// The actual entrypoint JS file which injects all the necessary scripts to
/// run the app.
String _entryPointJs(String bootstrapModuleName) => '''
(function() {
$_currentDirectoryScript
$_baseUrlScript
var mapperUri = baseUrl + "packages/build_web_compilers/src/" +
"dev_compiler_stack_trace/stack_trace_mapper.dart.js";
var requireUri = baseUrl +
"packages/build_web_compilers/src/dev_compiler/require.js";
var mainUri = _currentDirectory + "$bootstrapModuleName";
if (typeof document != 'undefined') {
var el = document.createElement("script");
el.defer = true;
el.async = false;
el.src = mapperUri;
document.head.appendChild(el);
el = document.createElement("script");
el.defer = true;
el.async = false;
el.src = requireUri;
el.setAttribute("data-main", mainUri);
document.head.appendChild(el);
} else {
importScripts(mapperUri, requireUri);
require.config({
baseUrl: baseUrl,
});
// TODO: update bootstrap code to take argument - dart-lang/build#1115
window = self;
require([mainUri + '.js']);
}
})();
''';
/// JavaScript snippet to determine the directory a script was run from.
final _currentDirectoryScript = r'''
var _currentDirectory = (function () {
var _url;
var lines = new Error().stack.split('\n');
function lookupUrl() {
if (lines.length > 2) {
var match = lines[1].match(/^\s+at (.+):\d+:\d+$/);
// Chrome.
if (match) return match[1];
// Chrome nested eval case.
match = lines[1].match(/^\s+at eval [(](.+):\d+:\d+[)]$/);
if (match) return match[1];
// Edge.
match = lines[1].match(/^\s+at.+\((.+):\d+:\d+\)$/);
if (match) return match[1];
// Firefox.
match = lines[0].match(/[<][@](.+):\d+:\d+$/)
if (match) return match[1];
}
// Safari.
return lines[0].match(/(.+):\d+:\d+$/)[1];
}
_url = lookupUrl();
var lastSlash = _url.lastIndexOf('/');
if (lastSlash == -1) return _url;
var currentDirectory = _url.substring(0, lastSlash + 1);
return currentDirectory;
})();
''';
/// Sets up `window.$dartLoader` based on [modulePaths].
String _dartLoaderSetup(Map<String, String> modulePaths, String appDigests) =>
'''
$_currentDirectoryScript
$_baseUrlScript
let modulePaths = ${const JsonEncoder.withIndent(" ").convert(modulePaths)};
if(!window.\$dartLoader) {
window.\$dartLoader = {
appDigests: _currentDirectory + '$appDigests',
moduleIdToUrl: new Map(),
urlToModuleId: new Map(),
rootDirectories: new Array(),
// Used in package:build_runner/src/server/build_updates_client/hot_reload_client.dart
moduleParentsGraph: new Map(),
moduleLoadingErrorCallbacks: new Map(),
forceLoadModule: function (moduleName, callback, onError) {
// dartdevc only strips the final extension when adding modules to source
// maps, so we need to do the same.
if (moduleName.endsWith('$_modulePartialExtension')) {
moduleName = moduleName.substring(0, moduleName.length - ${_modulePartialExtension.length});
}
if (typeof onError != 'undefined') {
var errorCallbacks = \$dartLoader.moduleLoadingErrorCallbacks;
if (!errorCallbacks.has(moduleName)) {
errorCallbacks.set(moduleName, new Set());
}
errorCallbacks.get(moduleName).add(onError);
}
requirejs.undef(moduleName);
requirejs([moduleName], function() {
if (typeof onError != 'undefined') {
errorCallbacks.get(moduleName).delete(onError);
}
if (typeof callback != 'undefined') {
callback();
}
});
},
getModuleLibraries: null, // set up by _initializeTools
};
}
let customModulePaths = {};
window.\$dartLoader.rootDirectories.push(window.location.origin + baseUrl);
for (let moduleName of Object.getOwnPropertyNames(modulePaths)) {
let modulePath = modulePaths[moduleName];
if (modulePath != moduleName) {
customModulePaths[moduleName] = modulePath;
}
var src = window.location.origin + '/' + modulePath + '.js';
if (window.\$dartLoader.moduleIdToUrl.has(moduleName)) {
continue;
}
\$dartLoader.moduleIdToUrl.set(moduleName, src);
\$dartLoader.urlToModuleId.set(src, moduleName);
}
''';
/// Code to initialize the dev tools formatter, stack trace mapper, and any
/// other tools.
///
/// Posts a message to the window when done.
final _initializeTools = '''
$_baseUrlScript
dart_sdk._debugger.registerDevtoolsFormatter();
\$dartLoader.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) {
url = url.replace(baseUrl, '/');
var module = window.\$dartLoader.urlToModuleId.get(url);
if (!module) return null;
return dart.getSourceMap(module);
});
}
if (typeof document != 'undefined') {
window.postMessage({ type: "DDC_STATE_CHANGE", state: "start" }, "*");
}
''';
/// Require JS config for ddc.
///
/// Sets the base url to `/` so that all modules can be loaded using absolute
/// paths which simplifies a lot of scenarios.
///
/// Sets the timeout for loading modules to infinity (0).
///
/// Sets up the custom module paths.
///
/// Adds error handler code for require.js which requests a `.errors` file for
/// any failed module, and logs it to the console.
final _requireJsConfig = '''
// Whenever we fail to load a JS module, try to request the corresponding
// `.errors` file, and log it to the console.
(function() {
var oldOnError = requirejs.onError;
requirejs.onError = function(e) {
if (e.requireModules) {
if (e.message) {
// If error occurred on loading dependencies, we need to invalidate ancessor too.
var ancesor = e.message.match(/needed by: (.*)/);
if (ancesor) {
e.requireModules.push(ancesor[1]);
}
}
for (const module of e.requireModules) {
var errorCallbacks = \$dartLoader.moduleLoadingErrorCallbacks.get(module);
if (errorCallbacks) {
for (const callback of errorCallbacks) callback(e);
errorCallbacks.clear();
}
}
}
if (e.originalError && e.originalError.srcElement) {
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (this.readyState == 4) {
var message;
if (this.status == 200) {
message = this.responseText;
} else {
message = "Unknown error loading " + e.originalError.srcElement.src;
}
console.error(message);
var errorEvent = new CustomEvent(
'dartLoadException', { detail: message });
window.dispatchEvent(errorEvent);
}
};
xhr.open("GET", e.originalError.srcElement.src + ".errors", true);
xhr.send();
}
// Also handle errors the normal way.
if (oldOnError) oldOnError(e);
};
}());
$_baseUrlScript;
require.config({
baseUrl: baseUrl,
waitSeconds: 0,
paths: customModulePaths
});
const modulesGraph = new Map();
function getRegisteredModuleName(moduleMap) {
if (\$dartLoader.moduleIdToUrl.has(moduleMap.name + '$_modulePartialExtension')) {
return moduleMap.name + '$_modulePartialExtension';
}
return moduleMap.name;
}
requirejs.onResourceLoad = function (context, map, depArray) {
const name = getRegisteredModuleName(map);
const depNameArray = depArray.map(getRegisteredModuleName);
if (modulesGraph.has(name)) {
// TODO Move this logic to better place
var previousDeps = modulesGraph.get(name);
var changed = previousDeps.length != depNameArray.length;
changed = changed || depNameArray.some(function(depName) {
return !previousDeps.includes(depName);
});
if (changed) {
console.warn("Dependencies graph change for module '" + name + "' detected. " +
"Dependencies was [" + previousDeps + "], now [" + depNameArray.map((depName) => depName) +"]. " +
"Page can't be hot-reloaded, firing full page reload.");
window.location.reload();
}
} else {
modulesGraph.set(name, []);
for (const depName of depNameArray) {
if (!\$dartLoader.moduleParentsGraph.has(depName)) {
\$dartLoader.moduleParentsGraph.set(depName, []);
}
\$dartLoader.moduleParentsGraph.get(depName).push(name);
modulesGraph.get(name).push(depName);
}
}
};
''';
/// Marker comment used by tools to identify the entrypoint file,
/// to inject custom code.
final _entrypointExtensionMarker = '/* ENTRYPOINT_EXTENTION_MARKER */';
/// Marker comment used by tools to identify the main function
/// to inject custom code.
final _mainExtensionMarker = '/* MAIN_EXTENSION_MARKER */';
final _baseUrlScript = '''
var baseUrl = (function () {
// Attempt to detect --precompiled mode for tests, and set the base url
// appropriately, otherwise set it to '/'.
var pathParts = location.pathname.split("/");
if (pathParts[0] == "") {
pathParts.shift();
}
if (pathParts.length > 1 && pathParts[1] == "test") {
return "/" + pathParts.slice(0, 2).join("/") + "/";
}
// Attempt to detect base url using <base href> html tag
// base href should start and end with "/"
if (typeof document !== 'undefined') {
var el = document.getElementsByTagName('base');
if (el && el[0] && el[0].getAttribute("href") && el[0].getAttribute
("href").startsWith("/") && el[0].getAttribute("href").endsWith("/")){
return el[0].getAttribute("href");
}
}
// return default value
return "/";
}());
''';
/// Ensures that all of [resources] are built successfully, and adds them as
/// an input dependency to this action.
///
/// This also has the effect of ensuring these resources are present whenever
/// a DDC app is built - reducing the need to explicitly list these files as
/// build filters.
Future<void> _ensureResources(
BuildStep buildStep, Iterable<AssetId> resources) async {
for (var resource in resources) {
if (!await buildStep.canRead(resource)) {
throw StateError('Unable to locate required sdk resource $resource');
}
}
}