blob: cd594d43afb69964971539cc7d12974753707dd4 [file] [log] [blame]
// Copyright (c) 2015, 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 'package:html/dom.dart';
import 'package:html/parser.dart' show parseFragment;
import 'package:logging/logging.dart' show Logger;
import 'package:path/path.dart' as path;
import '../compiler.dart' show AbstractCompiler;
import '../server/dependency_graph.dart';
import '../utils.dart' show colorOf, resourceOutputPath;
/// Emits an entry point HTML file corresponding to [inputFile] that can load
/// the code generated by the dev compiler.
///
/// This internally transforms the given HTML [document]. When compiling to
/// JavaScript, we remove any Dart script tags, add new script tags to load our
/// runtime and the compiled code, and to execute the main method of the
/// application. When compiling to Dart, we ensure that the document contains a
/// single Dart script tag, but otherwise emit the original document
/// unmodified.
String generateEntryHtml(HtmlSourceNode root, AbstractCompiler compiler) {
var options = compiler.options;
var document = root.document.clone(true);
var scripts = document.querySelectorAll('script[type="application/dart"]');
if (scripts.isEmpty) {
_log.warning('No <script type="application/dart"> found in ${root.uri}');
// TODO(jacobr): we would rather return document.outerHtml to avoid future
// bugs that would only show up with html files include references to Dart
// scripts. Passing through the input content is needed to avoid breaking
// Angular ecause the spec-compliant HTML parser used modifies the HTML
// in a way that breaks Angular templates. An alternate fix would be to
// write an HTML emitter that preserves the structure of the input HTML as
// much as possible.
return root.contents;
}
scripts.skip(1).forEach((s) {
// TODO(sigmund): allow more than one Dart script tags?
_log.warning(s.sourceSpan.message(
'unexpected script. Only one Dart script tag allowed '
'(see https://github.com/dart-lang/dart-dev-compiler/issues/53).',
color: options.useColors ? colorOf('warning') : false));
s.remove();
});
var libraries = [];
var resources = new Set();
visitInPostOrder(root, (n) {
if (n is DartSourceNode) libraries.add(n);
if (n is ResourceSourceNode) resources.add(n);
}, includeParts: false);
root.htmlResourceNodes.forEach((element, resource) {
// Make sure we don't try and add this node again.
resources.remove(resource);
var resourcePath =
resourceOutputPath(resource.uri, root.uri, options.runtimeDir);
if (resource.cachingHash != null) {
resourcePath = _addHash(resourcePath, resource.cachingHash);
}
var attrs = element.attributes;
if (attrs.containsKey('href')) {
attrs['href'] = resourcePath;
} else if (attrs.containsKey('src')) {
attrs['src'] = resourcePath;
}
});
var rootDir = path.dirname(root.uri.path);
String rootRelative(String fullPath) {
return path.relative(path.join(compiler.inputBaseDir, fullPath),
from: rootDir);
}
var fragment = new DocumentFragment();
for (var resource in resources) {
var resourcePath = rootRelative(
resourceOutputPath(resource.uri, root.uri, options.runtimeDir));
var ext = path.extension(resourcePath);
if (resource.cachingHash != null) {
resourcePath = _addHash(resourcePath, resource.cachingHash);
}
if (ext == '.js') {
fragment.nodes.add(libraryInclude(resourcePath));
} else if (ext == '.css') {
var stylesheetLink = '<link rel="stylesheet" href="$resourcePath">\n';
fragment.nodes.add(parseFragment(stylesheetLink));
}
}
String mainLibraryName;
var src = scripts[0].attributes["src"];
var scriptUri = root.source.resolveRelativeUri(Uri.parse(src));
for (var lib in libraries) {
var info = lib.info;
if (info == null) continue;
var uri = info.library.source.uri;
var jsPath = rootRelative(compiler.getModulePath(uri));
if (uri == scriptUri) mainLibraryName = compiler.getModuleName(uri);
if (lib.cachingHash != null) {
jsPath = _addHash(jsPath, lib.cachingHash);
}
fragment.nodes.add(libraryInclude(jsPath));
}
fragment.nodes.add(invokeMain(mainLibraryName));
scripts[0].replaceWith(fragment);
return '${document.outerHtml}\n';
}
// TODO(jmesserly): the string interpolation in these could lead to injection
// bugs. Not really a security issue since input is trusted, but the resulting
// parse tree may not match expectations if interpolated strings contain quotes.
/// A script tag that loads the .js code for a compiled library.
Node libraryInclude(String jsUrl) =>
parseFragment('<script src="$jsUrl"></script>\n');
/// A script tag that invokes the main function on the entry point library.
Node invokeMain(String mainLibraryName) {
var code = mainLibraryName == null
? 'console.error("dev_compiler error: main was not generated");'
// TODO(vsm): Can we simplify this?
// See: https://github.com/dart-lang/dev_compiler/issues/164
: "dart_library.start('$mainLibraryName');";
return parseFragment('<script>$code</script>\n');
}
/// Convert the outputPath to include the hash in it. This function is the
/// reverse of what the server does to determine whether a request needs to have
/// cache headers added to it.
_addHash(String outPath, String hash) {
// (the ____ prefix makes it look better in the web inspector)
return '$outPath?____cached=$hash';
}
final _log = new Logger('dev_compiler.src.codegen.html_codegen');