blob: 417d1e68efb716bc57140edcd057b12e54401a7d [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:convert' show JsonEncoder;
import 'dart:io';
import 'package:analyzer/analyzer.dart';
import 'package:barback/barback.dart';
import 'package:bazel_worker/bazel_worker.dart';
import 'package:cli_util/cli_util.dart' as cli_util;
import 'package:path/path.dart' as p;
import '../dart.dart';
import '../io.dart';
import 'errors.dart';
import 'module_reader.dart';
import 'scratch_space.dart';
import 'summaries.dart';
import 'workers.dart';
/// 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;
})();
''';
/// Returns whether or not [dartId] is an app entrypoint (basically, whether or
/// not it has a `main` function).
Future<bool> isAppEntryPoint(
AssetId dartId, Future<Asset> getAsset(AssetId id)) async {
assert(dartId.extension == '.dart');
var dartAsset = await getAsset(dartId);
// Skip reporting errors here, dartdevc will report them later with nicer
// formatting.
var parsed = parseCompilationUnit(await dartAsset.readAsString(),
suppressErrors: true);
return isEntrypoint(parsed);
}
/// Bootstraps the JS module for the entrypoint dart file [dartEntrypointId]
/// with two additional JS files:
///
/// * A `$dartEntrypointId.js` file which is the main entrypoint for the app. It
/// injects a script tag whose src is `require.js` and whose `data-main`
/// attribute points at a `$dartEntrypointId.bootstrap.js` file.
/// * A `$dartEntrypointId.bootstrap.js` file which invokes the top level `main`
/// function from the entrypoint module, after performing some necessary SDK
/// setup.
///
/// In debug mode an empty sourcemap will be output for the entrypoint JS file
/// to satisfy the test package runner (there is no original dart file to map it
/// back to though).
///
/// Synchronously returns a `Map<AssetId, Future<Asset>>` so that you can know
/// immediately what assets will be output.
Map<AssetId, Future<Asset>> bootstrapDartDevcEntrypoint(
AssetId dartEntrypointId, BarbackMode mode, ModuleReader moduleReader) {
var bootstrapId = dartEntrypointId.addExtension('.bootstrap.js');
var jsEntrypointId = dartEntrypointId.addExtension('.js');
var jsMapEntrypointId = jsEntrypointId.addExtension('.map');
var outputCompleters = <AssetId, Completer<Asset>>{
bootstrapId: new Completer(),
jsEntrypointId: new Completer(),
};
var isDebug = mode == BarbackMode.DEBUG;
if (isDebug) {
outputCompleters[jsMapEntrypointId] = new Completer<Asset>();
}
return _ensureComplete(outputCompleters, () async {
var module = await moduleReader.moduleFor(dartEntrypointId);
// The path to the entrypoint JS module as it should appear in the call to
// `require` in the bootstrap file.
var moduleDir = topLevelDir(dartEntrypointId.path);
var appModulePath = p.url.relative(p.url.join(moduleDir, module.id.name),
from: p.url.dirname(dartEntrypointId.path));
// 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 appModuleScope = p.url
.split(p.url.withoutExtension(
p.url.relative(dartEntrypointId.path, from: moduleDir)))
.join("__")
.replaceAll('.', '\$46');
// Map from module name to module path.
// Modules outside of the `packages` directory have different module path
// and module names.
var modulePaths = {appModulePath: appModulePath, 'dart_sdk': 'dart_sdk'};
var transitiveDeps = await moduleReader.readTransitiveDeps(module);
for (var dep in transitiveDeps) {
if (dep.dir != 'lib') {
var relativePath = p.url.relative(p.url.join(moduleDir, dep.name),
from: p.url.dirname(bootstrapId.path));
var jsModuleName = '${dep.dir}/${dep.name}';
modulePaths[jsModuleName] = relativePath;
} else {
var jsModuleName = 'packages/${dep.package}/${dep.name}';
var actualModulePath = p.url.relative(
p.url.join(moduleDir, jsModuleName),
from: p.url.dirname(bootstrapId.path));
modulePaths[jsModuleName] = actualModulePath;
}
}
var bootstrapContent = new StringBuffer('(function() {\n');
if (isDebug) {
bootstrapContent.write('''
$_currentDirectoryScript
let modulePaths = ${const JsonEncoder.withIndent(" ").convert(modulePaths)};
if(!window.\$dartLoader) {
window.\$dartLoader = {
moduleIdToUrl: new Map(),
urlToModuleId: new Map(),
rootDirectories: new Set(),
};
}
let customModulePaths = {};
window.\$dartLoader.rootDirectories.add(_currentDirectory);
for (let moduleName of Object.getOwnPropertyNames(modulePaths)) {
let modulePath = modulePaths[moduleName];
if (modulePath != moduleName) {
customModulePaths[moduleName] = modulePath;
}
var src = _currentDirectory + modulePath + '.js';
if (window.\$dartLoader.moduleIdToUrl.has(moduleName)) {
continue;
}
\$dartLoader.moduleIdToUrl.set(moduleName, src);
\$dartLoader.urlToModuleId.set(src, moduleName);
}
''');
} else {
var customModulePaths = <String, String>{};
modulePaths.forEach((name, path) {
if (name != path) customModulePaths[name] = path;
});
var json = const JsonEncoder.withIndent(" ").convert(customModulePaths);
bootstrapContent.write('let customModulePaths = ${json};\n');
}
bootstrapContent.write('''
// 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.originalError && e.originalError.srcElement) {
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
console.error(this.responseText);
}
};
xhr.open("GET", e.originalError.srcElement.src + ".errors", true);
xhr.send();
}
// Also handle errors the normal way.
if (oldOnError) oldOnError(e);
};
}());
require.config({
waitSeconds: 30,
paths: customModulePaths
});
require(["$appModulePath", "dart_sdk"], function(app, dart_sdk) {
dart_sdk._isolate_helper.startRootIsolate(() => {}, []);
''');
if (isDebug) {
bootstrapContent.write('''
dart_sdk._debugger.registerDevtoolsFormatter();
if (window.\$dartStackTraceUtility && !window.\$dartStackTraceUtility.ready) {
window.\$dartStackTraceUtility.ready = true;
let dart = dart_sdk.dart;
window.\$dartStackTraceUtility.setSourceMapProvider(
function(url) {
var module = window.\$dartLoader.urlToModuleId.get(url);
if (!module) return null;
return dart.getSourceMap(module);
});
}
window.postMessage({ type: "DDC_STATE_CHANGE", state: "start" }, "*");
''');
}
bootstrapContent.write('''
app.$appModuleScope.main();
});
})();
''');
outputCompleters[bootstrapId].complete(
new Asset.fromString(bootstrapId, bootstrapContent.toString()));
var bootstrapModuleName = p.withoutExtension(
p.relative(bootstrapId.path, from: p.dirname(dartEntrypointId.path)));
var entrypointJsContent = new StringBuffer('''
var el;
''');
if (isDebug) {
entrypointJsContent.write('''
el = document.createElement("script");
el.defer = true;
el.async = false;
el.src = "dart_stack_trace_mapper.js";
document.head.appendChild(el);
''');
}
entrypointJsContent.write('''
el = document.createElement("script");
el.defer = true;
el.async = false;
el.src = "require.js";
el.setAttribute("data-main", "$bootstrapModuleName");
document.head.appendChild(el);
''');
outputCompleters[jsEntrypointId].complete(
new Asset.fromString(jsEntrypointId, entrypointJsContent.toString()));
if (isDebug) {
outputCompleters[jsMapEntrypointId].complete(new Asset.fromString(
jsMapEntrypointId,
'{"version":3,"sourceRoot":"","sources":[],"names":[],"mappings":"",'
'"file":""}'));
}
});
}
/// Compiles [module] using the `dartdevc` binary from the SDK to a relative
/// path under the package that looks like `$outputDir/${module.id.name}.js`.
///
/// Synchronously returns a `Map<AssetId, Future<Asset>>` so that you can know
/// immediately what assets will be output.
Map<AssetId, Future<Asset>> createDartdevcModule(
AssetId id,
ModuleReader moduleReader,
ScratchSpace scratchSpace,
Map<String, String> environmentConstants,
BarbackMode mode) {
assert(id.extension == '.js');
var outputCompleters = <AssetId, Completer<Asset>>{
id: new Completer(),
};
var isDebug = mode == BarbackMode.DEBUG;
if (isDebug) {
outputCompleters[id.addExtension('.map')] = new Completer();
}
return _ensureComplete(outputCompleters, () async {
var module = await moduleReader.moduleFor(id);
var transitiveModuleDeps = await moduleReader.readTransitiveDeps(module);
var linkedSummaryIds =
transitiveModuleDeps.map((depId) => depId.linkedSummaryId).toSet();
var allAssetIds = new Set<AssetId>()
..addAll(module.assetIds)
..addAll(linkedSummaryIds);
await scratchSpace.ensureAssets(allAssetIds);
var jsOutputFile = scratchSpace.fileFor(module.id.jsId);
var sdk_summary = p.url.join(sdkDir.path, 'lib/_internal/ddc_sdk.sum');
var request = new WorkRequest();
request.arguments.addAll([
'--dart-sdk-summary=$sdk_summary',
'--modules=amd',
'--dart-sdk=${sdkDir.path}',
'--module-root=${scratchSpace.tempDir.path}',
'--library-root=${p.dirname(jsOutputFile.path)}',
'--summary-extension=${linkedSummaryExtension.substring(1)}',
'--no-summarize',
'-o',
jsOutputFile.path,
]);
if (isDebug) {
request.arguments.addAll([
'--source-map',
'--source-map-comment',
'--inline-source-map',
]);
} else {
request.arguments.add('--no-source-map');
}
// Add environment constants.
environmentConstants.forEach((key, value) {
request.arguments.add('-D$key=$value');
});
// Add all the linked summaries as summary inputs.
for (var id in linkedSummaryIds) {
request.arguments.addAll(['-s', scratchSpace.fileFor(id).path]);
}
// Add URL mappings for all the package: files to tell DartDevc where to
// find them.
for (var id in module.assetIds) {
var uri = canonicalUriFor(id);
if (uri.startsWith('package:')) {
request.arguments
.add('--url-mapping=$uri,${scratchSpace.fileFor(id).path}');
}
}
// And finally add all the urls to compile, using the package: path for
// files under lib and the full absolute path for other files.
request.arguments.addAll(module.assetIds.map((id) {
var uri = canonicalUriFor(id);
if (uri.startsWith('package:')) {
return uri;
}
return scratchSpace.fileFor(id).path;
}));
var response = await dartdevcDriver.doWork(request);
// TODO(jakemac53): Fix the ddc worker mode so it always sends back a bad
// status code if something failed. Today we just make sure there is an output
// JS file to verify it was successful.
if (response.exitCode != EXIT_CODE_OK || !jsOutputFile.existsSync()) {
outputCompleters[module.id.jsId].completeError(
new DartDevcCompilationException(module.id.jsId, response.output));
} else {
outputCompleters[module.id.jsId].complete(
new Asset.fromBytes(module.id.jsId, jsOutputFile.readAsBytesSync()));
if (isDebug) {
var sourceMapFile = scratchSpace.fileFor(module.id.jsSourceMapId);
outputCompleters[module.id.jsSourceMapId].complete(new Asset.fromBytes(
module.id.jsSourceMapId, sourceMapFile.readAsBytesSync()));
}
}
});
}
/// Copies the `dart_sdk.js` and `require.js` AMD files from the SDK into
/// [outputDir].
///
/// Returns a `Map<AssetId, Asset>` of the created assets.
Map<AssetId, Asset> copyDartDevcResources(String package, String outputDir) {
var sdk = cli_util.getSdkDir();
var outputs = <AssetId, Asset>{};
// Copy the dart_sdk.js file for AMD into the output folder.
var sdkJsOutputId =
new AssetId(package, p.url.join(outputDir, 'dart_sdk.js'));
var sdkAmdJsPath = p.url.join(sdk.path, 'lib/dev_compiler/amd/dart_sdk.js');
outputs[sdkJsOutputId] =
new Asset.fromFile(sdkJsOutputId, new File(sdkAmdJsPath));
// Copy the require.js file for AMD into the output folder.
var requireJsPath = p.url.join(sdk.path, 'lib/dev_compiler/amd/require.js');
var requireJsOutputId =
new AssetId(package, p.url.join(outputDir, 'require.js'));
outputs[requireJsOutputId] =
new Asset.fromFile(requireJsOutputId, new File(requireJsPath));
return outputs;
}
/// Runs [fn], and then ensures that all [completers] are completed.
///
/// If an error is caught, then all [completers] that weren't completed are
/// completed with that error and stack trace.
///
/// If no error was caught then all [completers] that weren't completed are
/// completed with an [AssetNotFoundException].
///
/// Synchronously returns a `Map<AssetId, Future<Asset>` which is derived from
/// the original [completers].
Map<AssetId, Future<Asset>> _ensureComplete(
Map<AssetId, Completer<Asset>> completers, Future fn()) {
var futures = <AssetId, Future<Asset>>{};
completers.forEach((k, v) => futures[k] = v.future);
() async {
try {
await fn();
} catch (e, s) {
for (var completer in completers.values) {
if (!completer.isCompleted) completer.completeError(e, s);
}
} finally {
for (var id in completers.keys) {
if (!completers[id].isCompleted) {
completers[id].completeError(new AssetNotFoundException(id));
}
}
}
}();
return futures;
}