Newer
Older
// 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: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;
Jacob MacDonald
committed
import '../dart.dart';
import '../io.dart';
Jacob MacDonald
committed
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(
Jacob MacDonald
committed
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;
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.
Jacob MacDonald
committed
var modulePaths = {appModulePath: appModulePath, 'dart_sdk': 'dart_sdk'};
var transitiveDeps = await moduleReader.readTransitiveDeps(module);
for (var dep in transitiveDeps) {
if (dep.dir != 'lib') {
Jacob MacDonald
committed
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;
Jacob MacDonald
committed
} 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');
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
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');
}
Jacob MacDonald
committed
// 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();
}
Jacob MacDonald
committed
// Also handle errors the normal way.
if (oldOnError) oldOnError(e);
};
}());
require.config({
});
require(["$appModulePath", "dart_sdk"], function(app, dart_sdk) {
dart_sdk._isolate_helper.startRootIsolate(() => {}, []);
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;
''');
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()));
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,
Jacob MacDonald
committed
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,
]);
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()) {
Jacob MacDonald
committed
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()));
var sourceMapFile = scratchSpace.fileFor(module.id.jsSourceMapId);
outputCompleters[module.id.jsSourceMapId].complete(new Asset.fromBytes(
module.id.jsSourceMapId, sourceMapFile.readAsBytesSync()));
}
}
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
}
/// 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;
}
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
/// 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;
}