blob: 2dec254f83c763f5c5080f708330f92bb4efae11 [file] [log] [blame]
// Copyright (c) 2013, 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.
/// A library for compiling Dart code and manipulating analyzer parse trees.
import 'dart:async';
import 'dart:io';
import 'dart:isolate';
import 'package:analyzer/analyzer.dart';
import 'package:barback/barback.dart';
import 'package:compiler_unsupported/compiler.dart' as compiler;
import 'package:compiler_unsupported/src/filenames.dart' show appendSlash;
import 'package:path/path.dart' as p;
import 'exceptions.dart';
import 'io.dart';
import 'log.dart' as log;
/// Interface to communicate with dart2js.
///
/// This is basically an amalgamation of dart2js's
/// [compiler.CompilerInputProvider], [compiler.CompilerOutputProvider], and
/// [compiler.DiagnosticHandler] function types so that we can provide them
/// as a single unit.
abstract class CompilerProvider {
/// The URI to the root directory where "dart:" libraries can be found.
///
/// This is used as the base URL to generate library URLs that are then sent
/// back to [provideInput].
Uri get libraryRoot;
/// Given [uri], responds with a future that completes to the contents of
/// the input file at that URI.
///
/// The future can complete to a string or a list of bytes.
Future provideInput(Uri uri);
/// Reports a diagnostic message from dart2js to the user.
void handleDiagnostic(
Uri uri, int begin, int end, String message, compiler.Diagnostic kind);
/// Given a [name] (which will be "" for the entrypoint) and a file extension,
/// returns an [EventSink] that dart2js can write to to emit an output file.
EventSink<String> provideOutput(String name, String extension);
}
/// Compiles [entrypoint] to JavaScript (or to Dart if [toDart] is true) as
/// well as any ancillary outputs dart2js creates.
///
/// Uses [provider] to communcate between dart2js and the caller. Returns a
/// future that completes when compilation is done.
///
/// By default, the package root is assumed to be adjacent to [entrypoint], but
/// if [packageRoot] is passed that will be used instead.
Future compile(String entrypoint, CompilerProvider provider,
{Iterable<String> commandLineOptions,
bool checked: false,
bool csp: false,
bool minify: true,
bool verbose: false,
Map<String, String> environment,
String packageRoot,
bool analyzeAll: false,
bool preserveUris: false,
bool suppressWarnings: false,
bool suppressHints: false,
bool suppressPackageWarnings: true,
bool terse: false,
bool includeSourceMapUrls: false,
bool toDart: false,
String platformBinaries}) async {
// dart2js chokes on relative paths. Including "/./" can also confuse it, so
// we normalize as well.
entrypoint = p.normalize(p.absolute(entrypoint));
var options = <String>['--categories=Client,Server'];
if (checked) options.add('--enable-checked-mode');
if (csp) options.add('--csp');
if (minify) options.add('--minify');
if (verbose) options.add('--verbose');
if (analyzeAll) options.add('--analyze-all');
if (preserveUris) options.add('--preserve-uris');
if (suppressWarnings) options.add('--suppress-warnings');
if (suppressHints) options.add('--suppress-hints');
if (!suppressPackageWarnings) options.add('--show-package-warnings');
if (terse) options.add('--terse');
if (toDart) options.add('--output-type=dart');
if (platformBinaries != null) {
options.add('--platform-binaries=$platformBinaries');
}
var sourceUrl = p.toUri(entrypoint);
options.add("--out=$sourceUrl.js");
// Add the source map URLs.
if (includeSourceMapUrls) {
options.add("--source-map=$sourceUrl.js.map");
}
if (environment == null) environment = {};
if (commandLineOptions != null) options.addAll(commandLineOptions);
if (packageRoot == null) {
packageRoot = p.join(p.dirname(entrypoint), 'packages');
} else {
packageRoot = p.normalize(p.absolute(packageRoot));
}
await compiler.compile(
p.toUri(entrypoint),
provider.libraryRoot,
p.toUri(appendSlash(packageRoot)),
provider.provideInput,
provider.handleDiagnostic,
options,
provider.provideOutput,
environment);
}
/// Returns whether [dart] looks like an entrypoint file.
bool isEntrypoint(CompilationUnit dart) {
// Allow two or fewer arguments so that entrypoints intended for use with
// [spawnUri] get counted.
//
// TODO(nweiz): this misses the case where a Dart file doesn't contain main(),
// but it parts in another file that does.
return dart.declarations.any((node) {
return node is FunctionDeclaration &&
node.name.name == "main" &&
node.functionExpression.parameters.parameters.length <= 2;
});
}
/// Returns whether [dart] contains a [PartOfDirective].
bool isPart(CompilationUnit dart) =>
dart.directives.any((directive) => directive is PartOfDirective);
/// Efficiently parses the import and export directives in [contents].
///
/// If [name] is passed, it's used as the filename for error reporting.
List<UriBasedDirective> parseImportsAndExports(String contents, {String name}) {
var collector = new _DirectiveCollector();
parseDirectives(contents, name: name).accept(collector);
return collector.directives;
}
/// A simple visitor that collects import and export nodes.
class _DirectiveCollector extends GeneralizingAstVisitor {
final directives = <UriBasedDirective>[];
visitUriBasedDirective(UriBasedDirective node) => directives.add(node);
}
/// Runs [code] in an isolate.
///
/// [code] should be the contents of a Dart entrypoint. It may contain imports;
/// they will be resolved in the same context as the host isolate. [message] is
/// passed to the [main] method of the code being run; the caller is responsible
/// for using this to establish communication with the isolate.
///
/// [packageRoot] controls the package root of the isolate. It may be either a
/// [String] or a [Uri].
///
/// If [snapshot] is passed, the isolate will be loaded from that path if it
/// exists. Otherwise, a snapshot of the isolate's code will be saved to that
/// path once the isolate is loaded.
Future runInIsolate(String code, message,
{packageRoot, String snapshot}) async {
if (snapshot != null && fileExists(snapshot)) {
log.fine("Spawning isolate from $snapshot.");
if (packageRoot != null) packageRoot = Uri.parse(packageRoot.toString());
try {
// Make the snapshot URI absolute to work around sdk#8440.
await Isolate.spawnUri(p.toUri(p.absolute(snapshot)), [], message,
packageRoot: packageRoot);
return;
} on IsolateSpawnException catch (error) {
log.fine("Couldn't load existing snapshot $snapshot:\n$error");
// Do nothing, we will regenerate the snapshot below.
}
}
await withTempDir((dir) async {
var dartPath = p.join(dir, 'runInIsolate.dart');
writeTextFile(dartPath, code, dontLogContents: true);
await Isolate.spawnUri(p.toUri(p.absolute(dartPath)), [], message,
packageRoot: packageRoot);
if (snapshot == null) return;
ensureDir(p.dirname(snapshot));
var snapshotArgs = <String>[];
if (packageRoot != null) snapshotArgs.add('--package-root=$packageRoot');
snapshotArgs.addAll(['--snapshot=$snapshot', dartPath]);
var result = await runProcess(Platform.executable, snapshotArgs);
if (result.success) return;
// Don't emit a fatal error here, since we don't want to crash the
// otherwise successful isolate load.
log.warning("Failed to compile a snapshot to "
"${p.relative(snapshot)}:\n" +
result.stderr.join("\n"));
});
}
/// Snapshots the Dart executable at [executableUrl] to a snapshot at
/// [snapshotPath].
///
/// If [packagesFile] is passed, it's used to resolve `package:` URIs in the
/// executable. Otherwise, a `packages/` directory or a package spec is inferred
/// from the executable's location.
///
/// If [id] is passed, it's used to describe the executable in logs and error
/// messages.
Future snapshot(Uri executableUrl, String snapshotPath,
{Uri packagesFile, AssetId id}) async {
var name = log.bold(id == null
? executableUrl.toString()
: "${id.package}:${p.url.basenameWithoutExtension(id.path)}");
var args = ['--snapshot=$snapshotPath', executableUrl.toString()];
if (packagesFile != null) args.insert(0, "--packages=$packagesFile");
var result = await runProcess(Platform.executable, args);
if (result.success) {
log.message("Precompiled $name.");
} else {
throw new ApplicationException(
log.yellow("Failed to precompile $name:\n") + result.stderr.join('\n'));
}
}