blob: 570a7c5df92cddf228003fc5ea95baeba567223a [file] [log] [blame]
// Copyright (c) 2016, 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' show json;
import 'dart:io' show File;
import 'package:analyzer/analyzer.dart'
show AnalysisError, CompilationUnit, StaticWarningCode;
import 'package:analyzer/dart/element/element.dart'
show LibraryElement, UriReferencedElement;
import 'package:analyzer/src/generated/engine.dart' show AnalysisContext;
import 'package:args/args.dart' show ArgParser, ArgResults;
import 'package:path/path.dart' as path;
import 'package:source_maps/source_maps.dart';
import '../compiler/js_names.dart' as JS;
import '../compiler/module_builder.dart'
show transformModuleFormat, ModuleFormat;
import '../compiler/shared_command.dart';
import '../js_ast/js_ast.dart' as JS;
import '../js_ast/js_ast.dart' show js;
import '../js_ast/source_map_printer.dart' show SourceMapPrintingContext;
import 'code_generator.dart' show CodeGenerator;
import 'context.dart';
import 'driver.dart';
import 'error_helpers.dart';
/// Compiles a set of Dart files into a single JavaScript module.
///
/// For a single build unit, this will produce a [JSModuleFile].
///
/// A build unit is a collection of Dart sources that is sufficient to be
/// compiled together. This can be as small as a single Dart library file, but
/// if the library has parts, or if the library has cyclic dependencies on other
/// libraries, those must be included as well. A common build unit is the lib
/// directory of a Dart package.
///
/// This class exists to cache global state associated with a single in-memory
/// [AnalysisContext], such as information about extension types in the Dart
/// SDK. It can be used once to produce a single module, or reused to save
/// warm-up time. (Currently there is no warm up, but there may be in the
/// future.)
///
/// The SDK source code is assumed to be immutable for the life of this class.
///
/// For all other files, it is up to the analysis context to decide whether or
/// not any caching is performed. By default an analysis context will assume
/// sources are immutable for the life of the context, and cache information
/// about them.
JSModuleFile compileWithAnalyzer(
CompilerAnalysisDriver compilerDriver,
List<String> sourcePaths,
AnalyzerOptions analyzerOptions,
CompilerOptions options) {
var trees = <CompilationUnit>[];
var errors = <AnalysisError>[];
var explicitSources = <Uri>[];
var compilingSdk = false;
for (var sourcePath in sourcePaths) {
var sourceUri = sourcePathToUri(sourcePath);
if (sourceUri.scheme == "dart") {
compilingSdk = true;
}
explicitSources.add(sourceUri);
}
var driver = compilerDriver.linkLibraries(explicitSources, analyzerOptions);
for (var libraryUri in driver.libraryUris) {
var library = driver.getLibrary(libraryUri);
// TODO(jmesserly): remove "dart:mirrors" from DDC's SDK, and then remove
// this special case error message.
if (!compilingSdk && !options.emitMetadata) {
var node = _getDartMirrorsImport(library);
if (node != null) {
errors.add(AnalysisError(library.source, node.uriOffset, node.uriEnd,
invalidImportDartMirrors));
}
}
var analysisResults = driver.analyzeLibrary(libraryUri);
for (var result in analysisResults.values) {
errors.addAll(_filterJsErrors(libraryUri, result.errors));
trees.add(result.unit);
}
}
var context = driver.context;
bool anyFatalErrors() {
return errors.any((e) => isFatalError(context, e, options.replCompile));
}
JS.Program jsProgram;
if (options.unsafeForceCompile || !anyFatalErrors()) {
var codeGenerator = CodeGenerator(
driver,
driver.context.typeProvider,
compilerDriver.summaryData,
options,
compilerDriver.extensionTypes,
errors);
try {
jsProgram = codeGenerator.compile(trees);
} catch (e) {
// If force compilation failed, suppress the exception and report the
// static errors instead. Otherwise, rethrow an internal compiler error.
if (!anyFatalErrors()) rethrow;
}
if (!options.unsafeForceCompile && anyFatalErrors()) {
jsProgram = null;
}
}
var jsModule = JSModuleFile(
formatErrors(context, errors), options, jsProgram, driver.summaryBytes);
driver.dispose();
return jsModule;
}
Iterable<AnalysisError> _filterJsErrors(
String libraryUriStr, Iterable<AnalysisError> errors) {
if (libraryUriStr == 'dart:html' ||
libraryUriStr == 'dart:svg' ||
libraryUriStr == 'dart:_interceptors') {
return errors.where((error) {
return error.errorCode !=
StaticWarningCode.FINAL_NOT_INITIALIZED_CONSTRUCTOR_1 &&
error.errorCode !=
StaticWarningCode.FINAL_NOT_INITIALIZED_CONSTRUCTOR_2 &&
error.errorCode !=
StaticWarningCode.FINAL_NOT_INITIALIZED_CONSTRUCTOR_3_PLUS;
});
}
return errors;
}
UriReferencedElement _getDartMirrorsImport(LibraryElement library) {
return library.imports.firstWhere(_isDartMirrorsImort, orElse: () => null) ??
library.exports.firstWhere(_isDartMirrorsImort, orElse: () => null);
}
bool _isDartMirrorsImort(UriReferencedElement import) {
return import.uri == 'dart:mirrors';
}
class CompilerOptions extends SharedCompilerOptions {
/// If [sourceMap] is emitted, this will emit a `sourceMappingUrl` comment
/// into the output JavaScript module.
final bool sourceMapComment;
/// Whether to emit the source mapping file inline as a data url.
final bool inlineSourceMap;
/// The file extension for summaries.
final String summaryExtension;
/// Whether to force compilation of code with static errors.
final bool unsafeForceCompile;
/// If specified, the path to write the summary file.
/// Used when building the SDK.
final String summaryOutPath;
/// *deprecated* If specified, this is used to initialize the import paths for
/// [summaryModules].
final String moduleRoot;
/// *deprecated* If specified, `dartdevc` will synthesize library names that
/// are relative to this path for all libraries in the JS module.
final String libraryRoot;
CompilerOptions(
{bool sourceMap = true,
this.sourceMapComment = true,
this.inlineSourceMap = false,
bool summarizeApi = true,
this.summaryExtension = 'sum',
this.unsafeForceCompile = false,
bool replCompile = false,
bool emitMetadata = false,
bool enableAsserts = true,
Map<String, String> bazelMapping = const {},
this.summaryOutPath,
Map<String, String> summaryModules = const {},
this.moduleRoot,
this.libraryRoot})
: super(
sourceMap: sourceMap,
summarizeApi: summarizeApi,
emitMetadata: emitMetadata,
enableAsserts: enableAsserts,
replCompile: replCompile,
bazelMapping: bazelMapping,
summaryModules: summaryModules);
CompilerOptions.fromArguments(ArgResults args)
: sourceMapComment = args['source-map-comment'] as bool,
inlineSourceMap = args['inline-source-map'] as bool,
summaryExtension = args['summary-extension'] as String,
unsafeForceCompile = args['unsafe-force-compile'] as bool,
summaryOutPath = args['summary-out'] as String,
moduleRoot = args['module-root'] as String,
libraryRoot = _getLibraryRoot(args),
super.fromArguments(args, args['module-root'] as String,
args['summary-extension'] as String);
static void addArguments(ArgParser parser, {bool hide = true}) {
SharedCompilerOptions.addArguments(parser, hide: hide);
parser
..addOption('summary-extension',
help: 'file extension for Dart summary files',
defaultsTo: 'sum',
hide: hide)
..addFlag('source-map-comment',
help: 'adds a sourceMappingURL comment to the end of the JS,\n'
'disable if using X-SourceMap header',
defaultsTo: true,
hide: hide)
..addFlag('inline-source-map',
help: 'emit source mapping inline', defaultsTo: false, hide: hide)
..addFlag('unsafe-force-compile',
help: 'Compile code even if it has errors. ಠ_ಠ\n'
'This has undefined behavior!',
hide: hide)
..addOption('summary-out',
help: 'location to write the summary file', hide: hide)
..addOption('module-root',
help: '(deprecated) used to determine the default module name and\n'
'summary import name if those are not provided.',
hide: hide)
..addOption('library-root',
help: '(deprecated) used to name libraries inside the module.');
}
static String _getLibraryRoot(ArgResults args) {
var root = args['library-root'] as String;
return root != null ? path.absolute(root) : path.current;
}
}
/// The output of Dart->JS compilation.
///
/// This contains the file contents of the JS module, as well as a list of
/// Dart libraries that are contained in this module.
class JSModuleFile {
/// The list of messages (errors and warnings)
final List<String> errors;
/// The AST that will be used to generate the [code] and [sourceMap] for this
/// module.
final JS.Program moduleTree;
/// The compiler options used to generate this module.
final CompilerOptions options;
/// The binary contents of the API summary file, including APIs from each of
/// the libraries in this module.
final List<int> summaryBytes;
/// Unique identifier indicating hole to inline the source map.
///
/// We cannot generate the source map before the script it is for is
/// generated so we have generate the script including this id and then
/// replace the ID once the source map is generated.
static String sourceMapHoleID = 'SourceMap3G5a8h6JVhHfdGuDxZr1EF9GQC8y0e6u';
JSModuleFile(this.errors, this.options, this.moduleTree, this.summaryBytes);
JSModuleFile.invalid(this.errors, this.options)
: moduleTree = null,
summaryBytes = null;
/// The name of this module.
String get name => options.moduleName;
/// True if this library was successfully compiled.
bool get isValid => moduleTree != null;
/// Gets the source code and source map for this JS module, given the
/// locations where the JS file and map file will be served from.
///
/// Relative URLs will be used to point from the .js file to the .map file
//
// TODO(jmesserly): this should match our old logic, but I'm not sure we are
// correctly handling the pointer from the .js file to the .map file.
JSModuleCode getCode(ModuleFormat format, String jsUrl, String mapUrl) {
var opts = JS.JavaScriptPrintingOptions(
allowKeywordsInProperties: true, allowSingleLineIfStatements: true);
JS.SimpleJavaScriptPrintingContext printer;
SourceMapBuilder sourceMap;
if (options.sourceMap) {
var sourceMapContext = SourceMapPrintingContext();
sourceMap = sourceMapContext.sourceMap;
printer = sourceMapContext;
} else {
printer = JS.SimpleJavaScriptPrintingContext();
}
var tree = transformModuleFormat(format, moduleTree);
tree.accept(JS.Printer(opts, printer, localNamer: JS.TemporaryNamer(tree)));
Map builtMap;
if (options.sourceMap && sourceMap != null) {
builtMap = placeSourceMap(
sourceMap.build(jsUrl), mapUrl, options.bazelMapping, null);
if (options.sourceMapComment) {
var jsDir = path.dirname(path.fromUri(jsUrl));
var relative = path.relative(path.fromUri(mapUrl), from: jsDir);
var relativeMapUrl = path.toUri(relative).toString();
assert(path.dirname(jsUrl) == path.dirname(mapUrl));
printer.emit('\n//# sourceMappingURL=');
printer.emit(relativeMapUrl);
printer.emit('\n');
}
}
var text = printer.getText();
var rawSourceMap = options.inlineSourceMap
? js.escapedString(json.encode(builtMap), "'").value
: 'null';
text = text.replaceFirst(sourceMapHoleID, rawSourceMap);
return JSModuleCode(text, builtMap);
}
/// Similar to [getCode] but immediately writes the resulting files.
///
/// If [mapPath] is not supplied but [options.sourceMap] is set, mapPath
/// will default to [jsPath].map.
void writeCodeSync(ModuleFormat format, String jsPath) {
String mapPath = jsPath + '.map';
var code = getCode(
format, path.toUri(jsPath).toString(), path.toUri(mapPath).toString());
var c = code.code;
if (format == ModuleFormat.amdConcat ||
format == ModuleFormat.legacyConcat) {
// In single-out-file mode we wrap each module in an eval statement to
// leverage sourceURL to improve the debugging experience when source maps
// are not enabled.
//
// Note: We replace all `/` with `.` so that we don't break relative urls
// to sources in the original sourcemap. The name of this file is bogus
// anyways, so it has very little effect on things.
c += '\n//# sourceURL=${name.replaceAll("/", ".")}.js\n';
c = 'eval(${json.encode(c)});\n';
}
var file = File(jsPath);
if (!file.parent.existsSync()) file.parent.createSync(recursive: true);
file.writeAsStringSync(c);
// TODO(jacobr): it is a bit strange we are writing the source map to a file
// even when options.inlineSourceMap is true. To be consistent perhaps we
// should also write a copy of the source file without a sourcemap even when
// inlineSourceMap is true.
if (code.sourceMap != null) {
file = File(mapPath);
if (!file.parent.existsSync()) file.parent.createSync(recursive: true);
file.writeAsStringSync(json.encode(code.sourceMap));
}
}
}
/// The output of compiling a JavaScript module in a particular format.
class JSModuleCode {
/// The JavaScript code for this module.
///
/// If a [sourceMap] is available, this will include the `sourceMappingURL`
/// comment at end of the file.
final String code;
/// The JSON of the source map, if generated, otherwise `null`.
///
/// The source paths will initially be absolute paths. They can be adjusted
/// using [placeSourceMap].
final Map sourceMap;
JSModuleCode(this.code, this.sourceMap);
}